feat(ticket): 添加套票发放定时任务和核心服务
- 实现 GltTicketIssue10584Task 定时任务,每分钟扫描今日订单并生成套票账户 - 创建 GltTicketIssueService 服务,处理从订单生成用户套票和释放计划的完整流程 - 支持幂等处理,防止重复发放套票 - 实现月度释放计划生成功能,支持首期立即释放或次月释放模式 - 添加多租户支持和并发控制,确保任务执行安全 - 集成订单状态检查、套票模板验证和发放流水记录功能
This commit is contained in:
@@ -0,0 +1,334 @@
|
||||
package com.gxwebsoft.glt.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
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.service.ShopOrderGoodsService;
|
||||
import com.gxwebsoft.shop.service.ShopOrderService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 套票发放(从订单生成用户套票 + 释放计划)的业务逻辑。
|
||||
*
|
||||
* 说明:
|
||||
* - 定时任务无登录态时,MyBatis-Plus 多租户插件可能拿不到 tenantId;
|
||||
* 外层任务方法会通过 @IgnoreTenant 禁用租户拦截,本服务内部强制用 tenantId 过滤。
|
||||
* - 幂等:以 (tenantId, templateId, orderNo, orderGoodsId) 判断是否已发放。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class GltTicketIssueService {
|
||||
|
||||
public static final int CHANGE_TYPE_ISSUE = 10;
|
||||
|
||||
private final ShopOrderService shopOrderService;
|
||||
private final ShopOrderGoodsService shopOrderGoodsService;
|
||||
|
||||
private final GltTicketTemplateService gltTicketTemplateService;
|
||||
private final GltUserTicketService gltUserTicketService;
|
||||
private final GltUserTicketReleaseService gltUserTicketReleaseService;
|
||||
private final GltUserTicketLogService gltUserTicketLogService;
|
||||
private final TransactionTemplate transactionTemplate;
|
||||
|
||||
/**
|
||||
* 扫描“今日订单”,执行套票发放。
|
||||
*/
|
||||
public void issueTodayOrders(Integer tenantId, Integer formId) {
|
||||
LocalDateTime todayStart = LocalDate.now().atStartOfDay();
|
||||
LocalDateTime tomorrowStart = todayStart.plusDays(1);
|
||||
|
||||
List<ShopOrder> orders = shopOrderService.list(
|
||||
new LambdaQueryWrapper<ShopOrder>()
|
||||
.eq(ShopOrder::getTenantId, tenantId)
|
||||
.eq(ShopOrder::getFormId, formId)
|
||||
.eq(ShopOrder::getPayStatus, true)
|
||||
.eq(ShopOrder::getOrderStatus, 0)
|
||||
// 今日订单(兼容:以 create_time 或 pay_time 任一落在今日即可)
|
||||
.and(w -> w
|
||||
.ge(ShopOrder::getCreateTime, todayStart).lt(ShopOrder::getCreateTime, tomorrowStart)
|
||||
.or()
|
||||
.ge(ShopOrder::getPayTime, todayStart).lt(ShopOrder::getPayTime, tomorrowStart)
|
||||
)
|
||||
.orderByAsc(ShopOrder::getPayTime)
|
||||
.orderByAsc(ShopOrder::getOrderId)
|
||||
);
|
||||
|
||||
if (orders.isEmpty()) {
|
||||
log.debug("套票发放扫描:今日无符合条件的订单 tenantId={}, formId={}", tenantId, formId);
|
||||
return;
|
||||
}
|
||||
|
||||
int success = 0;
|
||||
int skipped = 0;
|
||||
int failed = 0;
|
||||
|
||||
for (ShopOrder order : orders) {
|
||||
try {
|
||||
int issuedCount = issueForOrder(tenantId, formId, order);
|
||||
if (issuedCount > 0) {
|
||||
success += issuedCount;
|
||||
} else {
|
||||
skipped++;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
failed++;
|
||||
log.error("套票发放失败 - tenantId={}, orderNo={}, orderId={}",
|
||||
tenantId, order.getOrderNo(), order.getOrderId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("套票发放扫描完成 - tenantId={}, formId={}, 订单数={}, 发放成功={}, 跳过={}, 失败={}",
|
||||
tenantId, formId, orders.size(), success, skipped, failed);
|
||||
}
|
||||
|
||||
private int issueForOrder(Integer tenantId, Integer formId, ShopOrder order) {
|
||||
List<ShopOrderGoods> goodsList = shopOrderGoodsService.getListByOrderIdIgnoreTenant(order.getOrderId());
|
||||
if (goodsList == null || goodsList.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int issuedCount = 0;
|
||||
for (ShopOrderGoods og : goodsList) {
|
||||
if (!Objects.equals(og.getGoodsId(), formId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean issued = Boolean.TRUE.equals(transactionTemplate.execute(status -> doIssueOne(tenantId, order, og)));
|
||||
if (issued) {
|
||||
issuedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return issuedCount;
|
||||
}
|
||||
|
||||
private boolean doIssueOne(Integer tenantId, ShopOrder order, ShopOrderGoods og) {
|
||||
if (order.getUserId() == null) {
|
||||
log.warn("套票发放跳过:订单缺少 userId - tenantId={}, orderNo={}", tenantId, order.getOrderNo());
|
||||
return false;
|
||||
}
|
||||
if (og.getId() == null) {
|
||||
log.warn("套票发放跳过:订单商品缺少 id - tenantId={}, orderNo={}", tenantId, order.getOrderNo());
|
||||
return false;
|
||||
}
|
||||
|
||||
GltTicketTemplate template = gltTicketTemplateService.getOne(
|
||||
new LambdaQueryWrapper<GltTicketTemplate>()
|
||||
.eq(GltTicketTemplate::getTenantId, tenantId)
|
||||
.eq(GltTicketTemplate::getGoodsId, og.getGoodsId())
|
||||
.eq(GltTicketTemplate::getDeleted, 0)
|
||||
.last("limit 1")
|
||||
);
|
||||
|
||||
if (template == null) {
|
||||
log.warn("套票发放跳过:未配置套票模板 - tenantId={}, goodsId={}, orderNo={}",
|
||||
tenantId, og.getGoodsId(), order.getOrderNo());
|
||||
return false;
|
||||
}
|
||||
if (!Boolean.TRUE.equals(template.getEnabled())) {
|
||||
log.info("套票发放跳过:套票模板未启用 - tenantId={}, templateId={}, goodsId={}",
|
||||
tenantId, template.getId(), template.getGoodsId());
|
||||
return false;
|
||||
}
|
||||
|
||||
// 幂等:同一 orderNo + orderGoodsId 只发放一次
|
||||
GltUserTicket existing = gltUserTicketService.getOne(
|
||||
new LambdaQueryWrapper<GltUserTicket>()
|
||||
.eq(GltUserTicket::getTenantId, tenantId)
|
||||
.eq(GltUserTicket::getTemplateId, template.getId())
|
||||
.eq(GltUserTicket::getOrderNo, order.getOrderNo())
|
||||
.eq(GltUserTicket::getOrderGoodsId, og.getId())
|
||||
.eq(GltUserTicket::getDeleted, 0)
|
||||
.last("limit 1")
|
||||
);
|
||||
if (existing != null) {
|
||||
log.debug("套票已发放,跳过 - tenantId={}, orderNo={}, orderGoodsId={}, userTicketId={}",
|
||||
tenantId, order.getOrderNo(), og.getId(), existing.getId());
|
||||
return false;
|
||||
}
|
||||
|
||||
int buyQty = og.getTotalNum() != null ? og.getTotalNum() : 0;
|
||||
if (buyQty <= 0) {
|
||||
log.warn("套票发放跳过:购买数量无效 - tenantId={}, orderNo={}, orderGoodsId={}, totalNum={}",
|
||||
tenantId, order.getOrderNo(), og.getId(), og.getTotalNum());
|
||||
return false;
|
||||
}
|
||||
|
||||
Integer minBuyQty = template.getMinBuyQty();
|
||||
if (minBuyQty != null && minBuyQty > 0 && buyQty < minBuyQty) {
|
||||
log.info("套票发放跳过:未达到最小购买数量 - tenantId={}, orderNo={}, buyQty={}, minBuyQty={}",
|
||||
tenantId, order.getOrderNo(), buyQty, minBuyQty);
|
||||
return false;
|
||||
}
|
||||
|
||||
int giftMultiplier = template.getGiftMultiplier() != null ? template.getGiftMultiplier() : 0;
|
||||
int giftQty = buyQty * Math.max(giftMultiplier, 0);
|
||||
|
||||
int totalQty = giftQty;
|
||||
if (Boolean.TRUE.equals(template.getIncludeBuyQty())) {
|
||||
totalQty += buyQty;
|
||||
}
|
||||
|
||||
if (totalQty <= 0) {
|
||||
log.info("套票发放跳过:计算结果为0 - tenantId={}, orderNo={}, buyQty={}, giftMultiplier={}, includeBuyQty={}",
|
||||
tenantId, order.getOrderNo(), buyQty, giftMultiplier, template.getIncludeBuyQty());
|
||||
return false;
|
||||
}
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
GltUserTicket userTicket = new GltUserTicket();
|
||||
userTicket.setTemplateId(template.getId());
|
||||
userTicket.setGoodsId(og.getGoodsId());
|
||||
userTicket.setOrderId(order.getOrderId());
|
||||
userTicket.setOrderNo(order.getOrderNo());
|
||||
userTicket.setOrderGoodsId(og.getId());
|
||||
userTicket.setTotalQty(totalQty);
|
||||
userTicket.setAvailableQty(0);
|
||||
userTicket.setFrozenQty(totalQty);
|
||||
userTicket.setUsedQty(0);
|
||||
userTicket.setReleasedQty(0);
|
||||
userTicket.setUserId(order.getUserId());
|
||||
userTicket.setSortNumber(0);
|
||||
userTicket.setComments("订单发放套票");
|
||||
userTicket.setStatus(0);
|
||||
userTicket.setDeleted(0);
|
||||
userTicket.setTenantId(tenantId);
|
||||
userTicket.setCreateTime(now);
|
||||
userTicket.setUpdateTime(now);
|
||||
|
||||
gltUserTicketService.save(userTicket);
|
||||
|
||||
// 生成释放计划(按月)
|
||||
LocalDateTime baseTime = order.getPayTime() != null ? order.getPayTime() : order.getCreateTime();
|
||||
if (baseTime == null) {
|
||||
baseTime = now;
|
||||
}
|
||||
List<GltUserTicketRelease> releases = buildReleasePlan(template, userTicket, baseTime, totalQty, now);
|
||||
if (!releases.isEmpty()) {
|
||||
gltUserTicketReleaseService.saveBatch(releases);
|
||||
}
|
||||
|
||||
// 发放流水
|
||||
GltUserTicketLog issueLog = new GltUserTicketLog();
|
||||
issueLog.setUserTicketId(userTicket.getId());
|
||||
issueLog.setChangeType(CHANGE_TYPE_ISSUE);
|
||||
issueLog.setChangeAvailable(0);
|
||||
issueLog.setChangeFrozen(totalQty);
|
||||
issueLog.setChangeUsed(0);
|
||||
issueLog.setAvailableAfter(0);
|
||||
issueLog.setFrozenAfter(totalQty);
|
||||
issueLog.setUsedAfter(0);
|
||||
issueLog.setOrderId(order.getOrderId());
|
||||
issueLog.setOrderNo(order.getOrderNo());
|
||||
issueLog.setUserId(order.getUserId());
|
||||
issueLog.setSortNumber(0);
|
||||
issueLog.setComments("套票发放");
|
||||
issueLog.setStatus(0);
|
||||
issueLog.setDeleted(0);
|
||||
issueLog.setTenantId(tenantId);
|
||||
issueLog.setCreateTime(now);
|
||||
issueLog.setUpdateTime(now);
|
||||
gltUserTicketLogService.save(issueLog);
|
||||
|
||||
log.info("套票发放成功 - tenantId={}, orderNo={}, orderGoodsId={}, templateId={}, userTicketId={}, totalQty={}",
|
||||
tenantId, order.getOrderNo(), og.getId(), template.getId(), userTicket.getId(), totalQty);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private List<GltUserTicketRelease> buildReleasePlan(GltTicketTemplate template,
|
||||
GltUserTicket userTicket,
|
||||
LocalDateTime baseTime,
|
||||
int totalQty,
|
||||
LocalDateTime now) {
|
||||
List<GltUserTicketRelease> list = new ArrayList<>();
|
||||
|
||||
if (totalQty <= 0) {
|
||||
return list;
|
||||
}
|
||||
|
||||
// 首期释放时间
|
||||
LocalDateTime firstReleaseTime;
|
||||
if (Objects.equals(template.getFirstReleaseMode(), 1)) {
|
||||
firstReleaseTime = nextMonthSameDay(baseTime);
|
||||
} else {
|
||||
firstReleaseTime = baseTime;
|
||||
}
|
||||
|
||||
// 每期释放数量计算
|
||||
Integer releasePeriods = template.getReleasePeriods();
|
||||
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);
|
||||
if (qty <= 0) {
|
||||
continue;
|
||||
}
|
||||
list.add(buildRelease(userTicket, i, qty, firstReleaseTime.plusMonths(i - 1), now));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
int monthlyReleaseQty = template.getMonthlyReleaseQty() != null && template.getMonthlyReleaseQty() > 0
|
||||
? template.getMonthlyReleaseQty()
|
||||
: 10;
|
||||
int periods = (totalQty + monthlyReleaseQty - 1) / monthlyReleaseQty;
|
||||
int remaining = totalQty;
|
||||
for (int i = 1; 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));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private GltUserTicketRelease buildRelease(GltUserTicket userTicket,
|
||||
int periodNo,
|
||||
int releaseQty,
|
||||
LocalDateTime releaseTime,
|
||||
LocalDateTime now) {
|
||||
GltUserTicketRelease r = new GltUserTicketRelease();
|
||||
r.setUserTicketId(userTicket.getId() != null ? userTicket.getId().longValue() : null);
|
||||
r.setUserId(userTicket.getUserId());
|
||||
r.setPeriodNo(periodNo);
|
||||
r.setReleaseQty(releaseQty);
|
||||
r.setReleaseTime(releaseTime);
|
||||
r.setStatus(0);
|
||||
r.setDeleted(0);
|
||||
r.setTenantId(userTicket.getTenantId());
|
||||
r.setCreateTime(now);
|
||||
r.setUpdateTime(now);
|
||||
return r;
|
||||
}
|
||||
|
||||
private static LocalDateTime nextMonthSameDay(LocalDateTime baseTime) {
|
||||
LocalDate baseDate = baseTime.toLocalDate();
|
||||
LocalTime time = baseTime.toLocalTime();
|
||||
|
||||
LocalDate nextMonth = baseDate.plusMonths(1);
|
||||
int day = Math.min(baseDate.getDayOfMonth(), nextMonth.lengthOfMonth());
|
||||
LocalDate adjusted = nextMonth.withDayOfMonth(day);
|
||||
return LocalDateTime.of(adjusted, time);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.gxwebsoft.glt.task;
|
||||
|
||||
import com.gxwebsoft.common.core.annotation.IgnoreTenant;
|
||||
import com.gxwebsoft.glt.service.GltTicketIssueService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* GLT 套票发放任务:
|
||||
* - 每分钟扫描一次今日订单(tenantId=10584, formId=10074, payStatus=1, orderStatus=0)
|
||||
* - 为订单生成用户套票账户 + 释放计划(幂等)
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnProperty(prefix = "glt.ticket.issue10584", name = "enabled", havingValue = "true", matchIfMissing = true)
|
||||
public class GltTicketIssue10584Task {
|
||||
|
||||
private static final int TENANT_ID = 10584;
|
||||
private static final int FORM_ID = 10074;
|
||||
|
||||
private final GltTicketIssueService gltTicketIssueService;
|
||||
|
||||
private final AtomicBoolean running = new AtomicBoolean(false);
|
||||
|
||||
@Scheduled(cron = "${glt.ticket.issue10584.cron:0 */1 * * * ?}")
|
||||
@IgnoreTenant("定时任务无登录态,需忽略租户隔离;内部使用 tenantId=10584 精确过滤")
|
||||
public void run() {
|
||||
if (!running.compareAndSet(false, true)) {
|
||||
log.warn("套票发放任务仍在执行中,本轮跳过 - tenantId={}, formId={}", TENANT_ID, FORM_ID);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
gltTicketIssueService.issueTodayOrders(TENANT_ID, FORM_ID);
|
||||
} finally {
|
||||
running.set(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user