diff --git a/src/main/java/com/imyeyu/api/modules/common/entity/Attachment.java b/src/main/java/com/imyeyu/api/modules/common/entity/Attachment.java index 79489ed..64f8561 100644 --- a/src/main/java/com/imyeyu/api/modules/common/entity/Attachment.java +++ b/src/main/java/com/imyeyu/api/modules/common/entity/Attachment.java @@ -38,12 +38,16 @@ public class Attachment extends UUIDEntity { GAO_CUSTOMER, + GAO_PRODUCT, + GAO_EVENT_RECORD, - GAO_PACKAGE_DELIVERY, - GAO_FUND_RECORD, + GAO_STOCK_RECORD, + + GAO_ORDER_FULFILLMENT, + TEMP_FILE, MIRROR diff --git a/src/main/java/com/imyeyu/api/modules/common/service/ArticleService.java b/src/main/java/com/imyeyu/api/modules/common/service/ArticleService.java index 8590a20..270deb6 100644 --- a/src/main/java/com/imyeyu/api/modules/common/service/ArticleService.java +++ b/src/main/java/com/imyeyu/api/modules/common/service/ArticleService.java @@ -3,6 +3,8 @@ package com.imyeyu.api.modules.common.service; import com.imyeyu.api.modules.common.entity.Article; import com.imyeyu.spring.service.BaseService; +import java.util.List; + /** * 文章服务 * @@ -10,4 +12,10 @@ import com.imyeyu.spring.service.BaseService; * @since 2021-02-23 21:33 */ public interface ArticleService extends BaseService { + + /// 按示例查询文章 + /// + /// @param example 查询示例 + /// @return 文章列表 + List
listByExample(Article example); } diff --git a/src/main/java/com/imyeyu/api/modules/common/service/implement/ArticleServiceImplement.java b/src/main/java/com/imyeyu/api/modules/common/service/implement/ArticleServiceImplement.java index 9f8b58a..945b7a1 100644 --- a/src/main/java/com/imyeyu/api/modules/common/service/implement/ArticleServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/common/service/implement/ArticleServiceImplement.java @@ -16,6 +16,8 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import java.util.List; + /** * 文章服务实现 * @@ -37,6 +39,11 @@ public class ArticleServiceImplement extends AbstractEntityService listByExample(Article example) { + return mapper.selectAllByExample(example); + } + @Override public void create(Article article) { Logger logger = new Logger(Logger.Module.COMMON, "COMMON_ARTICLE_CREATE"); diff --git a/src/main/java/com/imyeyu/api/modules/common/service/implement/TempFileServiceImplement.java b/src/main/java/com/imyeyu/api/modules/common/service/implement/TempFileServiceImplement.java index d068d59..bc9f403 100644 --- a/src/main/java/com/imyeyu/api/modules/common/service/implement/TempFileServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/common/service/implement/TempFileServiceImplement.java @@ -71,6 +71,7 @@ public class TempFileServiceImplement implements TempFileService { page.setSize(Long.MAX_VALUE); { Attachment example = new Attachment(); + example.setBizType(BIZ_TYPE); example.setUploaderIp(TimiSpring.getRequestIP()); page.setEqualsExample(example); } diff --git a/src/main/java/com/imyeyu/api/modules/gao/bean/GaoPermissionCode.java b/src/main/java/com/imyeyu/api/modules/gao/bean/GaoPermissionCode.java index 0609c98..d269d80 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/bean/GaoPermissionCode.java +++ b/src/main/java/com/imyeyu/api/modules/gao/bean/GaoPermissionCode.java @@ -59,6 +59,38 @@ public enum GaoPermissionCode implements BuiltinPermissionCode { EVENT_RECORD_DELETE("EVENT_RECORD:DELETE", "登记事件记录删除", false, false), + PRODUCT_CATEGORY_CREATE("PRODUCT_CATEGORY:CREATE", "产品分类创建", true, true), + + PRODUCT_CATEGORY_READ("PRODUCT_CATEGORY:READ", "产品分类读取", true, true), + + PRODUCT_CATEGORY_UPDATE("PRODUCT_CATEGORY:UPDATE", "产品分类修改", true, true), + + PRODUCT_CREATE("PRODUCT:CREATE", "产品创建", true, true), + + PRODUCT_READ("PRODUCT:READ", "产品读取", true, true), + + PRODUCT_UPDATE("PRODUCT:UPDATE", "产品修改", true, true), + + PRODUCT_STOCK_READ("PRODUCT_STOCK:READ", "产品库存读取", true, true), + + PRODUCT_STOCK_CREATE("PRODUCT_STOCK:CREATE", "产品库存操作", true, true), + + SALES_ORDER_CREATE("SALES_ORDER:CREATE", "销售订单创建", true, true), + + SALES_ORDER_READ("SALES_ORDER:READ", "销售订单读取", true, true), + + SALES_ORDER_CANCEL("SALES_ORDER:CANCEL", "销售订单取消", true, true), + + SALES_ORDER_PRICE_OVERRIDE("SALES_ORDER:PRICE_OVERRIDE", "销售订单价格修改", true, true), + + ORDER_FULFILLMENT_CREATE("ORDER_FULFILLMENT:CREATE", "统一履约创建", true, true), + + ORDER_FULFILLMENT_READ("ORDER_FULFILLMENT:READ", "统一履约读取", true, true), + + ORDER_FULFILLMENT_VERIFY("ORDER_FULFILLMENT:VERIFY", "统一履约核销", true, true), + + ORDER_FULFILLMENT_REVOKE("ORDER_FULFILLMENT:REVOKE", "统一履约冲正", true, true), + PACKAGE_CONFIG_CREATE("PACKAGE_CONFIG:CREATE", "套餐配置创建", true, true), PACKAGE_CONFIG_READ("PACKAGE_CONFIG:READ", "套餐配置读取", true, true), diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoCustomerPackageController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoCustomerPackageController.java index 455ae40..1243017 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoCustomerPackageController.java +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoCustomerPackageController.java @@ -7,23 +7,14 @@ import com.imyeyu.api.modules.gao.bean.GaoPermissionCode; import com.imyeyu.api.modules.gao.entity.GaoCustomer; import com.imyeyu.api.modules.gao.entity.GaoCustomerPackage; import com.imyeyu.api.modules.gao.entity.GaoCustomerPackageItem; -import com.imyeyu.api.modules.gao.entity.GaoEventRecord; -import com.imyeyu.api.modules.gao.entity.GaoFundRecord; -import com.imyeyu.api.modules.gao.entity.GaoPackageDelivery; -import com.imyeyu.api.modules.gao.entity.GaoPackageDeliveryItem; +import com.imyeyu.api.modules.gao.entity.GaoOrderFulfillment; import com.imyeyu.api.modules.gao.service.GaoCustomerPackageService; import com.imyeyu.api.modules.gao.service.GaoCustomerService; -import com.imyeyu.api.modules.gao.service.GaoEventRecordService; -import com.imyeyu.api.modules.gao.service.GaoFundRecordService; -import com.imyeyu.api.modules.gao.service.GaoPackageDeliveryService; +import com.imyeyu.api.modules.gao.service.GaoOrderFulfillmentService; import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; import com.imyeyu.api.modules.gao.vo.GaoCustomerPackageCancelRequest; import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePage; import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePurchaseRequest; -import com.imyeyu.api.modules.gao.vo.GaoFundActionRequest; -import com.imyeyu.api.modules.gao.vo.GaoFundConfirmNoRefundRequest; -import com.imyeyu.api.modules.gao.vo.GaoFundReceiveRequest; -import com.imyeyu.api.modules.gao.vo.GaoFundRefundRequest; import com.imyeyu.api.modules.user.entity.User; import com.imyeyu.api.modules.user.service.UserService; import com.imyeyu.java.bean.timi.TimiException; @@ -40,9 +31,8 @@ import org.springframework.web.bind.annotation.RestController; import java.math.BigDecimal; import java.util.List; import java.util.Map; -import java.util.stream.Stream; -/// GAO 客户套餐和收退款接口 +/// GAO 客户套餐接口 /// /// @author Codex /// @since 2026-08-31 @@ -53,9 +43,7 @@ public class GaoCustomerPackageController { private final GaoCustomerPackageService service; private final GaoCustomerService customerService; - private final GaoEventRecordService eventRecordService; - private final GaoPackageDeliveryService deliveryService; - private final GaoFundRecordService fundRecordService; + private final GaoOrderFulfillmentService fulfillmentService; private final AttachmentService attachmentService; private final GaoStoreScopeService storeScopeService; private final UserService userService; @@ -102,15 +90,7 @@ public class GaoCustomerPackageController { customer.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.GAO_CUSTOMER, customer.getId())); } result.setCustomer(customer); - GaoEventRecord purchaseRecord = eventRecordService.get(result.getPurchaseEventRecordId()); - if (purchaseRecord != null) { - purchaseRecord.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.GAO_EVENT_RECORD, purchaseRecord.getId())); - } - result.setPurchaseEventRecord(purchaseRecord); - result.setDeliveryList(deliveryService.listByCustomerPackageId(result.getId())); - fillDeliveryTransient(result.getDeliveryList()); - result.setFundRecordList(fundRecordService.listByCustomerPackageId(result.getId())); - fillFundTransient(result.getFundRecordList()); + result.setFulfillmentList(fulfillmentService.listBySalesOrderId(result.getSalesOrderId())); return result; } @@ -134,80 +114,13 @@ public class GaoCustomerPackageController { return result; } - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.FUND_RECEIVE_CREATE) - @PostMapping("/payment/receive") - public GaoCustomerPackage receive(@RequestBody GaoFundReceiveRequest request) { - fundRecordService.receive(request); - return packageResponse(request.getCustomerPackageId()); - } - - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.FUND_RECEIVE_VERIFY) - @PostMapping("/payment/verify") - public GaoCustomerPackage verifyPayment(@RequestParam String id) { - fundRecordService.verify(id); - return packageAfterFundAction(id); - } - - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.FUND_RECEIVE_VERIFY) - @PostMapping("/payment/reject") - public GaoCustomerPackage rejectPayment(@RequestBody GaoFundActionRequest request) { - fundRecordService.reject(request); - return packageAfterFundAction(request.getFundRecordId()); - } - - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.FUND_RECEIVE_CREATE) - @PostMapping("/payment/cancel") - public GaoCustomerPackage cancelPayment(@RequestBody GaoFundActionRequest request) { - fundRecordService.cancel(request); - return packageAfterFundAction(request.getFundRecordId()); - } - - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.FUND_REFUND) - @PostMapping("/refund") - public GaoCustomerPackage refund(@RequestBody GaoFundRefundRequest request) { - fundRecordService.refund(request); - return packageResponse(request.getCustomerPackageId()); - } - - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.FUND_REFUND) - @PostMapping("/refund/confirm-none") - public GaoCustomerPackage confirmNoRefund(@RequestBody GaoFundConfirmNoRefundRequest request) { - fundRecordService.confirmNoRefund(request); - return packageResponse(request.getCustomerPackageId()); - } - - private GaoCustomerPackage packageAfterFundAction(String fundRecordId) { - var record = fundRecordService.get(fundRecordId); - TimiException.required(record, "not found fund record"); - return packageResponse(record.getBizId()); - } - - private GaoCustomerPackage packageResponse(String packageId) { - GaoCustomerPackage result = service.get(packageId); - TimiException.required(result, "not found customer package"); - fillTransient(List.of(result)); - return result; - } - private void fillTransient(List packageList) { if (packageList == null || packageList.isEmpty()) { return; } Map> itemMap = service.mapItemListByCustomerPackageIdList(packageList.stream().map(GaoCustomerPackage::getId).toList()); List userIdList = packageList.stream() - .flatMap(packageEntity -> Stream.of(packageEntity.getOperatorUserId(), packageEntity.getCancelOperatorUserId())) + .map(GaoCustomerPackage::getCancelOperatorUserId) .filter(value -> value != null && !value.isBlank()) .distinct() .toList(); @@ -218,57 +131,16 @@ public class GaoCustomerPackageController { fillDerived(item); } packageEntity.setItemList(itemList); - packageEntity.setOperator(userMap.get(packageEntity.getOperatorUserId())); packageEntity.setCancelOperator(userMap.get(packageEntity.getCancelOperatorUserId())); } } - private void fillDeliveryTransient(List deliveryList) { - if (deliveryList == null || deliveryList.isEmpty()) { - return; - } - List idList = deliveryList.stream().map(GaoPackageDelivery::getId).toList(); - Map> itemMap = deliveryService.mapItemListByDeliveryIdList(idList); - Map> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_PACKAGE_DELIVERY, idList); - List userIdList = deliveryList.stream() - .flatMap(delivery -> Stream.of(delivery.getOperatorUserId(), delivery.getVerifierUserId())) - .filter(value -> value != null && !value.isBlank()) - .distinct() - .toList(); - Map userMap = userIdList.isEmpty() ? Map.of() : userService.mapByIdList(userIdList); - for (GaoPackageDelivery delivery : deliveryList) { - delivery.setItemList(itemMap.getOrDefault(delivery.getId(), List.of())); - delivery.setAttachmentList(attachmentMap.getOrDefault(delivery.getId(), List.of())); - delivery.setOperator(userMap.get(delivery.getOperatorUserId())); - delivery.setVerifier(userMap.get(delivery.getVerifierUserId())); - } - } - - private void fillFundTransient(List recordList) { - if (recordList == null || recordList.isEmpty()) { - return; - } - List idList = recordList.stream().map(GaoFundRecord::getId).toList(); - Map> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_FUND_RECORD, idList); - List userIdList = recordList.stream() - .flatMap(record -> Stream.of(record.getOperatorUserId(), record.getVerifierUserId())) - .filter(value -> value != null && !value.isBlank()) - .distinct() - .toList(); - Map userMap = userIdList.isEmpty() ? Map.of() : userService.mapByIdList(userIdList); - for (GaoFundRecord record : recordList) { - record.setAttachmentList(attachmentMap.getOrDefault(record.getId(), List.of())); - record.setOperator(userMap.get(record.getOperatorUserId())); - record.setVerifier(userMap.get(record.getVerifierUserId())); - } - } - private void fillDerived(GaoCustomerPackageItem item) { BigDecimal reserved = item.getReservedQuantity() == null ? BigDecimal.ZERO : item.getReservedQuantity(); - BigDecimal delivered = item.getDeliveredQuantity() == null ? BigDecimal.ZERO : item.getDeliveredQuantity(); + BigDecimal fulfilled = item.getFulfilledQuantity() == null ? BigDecimal.ZERO : item.getFulfilledQuantity(); BigDecimal total = item.getTotalQuantity() == null ? BigDecimal.ZERO : item.getTotalQuantity(); item.setPendingQuantity(reserved); - item.setAvailableQuantity(total.subtract(reserved).subtract(delivered)); - item.setRemainingQuantity(total.subtract(delivered)); + item.setAvailableQuantity(total.subtract(reserved).subtract(fulfilled)); + item.setRemainingQuantity(total.subtract(fulfilled)); } } diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoEventController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoEventController.java index 1cd87f1..4b306fe 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoEventController.java +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoEventController.java @@ -127,6 +127,7 @@ public class GaoEventController { @PostMapping("/create") public void create(@RequestBody GaoEvent event) { event.setStoreId(storeScopeService.resolveStoreId(event.getStoreId())); + validatePackageConfigBinding(event); service.create(event); } @@ -141,9 +142,21 @@ public class GaoEventController { TimiException.required(dbEvent, "not found event"); storeScopeService.checkStoreId(dbEvent.getStoreId()); event.setStoreId(dbEvent.getStoreId()); + validatePackageConfigBinding(event); service.update(event); } + private void validatePackageConfigBinding(GaoEvent event) { + if (event.getBizType() == GaoEvent.BizType.PACKAGE_PURCHASE) { + TimiException.required(event.getPackageConfigId(), "购买套餐事件必须绑定套餐配置"); + GaoPackageConfig packageConfig = packageConfigService.get(event.getPackageConfigId()); + TimiException.required(packageConfig, "购买套餐事件绑定的套餐配置不存在"); + TimiException.requiredTrue(event.getStoreId().equals(packageConfig.getStoreId()), "购买事件和套餐配置不属于同一门店"); + } else { + TimiException.requiredNull(event.getPackageConfigId(), "普通事件不能绑定套餐配置"); + } + } + @AOPLog @RequestRateLimit @RequireGaoPermission(GaoPermissionCode.EVENT_UPDATE) diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoFundRecordController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoFundRecordController.java index 11686dc..033259b 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoFundRecordController.java +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoFundRecordController.java @@ -6,9 +6,11 @@ import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission; import com.imyeyu.api.modules.gao.bean.GaoPermissionCode; import com.imyeyu.api.modules.gao.entity.GaoFundRecord; import com.imyeyu.api.modules.gao.service.GaoFundRecordService; +import com.imyeyu.api.modules.gao.service.GaoOrderPaymentService; import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; import com.imyeyu.api.modules.gao.vo.GaoFundRecordPage; import com.imyeyu.api.modules.gao.vo.GaoFundRevokeRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentRevokeRequest; import com.imyeyu.api.modules.user.entity.User; import com.imyeyu.api.modules.user.service.UserService; import com.imyeyu.java.bean.timi.TimiException; @@ -36,6 +38,7 @@ import java.util.stream.Stream; public class GaoFundRecordController { private final GaoFundRecordService service; + private final GaoOrderPaymentService orderPaymentService; private final GaoStoreScopeService storeScopeService; private final AttachmentService attachmentService; private final UserService userService; @@ -69,7 +72,14 @@ public class GaoFundRecordController { @RequireGaoPermission(GaoPermissionCode.FUND_REVOKE) @PostMapping("/revoke") public GaoFundRecord revoke(@RequestBody GaoFundRevokeRequest request) { - GaoFundRecord result = service.revoke(request); + GaoOrderPaymentRevokeRequest orderRequest = new GaoOrderPaymentRevokeRequest(); + orderRequest.setRequestNo(request.getRequestNo()); + orderRequest.setFundRecordId(request.getFundRecordId()); + orderRequest.setOccurredAt(request.getOccurredAt()); + orderRequest.setReason(request.getReason()); + orderRequest.setAttachmentList(request.getAttachmentList()); + orderRequest.setRemark(request.getRemark()); + GaoFundRecord result = orderPaymentService.revoke(orderRequest); fillTransient(List.of(result)); return result; } diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoOrderFulfillmentController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoOrderFulfillmentController.java new file mode 100644 index 0000000..5be9243 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoOrderFulfillmentController.java @@ -0,0 +1,146 @@ +package com.imyeyu.api.modules.gao.controller; + +import com.imyeyu.api.modules.common.entity.Attachment; +import com.imyeyu.api.modules.common.service.AttachmentService; +import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission; +import com.imyeyu.api.modules.gao.bean.GaoPermissionCode; +import com.imyeyu.api.modules.gao.entity.GaoOrderFulfillment; +import com.imyeyu.api.modules.gao.service.GaoOrderFulfillmentService; +import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; +import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentActionRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentCreateRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentPage; +import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentPrepareStockRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentRevokeRequest; +import com.imyeyu.api.modules.user.entity.User; +import com.imyeyu.api.modules.user.service.UserService; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.spring.annotation.AOPLog; +import com.imyeyu.spring.annotation.RequestRateLimit; +import com.imyeyu.spring.bean.PageResult; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +/// GAO 统一履约接口 +/// +/// @author Codex +/// @since 2026-09-02 +@RestController +@RequiredArgsConstructor +@RequestMapping("/gao/order/fulfillment") +public class GaoOrderFulfillmentController { + + private final GaoOrderFulfillmentService service; + private final GaoStoreScopeService storeScopeService; + private final AttachmentService attachmentService; + private final UserService userService; + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.ORDER_FULFILLMENT_READ) + @PostMapping("/list") + public PageResult list(@RequestBody GaoOrderFulfillmentPage page) { + TimiException.required(page, "not found fulfillment page"); + page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId())); + PageResult result = service.pageByQuery(page); + fillTransient(result.getList()); + return result; + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.ORDER_FULFILLMENT_READ) + @PostMapping("/detail") + public GaoOrderFulfillment detail(@RequestParam String id) { + GaoOrderFulfillment result = service.detail(id); + fillTransient(List.of(result)); + return result; + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.ORDER_FULFILLMENT_CREATE) + @PostMapping("/create") + public GaoOrderFulfillment create(@RequestBody GaoOrderFulfillmentCreateRequest request) { + GaoOrderFulfillment result = service.create(request); + fillTransient(List.of(result)); + return result; + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.ORDER_FULFILLMENT_VERIFY) + @PostMapping("/prepare-stock") + public GaoOrderFulfillment prepareStock(@RequestBody GaoOrderFulfillmentPrepareStockRequest request) { + GaoOrderFulfillment result = service.prepareStock(request); + fillTransient(List.of(result)); + return result; + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.ORDER_FULFILLMENT_VERIFY) + @PostMapping("/verify") + public GaoOrderFulfillment verify(@RequestBody GaoOrderFulfillmentActionRequest request) { + GaoOrderFulfillment result = service.verify(request); + fillTransient(List.of(result)); + return result; + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.ORDER_FULFILLMENT_VERIFY) + @PostMapping("/reject") + public GaoOrderFulfillment reject(@RequestBody GaoOrderFulfillmentActionRequest request) { + GaoOrderFulfillment result = service.reject(request); + fillTransient(List.of(result)); + return result; + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.ORDER_FULFILLMENT_CREATE) + @PostMapping("/cancel") + public GaoOrderFulfillment cancel(@RequestBody GaoOrderFulfillmentActionRequest request) { + GaoOrderFulfillment result = service.cancel(request); + fillTransient(List.of(result)); + return result; + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.ORDER_FULFILLMENT_REVOKE) + @PostMapping("/revoke") + public GaoOrderFulfillment revoke(@RequestBody GaoOrderFulfillmentRevokeRequest request) { + GaoOrderFulfillment result = service.revoke(request); + fillTransient(List.of(result)); + return result; + } + + private void fillTransient(List fulfillmentList) { + if (fulfillmentList == null || fulfillmentList.isEmpty()) { + return; + } + List idList = fulfillmentList.stream().map(GaoOrderFulfillment::getId).toList(); + Map> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_ORDER_FULFILLMENT, idList); + List userIdList = fulfillmentList.stream() + .flatMap(item -> Stream.of(item.getOperatorUserId(), item.getVerifierUserId())) + .filter(value -> value != null && !value.isBlank()) + .distinct() + .toList(); + Map userMap = userIdList.isEmpty() ? Map.of() : userService.mapByIdList(userIdList); + for (GaoOrderFulfillment fulfillment : fulfillmentList) { + fulfillment.setAttachmentList(attachmentMap.getOrDefault(fulfillment.getId(), List.of())); + fulfillment.setOperator(userMap.get(fulfillment.getOperatorUserId())); + fulfillment.setVerifier(userMap.get(fulfillment.getVerifierUserId())); + } + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoPackageConfigController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoPackageConfigController.java index a638a19..af4cea9 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoPackageConfigController.java +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoPackageConfigController.java @@ -4,6 +4,7 @@ import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission; import com.imyeyu.api.modules.gao.bean.GaoPermissionCode; import com.imyeyu.api.modules.gao.entity.GaoPackageConfig; import com.imyeyu.api.modules.gao.entity.GaoPackageConfigItem; +import com.imyeyu.api.modules.gao.service.GaoEventService; import com.imyeyu.api.modules.gao.service.GaoPackageConfigService; import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; import com.imyeyu.api.modules.gao.vo.GaoPackageConfigPage; @@ -33,6 +34,7 @@ import java.util.Map; public class GaoPackageConfigController { private final GaoPackageConfigService service; + private final GaoEventService eventService; private final GaoStoreScopeService storeScopeService; private final UserService userService; @@ -116,6 +118,7 @@ public class GaoPackageConfigController { GaoPackageConfig config = service.get(id); TimiException.required(config, "not found package config"); storeScopeService.checkStoreId(config.getStoreId()); + TimiException.requiredTrue(0 == eventService.countByPackageConfigId(id), "套餐配置已绑定购买事件,不能删除"); service.delete(id); } } diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoPackageDeliveryController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoPackageDeliveryController.java deleted file mode 100644 index 716e04b..0000000 --- a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoPackageDeliveryController.java +++ /dev/null @@ -1,140 +0,0 @@ -package com.imyeyu.api.modules.gao.controller; - -import com.imyeyu.api.modules.common.entity.Attachment; -import com.imyeyu.api.modules.common.service.AttachmentService; -import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission; -import com.imyeyu.api.modules.gao.bean.GaoPermissionCode; -import com.imyeyu.api.modules.gao.entity.GaoPackageDelivery; -import com.imyeyu.api.modules.gao.entity.GaoPackageDeliveryItem; -import com.imyeyu.api.modules.gao.service.GaoPackageDeliveryService; -import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; -import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryActionRequest; -import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryCreateRequest; -import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryPage; -import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryRevokeRequest; -import com.imyeyu.api.modules.user.entity.User; -import com.imyeyu.api.modules.user.service.UserService; -import com.imyeyu.java.bean.timi.TimiException; -import com.imyeyu.spring.annotation.AOPLog; -import com.imyeyu.spring.annotation.RequestRateLimit; -import com.imyeyu.spring.bean.PageResult; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -import java.util.List; -import java.util.Map; -import java.util.stream.Stream; - -/// GAO 套餐发放接口 -/// -/// @author Codex -/// @since 2026-08-31 -@RestController -@RequiredArgsConstructor -@RequestMapping("/gao/package/delivery") -public class GaoPackageDeliveryController { - - private final GaoPackageDeliveryService service; - private final GaoStoreScopeService storeScopeService; - private final AttachmentService attachmentService; - private final UserService userService; - - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.PACKAGE_DELIVERY_READ) - @PostMapping("/list") - public PageResult list(@RequestBody GaoPackageDeliveryPage page) { - TimiException.required(page, "not found page"); - page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId())); - PageResult result = service.pageByQuery(page); - fillTransient(result.getList()); - return result; - } - - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.PACKAGE_DELIVERY_READ) - @PostMapping("/detail") - public GaoPackageDelivery detail(@RequestParam String id) { - GaoPackageDelivery result = service.get(id); - TimiException.required(result, "not found delivery"); - storeScopeService.checkStoreId(result.getStoreId()); - fillTransient(List.of(result)); - return result; - } - - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.PACKAGE_DELIVERY_CREATE) - @PostMapping("/create") - public GaoPackageDelivery create(@RequestBody GaoPackageDeliveryCreateRequest request) { - GaoPackageDelivery result = service.createDelivery(request); - fillTransient(List.of(result)); - return result; - } - - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.PACKAGE_DELIVERY_VERIFY) - @PostMapping("/verify") - public GaoPackageDelivery verify(@RequestParam String id) { - GaoPackageDelivery result = service.verify(id); - fillTransient(List.of(result)); - return result; - } - - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.PACKAGE_DELIVERY_VERIFY) - @PostMapping("/reject") - public GaoPackageDelivery reject(@RequestBody GaoPackageDeliveryActionRequest request) { - GaoPackageDelivery result = service.reject(request); - fillTransient(List.of(result)); - return result; - } - - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.PACKAGE_DELIVERY_CREATE) - @PostMapping("/cancel") - public GaoPackageDelivery cancel(@RequestBody GaoPackageDeliveryActionRequest request) { - GaoPackageDelivery result = service.cancel(request); - fillTransient(List.of(result)); - return result; - } - - @AOPLog - @RequestRateLimit - @RequireGaoPermission(GaoPermissionCode.PACKAGE_DELIVERY_REVOKE) - @PostMapping("/revoke") - public GaoPackageDelivery revoke(@RequestBody GaoPackageDeliveryRevokeRequest request) { - GaoPackageDelivery result = service.revoke(request); - fillTransient(List.of(result)); - return result; - } - - private void fillTransient(List deliveryList) { - if (deliveryList == null || deliveryList.isEmpty()) { - return; - } - List idList = deliveryList.stream().map(GaoPackageDelivery::getId).toList(); - Map> itemMap = service.mapItemListByDeliveryIdList(idList); - Map> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_PACKAGE_DELIVERY, idList); - List userIdList = deliveryList.stream() - .flatMap(delivery -> Stream.of(delivery.getOperatorUserId(), delivery.getVerifierUserId())) - .filter(value -> value != null && !value.isBlank()) - .distinct() - .toList(); - Map userMap = userIdList.isEmpty() ? Map.of() : userService.mapByIdList(userIdList); - for (GaoPackageDelivery delivery : deliveryList) { - delivery.setItemList(itemMap.getOrDefault(delivery.getId(), List.of())); - delivery.setAttachmentList(attachmentMap.getOrDefault(delivery.getId(), List.of())); - delivery.setOperator(userMap.get(delivery.getOperatorUserId())); - delivery.setVerifier(userMap.get(delivery.getVerifierUserId())); - } - } -} diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoProductController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoProductController.java new file mode 100644 index 0000000..df2ee06 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoProductController.java @@ -0,0 +1,154 @@ +package com.imyeyu.api.modules.gao.controller; + +import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission; +import com.imyeyu.api.modules.gao.bean.GaoPermissionCode; +import com.imyeyu.api.modules.gao.entity.GaoProduct; +import com.imyeyu.api.modules.gao.entity.GaoProductCategory; +import com.imyeyu.api.modules.gao.service.GaoProductCategoryService; +import com.imyeyu.api.modules.gao.service.GaoProductService; +import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; +import com.imyeyu.api.modules.gao.vo.GaoProductCategoryPage; +import com.imyeyu.api.modules.gao.vo.GaoProductPage; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.spring.annotation.AOPLog; +import com.imyeyu.spring.annotation.RequestRateLimit; +import com.imyeyu.spring.bean.PageResult; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/// GAO 产品库接口 +/// +/// @author Codex +/// @since 2026-09-02 +@RestController +@RequiredArgsConstructor +@RequestMapping("/gao/product") +public class GaoProductController { + + private final GaoProductCategoryService categoryService; + private final GaoProductService productService; + private final GaoStoreScopeService storeScopeService; + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_CATEGORY_READ) + @PostMapping("/category/list") + public PageResult categoryList(@RequestBody GaoProductCategoryPage page) { + TimiException.required(page, "not found category page"); + page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId())); + return categoryService.pageByQuery(page); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_CATEGORY_CREATE) + @PostMapping("/category/create") + public String categoryCreate(@RequestBody GaoProductCategory category) { + TimiException.required(category, "not found category"); + category.setStoreId(storeScopeService.resolveStoreId(category.getStoreId())); + categoryService.create(category); + return category.getId(); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_CATEGORY_UPDATE) + @PostMapping("/category/update") + public void categoryUpdate(@RequestBody GaoProductCategory request) { + TimiException.required(request, "not found category"); + TimiException.required(request.getId(), "not found category.id"); + GaoProductCategory category = categoryService.get(request.getId()); + TimiException.required(category, "not found category"); + storeScopeService.checkStoreId(category.getStoreId()); + request.setStoreId(category.getStoreId()); + categoryService.update(request); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_CATEGORY_UPDATE) + @PostMapping("/category/sort") + public void categorySort(@RequestBody List idList) { + categoryService.sort(idList); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_CATEGORY_UPDATE) + @PostMapping("/category/disable") + public void categoryDisable(@RequestParam String id) { + categoryService.disable(id); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_READ) + @PostMapping("/list") + public PageResult list(@RequestBody GaoProductPage page) { + TimiException.required(page, "not found product page"); + page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId())); + return productService.pageByQuery(page); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_READ) + @PostMapping("/detail") + public GaoProduct detail(@RequestParam String id) { + GaoProduct result = productService.detail(id); + storeScopeService.checkStoreId(result.getStoreId()); + return result; + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_CREATE) + @PostMapping("/create") + public String create(@RequestBody GaoProduct product) { + TimiException.required(product, "not found product"); + product.setStoreId(storeScopeService.resolveStoreId(product.getStoreId())); + productService.create(product); + return product.getId(); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_UPDATE) + @PostMapping("/update") + public void update(@RequestBody GaoProduct request) { + TimiException.required(request, "not found product"); + TimiException.required(request.getId(), "not found product.id"); + GaoProduct product = productService.get(request.getId()); + TimiException.required(product, "not found product"); + storeScopeService.checkStoreId(product.getStoreId()); + request.setStoreId(product.getStoreId()); + productService.update(request); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_UPDATE) + @PostMapping("/disable") + public void disable(@RequestParam String id) { + GaoProduct product = productService.get(id); + TimiException.required(product, "not found product"); + storeScopeService.checkStoreId(product.getStoreId()); + productService.disable(id); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_READ) + @PostMapping("/sellable/list") + public List sellableList(@RequestBody GaoProductPage page) { + TimiException.required(page, "not found product page"); + page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId())); + return productService.listSellable(page); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoProductStockController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoProductStockController.java new file mode 100644 index 0000000..06111bd --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoProductStockController.java @@ -0,0 +1,110 @@ +package com.imyeyu.api.modules.gao.controller; + +import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission; +import com.imyeyu.api.modules.gao.bean.GaoPermissionCode; +import com.imyeyu.api.modules.gao.entity.GaoProductStock; +import com.imyeyu.api.modules.gao.entity.GaoStockRecord; +import com.imyeyu.api.modules.gao.service.GaoProductStockService; +import com.imyeyu.api.modules.gao.service.GaoStockRecordService; +import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; +import com.imyeyu.api.modules.gao.vo.GaoProductStockChangeRequest; +import com.imyeyu.api.modules.gao.vo.GaoProductStockPage; +import com.imyeyu.api.modules.gao.vo.GaoStockRecordPage; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.spring.annotation.AOPLog; +import com.imyeyu.spring.annotation.RequestRateLimit; +import com.imyeyu.spring.bean.PageResult; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/// GAO 产品库存接口 +/// +/// @author Codex +/// @since 2026-09-02 +@RestController +@RequiredArgsConstructor +@RequestMapping("/gao/product/stock") +public class GaoProductStockController { + + private final GaoProductStockService stockService; + private final GaoStockRecordService recordService; + private final GaoStoreScopeService storeScopeService; + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_STOCK_READ) + @PostMapping("/list") + public PageResult list(@RequestBody GaoProductStockPage page) { + TimiException.required(page, "not found stock page"); + page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId())); + return stockService.pageByQuery(page); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_STOCK_READ) + @PostMapping("/detail") + public GaoProductStock detail(@RequestParam(required = false) String storeId, @RequestParam String productId) { + return stockService.detail(storeScopeService.resolveStoreId(storeId), productId); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_STOCK_CREATE) + @PostMapping("/initial-in") + public GaoProductStock initialIn(@RequestBody GaoProductStockChangeRequest request) { + return stockService.initialIn(normalize(request)); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_STOCK_CREATE) + @PostMapping("/manual-in") + public GaoProductStock manualIn(@RequestBody GaoProductStockChangeRequest request) { + return stockService.manualIn(normalize(request)); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_STOCK_CREATE) + @PostMapping("/manual-out") + public GaoProductStock manualOut(@RequestBody GaoProductStockChangeRequest request) { + return stockService.manualOut(normalize(request)); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_STOCK_CREATE) + @PostMapping("/stocktake-gain") + public GaoProductStock stocktakeGain(@RequestBody GaoProductStockChangeRequest request) { + return stockService.stocktakeGain(normalize(request)); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_STOCK_CREATE) + @PostMapping("/stocktake-loss") + public GaoProductStock stocktakeLoss(@RequestBody GaoProductStockChangeRequest request) { + return stockService.stocktakeLoss(normalize(request)); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_STOCK_READ) + @PostMapping("/record/list") + public PageResult recordList(@RequestBody GaoStockRecordPage page) { + TimiException.required(page, "not found stock record page"); + page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId())); + return recordService.pageByQuery(page); + } + + private GaoProductStockChangeRequest normalize(GaoProductStockChangeRequest request) { + TimiException.required(request, "not found stock change request"); + request.setStoreId(storeScopeService.resolveStoreId(request.getStoreId())); + return request; + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoSalesOrderController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoSalesOrderController.java new file mode 100644 index 0000000..91d774b --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoSalesOrderController.java @@ -0,0 +1,133 @@ +package com.imyeyu.api.modules.gao.controller; + +import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission; +import com.imyeyu.api.modules.gao.bean.GaoPermissionCode; +import com.imyeyu.api.modules.gao.entity.GaoFundRecord; +import com.imyeyu.api.modules.gao.entity.GaoSalesOrder; +import com.imyeyu.api.modules.gao.service.GaoOrderPaymentService; +import com.imyeyu.api.modules.gao.service.GaoSalesOrderService; +import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; +import com.imyeyu.api.modules.gao.vo.GaoDirectProductOrderCreateRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentActionRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentNoRefundRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentReceiveRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentRefundRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentRevokeRequest; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderCancelRequest; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderPage; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.spring.annotation.AOPLog; +import com.imyeyu.spring.annotation.RequestRateLimit; +import com.imyeyu.spring.bean.PageResult; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/// GAO 销售订单和资金接口 +/// +/// @author Codex +/// @since 2026-09-02 +@RestController +@RequiredArgsConstructor +@RequestMapping("/gao/sales/order") +public class GaoSalesOrderController { + + private final GaoSalesOrderService service; + private final GaoOrderPaymentService paymentService; + private final GaoStoreScopeService storeScopeService; + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.SALES_ORDER_READ) + @PostMapping("/list") + public PageResult list(@RequestBody GaoSalesOrderPage page) { + TimiException.required(page, "not found sales order page"); + page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId())); + return service.pageByQuery(page); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.SALES_ORDER_READ) + @PostMapping("/detail") + public GaoSalesOrder detail(@RequestParam String id) { + return service.detail(id); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.SALES_ORDER_CREATE) + @PostMapping("/direct-product/create") + public GaoSalesOrder createDirectProduct(@RequestBody GaoDirectProductOrderCreateRequest request) { + TimiException.required(request, "not found direct product order request"); + request.setStoreId(storeScopeService.resolveStoreId(request.getStoreId())); + return service.createDirectProductOrder(request); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.SALES_ORDER_CANCEL) + @PostMapping("/cancel") + public GaoSalesOrder cancel(@RequestBody GaoSalesOrderCancelRequest request) { + return service.cancel(request); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.FUND_RECEIVE_CREATE) + @PostMapping("/payment/receive") + public GaoFundRecord receive(@RequestBody GaoOrderPaymentReceiveRequest request) { + return paymentService.receive(request); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.FUND_RECEIVE_VERIFY) + @PostMapping("/payment/verify") + public GaoFundRecord verify(@RequestParam String id) { + return paymentService.verify(id); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.FUND_RECEIVE_VERIFY) + @PostMapping("/payment/reject") + public GaoFundRecord reject(@RequestBody GaoOrderPaymentActionRequest request) { + return paymentService.reject(request); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.FUND_RECEIVE_CREATE) + @PostMapping("/payment/cancel") + public GaoFundRecord cancelPayment(@RequestBody GaoOrderPaymentActionRequest request) { + return paymentService.cancel(request); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.FUND_REFUND) + @PostMapping("/refund") + public GaoFundRecord refund(@RequestBody GaoOrderPaymentRefundRequest request) { + return paymentService.refund(request); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.FUND_REFUND) + @PostMapping("/refund/confirm-none") + public GaoFundRecord confirmNoRefund(@RequestBody GaoOrderPaymentNoRefundRequest request) { + return paymentService.confirmNoRefund(request); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.FUND_REVOKE) + @PostMapping("/fund/revoke") + public GaoFundRecord revoke(@RequestBody GaoOrderPaymentRevokeRequest request) { + return paymentService.revoke(request); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoStoreController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoStoreController.java index 85616cc..667d914 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoStoreController.java +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoStoreController.java @@ -2,6 +2,7 @@ package com.imyeyu.api.modules.gao.controller; import com.imyeyu.api.modules.gao.annotation.RequireGaoPermission; import com.imyeyu.api.modules.gao.bean.GaoPermissionCode; +import com.imyeyu.api.modules.gao.entity.GaoEvent; import com.imyeyu.api.modules.gao.entity.GaoStore; import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; import com.imyeyu.api.modules.gao.service.GaoStoreService; @@ -64,6 +65,16 @@ public class GaoStoreController { return store; } + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STORE_UPDATE) + @PostMapping("/direct-product-sale-event/list") + public List listDirectProductSaleEvents(@RequestParam String storeId) { + GaoStore store = service.get(storeId); + storeScopeService.checkStore(store); + return service.listDirectProductSaleEvents(storeId); + } + @AOPLog @RequestRateLimit @RequireGaoPermission(GaoPermissionCode.STORE_CREATE) @@ -90,6 +101,7 @@ public class GaoStoreController { dbStore.setAddress(store.getAddress()); dbStore.setRemark(store.getRemark()); dbStore.setAutoClosedDay(TimiJava.defaultIfNull(store.getAutoClosedDay(), false)); + dbStore.setDirectProductSaleEventId(store.getDirectProductSaleEventId()); service.update(dbStore); } } diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoCustomerPackage.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoCustomerPackage.java index facd096..0e877b3 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoCustomerPackage.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoCustomerPackage.java @@ -29,80 +29,27 @@ public class GaoCustomerPackage extends UUIDEntity { CANCELLED } - /// 付款状态 - public enum PaymentStatus { - - /// 未付款 - UNPAID, - - /// 部分付款 - PARTIALLY_PAID, - - /// 已付清 - PAID - } - - /// 退款状态 - public enum RefundStatus { - - /// 无需退款 - NOT_REQUIRED, - - /// 待退款或待确认无需退款 - PENDING, - - /// 已完成退款处理 - REFUNDED - } - /// 门店 ID private String storeId; + /// 销售订单 ID + private String salesOrderId; + /// 客户 ID private String customerId; /// 来源套餐配置 ID private String packageConfigId; - /// 购买事件登记 ID - private String purchaseEventRecordId; - - /// 购买幂等请求号 - private String requestNo; - /// 套餐名称快照 private String packageNameSnapshot; /// 标准价快照,单位分 private Long standardAmountSnapshot; - /// 实际成交价,单位分 - private Long saleAmount; - - /// 待核销收款合计,单位分 - private Long pendingPaidAmount; - - /// 累计有效收款,单位分 - private Long paidAmount; - - /// 累计有效退款,单位分 - private Long refundedAmount; - /// 套餐状态 private Status status; - /// 付款状态 - private PaymentStatus paymentStatus; - - /// 退款状态 - private RefundStatus refundStatus; - - /// 购买时间 - private Long purchasedAt; - - /// 购买登记人 - private String operatorUserId; - /// 取消时间 private Long cancelledAt; @@ -119,14 +66,6 @@ public class GaoCustomerPackage extends UUIDEntity { @Transient private GaoCustomer customer; - /// 购买事件登记 - @Transient - private GaoEventRecord purchaseEventRecord; - - /// 购买操作人 - @Transient - private User operator; - /// 取消操作人 @Transient private User cancelOperator; @@ -135,11 +74,7 @@ public class GaoCustomerPackage extends UUIDEntity { @Transient private List itemList; - /// 发放历史 + /// 统一履约历史 @Transient - private List deliveryList; - - /// 资金历史 - @Transient - private List fundRecordList; + private List fulfillmentList; } diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoCustomerPackageItem.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoCustomerPackageItem.java index 450f06a..ea4b5fd 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoCustomerPackageItem.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoCustomerPackageItem.java @@ -18,20 +18,29 @@ public class GaoCustomerPackageItem extends UUIDEntity { /// 客户套餐 ID private String customerPackageId; + /// 产品 ID + private String productId; + + /// 产品单位 ID + private String productUnitId; + /// 产品名称快照 private String productNameSnapshot; /// 产品单位快照 private String unitSnapshot; + /// 折算率快照 + private BigDecimal conversionToBaseSnapshot; + /// 总权益数量 private BigDecimal totalQuantity; /// 待核销占用数量 private BigDecimal reservedQuantity; - /// 已核销净发放数量 - private BigDecimal deliveredQuantity; + /// 已履约数量 + private BigDecimal fulfilledQuantity; /// 排序值 private Integer sort; diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoFundRecord.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoFundRecord.java index 7292ca0..85992c8 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoFundRecord.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoFundRecord.java @@ -9,7 +9,7 @@ import lombok.EqualsAndHashCode; import java.util.List; -/// GAO 通用资金操作流水 +/// GAO 销售订单资金操作流水 /// /// @author Codex /// @since 2026-08-31 @@ -20,8 +20,8 @@ public class GaoFundRecord extends UUIDEntity { /// 资金业务类型 public enum BizType { - /// 客户套餐 - CUSTOMER_PACKAGE + /// 销售订单 + SALES_ORDER } /// 资金操作类型 diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageDelivery.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoOrderFulfillment.java similarity index 60% rename from src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageDelivery.java rename to src/main/java/com/imyeyu/api/modules/gao/entity/GaoOrderFulfillment.java index 34f2b6e..21e208d 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageDelivery.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoOrderFulfillment.java @@ -9,27 +9,30 @@ import lombok.EqualsAndHashCode; import java.util.List; -/// GAO 套餐发放操作 +/// GAO 统一履约单 /// /// @author Codex -/// @since 2026-08-31 +/// @since 2026-09-02 @Data @EqualsAndHashCode(callSuper = true) -public class GaoPackageDelivery extends UUIDEntity { +public class GaoOrderFulfillment extends UUIDEntity { - /// 发放操作类型 - public enum OperationType { + /// 履约来源类型 + public enum FulfillmentType { - /// 正常发放 - DELIVER, + /// 客户套餐权益 + PACKAGE_ENTITLEMENT, - /// 整单冲正 - REVOKE + /// 直接产品订单项 + DIRECT_PRODUCT } - /// 发放状态 + /// 履约状态 public enum Status { + /// 等待库存 + WAITING_STOCK, + /// 待核销 PENDING, @@ -42,12 +45,12 @@ public class GaoPackageDelivery extends UUIDEntity { /// 提交人已撤销 CANCELLED, - /// 已核销后被整单冲正 + /// 已核销后冲正 REVOKED } - /// 客户套餐 ID - private String customerPackageId; + /// 销售订单 ID + private String salesOrderId; /// 门店 ID private String storeId; @@ -55,10 +58,10 @@ public class GaoPackageDelivery extends UUIDEntity { /// 客户 ID private String customerId; - /// 操作类型 - private OperationType operationType; + /// 履约来源类型 + private FulfillmentType fulfillmentType; - /// 发放状态 + /// 履约状态 private Status status; /// 客户端幂等请求号 @@ -67,7 +70,7 @@ public class GaoPackageDelivery extends UUIDEntity { /// 提交人 private String operatorUserId; - /// 实际发放时间 + /// 实际履约时间 private Long deliveredAt; /// 核销或拒绝操作人 @@ -76,8 +79,8 @@ public class GaoPackageDelivery extends UUIDEntity { /// 核销或拒绝时间 private Long verifiedAt; - /// 冲正单指向原发放单 - private String sourceDeliveryId; + /// 冲正单指向原履约单 + private String sourceFulfillmentId; /// 拒绝、撤销或冲正原因 private String reason; @@ -85,7 +88,7 @@ public class GaoPackageDelivery extends UUIDEntity { /// 备注 private String remark; - /// 操作人 + /// 提交人 @Transient private User operator; @@ -93,13 +96,17 @@ public class GaoPackageDelivery extends UUIDEntity { @Transient private User verifier; - /// 原发放单 + /// 原履约单 @Transient - private GaoPackageDelivery sourceDelivery; + private GaoOrderFulfillment sourceFulfillment; - /// 发放明细 + /// 履约明细 @Transient - private List itemList; + private List itemList; + + /// 库存处理流水 + @Transient + private List stockRecordList; /// 附件 @Transient diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoOrderFulfillmentItem.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoOrderFulfillmentItem.java new file mode 100644 index 0000000..16f1ae6 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoOrderFulfillmentItem.java @@ -0,0 +1,58 @@ +package com.imyeyu.api.modules.gao.entity; + +import com.imyeyu.spring.entity.UUIDEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; + +/// GAO 统一履约明细 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoOrderFulfillmentItem extends UUIDEntity { + + /// 履约单 ID + private String fulfillmentId; + + /// 产品 ID + private String productId; + + /// 产品单位 ID + private String productUnitId; + + /// 直接订单明细 ID + private String sourceOrderItemId; + + /// 客户套餐权益项 ID + private String sourceCustomerPackageItemId; + + /// 本次履约数量 + private BigDecimal quantity; + + /// 基础单位数量 + private BigDecimal baseQuantity; + + /// 操作前待履约数量 + private BigDecimal reservedBefore; + + /// 操作后待履约数量 + private BigDecimal reservedAfter; + + /// 操作前已履约数量 + private BigDecimal fulfilledBefore; + + /// 操作后已履约数量 + private BigDecimal fulfilledAfter; + + /// 产品名称快照 + private String productNameSnapshot; + + /// 单位快照 + private String unitSnapshot; + + /// 折算率快照 + private BigDecimal conversionToBaseSnapshot; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageConfigItem.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageConfigItem.java index 205d250..ff2f76b 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageConfigItem.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageConfigItem.java @@ -17,11 +17,20 @@ public class GaoPackageConfigItem extends UUIDEntity { /// 套餐配置 ID private String packageConfigId; - /// 产品名称 - private String productName; + /// 产品 ID + private String productId; - /// 产品单位 - private String unit; + /// 产品单位 ID + private String productUnitId; + + /// 产品名称快照 + private String productNameSnapshot; + + /// 单位快照 + private String unitSnapshot; + + /// 折算率快照 + private BigDecimal conversionToBaseSnapshot; /// 默认数量 private BigDecimal defaultQuantity; diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageDeliveryItem.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageDeliveryItem.java deleted file mode 100644 index f1fab81..0000000 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageDeliveryItem.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.imyeyu.api.modules.gao.entity; - -import com.imyeyu.spring.entity.UUIDEntity; -import lombok.Data; -import lombok.EqualsAndHashCode; - -import java.math.BigDecimal; - -/// GAO 套餐发放产品明细 -/// -/// @author Codex -/// @since 2026-08-31 -@Data -@EqualsAndHashCode(callSuper = true) -public class GaoPackageDeliveryItem extends UUIDEntity { - - /// 发放单 ID - private String deliveryId; - - /// 客户套餐产品 ID - private String customerPackageItemId; - - /// 本次数量 - private BigDecimal quantity; - - /// 操作前占用数量快照 - private BigDecimal reservedBefore; - - /// 操作后占用数量快照 - private BigDecimal reservedAfter; - - /// 操作前已发数量快照 - private BigDecimal deliveredBefore; - - /// 操作后已发数量快照 - private BigDecimal deliveredAfter; - - /// 产品名称快照 - private String productNameSnapshot; - - /// 产品单位快照 - private String unitSnapshot; -} diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProduct.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProduct.java new file mode 100644 index 0000000..b8fe90a --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProduct.java @@ -0,0 +1,109 @@ +package com.imyeyu.api.modules.gao.entity; + +import com.imyeyu.api.modules.common.entity.Attachment; +import com.imyeyu.spring.annotation.table.Transient; +import com.imyeyu.spring.entity.UUIDEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; +import java.util.List; + +/// GAO 产品 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoProduct extends UUIDEntity { + + /// 产品类型 + public enum ProductType { + + /// 服务,不管理库存 + SERVICE, + + /// 实物,默认管理库存 + GOODS, + + /// 消耗品,可选择是否管理库存 + CONSUMABLE + } + + /// 产品附件类型 + public enum AttachType { + + /// 产品图片 + PHOTO, + + /// 其他产品文件 + OTHER + } + + /// 产品状态 + public enum Status { + + /// 启用 + ACTIVE, + + /// 停用 + INACTIVE + } + + /// 门店 ID + private String storeId; + + /// 分类 ID + private String categoryId; + + /// 门店内产品编码 + private String code; + + /// 产品名称 + private String name; + + /// 产品类型 + private ProductType productType; + + /// 基础库存单位名称 + private String baseUnit; + + /// true 为启用库存管理 + private Boolean stockEnabled; + + /// true 为允许业务数量保留小数 + private Boolean allowDecimalQuantity; + + /// 状态 + private Status status; + + /// 建议售价,单位分 + private Long salePrice; + + /// 参考成本,单位分 + private Long referenceCost; + + /// 最低库存数量,基础单位 + private BigDecimal minStockQuantity; + + /// 排序值 + private Integer sort; + + /// 备注 + private String remark; + + /// 最后修改用户 ID + private String updatedBy; + + /// 产品分类 + @Transient + private GaoProductCategory category; + + /// 产品单位列表 + @Transient + private List unitList; + + /// 产品附件列表 + @Transient + private List attachmentList; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductCategory.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductCategory.java new file mode 100644 index 0000000..f50546c --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductCategory.java @@ -0,0 +1,36 @@ +package com.imyeyu.api.modules.gao.entity; + +import com.imyeyu.spring.entity.UUIDEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/// GAO 产品分类 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoProductCategory extends UUIDEntity { + + /// 分类状态 + public enum Status { + + /// 启用 + ACTIVE, + + /// 停用 + INACTIVE + } + + /// 门店 ID + private String storeId; + + /// 分类名称 + private String name; + + /// 排序值 + private Integer sort; + + /// 状态 + private Status status; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductStock.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductStock.java new file mode 100644 index 0000000..cd87944 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductStock.java @@ -0,0 +1,40 @@ +package com.imyeyu.api.modules.gao.entity; + +import com.imyeyu.spring.annotation.table.Transient; +import com.imyeyu.spring.entity.UUIDEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; + +/// GAO 产品库存余额 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoProductStock extends UUIDEntity { + + /// 门店 ID + private String storeId; + + /// 产品 ID + private String productId; + + /// 实际库存,基础单位 + private BigDecimal onHandQuantity; + + /// 已预占库存,基础单位 + private BigDecimal reservedQuantity; + + /// 最后操作人 + private String updatedBy; + + /// 产品 + @Transient + private GaoProduct product; + + /// 可用库存,基础单位 + @Transient + private BigDecimal availableQuantity; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductUnit.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductUnit.java new file mode 100644 index 0000000..fc94032 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductUnit.java @@ -0,0 +1,37 @@ +package com.imyeyu.api.modules.gao.entity; + +import com.imyeyu.spring.entity.UUIDEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; + +/// GAO 产品业务单位 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoProductUnit extends UUIDEntity { + + /// 产品 ID + private String productId; + + /// 单位名称 + private String name; + + /// 折算为基础单位的比例 + private BigDecimal conversionToBase; + + /// true 为基础库存单位 + private Boolean isBase; + + /// true 为允许采购入库 + private Boolean allowPurchase; + + /// true 为允许销售或套餐配置 + private Boolean allowSale; + + /// 排序值 + private Integer sort; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoSalesOrder.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoSalesOrder.java new file mode 100644 index 0000000..b3c43e9 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoSalesOrder.java @@ -0,0 +1,157 @@ +package com.imyeyu.api.modules.gao.entity; + +import com.imyeyu.api.modules.common.entity.Attachment; +import com.imyeyu.api.modules.user.entity.User; +import com.imyeyu.spring.annotation.table.Transient; +import com.imyeyu.spring.entity.UUIDEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/// GAO 销售订单 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoSalesOrder extends UUIDEntity { + + /// 订单类型 + public enum OrderType { + + /// 套餐订单 + PACKAGE, + + /// 直接产品订单 + DIRECT_PRODUCT + } + + /// 订单状态 + public enum Status { + + /// 进行中 + ACTIVE, + + /// 全部履约 + FULFILLED, + + /// 已取消 + CANCELLED + } + + /// 付款状态 + public enum PaymentStatus { + + /// 未付款 + UNPAID, + + /// 部分付款 + PARTIALLY_PAID, + + /// 已付清 + PAID + } + + /// 退款状态 + public enum RefundStatus { + + /// 无需退款 + NOT_REQUIRED, + + /// 待退款或待确认无需退款 + PENDING, + + /// 已完成退款 + REFUNDED + } + + /// 门店 ID + private String storeId; + + /// 客户 ID + private String customerId; + + /// 门店内订单号 + private String orderNo; + + /// 订单类型 + private OrderType orderType; + + /// 订单状态 + private Status status; + + /// 付款状态 + private PaymentStatus paymentStatus; + + /// 退款状态 + private RefundStatus refundStatus; + + /// 成交金额,单位分 + private Long saleAmount; + + /// 待核销收款,单位分 + private Long pendingPaidAmount; + + /// 有效收款,单位分 + private Long paidAmount; + + /// 有效退款,单位分 + private Long refundedAmount; + + /// 来源事件记录 ID + private String sourceEventRecordId; + + /// 创建订单幂等号 + private String requestNo; + + /// 下单时间 + private Long purchasedAt; + + /// 下单操作人 + private String operatorUserId; + + /// 取消时间 + private Long cancelledAt; + + /// 取消操作人 + private String cancelOperatorUserId; + + /// 取消原因 + private String cancelReason; + + /// 备注 + private String remark; + + /// 客户 + @Transient + private GaoCustomer customer; + + /// 下单操作人 + @Transient + private User operator; + + /// 取消操作人 + @Transient + private User cancelOperator; + + /// 直接产品订单明细 + @Transient + private List itemList; + + /// 资金流水 + @Transient + private List fundRecordList; + + /// 来源事件记录 + @Transient + private GaoEventRecord sourceEventRecord; + + /// 未收金额,单位分 + @Transient + private Long unpaidAmount; + + /// 订单附件 + @Transient + private List attachmentList; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoSalesOrderItem.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoSalesOrderItem.java new file mode 100644 index 0000000..efb2734 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoSalesOrderItem.java @@ -0,0 +1,55 @@ +package com.imyeyu.api.modules.gao.entity; + +import com.imyeyu.spring.entity.UUIDEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.math.BigDecimal; + +/// GAO 直接产品订单明细 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoSalesOrderItem extends UUIDEntity { + + /// 销售订单 ID + private String salesOrderId; + + /// 产品 ID + private String productId; + + /// 产品单位 ID + private String productUnitId; + + /// 产品名称快照 + private String productNameSnapshot; + + /// 单位快照 + private String unitSnapshot; + + /// 折算率快照 + private BigDecimal conversionToBaseSnapshot; + + /// 购买数量 + private BigDecimal quantity; + + /// 待履约数量 + private BigDecimal reservedQuantity; + + /// 已履约数量 + private BigDecimal fulfilledQuantity; + + /// 实际单价,单位分 + private Long unitPrice; + + /// 行金额,单位分 + private Long lineAmount; + + /// 排序值 + private Integer sort; + + /// 备注 + private String remark; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoStockRecord.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoStockRecord.java new file mode 100644 index 0000000..31a74c9 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoStockRecord.java @@ -0,0 +1,145 @@ +package com.imyeyu.api.modules.gao.entity; + +import com.imyeyu.api.modules.common.entity.Attachment; +import com.imyeyu.api.modules.user.entity.User; +import com.imyeyu.spring.annotation.table.AutoUUID; +import com.imyeyu.spring.annotation.table.Id; +import com.imyeyu.spring.annotation.table.Transient; +import com.imyeyu.spring.entity.Creatable; +import com.imyeyu.spring.entity.IDEntity; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +/// GAO 库存变更流水 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +public class GaoStockRecord implements IDEntity, Creatable { + + /// 库存流水业务类型 + public enum BizType { + + /// 人工库存操作 + PRODUCT_STOCK, + + /// 订单履约库存操作 + ORDER_FULFILLMENT + } + + /// 库存操作类型 + public enum OperationType { + + /// 期初入库 + INITIAL_IN, + + /// 手工入库 + MANUAL_IN, + + /// 手工出库 + MANUAL_OUT, + + /// 履约预占 + ORDER_RESERVE, + + /// 履约释放预占 + ORDER_RELEASE, + + /// 履约出库 + ORDER_OUT, + + /// 履约冲正入库 + ORDER_REVOKE, + + /// 盘盈 + STOCKTAKE_GAIN, + + /// 盘亏 + STOCKTAKE_LOSS + } + + @Id + @AutoUUID + private String id; + + /// 门店 ID + private String storeId; + + /// 产品 ID + private String productId; + + /// 产品单位 ID + private String productUnitId; + + /// 产品名称快照 + private String productNameSnapshot; + + /// 单位名称快照 + private String unitSnapshot; + + /// 折算率快照 + private BigDecimal conversionToBaseSnapshot; + + /// 业务数量 + private BigDecimal quantity; + + /// 基础单位数量 + private BigDecimal baseQuantity; + + /// 操作类型 + private OperationType operationType; + + /// 实际库存增量 + private BigDecimal onHandDelta; + + /// 预占库存增量 + private BigDecimal reservedDelta; + + /// 操作前实际库存 + private BigDecimal onHandBefore; + + /// 操作后实际库存 + private BigDecimal onHandAfter; + + /// 操作前预占库存 + private BigDecimal reservedBefore; + + /// 操作后预占库存 + private BigDecimal reservedAfter; + + /// 来源业务类型 + private BizType bizType; + + /// 来源业务 ID + private String bizId; + + /// 冲正来源流水 ID + private String sourceRecordId; + + /// 幂等请求号 + private String requestNo; + + /// 操作用户 ID + private String operatorUserId; + + /// 发生时间 + private Long occurredAt; + + /// 原因 + private String reason; + + /// 备注 + private String remark; + + private Long createdAt; + + /// 操作用户 + @Transient + private User operator; + + /// 附件 + @Transient + private List attachmentList; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoStore.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoStore.java index 07ea5f7..20ab456 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoStore.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoStore.java @@ -54,6 +54,9 @@ public class GaoStore extends UUIDEntity { /// 排序值 private Integer sort; + /// 直接产品订单使用的消费事件 ID + private String directProductSaleEventId; + public boolean isActivated() { return status == Status.ACTIVE; } diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoCustomerPackageItemMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoCustomerPackageItemMapper.java index 79f9ba4..9766b60 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoCustomerPackageItemMapper.java +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoCustomerPackageItemMapper.java @@ -25,6 +25,6 @@ public interface GaoCustomerPackageItemMapper extends BaseMapper { - /// 按门店和请求号查询购买结果 - GaoCustomerPackage selectByRequestNo(@Param("storeId") String storeId, @Param("requestNo") String requestNo); - - /// 按请求号查询购买结果,用于配置删除后的幂等重试 - GaoCustomerPackage selectByRequestNoAnyStore(@Param("requestNo") String requestNo); - /// 锁定客户套餐 GaoCustomerPackage selectForUpdate(@Param("id") String id); - /// 按购买事件登记查询客户套餐 - GaoCustomerPackage selectByPurchaseEventRecordId(@Param("purchaseEventRecordId") String purchaseEventRecordId); + /// 按销售订单查询客户套餐 + GaoCustomerPackage selectBySalesOrderId(@Param("salesOrderId") String salesOrderId); /// 按条件统计客户套餐 long countByQuery(GaoCustomerPackagePage page); @@ -29,12 +23,4 @@ public interface GaoCustomerPackageMapper extends BaseMapper selectByQuery(GaoCustomerPackagePage page); - /// 原子调整待核销收款 - int updatePendingPaidAmountDelta(@Param("id") String id, @Param("delta") Long delta); - - /// 原子调整有效收款 - int updatePaidAmountDelta(@Param("id") String id, @Param("delta") Long delta); - - /// 带状态条件更新套餐状态 - int updateStatusIf(@Param("id") String id, @Param("fromStatus") GaoCustomerPackage.Status fromStatus, @Param("toStatus") GaoCustomerPackage.Status toStatus, @Param("cancelledAt") Long cancelledAt, @Param("cancelOperatorUserId") String cancelOperatorUserId, @Param("cancelReason") String cancelReason, @Param("refundStatus") GaoCustomerPackage.RefundStatus refundStatus); } diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventRecordMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventRecordMapper.java index 6b473da..a71e9af 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventRecordMapper.java +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventRecordMapper.java @@ -29,6 +29,10 @@ public interface GaoEventRecordMapper extends BaseMapper @Select("SELECT COUNT(*) FROM `gao_event_record` WHERE `event_id` = #{eventId} AND `deleted_at` IS NULL") long countByEventId(String eventId); + /// 查询事件记录是否被订单引用 + @Select("SELECT COUNT(*) FROM `gao_sales_order` WHERE `source_event_record_id` = #{recordId}") + long countBySourceEventRecordId(@Param("recordId") String recordId); + Long count(String storeId, String customerId, String eventId, Long beginAt, Long endAt); /// 查询事件中已携带数值的登记记录数量 diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoOrderFulfillmentItemMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoOrderFulfillmentItemMapper.java new file mode 100644 index 0000000..ead64ba --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoOrderFulfillmentItemMapper.java @@ -0,0 +1,20 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoOrderFulfillmentItem; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/// GAO 统一履约明细 Mapper +/// +/// @author Codex +/// @since 2026-09-02 +public interface GaoOrderFulfillmentItemMapper extends BaseMapper { + + /// 按履约单查询明细 + List selectByFulfillmentId(@Param("fulfillmentId") String fulfillmentId); + + /// 锁定履约明细 + List selectByFulfillmentIdForUpdate(@Param("fulfillmentId") String fulfillmentId); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoOrderFulfillmentMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoOrderFulfillmentMapper.java new file mode 100644 index 0000000..55fc95c --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoOrderFulfillmentMapper.java @@ -0,0 +1,44 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoOrderFulfillment; +import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentPage; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/// GAO 统一履约 Mapper +/// +/// @author Codex +/// @since 2026-09-02 +public interface GaoOrderFulfillmentMapper extends BaseMapper { + + /// 按门店和幂等号查询履约单 + GaoOrderFulfillment selectByRequestNo(@Param("storeId") String storeId, @Param("requestNo") String requestNo); + + /// 按 ID 查询履约单 + GaoOrderFulfillment selectById(@Param("id") String id); + + /// 锁定履约单 + GaoOrderFulfillment selectForUpdate(@Param("id") String id); + + /// 按订单查询履约单 + List selectBySalesOrderId(@Param("salesOrderId") String salesOrderId); + + /// 按冲正来源查询履约单 + GaoOrderFulfillment selectBySourceFulfillmentId(@Param("sourceFulfillmentId") String sourceFulfillmentId); + + /// 统计订单中仍占用权益的履约单 + long countPendingBySalesOrderId(@Param("salesOrderId") String salesOrderId); + + /// 按条件统计履约单 + long countByQuery(GaoOrderFulfillmentPage page); + + /// 按条件查询履约单 + List selectByQuery(GaoOrderFulfillmentPage page); + + /// 更新履约状态 + int updateStatus(@Param("id") String id, @Param("status") GaoOrderFulfillment.Status status, + @Param("verifierUserId") String verifierUserId, @Param("verifiedAt") Long verifiedAt, + @Param("reason") String reason); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPackageDeliveryItemMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPackageDeliveryItemMapper.java deleted file mode 100644 index ebee2fb..0000000 --- a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPackageDeliveryItemMapper.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.imyeyu.api.modules.gao.mapper; - -import com.imyeyu.api.modules.gao.entity.GaoPackageDeliveryItem; -import com.imyeyu.spring.mapper.BaseMapper; -import org.apache.ibatis.annotations.Param; - -import java.util.Collection; -import java.util.List; - -/// GAO 套餐发放明细 Mapper -/// -/// @author Codex -/// @since 2026-08-31 -public interface GaoPackageDeliveryItemMapper extends BaseMapper { - - /// 按发放单查询明细 - List selectByDeliveryId(@Param("deliveryId") String deliveryId); - - /// 批量查询发放明细 - List selectByDeliveryIdList(@Param("deliveryIdList") Collection deliveryIdList); -} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPackageDeliveryMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPackageDeliveryMapper.java deleted file mode 100644 index 7ee4923..0000000 --- a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPackageDeliveryMapper.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.imyeyu.api.modules.gao.mapper; - -import com.imyeyu.api.modules.gao.entity.GaoPackageDelivery; -import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryPage; -import com.imyeyu.spring.mapper.BaseMapper; -import org.apache.ibatis.annotations.Param; - -import java.util.List; - -/// GAO 套餐发放 Mapper -/// -/// @author Codex -/// @since 2026-08-31 -public interface GaoPackageDeliveryMapper extends BaseMapper { - - /// 按门店和请求号查询发放结果 - GaoPackageDelivery selectByRequestNo(@Param("storeId") String storeId, @Param("requestNo") String requestNo); - - /// 锁定发放单 - GaoPackageDelivery selectForUpdate(@Param("id") String id); - - /// 查询原发放单的冲正记录 - GaoPackageDelivery selectRevokeBySourceDeliveryId(@Param("sourceDeliveryId") String sourceDeliveryId); - - /// 查询客户套餐待核销发放数量 - long countPendingByCustomerPackageId(@Param("customerPackageId") String customerPackageId); - - /// 查询客户套餐已核销正常发放数量 - long countVerifiedByCustomerPackageId(@Param("customerPackageId") String customerPackageId); - - /// 查询客户套餐发放历史 - java.util.List selectByCustomerPackageId(@Param("customerPackageId") String customerPackageId); - - /// 按条件统计发放单 - long countByQuery(GaoPackageDeliveryPage page); - - /// 按条件查询发放单 - List selectByQuery(GaoPackageDeliveryPage page); - - /// 带状态条件更新发放单 - int updateStatusIf(@Param("id") String id, @Param("fromStatus") GaoPackageDelivery.Status fromStatus, @Param("toStatus") GaoPackageDelivery.Status toStatus, @Param("verifierUserId") String verifierUserId, @Param("verifiedAt") Long verifiedAt, @Param("reason") String reason); -} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductCategoryMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductCategoryMapper.java new file mode 100644 index 0000000..8b83fdc --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductCategoryMapper.java @@ -0,0 +1,37 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoProductCategory; +import com.imyeyu.api.modules.gao.vo.GaoProductCategoryPage; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Collection; +import java.util.List; + +/// GAO 产品分类 Mapper +/// +/// @author Codex +/// @since 2026-09-02 +public interface GaoProductCategoryMapper extends BaseMapper { + + /// 查询分类 + GaoProductCategory selectById(@Param("id") String id); + + /// 按 ID 批量查询分类 + List selectByIdList(@Param("idList") Collection idList); + + /// 一次性更新分类排序 + void updateSort(@Param("idList") List idList, @Param("updatedAt") long updatedAt); + + /// 按条件统计产品分类 + long countByQuery(GaoProductCategoryPage page); + + /// 按条件查询产品分类 + List selectByQuery(GaoProductCategoryPage page); + + /// 查询门店分类 + List selectByStoreId(@Param("storeId") String storeId); + + /// 查询门店内同名分类 + GaoProductCategory selectByStoreIdAndName(@Param("storeId") String storeId, @Param("name") String name, @Param("excludeId") String excludeId); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductMapper.java new file mode 100644 index 0000000..f46e8da --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductMapper.java @@ -0,0 +1,27 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoProduct; +import com.imyeyu.api.modules.gao.vo.GaoProductPage; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/// GAO 产品 Mapper +/// +/// @author Codex +/// @since 2026-09-02 +public interface GaoProductMapper extends BaseMapper { + + /// 按条件统计产品 + long countByQuery(GaoProductPage page); + + /// 按条件查询产品 + List selectByQuery(GaoProductPage page); + + /// 查询产品编码 + GaoProduct selectByStoreIdAndCode(@Param("storeId") String storeId, @Param("code") String code, @Param("excludeId") String excludeId); + + /// 锁定产品 + GaoProduct selectForUpdate(@Param("id") String id); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductStockMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductStockMapper.java new file mode 100644 index 0000000..7b5c468 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductStockMapper.java @@ -0,0 +1,39 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoProductStock; +import com.imyeyu.api.modules.gao.vo.GaoProductStockPage; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/// GAO 产品库存 Mapper +/// +/// @author Codex +/// @since 2026-09-02 +public interface GaoProductStockMapper extends BaseMapper { + + /// 锁定库存余额 + GaoProductStock selectForUpdate(@Param("storeId") String storeId, @Param("productId") String productId); + + /// 按条件统计库存余额 + long countByQuery(GaoProductStockPage page); + + /// 按条件查询库存余额 + List selectByQuery(GaoProductStockPage page); + + /// 按门店和产品查询库存余额 + GaoProductStock selectByStoreIdAndProductId(@Param("storeId") String storeId, @Param("productId") String productId); + + /// 条件更新库存余额 + int updateQuantity(@Param("id") String id, @Param("onHandDelta") java.math.BigDecimal onHandDelta, @Param("reservedDelta") java.math.BigDecimal reservedDelta, @Param("updatedBy") String updatedBy); + + /// 条件增加预占库存 + int reserve(@Param("id") String id, @Param("quantity") java.math.BigDecimal quantity, @Param("updatedBy") String updatedBy); + + /// 条件释放预占库存 + int release(@Param("id") String id, @Param("quantity") java.math.BigDecimal quantity, @Param("updatedBy") String updatedBy); + + /// 插入库存余额,已存在时忽略 + int insertIgnore(GaoProductStock stock); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductUnitMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductUnitMapper.java new file mode 100644 index 0000000..596082d --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductUnitMapper.java @@ -0,0 +1,27 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoProductUnit; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Collection; +import java.util.List; + +/// GAO 产品单位 Mapper +/// +/// @author Codex +/// @since 2026-09-02 +public interface GaoProductUnitMapper extends BaseMapper { + + /// 按产品查询有效单位 + List selectByProductId(@Param("productId") String productId); + + /// 批量查询产品有效单位 + List selectByProductIdList(@Param("productIdList") Collection productIdList); + + /// 锁定单位 + GaoProductUnit selectForUpdate(@Param("id") String id); + + /// 更新有效单位 + int updateUnit(GaoProductUnit unit); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoSalesOrderItemMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoSalesOrderItemMapper.java new file mode 100644 index 0000000..cb3a58b --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoSalesOrderItemMapper.java @@ -0,0 +1,27 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoSalesOrderItem; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.math.BigDecimal; +import java.util.List; + +/// GAO 销售订单明细 Mapper +/// +/// @author Codex +/// @since 2026-09-02 +public interface GaoSalesOrderItemMapper extends BaseMapper { + + /// 查询订单明细 + List selectBySalesOrderId(@Param("salesOrderId") String salesOrderId); + + /// 锁定订单明细 + GaoSalesOrderItem selectForUpdate(@Param("id") String id); + + /// 原子调整待履约数量 + int updateReservedQuantity(@Param("id") String id, @Param("delta") BigDecimal delta); + + /// 原子调整已履约数量 + int updateFulfilledQuantity(@Param("id") String id, @Param("delta") BigDecimal delta); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoSalesOrderMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoSalesOrderMapper.java new file mode 100644 index 0000000..f80b86e --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoSalesOrderMapper.java @@ -0,0 +1,43 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoSalesOrder; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderPage; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/// GAO 销售订单 Mapper +/// +/// @author Codex +/// @since 2026-09-02 +public interface GaoSalesOrderMapper extends BaseMapper { + + /// 按门店和幂等号查询订单 + GaoSalesOrder selectByRequestNo(@Param("storeId") String storeId, @Param("requestNo") String requestNo); + + /// 锁定订单 + GaoSalesOrder selectForUpdate(@Param("id") String id); + + /// 按条件统计订单 + long countByQuery(GaoSalesOrderPage page); + + /// 按条件查询订单 + List selectByQuery(GaoSalesOrderPage page); + + /// 更新订单付款汇总 + int updatePayment(@Param("id") String id, @Param("pendingPaidAmount") Long pendingPaidAmount, @Param("paidAmount") Long paidAmount, + @Param("refundedAmount") Long refundedAmount, @Param("paymentStatus") GaoSalesOrder.PaymentStatus paymentStatus, + @Param("refundStatus") GaoSalesOrder.RefundStatus refundStatus); + + /// 带状态条件取消订单 + int cancelIf(@Param("id") String id, @Param("fromStatus") GaoSalesOrder.Status fromStatus, @Param("toStatus") GaoSalesOrder.Status toStatus, + @Param("cancelledAt") Long cancelledAt, @Param("cancelOperatorUserId") String cancelOperatorUserId, + @Param("cancelReason") String cancelReason, @Param("refundStatus") GaoSalesOrder.RefundStatus refundStatus); + + /// 写入订单来源事件 + int updateSourceEventRecordId(@Param("id") String id, @Param("sourceEventRecordId") String sourceEventRecordId); + + /// 更新订单履约状态 + int updateStatus(@Param("id") String id, @Param("status") GaoSalesOrder.Status status); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoStockRecordMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoStockRecordMapper.java new file mode 100644 index 0000000..7c36e2d --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoStockRecordMapper.java @@ -0,0 +1,30 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoStockRecord; +import com.imyeyu.api.modules.gao.vo.GaoStockRecordPage; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/// GAO 库存流水 Mapper +/// +/// @author Codex +/// @since 2026-09-02 +public interface GaoStockRecordMapper extends BaseMapper { + + /// 按门店和请求号查询库存流水 + List selectByRequestNo(@Param("storeId") String storeId, @Param("requestNo") String requestNo); + + /// 按业务查询库存流水 + List selectByBiz(@Param("bizType") GaoStockRecord.BizType bizType, @Param("bizId") String bizId); + + /// 查询指定业务和产品的库存流水 + GaoStockRecord selectByBizProduct(@Param("bizType") GaoStockRecord.BizType bizType, @Param("bizId") String bizId, @Param("productId") String productId, @Param("operationType") GaoStockRecord.OperationType operationType); + + /// 按条件统计库存流水 + long countByQuery(GaoStockRecordPage page); + + /// 按条件查询库存流水 + List selectByQuery(GaoStockRecordPage page); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoCustomerPackageService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoCustomerPackageService.java index 55ba3b2..8c98a01 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/GaoCustomerPackageService.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoCustomerPackageService.java @@ -34,6 +34,9 @@ public interface GaoCustomerPackageService extends BaseService listByCustomerId(String customerId); + /// 查询指定事件的登记数量 + /// + /// @param eventId 事件 ID + /// @return 登记数量 + long countByEventId(String eventId); + + /// 查询指定事件中已携带数值的登记数量 + /// + /// @param eventId 事件 ID + /// @return 登记数量 + long countByEventIdAndNumberValue(String eventId); + + /// 按门店和登记日期查询登记数量 + /// + /// @param storeId 门店 ID + /// @param registeredDateValue 登记日期值 + /// @return 登记数量 + long countByStoreIdAndRegisteredDateValue(String storeId, Integer registeredDateValue); + + /// 同步登记记录营业日序号快照 + /// + /// @param storeId 门店 ID + /// @param registeredDateValue 登记日期值 + /// @param businessDayNo 营业日序号 + void updateBusinessDayNo(String storeId, Integer registeredDateValue, long businessDayNo); + + /// 按条件统计登记记录数量 + /// + /// @param storeId 门店 ID + /// @param customerId 客户 ID + /// @param eventId 事件 ID + /// @param beginAt 起始时间,包含 + /// @param endAt 结束时间,不包含 + /// @return 登记数量 + long count(String storeId, String customerId, String eventId, Long beginAt, Long endAt); + + /// 汇总客户指定事件登记数值 + /// + /// @param storeId 门店 ID + /// @param customerId 客户 ID + /// @param eventId 事件 ID + /// @param beginAt 起始时间,包含 + /// @param endAt 结束时间,不包含 + /// @return 数值总和 + long sumNumberValue(String storeId, String customerId, String eventId, Long beginAt, Long endAt); + + /// 回写登记记录获得的积分快照 + /// + /// @param id 登记记录 ID + /// @param earnedPointValue 获得积分 + void updateEarnedPointValue(String id, long earnedPointValue); + /// 为登记记录附加已选择的选项组 /// /// @param recordList 登记记录列表 @@ -53,6 +110,22 @@ public interface GaoEventRecordService extends BaseService pageByRange(GaoEventRecordPage req); + /// 按时间范围统计门店登记数 + /// + /// @param req 门店报表查询请求 + /// @return 每日登记数 + List stateDailyByRange(GaoStoreChartReq req); + + /// 按时间范围统计门店登记数值 + /// + /// @param req 门店报表查询请求 + /// @return 每日登记数值 + List stateNumberValueDailyByRange(GaoStoreChartReq req); + + /// 按时间范围查询门店登记日历 + /// + /// @param req 门店报表查询请求 + /// @return 登记日历 + List calendarByStoreRange(GaoStoreChartReq req); + + /// 按客户和时间范围统计登记数 + /// + /// @param req 客户报表查询请求 + /// @return 每日登记数 + List stateCustomerDailyByRange(GaoCustomerChartReq req); + + /// 按客户和时间范围查询登记日历 + /// + /// @param req 客户报表查询请求 + /// @return 登记日历 + List calendarByCustomerRange(GaoCustomerChartReq req); + /// 按登记记录列表条件汇总数值 /// /// @param req 登记记录范围查询请求 diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventService.java index cef97d2..0fce4b1 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventService.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventService.java @@ -25,6 +25,12 @@ public interface GaoEventService extends BaseService { Map mapByIdList(Collection idList); + /// 查询绑定指定套餐配置的事件数量 + /// + /// @param packageConfigId 套餐配置 ID + /// @return 事件数量 + long countByPackageConfigId(String packageConfigId); + Map> mapRoleCodeListByEventIdList(Collection eventIdList); /// 查询启用的登记事件 diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoFundRecordService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoFundRecordService.java index b90fb98..9595a3f 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/GaoFundRecordService.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoFundRecordService.java @@ -1,18 +1,13 @@ package com.imyeyu.api.modules.gao.service; import com.imyeyu.api.modules.gao.entity.GaoFundRecord; -import com.imyeyu.api.modules.gao.vo.GaoFundActionRequest; -import com.imyeyu.api.modules.gao.vo.GaoFundConfirmNoRefundRequest; import com.imyeyu.api.modules.gao.vo.GaoFundRecordPage; -import com.imyeyu.api.modules.gao.vo.GaoFundReceiveRequest; -import com.imyeyu.api.modules.gao.vo.GaoFundRefundRequest; -import com.imyeyu.api.modules.gao.vo.GaoFundRevokeRequest; import com.imyeyu.spring.bean.PageResult; import com.imyeyu.spring.service.BaseService; import java.util.List; -/// GAO 通用资金流水服务 +/// GAO 销售订单资金流水服务 /// /// @author Codex /// @since 2026-08-31 @@ -21,33 +16,44 @@ public interface GaoFundRecordService extends BaseService /// 按条件查询资金流水 PageResult pageByQuery(GaoFundRecordPage page); - /// 查询客户套餐资金历史 - List listByCustomerPackageId(String customerPackageId); + /// 按销售订单查询资金流水 + List listByBizId(String salesOrderId); - /// 登记线下收款 - GaoFundRecord receive(GaoFundReceiveRequest request); - - /// 核销收款 - GaoFundRecord verify(String fundRecordId); - - /// 拒绝待核销收款 - GaoFundRecord reject(GaoFundActionRequest request); - - /// 撤销自己提交的待核销收款 - GaoFundRecord cancel(GaoFundActionRequest request); - - /// 登记退款 - GaoFundRecord refund(GaoFundRefundRequest request); - - /// 确认无需退款 - GaoFundRecord confirmNoRefund(GaoFundConfirmNoRefundRequest request); - - /// 冲正资金流水 - GaoFundRecord revoke(GaoFundRevokeRequest request); - - /// 统计客户套餐待核销收款数量 + /// 由销售订单资金服务创建资金流水 /// - /// @param bizId 业务 ID - /// @return 待核销收款数量 - long countPendingReceiveByBizId(String bizId); + /// @param record 资金记录 + void createByOrderPayment(GaoFundRecord record); + + /// 查询销售订单待核销收款数量 + long countPendingReceiveByBizId(String salesOrderId); + + /// 按门店和请求号查询资金记录 + /// + /// @param storeId 门店 ID + /// @param requestNo 请求号 + /// @return 资金记录 + GaoFundRecord getByRequestNo(String storeId, String requestNo); + + /// 锁定资金记录 + /// + /// @param id 资金记录 ID + /// @return 资金记录 + GaoFundRecord getForUpdate(String id); + + /// 查询资金冲正记录 + /// + /// @param sourceRecordId 原资金记录 ID + /// @return 冲正记录 + GaoFundRecord getRevokeBySourceRecordId(String sourceRecordId); + + /// 按状态条件更新资金记录 + /// + /// @param id 资金记录 ID + /// @param fromStatus 原状态 + /// @param toStatus 目标状态 + /// @param verifierUserId 核销用户 ID + /// @param verifiedAt 核销时间 + /// @param reason 原因 + /// @return 更新行数 + int updateStatusIf(String id, GaoFundRecord.Status fromStatus, GaoFundRecord.Status toStatus, String verifierUserId, Long verifiedAt, String reason); } diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoOrderFulfillmentService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoOrderFulfillmentService.java new file mode 100644 index 0000000..d80f6a7 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoOrderFulfillmentService.java @@ -0,0 +1,49 @@ +package com.imyeyu.api.modules.gao.service; + +import com.imyeyu.api.modules.gao.entity.GaoOrderFulfillment; +import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentActionRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentCreateRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentPage; +import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentPrepareStockRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentRevokeRequest; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.service.BaseService; + +import java.util.List; + +/// GAO 统一履约服务 +/// +/// @author Codex +/// @since 2026-09-02 +public interface GaoOrderFulfillmentService extends BaseService { + + /// 按条件分页查询履约单 + PageResult pageByQuery(GaoOrderFulfillmentPage page); + + /// 查询履约单详情 + GaoOrderFulfillment detail(String id); + + /// 按销售订单查询履约历史 + List listBySalesOrderId(String salesOrderId); + + /// 查询销售订单仍占用权益的履约单数量 + long countPendingBySalesOrderId(String salesOrderId); + + /// 创建履约单 + GaoOrderFulfillment create(GaoOrderFulfillmentCreateRequest request); + + /// 为库存不足履约单准备库存 + GaoOrderFulfillment prepareStock(GaoOrderFulfillmentPrepareStockRequest request); + + /// 核销履约单 + GaoOrderFulfillment verify(GaoOrderFulfillmentActionRequest request); + + /// 拒绝履约单 + GaoOrderFulfillment reject(GaoOrderFulfillmentActionRequest request); + + /// 撤销待核销履约单 + GaoOrderFulfillment cancel(GaoOrderFulfillmentActionRequest request); + + /// 冲正已核销履约单 + GaoOrderFulfillment revoke(GaoOrderFulfillmentRevokeRequest request); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoOrderPaymentService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoOrderPaymentService.java new file mode 100644 index 0000000..71f51ec --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoOrderPaymentService.java @@ -0,0 +1,36 @@ +package com.imyeyu.api.modules.gao.service; + +import com.imyeyu.api.modules.gao.entity.GaoFundRecord; +import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentActionRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentNoRefundRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentReceiveRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentRefundRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentRevokeRequest; + +/// GAO 销售订单资金服务 +/// +/// @author Codex +/// @since 2026-09-02 +public interface GaoOrderPaymentService { + + /// 登记收款 + GaoFundRecord receive(GaoOrderPaymentReceiveRequest request); + + /// 核销收款 + GaoFundRecord verify(String fundRecordId); + + /// 拒绝收款 + GaoFundRecord reject(GaoOrderPaymentActionRequest request); + + /// 撤销自己提交的收款 + GaoFundRecord cancel(GaoOrderPaymentActionRequest request); + + /// 登记退款 + GaoFundRecord refund(GaoOrderPaymentRefundRequest request); + + /// 确认无需退款 + GaoFundRecord confirmNoRefund(GaoOrderPaymentNoRefundRequest request); + + /// 冲正资金流水 + GaoFundRecord revoke(GaoOrderPaymentRevokeRequest request); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoPackageDeliveryService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoPackageDeliveryService.java deleted file mode 100644 index 078e2c5..0000000 --- a/src/main/java/com/imyeyu/api/modules/gao/service/GaoPackageDeliveryService.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.imyeyu.api.modules.gao.service; - -import com.imyeyu.api.modules.gao.entity.GaoPackageDelivery; -import com.imyeyu.api.modules.gao.entity.GaoPackageDeliveryItem; -import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryActionRequest; -import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryCreateRequest; -import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryPage; -import com.imyeyu.api.modules.gao.vo.GaoPackageDeliveryRevokeRequest; -import com.imyeyu.spring.bean.PageResult; -import com.imyeyu.spring.service.BaseService; - -import java.util.Collection; -import java.util.List; -import java.util.Map; - -/// GAO 套餐发放服务 -/// -/// @author Codex -/// @since 2026-08-31 -public interface GaoPackageDeliveryService extends BaseService { - - /// 按条件查询发放单 - PageResult pageByQuery(GaoPackageDeliveryPage page); - - /// 查询客户套餐发放历史 - List listByCustomerPackageId(String customerPackageId); - - /// 批量查询发放明细 - /// - /// @param deliveryIdList 发放单 ID 列表 - /// @return 按发放单 ID 分组的发放明细 - Map> mapItemListByDeliveryIdList(Collection deliveryIdList); - - /// 创建待核销发放单 - GaoPackageDelivery createDelivery(GaoPackageDeliveryCreateRequest request); - - /// 核销发放单 - GaoPackageDelivery verify(String deliveryId); - - /// 拒绝待核销发放单 - GaoPackageDelivery reject(GaoPackageDeliveryActionRequest request); - - /// 撤销自己提交的待核销发放单 - GaoPackageDelivery cancel(GaoPackageDeliveryActionRequest request); - - /// 整单冲正已核销发放单 - GaoPackageDelivery revoke(GaoPackageDeliveryRevokeRequest request); - - /// 统计客户套餐待核销发放单数量 - /// - /// @param customerPackageId 客户套餐 ID - /// @return 待核销发放单数量 - long countPendingByCustomerPackageId(String customerPackageId); - - /// 统计客户套餐已核销正常发放单数量 - /// - /// @param customerPackageId 客户套餐 ID - /// @return 已核销正常发放单数量 - long countVerifiedByCustomerPackageId(String customerPackageId); -} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoPointLedgerService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoPointLedgerService.java index 442d9b9..5e6fd43 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/GaoPointLedgerService.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoPointLedgerService.java @@ -9,6 +9,8 @@ import com.imyeyu.api.modules.gao.vo.GaoPointLedgerPage; import com.imyeyu.spring.bean.PageResult; import com.imyeyu.spring.service.BaseService; +import java.util.List; + /// GAO 客户积分流水服务 /// /// @author Codex @@ -38,6 +40,18 @@ public interface GaoPointLedgerService extends BaseService listRuleChangeByEventRecordId(String eventRecordId); + + /// 按幂等键批量查询积分流水 + /// + /// @param idempotencyKeyList 幂等键列表 + /// @return 已存在流水 + List listByIdempotencyKeyList(List idempotencyKeyList); + /// 分页查询积分流水 /// /// @param page 查询条件 diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductCategoryService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductCategoryService.java new file mode 100644 index 0000000..68c2842 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductCategoryService.java @@ -0,0 +1,27 @@ +package com.imyeyu.api.modules.gao.service; + +import com.imyeyu.api.modules.gao.entity.GaoProductCategory; +import com.imyeyu.api.modules.gao.vo.GaoProductCategoryPage; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.service.BaseService; + +import java.util.List; + +/// GAO 产品分类服务 +/// +/// @author Codex +/// @since 2026-09-02 +public interface GaoProductCategoryService extends BaseService { + + /// 按条件分页查询分类 + PageResult pageByQuery(GaoProductCategoryPage page); + + /// 查询门店分类 + List listByStore(String storeId); + + /// 批量更新分类排序 + void sort(List idList); + + /// 停用分类 + void disable(String id); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductService.java new file mode 100644 index 0000000..139c85f --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductService.java @@ -0,0 +1,44 @@ +package com.imyeyu.api.modules.gao.service; + +import com.imyeyu.api.modules.gao.entity.GaoProduct; +import com.imyeyu.api.modules.gao.entity.GaoProductUnit; +import com.imyeyu.api.modules.gao.vo.GaoProductPage; +import com.imyeyu.api.modules.gao.vo.GaoProductUnitSelection; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.service.BaseService; + +import java.util.List; + +/// GAO 产品服务 +/// +/// @author Codex +/// @since 2026-09-02 +public interface GaoProductService extends BaseService { + + /// 按条件分页查询产品 + PageResult pageByQuery(GaoProductPage page); + + /// 查询产品详情单位 + GaoProduct detail(String id); + + /// 查询启用且允许销售的产品单位 + List listSellable(GaoProductPage page); + + /// 查询并校验可销售产品单位 + GaoProductUnitSelection getSellableProductUnit(String storeId, String productId, String productUnitId); + + /// 查询当前门店的有效产品单位 + /// + /// @param storeId 门店 ID + /// @param productId 产品 ID + /// @param productUnitId 产品单位 ID + /// @param purchase true 为按采购入库用途校验 + /// @return 已校验的产品单位 + GaoProductUnitSelection getProductUnit(String storeId, String productId, String productUnitId, boolean purchase); + + /// 查询产品单位 + List listUnitByProductId(String productId); + + /// 停用产品 + void disable(String id); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductStockService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductStockService.java new file mode 100644 index 0000000..c526b85 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductStockService.java @@ -0,0 +1,53 @@ +package com.imyeyu.api.modules.gao.service; + +import com.imyeyu.api.modules.gao.entity.GaoProductStock; +import com.imyeyu.api.modules.gao.entity.GaoStockRecord; +import com.imyeyu.api.modules.gao.vo.GaoProductStockChangeRequest; +import com.imyeyu.api.modules.gao.vo.GaoProductStockPage; +import com.imyeyu.api.modules.gao.vo.GaoStockChangeItem; +import com.imyeyu.spring.bean.PageResult; + +import java.util.List; + +/// GAO 产品库存服务 +/// +/// @author Codex +/// @since 2026-09-02 +public interface GaoProductStockService { + + /// 查询库存余额 + PageResult pageByQuery(GaoProductStockPage page); + + /// 查询库存余额详情 + GaoProductStock detail(String storeId, String productId); + + /// 期初入库 + GaoProductStock initialIn(GaoProductStockChangeRequest request); + + /// 手工入库 + GaoProductStock manualIn(GaoProductStockChangeRequest request); + + /// 手工出库 + GaoProductStock manualOut(GaoProductStockChangeRequest request); + + /// 盘盈 + GaoProductStock stocktakeGain(GaoProductStockChangeRequest request); + + /// 盘亏 + GaoProductStock stocktakeLoss(GaoProductStockChangeRequest request); + + /// 为履约预占库存 + boolean hasSufficientForFulfillment(String storeId, List items); + + /// 为履约预占库存 + List reserveForFulfillment(String storeId, String bizId, List items, String requestNo, String operatorUserId); + + /// 释放履约预占库存 + List releaseForFulfillment(String storeId, String bizId, List items, String requestNo, String operatorUserId); + + /// 履约出库 + List outForFulfillment(String storeId, String bizId, List items, String requestNo, String operatorUserId); + + /// 履约库存冲正入库 + List revokeForFulfillment(String storeId, String bizId, List items, String requestNo, String operatorUserId); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductUnitService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductUnitService.java new file mode 100644 index 0000000..5ddc33c --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductUnitService.java @@ -0,0 +1,28 @@ +package com.imyeyu.api.modules.gao.service; + +import com.imyeyu.api.modules.gao.entity.GaoProduct; +import com.imyeyu.api.modules.gao.entity.GaoProductUnit; + +import java.math.BigDecimal; +import java.util.List; + +/// GAO 产品单位服务 +/// +/// @author Codex +/// @since 2026-09-02 +public interface GaoProductUnitService { + + /// 校验并规范化产品单位 + /// + /// @param product 产品 + /// @param unitList 单位列表 + /// @return 规范化后的单位列表 + List validateAndNormalize(GaoProduct product, List unitList); + + /// 按基础单位换算数量 + /// + /// @param unit 单位 + /// @param quantity 业务数量 + /// @return 基础单位数量 + BigDecimal convertToBase(GaoProductUnit unit, BigDecimal quantity); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoSalesOrderService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoSalesOrderService.java new file mode 100644 index 0000000..e6d9bea --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoSalesOrderService.java @@ -0,0 +1,72 @@ +package com.imyeyu.api.modules.gao.service; + +import com.imyeyu.api.modules.gao.entity.GaoSalesOrder; +import com.imyeyu.api.modules.gao.entity.GaoSalesOrderItem; +import com.imyeyu.api.modules.gao.vo.GaoDirectProductOrderCreateRequest; +import com.imyeyu.api.modules.gao.vo.GaoPackageOrderCreateData; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderCancelRequest; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderPage; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.service.BaseService; + +import java.math.BigDecimal; +import java.util.List; + +/// GAO 销售订单服务 +/// +/// @author Codex +/// @since 2026-09-02 +public interface GaoSalesOrderService extends BaseService { + + /// 按条件分页查询订单 + PageResult pageByQuery(GaoSalesOrderPage page); + + /// 查询订单详情 + GaoSalesOrder detail(String id); + + /// 创建直接产品订单 + GaoSalesOrder createDirectProductOrder(GaoDirectProductOrderCreateRequest request); + + /// 按门店和幂等号查询订单 + /// + /// @param storeId 门店 ID + /// @param requestNo 幂等号 + /// @return 销售订单,不存在时为空 + GaoSalesOrder getByStoreIdAndRequestNo(String storeId, String requestNo); + + /// 由套餐购买流程创建套餐订单 + GaoSalesOrder createPackageOrder(GaoPackageOrderCreateData data); + + /// 锁定订单 + GaoSalesOrder getForUpdate(String id); + + /// 取消订单 + GaoSalesOrder cancel(GaoSalesOrderCancelRequest request); + + /// 更新订单付款汇总 + void updatePayment(GaoSalesOrder order); + + /// 重算付款状态 + void recomputePaymentStatus(GaoSalesOrder order); + + /// 检查履约权限 + void checkFulfillmentPermission(GaoSalesOrder order); + + /// 原子调整订单项待履约数量 + int updateItemReservedQuantity(String itemId, BigDecimal delta); + + /// 原子调整订单项已履约数量 + int updateItemFulfilledQuantity(String itemId, BigDecimal delta); + + /// 锁定订单明细 + GaoSalesOrderItem getItemForUpdate(String itemId); + + /// 根据订单明细刷新订单履约状态 + void refreshFulfillmentStatus(GaoSalesOrder order); + + /// 写入套餐订单履约状态 + void updateFulfillmentStatus(GaoSalesOrder order, boolean fulfilled); + + /// 查询订单明细 + List listItemByOrderId(String orderId); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoStockRecordService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoStockRecordService.java new file mode 100644 index 0000000..a58ad5c --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoStockRecordService.java @@ -0,0 +1,32 @@ +package com.imyeyu.api.modules.gao.service; + +import com.imyeyu.api.modules.common.entity.Attachment; +import com.imyeyu.api.modules.gao.entity.GaoStockRecord; +import com.imyeyu.api.modules.gao.vo.GaoStockRecordPage; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.service.BaseService; + +import java.util.List; + +/// GAO 库存流水服务 +/// +/// @author Codex +/// @since 2026-09-02 +public interface GaoStockRecordService extends BaseService { + + /// 按条件查询库存流水 + PageResult pageByQuery(GaoStockRecordPage page); + + /// 按来源业务查询库存流水 + List listByBiz(GaoStockRecord.BizType bizType, String bizId); + + /// 按门店和请求号查询库存流水 + List listByRequestNo(String storeId, String requestNo); + + /// 查询指定业务和产品的库存流水 + GaoStockRecord getByBizProduct(GaoStockRecord.BizType bizType, String bizId, String productId, + GaoStockRecord.OperationType operationType); + + /// 保存库存操作流水及附件 + void createOperationRecord(GaoStockRecord record, List attachmentList); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoStoreService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoStoreService.java index b62d724..0c35476 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/GaoStoreService.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoStoreService.java @@ -1,5 +1,6 @@ package com.imyeyu.api.modules.gao.service; +import com.imyeyu.api.modules.gao.entity.GaoEvent; import com.imyeyu.api.modules.gao.entity.GaoStore; import com.imyeyu.spring.bean.Page; import com.imyeyu.spring.service.BaseService; @@ -14,6 +15,18 @@ import java.util.List; /// @since 2026-08-06 public interface GaoStoreService extends BaseService { + /// 校验直接产品订单使用的消费事件 + /// + /// @param store 门店 + /// @return 合法的消费事件 + GaoEvent validateDirectProductSaleEvent(GaoStore store); + + /// 查询可绑定到直接产品订单的消费事件 + /// + /// @param storeId 门店 ID + /// @return 当前门店可绑定的消费事件 + List listDirectProductSaleEvents(String storeId); + /// 查询用户所属门店 /// /// @param userId 用户 ID diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerChartServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerChartServiceImplement.java index e2e1d32..f1341d0 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerChartServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerChartServiceImplement.java @@ -1,7 +1,7 @@ package com.imyeyu.api.modules.gao.service.implement; -import com.imyeyu.api.modules.gao.mapper.GaoEventRecordMapper; import com.imyeyu.api.modules.gao.service.GaoCustomerChartService; +import com.imyeyu.api.modules.gao.service.GaoEventRecordService; import com.imyeyu.api.modules.gao.vo.GaoCustomerChartReq; import com.imyeyu.api.modules.gao.vo.GaoEventRecordCalendarView; import com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView; @@ -19,18 +19,18 @@ import java.util.List; @RequiredArgsConstructor public class GaoCustomerChartServiceImplement implements GaoCustomerChartService { - private final GaoEventRecordMapper eventRecordMapper; + private final GaoEventRecordService eventRecordService; @Override public List eventRecordDailyStat(GaoCustomerChartReq req) { checkRangeReq(req); - return eventRecordMapper.stateCustomerDailyByRange(req); + return eventRecordService.stateCustomerDailyByRange(req); } @Override public List eventRecordCalendar(GaoCustomerChartReq req) { checkRangeReq(req); - return eventRecordMapper.selectCustomerCalendarByRange(req); + return eventRecordService.calendarByCustomerRange(req); } private void checkRangeReq(GaoCustomerChartReq req) { diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerPackageServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerPackageServiceImplement.java index 78f8b6d..5f92d19 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerPackageServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerPackageServiceImplement.java @@ -9,20 +9,23 @@ import com.imyeyu.api.modules.gao.entity.GaoEvent; import com.imyeyu.api.modules.gao.entity.GaoEventRecord; import com.imyeyu.api.modules.gao.entity.GaoPackageConfig; import com.imyeyu.api.modules.gao.entity.GaoPackageConfigItem; +import com.imyeyu.api.modules.gao.entity.GaoSalesOrder; import com.imyeyu.api.modules.gao.mapper.GaoCustomerPackageItemMapper; import com.imyeyu.api.modules.gao.mapper.GaoCustomerPackageMapper; +import com.imyeyu.api.modules.gao.service.GaoSalesOrderService; import com.imyeyu.api.modules.gao.service.GaoCustomerService; import com.imyeyu.api.modules.gao.service.GaoCustomerPackageService; import com.imyeyu.api.modules.gao.service.GaoEventRecordService; import com.imyeyu.api.modules.gao.service.GaoEventService; -import com.imyeyu.api.modules.gao.service.GaoFundRecordService; -import com.imyeyu.api.modules.gao.service.GaoPackageDeliveryService; import com.imyeyu.api.modules.gao.service.GaoPackageConfigService; +import com.imyeyu.api.modules.gao.service.GaoProductService; import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; import com.imyeyu.api.modules.gao.vo.GaoCustomerPackageCancelRequest; import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePage; import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePurchaseRequest; import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePurchaseItem; +import com.imyeyu.api.modules.gao.vo.GaoPackageOrderCreateData; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderCancelRequest; import com.imyeyu.api.modules.system.entity.Logger; import com.imyeyu.api.modules.system.service.LoggerService; import com.imyeyu.api.modules.user.service.PermissionChecker; @@ -34,7 +37,6 @@ import com.imyeyu.spring.mapper.BaseMapper; import com.imyeyu.spring.service.AbstractEntityService; import com.imyeyu.utils.Time; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.ObjectProvider; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -59,12 +61,12 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService deliveryServiceProvider; - private final ObjectProvider fundRecordServiceProvider; private final GaoPackageConfigService packageConfigService; + private final GaoProductService productService; private final GaoCustomerService customerService; private final GaoEventService eventService; private final GaoEventRecordService eventRecordService; + private final GaoSalesOrderService salesOrderService; private final GaoStoreScopeService storeScopeService; private final UserLoginService userLoginService; private final PermissionChecker permissionChecker; @@ -80,6 +82,12 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService listItemByCustomerPackageId(String customerPackageId) { return itemMapper.selectByCustomerPackageId(customerPackageId); @@ -96,18 +104,8 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService itemList = itemMapper.selectByCustomerPackageId(packageEntity.getId()); - for (GaoCustomerPackageItem item : itemList) { - TimiException.requiredTrue(item.getDeliveredQuantity().compareTo(item.getTotalQuantity()) < 0, "已全部履约的套餐不能取消"); - } + TimiException.required(packageEntity.getSalesOrderId(), "套餐未关联销售订单"); + GaoSalesOrderCancelRequest orderCancelRequest = new GaoSalesOrderCancelRequest(); + orderCancelRequest.setSalesOrderId(packageEntity.getSalesOrderId()); + orderCancelRequest.setReason(reason); + GaoSalesOrder order = salesOrderService.cancel(orderCancelRequest); packageEntity.setStatus(GaoCustomerPackage.Status.CANCELLED); - packageEntity.setCancelledAt(Time.now()); - packageEntity.setCancelOperatorUserId(userLoginService.getRequireLoginUserId()); + packageEntity.setCancelledAt(order.getCancelledAt()); + packageEntity.setCancelOperatorUserId(order.getCancelOperatorUserId()); packageEntity.setCancelReason(reason); - packageEntity.setRefundStatus(0 < safeLong(packageEntity.getPaidAmount()) ? GaoCustomerPackage.RefundStatus.PENDING : GaoCustomerPackage.RefundStatus.NOT_REQUIRED); mapper.update(packageEntity); return packageEntity; } @@ -307,8 +311,11 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService defaultPurchaseItems(List configItemList) { return configItemList.stream().map(configItem -> { GaoCustomerPackagePurchaseItem item = new GaoCustomerPackagePurchaseItem(); - item.setProductName(configItem.getProductName()); - item.setUnit(configItem.getUnit()); + item.setProductId(configItem.getProductId()); + item.setProductUnitId(configItem.getProductUnitId()); + item.setProductNameSnapshot(configItem.getProductNameSnapshot()); + item.setUnitSnapshot(configItem.getUnitSnapshot()); + item.setConversionToBaseSnapshot(configItem.getConversionToBaseSnapshot()); item.setTotalQuantity(configItem.getDefaultQuantity()); item.setSort(configItem.getSort()); item.setRemark(configItem.getRemark()); @@ -322,53 +329,65 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService= item.getTotalQuantity().scale(), "购买产品数量最多保留三位小数"); - String key = item.getProductName().trim() + "|" + item.getUnit().trim(); - TimiException.requiredTrue(keySet.add(key), "购买套餐内产品名称和单位不能重复"); - item.setProductName(item.getProductName().trim()); - item.setUnit(item.getUnit().trim()); + TimiException.requiredTrue(3 >= item.getTotalQuantity().stripTrailingZeros().scale(), "购买产品数量最多保留三位小数"); + String key = item.getProductId() + "|" + item.getProductUnitId(); + TimiException.requiredTrue(keySet.add(key), "购买套餐内产品和单位不能重复"); item.setSort(TimiJava.defaultIfNull(item.getSort(), i)); } } + private void verifyConfigSellableItems(String storeId, List itemList) { + for (GaoPackageConfigItem item : itemList) { + TimiException.required(item.getProductId(), "套餐配置缺少产品"); + TimiException.required(item.getProductUnitId(), "套餐配置缺少产品单位"); + // 通过产品库唯一选品入口重新校验产品和单位仍处于可销售状态 + productService.getSellableProductUnit(storeId, item.getProductId(), item.getProductUnitId()); + } + } + private void verifySameStructureAsConfig(List itemList, List configItemList) { TimiException.requiredTrue(itemList.size() == configItemList.size(), "套餐权益项目必须与事件绑定套餐一致"); - Map itemMap = itemList.stream().collect(Collectors.toMap(item -> item.getProductName() + "|" + item.getUnit(), Function.identity())); + Map itemMap = itemList.stream().collect(Collectors.toMap(item -> item.getProductId() + "|" + item.getProductUnitId(), Function.identity())); for (GaoPackageConfigItem configItem : configItemList) { - GaoCustomerPackagePurchaseItem item = itemMap.get(configItem.getProductName() + "|" + configItem.getUnit()); + GaoCustomerPackagePurchaseItem item = itemMap.get(configItem.getProductId() + "|" + configItem.getProductUnitId()); TimiException.required(item, "套餐权益项目必须与事件绑定套餐一致"); + item.setProductNameSnapshot(configItem.getProductNameSnapshot()); + item.setUnitSnapshot(configItem.getUnitSnapshot()); + item.setConversionToBaseSnapshot(configItem.getConversionToBaseSnapshot()); TimiException.requiredTrue(item.getSort().equals(configItem.getSort()) && sameText(item.getRemark(), configItem.getRemark()), "套餐权益项目的名称、单位、排序和备注不可修改"); } } private void verifySameQuantityAsConfig(List itemList, List configItemList) { - Map itemMap = itemList.stream().collect(Collectors.toMap(item -> item.getProductName() + "|" + item.getUnit(), Function.identity())); + Map itemMap = itemList.stream().collect(Collectors.toMap(item -> item.getProductId() + "|" + item.getProductUnitId(), Function.identity())); for (GaoPackageConfigItem configItem : configItemList) { - GaoCustomerPackagePurchaseItem item = itemMap.get(configItem.getProductName() + "|" + configItem.getUnit()); + GaoCustomerPackagePurchaseItem item = itemMap.get(configItem.getProductId() + "|" + configItem.getProductUnitId()); TimiException.requiredTrue(item.getTotalQuantity().compareTo(configItem.getDefaultQuantity()) == 0, "普通员工不能修改套餐权益配额"); } } - private void verifyPurchaseIdempotency(GaoCustomerPackage existing, GaoCustomerPackagePurchaseRequest request) { - TimiException.requiredTrue(existing.getCustomerId().equals(request.getCustomerId()) - && existing.getSaleAmount().equals(request.getSaleAmount()), "requestNo 已被其他购买请求使用"); - GaoEventRecord eventRecord = eventRecordService.get(existing.getPurchaseEventRecordId()); + private void verifyPurchaseIdempotency(GaoSalesOrder existingOrder, GaoCustomerPackage existingPackage, GaoCustomerPackagePurchaseRequest request) { + TimiException.required(existingPackage, "幂等订单未找到客户套餐权益"); + TimiException.requiredTrue(existingOrder.getOrderType() == GaoSalesOrder.OrderType.PACKAGE + && existingOrder.getCustomerId().equals(request.getCustomerId()) + && existingOrder.getSaleAmount().equals(request.getSaleAmount()), "requestNo 已被其他购买请求使用"); + GaoEventRecord eventRecord = eventRecordService.get(existingOrder.getSourceEventRecordId()); TimiException.required(eventRecord, "购买事件登记不存在"); TimiException.requiredTrue(eventRecord.getEventId().equals(request.getEventId()) - && (request.getPurchasedAt() == null || eventRecord.getRegisteredAt().equals(request.getPurchasedAt())) - && sameText(existing.getRemark(), request.getRemark()), "requestNo 对应购买请求内容不一致"); - List existingItemList = itemMapper.selectByCustomerPackageId(existing.getId()); + && (request.getPurchasedAt() == null || existingOrder.getPurchasedAt().equals(request.getPurchasedAt())) + && sameText(existingOrder.getRemark(), request.getRemark()), "requestNo 对应购买请求内容不一致"); + List existingItemList = itemMapper.selectByCustomerPackageId(existingPackage.getId()); List requestItemList = TimiJava.defaultIfNull(request.getItemList(), List.of()); verifyPurchaseItems(requestItemList); TimiException.requiredTrue(existingItemList.size() == requestItemList.size(), "requestNo 对应购买请求内容不一致"); - Map existingItemMap = existingItemList.stream().collect(Collectors.toMap(item -> item.getProductNameSnapshot() + "|" + item.getUnitSnapshot(), Function.identity())); + Map existingItemMap = existingItemList.stream().collect(Collectors.toMap(item -> item.getProductId() + "|" + item.getProductUnitId(), Function.identity())); for (GaoCustomerPackagePurchaseItem item : requestItemList) { - GaoCustomerPackageItem existingItem = existingItemMap.get(item.getProductName() + "|" + item.getUnit()); + GaoCustomerPackageItem existingItem = existingItemMap.get(item.getProductId() + "|" + item.getProductUnitId()); TimiException.required(existingItem, "requestNo 对应购买请求内容不一致"); TimiException.requiredTrue(existingItem.getTotalQuantity().compareTo(item.getTotalQuantity()) == 0 && existingItem.getSort().equals(item.getSort()) @@ -385,14 +404,6 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService recordList) { List recordIdList = new ArrayList<>(); @@ -161,22 +214,39 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService stateDailyByRange(GaoStoreChartReq req) { + return mapper.stateDailyByRange(req); + } + + @Override + public List stateNumberValueDailyByRange(GaoStoreChartReq req) { + return mapper.stateNumberValueDailyByRange(req); + } + + @Override + public List calendarByStoreRange(GaoStoreChartReq req) { + return mapper.selectCalendarByRange(req); + } + + @Override + public List stateCustomerDailyByRange(GaoCustomerChartReq req) { + return mapper.stateCustomerDailyByRange(req); + } + + @Override + public List calendarByCustomerRange(GaoCustomerChartReq req) { + return mapper.selectCustomerCalendarByRange(req); + } + @Override public Long sumNumberValueByRange(GaoEventRecordPage page) { TimiException.required(page.getStoreId(), "not found page.storeId"); diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoEventServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoEventServiceImplement.java index e099af8..65688c3 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoEventServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoEventServiceImplement.java @@ -6,14 +6,12 @@ import com.imyeyu.api.modules.gao.entity.GaoEvent; import com.imyeyu.api.modules.gao.entity.GaoEventOption; import com.imyeyu.api.modules.gao.entity.GaoEventOptionGroup; import com.imyeyu.api.modules.gao.entity.GaoEventRoleRelation; -import com.imyeyu.api.modules.gao.entity.GaoPackageConfig; import com.imyeyu.api.modules.gao.mapper.GaoEventMapper; import com.imyeyu.api.modules.gao.mapper.GaoEventOptionGroupMapper; import com.imyeyu.api.modules.gao.mapper.GaoEventOptionMapper; -import com.imyeyu.api.modules.gao.mapper.GaoEventRecordMapper; import com.imyeyu.api.modules.gao.mapper.GaoEventRoleRelationMapper; +import com.imyeyu.api.modules.gao.service.GaoEventRecordService; import com.imyeyu.api.modules.gao.service.GaoEventService; -import com.imyeyu.api.modules.gao.service.GaoPackageConfigService; import com.imyeyu.api.modules.system.entity.Logger; import com.imyeyu.api.modules.system.service.LoggerService; import com.imyeyu.java.TimiJava; @@ -26,6 +24,7 @@ import com.imyeyu.spring.service.AbstractEntityService; import com.imyeyu.utils.Time; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -57,9 +56,8 @@ public class GaoEventServiceImplement extends AbstractEntityService eventRecordServiceProvider; @Override protected BaseMapper mapper() { @@ -134,7 +132,7 @@ public class GaoEventServiceImplement extends AbstractEntityService(idList)).stream().collect(Collectors.toMap(GaoEvent::getId, Function.identity())); } + @Override + public long countByPackageConfigId(String packageConfigId) { + TimiException.required(packageConfigId, "not found packageConfigId"); + return mapper.countByPackageConfigId(packageConfigId); + } + @Override public Map> mapRoleCodeListByEventIdList(Collection eventIdList) { return roleRelationMapper.selectAllByEventIdList(new HashSet<>(eventIdList)) @@ -325,9 +329,6 @@ public class GaoEventServiceImplement extends AbstractEntityService implements GaoFundRecordService { private final GaoFundRecordMapper mapper; - private final GaoCustomerPackageService customerPackageService; - private final GaoPackageDeliveryService deliveryService; - private final AttachmentService attachmentService; - private final GaoStoreScopeService storeScopeService; - private final UserLoginService userLoginService; - private final LoggerService loggerService; @Override protected BaseMapper mapper() { return mapper; } - /// 资金流水必须通过专用操作创建 + /// 资金流水必须通过销售订单资金接口创建 /// /// @param record 资金流水 @Override public void create(GaoFundRecord record) { - TimiException.requiredTrue(false, "资金流水必须通过专用接口创建"); + TimiException.requiredTrue(false, "资金流水必须通过销售订单资金接口创建"); + } + + @Override + public void createByOrderPayment(GaoFundRecord record) { + TimiException.required(record, "not found fund record"); + super.create(record); } /// 资金流水不允许通用修改 @@ -94,296 +76,41 @@ public class GaoFundRecordServiceImplement extends AbstractEntityService listByCustomerPackageId(String customerPackageId) { - TimiException.required(customerPackageId, "not found customerPackageId"); - return mapper.selectByBizId(customerPackageId); + public List listByBizId(String salesOrderId) { + TimiException.required(salesOrderId, "not found salesOrderId"); + return mapper.selectByBizId(salesOrderId); } @Override - public long countPendingReceiveByBizId(String bizId) { - return mapper.countPendingReceiveByBizId(bizId); + public long countPendingReceiveByBizId(String salesOrderId) { + TimiException.required(salesOrderId, "not found salesOrderId"); + return mapper.countPendingReceiveByBizId(salesOrderId); } @Override - public GaoFundRecord get(String id) { - return super.get(id); + public GaoFundRecord getByRequestNo(String storeId, String requestNo) { + TimiException.required(storeId, "not found storeId"); + TimiException.required(requestNo, "not found requestNo"); + return mapper.selectByRequestNo(storeId, requestNo); } - @Transactional(TimiServerDBConfig.ROLLBACKER) @Override - public GaoFundRecord receive(GaoFundReceiveRequest request) { - return loggerService.execute(Logger.Module.GAO, "GAO_FUND_RECEIVE_CREATE", request, () -> receiveInternal(request)); + public GaoFundRecord getForUpdate(String id) { + TimiException.required(id, "not found fundRecordId"); + return mapper.selectForUpdate(id); } - private GaoFundRecord receiveInternal(GaoFundReceiveRequest request) { - TimiException.required(request, "not found receive request"); - TimiException.required(request.getRequestNo(), "not found requestNo"); - request.setRequestNo(request.getRequestNo().trim()); - TimiException.required(request.getCustomerPackageId(), "not found customerPackageId"); - TimiException.required(request.getAmount(), "not found amount"); - TimiException.requiredTrue(0 < request.getAmount(), "收款金额必须大于 0"); - GaoCustomerPackage packageEntity = lockPackage(request.getCustomerPackageId()); - GaoFundRecord existing = mapper.selectByRequestNo(packageEntity.getStoreId(), request.getRequestNo()); - if (existing != null) { - TimiException.requiredTrue(existing.getBizId().equals(packageEntity.getId()) - && existing.getOperationType() == GaoFundRecord.OperationType.RECEIVE - && existing.getAmount().equals(request.getAmount()) - && (request.getOccurredAt() == null || request.getOccurredAt().equals(existing.getOccurredAt())) - && sameText(existing.getRemark(), request.getRemark()), "requestNo 已被其他资金操作使用"); - return get(existing.getId()); - } - TimiException.requiredTrue(hasAttachment(request.getAttachmentList()), "收款必须上传照片"); - checkPackageReceivable(packageEntity); - TimiException.requiredTrue(safeLong(packageEntity.getPaidAmount()) + safeLong(packageEntity.getPendingPaidAmount()) + request.getAmount() <= safeLong(packageEntity.getSaleAmount()), "累计收款不能超过成交价"); - GaoFundRecord record = newRecord(packageEntity, GaoFundRecord.OperationType.RECEIVE, request.getRequestNo(), request.getAmount(), request.getOccurredAt(), request.getRemark()); - record.setStatus(GaoFundRecord.Status.PENDING); - super.create(record); - TimiException.requiredTrue(0 < customerPackageService.updatePendingPaidAmountDelta(packageEntity.getId(), request.getAmount()), "待核销收款更新失败"); - attachmentService.updateByBizId(Attachment.BizType.GAO_FUND_RECORD, record.getId(), request.getAttachmentList()); - return record; - } - - @Transactional(TimiServerDBConfig.ROLLBACKER) @Override - public GaoFundRecord verify(String fundRecordId) { - return loggerService.execute(Logger.Module.GAO, "GAO_FUND_RECEIVE_VERIFY", fundRecordId, () -> verifyInternal(fundRecordId)); + public GaoFundRecord getRevokeBySourceRecordId(String sourceRecordId) { + TimiException.required(sourceRecordId, "not found sourceRecordId"); + return mapper.selectRevokeBySourceRecordId(sourceRecordId); } - private GaoFundRecord verifyInternal(String fundRecordId) { - GaoFundRecord record = mapper.selectForUpdate(fundRecordId); - TimiException.required(record, "not found fund record"); - storeScopeService.checkStoreId(record.getStoreId()); - TimiException.requiredTrue(record.getOperationType() == GaoFundRecord.OperationType.RECEIVE && record.getStatus() == GaoFundRecord.Status.PENDING, "收款记录不是待核销状态"); - GaoCustomerPackage packageEntity = lockPackage(record.getBizId()); - TimiException.requiredTrue(0 < customerPackageService.updatePendingPaidAmountDelta(packageEntity.getId(), -record.getAmount()), "待核销收款更新失败"); - TimiException.requiredTrue(0 < customerPackageService.updatePaidAmountDelta(packageEntity.getId(), record.getAmount()), "有效收款更新失败"); - String userId = userLoginService.getRequireLoginUserId(); - TimiException.requiredTrue(0 < mapper.updateStatusIf(record.getId(), GaoFundRecord.Status.PENDING, GaoFundRecord.Status.VERIFIED, userId, Time.now(), record.getReason()), "收款记录状态已变化"); - updatePaymentStatus(packageEntity, safeLong(packageEntity.getPaidAmount()) + record.getAmount(), safeLong(packageEntity.getPendingPaidAmount()) - record.getAmount()); - record.setStatus(GaoFundRecord.Status.VERIFIED); - record.setVerifierUserId(userId); - record.setVerifiedAt(Time.now()); - return record; - } - - @Transactional(TimiServerDBConfig.ROLLBACKER) @Override - public GaoFundRecord reject(GaoFundActionRequest request) { - return loggerService.execute(Logger.Module.GAO, "GAO_FUND_RECEIVE_REJECT", request, () -> releasePending(request, GaoFundRecord.Status.REJECTED)); - } - - @Transactional(TimiServerDBConfig.ROLLBACKER) - @Override - public GaoFundRecord cancel(GaoFundActionRequest request) { - return loggerService.execute(Logger.Module.GAO, "GAO_FUND_RECEIVE_CANCEL", request, () -> { - TimiException.required(request, "not found fund action request"); - TimiException.required(request.getFundRecordId(), "not found fundRecordId"); - GaoFundRecord record = mapper.selectForUpdate(request.getFundRecordId()); - TimiException.required(record, "not found fund record"); - TimiException.requiredTrue(userLoginService.getRequireLoginUserId().equals(record.getOperatorUserId()), "只能撤销自己提交的收款"); - return releasePending(request, GaoFundRecord.Status.CANCELLED); - }); - } - - @Transactional(TimiServerDBConfig.ROLLBACKER) - @Override - public GaoFundRecord refund(GaoFundRefundRequest request) { - return loggerService.execute(Logger.Module.GAO, "GAO_FUND_REFUND", request, () -> refundInternal(request)); - } - - private GaoFundRecord refundInternal(GaoFundRefundRequest request) { - TimiException.required(request, "not found refund request"); - TimiException.required(request.getRequestNo(), "not found requestNo"); - request.setRequestNo(request.getRequestNo().trim()); - TimiException.required(request.getCustomerPackageId(), "not found customerPackageId"); - TimiException.required(request.getAmount(), "not found amount"); - TimiException.requiredTrue(0 < request.getAmount(), "退款金额必须大于 0"); - TimiException.requiredTrue(hasReason(request.getReason()), "退款原因不能为空"); - GaoCustomerPackage packageEntity = lockPackage(request.getCustomerPackageId()); - GaoFundRecord existing = mapper.selectByRequestNo(packageEntity.getStoreId(), request.getRequestNo()); - if (existing != null) { - TimiException.requiredTrue(existing.getBizId().equals(packageEntity.getId()) - && existing.getOperationType() == GaoFundRecord.OperationType.REFUND - && existing.getAmount().equals(request.getAmount()) - && (request.getOccurredAt() == null || request.getOccurredAt().equals(existing.getOccurredAt())) - && sameText(existing.getReason(), request.getReason().trim()) - && sameText(existing.getRemark(), request.getRemark()), "requestNo 已被其他资金操作使用"); - return get(existing.getId()); - } - TimiException.requiredTrue(hasAttachment(request.getAttachmentList()), "退款必须上传照片"); - checkRefundable(packageEntity); - TimiException.requiredTrue(request.getAmount() <= safeLong(packageEntity.getPaidAmount()), "退款金额不能超过累计有效收款"); - GaoFundRecord record = newRecord(packageEntity, GaoFundRecord.OperationType.REFUND, request.getRequestNo(), request.getAmount(), request.getOccurredAt(), request.getRemark()); - record.setStatus(GaoFundRecord.Status.VERIFIED); - record.setReason(request.getReason().trim()); - super.create(record); - packageEntity.setRefundedAmount(request.getAmount()); - packageEntity.setRefundStatus(GaoCustomerPackage.RefundStatus.REFUNDED); - customerPackageService.updateAggregateState(packageEntity); - attachmentService.updateByBizId(Attachment.BizType.GAO_FUND_RECORD, record.getId(), request.getAttachmentList()); - return record; - } - - @Transactional(TimiServerDBConfig.ROLLBACKER) - @Override - public GaoFundRecord confirmNoRefund(GaoFundConfirmNoRefundRequest request) { - return loggerService.execute(Logger.Module.GAO, "GAO_FUND_CONFIRM_NO_REFUND", request, () -> confirmNoRefundInternal(request)); - } - - private GaoFundRecord confirmNoRefundInternal(GaoFundConfirmNoRefundRequest request) { - TimiException.required(request, "not found no refund request"); - TimiException.required(request.getRequestNo(), "not found requestNo"); - request.setRequestNo(request.getRequestNo().trim()); - TimiException.required(request.getCustomerPackageId(), "not found customerPackageId"); - TimiException.requiredTrue(hasReason(request.getReason()), "确认无需退款原因不能为空"); - GaoCustomerPackage packageEntity = lockPackage(request.getCustomerPackageId()); - GaoFundRecord existing = mapper.selectByRequestNo(packageEntity.getStoreId(), request.getRequestNo()); - if (existing != null) { - TimiException.requiredTrue(existing.getBizId().equals(packageEntity.getId()) - && existing.getOperationType() == GaoFundRecord.OperationType.CONFIRM_NO_REFUND - && (request.getOccurredAt() == null || request.getOccurredAt().equals(existing.getOccurredAt())) - && sameText(existing.getReason(), request.getReason().trim()) - && sameText(existing.getRemark(), request.getRemark()), "requestNo 已被其他资金操作使用"); - return get(existing.getId()); - } - checkRefundable(packageEntity); - GaoFundRecord record = newRecord(packageEntity, GaoFundRecord.OperationType.CONFIRM_NO_REFUND, request.getRequestNo(), 0L, request.getOccurredAt(), request.getRemark()); - record.setStatus(GaoFundRecord.Status.VERIFIED); - record.setReason(request.getReason().trim()); - super.create(record); - packageEntity.setRefundedAmount(0L); - packageEntity.setRefundStatus(GaoCustomerPackage.RefundStatus.REFUNDED); - customerPackageService.updateAggregateState(packageEntity); - return record; - } - - @Transactional(TimiServerDBConfig.ROLLBACKER) - @Override - public GaoFundRecord revoke(GaoFundRevokeRequest request) { - return loggerService.execute(Logger.Module.GAO, "GAO_FUND_REVOKE", request, () -> revokeInternal(request)); - } - - private GaoFundRecord revokeInternal(GaoFundRevokeRequest request) { - TimiException.required(request, "not found fund revoke request"); - TimiException.required(request.getRequestNo(), "not found requestNo"); - request.setRequestNo(request.getRequestNo().trim()); - TimiException.required(request.getFundRecordId(), "not found fundRecordId"); - TimiException.requiredTrue(hasReason(request.getReason()), "资金冲正原因不能为空"); - GaoFundRecord source = mapper.selectForUpdate(request.getFundRecordId()); - TimiException.required(source, "not found fund record"); - storeScopeService.checkStoreId(source.getStoreId()); - GaoFundRecord existing = mapper.selectByRequestNo(source.getStoreId(), request.getRequestNo()); - if (existing != null) { - TimiException.requiredTrue(existing.getOperationType() == GaoFundRecord.OperationType.REVOKE - && source.getId().equals(existing.getSourceRecordId()), "requestNo 已被其他资金操作使用"); - return get(existing.getId()); - } - TimiException.requiredTrue(hasAttachment(request.getAttachmentList()), "资金冲正必须上传照片"); - TimiException.requiredTrue(source.getStatus() == GaoFundRecord.Status.VERIFIED && source.getOperationType() != GaoFundRecord.OperationType.REVOKE, "只有已生效资金记录可以冲正"); - TimiException.requiredNull(mapper.selectRevokeBySourceRecordId(source.getId()), "该资金记录已经冲正"); - GaoCustomerPackage packageEntity = lockPackage(source.getBizId()); - if (source.getOperationType() == GaoFundRecord.OperationType.RECEIVE) { - TimiException.requiredTrue(packageEntity.getRefundStatus() != GaoCustomerPackage.RefundStatus.REFUNDED, "退款完成后不能冲正相关收款"); - long paidAfter = safeLong(packageEntity.getPaidAmount()) - source.getAmount(); - if (0 < deliveryService.countVerifiedByCustomerPackageId(packageEntity.getId())) { - TimiException.requiredTrue(packageEntity.getSaleAmount() <= paidAfter, "存在已核销发放,冲正收款后套餐仍须付清"); - } - TimiException.requiredTrue(0 < customerPackageService.updatePaidAmountDelta(packageEntity.getId(), -source.getAmount()), "有效收款冲正失败"); - packageEntity.setPaidAmount(paidAfter); - updatePaymentStatus(packageEntity, paidAfter, packageEntity.getPendingPaidAmount()); - } else { - TimiException.requiredTrue(packageEntity.getRefundStatus() == GaoCustomerPackage.RefundStatus.REFUNDED, "资金记录退款状态无效"); - packageEntity.setRefundedAmount(0L); - packageEntity.setRefundStatus(GaoCustomerPackage.RefundStatus.PENDING); - customerPackageService.updateAggregateState(packageEntity); - } - GaoFundRecord revoke = newRecord(packageEntity, GaoFundRecord.OperationType.REVOKE, request.getRequestNo(), source.getAmount(), request.getOccurredAt(), request.getRemark()); - revoke.setStatus(GaoFundRecord.Status.VERIFIED); - revoke.setSourceRecordId(source.getId()); - revoke.setReason(request.getReason().trim()); - super.create(revoke); - attachmentService.updateByBizId(Attachment.BizType.GAO_FUND_RECORD, revoke.getId(), request.getAttachmentList()); - return revoke; - } - - private GaoFundRecord releasePending(GaoFundActionRequest request, GaoFundRecord.Status targetStatus) { - TimiException.required(request, "not found fund action request"); - TimiException.required(request.getFundRecordId(), "not found fundRecordId"); - TimiException.requiredTrue(hasReason(request.getReason()), "拒绝或撤销原因不能为空"); - GaoFundRecord record = mapper.selectForUpdate(request.getFundRecordId()); - TimiException.required(record, "not found fund record"); - storeScopeService.checkStoreId(record.getStoreId()); - TimiException.requiredTrue(record.getOperationType() == GaoFundRecord.OperationType.RECEIVE && record.getStatus() == GaoFundRecord.Status.PENDING, "收款记录不是待核销状态"); - GaoCustomerPackage packageEntity = lockPackage(record.getBizId()); - TimiException.requiredTrue(0 < customerPackageService.updatePendingPaidAmountDelta(packageEntity.getId(), -record.getAmount()), "待核销收款更新失败"); - String userId = userLoginService.getRequireLoginUserId(); - TimiException.requiredTrue(0 < mapper.updateStatusIf(record.getId(), GaoFundRecord.Status.PENDING, targetStatus, userId, Time.now(), request.getReason().trim()), "收款记录状态已变化"); - record.setStatus(targetStatus); - record.setVerifierUserId(userId); - record.setVerifiedAt(Time.now()); - record.setReason(request.getReason().trim()); - return record; - } - - private GaoCustomerPackage lockPackage(String id) { - GaoCustomerPackage result = customerPackageService.getForUpdate(id); - TimiException.required(result, "not found customer package"); - storeScopeService.checkStoreId(result.getStoreId()); - return result; - } - - private void checkPackageReceivable(GaoCustomerPackage packageEntity) { - TimiException.requiredTrue(packageEntity.getStatus() != GaoCustomerPackage.Status.CANCELLED, "已取消套餐不能收款"); - TimiException.requiredTrue(packageEntity.getRefundStatus() != GaoCustomerPackage.RefundStatus.REFUNDED, "已完成退款套餐不能收款"); - } - - private void checkRefundable(GaoCustomerPackage packageEntity) { - TimiException.requiredTrue(packageEntity.getStatus() == GaoCustomerPackage.Status.CANCELLED, "只有已取消套餐可以退款"); - TimiException.requiredTrue(packageEntity.getRefundStatus() == GaoCustomerPackage.RefundStatus.PENDING, "当前套餐不在待退款状态"); - } - - private GaoFundRecord newRecord(GaoCustomerPackage packageEntity, GaoFundRecord.OperationType operationType, String requestNo, Long amount, Long occurredAt, String remark) { - GaoFundRecord record = new GaoFundRecord(); - record.setStoreId(packageEntity.getStoreId()); - record.setCustomerId(packageEntity.getCustomerId()); - record.setBizType(GaoFundRecord.BizType.CUSTOMER_PACKAGE); - record.setBizId(packageEntity.getId()); - record.setOperationType(operationType); - record.setAmount(amount); - record.setRequestNo(requestNo); - record.setOperatorUserId(userLoginService.getRequireLoginUserId()); - record.setOccurredAt(TimiJava.defaultIfNull(occurredAt, Time.now())); - record.setRemark(remark); - return record; - } - - private void updatePaymentStatus(GaoCustomerPackage packageEntity, long paidAmount, long pendingAmount) { - packageEntity.setPaidAmount(paidAmount); - packageEntity.setPendingPaidAmount(pendingAmount); - packageEntity.setPaymentStatus(safeLong(packageEntity.getSaleAmount()) == 0 || paidAmount >= safeLong(packageEntity.getSaleAmount()) - ? GaoCustomerPackage.PaymentStatus.PAID - : (0 < paidAmount ? GaoCustomerPackage.PaymentStatus.PARTIALLY_PAID : GaoCustomerPackage.PaymentStatus.UNPAID)); - customerPackageService.updateAggregateState(packageEntity); - } - - private boolean hasAttachment(List attachmentList) { - for (Attachment attachment : TimiJava.safeIterable(attachmentList)) { - if (attachment != null && (attachment.getAttachType() == null || "PHOTO".equalsIgnoreCase(attachment.getAttachType()))) { - return true; - } - } - return false; - } - - private boolean hasReason(String reason) { - return reason != null && !reason.trim().isEmpty(); - } - - private boolean sameText(String left, String right) { - return left == null ? right == null : left.equals(right); - } - - private long safeLong(Long value) { - return value == null ? 0L : value; + public int updateStatusIf(String id, GaoFundRecord.Status fromStatus, GaoFundRecord.Status toStatus, String verifierUserId, Long verifiedAt, String reason) { + TimiException.required(id, "not found fundRecordId"); + TimiException.required(fromStatus, "not found fromStatus"); + TimiException.required(toStatus, "not found toStatus"); + return mapper.updateStatusIf(id, fromStatus, toStatus, verifierUserId, verifiedAt, reason); } } diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoOrderFulfillmentServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoOrderFulfillmentServiceImplement.java new file mode 100644 index 0000000..9e8f786 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoOrderFulfillmentServiceImplement.java @@ -0,0 +1,695 @@ +package com.imyeyu.api.modules.gao.service.implement; + +import com.imyeyu.api.bean.ModuleCode; +import com.imyeyu.api.config.dbsource.TimiServerDBConfig; +import com.imyeyu.api.modules.common.entity.Attachment; +import com.imyeyu.api.modules.common.service.AttachmentService; +import com.imyeyu.api.modules.gao.bean.GaoRoleCode; +import com.imyeyu.api.modules.gao.entity.GaoCustomerPackage; +import com.imyeyu.api.modules.gao.entity.GaoCustomerPackageItem; +import com.imyeyu.api.modules.gao.entity.GaoProduct; +import com.imyeyu.api.modules.gao.entity.GaoProductUnit; +import com.imyeyu.api.modules.gao.entity.GaoSalesOrder; +import com.imyeyu.api.modules.gao.entity.GaoSalesOrderItem; +import com.imyeyu.api.modules.gao.entity.GaoStockRecord; +import com.imyeyu.api.modules.gao.entity.GaoOrderFulfillment; +import com.imyeyu.api.modules.gao.entity.GaoOrderFulfillmentItem; +import com.imyeyu.api.modules.gao.mapper.GaoOrderFulfillmentItemMapper; +import com.imyeyu.api.modules.gao.mapper.GaoOrderFulfillmentMapper; +import com.imyeyu.api.modules.gao.service.GaoCustomerPackageService; +import com.imyeyu.api.modules.gao.service.GaoOrderFulfillmentService; +import com.imyeyu.api.modules.gao.service.GaoProductService; +import com.imyeyu.api.modules.gao.service.GaoProductStockService; +import com.imyeyu.api.modules.gao.service.GaoProductUnitService; +import com.imyeyu.api.modules.gao.service.GaoSalesOrderService; +import com.imyeyu.api.modules.gao.service.GaoStockRecordService; +import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; +import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentActionRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentCreateRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentItemRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentPage; +import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentPrepareStockRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentRevokeRequest; +import com.imyeyu.api.modules.gao.vo.GaoStockChangeItem; +import com.imyeyu.api.modules.system.entity.Logger; +import com.imyeyu.api.modules.system.service.LoggerService; +import com.imyeyu.api.modules.user.service.RoleChecker; +import com.imyeyu.api.modules.user.service.UserLoginService; +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.mapper.BaseMapper; +import com.imyeyu.spring.service.AbstractEntityService; +import com.imyeyu.utils.Time; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +/// GAO 统一履约服务实现 +/// +/// @author Codex +/// @since 2026-09-02 +@Service +@RequiredArgsConstructor +public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService implements GaoOrderFulfillmentService { + + private static final BigDecimal ZERO = BigDecimal.ZERO; + + private final GaoOrderFulfillmentMapper mapper; + private final GaoOrderFulfillmentItemMapper itemMapper; + private final GaoSalesOrderService salesOrderService; + private final GaoCustomerPackageService customerPackageService; + private final GaoProductService productService; + private final GaoProductUnitService unitService; + private final GaoProductStockService productStockService; + private final GaoStockRecordService stockRecordService; + private final GaoStoreScopeService storeScopeService; + private final RoleChecker roleChecker; + private final UserLoginService userLoginService; + private final AttachmentService attachmentService; + private final LoggerService loggerService; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + /// 履约单必须通过专用流程创建 + @Override + public void create(GaoOrderFulfillment fulfillment) { + throw new TimiException(TimiCode.ERROR, "履约单必须通过专用接口创建"); + } + + /// 履约单不允许通用修改 + @Override + public void update(GaoOrderFulfillment fulfillment) { + throw new TimiException(TimiCode.ERROR, "履约单不允许通用修改"); + } + + /// 履约单不允许删除 + @Override + public void delete(String id) { + throw new TimiException(TimiCode.ERROR, "履约单不允许删除"); + } + + /// 履约单不允许物理删除 + @Override + public void destroy(String id) { + throw new TimiException(TimiCode.ERROR, "履约单不允许物理删除"); + } + + @Override + public PageResult pageByQuery(GaoOrderFulfillmentPage page) { + TimiException.required(page, "not found fulfillment page"); + PageResult result = new PageResult<>(); + result.setTotal(mapper.countByQuery(page)); + List list = mapper.selectByQuery(page); + fillItemList(list); + result.setList(list); + return result; + } + + @Override + public GaoOrderFulfillment detail(String id) { + GaoOrderFulfillment fulfillment = mapper.selectById(id); + TimiException.required(fulfillment, "not found fulfillment"); + storeScopeService.checkStoreId(fulfillment.getStoreId()); + fulfillment.setItemList(itemMapper.selectByFulfillmentId(fulfillment.getId())); + fulfillment.setStockRecordList(stockRecordService.listByBiz(GaoStockRecord.BizType.ORDER_FULFILLMENT, fulfillment.getId())); + return fulfillment; + } + + @Override + public List listBySalesOrderId(String salesOrderId) { + TimiException.required(salesOrderId, "not found salesOrderId"); + List list = mapper.selectBySalesOrderId(salesOrderId); + fillItemList(list); + return list; + } + + @Override + public long countPendingBySalesOrderId(String salesOrderId) { + TimiException.required(salesOrderId, "not found salesOrderId"); + return mapper.countPendingBySalesOrderId(salesOrderId); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoOrderFulfillment create(GaoOrderFulfillmentCreateRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_ORDER_FULFILLMENT_CREATE", request, () -> createInternal(request)); + } + + private GaoOrderFulfillment createInternal(GaoOrderFulfillmentCreateRequest request) { + TimiException.required(request, "not found fulfillment request"); + String requestNo = requiredText(request.getRequestNo(), "not found requestNo"); + TimiException.required(request.getSalesOrderId(), "not found salesOrderId"); + GaoSalesOrder order = salesOrderService.getForUpdate(request.getSalesOrderId()); + GaoOrderFulfillment existing = mapper.selectByRequestNo(order.getStoreId(), requestNo); + if (existing != null) { + verifyIdempotency(existing, request); + return detail(existing.getId()); + } + TimiException.requiredTrue(hasAttachment(request.getAttachmentList()), "履约必须上传照片"); + TimiException.requiredTrue(order.getStatus() != GaoSalesOrder.Status.CANCELLED, "已取消订单不能履约"); + salesOrderService.checkFulfillmentPermission(order); + GaoCustomerPackage packageEntity = lockPackage(order, true); + List sourceList = loadSources(order, packageEntity, requestItems(request.getItemList()), true); + List stockItems = buildStockItems(order.getStoreId(), sourceList); + boolean stockReady = stockItems.isEmpty() || productStockService.hasSufficientForFulfillment(order.getStoreId(), stockItems); + String operatorUserId = userLoginService.getRequireLoginUserId(); + GaoOrderFulfillment fulfillment = newFulfillment(order, sourceList.getFirst().packageSource() + ? GaoOrderFulfillment.FulfillmentType.PACKAGE_ENTITLEMENT : GaoOrderFulfillment.FulfillmentType.DIRECT_PRODUCT, + stockReady ? GaoOrderFulfillment.Status.PENDING : GaoOrderFulfillment.Status.WAITING_STOCK, + requestNo, operatorUserId, TimiJava.defaultIfNull(request.getDeliveredAt(), Time.now()), request.getRemark()); + super.create(fulfillment); + insertSourceReservations(fulfillment, sourceList); + if (stockReady && !stockItems.isEmpty()) { + productStockService.reserveForFulfillment(order.getStoreId(), fulfillment.getId(), stockItems, + stockRequestNo(fulfillment, "RESERVE"), operatorUserId); + } + attachmentService.updateByBizId(Attachment.BizType.GAO_ORDER_FULFILLMENT, fulfillment.getId(), request.getAttachmentList()); + return detail(fulfillment.getId()); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoOrderFulfillment prepareStock(GaoOrderFulfillmentPrepareStockRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_ORDER_FULFILLMENT_PREPARE_STOCK", request, () -> prepareStockInternal(request)); + } + + private GaoOrderFulfillment prepareStockInternal(GaoOrderFulfillmentPrepareStockRequest request) { + TimiException.required(request, "not found prepare stock request"); + TimiException.required(request.getFulfillmentId(), "not found fulfillmentId"); + roleChecker.checkAny(ModuleCode.GAO, GaoRoleCode.STORE_MANAGER.name()); + GaoOrderFulfillment fulfillment = mapper.selectForUpdate(request.getFulfillmentId()); + TimiException.required(fulfillment, "not found fulfillment"); + storeScopeService.checkStoreId(fulfillment.getStoreId()); + TimiException.requiredTrue(fulfillment.getStatus() == GaoOrderFulfillment.Status.WAITING_STOCK, "履约单不是等待库存状态"); + GaoSalesOrder order = salesOrderService.getForUpdate(fulfillment.getSalesOrderId()); + TimiException.requiredTrue(order.getStatus() != GaoSalesOrder.Status.CANCELLED, "已取消订单不能准备库存"); + salesOrderService.checkFulfillmentPermission(order); + GaoCustomerPackage packageEntity = lockPackage(order, false); + List sourceList = loadSources(order, packageEntity, copyRequestItems(itemMapper.selectByFulfillmentIdForUpdate(fulfillment.getId())), false); + List stockItems = buildStockItems(order.getStoreId(), sourceList); + TimiException.requiredTrue(!stockItems.isEmpty(), "履约单没有需要占用的实物库存"); + TimiException.requiredTrue(productStockService.hasSufficientForFulfillment(order.getStoreId(), stockItems), "库存仍然不足,不能准备履约"); + String operatorUserId = userLoginService.getRequireLoginUserId(); + productStockService.reserveForFulfillment(order.getStoreId(), fulfillment.getId(), stockItems, + stockRequestNo(fulfillment, "RESERVE"), operatorUserId); + TimiException.requiredTrue(1 == mapper.updateStatus(fulfillment.getId(), GaoOrderFulfillment.Status.PENDING, null, null, request.getReason()), "履约状态已变化"); + if (request.getAttachmentList() != null && !request.getAttachmentList().isEmpty()) { + attachmentService.updateByBizId(Attachment.BizType.GAO_ORDER_FULFILLMENT, fulfillment.getId(), request.getAttachmentList()); + } + return detail(fulfillment.getId()); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoOrderFulfillment verify(GaoOrderFulfillmentActionRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_ORDER_FULFILLMENT_VERIFY", request, () -> verifyInternal(request)); + } + + private GaoOrderFulfillment verifyInternal(GaoOrderFulfillmentActionRequest request) { + TimiException.required(request, "not found verify request"); + TimiException.required(request.getFulfillmentId(), "not found fulfillmentId"); + GaoOrderFulfillment fulfillment = mapper.selectForUpdate(request.getFulfillmentId()); + TimiException.required(fulfillment, "not found fulfillment"); + storeScopeService.checkStoreId(fulfillment.getStoreId()); + TimiException.requiredTrue(fulfillment.getStatus() == GaoOrderFulfillment.Status.PENDING, "履约单不是待核销状态"); + GaoSalesOrder order = salesOrderService.getForUpdate(fulfillment.getSalesOrderId()); + TimiException.requiredTrue(order.getStatus() != GaoSalesOrder.Status.CANCELLED, "已取消订单不能核销履约"); + salesOrderService.checkFulfillmentPermission(order); + GaoCustomerPackage packageEntity = lockPackage(order, false); + List fulfillmentItemList = itemMapper.selectByFulfillmentIdForUpdate(fulfillment.getId()); + List sourceList = loadSources(order, packageEntity, copyRequestItems(fulfillmentItemList), false); + assertReserved(sourceList); + for (SourceEntry source : sourceList) { + decreaseReserved(source); + increaseFulfilled(source); + } + List stockItems = buildStockItems(order.getStoreId(), sourceList); + String verifierUserId = userLoginService.getRequireLoginUserId(); + if (!stockItems.isEmpty()) { + productStockService.outForFulfillment(order.getStoreId(), fulfillment.getId(), stockItems, + stockRequestNo(fulfillment, "OUT"), verifierUserId); + } + TimiException.requiredTrue(1 == mapper.updateStatus(fulfillment.getId(), GaoOrderFulfillment.Status.VERIFIED, + verifierUserId, Time.now(), request.getReason()), "履约状态已变化"); + refreshAggregateStatus(order, packageEntity); + return detail(fulfillment.getId()); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoOrderFulfillment reject(GaoOrderFulfillmentActionRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_ORDER_FULFILLMENT_REJECT", request, () -> releaseInternal(request, GaoOrderFulfillment.Status.REJECTED)); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoOrderFulfillment cancel(GaoOrderFulfillmentActionRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_ORDER_FULFILLMENT_CANCEL", request, () -> cancelInternal(request)); + } + + private GaoOrderFulfillment cancelInternal(GaoOrderFulfillmentActionRequest request) { + TimiException.required(request, "not found cancel request"); + GaoOrderFulfillment fulfillment = mapper.selectForUpdate(request.getFulfillmentId()); + TimiException.required(fulfillment, "not found fulfillment"); + String loginUserId = userLoginService.getRequireLoginUserId(); + TimiException.requiredTrue(loginUserId.equals(fulfillment.getOperatorUserId()), "只能撤销自己提交的履约"); + return releaseInternal(request, GaoOrderFulfillment.Status.CANCELLED); + } + + private GaoOrderFulfillment releaseInternal(GaoOrderFulfillmentActionRequest request, GaoOrderFulfillment.Status targetStatus) { + TimiException.required(request, "not found action request"); + TimiException.required(request.getFulfillmentId(), "not found fulfillmentId"); + String reason = requiredText(request.getReason(), "拒绝或撤销原因不能为空"); + GaoOrderFulfillment fulfillment = mapper.selectForUpdate(request.getFulfillmentId()); + TimiException.required(fulfillment, "not found fulfillment"); + storeScopeService.checkStoreId(fulfillment.getStoreId()); + TimiException.requiredTrue(fulfillment.getStatus() == GaoOrderFulfillment.Status.WAITING_STOCK + || fulfillment.getStatus() == GaoOrderFulfillment.Status.PENDING, "履约单不是可释放状态"); + GaoSalesOrder order = salesOrderService.getForUpdate(fulfillment.getSalesOrderId()); + GaoCustomerPackage packageEntity = lockPackage(order, false); + List sourceList = loadSources(order, packageEntity, copyRequestItems(itemMapper.selectByFulfillmentIdForUpdate(fulfillment.getId())), false); + if (fulfillment.getStatus() == GaoOrderFulfillment.Status.PENDING) { + List stockItems = buildStockItems(order.getStoreId(), sourceList); + if (!stockItems.isEmpty()) { + productStockService.releaseForFulfillment(order.getStoreId(), fulfillment.getId(), stockItems, + stockRequestNo(fulfillment, "RELEASE"), userLoginService.getRequireLoginUserId()); + } + } + for (SourceEntry source : sourceList) { + decreaseReserved(source); + } + String operatorUserId = userLoginService.getRequireLoginUserId(); + TimiException.requiredTrue(1 == mapper.updateStatus(fulfillment.getId(), targetStatus, operatorUserId, Time.now(), reason), "履约状态已变化"); + return detail(fulfillment.getId()); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoOrderFulfillment revoke(GaoOrderFulfillmentRevokeRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_ORDER_FULFILLMENT_REVOKE", request, () -> revokeInternal(request)); + } + + private GaoOrderFulfillment revokeInternal(GaoOrderFulfillmentRevokeRequest request) { + TimiException.required(request, "not found revoke request"); + String requestNo = requiredText(request.getRequestNo(), "not found requestNo"); + TimiException.required(request.getFulfillmentId(), "not found fulfillmentId"); + String reason = requiredText(request.getReason(), "冲正原因不能为空"); + GaoOrderFulfillment source = mapper.selectForUpdate(request.getFulfillmentId()); + TimiException.required(source, "not found fulfillment"); + storeScopeService.checkStoreId(source.getStoreId()); + GaoOrderFulfillment existing = mapper.selectByRequestNo(source.getStoreId(), requestNo); + if (existing != null) { + TimiException.requiredTrue(source.getId().equals(existing.getSourceFulfillmentId()), "requestNo 已被其他履约操作使用"); + return detail(existing.getId()); + } + TimiException.requiredTrue(source.getStatus() == GaoOrderFulfillment.Status.VERIFIED && source.getSourceFulfillmentId() == null, "只有原始已核销履约可以冲正"); + TimiException.requiredNull(mapper.selectBySourceFulfillmentId(source.getId()), "该履约单已经冲正"); + GaoSalesOrder order = salesOrderService.getForUpdate(source.getSalesOrderId()); + GaoCustomerPackage packageEntity = lockPackage(order, false); + List sourceItemList = itemMapper.selectByFulfillmentIdForUpdate(source.getId()); + List sourceList = loadSources(order, packageEntity, copyRequestItems(sourceItemList), false); + List stockItems = buildStockItems(order.getStoreId(), sourceList); + if (!stockItems.isEmpty()) { + TimiException.requiredTrue(Boolean.TRUE.equals(request.getReturnedToStore()), "实物履约冲正必须确认已退回门店"); + TimiException.requiredTrue(hasAttachment(request.getAttachmentList()), "实物履约冲正必须上传退回照片"); + } + String operatorUserId = userLoginService.getRequireLoginUserId(); + GaoOrderFulfillment reverse = newFulfillment(order, source.getFulfillmentType(), GaoOrderFulfillment.Status.VERIFIED, + requestNo, operatorUserId, TimiJava.defaultIfNull(request.getDeliveredAt(), Time.now()), request.getRemark()); + reverse.setVerifierUserId(operatorUserId); + reverse.setVerifiedAt(Time.now()); + reverse.setSourceFulfillmentId(source.getId()); + reverse.setReason(reason); + super.create(reverse); + for (int i = 0; i < sourceList.size(); i++) { + SourceEntry sourceEntry = sourceList.get(i); + decreaseFulfilled(sourceEntry); + GaoOrderFulfillmentItem sourceItem = sourceItemList.get(i); + GaoOrderFulfillmentItem reverseItem = copyFulfillmentItem(sourceItem); + reverseItem.setFulfillmentId(reverse.getId()); + reverseItem.setReservedBefore(sourceEntry.reservedBefore()); + reverseItem.setReservedAfter(sourceEntry.reservedBefore()); + reverseItem.setFulfilledBefore(sourceEntry.fulfilledBefore()); + reverseItem.setFulfilledAfter(sourceEntry.fulfilledBefore().subtract(sourceEntry.quantity())); + itemMapper.insert(reverseItem); + } + if (!stockItems.isEmpty()) { + for (GaoStockChangeItem item : stockItems) { + GaoStockRecord stockRecord = stockRecordService.getByBizProduct(GaoStockRecord.BizType.ORDER_FULFILLMENT, + source.getId(), item.getProductId(), GaoStockRecord.OperationType.ORDER_OUT); + if (stockRecord != null) { + item.setSourceRecordId(stockRecord.getId()); + } + } + productStockService.revokeForFulfillment(order.getStoreId(), reverse.getId(), stockItems, + stockRequestNo(reverse, "REVOKE"), operatorUserId); + } + TimiException.requiredTrue(1 == mapper.updateStatus(source.getId(), GaoOrderFulfillment.Status.REVOKED, + operatorUserId, Time.now(), reason), "原履约状态已变化"); + attachmentService.updateByBizId(Attachment.BizType.GAO_ORDER_FULFILLMENT, reverse.getId(), request.getAttachmentList()); + refreshAggregateStatus(order, packageEntity); + return detail(reverse.getId()); + } + + private GaoOrderFulfillment newFulfillment(GaoSalesOrder order, GaoOrderFulfillment.FulfillmentType type, + GaoOrderFulfillment.Status status, String requestNo, String operatorUserId, Long deliveredAt, String remark) { + GaoOrderFulfillment result = new GaoOrderFulfillment(); + result.setSalesOrderId(order.getId()); + result.setStoreId(order.getStoreId()); + result.setCustomerId(order.getCustomerId()); + result.setFulfillmentType(type); + result.setStatus(status); + result.setRequestNo(requestNo); + result.setOperatorUserId(operatorUserId); + result.setDeliveredAt(deliveredAt); + result.setRemark(remark); + return result; + } + + private void insertSourceReservations(GaoOrderFulfillment fulfillment, List sourceList) { + for (SourceEntry source : sourceList) { + if (source.packageSource()) { + TimiException.requiredTrue(1 == customerPackageService.updateItemReservedQuantity(source.sourceId(), source.quantity()), "套餐权益占用失败"); + } else { + TimiException.requiredTrue(1 == salesOrderService.updateItemReservedQuantity(source.sourceId(), source.quantity()), "订单明细占用失败"); + } + GaoOrderFulfillmentItem item = source.toFulfillmentItem(fulfillment.getId()); + itemMapper.insert(item); + } + } + + private void decreaseReserved(SourceEntry source) { + if (source.packageSource()) { + TimiException.requiredTrue(1 == customerPackageService.updateItemReservedQuantity(source.sourceId(), source.quantity().negate()), "套餐权益占用释放失败"); + } else { + TimiException.requiredTrue(1 == salesOrderService.updateItemReservedQuantity(source.sourceId(), source.quantity().negate()), "订单明细占用释放失败"); + } + } + + private void increaseFulfilled(SourceEntry source) { + if (source.packageSource()) { + TimiException.requiredTrue(1 == customerPackageService.updateItemFulfilledQuantity(source.sourceId(), source.quantity()), "套餐权益履约失败"); + } else { + TimiException.requiredTrue(1 == salesOrderService.updateItemFulfilledQuantity(source.sourceId(), source.quantity()), "订单明细履约失败"); + } + } + + private void decreaseFulfilled(SourceEntry source) { + TimiException.requiredTrue(0 <= source.fulfilledBefore().compareTo(source.quantity()), "已履约数量不足以冲正"); + if (source.packageSource()) { + TimiException.requiredTrue(1 == customerPackageService.updateItemFulfilledQuantity(source.sourceId(), source.quantity().negate()), "套餐权益冲正失败"); + } else { + TimiException.requiredTrue(1 == salesOrderService.updateItemFulfilledQuantity(source.sourceId(), source.quantity().negate()), "订单明细冲正失败"); + } + } + + private void assertReserved(List sourceList) { + for (SourceEntry source : sourceList) { + TimiException.requiredTrue(0 <= source.reservedBefore().compareTo(source.quantity()), "履约待占用数量不足"); + } + } + + private List loadSources(GaoSalesOrder order, GaoCustomerPackage packageEntity, + List requestItems, boolean checkAvailable) { + TimiException.requiredTrue(requestItems != null && !requestItems.isEmpty(), "履约至少需要一个产品"); + Set sourceIdSet = new HashSet<>(); + List result = new ArrayList<>(); + for (GaoOrderFulfillmentItemRequest requestItem : requestItems) { + TimiException.required(requestItem, "履约明细不能为空"); + boolean hasOrderSource = hasText(requestItem.getSourceOrderItemId()); + boolean hasPackageSource = hasText(requestItem.getSourceCustomerPackageItemId()); + TimiException.requiredTrue(hasOrderSource ^ hasPackageSource, "履约明细来源必须二选一"); + String sourceId = hasOrderSource ? requestItem.getSourceOrderItemId() : requestItem.getSourceCustomerPackageItemId(); + TimiException.requiredTrue(sourceIdSet.add(sourceId), "同一履约单不能重复来源明细"); + TimiException.required(requestItem.getQuantity(), "履约数量不能为空"); + TimiException.requiredTrue(0 < requestItem.getQuantity().signum(), "履约数量必须大于 0"); + TimiException.requiredTrue(3 >= requestItem.getQuantity().stripTrailingZeros().scale(), "履约数量最多保留三位小数"); + if (order.getOrderType() == GaoSalesOrder.OrderType.DIRECT_PRODUCT) { + TimiException.requiredTrue(hasOrderSource && !hasPackageSource, "直接产品订单只能引用订单明细"); + GaoSalesOrderItem source = salesOrderService.getItemForUpdate(sourceId); + TimiException.required(source, "not found sales order item"); + TimiException.requiredTrue(order.getId().equals(source.getSalesOrderId()), "订单明细不属于当前订单"); + result.add(toSource(order, source, requestItem.getQuantity(), false, checkAvailable)); + } else { + TimiException.requiredTrue(hasPackageSource && !hasOrderSource, "套餐订单只能引用客户套餐权益"); + TimiException.required(packageEntity, "订单未找到客户套餐权益"); + GaoCustomerPackageItem source = customerPackageService.getItemForUpdate(sourceId); + TimiException.required(source, "not found customer package item"); + TimiException.requiredTrue(packageEntity.getId().equals(source.getCustomerPackageId()), "套餐权益不属于当前订单"); + result.add(toSource(order, source, requestItem.getQuantity(), true, checkAvailable)); + } + } + return result; + } + + private SourceEntry toSource(GaoSalesOrder order, GaoSalesOrderItem source, BigDecimal quantity, boolean packageSource, boolean checkAvailable) { + GaoProduct product = productService.get(source.getProductId()); + TimiException.required(product, "履约来源产品不存在"); + TimiException.requiredTrue(order.getStoreId().equals(product.getStoreId()), "履约来源产品不属于当前门店"); + BigDecimal conversion = requiredConversion(source.getConversionToBaseSnapshot()); + GaoProductUnit unit = snapshotUnit(source.getProductUnitId(), source.getUnitSnapshot(), conversion); + return buildSource(order, source.getId(), null, source.getProductId(), source.getProductUnitId(), source.getProductNameSnapshot(), + source.getUnitSnapshot(), conversion, quantity, source.getQuantity(), source.getReservedQuantity(), source.getFulfilledQuantity(), + product, unit, packageSource, checkAvailable); + } + + private SourceEntry toSource(GaoSalesOrder order, GaoCustomerPackageItem source, BigDecimal quantity, boolean packageSource, boolean checkAvailable) { + TimiException.required(source.getProductId(), "套餐权益缺少产品快照,不能履约"); + TimiException.required(source.getProductUnitId(), "套餐权益缺少单位快照,不能履约"); + GaoProduct product = productService.get(source.getProductId()); + TimiException.required(product, "履约来源产品不存在"); + TimiException.requiredTrue(order.getStoreId().equals(product.getStoreId()), "履约来源产品不属于当前门店"); + BigDecimal conversion = requiredConversion(source.getConversionToBaseSnapshot()); + GaoProductUnit unit = snapshotUnit(source.getProductUnitId(), source.getUnitSnapshot(), conversion); + return buildSource(order, null, source.getId(), source.getProductId(), source.getProductUnitId(), source.getProductNameSnapshot(), + source.getUnitSnapshot(), conversion, quantity, source.getTotalQuantity(), source.getReservedQuantity(), fulfilledQuantity(source), + product, unit, packageSource, checkAvailable); + } + + private SourceEntry buildSource(GaoSalesOrder order, String orderItemId, String packageItemId, String productId, String unitId, + String productName, String unitName, BigDecimal conversion, BigDecimal quantity, BigDecimal totalQuantity, + BigDecimal reservedQuantity, BigDecimal fulfilledQuantity, GaoProduct product, GaoProductUnit unit, + boolean packageSource, boolean checkAvailable) { + if (!Boolean.TRUE.equals(product.getAllowDecimalQuantity())) { + TimiException.requiredTrue(quantity.stripTrailingZeros().scale() <= 0, "该产品不允许小数数量"); + } + BigDecimal reservedBefore = decimal(reservedQuantity); + BigDecimal fulfilledBefore = decimal(fulfilledQuantity); + if (checkAvailable) { + BigDecimal available = decimal(totalQuantity).subtract(reservedBefore).subtract(fulfilledBefore); + TimiException.requiredTrue(0 <= available.compareTo(quantity), "来源产品可履约数量不足"); + } + return new SourceEntry(orderItemId, packageItemId, productId, unitId, productName, unitName, conversion, + quantity, decimal(totalQuantity), reservedBefore, fulfilledBefore, product, unit, packageSource); + } + + private List buildStockItems(String storeId, List sourceList) { + Map itemMap = new LinkedHashMap<>(); + for (SourceEntry source : sourceList) { + if (!Boolean.TRUE.equals(source.product().getStockEnabled())) { + continue; + } + GaoProductUnit baseUnit = productService.listUnitByProductId(source.product().getId()).stream() + .filter(unit -> Boolean.TRUE.equals(unit.getIsBase())).findFirst().orElse(null); + TimiException.required(baseUnit, "实物产品缺少基础库存单位"); + BigDecimal baseQuantity = unitService.convertToBase(source.unit(), source.quantity()); + GaoStockChangeItem item = itemMap.get(source.product().getId()); + if (item == null) { + item = GaoStockChangeItem.of(source.product(), baseUnit, baseQuantity, baseQuantity); + itemMap.put(source.product().getId(), item); + } else { + item.setQuantity(item.getQuantity().add(baseQuantity)); + item.setBaseQuantity(item.getBaseQuantity().add(baseQuantity)); + } + } + return itemMap.values().stream().sorted(Comparator.comparing(GaoStockChangeItem::getProductId)).toList(); + } + + private GaoCustomerPackage lockPackage(GaoSalesOrder order, boolean requireActive) { + if (order.getOrderType() != GaoSalesOrder.OrderType.PACKAGE) { + return null; + } + GaoCustomerPackage source = customerPackageService.getBySalesOrderId(order.getId()); + TimiException.required(source, "订单未找到客户套餐权益"); + GaoCustomerPackage result = customerPackageService.getForUpdate(source.getId()); + TimiException.required(result, "not found customer package"); + if (requireActive) { + TimiException.requiredTrue(result.getStatus() == GaoCustomerPackage.Status.ACTIVE, "当前套餐不可履约"); + } else { + TimiException.requiredTrue(result.getStatus() != GaoCustomerPackage.Status.CANCELLED, "已取消套餐不能履约"); + } + return result; + } + + private BigDecimal fulfilledQuantity(GaoCustomerPackageItem item) { + return item.getFulfilledQuantity(); + } + + private void refreshAggregateStatus(GaoSalesOrder order, GaoCustomerPackage packageEntity) { + if (packageEntity != null) { + List itemList = customerPackageService.listItemByCustomerPackageId(packageEntity.getId()); + boolean fulfilled = !itemList.isEmpty(); + for (GaoCustomerPackageItem item : itemList) { + if (decimal(fulfilledQuantity(item)).compareTo(decimal(item.getTotalQuantity())) < 0) { + fulfilled = false; + break; + } + } + if (fulfilled && packageEntity.getStatus() == GaoCustomerPackage.Status.ACTIVE) { + packageEntity.setStatus(GaoCustomerPackage.Status.FULFILLED); + customerPackageService.updateAggregateState(packageEntity); + } else if (!fulfilled && packageEntity.getStatus() == GaoCustomerPackage.Status.FULFILLED) { + packageEntity.setStatus(GaoCustomerPackage.Status.ACTIVE); + customerPackageService.updateAggregateState(packageEntity); + } + salesOrderService.updateFulfillmentStatus(order, fulfilled); + } else { + salesOrderService.refreshFulfillmentStatus(order); + } + } + + private List requestItems(List itemList) { + return itemList; + } + + private List copyRequestItems(List itemList) { + List result = new ArrayList<>(); + for (GaoOrderFulfillmentItem item : TimiJava.safeIterable(itemList)) { + GaoOrderFulfillmentItemRequest request = new GaoOrderFulfillmentItemRequest(); + request.setSourceOrderItemId(item.getSourceOrderItemId()); + request.setSourceCustomerPackageItemId(item.getSourceCustomerPackageItemId()); + request.setQuantity(item.getQuantity()); + result.add(request); + } + return result; + } + + private GaoOrderFulfillmentItem copyFulfillmentItem(GaoOrderFulfillmentItem source) { + GaoOrderFulfillmentItem result = new GaoOrderFulfillmentItem(); + result.setProductId(source.getProductId()); + result.setProductUnitId(source.getProductUnitId()); + result.setSourceOrderItemId(source.getSourceOrderItemId()); + result.setSourceCustomerPackageItemId(source.getSourceCustomerPackageItemId()); + result.setQuantity(source.getQuantity()); + result.setBaseQuantity(source.getBaseQuantity()); + result.setProductNameSnapshot(source.getProductNameSnapshot()); + result.setUnitSnapshot(source.getUnitSnapshot()); + result.setConversionToBaseSnapshot(source.getConversionToBaseSnapshot()); + return result; + } + + private GaoProductUnit snapshotUnit(String id, String name, BigDecimal conversion) { + GaoProductUnit unit = new GaoProductUnit(); + unit.setId(id); + unit.setName(name); + unit.setConversionToBase(conversion); + return unit; + } + + private BigDecimal requiredConversion(BigDecimal value) { + TimiException.required(value, "履约来源缺少折算率快照"); + TimiException.requiredTrue(0 < value.signum(), "履约来源折算率无效"); + return value; + } + + private void verifyIdempotency(GaoOrderFulfillment existing, GaoOrderFulfillmentCreateRequest request) { + TimiException.requiredTrue(existing.getSalesOrderId().equals(request.getSalesOrderId()) + && (request.getDeliveredAt() == null || request.getDeliveredAt().equals(existing.getDeliveredAt())) + && sameText(existing.getRemark(), request.getRemark()), "requestNo 对应履约请求内容不一致"); + List existingItems = itemMapper.selectByFulfillmentId(existing.getId()); + List requestItems = requestItems(request.getItemList()); + TimiException.requiredTrue(existingItems.size() == (requestItems == null ? 0 : requestItems.size()), "requestNo 对应履约请求内容不一致"); + Map requestMap = new HashMap<>(); + for (GaoOrderFulfillmentItemRequest item : TimiJava.safeIterable(requestItems)) { + String key = hasText(item.getSourceOrderItemId()) ? "O:" + item.getSourceOrderItemId() : "P:" + item.getSourceCustomerPackageItemId(); + requestMap.put(key, item.getQuantity()); + } + for (GaoOrderFulfillmentItem item : existingItems) { + String key = item.getSourceOrderItemId() == null ? "P:" + item.getSourceCustomerPackageItemId() : "O:" + item.getSourceOrderItemId(); + TimiException.requiredTrue(item.getQuantity().compareTo(requestMap.getOrDefault(key, ZERO)) == 0, "requestNo 对应履约请求内容不一致"); + } + } + + private String stockRequestNo(GaoOrderFulfillment fulfillment, String operation) { + return "FULFILLMENT-%s-%s".formatted(fulfillment.getId(), operation); + } + + private boolean hasAttachment(List attachmentList) { + for (Attachment attachment : TimiJava.safeIterable(attachmentList)) { + if (attachment != null && (attachment.getAttachType() == null || "PHOTO".equalsIgnoreCase(attachment.getAttachType()))) { + return true; + } + } + return false; + } + + private boolean hasText(String value) { + return value != null && !value.trim().isEmpty(); + } + + private String requiredText(String value, String message) { + TimiException.required(value, message); + String result = value.trim(); + TimiException.requiredTrue(!result.isEmpty(), message); + return result; + } + + private boolean sameText(String left, String right) { + return left == null ? right == null : left.equals(right); + } + + private BigDecimal decimal(BigDecimal value) { + return value == null ? ZERO : value; + } + + private void fillItemList(List list) { + for (GaoOrderFulfillment fulfillment : TimiJava.safeIterable(list)) { + fulfillment.setItemList(itemMapper.selectByFulfillmentId(fulfillment.getId())); + } + } + + private record SourceEntry(String sourceOrderItemId, String sourceCustomerPackageItemId, String productId, String productUnitId, + String productNameSnapshot, String unitSnapshot, BigDecimal conversionToBaseSnapshot, BigDecimal quantity, + BigDecimal totalQuantity, BigDecimal reservedBefore, BigDecimal fulfilledBefore, GaoProduct product, + GaoProductUnit unit, boolean packageSource) { + + private String sourceId() { + return packageSource ? sourceCustomerPackageItemId : sourceOrderItemId; + } + + private GaoOrderFulfillmentItem toFulfillmentItem(String fulfillmentId) { + GaoOrderFulfillmentItem result = new GaoOrderFulfillmentItem(); + result.setFulfillmentId(fulfillmentId); + result.setProductId(productId); + result.setProductUnitId(productUnitId); + result.setSourceOrderItemId(sourceOrderItemId); + result.setSourceCustomerPackageItemId(sourceCustomerPackageItemId); + result.setQuantity(quantity); + result.setBaseQuantity(quantity.multiply(conversionToBaseSnapshot).setScale(3)); + result.setReservedBefore(reservedBefore); + result.setReservedAfter(reservedBefore.add(quantity)); + result.setFulfilledBefore(fulfilledBefore); + result.setFulfilledAfter(fulfilledBefore); + result.setProductNameSnapshot(productNameSnapshot); + result.setUnitSnapshot(unitSnapshot); + result.setConversionToBaseSnapshot(conversionToBaseSnapshot); + return result; + } + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoOrderPaymentServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoOrderPaymentServiceImplement.java new file mode 100644 index 0000000..3794832 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoOrderPaymentServiceImplement.java @@ -0,0 +1,280 @@ +package com.imyeyu.api.modules.gao.service.implement; + +import com.imyeyu.api.config.dbsource.TimiServerDBConfig; +import com.imyeyu.api.modules.common.entity.Attachment; +import com.imyeyu.api.modules.common.service.AttachmentService; +import com.imyeyu.api.modules.gao.entity.GaoFundRecord; +import com.imyeyu.api.modules.gao.entity.GaoSalesOrder; +import com.imyeyu.api.modules.gao.service.GaoFundRecordService; +import com.imyeyu.api.modules.gao.service.GaoOrderPaymentService; +import com.imyeyu.api.modules.gao.service.GaoSalesOrderService; +import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentActionRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentNoRefundRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentReceiveRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentRefundRequest; +import com.imyeyu.api.modules.gao.vo.GaoOrderPaymentRevokeRequest; +import com.imyeyu.api.modules.system.entity.Logger; +import com.imyeyu.api.modules.system.service.LoggerService; +import com.imyeyu.api.modules.user.service.UserLoginService; +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.spring.service.AbstractEntityService; +import com.imyeyu.utils.Time; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/// GAO 销售订单资金服务实现 +/// +/// @author Codex +/// @since 2026-09-02 +@Service +@RequiredArgsConstructor +public class GaoOrderPaymentServiceImplement implements GaoOrderPaymentService { + + private final GaoFundRecordService fundRecordService; + private final GaoSalesOrderService orderService; + private final AttachmentService attachmentService; + private final UserLoginService userLoginService; + private final LoggerService loggerService; + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoFundRecord receive(GaoOrderPaymentReceiveRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_SALES_ORDER_PAYMENT_RECEIVE", request, () -> receiveInternal(request)); + } + + private GaoFundRecord receiveInternal(GaoOrderPaymentReceiveRequest request) { + TimiException.required(request, "not found order payment request"); + TimiException.required(request.getSalesOrderId(), "not found salesOrderId"); + TimiException.required(request.getAmount(), "收款金额不能为空"); + TimiException.requiredTrue(0 < request.getAmount(), "收款金额必须大于 0"); + GaoSalesOrder order = orderService.getForUpdate(request.getSalesOrderId()); + String requestNo = requiredText(request.getRequestNo(), "not found requestNo"); + GaoFundRecord existing = fundRecordService.getByRequestNo(order.getStoreId(), requestNo); + if (existing != null) { + verifySameReceive(existing, order, request); + return existing; + } + TimiException.requiredTrue(order.getStatus() != GaoSalesOrder.Status.CANCELLED, "已取消订单不能收款"); + TimiException.requiredTrue(order.getRefundStatus() != GaoSalesOrder.RefundStatus.REFUNDED, "已完成退款订单不能收款"); + long pending = safe(order.getPendingPaidAmount()); + long paid = safe(order.getPaidAmount()); + TimiException.requiredTrue(pending + paid + request.getAmount() <= safe(order.getSaleAmount()), "累计收款不能超过成交价"); + TimiException.requiredTrue(hasPhoto(request.getAttachmentList()), "收款必须上传照片"); + GaoFundRecord record = newRecord(order, GaoFundRecord.OperationType.RECEIVE, requestNo, request.getAmount(), request.getOccurredAt(), request.getRemark()); + record.setStatus(GaoFundRecord.Status.PENDING); + fundRecordService.createByOrderPayment(record); + order.setPendingPaidAmount(pending + request.getAmount()); + orderService.recomputePaymentStatus(order); + orderService.updatePayment(order); + attachmentService.updateByBizId(Attachment.BizType.GAO_FUND_RECORD, record.getId(), request.getAttachmentList()); + return record; + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoFundRecord verify(String fundRecordId) { + return loggerService.execute(Logger.Module.GAO, "GAO_SALES_ORDER_PAYMENT_VERIFY", fundRecordId, () -> { + GaoFundRecord record = lockOrderRecord(fundRecordId); + TimiException.requiredTrue(record.getOperationType() == GaoFundRecord.OperationType.RECEIVE && record.getStatus() == GaoFundRecord.Status.PENDING, "收款记录不是待核销状态"); + GaoSalesOrder order = orderService.getForUpdate(record.getBizId()); + order.setPendingPaidAmount(safe(order.getPendingPaidAmount()) - record.getAmount()); + order.setPaidAmount(safe(order.getPaidAmount()) + record.getAmount()); + orderService.recomputePaymentStatus(order); + String userId = userLoginService.getRequireLoginUserId(); + TimiException.requiredTrue(1 == fundRecordService.updateStatusIf(record.getId(), GaoFundRecord.Status.PENDING, GaoFundRecord.Status.VERIFIED, userId, Time.now(), record.getReason()), "收款记录状态已变化"); + orderService.updatePayment(order); + record.setStatus(GaoFundRecord.Status.VERIFIED); + record.setVerifierUserId(userId); + record.setVerifiedAt(Time.now()); + return record; + }); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoFundRecord reject(GaoOrderPaymentActionRequest request) { + return releasePending(request, GaoFundRecord.Status.REJECTED, false); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoFundRecord cancel(GaoOrderPaymentActionRequest request) { + return releasePending(request, GaoFundRecord.Status.CANCELLED, true); + } + + private GaoFundRecord releasePending(GaoOrderPaymentActionRequest request, GaoFundRecord.Status targetStatus, boolean onlyOperator) { + TimiException.required(request, "not found order payment action request"); + TimiException.required(request.getFundRecordId(), "not found fundRecordId"); + String reason = requiredText(request.getReason(), "资金操作原因不能为空"); + GaoFundRecord record = lockOrderRecord(request.getFundRecordId()); + TimiException.requiredTrue(record.getOperationType() == GaoFundRecord.OperationType.RECEIVE && record.getStatus() == GaoFundRecord.Status.PENDING, "收款记录不是待核销状态"); + if (onlyOperator) { + TimiException.requiredTrue(userLoginService.getRequireLoginUserId().equals(record.getOperatorUserId()), "只能撤销自己提交的收款"); + } + GaoSalesOrder order = orderService.getForUpdate(record.getBizId()); + order.setPendingPaidAmount(safe(order.getPendingPaidAmount()) - record.getAmount()); + orderService.recomputePaymentStatus(order); + String userId = userLoginService.getRequireLoginUserId(); + TimiException.requiredTrue(1 == fundRecordService.updateStatusIf(record.getId(), GaoFundRecord.Status.PENDING, targetStatus, userId, Time.now(), reason), "收款记录状态已变化"); + orderService.updatePayment(order); + record.setStatus(targetStatus); + record.setReason(reason); + record.setVerifierUserId(userId); + record.setVerifiedAt(Time.now()); + return record; + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoFundRecord refund(GaoOrderPaymentRefundRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_SALES_ORDER_REFUND", request, () -> refundInternal(request)); + } + + private GaoFundRecord refundInternal(GaoOrderPaymentRefundRequest request) { + TimiException.required(request, "not found refund request"); + TimiException.required(request.getSalesOrderId(), "not found salesOrderId"); + TimiException.required(request.getAmount(), "退款金额不能为空"); + TimiException.requiredTrue(0 < request.getAmount(), "退款金额必须大于 0"); + String reason = requiredText(request.getReason(), "退款原因不能为空"); + GaoSalesOrder order = orderService.getForUpdate(request.getSalesOrderId()); + String requestNo = requiredText(request.getRequestNo(), "not found requestNo"); + GaoFundRecord existing = fundRecordService.getByRequestNo(order.getStoreId(), requestNo); + if (existing != null) { + TimiException.requiredTrue(existing.getBizId().equals(order.getId()) && existing.getOperationType() == GaoFundRecord.OperationType.REFUND && existing.getAmount().equals(request.getAmount()), "requestNo 已被其他资金操作使用"); + return existing; + } + TimiException.requiredTrue(order.getStatus() == GaoSalesOrder.Status.CANCELLED, "只有已取消订单可以退款"); + TimiException.requiredTrue(order.getRefundStatus() == GaoSalesOrder.RefundStatus.PENDING, "当前订单不在待退款状态"); + long refundableAmount = safe(order.getPaidAmount()) - safe(order.getRefundedAmount()); + TimiException.requiredTrue(request.getAmount() <= refundableAmount, "退款金额不能超过有效收款余额"); + TimiException.requiredTrue(hasPhoto(request.getAttachmentList()), "退款必须上传照片"); + GaoFundRecord record = newRecord(order, GaoFundRecord.OperationType.REFUND, requestNo, request.getAmount(), request.getOccurredAt(), request.getRemark()); + record.setStatus(GaoFundRecord.Status.VERIFIED); + record.setReason(reason); + fundRecordService.createByOrderPayment(record); + long refundedAmount = safe(order.getRefundedAmount()) + request.getAmount(); + order.setRefundedAmount(refundedAmount); + order.setRefundStatus(refundedAmount == safe(order.getPaidAmount()) + ? GaoSalesOrder.RefundStatus.REFUNDED : GaoSalesOrder.RefundStatus.PENDING); + orderService.updatePayment(order); + attachmentService.updateByBizId(Attachment.BizType.GAO_FUND_RECORD, record.getId(), request.getAttachmentList()); + return record; + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoFundRecord confirmNoRefund(GaoOrderPaymentNoRefundRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_SALES_ORDER_CONFIRM_NO_REFUND", request, () -> { + TimiException.required(request, "not found no refund request"); + GaoSalesOrder order = orderService.getForUpdate(request.getSalesOrderId()); + String requestNo = requiredText(request.getRequestNo(), "not found requestNo"); + String reason = requiredText(request.getReason(), "确认无需退款原因不能为空"); + GaoFundRecord existing = fundRecordService.getByRequestNo(order.getStoreId(), requestNo); + if (existing != null) { + return existing; + } + TimiException.requiredTrue(order.getStatus() == GaoSalesOrder.Status.CANCELLED + && order.getRefundStatus() == GaoSalesOrder.RefundStatus.PENDING + && safe(order.getRefundedAmount()) == 0, "当前订单不在待确认退款状态"); + GaoFundRecord record = newRecord(order, GaoFundRecord.OperationType.CONFIRM_NO_REFUND, requestNo, 0L, request.getOccurredAt(), request.getRemark()); + record.setStatus(GaoFundRecord.Status.VERIFIED); + record.setReason(reason); + fundRecordService.createByOrderPayment(record); + order.setRefundStatus(GaoSalesOrder.RefundStatus.REFUNDED); + orderService.updatePayment(order); + return record; + }); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoFundRecord revoke(GaoOrderPaymentRevokeRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_SALES_ORDER_FUND_REVOKE", request, () -> revokeInternal(request)); + } + + private GaoFundRecord revokeInternal(GaoOrderPaymentRevokeRequest request) { + TimiException.required(request, "not found fund revoke request"); + String requestNo = requiredText(request.getRequestNo(), "not found requestNo"); + String reason = requiredText(request.getReason(), "资金冲正原因不能为空"); + GaoFundRecord source = lockOrderRecord(request.getFundRecordId()); + GaoSalesOrder order = orderService.getForUpdate(source.getBizId()); + GaoFundRecord existing = fundRecordService.getByRequestNo(order.getStoreId(), requestNo); + if (existing != null) { + TimiException.requiredTrue(existing.getOperationType() == GaoFundRecord.OperationType.REVOKE && source.getId().equals(existing.getSourceRecordId()), "requestNo 已被其他资金操作使用"); + return existing; + } + TimiException.requiredTrue(source.getStatus() == GaoFundRecord.Status.VERIFIED && source.getOperationType() != GaoFundRecord.OperationType.REVOKE, "只有已生效资金记录可以冲正"); + TimiException.requiredNull(fundRecordService.getRevokeBySourceRecordId(source.getId()), "该资金记录已经冲正"); + if (source.getOperationType() == GaoFundRecord.OperationType.RECEIVE) { + order.setPaidAmount(safe(order.getPaidAmount()) - source.getAmount()); + orderService.recomputePaymentStatus(order); + } else if (source.getOperationType() == GaoFundRecord.OperationType.REFUND) { + order.setRefundedAmount(safe(order.getRefundedAmount()) - source.getAmount()); + order.setRefundStatus(GaoSalesOrder.RefundStatus.PENDING); + } else { + order.setRefundStatus(GaoSalesOrder.RefundStatus.PENDING); + } + GaoFundRecord revoke = newRecord(order, GaoFundRecord.OperationType.REVOKE, requestNo, source.getAmount(), request.getOccurredAt(), request.getRemark()); + revoke.setStatus(GaoFundRecord.Status.VERIFIED); + revoke.setSourceRecordId(source.getId()); + revoke.setReason(reason); + fundRecordService.createByOrderPayment(revoke); + orderService.updatePayment(order); + TimiException.requiredTrue(hasPhoto(request.getAttachmentList()), "资金冲正必须上传照片"); + attachmentService.updateByBizId(Attachment.BizType.GAO_FUND_RECORD, revoke.getId(), request.getAttachmentList()); + return revoke; + } + + private GaoFundRecord lockOrderRecord(String id) { + GaoFundRecord record = fundRecordService.getForUpdate(id); + TimiException.required(record, "not found fund record"); + TimiException.requiredTrue(record.getBizType() == GaoFundRecord.BizType.SALES_ORDER, "资金记录不是销售订单资金"); + return record; + } + + private GaoFundRecord newRecord(GaoSalesOrder order, GaoFundRecord.OperationType operationType, String requestNo, Long amount, Long occurredAt, String remark) { + GaoFundRecord record = new GaoFundRecord(); + record.setStoreId(order.getStoreId()); + record.setCustomerId(order.getCustomerId()); + record.setBizType(GaoFundRecord.BizType.SALES_ORDER); + record.setBizId(order.getId()); + record.setOperationType(operationType); + record.setAmount(amount); + record.setRequestNo(requestNo); + record.setOperatorUserId(userLoginService.getRequireLoginUserId()); + record.setOccurredAt(TimiJava.defaultIfNull(occurredAt, Time.now())); + record.setRemark(remark); + return record; + } + + private boolean hasPhoto(List attachmentList) { + for (Attachment attachment : TimiJava.safeIterable(attachmentList)) { + if (attachment != null && (attachment.getAttachType() == null || "PHOTO".equalsIgnoreCase(attachment.getAttachType()))) { + return true; + } + } + return false; + } + + private void verifySameReceive(GaoFundRecord existing, GaoSalesOrder order, GaoOrderPaymentReceiveRequest request) { + TimiException.requiredTrue(existing.getBizType() == GaoFundRecord.BizType.SALES_ORDER + && existing.getBizId().equals(order.getId()) + && existing.getOperationType() == GaoFundRecord.OperationType.RECEIVE + && existing.getAmount().equals(request.getAmount()), "requestNo 已被其他资金操作使用"); + } + + private String requiredText(String value, String message) { + TimiException.required(value, message); + String result = value.trim(); + TimiException.requiredTrue(!result.isEmpty(), message); + return result; + } + + private long safe(Long value) { + return value == null ? 0L : value; + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPackageConfigServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPackageConfigServiceImplement.java index a36f0f6..ed50afc 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPackageConfigServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPackageConfigServiceImplement.java @@ -3,13 +3,17 @@ package com.imyeyu.api.modules.gao.service.implement; import com.imyeyu.api.config.dbsource.TimiServerDBConfig; import com.imyeyu.api.modules.gao.entity.GaoPackageConfig; import com.imyeyu.api.modules.gao.entity.GaoPackageConfigItem; +import com.imyeyu.api.modules.gao.entity.GaoProduct; +import com.imyeyu.api.modules.gao.entity.GaoProductUnit; import com.imyeyu.api.modules.system.entity.Logger; import com.imyeyu.api.modules.system.service.LoggerService; import com.imyeyu.api.modules.gao.mapper.GaoPackageConfigItemMapper; import com.imyeyu.api.modules.gao.mapper.GaoPackageConfigMapper; -import com.imyeyu.api.modules.gao.mapper.GaoEventMapper; import com.imyeyu.api.modules.gao.service.GaoPackageConfigService; +import com.imyeyu.api.modules.gao.service.GaoProductService; +import com.imyeyu.api.modules.gao.service.GaoProductUnitService; import com.imyeyu.api.modules.gao.vo.GaoPackageConfigPage; +import com.imyeyu.api.modules.gao.vo.GaoProductUnitSelection; import com.imyeyu.api.modules.user.service.UserLoginService; import com.imyeyu.java.TimiJava; import com.imyeyu.java.bean.timi.TimiException; @@ -38,7 +42,8 @@ public class GaoPackageConfigServiceImplement extends AbstractEntityService { GaoPackageConfig config = get(id); TimiException.required(config, "not found package config"); - TimiException.requiredTrue(0 == eventMapper.countByPackageConfigId(id), "套餐配置已绑定购买事件,不能删除"); super.delete(id); return id; }); @@ -169,26 +173,35 @@ public class GaoPackageConfigServiceImplement extends AbstractEntityService itemList) { + private void verifyItems(String storeId, List itemList) { TimiException.requiredTrue(!itemList.isEmpty(), "套餐至少配置一个产品"); + TimiException.required(storeId, "not found packageConfig.storeId"); Set keySet = new HashSet<>(); for (int i = 0; i < itemList.size(); i++) { GaoPackageConfigItem item = itemList.get(i); TimiException.required(item, "套餐产品明细不能为空"); - String name = trimRequired(item.getProductName(), "产品名称不能为空"); - String unit = trimRequired(item.getUnit(), "产品单位不能为空"); + TimiException.required(item.getProductId(), "产品不能为空"); + TimiException.required(item.getProductUnitId(), "产品单位不能为空"); + GaoProductUnitSelection selection = productService.getSellableProductUnit(storeId, item.getProductId(), item.getProductUnitId()); + GaoProduct product = selection.getProduct(); + GaoProductUnit unitEntity = selection.getUnit(); BigDecimal quantity = item.getDefaultQuantity(); TimiException.required(quantity, "产品默认数量不能为空"); TimiException.requiredTrue(0 < quantity.signum(), "产品默认数量必须大于 0"); - TimiException.requiredTrue(3 >= quantity.scale(), "产品数量最多保留三位小数"); - String key = name + "|" + unit; - TimiException.requiredTrue(keySet.add(key), "同一套餐内产品名称和单位不能重复"); - item.setProductName(name); - item.setUnit(unit); + TimiException.requiredTrue(3 >= quantity.stripTrailingZeros().scale(), "产品数量最多保留三位小数"); + if (!Boolean.TRUE.equals(product.getAllowDecimalQuantity())) { + TimiException.requiredTrue(quantity.stripTrailingZeros().scale() <= 0, "该产品不允许小数数量"); + } + unitService.convertToBase(unitEntity, quantity); + String key = item.getProductId() + "|" + item.getProductUnitId(); + TimiException.requiredTrue(keySet.add(key), "同一套餐内产品和单位不能重复"); + item.setProductNameSnapshot(product.getName()); + item.setUnitSnapshot(unitEntity.getName()); + item.setConversionToBaseSnapshot(unitEntity.getConversionToBase()); item.setSort(TimiJava.defaultIfNull(item.getSort(), i)); } } @@ -197,8 +210,11 @@ public class GaoPackageConfigServiceImplement extends AbstractEntityService implements GaoPackageDeliveryService { - - private static final BigDecimal ZERO = BigDecimal.ZERO; - - private final GaoPackageDeliveryMapper mapper; - private final GaoPackageDeliveryItemMapper itemMapper; - private final GaoCustomerPackageService customerPackageService; - private final AttachmentService attachmentService; - private final GaoStoreScopeService storeScopeService; - private final RoleChecker roleChecker; - private final UserLoginService userLoginService; - private final LoggerService loggerService; - - @Override - protected BaseMapper mapper() { - return mapper; - } - - /// 发放流水必须通过专用操作创建 - /// - /// @param delivery 发放流水 - @Override - public void create(GaoPackageDelivery delivery) { - throw new TimiException(TimiCode.ERROR, "发放流水必须通过专用接口创建"); - } - - /// 发放流水不允许通用修改 - /// - /// @param delivery 发放流水 - @Override - public void update(GaoPackageDelivery delivery) { - throw new TimiException(TimiCode.ERROR, "发放流水不允许通用修改"); - } - - /// 发放流水不允许删除 - /// - /// @param id 发放流水 ID - @Override - public void delete(String id) { - throw new TimiException(TimiCode.ERROR, "发放流水不允许删除"); - } - - /// 发放流水不允许物理删除 - /// - /// @param id 发放流水 ID - @Override - public void destroy(String id) { - throw new TimiException(TimiCode.ERROR, "发放流水不允许物理删除"); - } - - @Override - public PageResult pageByQuery(GaoPackageDeliveryPage page) { - TimiException.required(page, "not found page"); - PageResult result = new PageResult<>(); - result.setTotal(mapper.countByQuery(page)); - result.setList(mapper.selectByQuery(page)); - return result; - } - - @Override - public List listByCustomerPackageId(String customerPackageId) { - TimiException.required(customerPackageId, "not found customerPackageId"); - return mapper.selectByCustomerPackageId(customerPackageId); - } - - @Override - public Map> mapItemListByDeliveryIdList(Collection deliveryIdList) { - if (deliveryIdList == null || deliveryIdList.isEmpty()) { - return Map.of(); - } - return itemMapper.selectByDeliveryIdList(deliveryIdList).stream() - .collect(java.util.stream.Collectors.groupingBy(GaoPackageDeliveryItem::getDeliveryId)); - } - - @Override - public long countPendingByCustomerPackageId(String customerPackageId) { - return mapper.countPendingByCustomerPackageId(customerPackageId); - } - - @Override - public long countVerifiedByCustomerPackageId(String customerPackageId) { - return mapper.countVerifiedByCustomerPackageId(customerPackageId); - } - - @Override - public GaoPackageDelivery get(String id) { - return super.get(id); - } - - @Transactional(TimiServerDBConfig.ROLLBACKER) - @Override - public GaoPackageDelivery createDelivery(GaoPackageDeliveryCreateRequest request) { - return loggerService.execute(Logger.Module.GAO, "GAO_PACKAGE_DELIVERY_CREATE", request, () -> createDeliveryInternal(request)); - } - - private GaoPackageDelivery createDeliveryInternal(GaoPackageDeliveryCreateRequest request) { - TimiException.required(request, "not found delivery request"); - TimiException.required(request.getRequestNo(), "not found requestNo"); - request.setRequestNo(request.getRequestNo().trim()); - TimiException.required(request.getCustomerPackageId(), "not found customerPackageId"); - GaoCustomerPackage packageEntity = customerPackageService.getForUpdate(request.getCustomerPackageId()); - TimiException.required(packageEntity, "not found customer package"); - storeScopeService.checkStoreId(packageEntity.getStoreId()); - GaoPackageDelivery existing = mapper.selectByRequestNo(packageEntity.getStoreId(), request.getRequestNo()); - if (existing != null) { - TimiException.requiredTrue(existing.getOperationType() == GaoPackageDelivery.OperationType.DELIVER, "requestNo 已被其他发放操作使用"); - verifyCreateIdempotency(existing, request); - return get(existing.getId()); - } - TimiException.requiredTrue(hasAttachment(request.getAttachmentList()), "发放必须上传照片"); - TimiException.requiredTrue(packageEntity.getStatus() == GaoCustomerPackage.Status.ACTIVE, "当前套餐不可发放"); - checkPaymentForDelivery(packageEntity); - List requestItemList = validateRequestItems(request.getItemList()); - Map packageItemMap = new HashMap<>(); - for (GaoPackageDeliveryItem requestItem : requestItemList) { - GaoCustomerPackageItem packageItem = customerPackageService.getItemForUpdate(requestItem.getCustomerPackageItemId()); - TimiException.required(packageItem, "not found customer package item"); - TimiException.requiredTrue(packageEntity.getId().equals(packageItem.getCustomerPackageId()), "发放产品不属于当前套餐"); - packageItemMap.put(packageItem.getId(), packageItem); - } - - GaoPackageDelivery delivery = new GaoPackageDelivery(); - delivery.setCustomerPackageId(packageEntity.getId()); - delivery.setStoreId(packageEntity.getStoreId()); - delivery.setCustomerId(packageEntity.getCustomerId()); - delivery.setOperationType(GaoPackageDelivery.OperationType.DELIVER); - delivery.setStatus(GaoPackageDelivery.Status.PENDING); - delivery.setRequestNo(request.getRequestNo()); - delivery.setOperatorUserId(userLoginService.getRequireLoginUserId()); - delivery.setDeliveredAt(TimiJava.defaultIfNull(request.getDeliveredAt(), Time.now())); - delivery.setRemark(request.getRemark()); - super.create(delivery); - for (GaoPackageDeliveryItem requestItem : requestItemList) { - GaoCustomerPackageItem packageItem = packageItemMap.get(requestItem.getCustomerPackageItemId()); - BigDecimal reservedBefore = decimal(packageItem.getReservedQuantity()); - BigDecimal deliveredBefore = decimal(packageItem.getDeliveredQuantity()); - BigDecimal available = decimal(packageItem.getTotalQuantity()).subtract(reservedBefore).subtract(deliveredBefore); - TimiException.requiredTrue(0 <= available.compareTo(requestItem.getQuantity()), "产品可发数量不足"); - int updated = customerPackageService.updateItemReservedQuantity(packageItem.getId(), requestItem.getQuantity()); - TimiException.requiredTrue(0 < updated, "产品可发数量不足"); - GaoPackageDeliveryItem savedItem = copyDeliveryItem(requestItem); - savedItem.setDeliveryId(delivery.getId()); - savedItem.setReservedBefore(reservedBefore); - savedItem.setReservedAfter(reservedBefore.add(requestItem.getQuantity())); - savedItem.setDeliveredBefore(deliveredBefore); - savedItem.setDeliveredAfter(deliveredBefore); - savedItem.setProductNameSnapshot(packageItem.getProductNameSnapshot()); - savedItem.setUnitSnapshot(packageItem.getUnitSnapshot()); - itemMapper.insert(savedItem); - } - attachmentService.updateByBizId(Attachment.BizType.GAO_PACKAGE_DELIVERY, delivery.getId(), request.getAttachmentList()); - return delivery; - } - - @Transactional(TimiServerDBConfig.ROLLBACKER) - @Override - public GaoPackageDelivery verify(String deliveryId) { - return loggerService.execute(Logger.Module.GAO, "GAO_PACKAGE_DELIVERY_VERIFY", deliveryId, () -> verifyInternal(deliveryId)); - } - - private GaoPackageDelivery verifyInternal(String deliveryId) { - GaoPackageDelivery delivery = mapper.selectForUpdate(deliveryId); - TimiException.required(delivery, "not found delivery"); - storeScopeService.checkStoreId(delivery.getStoreId()); - TimiException.requiredTrue(delivery.getStatus() == GaoPackageDelivery.Status.PENDING && delivery.getOperationType() == GaoPackageDelivery.OperationType.DELIVER, "发放单不是待核销状态"); - GaoCustomerPackage packageEntity = customerPackageService.getForUpdate(delivery.getCustomerPackageId()); - TimiException.required(packageEntity, "not found customer package"); - TimiException.requiredTrue(packageEntity.getStatus() == GaoCustomerPackage.Status.ACTIVE, "当前套餐不可核销发放"); - checkPaymentForDelivery(packageEntity); - List itemList = itemMapper.selectByDeliveryId(delivery.getId()); - for (GaoPackageDeliveryItem deliveryItem : itemList) { - GaoCustomerPackageItem packageItem = customerPackageService.getItemForUpdate(deliveryItem.getCustomerPackageItemId()); - TimiException.required(packageItem, "not found customer package item"); - TimiException.requiredTrue(0 < customerPackageService.updateItemReservedQuantity(packageItem.getId(), deliveryItem.getQuantity().negate()), "发放占用数量异常"); - TimiException.requiredTrue(0 < customerPackageService.updateItemDeliveredQuantity(packageItem.getId(), deliveryItem.getQuantity()), "发放数量异常"); - } - TimiException.requiredTrue(0 < mapper.updateStatusIf(delivery.getId(), GaoPackageDelivery.Status.PENDING, GaoPackageDelivery.Status.VERIFIED, userLoginService.getRequireLoginUserId(), Time.now(), delivery.getReason()), "发放单状态已变化"); - delivery.setStatus(GaoPackageDelivery.Status.VERIFIED); - delivery.setVerifierUserId(userLoginService.getRequireLoginUserId()); - delivery.setVerifiedAt(Time.now()); - refreshPackageStatus(packageEntity); - return delivery; - } - - @Transactional(TimiServerDBConfig.ROLLBACKER) - @Override - public GaoPackageDelivery reject(GaoPackageDeliveryActionRequest request) { - return loggerService.execute(Logger.Module.GAO, "GAO_PACKAGE_DELIVERY_REJECT", request, () -> releasePending(request, GaoPackageDelivery.Status.REJECTED)); - } - - @Transactional(TimiServerDBConfig.ROLLBACKER) - @Override - public GaoPackageDelivery cancel(GaoPackageDeliveryActionRequest request) { - return loggerService.execute(Logger.Module.GAO, "GAO_PACKAGE_DELIVERY_CANCEL", request, () -> cancelInternal(request)); - } - - private GaoPackageDelivery cancelInternal(GaoPackageDeliveryActionRequest request) { - TimiException.required(request, "not found delivery request"); - GaoPackageDelivery delivery = mapper.selectForUpdate(request.getDeliveryId()); - TimiException.required(delivery, "not found delivery"); - String loginUserId = userLoginService.getRequireLoginUserId(); - TimiException.requiredTrue(loginUserId.equals(delivery.getOperatorUserId()), "只能撤销自己提交的发放"); - return releasePending(request, GaoPackageDelivery.Status.CANCELLED); - } - - @Transactional(TimiServerDBConfig.ROLLBACKER) - @Override - public GaoPackageDelivery revoke(GaoPackageDeliveryRevokeRequest request) { - return loggerService.execute(Logger.Module.GAO, "GAO_PACKAGE_DELIVERY_REVOKE", request, () -> revokeInternal(request)); - } - - private GaoPackageDelivery revokeInternal(GaoPackageDeliveryRevokeRequest request) { - TimiException.required(request, "not found revoke request"); - TimiException.required(request.getRequestNo(), "not found requestNo"); - request.setRequestNo(request.getRequestNo().trim()); - TimiException.required(request.getDeliveryId(), "not found deliveryId"); - TimiException.required(hasReason(request.getReason()), "冲正原因不能为空"); - GaoPackageDelivery source = mapper.selectForUpdate(request.getDeliveryId()); - TimiException.required(source, "not found delivery"); - storeScopeService.checkStoreId(source.getStoreId()); - GaoPackageDelivery existing = mapper.selectByRequestNo(source.getStoreId(), request.getRequestNo()); - if (existing != null) { - TimiException.requiredTrue(existing.getOperationType() == GaoPackageDelivery.OperationType.REVOKE - && source.getId().equals(existing.getSourceDeliveryId()), "requestNo 已被其他发放操作使用"); - return get(existing.getId()); - } - TimiException.requiredTrue(hasAttachment(request.getAttachmentList()), "发放冲正必须上传照片"); - TimiException.requiredTrue(source.getOperationType() == GaoPackageDelivery.OperationType.DELIVER && source.getStatus() == GaoPackageDelivery.Status.VERIFIED, "只有已核销发放可以冲正"); - TimiException.requiredNull(mapper.selectRevokeBySourceDeliveryId(source.getId()), "该发放单已经冲正"); - GaoCustomerPackage packageEntity = customerPackageService.getForUpdate(source.getCustomerPackageId()); - TimiException.required(packageEntity, "not found customer package"); - List sourceItemList = itemMapper.selectByDeliveryId(source.getId()); - GaoPackageDelivery revoke = new GaoPackageDelivery(); - revoke.setCustomerPackageId(source.getCustomerPackageId()); - revoke.setStoreId(source.getStoreId()); - revoke.setCustomerId(source.getCustomerId()); - revoke.setOperationType(GaoPackageDelivery.OperationType.REVOKE); - revoke.setStatus(GaoPackageDelivery.Status.VERIFIED); - revoke.setRequestNo(request.getRequestNo()); - revoke.setOperatorUserId(userLoginService.getRequireLoginUserId()); - revoke.setDeliveredAt(TimiJava.defaultIfNull(request.getDeliveredAt(), Time.now())); - revoke.setVerifierUserId(userLoginService.getRequireLoginUserId()); - revoke.setVerifiedAt(Time.now()); - revoke.setSourceDeliveryId(source.getId()); - revoke.setReason(request.getReason().trim()); - revoke.setRemark(request.getRemark()); - super.create(revoke); - for (GaoPackageDeliveryItem sourceItem : sourceItemList) { - GaoCustomerPackageItem packageItem = customerPackageService.getItemForUpdate(sourceItem.getCustomerPackageItemId()); - TimiException.required(packageItem, "not found customer package item"); - BigDecimal deliveredBefore = decimal(packageItem.getDeliveredQuantity()); - TimiException.requiredTrue(0 < deliveredBefore.compareTo(sourceItem.getQuantity()) || deliveredBefore.compareTo(sourceItem.getQuantity()) == 0, "已发数量不足以冲正"); - TimiException.requiredTrue(0 < customerPackageService.updateItemDeliveredQuantity(packageItem.getId(), sourceItem.getQuantity().negate()), "发放数量冲正失败"); - GaoPackageDeliveryItem revokeItem = copyDeliveryItem(sourceItem); - revokeItem.setDeliveryId(revoke.getId()); - revokeItem.setReservedBefore(packageItem.getReservedQuantity()); - revokeItem.setReservedAfter(packageItem.getReservedQuantity()); - revokeItem.setDeliveredBefore(deliveredBefore); - revokeItem.setDeliveredAfter(deliveredBefore.subtract(sourceItem.getQuantity())); - itemMapper.insert(revokeItem); - } - TimiException.requiredTrue(0 < mapper.updateStatusIf(source.getId(), GaoPackageDelivery.Status.VERIFIED, GaoPackageDelivery.Status.REVOKED, source.getVerifierUserId(), source.getVerifiedAt(), source.getReason()), "发放单状态已变化"); - if (packageEntity.getStatus() == GaoCustomerPackage.Status.FULFILLED) { - packageEntity.setStatus(GaoCustomerPackage.Status.ACTIVE); - customerPackageService.updateAggregateState(packageEntity); - } - attachmentService.updateByBizId(Attachment.BizType.GAO_PACKAGE_DELIVERY, revoke.getId(), request.getAttachmentList()); - return revoke; - } - - private GaoPackageDelivery releasePending(GaoPackageDeliveryActionRequest request, GaoPackageDelivery.Status targetStatus) { - TimiException.required(request, "not found delivery request"); - TimiException.required(request.getDeliveryId(), "not found deliveryId"); - TimiException.required(hasReason(request.getReason()), "拒绝或撤销原因不能为空"); - GaoPackageDelivery delivery = mapper.selectForUpdate(request.getDeliveryId()); - TimiException.required(delivery, "not found delivery"); - storeScopeService.checkStoreId(delivery.getStoreId()); - TimiException.requiredTrue(delivery.getStatus() == GaoPackageDelivery.Status.PENDING && delivery.getOperationType() == GaoPackageDelivery.OperationType.DELIVER, "发放单不是待核销状态"); - for (GaoPackageDeliveryItem item : itemMapper.selectByDeliveryId(delivery.getId())) { - TimiException.requiredTrue(0 < customerPackageService.updateItemReservedQuantity(item.getCustomerPackageItemId(), item.getQuantity().negate()), "发放占用数量异常"); - } - String loginUserId = userLoginService.getRequireLoginUserId(); - TimiException.requiredTrue(0 < mapper.updateStatusIf(delivery.getId(), GaoPackageDelivery.Status.PENDING, targetStatus, loginUserId, Time.now(), request.getReason().trim()), "发放单状态已变化"); - delivery.setStatus(targetStatus); - delivery.setVerifierUserId(loginUserId); - delivery.setVerifiedAt(Time.now()); - delivery.setReason(request.getReason().trim()); - return delivery; - } - - private void refreshPackageStatus(GaoCustomerPackage packageEntity) { - List itemList = customerPackageService.listItemByCustomerPackageId(packageEntity.getId()); - boolean fulfilled = !itemList.isEmpty(); - for (GaoCustomerPackageItem item : itemList) { - if (decimal(item.getDeliveredQuantity()).compareTo(decimal(item.getTotalQuantity())) < 0) { - fulfilled = false; - break; - } - } - if (fulfilled && packageEntity.getStatus() == GaoCustomerPackage.Status.ACTIVE) { - packageEntity.setStatus(GaoCustomerPackage.Status.FULFILLED); - customerPackageService.updateAggregateState(packageEntity); - } - } - - private List validateRequestItems(List sourceList) { - TimiException.requiredTrue(sourceList != null && !sourceList.isEmpty(), "发放至少需要一个产品"); - Set idSet = new HashSet<>(); - List result = new ArrayList<>(); - for (GaoPackageDeliveryItem source : sourceList) { - TimiException.required(source, "发放产品明细不能为空"); - TimiException.required(source.getCustomerPackageItemId(), "not found customerPackageItemId"); - TimiException.required(source.getQuantity(), "发放数量不能为空"); - TimiException.requiredTrue(0 < source.getQuantity().signum(), "发放数量必须大于 0"); - TimiException.requiredTrue(3 >= source.getQuantity().scale(), "发放数量最多保留三位小数"); - TimiException.requiredTrue(idSet.add(source.getCustomerPackageItemId()), "同一发放单产品不能重复"); - result.add(source); - } - result.sort(Comparator.comparing(GaoPackageDeliveryItem::getCustomerPackageItemId)); - return result; - } - - private GaoPackageDeliveryItem copyDeliveryItem(GaoPackageDeliveryItem source) { - GaoPackageDeliveryItem result = new GaoPackageDeliveryItem(); - result.setCustomerPackageItemId(source.getCustomerPackageItemId()); - result.setQuantity(source.getQuantity()); - return result; - } - - private void verifyCreateIdempotency(GaoPackageDelivery existing, GaoPackageDeliveryCreateRequest request) { - TimiException.requiredTrue(existing.getCustomerPackageId().equals(request.getCustomerPackageId()) - && (request.getDeliveredAt() == null || request.getDeliveredAt().equals(existing.getDeliveredAt())) - && sameText(existing.getRemark(), request.getRemark()), "requestNo 对应请求内容不一致"); - List existingItems = itemMapper.selectByDeliveryId(existing.getId()); - List requestItems = validateRequestItems(request.getItemList()); - TimiException.requiredTrue(existingItems.size() == requestItems.size(), "requestNo 对应请求内容不一致"); - Map requestMap = requestItems.stream().collect(java.util.stream.Collectors.toMap(GaoPackageDeliveryItem::getCustomerPackageItemId, GaoPackageDeliveryItem::getQuantity)); - for (GaoPackageDeliveryItem item : existingItems) { - TimiException.requiredTrue(item.getQuantity().compareTo(requestMap.getOrDefault(item.getCustomerPackageItemId(), ZERO)) == 0, "requestNo 对应请求内容不一致"); - } - } - - private boolean hasAttachment(List attachmentList) { - for (Attachment attachment : TimiJava.safeIterable(attachmentList)) { - if (attachment != null && (attachment.getAttachType() == null || "PHOTO".equalsIgnoreCase(attachment.getAttachType()))) { - return true; - } - } - return false; - } - - private boolean hasReason(String reason) { - return reason != null && !reason.trim().isEmpty(); - } - - private boolean canDeliver(GaoCustomerPackage packageEntity) { - return safeLong(packageEntity.getSaleAmount()) == 0 || 0 < safeLong(packageEntity.getPaidAmount()); - } - - private void checkPaymentForDelivery(GaoCustomerPackage packageEntity) { - long saleAmount = safeLong(packageEntity.getSaleAmount()); - long paidAmount = safeLong(packageEntity.getPaidAmount()); - TimiException.requiredTrue(canDeliver(packageEntity), "套餐未有已核销收款,不能发放"); - if (0 < paidAmount && paidAmount < saleAmount) { - roleChecker.checkAny(ModuleCode.GAO, GaoRoleCode.STORE_MANAGER.name()); - } - } - - private boolean sameText(String left, String right) { - return left == null ? right == null : left.equals(right); - } - - private BigDecimal decimal(BigDecimal value) { - return value == null ? ZERO : value; - } - - private long safeLong(Long value) { - return value == null ? 0L : value; - } -} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPointAccountServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPointAccountServiceImplement.java index c1579ea..180813c 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPointAccountServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPointAccountServiceImplement.java @@ -5,8 +5,8 @@ import com.imyeyu.api.modules.common.entity.Attachment; import com.imyeyu.api.modules.common.service.AttachmentService; import com.imyeyu.api.modules.gao.entity.GaoCustomer; import com.imyeyu.api.modules.gao.entity.GaoPointAccount; -import com.imyeyu.api.modules.gao.mapper.GaoCustomerMapper; import com.imyeyu.api.modules.gao.mapper.GaoPointAccountMapper; +import com.imyeyu.api.modules.gao.service.GaoCustomerService; import com.imyeyu.api.modules.gao.service.GaoPointAccountService; import com.imyeyu.api.modules.gao.vo.GaoPointAccountPage; import com.imyeyu.java.bean.timi.TimiException; @@ -15,13 +15,13 @@ import com.imyeyu.spring.mapper.BaseMapper; import com.imyeyu.spring.service.AbstractEntityService; import com.imyeyu.utils.Time; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Map; import java.util.UUID; -import java.util.stream.Collectors; /// GAO 客户积分账户服务实现 /// @@ -32,7 +32,7 @@ import java.util.stream.Collectors; public class GaoPointAccountServiceImplement extends AbstractEntityService implements GaoPointAccountService { private final GaoPointAccountMapper mapper; - private final GaoCustomerMapper customerMapper; + private final ObjectProvider customerServiceProvider; private final AttachmentService attachmentService; @Override @@ -88,8 +88,7 @@ public class GaoPointAccountServiceImplement extends AbstractEntityService customerMap = customerMapper.selectByIdList(customerIdList).stream() - .collect(Collectors.toMap(GaoCustomer::getId, item -> item)); + Map customerMap = customerServiceProvider.getObject().mapByIdList(customerIdList); Map> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_CUSTOMER, customerIdList); for (GaoPointAccount account : accountList) { GaoCustomer customer = customerMap.get(account.getCustomerId()); diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPointLedgerServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPointLedgerServiceImplement.java index 529e00a..2ba9ff4 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPointLedgerServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPointLedgerServiceImplement.java @@ -105,6 +105,18 @@ public class GaoPointLedgerServiceImplement extends AbstractEntityService listRuleChangeByEventRecordId(String eventRecordId) { + TimiException.required(eventRecordId, "not found eventRecordId"); + return mapper.selectRuleChangeByEventRecordId(eventRecordId); + } + + @Override + public List listByIdempotencyKeyList(List idempotencyKeyList) { + TimiException.required(idempotencyKeyList, "not found idempotencyKeyList"); + return mapper.selectByIdempotencyKeyList(idempotencyKeyList); + } + @Override public PageResult pageByQuery(GaoPointLedgerPage page) { TimiException.required(page, "not found page"); diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPointTriggerServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPointTriggerServiceImplement.java index eaff979..d95e635 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPointTriggerServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPointTriggerServiceImplement.java @@ -10,9 +10,8 @@ import com.imyeyu.api.modules.gao.entity.GaoCustomer; import com.imyeyu.api.modules.gao.entity.GaoEventRecord; import com.imyeyu.api.modules.gao.entity.GaoPointLedger; import com.imyeyu.api.modules.gao.entity.GaoPointRule; -import com.imyeyu.api.modules.gao.mapper.GaoEventRecordMapper; -import com.imyeyu.api.modules.gao.mapper.GaoPointLedgerMapper; import com.imyeyu.api.modules.gao.service.GaoCustomerService; +import com.imyeyu.api.modules.gao.service.GaoEventRecordService; import com.imyeyu.api.modules.gao.service.GaoPointLedgerService; import com.imyeyu.api.modules.gao.service.GaoPointRuleService; import com.imyeyu.api.modules.gao.service.GaoPointTriggerService; @@ -23,6 +22,7 @@ import com.imyeyu.java.TimiJava; import com.imyeyu.java.bean.timi.TimiException; import com.imyeyu.utils.Time; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -47,8 +47,7 @@ public class GaoPointTriggerServiceImplement implements GaoPointTriggerService { private final GaoPointRuleService ruleService; private final GaoPointLedgerService ledgerService; private final GaoCustomerService customerService; - private final GaoPointLedgerMapper ledgerMapper; - private final GaoEventRecordMapper eventRecordMapper; + private final ObjectProvider eventRecordServiceProvider; @Transactional(TimiServerDBConfig.ROLLBACKER) @Override @@ -81,20 +80,20 @@ public class GaoPointTriggerServiceImplement implements GaoPointTriggerService { } } record.setEarnedPointValue(pointDeltaTotal); - eventRecordMapper.updateEarnedPointValue(record.getId(), pointDeltaTotal); + eventRecordServiceProvider.getObject().updateEarnedPointValue(record.getId(), pointDeltaTotal); } @Transactional(TimiServerDBConfig.ROLLBACKER) @Override public void revokeByEventRecord(GaoEventRecord record) { TimiException.required(record, "not found event record"); - List originLedgerList = ledgerMapper.selectRuleChangeByEventRecordId(record.getId()); + List originLedgerList = ledgerService.listRuleChangeByEventRecordId(record.getId()); if (originLedgerList.isEmpty()) { return; } Set revokedKeySet = new HashSet<>(); List revokeKeyList = originLedgerList.stream().map(origin -> revokeKey(record, origin)).toList(); - for (GaoPointLedger ledger : ledgerMapper.selectByIdempotencyKeyList(revokeKeyList)) { + for (GaoPointLedger ledger : ledgerService.listByIdempotencyKeyList(revokeKeyList)) { revokedKeySet.add(ledger.getIdempotencyKey()); } for (GaoPointLedger originLedger : originLedgerList) { @@ -162,7 +161,7 @@ public class GaoPointTriggerServiceImplement implements GaoPointTriggerService { LocalDate currentDate = Instant.ofEpochMilli(record.getRegisteredAt()).atZone(zone).toLocalDate(); long windowBeginAt = currentDate.minusYears(amount).atStartOfDay(zone).toInstant().toEpochMilli(); long windowEndAt = Math.addExact(record.getRegisteredAt(), 1L); - long sumValue = TimiJava.defaultIfNull(eventRecordMapper.sumNumberValue(record.getStoreId(), record.getCustomerId(), sourceEventId, windowBeginAt, windowEndAt), 0L); + long sumValue = eventRecordServiceProvider.getObject().sumNumberValue(record.getStoreId(), record.getCustomerId(), sourceEventId, windowBeginAt, windowEndAt); JsonNode matchedTier = null; for (JsonNode tier : condition.get("tiers")) { @@ -196,7 +195,7 @@ public class GaoPointTriggerServiceImplement implements GaoPointTriggerService { } private long count(GaoEventRecord record) { - return TimiJava.defaultIfNull(eventRecordMapper.count(record.getStoreId(), record.getCustomerId(), record.getEventId(), null, null), 0L); + return eventRecordServiceProvider.getObject().count(record.getStoreId(), record.getCustomerId(), record.getEventId(), null, null); } private String revokeKey(GaoEventRecord record, GaoPointLedger originLedger) { diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductCategoryServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductCategoryServiceImplement.java new file mode 100644 index 0000000..63ae599 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductCategoryServiceImplement.java @@ -0,0 +1,152 @@ +package com.imyeyu.api.modules.gao.service.implement; + +import com.imyeyu.api.config.dbsource.TimiServerDBConfig; +import com.imyeyu.api.modules.gao.entity.GaoProductCategory; +import com.imyeyu.api.modules.gao.mapper.GaoProductCategoryMapper; +import com.imyeyu.api.modules.gao.service.GaoProductCategoryService; +import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; +import com.imyeyu.api.modules.gao.vo.GaoProductCategoryPage; +import com.imyeyu.api.modules.system.entity.Logger; +import com.imyeyu.api.modules.system.service.LoggerService; +import com.imyeyu.api.modules.user.service.UserLoginService; +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.mapper.BaseMapper; +import com.imyeyu.spring.service.AbstractEntityService; +import com.imyeyu.utils.Time; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +/// GAO 产品分类服务实现 +/// +/// @author Codex +/// @since 2026-09-02 +@Service +@RequiredArgsConstructor +public class GaoProductCategoryServiceImplement extends AbstractEntityService implements GaoProductCategoryService { + + private final GaoProductCategoryMapper mapper; + private final GaoStoreScopeService storeScopeService; + private final UserLoginService userLoginService; + private final LoggerService loggerService; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + @Override + public PageResult pageByQuery(GaoProductCategoryPage page) { + TimiException.required(page, "not found category page"); + PageResult result = new PageResult<>(); + result.setTotal(mapper.countByQuery(page)); + result.setList(mapper.selectByQuery(page)); + return result; + } + + @Override + public List listByStore(String storeId) { + TimiException.required(storeId, "not found storeId"); + return mapper.selectByStoreId(storeId); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void sort(List idList) { + if (TimiJava.isEmpty(idList)) { + return; + } + List distinctIdList = new ArrayList<>(new LinkedHashSet<>(idList)); + loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_CATEGORY_SORT", distinctIdList, () -> { + Map categoryMap = mapper.selectByIdList(new LinkedHashSet<>(distinctIdList)).stream() + .collect(Collectors.toMap(GaoProductCategory::getId, Function.identity())); + TimiException.requiredTrue(categoryMap.size() == distinctIdList.size(), "产品分类列表包含不存在的分类"); + for (GaoProductCategory category : categoryMap.values()) { + storeScopeService.checkStoreId(category.getStoreId()); + } + mapper.updateSort(distinctIdList, Time.now()); + }); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void create(GaoProductCategory category) { + loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_CATEGORY_CREATE", category, () -> { + prepare(category, null); + super.create(category); + return category; + }); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void update(GaoProductCategory request) { + loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_CATEGORY_UPDATE", request, () -> { + TimiException.required(request, "not found category"); + TimiException.required(request.getId(), "not found category.id"); + GaoProductCategory category = get(request.getId()); + TimiException.required(category, "not found category"); + storeScopeService.checkStoreId(category.getStoreId()); + category.setName(request.getName()); + category.setSort(request.getSort()); + category.setStatus(request.getStatus()); + prepare(category, category.getId()); + super.update(category); + return category; + }); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void disable(String id) { + loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_CATEGORY_DISABLE", id, () -> { + GaoProductCategory category = get(id); + TimiException.required(category, "not found category"); + storeScopeService.checkStoreId(category.getStoreId()); + category.setStatus(GaoProductCategory.Status.INACTIVE); + category.setUpdatedAt(Time.now()); + category.setSort(TimiJava.defaultIfNull(category.getSort(), 0)); + super.update(category); + return category; + }); + } + + @Override + public void delete(String id) { + disable(id); + } + + @Override + public void destroy(String id) { + throw new TimiException(com.imyeyu.java.bean.timi.TimiCode.ERROR, "产品分类不允许物理删除"); + } + + private void prepare(GaoProductCategory category, String excludeId) { + TimiException.required(category, "not found category"); + TimiException.required(category.getStoreId(), "not found category.storeId"); + storeScopeService.checkStoreId(category.getStoreId()); + String name = requiredText(category.getName(), "分类名称不能为空"); + TimiException.requiredNull(mapper.selectByStoreIdAndName(category.getStoreId(), name, excludeId), "门店内分类名称不能重复"); + category.setName(name); + category.setSort(TimiJava.defaultIfNull(category.getSort(), 0)); + category.setStatus(TimiJava.defaultIfNull(category.getStatus(), GaoProductCategory.Status.ACTIVE)); + TimiException.requiredTrue(category.getStatus() != null, "分类状态不能为空"); + TimiException.required(userLoginService.getRequireLoginUserId(), "not found operatorUserId"); + } + + private String requiredText(String value, String message) { + TimiException.required(value, message); + String result = value.trim(); + TimiException.requiredTrue(!result.isEmpty(), message); + return result; + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductServiceImplement.java new file mode 100644 index 0000000..72bb916 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductServiceImplement.java @@ -0,0 +1,284 @@ +package com.imyeyu.api.modules.gao.service.implement; + +import com.imyeyu.api.config.dbsource.TimiServerDBConfig; +import com.imyeyu.api.modules.common.entity.Attachment; +import com.imyeyu.api.modules.common.service.AttachmentService; +import com.imyeyu.api.modules.gao.entity.GaoProduct; +import com.imyeyu.api.modules.gao.entity.GaoProductCategory; +import com.imyeyu.api.modules.gao.entity.GaoProductUnit; +import com.imyeyu.api.modules.gao.mapper.GaoProductMapper; +import com.imyeyu.api.modules.gao.mapper.GaoProductUnitMapper; +import com.imyeyu.api.modules.gao.service.GaoProductCategoryService; +import com.imyeyu.api.modules.gao.service.GaoProductService; +import com.imyeyu.api.modules.gao.service.GaoProductUnitService; +import com.imyeyu.api.modules.gao.vo.GaoProductPage; +import com.imyeyu.api.modules.gao.vo.GaoProductUnitSelection; +import com.imyeyu.api.modules.system.entity.Logger; +import com.imyeyu.api.modules.system.service.LoggerService; +import com.imyeyu.api.modules.user.service.UserLoginService; +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.mapper.BaseMapper; +import com.imyeyu.spring.service.AbstractEntityService; +import com.imyeyu.utils.Time; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/// GAO 产品服务实现 +/// +/// @author Codex +/// @since 2026-09-02 +@Service +@RequiredArgsConstructor +public class GaoProductServiceImplement extends AbstractEntityService implements GaoProductService { + + private final GaoProductMapper mapper; + private final GaoProductUnitMapper unitMapper; + private final AttachmentService attachmentService; + private final ObjectProvider categoryServiceProvider; + private final GaoProductUnitService unitService; + private final UserLoginService userLoginService; + private final LoggerService loggerService; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + @Override + public PageResult pageByQuery(GaoProductPage page) { + TimiException.required(page, "not found product page"); + PageResult result = new PageResult<>(); + result.setTotal(mapper.countByQuery(page)); + List list = mapper.selectByQuery(page); + fillTransient(list); + result.setList(list); + return result; + } + + @Override + public GaoProduct detail(String id) { + GaoProduct product = get(id); + TimiException.required(product, "not found product"); + fillTransient(List.of(product)); + return product; + } + + @Override + public List listSellable(GaoProductPage page) { + TimiException.required(page, "not found product page"); + page.setStatus(GaoProduct.Status.ACTIVE); + List list = mapper.selectByQuery(page); + fillTransient(list); + for (GaoProduct product : list) { + product.setUnitList(product.getUnitList().stream().filter(unit -> Boolean.TRUE.equals(unit.getAllowSale())).toList()); + } + return list.stream().filter(product -> !product.getUnitList().isEmpty()).toList(); + } + + @Override + public GaoProductUnitSelection getSellableProductUnit(String storeId, String productId, String productUnitId) { + return getProductUnit(storeId, productId, productUnitId, false); + } + + @Override + public GaoProductUnitSelection getProductUnit(String storeId, String productId, String productUnitId, boolean purchase) { + TimiException.required(storeId, "not found storeId"); + TimiException.required(productId, "not found productId"); + TimiException.required(productUnitId, "not found productUnitId"); + GaoProduct product = get(productId); + TimiException.required(product, "not found product"); + TimiException.requiredTrue(storeId.equals(product.getStoreId()), "产品不属于当前门店"); + TimiException.requiredTrue(product.getStatus() == GaoProduct.Status.ACTIVE, "产品已停用"); + GaoProductUnit unit = unitMapper.selectForUpdate(productUnitId); + TimiException.required(unit, "not found product unit"); + TimiException.requiredTrue(productId.equals(unit.getProductId()), "产品单位不属于当前产品"); + TimiException.requiredTrue(!purchase || Boolean.TRUE.equals(unit.getAllowPurchase()), "产品单位不允许采购入库"); + TimiException.requiredTrue(purchase || Boolean.TRUE.equals(unit.getAllowSale()), "产品单位不允许销售"); + return new GaoProductUnitSelection(product, unit); + } + + @Override + public List listUnitByProductId(String productId) { + TimiException.required(productId, "not found productId"); + return unitMapper.selectByProductId(productId); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void create(GaoProduct product) { + loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_CREATE", product, () -> { + prepare(product, null); + super.create(product); + saveUnits(product.getId(), product.getUnitList(), List.of()); + attachmentService.updateByBizId(Attachment.BizType.GAO_PRODUCT, product.getId(), product.getAttachmentList()); + return product; + }); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void update(GaoProduct request) { + loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_UPDATE", request, () -> { + TimiException.required(request, "not found product"); + TimiException.required(request.getId(), "not found product.id"); + GaoProduct product = get(request.getId()); + TimiException.required(product, "not found product"); + product.setCategoryId(request.getCategoryId()); + product.setCode(TimiJava.defaultIfNull(request.getCode(), product.getCode())); + product.setName(TimiJava.defaultIfNull(request.getName(), product.getName())); + product.setProductType(TimiJava.defaultIfNull(request.getProductType(), product.getProductType())); + product.setBaseUnit(TimiJava.defaultIfNull(request.getBaseUnit(), product.getBaseUnit())); + product.setStockEnabled(TimiJava.defaultIfNull(request.getStockEnabled(), product.getStockEnabled())); + product.setAllowDecimalQuantity(TimiJava.defaultIfNull(request.getAllowDecimalQuantity(), product.getAllowDecimalQuantity())); + product.setStatus(TimiJava.defaultIfNull(request.getStatus(), product.getStatus())); + product.setSalePrice(request.getSalePrice()); + product.setReferenceCost(request.getReferenceCost()); + product.setMinStockQuantity(request.getMinStockQuantity()); + product.setSort(TimiJava.defaultIfNull(request.getSort(), product.getSort())); + product.setRemark(request.getRemark()); + List existingUnitList = unitMapper.selectByProductId(product.getId()); + product.setUnitList(request.getUnitList() == null ? existingUnitList : request.getUnitList()); + prepare(product, product.getId()); + super.update(product); + if (request.getUnitList() != null) { + saveUnits(product.getId(), product.getUnitList(), existingUnitList); + } + if (request.getAttachmentList() != null) { + attachmentService.updateByBizId(Attachment.BizType.GAO_PRODUCT, product.getId(), request.getAttachmentList()); + } + return product; + }); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void disable(String id) { + loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_DISABLE", id, () -> { + GaoProduct product = get(id); + TimiException.required(product, "not found product"); + product.setStatus(GaoProduct.Status.INACTIVE); + product.setUpdatedBy(userLoginService.getRequireLoginUserId()); + product.setUpdatedAt(Time.now()); + super.update(product); + return product; + }); + } + + @Override + public void delete(String id) { + disable(id); + } + + @Override + public void destroy(String id) { + throw new TimiException(TimiCode.ERROR, "已建立产品库的产品不允许物理删除"); + } + + private void prepare(GaoProduct product, String excludeId) { + TimiException.required(product, "not found product"); + TimiException.required(product.getStoreId(), "not found product.storeId"); + product.setCode(requiredText(product.getCode(), "产品编码不能为空")); + product.setName(requiredText(product.getName(), "产品名称不能为空")); + product.setProductType(TimiJava.defaultIfNull(product.getProductType(), GaoProduct.ProductType.GOODS)); + product.setBaseUnit(requiredText(product.getBaseUnit(), "基础单位不能为空")); + product.setStockEnabled(TimiJava.defaultIfNull(product.getStockEnabled(), product.getProductType() != GaoProduct.ProductType.SERVICE)); + TimiException.requiredTrue(product.getProductType() != GaoProduct.ProductType.SERVICE || !product.getStockEnabled(), "SERVICE 产品不能启用库存"); + product.setAllowDecimalQuantity(TimiJava.defaultIfNull(product.getAllowDecimalQuantity(), false)); + product.setStatus(TimiJava.defaultIfNull(product.getStatus(), GaoProduct.Status.ACTIVE)); + product.setSalePrice(nonNegative(product.getSalePrice(), "建议售价不能为负数")); + product.setReferenceCost(nonNegative(product.getReferenceCost(), "参考成本不能为负数")); + if (!Boolean.TRUE.equals(product.getStockEnabled())) { + TimiException.requiredNull(product.getMinStockQuantity(), "未启用库存的产品不能填写最低库存"); + } + if (product.getMinStockQuantity() != null) { + TimiException.requiredTrue(0 <= product.getMinStockQuantity().signum(), "最低库存不能为负数"); + TimiException.requiredTrue(3 >= product.getMinStockQuantity().scale(), "最低库存最多保留三位小数"); + } + product.setSort(TimiJava.defaultIfNull(product.getSort(), 0)); + TimiException.requiredNull(mapper.selectByStoreIdAndCode(product.getStoreId(), product.getCode(), excludeId), "门店内产品编码不能重复"); + if (product.getCategoryId() != null) { + GaoProductCategory category = categoryServiceProvider.getObject().get(product.getCategoryId()); + TimiException.required(category, "产品分类不存在"); + TimiException.requiredTrue(product.getStoreId().equals(category.getStoreId()), "产品分类不属于当前门店"); + } + product.setUnitList(unitService.validateAndNormalize(product, product.getUnitList())); + product.setUpdatedBy(userLoginService.getRequireLoginUserId()); + } + + private void saveUnits(String productId, List unitList, List existingUnitList) { + Map existingById = existingUnitList.stream() + .collect(Collectors.toMap(GaoProductUnit::getId, item -> item)); + Map existingByName = existingUnitList.stream() + .collect(Collectors.toMap(GaoProductUnit::getName, item -> item)); + Set retainedIdSet = new HashSet<>(); + for (GaoProductUnit source : unitList) { + GaoProductUnit existing = source.getId() == null ? existingByName.get(source.getName()) : existingById.get(source.getId()); + if (source.getId() != null && existing == null && !existingUnitList.isEmpty()) { + throw new TimiException(TimiCode.ERROR, "产品单位不存在或不属于当前产品"); + } + GaoProductUnit unit = new GaoProductUnit(); + unit.setId(existing == null ? null : existing.getId()); + unit.setProductId(productId); + unit.setName(source.getName()); + unit.setConversionToBase(source.getConversionToBase()); + unit.setIsBase(source.getIsBase()); + unit.setAllowPurchase(source.getAllowPurchase()); + unit.setAllowSale(source.getAllowSale()); + unit.setSort(source.getSort()); + if (existing == null) { + unitMapper.insert(unit); + } else { + retainedIdSet.add(existing.getId()); + TimiException.requiredTrue(1 == unitMapper.updateUnit(unit), "产品单位更新失败"); + } + } + for (GaoProductUnit existing : existingUnitList) { + if (!retainedIdSet.contains(existing.getId())) { + unitMapper.delete(existing.getId()); + } + } + } + + private void fillTransient(List list) { + if (list == null || list.isEmpty()) { + return; + } + List productIdList = list.stream().map(GaoProduct::getId).toList(); + Map> unitMap = unitMapper.selectByProductIdList(productIdList).stream() + .collect(Collectors.groupingBy(GaoProductUnit::getProductId)); + Map> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_PRODUCT, productIdList); + for (GaoProduct product : list) { + product.setUnitList(unitMap.getOrDefault(product.getId(), List.of())); + product.setAttachmentList(attachmentMap.getOrDefault(product.getId(), List.of())); + if (product.getCategoryId() != null) { + product.setCategory(categoryServiceProvider.getObject().get(product.getCategoryId())); + } + } + } + + private Long nonNegative(Long value, String message) { + if (value != null) { + TimiException.requiredTrue(0 <= value, message); + } + return value; + } + + private String requiredText(String value, String message) { + TimiException.required(value, message); + String result = value.trim(); + TimiException.requiredTrue(!result.isEmpty(), message); + return result; + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductStockServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductStockServiceImplement.java new file mode 100644 index 0000000..5c534c5 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductStockServiceImplement.java @@ -0,0 +1,374 @@ +package com.imyeyu.api.modules.gao.service.implement; + +import com.imyeyu.api.config.dbsource.TimiServerDBConfig; +import com.imyeyu.api.modules.common.entity.Attachment; +import com.imyeyu.api.modules.gao.entity.GaoProduct; +import com.imyeyu.api.modules.gao.entity.GaoProductStock; +import com.imyeyu.api.modules.gao.entity.GaoProductUnit; +import com.imyeyu.api.modules.gao.entity.GaoStockRecord; +import com.imyeyu.api.modules.gao.mapper.GaoProductStockMapper; +import com.imyeyu.api.modules.gao.service.GaoProductService; +import com.imyeyu.api.modules.gao.service.GaoProductStockService; +import com.imyeyu.api.modules.gao.service.GaoProductUnitService; +import com.imyeyu.api.modules.gao.service.GaoStockRecordService; +import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; +import com.imyeyu.api.modules.gao.vo.GaoProductStockChangeRequest; +import com.imyeyu.api.modules.gao.vo.GaoProductStockPage; +import com.imyeyu.api.modules.gao.vo.GaoProductUnitSelection; +import com.imyeyu.api.modules.gao.vo.GaoStockChangeItem; +import com.imyeyu.api.modules.system.entity.Logger; +import com.imyeyu.api.modules.system.service.LoggerService; +import com.imyeyu.api.modules.user.service.UserLoginService; +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.utils.Time; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +/// GAO 产品库存服务实现 +/// +/// @author Codex +/// @since 2026-09-02 +@Service +@RequiredArgsConstructor +public class GaoProductStockServiceImplement implements GaoProductStockService { + + private static final BigDecimal ZERO = BigDecimal.ZERO; + + private final GaoProductStockMapper stockMapper; + private final GaoStockRecordService recordService; + private final GaoProductService productService; + private final GaoProductUnitService unitService; + private final GaoStoreScopeService storeScopeService; + private final UserLoginService userLoginService; + private final LoggerService loggerService; + + @Override + public PageResult pageByQuery(GaoProductStockPage page) { + TimiException.required(page, "not found stock page"); + PageResult result = new PageResult<>(); + result.setTotal(stockMapper.countByQuery(page)); + List list = stockMapper.selectByQuery(page); + for (GaoProductStock stock : list) { + fillBalance(stock); + } + result.setList(list); + return result; + } + + @Override + public GaoProductStock detail(String storeId, String productId) { + TimiException.required(storeId, "not found storeId"); + TimiException.required(productId, "not found productId"); + storeScopeService.checkStoreId(storeId); + GaoProduct product = productService.get(productId); + TimiException.required(product, "not found product"); + TimiException.requiredTrue(storeId.equals(product.getStoreId()), "产品不属于当前门店"); + GaoProductStock stock = stockMapper.selectByStoreIdAndProductId(storeId, productId); + if (stock == null) { + stock = new GaoProductStock(); + stock.setStoreId(storeId); + stock.setProductId(productId); + stock.setOnHandQuantity(ZERO); + stock.setReservedQuantity(ZERO); + } + stock.setProduct(product); + fillBalance(stock); + return stock; + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoProductStock initialIn(GaoProductStockChangeRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_STOCK_INITIAL_IN", request, () -> change(request, GaoStockRecord.OperationType.INITIAL_IN)); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoProductStock manualIn(GaoProductStockChangeRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_STOCK_MANUAL_IN", request, () -> change(request, GaoStockRecord.OperationType.MANUAL_IN)); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoProductStock manualOut(GaoProductStockChangeRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_STOCK_MANUAL_OUT", request, () -> change(request, GaoStockRecord.OperationType.MANUAL_OUT)); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoProductStock stocktakeGain(GaoProductStockChangeRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_STOCK_STOCKTAKE_GAIN", request, () -> change(request, GaoStockRecord.OperationType.STOCKTAKE_GAIN)); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoProductStock stocktakeLoss(GaoProductStockChangeRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_STOCK_STOCKTAKE_LOSS", request, () -> change(request, GaoStockRecord.OperationType.STOCKTAKE_LOSS)); + } + + @Override + @Transactional(TimiServerDBConfig.ROLLBACKER) + public boolean hasSufficientForFulfillment(String storeId, List items) { + TimiException.required(storeId, "not found storeId"); + if (items == null || items.isEmpty()) { + return true; + } + List sortedItems = new ArrayList<>(items); + sortedItems.sort(Comparator.comparing(GaoStockChangeItem::getProductId)); + for (GaoStockChangeItem item : sortedItems) { + GaoProductStock stock = stockMapper.selectForUpdate(storeId, item.getProductId()); + if (stock == null || decimal(stock.getOnHandQuantity()).subtract(decimal(stock.getReservedQuantity())).compareTo(item.getBaseQuantity()) < 0) { + return false; + } + } + return true; + } + + @Override + @Transactional(TimiServerDBConfig.ROLLBACKER) + public List reserveForFulfillment(String storeId, String bizId, List items, String requestNo, String operatorUserId) { + return executeInternal(storeId, bizId, items, requestNo, operatorUserId, GaoStockRecord.OperationType.ORDER_RESERVE, null, null); + } + + @Override + @Transactional(TimiServerDBConfig.ROLLBACKER) + public List releaseForFulfillment(String storeId, String bizId, List items, String requestNo, String operatorUserId) { + return executeInternal(storeId, bizId, items, requestNo, operatorUserId, GaoStockRecord.OperationType.ORDER_RELEASE, null, null); + } + + @Override + @Transactional(TimiServerDBConfig.ROLLBACKER) + public List outForFulfillment(String storeId, String bizId, List items, String requestNo, String operatorUserId) { + return executeInternal(storeId, bizId, items, requestNo, operatorUserId, GaoStockRecord.OperationType.ORDER_OUT, null, null); + } + + @Override + @Transactional(TimiServerDBConfig.ROLLBACKER) + public List revokeForFulfillment(String storeId, String bizId, List items, String requestNo, String operatorUserId) { + return executeInternal(storeId, bizId, items, requestNo, operatorUserId, GaoStockRecord.OperationType.ORDER_REVOKE, null, null); + } + + private GaoProductStock change(GaoProductStockChangeRequest request, GaoStockRecord.OperationType operationType) { + TimiException.required(request, "not found stock change request"); + String requestNo = requiredText(request.getRequestNo(), "not found requestNo"); + String storeId = storeScopeService.resolveStoreId(request.getStoreId()); + TimiException.required(storeId, "not found storeId"); + boolean purchase = operationType == GaoStockRecord.OperationType.INITIAL_IN + || operationType == GaoStockRecord.OperationType.MANUAL_IN + || operationType == GaoStockRecord.OperationType.STOCKTAKE_GAIN; + GaoProductUnitSelection selection = productService.getProductUnit(storeId, request.getProductId(), request.getProductUnitId(), purchase); + GaoProduct product = selection.getProduct(); + GaoProductUnit unit = selection.getUnit(); + TimiException.requiredTrue(Boolean.TRUE.equals(product.getStockEnabled()), "该产品未启用库存管理"); + BigDecimal quantity = validateQuantity(product, request.getQuantity()); + BigDecimal baseQuantity = unitService.convertToBase(unit, quantity); + List existingList = recordService.listByRequestNo(storeId, requestNo); + if (!existingList.isEmpty()) { + GaoStockRecord existing = existingList.getFirst(); + verifyExisting(existing, operationType, product.getId(), unit.getId(), quantity, baseQuantity, request.getRemark(), request.getReason()); + return detail(storeId, product.getId()); + } + TimiException.requiredTrue(hasAttachment(request.getAttachmentList()), "库存操作必须上传照片"); + TimiException.requiredTrue(hasReason(request.getReason()) || hasReason(request.getRemark()), "库存操作必须填写原因或备注"); + GaoStockChangeItem item = GaoStockChangeItem.of(product, unit, quantity, baseQuantity); + List result = executeInternal(storeId, product.getId(), List.of(item), requestNo, + userLoginService.getRequireLoginUserId(), operationType, request.getReason(), request.getRemark(), request.getAttachmentList()); + return result.getFirst(); + } + + private List executeInternal(String storeId, String bizId, List sourceList, String requestNo, + String operatorUserId, GaoStockRecord.OperationType operationType, String reason, String remark) { + return executeInternal(storeId, bizId, sourceList, requestNo, operatorUserId, operationType, reason, remark, null); + } + + private List executeInternal(String storeId, String bizId, List sourceList, String requestNo, + String operatorUserId, GaoStockRecord.OperationType operationType, String reason, String remark, List attachmentList) { + TimiException.required(storeId, "not found storeId"); + TimiException.required(bizId, "not found stock bizId"); + TimiException.requiredTrue(sourceList != null && !sourceList.isEmpty(), "库存变更至少需要一个产品"); + String normalizedRequestNo = requiredText(requestNo, "not found requestNo"); + storeScopeService.checkStoreId(storeId); + List itemList = new ArrayList<>(sourceList); + itemList.sort(Comparator.comparing(GaoStockChangeItem::getProductId)); + Set productIdSet = new HashSet<>(); + for (GaoStockChangeItem item : itemList) { + TimiException.required(item, "库存变更项不能为空"); + TimiException.requiredTrue(productIdSet.add(item.getProductId()), "同一库存操作不能重复产品"); + TimiException.required(item.getBaseQuantity(), "基础单位数量不能为空"); + TimiException.requiredTrue(0 < item.getBaseQuantity().signum(), "基础单位数量必须大于 0"); + } + List existingList = recordService.listByRequestNo(storeId, normalizedRequestNo); + if (!existingList.isEmpty()) { + verifyExistingList(existingList, itemList, operationType, bizId); + List result = new ArrayList<>(); + for (GaoStockChangeItem item : itemList) { + result.add(detail(storeId, item.getProductId())); + } + return result; + } + List result = new ArrayList<>(); + for (GaoStockChangeItem item : itemList) { + GaoProductStock stock = lockOrCreateStock(storeId, item.getProductId()); + BigDecimal onHandBefore = decimal(stock.getOnHandQuantity()); + BigDecimal reservedBefore = decimal(stock.getReservedQuantity()); + BigDecimal onHandDelta = onHandDelta(operationType, item.getBaseQuantity()); + BigDecimal reservedDelta = reservedDelta(operationType, item.getBaseQuantity()); + int updated = stockMapper.updateQuantity(stock.getId(), onHandDelta, reservedDelta, operatorUserId); + TimiException.requiredTrue(updated == 1, "库存余额已变化或库存数量不足"); + BigDecimal onHandAfter = onHandBefore.add(onHandDelta); + BigDecimal reservedAfter = reservedBefore.add(reservedDelta); + GaoStockRecord record = new GaoStockRecord(); + record.setStoreId(storeId); + record.setProductId(item.getProductId()); + record.setProductUnitId(item.getProductUnitId()); + record.setProductNameSnapshot(item.getProductNameSnapshot()); + record.setUnitSnapshot(item.getUnitSnapshot()); + record.setConversionToBaseSnapshot(item.getConversionToBaseSnapshot()); + record.setQuantity(item.getQuantity()); + record.setBaseQuantity(item.getBaseQuantity()); + record.setOperationType(operationType); + record.setOnHandDelta(onHandDelta); + record.setReservedDelta(reservedDelta); + record.setOnHandBefore(onHandBefore); + record.setOnHandAfter(onHandAfter); + record.setReservedBefore(reservedBefore); + record.setReservedAfter(reservedAfter); + record.setBizType(operationType.name().startsWith("ORDER_") ? GaoStockRecord.BizType.ORDER_FULFILLMENT : GaoStockRecord.BizType.PRODUCT_STOCK); + record.setBizId(bizId); + record.setSourceRecordId(item.getSourceRecordId()); + record.setRequestNo(normalizedRequestNo); + record.setOperatorUserId(operatorUserId); + record.setOccurredAt(Time.now()); + record.setReason(reason); + record.setRemark(remark); + recordService.createOperationRecord(record, attachmentList); + stock.setOnHandQuantity(onHandAfter); + stock.setReservedQuantity(reservedAfter); + stock.setUpdatedBy(operatorUserId); + fillBalance(stock); + result.add(stock); + } + return result; + } + + private GaoProductStock lockOrCreateStock(String storeId, String productId) { + GaoProductStock stock = stockMapper.selectForUpdate(storeId, productId); + if (stock != null) { + return stock; + } + stock = new GaoProductStock(); + stock.setId(UUID.randomUUID().toString()); + stock.setStoreId(storeId); + stock.setProductId(productId); + stock.setOnHandQuantity(ZERO); + stock.setReservedQuantity(ZERO); + stock.setUpdatedBy(userLoginService.getRequireLoginUserId()); + stock.setCreatedAt(Time.now()); + stockMapper.insertIgnore(stock); + stock = stockMapper.selectForUpdate(storeId, productId); + TimiException.required(stock, "库存余额创建失败"); + return stock; + } + + private BigDecimal onHandDelta(GaoStockRecord.OperationType operationType, BigDecimal quantity) { + return switch (operationType) { + case INITIAL_IN, MANUAL_IN, STOCKTAKE_GAIN, ORDER_REVOKE -> quantity; + case MANUAL_OUT, STOCKTAKE_LOSS, ORDER_OUT -> quantity.negate(); + case ORDER_RESERVE, ORDER_RELEASE -> ZERO; + }; + } + + private BigDecimal reservedDelta(GaoStockRecord.OperationType operationType, BigDecimal quantity) { + return switch (operationType) { + case ORDER_RESERVE -> quantity; + case ORDER_RELEASE, ORDER_OUT -> quantity.negate(); + default -> ZERO; + }; + } + + private BigDecimal validateQuantity(GaoProduct product, BigDecimal quantity) { + TimiException.required(quantity, "数量不能为空"); + TimiException.requiredTrue(0 < quantity.signum(), "数量必须大于 0"); + TimiException.requiredTrue(3 >= quantity.stripTrailingZeros().scale(), "数量最多保留三位小数"); + if (!Boolean.TRUE.equals(product.getAllowDecimalQuantity())) { + TimiException.requiredTrue(quantity.stripTrailingZeros().scale() <= 0, "该产品不允许小数数量"); + } + return quantity; + } + + private void verifyExisting(GaoStockRecord existing, GaoStockRecord.OperationType operationType, String productId, String unitId, + BigDecimal quantity, BigDecimal baseQuantity, String remark, String reason) { + TimiException.requiredTrue(existing.getOperationType() == operationType + && productId.equals(existing.getProductId()) + && unitId.equals(existing.getProductUnitId()) + && quantity.compareTo(existing.getQuantity()) == 0 + && baseQuantity.compareTo(existing.getBaseQuantity()) == 0 + && sameText(existing.getRemark(), remark) + && sameText(existing.getReason(), reason), "requestNo 对应库存操作内容不一致"); + } + + private void verifyExistingList(List existingList, List itemList, GaoStockRecord.OperationType operationType, String bizId) { + Map recordMap = new HashMap<>(); + for (GaoStockRecord record : existingList) { + TimiException.requiredTrue(record.getOperationType() == operationType && bizId.equals(record.getBizId()), "requestNo 已被其他库存操作使用"); + recordMap.put(record.getProductId(), record); + } + TimiException.requiredTrue(recordMap.size() == itemList.size(), "requestNo 对应库存操作内容不一致"); + for (GaoStockChangeItem item : itemList) { + GaoStockRecord record = recordMap.get(item.getProductId()); + TimiException.required(record, "requestNo 对应库存操作内容不一致"); + TimiException.requiredTrue(item.getBaseQuantity().compareTo(record.getBaseQuantity()) == 0, "requestNo 对应库存操作内容不一致"); + } + } + + private void fillBalance(GaoProductStock stock) { + stock.setOnHandQuantity(decimal(stock.getOnHandQuantity())); + stock.setReservedQuantity(decimal(stock.getReservedQuantity())); + stock.setAvailableQuantity(stock.getOnHandQuantity().subtract(stock.getReservedQuantity())); + if (stock.getProduct() == null && stock.getProductId() != null) { + stock.setProduct(productService.get(stock.getProductId())); + } + } + + private boolean hasAttachment(List list) { + for (Attachment attachment : TimiJava.safeIterable(list)) { + if (attachment != null && (attachment.getAttachType() == null || "PHOTO".equalsIgnoreCase(attachment.getAttachType()))) { + return true; + } + } + return false; + } + + private boolean hasReason(String value) { + return value != null && !value.trim().isEmpty(); + } + + private String requiredText(String value, String message) { + TimiException.required(value, message); + String result = value.trim(); + TimiException.requiredTrue(!result.isEmpty(), message); + return result; + } + + private boolean sameText(String left, String right) { + return left == null ? right == null : left.equals(right); + } + + private BigDecimal decimal(BigDecimal value) { + return value == null ? ZERO : value; + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductUnitServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductUnitServiceImplement.java new file mode 100644 index 0000000..4af2824 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductUnitServiceImplement.java @@ -0,0 +1,97 @@ +package com.imyeyu.api.modules.gao.service.implement; + +import com.imyeyu.api.modules.gao.entity.GaoProduct; +import com.imyeyu.api.modules.gao.entity.GaoProductUnit; +import com.imyeyu.api.modules.gao.service.GaoProductUnitService; +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiException; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/// GAO 产品单位服务实现 +/// +/// @author Codex +/// @since 2026-09-02 +@Service +public class GaoProductUnitServiceImplement implements GaoProductUnitService { + + private static final BigDecimal ONE = BigDecimal.ONE; + + @Override + public List validateAndNormalize(GaoProduct product, List sourceList) { + TimiException.required(product, "not found product"); + String baseUnit = requiredText(product.getBaseUnit(), "基础单位不能为空"); + product.setBaseUnit(baseUnit); + List inputList = sourceList == null || sourceList.isEmpty() + ? List.of(createBaseUnit(product)) : sourceList; + List result = new ArrayList<>(); + Set nameSet = new HashSet<>(); + Set idSet = new HashSet<>(); + GaoProductUnit base = null; + for (int i = 0; i < inputList.size(); i++) { + GaoProductUnit source = inputList.get(i); + TimiException.required(source, "产品单位不能为空"); + GaoProductUnit unit = new GaoProductUnit(); + unit.setId(source.getId()); + if (unit.getId() != null) { + TimiException.requiredTrue(idSet.add(unit.getId()), "产品单位 ID 不能重复"); + } + unit.setName(requiredText(source.getName(), "产品单位名称不能为空")); + TimiException.requiredTrue(nameSet.add(unit.getName()), "同一产品的单位名称不能重复"); + unit.setConversionToBase(TimiJava.defaultIfNull(source.getConversionToBase(), ONE)); + TimiException.requiredTrue(0 < unit.getConversionToBase().signum(), "单位折算率必须大于 0"); + TimiException.requiredTrue(6 >= unit.getConversionToBase().scale(), "单位折算率最多保留六位小数"); + unit.setIsBase(Boolean.TRUE.equals(source.getIsBase())); + unit.setAllowPurchase(TimiJava.defaultIfNull(source.getAllowPurchase(), true)); + unit.setAllowSale(TimiJava.defaultIfNull(source.getAllowSale(), true)); + unit.setSort(TimiJava.defaultIfNull(source.getSort(), i)); + if (unit.getIsBase()) { + TimiException.requiredNull(base, "一个产品只能有一个基础单位"); + TimiException.requiredTrue(unit.getConversionToBase().compareTo(ONE) == 0, "基础单位折算率必须为 1"); + TimiException.requiredTrue(baseUnit.equals(unit.getName()), "基础单位名称必须与产品基础单位一致"); + base = unit; + } + result.add(unit); + } + TimiException.required(base, "产品必须有一个基础单位"); + TimiException.requiredTrue(product.getProductType() != GaoProduct.ProductType.SERVICE || !Boolean.TRUE.equals(product.getStockEnabled()), "SERVICE 产品不能启用库存"); + return result; + } + + @Override + public BigDecimal convertToBase(GaoProductUnit unit, BigDecimal quantity) { + TimiException.required(unit, "not found product unit"); + TimiException.required(quantity, "数量不能为空"); + TimiException.requiredTrue(0 < quantity.signum(), "数量必须大于 0"); + BigDecimal result = quantity.multiply(unit.getConversionToBase()); + try { + return result.setScale(3, RoundingMode.UNNECESSARY); + } catch (ArithmeticException e) { + throw new TimiException(com.imyeyu.java.bean.timi.TimiCode.ERROR, "业务数量折算后不能精确保存为三位小数", e); + } + } + + private GaoProductUnit createBaseUnit(GaoProduct product) { + GaoProductUnit unit = new GaoProductUnit(); + unit.setName(product.getBaseUnit()); + unit.setConversionToBase(ONE); + unit.setIsBase(true); + unit.setAllowPurchase(Boolean.TRUE.equals(product.getStockEnabled())); + unit.setAllowSale(true); + unit.setSort(0); + return unit; + } + + private String requiredText(String value, String message) { + TimiException.required(value, message); + String result = value.trim(); + TimiException.requiredTrue(!result.isEmpty(), message); + return result; + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoSalesOrderServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoSalesOrderServiceImplement.java new file mode 100644 index 0000000..61af01d --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoSalesOrderServiceImplement.java @@ -0,0 +1,470 @@ +package com.imyeyu.api.modules.gao.service.implement; + +import com.imyeyu.api.bean.ModuleCode; +import com.imyeyu.api.config.dbsource.TimiServerDBConfig; +import com.imyeyu.api.modules.gao.bean.GaoPermissionCode; +import com.imyeyu.api.modules.gao.bean.GaoRoleCode; +import com.imyeyu.api.modules.gao.entity.GaoCustomer; +import com.imyeyu.api.modules.gao.entity.GaoCustomerPackage; +import com.imyeyu.api.modules.gao.entity.GaoEvent; +import com.imyeyu.api.modules.gao.entity.GaoEventRecord; +import com.imyeyu.api.modules.gao.entity.GaoFundRecord; +import com.imyeyu.api.modules.gao.entity.GaoProduct; +import com.imyeyu.api.modules.gao.entity.GaoProductUnit; +import com.imyeyu.api.modules.gao.entity.GaoSalesOrder; +import com.imyeyu.api.modules.gao.entity.GaoSalesOrderItem; +import com.imyeyu.api.modules.gao.entity.GaoStore; +import com.imyeyu.api.modules.gao.mapper.GaoSalesOrderItemMapper; +import com.imyeyu.api.modules.gao.mapper.GaoSalesOrderMapper; +import com.imyeyu.api.modules.gao.service.GaoCustomerService; +import com.imyeyu.api.modules.gao.service.GaoCustomerPackageService; +import com.imyeyu.api.modules.gao.service.GaoEventRecordService; +import com.imyeyu.api.modules.gao.service.GaoFundRecordService; +import com.imyeyu.api.modules.gao.service.GaoOrderFulfillmentService; +import com.imyeyu.api.modules.gao.service.GaoProductService; +import com.imyeyu.api.modules.gao.service.GaoProductUnitService; +import com.imyeyu.api.modules.gao.service.GaoSalesOrderService; +import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; +import com.imyeyu.api.modules.gao.service.GaoStoreService; +import com.imyeyu.api.modules.gao.vo.GaoDirectProductOrderCreateRequest; +import com.imyeyu.api.modules.gao.vo.GaoPackageOrderCreateData; +import com.imyeyu.api.modules.gao.vo.GaoProductUnitSelection; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderCancelRequest; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderItemRequest; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderPage; +import com.imyeyu.api.modules.system.entity.Logger; +import com.imyeyu.api.modules.system.service.LoggerService; +import com.imyeyu.api.modules.user.service.PermissionChecker; +import com.imyeyu.api.modules.user.service.RoleChecker; +import com.imyeyu.api.modules.user.service.UserLoginService; +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.mapper.BaseMapper; +import com.imyeyu.spring.service.AbstractEntityService; +import org.springframework.beans.factory.ObjectProvider; +import com.imyeyu.utils.Time; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +/// GAO 销售订单服务实现 +/// +/// @author Codex +/// @since 2026-09-02 +@Service +@RequiredArgsConstructor +public class GaoSalesOrderServiceImplement extends AbstractEntityService implements GaoSalesOrderService { + + private static final BigDecimal ZERO = BigDecimal.ZERO; + + private final GaoSalesOrderMapper mapper; + private final GaoSalesOrderItemMapper itemMapper; + private final GaoFundRecordService fundRecordService; + private final ObjectProvider fulfillmentServiceProvider; + private final ObjectProvider customerPackageServiceProvider; + private final GaoCustomerService customerService; + private final GaoProductService productService; + private final GaoProductUnitService unitService; + private final GaoStoreService storeService; + private final GaoStoreScopeService storeScopeService; + private final GaoEventRecordService eventRecordService; + private final PermissionChecker permissionChecker; + private final RoleChecker roleChecker; + private final UserLoginService userLoginService; + private final LoggerService loggerService; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + @Override + public PageResult pageByQuery(GaoSalesOrderPage page) { + TimiException.required(page, "not found sales order page"); + PageResult result = new PageResult<>(); + result.setTotal(mapper.countByQuery(page)); + List list = mapper.selectByQuery(page); + fillItemList(list); + result.setList(list); + return result; + } + + @Override + public GaoSalesOrder detail(String id) { + GaoSalesOrder order = get(id); + TimiException.required(order, "not found sales order"); + storeScopeService.checkStoreId(order.getStoreId()); + order.setItemList(itemMapper.selectBySalesOrderId(order.getId())); + order.setFundRecordList(fundRecordService.listByBizId(order.getId())); + if (order.getSourceEventRecordId() != null) { + order.setSourceEventRecord(eventRecordService.get(order.getSourceEventRecordId())); + } + order.setUnpaidAmount(safe(order.getSaleAmount()) - safe(order.getPaidAmount())); + return order; + } + + @Override + public GaoSalesOrder getForUpdate(String id) { + GaoSalesOrder order = mapper.selectForUpdate(id); + TimiException.required(order, "not found sales order"); + storeScopeService.checkStoreId(order.getStoreId()); + return order; + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoSalesOrder createDirectProductOrder(GaoDirectProductOrderCreateRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_SALES_ORDER_DIRECT_PRODUCT_CREATE", request, () -> createDirectInternal(request)); + } + + private GaoSalesOrder createDirectInternal(GaoDirectProductOrderCreateRequest request) { + TimiException.required(request, "not found direct product order request"); + String requestNo = requiredText(request.getRequestNo(), "not found requestNo"); + String storeId = storeScopeService.resolveStoreId(request.getStoreId()); + TimiException.required(storeId, "not found storeId"); + GaoSalesOrder existing = mapper.selectByRequestNo(storeId, requestNo); + if (existing != null) { + TimiException.requiredTrue(existing.getOrderType() == GaoSalesOrder.OrderType.DIRECT_PRODUCT, "requestNo 已被其他订单使用"); + verifyExistingDirectItems(existing, request.getItemList()); + return detail(existing.getId()); + } + GaoCustomer customer = customerService.get(request.getCustomerId()); + TimiException.required(customer, "not found customer"); + TimiException.requiredTrue(storeId.equals(customer.getStoreId()), "客户不属于当前门店"); + GaoStore store = storeService.get(storeId); + TimiException.required(store, "not found store"); + storeScopeService.checkStore(store); + GaoEvent consumeEvent = storeService.validateDirectProductSaleEvent(store); + List itemList = resolveDirectItems(storeId, request.getItemList()); + long saleAmount = 0L; + for (GaoSalesOrderItem item : itemList) { + saleAmount = Math.addExact(saleAmount, item.getLineAmount()); + } + Long purchasedAt = TimiJava.defaultIfNull(request.getPurchasedAt(), Time.now()); + String operatorUserId = userLoginService.getRequireLoginUserId(); + GaoSalesOrder order = newOrder(storeId, customer.getId(), GaoSalesOrder.OrderType.DIRECT_PRODUCT, requestNo, purchasedAt, operatorUserId, request.getRemark()); + order.setSaleAmount(saleAmount); + order.setPaymentStatus(saleAmount == 0 ? GaoSalesOrder.PaymentStatus.PAID : GaoSalesOrder.PaymentStatus.UNPAID); + super.create(order); + for (GaoSalesOrderItem item : itemList) { + item.setSalesOrderId(order.getId()); + itemMapper.insert(item); + } + GaoEventRecord record = new GaoEventRecord(); + record.setCustomerId(customer.getId()); + record.setStoreId(storeId); + record.setEventId(consumeEvent.getId()); + record.setNumberValue(saleAmount); + record.setRegisteredAt(purchasedAt); + record.setRemark(request.getRemark()); + record.setAttachmentList(request.getAttachmentList()); + GaoEventRecord createdRecord = eventRecordService.createOrderConsumeRecord(record); + TimiException.requiredTrue(1 == mapper.updateSourceEventRecordId(order.getId(), createdRecord.getId()), "订单来源事件写入失败"); + order.setSourceEventRecordId(createdRecord.getId()); + order.setItemList(itemList); + return order; + } + + @Override + public GaoSalesOrder getByStoreIdAndRequestNo(String storeId, String requestNo) { + TimiException.required(storeId, "not found storeId"); + TimiException.required(requestNo, "not found requestNo"); + return mapper.selectByRequestNo(storeId, requestNo); + } + + @Override + @Transactional(TimiServerDBConfig.ROLLBACKER) + public GaoSalesOrder createPackageOrder(GaoPackageOrderCreateData data) { + TimiException.required(data, "not found package order data"); + String requestNo = requiredText(data.getRequestNo(), "not found requestNo"); + String storeId = requiredText(data.getStoreId(), "not found storeId"); + storeScopeService.checkStoreId(storeId); + GaoSalesOrder existing = mapper.selectByRequestNo(storeId, requestNo); + if (existing != null) { + TimiException.requiredTrue(existing.getOrderType() == GaoSalesOrder.OrderType.PACKAGE + && data.getCustomerId().equals(existing.getCustomerId()) + && data.getSaleAmount().equals(existing.getSaleAmount()) + && data.getSourceEventRecordId().equals(existing.getSourceEventRecordId()), "requestNo 已被其他订单使用"); + return existing; + } + GaoCustomer customer = customerService.get(data.getCustomerId()); + TimiException.required(customer, "not found customer"); + TimiException.requiredTrue(storeId.equals(customer.getStoreId()), "客户不属于当前门店"); + TimiException.required(data.getPackageConfigId(), "not found packageConfigId"); + TimiException.required(data.getSourceEventRecordId(), "not found sourceEventRecordId"); + TimiException.requiredTrue(0 <= data.getSaleAmount(), "成交价不能为负数"); + GaoSalesOrder order = newOrder(storeId, customer.getId(), GaoSalesOrder.OrderType.PACKAGE, requestNo, + TimiJava.defaultIfNull(data.getPurchasedAt(), Time.now()), data.getOperatorUserId(), data.getRemark()); + order.setSaleAmount(data.getSaleAmount()); + order.setSourceEventRecordId(data.getSourceEventRecordId()); + order.setPaymentStatus(data.getSaleAmount() == 0 ? GaoSalesOrder.PaymentStatus.PAID : GaoSalesOrder.PaymentStatus.UNPAID); + super.create(order); + return order; + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoSalesOrder cancel(GaoSalesOrderCancelRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_SALES_ORDER_CANCEL", request, () -> cancelInternal(request)); + } + + private GaoSalesOrder cancelInternal(GaoSalesOrderCancelRequest request) { + TimiException.required(request, "not found cancel request"); + TimiException.required(request.getSalesOrderId(), "not found salesOrderId"); + TimiException.required(request.getReason(), "取消原因不能为空"); + String reason = request.getReason().trim(); + TimiException.requiredTrue(!reason.isEmpty(), "取消原因不能为空"); + GaoSalesOrder order = getForUpdate(request.getSalesOrderId()); + if (order.getStatus() == GaoSalesOrder.Status.CANCELLED) { + return order; + } + TimiException.requiredTrue(order.getStatus() == GaoSalesOrder.Status.ACTIVE, "当前订单不可取消"); + TimiException.requiredTrue(fundRecordService.countPendingReceiveByBizId(order.getId()) == 0, "订单存在待核销收款,不能取消"); + TimiException.requiredTrue(fulfillmentServiceProvider.getObject().countPendingBySalesOrderId(order.getId()) == 0, "订单存在待履约操作,不能取消"); + GaoEventRecord sourceRecord = order.getSourceEventRecordId() == null ? null : eventRecordService.get(order.getSourceEventRecordId()); + if (sourceRecord != null) { + eventRecordService.revokeOrderConsumeRecord(sourceRecord); + } + String userId = userLoginService.getRequireLoginUserId(); + GaoSalesOrder.RefundStatus refundStatus = safe(order.getPaidAmount()) == 0 ? GaoSalesOrder.RefundStatus.REFUNDED : GaoSalesOrder.RefundStatus.PENDING; + TimiException.requiredTrue(1 == mapper.cancelIf(order.getId(), GaoSalesOrder.Status.ACTIVE, GaoSalesOrder.Status.CANCELLED, + Time.now(), userId, reason, refundStatus), "订单状态已变化"); + order.setStatus(GaoSalesOrder.Status.CANCELLED); + order.setCancelledAt(Time.now()); + order.setCancelOperatorUserId(userId); + order.setCancelReason(reason); + order.setRefundStatus(refundStatus); + if (order.getOrderType() == GaoSalesOrder.OrderType.PACKAGE) { + GaoCustomerPackageService customerPackageService = customerPackageServiceProvider.getObject(); + GaoCustomerPackage packageEntity = customerPackageService.getBySalesOrderId(order.getId()); + if (packageEntity != null && packageEntity.getStatus() == GaoCustomerPackage.Status.ACTIVE) { + packageEntity.setStatus(GaoCustomerPackage.Status.CANCELLED); + packageEntity.setCancelledAt(order.getCancelledAt()); + packageEntity.setCancelOperatorUserId(userId); + packageEntity.setCancelReason(reason); + customerPackageService.updateAggregateState(packageEntity); + } + } + return order; + } + + @Override + public void updatePayment(GaoSalesOrder order) { + TimiException.required(order, "not found sales order"); + TimiException.requiredTrue(1 == mapper.updatePayment(order.getId(), safeLong(order.getPendingPaidAmount()), safeLong(order.getPaidAmount()), + safeLong(order.getRefundedAmount()), order.getPaymentStatus(), order.getRefundStatus()), "订单付款汇总更新失败"); + } + + @Override + public void recomputePaymentStatus(GaoSalesOrder order) { + long saleAmount = safeLong(order.getSaleAmount()); + long paidAmount = safeLong(order.getPaidAmount()); + long pendingAmount = safeLong(order.getPendingPaidAmount()); + TimiException.requiredTrue(0 <= paidAmount && paidAmount <= saleAmount, "有效收款超出订单金额"); + TimiException.requiredTrue(0 <= pendingAmount && pendingAmount + paidAmount <= saleAmount, "待核销收款超出订单金额"); + order.setPaymentStatus(saleAmount == 0 || paidAmount == saleAmount + ? GaoSalesOrder.PaymentStatus.PAID : (0 < paidAmount ? GaoSalesOrder.PaymentStatus.PARTIALLY_PAID : GaoSalesOrder.PaymentStatus.UNPAID)); + } + + @Override + public void checkFulfillmentPermission(GaoSalesOrder order) { + TimiException.required(order, "not found sales order"); + if (safe(order.getSaleAmount()) == 0 || safe(order.getPaidAmount()) == safe(order.getSaleAmount())) { + return; + } + TimiException.requiredTrue(0 < safe(order.getPaidAmount()), "订单未有已核销收款,不能履约"); + roleChecker.checkAny(ModuleCode.GAO, GaoRoleCode.STORE_MANAGER.name()); + } + + @Override + public int updateItemReservedQuantity(String itemId, BigDecimal delta) { + return itemMapper.updateReservedQuantity(itemId, delta); + } + + @Override + public int updateItemFulfilledQuantity(String itemId, BigDecimal delta) { + return itemMapper.updateFulfilledQuantity(itemId, delta); + } + + @Override + public GaoSalesOrderItem getItemForUpdate(String itemId) { + TimiException.required(itemId, "not found salesOrderItemId"); + return itemMapper.selectForUpdate(itemId); + } + + @Override + public void refreshFulfillmentStatus(GaoSalesOrder order) { + TimiException.required(order, "not found sales order"); + if (order.getStatus() == GaoSalesOrder.Status.CANCELLED) { + return; + } + List itemList = itemMapper.selectBySalesOrderId(order.getId()); + boolean fulfilled = false; + if (order.getOrderType() == GaoSalesOrder.OrderType.DIRECT_PRODUCT) { + fulfilled = !itemList.isEmpty(); + for (GaoSalesOrderItem item : itemList) { + if (decimal(item.getFulfilledQuantity()).compareTo(decimal(item.getQuantity())) < 0) { + fulfilled = false; + break; + } + } + } + updateFulfillmentStatus(order, fulfilled); + } + + @Override + public void updateFulfillmentStatus(GaoSalesOrder order, boolean fulfilled) { + TimiException.required(order, "not found sales order"); + if (order.getStatus() == GaoSalesOrder.Status.CANCELLED) { + return; + } + GaoSalesOrder.Status status = fulfilled ? GaoSalesOrder.Status.FULFILLED : GaoSalesOrder.Status.ACTIVE; + if (order.getStatus() != status) { + mapper.updateStatus(order.getId(), status); + order.setStatus(status); + } + } + + @Override + public List listItemByOrderId(String orderId) { + TimiException.required(orderId, "not found salesOrderId"); + return itemMapper.selectBySalesOrderId(orderId); + } + + private List resolveDirectItems(String storeId, List sourceList) { + TimiException.requiredTrue(sourceList != null && !sourceList.isEmpty(), "直接产品订单至少需要一个产品"); + Set productUnitKeySet = new HashSet<>(); + List result = new ArrayList<>(); + boolean canOverridePrice = permissionChecker.hasAny(ModuleCode.GAO, GaoPermissionCode.SALES_ORDER_PRICE_OVERRIDE.getValue()); + for (int i = 0; i < sourceList.size(); i++) { + GaoSalesOrderItemRequest source = sourceList.get(i); + TimiException.required(source, "订单产品明细不能为空"); + TimiException.required(source.getProductId(), "产品不能为空"); + TimiException.required(source.getProductUnitId(), "产品单位不能为空"); + TimiException.requiredTrue(productUnitKeySet.add(source.getProductId() + "|" + source.getProductUnitId()), "同一订单不能重复产品和单位"); + GaoProductUnitSelection selection = productService.getSellableProductUnit(storeId, source.getProductId(), source.getProductUnitId()); + GaoProduct product = selection.getProduct(); + GaoProductUnit unit = selection.getUnit(); + BigDecimal quantity = validateQuantity(product, source.getQuantity()); + unitService.convertToBase(unit, quantity); + Long unitPrice = TimiJava.defaultIfNull(source.getUnitPrice(), product.getSalePrice()); + TimiException.required(unitPrice, "产品单价不能为空"); + TimiException.requiredTrue(0 <= unitPrice, "产品单价不能为负数"); + if (!canOverridePrice) { + TimiException.required(product.getSalePrice(), "普通员工只能使用建议售价"); + TimiException.requiredTrue(product.getSalePrice().equals(unitPrice), "普通员工只能使用建议售价"); + } + long lineAmount = toCentAmount(quantity, unitPrice); + GaoSalesOrderItem item = new GaoSalesOrderItem(); + item.setProductId(product.getId()); + item.setProductUnitId(unit.getId()); + item.setProductNameSnapshot(product.getName()); + item.setUnitSnapshot(unit.getName()); + item.setConversionToBaseSnapshot(unit.getConversionToBase()); + item.setQuantity(quantity); + item.setReservedQuantity(ZERO); + item.setFulfilledQuantity(ZERO); + item.setUnitPrice(unitPrice); + item.setLineAmount(lineAmount); + item.setSort(TimiJava.defaultIfNull(source.getSort(), i)); + item.setRemark(source.getRemark()); + result.add(item); + } + result.sort(Comparator.comparing(GaoSalesOrderItem::getSort)); + return result; + } + + private void verifyExistingDirectItems(GaoSalesOrder existing, List requestList) { + List existingList = itemMapper.selectBySalesOrderId(existing.getId()); + TimiException.requiredTrue(requestList != null && existingList.size() == requestList.size(), "requestNo 对应订单内容不一致"); + Set productUnitKeySet = new HashSet<>(); + for (GaoSalesOrderItemRequest request : requestList) { + TimiException.required(request, "订单产品明细不能为空"); + TimiException.required(request.getProductId(), "产品不能为空"); + TimiException.required(request.getProductUnitId(), "产品单位不能为空"); + TimiException.required(request.getQuantity(), "购买数量不能为空"); + TimiException.requiredTrue(productUnitKeySet.add(request.getProductId() + "|" + request.getProductUnitId()), "同一订单不能重复产品和单位"); + GaoSalesOrderItem item = existingList.stream() + .filter(value -> value.getProductId().equals(request.getProductId()) && value.getProductUnitId().equals(request.getProductUnitId())) + .findFirst().orElse(null); + TimiException.required(item, "requestNo 对应订单内容不一致"); + TimiException.requiredTrue(item.getProductUnitId().equals(request.getProductUnitId()) + && item.getQuantity().compareTo(request.getQuantity()) == 0 + && (request.getUnitPrice() == null || item.getUnitPrice().equals(request.getUnitPrice())), "requestNo 对应订单内容不一致"); + } + } + + private GaoSalesOrder newOrder(String storeId, String customerId, GaoSalesOrder.OrderType orderType, String requestNo, Long purchasedAt, String operatorUserId, String remark) { + GaoSalesOrder order = new GaoSalesOrder(); + order.setStoreId(storeId); + order.setCustomerId(customerId); + order.setOrderNo("SO-%d-%s".formatted(purchasedAt, UUID.randomUUID().toString().replace("-", "").substring(0, 8))); + order.setOrderType(orderType); + order.setStatus(GaoSalesOrder.Status.ACTIVE); + order.setRefundStatus(GaoSalesOrder.RefundStatus.NOT_REQUIRED); + order.setSaleAmount(0L); + order.setPendingPaidAmount(0L); + order.setPaidAmount(0L); + order.setRefundedAmount(0L); + order.setRequestNo(requestNo); + order.setPurchasedAt(purchasedAt); + order.setOperatorUserId(operatorUserId == null ? userLoginService.getRequireLoginUserId() : operatorUserId); + order.setRemark(remark); + return order; + } + + private void fillItemList(List orderList) { + for (GaoSalesOrder order : TimiJava.safeIterable(orderList)) { + order.setItemList(itemMapper.selectBySalesOrderId(order.getId())); + order.setUnpaidAmount(safe(order.getSaleAmount()) - safe(order.getPaidAmount())); + } + } + + private BigDecimal validateQuantity(GaoProduct product, BigDecimal quantity) { + TimiException.required(quantity, "购买数量不能为空"); + TimiException.requiredTrue(0 < quantity.signum(), "购买数量必须大于 0"); + TimiException.requiredTrue(3 >= quantity.stripTrailingZeros().scale(), "购买数量最多保留三位小数"); + if (!Boolean.TRUE.equals(product.getAllowDecimalQuantity())) { + TimiException.requiredTrue(quantity.stripTrailingZeros().scale() <= 0, "该产品不允许小数数量"); + } + return quantity; + } + + private long toCentAmount(BigDecimal quantity, long unitPrice) { + try { + return BigDecimal.valueOf(unitPrice).multiply(quantity).setScale(0, RoundingMode.UNNECESSARY).longValueExact(); + } catch (ArithmeticException e) { + throw new TimiException(TimiCode.ERROR, "数量乘单价后不能精确保存为分", e); + } + } + + private String requiredText(String value, String message) { + TimiException.required(value, message); + String result = value.trim(); + TimiException.requiredTrue(!result.isEmpty(), message); + return result; + } + + private long safe(Long value) { + return value == null ? 0L : value; + } + + private long safeLong(Long value) { + return safe(value); + } + + private BigDecimal decimal(BigDecimal value) { + return value == null ? ZERO : value; + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStockRecordServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStockRecordServiceImplement.java new file mode 100644 index 0000000..769740f --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStockRecordServiceImplement.java @@ -0,0 +1,118 @@ +package com.imyeyu.api.modules.gao.service.implement; + +import com.imyeyu.api.modules.common.entity.Attachment; +import com.imyeyu.api.modules.common.service.AttachmentService; +import com.imyeyu.api.modules.gao.entity.GaoStockRecord; +import com.imyeyu.api.modules.gao.mapper.GaoStockRecordMapper; +import com.imyeyu.api.modules.gao.service.GaoStockRecordService; +import com.imyeyu.api.modules.gao.vo.GaoStockRecordPage; +import com.imyeyu.api.modules.user.entity.User; +import com.imyeyu.api.modules.user.service.UserService; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.spring.bean.PageResult; +import com.imyeyu.spring.mapper.BaseMapper; +import com.imyeyu.spring.service.AbstractEntityService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; + +/// GAO 库存流水服务实现 +/// +/// @author Codex +/// @since 2026-09-02 +@Service +@RequiredArgsConstructor +public class GaoStockRecordServiceImplement extends AbstractEntityService implements GaoStockRecordService { + + private final GaoStockRecordMapper mapper; + private final AttachmentService attachmentService; + private final UserService userService; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + @Override + public PageResult pageByQuery(GaoStockRecordPage page) { + TimiException.required(page, "not found stock record page"); + PageResult result = new PageResult<>(); + result.setTotal(mapper.countByQuery(page)); + result.setList(mapper.selectByQuery(page)); + fillTransient(result.getList()); + return result; + } + + @Override + public List listByBiz(GaoStockRecord.BizType bizType, String bizId) { + TimiException.required(bizType, "not found stock bizType"); + TimiException.required(bizId, "not found stock bizId"); + List result = mapper.selectByBiz(bizType, bizId); + fillTransient(result); + return result; + } + + @Override + public List listByRequestNo(String storeId, String requestNo) { + TimiException.required(storeId, "not found storeId"); + TimiException.required(requestNo, "not found requestNo"); + return mapper.selectByRequestNo(storeId, requestNo); + } + + @Override + public GaoStockRecord getByBizProduct(GaoStockRecord.BizType bizType, String bizId, String productId, + GaoStockRecord.OperationType operationType) { + TimiException.required(bizType, "not found stock bizType"); + TimiException.required(bizId, "not found stock bizId"); + TimiException.required(productId, "not found productId"); + TimiException.required(operationType, "not found operationType"); + return mapper.selectByBizProduct(bizType, bizId, productId, operationType); + } + + @Override + public void createOperationRecord(GaoStockRecord record, List attachmentList) { + TimiException.required(record, "not found stock record"); + mapper.insert(record); + if (attachmentList != null && !attachmentList.isEmpty()) { + attachmentService.updateByBizId(Attachment.BizType.GAO_STOCK_RECORD, record.getId(), attachmentList); + } + } + + @Override + public void create(GaoStockRecord record) { + throw new TimiException(TimiCode.ERROR, "库存流水必须通过库存操作创建"); + } + + @Override + public void update(GaoStockRecord record) { + throw new TimiException(TimiCode.ERROR, "库存流水不允许修改"); + } + + @Override + public void delete(String id) { + throw new TimiException(TimiCode.ERROR, "库存流水不允许删除"); + } + + @Override + public void destroy(String id) { + throw new TimiException(TimiCode.ERROR, "库存流水不允许物理删除"); + } + + private void fillTransient(List recordList) { + if (recordList == null || recordList.isEmpty()) { + return; + } + List idList = recordList.stream().map(GaoStockRecord::getId).toList(); + Map> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_STOCK_RECORD, idList); + List userIdList = recordList.stream().map(GaoStockRecord::getOperatorUserId) + .filter(value -> value != null && !value.isBlank()).distinct().toList(); + Map userMap = userIdList.isEmpty() ? Map.of() : userService.mapByIdList(userIdList); + for (GaoStockRecord record : recordList) { + record.setAttachmentList(attachmentMap.getOrDefault(record.getId(), List.of())); + record.setOperator(userMap.get(record.getOperatorUserId())); + } + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreBusinessDayServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreBusinessDayServiceImplement.java index 4f289af..112923e 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreBusinessDayServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreBusinessDayServiceImplement.java @@ -2,14 +2,15 @@ package com.imyeyu.api.modules.gao.service.implement; import com.imyeyu.api.config.dbsource.TimiServerDBConfig; import com.imyeyu.api.modules.gao.entity.GaoStoreBusinessDay; -import com.imyeyu.api.modules.gao.mapper.GaoEventRecordMapper; import com.imyeyu.api.modules.gao.mapper.GaoStoreBusinessDayMapper; +import com.imyeyu.api.modules.gao.service.GaoEventRecordService; import com.imyeyu.api.modules.gao.service.GaoStoreBusinessDayService; import com.imyeyu.java.TimiJava; import com.imyeyu.java.bean.timi.TimiException; import com.imyeyu.spring.mapper.BaseMapper; import com.imyeyu.spring.service.AbstractEntityService; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -25,7 +26,7 @@ import java.util.List; public class GaoStoreBusinessDayServiceImplement extends AbstractEntityService implements GaoStoreBusinessDayService { private final GaoStoreBusinessDayMapper mapper; - private final GaoEventRecordMapper eventRecordMapper; + private final ObjectProvider eventRecordServiceProvider; @Override protected BaseMapper mapper() { @@ -112,7 +113,7 @@ public class GaoStoreBusinessDayServiceImplement extends AbstractEntityService customerDailyTrend(GaoStoreChartReq req) { @@ -46,19 +46,19 @@ public class GaoStoreChartServiceImplement implements GaoStoreChartService { @Override public List eventRecordDailyStat(GaoStoreChartReq req) { checkRangeReq(req); - return eventRecordMapper.stateDailyByRange(req); + return eventRecordService.stateDailyByRange(req); } @Override public List eventRecordNumberValueDailyStat(GaoStoreChartReq req) { checkRangeReq(req); - return eventRecordMapper.stateNumberValueDailyByRange(req); + return eventRecordService.stateNumberValueDailyByRange(req); } @Override public List eventRecordCalendar(GaoStoreChartReq req) { checkRangeReq(req); - return eventRecordMapper.selectCalendarByRange(req); + return eventRecordService.calendarByStoreRange(req); } private void checkRangeReq(GaoStoreChartReq req) { diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreDiaryServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreDiaryServiceImplement.java index 81c93c0..8827db4 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreDiaryServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreDiaryServiceImplement.java @@ -3,7 +3,6 @@ package com.imyeyu.api.modules.gao.service.implement; import com.imyeyu.api.config.dbsource.TimiServerDBConfig; import com.imyeyu.api.modules.common.entity.Article; import com.imyeyu.api.modules.common.entity.Attachment; -import com.imyeyu.api.modules.common.mapper.ArticleMapper; import com.imyeyu.api.modules.common.service.ArticleService; import com.imyeyu.api.modules.common.service.AttachmentService; import com.imyeyu.api.modules.gao.mapper.GaoStoreDiaryMapper; @@ -26,7 +25,6 @@ public class GaoStoreDiaryServiceImplement implements GaoStoreDiaryService { private final ArticleService articleService; private final AttachmentService attachmentService; - private final ArticleMapper articleMapper; private final GaoStoreDiaryMapper diaryMapper; @Override @@ -83,7 +81,7 @@ public class GaoStoreDiaryServiceImplement implements GaoStoreDiaryService { Article example = new Article(); example.setBizType(Article.BizType.GAO_STORE_DIARY); example.setBizId(storeId); - for (Article article : articleMapper.selectAllByExample(example)) { + for (Article article : articleService.listByExample(example)) { attachmentService.deleteByBizId(Attachment.BizType.ARTICLE, article.getId()); articleService.delete(article.getId()); } diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreServiceImplement.java index 4a1fdad..48cdabc 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStoreServiceImplement.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.imyeyu.api.config.dbsource.TimiServerDBConfig; import com.imyeyu.api.modules.common.entity.Tag; import com.imyeyu.api.modules.common.service.TagService; +import com.imyeyu.api.modules.gao.entity.GaoEvent; import com.imyeyu.api.modules.gao.entity.GaoStore; import com.imyeyu.api.modules.gao.mapper.GaoStoreMapper; import com.imyeyu.api.modules.gao.service.GaoCustomerService; @@ -79,6 +80,7 @@ public class GaoStoreServiceImplement extends AbstractEntityService listDirectProductSaleEvents(String storeId) { + TimiException.required(storeId, "not found storeId"); + return eventService.listValid(storeId).stream() + .filter(this::isValidDirectProductSaleEvent) + .toList(); + } + + private boolean isValidDirectProductSaleEvent(GaoEvent event) { + if (event == null + || event.getStatus() != GaoEvent.Status.ACTIVE + || event.getBizType() != GaoEvent.BizType.GENERAL + || event.getValueType() != GaoEvent.ValueType.NUMBER + || event.getValueUnit() != GaoEvent.ValueUnit.CENT + || Boolean.TRUE.equals(event.getRequireVerification())) { + return false; + } + for (var group : TimiJava.safeIterable(event.getOptionGroupList())) { + if (Boolean.TRUE.equals(group.getRequired())) { + return false; + } + } + return true; + } + + private void validateDirectProductSaleEventIfPresent(GaoStore store) { + if (store.getDirectProductSaleEventId() != null && !store.getDirectProductSaleEventId().isBlank()) { + validateDirectProductSaleEvent(store); + } + } + @Override public String getBelongIdByRequiredLoginUserId() { return getByUserId(userLoginService.getRequireLoginUserId()).getId(); diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerPackagePage.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerPackagePage.java index eab3cb6..000841c 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerPackagePage.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerPackagePage.java @@ -25,15 +25,4 @@ public class GaoCustomerPackagePage extends BasePage { /// 套餐状态 private GaoCustomerPackage.Status status; - /// 付款状态 - private GaoCustomerPackage.PaymentStatus paymentStatus; - - /// 退款状态 - private GaoCustomerPackage.RefundStatus refundStatus; - - /// 购买开始时间 - private Long beginAt; - - /// 购买结束时间 - private Long endAt; } diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerPackagePurchaseItem.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerPackagePurchaseItem.java index bd877a5..c1bbbf0 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerPackagePurchaseItem.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerPackagePurchaseItem.java @@ -11,11 +11,20 @@ import java.math.BigDecimal; @Data public class GaoCustomerPackagePurchaseItem { - /// 产品名称 - private String productName; + /// 产品 ID + private String productId; - /// 产品单位 - private String unit; + /// 产品单位 ID + private String productUnitId; + + /// 产品名称快照,由后端生成 + private String productNameSnapshot; + + /// 单位快照,由后端生成 + private String unitSnapshot; + + /// 折算率快照,由后端生成 + private BigDecimal conversionToBaseSnapshot; /// 总权益数量 private BigDecimal totalQuantity; diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoDirectProductOrderCreateRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoDirectProductOrderCreateRequest.java new file mode 100644 index 0000000..2fd8703 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoDirectProductOrderCreateRequest.java @@ -0,0 +1,35 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.common.entity.Attachment; +import lombok.Data; + +import java.util.List; + +/// GAO 直接产品订单创建请求 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +public class GaoDirectProductOrderCreateRequest { + + /// 创建订单幂等号 + private String requestNo; + + /// 门店 ID + private String storeId; + + /// 客户 ID + private String customerId; + + /// 下单时间 + private Long purchasedAt; + + /// 订单明细 + private List itemList; + + /// 购买照片 + private List attachmentList; + + /// 备注 + private String remark; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundRecordPage.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundRecordPage.java index ec0d9f2..2e3e568 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundRecordPage.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundRecordPage.java @@ -19,6 +19,12 @@ public class GaoFundRecordPage extends BasePage { /// 客户套餐 ID private String customerPackageId; + /// 销售订单 ID + private String salesOrderId; + + /// 资金业务类型 + private GaoFundRecord.BizType bizType; + /// 客户 ID private String customerId; diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentActionRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentActionRequest.java new file mode 100644 index 0000000..0774d8e --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentActionRequest.java @@ -0,0 +1,17 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +/// GAO 统一履约状态操作请求 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +public class GaoOrderFulfillmentActionRequest { + + /// 履约单 ID + private String fulfillmentId; + + /// 拒绝或撤销原因 + private String reason; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentCreateRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentCreateRequest.java new file mode 100644 index 0000000..1086a57 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentCreateRequest.java @@ -0,0 +1,32 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.common.entity.Attachment; +import lombok.Data; + +import java.util.List; + +/// GAO 统一履约创建请求 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +public class GaoOrderFulfillmentCreateRequest { + + /// 销售订单 ID + private String salesOrderId; + + /// 客户端幂等请求号 + private String requestNo; + + /// 实际履约时间 + private Long deliveredAt; + + /// 履约明细 + private List itemList; + + /// 履约照片 + private List attachmentList; + + /// 备注 + private String remark; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentItemRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentItemRequest.java new file mode 100644 index 0000000..47e4f3a --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentItemRequest.java @@ -0,0 +1,22 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +/// GAO 统一履约明细请求 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +public class GaoOrderFulfillmentItemRequest { + + /// 直接订单明细 ID,与客户套餐权益 ID 二选一 + private String sourceOrderItemId; + + /// 客户套餐权益项 ID,与直接订单明细 ID 二选一 + private String sourceCustomerPackageItemId; + + /// 本次履约数量 + private BigDecimal quantity; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentPage.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentPage.java new file mode 100644 index 0000000..19b8c37 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentPage.java @@ -0,0 +1,36 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.gao.entity.GaoOrderFulfillment; +import com.imyeyu.java.bean.BasePage; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/// GAO 统一履约分页查询请求 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoOrderFulfillmentPage extends BasePage { + + /// 门店 ID + private String storeId; + + /// 销售订单 ID + private String salesOrderId; + + /// 客户 ID + private String customerId; + + /// 履约类型 + private GaoOrderFulfillment.FulfillmentType fulfillmentType; + + /// 履约状态 + private GaoOrderFulfillment.Status status; + + /// 起始时间 + private Long beginAt; + + /// 结束时间 + private Long endAt; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentPrepareStockRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentPrepareStockRequest.java new file mode 100644 index 0000000..a0eed07 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentPrepareStockRequest.java @@ -0,0 +1,26 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.common.entity.Attachment; +import lombok.Data; + +import java.util.List; + +/// GAO 履约占库准备请求 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +public class GaoOrderFulfillmentPrepareStockRequest { + + /// 履约单 ID + private String fulfillmentId; + + /// 占库准备原因 + private String reason; + + /// 备注 + private String remark; + + /// 补充照片 + private List attachmentList; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryCreateRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentRevokeRequest.java similarity index 50% rename from src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryCreateRequest.java rename to src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentRevokeRequest.java index 4eede73..e0e8a81 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryCreateRequest.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentRevokeRequest.java @@ -1,31 +1,33 @@ package com.imyeyu.api.modules.gao.vo; import com.imyeyu.api.modules.common.entity.Attachment; -import com.imyeyu.api.modules.gao.entity.GaoPackageDeliveryItem; import lombok.Data; import java.util.List; -/// GAO 套餐发放请求 +/// GAO 已核销履约整单冲正请求 /// /// @author Codex -/// @since 2026-08-31 +/// @since 2026-09-02 @Data -public class GaoPackageDeliveryCreateRequest { +public class GaoOrderFulfillmentRevokeRequest { + + /// 原履约单 ID + private String fulfillmentId; /// 客户端幂等请求号 private String requestNo; - /// 客户套餐 ID - private String customerPackageId; - - /// 实际发放时间 + /// 冲正发生时间 private Long deliveredAt; - /// 发放产品明细 - private List itemList; + /// 冲正原因 + private String reason; - /// 发放照片 + /// true 为实物已退回门店 + private Boolean returnedToStore; + + /// 退回或冲正照片 private List attachmentList; /// 备注 diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundActionRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderPaymentActionRequest.java similarity index 50% rename from src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundActionRequest.java rename to src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderPaymentActionRequest.java index 4ca1626..1436c7a 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundActionRequest.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderPaymentActionRequest.java @@ -2,16 +2,16 @@ package com.imyeyu.api.modules.gao.vo; import lombok.Data; -/// GAO 资金收款状态操作请求 +/// GAO 销售订单收款操作请求 /// /// @author Codex -/// @since 2026-08-31 +/// @since 2026-09-02 @Data -public class GaoFundActionRequest { +public class GaoOrderPaymentActionRequest { - /// 资金记录 ID + /// 资金流水 ID private String fundRecordId; - /// 拒绝或撤销原因 + /// 原因 private String reason; } diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundConfirmNoRefundRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderPaymentNoRefundRequest.java similarity index 52% rename from src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundConfirmNoRefundRequest.java rename to src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderPaymentNoRefundRequest.java index 786ce1b..358baa7 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundConfirmNoRefundRequest.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderPaymentNoRefundRequest.java @@ -2,25 +2,25 @@ package com.imyeyu.api.modules.gao.vo; import lombok.Data; -/// GAO 确认无需退款请求 +/// GAO 销售订单确认无需退款请求 /// /// @author Codex -/// @since 2026-08-31 +/// @since 2026-09-02 @Data -public class GaoFundConfirmNoRefundRequest { +public class GaoOrderPaymentNoRefundRequest { - /// 客户端幂等请求号 + /// 订单 ID + private String salesOrderId; + + /// 幂等请求号 private String requestNo; - /// 客户套餐 ID - private String customerPackageId; - - /// 确认时间 - private Long occurredAt; - /// 确认原因 private String reason; + /// 发生时间 + private Long occurredAt; + /// 备注 private String remark; } diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundReceiveRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderPaymentReceiveRequest.java similarity index 65% rename from src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundReceiveRequest.java rename to src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderPaymentReceiveRequest.java index aedb2e8..365bd91 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundReceiveRequest.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderPaymentReceiveRequest.java @@ -5,23 +5,23 @@ import lombok.Data; import java.util.List; -/// GAO 线下收款登记请求 +/// GAO 销售订单收款请求 /// /// @author Codex -/// @since 2026-08-31 +/// @since 2026-09-02 @Data -public class GaoFundReceiveRequest { +public class GaoOrderPaymentReceiveRequest { - /// 客户端幂等请求号 + /// 订单 ID + private String salesOrderId; + + /// 幂等请求号 private String requestNo; - /// 客户套餐 ID - private String customerPackageId; - /// 收款金额,单位分 private Long amount; - /// 收款发生时间 + /// 实际收款时间 private Long occurredAt; /// 收款照片 diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundRefundRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderPaymentRefundRequest.java similarity index 72% rename from src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundRefundRequest.java rename to src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderPaymentRefundRequest.java index 81dbb35..9a29d1c 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoFundRefundRequest.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderPaymentRefundRequest.java @@ -5,19 +5,19 @@ import lombok.Data; import java.util.List; -/// GAO 退款登记请求 +/// GAO 销售订单退款请求 /// /// @author Codex -/// @since 2026-08-31 +/// @since 2026-09-02 @Data -public class GaoFundRefundRequest { +public class GaoOrderPaymentRefundRequest { - /// 客户端幂等请求号 + /// 订单 ID + private String salesOrderId; + + /// 幂等请求号 private String requestNo; - /// 客户套餐 ID - private String customerPackageId; - /// 退款金额,单位分 private Long amount; diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryRevokeRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderPaymentRevokeRequest.java similarity index 62% rename from src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryRevokeRequest.java rename to src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderPaymentRevokeRequest.java index ec9e207..bc44bba 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryRevokeRequest.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderPaymentRevokeRequest.java @@ -5,25 +5,25 @@ import lombok.Data; import java.util.List; -/// GAO 已核销发放整单冲正请求 +/// GAO 销售订单资金冲正请求 /// /// @author Codex -/// @since 2026-08-31 +/// @since 2026-09-02 @Data -public class GaoPackageDeliveryRevokeRequest { +public class GaoOrderPaymentRevokeRequest { - /// 客户端幂等请求号 + /// 原资金流水 ID + private String fundRecordId; + + /// 幂等请求号 private String requestNo; - /// 原发放单 ID - private String deliveryId; - - /// 冲正发生时间 - private Long deliveredAt; - /// 冲正原因 private String reason; + /// 冲正发生时间 + private Long occurredAt; + /// 冲正照片 private List attachmentList; diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryActionRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryActionRequest.java deleted file mode 100644 index dfd2e7c..0000000 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryActionRequest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.imyeyu.api.modules.gao.vo; - -import lombok.Data; - -/// GAO 套餐发放状态操作请求 -/// -/// @author Codex -/// @since 2026-08-31 -@Data -public class GaoPackageDeliveryActionRequest { - - /// 发放单 ID - private String deliveryId; - - /// 拒绝或撤销原因 - private String reason; -} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryPage.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryPage.java deleted file mode 100644 index 73c788e..0000000 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryPage.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.imyeyu.api.modules.gao.vo; - -import com.imyeyu.api.modules.gao.entity.GaoPackageDelivery; -import com.imyeyu.java.bean.BasePage; -import lombok.Data; -import lombok.EqualsAndHashCode; - -/// GAO 套餐发放分页查询请求 -/// -/// @author Codex -/// @since 2026-08-31 -@Data -@EqualsAndHashCode(callSuper = true) -public class GaoPackageDeliveryPage extends BasePage { - - /// 门店 ID - private String storeId; - - /// 客户套餐 ID - private String customerPackageId; - - /// 客户 ID - private String customerId; - - /// 发放状态 - private GaoPackageDelivery.Status status; - - /// 操作类型 - private GaoPackageDelivery.OperationType operationType; - - /// 发放开始时间 - private Long beginAt; - - /// 发放结束时间 - private Long endAt; -} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageOrderCreateData.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageOrderCreateData.java new file mode 100644 index 0000000..0433636 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageOrderCreateData.java @@ -0,0 +1,44 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +/// GAO 套餐订单内部创建数据 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +public class GaoPackageOrderCreateData { + + /// 门店 ID + private String storeId; + + /// 客户 ID + private String customerId; + + /// 套餐配置 ID + private String packageConfigId; + + /// 套餐名称快照 + private String packageNameSnapshot; + + /// 套餐标准价快照 + private Long standardAmountSnapshot; + + /// 成交价 + private Long saleAmount; + + /// 来源购买事件记录 ID + private String sourceEventRecordId; + + /// 幂等号 + private String requestNo; + + /// 下单时间 + private Long purchasedAt; + + /// 操作人 + private String operatorUserId; + + /// 备注 + private String remark; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductCategoryPage.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductCategoryPage.java new file mode 100644 index 0000000..5dd63da --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductCategoryPage.java @@ -0,0 +1,24 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.gao.entity.GaoProductCategory; +import com.imyeyu.java.bean.BasePage; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/// GAO 产品分类分页请求 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoProductCategoryPage extends BasePage { + + /// 门店 ID + private String storeId; + + /// 分类状态 + private GaoProductCategory.Status status; + + /// 名称关键词 + private String keyword; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductPage.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductPage.java new file mode 100644 index 0000000..e958c44 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductPage.java @@ -0,0 +1,35 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.gao.entity.GaoProduct; +import com.imyeyu.java.bean.BasePage; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/// GAO 产品分页请求 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoProductPage extends BasePage { + + /// 门店 ID + private String storeId; + + /// 分类 ID + private String categoryId; + + /// 分类 ID 列表 + private List categoryIdList; + + /// 产品状态 + private GaoProduct.Status status; + + /// 产品类型 + private GaoProduct.ProductType productType; + + /// 名称或编码关键词 + private String keyword; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockChangeRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockChangeRequest.java new file mode 100644 index 0000000..fdd85ac --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockChangeRequest.java @@ -0,0 +1,39 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.common.entity.Attachment; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +/// GAO 人工库存变更请求 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +public class GaoProductStockChangeRequest { + + /// 客户端幂等请求号 + private String requestNo; + + /// 门店 ID + private String storeId; + + /// 产品 ID + private String productId; + + /// 产品单位 ID + private String productUnitId; + + /// 业务数量 + private BigDecimal quantity; + + /// 原因 + private String reason; + + /// 备注 + private String remark; + + /// 操作照片 + private List attachmentList; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockPage.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockPage.java new file mode 100644 index 0000000..e9260cd --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockPage.java @@ -0,0 +1,23 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.java.bean.BasePage; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/// GAO 产品库存分页请求 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoProductStockPage extends BasePage { + + /// 门店 ID + private String storeId; + + /// 产品 ID + private String productId; + + /// 产品名称或编码关键词 + private String keyword; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductUnitSelection.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductUnitSelection.java new file mode 100644 index 0000000..d465143 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductUnitSelection.java @@ -0,0 +1,21 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.gao.entity.GaoProduct; +import com.imyeyu.api.modules.gao.entity.GaoProductUnit; +import lombok.AllArgsConstructor; +import lombok.Data; + +/// 已校验的产品单位选项 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +@AllArgsConstructor +public class GaoProductUnitSelection { + + /// 产品 + private GaoProduct product; + + /// 单位 + private GaoProductUnit unit; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderCancelRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderCancelRequest.java new file mode 100644 index 0000000..73110d6 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderCancelRequest.java @@ -0,0 +1,20 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +/// GAO 销售订单取消请求 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +public class GaoSalesOrderCancelRequest { + + /// 订单 ID + private String salesOrderId; + + /// 取消原因 + private String reason; + + /// 幂等请求号 + private String requestNo; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderItemRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderItemRequest.java new file mode 100644 index 0000000..c88e65c --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderItemRequest.java @@ -0,0 +1,31 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +/// GAO 直接产品订单明细请求 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +public class GaoSalesOrderItemRequest { + + /// 产品 ID + private String productId; + + /// 产品单位 ID + private String productUnitId; + + /// 购买数量 + private BigDecimal quantity; + + /// 实际单价,单位分 + private Long unitPrice; + + /// 排序值 + private Integer sort; + + /// 备注 + private String remark; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderPage.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderPage.java new file mode 100644 index 0000000..32a73cd --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderPage.java @@ -0,0 +1,42 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.gao.entity.GaoSalesOrder; +import com.imyeyu.java.bean.BasePage; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/// GAO 销售订单分页请求 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoSalesOrderPage extends BasePage { + + /// 门店 ID + private String storeId; + + /// 客户 ID + private String customerId; + + /// 订单类型 + private GaoSalesOrder.OrderType orderType; + + /// 订单状态 + private GaoSalesOrder.Status status; + + /// 付款状态 + private GaoSalesOrder.PaymentStatus paymentStatus; + + /// true 为筛选已履约但未付清订单 + private Boolean fulfilledButUnpaid; + + /// 关键词 + private String keyword; + + /// 起始下单时间 + private Long beginAt; + + /// 结束下单时间 + private Long endAt; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoStockChangeItem.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoStockChangeItem.java new file mode 100644 index 0000000..7254907 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoStockChangeItem.java @@ -0,0 +1,58 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.gao.entity.GaoProduct; +import com.imyeyu.api.modules.gao.entity.GaoProductUnit; +import lombok.Data; + +import java.math.BigDecimal; + +/// 已校验的库存变更项 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +public class GaoStockChangeItem { + + /// 产品 ID + private String productId; + + /// 产品单位 ID + private String productUnitId; + + /// 产品名称快照 + private String productNameSnapshot; + + /// 单位名称快照 + private String unitSnapshot; + + /// 折算率快照 + private BigDecimal conversionToBaseSnapshot; + + /// 业务数量 + private BigDecimal quantity; + + /// 基础单位数量 + private BigDecimal baseQuantity; + + /// 冲正来源流水 ID + private String sourceRecordId; + + /// 生成库存项 + /// + /// @param product 产品 + /// @param unit 单位 + /// @param quantity 业务数量 + /// @param baseQuantity 基础单位数量 + /// @return 库存变更项 + public static GaoStockChangeItem of(GaoProduct product, GaoProductUnit unit, BigDecimal quantity, BigDecimal baseQuantity) { + GaoStockChangeItem result = new GaoStockChangeItem(); + result.productId = product.getId(); + result.productUnitId = unit.getId(); + result.productNameSnapshot = product.getName(); + result.unitSnapshot = unit.getName(); + result.conversionToBaseSnapshot = unit.getConversionToBase(); + result.quantity = quantity; + result.baseQuantity = baseQuantity; + return result; + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoStockRecordPage.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoStockRecordPage.java new file mode 100644 index 0000000..5efdb2e --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoStockRecordPage.java @@ -0,0 +1,36 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.gao.entity.GaoStockRecord; +import com.imyeyu.java.bean.BasePage; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/// GAO 库存流水分页请求 +/// +/// @author Codex +/// @since 2026-09-02 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoStockRecordPage extends BasePage { + + /// 门店 ID + private String storeId; + + /// 产品 ID + private String productId; + + /// 来源业务类型 + private GaoStockRecord.BizType bizType; + + /// 来源业务 ID + private String bizId; + + /// 操作类型 + private GaoStockRecord.OperationType operationType; + + /// 起始发生时间 + private Long beginAt; + + /// 结束发生时间 + private Long endAt; +} diff --git a/src/main/resources/db/migration/timiserver/V53__create_gao_product_tables.sql b/src/main/resources/db/migration/timiserver/V53__create_gao_product_tables.sql new file mode 100644 index 0000000..901fe91 --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V53__create_gao_product_tables.sql @@ -0,0 +1,53 @@ +CREATE TABLE IF NOT EXISTS `gao_product_category` ( + `id` VARCHAR(36) NOT NULL, + `store_id` VARCHAR(36) NOT NULL COMMENT '所属门店 ID', + `name` VARCHAR(64) NOT NULL COMMENT '分类名称', + `sort` INT NOT NULL DEFAULT 0 COMMENT '排序值', + `status` VARCHAR(32) NOT NULL COMMENT 'ACTIVE 启用,INACTIVE 停用', + `created_at` BIGINT NOT NULL, + `updated_at` BIGINT NULL, + `deleted_at` BIGINT NULL, + PRIMARY KEY (`id`), + KEY `idx_gao_product_category_store_status` (`store_id`, `status`, `deleted_at`, `sort`, `name`) +) COMMENT='GAO 产品分类'; + +CREATE TABLE IF NOT EXISTS `gao_product` ( + `id` VARCHAR(36) NOT NULL, + `store_id` VARCHAR(36) NOT NULL COMMENT '所属门店 ID', + `category_id` VARCHAR(36) NULL COMMENT '产品分类 ID', + `code` VARCHAR(64) NOT NULL COMMENT '门店内产品编码', + `name` VARCHAR(128) NOT NULL COMMENT '产品名称', + `product_type` VARCHAR(32) NOT NULL COMMENT 'SERVICE 服务,GOODS 实物,CONSUMABLE 消耗品', + `base_unit` VARCHAR(32) NOT NULL COMMENT '基础库存单位', + `stock_enabled` TINYINT(1) NOT NULL COMMENT 'true 为启用库存管理', + `allow_decimal_quantity` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'true 为允许业务数量保留小数', + `status` VARCHAR(32) NOT NULL COMMENT 'ACTIVE 启用,INACTIVE 停用', + `sale_price` BIGINT NULL COMMENT '建议售价,单位分', + `reference_cost` BIGINT NULL COMMENT '参考成本,单位分', + `min_stock_quantity` DECIMAL(18,3) NULL COMMENT '最低库存,基础单位', + `sort` INT NOT NULL DEFAULT 0 COMMENT '排序值', + `remark` VARCHAR(255) NULL COMMENT '备注', + `updated_by` VARCHAR(36) NOT NULL COMMENT '最后修改用户 ID', + `created_at` BIGINT NOT NULL, + `updated_at` BIGINT NULL, + `deleted_at` BIGINT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_gao_product_store_code` (`store_id`, `code`), + KEY `idx_gao_product_store_category` (`store_id`, `category_id`, `status`, `deleted_at`, `sort`, `name`) +) COMMENT='GAO 产品'; + +CREATE TABLE IF NOT EXISTS `gao_product_unit` ( + `id` VARCHAR(36) NOT NULL, + `product_id` VARCHAR(36) NOT NULL COMMENT '产品 ID', + `name` VARCHAR(32) NOT NULL COMMENT '单位名称', + `conversion_to_base` DECIMAL(18,6) NOT NULL COMMENT '折算为基础单位的比例', + `is_base` TINYINT(1) NOT NULL COMMENT 'true 为基础库存单位', + `allow_purchase` TINYINT(1) NOT NULL COMMENT 'true 为允许采购入库', + `allow_sale` TINYINT(1) NOT NULL COMMENT 'true 为允许销售或套餐配置', + `sort` INT NOT NULL DEFAULT 0 COMMENT '排序值', + `created_at` BIGINT NOT NULL, + `updated_at` BIGINT NULL, + `deleted_at` BIGINT NULL, + PRIMARY KEY (`id`), + KEY `idx_gao_product_unit_product` (`product_id`, `deleted_at`, `sort`) +) COMMENT='GAO 产品单位'; diff --git a/src/main/resources/db/migration/timiserver/V54__create_gao_product_stock_tables.sql b/src/main/resources/db/migration/timiserver/V54__create_gao_product_stock_tables.sql new file mode 100644 index 0000000..11644fd --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V54__create_gao_product_stock_tables.sql @@ -0,0 +1,46 @@ +CREATE TABLE IF NOT EXISTS `gao_product_stock` ( + `id` VARCHAR(36) NOT NULL, + `store_id` VARCHAR(36) NOT NULL COMMENT '门店 ID', + `product_id` VARCHAR(36) NOT NULL COMMENT '产品 ID', + `on_hand_quantity` DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT '实际库存,基础单位', + `reserved_quantity` DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT '预占库存,基础单位', + `updated_by` VARCHAR(36) NOT NULL COMMENT '最后操作人', + `created_at` BIGINT NOT NULL, + `updated_at` BIGINT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_gao_product_stock_store_product` (`store_id`, `product_id`), + KEY `idx_gao_product_stock_product` (`product_id`) +) COMMENT='GAO 产品库存余额'; + +CREATE TABLE IF NOT EXISTS `gao_stock_record` ( + `id` VARCHAR(36) NOT NULL, + `store_id` VARCHAR(36) NOT NULL COMMENT '门店 ID', + `product_id` VARCHAR(36) NOT NULL COMMENT '产品 ID', + `product_unit_id` VARCHAR(36) NOT NULL COMMENT '产品单位 ID', + `product_name_snapshot` VARCHAR(128) NOT NULL COMMENT '产品名称快照', + `unit_snapshot` VARCHAR(32) NOT NULL COMMENT '单位快照', + `conversion_to_base_snapshot` DECIMAL(18,6) NOT NULL COMMENT '折算率快照', + `quantity` DECIMAL(18,3) NOT NULL COMMENT '业务数量', + `base_quantity` DECIMAL(18,3) NOT NULL COMMENT '基础单位数量', + `operation_type` VARCHAR(32) NOT NULL COMMENT '库存操作类型', + `on_hand_delta` DECIMAL(18,3) NOT NULL COMMENT '实际库存增量', + `reserved_delta` DECIMAL(18,3) NOT NULL COMMENT '预占库存增量', + `on_hand_before` DECIMAL(18,3) NOT NULL COMMENT '操作前实际库存', + `on_hand_after` DECIMAL(18,3) NOT NULL COMMENT '操作后实际库存', + `reserved_before` DECIMAL(18,3) NOT NULL COMMENT '操作前预占库存', + `reserved_after` DECIMAL(18,3) NOT NULL COMMENT '操作后预占库存', + `biz_type` VARCHAR(32) NOT NULL COMMENT '来源业务类型', + `biz_id` VARCHAR(36) NOT NULL COMMENT '来源业务 ID', + `source_record_id` VARCHAR(36) NULL COMMENT '冲正来源流水 ID', + `request_no` VARCHAR(128) NOT NULL COMMENT '幂等请求号', + `operator_user_id` VARCHAR(36) NOT NULL COMMENT '操作人', + `occurred_at` BIGINT NOT NULL COMMENT '发生时间', + `reason` TEXT NULL COMMENT '原因', + `remark` TEXT NULL COMMENT '备注', + `created_at` BIGINT NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_gao_stock_record_request_product` (`store_id`, `request_no`, `operation_type`, `product_id`), + UNIQUE KEY `uk_gao_stock_record_source` (`source_record_id`), + KEY `idx_gao_stock_record_product_time` (`store_id`, `product_id`, `occurred_at`, `id`), + KEY `idx_gao_stock_record_biz_time` (`store_id`, `biz_type`, `biz_id`, `occurred_at`, `id`) +) COMMENT='GAO 库存变更流水'; diff --git a/src/main/resources/db/migration/timiserver/V55__create_gao_sales_order_tables.sql b/src/main/resources/db/migration/timiserver/V55__create_gao_sales_order_tables.sql new file mode 100644 index 0000000..321fdb1 --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V55__create_gao_sales_order_tables.sql @@ -0,0 +1,61 @@ +ALTER TABLE `gao_store` + ADD COLUMN `direct_product_sale_event_id` VARCHAR(36) NULL COMMENT '直接产品订单使用的消费事件 ID' AFTER `auto_closed_day`, + ADD KEY `idx_gao_store_direct_product_sale_event` (`direct_product_sale_event_id`); + +CREATE TABLE IF NOT EXISTS `gao_sales_order` ( + `id` VARCHAR(36) NOT NULL, + `store_id` VARCHAR(36) NOT NULL COMMENT '门店 ID', + `customer_id` VARCHAR(36) NOT NULL COMMENT '客户 ID', + `order_no` VARCHAR(64) NOT NULL COMMENT '门店内订单号', + `order_type` VARCHAR(32) NOT NULL COMMENT 'PACKAGE 套餐,DIRECT_PRODUCT 直接产品', + `status` VARCHAR(32) NOT NULL COMMENT '订单状态', + `payment_status` VARCHAR(32) NOT NULL COMMENT '付款状态', + `refund_status` VARCHAR(32) NOT NULL COMMENT '退款状态', + `sale_amount` BIGINT NOT NULL COMMENT '成交金额,单位分', + `pending_paid_amount` BIGINT NOT NULL DEFAULT 0 COMMENT '待核销收款,单位分', + `paid_amount` BIGINT NOT NULL DEFAULT 0 COMMENT '有效收款,单位分', + `refunded_amount` BIGINT NOT NULL DEFAULT 0 COMMENT '有效退款,单位分', + `source_event_record_id` VARCHAR(36) NULL COMMENT '来源事件记录 ID', + `request_no` VARCHAR(128) NOT NULL COMMENT '创建订单幂等号', + `purchased_at` BIGINT NOT NULL COMMENT '下单时间', + `operator_user_id` VARCHAR(36) NOT NULL COMMENT '下单人', + `cancelled_at` BIGINT NULL COMMENT '取消时间', + `cancel_operator_user_id` VARCHAR(36) NULL COMMENT '取消人', + `cancel_reason` TEXT NULL COMMENT '取消原因', + `remark` TEXT NULL COMMENT '备注', + `created_at` BIGINT NOT NULL, + `updated_at` BIGINT NULL, + `deleted_at` BIGINT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_gao_sales_order_store_no` (`store_id`, `order_no`), + UNIQUE KEY `uk_gao_sales_order_store_request` (`store_id`, `request_no`), + UNIQUE KEY `uk_gao_sales_order_source_event` (`source_event_record_id`), + KEY `idx_gao_sales_order_customer` (`store_id`, `customer_id`, `status`, `deleted_at`, `purchased_at`), + KEY `idx_gao_sales_order_payment` (`store_id`, `order_type`, `payment_status`, `refund_status`, `deleted_at`, `purchased_at`) +) COMMENT='GAO 销售订单'; + +CREATE TABLE IF NOT EXISTS `gao_sales_order_item` ( + `id` VARCHAR(36) NOT NULL, + `sales_order_id` VARCHAR(36) NOT NULL COMMENT '销售订单 ID', + `product_id` VARCHAR(36) NOT NULL COMMENT '产品 ID', + `product_unit_id` VARCHAR(36) NOT NULL COMMENT '产品单位 ID', + `product_name_snapshot` VARCHAR(128) NOT NULL COMMENT '产品名称快照', + `unit_snapshot` VARCHAR(32) NOT NULL COMMENT '单位快照', + `conversion_to_base_snapshot` DECIMAL(18,6) NOT NULL COMMENT '折算率快照', + `quantity` DECIMAL(18,3) NOT NULL COMMENT '购买数量', + `reserved_quantity` DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT '待履约数量', + `fulfilled_quantity` DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT '已履约数量', + `unit_price` BIGINT NOT NULL COMMENT '实际单价,单位分', + `line_amount` BIGINT NOT NULL COMMENT '行金额,单位分', + `sort` INT NOT NULL DEFAULT 0 COMMENT '排序值', + `remark` VARCHAR(255) NULL COMMENT '备注', + `created_at` BIGINT NOT NULL, + `updated_at` BIGINT NULL, + `deleted_at` BIGINT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_gao_sales_order_item_product_unit` (`sales_order_id`, `product_id`, `product_unit_id`), + KEY `idx_gao_sales_order_item_order` (`sales_order_id`, `sort`, `created_at`) +) COMMENT='GAO 直接产品订单明细'; + +ALTER TABLE `gao_fund_record` + MODIFY COLUMN `biz_type` VARCHAR(32) NOT NULL COMMENT '业务类型,销售订单'; diff --git a/src/main/resources/db/migration/timiserver/V56__add_gao_package_product_snapshots.sql b/src/main/resources/db/migration/timiserver/V56__add_gao_package_product_snapshots.sql new file mode 100644 index 0000000..2fbd2ef --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V56__add_gao_package_product_snapshots.sql @@ -0,0 +1,29 @@ +-- 套餐功能尚未上线,旧套餐记录无法可靠补齐产品主数据引用,清理不可兼容的旧数据 +DELETE FROM `gao_customer_package_item`; +DELETE FROM `gao_customer_package`; +DELETE FROM `gao_package_config_item`; +UPDATE `gao_package_config` + SET `status` = 'INACTIVE', `deleted_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) + WHERE `deleted_at` IS NULL; +UPDATE `gao_event` + SET `status` = 'DELETED', `deleted_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) + WHERE `biz_type` = 'PACKAGE_PURCHASE' AND `deleted_at` IS NULL; + +ALTER TABLE `gao_package_config_item` + ADD COLUMN IF NOT EXISTS `product_id` VARCHAR(36) NOT NULL COMMENT '产品 ID' AFTER `package_config_id`, + ADD COLUMN IF NOT EXISTS `product_unit_id` VARCHAR(36) NOT NULL COMMENT '产品单位 ID' AFTER `product_id`, + ADD COLUMN IF NOT EXISTS `product_name_snapshot` VARCHAR(128) NOT NULL COMMENT '产品名称快照' AFTER `product_unit_id`, + ADD COLUMN IF NOT EXISTS `unit_snapshot` VARCHAR(32) NOT NULL COMMENT '单位快照' AFTER `product_name_snapshot`, + ADD COLUMN IF NOT EXISTS `conversion_to_base_snapshot` DECIMAL(18,6) NOT NULL COMMENT '折算率快照' AFTER `unit_snapshot`, + ADD KEY IF NOT EXISTS `idx_gao_package_config_item_product` (`product_id`, `product_unit_id`); + +ALTER TABLE `gao_customer_package` + ADD COLUMN IF NOT EXISTS `sales_order_id` VARCHAR(36) NOT NULL COMMENT '销售订单 ID' AFTER `id`, + ADD UNIQUE KEY IF NOT EXISTS `uk_gao_customer_package_sales_order` (`sales_order_id`); + +ALTER TABLE `gao_customer_package_item` + ADD COLUMN IF NOT EXISTS `product_id` VARCHAR(36) NOT NULL COMMENT '产品 ID' AFTER `customer_package_id`, + ADD COLUMN IF NOT EXISTS `product_unit_id` VARCHAR(36) NOT NULL COMMENT '产品单位 ID' AFTER `product_id`, + ADD COLUMN IF NOT EXISTS `conversion_to_base_snapshot` DECIMAL(18,6) NOT NULL COMMENT '折算率快照' AFTER `unit_snapshot`, + ADD COLUMN IF NOT EXISTS `fulfilled_quantity` DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT '已履约数量' AFTER `reserved_quantity`, + ADD KEY IF NOT EXISTS `idx_gao_customer_package_item_product` (`product_id`, `product_unit_id`); diff --git a/src/main/resources/db/migration/timiserver/V57__create_gao_order_fulfillment_tables.sql b/src/main/resources/db/migration/timiserver/V57__create_gao_order_fulfillment_tables.sql new file mode 100644 index 0000000..6ea9455 --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V57__create_gao_order_fulfillment_tables.sql @@ -0,0 +1,49 @@ +CREATE TABLE IF NOT EXISTS `gao_order_fulfillment` ( + `id` VARCHAR(36) NOT NULL, + `sales_order_id` VARCHAR(36) NOT NULL COMMENT '销售订单 ID', + `store_id` VARCHAR(36) NOT NULL COMMENT '门店 ID', + `customer_id` VARCHAR(36) NOT NULL COMMENT '客户 ID', + `fulfillment_type` VARCHAR(32) NOT NULL COMMENT '履约类型', + `status` VARCHAR(32) NOT NULL COMMENT '履约状态', + `request_no` VARCHAR(128) NOT NULL COMMENT '履约幂等号', + `operator_user_id` VARCHAR(36) NOT NULL COMMENT '提交人', + `delivered_at` BIGINT NOT NULL COMMENT '实际履约时间', + `verifier_user_id` VARCHAR(36) NULL COMMENT '核销或拒绝操作人', + `verified_at` BIGINT NULL COMMENT '核销或拒绝时间', + `source_fulfillment_id` VARCHAR(36) NULL COMMENT '冲正来源履约单 ID', + `reason` TEXT NULL COMMENT '原因', + `remark` TEXT NULL COMMENT '备注', + `created_at` BIGINT NOT NULL, + `updated_at` BIGINT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_gao_order_fulfillment_store_request` (`store_id`, `request_no`), + UNIQUE KEY `uk_gao_order_fulfillment_source` (`source_fulfillment_id`), + KEY `idx_gao_order_fulfillment_order` (`sales_order_id`, `created_at`), + KEY `idx_gao_order_fulfillment_customer` (`store_id`, `customer_id`, `status`, `delivered_at`), + KEY `idx_gao_order_fulfillment_status` (`store_id`, `status`, `delivered_at`) +) COMMENT='GAO 统一履约单'; + +CREATE TABLE IF NOT EXISTS `gao_order_fulfillment_item` ( + `id` VARCHAR(36) NOT NULL, + `fulfillment_id` VARCHAR(36) NOT NULL COMMENT '履约单 ID', + `product_id` VARCHAR(36) NOT NULL COMMENT '产品 ID', + `product_unit_id` VARCHAR(36) NOT NULL COMMENT '产品单位 ID', + `source_order_item_id` VARCHAR(36) NULL COMMENT '直接订单明细 ID', + `source_customer_package_item_id` VARCHAR(36) NULL COMMENT '客户套餐权益项 ID', + `quantity` DECIMAL(18,3) NOT NULL COMMENT '本次履约数量', + `base_quantity` DECIMAL(18,3) NOT NULL COMMENT '基础单位数量', + `reserved_before` DECIMAL(18,3) NOT NULL COMMENT '操作前待履约数量', + `reserved_after` DECIMAL(18,3) NOT NULL COMMENT '操作后待履约数量', + `fulfilled_before` DECIMAL(18,3) NOT NULL COMMENT '操作前已履约数量', + `fulfilled_after` DECIMAL(18,3) NOT NULL COMMENT '操作后已履约数量', + `product_name_snapshot` VARCHAR(128) NOT NULL COMMENT '产品名称快照', + `unit_snapshot` VARCHAR(32) NOT NULL COMMENT '单位快照', + `conversion_to_base_snapshot` DECIMAL(18,6) NOT NULL COMMENT '折算率快照', + `created_at` BIGINT NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_gao_order_fulfillment_item_order` (`fulfillment_id`, `source_order_item_id`), + UNIQUE KEY `uk_gao_order_fulfillment_item_package` (`fulfillment_id`, `source_customer_package_item_id`), + KEY `idx_gao_order_fulfillment_item_fulfillment` (`fulfillment_id`, `created_at`), + KEY `idx_gao_order_fulfillment_item_order_source` (`source_order_item_id`), + KEY `idx_gao_order_fulfillment_item_package_source` (`source_customer_package_item_id`) +) COMMENT='GAO 统一履约明细'; diff --git a/src/main/resources/db/migration/timiserver/V58__remove_gao_package_legacy_chain.sql b/src/main/resources/db/migration/timiserver/V58__remove_gao_package_legacy_chain.sql new file mode 100644 index 0000000..28267b1 --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V58__remove_gao_package_legacy_chain.sql @@ -0,0 +1,34 @@ +ALTER TABLE `gao_package_config_item` + MODIFY COLUMN `product_id` VARCHAR(36) NOT NULL COMMENT '产品 ID', + MODIFY COLUMN `product_unit_id` VARCHAR(36) NOT NULL COMMENT '产品单位 ID', + MODIFY COLUMN `product_name_snapshot` VARCHAR(128) NOT NULL COMMENT '产品名称快照', + MODIFY COLUMN `unit_snapshot` VARCHAR(32) NOT NULL COMMENT '单位快照', + MODIFY COLUMN `conversion_to_base_snapshot` DECIMAL(18,6) NOT NULL COMMENT '折算率快照', + DROP COLUMN `product_name`, + DROP COLUMN `unit`, + ADD UNIQUE KEY `uk_gao_package_config_item_product_unit` (`package_config_id`, `product_id`, `product_unit_id`, `deleted_at`); + +ALTER TABLE `gao_customer_package` + MODIFY COLUMN `sales_order_id` VARCHAR(36) NOT NULL COMMENT '销售订单 ID', + DROP INDEX `uk_gao_customer_package_purchase_event`, + DROP INDEX `uk_gao_customer_package_request`, + DROP INDEX `idx_gao_customer_package_payment`, + DROP COLUMN `purchase_event_record_id`, + DROP COLUMN `request_no`, + DROP COLUMN `sale_amount`, + DROP COLUMN `pending_paid_amount`, + DROP COLUMN `paid_amount`, + DROP COLUMN `refunded_amount`, + DROP COLUMN `payment_status`, + DROP COLUMN `refund_status`, + DROP COLUMN `purchased_at`, + DROP COLUMN `operator_user_id`; + +ALTER TABLE `gao_customer_package_item` + MODIFY COLUMN `product_id` VARCHAR(36) NOT NULL COMMENT '产品 ID', + MODIFY COLUMN `product_unit_id` VARCHAR(36) NOT NULL COMMENT '产品单位 ID', + MODIFY COLUMN `conversion_to_base_snapshot` DECIMAL(18,6) NOT NULL COMMENT '折算率快照', + DROP COLUMN `delivered_quantity`; + +DROP TABLE IF EXISTS `gao_package_delivery_item`; +DROP TABLE IF EXISTS `gao_package_delivery`; diff --git a/src/main/resources/mapper/timi-server/gao/GaoCustomerPackageItemMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoCustomerPackageItemMapper.xml index fbb8168..0ec6bde 100644 --- a/src/main/resources/mapper/timi-server/gao/GaoCustomerPackageItemMapper.xml +++ b/src/main/resources/mapper/timi-server/gao/GaoCustomerPackageItemMapper.xml @@ -27,13 +27,13 @@ SET `reserved_quantity` = `reserved_quantity` + #{delta}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) WHERE `id` = #{id} AND `deleted_at` IS NULL AND `reserved_quantity` + #{delta} >= 0 - AND `reserved_quantity` + `delivered_quantity` + #{delta} <= `total_quantity` + AND `reserved_quantity` + `fulfilled_quantity` + #{delta} <= `total_quantity` - + UPDATE `gao_customer_package_item` - SET `delivered_quantity` = `delivered_quantity` + #{delta}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) + SET `fulfilled_quantity` = `fulfilled_quantity` + #{delta}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) WHERE `id` = #{id} AND `deleted_at` IS NULL - AND `delivered_quantity` + #{delta} >= 0 - AND `reserved_quantity` + `delivered_quantity` + #{delta} <= `total_quantity` + AND `fulfilled_quantity` + #{delta} >= 0 + AND `reserved_quantity` + `fulfilled_quantity` + #{delta} <= `total_quantity` diff --git a/src/main/resources/mapper/timi-server/gao/GaoCustomerPackageMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoCustomerPackageMapper.xml index d6696fc..ce6f6b6 100644 --- a/src/main/resources/mapper/timi-server/gao/GaoCustomerPackageMapper.xml +++ b/src/main/resources/mapper/timi-server/gao/GaoCustomerPackageMapper.xml @@ -1,24 +1,14 @@ - - - SELECT * FROM `gao_customer_package` - WHERE `purchase_event_record_id` = #{purchaseEventRecordId} + WHERE `sales_order_id` = #{salesOrderId} AND `deleted_at` IS NULL LIMIT 1 @@ -26,10 +16,6 @@ AND p.`store_id` = #{storeId} AND p.`customer_id` = #{customerId} AND p.`status` = #{status} - AND p.`payment_status` = #{paymentStatus} - AND p.`refund_status` = #{refundStatus} - AND p.`purchased_at` >= #{beginAt} - AND p.`purchased_at` <= #{endAt} AND (p.`package_name_snapshot` LIKE CONCAT('%', #{keyword}, '%') OR c.`name` LIKE CONCAT('%', #{keyword}, '%') OR c.`code` LIKE CONCAT('%', #{keyword}, '%')) - - UPDATE `gao_customer_package` - SET `pending_paid_amount` = `pending_paid_amount` + #{delta}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) - WHERE `id` = #{id} AND `deleted_at` IS NULL AND `pending_paid_amount` + #{delta} >= 0 - - - UPDATE `gao_customer_package` - SET `paid_amount` = `paid_amount` + #{delta}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) - WHERE `id` = #{id} AND `deleted_at` IS NULL AND `paid_amount` + #{delta} >= 0 - - - UPDATE `gao_customer_package` - SET `status` = #{toStatus}, `cancelled_at` = #{cancelledAt}, `cancel_operator_user_id` = #{cancelOperatorUserId}, `cancel_reason` = #{cancelReason}, `refund_status` = #{refundStatus}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) - WHERE `id` = #{id} AND `status` = #{fromStatus} AND `deleted_at` IS NULL - diff --git a/src/main/resources/mapper/timi-server/gao/GaoFundRecordMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoFundRecordMapper.xml index 9b445d6..d97f06f 100644 --- a/src/main/resources/mapper/timi-server/gao/GaoFundRecordMapper.xml +++ b/src/main/resources/mapper/timi-server/gao/GaoFundRecordMapper.xml @@ -29,7 +29,9 @@ f.`deleted_at` IS NULL AND f.`store_id` = #{storeId} AND f.`biz_id` = #{customerPackageId} + AND f.`biz_id` = #{salesOrderId} AND f.`customer_id` = #{customerId} + AND f.`biz_type` = #{bizType} AND f.`operation_type` = #{operationType} AND f.`status` = #{status} AND f.`occurred_at` >= #{beginAt} diff --git a/src/main/resources/mapper/timi-server/gao/GaoOrderFulfillmentItemMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoOrderFulfillmentItemMapper.xml new file mode 100644 index 0000000..2a1bd6f --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoOrderFulfillmentItemMapper.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/src/main/resources/mapper/timi-server/gao/GaoOrderFulfillmentMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoOrderFulfillmentMapper.xml new file mode 100644 index 0000000..77b12fe --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoOrderFulfillmentMapper.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + 1 = 1 + AND `store_id` = #{storeId} + AND `sales_order_id` = #{salesOrderId} + AND `customer_id` = #{customerId} + AND `fulfillment_type` = #{fulfillmentType} + AND `status` = #{status} + AND `delivered_at` >= #{beginAt} + AND `delivered_at` <= #{endAt} + + + + + UPDATE `gao_order_fulfillment` + SET `status` = #{status}, `verifier_user_id` = #{verifierUserId}, `verified_at` = #{verifiedAt}, + `reason` = #{reason}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) + WHERE `id` = #{id} + + diff --git a/src/main/resources/mapper/timi-server/gao/GaoPackageDeliveryItemMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoPackageDeliveryItemMapper.xml deleted file mode 100644 index 3c723c7..0000000 --- a/src/main/resources/mapper/timi-server/gao/GaoPackageDeliveryItemMapper.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - diff --git a/src/main/resources/mapper/timi-server/gao/GaoPackageDeliveryMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoPackageDeliveryMapper.xml deleted file mode 100644 index 3f2c6c4..0000000 --- a/src/main/resources/mapper/timi-server/gao/GaoPackageDeliveryMapper.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - d.`deleted_at` IS NULL - AND d.`store_id` = #{storeId} - AND d.`customer_package_id` = #{customerPackageId} - AND d.`customer_id` = #{customerId} - AND d.`status` = #{status} - AND d.`operation_type` = #{operationType} - AND d.`delivered_at` >= #{beginAt} - AND d.`delivered_at` <= #{endAt} - - - - - UPDATE `gao_package_delivery` - SET `status` = #{toStatus}, `verifier_user_id` = #{verifierUserId}, `verified_at` = #{verifiedAt}, `reason` = #{reason}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) - WHERE `id` = #{id} AND `status` = #{fromStatus} AND `deleted_at` IS NULL - - diff --git a/src/main/resources/mapper/timi-server/gao/GaoProductCategoryMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoProductCategoryMapper.xml new file mode 100644 index 0000000..1e56b64 --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoProductCategoryMapper.xml @@ -0,0 +1,63 @@ + + + + + + + UPDATE `gao_product_category` + SET + `sort` = CASE `id` + + WHEN #{id} THEN #{sort} + + END, + `updated_at` = #{updatedAt} + WHERE `deleted_at` IS NULL + AND `id` IN + + #{id} + + + + + + + diff --git a/src/main/resources/mapper/timi-server/gao/GaoProductMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoProductMapper.xml new file mode 100644 index 0000000..fc329a7 --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoProductMapper.xml @@ -0,0 +1,45 @@ + + + + + + + + diff --git a/src/main/resources/mapper/timi-server/gao/GaoProductStockMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoProductStockMapper.xml new file mode 100644 index 0000000..9b3bf7f --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoProductStockMapper.xml @@ -0,0 +1,60 @@ + + + + + + + + + INSERT IGNORE INTO `gao_product_stock` + (`id`, `store_id`, `product_id`, `on_hand_quantity`, `reserved_quantity`, `updated_by`, `created_at`) + VALUES + (#{id}, #{storeId}, #{productId}, #{onHandQuantity}, #{reservedQuantity}, #{updatedBy}, #{createdAt}) + + + UPDATE `gao_product_stock` + SET `on_hand_quantity` = `on_hand_quantity` + #{onHandDelta}, + `reserved_quantity` = `reserved_quantity` + #{reservedDelta}, + `updated_by` = #{updatedBy}, + `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) + WHERE `id` = #{id} + AND `on_hand_quantity` + #{onHandDelta} >= 0 + AND `reserved_quantity` + #{reservedDelta} >= 0 + AND `reserved_quantity` + #{reservedDelta} <= `on_hand_quantity` + #{onHandDelta} + + + UPDATE `gao_product_stock` + SET `reserved_quantity` = `reserved_quantity` + #{quantity}, `updated_by` = #{updatedBy}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) + WHERE `id` = #{id} AND `reserved_quantity` + #{quantity} <= `on_hand_quantity` + + + UPDATE `gao_product_stock` + SET `reserved_quantity` = `reserved_quantity` - #{quantity}, `updated_by` = #{updatedBy}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) + WHERE `id` = #{id} AND `reserved_quantity` >= #{quantity} + + diff --git a/src/main/resources/mapper/timi-server/gao/GaoProductUnitMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoProductUnitMapper.xml new file mode 100644 index 0000000..ad4358e --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoProductUnitMapper.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + UPDATE `gao_product_unit` + SET `name` = #{name}, `conversion_to_base` = #{conversionToBase}, `is_base` = #{isBase}, + `allow_purchase` = #{allowPurchase}, `allow_sale` = #{allowSale}, `sort` = #{sort}, + `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) + WHERE `id` = #{id} AND `product_id` = #{productId} AND `deleted_at` IS NULL + + diff --git a/src/main/resources/mapper/timi-server/gao/GaoSalesOrderItemMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoSalesOrderItemMapper.xml new file mode 100644 index 0000000..38669f4 --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoSalesOrderItemMapper.xml @@ -0,0 +1,26 @@ + + + + + + + UPDATE `gao_sales_order_item` + SET `reserved_quantity` = `reserved_quantity` + #{delta}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) + WHERE `id` = #{id} AND `reserved_quantity` + #{delta} >= 0 + AND `fulfilled_quantity` + `reserved_quantity` + #{delta} <= `quantity` + + + UPDATE `gao_sales_order_item` + SET `fulfilled_quantity` = `fulfilled_quantity` + #{delta}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) + WHERE `id` = #{id} AND `fulfilled_quantity` + #{delta} >= 0 + AND `fulfilled_quantity` + #{delta} <= `quantity` + + diff --git a/src/main/resources/mapper/timi-server/gao/GaoSalesOrderMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoSalesOrderMapper.xml new file mode 100644 index 0000000..09fc2bd --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoSalesOrderMapper.xml @@ -0,0 +1,56 @@ + + + + + + + o.`deleted_at` IS NULL + AND o.`store_id` = #{storeId} + AND o.`customer_id` = #{customerId} + AND o.`order_type` = #{orderType} + AND o.`status` = #{status} + AND o.`payment_status` = #{paymentStatus} + AND o.`status` = 'FULFILLED' AND o.`paid_amount` < o.`sale_amount` + AND (o.`order_no` LIKE CONCAT('%', #{keyword}, '%') OR c.`name` LIKE CONCAT('%', #{keyword}, '%') OR c.`code` LIKE CONCAT('%', #{keyword}, '%')) + AND o.`purchased_at` >= #{beginAt} + AND o.`purchased_at` <= #{endAt} + + + + + UPDATE `gao_sales_order` + SET `pending_paid_amount` = #{pendingPaidAmount}, `paid_amount` = #{paidAmount}, `refunded_amount` = #{refundedAmount}, + `payment_status` = #{paymentStatus}, `refund_status` = #{refundStatus}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) + WHERE `id` = #{id} AND `deleted_at` IS NULL + + + UPDATE `gao_sales_order` + SET `status` = #{toStatus}, `cancelled_at` = #{cancelledAt}, `cancel_operator_user_id` = #{cancelOperatorUserId}, + `cancel_reason` = #{cancelReason}, `refund_status` = #{refundStatus}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) + WHERE `id` = #{id} AND `status` = #{fromStatus} AND `deleted_at` IS NULL + + + UPDATE `gao_sales_order` + SET `source_event_record_id` = #{sourceEventRecordId}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) + WHERE `id` = #{id} AND `source_event_record_id` IS NULL AND `deleted_at` IS NULL + + + UPDATE `gao_sales_order` + SET `status` = #{status}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) + WHERE `id` = #{id} AND `deleted_at` IS NULL + + diff --git a/src/main/resources/mapper/timi-server/gao/GaoStockRecordMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoStockRecordMapper.xml new file mode 100644 index 0000000..3e26e41 --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoStockRecordMapper.xml @@ -0,0 +1,38 @@ + + + + + + + + 1 = 1 + AND r.`store_id` = #{storeId} + AND r.`product_id` = #{productId} + AND r.`biz_type` = #{bizType} + AND r.`biz_id` = #{bizId} + AND r.`operation_type` = #{operationType} + AND r.`occurred_at` >= #{beginAt} + AND r.`occurred_at` <= #{endAt} + + + +