v1.0.36 #48
@@ -29,7 +29,7 @@ public class GaoOrderFulfillmentItem implements IDEntity<String>, Creatable {
|
|||||||
/// SKU ID
|
/// SKU ID
|
||||||
private String skuId;
|
private String skuId;
|
||||||
|
|
||||||
/// SKU 单位 ID
|
/// 本次实际履约 SKU 单位 ID
|
||||||
private String skuUnitId;
|
private String skuUnitId;
|
||||||
|
|
||||||
/// 产品图片附件
|
/// 产品图片附件
|
||||||
@@ -45,17 +45,17 @@ public class GaoOrderFulfillmentItem implements IDEntity<String>, Creatable {
|
|||||||
/// 基础单位数量
|
/// 基础单位数量
|
||||||
private BigDecimal baseQuantity;
|
private BigDecimal baseQuantity;
|
||||||
|
|
||||||
/// 操作前待履约数量
|
/// 操作前待履约基础单位数量
|
||||||
private BigDecimal reservedBefore;
|
private BigDecimal reservedBaseBefore;
|
||||||
|
|
||||||
/// 操作后待履约数量
|
/// 操作后待履约基础单位数量
|
||||||
private BigDecimal reservedAfter;
|
private BigDecimal reservedBaseAfter;
|
||||||
|
|
||||||
/// 操作前已履约数量
|
/// 操作前已履约基础单位数量
|
||||||
private BigDecimal fulfilledBefore;
|
private BigDecimal fulfilledBaseBefore;
|
||||||
|
|
||||||
/// 操作后已履约数量
|
/// 操作后已履约基础单位数量
|
||||||
private BigDecimal fulfilledAfter;
|
private BigDecimal fulfilledBaseAfter;
|
||||||
|
|
||||||
/// 产品名称快照
|
/// 产品名称快照
|
||||||
private String productNameSnapshot;
|
private String productNameSnapshot;
|
||||||
|
|||||||
@@ -71,11 +71,14 @@ public class GaoSalesOrderItem extends UUIDEntity {
|
|||||||
/// 购买数量
|
/// 购买数量
|
||||||
private BigDecimal quantity;
|
private BigDecimal quantity;
|
||||||
|
|
||||||
/// 待履约数量
|
/// 购买基础单位数量
|
||||||
private BigDecimal reservedQuantity;
|
private BigDecimal baseQuantity;
|
||||||
|
|
||||||
/// 已履约数量
|
/// 待履约基础单位数量
|
||||||
private BigDecimal fulfilledQuantity;
|
private BigDecimal reservedBaseQuantity;
|
||||||
|
|
||||||
|
/// 已履约基础单位数量
|
||||||
|
private BigDecimal fulfilledBaseQuantity;
|
||||||
|
|
||||||
/// 实际单价,单位分
|
/// 实际单价,单位分
|
||||||
private Long unitPrice;
|
private Long unitPrice;
|
||||||
@@ -89,15 +92,15 @@ public class GaoSalesOrderItem extends UUIDEntity {
|
|||||||
/// 备注
|
/// 备注
|
||||||
private String remark;
|
private String remark;
|
||||||
|
|
||||||
/// 待核销数量
|
/// 待核销基础单位数量
|
||||||
@Transient
|
@Transient
|
||||||
private BigDecimal pendingQuantity;
|
private BigDecimal pendingBaseQuantity;
|
||||||
|
|
||||||
/// 当前可履约数量
|
/// 当前可履约基础单位数量
|
||||||
@Transient
|
@Transient
|
||||||
private BigDecimal availableQuantity;
|
private BigDecimal availableBaseQuantity;
|
||||||
|
|
||||||
/// 权益剩余数量
|
/// 权益剩余基础单位数量
|
||||||
@Transient
|
@Transient
|
||||||
private BigDecimal remainingQuantity;
|
private BigDecimal remainingBaseQuantity;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ public interface GaoSalesOrderItemMapper extends BaseMapper<GaoSalesOrderItem, S
|
|||||||
/// 锁定订单明细
|
/// 锁定订单明细
|
||||||
GaoSalesOrderItem selectForUpdate(@Param("id") String id);
|
GaoSalesOrderItem selectForUpdate(@Param("id") String id);
|
||||||
|
|
||||||
/// 原子调整待履约数量
|
/// 原子调整待履约基础单位数量
|
||||||
int updateReservedQuantity(@Param("id") String id, @Param("delta") BigDecimal delta);
|
int updateReservedBaseQuantity(@Param("id") String id, @Param("delta") BigDecimal delta);
|
||||||
|
|
||||||
/// 原子调整已履约数量
|
/// 原子调整已履约基础单位数量
|
||||||
int updateFulfilledQuantity(@Param("id") String id, @Param("delta") BigDecimal delta);
|
int updateFulfilledBaseQuantity(@Param("id") String id, @Param("delta") BigDecimal delta);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,11 +58,11 @@ public interface GaoSalesOrderService extends BaseService<GaoSalesOrder, String>
|
|||||||
/// 检查履约权限
|
/// 检查履约权限
|
||||||
void checkFulfillmentPermission(GaoSalesOrder order);
|
void checkFulfillmentPermission(GaoSalesOrder order);
|
||||||
|
|
||||||
/// 原子调整订单项待履约数量
|
/// 原子调整订单项待履约基础单位数量
|
||||||
int updateItemReservedQuantity(String itemId, BigDecimal delta);
|
int updateItemReservedBaseQuantity(String itemId, BigDecimal delta);
|
||||||
|
|
||||||
/// 原子调整订单项已履约数量
|
/// 原子调整订单项已履约基础单位数量
|
||||||
int updateItemFulfilledQuantity(String itemId, BigDecimal delta);
|
int updateItemFulfilledBaseQuantity(String itemId, BigDecimal delta);
|
||||||
|
|
||||||
/// 锁定订单明细
|
/// 锁定订单明细
|
||||||
GaoSalesOrderItem getItemForUpdate(String itemId);
|
GaoSalesOrderItem getItemForUpdate(String itemId);
|
||||||
|
|||||||
+76
-39
@@ -22,6 +22,7 @@ import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePage;
|
|||||||
import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePurchaseRequest;
|
import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePurchaseRequest;
|
||||||
import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePurchaseItem;
|
import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePurchaseItem;
|
||||||
import com.imyeyu.api.modules.gao.vo.GaoPackageOrderCreateData;
|
import com.imyeyu.api.modules.gao.vo.GaoPackageOrderCreateData;
|
||||||
|
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.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;
|
||||||
@@ -38,6 +39,10 @@ import org.springframework.stereotype.Service;
|
|||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -53,6 +58,8 @@ import java.util.stream.Collectors;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class GaoCustomerPackageServiceImplement extends AbstractEntityService<GaoCustomerPackage, String> implements GaoCustomerPackageService {
|
public class GaoCustomerPackageServiceImplement extends AbstractEntityService<GaoCustomerPackage, String> implements GaoCustomerPackageService {
|
||||||
|
|
||||||
|
private static final BigDecimal ZERO = BigDecimal.ZERO;
|
||||||
|
|
||||||
private final GaoCustomerPackageMapper mapper;
|
private final GaoCustomerPackageMapper mapper;
|
||||||
private final GaoPackageConfigService packageConfigService;
|
private final GaoPackageConfigService packageConfigService;
|
||||||
private final GaoProductSkuService skuService;
|
private final GaoProductSkuService skuService;
|
||||||
@@ -143,9 +150,13 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
|
|||||||
if (request.getItemList() == null) {
|
if (request.getItemList() == null) {
|
||||||
request.setItemList(defaultPurchaseItems(configItemList));
|
request.setItemList(defaultPurchaseItems(configItemList));
|
||||||
}
|
}
|
||||||
List<GaoCustomerPackagePurchaseItem> itemList = new java.util.ArrayList<>(request.getItemList());
|
List<GaoCustomerPackagePurchaseItem> requestItemList = new ArrayList<>(request.getItemList());
|
||||||
verifyPurchaseItems(itemList);
|
verifyPurchaseItems(requestItemList);
|
||||||
itemList.sort(java.util.Comparator.comparing(GaoCustomerPackagePurchaseItem::getSort));
|
boolean canOverridePriceAndQuantity = permissionChecker.hasAny(ModuleCode.GAO,
|
||||||
|
com.imyeyu.api.modules.gao.bean.GaoPermissionCode.CUSTOMER_PACKAGE_PRICE_OVERRIDE.getValue());
|
||||||
|
List<GaoCustomerPackagePurchaseItem> itemList = resolvePurchaseItems(config.getStoreId(), requestItemList, configItemList, canOverridePriceAndQuantity);
|
||||||
|
itemList.sort(Comparator.comparing(GaoCustomerPackagePurchaseItem::getSort).thenComparing(GaoCustomerPackagePurchaseItem::getSkuId)
|
||||||
|
.thenComparing(GaoCustomerPackagePurchaseItem::getSkuUnitId));
|
||||||
request.setItemList(itemList);
|
request.setItemList(itemList);
|
||||||
|
|
||||||
GaoSalesOrder existingOrder = salesOrderService.getByStoreIdAndRequestNo(config.getStoreId(), request.getRequestNo());
|
GaoSalesOrder existingOrder = salesOrderService.getByStoreIdAndRequestNo(config.getStoreId(), request.getRequestNo());
|
||||||
@@ -154,19 +165,14 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
|
|||||||
verifyPurchaseIdempotency(existingOrder, existingPackage, request);
|
verifyPurchaseIdempotency(existingOrder, existingPackage, request);
|
||||||
return existingPackage;
|
return existingPackage;
|
||||||
}
|
}
|
||||||
verifyConfigSellableItems(config.getStoreId(), configItemList);
|
|
||||||
|
|
||||||
Long purchasedAt = TimiJava.defaultIfNull(request.getPurchasedAt(), Time.now());
|
Long purchasedAt = TimiJava.defaultIfNull(request.getPurchasedAt(), Time.now());
|
||||||
Long now = Time.now();
|
Long now = Time.now();
|
||||||
TimiException.requiredTrue(config.getStatus() == GaoPackageConfig.Status.ACTIVE, "套餐配置不可购买");
|
TimiException.requiredTrue(config.getStatus() == GaoPackageConfig.Status.ACTIVE, "套餐配置不可购买");
|
||||||
TimiException.requiredTrue(config.getBeginAt() == null || config.getBeginAt() <= now, "套餐配置尚未生效");
|
TimiException.requiredTrue(config.getBeginAt() == null || config.getBeginAt() <= now, "套餐配置尚未生效");
|
||||||
TimiException.requiredTrue(config.getEndAt() == null || now <= config.getEndAt(), "套餐配置已过期");
|
TimiException.requiredTrue(config.getEndAt() == null || now <= config.getEndAt(), "套餐配置已过期");
|
||||||
GaoCustomer customer = validateCustomer(request.getCustomerId(), config.getStoreId());
|
GaoCustomer customer = validateCustomer(request.getCustomerId(), config.getStoreId());
|
||||||
verifySameStructureAsConfig(itemList, configItemList);
|
|
||||||
boolean canOverridePriceAndQuantity = permissionChecker.hasAny(ModuleCode.GAO, com.imyeyu.api.modules.gao.bean.GaoPermissionCode.CUSTOMER_PACKAGE_PRICE_OVERRIDE.getValue());
|
|
||||||
if (!canOverridePriceAndQuantity) {
|
if (!canOverridePriceAndQuantity) {
|
||||||
TimiException.requiredTrue(request.getSaleAmount().equals(config.getStandardAmount()), "普通员工不能修改套餐成交价");
|
TimiException.requiredTrue(request.getSaleAmount().equals(config.getStandardAmount()), "普通员工不能修改套餐成交价");
|
||||||
verifySameQuantityAsConfig(itemList, configItemList);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<GaoSalesOrderItem> orderItemList = itemList.stream().map(this::toOrderItem).toList();
|
List<GaoSalesOrderItem> orderItemList = itemList.stream().map(this::toOrderItem).toList();
|
||||||
@@ -267,8 +273,9 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
|
|||||||
item.setUnitSnapshot(source.getUnitSnapshot());
|
item.setUnitSnapshot(source.getUnitSnapshot());
|
||||||
item.setBaseQuantityPerUnitSnapshot(source.getBaseQuantityPerUnitSnapshot());
|
item.setBaseQuantityPerUnitSnapshot(source.getBaseQuantityPerUnitSnapshot());
|
||||||
item.setQuantity(source.getTotalQuantity());
|
item.setQuantity(source.getTotalQuantity());
|
||||||
item.setReservedQuantity(BigDecimal.ZERO);
|
item.setBaseQuantity(toBaseQuantity(source.getTotalQuantity(), source.getBaseQuantityPerUnitSnapshot(), "套餐权益基础数量最多保留三位小数"));
|
||||||
item.setFulfilledQuantity(BigDecimal.ZERO);
|
item.setReservedBaseQuantity(ZERO);
|
||||||
|
item.setFulfilledBaseQuantity(ZERO);
|
||||||
item.setUnitPrice(null);
|
item.setUnitPrice(null);
|
||||||
item.setLineAmount(null);
|
item.setLineAmount(null);
|
||||||
item.setSort(source.getSort());
|
item.setSort(source.getSort());
|
||||||
@@ -277,7 +284,6 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void verifyPurchaseItems(List<GaoCustomerPackagePurchaseItem> itemList) {
|
private void verifyPurchaseItems(List<GaoCustomerPackagePurchaseItem> itemList) {
|
||||||
TimiException.requiredTrue(!itemList.isEmpty(), "购买套餐至少需要一个产品");
|
|
||||||
Set<String> keySet = new HashSet<>();
|
Set<String> keySet = new HashSet<>();
|
||||||
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);
|
||||||
@@ -285,7 +291,7 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
|
|||||||
TimiException.required(item.getSkuId(), "购买 SKU 不能为空");
|
TimiException.required(item.getSkuId(), "购买 SKU 不能为空");
|
||||||
TimiException.required(item.getSkuUnitId(), "购买 SKU 单位不能为空");
|
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(), "购买产品数量不能为负数");
|
||||||
TimiException.requiredTrue(3 >= item.getTotalQuantity().stripTrailingZeros().scale(), "购买产品数量最多保留三位小数");
|
TimiException.requiredTrue(3 >= item.getTotalQuantity().stripTrailingZeros().scale(), "购买产品数量最多保留三位小数");
|
||||||
String key = item.getSkuId() + "|" + item.getSkuUnitId();
|
String key = item.getSkuId() + "|" + item.getSkuUnitId();
|
||||||
TimiException.requiredTrue(keySet.add(key), "购买套餐内 SKU 和单位不能重复");
|
TimiException.requiredTrue(keySet.add(key), "购买套餐内 SKU 和单位不能重复");
|
||||||
@@ -293,38 +299,69 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void verifyConfigSellableItems(String storeId, List<GaoPackageConfigItem> itemList) {
|
private List<GaoCustomerPackagePurchaseItem> resolvePurchaseItems(String storeId, List<GaoCustomerPackagePurchaseItem> requestItemList,
|
||||||
for (GaoPackageConfigItem item : itemList) {
|
List<GaoPackageConfigItem> configItemList, boolean canOverridePriceAndQuantity) {
|
||||||
TimiException.required(item.getSkuId(), "套餐配置缺少 SKU");
|
Map<String, List<GaoPackageConfigItem>> configItemMap = configItemList.stream()
|
||||||
TimiException.required(item.getSkuUnitId(), "套餐配置缺少 SKU 单位");
|
.collect(Collectors.groupingBy(GaoPackageConfigItem::getSkuId));
|
||||||
// 通过 SKU 服务唯一选品入口重新校验 SKU 和单位仍处于可销售状态
|
Map<String, BigDecimal> configBaseQuantityMap = new HashMap<>();
|
||||||
skuService.getSellableSkuUnit(storeId, item.getSkuId(), item.getSkuUnitId());
|
for (GaoPackageConfigItem configItem : configItemList) {
|
||||||
|
configBaseQuantityMap.merge(configItem.getSkuId(),
|
||||||
|
toBaseQuantity(configItem.getDefaultQuantity(), configItem.getBaseQuantityPerUnitSnapshot(), "套餐配置基础数量最多保留三位小数"), BigDecimal::add);
|
||||||
}
|
}
|
||||||
|
Map<String, BigDecimal> requestBaseQuantityMap = new HashMap<>();
|
||||||
|
List<GaoCustomerPackagePurchaseItem> result = new ArrayList<>();
|
||||||
|
for (GaoCustomerPackagePurchaseItem requestItem : requestItemList) {
|
||||||
|
List<GaoPackageConfigItem> skuConfigItemList = configItemMap.get(requestItem.getSkuId());
|
||||||
|
TimiException.requiredTrue(skuConfigItemList != null && !skuConfigItemList.isEmpty(), "购买 SKU 不属于套餐配置");
|
||||||
|
GaoProductSkuUnitSelection selection = skuService.getSellableSkuUnit(storeId, requestItem.getSkuId(), requestItem.getSkuUnitId());
|
||||||
|
TimiException.requiredTrue(skuConfigItemList.stream().anyMatch(configItem -> 0 <= configItem.getBaseQuantityPerUnitSnapshot()
|
||||||
|
.compareTo(selection.getUnit().getBaseQuantityPerUnit())), "套餐权益单位不能高于配置单位");
|
||||||
|
BigDecimal requestBaseQuantity = toBaseQuantity(requestItem.getTotalQuantity(), selection.getUnit().getBaseQuantityPerUnit(), "套餐权益基础数量最多保留三位小数");
|
||||||
|
requestBaseQuantityMap.merge(requestItem.getSkuId(), requestBaseQuantity, BigDecimal::add);
|
||||||
|
if (requestItem.getTotalQuantity().signum() == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!Boolean.TRUE.equals(selection.getSku().getAllowDecimalQuantity())) {
|
||||||
|
TimiException.requiredTrue(requestItem.getTotalQuantity().stripTrailingZeros().scale() <= 0, "该 SKU 不允许小数数量");
|
||||||
|
}
|
||||||
|
GaoPackageConfigItem template = resolveItemTemplate(skuConfigItemList, selection.getUnit().getId());
|
||||||
|
GaoCustomerPackagePurchaseItem item = new GaoCustomerPackagePurchaseItem();
|
||||||
|
item.setSkuId(selection.getSku().getId());
|
||||||
|
item.setSkuUnitId(selection.getUnit().getId());
|
||||||
|
item.setProductNameSnapshot(selection.getProduct().getName());
|
||||||
|
item.setProductIdSnapshot(selection.getProduct().getId());
|
||||||
|
item.setSkuCodeSnapshot(selection.getSku().getCode());
|
||||||
|
item.setSkuNameSnapshot(selection.getSku().getName());
|
||||||
|
item.setSpecSummarySnapshot(selection.getSku().getSpecSummary());
|
||||||
|
item.setUnitSnapshot(selection.getUnit().getName());
|
||||||
|
item.setBaseQuantityPerUnitSnapshot(selection.getUnit().getBaseQuantityPerUnit());
|
||||||
|
item.setTotalQuantity(requestItem.getTotalQuantity());
|
||||||
|
item.setSort(template.getSort());
|
||||||
|
item.setRemark(template.getRemark());
|
||||||
|
result.add(item);
|
||||||
|
}
|
||||||
|
TimiException.requiredTrue(!result.isEmpty(), "购买套餐至少需要一个产品");
|
||||||
|
if (!canOverridePriceAndQuantity) {
|
||||||
|
for (Map.Entry<String, BigDecimal> entry : configBaseQuantityMap.entrySet()) {
|
||||||
|
TimiException.requiredTrue(entry.getValue().compareTo(requestBaseQuantityMap.getOrDefault(entry.getKey(), ZERO)) == 0,
|
||||||
|
"普通员工不能修改套餐权益配额");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void verifySameStructureAsConfig(List<GaoCustomerPackagePurchaseItem> itemList, List<GaoPackageConfigItem> configItemList) {
|
private GaoPackageConfigItem resolveItemTemplate(List<GaoPackageConfigItem> configItemList, String skuUnitId) {
|
||||||
TimiException.requiredTrue(itemList.size() == configItemList.size(), "套餐权益项目必须与套餐配置一致");
|
return configItemList.stream().filter(item -> skuUnitId.equals(item.getSkuUnitId())).findFirst()
|
||||||
Map<String, GaoCustomerPackagePurchaseItem> itemMap = itemList.stream().collect(Collectors.toMap(item -> item.getSkuId() + "|" + item.getSkuUnitId(), Function.identity()));
|
.orElseGet(() -> configItemList.stream().min(Comparator.comparing(GaoPackageConfigItem::getSort)).orElseThrow());
|
||||||
for (GaoPackageConfigItem configItem : configItemList) {
|
|
||||||
GaoCustomerPackagePurchaseItem item = itemMap.get(configItem.getSkuId() + "|" + configItem.getSkuUnitId());
|
|
||||||
TimiException.required(item, "套餐权益项目必须与套餐配置一致");
|
|
||||||
item.setProductNameSnapshot(configItem.getProductNameSnapshot());
|
|
||||||
item.setProductIdSnapshot(configItem.getProductIdSnapshot());
|
|
||||||
item.setSkuCodeSnapshot(configItem.getSkuCodeSnapshot());
|
|
||||||
item.setSkuNameSnapshot(configItem.getSkuNameSnapshot());
|
|
||||||
item.setSpecSummarySnapshot(configItem.getSpecSummarySnapshot());
|
|
||||||
item.setUnitSnapshot(configItem.getUnitSnapshot());
|
|
||||||
item.setBaseQuantityPerUnitSnapshot(configItem.getBaseQuantityPerUnitSnapshot());
|
|
||||||
TimiException.requiredTrue(item.getSort().equals(configItem.getSort())
|
|
||||||
&& sameText(item.getRemark(), configItem.getRemark()), "套餐权益项目的名称、单位、排序和备注不可修改");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void verifySameQuantityAsConfig(List<GaoCustomerPackagePurchaseItem> itemList, List<GaoPackageConfigItem> configItemList) {
|
private BigDecimal toBaseQuantity(BigDecimal quantity, BigDecimal baseQuantityPerUnit, String message) {
|
||||||
Map<String, GaoCustomerPackagePurchaseItem> itemMap = itemList.stream().collect(Collectors.toMap(item -> item.getSkuId() + "|" + item.getSkuUnitId(), Function.identity()));
|
BigDecimal result = quantity.multiply(baseQuantityPerUnit);
|
||||||
for (GaoPackageConfigItem configItem : configItemList) {
|
TimiException.requiredTrue(3 >= result.stripTrailingZeros().scale(), message);
|
||||||
GaoCustomerPackagePurchaseItem item = itemMap.get(configItem.getSkuId() + "|" + configItem.getSkuUnitId());
|
try {
|
||||||
TimiException.requiredTrue(item.getTotalQuantity().compareTo(configItem.getDefaultQuantity()) == 0, "普通员工不能修改套餐权益配额");
|
return result.setScale(3, RoundingMode.UNNECESSARY);
|
||||||
|
} catch (ArithmeticException e) {
|
||||||
|
throw new TimiException(com.imyeyu.java.bean.timi.TimiCode.ERROR, message, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+106
-69
@@ -29,6 +29,7 @@ import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentItemRequest;
|
|||||||
import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentPage;
|
import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentPage;
|
||||||
import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentPrepareStockRequest;
|
import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentPrepareStockRequest;
|
||||||
import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentRevokeRequest;
|
import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentRevokeRequest;
|
||||||
|
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;
|
||||||
@@ -46,6 +47,7 @@ import org.springframework.stereotype.Service;
|
|||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -194,12 +196,11 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
|
|||||||
GaoOrderFulfillment snapshot = mapper.selectById(request.getFulfillmentId());
|
GaoOrderFulfillment snapshot = mapper.selectById(request.getFulfillmentId());
|
||||||
TimiException.required(snapshot, "not found fulfillment");
|
TimiException.required(snapshot, "not found fulfillment");
|
||||||
GaoSalesOrder order = salesOrderService.getForUpdate(snapshot.getSalesOrderId());
|
GaoSalesOrder order = salesOrderService.getForUpdate(snapshot.getSalesOrderId());
|
||||||
List<SourceEntry> sourceList = loadSources(order, itemRequests(snapshot.getId()), false);
|
List<SourceEntry> sourceList = loadPersistedSources(order, itemMapper.selectByFulfillmentId(snapshot.getId()));
|
||||||
GaoOrderFulfillment fulfillment = lockFulfillment(snapshot.getId(), order);
|
GaoOrderFulfillment fulfillment = lockFulfillment(snapshot.getId(), order);
|
||||||
storeScopeService.checkStoreId(fulfillment.getStoreId());
|
storeScopeService.checkStoreId(fulfillment.getStoreId());
|
||||||
TimiException.requiredTrue(fulfillment.getStatus() == GaoOrderFulfillment.Status.WAITING_STOCK, "履约单不是等待库存状态");
|
TimiException.requiredTrue(fulfillment.getStatus() == GaoOrderFulfillment.Status.WAITING_STOCK, "履约单不是等待库存状态");
|
||||||
TimiException.requiredTrue(order.getStatus() != GaoSalesOrder.Status.CANCELLED, "已取消订单不能准备库存");
|
TimiException.requiredTrue(order.getStatus() != GaoSalesOrder.Status.CANCELLED, "已取消订单不能准备库存");
|
||||||
salesOrderService.checkFulfillmentPermission(order);
|
|
||||||
List<GaoOrderFulfillmentItem> fulfillmentItemList = itemMapper.selectByFulfillmentIdForUpdate(fulfillment.getId());
|
List<GaoOrderFulfillmentItem> fulfillmentItemList = itemMapper.selectByFulfillmentIdForUpdate(fulfillment.getId());
|
||||||
TimiException.requiredTrue(!fulfillmentItemList.isEmpty(), "履约单没有明细");
|
TimiException.requiredTrue(!fulfillmentItemList.isEmpty(), "履约单没有明细");
|
||||||
List<GaoStockChangeItem> stockItems = buildStockItems(order.getStoreId(), sourceList);
|
List<GaoStockChangeItem> stockItems = buildStockItems(order.getStoreId(), sourceList);
|
||||||
@@ -228,12 +229,11 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
|
|||||||
GaoOrderFulfillment snapshot = mapper.selectById(request.getFulfillmentId());
|
GaoOrderFulfillment snapshot = mapper.selectById(request.getFulfillmentId());
|
||||||
TimiException.required(snapshot, "not found fulfillment");
|
TimiException.required(snapshot, "not found fulfillment");
|
||||||
GaoSalesOrder order = salesOrderService.getForUpdate(snapshot.getSalesOrderId());
|
GaoSalesOrder order = salesOrderService.getForUpdate(snapshot.getSalesOrderId());
|
||||||
List<SourceEntry> sourceList = loadSources(order, itemRequests(snapshot.getId()), false);
|
List<SourceEntry> sourceList = loadPersistedSources(order, itemMapper.selectByFulfillmentId(snapshot.getId()));
|
||||||
GaoOrderFulfillment fulfillment = lockFulfillment(snapshot.getId(), order);
|
GaoOrderFulfillment fulfillment = lockFulfillment(snapshot.getId(), order);
|
||||||
storeScopeService.checkStoreId(fulfillment.getStoreId());
|
storeScopeService.checkStoreId(fulfillment.getStoreId());
|
||||||
TimiException.requiredTrue(fulfillment.getStatus() == GaoOrderFulfillment.Status.PENDING, "履约单不是待核销状态");
|
TimiException.requiredTrue(fulfillment.getStatus() == GaoOrderFulfillment.Status.PENDING, "履约单不是待核销状态");
|
||||||
TimiException.requiredTrue(order.getStatus() != GaoSalesOrder.Status.CANCELLED, "已取消订单不能核销履约");
|
TimiException.requiredTrue(order.getStatus() != GaoSalesOrder.Status.CANCELLED, "已取消订单不能核销履约");
|
||||||
salesOrderService.checkFulfillmentPermission(order);
|
|
||||||
List<GaoOrderFulfillmentItem> fulfillmentItemList = itemMapper.selectByFulfillmentIdForUpdate(fulfillment.getId());
|
List<GaoOrderFulfillmentItem> fulfillmentItemList = itemMapper.selectByFulfillmentIdForUpdate(fulfillment.getId());
|
||||||
TimiException.requiredTrue(!fulfillmentItemList.isEmpty(), "履约单没有明细");
|
TimiException.requiredTrue(!fulfillmentItemList.isEmpty(), "履约单没有明细");
|
||||||
assertReserved(sourceList);
|
assertReserved(sourceList);
|
||||||
@@ -283,7 +283,7 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
|
|||||||
GaoOrderFulfillment snapshot = mapper.selectById(request.getFulfillmentId());
|
GaoOrderFulfillment snapshot = mapper.selectById(request.getFulfillmentId());
|
||||||
TimiException.required(snapshot, "not found fulfillment");
|
TimiException.required(snapshot, "not found fulfillment");
|
||||||
GaoSalesOrder order = salesOrderService.getForUpdate(snapshot.getSalesOrderId());
|
GaoSalesOrder order = salesOrderService.getForUpdate(snapshot.getSalesOrderId());
|
||||||
List<SourceEntry> sourceList = loadSources(order, itemRequests(snapshot.getId()), false);
|
List<SourceEntry> sourceList = loadPersistedSources(order, itemMapper.selectByFulfillmentId(snapshot.getId()));
|
||||||
GaoOrderFulfillment fulfillment = lockFulfillment(snapshot.getId(), order);
|
GaoOrderFulfillment fulfillment = lockFulfillment(snapshot.getId(), order);
|
||||||
storeScopeService.checkStoreId(fulfillment.getStoreId());
|
storeScopeService.checkStoreId(fulfillment.getStoreId());
|
||||||
TimiException.requiredTrue(fulfillment.getStatus() == GaoOrderFulfillment.Status.WAITING_STOCK
|
TimiException.requiredTrue(fulfillment.getStatus() == GaoOrderFulfillment.Status.WAITING_STOCK
|
||||||
@@ -326,7 +326,7 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
|
|||||||
}
|
}
|
||||||
GaoSalesOrder order = salesOrderService.getForUpdate(sourceSnapshot.getSalesOrderId());
|
GaoSalesOrder order = salesOrderService.getForUpdate(sourceSnapshot.getSalesOrderId());
|
||||||
List<GaoOrderFulfillmentItem> sourceItemList = itemMapper.selectByFulfillmentId(sourceSnapshot.getId());
|
List<GaoOrderFulfillmentItem> sourceItemList = itemMapper.selectByFulfillmentId(sourceSnapshot.getId());
|
||||||
List<SourceEntry> sourceList = loadSources(order, copyRequestItems(sourceItemList), false);
|
List<SourceEntry> sourceList = loadPersistedSources(order, sourceItemList);
|
||||||
GaoOrderFulfillment source = lockFulfillment(sourceSnapshot.getId(), order);
|
GaoOrderFulfillment source = lockFulfillment(sourceSnapshot.getId(), order);
|
||||||
storeScopeService.checkStoreId(source.getStoreId());
|
storeScopeService.checkStoreId(source.getStoreId());
|
||||||
TimiException.requiredTrue(source.getStatus() == GaoOrderFulfillment.Status.VERIFIED && source.getSourceFulfillmentId() == null, "只有原始已核销履约可以冲正");
|
TimiException.requiredTrue(source.getStatus() == GaoOrderFulfillment.Status.VERIFIED && source.getSourceFulfillmentId() == null, "只有原始已核销履约可以冲正");
|
||||||
@@ -356,10 +356,10 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
|
|||||||
TimiException.required(sourceItem, "履约明细来源订单项不存在");
|
TimiException.required(sourceItem, "履约明细来源订单项不存在");
|
||||||
GaoOrderFulfillmentItem reverseItem = copyFulfillmentItem(sourceItem);
|
GaoOrderFulfillmentItem reverseItem = copyFulfillmentItem(sourceItem);
|
||||||
reverseItem.setFulfillmentId(reverse.getId());
|
reverseItem.setFulfillmentId(reverse.getId());
|
||||||
reverseItem.setReservedBefore(sourceEntry.reservedBefore());
|
reverseItem.setReservedBaseBefore(sourceEntry.reservedBaseBefore());
|
||||||
reverseItem.setReservedAfter(sourceEntry.reservedBefore());
|
reverseItem.setReservedBaseAfter(sourceEntry.reservedBaseBefore());
|
||||||
reverseItem.setFulfilledBefore(sourceEntry.fulfilledBefore());
|
reverseItem.setFulfilledBaseBefore(sourceEntry.fulfilledBaseBefore());
|
||||||
reverseItem.setFulfilledAfter(sourceEntry.fulfilledBefore().subtract(sourceEntry.quantity()));
|
reverseItem.setFulfilledBaseAfter(sourceEntry.fulfilledBaseBefore().subtract(sourceEntry.baseQuantity()));
|
||||||
itemMapper.insert(reverseItem);
|
itemMapper.insert(reverseItem);
|
||||||
}
|
}
|
||||||
if (!stockItems.isEmpty()) {
|
if (!stockItems.isEmpty()) {
|
||||||
@@ -397,28 +397,28 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
|
|||||||
|
|
||||||
private void insertSourceReservations(GaoOrderFulfillment fulfillment, List<SourceEntry> sourceList) {
|
private void insertSourceReservations(GaoOrderFulfillment fulfillment, List<SourceEntry> sourceList) {
|
||||||
for (SourceEntry source : sourceList) {
|
for (SourceEntry source : sourceList) {
|
||||||
TimiException.requiredTrue(1 == salesOrderService.updateItemReservedQuantity(source.sourceId(), source.quantity()), "订单明细占用失败");
|
TimiException.requiredTrue(1 == salesOrderService.updateItemReservedBaseQuantity(source.sourceId(), source.baseQuantity()), "订单明细占用失败");
|
||||||
GaoOrderFulfillmentItem item = source.toFulfillmentItem(fulfillment.getId());
|
GaoOrderFulfillmentItem item = source.toFulfillmentItem(fulfillment.getId());
|
||||||
itemMapper.insert(item);
|
itemMapper.insert(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void decreaseReserved(SourceEntry source) {
|
private void decreaseReserved(SourceEntry source) {
|
||||||
TimiException.requiredTrue(1 == salesOrderService.updateItemReservedQuantity(source.sourceId(), source.quantity().negate()), "订单明细占用释放失败");
|
TimiException.requiredTrue(1 == salesOrderService.updateItemReservedBaseQuantity(source.sourceId(), source.baseQuantity().negate()), "订单明细占用释放失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void increaseFulfilled(SourceEntry source) {
|
private void increaseFulfilled(SourceEntry source) {
|
||||||
TimiException.requiredTrue(1 == salesOrderService.updateItemFulfilledQuantity(source.sourceId(), source.quantity()), "订单明细履约失败");
|
TimiException.requiredTrue(1 == salesOrderService.updateItemFulfilledBaseQuantity(source.sourceId(), source.baseQuantity()), "订单明细履约失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void decreaseFulfilled(SourceEntry source) {
|
private void decreaseFulfilled(SourceEntry source) {
|
||||||
TimiException.requiredTrue(0 <= source.fulfilledBefore().compareTo(source.quantity()), "已履约数量不足以冲正");
|
TimiException.requiredTrue(0 <= source.fulfilledBaseBefore().compareTo(source.baseQuantity()), "已履约数量不足以冲正");
|
||||||
TimiException.requiredTrue(1 == salesOrderService.updateItemFulfilledQuantity(source.sourceId(), source.quantity().negate()), "订单明细冲正失败");
|
TimiException.requiredTrue(1 == salesOrderService.updateItemFulfilledBaseQuantity(source.sourceId(), source.baseQuantity().negate()), "订单明细冲正失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertReserved(List<SourceEntry> sourceList) {
|
private void assertReserved(List<SourceEntry> sourceList) {
|
||||||
for (SourceEntry source : sourceList) {
|
for (SourceEntry source : sourceList) {
|
||||||
TimiException.requiredTrue(0 <= source.reservedBefore().compareTo(source.quantity()), "履约待占用数量不足");
|
TimiException.requiredTrue(0 <= source.reservedBaseBefore().compareTo(source.baseQuantity()), "履约待占用数量不足");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -430,6 +430,7 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
|
|||||||
TimiException.required(requestItem, "履约明细不能为空");
|
TimiException.required(requestItem, "履约明细不能为空");
|
||||||
String sourceId = requiredText(requestItem.getSourceOrderItemId(), "履约明细来源订单项不能为空");
|
String sourceId = requiredText(requestItem.getSourceOrderItemId(), "履约明细来源订单项不能为空");
|
||||||
requestItem.setSourceOrderItemId(sourceId);
|
requestItem.setSourceOrderItemId(sourceId);
|
||||||
|
requestItem.setSkuUnitId(requiredText(requestItem.getSkuUnitId(), "履约 SKU 单位不能为空"));
|
||||||
TimiException.requiredTrue(sourceIdSet.add(sourceId), "同一履约单不能重复来源明细");
|
TimiException.requiredTrue(sourceIdSet.add(sourceId), "同一履约单不能重复来源明细");
|
||||||
TimiException.required(requestItem.getQuantity(), "履约数量不能为空");
|
TimiException.required(requestItem.getQuantity(), "履约数量不能为空");
|
||||||
TimiException.requiredTrue(0 < requestItem.getQuantity().signum(), "履约数量必须大于 0");
|
TimiException.requiredTrue(0 < requestItem.getQuantity().signum(), "履约数量必须大于 0");
|
||||||
@@ -444,40 +445,73 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
|
|||||||
GaoSalesOrderItem.ItemType expectedItemType = order.getOrderType() == GaoSalesOrder.OrderType.PACKAGE
|
GaoSalesOrderItem.ItemType expectedItemType = order.getOrderType() == GaoSalesOrder.OrderType.PACKAGE
|
||||||
? GaoSalesOrderItem.ItemType.PACKAGE_ENTITLEMENT : GaoSalesOrderItem.ItemType.DIRECT_PRODUCT;
|
? GaoSalesOrderItem.ItemType.PACKAGE_ENTITLEMENT : GaoSalesOrderItem.ItemType.DIRECT_PRODUCT;
|
||||||
TimiException.requiredTrue(source.getItemType() == expectedItemType, "订单项类型与订单类型不匹配");
|
TimiException.requiredTrue(source.getItemType() == expectedItemType, "订单项类型与订单类型不匹配");
|
||||||
result.add(toSource(order, source, requestItem.getQuantity(), checkAvailable));
|
GaoProductSkuUnitSelection selection = skuService.getSellableSkuUnit(order.getStoreId(), source.getSkuId(), requestItem.getSkuUnitId());
|
||||||
|
result.add(toSource(order, source, selection.getProduct(), selection.getSku(), selection.getUnit(), requestItem.getQuantity(), checkAvailable));
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private SourceEntry toSource(GaoSalesOrder order, GaoSalesOrderItem source, BigDecimal quantity, boolean checkAvailable) {
|
private List<SourceEntry> loadPersistedSources(GaoSalesOrder order, List<GaoOrderFulfillmentItem> itemList) {
|
||||||
|
TimiException.requiredTrue(itemList != null && !itemList.isEmpty(), "履约单没有明细");
|
||||||
|
List<GaoOrderFulfillmentItem> sortedItemList = new ArrayList<>(itemList);
|
||||||
|
sortedItemList.sort(Comparator.comparing(GaoOrderFulfillmentItem::getSourceOrderItemId));
|
||||||
|
Set<String> sourceIdSet = new HashSet<>();
|
||||||
|
List<SourceEntry> result = new ArrayList<>();
|
||||||
|
for (GaoOrderFulfillmentItem item : sortedItemList) {
|
||||||
|
TimiException.required(item, "履约明细不能为空");
|
||||||
|
String sourceId = requiredText(item.getSourceOrderItemId(), "履约明细来源订单项不能为空");
|
||||||
|
TimiException.requiredTrue(sourceIdSet.add(sourceId), "同一履约单不能重复来源明细");
|
||||||
|
GaoSalesOrderItem source = salesOrderService.getItemForUpdate(sourceId);
|
||||||
|
validateSourceOrderItem(order, source);
|
||||||
GaoProductSku sku = skuService.get(source.getSkuId());
|
GaoProductSku sku = skuService.get(source.getSkuId());
|
||||||
TimiException.required(sku, "履约来源 SKU 不存在");
|
TimiException.required(sku, "履约来源 SKU 不存在");
|
||||||
GaoProduct product = productService.get(sku.getProductId());
|
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(item.getBaseQuantityPerUnitSnapshot());
|
||||||
GaoProductSkuUnit unit = snapshotUnit(source.getSkuUnitId(), source.getUnitSnapshot(), baseQuantityPerUnit);
|
BigDecimal quantity = requiredPositiveQuantity(item.getQuantity(), "履约数量");
|
||||||
JsonNode skuSpecSnapshot = source.getSkuSpecSnapshot();
|
BigDecimal baseQuantity = requiredBaseQuantity(item.getBaseQuantity());
|
||||||
return buildSource(order, source.getId(), source.getSkuId(), source.getSkuUnitId(), source.getProductIdSnapshot(), source.getProductNameSnapshot(), source.getSkuCodeSnapshot(), source.getSkuNameSnapshot(), source.getSpecSummarySnapshot(), skuSpecSnapshot,
|
TimiException.requiredTrue(baseQuantity.compareTo(toBaseQuantity(quantity, baseQuantityPerUnit, "履约基础数量最多保留三位小数")) == 0,
|
||||||
source.getUnitSnapshot(), baseQuantityPerUnit, quantity, source.getQuantity(), source.getReservedQuantity(), source.getFulfilledQuantity(),
|
"履约基础数量与单位换算快照不一致");
|
||||||
product, sku, unit, checkAvailable);
|
result.add(buildSource(order, source, item.getSkuUnitId(), item.getUnitSnapshot(), baseQuantityPerUnit, quantity, baseQuantity, product, sku, false));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private SourceEntry buildSource(GaoSalesOrder order, String orderItemId, String skuId, String unitId, String productId,
|
private SourceEntry toSource(GaoSalesOrder order, GaoSalesOrderItem source, GaoProduct product, GaoProductSku sku,
|
||||||
String productName, String skuCode, String skuName, String specSummary, JsonNode skuSpecSnapshot, String unitName, BigDecimal baseQuantityPerUnit, BigDecimal quantity, BigDecimal totalQuantity,
|
GaoProductSkuUnit unit, BigDecimal quantity, boolean checkAvailable) {
|
||||||
BigDecimal reservedQuantity, BigDecimal fulfilledQuantity, GaoProduct product, GaoProductSku sku, GaoProductSkuUnit unit,
|
TimiException.requiredTrue(source.getSkuId().equals(sku.getId()), "履约单位不属于来源 SKU");
|
||||||
boolean checkAvailable) {
|
TimiException.requiredTrue(order.getStoreId().equals(product.getStoreId()), "履约来源产品不属于当前门店");
|
||||||
|
quantity = requiredPositiveQuantity(quantity, "履约数量");
|
||||||
if (!Boolean.TRUE.equals(sku.getAllowDecimalQuantity())) {
|
if (!Boolean.TRUE.equals(sku.getAllowDecimalQuantity())) {
|
||||||
TimiException.requiredTrue(quantity.stripTrailingZeros().scale() <= 0, "该产品不允许小数数量");
|
TimiException.requiredTrue(quantity.stripTrailingZeros().scale() <= 0, "该产品不允许小数数量");
|
||||||
}
|
}
|
||||||
BigDecimal reservedBefore = decimal(reservedQuantity);
|
BigDecimal baseQuantityPerUnit = requiredBaseQuantityPerUnit(unit.getBaseQuantityPerUnit());
|
||||||
BigDecimal fulfilledBefore = decimal(fulfilledQuantity);
|
BigDecimal baseQuantity = toBaseQuantity(quantity, baseQuantityPerUnit, "履约基础数量最多保留三位小数");
|
||||||
if (checkAvailable) {
|
return buildSource(order, source, unit.getId(), unit.getName(), baseQuantityPerUnit, quantity, baseQuantity, product, sku, checkAvailable);
|
||||||
BigDecimal available = decimal(totalQuantity).subtract(reservedBefore).subtract(fulfilledBefore);
|
|
||||||
TimiException.requiredTrue(0 <= available.compareTo(quantity), "来源产品可履约数量不足");
|
|
||||||
}
|
}
|
||||||
return new SourceEntry(orderItemId, skuId, unitId, productId, productName, skuCode, skuName, specSummary, skuSpecSnapshot, unitName, baseQuantityPerUnit,
|
|
||||||
quantity, decimal(totalQuantity), reservedBefore, fulfilledBefore, product, sku, unit);
|
private SourceEntry buildSource(GaoSalesOrder order, GaoSalesOrderItem source, String unitId, String unitName,
|
||||||
|
BigDecimal baseQuantityPerUnit, BigDecimal quantity, BigDecimal baseQuantity, GaoProduct product, GaoProductSku sku,
|
||||||
|
boolean checkAvailable) {
|
||||||
|
BigDecimal reservedBaseBefore = decimal(source.getReservedBaseQuantity());
|
||||||
|
BigDecimal fulfilledBaseBefore = decimal(source.getFulfilledBaseQuantity());
|
||||||
|
BigDecimal totalBaseQuantity = requiredBaseQuantity(source.getBaseQuantity());
|
||||||
|
if (checkAvailable) {
|
||||||
|
BigDecimal availableBaseQuantity = totalBaseQuantity.subtract(reservedBaseBefore).subtract(fulfilledBaseBefore);
|
||||||
|
TimiException.requiredTrue(0 <= availableBaseQuantity.compareTo(baseQuantity), "来源产品可履约数量不足");
|
||||||
|
}
|
||||||
|
return new SourceEntry(source.getId(), source.getSkuId(), unitId, source.getProductIdSnapshot(), source.getProductNameSnapshot(),
|
||||||
|
source.getSkuCodeSnapshot(), source.getSkuNameSnapshot(), source.getSpecSummarySnapshot(), source.getSkuSpecSnapshot(), unitName,
|
||||||
|
baseQuantityPerUnit, quantity, baseQuantity, totalBaseQuantity, reservedBaseBefore, fulfilledBaseBefore, product, sku);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateSourceOrderItem(GaoSalesOrder order, GaoSalesOrderItem source) {
|
||||||
|
TimiException.required(source, "not found sales order item");
|
||||||
|
TimiException.requiredTrue(order.getId().equals(source.getSalesOrderId()), "订单明细不属于当前订单");
|
||||||
|
GaoSalesOrderItem.ItemType expectedItemType = order.getOrderType() == GaoSalesOrder.OrderType.PACKAGE
|
||||||
|
? GaoSalesOrderItem.ItemType.PACKAGE_ENTITLEMENT : GaoSalesOrderItem.ItemType.DIRECT_PRODUCT;
|
||||||
|
TimiException.requiredTrue(source.getItemType() == expectedItemType, "订单项类型与订单类型不匹配");
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<GaoStockChangeItem> buildStockItems(String storeId, List<SourceEntry> sourceList) {
|
private List<GaoStockChangeItem> buildStockItems(String storeId, List<SourceEntry> sourceList) {
|
||||||
@@ -489,7 +523,7 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
|
|||||||
GaoProductSkuUnit baseUnit = skuService.listUnitBySkuId(source.sku().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 = source.quantity().multiply(source.unit().getBaseQuantityPerUnit());
|
BigDecimal baseQuantity = source.baseQuantity();
|
||||||
GaoStockChangeItem item = itemMap.get(source.sku().getId());
|
GaoStockChangeItem item = itemMap.get(source.sku().getId());
|
||||||
if (item == null) {
|
if (item == null) {
|
||||||
item = GaoStockChangeItem.of(source.product(), source.sku(), baseUnit, baseQuantity, baseQuantity);
|
item = GaoStockChangeItem.of(source.product(), source.sku(), baseUnit, baseQuantity, baseQuantity);
|
||||||
@@ -528,21 +562,6 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<GaoOrderFulfillmentItemRequest> itemRequests(String fulfillmentId) {
|
|
||||||
return copyRequestItems(itemMapper.selectByFulfillmentId(fulfillmentId));
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<GaoOrderFulfillmentItemRequest> copyRequestItems(List<GaoOrderFulfillmentItem> itemList) {
|
|
||||||
List<GaoOrderFulfillmentItemRequest> result = new ArrayList<>();
|
|
||||||
for (GaoOrderFulfillmentItem item : TimiJava.safeIterable(itemList)) {
|
|
||||||
GaoOrderFulfillmentItemRequest request = new GaoOrderFulfillmentItemRequest();
|
|
||||||
request.setSourceOrderItemId(item.getSourceOrderItemId());
|
|
||||||
request.setQuantity(item.getQuantity());
|
|
||||||
result.add(request);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private GaoOrderFulfillmentItem copyFulfillmentItem(GaoOrderFulfillmentItem source) {
|
private GaoOrderFulfillmentItem copyFulfillmentItem(GaoOrderFulfillmentItem source) {
|
||||||
GaoOrderFulfillmentItem result = new GaoOrderFulfillmentItem();
|
GaoOrderFulfillmentItem result = new GaoOrderFulfillmentItem();
|
||||||
result.setSkuId(source.getSkuId());
|
result.setSkuId(source.getSkuId());
|
||||||
@@ -561,20 +580,36 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private GaoProductSkuUnit snapshotUnit(String id, String name, BigDecimal baseQuantityPerUnit) {
|
|
||||||
GaoProductSkuUnit unit = new GaoProductSkuUnit();
|
|
||||||
unit.setId(id);
|
|
||||||
unit.setName(name);
|
|
||||||
unit.setBaseQuantityPerUnit(baseQuantityPerUnit);
|
|
||||||
return unit;
|
|
||||||
}
|
|
||||||
|
|
||||||
private BigDecimal requiredBaseQuantityPerUnit(BigDecimal value) {
|
private BigDecimal requiredBaseQuantityPerUnit(BigDecimal value) {
|
||||||
TimiException.required(value, "履约来源缺少基础单位数量系数快照");
|
TimiException.required(value, "履约来源缺少基础单位数量系数快照");
|
||||||
TimiException.requiredTrue(0 < value.signum(), "履约来源基础单位数量系数无效");
|
TimiException.requiredTrue(0 < value.signum(), "履约来源基础单位数量系数无效");
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private BigDecimal requiredPositiveQuantity(BigDecimal value, String fieldName) {
|
||||||
|
TimiException.required(value, fieldName + "不能为空");
|
||||||
|
TimiException.requiredTrue(0 < value.signum(), fieldName + "必须大于 0");
|
||||||
|
TimiException.requiredTrue(3 >= value.stripTrailingZeros().scale(), fieldName + "最多保留三位小数");
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal requiredBaseQuantity(BigDecimal value) {
|
||||||
|
TimiException.required(value, "基础单位数量不能为空");
|
||||||
|
TimiException.requiredTrue(0 < value.signum(), "基础单位数量必须大于 0");
|
||||||
|
TimiException.requiredTrue(3 >= value.stripTrailingZeros().scale(), "基础单位数量最多保留三位小数");
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal toBaseQuantity(BigDecimal quantity, BigDecimal baseQuantityPerUnit, String message) {
|
||||||
|
BigDecimal result = quantity.multiply(baseQuantityPerUnit);
|
||||||
|
TimiException.requiredTrue(3 >= result.stripTrailingZeros().scale(), message);
|
||||||
|
try {
|
||||||
|
return result.setScale(3, RoundingMode.UNNECESSARY);
|
||||||
|
} catch (ArithmeticException e) {
|
||||||
|
throw new TimiException(TimiCode.ERROR, message, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void verifyIdempotency(GaoOrderFulfillment existing, GaoOrderFulfillmentCreateRequest request) {
|
private void verifyIdempotency(GaoOrderFulfillment existing, GaoOrderFulfillmentCreateRequest request) {
|
||||||
TimiException.requiredTrue(existing.getSalesOrderId().equals(request.getSalesOrderId())
|
TimiException.requiredTrue(existing.getSalesOrderId().equals(request.getSalesOrderId())
|
||||||
&& (request.getDeliveredAt() == null || request.getDeliveredAt().equals(existing.getDeliveredAt()))
|
&& (request.getDeliveredAt() == null || request.getDeliveredAt().equals(existing.getDeliveredAt()))
|
||||||
@@ -582,12 +617,14 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
|
|||||||
List<GaoOrderFulfillmentItem> existingItems = itemMapper.selectByFulfillmentId(existing.getId());
|
List<GaoOrderFulfillmentItem> existingItems = itemMapper.selectByFulfillmentId(existing.getId());
|
||||||
List<GaoOrderFulfillmentItemRequest> requestItems = request.getItemList();
|
List<GaoOrderFulfillmentItemRequest> requestItems = request.getItemList();
|
||||||
TimiException.requiredTrue(existingItems.size() == (requestItems == null ? 0 : requestItems.size()), "requestNo 对应履约请求内容不一致");
|
TimiException.requiredTrue(existingItems.size() == (requestItems == null ? 0 : requestItems.size()), "requestNo 对应履约请求内容不一致");
|
||||||
Map<String, BigDecimal> requestMap = new HashMap<>();
|
Map<String, GaoOrderFulfillmentItemRequest> requestMap = new HashMap<>();
|
||||||
for (GaoOrderFulfillmentItemRequest item : TimiJava.safeIterable(requestItems)) {
|
for (GaoOrderFulfillmentItemRequest item : TimiJava.safeIterable(requestItems)) {
|
||||||
requestMap.put(item.getSourceOrderItemId(), item.getQuantity());
|
requestMap.put(item.getSourceOrderItemId(), item);
|
||||||
}
|
}
|
||||||
for (GaoOrderFulfillmentItem item : existingItems) {
|
for (GaoOrderFulfillmentItem item : existingItems) {
|
||||||
TimiException.requiredTrue(item.getQuantity().compareTo(requestMap.getOrDefault(item.getSourceOrderItemId(), ZERO)) == 0, "requestNo 对应履约请求内容不一致");
|
GaoOrderFulfillmentItemRequest requestItem = requestMap.get(item.getSourceOrderItemId());
|
||||||
|
TimiException.requiredTrue(requestItem != null && item.getSkuUnitId().equals(requestItem.getSkuUnitId())
|
||||||
|
&& item.getQuantity().compareTo(requestItem.getQuantity()) == 0, "requestNo 对应履约请求内容不一致");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -671,8 +708,8 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
|
|||||||
private record SourceEntry(String sourceOrderItemId, String skuId, String skuUnitId, String productIdSnapshot,
|
private record SourceEntry(String sourceOrderItemId, String skuId, String skuUnitId, String productIdSnapshot,
|
||||||
String productNameSnapshot, String skuCodeSnapshot, String skuNameSnapshot, String specSummarySnapshot, JsonNode skuSpecSnapshot,
|
String productNameSnapshot, String skuCodeSnapshot, String skuNameSnapshot, String specSummarySnapshot, JsonNode skuSpecSnapshot,
|
||||||
String unitSnapshot, BigDecimal baseQuantityPerUnitSnapshot, BigDecimal quantity,
|
String unitSnapshot, BigDecimal baseQuantityPerUnitSnapshot, BigDecimal quantity,
|
||||||
BigDecimal totalQuantity, BigDecimal reservedBefore, BigDecimal fulfilledBefore, GaoProduct product, GaoProductSku sku,
|
BigDecimal baseQuantity, BigDecimal totalBaseQuantity, BigDecimal reservedBaseBefore, BigDecimal fulfilledBaseBefore,
|
||||||
GaoProductSkuUnit unit) {
|
GaoProduct product, GaoProductSku sku) {
|
||||||
|
|
||||||
private String sourceId() {
|
private String sourceId() {
|
||||||
return sourceOrderItemId;
|
return sourceOrderItemId;
|
||||||
@@ -686,11 +723,11 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
|
|||||||
result.setProductIdSnapshot(productIdSnapshot);
|
result.setProductIdSnapshot(productIdSnapshot);
|
||||||
result.setSourceOrderItemId(sourceOrderItemId);
|
result.setSourceOrderItemId(sourceOrderItemId);
|
||||||
result.setQuantity(quantity);
|
result.setQuantity(quantity);
|
||||||
result.setBaseQuantity(quantity.multiply(baseQuantityPerUnitSnapshot).setScale(3));
|
result.setBaseQuantity(baseQuantity);
|
||||||
result.setReservedBefore(reservedBefore);
|
result.setReservedBaseBefore(reservedBaseBefore);
|
||||||
result.setReservedAfter(reservedBefore.add(quantity));
|
result.setReservedBaseAfter(reservedBaseBefore.add(baseQuantity));
|
||||||
result.setFulfilledBefore(fulfilledBefore);
|
result.setFulfilledBaseBefore(fulfilledBaseBefore);
|
||||||
result.setFulfilledAfter(fulfilledBefore);
|
result.setFulfilledBaseAfter(fulfilledBaseBefore);
|
||||||
result.setProductNameSnapshot(productNameSnapshot);
|
result.setProductNameSnapshot(productNameSnapshot);
|
||||||
result.setSkuCodeSnapshot(skuCodeSnapshot);
|
result.setSkuCodeSnapshot(skuCodeSnapshot);
|
||||||
result.setSkuNameSnapshot(skuNameSnapshot);
|
result.setSkuNameSnapshot(skuNameSnapshot);
|
||||||
|
|||||||
+45
-17
@@ -73,6 +73,9 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
|
|||||||
|
|
||||||
private static final BigDecimal ZERO = BigDecimal.ZERO;
|
private static final BigDecimal ZERO = BigDecimal.ZERO;
|
||||||
|
|
||||||
|
// 过渡兼容:旧事件登记系统已收款,但新订单可能仍为零有效收款,暂时允许店长发起履约;过渡结束改为 false 并删除此分支
|
||||||
|
private static final boolean ALLOW_STORE_MANAGER_ZERO_PAYMENT_FULFILLMENT = true;
|
||||||
|
|
||||||
private final GaoSalesOrderMapper mapper;
|
private final GaoSalesOrderMapper mapper;
|
||||||
private final GaoSalesOrderItemMapper itemMapper;
|
private final GaoSalesOrderItemMapper itemMapper;
|
||||||
private final GaoFundRecordService fundRecordService;
|
private final GaoFundRecordService fundRecordService;
|
||||||
@@ -280,18 +283,21 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
|
|||||||
if (safe(order.getSaleAmount()) == 0 || safe(order.getPaidAmount()) == safe(order.getSaleAmount())) {
|
if (safe(order.getSaleAmount()) == 0 || safe(order.getPaidAmount()) == safe(order.getSaleAmount())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
TimiException.requiredTrue(0 < safe(order.getPaidAmount()), "订单未有已核销收款,不能履约");
|
if (ALLOW_STORE_MANAGER_ZERO_PAYMENT_FULFILLMENT) {
|
||||||
roleChecker.checkAny(ModuleCode.GAO, GaoRoleCode.STORE_MANAGER.name());
|
roleChecker.checkAny(ModuleCode.GAO, GaoRoleCode.STORE_MANAGER.name());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
TimiException.requiredTrue(false, "订单未全额收款,不能发起履约");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int updateItemReservedQuantity(String itemId, BigDecimal delta) {
|
public int updateItemReservedBaseQuantity(String itemId, BigDecimal delta) {
|
||||||
return itemMapper.updateReservedQuantity(itemId, delta);
|
return itemMapper.updateReservedBaseQuantity(itemId, delta);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int updateItemFulfilledQuantity(String itemId, BigDecimal delta) {
|
public int updateItemFulfilledBaseQuantity(String itemId, BigDecimal delta) {
|
||||||
return itemMapper.updateFulfilledQuantity(itemId, delta);
|
return itemMapper.updateFulfilledBaseQuantity(itemId, delta);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -309,7 +315,7 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
|
|||||||
List<GaoSalesOrderItem> itemList = itemMapper.selectBySalesOrderId(order.getId());
|
List<GaoSalesOrderItem> itemList = itemMapper.selectBySalesOrderId(order.getId());
|
||||||
boolean fulfilled = !itemList.isEmpty();
|
boolean fulfilled = !itemList.isEmpty();
|
||||||
for (GaoSalesOrderItem item : itemList) {
|
for (GaoSalesOrderItem item : itemList) {
|
||||||
if (decimal(item.getFulfilledQuantity()).compareTo(decimal(item.getQuantity())) < 0) {
|
if (decimal(item.getFulfilledBaseQuantity()).compareTo(decimal(item.getBaseQuantity())) < 0) {
|
||||||
fulfilled = false;
|
fulfilled = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -383,8 +389,9 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
|
|||||||
item.setUnitSnapshot(unit.getName());
|
item.setUnitSnapshot(unit.getName());
|
||||||
item.setBaseQuantityPerUnitSnapshot(unit.getBaseQuantityPerUnit());
|
item.setBaseQuantityPerUnitSnapshot(unit.getBaseQuantityPerUnit());
|
||||||
item.setQuantity(quantity);
|
item.setQuantity(quantity);
|
||||||
item.setReservedQuantity(ZERO);
|
item.setBaseQuantity(toBaseQuantity(quantity, unit.getBaseQuantityPerUnit(), "订单项基础单位数量最多保留三位小数"));
|
||||||
item.setFulfilledQuantity(ZERO);
|
item.setReservedBaseQuantity(ZERO);
|
||||||
|
item.setFulfilledBaseQuantity(ZERO);
|
||||||
item.setUnitPrice(unitPrice);
|
item.setUnitPrice(unitPrice);
|
||||||
item.setLineAmount(lineAmount);
|
item.setLineAmount(lineAmount);
|
||||||
item.setSort(TimiJava.defaultIfNull(source.getSort(), i));
|
item.setSort(TimiJava.defaultIfNull(source.getSort(), i));
|
||||||
@@ -454,11 +461,14 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
|
|||||||
TimiException.requiredTrue(3 >= item.getQuantity().stripTrailingZeros().scale(), "订单项数量最多保留三位小数");
|
TimiException.requiredTrue(3 >= item.getQuantity().stripTrailingZeros().scale(), "订单项数量最多保留三位小数");
|
||||||
TimiException.required(item.getBaseQuantityPerUnitSnapshot(), "订单项单位换算快照不能为空");
|
TimiException.required(item.getBaseQuantityPerUnitSnapshot(), "订单项单位换算快照不能为空");
|
||||||
TimiException.requiredTrue(0 < item.getBaseQuantityPerUnitSnapshot().signum(), "订单项单位换算快照必须大于 0");
|
TimiException.requiredTrue(0 < item.getBaseQuantityPerUnitSnapshot().signum(), "订单项单位换算快照必须大于 0");
|
||||||
TimiException.requiredTrue(item.getReservedQuantity() != null && item.getFulfilledQuantity() != null,
|
TimiException.required(item.getBaseQuantity(), "订单项基础单位数量不能为空");
|
||||||
|
TimiException.requiredTrue(item.getBaseQuantity().compareTo(toBaseQuantity(item.getQuantity(), item.getBaseQuantityPerUnitSnapshot(), "订单项基础单位数量最多保留三位小数")) == 0,
|
||||||
|
"订单项基础单位数量与单位换算快照不一致");
|
||||||
|
TimiException.requiredTrue(item.getReservedBaseQuantity() != null && item.getFulfilledBaseQuantity() != null,
|
||||||
"订单项履约数量不能为空");
|
"订单项履约数量不能为空");
|
||||||
TimiException.requiredTrue(0 <= item.getReservedQuantity().signum() && 0 <= item.getFulfilledQuantity().signum(),
|
TimiException.requiredTrue(0 <= item.getReservedBaseQuantity().signum() && 0 <= item.getFulfilledBaseQuantity().signum(),
|
||||||
"订单项履约数量不能为负数");
|
"订单项履约数量不能为负数");
|
||||||
TimiException.requiredTrue(item.getReservedQuantity().add(item.getFulfilledQuantity()).compareTo(item.getQuantity()) <= 0,
|
TimiException.requiredTrue(item.getReservedBaseQuantity().add(item.getFulfilledBaseQuantity()).compareTo(item.getBaseQuantity()) <= 0,
|
||||||
"订单项履约数量不能超过购买数量");
|
"订单项履约数量不能超过购买数量");
|
||||||
if (expectedItemType == GaoSalesOrderItem.ItemType.PACKAGE_ENTITLEMENT) {
|
if (expectedItemType == GaoSalesOrderItem.ItemType.PACKAGE_ENTITLEMENT) {
|
||||||
TimiException.requiredNull(item.getUnitPrice(), "套餐订单项不能填写单价");
|
TimiException.requiredNull(item.getUnitPrice(), "套餐订单项不能填写单价");
|
||||||
@@ -544,12 +554,12 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
|
|||||||
///
|
///
|
||||||
/// @param item 订单项
|
/// @param item 订单项
|
||||||
private void fillItemBalance(GaoSalesOrderItem item) {
|
private void fillItemBalance(GaoSalesOrderItem item) {
|
||||||
BigDecimal quantity = decimal(item.getQuantity());
|
BigDecimal baseQuantity = decimal(item.getBaseQuantity());
|
||||||
BigDecimal reserved = decimal(item.getReservedQuantity());
|
BigDecimal reservedBaseQuantity = decimal(item.getReservedBaseQuantity());
|
||||||
BigDecimal fulfilled = decimal(item.getFulfilledQuantity());
|
BigDecimal fulfilledBaseQuantity = decimal(item.getFulfilledBaseQuantity());
|
||||||
item.setPendingQuantity(reserved);
|
item.setPendingBaseQuantity(reservedBaseQuantity);
|
||||||
item.setAvailableQuantity(quantity.subtract(reserved).subtract(fulfilled));
|
item.setAvailableBaseQuantity(baseQuantity.subtract(reservedBaseQuantity).subtract(fulfilledBaseQuantity));
|
||||||
item.setRemainingQuantity(quantity.subtract(fulfilled));
|
item.setRemainingBaseQuantity(baseQuantity.subtract(fulfilledBaseQuantity));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void fillItemAttachments(List<GaoSalesOrderItem> itemList) {
|
private void fillItemAttachments(List<GaoSalesOrderItem> itemList) {
|
||||||
@@ -606,6 +616,24 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
|
|||||||
return quantity;
|
return quantity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 转换基础单位数量
|
||||||
|
///
|
||||||
|
/// @param quantity 当前单位数量
|
||||||
|
/// @param baseQuantityPerUnit 当前单位折算基础单位系数
|
||||||
|
/// @param message 精度错误信息
|
||||||
|
/// @return 三位小数的基础单位数量
|
||||||
|
private BigDecimal toBaseQuantity(BigDecimal quantity, BigDecimal baseQuantityPerUnit, String message) {
|
||||||
|
TimiException.required(quantity, "数量不能为空");
|
||||||
|
TimiException.required(baseQuantityPerUnit, "基础单位数量系数不能为空");
|
||||||
|
BigDecimal result = quantity.multiply(baseQuantityPerUnit);
|
||||||
|
TimiException.requiredTrue(3 >= result.stripTrailingZeros().scale(), message);
|
||||||
|
try {
|
||||||
|
return result.setScale(3, RoundingMode.UNNECESSARY);
|
||||||
|
} catch (ArithmeticException e) {
|
||||||
|
throw new TimiException(TimiCode.ERROR, message, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private long toCentAmount(BigDecimal quantity, long unitPrice) {
|
private long toCentAmount(BigDecimal quantity, long unitPrice) {
|
||||||
try {
|
try {
|
||||||
return BigDecimal.valueOf(unitPrice).multiply(quantity).setScale(0, RoundingMode.UNNECESSARY).longValueExact();
|
return BigDecimal.valueOf(unitPrice).multiply(quantity).setScale(0, RoundingMode.UNNECESSARY).longValueExact();
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ public class GaoOrderFulfillmentItemRequest {
|
|||||||
/// 来源销售订单明细 ID
|
/// 来源销售订单明细 ID
|
||||||
private String sourceOrderItemId;
|
private String sourceOrderItemId;
|
||||||
|
|
||||||
|
/// 本次实际履约 SKU 单位 ID
|
||||||
|
private String skuUnitId;
|
||||||
|
|
||||||
/// 本次履约数量
|
/// 本次履约数量
|
||||||
private BigDecimal quantity;
|
private BigDecimal quantity;
|
||||||
}
|
}
|
||||||
|
|||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
-- 销售订单明细的履约计数统一改为基础单位,订单下单单位和数量仍保留用于展示
|
||||||
|
ALTER TABLE `gao_sales_order_item`
|
||||||
|
ADD COLUMN `base_quantity` DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT '购买基础单位数量' AFTER `quantity`,
|
||||||
|
ADD COLUMN `reserved_base_quantity` DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT '待履约基础单位数量' AFTER `base_quantity`,
|
||||||
|
ADD COLUMN `fulfilled_base_quantity` DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT '已履约基础单位数量' AFTER `reserved_base_quantity`;
|
||||||
|
|
||||||
|
UPDATE `gao_sales_order_item`
|
||||||
|
SET `base_quantity` = `quantity` * `base_quantity_per_unit_snapshot`,
|
||||||
|
`reserved_base_quantity` = `reserved_quantity` * `base_quantity_per_unit_snapshot`,
|
||||||
|
`fulfilled_base_quantity` = `fulfilled_quantity` * `base_quantity_per_unit_snapshot`;
|
||||||
|
|
||||||
|
ALTER TABLE `gao_sales_order_item`
|
||||||
|
DROP COLUMN `reserved_quantity`,
|
||||||
|
DROP COLUMN `fulfilled_quantity`;
|
||||||
|
|
||||||
|
-- 履约明细的前后值改名,明确其计量单位为基础单位
|
||||||
|
ALTER TABLE `gao_order_fulfillment_item`
|
||||||
|
CHANGE COLUMN `reserved_before` `reserved_base_before` DECIMAL(18,3) NOT NULL COMMENT '操作前待履约基础单位数量',
|
||||||
|
CHANGE COLUMN `reserved_after` `reserved_base_after` DECIMAL(18,3) NOT NULL COMMENT '操作后待履约基础单位数量',
|
||||||
|
CHANGE COLUMN `fulfilled_before` `fulfilled_base_before` DECIMAL(18,3) NOT NULL COMMENT '操作前已履约基础单位数量',
|
||||||
|
CHANGE COLUMN `fulfilled_after` `fulfilled_base_after` DECIMAL(18,3) NOT NULL COMMENT '操作后已履约基础单位数量';
|
||||||
|
|
||||||
|
-- 套餐配置继续使用 (package_config_id, sku_id, sku_unit_id) 唯一键:同 SKU 多单位合法,同 SKU 同单位重复由既有唯一键阻止
|
||||||
@@ -103,14 +103,14 @@
|
|||||||
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_base_quantity`), 0) AS `reservedQuantity`,
|
||||||
COALESCE(SUM(i.`fulfilled_quantity`), 0) AS `fulfilledQuantity`,
|
COALESCE(SUM(i.`fulfilled_base_quantity`), 0) AS `fulfilledQuantity`,
|
||||||
COALESCE(SUM(i.`quantity` - i.`reserved_quantity` - i.`fulfilled_quantity`), 0) AS `availableQuantity`,
|
COALESCE(SUM(i.`base_quantity` - i.`reserved_base_quantity` - i.`fulfilled_base_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` = 'DIRECT_PRODUCT' THEN i.`base_quantity` 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` = 'PACKAGE_ENTITLEMENT' THEN i.`base_quantity` 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.`reserved_base_quantity`), 0) AS `reservedBaseQuantity`,
|
||||||
COALESCE(SUM(i.`fulfilled_quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `fulfilledBaseQuantity`,
|
COALESCE(SUM(i.`fulfilled_base_quantity`), 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(i.`base_quantity` - i.`reserved_base_quantity` - i.`fulfilled_base_quantity`), 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_snapshot` 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`
|
||||||
|
|||||||
@@ -127,9 +127,9 @@
|
|||||||
COUNT(DISTINCT o.`id`) AS `orderCount`,
|
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` = '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` = '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.`reserved_base_quantity`), 0) AS `reservedBaseQuantity`,
|
||||||
COALESCE(SUM(i.`fulfilled_quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `fulfilledBaseQuantity`,
|
COALESCE(SUM(i.`fulfilled_base_quantity`), 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(i.`base_quantity` - i.`reserved_base_quantity` - i.`fulfilled_base_quantity`), 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_snapshot` 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`
|
||||||
|
|||||||
@@ -86,14 +86,14 @@
|
|||||||
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_base_quantity`), 0) AS `reservedQuantity`,
|
||||||
COALESCE(SUM(i.`fulfilled_quantity`), 0) AS `fulfilledQuantity`,
|
COALESCE(SUM(i.`fulfilled_base_quantity`), 0) AS `fulfilledQuantity`,
|
||||||
COALESCE(SUM(i.`quantity` - i.`reserved_quantity` - i.`fulfilled_quantity`), 0) AS `availableQuantity`,
|
COALESCE(SUM(i.`base_quantity` - i.`reserved_base_quantity` - i.`fulfilled_base_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` = 'DIRECT_PRODUCT' THEN i.`base_quantity` 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` = 'PACKAGE_ENTITLEMENT' THEN i.`base_quantity` 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.`reserved_base_quantity`), 0) AS `reservedBaseQuantity`,
|
||||||
COALESCE(SUM(i.`fulfilled_quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `fulfilledBaseQuantity`,
|
COALESCE(SUM(i.`fulfilled_base_quantity`), 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(i.`base_quantity` - i.`reserved_base_quantity` - i.`fulfilled_base_quantity`), 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`
|
||||||
@@ -124,11 +124,11 @@
|
|||||||
i.`product_id_snapshot` 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` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1) ELSE 0 END), 0) AS `directBaseQuantity`,
|
COALESCE(SUM(CASE WHEN i.`item_type` = 'DIRECT_PRODUCT' THEN i.`base_quantity` 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` = 'PACKAGE_ENTITLEMENT' THEN i.`base_quantity` 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.`reserved_base_quantity`), 0) AS `reservedBaseQuantity`,
|
||||||
COALESCE(SUM(i.`fulfilled_quantity` * COALESCE(i.`base_quantity_per_unit_snapshot`, 1)), 0) AS `fulfilledBaseQuantity`,
|
COALESCE(SUM(i.`fulfilled_base_quantity`), 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(i.`base_quantity` - i.`reserved_base_quantity` - i.`fulfilled_base_quantity`), 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`
|
||||||
|
|||||||
@@ -20,16 +20,16 @@
|
|||||||
WHERE `id` = #{id} AND `deleted_at` IS NULL
|
WHERE `id` = #{id} AND `deleted_at` IS NULL
|
||||||
FOR UPDATE
|
FOR UPDATE
|
||||||
</select>
|
</select>
|
||||||
<update id="updateReservedQuantity">
|
<update id="updateReservedBaseQuantity">
|
||||||
UPDATE `gao_sales_order_item`
|
UPDATE `gao_sales_order_item`
|
||||||
SET `reserved_quantity` = `reserved_quantity` + #{delta}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000)
|
SET `reserved_base_quantity` = `reserved_base_quantity` + #{delta}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000)
|
||||||
WHERE `id` = #{id} AND `reserved_quantity` + #{delta} >= 0
|
WHERE `id` = #{id} AND `reserved_base_quantity` + #{delta} >= 0
|
||||||
AND `fulfilled_quantity` + `reserved_quantity` + #{delta} <= `quantity`
|
AND `fulfilled_base_quantity` + `reserved_base_quantity` + #{delta} <= `base_quantity`
|
||||||
</update>
|
</update>
|
||||||
<update id="updateFulfilledQuantity">
|
<update id="updateFulfilledBaseQuantity">
|
||||||
UPDATE `gao_sales_order_item`
|
UPDATE `gao_sales_order_item`
|
||||||
SET `fulfilled_quantity` = `fulfilled_quantity` + #{delta}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000)
|
SET `fulfilled_base_quantity` = `fulfilled_base_quantity` + #{delta}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000)
|
||||||
WHERE `id` = #{id} AND `fulfilled_quantity` + #{delta} >= 0
|
WHERE `id` = #{id} AND `fulfilled_base_quantity` + #{delta} >= 0
|
||||||
AND `fulfilled_quantity` + #{delta} <= `quantity`
|
AND `fulfilled_base_quantity` + #{delta} <= `base_quantity`
|
||||||
</update>
|
</update>
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
Reference in New Issue
Block a user