feat(ticket): 实现套票分期释放功能核心数据结构

- 修改 GltUserTicketReleaseParam 中 id 和 userTicketId 类型从 Long 改为 Integer
- 移除 ShopOrderServiceImpl 中的 shopTicketBizService 依赖注入
- 注释掉订单支付成功后的套票发放调用
- 添加套票功能开发计划文档,定义套票模板、用户套票账户、释放计划和变更流水的核心概念
- 设计并创建套票相关数据库表,包括套票模板表、用户套票账户表、释放计划表和变更流水表
This commit is contained in:
2026-02-03 20:37:11 +08:00
parent 24133ef8a8
commit d7a6b7cc94
4 changed files with 155 additions and 5 deletions

View File

@@ -0,0 +1,31 @@
# 套票(冻结/可用、分期释放)功能开发计划
## 目标
- 为“买N送M例如买1送4、起售例如20桶起售、按月释放例如每月释放10桶、可用未用完叠加”的套票/权益,提供后端可配置、可发放、可释放、可消费的能力。
## 核心概念
- 套票模板:按商品(goodsId)配置买赠规则、起售/起送、释放规则(每期释放数或释放期数)、首期释放时机。
- 用户套票账户:记录总量、可用量、冻结量、已用量、已释放量;绑定订单(用于幂等与追溯)。
- 释放计划:每期一条,到期后把冻结转可用(可用未用完自然叠加)。
- 变更流水:发放/释放/消费等都记录流水,便于对账与排查。
## 关键默认规则(可在模板里改)
- 仅赠送量进入套票账户默认不包含“购买量”本身如需“买20送80=总100”可在模板设置 `includeBuyQty=true`
- 首期释放:默认“支付成功当日/当刻”释放(`firstReleaseMode=0`);如需“下个月同日释放”,设 `firstReleaseMode=1`
- 每期释放:默认按 `monthlyReleaseQty`;如配置了 `releasePeriods`,则平均分摊并处理余数。
## 开发步骤(建议按顺序)
1. 需求确认与接入点确认(订单哪个节点发放、桶票如何消费/核销、退款是否回滚、释放日期规则)。
2. 数据表设计与SQL输出模板/账户/释放计划/流水)。
3. 实现套票模板后台CRUD接口。
4. 支付成功接入:在订单支付成功后发放套票账户+生成释放计划(幂等)。
5. 定时任务释放:扫描到期释放计划,执行“冻结->可用”转账(幂等)。
6. 消费扣减对用户可用量做扣减支持跨多套票账户FIFO扣减并落流水。
7. 测试与验收:至少覆盖(买赠计算、分期拆分、叠加逻辑、幂等、并发扣减)。
## 待确认点(不确认也可先按默认实现)
- 套票数量=赠送量?还是(购买量+赠送量)?
- “释放日期”是按支付时间的“日”还是固定每月某一天31号跨月如何处理
- 退款/取消:是否回滚未使用的可用/冻结?已释放但未用如何处理?
- 消费场景:在哪个业务点扣减(下单抵扣/提货核销/线下核销)?

View File

