From 90965b6d0aa8f18c2d66729020c04eac29ca53d5 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, 13 Feb 2026 02:22:15 +0800 Subject: [PATCH] =?UTF-8?q?feat(ticket):=20=E4=BC=98=E5=8C=96=E5=A5=97?= =?UTF-8?q?=E7=A5=A8=E5=8F=91=E6=94=BE=E6=9C=8D=E5=8A=A1=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=A4=9A=E5=95=86=E5=93=81=E5=B9=B6=E5=AE=9E=E7=8E=B0=E8=B5=B7?= =?UTF-8?q?=E5=A7=8B=E9=80=81=E6=B0=B4=E8=87=AA=E5=8A=A8=E6=A0=B8=E9=94=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 增加对多个商品/模板的支持,修改issueTodayOrders方法接受商品ID列表 - 实现起始送水自动核销功能,按模板配置自动消耗初始水票数量 - 添加起始送水订单生成功能,用于配送端跟踪和后台管理 - 新增GltTicketOrder相关服务和地址快照构建逻辑 - 修改用户水票统计方法,从统计总数量改为统计可用数量 - 增加根据商品ID查询水票模板的API接口 - 优化订单状态更新逻辑,移除发货状态设置 - 添加数据安全检查,防止空值导致的异常处理 --- .../GltTicketTemplateController.java | 21 ++- .../controller/GltUserTicketController.java | 12 +- .../GltUserTicketLogController.java | 2 - .../gxwebsoft/glt/entity/GltUserTicket.java | 3 + .../glt/service/GltTicketIssueService.java | 170 ++++++++++++++++-- .../glt/service/GltUserTicketService.java | 7 +- .../impl/GltTicketOrderServiceImpl.java | 8 +- 7 files changed, 199 insertions(+), 24 deletions(-) diff --git a/src/main/java/com/gxwebsoft/glt/controller/GltTicketTemplateController.java b/src/main/java/com/gxwebsoft/glt/controller/GltTicketTemplateController.java index 2158b56..ae7dce9 100644 --- a/src/main/java/com/gxwebsoft/glt/controller/GltTicketTemplateController.java +++ b/src/main/java/com/gxwebsoft/glt/controller/GltTicketTemplateController.java @@ -1,5 +1,6 @@ package com.gxwebsoft.glt.controller; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.gxwebsoft.common.core.web.BaseController; import com.gxwebsoft.common.system.entity.User; import com.gxwebsoft.glt.service.GltTicketTemplateService; @@ -54,7 +55,23 @@ public class GltTicketTemplateController extends BaseController { return success(gltTicketTemplateService.getByIdRel(id)); } - @PreAuthorize("hasAuthority('glt:gltTicketTemplate:save')") + @Operation(summary = "根据商品ID查询水票") + @GetMapping("/getByGoodsId/{goodsId}") + public ApiResult getByGoodsId(@PathVariable("goodsId") Integer goodsId) { + GltTicketTemplate template = gltTicketTemplateService.getOne( + new LambdaQueryWrapper() + .eq(GltTicketTemplate::getGoodsId, goodsId) + .eq(GltTicketTemplate::getDeleted, 0) + .orderByAsc(GltTicketTemplate::getSortNumber) + .orderByDesc(GltTicketTemplate::getCreateTime) + .last("limit 1") + ); + return success(template); + } + + + + @PreAuthorize("hasAuthority('glt:gltTicketTemplate:update')") @OperationLog @Operation(summary = "添加水票") @PostMapping() @@ -92,7 +109,7 @@ public class GltTicketTemplateController extends BaseController { return fail("删除失败"); } - @PreAuthorize("hasAuthority('glt:gltTicketTemplate:save')") + @PreAuthorize("hasAuthority('glt:gltTicketTemplate:update')") @OperationLog @Operation(summary = "批量添加水票") @PostMapping("/batch") diff --git a/src/main/java/com/gxwebsoft/glt/controller/GltUserTicketController.java b/src/main/java/com/gxwebsoft/glt/controller/GltUserTicketController.java index 15848a1..51bd269 100644 --- a/src/main/java/com/gxwebsoft/glt/controller/GltUserTicketController.java +++ b/src/main/java/com/gxwebsoft/glt/controller/GltUserTicketController.java @@ -38,17 +38,23 @@ public class GltUserTicketController extends BaseController { return success(gltUserTicketService.pageRel(param)); } - @Operation(summary = "我的水票总数") + @Operation(summary = "可用水票总数") @GetMapping("/my-total") public ApiResult myTotal() { Integer userId = getLoginUserId(); if (userId == null) { return fail("未登录"); } - Integer totalQty = gltUserTicketService.sumTotalQtyByUserId(userId); + Integer tenantId = getTenantId(); + if (tenantId == null) { + return fail("租户信息缺失"); + } + Integer availableQty = gltUserTicketService.sumAvailableQtyByUserId(userId, tenantId); Map data = new HashMap<>(); data.put("userId", userId); - data.put("totalQty", totalQty); + // 兼容旧字段:totalQty 表示“可用水票总数” + data.put("totalQty", availableQty); + data.put("availableQty", availableQty); return success(data); } diff --git a/src/main/java/com/gxwebsoft/glt/controller/GltUserTicketLogController.java b/src/main/java/com/gxwebsoft/glt/controller/GltUserTicketLogController.java index 19ab463..8cde690 100644 --- a/src/main/java/com/gxwebsoft/glt/controller/GltUserTicketLogController.java +++ b/src/main/java/com/gxwebsoft/glt/controller/GltUserTicketLogController.java @@ -55,8 +55,6 @@ public class GltUserTicketLogController extends BaseController { return success(gltUserTicketLogService.getByIdRel(id)); } - @PreAuthorize("hasAuthority('glt:gltUserTicketLog:save')") - @OperationLog @Operation(summary = "添加消费日志") @PostMapping() public ApiResult save(@RequestBody GltUserTicketLog gltUserTicketLog) { diff --git a/src/main/java/com/gxwebsoft/glt/entity/GltUserTicket.java b/src/main/java/com/gxwebsoft/glt/entity/GltUserTicket.java index ecae32a..0448d2a 100644 --- a/src/main/java/com/gxwebsoft/glt/entity/GltUserTicket.java +++ b/src/main/java/com/gxwebsoft/glt/entity/GltUserTicket.java @@ -55,6 +55,9 @@ public class GltUserTicket implements Serializable { @Schema(description = "订单商品ID") private Integer orderGoodsId; + @Schema(description = "订单商品数量") + private Integer orderGoodsQty; + @Schema(description = "总数量") private Integer totalQty; diff --git a/src/main/java/com/gxwebsoft/glt/service/GltTicketIssueService.java b/src/main/java/com/gxwebsoft/glt/service/GltTicketIssueService.java index 6ac9261..2751277 100644 --- a/src/main/java/com/gxwebsoft/glt/service/GltTicketIssueService.java +++ b/src/main/java/com/gxwebsoft/glt/service/GltTicketIssueService.java @@ -2,25 +2,32 @@ package com.gxwebsoft.glt.service; 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.GltTicketTemplate; import com.gxwebsoft.glt.entity.GltUserTicket; import com.gxwebsoft.glt.entity.GltUserTicketLog; import com.gxwebsoft.glt.entity.GltUserTicketRelease; import com.gxwebsoft.shop.entity.ShopOrder; import com.gxwebsoft.shop.entity.ShopOrderGoods; +import com.gxwebsoft.shop.entity.ShopUserAddress; import com.gxwebsoft.shop.service.ShopOrderGoodsService; import com.gxwebsoft.shop.service.ShopOrderService; +import com.gxwebsoft.shop.service.ShopUserAddressService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.support.TransactionTemplate; +import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Objects; +import java.util.Set; /** * 套票发放(从订单生成用户套票 + 释放计划)的业务逻辑。 @@ -36,6 +43,8 @@ import java.util.Objects; public class GltTicketIssueService { public static final int CHANGE_TYPE_ISSUE = 10; + /** 变更类型:起始送水自动核销(按模板 startSendQty 在发放时自动消耗) */ + public static final int CHANGE_TYPE_START_SEND_WRITE_OFF = 12; private enum IssueOutcome { ISSUED, @@ -51,19 +60,43 @@ public class GltTicketIssueService { private final GltUserTicketService gltUserTicketService; private final GltUserTicketReleaseService gltUserTicketReleaseService; private final GltUserTicketLogService gltUserTicketLogService; + private final GltTicketOrderService gltTicketOrderService; + private final ShopUserAddressService shopUserAddressService; private final TransactionTemplate transactionTemplate; /** * 扫描“今日订单”,执行套票发放。 */ public void issueTodayOrders(Integer tenantId, Integer formId) { + if (formId == null) { + return; + } + issueTodayOrders(tenantId, List.of(formId)); + } + + /** + * 扫描“今日订单”,执行套票发放(支持多个商品/模板)。 + */ + public void issueTodayOrders(Integer tenantId, List goodsIds) { + if (tenantId == null || goodsIds == null || goodsIds.isEmpty()) { + return; + } + List uniqueGoodsIds = goodsIds.stream() + .filter(Objects::nonNull) + .distinct() + .toList(); + if (uniqueGoodsIds.isEmpty()) { + return; + } + Set goodsIdSet = new HashSet<>(uniqueGoodsIds); + LocalDateTime todayStart = LocalDate.now().atStartOfDay(); LocalDateTime tomorrowStart = todayStart.plusDays(1); List orders = shopOrderService.list( new LambdaQueryWrapper() .eq(ShopOrder::getTenantId, tenantId) - .eq(ShopOrder::getFormId, formId) + .in(ShopOrder::getFormId, uniqueGoodsIds) .eq(ShopOrder::getPayStatus, true) .eq(ShopOrder::getOrderStatus, 0) // 今日订单(兼容:以 create_time 或 pay_time 任一落在今日即可) @@ -77,7 +110,7 @@ public class GltTicketIssueService { ); if (orders.isEmpty()) { - log.debug("套票发放扫描:今日无符合条件的订单 tenantId={}, formId={}", tenantId, formId); + log.debug("套票发放扫描:今日无符合条件的订单 tenantId={}, goodsIds={}", tenantId, uniqueGoodsIds); return; } @@ -87,7 +120,7 @@ public class GltTicketIssueService { for (ShopOrder order : orders) { try { - int issuedCount = issueForOrder(tenantId, formId, order); + int issuedCount = issueForOrder(tenantId, goodsIdSet, order); if (issuedCount > 0) { success += issuedCount; } else { @@ -100,11 +133,11 @@ public class GltTicketIssueService { } } - log.info("套票发放扫描完成 - tenantId={}, formId={}, 订单数={}, 发放成功={}, 跳过={}, 失败={}", - tenantId, formId, orders.size(), success, skipped, failed); + log.info("套票发放扫描完成 - tenantId={}, goodsIds={}, 订单数={}, 发放成功={}, 跳过={}, 失败={}", + tenantId, uniqueGoodsIds, orders.size(), success, skipped, failed); } - private int issueForOrder(Integer tenantId, Integer formId, ShopOrder order) { + private int issueForOrder(Integer tenantId, Set goodsIds, ShopOrder order) { List goodsList = shopOrderGoodsService.getListByOrderIdIgnoreTenant(order.getOrderId()); if (goodsList == null || goodsList.isEmpty()) { return 0; @@ -114,7 +147,7 @@ public class GltTicketIssueService { boolean shouldCompleteOrder = false; for (ShopOrderGoods og : goodsList) { - if (!Objects.equals(og.getGoodsId(), formId)) { + if (og.getGoodsId() == null || !goodsIds.contains(og.getGoodsId())) { continue; } @@ -135,9 +168,6 @@ public class GltTicketIssueService { new LambdaUpdateWrapper() .eq(ShopOrder::getOrderId, order.getOrderId()) .eq(ShopOrder::getTenantId, tenantId) - .set(ShopOrder::getOrderStatus, 1) - // 同步更新发货状态为“已发货” - .set(ShopOrder::getDeliveryStatus, 20) .set(ShopOrder::getHasTakeGift, true) .set(ShopOrder::getUpdateTime, now) ); @@ -231,6 +261,7 @@ public class GltTicketIssueService { userTicket.setUsedQty(0); // 初始可用量来自“购买量”,视为已释放 userTicket.setReleasedQty(buyQty); + userTicket.setOrderGoodsQty(og.getTotalNum()); userTicket.setUserId(order.getUserId()); userTicket.setSortNumber(0); userTicket.setComments("订单发放套票"); @@ -274,6 +305,51 @@ public class GltTicketIssueService { issueLog.setUpdateTime(now); gltUserTicketLogService.save(issueLog); + // 按模板配置:自动“使用掉第一次水票”(起始送水数量) + Integer startSendQtyObj = template.getStartSendQty(); + int startSendQty = startSendQtyObj != null ? startSendQtyObj : 0; + if (startSendQty > 0) { + int availableBefore = userTicket.getAvailableQty() != null ? userTicket.getAvailableQty() : 0; + int usedBefore = userTicket.getUsedQty() != null ? userTicket.getUsedQty() : 0; + int toUse = Math.min(startSendQty, availableBefore); + if (toUse > 0) { + userTicket.setAvailableQty(availableBefore - toUse); + userTicket.setUsedQty(usedBefore + toUse); + userTicket.setUpdateTime(now); + if (!gltUserTicketService.updateById(userTicket)) { + throw new IllegalStateException("起始送水自动核销:更新用户水票失败 userTicketId=" + userTicket.getId()); + } + + // 起始送水:自动核销成功后,生成一条送水订单(用于配送端/后台跟踪) + GltTicketOrder ticketOrder = buildStartSendTicketOrder(tenantId, order, userTicket, toUse, now); + if (!gltTicketOrderService.save(ticketOrder)) { + throw new IllegalStateException("起始送水自动核销:创建送水订单失败 userTicketId=" + userTicket.getId()); + } + + GltUserTicketLog writeOffLog = new GltUserTicketLog(); + writeOffLog.setUserTicketId(userTicket.getId()); + writeOffLog.setChangeType(CHANGE_TYPE_START_SEND_WRITE_OFF); + writeOffLog.setChangeAvailable(-toUse); + writeOffLog.setChangeFrozen(0); + writeOffLog.setChangeUsed(toUse); + writeOffLog.setAvailableAfter(userTicket.getAvailableQty()); + writeOffLog.setFrozenAfter(userTicket.getFrozenQty() != null ? userTicket.getFrozenQty() : 0); + writeOffLog.setUsedAfter(userTicket.getUsedQty()); + // 关联送水订单(保持与“用户下单核销”的日志一致),并在备注里保留来源商城订单号便于追溯 + writeOffLog.setOrderId(ticketOrder.getId()); + writeOffLog.setOrderNo(ticketOrder.getId() == null ? null : String.valueOf(ticketOrder.getId())); + writeOffLog.setUserId(order.getUserId()); + writeOffLog.setSortNumber(0); + writeOffLog.setComments("起始送水自动核销(来源商城订单:" + safe(order.getOrderNo()) + ")"); + writeOffLog.setStatus(0); + writeOffLog.setDeleted(0); + writeOffLog.setTenantId(tenantId); + writeOffLog.setCreateTime(now); + writeOffLog.setUpdateTime(now); + gltUserTicketLogService.save(writeOffLog); + } + } + log.info("套票发放成功 - tenantId={}, orderNo={}, orderGoodsId={}, templateId={}, userTicketId={}, totalQty={}", tenantId, order.getOrderNo(), og.getId(), template.getId(), userTicket.getId(), totalQty); @@ -359,4 +435,78 @@ public class GltTicketIssueService { LocalDate adjusted = nextMonth.withDayOfMonth(day); return LocalDateTime.of(adjusted, time); } + + private static final DateTimeFormatter DATETIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + private GltTicketOrder buildStartSendTicketOrder(Integer tenantId, + ShopOrder shopOrder, + GltUserTicket userTicket, + int totalNum, + LocalDateTime now) { + GltTicketOrder o = new GltTicketOrder(); + o.setUserTicketId(userTicket.getId()); + o.setUserId(shopOrder.getUserId()); + o.setStoreId(shopOrder.getStoreId()); + o.setWarehouseId(shopOrder.getWarehouseId()); + o.setRiderId(shopOrder.getRiderId()); + o.setTotalNum(totalNum); + o.setPrice(BigDecimal.ZERO); + o.setBuyerRemarks(shopOrder.getBuyerRemarks()); + + // 地址快照:优先使用地址表快照;兜底使用商城订单上的 address 字段 + Integer addressId = shopOrder.getAddressId(); + o.setAddressId(addressId); + String addressSnapshot = null; + if (addressId != null) { + ShopUserAddress addr = shopUserAddressService.getOne(new LambdaQueryWrapper() + .eq(ShopUserAddress::getId, addressId) + .eq(ShopUserAddress::getUserId, shopOrder.getUserId()) + .eq(ShopUserAddress::getTenantId, tenantId) + .last("limit 1")); + addressSnapshot = buildAddressSnapshot(addr); + } + if (addressSnapshot == null || addressSnapshot.isBlank()) { + addressSnapshot = shopOrder.getAddress(); + } + o.setAddress(addressSnapshot); + + String preferredSendTime = shopOrder.getSendStartTime(); + if (preferredSendTime == null || preferredSendTime.isBlank()) { + preferredSendTime = now.format(DATETIME_FMT); + } + o.setSendTime(preferredSendTime); + o.setDeliveryStatus(GltTicketOrderService.DELIVERY_STATUS_WAITING); + o.setSortNumber(0); + o.setComments("起始送水自动下单(来源商城订单:" + safe(shopOrder.getOrderNo()) + ")"); + o.setStatus(0); + o.setDeleted(0); + o.setTenantId(tenantId); + o.setCreateTime(now); + o.setUpdateTime(now); + return o; + } + + private static String buildAddressSnapshot(ShopUserAddress addr) { + if (addr == null) { + return null; + } + if (addr.getFullAddress() != null && !addr.getFullAddress().isBlank()) { + return addr.getFullAddress(); + } + // 兼容旧数据:fullAddress 为空时,拼接省市区 + 详细地址 + StringBuilder sb = new StringBuilder(); + if (addr.getProvince() != null) sb.append(addr.getProvince()); + if (addr.getCity() != null) sb.append(addr.getCity()); + if (addr.getRegion() != null) sb.append(addr.getRegion()); + if (addr.getAddress() != null) sb.append(addr.getAddress()); + String s = sb.toString(); + if (!s.isBlank()) { + return s; + } + return addr.getAddress(); + } + + private static String safe(String s) { + return s == null ? "" : s; + } } diff --git a/src/main/java/com/gxwebsoft/glt/service/GltUserTicketService.java b/src/main/java/com/gxwebsoft/glt/service/GltUserTicketService.java index 1c292e9..615ef75 100644 --- a/src/main/java/com/gxwebsoft/glt/service/GltUserTicketService.java +++ b/src/main/java/com/gxwebsoft/glt/service/GltUserTicketService.java @@ -40,11 +40,12 @@ public interface GltUserTicketService extends IService { GltUserTicket getByIdRel(Integer id); /** - * 统计指定用户水票总数量(sum(total_qty)) + * 统计指定用户可用水票总数(sum(available_qty)) * * @param userId 用户ID - * @return 总数量(无记录返回0) + * @param tenantId 租户ID + * @return 可用总数(无记录返回0) */ - Integer sumTotalQtyByUserId(Integer userId); + Integer sumAvailableQtyByUserId(Integer userId, Integer tenantId); } diff --git a/src/main/java/com/gxwebsoft/glt/service/impl/GltTicketOrderServiceImpl.java b/src/main/java/com/gxwebsoft/glt/service/impl/GltTicketOrderServiceImpl.java index cca8400..f2b1069 100644 --- a/src/main/java/com/gxwebsoft/glt/service/impl/GltTicketOrderServiceImpl.java +++ b/src/main/java/com/gxwebsoft/glt/service/impl/GltTicketOrderServiceImpl.java @@ -790,7 +790,7 @@ public class GltTicketOrderServiceImpl extends ServiceImpl() + new LambdaQueryWrapper() .eq(ShopDealerCapital::getTenantId, tenantId) .eq(ShopDealerCapital::getFlowType, 10) .eq(ShopDealerCapital::getUserId, riderId) @@ -804,7 +804,7 @@ public class GltTicketOrderServiceImpl extends ServiceImpl() + new LambdaUpdateWrapper() .eq(ShopDealerUser::getTenantId, tenantId) .eq(ShopDealerUser::getUserId, riderId) .setSql("freeze_money = IFNULL(freeze_money,0) + " + money.toPlainString()) @@ -815,7 +815,7 @@ public class GltTicketOrderServiceImpl extends ServiceImpl() + new LambdaQueryWrapper() .eq(ShopDealerUser::getTenantId, tenantId) .eq(ShopDealerUser::getUserId, riderId) .last("limit 1") @@ -848,7 +848,7 @@ public class GltTicketOrderServiceImpl extends ServiceImpl() + new LambdaUpdateWrapper() .eq(ShopDealerUser::getTenantId, tenantId) .eq(ShopDealerUser::getUserId, riderId) .setSql("freeze_money = IFNULL(freeze_money,0) + " + money.toPlainString())