From a8af20bcde47da94b2ef1c5951bf3ca1b71fcc30 Mon Sep 17 00:00:00 2001
From: gxwebsoft <170083662@qq.com>
Date: Sun, 1 Mar 2026 00:30:21 +0800
Subject: [PATCH] =?UTF-8?q?feat(shop):=20=E6=B7=BB=E5=8A=A0=E8=AE=A2?=
=?UTF-8?q?=E5=8D=95=E5=8F=96=E6=B6=88=E5=92=8C=E9=80=80=E6=AC=BE=E6=97=B6?=
=?UTF-8?q?=E7=9A=84=E6=B0=B4=E7=A5=A8=E6=92=A4=E9=94=80=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 在ShopOrderController中注入GltTicketRevokeService服务
- 实现订单状态改为已取消时同步撤销相关水票、释放计划和送水订单
- 实现退款成功后自动撤销水票相关数据的功能
- 新增GltTicketRevokeService服务处理水票撤销逻辑
- 添加批量订单取消时的水票撤销支持
- 实现撤销操作的幂等性确保无副作用
- 添加单元测试验证水票撤销功能的正确性
---
.../glt/service/GltTicketRevokeService.java | 130 ++++++++++++++++++
.../shop/controller/ShopOrderController.java | 69 +++++++++-
.../service/GltTicketRevokeServiceTest.java | 65 +++++++++
3 files changed, 263 insertions(+), 1 deletion(-)
create mode 100644 src/main/java/com/gxwebsoft/glt/service/GltTicketRevokeService.java
create mode 100644 src/test/java/com/gxwebsoft/glt/service/GltTicketRevokeServiceTest.java
diff --git a/src/main/java/com/gxwebsoft/glt/service/GltTicketRevokeService.java b/src/main/java/com/gxwebsoft/glt/service/GltTicketRevokeService.java
new file mode 100644
index 0000000..a73b8b7
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/glt/service/GltTicketRevokeService.java
@@ -0,0 +1,130 @@
+package com.gxwebsoft.glt.service;
+
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.gxwebsoft.glt.entity.GltTicketOrder;
+import com.gxwebsoft.glt.entity.GltUserTicket;
+import com.gxwebsoft.glt.entity.GltUserTicketRelease;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 水票撤销(订单取消/退款成功后的清理):
+ * - 取消/隐藏用户水票(glt_user_ticket.deleted=1)
+ * - 删除未完成的释放计划(glt_user_ticket_release.deleted=1, status!=1)
+ * - 删除未完成的送水订单(glt_ticket_order.deleted=1, delivery_status!=40)
+ *
+ *
说明:该操作需保证幂等;若无关联水票则无任何副作用。
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class GltTicketRevokeService {
+
+ /** release.status:已释放 */
+ private static final int RELEASE_STATUS_DONE = 1;
+ /** ticketOrder.deliveryStatus:已完成 */
+ private static final int TICKET_ORDER_DELIVERY_STATUS_FINISHED = 40;
+
+ private final GltUserTicketService gltUserTicketService;
+ private final GltUserTicketReleaseService gltUserTicketReleaseService;
+ private final GltTicketOrderService gltTicketOrderService;
+
+ @Transactional(rollbackFor = Exception.class)
+ public int revokeByShopOrder(Integer tenantId, Integer shopOrderId, String shopOrderNo, String reason) {
+ if (tenantId == null) {
+ return 0;
+ }
+ if (shopOrderId == null && StrUtil.isBlank(shopOrderNo)) {
+ return 0;
+ }
+
+ LambdaQueryWrapper qw = new LambdaQueryWrapper()
+ .eq(GltUserTicket::getTenantId, tenantId)
+ .eq(GltUserTicket::getDeleted, 0);
+
+ if (shopOrderId != null && StrUtil.isNotBlank(shopOrderNo)) {
+ qw.and(w -> w.eq(GltUserTicket::getOrderId, shopOrderId).or().eq(GltUserTicket::getOrderNo, shopOrderNo));
+ } else if (shopOrderId != null) {
+ qw.eq(GltUserTicket::getOrderId, shopOrderId);
+ } else {
+ qw.eq(GltUserTicket::getOrderNo, shopOrderNo);
+ }
+
+ List tickets = gltUserTicketService.list(qw);
+ if (tickets == null || tickets.isEmpty()) {
+ return 0;
+ }
+
+ LocalDateTime now = LocalDateTime.now();
+ int revoked = 0;
+ // 不强制覆盖 comments,避免影响后台人工备注;reason 仅用于日志。
+ String reasonForLog = StrUtil.isBlank(reason) ? "订单取消/退款撤销水票" : reason.trim();
+
+ for (GltUserTicket t : tickets) {
+ if (t == null || t.getId() == null) {
+ continue;
+ }
+
+ Integer userTicketId = t.getId();
+
+ // 1) 删除未完成的送水订单(避免继续配送/接单/确认)
+ try {
+ LambdaUpdateWrapper uw = new LambdaUpdateWrapper()
+ .eq(GltTicketOrder::getTenantId, tenantId)
+ .eq(GltTicketOrder::getDeleted, 0)
+ .eq(GltTicketOrder::getUserTicketId, userTicketId)
+ // 兼容历史/脏数据:deliveryStatus 为空时也按“未完成”处理
+ .and(w -> w.ne(GltTicketOrder::getDeliveryStatus, TICKET_ORDER_DELIVERY_STATUS_FINISHED)
+ .or().isNull(GltTicketOrder::getDeliveryStatus))
+ .set(GltTicketOrder::getDeleted, 1)
+ .set(GltTicketOrder::getUpdateTime, now);
+ gltTicketOrderService.update(null, uw);
+ } catch (Exception e) {
+ log.warn("撤销送水订单失败(继续尝试撤销水票/释放计划) - tenantId={}, shopOrderId={}, userTicketId={}",
+ tenantId, shopOrderId, userTicketId, e);
+ }
+
+ // 2) 删除未完成的释放计划(防止后续继续自动释放)
+ try {
+ LambdaUpdateWrapper uw = new LambdaUpdateWrapper()
+ .eq(GltUserTicketRelease::getTenantId, tenantId)
+ .eq(GltUserTicketRelease::getDeleted, 0)
+ .eq(GltUserTicketRelease::getUserTicketId, userTicketId.longValue())
+ // status 为空时也视为“未完成”
+ .and(w -> w.ne(GltUserTicketRelease::getStatus, RELEASE_STATUS_DONE)
+ .or().isNull(GltUserTicketRelease::getStatus))
+ .set(GltUserTicketRelease::getDeleted, 1)
+ .set(GltUserTicketRelease::getUpdateTime, now);
+ gltUserTicketReleaseService.update(null, uw);
+ } catch (Exception e) {
+ log.warn("撤销水票释放计划失败(继续尝试撤销水票) - tenantId={}, shopOrderId={}, userTicketId={}",
+ tenantId, shopOrderId, userTicketId, e);
+ }
+
+ // 3) 撤销水票本身(软删除;幂等)
+ boolean ok = gltUserTicketService.update(
+ null,
+ new LambdaUpdateWrapper()
+ .eq(GltUserTicket::getTenantId, tenantId)
+ .eq(GltUserTicket::getDeleted, 0)
+ .eq(GltUserTicket::getId, userTicketId)
+ .set(GltUserTicket::getDeleted, 1)
+ .set(GltUserTicket::getUpdateTime, now)
+ );
+ if (ok) {
+ revoked++;
+ }
+ }
+
+ log.info("撤销水票完成 - tenantId={}, shopOrderId={}, shopOrderNo={}, tickets={}, reason={}",
+ tenantId, shopOrderId, shopOrderNo, revoked, reasonForLog);
+ return revoked;
+ }
+}
diff --git a/src/main/java/com/gxwebsoft/shop/controller/ShopOrderController.java b/src/main/java/com/gxwebsoft/shop/controller/ShopOrderController.java
index 6aafdbb..508ec67 100644
--- a/src/main/java/com/gxwebsoft/shop/controller/ShopOrderController.java
+++ b/src/main/java/com/gxwebsoft/shop/controller/ShopOrderController.java
@@ -16,6 +16,7 @@ import com.gxwebsoft.common.system.entity.Payment;
import com.gxwebsoft.shop.entity.ShopOrderDelivery;
import com.gxwebsoft.shop.entity.ShopUserAddress;
import com.gxwebsoft.shop.service.*;
+import com.gxwebsoft.glt.service.GltTicketRevokeService;
import com.gxwebsoft.shop.service.impl.KuaiDi100Impl;
import com.gxwebsoft.shop.task.OrderAutoCancelTask;
import com.gxwebsoft.shop.entity.ShopOrder;
@@ -102,6 +103,8 @@ public class ShopOrderController extends BaseController {
private PaymentService paymentService;
@Resource
private ShopStoreFenceService shopStoreFenceService;
+ @Resource
+ private GltTicketRevokeService gltTicketRevokeService;
@Operation(summary = "分页查询订单")
@GetMapping("/page")
@@ -373,6 +376,21 @@ public class ShopOrderController extends BaseController {
}
if (shopOrderService.updateById(shopOrder)) {
+ // 后台手工将订单改为“已取消”(2)时,需同步撤销可能已发放的水票/释放计划/送水订单
+ try {
+ if (Objects.equals(shopOrder.getOrderStatus(), 2) && !Objects.equals(shopOrderNow.getOrderStatus(), 2)) {
+ gltTicketRevokeService.revokeByShopOrder(
+ ObjectUtil.defaultIfNull(shopOrder.getTenantId(), shopOrderNow.getTenantId()),
+ shopOrderNow.getOrderId(),
+ shopOrderNow.getOrderNo(),
+ "订单取消撤销水票"
+ );
+ }
+ } catch (Exception e) {
+ logger.error("订单更新为取消后撤销水票失败 - orderId={}, orderNo={}",
+ shopOrderNow.getOrderId(), shopOrderNow.getOrderNo(), e);
+ }
+
// 如果订单上带了快递单号(常见于后台手工修正/补录),同步到发货单表,避免发货单还是旧单号
if (StrUtil.isNotBlank(shopOrder.getExpressNo()) && shopOrder.getOrderId() != null) {
try {
@@ -527,6 +545,20 @@ public class ShopOrderController extends BaseController {
return fail("退款成功,但订单状态更新失败,请联系管理员");
}
+ // 退款成功后撤销水票相关数据(幂等;无水票则无副作用)
+ try {
+ gltTicketRevokeService.revokeByShopOrder(
+ current.getTenantId(),
+ current.getOrderId(),
+ current.getOrderNo(),
+ "订单退款成功撤销水票"
+ );
+ } catch (Exception e) {
+ // 退款已完成,不能因为撤销失败而回滚;记录日志以便人工补偿
+ logger.error("退款成功但撤销水票失败 - tenantId={}, orderId={}, orderNo={}",
+ current.getTenantId(), current.getOrderId(), current.getOrderNo(), e);
+ }
+
logger.info("订单退款请求成功 - 订单号: {}, 退款单号: {}, 微信退款单号: {}",
current.getOrderNo(), refundNo, refundResponse.getTransactionId());
return success("退款成功");
@@ -568,7 +600,42 @@ public class ShopOrderController extends BaseController {
return fail("退款相关操作请使用退款接口: PUT /api/shop/shop-order/refund");
}
}
- if (batchParam.update(shopOrderService, "order_id")) {
+ boolean ok = batchParam.update(shopOrderService, "order_id");
+ if (ok) {
+ // 兼容后台直接将订单改为“已取消”(2)的场景:同步撤销可能已发放的水票/释放计划/送水订单
+ try {
+ if (batchParam != null && batchParam.getData() != null
+ && Objects.equals(batchParam.getData().getOrderStatus(), 2)
+ && batchParam.getIds() != null && !batchParam.getIds().isEmpty()) {
+ for (Object rawId : batchParam.getIds()) {
+ Integer orderId = null;
+ if (rawId instanceof Integer) {
+ orderId = (Integer) rawId;
+ } else if (rawId != null) {
+ try {
+ orderId = Integer.valueOf(rawId.toString());
+ } catch (Exception ignore) {
+ // ignore malformed id
+ }
+ }
+ if (orderId == null) {
+ continue;
+ }
+ ShopOrder order = shopOrderService.getById(orderId);
+ if (order == null) {
+ continue;
+ }
+ gltTicketRevokeService.revokeByShopOrder(
+ order.getTenantId(),
+ order.getOrderId(),
+ order.getOrderNo(),
+ "订单取消撤销水票"
+ );
+ }
+ }
+ } catch (Exception e) {
+ logger.error("批量取消订单后撤销水票失败", e);
+ }
return success("修改成功");
}
return fail("修改失败");
diff --git a/src/test/java/com/gxwebsoft/glt/service/GltTicketRevokeServiceTest.java b/src/test/java/com/gxwebsoft/glt/service/GltTicketRevokeServiceTest.java
new file mode 100644
index 0000000..39f42c4
--- /dev/null
+++ b/src/test/java/com/gxwebsoft/glt/service/GltTicketRevokeServiceTest.java
@@ -0,0 +1,65 @@
+package com.gxwebsoft.glt.service;
+
+import com.gxwebsoft.glt.entity.GltUserTicket;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+class GltTicketRevokeServiceTest {
+
+ @Mock
+ private GltUserTicketService gltUserTicketService;
+ @Mock
+ private GltUserTicketReleaseService gltUserTicketReleaseService;
+ @Mock
+ private GltTicketOrderService gltTicketOrderService;
+
+ @InjectMocks
+ private GltTicketRevokeService gltTicketRevokeService;
+
+ @Test
+ void revokeByShopOrder_noTenant_noop() {
+ int revoked = gltTicketRevokeService.revokeByShopOrder(null, 1, "O1", "r");
+ assertEquals(0, revoked);
+ verifyNoInteractions(gltUserTicketService, gltUserTicketReleaseService, gltTicketOrderService);
+ }
+
+ @Test
+ void revokeByShopOrder_noTickets_noop() {
+ when(gltUserTicketService.list(any())).thenReturn(List.of());
+ int revoked = gltTicketRevokeService.revokeByShopOrder(10584, 1, "O1", "r");
+ assertEquals(0, revoked);
+ verify(gltUserTicketService, times(1)).list(any());
+ verifyNoMoreInteractions(gltUserTicketService);
+ verifyNoInteractions(gltUserTicketReleaseService, gltTicketOrderService);
+ }
+
+ @Test
+ void revokeByShopOrder_hasTickets_revokesAll() {
+ GltUserTicket t = new GltUserTicket();
+ t.setId(123);
+ t.setTenantId(10584);
+ when(gltUserTicketService.list(any())).thenReturn(List.of(t));
+ when(gltUserTicketService.update(isNull(), any())).thenReturn(true);
+ when(gltTicketOrderService.update(isNull(), any())).thenReturn(true);
+ when(gltUserTicketReleaseService.update(isNull(), any())).thenReturn(true);
+
+ int revoked = gltTicketRevokeService.revokeByShopOrder(10584, 1, "O1", "退款撤销");
+
+ assertEquals(1, revoked);
+ verify(gltTicketOrderService, times(1)).update(isNull(), any());
+ verify(gltUserTicketReleaseService, times(1)).update(isNull(), any());
+ verify(gltUserTicketService, times(1)).update(isNull(), any());
+ }
+}
+