feat(aigc): WU2① new-api 额度池表 V32 + DO/Mapper——离线预建 FREE 条目 claim 抢占

内测 per-user ¥100 额度落 new-api 网关账本,game-cloud 维护一张预置池表:
ops 脚本离线预建 (user+token+¥100) 灌成 FREE 条目,注册/懒 claim 时 CAS 抢一个绑玩家。
- V32.0.0 迁移双落契约源 + huijing-server 执行副本(守门①),列名对齐 ops 脚本 INSERT,
  uk_newapi_user/uk_claimed_by/uk_biz_no 三键仿 trade GrantDO 的 uk_biz_no 保幂等。
- NewapiQuotaPoolDO 继承 TenantBaseDO(系统级池,运行时 executeIgnore 跨租户);
  Mapper 提供 selectClaimedByPlayer/countFree 水位/claimOneFree 单条 FREE→CLAIMED CAS。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lili 2026-07-07 12:21:33 -07:00
parent 28bd5339d4
commit c13595fbc2
5 changed files with 276 additions and 0 deletions

View File

@ -0,0 +1,58 @@
-- =============================================================================
-- 契约 #2 DB 迁移 | 主题内测·new-api per-user ¥100 额度池WU22026-07-07 设计 §3.5| owneraigc派发期映射查询 co-locate
-- 文件V32.0.0__create_newapi_quota_pool.sqlFlyway纯新增表已合入禁止修改回滚 = drop 表 + 摘事件消费者)
-- 依据docs/agent-specs/2026-07-07-内测-WU2-newapi额度接入-设计.md草案创始人拍定「离线预置池 + 注册 claim」
-- 版本定序:执行副本 db/migration/ 实测已连续到 V31.0.0WU1 game_player 增列 + outbox本件顺延取 V32.0.0(已 ls|sort -V|tail 复核)。
-- WU1 与 WU2 同批新增迁移——WU1 取 V31、WU2 顺延 V32避免 Flyway 撞号校验失败。
-- 守门①(沿用 V11/V31本件同时落 contracts/db-schemas/(契约源)+ huijing-server 执行副本(唯一执行 classpath
-- 不放任何单模块 -server/db/migration/(避免同版本出现在多个 classpath jar 触发 Flyway 重复校验失败)。
-- 守门②:含中文 SQLmini-desktop 执行必须 --default-character-set=utf8mb4。
-- 内容CREATE newapi_quota_pool —— 离线 ops 脚本预建 FREE 条目 + 注册 claim 绑玩家FREE→CLAIMED账本在 new-api 网关。
-- =============================================================================
-- -----------------------------------------------------------------------------
-- CREATE newapi_quota_pool —— new-api per-user 额度预置池§3.5
-- 语义ops 脚本game-runtime/tools/newapi_pool_provision.py离线预建 N 个 (new-api user + token + ¥100)
-- 按条目 INSERTstatus=FREE、claimed_* 空);注册/懒 claim 时某条被 CAS 抢占翻转 CLAIMED 并绑 game_player。
-- 账本口径唯一 = new-api 网关(权威余额=token remain_quota、权威消耗=user used_quota本表只存 grant_quota 审计快照,不在 game-cloud 记余额。
-- 列名严格对齐 ops 脚本 emit 的 INSERT(newapi_user_id, newapi_token_id, newapi_token_key, grant_quota,
-- quota_per_unit_snapshot, usd_rate_snapshot, status),去重键 uk_newapi_user(newapi_user_id) 供 ON DUPLICATE KEY UPDATE 幂等补池。
-- 审计列creator/create_time/updater/update_time/deleted与 tenant_id 均给默认值ops 脚本 raw INSERT 只给业务列,
-- 审计列/租户列走 DB 默认落值;运行时 claim/查询经 TenantUtils.executeIgnore 跨租户存取(池是系统级资源)。
-- 幂等三键(仿 game_trade_grant 的 uk_biz_no
-- · uk_newapi_user —— 预建条目按 new-api 用户去重(脚本补池锚);
-- · uk_claimed_by —— 一玩家至多一条 CLAIMEDFREE 时 claimed_by 为 NULLNULL 不参与唯一约束,多条 FREE 并存);
-- · uk_biz_no —— claim 幂等键 claim_<gamePlayerId>FREE 时为 NULL
-- 并发/重投由 status='FREE' 单条 CAS + 上述两 NULL 唯一键三重收敛成一条:同玩家不占第二条、两玩家不抢同一条。
-- newapi_token_key 是 new-api 调用凭据明文落库(内测可接受、须视为敏感):随 job 内网下发须日志脱敏,生产化应加密列(红线待办,本阶段不做)。
-- 前后兼容:纯新增表,不改任何既有表结构,无迁移风险。
-- ⚠ 再 claimS4 凭据失效路,本单不实现):将 CLAIMED 隔离为 INVALID 时必须同步清空 claimed_by_game_player_id/biz_no
-- 否则与该玩家新 CLAIMED 条目撞 uk_claimed_by/uk_biz_no。本单只落 FREE→CLAIMEDINVALID 转换延后到 S4。
-- -----------------------------------------------------------------------------
CREATE TABLE `newapi_quota_pool` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '池条目编号',
`newapi_user_id` BIGINT NOT NULL COMMENT '预建的 new-api users.id脚本灌数去重键',
`newapi_token_id` BIGINT NOT NULL COMMENT '预建的 new-api tokens.id',
`newapi_token_key` VARCHAR(64) NOT NULL COMMENT 'new-api tokens.key48 位 sk-…;⚠ 敏感,随 job 下发须脱敏,永不整条落日志)',
`grant_quota` BIGINT NOT NULL COMMENT '该条目预置的 ¥100 折算 quota审计快照权威余额以网关 token.remain_quota 为准)',
`quota_per_unit_snapshot` BIGINT NOT NULL COMMENT '折算时网关 quota_per_unit默认 500000审计追溯用',
`usd_rate_snapshot` DECIMAL(10,4) NOT NULL COMMENT '折算时 usd_exchange_rate默认 7.3,审计追溯用)',
`status` VARCHAR(16) NOT NULL DEFAULT 'FREE' COMMENT '状态FREE可claim / CLAIMED已绑玩家 / INVALID token失效隔离S4claim CAS 抢占锚',
`claimed_by_game_player_id` BIGINT NULL COMMENT 'claim 后 ↔ game_player.idFREE 时 NULLuk 允许多 NULLCLAIMED 保证一玩家至多一条)',
`claimed_at` DATETIME NULL COMMENT 'claim 时间CLAIMED 时回填)',
`biz_no` VARCHAR(64) NULL COMMENT 'claim 幂等键 claim_<gamePlayerId>FREE 时 NULL',
`retry_count` INT NOT NULL DEFAULT 0 COMMENT '预留claim/隔离重试计数',
`remark` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '备注(隔离原因等)',
-- 标准审计列DO 继承 TenantBaseDOops 脚本 raw INSERT 不给这些列,走默认落值,运行时 executeIgnore 跨租户)
`creator` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建者(审计列)',
`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 '租户编号(系统级池资源,运行时 executeIgnore 跨租户存取,值仅占位)',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_newapi_user` (`newapi_user_id`) COMMENT '预建条目按 new-api 用户去重(脚本 ON DUPLICATE KEY UPDATE 补池锚)',
UNIQUE KEY `uk_claimed_by` (`claimed_by_game_player_id`) COMMENT '一玩家至多一条 CLAIMEDNULL 不参与约束,多条 FREE 并存)',
UNIQUE KEY `uk_biz_no` (`biz_no`) COMMENT 'claim 幂等键唯一NULL 不参与约束)',
KEY `idx_status` (`status`) COMMENT 'claim 抢占按 status=FREE 扫、水位按 FREE 计数'
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = 'new-api per-user ¥100 额度预置池(离线预建 FREE + 注册 claim 绑玩家WU2 §3.5';

