games-development-ai/contracts/db-schemas/V21.0.0__create_game_trade_subscription.sql
zizi ca53a0db0c feat(trade): U2 R-ECON admin赋余额+会员订阅(独立流水表+幂等)
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>
2026-06-18 03:56:58 +00:00

115 lines
14 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- =============================================================================
-- 契约 #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 重放重复续期)';