From c13595fbc29c79ae04dcfae4112c27c09999bfd7 Mon Sep 17 00:00:00 2001 From: lili Date: Tue, 7 Jul 2026 12:21:33 -0700 Subject: [PATCH] =?UTF-8?q?feat(aigc):=20WU2=E2=91=A0=20new-api=20?= =?UTF-8?q?=E9=A2=9D=E5=BA=A6=E6=B1=A0=E8=A1=A8=20V32=20+=20DO/Mapper?= =?UTF-8?q?=E2=80=94=E2=80=94=E7=A6=BB=E7=BA=BF=E9=A2=84=E5=BB=BA=20FREE?= =?UTF-8?q?=20=E6=9D=A1=E7=9B=AE=20claim=20=E6=8A=A2=E5=8D=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 内测 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) --- .../V32.0.0__create_newapi_quota_pool.sql | 58 ++++++++++++++++ .../dataobject/quota/NewapiQuotaPoolDO.java | 62 +++++++++++++++++ .../mysql/quota/NewapiQuotaPoolMapper.java | 69 +++++++++++++++++++ .../aigc/enums/NewapiQuotaStatusEnum.java | 29 ++++++++ .../V32.0.0__create_newapi_quota_pool.sql | 58 ++++++++++++++++ 5 files changed, 276 insertions(+) create mode 100644 contracts/db-schemas/V32.0.0__create_newapi_quota_pool.sql create mode 100644 game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/dal/dataobject/quota/NewapiQuotaPoolDO.java create mode 100644 game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/dal/mysql/quota/NewapiQuotaPoolMapper.java create mode 100644 game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/enums/NewapiQuotaStatusEnum.java create mode 100644 game-cloud/huijing-server/src/main/resources/db/migration/V32.0.0__create_newapi_quota_pool.sql diff --git a/contracts/db-schemas/V32.0.0__create_newapi_quota_pool.sql b/contracts/db-schemas/V32.0.0__create_newapi_quota_pool.sql new file mode 100644 index 00000000..b63a0ce1 --- /dev/null +++ b/contracts/db-schemas/V32.0.0__create_newapi_quota_pool.sql @@ -0,0 +1,58 @@ +-- ============================================================================= +-- 契约 #2 DB 迁移 | 主题:内测·new-api per-user ¥100 额度池(WU2,2026-07-07 设计 §3.5)| owner:aigc(派发期映射查询 co-locate) +-- 文件:V32.0.0__create_newapi_quota_pool.sql(Flyway,纯新增表;已合入禁止修改,回滚 = drop 表 + 摘事件消费者) +-- 依据:docs/agent-specs/2026-07-07-内测-WU2-newapi额度接入-设计.md(草案,创始人拍定「离线预置池 + 注册 claim」)。 +-- 版本定序:执行副本 db/migration/ 实测已连续到 V31.0.0(WU1 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 重复校验失败)。 +-- 守门②:含中文 SQL,mini-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), +-- 按条目 INSERT(status=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 —— 一玩家至多一条 CLAIMED(FREE 时 claimed_by 为 NULL,NULL 不参与唯一约束,多条 FREE 并存); +-- · uk_biz_no —— claim 幂等键 claim_(FREE 时为 NULL)。 +-- 并发/重投由 status='FREE' 单条 CAS + 上述两 NULL 唯一键三重收敛成一条:同玩家不占第二条、两玩家不抢同一条。 +-- newapi_token_key 是 new-api 调用凭据明文落库(内测可接受、须视为敏感):随 job 内网下发须日志脱敏,生产化应加密列(红线待办,本阶段不做)。 +-- 前后兼容:纯新增表,不改任何既有表结构,无迁移风险。 +-- ⚠ 再 claim(S4 凭据失效路,本单不实现):将 CLAIMED 隔离为 INVALID 时必须同步清空 claimed_by_game_player_id/biz_no, +-- 否则与该玩家新 CLAIMED 条目撞 uk_claimed_by/uk_biz_no。本单只落 FREE→CLAIMED,INVALID 转换延后到 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.key(48 位 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失效隔离(S4);claim CAS 抢占锚', + `claimed_by_game_player_id` BIGINT NULL COMMENT 'claim 后 ↔ game_player.id;FREE 时 NULL(uk 允许多 NULL,CLAIMED 保证一玩家至多一条)', + `claimed_at` DATETIME NULL COMMENT 'claim 时间(CLAIMED 时回填)', + `biz_no` VARCHAR(64) NULL COMMENT 'claim 幂等键 claim_;FREE 时 NULL', + `retry_count` INT NOT NULL DEFAULT 0 COMMENT '预留:claim/隔离重试计数', + `remark` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '备注(隔离原因等)', + -- 标准审计列(DO 继承 TenantBaseDO;ops 脚本 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 '一玩家至多一条 CLAIMED(NULL 不参与约束,多条 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)'; diff --git a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/dal/dataobject/quota/NewapiQuotaPoolDO.java b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/dal/dataobject/quota/NewapiQuotaPoolDO.java new file mode 100644 index 00000000..ea5caa9e --- /dev/null +++ b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/dal/dataobject/quota/NewapiQuotaPoolDO.java @@ -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_pool,WU2 §3.5) + * + * 池表存的是「预建条目」而非「一玩家一映射行」——离线 ops 脚本 + * (game-runtime/tools/newapi_pool_provision.py)预建 N 个 (new-api user + token + ¥100) 按条目 INSERT + * (status=FREE、claimed_* 空);注册/懒 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_no):uk_newapi_user(脚本去重)/ uk_claimed_by(一玩家至多一条 CLAIMED)/ + * uk_biz_no(claim 幂等键 claim_<gamePlayerId>)——并发/重投由 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.key(48 位 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.id;FREE 时 NULL(uk_claimed_by 允许多 NULL) */ + private Long claimedByGamePlayerId; + /** claim 时间(CLAIMED 时回填) */ + private LocalDateTime claimedAt; + /** claim 幂等键 claim_<gamePlayerId>;FREE 时 NULL */ + private String bizNo; + /** 预留:claim/隔离重试计数 */ + private Integer retryCount; + /** 备注(隔离原因等) */ + private String remark; + +} diff --git a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/dal/mysql/quota/NewapiQuotaPoolMapper.java b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/dal/mysql/quota/NewapiQuotaPoolMapper.java new file mode 100644 index 00000000..424aebb0 --- /dev/null +++ b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/dal/mysql/quota/NewapiQuotaPoolMapper.java @@ -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 额度池 Mapper(WU2 §3.5) + * + *

