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 3b586cc..deabf72 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 @@ -40,6 +40,8 @@ public class Attachment extends UUIDEntity { GAO_PRODUCT, + GAO_PRODUCT_SKU, + GAO_EVENT_RECORD, GAO_FUND_RECORD, 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 9ef9dc2..840152d 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,10 +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.entity.GaoProductSku; 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.GaoProductSkuService; import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; import com.imyeyu.api.modules.gao.vo.GaoPackageConfigPage; import com.imyeyu.api.modules.user.entity.User; @@ -37,7 +37,7 @@ public class GaoPackageConfigController { private final GaoPackageConfigService service; private final GaoEventService eventService; - private final GaoProductService productService; + private final GaoProductSkuService skuService; private final GaoStoreScopeService storeScopeService; private final UserService userService; @@ -92,20 +92,20 @@ public class GaoPackageConfigController { } private void fillProductDetails(List configList) { - List productIdList = configList.stream() + List skuIdList = configList.stream() .flatMap(config -> config.getItemList().stream()) - .map(GaoPackageConfigItem::getProductId) + .map(GaoPackageConfigItem::getSkuId) .filter(value -> value != null && !value.isBlank()) .distinct() .toList(); - if (productIdList.isEmpty()) { + if (skuIdList.isEmpty()) { return; } - Map productMap = productService.listByIdList(productIdList).stream() - .collect(java.util.stream.Collectors.toMap(GaoProduct::getId, item -> item)); + Map skuMap = skuIdList.stream() + .collect(java.util.stream.Collectors.toMap(item -> item, skuService::detail)); for (GaoPackageConfig config : configList) { for (GaoPackageConfigItem item : config.getItemList()) { - item.setProduct(productMap.get(item.getProductId())); + item.setSku(skuMap.get(item.getSkuId())); } } } 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 index df2ee06..101003b 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoProductController.java +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoProductController.java @@ -4,8 +4,13 @@ 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.entity.GaoProductSku; import com.imyeyu.api.modules.gao.service.GaoProductCategoryService; import com.imyeyu.api.modules.gao.service.GaoProductService; +import com.imyeyu.api.modules.gao.service.GaoProductSkuService; +import com.imyeyu.api.modules.gao.service.GaoProductSpecService; +import com.imyeyu.api.modules.gao.entity.GaoProductSpec; +import com.imyeyu.api.modules.gao.entity.GaoProductSpecValue; import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; import com.imyeyu.api.modules.gao.vo.GaoProductCategoryPage; import com.imyeyu.api.modules.gao.vo.GaoProductPage; @@ -33,6 +38,8 @@ public class GaoProductController { private final GaoProductCategoryService categoryService; private final GaoProductService productService; + private final GaoProductSkuService skuService; + private final GaoProductSpecService specService; private final GaoStoreScopeService storeScopeService; @AOPLog @@ -142,6 +149,93 @@ public class GaoProductController { productService.disable(id); } + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_READ) + @PostMapping("/sku/detail") + public GaoProductSku skuDetail(@RequestParam String id) { + GaoProductSku result = skuService.detail(id); + storeScopeService.checkStoreId(result.getStoreId()); + return result; + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_CREATE) + @PostMapping("/sku/create") + public String skuCreate(@RequestBody GaoProductSku sku) { + TimiException.required(sku, "not found product sku"); + GaoProduct product = productService.get(sku.getProductId()); + TimiException.required(product, "not found product"); + storeScopeService.checkStoreId(product.getStoreId()); + skuService.create(sku); + return sku.getId(); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_UPDATE) + @PostMapping("/sku/update") + public void skuUpdate(@RequestBody GaoProductSku request) { + TimiException.required(request, "not found product sku"); + TimiException.required(request.getId(), "not found product sku.id"); + GaoProductSku sku = skuService.get(request.getId()); + TimiException.required(sku, "not found product sku"); + storeScopeService.checkStoreId(sku.getStoreId()); + request.setProductId(sku.getProductId()); + skuService.update(request); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_UPDATE) + @PostMapping("/sku/disable") + public void skuDisable(@RequestParam String id) { + GaoProductSku sku = skuService.get(id); + TimiException.required(sku, "not found product sku"); + storeScopeService.checkStoreId(sku.getStoreId()); + skuService.disable(id); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_READ) + @PostMapping("/spec/list") + public List specList(@RequestParam String productId) { + GaoProduct product = productService.get(productId); + TimiException.required(product, "not found product"); + storeScopeService.checkStoreId(product.getStoreId()); + return specService.listByProductId(productId); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_UPDATE) + @PostMapping("/spec/create") + public String specCreate(@RequestBody GaoProductSpec spec) { + TimiException.required(spec, "not found product spec"); + GaoProduct product = productService.get(spec.getProductId()); + TimiException.required(product, "not found product"); + storeScopeService.checkStoreId(product.getStoreId()); + specService.createSpec(spec); + return spec.getId(); + } + + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_UPDATE) + @PostMapping("/spec/value/create") + public String specValueCreate(@RequestBody GaoProductSpecValue value) { + TimiException.required(value, "not found product spec value"); + String productId = specService.getProductIdBySpecId(value.getSpecId()); + TimiException.required(productId, "规格项不存在"); + GaoProduct product = productService.get(productId); + TimiException.required(product, "not found product"); + storeScopeService.checkStoreId(product.getStoreId()); + specService.createValue(value); + return value.getId(); + } + @AOPLog @RequestRateLimit @RequireGaoPermission(GaoPermissionCode.PRODUCT_READ) diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoProductSkuChartController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoProductSkuChartController.java new file mode 100644 index 0000000..7f6d42c --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoProductSkuChartController.java @@ -0,0 +1,106 @@ +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.GaoProductSkuChartService; +import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; +import com.imyeyu.api.modules.gao.vo.GaoProductChartReq; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuFulfillmentDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuFulfillmentRankView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuSalesDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuSalesRankView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuStockHealthView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuStockMovementDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuUnitSalesRankView; +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 SKU 图表接口 +/// +/// @author Codex +/// @since 2026-09-07 +@RestController +@RequiredArgsConstructor +@RequestMapping("/gao/product/sku/chart") +public class GaoProductSkuChartController { + + private final GaoProductSkuChartService service; + private final GaoStoreScopeService storeScopeService; + + /// 查询 SKU 每日销售统计 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/sales/daily") + public List salesDaily(@RequestBody GaoProductChartReq req) { + return service.salesDaily(normalize(req)); + } + + /// 查询 SKU 销售排行 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/sales/rank") + public List salesRank(@RequestBody GaoProductChartReq req) { + return service.salesRank(normalize(req)); + } + + /// 查询 SKU 单位销售排行 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/sales/unit/rank") + public List unitSalesRank(@RequestBody GaoProductChartReq req) { + return service.unitSalesRank(normalize(req)); + } + + /// 查询 SKU 每日履约统计 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/fulfillment/daily") + public List fulfillmentDaily(@RequestBody GaoProductChartReq req) { + return service.fulfillmentDaily(normalize(req)); + } + + /// 查询 SKU 履约进度排行 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/fulfillment/rank") + public List fulfillmentRank(@RequestBody GaoProductChartReq req) { + return service.fulfillmentRank(normalize(req)); + } + + /// 查询 SKU 每日库存变动统计 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/stock/movement/daily") + public List stockMovementDaily(@RequestBody GaoProductChartReq req) { + return service.stockMovementDaily(normalize(req)); + } + + /// 查询 SKU 库存健康度 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/stock/health") + public List stockHealth(@RequestBody GaoProductChartReq req) { + return service.stockHealth(normalize(req)); + } + + private GaoProductChartReq normalize(GaoProductChartReq req) { + TimiException.required(req, "SKU 图表请求不能为空"); + req.setStoreId(storeScopeService.resolveStoreId(req.getStoreId())); + return req; + } +} 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 c59c93c..b8e4777 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 @@ -10,6 +10,7 @@ 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.GaoProductStockProductView; import com.imyeyu.api.modules.gao.vo.GaoProductStockRecordPage; import com.imyeyu.java.bean.timi.TimiException; import com.imyeyu.spring.annotation.AOPLog; @@ -45,12 +46,22 @@ public class GaoProductStockController { return stockService.pageByQuery(page); } + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.PRODUCT_STOCK_READ) + @PostMapping("/product-list") + public PageResult productList(@RequestBody GaoProductStockPage page) { + TimiException.required(page, "not found stock product page"); + page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId())); + return stockService.pageProductByQuery(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); + public GaoProductStock detail(@RequestParam(required = false) String storeId, @RequestParam String skuId) { + return stockService.detail(storeScopeService.resolveStoreId(storeId), skuId); } @AOPLog 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 index d8417b9..a01ec67 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoSalesOrderChartController.java +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoSalesOrderChartController.java @@ -6,6 +6,7 @@ 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.GaoCustomerOrderSkuRankView; import com.imyeyu.api.modules.gao.vo.GaoCustomerRepeatDailyStatView; import com.imyeyu.api.modules.gao.vo.GaoCustomerRepeatIntervalStatView; import com.imyeyu.api.modules.gao.vo.GaoSalesOrderChartReq; @@ -84,6 +85,15 @@ public class GaoSalesOrderChartController { return service.customerProductRank(normalize(req)); } + /// 查询指定客户订单 SKU 排行 + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.STAT_READ) + @PostMapping("/customer/sku/rank") + public List customerSkuRank(@RequestBody GaoSalesOrderChartReq req) { + return service.customerSkuRank(normalize(req)); + } + /// 查询销售订单状态统计 @AOPLog @RequestRateLimit 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 1621d88..8f847a1 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 @@ -25,11 +25,11 @@ public class GaoOrderFulfillmentItem implements IDEntity, Creatable { /// 履约单 ID private String fulfillmentId; - /// 产品 ID - private String productId; + /// SKU ID + private String skuId; - /// 产品单位 ID - private String productUnitId; + /// SKU 单位 ID + private String skuUnitId; /// 产品图片附件 @Transient @@ -59,6 +59,18 @@ public class GaoOrderFulfillmentItem implements IDEntity, Creatable { /// 产品名称快照 private String productNameSnapshot; + /// 产品 ID 快照 + private String productIdSnapshot; + + /// SKU 编码快照 + private String skuCodeSnapshot; + + /// SKU 名称快照 + private String skuNameSnapshot; + + /// 规格摘要快照 + private String specSummarySnapshot; + /// 单位快照 private String unitSnapshot; 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 7562100..cb40d94 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 @@ -18,15 +18,27 @@ public class GaoPackageConfigItem extends UUIDEntity { /// 套餐配置 ID private String packageConfigId; - /// 产品 ID - private String productId; + /// SKU ID + private String skuId; - /// 产品单位 ID - private String productUnitId; + /// SKU 单位 ID + private String skuUnitId; /// 产品名称快照 private String productNameSnapshot; + /// 产品 ID 快照 + private String productIdSnapshot; + + /// SKU 编码快照 + private String skuCodeSnapshot; + + /// SKU 名称快照 + private String skuNameSnapshot; + + /// 规格摘要快照 + private String specSummarySnapshot; + /// 单位快照 private String unitSnapshot; @@ -42,7 +54,7 @@ public class GaoPackageConfigItem extends UUIDEntity { /// 备注 private String remark; - /// 产品详情 + /// SKU 详情 @Transient - private GaoProduct product; + private GaoProductSku sku; } 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 34024cb..6186823 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 @@ -6,7 +6,6 @@ import com.imyeyu.spring.entity.UUIDEntity; import lombok.Data; import lombok.EqualsAndHashCode; -import java.math.BigDecimal; import java.util.List; /// GAO 产品 @@ -68,15 +67,9 @@ public class GaoProduct extends UUIDEntity { /// true 为启用库存管理 private Boolean stockEnabled; - /// true 为允许业务数量保留小数 - private Boolean allowDecimalQuantity; - /// 状态 private Status status; - /// 最低库存数量,基础单位 - private BigDecimal minStockQuantity; - /// 排序值 private Integer sort; @@ -90,9 +83,13 @@ public class GaoProduct extends UUIDEntity { @Transient private GaoProductCategory category; - /// 产品单位列表 + /// 产品规格列表 @Transient - private List unitList; + private List specList; + + /// 产品 SKU 列表 + @Transient + private List skuList; /// 产品附件列表 @Transient diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductSku.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductSku.java new file mode 100644 index 0000000..732cc79 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductSku.java @@ -0,0 +1,83 @@ +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 产品 SKU +/// +/// @author Codex +/// @since 2026-09-06 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoProductSku extends UUIDEntity { + + /// SKU 状态 + public enum Status { + + /// 启用 + ACTIVE, + + /// 停用 + INACTIVE + } + + /// 门店 ID + private String storeId; + + /// 产品 ID + private String productId; + + /// 门店内 SKU 编码 + private String code; + + /// 条码 + private String barcode; + + /// SKU 展示名称 + private String name; + + /// 规格摘要 + private String specSummary; + + /// 规格值组合键 + private String specValueKey; + + /// true 为启用库存管理 + private Boolean stockEnabled; + + /// true 为允许业务数量保留小数 + private Boolean allowDecimalQuantity; + + /// 最低库存数量,基础单位 + private BigDecimal minStockQuantity; + + /// 状态 + private Status status; + + /// 排序值 + private Integer sort; + + /// 备注 + private String remark; + + /// 最后修改用户 ID + private String updatedBy; + + /// 规格值 ID 列表 + @Transient + private List specValueIdList; + + /// SKU 单位列表 + @Transient + private List unitList; + + /// SKU 附件列表 + @Transient + private List attachmentList; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductSkuSpecValue.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductSkuSpecValue.java new file mode 100644 index 0000000..fc5d847 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductSkuSpecValue.java @@ -0,0 +1,29 @@ +package com.imyeyu.api.modules.gao.entity; + +import com.imyeyu.spring.annotation.table.AutoUUID; +import com.imyeyu.spring.annotation.table.Id; +import com.imyeyu.spring.entity.Creatable; +import com.imyeyu.spring.entity.IDEntity; +import com.imyeyu.spring.entity.UUIDEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/// GAO SKU 规格值关联 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +public class GaoProductSkuSpecValue implements IDEntity, Creatable { + + @Id + @AutoUUID + private String id; + + /// SKU ID + private String skuId; + + /// 规格值 ID + private String specValueId; + + private Long createdAt; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductUnit.java b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductSkuUnit.java similarity index 82% rename from src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductUnit.java rename to src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductSkuUnit.java index 38f3830..af09197 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductUnit.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductSkuUnit.java @@ -6,16 +6,16 @@ import lombok.EqualsAndHashCode; import java.math.BigDecimal; -/// GAO 产品业务单位 +/// GAO 产品 SKU 业务单位 /// /// @author Codex -/// @since 2026-09-02 +/// @since 2026-09-06 @Data @EqualsAndHashCode(callSuper = true) -public class GaoProductUnit extends UUIDEntity { +public class GaoProductSkuUnit extends UUIDEntity { - /// 产品 ID - private String productId; + /// SKU ID + private String skuId; /// 单位名称 private String name; 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 new file mode 100644 index 0000000..d809a67 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductSpec.java @@ -0,0 +1,30 @@ +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.util.List; + +/// GAO 产品规格项 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoProductSpec extends UUIDEntity { + + /// 产品 ID + private String productId; + + /// 规格名称 + private String name; + + /// 排序值 + private Integer sort; + + /// 规格值列表 + @Transient + private List valueList; +} 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 new file mode 100644 index 0000000..e6c1b40 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductSpecValue.java @@ -0,0 +1,26 @@ +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-06 +@Data +@EqualsAndHashCode(callSuper = true) +public class GaoProductSpecValue extends UUIDEntity { + + /// 规格项 ID + private String specId; + + /// 规格值名称 + private String name; + + /// true 为启用 + private Boolean enabled; + + /// 排序值 + private Integer sort; +} 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 index cd87944..df352b6 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductStock.java +++ b/src/main/java/com/imyeyu/api/modules/gao/entity/GaoProductStock.java @@ -18,8 +18,8 @@ public class GaoProductStock extends UUIDEntity { /// 门店 ID private String storeId; - /// 产品 ID - private String productId; + /// SKU ID + private String skuId; /// 实际库存,基础单位 private BigDecimal onHandQuantity; @@ -30,9 +30,9 @@ public class GaoProductStock extends UUIDEntity { /// 最后操作人 private String updatedBy; - /// 产品 + /// SKU @Transient - private GaoProduct product; + private GaoProductSku sku; /// 可用库存,基础单位 @Transient 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 b5d007a..b670e31 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 @@ -70,15 +70,27 @@ public class GaoProductStockRecord implements IDEntity, Creatable { /// 门店 ID private String storeId; - /// 产品 ID - private String productId; + /// SKU ID + private String skuId; - /// 产品单位 ID - private String productUnitId; + /// SKU 单位 ID + private String skuUnitId; /// 产品名称快照 private String productNameSnapshot; + /// 产品 ID 快照 + private String productIdSnapshot; + + /// SKU 编码快照 + private String skuCodeSnapshot; + + /// SKU 名称快照 + private String skuNameSnapshot; + + /// 规格摘要快照 + private String specSummarySnapshot; + /// 单位名称快照 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 da0b33c..0920666 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 @@ -33,11 +33,11 @@ public class GaoSalesOrderItem extends UUIDEntity { /// 订单项类型 private ItemType itemType; - /// 产品 ID - private String productId; + /// SKU ID + private String skuId; - /// 产品单位 ID - private String productUnitId; + /// SKU 单位 ID + private String skuUnitId; /// 产品图片附件 @Transient @@ -46,6 +46,18 @@ public class GaoSalesOrderItem extends UUIDEntity { /// 产品名称快照 private String productNameSnapshot; + /// 产品 ID 快照 + private String productIdSnapshot; + + /// SKU 编码快照 + private String skuCodeSnapshot; + + /// SKU 名称快照 + private String skuNameSnapshot; + + /// 规格摘要快照 + private String specSummarySnapshot; + /// 单位快照 private String unitSnapshot; diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductSkuChartMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductSkuChartMapper.java new file mode 100644 index 0000000..171750a --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductSkuChartMapper.java @@ -0,0 +1,40 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.vo.GaoProductChartReq; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuFulfillmentDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuFulfillmentRankView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuSalesDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuSalesRankView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuStockHealthView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuStockMovementDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuUnitSalesRankView; + +import java.util.List; + +/// GAO SKU 图表 Mapper +/// +/// @author Codex +/// @since 2026-09-07 +public interface GaoProductSkuChartMapper { + + /// 查询 SKU 每日销售统计 + List selectSalesDaily(GaoProductChartReq req); + + /// 查询 SKU 销售排行 + List selectSalesRank(GaoProductChartReq req); + + /// 查询 SKU 单位销售排行 + List selectUnitSalesRank(GaoProductChartReq req); + + /// 查询 SKU 每日履约统计 + List selectFulfillmentDaily(GaoProductChartReq req); + + /// 查询 SKU 履约进度排行 + List selectFulfillmentRank(GaoProductChartReq req); + + /// 查询 SKU 每日库存变动统计 + List selectStockMovementDaily(GaoProductChartReq req); + + /// 查询 SKU 库存健康度 + List selectStockHealth(GaoProductChartReq req); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductSkuMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductSkuMapper.java new file mode 100644 index 0000000..a14e681 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductSkuMapper.java @@ -0,0 +1,33 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoProductSku; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Collection; +import java.util.List; + +/// GAO 产品 SKU Mapper +/// +/// @author Codex +/// @since 2026-09-06 +public interface GaoProductSkuMapper extends BaseMapper { + + /// 按产品查询 SKU + List selectByProductId(@Param("productId") String productId); + + /// 批量按产品查询 SKU + List selectByProductIdList(@Param("productIdList") Collection productIdList); + + /// 查询 SKU 编码 + GaoProductSku selectByStoreIdAndCode(@Param("storeId") String storeId, @Param("code") String code, @Param("excludeId") String excludeId); + + /// 查询 SKU 条码 + GaoProductSku selectByStoreIdAndBarcode(@Param("storeId") String storeId, @Param("barcode") String barcode, @Param("excludeId") String excludeId); + + /// 查询产品下相同规格值组合的 SKU + GaoProductSku selectByProductIdAndSpecValueKey(@Param("productId") String productId, @Param("specValueKey") String specValueKey, @Param("excludeId") String excludeId); + + /// 锁定 SKU + GaoProductSku selectForUpdate(@Param("id") String id); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductSkuSpecValueMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductSkuSpecValueMapper.java new file mode 100644 index 0000000..ed78800 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductSkuSpecValueMapper.java @@ -0,0 +1,21 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoProductSkuSpecValue; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Collection; +import java.util.List; + +/// GAO SKU 规格值关联 Mapper +/// +/// @author Codex +/// @since 2026-09-06 +public interface GaoProductSkuSpecValueMapper extends BaseMapper { + + /// 批量查询 SKU 规格值关联 + List selectBySkuIdList(@Param("skuIdList") Collection skuIdList); + + /// 删除 SKU 的规格值关联 + void deleteBySkuId(@Param("skuId") String skuId); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductSkuUnitMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductSkuUnitMapper.java new file mode 100644 index 0000000..4163550 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductSkuUnitMapper.java @@ -0,0 +1,27 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoProductSkuUnit; +import com.imyeyu.spring.mapper.BaseMapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Collection; +import java.util.List; + +/// GAO 产品 SKU 单位 Mapper +/// +/// @author Codex +/// @since 2026-09-06 +public interface GaoProductSkuUnitMapper extends BaseMapper { + + /// 按 SKU 查询有效单位 + List selectBySkuId(@Param("skuId") String skuId); + + /// 批量查询 SKU 有效单位 + List selectBySkuIdList(@Param("skuIdList") Collection skuIdList); + + /// 锁定单位 + GaoProductSkuUnit selectForUpdate(@Param("id") String id); + + /// 更新有效单位 + int updateUnit(GaoProductSkuUnit unit); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductSpecMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductSpecMapper.java new file mode 100644 index 0000000..bc5eef0 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductSpecMapper.java @@ -0,0 +1,27 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoProductSpec; +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-06 +public interface GaoProductSpecMapper extends BaseMapper { + + /// 按产品查询规格项 + List selectByProductId(@Param("productId") String productId); + + /// 批量按产品查询规格项 + List selectByProductIdList(@Param("productIdList") Collection productIdList); + + /// 查询规格值所属产品 + String selectProductIdBySpecValueId(@Param("specValueId") String specValueId); + + /// 查询规格项所属产品 + String selectProductIdBySpecId(@Param("specId") String specId); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductSpecValueMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductSpecValueMapper.java new file mode 100644 index 0000000..0caba09 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductSpecValueMapper.java @@ -0,0 +1,21 @@ +package com.imyeyu.api.modules.gao.mapper; + +import com.imyeyu.api.modules.gao.entity.GaoProductSpecValue; +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-06 +public interface GaoProductSpecValueMapper extends BaseMapper { + + /// 按 ID 查询有效规格值 + GaoProductSpecValue selectById(@Param("id") String id); + + /// 按规格项查询规格值 + List selectBySpecIdList(@Param("specIdList") Collection specIdList); +} 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 index 7b5c468..f9f8f7f 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductStockMapper.java +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductStockMapper.java @@ -2,6 +2,7 @@ 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.api.modules.gao.vo.GaoProductStockProductView; import com.imyeyu.spring.mapper.BaseMapper; import org.apache.ibatis.annotations.Param; @@ -14,7 +15,7 @@ import java.util.List; public interface GaoProductStockMapper extends BaseMapper { /// 锁定库存余额 - GaoProductStock selectForUpdate(@Param("storeId") String storeId, @Param("productId") String productId); + GaoProductStock selectForUpdate(@Param("storeId") String storeId, @Param("skuId") String skuId); /// 按条件统计库存余额 long countByQuery(GaoProductStockPage page); @@ -22,8 +23,14 @@ public interface GaoProductStockMapper extends BaseMapper selectByQuery(GaoProductStockPage page); + /// 按产品汇总库存数量 + long countProductByQuery(GaoProductStockPage page); + + /// 按产品分页查询库存汇总 + List selectProductByQuery(GaoProductStockPage page); + /// 按门店和产品查询库存余额 - GaoProductStock selectByStoreIdAndProductId(@Param("storeId") String storeId, @Param("productId") String productId); + GaoProductStock selectByStoreIdAndSkuId(@Param("storeId") String storeId, @Param("skuId") String skuId); /// 条件更新库存余额 int updateQuantity(@Param("id") String id, @Param("onHandDelta") java.math.BigDecimal onHandDelta, @Param("reservedDelta") java.math.BigDecimal reservedDelta, @Param("updatedBy") String updatedBy); 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 index f675b8d..bd5ea37 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductStockRecordMapper.java +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductStockRecordMapper.java @@ -26,7 +26,7 @@ public interface GaoProductStockRecordMapper extends BaseMapper 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); + GaoProductStockRecord selectByBizSku(@Param("bizType") GaoProductStockRecord.BizType bizType, @Param("bizId") String bizId, @Param("skuId") String skuId, @Param("operationType") GaoProductStockRecord.OperationType operationType); /// 按条件统计库存流水 long countByQuery(GaoProductStockRecordPage page); 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 deleted file mode 100644 index 596082d..0000000 --- a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoProductUnitMapper.java +++ /dev/null @@ -1,27 +0,0 @@ -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/GaoSalesOrderChartMapper.java b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoSalesOrderChartMapper.java index 70c55e1..43b831c 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoSalesOrderChartMapper.java +++ b/src/main/java/com/imyeyu/api/modules/gao/mapper/GaoSalesOrderChartMapper.java @@ -2,6 +2,7 @@ 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.GaoCustomerOrderSkuRankView; import com.imyeyu.api.modules.gao.vo.GaoCustomerRepeatDailyStatView; import com.imyeyu.api.modules.gao.vo.GaoCustomerRepeatIntervalStatView; import com.imyeyu.api.modules.gao.vo.GaoSalesOrderChartReq; @@ -36,6 +37,9 @@ public interface GaoSalesOrderChartMapper { /// 查询客户订单产品排行 List selectCustomerProductRank(GaoSalesOrderChartReq req); + /// 查询客户订单 SKU 排行 + List selectCustomerSkuRank(GaoSalesOrderChartReq req); + /// 查询销售订单状态统计 List selectStatus(GaoSalesOrderChartReq req); 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 84814cd..f5a8070 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 @@ -1,9 +1,7 @@ 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; @@ -28,21 +26,6 @@ public interface GaoProductService extends BaseService { /// 查询启用且允许销售的产品单位 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/GaoProductSkuChartService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductSkuChartService.java new file mode 100644 index 0000000..a86930a --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductSkuChartService.java @@ -0,0 +1,40 @@ +package com.imyeyu.api.modules.gao.service; + +import com.imyeyu.api.modules.gao.vo.GaoProductChartReq; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuFulfillmentDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuFulfillmentRankView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuSalesDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuSalesRankView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuStockHealthView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuStockMovementDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuUnitSalesRankView; + +import java.util.List; + +/// GAO SKU 图表服务 +/// +/// @author Codex +/// @since 2026-09-07 +public interface GaoProductSkuChartService { + + /// 查询 SKU 每日销售统计 + List salesDaily(GaoProductChartReq req); + + /// 查询 SKU 销售排行 + List salesRank(GaoProductChartReq req); + + /// 查询 SKU 单位销售排行 + List unitSalesRank(GaoProductChartReq req); + + /// 查询 SKU 每日履约统计 + List fulfillmentDaily(GaoProductChartReq req); + + /// 查询 SKU 履约进度排行 + List fulfillmentRank(GaoProductChartReq req); + + /// 查询 SKU 每日库存变动统计 + List stockMovementDaily(GaoProductChartReq req); + + /// 查询 SKU 库存健康度 + List stockHealth(GaoProductChartReq req); +} 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 new file mode 100644 index 0000000..091b465 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductSkuService.java @@ -0,0 +1,37 @@ +package com.imyeyu.api.modules.gao.service; + +import com.imyeyu.api.modules.gao.entity.GaoProductSku; +import com.imyeyu.api.modules.gao.entity.GaoProductSkuUnit; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuUnitSelection; +import com.imyeyu.spring.service.BaseService; + +import java.util.Collection; +import java.util.List; + +/// GAO 产品 SKU 服务 +/// +/// @author Codex +/// @since 2026-09-06 +public interface GaoProductSkuService extends BaseService { + + /// 查询 SKU 详情 + GaoProductSku detail(String id); + + /// 按产品查询 SKU + List listByProductId(String productId); + + /// 批量按产品查询 SKU + List listByProductIdList(Collection productIdList); + + /// 查询并校验可销售 SKU 单位 + GaoProductSkuUnitSelection getSellableSkuUnit(String storeId, String skuId, String skuUnitId); + + /// 查询并校验 SKU 单位 + GaoProductSkuUnitSelection getSkuUnit(String storeId, String skuId, String skuUnitId, boolean purchase); + + /// 查询 SKU 单位 + List listUnitBySkuId(String skuId); + + /// 停用 SKU + void disable(String id); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductSpecService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductSpecService.java new file mode 100644 index 0000000..353ccbc --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductSpecService.java @@ -0,0 +1,25 @@ +package com.imyeyu.api.modules.gao.service; + +import com.imyeyu.api.modules.gao.entity.GaoProductSpec; +import com.imyeyu.api.modules.gao.entity.GaoProductSpecValue; + +import java.util.List; + +/// GAO 产品规格服务 +/// +/// @author Codex +/// @since 2026-09-06 +public interface GaoProductSpecService { + + /// 查询产品规格 + List listByProductId(String productId); + + /// 查询规格项所属产品 ID + String getProductIdBySpecId(String specId); + + /// 创建规格项 + void createSpec(GaoProductSpec spec); + + /// 创建规格值 + void createValue(GaoProductSpecValue value); +} 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 index 0ced846..3686785 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductStockRecordService.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductStockRecordService.java @@ -33,7 +33,7 @@ public interface GaoProductStockRecordService extends BaseService pageByQuery(GaoProductStockPage page); + /// 按产品汇总查询库存 + PageResult pageProductByQuery(GaoProductStockPage page); + /// 查询库存余额详情 - GaoProductStock detail(String storeId, String productId); + GaoProductStock detail(String storeId, String skuId); /// 期初入库 GaoProductStock initialIn(GaoProductStockChangeRequest request); 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 deleted file mode 100644 index f9ae789..0000000 --- a/src/main/java/com/imyeyu/api/modules/gao/service/GaoProductUnitService.java +++ /dev/null @@ -1,28 +0,0 @@ -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 convertQuantityToBase(GaoProductUnit unit, BigDecimal quantity); -} 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 index b42328a..c95c273 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/GaoSalesOrderChartService.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoSalesOrderChartService.java @@ -2,6 +2,7 @@ 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.GaoCustomerOrderSkuRankView; import com.imyeyu.api.modules.gao.vo.GaoCustomerRepeatDailyStatView; import com.imyeyu.api.modules.gao.vo.GaoCustomerRepeatIntervalStatView; import com.imyeyu.api.modules.gao.vo.GaoSalesOrderChartReq; @@ -36,6 +37,9 @@ public interface GaoSalesOrderChartService { /// 查询客户订单产品排行 List customerProductRank(GaoSalesOrderChartReq req); + /// 查询客户订单 SKU 排行 + List customerSkuRank(GaoSalesOrderChartReq req); + /// 查询销售订单状态统计 List status(GaoSalesOrderChartReq 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 a601b28..17178c0 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 @@ -15,7 +15,7 @@ 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.GaoPackageConfigService; -import com.imyeyu.api.modules.gao.service.GaoProductService; +import com.imyeyu.api.modules.gao.service.GaoProductSkuService; import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; import com.imyeyu.api.modules.gao.vo.GaoCustomerPackageCancelRequest; import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePage; @@ -55,7 +55,7 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService defaultPurchaseItems(List configItemList) { return configItemList.stream().map(configItem -> { GaoCustomerPackagePurchaseItem item = new GaoCustomerPackagePurchaseItem(); - item.setProductId(configItem.getProductId()); - item.setProductUnitId(configItem.getProductUnitId()); + item.setSkuId(configItem.getSkuId()); + item.setSkuUnitId(configItem.getSkuUnitId()); item.setProductNameSnapshot(configItem.getProductNameSnapshot()); + item.setProductIdSnapshot(configItem.getProductIdSnapshot()); + item.setSkuCodeSnapshot(configItem.getSkuCodeSnapshot()); + item.setSkuNameSnapshot(configItem.getSkuNameSnapshot()); + item.setSpecSummarySnapshot(configItem.getSpecSummarySnapshot()); item.setUnitSnapshot(configItem.getUnitSnapshot()); item.setBaseQuantityPerUnitSnapshot(configItem.getBaseQuantityPerUnitSnapshot()); item.setTotalQuantity(configItem.getDefaultQuantity()); @@ -252,9 +256,13 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService= item.getTotalQuantity().stripTrailingZeros().scale(), "购买产品数量最多保留三位小数"); - String key = item.getProductId() + "|" + item.getProductUnitId(); - TimiException.requiredTrue(keySet.add(key), "购买套餐内产品和单位不能重复"); + String key = item.getSkuId() + "|" + item.getSkuUnitId(); + TimiException.requiredTrue(keySet.add(key), "购买套餐内 SKU 和单位不能重复"); 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()); + TimiException.required(item.getSkuId(), "套餐配置缺少 SKU"); + TimiException.required(item.getSkuUnitId(), "套餐配置缺少 SKU 单位"); + // 通过 SKU 服务唯一选品入口重新校验 SKU 和单位仍处于可销售状态 + skuService.getSellableSkuUnit(storeId, item.getSkuId(), item.getSkuUnitId()); } } private void verifySameStructureAsConfig(List itemList, List configItemList) { TimiException.requiredTrue(itemList.size() == configItemList.size(), "套餐权益项目必须与套餐配置一致"); - Map itemMap = itemList.stream().collect(Collectors.toMap(item -> item.getProductId() + "|" + item.getProductUnitId(), Function.identity())); + Map itemMap = itemList.stream().collect(Collectors.toMap(item -> item.getSkuId() + "|" + item.getSkuUnitId(), Function.identity())); for (GaoPackageConfigItem configItem : configItemList) { - GaoCustomerPackagePurchaseItem item = itemMap.get(configItem.getProductId() + "|" + configItem.getProductUnitId()); + GaoCustomerPackagePurchaseItem item = itemMap.get(configItem.getSkuId() + "|" + configItem.getSkuUnitId()); TimiException.required(item, "套餐权益项目必须与套餐配置一致"); item.setProductNameSnapshot(configItem.getProductNameSnapshot()); + item.setProductIdSnapshot(configItem.getProductIdSnapshot()); + item.setSkuCodeSnapshot(configItem.getSkuCodeSnapshot()); + item.setSkuNameSnapshot(configItem.getSkuNameSnapshot()); + item.setSpecSummarySnapshot(configItem.getSpecSummarySnapshot()); item.setUnitSnapshot(configItem.getUnitSnapshot()); item.setBaseQuantityPerUnitSnapshot(configItem.getBaseQuantityPerUnitSnapshot()); TimiException.requiredTrue(item.getSort().equals(configItem.getSort()) @@ -308,9 +320,9 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService itemList, List configItemList) { - Map itemMap = itemList.stream().collect(Collectors.toMap(item -> item.getProductId() + "|" + item.getProductUnitId(), Function.identity())); + Map itemMap = itemList.stream().collect(Collectors.toMap(item -> item.getSkuId() + "|" + item.getSkuUnitId(), Function.identity())); for (GaoPackageConfigItem configItem : configItemList) { - GaoCustomerPackagePurchaseItem item = itemMap.get(configItem.getProductId() + "|" + configItem.getProductUnitId()); + GaoCustomerPackagePurchaseItem item = itemMap.get(configItem.getSkuId() + "|" + configItem.getSkuUnitId()); TimiException.requiredTrue(item.getTotalQuantity().compareTo(configItem.getDefaultQuantity()) == 0, "普通员工不能修改套餐权益配额"); } } @@ -327,9 +339,9 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService 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.getProductId() + "|" + item.getProductUnitId(), Function.identity())); + Map existingItemMap = existingItemList.stream().collect(Collectors.toMap(item -> item.getSkuId() + "|" + item.getSkuUnitId(), Function.identity())); for (GaoCustomerPackagePurchaseItem item : requestItemList) { - GaoSalesOrderItem existingItem = existingItemMap.get(item.getProductId() + "|" + item.getProductUnitId()); + GaoSalesOrderItem existingItem = existingItemMap.get(item.getSkuId() + "|" + item.getSkuUnitId()); TimiException.required(existingItem, "requestNo 对应购买请求内容不一致"); TimiException.requiredTrue(existingItem.getItemType() == GaoSalesOrderItem.ItemType.PACKAGE_ENTITLEMENT && existingItem.getQuantity().compareTo(item.getTotalQuantity()) == 0 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 0210518..9f4e9fd 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 @@ -6,7 +6,8 @@ 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.GaoProduct; -import com.imyeyu.api.modules.gao.entity.GaoProductUnit; +import com.imyeyu.api.modules.gao.entity.GaoProductSku; +import com.imyeyu.api.modules.gao.entity.GaoProductSkuUnit; import com.imyeyu.api.modules.gao.entity.GaoSalesOrder; import com.imyeyu.api.modules.gao.entity.GaoSalesOrderItem; import com.imyeyu.api.modules.gao.entity.GaoProductStockRecord; @@ -17,7 +18,7 @@ import com.imyeyu.api.modules.gao.mapper.GaoOrderFulfillmentMapper; 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.GaoProductSkuService; import com.imyeyu.api.modules.gao.service.GaoSalesOrderService; import com.imyeyu.api.modules.gao.service.GaoProductStockRecordService; import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; @@ -68,7 +69,7 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService buildStockItems(String storeId, List sourceList) { Map itemMap = new LinkedHashMap<>(); for (SourceEntry source : sourceList) { - if (!Boolean.TRUE.equals(source.product().getStockEnabled())) { + if (!Boolean.TRUE.equals(source.sku().getStockEnabled())) { continue; } - GaoProductUnit baseUnit = productService.listUnitByProductId(source.product().getId()).stream() + GaoProductSkuUnit baseUnit = skuService.listUnitBySkuId(source.sku().getId()).stream() .filter(unit -> Boolean.TRUE.equals(unit.getIsBase())).findFirst().orElse(null); TimiException.required(baseUnit, "实物产品缺少基础库存单位"); - BigDecimal baseQuantity = unitService.convertQuantityToBase(source.unit(), source.quantity()); - GaoStockChangeItem item = itemMap.get(source.product().getId()); + BigDecimal baseQuantity = source.quantity().multiply(source.unit().getBaseQuantityPerUnit()); + GaoStockChangeItem item = itemMap.get(source.sku().getId()); if (item == null) { - item = GaoStockChangeItem.of(source.product(), baseUnit, baseQuantity, baseQuantity); - itemMap.put(source.product().getId(), item); + item = GaoStockChangeItem.of(source.product(), source.sku(), baseUnit, baseQuantity, baseQuantity); + itemMap.put(source.sku().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(); + return itemMap.values().stream().sorted(Comparator.comparing(GaoStockChangeItem::getSkuId)).toList(); } private GaoOrderFulfillment lockFulfillment(String fulfillmentId, GaoSalesOrder order) { @@ -517,19 +521,23 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService productIdList = itemList.stream() - .map(GaoOrderFulfillmentItem::getProductId) + List skuIdList = itemList.stream() + .map(GaoOrderFulfillmentItem::getSkuId) .filter(value -> value != null && !value.isBlank()) .distinct() .toList(); - if (productIdList.isEmpty()) { + if (skuIdList.isEmpty()) { return; } Map> attachmentMap = attachmentService.mapByBizIdList( - Attachment.BizType.GAO_PRODUCT, - productIdList, - GaoProduct.AttachType.PHOTO.name()); + Attachment.BizType.GAO_PRODUCT_SKU, + skuIdList); for (GaoOrderFulfillmentItem item : itemList) { - item.setAttachmentList(attachmentMap.getOrDefault(item.getProductId(), List.of())); + item.setAttachmentList(attachmentMap.getOrDefault(item.getSkuId(), List.of())); } } - private record SourceEntry(String sourceOrderItemId, String productId, String productUnitId, - String productNameSnapshot, String unitSnapshot, BigDecimal baseQuantityPerUnitSnapshot, BigDecimal quantity, - BigDecimal totalQuantity, BigDecimal reservedBefore, BigDecimal fulfilledBefore, GaoProduct product, - GaoProductUnit unit) { + private record SourceEntry(String sourceOrderItemId, String skuId, String skuUnitId, String productIdSnapshot, + String productNameSnapshot, String skuCodeSnapshot, String skuNameSnapshot, String specSummarySnapshot, + String unitSnapshot, BigDecimal baseQuantityPerUnitSnapshot, BigDecimal quantity, + BigDecimal totalQuantity, BigDecimal reservedBefore, BigDecimal fulfilledBefore, GaoProduct product, GaoProductSku sku, + GaoProductSkuUnit unit) { private String sourceId() { return sourceOrderItemId; @@ -648,8 +656,9 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService= quantity.stripTrailingZeros().scale(), "产品数量最多保留三位小数"); - if (!Boolean.TRUE.equals(product.getAllowDecimalQuantity())) { - TimiException.requiredTrue(quantity.stripTrailingZeros().scale() <= 0, "该产品不允许小数数量"); + if (!Boolean.TRUE.equals(sku.getAllowDecimalQuantity())) { + TimiException.requiredTrue(quantity.stripTrailingZeros().scale() <= 0, "该 SKU 不允许小数数量"); } - unitService.convertQuantityToBase(unitEntity, quantity); - String key = item.getProductId() + "|" + item.getProductUnitId(); - TimiException.requiredTrue(keySet.add(key), "同一套餐内产品和单位不能重复"); + quantity.multiply(unitEntity.getBaseQuantityPerUnit()); + String key = item.getSkuId() + "|" + item.getSkuUnitId(); + TimiException.requiredTrue(keySet.add(key), "同一套餐内 SKU 和单位不能重复"); item.setProductNameSnapshot(product.getName()); + item.setProductIdSnapshot(product.getId()); + item.setSkuCodeSnapshot(sku.getCode()); + item.setSkuNameSnapshot(sku.getName()); + item.setSpecSummarySnapshot(sku.getSpecSummary()); item.setUnitSnapshot(unitEntity.getName()); item.setBaseQuantityPerUnitSnapshot(unitEntity.getBaseQuantityPerUnit()); item.setSort(TimiJava.defaultIfNull(item.getSort(), i)); @@ -210,9 +214,13 @@ public class GaoPackageConfigServiceImplement 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 ObjectProvider skuServiceProvider; + private final ObjectProvider specServiceProvider; private final UserLoginService userLoginService; private final LoggerService loggerService; @@ -93,37 +91,9 @@ public class GaoProductServiceImplement extends AbstractEntityService list = mapper.selectByQuery(page); fillTransient(list); for (GaoProduct product : list) { - product.setUnitList(product.getUnitList().stream().filter(unit -> Boolean.TRUE.equals(unit.getAllowSale())).toList()); + product.setSkuList(product.getSkuList().stream().filter(sku -> sku.getStatus() == com.imyeyu.api.modules.gao.entity.GaoProductSku.Status.ACTIVE && sku.getUnitList().stream().anyMatch(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); + return list.stream().filter(product -> !product.getSkuList().isEmpty()).toList(); } @Transactional(TimiServerDBConfig.ROLLBACKER) @@ -132,7 +102,7 @@ public class GaoProductServiceImplement extends AbstractEntityService { prepare(product, null); super.create(product); - saveUnits(product.getId(), product.getUnitList(), List.of()); + saveSkus(product, product.getSkuList()); attachmentService.updateByBizId(Attachment.BizType.GAO_PRODUCT, product.getId(), product.getAttachmentList()); return product; }); @@ -151,17 +121,14 @@ public class GaoProductServiceImplement extends AbstractEntityService existingUnitList = unitMapper.selectByProductId(product.getId()); - product.setUnitList(request.getUnitList() == null ? existingUnitList : request.getUnitList()); + product.setSkuList(request.getSkuList() == null ? skuServiceProvider.getObject().listByProductId(product.getId()) : request.getSkuList()); prepare(product, product.getId()); super.update(product); - if (request.getUnitList() != null) { - saveUnits(product.getId(), product.getUnitList(), existingUnitList); + if (request.getSkuList() != null) { + saveSkus(product, request.getSkuList()); } if (request.getAttachmentList() != null) { attachmentService.updateByBizId(Attachment.BizType.GAO_PRODUCT, product.getId(), request.getAttachmentList()); @@ -176,6 +143,7 @@ public class GaoProductServiceImplement extends AbstractEntityService { GaoProduct product = get(id); TimiException.required(product, "not found product"); + TimiException.requiredTrue(skuServiceProvider.getObject().listByProductId(id).stream().noneMatch(sku -> sku.getStatus() == com.imyeyu.api.modules.gao.entity.GaoProductSku.Status.ACTIVE), "请先停用产品的全部 SKU"); product.setStatus(GaoProduct.Status.INACTIVE); product.setUpdatedBy(userLoginService.getRequireLoginUserId()); product.setUpdatedAt(Time.now()); @@ -202,15 +170,7 @@ public class GaoProductServiceImplement extends AbstractEntityService= product.getMinStockQuantity().scale(), "最低库存最多保留三位小数"); - } product.setSort(TimiJava.defaultIfNull(product.getSort(), 0)); TimiException.requiredNull(mapper.selectByStoreIdAndCode(product.getStoreId(), product.getCode(), excludeId), "门店内产品编码不能重复"); if (product.getCategoryId() != null) { @@ -218,42 +178,21 @@ public class GaoProductServiceImplement extends AbstractEntityService 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.setBaseQuantityPerUnit(source.getBaseQuantityPerUnit()); - unit.setSalePrice(source.getSalePrice()); - 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), "产品单位更新失败"); - } + private void saveSkus(GaoProduct product, List skuList) { + List sourceList = skuList; + if (sourceList == null || sourceList.isEmpty()) { + com.imyeyu.api.modules.gao.entity.GaoProductSku sku = new com.imyeyu.api.modules.gao.entity.GaoProductSku(); + sku.setProductId(product.getId()); sku.setCode(product.getCode()); sku.setName(product.getName()); + com.imyeyu.api.modules.gao.entity.GaoProductSkuUnit unit = new com.imyeyu.api.modules.gao.entity.GaoProductSkuUnit(); + unit.setName("件"); unit.setIsBase(true); unit.setBaseQuantityPerUnit(BigDecimal.ONE); + sku.setUnitList(List.of(unit)); sourceList = List.of(sku); } - for (GaoProductUnit existing : existingUnitList) { - if (!retainedIdSet.contains(existing.getId())) { - unitMapper.delete(existing.getId()); - } + for (com.imyeyu.api.modules.gao.entity.GaoProductSku source : sourceList) { + source.setProductId(product.getId()); + if (source.getId() == null) skuServiceProvider.getObject().create(source); else skuServiceProvider.getObject().update(source); } } @@ -262,11 +201,12 @@ public class GaoProductServiceImplement extends AbstractEntityService 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); + Map> skuMap = skuServiceProvider.getObject().listByProductIdList(productIdList).stream() + .collect(Collectors.groupingBy(com.imyeyu.api.modules.gao.entity.GaoProductSku::getProductId)); for (GaoProduct product : list) { - product.setUnitList(unitMap.getOrDefault(product.getId(), List.of())); + product.setSkuList(skuMap.getOrDefault(product.getId(), List.of())); + product.setSpecList(specServiceProvider.getObject().listByProductId(product.getId())); product.setAttachmentList(attachmentMap.getOrDefault(product.getId(), List.of())); if (product.getCategoryId() != null) { product.setCategory(categoryServiceProvider.getObject().get(product.getCategoryId())); diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductSkuChartServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductSkuChartServiceImplement.java new file mode 100644 index 0000000..713f3bf --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductSkuChartServiceImplement.java @@ -0,0 +1,95 @@ +package com.imyeyu.api.modules.gao.service.implement; + +import com.imyeyu.api.modules.gao.mapper.GaoProductSkuChartMapper; +import com.imyeyu.api.modules.gao.service.GaoProductSkuChartService; +import com.imyeyu.api.modules.gao.vo.GaoProductChartReq; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuFulfillmentDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuFulfillmentRankView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuSalesDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuSalesRankView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuStockHealthView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuStockMovementDailyStatView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuUnitSalesRankView; +import com.imyeyu.java.bean.timi.TimiException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +/// GAO SKU 图表服务实现 +/// +/// @author Codex +/// @since 2026-09-07 +@Service +@RequiredArgsConstructor +public class GaoProductSkuChartServiceImplement implements GaoProductSkuChartService { + + private static final int DEFAULT_TOP_N = 20; + private static final int MAX_TOP_N = 100; + + private final GaoProductSkuChartMapper 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 unitSalesRank(GaoProductChartReq req) { + checkRangeReq(req); + normalizeTopN(req); + return mapper.selectUnitSalesRank(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); + } + + private void checkRangeReq(GaoProductChartReq req) { + checkStoreReq(req); + TimiException.required(req.getBeginAt(), "SKU 图表开始时间不能为空"); + TimiException.required(req.getEndAt(), "SKU 图表结束时间不能为空"); + TimiException.requiredTrue(req.getBeginAt() <= req.getEndAt(), "SKU 图表结束时间早于开始时间"); + } + + private void checkStoreReq(GaoProductChartReq req) { + TimiException.required(req, "SKU 图表请求不能为空"); + TimiException.required(req.getStoreId(), "SKU 图表门店不能为空"); + } + + private void normalizeTopN(GaoProductChartReq req) { + int topN = req.getTopN() == null ? DEFAULT_TOP_N : req.getTopN(); + TimiException.requiredTrue(0 < topN && topN < MAX_TOP_N + 1, "SKU 图表排行数量无效"); + req.setTopN(topN); + } +} 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 new file mode 100644 index 0000000..e820abb --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductSkuServiceImplement.java @@ -0,0 +1,349 @@ +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.GaoProductSku; +import com.imyeyu.api.modules.gao.entity.GaoProductSkuSpecValue; +import com.imyeyu.api.modules.gao.entity.GaoProductSkuUnit; +import com.imyeyu.api.modules.gao.entity.GaoProductSpec; +import com.imyeyu.api.modules.gao.entity.GaoProductSpecValue; +import com.imyeyu.api.modules.gao.mapper.GaoProductSkuMapper; +import com.imyeyu.api.modules.gao.mapper.GaoProductSkuSpecValueMapper; +import com.imyeyu.api.modules.gao.mapper.GaoProductSkuUnitMapper; +import com.imyeyu.api.modules.gao.mapper.GaoProductSpecMapper; +import com.imyeyu.api.modules.gao.mapper.GaoProductSpecValueMapper; +import com.imyeyu.api.modules.gao.service.GaoProductService; +import com.imyeyu.api.modules.gao.service.GaoProductSkuService; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuUnitSelection; +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.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.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/// GAO 产品 SKU 服务实现 +/// +/// @author Codex +/// @since 2026-09-06 +@Service +@RequiredArgsConstructor +public class GaoProductSkuServiceImplement extends AbstractEntityService implements GaoProductSkuService { + + private static final BigDecimal ONE = BigDecimal.ONE; + + private final GaoProductSkuMapper mapper; + private final GaoProductSkuUnitMapper unitMapper; + private final GaoProductSkuSpecValueMapper skuSpecValueMapper; + private final GaoProductSpecMapper specMapper; + private final GaoProductSpecValueMapper valueMapper; + private final AttachmentService attachmentService; + private final ObjectProvider productServiceProvider; + private final UserLoginService userLoginService; + + @Override + protected BaseMapper mapper() { + return mapper; + } + + @Override + public GaoProductSku detail(String id) { + GaoProductSku sku = get(id); + TimiException.required(sku, "not found product sku"); + fillTransient(List.of(sku)); + return sku; + } + + @Override + public List listByProductId(String productId) { + TimiException.required(productId, "not found productId"); + List result = mapper.selectByProductId(productId); + fillTransient(result); + return result; + } + + @Override + public List listByProductIdList(Collection productIdList) { + if (productIdList == null || productIdList.isEmpty()) { + return List.of(); + } + List result = mapper.selectByProductIdList(productIdList); + fillTransient(result); + return result; + } + + @Override + public GaoProductSkuUnitSelection getSellableSkuUnit(String storeId, String skuId, String skuUnitId) { + return getSkuUnit(storeId, skuId, skuUnitId, false); + } + + @Override + public GaoProductSkuUnitSelection getSkuUnit(String storeId, String skuId, String skuUnitId, boolean purchase) { + TimiException.required(storeId, "not found storeId"); + TimiException.required(skuId, "not found skuId"); + TimiException.required(skuUnitId, "not found skuUnitId"); + GaoProductSku sku = mapper.selectForUpdate(skuId); + TimiException.required(sku, "not found product sku"); + TimiException.requiredTrue(storeId.equals(sku.getStoreId()), "SKU 不属于当前门店"); + TimiException.requiredTrue(sku.getStatus() == GaoProductSku.Status.ACTIVE, "SKU 已停用"); + GaoProduct product = productServiceProvider.getObject().get(sku.getProductId()); + TimiException.required(product, "not found product"); + TimiException.requiredTrue(product.getStatus() == GaoProduct.Status.ACTIVE, "产品已停用"); + GaoProductSkuUnit unit = unitMapper.selectForUpdate(skuUnitId); + TimiException.required(unit, "not found product sku unit"); + TimiException.requiredTrue(skuId.equals(unit.getSkuId()), "SKU 单位不属于当前 SKU"); + TimiException.requiredTrue(!purchase || Boolean.TRUE.equals(unit.getAllowPurchase()), "SKU 单位不允许采购入库"); + TimiException.requiredTrue(purchase || Boolean.TRUE.equals(unit.getAllowSale()), "SKU 单位不允许销售"); + return new GaoProductSkuUnitSelection(product, sku, unit); + } + + @Override + public List listUnitBySkuId(String skuId) { + TimiException.required(skuId, "not found skuId"); + return unitMapper.selectBySkuId(skuId); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void create(GaoProductSku sku) { + prepare(sku, null); + super.create(sku); + saveSpecValues(sku.getId(), sku.getSpecValueIdList()); + saveUnits(sku.getId(), sku.getUnitList(), List.of()); + attachmentService.updateByBizId(Attachment.BizType.GAO_PRODUCT_SKU, sku.getId(), sku.getAttachmentList()); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void update(GaoProductSku request) { + TimiException.required(request, "not found product sku"); + TimiException.required(request.getId(), "not found product sku.id"); + GaoProductSku sku = get(request.getId()); + TimiException.required(sku, "not found product sku"); + sku.setCode(TimiJava.defaultIfNull(request.getCode(), sku.getCode())); + sku.setBarcode(request.getBarcode()); + sku.setName(TimiJava.defaultIfNull(request.getName(), sku.getName())); + sku.setSpecSummary(TimiJava.defaultIfNull(request.getSpecSummary(), sku.getSpecSummary())); + sku.setStockEnabled(TimiJava.defaultIfNull(request.getStockEnabled(), sku.getStockEnabled())); + sku.setAllowDecimalQuantity(TimiJava.defaultIfNull(request.getAllowDecimalQuantity(), sku.getAllowDecimalQuantity())); + sku.setMinStockQuantity(request.getMinStockQuantity()); + sku.setStatus(TimiJava.defaultIfNull(request.getStatus(), sku.getStatus())); + sku.setSort(TimiJava.defaultIfNull(request.getSort(), sku.getSort())); + sku.setRemark(request.getRemark()); + sku.setSpecValueIdList(request.getSpecValueIdList() == null ? currentSpecValueIds(sku.getId()) : request.getSpecValueIdList()); + sku.setUnitList(request.getUnitList() == null ? unitMapper.selectBySkuId(sku.getId()) : request.getUnitList()); + prepare(sku, sku.getId()); + super.update(sku); + if (request.getSpecValueIdList() != null) { + saveSpecValues(sku.getId(), sku.getSpecValueIdList()); + } + if (request.getUnitList() != null) { + saveUnits(sku.getId(), sku.getUnitList(), unitMapper.selectBySkuId(sku.getId())); + } + if (request.getAttachmentList() != null) { + attachmentService.updateByBizId(Attachment.BizType.GAO_PRODUCT_SKU, sku.getId(), request.getAttachmentList()); + } + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public void disable(String id) { + GaoProductSku sku = get(id); + TimiException.required(sku, "not found product sku"); + sku.setStatus(GaoProductSku.Status.INACTIVE); + sku.setUpdatedBy(userLoginService.getRequireLoginUserId()); + sku.setUpdatedAt(Time.now()); + super.update(sku); + } + + @Override + public void delete(String id) { + disable(id); + } + + @Override + public void destroy(String id) { + throw new TimiException(TimiCode.ERROR, "已建立 SKU 的产品不允许物理删除"); + } + + private void prepare(GaoProductSku sku, String excludeId) { + TimiException.required(sku, "not found product sku"); + GaoProduct product = productServiceProvider.getObject().get(sku.getProductId()); + TimiException.required(product, "产品不存在"); + sku.setStoreId(product.getStoreId()); + sku.setCode(requiredText(sku.getCode(), "SKU 编码不能为空")); + sku.setName(requiredText(sku.getName(), "SKU 名称不能为空")); + sku.setBarcode(trimToNull(sku.getBarcode())); + sku.setSpecSummary(trimToNull(sku.getSpecSummary())); + sku.setStockEnabled(TimiJava.defaultIfNull(sku.getStockEnabled(), product.getStockEnabled())); + TimiException.requiredTrue(product.getProductType() != GaoProduct.ProductType.SERVICE || !sku.getStockEnabled(), "SERVICE 产品的 SKU 不能启用库存"); + sku.setAllowDecimalQuantity(TimiJava.defaultIfNull(sku.getAllowDecimalQuantity(), false)); + sku.setStatus(TimiJava.defaultIfNull(sku.getStatus(), GaoProductSku.Status.ACTIVE)); + if (!Boolean.TRUE.equals(sku.getStockEnabled())) { + TimiException.requiredNull(sku.getMinStockQuantity(), "未启用库存的 SKU 不能填写最低库存"); + } + if (sku.getMinStockQuantity() != null) { + TimiException.requiredTrue(0 <= sku.getMinStockQuantity().signum(), "最低库存不能为负数"); + TimiException.requiredTrue(3 >= sku.getMinStockQuantity().scale(), "最低库存最多保留三位小数"); + } + sku.setSort(TimiJava.defaultIfNull(sku.getSort(), 0)); + TimiException.requiredNull(mapper.selectByStoreIdAndCode(sku.getStoreId(), sku.getCode(), excludeId), "门店内 SKU 编码不能重复"); + if (sku.getBarcode() != null) { + TimiException.requiredNull(mapper.selectByStoreIdAndBarcode(sku.getStoreId(), sku.getBarcode(), excludeId), "门店内 SKU 条码不能重复"); + } + validateSpecValues(sku); + sku.setSpecValueKey(buildSpecValueKey(sku.getSpecValueIdList())); + TimiException.requiredNull(mapper.selectByProductIdAndSpecValueKey(sku.getProductId(), sku.getSpecValueKey(), excludeId), "同一产品的 SKU 规格组合不能重复"); + sku.setUnitList(validateAndNormalizeUnits(sku.getUnitList())); + sku.setUpdatedBy(userLoginService.getRequireLoginUserId()); + } + + private void validateSpecValues(GaoProductSku sku) { + List specValueIdList = sku.getSpecValueIdList() == null ? List.of() : sku.getSpecValueIdList(); + TimiException.requiredTrue(specValueIdList.size() == new HashSet<>(specValueIdList).size(), "SKU 规格值不能重复"); + Set specIdSet = new HashSet<>(); + for (String specValueId : specValueIdList) { + GaoProductSpecValue value = valueMapper.selectById(specValueId); + TimiException.required(value, "SKU 规格值不存在"); + TimiException.requiredTrue(Boolean.TRUE.equals(value.getEnabled()), "SKU 规格值已停用"); + TimiException.requiredTrue(sku.getProductId().equals(specMapper.selectProductIdBySpecValueId(specValueId)), "SKU 规格值不属于当前产品"); + TimiException.requiredTrue(specIdSet.add(value.getSpecId()), "同一规格项只能选择一个规格值"); + } + Set requiredSpecIdSet = specMapper.selectByProductId(sku.getProductId()).stream().map(GaoProductSpec::getId).collect(Collectors.toSet()); + TimiException.requiredTrue(requiredSpecIdSet.equals(specIdSet), "SKU 必须为每个规格项选择一个规格值"); + } + + private String buildSpecValueKey(List specValueIdList) { + if (specValueIdList == null || specValueIdList.isEmpty()) { + return "DEFAULT"; + } + return specValueIdList.stream().sorted().collect(Collectors.joining(",")); + } + + private List validateAndNormalizeUnits(List sourceList) { + TimiException.requiredTrue(sourceList != null && !sourceList.isEmpty(), "SKU 至少需要一个单位"); + List result = new ArrayList<>(); + Set nameSet = new HashSet<>(); + Set idSet = new HashSet<>(); + int baseCount = 0; + for (int i = 0; i < sourceList.size(); i++) { + GaoProductSkuUnit source = sourceList.get(i); + TimiException.required(source, "SKU 单位不能为空"); + GaoProductSkuUnit unit = new GaoProductSkuUnit(); + unit.setId(source.getId()); + if (unit.getId() != null) { + TimiException.requiredTrue(idSet.add(unit.getId()), "SKU 单位 ID 不能重复"); + } + unit.setName(requiredText(source.getName(), "SKU 单位名称不能为空")); + TimiException.requiredTrue(nameSet.add(unit.getName()), "同一 SKU 的单位名称不能重复"); + unit.setBaseQuantityPerUnit(TimiJava.defaultIfNull(source.getBaseQuantityPerUnit(), ONE)); + TimiException.requiredTrue(0 < unit.getBaseQuantityPerUnit().signum(), "基础单位数量系数必须大于 0"); + TimiException.requiredTrue(6 >= unit.getBaseQuantityPerUnit().scale(), "基础单位数量系数最多保留六位小数"); + if (source.getSalePrice() != null) { + TimiException.requiredTrue(0 <= source.getSalePrice(), "单位建议售价不能为负数"); + } + unit.setSalePrice(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()) { + baseCount++; + TimiException.requiredTrue(1 >= baseCount, "一个 SKU 只能有一个基础单位"); + TimiException.requiredTrue(ONE.compareTo(unit.getBaseQuantityPerUnit()) == 0, "基础单位数量系数必须为 1"); + } + result.add(unit); + } + TimiException.requiredTrue(0 < baseCount, "SKU 必须有一个基础单位"); + return result; + } + + private void saveUnits(String skuId, List unitList, List existingUnitList) { + Map existingById = existingUnitList.stream().collect(Collectors.toMap(GaoProductSkuUnit::getId, item -> item)); + Set retainedIdSet = new HashSet<>(); + for (GaoProductSkuUnit source : unitList) { + GaoProductSkuUnit existing = source.getId() == null ? null : existingById.get(source.getId()); + if (source.getId() != null && existing == null) { + throw new TimiException(TimiCode.ERROR, "SKU 单位不存在或不属于当前 SKU"); + } + GaoProductSkuUnit unit = new GaoProductSkuUnit(); + unit.setId(existing == null ? null : existing.getId()); + unit.setSkuId(skuId); + unit.setName(source.getName()); + unit.setBaseQuantityPerUnit(source.getBaseQuantityPerUnit()); + unit.setSalePrice(source.getSalePrice()); + 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), "SKU 单位更新失败"); + } + } + for (GaoProductSkuUnit existing : existingUnitList) { + if (!retainedIdSet.contains(existing.getId())) { + unitMapper.delete(existing.getId()); + } + } + } + + private void saveSpecValues(String skuId, List specValueIdList) { + skuSpecValueMapper.deleteBySkuId(skuId); + for (String specValueId : specValueIdList == null ? List.of() : specValueIdList) { + GaoProductSkuSpecValue relation = new GaoProductSkuSpecValue(); + relation.setSkuId(skuId); + relation.setSpecValueId(specValueId); + skuSpecValueMapper.insert(relation); + } + } + + private List currentSpecValueIds(String skuId) { + return skuSpecValueMapper.selectBySkuIdList(List.of(skuId)).stream().map(GaoProductSkuSpecValue::getSpecValueId).toList(); + } + + private void fillTransient(List skuList) { + if (skuList.isEmpty()) return; + List skuIdList = skuList.stream().map(GaoProductSku::getId).toList(); + Map> unitMap = unitMapper.selectBySkuIdList(skuIdList).stream().collect(Collectors.groupingBy(GaoProductSkuUnit::getSkuId)); + Map> specValueIdMap = skuSpecValueMapper.selectBySkuIdList(skuIdList) + .stream() + .collect(Collectors.groupingBy(GaoProductSkuSpecValue::getSkuId, Collectors.mapping(GaoProductSkuSpecValue::getSpecValueId, Collectors.toList()))); + Map> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_PRODUCT_SKU, skuIdList); + for (GaoProductSku sku : skuList) { + sku.setUnitList(unitMap.getOrDefault(sku.getId(), List.of())); + sku.setSpecValueIdList(specValueIdMap.getOrDefault(sku.getId(), List.of())); + sku.setAttachmentList(attachmentMap.getOrDefault(sku.getId(), List.of())); + } + } + + private String requiredText(String value, String message) { + TimiException.required(value, message); + String result = value.trim(); + TimiException.requiredTrue(!result.isEmpty(), message); + return result; + } + + private String trimToNull(String value) { + return value == null || value.trim().isEmpty() ? null : value.trim(); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductSpecServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductSpecServiceImplement.java new file mode 100644 index 0000000..15669e8 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductSpecServiceImplement.java @@ -0,0 +1,65 @@ +package com.imyeyu.api.modules.gao.service.implement; + +import com.imyeyu.api.modules.gao.entity.GaoProduct; +import com.imyeyu.api.modules.gao.entity.GaoProductSpec; +import com.imyeyu.api.modules.gao.entity.GaoProductSpecValue; +import com.imyeyu.api.modules.gao.mapper.GaoProductSpecMapper; +import com.imyeyu.api.modules.gao.mapper.GaoProductSpecValueMapper; +import com.imyeyu.api.modules.gao.service.GaoProductService; +import com.imyeyu.api.modules.gao.service.GaoProductSpecService; +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/// GAO 产品规格服务实现 +/// +/// @author Codex +/// @since 2026-09-06 +@Service +@RequiredArgsConstructor +public class GaoProductSpecServiceImplement implements GaoProductSpecService { + + private final GaoProductSpecMapper specMapper; + private final GaoProductSpecValueMapper valueMapper; + private final GaoProductService productService; + + @Override + public List listByProductId(String productId) { + List specList = specMapper.selectByProductId(productId); + Map> valueMap = valueMapper.selectBySpecIdList(specList.stream().map(GaoProductSpec::getId).toList()).stream().collect(Collectors.groupingBy(GaoProductSpecValue::getSpecId)); + for (GaoProductSpec spec : specList) spec.setValueList(valueMap.getOrDefault(spec.getId(), List.of())); + return specList; + } + + @Override + public String getProductIdBySpecId(String specId) { + return specMapper.selectProductIdBySpecId(specId); + } + + @Override + public void createSpec(GaoProductSpec spec) { + TimiException.required(spec, "not found product spec"); + GaoProduct product = productService.get(spec.getProductId()); + TimiException.required(product, "产品不存在"); + spec.setName(requiredText(spec.getName(), "规格名称不能为空")); + spec.setSort(TimiJava.defaultIfNull(spec.getSort(), 0)); + specMapper.insert(spec); + } + + @Override + public void createValue(GaoProductSpecValue value) { + TimiException.required(value, "not found product spec value"); + TimiException.required(specMapper.selectProductIdBySpecId(value.getSpecId()), "规格项不存在"); + value.setName(requiredText(value.getName(), "规格值名称不能为空")); + value.setEnabled(TimiJava.defaultIfNull(value.getEnabled(), true)); + value.setSort(TimiJava.defaultIfNull(value.getSort(), 0)); + valueMapper.insert(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/GaoProductStockRecordServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductStockRecordServiceImplement.java index 322e31c..4e96633 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 @@ -86,13 +86,13 @@ public class GaoProductStockRecordServiceImplement extends AbstractEntityService } @Override - public GaoProductStockRecord getByBizProduct(GaoProductStockRecord.BizType bizType, String bizId, String productId, + public GaoProductStockRecord getByBizSku(GaoProductStockRecord.BizType bizType, String bizId, String skuId, GaoProductStockRecord.OperationType operationType) { TimiException.required(bizType, "not found stock bizType"); TimiException.required(bizId, "not found stock bizId"); - TimiException.required(productId, "not found productId"); + TimiException.required(skuId, "not found skuId"); TimiException.required(operationType, "not found operationType"); - return mapper.selectByBizProduct(bizType, bizId, productId, operationType); + return mapper.selectByBizSku(bizType, bizId, skuId, operationType); } @Override 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 6957762..06ad1c6 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 @@ -5,18 +5,20 @@ 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.GaoProductSku; +import com.imyeyu.api.modules.gao.entity.GaoProductSkuUnit; 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.GaoProductSkuService; 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.GaoProductStockProductView; +import com.imyeyu.api.modules.gao.vo.GaoProductSkuUnitSelection; import com.imyeyu.api.modules.gao.vo.GaoStockChangeItem; import com.imyeyu.api.modules.system.entity.Logger; import com.imyeyu.api.modules.system.service.LoggerService; @@ -54,7 +56,7 @@ public class GaoProductStockServiceImplement implements GaoProductStockService { private final AttachmentService attachmentService; private final GaoProductStockRecordService recordService; private final GaoProductService productService; - private final GaoProductUnitService unitService; + private final GaoProductSkuService skuService; private final GaoStoreScopeService storeScopeService; private final UserLoginService userLoginService; private final LoggerService loggerService; @@ -66,15 +68,14 @@ public class GaoProductStockServiceImplement implements GaoProductStockService { 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() + Attachment.BizType.GAO_PRODUCT_SKU, + list.stream().map(GaoProductStock::getSkuId).filter(id -> id != null && !id.isBlank()).toList() ); 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)); + if (stock.getSku() != null) { + Attachment photo = photoMap.get(stock.getSkuId()); + stock.getSku().setAttachmentList(photo == null ? List.of() : List.of(photo)); } } result.setList(list); @@ -82,22 +83,31 @@ public class GaoProductStockServiceImplement implements GaoProductStockService { } @Override - public GaoProductStock detail(String storeId, String productId) { + public PageResult pageProductByQuery(GaoProductStockPage page) { + TimiException.required(page, "not found stock product page"); + PageResult result = new PageResult<>(); + result.setTotal(stockMapper.countProductByQuery(page)); + result.setList(stockMapper.selectProductByQuery(page)); + return result; + } + + @Override + public GaoProductStock detail(String storeId, String skuId) { TimiException.required(storeId, "not found storeId"); - TimiException.required(productId, "not found productId"); + TimiException.required(skuId, "not found skuId"); 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); + GaoProductSku sku = skuService.get(skuId); + TimiException.required(sku, "not found sku"); + TimiException.requiredTrue(storeId.equals(sku.getStoreId()), "SKU 不属于当前门店"); + GaoProductStock stock = stockMapper.selectByStoreIdAndSkuId(storeId, skuId); if (stock == null) { stock = new GaoProductStock(); stock.setStoreId(storeId); - stock.setProductId(productId); + stock.setSkuId(skuId); stock.setOnHandQuantity(ZERO); stock.setReservedQuantity(ZERO); } - stock.setProduct(product); + stock.setSku(sku); fillBalance(stock); return stock; } @@ -146,9 +156,9 @@ public class GaoProductStockServiceImplement implements GaoProductStockService { return true; } List sortedItems = new ArrayList<>(items); - sortedItems.sort(Comparator.comparing(GaoStockChangeItem::getProductId)); + sortedItems.sort(Comparator.comparing(GaoStockChangeItem::getSkuId)); for (GaoStockChangeItem item : sortedItems) { - GaoProductStock stock = stockMapper.selectForUpdate(storeId, item.getProductId()); + GaoProductStock stock = stockMapper.selectForUpdate(storeId, item.getSkuId()); if (stock == null || decimal(stock.getOnHandQuantity()).subtract(decimal(stock.getReservedQuantity())).compareTo(item.getBaseQuantity()) < 0) { return false; } @@ -188,22 +198,23 @@ public class GaoProductStockServiceImplement implements GaoProductStockService { 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); + GaoProductSkuUnitSelection selection = skuService.getSkuUnit(storeId, request.getSkuId(), request.getSkuUnitId(), purchase); + GaoProductSku sku = selection.getSku(); 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); + GaoProductSkuUnit unit = selection.getUnit(); + TimiException.requiredTrue(Boolean.TRUE.equals(sku.getStockEnabled()), "该 SKU 未启用库存管理"); + BigDecimal quantity = validateQuantity(sku, request.getQuantity()); + BigDecimal baseQuantity = quantity.multiply(unit.getBaseQuantityPerUnit()); List existingList = recordService.listByRequestNo(storeId, requestNo); if (!existingList.isEmpty()) { GaoProductStockRecord existing = existingList.getFirst(); - verifyExisting(existing, operationType, product.getId(), unit.getId(), quantity, baseQuantity, request.getRemark()); - return detail(storeId, product.getId()); + verifyExisting(existing, operationType, sku.getId(), unit.getId(), quantity, baseQuantity, request.getRemark()); + return detail(storeId, sku.getId()); } TimiException.requiredTrue(hasAttachment(request.getAttachmentList()), "库存操作必须上传照片"); TimiException.requiredTrue(hasText(request.getRemark()), "库存操作必须填写备注"); - GaoStockChangeItem item = GaoStockChangeItem.of(product, unit, quantity, baseQuantity); - List result = executeInternal(storeId, product.getId(), List.of(item), requestNo, + GaoStockChangeItem item = GaoStockChangeItem.of(product, sku, unit, quantity, baseQuantity); + List result = executeInternal(storeId, sku.getId(), List.of(item), requestNo, userLoginService.getRequireLoginUserId(), operationType, request.getRemark(), request.getAttachmentList()); return result.getFirst(); } @@ -220,13 +231,13 @@ public class GaoProductStockServiceImplement implements GaoProductStockService { || source.getOperationType() == GaoProductStockRecord.OperationType.MANUAL_OUT, "只有手工入库或手工出库流水可以冲正"); TimiException.requiredNull(source.getSourceRecordId(), "只能冲正原始手工库存流水"); String storeId = requiredText(source.getStoreId(), "原库存流水缺少门店"); - String productId = requiredText(source.getProductId(), "原库存流水缺少产品"); + String skuId = requiredText(source.getSkuId(), "原库存流水缺少 SKU"); storeScopeService.checkStoreId(storeId); List existingList = recordService.listByRequestNo(storeId, requestNo); if (!existingList.isEmpty()) { verifyManualRevokeExisting(existingList, sourceRecordId, remark); - return detail(storeId, productId); + return detail(storeId, skuId); } TimiException.requiredNull(recordService.getRevokeBySourceRecordId(sourceRecordId), "该手工库存流水已经冲正"); TimiException.requiredTrue(hasAttachment(request.getAttachmentList()), "库存流水冲正必须上传照片"); @@ -236,7 +247,7 @@ public class GaoProductStockServiceImplement implements GaoProductStockService { BigDecimal sourceReservedDelta = decimal(source.getReservedDelta()); TimiException.requiredTrue(sourceReservedDelta.signum() == 0, "手工库存流水不能包含预占变动"); - GaoProductStock stock = lockOrCreateStock(storeId, productId); + GaoProductStock stock = lockOrCreateStock(storeId, skuId); BigDecimal onHandBefore = decimal(stock.getOnHandQuantity()); BigDecimal reservedBefore = decimal(stock.getReservedQuantity()); BigDecimal onHandDelta = source.getOnHandDelta().negate(); @@ -247,9 +258,13 @@ public class GaoProductStockServiceImplement implements GaoProductStockService { BigDecimal reservedAfter = reservedBefore.add(reservedDelta); GaoProductStockRecord revoke = new GaoProductStockRecord(); revoke.setStoreId(storeId); - revoke.setProductId(productId); - revoke.setProductUnitId(source.getProductUnitId()); + revoke.setSkuId(skuId); + revoke.setSkuUnitId(source.getSkuUnitId()); + revoke.setProductIdSnapshot(source.getProductIdSnapshot()); revoke.setProductNameSnapshot(source.getProductNameSnapshot()); + revoke.setSkuCodeSnapshot(source.getSkuCodeSnapshot()); + revoke.setSkuNameSnapshot(source.getSkuNameSnapshot()); + revoke.setSpecSummarySnapshot(source.getSpecSummarySnapshot()); revoke.setUnitSnapshot(source.getUnitSnapshot()); revoke.setBaseQuantityPerUnitSnapshot(source.getBaseQuantityPerUnitSnapshot()); revoke.setQuantity(source.getQuantity()); @@ -289,11 +304,11 @@ public class GaoProductStockServiceImplement implements GaoProductStockService { 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<>(); + itemList.sort(Comparator.comparing(GaoStockChangeItem::getSkuId)); + Set skuIdSet = new HashSet<>(); for (GaoStockChangeItem item : itemList) { TimiException.required(item, "库存变更项不能为空"); - TimiException.requiredTrue(productIdSet.add(item.getProductId()), "同一库存操作不能重复产品"); + TimiException.requiredTrue(skuIdSet.add(item.getSkuId()), "同一库存操作不能重复 SKU"); TimiException.required(item.getBaseQuantity(), "基础单位数量不能为空"); TimiException.requiredTrue(0 < item.getBaseQuantity().signum(), "基础单位数量必须大于 0"); } @@ -302,13 +317,13 @@ public class GaoProductStockServiceImplement implements GaoProductStockService { verifyExistingList(existingList, itemList, operationType, bizId); List result = new ArrayList<>(); for (GaoStockChangeItem item : itemList) { - result.add(detail(storeId, item.getProductId())); + result.add(detail(storeId, item.getSkuId())); } return result; } List result = new ArrayList<>(); for (GaoStockChangeItem item : itemList) { - GaoProductStock stock = lockOrCreateStock(storeId, item.getProductId()); + GaoProductStock stock = lockOrCreateStock(storeId, item.getSkuId()); BigDecimal onHandBefore = decimal(stock.getOnHandQuantity()); BigDecimal reservedBefore = decimal(stock.getReservedQuantity()); BigDecimal onHandDelta = onHandDelta(operationType, item.getBaseQuantity()); @@ -319,9 +334,13 @@ public class GaoProductStockServiceImplement implements GaoProductStockService { BigDecimal reservedAfter = reservedBefore.add(reservedDelta); GaoProductStockRecord record = new GaoProductStockRecord(); record.setStoreId(storeId); - record.setProductId(item.getProductId()); - record.setProductUnitId(item.getProductUnitId()); + record.setSkuId(item.getSkuId()); + record.setSkuUnitId(item.getSkuUnitId()); + record.setProductIdSnapshot(item.getProductIdSnapshot()); record.setProductNameSnapshot(item.getProductNameSnapshot()); + record.setSkuCodeSnapshot(item.getSkuCodeSnapshot()); + record.setSkuNameSnapshot(item.getSkuNameSnapshot()); + record.setSpecSummarySnapshot(item.getSpecSummarySnapshot()); record.setUnitSnapshot(item.getUnitSnapshot()); record.setBaseQuantityPerUnitSnapshot(item.getBaseQuantityPerUnitSnapshot()); record.setQuantity(item.getQuantity()); @@ -350,21 +369,21 @@ public class GaoProductStockServiceImplement implements GaoProductStockService { return result; } - private GaoProductStock lockOrCreateStock(String storeId, String productId) { - GaoProductStock stock = stockMapper.selectForUpdate(storeId, productId); + private GaoProductStock lockOrCreateStock(String storeId, String skuId) { + GaoProductStock stock = stockMapper.selectForUpdate(storeId, skuId); if (stock != null) { return stock; } stock = new GaoProductStock(); stock.setId(UUID.randomUUID().toString()); stock.setStoreId(storeId); - stock.setProductId(productId); + stock.setSkuId(skuId); stock.setOnHandQuantity(ZERO); stock.setReservedQuantity(ZERO); stock.setUpdatedBy(userLoginService.getRequireLoginUserId()); stock.setCreatedAt(Time.now()); stockMapper.insertIgnore(stock); - stock = stockMapper.selectForUpdate(storeId, productId); + stock = stockMapper.selectForUpdate(storeId, skuId); TimiException.required(stock, "库存余额创建失败"); return stock; } @@ -386,21 +405,21 @@ public class GaoProductStockServiceImplement implements GaoProductStockService { }; } - private BigDecimal validateQuantity(GaoProduct product, BigDecimal quantity) { + private BigDecimal validateQuantity(GaoProductSku sku, BigDecimal quantity) { TimiException.required(quantity, "数量不能为空"); TimiException.requiredTrue(0 < quantity.signum(), "数量必须大于 0"); TimiException.requiredTrue(3 >= quantity.stripTrailingZeros().scale(), "数量最多保留三位小数"); - if (!Boolean.TRUE.equals(product.getAllowDecimalQuantity())) { + if (!Boolean.TRUE.equals(sku.getAllowDecimalQuantity())) { TimiException.requiredTrue(quantity.stripTrailingZeros().scale() <= 0, "该产品不允许小数数量"); } return quantity; } - private void verifyExisting(GaoProductStockRecord existing, GaoProductStockRecord.OperationType operationType, String productId, String unitId, + private void verifyExisting(GaoProductStockRecord existing, GaoProductStockRecord.OperationType operationType, String skuId, String unitId, BigDecimal quantity, BigDecimal baseQuantity, String remark) { TimiException.requiredTrue(existing.getOperationType() == operationType - && productId.equals(existing.getProductId()) - && unitId.equals(existing.getProductUnitId()) + && skuId.equals(existing.getSkuId()) + && unitId.equals(existing.getSkuUnitId()) && quantity.compareTo(existing.getQuantity()) == 0 && baseQuantity.compareTo(existing.getBaseQuantity()) == 0 && sameText(existing.getRemark(), remark), "requestNo 对应库存操作内容不一致"); @@ -418,11 +437,11 @@ public class GaoProductStockServiceImplement implements GaoProductStockService { Map recordMap = new HashMap<>(); for (GaoProductStockRecord record : existingList) { TimiException.requiredTrue(record.getOperationType() == operationType && bizId.equals(record.getBizId()), "requestNo 已被其他库存操作使用"); - recordMap.put(record.getProductId(), record); + recordMap.put(record.getSkuId(), record); } TimiException.requiredTrue(recordMap.size() == itemList.size(), "requestNo 对应库存操作内容不一致"); for (GaoStockChangeItem item : itemList) { - GaoProductStockRecord record = recordMap.get(item.getProductId()); + GaoProductStockRecord record = recordMap.get(item.getSkuId()); TimiException.required(record, "requestNo 对应库存操作内容不一致"); TimiException.requiredTrue(item.getBaseQuantity().compareTo(record.getBaseQuantity()) == 0, "requestNo 对应库存操作内容不一致"); } @@ -432,8 +451,8 @@ public class GaoProductStockServiceImplement implements GaoProductStockService { 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())); + if (stock.getSku() == null && stock.getSkuId() != null) { + stock.setSku(skuService.get(stock.getSkuId())); } } 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 deleted file mode 100644 index c0d0022..0000000 --- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoProductUnitServiceImplement.java +++ /dev/null @@ -1,90 +0,0 @@ -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"); - TimiException.requiredTrue(sourceList != null && !sourceList.isEmpty(), "产品至少需要一个单位"); - List result = new ArrayList<>(); - Set nameSet = new HashSet<>(); - Set idSet = new HashSet<>(); - 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()); - if (unit.getId() != null) { - TimiException.requiredTrue(idSet.add(unit.getId()), "产品单位 ID 不能重复"); - } - unit.setName(requiredText(source.getName(), "产品单位名称不能为空")); - TimiException.requiredTrue(nameSet.add(unit.getName()), "同一产品的单位名称不能重复"); - 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()) { - baseCount++; - TimiException.requiredTrue(1 >= baseCount, "一个产品只能有一个基础单位"); - TimiException.requiredTrue(unit.getBaseQuantityPerUnit().compareTo(ONE) == 0, "基础单位数量系数必须为 1"); - } - result.add(unit); - } - TimiException.requiredTrue(0 < baseCount, "产品必须有一个基础单位"); - TimiException.requiredTrue(product.getProductType() != GaoProduct.ProductType.SERVICE || !Boolean.TRUE.equals(product.getStockEnabled()), "SERVICE 产品不能启用库存"); - return result; - } - - @Override - 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.getBaseQuantityPerUnit()); - try { - return result.setScale(3, RoundingMode.UNNECESSARY); - } catch (ArithmeticException e) { - throw new TimiException(com.imyeyu.java.bean.timi.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 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/GaoSalesOrderChartServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoSalesOrderChartServiceImplement.java index 7920820..2385ec7 100644 --- 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 @@ -4,6 +4,7 @@ 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.GaoCustomerOrderSkuRankView; import com.imyeyu.api.modules.gao.vo.GaoCustomerRepeatDailyStatView; import com.imyeyu.api.modules.gao.vo.GaoCustomerRepeatIntervalStatView; import com.imyeyu.api.modules.gao.vo.GaoSalesOrderChartReq; @@ -66,6 +67,14 @@ public class GaoSalesOrderChartServiceImplement implements GaoSalesOrderChartSer return mapper.selectCustomerProductRank(req); } + @Override + public List customerSkuRank(GaoSalesOrderChartReq req) { + checkRangeReq(req); + TimiException.required(req.getCustomerId(), "客户订单 SKU 排行必须指定客户"); + normalizeTopN(req); + return mapper.selectCustomerSkuRank(req); + } + @Override public List status(GaoSalesOrderChartReq req) { checkRangeReq(req); 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 85ba268..7e84042 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 @@ -10,7 +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.GaoFundRecord; import com.imyeyu.api.modules.gao.entity.GaoProduct; -import com.imyeyu.api.modules.gao.entity.GaoProductUnit; +import com.imyeyu.api.modules.gao.entity.GaoProductSku; +import com.imyeyu.api.modules.gao.entity.GaoProductSkuUnit; import com.imyeyu.api.modules.gao.entity.GaoSalesOrder; import com.imyeyu.api.modules.gao.entity.GaoSalesOrderItem; import com.imyeyu.api.modules.gao.entity.GaoStore; @@ -22,13 +23,13 @@ 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.GaoProductSkuService; 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.GaoProductSkuUnitSelection; import com.imyeyu.api.modules.gao.vo.GaoSalesOrderCancelRequest; import com.imyeyu.api.modules.gao.vo.GaoSalesOrderItemRequest; import com.imyeyu.api.modules.gao.vo.GaoSalesOrderPage; @@ -78,7 +79,7 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService fulfillmentServiceProvider; private final GaoCustomerService customerService; private final GaoProductService productService; - private final GaoProductUnitService unitService; + private final GaoProductSkuService skuService; private final GaoStoreService storeService; private final GaoStoreScopeService storeScopeService; private final GaoEventRecordService eventRecordService; @@ -347,20 +348,20 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService resolveDirectItems(String storeId, List sourceList) { TimiException.requiredTrue(sourceList != null && !sourceList.isEmpty(), "直接产品订单至少需要一个产品"); - Set productUnitKeySet = new HashSet<>(); + Set skuUnitKeySet = 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()); + TimiException.required(source.getSkuId(), "SKU 不能为空"); + TimiException.required(source.getSkuUnitId(), "SKU 单位不能为空"); + TimiException.requiredTrue(skuUnitKeySet.add(source.getSkuId() + "|" + source.getSkuUnitId()), "同一订单不能重复 SKU 和单位"); + GaoProductSkuUnitSelection selection = skuService.getSellableSkuUnit(storeId, source.getSkuId(), source.getSkuUnitId()); GaoProduct product = selection.getProduct(); - GaoProductUnit unit = selection.getUnit(); - BigDecimal quantity = validateQuantity(product, source.getQuantity()); - unitService.convertQuantityToBase(unit, quantity); + GaoProductSku sku = selection.getSku(); + GaoProductSkuUnit unit = selection.getUnit(); + BigDecimal quantity = validateQuantity(sku, source.getQuantity()); Long unitPrice = TimiJava.defaultIfNull(source.getUnitPrice(), unit.getSalePrice()); TimiException.required(unitPrice, "产品单价不能为空"); TimiException.requiredTrue(0 <= unitPrice, "产品单价不能为负数"); @@ -370,10 +371,14 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService requestList) { List existingList = itemMapper.selectBySalesOrderId(existing.getId()); TimiException.requiredTrue(requestList != null && existingList.size() == requestList.size(), "requestNo 对应订单内容不一致"); - Set productUnitKeySet = new HashSet<>(); + Set skuUnitKeySet = new HashSet<>(); for (GaoSalesOrderItemRequest request : requestList) { TimiException.required(request, "订单产品明细不能为空"); - TimiException.required(request.getProductId(), "产品不能为空"); - TimiException.required(request.getProductUnitId(), "产品单位不能为空"); + TimiException.required(request.getSkuId(), "SKU 不能为空"); + TimiException.required(request.getSkuUnitId(), "SKU 单位不能为空"); TimiException.required(request.getQuantity(), "购买数量不能为空"); - TimiException.requiredTrue(productUnitKeySet.add(request.getProductId() + "|" + request.getProductUnitId()), "同一订单不能重复产品和单位"); + TimiException.requiredTrue(skuUnitKeySet.add(request.getSkuId() + "|" + request.getSkuUnitId()), "同一订单不能重复 SKU 和单位"); GaoSalesOrderItem item = existingList.stream() - .filter(value -> value.getProductId().equals(request.getProductId()) && value.getProductUnitId().equals(request.getProductUnitId())) + .filter(value -> value.getSkuId().equals(request.getSkuId()) && value.getSkuUnitId().equals(request.getSkuUnitId())) .findFirst().orElse(null); TimiException.required(item, "requestNo 对应订单内容不一致"); - TimiException.requiredTrue(item.getProductUnitId().equals(request.getProductUnitId()) + TimiException.requiredTrue(item.getSkuUnitId().equals(request.getSkuUnitId()) && item.getQuantity().compareTo(request.getQuantity()) == 0 && (request.getUnitPrice() == null || item.getUnitPrice().equals(request.getUnitPrice())), "requestNo 对应订单内容不一致"); } @@ -434,15 +439,15 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService productUnitKeySet = new HashSet<>(); + Set skuUnitKeySet = new HashSet<>(); long lineAmountSum = 0L; for (GaoSalesOrderItem item : itemList) { TimiException.required(item, "销售订单明细不能为空"); TimiException.requiredTrue(item.getItemType() == expectedItemType, "订单项类型与订单类型不匹配"); - TimiException.required(item.getProductId(), "订单项产品不能为空"); - TimiException.required(item.getProductUnitId(), "订单项单位不能为空"); - TimiException.requiredTrue(productUnitKeySet.add(item.getProductId() + "|" + item.getProductUnitId()), - "同一订单不能重复产品和单位"); + TimiException.required(item.getSkuId(), "订单项 SKU 不能为空"); + TimiException.required(item.getSkuUnitId(), "订单项 SKU 单位不能为空"); + TimiException.requiredTrue(skuUnitKeySet.add(item.getSkuId() + "|" + item.getSkuUnitId()), + "同一订单不能重复 SKU 和单位"); TimiException.required(item.getQuantity(), "订单项数量不能为空"); TimiException.requiredTrue(0 < item.getQuantity().signum(), "订单项数量必须大于 0"); TimiException.requiredTrue(3 >= item.getQuantity().stripTrailingZeros().scale(), "订单项数量最多保留三位小数"); @@ -494,9 +499,9 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService existingItemList = itemMapper.selectBySalesOrderId(existing.getId()); TimiException.requiredTrue(existingItemList.size() == requestItemList.size(), "requestNo 对应订单内容不一致"); Map existingItemMap = existingItemList.stream() - .collect(java.util.stream.Collectors.toMap(item -> item.getProductId() + "|" + item.getProductUnitId(), java.util.function.Function.identity())); + .collect(java.util.stream.Collectors.toMap(item -> item.getSkuId() + "|" + item.getSkuUnitId(), java.util.function.Function.identity())); for (GaoSalesOrderItem requestItem : requestItemList) { - GaoSalesOrderItem existingItem = existingItemMap.get(requestItem.getProductId() + "|" + requestItem.getProductUnitId()); + GaoSalesOrderItem existingItem = existingItemMap.get(requestItem.getSkuId() + "|" + requestItem.getSkuUnitId()); TimiException.required(existingItem, "requestNo 对应订单内容不一致"); TimiException.requiredTrue(existingItem.getItemType() == GaoSalesOrderItem.ItemType.PACKAGE_ENTITLEMENT && existingItem.getQuantity().compareTo(requestItem.getQuantity()) == 0 @@ -550,20 +555,19 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService productIdList = itemList.stream() - .map(GaoSalesOrderItem::getProductId) + List skuIdList = itemList.stream() + .map(GaoSalesOrderItem::getSkuId) .filter(value -> value != null && !value.isBlank()) .distinct() .toList(); - if (productIdList.isEmpty()) { + if (skuIdList.isEmpty()) { return; } Map> attachmentMap = attachmentService.mapByBizIdList( - Attachment.BizType.GAO_PRODUCT, - productIdList, - GaoProduct.AttachType.PHOTO.name()); + Attachment.BizType.GAO_PRODUCT_SKU, + skuIdList); for (GaoSalesOrderItem item : itemList) { - item.setAttachmentList(attachmentMap.getOrDefault(item.getProductId(), List.of())); + item.setAttachmentList(attachmentMap.getOrDefault(item.getSkuId(), List.of())); } } @@ -591,11 +595,11 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService= quantity.stripTrailingZeros().scale(), "购买数量最多保留三位小数"); - if (!Boolean.TRUE.equals(product.getAllowDecimalQuantity())) { + if (!Boolean.TRUE.equals(sku.getAllowDecimalQuantity())) { TimiException.requiredTrue(quantity.stripTrailingZeros().scale() <= 0, "该产品不允许小数数量"); } return quantity; 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 index ee0ebbd..09d71f7 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerOrderProductRankView.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerOrderProductRankView.java @@ -37,4 +37,19 @@ public class GaoCustomerOrderProductRankView { /// 直接产品销售金额,单位分 private Long directSaleAmount; + + /// 直接产品购买数量,基础单位 + private BigDecimal directBaseQuantity; + + /// 套餐权益购买数量,基础单位 + private BigDecimal packageBaseQuantity; + + /// 已预占数量,基础单位 + private BigDecimal reservedBaseQuantity; + + /// 已履约数量,基础单位 + private BigDecimal fulfilledBaseQuantity; + + /// 当前可履约数量,基础单位 + private BigDecimal availableBaseQuantity; } diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerOrderSkuRankView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerOrderSkuRankView.java new file mode 100644 index 0000000..b3d1014 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoCustomerOrderSkuRankView.java @@ -0,0 +1,52 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +/// 客户订单 SKU 排行视图 +/// +/// @author Codex +/// @since 2026-09-07 +@Data +public class GaoCustomerOrderSkuRankView { + + /// SKU ID + private String skuId; + + /// SKU 编码快照 + private String skuCode; + + /// SKU 名称快照 + private String skuName; + + /// 规格摘要快照 + private String specSummary; + + /// 产品 ID + private String productId; + + /// 产品名称快照 + private String productName; + + /// 订单数 + private Long orderCount; + + /// 直接产品购买数量,基础单位 + private BigDecimal directBaseQuantity; + + /// 套餐权益购买数量,基础单位 + private BigDecimal packageBaseQuantity; + + /// 待履约数量,基础单位 + private BigDecimal reservedBaseQuantity; + + /// 已履约数量,基础单位 + private BigDecimal fulfilledBaseQuantity; + + /// 当前可履约数量,基础单位 + private BigDecimal availableBaseQuantity; + + /// 直接产品销售金额,单位分 + private Long directSaleAmount; +} 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 42ba0e6..9058943 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,15 +11,27 @@ import java.math.BigDecimal; @Data public class GaoCustomerPackagePurchaseItem { - /// 产品 ID - private String productId; + /// SKU ID + private String skuId; - /// 产品单位 ID - private String productUnitId; + /// SKU 单位 ID + private String skuUnitId; /// 产品名称快照,由后端生成 private String productNameSnapshot; + /// 产品 ID 快照,由后端生成 + private String productIdSnapshot; + + /// SKU 编码快照,由后端生成 + private String skuCodeSnapshot; + + /// SKU 名称快照,由后端生成 + private String skuNameSnapshot; + + /// 规格摘要快照,由后端生成 + private String specSummarySnapshot; + /// 单位快照,由后端生成 private String unitSnapshot; 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 index 619ecb1..51a8144 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductChartReq.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductChartReq.java @@ -16,6 +16,9 @@ public class GaoProductChartReq { /// 产品 ID private String productId; + /// SKU ID + private String skuId; + /// 分类 ID private String categoryId; 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 index 598c81b..648bdaa 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductFulfillmentDailyStatView.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductFulfillmentDailyStatView.java @@ -20,6 +20,12 @@ public class GaoProductFulfillmentDailyStatView { /// 套餐权益履约数量 private BigDecimal packageQuantity; + /// 直接产品履约数量,基础单位 + private BigDecimal directBaseQuantity; + + /// 套餐权益履约数量,基础单位 + private BigDecimal packageBaseQuantity; + /// 履约基础单位数量 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 index 0060243..86bcca5 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductFulfillmentRankView.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductFulfillmentRankView.java @@ -34,4 +34,19 @@ public class GaoProductFulfillmentRankView { /// 当前可履约数量 private BigDecimal availableQuantity; + + /// 直接产品购买数量,基础单位 + private BigDecimal directBaseQuantity; + + /// 套餐权益购买数量,基础单位 + private BigDecimal packageBaseQuantity; + + /// 待履约数量,基础单位 + private BigDecimal reservedBaseQuantity; + + /// 已履约数量,基础单位 + private BigDecimal fulfilledBaseQuantity; + + /// 当前可履约数量,基础单位 + private BigDecimal availableBaseQuantity; } 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 index 548f56c..5317fa3 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSalesDailyStatView.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSalesDailyStatView.java @@ -23,6 +23,12 @@ public class GaoProductSalesDailyStatView { /// 套餐权益数量 private BigDecimal packageQuantity; + /// 直接产品销售数量,基础单位 + private BigDecimal directBaseQuantity; + + /// 套餐权益数量,基础单位 + private BigDecimal packageBaseQuantity; + /// 直接产品销售金额,单位分 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 index e45b61a..ce10e8c 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSalesRankView.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSalesRankView.java @@ -26,6 +26,12 @@ public class GaoProductSalesRankView { /// 套餐权益数量 private BigDecimal packageQuantity; + /// 直接产品销售数量,基础单位 + private BigDecimal directBaseQuantity; + + /// 套餐权益数量,基础单位 + private BigDecimal packageBaseQuantity; + /// 直接产品销售金额,单位分 private Long directSaleAmount; } diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuFulfillmentDailyStatView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuFulfillmentDailyStatView.java new file mode 100644 index 0000000..70fd4bc --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuFulfillmentDailyStatView.java @@ -0,0 +1,43 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +/// SKU 每日履约统计视图 +/// +/// @author Codex +/// @since 2026-09-07 +@Data +public class GaoProductSkuFulfillmentDailyStatView { + + /// 日期值,格式为 yyyyMMdd + private Integer dateValue; + + /// SKU ID + private String skuId; + + /// SKU 编码快照 + private String skuCode; + + /// SKU 名称快照 + private String skuName; + + /// 规格摘要快照 + private String specSummary; + + /// 产品 ID + private String productId; + + /// 产品名称快照 + private String productName; + + /// 直接产品履约数量,基础单位 + private BigDecimal directBaseQuantity; + + /// 套餐权益履约数量,基础单位 + private BigDecimal packageBaseQuantity; + + /// 履约基础单位数量 + private BigDecimal baseQuantity; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuFulfillmentRankView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuFulfillmentRankView.java new file mode 100644 index 0000000..e2995de --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuFulfillmentRankView.java @@ -0,0 +1,49 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +/// SKU 履约进度排行视图 +/// +/// @author Codex +/// @since 2026-09-07 +@Data +public class GaoProductSkuFulfillmentRankView { + + /// SKU ID + private String skuId; + + /// SKU 编码快照 + private String skuCode; + + /// SKU 名称快照 + private String skuName; + + /// 规格摘要快照 + private String specSummary; + + /// 产品 ID + private String productId; + + /// 产品名称快照 + private String productName; + + /// 订单数 + private Long orderCount; + + /// 直接产品购买数量,基础单位 + private BigDecimal directBaseQuantity; + + /// 套餐权益购买数量,基础单位 + private BigDecimal packageBaseQuantity; + + /// 待履约数量,基础单位 + private BigDecimal reservedBaseQuantity; + + /// 已履约数量,基础单位 + private BigDecimal fulfilledBaseQuantity; + + /// 当前可履约数量,基础单位 + private BigDecimal availableBaseQuantity; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuSalesDailyStatView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuSalesDailyStatView.java new file mode 100644 index 0000000..713ebcd --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuSalesDailyStatView.java @@ -0,0 +1,46 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +/// SKU 每日销售统计视图 +/// +/// @author Codex +/// @since 2026-09-07 +@Data +public class GaoProductSkuSalesDailyStatView { + + /// 日期值,格式为 yyyyMMdd + private Integer dateValue; + + /// SKU ID + private String skuId; + + /// SKU 编码快照 + private String skuCode; + + /// SKU 名称快照 + private String skuName; + + /// 规格摘要快照 + private String specSummary; + + /// 产品 ID + private String productId; + + /// 产品名称快照 + private String productName; + + /// 包含该 SKU 的订单数 + private Long orderCount; + + /// 直接产品销售数量,基础单位 + private BigDecimal directBaseQuantity; + + /// 套餐权益数量,基础单位 + private BigDecimal packageBaseQuantity; + + /// 直接产品销售金额,单位分 + private Long directSaleAmount; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuSalesRankView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuSalesRankView.java new file mode 100644 index 0000000..192a9d6 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuSalesRankView.java @@ -0,0 +1,43 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +/// SKU 销售排行视图 +/// +/// @author Codex +/// @since 2026-09-07 +@Data +public class GaoProductSkuSalesRankView { + + /// SKU ID + private String skuId; + + /// SKU 编码快照 + private String skuCode; + + /// SKU 名称快照 + private String skuName; + + /// 规格摘要快照 + private String specSummary; + + /// 产品 ID + private String productId; + + /// 产品名称快照 + private String productName; + + /// 包含该 SKU 的订单数 + private Long orderCount; + + /// 直接产品销售数量,基础单位 + private BigDecimal directBaseQuantity; + + /// 套餐权益数量,基础单位 + private BigDecimal packageBaseQuantity; + + /// 直接产品销售金额,单位分 + private Long directSaleAmount; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuStockHealthView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuStockHealthView.java new file mode 100644 index 0000000..042b624 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuStockHealthView.java @@ -0,0 +1,56 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.gao.entity.GaoProduct; +import lombok.Data; + +import java.math.BigDecimal; + +/// SKU 库存健康度视图 +/// +/// @author Codex +/// @since 2026-09-07 +@Data +public class GaoProductSkuStockHealthView { + + /// SKU ID + private String skuId; + + /// SKU 编码 + private String skuCode; + + /// SKU 名称 + private String skuName; + + /// 规格摘要 + private String specSummary; + + /// 产品 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/GaoProductSkuStockMovementDailyStatView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuStockMovementDailyStatView.java new file mode 100644 index 0000000..a7f05dd --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuStockMovementDailyStatView.java @@ -0,0 +1,49 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +/// SKU 每日库存变动统计视图 +/// +/// @author Codex +/// @since 2026-09-07 +@Data +public class GaoProductSkuStockMovementDailyStatView { + + /// 日期值,格式为 yyyyMMdd + private Integer dateValue; + + /// SKU ID + private String skuId; + + /// SKU 编码快照 + private String skuCode; + + /// SKU 名称快照 + private String skuName; + + /// 规格摘要快照 + private String specSummary; + + /// 产品 ID + private String productId; + + /// 产品名称快照 + private String productName; + + /// 实际入库数量,基础单位 + 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/GaoProductSkuUnitSalesRankView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuUnitSalesRankView.java new file mode 100644 index 0000000..698d72f --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuUnitSalesRankView.java @@ -0,0 +1,49 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +/// SKU 单位销售排行视图 +/// +/// @author Codex +/// @since 2026-09-07 +@Data +public class GaoProductSkuUnitSalesRankView { + + /// SKU ID + private String skuId; + + /// SKU 编码快照 + private String skuCode; + + /// SKU 名称快照 + private String skuName; + + /// 规格摘要快照 + private String specSummary; + + /// 产品 ID + private String productId; + + /// 产品名称快照 + private String productName; + + /// SKU 单位 ID + private String skuUnitId; + + /// 单位名称快照 + private String unitName; + + /// 订单数 + private Long orderCount; + + /// 销售数量 + private BigDecimal quantity; + + /// 销售数量,基础单位 + private BigDecimal baseQuantity; + + /// 销售金额,单位分 + private Long saleAmount; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuUnitSelection.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuUnitSelection.java new file mode 100644 index 0000000..b411dd4 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductSkuUnitSelection.java @@ -0,0 +1,25 @@ +package com.imyeyu.api.modules.gao.vo; + +import com.imyeyu.api.modules.gao.entity.GaoProduct; +import com.imyeyu.api.modules.gao.entity.GaoProductSku; +import com.imyeyu.api.modules.gao.entity.GaoProductSkuUnit; +import lombok.AllArgsConstructor; +import lombok.Data; + +/// 已校验的 SKU 单位选项 +/// +/// @author Codex +/// @since 2026-09-06 +@Data +@AllArgsConstructor +public class GaoProductSkuUnitSelection { + + /// 产品 + private GaoProduct product; + + /// SKU + private GaoProductSku sku; + + /// SKU 单位 + private GaoProductSkuUnit unit; +} 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 index 69738d6..b161676 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockChangeRequest.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockChangeRequest.java @@ -19,11 +19,11 @@ public class GaoProductStockChangeRequest { /// 门店 ID private String storeId; - /// 产品 ID - private String productId; + /// SKU ID + private String skuId; - /// 产品单位 ID - private String productUnitId; + /// SKU 单位 ID + private String skuUnitId; /// 业务数量 private BigDecimal quantity; 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 index c4bc5df..c2b9aa7 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockHealthView.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockHealthView.java @@ -41,4 +41,10 @@ public class GaoProductStockHealthView { /// 1 为低于最低库存,0 为正常 private Integer lowStock; + + /// 参与库存统计的 SKU 数量 + private Long skuCount; + + /// 低于最低库存的 SKU 数量 + private Long lowStockSkuCount; } 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 index e9260cd..af3b0d8 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockPage.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockPage.java @@ -18,6 +18,6 @@ public class GaoProductStockPage extends BasePage { /// 产品 ID private String productId; - /// 产品名称或编码关键词 + /// 产品或 SKU 名称、编码、规格摘要关键词 private String keyword; } diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockProductView.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockProductView.java new file mode 100644 index 0000000..bb891d0 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductStockProductView.java @@ -0,0 +1,38 @@ +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-07 +@Data +public class GaoProductStockProductView { + + /// 产品 ID + private String productId; + + /// 产品编码 + private String productCode; + + /// 产品名称 + private String productName; + + /// 产品类型 + private GaoProduct.ProductType productType; + + /// 可库存 SKU 数量 + private Long skuCount; + + /// 库存数量,使用基础单位 + private BigDecimal onHandQuantity; + + /// 预占数量,使用基础单位 + private BigDecimal reservedQuantity; + + /// 可用数量,使用基础单位 + private BigDecimal availableQuantity; +} 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 index f2246f2..4427b10 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductUnitSalesRankView.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductUnitSalesRankView.java @@ -17,9 +17,24 @@ public class GaoProductUnitSalesRankView { /// 产品名称 private String productName; + /// SKU ID + private String skuId; + + /// SKU 编码快照 + private String skuCode; + + /// SKU 名称快照 + private String skuName; + + /// 规格摘要快照 + private String specSummary; + /// 产品单位 ID private String productUnitId; + /// SKU 单位 ID + private String skuUnitId; + /// 单位名称 private String unitName; @@ -29,6 +44,9 @@ public class GaoProductUnitSalesRankView { /// 销售数量 private BigDecimal quantity; + /// 销售数量,基础单位 + private BigDecimal baseQuantity; + /// 销售金额,单位分 private Long saleAmount; } 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 deleted file mode 100644 index d465143..0000000 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoProductUnitSelection.java +++ /dev/null @@ -1,21 +0,0 @@ -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/GaoSalesOrderChartReq.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderChartReq.java index 7f7825b..a484dc1 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderChartReq.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderChartReq.java @@ -16,6 +16,12 @@ public class GaoSalesOrderChartReq { /// 客户 ID private String customerId; + /// 产品 ID + private String productId; + + /// SKU ID + private String skuId; + /// 订单类型 private GaoSalesOrder.OrderType orderType; 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 index c88e65c..89f995c 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderItemRequest.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoSalesOrderItemRequest.java @@ -11,11 +11,11 @@ import java.math.BigDecimal; @Data public class GaoSalesOrderItemRequest { - /// 产品 ID - private String productId; + /// SKU ID + private String skuId; - /// 产品单位 ID - private String productUnitId; + /// SKU 单位 ID + private String skuUnitId; /// 购买数量 private BigDecimal quantity; 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 index 7eb5585..82f26ce 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoStockChangeItem.java +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoStockChangeItem.java @@ -1,7 +1,8 @@ package com.imyeyu.api.modules.gao.vo; import com.imyeyu.api.modules.gao.entity.GaoProduct; -import com.imyeyu.api.modules.gao.entity.GaoProductUnit; +import com.imyeyu.api.modules.gao.entity.GaoProductSku; +import com.imyeyu.api.modules.gao.entity.GaoProductSkuUnit; import lombok.Data; import java.math.BigDecimal; @@ -13,15 +14,27 @@ import java.math.BigDecimal; @Data public class GaoStockChangeItem { - /// 产品 ID - private String productId; + /// SKU ID + private String skuId; - /// 产品单位 ID - private String productUnitId; + /// SKU 单位 ID + private String skuUnitId; + + /// 产品 ID 快照 + private String productIdSnapshot; /// 产品名称快照 private String productNameSnapshot; + /// SKU 编码快照 + private String skuCodeSnapshot; + + /// SKU 名称快照 + private String skuNameSnapshot; + + /// 规格摘要快照 + private String specSummarySnapshot; + /// 单位名称快照 private String unitSnapshot; @@ -40,15 +53,20 @@ public class GaoStockChangeItem { /// 生成库存项 /// /// @param product 产品 - /// @param unit 单位 + /// @param sku SKU + /// @param unit SKU 单位 /// @param quantity 业务数量 /// @param baseQuantity 基础单位数量 /// @return 库存变更项 - public static GaoStockChangeItem of(GaoProduct product, GaoProductUnit unit, BigDecimal quantity, BigDecimal baseQuantity) { + public static GaoStockChangeItem of(GaoProduct product, GaoProductSku sku, GaoProductSkuUnit unit, BigDecimal quantity, BigDecimal baseQuantity) { GaoStockChangeItem result = new GaoStockChangeItem(); - result.productId = product.getId(); - result.productUnitId = unit.getId(); + result.skuId = sku.getId(); + result.skuUnitId = unit.getId(); + result.productIdSnapshot = product.getId(); result.productNameSnapshot = product.getName(); + result.skuCodeSnapshot = sku.getCode(); + result.skuNameSnapshot = sku.getName(); + result.specSummarySnapshot = sku.getSpecSummary(); result.unitSnapshot = unit.getName(); result.baseQuantityPerUnitSnapshot = unit.getBaseQuantityPerUnit(); result.quantity = quantity; diff --git a/src/main/resources/db/migration/timiserver/V70__refactor_gao_product_to_spu_sku.sql b/src/main/resources/db/migration/timiserver/V70__refactor_gao_product_to_spu_sku.sql new file mode 100644 index 0000000..901714c --- /dev/null +++ b/src/main/resources/db/migration/timiserver/V70__refactor_gao_product_to_spu_sku.sql @@ -0,0 +1,139 @@ +-- GAO 产品模块尚未上线,直接切换为 SPU -> SKU -> SKU 单位模型,不保留旧产品单位引用 + +CREATE TABLE `gao_product_spec` ( + `id` VARCHAR(36) NOT NULL, + `product_id` VARCHAR(36) NOT NULL COMMENT '产品 ID', + `name` VARCHAR(32) NOT NULL COMMENT '规格项名称', + `sort` INT NOT NULL DEFAULT 0 COMMENT '排序值', + `created_at` BIGINT NOT NULL, + `updated_at` BIGINT NULL, + `deleted_at` BIGINT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_gao_product_spec_product_name` (`product_id`, `name`), + KEY `idx_gao_product_spec_product` (`product_id`, `deleted_at`, `sort`, `created_at`) +) COMMENT='GAO 产品规格项'; + +CREATE TABLE `gao_product_spec_value` ( + `id` VARCHAR(36) NOT NULL, + `spec_id` VARCHAR(36) NOT NULL COMMENT '规格项 ID', + `name` VARCHAR(64) NOT NULL COMMENT '规格值名称', + `enabled` 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`), + UNIQUE KEY `uk_gao_product_spec_value_spec_name` (`spec_id`, `name`), + KEY `idx_gao_product_spec_value_spec` (`spec_id`, `enabled`, `deleted_at`, `sort`, `created_at`) +) COMMENT='GAO 产品规格值'; + +CREATE TABLE `gao_product_sku` ( + `id` VARCHAR(36) NOT NULL, + `store_id` VARCHAR(36) NOT NULL COMMENT '所属门店 ID', + `product_id` VARCHAR(36) NOT NULL COMMENT '所属产品 ID', + `code` VARCHAR(64) NOT NULL COMMENT '门店内 SKU 编码', + `barcode` VARCHAR(128) NULL COMMENT '门店内条码', + `name` VARCHAR(128) NOT NULL COMMENT 'SKU 展示名称', + `spec_summary` VARCHAR(255) NULL COMMENT '规格展示摘要', + `spec_value_key` VARCHAR(512) NOT NULL DEFAULT '' COMMENT '按规格项排序的规格值组合键', + `stock_enabled` TINYINT(1) NOT NULL COMMENT 'true 为启用库存管理', + `allow_decimal_quantity` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'true 为允许业务数量保留小数', + `min_stock_quantity` DECIMAL(18,3) NULL COMMENT '最低库存,基础单位', + `status` VARCHAR(32) NOT NULL COMMENT 'ACTIVE 启用,INACTIVE 停用', + `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_sku_store_code` (`store_id`, `code`), + UNIQUE KEY `uk_gao_product_sku_store_barcode` (`store_id`, `barcode`), + UNIQUE KEY `uk_gao_product_sku_product_spec_value_key` (`product_id`, `spec_value_key`), + KEY `idx_gao_product_sku_product` (`product_id`, `status`, `deleted_at`, `sort`, `name`), + KEY `idx_gao_product_sku_store_status` (`store_id`, `status`, `deleted_at`, `sort`, `name`) +) COMMENT='GAO 产品 SKU'; + +CREATE TABLE `gao_product_sku_spec_value` ( + `id` VARCHAR(36) NOT NULL, + `sku_id` VARCHAR(36) NOT NULL COMMENT 'SKU ID', + `spec_value_id` VARCHAR(36) NOT NULL COMMENT '规格值 ID', + `created_at` BIGINT NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_gao_product_sku_spec_value_sku_value` (`sku_id`, `spec_value_id`), + KEY `idx_gao_product_sku_spec_value_value` (`spec_value_id`, `sku_id`) +) COMMENT='GAO SKU 规格值关联'; + +CREATE TABLE `gao_product_sku_unit` ( + `id` VARCHAR(36) NOT NULL, + `sku_id` VARCHAR(36) NOT NULL COMMENT 'SKU ID', + `name` VARCHAR(32) NOT NULL COMMENT '单位名称', + `base_quantity_per_unit` DECIMAL(18,6) NOT NULL COMMENT '每个业务单位对应的基础单位数量', + `sale_price` BIGINT 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`), + UNIQUE KEY `uk_gao_product_sku_unit_sku_name` (`sku_id`, `name`), + KEY `idx_gao_product_sku_unit_sku` (`sku_id`, `deleted_at`, `sort`) +) COMMENT='GAO 产品 SKU 单位'; + +ALTER TABLE `gao_product` + DROP COLUMN `allow_decimal_quantity`, + DROP COLUMN `min_stock_quantity`; + +DROP TABLE `gao_product_unit`; + +ALTER TABLE `gao_product_stock` + DROP KEY `uk_gao_product_stock_store_product`, + DROP KEY `idx_gao_product_stock_product`, + CHANGE COLUMN `product_id` `sku_id` VARCHAR(36) NOT NULL COMMENT 'SKU ID', + ADD UNIQUE KEY `uk_gao_product_stock_store_sku` (`store_id`, `sku_id`), + ADD KEY `idx_gao_product_stock_sku` (`sku_id`); + +ALTER TABLE `gao_product_stock_record` + DROP KEY `uk_gao_product_stock_record_request_product`, + DROP KEY `idx_gao_product_stock_record_product_time`, + CHANGE COLUMN `product_id` `sku_id` VARCHAR(36) NOT NULL COMMENT 'SKU ID', + CHANGE COLUMN `product_unit_id` `sku_unit_id` VARCHAR(36) NOT NULL COMMENT 'SKU 单位 ID', + ADD COLUMN `product_id_snapshot` VARCHAR(36) NOT NULL COMMENT '产品 ID 快照' AFTER `sku_unit_id`, + ADD COLUMN `sku_code_snapshot` VARCHAR(64) NOT NULL COMMENT 'SKU 编码快照' AFTER `product_name_snapshot`, + ADD COLUMN `sku_name_snapshot` VARCHAR(128) NOT NULL COMMENT 'SKU 名称快照' AFTER `sku_code_snapshot`, + ADD COLUMN `spec_summary_snapshot` VARCHAR(255) NULL COMMENT '规格摘要快照' AFTER `sku_name_snapshot`, + ADD UNIQUE KEY `uk_gao_product_stock_record_request_sku` (`store_id`, `request_no`, `operation_type`, `sku_id`), + ADD KEY `idx_gao_product_stock_record_sku_time` (`store_id`, `sku_id`, `occurred_at`, `id`); + +ALTER TABLE `gao_sales_order_item` + DROP KEY `uk_gao_sales_order_item_product_unit`, + CHANGE COLUMN `product_id` `sku_id` VARCHAR(36) NOT NULL COMMENT 'SKU ID', + CHANGE COLUMN `product_unit_id` `sku_unit_id` VARCHAR(36) NOT NULL COMMENT 'SKU 单位 ID', + ADD COLUMN `product_id_snapshot` VARCHAR(36) NOT NULL COMMENT '产品 ID 快照' AFTER `sku_unit_id`, + ADD COLUMN `sku_code_snapshot` VARCHAR(64) NOT NULL COMMENT 'SKU 编码快照' AFTER `product_name_snapshot`, + ADD COLUMN `sku_name_snapshot` VARCHAR(128) NOT NULL COMMENT 'SKU 名称快照' AFTER `sku_code_snapshot`, + ADD COLUMN `spec_summary_snapshot` VARCHAR(255) NULL COMMENT '规格摘要快照' AFTER `sku_name_snapshot`, + ADD UNIQUE KEY `uk_gao_sales_order_item_sku_unit` (`sales_order_id`, `sku_id`, `sku_unit_id`); + +ALTER TABLE `gao_package_config_item` + DROP KEY `idx_gao_package_config_item_product`, + DROP KEY `uk_gao_package_config_item_product_unit`, + CHANGE COLUMN `product_id` `sku_id` VARCHAR(36) NOT NULL COMMENT 'SKU ID', + CHANGE COLUMN `product_unit_id` `sku_unit_id` VARCHAR(36) NOT NULL COMMENT 'SKU 单位 ID', + ADD COLUMN `product_id_snapshot` VARCHAR(36) NOT NULL COMMENT '产品 ID 快照' AFTER `sku_unit_id`, + ADD COLUMN `sku_code_snapshot` VARCHAR(64) NOT NULL COMMENT 'SKU 编码快照' AFTER `product_name_snapshot`, + ADD COLUMN `sku_name_snapshot` VARCHAR(128) NOT NULL COMMENT 'SKU 名称快照' AFTER `sku_code_snapshot`, + ADD COLUMN `spec_summary_snapshot` VARCHAR(255) NULL COMMENT '规格摘要快照' AFTER `sku_name_snapshot`, + ADD KEY `idx_gao_package_config_item_sku` (`sku_id`, `sku_unit_id`), + ADD UNIQUE KEY `uk_gao_package_config_item_sku_unit` (`package_config_id`, `sku_id`, `sku_unit_id`); + +ALTER TABLE `gao_order_fulfillment_item` + CHANGE COLUMN `product_id` `sku_id` VARCHAR(36) NOT NULL COMMENT 'SKU ID', + CHANGE COLUMN `product_unit_id` `sku_unit_id` VARCHAR(36) NOT NULL COMMENT 'SKU 单位 ID', + ADD COLUMN `product_id_snapshot` VARCHAR(36) NOT NULL COMMENT '产品 ID 快照' AFTER `sku_unit_id`, + ADD COLUMN `sku_code_snapshot` VARCHAR(64) NOT NULL COMMENT 'SKU 编码快照' AFTER `product_name_snapshot`, + ADD COLUMN `sku_name_snapshot` VARCHAR(128) NOT NULL COMMENT 'SKU 名称快照' AFTER `sku_code_snapshot`, + ADD COLUMN `spec_summary_snapshot` VARCHAR(255) NULL COMMENT '规格摘要快照' AFTER `sku_name_snapshot`, + ADD KEY `idx_gao_order_fulfillment_item_sku` (`sku_id`, `sku_unit_id`); diff --git a/src/main/resources/mapper/timi-server/gao/GaoProductChartMapper.xml b/src/main/resources/mapper/timi-server/gao/GaoProductChartMapper.xml index 6c2e9e2..7678d4d 100644 --- a/src/main/resources/mapper/timi-server/gao/GaoProductChartMapper.xml +++ b/src/main/resources/mapper/timi-server/gao/GaoProductChartMapper.xml @@ -5,6 +5,10 @@ AND p.`deleted_at` IS NULL AND p.`store_id` = #{storeId} AND p.`id` = #{productId} + AND EXISTS ( + SELECT 1 FROM `gao_product_sku` k_filter + WHERE k_filter.`id` = #{skuId} AND k_filter.`product_id` = p.`id` AND k_filter.`deleted_at` IS NULL + ) AND p.`category_id` = #{categoryId} AND p.`product_type` = #{productType} @@ -14,7 +18,8 @@ AND i.`deleted_at` IS NULL AND o.`status` <> 'CANCELLED' AND o.`store_id` = #{storeId} - AND i.`product_id` = #{productId} + 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} @@ -27,10 +32,12 @@ 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 DATE_FORMAT(FROM_UNIXTIME(o.`purchased_at` / 1000), '%Y%m%d') @@ -39,22 +46,24 @@ @@ -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 @@ + +