docs(api): 契约补录 POST /api/v1/events(关闭 D-1)
CI / docs-build (push) Successful in 1m22s

以 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>
This commit is contained in:
2026-09-07 14:09:45 +08:00
parent 1891d9b7b4
commit 2ceab6b296
2 changed files with 207 additions and 5 deletions
+4 -1
View File
@@ -1,5 +1,8 @@
# API 契约 # API 契约
第一迭代认证域的正式契约见 [openapi.yaml](openapi.yaml)OpenAPI 3:注册、登录、刷新、退出、当前用户 5 个端点,统一错误信封 `{code, message, data}` 与错误码表(40000/40100/40101/40102/40900/40901/42300),以及会话轮换与登录锁定策略说明。 正式契约见 [openapi.yaml](openapi.yaml)OpenAPI 3,当前 6 个端点:
- 认证域(第一迭代冻结):注册、登录、刷新、退出、当前用户 5 个端点,统一错误信封 `{code, message, data}` 与错误码表(40000/40100/40101/40102/40900/40901/42300),以及会话轮换与登录锁定策略说明。
- 埋点域(M2 第一波补录):`POST /api/v1/events` 批量上报产品事件——单批 1–50 条、202 逐条结果(accepted/duplicate/rejected)、`eventId` 幂等去重、唯一允许匿名的写端点(携带 Bearer 则完整校验)。
约定:契约变更须先改本文件目录下的 OpenAPI,再改实现(契约先行);错误码只增不改义。 约定:契约变更须先改本文件目录下的 OpenAPI,再改实现(契约先行);错误码只增不改义。
+203 -4
View File
@@ -1,9 +1,10 @@
openapi: 3.0.3 openapi: 3.0.3
info: info:
title: Patbond API — Auth & Me(第一批公开接口 title: Patbond API — Auth / Me / Events(公开契约
version: 1.0.0 version: 1.1.0
description: | description: |
Patbond 第一迭代「真实登录纵切」公开契约(冻结稿的正式化,字段与草案零偏差) Patbond 第一迭代「真实登录纵切」公开契约(冻结稿的正式化,字段与草案零偏差)
1.1.0 追加埋点上报端点 `POST /api/v1/events`M2 第一波契约补录,以实现实测行为为准)。
## 通用约定(development-plan 第 6 节) ## 通用约定(development-plan 第 6 节)
- 公开接口统一前缀 `/api/v1`JSON 字段一律 `camelCase`;资源 ID 为 UUID 字符串。 - 公开接口统一前缀 `/api/v1`JSON 字段一律 `camelCase`;资源 ID 为 UUID 字符串。
@@ -42,13 +43,15 @@ servers:
- url: http://127.0.0.1:8081 - url: http://127.0.0.1:8081
description: patbond-auth(本地开发,/api/v1/auth/** description: patbond-auth(本地开发,/api/v1/auth/**
- url: http://127.0.0.1:8082 - url: http://127.0.0.1:8082
description: patbond-user(本地开发,/api/v1/me description: patbond-user(本地开发,/api/v1/me、/api/v1/events
tags: tags:
- name: auth - name: auth
description: 注册 / 登录 / 刷新 / 退出(patbond-auth description: 注册 / 登录 / 刷新 / 退出(patbond-auth
- name: user - name: user
description: 当前用户(patbond-user description: 当前用户(patbond-user
- name: analytics
description: 产品事件批量上报(patbond-user
paths: paths:
/api/v1/auth/register: /api/v1/auth/register:
@@ -209,6 +212,63 @@ paths:
schema: schema:
$ref: '#/components/schemas/ErrorEnvelope' $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: components:
securitySchemes: securitySchemes:
bearerAuth: bearerAuth:
@@ -409,3 +469,142 @@ components:
data: data:
nullable: true nullable: true
example: null 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'