From 3cadaab2143c8c7744b45536a28c6a837fb831b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Fri, 27 Feb 2026 22:06:19 +0800 Subject: [PATCH] =?UTF-8?q?fix(ticket):=20=E8=A7=A3=E5=86=B3=E8=AE=A2?= =?UTF-8?q?=E5=8D=95=E7=8A=B6=E6=80=81=E6=9B=B4=E6=96=B0=E5=92=8C=E5=B9=B6?= =?UTF-8?q?=E5=8F=91=E5=A4=84=E7=90=86=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在订单完成后添加状态检查和更新,避免重复处理 - 增加行锁机制防止多实例并发执行导致的重复发放 - 对票券模板查询添加确定性排序确保一致性 - 添加详细日志记录起始送水核销和套票发放过程 - 优化查询条件防止重复扫描已处理订单 --- .../glt/service/GltTicketIssueService.java | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/gxwebsoft/glt/service/GltTicketIssueService.java b/src/main/java/com/gxwebsoft/glt/service/GltTicketIssueService.java index 2751277..70fd33b 100644 --- a/src/main/java/com/gxwebsoft/glt/service/GltTicketIssueService.java +++ b/src/main/java/com/gxwebsoft/glt/service/GltTicketIssueService.java @@ -163,11 +163,13 @@ public class GltTicketIssueService { if (shouldCompleteOrder) { LocalDateTime now = LocalDateTime.now(); - // 任务执行完后将订单置为“已完成”:order_status=1 + // 任务执行完后将订单置为“已完成”,避免后续扫描重复处理(幂等虽可挡住,但会产生大量无意义查询)。 shopOrderService.update( new LambdaUpdateWrapper() .eq(ShopOrder::getOrderId, order.getOrderId()) .eq(ShopOrder::getTenantId, tenantId) + .eq(ShopOrder::getOrderStatus, 0) + .set(ShopOrder::getOrderStatus, 1) .set(ShopOrder::getHasTakeGift, true) .set(ShopOrder::getUpdateTime, now) ); @@ -186,11 +188,29 @@ public class GltTicketIssueService { return IssueOutcome.SKIPPED; } + // 并发幂等兜底: + // - 该任务可能在多实例部署下并发执行;若不加锁,在没有唯一索引的情况下可能重复发放/重复核销。 + // - 这里先对商城订单行加行锁,保证同一订单在同一时刻只会被一个事务处理。 + // (注意:需数据库支持 SELECT ... FOR UPDATE,且 shop_order.order_id 为主键/有索引) + if (order.getOrderId() != null) { + shopOrderService.getOne( + new LambdaQueryWrapper() + .eq(ShopOrder::getOrderId, order.getOrderId()) + .eq(ShopOrder::getTenantId, tenantId) + .last("for update") + ); + } + + // 同一商品允许存在多条模板记录(历史数据/误操作)。为避免取到“错误模板”,这里做确定性排序取第一条。 + // 排序规则与后台 getByGoodsId 保持一致:sortNumber 越小越靠前,其次取最新创建时间。 GltTicketTemplate template = gltTicketTemplateService.getOne( new LambdaQueryWrapper() .eq(GltTicketTemplate::getTenantId, tenantId) .eq(GltTicketTemplate::getGoodsId, og.getGoodsId()) .eq(GltTicketTemplate::getDeleted, 0) + .orderByAsc(GltTicketTemplate::getSortNumber) + .orderByDesc(GltTicketTemplate::getCreateTime) + .orderByDesc(GltTicketTemplate::getId) .last("limit 1") ); @@ -313,6 +333,8 @@ public class GltTicketIssueService { int usedBefore = userTicket.getUsedQty() != null ? userTicket.getUsedQty() : 0; int toUse = Math.min(startSendQty, availableBefore); if (toUse > 0) { + log.info("起始送水自动核销 - tenantId={}, orderNo={}, templateId={}, userTicketId={}, startSendQty={}, availableBefore={}, toUse={}", + tenantId, order.getOrderNo(), template.getId(), userTicket.getId(), startSendQty, availableBefore, toUse); userTicket.setAvailableQty(availableBefore - toUse); userTicket.setUsedQty(usedBefore + toUse); userTicket.setUpdateTime(now); @@ -350,8 +372,8 @@ public class GltTicketIssueService { } } - log.info("套票发放成功 - tenantId={}, orderNo={}, orderGoodsId={}, templateId={}, userTicketId={}, totalQty={}", - tenantId, order.getOrderNo(), og.getId(), template.getId(), userTicket.getId(), totalQty); + log.info("套票发放成功 - tenantId={}, orderNo={}, orderGoodsId={}, templateId={}, userTicketId={}, orderGoodsQty={}, buyQty={}, giftQty={}, startSendQty={}, totalQty={}", + tenantId, order.getOrderNo(), og.getId(), template.getId(), userTicket.getId(), og.getTotalNum(), buyQty, giftQty, startSendQty, totalQty); return IssueOutcome.ISSUED; }