feat(shop): 添加订单取消和退款时的水票撤销功能
- 在ShopOrderController中注入GltTicketRevokeService服务 - 实现订单状态改为已取消时同步撤销相关水票、释放计划和送水订单 - 实现退款成功后自动撤销水票相关数据的功能 - 新增GltTicketRevokeService服务处理水票撤销逻辑 - 添加批量订单取消时的水票撤销支持 - 实现撤销操作的幂等性确保无副作用 - 添加单元测试验证水票撤销功能的正确性
This commit is contained in:
@@ -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)
|
||||
*
|
||||
* <p>说明:该操作需保证幂等;若无关联水票则无任何副作用。</p>
|
||||
*/
|
||||
@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<GltUserTicket> qw = new LambdaQueryWrapper<GltUserTicket>()
|
||||
.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<GltUserTicket> 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<GltTicketOrder> uw = new LambdaUpdateWrapper<GltTicketOrder>()
|
||||
.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<GltUserTicketRelease> uw = new LambdaUpdateWrapper<GltUserTicketRelease>()
|
||||
.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<GltUserTicket>()
|
||||
.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;
|
||||
}
|
||||
}
|
||||
@@ -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("修改失败");
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user