v1.0.31 #43

Merged
timi merged 10 commits from dev into master 2026-09-07 07:40:03 +00:00
84 changed files with 2697 additions and 620 deletions
Showing only changes of commit 37dc1c9904 - Show all commits
@@ -40,6 +40,8 @@ public class Attachment extends UUIDEntity {
GAO_PRODUCT, GAO_PRODUCT,
GAO_PRODUCT_SKU,
GAO_EVENT_RECORD, GAO_EVENT_RECORD,
GAO_FUND_RECORD, GAO_FUND_RECORD,
@@ -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.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.entity.GaoPackageConfig; import com.imyeyu.api.modules.gao.entity.GaoPackageConfig;
import com.imyeyu.api.modules.gao.entity.GaoPackageConfigItem; 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.GaoEventService;
import com.imyeyu.api.modules.gao.service.GaoPackageConfigService; 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.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoPackageConfigPage; import com.imyeyu.api.modules.gao.vo.GaoPackageConfigPage;
import com.imyeyu.api.modules.user.entity.User; import com.imyeyu.api.modules.user.entity.User;
@@ -37,7 +37,7 @@ public class GaoPackageConfigController {
private final GaoPackageConfigService service; private final GaoPackageConfigService service;
private final GaoEventService eventService; private final GaoEventService eventService;
private final GaoProductService productService; private final GaoProductSkuService skuService;
private final GaoStoreScopeService storeScopeService; private final GaoStoreScopeService storeScopeService;
private final UserService userService; private final UserService userService;
@@ -92,20 +92,20 @@ public class GaoPackageConfigController {
} }
private void fillProductDetails(List<GaoPackageConfig> configList) { private void fillProductDetails(List<GaoPackageConfig> configList) {
List<String> productIdList = configList.stream() List<String> skuIdList = configList.stream()
.flatMap(config -> config.getItemList().stream()) .flatMap(config -> config.getItemList().stream())
.map(GaoPackageConfigItem::getProductId) .map(GaoPackageConfigItem::getSkuId)
.filter(value -> value != null && !value.isBlank()) .filter(value -> value != null && !value.isBlank())
.distinct() .distinct()
.toList(); .toList();
if (productIdList.isEmpty()) { if (skuIdList.isEmpty()) {
return; return;
} }
Map<String, GaoProduct> productMap = productService.listByIdList(productIdList).stream() Map<String, GaoProductSku> skuMap = skuIdList.stream()
.collect(java.util.stream.Collectors.toMap(GaoProduct::getId, item -> item)); .collect(java.util.stream.Collectors.toMap(item -> item, skuService::detail));
for (GaoPackageConfig config : configList) { for (GaoPackageConfig config : configList) {
for (GaoPackageConfigItem item : config.getItemList()) { for (GaoPackageConfigItem item : config.getItemList()) {
item.setProduct(productMap.get(item.getProductId())); item.setSku(skuMap.get(item.getSkuId()));
} }
} }
} }
@@ -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.bean.GaoPermissionCode;
import com.imyeyu.api.modules.gao.entity.GaoProduct; import com.imyeyu.api.modules.gao.entity.GaoProduct;
import com.imyeyu.api.modules.gao.entity.GaoProductCategory; 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.GaoProductCategoryService;
import com.imyeyu.api.modules.gao.service.GaoProductService; 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.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoProductCategoryPage; import com.imyeyu.api.modules.gao.vo.GaoProductCategoryPage;
import com.imyeyu.api.modules.gao.vo.GaoProductPage; import com.imyeyu.api.modules.gao.vo.GaoProductPage;
@@ -33,6 +38,8 @@ public class GaoProductController {
private final GaoProductCategoryService categoryService; private final GaoProductCategoryService categoryService;
private final GaoProductService productService; private final GaoProductService productService;
private final GaoProductSkuService skuService;
private final GaoProductSpecService specService;
private final GaoStoreScopeService storeScopeService; private final GaoStoreScopeService storeScopeService;
@AOPLog @AOPLog
@@ -142,6 +149,93 @@ public class GaoProductController {
productService.disable(id); 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<GaoProductSpec> 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 @AOPLog
@RequestRateLimit @RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.PRODUCT_READ) @RequireGaoPermission(GaoPermissionCode.PRODUCT_READ)
@@ -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<GaoProductSkuSalesDailyStatView> salesDaily(@RequestBody GaoProductChartReq req) {
return service.salesDaily(normalize(req));
}
/// 查询 SKU 销售排行
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.STAT_READ)
@PostMapping("/sales/rank")
public List<GaoProductSkuSalesRankView> salesRank(@RequestBody GaoProductChartReq req) {
return service.salesRank(normalize(req));
}
/// 查询 SKU 单位销售排行
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.STAT_READ)
@PostMapping("/sales/unit/rank")
public List<GaoProductSkuUnitSalesRankView> unitSalesRank(@RequestBody GaoProductChartReq req) {
return service.unitSalesRank(normalize(req));
}
/// 查询 SKU 每日履约统计
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.STAT_READ)
@PostMapping("/fulfillment/daily")
public List<GaoProductSkuFulfillmentDailyStatView> fulfillmentDaily(@RequestBody GaoProductChartReq req) {
return service.fulfillmentDaily(normalize(req));
}
/// 查询 SKU 履约进度排行
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.STAT_READ)
@PostMapping("/fulfillment/rank")
public List<GaoProductSkuFulfillmentRankView> fulfillmentRank(@RequestBody GaoProductChartReq req) {
return service.fulfillmentRank(normalize(req));
}
/// 查询 SKU 每日库存变动统计
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.STAT_READ)
@PostMapping("/stock/movement/daily")
public List<GaoProductSkuStockMovementDailyStatView> stockMovementDaily(@RequestBody GaoProductChartReq req) {
return service.stockMovementDaily(normalize(req));
}
/// 查询 SKU 库存健康度
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.STAT_READ)
@PostMapping("/stock/health")
public List<GaoProductSkuStockHealthView> 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;
}
}
@@ -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.GaoProductStockChangeRequest;
import com.imyeyu.api.modules.gao.vo.GaoProductStockManualRevokeRequest; import com.imyeyu.api.modules.gao.vo.GaoProductStockManualRevokeRequest;
import com.imyeyu.api.modules.gao.vo.GaoProductStockPage; 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.api.modules.gao.vo.GaoProductStockRecordPage;
import com.imyeyu.java.bean.timi.TimiException; import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.annotation.AOPLog; import com.imyeyu.spring.annotation.AOPLog;
@@ -45,12 +46,22 @@ public class GaoProductStockController {
return stockService.pageByQuery(page); return stockService.pageByQuery(page);
} }
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.PRODUCT_STOCK_READ)
@PostMapping("/product-list")
public PageResult<GaoProductStockProductView> productList(@RequestBody GaoProductStockPage page) {
TimiException.required(page, "not found stock product page");
page.setStoreId(storeScopeService.resolveStoreId(page.getStoreId()));
return stockService.pageProductByQuery(page);
}
@AOPLog @AOPLog
@RequestRateLimit @RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.PRODUCT_STOCK_READ) @RequireGaoPermission(GaoPermissionCode.PRODUCT_STOCK_READ)
@PostMapping("/detail") @PostMapping("/detail")
public GaoProductStock detail(@RequestParam(required = false) String storeId, @RequestParam String productId) { public GaoProductStock detail(@RequestParam(required = false) String storeId, @RequestParam String skuId) {
return stockService.detail(storeScopeService.resolveStoreId(storeId), productId); return stockService.detail(storeScopeService.resolveStoreId(storeId), skuId);
} }
@AOPLog @AOPLog
@@ -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.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoCustomerOrderProductRankView; import com.imyeyu.api.modules.gao.vo.GaoCustomerOrderProductRankView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerOrderRankView; 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.GaoCustomerRepeatDailyStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerRepeatIntervalStatView; import com.imyeyu.api.modules.gao.vo.GaoCustomerRepeatIntervalStatView;
import com.imyeyu.api.modules.gao.vo.GaoSalesOrderChartReq; import com.imyeyu.api.modules.gao.vo.GaoSalesOrderChartReq;
@@ -84,6 +85,15 @@ public class GaoSalesOrderChartController {
return service.customerProductRank(normalize(req)); return service.customerProductRank(normalize(req));
} }
/// 查询指定客户订单 SKU 排行
@AOPLog
@RequestRateLimit
@RequireGaoPermission(GaoPermissionCode.STAT_READ)
@PostMapping("/customer/sku/rank")
public List<GaoCustomerOrderSkuRankView> customerSkuRank(@RequestBody GaoSalesOrderChartReq req) {
return service.customerSkuRank(normalize(req));
}
/// 查询销售订单状态统计 /// 查询销售订单状态统计
@AOPLog @AOPLog
@RequestRateLimit @RequestRateLimit
@@ -25,11 +25,11 @@ public class GaoOrderFulfillmentItem implements IDEntity<String>, Creatable {
/// 履约单 ID /// 履约单 ID
private String fulfillmentId; private String fulfillmentId;
/// 产品 ID /// SKU ID
private String productId; private String skuId;
/// 产品单位 ID /// SKU 单位 ID
private String productUnitId; private String skuUnitId;
/// 产品图片附件 /// 产品图片附件
@Transient @Transient
@@ -59,6 +59,18 @@ public class GaoOrderFulfillmentItem implements IDEntity<String>, Creatable {
/// 产品名称快照 /// 产品名称快照
private String productNameSnapshot; private String productNameSnapshot;
/// 产品 ID 快照
private String productIdSnapshot;
/// SKU 编码快照
private String skuCodeSnapshot;
/// SKU 名称快照
private String skuNameSnapshot;
/// 规格摘要快照
private String specSummarySnapshot;
/// 单位快照 /// 单位快照
private String unitSnapshot; private String unitSnapshot;
@@ -18,15 +18,27 @@ public class GaoPackageConfigItem extends UUIDEntity {
/// 套餐配置 ID /// 套餐配置 ID
private String packageConfigId; private String packageConfigId;
/// 产品 ID /// SKU ID
private String productId; private String skuId;
/// 产品单位 ID /// SKU 单位 ID
private String productUnitId; private String skuUnitId;
/// 产品名称快照 /// 产品名称快照
private String productNameSnapshot; private String productNameSnapshot;
/// 产品 ID 快照
private String productIdSnapshot;
/// SKU 编码快照
private String skuCodeSnapshot;
/// SKU 名称快照
private String skuNameSnapshot;
/// 规格摘要快照
private String specSummarySnapshot;
/// 单位快照 /// 单位快照
private String unitSnapshot; private String unitSnapshot;
@@ -42,7 +54,7 @@ public class GaoPackageConfigItem extends UUIDEntity {
/// 备注 /// 备注
private String remark; private String remark;
/// 产品详情 /// SKU 详情
@Transient @Transient
private GaoProduct product; private GaoProductSku sku;
} }
@@ -6,7 +6,6 @@ import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.util.List; import java.util.List;
/// GAO 产品 /// GAO 产品
@@ -68,15 +67,9 @@ public class GaoProduct extends UUIDEntity {
/// true 为启用库存管理 /// true 为启用库存管理
private Boolean stockEnabled; private Boolean stockEnabled;
/// true 为允许业务数量保留小数
private Boolean allowDecimalQuantity;
/// 状态 /// 状态
private Status status; private Status status;
/// 最低库存数量,基础单位
private BigDecimal minStockQuantity;
/// 排序值 /// 排序值
private Integer sort; private Integer sort;
@@ -90,9 +83,13 @@ public class GaoProduct extends UUIDEntity {
@Transient @Transient
private GaoProductCategory category; private GaoProductCategory category;
/// 产品单位列表 /// 产品规格列表
@Transient @Transient
private List<GaoProductUnit> unitList; private List<GaoProductSpec> specList;
/// 产品 SKU 列表
@Transient
private List<GaoProductSku> skuList;
/// 产品附件列表 /// 产品附件列表
@Transient @Transient
@@ -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<String> specValueIdList;
/// SKU 单位列表
@Transient
private List<GaoProductSkuUnit> unitList;
/// SKU 附件列表
@Transient
private List<Attachment> attachmentList;
}
@@ -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<String>, Creatable {
@Id
@AutoUUID
private String id;
/// SKU ID
private String skuId;
/// 规格值 ID
private String specValueId;
private Long createdAt;
}
@@ -6,16 +6,16 @@ import lombok.EqualsAndHashCode;
import java.math.BigDecimal; import java.math.BigDecimal;
/// GAO 产品业务单位 /// GAO 产品 SKU 业务单位
/// ///
/// @author Codex /// @author Codex
/// @since 2026-09-02 /// @since 2026-09-06
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class GaoProductUnit extends UUIDEntity { public class GaoProductSkuUnit extends UUIDEntity {
/// 产品 ID /// SKU ID
private String productId; private String skuId;
/// 单位名称 /// 单位名称
private String name; private String name;
@@ -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<GaoProductSpecValue> valueList;
}
@@ -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;
}
@@ -18,8 +18,8 @@ public class GaoProductStock extends UUIDEntity {
/// 门店 ID /// 门店 ID
private String storeId; private String storeId;
/// 产品 ID /// SKU ID
private String productId; private String skuId;
/// 实际库存,基础单位 /// 实际库存,基础单位
private BigDecimal onHandQuantity; private BigDecimal onHandQuantity;
@@ -30,9 +30,9 @@ public class GaoProductStock extends UUIDEntity {
/// 最后操作人 /// 最后操作人
private String updatedBy; private String updatedBy;
/// 产品 /// SKU
@Transient @Transient
private GaoProduct product; private GaoProductSku sku;
/// 可用库存,基础单位 /// 可用库存,基础单位
@Transient @Transient
@@ -70,15 +70,27 @@ public class GaoProductStockRecord implements IDEntity<String>, Creatable {
/// 门店 ID /// 门店 ID
private String storeId; private String storeId;
/// 产品 ID /// SKU ID
private String productId; private String skuId;
/// 产品单位 ID /// SKU 单位 ID
private String productUnitId; private String skuUnitId;
/// 产品名称快照 /// 产品名称快照
private String productNameSnapshot; private String productNameSnapshot;
/// 产品 ID 快照
private String productIdSnapshot;
/// SKU 编码快照
private String skuCodeSnapshot;
/// SKU 名称快照
private String skuNameSnapshot;
/// 规格摘要快照
private String specSummarySnapshot;
/// 单位名称快照 /// 单位名称快照
private String unitSnapshot; private String unitSnapshot;
@@ -33,11 +33,11 @@ public class GaoSalesOrderItem extends UUIDEntity {
/// 订单项类型 /// 订单项类型
private ItemType itemType; private ItemType itemType;
/// 产品 ID /// SKU ID
private String productId; private String skuId;
/// 产品单位 ID /// SKU 单位 ID
private String productUnitId; private String skuUnitId;
/// 产品图片附件 /// 产品图片附件
@Transient @Transient
@@ -46,6 +46,18 @@ public class GaoSalesOrderItem extends UUIDEntity {
/// 产品名称快照 /// 产品名称快照
private String productNameSnapshot; private String productNameSnapshot;
/// 产品 ID 快照
private String productIdSnapshot;
/// SKU 编码快照
private String skuCodeSnapshot;
/// SKU 名称快照
private String skuNameSnapshot;
/// 规格摘要快照
private String specSummarySnapshot;
/// 单位快照 /// 单位快照
private String unitSnapshot; private String unitSnapshot;
@@ -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<GaoProductSkuSalesDailyStatView> selectSalesDaily(GaoProductChartReq req);
/// 查询 SKU 销售排行
List<GaoProductSkuSalesRankView> selectSalesRank(GaoProductChartReq req);
/// 查询 SKU 单位销售排行
List<GaoProductSkuUnitSalesRankView> selectUnitSalesRank(GaoProductChartReq req);
/// 查询 SKU 每日履约统计
List<GaoProductSkuFulfillmentDailyStatView> selectFulfillmentDaily(GaoProductChartReq req);
/// 查询 SKU 履约进度排行
List<GaoProductSkuFulfillmentRankView> selectFulfillmentRank(GaoProductChartReq req);
/// 查询 SKU 每日库存变动统计
List<GaoProductSkuStockMovementDailyStatView> selectStockMovementDaily(GaoProductChartReq req);
/// 查询 SKU 库存健康度
List<GaoProductSkuStockHealthView> selectStockHealth(GaoProductChartReq req);
}
@@ -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<GaoProductSku, String> {
/// 按产品查询 SKU
List<GaoProductSku> selectByProductId(@Param("productId") String productId);
/// 批量按产品查询 SKU
List<GaoProductSku> selectByProductIdList(@Param("productIdList") Collection<String> 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);
}
@@ -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<GaoProductSkuSpecValue, String> {
/// 批量查询 SKU 规格值关联
List<GaoProductSkuSpecValue> selectBySkuIdList(@Param("skuIdList") Collection<String> skuIdList);
/// 删除 SKU 的规格值关联
void deleteBySkuId(@Param("skuId") String skuId);
}
@@ -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<GaoProductSkuUnit, String> {
/// 按 SKU 查询有效单位
List<GaoProductSkuUnit> selectBySkuId(@Param("skuId") String skuId);
/// 批量查询 SKU 有效单位
List<GaoProductSkuUnit> selectBySkuIdList(@Param("skuIdList") Collection<String> skuIdList);
/// 锁定单位
GaoProductSkuUnit selectForUpdate(@Param("id") String id);
/// 更新有效单位
int updateUnit(GaoProductSkuUnit unit);
}
@@ -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<GaoProductSpec, String> {
/// 按产品查询规格项
List<GaoProductSpec> selectByProductId(@Param("productId") String productId);
/// 批量按产品查询规格项
List<GaoProductSpec> selectByProductIdList(@Param("productIdList") Collection<String> productIdList);
/// 查询规格值所属产品
String selectProductIdBySpecValueId(@Param("specValueId") String specValueId);
/// 查询规格项所属产品
String selectProductIdBySpecId(@Param("specId") String specId);
}
@@ -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<GaoProductSpecValue, String> {
/// 按 ID 查询有效规格值
GaoProductSpecValue selectById(@Param("id") String id);
/// 按规格项查询规格值
List<GaoProductSpecValue> selectBySpecIdList(@Param("specIdList") Collection<String> specIdList);
}
@@ -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.entity.GaoProductStock;
import com.imyeyu.api.modules.gao.vo.GaoProductStockPage; import com.imyeyu.api.modules.gao.vo.GaoProductStockPage;
import com.imyeyu.api.modules.gao.vo.GaoProductStockProductView;
import com.imyeyu.spring.mapper.BaseMapper; import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
@@ -14,7 +15,7 @@ import java.util.List;
public interface GaoProductStockMapper extends BaseMapper<GaoProductStock, String> { public interface GaoProductStockMapper extends BaseMapper<GaoProductStock, String> {
/// 锁定库存余额 /// 锁定库存余额
GaoProductStock selectForUpdate(@Param("storeId") String storeId, @Param("productId") String productId); GaoProductStock selectForUpdate(@Param("storeId") String storeId, @Param("skuId") String skuId);
/// 按条件统计库存余额 /// 按条件统计库存余额
long countByQuery(GaoProductStockPage page); long countByQuery(GaoProductStockPage page);
@@ -22,8 +23,14 @@ public interface GaoProductStockMapper extends BaseMapper<GaoProductStock, Strin
/// 按条件查询库存余额 /// 按条件查询库存余额
List<GaoProductStock> selectByQuery(GaoProductStockPage page); List<GaoProductStock> selectByQuery(GaoProductStockPage page);
/// 按产品汇总库存数量
long countProductByQuery(GaoProductStockPage page);
/// 按产品分页查询库存汇总
List<GaoProductStockProductView> 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); int updateQuantity(@Param("id") String id, @Param("onHandDelta") java.math.BigDecimal onHandDelta, @Param("reservedDelta") java.math.BigDecimal reservedDelta, @Param("updatedBy") String updatedBy);
@@ -26,7 +26,7 @@ public interface GaoProductStockRecordMapper extends BaseMapper<GaoProductStockR
List<GaoProductStockRecord> selectByBiz(@Param("bizType") GaoProductStockRecord.BizType bizType, @Param("bizId") String bizId); List<GaoProductStockRecord> 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); long countByQuery(GaoProductStockRecordPage page);
@@ -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<GaoProductUnit, String> {
/// 按产品查询有效单位
List<GaoProductUnit> selectByProductId(@Param("productId") String productId);
/// 批量查询产品有效单位
List<GaoProductUnit> selectByProductIdList(@Param("productIdList") Collection<String> productIdList);
/// 锁定单位
GaoProductUnit selectForUpdate(@Param("id") String id);
/// 更新有效单位
int updateUnit(GaoProductUnit unit);
}
@@ -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.GaoCustomerOrderProductRankView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerOrderRankView; 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.GaoCustomerRepeatDailyStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerRepeatIntervalStatView; import com.imyeyu.api.modules.gao.vo.GaoCustomerRepeatIntervalStatView;
import com.imyeyu.api.modules.gao.vo.GaoSalesOrderChartReq; import com.imyeyu.api.modules.gao.vo.GaoSalesOrderChartReq;
@@ -36,6 +37,9 @@ public interface GaoSalesOrderChartMapper {
/// 查询客户订单产品排行 /// 查询客户订单产品排行
List<GaoCustomerOrderProductRankView> selectCustomerProductRank(GaoSalesOrderChartReq req); List<GaoCustomerOrderProductRankView> selectCustomerProductRank(GaoSalesOrderChartReq req);
/// 查询客户订单 SKU 排行
List<GaoCustomerOrderSkuRankView> selectCustomerSkuRank(GaoSalesOrderChartReq req);
/// 查询销售订单状态统计 /// 查询销售订单状态统计
List<GaoSalesOrderStatusStatView> selectStatus(GaoSalesOrderChartReq req); List<GaoSalesOrderStatusStatView> selectStatus(GaoSalesOrderChartReq req);
@@ -1,9 +1,7 @@
package com.imyeyu.api.modules.gao.service; package com.imyeyu.api.modules.gao.service;
import com.imyeyu.api.modules.gao.entity.GaoProduct; 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.GaoProductPage;
import com.imyeyu.api.modules.gao.vo.GaoProductUnitSelection;
import com.imyeyu.spring.bean.PageResult; import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.service.BaseService; import com.imyeyu.spring.service.BaseService;
@@ -28,21 +26,6 @@ public interface GaoProductService extends BaseService<GaoProduct, String> {
/// 查询启用且允许销售的产品单位 /// 查询启用且允许销售的产品单位
List<GaoProduct> listSellable(GaoProductPage page); List<GaoProduct> 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<GaoProductUnit> listUnitByProductId(String productId);
/// 停用产品 /// 停用产品
void disable(String id); void disable(String id);
} }
@@ -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<GaoProductSkuSalesDailyStatView> salesDaily(GaoProductChartReq req);
/// 查询 SKU 销售排行
List<GaoProductSkuSalesRankView> salesRank(GaoProductChartReq req);
/// 查询 SKU 单位销售排行
List<GaoProductSkuUnitSalesRankView> unitSalesRank(GaoProductChartReq req);
/// 查询 SKU 每日履约统计
List<GaoProductSkuFulfillmentDailyStatView> fulfillmentDaily(GaoProductChartReq req);
/// 查询 SKU 履约进度排行
List<GaoProductSkuFulfillmentRankView> fulfillmentRank(GaoProductChartReq req);
/// 查询 SKU 每日库存变动统计
List<GaoProductSkuStockMovementDailyStatView> stockMovementDaily(GaoProductChartReq req);
/// 查询 SKU 库存健康度
List<GaoProductSkuStockHealthView> stockHealth(GaoProductChartReq req);
}
@@ -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<GaoProductSku, String> {
/// 查询 SKU 详情
GaoProductSku detail(String id);
/// 按产品查询 SKU
List<GaoProductSku> listByProductId(String productId);
/// 批量按产品查询 SKU
List<GaoProductSku> listByProductIdList(Collection<String> productIdList);
/// 查询并校验可销售 SKU 单位
GaoProductSkuUnitSelection getSellableSkuUnit(String storeId, String skuId, String skuUnitId);
/// 查询并校验 SKU 单位
GaoProductSkuUnitSelection getSkuUnit(String storeId, String skuId, String skuUnitId, boolean purchase);
/// 查询 SKU 单位
List<GaoProductSkuUnit> listUnitBySkuId(String skuId);
/// 停用 SKU
void disable(String id);
}
@@ -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<GaoProductSpec> listByProductId(String productId);
/// 查询规格项所属产品 ID
String getProductIdBySpecId(String specId);
/// 创建规格项
void createSpec(GaoProductSpec spec);
/// 创建规格值
void createValue(GaoProductSpecValue value);
}
@@ -33,7 +33,7 @@ public interface GaoProductStockRecordService extends BaseService<GaoProductStoc
GaoProductStockRecord getRevokeBySourceRecordId(String sourceRecordId); GaoProductStockRecord getRevokeBySourceRecordId(String sourceRecordId);
/// 查询指定业务和产品的库存流水 /// 查询指定业务和产品的库存流水
GaoProductStockRecord getByBizProduct(GaoProductStockRecord.BizType bizType, String bizId, String productId, GaoProductStockRecord getByBizSku(GaoProductStockRecord.BizType bizType, String bizId, String skuId,
GaoProductStockRecord.OperationType operationType); GaoProductStockRecord.OperationType operationType);
/// 保存库存操作流水及附件 /// 保存库存操作流水及附件
@@ -4,6 +4,7 @@ import com.imyeyu.api.modules.gao.entity.GaoProductStock;
import com.imyeyu.api.modules.gao.vo.GaoProductStockChangeRequest; import com.imyeyu.api.modules.gao.vo.GaoProductStockChangeRequest;
import com.imyeyu.api.modules.gao.vo.GaoProductStockManualRevokeRequest; import com.imyeyu.api.modules.gao.vo.GaoProductStockManualRevokeRequest;
import com.imyeyu.api.modules.gao.vo.GaoProductStockPage; import com.imyeyu.api.modules.gao.vo.GaoProductStockPage;
import com.imyeyu.api.modules.gao.vo.GaoProductStockProductView;
import com.imyeyu.api.modules.gao.vo.GaoStockChangeItem; import com.imyeyu.api.modules.gao.vo.GaoStockChangeItem;
import com.imyeyu.spring.bean.PageResult; import com.imyeyu.spring.bean.PageResult;
@@ -18,8 +19,11 @@ public interface GaoProductStockService {
/// 查询库存余额 /// 查询库存余额
PageResult<GaoProductStock> pageByQuery(GaoProductStockPage page); PageResult<GaoProductStock> pageByQuery(GaoProductStockPage page);
/// 按产品汇总查询库存
PageResult<GaoProductStockProductView> pageProductByQuery(GaoProductStockPage page);
/// 查询库存余额详情 /// 查询库存余额详情
GaoProductStock detail(String storeId, String productId); GaoProductStock detail(String storeId, String skuId);
/// 期初入库 /// 期初入库
GaoProductStock initialIn(GaoProductStockChangeRequest request); GaoProductStock initialIn(GaoProductStockChangeRequest request);
@@ -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<GaoProductUnit> validateAndNormalize(GaoProduct product, List<GaoProductUnit> unitList);
/// 按基础单位数量系数换算业务数量
///
/// @param unit 单位
/// @param quantity 业务数量
/// @return 基础单位数量
BigDecimal convertQuantityToBase(GaoProductUnit unit, BigDecimal quantity);
}
@@ -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.GaoCustomerOrderProductRankView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerOrderRankView; 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.GaoCustomerRepeatDailyStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerRepeatIntervalStatView; import com.imyeyu.api.modules.gao.vo.GaoCustomerRepeatIntervalStatView;
import com.imyeyu.api.modules.gao.vo.GaoSalesOrderChartReq; import com.imyeyu.api.modules.gao.vo.GaoSalesOrderChartReq;
@@ -36,6 +37,9 @@ public interface GaoSalesOrderChartService {
/// 查询客户订单产品排行 /// 查询客户订单产品排行
List<GaoCustomerOrderProductRankView> customerProductRank(GaoSalesOrderChartReq req); List<GaoCustomerOrderProductRankView> customerProductRank(GaoSalesOrderChartReq req);
/// 查询客户订单 SKU 排行
List<GaoCustomerOrderSkuRankView> customerSkuRank(GaoSalesOrderChartReq req);
/// 查询销售订单状态统计 /// 查询销售订单状态统计
List<GaoSalesOrderStatusStatView> status(GaoSalesOrderChartReq req); List<GaoSalesOrderStatusStatView> status(GaoSalesOrderChartReq req);
@@ -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.GaoCustomerService;
import com.imyeyu.api.modules.gao.service.GaoCustomerPackageService; import com.imyeyu.api.modules.gao.service.GaoCustomerPackageService;
import com.imyeyu.api.modules.gao.service.GaoPackageConfigService; 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.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoCustomerPackageCancelRequest; import com.imyeyu.api.modules.gao.vo.GaoCustomerPackageCancelRequest;
import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePage; import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePage;
@@ -55,7 +55,7 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
private final GaoCustomerPackageMapper mapper; private final GaoCustomerPackageMapper mapper;
private final GaoPackageConfigService packageConfigService; private final GaoPackageConfigService packageConfigService;
private final GaoProductService productService; private final GaoProductSkuService skuService;
private final GaoCustomerService customerService; private final GaoCustomerService customerService;
private final GaoSalesOrderService salesOrderService; private final GaoSalesOrderService salesOrderService;
private final GaoStoreScopeService storeScopeService; private final GaoStoreScopeService storeScopeService;
@@ -237,9 +237,13 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
private List<GaoCustomerPackagePurchaseItem> defaultPurchaseItems(List<GaoPackageConfigItem> configItemList) { private List<GaoCustomerPackagePurchaseItem> defaultPurchaseItems(List<GaoPackageConfigItem> configItemList) {
return configItemList.stream().map(configItem -> { return configItemList.stream().map(configItem -> {
GaoCustomerPackagePurchaseItem item = new GaoCustomerPackagePurchaseItem(); GaoCustomerPackagePurchaseItem item = new GaoCustomerPackagePurchaseItem();
item.setProductId(configItem.getProductId()); item.setSkuId(configItem.getSkuId());
item.setProductUnitId(configItem.getProductUnitId()); item.setSkuUnitId(configItem.getSkuUnitId());
item.setProductNameSnapshot(configItem.getProductNameSnapshot()); 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.setUnitSnapshot(configItem.getUnitSnapshot());
item.setBaseQuantityPerUnitSnapshot(configItem.getBaseQuantityPerUnitSnapshot()); item.setBaseQuantityPerUnitSnapshot(configItem.getBaseQuantityPerUnitSnapshot());
item.setTotalQuantity(configItem.getDefaultQuantity()); item.setTotalQuantity(configItem.getDefaultQuantity());
@@ -252,9 +256,13 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
private GaoSalesOrderItem toOrderItem(GaoCustomerPackagePurchaseItem source) { private GaoSalesOrderItem toOrderItem(GaoCustomerPackagePurchaseItem source) {
GaoSalesOrderItem item = new GaoSalesOrderItem(); GaoSalesOrderItem item = new GaoSalesOrderItem();
item.setItemType(GaoSalesOrderItem.ItemType.PACKAGE_ENTITLEMENT); item.setItemType(GaoSalesOrderItem.ItemType.PACKAGE_ENTITLEMENT);
item.setProductId(source.getProductId()); item.setSkuId(source.getSkuId());
item.setProductUnitId(source.getProductUnitId()); item.setSkuUnitId(source.getSkuUnitId());
item.setProductNameSnapshot(source.getProductNameSnapshot()); item.setProductNameSnapshot(source.getProductNameSnapshot());
item.setProductIdSnapshot(source.getProductIdSnapshot());
item.setSkuCodeSnapshot(source.getSkuCodeSnapshot());
item.setSkuNameSnapshot(source.getSkuNameSnapshot());
item.setSpecSummarySnapshot(source.getSpecSummarySnapshot());
item.setUnitSnapshot(source.getUnitSnapshot()); item.setUnitSnapshot(source.getUnitSnapshot());
item.setBaseQuantityPerUnitSnapshot(source.getBaseQuantityPerUnitSnapshot()); item.setBaseQuantityPerUnitSnapshot(source.getBaseQuantityPerUnitSnapshot());
item.setQuantity(source.getTotalQuantity()); item.setQuantity(source.getTotalQuantity());
@@ -273,33 +281,37 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
for (int i = 0; i < itemList.size(); i++) { for (int i = 0; i < itemList.size(); i++) {
GaoCustomerPackagePurchaseItem item = itemList.get(i); GaoCustomerPackagePurchaseItem item = itemList.get(i);
TimiException.required(item, "购买产品明细不能为空"); TimiException.required(item, "购买产品明细不能为空");
TimiException.required(item.getProductId(), "购买产品不能为空"); TimiException.required(item.getSkuId(), "购买 SKU 不能为空");
TimiException.required(item.getProductUnitId(), "购买产品单位不能为空"); TimiException.required(item.getSkuUnitId(), "购买 SKU 单位不能为空");
TimiException.required(item.getTotalQuantity(), "购买产品数量不能为空"); TimiException.required(item.getTotalQuantity(), "购买产品数量不能为空");
TimiException.requiredTrue(0 < item.getTotalQuantity().signum(), "购买产品数量必须大于 0"); TimiException.requiredTrue(0 < item.getTotalQuantity().signum(), "购买产品数量必须大于 0");
TimiException.requiredTrue(3 >= item.getTotalQuantity().stripTrailingZeros().scale(), "购买产品数量最多保留三位小数"); TimiException.requiredTrue(3 >= item.getTotalQuantity().stripTrailingZeros().scale(), "购买产品数量最多保留三位小数");
String key = item.getProductId() + "|" + item.getProductUnitId(); String key = item.getSkuId() + "|" + item.getSkuUnitId();
TimiException.requiredTrue(keySet.add(key), "购买套餐内产品和单位不能重复"); TimiException.requiredTrue(keySet.add(key), "购买套餐内 SKU 和单位不能重复");
item.setSort(TimiJava.defaultIfNull(item.getSort(), i)); item.setSort(TimiJava.defaultIfNull(item.getSort(), i));
} }
} }
private void verifyConfigSellableItems(String storeId, List<GaoPackageConfigItem> itemList) { private void verifyConfigSellableItems(String storeId, List<GaoPackageConfigItem> itemList) {
for (GaoPackageConfigItem item : itemList) { for (GaoPackageConfigItem item : itemList) {
TimiException.required(item.getProductId(), "套餐配置缺少产品"); TimiException.required(item.getSkuId(), "套餐配置缺少 SKU");
TimiException.required(item.getProductUnitId(), "套餐配置缺少产品单位"); TimiException.required(item.getSkuUnitId(), "套餐配置缺少 SKU 单位");
// 通过产品库唯一选品入口重新校验产品和单位仍处于可销售状态 // 通过 SKU 服务唯一选品入口重新校验 SKU 和单位仍处于可销售状态
productService.getSellableProductUnit(storeId, item.getProductId(), item.getProductUnitId()); skuService.getSellableSkuUnit(storeId, item.getSkuId(), item.getSkuUnitId());
} }
} }
private void verifySameStructureAsConfig(List<GaoCustomerPackagePurchaseItem> itemList, List<GaoPackageConfigItem> configItemList) { private void verifySameStructureAsConfig(List<GaoCustomerPackagePurchaseItem> itemList, List<GaoPackageConfigItem> configItemList) {
TimiException.requiredTrue(itemList.size() == configItemList.size(), "套餐权益项目必须与套餐配置一致"); TimiException.requiredTrue(itemList.size() == configItemList.size(), "套餐权益项目必须与套餐配置一致");
Map<String, GaoCustomerPackagePurchaseItem> itemMap = itemList.stream().collect(Collectors.toMap(item -> item.getProductId() + "|" + item.getProductUnitId(), Function.identity())); Map<String, GaoCustomerPackagePurchaseItem> itemMap = itemList.stream().collect(Collectors.toMap(item -> item.getSkuId() + "|" + item.getSkuUnitId(), Function.identity()));
for (GaoPackageConfigItem configItem : configItemList) { for (GaoPackageConfigItem configItem : configItemList) {
GaoCustomerPackagePurchaseItem item = itemMap.get(configItem.getProductId() + "|" + configItem.getProductUnitId()); GaoCustomerPackagePurchaseItem item = itemMap.get(configItem.getSkuId() + "|" + configItem.getSkuUnitId());
TimiException.required(item, "套餐权益项目必须与套餐配置一致"); TimiException.required(item, "套餐权益项目必须与套餐配置一致");
item.setProductNameSnapshot(configItem.getProductNameSnapshot()); 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.setUnitSnapshot(configItem.getUnitSnapshot());
item.setBaseQuantityPerUnitSnapshot(configItem.getBaseQuantityPerUnitSnapshot()); item.setBaseQuantityPerUnitSnapshot(configItem.getBaseQuantityPerUnitSnapshot());
TimiException.requiredTrue(item.getSort().equals(configItem.getSort()) TimiException.requiredTrue(item.getSort().equals(configItem.getSort())
@@ -308,9 +320,9 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
} }
private void verifySameQuantityAsConfig(List<GaoCustomerPackagePurchaseItem> itemList, List<GaoPackageConfigItem> configItemList) { private void verifySameQuantityAsConfig(List<GaoCustomerPackagePurchaseItem> itemList, List<GaoPackageConfigItem> configItemList) {
Map<String, GaoCustomerPackagePurchaseItem> itemMap = itemList.stream().collect(Collectors.toMap(item -> item.getProductId() + "|" + item.getProductUnitId(), Function.identity())); Map<String, GaoCustomerPackagePurchaseItem> itemMap = itemList.stream().collect(Collectors.toMap(item -> item.getSkuId() + "|" + item.getSkuUnitId(), Function.identity()));
for (GaoPackageConfigItem configItem : configItemList) { 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, "普通员工不能修改套餐权益配额"); TimiException.requiredTrue(item.getTotalQuantity().compareTo(configItem.getDefaultQuantity()) == 0, "普通员工不能修改套餐权益配额");
} }
} }
@@ -327,9 +339,9 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
List<GaoCustomerPackagePurchaseItem> requestItemList = TimiJava.defaultIfNull(request.getItemList(), List.of()); List<GaoCustomerPackagePurchaseItem> requestItemList = TimiJava.defaultIfNull(request.getItemList(), List.of());
verifyPurchaseItems(requestItemList); verifyPurchaseItems(requestItemList);
TimiException.requiredTrue(existingItemList.size() == requestItemList.size(), "requestNo 对应购买请求内容不一致"); TimiException.requiredTrue(existingItemList.size() == requestItemList.size(), "requestNo 对应购买请求内容不一致");
Map<String, GaoSalesOrderItem> existingItemMap = existingItemList.stream().collect(Collectors.toMap(item -> item.getProductId() + "|" + item.getProductUnitId(), Function.identity())); Map<String, GaoSalesOrderItem> existingItemMap = existingItemList.stream().collect(Collectors.toMap(item -> item.getSkuId() + "|" + item.getSkuUnitId(), Function.identity()));
for (GaoCustomerPackagePurchaseItem item : requestItemList) { 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.required(existingItem, "requestNo 对应购买请求内容不一致");
TimiException.requiredTrue(existingItem.getItemType() == GaoSalesOrderItem.ItemType.PACKAGE_ENTITLEMENT TimiException.requiredTrue(existingItem.getItemType() == GaoSalesOrderItem.ItemType.PACKAGE_ENTITLEMENT
&& existingItem.getQuantity().compareTo(item.getTotalQuantity()) == 0 && existingItem.getQuantity().compareTo(item.getTotalQuantity()) == 0
@@ -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.common.service.AttachmentService;
import com.imyeyu.api.modules.gao.bean.GaoRoleCode; import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
import com.imyeyu.api.modules.gao.entity.GaoProduct; 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.GaoSalesOrder;
import com.imyeyu.api.modules.gao.entity.GaoSalesOrderItem; import com.imyeyu.api.modules.gao.entity.GaoSalesOrderItem;
import com.imyeyu.api.modules.gao.entity.GaoProductStockRecord; 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.GaoOrderFulfillmentService;
import com.imyeyu.api.modules.gao.service.GaoProductService; import com.imyeyu.api.modules.gao.service.GaoProductService;
import com.imyeyu.api.modules.gao.service.GaoProductStockService; 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.GaoSalesOrderService;
import com.imyeyu.api.modules.gao.service.GaoProductStockRecordService; import com.imyeyu.api.modules.gao.service.GaoProductStockRecordService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
@@ -68,7 +69,7 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
private final GaoOrderFulfillmentItemMapper itemMapper; private final GaoOrderFulfillmentItemMapper itemMapper;
private final GaoSalesOrderService salesOrderService; private final GaoSalesOrderService salesOrderService;
private final GaoProductService productService; private final GaoProductService productService;
private final GaoProductUnitService unitService; private final GaoProductSkuService skuService;
private final GaoProductStockService productStockService; private final GaoProductStockService productStockService;
private final GaoProductStockRecordService stockRecordService; private final GaoProductStockRecordService stockRecordService;
private final GaoStoreScopeService storeScopeService; private final GaoStoreScopeService storeScopeService;
@@ -359,8 +360,8 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
} }
if (!stockItems.isEmpty()) { if (!stockItems.isEmpty()) {
for (GaoStockChangeItem item : stockItems) { for (GaoStockChangeItem item : stockItems) {
GaoProductStockRecord stockRecord = stockRecordService.getByBizProduct(GaoProductStockRecord.BizType.ORDER_FULFILLMENT, GaoProductStockRecord stockRecord = stockRecordService.getByBizSku(GaoProductStockRecord.BizType.ORDER_FULFILLMENT,
source.getId(), item.getProductId(), GaoProductStockRecord.OperationType.ORDER_OUT); source.getId(), item.getSkuId(), GaoProductStockRecord.OperationType.ORDER_OUT);
if (stockRecord != null) { if (stockRecord != null) {
item.setSourceRecordId(stockRecord.getId()); item.setSourceRecordId(stockRecord.getId());
} }
@@ -444,21 +445,24 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
} }
private SourceEntry toSource(GaoSalesOrder order, GaoSalesOrderItem source, BigDecimal quantity, boolean checkAvailable) { private SourceEntry toSource(GaoSalesOrder order, GaoSalesOrderItem source, BigDecimal quantity, boolean checkAvailable) {
GaoProduct product = productService.get(source.getProductId()); GaoProductSku sku = skuService.get(source.getSkuId());
TimiException.required(sku, "履约来源 SKU 不存在");
GaoProduct product = productService.get(sku.getProductId());
TimiException.required(product, "履约来源产品不存在"); TimiException.required(product, "履约来源产品不存在");
TimiException.requiredTrue(order.getStoreId().equals(product.getStoreId()), "履约来源产品不属于当前门店"); TimiException.requiredTrue(order.getStoreId().equals(product.getStoreId()), "履约来源产品不属于当前门店");
BigDecimal baseQuantityPerUnit = requiredBaseQuantityPerUnit(source.getBaseQuantityPerUnitSnapshot()); BigDecimal baseQuantityPerUnit = requiredBaseQuantityPerUnit(source.getBaseQuantityPerUnitSnapshot());
GaoProductUnit unit = snapshotUnit(source.getProductUnitId(), source.getUnitSnapshot(), baseQuantityPerUnit); GaoProductSkuUnit unit = snapshotUnit(source.getSkuUnitId(), source.getUnitSnapshot(), baseQuantityPerUnit);
return buildSource(order, source.getId(), source.getProductId(), source.getProductUnitId(), source.getProductNameSnapshot(), return buildSource(order, source.getId(), source.getSkuId(), source.getSkuUnitId(), source.getProductIdSnapshot(), source.getProductNameSnapshot(), source.getSkuCodeSnapshot(), source.getSkuNameSnapshot(), source.getSpecSummarySnapshot(),
source.getUnitSnapshot(), baseQuantityPerUnit, quantity, source.getQuantity(), source.getReservedQuantity(), source.getFulfilledQuantity(), source.getUnitSnapshot(), baseQuantityPerUnit, quantity, source.getQuantity(), source.getReservedQuantity(), source.getFulfilledQuantity(),
product, unit, checkAvailable); product, unit, checkAvailable);
} }
private SourceEntry buildSource(GaoSalesOrder order, String orderItemId, String productId, String unitId, private SourceEntry buildSource(GaoSalesOrder order, String orderItemId, String skuId, String unitId, String productId,
String productName, String unitName, BigDecimal baseQuantityPerUnit, BigDecimal quantity, BigDecimal totalQuantity, String productName, String skuCode, String skuName, String specSummary, String unitName, BigDecimal baseQuantityPerUnit, BigDecimal quantity, BigDecimal totalQuantity,
BigDecimal reservedQuantity, BigDecimal fulfilledQuantity, GaoProduct product, GaoProductUnit unit, BigDecimal reservedQuantity, BigDecimal fulfilledQuantity, GaoProduct product, GaoProductSkuUnit unit,
boolean checkAvailable) { boolean checkAvailable) {
if (!Boolean.TRUE.equals(product.getAllowDecimalQuantity())) { GaoProductSku sku = skuService.get(skuId);
if (!Boolean.TRUE.equals(sku.getAllowDecimalQuantity())) {
TimiException.requiredTrue(quantity.stripTrailingZeros().scale() <= 0, "该产品不允许小数数量"); TimiException.requiredTrue(quantity.stripTrailingZeros().scale() <= 0, "该产品不允许小数数量");
} }
BigDecimal reservedBefore = decimal(reservedQuantity); BigDecimal reservedBefore = decimal(reservedQuantity);
@@ -467,30 +471,30 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
BigDecimal available = decimal(totalQuantity).subtract(reservedBefore).subtract(fulfilledBefore); BigDecimal available = decimal(totalQuantity).subtract(reservedBefore).subtract(fulfilledBefore);
TimiException.requiredTrue(0 <= available.compareTo(quantity), "来源产品可履约数量不足"); TimiException.requiredTrue(0 <= available.compareTo(quantity), "来源产品可履约数量不足");
} }
return new SourceEntry(orderItemId, productId, unitId, productName, unitName, baseQuantityPerUnit, return new SourceEntry(orderItemId, skuId, unitId, productId, productName, skuCode, skuName, specSummary, unitName, baseQuantityPerUnit,
quantity, decimal(totalQuantity), reservedBefore, fulfilledBefore, product, unit); quantity, decimal(totalQuantity), reservedBefore, fulfilledBefore, product, sku, unit);
} }
private List<GaoStockChangeItem> buildStockItems(String storeId, List<SourceEntry> sourceList) { private List<GaoStockChangeItem> buildStockItems(String storeId, List<SourceEntry> sourceList) {
Map<String, GaoStockChangeItem> itemMap = new LinkedHashMap<>(); Map<String, GaoStockChangeItem> itemMap = new LinkedHashMap<>();
for (SourceEntry source : sourceList) { for (SourceEntry source : sourceList) {
if (!Boolean.TRUE.equals(source.product().getStockEnabled())) { if (!Boolean.TRUE.equals(source.sku().getStockEnabled())) {
continue; 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); .filter(unit -> Boolean.TRUE.equals(unit.getIsBase())).findFirst().orElse(null);
TimiException.required(baseUnit, "实物产品缺少基础库存单位"); TimiException.required(baseUnit, "实物产品缺少基础库存单位");
BigDecimal baseQuantity = unitService.convertQuantityToBase(source.unit(), source.quantity()); BigDecimal baseQuantity = source.quantity().multiply(source.unit().getBaseQuantityPerUnit());
GaoStockChangeItem item = itemMap.get(source.product().getId()); GaoStockChangeItem item = itemMap.get(source.sku().getId());
if (item == null) { if (item == null) {
item = GaoStockChangeItem.of(source.product(), baseUnit, baseQuantity, baseQuantity); item = GaoStockChangeItem.of(source.product(), source.sku(), baseUnit, baseQuantity, baseQuantity);
itemMap.put(source.product().getId(), item); itemMap.put(source.sku().getId(), item);
} else { } else {
item.setQuantity(item.getQuantity().add(baseQuantity)); item.setQuantity(item.getQuantity().add(baseQuantity));
item.setBaseQuantity(item.getBaseQuantity().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) { private GaoOrderFulfillment lockFulfillment(String fulfillmentId, GaoSalesOrder order) {
@@ -517,19 +521,23 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
private GaoOrderFulfillmentItem copyFulfillmentItem(GaoOrderFulfillmentItem source) { private GaoOrderFulfillmentItem copyFulfillmentItem(GaoOrderFulfillmentItem source) {
GaoOrderFulfillmentItem result = new GaoOrderFulfillmentItem(); GaoOrderFulfillmentItem result = new GaoOrderFulfillmentItem();
result.setProductId(source.getProductId()); result.setSkuId(source.getSkuId());
result.setProductUnitId(source.getProductUnitId()); result.setSkuUnitId(source.getSkuUnitId());
result.setProductIdSnapshot(source.getProductIdSnapshot());
result.setSourceOrderItemId(source.getSourceOrderItemId()); result.setSourceOrderItemId(source.getSourceOrderItemId());
result.setQuantity(source.getQuantity()); result.setQuantity(source.getQuantity());
result.setBaseQuantity(source.getBaseQuantity()); result.setBaseQuantity(source.getBaseQuantity());
result.setProductNameSnapshot(source.getProductNameSnapshot()); result.setProductNameSnapshot(source.getProductNameSnapshot());
result.setSkuCodeSnapshot(source.getSkuCodeSnapshot());
result.setSkuNameSnapshot(source.getSkuNameSnapshot());
result.setSpecSummarySnapshot(source.getSpecSummarySnapshot());
result.setUnitSnapshot(source.getUnitSnapshot()); result.setUnitSnapshot(source.getUnitSnapshot());
result.setBaseQuantityPerUnitSnapshot(source.getBaseQuantityPerUnitSnapshot()); result.setBaseQuantityPerUnitSnapshot(source.getBaseQuantityPerUnitSnapshot());
return result; return result;
} }
private GaoProductUnit snapshotUnit(String id, String name, BigDecimal baseQuantityPerUnit) { private GaoProductSkuUnit snapshotUnit(String id, String name, BigDecimal baseQuantityPerUnit) {
GaoProductUnit unit = new GaoProductUnit(); GaoProductSkuUnit unit = new GaoProductSkuUnit();
unit.setId(id); unit.setId(id);
unit.setName(name); unit.setName(name);
unit.setBaseQuantityPerUnit(baseQuantityPerUnit); unit.setBaseQuantityPerUnit(baseQuantityPerUnit);
@@ -619,27 +627,27 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
if (itemList == null || itemList.isEmpty()) { if (itemList == null || itemList.isEmpty()) {
return; return;
} }
List<String> productIdList = itemList.stream() List<String> skuIdList = itemList.stream()
.map(GaoOrderFulfillmentItem::getProductId) .map(GaoOrderFulfillmentItem::getSkuId)
.filter(value -> value != null && !value.isBlank()) .filter(value -> value != null && !value.isBlank())
.distinct() .distinct()
.toList(); .toList();
if (productIdList.isEmpty()) { if (skuIdList.isEmpty()) {
return; return;
} }
Map<String, List<Attachment>> attachmentMap = attachmentService.mapByBizIdList( Map<String, List<Attachment>> attachmentMap = attachmentService.mapByBizIdList(
Attachment.BizType.GAO_PRODUCT, Attachment.BizType.GAO_PRODUCT_SKU,
productIdList, skuIdList);
GaoProduct.AttachType.PHOTO.name());
for (GaoOrderFulfillmentItem item : itemList) { 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, private record SourceEntry(String sourceOrderItemId, String skuId, String skuUnitId, String productIdSnapshot,
String productNameSnapshot, String unitSnapshot, BigDecimal baseQuantityPerUnitSnapshot, BigDecimal quantity, String productNameSnapshot, String skuCodeSnapshot, String skuNameSnapshot, String specSummarySnapshot,
BigDecimal totalQuantity, BigDecimal reservedBefore, BigDecimal fulfilledBefore, GaoProduct product, String unitSnapshot, BigDecimal baseQuantityPerUnitSnapshot, BigDecimal quantity,
GaoProductUnit unit) { BigDecimal totalQuantity, BigDecimal reservedBefore, BigDecimal fulfilledBefore, GaoProduct product, GaoProductSku sku,
GaoProductSkuUnit unit) {
private String sourceId() { private String sourceId() {
return sourceOrderItemId; return sourceOrderItemId;
@@ -648,8 +656,9 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
private GaoOrderFulfillmentItem toFulfillmentItem(String fulfillmentId) { private GaoOrderFulfillmentItem toFulfillmentItem(String fulfillmentId) {
GaoOrderFulfillmentItem result = new GaoOrderFulfillmentItem(); GaoOrderFulfillmentItem result = new GaoOrderFulfillmentItem();
result.setFulfillmentId(fulfillmentId); result.setFulfillmentId(fulfillmentId);
result.setProductId(productId); result.setSkuId(skuId);
result.setProductUnitId(productUnitId); result.setSkuUnitId(skuUnitId);
result.setProductIdSnapshot(productIdSnapshot);
result.setSourceOrderItemId(sourceOrderItemId); result.setSourceOrderItemId(sourceOrderItemId);
result.setQuantity(quantity); result.setQuantity(quantity);
result.setBaseQuantity(quantity.multiply(baseQuantityPerUnitSnapshot).setScale(3)); result.setBaseQuantity(quantity.multiply(baseQuantityPerUnitSnapshot).setScale(3));
@@ -658,6 +667,9 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
result.setFulfilledBefore(fulfilledBefore); result.setFulfilledBefore(fulfilledBefore);
result.setFulfilledAfter(fulfilledBefore); result.setFulfilledAfter(fulfilledBefore);
result.setProductNameSnapshot(productNameSnapshot); result.setProductNameSnapshot(productNameSnapshot);
result.setSkuCodeSnapshot(skuCodeSnapshot);
result.setSkuNameSnapshot(skuNameSnapshot);
result.setSpecSummarySnapshot(specSummarySnapshot);
result.setUnitSnapshot(unitSnapshot); result.setUnitSnapshot(unitSnapshot);
result.setBaseQuantityPerUnitSnapshot(baseQuantityPerUnitSnapshot); result.setBaseQuantityPerUnitSnapshot(baseQuantityPerUnitSnapshot);
return result; return result;
@@ -4,16 +4,16 @@ import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.gao.entity.GaoPackageConfig; import com.imyeyu.api.modules.gao.entity.GaoPackageConfig;
import com.imyeyu.api.modules.gao.entity.GaoPackageConfigItem; import com.imyeyu.api.modules.gao.entity.GaoPackageConfigItem;
import com.imyeyu.api.modules.gao.entity.GaoProduct; 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.system.entity.Logger; import com.imyeyu.api.modules.system.entity.Logger;
import com.imyeyu.api.modules.system.service.LoggerService; import com.imyeyu.api.modules.system.service.LoggerService;
import com.imyeyu.api.modules.gao.mapper.GaoPackageConfigItemMapper; import com.imyeyu.api.modules.gao.mapper.GaoPackageConfigItemMapper;
import com.imyeyu.api.modules.gao.mapper.GaoPackageConfigMapper; import com.imyeyu.api.modules.gao.mapper.GaoPackageConfigMapper;
import com.imyeyu.api.modules.gao.service.GaoPackageConfigService; 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.GaoProductUnitService;
import com.imyeyu.api.modules.gao.vo.GaoPackageConfigPage; import com.imyeyu.api.modules.gao.vo.GaoPackageConfigPage;
import com.imyeyu.api.modules.gao.vo.GaoProductUnitSelection; import com.imyeyu.api.modules.gao.vo.GaoProductSkuUnitSelection;
import com.imyeyu.api.modules.user.service.UserLoginService; import com.imyeyu.api.modules.user.service.UserLoginService;
import com.imyeyu.java.TimiJava; import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException; import com.imyeyu.java.bean.timi.TimiException;
@@ -42,8 +42,7 @@ public class GaoPackageConfigServiceImplement extends AbstractEntityService<GaoP
private final GaoPackageConfigMapper mapper; private final GaoPackageConfigMapper mapper;
private final GaoPackageConfigItemMapper itemMapper; private final GaoPackageConfigItemMapper itemMapper;
private final GaoProductService productService; private final GaoProductSkuService skuService;
private final GaoProductUnitService unitService;
private final UserLoginService userLoginService; private final UserLoginService userLoginService;
private final LoggerService loggerService; private final LoggerService loggerService;
@@ -184,22 +183,27 @@ public class GaoPackageConfigServiceImplement extends AbstractEntityService<GaoP
for (int i = 0; i < itemList.size(); i++) { for (int i = 0; i < itemList.size(); i++) {
GaoPackageConfigItem item = itemList.get(i); GaoPackageConfigItem item = itemList.get(i);
TimiException.required(item, "套餐产品明细不能为空"); TimiException.required(item, "套餐产品明细不能为空");
TimiException.required(item.getProductId(), "产品不能为空"); TimiException.required(item.getSkuId(), "SKU 不能为空");
TimiException.required(item.getProductUnitId(), "产品单位不能为空"); TimiException.required(item.getSkuUnitId(), "SKU 单位不能为空");
GaoProductUnitSelection selection = productService.getSellableProductUnit(storeId, item.getProductId(), item.getProductUnitId()); GaoProductSkuUnitSelection selection = skuService.getSellableSkuUnit(storeId, item.getSkuId(), item.getSkuUnitId());
GaoProduct product = selection.getProduct(); GaoProduct product = selection.getProduct();
GaoProductUnit unitEntity = selection.getUnit(); GaoProductSku sku = selection.getSku();
GaoProductSkuUnit unitEntity = selection.getUnit();
BigDecimal quantity = item.getDefaultQuantity(); BigDecimal quantity = item.getDefaultQuantity();
TimiException.required(quantity, "产品默认数量不能为空"); TimiException.required(quantity, "产品默认数量不能为空");
TimiException.requiredTrue(0 < quantity.signum(), "产品默认数量必须大于 0"); TimiException.requiredTrue(0 < quantity.signum(), "产品默认数量必须大于 0");
TimiException.requiredTrue(3 >= quantity.stripTrailingZeros().scale(), "产品数量最多保留三位小数"); TimiException.requiredTrue(3 >= quantity.stripTrailingZeros().scale(), "产品数量最多保留三位小数");
if (!Boolean.TRUE.equals(product.getAllowDecimalQuantity())) { if (!Boolean.TRUE.equals(sku.getAllowDecimalQuantity())) {
TimiException.requiredTrue(quantity.stripTrailingZeros().scale() <= 0, "该产品不允许小数数量"); TimiException.requiredTrue(quantity.stripTrailingZeros().scale() <= 0, "该 SKU 不允许小数数量");
} }
unitService.convertQuantityToBase(unitEntity, quantity); quantity.multiply(unitEntity.getBaseQuantityPerUnit());
String key = item.getProductId() + "|" + item.getProductUnitId(); String key = item.getSkuId() + "|" + item.getSkuUnitId();
TimiException.requiredTrue(keySet.add(key), "同一套餐内产品和单位不能重复"); TimiException.requiredTrue(keySet.add(key), "同一套餐内 SKU 和单位不能重复");
item.setProductNameSnapshot(product.getName()); 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.setUnitSnapshot(unitEntity.getName());
item.setBaseQuantityPerUnitSnapshot(unitEntity.getBaseQuantityPerUnit()); item.setBaseQuantityPerUnitSnapshot(unitEntity.getBaseQuantityPerUnit());
item.setSort(TimiJava.defaultIfNull(item.getSort(), i)); item.setSort(TimiJava.defaultIfNull(item.getSort(), i));
@@ -210,9 +214,13 @@ public class GaoPackageConfigServiceImplement extends AbstractEntityService<GaoP
for (GaoPackageConfigItem source : TimiJava.safeIterable(itemList)) { for (GaoPackageConfigItem source : TimiJava.safeIterable(itemList)) {
GaoPackageConfigItem item = new GaoPackageConfigItem(); GaoPackageConfigItem item = new GaoPackageConfigItem();
item.setPackageConfigId(configId); item.setPackageConfigId(configId);
item.setProductId(source.getProductId()); item.setSkuId(source.getSkuId());
item.setProductUnitId(source.getProductUnitId()); item.setSkuUnitId(source.getSkuUnitId());
item.setProductNameSnapshot(source.getProductNameSnapshot()); item.setProductNameSnapshot(source.getProductNameSnapshot());
item.setProductIdSnapshot(source.getProductIdSnapshot());
item.setSkuCodeSnapshot(source.getSkuCodeSnapshot());
item.setSkuNameSnapshot(source.getSkuNameSnapshot());
item.setSpecSummarySnapshot(source.getSpecSummarySnapshot());
item.setUnitSnapshot(source.getUnitSnapshot()); item.setUnitSnapshot(source.getUnitSnapshot());
item.setBaseQuantityPerUnitSnapshot(source.getBaseQuantityPerUnitSnapshot()); item.setBaseQuantityPerUnitSnapshot(source.getBaseQuantityPerUnitSnapshot());
item.setDefaultQuantity(source.getDefaultQuantity()); item.setDefaultQuantity(source.getDefaultQuantity());
@@ -5,14 +5,12 @@ import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService; import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.gao.entity.GaoProduct; import com.imyeyu.api.modules.gao.entity.GaoProduct;
import com.imyeyu.api.modules.gao.entity.GaoProductCategory; import com.imyeyu.api.modules.gao.entity.GaoProductCategory;
import com.imyeyu.api.modules.gao.entity.GaoProductUnit;
import com.imyeyu.api.modules.gao.mapper.GaoProductMapper; import com.imyeyu.api.modules.gao.mapper.GaoProductMapper;
import com.imyeyu.api.modules.gao.mapper.GaoProductUnitMapper;
import com.imyeyu.api.modules.gao.service.GaoProductCategoryService; import com.imyeyu.api.modules.gao.service.GaoProductCategoryService;
import com.imyeyu.api.modules.gao.service.GaoProductService; 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.GaoProductSpecService;
import com.imyeyu.api.modules.gao.vo.GaoProductPage; import com.imyeyu.api.modules.gao.vo.GaoProductPage;
import com.imyeyu.api.modules.gao.vo.GaoProductUnitSelection;
import com.imyeyu.api.modules.system.entity.Logger; import com.imyeyu.api.modules.system.entity.Logger;
import com.imyeyu.api.modules.system.service.LoggerService; import com.imyeyu.api.modules.system.service.LoggerService;
import com.imyeyu.api.modules.user.service.UserLoginService; import com.imyeyu.api.modules.user.service.UserLoginService;
@@ -45,10 +43,10 @@ import java.util.stream.Collectors;
public class GaoProductServiceImplement extends AbstractEntityService<GaoProduct, String> implements GaoProductService { public class GaoProductServiceImplement extends AbstractEntityService<GaoProduct, String> implements GaoProductService {
private final GaoProductMapper mapper; private final GaoProductMapper mapper;
private final GaoProductUnitMapper unitMapper;
private final AttachmentService attachmentService; private final AttachmentService attachmentService;
private final ObjectProvider<GaoProductCategoryService> categoryServiceProvider; private final ObjectProvider<GaoProductCategoryService> categoryServiceProvider;
private final GaoProductUnitService unitService; private final ObjectProvider<GaoProductSkuService> skuServiceProvider;
private final ObjectProvider<GaoProductSpecService> specServiceProvider;
private final UserLoginService userLoginService; private final UserLoginService userLoginService;
private final LoggerService loggerService; private final LoggerService loggerService;
@@ -93,37 +91,9 @@ public class GaoProductServiceImplement extends AbstractEntityService<GaoProduct
List<GaoProduct> list = mapper.selectByQuery(page); List<GaoProduct> list = mapper.selectByQuery(page);
fillTransient(list); fillTransient(list);
for (GaoProduct product : 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(); return list.stream().filter(product -> !product.getSkuList().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<GaoProductUnit> listUnitByProductId(String productId) {
TimiException.required(productId, "not found productId");
return unitMapper.selectByProductId(productId);
} }
@Transactional(TimiServerDBConfig.ROLLBACKER) @Transactional(TimiServerDBConfig.ROLLBACKER)
@@ -132,7 +102,7 @@ public class GaoProductServiceImplement extends AbstractEntityService<GaoProduct
loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_CREATE", product, () -> { loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_CREATE", product, () -> {
prepare(product, null); prepare(product, null);
super.create(product); super.create(product);
saveUnits(product.getId(), product.getUnitList(), List.of()); saveSkus(product, product.getSkuList());
attachmentService.updateByBizId(Attachment.BizType.GAO_PRODUCT, product.getId(), product.getAttachmentList()); attachmentService.updateByBizId(Attachment.BizType.GAO_PRODUCT, product.getId(), product.getAttachmentList());
return product; return product;
}); });
@@ -151,17 +121,14 @@ public class GaoProductServiceImplement extends AbstractEntityService<GaoProduct
product.setName(TimiJava.defaultIfNull(request.getName(), product.getName())); product.setName(TimiJava.defaultIfNull(request.getName(), product.getName()));
product.setProductType(TimiJava.defaultIfNull(request.getProductType(), product.getProductType())); product.setProductType(TimiJava.defaultIfNull(request.getProductType(), product.getProductType()));
product.setStockEnabled(TimiJava.defaultIfNull(request.getStockEnabled(), product.getStockEnabled())); product.setStockEnabled(TimiJava.defaultIfNull(request.getStockEnabled(), product.getStockEnabled()));
product.setAllowDecimalQuantity(TimiJava.defaultIfNull(request.getAllowDecimalQuantity(), product.getAllowDecimalQuantity()));
product.setStatus(TimiJava.defaultIfNull(request.getStatus(), product.getStatus())); product.setStatus(TimiJava.defaultIfNull(request.getStatus(), product.getStatus()));
product.setMinStockQuantity(request.getMinStockQuantity());
product.setSort(TimiJava.defaultIfNull(request.getSort(), product.getSort())); product.setSort(TimiJava.defaultIfNull(request.getSort(), product.getSort()));
product.setRemark(request.getRemark()); product.setRemark(request.getRemark());
List<GaoProductUnit> existingUnitList = unitMapper.selectByProductId(product.getId()); product.setSkuList(request.getSkuList() == null ? skuServiceProvider.getObject().listByProductId(product.getId()) : request.getSkuList());
product.setUnitList(request.getUnitList() == null ? existingUnitList : request.getUnitList());
prepare(product, product.getId()); prepare(product, product.getId());
super.update(product); super.update(product);
if (request.getUnitList() != null) { if (request.getSkuList() != null) {
saveUnits(product.getId(), product.getUnitList(), existingUnitList); saveSkus(product, request.getSkuList());
} }
if (request.getAttachmentList() != null) { if (request.getAttachmentList() != null) {
attachmentService.updateByBizId(Attachment.BizType.GAO_PRODUCT, product.getId(), request.getAttachmentList()); attachmentService.updateByBizId(Attachment.BizType.GAO_PRODUCT, product.getId(), request.getAttachmentList());
@@ -176,6 +143,7 @@ public class GaoProductServiceImplement extends AbstractEntityService<GaoProduct
loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_DISABLE", id, () -> { loggerService.execute(Logger.Module.GAO, "GAO_PRODUCT_DISABLE", id, () -> {
GaoProduct product = get(id); GaoProduct product = get(id);
TimiException.required(product, "not found product"); 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.setStatus(GaoProduct.Status.INACTIVE);
product.setUpdatedBy(userLoginService.getRequireLoginUserId()); product.setUpdatedBy(userLoginService.getRequireLoginUserId());
product.setUpdatedAt(Time.now()); product.setUpdatedAt(Time.now());
@@ -202,15 +170,7 @@ public class GaoProductServiceImplement extends AbstractEntityService<GaoProduct
product.setProductType(TimiJava.defaultIfNull(product.getProductType(), GaoProduct.ProductType.GOODS)); product.setProductType(TimiJava.defaultIfNull(product.getProductType(), GaoProduct.ProductType.GOODS));
product.setStockEnabled(TimiJava.defaultIfNull(product.getStockEnabled(), product.getProductType() != GaoProduct.ProductType.SERVICE)); product.setStockEnabled(TimiJava.defaultIfNull(product.getStockEnabled(), product.getProductType() != GaoProduct.ProductType.SERVICE));
TimiException.requiredTrue(product.getProductType() != GaoProduct.ProductType.SERVICE || !product.getStockEnabled(), "SERVICE 产品不能启用库存"); TimiException.requiredTrue(product.getProductType() != GaoProduct.ProductType.SERVICE || !product.getStockEnabled(), "SERVICE 产品不能启用库存");
product.setAllowDecimalQuantity(TimiJava.defaultIfNull(product.getAllowDecimalQuantity(), false));
product.setStatus(TimiJava.defaultIfNull(product.getStatus(), GaoProduct.Status.ACTIVE)); product.setStatus(TimiJava.defaultIfNull(product.getStatus(), GaoProduct.Status.ACTIVE));
if (!Boolean.TRUE.equals(product.getStockEnabled())) {
TimiException.requiredNull(product.getMinStockQuantity(), "未启用库存的产品不能填写最低库存");
}
if (product.getMinStockQuantity() != null) {
TimiException.requiredTrue(0 <= product.getMinStockQuantity().signum(), "最低库存不能为负数");
TimiException.requiredTrue(3 >= product.getMinStockQuantity().scale(), "最低库存最多保留三位小数");
}
product.setSort(TimiJava.defaultIfNull(product.getSort(), 0)); product.setSort(TimiJava.defaultIfNull(product.getSort(), 0));
TimiException.requiredNull(mapper.selectByStoreIdAndCode(product.getStoreId(), product.getCode(), excludeId), "门店内产品编码不能重复"); TimiException.requiredNull(mapper.selectByStoreIdAndCode(product.getStoreId(), product.getCode(), excludeId), "门店内产品编码不能重复");
if (product.getCategoryId() != null) { if (product.getCategoryId() != null) {
@@ -218,42 +178,21 @@ public class GaoProductServiceImplement extends AbstractEntityService<GaoProduct
TimiException.required(category, "产品分类不存在"); TimiException.required(category, "产品分类不存在");
TimiException.requiredTrue(product.getStoreId().equals(category.getStoreId()), "产品分类不属于当前门店"); TimiException.requiredTrue(product.getStoreId().equals(category.getStoreId()), "产品分类不属于当前门店");
} }
product.setUnitList(unitService.validateAndNormalize(product, product.getUnitList()));
product.setUpdatedBy(userLoginService.getRequireLoginUserId()); product.setUpdatedBy(userLoginService.getRequireLoginUserId());
} }
private void saveUnits(String productId, List<GaoProductUnit> unitList, List<GaoProductUnit> existingUnitList) { private void saveSkus(GaoProduct product, List<com.imyeyu.api.modules.gao.entity.GaoProductSku> skuList) {
Map<String, GaoProductUnit> existingById = existingUnitList.stream() List<com.imyeyu.api.modules.gao.entity.GaoProductSku> sourceList = skuList;
.collect(Collectors.toMap(GaoProductUnit::getId, item -> item)); if (sourceList == null || sourceList.isEmpty()) {
Map<String, GaoProductUnit> existingByName = existingUnitList.stream() com.imyeyu.api.modules.gao.entity.GaoProductSku sku = new com.imyeyu.api.modules.gao.entity.GaoProductSku();
.collect(Collectors.toMap(GaoProductUnit::getName, item -> item)); sku.setProductId(product.getId()); sku.setCode(product.getCode()); sku.setName(product.getName());
Set<String> retainedIdSet = new HashSet<>(); com.imyeyu.api.modules.gao.entity.GaoProductSkuUnit unit = new com.imyeyu.api.modules.gao.entity.GaoProductSkuUnit();
for (GaoProductUnit source : unitList) { unit.setName("件"); unit.setIsBase(true); unit.setBaseQuantityPerUnit(BigDecimal.ONE);
GaoProductUnit existing = source.getId() == null ? existingByName.get(source.getName()) : existingById.get(source.getId()); sku.setUnitList(List.of(unit)); sourceList = List.of(sku);
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), "产品单位更新失败");
}
} }
for (GaoProductUnit existing : existingUnitList) { for (com.imyeyu.api.modules.gao.entity.GaoProductSku source : sourceList) {
if (!retainedIdSet.contains(existing.getId())) { source.setProductId(product.getId());
unitMapper.delete(existing.getId()); if (source.getId() == null) skuServiceProvider.getObject().create(source); else skuServiceProvider.getObject().update(source);
}
} }
} }
@@ -262,11 +201,12 @@ public class GaoProductServiceImplement extends AbstractEntityService<GaoProduct
return; return;
} }
List<String> productIdList = list.stream().map(GaoProduct::getId).toList(); List<String> productIdList = list.stream().map(GaoProduct::getId).toList();
Map<String, List<GaoProductUnit>> unitMap = unitMapper.selectByProductIdList(productIdList).stream()
.collect(Collectors.groupingBy(GaoProductUnit::getProductId));
Map<String, List<Attachment>> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_PRODUCT, productIdList); Map<String, List<Attachment>> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_PRODUCT, productIdList);
Map<String, List<com.imyeyu.api.modules.gao.entity.GaoProductSku>> skuMap = skuServiceProvider.getObject().listByProductIdList(productIdList).stream()
.collect(Collectors.groupingBy(com.imyeyu.api.modules.gao.entity.GaoProductSku::getProductId));
for (GaoProduct product : list) { 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())); product.setAttachmentList(attachmentMap.getOrDefault(product.getId(), List.of()));
if (product.getCategoryId() != null) { if (product.getCategoryId() != null) {
product.setCategory(categoryServiceProvider.getObject().get(product.getCategoryId())); product.setCategory(categoryServiceProvider.getObject().get(product.getCategoryId()));
@@ -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<GaoProductSkuSalesDailyStatView> salesDaily(GaoProductChartReq req) {
checkRangeReq(req);
return mapper.selectSalesDaily(req);
}
@Override
public List<GaoProductSkuSalesRankView> salesRank(GaoProductChartReq req) {
checkRangeReq(req);
normalizeTopN(req);
return mapper.selectSalesRank(req);
}
@Override
public List<GaoProductSkuUnitSalesRankView> unitSalesRank(GaoProductChartReq req) {
checkRangeReq(req);
normalizeTopN(req);
return mapper.selectUnitSalesRank(req);
}
@Override
public List<GaoProductSkuFulfillmentDailyStatView> fulfillmentDaily(GaoProductChartReq req) {
checkRangeReq(req);
return mapper.selectFulfillmentDaily(req);
}
@Override
public List<GaoProductSkuFulfillmentRankView> fulfillmentRank(GaoProductChartReq req) {
checkRangeReq(req);
normalizeTopN(req);
return mapper.selectFulfillmentRank(req);
}
@Override
public List<GaoProductSkuStockMovementDailyStatView> stockMovementDaily(GaoProductChartReq req) {
checkRangeReq(req);
return mapper.selectStockMovementDaily(req);
}
@Override
public List<GaoProductSkuStockHealthView> 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);
}
}
@@ -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<GaoProductSku, String> 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<GaoProductService> productServiceProvider;
private final UserLoginService userLoginService;
@Override
protected BaseMapper<GaoProductSku, String> 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<GaoProductSku> listByProductId(String productId) {
TimiException.required(productId, "not found productId");
List<GaoProductSku> result = mapper.selectByProductId(productId);
fillTransient(result);
return result;
}
@Override
public List<GaoProductSku> listByProductIdList(Collection<String> productIdList) {
if (productIdList == null || productIdList.isEmpty()) {
return List.of();
}
List<GaoProductSku> 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<GaoProductSkuUnit> 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<String> specValueIdList = sku.getSpecValueIdList() == null ? List.of() : sku.getSpecValueIdList();
TimiException.requiredTrue(specValueIdList.size() == new HashSet<>(specValueIdList).size(), "SKU 规格值不能重复");
Set<String> 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<String> requiredSpecIdSet = specMapper.selectByProductId(sku.getProductId()).stream().map(GaoProductSpec::getId).collect(Collectors.toSet());
TimiException.requiredTrue(requiredSpecIdSet.equals(specIdSet), "SKU 必须为每个规格项选择一个规格值");
}
private String buildSpecValueKey(List<String> specValueIdList) {
if (specValueIdList == null || specValueIdList.isEmpty()) {
return "DEFAULT";
}
return specValueIdList.stream().sorted().collect(Collectors.joining(","));
}
private List<GaoProductSkuUnit> validateAndNormalizeUnits(List<GaoProductSkuUnit> sourceList) {
TimiException.requiredTrue(sourceList != null && !sourceList.isEmpty(), "SKU 至少需要一个单位");
List<GaoProductSkuUnit> result = new ArrayList<>();
Set<String> nameSet = new HashSet<>();
Set<String> 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<GaoProductSkuUnit> unitList, List<GaoProductSkuUnit> existingUnitList) {
Map<String, GaoProductSkuUnit> existingById = existingUnitList.stream().collect(Collectors.toMap(GaoProductSkuUnit::getId, item -> item));
Set<String> 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<String> specValueIdList) {
skuSpecValueMapper.deleteBySkuId(skuId);
for (String specValueId : specValueIdList == null ? List.<String>of() : specValueIdList) {
GaoProductSkuSpecValue relation = new GaoProductSkuSpecValue();
relation.setSkuId(skuId);
relation.setSpecValueId(specValueId);
skuSpecValueMapper.insert(relation);
}
}
private List<String> currentSpecValueIds(String skuId) {
return skuSpecValueMapper.selectBySkuIdList(List.of(skuId)).stream().map(GaoProductSkuSpecValue::getSpecValueId).toList();
}
private void fillTransient(List<GaoProductSku> skuList) {
if (skuList.isEmpty()) return;
List<String> skuIdList = skuList.stream().map(GaoProductSku::getId).toList();
Map<String, List<GaoProductSkuUnit>> unitMap = unitMapper.selectBySkuIdList(skuIdList).stream().collect(Collectors.groupingBy(GaoProductSkuUnit::getSkuId));
Map<String, List<String>> specValueIdMap = skuSpecValueMapper.selectBySkuIdList(skuIdList)
.stream()
.collect(Collectors.groupingBy(GaoProductSkuSpecValue::getSkuId, Collectors.mapping(GaoProductSkuSpecValue::getSpecValueId, Collectors.toList())));
Map<String, List<Attachment>> 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();
}
}
@@ -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<GaoProductSpec> listByProductId(String productId) {
List<GaoProductSpec> specList = specMapper.selectByProductId(productId);
Map<String, List<GaoProductSpecValue>> 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; }
}
@@ -86,13 +86,13 @@ public class GaoProductStockRecordServiceImplement extends AbstractEntityService
} }
@Override @Override
public GaoProductStockRecord getByBizProduct(GaoProductStockRecord.BizType bizType, String bizId, String productId, public GaoProductStockRecord getByBizSku(GaoProductStockRecord.BizType bizType, String bizId, String skuId,
GaoProductStockRecord.OperationType operationType) { GaoProductStockRecord.OperationType operationType) {
TimiException.required(bizType, "not found stock bizType"); TimiException.required(bizType, "not found stock bizType");
TimiException.required(bizId, "not found stock bizId"); 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"); TimiException.required(operationType, "not found operationType");
return mapper.selectByBizProduct(bizType, bizId, productId, operationType); return mapper.selectByBizSku(bizType, bizId, skuId, operationType);
} }
@Override @Override
@@ -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.common.service.AttachmentService;
import com.imyeyu.api.modules.gao.entity.GaoProduct; import com.imyeyu.api.modules.gao.entity.GaoProduct;
import com.imyeyu.api.modules.gao.entity.GaoProductStock; 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.entity.GaoProductStockRecord;
import com.imyeyu.api.modules.gao.mapper.GaoProductStockMapper; import com.imyeyu.api.modules.gao.mapper.GaoProductStockMapper;
import com.imyeyu.api.modules.gao.service.GaoProductService; import com.imyeyu.api.modules.gao.service.GaoProductService;
import com.imyeyu.api.modules.gao.service.GaoProductStockService; 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.GaoProductStockRecordService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.vo.GaoProductStockChangeRequest; import com.imyeyu.api.modules.gao.vo.GaoProductStockChangeRequest;
import com.imyeyu.api.modules.gao.vo.GaoProductStockManualRevokeRequest; import com.imyeyu.api.modules.gao.vo.GaoProductStockManualRevokeRequest;
import com.imyeyu.api.modules.gao.vo.GaoProductStockPage; 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.gao.vo.GaoStockChangeItem;
import com.imyeyu.api.modules.system.entity.Logger; import com.imyeyu.api.modules.system.entity.Logger;
import com.imyeyu.api.modules.system.service.LoggerService; import com.imyeyu.api.modules.system.service.LoggerService;
@@ -54,7 +56,7 @@ public class GaoProductStockServiceImplement implements GaoProductStockService {
private final AttachmentService attachmentService; private final AttachmentService attachmentService;
private final GaoProductStockRecordService recordService; private final GaoProductStockRecordService recordService;
private final GaoProductService productService; private final GaoProductService productService;
private final GaoProductUnitService unitService; private final GaoProductSkuService skuService;
private final GaoStoreScopeService storeScopeService; private final GaoStoreScopeService storeScopeService;
private final UserLoginService userLoginService; private final UserLoginService userLoginService;
private final LoggerService loggerService; private final LoggerService loggerService;
@@ -66,15 +68,14 @@ public class GaoProductStockServiceImplement implements GaoProductStockService {
result.setTotal(stockMapper.countByQuery(page)); result.setTotal(stockMapper.countByQuery(page));
List<GaoProductStock> list = stockMapper.selectByQuery(page); List<GaoProductStock> list = stockMapper.selectByQuery(page);
Map<String, Attachment> photoMap = attachmentService.mapFirstImageByIdList( Map<String, Attachment> photoMap = attachmentService.mapFirstImageByIdList(
Attachment.BizType.GAO_PRODUCT, Attachment.BizType.GAO_PRODUCT_SKU,
list.stream().map(GaoProductStock::getProductId).filter(id -> id != null && !id.isBlank()).toList(), list.stream().map(GaoProductStock::getSkuId).filter(id -> id != null && !id.isBlank()).toList()
GaoProduct.AttachType.PHOTO.name()
); );
for (GaoProductStock stock : list) { for (GaoProductStock stock : list) {
fillBalance(stock); fillBalance(stock);
if (stock.getProduct() != null) { if (stock.getSku() != null) {
Attachment photo = photoMap.get(stock.getProductId()); Attachment photo = photoMap.get(stock.getSkuId());
stock.getProduct().setAttachmentList(photo == null ? List.of() : List.of(photo)); stock.getSku().setAttachmentList(photo == null ? List.of() : List.of(photo));
} }
} }
result.setList(list); result.setList(list);
@@ -82,22 +83,31 @@ public class GaoProductStockServiceImplement implements GaoProductStockService {
} }
@Override @Override
public GaoProductStock detail(String storeId, String productId) { public PageResult<GaoProductStockProductView> pageProductByQuery(GaoProductStockPage page) {
TimiException.required(page, "not found stock product page");
PageResult<GaoProductStockProductView> 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(storeId, "not found storeId");
TimiException.required(productId, "not found productId"); TimiException.required(skuId, "not found skuId");
storeScopeService.checkStoreId(storeId); storeScopeService.checkStoreId(storeId);
GaoProduct product = productService.get(productId); GaoProductSku sku = skuService.get(skuId);
TimiException.required(product, "not found product"); TimiException.required(sku, "not found sku");
TimiException.requiredTrue(storeId.equals(product.getStoreId()), "产品不属于当前门店"); TimiException.requiredTrue(storeId.equals(sku.getStoreId()), "SKU 不属于当前门店");
GaoProductStock stock = stockMapper.selectByStoreIdAndProductId(storeId, productId); GaoProductStock stock = stockMapper.selectByStoreIdAndSkuId(storeId, skuId);
if (stock == null) { if (stock == null) {
stock = new GaoProductStock(); stock = new GaoProductStock();
stock.setStoreId(storeId); stock.setStoreId(storeId);
stock.setProductId(productId); stock.setSkuId(skuId);
stock.setOnHandQuantity(ZERO); stock.setOnHandQuantity(ZERO);
stock.setReservedQuantity(ZERO); stock.setReservedQuantity(ZERO);
} }
stock.setProduct(product); stock.setSku(sku);
fillBalance(stock); fillBalance(stock);
return stock; return stock;
} }
@@ -146,9 +156,9 @@ public class GaoProductStockServiceImplement implements GaoProductStockService {
return true; return true;
} }
List<GaoStockChangeItem> sortedItems = new ArrayList<>(items); List<GaoStockChangeItem> sortedItems = new ArrayList<>(items);
sortedItems.sort(Comparator.comparing(GaoStockChangeItem::getProductId)); sortedItems.sort(Comparator.comparing(GaoStockChangeItem::getSkuId));
for (GaoStockChangeItem item : sortedItems) { 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) { if (stock == null || decimal(stock.getOnHandQuantity()).subtract(decimal(stock.getReservedQuantity())).compareTo(item.getBaseQuantity()) < 0) {
return false; return false;
} }
@@ -188,22 +198,23 @@ public class GaoProductStockServiceImplement implements GaoProductStockService {
boolean purchase = operationType == GaoProductStockRecord.OperationType.INITIAL_IN boolean purchase = operationType == GaoProductStockRecord.OperationType.INITIAL_IN
|| operationType == GaoProductStockRecord.OperationType.MANUAL_IN || operationType == GaoProductStockRecord.OperationType.MANUAL_IN
|| operationType == GaoProductStockRecord.OperationType.STOCKTAKE_GAIN; || 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(); GaoProduct product = selection.getProduct();
GaoProductUnit unit = selection.getUnit(); GaoProductSkuUnit unit = selection.getUnit();
TimiException.requiredTrue(Boolean.TRUE.equals(product.getStockEnabled()), "该产品未启用库存管理"); TimiException.requiredTrue(Boolean.TRUE.equals(sku.getStockEnabled()), "该 SKU 未启用库存管理");
BigDecimal quantity = validateQuantity(product, request.getQuantity()); BigDecimal quantity = validateQuantity(sku, request.getQuantity());
BigDecimal baseQuantity = unitService.convertQuantityToBase(unit, quantity); BigDecimal baseQuantity = quantity.multiply(unit.getBaseQuantityPerUnit());
List<GaoProductStockRecord> existingList = recordService.listByRequestNo(storeId, requestNo); List<GaoProductStockRecord> existingList = recordService.listByRequestNo(storeId, requestNo);
if (!existingList.isEmpty()) { if (!existingList.isEmpty()) {
GaoProductStockRecord existing = existingList.getFirst(); GaoProductStockRecord existing = existingList.getFirst();
verifyExisting(existing, operationType, product.getId(), unit.getId(), quantity, baseQuantity, request.getRemark()); verifyExisting(existing, operationType, sku.getId(), unit.getId(), quantity, baseQuantity, request.getRemark());
return detail(storeId, product.getId()); return detail(storeId, sku.getId());
} }
TimiException.requiredTrue(hasAttachment(request.getAttachmentList()), "库存操作必须上传照片"); TimiException.requiredTrue(hasAttachment(request.getAttachmentList()), "库存操作必须上传照片");
TimiException.requiredTrue(hasText(request.getRemark()), "库存操作必须填写备注"); TimiException.requiredTrue(hasText(request.getRemark()), "库存操作必须填写备注");
GaoStockChangeItem item = GaoStockChangeItem.of(product, unit, quantity, baseQuantity); GaoStockChangeItem item = GaoStockChangeItem.of(product, sku, unit, quantity, baseQuantity);
List<GaoProductStock> result = executeInternal(storeId, product.getId(), List.of(item), requestNo, List<GaoProductStock> result = executeInternal(storeId, sku.getId(), List.of(item), requestNo,
userLoginService.getRequireLoginUserId(), operationType, request.getRemark(), request.getAttachmentList()); userLoginService.getRequireLoginUserId(), operationType, request.getRemark(), request.getAttachmentList());
return result.getFirst(); return result.getFirst();
} }
@@ -220,13 +231,13 @@ public class GaoProductStockServiceImplement implements GaoProductStockService {
|| source.getOperationType() == GaoProductStockRecord.OperationType.MANUAL_OUT, "只有手工入库或手工出库流水可以冲正"); || source.getOperationType() == GaoProductStockRecord.OperationType.MANUAL_OUT, "只有手工入库或手工出库流水可以冲正");
TimiException.requiredNull(source.getSourceRecordId(), "只能冲正原始手工库存流水"); TimiException.requiredNull(source.getSourceRecordId(), "只能冲正原始手工库存流水");
String storeId = requiredText(source.getStoreId(), "原库存流水缺少门店"); String storeId = requiredText(source.getStoreId(), "原库存流水缺少门店");
String productId = requiredText(source.getProductId(), "原库存流水缺少产品"); String skuId = requiredText(source.getSkuId(), "原库存流水缺少 SKU");
storeScopeService.checkStoreId(storeId); storeScopeService.checkStoreId(storeId);
List<GaoProductStockRecord> existingList = recordService.listByRequestNo(storeId, requestNo); List<GaoProductStockRecord> existingList = recordService.listByRequestNo(storeId, requestNo);
if (!existingList.isEmpty()) { if (!existingList.isEmpty()) {
verifyManualRevokeExisting(existingList, sourceRecordId, remark); verifyManualRevokeExisting(existingList, sourceRecordId, remark);
return detail(storeId, productId); return detail(storeId, skuId);
} }
TimiException.requiredNull(recordService.getRevokeBySourceRecordId(sourceRecordId), "该手工库存流水已经冲正"); TimiException.requiredNull(recordService.getRevokeBySourceRecordId(sourceRecordId), "该手工库存流水已经冲正");
TimiException.requiredTrue(hasAttachment(request.getAttachmentList()), "库存流水冲正必须上传照片"); TimiException.requiredTrue(hasAttachment(request.getAttachmentList()), "库存流水冲正必须上传照片");
@@ -236,7 +247,7 @@ public class GaoProductStockServiceImplement implements GaoProductStockService {
BigDecimal sourceReservedDelta = decimal(source.getReservedDelta()); BigDecimal sourceReservedDelta = decimal(source.getReservedDelta());
TimiException.requiredTrue(sourceReservedDelta.signum() == 0, "手工库存流水不能包含预占变动"); TimiException.requiredTrue(sourceReservedDelta.signum() == 0, "手工库存流水不能包含预占变动");
GaoProductStock stock = lockOrCreateStock(storeId, productId); GaoProductStock stock = lockOrCreateStock(storeId, skuId);
BigDecimal onHandBefore = decimal(stock.getOnHandQuantity()); BigDecimal onHandBefore = decimal(stock.getOnHandQuantity());
BigDecimal reservedBefore = decimal(stock.getReservedQuantity()); BigDecimal reservedBefore = decimal(stock.getReservedQuantity());
BigDecimal onHandDelta = source.getOnHandDelta().negate(); BigDecimal onHandDelta = source.getOnHandDelta().negate();
@@ -247,9 +258,13 @@ public class GaoProductStockServiceImplement implements GaoProductStockService {
BigDecimal reservedAfter = reservedBefore.add(reservedDelta); BigDecimal reservedAfter = reservedBefore.add(reservedDelta);
GaoProductStockRecord revoke = new GaoProductStockRecord(); GaoProductStockRecord revoke = new GaoProductStockRecord();
revoke.setStoreId(storeId); revoke.setStoreId(storeId);
revoke.setProductId(productId); revoke.setSkuId(skuId);
revoke.setProductUnitId(source.getProductUnitId()); revoke.setSkuUnitId(source.getSkuUnitId());
revoke.setProductIdSnapshot(source.getProductIdSnapshot());
revoke.setProductNameSnapshot(source.getProductNameSnapshot()); revoke.setProductNameSnapshot(source.getProductNameSnapshot());
revoke.setSkuCodeSnapshot(source.getSkuCodeSnapshot());
revoke.setSkuNameSnapshot(source.getSkuNameSnapshot());
revoke.setSpecSummarySnapshot(source.getSpecSummarySnapshot());
revoke.setUnitSnapshot(source.getUnitSnapshot()); revoke.setUnitSnapshot(source.getUnitSnapshot());
revoke.setBaseQuantityPerUnitSnapshot(source.getBaseQuantityPerUnitSnapshot()); revoke.setBaseQuantityPerUnitSnapshot(source.getBaseQuantityPerUnitSnapshot());
revoke.setQuantity(source.getQuantity()); revoke.setQuantity(source.getQuantity());
@@ -289,11 +304,11 @@ public class GaoProductStockServiceImplement implements GaoProductStockService {
String normalizedRequestNo = requiredText(requestNo, "not found requestNo"); String normalizedRequestNo = requiredText(requestNo, "not found requestNo");
storeScopeService.checkStoreId(storeId); storeScopeService.checkStoreId(storeId);
List<GaoStockChangeItem> itemList = new ArrayList<>(sourceList); List<GaoStockChangeItem> itemList = new ArrayList<>(sourceList);
itemList.sort(Comparator.comparing(GaoStockChangeItem::getProductId)); itemList.sort(Comparator.comparing(GaoStockChangeItem::getSkuId));
Set<String> productIdSet = new HashSet<>(); Set<String> skuIdSet = new HashSet<>();
for (GaoStockChangeItem item : itemList) { for (GaoStockChangeItem item : itemList) {
TimiException.required(item, "库存变更项不能为空"); TimiException.required(item, "库存变更项不能为空");
TimiException.requiredTrue(productIdSet.add(item.getProductId()), "同一库存操作不能重复产品"); TimiException.requiredTrue(skuIdSet.add(item.getSkuId()), "同一库存操作不能重复 SKU");
TimiException.required(item.getBaseQuantity(), "基础单位数量不能为空"); TimiException.required(item.getBaseQuantity(), "基础单位数量不能为空");
TimiException.requiredTrue(0 < item.getBaseQuantity().signum(), "基础单位数量必须大于 0"); TimiException.requiredTrue(0 < item.getBaseQuantity().signum(), "基础单位数量必须大于 0");
} }
@@ -302,13 +317,13 @@ public class GaoProductStockServiceImplement implements GaoProductStockService {
verifyExistingList(existingList, itemList, operationType, bizId); verifyExistingList(existingList, itemList, operationType, bizId);
List<GaoProductStock> result = new ArrayList<>(); List<GaoProductStock> result = new ArrayList<>();
for (GaoStockChangeItem item : itemList) { for (GaoStockChangeItem item : itemList) {
result.add(detail(storeId, item.getProductId())); result.add(detail(storeId, item.getSkuId()));
} }
return result; return result;
} }
List<GaoProductStock> result = new ArrayList<>(); List<GaoProductStock> result = new ArrayList<>();
for (GaoStockChangeItem item : itemList) { for (GaoStockChangeItem item : itemList) {
GaoProductStock stock = lockOrCreateStock(storeId, item.getProductId()); GaoProductStock stock = lockOrCreateStock(storeId, item.getSkuId());
BigDecimal onHandBefore = decimal(stock.getOnHandQuantity()); BigDecimal onHandBefore = decimal(stock.getOnHandQuantity());
BigDecimal reservedBefore = decimal(stock.getReservedQuantity()); BigDecimal reservedBefore = decimal(stock.getReservedQuantity());
BigDecimal onHandDelta = onHandDelta(operationType, item.getBaseQuantity()); BigDecimal onHandDelta = onHandDelta(operationType, item.getBaseQuantity());
@@ -319,9 +334,13 @@ public class GaoProductStockServiceImplement implements GaoProductStockService {
BigDecimal reservedAfter = reservedBefore.add(reservedDelta); BigDecimal reservedAfter = reservedBefore.add(reservedDelta);
GaoProductStockRecord record = new GaoProductStockRecord(); GaoProductStockRecord record = new GaoProductStockRecord();
record.setStoreId(storeId); record.setStoreId(storeId);
record.setProductId(item.getProductId()); record.setSkuId(item.getSkuId());
record.setProductUnitId(item.getProductUnitId()); record.setSkuUnitId(item.getSkuUnitId());
record.setProductIdSnapshot(item.getProductIdSnapshot());
record.setProductNameSnapshot(item.getProductNameSnapshot()); record.setProductNameSnapshot(item.getProductNameSnapshot());
record.setSkuCodeSnapshot(item.getSkuCodeSnapshot());
record.setSkuNameSnapshot(item.getSkuNameSnapshot());
record.setSpecSummarySnapshot(item.getSpecSummarySnapshot());
record.setUnitSnapshot(item.getUnitSnapshot()); record.setUnitSnapshot(item.getUnitSnapshot());
record.setBaseQuantityPerUnitSnapshot(item.getBaseQuantityPerUnitSnapshot()); record.setBaseQuantityPerUnitSnapshot(item.getBaseQuantityPerUnitSnapshot());
record.setQuantity(item.getQuantity()); record.setQuantity(item.getQuantity());
@@ -350,21 +369,21 @@ public class GaoProductStockServiceImplement implements GaoProductStockService {
return result; return result;
} }
private GaoProductStock lockOrCreateStock(String storeId, String productId) { private GaoProductStock lockOrCreateStock(String storeId, String skuId) {
GaoProductStock stock = stockMapper.selectForUpdate(storeId, productId); GaoProductStock stock = stockMapper.selectForUpdate(storeId, skuId);
if (stock != null) { if (stock != null) {
return stock; return stock;
} }
stock = new GaoProductStock(); stock = new GaoProductStock();
stock.setId(UUID.randomUUID().toString()); stock.setId(UUID.randomUUID().toString());
stock.setStoreId(storeId); stock.setStoreId(storeId);
stock.setProductId(productId); stock.setSkuId(skuId);
stock.setOnHandQuantity(ZERO); stock.setOnHandQuantity(ZERO);
stock.setReservedQuantity(ZERO); stock.setReservedQuantity(ZERO);
stock.setUpdatedBy(userLoginService.getRequireLoginUserId()); stock.setUpdatedBy(userLoginService.getRequireLoginUserId());
stock.setCreatedAt(Time.now()); stock.setCreatedAt(Time.now());
stockMapper.insertIgnore(stock); stockMapper.insertIgnore(stock);
stock = stockMapper.selectForUpdate(storeId, productId); stock = stockMapper.selectForUpdate(storeId, skuId);
TimiException.required(stock, "库存余额创建失败"); TimiException.required(stock, "库存余额创建失败");
return 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.required(quantity, "数量不能为空");
TimiException.requiredTrue(0 < quantity.signum(), "数量必须大于 0"); TimiException.requiredTrue(0 < quantity.signum(), "数量必须大于 0");
TimiException.requiredTrue(3 >= quantity.stripTrailingZeros().scale(), "数量最多保留三位小数"); TimiException.requiredTrue(3 >= quantity.stripTrailingZeros().scale(), "数量最多保留三位小数");
if (!Boolean.TRUE.equals(product.getAllowDecimalQuantity())) { if (!Boolean.TRUE.equals(sku.getAllowDecimalQuantity())) {
TimiException.requiredTrue(quantity.stripTrailingZeros().scale() <= 0, "该产品不允许小数数量"); TimiException.requiredTrue(quantity.stripTrailingZeros().scale() <= 0, "该产品不允许小数数量");
} }
return quantity; 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) { BigDecimal quantity, BigDecimal baseQuantity, String remark) {
TimiException.requiredTrue(existing.getOperationType() == operationType TimiException.requiredTrue(existing.getOperationType() == operationType
&& productId.equals(existing.getProductId()) && skuId.equals(existing.getSkuId())
&& unitId.equals(existing.getProductUnitId()) && unitId.equals(existing.getSkuUnitId())
&& quantity.compareTo(existing.getQuantity()) == 0 && quantity.compareTo(existing.getQuantity()) == 0
&& baseQuantity.compareTo(existing.getBaseQuantity()) == 0 && baseQuantity.compareTo(existing.getBaseQuantity()) == 0
&& sameText(existing.getRemark(), remark), "requestNo 对应库存操作内容不一致"); && sameText(existing.getRemark(), remark), "requestNo 对应库存操作内容不一致");
@@ -418,11 +437,11 @@ public class GaoProductStockServiceImplement implements GaoProductStockService {
Map<String, GaoProductStockRecord> recordMap = new HashMap<>(); Map<String, GaoProductStockRecord> recordMap = new HashMap<>();
for (GaoProductStockRecord record : existingList) { for (GaoProductStockRecord record : existingList) {
TimiException.requiredTrue(record.getOperationType() == operationType && bizId.equals(record.getBizId()), "requestNo 已被其他库存操作使用"); 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 对应库存操作内容不一致"); TimiException.requiredTrue(recordMap.size() == itemList.size(), "requestNo 对应库存操作内容不一致");
for (GaoStockChangeItem item : itemList) { for (GaoStockChangeItem item : itemList) {
GaoProductStockRecord record = recordMap.get(item.getProductId()); GaoProductStockRecord record = recordMap.get(item.getSkuId());
TimiException.required(record, "requestNo 对应库存操作内容不一致"); TimiException.required(record, "requestNo 对应库存操作内容不一致");
TimiException.requiredTrue(item.getBaseQuantity().compareTo(record.getBaseQuantity()) == 0, "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.setOnHandQuantity(decimal(stock.getOnHandQuantity()));
stock.setReservedQuantity(decimal(stock.getReservedQuantity())); stock.setReservedQuantity(decimal(stock.getReservedQuantity()));
stock.setAvailableQuantity(stock.getOnHandQuantity().subtract(stock.getReservedQuantity())); stock.setAvailableQuantity(stock.getOnHandQuantity().subtract(stock.getReservedQuantity()));
if (stock.getProduct() == null && stock.getProductId() != null) { if (stock.getSku() == null && stock.getSkuId() != null) {
stock.setProduct(productService.get(stock.getProductId())); stock.setSku(skuService.get(stock.getSkuId()));
} }
} }
@@ -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<GaoProductUnit> validateAndNormalize(GaoProduct product, List<GaoProductUnit> sourceList) {
TimiException.required(product, "not found product");
TimiException.requiredTrue(sourceList != null && !sourceList.isEmpty(), "产品至少需要一个单位");
List<GaoProductUnit> result = new ArrayList<>();
Set<String> nameSet = new HashSet<>();
Set<String> 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;
}
}
@@ -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.service.GaoSalesOrderChartService;
import com.imyeyu.api.modules.gao.vo.GaoCustomerOrderProductRankView; import com.imyeyu.api.modules.gao.vo.GaoCustomerOrderProductRankView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerOrderRankView; 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.GaoCustomerRepeatDailyStatView;
import com.imyeyu.api.modules.gao.vo.GaoCustomerRepeatIntervalStatView; import com.imyeyu.api.modules.gao.vo.GaoCustomerRepeatIntervalStatView;
import com.imyeyu.api.modules.gao.vo.GaoSalesOrderChartReq; import com.imyeyu.api.modules.gao.vo.GaoSalesOrderChartReq;
@@ -66,6 +67,14 @@ public class GaoSalesOrderChartServiceImplement implements GaoSalesOrderChartSer
return mapper.selectCustomerProductRank(req); return mapper.selectCustomerProductRank(req);
} }
@Override
public List<GaoCustomerOrderSkuRankView> customerSkuRank(GaoSalesOrderChartReq req) {
checkRangeReq(req);
TimiException.required(req.getCustomerId(), "客户订单 SKU 排行必须指定客户");
normalizeTopN(req);
return mapper.selectCustomerSkuRank(req);
}
@Override @Override
public List<GaoSalesOrderStatusStatView> status(GaoSalesOrderChartReq req) { public List<GaoSalesOrderStatusStatView> status(GaoSalesOrderChartReq req) {
checkRangeReq(req); checkRangeReq(req);
@@ -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.GaoEventRecord;
import com.imyeyu.api.modules.gao.entity.GaoFundRecord; import com.imyeyu.api.modules.gao.entity.GaoFundRecord;
import com.imyeyu.api.modules.gao.entity.GaoProduct; 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.GaoSalesOrder;
import com.imyeyu.api.modules.gao.entity.GaoSalesOrderItem; import com.imyeyu.api.modules.gao.entity.GaoSalesOrderItem;
import com.imyeyu.api.modules.gao.entity.GaoStore; 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.GaoOrderFulfillmentService;
import com.imyeyu.api.modules.gao.service.GaoPointTriggerService; import com.imyeyu.api.modules.gao.service.GaoPointTriggerService;
import com.imyeyu.api.modules.gao.service.GaoProductService; 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.GaoSalesOrderService;
import com.imyeyu.api.modules.gao.service.GaoStoreScopeService; import com.imyeyu.api.modules.gao.service.GaoStoreScopeService;
import com.imyeyu.api.modules.gao.service.GaoStoreService; import com.imyeyu.api.modules.gao.service.GaoStoreService;
import com.imyeyu.api.modules.gao.vo.GaoDirectProductOrderCreateRequest; import com.imyeyu.api.modules.gao.vo.GaoDirectProductOrderCreateRequest;
import com.imyeyu.api.modules.gao.vo.GaoPackageOrderCreateData; 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.GaoSalesOrderCancelRequest;
import com.imyeyu.api.modules.gao.vo.GaoSalesOrderItemRequest; import com.imyeyu.api.modules.gao.vo.GaoSalesOrderItemRequest;
import com.imyeyu.api.modules.gao.vo.GaoSalesOrderPage; import com.imyeyu.api.modules.gao.vo.GaoSalesOrderPage;
@@ -78,7 +79,7 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
private final ObjectProvider<GaoOrderFulfillmentService> fulfillmentServiceProvider; private final ObjectProvider<GaoOrderFulfillmentService> fulfillmentServiceProvider;
private final GaoCustomerService customerService; private final GaoCustomerService customerService;
private final GaoProductService productService; private final GaoProductService productService;
private final GaoProductUnitService unitService; private final GaoProductSkuService skuService;
private final GaoStoreService storeService; private final GaoStoreService storeService;
private final GaoStoreScopeService storeScopeService; private final GaoStoreScopeService storeScopeService;
private final GaoEventRecordService eventRecordService; private final GaoEventRecordService eventRecordService;
@@ -347,20 +348,20 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
private List<GaoSalesOrderItem> resolveDirectItems(String storeId, List<GaoSalesOrderItemRequest> sourceList) { private List<GaoSalesOrderItem> resolveDirectItems(String storeId, List<GaoSalesOrderItemRequest> sourceList) {
TimiException.requiredTrue(sourceList != null && !sourceList.isEmpty(), "直接产品订单至少需要一个产品"); TimiException.requiredTrue(sourceList != null && !sourceList.isEmpty(), "直接产品订单至少需要一个产品");
Set<String> productUnitKeySet = new HashSet<>(); Set<String> skuUnitKeySet = new HashSet<>();
List<GaoSalesOrderItem> result = new ArrayList<>(); List<GaoSalesOrderItem> result = new ArrayList<>();
boolean canOverridePrice = permissionChecker.hasAny(ModuleCode.GAO, GaoPermissionCode.SALES_ORDER_PRICE_OVERRIDE.getValue()); boolean canOverridePrice = permissionChecker.hasAny(ModuleCode.GAO, GaoPermissionCode.SALES_ORDER_PRICE_OVERRIDE.getValue());
for (int i = 0; i < sourceList.size(); i++) { for (int i = 0; i < sourceList.size(); i++) {
GaoSalesOrderItemRequest source = sourceList.get(i); GaoSalesOrderItemRequest source = sourceList.get(i);
TimiException.required(source, "订单产品明细不能为空"); TimiException.required(source, "订单产品明细不能为空");
TimiException.required(source.getProductId(), "产品不能为空"); TimiException.required(source.getSkuId(), "SKU 不能为空");
TimiException.required(source.getProductUnitId(), "产品单位不能为空"); TimiException.required(source.getSkuUnitId(), "SKU 单位不能为空");
TimiException.requiredTrue(productUnitKeySet.add(source.getProductId() + "|" + source.getProductUnitId()), "同一订单不能重复产品和单位"); TimiException.requiredTrue(skuUnitKeySet.add(source.getSkuId() + "|" + source.getSkuUnitId()), "同一订单不能重复 SKU 和单位");
GaoProductUnitSelection selection = productService.getSellableProductUnit(storeId, source.getProductId(), source.getProductUnitId()); GaoProductSkuUnitSelection selection = skuService.getSellableSkuUnit(storeId, source.getSkuId(), source.getSkuUnitId());
GaoProduct product = selection.getProduct(); GaoProduct product = selection.getProduct();
GaoProductUnit unit = selection.getUnit(); GaoProductSku sku = selection.getSku();
BigDecimal quantity = validateQuantity(product, source.getQuantity()); GaoProductSkuUnit unit = selection.getUnit();
unitService.convertQuantityToBase(unit, quantity); BigDecimal quantity = validateQuantity(sku, source.getQuantity());
Long unitPrice = TimiJava.defaultIfNull(source.getUnitPrice(), unit.getSalePrice()); Long unitPrice = TimiJava.defaultIfNull(source.getUnitPrice(), unit.getSalePrice());
TimiException.required(unitPrice, "产品单价不能为空"); TimiException.required(unitPrice, "产品单价不能为空");
TimiException.requiredTrue(0 <= unitPrice, "产品单价不能为负数"); TimiException.requiredTrue(0 <= unitPrice, "产品单价不能为负数");
@@ -370,10 +371,14 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
} }
long lineAmount = toCentAmount(quantity, unitPrice); long lineAmount = toCentAmount(quantity, unitPrice);
GaoSalesOrderItem item = new GaoSalesOrderItem(); GaoSalesOrderItem item = new GaoSalesOrderItem();
item.setProductId(product.getId()); item.setSkuId(sku.getId());
item.setProductUnitId(unit.getId()); item.setSkuUnitId(unit.getId());
item.setItemType(GaoSalesOrderItem.ItemType.DIRECT_PRODUCT); item.setItemType(GaoSalesOrderItem.ItemType.DIRECT_PRODUCT);
item.setProductNameSnapshot(product.getName()); item.setProductNameSnapshot(product.getName());
item.setProductIdSnapshot(product.getId());
item.setSkuCodeSnapshot(sku.getCode());
item.setSkuNameSnapshot(sku.getName());
item.setSpecSummarySnapshot(sku.getSpecSummary());
item.setUnitSnapshot(unit.getName()); item.setUnitSnapshot(unit.getName());
item.setBaseQuantityPerUnitSnapshot(unit.getBaseQuantityPerUnit()); item.setBaseQuantityPerUnitSnapshot(unit.getBaseQuantityPerUnit());
item.setQuantity(quantity); item.setQuantity(quantity);
@@ -392,18 +397,18 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
private void verifyExistingDirectItems(GaoSalesOrder existing, List<GaoSalesOrderItemRequest> requestList) { private void verifyExistingDirectItems(GaoSalesOrder existing, List<GaoSalesOrderItemRequest> requestList) {
List<GaoSalesOrderItem> existingList = itemMapper.selectBySalesOrderId(existing.getId()); List<GaoSalesOrderItem> existingList = itemMapper.selectBySalesOrderId(existing.getId());
TimiException.requiredTrue(requestList != null && existingList.size() == requestList.size(), "requestNo 对应订单内容不一致"); TimiException.requiredTrue(requestList != null && existingList.size() == requestList.size(), "requestNo 对应订单内容不一致");
Set<String> productUnitKeySet = new HashSet<>(); Set<String> skuUnitKeySet = new HashSet<>();
for (GaoSalesOrderItemRequest request : requestList) { for (GaoSalesOrderItemRequest request : requestList) {
TimiException.required(request, "订单产品明细不能为空"); TimiException.required(request, "订单产品明细不能为空");
TimiException.required(request.getProductId(), "产品不能为空"); TimiException.required(request.getSkuId(), "SKU 不能为空");
TimiException.required(request.getProductUnitId(), "产品单位不能为空"); TimiException.required(request.getSkuUnitId(), "SKU 单位不能为空");
TimiException.required(request.getQuantity(), "购买数量不能为空"); 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() 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); .findFirst().orElse(null);
TimiException.required(item, "requestNo 对应订单内容不一致"); TimiException.required(item, "requestNo 对应订单内容不一致");
TimiException.requiredTrue(item.getProductUnitId().equals(request.getProductUnitId()) TimiException.requiredTrue(item.getSkuUnitId().equals(request.getSkuUnitId())
&& item.getQuantity().compareTo(request.getQuantity()) == 0 && item.getQuantity().compareTo(request.getQuantity()) == 0
&& (request.getUnitPrice() == null || item.getUnitPrice().equals(request.getUnitPrice())), "requestNo 对应订单内容不一致"); && (request.getUnitPrice() == null || item.getUnitPrice().equals(request.getUnitPrice())), "requestNo 对应订单内容不一致");
} }
@@ -434,15 +439,15 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
TimiException.requiredTrue(0 <= saleAmount, "成交价不能为负数"); TimiException.requiredTrue(0 <= saleAmount, "成交价不能为负数");
GaoSalesOrderItem.ItemType expectedItemType = orderType == GaoSalesOrder.OrderType.PACKAGE GaoSalesOrderItem.ItemType expectedItemType = orderType == GaoSalesOrder.OrderType.PACKAGE
? GaoSalesOrderItem.ItemType.PACKAGE_ENTITLEMENT : GaoSalesOrderItem.ItemType.DIRECT_PRODUCT; ? GaoSalesOrderItem.ItemType.PACKAGE_ENTITLEMENT : GaoSalesOrderItem.ItemType.DIRECT_PRODUCT;
Set<String> productUnitKeySet = new HashSet<>(); Set<String> skuUnitKeySet = new HashSet<>();
long lineAmountSum = 0L; long lineAmountSum = 0L;
for (GaoSalesOrderItem item : itemList) { for (GaoSalesOrderItem item : itemList) {
TimiException.required(item, "销售订单明细不能为空"); TimiException.required(item, "销售订单明细不能为空");
TimiException.requiredTrue(item.getItemType() == expectedItemType, "订单项类型与订单类型不匹配"); TimiException.requiredTrue(item.getItemType() == expectedItemType, "订单项类型与订单类型不匹配");
TimiException.required(item.getProductId(), "订单项产品不能为空"); TimiException.required(item.getSkuId(), "订单项 SKU 不能为空");
TimiException.required(item.getProductUnitId(), "订单项单位不能为空"); TimiException.required(item.getSkuUnitId(), "订单项 SKU 单位不能为空");
TimiException.requiredTrue(productUnitKeySet.add(item.getProductId() + "|" + item.getProductUnitId()), TimiException.requiredTrue(skuUnitKeySet.add(item.getSkuId() + "|" + item.getSkuUnitId()),
"同一订单不能重复产品和单位"); "同一订单不能重复 SKU 和单位");
TimiException.required(item.getQuantity(), "订单项数量不能为空"); TimiException.required(item.getQuantity(), "订单项数量不能为空");
TimiException.requiredTrue(0 < item.getQuantity().signum(), "订单项数量必须大于 0"); TimiException.requiredTrue(0 < item.getQuantity().signum(), "订单项数量必须大于 0");
TimiException.requiredTrue(3 >= item.getQuantity().stripTrailingZeros().scale(), "订单项数量最多保留三位小数"); TimiException.requiredTrue(3 >= item.getQuantity().stripTrailingZeros().scale(), "订单项数量最多保留三位小数");
@@ -494,9 +499,9 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
List<GaoSalesOrderItem> existingItemList = itemMapper.selectBySalesOrderId(existing.getId()); List<GaoSalesOrderItem> existingItemList = itemMapper.selectBySalesOrderId(existing.getId());
TimiException.requiredTrue(existingItemList.size() == requestItemList.size(), "requestNo 对应订单内容不一致"); TimiException.requiredTrue(existingItemList.size() == requestItemList.size(), "requestNo 对应订单内容不一致");
Map<String, GaoSalesOrderItem> existingItemMap = existingItemList.stream() Map<String, GaoSalesOrderItem> 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) { 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.required(existingItem, "requestNo 对应订单内容不一致");
TimiException.requiredTrue(existingItem.getItemType() == GaoSalesOrderItem.ItemType.PACKAGE_ENTITLEMENT TimiException.requiredTrue(existingItem.getItemType() == GaoSalesOrderItem.ItemType.PACKAGE_ENTITLEMENT
&& existingItem.getQuantity().compareTo(requestItem.getQuantity()) == 0 && existingItem.getQuantity().compareTo(requestItem.getQuantity()) == 0
@@ -550,20 +555,19 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
if (itemList == null || itemList.isEmpty()) { if (itemList == null || itemList.isEmpty()) {
return; return;
} }
List<String> productIdList = itemList.stream() List<String> skuIdList = itemList.stream()
.map(GaoSalesOrderItem::getProductId) .map(GaoSalesOrderItem::getSkuId)
.filter(value -> value != null && !value.isBlank()) .filter(value -> value != null && !value.isBlank())
.distinct() .distinct()
.toList(); .toList();
if (productIdList.isEmpty()) { if (skuIdList.isEmpty()) {
return; return;
} }
Map<String, List<Attachment>> attachmentMap = attachmentService.mapByBizIdList( Map<String, List<Attachment>> attachmentMap = attachmentService.mapByBizIdList(
Attachment.BizType.GAO_PRODUCT, Attachment.BizType.GAO_PRODUCT_SKU,
productIdList, skuIdList);
GaoProduct.AttachType.PHOTO.name());
for (GaoSalesOrderItem item : itemList) { 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<GaoSale
} }
} }
private BigDecimal validateQuantity(GaoProduct product, BigDecimal quantity) { private BigDecimal validateQuantity(GaoProductSku sku, BigDecimal quantity) {
TimiException.required(quantity, "购买数量不能为空"); TimiException.required(quantity, "购买数量不能为空");
TimiException.requiredTrue(0 < quantity.signum(), "购买数量必须大于 0"); TimiException.requiredTrue(0 < quantity.signum(), "购买数量必须大于 0");
TimiException.requiredTrue(3 >= quantity.stripTrailingZeros().scale(), "购买数量最多保留三位小数"); TimiException.requiredTrue(3 >= quantity.stripTrailingZeros().scale(), "购买数量最多保留三位小数");
if (!Boolean.TRUE.equals(product.getAllowDecimalQuantity())) { if (!Boolean.TRUE.equals(sku.getAllowDecimalQuantity())) {
TimiException.requiredTrue(quantity.stripTrailingZeros().scale() <= 0, "该产品不允许小数数量"); TimiException.requiredTrue(quantity.stripTrailingZeros().scale() <= 0, "该产品不允许小数数量");
} }
return quantity; return quantity;
@@ -37,4 +37,19 @@ public class GaoCustomerOrderProductRankView {
/// 直接产品销售金额,单位分 /// 直接产品销售金额,单位分
private Long directSaleAmount; private Long directSaleAmount;
/// 直接产品购买数量,基础单位
private BigDecimal directBaseQuantity;
/// 套餐权益购买数量,基础单位
private BigDecimal packageBaseQuantity;
/// 已预占数量,基础单位
private BigDecimal reservedBaseQuantity;
/// 已履约数量,基础单位
private BigDecimal fulfilledBaseQuantity;
/// 当前可履约数量,基础单位
private BigDecimal availableBaseQuantity;
} }
@@ -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;
}
@@ -11,15 +11,27 @@ import java.math.BigDecimal;
@Data @Data
public class GaoCustomerPackagePurchaseItem { public class GaoCustomerPackagePurchaseItem {
/// 产品 ID /// SKU ID
private String productId; private String skuId;
/// 产品单位 ID /// SKU 单位 ID
private String productUnitId; private String skuUnitId;
/// 产品名称快照,由后端生成 /// 产品名称快照,由后端生成
private String productNameSnapshot; private String productNameSnapshot;
/// 产品 ID 快照,由后端生成
private String productIdSnapshot;
/// SKU 编码快照,由后端生成
private String skuCodeSnapshot;
/// SKU 名称快照,由后端生成
private String skuNameSnapshot;
/// 规格摘要快照,由后端生成
private String specSummarySnapshot;
/// 单位快照,由后端生成 /// 单位快照,由后端生成
private String unitSnapshot; private String unitSnapshot;
@@ -16,6 +16,9 @@ public class GaoProductChartReq {
/// 产品 ID /// 产品 ID
private String productId; private String productId;
/// SKU ID
private String skuId;
/// 分类 ID /// 分类 ID
private String categoryId; private String categoryId;
@@ -20,6 +20,12 @@ public class GaoProductFulfillmentDailyStatView {
/// 套餐权益履约数量 /// 套餐权益履约数量
private BigDecimal packageQuantity; private BigDecimal packageQuantity;
/// 直接产品履约数量,基础单位
private BigDecimal directBaseQuantity;
/// 套餐权益履约数量,基础单位
private BigDecimal packageBaseQuantity;
/// 履约基础单位数量 /// 履约基础单位数量
private BigDecimal baseQuantity; private BigDecimal baseQuantity;
} }
@@ -34,4 +34,19 @@ public class GaoProductFulfillmentRankView {
/// 当前可履约数量 /// 当前可履约数量
private BigDecimal availableQuantity; private BigDecimal availableQuantity;
/// 直接产品购买数量,基础单位
private BigDecimal directBaseQuantity;
/// 套餐权益购买数量,基础单位
private BigDecimal packageBaseQuantity;
/// 待履约数量,基础单位
private BigDecimal reservedBaseQuantity;
/// 已履约数量,基础单位
private BigDecimal fulfilledBaseQuantity;
/// 当前可履约数量,基础单位
private BigDecimal availableBaseQuantity;
} }
@@ -23,6 +23,12 @@ public class GaoProductSalesDailyStatView {
/// 套餐权益数量 /// 套餐权益数量
private BigDecimal packageQuantity; private BigDecimal packageQuantity;
/// 直接产品销售数量,基础单位
private BigDecimal directBaseQuantity;
/// 套餐权益数量,基础单位
private BigDecimal packageBaseQuantity;
/// 直接产品销售金额,单位分 /// 直接产品销售金额,单位分
private Long directSaleAmount; private Long directSaleAmount;
} }
@@ -26,6 +26,12 @@ public class GaoProductSalesRankView {
/// 套餐权益数量 /// 套餐权益数量
private BigDecimal packageQuantity; private BigDecimal packageQuantity;
/// 直接产品销售数量,基础单位
private BigDecimal directBaseQuantity;
/// 套餐权益数量,基础单位
private BigDecimal packageBaseQuantity;
/// 直接产品销售金额,单位分 /// 直接产品销售金额,单位分
private Long directSaleAmount; private Long directSaleAmount;
} }
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -19,11 +19,11 @@ public class GaoProductStockChangeRequest {
/// 门店 ID /// 门店 ID
private String storeId; private String storeId;
/// 产品 ID /// SKU ID
private String productId; private String skuId;
/// 产品单位 ID /// SKU 单位 ID
private String productUnitId; private String skuUnitId;
/// 业务数量 /// 业务数量
private BigDecimal quantity; private BigDecimal quantity;
@@ -41,4 +41,10 @@ public class GaoProductStockHealthView {
/// 1 为低于最低库存,0 为正常 /// 1 为低于最低库存,0 为正常
private Integer lowStock; private Integer lowStock;
/// 参与库存统计的 SKU 数量
private Long skuCount;
/// 低于最低库存的 SKU 数量
private Long lowStockSkuCount;
} }
@@ -18,6 +18,6 @@ public class GaoProductStockPage extends BasePage {
/// 产品 ID /// 产品 ID
private String productId; private String productId;
/// 产品名称或编码关键词 /// 产品或 SKU 名称、编码、规格摘要关键词
private String keyword; private String keyword;
} }
@@ -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;
}
@@ -17,9 +17,24 @@ public class GaoProductUnitSalesRankView {
/// 产品名称 /// 产品名称
private String productName; private String productName;
/// SKU ID
private String skuId;
/// SKU 编码快照
private String skuCode;
/// SKU 名称快照
private String skuName;
/// 规格摘要快照
private String specSummary;
/// 产品单位 ID /// 产品单位 ID
private String productUnitId; private String productUnitId;
/// SKU 单位 ID
private String skuUnitId;
/// 单位名称 /// 单位名称
private String unitName; private String unitName;
@@ -29,6 +44,9 @@ public class GaoProductUnitSalesRankView {
/// 销售数量 /// 销售数量
private BigDecimal quantity; private BigDecimal quantity;
/// 销售数量,基础单位
private BigDecimal baseQuantity;
/// 销售金额,单位分 /// 销售金额,单位分
private Long saleAmount; private Long saleAmount;
} }
@@ -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;
}
@@ -16,6 +16,12 @@ public class GaoSalesOrderChartReq {
/// 客户 ID /// 客户 ID
private String customerId; private String customerId;
/// 产品 ID
private String productId;
/// SKU ID
private String skuId;
/// 订单类型 /// 订单类型
private GaoSalesOrder.OrderType orderType; private GaoSalesOrder.OrderType orderType;
@@ -11,11 +11,11 @@ import java.math.BigDecimal;
@Data @Data
public class GaoSalesOrderItemRequest { public class GaoSalesOrderItemRequest {
/// 产品 ID /// SKU ID
private String productId; private String skuId;
/// 产品单位 ID /// SKU 单位 ID
private String productUnitId; private String skuUnitId;
/// 购买数量 /// 购买数量
private BigDecimal quantity; private BigDecimal quantity;
@@ -1,7 +1,8 @@
package com.imyeyu.api.modules.gao.vo; package com.imyeyu.api.modules.gao.vo;
import com.imyeyu.api.modules.gao.entity.GaoProduct; 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 lombok.Data;
import java.math.BigDecimal; import java.math.BigDecimal;
@@ -13,15 +14,27 @@ import java.math.BigDecimal;
@Data @Data
public class GaoStockChangeItem { public class GaoStockChangeItem {
/// 产品 ID /// SKU ID
private String productId; private String skuId;
/// 产品单位 ID /// SKU 单位 ID
private String productUnitId; private String skuUnitId;
/// 产品 ID 快照
private String productIdSnapshot;
/// 产品名称快照 /// 产品名称快照
private String productNameSnapshot; private String productNameSnapshot;
/// SKU 编码快照
private String skuCodeSnapshot;
/// SKU 名称快照
private String skuNameSnapshot;
/// 规格摘要快照
private String specSummarySnapshot;
/// 单位名称快照 /// 单位名称快照
private String unitSnapshot; private String unitSnapshot;
@@ -40,15 +53,20 @@ public class GaoStockChangeItem {
/// 生成库存项 /// 生成库存项
/// ///
/// @param product 产品 /// @param product 产品
/// @param unit 单位 /// @param sku SKU
/// @param unit SKU 单位
/// @param quantity 业务数量 /// @param quantity 业务数量
/// @param baseQuantity 基础单位数量 /// @param baseQuantity 基础单位数量
/// @return 库存变更项 /// @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(); GaoStockChangeItem result = new GaoStockChangeItem();
result.productId = product.getId(); result.skuId = sku.getId();
result.productUnitId = unit.getId(); result.skuUnitId = unit.getId();
result.productIdSnapshot = product.getId();
result.productNameSnapshot = product.getName(); result.productNameSnapshot = product.getName();
result.skuCodeSnapshot = sku.getCode();
result.skuNameSnapshot = sku.getName();
result.specSummarySnapshot = sku.getSpecSummary();
result.unitSnapshot = unit.getName(); result.unitSnapshot = unit.getName();
result.baseQuantityPerUnitSnapshot = unit.getBaseQuantityPerUnit(); result.baseQuantityPerUnitSnapshot = unit.getBaseQuantityPerUnit();
result.quantity = quantity; result.quantity = quantity;
@@ -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`);
@@ -5,6 +5,10 @@
AND p.`deleted_at` IS NULL AND p.`deleted_at` IS NULL
<if test="storeId != null and storeId != ''">AND p.`store_id` = #{storeId}</if> <if test="storeId != null and storeId != ''">AND p.`store_id` = #{storeId}</if>
<if test="productId != null and productId != ''">AND p.`id` = #{productId}</if> <if test="productId != null and productId != ''">AND p.`id` = #{productId}</if>
<if test="skuId != null and skuId != ''">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
)</if>
<if test="categoryId != null and categoryId != ''">AND p.`category_id` = #{categoryId}</if> <if test="categoryId != null and categoryId != ''">AND p.`category_id` = #{categoryId}</if>
<if test="productType != null">AND p.`product_type` = #{productType}</if> <if test="productType != null">AND p.`product_type` = #{productType}</if>
</sql> </sql>
@@ -14,7 +18,8 @@
AND i.`deleted_at` IS NULL AND i.`deleted_at` IS NULL
AND o.`status` &lt;&gt; 'CANCELLED' AND o.`status` &lt;&gt; 'CANCELLED'
<if test="storeId != null and storeId != ''">AND o.`store_id` = #{storeId}</if> <if test="storeId != null and storeId != ''">AND o.`store_id` = #{storeId}</if>
<if test="productId != null and productId != ''">AND i.`product_id` = #{productId}</if> <if test="productId != null and productId != ''">AND i.`product_id_snapshot` = #{productId}</if>
<if test="skuId != null and skuId != ''">AND i.`sku_id` = #{skuId}</if>
<if test="categoryId != null and categoryId != ''">AND p.`category_id` = #{categoryId}</if> <if test="categoryId != null and categoryId != ''">AND p.`category_id` = #{categoryId}</if>
<if test="productType != null">AND p.`product_type` = #{productType}</if> <if test="productType != null">AND p.`product_type` = #{productType}</if>
AND o.`purchased_at` &gt;= #{beginAt} AND o.`purchased_at` &gt;= #{beginAt}
@@ -27,10 +32,12 @@
COUNT(DISTINCT o.`id`) AS `orderCount`, 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` = '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` = '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` 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 FROM `gao_sales_order_item` i
INNER JOIN `gao_sales_order` o ON o.`id` = i.`sales_order_id` 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 WHERE 1 = 1
<include refid="orderProductFilter"/> <include refid="orderProductFilter"/>
GROUP BY DATE_FORMAT(FROM_UNIXTIME(o.`purchased_at` / 1000), '%Y%m%d') GROUP BY DATE_FORMAT(FROM_UNIXTIME(o.`purchased_at` / 1000), '%Y%m%d')
@@ -39,22 +46,24 @@
<select id="selectSalesRank" resultType="com.imyeyu.api.modules.gao.vo.GaoProductSalesRankView"> <select id="selectSalesRank" resultType="com.imyeyu.api.modules.gao.vo.GaoProductSalesRankView">
SELECT SELECT
i.`product_id` AS `productId`, i.`product_id_snapshot` AS `productId`,
MAX(i.`product_name_snapshot`) AS `productName`, MAX(i.`product_name_snapshot`) AS `productName`,
COUNT(DISTINCT o.`id`) AS `orderCount`, 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` = '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` = '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` 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 FROM `gao_sales_order_item` i
INNER JOIN `gao_sales_order` o ON o.`id` = i.`sales_order_id` 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 WHERE 1 = 1
<include refid="orderProductFilter"/> <include refid="orderProductFilter"/>
GROUP BY i.`product_id` GROUP BY i.`product_id_snapshot`
ORDER BY (SUM(CASE WHEN i.`item_type` = 'DIRECT_PRODUCT' THEN i.`quantity` ELSE 0 END) ORDER BY (SUM(CASE WHEN i.`item_type` = 'DIRECT_PRODUCT' THEN i.`quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1) ELSE 0 END)
+ SUM(CASE WHEN i.`item_type` = 'PACKAGE_ENTITLEMENT' THEN i.`quantity` ELSE 0 END)) DESC, + SUM(CASE WHEN i.`item_type` = 'PACKAGE_ENTITLEMENT' THEN i.`quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1) ELSE 0 END)) DESC,
directSaleAmount DESC, directSaleAmount DESC,
i.`product_id` ASC i.`product_id_snapshot` ASC
LIMIT #{topN} LIMIT #{topN}
</select> </select>
@@ -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`, 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' 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`, 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` 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 FROM `gao_order_fulfillment` f
INNER JOIN `gao_order_fulfillment_item` fi ON fi.`fulfillment_id` = f.`id` 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_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_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} WHERE f.`store_id` = #{storeId}
AND f.`status` = 'VERIFIED' AND f.`status` = 'VERIFIED'
AND f.`delivered_at` &gt;= #{beginAt} AND f.`delivered_at` &gt;= #{beginAt}
AND f.`delivered_at` &lt;= #{endAt} AND f.`delivered_at` &lt;= #{endAt}
<if test="productId != null and productId != ''">AND fi.`product_id` = #{productId}</if> <if test="productId != null and productId != ''">AND fi.`product_id_snapshot` = #{productId}</if>
<if test="skuId != null and skuId != ''">AND fi.`sku_id` = #{skuId}</if>
<if test="categoryId != null and categoryId != ''">AND p.`category_id` = #{categoryId}</if> <if test="categoryId != null and categoryId != ''">AND p.`category_id` = #{categoryId}</if>
<if test="productType != null">AND p.`product_type` = #{productType}</if> <if test="productType != null">AND p.`product_type` = #{productType}</if>
GROUP BY DATE_FORMAT(FROM_UNIXTIME(f.`delivered_at` / 1000), '%Y%m%d') GROUP BY DATE_FORMAT(FROM_UNIXTIME(f.`delivered_at` / 1000), '%Y%m%d')
@@ -84,21 +98,26 @@
<select id="selectFulfillmentRank" resultType="com.imyeyu.api.modules.gao.vo.GaoProductFulfillmentRankView"> <select id="selectFulfillmentRank" resultType="com.imyeyu.api.modules.gao.vo.GaoProductFulfillmentRankView">
SELECT SELECT
i.`product_id` AS `productId`, i.`product_id_snapshot` AS `productId`,
MAX(i.`product_name_snapshot`) AS `productName`, MAX(i.`product_name_snapshot`) AS `productName`,
COUNT(DISTINCT o.`id`) AS `orderCount`, 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` = '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` = 'PACKAGE_ENTITLEMENT' THEN i.`quantity` ELSE 0 END), 0) AS `packageQuantity`,
COALESCE(SUM(i.`reserved_quantity`), 0) AS `reservedQuantity`, COALESCE(SUM(i.`reserved_quantity`), 0) AS `reservedQuantity`,
COALESCE(SUM(i.`fulfilled_quantity`), 0) AS `fulfilledQuantity`, COALESCE(SUM(i.`fulfilled_quantity`), 0) AS `fulfilledQuantity`,
COALESCE(SUM(i.`quantity` - i.`reserved_quantity` - i.`fulfilled_quantity`), 0) AS `availableQuantity` COALESCE(SUM(i.`quantity` - i.`reserved_quantity` - i.`fulfilled_quantity`), 0) AS `availableQuantity`,
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(i.`reserved_quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `reservedBaseQuantity`,
COALESCE(SUM(i.`fulfilled_quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `fulfilledBaseQuantity`,
COALESCE(SUM((i.`quantity` - i.`reserved_quantity` - i.`fulfilled_quantity`) * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `availableBaseQuantity`
FROM `gao_sales_order_item` i FROM `gao_sales_order_item` i
INNER JOIN `gao_sales_order` o ON o.`id` = i.`sales_order_id` 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 WHERE 1 = 1
<include refid="orderProductFilter"/> <include refid="orderProductFilter"/>
GROUP BY i.`product_id` GROUP BY i.`product_id_snapshot`
ORDER BY `availableQuantity` DESC, `fulfilledQuantity` DESC, i.`product_id` ASC ORDER BY `availableBaseQuantity` DESC, `fulfilledBaseQuantity` DESC, i.`product_id_snapshot` ASC
LIMIT #{topN} LIMIT #{topN}
</select> </select>
@@ -111,11 +130,12 @@
COALESCE(SUM(CASE WHEN r.`reserved_delta` &gt; 0 THEN r.`reserved_delta` ELSE 0 END), 0) AS `reserveQuantity`, COALESCE(SUM(CASE WHEN r.`reserved_delta` &gt; 0 THEN r.`reserved_delta` ELSE 0 END), 0) AS `reserveQuantity`,
COALESCE(SUM(CASE WHEN r.`reserved_delta` &lt; 0 THEN -r.`reserved_delta` ELSE 0 END), 0) AS `releaseQuantity` COALESCE(SUM(CASE WHEN r.`reserved_delta` &lt; 0 THEN -r.`reserved_delta` ELSE 0 END), 0) AS `releaseQuantity`
FROM `gao_product_stock_record` r 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} WHERE r.`store_id` = #{storeId}
AND r.`occurred_at` &gt;= #{beginAt} AND r.`occurred_at` &gt;= #{beginAt}
AND r.`occurred_at` &lt;= #{endAt} AND r.`occurred_at` &lt;= #{endAt}
<if test="productId != null and productId != ''">AND r.`product_id` = #{productId}</if> <if test="productId != null and productId != ''">AND r.`product_id_snapshot` = #{productId}</if>
<if test="skuId != null and skuId != ''">AND r.`sku_id` = #{skuId}</if>
<if test="categoryId != null and categoryId != ''">AND p.`category_id` = #{categoryId}</if> <if test="categoryId != null and categoryId != ''">AND p.`category_id` = #{categoryId}</if>
<if test="productType != null">AND p.`product_type` = #{productType}</if> <if test="productType != null">AND p.`product_type` = #{productType}</if>
GROUP BY DATE_FORMAT(FROM_UNIXTIME(r.`occurred_at` / 1000), '%Y%m%d') GROUP BY DATE_FORMAT(FROM_UNIXTIME(r.`occurred_at` / 1000), '%Y%m%d')
@@ -129,19 +149,25 @@
p.`category_id` AS `categoryId`, p.`category_id` AS `categoryId`,
c.`name` AS `categoryName`, c.`name` AS `categoryName`,
p.`product_type` AS `productType`, p.`product_type` AS `productType`,
COALESCE(s.`on_hand_quantity`, 0) AS `onHandQuantity`, COALESCE(SUM(s.`on_hand_quantity`), 0) AS `onHandQuantity`,
COALESCE(s.`reserved_quantity`, 0) AS `reservedQuantity`, COALESCE(SUM(s.`reserved_quantity`), 0) AS `reservedQuantity`,
COALESCE(s.`on_hand_quantity`, 0) - COALESCE(s.`reserved_quantity`, 0) AS `availableQuantity`, COALESCE(SUM(s.`on_hand_quantity` - s.`reserved_quantity`), 0) AS `availableQuantity`,
p.`min_stock_quantity` AS `minStockQuantity`, CASE WHEN COUNT(k.`min_stock_quantity`) = 0 THEN NULL ELSE SUM(k.`min_stock_quantity`) END AS `minStockQuantity`,
CASE WHEN p.`min_stock_quantity` IS NOT NULL MAX(CASE WHEN k.`min_stock_quantity` IS NOT NULL
AND COALESCE(s.`on_hand_quantity`, 0) - COALESCE(s.`reserved_quantity`, 0) &lt; p.`min_stock_quantity` AND COALESCE(s.`on_hand_quantity`, 0) - COALESCE(s.`reserved_quantity`, 0) &lt; k.`min_stock_quantity`
THEN 1 ELSE 0 END AS `lowStock` 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) &lt; k.`min_stock_quantity`
THEN 1 ELSE 0 END) AS `lowStockSkuCount`
FROM `gao_product` p 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_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} WHERE p.`store_id` = #{storeId}
AND p.`stock_enabled` = 1 AND k.`stock_enabled` = 1
<include refid="productFilter"/> <include refid="productFilter"/>
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 ORDER BY `lowStock` DESC, `availableQuantity` ASC, p.`sort` ASC, p.`name` ASC
LIMIT #{topN} LIMIT #{topN}
</select> </select>
@@ -172,20 +198,26 @@
<select id="selectUnitSalesRank" resultType="com.imyeyu.api.modules.gao.vo.GaoProductUnitSalesRankView"> <select id="selectUnitSalesRank" resultType="com.imyeyu.api.modules.gao.vo.GaoProductUnitSalesRankView">
SELECT SELECT
i.`product_id` AS `productId`, i.`product_id_snapshot` AS `productId`,
MAX(i.`product_name_snapshot`) AS `productName`, MAX(i.`product_name_snapshot`) AS `productName`,
i.`product_unit_id` AS `productUnitId`, i.`sku_id` AS `skuId`,
MAX(i.`sku_code_snapshot`) AS `skuCode`,
MAX(i.`sku_name_snapshot`) AS `skuName`,
MAX(i.`spec_summary_snapshot`) AS `specSummary`,
i.`sku_unit_id` AS `productUnitId`,
i.`sku_unit_id` AS `skuUnitId`,
MAX(i.`unit_snapshot`) AS `unitName`, MAX(i.`unit_snapshot`) AS `unitName`,
COUNT(DISTINCT o.`id`) AS `orderCount`, COUNT(DISTINCT o.`id`) AS `orderCount`,
COALESCE(SUM(i.`quantity`), 0) AS `quantity`, COALESCE(SUM(i.`quantity`), 0) AS `quantity`,
COALESCE(SUM(i.`quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `baseQuantity`,
COALESCE(SUM(i.`line_amount`), 0) AS `saleAmount` COALESCE(SUM(i.`line_amount`), 0) AS `saleAmount`
FROM `gao_sales_order_item` i FROM `gao_sales_order_item` i
INNER JOIN `gao_sales_order` o ON o.`id` = i.`sales_order_id` 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 i.`item_type` = 'DIRECT_PRODUCT' WHERE i.`item_type` = 'DIRECT_PRODUCT'
<include refid="orderProductFilter"/> <include refid="orderProductFilter"/>
GROUP BY i.`product_id`, i.`product_unit_id` GROUP BY i.`product_id_snapshot`, i.`sku_id`, i.`sku_unit_id`
ORDER BY `quantity` DESC, `saleAmount` DESC, i.`product_id` ASC, i.`product_unit_id` ASC ORDER BY `baseQuantity` DESC, `saleAmount` DESC, i.`product_id_snapshot` ASC, i.`sku_id` ASC, i.`sku_unit_id` ASC
LIMIT #{topN} LIMIT #{topN}
</select> </select>
</mapper> </mapper>
@@ -0,0 +1,199 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.imyeyu.api.modules.gao.mapper.GaoProductSkuChartMapper">
<sql id="skuOrderProductFilter">
AND o.`deleted_at` IS NULL
AND i.`deleted_at` IS NULL
AND o.`status` &lt;&gt; 'CANCELLED'
AND o.`store_id` = #{storeId}
<if test="productId != null and productId != ''">AND i.`product_id_snapshot` = #{productId}</if>
<if test="skuId != null and skuId != ''">AND i.`sku_id` = #{skuId}</if>
<if test="categoryId != null and categoryId != ''">AND p.`category_id` = #{categoryId}</if>
<if test="productType != null">AND p.`product_type` = #{productType}</if>
AND o.`purchased_at` &gt;= #{beginAt}
AND o.`purchased_at` &lt;= #{endAt}
</sql>
<select id="selectSalesDaily" resultType="com.imyeyu.api.modules.gao.vo.GaoProductSkuSalesDailyStatView">
SELECT
CAST(DATE_FORMAT(FROM_UNIXTIME(o.`purchased_at` / 1000), '%Y%m%d') AS UNSIGNED) AS `dateValue`,
i.`sku_id` AS `skuId`,
MAX(i.`sku_code_snapshot`) AS `skuCode`,
MAX(i.`sku_name_snapshot`) AS `skuName`,
MAX(i.`spec_summary_snapshot`) AS `specSummary`,
MAX(i.`product_id_snapshot`) AS `productId`,
MAX(i.`product_name_snapshot`) AS `productName`,
COUNT(DISTINCT o.`id`) AS `orderCount`,
COALESCE(SUM(CASE WHEN i.`item_type` = 'DIRECT_PRODUCT' THEN i.`quantity` * 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_snapshot` AND p.`store_id` = o.`store_id`
WHERE 1 = 1
<include refid="skuOrderProductFilter"/>
GROUP BY DATE_FORMAT(FROM_UNIXTIME(o.`purchased_at` / 1000), '%Y%m%d'), i.`sku_id`
ORDER BY `dateValue` ASC, i.`sku_id` ASC
</select>
<select id="selectSalesRank" resultType="com.imyeyu.api.modules.gao.vo.GaoProductSkuSalesRankView">
SELECT
i.`sku_id` AS `skuId`,
MAX(i.`sku_code_snapshot`) AS `skuCode`,
MAX(i.`sku_name_snapshot`) AS `skuName`,
MAX(i.`spec_summary_snapshot`) AS `specSummary`,
MAX(i.`product_id_snapshot`) AS `productId`,
MAX(i.`product_name_snapshot`) AS `productName`,
COUNT(DISTINCT o.`id`) AS `orderCount`,
COALESCE(SUM(CASE WHEN i.`item_type` = 'DIRECT_PRODUCT' THEN i.`quantity` * 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_snapshot` AND p.`store_id` = o.`store_id`
WHERE 1 = 1
<include refid="skuOrderProductFilter"/>
GROUP BY i.`sku_id`
ORDER BY (SUM(CASE WHEN i.`item_type` = 'DIRECT_PRODUCT' THEN i.`quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1) ELSE 0 END)
+ SUM(CASE WHEN i.`item_type` = 'PACKAGE_ENTITLEMENT' THEN i.`quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1) ELSE 0 END)) DESC,
directSaleAmount DESC, i.`sku_id` ASC
LIMIT #{topN}
</select>
<select id="selectUnitSalesRank" resultType="com.imyeyu.api.modules.gao.vo.GaoProductSkuUnitSalesRankView">
SELECT
i.`sku_id` AS `skuId`,
MAX(i.`sku_code_snapshot`) AS `skuCode`,
MAX(i.`sku_name_snapshot`) AS `skuName`,
MAX(i.`spec_summary_snapshot`) AS `specSummary`,
MAX(i.`product_id_snapshot`) AS `productId`,
MAX(i.`product_name_snapshot`) AS `productName`,
i.`sku_unit_id` AS `skuUnitId`,
MAX(i.`unit_snapshot`) AS `unitName`,
COUNT(DISTINCT o.`id`) AS `orderCount`,
COALESCE(SUM(i.`quantity`), 0) AS `quantity`,
COALESCE(SUM(i.`quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `baseQuantity`,
COALESCE(SUM(i.`line_amount`), 0) AS `saleAmount`
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_snapshot` AND p.`store_id` = o.`store_id`
WHERE i.`item_type` = 'DIRECT_PRODUCT'
<include refid="skuOrderProductFilter"/>
GROUP BY i.`sku_id`, i.`sku_unit_id`
ORDER BY `baseQuantity` DESC, `saleAmount` DESC, i.`sku_id` ASC, i.`sku_unit_id` ASC
LIMIT #{topN}
</select>
<select id="selectFulfillmentDaily" resultType="com.imyeyu.api.modules.gao.vo.GaoProductSkuFulfillmentDailyStatView">
SELECT
CAST(DATE_FORMAT(FROM_UNIXTIME(f.`delivered_at` / 1000), '%Y%m%d') AS UNSIGNED) AS `dateValue`,
fi.`sku_id` AS `skuId`,
MAX(fi.`sku_code_snapshot`) AS `skuCode`,
MAX(fi.`sku_name_snapshot`) AS `skuName`,
MAX(fi.`spec_summary_snapshot`) AS `specSummary`,
MAX(fi.`product_id_snapshot`) AS `productId`,
MAX(fi.`product_name_snapshot`) AS `productName`,
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_snapshot` AND p.`store_id` = f.`store_id`
WHERE f.`store_id` = #{storeId}
AND f.`status` = 'VERIFIED'
AND i.`deleted_at` IS NULL
AND f.`delivered_at` &gt;= #{beginAt}
AND f.`delivered_at` &lt;= #{endAt}
<if test="productId != null and productId != ''">AND fi.`product_id_snapshot` = #{productId}</if>
<if test="skuId != null and skuId != ''">AND fi.`sku_id` = #{skuId}</if>
<if test="categoryId != null and categoryId != ''">AND p.`category_id` = #{categoryId}</if>
<if test="productType != null">AND p.`product_type` = #{productType}</if>
GROUP BY DATE_FORMAT(FROM_UNIXTIME(f.`delivered_at` / 1000), '%Y%m%d'), fi.`sku_id`
ORDER BY `dateValue` ASC, fi.`sku_id` ASC
</select>
<select id="selectFulfillmentRank" resultType="com.imyeyu.api.modules.gao.vo.GaoProductSkuFulfillmentRankView">
SELECT
i.`sku_id` AS `skuId`,
MAX(i.`sku_code_snapshot`) AS `skuCode`,
MAX(i.`sku_name_snapshot`) AS `skuName`,
MAX(i.`spec_summary_snapshot`) AS `specSummary`,
MAX(i.`product_id_snapshot`) AS `productId`,
MAX(i.`product_name_snapshot`) AS `productName`,
COUNT(DISTINCT o.`id`) AS `orderCount`,
COALESCE(SUM(CASE WHEN i.`item_type` = 'DIRECT_PRODUCT' THEN i.`quantity` * 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(i.`reserved_quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `reservedBaseQuantity`,
COALESCE(SUM(i.`fulfilled_quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `fulfilledBaseQuantity`,
COALESCE(SUM((i.`quantity` - i.`reserved_quantity` - i.`fulfilled_quantity`) * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `availableBaseQuantity`
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_snapshot` AND p.`store_id` = o.`store_id`
WHERE 1 = 1
<include refid="skuOrderProductFilter"/>
GROUP BY i.`sku_id`
ORDER BY `availableBaseQuantity` DESC, `fulfilledBaseQuantity` DESC, i.`sku_id` ASC
LIMIT #{topN}
</select>
<select id="selectStockMovementDaily" resultType="com.imyeyu.api.modules.gao.vo.GaoProductSkuStockMovementDailyStatView">
SELECT
CAST(DATE_FORMAT(FROM_UNIXTIME(r.`occurred_at` / 1000), '%Y%m%d') AS UNSIGNED) AS `dateValue`,
r.`sku_id` AS `skuId`,
MAX(r.`sku_code_snapshot`) AS `skuCode`,
MAX(r.`sku_name_snapshot`) AS `skuName`,
MAX(r.`spec_summary_snapshot`) AS `specSummary`,
MAX(r.`product_id_snapshot`) AS `productId`,
MAX(r.`product_name_snapshot`) AS `productName`,
COALESCE(SUM(CASE WHEN r.`on_hand_delta` &gt; 0 THEN r.`on_hand_delta` ELSE 0 END), 0) AS `inQuantity`,
COALESCE(SUM(CASE WHEN r.`on_hand_delta` &lt; 0 THEN -r.`on_hand_delta` ELSE 0 END), 0) AS `outQuantity`,
COALESCE(SUM(r.`on_hand_delta`), 0) AS `netOnHandQuantity`,
COALESCE(SUM(CASE WHEN r.`reserved_delta` &gt; 0 THEN r.`reserved_delta` ELSE 0 END), 0) AS `reserveQuantity`,
COALESCE(SUM(CASE WHEN r.`reserved_delta` &lt; 0 THEN -r.`reserved_delta` ELSE 0 END), 0) AS `releaseQuantity`
FROM `gao_product_stock_record` r
WHERE r.`store_id` = #{storeId}
AND r.`occurred_at` &gt;= #{beginAt}
AND r.`occurred_at` &lt;= #{endAt}
<if test="productId != null and productId != ''">AND r.`product_id_snapshot` = #{productId}</if>
<if test="skuId != null and skuId != ''">AND r.`sku_id` = #{skuId}</if>
GROUP BY DATE_FORMAT(FROM_UNIXTIME(r.`occurred_at` / 1000), '%Y%m%d'), r.`sku_id`
ORDER BY `dateValue` ASC, r.`sku_id` ASC
</select>
<select id="selectStockHealth" resultType="com.imyeyu.api.modules.gao.vo.GaoProductSkuStockHealthView">
SELECT
k.`id` AS `skuId`,
k.`code` AS `skuCode`,
k.`name` AS `skuName`,
k.`spec_summary` AS `specSummary`,
p.`id` AS `productId`,
p.`name` AS `productName`,
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` - s.`reserved_quantity`, 0) AS `availableQuantity`,
k.`min_stock_quantity` AS `minStockQuantity`,
CASE WHEN k.`min_stock_quantity` IS NOT NULL
AND COALESCE(s.`on_hand_quantity`, 0) - COALESCE(s.`reserved_quantity`, 0) &lt; k.`min_stock_quantity`
THEN 1 ELSE 0 END AS `lowStock`
FROM `gao_product_sku` k
INNER JOIN `gao_product` p ON p.`id` = k.`product_id` AND p.`store_id` = k.`store_id` AND p.`deleted_at` IS NULL
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.`sku_id` = k.`id` AND s.`store_id` = k.`store_id`
WHERE k.`store_id` = #{storeId}
AND k.`deleted_at` IS NULL
AND k.`stock_enabled` = 1
<if test="productId != null and productId != ''">AND k.`product_id` = #{productId}</if>
<if test="skuId != null and skuId != ''">AND k.`id` = #{skuId}</if>
<if test="categoryId != null and categoryId != ''">AND p.`category_id` = #{categoryId}</if>
<if test="productType != null">AND p.`product_type` = #{productType}</if>
ORDER BY `lowStock` DESC, `availableQuantity` ASC, p.`sort` ASC, k.`sort` ASC, k.`id` ASC
LIMIT #{topN}
</select>
</mapper>
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.imyeyu.api.modules.gao.mapper.GaoProductSkuMapper">
<select id="selectByProductId" resultType="com.imyeyu.api.modules.gao.entity.GaoProductSku">
SELECT * FROM `gao_product_sku` WHERE `product_id` = #{productId} AND `deleted_at` IS NULL ORDER BY `sort`, `created_at`
</select>
<select id="selectByProductIdList" resultType="com.imyeyu.api.modules.gao.entity.GaoProductSku">
SELECT * FROM `gao_product_sku` WHERE `deleted_at` IS NULL
<choose><when test="productIdList != null and !productIdList.isEmpty()">AND `product_id` IN <foreach collection="productIdList" item="id" open="(" separator="," close=")">#{id}</foreach></when><otherwise>AND FALSE</otherwise></choose>
ORDER BY `product_id`, `sort`, `created_at`
</select>
<select id="selectByStoreIdAndCode" resultType="com.imyeyu.api.modules.gao.entity.GaoProductSku">
SELECT * FROM `gao_product_sku` WHERE `store_id` = #{storeId} AND `code` = #{code} AND `deleted_at` IS NULL <if test="excludeId != null">AND `id` != #{excludeId}</if> LIMIT 1
</select>
<select id="selectByStoreIdAndBarcode" resultType="com.imyeyu.api.modules.gao.entity.GaoProductSku">
SELECT * FROM `gao_product_sku` WHERE `store_id` = #{storeId} AND `barcode` = #{barcode} AND `deleted_at` IS NULL <if test="excludeId != null">AND `id` != #{excludeId}</if> LIMIT 1
</select>
<select id="selectByProductIdAndSpecValueKey" resultType="com.imyeyu.api.modules.gao.entity.GaoProductSku">
SELECT * FROM `gao_product_sku` WHERE `product_id` = #{productId} AND `spec_value_key` = #{specValueKey} AND `deleted_at` IS NULL <if test="excludeId != null">AND `id` != #{excludeId}</if> LIMIT 1
</select>
<select id="selectForUpdate" resultType="com.imyeyu.api.modules.gao.entity.GaoProductSku">
SELECT * FROM `gao_product_sku` WHERE `id` = #{id} AND `deleted_at` IS NULL FOR UPDATE
</select>
</mapper>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.imyeyu.api.modules.gao.mapper.GaoProductSkuSpecValueMapper">
<select id="selectBySkuIdList" resultType="com.imyeyu.api.modules.gao.entity.GaoProductSkuSpecValue">SELECT * FROM `gao_product_sku_spec_value` WHERE 1 = 1 <choose><when test="skuIdList != null and !skuIdList.isEmpty()">AND `sku_id` IN <foreach collection="skuIdList" item="id" open="(" separator="," close=")">#{id}</foreach></when><otherwise>AND FALSE</otherwise></choose></select>
<delete id="deleteBySkuId">DELETE FROM `gao_product_sku_spec_value` WHERE `sku_id`=#{skuId}</delete>
</mapper>
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.imyeyu.api.modules.gao.mapper.GaoProductSkuUnitMapper">
<select id="selectBySkuId" resultType="com.imyeyu.api.modules.gao.entity.GaoProductSkuUnit">
SELECT * FROM `gao_product_sku_unit` WHERE `sku_id` = #{skuId} AND `deleted_at` IS NULL ORDER BY `sort`, `created_at`
</select>
<select id="selectBySkuIdList" resultType="com.imyeyu.api.modules.gao.entity.GaoProductSkuUnit">
SELECT * FROM `gao_product_sku_unit` WHERE `deleted_at` IS NULL
<choose><when test="skuIdList != null and !skuIdList.isEmpty()">AND `sku_id` IN <foreach collection="skuIdList" item="id" open="(" separator="," close=")">#{id}</foreach></when><otherwise>AND FALSE</otherwise></choose>
ORDER BY `sku_id`, `sort`, `created_at`
</select>
<select id="selectForUpdate" resultType="com.imyeyu.api.modules.gao.entity.GaoProductSkuUnit">
SELECT * FROM `gao_product_sku_unit` WHERE `id` = #{id} AND `deleted_at` IS NULL FOR UPDATE
</select>
<update id="updateUnit">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</update>
</mapper>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.imyeyu.api.modules.gao.mapper.GaoProductSpecMapper">
<select id="selectByProductId" resultType="com.imyeyu.api.modules.gao.entity.GaoProductSpec">SELECT * FROM `gao_product_spec` WHERE `product_id`=#{productId} AND `deleted_at` IS NULL ORDER BY `sort`, `created_at`</select>
<select id="selectByProductIdList" resultType="com.imyeyu.api.modules.gao.entity.GaoProductSpec">SELECT * FROM `gao_product_spec` WHERE `deleted_at` IS NULL <choose><when test="productIdList != null and !productIdList.isEmpty()">AND `product_id` IN <foreach collection="productIdList" item="id" open="(" separator="," close=")">#{id}</foreach></when><otherwise>AND FALSE</otherwise></choose> ORDER BY `product_id`, `sort`, `created_at`</select>
<select id="selectProductIdBySpecValueId" resultType="string">SELECT `s`.`product_id` FROM `gao_product_spec_value` `v` INNER JOIN `gao_product_spec` `s` ON `s`.`id`=`v`.`spec_id` AND `s`.`deleted_at` IS NULL WHERE `v`.`id`=#{specValueId} AND `v`.`deleted_at` IS NULL</select>
<select id="selectProductIdBySpecId" resultType="string">SELECT `product_id` FROM `gao_product_spec` WHERE `id`=#{specId} AND `deleted_at` IS NULL</select>
</mapper>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.imyeyu.api.modules.gao.mapper.GaoProductSpecValueMapper">
<select id="selectById" resultType="com.imyeyu.api.modules.gao.entity.GaoProductSpecValue">SELECT * FROM `gao_product_spec_value` WHERE `id` = #{id} AND `deleted_at` IS NULL</select>
<select id="selectBySpecIdList" resultType="com.imyeyu.api.modules.gao.entity.GaoProductSpecValue">SELECT * FROM `gao_product_spec_value` WHERE `deleted_at` IS NULL <choose><when test="specIdList != null and !specIdList.isEmpty()">AND `spec_id` IN <foreach collection="specIdList" item="id" open="(" separator="," close=")">#{id}</foreach></when><otherwise>AND FALSE</otherwise></choose> ORDER BY `spec_id`, `sort`, `created_at`</select>
</mapper>
@@ -3,55 +3,92 @@
<mapper namespace="com.imyeyu.api.modules.gao.mapper.GaoProductStockMapper"> <mapper namespace="com.imyeyu.api.modules.gao.mapper.GaoProductStockMapper">
<select id="selectForUpdate" resultType="com.imyeyu.api.modules.gao.entity.GaoProductStock"> <select id="selectForUpdate" resultType="com.imyeyu.api.modules.gao.entity.GaoProductStock">
SELECT * FROM `gao_product_stock` SELECT * FROM `gao_product_stock`
WHERE `store_id` = #{storeId} AND `product_id` = #{productId} WHERE `store_id` = #{storeId} AND `sku_id` = #{skuId}
FOR UPDATE FOR UPDATE
</select> </select>
<select id="selectByStoreIdAndProductId" resultType="com.imyeyu.api.modules.gao.entity.GaoProductStock"> <select id="selectByStoreIdAndSkuId" resultType="com.imyeyu.api.modules.gao.entity.GaoProductStock">
SELECT * FROM `gao_product_stock` SELECT * FROM `gao_product_stock`
WHERE `store_id` = #{storeId} AND `product_id` = #{productId} WHERE `store_id` = #{storeId} AND `sku_id` = #{skuId}
</select> </select>
<select id="countByQuery" resultType="long"> <select id="countByQuery" resultType="long">
SELECT COUNT(1) SELECT COUNT(1)
FROM `gao_product` p FROM `gao_product_sku` k
LEFT JOIN `gao_product_stock` s ON s.`product_id` = p.`id` JOIN `gao_product` p ON p.`id` = k.`product_id`
LEFT JOIN `gao_product_stock` s ON s.`sku_id` = k.`id`
<choose> <choose>
<when test="storeId != null and storeId != ''">AND s.`store_id` = #{storeId}</when> <when test="storeId != null and storeId != ''">AND s.`store_id` = #{storeId}</when>
<otherwise>AND s.`store_id` = p.`store_id`</otherwise> <otherwise>AND s.`store_id` = k.`store_id`</otherwise>
</choose> </choose>
WHERE p.`deleted_at` IS NULL WHERE p.`deleted_at` IS NULL AND k.`deleted_at` IS NULL
AND p.`stock_enabled` = 1 AND k.`stock_enabled` = 1 AND k.`status` = 'ACTIVE'
<if test="storeId != null and storeId != ''">AND p.`store_id` = #{storeId}</if> <if test="storeId != null and storeId != ''">AND p.`store_id` = #{storeId}</if>
<if test="productId != null and productId != ''">AND p.`id` = #{productId}</if> <if test="productId != null and productId != ''">AND k.`product_id` = #{productId}</if>
<if test="keyword != null and keyword.trim() != ''">AND (p.`code` LIKE CONCAT('%', #{keyword}, '%') OR p.`name` LIKE CONCAT('%', #{keyword}, '%'))</if> <if test="keyword != null and keyword.trim() != ''">AND (p.`code` LIKE CONCAT('%', #{keyword}, '%') OR p.`name` LIKE CONCAT('%', #{keyword}, '%') OR k.`code` LIKE CONCAT('%', #{keyword}, '%') OR k.`name` LIKE CONCAT('%', #{keyword}, '%') OR k.`spec_summary` LIKE CONCAT('%', #{keyword}, '%') OR k.`barcode` LIKE CONCAT('%', #{keyword}, '%'))</if>
</select> </select>
<select id="selectByQuery" resultType="com.imyeyu.api.modules.gao.entity.GaoProductStock"> <select id="selectByQuery" resultType="com.imyeyu.api.modules.gao.entity.GaoProductStock">
SELECT s.`id`, SELECT s.`id`,
p.`store_id`, k.`store_id`,
p.`id` AS `product_id`, k.`id` AS `sku_id`,
COALESCE(s.`on_hand_quantity`, 0) AS `on_hand_quantity`, COALESCE(s.`on_hand_quantity`, 0) AS `on_hand_quantity`,
COALESCE(s.`reserved_quantity`, 0) AS `reserved_quantity`, COALESCE(s.`reserved_quantity`, 0) AS `reserved_quantity`,
s.`updated_by`, s.`updated_by`,
s.`created_at`, s.`created_at`,
s.`updated_at` s.`updated_at`
FROM `gao_product` p FROM `gao_product_sku` k
LEFT JOIN `gao_product_stock` s ON s.`product_id` = p.`id` JOIN `gao_product` p ON p.`id` = k.`product_id`
LEFT JOIN `gao_product_stock` s ON s.`sku_id` = k.`id`
<choose> <choose>
<when test="storeId != null and storeId != ''">AND s.`store_id` = #{storeId}</when> <when test="storeId != null and storeId != ''">AND s.`store_id` = #{storeId}</when>
<otherwise>AND s.`store_id` = p.`store_id`</otherwise> <otherwise>AND s.`store_id` = k.`store_id`</otherwise>
</choose> </choose>
WHERE p.`deleted_at` IS NULL WHERE p.`deleted_at` IS NULL AND k.`deleted_at` IS NULL
AND k.`stock_enabled` = 1 AND k.`status` = 'ACTIVE'
<if test="storeId != null and storeId != ''">AND p.`store_id` = #{storeId}</if>
<if test="productId != null and productId != ''">AND k.`product_id` = #{productId}</if>
<if test="keyword != null and keyword.trim() != ''">AND (p.`code` LIKE CONCAT('%', #{keyword}, '%') OR p.`name` LIKE CONCAT('%', #{keyword}, '%') OR k.`code` LIKE CONCAT('%', #{keyword}, '%') OR k.`name` LIKE CONCAT('%', #{keyword}, '%') OR k.`spec_summary` LIKE CONCAT('%', #{keyword}, '%') OR k.`barcode` LIKE CONCAT('%', #{keyword}, '%'))</if>
ORDER BY p.`sort`, p.`name`, p.`code`, k.`sort`, k.`name`, k.`code`
LIMIT #{offset}, #{limit}
</select>
<select id="countProductByQuery" resultType="long">
SELECT COUNT(DISTINCT p.`id`)
FROM `gao_product_sku` k
JOIN `gao_product` p ON p.`id` = k.`product_id`
WHERE p.`deleted_at` IS NULL AND k.`deleted_at` IS NULL
AND p.`stock_enabled` = 1 AND p.`stock_enabled` = 1
AND k.`stock_enabled` = 1 AND k.`status` = 'ACTIVE'
<if test="storeId != null and storeId != ''">AND p.`store_id` = #{storeId}</if> <if test="storeId != null and storeId != ''">AND p.`store_id` = #{storeId}</if>
<if test="productId != null and productId != ''">AND p.`id` = #{productId}</if>
<if test="keyword != null and keyword.trim() != ''">AND (p.`code` LIKE CONCAT('%', #{keyword}, '%') OR p.`name` LIKE CONCAT('%', #{keyword}, '%'))</if> <if test="keyword != null and keyword.trim() != ''">AND (p.`code` LIKE CONCAT('%', #{keyword}, '%') OR p.`name` LIKE CONCAT('%', #{keyword}, '%'))</if>
</select>
<select id="selectProductByQuery" resultType="com.imyeyu.api.modules.gao.vo.GaoProductStockProductView">
SELECT p.`id` AS `product_id`,
p.`code` AS `product_code`,
p.`name` AS `product_name`,
p.`product_type`,
COUNT(k.`id`) AS `sku_count`,
COALESCE(SUM(COALESCE(s.`on_hand_quantity`, 0)), 0) AS `on_hand_quantity`,
COALESCE(SUM(COALESCE(s.`reserved_quantity`, 0)), 0) AS `reserved_quantity`,
COALESCE(SUM(COALESCE(s.`on_hand_quantity`, 0) - COALESCE(s.`reserved_quantity`, 0)), 0) AS `available_quantity`
FROM `gao_product_sku` k
JOIN `gao_product` p ON p.`id` = k.`product_id`
LEFT JOIN `gao_product_stock` s ON s.`sku_id` = k.`id`
<choose>
<when test="storeId != null and storeId != ''">AND s.`store_id` = #{storeId}</when>
<otherwise>AND s.`store_id` = k.`store_id`</otherwise>
</choose>
WHERE p.`deleted_at` IS NULL AND k.`deleted_at` IS NULL
AND p.`stock_enabled` = 1
AND k.`stock_enabled` = 1 AND k.`status` = 'ACTIVE'
<if test="storeId != null and storeId != ''">AND p.`store_id` = #{storeId}</if>
<if test="keyword != null and keyword.trim() != ''">AND (p.`code` LIKE CONCAT('%', #{keyword}, '%') OR p.`name` LIKE CONCAT('%', #{keyword}, '%'))</if>
GROUP BY p.`id`, p.`code`, p.`name`, p.`product_type`, p.`sort`
ORDER BY p.`sort`, p.`name`, p.`code` ORDER BY p.`sort`, p.`name`, p.`code`
LIMIT #{offset}, #{limit} LIMIT #{offset}, #{limit}
</select> </select>
<insert id="insertIgnore"> <insert id="insertIgnore">
INSERT IGNORE INTO `gao_product_stock` 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 VALUES
(#{id}, #{storeId}, #{productId}, #{onHandQuantity}, #{reservedQuantity}, #{updatedBy}, #{createdAt}) (#{id}, #{storeId}, #{skuId}, #{onHandQuantity}, #{reservedQuantity}, #{updatedBy}, #{createdAt})
</insert> </insert>
<update id="updateQuantity"> <update id="updateQuantity">
UPDATE `gao_product_stock` UPDATE `gao_product_stock`
@@ -4,7 +4,7 @@
<select id="selectByRequestNo" resultType="com.imyeyu.api.modules.gao.entity.GaoProductStockRecord"> <select id="selectByRequestNo" resultType="com.imyeyu.api.modules.gao.entity.GaoProductStockRecord">
SELECT * FROM `gao_product_stock_record` SELECT * FROM `gao_product_stock_record`
WHERE `store_id` = #{storeId} AND `request_no` = #{requestNo} WHERE `store_id` = #{storeId} AND `request_no` = #{requestNo}
ORDER BY `product_id`, `id` ORDER BY `sku_id`, `id`
</select> </select>
<select id="selectForUpdate" resultType="com.imyeyu.api.modules.gao.entity.GaoProductStockRecord"> <select id="selectForUpdate" resultType="com.imyeyu.api.modules.gao.entity.GaoProductStockRecord">
SELECT * FROM `gao_product_stock_record` SELECT * FROM `gao_product_stock_record`
@@ -21,16 +21,16 @@
WHERE `biz_type` = #{bizType} AND `biz_id` = #{bizId} WHERE `biz_type` = #{bizType} AND `biz_id` = #{bizId}
ORDER BY `occurred_at`, `id` ORDER BY `occurred_at`, `id`
</select> </select>
<select id="selectByBizProduct" resultType="com.imyeyu.api.modules.gao.entity.GaoProductStockRecord"> <select id="selectByBizSku" resultType="com.imyeyu.api.modules.gao.entity.GaoProductStockRecord">
SELECT * FROM `gao_product_stock_record` 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` ORDER BY `occurred_at`, `id`
LIMIT 1 LIMIT 1
</select> </select>
<sql id="queryWhere"> <sql id="queryWhere">
1 = 1 1 = 1
<if test="storeId != null and storeId != ''">AND r.`store_id` = #{storeId}</if> <if test="storeId != null and storeId != ''">AND r.`store_id` = #{storeId}</if>
<if test="productId != null and productId != ''">AND r.`product_id` = #{productId}</if> <if test="productId != null and productId != ''">AND k.`product_id` = #{productId}</if>
<if test="keyword != null and keyword.trim() != ''">AND (r.`product_name_snapshot` LIKE CONCAT('%', #{keyword}, '%') OR p.`name` LIKE CONCAT('%', #{keyword}, '%') OR p.`code` LIKE CONCAT('%', #{keyword}, '%'))</if> <if test="keyword != null and keyword.trim() != ''">AND (r.`product_name_snapshot` LIKE CONCAT('%', #{keyword}, '%') OR p.`name` LIKE CONCAT('%', #{keyword}, '%') OR p.`code` LIKE CONCAT('%', #{keyword}, '%'))</if>
<if test="bizType != null">AND r.`biz_type` = #{bizType}</if> <if test="bizType != null">AND r.`biz_type` = #{bizType}</if>
<if test="bizId != null and bizId != ''">AND r.`biz_id` = #{bizId}</if> <if test="bizId != null and bizId != ''">AND r.`biz_id` = #{bizId}</if>
@@ -43,7 +43,7 @@
WHERE c.`store_id` = r.`store_id` WHERE c.`store_id` = r.`store_id`
AND c.`biz_type` = r.`biz_type` AND c.`biz_type` = r.`biz_type`
AND c.`biz_id` = r.`biz_id` 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') AND c.`operation_type` IN ('ORDER_RELEASE', 'ORDER_OUT')
) )
</if> </if>
@@ -53,10 +53,10 @@
<if test="endAt != null">AND r.`occurred_at` &lt;= #{endAt}</if> <if test="endAt != null">AND r.`occurred_at` &lt;= #{endAt}</if>
</sql> </sql>
<select id="countByQuery" resultType="long"> <select id="countByQuery" resultType="long">
SELECT COUNT(1) FROM `gao_product_stock_record` r LEFT JOIN `gao_product` p ON p.`id` = r.`product_id` AND p.`store_id` = r.`store_id` WHERE <include refid="queryWhere"/> SELECT COUNT(1) FROM `gao_product_stock_record` r LEFT JOIN `gao_product_sku` k ON k.`id` = r.`sku_id` AND k.`store_id` = r.`store_id` LEFT JOIN `gao_product` p ON p.`id` = k.`product_id` WHERE <include refid="queryWhere"/>
</select> </select>
<select id="selectByQuery" resultType="com.imyeyu.api.modules.gao.entity.GaoProductStockRecord"> <select id="selectByQuery" resultType="com.imyeyu.api.modules.gao.entity.GaoProductStockRecord">
SELECT r.* FROM `gao_product_stock_record` r LEFT JOIN `gao_product` p ON p.`id` = r.`product_id` AND p.`store_id` = r.`store_id` WHERE <include refid="queryWhere"/> SELECT r.* FROM `gao_product_stock_record` r LEFT JOIN `gao_product_sku` k ON k.`id` = r.`sku_id` AND k.`store_id` = r.`store_id` LEFT JOIN `gao_product` p ON p.`id` = k.`product_id` WHERE <include refid="queryWhere"/>
ORDER BY r.`occurred_at` DESC, r.`id` DESC ORDER BY r.`occurred_at` DESC, r.`id` DESC
LIMIT #{offset}, #{limit} LIMIT #{offset}, #{limit}
</select> </select>
@@ -1,46 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.imyeyu.api.modules.gao.mapper.GaoProductUnitMapper">
<resultMap id="unitResultMap" type="com.imyeyu.api.modules.gao.entity.GaoProductUnit">
<id column="id" property="id"/>
<result column="product_id" property="productId"/>
<result column="name" property="name"/>
<result column="base_quantity_per_unit" property="baseQuantityPerUnit"/>
<result column="sale_price" property="salePrice"/>
<result column="is_base" property="isBase"/>
<result column="allow_purchase" property="allowPurchase"/>
<result column="allow_sale" property="allowSale"/>
<result column="sort" property="sort"/>
<result column="created_at" property="createdAt"/>
<result column="updated_at" property="updatedAt"/>
<result column="deleted_at" property="deletedAt"/>
</resultMap>
<select id="selectByProductId" resultMap="unitResultMap">
SELECT * FROM `gao_product_unit`
WHERE `product_id` = #{productId} AND `deleted_at` IS NULL
ORDER BY `sort`, `created_at`
</select>
<select id="selectByProductIdList" resultMap="unitResultMap">
SELECT * FROM `gao_product_unit`
WHERE `deleted_at` IS NULL
<choose>
<when test="productIdList != null and !productIdList.isEmpty()">
AND `product_id` IN <foreach collection="productIdList" item="id" open="(" separator="," close=")">#{id}</foreach>
</when>
<otherwise>AND FALSE</otherwise>
</choose>
ORDER BY `product_id`, `sort`, `created_at`
</select>
<select id="selectForUpdate" resultMap="unitResultMap">
SELECT * FROM `gao_product_unit`
WHERE `id` = #{id} AND `deleted_at` IS NULL
FOR UPDATE
</select>
<update id="updateUnit">
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
</update>
</mapper>
@@ -81,7 +81,7 @@
<select id="selectCustomerProductRank" resultType="com.imyeyu.api.modules.gao.vo.GaoCustomerOrderProductRankView"> <select id="selectCustomerProductRank" resultType="com.imyeyu.api.modules.gao.vo.GaoCustomerOrderProductRankView">
SELECT SELECT
i.`product_id` AS `productId`, i.`product_id_snapshot` AS `productId`,
MAX(i.`product_name_snapshot`) AS `productName`, MAX(i.`product_name_snapshot`) AS `productName`,
COUNT(DISTINCT o.`id`) AS `orderCount`, 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` = 'DIRECT_PRODUCT' THEN i.`quantity` ELSE 0 END), 0) AS `directQuantity`,
@@ -89,10 +89,15 @@
COALESCE(SUM(i.`reserved_quantity`), 0) AS `reservedQuantity`, COALESCE(SUM(i.`reserved_quantity`), 0) AS `reservedQuantity`,
COALESCE(SUM(i.`fulfilled_quantity`), 0) AS `fulfilledQuantity`, COALESCE(SUM(i.`fulfilled_quantity`), 0) AS `fulfilledQuantity`,
COALESCE(SUM(i.`quantity` - i.`reserved_quantity` - i.`fulfilled_quantity`), 0) AS `availableQuantity`, COALESCE(SUM(i.`quantity` - i.`reserved_quantity` - i.`fulfilled_quantity`), 0) AS `availableQuantity`,
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(i.`reserved_quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `reservedBaseQuantity`,
COALESCE(SUM(i.`fulfilled_quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `fulfilledBaseQuantity`,
COALESCE(SUM((i.`quantity` - i.`reserved_quantity` - i.`fulfilled_quantity`) * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `availableBaseQuantity`,
COALESCE(SUM(CASE WHEN i.`item_type` = 'DIRECT_PRODUCT' THEN COALESCE(i.`line_amount`, 0) ELSE 0 END), 0) AS `directSaleAmount` 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 FROM `gao_sales_order_item` i
INNER JOIN `gao_sales_order` o ON o.`id` = i.`sales_order_id` 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 o.`store_id` = #{storeId} WHERE o.`store_id` = #{storeId}
AND o.`customer_id` = #{customerId} AND o.`customer_id` = #{customerId}
AND o.`deleted_at` IS NULL AND o.`deleted_at` IS NULL
@@ -100,11 +105,45 @@
AND o.`status` &lt;&gt; 'CANCELLED' AND o.`status` &lt;&gt; 'CANCELLED'
AND o.`purchased_at` &gt;= #{beginAt} AND o.`purchased_at` &gt;= #{beginAt}
AND o.`purchased_at` &lt;= #{endAt} AND o.`purchased_at` &lt;= #{endAt}
GROUP BY i.`product_id` <if test="productId != null and productId != ''">AND i.`product_id_snapshot` = #{productId}</if>
ORDER BY (SUM(CASE WHEN i.`item_type` = 'DIRECT_PRODUCT' THEN i.`quantity` ELSE 0 END) <if test="skuId != null and skuId != ''">AND i.`sku_id` = #{skuId}</if>
+ SUM(CASE WHEN i.`item_type` = 'PACKAGE_ENTITLEMENT' THEN i.`quantity` ELSE 0 END)) DESC, GROUP BY i.`product_id_snapshot`
ORDER BY (SUM(CASE WHEN i.`item_type` = 'DIRECT_PRODUCT' THEN i.`quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1) ELSE 0 END)
+ SUM(CASE WHEN i.`item_type` = 'PACKAGE_ENTITLEMENT' THEN i.`quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1) ELSE 0 END)) DESC,
directSaleAmount DESC, directSaleAmount DESC,
i.`product_id` ASC i.`product_id_snapshot` ASC
LIMIT #{topN}
</select>
<select id="selectCustomerSkuRank" resultType="com.imyeyu.api.modules.gao.vo.GaoCustomerOrderSkuRankView">
SELECT
i.`sku_id` AS `skuId`,
MAX(i.`sku_code_snapshot`) AS `skuCode`,
MAX(i.`sku_name_snapshot`) AS `skuName`,
MAX(i.`spec_summary_snapshot`) AS `specSummary`,
i.`product_id_snapshot` AS `productId`,
MAX(i.`product_name_snapshot`) AS `productName`,
COUNT(DISTINCT o.`id`) AS `orderCount`,
COALESCE(SUM(CASE WHEN i.`item_type` = 'DIRECT_PRODUCT' THEN i.`quantity` * 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(i.`reserved_quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `reservedBaseQuantity`,
COALESCE(SUM(i.`fulfilled_quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `fulfilledBaseQuantity`,
COALESCE(SUM((i.`quantity` - i.`reserved_quantity` - i.`fulfilled_quantity`) * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `availableBaseQuantity`,
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_snapshot` AND p.`store_id` = o.`store_id`
WHERE o.`store_id` = #{storeId}
AND o.`customer_id` = #{customerId}
AND o.`deleted_at` IS NULL
AND i.`deleted_at` IS NULL
AND o.`status` &lt;&gt; 'CANCELLED'
AND o.`purchased_at` &gt;= #{beginAt}
AND o.`purchased_at` &lt;= #{endAt}
<if test="productId != null and productId != ''">AND i.`product_id_snapshot` = #{productId}</if>
<if test="skuId != null and skuId != ''">AND i.`sku_id` = #{skuId}</if>
GROUP BY i.`sku_id`, i.`product_id_snapshot`
ORDER BY `availableBaseQuantity` DESC, `fulfilledBaseQuantity` DESC, `directSaleAmount` DESC, i.`sku_id` ASC
LIMIT #{topN} LIMIT #{topN}
</select> </select>