@@ -0,0 +1,121 @@
-- 套票(冻结/可用、分期释放)相关表
-- 说明:项目使用了 MyBatis-Plus 的 tenant 拦截;表内显式保留 tenant_id 字段并建议建立联合唯一索引。
-- 1) 套票模板(按商品配置)
CREATE TABLE IF NOT EXISTS shop_ticket_template (
id INT AUTO_INCREMENT PRIMARY KEY,
goods_id INT NOT NULL,
name VARCHAR(100) NOT NULL,
enabled TINYINT(1) NOT NULL DEFAULT 1,
unit_name VARCHAR(20) NOT NULL DEFAULT '',
-- 起售/起送
min_buy_qty INT NOT NULL DEFAULT 1,
start_send_qty INT NOT NULL DEFAULT 1,
-- 买赠买1送4 => gift_multiplier=4
gift_multiplier INT NOT NULL DEFAULT 0,
-- 是否把购买量也计入套票总量(默认仅计入赠送量)
include_buy_qty TINYINT(1) NOT NULL DEFAULT 0,
-- 释放规则:二选一
-- A) 每期释放数量默认每月释放10
monthly_release_qty INT NOT NULL DEFAULT 10,
-- B) 总共释放多少期(若配置>0则按期数平均分摊
release_periods INT NULL,
-- 首期释放时机0=支付成功当刻1=下个月同日
first_release_mode INT NOT NULL DEFAULT 0,
comments VARCHAR(255) NULL,
sort_number INT NOT NULL DEFAULT 0,
user_id INT NULL,
deleted INT NOT NULL DEFAULT 0,
tenant_id INT NOT NULL,
create_time DATETIME NULL,
update_time DATETIME NULL,
UNIQUE KEY uk_ticket_template_tenant_goods (tenant_id, goods_id),
KEY idx_ticket_template_tenant (tenant_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 2) 用户套票账户(一次购买通常生成一条)
CREATE TABLE IF NOT EXISTS shop_user_ticket (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
template_id INT NOT NULL,
goods_id INT NOT NULL,
user_id INT NOT NULL,
order_id INT NULL,
order_no VARCHAR(64) NULL,
order_goods_id INT NULL,
total_qty INT NOT NULL DEFAULT 0,
available_qty INT NOT NULL DEFAULT 0,
frozen_qty INT NOT NULL DEFAULT 0,
used_qty INT NOT NULL DEFAULT 0,
released_qty INT NOT NULL DEFAULT 0,
status INT NOT NULL DEFAULT 0,
deleted INT NOT NULL DEFAULT 0,
tenant_id INT NOT NULL,
create_time DATETIME NULL,
update_time DATETIME NULL,
KEY idx_user_ticket_user (tenant_id, user_id),
KEY idx_user_ticket_order (tenant_id, order_no),
UNIQUE KEY uk_user_ticket_order_goods (tenant_id, template_id, order_no, order_goods_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 3) 释放计划/释放记录(每期一条,幂等执行)
CREATE TABLE IF NOT EXISTS shop_user_ticket_release (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_ticket_id BIGINT NOT NULL,
user_id INT NOT NULL,
period_no INT NOT NULL,
release_qty INT NOT NULL,
release_time DATETIME NOT NULL,
status INT NOT NULL DEFAULT 0, -- 0待释放 1已释放 2作废
released_time DATETIME NULL,
tenant_id INT NOT NULL,
create_time DATETIME NULL,
update_time DATETIME NULL,
UNIQUE KEY uk_ticket_release_period (tenant_id, user_ticket_id, period_no),
KEY idx_ticket_release_due (status, release_time),
KEY idx_ticket_release_user (tenant_id, user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 4) 套票变更流水(发放/释放/消费/回滚等)
CREATE TABLE IF NOT EXISTS shop_user_ticket_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_ticket_id BIGINT NOT NULL,
user_id INT NOT NULL,
change_type INT NOT NULL, -- 10发放 20释放 30消费 40回滚/退款
change_available INT NOT NULL DEFAULT 0,
change_frozen INT NOT NULL DEFAULT 0,
change_used INT NOT NULL DEFAULT 0,
available_after INT NOT NULL DEFAULT 0,
frozen_after INT NOT NULL DEFAULT 0,
used_after INT NOT NULL DEFAULT 0,
order_id INT NULL,
order_no VARCHAR(64) NULL,
remark VARCHAR(255) NULL,
tenant_id INT NOT NULL,
create_time DATETIME NULL,
update_time DATETIME NULL,
KEY idx_ticket_log_user (tenant_id, user_id),
KEY idx_ticket_log_ticket (tenant_id, user_ticket_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View File

@@ -23,10 +23,10 @@ public class GltUserTicketReleaseParam extends BaseParam {
private static final long serialVersionUID = 1L;
@QueryField(type = QueryType.EQ)
private Long id;
private Integer id;
@Schema(description = "水票ID")
private Long userTicketId;
private Integer userTicketId;
@Schema(description = "用户ID")
@QueryField(type = QueryType.EQ)

View File

@@ -84,8 +84,6 @@ public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrder
private ShopOrderDeliveryService shopOrderDeliveryService;
@Resource
private ShopExpressService shopExpressService;
@Resource
private ShopTicketBizService shopTicketBizService;
private static final long USER_ORDER_STATS_CACHE_SECONDS = 60L;
@@ -531,7 +529,7 @@ public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrder
updateGoodsSales(order);
// 3. 套票发放(冻结/可用、分期释放)
shopTicketBizService.grantTicketsForPaidOrder(order);
// shopTicketBizService.grantTicketsForPaidOrder(order);
log.info("支付成功后业务逻辑处理完成 - 订单号:{}", order.getOrderNo());
} catch (Exception e) {