Files
lixi 60258324e4
CI / docs-build (push) Successful in 2m3s
docs: M2 第一波收口——报告 09/10/11 入档挂导航
- 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>
2026-09-07 14:54:19 +08:00

40 lines
4.4 KiB
Markdown
Raw Permalink 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.
# 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;本报告按波末统一提交约定暂不入库。