feat(ticket): 优化套票发放逻辑支持购买量与赠送量分离处理

- 引入 GltTicketOrder 实体和服务类处理送水订单
- 新增 DateTimeFormatter 用于时间格式化
- 修改购买数量计算逻辑,优先使用订单总数量提高准确性
- 实现购买量与赠送量分离,支持 includeBuyQty 配置决定是否将购买量计入水票账户
- 更新用户水票的可用量、冻结量和已释放数量字段逻辑
- 优化起始送水功能,支持从可用和冻结票中同时扣除
- 添加订单总数量与订单商品数量不一致的提示日志
- 在起始送水时自动生成对应的送水订单记录
- 调整释放计划生成逻辑,基于实际冻结量进行计算
This commit is contained in:
2026-02-09 18:09:24 +08:00
parent aa4a6d9725
commit e8ce2d162f

View File

@@ -3,6 +3,7 @@ package com.gxwebsoft.glt.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.gxwebsoft.glt.entity.GltTicketTemplate; import com.gxwebsoft.glt.entity.GltTicketTemplate;
import com.gxwebsoft.glt.entity.GltTicketOrder;
import com.gxwebsoft.glt.entity.GltUserTicket; import com.gxwebsoft.glt.entity.GltUserTicket;
import com.gxwebsoft.glt.entity.GltUserTicketLog; import com.gxwebsoft.glt.entity.GltUserTicketLog;
import com.gxwebsoft.glt.entity.GltUserTicketRelease; import com.gxwebsoft.glt.entity.GltUserTicketRelease;
@@ -18,6 +19,7 @@ import org.springframework.transaction.support.TransactionTemplate;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.LocalTime; import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@@ -55,6 +57,7 @@ public class GltTicketIssueService {
private final GltUserTicketService gltUserTicketService; private final GltUserTicketService gltUserTicketService;
private final GltUserTicketReleaseService gltUserTicketReleaseService; private final GltUserTicketReleaseService gltUserTicketReleaseService;
private final GltUserTicketLogService gltUserTicketLogService; private final GltUserTicketLogService gltUserTicketLogService;
private final GltTicketOrderService gltTicketOrderService;
private final TransactionTemplate transactionTemplate; private final TransactionTemplate transactionTemplate;
/** /**
@@ -217,29 +220,38 @@ public class GltTicketIssueService {
return IssueOutcome.ALREADY_ISSUED; return IssueOutcome.ALREADY_ISSUED;
} }
int buyQty = og.getTotalNum() != null ? og.getTotalNum() : 0; // water 套餐场景:订单表 totalNum 往往更可信(有些订单商品行会出现 totalNum != 订单总数的情况)
if (buyQty <= 0) { int rawBuyQty = og.getTotalNum() != null ? og.getTotalNum() : 0;
if (order.getTotalNum() != null && order.getTotalNum() > 0 && Objects.equals(order.getFormId(), og.getGoodsId())) {
rawBuyQty = order.getTotalNum();
}
if (rawBuyQty <= 0) {
log.warn("套票发放跳过:购买数量无效 - tenantId={}, orderNo={}, orderGoodsId={}, totalNum={}", log.warn("套票发放跳过:购买数量无效 - tenantId={}, orderNo={}, orderGoodsId={}, totalNum={}",
tenantId, order.getOrderNo(), og.getId(), og.getTotalNum()); tenantId, order.getOrderNo(), og.getId(), og.getTotalNum());
return IssueOutcome.SKIPPED; return IssueOutcome.SKIPPED;
} }
Integer minBuyQty = template.getMinBuyQty(); Integer minBuyQty = template.getMinBuyQty();
if (minBuyQty != null && minBuyQty > 0 && buyQty < minBuyQty) { if (minBuyQty != null && minBuyQty > 0 && rawBuyQty < minBuyQty) {
log.info("套票发放跳过:未达到最小购买数量 - tenantId={}, orderNo={}, buyQty={}, minBuyQty={}", log.info("套票发放跳过:未达到最小购买数量 - tenantId={}, orderNo={}, buyQty={}, minBuyQty={}",
tenantId, order.getOrderNo(), buyQty, minBuyQty); tenantId, order.getOrderNo(), rawBuyQty, minBuyQty);
return IssueOutcome.SKIPPED; return IssueOutcome.SKIPPED;
} }
int giftMultiplier = template.getGiftMultiplier() != null ? template.getGiftMultiplier() : 0; int giftMultiplier = template.getGiftMultiplier() != null ? template.getGiftMultiplier() : 0;
int giftQty = buyQty * Math.max(giftMultiplier, 0); int giftQty = rawBuyQty * Math.max(giftMultiplier, 0);
// 购买量buyQty应立即可用赠送量giftQty进入冻结并按计划释放 // 默认仅把“赠送量”计入水票账户;如需把购买量也纳入,设置 includeBuyQty=true
int totalQty = buyQty + giftQty; int buyQtyForTicket = Boolean.TRUE.equals(template.getIncludeBuyQty()) ? rawBuyQty : 0;
// 购买量buyQtyForTicket应立即可用赠送量giftQty进入冻结并按计划释放。
int availableQty = buyQtyForTicket;
int frozenQty = giftQty;
int totalQty = availableQty + frozenQty;
if (totalQty <= 0) { if (totalQty <= 0) {
log.info("套票发放跳过计算结果为0 - tenantId={}, orderNo={}, buyQty={}, giftMultiplier={}, includeBuyQty={}", log.info("套票发放跳过计算结果为0 - tenantId={}, orderNo={}, buyQty={}, giftMultiplier={}, includeBuyQty={}",
tenantId, order.getOrderNo(), buyQty, giftMultiplier, template.getIncludeBuyQty()); tenantId, order.getOrderNo(), rawBuyQty, giftMultiplier, template.getIncludeBuyQty());
return IssueOutcome.SKIPPED; return IssueOutcome.SKIPPED;
} }
@@ -252,11 +264,11 @@ public class GltTicketIssueService {
userTicket.setOrderNo(order.getOrderNo()); userTicket.setOrderNo(order.getOrderNo());
userTicket.setOrderGoodsId(og.getId()); userTicket.setOrderGoodsId(og.getId());
userTicket.setTotalQty(totalQty); userTicket.setTotalQty(totalQty);
userTicket.setAvailableQty(buyQty); userTicket.setAvailableQty(availableQty);
userTicket.setFrozenQty(giftQty); userTicket.setFrozenQty(frozenQty);
userTicket.setUsedQty(0); userTicket.setUsedQty(0);
// 初始可用量来自“购买量”,视为已释放 // 已释放数量仅用于统计“从冻结释放到可用”的累计值初始为0释放任务会递增它
userTicket.setReleasedQty(buyQty); userTicket.setReleasedQty(0);
userTicket.setUserId(order.getUserId()); userTicket.setUserId(order.getUserId());
userTicket.setSortNumber(0); userTicket.setSortNumber(0);
userTicket.setComments("订单发放套票"); userTicket.setComments("订单发放套票");
@@ -268,25 +280,15 @@ public class GltTicketIssueService {
gltUserTicketService.save(userTicket); gltUserTicketService.save(userTicket);
// 生成释放计划(按月)
LocalDateTime baseTime = order.getPayTime() != null ? order.getPayTime() : order.getCreateTime();
if (baseTime == null) {
baseTime = now;
}
List<GltUserTicketRelease> releases = buildReleasePlan(template, userTicket, baseTime, giftQty, now);
if (!releases.isEmpty()) {
gltUserTicketReleaseService.saveBatch(releases);
}
// 发放流水 // 发放流水
GltUserTicketLog issueLog = new GltUserTicketLog(); GltUserTicketLog issueLog = new GltUserTicketLog();
issueLog.setUserTicketId(userTicket.getId()); issueLog.setUserTicketId(userTicket.getId());
issueLog.setChangeType(CHANGE_TYPE_ISSUE); issueLog.setChangeType(CHANGE_TYPE_ISSUE);
issueLog.setChangeAvailable(buyQty); issueLog.setChangeAvailable(availableQty);
issueLog.setChangeFrozen(giftQty); issueLog.setChangeFrozen(frozenQty);
issueLog.setChangeUsed(0); issueLog.setChangeUsed(0);
issueLog.setAvailableAfter(buyQty); issueLog.setAvailableAfter(availableQty);
issueLog.setFrozenAfter(giftQty); issueLog.setFrozenAfter(frozenQty);
issueLog.setUsedAfter(0); issueLog.setUsedAfter(0);
issueLog.setOrderId(order.getOrderId()); issueLog.setOrderId(order.getOrderId());
issueLog.setOrderNo(order.getOrderNo()); issueLog.setOrderNo(order.getOrderNo());
@@ -300,31 +302,69 @@ public class GltTicketIssueService {
issueLog.setUpdateTime(now); issueLog.setUpdateTime(now);
gltUserTicketLogService.save(issueLog); gltUserTicketLogService.save(issueLog);
if (order.getTotalNum() != null && og.getTotalNum() != null && !Objects.equals(order.getTotalNum(), og.getTotalNum())) {
log.info("套票发放提示:订单总数量与订单商品数量不一致 - tenantId={}, orderNo={}, orderTotalNum={}, orderGoodsId={}, goodsId={}, orderGoodsTotalNum={}",
tenantId, order.getOrderNo(), order.getTotalNum(), og.getId(), og.getGoodsId(), og.getTotalNum());
}
// 按模板配置:自动“使用掉第一次水票”(起始送水数量) // 按模板配置:自动“使用掉第一次水票”(起始送水数量)
Integer startSendQtyObj = template.getStartSendQty(); Integer startSendQtyObj = template.getStartSendQty();
int startSendQty = startSendQtyObj != null ? startSendQtyObj : 0; int startSendQty = startSendQtyObj != null ? startSendQtyObj : 0;
if (startSendQty > 0) { if (startSendQty > 0) {
int availableBefore = userTicket.getAvailableQty() != null ? userTicket.getAvailableQty() : 0; int availableBefore = userTicket.getAvailableQty() != null ? userTicket.getAvailableQty() : 0;
int frozenBefore = userTicket.getFrozenQty() != null ? userTicket.getFrozenQty() : 0;
int usedBefore = userTicket.getUsedQty() != null ? userTicket.getUsedQty() : 0; int usedBefore = userTicket.getUsedQty() != null ? userTicket.getUsedQty() : 0;
int toUse = Math.min(startSendQty, availableBefore); int toUse = Math.min(startSendQty, availableBefore + frozenBefore);
if (toUse > 0) { if (toUse > 0) {
userTicket.setAvailableQty(availableBefore - toUse); int useFromAvailable = Math.min(toUse, availableBefore);
int useFromFrozen = Math.min(toUse - useFromAvailable, frozenBefore);
userTicket.setAvailableQty(availableBefore - useFromAvailable);
userTicket.setFrozenQty(frozenBefore - useFromFrozen);
userTicket.setUsedQty(usedBefore + toUse); userTicket.setUsedQty(usedBefore + toUse);
userTicket.setUpdateTime(now); userTicket.setUpdateTime(now);
gltUserTicketService.updateById(userTicket); if (!gltUserTicketService.updateById(userTicket)) {
throw new IllegalStateException("起始送水核销失败:更新用户水票失败 userTicketId=" + userTicket.getId());
}
// 同步生成一条“送水订单”(待配送),用于承载这次起始送水的核销。
// 这里不走 createWithWriteOff它会再次扣减水票而是复用本次已完成的扣减结果。
GltTicketOrder firstOrder = new GltTicketOrder();
firstOrder.setUserTicketId(userTicket.getId());
firstOrder.setStoreId(order.getStoreId());
firstOrder.setWarehouseId(order.getWarehouseId());
firstOrder.setAddressId(order.getAddressId());
firstOrder.setAddress(order.getAddress());
firstOrder.setBuyerRemarks(order.getBuyerRemarks());
firstOrder.setPrice(order.getPrice());
firstOrder.setTotalNum(toUse);
LocalDateTime sendTimeBase = order.getPayTime() != null ? order.getPayTime() : (order.getCreateTime() != null ? order.getCreateTime() : now);
firstOrder.setSendTime(sendTimeBase.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
firstOrder.setDeliveryStatus(GltTicketOrderService.DELIVERY_STATUS_WAITING);
firstOrder.setUserId(order.getUserId());
firstOrder.setSortNumber(0);
firstOrder.setComments("起始送水自动生成");
firstOrder.setStatus(0);
firstOrder.setDeleted(0);
firstOrder.setTenantId(tenantId);
firstOrder.setCreateTime(now);
firstOrder.setUpdateTime(now);
if (!gltTicketOrderService.save(firstOrder)) {
throw new IllegalStateException("起始送水核销失败:生成送水订单失败 userTicketId=" + userTicket.getId());
}
GltUserTicketLog writeOffLog = new GltUserTicketLog(); GltUserTicketLog writeOffLog = new GltUserTicketLog();
writeOffLog.setUserTicketId(userTicket.getId()); writeOffLog.setUserTicketId(userTicket.getId());
writeOffLog.setChangeType(CHANGE_TYPE_START_SEND_WRITE_OFF); writeOffLog.setChangeType(CHANGE_TYPE_START_SEND_WRITE_OFF);
writeOffLog.setChangeAvailable(-toUse); writeOffLog.setChangeAvailable(-useFromAvailable);
writeOffLog.setChangeFrozen(0); writeOffLog.setChangeFrozen(-useFromFrozen);
writeOffLog.setChangeUsed(toUse); writeOffLog.setChangeUsed(toUse);
writeOffLog.setAvailableAfter(userTicket.getAvailableQty()); writeOffLog.setAvailableAfter(userTicket.getAvailableQty());
writeOffLog.setFrozenAfter(userTicket.getFrozenQty() != null ? userTicket.getFrozenQty() : 0); writeOffLog.setFrozenAfter(userTicket.getFrozenQty() != null ? userTicket.getFrozenQty() : 0);
writeOffLog.setUsedAfter(userTicket.getUsedQty()); writeOffLog.setUsedAfter(userTicket.getUsedQty());
// 关联原始商城订单,便于追溯该次自动核销来源 // 关联本次生成的送水订单(与 createWithWriteOff 行为一致)
writeOffLog.setOrderId(order.getOrderId()); writeOffLog.setOrderId(firstOrder.getId());
writeOffLog.setOrderNo(order.getOrderNo()); writeOffLog.setOrderNo(firstOrder.getId() == null ? null : String.valueOf(firstOrder.getId()));
writeOffLog.setUserId(order.getUserId()); writeOffLog.setUserId(order.getUserId());
writeOffLog.setSortNumber(0); writeOffLog.setSortNumber(0);
writeOffLog.setComments("起始送水自动核销"); writeOffLog.setComments("起始送水自动核销");
@@ -337,6 +377,19 @@ public class GltTicketIssueService {
} }
} }
// 生成释放计划(按月):以“当前冻结量”为准(若起始送水消耗了冻结票,则相应减少释放计划)
int frozenToRelease = userTicket.getFrozenQty() != null ? userTicket.getFrozenQty() : 0;
if (frozenToRelease > 0) {
LocalDateTime baseTime = order.getPayTime() != null ? order.getPayTime() : order.getCreateTime();
if (baseTime == null) {
baseTime = now;
}
List<GltUserTicketRelease> releases = buildReleasePlan(template, userTicket, baseTime, frozenToRelease, now);
if (!releases.isEmpty()) {
gltUserTicketReleaseService.saveBatch(releases);
}
}
log.info("套票发放成功 - tenantId={}, orderNo={}, orderGoodsId={}, templateId={}, userTicketId={}, totalQty={}", log.info("套票发放成功 - tenantId={}, orderNo={}, orderGoodsId={}, templateId={}, userTicketId={}, totalQty={}",
tenantId, order.getOrderNo(), og.getId(), template.getId(), userTicket.getId(), totalQty); tenantId, order.getOrderNo(), og.getId(), template.getId(), userTicket.getId(), totalQty);