Files
patbond-doc/docs/api/openapi.yaml
T
lixi 2ceab6b296
CI / docs-build (push) Successful in 1m22s
docs(api): 契约补录 POST /api/v1/events(关闭 D-1)
以 AnalyticsController 实测行为为准补录埋点上报端点:批量 1-50、
202 逐条结果(accepted/duplicate/rejected + 4 种拒绝原因)、eventId
幂等、唯一允许匿名的写端点(带 Bearer 则完整校验 401/40101)、
400/40000 整批拒绝。info.version 1.0.0 -> 1.1.0(纯增量);
index.md 端点清单同步为 6 端点。python yaml 解析 +
mkdocs build --strict 均通过。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-07 14:09:45 +08:00

611 lines
21 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
openapi: 3.0.3
info:
title: Patbond API — Auth / Me / Events(公开契约)
version: 1.1.0
description: |
Patbond 第一迭代「真实登录纵切」公开契约(冻结稿的正式化,字段与草案零偏差),
1.1.0 追加埋点上报端点 `POST /api/v1/events`M2 第一波契约补录,以实现实测行为为准)。
## 通用约定(development-plan 第 6 节)
- 公开接口统一前缀 `/api/v1`JSON 字段一律 `camelCase`;资源 ID 为 UUID 字符串。
- 所有时间字段为 ISO 8601 且带时区偏移(如 `2026-09-04T04:05:06.789Z`)。
- 统一响应信封 `{"code": 0, "message": "success", "data": …}`;错误同时携带正确的
HTTP 状态码与稳定业务码,业务码永不复用或改号。
- `/internal/**` 为服务间接口,不属于本公开契约,需 `X-Internal-Token` 服务凭证,
未携带或错误一律 401。
## 错误码表
| 业务码 | HTTP | 场景 |
| --- | --- | --- |
| 0 | 200 | 成功 |
| 40000 | 400 | 参数校验失败(含 JSON 不可解析;message 为首个字段错误) |
| 40100 | 401 | 用户名或密码错误 |
| 40101 | 401 | access token 无效或过期(缺失、伪造、篡改、过期) |
| 40102 | 401 | refresh token 已失效或被重用(未知、过期、已轮换、已退出、家族已撤销) |
| 40400 | 404 | 资源不存在 |
| 40900 | 409 | 用户名已存在(大小写不敏感) |
| 40901 | 409 | 手机号已被使用 |
| 42300 | 423 | 登录失败次数过多,账号已临时锁定(见下) |
| 50000 | 500 | 服务器内部错误 |
| 50300 | 503 | 依赖服务暂不可用 |
## 会话模型(ADR-003,数值均为服务端配置项)
- access tokenJWTRS256),有效期 15 分钟;由资源服务用公钥本地验签。
- refresh token:不透明随机串,有效期 30 天;**每次刷新即轮换**,旧值立即失效。
- 已轮换/已失效的 refresh token 再次被使用时,判定为重用,**整个 token family
(该登录会话链)全部撤销**,持有者需重新登录。
- 允许多设备并行会话;退出仅撤销当前会话(由所提交的 refreshToken 标识),
其他设备不受影响。已签发的 access token 在剩余有效期内仍可用。
- 登录失败限制:同一账号在 15 分钟窗口内密码错误累计 5 次(配置项),账号锁定
15 分钟;锁定期间即使密码正确也返回 423/42300;一次成功登录重置计数窗口。
servers:
- url: http://127.0.0.1:8081
description: patbond-auth(本地开发,/api/v1/auth/**
- url: http://127.0.0.1:8082
description: patbond-user(本地开发,/api/v1/me、/api/v1/events
tags:
- name: auth
description: 注册 / 登录 / 刷新 / 退出(patbond-auth
- name: user
description: 当前用户(patbond-user
- name: analytics
description: 产品事件批量上报(patbond-user
paths:
/api/v1/auth/register:
post:
tags: [auth]
summary: 注册并创建会话
operationId: register
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RegisterRequest'
responses:
'200':
description: 注册成功,返回令牌对
content:
application/json:
schema:
$ref: '#/components/schemas/AuthTokenEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'409':
description: 用户名或手机号已被占用(code 40900 / 40901
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
usernameTaken:
value: { code: 40900, message: 用户名已存在, data: null }
phoneTaken:
value: { code: 40901, message: 手机号已被使用, data: null }
/api/v1/auth/login:
post:
tags: [auth]
summary: 登录并创建会话
description: 多设备并行:每次登录开启独立会话(独立 token family),互不影响。
operationId: login
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/LoginRequest'
responses:
'200':
description: 登录成功,返回令牌对
content:
application/json:
schema:
$ref: '#/components/schemas/AuthTokenEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
description: 用户名或密码错误(code 40100)
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
invalidCredentials:
value: { code: 40100, message: 用户名或密码错误, data: null }
'423':
description: 登录失败次数过多,账号临时锁定(code 42300)
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
locked:
value: { code: 42300, message: 登录失败次数过多,账号已临时锁定, data: null }
/api/v1/auth/refresh:
post:
tags: [auth]
summary: 轮换 refresh token
description: |
成功时返回全新令牌对,旧 refreshToken 立即失效(轮换)。提交已轮换或已失效的
refreshToken 返回 401/40102,且视为重用攻击:该 token family 的全部会话被撤销。
operationId: refresh
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RefreshRequest'
responses:
'200':
description: 轮换成功,返回新的令牌对
content:
application/json:
schema:
$ref: '#/components/schemas/AuthTokenEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
description: refresh token 已失效或被重用(code 40102
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
invalidated:
value: { code: 40102, message: refresh token 已失效或被重用, data: null }
/api/v1/auth/logout:
post:
tags: [auth]
summary: 退出(撤销当前会话)
description: |
撤销 body 中 refreshToken 对应的会话;其他设备的会话不受影响(ADR-003)。
需携带有效的 access token(从中取用户身份,防止跨账号撤销)。幂等:对已
失效的 refreshToken 仍返回成功。
operationId: logout
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/LogoutRequest'
responses:
'200':
description: 已退出
content:
application/json:
schema:
$ref: '#/components/schemas/VoidEnvelope'
'400':
$ref: '#/components/responses/ValidationError'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
/api/v1/me:
get:
tags: [user]
summary: 当前用户资料
description: 由 patbond-user 提供;access token 以 RS256 公钥本地验签,无需经过 auth 服务。
operationId: me
security:
- bearerAuth: []
responses:
'200':
description: 当前用户
content:
application/json:
schema:
$ref: '#/components/schemas/MeEnvelope'
'401':
$ref: '#/components/responses/AccessTokenInvalid'
'404':
description: 用户不存在(如已注销;code 40400)
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
/api/v1/events:
post:
tags: [analytics]
summary: 批量上报产品事件
description: |
埋点批量上报(事件字典见迭代报告 13 与第二迭代 06 号报告)。
`/api/v1` 下唯一允许匿名调用的写端点:`Authorization: Bearer` 可选——
缺失时按匿名处理放行;**一旦携带则完整校验**,无效 token 仍返回 401/40101。
- 单批 1–50 条;条数越界、字段校验失败或 JSON 不可解析时**整批** 400/40000。
- 通过请求级校验的批次一律返回 **202**`data.results` 与请求 `events`
等长且按原顺序逐条给出结果(accepted / duplicate / rejected);
客户端收到 202 即可删除本地队列中该批全部事件(rejected 条目不重试)。
- 幂等以每条事件的 `eventId` 去重(落库 ON CONFLICT DO NOTHING),重复条目
返回 `duplicate`(视为成功);**不使用** `Idempotency-Key` 请求头。
- 单条拒绝原因:事件名不在字典(`unknown_event_name`);已认证请求中事件
`userId` 与 token subject 不一致(`identity_mismatch`);props 的键命中
隐私红线模式 password/token/secret/phone/mobile/email/credential/idfa/gaid
`forbidden_field`);落库失败(`schema_invalid`)。
- props 中字典白名单之外的键**剥离后入库**(事件保留,不拒绝)。
- 匿名请求中事件携带的 `userId` 原样落库(分析归因数据,不参与权限判断)。
operationId: trackEvents
security:
- {} # 匿名(注册/登录前)
- bearerAuth: [] # 登录后携带未过期 access token
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/TrackEventsRequest'
responses:
'202':
description: 批次已受理,逐条结果见 data.results(与请求 events 等长、原顺序)
content:
application/json:
schema:
$ref: '#/components/schemas/TrackEventsEnvelope'
'400':
description: 整批拒绝——JSON 不可解析、events 为空或超过 50 条、单条事件字段校验失败(code 40000)
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
emptyBatch:
value: { code: 40000, message: events 长度必须在 1-50 之间, data: null }
'401':
description: 携带了 Authorization 头但 access token 无效或过期(code 40101);不携带该头则按匿名放行,不会返回 401
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
tokenInvalid:
value: { code: 40101, message: token 无效或过期, data: null }
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: 'Authorization: Bearer <accessToken>RS256 JWT'
responses:
ValidationError:
description: 参数校验失败(code 40000
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
validation:
value: { code: 40000, message: 参数校验失败, data: null }
AccessTokenInvalid:
description: access token 缺失、无效或过期(code 40101)
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
examples:
tokenInvalid:
value: { code: 40101, message: token 无效或过期, data: null }
schemas:
RegisterRequest:
type: object
required: [username, password]
properties:
username:
type: string
minLength: 3
maxLength: 32
description: 用户名,大小写不敏感唯一
example: demo_user
phone:
type: string
pattern: '^\+[1-9][0-9]{7,14}$'
description: 手机号,E.164 格式;可选,唯一
example: '+8613800138000'
password:
type: string
format: password
minLength: 6
maxLength: 64
example: secret123
nickname:
type: string
minLength: 1
maxLength: 32
description: 昵称;可选(冻结稿之外的可选扩展字段,前端可忽略)
example: 小柴
LoginRequest:
type: object
required: [username, password]
properties:
username:
type: string
example: demo_user
password:
type: string
format: password
example: secret123
RefreshRequest:
type: object
required: [refreshToken]
properties:
refreshToken:
type: string
description: 当前持有的 refresh token(不透明随机串)
example: Zx3v…43位base64url…Qk
LogoutRequest:
type: object
required: [refreshToken]
properties:
refreshToken:
type: string
description: 要撤销的当前会话的 refresh token
example: Zx3v…43位base64url…Qk
AuthTokens:
type: object
description: 注册 / 登录 / 刷新共用的令牌对(冻结契约,恰好这 6 个字段)
required:
- userId
- tokenType
- accessToken
- accessTokenExpiresAt
- refreshToken
- refreshTokenExpiresAt
properties:
userId:
type: string
format: uuid
example: 019212aa-0000-7000-8000-000000000001
tokenType:
type: string
enum: [Bearer]
example: Bearer
accessToken:
type: string
description: RS256 JWT,有效期 15 分钟(配置项)
example: eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiI…
accessTokenExpiresAt:
type: string
format: date-time
description: ISO 8601 带时区
example: '2026-09-04T04:20:06.789Z'
refreshToken:
type: string
description: 不透明随机串,有效期 30 天(配置项),每次刷新轮换
example: Zx3v…43位base64url…Qk
refreshTokenExpiresAt:
type: string
format: date-time
example: '2026-10-04T04:05:06.789Z'
Me:
type: object
description: 当前用户资料(冻结契约,恰好这 4 个字段)
required: [userId, username, createdAt]
properties:
userId:
type: string
format: uuid
example: 019212aa-0000-7000-8000-000000000001
username:
type: string
example: demo_user
phone:
type: string
nullable: true
description: E.164;未绑定时为 null
example: '+8613800138000'
createdAt:
type: string
format: date-time
example: '2026-09-04T04:05:06.789Z'
AuthTokenEnvelope:
type: object
required: [code, message]
properties:
code:
type: integer
enum: [0]
message:
type: string
example: success
data:
$ref: '#/components/schemas/AuthTokens'
MeEnvelope:
type: object
required: [code, message]
properties:
code:
type: integer
enum: [0]
message:
type: string
example: success
data:
$ref: '#/components/schemas/Me'
VoidEnvelope:
type: object
required: [code, message]
properties:
code:
type: integer
enum: [0]
message:
type: string
example: success
data:
nullable: true
example: null
ErrorEnvelope:
type: object
required: [code, message]
properties:
code:
type: integer
description: 稳定业务错误码(见顶部错误码表)
example: 40101
message:
type: string
example: token 无效或过期
data:
nullable: true
example: null
TrackEventsRequest:
type: object
required: [events]
properties:
events:
type: array
minItems: 1
maxItems: 50
description: 单批 150 条;越界整批 400/40000
items:
$ref: '#/components/schemas/TrackedEvent'
TrackedEvent:
type: object
required:
- eventId
- eventName
- eventVersion
- anonymousId
- sessionId
- clientTs
- appVersion
- platform
- osVersion
properties:
eventId:
type: string
format: uuid
description: 客户端生成的 UUID(规范要求 v7),服务端幂等去重键
example: 019212aa-4444-7000-8000-000000000001
eventName:
type: string
pattern: '^[a-z][a-z0-9_]{1,63}$'
description: 须在服务端事件字典内;不在字典中的事件名整条 rejectedunknown_event_name
example: auth_login_succeeded
eventVersion:
type: integer
description: 事件 schema 版本(字典 v1 全部为 1)
example: 1
anonymousId:
type: string
format: uuid
description: 设备级匿名标识,首次启动生成
example: 019212aa-0000-7000-8000-000000000001
userId:
type: string
format: uuid
nullable: true
description: |
登录后填充,可选。已认证请求中若与 token subject 不一致,该条
rejectedidentity_mismatch);匿名请求中原样落库,不做校验。
example: 019212aa-0000-7000-8000-000000000001
sessionId:
type: string
format: uuid
description: 客户端会话标识
example: 019212aa-1111-7000-8000-000000000001
clientTs:
type: string
format: date-time
description: 客户端本地时间(ISO 8601 带时区);serverTs 由服务端补写,客户端不发
example: '2026-09-07T04:05:06.789Z'
appVersion:
type: string
minLength: 1
maxLength: 32
example: 1.0.0+12
platform:
type: string
enum: [android, ios]
example: android
osVersion:
type: string
minLength: 1
maxLength: 32
example: android-14
props:
type: object
additionalProperties: true
description: |
事件专有属性,可选。按事件字典白名单处理:白名单外的键剥离后入库
(事件保留);键名命中隐私红线模式(password/token/secret/phone/
mobile/email/credential/idfa/gaid,不区分大小写、子串匹配)则整条
rejectedforbidden_field)。
example: { identifierType: username, durationMs: 123 }
TrackEventsResult:
type: object
description: 批次逐条结果(results 与请求 events 等长、按原顺序对应)
required: [accepted, duplicated, rejected, results]
properties:
accepted:
type: integer
description: 新落库条数
example: 1
duplicated:
type: integer
description: eventId 去重命中条数(视为成功,客户端不必重试)
example: 0
rejected:
type: integer
description: 被拒条数(客户端不重试)
example: 0
results:
type: array
items:
$ref: '#/components/schemas/EventResult'
EventResult:
type: object
required: [eventId, status]
properties:
eventId:
type: string
format: uuid
example: 019212aa-4444-7000-8000-000000000001
status:
type: string
enum: [accepted, duplicate, rejected]
example: accepted
reason:
type: string
enum: [unknown_event_name, identity_mismatch, forbidden_field, schema_invalid]
description: 仅 status=rejected 时出现(accepted/duplicate 不含该字段)
example: unknown_event_name
TrackEventsEnvelope:
type: object
required: [code, message]
properties:
code:
type: integer
enum: [0]
message:
type: string
example: success
data:
$ref: '#/components/schemas/TrackEventsResult'