feat(ticket): 实现水票分期释放功能支持

- 新增 releasePeriods 配置支持,可按总数量分期平均释放
- 修改原有逻辑:购买量立即可用,赠送量冻结按计划释放
- 当配置期数时按总票数生成每期释放计划,否则保持原逻辑
- 支持首期释放时机控制,支付成功当刻可立即释放首期票数
- 更新水票释放计划生成逻辑,期号从0开始计数
- 修正水票日志记录中的数量统计逻辑
This commit is contained in:
2026-03-02 17:32:21 +08:00
parent 521de8509b
commit 808ac75253

View File

@@ -258,7 +258,7 @@ public class GltTicketIssueService {
int giftMultiplier = template.getGiftMultiplier() != null ? template.getGiftMultiplier() : 0;
int giftQty = buyQty * Math.max(giftMultiplier, 0);
// 购买量buyQty应立即可用赠送量giftQty进入冻结并按计划释放。
// 总票数(购买量 + 赠送量)
int totalQty = buyQty + giftQty;
if (totalQty <= 0) {
@@ -269,6 +269,15 @@ public class GltTicketIssueService {
LocalDateTime now = LocalDateTime.now();
boolean useReleasePeriods = template.getReleasePeriods() != null && template.getReleasePeriods() > 0;
// 释放期数releasePeriods为高优先级
// - 配置了期数:按期数平均分摊 totalQty每期释放不再“先把购买桶数一次性释放”。
// - 未配置期数:保持原逻辑(购买量立即可用,赠送量冻结并按计划释放)。
int initAvailableQty = useReleasePeriods ? 0 : buyQty;
int initFrozenQty = useReleasePeriods ? totalQty : giftQty;
int initReleasedQty = useReleasePeriods ? 0 : buyQty;
GltUserTicket userTicket = new GltUserTicket();
userTicket.setTemplateId(template.getId());
userTicket.setGoodsId(og.getGoodsId());
@@ -276,11 +285,10 @@ public class GltTicketIssueService {
userTicket.setOrderNo(order.getOrderNo());
userTicket.setOrderGoodsId(og.getId());
userTicket.setTotalQty(totalQty);
userTicket.setAvailableQty(buyQty);
userTicket.setFrozenQty(giftQty);
userTicket.setAvailableQty(initAvailableQty);
userTicket.setFrozenQty(initFrozenQty);
userTicket.setUsedQty(0);
// 初始可用量来自“购买量”,视为已释放
userTicket.setReleasedQty(buyQty);
userTicket.setReleasedQty(initReleasedQty);
userTicket.setOrderGoodsQty(og.getTotalNum());
userTicket.setUserId(order.getUserId());
userTicket.setSortNumber(0);
@@ -293,12 +301,36 @@ public class GltTicketIssueService {
gltUserTicketService.save(userTicket);
// 生成释放计划(按月)
// 生成释放计划
// - 配置 releasePeriods按 totalQty 生成每期释放量periods 优先)
// - 未配置 releasePeriods按 giftQty 生成每期释放量
LocalDateTime baseTime = order.getPayTime() != null ? order.getPayTime() : order.getCreateTime();
if (baseTime == null) {
baseTime = now;
}
List<GltUserTicketRelease> releases = buildReleasePlan(template, userTicket, baseTime, giftQty, now);
int planQty = useReleasePeriods ? totalQty : giftQty;
List<GltUserTicketRelease> releases = buildReleasePlan(template, userTicket, baseTime, planQty, now);
// 若启用了 releasePeriods 且首期释放时机为“支付成功当刻”,则将首期释放量直接计入可用,
// 避免用户刚购买后短时间内无可用水票;后续期数仍由自动释放任务按 release_time 释放。
if (useReleasePeriods && !releases.isEmpty() && !Objects.equals(template.getFirstReleaseMode(), 1)) {
GltUserTicketRelease first = releases.get(0);
Integer firstQtyObj = first.getReleaseQty();
LocalDateTime firstTime = first.getReleaseTime();
int firstQty = firstQtyObj != null ? firstQtyObj : 0;
if (firstQty > 0 && (firstTime == null || !firstTime.isAfter(now))) {
first.setStatus(1);
first.setUpdateTime(now);
userTicket.setAvailableQty((userTicket.getAvailableQty() != null ? userTicket.getAvailableQty() : 0) + firstQty);
userTicket.setFrozenQty((userTicket.getFrozenQty() != null ? userTicket.getFrozenQty() : 0) - firstQty);
userTicket.setReleasedQty((userTicket.getReleasedQty() != null ? userTicket.getReleasedQty() : 0) + firstQty);
userTicket.setUpdateTime(now);
if (!gltUserTicketService.updateById(userTicket)) {
throw new IllegalStateException("首期释放:更新用户水票失败 userTicketId=" + userTicket.getId());
}
}
}
if (!releases.isEmpty()) {
gltUserTicketReleaseService.saveBatch(releases);
}
@@ -307,11 +339,11 @@ public class GltTicketIssueService {
GltUserTicketLog issueLog = new GltUserTicketLog();
issueLog.setUserTicketId(userTicket.getId());
issueLog.setChangeType(CHANGE_TYPE_ISSUE);
issueLog.setChangeAvailable(buyQty);
issueLog.setChangeFrozen(giftQty);
issueLog.setChangeAvailable(userTicket.getAvailableQty() != null ? userTicket.getAvailableQty() : 0);
issueLog.setChangeFrozen(userTicket.getFrozenQty() != null ? userTicket.getFrozenQty() : 0);
issueLog.setChangeUsed(0);
issueLog.setAvailableAfter(buyQty);
issueLog.setFrozenAfter(giftQty);
issueLog.setAvailableAfter(userTicket.getAvailableQty() != null ? userTicket.getAvailableQty() : 0);
issueLog.setFrozenAfter(userTicket.getFrozenQty() != null ? userTicket.getFrozenQty() : 0);
issueLog.setUsedAfter(0);
issueLog.setOrderId(order.getOrderId());
issueLog.setOrderNo(order.getOrderNo());
@@ -402,12 +434,13 @@ public class GltTicketIssueService {
if (releasePeriods != null && releasePeriods > 0) {
int base = totalQty / releasePeriods;
int remainder = totalQty % releasePeriods;
for (int i = 1; i <= releasePeriods; i++) {
int qty = base + (i <= remainder ? 1 : 0);
// periodNo 从 0 开始第0期、第1期……更贴近任务执行计数
for (int i = 0; i < releasePeriods; i++) {
int qty = base + (i < remainder ? 1 : 0);
if (qty <= 0) {
continue;
}
list.add(buildRelease(userTicket, i, qty, firstReleaseTime.plusMonths(i - 1), now));
list.add(buildRelease(userTicket, i, qty, firstReleaseTime.plusMonths(i), now));
}
return list;
}
@@ -417,13 +450,14 @@ public class GltTicketIssueService {
: 10;
int periods = (totalQty + monthlyReleaseQty - 1) / monthlyReleaseQty;
int remaining = totalQty;
for (int i = 1; i <= periods; i++) {
// periodNo 从 0 开始第0期、第1期……
for (int i = 0; i < periods; i++) {
int qty = Math.min(monthlyReleaseQty, remaining);
if (qty <= 0) {
break;
}
remaining -= qty;
list.add(buildRelease(userTicket, i, qty, firstReleaseTime.plusMonths(i - 1), now));
list.add(buildRelease(userTicket, i, qty, firstReleaseTime.plusMonths(i), now));
}
return list;