codex 评审 NO-MERGE→修 P0(订阅幂等漏洞:独立幂等账本表 game_trade_subscription_grant+uk_biz_no,延期前先写、同事务)+P1(VO @Size/totalIncome注释对齐/权限种子SQL)。V21。plan 2026-06-18-002 U2。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
115 lines
14 KiB
SQL
115 lines
14 KiB
SQL
-- =============================================================================
|
||
-- 契约 #2 DB 迁移 | 模块:trade(game-module-trade,U2 经济:admin 赋余额 + 会员订阅)| owner:6c6g 后端线
|
||
-- 文件:V21.0.0__create_game_trade_subscription.sql(Flyway,只新增;已合入禁止修改,回滚写新补偿迁移)
|
||
-- 内容:U2 R-ECON 三张新表 ——
|
||
-- ① game_trade_grant —— admin 赋余额流水(运营手动给创作者账户充值的明细,余额增的来源凭证,可对账)
|
||
-- ② game_trade_subscription —— 会员订阅(三档套餐 plan + 到期时间 expire_time + 状态 status,admin 赋订阅 + C 端查订阅态)
|
||
-- ③ game_trade_subscription_grant —— 会员订阅赋值幂等账本(每笔赋订阅记一行 + uk_biz_no,挡任意历史 bizNo 重放重复续期)
|
||
-- 背景(AGENTS §1 三条变现线:广告分成 / 订阅会员 / B 端定制):
|
||
-- - 钱包/收益/提现已 real(V7/V14);本轮补「admin 赋余额」(运营给余额,real 入账 + 流水)与「订阅域」(实体表 + 赋订阅 + 查态)。
|
||
-- - admin 赋余额「不」走 game_trade_income:income 是「广告/打赏分账」流水,进平台营收报表 gross/net 聚合;
|
||
-- admin 手动赋余额是运营动作,混入 income 会污染营收报表与对账锚点(source_ref 对齐 ad 台账),故单独建 game_trade_grant 流水表。
|
||
-- - 账户 game_trade_account 余额仍由原子增(balance += amount、total_income += amount,维持不变式 balance+frozen+total_withdraw=total_income)。
|
||
-- 范围边界:真实支付收单/自助购买订阅(接 pay 下单回调开通)= 日历闸门,本轮不做;订阅由 admin 手动赋(运营/B 端开通)。
|
||
-- 约定:InnoDB + utf8mb4;显式列;含 Huijing 审计列 creator/create_time/updater/update_time/deleted + tenant_id;
|
||
-- 金额一律「分」(BIGINT) 禁浮点;资金写入有幂等键(biz_no + uk);状态机用 tinyint;中文列注释;关键查询建索引、禁裸 select*。
|
||
-- 错误码段:trade = 1-106-***-***(赋余额 005 / 订阅 006)
|
||
-- =============================================================================
|
||
|
||
-- -----------------------------------------------------------------------------
|
||
-- 表:game_trade_grant —— admin 赋余额流水(运营手动给创作者账户充值的明细凭证;余额增的来源,可对账)
|
||
-- 写入点:admin 赋余额(AccountService.grant)→ 写一条 grant 流水 + 账户 balance/total_income 原子增(同事务)。
|
||
-- 幂等核心:uk_biz_no(biz_no, tenant_id) 防重复赋余额——同一 biz_no 重复提交返回已有记录,不重复入账(不重复发钱)。
|
||
-- biz_no 由调用端(admin 前端/运营)生成唯一;幂等范式与 game_trade_withdraw.uk_biz_no 一致。
|
||
-- 归属:user_id = 被赋余额的创作者;operator_user_id = 操作的运营/admin(审计可溯,区别于 Huijing 审计列 creator)。
|
||
-- 金额单位:amount 一律「分」(BIGINT) 禁浮点;赋余额只增不减(amount > 0,由 Service 校验)。
|
||
-- -----------------------------------------------------------------------------
|
||
CREATE TABLE `game_trade_grant` (
|
||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '赋余额流水 ID',
|
||
`user_id` BIGINT NOT NULL COMMENT '被赋余额的创作者用户 ID(账户归属)',
|
||
`amount` BIGINT NOT NULL COMMENT '赋余额金额(单位:分,BIGINT 禁浮点;只增不减,>0)',
|
||
`biz_no` VARCHAR(64) NOT NULL COMMENT '赋余额业务单号 = 幂等键(调用端生成唯一;重复提交同一 biz_no 不重复入账)',
|
||
`source` VARCHAR(32) NOT NULL DEFAULT 'admin_grant' COMMENT '赋余额来源标识(固定 admin_grant;区别于收益流水 income 的 ad/tip 分账来源)',
|
||
`operator_user_id` BIGINT NULL COMMENT '操作人用户 ID(运营/admin,RBAC;审计可溯,区别于审计列 creator)',
|
||
`remark` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '赋余额备注(运营填写赋值原因,如「活动奖励」「客诉补偿」)',
|
||
`creator` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建者(Huijing 审计列)',
|
||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间(赋余额时间)',
|
||
`updater` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '更新者',
|
||
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '逻辑删除:0未删 1已删',
|
||
`tenant_id` BIGINT NOT NULL DEFAULT 0 COMMENT '租户 ID(Huijing 多租户兼容,MVP 单租户=0)',
|
||
PRIMARY KEY (`id`),
|
||
UNIQUE KEY `uk_biz_no` (`biz_no`, `tenant_id`) COMMENT '赋余额幂等键:同一 biz_no 只入账一次(防重复赋余额/重复发钱)',
|
||
KEY `idx_user` (`user_id`) COMMENT '按创作者查赋余额记录(运营对账)'
|
||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = 'admin 赋余额流水表(运营手动充值明细,余额增来源,uk 防重复入账)';
|
||
|
||
-- -----------------------------------------------------------------------------
|
||
-- 表:game_trade_subscription —— 会员订阅(三档套餐 + 到期时间 + 状态;admin 赋订阅 + C 端查订阅态)
|
||
-- 一个用户一条订阅记录:uk_user(user_id, tenant_id) —— 赋订阅/续期 upsert 同一行(叠加 expire_time),不为每次赋值建新行。
|
||
-- 赋订阅幂等(P0 修复):由独立账本表 game_trade_subscription_grant.uk_biz_no 保证——挡「任意历史 bizNo」重放重复续期。
|
||
-- 注:本行 last_grant_biz_no 仅记「最近一次」赋订阅 bizNo 供审计追溯,**不再作幂等判据**(行内键只能挡最近一次重放,
|
||
-- 挡不住历史 bizNo 重放:A 续期→键=A,B 续期→键=B,重放 A 时键(B)≠A 会误判新请求重复续期)。订阅是「一用户一行」状态聚合,
|
||
-- 续期叠加在同一行,但幂等必须靠独立账本(与赋余额 grant、提现 withdraw 同范式:每笔一行 + uk_biz_no)。
|
||
-- 套餐 plan(枚举 TradeSubscriptionPlanEnum):1 月度 / 2 季度 / 3 年度(仅标识权益档位,到期时间由赋订阅时长决定,不写死天数)。
|
||
-- 状态 status(枚举 TradeSubscriptionStatusEnum):0 生效中 / 1 已过期。
|
||
-- MVP 无 cron 物化过期:「是否有效」以 expire_time 与当前时间比较为权威实时判定(Service 查询时按 expire_time>now 回算 effectiveStatus),
|
||
-- status 列落「赋订阅初始态」与后续定时 job 物化预留;C 端查态不依赖定时 job 也能正确反映过期。
|
||
-- 续期叠加:赋订阅时若已有未过期订阅,新到期 = max(now, 旧 expire_time) + 时长(不丢未用时长);已过期则从 now 起算。
|
||
-- 范围边界:真实支付收单/自助购买 = 日历闸门,本轮 admin 手动赋;source 标识赋值来源(admin_grant)。
|
||
-- -----------------------------------------------------------------------------
|
||
CREATE TABLE `game_trade_subscription` (
|
||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '订阅记录 ID',
|
||
`user_id` BIGINT NOT NULL COMMENT '订阅用户 ID(DataPermission:用户只见自己订阅)',
|
||
`plan` TINYINT NOT NULL COMMENT '订阅套餐:1 月度 / 2 季度 / 3 年度(枚举 TradeSubscriptionPlanEnum)',
|
||
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0 生效中 / 1 已过期(枚举 TradeSubscriptionStatusEnum;权威有效性以 expire_time>now 实时判定)',
|
||
`start_time` DATETIME NOT NULL COMMENT '本次订阅生效起始时间(首次赋订阅或续期起算点)',
|
||
`expire_time` DATETIME NOT NULL COMMENT '到期时间(权威字段:expire_time>now 即有效;续期时叠加,不丢未用时长)',
|
||
`source` VARCHAR(32) NOT NULL DEFAULT 'admin_grant' COMMENT '订阅来源标识(固定 admin_grant;真实自助购买 later 接 pay 回调)',
|
||
`last_grant_biz_no` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '最近一次赋订阅业务单号(仅审计追溯;幂等由 game_trade_subscription_grant.uk_biz_no 保证,本列非幂等键)',
|
||
`operator_user_id` BIGINT NULL COMMENT '操作人用户 ID(运营/admin 赋订阅,RBAC;审计可溯)',
|
||
`remark` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '订阅备注(运营填写开通原因,如「B 端定制开通」)',
|
||
`creator` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建者(Huijing 审计列)',
|
||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间(首次赋订阅时间)',
|
||
`updater` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '更新者',
|
||
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间(续期时更新)',
|
||
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '逻辑删除:0未删 1已删',
|
||
`tenant_id` BIGINT NOT NULL DEFAULT 0 COMMENT '租户 ID(Huijing 多租户兼容,MVP 单租户=0)',
|
||
PRIMARY KEY (`id`),
|
||
UNIQUE KEY `uk_user` (`user_id`, `tenant_id`) COMMENT '一个用户一条订阅记录(赋订阅/续期 upsert 同一行,防并发重复建)',
|
||
KEY `idx_expire` (`expire_time`) COMMENT '按到期时间扫描(后续过期物化 job 用)'
|
||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '会员订阅表(三档套餐+到期时间+状态;admin 赋订阅,expire_time 权威判有效)';
|
||
|
||
-- -----------------------------------------------------------------------------
|
||
-- 表:game_trade_subscription_grant —— 会员订阅赋值幂等账本(每笔赋订阅记一行;挡任意历史 bizNo 重放重复续期)
|
||
-- 写入点:admin 赋订阅(SubscriptionService.grantSubscription)→ 续期前先 insert 一行账本 + 订阅 upsert 续期(同事务)。
|
||
-- 【为什么需要本表(P0 NO-MERGE 修复)】
|
||
-- 订阅是「一用户一行」状态聚合(game_trade_subscription.uk_user,续期叠加 expire_time),其行内 last_grant_biz_no
|
||
-- 只记「最近一次」赋订阅 bizNo——只能挡最近一次重放,挡不住历史 bizNo 重放:
|
||
-- bizNo=A 续期 → 行内键=A;bizNo=B 续期 → 行内键=B;此时重放 bizNo=A,行内键(B)≠A → 误判新请求再次续期(重复发权益)。
|
||
-- 故照搬赋余额 game_trade_grant 的「独立流水表 + uk_biz_no」范式:每笔赋订阅先 insert 本表,撞键即「已处理」幂等返回。
|
||
-- 幂等核心:uk_biz_no(biz_no, tenant_id) 防重复赋订阅——同一 biz_no 重复提交返回当前订阅态,不重复延长(重试安全)。
|
||
-- biz_no 由调用端(admin 前端/运营)生成唯一;幂等范式与 game_trade_grant.uk_biz_no / game_trade_withdraw.uk_biz_no 一致。
|
||
-- 归属:user_id = 被赋订阅的用户;operator_user_id = 操作的运营/admin(审计可溯,区别于 Huijing 审计列 creator)。
|
||
-- 落账快照:plan/duration_days 记本次赋订阅入参;expire_time_after 记赋值后到期时间(续期成功后回填,供对账与幂等回显)。
|
||
-- -----------------------------------------------------------------------------
|
||
CREATE TABLE `game_trade_subscription_grant` (
|
||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '赋订阅流水 ID',
|
||
`user_id` BIGINT NOT NULL COMMENT '被赋订阅的用户 ID(订阅归属)',
|
||
`biz_no` VARCHAR(64) NOT NULL COMMENT '赋订阅业务单号 = 幂等键(调用端生成唯一;重复提交同一 biz_no 不重复延长权益)',
|
||
`plan` TINYINT NOT NULL COMMENT '本次赋订阅套餐:1 月度 / 2 季度 / 3 年度(枚举 TradeSubscriptionPlanEnum,落账快照)',
|
||
`duration_days` INT NOT NULL COMMENT '本次赋订阅时长(天,落账快照;续期叠加的时长来源凭证,>0)',
|
||
`expire_time_after` DATETIME NULL COMMENT '本次赋值后到期时间(续期成功后回填的快照,供对账与幂等命中回显)',
|
||
`source` VARCHAR(32) NOT NULL DEFAULT 'admin_grant' COMMENT '赋订阅来源标识(固定 admin_grant;真实自助购买 later 接 pay 回调)',
|
||
`operator_user_id` BIGINT NULL COMMENT '操作人用户 ID(运营/admin,RBAC;审计可溯,区别于审计列 creator)',
|
||
`remark` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '赋订阅备注(运营填写开通原因,如「B 端定制开通」)',
|
||
`creator` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建者(Huijing 审计列)',
|
||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间(赋订阅时间)',
|
||
`updater` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '更新者',
|
||
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间(回填到期快照时更新)',
|
||
`deleted` BIT(1) NOT NULL DEFAULT b'0' COMMENT '逻辑删除:0未删 1已删',
|
||
`tenant_id` BIGINT NOT NULL DEFAULT 0 COMMENT '租户 ID(Huijing 多租户兼容,MVP 单租户=0)',
|
||
PRIMARY KEY (`id`),
|
||
UNIQUE KEY `uk_biz_no` (`biz_no`, `tenant_id`) COMMENT '赋订阅幂等键:同一 biz_no 只赋一次(防重复延长订阅权益/重复发会员)',
|
||
KEY `idx_user` (`user_id`) COMMENT '按用户查赋订阅记录(运营对账)'
|
||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '会员订阅赋值幂等账本(每笔赋订阅一行,uk 挡任意历史 bizNo 重放重复续期)';
|