跨租户存取铁律:池是系统级资源、注册消费/派发线均无租户上下文——调用方({@code NewapiQuotaServiceImpl}) + * 必须用 {@code TenantUtils.executeIgnore(...)} 包裹本 Mapper 的每次调用,否则多租户插件会按空租户上下文过滤致查不到条目。 + * + *

claim 幂等靠 CAS + 唯一键:{@link #claimOneFree} 是单条 {@code FREE→CLAIMED} 行级 CAS;并发/重投由 + * {@code status='FREE'} 条件 + uk_claimed_by + uk_biz_no 三重收敛,任何路径不给同一玩家占第二条、不让两玩家抢同一条。 + * + * @author 绘境AI + */ +@Mapper +public interface NewapiQuotaPoolMapper extends BaseMapperX { + + /** + * 按已绑玩家取其 CLAIMED 条目(懒 claim 前查 / 派发取 token;uk_claimed_by 保证至多一条)。 + * + * @param gamePlayerId game_player.id + * @return CLAIMED 条目;未绑返回 null + */ + default NewapiQuotaPoolDO selectClaimedByPlayer(Long gamePlayerId) { + return selectOne(new LambdaQueryWrapperX() + .eq(NewapiQuotaPoolDO::getClaimedByGamePlayerId, gamePlayerId) + .eq(NewapiQuotaPoolDO::getStatus, NewapiQuotaStatusEnum.CLAIMED.getStatus())); + } + + /** + * 池水位:FREE 条目计数(低于阈值触发补池告警,§3.4)。 + * + * @return 当前 FREE 条目数 + */ + default Long countFree() { + return selectCount(new LambdaQueryWrapperX() + .eq(NewapiQuotaPoolDO::getStatus, NewapiQuotaStatusEnum.FREE.getStatus())); + } + + /** + * 原子 CAS:抢一个 FREE 条目翻转 CLAIMED 并绑玩家(§3.4)。 + * + *

双层派生表 {@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_<gamePlayerId> + * @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); + +} diff --git a/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/enums/NewapiQuotaStatusEnum.java b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/enums/NewapiQuotaStatusEnum.java new file mode 100644 index 00000000..798154e9 --- /dev/null +++ b/game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/com/wanxiang/huijing/game/module/aigc/enums/NewapiQuotaStatusEnum.java @@ -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 路,本单只落 FREE→CLAIMED,不产出 INVALID)。 + * 落库列 newapi_quota_pool.status(varchar(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; + +} diff --git a/game-cloud/huijing-server/src/main/resources/db/migration/V32.0.0__create_newapi_quota_pool.sql b/game-cloud/huijing-server/src/main/resources/db/migration/V32.0.0__create_newapi_quota_pool.sql new file mode 100644 index 00000000..b63a0ce1 --- /dev/null +++ b/game-cloud/huijing-server/src/main/resources/db/migration/V32.0.0__create_newapi_quota_pool.sql @@ -0,0 +1,58 @@ +-- ============================================================================= +-- 契约 #2 DB 迁移 | 主题:内测·new-api per-user ¥100 额度池(WU2,2026-07-07 设计 §3.5)| owner:aigc(派发期映射查询 co-locate) +-- 文件:V32.0.0__create_newapi_quota_pool.sql(Flyway,纯新增表;已合入禁止修改,回滚 = drop 表 + 摘事件消费者) +-- 依据:docs/agent-specs/2026-07-07-内测-WU2-newapi额度接入-设计.md(草案,创始人拍定「离线预置池 + 注册 claim」)。 +-- 版本定序:执行副本 db/migration/ 实测已连续到 V31.0.0(WU1 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 重复校验失败)。 +-- 守门②:含中文 SQL,mini-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), +-- 按条目 INSERT(status=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 —— 一玩家至多一条 CLAIMED(FREE 时 claimed_by 为 NULL,NULL 不参与唯一约束,多条 FREE 并存); +-- · uk_biz_no —— claim 幂等键 claim_(FREE 时为 NULL)。 +-- 并发/重投由 status='FREE' 单条 CAS + 上述两 NULL 唯一键三重收敛成一条:同玩家不占第二条、两玩家不抢同一条。 +-- newapi_token_key 是 new-api 调用凭据明文落库(内测可接受、须视为敏感):随 job 内网下发须日志脱敏,生产化应加密列(红线待办,本阶段不做)。 +-- 前后兼容:纯新增表,不改任何既有表结构,无迁移风险。 +-- ⚠ 再 claim(S4 凭据失效路,本单不实现):将 CLAIMED 隔离为 INVALID 时必须同步清空 claimed_by_game_player_id/biz_no, +-- 否则与该玩家新 CLAIMED 条目撞 uk_claimed_by/uk_biz_no。本单只落 FREE→CLAIMED,INVALID 转换延后到 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.key(48 位 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失效隔离(S4);claim CAS 抢占锚', + `claimed_by_game_player_id` BIGINT NULL COMMENT 'claim 后 ↔ game_player.id;FREE 时 NULL(uk 允许多 NULL,CLAIMED 保证一玩家至多一条)', + `claimed_at` DATETIME NULL COMMENT 'claim 时间(CLAIMED 时回填)', + `biz_no` VARCHAR(64) NULL COMMENT 'claim 幂等键 claim_;FREE 时 NULL', + `retry_count` INT NOT NULL DEFAULT 0 COMMENT '预留:claim/隔离重试计数', + `remark` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '备注(隔离原因等)', + -- 标准审计列(DO 继承 TenantBaseDO;ops 脚本 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 '一玩家至多一条 CLAIMED(NULL 不参与约束,多条 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)';