View File

@ -0,0 +1,62 @@
package com.wanxiang.huijing.game.module.aigc.dal.dataobject.quota;
import com.wanxiang.huijing.framework.tenant.core.db.TenantBaseDO;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* new-api per-user 额度池条目 DO对应表 newapi_quota_poolWU2 §3.5
*
* 池表存的是预建条目而非一玩家一映射行离线 ops 脚本
* game-runtime/tools/newapi_pool_provision.py预建 N (new-api user + token + ¥100) 按条目 INSERT
* status=FREEclaimed_* 注册/ claim 时某条被 CAS 抢占翻转 CLAIMED 并绑 game_player
*
* 继承 {@link TenantBaseDO}自带 creator/create_time/updater/update_time/deleted + tenant_id 审计/租户列
* 池是系统级资源运行时 claim/查询经 {@code TenantUtils.executeIgnore} 跨租户存取tenant_id 值仅占位
* ops 脚本 raw INSERT 只给业务列审计/租户列走 DB 默认落值
*
* 账本口径唯一 = new-api 网关权威余额=token remain_quota权威消耗=user used_quota DO
* {@link #grantQuota} 只是审计快照game-cloud 侧不记余额
*
* 幂等三键仿 {@code GrantDO} uk_biz_nouk_newapi_user脚本去重/ uk_claimed_by一玩家至多一条 CLAIMED/
* uk_biz_noclaim 幂等键 claim_&lt;gamePlayerId&gt;并发/重投由 status='FREE' 单条 CAS + NULL 唯一键三重收敛
*
* @author 绘境AI
*/
@TableName("newapi_quota_pool")
@Data
@EqualsAndHashCode(callSuper = true)
public class NewapiQuotaPoolDO extends TenantBaseDO {
/** 池条目主键 */
private Long id;
/** 预建的 new-api users.id脚本灌数去重键 uk_newapi_user */
private Long newapiUserId;
/** 预建的 new-api tokens.id */
private Long newapiTokenId;
/** new-api tokens.key48 位 sk-…;⚠ 敏感,随 job 下发须脱敏,永不整条落日志) */
private String newapiTokenKey;
/** 该条目预置的 ¥100 折算 quota审计快照权威余额以网关 token.remain_quota 为准) */
private Long grantQuota;
/** 折算时网关 quota_per_unit默认 500000审计追溯用 */
private Long quotaPerUnitSnapshot;
/** 折算时 usd_exchange_rate默认 7.3,审计追溯用) */
private BigDecimal usdRateSnapshot;
/** 状态FREE/CLAIMED/INVALID{@link com.wanxiang.huijing.game.module.aigc.enums.NewapiQuotaStatusEnum}claim CAS 抢占锚 */
private String status;
/** claim 后 ↔ game_player.idFREE 时 NULLuk_claimed_by 允许多 NULL */
private Long claimedByGamePlayerId;
/** claim 时间CLAIMED 时回填) */
private LocalDateTime claimedAt;
/** claim 幂等键 claim_&lt;gamePlayerId&gt;FREE 时 NULL */
private String bizNo;
/** 预留claim/隔离重试计数 */
private Integer retryCount;
/** 备注(隔离原因等) */
private String remark;
}

View File

@ -0,0 +1,69 @@
package com.wanxiang.huijing.game.module.aigc.dal.mysql.quota;
import com.wanxiang.huijing.game.module.aigc.dal.dataobject.quota.NewapiQuotaPoolDO;
import com.wanxiang.huijing.game.module.aigc.enums.NewapiQuotaStatusEnum;
import com.wanxiang.huijing.framework.mybatis.core.mapper.BaseMapperX;
import com.wanxiang.huijing.framework.mybatis.core.query.LambdaQueryWrapperX;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
import java.time.LocalDateTime;
/**
* new-api 额度池 MapperWU2 §3.5
*
* <p><b>跨租户存取铁律</b>池是系统级资源注册消费/派发线均无租户上下文调用方{@code NewapiQuotaServiceImpl}
* 必须用 {@code TenantUtils.executeIgnore(...)} 包裹本 Mapper 的每次调用否则多租户插件会按空租户上下文过滤致查不到条目
*
* <p><b>claim 幂等靠 CAS + 唯一键</b>{@link #claimOneFree} 是单条 {@code FREECLAIMED} 行级 CAS并发/重投由
* {@code status='FREE'} 条件 + uk_claimed_by + uk_biz_no 三重收敛任何路径不给同一玩家占第二条不让两玩家抢同一条
*
* @author 绘境AI
*/
@Mapper
public interface NewapiQuotaPoolMapper extends BaseMapperX<NewapiQuotaPoolDO> {
/**
* 按已绑玩家取其 CLAIMED 条目 claim 前查 / 派发取 tokenuk_claimed_by 保证至多一条
*
* @param gamePlayerId game_player.id
* @return CLAIMED 条目未绑返回 null
*/
default NewapiQuotaPoolDO selectClaimedByPlayer(Long gamePlayerId) {
return selectOne(new LambdaQueryWrapperX<NewapiQuotaPoolDO>()
.eq(NewapiQuotaPoolDO::getClaimedByGamePlayerId, gamePlayerId)
.eq(NewapiQuotaPoolDO::getStatus, NewapiQuotaStatusEnum.CLAIMED.getStatus()));
}
/**
* 池水位FREE 条目计数低于阈值触发补池告警§3.4
*
* @return 当前 FREE 条目数
*/
default Long countFree() {
return selectCount(new LambdaQueryWrapperX<NewapiQuotaPoolDO>()
.eq(NewapiQuotaPoolDO::getStatus, NewapiQuotaStatusEnum.FREE.getStatus()));
}
/**
* 原子 CAS抢一个 FREE 条目翻转 CLAIMED 并绑玩家§3.4
*
* <p>双层派生表 {@code (SELECT id FROM (SELECT id ... LIMIT 1) t)} MySQL不能 UPDATE 又在 FROM 子查询引用同表
* 的标准物化绕法{@code AND status='FREE'} 再做一次行级 CAS 兜底并发返回1=抢到0=池空子查询 NULL 无命中
* 显式带 {@code deleted=0}raw @Update 不经多租户/逻辑删自动改写调用方 executeIgnore 已免租户过滤
*
* @param gamePlayerId 绑定的 game_player.id
* @param bizNo 幂等键 claim_&lt;gamePlayerId&gt;
* @param claimedAt claim 时间
* @return 影响行数1=抢到0=池空无 FREE 条目
*/
@Update("UPDATE newapi_quota_pool "
+ "SET status='CLAIMED', claimed_by_game_player_id=#{gamePlayerId}, claimed_at=#{claimedAt}, biz_no=#{bizNo} "
+ "WHERE id = (SELECT id FROM (SELECT id FROM newapi_quota_pool WHERE status='FREE' AND deleted=0 ORDER BY id LIMIT 1) t) "
+ "AND status='FREE' AND deleted=0")
int claimOneFree(@Param("gamePlayerId") Long gamePlayerId,
@Param("bizNo") String bizNo,
@Param("claimedAt") LocalDateTime claimedAt);
}

View File

@ -0,0 +1,29 @@
package com.wanxiang.huijing.game.module.aigc.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* new-api 额度池条目状态枚举WU2 §3.4/§3.5 状态机
*
* FREE 离线 ops 脚本预建的可 claim 条目CLAIMED 已被注册/ claim CAS 抢占绑玩家
* INVALID token 失效隔离S4 claim 本单只落 FREECLAIMED不产出 INVALID
* 落库列 newapi_quota_pool.statusvarchar(16)CAS 抢占以 status='FREE' 为锚
*
* @author 绘境AI
*/
@Getter
@AllArgsConstructor
public enum NewapiQuotaStatusEnum {
/** 可 claim离线预建claimed_* 空) */
FREE("FREE"),
/** 已绑玩家claim CAS 抢占后翻转) */
CLAIMED("CLAIMED"),
/** token 失效隔离S4不再派发本单不产出 */
INVALID("INVALID");
/** 落库值(对齐 DDL status 列取值) */
private final String status;
}

View File

@ -0,0 +1,58 @@
-- =============================================================================
-- 契约 #2 DB 迁移 | 主题内测·new-api per-user ¥100 额度池WU22026-07-07 设计 §3.5| owneraigc派发期映射查询 co-locate
-- 文件V32.0.0__create_newapi_quota_pool.sqlFlyway纯新增表已合入禁止修改回滚 = drop 表 + 摘事件消费者)
-- 依据docs/agent-specs/2026-07-07-内测-WU2-newapi额度接入-设计.md草案创始人拍定「离线预置池 + 注册 claim」
-- 版本定序:执行副本 db/migration/ 实测已连续到 V31.0.0WU1 game_player 增列 + outbox本件顺延取 V32.0.0(已 ls|sort -V|tail 复核)。
-- WU1 与 WU2 同批新增迁移——WU1 取 V31、WU2 顺延 V32避免 Flyway 撞号校验失败。
-- 守门①(沿用 V11/V31本件同时落 contracts/db-schemas/(契约源)+ huijing-server 执行副本(唯一执行 classpath
-- 不放任何单模块 -server/db/migration/(避免同版本出现在多个 classpath jar 触发 Flyway 重复校验失败)。
-- 守门②:含中文 SQLmini-desktop 执行必须 --default-character-set=utf8mb4。
-- 内容CREATE newapi_quota_pool —— 离线 ops 脚本预建 FREE 条目 + 注册 claim 绑玩家FREE→CLAIMED账本在 new-api 网关。
-- =============================================================================
-- -----------------------------------------------------------------------------
-- CREATE newapi_quota_pool —— new-api per-user 额度预置池§3.5
-- 语义ops 脚本game-runtime/tools/newapi_pool_provision.py离线预建 N 个 (new-api user + token + ¥100)
-- 按条目 INSERTstatus=FREE、claimed_* 空);注册/懒 claim 时某条被 CAS 抢占翻转 CLAIMED 并绑 game_player。
-- 账本口径唯一 = new-api 网关(权威余额=token remain_quota、权威消耗=user used_quota本表只存 grant_quota 审计快照,不在 game-cloud 记余额。
-- 列名严格对齐 ops 脚本 emit 的 INSERT(newapi_user_id, newapi_token_id, newapi_token_key, grant_quota,
-- quota_per_unit_snapshot, usd_rate_snapshot, status),去重键 uk_newapi_user(newapi_user_id) 供 ON DUPLICATE KEY UPDATE 幂等补池。
-- 审计列creator/create_time/updater/update_time/deleted与 tenant_id 均给默认值ops 脚本 raw INSERT 只给业务列,
-- 审计列/租户列走 DB 默认落值;运行时 claim/查询经 TenantUtils.executeIgnore 跨租户存取(池是系统级资源)。
-- 幂等三键(仿 game_trade_grant 的 uk_biz_no
-- · uk_newapi_user —— 预建条目按 new-api 用户去重(脚本补池锚);
-- · uk_claimed_by —— 一玩家至多一条 CLAIMEDFREE 时 claimed_by 为 NULLNULL 不参与唯一约束,多条 FREE 并存);
-- · uk_biz_no —— claim 幂等键 claim_<gamePlayerId>FREE 时为 NULL
-- 并发/重投由 status='FREE' 单条 CAS + 上述两 NULL 唯一键三重收敛成一条:同玩家不占第二条、两玩家不抢同一条。
-- newapi_token_key 是 new-api 调用凭据明文落库(内测可接受、须视为敏感):随 job 内网下发须日志脱敏,生产化应加密列(红线待办,本阶段不做)。
-- 前后兼容:纯新增表,不改任何既有表结构,无迁移风险。
-- ⚠ 再 claimS4 凭据失效路,本单不实现):将 CLAIMED 隔离为 INVALID 时必须同步清空 claimed_by_game_player_id/biz_no
-- 否则与该玩家新 CLAIMED 条目撞 uk_claimed_by/uk_biz_no。本单只落 FREE→CLAIMEDINVALID 转换延后到 S4。
-- -----------------------------------------------------------------------------
CREATE TABLE `newapi_quota_pool` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '池条目编号',
`newapi_user_id` BIGINT NOT NULL COMMENT '预建的 new-api users.id脚本灌数去重键',
`newapi_token_id` BIGINT NOT NULL COMMENT '预建的 new-api tokens.id',
`newapi_token_key` VARCHAR(64) NOT NULL COMMENT 'new-api tokens.key48 位 sk-…;⚠ 敏感,随 job 下发须脱敏,永不整条落日志)',
`grant_quota` BIGINT NOT NULL COMMENT '该条目预置的 ¥100 折算 quota审计快照权威余额以网关 token.remain_quota 为准)',
`quota_per_unit_snapshot` BIGINT NOT NULL COMMENT '折算时网关 quota_per_unit默认 500000审计追溯用',
`usd_rate_snapshot` DECIMAL(10,4) NOT NULL COMMENT '折算时 usd_exchange_rate默认 7.3,审计追溯用)',
`status` VARCHAR(16) NOT NULL DEFAULT 'FREE' COMMENT '状态FREE可claim / CLAIMED已绑玩家 / INVALID token失效隔离S4claim CAS 抢占锚',
`claimed_by_game_player_id` BIGINT NULL COMMENT 'claim 后 ↔ game_player.idFREE 时 NULLuk 允许多 NULLCLAIMED 保证一玩家至多一条)',
`claimed_at` DATETIME NULL COMMENT 'claim 时间CLAIMED 时回填)',
`biz_no` VARCHAR(64) NULL COMMENT 'claim 幂等键 claim_<gamePlayerId>FREE 时 NULL',
`retry_count` INT NOT NULL DEFAULT 0 COMMENT '预留claim/隔离重试计数',
`remark` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '备注(隔离原因等)',
-- 标准审计列DO 继承 TenantBaseDOops 脚本 raw INSERT 不给这些列,走默认落值,运行时 executeIgnore 跨租户)
`creator` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建者(审计列)',
`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 '租户编号(系统级池资源,运行时 executeIgnore 跨租户存取,值仅占位)',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_newapi_user` (`newapi_user_id`) COMMENT '预建条目按 new-api 用户去重(脚本 ON DUPLICATE KEY UPDATE 补池锚)',
UNIQUE KEY `uk_claimed_by` (`claimed_by_game_player_id`) COMMENT '一玩家至多一条 CLAIMEDNULL 不参与约束,多条 FREE 并存)',
UNIQUE KEY `uk_biz_no` (`biz_no`) COMMENT 'claim 幂等键唯一NULL 不参与约束)',
KEY `idx_status` (`status`) COMMENT 'claim 抢占按 status=FREE 扫、水位按 FREE 计数'
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = 'new-api per-user ¥100 额度预置池(离线预建 FREE + 注册 claim 绑玩家WU2 §3.5';