openapi: 3.0.3 # 契约 #8 API | 模块:passport(真实鉴权 / 匿名玩家 / 邀请码受限激活)| owner:passport(system 内扩展,方案乙),全员 review # 端:/app-api(产品端 game-studio:手机号验证码登录 / 邀请码注册 / 当前用户) # /admin-api(管理端 game-admin:邀请码生成/分页/停用 + 玩家创作者白名单置位,RBAC) # 前缀由 yudao 框架按 controller.app/admin 包名自动添加;本文件 path 直接写含前缀的完整对外路径。 # 错误码段(落 system 段 1-002-***-***,模块归属决定,不占 game 业务 1-1xx 段;取远段避上游增长区,现最大 1-002-028): # 1-002-090-*** = 玩家身份(手机号已注册/未注册/被禁用/非白名单创作被拒…) # 1-002-091-*** = 邀请码(无效/已停用/已过期/已耗尽/旁路已退役…) # 响应统一 Yudao CommonResult 信封:{ code, data, msg };code=0 成功。 # 拍板依据:2026-06-10 鉴权七项(评审版 §9.1)——拍板1=C 验证码为主+邀请码旁路 / 拍板3 开放注册(受限期凭码)/ # 拍板4 创作限 A2 白名单 / 拍板5 互动即弹一键登录 / 拍板7=B 纯客户端 anonId。 # 鉴权口径:send-sms-code / sms-login / invite-register 三个 @PermitAll(免登);me 登录态(不加 @PermitAll)。 # 发 token=OAuth2TokenService.createAccessToken(userId, UserTypeEnum.MEMBER(1), clientId, scopes);refresh 7d/30d 由 client 配置承载。 # 安全基线:日志与响应手机号一律脱敏(如 138****0000);token 不落日志(验收5)。 # 防枚举口径:send-sms-code 响应不区分手机号是否已注册(见端点 description)。 # 边界 OUT:不做三方社交登录 / 不开多租户(tenant.enable=false,新表 tenant_id=1)/ 不做实名收集流程(仅表字段预留)/ # 不做匿名影子账号合并(拍板5=A)/ 不做服务端 anonId 校验签名(拍板7=B 既定代价)/ game-admin 前端零改动(邀请码经 Knife4j/脚本调 admin-api,RBAC 管控,§5.3)。 info: title: 绘境AI passport 模块 API version: 1.0.0 description: >- 真实鉴权与匿名玩家身份:种子创作者手机号验证码登录(发真 userType=MEMBER OAuth2 token)+ 陌生玩家邀请码受限激活注册(零短信依赖,报备后退役)+ 当前用户信息(前端守卫/白名单 UI 依据)+ 管理端邀请码全生命周期运营(生成/分页/停用)与创作者白名单置位(A2 名单)。 匿名玩家免登刷 feed/试玩/遥测(读端点 @PermitAll,见 feed/runtime/telemetry 契约),互动才弹一键登录。 覆盖产品功能 P-ACC(账号鉴权)受限激活与 A2 白名单口径。 前端据此 vite-plugin-mock 自动生成 mock。 servers: - url: http://localhost:48080 description: 本地(Swagger/Knife4j http://localhost:48080/doc.html) paths: # =========================================================================== # app 端(产品端 game-studio):验证码登录 / 邀请码注册 / 当前用户 # =========================================================================== /app-api/passport/send-sms-code: post: tags: [app-passport] summary: 发送登录短信验证码(@PermitAll;拍板1C 主路径) description: >- 场景固定 SmsSceneEnum.MEMBER_LOGIN(1);种子期走 Debug 渠道(码落 system_sms_code 表,运营后台查码)。 鉴权:@PermitAll 免登(R6 安全前置:发码端点加 @RateLimiter IP 维度限频 + IP 每日/每小时上限,切真实渠道前补图形验证码,见 execution §7)。 防枚举口径:响应不区分手机号是否已注册(已注册→登录路径用;未注册→自动注册路径用,均发码),统一返回成功,避免手机号是否注册被探测。 requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/SendSmsCodeReqVO' } responses: '200': description: 成功(发码受理;不区分手机号是否已注册) content: application/json: schema: { $ref: '#/components/schemas/CommonResultBoolean' } /app-api/passport/sms-login: post: tags: [app-passport] summary: 手机号验证码登录(已注册登录 / 未注册自动注册一体,@PermitAll;拍板3 开放注册) description: >- 鉴权:@PermitAll 免登。已注册→登录;未注册→自动注册(register_channel=sms)+ 登录一体(验证码即占有证明)。 anonId 可选:仅注册时落 game_player.first_anon_id 一次(匿名↔登录归因衔接,目标3;缺失落空串,归因降级不阻断登录)。 竞态兜底:uk_mobile 唯一键冲突 → 静默转登录语义(与防枚举口径协调)。 前端登录成功后另补发 user_login 遥测事件(envelope.user 双带 userId+anonId,见 events.schema.json)。 响应安全基线:手机号脱敏、token 不落日志。 requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/SmsLoginReqVO' } responses: '200': description: 成功(返回真 OAuth2 token 与玩家身份摘要) content: application/json: schema: { $ref: '#/components/schemas/CommonResultLogin' } /app-api/passport/invite-register: post: tags: [app-passport] summary: 邀请码受限激活注册(手机号+邀请码,零短信依赖,@PermitAll;拍板1C/2/3) description: >- 鉴权:@PermitAll 免登,受退役开关 wanxiang.passport.invite-register-enabled 管控(§5.4,关闭后返回 1-002-091 旁路已退役错误码)。 报备前陌生玩家旁路:mobile+inviteCode 注册(register_channel=invite,关联核销的 invite_code_id),零短信依赖(拍板1C)。 核销语义:核销与建号同一本地事务,条件更新原子累加(耗尽/停用/过期由前置查询给精确 1-002-091 错误码,条件更新做最终一致兜底),建号失败整体回滚。 手机号已注册 → 提示直接验证码登录(错误码 1-002-090);anonId 可选,落 first_anon_id(同 sms-login 口径)。 requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/InviteRegisterReqVO' } responses: '200': description: 成功(返回真 OAuth2 token 与玩家身份摘要,同 sms-login 响应) content: application/json: schema: { $ref: '#/components/schemas/CommonResultLogin' } /app-api/passport/me: get: tags: [app-passport] summary: 当前登录玩家信息(登录态,不加 @PermitAll;前端守卫/白名单 UI 依据) description: >- 鉴权:登录态必需(无 @PermitAll,未登录返回 401)。 返回当前 token 对应玩家身份摘要,手机号脱敏(138****0000)。前端据 creatorFlag 渲染创作入口/守卫。 responses: '200': description: 成功 content: application/json: schema: { $ref: '#/components/schemas/CommonResultPlayerMe' } # =========================================================================== # admin 端(管理端 game-admin):邀请码生成/分页/停用 + 玩家创作者白名单置位 # RBAC 权限位:邀请码 system:passport-invite:*;玩家 system:passport-player:*(信任边界在后端,不依赖前端,§5.3) # =========================================================================== /admin-api/passport/invite-code/generate: post: tags: [admin-passport] summary: 批量生成邀请码(RBAC system:passport-invite:create;拍板2 受限激活) description: >- 随机生成 count 个邀请码(大写字母数字、避易混淆字符,uk_code 冲突重试),统一批次号/限量/有效期。 权限位 system:passport-invite:create。运营经 Knife4j(/doc.html) 或脚本调用(game-admin 零页面,§5.3)。 requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/InviteCodeGenerateReqVO' } responses: '200': description: 成功(返回生成的码值列表) content: application/json: schema: { $ref: '#/components/schemas/CommonResultInviteCodeList' } /admin-api/passport/invite-code/page: get: tags: [admin-passport] summary: 邀请码分页查询(RBAC system:passport-invite:query) description: 按 batchNo/status 过滤分页查邀请码,含 used_count 核销进度。权限位 system:passport-invite:query。 parameters: - { name: batchNo, in: query, required: false, schema: { type: string }, description: 批次号过滤 } - { name: status, in: query, required: false, schema: { type: integer, enum: [0, 1] }, description: '状态过滤:0启用 1停用' } - { name: pageNo, in: query, required: false, schema: { type: integer, default: 1 } } - { name: pageSize, in: query, required: false, schema: { type: integer, default: 10 } } responses: '200': description: 成功 content: application/json: schema: { $ref: '#/components/schemas/CommonResultInviteCodePage' } /admin-api/passport/invite-code/disable: put: tags: [admin-passport] summary: 单码停用/退役(RBAC system:passport-invite:update) description: >- 将指定邀请码 status 置 1(单码退役;全量旁路退役走配置开关 §5.4)。权限位 system:passport-invite:update。 requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/InviteCodeDisableReqVO' } responses: '200': description: 成功 content: application/json: schema: { $ref: '#/components/schemas/CommonResultBoolean' } /admin-api/passport/player/set-creator: put: tags: [admin-passport] summary: 玩家创作者白名单置位(RBAC system:passport-player:update;拍板4 A2 名单入口) description: >- 置位/取消玩家的 creator_flag(A2 种子白名单的运营动作落点,拍板4)。 置位后该玩家方可走创作/发布写入口(后端 PlayerApi#validateCreator 校验,信任边界在后端)。 权限位 system:passport-player:update。 requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/PlayerSetCreatorReqVO' } responses: '200': description: 成功 content: application/json: schema: { $ref: '#/components/schemas/CommonResultBoolean' } components: schemas: # ---- Yudao CommonResult 信封 ---- CommonResultBoolean: type: object properties: code: { type: integer, description: '0=成功,非0=错误码 1-002-090/091-***' } data: { type: boolean } msg: { type: string } CommonResultLogin: type: object properties: code: { type: integer } data: { $ref: '#/components/schemas/LoginRespVO' } msg: { type: string } CommonResultPlayerMe: type: object properties: code: { type: integer } data: { $ref: '#/components/schemas/PlayerMeRespVO' } msg: { type: string } CommonResultInviteCodeList: type: object properties: code: { type: integer } data: { type: array, items: { $ref: '#/components/schemas/InviteCodeRespVO' } } msg: { type: string } CommonResultInviteCodePage: type: object properties: code: { type: integer } data: type: object properties: list: { type: array, items: { $ref: '#/components/schemas/InviteCodeRespVO' } } total: { type: integer, format: int64 } msg: { type: string } # ---- app 端请求 ---- SendSmsCodeReqVO: type: object required: [mobile] properties: mobile: { type: string, maxLength: 11, description: '手机号(场景固定 MEMBER_LOGIN;响应不区分是否已注册,防枚举)' } SmsLoginReqVO: type: object required: [mobile, code] properties: mobile: { type: string, maxLength: 11, description: 手机号(登录主键) } code: { type: string, description: '短信验证码(Debug 渠道运营查 system_sms_code 表)' } anonId: { type: string, maxLength: 64, description: '客户端 anonId(可选;仅注册时落 first_anon_id 一次,匿名↔登录归因,拍板7=B)' } InviteRegisterReqVO: type: object required: [mobile, inviteCode] properties: mobile: { type: string, maxLength: 11, description: 手机号(登录主键) } inviteCode: { type: string, maxLength: 16, description: '邀请码(核销语义见 execution §4.2;无效/停用/过期/耗尽返回 1-002-091)' } anonId: { type: string, maxLength: 64, description: '客户端 anonId(可选;同 sms-login 口径)' } nickname: { type: string, maxLength: 30, description: '昵称(可选;不传则默认生成)' } # ---- app 端响应 ---- LoginRespVO: type: object description: 登录/注册成功响应(sms-login 与 invite-register 共用);token 不落日志,手机号不在此返回 properties: userId: { type: integer, format: int64, description: '玩家编号(即 OAuth2 token 的 userId,userType=MEMBER)' } accessToken: { type: string, description: '访问令牌(Bearer);不得落日志' } refreshToken: { type: string, description: '刷新令牌(refresh 7d/30d 由 OAuth2 client 配置承载,§3.4)' } expiresTime: { type: integer, format: int64, description: '访问令牌过期时间戳(毫秒)' } nickname: { type: string, description: 昵称 } creatorFlag: { type: integer, description: '创作者白名单位:0玩家 1创作者(拍板4;前端据此渲染创作入口)' } PlayerMeRespVO: type: object description: 当前登录玩家信息(前端守卫/白名单 UI 依据);手机号脱敏返回 properties: userId: { type: integer, format: int64, description: 玩家编号 } mobile: { type: string, description: '脱敏手机号(如 138****0000;安全基线,禁回明文)' } nickname: { type: string, description: 昵称 } avatar: { type: string, description: 头像 URL } creatorFlag: { type: integer, description: '创作者白名单位:0玩家 1创作者' } # ---- admin 端请求 ---- InviteCodeGenerateReqVO: type: object required: [count, maxUses] properties: count: { type: integer, minimum: 1, maximum: 100, description: '本批生成数量(≤100)' } maxUses: { type: integer, minimum: 1, default: 1, description: '单码最大核销次数(限量)' } expireTime: { type: string, format: date-time, description: '有效期(可选;不传=不过期)' } batchNo: { type: string, maxLength: 32, description: '批次号(可选;运营按渠道发放归因,如 batch-xhs-01)' } remark: { type: string, maxLength: 255, description: '备注(可选;发放对象/用途)' } InviteCodeDisableReqVO: type: object required: [id] properties: id: { type: integer, format: int64, description: '邀请码编号(停用即 status=1,单码退役)' } PlayerSetCreatorReqVO: type: object required: [userId, creatorFlag] properties: userId: { type: integer, format: int64, description: '玩家编号(game_player.id)' } creatorFlag: { type: integer, enum: [0, 1], description: '创作者白名单位:0取消 1置位(A2 名单,拍板4)' } # ---- admin 端响应 ---- InviteCodeRespVO: type: object description: 邀请码(生成返回 / 分页项;含核销进度) properties: id: { type: integer, format: int64, description: 编号 } code: { type: string, description: 邀请码/口令 } batchNo: { type: string, description: 批次号 } maxUses: { type: integer, description: 最大核销次数 } usedCount: { type: integer, description: '已核销次数(核销进度)' } expireTime: { type: string, format: date-time, description: '有效期(NULL=不过期)' } status: { type: integer, description: '状态:0启用 1停用' } remark: { type: string, description: 备注 } createTime: { type: string, format: date-time, description: 创建时间 }