GitHub

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 的 dataomitempty)。下游必须把「data 缺省」当作 **「从未申请」**处理,不要当错误。曾导致论坛首次访问用户 500(forum 3f4a61b5 修复前)。

// 从未申请:
{ "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..."
}

状态枚举 statuspending(待审核)| approved(已通过)| declined(已拒绝)。

管理员列表额外字段(仅 GET /admin/creator/applications:每个 item 在上述申请对象字段之外再带一个 user —— 申请人的 UserBriefid / name / avatar / avatar_image_hash / status / roles / …),由 OAuth 服务端注入,管理端 UI 直接展示,无需再调 S2S 专用的 GET /users/batch(那是 client Basic 鉴权的服务间端点,浏览器调会 401)。用户行已不存在时 usernullGET /creator/applications/me(用户查自己)不带 user

下游耦合点(重命名 / 重构时务必同步本节)

下游已硬编码以下事实,改动需在同一 PR 更新本契约并跑 docs:sync

  1. 角色字符串恰为 creator——下游徽章 / 直发判定按 slices.Contains(roles, "creator"),重命名会静默失效。
  2. 状态枚举值 pending / approved / declined(字符串,非数字)。
  3. GET /creator/applications/me 在「从未申请」时省略 data 字段(非 data: null)——下游须按「缺省 = 从未申请」处理。
  4. 申请 sourceoneof=forum moyu(新增下游站点需在此扩展)。
  5. 业务错误码 17001 / 17002 / 17003 的 message 是可直接展示的中文——下游 verbatim 透传给用户。

已知限制(可接受)

  • 生效有延迟:approve / decline 不向下游推事件。下游在下一次 /users/batch 缓存刷新(约 10 分钟 TTL)或用户轮询 /creator/applications/me 时才反映新授予的角色。当前可接受;若将来需即时反馈,可加 webhook / role 版本号让下游在 approve 时失效缓存。
  • 角色生效本身经 token 刷新后才进 JWT 的 roles claim,符合既有 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