docs: M2 第一波收口——报告 09/10/11 入档挂导航
CI / docs-build (push) Successful in 2m3s

- 09 契约补录 events(关闭 D-1/放行条件②,另含实现与旧规范 5 处出入记录)
- 10 Flutter 埋点修复(接线+三偏差+SessionTracker+page_viewed,34→51 测试,12/12 验收)
- 11 后端地基(V3/V4 迁移 8 表+种子、4 条跨 schema FK 剥离、patbond-pet 骨架、ADR-013 执行,82→95 测试)
- mkdocs build --strict 通过

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-07 14:54:19 +08:00
parent 2ceab6b296
commit 60258324e4
4 changed files with 422 additions and 0 deletions
@@ -0,0 +1,39 @@
# 09 · POST /api/v1/events 契约补录(D-1 关闭)
> 角色:API 契约工程师 · 日期:2026-09-07 · 对应:04 号报告 RC-5 / D-1,放行条件②
## 1. 做了什么
- `docs/api/openapi.yaml` 从 5 端点扩为 6 端点:新增 `POST /api/v1/events`tag `analytics`operationId `trackEvents`),info.version 1.0.0 → 1.1.0(纯增量,无既有字段变动)。
- 新增组件:`TrackEventsRequest` / `TrackedEvent` / `TrackEventsEnvelope` / `TrackEventsResult` / `EventResult`,错误分支复用既有 `ErrorEnvelope`,鉴权复用既有 `bearerAuth`,风格(camelCase、信封 `{code,message,data}`、examples 写法)与既有 5 端点一致。
- `docs/api/index.md` 端点清单同步为 6 端点。
- 校验:`python3 yaml.safe_load` 解析通过;`mkdocs build --strict` 通过(0.60s)。
**契约推导以代码实测行为为准**`patbond-api/patbond-user` analytics 包 + `AnalyticsIntegrationTest` 7 用例),不照抄 13 号报告草案——草案与实现的出入见 §3。
## 2. 逐项对照证据(契约条目 ↔ 实现)
| 契约条目 | 实现证据 |
| --- | --- |
| 批量 150,越界整批 400/40000 | `TrackEventsRequest.events``@Size(min=1,max=50)`;测试 `validationRejects400OnEmptyBatch`(空数组 → 400 + code 40000 |
| 合法批次一律 202 + 信封 `{code:0,…}` | Controller `ResponseEntity.status(ACCEPTED).body(ApiResponse.success(...))`;测试 `acceptsAnonymousEventBatch`202 + `$.code=0` |
| 逐条结果 `{accepted,duplicated,rejected,results[]}`results 与请求等长同序 | `TrackEventsResponse` 四字段;`AnalyticsService.trackEvents` 按输入顺序 append |
| `results[].status ∈ {accepted, duplicate, rejected}``reason` 仅 rejected 时出现 | `EventResult` 三个工厂方法;`@JsonInclude(NON_NULL)` + record 的 null reasonaccepted/duplicate 时 reason=null 不序列化) |
| `eventId` 幂等去重 → duplicate | repository `ON CONFLICT DO NOTHING`;测试 `deduplicationReturnsDuplicate`(同 eventId 二发 → `duplicated=1` |
| 匿名可报;带 Bearer 则完整校验,无效 401/40101 | `BearerAuthFilter.OPTIONAL_AUTH_PATHS = {"/api/v1/events"}`——仅 Authorization 头缺失时放行,头存在则走完整验签;测试 `acceptsAnonymousEventBatch` 无 Authorization 头成功 |
| 拒绝原因 4 枚举 | `unknown_event_name`(测试 `rejectsBatchWithUnknownEventName`)、`identity_mismatch`Service 第 2 步,token subject ≠ 事件 userId)、`forbidden_field`(测试 `rejectsEventWithForbiddenFieldPattern`,红线正则 password/token/secret/phone/mobile/email/credential/idfa/gaid)、`schema_invalid`(插入异常兜底) |
| 白名单外 props 剥离但事件保留 | `sanitizeProps`;测试 `stripsPropsOutsideWhitelist``forbiddenExtraField` 剥离,事件 accepted 且落库) |
| 单条事件 10 必填 + 2 可选(userId、props);platform 枚举 android/ioseventName 正则 `^[a-z][a-z0-9_]{1,63}$`appVersion/osVersion 132 | `TrackedEvent` 各字段的 `@NotNull/@Pattern/@Size` 注解逐一对应 |
| 不使用 Idempotency-Key 头 | Controller 无该头参数;13 号报告 §1.1 明文排除 |
## 3. 实现与草案/规范的不一致(仅记录,不改后端)
1. **64KB 请求体上限未实现**:13 号报告草案写「body ≤ 64KB 超限 400」,`application.yml` 无相应 max-size 配置、代码无检查(实际由 servlet 容器默认上限兜底)。契约据实**未写** 64KB;对应地草案的 `event_too_large` 拒绝原因实现中不存在,契约枚举未收录。
2. **429 限流未实现**:草案有「60 请求/5 分钟」429 + Retry-After,实现无任何限流。契约据实未写 429;后续若加限流属新增错误分支(additive),补契约即可。
3. **eventId 未强制 UUIDv7**:规范要求 v7,服务端仅校验 UUID 格式(客户端实际发 v4,见 06 号报告 §0.1 偏差 2)。契约在 description 注明「规范要求 v7」,schema 层保持 `format: uuid` 与实现一致。
4. **eventVersion 无 `minimum: 1` 校验**:草案 schema 有 `minimum: 1`,实现仅 `@NotNull`(0/负数可通过请求级校验)。契约据实不写 minimum,避免声称不存在的校验。
5. `platform` 枚举实现为正则 `^(android|ios)$`,与草案枚举等价,契约用 enum 表达。
## 4. 提交
独立提交(仅 openapi.yaml + index.md)已推送 patbond-doc main;本报告按波末统一提交约定暂不入库。