feat(order): 实现付款减库存功能并优化订单支付状态同步

- 在GltTicketOrder实体中新增orderNo字段用于订单编号关联
- 在订单查询SQL中关联glt_user_ticket表获取订单编号
- 新增DEDUCT_STOCK_TYPE_ORDER和DEDUCT_STOCK_TYPE_PAY常量定义库存扣除时机
- 实现下单时跳过付款减库存商品的库存扣除逻辑
- 实现订单取消时跳过付款减库存商品的库存回退逻辑
- 在ShopGoods实体中添加deductStockType字段支持库存计算方式配置
- 通过@JsonAlias注解支持前端字段别名映射
- 实现支付成功后触发付款减库存的库存扣除逻辑
- 添加deductStockAfterPaidIfNeeded方法处理支付后库存扣除
- 优化syncPaymentStatus方法确保支付状态同步时触发相关业务逻辑
- 添加重复支付状态检查避免重复执行支付成功业务逻辑
- 实现租户隔离的库存扣除操作和异常兜底机制
This commit is contained in:
2026-02-10 17:05:57 +08:00
parent 1177730464
commit e1ef21f140
6 changed files with 165 additions and 36 deletions

View File

@@ -31,6 +31,10 @@ public class GltTicketOrder implements Serializable {
@Schema(description = "用户水票ID")
private Integer userTicketId;
@Schema(description = "订单编号")
@TableField(exist = false)
private String orderNo;
@Schema(description = "门店ID")
private Integer storeId;

View File

@@ -10,13 +10,15 @@
u.nickname, u.phone, u.avatar,
d.name as receiverName, d.phone as receiverPhone,
d.province as receiverProvince, d.city as receiverCity, d.region as receiverRegion,
d.address as receiverAddress, d.full_address as receiverFullAddress, d.lat as receiverLat, d.lng as receiverLng
d.address as receiverAddress, d.full_address as receiverFullAddress, d.lat as receiverLat, d.lng as receiverLng,
f.order_no as orderNo
FROM glt_ticket_order a
LEFT JOIN shop_store b ON a.store_id = b.id
LEFT JOIN shop_store_warehouse w ON a.warehouse_id = w.id
LEFT JOIN shop_store_rider c ON a.rider_id = c.user_id
LEFT JOIN gxwebsoft_core.sys_user u ON a.user_id = u.user_id
LEFT JOIN shop_user_address d ON a.address_id = d.id
LEFT JOIN glt_user_ticket f ON a.user_ticket_id = f.id
<where>
<if test="param.id != null">

View File

@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDateTime;
import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonFormat;
import java.io.Serializable;
import io.swagger.v3.oas.annotations.media.Schema;
@@ -101,6 +102,7 @@ public class ShopGoods implements Serializable {
private BigDecimal secondDividend;
@Schema(description = "库存计算方式(10下单减库存 20付款减库存)")
@JsonAlias({"cdeductStockType"})
private Integer deductStockType;
@Schema(description = "交付方式(0不启用)")

View File

@@ -32,6 +32,8 @@ import java.util.Map;
@Slf4j
@Service
public class OrderBusinessService {
private static final int DEDUCT_STOCK_TYPE_ORDER = 10; // 下单减库存
private static final int DEDUCT_STOCK_TYPE_PAY = 20; // 付款减库存
@Resource
private ShopOrderService shopOrderService;
@@ -713,6 +715,15 @@ public class OrderBusinessService {
*/
private void deductStock(OrderCreateRequest request) {
for (OrderCreateRequest.OrderGoodsItem item : request.getGoodsItems()) {
// 付款减库存的商品:创建订单时不扣库存
ShopGoods goodsForType = shopGoodsService.getById(item.getGoodsId());
Integer deductStockType = goodsForType != null ? goodsForType.getDeductStockType() : null;
if (deductStockType != null && deductStockType == DEDUCT_STOCK_TYPE_PAY) {
log.debug("跳过下单扣库存(付款减库存) - goodsId={}, skuId={}, qty={}",
item.getGoodsId(), item.getSkuId(), item.getQuantity());
continue;
}
if (item.getSkuId() != null) {
// 多规格商品扣减SKU库存
ShopGoodsSku sku = shopGoodsSkuService.getById(item.getSkuId());

View File

@@ -24,6 +24,7 @@ import java.util.List;
@Slf4j
@Service
public class OrderCancelServiceImpl implements OrderCancelService {
private static final int DEDUCT_STOCK_TYPE_PAY = 20; // 付款减库存:下单不扣库存,因此未支付取消无需回退
@Autowired
private ShopOrderService shopOrderService;
@@ -182,6 +183,20 @@ public class OrderCancelServiceImpl implements OrderCancelService {
}
for (ShopOrderGoods orderGood : orderGoods) {
// 付款减库存的商品:创建订单时未扣库存,未支付取消时无需回退(避免库存被“加多”)
try {
ShopGoods goods = shopGoodsService.getById(orderGood.getGoodsId());
if (goods != null && goods.getDeductStockType() != null && goods.getDeductStockType() == DEDUCT_STOCK_TYPE_PAY) {
log.debug("跳过未支付取消的库存回退(付款减库存) - orderId={}, goodsId={}, skuId={}, qty={}",
order.getOrderId(), orderGood.getGoodsId(), orderGood.getSkuId(), orderGood.getTotalNum());
continue;
}
} catch (Exception e) {
// 查不到商品或查询异常时,保持原有回退逻辑,避免出现“库存无法回退”的更坏情况
log.warn("读取商品扣库存方式失败,继续执行库存回退 - orderId={}, goodsId={}",
order.getOrderId(), orderGood.getGoodsId(), e);
}
if (orderGood.getSkuId() != null && orderGood.getSkuId() > 0) {
// 多规格商品恢复SKU库存
restoreSkuStock(orderGood);

View File

@@ -1,8 +1,10 @@
package com.gxwebsoft.shop.service.impl;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.gxwebsoft.common.core.context.TenantContext;
import com.gxwebsoft.common.core.config.ConfigProperties;
import com.gxwebsoft.common.core.config.CertificateProperties;
import com.gxwebsoft.common.core.utils.*;
@@ -86,10 +88,13 @@ public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrder
private ShopOrderDeliveryService shopOrderDeliveryService;
@Resource
private ShopExpressService shopExpressService;
@Resource
private ShopGoodsSkuService shopGoodsSkuService;
private static final long USER_ORDER_STATS_CACHE_SECONDS = 60L;
private static final long WECHAT_PREPAY_SNAPSHOT_TTL_MINUTES = 30L;
private static final String WECHAT_PREPAY_SNAPSHOT_KEY_PREFIX = "wxpay:prepay:snapshot:";
private static final int DEDUCT_STOCK_TYPE_PAY = 20; // 付款减库存
@Data
private static class WechatPrepaySnapshot {
@@ -769,10 +774,15 @@ public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrder
try {
Transaction result = service.queryOrderByOutTradeNo(queryRequest);
if (result.getTradeState().equals(Transaction.TradeStateEnum.SUCCESS)) {
if (Boolean.TRUE.equals(shopOrder.getPayStatus())) {
// 已是支付成功状态,避免重复触发支付成功后的业务逻辑(销量/库存等)
return true;
}
shopOrder.setPayStatus(true);
shopOrder.setPayTime(LocalDateTime.now());
shopOrder.setTransactionId(result.getTransactionId());
updateById(shopOrder);
handlePaymentSuccess(shopOrder);
return true;
}
} catch (ServiceException e) {
@@ -801,6 +811,9 @@ public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrder
*/
private void handlePaymentSuccess(ShopOrder order) {
try {
// 0. 付款减库存:支付成功后扣库存(下单时不扣)
deductStockAfterPaidIfNeeded(order);
// 1. 使用优惠券
if (order.getCouponId() != null && order.getCouponId() > 0) {
markCouponAsUsed(order);
@@ -816,6 +829,90 @@ public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrder
}
}
/**
* 付款减库存:支付成功后按商品设置扣减库存。
*
* 注意:此处使用忽略租户隔离 + 显式 tenantId 条件,避免支付回调没有 tenantId header 时更新失败。
* 若扣减失败(库存不足/数据缺失),仅记录日志,不阻断支付回调主流程。
*/
private void deductStockAfterPaidIfNeeded(ShopOrder order) {
if (order == null || order.getOrderId() == null || order.getTenantId() == null) {
return;
}
final List<ShopOrderGoods> orderGoodsList = shopOrderGoodsService.getListByOrderIdIgnoreTenant(order.getOrderId());
if (CollectionUtils.isEmpty(orderGoodsList)) {
return;
}
TenantContext.runIgnoreTenant(() -> {
for (ShopOrderGoods og : orderGoodsList) {
if (og == null || og.getGoodsId() == null) {
continue;
}
int qty = og.getTotalNum() == null ? 0 : og.getTotalNum();
if (qty <= 0) {
continue;
}
ShopGoods goods = shopGoodsService.getById(og.getGoodsId());
Integer deductStockType = goods != null ? goods.getDeductStockType() : null;
if (deductStockType == null || deductStockType != DEDUCT_STOCK_TYPE_PAY) {
continue;
}
try {
if (og.getSkuId() != null && og.getSkuId() > 0) {
// 多规格:扣 SKU 库存
boolean updated = shopGoodsSkuService.update(
new LambdaUpdateWrapper<ShopGoodsSku>()
.eq(ShopGoodsSku::getId, og.getSkuId())
.eq(ShopGoodsSku::getTenantId, order.getTenantId())
.apply("IFNULL(stock,0) >= {0}", qty)
.setSql("stock = IFNULL(stock,0) - " + qty)
);
if (!updated) {
log.warn("支付成功后扣SKU库存失败 - tenantId={}, orderId={}, skuId={}, goodsId={}, qty={}",
order.getTenantId(), order.getOrderId(), og.getSkuId(), og.getGoodsId(), qty);
// 兜底库存不足时至少将库存置0避免出现“明明已售出但库存仍大于0”的情况
shopGoodsSkuService.update(
new LambdaUpdateWrapper<ShopGoodsSku>()
.eq(ShopGoodsSku::getId, og.getSkuId())
.eq(ShopGoodsSku::getTenantId, order.getTenantId())
.apply("IFNULL(stock,0) < {0}", qty)
.setSql("stock = 0")
);
}
} else {
// 单规格:扣商品库存
boolean updated = shopGoodsService.update(
new LambdaUpdateWrapper<ShopGoods>()
.eq(ShopGoods::getGoodsId, og.getGoodsId())
.eq(ShopGoods::getTenantId, order.getTenantId())
.apply("IFNULL(stock,0) >= {0}", qty)
.setSql("stock = IFNULL(stock,0) - " + qty)
);
if (!updated) {
log.warn("支付成功后扣商品库存失败 - tenantId={}, orderId={}, goodsId={}, qty={}",
order.getTenantId(), order.getOrderId(), og.getGoodsId(), qty);
// 兜底库存不足时至少将库存置0避免出现“明明已售出但库存仍大于0”的情况
shopGoodsService.update(
new LambdaUpdateWrapper<ShopGoods>()
.eq(ShopGoods::getGoodsId, og.getGoodsId())
.eq(ShopGoods::getTenantId, order.getTenantId())
.apply("IFNULL(stock,0) < {0}", qty)
.setSql("stock = 0")
);
}
}
} catch (Exception e) {
log.warn("支付成功后扣库存异常 - tenantId={}, orderId={}, goodsId={}, skuId={}, qty={}",
order.getTenantId(), order.getOrderId(), og.getGoodsId(), og.getSkuId(), qty, e);
}
}
});
}
/**
* 标记优惠券为已使用
*/
@@ -1452,7 +1549,6 @@ public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrder
@Override
public boolean syncPaymentStatus(String orderNo, Integer paymentStatus, String transactionId, String payTime, Integer tenantId) {
try {
// 查询订单
ShopOrder order = getByOrderNo(orderNo, tenantId);
if (order == null) {
@@ -1461,7 +1557,7 @@ public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrder
}
// 如果订单已经是支付成功状态,不需要更新
if (order.getPayStatus() && paymentStatus == 1) {
if (Boolean.TRUE.equals(order.getPayStatus()) && paymentStatus == 1) {
log.info("订单已经是支付成功状态,无需更新: orderNo={}", orderNo);
return true;
}
@@ -1479,16 +1575,15 @@ public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrder
if (updated) {
log.info("订单支付状态同步成功: orderNo={}, paymentStatus={}, transactionId={}",
orderNo, paymentStatus, transactionId);
// paymentStatus=1时同步触发支付成功后业务逻辑包括付款减库存/销量等)
if (paymentStatus != null && paymentStatus == 1) {
handlePaymentSuccess(order);
}
} else {
log.warn("订单支付状态同步失败: orderNo={}, paymentStatus={}", orderNo, paymentStatus);
}
return updated;
} catch (Exception e) {
log.error("同步订单支付状态异常: orderNo={}, error={}", orderNo, e.getMessage(), e);
return false;
}
}