- 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:
@@ -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. 逐项对照证据(契约条目 ↔ 实现)
|
||||
|
||||
| 契约条目 | 实现证据 |
|
||||
| --- | --- |
|
||||
| 批量 1–50,越界整批 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 reason(accepted/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/ios;eventName 正则 `^[a-z][a-z0-9_]{1,63}$`;appVersion/osVersion 1–32 | `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;本报告按波末统一提交约定暂不入库。
|
||||
@@ -0,0 +1,260 @@
|
||||
# M2 第一波 Flutter 埋点修复报告
|
||||
|
||||
> 角色:Frontend Developer (Flutter)
|
||||
> 日期:2026-09-07
|
||||
> 依据:03 号技术评估、06 号埋点与实验规划、13 号埋点落地工程规范
|
||||
> 仓库:patbond-flutter @ dev 分支,基线 34 测试全绿
|
||||
> 任务:修复 M1 遗留的两个高优先埋点项(sessionId 生命周期、page_viewed 路由埋点),确保 M2 新事件的会话与版本维度可用
|
||||
|
||||
---
|
||||
|
||||
## 0. 执行摘要
|
||||
|
||||
**改动范围**:15 文件(6 新增 + 9 修改),766 行插入 / 49 行删除
|
||||
**测试数变化**:34 → 51(+17 新增:session_tracker 5 + analytics_service 补强 4 + route_observer 8)
|
||||
**质量门禁**:flutter analyze 0 问题,dart format 0 变更,51 测试全绿
|
||||
**提交**:2 个逻辑提交(4c2f839 接线修复 + SessionTracker + 三处偏差;6fef0db page_viewed 路由埋点),已推送 origin/dev
|
||||
|
||||
**核心修复**:
|
||||
|
||||
1. **生产接线修复**(03 §1.4 #4):app.dart 组装时传 analytics 实例给 ApiAuthRepository,修复 M1 遗留的「生产环境 `_analytics` 恒为 null、登录纵切埋点空转」问题
|
||||
2. **sessionId 生命周期**(06 §5.1 + 13 §3.1):新建 SessionTracker (WidgetsBindingObserver),冷启动/后台超 30 分钟换新 UUIDv7 sessionId,不再每事件随机生成
|
||||
3. **三处偏差修复**(06 §0.1):eventId 改 UUIDv7、appVersion 改 package_info_plus 动态读取、osVersion 改 Platform.operatingSystemVersion 正则提取
|
||||
4. **page_viewed 路由埋点**(06 §5.2 + 03 §3.2):AnalyticsRouteObserver 集中式捕获 didPush/didReplace/didPop,pageName 枚举化,referrer 链跨机制连贯,三类非路由曝光手动补点
|
||||
|
||||
---
|
||||
|
||||
## 1. 改动清单(按施工顺序)
|
||||
|
||||
### 1.1 SessionTracker(新建 lib/analytics/session_tracker.dart)
|
||||
|
||||
**职责**:管理 sessionId 生命周期,WidgetsBindingObserver 监听 app 生命周期状态。
|
||||
|
||||
**语义三条**(13 §4.0 + 06 §5.1):
|
||||
|
||||
1. 冷启动生成新 sessionId(构造时 `Uuid().v7()`)
|
||||
2. `AppLifecycleState.paused` → `resumed` 间隔 > 30 分钟:生成新 sessionId
|
||||
3. 间隔 ≤ 30 分钟:沿用原 sessionId
|
||||
|
||||
**实现要点**:
|
||||
|
||||
- 只在首次离开 `resumed` 状态时记录 `_leftForegroundAt`(level 级联 inactive/hidden/paused 不覆盖,否则间隔永趋近零)
|
||||
- sessionId 纯内存存储,不落 shared_preferences(03 §3.1 决策:冷启动本来就换新,持久化无增量价值)
|
||||
- 构造参数化 timeout(默认 30 分钟)与时钟注入(测试免真实等待)
|
||||
|
||||
**单测 5 例**(test/analytics/session_tracker_test.dart,验收标准第 5 条):
|
||||
|
||||
1. 冷启动生成 UUIDv7 格式
|
||||
2. 短后台(≤30 分钟)沿用原值
|
||||
3. 长后台(>30 分钟)换新
|
||||
4. 真实级联状态下退后台时刻不被 inactive 覆盖
|
||||
5. 连续多次短后台幂等,仅超阈值才换新
|
||||
|
||||
### 1.2 AnalyticsService 三处偏差修复(修改 lib/analytics/analytics_service.dart)
|
||||
|
||||
**改动点**:
|
||||
|
||||
| # | 偏差(06 §0.1) | 修复 | 验收证据 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | eventId 为 UUID v4 | 改为 `Uuid().v7()`(uuid 包已在依赖,直接用) | 单测断言 UUIDv7 正则 + 逐事件唯一 |
|
||||
| 2 | sessionId 每事件生成 | 构造参数 `getSessionId`,从 SessionTracker 注入 | 单测:同 tracker 下多事件 sessionId 相同 |
|
||||
| 3 | appVersion/osVersion 硬编码 | appVersion 构造默认 'unknown'、异步 `setAppVersion()`;osVersion 从 `Platform.operatingSystemVersion` 正则提取主版本 | 单测:setAppVersion 后事件携带注入值 |
|
||||
|
||||
**顺手加固**(03 §1.4 #1 的一行级缓解):上传失败批次重回队首而非整批丢弃(M0 行为),上限 500 条超限丢最旧。真正的 shared_preferences 分段持久化队列属 M2 第二波(03 §3.2 注意)。
|
||||
|
||||
**单测补强 4 例**(test/analytics/analytics_service_test.dart):
|
||||
|
||||
1. eventId 为 UUIDv7 且逐事件唯一
|
||||
2. 同一 tracker 下多事件 sessionId 相同,不再每事件生成
|
||||
3. appVersion 可注入更新(不再硬编码)
|
||||
4. 上传失败批次重回队列而非整批丢弃
|
||||
|
||||
### 1.3 生产接线修复(修改 lib/app/app.dart)
|
||||
|
||||
**问题根源**(03 §1.4 #4):`_buildRepository()` 构造 ApiAuthRepository 时未传 analytics 实例,生产构建里 `_analytics` 恒为 null,登录/注册/退出纵切的 5 处挂接点空转。
|
||||
|
||||
**修复**:
|
||||
|
||||
1. app.dart 的 `_AppState.initState()` 实例化 `AnalyticsService`(传入 `getSessionId: () => _sessionTracker.sessionId`)
|
||||
2. `_buildRepository()` 构造 ApiAuthRepository 时传 `analytics: _analytics`
|
||||
3. 异步初始化 appVersion(`PackageInfo.fromPlatform()` 后 `_analytics.setAppVersion()`)
|
||||
4. SessionTracker 注册/注销为 WidgetsBinding observer
|
||||
|
||||
**auth_repository.dart 签名调整**:构造器接收 `AnalyticsService? analytics`(沿用既有 `this._analytics` 私有命名参数风格)。
|
||||
|
||||
**新增依赖**:pubspec.yaml 加 `package_info_plus: ^8.1.2`(flutter pub add 自动选最新兼容版)。
|
||||
|
||||
### 1.4 page_viewed 路由埋点(新增 4 文件)
|
||||
|
||||
**架构**(03 §3.2 集中式 NavigatorObserver 方案):
|
||||
|
||||
| 文件 | 职责 |
|
||||
| --- | --- |
|
||||
| `analytics_page_name.dart` | pageName 编译期枚举(06 §5.2 字典 v2 + 03 Tab 映射),禁止自由字符串 |
|
||||
| `page_view_tracker.dart` | 上报单一出口:维护 referrer 链、去重、`reportTab` 记录主壳当前 Tab |
|
||||
| `analytics_route_observer.dart` | NavigatorObserver 派生:didPush/didReplace/didPop,字典外路由不上报 |
|
||||
| app.dart | 组装:routeObserver 挂 MaterialApp.navigatorObservers,resolveRootPage 回栈到无名根路由时解析当前页 |
|
||||
|
||||
**pageName 枚举**(AnalyticsPageName):
|
||||
|
||||
- **字典 v2 初始集合**(06 §5.2):login / register / home / profile / pet_list / pet_detail / pet_form / record_form / record_detail
|
||||
- **客户端现存页/Tab 补充**(03 §3.2):create / pet_archive / services / post_detail
|
||||
- M2 健康档案页面族尚未落地,pet_list 等先留枚举定义不接线
|
||||
|
||||
**三类非路由曝光手动补点**(03 §3.2):
|
||||
|
||||
1. **主壳 Tab 切换**(IndexedStack 无路由事件):`MainShellPage.selectTab()` 内 `pageViewTracker.reportTab()`,initState 补初始 Tab
|
||||
2. **认证状态机切页**(根部 AnimatedSwitcher 无路由事件):app.dart 的 `sessionManager.addListener(_reportAuthStateChange)`
|
||||
3. **回栈到无名根路由**(observer 的 didPop 无 previousRoute.name):`resolveRootPage` 回调按认证状态 + 主壳当前 Tab 返回页面
|
||||
|
||||
**既有 push 挂路由名**(03 §3.2 清单 4):
|
||||
|
||||
- login_page.dart:`Navigator.push(fadePageRoute(..., settings: RouteSettings(name: AnalyticsPageName.register.pageName)))`
|
||||
- main_shell_page.dart:`openPost()` 的 MaterialPageRoute 挂 `post_detail`
|
||||
- fade_route.dart:签名扩展可选 `RouteSettings? settings` 参数
|
||||
|
||||
**单测 8 例**(test/analytics/analytics_route_observer_test.dart,验收标准第 5 条):
|
||||
|
||||
1. push 路由报 page_viewed
|
||||
2. push 两页 referrer 链正确,pop 返回补报前一页(didPopNext)
|
||||
3. pop 回无名根路由经 resolveRootPage 补报
|
||||
4. 未在枚举的路由名不上报
|
||||
5. dialog 不上报(PopupRoute 不是 PageRoute)
|
||||
6. PageViewTracker:连续相同页面去重(Tab 重复点选)
|
||||
7. referrer 链跨机制连贯(首页无 referrer)
|
||||
8. reportTab 记录当前 Tab
|
||||
|
||||
---
|
||||
|
||||
## 2. 对照验收标准自证
|
||||
|
||||
### 2.1 sessionId 生命周期(06 §5.1 六条)
|
||||
|
||||
| # | 验收标准 | 自证 |
|
||||
| --- | --- | --- |
|
||||
| 1 | 新建 `lib/analytics/session_tracker.dart`,注册为 WidgetsBindingObserver;AnalyticsService 从它读 sessionId | ✓ session_tracker.dart 新建,app.dart 注册 observer,AnalyticsService 构造接收 `getSessionId` 注入 |
|
||||
| 2 | 语义三条:冷启动生成新;paused→resumed 超 30 分钟生成新;≤30 分钟沿用 | ✓ 构造时生成、didChangeAppLifecycleState 判定间隔 |
|
||||
| 3 | 同一前台会话内所有事件 sessionId 完全一致 | ✓ 单测「同一 tracker 下多事件 sessionId 相同」通过 |
|
||||
| 4 | sessionId 为 UUID,不落任何持久化存储 | ✓ `Uuid().v7()` 生成,纯内存字段 `_sessionId` |
|
||||
| 5 | 单元测试 ≥3 例:冷启动/短后台/长后台 | ✓ session_tracker_test.dart 5 例(冷启动/短≤30min/长>30min/级联状态/连续幂等) |
|
||||
| 6 | 真机手测脚本 | 交付 QA/开发者手测(登录→退后台 5min→回前台操作→退后台 35min→回前台,库内应恰好 2 个 sessionId) |
|
||||
|
||||
### 2.2 page_viewed 路由埋点(06 §5.2 六条)
|
||||
|
||||
| # | 验收标准 | 自证 |
|
||||
| --- | --- | --- |
|
||||
| 1 | RouteObserver 注册进 MaterialApp.navigatorObservers,didPush/didPopNext 触发 page_viewed | ✓ AnalyticsRouteObserver 注册,didPush/didReplace/didPop 实现 |
|
||||
| 2 | pageName 是编译期枚举,v2 初始集合 9 个 + 客户端现存 4 个;带参数路由归一化 | ✓ AnalyticsPageName 枚举 13 个值,fromRouteName 映射,路由名取自枚举 pageName 字段 |
|
||||
| 3 | referrer = 前一页 pageName,栈底/冷启动首页为 null | ✓ PageViewTracker 维护 `_lastPageName`,首次报告无 referrer;单测「referrer 链跨机制连贯」通过 |
|
||||
| 4 | 不在字典枚举内的路由不上报 | ✓ AnalyticsPageName.fromRouteName 返回 null 时 observer 不调 track;单测「未在枚举的路由名不上报」通过 |
|
||||
| 5 | 单测/widget 测试:push 两页断言两条事件且 referrer 链正确;pop 返回断言 didPopNext 补报 | ✓ analytics_route_observer_test.dart:「push 两页 referrer 链正确,pop 返回补报前一页」通过 |
|
||||
| 6 | M1 存量四页与 M2 新页一次性挂全 | ✓ login/register 由 RouteSettings 挂;home/create/pet_archive/services/profile 由 Tab 补点;post_detail 由 openPost 挂;M2 健康档案页面族枚举已预留、待功能落地接线 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 测试数变化与覆盖
|
||||
|
||||
**基线**:34 测试全绿(第一迭代收官记录)
|
||||
**收官**:51 测试全绿(+17 新增)
|
||||
|
||||
**新增分布**:
|
||||
|
||||
- `test/analytics/session_tracker_test.dart`:5 例(冷启动/短后台/长后台/级联状态/连续幂等)
|
||||
- `test/analytics/analytics_service_test.dart` 补强:4 例(UUIDv7/sessionId 不再逐事件生成/appVersion 可注入/失败重回队列)
|
||||
- `test/analytics/analytics_route_observer_test.dart`:8 例(push/pop/referrer 链/根路由 resolveRootPage/枚举外不报/dialog 不报/Tab 去重/reportTab)
|
||||
|
||||
**既有测试回归**:34 测试 0 失败,登录/注册/主壳冒烟测试、auth_repository 单测、widget 组件测试均不受影响(analytics 注入为可选参数,测试继续传 null/FakeAuthRepository)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 新增依赖说明
|
||||
|
||||
| 依赖 | 版本 | 用途 | 引入理由 |
|
||||
| --- | --- | --- | --- |
|
||||
| package_info_plus | ^8.1.2 | 读取 app 版本号(version + buildNumber) | 替代硬编码 appVersion,使版本维度指标可用(M2 起按版本切片看回归,06 §0.1 偏差 3) |
|
||||
|
||||
uuid ^4.6.0 已在既有依赖(M1 用于 Idempotency-Key 与 anonymousId),直接用其 v7() 方法。Platform 来自 dart:io 标准库,无需新增依赖。
|
||||
|
||||
---
|
||||
|
||||
## 5. 遗留与下一波
|
||||
|
||||
### 5.1 本波完成项(06 §7.2 三处客户端小修)
|
||||
|
||||
1. ✅ sessionId 生命周期(P0,会话维度指标前置)
|
||||
2. ✅ eventId 改 UUIDv7(顺手修,保留插入局部性)
|
||||
3. ✅ appVersion/osVersion 动态读取(版本维度可用)
|
||||
4. ✅ page_viewed 路由埋点(M2 新增高频事件,漏斗前置)
|
||||
5. ✅ 生产接线修复(M1 遗留,本波一并关闭)
|
||||
|
||||
### 5.2 未闭环项(排入 M2 第二波或后续迭代)
|
||||
|
||||
1. **shared_preferences 分段持久化队列**(13 §3.3 原规范):本波仅顺手加固失败重回队列(一行级),真正的 500 条分段、20 条/段、溢出淘汰最旧段排 M2 第二波(03 §3.2 注意、06 §7.2 工单拆分 1)
|
||||
2. **主壳 Tab/认证切页的 page_viewed 单测**:widget_test.dart 主壳冒烟测试未断言 page_viewed 事件(本波集成测试成本高,Tab 切换逻辑已由 PageViewTracker 单测覆盖去重语义)
|
||||
3. **健康档案页面族 RouteSettings 接线**:pageName 枚举已预留 pet_list/pet_detail/pet_form/record_form/record_detail,待 M2 健康档案功能落地时挂接(06 §5.2 验收 6 注明「M2 新页待功能落地接线」)
|
||||
4. **真机手测脚本执行**:sessionId 生命周期的 30 分钟后台判定需真机/模拟器验证(06 §5.1 验收 6),交付 QA 或开发者手测
|
||||
|
||||
---
|
||||
|
||||
## 6. 质量门禁通过记录
|
||||
|
||||
```bash
|
||||
$ flutter analyze
|
||||
No issues found! (ran in 0.9s)
|
||||
|
||||
$ dart format --set-exit-if-changed lib test
|
||||
Formatted 46 files (0 changed) in 0.21 seconds.
|
||||
|
||||
$ flutter test
|
||||
00:03 +51: All tests passed!
|
||||
```
|
||||
|
||||
**代码行数**:+766 插入 / -49 删除,净增 717 行(含注释与测试)
|
||||
**文件数**:6 新增(session_tracker + page_name + route_observer + page_view_tracker + 2 测试文件)+ 9 修改
|
||||
|
||||
---
|
||||
|
||||
## 7. 提交记录
|
||||
|
||||
**仓库**:patbond-flutter @ dev 分支
|
||||
**基线**:3f8388e fix: 清零 flutter analyze 问题并修复隐私红线正则缺陷(CI 门禁)
|
||||
**提交**:
|
||||
|
||||
```
|
||||
4c2f839 修复:埋点接线与三处偏差(sessionId/eventId/设备信息)
|
||||
- 生产接线修复:app.dart 传 analytics 给 ApiAuthRepository
|
||||
- sessionId 生命周期:SessionTracker (WidgetsBindingObserver)
|
||||
- eventId 改 UUIDv7;appVersion 动态注入;osVersion 动态读取
|
||||
- 队列顺手加固:失败批次重回队列
|
||||
- 新增依赖 package_info_plus
|
||||
- 测试 +9 例(session_tracker 5 + analytics_service 补强 4)
|
||||
|
||||
6fef0db 新增:page_viewed 集中式路由埋点(NavigatorObserver)
|
||||
- AnalyticsRouteObserver 页面零侵入
|
||||
- AnalyticsPageName 枚举编译期锁死
|
||||
- PageViewTracker 维护 referrer 链与去重
|
||||
- 三类非路由曝光手动补点(Tab/认证/根路由)
|
||||
- 测试 +8 例(analytics_route_observer_test)
|
||||
```
|
||||
|
||||
**已推送**:origin/dev(施工过程中曾误将全部改动合并进单提交 f501a95 并推送,随即以同内容的上述两个拆分提交 `--force-with-lease` 替换,内容零差异)。
|
||||
|
||||
---
|
||||
|
||||
## 8. 施工过程记录(debug trail)
|
||||
|
||||
1. 通读三份规范(06/03/13)与仓库现状,核对既有 34 测试基线
|
||||
2. 发现 package_info_plus 未在依赖,`flutter pub add package_info_plus` 新增
|
||||
3. 新建 SessionTracker(WidgetsBindingObserver),注意级联状态处理(首次离开 resumed 才记时)
|
||||
4. 修改 AnalyticsService 三处偏差(eventId v7 / sessionId 注入 / appVersion 可变 / osVersion 动态)
|
||||
5. 修改 app.dart 接线(实例化 analytics + tracker,传给 repository,注册 observer,认证切换监听)
|
||||
6. 新建 page_viewed 四件套(枚举/tracker/observer/接线),主壳 Tab 补点,login_page/fade_route 挂路由名
|
||||
7. 编写 session_tracker_test(5 例)+ analytics_service_test 补强(4 例)+ analytics_route_observer_test(8 例)
|
||||
8. `flutter test` 第一轮编译错误:测试 lambda 签名不匹配(`(name, props)` 改 `(name, [props])`)
|
||||
9. `flutter analyze` 第一轮警告:page_view_tracker 的 map literal 空安全操作符误用,改为命令式条件插入
|
||||
10. 全绿后 `dart format` 确认无格式变更,提交代码(1 个合并提交),推送 origin/dev
|
||||
|
||||
---
|
||||
|
||||
**Frontend Developer** · 2026-09-07
|
||||
基线测试 34 全绿 → 收官 51 全绿(+17),flutter analyze 0 问题,已推送。
|
||||
@@ -0,0 +1,120 @@
|
||||
# M2 第一波后端地基施工报告(B 线:V3 迁移 + patbond-pet 骨架 + ADR-013)
|
||||
|
||||
> 作者:Senior Developer(后端)
|
||||
> 日期:2026-09-07
|
||||
> 工单:T2-01(Flyway V3/V4)、T2-02 前置(patbond-pet 模块骨架)、ADR-013 执行、错误码预置
|
||||
> 代码基线:patbond-api `0d81c38`(82 测试全绿)→ 交付 `58576f8`(95 测试全绿)
|
||||
> 结论先行:**V3 建 8 表(health_event_media 按 ADR-010 不建),4 条 marketplace 跨 schema 外键全部剥离;patbond-pet 模块挂入构建链并纳入 compose;health_record_action 已从白名单移除;全套 95 测试在干净 postgres:18 上全绿。**
|
||||
|
||||
---
|
||||
|
||||
## 1. 提交清单
|
||||
|
||||
按拆分建议分三个提交,全部已推送 `origin/dev`:
|
||||
|
||||
| 提交 | 内容 |
|
||||
| --- | --- |
|
||||
| `49299fb` | feat: Flyway V3 pet_health 结构基线 + V4 字典种子 + pet 域错误码(T2-01) |
|
||||
| `0eae1c9` | feat: 新建 patbond-pet 模块骨架(ADR-009,T2-02 前置) |
|
||||
| `58576f8` | refactor: 移除 EventDictionary 的 health_record_action(ADR-013) |
|
||||
|
||||
> **流程说明**:iteration-2/08 规划中 Flyway 迁移属「短命分支 + PR 合入 dev」的推荐实践;本次第一波经用户拍板直接推 dev,特此注明。
|
||||
|
||||
## 2. Flyway V3/V4:表清单与裁剪对照
|
||||
|
||||
### 2.1 V3 结构基线(`patbond-user/src/main/resources/db/migration/V3__pet_health_baseline.sql`)
|
||||
|
||||
从目标模型 `patbond-doc/docs/database/patbond_postgresql.sql`(333~554 行)原样提取,共建 **8 张表**:
|
||||
|
||||
| # | 表 | 处置 | 与目标模型的差异 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | `pet_health.breeds` | 建 | 无差异 |
|
||||
| 2 | `pet_health.pets` | 建 | 无差异(`avatar_asset_id` FK 到 `media.assets` 保留——media 表 V1 已建,仅上传流程未实现,列 M2 不写入) |
|
||||
| 3 | `pet_health.pet_owners` | 建 | 无差异(含 owner/caregiver/viewer 角色约束与 primary owner 部分唯一索引,ADR-015 权限模型的数据基础) |
|
||||
| 4 | `pet_health.pet_weight_records` | 建 | 无差异 |
|
||||
| 5 | `pet_health.vaccine_catalog` | 建 | 无差异 |
|
||||
| 6 | `pet_health.pet_vaccinations` | 建 | **剥离 2 条跨 schema FK**(见 2.2);列全保留 |
|
||||
| 7 | `pet_health.health_events` | 建 | **剥离 2 条跨 schema FK**(见 2.2);列全保留 |
|
||||
| 8 | `pet_health.care_reminders` | 建 | 无差异 |
|
||||
| — | `pet_health.health_event_media` | **不建** | ADR-010:media/附件剪出 M2;该表 `asset_id` 为 NOT NULL FK 到 `media.assets` 且 media 上传流程零代码,与 pet 域业务强耦合无意义。纯增量表,待 media 专项落地时以新版本迁移补建,零成本 |
|
||||
|
||||
其余保留项:全部 CHECK 约束、部分唯一索引(`uq_pet_vaccination_dose`、`uq_pet_primary_owner`、`uq_pets_microchip` 等)、4 个 `updated_at` 触发器(复用 V1 的 `platform.set_updated_at()`,无需新建函数)。`pet_owners.user_id`、`health_events.created_by_user_id` 到 `identity.users` 的跨 schema FK 保留(与 V1 中 `media.assets.owner_user_id` 先例一致,共库阶段成立)。
|
||||
|
||||
### 2.2 强制裁剪:4 条 marketplace 跨 schema 外键(逐条对照)
|
||||
|
||||
bootstrap SQL 第 **1156~1166 行**(现实核查已证实行号)以 `ALTER TABLE` 追加的 4 条约束,V3 **全部剥离**,对应字段保留为裸可空 uuid 列,索引照建:
|
||||
|
||||
| # | 约束名 | 原定义 | V3 处置 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | `fk_vaccinations_provider` | `pet_vaccinations.provider_id → marketplace.providers(id) ON DELETE SET NULL` | 剥离;`provider_id uuid` 裸列保留,`ix_vaccinations_provider` 索引保留 |
|
||||
| 2 | `fk_vaccinations_booking` | `pet_vaccinations.booking_id → marketplace.bookings(id) ON DELETE SET NULL` | 剥离;`booking_id uuid` 裸列保留,`ix_vaccinations_booking` 索引保留 |
|
||||
| 3 | `fk_health_events_provider` | `health_events.provider_id → marketplace.providers(id) ON DELETE SET NULL` | 剥离;裸列 + `ix_health_events_provider` 保留 |
|
||||
| 4 | `fk_health_events_booking` | `health_events.booking_id → marketplace.bookings(id) ON DELETE SET NULL` | 剥离;裸列 + `ix_health_events_booking` 保留 |
|
||||
|
||||
迁移文件头部注释已逐条列出并标明「**M5 迁移 marketplace schema 时以新版本迁移补回**」。集成测试断言这 4 条 FK 确不存在(防照抄回归)。
|
||||
|
||||
### 2.3 V4 字典种子(`V4__pet_health_dictionary_seed.sql`)
|
||||
|
||||
按 02 号评估建议采用「V3 结构 + V4 种子」划分:breeds/vaccine_catalog 是应用 FK 指向的生产参考数据,走正式迁移链而非 `db/dev`(与开发 fixture 性质不同)。
|
||||
|
||||
- `breeds`:28 条(犬 16 + 猫 12,常见品种,含「中华田园犬/猫」兜底项)
|
||||
- `vaccine_catalog`:10 条(犬 6:二/四/五/八联、狂犬、犬窝咳;猫 4:三联、狂犬、白血病、衣原体)
|
||||
- 正典目录内容与量级按 D2-6 由产品侧供稿,届时以后续迁移追加/修订
|
||||
|
||||
## 3. patbond-pet 模块骨架(ADR-009)
|
||||
|
||||
```text
|
||||
patbond-pet/
|
||||
├── Dockerfile # 同 user/auth 模式(temurin-17-jre,uid 10001,无状态)
|
||||
├── pom.xml # 挂入父 pom,依赖对齐既有模块(common/web/validation/jdbc + Testcontainers)
|
||||
└── src/
|
||||
├── main/java/com/patbond/patbond/pet/
|
||||
│ ├── PetApplication.java # Spring Boot 入口
|
||||
│ ├── controller/HealthController.java # GET /health 探活(含 SELECT 1 连通检查)
|
||||
│ └── web/GlobalExceptionHandler.java # 同一 {code,message,data} 信封契约
|
||||
├── main/resources/application.yml.sample # .sample 模式,默认端口 8083,DB 经环境变量注入
|
||||
└── test/java/com/patbond/patbond/pet/
|
||||
├── TestcontainersConfiguration.java # postgres:18 @ServiceConnection
|
||||
├── PetApplicationTests.java # 上下文启动冒烟
|
||||
└── controller/HealthControllerTest.java # /health 200 + db=up 断言
|
||||
```
|
||||
|
||||
关键取舍:
|
||||
|
||||
- **Flyway 归属不拆**:pet 模块**不携带 Flyway**。单一迁移链(V1..V4,含 pet_health 基线)仍由 patbond-user 启动时统一执行——共库单 `flyway_schema_history`,拆链需为新模块配独立 history 表,收益为零。pet 模块只经 JdbcClient 读写 `pet_health` schema(第二波接口落地时)。
|
||||
- **compose 编排已纳入**:既有模式是每服务一个 compose service(build + .sample 挂载 + 环境变量注入),pet 照此加入;`depends_on` postgres 健康 + user 先起(保证迁移已执行、pet_health schema 就绪)。
|
||||
- **鉴权后置第二波**:骨架暂无 `/api/v1` 业务端点,故未接入 JWT 校验;`/health` 刻意放在 `/api/v1` 之外(基础设施探针无业务数据)。第二波接口落地时按 user 模块同一约定接入 RS256 本地验签(`BearerAuthFilter` 模式,届时评估下沉 common 或复制)。
|
||||
|
||||
## 4. ADR-013 执行与错误码预置
|
||||
|
||||
- `EventDictionary` 移除 `health_record_action` 白名单项,注释同步改写(引 ADR-013);`page_viewed` 与 v1 auth 漏斗事件保留为完整白名单。既有测试无一引用该事件,零测试改动;新增 `EventDictionaryTest`(3 例)锁定移除后的白名单边界。
|
||||
- `ErrorCode`(patbond-common)按 02 号建议预置 4 个 pets 域错误码,延续既有编号段、不重编号:
|
||||
|
||||
| code | 枚举名 | HTTP | 语义 |
|
||||
| --- | --- | --- | --- |
|
||||
| 40300 | `PET_ACCESS_DENIED` | 403 | 对可见宠物无相应操作权限(如 viewer 尝试写) |
|
||||
| 40401 | `PET_NOT_FOUND` | 404 | 宠物不存在或调用者不可见(防 ID 枚举) |
|
||||
| 40402 | `RECORD_NOT_FOUND` | 404 | 宠物下的记录不存在 |
|
||||
| 40902 | `VERSION_CONFLICT` | 409 | 乐观锁版本冲突 |
|
||||
|
||||
当前无消费方,第二波接口纵切直接使用;契约(openapi.yaml)本波不动,随 T2-09 冻结时一并写入。
|
||||
|
||||
## 5. 测试数变化:82 → 95(+13,0 回归)
|
||||
|
||||
| 模块 | 基线 | 交付 | 新增内容 |
|
||||
| --- | --- | --- | --- |
|
||||
| patbond-common | 3 | 3 | — |
|
||||
| patbond-user | 48 | 59 | `PetHealthMigrationIntegrationTest` 8 例(schema 存在、8 表齐、结构抽查、**4 条 marketplace FK 确不存在**、触发器 4 个、V4 种子非空与抽查);`EventDictionaryTest` 3 例 |
|
||||
| patbond-auth | 31 | 31 | — |
|
||||
| patbond-pet | — | 2 | 上下文冒烟 + /health 探活 |
|
||||
| **合计** | **82** | **95** | `JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./mvnw clean test` 一次通过,BUILD SUCCESS |
|
||||
|
||||
V1→V2→V3→V4 全量迁移经 Testcontainers 在全新 postgres:18 容器上自动验证通过(每个 @SpringBootTest 上下文启动即执行全链迁移)。
|
||||
|
||||
## 6. 遗留与下一波衔接
|
||||
|
||||
- **T2-02 剩余部分**(第二波):pet 模块接入 JWT 资源侧校验(`BearerAuthFilter`/`JwtVerifier`/`UuidV7` 下沉 common 或复制的决策届时定)、当前用户解析注入。
|
||||
- **health_event_media**:随 media 专项(对象存储选型拍板后)以新迁移补建。
|
||||
- **4 条 marketplace FK**:M5 迁移 marketplace schema 的版本迁移中补回(V3 文件注释已标明)。
|
||||
- **CI**:`.gitea/workflows/ci.yml` 跑 `./mvnw -B clean test`,多模块 reactor 自动含 patbond-pet,无需改动;push 后 CI 状态由波末闭环核对。
|
||||
- **正典字典数据**:D2-6 产品侧供稿后以后续迁移替换/扩充 V4 种子。
|
||||
Reference in New Issue
Block a user