From 731c777b8beab814a0cb8040515a756b55dfc747 Mon Sep 17 00:00:00 2001 From: Timi Date: Thu, 3 Sep 2026 10:47:58 +0800 Subject: [PATCH 01/10] add gao product --- .../api/modules/common/entity/Attachment.java | 8 +- .../common/service/ArticleService.java | 8 + .../implement/ArticleServiceImplement.java | 7 + .../implement/TempFileServiceImplement.java | 1 + .../modules/gao/bean/GaoPermissionCode.java | 32 + .../GaoCustomerPackageController.java | 146 +--- .../gao/controller/GaoEventController.java | 13 + .../controller/GaoFundRecordController.java | 12 +- .../GaoOrderFulfillmentController.java | 146 ++++ .../GaoPackageConfigController.java | 3 + .../GaoPackageDeliveryController.java | 140 ---- .../gao/controller/GaoProductController.java | 154 ++++ .../controller/GaoProductStockController.java | 110 +++ .../controller/GaoSalesOrderController.java | 133 ++++ .../gao/controller/GaoStoreController.java | 12 + .../gao/entity/GaoCustomerPackage.java | 75 +- .../gao/entity/GaoCustomerPackageItem.java | 13 +- .../api/modules/gao/entity/GaoFundRecord.java | 6 +- ...Delivery.java => GaoOrderFulfillment.java} | 55 +- .../gao/entity/GaoOrderFulfillmentItem.java | 58 ++ .../gao/entity/GaoPackageConfigItem.java | 17 +- .../gao/entity/GaoPackageDeliveryItem.java | 43 -- .../api/modules/gao/entity/GaoProduct.java | 109 +++ .../gao/entity/GaoProductCategory.java | 36 + .../modules/gao/entity/GaoProductStock.java | 40 + .../modules/gao/entity/GaoProductUnit.java | 37 + .../api/modules/gao/entity/GaoSalesOrder.java | 157 ++++ .../modules/gao/entity/GaoSalesOrderItem.java | 55 ++ .../modules/gao/entity/GaoStockRecord.java | 145 ++++ .../api/modules/gao/entity/GaoStore.java | 3 + .../mapper/GaoCustomerPackageItemMapper.java | 4 +- .../gao/mapper/GaoCustomerPackageMapper.java | 18 +- .../gao/mapper/GaoEventRecordMapper.java | 4 + .../mapper/GaoOrderFulfillmentItemMapper.java | 20 + .../gao/mapper/GaoOrderFulfillmentMapper.java | 44 ++ .../mapper/GaoPackageDeliveryItemMapper.java | 21 - .../gao/mapper/GaoPackageDeliveryMapper.java | 42 -- .../gao/mapper/GaoProductCategoryMapper.java | 37 + .../modules/gao/mapper/GaoProductMapper.java | 27 + .../gao/mapper/GaoProductStockMapper.java | 39 + .../gao/mapper/GaoProductUnitMapper.java | 27 + .../gao/mapper/GaoSalesOrderItemMapper.java | 27 + .../gao/mapper/GaoSalesOrderMapper.java | 43 ++ .../gao/mapper/GaoStockRecordMapper.java | 30 + .../service/GaoCustomerPackageService.java | 21 +- .../gao/service/GaoEventRecordService.java | 103 +++ .../modules/gao/service/GaoEventService.java | 6 + .../gao/service/GaoFundRecordService.java | 72 +- .../service/GaoOrderFulfillmentService.java | 49 ++ .../gao/service/GaoOrderPaymentService.java | 36 + .../service/GaoPackageDeliveryService.java | 60 -- .../gao/service/GaoPointLedgerService.java | 14 + .../service/GaoProductCategoryService.java | 27 + .../gao/service/GaoProductService.java | 44 ++ .../gao/service/GaoProductStockService.java | 53 ++ .../gao/service/GaoProductUnitService.java | 28 + .../gao/service/GaoSalesOrderService.java | 72 ++ .../gao/service/GaoStockRecordService.java | 32 + .../modules/gao/service/GaoStoreService.java | 13 + .../GaoCustomerChartServiceImplement.java | 8 +- .../GaoCustomerPackageServiceImplement.java | 159 ++-- .../GaoEventRecordServiceImplement.java | 109 ++- .../implement/GaoEventServiceImplement.java | 23 +- .../GaoFundRecordServiceImplement.java | 335 +-------- .../GaoOrderFulfillmentServiceImplement.java | 695 ++++++++++++++++++ .../GaoOrderPaymentServiceImplement.java | 280 +++++++ .../GaoPackageConfigServiceImplement.java | 46 +- .../GaoPackageDeliveryServiceImplement.java | 431 ----------- .../GaoPointAccountServiceImplement.java | 9 +- .../GaoPointLedgerServiceImplement.java | 12 + .../GaoPointTriggerServiceImplement.java | 17 +- .../GaoProductCategoryServiceImplement.java | 152 ++++ .../implement/GaoProductServiceImplement.java | 284 +++++++ .../GaoProductStockServiceImplement.java | 374 ++++++++++ .../GaoProductUnitServiceImplement.java | 97 +++ .../GaoSalesOrderServiceImplement.java | 470 ++++++++++++ .../GaoStockRecordServiceImplement.java | 118 +++ .../GaoStoreBusinessDayServiceImplement.java | 15 +- .../GaoStoreChartServiceImplement.java | 10 +- .../GaoStoreDiaryServiceImplement.java | 4 +- .../implement/GaoStoreServiceImplement.java | 53 ++ .../gao/vo/GaoCustomerPackagePage.java | 11 - .../vo/GaoCustomerPackagePurchaseItem.java | 17 +- .../GaoDirectProductOrderCreateRequest.java | 35 + .../api/modules/gao/vo/GaoFundRecordPage.java | 6 + .../vo/GaoOrderFulfillmentActionRequest.java | 17 + .../vo/GaoOrderFulfillmentCreateRequest.java | 32 + .../vo/GaoOrderFulfillmentItemRequest.java | 22 + .../gao/vo/GaoOrderFulfillmentPage.java | 36 + ...aoOrderFulfillmentPrepareStockRequest.java | 26 + ... => GaoOrderFulfillmentRevokeRequest.java} | 24 +- ...java => GaoOrderPaymentActionRequest.java} | 10 +- ...va => GaoOrderPaymentNoRefundRequest.java} | 20 +- ...ava => GaoOrderPaymentReceiveRequest.java} | 16 +- ...java => GaoOrderPaymentRefundRequest.java} | 14 +- ...java => GaoOrderPaymentRevokeRequest.java} | 20 +- .../vo/GaoPackageDeliveryActionRequest.java | 17 - .../gao/vo/GaoPackageDeliveryPage.java | 36 - .../gao/vo/GaoPackageOrderCreateData.java | 44 ++ .../gao/vo/GaoProductCategoryPage.java | 24 + .../api/modules/gao/vo/GaoProductPage.java | 35 + .../gao/vo/GaoProductStockChangeRequest.java | 39 + .../modules/gao/vo/GaoProductStockPage.java | 23 + .../gao/vo/GaoProductUnitSelection.java | 21 + .../gao/vo/GaoSalesOrderCancelRequest.java | 20 + .../gao/vo/GaoSalesOrderItemRequest.java | 31 + .../api/modules/gao/vo/GaoSalesOrderPage.java | 42 ++ .../modules/gao/vo/GaoStockChangeItem.java | 58 ++ .../modules/gao/vo/GaoStockRecordPage.java | 36 + .../V53__create_gao_product_tables.sql | 53 ++ .../V54__create_gao_product_stock_tables.sql | 46 ++ .../V55__create_gao_sales_order_tables.sql | 61 ++ ...V56__add_gao_package_product_snapshots.sql | 29 + ...7__create_gao_order_fulfillment_tables.sql | 49 ++ .../V58__remove_gao_package_legacy_chain.sql | 34 + .../gao/GaoCustomerPackageItemMapper.xml | 10 +- .../gao/GaoCustomerPackageMapper.xml | 33 +- .../timi-server/gao/GaoFundRecordMapper.xml | 2 + .../gao/GaoOrderFulfillmentItemMapper.xml | 15 + .../gao/GaoOrderFulfillmentMapper.xml | 57 ++ .../gao/GaoPackageDeliveryItemMapper.xml | 20 - .../gao/GaoPackageDeliveryMapper.xml | 55 -- .../gao/GaoProductCategoryMapper.xml | 63 ++ .../timi-server/gao/GaoProductMapper.xml | 45 ++ .../timi-server/gao/GaoProductStockMapper.xml | 60 ++ .../timi-server/gao/GaoProductUnitMapper.xml | 45 ++ .../gao/GaoSalesOrderItemMapper.xml | 26 + .../timi-server/gao/GaoSalesOrderMapper.xml | 56 ++ .../timi-server/gao/GaoStockRecordMapper.xml | 38 + 129 files changed, 6298 insertions(+), 1719 deletions(-) create mode 100644 src/main/java/com/imyeyu/api/modules/gao/controller/GaoOrderFulfillmentController.java delete mode 100644 src/main/java/com/imyeyu/api/modules/gao/controller/GaoPackageDeliveryController.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/controller/GaoProductController.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/controller/GaoProductStockController.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/controller/GaoSalesOrderController.java rename src/main/java/com/imyeyu/api/modules/gao/entity/{GaoPackageDelivery.java => GaoOrderFulfillment.java} (60%) create mode 100644 src/main/java/com/imyeyu/api/modules/gao/entity/GaoOrderFulfillmentItem.java delete mode 100644 src/main/java/com/imyeyu/api/modules/gao/entity/GaoPackageDeliveryItem.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/entity/GaoProduct.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductCategory.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductStock.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductUnit.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/entity/GaoSalesOrder.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/entity/GaoSalesOrderItem.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/entity/GaoStockRecord.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/mapper/GaoOrderFulfillmentItemMapper.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/mapper/GaoOrderFulfillmentMapper.java delete mode 100644 src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPackageDeliveryItemMapper.java delete mode 100644 src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPackageDeliveryMapper.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductCategoryMapper.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductMapper.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductStockMapper.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductUnitMapper.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/mapper/GaoSalesOrderItemMapper.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/mapper/GaoSalesOrderMapper.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/mapper/GaoStockRecordMapper.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/service/GaoOrderFulfillmentService.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/service/GaoOrderPaymentService.java delete mode 100644 src/main/java/com/imyeyu/api/modules/gao/service/GaoPackageDeliveryService.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/service/GaoProductCategoryService.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/service/GaoProductService.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/service/GaoProductStockService.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/service/GaoProductUnitService.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/service/GaoSalesOrderService.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/service/GaoStockRecordService.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoOrderFulfillmentServiceImplement.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoOrderPaymentServiceImplement.java delete mode 100644 src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoPackageDeliveryServiceImplement.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductCategoryServiceImplement.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductServiceImplement.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductStockServiceImplement.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductUnitServiceImplement.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoSalesOrderServiceImplement.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoStockRecordServiceImplement.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoDirectProductOrderCreateRequest.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentActionRequest.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentCreateRequest.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentItemRequest.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentPage.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderFulfillmentPrepareStockRequest.java rename src/main/java/com/imyeyu/api/modules/gao/vo/{GaoPackageDeliveryCreateRequest.java => GaoOrderFulfillmentRevokeRequest.java} (50%) rename src/main/java/com/imyeyu/api/modules/gao/vo/{GaoFundActionRequest.java => GaoOrderPaymentActionRequest.java} (50%) rename src/main/java/com/imyeyu/api/modules/gao/vo/{GaoFundConfirmNoRefundRequest.java => GaoOrderPaymentNoRefundRequest.java} (52%) rename src/main/java/com/imyeyu/api/modules/gao/vo/{GaoFundReceiveRequest.java => GaoOrderPaymentReceiveRequest.java} (65%) rename src/main/java/com/imyeyu/api/modules/gao/vo/{GaoFundRefundRequest.java => GaoOrderPaymentRefundRequest.java} (72%) rename src/main/java/com/imyeyu/api/modules/gao/vo/{GaoPackageDeliveryRevokeRequest.java => GaoOrderPaymentRevokeRequest.java} (62%) delete mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryActionRequest.java delete mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageDeliveryPage.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageOrderCreateData.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductCategoryPage.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductPage.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockChangeRequest.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockPage.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductUnitSelection.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderCancelRequest.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderItemRequest.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderPage.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoStockChangeItem.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoStockRecordPage.java create mode 100644 src/main/resources/db/migration/timiserver/V53__create_gao_product_tables.sql create mode 100644 src/main/resources/db/migration/timiserver/V54__create_gao_product_stock_tables.sql create mode 100644 src/main/resources/db/migration/timiserver/V55__create_gao_sales_order_tables.sql create mode 100644 src/main/resources/db/migration/timiserver/V56__add_gao_package_product_snapshots.sql create mode 100644 src/main/resources/db/migration/timiserver/V57__create_gao_order_fulfillment_tables.sql create mode 100644 src/main/resources/db/migration/timiserver/V58__remove_gao_package_legacy_chain.sql create mode 100644 src/main/resources/mapper/timi-server/gao/GaoOrderFulfillmentItemMapper.xml create mode 100644 src/main/resources/mapper/timi-server/gao/GaoOrderFulfillmentMapper.xml delete mode 100644 src/main/resources/mapper/timi-server/gao/GaoPackageDeliveryItemMapper.xml delete mode 100644 src/main/resources/mapper/timi-server/gao/GaoPackageDeliveryMapper.xml create mode 100644 src/main/resources/mapper/timi-server/gao/GaoProductCategoryMapper.xml create mode 100644 src/main/resources/mapper/timi-server/gao/GaoProductMapper.xml create mode 100644 src/main/resources/mapper/timi-server/gao/GaoProductStockMapper.xml create mode 100644 src/main/resources/mapper/timi-server/gao/GaoProductUnitMapper.xml create mode 100644 src/main/resources/mapper/timi-server/gao/GaoSalesOrderItemMapper.xml create mode 100644 src/main/resources/mapper/timi-server/gao/GaoSalesOrderMapper.xml create mode 100644 src/main/resources/mapper/timi-server/gao/GaoStockRecordMapper.xml 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} + + + + From 913dc3d278a3de932e6f543390ccdf839d8c5966 Mon Sep 17 00:00:00 2001 From: Timi Date: Thu, 3 Sep 2026 15:16:16 +0800 Subject: [PATCH 02/10] update product unit --- .../gao/entity/GaoCustomerPackageItem.java | 4 +-- .../gao/entity/GaoOrderFulfillmentItem.java | 4 +-- .../gao/entity/GaoPackageConfigItem.java | 4 +-- .../api/modules/gao/entity/GaoProduct.java | 3 -- .../modules/gao/entity/GaoProductUnit.java | 7 ++-- .../modules/gao/entity/GaoSalesOrderItem.java | 4 +-- .../modules/gao/entity/GaoStockRecord.java | 4 +-- .../gao/service/GaoProductUnitService.java | 4 +-- .../GaoCustomerPackageServiceImplement.java | 6 ++-- .../GaoOrderFulfillmentServiceImplement.java | 36 +++++++++---------- .../GaoPackageConfigServiceImplement.java | 6 ++-- .../implement/GaoProductServiceImplement.java | 5 ++- .../GaoProductStockServiceImplement.java | 4 +-- .../GaoProductUnitServiceImplement.java | 24 ++++++++----- .../GaoSalesOrderServiceImplement.java | 10 +++--- .../vo/GaoCustomerPackagePurchaseItem.java | 4 +-- .../modules/gao/vo/GaoStockChangeItem.java | 6 ++-- ...9__move_gao_product_sale_price_to_unit.sql | 28 +++++++++++++++ .../timi-server/gao/GaoProductUnitMapper.xml | 5 +-- 19 files changed, 102 insertions(+), 66 deletions(-) create mode 100644 src/main/resources/db/migration/timiserver/V59__move_gao_product_sale_price_to_unit.sql 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 ea4b5fd..84fc8b6 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 @@ -30,8 +30,8 @@ public class GaoCustomerPackageItem extends UUIDEntity { /// 产品单位快照 private String unitSnapshot; - /// 折算率快照 - private BigDecimal conversionToBaseSnapshot; + /// 基础单位数量系数快照 + private BigDecimal baseQuantityPerUnitSnapshot; /// 总权益数量 private BigDecimal totalQuantity; 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 index 16f1ae6..4c08de0 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoOrderFulfillmentItem.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoOrderFulfillmentItem.java @@ -53,6 +53,6 @@ public class GaoOrderFulfillmentItem extends UUIDEntity { /// 单位快照 private String unitSnapshot; - /// 折算率快照 - private BigDecimal conversionToBaseSnapshot; + /// 基础单位数量系数快照 + private BigDecimal baseQuantityPerUnitSnapshot; } 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 ff2f76b..a3d7a1b 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 @@ -29,8 +29,8 @@ public class GaoPackageConfigItem extends UUIDEntity { /// 单位快照 private String unitSnapshot; - /// 折算率快照 - private BigDecimal conversionToBaseSnapshot; + /// 基础单位数量系数快照 + private BigDecimal baseQuantityPerUnitSnapshot; /// 默认数量 private BigDecimal defaultQuantity; 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 index b8fe90a..4584bb0 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProduct.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProduct.java @@ -77,9 +77,6 @@ public class GaoProduct extends UUIDEntity { /// 状态 private Status status; - /// 建议售价,单位分 - private Long salePrice; - /// 参考成本,单位分 private Long referenceCost; 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 index fc94032..38f3830 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductUnit.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductUnit.java @@ -20,8 +20,11 @@ public class GaoProductUnit extends UUIDEntity { /// 单位名称 private String name; - /// 折算为基础单位的比例 - private BigDecimal conversionToBase; + /// 每个业务单位对应的基础单位数量 + private BigDecimal baseQuantityPerUnit; + + /// 该业务单位建议售价,单位分 + private Long salePrice; /// true 为基础库存单位 private Boolean isBase; 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 index efb2734..b41b8f0 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoSalesOrderItem.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoSalesOrderItem.java @@ -29,8 +29,8 @@ public class GaoSalesOrderItem extends UUIDEntity { /// 单位快照 private String unitSnapshot; - /// 折算率快照 - private BigDecimal conversionToBaseSnapshot; + /// 基础单位数量系数快照 + private BigDecimal baseQuantityPerUnitSnapshot; /// 购买数量 private BigDecimal quantity; 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 index 31a74c9..6b5b593 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoStockRecord.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoStockRecord.java @@ -79,8 +79,8 @@ public class GaoStockRecord implements IDEntity, Creatable { /// 单位名称快照 private String unitSnapshot; - /// 折算率快照 - private BigDecimal conversionToBaseSnapshot; + /// 基础单位数量系数快照 + private BigDecimal baseQuantityPerUnitSnapshot; /// 业务数量 private BigDecimal quantity; 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 index 5ddc33c..f9ae789 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductUnitService.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductUnitService.java @@ -19,10 +19,10 @@ public interface GaoProductUnitService { /// @return 规范化后的单位列表 List validateAndNormalize(GaoProduct product, List unitList); - /// 按基础单位换算数量 + /// 按基础单位数量系数换算业务数量 /// /// @param unit 单位 /// @param quantity 业务数量 /// @return 基础单位数量 - BigDecimal convertToBase(GaoProductUnit unit, BigDecimal quantity); + BigDecimal convertQuantityToBase(GaoProductUnit unit, BigDecimal quantity); } 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 5f92d19..71eb264 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 @@ -250,7 +250,7 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService Boolean.TRUE.equals(unit.getIsBase())).findFirst().orElse(null); TimiException.required(baseUnit, "实物产品缺少基础库存单位"); - BigDecimal baseQuantity = unitService.convertToBase(source.unit(), source.quantity()); + BigDecimal baseQuantity = unitService.convertQuantityToBase(source.unit(), source.quantity()); GaoStockChangeItem item = itemMap.get(source.product().getId()); if (item == null) { item = GaoStockChangeItem.of(source.product(), baseUnit, baseQuantity, baseQuantity); @@ -590,21 +590,21 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService existingList = recordService.listByRequestNo(storeId, requestNo); if (!existingList.isEmpty()) { GaoStockRecord existing = existingList.getFirst(); @@ -237,7 +237,7 @@ public class GaoProductStockServiceImplement implements GaoProductStockService { record.setProductUnitId(item.getProductUnitId()); record.setProductNameSnapshot(item.getProductNameSnapshot()); record.setUnitSnapshot(item.getUnitSnapshot()); - record.setConversionToBaseSnapshot(item.getConversionToBaseSnapshot()); + record.setBaseQuantityPerUnitSnapshot(item.getBaseQuantityPerUnitSnapshot()); record.setQuantity(item.getQuantity()); record.setBaseQuantity(item.getBaseQuantity()); record.setOperationType(operationType); 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 index 4af2824..9ec318a 100644 --- 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 @@ -44,16 +44,17 @@ public class GaoProductUnitServiceImplement implements GaoProductUnitService { } 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.setBaseQuantityPerUnit(TimiJava.defaultIfNull(source.getBaseQuantityPerUnit(), ONE)); + TimiException.requiredTrue(0 < unit.getBaseQuantityPerUnit().signum(), "基础单位数量系数必须大于 0"); + TimiException.requiredTrue(6 >= unit.getBaseQuantityPerUnit().scale(), "基础单位数量系数最多保留六位小数"); + unit.setSalePrice(nonNegative(source.getSalePrice(), "单位建议售价不能为负数")); 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(unit.getBaseQuantityPerUnit().compareTo(ONE) == 0, "基础单位数量系数必须为 1"); TimiException.requiredTrue(baseUnit.equals(unit.getName()), "基础单位名称必须与产品基础单位一致"); base = unit; } @@ -65,22 +66,22 @@ public class GaoProductUnitServiceImplement implements GaoProductUnitService { } @Override - public BigDecimal convertToBase(GaoProductUnit unit, BigDecimal quantity) { + public BigDecimal convertQuantityToBase(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()); + BigDecimal result = quantity.multiply(unit.getBaseQuantityPerUnit()); try { return result.setScale(3, RoundingMode.UNNECESSARY); } catch (ArithmeticException e) { - throw new TimiException(com.imyeyu.java.bean.timi.TimiCode.ERROR, "业务数量折算后不能精确保存为三位小数", 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.setBaseQuantityPerUnit(ONE); unit.setIsBase(true); unit.setAllowPurchase(Boolean.TRUE.equals(product.getStockEnabled())); unit.setAllowSale(true); @@ -94,4 +95,11 @@ public class GaoProductUnitServiceImplement implements GaoProductUnitService { TimiException.requiredTrue(!result.isEmpty(), message); return result; } + + private Long nonNegative(Long value, String message) { + if (value != null) { + TimiException.requiredTrue(0 <= value, message); + } + return value; + } } 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 index 61af01d..198a006 100644 --- 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 @@ -357,13 +357,13 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService - + + @@ -37,7 +38,7 @@ UPDATE `gao_product_unit` - SET `name` = #{name}, `conversion_to_base` = #{conversionToBase}, `is_base` = #{isBase}, + SET `name` = #{name}, `base_quantity_per_unit` = #{baseQuantityPerUnit}, `sale_price` = #{salePrice}, `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 From df16ac3c8e14d7f68b3746b03ec40430b8bea29f Mon Sep 17 00:00:00 2001 From: Timi Date: Fri, 4 Sep 2026 10:37:43 +0800 Subject: [PATCH 03/10] rename GaoStockRecord to GaoProductStockRecord --- .../modules/gao/bean/GaoPermissionCode.java | 2 + .../controller/GaoProductStockController.java | 29 ++- .../gao/entity/GaoOrderFulfillment.java | 2 +- .../api/modules/gao/entity/GaoProduct.java | 6 - ...Record.java => GaoProductStockRecord.java} | 12 +- .../mapper/GaoProductStockRecordMapper.java | 36 ++++ .../gao/mapper/GaoStockRecordMapper.java | 30 --- .../service/GaoProductStockRecordService.java | 41 +++++ .../gao/service/GaoProductStockService.java | 5 +- .../gao/service/GaoStockRecordService.java | 32 ---- .../GaoOrderFulfillmentServiceImplement.java | 12 +- .../implement/GaoProductServiceImplement.java | 11 -- ...aoProductStockRecordServiceImplement.java} | 65 ++++--- .../GaoProductStockServiceImplement.java | 173 ++++++++++++++---- .../GaoProductUnitServiceImplement.java | 29 +-- .../implement/GaoStoreServiceImplement.java | 2 +- .../gao/vo/GaoProductStockChangeRequest.java | 3 - .../GaoProductStockManualRevokeRequest.java | 26 +++ ...ge.java => GaoProductStockRecordPage.java} | 11 +- .../V60__remove_gao_product_base_unit.sql | 2 + ...V61__remove_gao_product_reference_cost.sql | 2 + ...ame_gao_stock_record_and_remove_reason.sql | 12 ++ ...er.xml => GaoProductStockRecordMapper.xml} | 31 +++- 23 files changed, 377 insertions(+), 197 deletions(-) rename src/main/java/com/imyeyu/api/modules/gao/entity/{GaoStockRecord.java => GaoProductStockRecord.java} (92%) create mode 100644 src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductStockRecordMapper.java delete mode 100644 src/main/java/com/imyeyu/api/modules/gao/mapper/GaoStockRecordMapper.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/service/GaoProductStockRecordService.java delete mode 100644 src/main/java/com/imyeyu/api/modules/gao/service/GaoStockRecordService.java rename src/main/java/com/imyeyu/api/modules/gao/service/implement/{GaoStockRecordServiceImplement.java => GaoProductStockRecordServiceImplement.java} (55%) create mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockManualRevokeRequest.java rename src/main/java/com/imyeyu/api/modules/gao/vo/{GaoStockRecordPage.java => GaoProductStockRecordPage.java} (64%) create mode 100644 src/main/resources/db/migration/timiserver/V60__remove_gao_product_base_unit.sql create mode 100644 src/main/resources/db/migration/timiserver/V61__remove_gao_product_reference_cost.sql create mode 100644 src/main/resources/db/migration/timiserver/V62__rename_gao_stock_record_and_remove_reason.sql rename src/main/resources/mapper/timi-server/gao/{GaoStockRecordMapper.xml => GaoProductStockRecordMapper.xml} (53%) 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 d269d80..79b16fb 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 @@ -75,6 +75,8 @@ public enum GaoPermissionCode implements BuiltinPermissionCode { PRODUCT_STOCK_CREATE("PRODUCT_STOCK:CREATE", "产品库存操作", true, true), + PRODUCT_STOCK_MANUAL_REVOKE("PRODUCT_STOCK:MANUAL_REVOKE", "手工库存流水冲正", true, true), + SALES_ORDER_CREATE("SALES_ORDER:CREATE", "销售订单创建", true, true), SALES_ORDER_READ("SALES_ORDER:READ", "销售订单读取", true, true), 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 index 06111bd..c59c93c 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoProductStockController.java +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoProductStockController.java @@ -3,13 +3,14 @@ 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.entity.GaoProductStockRecord; import com.imyeyu.api.modules.gao.service.GaoProductStockService; -import com.imyeyu.api.modules.gao.service.GaoStockRecordService; +import com.imyeyu.api.modules.gao.service.GaoProductStockRecordService; import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; import com.imyeyu.api.modules.gao.vo.GaoProductStockChangeRequest; +import com.imyeyu.api.modules.gao.vo.GaoProductStockManualRevokeRequest; import com.imyeyu.api.modules.gao.vo.GaoProductStockPage; -import com.imyeyu.api.modules.gao.vo.GaoStockRecordPage; +import com.imyeyu.api.modules.gao.vo.GaoProductStockRecordPage; import com.imyeyu.java.bean.timi.TimiException; import com.imyeyu.spring.annotation.AOPLog; import com.imyeyu.spring.annotation.RequestRateLimit; @@ -31,7 +32,7 @@ import org.springframework.web.bind.annotation.RestController; public class GaoProductStockController { private final GaoProductStockService stockService; - private final GaoStockRecordService recordService; + private final GaoProductStockRecordService recordService; private final GaoStoreScopeService storeScopeService; @AOPLog @@ -76,6 +77,14 @@ public class GaoProductStockController { return stockService.manualOut(normalize(request)); } + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_STOCK_MANUAL_REVOKE) + @PostMapping("/manual-revoke") + public GaoProductStock manualRevoke(@RequestBody GaoProductStockManualRevokeRequest request) { + return stockService.manualRevoke(request); + } + @AOPLog @RequestRateLimit @RequireGaoPermission(GaoPermissionCode.PRODUCT_STOCK_CREATE) @@ -96,12 +105,22 @@ public class GaoProductStockController { @RequestRateLimit @RequireGaoPermission(GaoPermissionCode.PRODUCT_STOCK_READ) @PostMapping("/record/list") - public PageResult recordList(@RequestBody GaoStockRecordPage page) { + public PageResult recordList(@RequestBody GaoProductStockRecordPage page) { TimiException.required(page, "not found stock record page"); page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId())); return recordService.pageByQuery(page); } + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_STOCK_READ) + @PostMapping("/record/detail") + public GaoProductStockRecord recordDetail(@RequestParam String id) { + GaoProductStockRecord result = recordService.detail(id); + storeScopeService.checkStoreId(result.getStoreId()); + return result; + } + private GaoProductStockChangeRequest normalize(GaoProductStockChangeRequest request) { TimiException.required(request, "not found stock change request"); request.setStoreId(storeScopeService.resolveStoreId(request.getStoreId())); diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoOrderFulfillment.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoOrderFulfillment.java index 21e208d..c7b5e88 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoOrderFulfillment.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoOrderFulfillment.java @@ -106,7 +106,7 @@ public class GaoOrderFulfillment extends UUIDEntity { /// 库存处理流水 @Transient - private List stockRecordList; + private List stockRecordList; /// 附件 @Transient 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 index 4584bb0..34024cb 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProduct.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProduct.java @@ -65,9 +65,6 @@ public class GaoProduct extends UUIDEntity { /// 产品类型 private ProductType productType; - /// 基础库存单位名称 - private String baseUnit; - /// true 为启用库存管理 private Boolean stockEnabled; @@ -77,9 +74,6 @@ public class GaoProduct extends UUIDEntity { /// 状态 private Status status; - /// 参考成本,单位分 - private Long referenceCost; - /// 最低库存数量,基础单位 private BigDecimal minStockQuantity; diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoStockRecord.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductStockRecord.java similarity index 92% rename from src/main/java/com/imyeyu/api/modules/gao/entity/GaoStockRecord.java rename to src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductStockRecord.java index 6b5b593..b5d007a 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoStockRecord.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductStockRecord.java @@ -17,7 +17,7 @@ import java.util.List; /// @author Codex /// @since 2026-09-02 @Data -public class GaoStockRecord implements IDEntity, Creatable { +public class GaoProductStockRecord implements IDEntity, Creatable { /// 库存流水业务类型 public enum BizType { @@ -41,6 +41,9 @@ public class GaoStockRecord implements IDEntity, Creatable { /// 手工出库 MANUAL_OUT, + /// 手工流水冲正 + MANUAL_REVOKE, + /// 履约预占 ORDER_RESERVE, @@ -118,6 +121,10 @@ public class GaoStockRecord implements IDEntity, Creatable { /// 冲正来源流水 ID private String sourceRecordId; + /// 关联的冲正流水 ID + @Transient + private String revokeRecordId; + /// 幂等请求号 private String requestNo; @@ -127,9 +134,6 @@ public class GaoStockRecord implements IDEntity, Creatable { /// 发生时间 private Long occurredAt; - /// 原因 - private String reason; - /// 备注 private String remark; diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductStockRecordMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductStockRecordMapper.java new file mode 100644 index 0000000..f675b8d --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductStockRecordMapper.java @@ -0,0 +1,36 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoProductStockRecord; +import com.imyeyu.api.modules.gao.vo.GaoProductStockRecordPage; +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 GaoProductStockRecordMapper extends BaseMapper { + + /// 按门店和请求号查询库存流水 + List selectByRequestNo(@Param("storeId") String storeId, @Param("requestNo") String requestNo); + + /// 锁定库存流水 + GaoProductStockRecord selectForUpdate(@Param("id") String id); + + /// 查询库存冲正流水 + GaoProductStockRecord selectRevokeBySourceRecordId(@Param("sourceRecordId") String sourceRecordId); + + /// 按业务查询库存流水 + List selectByBiz(@Param("bizType") GaoProductStockRecord.BizType bizType, @Param("bizId") String bizId); + + /// 查询指定业务和产品的库存流水 + GaoProductStockRecord selectByBizProduct(@Param("bizType") GaoProductStockRecord.BizType bizType, @Param("bizId") String bizId, @Param("productId") String productId, @Param("operationType") GaoProductStockRecord.OperationType operationType); + + /// 按条件统计库存流水 + long countByQuery(GaoProductStockRecordPage page); + + /// 按条件查询库存流水 + List selectByQuery(GaoProductStockRecordPage page); +} 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 deleted file mode 100644 index 7c36e2d..0000000 --- a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoStockRecordMapper.java +++ /dev/null @@ -1,30 +0,0 @@ -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/GaoProductStockRecordService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductStockRecordService.java new file mode 100644 index 0000000..0ced846 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductStockRecordService.java @@ -0,0 +1,41 @@ +package com.imyeyu.api.modules.gao.service; + +import com.imyeyu.api.modules.common.entity.Attachment; +import com.imyeyu.api.modules.gao.entity.GaoProductStockRecord; +import com.imyeyu.api.modules.gao.vo.GaoProductStockRecordPage; +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 GaoProductStockRecordService extends BaseService { + + /// 按条件查询库存流水 + PageResult pageByQuery(GaoProductStockRecordPage page); + + /// 查询库存流水详情 + GaoProductStockRecord detail(String id); + + /// 按来源业务查询库存流水 + List listByBiz(GaoProductStockRecord.BizType bizType, String bizId); + + /// 按门店和请求号查询库存流水 + List listByRequestNo(String storeId, String requestNo); + + /// 锁定库存流水 + GaoProductStockRecord getForUpdate(String id); + + /// 查询库存冲正流水 + GaoProductStockRecord getRevokeBySourceRecordId(String sourceRecordId); + + /// 查询指定业务和产品的库存流水 + GaoProductStockRecord getByBizProduct(GaoProductStockRecord.BizType bizType, String bizId, String productId, + GaoProductStockRecord.OperationType operationType); + + /// 保存库存操作流水及附件 + void createOperationRecord(GaoProductStockRecord record, List attachmentList); +} 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 index c526b85..e44a629 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductStockService.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductStockService.java @@ -1,8 +1,8 @@ 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.GaoProductStockManualRevokeRequest; import com.imyeyu.api.modules.gao.vo.GaoProductStockPage; import com.imyeyu.api.modules.gao.vo.GaoStockChangeItem; import com.imyeyu.spring.bean.PageResult; @@ -30,6 +30,9 @@ public interface GaoProductStockService { /// 手工出库 GaoProductStock manualOut(GaoProductStockChangeRequest request); + /// 手工库存流水冲正 + GaoProductStock manualRevoke(GaoProductStockManualRevokeRequest request); + /// 盘盈 GaoProductStock stocktakeGain(GaoProductStockChangeRequest request); 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 deleted file mode 100644 index a58ad5c..0000000 --- a/src/main/java/com/imyeyu/api/modules/gao/service/GaoStockRecordService.java +++ /dev/null @@ -1,32 +0,0 @@ -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/implement/GaoOrderFulfillmentServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoOrderFulfillmentServiceImplement.java index 7768185..0b4ec7b 100644 --- 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 @@ -11,7 +11,7 @@ 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.GaoProductStockRecord; import com.imyeyu.api.modules.gao.entity.GaoOrderFulfillment; import com.imyeyu.api.modules.gao.entity.GaoOrderFulfillmentItem; import com.imyeyu.api.modules.gao.mapper.GaoOrderFulfillmentItemMapper; @@ -22,7 +22,7 @@ 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.GaoProductStockRecordService; import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentActionRequest; import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentCreateRequest; @@ -74,7 +74,7 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService implements GaoStockRecordService { +public class GaoProductStockRecordServiceImplement extends AbstractEntityService implements GaoProductStockRecordService { - private final GaoStockRecordMapper mapper; + private final GaoProductStockRecordMapper mapper; private final AttachmentService attachmentService; private final UserService userService; @Override - protected BaseMapper mapper() { + protected BaseMapper mapper() { return mapper; } @Override - public PageResult pageByQuery(GaoStockRecordPage page) { + public PageResult pageByQuery(GaoProductStockRecordPage page) { TimiException.required(page, "not found stock record page"); - PageResult result = new PageResult<>(); + PageResult result = new PageResult<>(); result.setTotal(mapper.countByQuery(page)); result.setList(mapper.selectByQuery(page)); fillTransient(result.getList()); @@ -47,24 +47,47 @@ public class GaoStockRecordServiceImplement extends AbstractEntityService listByBiz(GaoStockRecord.BizType bizType, String bizId) { + public GaoProductStockRecord detail(String id) { + TimiException.required(id, "not found stock record id"); + GaoProductStockRecord result = get(id); + TimiException.required(result, "not found stock record"); + GaoProductStockRecord revoke = mapper.selectRevokeBySourceRecordId(result.getId()); + result.setRevokeRecordId(revoke == null ? null : revoke.getId()); + fillTransient(List.of(result)); + return result; + } + + @Override + public List listByBiz(GaoProductStockRecord.BizType bizType, String bizId) { TimiException.required(bizType, "not found stock bizType"); TimiException.required(bizId, "not found stock bizId"); - List result = mapper.selectByBiz(bizType, bizId); + List result = mapper.selectByBiz(bizType, bizId); fillTransient(result); return result; } @Override - public List listByRequestNo(String storeId, String requestNo) { + 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) { + public GaoProductStockRecord getForUpdate(String id) { + TimiException.required(id, "not found stock record id"); + return mapper.selectForUpdate(id); + } + + @Override + public GaoProductStockRecord getRevokeBySourceRecordId(String sourceRecordId) { + TimiException.required(sourceRecordId, "not found sourceRecordId"); + return mapper.selectRevokeBySourceRecordId(sourceRecordId); + } + + @Override + public GaoProductStockRecord getByBizProduct(GaoProductStockRecord.BizType bizType, String bizId, String productId, + GaoProductStockRecord.OperationType operationType) { TimiException.required(bizType, "not found stock bizType"); TimiException.required(bizId, "not found stock bizId"); TimiException.required(productId, "not found productId"); @@ -73,7 +96,7 @@ public class GaoStockRecordServiceImplement extends AbstractEntityService attachmentList) { + public void createOperationRecord(GaoProductStockRecord record, List attachmentList) { TimiException.required(record, "not found stock record"); mapper.insert(record); if (attachmentList != null && !attachmentList.isEmpty()) { @@ -82,12 +105,12 @@ public class GaoStockRecordServiceImplement extends AbstractEntityService recordList) { + private void fillTransient(List recordList) { if (recordList == null || recordList.isEmpty()) { return; } - List idList = recordList.stream().map(GaoStockRecord::getId).toList(); + List idList = recordList.stream().map(GaoProductStockRecord::getId).toList(); Map> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_STOCK_RECORD, idList); - List userIdList = recordList.stream().map(GaoStockRecord::getOperatorUserId) + List userIdList = recordList.stream().map(GaoProductStockRecord::getOperatorUserId) .filter(value -> value != null && !value.isBlank()).distinct().toList(); Map userMap = userIdList.isEmpty() ? Map.of() : userService.mapByIdList(userIdList); - for (GaoStockRecord record : recordList) { + for (GaoProductStockRecord 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/GaoProductStockServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductStockServiceImplement.java index dcfc7ed..6957762 100644 --- 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 @@ -2,17 +2,19 @@ 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.GaoProductStock; import com.imyeyu.api.modules.gao.entity.GaoProductUnit; -import com.imyeyu.api.modules.gao.entity.GaoStockRecord; +import com.imyeyu.api.modules.gao.entity.GaoProductStockRecord; 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.GaoProductStockRecordService; import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; import com.imyeyu.api.modules.gao.vo.GaoProductStockChangeRequest; +import com.imyeyu.api.modules.gao.vo.GaoProductStockManualRevokeRequest; import com.imyeyu.api.modules.gao.vo.GaoProductStockPage; import com.imyeyu.api.modules.gao.vo.GaoProductUnitSelection; import com.imyeyu.api.modules.gao.vo.GaoStockChangeItem; @@ -49,7 +51,8 @@ public class GaoProductStockServiceImplement implements GaoProductStockService { private static final BigDecimal ZERO = BigDecimal.ZERO; private final GaoProductStockMapper stockMapper; - private final GaoStockRecordService recordService; + private final AttachmentService attachmentService; + private final GaoProductStockRecordService recordService; private final GaoProductService productService; private final GaoProductUnitService unitService; private final GaoStoreScopeService storeScopeService; @@ -62,8 +65,17 @@ public class GaoProductStockServiceImplement implements GaoProductStockService { PageResult result = new PageResult<>(); result.setTotal(stockMapper.countByQuery(page)); List list = stockMapper.selectByQuery(page); + Map photoMap = attachmentService.mapFirstImageByIdList( + Attachment.BizType.GAO_PRODUCT, + list.stream().map(GaoProductStock::getProductId).filter(id -> id != null && !id.isBlank()).toList(), + GaoProduct.AttachType.PHOTO.name() + ); for (GaoProductStock stock : list) { fillBalance(stock); + if (stock.getProduct() != null) { + Attachment photo = photoMap.get(stock.getProductId()); + stock.getProduct().setAttachmentList(photo == null ? List.of() : List.of(photo)); + } } result.setList(list); return result; @@ -93,31 +105,37 @@ public class GaoProductStockServiceImplement implements GaoProductStockService { @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)); + return loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_STOCK_INITIAL_IN", request, () -> change(request, GaoProductStockRecord.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)); + return loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_STOCK_MANUAL_IN", request, () -> change(request, GaoProductStockRecord.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)); + return loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_STOCK_MANUAL_OUT", request, () -> change(request, GaoProductStockRecord.OperationType.MANUAL_OUT)); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoProductStock manualRevoke(GaoProductStockManualRevokeRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_STOCK_MANUAL_REVOKE", request, () -> manualRevokeInternal(request)); } @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)); + return loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_STOCK_STOCKTAKE_GAIN", request, () -> change(request, GaoProductStockRecord.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)); + return loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_STOCK_STOCKTAKE_LOSS", request, () -> change(request, GaoProductStockRecord.OperationType.STOCKTAKE_LOSS)); } @Override @@ -141,62 +159,130 @@ public class GaoProductStockServiceImplement implements GaoProductStockService { @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); + return executeInternal(storeId, bizId, items, requestNo, operatorUserId, GaoProductStockRecord.OperationType.ORDER_RESERVE, 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); + return executeInternal(storeId, bizId, items, requestNo, operatorUserId, GaoProductStockRecord.OperationType.ORDER_RELEASE, 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); + return executeInternal(storeId, bizId, items, requestNo, operatorUserId, GaoProductStockRecord.OperationType.ORDER_OUT, 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); + return executeInternal(storeId, bizId, items, requestNo, operatorUserId, GaoProductStockRecord.OperationType.ORDER_REVOKE, null); } - private GaoProductStock change(GaoProductStockChangeRequest request, GaoStockRecord.OperationType operationType) { + private GaoProductStock change(GaoProductStockChangeRequest request, GaoProductStockRecord.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; + boolean purchase = operationType == GaoProductStockRecord.OperationType.INITIAL_IN + || operationType == GaoProductStockRecord.OperationType.MANUAL_IN + || operationType == GaoProductStockRecord.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.convertQuantityToBase(unit, quantity); - List existingList = recordService.listByRequestNo(storeId, requestNo); + 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()); + GaoProductStockRecord existing = existingList.getFirst(); + verifyExisting(existing, operationType, product.getId(), unit.getId(), quantity, baseQuantity, request.getRemark()); return detail(storeId, product.getId()); } TimiException.requiredTrue(hasAttachment(request.getAttachmentList()), "库存操作必须上传照片"); - TimiException.requiredTrue(hasReason(request.getReason()) || hasReason(request.getRemark()), "库存操作必须填写原因或备注"); + TimiException.requiredTrue(hasText(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()); + userLoginService.getRequireLoginUserId(), operationType, 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 GaoProductStock manualRevokeInternal(GaoProductStockManualRevokeRequest request) { + TimiException.required(request, "not found stock manual revoke request"); + String requestNo = requiredText(request.getRequestNo(), "not found requestNo"); + String sourceRecordId = requiredText(request.getSourceRecordId(), "not found sourceRecordId"); + String remark = requiredText(request.getRemark(), "库存流水冲正备注不能为空"); + GaoProductStockRecord source = recordService.getForUpdate(sourceRecordId); + TimiException.required(source, "not found source stock record"); + TimiException.requiredTrue(source.getBizType() == GaoProductStockRecord.BizType.PRODUCT_STOCK, "原库存流水不是手工库存流水"); + TimiException.requiredTrue(source.getOperationType() == GaoProductStockRecord.OperationType.MANUAL_IN + || source.getOperationType() == GaoProductStockRecord.OperationType.MANUAL_OUT, "只有手工入库或手工出库流水可以冲正"); + TimiException.requiredNull(source.getSourceRecordId(), "只能冲正原始手工库存流水"); + String storeId = requiredText(source.getStoreId(), "原库存流水缺少门店"); + String productId = requiredText(source.getProductId(), "原库存流水缺少产品"); + storeScopeService.checkStoreId(storeId); + + List existingList = recordService.listByRequestNo(storeId, requestNo); + if (!existingList.isEmpty()) { + verifyManualRevokeExisting(existingList, sourceRecordId, remark); + return detail(storeId, productId); + } + TimiException.requiredNull(recordService.getRevokeBySourceRecordId(sourceRecordId), "该手工库存流水已经冲正"); + TimiException.requiredTrue(hasAttachment(request.getAttachmentList()), "库存流水冲正必须上传照片"); + TimiException.required(source.getQuantity(), "原库存流水缺少业务数量"); + TimiException.required(source.getBaseQuantity(), "原库存流水缺少基础单位数量"); + TimiException.required(source.getOnHandDelta(), "原库存流水缺少库存变动"); + BigDecimal sourceReservedDelta = decimal(source.getReservedDelta()); + TimiException.requiredTrue(sourceReservedDelta.signum() == 0, "手工库存流水不能包含预占变动"); + + GaoProductStock stock = lockOrCreateStock(storeId, productId); + BigDecimal onHandBefore = decimal(stock.getOnHandQuantity()); + BigDecimal reservedBefore = decimal(stock.getReservedQuantity()); + BigDecimal onHandDelta = source.getOnHandDelta().negate(); + BigDecimal reservedDelta = sourceReservedDelta.negate(); + int updated = stockMapper.updateQuantity(stock.getId(), onHandDelta, reservedDelta, userLoginService.getRequireLoginUserId()); + TimiException.requiredTrue(updated == 1, "库存余额已变化,无法冲正该流水"); + BigDecimal onHandAfter = onHandBefore.add(onHandDelta); + BigDecimal reservedAfter = reservedBefore.add(reservedDelta); + GaoProductStockRecord revoke = new GaoProductStockRecord(); + revoke.setStoreId(storeId); + revoke.setProductId(productId); + revoke.setProductUnitId(source.getProductUnitId()); + revoke.setProductNameSnapshot(source.getProductNameSnapshot()); + revoke.setUnitSnapshot(source.getUnitSnapshot()); + revoke.setBaseQuantityPerUnitSnapshot(source.getBaseQuantityPerUnitSnapshot()); + revoke.setQuantity(source.getQuantity()); + revoke.setBaseQuantity(source.getBaseQuantity()); + revoke.setOperationType(GaoProductStockRecord.OperationType.MANUAL_REVOKE); + revoke.setOnHandDelta(onHandDelta); + revoke.setReservedDelta(reservedDelta); + revoke.setOnHandBefore(onHandBefore); + revoke.setOnHandAfter(onHandAfter); + revoke.setReservedBefore(reservedBefore); + revoke.setReservedAfter(reservedAfter); + revoke.setBizType(GaoProductStockRecord.BizType.PRODUCT_STOCK); + revoke.setBizId(source.getBizId()); + revoke.setSourceRecordId(source.getId()); + revoke.setRequestNo(requestNo); + revoke.setOperatorUserId(userLoginService.getRequireLoginUserId()); + revoke.setOccurredAt(Time.now()); + revoke.setRemark(remark); + recordService.createOperationRecord(revoke, request.getAttachmentList()); + stock.setOnHandQuantity(onHandAfter); + stock.setReservedQuantity(reservedAfter); + stock.setUpdatedBy(revoke.getOperatorUserId()); + fillBalance(stock); + return stock; } private List executeInternal(String storeId, String bizId, List sourceList, String requestNo, - String operatorUserId, GaoStockRecord.OperationType operationType, String reason, String remark, List attachmentList) { + String operatorUserId, GaoProductStockRecord.OperationType operationType, String remark) { + return executeInternal(storeId, bizId, sourceList, requestNo, operatorUserId, operationType, remark, null); + } + + private List executeInternal(String storeId, String bizId, List sourceList, String requestNo, + String operatorUserId, GaoProductStockRecord.OperationType operationType, String remark, List attachmentList) { TimiException.required(storeId, "not found storeId"); TimiException.required(bizId, "not found stock bizId"); TimiException.requiredTrue(sourceList != null && !sourceList.isEmpty(), "库存变更至少需要一个产品"); @@ -211,7 +297,7 @@ public class GaoProductStockServiceImplement implements GaoProductStockService { TimiException.required(item.getBaseQuantity(), "基础单位数量不能为空"); TimiException.requiredTrue(0 < item.getBaseQuantity().signum(), "基础单位数量必须大于 0"); } - List existingList = recordService.listByRequestNo(storeId, normalizedRequestNo); + List existingList = recordService.listByRequestNo(storeId, normalizedRequestNo); if (!existingList.isEmpty()) { verifyExistingList(existingList, itemList, operationType, bizId); List result = new ArrayList<>(); @@ -231,7 +317,7 @@ public class GaoProductStockServiceImplement implements GaoProductStockService { TimiException.requiredTrue(updated == 1, "库存余额已变化或库存数量不足"); BigDecimal onHandAfter = onHandBefore.add(onHandDelta); BigDecimal reservedAfter = reservedBefore.add(reservedDelta); - GaoStockRecord record = new GaoStockRecord(); + GaoProductStockRecord record = new GaoProductStockRecord(); record.setStoreId(storeId); record.setProductId(item.getProductId()); record.setProductUnitId(item.getProductUnitId()); @@ -247,13 +333,12 @@ public class GaoProductStockServiceImplement implements GaoProductStockService { record.setOnHandAfter(onHandAfter); record.setReservedBefore(reservedBefore); record.setReservedAfter(reservedAfter); - record.setBizType(operationType.name().startsWith("ORDER_") ? GaoStockRecord.BizType.ORDER_FULFILLMENT : GaoStockRecord.BizType.PRODUCT_STOCK); + record.setBizType(operationType.name().startsWith("ORDER_") ? GaoProductStockRecord.BizType.ORDER_FULFILLMENT : GaoProductStockRecord.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); @@ -284,15 +369,16 @@ public class GaoProductStockServiceImplement implements GaoProductStockService { return stock; } - private BigDecimal onHandDelta(GaoStockRecord.OperationType operationType, BigDecimal quantity) { + private BigDecimal onHandDelta(GaoProductStockRecord.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; + case MANUAL_REVOKE -> throw new TimiException(TimiCode.ERROR, "手工库存冲正必须基于原库存流水"); }; } - private BigDecimal reservedDelta(GaoStockRecord.OperationType operationType, BigDecimal quantity) { + private BigDecimal reservedDelta(GaoProductStockRecord.OperationType operationType, BigDecimal quantity) { return switch (operationType) { case ORDER_RESERVE -> quantity; case ORDER_RELEASE, ORDER_OUT -> quantity.negate(); @@ -310,26 +396,33 @@ public class GaoProductStockServiceImplement implements GaoProductStockService { return quantity; } - private void verifyExisting(GaoStockRecord existing, GaoStockRecord.OperationType operationType, String productId, String unitId, - BigDecimal quantity, BigDecimal baseQuantity, String remark, String reason) { + private void verifyExisting(GaoProductStockRecord existing, GaoProductStockRecord.OperationType operationType, String productId, String unitId, + BigDecimal quantity, BigDecimal baseQuantity, String remark) { 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 对应库存操作内容不一致"); + && sameText(existing.getRemark(), remark), "requestNo 对应库存操作内容不一致"); } - private void verifyExistingList(List existingList, List itemList, GaoStockRecord.OperationType operationType, String bizId) { - Map recordMap = new HashMap<>(); - for (GaoStockRecord record : existingList) { + private void verifyManualRevokeExisting(List existingList, String sourceRecordId, String remark) { + TimiException.requiredTrue(existingList.size() == 1, "requestNo 对应库存操作内容不一致"); + GaoProductStockRecord existing = existingList.getFirst(); + TimiException.requiredTrue(existing.getOperationType() == GaoProductStockRecord.OperationType.MANUAL_REVOKE + && sourceRecordId.equals(existing.getSourceRecordId()) + && sameText(existing.getRemark(), remark), "requestNo 对应库存操作内容不一致"); + } + + private void verifyExistingList(List existingList, List itemList, GaoProductStockRecord.OperationType operationType, String bizId) { + Map recordMap = new HashMap<>(); + for (GaoProductStockRecord 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()); + GaoProductStockRecord record = recordMap.get(item.getProductId()); TimiException.required(record, "requestNo 对应库存操作内容不一致"); TimiException.requiredTrue(item.getBaseQuantity().compareTo(record.getBaseQuantity()) == 0, "requestNo 对应库存操作内容不一致"); } @@ -353,7 +446,7 @@ public class GaoProductStockServiceImplement implements GaoProductStockService { return false; } - private boolean hasReason(String value) { + private boolean hasText(String value) { return value != null && !value.trim().isEmpty(); } 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 index 9ec318a..c0d0022 100644 --- 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 @@ -26,16 +26,13 @@ public class GaoProductUnitServiceImplement implements GaoProductUnitService { @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; + TimiException.requiredTrue(sourceList != null && !sourceList.isEmpty(), "产品至少需要一个单位"); 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); + int baseCount = 0; + for (int i = 0; i < sourceList.size(); i++) { + GaoProductUnit source = sourceList.get(i); TimiException.required(source, "产品单位不能为空"); GaoProductUnit unit = new GaoProductUnit(); unit.setId(source.getId()); @@ -53,14 +50,13 @@ public class GaoProductUnitServiceImplement implements GaoProductUnitService { unit.setAllowSale(TimiJava.defaultIfNull(source.getAllowSale(), true)); unit.setSort(TimiJava.defaultIfNull(source.getSort(), i)); if (unit.getIsBase()) { - TimiException.requiredNull(base, "一个产品只能有一个基础单位"); + baseCount++; + TimiException.requiredTrue(1 >= baseCount, "一个产品只能有一个基础单位"); TimiException.requiredTrue(unit.getBaseQuantityPerUnit().compareTo(ONE) == 0, "基础单位数量系数必须为 1"); - TimiException.requiredTrue(baseUnit.equals(unit.getName()), "基础单位名称必须与产品基础单位一致"); - base = unit; } result.add(unit); } - TimiException.required(base, "产品必须有一个基础单位"); + TimiException.requiredTrue(0 < baseCount, "产品必须有一个基础单位"); TimiException.requiredTrue(product.getProductType() != GaoProduct.ProductType.SERVICE || !Boolean.TRUE.equals(product.getStockEnabled()), "SERVICE 产品不能启用库存"); return result; } @@ -78,17 +74,6 @@ public class GaoProductUnitServiceImplement implements GaoProductUnitService { } } - private GaoProductUnit createBaseUnit(GaoProduct product) { - GaoProductUnit unit = new GaoProductUnit(); - unit.setName(product.getBaseUnit()); - unit.setBaseQuantityPerUnit(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(); 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 48cdabc..64899a9 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 @@ -260,7 +260,7 @@ public class GaoStoreServiceImplement extends AbstractEntityService attachmentList; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoStockRecordPage.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockRecordPage.java similarity index 64% rename from src/main/java/com/imyeyu/api/modules/gao/vo/GaoStockRecordPage.java rename to src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockRecordPage.java index 5efdb2e..7e68124 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoStockRecordPage.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockRecordPage.java @@ -1,6 +1,6 @@ package com.imyeyu.api.modules.gao.vo; -import com.imyeyu.api.modules.gao.entity.GaoStockRecord; +import com.imyeyu.api.modules.gao.entity.GaoProductStockRecord; import com.imyeyu.java.bean.BasePage; import lombok.Data; import lombok.EqualsAndHashCode; @@ -11,7 +11,7 @@ import lombok.EqualsAndHashCode; /// @since 2026-09-02 @Data @EqualsAndHashCode(callSuper = true) -public class GaoStockRecordPage extends BasePage { +public class GaoProductStockRecordPage extends BasePage { /// 门店 ID private String storeId; @@ -19,14 +19,17 @@ public class GaoStockRecordPage extends BasePage { /// 产品 ID private String productId; + /// 产品名称或编码关键词 + private String keyword; + /// 来源业务类型 - private GaoStockRecord.BizType bizType; + private GaoProductStockRecord.BizType bizType; /// 来源业务 ID private String bizId; /// 操作类型 - private GaoStockRecord.OperationType operationType; + private GaoProductStockRecord.OperationType operationType; /// 起始发生时间 private Long beginAt; diff --git a/src/main/resources/db/migration/timiserver/V60__remove_gao_product_base_unit.sql b/src/main/resources/db/migration/timiserver/V60__remove_gao_product_base_unit.sql new file mode 100644 index 0000000..67ba316 --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V60__remove_gao_product_base_unit.sql @@ -0,0 +1,2 @@ +ALTER TABLE `gao_product` + DROP COLUMN `base_unit`; diff --git a/src/main/resources/db/migration/timiserver/V61__remove_gao_product_reference_cost.sql b/src/main/resources/db/migration/timiserver/V61__remove_gao_product_reference_cost.sql new file mode 100644 index 0000000..c234533 --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V61__remove_gao_product_reference_cost.sql @@ -0,0 +1,2 @@ +ALTER TABLE `gao_product` + DROP COLUMN `reference_cost`; diff --git a/src/main/resources/db/migration/timiserver/V62__rename_gao_stock_record_and_remove_reason.sql b/src/main/resources/db/migration/timiserver/V62__rename_gao_stock_record_and_remove_reason.sql new file mode 100644 index 0000000..fef9b83 --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V62__rename_gao_stock_record_and_remove_reason.sql @@ -0,0 +1,12 @@ +RENAME TABLE `gao_stock_record` TO `gao_product_stock_record`; + +ALTER TABLE `gao_product_stock_record` + DROP KEY `uk_gao_stock_record_request_product`, + DROP KEY `uk_gao_stock_record_source`, + DROP KEY `idx_gao_stock_record_product_time`, + DROP KEY `idx_gao_stock_record_biz_time`, + DROP COLUMN `reason`, + ADD UNIQUE KEY `uk_gao_product_stock_record_request_product` (`store_id`, `request_no`, `operation_type`, `product_id`), + ADD UNIQUE KEY `uk_gao_product_stock_record_source` (`source_record_id`), + ADD KEY `idx_gao_product_stock_record_product_time` (`store_id`, `product_id`, `occurred_at`, `id`), + ADD KEY `idx_gao_product_stock_record_biz_time` (`store_id`, `biz_type`, `biz_id`, `occurred_at`, `id`); diff --git a/src/main/resources/mapper/timi-server/gao/GaoStockRecordMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoProductStockRecordMapper.xml similarity index 53% rename from src/main/resources/mapper/timi-server/gao/GaoStockRecordMapper.xml rename to src/main/resources/mapper/timi-server/gao/GaoProductStockRecordMapper.xml index 3e26e41..6f54333 100644 --- a/src/main/resources/mapper/timi-server/gao/GaoStockRecordMapper.xml +++ b/src/main/resources/mapper/timi-server/gao/GaoProductStockRecordMapper.xml @@ -1,18 +1,28 @@ - - + SELECT * FROM `gao_product_stock_record` WHERE `store_id` = #{storeId} AND `request_no` = #{requestNo} ORDER BY `product_id`, `id` - + SELECT * FROM `gao_product_stock_record` + WHERE `id` = #{id} + FOR UPDATE + + + - + SELECT * FROM `gao_product_stock_record` WHERE `biz_type` = #{bizType} AND `biz_id` = #{bizId} AND `product_id` = #{productId} AND `operation_type` = #{operationType} ORDER BY `occurred_at`, `id` LIMIT 1 @@ -21,6 +31,7 @@ 1 = 1 AND r.`store_id` = #{storeId} AND r.`product_id` = #{productId} + AND (r.`product_name_snapshot` LIKE CONCAT('%', #{keyword}, '%') OR p.`name` LIKE CONCAT('%', #{keyword}, '%') OR p.`code` LIKE CONCAT('%', #{keyword}, '%')) AND r.`biz_type` = #{bizType} AND r.`biz_id` = #{bizId} AND r.`operation_type` = #{operationType} @@ -28,10 +39,10 @@ AND r.`occurred_at` <= #{endAt} - + SELECT r.* FROM `gao_product_stock_record` r LEFT JOIN `gao_product` p ON p.`id` = r.`product_id` AND p.`store_id` = r.`store_id` WHERE ORDER BY r.`occurred_at` DESC, r.`id` DESC LIMIT #{offset}, #{limit} From 41f692aad91a1a32e92da64cb265356b713dd8ac Mon Sep 17 00:00:00 2001 From: Timi Date: Fri, 4 Sep 2026 14:55:21 +0800 Subject: [PATCH 04/10] add groovy script --- pom.xml | 5 + .../bean/GroovyScriptExecutionResult.java | 9 + .../api/modules/common/entity/Attachment.java | 2 + .../modules/common/entity/GroovyScript.java | 42 +++ .../common/entity/GroovyScriptVersion.java | 26 ++ .../common/mapper/GroovyScriptMapper.java | 11 + .../mapper/GroovyScriptVersionMapper.java | 11 + .../common/service/GroovyScriptService.java | 120 +++++++ .../GroovyScriptServiceImplement.java | 293 +++++++++++++++++ .../gao/controller/GaoStoreController.java | 11 - .../entity/GaoCustomerConsumptionRecord.java | 44 +++ .../api/modules/gao/entity/GaoEvent.java | 5 +- .../modules/gao/entity/GaoPointLedger.java | 33 +- .../api/modules/gao/entity/GaoPointRule.java | 67 ++-- .../api/modules/gao/entity/GaoSalesOrder.java | 3 + .../api/modules/gao/entity/GaoStore.java | 3 - .../GaoCustomerConsumptionRecordMapper.java | 20 ++ .../gao/mapper/GaoEventSQLProvider.java | 2 +- ...GaoPointLedgerConsumptionRecordMapper.java | 38 +++ .../gao/mapper/GaoPointLedgerMapper.java | 13 +- .../gao/mapper/GaoPointRuleMapper.java | 5 +- .../gao/mapper/GaoSalesOrderMapper.java | 5 +- .../modules/gao/script/GaoPointDecision.java | 17 + .../gao/script/GaoPointScriptContext.java | 154 +++++++++ .../GaoCustomerConsumptionRecordService.java | 54 ++++ .../gao/service/GaoEventRecordService.java | 17 - .../gao/service/GaoPointLedgerService.java | 46 ++- .../gao/service/GaoPointRuleService.java | 9 +- .../gao/service/GaoPointTriggerService.java | 16 +- .../modules/gao/service/GaoStoreService.java | 13 - ...omerConsumptionRecordServiceImplement.java | 80 +++++ .../GaoCustomerPackageServiceImplement.java | 52 +-- .../GaoEventRecordServiceImplement.java | 37 +-- .../implement/GaoEventServiceImplement.java | 5 + .../GaoOrderPaymentServiceImplement.java | 36 ++- .../GaoPointLedgerServiceImplement.java | 127 ++++++-- .../GaoPointRuleServiceImplement.java | 161 ++-------- .../GaoPointTriggerServiceImplement.java | 300 +++++++++++------- .../GaoSalesOrderServiceImplement.java | 59 ++-- .../implement/GaoStoreServiceImplement.java | 52 --- .../vo/GaoCustomerPackagePurchaseRequest.java | 4 +- .../gao/vo/GaoPackageOrderCreateData.java | 3 - .../V63__create_groovy_script_tables.sql | 30 ++ ...gao_consumption_and_point_script_model.sql | 62 ++++ ...V65__migrate_gao_point_rules_to_groovy.sql | 23 ++ ..._migrate_gao_legacy_consumption_events.sql | 44 +++ ...7__fix_gao_point_rule_trigger_ref_type.sql | 4 + .../GaoCustomerConsumptionRecordMapper.xml | 20 ++ .../mapper/timi-server/gao/GaoEventMapper.xml | 1 + .../timi-server/gao/GaoEventRecordMapper.xml | 24 ++ .../timi-server/gao/GaoPointLedgerMapper.xml | 15 +- .../timi-server/gao/GaoPointRuleMapper.xml | 9 +- .../timi-server/gao/GaoSalesOrderMapper.xml | 7 +- 53 files changed, 1708 insertions(+), 541 deletions(-) create mode 100644 src/main/java/com/imyeyu/api/modules/common/bean/GroovyScriptExecutionResult.java create mode 100644 src/main/java/com/imyeyu/api/modules/common/entity/GroovyScript.java create mode 100644 src/main/java/com/imyeyu/api/modules/common/entity/GroovyScriptVersion.java create mode 100644 src/main/java/com/imyeyu/api/modules/common/mapper/GroovyScriptMapper.java create mode 100644 src/main/java/com/imyeyu/api/modules/common/mapper/GroovyScriptVersionMapper.java create mode 100644 src/main/java/com/imyeyu/api/modules/common/service/GroovyScriptService.java create mode 100644 src/main/java/com/imyeyu/api/modules/common/service/implement/GroovyScriptServiceImplement.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/entity/GaoCustomerConsumptionRecord.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/mapper/GaoCustomerConsumptionRecordMapper.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPointLedgerConsumptionRecordMapper.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/script/GaoPointDecision.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/script/GaoPointScriptContext.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/service/GaoCustomerConsumptionRecordService.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerConsumptionRecordServiceImplement.java create mode 100644 src/main/resources/db/migration/timiserver/V63__create_groovy_script_tables.sql create mode 100644 src/main/resources/db/migration/timiserver/V64__add_gao_consumption_and_point_script_model.sql create mode 100644 src/main/resources/db/migration/timiserver/V65__migrate_gao_point_rules_to_groovy.sql create mode 100644 src/main/resources/db/migration/timiserver/V66__migrate_gao_legacy_consumption_events.sql create mode 100644 src/main/resources/db/migration/timiserver/V67__fix_gao_point_rule_trigger_ref_type.sql create mode 100644 src/main/resources/mapper/timi-server/gao/GaoCustomerConsumptionRecordMapper.xml diff --git a/pom.xml b/pom.xml index d7cfc82..73fc325 100644 --- a/pom.xml +++ b/pom.xml @@ -180,6 +180,11 @@ + + org.apache.groovy + groovy + 4.0.29 + com.imyeyu.spring timi-spring diff --git a/src/main/java/com/imyeyu/api/modules/common/bean/GroovyScriptExecutionResult.java b/src/main/java/com/imyeyu/api/modules/common/bean/GroovyScriptExecutionResult.java new file mode 100644 index 0000000..22de318 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/common/bean/GroovyScriptExecutionResult.java @@ -0,0 +1,9 @@ +package com.imyeyu.api.modules.common.bean; + +/// Groovy 脚本执行结果 +/// +/// @param 脚本返回值类型 +/// @author Codex +/// @since 2026-09-04 +public record GroovyScriptExecutionResult(String scriptId, String versionId, Integer version, String checksum, T result) { +} 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 64f8561..3b586cc 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 @@ -48,6 +48,8 @@ public class Attachment extends UUIDEntity { GAO_ORDER_FULFILLMENT, + GAO_SALES_ORDER, + TEMP_FILE, MIRROR diff --git a/src/main/java/com/imyeyu/api/modules/common/entity/GroovyScript.java b/src/main/java/com/imyeyu/api/modules/common/entity/GroovyScript.java new file mode 100644 index 0000000..a6c3d02 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/common/entity/GroovyScript.java @@ -0,0 +1,42 @@ +package com.imyeyu.api.modules.common.entity; + +import com.imyeyu.spring.entity.UUIDEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/// Groovy 脚本 +/// +/// @author Codex +/// @since 2026-09-04 +@Data +@EqualsAndHashCode(callSuper = true) +public class GroovyScript extends UUIDEntity { + + /// + /// + /// @author 夜雨 + /// @since 2026/9/4 14:54 + public enum BizType { + + /// GAO 积分规则 + GAO_POINT_RULE + } + + /// 业务归属 + private BizType bizType; + + /// 业务内唯一代码 + private String code; + + /// 名称 + private String name; + + /// 说明 + private String description; + + /// true 为允许执行已发布版本 + private Boolean enabled; + + /// 当前发布版本 ID + private String publishedVersionId; +} diff --git a/src/main/java/com/imyeyu/api/modules/common/entity/GroovyScriptVersion.java b/src/main/java/com/imyeyu/api/modules/common/entity/GroovyScriptVersion.java new file mode 100644 index 0000000..f8c57f1 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/common/entity/GroovyScriptVersion.java @@ -0,0 +1,26 @@ +package com.imyeyu.api.modules.common.entity; + +import com.imyeyu.spring.entity.UUIDEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/// Groovy 脚本版本 +/// +/// @author Codex +/// @since 2026-09-04 +@Data +@EqualsAndHashCode(callSuper = true) +public class GroovyScriptVersion extends UUIDEntity { + + /// 脚本 ID + private String scriptId; + + /// 版本号 + private Integer version; + + /// 脚本内容 + private String content; + + /// 脚本内容 SHA-256 + private String checksum; +} diff --git a/src/main/java/com/imyeyu/api/modules/common/mapper/GroovyScriptMapper.java b/src/main/java/com/imyeyu/api/modules/common/mapper/GroovyScriptMapper.java new file mode 100644 index 0000000..b7e16f6 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/common/mapper/GroovyScriptMapper.java @@ -0,0 +1,11 @@ +package com.imyeyu.api.modules.common.mapper; + +import com.imyeyu.api.modules.common.entity.GroovyScript; +import com.imyeyu.spring.mapper.BaseMapper; + +/// Groovy 脚本 Mapper +/// +/// @author Codex +/// @since 2026-09-04 +public interface GroovyScriptMapper extends BaseMapper { +} diff --git a/src/main/java/com/imyeyu/api/modules/common/mapper/GroovyScriptVersionMapper.java b/src/main/java/com/imyeyu/api/modules/common/mapper/GroovyScriptVersionMapper.java new file mode 100644 index 0000000..f563e21 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/common/mapper/GroovyScriptVersionMapper.java @@ -0,0 +1,11 @@ +package com.imyeyu.api.modules.common.mapper; + +import com.imyeyu.api.modules.common.entity.GroovyScriptVersion; +import com.imyeyu.spring.mapper.BaseMapper; + +/// Groovy 脚本版本 Mapper +/// +/// @author Codex +/// @since 2026-09-04 +public interface GroovyScriptVersionMapper extends BaseMapper { +} diff --git a/src/main/java/com/imyeyu/api/modules/common/service/GroovyScriptService.java b/src/main/java/com/imyeyu/api/modules/common/service/GroovyScriptService.java new file mode 100644 index 0000000..eee5ef4 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/common/service/GroovyScriptService.java @@ -0,0 +1,120 @@ +package com.imyeyu.api.modules.common.service; + +import com.imyeyu.api.modules.common.entity.GroovyScript; +import com.imyeyu.api.modules.common.entity.GroovyScriptVersion; +import com.imyeyu.api.modules.common.bean.GroovyScriptExecutionResult; +import com.imyeyu.spring.service.BaseService; + +import java.util.List; +import java.util.Map; + +/// Groovy 脚本服务 +/// +/// 脚本仅限可信管理员维护,不能作为不可信用户代码的执行环境 +/// +/// @author Codex +/// @since 2026-09-04 +public interface GroovyScriptService extends BaseService { + + /// 按业务归属和代码查询脚本 + /// + /// @param bizType 业务归属 + /// @param code 业务内唯一代码 + /// @return 脚本,不存在时返回 null + GroovyScript getByCode(GroovyScript.BizType bizType, String code); + + /// 创建脚本元数据 + /// + /// 新建脚本默认停用,且没有已发布版本 + /// + /// @param script 脚本元数据 + void create(GroovyScript script); + + /// 更新脚本元数据 + /// + /// 业务归属和代码是脚本稳定标识,创建后不可变更 + /// + /// @param script 脚本元数据 + void update(GroovyScript script); + + /// 设置脚本启用状态 + /// + /// @param id 脚本 ID + /// @param enabled true 为允许执行已发布版本 + void setEnabled(String id, boolean enabled); + + /// 创建不可变的脚本版本 + /// + /// @param scriptId 脚本 ID + /// @param content 脚本内容 + /// @return 新版本 + GroovyScriptVersion createVersion(String scriptId, String content); + + /// 查询脚本的全部版本 + /// + /// @param scriptId 脚本 ID + /// @return 按版本号升序的版本列表 + List listVersion(String scriptId); + + /// 发布指定版本 + /// + /// @param scriptId 脚本 ID + /// @param versionId 脚本版本 ID + void publish(String scriptId, String versionId); + + /// 取消当前发布版本 + /// + /// @param scriptId 脚本 ID + void unpublish(String scriptId); + + /// 删除脚本 + /// + /// @param id 脚本 ID + void delete(String id); + + /// 执行已发布且启用的脚本 + /// + /// @param bizType 业务归属 + /// @param code 业务内唯一代码 + /// @param bindings 传给脚本的变量 + /// @return 脚本返回值 + Object execute(GroovyScript.BizType bizType, String code, Map bindings); + + /// 执行已发布且启用的脚本并返回实际执行版本 + /// + /// @param bizType 业务归属 + /// @param code 业务内唯一代码 + /// @param bindings 传给脚本的变量 + /// @param resultType 返回值类型 + /// @param 返回值类型 + /// @return 脚本执行结果 + GroovyScriptExecutionResult executePublished(GroovyScript.BizType bizType, String code, Map bindings, Class resultType); + + /// 按指定历史版本执行脚本 + /// + /// 用于按积分发生时的脚本版本复算,脚本当前是否发布或启用不影响执行;脚本返回 null 表示未命中,属合法结果 + /// + /// @param versionId 脚本版本 ID + /// @param bindings 传给脚本的变量 + /// @param resultType 返回值类型 + /// @param 返回值类型 + /// @return 脚本执行结果 + GroovyScriptExecutionResult executeVersion(String versionId, Map bindings, Class resultType); + + /// 执行已发布且启用的脚本并验证返回值类型 + /// + /// @param bizType 业务归属 + /// @param code 业务内唯一代码 + /// @param bindings 传给脚本的变量 + /// @param resultType 返回值类型 + /// @param 返回值类型 + /// @return 脚本返回值 + T execute(GroovyScript.BizType bizType, String code, Map bindings, Class resultType); + + /// 清除脚本的本地编译缓存 + /// + /// 多实例部署时,其他节点在收到脚本变更通知后调用此方法 + /// + /// @param scriptId 脚本 ID + void invalidate(String scriptId); +} diff --git a/src/main/java/com/imyeyu/api/modules/common/service/implement/GroovyScriptServiceImplement.java b/src/main/java/com/imyeyu/api/modules/common/service/implement/GroovyScriptServiceImplement.java new file mode 100644 index 0000000..890571c --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/common/service/implement/GroovyScriptServiceImplement.java @@ -0,0 +1,293 @@ +package com.imyeyu.api.modules.common.service.implement; + +import com.imyeyu.api.config.dbsource.TimiServerDBConfig; +import com.imyeyu.api.modules.common.entity.GroovyScript; +import com.imyeyu.api.modules.common.entity.GroovyScriptVersion; +import com.imyeyu.api.modules.common.bean.GroovyScriptExecutionResult; +import com.imyeyu.api.modules.common.mapper.GroovyScriptMapper; +import com.imyeyu.api.modules.common.mapper.GroovyScriptVersionMapper; +import com.imyeyu.api.modules.common.service.GroovyScriptService; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.spring.mapper.BaseMapper; +import com.imyeyu.spring.service.AbstractEntityService; +import groovy.lang.Binding; +import groovy.lang.GroovyClassLoader; +import groovy.lang.Script; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/// Groovy 脚本服务实现 +/// +/// 每个已编译版本使用独立类加载器,缓存失效后可释放对应类及其元数据 +/// +/// @author Codex +/// @since 2026-09-04 +@Slf4j +@Service +@RequiredArgsConstructor +public class GroovyScriptServiceImplement extends AbstractEntityService implements GroovyScriptService { + + private final GroovyScriptMapper mapper; + private final GroovyScriptVersionMapper versionMapper; + private final Map compiledScriptMap = new ConcurrentHashMap<>(); + + @Override + protected BaseMapper mapper() { + return mapper; + } + + @Override + public GroovyScript getByCode(GroovyScript.BizType bizType, String code) { + GroovyScript example = new GroovyScript(); + example.setBizType(bizType); + example.setCode(code); + return mapper.selectByExample(example); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void create(GroovyScript script) { + TimiException.required(script, "未找到脚本"); + TimiException.required(script.getBizType(), "未找到脚本业务归属"); + TimiException.required(script.getCode(), "未找到脚本代码"); + TimiException.required(script.getName(), "未找到脚本名称"); + TimiException.requiredNull(getByCode(script.getBizType(), script.getCode()), "Groovy 脚本已存在"); + script.setEnabled(false); + script.setPublishedVersionId(null); + super.create(script); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void update(GroovyScript script) { + TimiException.required(script, "未找到脚本"); + TimiException.required(script.getId(), "未找到脚本 ID"); + TimiException.required(script.getName(), "未找到脚本名称"); + GroovyScript dbScript = get(script.getId()); + TimiException.required(dbScript, "未找到脚本"); + dbScript.setName(script.getName()); + dbScript.setDescription(script.getDescription()); + mapper.update(dbScript); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void setEnabled(String id, boolean enabled) { + GroovyScript script = requireScript(id); + script.setEnabled(enabled); + mapper.update(script); + invalidateAfterCommit(script.getId()); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GroovyScriptVersion createVersion(String scriptId, String content) { + requireScript(scriptId); + TimiException.required(content, "未找到脚本内容"); + TimiException.requiredTrue(!content.isBlank(), "脚本内容不能为空"); + + GroovyScriptVersion version = new GroovyScriptVersion(); + version.setScriptId(scriptId); + version.setVersion(nextVersion(scriptId)); + version.setContent(content); + version.setChecksum(checksum(content)); + versionMapper.insert(version); + return version; + } + + @Override + public List listVersion(String scriptId) { + TimiException.required(scriptId, "未找到脚本 ID"); + GroovyScriptVersion example = new GroovyScriptVersion(); + example.setScriptId(scriptId); + List versionList = new ArrayList<>(versionMapper.selectAllByExample(example)); + versionList.sort(Comparator.comparing(GroovyScriptVersion::getVersion)); + return versionList; + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void publish(String scriptId, String versionId) { + GroovyScript script = requireScript(scriptId); + GroovyScriptVersion version = requireVersion(versionId); + TimiException.requiredTrue(script.getId().equals(version.getScriptId()), "脚本版本不属于当前脚本"); + script.setPublishedVersionId(version.getId()); + mapper.update(script); + invalidateAfterCommit(script.getId()); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void unpublish(String scriptId) { + GroovyScript script = requireScript(scriptId); + script.setPublishedVersionId(null); + mapper.update(script); + invalidateAfterCommit(script.getId()); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void delete(String id) { + requireScript(id); + super.delete(id); + invalidateAfterCommit(id); + } + + @Override + public Object execute(GroovyScript.BizType bizType, String code, Map bindings) { + return executePublished(bizType, code, bindings, Object.class).result(); + } + + @Override + public GroovyScriptExecutionResult executePublished(GroovyScript.BizType bizType, String code, Map bindings, Class resultType) { + TimiException.required(bizType, "未找到脚本业务归属"); + TimiException.required(code, "未找到脚本代码"); + TimiException.required(resultType, "未找到返回值类型"); + GroovyScript script = getByCode(bizType, code); + TimiException.required(script, "未找到 Groovy 脚本"); + TimiException.requiredTrue(Boolean.TRUE.equals(script.getEnabled()), "Groovy 脚本已停用"); + TimiException.required(script.getPublishedVersionId(), "Groovy 脚本尚未发布"); + GroovyScriptExecutionResult result = executeVersion(script.getPublishedVersionId(), bindings, resultType); + TimiException.requiredTrue(script.getId().equals(result.scriptId()), "Groovy 脚本已发布版本无效"); + return result; + } + + @Override + public T execute(GroovyScript.BizType bizType, String code, Map bindings, Class resultType) { + return executePublished(bizType, code, bindings, resultType).result(); + } + + @Override + public GroovyScriptExecutionResult executeVersion(String versionId, Map bindings, Class resultType) { + TimiException.required(resultType, "未找到返回值类型"); + GroovyScriptVersion version = requireVersion(versionId); + CompiledScript compiledScript = compiledScriptMap.computeIfAbsent(cacheKey(version), key -> compile(version.getScriptId(), version)); + try { + Object result = compiledScript.execute(bindings); + TimiException.requiredTrue(result == null || resultType.isInstance(result), "Groovy 脚本返回值类型不匹配"); + return new GroovyScriptExecutionResult<>(version.getScriptId(), version.getId(), version.getVersion(), version.getChecksum(), resultType.cast(result)); + } catch (TimiException e) { + throw e; + } catch (RuntimeException e) { + log.warn("Groovy 脚本执行失败,scriptId={}, versionId={}", version.getScriptId(), version.getId(), e); + throw new TimiException(TimiCode.ERROR, "Groovy 脚本执行失败"); + } + } + + @Override + public void invalidate(String scriptId) { + if (scriptId == null) { + return; + } + compiledScriptMap.entrySet().removeIf(entry -> { + if (!scriptId.equals(entry.getValue().scriptId())) { + return false; + } + entry.getValue().close(); + return true; + }); + } + + private GroovyScript requireScript(String id) { + TimiException.required(id, "未找到脚本 ID"); + GroovyScript script = get(id); + TimiException.required(script, "未找到脚本"); + return script; + } + + private GroovyScriptVersion requireVersion(String id) { + TimiException.required(id, "未找到脚本版本 ID"); + GroovyScriptVersion version = versionMapper.select(id); + TimiException.required(version, "未找到脚本版本"); + return version; + } + + private int nextVersion(String scriptId) { + return listVersion(scriptId).stream() + .map(GroovyScriptVersion::getVersion) + .max(Integer::compareTo) + .orElse(0) + 1; + } + + private String cacheKey(GroovyScriptVersion version) { + return "%s:%s".formatted(version.getId(), version.getChecksum()); + } + + private CompiledScript compile(String scriptId, GroovyScriptVersion version) { + GroovyClassLoader classLoader = new GroovyClassLoader(getClass().getClassLoader()); + try { + Class type = classLoader.parseClass(version.getContent(), "GroovyScript_%s_%d.groovy".formatted(version.getId().replace('-', '_'), version.getVersion())); + return new CompiledScript(scriptId, type.asSubclass(Script.class), classLoader); + } catch (RuntimeException e) { + close(classLoader); + log.warn("Groovy 脚本编译失败,scriptId={}, versionId={}", scriptId, version.getId(), e); + throw new TimiException(TimiCode.ERROR, "Groovy 脚本编译失败"); + } + } + + private String checksum(String content) { + try { + return java.util.HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(content.getBytes(StandardCharsets.UTF_8))); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("JVM 缺少 SHA-256 算法", e); + } + } + + private void invalidateAfterCommit(String scriptId) { + if (!TransactionSynchronizationManager.isSynchronizationActive()) { + invalidate(scriptId); + return; + } + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + + @Override + public void afterCommit() { + invalidate(scriptId); + } + }); + } + + private void close(GroovyClassLoader classLoader) { + try { + classLoader.close(); + } catch (IOException e) { + log.warn("关闭 Groovy 脚本类加载器失败", e); + } + } + + private record CompiledScript(String scriptId, Class type, GroovyClassLoader classLoader) { + + private Object execute(Map bindings) { + try { + Script script = type.getDeclaredConstructor().newInstance(); + script.setBinding(new Binding(bindings == null ? Map.of() : new HashMap<>(bindings))); + return script.run(); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException("Groovy 脚本实例化失败", e); + } + } + + private void close() { + try { + classLoader.close(); + } catch (IOException ignored) { + } + } + } +} 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 667d914..bf1086d 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 @@ -65,16 +65,6 @@ 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) @@ -101,7 +91,6 @@ 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/GaoCustomerConsumptionRecord.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoCustomerConsumptionRecord.java new file mode 100644 index 0000000..74d3ff0 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoCustomerConsumptionRecord.java @@ -0,0 +1,44 @@ +package com.imyeyu.api.modules.gao.entity; + +import com.fasterxml.jackson.databind.JsonNode; +import com.imyeyu.spring.entity.UUIDEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/// GAO 客户有效消费事实 +/// +/// @author Codex +/// @since 2026-09-04 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoCustomerConsumptionRecord extends UUIDEntity { + + /// 消费来源类型 + public enum SourceType { + /// 迁移的旧消费事件 + LEGACY_EVENT, + /// 销售订单全额实收 + SALES_ORDER + } + + /// 消费事实状态 + public enum Status { + /// 有效并参与累计 + ACTIVE, + /// 已失效,不参与累计 + REVOKED + } + + private String storeId; + private String customerId; + private SourceType sourceType; + private String sourceId; + private String salesOrderId; + private GaoSalesOrder.OrderType orderType; + private Long amount; + private Long effectiveAt; + private Status status; + private Long revokedAt; + private String revokeReason; + private JsonNode sourcePayload; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEvent.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEvent.java index c48b270..f9c087b 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEvent.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoEvent.java @@ -25,7 +25,10 @@ public class GaoEvent extends UUIDEntity { GENERAL, /// 购买产品套餐 - PACKAGE_PURCHASE + PACKAGE_PURCHASE, + + /// 已退役的历史消费事件,仅保留归档和溯源 + LEGACY_CONSUMPTION } /// diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPointLedger.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPointLedger.java index f0e6826..97a385c 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPointLedger.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPointLedger.java @@ -17,13 +17,13 @@ public class GaoPointLedger extends UUIDEntity { /// 流水变化类型 public enum ChangeType { - /// 登记事件发放积分 + /// 规则发放积分 EARN, - /// 登记事件消费积分 + /// 规则扣减积分 CONSUME, - /// 删除登记记录撤销积分 + /// 撤销积分 REVOKE, /// 人工调整积分 @@ -40,6 +40,16 @@ public class GaoPointLedger extends UUIDEntity { DECREASE } + /// 直接触发来源类型 + public enum TriggerSourceType { + + /// 事件登记 + EVENT_RECORD, + + /// 订单首次全额实收 + SALES_ORDER_PAID + } + /// 客户积分账户 ID private String accountId; @@ -67,11 +77,20 @@ public class GaoPointLedger extends UUIDEntity { /// 命中的积分规则 ID private String ruleId; - /// 登记事件 ID - private String eventId; + /// 直接触发来源类型 + private TriggerSourceType triggerSourceType; - /// 登记记录 ID - private String eventRecordId; + /// 直接触发来源 ID + private String triggerSourceId; + + /// 关联销售订单 ID + private String salesOrderId; + + /// 实际执行 Groovy 脚本版本 ID,简单规则为空 + private String groovyScriptVersionId; + + /// 命中时的规则快照 + private JsonNode ruleSnapshot; /// 来源快照 private JsonNode sourcePayload; diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPointRule.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPointRule.java index a17a27c..eba636a 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPointRule.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoPointRule.java @@ -1,6 +1,5 @@ package com.imyeyu.api.modules.gao.entity; -import com.fasterxml.jackson.databind.JsonNode; import com.imyeyu.spring.entity.UUIDEntity; import lombok.Data; import lombok.EqualsAndHashCode; @@ -29,43 +28,47 @@ public class GaoPointRule extends UUIDEntity { DELETED } - /// 触发类型 - public enum TriggerType { + /// 规则模式 + public enum RuleMode { - /// 每登记一次触发一次 - EVERY_TIME, + /// 简单规则,固定积分值 + SIMPLE, - /// 客户首次登记该事件触发一次 - FIRST_TIME, - - /// 客户累计登记达到 N 次后每次触发 - AFTER_COUNT_EVERY_TIME, - - /// 使用登记接口提交的积分值触发 - SOURCE_VALUE, - - /// 按来源事件数值累计分档触发 - CUSTOMER_EVENT_VALUE_TIER + /// Groovy 脚本规则 + GROOVY } - /// 积分值模式 - public enum PointMode { + /// 直接触发来源类型 + public enum TriggerSourceType { - /// 使用规则固定积分值 - FIXED, + /// 事件登记 + EVENT_RECORD, - /// 使用登记接口提交的积分值 - SOURCE_VALUE, + /// 订单首次全额实收 + SALES_ORDER_PAID + } - /// 使用条件配置中的分档积分值 - TIER + /// 触发对象筛选类型 + public enum TriggerRefType { + + /// GAO 登记事件 + EVENT } /// 门店 ID private String storeId; - /// 登记事件 ID - private String eventId; + /// 规则模式 + private RuleMode ruleMode; + + /// 直接触发来源类型 + private TriggerSourceType triggerSourceType; + + /// 触发对象筛选类型 + private TriggerRefType triggerRefType; + + /// 触发对象筛选 ID + private String triggerRefId; /// 规则名称 private String name; @@ -73,17 +76,11 @@ public class GaoPointRule extends UUIDEntity { /// 规则状态 private Status status; - /// 触发类型 - private TriggerType triggerType; - - /// 积分值模式 - private PointMode pointMode; - - /// 固定积分值 + /// 固定积分值,简单规则使用 private Long pointValue; - /// 条件配置 - private JsonNode conditionJson; + /// Groovy 脚本 ID,GROOVY 模式使用 + private String groovyScriptId; /// 生效开始时间戳,毫秒,空为不限制 private Long beginAt; 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 index b3c43e9..6bb8783 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoSalesOrder.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoSalesOrder.java @@ -96,6 +96,9 @@ public class GaoSalesOrder extends UUIDEntity { /// 有效收款,单位分 private Long paidAmount; + /// 首次全额实收时间 + private Long paidAt; + /// 有效退款,单位分 private Long refundedAmount; 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 20ab456..07ea5f7 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,9 +54,6 @@ 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/GaoCustomerConsumptionRecordMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoCustomerConsumptionRecordMapper.java new file mode 100644 index 0000000..a9c7261 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoCustomerConsumptionRecordMapper.java @@ -0,0 +1,20 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoCustomerConsumptionRecord; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/// GAO 客户有效消费事实 Mapper +/// +/// @author Codex +/// @since 2026-09-04 +public interface GaoCustomerConsumptionRecordMapper extends BaseMapper { + + GaoCustomerConsumptionRecord selectBySource(@Param("sourceType") GaoCustomerConsumptionRecord.SourceType sourceType, @Param("sourceId") String sourceId); + + List selectActiveByWindow(@Param("storeId") String storeId, @Param("customerId") String customerId, @Param("beginAt") long beginAt, @Param("endAt") long endAt); + + int revokeBySource(@Param("sourceType") GaoCustomerConsumptionRecord.SourceType sourceType, @Param("sourceId") String sourceId, @Param("revokedAt") long revokedAt, @Param("revokeReason") String revokeReason); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventSQLProvider.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventSQLProvider.java index 6777a3e..ce14ce9 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventSQLProvider.java +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoEventSQLProvider.java @@ -26,7 +26,7 @@ public class GaoEventSQLProvider extends SQLProvider { private String appendWithoutDeleted(String sql) { return sql.replace( "WHERE 1 = 1", - "WHERE 1 = 1 AND (`status` IS NULL OR `status` <> '%s')".formatted(GaoEvent.Status.DELETED.name()) + "WHERE 1 = 1 AND (`status` IS NULL OR `status` <> '%s') AND (`biz_type` IS NULL OR `biz_type` <> '%s')".formatted(GaoEvent.Status.DELETED.name(), GaoEvent.BizType.LEGACY_CONSUMPTION.name()) ); } } diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPointLedgerConsumptionRecordMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPointLedgerConsumptionRecordMapper.java new file mode 100644 index 0000000..9e486ed --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPointLedgerConsumptionRecordMapper.java @@ -0,0 +1,38 @@ +package com.imyeyu.api.modules.gao.mapper; + +import org.apache.ibatis.annotations.Delete; +import org.apache.ibatis.annotations.Insert; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +/// GAO 积分流水消费事实依赖 Mapper +/// +/// @author Codex +/// @since 2026-09-04 +public interface GaoPointLedgerConsumptionRecordMapper { + + /// 写入积分流水对消费事实的依赖 + /// + /// @param pointLedgerId 积分流水 ID + /// @param consumptionRecordId 消费事实 ID + /// @param createdAt 创建时间 + /// @return 写入行数 + @Insert("INSERT IGNORE INTO `gao_point_ledger_consumption_record` (`point_ledger_id`, `consumption_record_id`, `created_at`) VALUES (#{pointLedgerId}, #{consumptionRecordId}, #{createdAt})") + int insert(@Param("pointLedgerId") String pointLedgerId, @Param("consumptionRecordId") String consumptionRecordId, @Param("createdAt") long createdAt); + + /// 查询积分流水依赖的消费事实 ID + /// + /// @param pointLedgerId 积分流水 ID + /// @return 消费事实 ID 列表 + @Select("SELECT `consumption_record_id` FROM `gao_point_ledger_consumption_record` WHERE `point_ledger_id` = #{pointLedgerId}") + List selectConsumptionRecordIdList(@Param("pointLedgerId") String pointLedgerId); + + /// 删除积分流水的依赖关系 + /// + /// @param pointLedgerId 积分流水 ID + /// @return 删除行数 + @Delete("DELETE FROM `gao_point_ledger_consumption_record` WHERE `point_ledger_id` = #{pointLedgerId}") + int deleteByPointLedgerId(@Param("pointLedgerId") String pointLedgerId); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPointLedgerMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPointLedgerMapper.java index 197349d..4a8e7c8 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPointLedgerMapper.java +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPointLedgerMapper.java @@ -19,11 +19,18 @@ public interface GaoPointLedgerMapper extends BaseMapper /// @return 流水 GaoPointLedger selectByIdempotencyKey(@Param("idempotencyKey") String idempotencyKey); - /// 查询登记记录产生的原始规则积分流水 + /// 查询触发来源产生的原始规则积分流水 /// - /// @param eventRecordId 登记记录 ID + /// @param triggerSourceType 直接触发来源类型 + /// @param triggerSourceId 直接触发来源 ID /// @return 原始规则积分流水 - List selectRuleChangeByEventRecordId(@Param("eventRecordId") String eventRecordId); + List selectRuleChangeByTriggerSource(@Param("triggerSourceType") GaoPointLedger.TriggerSourceType triggerSourceType, @Param("triggerSourceId") String triggerSourceId); + + /// 查询依赖指定消费事实的原始规则积分流水 + /// + /// @param consumptionRecordId 消费事实 ID + /// @return 原始规则积分流水 + List selectRuleChangeByConsumptionRecordId(@Param("consumptionRecordId") String consumptionRecordId); /// 批量查询幂等键对应的流水 /// diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPointRuleMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPointRuleMapper.java index 97a1fe1..af230c4 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPointRuleMapper.java +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoPointRuleMapper.java @@ -18,8 +18,9 @@ public interface GaoPointRuleMapper extends BaseMapper { /// 查询当前时间可匹配的启用规则 /// /// @param storeId 门店 ID - /// @param eventId 登记事件 ID + /// @param triggerSourceType 直接触发来源类型 + /// @param triggerRefId 触发对象筛选 ID,空为不限 /// @param now 当前时间戳 /// @return 按优先级排序的规则列表 - List selectActiveByStoreIdAndEventId(@Param("storeId") String storeId, @Param("eventId") String eventId, @Param("now") long now); + List selectActiveByTrigger(@Param("storeId") String storeId, @Param("triggerSourceType") GaoPointRule.TriggerSourceType triggerSourceType, @Param("triggerRefId") String triggerRefId, @Param("now") long now); } 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 index f80b86e..7829995 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoSalesOrderMapper.java +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoSalesOrderMapper.java @@ -27,7 +27,7 @@ public interface GaoSalesOrderMapper extends BaseMapper { /// 更新订单付款汇总 int updatePayment(@Param("id") String id, @Param("pendingPaidAmount") Long pendingPaidAmount, @Param("paidAmount") Long paidAmount, - @Param("refundedAmount") Long refundedAmount, @Param("paymentStatus") GaoSalesOrder.PaymentStatus paymentStatus, + @Param("paidAt") Long paidAt, @Param("refundedAmount") Long refundedAmount, @Param("paymentStatus") GaoSalesOrder.PaymentStatus paymentStatus, @Param("refundStatus") GaoSalesOrder.RefundStatus refundStatus); /// 带状态条件取消订单 @@ -35,9 +35,6 @@ public interface GaoSalesOrderMapper extends BaseMapper { @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/script/GaoPointDecision.java b/src/main/java/com/imyeyu/api/modules/gao/script/GaoPointDecision.java new file mode 100644 index 0000000..0465bfc --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/script/GaoPointDecision.java @@ -0,0 +1,17 @@ +package com.imyeyu.api.modules.gao.script; + +import java.util.List; +import java.util.Map; + +/// GAO 积分规则脚本决策 +/// +/// 脚本通过上下文构造本对象返回声明式积分决策,不能直接写积分账户或积分流水 +/// +/// @param pointDelta 积分变化,正为发放,负为扣减,不能为 0 +/// @param reason 业务原因 +/// @param conditionSnapshot 条件快照 +/// @param consumptionRecordIdList 依赖的消费事实 ID 列表 +/// @author Codex +/// @since 2026-09-04 +public record GaoPointDecision(long pointDelta, String reason, Map conditionSnapshot, List consumptionRecordIdList) { +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/script/GaoPointScriptContext.java b/src/main/java/com/imyeyu/api/modules/gao/script/GaoPointScriptContext.java new file mode 100644 index 0000000..cd98c2c --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/script/GaoPointScriptContext.java @@ -0,0 +1,154 @@ +package com.imyeyu.api.modules.gao.script; + +import com.imyeyu.api.modules.gao.entity.GaoCustomerConsumptionRecord; +import com.imyeyu.api.modules.gao.entity.GaoPointRule; +import com.imyeyu.api.modules.gao.service.GaoCustomerConsumptionRecordService; +import com.imyeyu.api.modules.gao.service.GaoEventRecordService; +import com.imyeyu.java.bean.timi.TimiException; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/// GAO 积分规则脚本执行上下文 +/// +/// 只向脚本提供归一化触发信息和只读消费事实查询,滚动窗口固定使用 Asia/Shanghai 时区 +/// +/// @author Codex +/// @since 2026-09-04 +public class GaoPointScriptContext { + + /// 滚动窗口固定时区 + public static final ZoneId ZONE = ZoneId.of("Asia/Shanghai"); + + private final GaoCustomerConsumptionRecordService consumptionService; + private final GaoEventRecordService eventRecordService; + private final GaoPointRule rule; + private final String storeId; + private final String customerId; + private final String triggerSourceType; + private final String triggerRefType; + private final String triggerRefId; + private final long occurredAt; + + /// 参与过金额计算的消费事实,ID 到金额摘要 + private final Map consumptionFactMap = new LinkedHashMap<>(); + + public GaoPointScriptContext(GaoCustomerConsumptionRecordService consumptionService, GaoEventRecordService eventRecordService, + GaoPointRule rule, String storeId, String customerId, String triggerRefType, String triggerRefId, long occurredAt) { + this.consumptionService = consumptionService; + this.eventRecordService = eventRecordService; + this.rule = rule; + this.storeId = storeId; + this.customerId = customerId; + this.triggerSourceType = rule.getTriggerSourceType() == null ? null : rule.getTriggerSourceType().name(); + this.triggerRefType = triggerRefType; + this.triggerRefId = triggerRefId; + this.occurredAt = occurredAt; + } + + /// 触发来源门店 ID + public String getStoreId() { + return storeId; + } + + /// 触发来源客户 ID + public String getCustomerId() { + return customerId; + } + + /// 直接触发来源类型 + public String getTriggerSourceType() { + return triggerSourceType; + } + + /// 触发对象筛选类型 + public String getTriggerRefType() { + return triggerRefType; + } + + /// 触发对象筛选 ID + public String getTriggerRefId() { + return triggerRefId; + } + + /// 触发发生时间,毫秒 + public long getOccurredAt() { + return occurredAt; + } + + /// 查询触发时间往前滚动一年的有效消费金额,窗口按 Asia/Shanghai 自然日对齐 + /// + /// @return 窗口内 ACTIVE 消费事实金额合计,单位分 + public long rollingYearConsumptionAmount() { + LocalDate currentDate = Instant.ofEpochMilli(occurredAt).atZone(ZONE).toLocalDate(); + long beginAt = currentDate.minusYears(1).atStartOfDay(ZONE).toInstant().toEpochMilli(); + return consumptionAmountBetween(beginAt, occurredAt); + } + + /// 查询指定时间窗口内的有效消费金额 + /// + /// 查询上限不会晚于触发时间,保证历史复算不受后续新增消费影响 + /// + /// @param beginAt 窗口起始,包含 + /// @param endAt 窗口结束,包含 + /// @return 窗口内 ACTIVE 消费事实金额合计,单位分 + public long consumptionAmountBetween(long beginAt, long endAt) { + long cappedEndAt = Math.min(endAt, occurredAt); + long total = 0L; + for (GaoCustomerConsumptionRecord record : consumptionService.listActiveByWindow(storeId, customerId, beginAt, cappedEndAt)) { + consumptionFactMap.putIfAbsent(record.getId(), record.getAmount()); + total = Math.addExact(total, record.getAmount()); + } + return total; + } + + /// 查询客户在触发对象事件上截至触发时间的登记次数 + /// + /// @return 登记次数 + public long eventCount() { + return eventRecordService.count(storeId, customerId, triggerRefId, null, occurredAt); + } + + /// 构造积分决策 + /// + /// @param pointDelta 积分变化,不能为 0 + /// @param reason 业务原因 + /// @param conditionSnapshot 条件快照 + /// @return 积分决策 + public GaoPointDecision decision(long pointDelta, String reason, Map conditionSnapshot) { + return decision(pointDelta, reason, conditionSnapshot, null); + } + + /// 构造积分决策并显式追加依赖的消费事实 + /// + /// @param pointDelta 积分变化,不能为 0 + /// @param reason 业务原因 + /// @param conditionSnapshot 条件快照 + /// @param consumptionRecordIdList 额外依赖的消费事实 ID 列表 + /// @return 积分决策 + public GaoPointDecision decision(long pointDelta, String reason, Map conditionSnapshot, List consumptionRecordIdList) { + TimiException.requiredTrue(pointDelta != 0, "脚本积分变化不能为 0"); + Map snapshot = conditionSnapshot == null ? new LinkedHashMap<>() : new LinkedHashMap<>(conditionSnapshot); + List> consumptionSummary = new ArrayList<>(); + for (Map.Entry entry : consumptionFactMap.entrySet()) { + Map item = new LinkedHashMap<>(); + item.put("id", entry.getKey()); + item.put("amount", entry.getValue()); + consumptionSummary.add(item); + } + snapshot.put("consumptionRecords", consumptionSummary); + snapshot.put("consumptionTotal", consumptionFactMap.values().stream().mapToLong(Long::longValue).sum()); + List deps = new ArrayList<>(consumptionFactMap.keySet()); + for (String id : consumptionRecordIdList == null ? List.of() : consumptionRecordIdList) { + if (id != null && !deps.contains(id)) { + deps.add(id); + } + } + return new GaoPointDecision(pointDelta, reason, snapshot, deps); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoCustomerConsumptionRecordService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoCustomerConsumptionRecordService.java new file mode 100644 index 0000000..8ec9e2e --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoCustomerConsumptionRecordService.java @@ -0,0 +1,54 @@ +package com.imyeyu.api.modules.gao.service; + +import com.imyeyu.api.modules.gao.entity.GaoCustomerConsumptionRecord; +import com.imyeyu.spring.service.BaseService; + +import java.util.List; + +/// GAO 客户有效消费事实服务 +/// +/// @author Codex +/// @since 2026-09-04 +public interface GaoCustomerConsumptionRecordService extends BaseService { + + /// 写入销售订单消费事实 + /// + /// 幂等,重复执行返回已有事实 + /// + /// @param salesOrderId 销售订单 ID + /// @param storeId 门店 ID + /// @param customerId 客户 ID + /// @param amount 有效消费金额,单位分,必须大于 0 + /// @param effectiveAt 全额实收时间 + /// @param orderType 订单类型 + /// @return 消费事实 + GaoCustomerConsumptionRecord createSalesOrder(String salesOrderId, String storeId, String customerId, Long amount, Long effectiveAt, com.imyeyu.api.modules.gao.entity.GaoSalesOrder.OrderType orderType); + + /// 撤销销售订单消费事实 + /// + /// @param salesOrderId 销售订单 ID + /// @param reason 失效原因 + void revokeSalesOrder(String salesOrderId, String reason); + + /// 撤销历史事件迁移出的消费事实 + /// + /// @param eventRecordId 原事件记录 ID + /// @param reason 失效原因 + void revokeLegacyEvent(String eventRecordId, String reason); + + /// 按来源查询消费事实 + /// + /// @param sourceType 来源类型 + /// @param sourceId 来源业务对象 ID + /// @return 消费事实,不存在时返回 null + GaoCustomerConsumptionRecord getBySource(GaoCustomerConsumptionRecord.SourceType sourceType, String sourceId); + + /// 查询时间窗口内的 ACTIVE 消费事实 + /// + /// @param storeId 门店 ID + /// @param customerId 客户 ID + /// @param beginAt 起始时间,包含 + /// @param endAt 结束时间,包含 + /// @return 消费事实列表 + List listActiveByWindow(String storeId, String customerId, long beginAt, long endAt); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventRecordService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventRecordService.java index f832fc4..a798883 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventRecordService.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoEventRecordService.java @@ -104,23 +104,6 @@ public interface GaoEventRecordService extends BaseService { - /// 写入登记规则积分流水 + /// 写入规则发放或扣减积分流水 + /// + /// 幂等,重复触发返回已有流水;积分流水只追加,不修改不删除 /// - /// @param record 登记记录 /// @param rule 命中规则 - /// @param pointDelta 积分变化 - /// @param sourcePayload 来源快照 + /// @param decision 积分决策 + /// @param execution Groovy 脚本执行结果,简单规则为空 + /// @param triggerSourceType 直接触发来源类型 + /// @param triggerSourceId 直接触发来源 ID + /// @param salesOrderId 关联销售订单 ID,可空 + /// @param storeId 门店 ID + /// @param customerId 客户 ID + /// @param occurredAt 发生时间 + /// @param operatorUserId 操作用户 ID + /// @param consumptionRecordIdList 依赖的消费事实 ID 列表 /// @return 积分流水 - GaoPointLedger applyRule(GaoEventRecord record, GaoPointRule rule, long pointDelta, JsonNode sourcePayload); + GaoPointLedger applyRule(GaoPointRule rule, GaoPointDecision decision, GroovyScriptExecutionResult execution, + GaoPointLedger.TriggerSourceType triggerSourceType, String triggerSourceId, String salesOrderId, + String storeId, String customerId, Long occurredAt, String operatorUserId, List consumptionRecordIdList); - /// 写入删除登记记录的撤销流水 + /// 追加撤销流水 + /// + /// 撤销流水以原流水为基准反向冲抵,幂等键按原流水设计 /// /// @param originLedger 原始规则积分流水 - /// @param record 登记记录 + /// @param reason 撤销原因 /// @return 撤销流水 - GaoPointLedger revoke(GaoPointLedger originLedger, GaoEventRecord record); + GaoPointLedger revoke(GaoPointLedger originLedger, String reason); /// 写入人工调整流水 /// @@ -40,11 +53,18 @@ public interface GaoPointLedgerService extends BaseService listRuleChangeByEventRecordId(String eventRecordId); + List listRuleChangeByTriggerSource(GaoPointLedger.TriggerSourceType triggerSourceType, String triggerSourceId); + + /// 查询依赖指定消费事实的原始规则积分流水 + /// + /// @param consumptionRecordId 消费事实 ID + /// @return 原始积分流水 + List listRuleChangeByConsumptionRecordId(String consumptionRecordId); /// 按幂等键批量查询积分流水 /// diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoPointRuleService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoPointRuleService.java index 9bae0b8..8a5d24d 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/GaoPointRuleService.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoPointRuleService.java @@ -13,11 +13,14 @@ public interface GaoPointRuleService extends BaseService { /// 查询当前时间可匹配的启用规则 /// + /// 触发对象筛选 ID 为空时匹配未设置触发对象引用的规则 + /// /// @param storeId 门店 ID - /// @param eventId 登记事件 ID + /// @param triggerSourceType 直接触发来源类型 + /// @param triggerRefId 触发对象筛选 ID,可空 /// @param now 当前时间戳 - /// @return 规则列表 - List listActive(String storeId, String eventId, long now); + /// @return 按优先级排序的规则列表 + List listActive(String storeId, GaoPointRule.TriggerSourceType triggerSourceType, String triggerRefId, long now); /// 查询规则 ID 列表 /// diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoPointTriggerService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoPointTriggerService.java index b2e47db..0b7fea9 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/GaoPointTriggerService.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoPointTriggerService.java @@ -1,7 +1,8 @@ package com.imyeyu.api.modules.gao.service; -import com.imyeyu.api.modules.gao.entity.GaoPointLedger; import com.imyeyu.api.modules.gao.entity.GaoEventRecord; +import com.imyeyu.api.modules.gao.entity.GaoPointLedger; +import com.imyeyu.api.modules.gao.entity.GaoSalesOrder; import com.imyeyu.api.modules.gao.vo.GaoPointAdjustRequest; /// GAO 客户积分触发服务 @@ -15,11 +16,22 @@ public interface GaoPointTriggerService { /// @param record 登记记录 void onEventRecordCreated(GaoEventRecord record); - /// 撤销登记记录产生的全部获得积分 + /// 撤销登记记录触发的全部获得积分,并撤销迁移消费事实及其依赖积分 /// /// @param record 登记记录 void revokeByEventRecord(GaoEventRecord record); + /// 在订单首次全额实收时写入消费事实并触发订单积分 + /// + /// @param order 销售订单 + void onSalesOrderPaid(GaoSalesOrder order); + + /// 订单取消或全额实收被冲正时撤销消费事实及其依赖积分 + /// + /// @param order 销售订单 + /// @param reason 撤销原因 + void revokeBySalesOrder(GaoSalesOrder order, String reason); + /// 执行人工调整 /// /// @param request 人工调整请求 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 0c35476..b62d724 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,6 +1,5 @@ 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; @@ -15,18 +14,6 @@ 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/GaoCustomerConsumptionRecordServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerConsumptionRecordServiceImplement.java new file mode 100644 index 0000000..73f9917 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoCustomerConsumptionRecordServiceImplement.java @@ -0,0 +1,80 @@ +package com.imyeyu.api.modules.gao.service.implement; + +import com.imyeyu.api.config.dbsource.TimiServerDBConfig; +import com.imyeyu.api.modules.gao.entity.GaoCustomerConsumptionRecord; +import com.imyeyu.api.modules.gao.entity.GaoSalesOrder; +import com.imyeyu.api.modules.gao.mapper.GaoCustomerConsumptionRecordMapper; +import com.imyeyu.api.modules.gao.service.GaoCustomerConsumptionRecordService; +import com.imyeyu.java.bean.timi.TimiException; +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.List; + +/// GAO 客户有效消费事实服务实现 +/// +/// @author Codex +/// @since 2026-09-04 +@Service +@RequiredArgsConstructor +public class GaoCustomerConsumptionRecordServiceImplement extends AbstractEntityService implements GaoCustomerConsumptionRecordService { + + private final GaoCustomerConsumptionRecordMapper mapper; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoCustomerConsumptionRecord createSalesOrder(String salesOrderId, String storeId, String customerId, Long amount, Long effectiveAt, GaoSalesOrder.OrderType orderType) { + TimiException.requiredTrue(amount != null && 0 < amount, "有效消费金额必须大于 0"); + GaoCustomerConsumptionRecord existing = mapper.selectBySource(GaoCustomerConsumptionRecord.SourceType.SALES_ORDER, salesOrderId); + if (existing != null) { + return existing; + } + GaoCustomerConsumptionRecord record = new GaoCustomerConsumptionRecord(); + record.setStoreId(storeId); + record.setCustomerId(customerId); + record.setSourceType(GaoCustomerConsumptionRecord.SourceType.SALES_ORDER); + record.setSourceId(salesOrderId); + record.setSalesOrderId(salesOrderId); + record.setOrderType(orderType); + record.setAmount(amount); + record.setEffectiveAt(effectiveAt); + record.setStatus(GaoCustomerConsumptionRecord.Status.ACTIVE); + super.create(record); + return record; + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void revokeSalesOrder(String salesOrderId, String reason) { + mapper.revokeBySource(GaoCustomerConsumptionRecord.SourceType.SALES_ORDER, salesOrderId, Time.now(), reason); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void revokeLegacyEvent(String eventRecordId, String reason) { + mapper.revokeBySource(GaoCustomerConsumptionRecord.SourceType.LEGACY_EVENT, eventRecordId, Time.now(), reason); + } + + @Override + public GaoCustomerConsumptionRecord getBySource(GaoCustomerConsumptionRecord.SourceType sourceType, String sourceId) { + TimiException.required(sourceType, "not found sourceType"); + TimiException.required(sourceId, "not found sourceId"); + return mapper.selectBySource(sourceType, sourceId); + } + + @Override + public List listActiveByWindow(String storeId, String customerId, long beginAt, long endAt) { + TimiException.required(storeId, "not found storeId"); + TimiException.required(customerId, "not found customerId"); + return mapper.selectActiveByWindow(storeId, customerId, beginAt, endAt); + } +} 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 71eb264..c2aa17a 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 @@ -2,11 +2,11 @@ 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.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.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; @@ -15,8 +15,6 @@ 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.GaoPackageConfigService; import com.imyeyu.api.modules.gao.service.GaoProductService; import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; @@ -64,10 +62,9 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService configItemList = packageConfigService.listItemByPackageConfigId(config.getId()); request.setSaleAmount(TimiJava.defaultIfNull(request.getSaleAmount(), config.getStandardAmount())); TimiException.requiredTrue(0 <= request.getSaleAmount(), "成交价不能为负数"); @@ -209,16 +204,6 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService defaultPurchaseItems(List configItemList) { return configItemList.stream().map(configItem -> { GaoCustomerPackagePurchaseItem item = new GaoCustomerPackagePurchaseItem(); @@ -350,11 +322,11 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService itemList, List configItemList) { - TimiException.requiredTrue(itemList.size() == configItemList.size(), "套餐权益项目必须与事件绑定套餐一致"); + TimiException.requiredTrue(itemList.size() == configItemList.size(), "套餐权益项目必须与套餐配置一致"); Map itemMap = itemList.stream().collect(Collectors.toMap(item -> item.getProductId() + "|" + item.getProductUnitId(), Function.identity())); for (GaoPackageConfigItem configItem : configItemList) { GaoCustomerPackagePurchaseItem item = itemMap.get(configItem.getProductId() + "|" + configItem.getProductUnitId()); - TimiException.required(item, "套餐权益项目必须与事件绑定套餐一致"); + TimiException.required(item, "套餐权益项目必须与套餐配置一致"); item.setProductNameSnapshot(configItem.getProductNameSnapshot()); item.setUnitSnapshot(configItem.getUnitSnapshot()); item.setBaseQuantityPerUnitSnapshot(configItem.getBaseQuantityPerUnitSnapshot()); @@ -376,9 +348,7 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService existingItemList = itemMapper.selectByCustomerPackageId(existingPackage.getId()); diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoEventRecordServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoEventRecordServiceImplement.java index 84670d6..f90b65c 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoEventRecordServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoEventRecordServiceImplement.java @@ -214,30 +214,13 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService canonicalOptionList = resolveRecordOptionList(event, record.getSelectedOptionGroupList()); verifyRecordValue(event, record.getNumberValue()); verifyRecordAttachment(event, record.getAttachmentList()); // 检查权限 - if (!orderConsume) { - checkEventPermission(event); - } + checkEventPermission(event); int registeredDateValue = dateValue(registeredAt); long businessDayNo = businessDayService.resolveOpenBusinessDay(event.getStoreId(), registeredDateValue).getBusinessDayNo(); @@ -383,6 +360,7 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService canonicalOptionList = null; if (record.getSelectedOptionGroupList() != null) { @@ -428,6 +406,7 @@ public class GaoEventRecordServiceImplement extends AbstractEntityService { 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; + TimiException.requiredTrue(record.getOperationType() == GaoFundRecord.OperationType.RECEIVE && record.getStatus() == GaoFundRecord.Status.PENDING, "收款记录不是待核销状态"); + GaoSalesOrder order = orderService.getForUpdate(record.getBizId()); + Long paidAtBefore = order.getPaidAt(); + 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()); + if (paidAtBefore == null && order.getPaidAt() != null) { + // 订单首次全额实收,写入消费事实并触发订单积分 + pointTriggerService.onSalesOrderPaid(order); + } + return record; }); } @@ -209,6 +216,7 @@ public class GaoOrderPaymentServiceImplement implements GaoOrderPaymentService { } TimiException.requiredTrue(source.getStatus() == GaoFundRecord.Status.VERIFIED && source.getOperationType() != GaoFundRecord.OperationType.REVOKE, "只有已生效资金记录可以冲正"); TimiException.requiredNull(fundRecordService.getRevokeBySourceRecordId(source.getId()), "该资金记录已经冲正"); + Long paidAtBefore = order.getPaidAt(); if (source.getOperationType() == GaoFundRecord.OperationType.RECEIVE) { order.setPaidAmount(safe(order.getPaidAmount()) - source.getAmount()); orderService.recomputePaymentStatus(order); @@ -224,6 +232,10 @@ public class GaoOrderPaymentServiceImplement implements GaoOrderPaymentService { revoke.setReason(reason); fundRecordService.createByOrderPayment(revoke); orderService.updatePayment(order); + if (paidAtBefore != null && order.getPaymentStatus() != GaoSalesOrder.PaymentStatus.PAID) { + // 已全额实收的收款被冲正,撤销消费事实及其依赖积分 + pointTriggerService.revokeBySalesOrder(order, reason); + } TimiException.requiredTrue(hasPhoto(request.getAttachmentList()), "资金冲正必须上传照片"); attachmentService.updateByBizId(Attachment.BizType.GAO_FUND_RECORD, revoke.getId(), request.getAttachmentList()); return revoke; 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 2ba9ff4..06c77d0 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 @@ -1,15 +1,19 @@ package com.imyeyu.api.modules.gao.service.implement; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.imyeyu.api.config.dbsource.TimiServerDBConfig; +import com.imyeyu.api.modules.common.bean.GroovyScriptExecutionResult; 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.entity.GaoPointLedger; import com.imyeyu.api.modules.gao.entity.GaoPointRule; -import com.imyeyu.api.modules.gao.entity.GaoEventRecord; +import com.imyeyu.api.modules.gao.mapper.GaoPointLedgerConsumptionRecordMapper; import com.imyeyu.api.modules.gao.mapper.GaoPointLedgerMapper; +import com.imyeyu.api.modules.gao.script.GaoPointDecision; import com.imyeyu.api.modules.gao.service.GaoCustomerService; import com.imyeyu.api.modules.gao.service.GaoPointAccountService; import com.imyeyu.api.modules.gao.service.GaoPointLedgerService; @@ -42,7 +46,9 @@ import java.util.Map; @RequiredArgsConstructor public class GaoPointLedgerServiceImplement extends AbstractEntityService implements GaoPointLedgerService { + private final ObjectMapper jackson; private final GaoPointLedgerMapper mapper; + private final GaoPointLedgerConsumptionRecordMapper consumptionRecordMapper; private final GaoPointAccountService accountService; private final GaoCustomerService customerService; private final AttachmentService attachmentService; @@ -71,23 +77,35 @@ public class GaoPointLedgerServiceImplement extends AbstractEntityService execution, + GaoPointLedger.TriggerSourceType triggerSourceType, String triggerSourceId, String salesOrderId, + String storeId, String customerId, Long occurredAt, String operatorUserId, List consumptionRecordIdList) { TimiException.required(rule, "not found point rule"); - TimiException.requiredTrue(pointDelta != 0, "pointDelta invalid"); - String idempotencyKey = "GAO_EVENT_RECORD:%s:RULE:%s:EARN".formatted(record.getId(), rule.getId()); + TimiException.required(decision, "not found point decision"); + TimiException.required(triggerSourceType, "not found triggerSourceType"); + TimiException.required(triggerSourceId, "not found triggerSourceId"); + TimiException.requiredTrue(decision.pointDelta() != 0, "pointDelta invalid"); + long pointDelta = decision.pointDelta(); + String idempotencyKey = earnKey(triggerSourceType, triggerSourceId, rule.getId()); GaoPointLedger.ChangeType changeType = pointDelta < 0 ? GaoPointLedger.ChangeType.CONSUME : GaoPointLedger.ChangeType.EARN; - return apply(record.getStoreId(), record.getCustomerId(), changeType, pointDelta, rule.getId(), record.getEventId(), record.getId(), sourcePayload, idempotencyKey, rule.getRemark(), record.getOperatorUserId(), record.getRegisteredAt()); + GaoPointLedger ledger = apply(storeId, customerId, changeType, pointDelta, rule.getId(), triggerSourceType, triggerSourceId, salesOrderId, + ruleSnapshot(rule, execution), sourcePayload(decision, execution), idempotencyKey, + TimiJava.defaultIfNull(decision.reason(), rule.getName()), operatorUserId, occurredAt); + saveConsumptionDependencies(ledger.getId(), consumptionRecordIdList); + return ledger; } @Transactional(TimiServerDBConfig.ROLLBACKER) @Override - public GaoPointLedger revoke(GaoPointLedger originLedger, GaoEventRecord record) { + public GaoPointLedger revoke(GaoPointLedger originLedger, String reason) { TimiException.required(originLedger, "not found origin ledger"); - TimiException.required(record, "not found event record"); - TimiException.requiredTrue(originLedger.getPointDelta() != 0, "origin ledger pointDelta invalid"); - String idempotencyKey = "GAO_EVENT_RECORD:%s:LEDGER:%s:REVOKE".formatted(record.getId(), originLedger.getId()); - return apply(originLedger.getStoreId(), originLedger.getCustomerId(), GaoPointLedger.ChangeType.REVOKE, -originLedger.getPointDelta(), originLedger.getRuleId(), originLedger.getEventId(), originLedger.getEventRecordId(), originLedger.getSourcePayload(), idempotencyKey, "撤销登记记录积分", userLoginService.getRequireLoginUserId(), Time.now()); + TimiException.required(originLedger.getId(), "not found origin ledger.id"); + TimiException.requiredTrue(originLedger.getPointDelta() != null && originLedger.getPointDelta() != 0, "origin ledger pointDelta invalid"); + String idempotencyKey = "GAO_POINT_LEDGER:%s:REVOKE".formatted(originLedger.getId()); + return apply(originLedger.getStoreId(), originLedger.getCustomerId(), GaoPointLedger.ChangeType.REVOKE, -originLedger.getPointDelta(), + originLedger.getRuleId(), originLedger.getTriggerSourceType(), originLedger.getTriggerSourceId(), originLedger.getSalesOrderId(), + originLedger.getRuleSnapshot(), originLedger.getSourcePayload(), idempotencyKey, reason, + userLoginService.getRequireLoginUserId(), Time.now()); } @Transactional(TimiServerDBConfig.ROLLBACKER) @@ -102,13 +120,21 @@ public class GaoPointLedgerServiceImplement extends AbstractEntityService listRuleChangeByEventRecordId(String eventRecordId) { - TimiException.required(eventRecordId, "not found eventRecordId"); - return mapper.selectRuleChangeByEventRecordId(eventRecordId); + public List listRuleChangeByTriggerSource(GaoPointLedger.TriggerSourceType triggerSourceType, String triggerSourceId) { + TimiException.required(triggerSourceType, "not found triggerSourceType"); + TimiException.required(triggerSourceId, "not found triggerSourceId"); + return mapper.selectRuleChangeByTriggerSource(triggerSourceType, triggerSourceId); + } + + @Override + public List listRuleChangeByConsumptionRecordId(String consumptionRecordId) { + TimiException.required(consumptionRecordId, "not found consumptionRecordId"); + return mapper.selectRuleChangeByConsumptionRecordId(consumptionRecordId); } @Override @@ -145,7 +171,62 @@ public class GaoPointLedgerServiceImplement extends AbstractEntityService consumptionRecordIdList) { + if (TimiJava.isEmpty(consumptionRecordIdList)) { + return; + } + long now = Time.now(); + for (String consumptionRecordId : consumptionRecordIdList) { + TimiException.required(consumptionRecordId, "not found consumptionRecordId"); + consumptionRecordMapper.insert(pointLedgerId, consumptionRecordId, now); + } + } + + private String earnKey(GaoPointLedger.TriggerSourceType triggerSourceType, String triggerSourceId, String ruleId) { + return switch (triggerSourceType) { + case EVENT_RECORD -> "GAO_EVENT_RECORD:%s:RULE:%s:EARN".formatted(triggerSourceId, ruleId); + case SALES_ORDER_PAID -> "GAO_SALES_ORDER:%s:RULE:%s:EARN".formatted(triggerSourceId, ruleId); + }; + } + + private JsonNode ruleSnapshot(GaoPointRule rule, GroovyScriptExecutionResult execution) { + ObjectNode snapshot = jackson.createObjectNode(); + snapshot.put("ruleId", rule.getId()); + snapshot.put("ruleName", rule.getName()); + if (rule.getRuleMode() != null) { + snapshot.put("ruleMode", rule.getRuleMode().name()); + } + if (rule.getPointValue() != null) { + snapshot.put("pointValue", rule.getPointValue()); + } + if (rule.getGroovyScriptId() != null) { + snapshot.put("groovyScriptId", rule.getGroovyScriptId()); + } + if (execution != null) { + snapshot.put("groovyScriptVersionId", execution.versionId()); + snapshot.put("groovyScriptVersion", execution.version()); + snapshot.put("groovyScriptChecksum", execution.checksum()); + } + return snapshot; + } + + private JsonNode sourcePayload(GaoPointDecision decision, GroovyScriptExecutionResult execution) { + ObjectNode payload = jackson.createObjectNode(); + payload.put("reason", decision.reason()); + payload.set("consumptionRecordIdList", jackson.valueToTree(decision.consumptionRecordIdList())); + if (decision.conditionSnapshot() != null) { + payload.set("conditionSnapshot", jackson.valueToTree(decision.conditionSnapshot())); + } + if (execution != null) { + payload.put("groovyScriptId", execution.scriptId()); + payload.put("groovyScriptVersionId", execution.versionId()); + } + return payload; + } + + private GaoPointLedger apply(String storeId, String customerId, GaoPointLedger.ChangeType changeType, long pointDelta, String ruleId, + GaoPointLedger.TriggerSourceType triggerSourceType, String triggerSourceId, String salesOrderId, JsonNode ruleSnapshot, + JsonNode sourcePayload, String idempotencyKey, String remark, String operatorUserId, Long occurredAt) { Logger logger = new Logger(Logger.Module.GAO, "GAO_POINT_LEDGER_APPLY"); logger.setContent("storeId=%s, customerId=%s, changeType=%s, pointDelta=%d, idempotencyKey=%s, remark=%s".formatted(storeId, customerId, changeType, pointDelta, idempotencyKey, remark)); try { @@ -181,8 +262,11 @@ public class GaoPointLedgerServiceImplement extends AbstractEntityService implements GaoPointRuleService { - private static final int MAX_TIER_COUNT = 100; - private final GaoPointRuleMapper mapper; private final GaoEventService eventService; + private final GroovyScriptService groovyScriptService; @Override protected BaseMapper mapper() { @@ -41,10 +41,10 @@ public class GaoPointRuleServiceImplement extends AbstractEntityService listActive(String storeId, String eventId, long now) { + public List listActive(String storeId, GaoPointRule.TriggerSourceType triggerSourceType, String triggerRefId, long now) { TimiException.required(storeId, "not found storeId"); - TimiException.required(eventId, "not found eventId"); - return mapper.selectActiveByStoreIdAndEventId(storeId, eventId, now); + TimiException.required(triggerSourceType, "not found triggerSourceType"); + return mapper.selectActiveByTrigger(storeId, triggerSourceType, triggerRefId, now); } @Override @@ -89,9 +89,7 @@ public class GaoPointRuleServiceImplement extends AbstractEntityService eventRecordServiceProvider; @@ -59,22 +63,16 @@ public class GaoPointTriggerServiceImplement implements GaoPointTriggerService { TimiException.required(record.getEventId(), "not found record.eventId"); TimiException.required(record.getRegisteredAt(), "not found record.registeredAt"); - List ruleList = ruleService.listActive(record.getStoreId(), record.getEventId(), Time.now()); - Long count = null; long pointDeltaTotal = 0L; - for (GaoPointRule rule : ruleList) { - RuleResolution resolution = resolveRule(record, rule, count); - if (resolution == null) { + for (GaoPointRule rule : ruleService.listActive(record.getStoreId(), GaoPointRule.TriggerSourceType.EVENT_RECORD, record.getEventId(), record.getRegisteredAt())) { + GaoPointScriptContext context = eventContext(rule, record); + GroovyScriptExecutionResult execution = evaluate(rule, record.getStoreId(), record.getCustomerId(), record.getRegisteredAt(), + GaoPointRule.TriggerRefType.EVENT.name(), record.getEventId()); + if (execution == null) { continue; } - if (rule.getTriggerType() == GaoPointRule.TriggerType.FIRST_TIME || rule.getTriggerType() == GaoPointRule.TriggerType.AFTER_COUNT_EVERY_TIME) { - count = count == null ? count(record) : count; - } - long pointDelta = resolution.pointDelta(); - if (pointDelta != 0) { - ledgerService.applyRule(record, rule, pointDelta, resolution.sourcePayload()); - pointDeltaTotal = Math.addExact(pointDeltaTotal, pointDelta); - } + pointDeltaTotal += applyDecision(rule, execution, GaoPointLedger.TriggerSourceType.EVENT_RECORD, record.getId(), null, + record.getStoreId(), record.getCustomerId(), record.getRegisteredAt(), record.getOperatorUserId(), context); if (Boolean.TRUE.equals(rule.getExclusive())) { break; } @@ -87,19 +85,60 @@ public class GaoPointTriggerServiceImplement implements GaoPointTriggerService { @Override public void revokeByEventRecord(GaoEventRecord record) { TimiException.required(record, "not found event record"); - List originLedgerList = ledgerService.listRuleChangeByEventRecordId(record.getId()); - if (originLedgerList.isEmpty()) { - return; + TimiException.required(record.getId(), "not found event record.id"); + revokeByTriggerSource(GaoPointLedger.TriggerSourceType.EVENT_RECORD, record.getId(), "撤销登记记录积分"); + GaoCustomerConsumptionRecord fact = consumptionService.getBySource(GaoCustomerConsumptionRecord.SourceType.LEGACY_EVENT, record.getId()); + if (fact != null) { + consumptionService.revokeLegacyEvent(record.getId(), "撤销历史消费事件对应的消费事实"); + revokeDependentLedgers(fact.getId()); } - Set revokedKeySet = new HashSet<>(); - List revokeKeyList = originLedgerList.stream().map(origin -> revokeKey(record, origin)).toList(); - for (GaoPointLedger ledger : ledgerService.listByIdempotencyKeyList(revokeKeyList)) { - revokedKeySet.add(ledger.getIdempotencyKey()); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void onSalesOrderPaid(GaoSalesOrder order) { + TimiException.required(order, "not found sales order"); + TimiException.required(order.getId(), "not found sales order.id"); + TimiException.required(order.getStoreId(), "not found order.storeId"); + TimiException.required(order.getCustomerId(), "not found order.customerId"); + TimiException.requiredTrue(order.getPaymentStatus() == GaoSalesOrder.PaymentStatus.PAID, "订单尚未全额实收"); + TimiException.required(order.getPaidAt(), "not found order.paidAt"); + if (order.getSaleAmount() != null && 0 < order.getSaleAmount()) { + consumptionService.createSalesOrder(order.getId(), order.getStoreId(), order.getCustomerId(), order.getSaleAmount(), order.getPaidAt(), order.getOrderType()); } - for (GaoPointLedger originLedger : originLedgerList) { - if (!revokedKeySet.contains(revokeKey(record, originLedger))) { - ledgerService.revoke(originLedger, record); + for (GaoPointRule rule : ruleService.listActive(order.getStoreId(), GaoPointRule.TriggerSourceType.SALES_ORDER_PAID, null, order.getPaidAt())) { + GaoPointScriptContext context = orderContext(rule, order); + GroovyScriptExecutionResult execution = evaluate(rule, order.getStoreId(), order.getCustomerId(), order.getPaidAt(), null, null); + if (execution == null) { + continue; } + applyDecision(rule, execution, GaoPointLedger.TriggerSourceType.SALES_ORDER_PAID, order.getId(), order.getId(), + order.getStoreId(), order.getCustomerId(), order.getPaidAt(), order.getOperatorUserId(), context); + if (Boolean.TRUE.equals(rule.getExclusive())) { + break; + } + } + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void revokeBySalesOrder(GaoSalesOrder order, String reason) { + TimiException.required(order, "not found sales order"); + TimiException.required(order.getId(), "not found sales order.id"); + TimiException.required(reason, "not found revoke reason"); + revokeByTriggerSource(GaoPointLedger.TriggerSourceType.SALES_ORDER_PAID, order.getId(), reason); + GaoCustomerConsumptionRecord fact = consumptionService.getBySource(GaoCustomerConsumptionRecord.SourceType.SALES_ORDER, order.getId()); + if (fact != null) { + consumptionService.revokeSalesOrder(order.getId(), reason); + } else if (TimiJava.isNotEmpty(order.getSourceEventRecordId())) { + // 切换前的旧订单通过原事件记录映射定位迁移出的消费事实 + fact = consumptionService.getBySource(GaoCustomerConsumptionRecord.SourceType.LEGACY_EVENT, order.getSourceEventRecordId()); + if (fact != null) { + consumptionService.revokeLegacyEvent(order.getSourceEventRecordId(), reason); + } + } + if (fact != null) { + revokeDependentLedgers(fact.getId()); } } @@ -114,94 +153,139 @@ public class GaoPointTriggerServiceImplement implements GaoPointTriggerService { return ledgerService.adjust(request, userLoginService.getRequireLoginUserId()); } - private RuleResolution resolveRule(GaoEventRecord record, GaoPointRule rule, Long count) { - return switch (rule.getTriggerType()) { - case EVERY_TIME -> new RuleResolution(rule.getPointValue(), null); - case FIRST_TIME -> { - long currentCount = count == null ? count(record) : count; - yield currentCount == 1 ? new RuleResolution(rule.getPointValue(), null) : null; - } - case AFTER_COUNT_EVERY_TIME -> { - JsonNode condition = rule.getConditionJson(); - long threshold = condition.get("threshold").longValue(); - boolean includeCurrent = condition.get("includeCurrent") == null || condition.get("includeCurrent").isNull() || condition.get("includeCurrent").asBoolean(); - long currentCount = count == null ? count(record) : count; - yield includeCurrent ? threshold <= currentCount ? new RuleResolution(rule.getPointValue(), null) : null : threshold < currentCount ? new RuleResolution(rule.getPointValue(), null) : null; - } - case SOURCE_VALUE -> resolveSourceValue(record, rule); - case CUSTOMER_EVENT_VALUE_TIER -> resolveTier(record, rule); - }; + /// 按规则评估积分决策 + /// + /// 简单规则直接构造固定积分决策;Groovy 规则执行发布版本脚本;脚本未返回决策视为未命中 + /// + /// @param rule 积分规则 + /// @param storeId 门店 ID + /// @param customerId 客户 ID + /// @param occurredAt 发生时间 + /// @param triggerRefType 触发对象筛选类型,可空 + /// @param triggerRefId 触发对象筛选 ID,可空 + /// @return 脚本执行结果,未命中返回 null + private GroovyScriptExecutionResult evaluate(GaoPointRule rule, String storeId, String customerId, long occurredAt, String triggerRefType, String triggerRefId) { + if (rule.getRuleMode() == GaoPointRule.RuleMode.SIMPLE) { + GaoPointDecision decision = new GaoPointDecision(rule.getPointValue(), rule.getName(), Map.of("ruleMode", GaoPointRule.RuleMode.SIMPLE.name()), List.of()); + return new GroovyScriptExecutionResult<>(null, null, null, null, decision); + } + GroovyScript script = groovyScriptService.get(rule.getGroovyScriptId()); + TimiException.required(script, "规则绑定的 Groovy 脚本不存在"); + GaoPointScriptContext context = buildContext(rule, storeId, customerId, occurredAt, triggerRefType, triggerRefId); + Map bindings = new HashMap<>(); + bindings.put("context", context); + return groovyScriptService.executePublished(script.getBizType(), script.getCode(), bindings, GaoPointDecision.class); } - private RuleResolution resolveSourceValue(GaoEventRecord record, GaoPointRule rule) { - if (record.getPointValue() == null) { - return null; - } - permissionChecker.checkAny(ModuleCode.GAO, GaoPermissionCode.POINT_SOURCE_VALUE.getValue()); - long pointValue = record.getPointValue(); - TimiException.requiredTrue(pointValue != 0, "pointValue invalid"); - JsonNode condition = rule.getConditionJson(); - if (condition != null) { - JsonNode min = condition.get("min"); - JsonNode max = condition.get("max"); - TimiException.requiredTrue(min == null || min.isNull() || min.longValue() <= pointValue, "pointValue 小于规则最小值"); - TimiException.requiredTrue(max == null || max.isNull() || pointValue <= max.longValue(), "pointValue 大于规则最大值"); - } - ObjectNode payload = jackson.createObjectNode(); - payload.put("submittedPointValue", pointValue); - payload.put("validatedPointValue", pointValue); - return new RuleResolution(pointValue, payload); + /// 写入积分流水并累计决策结果 + /// + /// @return 本次决策的积分变化 + private long applyDecision(GaoPointRule rule, GroovyScriptExecutionResult execution, + GaoPointLedger.TriggerSourceType triggerSourceType, String triggerSourceId, String salesOrderId, + String storeId, String customerId, Long occurredAt, String operatorUserId, GaoPointScriptContext context) { + GaoPointDecision decision = execution.result(); + TimiException.required(decision, "规则未返回积分决策"); + TimiException.requiredTrue(decision.pointDelta() != 0, "规则积分变化不能为 0"); + ledgerService.applyRule(rule, decision, isGroovy(execution) ? execution : null, triggerSourceType, triggerSourceId, salesOrderId, + storeId, customerId, occurredAt, operatorUserId, decision.consumptionRecordIdList()); + return decision.pointDelta(); } - private RuleResolution resolveTier(GaoEventRecord record, GaoPointRule rule) { - JsonNode condition = rule.getConditionJson(); - String sourceEventId = condition.get("sourceEventId").asText(); - long amount = condition.get("window").get("amount").longValue(); - ZoneId zone = ZoneId.systemDefault(); - 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 = eventRecordServiceProvider.getObject().sumNumberValue(record.getStoreId(), record.getCustomerId(), sourceEventId, windowBeginAt, windowEndAt); + /// 撤销指定触发来源产生的全部规则积分 + private void revokeByTriggerSource(GaoPointLedger.TriggerSourceType triggerSourceType, String triggerSourceId, String reason) { + List originLedgerList = ledgerService.listRuleChangeByTriggerSource(triggerSourceType, triggerSourceId); + revokeAll(originLedgerList, reason); + } - JsonNode matchedTier = null; - for (JsonNode tier : condition.get("tiers")) { - long min = tier.get("min").longValue(); - JsonNode maxNode = tier.get("max"); - if (min <= sumValue && (maxNode == null || maxNode.isNull() || sumValue <= maxNode.longValue())) { - matchedTier = tier; - break; + /// 复算并撤销依赖指定消费事实的条件积分 + /// + /// 按原积分流水保存的脚本版本和原触发时间重算,只有不再满足原条件的积分才撤销 + private void revokeDependentLedgers(String consumptionRecordId) { + List dependentList = ledgerService.listRuleChangeByConsumptionRecordId(consumptionRecordId); + if (dependentList.isEmpty()) { + return; + } + List revokeCandidateList = new java.util.ArrayList<>(); + for (GaoPointLedger origin : dependentList) { + if (!isGroovyLedger(origin) || stillSatisfied(origin)) { + continue; + } + revokeCandidateList.add(origin); + } + revokeAll(revokeCandidateList, "撤销依赖已失效消费事实的积分"); + } + + /// 判断条件积分按原脚本版本和原触发时间重算后是否仍满足 + private boolean stillSatisfied(GaoPointLedger origin) { + if (TimiJava.isEmpty(origin.getGroovyScriptVersionId()) || origin.getTriggerSourceType() == null) { + // 无脚本版本的条件积分无法复算,保守起见视为不再满足 + return false; + } + GaoEventRecord record = null; + if (origin.getTriggerSourceType() == GaoPointLedger.TriggerSourceType.EVENT_RECORD) { + record = eventRecordServiceProvider.getObject().get(origin.getTriggerSourceId()); + if (record == null) { + return false; } } - if (matchedTier == null) { - return null; + GaoPointRule replayRule = new GaoPointRule(); + replayRule.setRuleMode(GaoPointRule.RuleMode.GROOVY); + replayRule.setTriggerSourceType(switch (origin.getTriggerSourceType()) { + case EVENT_RECORD -> GaoPointRule.TriggerSourceType.EVENT_RECORD; + case SALES_ORDER_PAID -> GaoPointRule.TriggerSourceType.SALES_ORDER_PAID; + }); + GaoPointScriptContext context = buildContext(replayRule, origin.getStoreId(), origin.getCustomerId(), + origin.getOccurredAt() == null ? Time.now() : origin.getOccurredAt(), + origin.getTriggerSourceType() == GaoPointLedger.TriggerSourceType.EVENT_RECORD ? GaoPointRule.TriggerRefType.EVENT.name() : null, + record == null ? null : record.getEventId()); + Map bindings = new HashMap<>(); + bindings.put("context", context); + try { + GroovyScriptExecutionResult execution = groovyScriptService.executeVersion(origin.getGroovyScriptVersionId(), bindings, GaoPointDecision.class); + GaoPointDecision decision = execution.result(); + return decision != null && decision.pointDelta() == origin.getPointDelta(); + } catch (RuntimeException e) { + // 脚本版本已不可用或执行失败时按不满足处理,交给撤销流程兜底 + return false; } - long pointDelta = matchedTier.get("pointValue").longValue(); - ObjectNode payload = jackson.createObjectNode(); - payload.put("triggerType", GaoPointRule.TriggerType.CUSTOMER_EVENT_VALUE_TIER.name()); - payload.put("sourceEventId", sourceEventId); - payload.put("sourceValueField", "numberValue"); - payload.put("windowBeginAt", windowBeginAt); - payload.put("windowEndAt", windowEndAt); - payload.put("sumValue", sumValue); - ObjectNode tierPayload = payload.putObject("tier"); - tierPayload.put("min", matchedTier.get("min").longValue()); - JsonNode maxNode = matchedTier.get("max"); - if (maxNode != null && !maxNode.isNull()) { - tierPayload.put("max", maxNode.longValue()); + } + + /// 幂等撤销一批原始积分流水 + private void revokeAll(List originLedgerList, String reason) { + if (originLedgerList == null || originLedgerList.isEmpty()) { + return; + } + List revokeKeyList = originLedgerList.stream().map(origin -> "GAO_POINT_LEDGER:%s:REVOKE".formatted(origin.getId())).toList(); + Set revokedKeySet = new HashSet<>(); + for (GaoPointLedger ledger : ledgerService.listByIdempotencyKeyList(revokeKeyList)) { + revokedKeySet.add(ledger.getIdempotencyKey()); + } + for (GaoPointLedger originLedger : originLedgerList) { + String revokeKey = "GAO_POINT_LEDGER:%s:REVOKE".formatted(originLedger.getId()); + if (!revokedKeySet.contains(revokeKey)) { + ledgerService.revoke(originLedger, reason); + } } - tierPayload.put("pointValue", pointDelta); - payload.put("pointDelta", pointDelta); - return new RuleResolution(pointDelta, payload); } - private long count(GaoEventRecord record) { - return eventRecordServiceProvider.getObject().count(record.getStoreId(), record.getCustomerId(), record.getEventId(), null, null); + private GaoPointScriptContext eventContext(GaoPointRule rule, GaoEventRecord record) { + return buildContext(rule, record.getStoreId(), record.getCustomerId(), record.getRegisteredAt(), + GaoPointRule.TriggerRefType.EVENT.name(), record.getEventId()); } - private String revokeKey(GaoEventRecord record, GaoPointLedger originLedger) { - return "GAO_EVENT_RECORD:%s:LEDGER:%s:REVOKE".formatted(record.getId(), originLedger.getId()); + private GaoPointScriptContext orderContext(GaoPointRule rule, GaoSalesOrder order) { + return buildContext(rule, order.getStoreId(), order.getCustomerId(), order.getPaidAt(), null, null); } - private record RuleResolution(Long pointDelta, JsonNode sourcePayload) { + private GaoPointScriptContext buildContext(GaoPointRule rule, String storeId, String customerId, long occurredAt, String triggerRefType, String triggerRefId) { + return new GaoPointScriptContext(consumptionService, eventRecordServiceProvider.getObject(), rule, storeId, customerId, triggerRefType, triggerRefId, occurredAt); + } + + private boolean isGroovy(GroovyScriptExecutionResult execution) { + return execution != null && execution.versionId() != null; + } + + private boolean isGroovyLedger(GaoPointLedger origin) { + return TimiJava.isNotEmpty(origin.getGroovyScriptVersionId()); } } 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 index 198a006..62322d6 100644 --- 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 @@ -2,11 +2,12 @@ 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.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; @@ -21,6 +22,7 @@ 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.GaoPointTriggerService; import com.imyeyu.api.modules.gao.service.GaoProductService; import com.imyeyu.api.modules.gao.service.GaoProductUnitService; import com.imyeyu.api.modules.gao.service.GaoSalesOrderService; @@ -79,6 +81,8 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService itemList = resolveDirectItems(storeId, request.getItemList()); long saleAmount = 0L; for (GaoSalesOrderItem item : itemList) { @@ -155,24 +158,24 @@ public class GaoSalesOrderServiceImplement 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())) { // 消费事件为 true,订单要求为 false,检查是否设计冲突 - 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/GaoCustomerPackagePurchaseRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerPackagePurchaseRequest.java index c87ceef..b6f7e57 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerPackagePurchaseRequest.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerPackagePurchaseRequest.java @@ -15,8 +15,8 @@ public class GaoCustomerPackagePurchaseRequest { /// 客户端幂等请求号 private String requestNo; - /// 购买事件 ID - private String eventId; + /// 套餐配置 ID + private String packageConfigId; /// 客户 ID private String customerId; 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 index 0433636..bfcded5 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageOrderCreateData.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoPackageOrderCreateData.java @@ -27,9 +27,6 @@ public class GaoPackageOrderCreateData { /// 成交价 private Long saleAmount; - /// 来源购买事件记录 ID - private String sourceEventRecordId; - /// 幂等号 private String requestNo; diff --git a/src/main/resources/db/migration/timiserver/V63__create_groovy_script_tables.sql b/src/main/resources/db/migration/timiserver/V63__create_groovy_script_tables.sql new file mode 100644 index 0000000..2309aef --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V63__create_groovy_script_tables.sql @@ -0,0 +1,30 @@ +CREATE TABLE `groovy_script` ( + `id` VARCHAR(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `biz_type` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '业务归属', + `code` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '业务内唯一代码', + `name` VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '名称', + `description` LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '说明', + `enabled` BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'true 为允许执行已发布版本', + `published_version_id` VARCHAR(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '当前发布版本 ID', + `created_at` BIGINT(20), + `updated_at` BIGINT(20), + `deleted_at` BIGINT(20), + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_groovy_script_biz_code` (`biz_type`, `code`), + KEY `idx_groovy_script_published_version` (`published_version_id`), + KEY `idx_groovy_script_deleted_created` (`deleted_at`, `created_at`) +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'Groovy 脚本' ROW_FORMAT = Dynamic; + +CREATE TABLE `groovy_script_version` ( + `id` VARCHAR(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `script_id` VARCHAR(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '脚本 ID', + `version` INT NOT NULL COMMENT '版本号', + `content` LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '脚本内容', + `checksum` CHAR(64) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL COMMENT '脚本内容 SHA-256', + `created_at` BIGINT(20), + `updated_at` BIGINT(20), + `deleted_at` BIGINT(20), + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_groovy_script_version` (`script_id`, `version`), + KEY `idx_groovy_script_version_script_created` (`script_id`, `created_at`) +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'Groovy 脚本版本' ROW_FORMAT = Dynamic; diff --git a/src/main/resources/db/migration/timiserver/V64__add_gao_consumption_and_point_script_model.sql b/src/main/resources/db/migration/timiserver/V64__add_gao_consumption_and_point_script_model.sql new file mode 100644 index 0000000..d14f80b --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V64__add_gao_consumption_and_point_script_model.sql @@ -0,0 +1,62 @@ +CREATE TABLE `gao_customer_consumption_record` ( + `id` VARCHAR(36) NOT NULL, + `store_id` VARCHAR(36) NOT NULL COMMENT '门店 ID', + `customer_id` VARCHAR(36) NOT NULL COMMENT '客户 ID', + `source_type` VARCHAR(32) NOT NULL COMMENT '来源类型', + `source_id` VARCHAR(36) NOT NULL COMMENT '来源业务对象 ID', + `sales_order_id` VARCHAR(36) NULL COMMENT '销售订单 ID', + `order_type` VARCHAR(32) NULL COMMENT '订单类型', + `amount` BIGINT NOT NULL COMMENT '有效消费金额,单位分', + `effective_at` BIGINT NOT NULL COMMENT '成为有效消费时间', + `status` VARCHAR(32) NOT NULL COMMENT '状态', + `revoked_at` BIGINT NULL COMMENT '失效时间', + `revoke_reason` TEXT NULL COMMENT '失效原因', + `source_payload` JSON NULL COMMENT '来源快照', + `created_at` BIGINT NOT NULL, + `updated_at` BIGINT NULL, + `deleted_at` BIGINT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_gao_customer_consumption_source` (`source_type`, `source_id`), + KEY `idx_gao_customer_consumption_customer_time` (`store_id`, `customer_id`, `status`, `effective_at`), + KEY `idx_gao_customer_consumption_order` (`sales_order_id`) +) COMMENT='GAO 客户有效消费事实'; + +CREATE TABLE `gao_point_ledger_consumption_record` ( + `point_ledger_id` VARCHAR(36) NOT NULL COMMENT '积分流水 ID', + `consumption_record_id` VARCHAR(36) NOT NULL COMMENT '消费事实 ID', + `created_at` BIGINT NOT NULL, + PRIMARY KEY (`point_ledger_id`, `consumption_record_id`), + KEY `idx_gao_point_ledger_consumption_record_consumption` (`consumption_record_id`) +) COMMENT='GAO 积分流水消费事实依赖'; + +ALTER TABLE `gao_sales_order` + ADD COLUMN `paid_at` BIGINT NULL COMMENT '首次全额实收时间' AFTER `paid_amount`; + +ALTER TABLE `gao_point_rule` + ADD COLUMN `rule_mode` VARCHAR(16) NULL COMMENT '规则模式' AFTER `status`, + ADD COLUMN `trigger_source_type` VARCHAR(32) NULL COMMENT '触发来源类型' AFTER `rule_mode`, + ADD COLUMN `trigger_ref_type` VARCHAR(32) NULL COMMENT '触发对象类型' AFTER `trigger_source_type`, + ADD COLUMN `trigger_ref_id` VARCHAR(36) NULL COMMENT '触发对象 ID' AFTER `trigger_ref_type`, + ADD COLUMN `groovy_script_id` VARCHAR(36) NULL COMMENT 'Groovy 脚本 ID' AFTER `point_value`, + ADD KEY `idx_gao_point_rule_trigger` (`store_id`, `trigger_source_type`, `trigger_ref_id`, `status`, `deleted_at`, `priority`); + +ALTER TABLE `gao_point_ledger` + ADD COLUMN `trigger_source_type` VARCHAR(32) NULL COMMENT '直接触发来源类型' AFTER `rule_id`, + ADD COLUMN `trigger_source_id` VARCHAR(36) NULL COMMENT '直接触发来源 ID' AFTER `trigger_source_type`, + ADD COLUMN `groovy_script_version_id` VARCHAR(36) NULL COMMENT '实际执行 Groovy 脚本版本 ID' AFTER `trigger_source_id`, + ADD COLUMN `rule_snapshot` JSON NULL COMMENT '规则快照' AFTER `groovy_script_version_id`, + ADD KEY `idx_gao_point_ledger_trigger_source` (`trigger_source_type`, `trigger_source_id`, `change_type`); + +UPDATE `gao_point_rule` +SET `rule_mode` = 'SIMPLE', + `trigger_source_type` = 'EVENT_RECORD', + `trigger_ref_type` = 'GAO_EVENT', + `trigger_ref_id` = `event_id` +WHERE `trigger_type` = 'EVERY_TIME' + AND `point_mode` = 'FIXED' + AND `condition_json` IS NULL; + +UPDATE `gao_point_ledger` +SET `trigger_source_type` = 'EVENT_RECORD', + `trigger_source_id` = `event_record_id` +WHERE `event_record_id` IS NOT NULL; diff --git a/src/main/resources/db/migration/timiserver/V65__migrate_gao_point_rules_to_groovy.sql b/src/main/resources/db/migration/timiserver/V65__migrate_gao_point_rules_to_groovy.sql new file mode 100644 index 0000000..ccb4858 --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V65__migrate_gao_point_rules_to_groovy.sql @@ -0,0 +1,23 @@ +INSERT INTO `groovy_script` (`id`, `biz_type`, `code`, `name`, `description`, `enabled`, `published_version_id`, `created_at`) +VALUES + ('f2511e0e-6e55-4a59-a221-700000000001', 'GAO_POINT_RULE', 'SIGN_IN_CONSUMPTION_TIER', '签到按消费分档积分', '签到时按过去一年有效消费分档积分', TRUE, 'f2511e0e-6e55-4a59-a221-700000000101', 1787248052654), + ('f2511e0e-6e55-4a59-a221-700000000002', 'GAO_POINT_RULE', 'SIGN_IN_AFTER_THIRTY', '无消费累计签到积分', '第 30 次签到及以后每次积分', TRUE, 'f2511e0e-6e55-4a59-a221-700000000102', 1787041413269); + +INSERT INTO `groovy_script_version` (`id`, `script_id`, `version`, `content`, `checksum`, `created_at`) +VALUES + ('f2511e0e-6e55-4a59-a221-700000000101', 'f2511e0e-6e55-4a59-a221-700000000001', 1, + 'long amount = context.rollingYearConsumptionAmount()\nif (amount < 1L) return null\nlong point = amount < 200000L ? 20L : amount < 2000000L ? 50L : amount < 5000000L ? 80L : 100L\nreturn context.decision(point, "签到消费分档积分", [amount: amount, window: "ROLLING_1_YEAR"])', + SHA2('long amount = context.rollingYearConsumptionAmount()\nif (amount < 1L) return null\nlong point = amount < 200000L ? 20L : amount < 2000000L ? 50L : amount < 5000000L ? 80L : 100L\nreturn context.decision(point, "签到消费分档积分", [amount: amount, window: "ROLLING_1_YEAR"])', 256), 1787248052654), + ('f2511e0e-6e55-4a59-a221-700000000102', 'f2511e0e-6e55-4a59-a221-700000000002', 1, + 'if (context.eventCount() < 30L) return null\nreturn context.decision(10L, "第 30 次及以后签到积分", [eventCount: context.eventCount()])', + SHA2('if (context.eventCount() < 30L) return null\nreturn context.decision(10L, "第 30 次及以后签到积分", [eventCount: context.eventCount()])', 256), 1787041413269); + +UPDATE `gao_point_rule` +SET `rule_mode` = 'GROOVY', `trigger_source_type` = 'EVENT_RECORD', `trigger_ref_type` = 'EVENT', + `trigger_ref_id` = `event_id`, `groovy_script_id` = 'f2511e0e-6e55-4a59-a221-700000000001', `point_value` = NULL +WHERE `id` = '1746879e-3726-4927-95a6-a3b738cbc134'; + +UPDATE `gao_point_rule` +SET `rule_mode` = 'GROOVY', `trigger_source_type` = 'EVENT_RECORD', `trigger_ref_type` = 'EVENT', + `trigger_ref_id` = `event_id`, `groovy_script_id` = 'f2511e0e-6e55-4a59-a221-700000000002', `point_value` = NULL +WHERE `id` = '1b71dbb8-be2e-4783-9209-e6a94a8745b4'; diff --git a/src/main/resources/db/migration/timiserver/V66__migrate_gao_legacy_consumption_events.sql b/src/main/resources/db/migration/timiserver/V66__migrate_gao_legacy_consumption_events.sql new file mode 100644 index 0000000..4321c74 --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V66__migrate_gao_legacy_consumption_events.sql @@ -0,0 +1,44 @@ +ALTER TABLE `gao_point_ledger` + ADD COLUMN `sales_order_id` VARCHAR(36) NULL COMMENT '关联销售订单 ID' AFTER `trigger_source_id`, + ADD KEY `idx_gao_point_ledger_sales_order` (`sales_order_id`); + +-- 将旧规则 sourceEventId 指向的登记事件标记为退役消费事件 +UPDATE `gao_event` e +SET e.`biz_type` = 'LEGACY_CONSUMPTION' +WHERE e.`deleted_at` IS NULL + AND e.`biz_type` <> 'PACKAGE_PURCHASE' + AND e.`id` IN ( + SELECT DISTINCT JSON_UNQUOTE(JSON_EXTRACT(r.`condition_json`, '$.sourceEventId')) + FROM `gao_point_rule` r + WHERE r.`condition_json` IS NOT NULL + AND JSON_EXTRACT(r.`condition_json`, '$.sourceEventId') IS NOT NULL + ); + +-- 历史消费事件迁移为 LEGACY_EVENT 消费事实,来源唯一约束保证可重跑 +-- 不能使用 INSERT IGNORE,否则数据异常会被静默吞掉,导致消费累计缺失 +INSERT INTO `gao_customer_consumption_record` + (`id`, `store_id`, `customer_id`, `source_type`, `source_id`, `sales_order_id`, `order_type`, `amount`, `effective_at`, `status`, `revoked_at`, `revoke_reason`, `source_payload`, `created_at`, `updated_at`, `deleted_at`) +SELECT + UUID(), + r.`store_id`, + r.`customer_id`, + 'LEGACY_EVENT', + r.`id`, + NULL, + NULL, + r.`number_value`, + r.`registered_at`, + 'ACTIVE', + NULL, + NULL, + JSON_OBJECT('migrationBatch', 'V66', 'eventId', r.`event_id`, 'eventName', e.`name`, 'eventRecordId', r.`id`, 'registeredAt', r.`registered_at`), + FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000), + NULL, + NULL +FROM `gao_event_record` r +INNER JOIN `gao_event` e ON e.`id` = r.`event_id` AND e.`deleted_at` IS NULL +WHERE e.`biz_type` = 'LEGACY_CONSUMPTION' + AND r.`number_value` IS NOT NULL + AND r.`number_value` > 0 + AND r.`deleted_at` IS NULL +ON DUPLICATE KEY UPDATE `id` = `id`; diff --git a/src/main/resources/db/migration/timiserver/V67__fix_gao_point_rule_trigger_ref_type.sql b/src/main/resources/db/migration/timiserver/V67__fix_gao_point_rule_trigger_ref_type.sql new file mode 100644 index 0000000..849de79 --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V67__fix_gao_point_rule_trigger_ref_type.sql @@ -0,0 +1,4 @@ +-- V64 曾把枚举值误写为 GAO_EVENT,MyBatis 读取时会无法映射到 TriggerRefType.EVENT +UPDATE `gao_point_rule` +SET `trigger_ref_type` = 'EVENT' +WHERE `trigger_ref_type` = 'GAO_EVENT'; diff --git a/src/main/resources/mapper/timi-server/gao/GaoCustomerConsumptionRecordMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoCustomerConsumptionRecordMapper.xml new file mode 100644 index 0000000..79f092c --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoCustomerConsumptionRecordMapper.xml @@ -0,0 +1,20 @@ + + + + + + + UPDATE `gao_customer_consumption_record` + SET `status` = 'REVOKED', `revoked_at` = #{revokedAt}, `revoke_reason` = #{revokeReason}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) + WHERE `source_type` = #{sourceType} AND `source_id` = #{sourceId} AND `status` = 'ACTIVE' AND `deleted_at` IS NULL + + diff --git a/src/main/resources/mapper/timi-server/gao/GaoEventMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoEventMapper.xml index 9b08997..9c5283e 100644 --- a/src/main/resources/mapper/timi-server/gao/GaoEventMapper.xml +++ b/src/main/resources/mapper/timi-server/gao/GaoEventMapper.xml @@ -26,6 +26,7 @@ WHERE `store_id` = #{storeId} AND `status` = '${@com.imyeyu.api.modules.gao.entity.GaoEvent$Status@ACTIVE.name()}' + AND (`biz_type` IS NULL OR `biz_type` != 'LEGACY_CONSUMPTION') AND (`begin_at` IS NULL OR `begin_at` <= #{now}) AND (`end_at` IS NULL OR #{now} <= `end_at`) AND `deleted_at` IS NULL diff --git a/src/main/resources/mapper/timi-server/gao/GaoEventRecordMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoEventRecordMapper.xml index 869d9c6..55ee1e1 100644 --- a/src/main/resources/mapper/timi-server/gao/GaoEventRecordMapper.xml +++ b/src/main/resources/mapper/timi-server/gao/GaoEventRecordMapper.xml @@ -119,6 +119,12 @@ #{eventId} + + AND NOT EXISTS ( + SELECT 1 FROM `gao_event` re + WHERE re.`id` = r.`event_id` AND re.`biz_type` = 'LEGACY_CONSUMPTION' AND re.`deleted_at` IS NULL + ) + AND EXISTS ( @@ -212,6 +218,12 @@ #{eventId} + + AND NOT EXISTS ( + SELECT 1 FROM `gao_event` re + WHERE re.`id` = r.`event_id` AND re.`biz_type` = 'LEGACY_CONSUMPTION' AND re.`deleted_at` IS NULL + ) + AND r.`number_value` IS NOT NULL @@ -386,6 +398,12 @@ #{eventId} + + AND NOT EXISTS ( + SELECT 1 FROM `gao_event` re + WHERE re.`id` = r.`event_id` AND re.`biz_type` = 'LEGACY_CONSUMPTION' AND re.`deleted_at` IS NULL + ) + SELECT diff --git a/src/main/resources/mapper/timi-server/gao/GaoPointLedgerMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoPointLedgerMapper.xml index 1589b75..6fc810c 100644 --- a/src/main/resources/mapper/timi-server/gao/GaoPointLedgerMapper.xml +++ b/src/main/resources/mapper/timi-server/gao/GaoPointLedgerMapper.xml @@ -9,15 +9,26 @@ LIMIT 1 - SELECT * FROM `gao_point_ledger` - WHERE `event_record_id` = #{eventRecordId} + WHERE `trigger_source_type` = #{triggerSourceType} + AND `trigger_source_id` = #{triggerSourceId} AND `change_type` IN ('EARN', 'CONSUME') AND `deleted_at` IS NULL ORDER BY `created_at` ASC, `id` ASC + + - SELECT * FROM `gao_point_rule` WHERE `store_id` = #{storeId} - AND `event_id` = #{eventId} + AND `trigger_source_type` = #{triggerSourceType} + AND (`trigger_ref_id` IS NULL + + OR `trigger_ref_id` = #{triggerRefId} + + ) AND `status` = 'ACTIVE' AND (`begin_at` IS NULL OR `begin_at` <= #{now}) AND (`end_at` IS NULL OR #{now} <= `end_at`) diff --git a/src/main/resources/mapper/timi-server/gao/GaoSalesOrderMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoSalesOrderMapper.xml index 09fc2bd..520e968 100644 --- a/src/main/resources/mapper/timi-server/gao/GaoSalesOrderMapper.xml +++ b/src/main/resources/mapper/timi-server/gao/GaoSalesOrderMapper.xml @@ -33,7 +33,7 @@ UPDATE `gao_sales_order` - SET `pending_paid_amount` = #{pendingPaidAmount}, `paid_amount` = #{paidAmount}, `refunded_amount` = #{refundedAmount}, + SET `pending_paid_amount` = #{pendingPaidAmount}, `paid_amount` = #{paidAmount}, `paid_at` = #{paidAt}, `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 @@ -43,11 +43,6 @@ `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) From f3166f041367083677d1c786c4c3f4e003cc9d1f Mon Sep 17 00:00:00 2001 From: Timi Date: Sun, 6 Sep 2026 12:09:58 +0800 Subject: [PATCH 05/10] add sales order --- .../modules/gao/bean/GaoPermissionCode.java | 2 + .../GaoCustomerPackageController.java | 26 +++++- .../controller/GaoFundRecordController.java | 3 +- .../GaoOrderFulfillmentController.java | 17 ++++ .../GaoPackageConfigController.java | 33 ++++++- .../controller/GaoSalesOrderController.java | 11 +++ .../gao/entity/GaoCustomerPackageItem.java | 6 ++ .../api/modules/gao/entity/GaoFundRecord.java | 11 +++ .../gao/entity/GaoOrderFulfillment.java | 20 ++++- .../gao/entity/GaoOrderFulfillmentItem.java | 22 ++++- .../gao/entity/GaoPackageConfigItem.java | 5 ++ .../modules/gao/entity/GaoSalesOrderItem.java | 7 ++ .../gao/mapper/GaoFundRecordMapper.java | 3 + .../mapper/GaoOrderFulfillmentItemMapper.java | 3 + .../modules/gao/mapper/GaoProductMapper.java | 4 + .../gao/mapper/GaoSalesOrderItemMapper.java | 3 + .../gao/mapper/GaoSalesOrderMapper.java | 3 + .../gao/script/GaoPointScriptContext.java | 38 ++------ .../gao/service/GaoFundRecordService.java | 3 + .../gao/service/GaoProductService.java | 4 + .../gao/service/GaoSalesOrderService.java | 4 + .../GaoFundRecordServiceImplement.java | 10 +++ .../GaoOrderFulfillmentServiceImplement.java | 52 ++++++++++- .../GaoOrderPaymentServiceImplement.java | 1 + .../implement/GaoProductServiceImplement.java | 11 +++ .../GaoSalesOrderServiceImplement.java | 89 ++++++++++++++++++- .../gao/vo/GaoProductStockRecordPage.java | 12 +++ .../api/modules/gao/vo/GaoSalesOrderPage.java | 6 ++ .../gao/vo/GaoSalesOrderUpdateRequest.java | 25 ++++++ .../user/controller/RoleController.java | 27 +++++- .../api/modules/user/service/RoleService.java | 6 ++ ...AuthorizationBoundaryServiceImplement.java | 16 +++- .../implement/RoleServiceImplement.java | 19 ++++ ..._migrate_gao_legacy_consumption_events.sql | 6 +- ...ot_gao_fund_record_order_unpaid_amount.sql | 2 + .../gao/GaoCustomerPackageMapper.xml | 3 +- .../timi-server/gao/GaoFundRecordMapper.xml | 33 ++++++- .../gao/GaoOrderFulfillmentItemMapper.xml | 8 ++ .../timi-server/gao/GaoProductMapper.xml | 10 +++ .../timi-server/gao/GaoProductStockMapper.xml | 39 +++++--- .../gao/GaoProductStockRecordMapper.xml | 14 +++ .../gao/GaoSalesOrderItemMapper.xml | 9 ++ .../timi-server/gao/GaoSalesOrderMapper.xml | 7 ++ 43 files changed, 560 insertions(+), 73 deletions(-) create mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderUpdateRequest.java create mode 100644 src/main/resources/db/migration/timiserver/V68__snapshot_gao_fund_record_order_unpaid_amount.sql 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 79b16fb..c423608 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 @@ -81,6 +81,8 @@ public enum GaoPermissionCode implements BuiltinPermissionCode { SALES_ORDER_READ("SALES_ORDER:READ", "销售订单读取", true, true), + SALES_ORDER_UPDATE("SALES_ORDER:UPDATE", "销售订单修改", true, true), + SALES_ORDER_CANCEL("SALES_ORDER:CANCEL", "销售订单取消", true, true), SALES_ORDER_PRICE_OVERRIDE("SALES_ORDER:PRICE_OVERRIDE", "销售订单价格修改", 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 1243017..9fdc013 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 @@ -8,6 +8,7 @@ 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.GaoOrderFulfillment; +import com.imyeyu.api.modules.gao.entity.GaoProduct; import com.imyeyu.api.modules.gao.service.GaoCustomerPackageService; import com.imyeyu.api.modules.gao.service.GaoCustomerService; import com.imyeyu.api.modules.gao.service.GaoOrderFulfillmentService; @@ -127,11 +128,34 @@ public class GaoCustomerPackageController { Map userMap = userIdList.isEmpty() ? Map.of() : userService.mapByIdList(userIdList); for (GaoCustomerPackage packageEntity : packageList) { List itemList = itemMap.getOrDefault(packageEntity.getId(), List.of()); + fillItemAttachments(itemList); for (GaoCustomerPackageItem item : itemList) { fillDerived(item); } packageEntity.setItemList(itemList); - packageEntity.setCancelOperator(userMap.get(packageEntity.getCancelOperatorUserId())); + String cancelOperatorUserId = packageEntity.getCancelOperatorUserId(); + packageEntity.setCancelOperator(cancelOperatorUserId == null ? null : userMap.get(cancelOperatorUserId)); + } + } + + private void fillItemAttachments(List itemList) { + if (itemList == null || itemList.isEmpty()) { + return; + } + List productIdList = itemList.stream() + .map(GaoCustomerPackageItem::getProductId) + .filter(value -> value != null && !value.isBlank()) + .distinct() + .toList(); + if (productIdList.isEmpty()) { + return; + } + Map> attachmentMap = attachmentService.mapByBizIdList( + Attachment.BizType.GAO_PRODUCT, + productIdList, + GaoProduct.AttachType.PHOTO.name()); + for (GaoCustomerPackageItem item : itemList) { + item.setAttachmentList(attachmentMap.getOrDefault(item.getProductId(), List.of())); } } 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 033259b..30d1ae3 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 @@ -60,8 +60,7 @@ public class GaoFundRecordController { @RequireGaoPermission(GaoPermissionCode.FUND_RECORD_READ) @PostMapping("/detail") public GaoFundRecord detail(@RequestParam String id) { - GaoFundRecord result = service.get(id); - TimiException.required(result, "not found fund record"); + GaoFundRecord result = service.detail(id); storeScopeService.checkStoreId(result.getStoreId()); 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 index 5be9243..940c519 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoOrderFulfillmentController.java +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoOrderFulfillmentController.java @@ -4,7 +4,9 @@ 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.GaoCustomer; import com.imyeyu.api.modules.gao.entity.GaoOrderFulfillment; +import com.imyeyu.api.modules.gao.service.GaoCustomerService; import com.imyeyu.api.modules.gao.service.GaoOrderFulfillmentService; import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentActionRequest; @@ -41,6 +43,7 @@ public class GaoOrderFulfillmentController { private final GaoOrderFulfillmentService service; private final GaoStoreScopeService storeScopeService; private final AttachmentService attachmentService; + private final GaoCustomerService customerService; private final UserService userService; @AOPLog @@ -131,6 +134,15 @@ public class GaoOrderFulfillmentController { } List idList = fulfillmentList.stream().map(GaoOrderFulfillment::getId).toList(); Map> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_ORDER_FULFILLMENT, idList); + List customerIdList = fulfillmentList.stream() + .map(GaoOrderFulfillment::getCustomerId) + .filter(value -> value != null && !value.isBlank()) + .distinct() + .toList(); + Map customerMap = customerIdList.isEmpty() ? Map.of() : customerService.mapByIdList(customerIdList); + Map> customerAttachmentMap = customerIdList.isEmpty() + ? Map.of() + : attachmentService.mapByBizIdList(Attachment.BizType.GAO_CUSTOMER, customerIdList, GaoCustomer.AttachType.PHOTO.name()); List userIdList = fulfillmentList.stream() .flatMap(item -> Stream.of(item.getOperatorUserId(), item.getVerifierUserId())) .filter(value -> value != null && !value.isBlank()) @@ -139,6 +151,11 @@ public class GaoOrderFulfillmentController { Map userMap = userIdList.isEmpty() ? Map.of() : userService.mapByIdList(userIdList); for (GaoOrderFulfillment fulfillment : fulfillmentList) { fulfillment.setAttachmentList(attachmentMap.getOrDefault(fulfillment.getId(), List.of())); + GaoCustomer customer = customerMap.get(fulfillment.getCustomerId()); + if (customer != null) { + customer.setAttachmentList(customerAttachmentMap.getOrDefault(customer.getId(), List.of())); + } + fulfillment.setCustomer(customer); 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 af4cea9..9ef9dc2 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,8 +4,10 @@ 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.entity.GaoProduct; import com.imyeyu.api.modules.gao.service.GaoEventService; 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.GaoPackageConfigPage; import com.imyeyu.api.modules.user.entity.User; @@ -35,6 +37,7 @@ public class GaoPackageConfigController { private final GaoPackageConfigService service; private final GaoEventService eventService; + private final GaoProductService productService; private final GaoStoreScopeService storeScopeService; private final UserService userService; @@ -46,7 +49,7 @@ public class GaoPackageConfigController { TimiException.required(page, "not found page"); page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId())); PageResult result = service.pageByQuery(page); - fillTransient(result.getList()); + fillTransient(result.getList(), false); return result; } @@ -56,7 +59,7 @@ public class GaoPackageConfigController { @PostMapping("/list/valid") public List listValid(@RequestParam String storeId) { List result = service.listValid(storeScopeService.resolveStoreId(storeId)); - fillTransient(result); + fillTransient(result, false); return result; } @@ -68,11 +71,11 @@ public class GaoPackageConfigController { GaoPackageConfig config = service.get(id); TimiException.required(config, "not found package config"); storeScopeService.checkStoreId(config.getStoreId()); - fillTransient(List.of(config)); + fillTransient(List.of(config), true); return config; } - private void fillTransient(List configList) { + private void fillTransient(List configList, boolean includeProductDetail) { if (configList == null || configList.isEmpty()) { return; } @@ -83,6 +86,28 @@ public class GaoPackageConfigController { config.setItemList(itemMap.getOrDefault(config.getId(), List.of())); config.setUpdater(updaterMap.get(config.getUpdatedBy())); } + if (includeProductDetail) { + fillProductDetails(configList); + } + } + + private void fillProductDetails(List configList) { + List productIdList = configList.stream() + .flatMap(config -> config.getItemList().stream()) + .map(GaoPackageConfigItem::getProductId) + .filter(value -> value != null && !value.isBlank()) + .distinct() + .toList(); + if (productIdList.isEmpty()) { + return; + } + Map productMap = productService.listByIdList(productIdList).stream() + .collect(java.util.stream.Collectors.toMap(GaoProduct::getId, item -> item)); + for (GaoPackageConfig config : configList) { + for (GaoPackageConfigItem item : config.getItemList()) { + item.setProduct(productMap.get(item.getProductId())); + } + } } @AOPLog 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 index 91d774b..1701140 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoSalesOrderController.java +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoSalesOrderController.java @@ -15,6 +15,7 @@ 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.api.modules.gao.vo.GaoSalesOrderUpdateRequest; import com.imyeyu.java.bean.timi.TimiException; import com.imyeyu.spring.annotation.AOPLog; import com.imyeyu.spring.annotation.RequestRateLimit; @@ -67,6 +68,16 @@ public class GaoSalesOrderController { return service.createDirectProductOrder(request); } + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.SALES_ORDER_UPDATE) + @PostMapping("/update") + public GaoSalesOrder update(@RequestBody GaoSalesOrderUpdateRequest request) { + TimiException.required(request, "not found sales order update request"); + TimiException.required(request.getId(), "not found sales order.id"); + return service.updateRemarkAndAttachments(request); + } + @AOPLog @RequestRateLimit @RequireGaoPermission(GaoPermissionCode.SALES_ORDER_CANCEL) 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 84fc8b6..c1f605c 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 @@ -1,11 +1,13 @@ 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 客户套餐产品权益快照 /// @@ -24,6 +26,10 @@ public class GaoCustomerPackageItem extends UUIDEntity { /// 产品单位 ID private String productUnitId; + /// 产品图片附件 + @Transient + private List attachmentList; + /// 产品名称快照 private String productNameSnapshot; 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 85992c8..20390d0 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 @@ -101,6 +101,17 @@ public class GaoFundRecord extends UUIDEntity { /// 备注 private String remark; + /// 关联的冲正流水 ID + @Transient + private String revokeRecordId; + + /// 关联销售订单总价,单位分 + @Transient + private Long salesOrderAmount; + + /// 流水发生时关联销售订单剩余待收款金额快照,单位分 + private Long salesOrderUnpaidAmountSnapshot; + /// 操作人 @Transient private User operator; diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoOrderFulfillment.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoOrderFulfillment.java index c7b5e88..bb662d6 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoOrderFulfillment.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoOrderFulfillment.java @@ -2,8 +2,13 @@ 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 com.imyeyu.spring.entity.UUIDEntity; +import com.imyeyu.spring.entity.Updatable; import lombok.Data; import lombok.EqualsAndHashCode; @@ -14,8 +19,7 @@ import java.util.List; /// @author Codex /// @since 2026-09-02 @Data -@EqualsAndHashCode(callSuper = true) -public class GaoOrderFulfillment extends UUIDEntity { +public class GaoOrderFulfillment implements IDEntity, Creatable, Updatable { /// 履约来源类型 public enum FulfillmentType { @@ -49,6 +53,10 @@ public class GaoOrderFulfillment extends UUIDEntity { REVOKED } + @Id + @AutoUUID + private String id; + /// 销售订单 ID private String salesOrderId; @@ -88,10 +96,18 @@ public class GaoOrderFulfillment extends UUIDEntity { /// 备注 private String remark; + private Long createdAt; + + private Long updatedAt; + /// 提交人 @Transient private User operator; + /// 客户 + @Transient + private GaoCustomer customer; + /// 核销或拒绝操作人 @Transient private User verifier; 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 index 4c08de0..3f8cf9a 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoOrderFulfillmentItem.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoOrderFulfillmentItem.java @@ -1,18 +1,26 @@ package com.imyeyu.api.modules.gao.entity; -import com.imyeyu.spring.entity.UUIDEntity; +import com.imyeyu.api.modules.common.entity.Attachment; +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 lombok.EqualsAndHashCode; import java.math.BigDecimal; +import java.util.List; /// GAO 统一履约明细 /// /// @author Codex /// @since 2026-09-02 @Data -@EqualsAndHashCode(callSuper = true) -public class GaoOrderFulfillmentItem extends UUIDEntity { +public class GaoOrderFulfillmentItem implements IDEntity, Creatable { + + @Id + @AutoUUID + private String id; /// 履约单 ID private String fulfillmentId; @@ -23,6 +31,10 @@ public class GaoOrderFulfillmentItem extends UUIDEntity { /// 产品单位 ID private String productUnitId; + /// 产品图片附件 + @Transient + private List attachmentList; + /// 直接订单明细 ID private String sourceOrderItemId; @@ -55,4 +67,6 @@ public class GaoOrderFulfillmentItem extends UUIDEntity { /// 基础单位数量系数快照 private BigDecimal baseQuantityPerUnitSnapshot; + + private Long createdAt; } 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 a3d7a1b..7562100 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 @@ -1,5 +1,6 @@ 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; @@ -40,4 +41,8 @@ public class GaoPackageConfigItem extends UUIDEntity { /// 备注 private String remark; + + /// 产品详情 + @Transient + private GaoProduct product; } 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 index b41b8f0..0c0479a 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoSalesOrderItem.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoSalesOrderItem.java @@ -1,10 +1,13 @@ 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 直接产品订单明细 /// @@ -23,6 +26,10 @@ public class GaoSalesOrderItem extends UUIDEntity { /// 产品单位 ID private String productUnitId; + /// 产品图片附件 + @Transient + private List attachmentList; + /// 产品名称快照 private String productNameSnapshot; diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoFundRecordMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoFundRecordMapper.java index 4f130b0..31d1289 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoFundRecordMapper.java +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoFundRecordMapper.java @@ -19,6 +19,9 @@ public interface GaoFundRecordMapper extends BaseMapper { /// 锁定资金记录 GaoFundRecord selectForUpdate(@Param("id") String id); + /// 查询资金记录及关联销售订单资金摘要 + GaoFundRecord selectWithSalesOrderSummary(@Param("id") String id); + /// 查询资金冲正记录 GaoFundRecord selectRevokeBySourceRecordId(@Param("sourceRecordId") String sourceRecordId); 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 index ead64ba..513d47b 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoOrderFulfillmentItemMapper.java +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoOrderFulfillmentItemMapper.java @@ -15,6 +15,9 @@ public interface GaoOrderFulfillmentItemMapper extends BaseMapper selectByFulfillmentId(@Param("fulfillmentId") String fulfillmentId); + /// 批量查询履约明细 + List selectByFulfillmentIdList(@Param("fulfillmentIdList") List fulfillmentIdList); + /// 锁定履约明细 List selectByFulfillmentIdForUpdate(@Param("fulfillmentId") String fulfillmentId); } 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 index f46e8da..5841a47 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductMapper.java +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductMapper.java @@ -5,6 +5,7 @@ import com.imyeyu.api.modules.gao.vo.GaoProductPage; import com.imyeyu.spring.mapper.BaseMapper; import org.apache.ibatis.annotations.Param; +import java.util.Collection; import java.util.List; /// GAO 产品 Mapper @@ -19,6 +20,9 @@ public interface GaoProductMapper extends BaseMapper { /// 按条件查询产品 List selectByQuery(GaoProductPage page); + /// 批量查询产品 + List selectByIdList(@Param("idList") Collection idList); + /// 查询产品编码 GaoProduct selectByStoreIdAndCode(@Param("storeId") String storeId, @Param("code") String code, @Param("excludeId") String excludeId); 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 index cb3a58b..a51eb31 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoSalesOrderItemMapper.java +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoSalesOrderItemMapper.java @@ -16,6 +16,9 @@ public interface GaoSalesOrderItemMapper extends BaseMapper selectBySalesOrderId(@Param("salesOrderId") String salesOrderId); + /// 批量查询订单明细 + List selectBySalesOrderIdList(@Param("salesOrderIdList") List salesOrderIdList); + /// 锁定订单明细 GaoSalesOrderItem selectForUpdate(@Param("id") String id); 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 index 7829995..185e0a8 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoSalesOrderMapper.java +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoSalesOrderMapper.java @@ -37,4 +37,7 @@ public interface GaoSalesOrderMapper extends BaseMapper { /// 更新订单履约状态 int updateStatus(@Param("id") String id, @Param("status") GaoSalesOrder.Status status); + + /// 更新订单备注 + int updateRemark(@Param("id") String id, @Param("remark") String remark); } diff --git a/src/main/java/com/imyeyu/api/modules/gao/script/GaoPointScriptContext.java b/src/main/java/com/imyeyu/api/modules/gao/script/GaoPointScriptContext.java index cd98c2c..944b7bd 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/script/GaoPointScriptContext.java +++ b/src/main/java/com/imyeyu/api/modules/gao/script/GaoPointScriptContext.java @@ -5,6 +5,7 @@ import com.imyeyu.api.modules.gao.entity.GaoPointRule; import com.imyeyu.api.modules.gao.service.GaoCustomerConsumptionRecordService; import com.imyeyu.api.modules.gao.service.GaoEventRecordService; import com.imyeyu.java.bean.timi.TimiException; +import lombok.Getter; import java.time.Instant; import java.time.LocalDate; @@ -28,11 +29,18 @@ public class GaoPointScriptContext { private final GaoCustomerConsumptionRecordService consumptionService; private final GaoEventRecordService eventRecordService; private final GaoPointRule rule; + + @Getter private final String storeId; + @Getter private final String customerId; + @Getter private final String triggerSourceType; + @Getter private final String triggerRefType; + @Getter private final String triggerRefId; + @Getter private final long occurredAt; /// 参与过金额计算的消费事实,ID 到金额摘要 @@ -51,36 +59,6 @@ public class GaoPointScriptContext { this.occurredAt = occurredAt; } - /// 触发来源门店 ID - public String getStoreId() { - return storeId; - } - - /// 触发来源客户 ID - public String getCustomerId() { - return customerId; - } - - /// 直接触发来源类型 - public String getTriggerSourceType() { - return triggerSourceType; - } - - /// 触发对象筛选类型 - public String getTriggerRefType() { - return triggerRefType; - } - - /// 触发对象筛选 ID - public String getTriggerRefId() { - return triggerRefId; - } - - /// 触发发生时间,毫秒 - public long getOccurredAt() { - return occurredAt; - } - /// 查询触发时间往前滚动一年的有效消费金额,窗口按 Asia/Shanghai 自然日对齐 /// /// @return 窗口内 ACTIVE 消费事实金额合计,单位分 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 9595a3f..7263186 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 @@ -16,6 +16,9 @@ public interface GaoFundRecordService extends BaseService /// 按条件查询资金流水 PageResult pageByQuery(GaoFundRecordPage page); + /// 查询资金流水详情 + GaoFundRecord detail(String id); + /// 按销售订单查询资金流水 List listByBizId(String salesOrderId); 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 index 139c85f..84814cd 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductService.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductService.java @@ -7,6 +7,7 @@ import com.imyeyu.api.modules.gao.vo.GaoProductUnitSelection; import com.imyeyu.spring.bean.PageResult; import com.imyeyu.spring.service.BaseService; +import java.util.Collection; import java.util.List; /// GAO 产品服务 @@ -21,6 +22,9 @@ public interface GaoProductService extends BaseService { /// 查询产品详情单位 GaoProduct detail(String id); + /// 批量查询产品详情 + List listByIdList(Collection idList); + /// 查询启用且允许销售的产品单位 List listSellable(GaoProductPage page); 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 index e6d9bea..1ba8598 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/GaoSalesOrderService.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoSalesOrderService.java @@ -6,6 +6,7 @@ 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.api.modules.gao.vo.GaoSalesOrderUpdateRequest; import com.imyeyu.spring.bean.PageResult; import com.imyeyu.spring.service.BaseService; @@ -27,6 +28,9 @@ public interface GaoSalesOrderService extends BaseService /// 创建直接产品订单 GaoSalesOrder createDirectProductOrder(GaoDirectProductOrderCreateRequest request); + /// 只更新订单备注和附件 + GaoSalesOrder updateRemarkAndAttachments(GaoSalesOrderUpdateRequest request); + /// 按门店和幂等号查询订单 /// /// @param storeId 门店 ID diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoFundRecordServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoFundRecordServiceImplement.java index f91c4e0..8ac00da 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoFundRecordServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoFundRecordServiceImplement.java @@ -75,6 +75,16 @@ public class GaoFundRecordServiceImplement extends AbstractEntityService listByBizId(String salesOrderId) { TimiException.required(salesOrderId, "not found salesOrderId"); 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 index 0b4ec7b..1b9a334 100644 --- 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 @@ -126,7 +126,9 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService itemList = itemMapper.selectByFulfillmentId(fulfillment.getId()); + fillItemAttachments(itemList); + fulfillment.setItemList(itemList); fulfillment.setStockRecordList(stockRecordService.listByBiz(GaoProductStockRecord.BizType.ORDER_FULFILLMENT, fulfillment.getId())); return fulfillment; } @@ -659,8 +661,52 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService list) { - for (GaoOrderFulfillment fulfillment : TimiJava.safeIterable(list)) { - fulfillment.setItemList(itemMapper.selectByFulfillmentId(fulfillment.getId())); + if (list == null || list.isEmpty()) { + return; + } + List fulfillmentIdList = list.stream() + .map(GaoOrderFulfillment::getId) + .filter(value -> value != null && !value.isBlank()) + .distinct() + .toList(); + if (fulfillmentIdList.isEmpty()) { + for (GaoOrderFulfillment fulfillment : list) { + fulfillment.setItemList(List.of()); + } + return; + } + Map> itemMap = new HashMap<>(); + List queriedItemList = itemMapper.selectByFulfillmentIdList(fulfillmentIdList); + for (GaoOrderFulfillmentItem item : queriedItemList) { + itemMap.computeIfAbsent(item.getFulfillmentId(), key -> new ArrayList<>()).add(item); + } + List itemList = new ArrayList<>(); + for (GaoOrderFulfillment fulfillment : list) { + List fulfillmentItemList = itemMap.getOrDefault(fulfillment.getId(), List.of()); + fulfillment.setItemList(fulfillmentItemList); + itemList.addAll(fulfillmentItemList); + } + fillItemAttachments(itemList); + } + + private void fillItemAttachments(List itemList) { + if (itemList == null || itemList.isEmpty()) { + return; + } + List productIdList = itemList.stream() + .map(GaoOrderFulfillmentItem::getProductId) + .filter(value -> value != null && !value.isBlank()) + .distinct() + .toList(); + if (productIdList.isEmpty()) { + return; + } + Map> attachmentMap = attachmentService.mapByBizIdList( + Attachment.BizType.GAO_PRODUCT, + productIdList, + GaoProduct.AttachType.PHOTO.name()); + for (GaoOrderFulfillmentItem item : itemList) { + item.setAttachmentList(attachmentMap.getOrDefault(item.getProductId(), List.of())); } } 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 index 47da3ad..c1dfd6b 100644 --- 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 @@ -256,6 +256,7 @@ public class GaoOrderPaymentServiceImplement implements GaoOrderPaymentService { record.setBizId(order.getId()); record.setOperationType(operationType); record.setAmount(amount); + record.setSalesOrderUnpaidAmountSnapshot(safe(order.getSaleAmount()) - safe(order.getPaidAmount())); record.setRequestNo(requestNo); record.setOperatorUserId(userLoginService.getRequireLoginUserId()); record.setOccurredAt(TimiJava.defaultIfNull(occurredAt, Time.now())); 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 index 7030a81..bfb6331 100644 --- 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 @@ -29,6 +29,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; +import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -75,6 +76,16 @@ public class GaoProductServiceImplement extends AbstractEntityService listByIdList(Collection idList) { + if (idList == null || idList.isEmpty()) { + return List.of(); + } + List list = mapper.selectByIdList(idList); + fillTransient(list); + return list; + } + @Override public List listSellable(GaoProductPage page) { TimiException.required(page, "not found product page"); 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 index 62322d6..addea2c 100644 --- 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 @@ -34,6 +34,7 @@ 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.gao.vo.GaoSalesOrderUpdateRequest; import com.imyeyu.api.modules.system.entity.Logger; import com.imyeyu.api.modules.system.service.LoggerService; import com.imyeyu.api.modules.user.service.PermissionChecker; @@ -55,8 +56,10 @@ import java.math.BigDecimal; import java.math.RoundingMode; 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; @@ -100,6 +103,7 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService list = mapper.selectByQuery(page); fillItemList(list); + fillCustomer(list); result.setList(list); return result; } @@ -109,7 +113,11 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService itemList = itemMapper.selectBySalesOrderId(order.getId()); + fillItemAttachments(itemList); + order.setItemList(itemList); + fillCustomer(List.of(order)); + order.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.GAO_SALES_ORDER, order.getId())); order.setFundRecordList(fundRecordService.listByBizId(order.getId())); if (order.getSourceEventRecordId() != null) { order.setSourceEventRecord(eventRecordService.get(order.getSourceEventRecordId())); @@ -132,6 +140,19 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService createDirectInternal(request)); } + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public GaoSalesOrder updateRemarkAndAttachments(GaoSalesOrderUpdateRequest request) { + return loggerService.execute(Logger.Module.GAO, "GAO_SALES_ORDER_UPDATE", request, () -> { + TimiException.required(request, "not found sales order update request"); + TimiException.required(request.getId(), "not found sales order.id"); + GaoSalesOrder order = getForUpdate(request.getId()); + TimiException.requiredTrue(1 == mapper.updateRemark(order.getId(), request.getRemark()), "订单备注更新失败"); + attachmentService.updateByBizId(Attachment.BizType.GAO_SALES_ORDER, order.getId(), request.getAttachmentList()); + return detail(order.getId()); + }); + } + private GaoSalesOrder createDirectInternal(GaoDirectProductOrderCreateRequest request) { TimiException.required(request, "not found direct product order request"); String requestNo = requiredText(request.getRequestNo(), "not found requestNo"); @@ -436,10 +457,74 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService orderList) { + if (orderList == null || orderList.isEmpty()) { + return; + } + List orderIdList = orderList.stream() + .map(GaoSalesOrder::getId) + .filter(value -> value != null && !value.isBlank()) + .distinct() + .toList(); + Map> itemMap = new HashMap<>(); + if (!orderIdList.isEmpty()) { + List queriedItemList = itemMapper.selectBySalesOrderIdList(orderIdList); + for (GaoSalesOrderItem item : TimiJava.safeIterable(queriedItemList)) { + itemMap.computeIfAbsent(item.getSalesOrderId(), key -> new ArrayList<>()).add(item); + } + } + List itemList = new ArrayList<>(); for (GaoSalesOrder order : TimiJava.safeIterable(orderList)) { - order.setItemList(itemMapper.selectBySalesOrderId(order.getId())); + List orderItemList = itemMap.getOrDefault(order.getId(), List.of()); + order.setItemList(orderItemList); + itemList.addAll(orderItemList); order.setUnpaidAmount(safe(order.getSaleAmount()) - safe(order.getPaidAmount())); } + fillItemAttachments(itemList); + } + + private void fillItemAttachments(List itemList) { + if (itemList == null || itemList.isEmpty()) { + return; + } + List productIdList = itemList.stream() + .map(GaoSalesOrderItem::getProductId) + .filter(value -> value != null && !value.isBlank()) + .distinct() + .toList(); + if (productIdList.isEmpty()) { + return; + } + Map> attachmentMap = attachmentService.mapByBizIdList( + Attachment.BizType.GAO_PRODUCT, + productIdList, + GaoProduct.AttachType.PHOTO.name()); + for (GaoSalesOrderItem item : itemList) { + item.setAttachmentList(attachmentMap.getOrDefault(item.getProductId(), List.of())); + } + } + + private void fillCustomer(List orderList) { + Set customerIdSet = new HashSet<>(); + for (GaoSalesOrder order : TimiJava.safeIterable(orderList)) { + if (TimiJava.isNotEmpty(order.getCustomerId())) { + customerIdSet.add(order.getCustomerId()); + } + } + if (customerIdSet.isEmpty()) { + return; + } + + List customerIdList = new ArrayList<>(customerIdSet); + Map customerMap = customerService.mapByIdList(customerIdList); + Map> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_CUSTOMER, customerIdList); + for (GaoSalesOrder order : TimiJava.safeIterable(orderList)) { + GaoCustomer customer = customerMap.get(order.getCustomerId()); + if (customer == null) { + continue; + } + customer.setAttachmentList(attachmentMap.getOrDefault(customer.getId(), List.of())); + order.setCustomer(customer); + } } private BigDecimal validateQuantity(GaoProduct product, BigDecimal quantity) { diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockRecordPage.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockRecordPage.java index 7e68124..0851368 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockRecordPage.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockRecordPage.java @@ -31,6 +31,18 @@ public class GaoProductStockRecordPage extends BasePage { /// 操作类型 private GaoProductStockRecord.OperationType operationType; + /// 仅查询仍然生效的履约预占流水 + private Boolean activeReservationOnly; + + /// 库存变动方向,IN 为入库,OUT 为出库 + public enum Direction { + IN, + OUT + } + + /// 库存变动方向 + private Direction direction; + /// 起始发生时间 private Long beginAt; 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 index 32a73cd..4c87fcb 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderPage.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderPage.java @@ -28,6 +28,12 @@ public class GaoSalesOrderPage extends BasePage { /// 付款状态 private GaoSalesOrder.PaymentStatus paymentStatus; + /// 退款状态 + private GaoSalesOrder.RefundStatus refundStatus; + + /// true 为筛选可收款且未付清订单 + private Boolean pendingReceive; + /// true 为筛选已履约但未付清订单 private Boolean fulfilledButUnpaid; diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderUpdateRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderUpdateRequest.java new file mode 100644 index 0000000..237194f --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderUpdateRequest.java @@ -0,0 +1,25 @@ +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-05 +@Data +public class GaoSalesOrderUpdateRequest { + + /// 订单 ID + private String id; + + /// 订单附件 + private List attachmentList; + + /// 备注 + private String remark; +} diff --git a/src/main/java/com/imyeyu/api/modules/user/controller/RoleController.java b/src/main/java/com/imyeyu/api/modules/user/controller/RoleController.java index d0f09f6..eac4d8d 100644 --- a/src/main/java/com/imyeyu/api/modules/user/controller/RoleController.java +++ b/src/main/java/com/imyeyu/api/modules/user/controller/RoleController.java @@ -49,10 +49,10 @@ public class RoleController { @RequireCorePermission(CorePermissionCode.ROLE_READ) @PostMapping("/list") public PageResult list(@RequestBody Page page) { - Set manageableRoleIdSet = listManageableRoleId(); + Set visibleRoleIdSet = listVisibleRoleId(); PageResult result = service.page(page); if (result.getList() != null) { - result.setList(result.getList().stream().filter(role -> manageableRoleIdSet.contains(role.getId())).toList()); + result.setList(result.getList().stream().filter(role -> visibleRoleIdSet.contains(role.getId())).toList()); result.getList().forEach(this::fillRoleDetail); } return result; @@ -83,7 +83,7 @@ public class RoleController { @PostMapping("/detail") public Role detail(@RequestParam String id) { Role role = service.get(id); - requireManageableRole(role.getId()); + requireVisibleRole(role.getId()); fillRoleDetail(role); return role; } @@ -105,7 +105,7 @@ public class RoleController { @RequireCorePermission(CorePermissionCode.ROLE_PERMISSION_READ) @PostMapping("/permission/grantable/list") public List listGrantableRolePermission(@RequestParam String roleId) { - requireManageableRole(roleId); + requireVisibleRole(roleId); return service.listGrantableRolePermission(roleId); } @@ -137,6 +137,12 @@ public class RoleController { return service.listGrantableParentRole(roleId); } + @RequireCorePermission(CorePermissionCode.ROLE_CREATE) + @PostMapping("/parent/create/grantable/list") + public List listCreateGrantableParentRole(@RequestParam ModuleCode moduleCode) { + return service.listCreateGrantableParentRole(moduleCode); + } + @AOPLog @RequireCorePermission(CorePermissionCode.ROLE_UPDATE) @PostMapping("/parent/update") @@ -197,6 +203,19 @@ public class RoleController { .collect(Collectors.toSet()); } + private Set listVisibleRoleId() { + Set roleIdSet = listManageableRoleId(); + String loginUserId = userLoginService.getRequireLoginUserId(); + roleIdSet.addAll(userRoleService.listRoleByUserId(loginUserId).stream().map(Role::getId).toList()); + return roleIdSet; + } + + private void requireVisibleRole(String roleId) { + if (!listVisibleRoleId().contains(roleId)) { + throw new TimiException(TimiCode.PERMISSION_ERROR); + } + } + private void requireManageableRole(String roleId) { if (!listManageableRoleId().contains(roleId)) { throw new TimiException(TimiCode.PERMISSION_ERROR); diff --git a/src/main/java/com/imyeyu/api/modules/user/service/RoleService.java b/src/main/java/com/imyeyu/api/modules/user/service/RoleService.java index f3c5332..73f7f2f 100644 --- a/src/main/java/com/imyeyu/api/modules/user/service/RoleService.java +++ b/src/main/java/com/imyeyu/api/modules/user/service/RoleService.java @@ -75,6 +75,12 @@ public interface RoleService extends BaseService { /// @return 角色列表 List listGrantableParentRole(String roleId); + /// 查询创建模块角色时可选的管理父级角色 + /// + /// @param moduleCode 目标模块代码 + /// @return 角色列表 + List listCreateGrantableParentRole(ModuleCode moduleCode); + /// 修改角色管理父级 /// /// @param role 角色 diff --git a/src/main/java/com/imyeyu/api/modules/user/service/implement/AuthorizationBoundaryServiceImplement.java b/src/main/java/com/imyeyu/api/modules/user/service/implement/AuthorizationBoundaryServiceImplement.java index 30baa9c..e927f70 100644 --- a/src/main/java/com/imyeyu/api/modules/user/service/implement/AuthorizationBoundaryServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/user/service/implement/AuthorizationBoundaryServiceImplement.java @@ -83,11 +83,12 @@ public class AuthorizationBoundaryServiceImplement implements AuthorizationBound @Override public void checkCanCreateRole(String operatorUserId, Role role) { checkCorePermission(operatorUserId, CorePermissionCode.ROLE_CREATE); + TimiException.required(role.getModuleCode(), "not found role.moduleCode"); + TimiException.required(role.getOwnerType(), "not found role.ownerType"); + checkOwnerDefinition(role.getModuleCode(), role.getOwnerType(), role.getOwnerId()); if (authorizationScopeService.isSystemAdmin(operatorUserId)) { return; } - TimiException.required(role.getModuleCode(), "not found role.moduleCode"); - TimiException.required(role.getOwnerType(), "not found role.ownerType"); TimiException.requiredTrue(!Boolean.TRUE.equals(role.getBuiltin()), "不能创建内置角色"); TimiException.requiredTrue(!Boolean.TRUE.equals(role.getProtectedRole()), "不能创建受保护角色"); checkOwnerScope(operatorUserId, role.getModuleCode(), role.getOwnerType(), role.getOwnerId()); @@ -98,6 +99,11 @@ public class AuthorizationBoundaryServiceImplement implements AuthorizationBound checkCorePermission(operatorUserId, CorePermissionCode.ROLE_UPDATE); Role dbRole = roleMapper.selectById(role.getId()); TimiException.required(dbRole, "not found role"); + if (role.getOwnerType() != null || role.getOwnerId() != null) { + AuthOwnerType ownerType = TimiJava.defaultIfNull(role.getOwnerType(), dbRole.getOwnerType()); + String ownerId = TimiJava.defaultIfEmpty(role.getOwnerId(), dbRole.getOwnerId()); + checkOwnerDefinition(dbRole.getModuleCode(), ownerType, ownerId); + } if (authorizationScopeService.isSystemAdmin(operatorUserId)) { return; } @@ -339,6 +345,12 @@ public class AuthorizationBoundaryServiceImplement implements AuthorizationBound TimiException.requiredTrue(resolver == null || resolver.canUseOwnerScope(operatorUserId, ownerType, ownerId), "归属范围不在可管理范围内"); } + private void checkOwnerDefinition(ModuleCode moduleCode, AuthOwnerType ownerType, String ownerId) { + if (ownerType == AuthOwnerType.MODULE) { + TimiException.requiredTrue(moduleCode.name().equals(ownerId), "模块级归属 ID 必须等于模块代码"); + } + } + private void checkCorePermission(String operatorUserId, CorePermissionCode permissionCode) { TimiException.requiredTrue(hasPermission(operatorUserId, ModuleCode.CORE, permissionCode.getValue()), "权限不足:%s".formatted(permissionCode.getValue())); } diff --git a/src/main/java/com/imyeyu/api/modules/user/service/implement/RoleServiceImplement.java b/src/main/java/com/imyeyu/api/modules/user/service/implement/RoleServiceImplement.java index 99a5850..f425eab 100644 --- a/src/main/java/com/imyeyu/api/modules/user/service/implement/RoleServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/user/service/implement/RoleServiceImplement.java @@ -355,6 +355,25 @@ public class RoleServiceImplement extends AbstractEntityService im .toList(); } + @Override + public List listCreateGrantableParentRole(ModuleCode moduleCode) { + String loginUserId = userLoginService.getRequireLoginUserId(); + if (authorizationScopeService.isSystemAdmin(loginUserId)) { + return mapper.selectAll(); + } + Set directOperatorRoleIdSet = userRoleMapper.selectByUserIdList(List.of(loginUserId)) + .stream() + .map(UserRole::getRoleId) + .collect(Collectors.toSet()); + if (TimiJava.isEmpty(directOperatorRoleIdSet)) { + return List.of(); + } + return mapper.selectByIdList(directOperatorRoleIdSet) + .stream() + .filter(role -> role.getModuleCode() == moduleCode) + .toList(); + } + @Transactional(TimiServerDBConfig.ROLLBACKER) @Override public void updateRoleParent(Role role) { diff --git a/src/main/resources/db/migration/timiserver/V66__migrate_gao_legacy_consumption_events.sql b/src/main/resources/db/migration/timiserver/V66__migrate_gao_legacy_consumption_events.sql index 4321c74..91f0a51 100644 --- a/src/main/resources/db/migration/timiserver/V66__migrate_gao_legacy_consumption_events.sql +++ b/src/main/resources/db/migration/timiserver/V66__migrate_gao_legacy_consumption_events.sql @@ -1,6 +1,6 @@ ALTER TABLE `gao_point_ledger` - ADD COLUMN `sales_order_id` VARCHAR(36) NULL COMMENT '关联销售订单 ID' AFTER `trigger_source_id`, - ADD KEY `idx_gao_point_ledger_sales_order` (`sales_order_id`); + ADD COLUMN IF NOT EXISTS `sales_order_id` VARCHAR(36) NULL COMMENT '关联销售订单 ID' AFTER `trigger_source_id`, + ADD KEY IF NOT EXISTS `idx_gao_point_ledger_sales_order` (`sales_order_id`); -- 将旧规则 sourceEventId 指向的登记事件标记为退役消费事件 UPDATE `gao_event` e @@ -41,4 +41,4 @@ WHERE e.`biz_type` = 'LEGACY_CONSUMPTION' AND r.`number_value` IS NOT NULL AND r.`number_value` > 0 AND r.`deleted_at` IS NULL -ON DUPLICATE KEY UPDATE `id` = `id`; +ON DUPLICATE KEY UPDATE `id` = `gao_customer_consumption_record`.`id`; diff --git a/src/main/resources/db/migration/timiserver/V68__snapshot_gao_fund_record_order_unpaid_amount.sql b/src/main/resources/db/migration/timiserver/V68__snapshot_gao_fund_record_order_unpaid_amount.sql new file mode 100644 index 0000000..374ccf4 --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V68__snapshot_gao_fund_record_order_unpaid_amount.sql @@ -0,0 +1,2 @@ +ALTER TABLE `gao_fund_record` + ADD COLUMN IF NOT EXISTS `sales_order_unpaid_amount_snapshot` BIGINT NULL COMMENT '流水发生时销售订单剩余待收款金额快照,单位分' AFTER `amount`; diff --git a/src/main/resources/mapper/timi-server/gao/GaoCustomerPackageMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoCustomerPackageMapper.xml index ce6f6b6..035f9c4 100644 --- a/src/main/resources/mapper/timi-server/gao/GaoCustomerPackageMapper.xml +++ b/src/main/resources/mapper/timi-server/gao/GaoCustomerPackageMapper.xml @@ -26,8 +26,9 @@ diff --git a/src/main/resources/mapper/timi-server/gao/GaoFundRecordMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoFundRecordMapper.xml index d97f06f..fa509cd 100644 --- a/src/main/resources/mapper/timi-server/gao/GaoFundRecordMapper.xml +++ b/src/main/resources/mapper/timi-server/gao/GaoFundRecordMapper.xml @@ -11,6 +11,13 @@ WHERE `id` = #{id} AND `deleted_at` IS NULL FOR UPDATE + f.`deleted_at` IS NULL @@ -41,7 +57,16 @@ SELECT COUNT(1) FROM `gao_fund_record` f WHERE diff --git a/src/main/resources/mapper/timi-server/gao/GaoOrderFulfillmentItemMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoOrderFulfillmentItemMapper.xml index 2a1bd6f..0d4854b 100644 --- a/src/main/resources/mapper/timi-server/gao/GaoOrderFulfillmentItemMapper.xml +++ b/src/main/resources/mapper/timi-server/gao/GaoOrderFulfillmentItemMapper.xml @@ -6,6 +6,14 @@ WHERE `fulfillment_id` = #{fulfillmentId} ORDER BY `created_at` + + + - SELECT * FROM `gao_customer_package_item` - WHERE `customer_package_id` = #{customerPackageId} AND `deleted_at` IS NULL - ORDER BY `sort`, `created_at` - - - - - UPDATE `gao_customer_package_item` - 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` + `fulfilled_quantity` + #{delta} <= `total_quantity` - - - UPDATE `gao_customer_package_item` - SET `fulfilled_quantity` = `fulfilled_quantity` + #{delta}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) - WHERE `id` = #{id} AND `deleted_at` IS NULL - 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 035f9c4..f5c2975 100644 --- a/src/main/resources/mapper/timi-server/gao/GaoCustomerPackageMapper.xml +++ b/src/main/resources/mapper/timi-server/gao/GaoCustomerPackageMapper.xml @@ -1,11 +1,6 @@ - SELECT COUNT(1) FROM `gao_customer_package` p LEFT JOIN `gao_customer` c ON c.`id` = p.`customer_id` + INNER JOIN `gao_sales_order` o ON o.`id` = p.`sales_order_id` AND o.`store_id` = p.`store_id` AND o.`deleted_at` IS NULL WHERE + o.`deleted_at` IS NULL AND o.`store_id` = #{storeId} From 01d33a1122c5ba3ee053c0e893a3a12cdea541f6 Mon Sep 17 00:00:00 2001 From: Timi Date: Sun, 6 Sep 2026 17:56:47 +0800 Subject: [PATCH 07/10] add product/order chart --- .../controller/GaoProductChartController.java | 126 ++++++++++ .../GaoSalesOrderChartController.java | 146 +++++++++++ .../gao/mapper/GaoProductChartMapper.java | 48 ++++ .../gao/mapper/GaoSalesOrderChartMapper.java | 56 +++++ .../gao/service/GaoProductChartService.java | 48 ++++ .../service/GaoSalesOrderChartService.java | 56 +++++ .../GaoProductChartServiceImplement.java | 109 +++++++++ .../GaoSalesOrderChartServiceImplement.java | 118 +++++++++ .../vo/GaoCustomerOrderProductRankView.java | 40 +++ .../gao/vo/GaoCustomerOrderRankView.java | 35 +++ .../vo/GaoCustomerRepeatDailyStatView.java | 23 ++ .../vo/GaoCustomerRepeatIntervalStatView.java | 20 ++ .../gao/vo/GaoProductCategoryStatView.java | 20 ++ .../modules/gao/vo/GaoProductChartReq.java | 33 +++ .../GaoProductFulfillmentDailyStatView.java | 25 ++ .../gao/vo/GaoProductFulfillmentRankView.java | 37 +++ .../gao/vo/GaoProductSalesDailyStatView.java | 28 +++ .../gao/vo/GaoProductSalesRankView.java | 31 +++ .../gao/vo/GaoProductStockHealthView.java | 44 ++++ .../GaoProductStockMovementDailyStatView.java | 31 +++ .../gao/vo/GaoProductTypeStatView.java | 18 ++ .../gao/vo/GaoProductUnitSalesRankView.java | 34 +++ .../gao/vo/GaoSalesOrderCalendarStatView.java | 23 ++ .../modules/gao/vo/GaoSalesOrderChartReq.java | 36 +++ ...GaoSalesOrderConsumptionDailyStatView.java | 23 ++ .../gao/vo/GaoSalesOrderDailyStatView.java | 32 +++ .../gao/vo/GaoSalesOrderPaymentStatView.java | 33 +++ .../vo/GaoSalesOrderReceivableStatView.java | 32 +++ .../gao/vo/GaoSalesOrderStatusStatView.java | 27 ++ .../gao/vo/GaoSalesOrderTypeStatView.java | 30 +++ .../timi-server/gao/GaoProductChartMapper.xml | 191 +++++++++++++++ .../gao/GaoSalesOrderChartMapper.xml | 230 ++++++++++++++++++ 32 files changed, 1783 insertions(+) create mode 100644 src/main/java/com/imyeyu/api/modules/gao/controller/GaoProductChartController.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/controller/GaoSalesOrderChartController.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductChartMapper.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/mapper/GaoSalesOrderChartMapper.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/service/GaoProductChartService.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/service/GaoSalesOrderChartService.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductChartServiceImplement.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoSalesOrderChartServiceImplement.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerOrderProductRankView.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerOrderRankView.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerRepeatDailyStatView.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerRepeatIntervalStatView.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductCategoryStatView.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductChartReq.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductFulfillmentDailyStatView.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductFulfillmentRankView.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSalesDailyStatView.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSalesRankView.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockHealthView.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockMovementDailyStatView.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductTypeStatView.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductUnitSalesRankView.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderCalendarStatView.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderChartReq.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderConsumptionDailyStatView.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderDailyStatView.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderPaymentStatView.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderReceivableStatView.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderStatusStatView.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderTypeStatView.java create mode 100644 src/main/resources/mapper/timi-server/gao/GaoProductChartMapper.xml create mode 100644 src/main/resources/mapper/timi-server/gao/GaoSalesOrderChartMapper.xml diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoProductChartController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoProductChartController.java new file mode 100644 index 0000000..5381646 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoProductChartController.java @@ -0,0 +1,126 @@ +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.service.GaoProductChartService; +import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; +import com.imyeyu.api.modules.gao.vo.GaoProductCategoryStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductChartReq; +import com.imyeyu.api.modules.gao.vo.GaoProductFulfillmentDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductFulfillmentRankView; +import com.imyeyu.api.modules.gao.vo.GaoProductSalesDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductSalesRankView; +import com.imyeyu.api.modules.gao.vo.GaoProductStockHealthView; +import com.imyeyu.api.modules.gao.vo.GaoProductStockMovementDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductTypeStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductUnitSalesRankView; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.spring.annotation.AOPLog; +import com.imyeyu.spring.annotation.RequestRateLimit; +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.RestController; + +import java.util.List; + +/// GAO 产品图表接口 +/// +/// @author Codex +/// @since 2026-09-06 +@RestController +@RequiredArgsConstructor +@RequestMapping("/gao/product/chart") +public class GaoProductChartController { + + private final GaoProductChartService service; + private final GaoStoreScopeService storeScopeService; + + /// 查询产品每日销售统计 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/sales/daily") + public List salesDaily(@RequestBody GaoProductChartReq req) { + return service.salesDaily(normalize(req)); + } + + /// 查询产品销售排行 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/sales/rank") + public List salesRank(@RequestBody GaoProductChartReq req) { + return service.salesRank(normalize(req)); + } + + /// 查询产品每日履约统计 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/fulfillment/daily") + public List fulfillmentDaily(@RequestBody GaoProductChartReq req) { + return service.fulfillmentDaily(normalize(req)); + } + + /// 查询产品履约进度排行 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/fulfillment/rank") + public List fulfillmentRank(@RequestBody GaoProductChartReq req) { + return service.fulfillmentRank(normalize(req)); + } + + /// 查询产品每日库存变动统计 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/stock/movement/daily") + public List stockMovementDaily(@RequestBody GaoProductChartReq req) { + return service.stockMovementDaily(normalize(req)); + } + + /// 查询产品库存健康度 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/stock/health") + public List stockHealth(@RequestBody GaoProductChartReq req) { + return service.stockHealth(normalize(req)); + } + + /// 查询产品类型统计 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/structure/type") + public List type(@RequestBody GaoProductChartReq req) { + return service.type(normalize(req)); + } + + /// 查询产品分类统计 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/structure/category") + public List category(@RequestBody GaoProductChartReq req) { + return service.category(normalize(req)); + } + + /// 查询产品销售单位排行 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/sales/unit/rank") + public List unitSalesRank(@RequestBody GaoProductChartReq req) { + return service.unitSalesRank(normalize(req)); + } + + private GaoProductChartReq normalize(GaoProductChartReq req) { + TimiException.required(req, "not found product chart req"); + req.setStoreId(storeScopeService.resolveStoreId(req.getStoreId())); + return req; + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoSalesOrderChartController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoSalesOrderChartController.java new file mode 100644 index 0000000..d8417b9 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoSalesOrderChartController.java @@ -0,0 +1,146 @@ +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.service.GaoSalesOrderChartService; +import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; +import com.imyeyu.api.modules.gao.vo.GaoCustomerOrderProductRankView; +import com.imyeyu.api.modules.gao.vo.GaoCustomerOrderRankView; +import com.imyeyu.api.modules.gao.vo.GaoCustomerRepeatDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoCustomerRepeatIntervalStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderChartReq; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderCalendarStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderConsumptionDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderPaymentStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderReceivableStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderStatusStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderTypeStatView; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.spring.annotation.AOPLog; +import com.imyeyu.spring.annotation.RequestRateLimit; +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.RestController; + +import java.util.List; + +/// GAO 销售订单图表接口 +/// +/// @author Codex +/// @since 2026-09-06 +@RestController +@RequiredArgsConstructor +@RequestMapping("/gao/sales/order/chart") +public class GaoSalesOrderChartController { + + private final GaoSalesOrderChartService service; + private final GaoStoreScopeService storeScopeService; + + /// 查询销售订单每日统计 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/daily") + public List daily(@RequestBody GaoSalesOrderChartReq req) { + return service.daily(normalize(req)); + } + + /// 查询销售订单类型统计 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/type") + public List type(@RequestBody GaoSalesOrderChartReq req) { + return service.type(normalize(req)); + } + + /// 查询销售订单付款状态统计 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/payment") + public List payment(@RequestBody GaoSalesOrderChartReq req) { + return service.payment(normalize(req)); + } + + /// 查询客户订单排行 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/customer/rank") + public List customerRank(@RequestBody GaoSalesOrderChartReq req) { + return service.customerRank(normalize(req)); + } + + /// 查询指定客户订单产品排行 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/customer/product/rank") + public List customerProductRank(@RequestBody GaoSalesOrderChartReq req) { + return service.customerProductRank(normalize(req)); + } + + /// 查询销售订单状态统计 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/status") + public List status(@RequestBody GaoSalesOrderChartReq req) { + return service.status(normalize(req)); + } + + /// 查询销售订单应收统计 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/receivable") + public GaoSalesOrderReceivableStatView receivable(@RequestBody GaoSalesOrderChartReq req) { + return service.receivable(normalize(req)); + } + + /// 查询客户每日有效消费统计 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/customer/consumption/daily") + public List consumptionDaily(@RequestBody GaoSalesOrderChartReq req) { + return service.consumptionDaily(normalize(req)); + } + + /// 查询客户订单日历统计 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/customer/calendar") + public List calendar(@RequestBody GaoSalesOrderChartReq req) { + return service.calendar(normalize(req)); + } + + /// 查询客户每日复购统计 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/customer/repeat/daily") + public List repeatDaily(@RequestBody GaoSalesOrderChartReq req) { + return service.repeatDaily(normalize(req)); + } + + /// 查询客户复购间隔统计 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/customer/repeat/interval") + public List repeatInterval(@RequestBody GaoSalesOrderChartReq req) { + return service.repeatInterval(normalize(req)); + } + + private GaoSalesOrderChartReq normalize(GaoSalesOrderChartReq req) { + TimiException.required(req, "not found sales order chart req"); + req.setStoreId(storeScopeService.resolveStoreId(req.getStoreId())); + return req; + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductChartMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductChartMapper.java new file mode 100644 index 0000000..f5e0924 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductChartMapper.java @@ -0,0 +1,48 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.vo.GaoProductChartReq; +import com.imyeyu.api.modules.gao.vo.GaoProductCategoryStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductFulfillmentDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductFulfillmentRankView; +import com.imyeyu.api.modules.gao.vo.GaoProductSalesDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductSalesRankView; +import com.imyeyu.api.modules.gao.vo.GaoProductStockHealthView; +import com.imyeyu.api.modules.gao.vo.GaoProductStockMovementDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductTypeStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductUnitSalesRankView; + +import java.util.List; + +/// GAO 产品图表 Mapper +/// +/// @author Codex +/// @since 2026-09-06 +public interface GaoProductChartMapper { + + /// 查询产品每日销售统计 + List selectSalesDaily(GaoProductChartReq req); + + /// 查询产品销售排行 + List selectSalesRank(GaoProductChartReq req); + + /// 查询产品每日履约统计 + List selectFulfillmentDaily(GaoProductChartReq req); + + /// 查询产品履约进度排行 + List selectFulfillmentRank(GaoProductChartReq req); + + /// 查询产品每日库存变动统计 + List selectStockMovementDaily(GaoProductChartReq req); + + /// 查询产品库存健康度 + List selectStockHealth(GaoProductChartReq req); + + /// 查询产品类型统计 + List selectType(GaoProductChartReq req); + + /// 查询产品分类统计 + List selectCategory(GaoProductChartReq req); + + /// 查询产品销售单位排行 + List selectUnitSalesRank(GaoProductChartReq req); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoSalesOrderChartMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoSalesOrderChartMapper.java new file mode 100644 index 0000000..70c55e1 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoSalesOrderChartMapper.java @@ -0,0 +1,56 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.vo.GaoCustomerOrderProductRankView; +import com.imyeyu.api.modules.gao.vo.GaoCustomerOrderRankView; +import com.imyeyu.api.modules.gao.vo.GaoCustomerRepeatDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoCustomerRepeatIntervalStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderChartReq; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderCalendarStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderConsumptionDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderPaymentStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderReceivableStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderStatusStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderTypeStatView; + +import java.util.List; + +/// GAO 销售订单图表 Mapper +/// +/// @author Codex +/// @since 2026-09-06 +public interface GaoSalesOrderChartMapper { + + /// 查询销售订单每日统计 + List selectDaily(GaoSalesOrderChartReq req); + + /// 查询销售订单类型统计 + List selectType(GaoSalesOrderChartReq req); + + /// 查询销售订单付款状态统计 + List selectPayment(GaoSalesOrderChartReq req); + + /// 查询客户订单排行 + List selectCustomerRank(GaoSalesOrderChartReq req); + + /// 查询客户订单产品排行 + List selectCustomerProductRank(GaoSalesOrderChartReq req); + + /// 查询销售订单状态统计 + List selectStatus(GaoSalesOrderChartReq req); + + /// 查询销售订单应收统计 + GaoSalesOrderReceivableStatView selectReceivable(GaoSalesOrderChartReq req); + + /// 查询客户每日有效消费统计 + List selectConsumptionDaily(GaoSalesOrderChartReq req); + + /// 查询客户订单日历统计 + List selectCalendar(GaoSalesOrderChartReq req); + + /// 查询客户每日复购统计 + List selectRepeatDaily(GaoSalesOrderChartReq req); + + /// 查询客户复购间隔统计 + List selectRepeatInterval(GaoSalesOrderChartReq req); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductChartService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductChartService.java new file mode 100644 index 0000000..174784c --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductChartService.java @@ -0,0 +1,48 @@ +package com.imyeyu.api.modules.gao.service; + +import com.imyeyu.api.modules.gao.vo.GaoProductChartReq; +import com.imyeyu.api.modules.gao.vo.GaoProductCategoryStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductFulfillmentDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductFulfillmentRankView; +import com.imyeyu.api.modules.gao.vo.GaoProductSalesDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductSalesRankView; +import com.imyeyu.api.modules.gao.vo.GaoProductStockHealthView; +import com.imyeyu.api.modules.gao.vo.GaoProductStockMovementDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductTypeStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductUnitSalesRankView; + +import java.util.List; + +/// GAO 产品图表服务 +/// +/// @author Codex +/// @since 2026-09-06 +public interface GaoProductChartService { + + /// 查询产品每日销售统计 + List salesDaily(GaoProductChartReq req); + + /// 查询产品销售排行 + List salesRank(GaoProductChartReq req); + + /// 查询产品每日履约统计 + List fulfillmentDaily(GaoProductChartReq req); + + /// 查询产品履约进度排行 + List fulfillmentRank(GaoProductChartReq req); + + /// 查询产品每日库存变动统计 + List stockMovementDaily(GaoProductChartReq req); + + /// 查询产品库存健康度 + List stockHealth(GaoProductChartReq req); + + /// 查询产品类型统计 + List type(GaoProductChartReq req); + + /// 查询产品分类统计 + List category(GaoProductChartReq req); + + /// 查询产品销售单位排行 + List unitSalesRank(GaoProductChartReq req); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoSalesOrderChartService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoSalesOrderChartService.java new file mode 100644 index 0000000..b42328a --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoSalesOrderChartService.java @@ -0,0 +1,56 @@ +package com.imyeyu.api.modules.gao.service; + +import com.imyeyu.api.modules.gao.vo.GaoCustomerOrderProductRankView; +import com.imyeyu.api.modules.gao.vo.GaoCustomerOrderRankView; +import com.imyeyu.api.modules.gao.vo.GaoCustomerRepeatDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoCustomerRepeatIntervalStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderChartReq; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderCalendarStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderConsumptionDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderPaymentStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderReceivableStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderStatusStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderTypeStatView; + +import java.util.List; + +/// GAO 销售订单图表服务 +/// +/// @author Codex +/// @since 2026-09-06 +public interface GaoSalesOrderChartService { + + /// 查询销售订单每日统计 + List daily(GaoSalesOrderChartReq req); + + /// 查询销售订单类型统计 + List type(GaoSalesOrderChartReq req); + + /// 查询销售订单付款状态统计 + List payment(GaoSalesOrderChartReq req); + + /// 查询客户订单排行 + List customerRank(GaoSalesOrderChartReq req); + + /// 查询客户订单产品排行 + List customerProductRank(GaoSalesOrderChartReq req); + + /// 查询销售订单状态统计 + List status(GaoSalesOrderChartReq req); + + /// 查询销售订单应收统计 + GaoSalesOrderReceivableStatView receivable(GaoSalesOrderChartReq req); + + /// 查询客户每日有效消费统计 + List consumptionDaily(GaoSalesOrderChartReq req); + + /// 查询客户订单日历统计 + List calendar(GaoSalesOrderChartReq req); + + /// 查询客户每日复购统计 + List repeatDaily(GaoSalesOrderChartReq req); + + /// 查询客户复购间隔统计 + List repeatInterval(GaoSalesOrderChartReq req); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductChartServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductChartServiceImplement.java new file mode 100644 index 0000000..9779df6 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductChartServiceImplement.java @@ -0,0 +1,109 @@ +package com.imyeyu.api.modules.gao.service.implement; + +import com.imyeyu.api.modules.gao.mapper.GaoProductChartMapper; +import com.imyeyu.api.modules.gao.service.GaoProductChartService; +import com.imyeyu.api.modules.gao.vo.GaoProductCategoryStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductChartReq; +import com.imyeyu.api.modules.gao.vo.GaoProductFulfillmentDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductFulfillmentRankView; +import com.imyeyu.api.modules.gao.vo.GaoProductSalesDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductSalesRankView; +import com.imyeyu.api.modules.gao.vo.GaoProductStockHealthView; +import com.imyeyu.api.modules.gao.vo.GaoProductStockMovementDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductTypeStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductUnitSalesRankView; +import com.imyeyu.java.bean.timi.TimiException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +/// GAO 产品图表服务实现 +/// +/// @author Codex +/// @since 2026-09-06 +@Service +@RequiredArgsConstructor +public class GaoProductChartServiceImplement implements GaoProductChartService { + + private static final int DEFAULT_TOP_N = 20; + private static final int MAX_TOP_N = 100; + + private final GaoProductChartMapper mapper; + + @Override + public List salesDaily(GaoProductChartReq req) { + checkRangeReq(req); + return mapper.selectSalesDaily(req); + } + + @Override + public List salesRank(GaoProductChartReq req) { + checkRangeReq(req); + normalizeTopN(req); + return mapper.selectSalesRank(req); + } + + @Override + public List fulfillmentDaily(GaoProductChartReq req) { + checkRangeReq(req); + return mapper.selectFulfillmentDaily(req); + } + + @Override + public List fulfillmentRank(GaoProductChartReq req) { + checkRangeReq(req); + normalizeTopN(req); + return mapper.selectFulfillmentRank(req); + } + + @Override + public List stockMovementDaily(GaoProductChartReq req) { + checkRangeReq(req); + return mapper.selectStockMovementDaily(req); + } + + @Override + public List stockHealth(GaoProductChartReq req) { + checkStoreReq(req); + normalizeTopN(req); + return mapper.selectStockHealth(req); + } + + @Override + public List type(GaoProductChartReq req) { + checkStoreReq(req); + return mapper.selectType(req); + } + + @Override + public List category(GaoProductChartReq req) { + checkStoreReq(req); + return mapper.selectCategory(req); + } + + @Override + public List unitSalesRank(GaoProductChartReq req) { + checkRangeReq(req); + normalizeTopN(req); + return mapper.selectUnitSalesRank(req); + } + + private void checkRangeReq(GaoProductChartReq req) { + checkStoreReq(req); + TimiException.required(req.getBeginAt(), "产品图表开始时间不能为空"); + TimiException.required(req.getEndAt(), "产品图表结束时间不能为空"); + TimiException.requiredTrue(req.getBeginAt() <= req.getEndAt(), "产品图表结束时间早于开始时间"); + } + + private void checkStoreReq(GaoProductChartReq req) { + TimiException.required(req, "产品图表请求不能为空"); + TimiException.required(req.getStoreId(), "产品图表门店不能为空"); + } + + private void normalizeTopN(GaoProductChartReq req) { + int topN = req.getTopN() == null ? DEFAULT_TOP_N : req.getTopN(); + TimiException.requiredTrue(0 < topN && topN < MAX_TOP_N + 1, "产品图表排行数量无效"); + req.setTopN(topN); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoSalesOrderChartServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoSalesOrderChartServiceImplement.java new file mode 100644 index 0000000..7920820 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoSalesOrderChartServiceImplement.java @@ -0,0 +1,118 @@ +package com.imyeyu.api.modules.gao.service.implement; + +import com.imyeyu.api.modules.gao.mapper.GaoSalesOrderChartMapper; +import com.imyeyu.api.modules.gao.service.GaoSalesOrderChartService; +import com.imyeyu.api.modules.gao.vo.GaoCustomerOrderProductRankView; +import com.imyeyu.api.modules.gao.vo.GaoCustomerOrderRankView; +import com.imyeyu.api.modules.gao.vo.GaoCustomerRepeatDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoCustomerRepeatIntervalStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderChartReq; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderCalendarStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderConsumptionDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderPaymentStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderReceivableStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderStatusStatView; +import com.imyeyu.api.modules.gao.vo.GaoSalesOrderTypeStatView; +import com.imyeyu.java.bean.timi.TimiException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +/// GAO 销售订单图表服务实现 +/// +/// @author Codex +/// @since 2026-09-06 +@Service +@RequiredArgsConstructor +public class GaoSalesOrderChartServiceImplement implements GaoSalesOrderChartService { + + private static final int DEFAULT_TOP_N = 20; + private static final int MAX_TOP_N = 100; + + private final GaoSalesOrderChartMapper mapper; + + @Override + public List daily(GaoSalesOrderChartReq req) { + checkRangeReq(req); + return mapper.selectDaily(req); + } + + @Override + public List type(GaoSalesOrderChartReq req) { + checkRangeReq(req); + return mapper.selectType(req); + } + + @Override + public List payment(GaoSalesOrderChartReq req) { + checkRangeReq(req); + return mapper.selectPayment(req); + } + + @Override + public List customerRank(GaoSalesOrderChartReq req) { + checkRangeReq(req); + normalizeTopN(req); + return mapper.selectCustomerRank(req); + } + + @Override + public List customerProductRank(GaoSalesOrderChartReq req) { + checkRangeReq(req); + TimiException.required(req.getCustomerId(), "客户订单产品排行必须指定客户"); + normalizeTopN(req); + return mapper.selectCustomerProductRank(req); + } + + @Override + public List status(GaoSalesOrderChartReq req) { + checkRangeReq(req); + return mapper.selectStatus(req); + } + + @Override + public GaoSalesOrderReceivableStatView receivable(GaoSalesOrderChartReq req) { + checkRangeReq(req); + return mapper.selectReceivable(req); + } + + @Override + public List consumptionDaily(GaoSalesOrderChartReq req) { + checkRangeReq(req); + return mapper.selectConsumptionDaily(req); + } + + @Override + public List calendar(GaoSalesOrderChartReq req) { + checkRangeReq(req); + return mapper.selectCalendar(req); + } + + @Override + public List repeatDaily(GaoSalesOrderChartReq req) { + checkRangeReq(req); + return mapper.selectRepeatDaily(req); + } + + @Override + public List repeatInterval(GaoSalesOrderChartReq req) { + checkRangeReq(req); + return mapper.selectRepeatInterval(req); + } + + private void checkRangeReq(GaoSalesOrderChartReq req) { + TimiException.required(req, "订单图表请求不能为空"); + TimiException.required(req.getStoreId(), "订单图表门店不能为空"); + TimiException.required(req.getBeginAt(), "订单图表开始时间不能为空"); + TimiException.required(req.getEndAt(), "订单图表结束时间不能为空"); + TimiException.requiredTrue(req.getBeginAt() <= req.getEndAt(), "订单图表结束时间早于开始时间"); + } + + private void normalizeTopN(GaoSalesOrderChartReq req) { + int topN = req.getTopN() == null ? DEFAULT_TOP_N : req.getTopN(); + TimiException.requiredTrue(0 < topN && topN < MAX_TOP_N + 1, "订单图表排行数量无效"); + req.setTopN(topN); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerOrderProductRankView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerOrderProductRankView.java new file mode 100644 index 0000000..ee0ebbd --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerOrderProductRankView.java @@ -0,0 +1,40 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +/// 客户订单产品排行视图 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoCustomerOrderProductRankView { + + /// 产品 ID + private String productId; + + /// 产品名称 + private String productName; + + /// 订单数 + private Long orderCount; + + /// 直接产品购买数量 + private BigDecimal directQuantity; + + /// 套餐权益数量 + private BigDecimal packageQuantity; + + /// 已预占数量 + private BigDecimal reservedQuantity; + + /// 已履约数量 + private BigDecimal fulfilledQuantity; + + /// 当前可履约数量 + private BigDecimal availableQuantity; + + /// 直接产品销售金额,单位分 + private Long directSaleAmount; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerOrderRankView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerOrderRankView.java new file mode 100644 index 0000000..55ebd03 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerOrderRankView.java @@ -0,0 +1,35 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +/// 客户订单排行视图 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoCustomerOrderRankView { + + /// 客户 ID + private String customerId; + + /// 客户编码 + private String customerCode; + + /// 客户名称 + private String customerName; + + /// 订单数 + private Long orderCount; + + /// 成交金额,单位分 + private Long saleAmount; + + /// 有效收款,单位分 + private Long paidAmount; + + /// 有效退款,单位分 + private Long refundedAmount; + + /// 净收款,单位分 + private Long netPaidAmount; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerRepeatDailyStatView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerRepeatDailyStatView.java new file mode 100644 index 0000000..b0b40bb --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerRepeatDailyStatView.java @@ -0,0 +1,23 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +/// 客户复购每日统计视图 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoCustomerRepeatDailyStatView { + + /// 日期值,格式为 yyyyMMdd + private Integer dateValue; + + /// 当日购买客户数 + private Long customerCount; + + /// 当日复购客户数 + private Long repeatCustomerCount; + + /// 当日复购订单数 + private Long repeatOrderCount; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerRepeatIntervalStatView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerRepeatIntervalStatView.java new file mode 100644 index 0000000..de99bce --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerRepeatIntervalStatView.java @@ -0,0 +1,20 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +/// 客户复购间隔统计视图 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoCustomerRepeatIntervalStatView { + + /// 复购间隔天数 + private Long intervalDays; + + /// 发生复购的客户数 + private Long customerCount; + + /// 复购订单数 + private Long orderCount; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductCategoryStatView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductCategoryStatView.java new file mode 100644 index 0000000..7536aa2 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductCategoryStatView.java @@ -0,0 +1,20 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +/// 产品分类统计视图 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoProductCategoryStatView { + + /// 分类 ID + private String categoryId; + + /// 分类名称 + private String categoryName; + + /// 产品数量 + private Long productCount; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductChartReq.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductChartReq.java new file mode 100644 index 0000000..619ecb1 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductChartReq.java @@ -0,0 +1,33 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.gao.entity.GaoProduct; +import lombok.Data; + +/// GAO 产品图表查询请求 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoProductChartReq { + + /// 门店 ID + private String storeId; + + /// 产品 ID + private String productId; + + /// 分类 ID + private String categoryId; + + /// 产品类型 + private GaoProduct.ProductType productType; + + /// 开始时间,包含 + private Long beginAt; + + /// 结束时间,包含 + private Long endAt; + + /// 返回排行数量 + private Integer topN; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductFulfillmentDailyStatView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductFulfillmentDailyStatView.java new file mode 100644 index 0000000..598c81b --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductFulfillmentDailyStatView.java @@ -0,0 +1,25 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +/// 产品每日履约统计视图 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoProductFulfillmentDailyStatView { + + /// 日期值,格式为 yyyyMMdd + private Integer dateValue; + + /// 直接产品履约数量 + private BigDecimal directQuantity; + + /// 套餐权益履约数量 + private BigDecimal packageQuantity; + + /// 履约基础单位数量 + private BigDecimal baseQuantity; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductFulfillmentRankView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductFulfillmentRankView.java new file mode 100644 index 0000000..0060243 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductFulfillmentRankView.java @@ -0,0 +1,37 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +/// 产品履约进度排行视图 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoProductFulfillmentRankView { + + /// 产品 ID + private String productId; + + /// 产品名称 + private String productName; + + /// 订单数 + private Long orderCount; + + /// 直接产品购买数量 + private BigDecimal directQuantity; + + /// 套餐权益数量 + private BigDecimal packageQuantity; + + /// 待履约数量 + private BigDecimal reservedQuantity; + + /// 已履约数量 + private BigDecimal fulfilledQuantity; + + /// 当前可履约数量 + private BigDecimal availableQuantity; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSalesDailyStatView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSalesDailyStatView.java new file mode 100644 index 0000000..548f56c --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSalesDailyStatView.java @@ -0,0 +1,28 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +/// 产品每日销售统计视图 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoProductSalesDailyStatView { + + /// 日期值,格式为 yyyyMMdd + private Integer dateValue; + + /// 包含该产品的订单数 + private Long orderCount; + + /// 直接产品销售数量 + private BigDecimal directQuantity; + + /// 套餐权益数量 + private BigDecimal packageQuantity; + + /// 直接产品销售金额,单位分 + private Long directSaleAmount; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSalesRankView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSalesRankView.java new file mode 100644 index 0000000..e45b61a --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSalesRankView.java @@ -0,0 +1,31 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +/// 产品销售排行视图 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoProductSalesRankView { + + /// 产品 ID + private String productId; + + /// 产品名称 + private String productName; + + /// 包含该产品的订单数 + private Long orderCount; + + /// 直接产品销售数量 + private BigDecimal directQuantity; + + /// 套餐权益数量 + private BigDecimal packageQuantity; + + /// 直接产品销售金额,单位分 + private Long directSaleAmount; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockHealthView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockHealthView.java new file mode 100644 index 0000000..c4bc5df --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockHealthView.java @@ -0,0 +1,44 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.gao.entity.GaoProduct; +import lombok.Data; + +import java.math.BigDecimal; + +/// 产品库存健康度视图 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoProductStockHealthView { + + /// 产品 ID + private String productId; + + /// 产品名称 + private String productName; + + /// 分类 ID + private String categoryId; + + /// 分类名称 + private String categoryName; + + /// 产品类型 + private GaoProduct.ProductType productType; + + /// 实际库存,基础单位 + private BigDecimal onHandQuantity; + + /// 预占库存,基础单位 + private BigDecimal reservedQuantity; + + /// 可用库存,基础单位 + private BigDecimal availableQuantity; + + /// 最低库存,基础单位 + private BigDecimal minStockQuantity; + + /// 1 为低于最低库存,0 为正常 + private Integer lowStock; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockMovementDailyStatView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockMovementDailyStatView.java new file mode 100644 index 0000000..c5b5e8b --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockMovementDailyStatView.java @@ -0,0 +1,31 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +/// 产品每日库存变动统计视图 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoProductStockMovementDailyStatView { + + /// 日期值,格式为 yyyyMMdd + private Integer dateValue; + + /// 实际入库数量,基础单位 + private BigDecimal inQuantity; + + /// 实际出库数量,基础单位 + private BigDecimal outQuantity; + + /// 实际库存净变化,基础单位 + private BigDecimal netOnHandQuantity; + + /// 新增预占数量,基础单位 + private BigDecimal reserveQuantity; + + /// 释放预占数量,基础单位 + private BigDecimal releaseQuantity; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductTypeStatView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductTypeStatView.java new file mode 100644 index 0000000..0009b94 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductTypeStatView.java @@ -0,0 +1,18 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.gao.entity.GaoProduct; +import lombok.Data; + +/// 产品类型统计视图 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoProductTypeStatView { + + /// 产品类型 + private GaoProduct.ProductType productType; + + /// 产品数量 + private Long productCount; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductUnitSalesRankView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductUnitSalesRankView.java new file mode 100644 index 0000000..f2246f2 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductUnitSalesRankView.java @@ -0,0 +1,34 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +/// 产品销售单位排行视图 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoProductUnitSalesRankView { + + /// 产品 ID + private String productId; + + /// 产品名称 + private String productName; + + /// 产品单位 ID + private String productUnitId; + + /// 单位名称 + private String unitName; + + /// 订单数 + private Long orderCount; + + /// 销售数量 + private BigDecimal quantity; + + /// 销售金额,单位分 + private Long saleAmount; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderCalendarStatView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderCalendarStatView.java new file mode 100644 index 0000000..de30244 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderCalendarStatView.java @@ -0,0 +1,23 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +/// 客户订单日历统计视图 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoSalesOrderCalendarStatView { + + /// 日期值,格式为 yyyyMMdd + private Integer dateValue; + + /// 订单数 + private Long orderCount; + + /// 成交金额,单位分 + private Long saleAmount; + + /// 净收款,单位分 + private Long netPaidAmount; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderChartReq.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderChartReq.java new file mode 100644 index 0000000..7f7825b --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderChartReq.java @@ -0,0 +1,36 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.gao.entity.GaoSalesOrder; +import lombok.Data; + +/// GAO 销售订单图表查询请求 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoSalesOrderChartReq { + + /// 门店 ID + private String storeId; + + /// 客户 ID + private String customerId; + + /// 订单类型 + private GaoSalesOrder.OrderType orderType; + + /// 订单状态 + private GaoSalesOrder.Status status; + + /// 付款状态 + private GaoSalesOrder.PaymentStatus paymentStatus; + + /// 开始时间,包含 + private Long beginAt; + + /// 结束时间,包含 + private Long endAt; + + /// 返回排行数量 + private Integer topN; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderConsumptionDailyStatView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderConsumptionDailyStatView.java new file mode 100644 index 0000000..6e6aa06 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderConsumptionDailyStatView.java @@ -0,0 +1,23 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +/// 客户每日有效消费统计视图 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoSalesOrderConsumptionDailyStatView { + + /// 日期值,格式为 yyyyMMdd + private Integer dateValue; + + /// 有效消费记录数 + private Long recordCount; + + /// 有效消费金额,单位分 + private Long amount; + + /// 截止当日累计有效消费金额,单位分 + private Long cumulativeAmount; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderDailyStatView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderDailyStatView.java new file mode 100644 index 0000000..c8b6b31 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderDailyStatView.java @@ -0,0 +1,32 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +/// 销售订单每日统计视图 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoSalesOrderDailyStatView { + + /// 日期值,格式为 yyyyMMdd + private Integer dateValue; + + /// 订单数 + private Long orderCount; + + /// 成交金额,单位分 + private Long saleAmount; + + /// 有效收款,单位分 + private Long paidAmount; + + /// 待核销收款,单位分 + private Long pendingPaidAmount; + + /// 有效退款,单位分 + private Long refundedAmount; + + /// 净收款,单位分 + private Long netPaidAmount; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderPaymentStatView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderPaymentStatView.java new file mode 100644 index 0000000..af68e5f --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderPaymentStatView.java @@ -0,0 +1,33 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.gao.entity.GaoSalesOrder; +import lombok.Data; + +/// 销售订单付款状态统计视图 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoSalesOrderPaymentStatView { + + /// 付款状态 + private GaoSalesOrder.PaymentStatus paymentStatus; + + /// 订单数 + private Long orderCount; + + /// 成交金额,单位分 + private Long saleAmount; + + /// 有效收款,单位分 + private Long paidAmount; + + /// 待核销收款,单位分 + private Long pendingPaidAmount; + + /// 未收金额,单位分 + private Long unpaidAmount; + + /// 有效退款,单位分 + private Long refundedAmount; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderReceivableStatView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderReceivableStatView.java new file mode 100644 index 0000000..b306990 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderReceivableStatView.java @@ -0,0 +1,32 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +/// 销售订单应收统计视图 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoSalesOrderReceivableStatView { + + /// 订单数 + private Long orderCount; + + /// 成交金额,单位分 + private Long saleAmount; + + /// 有效收款,单位分 + private Long paidAmount; + + /// 待核销收款,单位分 + private Long pendingPaidAmount; + + /// 未收金额,单位分 + private Long unpaidAmount; + + /// 有效退款,单位分 + private Long refundedAmount; + + /// 净收款,单位分 + private Long netPaidAmount; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderStatusStatView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderStatusStatView.java new file mode 100644 index 0000000..d5abe12 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderStatusStatView.java @@ -0,0 +1,27 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.gao.entity.GaoSalesOrder; +import lombok.Data; + +/// 销售订单状态统计视图 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoSalesOrderStatusStatView { + + /// 订单状态 + private GaoSalesOrder.Status status; + + /// 订单数 + private Long orderCount; + + /// 成交金额,单位分 + private Long saleAmount; + + /// 有效收款,单位分 + private Long paidAmount; + + /// 有效退款,单位分 + private Long refundedAmount; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderTypeStatView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderTypeStatView.java new file mode 100644 index 0000000..db5d249 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderTypeStatView.java @@ -0,0 +1,30 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.gao.entity.GaoSalesOrder; +import lombok.Data; + +/// 销售订单类型统计视图 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoSalesOrderTypeStatView { + + /// 订单类型 + private GaoSalesOrder.OrderType orderType; + + /// 订单数 + private Long orderCount; + + /// 成交金额,单位分 + private Long saleAmount; + + /// 有效收款,单位分 + private Long paidAmount; + + /// 有效退款,单位分 + private Long refundedAmount; + + /// 净收款,单位分 + private Long netPaidAmount; +} diff --git a/src/main/resources/mapper/timi-server/gao/GaoProductChartMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoProductChartMapper.xml new file mode 100644 index 0000000..6c2e9e2 --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoProductChartMapper.xml @@ -0,0 +1,191 @@ + + + + + AND p.`deleted_at` IS NULL + AND p.`store_id` = #{storeId} + AND p.`id` = #{productId} + AND p.`category_id` = #{categoryId} + AND p.`product_type` = #{productType} + + + + AND o.`deleted_at` IS NULL + AND i.`deleted_at` IS NULL + AND o.`status` <> 'CANCELLED' + AND o.`store_id` = #{storeId} + AND i.`product_id` = #{productId} + AND p.`category_id` = #{categoryId} + AND p.`product_type` = #{productType} + AND o.`purchased_at` >= #{beginAt} + AND o.`purchased_at` <= #{endAt} + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/mapper/timi-server/gao/GaoSalesOrderChartMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoSalesOrderChartMapper.xml new file mode 100644 index 0000000..42d90ef --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoSalesOrderChartMapper.xml @@ -0,0 +1,230 @@ + + + + + AND 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.`purchased_at` >= #{beginAt} + AND o.`purchased_at` <= #{endAt} + + + + + + + + + + + + + + + + + + + + + + AND 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.`status` <> 'CANCELLED' + + AND o.`purchased_at` <= #{endAt} + + + + + + From 3a4386261c498d01ddb7dce3bb22cea7db5cc572 Mon Sep 17 00:00:00 2001 From: Timi Date: Mon, 7 Sep 2026 11:38:41 +0800 Subject: [PATCH 08/10] add gao user remind --- .../modules/common/mapper/NotifyMapper.java | 5 + .../modules/common/service/NotifyService.java | 8 + .../implement/NotifyServiceImplement.java | 5 + .../gao/mapper/GaoOrderReminderMapper.java | 27 +++ .../api/modules/gao/mapper/GaoUserMapper.java | 6 + .../gao/service/GaoOrderReminderService.java | 14 ++ .../GaoOrderReminderServiceImplement.java | 174 ++++++++++++++++++ .../gao/task/GaoOrderReminderTask.java | 41 +++++ .../gao/vo/GaoOrderReminderCustomer.java | 20 ++ .../gao/GaoOrderReminderMapper.xml | 44 +++++ .../mapper/timi-server/gao/GaoUserMapper.xml | 12 ++ 11 files changed, 356 insertions(+) create mode 100644 src/main/java/com/imyeyu/api/modules/gao/mapper/GaoOrderReminderMapper.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/service/GaoOrderReminderService.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoOrderReminderServiceImplement.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/task/GaoOrderReminderTask.java create mode 100644 src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderReminderCustomer.java create mode 100644 src/main/resources/mapper/timi-server/gao/GaoOrderReminderMapper.xml diff --git a/src/main/java/com/imyeyu/api/modules/common/mapper/NotifyMapper.java b/src/main/java/com/imyeyu/api/modules/common/mapper/NotifyMapper.java index 10ff654..5636f9f 100644 --- a/src/main/java/com/imyeyu/api/modules/common/mapper/NotifyMapper.java +++ b/src/main/java/com/imyeyu/api/modules/common/mapper/NotifyMapper.java @@ -3,6 +3,7 @@ package com.imyeyu.api.modules.common.mapper; import com.imyeyu.api.modules.common.entity.Notify; import com.imyeyu.api.modules.common.vo.NotifyManagementItem; import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; import com.imyeyu.spring.mapper.BaseMapper; import java.util.List; @@ -15,6 +16,10 @@ import java.util.List; */ public interface NotifyMapper extends BaseMapper { + /// 查询业务提醒是否已创建 + @Select("SELECT EXISTS (SELECT 1 FROM `notify` WHERE `biz_type` = #{bizType} AND `biz_reminded_type` = #{bizRemindedType} AND `biz_reminded_id` = #{bizRemindedId} AND `deleted_at` IS NULL)") + boolean existsByBizReminded(@Param("bizType") String bizType, @Param("bizRemindedType") String bizRemindedType, @Param("bizRemindedId") String bizRemindedId); + /// 分页查询管理端通知 /// /// @param userId 管理员用户 ID diff --git a/src/main/java/com/imyeyu/api/modules/common/service/NotifyService.java b/src/main/java/com/imyeyu/api/modules/common/service/NotifyService.java index 4b41082..4b4bd09 100644 --- a/src/main/java/com/imyeyu/api/modules/common/service/NotifyService.java +++ b/src/main/java/com/imyeyu/api/modules/common/service/NotifyService.java @@ -14,6 +14,14 @@ import com.imyeyu.spring.service.BaseService; */ public interface NotifyService extends BaseService { + /// 查询业务提醒是否已创建 + /// + /// @param bizType 业务类型 + /// @param bizRemindedType 被提醒业务类型 + /// @param bizRemindedId 被提醒业务 ID + /// @return true 为已创建 + boolean existsByBizReminded(String bizType, String bizRemindedType, String bizRemindedId); + /// 分页查询当前用户创建的通知 /// /// @param page 分页参数 diff --git a/src/main/java/com/imyeyu/api/modules/common/service/implement/NotifyServiceImplement.java b/src/main/java/com/imyeyu/api/modules/common/service/implement/NotifyServiceImplement.java index 85ae375..fc631bd 100644 --- a/src/main/java/com/imyeyu/api/modules/common/service/implement/NotifyServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/common/service/implement/NotifyServiceImplement.java @@ -46,6 +46,11 @@ public class NotifyServiceImplement extends AbstractEntityService selectOrderReceiveCustomers(); + + /// 查询待核销收款客户 + /// + /// @return 收款客户列表 + List selectReceiveVerifyCustomers(); + + /// 查询待核销履约客户 + /// + /// @return 履约客户列表 + List selectFulfillmentVerifyCustomers(); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoUserMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoUserMapper.java index 9ad780e..81f9bf9 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoUserMapper.java +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoUserMapper.java @@ -15,6 +15,12 @@ import java.util.List; /// @since 2026-07-30 public interface GaoUserMapper extends BaseMapper { + /// 查询门店启用中的店长 + /// + /// @param storeId 门店 ID + /// @return 店长列表 + List selectEnabledStoreManagers(@Param("storeId") String storeId); + List selectByUserIdList(Collection userIdList); List selectRecipientPage(@Param("page") Page page); diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoOrderReminderService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoOrderReminderService.java new file mode 100644 index 0000000..ead6e45 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoOrderReminderService.java @@ -0,0 +1,14 @@ +package com.imyeyu.api.modules.gao.service; + +/// GAO 订单提醒服务 +/// +/// @author Codex +/// @since 2026-09-06 +public interface GaoOrderReminderService { + + /// 发送订单收款提醒 + void sendOrderReceiveReminder(); + + /// 发送核销提醒 + void sendVerifyReminder(); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoOrderReminderServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoOrderReminderServiceImplement.java new file mode 100644 index 0000000..0db3907 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoOrderReminderServiceImplement.java @@ -0,0 +1,174 @@ +package com.imyeyu.api.modules.gao.service.implement; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.imyeyu.api.modules.common.entity.Notify; +import com.imyeyu.api.modules.common.entity.NotifyDetail; +import com.imyeyu.api.modules.common.service.NotifyService; +import com.imyeyu.api.modules.gao.entity.GaoUser; +import com.imyeyu.api.modules.gao.mapper.GaoOrderReminderMapper; +import com.imyeyu.api.modules.gao.mapper.GaoUserMapper; +import com.imyeyu.api.modules.gao.service.GaoOrderReminderService; +import com.imyeyu.api.modules.gao.vo.GaoOrderReminderCustomer; +import com.imyeyu.api.modules.user.entity.User; +import com.imyeyu.api.modules.user.service.UserService; +import com.imyeyu.utils.Time; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/// GAO 订单提醒服务实现 +/// +/// @author Codex +/// @since 2026-09-06 +@Slf4j +@Service +@RequiredArgsConstructor +public class GaoOrderReminderServiceImplement implements GaoOrderReminderService { + + private static final String NOTIFY_BIZ_TYPE = "GAO_ORDER_REMINDER"; + private static final String SMS_TEMPLATE_ID = "2725097"; + private static final String ZONE_ID = "Asia/Shanghai"; + + private final GaoOrderReminderMapper mapper; + private final GaoUserMapper gaoUserMapper; + private final UserService userService; + private final NotifyService notifyService; + private final ObjectMapper jackson; + + @Override + public void sendOrderReceiveReminder() { + send(mapper.selectOrderReceiveCustomers(), ReminderType.ORDER_RECEIVE); + } + + @Override + public void sendVerifyReminder() { + Map> storeCustomerMap = new LinkedHashMap<>(); + Map> storeActionMap = new LinkedHashMap<>(); + mergeCustomers(storeCustomerMap, storeActionMap, mapper.selectReceiveVerifyCustomers(), "核销收款"); + mergeCustomers(storeCustomerMap, storeActionMap, mapper.selectFulfillmentVerifyCustomers(), "核销履约"); + for (Map.Entry> entry : storeCustomerMap.entrySet()) { + String action = String.join("、", storeActionMap.get(entry.getKey())); + sendStoreReminder(entry.getKey(), new ArrayList<>(entry.getValue().values()), ReminderType.VERIFY.name(), action); + } + } + + private void mergeCustomers(Map> storeCustomerMap, + Map> storeActionMap, List customerList, String action) { + if (customerList == null || customerList.isEmpty()) { + return; + } + for (GaoOrderReminderCustomer customer : customerList) { + storeCustomerMap.computeIfAbsent(customer.getStoreId(), key -> new LinkedHashMap<>()) + .putIfAbsent(customer.getCustomerId(), customer); + storeActionMap.computeIfAbsent(customer.getStoreId(), key -> new LinkedHashSet<>()).add(action); + } + } + + private void send(List customerList, ReminderType reminderType) { + if (customerList == null || customerList.isEmpty()) { + return; + } + Map> storeCustomerMap = customerList.stream() + .collect(Collectors.groupingBy(GaoOrderReminderCustomer::getStoreId, LinkedHashMap::new, Collectors.toList())); + for (Map.Entry> entry : storeCustomerMap.entrySet()) { + sendStoreReminder(entry.getKey(), entry.getValue(), reminderType.name(), reminderType.label); + } + } + + private void sendStoreReminder(String storeId, List customerList, String reminderType, + String action) { + String customerNames = formatCustomerNames(customerList); + String content = "您的客户 %s 的订单需要 %s,请留意订单状态。".formatted(customerNames, action); + List managerList = gaoUserMapper.selectEnabledStoreManagers(storeId); + if (managerList == null || managerList.isEmpty()) { + log.warn("GAO 订单提醒未找到启用中的店长: storeId[{}], reminderType[{}]", storeId, reminderType); + return; + } + List userIdList = managerList.stream().map(GaoUser::getUserId).distinct().toList(); + Map userMap = userService.mapByIdList(userIdList); + for (GaoUser manager : managerList) { + try { + User user = userMap.get(manager.getUserId()); + if (user == null) { + log.warn("GAO 订单提醒跳过不存在的店长用户: storeId[{}], userId[{}]", storeId, manager.getUserId()); + continue; + } + String remindedId = "%s:%s:%s:%s".formatted(LocalDate.now(ZoneId.of(ZONE_ID)), storeId, manager.getUserId(), reminderType); + if (notifyService.existsByBizReminded(NOTIFY_BIZ_TYPE, reminderType, remindedId)) { + continue; + } + long sendAt = Time.now(); + Notify notify = new Notify(); + notify.setBizType(NOTIFY_BIZ_TYPE); + notify.setBizRemindedType(reminderType); + notify.setBizRemindedId(remindedId); + notify.setDetailList(List.of( + createInternalDetail(manager.getUserId(), content, sendAt), + createSmsDetail(user.getPhoneNo(), customerNames, action, content, sendAt) + )); + notifyService.create(notify); + } catch (Exception e) { + log.error("GAO 订单提醒发送失败: storeId[{}], userId[{}], reminderType[{}]", storeId, manager.getUserId(), reminderType, e); + } + } + } + + private NotifyDetail createInternalDetail(String userId, String content, long sendAt) { + NotifyDetail detail = new NotifyDetail(); + detail.setMsgType(NotifyDetail.MsgType.INTERNAL); + detail.setTemplateId(SMS_TEMPLATE_ID); + detail.setSubject("订单状态提醒"); + detail.setContent(content); + detail.setContentType(NotifyDetail.ContentType.TEXT); + detail.setSendTo(userId); + detail.setSendAt(sendAt); + return detail; + } + + private NotifyDetail createSmsDetail(String phoneNo, String customerNames, String action, String content, long sendAt) { + NotifyDetail detail = new NotifyDetail(); + detail.setMsgType(NotifyDetail.MsgType.SMS); + detail.setTemplateId(SMS_TEMPLATE_ID); + detail.setContent(content); + detail.setArgs(jackson.valueToTree(List.of(customerNames, action))); + detail.setSendTo(phoneNo); + detail.setSendAt(sendAt); + return detail; + } + + private String formatCustomerNames(List customerList) { + List nameList = customerList.stream() + .map(GaoOrderReminderCustomer::getCustomerName) + .map(name -> name == null || name.isBlank() ? "未知客户" : name.trim()) + .toList(); + if (customerList.size() <= 2) { + return String.join("、", nameList); + } + return "%s、%s等 %d 位".formatted(nameList.getFirst(), nameList.get(1), customerList.size()); + } + + private enum ReminderType { + + /// 订单收款 + ORDER_RECEIVE("收款"), + + /// 统一核销 + VERIFY("核销收款、核销履约"); + + private final String label; + + ReminderType(String label) { + this.label = label; + } + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/task/GaoOrderReminderTask.java b/src/main/java/com/imyeyu/api/modules/gao/task/GaoOrderReminderTask.java new file mode 100644 index 0000000..fd5670a --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/task/GaoOrderReminderTask.java @@ -0,0 +1,41 @@ +package com.imyeyu.api.modules.gao.task; + +import com.imyeyu.api.modules.gao.service.GaoOrderReminderService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +/// GAO 订单提醒定时任务 +/// +/// @author Codex +/// @since 2026-09-06 +@Slf4j +@Service +@RequiredArgsConstructor +public class GaoOrderReminderTask { + + private static final String ZONE_ID = "Asia/Shanghai"; + + private final GaoOrderReminderService service; + + /// 每天早上八点发送订单收款提醒 + @Scheduled(cron = "0 0 8 * * ?", zone = ZONE_ID) + public void sendOrderReceiveReminder() { + execute("订单收款", service::sendOrderReceiveReminder); + } + + /// 每天晚上九点发送核销提醒 + @Scheduled(cron = "0 0 21 * * ?", zone = ZONE_ID) + public void sendVerifyReminder() { + execute("核销", service::sendVerifyReminder); + } + + private void execute(String reminderName, Runnable action) { + try { + action.run(); + } catch (Exception e) { + log.error("GAO {}提醒任务执行失败", reminderName, e); + } + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderReminderCustomer.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderReminderCustomer.java new file mode 100644 index 0000000..52f9bab --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoOrderReminderCustomer.java @@ -0,0 +1,20 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +/// GAO 订单提醒客户 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoOrderReminderCustomer { + + /// 门店 ID + private String storeId; + + /// 客户 ID + private String customerId; + + /// 客户名称 + private String customerName; +} diff --git a/src/main/resources/mapper/timi-server/gao/GaoOrderReminderMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoOrderReminderMapper.xml new file mode 100644 index 0000000..e1443e0 --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoOrderReminderMapper.xml @@ -0,0 +1,44 @@ + + + + + + + + + diff --git a/src/main/resources/mapper/timi-server/gao/GaoUserMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoUserMapper.xml index 18e84a3..04e9ab3 100644 --- a/src/main/resources/mapper/timi-server/gao/GaoUserMapper.xml +++ b/src/main/resources/mapper/timi-server/gao/GaoUserMapper.xml @@ -1,6 +1,18 @@ + + SELECT - i.`product_id` AS `productId`, + i.`product_id_snapshot` AS `productId`, MAX(i.`product_name_snapshot`) AS `productName`, COUNT(DISTINCT o.`id`) AS `orderCount`, COALESCE(SUM(CASE WHEN i.`item_type` = 'DIRECT_PRODUCT' THEN i.`quantity` ELSE 0 END), 0) AS `directQuantity`, COALESCE(SUM(CASE WHEN i.`item_type` = 'PACKAGE_ENTITLEMENT' THEN i.`quantity` ELSE 0 END), 0) AS `packageQuantity`, + COALESCE(SUM(CASE WHEN i.`item_type` = 'DIRECT_PRODUCT' THEN i.`quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1) ELSE 0 END), 0) AS `directBaseQuantity`, + COALESCE(SUM(CASE WHEN i.`item_type` = 'PACKAGE_ENTITLEMENT' THEN i.`quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1) ELSE 0 END), 0) AS `packageBaseQuantity`, COALESCE(SUM(CASE WHEN i.`item_type` = 'DIRECT_PRODUCT' THEN COALESCE(i.`line_amount`, 0) ELSE 0 END), 0) AS `directSaleAmount` FROM `gao_sales_order_item` i INNER JOIN `gao_sales_order` o ON o.`id` = i.`sales_order_id` - INNER JOIN `gao_product` p ON p.`id` = i.`product_id` AND p.`store_id` = o.`store_id` + INNER JOIN `gao_product` p ON p.`id` = i.`product_id_snapshot` AND p.`store_id` = o.`store_id` WHERE 1 = 1 - GROUP BY i.`product_id` - ORDER BY (SUM(CASE WHEN i.`item_type` = 'DIRECT_PRODUCT' THEN i.`quantity` ELSE 0 END) - + SUM(CASE WHEN i.`item_type` = 'PACKAGE_ENTITLEMENT' THEN i.`quantity` ELSE 0 END)) DESC, + GROUP BY i.`product_id_snapshot` + ORDER BY (SUM(CASE WHEN i.`item_type` = 'DIRECT_PRODUCT' THEN i.`quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1) ELSE 0 END) + + SUM(CASE WHEN i.`item_type` = 'PACKAGE_ENTITLEMENT' THEN i.`quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1) ELSE 0 END)) DESC, directSaleAmount DESC, - i.`product_id` ASC + i.`product_id_snapshot` ASC LIMIT #{topN} @@ -65,17 +74,22 @@ THEN CASE WHEN f.`source_fulfillment_id` IS NULL THEN fi.`quantity` ELSE -fi.`quantity` END ELSE 0 END), 0) AS `directQuantity`, COALESCE(SUM(CASE WHEN i.`item_type` = 'PACKAGE_ENTITLEMENT' THEN CASE WHEN f.`source_fulfillment_id` IS NULL THEN fi.`quantity` ELSE -fi.`quantity` END ELSE 0 END), 0) AS `packageQuantity`, + COALESCE(SUM(CASE WHEN i.`item_type` = 'DIRECT_PRODUCT' + THEN CASE WHEN f.`source_fulfillment_id` IS NULL THEN fi.`base_quantity` ELSE -fi.`base_quantity` END ELSE 0 END), 0) AS `directBaseQuantity`, + COALESCE(SUM(CASE WHEN i.`item_type` = 'PACKAGE_ENTITLEMENT' + THEN CASE WHEN f.`source_fulfillment_id` IS NULL THEN fi.`base_quantity` ELSE -fi.`base_quantity` END ELSE 0 END), 0) AS `packageBaseQuantity`, COALESCE(SUM(CASE WHEN f.`source_fulfillment_id` IS NULL THEN fi.`base_quantity` ELSE -fi.`base_quantity` END), 0) AS `baseQuantity` FROM `gao_order_fulfillment` f INNER JOIN `gao_order_fulfillment_item` fi ON fi.`fulfillment_id` = f.`id` INNER JOIN `gao_sales_order_item` i ON i.`id` = fi.`source_order_item_id` INNER JOIN `gao_sales_order` o ON o.`id` = f.`sales_order_id` AND o.`deleted_at` IS NULL - INNER JOIN `gao_product` p ON p.`id` = fi.`product_id` AND p.`store_id` = f.`store_id` + INNER JOIN `gao_product` p ON p.`id` = fi.`product_id_snapshot` AND p.`store_id` = f.`store_id` WHERE f.`store_id` = #{storeId} AND f.`status` = 'VERIFIED' AND f.`delivered_at` >= #{beginAt} AND f.`delivered_at` <= #{endAt} - AND fi.`product_id` = #{productId} + AND fi.`product_id_snapshot` = #{productId} + AND fi.`sku_id` = #{skuId} AND p.`category_id` = #{categoryId} AND p.`product_type` = #{productType} GROUP BY DATE_FORMAT(FROM_UNIXTIME(f.`delivered_at` / 1000), '%Y%m%d') @@ -84,21 +98,26 @@ @@ -111,11 +130,12 @@ COALESCE(SUM(CASE WHEN r.`reserved_delta` > 0 THEN r.`reserved_delta` ELSE 0 END), 0) AS `reserveQuantity`, COALESCE(SUM(CASE WHEN r.`reserved_delta` < 0 THEN -r.`reserved_delta` ELSE 0 END), 0) AS `releaseQuantity` FROM `gao_product_stock_record` r - INNER JOIN `gao_product` p ON p.`id` = r.`product_id` AND p.`store_id` = r.`store_id` + INNER JOIN `gao_product` p ON p.`id` = r.`product_id_snapshot` AND p.`store_id` = r.`store_id` WHERE r.`store_id` = #{storeId} AND r.`occurred_at` >= #{beginAt} AND r.`occurred_at` <= #{endAt} - AND r.`product_id` = #{productId} + AND r.`product_id_snapshot` = #{productId} + AND r.`sku_id` = #{skuId} AND p.`category_id` = #{categoryId} AND p.`product_type` = #{productType} GROUP BY DATE_FORMAT(FROM_UNIXTIME(r.`occurred_at` / 1000), '%Y%m%d') @@ -129,19 +149,25 @@ p.`category_id` AS `categoryId`, c.`name` AS `categoryName`, p.`product_type` AS `productType`, - COALESCE(s.`on_hand_quantity`, 0) AS `onHandQuantity`, - COALESCE(s.`reserved_quantity`, 0) AS `reservedQuantity`, - COALESCE(s.`on_hand_quantity`, 0) - COALESCE(s.`reserved_quantity`, 0) AS `availableQuantity`, - p.`min_stock_quantity` AS `minStockQuantity`, - CASE WHEN p.`min_stock_quantity` IS NOT NULL - AND COALESCE(s.`on_hand_quantity`, 0) - COALESCE(s.`reserved_quantity`, 0) < p.`min_stock_quantity` - THEN 1 ELSE 0 END AS `lowStock` + COALESCE(SUM(s.`on_hand_quantity`), 0) AS `onHandQuantity`, + COALESCE(SUM(s.`reserved_quantity`), 0) AS `reservedQuantity`, + COALESCE(SUM(s.`on_hand_quantity` - s.`reserved_quantity`), 0) AS `availableQuantity`, + CASE WHEN COUNT(k.`min_stock_quantity`) = 0 THEN NULL ELSE SUM(k.`min_stock_quantity`) END AS `minStockQuantity`, + MAX(CASE WHEN k.`min_stock_quantity` IS NOT NULL + AND COALESCE(s.`on_hand_quantity`, 0) - COALESCE(s.`reserved_quantity`, 0) < k.`min_stock_quantity` + THEN 1 ELSE 0 END) AS `lowStock` + , COUNT(k.`id`) AS `skuCount` + , SUM(CASE WHEN k.`min_stock_quantity` IS NOT NULL + AND COALESCE(s.`on_hand_quantity`, 0) - COALESCE(s.`reserved_quantity`, 0) < k.`min_stock_quantity` + THEN 1 ELSE 0 END) AS `lowStockSkuCount` FROM `gao_product` p LEFT JOIN `gao_product_category` c ON c.`id` = p.`category_id` AND c.`store_id` = p.`store_id` AND c.`deleted_at` IS NULL - LEFT JOIN `gao_product_stock` s ON s.`product_id` = p.`id` AND s.`store_id` = p.`store_id` + INNER JOIN `gao_product_sku` k ON k.`product_id` = p.`id` AND k.`deleted_at` IS NULL + LEFT JOIN `gao_product_stock` s ON s.`sku_id` = k.`id` AND s.`store_id` = p.`store_id` WHERE p.`store_id` = #{storeId} - AND p.`stock_enabled` = 1 + AND k.`stock_enabled` = 1 + GROUP BY p.`id`, p.`name`, p.`category_id`, c.`name`, p.`product_type`, p.`sort` ORDER BY `lowStock` DESC, `availableQuantity` ASC, p.`sort` ASC, p.`name` ASC LIMIT #{topN} @@ -172,20 +198,26 @@ diff --git a/src/main/resources/mapper/timi-server/gao/GaoProductSkuChartMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoProductSkuChartMapper.xml new file mode 100644 index 0000000..2f49939 --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoProductSkuChartMapper.xml @@ -0,0 +1,199 @@ + + + + + AND o.`deleted_at` IS NULL + AND i.`deleted_at` IS NULL + AND o.`status` <> 'CANCELLED' + AND o.`store_id` = #{storeId} + AND i.`product_id_snapshot` = #{productId} + AND i.`sku_id` = #{skuId} + AND p.`category_id` = #{categoryId} + AND p.`product_type` = #{productType} + AND o.`purchased_at` >= #{beginAt} + AND o.`purchased_at` <= #{endAt} + + + + + + + + + + + + + + + + diff --git a/src/main/resources/mapper/timi-server/gao/GaoProductSkuMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoProductSkuMapper.xml new file mode 100644 index 0000000..f46e775 --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoProductSkuMapper.xml @@ -0,0 +1,24 @@ + + + + + + + + + + diff --git a/src/main/resources/mapper/timi-server/gao/GaoProductSkuSpecValueMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoProductSkuSpecValueMapper.xml new file mode 100644 index 0000000..4663e47 --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoProductSkuSpecValueMapper.xml @@ -0,0 +1,6 @@ + + + + + DELETE FROM `gao_product_sku_spec_value` WHERE `sku_id`=#{skuId} + diff --git a/src/main/resources/mapper/timi-server/gao/GaoProductSkuUnitMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoProductSkuUnitMapper.xml new file mode 100644 index 0000000..5f206fe --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoProductSkuUnitMapper.xml @@ -0,0 +1,16 @@ + + + + + + + UPDATE `gao_product_sku_unit` SET `name`=#{name}, `base_quantity_per_unit`=#{baseQuantityPerUnit}, `sale_price`=#{salePrice}, `is_base`=#{isBase}, `allow_purchase`=#{allowPurchase}, `allow_sale`=#{allowSale}, `sort`=#{sort}, `updated_at`=FLOOR(UNIX_TIMESTAMP(NOW(3))*1000) WHERE `id`=#{id} AND `sku_id`=#{skuId} AND `deleted_at` IS NULL + diff --git a/src/main/resources/mapper/timi-server/gao/GaoProductSpecMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoProductSpecMapper.xml new file mode 100644 index 0000000..60f3e16 --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoProductSpecMapper.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/main/resources/mapper/timi-server/gao/GaoProductSpecValueMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoProductSpecValueMapper.xml new file mode 100644 index 0000000..1976e34 --- /dev/null +++ b/src/main/resources/mapper/timi-server/gao/GaoProductSpecValueMapper.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/main/resources/mapper/timi-server/gao/GaoProductStockMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoProductStockMapper.xml index 9b89849..c28beb9 100644 --- a/src/main/resources/mapper/timi-server/gao/GaoProductStockMapper.xml +++ b/src/main/resources/mapper/timi-server/gao/GaoProductStockMapper.xml @@ -3,55 +3,92 @@ - SELECT * FROM `gao_product_stock` - WHERE `store_id` = #{storeId} AND `product_id` = #{productId} + WHERE `store_id` = #{storeId} AND `sku_id` = #{skuId} + + INSERT IGNORE INTO `gao_product_stock` - (`id`, `store_id`, `product_id`, `on_hand_quantity`, `reserved_quantity`, `updated_by`, `created_at`) + (`id`, `store_id`, `sku_id`, `on_hand_quantity`, `reserved_quantity`, `updated_by`, `created_at`) VALUES - (#{id}, #{storeId}, #{productId}, #{onHandQuantity}, #{reservedQuantity}, #{updatedBy}, #{createdAt}) + (#{id}, #{storeId}, #{skuId}, #{onHandQuantity}, #{reservedQuantity}, #{updatedBy}, #{createdAt}) UPDATE `gao_product_stock` diff --git a/src/main/resources/mapper/timi-server/gao/GaoProductStockRecordMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoProductStockRecordMapper.xml index eb6ca3e..c8dd5c7 100644 --- a/src/main/resources/mapper/timi-server/gao/GaoProductStockRecordMapper.xml +++ b/src/main/resources/mapper/timi-server/gao/GaoProductStockRecordMapper.xml @@ -4,7 +4,7 @@ - SELECT * FROM `gao_product_stock_record` - WHERE `biz_type` = #{bizType} AND `biz_id` = #{bizId} AND `product_id` = #{productId} AND `operation_type` = #{operationType} + WHERE `biz_type` = #{bizType} AND `biz_id` = #{bizId} AND `sku_id` = #{skuId} AND `operation_type` = #{operationType} ORDER BY `occurred_at`, `id` LIMIT 1 1 = 1 AND r.`store_id` = #{storeId} - AND r.`product_id` = #{productId} + AND k.`product_id` = #{productId} AND (r.`product_name_snapshot` LIKE CONCAT('%', #{keyword}, '%') OR p.`name` LIKE CONCAT('%', #{keyword}, '%') OR p.`code` LIKE CONCAT('%', #{keyword}, '%')) AND r.`biz_type` = #{bizType} AND r.`biz_id` = #{bizId} @@ -43,7 +43,7 @@ WHERE c.`store_id` = r.`store_id` AND c.`biz_type` = r.`biz_type` AND c.`biz_id` = r.`biz_id` - AND c.`product_id` = r.`product_id` + AND c.`sku_id` = r.`sku_id` AND c.`operation_type` IN ('ORDER_RELEASE', 'ORDER_OUT') ) @@ -53,10 +53,10 @@ AND r.`occurred_at` <= #{endAt} diff --git a/src/main/resources/mapper/timi-server/gao/GaoProductUnitMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoProductUnitMapper.xml deleted file mode 100644 index 8f6c762..0000000 --- a/src/main/resources/mapper/timi-server/gao/GaoProductUnitMapper.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - UPDATE `gao_product_unit` - SET `name` = #{name}, `base_quantity_per_unit` = #{baseQuantityPerUnit}, `sale_price` = #{salePrice}, `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/GaoSalesOrderChartMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoSalesOrderChartMapper.xml index 42d90ef..a78f674 100644 --- a/src/main/resources/mapper/timi-server/gao/GaoSalesOrderChartMapper.xml +++ b/src/main/resources/mapper/timi-server/gao/GaoSalesOrderChartMapper.xml @@ -81,7 +81,7 @@ + + From 461af4d5811be0a9dcd664a93e0e9849e913fd2d Mon Sep 17 00:00:00 2001 From: Timi Date: Mon, 7 Sep 2026 15:33:29 +0800 Subject: [PATCH 10/10] v1.0.31 --- pom.xml | 2 +- .../gao/entity/GaoOrderFulfillmentItem.java | 4 ++ .../modules/gao/entity/GaoProductSpec.java | 6 +++ .../gao/entity/GaoProductSpecValue.java | 7 +++ .../gao/entity/GaoProductStockRecord.java | 4 ++ .../modules/gao/entity/GaoSalesOrderItem.java | 4 ++ .../gao/service/GaoProductSkuService.java | 13 +++++ .../GaoCustomerPackageServiceImplement.java | 1 + .../GaoOrderFulfillmentServiceImplement.java | 40 +++++++++++++--- .../implement/GaoProductServiceImplement.java | 48 +++++++++++++++++++ .../GaoProductSkuServiceImplement.java | 44 +++++++++++++++++ ...GaoProductStockRecordServiceImplement.java | 1 + .../GaoProductStockServiceImplement.java | 3 ++ .../GaoSalesOrderServiceImplement.java | 1 + .../modules/gao/vo/GaoStockChangeItem.java | 4 ++ .../V71__snapshot_gao_sku_spec_structure.sql | 5 ++ ...hot_gao_sales_order_sku_spec_structure.sql | 2 + 17 files changed, 181 insertions(+), 8 deletions(-) create mode 100644 src/main/resources/db/migration/timiserver/V71__snapshot_gao_sku_spec_structure.sql create mode 100644 src/main/resources/db/migration/timiserver/V72__snapshot_gao_sales_order_sku_spec_structure.sql diff --git a/pom.xml b/pom.xml index 73fc325..d4e9dd9 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ com.imyeyu.timiserverapi TimiServerAPI - 1.0.30 + 1.0.31 jar TimiServerAPI imyeyu.com API 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 index 8f847a1..645efbf 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoOrderFulfillmentItem.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoOrderFulfillmentItem.java @@ -1,5 +1,6 @@ package com.imyeyu.api.modules.gao.entity; +import com.fasterxml.jackson.databind.JsonNode; import com.imyeyu.api.modules.common.entity.Attachment; import com.imyeyu.spring.annotation.table.AutoUUID; import com.imyeyu.spring.annotation.table.Id; @@ -71,6 +72,9 @@ public class GaoOrderFulfillmentItem implements IDEntity, Creatable { /// 规格摘要快照 private String specSummarySnapshot; + /// SKU 规格结构快照,数组元素包含 specId、specName、specValueId、specValueName + private JsonNode skuSpecSnapshot; + /// 单位快照 private String unitSnapshot; diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductSpec.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductSpec.java index d809a67..947a1e9 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductSpec.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductSpec.java @@ -1,5 +1,6 @@ package com.imyeyu.api.modules.gao.entity; +import com.fasterxml.jackson.annotation.JsonProperty; import com.imyeyu.spring.annotation.table.Transient; import com.imyeyu.spring.entity.UUIDEntity; import lombok.Data; @@ -15,6 +16,11 @@ import java.util.List; @EqualsAndHashCode(callSuper = true) public class GaoProductSpec extends UUIDEntity { + /// 前端创建请求中的临时关联键 + @Transient + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + private String clientKey; + /// 产品 ID private String productId; diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductSpecValue.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductSpecValue.java index e6c1b40..e44ac55 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductSpecValue.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductSpecValue.java @@ -1,5 +1,7 @@ package com.imyeyu.api.modules.gao.entity; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.imyeyu.spring.annotation.table.Transient; import com.imyeyu.spring.entity.UUIDEntity; import lombok.Data; import lombok.EqualsAndHashCode; @@ -12,6 +14,11 @@ import lombok.EqualsAndHashCode; @EqualsAndHashCode(callSuper = true) public class GaoProductSpecValue extends UUIDEntity { + /// 前端创建请求中的临时关联键 + @Transient + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + private String clientKey; + /// 规格项 ID private String specId; diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductStockRecord.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductStockRecord.java index b670e31..e1f0ded 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductStockRecord.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductStockRecord.java @@ -1,5 +1,6 @@ package com.imyeyu.api.modules.gao.entity; +import com.fasterxml.jackson.databind.JsonNode; import com.imyeyu.api.modules.common.entity.Attachment; import com.imyeyu.api.modules.user.entity.User; import com.imyeyu.spring.annotation.table.AutoUUID; @@ -91,6 +92,9 @@ public class GaoProductStockRecord implements IDEntity, Creatable { /// 规格摘要快照 private String specSummarySnapshot; + /// SKU 规格结构快照,数组元素包含 specId、specName、specValueId、specValueName + private JsonNode skuSpecSnapshot; + /// 单位名称快照 private String unitSnapshot; 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 index 0920666..db2166d 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoSalesOrderItem.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoSalesOrderItem.java @@ -1,5 +1,6 @@ package com.imyeyu.api.modules.gao.entity; +import com.fasterxml.jackson.databind.JsonNode; import com.imyeyu.api.modules.common.entity.Attachment; import com.imyeyu.spring.annotation.table.Transient; import com.imyeyu.spring.entity.UUIDEntity; @@ -58,6 +59,9 @@ public class GaoSalesOrderItem extends UUIDEntity { /// 规格摘要快照 private String specSummarySnapshot; + /// SKU 规格结构快照,数组元素包含 specId、specName、specValueId、specValueName + private JsonNode skuSpecSnapshot; + /// 单位快照 private String unitSnapshot; diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductSkuService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductSkuService.java index 091b465..f7d4d21 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductSkuService.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductSkuService.java @@ -1,5 +1,6 @@ package com.imyeyu.api.modules.gao.service; +import com.fasterxml.jackson.databind.JsonNode; import com.imyeyu.api.modules.gao.entity.GaoProductSku; import com.imyeyu.api.modules.gao.entity.GaoProductSkuUnit; import com.imyeyu.api.modules.gao.vo.GaoProductSkuUnitSelection; @@ -32,6 +33,18 @@ public interface GaoProductSkuService extends BaseService /// 查询 SKU 单位 List listUnitBySkuId(String skuId); + /// 查询 SKU 规格结构快照 + /// + /// @param skuId SKU ID + /// @return SKU 规格结构快照;无规格时返回空数组 + JsonNode getSkuSpecSnapshot(String skuId); + + /// 根据已加载 SKU 查询规格结构快照 + /// + /// @param sku 已加载 SKU + /// @return SKU 规格结构快照;无规格时返回空数组 + JsonNode getSkuSpecSnapshot(GaoProductSku sku); + /// 停用 SKU void disable(String id); } 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 17178c0..d63d3e9 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 @@ -263,6 +263,7 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService fulfillmentItemList = itemMapper.selectByFulfillmentIdForUpdate(fulfillment.getId()); TimiException.requiredTrue(!fulfillmentItemList.isEmpty(), "履约单没有明细"); List stockItems = buildStockItems(order.getStoreId(), sourceList); + applySkuSpecSnapshots(stockItems, fulfillmentItemList); TimiException.requiredTrue(!stockItems.isEmpty(), "履约单没有需要占用的实物库存"); TimiException.requiredTrue(productStockService.hasSufficientForFulfillment(order.getStoreId(), stockItems), "库存仍然不足,不能准备履约"); String operatorUserId = userLoginService.getRequireLoginUserId(); @@ -240,6 +242,7 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService stockItems = buildStockItems(order.getStoreId(), sourceList); + applySkuSpecSnapshots(stockItems, fulfillmentItemList); String verifierUserId = userLoginService.getRequireLoginUserId(); if (!stockItems.isEmpty()) { productStockService.outForFulfillment(order.getStoreId(), fulfillment.getId(), stockItems, @@ -289,6 +292,7 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService stockItems = buildStockItems(order.getStoreId(), sourceList); + applySkuSpecSnapshots(stockItems, fulfillmentItemList); if (!stockItems.isEmpty()) { productStockService.releaseForFulfillment(order.getStoreId(), fulfillment.getId(), stockItems, stockRequestNo(fulfillment, "RELEASE"), userLoginService.getRequireLoginUserId()); @@ -364,6 +368,7 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService stockItems, List fulfillmentItemList) { + if (stockItems == null || stockItems.isEmpty() || fulfillmentItemList == null || fulfillmentItemList.isEmpty()) { + return; + } + Map snapshotMap = new HashMap<>(); + for (GaoOrderFulfillmentItem item : fulfillmentItemList) { + if (item.getSkuId() != null) { + snapshotMap.put(item.getSkuId(), item.getSkuSpecSnapshot()); + } + } + for (GaoStockChangeItem item : stockItems) { + JsonNode snapshot = snapshotMap.get(item.getSkuId()); + if (snapshot != null) { + item.setSkuSpecSnapshot(snapshot); + } + } + } + private List itemRequests(String fulfillmentId) { return copyRequestItems(itemMapper.selectByFulfillmentId(fulfillmentId)); } @@ -531,6 +555,7 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService { prepare(product, null); super.create(product); + Map specValueIdMap = saveSpecs(product, product.getSpecList()); + normalizeSkuSpecValueIds(product.getSkuList(), specValueIdMap); saveSkus(product, product.getSkuList()); attachmentService.updateByBizId(Attachment.BizType.GAO_PRODUCT, product.getId(), product.getAttachmentList()); return product; @@ -196,6 +202,48 @@ public class GaoProductServiceImplement extends AbstractEntityService saveSpecs(GaoProduct product, List specList) { + Map specValueIdMap = new HashMap<>(); + if (specList == null || specList.isEmpty()) { + return specValueIdMap; + } + for (int specIndex = 0; specIndex < specList.size(); specIndex++) { + GaoProductSpec spec = specList.get(specIndex); + spec.setId(null); + spec.setProductId(product.getId()); + spec.setSort(TimiJava.defaultIfNull(spec.getSort(), specIndex)); + specServiceProvider.getObject().createSpec(spec); + List valueList = spec.getValueList(); + if (valueList == null) { + continue; + } + for (int valueIndex = 0; valueIndex < valueList.size(); valueIndex++) { + GaoProductSpecValue value = valueList.get(valueIndex); + String clientKey = value.getClientKey(); + value.setId(null); + value.setSpecId(spec.getId()); + value.setSort(TimiJava.defaultIfNull(value.getSort(), valueIndex)); + specServiceProvider.getObject().createValue(value); + if (clientKey != null && !clientKey.isBlank()) { + specValueIdMap.put(clientKey, value.getId()); + } + } + } + return specValueIdMap; + } + + private void normalizeSkuSpecValueIds(List skuList, Map specValueIdMap) { + if (skuList == null || skuList.isEmpty() || specValueIdMap.isEmpty()) { + return; + } + for (GaoProductSku sku : skuList) { + List specValueIdList = sku.getSpecValueIdList(); + if (specValueIdList != null) { + sku.setSpecValueIdList(specValueIdList.stream().map(id -> specValueIdMap.getOrDefault(id, id)).toList()); + } + } + } + private void fillTransient(List list) { if (list == null || list.isEmpty()) { return; diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductSkuServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductSkuServiceImplement.java index e820abb..749979f 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductSkuServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductSkuServiceImplement.java @@ -1,5 +1,9 @@ package com.imyeyu.api.modules.gao.service.implement; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.imyeyu.api.config.dbsource.TimiServerDBConfig; import com.imyeyu.api.modules.common.entity.Attachment; import com.imyeyu.api.modules.common.service.AttachmentService; @@ -34,6 +38,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -120,6 +125,45 @@ public class GaoProductSkuServiceImplement extends AbstractEntityService specList = specMapper.selectByProductId(sku.getProductId()); + if (specList.isEmpty()) { + return JsonNodeFactory.instance.arrayNode(); + } + List specIdList = specList.stream().map(GaoProductSpec::getId).toList(); + Map> valueMap = new LinkedHashMap<>(); + for (GaoProductSpecValue value : valueMapper.selectBySpecIdList(specIdList)) { + valueMap.computeIfAbsent(value.getSpecId(), key -> new ArrayList<>()).add(value); + } + Set selectedValueIdSet = skuSpecValueMapper.selectBySkuIdList(List.of(sku.getId())).stream() + .map(GaoProductSkuSpecValue::getSpecValueId) + .collect(Collectors.toSet()); + ArrayNode result = JsonNodeFactory.instance.arrayNode(); + for (GaoProductSpec spec : specList) { + for (GaoProductSpecValue value : valueMap.getOrDefault(spec.getId(), List.of())) { + if (!selectedValueIdSet.contains(value.getId())) { + continue; + } + ObjectNode snapshot = result.addObject(); + snapshot.put("specId", spec.getId()); + snapshot.put("specName", spec.getName()); + snapshot.put("specValueId", value.getId()); + snapshot.put("specValueName", value.getName()); + } + } + return result; + } + @Transactional(TimiServerDBConfig.ROLLBACKER) @Override public void create(GaoProductSku sku) { diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductStockRecordServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductStockRecordServiceImplement.java index 4e96633..1cc297d 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductStockRecordServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductStockRecordServiceImplement.java @@ -138,4 +138,5 @@ public class GaoProductStockRecordServiceImplement extends AbstractEntityService record.setOperator(userMap.get(record.getOperatorUserId())); } } + } 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 index 06ad1c6..b2ff198 100644 --- 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 @@ -214,6 +214,7 @@ public class GaoProductStockServiceImplement implements GaoProductStockService { TimiException.requiredTrue(hasAttachment(request.getAttachmentList()), "库存操作必须上传照片"); TimiException.requiredTrue(hasText(request.getRemark()), "库存操作必须填写备注"); GaoStockChangeItem item = GaoStockChangeItem.of(product, sku, unit, quantity, baseQuantity); + item.setSkuSpecSnapshot(skuService.getSkuSpecSnapshot(sku)); List result = executeInternal(storeId, sku.getId(), List.of(item), requestNo, userLoginService.getRequireLoginUserId(), operationType, request.getRemark(), request.getAttachmentList()); return result.getFirst(); @@ -265,6 +266,7 @@ public class GaoProductStockServiceImplement implements GaoProductStockService { revoke.setSkuCodeSnapshot(source.getSkuCodeSnapshot()); revoke.setSkuNameSnapshot(source.getSkuNameSnapshot()); revoke.setSpecSummarySnapshot(source.getSpecSummarySnapshot()); + revoke.setSkuSpecSnapshot(source.getSkuSpecSnapshot()); revoke.setUnitSnapshot(source.getUnitSnapshot()); revoke.setBaseQuantityPerUnitSnapshot(source.getBaseQuantityPerUnitSnapshot()); revoke.setQuantity(source.getQuantity()); @@ -341,6 +343,7 @@ public class GaoProductStockServiceImplement implements GaoProductStockService { record.setSkuCodeSnapshot(item.getSkuCodeSnapshot()); record.setSkuNameSnapshot(item.getSkuNameSnapshot()); record.setSpecSummarySnapshot(item.getSpecSummarySnapshot()); + record.setSkuSpecSnapshot(item.getSkuSpecSnapshot()); record.setUnitSnapshot(item.getUnitSnapshot()); record.setBaseQuantityPerUnitSnapshot(item.getBaseQuantityPerUnitSnapshot()); record.setQuantity(item.getQuantity()); 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 index 7e84042..9d16dc6 100644 --- 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 @@ -379,6 +379,7 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService