套装书下单直接出库单品

This commit is contained in:
wyn
2026-08-04 13:42:47 +08:00
parent 6c86cc6a0d
commit 3ffa65863c
7 changed files with 246 additions and 113 deletions

View File

@@ -38,6 +38,14 @@ public interface ShopProductService extends IService<ShopProduct> {
PageUtils queryPageactivityprice(Map<String, Object> params); PageUtils queryPageactivityprice(Map<String, Object> params);
void rollbackStock(BuyOrder buyOrder); /**
* 回滚订单行 shop_product 库存与销量。
* @param skipShopProductIds 已由 inv 出库回滚加过 shop 的商品,跳过库存回加(仍回滚销量)
*/
void rollbackStock(BuyOrder buyOrder, java.util.Set<Integer> skipShopProductIds);
default void rollbackStock(BuyOrder buyOrder) {
rollbackStock(buyOrder, null);
}
} }

View File

@@ -1201,6 +1201,7 @@ public class BuyOrderServiceImpl extends ServiceImpl<BuyOrderDao, BuyOrder> impl
return refundFee; return refundFee;
} }
@Override @Override
@Transactional(rollbackFor = Exception.class)
public void refundOrder(BuyOrder buyOrder, MyUserEntity user, int refundId){ public void refundOrder(BuyOrder buyOrder, MyUserEntity user, int refundId){
if(buyOrder.getCouponId()!=null && buyOrder.getCouponId()!=0){ if(buyOrder.getCouponId()!=null && buyOrder.getCouponId()!=0){
couponService.rollbackCoupon(buyOrder.getCouponId()); couponService.rollbackCoupon(buyOrder.getCouponId());
@@ -1229,10 +1230,10 @@ public class BuyOrderServiceImpl extends ServiceImpl<BuyOrderDao, BuyOrder> impl
//撤回课程权限 //撤回课程权限
log.info("====remove========="+buyOrder.getOrderSn()); log.info("====remove========="+buyOrder.getOrderSn());
removeCourseToUser(buyOrder); removeCourseToUser(buyOrder);
//回滚库存 // 先按出库流水回加 inv remain + 出库商品 shop再回加订单行中未出现在出库流水的商品 shop如套书本身
shopProductService.rollbackStock(buyOrder); java.util.Set<Integer> invRestoredShopIds =
// 回滚 inv 售出流水:回加采购 remain_quantity售出记录 del_flag=1 inventoryManagementService.rollbackSaleByOrderId(buyOrder.getOrderId());
inventoryManagementService.rollbackSaleByOrderId(buyOrder.getOrderId()); shopProductService.rollbackStock(buyOrder, invRestoredShopIds);
} }
} }

View File

@@ -141,14 +141,23 @@ public class ShopProductServiceImpl extends ServiceImpl<ShopProductDao, ShopProd
} }
@Override @Override
public void rollbackStock(BuyOrder buyOrder){ public void rollbackStock(BuyOrder buyOrder, java.util.Set<Integer> skipShopProductIds){
QueryWrapper<BuyOrderProduct> buyOrderProductQueryWrapper = new QueryWrapper<>(); QueryWrapper<BuyOrderProduct> buyOrderProductQueryWrapper = new QueryWrapper<>();
buyOrderProductQueryWrapper.eq("order_id", buyOrder.getOrderId()); buyOrderProductQueryWrapper.eq("order_id", buyOrder.getOrderId());
List<BuyOrderProduct> buyOrderProductList = buyOrderProductService.list(buyOrderProductQueryWrapper); List<BuyOrderProduct> buyOrderProductList = buyOrderProductService.list(buyOrderProductQueryWrapper);
for (BuyOrderProduct buyOrderProduct : buyOrderProductList) { for (BuyOrderProduct buyOrderProduct : buyOrderProductList) {
Integer productId = buyOrderProduct.getProductId(); Integer productId = buyOrderProduct.getProductId();
ShopProduct product = this.getById(productId); ShopProduct product = this.getById(productId);
product.setProductStock(product.getProductStock() + buyOrderProduct.getQuantity()); if (product == null) {
continue;
}
// inv 出库回滚已加过 shop 的商品不再重复加库存;销量仍回滚
if (skipShopProductIds == null || !skipShopProductIds.contains(productId)) {
int stock = product.getProductStock() == null ? 0 : product.getProductStock();
product.setProductStock(stock + buyOrderProduct.getQuantity());
}
int sales = product.getSumSales() == null ? 0 : product.getSumSales();
product.setSumSales(Math.max(0, sales - buyOrderProduct.getQuantity()));
this.updateById(product); this.updateById(product);
} }
} }

View File

@@ -46,9 +46,10 @@ public class InventoryManagementController {
/** /**
* 采购/售出; * 采购/售出;
* 采购 sourcels灵枢 zm众秒 other其他 publisher出版社默认 other跨商户采购自动两步 * 采购 sourcels灵枢 zm众秒 other其他 publisher出版社默认 other跨商户采购自动两步套书不支持入库;
* 列表返回时 source 按 sys_dict_data(dict_label=inventory_source_type) 的 dict_type→dict_value 转换; * 列表返回时 source 按 sys_dict_data(dict_label=inventory_source_type) 的 dict_type→dict_value 转换;
* 售出出库类型outboundType 或 source取自 sys_dict_data(dict_label=inventory_outbound_type) 的 dict_type * 售出出库类型outboundType 或 source取自 sys_dict_data(dict_label=inventory_outbound_type) 的 dict_type
* 套书售出拆成单本出库
*/ */
@RequestMapping("/purchaseProduct") @RequestMapping("/purchaseProduct")
public R purchaseProduct(@RequestBody InvStockRecordDto dto) { public R purchaseProduct(@RequestBody InvStockRecordDto dto) {

View File

@@ -20,19 +20,21 @@ public interface InventoryManagementService {
/** /**
* 图书订单售出:写入 inv_stock_record 售出流水。 * 图书订单售出:写入 inv_stock_record 售出流水。
* 天医币支付固定扣众秒库存merchant_id=2其它支付按商品 payMerchant。 * 天医币支付固定扣众秒merchant_id=2其它支付严格按商品 payMerchant1→灵枢0→众秒不跨商户回退
* 套书shop_product_book 关联多本)拆成单本商品分别出库扣库存。
*/ */
void saleByBookOrder(Integer productId, int quantity, BuyOrder buyOrder, ShopProduct product); void saleByBookOrder(Integer productId, int quantity, BuyOrder buyOrder, ShopProduct product);
/** /**
* inv_stock_record 中是否存在该商品记录。 * inv_stock_record 中是否存在该商品记录。
* 天医币支付只查众秒merchant_id=2其它支付查对应商户无则任一商户有流水也算 * 天医币支付只查众秒merchant_id=2其它支付查对应商户无则任一商户有流水也算
* 套书查套内单本商品流水。
*/ */
boolean hasInvStockRecord(Integer productId, BuyOrder buyOrder, ShopProduct product); boolean hasInvStockRecord(Integer productId, BuyOrder buyOrder, ShopProduct product);
/** /**
* 校验图书库存流水:无记录则通过;有记录则剩余库存须不少于购买数量。 * 校验图书库存流水:无记录则通过;有记录则目标商户剩余须不少于购买数量。
* 天医币支付固定校验众秒库存。 * 天医币支付固定校验众秒库存。套书校验套内每本单品库存。
*/ */
boolean validateBookInvStock(Integer productId, int quantity, BuyOrder buyOrder, ShopProduct product); boolean validateBookInvStock(Integer productId, int quantity, BuyOrder buyOrder, ShopProduct product);
@@ -43,12 +45,14 @@ public interface InventoryManagementService {
List<Map<String, Object>> exportStockStatistics(Map<String, Object> params); List<Map<String, Object>> exportStockStatistics(Map<String, Object> params);
/** /**
* 按订单回滚库存流水售出:回加采购批次 remain_quantity售出记录 del_flag=1 * 按订单回滚出库:根据 order_id 查出全部售出流水,回加采购 remain
* 并按出库商品回加对应 shop_product返回已回加 shop 的商品ID集合供业务侧避免重复回加。
*/ */
void rollbackSaleByOrderId(Integer orderId); java.util.Set<Integer> rollbackSaleByOrderId(Integer orderId);
/** /**
* Excel 批量售出:全部解析/匹配成功才入库,任一条失败则全部不入库 * Excel 批量售出:全部解析/匹配成功才入库,任一条失败则全部不入库
* 套书行按 App 逻辑拆成单本出库。
* *
* @param merchantId 商户ID * @param merchantId 商户ID
* @param outboundType 出库类型 donation/taobao/app * @param outboundType 出库类型 donation/taobao/app

View File

@@ -2,6 +2,7 @@ package com.peanut.modules.master.service.impl;
import com.alipay.api.domain.Product; import com.alipay.api.domain.Product;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.yulichang.wrapper.MPJLambdaWrapper; import com.github.yulichang.wrapper.MPJLambdaWrapper;
@@ -228,6 +229,13 @@ public class InventoryManagementServiceImpl extends ServiceImpl<InvStockRecordDa
} }
if (dto.getBiz_type() == InvStockRecord.BIZ_TYPE_SALE) { if (dto.getBiz_type() == InvStockRecord.BIZ_TYPE_SALE) {
String outboundType = resolveOutboundType(dto.getOutboundType()); String outboundType = resolveOutboundType(dto.getOutboundType());
ShopProduct saleProduct = shopProductDao.selectById(dto.getProductId());
if (isSetBookProduct(saleProduct)) {
// 套书拆单出库:成本取各单本 FIFO 批次,不用请求里的套书 unitCost
saleSetBook(dto.getMerchantId(), dto.getQuantity(), saleProduct, outboundType,
null, dto.getRemark(), null, null, null);
return;
}
List<CostSplit> splits = deductPurchaseStockFifo(dto.getProductId(), dto.getMerchantId(), dto.getQuantity()); List<CostSplit> splits = deductPurchaseStockFifo(dto.getProductId(), dto.getMerchantId(), dto.getQuantity());
for (CostSplit split : splits) { for (CostSplit split : splits) {
BigDecimal saleUnitCost = (dto.getUnitCost() != null && dto.getUnitCost().compareTo(BigDecimal.ZERO) > 0) BigDecimal saleUnitCost = (dto.getUnitCost() != null && dto.getUnitCost().compareTo(BigDecimal.ZERO) > 0)
@@ -244,6 +252,11 @@ public class InventoryManagementServiceImpl extends ServiceImpl<InvStockRecordDa
throw new RuntimeException("单位成本必须大于0"); throw new RuntimeException("单位成本必须大于0");
} }
ShopProduct product = shopProductDao.selectById(dto.getProductId());
if (isSetBookProduct(product)) {
throw new RuntimeException("套书不支持入库");
}
// 采购:跨商户调拨 = 来源方按成本批次多条出库 + 本商户一条采购(成本取接口传值) // 采购:跨商户调拨 = 来源方按成本批次多条出库 + 本商户一条采购(成本取接口传值)
// 灵枢→众秒:灵枢出库 outbound_type=zm众秒→灵枢众秒出库 outbound_type=ls // 灵枢→众秒:灵枢出库 outbound_type=zm众秒→灵枢众秒出库 outbound_type=ls
if (isCrossMerchantTransfer(dto.getMerchantId(), dto.getSource())) { if (isCrossMerchantTransfer(dto.getMerchantId(), dto.getSource())) {
@@ -261,47 +274,11 @@ public class InventoryManagementServiceImpl extends ServiceImpl<InvStockRecordDa
} }
// 普通采购source=other 等) // 普通采购source=other 等)
ShopProduct product = shopProductDao.selectById(dto.getProductId());
if (isSetBookProduct(product)) {
purchaseSetBook(dto, product);
return;
}
insertRecord(dto.getProductId(), dto.getMerchantId(), normalizeSource(dto.getSource()), insertRecord(dto.getProductId(), dto.getMerchantId(), normalizeSource(dto.getSource()),
InvStockRecord.BIZ_TYPE_PURCHASE, dto.getQuantity(), dto.getUnitCost(), dto.getRemark()); InvStockRecord.BIZ_TYPE_PURCHASE, dto.getQuantity(), dto.getUnitCost(), dto.getRemark());
adjustShopProductStock(dto.getProductId(), dto.getQuantity()); adjustShopProductStock(dto.getProductId(), dto.getQuantity());
} }
/**
* 套装书采购:套书库存增加,套内每本单品按当前商户 FIFO 出库并扣减 shop_product 库存。
* 套内图书从 shop_product_book 按套书 product_id 关联查询。
*/
private void purchaseSetBook(InvStockRecordDto dto, ShopProduct setProduct) {
List<Integer> bookIds = listBookIdsByProductId(setProduct.getProductId());
if (bookIds.isEmpty()) {
throw new RuntimeException("套书未在shop_product_book中关联图书");
}
int merchantId = dto.getMerchantId();
int quantity = dto.getQuantity();
validateSetBookComponentStock(bookIds, merchantId, quantity, setProduct.getProductId());
String saleSource = merchantIdToSource(merchantId);
String outboundRemark = "套书采购出库[" + setProduct.getProductName() + ",productId=" + setProduct.getProductId() + "]";
Long setPurchaseRecordId = insertRecord(dto.getProductId(), merchantId, normalizeSource(dto.getSource()),
null, InvStockRecord.BIZ_TYPE_PURCHASE, quantity, dto.getUnitCost(), dto.getRemark(),
null, null, null, null, null);
for (Integer bookId : bookIds) {
ShopProduct component = findComponentProductByBookId(bookId, setProduct.getProductId());
List<CostSplit> splits = deductPurchaseStockFifo(component.getProductId(), merchantId, quantity);
for (CostSplit split : splits) {
insertRecord(component.getProductId(), merchantId, saleSource, InvStockRecord.OUTBOUND_BOOKS,
InvStockRecord.BIZ_TYPE_SALE, -split.quantity, split.unitCost, outboundRemark,
null, null, null, split.purchaseRecordId, setPurchaseRecordId);
}
adjustShopProductStock(component.getProductId(), -quantity);
}
adjustShopProductStock(dto.getProductId(), quantity);
}
/** 套书判定shop_product_book 中关联图书数 > 1 */ /** 套书判定shop_product_book 中关联图书数 > 1 */
private boolean isSetBookProduct(ShopProduct product) { private boolean isSetBookProduct(ShopProduct product) {
if (product == null || product.getProductId() == null) { if (product == null || product.getProductId() == null) {
@@ -346,7 +323,7 @@ public class InventoryManagementServiceImpl extends ServiceImpl<InvStockRecordDa
if (candidateId == null) { if (candidateId == null) {
continue; continue;
} }
// 同一 book_id 可能关联套书与单品,优先取 shop_product_book 中仅关联 1 本书的单品 product_id // 必须是 shop_product_book 中只绑定了 1 本书的单品 product_id
Long bookCount = shopProductBookDao.selectCount( Long bookCount = shopProductBookDao.selectCount(
new LambdaQueryWrapper<ShopProductBookEntity>() new LambdaQueryWrapper<ShopProductBookEntity>()
.eq(ShopProductBookEntity::getProductId, candidateId) .eq(ShopProductBookEntity::getProductId, candidateId)
@@ -355,9 +332,6 @@ public class InventoryManagementServiceImpl extends ServiceImpl<InvStockRecordDa
componentProductId = candidateId; componentProductId = candidateId;
break; break;
} }
if (componentProductId == null) {
componentProductId = candidateId;
}
} }
if (componentProductId == null) { if (componentProductId == null) {
return null; return null;
@@ -431,7 +405,13 @@ public class InventoryManagementServiceImpl extends ServiceImpl<InvStockRecordDa
if (quantity <= 0) { if (quantity <= 0) {
throw new RuntimeException("售出数量必须大于0"); throw new RuntimeException("售出数量必须大于0");
} }
int merchantId = resolveBookOrderMerchantId(productId, buyOrder, product, quantity); int merchantId = resolveBookOrderMerchantId(buyOrder, product);
if (isSetBookProduct(product)) {
saleSetBook(merchantId, quantity, product, InvStockRecord.OUTBOUND_APP,
null, "套书订单售出[" + product.getProductName() + ",productId=" + productId + "]",
buyOrder, null, null);
return;
}
List<CostSplit> splits = deductPurchaseStockFifo(productId, merchantId, quantity); List<CostSplit> splits = deductPurchaseStockFifo(productId, merchantId, quantity);
for (CostSplit split : splits) { for (CostSplit split : splits) {
@@ -440,11 +420,45 @@ public class InventoryManagementServiceImpl extends ServiceImpl<InvStockRecordDa
} }
} }
/**
* 套书出库:按 shop_product_book 拆成单本商品,分别 FIFO 扣库存并写售出流水。
* App 订单、手工出库、批量出库共用。
*/
private void saleSetBook(Integer merchantId, int quantity, ShopProduct setProduct, String outboundType,
BigDecimal overrideUnitCost, String remark, BuyOrder buyOrder,
Date createTime, String orderSn) {
Integer setProductId = setProduct.getProductId();
List<Integer> bookIds = listBookIdsByProductId(setProductId);
if (bookIds.isEmpty()) {
throw new RuntimeException("套书未在shop_product_book中关联图书");
}
validateSetBookComponentStock(bookIds, merchantId, quantity, setProductId);
String outboundRemark = (remark == null || remark.isBlank())
? "套书出库[" + setProduct.getProductName() + ",productId=" + setProductId + "]"
: remark;
for (Integer bookId : bookIds) {
ShopProduct component = findComponentProductByBookId(bookId, setProductId);
List<CostSplit> splits = deductPurchaseStockFifo(component.getProductId(), merchantId, quantity);
for (CostSplit split : splits) {
BigDecimal saleUnitCost = (overrideUnitCost != null && overrideUnitCost.compareTo(BigDecimal.ZERO) > 0)
? overrideUnitCost : split.unitCost;
insertSaleRecord(component.getProductId(), merchantId, outboundType,
-split.quantity, saleUnitCost, outboundRemark, buyOrder, createTime, orderSn,
split.purchaseRecordId);
}
adjustShopProductStock(component.getProductId(), -quantity);
}
}
/**
* 按订单回滚出库:出库记录有哪些商品,就回加哪些商品的 remain 与 shop_product。
* @return 已回加 shop 的商品ID供取消/退款跳过订单行重复回加
*/
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void rollbackSaleByOrderId(Integer orderId) { public Set<Integer> rollbackSaleByOrderId(Integer orderId) {
if (orderId == null) { if (orderId == null) {
return; return Collections.emptySet();
} }
List<InvStockRecord> saleRecords = invStockRecordDao.selectList( List<InvStockRecord> saleRecords = invStockRecordDao.selectList(
new LambdaQueryWrapper<InvStockRecord>() new LambdaQueryWrapper<InvStockRecord>()
@@ -453,43 +467,69 @@ public class InventoryManagementServiceImpl extends ServiceImpl<InvStockRecordDa
.orderByDesc(InvStockRecord::getId) .orderByDesc(InvStockRecord::getId)
); );
if (saleRecords == null || saleRecords.isEmpty()) { if (saleRecords == null || saleRecords.isEmpty()) {
return; return Collections.emptySet();
} }
Map<Integer, Integer> shopRestoreMap = new LinkedHashMap<>();
// 先全部回加 remain失败则整单回滚避免只软删出库流水、库存没加回
for (InvStockRecord sale : saleRecords) { for (InvStockRecord sale : saleRecords) {
int restoreQty = sale.getQuantity() == null ? 0 : Math.abs(sale.getQuantity()); int restoreQty = sale.getQuantity() == null ? 0 : Math.abs(sale.getQuantity());
if (restoreQty > 0) { if (restoreQty <= 0) {
if (sale.getPurchaseRecordId() != null) { continue;
restorePurchaseRemainById(sale.getPurchaseRecordId(), restoreQty);
} else {
restorePurchaseRemain(sale.getProductId(), sale.getMerchantId(), sale.getUnitCost(), restoreQty);
}
} }
// @TableLogicdeleteById 将 del_flag 置为 1 if (sale.getPurchaseRecordId() != null) {
restorePurchaseRemainById(sale.getPurchaseRecordId(), restoreQty);
} else {
restorePurchaseRemain(sale.getProductId(), sale.getMerchantId(), sale.getUnitCost(), restoreQty);
}
Integer saleProductId = sale.getProductId();
if (saleProductId != null) {
shopRestoreMap.merge(saleProductId, restoreQty, Integer::sum);
}
}
for (InvStockRecord sale : saleRecords) {
invStockRecordDao.deleteById(sale.getId()); invStockRecordDao.deleteById(sale.getId());
} }
for (Map.Entry<Integer, Integer> entry : shopRestoreMap.entrySet()) {
adjustShopProductStock(entry.getKey(), entry.getValue());
}
return shopRestoreMap.keySet();
} }
/** 按采购流水ID精确回加 remain_quantity(不超过该批次原始 quantity */ /** 按采购流水ID原子回加 remain_quantity;失败抛错,禁止静默跳过 */
private void restorePurchaseRemainById(Long purchaseRecordId, int restoreQty) { private void restorePurchaseRemainById(Long purchaseRecordId, int restoreQty) {
if (purchaseRecordId == null || restoreQty <= 0) { if (purchaseRecordId == null || restoreQty <= 0) {
throw new RuntimeException("回滚库存参数无效purchaseRecordId=" + purchaseRecordId + ", qty=" + restoreQty);
}
// 原子加回,避免 select+updateById 静默不生效
Integer rows = invStockRecordDao.update(null,
new LambdaUpdateWrapper<InvStockRecord>()
.eq(InvStockRecord::getId, purchaseRecordId)
.eq(InvStockRecord::getBizType, InvStockRecord.BIZ_TYPE_PURCHASE)
.apply("IFNULL(remain_quantity,0) + {0} <= IFNULL(quantity,0)", restoreQty)
.setSql("remain_quantity = IFNULL(remain_quantity,0) + " + restoreQty));
if (rows != null && rows > 0) {
return; return;
} }
InvStockRecord purchase = invStockRecordDao.selectById(purchaseRecordId); InvStockRecord purchase = invStockRecordDao.selectById(purchaseRecordId);
if (purchase == null || purchase.getBizType() == null if (purchase == null || purchase.getBizType() == null
|| purchase.getBizType() != InvStockRecord.BIZ_TYPE_PURCHASE) { || !Integer.valueOf(InvStockRecord.BIZ_TYPE_PURCHASE).equals(purchase.getBizType())) {
return; throw new RuntimeException("回滚库存失败采购流水不存在或类型错误purchaseRecordId=" + purchaseRecordId);
} }
int originQty = purchase.getQuantity() == null ? 0 : purchase.getQuantity(); int originQty = purchase.getQuantity() == null ? 0 : purchase.getQuantity();
int remainQty = purchase.getRemainQuantity() == null ? 0 : purchase.getRemainQuantity(); int remainQty = purchase.getRemainQuantity() == null ? 0 : purchase.getRemainQuantity();
int capacity = Math.max(0, originQty - remainQty); int capacity = Math.max(0, originQty - remainQty);
int add = Math.min(capacity, restoreQty); int add = Math.min(capacity, restoreQty);
if (add <= 0) { if (add > 0) {
return; Integer updated = invStockRecordDao.update(null,
new LambdaUpdateWrapper<InvStockRecord>()
.eq(InvStockRecord::getId, purchaseRecordId)
.eq(InvStockRecord::getBizType, InvStockRecord.BIZ_TYPE_PURCHASE)
.setSql("remain_quantity = IFNULL(remain_quantity,0) + " + add));
if (updated == null || updated <= 0) {
throw new RuntimeException("回滚库存失败更新采购剩余未生效purchaseRecordId=" + purchaseRecordId);
}
} }
purchase.setRemainQuantity(remainQty + add);
invStockRecordDao.updateById(purchase);
if (add < restoreQty) { if (add < restoreQty) {
// 容量不够时按旧逻辑兜底补足(兼容脏数据)
restorePurchaseRemain(purchase.getProductId(), purchase.getMerchantId(), restorePurchaseRemain(purchase.getProductId(), purchase.getMerchantId(),
purchase.getUnitCost(), restoreQty - add); purchase.getUnitCost(), restoreQty - add);
} }
@@ -499,9 +539,12 @@ public class InventoryManagementServiceImpl extends ServiceImpl<InvStockRecordDa
* 按成本单价逆 FIFO 回加采购批次剩余库存(不超过该批次原始 quantity * 按成本单价逆 FIFO 回加采购批次剩余库存(不超过该批次原始 quantity
*/ */
private void restorePurchaseRemain(Integer productId, Integer merchantId, BigDecimal unitCost, int restoreQty) { private void restorePurchaseRemain(Integer productId, Integer merchantId, BigDecimal unitCost, int restoreQty) {
if (productId == null || merchantId == null || restoreQty <= 0) { if (restoreQty <= 0) {
return; return;
} }
if (productId == null || merchantId == null) {
throw new RuntimeException("回滚库存失败缺少商品或商户productId=" + productId + ", merchantId=" + merchantId);
}
LambdaQueryWrapper<InvStockRecord> wrapper = new LambdaQueryWrapper<InvStockRecord>() LambdaQueryWrapper<InvStockRecord> wrapper = new LambdaQueryWrapper<InvStockRecord>()
.eq(InvStockRecord::getProductId, productId) .eq(InvStockRecord::getProductId, productId)
.eq(InvStockRecord::getMerchantId, merchantId) .eq(InvStockRecord::getMerchantId, merchantId)
@@ -523,8 +566,15 @@ public class InventoryManagementServiceImpl extends ServiceImpl<InvStockRecordDa
continue; continue;
} }
int add = Math.min(capacity, remaining); int add = Math.min(capacity, remaining);
purchase.setRemainQuantity(remainQty + add); Integer updated = invStockRecordDao.update(null,
invStockRecordDao.updateById(purchase); new LambdaUpdateWrapper<InvStockRecord>()
.eq(InvStockRecord::getId, purchase.getId())
.eq(InvStockRecord::getBizType, InvStockRecord.BIZ_TYPE_PURCHASE)
.apply("IFNULL(remain_quantity,0) + {0} <= IFNULL(quantity,0)", add)
.setSql("remain_quantity = IFNULL(remain_quantity,0) + " + add));
if (updated == null || updated <= 0) {
continue;
}
remaining -= add; remaining -= add;
} }
if (remaining > 0 && unitCost != null) { if (remaining > 0 && unitCost != null) {
@@ -541,6 +591,9 @@ public class InventoryManagementServiceImpl extends ServiceImpl<InvStockRecordDa
if (productId == null) { if (productId == null) {
return false; return false;
} }
if (isSetBookProduct(product)) {
return hasSetBookComponentInvRecord(productId, buyOrder);
}
if (isPeanutCoinPay(buyOrder)) { if (isPeanutCoinPay(buyOrder)) {
return hasInvOnMerchant(productId, InvStockRecord.MERCHANT_ZM); return hasInvOnMerchant(productId, InvStockRecord.MERCHANT_ZM);
} }
@@ -551,12 +604,50 @@ public class InventoryManagementServiceImpl extends ServiceImpl<InvStockRecordDa
return count != null && count > 0; return count != null && count > 0;
} }
/** 套书:任一单本组件有流水即进入 inv 校验(天医币只认众秒) */
private boolean hasSetBookComponentInvRecord(Integer setProductId, BuyOrder buyOrder) {
List<Integer> bookIds = listBookIdsByProductId(setProductId);
if (bookIds.isEmpty()) {
return false;
}
boolean peanut = isPeanutCoinPay(buyOrder);
for (Integer bookId : bookIds) {
ShopProduct component = findComponentProductByBookId(bookId, setProductId);
if (component == null || component.getProductId() == null) {
continue;
}
if (peanut) {
if (hasInvOnMerchant(component.getProductId(), InvStockRecord.MERCHANT_ZM)) {
return true;
}
} else {
Long count = invStockRecordDao.selectCount(
new LambdaQueryWrapper<InvStockRecord>()
.eq(InvStockRecord::getProductId, component.getProductId())
);
if (count != null && count > 0) {
return true;
}
}
}
return false;
}
@Override @Override
public boolean validateBookInvStock(Integer productId, int quantity, BuyOrder buyOrder, ShopProduct product) { public boolean validateBookInvStock(Integer productId, int quantity, BuyOrder buyOrder, ShopProduct product) {
if (!hasInvStockRecord(productId, buyOrder, product)) { if (!hasInvStockRecord(productId, buyOrder, product)) {
return true; return true;
} }
int merchantId = resolveBookOrderMerchantId(productId, buyOrder, product, quantity); int merchantId = resolveBookOrderMerchantId(buyOrder, product);
if (isSetBookProduct(product)) {
List<Integer> bookIds = listBookIdsByProductId(productId);
try {
validateSetBookComponentStock(bookIds, merchantId, quantity, productId);
return true;
} catch (RuntimeException ex) {
return false;
}
}
int remain = sumRemainStockByMerchant(productId, merchantId); int remain = sumRemainStockByMerchant(productId, merchantId);
return remain > 0 && remain >= quantity; return remain > 0 && remain >= quantity;
} }
@@ -566,35 +657,16 @@ public class InventoryManagementServiceImpl extends ServiceImpl<InvStockRecordDa
return buyOrder != null && Constants.PAYMENT_METHOD_VIRTUAL.equals(buyOrder.getPaymentMethod()); return buyOrder != null && Constants.PAYMENT_METHOD_VIRTUAL.equals(buyOrder.getPaymentMethod());
} }
private int resolveBookOrderMerchantId(Integer productId, BuyOrder buyOrder, ShopProduct product, int needQty) { /**
* 天医币固定众秒;其它支付严格按 payMerchant 映射,不因目标商户剩余为 0 而改扣另一商户。
* payMerchant=1 → 灵枢;否则 → 众秒。
*/
private int resolveBookOrderMerchantId(BuyOrder buyOrder, ShopProduct product) {
if (isPeanutCoinPay(buyOrder)) { if (isPeanutCoinPay(buyOrder)) {
return InvStockRecord.MERCHANT_ZM; return InvStockRecord.MERCHANT_ZM;
} }
int payMerchant = product == null || product.getPayMerchant() == null ? 0 : product.getPayMerchant(); int payMerchant = product == null || product.getPayMerchant() == null ? 0 : product.getPayMerchant();
return resolveInvMerchantId(productId, payMerchant, needQty); return payMerchant == 1 ? InvStockRecord.MERCHANT_LS : InvStockRecord.MERCHANT_ZM;
}
/**
* 优先按 payMerchant 对应商户扣库存;该商户剩余不足时,回退到另一商户(兼容入库商户与 pay_merchant 不一致)。
*/
private int resolveInvMerchantId(Integer productId, int payMerchant, int needQty) {
int preferred = payMerchant == 1 ? InvStockRecord.MERCHANT_LS : InvStockRecord.MERCHANT_ZM;
int other = preferred == InvStockRecord.MERCHANT_LS ? InvStockRecord.MERCHANT_ZM : InvStockRecord.MERCHANT_LS;
int preferredRemain = sumRemainStockByMerchant(productId, preferred);
if (preferredRemain >= needQty) {
return preferred;
}
int otherRemain = sumRemainStockByMerchant(productId, other);
if (otherRemain >= needQty) {
return other;
}
if (preferredRemain > 0 || hasInvOnMerchant(productId, preferred)) {
return preferred;
}
if (otherRemain > 0 || hasInvOnMerchant(productId, other)) {
return other;
}
return preferred;
} }
private boolean hasInvOnMerchant(Integer productId, int merchantId) { private boolean hasInvOnMerchant(Integer productId, int merchantId) {
@@ -874,11 +946,39 @@ public class InventoryManagementServiceImpl extends ServiceImpl<InvStockRecordDa
Map<Integer, Integer> needQtyMap = new LinkedHashMap<>(); Map<Integer, Integer> needQtyMap = new LinkedHashMap<>();
Map<Integer, String> productNameMap = new HashMap<>(); Map<Integer, String> productNameMap = new HashMap<>();
for (BatchSaleRow row : rows) { for (BatchSaleRow row : rows) {
needQtyMap.merge(row.productId, row.quantity, Integer::sum); ShopProduct matched = shopProductDao.selectById(row.productId);
if (row.matchedName != null && !row.matchedName.isBlank()) { if (isSetBookProduct(matched)) {
productNameMap.putIfAbsent(row.productId, row.matchedName); List<Integer> bookIds = listBookIdsByProductId(row.productId);
if (bookIds.isEmpty()) {
errors.add("" + row.excelRowNum + "套书未在shop_product_book中关联图书productId=" + row.productId);
continue;
}
for (Integer bookId : bookIds) {
ShopProduct component = findComponentProductByBookId(bookId, row.productId);
if (component == null || component.getProductId() == null) {
String bookName = resolveBookName(bookId);
String bookPart = bookName.isBlank()
? "书籍ID[" + bookId + "]"
: "" + bookName + "》(书籍ID:" + bookId + ")";
errors.add("" + row.excelRowNum + "行:套书拆分失败," + bookPart + "无只绑1本的单品商品");
continue;
}
needQtyMap.merge(component.getProductId(), row.quantity, Integer::sum);
String cName = component.getProductName();
if (cName != null && !cName.isBlank()) {
productNameMap.putIfAbsent(component.getProductId(), cName);
}
}
} else {
needQtyMap.merge(row.productId, row.quantity, Integer::sum);
if (row.matchedName != null && !row.matchedName.isBlank()) {
productNameMap.putIfAbsent(row.productId, row.matchedName);
}
} }
} }
if (!errors.isEmpty()) {
throw new RuntimeException("全部未入库:" + String.join("", errors));
}
for (Map.Entry<Integer, Integer> entry : needQtyMap.entrySet()) { for (Map.Entry<Integer, Integer> entry : needQtyMap.entrySet()) {
int remain = sumRemainStockByMerchant(entry.getKey(), merchantId); int remain = sumRemainStockByMerchant(entry.getKey(), merchantId);
if (remain < entry.getValue()) { if (remain < entry.getValue()) {
@@ -892,8 +992,14 @@ public class InventoryManagementServiceImpl extends ServiceImpl<InvStockRecordDa
} }
for (BatchSaleRow row : rows) { for (BatchSaleRow row : rows) {
List<CostSplit> splits = deductPurchaseStockFifo(row.productId, merchantId, row.quantity); ShopProduct matched = shopProductDao.selectById(row.productId);
String remark = "批量导入售出 orderSn=" + row.orderSn; String remark = "批量导入售出 orderSn=" + row.orderSn;
if (isSetBookProduct(matched)) {
saleSetBook(merchantId, row.quantity, matched, normalizedOutbound,
null, remark, null, row.createTime, row.orderSn);
continue;
}
List<CostSplit> splits = deductPurchaseStockFifo(row.productId, merchantId, row.quantity);
for (CostSplit split : splits) { for (CostSplit split : splits) {
insertSaleRecord(row.productId, merchantId, normalizedOutbound, insertSaleRecord(row.productId, merchantId, normalizedOutbound,
-split.quantity, split.unitCost, remark, null, row.createTime, row.orderSn, split.purchaseRecordId); -split.quantity, split.unitCost, remark, null, row.createTime, row.orderSn, split.purchaseRecordId);
@@ -1254,7 +1360,7 @@ public class InventoryManagementServiceImpl extends ServiceImpl<InvStockRecordDa
if (component == null || component.getProductId() == null) { if (component == null || component.getProductId() == null) {
continue; continue;
} }
// findComponentProductByBookId 已优先只绑 1 本书;再兜底确认 // findComponentProductByBookId 已要求只绑 1 本书;再兜底确认
if (listBookIdsByProductId(component.getProductId()).size() != 1) { if (listBookIdsByProductId(component.getProductId()).size() != 1) {
continue; continue;
} }

View File

@@ -74,23 +74,27 @@ public class OrderCancelConsumer {
couponService.rollbackCoupon(couponHistoryId); couponService.rollbackCoupon(couponHistoryId);
} }
// 回滚商品库存与销量 // 先按出库流水回加 inv remain + 出库商品 shop_product
java.util.Set<Integer> invRestoredShopIds =
inventoryManagementService.rollbackSaleByOrderId(buyOrder.getOrderId());
// 回滚订单行 shop出库流水已回加过的商品只回销量不再加库存单本避免双加套书订单行仍会加回套书库存
LambdaQueryWrapper<BuyOrderProduct> wrapper = new LambdaQueryWrapper<>(); LambdaQueryWrapper<BuyOrderProduct> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(BuyOrderProduct::getOrderId, buyOrder.getOrderId()); wrapper.eq(BuyOrderProduct::getOrderId, buyOrder.getOrderId());
List<BuyOrderProduct> buyOrderProducts = buyOrderProductDao.selectList(wrapper); List<BuyOrderProduct> buyOrderProducts = buyOrderProductDao.selectList(wrapper);
for (BuyOrderProduct b : buyOrderProducts) { for (BuyOrderProduct b : buyOrderProducts) {
ShopProduct shopProduct = shopProductDao.selectById(b.getProductId()); ShopProduct shopProduct = shopProductDao.selectById(b.getProductId());
if (shopProduct != null) { if (shopProduct != null) {
int stock = shopProduct.getProductStock() == null ? 0 : shopProduct.getProductStock();
int sales = shopProduct.getSumSales() == null ? 0 : shopProduct.getSumSales(); int sales = shopProduct.getSumSales() == null ? 0 : shopProduct.getSumSales();
int qty = b.getQuantity(); int qty = b.getQuantity();
shopProduct.setProductStock(stock + qty); if (invRestoredShopIds == null || !invRestoredShopIds.contains(b.getProductId())) {
int stock = shopProduct.getProductStock() == null ? 0 : shopProduct.getProductStock();
shopProduct.setProductStock(stock + qty);
}
shopProduct.setSumSales(Math.max(0, sales - qty)); shopProduct.setSumSales(Math.max(0, sales - qty));
shopProductDao.updateById(shopProduct); shopProductDao.updateById(shopProduct);
} }
} }
// 回滚 inv 售出流水:回加采购 remain_quantity售出记录 del_flag=1
inventoryManagementService.rollbackSaleByOrderId(buyOrder.getOrderId());
buyOrderService.updateById(buyOrder); buyOrderService.updateById(buyOrder);
//buyOrderService.removeById(buyOrder); //buyOrderService.removeById(buyOrder);