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") @Schema(description = "用户水票ID")
private Integer userTicketId; private Integer userTicketId;
@Schema(description = "订单编号")
@TableField(exist = false)
private String orderNo;
@Schema(description = "门店ID") @Schema(description = "门店ID")
private Integer storeId; private Integer storeId;

View File

@@ -10,13 +10,15 @@
u.nickname, u.phone, u.avatar, u.nickname, u.phone, u.avatar,
d.name as receiverName, d.phone as receiverPhone, d.name as receiverName, d.phone as receiverPhone,
d.province as receiverProvince, d.city as receiverCity, d.region as receiverRegion, 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 FROM glt_ticket_order a
LEFT JOIN shop_store b ON a.store_id = b.id 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_warehouse w ON a.warehouse_id = w.id
LEFT JOIN shop_store_rider c ON a.rider_id = c.user_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 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 shop_user_address d ON a.address_id = d.id
LEFT JOIN glt_user_ticket f ON a.user_ticket_id = f.id
<where> <where>
<if test="param.id != null"> <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.TableField;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import java.io.Serializable; import java.io.Serializable;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@@ -101,6 +102,7 @@ public class ShopGoods implements Serializable {
private BigDecimal secondDividend; private BigDecimal secondDividend;
@Schema(description = "库存计算方式(10下单减库存 20付款减库存)") @Schema(description = "库存计算方式(10下单减库存 20付款减库存)")
@JsonAlias({"cdeductStockType"})
private Integer deductStockType; private Integer deductStockType;
@Schema(description = "交付方式(0不启用)") @Schema(description = "交付方式(0不启用)")

View File

@@ -32,6 +32,8 @@ import java.util.Map;
@Slf4j @Slf4j
@Service @Service
public class OrderBusinessService { public class OrderBusinessService {
private static final int DEDUCT_STOCK_TYPE_ORDER = 10; // 下单减库存
private static final int DEDUCT_STOCK_TYPE_PAY = 20; // 付款减库存
@Resource @Resource
private ShopOrderService shopOrderService; private ShopOrderService shopOrderService;
@@ -713,6 +715,15 @@ public class OrderBusinessService {
*/ */
private void deductStock(OrderCreateRequest request) { private void deductStock(OrderCreateRequest request) {
for (OrderCreateRequest.OrderGoodsItem item : request.getGoodsItems()) { 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) { if (item.getSkuId() != null) {
// 多规格商品扣减SKU库存 // 多规格商品扣减SKU库存
ShopGoodsSku sku = shopGoodsSkuService.getById(item.getSkuId()); ShopGoodsSku sku = shopGoodsSkuService.getById(item.getSkuId());

View File

@@ -24,6 +24,7 @@ import java.util.List;
@Slf4j @Slf4j
@Service @Service
public class OrderCancelServiceImpl implements OrderCancelService { public class OrderCancelServiceImpl implements OrderCancelService {
private static final int DEDUCT_STOCK_TYPE_PAY = 20; // 付款减库存:下单不扣库存,因此未支付取消无需回退
@Autowired @Autowired
private ShopOrderService shopOrderService; private ShopOrderService shopOrderService;
@@ -182,6 +183,20 @@ public class OrderCancelServiceImpl implements OrderCancelService {
} }
for (ShopOrderGoods orderGood : orderGoods) { 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) { if (orderGood.getSkuId() != null && orderGood.getSkuId() > 0) {
// 多规格商品恢复SKU库存 // 多规格商品恢复SKU库存
restoreSkuStock(orderGood); restoreSkuStock(orderGood);

View File

@@ -1,8 +1,10 @@
package com.gxwebsoft.shop.service.impl; package com.gxwebsoft.shop.service.impl;
import cn.hutool.core.util.StrUtil; 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.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; 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.ConfigProperties;
import com.gxwebsoft.common.core.config.CertificateProperties; import com.gxwebsoft.common.core.config.CertificateProperties;
import com.gxwebsoft.common.core.utils.*; import com.gxwebsoft.common.core.utils.*;
@@ -86,10 +88,13 @@ public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrder
private ShopOrderDeliveryService shopOrderDeliveryService; private ShopOrderDeliveryService shopOrderDeliveryService;
@Resource @Resource
private ShopExpressService shopExpressService; private ShopExpressService shopExpressService;
@Resource
private ShopGoodsSkuService shopGoodsSkuService;
private static final long USER_ORDER_STATS_CACHE_SECONDS = 60L; private static final long USER_ORDER_STATS_CACHE_SECONDS = 60L;
private static final long WECHAT_PREPAY_SNAPSHOT_TTL_MINUTES = 30L; 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 String WECHAT_PREPAY_SNAPSHOT_KEY_PREFIX = "wxpay:prepay:snapshot:";
private static final int DEDUCT_STOCK_TYPE_PAY = 20; // 付款减库存
@Data @Data
private static class WechatPrepaySnapshot { private static class WechatPrepaySnapshot {
@@ -769,10 +774,15 @@ public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrder
try { try {
Transaction result = service.queryOrderByOutTradeNo(queryRequest); Transaction result = service.queryOrderByOutTradeNo(queryRequest);
if (result.getTradeState().equals(Transaction.TradeStateEnum.SUCCESS)) { if (result.getTradeState().equals(Transaction.TradeStateEnum.SUCCESS)) {
if (Boolean.TRUE.equals(shopOrder.getPayStatus())) {
// 已是支付成功状态,避免重复触发支付成功后的业务逻辑(销量/库存等)
return true;
}
shopOrder.setPayStatus(true); shopOrder.setPayStatus(true);
shopOrder.setPayTime(LocalDateTime.now()); shopOrder.setPayTime(LocalDateTime.now());
shopOrder.setTransactionId(result.getTransactionId()); shopOrder.setTransactionId(result.getTransactionId());
updateById(shopOrder); updateById(shopOrder);
handlePaymentSuccess(shopOrder);
return true; return true;
} }
} catch (ServiceException e) { } catch (ServiceException e) {
@@ -801,6 +811,9 @@ public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrder
*/ */
private void handlePaymentSuccess(ShopOrder order) { private void handlePaymentSuccess(ShopOrder order) {
try { try {
// 0. 付款减库存:支付成功后扣库存(下单时不扣)
deductStockAfterPaidIfNeeded(order);
// 1. 使用优惠券 // 1. 使用优惠券
if (order.getCouponId() != null && order.getCouponId() > 0) { if (order.getCouponId() != null && order.getCouponId() > 0) {
markCouponAsUsed(order); 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,43 +1549,41 @@ public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrder
@Override @Override
public boolean syncPaymentStatus(String orderNo, Integer paymentStatus, String transactionId, String payTime, Integer tenantId) { public boolean syncPaymentStatus(String orderNo, Integer paymentStatus, String transactionId, String payTime, Integer tenantId) {
try { // 查询订单
// 查询订单 ShopOrder order = getByOrderNo(orderNo, tenantId);
ShopOrder order = getByOrderNo(orderNo, tenantId); if (order == null) {
if (order == null) { log.warn("同步支付状态失败:订单不存在, orderNo={}, tenantId={}", orderNo, tenantId);
log.warn("同步支付状态失败:订单不存在, orderNo={}, tenantId={}", orderNo, tenantId);
return false;
}
// 如果订单已经是支付成功状态,不需要更新
if (order.getPayStatus() && paymentStatus == 1) {
log.info("订单已经是支付成功状态,无需更新: orderNo={}", orderNo);
return true;
}
// 更新订单状态
boolean updated = this.lambdaUpdate()
.eq(ShopOrder::getOrderNo, orderNo)
.eq(ShopOrder::getTenantId, tenantId)
.set(ShopOrder::getPayStatus, paymentStatus == 1)
.set(transactionId != null, ShopOrder::getTransactionId, transactionId)
.set(payTime != null, ShopOrder::getPayTime, payTime)
.set(ShopOrder::getUpdateTime, LocalDateTime.now())
.update();
if (updated) {
log.info("订单支付状态同步成功: orderNo={}, paymentStatus={}, transactionId={}",
orderNo, paymentStatus, transactionId);
} else {
log.warn("订单支付状态同步失败: orderNo={}, paymentStatus={}", orderNo, paymentStatus);
}
return updated;
} catch (Exception e) {
log.error("同步订单支付状态异常: orderNo={}, error={}", orderNo, e.getMessage(), e);
return false; return false;
} }
// 如果订单已经是支付成功状态,不需要更新
if (Boolean.TRUE.equals(order.getPayStatus()) && paymentStatus == 1) {
log.info("订单已经是支付成功状态,无需更新: orderNo={}", orderNo);
return true;
}
// 更新订单状态
boolean updated = this.lambdaUpdate()
.eq(ShopOrder::getOrderNo, orderNo)
.eq(ShopOrder::getTenantId, tenantId)
.set(ShopOrder::getPayStatus, paymentStatus == 1)
.set(transactionId != null, ShopOrder::getTransactionId, transactionId)
.set(payTime != null, ShopOrder::getPayTime, payTime)
.set(ShopOrder::getUpdateTime, LocalDateTime.now())
.update();
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;
} }