08 — 创作者申请(Creator-Role Application)
返回 README
本节是跨服务契约:下游(kungal 论坛 / moyu 补丁站)按此调用 OAuth 的创作者申请队列。 角色由 OAuth 独占授予(身份契约:只有 OAuth 能发角色),资格门槛由下游自治。 设计背景与取舍见 infra 内部设计文档《创作者角色设计》(
docs/auth/01-creator-role-design.md, 仅 infra 仓 / 文档门户可见,本契约文件对下游自包含)。
模型
creator(创作者)是 OAuth 的一个附加角色——可信发布者档位,赋予「galgame 直接发布
(含无 VNDB ID)」等发布信任能力。授予走 申请 → 管理员审核 → 通过 / 拒绝(可重申):
下游(论坛/补丁站): 按【自己的】门槛判定用户是否可申请(查 wiki /user/:id/stats + 自有数据)
→ 合格则带【用户 JWT】调 OAuth POST /creator/applications,附 evidence(满足了哪条)
OAuth: creator_applications 中央队列 → 管理员审核 → approve 授予 creator 角色 / decline 附理由
用户: GET /creator/applications/me 查自己的申请状态
- 资格判定在下游(软门槛,会变、各站不同);授予在 OAuth + 管理员(硬门槛)。下游门槛写松最坏只是塞满审核队列,绝不能绕过人工审核拿到角色。
evidence是下游随申请带的「满足了哪条」提示,供管理员参考,不作授权依据。
用户端点(Bearer / 用户 JWT)
挂 middleware.Auth,需登录用户的 access token。
POST /api/v1/creator/applications — 提交申请
// 请求体
{
"source": "forum", // 必填,oneof: "forum" | "moyu"(申请来源站点)
"message": "我的简介…", // 可选,max 1000,申请人附言(给管理员看)
"evidence": { // 可选,jsonb,下游提供的「满足了哪条门槛」提示
"pr_merged": 7,
"galgame_created": 12
}
}
成功:返回创建的申请对象(见下「申请对象」),status = pending。
失败(HTTP 400 + 业务码,message 为可直接展示给用户的中文):
| code | message | 含义 |
|---|---|---|
| 17001 | 你已经是创作者了 | 用户已持有 creator 角色,无需申请 |
| 17002 | 已有一份待审核的创作者申请 | 已有 pending 申请(同时只允许一个) |
| 17003 | 申请被拒绝后需等待冷却期才能重新申请 | 上一次被拒后未过冷却期(1 天) |
申请前 OAuth 不校验门槛(门槛在下游);这三个守卫只防重复 / 滥用申请。
GET /api/v1/creator/applications/me — 查我的申请
返回该用户最近一条申请对象。
⚠️ 从未申请过的用户:服务返回 nil → 响应省略
data字段(不是data: null, 是整个 key 不存在;envelope 的data带omitempty)。下游必须把「data缺省」当作 **「从未申请」**处理,不要当错误。曾导致论坛首次访问用户 500(forum3f4a61b5修复前)。// 从未申请: { "code": 0, "message": "成功" } // 注意:没有 data // 申请过: { "code": 0, "message": "成功", "data": { /* 申请对象 */ } }
管理员端点(Admin JWT,不在下游接入范围)
挂 admin 组(role=admin)。列在此仅为契约完整:
| 端点 | 作用 | 响应 |
|---|---|---|
GET /api/v1/admin/creator/applications?status=&page=&limit= |
审核队列(status 缺省 pending) |
{ items: [申请对象 + user], total }(每条多带申请人简介 user,见下「管理员列表额外字段」) |
POST /api/v1/admin/creator/applications/:id/approve |
通过 → 授予 creator 角色 + 标记 approved |
{ id, status: "approved" } |
POST /api/v1/admin/creator/applications/:id/decline |
拒绝(body { reason?: max 500 }) |
{ id, status: "declined" } |
审核为状态机原子转移;对已处理的申请重复审核 → 17005「该申请已被处理」;id 不存在 → 17004「申请不存在」。
申请对象
{
"id": 12,
"user_id": 1007,
"source": "forum", // "forum" | "moyu" | …
"status": "pending", // 状态枚举,见下
"evidence": { "pr_merged": 7 }, // omitempty:无则缺省
"message": "我的简介…", // 申请人附言(无则 "")
"reviewer_id": 2, // omitempty:审核后才有
"reviewed_at": "2026-06-18T...", // omitempty:审核后才有
"decline_reason": "", // 拒绝时填,可直接展示给申请人
"created_at": "2026-06-18T...",
"updated_at": "2026-06-18T..."
}
状态枚举 status:pending(待审核)| approved(已通过)| declined(已拒绝)。
管理员列表额外字段(仅 GET /admin/creator/applications):每个 item 在上述申请对象字段之外再带一个 user —— 申请人的 UserBrief(id / name / avatar / avatar_image_hash / status / roles / …),由 OAuth 服务端注入,管理端 UI 直接展示,无需再调 S2S 专用的 GET /users/batch(那是 client Basic 鉴权的服务间端点,浏览器调会 401)。用户行已不存在时 user 为 null。GET /creator/applications/me(用户查自己)不带 user。
下游耦合点(重命名 / 重构时务必同步本节)
下游已硬编码以下事实,改动需在同一 PR 更新本契约并跑 docs:sync:
- 角色字符串恰为
creator——下游徽章 / 直发判定按slices.Contains(roles, "creator"),重命名会静默失效。 - 状态枚举值
pending/approved/declined(字符串,非数字)。 GET /creator/applications/me在「从未申请」时省略data字段(非data: null)——下游须按「缺省 = 从未申请」处理。- 申请
source为oneof=forum moyu(新增下游站点需在此扩展)。 - 业务错误码 17001 / 17002 / 17003 的
message是可直接展示的中文——下游 verbatim 透传给用户。
已知限制(可接受)
- 生效有延迟:approve / decline 不向下游推事件。下游在下一次
/users/batch缓存刷新(约 10 分钟 TTL)或用户轮询/creator/applications/me时才反映新授予的角色。当前可接受;若将来需即时反馈,可加 webhook / role 版本号让下游在 approve 时失效缓存。 - 角色生效本身经 token 刷新后才进 JWT 的
rolesclaim,符合既有 OAuth 模型。
变更摘要
2026-06-18 新增:创作者申请契约纳入 OAuth 文档族并同步下游镜像。端点
POST /creator/applications+GET /creator/applications/me(用户)、/admin/creator/applications*(管理员);错误码 17001-17005。
源:kun-galgame-infra/docs/integration/oauth/08-creator-applications.md