Compare commits

..

2 Commits

Author SHA1 Message Date
lixi 209021e7d2 docs: 迁入第一迭代过程报告并建立进展看板
- 新增 development/iterations/iteration-1/:15 份角色报告 + 进展看板(已完成/未闭环/下一步),作为双人协作的进度事实来源
- 新增 ADR-006:测试与交付容器化策略(Testcontainers / 交付 Docker 包 / 本机库仅个人联调)
- Git 工作流规范补充:敏感信息只进忽略文件或 sample、测试数据不入库、测试代码限标准测试目录
- 门禁:mkdocs build --strict 通过(零警告)
2026-09-04 10:45:05 +08:00
lixi 027876ae00 docs: 增加 Git 工作流规范
- 三仓分支模型(api/flutter 以 dev 为集成分支、doc 直接 main)、feature 分支时机
- 固化中文语义前缀提交约定:正文写验收证据、引用 ADR 编号
- 禁止事项:敏感配置/构建产物不入库、共享分支不 force push、已推送 Flyway 迁移不可变(呼应开发计划 4.3)
- 按仓库分列提交前本地门禁命令清单,作为未来 CI 门禁蓝本
- 门禁:mkdocs build --strict 通过(零 warning)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-04 10:36:34 +08:00
19 changed files with 3120 additions and 0 deletions
+10
View File
@@ -61,3 +61,13 @@
- 主题以语义 token 组织(primary / canvas / ink / error 等),集中在单一主题文件。
- 新增 error 色 `#D0342C`(设计稿与现有主题均缺失)。
- 实心按钮使用加深的 `primaryStrong #D6431A` 以满足 WCAG AA 对比度;原珊瑚橙 `#FF6F4C` 用于装饰与非文字承载场景。
## ADR-006 测试与交付容器化策略
**决策**2026-09-04):
- 自动化测试中的数据库一律通过 Testcontainers 使用 Docker 临时容器(`postgres:16`),不依赖开发者本机数据库;每次测试在干净实例上执行全量 Flyway 迁移。
- 最终交付将提供 Docker 镜像/编排包(对应开发计划 M6 的容器镜像项)。
- 开发者本人手动测试/联调时可使用自己本机的 PostgreSQL,连接信息经环境变量注入,不入库。
**理由**:测试可复现、与目标版本(PostgreSQL 16)对齐、不受本机数据库账号/版本差异影响;交付形态与测试基础设施统一到 Docker。
+44
View File
@@ -0,0 +1,44 @@
# Git 工作流规范
适用于三个仓库:`patbond-api`dev)、`patbond-flutter`dev)、`patbond-doc`main)。
## 分支模型
- **patbond-api / patbond-flutter**`dev` 为集成分支,保持随时可构建(门禁全绿)。日常改动小步直接提交到 `dev`
- **patbond-doc**:直接提交 `main`
- **何时开 feature 分支**:改动跨多天、有破坏性风险(如大规模重构、依赖升级)、或多人并行同一仓库时,从最新 `dev` 拉出 `feat/<主题>` / `fix/<主题>` 分支,完成后合回并删除分支。短命分支,不留长期分叉。
## 提交信息约定
格式:`<前缀>: <中文主题>`,前缀取 `feat` / `fix` / `refactor` / `docs` / `test` / `chore`
- 主题一句话说清做了什么;涉及架构决策时在主题或正文引用 ADR 编号(如 `ADR-005`)。
- 正文用列表写关键改动与**验收证据**(测试数量与结果、门禁命令输出结论),让提交自证可用。
- 一次提交做一件事,可独立回退;不把无关改动混进同一提交。
示例(既有惯例):
```text
feat: 迁移珊瑚橙主题体系并新增认证基础组件(ADR-005)
- 新增 BrandMark/AppTextField 等 5 个组件及 6 个 widget 测试
- 门禁:dart format0 changed/ flutter analyze0 issues/ flutter test7 passed
```
## 禁止事项
- **不提交敏感配置与构建产物**:本地 `application.yml``target/``build/``.dart_tool/``.idea/`、密钥凭据一律不入库(.gitignore 已覆盖,提交前 `git status` 逐一核对暂存清单)。配置只提交 `*.sample`;任何关键/敏感信息只能存在于被忽略的文件或 `.sample` 占位中。
- **不提交测试产生的数据**:测试运行产生的数据文件、数据库导出、临时输出一律不入库。测试代码可以入库,但必须放在标准测试目录(Java 为 `src/test/`Flutter 为 `test/`),不得散落在业务代码目录。
- **不 force push 共享分支**`dev` / `main`)。个人 feature 分支整理历史后如需强推,用 `git push --force-with-lease`
- **不修改已推送的 Flyway 迁移**(呼应开发计划 4.3 节):`V1__*.sql` 等已进入 `dev` 的版本化迁移视为不可变,schema 变更一律新增 `V<n+1>__*.sql`
- 不改写已推送的提交历史(rebase/amend 仅限未推送内容)。
## 提交前本地门禁(未来 CI 将执行同一清单)
| 仓库 | 必跑命令 | 通过标准 |
| --- | --- | --- |
| patbond-api | `JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./mvnw clean test` | BUILD SUCCESS0 失败(需 Docker 供 Testcontainers |
| patbond-flutter | `dart format --output=none --set-exit-if-changed lib test`<br>`flutter analyze`<br>`flutter test` | 0 changed / No issues / All tests passed |
| patbond-doc | `mkdocs build --strict -d <临时目录>` | exit 0,零 warning;不把 `site/` 落进仓库 |
门禁不绿不提交。CI 载体(审计 M3 后半)落地后将原样执行上表命令作为合入门禁。
@@ -0,0 +1,221 @@
# Patbond 第一迭代任务分解(M0 工程基线 + 真实登录纵切)
> 作者:Senior Project Manager
> 日期:2026-09-03
> 依据:`patbond-doc/docs/development/development-plan.md`Draft 1.02026-09-03
> 范围声明:严格限定为第 7 节 M0 与第 8 节「真实登录纵切」8 项任务。社区、AI 创作、预约、宠物健康均不在本迭代范围内(文档第 8 节明确:"第一迭代暂不开发 AI Worker、社区 Feed 或预约")。
## 1. 范围与合并说明
- **M0 目标**(第 7 节):"让所有开发者能用一致方式启动、测试和联调。" 验收标准:"新机器仅依据仓库文档即可启动后端、连接本地数据库并运行自动化检查。"
- **第一迭代目标**(第 8 节):真实登录纵切,端到端验证"数据库、认证、客户端和测试链路能够贯通"。
- **重叠合并**M0 的"将 bootstrap SQL 转为 Flyway baseline,并分离开发种子数据"与第 8 节任务 1"从 bootstrap SQL 提取 identity/media 的 Flyway baseline"是同一件事在本迭代的落地范围。合并为工单 T1,本迭代只做 identity/media 两个 schema 的 baseline,其余 schema 的 Flyway 化随后续迭代进行。
- **媒体范围提示**:第 8 节 8 项任务不含媒体上传实现(那是 M1 后半段),T1 仅按文档字面提取 media 的表结构 baseline,不开发 `POST /api/v1/media/uploads`
预估规模口径:S ≈ 半天内,M ≈ 1-2 天,L ≈ 3-5 天(含测试与文档)。
---
## 2. 工单列表
### A 组:M0 工程基线(6 个工单)
#### T0-1 接入 Maven Wrapper
- **仓库**patbond-api
- **描述**:为多模块 Maven 工程添加 `mvnw`/`mvnw.cmd` 与 wrapper 配置,锁定 Maven 3.9+README 中的构建命令改用 `./mvnw`
- **验收标准**
- 干净检出后不安装本机 Maven`./mvnw -pl patbond-common -am install` 可通过。
- 文档中 Maven 版本与 wrapper 配置一致。
- **依赖**:无。
- **规模**S
#### T0-2 Flutter/Dart 版本锁定
- **仓库**patbond-flutter
- **描述**:提供 Flutter SDK 版本固定方案(如 FVM 或等效工具),版本满足 `pubspec.yaml` 约束,并写入仓库说明。
- **验收标准**
- 仓库内有明确的版本锁定文件与使用说明。
- 新机器按说明可 `flutter pub get && flutter analyze` 通过。
- **依赖**:无。
- **规模**S
#### T0-3 可提交的默认配置与环境变量注入
- **仓库**patbond-api
- **描述**:把 `application.yml.sample` 替换为可直接提交的 `application.yml` 默认配置;数据库、Nacos 等敏感/环境相关值全部通过环境变量注入,采用第 5.3 节变量名(`PATBOND_DB_URL``PATBOND_DB_USERNAME``PATBOND_DB_PASSWORD``NACOS_SERVER_ADDR`)。修复已知风险 5("干净检出无法按 README 直接启动服务")。
- **验收标准**
- 干净检出 + 设置环境变量即可启动 `patbond-user`8082)与 `patbond-auth`8081),无需手工复制 sample。
- 仓库中无任何密码、token、密钥明文(第 5.1 节红线)。
- **依赖**:无(与 T0-4 联调)。
- **规模**M
#### T0-4 本地基础设施编排(PostgreSQL + Nacos
- **仓库**patbond-api(编排文件),patbond-doc(启动文档)
- **描述**:提供本地一键编排(PostgreSQL 16、Nacos);RabbitMQ 按文档"在异步任务阶段启用",本迭代不加入。数据库容器初始化仅建空库,结构由 Flyway 负责(T1)。
- **验收标准**
- 一条命令拉起 PostgreSQL 16 与 Nacos,端口与 T0-3 的默认环境变量匹配。
- 文档说明启动、停止、重置数据的方式。
- **依赖**:无。
- **规模**M
#### T0-5 契约规范冻结文档
- **仓库**patbond-doc
- **描述**:把第 4.3、6.1 节规则固化为规范文档并纳入 `mkdocs.yml` 导航:UUID(应用层 UUIDv7)、`timestamptz` + ISO 8601、金额整数分、统一错误响应(HTTP 状态码 + 稳定业务错误码)、cursor 分页、`Idempotency-Key``version` 乐观锁、日志脱敏。
- **验收标准**
- 规范文档评审通过,`mkdocs build --strict` 通过。
- T3/T4/T6 的实现均引用此文档而非各自发明。
- **依赖**:无;是 T3、T6a 的前置。
- **规模**M
#### T0-6 建立 CI 最低门禁
- **仓库**patbond-api、patbond-flutter、patbond-doc(三条流水线)
- **描述**:按第 9 节 CI 最低门禁配置:API `mvn clean test`Flutter `dart format --set-exit-if-changed` + `flutter analyze` + `flutter test`;文档 `mkdocs build --strict`。加入基本代码检查。数据库迁移在全新 PostgreSQL 16 实例执行一次的校验,在 T1 合入后追加到 API 流水线。
- **验收标准**
- 三仓 PR 均触发对应门禁,当前主干全绿。
- 门禁失败可阻止合入。
- **依赖**T0-1API 用 wrapper 构建)、T0-2Flutter 版本确定)。
- **规模**M
### B 组:登录纵切(9 个工单,对应第 8 节 8 项任务,任务 6 拆为两单)
#### T1 identity/media Flyway baseline 与种子数据分离(第 8 节任务 1)
- **仓库**patbond-api(迁移脚本),patbond-doc(迁移说明)
- **描述**:从 `patbond-doc/docs/database/patbond_postgresql.sql` 提取 `identity``media` 两个 schema 的结构,转为 Flyway 版本化迁移;开发种子数据独立为不进生产的脚本。遵守第 4.3 节:已导入的本地库先备份,优先重建开发库或核对 checksum 后 baseline"禁止直接重复执行 bootstrap"。
- **验收标准**
- 全新 PostgreSQL 16 实例上 Flyway 迁移一次成功,`identity``media` 表结构与 bootstrap SQL 一致。
- 种子数据脚本与结构迁移分离,且不会进入正式环境。
- 本地既有库的接入路径(重建或 baseline)写入文档。
- **依赖**T0-4(本地 PostgreSQL 可用)。
- **规模**M
#### T2 patbond-user 接入 PostgreSQLUUID 用户持久化(第 8 节任务 2)
- **仓库**patbond-api
- **描述**:为 `patbond-user` 增加数据源与 Repository,把内存用户迁移到 `identity.users``identity.user_credentials`;用户 ID 由 `Long` 改为 UUID(应用层 UUIDv7 优先,`gen_random_uuid()` 兜底),消除已知风险 1。数据库账号使用最小权限(第 5.3 节)。
- **验收标准**
- 注册的用户写入 PostgreSQL,重启服务后数据不丢失。
- 对外 API 中用户 ID 为 UUID 字符串。
- 现有注册/登录/查询/密码校验接口在新存储上行为正确。
- **依赖**T1、T0-3。
- **规模**L
#### T3 统一异常响应与数据库一致校验(第 8 节任务 3)
- **仓库**patbond-api
- **描述**:实现统一错误响应(正确 HTTP 状态码 + 稳定业务错误码,禁止只返回异常文本,见 6.1 节);用户名、手机号唯一性校验以数据库约束为准,应用层校验与数据库约束一致,并发重复注册返回明确错误而非 500。
- **验收标准**
- 参数错误、重复用户名/手机号、资源不存在均返回规范错误体。
- 并发重复注册场景有测试覆盖,无脏数据。
- 日志不记录密码、token、手机号全文。
- **依赖**T2、T0-5。
- **规模**M
#### T4 可校验 access token 与 refresh session(第 8 节任务 4
- **仓库**patbond-api
- **描述**:将随机字符串 token 替换为可校验的 access token;实现 refresh token 轮换、退出与会话撤销(会话落 `identity` 相关表);补齐 `POST /api/v1/auth/refresh``POST /api/v1/auth/logout`;消除已知风险 2。`/internal/**` 的服务间访问控制(风险 3)按 M1 范围至少加基础保护。
- **验收标准**
- access token 可离线/在线校验,过期后用 refresh token 可换新。
- refresh token 轮换后旧 token 立即失效;退出后 refresh token 不可再次使用(M1 验收标准)。
- `/internal/users/**` 不可被无凭据外部调用直接访问。
- **依赖**:T2;token 有效期与多设备策略需产品拍板(见决策 D3),未拍板前按建议默认值实现并做成配置项。
- **规模**L
#### T5 认证链路集成测试(第 8 节任务 5)
- **仓库**patbond-api
- **描述**:使用真实 PostgreSQL/Testcontainers 为注册、登录、刷新、退出、鉴权建立集成测试(第 9 节),覆盖成功、参数错误、凭据错误、token 过期、已撤销 token 复用、无权限访问等路径。
- **验收标准**
- 上述场景全部有自动化断言并纳入 `mvn clean test`
- CI(T0-6)中稳定通过,包含迁移在全新实例执行一次的校验。
- **依赖**T2、T3、T4、T0-6。
- **规模**M
#### T6a 建立 OpenAPI 契约(第 8 节任务 6 前半)
- **仓库**patbond-doc(契约文档),patbond-api(保证实现一致)
- **描述**:为第 6.2 节第一批中本迭代涉及的接口编写 OpenAPI:`/api/v1/auth/register``/auth/login``/auth/refresh``/auth/logout``GET /api/v1/me`。统一 `/api/v1` 前缀、camelCase、UUID 字符串、规范错误体;加入契约测试验证实际响应与文档一致。
- **验收标准**
- OpenAPI 文件评审通过并纳入文档站导航。
- 契约测试在 CI 中验证以上接口响应与契约一致。
- **依赖**T0-5;接口最终形态受 T3/T4 影响(可先起草,随实现收敛)。登录方式字段依赖决策 D5。
- **规模**M
#### T6b Flutter API Client(第 8 节任务 6 后半)
- **仓库**patbond-flutter
- **描述**:依据 T6a 的 OpenAPI 生成或手写 API Client 与 DTO,建立第 4.2 节分层(Repository -> API Client),实现统一错误码解析。
- **验收标准**
- Client 覆盖 T6a 全部接口,DTO 映射有单元测试。
- 错误响应能映射为客户端可处理的类型化错误。
- **依赖**T6a。
- **规模**M
#### T7 Flutter 登录页、安全 token 存储与登录态恢复(第 8 节任务 7)
- **仓库**patbond-flutter
- **描述**:新增登录/注册页;access/refresh token 仅存安全存储(不得写入普通 `SharedPreferences`,第 4.2 节);实现鉴权拦截(自动附带 token、401 时刷新重试)与应用启动登录态恢复;`auth` 状态从 `AppState` 拆出独立 feature 状态。
- **验收标准**
- 可完成真实注册与登录;杀进程重开后登录态恢复;token 过期自动刷新。
- 登录页覆盖 loading、error、retry 状态(第 9 节要求)。
- 有登录流程 Widget 测试与 Repository/状态单元测试。
- **依赖**:T6b;端到端联调依赖 T4。
- **规模**L
#### T8 端到端用例:注册 → 登录 → 获取当前用户 → 退出(第 8 节任务 8)
- **仓库**patbond-flutterE2E 用例),patbond-api、patbond-doc(联调环境与文档)
- **描述**:建立一条贯通真实后端与数据库的端到端自动化用例:注册 → 登录 → `GET /api/v1/me` → 退出,退出后受保护接口访问失败。补充"新机器按文档从零跑通该用例"的操作说明,作为 M0 验收的最终证明。
- **验收标准**
- 用例可在本地编排环境(T0-4)下自动执行并通过。
- 新成员仅凭仓库文档可复现(M0 验收标准)。
- **依赖**T4、T5、T7。
- **规模**M
---
## 3. 关键路径与并行分组
### 关键路径(后端主线 → 客户端联调 → E2E)
```text
T0-4 编排 → T1 Flyway baseline → T2 用户持久化(L) → T4 token/会话(L) → T7 Flutter 登录联调(L) → T8 E2E
```
T2、T4、T7 三个 L 工单串在关键路径上,是迭代周期的决定因素。压缩手段:T4 的 token 方案设计、T7 的登录页 UI 与安全存储封装都可在前置工单完成前先行开工(见下)。
### 可并行任务组
| 组 | 工单 | 说明 |
| --- | --- | --- |
| P1 工程基线(迭代第一周全部并行) | T0-1、T0-2、T0-3、T0-4、T0-5 | 互相无依赖,可 3-4 人同时开工;完成后 T0-6 收口 |
| P2 后端主线(串行) | T1 → T2 → T4 | 关键路径,建议由同一名后端主力负责保持连续性 |
| P3 后端旁路 | T3、T5 | T3 与 T4 都只依赖 T2,可两人并行;T5 随 T3/T4 完成滚动补齐 |
| P4 契约与客户端 | T6a → T6b → T7 | T6a 可在 T0-5 后立即起草(与 T2 并行);T7 的 UI/安全存储部分可与后端并行,仅最终联调等 T4 |
| P5 收口 | T8 | 全链路就绪后执行 |
最小人力建议:1 名后端主力(P2)+ 1 名后端(P3 与部分 P1+ 1 名 Flutter(P4)即可维持关键路径不空转。
---
## 4. 开工前待确认决策清单(源自文档第 12 节)
文档明确:"上述事项未确认前,可以完成 M0 和身份持久化,但不应并行扩展所有业务模块。" 即本迭代大部分工单不被阻塞,但 D3、D5 直接影响本迭代实现,需优先拍板。
| # | 决策事项 | 对本迭代的影响 | 建议默认选项 |
| --- | --- | --- | --- |
| D1 | 第一版 MVP 是"注册登录 + 宠物档案"还是必须包含社区发布 | 不阻塞本迭代,决定第二、三迭代排期 | 注册登录 + 宠物档案(M1+M2),社区后置到 M3;与文档迭代顺序一致 |
| D2 | 后端多服务部署 vs 模块化单体 | 影响 T0-3/T0-4 的配置与编排复杂度、是否长期保留 Nacos | 先模块化单体:保留 Maven 模块边界与 schema 所有权,单进程/同机部署降低早期运维成本;当前 auth/user 双服务与 Nacos 维持现状不扩散,待拍板后再收敛 |
| D3 | access/refresh token 有效期与多设备登录策略 | **直接阻塞 T4 定稿**(可按默认值先实现为配置项) | access 15 分钟;refresh 30 天且每次刷新轮换;允许多设备并行会话,退出仅撤销当前会话 |
| D4 | 对象存储、AI 模型、天气、地图供应商 | 本迭代不阻塞(T1 仅建 media 表结构);对象存储需在 M1 媒体上传前确定 | 本迭代不定 AI/天气/地图;对象存储在下迭代开始前选定一家 S3 兼容服务 |
| D5 | 是否支持手机号登录、短信验证码、第三方登录 | **影响 T3 校验字段、T6a 注册/登录契约、T7 登录页表单** | 首版仅账号(用户名/手机号作为标识)+ 密码,不做短信验证码与第三方登录;数据模型预留凭证类型扩展 |
| D6 | 预约首版是否包含支付、退款和服务商后台 | 不阻塞本迭代;影响 M5 范围与数据库是否需补支付域 | 首版不含支付与服务商后台,预约仅到"确认/完成"状态机;文档已注明当前数据库不含支付域 |
| D7 | 首发平台:Android/iOS,还是含 Web/桌面 | 影响 T7/T8 的测试矩阵与后续 Golden 测试宽度 | 首发仅 Android + iOSWeb/桌面不在验收矩阵 |
另提请产品/技术负责人注意文档第 11 节风险 8(Spring Boot 2.7 已进入旧技术代际,是先交付 MVP 还是先升级 Boot 3)——它不在第 12 节清单中,但会影响 T4 选型的依赖库,建议与 D2 一并讨论。PM 建议:先按现有 Boot 2.7 交付本迭代,升级作为独立技术专项排期,避免纵切迭代被大版本升级绑架。
---
## 5. 质量要求(对全部工单生效)
- 遵守文档第 10 节 Definition of Done:不依赖 Demo 常量或仅存于进程内的数据;权限、校验、幂等、并发已处理;文档同步更新;干净环境可复现。
- 不提交任何密码、token、密钥(第 5.1 节)。
- 日志不得记录密码、token、手机号全文(第 6.1 节)。
- 新增文档必须同步更新 `mkdocs.yml` 导航(第 1 节)。
- 本迭代不实现社区、AI、预约、宠物健康的任何接口或页面改造;发现范围外需求一律记入 backlog。
## 6. 工单统计
- 工单总数:**15**(M0 工程基线 6 个 + 登录纵切 9 个)
- 规模分布:S × 2、M × 10、L × 3
- 关键路径长度:6 个工单(T0-4 → T1 → T2 → T4 → T7 → T8),其中 3 个 L
@@ -0,0 +1,178 @@
# Patbond 第一迭代技术评估(Dev
> 作者:Senior Developer
> 日期:2026-09-03
> 范围:第一迭代「真实登录纵切」(development-plan.md 第 8 节 8 项任务)的实现方案、阻塞点与技术选型建议
> 依据:development-plan.md(重点第 4、5、6、8、11 节)、patbond-api 三模块源码、patbond_postgresql.sql、patbond-flutter/lib 与 pubspec.yaml
---
## 1. 现状盘点(实际读码结论)
### 1.1 patbond-api
| 项 | 现状 | 关键文件 |
| --- | --- | --- |
| 框架 | Spring Boot 2.7.18 + Spring Cloud 2021.0.9 + Spring Cloud Alibaba 2021.0.6.0Java 17 | `patbond-api/pom.xml` |
| 用户存储 | `ConcurrentHashMap` 内存表,`AtomicLong` 自增 Long ID,重启即失;无任何 JDBC/JPA/Flyway 依赖 | `patbond-user/.../service/UserService.java` |
| 密码 | BCrypt`spring-security-crypto`),这是唯一可直接保留的安全实现 | 同上 |
| Token | `UUID.randomUUID()` 去掉横线的随机串,服务端不存储、不可验证、不可刷新、不可撤销;响应无 refreshToken`expiresAt``LocalDateTime`(无时区,违反第 4.3/6.1 节 ISO 8601 约定) | `patbond-auth/.../service/AuthService.java``dto/AuthTokenResponse.java` |
| 服务间调用 | auth 经 Feign + Nacos 发现调用 user 的 `/internal/users``/internal/users/verify-password`,**无任何服务间认证**(风险 11.3) | `patbond-auth/.../client/UserClient.java``patbond-user/.../controller/UserController.java` |
| 路由 | `/auth/*``/internal/users/*`,无 `/api/v1` 前缀(不符第 6 节) | 两个 Controller |
| 错误处理 | 直接抛 `ResponseStatusException`,返回 Spring 默认错误体;auth 的 `requireData()` 把 user 服务的 401/409 一律折叠成 400 | `AuthService.java:58-64` |
| 配置 | 仅 `application.yml.sample`,干净检出无法启动(风险 11.5);Nacos 是硬前置(`spring.config.import: nacos:`,靠 `optional:``fail-fast:false` 缓解) | 两个 `application.yml.sample` |
| common 模块 | 携带 `starter-web``starter-amqp``openfeign``loadbalancer``nacos-discovery``nacos-config`、hutool 全量传递依赖——user 模块被动引入 RabbitMQ 和 Feign**直接违反第 4.1 节约束** | `patbond-common/pom.xml` |
| 契约 DTO | `UserProfile.id``VerifyPasswordResponse.userId``AuthTokenResponse.userId` 均为 `Long``UserController` 路径参数 `Long id`;时间均为 `LocalDateTime` | `patbond-common/.../user/*.java` |
| 校验 | DTO 上 username 3-32、password 6-64 与 DB `ck_users_username` 一致;**phone 仅限长 ≤20**,而 DB 要求 E.164`^\+[1-9][0-9]{7,14}$`varchar(16))——现有校验通过的手机号会被数据库拒绝 | `RegisterRequest.java``CreateUserRequest.java` vs SQL `ck_users_phone` |
| 测试 | 零测试代码 | — |
### 1.2 patbond-flutter
- `pubspec.yaml` 依赖只有 `cupertino_icons``shared_preferences`^2.5.4),Dart SDK `^3.12.2`。**没有 dio/http、没有 secure storage、没有状态管理库**`AppState` 是手工 `ChangeNotifier`,经构造函数逐层传递)。
- `lib/state/app_state.dart`:单一 God-state,宠物/疫苗/帖子/天气全部 JSON 序列化进 `SharedPreferences`,与第 4.2 节目标架构(feature 拆分 + secure storage)完全不同。
- `lib/models/models.dart`:大量展示型字符串字段——`PostModel.time`"2小时前")、`ServiceProviderModel.distance`"1.2km")、`PetProfile.birthday`/`VaccineItem.date` 为 String(风险 11.4)。第一迭代只需动 auth,但新建的 auth 模型必须按事实字段(`DateTime`/UUID 字符串)建模,不复用这套模式。
- `app.dart` 直接 `home: MainShellPage(...)`,无路由守卫、无登录页;`test/` 仅一个 widget_test。
### 1.3 patbond-doc / 数据库
- `patbond_postgresql.sql`1950 行):7 schema、36 表,事务包裹的一次性 bootstrap;扩展依赖 `pgcrypto``citext``pg_trgm``btree_gist`
- 第一迭代关心的部分:
- `identity.users`uuid PK、`username citext UNIQUE``status``version` 乐观锁、软删;`phone_e164`/`email` 为部分唯一索引(`WHERE status <> 'deleted'`)——**唯一性无法只靠应用层判断,必须靠 DB 约束 + 冲突捕获**。
- `identity.user_credentials`:与 users 1:1,含 `failed_login_count``failure_window_started_at``locked_until`——登录失败限制(M1 要求)的落库结构已备好。
- `identity.auth_sessions`refresh 会话表已完整设计——`token_family_id`(家族撤销/重用检测)、`refresh_token_hash bytea` 且约束 `octet_length=32`(即 **SHA-256 摘要**,不是明文)、`access_token_jti``rotated_at`/`replaced_by_session_id` 轮换链、`revoked_at`。**任务 4 的数据模型不需要设计,照实现即可。**
- 跨 schema 依赖:`identity.users.avatar_asset_id -> media.assets`(文件尾部 ALTER TABLE 补加 FK),`identity.user_addresses/user_preferences -> platform.regions`。**Flyway baseline 必须同时含 platform.regions、identity 全部、media.assets,否则建不起来。**
- 种子数据集中在 1272 行之后(含开发用户、开发会话、演示预约),与结构部分天然可分割。
---
## 2. 必须先解决的阻塞点(按影响排序)
### B1. Long ID vs UUID —— 对外契约级阻塞(风险 11.1)
`Long` 贯穿 `UserProfile``VerifyPasswordResponse``AuthTokenResponse``UserController.getById(Long)` 和整个内存实现。数据库全部是 uuid。这不是重构项而是**契约变更**:OpenAPI(任务 6)和 Flutter 客户端(任务 7)都以最终契约为输入,所以 UUID 切换必须发生在任务 2,且在任何客户端代码开工之前冻结。对外 JSON 一律 UUID 字符串(第 6.1 节)。
### B2. Token 模型整体不可用 + auth_sessions 归属未定(风险 11.2
随机串 token 无法支撑 M1 的任何验收标准(可验证、可刷新、可撤销)。`AuthTokenResponse` 还缺 `refreshToken`/`expiresIn`,时间类型错误。同时有一个**必须先拍板的架构决策**:`identity` schema 的唯一数据所有者是 patbond-user(第 4.1 节),但发 token 的是 patbond-auth——`auth_sessions` 的读写归谁?不定下来任务 4 没法动工。我的建议见 §3 任务 4。
### B3. 干净检出不可启动、不可测试(风险 11.5 + common 依赖污染)
三件事叠加:`application.yml` 只有 sample`spring.config.import` 硬指 Nacos`patbond-common` 把 web/amqp/feign/nacos 塞给所有下游。后果是集成测试(任务 5)在 CI 里根本跑不起来(测试上下文会尝试连 Nacos/RabbitMQ)。需要:提交带环境变量占位的默认 `application.yml`(敏感值走 `PATBOND_DB_*`,第 5.3 节);common 瘦身为纯 DTO/契约(web/validation-api 之外全部下放到用的模块);test profile 关闭 nacos discovery/config、Feign 走静态 URL。
### B4. phone 校验与 DB 约束冲突
现有 `@Size(max=20)` 会放行 `13800138000` 这类值,落库时被 `ck_users_phone`(E.164)拒绝,用户看到的是 500 而不是 400。任务 3 必须统一:要么客户端只收 E.164,要么服务端把 CN 手机号规范化为 `+86...` 再入库。
### B5. bootstrap SQL 与 Flyway 的一次性冲突(风险 11.7)
本地库若已执行过 bootstrap,直接上 Flyway 会 checksum/对象冲突。按第 4.3 节:**优先重建开发库**(成本最低,现阶段无真实数据),备选 baseline 对齐。种子里的开发用户/会话绝不能进 V1 迁移。
---
## 3. 第 8 节 8 项任务逐项实现方案
### 任务 1:从 bootstrap SQL 提取 identity/media 的 Flyway baseline
- **涉及现有文件**`patbond-doc/docs/database/patbond_postgresql.sql`(结构源,50-331 行 + 文件尾部 identity 相关 ALTER TABLE)。
- **新建**
- `patbond-user/src/main/resources/db/migration/V1__baseline_platform_identity_media.sql`extensionspgcrypto/citext/btree_gist)、`platform` schema + `platform.regions``identity` 全部 5 张表及索引、`media.assets``users.avatar_asset_id` FK。pg_trgm/pg_trgm 相关索引不在本迭代范围可不带。
- `db/seed/dev-seed.sql``db/migration-dev/R__dev_seed.sql`:开发种子分离,仅在 `dev` profile 通过 `spring.flyway.locations` 追加(第 4.3 节"结构、种子、校验分离")。
- **库**`flyway-core`。注意 Boot 2.7 默认管理 Flyway 8.5.x,对 PostgreSQL 16 的官方支持要 Flyway 9.21+——需要显式 pin 版本并验证与 2.7 自动配置的兼容性。这是升 Boot 3 的加分项之一(Boot 3.x 默认 Flyway 9/10)。
- **配置**`spring.flyway.schemas=platform,identity,media``createSchemas=true``defaultSchema` 明确指定 history 表位置;数据源用 `PATBOND_DB_URL/USERNAME/PASSWORD` 环境变量注入(第 5.3 节),应用账号最小权限。
- **风险对应**:11.7(种子分离)、11.9(跨 schema FK 保留,MVP 共库,第 4.1 节允许)。
### 任务 2patbond-user 接 PostgreSQL RepositoryUUID 用户持久化
- **涉及现有文件**:重写 `UserService.java`(删除内存表和 `AtomicLong`);`UserController.java``Long id``UUID`);common 的 `UserProfile`/`VerifyPasswordResponse``Long``String` UUID`LocalDateTime``Instant`/`OffsetDateTime`);`patbond-user/pom.xml`
- **新建**`user/domain/UserEntity` + `UserCredentialEntity`(映射 `identity.users``identity.user_credentials`)、`user/repository/``user/config/`(数据源、时区 UTC)。
- **库选型**
- 持久化建议 **Spring Data JDBC**(或退一步 `JdbcTemplate`):schema 是 DB-first 且已冻结(citext、部分唯一索引、timestamptz),不需要 Hibernate 的 DDL 能力,JPA 的 citext/软删/version 映射反而添乱。若团队 JPA 熟练度高也可用 JPA,但必须 `ddl-auto=none`
- UUIDv7 用 `com.fasterxml.uuid:java-uuid-generator``Generators.timeBasedEpochGenerator()`)应用层生成,DB `gen_random_uuid()` 兜底(第 4.3 节原文要求)。
- `postgresql` JDBC driver、HikariCPstarter 自带)。**这些依赖只加在 patbond-user,不进 common。**
- **要点**:用户名唯一性放弃 `containsKey` 预检,改为依赖 citext UNIQUE + 捕获 `DuplicateKeyException` → 409(并发下预检不可靠);写入同事务落 users + user_credentials 两表;`version` 字段随实体带出为后续乐观锁做准备。
- **风险对应**11.1(UUID 统一)、11.5(配套提交默认 application.yml)。
### 任务 3:统一异常响应 + 用户名/手机号数据库一致校验
- **涉及现有文件**`ApiResponse.java`(补错误码语义);两个 DTO 的 phone 校验;`AuthService.requireData()`(废除"一律 400"的折叠逻辑)。
- **新建**
- common`ErrorCode` 枚举(稳定业务码,如 `USER_NAME_TAKEN``INVALID_CREDENTIALS``TOKEN_EXPIRED`)+ 统一错误体约定——common 只放契约类型,符合第 4.1 节。
- 各服务:`GlobalExceptionHandler``@RestControllerAdvice`),映射 `MethodArgumentNotValidException`→400、重复键→409、`ResponseStatusException` 透传、兜底 500 不泄内部信息;auth 端为 Feign 加 `ErrorDecoder`,把 user 服务的错误码和 HTTP 状态原样向客户端传递(第 6.1 节"正确状态码 + 稳定业务码")。
- 日志脱敏:异常日志不落密码/token/手机号全文(第 6.1 节)。
- **校验统一**phone 加 `@Pattern(regexp="^\\+[1-9][0-9]{7,14}$")` 或注册流程规范化 CN 号码为 E.164(需产品确认输入形态,建议后者);username 沿用 3-32 并 trimnickname 1-32。所有约束以 SQL CHECK 为准绳(第 1 节冲突处理顺序第 2 条)。
### 任务 4:可校验 access token + refresh session
- **架构决策(先拍板)**`identity.auth_sessions` 归 patbond-user 所有(它是 identity schema 唯一所有者,第 4.1 节)。**建议:登录/注册/刷新/撤销的会话逻辑全部下沉到 patbond-user**patbond-auth 保留为面向客户端的薄入口(校验参数、编排、签发 JWT)。备选方案是 user 暴露 `/internal/sessions` CRUD 给 auth 编排,但两跳事务边界更碎,MVP 不值得。
- **Token 方案**
- **Access tokenJWT**,建议 `jjwt 0.12.x``nimbus-jose-jwt`(比引入整套 spring-security-oauth2 轻)。claims`sub`=user UUID、`jti`(回写 `auth_sessions.access_token_jti`)、`iat/exp`15-30 分钟)、`sid`=session id。签名算法:MVP 单签发方可用 HS256(密钥走环境变量),但 M2 起 pet/community 模块都要本地验签,**建议直接上 RS256/EdDSA 非对称**,公钥随 common 契约或 JWKS 端点分发,避免以后共享密钥扩散。
- **Refresh token:不透明随机串**256-bit `SecureRandom`base64url),服务端只存 SHA-256 摘要进 `refresh_token_hash`(正好满足 `octet_length=32` 约束)。轮换:每次 refresh 新建 session 行、旧行写 `revoked_at + rotated_at + replaced_by_session_id`;同 `token_family_id` 检测重用(旧 refresh 被再次使用 → 撤销整个家族),schema 已为此建好索引。
- 有效期(access 15-30min / refresh 14-30 天 / 多设备策略)是第 12 节待确认事项,实现上做成配置项,不写死。
- **接口变更**:统一 `/api/v1/auth/{register,login,refresh,logout}`(第 6.2 节);`AuthTokenResponse` 增加 `refreshToken``expiresIn`(秒),`userId` 改 UUID 字符串,时间字段 ISO 8601 UTC。
- **/internal 保护(风险 11.3)**:第一迭代最小方案——环境变量注入的静态 service token`/internal/**``OncePerRequestFilter` 校验 + Feign `RequestInterceptor` 注入;网关/端口层面不对外暴露 internal 路由。留 ADR 记录后续换 mTLS 或 token exchange。
- **登录失败限制(M1 要求)**:verify 失败时原子更新 `failed_login_count/failure_window_started_at`,超阈值写 `locked_until`,命中返回 423/固定业务码——列全在 `user_credentials` 里。
- **风险对应**11.2、11.3。
### 任务 5:注册/登录/刷新/退出/鉴权集成测试
- **库**`spring-boot-starter-test` + **Testcontainers**`org.testcontainers:postgresql` 1.19+)。注意 Boot 2.7 没有 `@ServiceConnection`3.1+ 特性),用 `@DynamicPropertySource` 注入容器 URL。
- **前置**B3 必须先解决——test profile 关闭 nacos`spring.cloud.nacos.discovery.enabled=false` 等)、Feign 改静态 URL 或 mock,否则 `@SpringBootTest` 起不来。
- **分层**
- patbond-userFlyway 迁移可在空库执行 + Repository 落库/唯一冲突/失败锁定测试(真实 PG 容器,第 9 节要求)。
- patbond-authJWT 签发/过期/篡改验证单测;Controller 层 mock UserClient。
- 纵切集成:register → login → 携带 token 访问受保护端点 → refresh(旧 refresh 复用被拒)→ logoutsession 撤销后 refresh 不可用)——对应 M1 全部验收标准。
- **覆盖门禁**:每接口至少成功/参数错/401/409/重放五类(第 9 节)。
### 任务 6OpenAPI + Flutter API Client
- **建议契约先行(design-first**:手写 `patbond-doc/docs/api/openapi.yaml`OpenAPI 3.0),只含第一批 auth/me 端点,团队评审后冻结,再实现两端。代码生成文档的备选是 springdoc——注意 Boot 2.7 只能用 springdoc 1.7.x2.x 需要 Boot 3),又一个升级加分项。
- **Flutter 客户端**:端点只有 5-6 个,**建议手写 dio client + 手写 DTO`json_serializable` 可选)**,不引 openapi-generatordart-dio 生成器的产物风格重、定制成本高,等接口上量再评估)。契约一致性靠任务 5 的契约测试兜底。
- **新建**`lib/core/network/api_client.dart`dio 实例、baseUrl 环境区分、`ApiResponse<T>` 信封解包、错误码映射)。
### 任务 7Flutter 登录页、安全 token 存储、登录态恢复
- **新增依赖**`dio``flutter_secure_storage`(第 4.2 节"access token 仅保存在安全存储";注意 Linux 桌面调试需要 libsecret,团队若在桌面跑 demo 要提前确认)、`provider`(把手工传递的 ChangeNotifier 挂到树上,为 feature 拆分铺路;不建议本迭代就上 riverpod/bloc 全家桶)。
- **新建**
- `lib/features/auth/``login_page.dart``register_page.dart``auth_controller.dart``AuthState`: unknown/unauthenticated/authenticated)。
- `lib/data/repositories/auth_repository.dart``lib/data/dto/`(auth DTO,字段全部事实类型:UUID String、`DateTime`)。
- `lib/core/storage/token_storage.dart`secure storage 读写 access/refresh。
- dio `AuthInterceptor`:注入 Bearer401 时单飞(single-flightrefresh——并发请求排队等同一次刷新,刷新失败清 token 回登录页。
- **改动现有文件**`app.dart` 由 auth 状态决定 `MainShellPage` 还是 `LoginPage`(启动时读 secure storage → 调 `/api/v1/me` 校验 → 失败尝试 refresh);`AppState` 本迭代**不动**其宠物/帖子逻辑,只是不再是唯一状态入口(第 4.2 节的完整拆分留给 M2 逐页替换)。
- `SharedPreferences` 仅保留主题/引导(第 4.2 节),token 绝不落入。
- **风险对应**:11.4(新模型不复制展示字符串反模式)。
### 任务 8:端到端用例(注册 → 登录 → 获取当前用户 → 退出)
- **后端 E2E**(CI 必跑):任务 5 的纵切集成测试天然覆盖,作为门禁。
- **客户端 E2E**`integration_test` 包写一条 happy path(输入注册→进主页→退出回登录页),本地对着 docker-compose 起的后端跑;进 CI 依赖 M0 的编排产物,第一迭代可先手动执行 + 记录在测试文档。
- **前置**:需要一份最小 `docker-compose.yml`PostgreSQL + 两个服务;若采纳下面的建议甚至不需要 Nacos),属于 M0 范畴但被本任务依赖。
---
## 4. Spring Boot 2.7 vs Boot 3:建议
**建议:先升 Boot 3,再写第一迭代的业务代码。** 放在 M0/任务 1 之前,时间盒 1-2 天。
理由:
1. **迁移成本此刻处于历史最低点。** 真实业务代码不到 10 个类,javax→jakarta 只影响几处 validation import;没有任何持久化、安全、测试代码需要迁移。第一迭代恰恰要新写全部这些代码——JWT/资源服务器、Flyway、Testcontainers、springdoc、异常处理——现在按 2.7 写,就是主动制造一批"将来必须重写"的存量。
2. **2.7 与 Spring Cloud 2021 均已 EOL**(2.7.18 是最终版,OSS 安全补丁 2023 年底已停)。对一个要做真实凭证和会话的身份服务,跑在无补丁框架上是不可辩护的(风险 11.8 的答案本身就在第一迭代的安全语境里)。
3. **本迭代的选型在 2.7 上处处降级**Flyway 对 PG16 要手工 pin 9.21+、springdoc 只能停在 1.7、Testcontainers 没有 `@ServiceConnection`、jakarta 生态的新版本库逐个要挑旧 artifact。
4. **唯一的实质阻力是 Spring Cloud Alibaba/Nacos**Boot 3 需要 SCA 2022.x/2023.x 配套(可用,但要一次性把三个 BOM 同步升级)。附带建议:MVP 只有两个服务、部署拓扑固定,**可以评估干脆移除 Nacos**Feign 用静态 URL + 环境变量——同时消掉 B3 里配置导入和测试上下文两个痛点;等模块数量上来再引入注册中心也不迟。此项需与团队确认,不阻塞升级本身。
若团队仍决定先交付 MVP 后升级,则必须接受:任务 4/5/6 的产出物在升级时二次返工,且身份服务在无安全补丁的框架上对外——我不推荐。
## 5. 建议实现顺序
```text
0. 决策先行:Boot 3 升级(含是否移除 Nacos)、auth_sessions 归属(建议下沉 patbond-user
1. 工程基线:common 瘦身为纯契约、提交默认 application.ymlenv 注入)、test profile 可离线启动 [解 B3]
2. 任务 1 + 2 + 3Flyway baseline → UUID 持久化 → 统一异常与校验(同 PR 链,先冻结对外契约) [解 B1/B4/B5]
3. 任务 4JWT + refresh 轮换 + /internal 保护 + 登录失败锁定 [解 B2]
4. 任务 5Testcontainers 集成测试(随 2/3/4 增量补,不后置)
5. 任务 6openapi.yaml 评审冻结
6. 任务 7:Flutter 登录纵切(依赖 5 的冻结契约)
7. 任务 8:后端 E2E 进 CI 门禁;Flutter integration_test 本地跑通
```
测试(任务 5)实际应与 2-4 步同 PR 交付,此处单列仅表示依赖关系。
@@ -0,0 +1,147 @@
# Patbond 第一迭代 Reality Check 报告
- 核实人:TestingRealityCheckerReality Checker agent
- 核实日期:2026-09-03
- 核实对象:`patbond-api``patbond-flutter``patbond-doc`(位于 `/home/lx/workspace/patbond/`
- 参照文档:`patbond-doc/docs/development/development-plan.md` 第 2 节(当前基线)、第 11 节(当前已知风险)
- 方法:只读核查(源码阅读、grep、wc、ls、git 只读命令、版本检查),未启动任何服务、未修改任何文件、未运行构建
总体结论:**NEEDS WORK**。文档对现状的描述罕见地诚实、准确(未发现夸大),但本机环境存在两个硬阻塞(无 Nacos、数据库不可访问验证),且默认 JDK 与团队基线不符。当前三仓不构成任何端到端链路,「正式启动第一版」只能理解为"启动开发计划",而非"存在可运行的第一版"。
---
## 声明 1patbond-api 有 6 个接口;用户仅存内存;token 不可验证
**结论:CONFIRMED(三点全部属实)**
**接口共 6 个**,逐一定位:
| # | 接口 | 证据 |
| --- | --- | --- |
| 1 | `POST /auth/register` | `patbond-api/patbond-auth/src/main/java/com/patbond/patbond/auth/controller/AuthController.java:24-27` |
| 2 | `POST /auth/login` | 同上文件 `:29-32` |
| 3 | `POST /internal/users` | `patbond-api/patbond-user/src/main/java/com/patbond/patbond/user/controller/UserController.java:27-30` |
| 4 | `POST /internal/users/verify-password` | 同上文件 `:32-35` |
| 5 | `GET /internal/users/{id}` | 同上文件 `:37-40` |
| 6 | `GET /internal/users/by-username/{username}` | 同上文件 `:42-45` |
全仓库仅这两个 Controller,无其他 `@RestController`
**用户仅存内存**`patbond-user/.../service/UserService.java:21-23`
```java
private final AtomicLong idGenerator = new AtomicLong(1);
private final Map<Long, UserRecord> usersById = new ConcurrentHashMap<>();
private final Map<String, UserRecord> usersByUsername = new ConcurrentHashMap<>();
```
无任何 Repository、DataSource、JDBC/JPA 依赖或 PostgreSQL 连接配置。服务重启即丢失全部用户。密码确实用 BCrypt 加密(`UserService.java:24,35`),这是原型中唯一像样的安全措施。
**token 不可验证**`patbond-auth/.../service/AuthService.java:47-56`
```java
private AuthTokenResponse buildToken(Long userId, String username, String nickname) {
return new AuthTokenResponse(
"Bearer",
UUID.randomUUID().toString().replace("-", ""),
LocalDateTime.now().plusHours(2),
...
```
token 是随机 UUID 字符串,生成后不存储、无签名、无校验端点、无 refresh/logout 接口。所谓"2 小时过期"只是响应里的一个展示字段,服务端无法执行。文档风险 #2 属实。
**连带核实**:风险 #1API 用 `Long` 用户 ID)属实——`UserController.java:38` `@PathVariable Long id`;风险 #3`/internal/users/**` 无访问控制)属实——UserController 无任何鉴权注解,全仓库无 Security 配置类、无 Filter/Interceptor(源码共 15 个 Java 文件,逐一核对)。
---
## 声明 2patbond-flutter 无网络层、无登录页;测试仅一个导航冒烟测试
**结论:CONFIRMED**
**无网络层**`patbond-flutter/pubspec.yaml` 依赖仅 `cupertino_icons: ^1.0.8``shared_preferences: ^2.5.4`,没有 `http``dio` 或任何网络包。`grep -rniE "\bhttp\b|dio|HttpClient|Uri\.parse|login" lib` 在 14 个 Dart 文件中零命中(唯一命中是 `pets_page.dart:652``radio_button_unchecked` 图标名,属误匹配)。
**无登录页**`lib/` 全部文件为 `app/``core/theme/``data/demo_data.dart``features/{create,home,main,pets,post,profile,services}``models/``state/``widgets/`——没有任何 auth/login feature。数据全部来自 `lib/data/demo_data.dart`
**测试仅一个**`test/` 目录只有 `widget_test.dart` 一个文件,内含一个 `testWidgets('Patbond renders the main navigation', ...)`,断言五个 Tab 文案和写死的演示文案("北京 · 朝阳区"、"28°C 晴")。这正是文档风险 #6 所述的"一个导航冒烟测试"。
---
## 声明 3patbond_postgresql.sql 包含 7 个 schema、36 张表
**结论:CONFIRMED**
文件:`/home/lx/workspace/patbond/patbond-doc/docs/database/patbond_postgresql.sql`1950 行,91 KB)。
- `grep -icE "^\s*CREATE SCHEMA"` = 7`platform``identity``media``pet_health``community``creation``marketplace` —— 与文档第 3 节的 schema 表完全一致。
- `grep -icE "^\s*CREATE TABLE"` = **36**,分布:identity 5、media 1、pet_health 9、creation 3、community 8、marketplace 7、platform 3。
**注意**:文件在磁盘上属实,但"已导入本地数据库"这半句无法验证(见声明 5 的 PostgreSQL 项)。
---
## 声明 4application.yml 只有 .sample,干净检出无法直接启动
**结论:CONFIRMED——且发现一个文档未提的隐患**
- `patbond-auth/src/main/resources/``patbond-user/src/main/resources/` 各自只有 `application.yml.sample`,无 `application.yml``ls -la` 核实)。
- `patbond-api/.gitignore` 明确忽略 `patbond-*/src/main/resources/application.yml` 并保留 `.sample``git status` 工作区干净。因此干净检出后 Spring Boot 无配置文件可读,无法直接启动。风险 #5 属实。
- `Readme.md` 只写了 `mvn compile`,完全没提"复制 sample"这一步,佐证"无法按 README 直接启动"。
**隐患(文档未提)**:本地 `target/classes/` 里残留着**旧版真实配置**的编译产物:
- `patbond-user/target/classes/application.yml``server-addr: "${NACOS_SERVER_ADDR:patbond.cn:8848}"`
- `patbond-auth/target/classes/application.yml`**硬编码** `server-addr: http://patbond.cn:8848/nacos`,连环境变量覆盖都没有
这两份与 `.sample`(默认 `127.0.0.1:8848`)内容不一致(diff 核实)。若有人在不清理的情况下直接跑旧产物,服务会去连外部主机 `patbond.cn:8848`。target/ 已被 gitignore,不影响干净检出,但本机开工前应清理。
---
## 声明 5:环境检查(JDK 17 / Maven / Flutter / PostgreSQL / Nacos
**结论:PARTIAL——五项中两项有问题,一项无法验证**
| 组件 | 要求(文档 5.1) | 实际 | 判定 |
| --- | --- | --- | --- |
| JDK | 17"不要使用更高版本代替基线" | **默认 JDK 26**`java -version` → openjdk 26.0.2.1);java-17-openjdk 已安装但非默认(`archlinux-java status`) | PARTIAL:可用但需手动切换/设 JAVA_HOME |
| Maven | 3.9+ | 3.9.16(但运行在 Java 26 上,`mvn -v` 显示 runtime: java-26-openjdk | 满足,注意 JDK 绑定 |
| Flutter | 满足 pubspec `sdk: ^3.12.2` | Flutter 3.44.6 stableDart 3.12.2 | 满足 |
| PostgreSQL | 16+"现有本地库作为开发数据源" | 服务端 18.6 正在运行(`pgrep``/usr/bin/postgres -D /var/lib/postgres/data`psql/pg_ctl 18.6 | 版本满足;**但当前 OS 用户 `lx` 无数据库角色**`psql -ltq``FATAL: role "lx" does not exist`),无法验证 patbond 库和 7 个 schema 是否真的已导入。"已导入本地数据库"一说 **UNVERIFIED** |
| Nacos | 必需(auth 经 Nacos 发现 user | **完全缺失**:无二进制(`command -v nacos` 空)、无 `/opt/nacos`、无 systemd 单元、无运行进程 | **FAILED——硬阻塞** |
Nacos 缺失的影响是致命的:`UserClient.java:12``@FeignClient(name = "patbond-user")`,auth 必须经服务发现才能调用 user。没有 Nacos,连现有的登录原型都无法在本机端到端跑通。
---
## 声明 6:文档未提、但与「可开工」相悖的其他事实
1. **patbond-api 零测试**(风险 #6 说了一半):`find` 全仓库不存在任何 `src/test` 目录、任何 `*Test*.java`。不是"测试少",是一个测试都没有。
2. **mkdocs 未安装**`mkdocs: 未找到命令`。文档第 9 节把 `mkdocs build --strict` 列为 CI 最低门禁,本机现在跑不了。同时 `mkdocs.yml` 导航只挂了 `index.md``development-plan.md``database/patbond_postgresql.sql` 不在导航中——`docs/` 实际只有 3 个文件,第 1 节列出的 api/architecture/testing/operations 目录均不存在(文档自己声明"出现对应文档时创建",一致,但意味着 OpenAPI 契约为零,第 6 节的契约还全是"建议")。
3. **接口路径与规范不符**:现有接口是 `/auth/register`,文档 6.2 要求 `/api/v1/auth/register`——第一迭代要么改路径要么改文档,属于开工即遇的契约决策。
4. **patbond-common 强制传染依赖**(文档 4.1 提出原则但现状违反):`patbond-common/pom.xml:23-47` 直接依赖 `spring-boot-starter-amqp`RabbitMQ)、`openfeign``loadbalancer``nacos-discovery/config`。本机没有 RabbitMQ,README 却把它列进技术栈;所有模块被动拖入这些依赖。
5. **patbond-flutter 工作区不干净**`git status` 显示 `README.md` 有未提交修改(在运行步骤中加了一行 `flutter clean`)。"正式启动"时点上仓库状态未固化。
6. **target/ 残留指向外部主机 patbond.cn 的旧配置**(详见声明 4)——auth 那份是硬编码,无环境变量兜底。
7. **技术代际问题当场可见**:父 POM 锁定 Spring Boot 2.7.18 / Spring Cloud 2021.0.9`pom.xml`),而本机默认 JDK 26——Boot 2.7 在 JDK 26 上编译运行风险很高,风险 #8 的"先升级还是先交付"不是远虑,是第一次 `mvn compile` 就会撞上的问题(本次核查按约定未运行构建验证)。
---
## 最终判定
| 项 | 判定 |
| --- | --- |
| 文档第 2 节「当前基线」 | 准确,无夸大 |
| 文档第 11 节「已知风险」#1-#6 | 逐条核实属实 |
| 声明 1API 6 接口/内存用户/假 token | CONFIRMED |
| 声明 2(Flutter 无网络层/登录页,单测试) | CONFIRMED |
| 声明 37 schema / 36 表) | CONFIRMED(导入状态 UNVERIFIED |
| 声明 4(配置只有 sample | CONFIRMED |
| 声明 5(环境) | PARTIALMaven/Flutter 就绪;JDK 17 需切换;PostgreSQL 在跑但当前用户无访问角色;**Nacos 缺失** |
| 能否立即开工 | **NEEDS WORK** |
**开工前必须解决(按阻塞程度排序)**
1. 安装并配置本地 Nacos(否则现有原型都无法端到端运行)。
2. 为当前用户建立 PostgreSQL 角色/库访问,验证 patbond 库 7 个 schema 是否真的已导入。
3. 将构建 JDK 切换/固定为 17java-17-openjdk 已在 `/usr/lib/jvm/`,配 JAVA_HOME 或 Maven toolchain)。
4. 清理 `patbond-api` 各模块 `target/`,消除指向 `patbond.cn:8848` 的旧配置产物。
5. 安装 mkdocs(否则文档门禁不可执行)。
6. 提交或还原 `patbond-flutter/README.md` 的未提交修改,固化起点。
@@ -0,0 +1,346 @@
# Patbond 第一迭代 · 登录/注册 UI 设计规范
> 作者:UI Designer
> 日期:2026-09-03
> 迭代:Iteration 1「真实登录纵切」(development-plan.md §7 M1、§8
> 素材来源:`AI宠物_iOS_UI设计稿.html`(视觉语言)、`patbond-flutter/lib/`(已实现主题与组件)、`patbond-doc/docs/development/development-plan.md`(§6.2 接口、§7 M1、§9/§10 质量门禁)
---
## 0. 关键前提:两套视觉语言的分歧与决策
现有素材存在一个必须先决策的分歧:
| 来源 | 主色 | 底色 | 字体 | 定位 |
| --- | --- | --- | --- | --- |
| HTML 设计稿(正式设计交付物) | 珊瑚橙 `#FF6F4C` 暖色系 | 奶油色 `#FFF7ED` | Baloo 2 标题 + Inter 正文 | 品牌方向 |
| Flutter `app_theme.dart`(当前实现) | 靛蓝 `#4F46E5` | 冷灰 `#F8FAFC` | 系统字体 + CJK fallback | 脚手架占位 |
**决策:以 HTML 设计稿的暖色系为品牌正典(canonical)。** 理由:设计稿是明确的品牌交付物,宠物社区产品的暖色调是刻意的情感设计;而 Flutter 的靛蓝主题是典型的模板默认色。登录页是用户接触产品的第一屏,应当承载品牌。
**落地策略(降低迁移风险):** 本规范全部使用语义 token 命名(`primary``surface``ink`…),与 `AppColors` 现有字段一一对应。主题集中在 `app_theme.dart` 单文件,重映射色值即可全局切换。若团队决定第一迭代不动主题,本规范的布局、组件、状态定义在靛蓝主题下同样成立,仅色值不同——两种情况都不需要改登录页代码。
---
## 1. 现有设计系统提炼
### 1.1 色板(语义 token → 暖色正典值 / 现有 Flutter 值)
| Token | 暖色正典(设计稿) | 现有 Flutter | 用途 |
| --- | --- | --- | --- |
| `primary` | `#FF6F4C` 珊瑚橙 | `#4F46E5` | 品牌色、图标、装饰、渐变起点 |
| `primaryStrong` | `#D6431A`(新增,加深珊瑚) | — | 实心按钮填充、可点击文字链接。白字对比度约 4.5:1,满足 WCAG AA`#FF6F4C` 白字仅 2.75:1,不得用于承载文字的实心填充 |
| `primaryDark` | `#7A2E12` coral-dark | — | 浅色底上的强调文字、按钮反白替代 |
| `accent` | `#FFB648` amber | — | 渐变终点、徽章、会员/促销 CTA |
| `accentDark` | `#7A4B0A` amber-dark | — | amber 底上的文字 |
| `canvas` | `#FFF7ED` bg-cream | `#F8FAFC` | 页面背景 |
| `surface` | `#FFFFFF` | `#FFFFFF` | 卡片、输入框背景 |
| `surfaceTint` | `#FFE8D6` peach | `#E0E7FF` | 图标底、占位块、选中指示 |
| `ink` | `#3E2A1F` | `#0F172A` | 主文字 |
| `muted` | `#9C8977` | `#64748B` | 次级文字、占位符 |
| `border` | `#F0DCC8` | `#E2E8F0` | 描边、分隔线 |
| `success` | `#7FA88A` sage(文字用 `#3F5744`,底用 `#E8F0E8` | `#10B981` | 成功提示 |
| `warning` | `#F59E0B` | `#F59E0B` | 警告 |
| `error` | `#D0342C`(新增;设计稿与主题均缺失) | — | 校验错误文字、错误描边、失败提示。白底上约 5.4:1,AA 达标 |
| `brandGradient` | `linear-gradient(135deg, #FF6F4C, #FFB648)` | — | 品牌 hero 区、头像环、装饰性大面积(不承载正文文字) |
### 1.2 字号层级
设计稿是 320px 画框内的缩样(9–19px),不能按像素照搬;Flutter `textTheme` 已是放大到真机的合理映射,作为基准沿用:
| 层级 | 字号/字重 | 对应 | 登录页用途 |
| --- | --- | --- | --- |
| Display(品牌字标) | 32 / w700,标题字体(Baloo 2 或圆润中文标题体,可后置) | 设计稿 `.logo` 放大 | 登录页 "Patbond" 字标 |
| `headlineSmall` | 22 / w800 | 已有 | 页面标题("欢迎回来"/"创建账号" |
| `titleLarge` | 18 / w800 | 已有 | 分区标题 |
| `titleMedium` | 15 / w700 | 已有 | 按钮文字、表单 label |
| `bodyMedium` | 14 / 1.5 行高 | 已有 | 正文、输入内容(输入框内建议 16 防 iOS 缩放) |
| `bodySmall` | 12 / 1.4 行高,muted 色 | 已有 | 辅助说明、协议文案 |
| Caption | 11 / w700 | TagPill 已有 | 徽章、错误行(错误行用 12) |
### 1.3 圆角
设计稿圆角层级:芯片胶囊 999 > hero 20 > 卡片 1618 > 输入框/小卡 14 > 按钮 1012。Flutter 现值:Card 24、Input 18、SnackBar 16。取两者交集定标准:
| Token | 值 | 用途 |
| --- | --- | --- |
| `radiusSm` | 12 | 小按钮、内嵌 CTA |
| `radiusMd` | 16 | 主按钮、SnackBar、菜单组 |
| `radiusLg` | 18 | 输入框(沿用 `inputDecorationTheme` 现值)、feed 卡 |
| `radiusXl` | 24 | 大卡片(沿用 `cardTheme` 现值) |
| `radiusPill` | 999 | 芯片、徽章 |
### 1.4 间距
基数 4。常用刻度:4 / 8 / 12 / 16 / 24 / 32 / 48。页面水平留白 16(现有页面 `EdgeInsets.fromLTRB(16, 16, 16, 30)` 一致),卡片内边距 1824`SectionCard` 默认 18)。
### 1.5 既有组件风格基线
- **输入框**`inputDecorationTheme` 已定义,直接复用):白底 filled,圆角 18,内边距 H16/V14,默认描边 `border` 1px,聚焦描边 `primary` 1.5px。
- **按钮**:现有页面用 Material 3 `FilledButton` / `OutlinedButton` / `TextButton`,未做全局主题化——本迭代补齐(见 §6)。
- **卡片**:白底、1px 极浅描边、零 elevation(阴影极克制,与设计稿一致)。
- **已有可复用组件**`lib/widgets/common.dart`):`RemoteImage`loading/error 兜底)、`SectionCard``TagPill``EmptyState`
- **加载**`CircularProgressIndicator`(主壳启动、创作页、档案页已用)。
---
## 2. 登录页规范
### 2.1 布局结构
单列居中布局,无 AppBar,无底部 TabBar。页面背景 `canvas`,可在顶部叠加一层由 `surfaceTint` 到透明的极浅径向渐变作氛围(可选装饰,非必需)。
```text
┌──────────────────────────────────────┐
│ SafeArea + SingleChildScrollView │ 键盘弹出时可滚动,防溢出
│ 水平 padding 24 │
│ │
│ ↑ 弹性空间 (flex, min 48) │
│ │
│ [ 品牌区 ] │
│ 🐾 logo 图形 72×72 │ v1 可用 Icons.pets 于
│ Patbond │ brandGradient 圆底(radiusXl)代替
│ 和毛孩子在一起的每一天 │ 字标 32/w700 primary
│ │ slogan 14 muted,居中
│ 间距 48 │
│ │
│ ┌────────────────────────────────┐ │
│ │ 用户名 / 手机号 │ │ 输入框①,高 52
│ └────────────────────────────────┘ │
│ 间距 16 │
│ ┌────────────────────────────────┐ │
│ │ 密码 [👁] │ │ 输入框②,高 52,可见性切换
│ └────────────────────────────────┘ │
│ ⚠ 字段级错误显示在对应输入框下方 │
│ │
│ ┌ 表单级错误横幅(条件显示)───────┐ │ 见 §4.3
│ └────────────────────────────────┘ │
│ 间距 24 │
│ ┌────────────────────────────────┐ │
│ │ 登 录 │ │ 主按钮,高 52,全宽
│ └────────────────────────────────┘ │
│ 间距 16 │
│ 还没有账号? 立即注册 │ 行内文字链接,居中
│ │
│ ↑ 弹性空间 │
│ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ │
│ ┆ [预留区] 其他登录方式 ┆ │ v1 不渲染,见 2.4
│ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ │
│ 登录即代表同意《用户协议》《隐私政策》 │ bodySmall muted,底部 24
└──────────────────────────────────────┘
```
### 2.2 组件清单
| 组件 | 规格 |
| --- | --- |
| 品牌区 | logo 72×72;字标 32/w700 `primary`slogan 14 `muted`;整体居中 |
| 账号输入框 | label "用户名 / 手机号"`keyboardType: text``textInputAction: next``AutofillHints.username`,前缀图标 `person_outline_rounded``muted` 色) |
| 密码输入框 | label "密码"`obscureText` 默认开,后缀 `visibility_off/visibility` 切换按钮(点击目标 ≥44×44),`textInputAction: done`(提交表单),`AutofillHints.password`,前缀图标 `lock_outline_rounded` |
| 主按钮「登录」 | 全宽 × 52 高,圆角 `radiusMd`(16),填充 `primaryStrong`,文字白色 15/w700。状态见 §4.4 |
| 注册入口 | "还没有账号?"`muted`+ "立即注册"`primaryStrong`/w700,点击目标 ≥44 高),push 到注册页 |
| 协议行 | bodySmall,链接词 `primaryStrong`;v1 若协议页未就绪可先不渲染整行 |
### 2.3 表单字段与校验规则
| 字段 | 客户端校验(失焦 + 提交时) | 错误文案 |
| --- | --- | --- |
| 用户名/手机号 | 非空;去首尾空格 | "请输入用户名或手机号" |
| 密码 | 非空 | "请输入密码" |
登录页刻意不做格式强校验(用户名还是手机号由服务端判定),失败统一走服务端错误映射(§4.2)。
### 2.4 预留区(未确认功能,v1 不实现)
开发计划 §12 明确:手机号短信验证码登录、第三方登录**尚未确认**。设计上预留、代码上不渲染:
- **位置**:主按钮与协议行之间。展开后结构为:分隔线 + 居中文字"其他登录方式"12 `muted`)+ 一排 44×44 圆形图标按钮(白底、`border` 描边),间距 24。
- **短信验证码**:确认后以账号输入框上方的"密码登录 / 验证码登录"分段切换(SegmentedButton 或双 Tab 文字)接入,不改变整体布局。
- 布局采用居中弹性结构,预留区展开不会挤压表单——实现时无需为此留白占位。
---
## 3. 注册页规范
### 3.1 布局结构
从登录页 push 进入,有返回能力。与登录页同一视觉框架,改为顶部对齐(字段多,不做垂直居中)。
```text
┌──────────────────────────────────────┐
│ ← 返回(AppBar 透明,仅返回箭头) │
│ SafeArea + Scroll,水平 padding 24 │
│ │
│ 创建账号 │ headlineSmall 22/w800
│ 加入 Patbond,记录毛孩子的每一天 │ bodySmall muted,间距 8
│ 间距 32 │
│ ┌────────────────────────────────┐ │
│ │ 用户名 │ │
│ └────────────────────────────────┘ │
│ 间距 16 │
│ ┌────────────────────────────────┐ │
│ │ 手机号 │ │
│ └────────────────────────────────┘ │
│ ┄┄ [预留] 短信验证码行,v1 不渲染 ┄┄ │
│ 间距 16 │
│ ┌────────────────────────────────┐ │
│ │ 密码 [👁] │ │
│ └────────────────────────────────┘ │
│ 密码 8–32 位,需包含字母和数字 │ helperText 12 muted
│ 间距 16 │
│ ┌────────────────────────────────┐ │
│ │ 确认密码 [👁] │ │
│ └────────────────────────────────┘ │
│ 间距 32 │
│ ┌────────────────────────────────┐ │
│ │ 注 册 │ │ 主按钮同登录页
│ └────────────────────────────────┘ │
│ 间距 16 │
│ 已有账号? 直接登录 │ 返回登录页(pop)
└──────────────────────────────────────┘
```
### 3.2 字段与校验规则
| 字段 | 校验(失焦即校验,提交再总校验) | 错误文案 |
| --- | --- | --- |
| 用户名 | 非空;3–20 字符;字母/数字/下划线,字母开头(最终以 API 契约为准) | "请输入用户名" / "用户名需 3–20 位,字母开头,可含数字和下划线" |
| 手机号 | 非空;11 位大陆手机号 `^1\d{10}$` | "请输入手机号" / "请输入正确的 11 位手机号" |
| 密码 | 8–32 位,含字母和数字(最终以 API 契约为准);helperText 常显规则,出错时被 errorText 替换 | "密码需 8–32 位,且同时包含字母和数字" |
| 确认密码 | 与密码一致 | "两次输入的密码不一致" |
服务端唯一性冲突(M1 已列入范围)映射回字段:用户名已存在 → 用户名字段 errorText "该用户名已被使用";手机号已注册 → 手机号字段 "该手机号已注册,可直接登录",并可附带"去登录"文字链接。
注册成功即建立会话(`POST /auth/register` 注册并创建会话),直接进入首页,不回登录页重新登录。
### 3.3 预留区
- **短信验证码**(未确认):手机号下方一行——验证码输入框(flex)+ 右侧"获取验证码"次级按钮(`OutlinedButton`,高 52,倒计时态显示"59s 后重发"并禁用)。
- 第三方登录预留区同登录页 §2.4。
---
## 4. 状态设计(对应 DoDloading / error / retry;登录表单无 empty 态)
### 4.1 输入框状态
| 状态 | 视觉 |
| --- | --- |
| 默认 | 白底,`border` 1px 描边 |
| 聚焦 | `primary` 1.5px 描边(主题已有) |
| 错误 | `error` 1.5px 描边 + 输入框下方 errorText12`error` 色);聚焦重新输入后即清除该字段错误 |
| 禁用(提交中) | 整体 60% 不透明度,不可编辑 |
错误出现/消失会引起 8–20px 高度变化,可接受;不使用固定高度错误占位(多字段表单会过度稀疏)。
### 4.2 错误展示的层级策略
1. **字段级**(首选):能定位到具体字段的错误一律放该字段 errorText——包括本地校验和服务端 409/422 映射。
2. **表单级**:无法归属单一字段的业务错误(如 401 "用户名或密码错误"、429 "尝试次数过多,请稍后再试")→ 主按钮上方的行内错误横幅:`error` 8% 透明度底、`radiusSm` 圆角、内边距 12、左侧 `error_outline` 图标 18 + 文字 13 `error` 色。用横幅而非 SnackBar,因为错误需要停留在表单上下文里供用户对照修改。
3. **瞬态/网络错误**:请求超时、断网 → floating SnackBar(主题已有圆角 16):"网络异常,请检查网络后重试",附 action "重试"(重放上次提交)。
所有错误文案说人话、给出路,不透传服务端异常文本(契约规范 §6.1 禁止只返回异常文本,客户端同样禁止直接展示错误码)。
### 4.3 提交 loading 态
- 主按钮:文字替换为 20×20 白色 `CircularProgressIndicator`strokeWidth 2.5),按钮尺寸不变、不可再点。
- 两个输入框、注册/登录切换链接同时禁用,防止请求飞行中改动或重复导航。
- 不用全屏遮罩 loading——登录请求是单按钮动作,局部 loading 干扰最小。
### 4.4 主按钮状态汇总
| 状态 | 视觉 |
| --- | --- |
| 默认 | `primaryStrong` 填充,白字 15/w700 |
| 按下 | 填充加深 8%Material ripple 默认即可) |
| 禁用 | 主题 disabledonSurface 12% 底 / 38% 字)。仅在提交中禁用;**不做"表单没填完就置灰"**——允许点击后给出校验错误,比让用户猜为什么按钮是灰的更友好 |
| loading | 见 §4.3 |
---
## 5. 启动过渡:登录态恢复(Splash)
### 5.1 流程
```text
App 启动
└─ Splash 展示(最短 500ms,避免闪跳)
同时并行:从安全存储读 refresh token
├─ 无 token ────────────────→ 淡入登录页
├─ 有 token → POST /auth/refresh(客户端超时 5s
│ ├─ 成功(拿到新 access/refresh)→ 淡入首页(MainShell
│ ├─ 401/会话失效 → 清除本地凭证 → 淡入登录页
│ └─ 网络错误/超时 → Splash 切换为错误态(见 5.3
```
### 5.2 Splash 视觉
- 全屏 `canvas` 底色,品牌区(同登录页 §2.2:logo 72 + 字标 + slogan)垂直水平居中。
- 品牌区下方 32 处放 20×20 `CircularProgressIndicator``primary` 色,strokeWidth 2.5)——**仅当等待超过 300ms 才显示**,快速路径下用户只看到一闪而过的品牌屏。
- 页面切换用 300ms fade`PageRouteBuilder` + `FadeTransition`);Splash 与登录页品牌区位置刻意一致,淡入登录页时品牌区视觉上原地不动、表单浮现,过渡自然。
- 原生层(iOS LaunchScreen / Android launch theme)应配同色 `canvas` 纯色底,避免白屏→Splash 的颜色跳变(可延后到 M6 商店配置一并做)。
### 5.3 Splash 错误态(网络失败且本地有 token 时)
不能让用户卡在无限转圈:
```text
🐾 Patbond(品牌区不动)
网络连接失败,无法恢复登录
┌──────────────┐
│ 重试 │ 次级按钮 OutlinedButton,高 44
└──────────────┘
改用账号登录 文字链接 → 清除凭证进登录页
```
「重试」重新发起 refresh;「改用账号登录」是逃生通道。**注意**:网络失败(非 401)不得自动清除本地 refresh token——只有服务端明确判定会话无效才清除。
---
## 6. Flutter 实现建议
### 6.1 复用现有主题 token
| 现有资产 | 用法 |
| --- | --- |
| `AppColors.*` | 全部沿用;**新增** `primaryStrong``primaryDark``accent``surfaceTint``error``error` 是本迭代硬需求,其余配合暖色迁移时加) |
| `inputDecorationTheme` | 直接满足输入框默认/聚焦态;错误态补 `errorBorder` / `focusedErrorBorder``AppColors.error`1.5px)和 `errorStyle`12px |
| `textTheme` | 页面标题 `headlineSmall`、按钮/label `titleMedium`、辅助文字 `bodySmall` |
| `snackBarTheme` | 网络错误提示直接用 |
| `EmptyState`common.dart | 登录流用不到,但其"图标+文案"模式是 Splash 错误态的参照 |
建议在 `buildAppTheme()` 补充 `filledButtonTheme``minimumSize: Size.fromHeight(52)``RoundedRectangleBorder(borderRadius: BorderRadius.circular(16))``textStyle: 15/w700`——一次定义,登录/注册/后续所有主操作按钮受益。若采纳暖色迁移,同时把 `ColorScheme.fromSeed``seedColor` 换为暖色 `primary` 并显式覆盖 `colorScheme.error`
### 6.2 新增可复用组件(建议放 `lib/widgets/` 或 `lib/features/auth/widgets/`
| 组件 | 职责 |
| --- | --- |
| `BrandMark` | logo + 字标 + slogan`size` 参数;Splash 与登录页共用,保证两处渲染一致(这是 §5.2 淡入过渡成立的前提) |
| `AppTextField` | 封装 `TextFormField`label、前缀图标、errorText、enabled,密码模式内置可见性切换(含 ≥44 点击区)与 obscure 状态 |
| `PrimaryButton` | `FilledButton` + `isLoading`:loading 时换转圈、锁点击、尺寸不变。全 app 提交类按钮通用 |
| `InlineErrorBanner` | §4.2 表单级错误横幅;后续所有网络页面的错误态可复用 |
| `AuthScaffold` | 认证页统一框架:SafeArea + 滚动 + padding 24 + 键盘避让,登录/注册/找回密码共用 |
### 6.3 结构与状态(配合开发计划 §4.2)
- 新建 `lib/features/auth/``login_page.dart``register_page.dart``splash_page.dart``auth_state.dart`(或 controller)。
- 认证状态机:`unknown → authenticating → authenticated | unauthenticated``app.dart` 根据状态切换 `home`(替代现在直接进 `MainShellPage`),配 300ms fade。
- token 存 `flutter_secure_storage`(需新增依赖);开发计划明令禁止把 token 写进 `SharedPreferences`(现有 `AppState` 的持久化方式不可套用到凭证)。
- 表单用 `Form` + `AutofillGroup`(激活系统密码管理器自动填充),提交成功后调 `TextInput.finishAutofillContext()` 触发保存密码提示。
### 6.4 可访问性核对清单
- 触控目标 ≥44×44:主按钮 52、密码可见性切换与文字链接需显式保证点击区。
- 承载文字的色彩组合 ≥4.5:1`primaryStrong`/白 ≈4.5、`ink`/canvas、`error`/白 ≈5.4 均达标;`#FF6F4C` 只作装饰不载文。
- errorText 由 `InputDecoration` 原生渲染,TalkBack/VoiceOver 自动关联朗读;表单级横幅出现时用 `SemanticsService.announce` 播报。
- 键盘流:账号 `next` → 密码 `done` 提交;字体随系统缩放(不写死 `textScaleFactor`)。
---
## 7. 交付验收对照(DoD
- [ ] 登录/注册页具备 loading、错误、重试路径(本规范 §4);Splash 具备网络失败重试(§5.3)。
- [ ] 错误同时区分字段级/表单级/瞬态三层,无裸错误码透出。
- [ ] token 仅存安全存储;退出登录清除凭证并回登录页。
- [ ] 预留区(短信验证码、第三方登录)不渲染任何占位 UI,待 §12 事项确认后按本规范扩展。
- [ ] 若采纳暖色迁移:仅改 `app_theme.dart` 色值映射,全局回归五个既有 Tab 页无布局破坏。
@@ -0,0 +1,221 @@
# 第一迭代埋点与实验规划(身份漏斗)
> 角色:Experiment Tracker
> 日期:2026-09-03
> 依据:`patbond-doc/docs/development/development-plan.md` 第 8 节(第一迭代范围)、第 9 节「可观测性与产品验证」、第 11 节(已知风险)
> 范围:仅覆盖「注册 → 登录 → 获取当前用户 → 退出」身份纵切;不涉及社区、AI 创作、本地服务的事件与实验。
---
## 1. 埋点事件规范
### 1.1 设计原则
- 事件名使用 `snake_case`,格式为 `<域>_<对象>_<结果>`,域前缀统一为 `auth`
- 每个事件携带 `eventVersion`(本迭代全部为 `1`);schema 变更时递增版本,消费端按版本解析,禁止原地改语义。
- 事件在**客户端**采集(反映用户实际体验,含网络失败),`serverTs` 由接收端补写,作为统计的权威时间;`clientTs` 仅用于排序和时延诊断。
- 每个事件由客户端生成 `eventId`(UUIDv7),用于服务端幂等去重(配合 at-least-once 上报)。
- JSON 字段 `camelCase`、UUID 字符串、ISO 8601 时间,与第 6.1 节 API 通用规则一致。
### 1.2 公共属性(所有事件必带)
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `eventId` | UUID | 客户端生成(UUIDv7),服务端去重键 |
| `eventName` | string | 见 1.4 事件清单 |
| `eventVersion` | int | 本迭代固定 `1` |
| `anonymousId` | UUID | 设备级匿名标识,首次启动生成,存普通本地存储(非敏感);注册前唯一可用的主体标识 |
| `userId` | UUID / null | 登录后填充;注册开始、登录前事件为 null |
| `sessionId` | UUID | 客户端会话标识:应用冷启动或后台超过 30 分钟后重新生成 |
| `clientTs` | timestamptz | 客户端本地时间(ISO 8601 含时区) |
| `serverTs` | timestamptz | **服务端接收时补写**,客户端不发;统计窗口一律以此为准 |
| `appVersion` | string | 如 `1.0.0+12` |
| `platform` | string | `android` / `ios` |
| `osVersion` | string | 粗粒度主版本,如 `android-14` |
### 1.3 隐私红线(禁止字段,任何事件不得携带)
依据第 9 节「不得包含密码、token 或非必要个人信息」及 6.1 节日志规则:
1. **密码**:明文、哈希、长度、强度分数均禁止。
2. **凭证**access token、refresh token、验证码、上传签名、`Idempotency-Key` 等任何凭证或其片段。
3. **手机号 / 邮箱全文**:失败事件中不得回传用户输入的账号原文;仅允许 `identifierType``username` / `phone` / `email`)这类枚举。
4. **用户名原文**:注册失败重复冲突时只报 `username_taken` 分类,不报具体用户名。
5. **完整 IP、精确地理位置、设备广告标识(IDFA/GAID)**
6. **原始请求/响应报文、异常堆栈原文**:失败只允许上报枚举化的 `failureReason` + 业务错误码 + HTTP 状态码。
服务端接收器应对属性做白名单校验:schema 之外的字段直接丢弃并计数告警,防止未来有人顺手塞入敏感字段。
### 1.4 事件清单
#### 注册
| 事件名 | 触发时机 | 专有属性 |
| --- | --- | --- |
| `auth_register_started` | 用户进入注册页并产生首次输入(首字符),每个注册会话记一次 | `entryPoint``launch` / `login_page_link` |
| `auth_register_succeeded` | 客户端收到 `POST /api/v1/auth/register` 的成功响应(code=0)之后 | `durationMs`started → succeeded 耗时) |
| `auth_register_failed` | 收到失败响应、请求超时或本地校验拦截提交时 | `failureReason``errorCode`(业务码,可空)、`httpStatus`(可空)、`attemptSeq`(本注册会话第几次尝试) |
`auth_register_failed.failureReason` 枚举:
- `validation_error` — 本地或服务端参数校验失败(格式、必填)
- `username_taken` — 用户名已存在
- `phone_taken` — 手机号已存在
- `weak_password` — 密码不满足策略
- `rate_limited` — 触发频控
- `network_error` — 超时 / 无网络 / 连接失败
- `server_error` — 5xx 或未知业务码
#### 登录
| 事件名 | 触发时机 | 专有属性 |
| --- | --- | --- |
| `auth_login_succeeded` | 收到 `POST /api/v1/auth/login` 成功响应且 token 已写入安全存储后 | `identifierType``durationMs` |
| `auth_login_failed` | 收到失败响应或请求超时 | `identifierType``failureReason``errorCode``httpStatus``attemptSeq` |
`auth_login_failed.failureReason` 枚举:`invalid_credentials`(凭证错误,不区分账号不存在与密码错误,与接口防枚举策略一致)、`account_locked`(登录失败限制触发)、`rate_limited``validation_error``network_error``server_error`
#### Token 刷新
| 事件名 | 触发时机 | 专有属性 |
| --- | --- | --- |
| `auth_token_refresh_succeeded` | `POST /api/v1/auth/refresh` 轮换成功且新 token 落库安全存储后 | `trigger``proactive` 预刷新 / `on_401` 拦截器触发 / `restore` 启动恢复) |
| `auth_token_refresh_failed` | 刷新失败 | `trigger``failureReason``errorCode``httpStatus` |
`failureReason` 枚举:`refresh_expired``refresh_revoked`(含轮换重放被拒,是 M1 验收「退出后 refresh token 不可再次使用」的观测点)、`network_error``server_error`
#### 退出
| 事件名 | 触发时机 | 专有属性 |
| --- | --- | --- |
| `auth_logout` | 用户主动退出:本地凭证已清除时上报(不等待服务端撤销结果) | `serverRevoked`bool`POST /api/v1/auth/logout` 是否成功)|
被动登出(会话被撤销/过期导致跳登录页)不用此事件,由 `auth_session_restore_failed``auth_token_refresh_failed` 覆盖,避免口径混淆。
#### 登录态恢复
| 事件名 | 触发时机 | 专有属性 |
| --- | --- | --- |
| `auth_session_restore_started` | 冷启动时安全存储中存在 refresh token,开始恢复流程 | — |
| `auth_session_restore_succeeded` | 恢复完成:拿到有效 access token 且 `GET /api/v1/me` 成功 | `durationMs``usedRefresh`bool,是否经历了刷新) |
| `auth_session_restore_failed` | 恢复失败,用户被要求重新登录 | `failureReason``refresh_expired` / `refresh_revoked` / `network_error` / `server_error`)、`errorCode``httpStatus` |
说明:冷启动无存量凭证时不上报任何 restore 事件(不算失败),保证会话恢复成功率的分母干净。
### 1.5 与文档第 9 节首批漏斗事件的对应
文档建议首批覆盖六个漏斗事件,本迭代范围内落地其中两个:`auth_register_succeeded`(注册完成)、`auth_login_succeeded`(登录成功)。宠物创建、动态发布、AI 任务成功、预约确认分别属于 M2–M5,届时沿用本规范的公共属性与命名规则扩展,不在本轮定义。
---
## 2. 指标口径
统计一律以 `serverTs` 划定窗口(UTC 日界,展示层可换算);主体去重优先 `userId`,注册前用 `anonymousId`
### 2.1 注册转化率
- **分子**:窗口内产生 `auth_register_succeeded` 的去重 `anonymousId` 数。
- **分母**:窗口内产生 `auth_register_started` 的去重 `anonymousId` 数。
- **窗口**:按天统计;归因窗口 24 小时——`started` 落在 D 日的设备,其成功可发生在 `started` 后 24h 内,仍计入 D 日转化(避免跨零点漏算)。
- **辅助指标**`auth_register_failed``failureReason` 分布;`network_error`/`server_error` 占比是技术问题信号,`validation_error`/`weak_password` 占比是产品/文案问题信号。
### 2.2 登录成功率
- **尝试级(主口径)**:分子 = `auth_login_succeeded` 事件数;分母 = `auth_login_succeeded` + `auth_login_failed` 事件数。按天统计,另看 7 天滚动。
- **用户级(辅助口径)**:窗口内最终登录成功(至少一条 succeeded)的去重主体 / 发起过登录尝试的去重主体(`userId` 缺失时用 `anonymousId`),窗口 24 小时。
- **排除项**:主口径不排除 `invalid_credentials`(它反映真实用户体验);另设「系统登录成功率」= 排除 `invalid_credentials``validation_error``account_locked` 后的成功率,用于监控服务健康,目标应接近 100%。
### 2.3 会话恢复成功率
- **分子**`auth_session_restore_succeeded` 事件数。
- **分母**`auth_session_restore_started` 事件数(即冷启动时存在存量凭证并发起恢复的次数)。
- **窗口**:按天统计。无存量凭证的冷启动不进分母;`refresh_expired` 计入失败(是体验事实),但按 `failureReason` 拆分后单独解读——过期占比高提示 token 有效期策略问题(第 12 节待确认事项),`refresh_revoked`/`server_error` 占比高提示实现缺陷。
- 该指标同时是 M1 验收标准「客户端可恢复登录态」的量化观测。
---
## 3. 为什么第一迭代不启动 A/B 实验
文档第 9 节明确规则:**「A/B 实验必须使用稳定分流和独立曝光事件;在基础埋点、指标口径和样本量规则完成前不启动产品实验。」** 当前三个前置条件均不满足:
1. **基础埋点不存在**:三仓尚无任何事件采集链路(第 2 节基线:Flutter 没有网络层,API 无观测能力),本报告定义的事件本身就是待建设项。
2. **指标口径未经数据验证**:口径刚在本报告提出,尚未有真实数据校验事件触发准确性(重复、丢失、时序)——没有可信基线就无法解读实验差异。
3. **样本量规则无从谈起**:第一迭代是首个真实版本,DAU 基线为零。以典型场景估算:若登录成功率基线约 90%,要在 95% 置信度、80% 功效下检出 3 个百分点的绝对提升,每组约需 1,600 个独立用户;当前流量远达不到,实验只会产出噪声结论。
4. **没有分流与曝光基础设施**:无稳定分流(用户 ID 哈希 + 实验盐)组件,无独立曝光事件,无法保证随机化与分析对齐。
5. **也没有值得实验的对照**:第一迭代只有一条登录纵切路径,不存在需要 A/B 裁决的产品分叉;此时的正确动作是把漏斗测准,而不是测变体。
### 未来启动实验前必须就绪的前置条件清单
- [ ] 本报告第 1 节事件已上线,且通过数据质量验收:事件丢失率 < 5%,`eventId` 去重生效,`serverTs` 覆盖率 100%,属性白名单校验无敏感字段泄漏。
- [ ] 第 2 节三个指标已连续稳定产出 ≥ 2 周,形成基线均值与方差,且与服务端日志(如登录接口成功率)交叉核对一致。
- [ ] 样本量规则成文:给定基线率、最小可检测效应(MDE)、95% 置信度、80% 功效的样本量计算方法与查表,并据实际 DAU 判断实验最短运行时长。
- [ ] 稳定分流组件:`hash(userId, experimentSalt) % buckets`,同一用户在实验期内分组不变、跨端一致;登录前实验用 `anonymousId` 并定义登录后归并规则。
- [ ] 独立曝光事件(如 `experiment_exposed`,携带 `experimentKey``variant``eventVersion`):分析只统计实际曝光用户,杜绝按分配名单算分母。
- [ ] 实验设计文档模板与评审流程:假设、主指标、护栏指标、提前停止规则、多重比较校正约定。
- [ ] 安全机制:护栏指标(崩溃率、登录成功率等)实时监控与一键回滚(可复用后续的配置下发/feature flag 能力)。
- [ ] 隐私合规复核:实验分组数据同样遵守 1.3 节红线。
---
## 4. 埋点数据落地建议
约束:与现有技术栈一致(Spring Boot + PostgreSQL 16 + Flutter);**不引入第三方分析 SaaS/SDK**——文档第 11 节第 10 条明示外部供应商均未确定,埋点作为基础设施不应在此时绑定未评审的供应商,自建最小链路即可满足第一迭代验证需求,且数据留在自有库,规避合规不确定性。
### 4.1 客户端(Flutter
- 新增轻量 `analytics` 模块(与 `auth` 等 feature 平级),对外仅暴露 `track(eventName, props)`;事件构造时自动附加公共属性。
- **本地持久化队列**:事件先写本地队列(`sqflite` 表或追加式文件——Flutter 生态内置方案,非第三方分析服务),应用被杀不丢事件。注意:事件属于非敏感数据,存普通本地存储即可,**不占用安全存储**(安全存储按 4.2 节仅放 token)。
- **批量上报**:满 20 条或 30 秒定时触发,冷启动和进入后台时各冲刷一次;单批 ≤ 50 条。
- **重试**:指数退避(5s 起,上限 5 分钟)+ at-least-once;服务端靠 `eventId` 去重,客户端只在收到 2xx 后删除本地记录。4xx(schema 被拒)不重试,丢弃并本地计数。
- **背压**:队列上限 1,000 条,超限丢最旧事件;上报失败不得阻塞或影响任何业务流程(埋点永远是旁路)。
### 4.2 服务端(Spring
- 新增接口 `POST /api/v1/events`(批量数组体)。放在现有服务内(建议 `patbond-user` 或后续网关层)即可,第一迭代不为埋点单起服务。
- **鉴权**:登录后带 access token;注册/登录前的事件允许匿名上报(仅此端点),配合频控与 body 大小限制(如单批 ≤ 64KB)防滥用。
- **处理**:白名单校验事件名与属性 → 剥离/拒绝红线字段 → 补写 `serverTs` → 落库。响应 `202`,不因单条非法事件拒绝整批(返回逐条结果)。
- **存储**:落 `platform` schema(平台能力域,与第 3 节域划分一致),经 Flyway 迁移新建表:
```sql
create table platform.product_events (
event_id uuid primary key, -- 客户端 UUIDv7,天然幂等去重
event_name text not null,
event_version int not null,
anonymous_id uuid not null,
user_id uuid,
session_id uuid not null,
client_ts timestamptz not null,
server_ts timestamptz not null default now(),
app_version text not null,
platform text not null,
props jsonb not null default '{}'::jsonb -- 白名单内的专有属性
);
create index on platform.product_events (event_name, server_ts);
create index on platform.product_events (user_id, server_ts);
```
- 插入用 `on conflict (event_id) do nothing` 实现去重。第一迭代事件量极小,同库 SQL 直接算第 2 节指标即可(可沉淀成参考查询放入 `docs/database/`);将来量大再考虑异步化(RabbitMQ 已在技术栈规划内)或独立分析存储。
- **红线兜底**:该表数据同样受 6.1 节日志规则约束;接收端是最后一道防线,白名单之外字段一律丢弃并打点告警。
### 4.3 数据质量监控(上线即带)
- 服务端技术指标:`/api/v1/events` 请求量、拒绝率、去重命中率(与第 9 节技术指标要求一致)。
- 每日核对:`auth_login_succeeded` 事件数 vs 登录接口成功响应数,偏差 > 5% 告警——这是未来实验前置条件里「交叉核对」的日常化。
---
## 附:事件总览
| # | 事件名 | 版本 | 阶段 |
| --- | --- | --- | --- |
| 1 | `auth_register_started` | 1 | 注册 |
| 2 | `auth_register_succeeded` | 1 | 注册(漏斗事件) |
| 3 | `auth_register_failed` | 1 | 注册 |
| 4 | `auth_login_succeeded` | 1 | 登录(漏斗事件) |
| 5 | `auth_login_failed` | 1 | 登录 |
| 6 | `auth_token_refresh_succeeded` | 1 | 会话维持 |
| 7 | `auth_token_refresh_failed` | 1 | 会话维持 |
| 8 | `auth_logout` | 1 | 退出 |
| 9 | `auth_session_restore_started` | 1 | 恢复 |
| 10 | `auth_session_restore_succeeded` | 1 | 恢复 |
| 11 | `auth_session_restore_failed` | 1 | 恢复 |
@@ -0,0 +1,206 @@
# 06 证据驱动质量审计(开工前基线)
- 审计人:EvidenceQAEvidence Collector
- 日期:2026-09-03
- 方式:只读审计(ls / grep / 读文件 / git 只读命令)。未运行构建、未启动服务,所有涉及运行时行为的结论均标注「静态分析,需运行验证」。
- 验收依据:`/home/lx/workspace/patbond/patbond-doc/docs/development/development-plan.md` 第 9 节(测试与质量门禁)、第 10 节(Definition of Done)、第 8 节(第一迭代任务)、M1 验收标准(第 212-223 行)。
---
## 1. 测试资产审计
### 1.1 patbond-api:零测试
- 全仓库文件清单中**不存在任何 `src/test` 目录**`find patbond-api -type f` 结果仅含 `src/main`,见仓库文件列表)。
- 三个模块的 pom 中**均无 `spring-boot-starter-test` 依赖**`grep -rn "spring-boot-starter-test" patbond-api --include="pom.xml"` 无任何匹配)——即写测试的前置依赖都还没有。
- 与文档第 9 节要求的差距(`development-plan.md:294-300`):单元测试、集成测试(Testcontainers)、契约测试、安全测试全部缺失,缺口为 100%。
- 后果:CI 最低门禁 `mvn clean test``development-plan.md:313-314`)在当前仓库上是**空转通过**——0 个测试也会绿。这正是文档第 11 节风险 6(`development-plan.md:354`)自认的状态,审计确认属实。
### 1.2 patbond-flutter:仅 1 个冒烟测试
- `test/` 目录只有 1 个文件:`/home/lx/workspace/patbond/patbond-flutter/test/widget_test.dart`,共 21 行。
- 内容:单个 `testWidgets`,断言主导航 5 个 Tab 文案,以及两条**写死的演示数据字符串**:
- `test/widget_test.dart:18``expect(find.text('北京 · 朝阳区'), findsOneWidget)`
- `test/widget_test.dart:19``expect(find.text('28°C 晴'), findsOneWidget)`
- 这两个值来自 `lib/data/demo_data.dart:11-19``locationWeatherOptions` 第一项:city '北京'、district '朝阳区'、temperature 28、conditionText '晴')。演示数据一改,唯一的测试即失败。
- 与文档第 9 节 Flutter 要求的差距(`development-plan.md:302-308`):
- 单元测试(DTO 映射、Repository、缓存、状态转换):0 个。`lib/state/app_state.dart` 有可测的加载/持久化/回退逻辑(约 120 行),无对应测试。
- Widget 测试(登录、宠物编辑、动态互动等):0 个(登录页本身不存在)。
- Golden/响应式测试:0 个。
- Integration/E2E0 个(`integration_test/` 目录不存在)。
- loading/empty/error/retry/离线状态覆盖:0 个。
- 工具链本身可用:`analysis_options.yaml` 引入 `package:flutter_lints/flutter.yaml``analysis_options.yaml:10`),`pubspec.yaml:39-48``flutter_test``flutter_lints ^6.0.0`
### 1.3 门禁落地情况
- **两个代码仓库均无任何 CI 配置**:`ls -a` 未见 `.github``.gitlab-ci.yml`、Jenkinsfile 等(patbond-api 根目录仅 `.git``.gitignore``.idea`、三个模块与 `pom.xml``Readme.md`)。
- **无 Maven Wrapper**`mvnw` 不存在),与 M0 目标(`development-plan.md:118`、204 行前后)尚有差距——这是计划内待办,但意味着「CI 最低门禁」目前只存在于文档里,没有任何机制执行它。
---
## 2. 代码质量风险审计(逐条附证据)
### 2.1 认证与鉴权(后端)
- **token 不可验证**`patbond-api/patbond-auth/src/main/java/com/patbond/patbond/auth/service/AuthService.java:47-56` —— `buildToken` 返回 `UUID.randomUUID().toString().replace("-", "")``expiresAt` 只是响应里的展示字段。无签名、无存储、无校验途径、无 refresh token。与 M1 验收标准(`development-plan.md:223`)和风险 2`development-plan.md:350`)一致,审计确认。
- **`/internal/**` 完全无访问控制**`patbond-api/patbond-user/src/main/java/com/patbond/patbond/user/controller/UserController.java:18` 映射 `/internal/users`,全仓库 grep 无任何 Spring Security、拦截器或过滤器。`GET /internal/users/{id}`UserController.java:37-40)返回的 `UserProfile``phone` 字段(`UserService.java:69-71``toProfile` 传入 `user.getPhone()`)——任何能访问 8082 端口的人可枚举用户资料含手机号。对应风险 3(`development-plan.md:351`)。
- **用户仅存内存**`patbond-api/patbond-user/src/main/java/com/patbond/patbond/user/service/UserService.java:21-23` —— `AtomicLong idGenerator` + 两个 `ConcurrentHashMap`。重启即丢全部用户,直接不满足 M1 验收「注册后重启服务数据不丢失」(`development-plan.md:223`)。
- **错误状态码折叠**(静态分析,需运行验证):`AuthService.java:58-63``requireData` 把所有失败折叠为 `HttpStatus.BAD_REQUEST`。而 user 服务用 `ResponseStatusException` 返回 409/401/404`UserService.java:29,48,56,64`),Feign 客户端(`patbond-auth/.../client/UserClient.java`)收到非 2xx 时会抛 `FeignException` 而非进入 `requireData` 的业务分支——登录密码错误的调用链大概率对客户端表现为 500 或语义丢失的 400,违反契约规范「错误必须同时返回正确 HTTP 状态码和稳定业务错误码」(`development-plan.md:171`)。
### 2.2 硬编码密钥 / 密码 / 敏感信息
- **后端源码中未发现硬编码密钥、密码或密文**。`grep -rniE "password|secret|api[_-]?key|token"``patbond-api` 三个模块 `src` 下的命中全部是 DTO 字段赋值(如 `LoginRequest.java:26``CreateUserRequest.java:27,45`),无真实凭据。诚实结论:这一项没有问题,不编造。
- **配置卫生良好**:仓库只提交 `.yml.sample``patbond-auth/src/main/resources/application.yml.sample``patbond-user/.../application.yml.sample`),真实 `application.yml``.gitignore` 忽略(`patbond-api/.gitignore` 末段:`patbond-*/src/main/resources/application.yml` + `!...yml.sample`)。sample 中仅有 `${NACOS_SERVER_ADDR:127.0.0.1:8848}` 之类的本地默认值,无敏感值。
- **开发 fixture 凭据集中在 SQL**`patbond-doc/docs/database/patbond_postgresql.sql`
- 第 1269 行:注释明文声明 `accounts use the password: Patbond@123`
- 第 1289-1293 行:三个 fixture 账号共用同一 bcrypt 哈希 `$2y$12$UYCn5Zm...`
- 第 1283-1286 行:fixture 手机号 `+8613800000001` 等(明显假号段,风险低);
- 约第 1296-1308 行:`identity.auth_sessions` 插入固定 `refresh_token_hash = decode(repeat('ab', 32), 'hex')``access_token_jti = 'fixture-access-token-jti'`
- 文件自身已注明「Remove this section from the production Flyway baseline」(第 1266-1269 行),且这是开发种子数据而非泄露的生产密钥。风险在于:bootstrap 是**单文件**,结构与种子数据未拆分(第 4.3 节第 105 行的拆分要求未完成),误导入生产即产生三个已知密码账号和一个可预测 refresh token。
### 2.3 TODO / FIXME / 注释掉的代码
- `grep -rn "TODO|FIXME|HACK|XXX"``patbond-api``patbond-flutter/lib``patbond-flutter/test` 的 java/dart/yml 文件中:**0 命中**。
- 注释掉的代码块:Java 侧 `grep -rnE "^\s*//.*(;|\{)\s*$"` 0 命中;Dart 侧同类模式 0 命中(仅 `pubspec.yaml``analysis_options.yaml` 中的脚手架模板注释,属正常)。
- 诚实结论:这两类问题当前不存在,不虚报。
### 2.4 硬编码 URL 与展示型字段(Flutter
- 22 处硬编码 Unsplash 图片 URL,集中在:`lib/data/demo_data.dart`4、6、8、128、136、146、151、159、178、191、203、216、229、245、252、259 行)、`lib/features/home/home_page.dart:551,556``lib/features/pets/pets_page.dart:357-358`。属演示数据(文档第 33 行认可其非契约地位),但 DoD 要求「不依赖 Demo 常量」(`development-plan.md:340`),验收时必须逐页替换。
- 展示字符串被当数据持久化:`lib/data/demo_data.dart:117``time: '2小时前'`)、130、138、147、161 行;`distance: '1.2km'` 等在 172、185、197、210、223 行。违反第 4.3 节「相对时间、距离由事实字段计算,不持久化展示字符串」(`development-plan.md:111`),对应风险 4`development-plan.md:352`)。
- 无网络层、无登录页、无安全存储:`lib/` 下无 `http`/`dio` 依赖(`pubspec.yaml:30-37``cupertino_icons` + `shared_preferences`),`AppState` 直接持久化宠物/疫苗/帖子/天气到 `SharedPreferences``lib/state/app_state.dart:9-12,97-118`)。第一迭代需按 4.2 节新建全部网络与鉴权层;届时 token 必须走安全存储而非 `SharedPreferences``development-plan.md:97`)。
---
## 3. 文档一致性审计
| 检查项 | 结论 | 证据 |
| --- | --- | --- |
| API Readme 端点 vs 代码 | 一致 | `Readme.md:34-44` 的 6 个端点与 `AuthController.java:24-31``UserController.java:27-45` 逐一对应 |
| API Readme 端口 vs 配置 | 一致 | `Readme.md:33,40`8081/8082= 两个 `application.yml.sample:2` |
| API Readme 的 NACOS_SERVER_ADDR 说明 | 一致 | `Readme.md:48-53` vs sample 第 12 行 `${NACOS_SERVER_ADDR:127.0.0.1:8848}` |
| **API Readme 启动步骤完整性** | **不一致** | `Readme.md:22-26` 只有 `mvn compile`**未提及**必须先复制 `application.yml.sample``application.yml`(该步骤只在 `development-plan.md:127-138`),也无 `spring-boot:run` 命令。干净检出仅按 README 无法把服务跑起来——文档风险 5(`development-plan.md:353`)确认属实 |
| Readme 技术栈 RabbitMQ vs 计划 | 内部一致、与计划冲突 | `Readme.md:19` 列 RabbitMQ`patbond-common/pom.xml:120-123` 确实引入 `spring-boot-starter-amqp`;但计划 4.1 节明确 common「不应让所有服务被动引入 RabbitMQ、Feign 等依赖」(`development-plan.md:79` |
| Flutter README 平台声明 | 一致 | `README.md:5` 声明的 Android/iOS/Web/桌面对应仓库 `android/ ios/ web/ linux/ macos/ windows/` 目录均存在 |
| Flutter README shared_preferences 声明 | 一致 | `README.md:13` vs `lib/state/app_state.dart:9-12` 四个持久化 key |
| **Flutter README 校验命令 vs 计划 CI 门禁** | **不一致** | `README.md:34``dart format --set-exit-if-changed lib test`(缺 `--output=none`,实际会**改写文件**);计划门禁是 `dart format --output=none --set-exit-if-changed lib test``development-plan.md:317` |
| 计划 5.2 节命令中的文件路径 | 一致 | `development-plan.md:130-133` 引用的两个 `.yml.sample` 均存在于对应路径 |
| mkdocs.yml 导航 | 一致 | `patbond-doc/mkdocs.yml` nav 引用的 `index.md``development/development-plan.md` 均存在;`docs/index.md:14` 链接的 `database/patbond_postgresql.sql` 存在 |
| 其他观察 | — | `patbond-flutter` 工作区有未提交改动(`git status`` M README.md`);`patbond-api/.idea/` 在磁盘上但未被 git 跟踪(`git ls-files` 无 .idea 条目),无泄露 |
`mkdocs build --strict` 门禁未实际执行(审计约束禁止构建),仅完成静态引用核对。
---
## 4. 问题清单(按严重程度排序)
### Blocker
**B1 后端自动化测试为零,CI 门禁空转**
证据:`patbond-api` 无任何 `src/test` 目录;三个 pom 均无 `spring-boot-starter-test`
影响:第 9 节全部后端测试类别缺口 100%;`mvn clean test` 0 测试也通过,门禁无意义。第一迭代任务 5「注册、登录、刷新、退出和鉴权集成测试」(`development-plan.md:285`)从零开始。
**B2 认证纵切三件套全部缺位:内存用户 + 不可验证 token + `/internal` 裸奔**
证据:`UserService.java:21-23`ConcurrentHashMap 存储)、`AuthService.java:47-56`(随机 UUID 当 token)、`UserController.java:18``/internal/users` 无鉴权且经 `UserService.java:69-71` 返回手机号)。
影响:M1 全部四条验收标准(`development-plan.md:223`)当前均不满足;未鉴权的 `/internal` 是当前唯一对外可见的真实安全暴露面。
### Major
**M1 错误状态码在 auth→user 调用链上丢失(静态分析,需运行验证)**
证据:`AuthService.java:58-63` 将一切失败折叠为 400;user 侧用 `ResponseStatusException` 抛 409/401/404`UserService.java:29,48,56,64`),Feign 非 2xx 会抛异常绕过该分支。
影响:违反契约规范 `development-plan.md:171`;客户端无法区分「用户名已存在」「密码错误」。第一迭代任务 3 的统一异常响应必须覆盖此链路,验收时需用真实 curl 记录证明。
**M2 patbond-common 强制全体服务引入 AMQP/Feign/LoadBalancer/Nacos**
证据:`patbond-common/pom.xml:120-139``spring-boot-starter-amqp``spring-cloud-starter-openfeign``spring-cloud-starter-loadbalancer`、两个 nacos starter 全部为 compile 依赖)。
影响:与架构原则 `development-plan.md:79` 直接冲突;后续每个新业务模块都会被动携带消息队列与服务发现依赖。
**M3 两个代码仓库均无 CI 配置和 Maven Wrapper**
证据:`ls -a``.github`/`.gitlab-ci.yml`/`mvnw`
影响:第 9 节 CI 最低门禁(`development-plan.md:310-323`)没有执行载体,DoD 中「代码通过 CI」(`development-plan.md:345`)无法核查。
**M4 API Readme 启动步骤不完整,干净检出无法照做启动**
证据:`patbond-api/Readme.md:22-26``mvn compile`;配置复制步骤只存在于 `development-plan.md:127-138``.gitignore` 忽略 `application.yml` 且仓库只有 `.sample`
影响:违反 M0 验收「新机器仅依据仓库文档即可启动」(`development-plan.md:210`)。
**M5 数据库 bootstrap 单文件混装结构与开发凭据**
证据:`patbond_postgresql.sql:1266-1269`(明文声明 fixture 密码 `Patbond@123`)、1289-1293(三账号同 bcrypt 哈希)、约 1296-1308(固定 `refresh_token_hash` 与 JTI 的预置会话)。
影响:结构/种子未拆分(`development-plan.md:105` 要求拆开),一旦整文件被当生产 baseline 导入,即产生已知密码账号与可预测会话。风险 7(`development-plan.md:355`)确认属实。
### Minor
**m1 Flutter 唯一测试断言写死演示数据**
证据:`test/widget_test.dart:18-19` 断言 `'北京 · 朝阳区'``'28°C 晴'`,值来自 `lib/data/demo_data.dart:11-19`
影响:演示数据或默认城市一改,唯一的测试即挂;该测试对回归防护价值趋近于零。
**m2 Flutter README 校验命令与 CI 门禁不一致**
证据:`patbond-flutter/README.md:34``--output=none`,会改写文件;门禁版本在 `development-plan.md:317`
影响:开发者本机「校验」实际是格式化,CI(若建立)与本机行为不一致。
**m3 展示字符串与硬编码图片 URL 持久化在演示数据中**
证据:`lib/data/demo_data.dart:117,130,138,147,161`'2小时前' 等)、172,185,197,210,223'1.2km' 等)、22 处 Unsplash URL(见 2.4 节行号清单)。
影响:与 `development-plan.md:111` 及 DoD「不依赖 Demo 常量」冲突;属已知风险 4,逐页替换时必须清除,验收时应 grep 证明。
**m4 patbond-flutter 工作区有未提交改动**
证据:`git status --short`` M README.md`
影响:基线不干净,审计快照与远端不一致;开工前应提交或还原。
---
## 5. 第一迭代「登录纵切」验收证据清单
依据:第 8 节任务 1-8、M1 验收标准(`development-plan.md:223`)、第 9 节门禁、第 10 节 DoD。**没有下列证据即不通过验收,任何「已完成」的口头声明不作数。**
### 5.1 自动化测试输出(原始终端输出,不接受转述)
1. `mvn clean test` 完整输出:显示测试总数 > 0,且包含注册/登录/刷新/退出/鉴权的集成测试类名与用例数;Testcontainers 启动 PostgreSQL 16 的日志行可见。
2. 每个接口至少覆盖:成功、参数错误、资源不存在、无权限、并发冲突、幂等重试(`development-plan.md:300`)——以测试报告中的用例名逐条对应。
3. `dart format --output=none --set-exit-if-changed lib test``flutter analyze``flutter test` 三条命令的退出码为 0 的完整输出;`flutter test` 中包含登录页 widget 测试(loading/error/成功三态)。
4. `mkdocs build --strict` 成功输出(文档更新后)。
5. CI 运行链接或日志:以上门禁在 CI 中执行并全绿(M3 问题修复的证明)。
### 5.2 接口调用记录(curl/httpie 全文:请求 + 响应头 + 响应体)
按顺序一份完整 transcript
1. `POST /api/v1/auth/register` → 201/200,响应体含 UUID 格式 userId 与 `{code, message, data}` 包裹。
2. 重复用户名注册 → HTTP 409 + 稳定业务错误码(验证 M1 问题修复:不再折叠为 400/500)。
3. 错误密码登录 → HTTP 401 + 业务错误码。
4. `POST /api/v1/auth/login` 成功 → 含 access + refresh token。
5. `GET /api/v1/me` 带 token → 200;不带/伪造 token → 401(证明 token 可验证)。
6. `POST /api/v1/auth/refresh` → 新 token 对;随后用**旧 refresh token 重放** → 401(轮换生效)。
7. `POST /api/v1/auth/logout` → 成功;再用已撤销 refresh token → 401M1 验收「退出后 refresh token 不可再次使用」)。
8. 未携带服务间凭据直接调用 `/internal/users/{id}` → 被拒绝(401/403),对照当前裸奔状态(B2)。
### 5.3 数据库查询结果(psql 原始输出)
1. 注册后:`SELECT id, username, status FROM identity.users WHERE username='...'` 显示 UUID 主键行。
2. `SELECT hash_algorithm, left(password_hash, 7) FROM identity.user_credentials ...` 显示 bcrypt/argon2id 前缀——同时证明**非明文**。
3. 重启持久化证据:注册 → 服务重启(附带重启时间戳的服务日志)→ 登录成功 + 上述查询仍有该行(M1 验收「重启数据不丢失」,直接针对 B2 内存存储)。
4. refresh 轮换后:`SELECT ... FROM identity.auth_sessions` 显示旧会话 revoked/新会话 active。
5. `SELECT version, description, success FROM flyway_schema_history` 显示 identity/media baseline 迁移(任务 1),且在全新 PostgreSQL 16 实例执行过一次(`development-plan.md:325`)。
6. 生产 baseline 不含 fixture:对迁移产物 `grep -c "Patbond@123"` 为 0(针对 M5)。
### 5.4 界面截图(真机或模拟器,标注设备与时间)
1. 登录页:初始态、提交中 loading 态、错误态(错误密码后的可读提示)、成功跳转后首页。
2. 注册页同三态。
3. 登录态恢复:登录 → 完全杀掉 App → 重新打开直接进入已登录态(M1 验收),两张前后截图 + 中间的杀进程操作说明。
4. 退出登录后回到未登录态的截图。
5. 安全存储证据:代码评审指向 token 写入 secure storage 的调用点(文件+行号),并 `grep -rn "SharedPreferences" lib` 输出证明 token 未落入 `SharedPreferences``development-plan.md:97`)。
### 5.5 附加核查项(DoD
1. 日志片段 + `grep -inE "password|token" <日志文件>` 输出:证明日志不含密码与 token 全文(`development-plan.md:175,344`)。
2. OpenAPI 文件路径 + 契约测试输出(任务 6)。
3. 端到端用例(注册 → 登录 → 获取当前用户 → 退出,任务 8)的单次完整执行记录,与 5.2 的 transcript 可为同一份。
### 验收纪律
- 每条证据必须可复现:附命令、路径、时间。截图必须来自本次交付的构建,不接受历史截图。
- 声明「零问题」「production ready」而不附上述证据的交付,直接按 FAILED 处理并退回。
- 本报告第 4 节的 B1、B2 未关闭前,第一迭代不具备进入验收的资格。
---
## 附:本次审计执行的命令类别
`find`(文件清单)、`ls -a`CI/wrapper 探测)、`grep -rn`TODO/密钥/URL/展示字符串/测试依赖)、`cat`/`sed`(读源码与 SQL)、`wc -l`(体量)、`git status --short` / `git ls-files` / `git log --oneline`(只读仓库状态)。未修改任何被审计仓库的文件。
@@ -0,0 +1,101 @@
# 07 后端工程基线改造报告(第一迭代)
- 执行人:Senior Developer
- 日期:2026-09-03
- 仓库:`patbond-api`(改动全部留在工作区,未提交)
- 范围:ADR-001(升 Boot 3)、ADR-002(移除 Nacos)落地 + 审计问题 B1(零测试)、M2(common 依赖污染)、M3(无 Maven Wrapper)、M4(启动文档/配置)整改。不含数据库、JWT、refresh token、OpenAPI(后续工单)。
---
## 1. 所升版本
| 项 | 原值 | 新值 | 说明 |
| --- | --- | --- | --- |
| Spring Boot | 2.7.18 | **3.5.16** | 3.5 线最新补丁版(写作时 Maven Central 实查) |
| Spring Cloud | 2021.0.9 | **2025.0.3** | 与 Boot 3.5 配套版本线;仅使用 OpenFeign |
| Spring Cloud Alibaba | 2021.0.6.0 | **移除** | ADR-002BOM、两个 nacos starter、`spring.config.import` 全部删除 |
| Java 基线 | 17source/target | 17`<release>17</release>` + `<parameters>true</parameters>` | `release` 严格锁定 API 基线;`-parameters` 是 Boot 3.2+ 参数名推断的硬要求 |
| 命名空间 | `javax.validation` | `jakarta.validation` | 6 个源文件迁移,源码中已无任何 `javax.*` |
| 构建工具 | 依赖本机 mvn | **Maven WrappermvnwMaven 3.9.16** | `mvn wrapper:wrapper` 生成 |
本机默认 JDK 为 26,构建/运行统一以 `JAVA_HOME=/usr/lib/jvm/java-17-openjdk` 执行(README 已写明)。
## 2. 改动文件清单
**pom4 个,修改)**
- `pom.xml`Boot/Cloud 版本升级、删 Alibaba BOM、compiler 改 `release`+`parameters`、pin surefire 3.5.2。
- `patbond-common/pom.xml`**瘦身为纯契约模块**——只保留 `jakarta.validation-api`compile+ `spring-boot-starter-test`test);删除 web/amqp/openfeign/loadbalancer/nacos-discovery/nacos-config/hutool/lombokM2 关闭,对齐开发计划 4.1)。
- `patbond-user/pom.xml`:自持 `starter-web``starter-validation`(原经 common 传递),保留 `spring-security-crypto`,新增 `starter-test`
- `patbond-auth/pom.xml`:自持 `starter-web``starter-validation``spring-cloud-starter-openfeign`(不显式引 loadbalancer),新增 `starter-test`
**Java 源码(6 个,修改)**
- `patbond-auth/.../client/UserClient.java``@FeignClient(name = "patbond-user", url = "${patbond.user-service.url}")`ADR-002 静态地址)。
- `AuthController.java``LoginRequest.java``RegisterRequest.java``UserController.java``CreateUserRequest.java``VerifyPasswordRequest.java`javax→jakarta。
**配置(按用户中途指示采用 sample 模式:`application.yml` 保持 git 忽略,sample 入库)**
- 更新 `patbond-user/src/main/resources/application.yml.sample`(去 Nacos`PATBOND_USER_PORT:8082`)。
- 更新 `patbond-auth/src/main/resources/application.yml.sample`(去 Nacos`PATBOND_AUTH_PORT:8081``patbond.user-service.url: ${PATBOND_USER_SERVICE_URL:http://127.0.0.1:8082}`)。
- `.gitignore`:保留 `application.yml` 忽略 + `.sample` 白名单规则(去掉的是 Nacos 时代的内容而非规则本身)。
- 新增 `patbond-auth/src/test/resources/application.yml`(测试专用配置):干净检出无 `application.yml``@SpringBootTest` 仍可启动上下文,`./mvnw clean test` 不依赖复制步骤(已实测:移走本地 yml 后 clean test 依旧 BUILD SUCCESS)。
- 本机的两个真实 `application.yml` 保留在磁盘(被忽略,不入库),供本地 `spring-boot:run` 使用。
**测试(5 新增,共 21 个用例)**
- `patbond-common/src/test/.../ApiResponseTest.java`3)。
- `patbond-user/src/test/.../UserApplicationTests.java`context loads+ `UserControllerTest.java`(9:创建成功/重复 409/参数非法 400、verify-password 成功/错密码 401/未知用户 401、getById 200/404、getByUsername 200+404)。
- `patbond-auth/src/test/.../AuthApplicationTests.java`context loads+ `AuthControllerTest.java`7):`UserClient``@MockitoBean` 替换,不依赖 user 进程;按**现状行为**断言(业务失败折叠为 400、FeignException 未翻译逃逸 MVC 层)。
**其他**
- `Readme.md`:重写——技术栈更新、去 Nacos/RabbitMQ、Maven Wrapper 真实启动命令(含 sample→yml 复制步骤)、环境变量表、内存存储现状注记(M4 关闭)。
- 新增 `mvnw``mvnw.cmd``.mvn/wrapper/maven-wrapper.properties`
- 清理三个模块旧 `target/`(其中 auth/user 的 `target/classes/application.yml` 曾硬编码 `http://patbond.cn:8848/nacos`);全仓 grep 确认除测试注释中对 ADR-002 的引用外无任何 nacos/8848 残留。
## 3. 验收执行记录
### 3.1 `JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./mvnw clean test`
```
[INFO] Tests run: 3, ... -- in com.patbond.patbond.common.response.ApiResponseTest
[INFO] Tests run: 1, ... -- in com.patbond.patbond.user.UserApplicationTests
[INFO] Tests run: 9, ... -- in com.patbond.patbond.user.controller.UserControllerTest
[INFO] Tests run: 7, ... -- in com.patbond.patbond.auth.controller.AuthControllerTest
[INFO] Tests run: 1, ... -- in com.patbond.patbond.auth.AuthApplicationTests
[INFO] patbond-api ........................................ SUCCESS
[INFO] patbond-common ..................................... SUCCESS
[INFO] patbond-user ....................................... SUCCESS
[INFO] patbond-auth ....................................... SUCCESS
[INFO] BUILD SUCCESS
```
合计 **21 个测试,0 失败 0 错误**B1 的"0 测试空转"状态解除)。
首轮曾失败:`@PathVariable` 参数名反射不可用——Boot 3.2+ 行为变化,已在父 pom 加 `-parameters` 修复后全绿。
补充验证:临时移走两个本地 `application.yml`(模拟干净检出)后重跑 `clean test`,仍 BUILD SUCCESS——测试不依赖被忽略的本地配置。
### 3.2 干净配置启动验证(真实进程,验证后已全部停止)
`./mvnw -pl patbond-common install` 后两个服务分别 `spring-boot:run`
```
Tomcat started on port 8082 ... Started UserApplication in 1.691 seconds
Tomcat started on port 8081 ... Started AuthApplication in 1.789 seconds
```
端到端冒烟(auth 经静态 URL Feign 调 user):
```
POST /auth/register → {"code":0,...,"userId":1,"username":"smoketest",...}
POST /auth/login → {"code":0,...,"accessToken":"5c44a5d3...",...}
POST /auth/login(错密码) → HTTP 500 ← 已知问题,见 4.1
GET /internal/users/18082 → {"code":0,...,"username":"smoketest",...}
```
验证后进程已终止,`ss` 确认 8081/8082 端口释放,无遗留后台进程。
注意事项:单独 `-pl` 启动前必须先 `install` patbond-common,否则会从 `~/.m2` 拿到旧快照(首次启动失败正是踩到旧 common 里的 nacos 依赖);README 启动步骤已包含该命令。
## 4. 遗留问题(均为计划内后续工单,非本次回归)
1. **错误状态码折叠(审计 M1**user 返回的 401/409 经 Feign 变成 auth 侧 500/400。`AuthControllerTest.loginPropagatesFeignExceptionUnhandled` 已把现状钉死为基线,统一异常契约工单动工时该测试会按新契约改写。
2. **B2 三件套未动**:内存用户存储、不可验证 token、`/internal/**` 无访问控制——属数据库/JWT 后续工单,本次仅在 README 中如实标注。
3. **无 CI 配置(审计 M3 后半)**Wrapper 已就位,`./mvnw clean test` 已可作为门禁命令,但 CI 载体(如 GitHub Actions)仍缺。
4. `AuthTokenResponse.expiresAt` 仍为无时区 `LocalDateTime`,不符 ISO 8601 约定,随 token 重构一并处理。
5. 旧 common 快照仍在本机 `~/.m2`,已被本次 install 覆盖;其他开发机拉取后需重新 `./mvnw -pl patbond-common install`
@@ -0,0 +1,108 @@
# 08 · Flutter 主题迁移与登录组件实现报告
> 作者:Frontend Developer
> 日期:2026-09-03
> 依据:ADR-005patbond-doc/docs/architecture/decisions.md)、04-ui-login-design-spec.md
## 1. 完成内容
1. `lib/core/theme/app_theme.dart` 重写为语义 token 组织(`AppColors` + 新增 `AppRadius`),落地珊瑚橙正典色板;补齐 `filledButtonTheme`(高 52、圆角 16、`primaryStrong` 填充、显式 disabled 色)、`errorBorder` / `focusedErrorBorder` / `errorStyle``colorScheme.error` 覆盖。主题仍集中在单一文件。
2. 五个 Tab 页面及公共组件逐处替换旧靛蓝/冷灰硬编码色值为语义 token,仅换色、未动布局。
3. 新建 5 个可复用组件于 `lib/core/widgets/``BrandMark``AppTextField``PrimaryButton`(含 isLoading)、`InlineErrorBanner``AuthScaffold`。本次未建登录页面(等后端契约冻结)。
4.`PrimaryButton``AppTextField` 各写 3 个 widget 测试。
## 2. Token 映射表(旧 → 新)
### 2.1 AppColors 字段值变化(字段名不变,引用处无需改)
| Token | 旧值(脚手架靛蓝) | 新值(珊瑚橙正典) |
| --- | --- | --- |
| `primary` | `#4F46E5` | `#FF6F4C`(仅装饰/图标,不承载实心填充上的文字) |
| `canvas` | `#F8FAFC` | `#FFF7ED` 奶油底 |
| `ink` | `#0F172A` | `#3E2A1F` |
| `muted` | `#64748B` | `#9C8977` |
| `border` | `#E2E8F0` | `#F0DCC8` |
| `success` | `#10B981` | `#7FA88A` sage |
| `warning` | `#F59E0B` | `#F59E0B`(不变) |
### 2.2 新增 token
| Token | 值 | 用途 |
| --- | --- | --- |
| `primaryStrong` | `#D6431A` | 实心按钮填充、可点击文字链接(白字 ≈4.5:1,WCAG AA |
| `primaryDark` | `#7A2E12` | 浅色底上的强调文字 |
| `accent` / `accentDark` | `#FFB648` / `#7A4B0A` | 渐变终点、徽章 / amber 底文字 |
| `surface` | `#FFFFFF` | 卡片、输入框背景(原为字面量 `Colors.white` |
| `surfaceTint` | `#FFE8D6` peach | 图标底、占位块、选中指示 |
| `successInk` / `successSurface` | `#3F5744` / `#E8F0E8` | 成功提示的文字 / 底色 |
| `error` | `#D0342C` | 错误文字、错误描边(白底 ≈5.4:1,AA) |
| `brandGradient` | 135° `#FF6F4C → #FFB648` | 品牌渐变(装饰,不承载正文文字) |
| `AppRadius.sm/md/lg/xl/pill` | 12 / 16 / 18 / 24 / 999 | 圆角刻度(§1.3 |
### 2.3 页面内硬编码旧色值 → token
| 旧硬编码 | 新写法 | 位置 |
| --- | --- | --- |
| `#F1F5F9`(图片占位底) | `AppColors.surfaceTint` | common.dart ×2 |
| `#EEF2FF`indigo-50 图标底) | `AppColors.surfaceTint` | home ×2、profile |
| `#E0E7FF`(导航指示/渐变上副文字) | `AppColors.surfaceTint` / `Colors.white` | 主题、home 促销卡 |
| `[#4F46E5→#8B5CF6]``[primary→#7C3AED]` 渐变 | `AppColors.brandGradient` | home 故事环、促销卡 |
| `#ECFDF5→#FFF7ED` 问候卡渐变 | `surfaceTint → canvas` | home |
| `0x224F46E5` 装饰爪印 | `primary.withAlpha(34)` | home |
| `#475569`slate 次级文字/阴天色) | `AppColors.primaryDark` / `AppColors.ink` | home |
| `#64748B``#F59E0B`(多云/晴天) | `AppColors.muted` / `AppColors.warning` | home 天气色 |
| `0xCC0F172A` 图片压暗渐变 | `ink.withAlpha(204)` | create |
| `#E2E8F0`(scrim 上副文字/进度轨道) | `Colors.white70` / `AppColors.border` | create、pets |
| `#ECFDF5` / `#A7F3D0` / `#047857` 健康提醒卡 | `successSurface` / `success.withAlpha(140)` / `successInk` | pets |
| `#A5B4FC` / `#334155` / `#94A3B8`(深色卡上) | `AppColors.accent` / `Colors.white24` / `Colors.white70` | profile |
| 促销卡反白按钮前景 `primary` | `primaryStrong`AA | home |
| 导航选中 label `primary` | `primaryStrong`11px 文字保 AA | 主题 |
保留未动:天气雨/雪的功能性蓝色(`#0284C7``#0891B2`,非旧品牌色)、点赞红、卡片主题 `#F1F5F9` 描边改为 `AppColors.border`
## 3. 改动文件清单
修改(7):
- `patbond-flutter/lib/core/theme/app_theme.dart`(重写)
- `patbond-flutter/lib/widgets/common.dart`
- `patbond-flutter/lib/features/home/home_page.dart`
- `patbond-flutter/lib/features/create/create_page.dart`
- `patbond-flutter/lib/features/pets/pets_page.dart`
- `patbond-flutter/lib/features/profile/profile_page.dart`
- services / post_detail / main_shell 仅引用 token 字段,值随主题自动切换,无需改动)
新增(7):
- `patbond-flutter/lib/core/widgets/brand_mark.dart`
- `patbond-flutter/lib/core/widgets/app_text_field.dart`
- `patbond-flutter/lib/core/widgets/primary_button.dart`
- `patbond-flutter/lib/core/widgets/inline_error_banner.dart`
- `patbond-flutter/lib/core/widgets/auth_scaffold.dart`
- `patbond-flutter/test/core/widgets/primary_button_test.dart`
- `patbond-flutter/test/core/widgets/app_text_field_test.dart`
## 4. 验收命令输出摘要
```text
$ dart format --output=none --set-exit-if-changed lib test
Formatted 22 files (0 changed) in 0.15 seconds. # exit 0
$ flutter analyze
Analyzing patbond-flutter...
No issues found! (ran in 0.7s) # 无 error、无 warning
$ flutter test
00:01 +7: All tests passed! # 含原有导航冒烟测试,未改断言
```
## 5. 实现说明与遗留问题
1. **filledButtonTheme 的 minimumSize 用了 `Size(64, 52)` 而非规范建议的 `Size.fromHeight(52)`**:后者会给所有 FilledButton 无限最小宽度,令 Row 内既有按钮(首页促销卡「去使用」、服务页「立即预约」、对话框「确认恢复」)布局异常。改为仅约束高度 52;登录页的全宽由 `PrimaryButton` 自身 `width: double.infinity` 保证。副作用:既有小按钮从默认 40 高变为 52 高,视觉更厚重但协调,未破坏布局。
2. **PrimaryButton 的 loading 态**通过 `disabledBackgroundColor: primaryStrong` 保持珊瑚填充(规范 §4.3/§4.4:loading 不是置灰禁用态),已有测试锁定该行为。
3. **`ColorScheme.fromSeed` 仍以 `#FF6F4C` 为种子**Material 组件(Chip、SegmentedButton、tonal 按钮等)使用生成的暖色调色板,与正典色板协调但非逐一指定;`error` 已显式覆盖为 `#D0342C`。若后续设计对某组件色不满意,在主题内补对应 componentTheme 即可。
4. **天气语义色**:雨 `#0284C7`、雪 `#0891B2` 为功能色保留硬编码;阴天映射为 `ink`、多云映射为 `muted`,如需更细的天气色阶可后续补 token。
5. **品牌字体(Baloo 2 / 圆润中文标题体)未引入**,规范允许后置;`BrandMark` 字标暂用系统字体 32/w700。
6. **成功色 sage `#7FA88A` 作 11px TagPill 文字对比度偏弱**(约 2.4:1,沿用既有 TagPill 模式);正文类成功文字请用 `successInk`。此为设计规范自带取舍,未在本次擅改。
7. Splash / 登录 / 注册页、认证状态机、flutter_secure_storage 依赖均未实现,按计划等后端契约冻结后进行。
---
**Frontend Developer** · 2026-09-03
@@ -0,0 +1,149 @@
# 09 PM 任务板更新(第一迭代 · 第二轮开工)
> 作者:Senior Project Manager
> 日期:2026-09-04
> 依据:01-pm-task-breakdown.md(15 工单基线)、patbond-doc/docs/architecture/decisions.md(ADR-001~005,已 Accepted)、07-backend-baseline-report.md、08-flutter-theme-report.md
> 约定:状态口径为 Done / In Progress / Blocked / Not Started;"完成依据"必须指向具体报告章节,无证据不标 Done。
---
## 1. 15 工单状态总览
| 工单 | 名称 | 状态 | 完成依据 / 说明 |
| --- | --- | --- | --- |
| T0-1 | Maven Wrapper | **Done** | 报告 07 §2:`mvnw`/`mvnw.cmd`/wrapper 配置已生成(Maven 3.9.16),README 命令已改用 `./mvnw`;§3.1 全量 `./mvnw clean test` BUILD SUCCESS |
| T0-2 | Flutter/Dart 版本锁定 | **Not Started** | 报告 08 未涉及版本锁定文件;仍缺 FVM 或等效方案 |
| T0-3 | 默认配置与环境变量注入 | **Done(按修订口径)** | 报告 07 §2:执行中用户改令采用 **.sample 模式**(`application.yml` 保持忽略、sample 入库),原"可提交 yml"口径作废。Nacos 变量随 ADR-002 删除,新增 `PATBOND_USER_SERVICE_URL`;测试专用配置使干净检出 `clean test` 不依赖复制步骤(已实测)。残余:启动服务仍需一次 sample→yml 复制,README 已写明,接受为定稿方案 |
| T0-4 | 本地基础设施编排 | **Not Started(范围已修订)** | 见 §2 修订:Nacos 移出编排,只剩 PostgreSQL 16。本机 Docker 可用,自动化测试改走 Testcontainers,T0-4 降级为"手动联调/E2E 用编排",不再阻塞 T1/T5 |
| T0-5 | 契约规范冻结文档 | **Not Started** | 未有交付;注意 mkdocs 未安装,`mkdocs build --strict` 验收暂不可执行(见风险 R2) |
| T0-6 | CI 最低门禁 | **Blocked** | 门禁命令已具备(`./mvnw clean test``dart format`+`analyze`+`test` 均本地绿),但 **CI 载体缺失**且**三仓改动未提交**,无远端流水线可挂。解除条件:三仓完成首次提交并确定 CI 载体 |
| T1 | Flyway baseline(identity/media) | **In Progress** | 本轮第二波"后端持久化纵切"已启动(见 §3);依赖修订:验收环境由 T0-4 改为 Testcontainers |
| T2 | patbond-user 接 PostgreSQL + UUID | **In Progress** | 同上,持久化纵切范围内 |
| T3 | 统一异常响应与 DB 一致校验 | **In Progress** | 同上;描述按 ADR-004 修订(见 §2)。注意报告 07 §4.1:auth 侧错误折叠现状已被测试钉死,T3 落地时须同步改写 `loginPropagatesFeignExceptionUnhandled` |
| T4 | 可校验 access token + refresh 会话 | **Not Started** | ADR-003 已拍板参数,决策阻塞解除;ADR-001 落地后 JWT 选型无降级问题。第三波首项 |
| T5 | 认证链路集成测试 | **Not Started** | Testcontainers 路径已确认可行(Docker 可用);注册/登录持久化路径的测试可随第二波滚动补齐,完整验收仍等 T4 |
| T6a | OpenAPI 契约 | **Not Started** | D5 已由 ADR-004 拍板,登录方式字段无悬念;第三波第二项,冻结条件见 §3 |
| T6b | Flutter API Client | **Not Started** | 等 T6a 冻结 |
| T7 | Flutter 登录页 + 安全存储 + 登录态恢复 | **In Progress(前置件已交付)** | 报告 08 §1:5 个登录组件(BrandMark/AppTextField/PrimaryButton/InlineErrorBanner/AuthScaffold)+ 各 3 个 widget 测试已交付,`analyze`/`test` 全绿。**未动**:登录/注册页面、auth 状态机、flutter_secure_storage、拦截器(报告 08 §5.7,按计划等契约冻结) |
| T8 | E2E 用例 | **Not Started** | 全链路收口,等 T4/T5/T7 |
**新增工单(计划外,纳入任务板)**
| 工单 | 名称 | 状态 | 说明 |
| --- | --- | --- | --- |
| T9 | 珊瑚橙主题迁移(ADR-005) | **Done** | 原 15 工单外、ADR-005 新增范围。报告 08 §1-§4:语义 token 主题重写、五页替换、format/analyze/test 全绿。遗留取舍(sage 对比度、品牌字体后置等)已在报告 08 §5 如实记录 |
| T10 | UI 设计 QA(对照 04 号设计规范验收 T9/T7 组件) | **In Progress** | 本轮第二波启动 |
| T11 | 埋点实现规范(承接 05-experiment-tracking-plan) | **In Progress** | 本轮第二波启动 |
| T12 | 双重验证(复核 07/08 交付证据) | **In Progress** | 本轮第二波启动;其结论是第三波放行的前置之一 |
**完成率**:原 15 工单 Done 2(T0-1、T0-3)≈ **13%**;In Progress 4(T1/T2/T3/T7),Blocked 1(T0-6),Not Started 8。含新增工单口径:19 单中 Done 3 ≈ 16%,In Progress 7。第一波实际消化的还有两项不在工单编号内的大头:ADR-001 Boot 3 升级与 ADR-002 去 Nacos(报告 07 §1),它们是多个后续工单的解阻塞项,进度含金量高于百分比表象。
---
## 2. 受 ADR 影响的工单修订(以本节为准)
### T0-4 本地基础设施编排 —— 受 ADR-002
- **描述修订**:编排目标从"PostgreSQL 16 + Nacos"改为 **仅 PostgreSQL 16**。Nacos 相关的端口、健康检查、文档段落全部移出;开发计划 5.1-5.3 节涉及 Nacos 的内容以 ADR-002 为准不再执行。
- **验收标准修订**:
- 一条命令拉起 PostgreSQL 16,端口/库名/账号与 `.sample``PATBOND_DB_*` 默认值匹配(注意:配置采用 sample 模式,不再是原工单的"可提交 yml");
- 文档说明启动、停止、重置数据方式。
- **定位修订**:自动化测试(T1 迁移校验、T5 集成测试)一律走 Testcontainers,不依赖 T0-4;T0-4 只服务手动联调与 T8 E2E。它从关键路径前端移到 T7 联调之前完成即可。
### T0-3(已完成,记录口径变更)—— 受 ADR-002 + 执行中指示
- 原验收"无需手工复制 sample"作废,定稿为 sample 模式 + 测试专用配置兜底;`NACOS_SERVER_ADDR` 从环境变量表删除,新增 `PATBOND_USER_SERVICE_URL`(Feign 静态地址,ADR-002)。
### T3 统一异常响应 —— 受 ADR-004
- **描述修订**:唯一性校验范围收敛为 **用户名**(注册/登录仅用户名 + 密码)。手机号唯一性仅保留数据库约束(数据模型保留扩展能力),**不做**应用层手机号注册/登录校验流程,不做短信验证码相关错误码。
- **验收标准修订**:"重复用户名/手机号"改为"重复用户名";其余(规范错误体、并发重复注册测试、日志脱敏)不变。补充一条:改写报告 07 §4.1 钉死现状的 auth 侧测试,401/409 须正确穿透 Feign 传导(不得再折叠为 500/400)。
### T4 token/会话 —— 受 ADR-003 + ADR-001
- **描述修订**:删除"未拍板前按建议默认值"措辞。参数已定:access 15 分钟、refresh 30 天且每次刷新轮换、多设备并行、退出仅撤销当前会话;全部实现为配置项(ADR-003 原文要求)。JWT 依赖按 Boot 3.5 BOM 选型,无 2.7 降级顾虑。
- **验收标准补充**:配置项默认值与 ADR-003 数值一致并有测试锁定;`AuthTokenResponse.expiresAt``LocalDateTime` 改为带时区 ISO 8601(报告 07 §4.4 遗留,并入本单)。
### T6a OpenAPI 契约 —— 受 ADR-004 + ADR-003
- **描述修订**:删除"登录方式字段依赖决策 D5"。register/login 请求体定稿为用户名 + 密码两字段;不出现手机号登录、验证码、第三方登录端点。refresh/logout 语义按 ADR-003(轮换、仅撤销当前会话)编写。
- **验收标准补充**:若 mkdocs 仍未安装,"纳入文档站导航 + `mkdocs build --strict`"验收降级为"契约文件评审通过 + `mkdocs.yml` 导航条目已加(构建校验记入 R2 待补)",不阻塞冻结。
### T7 Flutter 登录页 —— 受 ADR-004 + ADR-005(轻微)
- **描述补充**:登录页表单仅用户名 + 密码;按 ADR-004 预留不渲染的凭证扩展区。UI 使用 T9 已交付的 5 个组件与珊瑚橙主题,以 04 号设计规范为验收基准(T10 的 QA 结论须先出)。
不受 ADR 影响、维持原文的工单:T0-1、T0-2、T0-5、T0-6、T1、T2、T5、T6b、T8(T1 仅依赖项由 T0-4 改为 Testcontainers,内容不变)。
---
## 3. 第二波执行映射与第三波启动条件
### 第二波(本轮已同时启动,四条并行线)
| 并行线 | 映射工单 | 交付物 |
| --- | --- | --- |
| 后端持久化纵切 | T1 + T2 + T3(串行纵切,一人连续负责) | Flyway baseline(identity/media)、UUID 用户持久化、统一异常契约;附 Testcontainers 下迁移一次成功 + 现有 21 测试改造后全绿 |
| UI 设计 QA | T10(验收 T9,兼查 T7 组件) | 对照 04 号规范的差异清单与放行结论 |
| 埋点实现规范 | T11 | 可实施的埋点/事件定义文档,供 T7 登录页开发时同步埋点 |
| 双重验证 | T12 | 对报告 07/08 声称结果的独立复核结论 |
第二波关键路径:**持久化纵切(T1→T2→T3)**,其余三线不占关键路径。
### 第三波(顺序:T4 JWT 会话 → T6a OpenAPI 冻结 → T6b/T7 Flutter 登录联调 → T5/T8 E2E)
各环节启动条件,满足即放行、不齐不开工:
1. **T4 JWT 会话** 启动条件:
- T1/T2 完成(identity 会话相关表已由迁移建立,用户持久化可用);
- T12 双重验证对报告 07 基线无否决性发现;
- (已满足)ADR-001 Boot 3 就位、ADR-003 参数拍板。
2. **T6a OpenAPI 冻结** 启动条件:
- T3 错误契约落地(错误体字段定型);
- T4 token 响应字段定型(含 ISO 8601 expiresAt);
- 起草可提前与 T4 并行,但**冻结**必须在上述两项之后。
3. **T6b + T7 联调** 启动条件:
- T6a 已冻结;
- T4 后端可运行且本地数据库路径打通(T0-4 编排完成,或本地 PostgreSQL 账号问题解决,二者其一);
- T10 UI QA 放行结论已出;T11 埋点规范可用(登录页开发同步埋点)。
4. **T5 收口 + T8 E2E** 启动条件:
- T4、T7 完成;
- T0-4 编排可用(E2E 必须真实编排,不能只靠 Testcontainers);
- **三仓已提交**且新增文档齐备(T8 验收含"新成员仅凭仓库文档复现",未提交的仓库谈不上复现)。
第三波前还应穿插两个小补课:T0-2(Flutter 版本锁定,S,随时可做)与 T0-6 解锁(首次提交 + CI 载体,见 R3/R4)。
---
## 4. 风险清单更新
### 已消除
| 原风险 | 消除依据 |
| --- | --- |
| Nacos 硬依赖导致干净检出无法启动/CI 无法跑 | ADR-002 + 报告 07:依赖、配置、旧 target 残留全清,grep 确认无 nacos/8848 残留 |
| Spring Boot 2.7 技术代际(原风险 8)拖累 JWT/Flyway/Testcontainers 选型 | ADR-001 + 报告 07 §1:已升 3.5.16 / Cloud 2025.0.3 |
| D3、D5 决策悬置阻塞 T4/T6a/T7 定稿 | ADR-003、ADR-004 已 Accepted |
| 零测试空转(审计 B1) | 报告 07 §3.1:21 测试全绿;报告 08:Flutter 7 测试全绿 |
| 干净检出测试不可运行 | 报告 07:测试专用配置,移走本地 yml 实测 clean test 仍绿 |
| 品牌色占位(靛蓝)与正典冲突 | ADR-005 + 报告 08 全量迁移 |
### 仍然存在 / 新增
| # | 风险 | 影响 | 缓解 / 责任 |
| --- | --- | --- | --- |
| R1 | 本地 PostgreSQL 无可用账号 | 手动联调、T7 联调、T8 E2E 无真实库可连 | 自动化侧已由 Testcontainers 兜住;联调前必须完成 T0-4 编排(Docker 可用)或申请到账号,列为 T7 启动条件 |
| R2 | mkdocs 未安装 | T0-5、T6a 的 `mkdocs build --strict` 验收无法执行,文档门禁缺一角 | 短期按 §2 降级验收;安装 mkdocs 列为文档线待办,补跑构建后风险关闭 |
| R3 | **三仓改动均未提交**(最高优先级) | 一波 + 二波两天工作量仅存在于工作区,误操作即全损;CI、协作、T8"可复现"验收全部无从谈起 | 本轮结束前完成三仓首次提交/分支提交;这是纯操作项,不需要任何新决策,建议第二波各线交付即提交 |
| R4 | CI 载体缺失 | T0-6 Blocked,门禁全靠本地人肉执行,回归防护为零 | 依赖 R3 解除;确定载体(如 GitHub Actions 或内部等价物)后按 T0-6 原验收落地 |
| R5 | auth 错误折叠现状被测试钉死(报告 07 §4.1) | 若 T3 改契约时漏改该测试,会出现"测试绿但契约错"的假象 | 已写入 T3 修订后验收标准 |
| R6 | 其他开发机 `~/.m2` 旧 common 快照(报告 07 §5) | 他人拉取后首次构建可能踩旧 nacos 依赖快照 | README 已含 `./mvnw -pl patbond-common install` 步骤;R3 解除后在提交说明中提示 |
| R7 | T0-2 Flutter 版本锁定缺失 | 多人开发时 SDK 漂移,analyze/test 结果不可比 | S 级工单,第三波前插队完成 |
---
## 5. 板面结论
- 第一波交付真实且质量可查:两份报告的验收命令输出齐全,无凭空声称。
- 第二波已把后端关键路径(T1→T2→T3)推入 In Progress,配套三条 QA/规范线并行,结构合理。
- 第三波唯一的硬闸门是 **T12 双重验证结论****持久化纵切完成**;流程性最大隐患是 **R3 三仓未提交**,应在任何新代码继续堆积前处理。
@@ -0,0 +1,116 @@
# 10 后端持久化与统一异常报告(第一迭代·第二波)
- 执行人:Senior Developer
- 日期:2026-09-04
- 仓库:`patbond-api`(改动全部留在工作区,未提交)
- 范围:开发计划第 8 节任务 1、2、3 —— Flyway baseline、用户 UUID 持久化、统一异常响应,以及配套 Testcontainers 集成测试。不含 JWT/refresh token、OpenAPI、`/internal` 鉴权、Flutter(后续工单)。
---
## 1. Flyway baseline(任务 1
### 迁移文件清单
| 文件 | 说明 |
| --- | --- |
| `patbond-user/src/main/resources/db/migration/V1__identity_media_baseline.sql` | 版本化 baseline:扩展 `pgcrypto`+`citext`schema `platform`(仅硬依赖:`set_updated_at()` 函数 + `regions` 表)、`identity` 全部 5 表(users、user_credentials、auth_sessions、user_addresses、user_preferences)、`media.assets`;全部 CHECK/UNIQUE 约束、部分索引、跨 schema 外键、updated_at 触发器,逐条对齐 bootstrap SQL |
| `patbond-user/src/main/resources/db/dev/afterMigrate__dev_seed.sql` | 开发种子(Flyway afterMigrate 回调),**默认不执行**——仅当 dev profile 把 `classpath:db/dev` 加入 `spring.flyway.locations` 时加载;内容只有 6 条 `platform.regions` 参考数据(幂等 `ON CONFLICT DO NOTHING` |
要点:
- 跨 schema 外键 `identity.users.avatar_asset_id → media.assets(id)` 按任务指示两 schema 一起 baseline 后原样保留。
- `identity.user_addresses`/`user_preferences` 外键引用 `platform.regions`,故 platform 以"最小硬依赖"方式进入 baseline(函数 + regions 表),`distance_km`、notifications、outbox 等均未纳入。
- **无任何 fixture 账号/凭证/预置会话**`grep -c "Patbond@123"` 对两个 SQL 文件均为 0(已实测)。bootstrap 里的 demo_user 三账号、预置 auth_session 刻意不迁移——开发环境请走真实注册接口造数。
- `auth_sessions` 表结构已就位但本波无代码读写,供下一波 refresh session 使用。
## 2. UUID 持久化(任务 2
`patbond-user` 从 ConcurrentHashMap 全面迁移到 PostgreSQL
- 新增 `user/support/UuidV7.java`:应用层 RFC 9562 UUIDv7 生成器(48 位毫秒时间戳 + 74 随机位),DB 的 `gen_random_uuid()` 保留为兜底默认值。
- 新增 `user/repository/UserRepository.java`JdbcClient 直写 `identity.users` + `identity.user_credentials`,软删行(`deleted_at IS NULL`)对所有读不可见;`created_at` 由 DB 默认值产生并 RETURNING 回带。
- `UserService` 重写:`createUser` 单事务插两表;密码 bcrypt`BCryptPasswordEncoder`);用户名/手机号唯一性**完全依赖数据库约束**,捕获 `DuplicateKeyException` 后按违反的约束名(`users_username_key` / `uq_users_phone`)翻译为 409 业务码;`verifyPassword` 对不存在的用户也做一次哑 hash 比对,避免时间侧信道暴露账号存在性。
- phone 校验对齐 DB `ck_users_phone``CreateUserRequest`common)与 `RegisterRequest`(auth) 均改为 `@Pattern("^\\+[1-9][0-9]{7,14}$")`E.164),审计 B4 关闭。
### ID 契约变更点(供 OpenAPI/Flutter 工单使用)
| 位置 | 旧 | 新 |
| --- | --- | --- |
| `UserProfile.id` | number (Long 自增) | **UUID 字符串**UUIDv7 |
| `VerifyPasswordResponse.userId` | number | UUID 字符串 |
| `AuthTokenResponse.userId`/auth/register、/auth/login 响应) | number | UUID 字符串 |
| `GET /internal/users/{id}` 路径参数 | Long | UUID;格式非法 → 400 + 40000 |
| `UserProfile.createdAt` | `LocalDateTime`(无时区) | `OffsetDateTime`ISO 8601 带偏移(对齐"timestamptz + ISO 8601 传输" |
| `phone`(注册/创建用户入参) | 任意 ≤20 字符 | 必须 E.164(`+8613800138000`),或不传 |
| `AuthTokenResponse.expiresAt` | `LocalDateTime` | **未改**——随下一波 token 重构一并处理(遗留 §5.2) |
## 3. 统一异常响应(任务 3
新增 `patbond-common/error/ErrorCode.java`(稳定业务码枚举)+ `BusinessException.java`(携带 code/httpStatus/message,可承载下游原样转发的任意码);`patbond-user``patbond-auth` 各一个 `GlobalExceptionHandler``@RestControllerAdvice`),响应维持 `{code, message, data}` 信封。
### 错误码表
| 业务码 | HTTP | 场景 |
| --- | --- | --- |
| 0 | 200 | 成功 |
| 40000 | 400 | 参数校验失败(含 JSON 不可解析、路径 UUID 非法;message 为首个字段错误) |
| 40100 | 401 | 用户名或密码错误 |
| 40400 | 404 | 用户不存在 / 资源不存在 |
| 40900 | 409 | 用户名已存在(citext,大小写不敏感) |
| 40901 | 409 | 手机号已被使用 |
| 50000 | 500 | 服务器内部错误(记日志,不外泄内部信息) |
| 50300 | 503 | 依赖服务暂不可用(Feign 传输层失败 / 下游响应非信封格式) |
### 错误码折叠修复(审计 M1
- auth 新增 `config/ApiErrorDecoder.java`(经 `FeignConfig` 注册为全局 ErrorDecoder):下游非 2xx 时解析 `{code, message}` 信封,**以原业务码 + 原 HTTP 状态**重新抛出 `BusinessException`——重复用户名注册经 auth 仍是 409/40900,错误密码登录仍是 401/40100,不再折叠为 400/500。
- 信封解析失败(HTML 网关页、空 body 等)→ 50300/503;连接被拒等传输层 `FeignException` 由 auth 的 handler 兜为 503,不再以 500 栈溢出到客户端。
- `AuthService.requireData` 的"一律 400"折叠逻辑废除,仅作为 2xx-但信封异常的防御性兜底(→ 50000)。
- 上一波钉现状的 `loginPropagatesFeignExceptionUnhandled` 测试按新契约改写(见 §4)。
## 4. 测试与验收执行记录(任务 4)
依赖:`spring-boot-starter-jdbc``flyway-core`+`flyway-database-postgresql``postgresql` 驱动;测试侧 `spring-boot-testcontainers` + Testcontainers `postgresql`/`junit-jupiter`(版本均由 Boot 3.5.16 BOM 管理)。user 模块所有 `@SpringBootTest` 通过 `TestcontainersConfiguration``@ServiceConnection`)对接一次性 postgres:16 容器,**未连接任何本地 PostgreSQL**。
`JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./mvnw clean test` 实际输出(关键行):
```
tc.postgres:16 : Container postgres:16 started in PT1.166108936S
o.f.core.internal.command.DbMigrate : Migrating schema "public" to version "1 - identity media baseline"
o.f.core.internal.command.DbMigrate : Successfully applied 1 migration to schema "public", now at version v1 (execution time 00:00.071s)
[INFO] Tests run: 3, ... -- in com.patbond.patbond.common.response.ApiResponseTest
[INFO] Tests run: 5, ... -- in com.patbond.patbond.user.persistence.UserPersistenceIntegrationTest
[INFO] Tests run: 1, ... -- in com.patbond.patbond.user.UserApplicationTests
[INFO] Tests run: 13, ... -- in com.patbond.patbond.user.controller.UserControllerTest
[INFO] Tests run: 3, ... -- in com.patbond.patbond.user.support.UuidV7Test
[INFO] Tests run: 8, ... -- in com.patbond.patbond.auth.controller.AuthControllerTest
[INFO] Tests run: 3, ... -- in com.patbond.patbond.auth.config.ApiErrorDecoderTest
[INFO] Tests run: 1, ... -- in com.patbond.patbond.auth.AuthApplicationTests
[INFO] patbond-common ..................................... SUCCESS
[INFO] patbond-user ....................................... SUCCESS [ 11.361 s]
[INFO] patbond-auth ....................................... SUCCESS
[INFO] BUILD SUCCESS
```
合计 **37 个测试(21 → 37),0 失败 0 错误**Flyway V1 在两个干净 postgres:16 容器上各自成功执行(user 模块两个测试上下文各起一容器),验证后由 Testcontainers/ryuk 自动回收,`docker ps` 无遗留容器、无遗留后台进程。
覆盖对照验收要求:
- **迁移在干净 postgres:16 可执行**:每个测试上下文启动即全量跑 V1(见上 Flyway 日志);`flywayBaselineAppliedOnCleanPostgres16` 断言 history 表 V1 成功 + 7 张目标表存在。
- **持久化/重启语义**`registeredUserIsDurablyStoredWithBcryptHash` 经 Service 注册后,用**全新原生 JDBC 连接**DriverManager 直连容器)读回该行——数据真实落库、任何重启后进程可见;断言密码为 `$2` bcrypt 且不含明文。
- **唯一约束生效**:重复用户名 409/40900(含大小写不敏感 citext 用例)、重复手机号 409/40901,均由 DB 约束触发。
- **接口层**:注册成功(UUID 断言)/参数错误 400/40000、非 E.164 手机号 400、错误密码 401/40100、未知用户 401、getById 404/40400、非法 UUID 400。
- **auth 转发不折叠**mock UserClient 抛 `BusinessException`(即 ErrorDecoder 的产物)→ 409/401 原样透出;ErrorDecoder 本体 3 个单元测试(信封透传/非信封 body/空 body);传输层 FeignException → 503/50300。
- **DB 兜底校验**:绕过 DTO 直插非 E.164 手机号被 `ck_users_phone` 拒绝(证明 DTO 与 CHECK 对齐且 DB 仍兜底)。
其余改动:`application.yml`(本地,仍 git-ignored)与 `application.yml.sample` 增加 datasource/flyway 配置及 dev-seed 开启方式说明;README 更新(Docker/Testcontainers 要求、DB 环境变量表、持久化现状注记)。
## 5. 遗留问题
1. **`/internal/**` 仍无访问控制**(审计 B2 之一):本波未动,属 `/internal` 鉴权工单;`UserProfile.phone` 仍会经该接口返回。
2. **token 仍为不可验证随机串**`AuthTokenResponse.expiresAt` 仍是无时区 `LocalDateTime`——两者随下一波 JWT/refresh session 工单处理(`identity.auth_sessions` 表已 baseline 就绪)。
3. **登录失败限制未实现**`user_credentials.failed_login_count/locked_until` 列已就位,逻辑留待 JWT 波或其后。
4. **ErrorDecoder 兜底语义**:下游返回非 Patbond 信封时统一报 50300/503(含理论上的非信封 4xx);两端 `GlobalExceptionHandler` 高度相似但因 common 是纯契约模块(无 spring-web)而各自持有,将来若出现第三个服务可考虑抽 patbond-web-starter。
5. **文档 SQL 观察**(不改 patbond-doc,仅记录):a) bootstrap 声称分阶段"identity/media → …",但 identity 对 `platform`regions 外键、set_updated_at 函数)有硬依赖,任何按域拆分的 baseline 都必须先带上 platform 最小集——本波已如此处理,后续 pet_health 等 baseline 同理;b) fixture 凭证 hash 为 `$2y$`PHP 风格 bcrypt),Spring 可校验但应用新产 hash 为 `$2a$`,如果未来有人把 fixture 账号导入开发库,两种前缀会并存(无功能影响)。
6. **本机 `~/.m2` 旧 common 快照**:本波 `clean test` 走 reactor 不受影响,但单模块 `spring-boot:run` 前仍需 `./mvnw -pl patbond-common install`(README 已写明,与上波结论一致)。
@@ -0,0 +1,57 @@
# 11 · 第一波交付独立复核(Reality Recheck
- 复核人:Reality Checker(独立验证,不采信报告自述)
- 日期:2026-09-04
- 复核对象:`07-backend-baseline-report.md``08-flutter-theme-report.md`
- 方法:patbond-api 完整复制到隔离 scratchpad 后独立构建(原目录零写入、零构建);patbond-flutter 原地只读执行三条验收命令;环境项逐一实测。
---
## 1. 后端声明复核(07 报告)
复制方式:`rsync -a --exclude='target/'` 至 scratchpad 副本;复制时刻原仓库 `git status` 为干净(见 §3 环境变化)。
| # | 声明 | 结论 | 证据 |
| --- | --- | --- | --- |
| B1 | Spring Boot 3.5.16 | **CONFIRMED** | 副本 `pom.xml:26` `<spring-boot.version>3.5.16</spring-boot.version>``pom.xml:27` `<spring-cloud.version>2025.0.3</spring-cloud.version>` 亦符 |
| B2 | 无 Nacos 残留(除注释) | **CONFIRMED** | 全仓 `grep -rni nacos` 仅命中 2 处测试注释(`UserApplicationTests.java:12``AuthApplicationTests.java:13`,均为 ADR-002 说明);`grep -rn 8848` 零命中 |
| B3 | `./mvnw clean test` 21 测试全绿 | **CONFIRMED** | 副本内 `JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./mvnw clean test`ApiResponseTest 3 + UserApplicationTests 1 + UserControllerTest 9 + AuthControllerTest 7 + AuthApplicationTests 1 = **210 失败 0 错误,BUILD SUCCESS**(日志:scratchpad/mvn-run1.log |
| B4 | 干净检出(无本地 application.yml)测试自足 | **CONFIRMED** | 移走副本中 user/auth 两个 `src/main/resources/application.yml` 后重跑 `clean test`:同样 21 全绿 BUILD SUCCESS(日志:scratchpad/mvn-run2-clean-checkout.log)。支撑点:`patbond-auth/src/test/resources/application.yml` 已入库(`git ls-files` 确认),user 模块上下文可零配置启动 |
| B5 | 附带核对:javax→jakarta、common 瘦身、-parameters | **CONFIRMED** | 源码 `grep javax.` 零命中;`patbond-common/pom.xml``jakarta.validation-api` + `spring-boot-starter-test` 两个依赖;父 pom `<release>` + `<parameters>true</parameters>` 在位 |
**注**:07 报告写"改动全部留在工作区,未提交",现原仓库已有提交 `c7ddaec 重构底层框架`Lixi202026-09-03 17:59)把这批基线改动入库——属报告撰写后的后续动作(原目录另有开发 agent 在续作),不构成报告失实,但"未提交"的描述已过时。
## 2. Flutter 声明复核(08 报告)
原地只读验证(本轮该仓库无并发修改方)。
| # | 声明 | 结论 | 证据 |
| --- | --- | --- | --- |
| F1 | `dart format --set-exit-if-changed lib test` 零改动 | **CONFIRMED** | 实测输出 `Formatted 22 files (0 changed)`exit 0 |
| F2 | `flutter analyze` 无问题 | **CONFIRMED** | 实测 `No issues found! (ran in 0.8s)`exit 0 |
| F3 | `flutter test` 7 个全绿 | **CONFIRMED** | 实测 `00:02 +7: All tests passed!`;用例分布核对:primary_button_test 3 + app_text_field_test 3 + widget_test 1 = 7,与报告"各写 3 个 widget 测试"一致 |
| F4 | 主题为珊瑚橙语义 token | **CONFIRMED** | `lib/core/theme/app_theme.dart``primary=#FF6F4C`:7)、`primaryStrong=#D6431A`:10)、`accent=#FFB648`:16)、`error=#D0342C`:51)、`AppRadius`(:62)均在位;全 lib/ 无旧靛蓝(4F46E5/8B5CF6/7C3AED)残留;5 个新组件文件齐全 |
| F5 | 未改 README | **REFUTED(轻微)** | `git diff README.md` 显示 README.md 有 1 行插入:运行步骤中新增 `flutter clean`。改动无害且可能确有必要,但与"未改 README"的声明不符,且未列入报告的改动文件清单 |
## 3. 环境复核
| 项 | 状态 | 证据 |
| --- | --- | --- |
| Docker | **可用**daemon 运行,0 容器) | `docker info`Client 29.7.2Server Containers: 0 |
| 本地 PostgreSQL | **仍无凭据**(服务在 5432 监听但拒绝无密码连接) | `psql -h localhost -U postgres``fe_sendauth: no password supplied`,与迭代启动时记录一致 |
| mkdocs | **仍缺失** | `which mkdocs` 无结果,命令未找到 |
| JDK 17 | **有效** | `/usr/lib/jvm/java-17-openjdk/bin/java -version` → OpenJDK 17.0.20.1 |
| patbond-api 仓库状态变化 | 基线改动已被提交(`c7ddaec 重构底层框架`),复制时刻工作树干净;另一开发 agent 续作中 | `git log`/`git status`(只读) |
## 4. 清理与合规确认
- patbond-api 原目录:只读操作(git status/log/ls-files/grep),零写入、零构建。
- 副本保留在 scratchpad`patbond-api-copy/`,含移出的两个 yml 于 `stashed-yml/`),构建日志同目录。
- 无本人遗留进程:8081/8082 端口空闲;ps 中仅另一用户(lx)的 IDE dart 守护进程,与本次验证无关。
- 未做任何 commit。
## 5. 总体结论
**两份报告的核心验收声明全部经独立复现属实**(后端 5/5 CONFIRMEDFlutter 4/5 CONFIRMED),唯一不符项是 Flutter 报告的"未改 README"(实际多了一行 `flutter clean`,REFUTED 但影响轻微)。此外 07 报告"未提交"的状态描述已因后续提交过时。
**状态判定:本波交付的自述可信度通过复核。** 但按流程提醒:本复核只验证了报告声称的范围(构建/静态检查/单测),未覆盖端到端运行时行为(07 报告 §3.2 的冒烟仅采信其自述未复现,因禁止在原目录起服务且副本起服务超出本次授权范围);07 报告自列的遗留问题(错误码折叠、B2 三件套、无 CI)仍然在册,整体仍处 **NEEDS WORK(计划内迭代中)**,与项目自身定位一致。
@@ -0,0 +1,347 @@
# 12 · UI 设计还原度 QA 与登录页组装稿
> 作者:UI Designer
> 日期:2026-09-04
> 依据:04-ui-login-design-spec.md(下称「04 规范」)、08-flutter-theme-report.md、`AI宠物_iOS_UI设计稿.html`(品牌正典)、`patbond-flutter` 工作区实现代码(未提交 diff)
> 性质:只读 QA + 组装级设计稿;需修正项由后续工单执行,本轮不改代码
---
## 1. QA 总结论
实现质量高,还原度好于预期。逐项对照后:
- **需修正:2 项**(1 项视觉回归 + 1 项主题遗漏),另有 1 项既有设计债列入规范修订。
- **可接受偏离:3 项**(含已声明的 `minimumSize`,裁决为采纳并正式写入规范)。
- **最严重项**:首页促销卡换用 `brandGradient` 后,白色文字对比度从旧靛蓝渐变的 ≥4.5:1 跌至 1.752.75:1,是本次换色**新引入**的可读性回归(见 §3 FIX-1)。
- 色值零误差:`app_theme.dart` 全部 token 与 04 规范 §1.1 及 HTML 正典逐字节一致(用正典 HTML 提取的 19 个 hex 值交叉验证)。
- 5 个登录组件的构造参数足以照 §5 组装稿直接拼装登录/注册/Splash,无需先改组件(一处失焦校验用 `Focus` 包裹解决,见 §5.4)。
---
## 2. 设计 QA 偏差清单
### 2.1 主题 token`lib/core/theme/app_theme.dart`
| # | 检查项 | 规范值 | 实现值 | 判定 |
| --- | --- | --- | --- | --- |
| T1 | `primary` | `#FF6F4C` | `0xFFFF6F4C`,注释明确"不用于承载文字的实心填充" | 符合 |
| T2 | `primaryStrong` / `primaryDark` | `#D6431A` / `#7A2E12` | 一致 | 符合 |
| T3 | `accent` / `accentDark` | `#FFB648` / `#7A4B0A` | 一致 | 符合 |
| T4 | `canvas` / `surface` / `surfaceTint` | `#FFF7ED` / `#FFFFFF` / `#FFE8D6` | 一致;`scaffoldBackgroundColor: canvas` | 符合 |
| T5 | `ink` / `muted` / `border` | `#3E2A1F` / `#9C8977` / `#F0DCC8` | 一致 | 符合 |
| T6 | 成功色族 | `success #7FA88A` / `successInk #3F5744` / `successSurface #E8F0E8` | 一致(`successInk`/`successSurface` 为合理的 token 化拆分) | 符合 |
| T7 | `error` | `#D0342C` | 一致,且 `colorScheme.error`/`onError` 显式覆盖 | 符合 |
| T8 | `brandGradient` | 135° `#FF6F4C → #FFB648` | `topLeft → bottomRight`(即 135°),色一致 | 符合 |
| T9 | 圆角 token | 12/16/18/24/999 | `AppRadius.sm/md/lg/xl/pill` 一致 | 符合 |
| T10 | 字号层级 | 22/w800、18/w800、15/w700、14/1.5、12/1.4 muted | `textTheme` 一致;输入框内文字走 M3 `bodyLarge` 默认 16,满足 §1.2"输入框内 16" | 符合 |
| T11 | `filledButtonTheme` | 高 52、圆角 16、`primaryStrong` 填充、白字 15/w700、disabled = onSurface 12%/38% | 全部落实;disabled 用 `ink.withAlpha(31/97)`=12.2%/38%,以 ink 代 onSurface 更贴暖色系) | 符合 |
| T12 | `minimumSize` | 规范建议 `Size.fromHeight(52)` | `Size(64, 52)`(已声明偏离) | **可接受偏离,采纳为规范修订值**(裁决见 §2.4 |
| T13 | `errorBorder` / `focusedErrorBorder` / `errorStyle` | error 1.5px / error 1.5px / 12px error 色 | 全部一致 | 符合 |
| T14 | `helperStyle` | §3.1 注册页 helperText"12 muted" | **未定义**,将回落到 M3 默认(bodySmall + 种子生成的 onSurfaceVariant,非 `muted #9C8977` | **需修正**FIX-2,一行改动) |
| T15 | `ColorScheme.fromSeed` 派生组件色 | 规范未逐一指定 | Chip/SegmentedButton 等用种子生成暖调色板 | 可接受偏离(登录纵切不涉及;哪个组件刺眼就补哪个 componentTheme,不整体重调) |
| T16 | 品牌字体 Baloo 2 / 圆润中文标题体 | 规范允许后置 | 未引入,`BrandMark` 用系统字体 32/w700 | 可接受偏离(04 规范 §1.2 原文允许) |
### 2.2 五个组件(`lib/core/widgets/`
| # | 组件 · 检查项 | 判定 | 说明 |
| --- | --- | --- | --- |
| C1 | `AppTextField` 错误态 | 符合 | `errorText``InputDecoration` 原生渲染(TalkBack/VoiceOver 自动关联),描边走主题 errorBorder;有测试 |
| C2 | `AppTextField` 禁用态 | 符合 | `enabled=false` 时整体 `Opacity 0.6`,对齐 §4.1"60% 不透明度" |
| C3 | `AppTextField` 密码切换 | 符合 | `obscurable` 默认遮蔽;切换按钮 `constraints 44×44`、带中文 tooltip、禁用时同步禁用;有测试锁定切换行为 |
| C4 | `PrimaryButton` loading | 符合 | 20×20 白圈 strokeWidth 2.5、外层 SizedBox 锁 52 高尺寸不变、`onPressed` 置 null 锁点击、`disabledBackgroundColor: primaryStrong` 保持珊瑚填充(§4.3"loading 不是置灰"),测试逐项锁定 |
| C5 | `PrimaryButton` 禁用 | 符合 | `onPressed: null` 走主题 disabledink 12%/38%),符合 §4.4 |
| C6 | `InlineErrorBanner` | 符合 | error 底 `withAlpha(20)`=7.84%,即 8%×255=20.4 的正确取整)、`radiusSm`、padding 12、`error_outline` 18 + 13px error 字、全宽 |
| C7 | `BrandMark` | 符合 | logo 72(可参数化)+ brandGradient `radiusXl` 圆底 + `Icons.pets` 占位(§2.1 允许 v1 替代);字标 32/w700 primaryslogan 14 muted 可传 null——正好满足 Splash/登录共用与 §5.2 过渡前提。字标 primary 于 canvas 上约 2.6:1,属品牌字标(logo 豁免),与规范原文一致 |
| C8 | `AuthScaffold` | 符合,附组装约束 | SafeArea + 滚动 + 水平 padding 24 + Scaffold 默认键盘避让。注意:`ConstrainedBox` 只给 **minHeight**,子 Column 高度仍无上界,**页面内不能用 `Spacer`/`Expanded` 做弹性空间**(会布局异常),垂直居中必须用 `mainAxisAlignment.center` + 固定间距——§5 组装稿已按此约束编写,不算缺陷 |
| C9 | 组件缺口:`AppTextField` 未暴露 `focusNode`/`validator` | 可接受 | 失焦校验用外层 `Focus(onFocusChange:)` 包裹即可(见 §5.4),v1 不需改组件;若后续表单变多,建议加 `focusNode` 参数(建议级,非工单) |
### 2.3 需修正项汇总(供开工单)
| 编号 | 严重度 | 位置 | 问题 | 修正建议 |
| --- | --- | --- | --- | --- |
| **FIX-1** | 高(视觉回归) | `lib/features/home/home_page.dart` `_PromoCard`(约 619 行起) | 促销卡底从旧靛蓝深色渐变换成 `brandGradient`(亮橙→亮琥珀)后,卡上 17px/w800 白色标题与 12px 白色副文字对比度仅 2.75:1primary 端)至 1.75:1accent 端),17px 加粗未达 large-text 门槛(18.7px),全部不达 AA。这也违反 04 规范 §1.1 自己的约定"brandGradient 不承载正文文字"。换色前白字在靛蓝上是达标的,属**本次迁移新引入的回归** | 促销卡渐变改为 `LinearGradient(colors: [AppColors.primaryStrong, AppColors.primary])`(文字在左侧 primaryStrong 端,白字 4.5:1 达标;右侧 primary 端只放"去使用"白底按钮与装饰)。备选:整卡 `primaryStrong` 纯色 + accent 装饰爪印。`brandGradient` 本身不动,故事环等纯装饰用法不受影响 |
| **FIX-2** | 低(一行) | `app_theme.dart` `inputDecorationTheme` | 缺 `helperStyle`,注册页密码 helperText 颜色将是种子派生灰而非 `muted` | 补 `helperStyle: TextStyle(color: AppColors.muted, fontSize: 12)` |
| **DEBT-1** | 低(既有设计债,不阻塞纵切) | `lib/widgets/common.dart` `TagPill` | 11px/w700 文字直接用传入色:默认 `primary`2.75:1)与服务页 `success` sage(约 2.4:1)都不达标。08 报告第 6 条已指出 sage 一例并正确地未擅改——责任在规范本身,本报告在此修订:**TagPill 应"底用 color 8%、文字用配套深变体"**primary→`primaryDark`、success→`successInk`、accent→`accentDark` | 给 `TagPill` 增加可选 `inkColor` 参数(默认按上述映射),另开工单,与登录纵切解耦 |
### 2.4 已声明偏离的设计裁决:`minimumSize: Size(64, 52)`
**裁决:采纳,并以 `Size(64, 52)` 作为规范修订值**04 规范 §6.1 建议的 `Size.fromHeight(52)` 作废)。理由:
1. `Size.fromHeight(52)` 展开为 `Size(double.infinity, 52)`,会给所有 `FilledButton` 无限最小宽度,Row 内既有按钮(首页"去使用"、服务页"立即预约"、对话框"确认恢复")必然布局异常——Frontend 的判断正确,`64` 恰是 Material 默认最小宽,语义上等于"只约束高度"。
2. 登录页全宽诉求本就该由 `PrimaryButton``width: double.infinity` 承担(组件职责),而非全局主题(全局职责)。实现的分工比规范原稿更对。
3. 副作用(既有小按钮 40→52 高)方向正确:52 高触控目标优于 40(§6.4 要求 ≥44)。若后续对话框内按钮显厚重,方案是补一个 compact 变体或对话框内改 `TextButton`**不回退全局 52**。
---
## 3. 五个 Tab 换色抽查
抽查方式:全量 grep 硬编码色值 + 逐页 diff 复核 + 未列入 diff 的三个文件(services / post_detail / main_shell)的 token 引用核对。
**残留硬编码:仅 2 处,均为已声明保留的功能色**——`home_page.dart:536``#0284C7``:538``#0891B2`。旧靛蓝/slate/indigo 系(`#4F46E5``#EEF2FF``#475569``#E2E8F0``Colors.indigo*` 等)全量清零。
逐页视觉协调性结论:
| 页面 | 结论 |
| --- | --- |
| home | 问候卡 `primaryDark` 12px/w600 于 `surfaceTint→canvas` 渐变上约 7:1,达标;装饰爪印 `primary.withAlpha(34)` 语义对;天气"多云→muted、阴天→ink"暖灰化协调(阴天用深棕做图标色略重,可接受);**促销卡见 FIX-1**;促销卡反白按钮前景改 `primaryStrong` 是正确的 AA 修正 |
| create | 图片压暗 `ink.withAlpha(204)` + scrim 上 `Colors.white70` 副文字,协调达标 |
| pets | 健康提醒卡 `successSurface` 底 + `successInk` 文字 + `success.withAlpha(140)` 描边,是成功色族的标准用法;进度环轨道 `border` 对 |
| profile | 深色头卡背景随 `ink` 自动变暖深棕,卡上白字 / `accent` amber 徽语 / `Colors.white70` 统计 label / `Colors.white24` 分隔线,全部协调达标(amber `#FFB648``#3E2A1F` 上约 7:1)——这是"深色底"抽查重点,无深字深底问题 |
| services | ⭐ 评分徽章白底 + ink 字达标;"认证服务" `TagPill(success)` 归 DEBT-1`Colors.white.withAlpha(235)` 徽章底为合理 scrim |
| main_shell / post_detail | 仅引用 token 字段,随主题自动切换;导航栏 `white.withAlpha(245)``surface` 一致;点赞/收藏 `primary` 图标为装饰性着色,达标豁免 |
---
## 4. 组装总则(三页共用)
- **垂直弹性**`AuthScaffold` 内禁用 `Spacer`(§2.2 C8)。登录页居中 = `Column(mainAxisAlignment: MainAxisAlignment.center)` + 首尾 `SizedBox`;注册页顶部对齐 = 默认 `MainAxisAlignment.start`
- **提交中整表单锁定**`_submitting == true` 时所有 `AppTextField.enabled = false`、切换链接 `onPressed = null``PrimaryButton.isLoading = true`
- **错误三层模型**(对齐 04 规范 §4.2,全 app 后续网络页面沿用):
| 层 | 触发 | 呈现 | 组件 |
| --- | --- | --- | --- |
| 字段级 | 本地校验失败;服务端 **409/422** 可归属字段的冲突(用户名已存在 / 手机号已注册) | 对应 `AppTextField.errorText`,该字段 `onChanged` 即清除 | `AppTextField` |
| 表单级 | **401**"用户名或密码错误"、**429**"尝试次数过多,请稍后再试"等无法归属字段的业务错误 | 主按钮上方横幅,出现时 `SemanticsService.announce(message, TextDirection.ltr)` 播报;任一字段 `onChanged` 即清除 | `InlineErrorBanner` |
| 瞬态 | 超时、断网 | floating SnackBar"网络异常,请检查网络后重试"+ action"重试"(重放 `_submit` | `ScaffoldMessenger` |
- 任何服务端异常文本/错误码不直接透出,一律映射为上表文案。
- 提交成功后调 `TextInput.finishAutofillContext()`(触发系统保存密码),随认证状态机 300ms fade 进 `MainShellPage`
## 5. 登录页组装稿(`lib/features/auth/login_page.dart`
页面状态:`_accountCtrl``_passwordCtrl``_accountError``_passwordError``_formError`String?)、`_submitting`bool)。
```dart
AuthScaffold( // 无 appBar
child: AutofillGroup(
child: Column(
mainAxisAlignment: MainAxisAlignment.center, // 垂直居中,禁 Spacer
children: [
const SizedBox(height: 48), // §2.1 顶部最小留白
const BrandMark(), // 默认 size 72 + 默认 slogan
const SizedBox(height: 48),
Focus( // 失焦校验,见 §5.4
onFocusChange: (has) { if (!has) _validateAccountOnBlur(); },
child: AppTextField(
label: '用户名 / 手机号',
controller: _accountCtrl,
prefixIcon: Icons.person_outline_rounded,
errorText: _accountError,
enabled: !_submitting,
keyboardType: TextInputType.text,
textInputAction: TextInputAction.next,
autofillHints: const [AutofillHints.username],
onChanged: (_) => _clearErrors(field: Field.account), // 清本字段 + 表单级
),
),
const SizedBox(height: 16),
Focus(
onFocusChange: (has) { if (!has) _validatePasswordOnBlur(); },
child: AppTextField(
label: '密码',
controller: _passwordCtrl,
prefixIcon: Icons.lock_outline_rounded,
obscurable: true,
errorText: _passwordError,
enabled: !_submitting,
textInputAction: TextInputAction.done,
autofillHints: const [AutofillHints.password],
onChanged: (_) => _clearErrors(field: Field.password),
onSubmitted: (_) => _submit(), // 键盘 done 直接提交
),
),
if (_formError != null) ...[
const SizedBox(height: 16),
InlineErrorBanner(message: _formError!),
],
const SizedBox(height: 24),
PrimaryButton(label: '登录', isLoading: _submitting, onPressed: _submit),
const SizedBox(height: 16),
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
const Text('还没有账号?',
style: TextStyle(color: AppColors.muted, fontSize: 14)),
TextButton( // 点击区 ≥44 高由 minimumSize 保证
style: TextButton.styleFrom(
foregroundColor: AppColors.primaryStrong,
minimumSize: const Size(44, 44),
padding: const EdgeInsets.symmetric(horizontal: 8),
textStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w700),
),
onPressed: _submitting ? null : _goRegister, // push RegisterPage
child: const Text('立即注册'),
),
]),
const SizedBox(height: 24), // 底部留白
// 协议行:v1 协议页未就绪,整行不渲染(§2.2)
// 预留区(短信验证码/第三方登录):不渲染任何占位(§2.4 / DoD)
],
),
),
)
```
校验与提交:两字段仅做**非空**校验(去首尾空格),失焦(曾聚焦过才触发)+ 提交时各校验一次;不做格式强校验。`_submit()`:先总校验,有字段错即 return;置 `_submitting`,调 `POST /auth/login`;结果映射按 §4 表(登录页无 409 场景,401/429 → 横幅)。
## 6. 注册页组装稿(`lib/features/auth/register_page.dart`
字段按 **ADR-004:仅用户名 + 手机号 + 密码 + 确认密码**;短信验证码行、第三方登录预留区一律不渲染。
```dart
AuthScaffold(
appBar: AppBar( // 透明返回栏(§3.1
backgroundColor: Colors.transparent,
elevation: 0,
scrolledUnderElevation: 0,
foregroundColor: AppColors.ink, // 返回箭头用 ink
),
child: AutofillGroup(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, // 顶部左对齐
children: [
const SizedBox(height: 8),
Text('创建账号', style: Theme.of(context).textTheme.headlineSmall),
const SizedBox(height: 8),
Text('加入 Patbond,记录毛孩子的每一天',
style: Theme.of(context).textTheme.bodySmall),
const SizedBox(height: 32),
// 四个字段统一模式:Focus 包裹做失焦校验,间距 16
Focus(onFocusChange: (has) { if (!has) _validateUsername(); },
child: AppTextField(
label: '用户名',
controller: _usernameCtrl,
prefixIcon: Icons.person_outline_rounded,
errorText: _usernameError,
enabled: !_submitting,
textInputAction: TextInputAction.next,
autofillHints: const [AutofillHints.newUsername],
onChanged: (_) => _clearError(Field.username),
)),
const SizedBox(height: 16),
Focus(onFocusChange: (has) { if (!has) _validatePhone(); },
child: AppTextField(
label: '手机号',
controller: _phoneCtrl,
prefixIcon: Icons.phone_iphone_rounded,
errorText: _phoneError,
enabled: !_submitting,
keyboardType: TextInputType.phone,
textInputAction: TextInputAction.next,
autofillHints: const [AutofillHints.telephoneNumber],
onChanged: (_) => _clearError(Field.phone),
)),
const SizedBox(height: 16),
Focus(onFocusChange: (has) { if (!has) _validatePassword(); },
child: AppTextField(
label: '密码',
controller: _passwordCtrl,
prefixIcon: Icons.lock_outline_rounded,
obscurable: true,
errorText: _passwordError,
helperText: '密码 832 位,需包含字母和数字', // 出错时被 errorText 替换
enabled: !_submitting,
textInputAction: TextInputAction.next,
autofillHints: const [AutofillHints.newPassword],
onChanged: (_) => _clearError(Field.password),
)),
const SizedBox(height: 16),
Focus(onFocusChange: (has) { if (!has) _validateConfirm(); },
child: AppTextField(
label: '确认密码',
controller: _confirmCtrl,
prefixIcon: Icons.lock_outline_rounded,
obscurable: true,
errorText: _confirmError,
enabled: !_submitting,
textInputAction: TextInputAction.done,
autofillHints: const [AutofillHints.newPassword],
onChanged: (_) => _clearError(Field.confirm),
onSubmitted: (_) => _submit(),
)),
if (_formError != null) ...[
const SizedBox(height: 16),
InlineErrorBanner(message: _formError!),
],
const SizedBox(height: 32),
PrimaryButton(label: '注册', isLoading: _submitting, onPressed: _submit),
const SizedBox(height: 16),
Center(child: Row(mainAxisSize: MainAxisSize.min, children: [
const Text('已有账号?',
style: TextStyle(color: AppColors.muted, fontSize: 14)),
TextButton(/* 同登录页样式 */,
onPressed: _submitting ? null : () => Navigator.of(context).pop(),
child: const Text('直接登录')),
])),
const SizedBox(height: 24),
],
),
),
)
```
校验规则(失焦即校验、提交再总校验,文案照 04 规范 §3.2):用户名非空 + 3–20 位字母开头字母/数字/下划线;手机号 `^1\d{10}$`;密码 8–32 位含字母和数字;确认密码与密码一致(密码字段变更时若确认已有值也重校验一致性)。
**409 映射(字段级)**:用户名冲突 → `_usernameError = '该用户名已被使用'`;手机号已注册 → `_phoneError = '该手机号已注册,可直接登录'`(v1 文案自带出路,"去登录"链接可后置)。401 在注册页理论上不出现,其余不可归属错误 → 横幅;网络 → SnackBar。注册成功即建立会话,`finishAutofillContext()` 后直接 fade 进首页,不回登录页。
## 7. Splash 组装稿(`lib/features/auth/splash_page.dart`
状态机:`checking`(默认)→ `failed`(refresh 网络错误/超时且本地有 token)。成功/无 token 不换态,直接 300ms fade 路由。
```dart
Scaffold(
backgroundColor: AppColors.canvas,
body: Center(
child: state == SplashState.checking
? Column(mainAxisSize: MainAxisSize.min, children: [
const BrandMark(), // 与登录页同一构造,保证 §5.2 过渡对位
const SizedBox(height: 32),
// 仅当等待 >300ms 才显示(进入页面时启动 300ms 定时器置 _showSpinner
SizedBox.square(dimension: 20,
child: _showSpinner
? const CircularProgressIndicator(
strokeWidth: 2.5, color: AppColors.primary)
: null), // 占位保高度,避免出现时跳动
])
: Column(mainAxisSize: MainAxisSize.min, children: [ // 错误态(§5.3
const BrandMark(),
const SizedBox(height: 24),
const Text('网络连接失败,无法恢复登录',
style: TextStyle(color: AppColors.ink, fontSize: 14)),
const SizedBox(height: 16),
OutlinedButton( // 次级按钮,高 44
style: OutlinedButton.styleFrom(
minimumSize: const Size(120, 44),
foregroundColor: AppColors.primaryStrong,
side: const BorderSide(color: AppColors.border),
),
onPressed: _retryRefresh,
child: const Text('重试')),
const SizedBox(height: 8),
TextButton( // 逃生通道:清除凭证进登录页
style: TextButton.styleFrom(
foregroundColor: AppColors.primaryStrong,
minimumSize: const Size(44, 44)),
onPressed: _clearCredentialsAndGoLogin,
child: const Text('改用账号登录')),
]),
),
)
```
流程约束(照 04 规范 §5.1,实现方注意):Splash 最短停留 500msrefresh 客户端超时 5s**网络失败不得清除本地 refresh token**(只有服务端明确 401 才清);所有页面切换用 `PageRouteBuilder` + `FadeTransition` 300ms。
## 8. 遗留给实现方的备忘
1. FIX-1、FIX-2 见 §2.3,建议与登录页开发同一工单批执行(FIX-2 直接影响注册页 helperText 观感)。
2. `Focus(onFocusChange:)` 包裹方案依赖祖先 Focus 节点聚合子孙焦点状态,Flutter 语义保证成立;若嫌样板多,可给 `AppTextField``focusNode` 参数(建议级)。
3. 三页均未用 `Form`/`validator`——`AppTextField` 是 errorText 受控模式,校验状态放页面 state,这与组件现状一致,勿混用两套校验。
4. DEBT-1(TagPill)另开工单,与登录纵切解耦。
---
**UI Designer** · 2026-09-04
@@ -0,0 +1,580 @@
# 埋点落地工程规范(身份漏斗 v1)
> 角色:Experiment Tracker
> 日期:2026-09-04
> 前序:`05-experiment-tracking-plan.md`(事件与指标规划)
> 依据:ADR-001~005(`patbond-doc/docs/architecture/decisions.md`)、开发计划第 6 节 API 契约规范、`patbond-doc/docs/database/patbond_postgresql.sql` platform schema 现有风格
> 性质:纯文档草案,供后续开发工单直接引用;DDL/OpenAPI 进 `patbond-doc` 由工单定夺
本文把 05 号报告的规划落到可实现粒度,共五部分:服务端契约(§1)、表 DDL(§2)、Flutter 采集模块(§3)、事件字典终稿 v1(§4)、数据质量验收清单(§5)。
与 05 号报告的差异(均由拍板决策驱动):
1. **ADR-004(仅账号密码)**:删除 `auth_register_failed.failureReason` 中的 `phone_taken`;`identifierType` 枚举 v1 仅保留 `username`(字段保留,为未来手机号/邮箱登录扩展)。
2. **Flutter 存储现实**:当前应用仅有 `shared_preferences`(已核对 `patbond-flutter/pubspec.yaml`),05 号报告建议的 sqflite/追加式文件均不可用(sqflite 未引入,追加文件需 path_provider)。事件队列改为 shared_preferences 分段方案(§3.3),队列上限相应从 1,000 降为 500。
3. **Flyway 基线已定**(ADR-001,Boot 3 + Flyway):DDL 以 Flyway 迁移草案形式给出。
---
## 1. 服务端契约:`POST /api/v1/events`
### 1.1 设计要点
- **批量上限**:单批 1–50 条事件,请求体 ≤ 64 KB。超限整批拒绝(400),客户端不重试、按批丢弃并本地计数。
- **部分失败语义**:合法批次一律响应 `202`,逐条返回结果(`accepted` / `duplicate` / `rejected`)。单条非法事件不拖累整批——这是 at-least-once 客户端能安全删除本地队列的前提:**客户端收到 202 即删除该批全部本地记录**,rejected 条目不重试(schema 错误重试无意义)。
- **幂等**:以每条事件的 `eventId`(客户端 UUIDv7)为去重键,落库 `ON CONFLICT DO NOTHING`,重复条目回 `duplicate`(也计成功)。**本端点不使用 `Idempotency-Key` 请求头**——开发计划 6.1 的幂等键是请求级语义,事件上报需要条目级幂等,`eventId` 已覆盖;避免两套幂等机制叠加。
- **鉴权(登录前事件)**:本端点是 `/api/v1` 下唯一**允许匿名**的写端点。`Authorization: Bearer` 可选:
- 客户端规则:上报时若持有**未过期**的 access token 则附带;过期/缺失则不带,**绝不因埋点触发 token 刷新**(埋点是旁路,不得驱动鉴权流量)。
- 服务端规则:带了 token 就正常校验,无效 token 回 401(客户端收到 401 去掉 Authorization 头重试一次)。已认证请求中,若某条事件 `userId` 非空且 ≠ token subject,该条 rejected(`identity_mismatch`)。
- 匿名请求中事件携带的 `userId` 原样落库(队列可能在退出登录后才冲刷历史事件)——它是分析归因数据,不参与任何权限判断。
- **防滥用**:按 `anonymousId` + 客户端 IP 双维度限流,建议 60 请求 / 5 分钟(正常客户端 30 秒一批,余量 15 倍),超限 429 + `Retry-After`;配合 body 64 KB 上限。429 客户端按退避重试(§3.4)。
- **白名单校验**:事件名不在字典(§4)→ 整条 rejected(`unknown_event_name`);`props` 中字典之外的字段**剥离后仍收下**该事件(丢弃字段计数告警),命中隐私红线字段名(password/token/phone/email 等模式)→ 整条 rejected(`forbidden_field`)。
- **响应结构**:沿用开发计划 6.1 统一信封 `{ "code": 0, "message": "success", "data": ... }`,字段 camelCase。
### 1.2 OpenAPI 3 片段(YAML 草案)
```yaml
paths:
/api/v1/events:
post:
tags: [analytics]
summary: 批量上报产品事件(身份漏斗 v1)
description: >
唯一允许匿名调用的写端点。单批 1-50 条、body <= 64KB。
以事件自身 eventId 幂等去重,不使用 Idempotency-Key 头。
合法批次一律 202 并逐条返回结果;客户端收到 202 即可删除本地队列中该批全部事件。
security:
- {} # 匿名(注册/登录前)
- bearerAuth: [] # 登录后携带未过期 access token
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/TrackEventsRequest'
responses:
'202':
description: 批次已受理,逐条结果见 data.results
content:
application/json:
schema:
$ref: '#/components/schemas/TrackEventsResponse'
'400':
description: 整批拒绝——JSON 非法、events 为空或超过 50 条、body 超过 64KB(客户端丢弃该批,不重试)
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'401':
description: 携带的 access token 无效或过期(客户端去掉 Authorization 重试一次)
'429':
description: 触发限流(建议 60 请求/5 分钟/anonymousId+IP),响应含 Retry-After;客户端按退避重试
headers:
Retry-After:
schema: { type: integer }
description: 建议等待秒数
components:
schemas:
TrackEventsRequest:
type: object
required: [events]
properties:
events:
type: array
minItems: 1
maxItems: 50
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: 客户端生成的 UUIDv7,服务端幂等去重键
eventName:
type: string
pattern: '^[a-z][a-z0-9_]{1,63}$'
description: 见事件字典 v1;不在字典中的事件名整条拒绝
example: auth_login_succeeded
eventVersion:
type: integer
minimum: 1
description: 事件 schema 版本,字典 v1 全部为 1
anonymousId:
type: string
format: uuid
description: 设备级匿名标识,首次启动生成
userId:
type: string
format: uuid
nullable: true
description: 登录后填充;认证请求中若与 token subject 不一致则该条 rejected
sessionId:
type: string
format: uuid
description: 客户端会话标识(冷启动或后台 30 分钟后重新生成)
clientTs:
type: string
format: date-time
description: 客户端本地时间(ISO 8601 含时区);serverTs 由服务端补写,客户端不发
appVersion:
type: string
maxLength: 32
example: 1.0.0+12
platform:
type: string
enum: [android, ios]
osVersion:
type: string
maxLength: 32
example: android-14
props:
type: object
description: >
事件专有属性,按事件字典 v1 白名单校验:字典外字段剥离并计数,
命中隐私红线模式(password/token/phone/email 等)整条拒绝。
additionalProperties: true
TrackEventsResponse:
type: object
properties:
code: { type: integer, example: 0 }
message: { type: string, example: success }
data:
type: object
required: [accepted, duplicated, rejected, results]
properties:
accepted: { type: integer, description: 新落库条数 }
duplicated: { type: integer, description: eventId 去重命中条数(视为成功) }
rejected: { type: integer, description: 被拒条数 }
results:
type: array
description: 与请求 events 等长、按原顺序对应
items:
type: object
required: [eventId, status]
properties:
eventId: { type: string, format: uuid }
status:
type: string
enum: [accepted, duplicate, rejected]
reason:
type: string
enum:
- unknown_event_name
- schema_invalid
- forbidden_field
- identity_mismatch
- event_too_large
description: 仅 status=rejected 时出现
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
```
落点建议维持 05 号报告结论:第一迭代放在现有服务内(`patbond-user` 或后续网关层),不为埋点单起服务。
---
## 2. 表 DDL:`platform.product_events` Flyway 迁移草案
### 2.1 设计要点
- **风格对齐现有 SQL**:`ck_` 约束前缀、`ix_`/`uq_` 索引前缀、varchar + CHECK 收敛取值、`jsonb_typeof` 校验、表上方英文注释,与 `platform.outbox_events` / `platform.notifications` 一致。
- **`event_id` 直接作主键**:客户端 UUIDv7 天然时间有序,作 PK 插入局部性好,且主键唯一约束就是幂等去重(`ON CONFLICT (event_id) DO NOTHING`)。不再需要 `DEFAULT gen_random_uuid()`——ID 必须来自客户端,服务端生成反而破坏去重。
- **`user_id` 不加外键**:分析事件是 append-only 旁路数据,不应阻塞 `identity.users` 的删除/清理,且乱序到达的事件可能引用尚未可见或已删除的用户。与 `outbox_events.aggregate_id` 不加外键的既有取舍一致。
- **`client_ts` 合理性约束**:允许滞后 30 天(离线队列最长积压)、超前 1 天(时钟漂移),超出即数据异常,宁可插入失败暴露问题。
- **分区(可选,v1 不做)**:见 2.3。
### 2.2 迁移草案
文件名建议 `V2__create_platform_product_events.sql`(假设 `V1__baseline.sql` 为 M0 的 bootstrap 基线;实际版本号以合入时迁移序列为准,`patbond-api` 目前尚无迁移文件)。
```sql
-- Client-side product analytics events (auth funnel, dictionary v1).
-- Append-only side channel: eventId is generated by the client (UUIDv7)
-- and doubles as the idempotency key for at-least-once upload, so the
-- primary key must NOT default to a server-generated uuid. user_id is
-- intentionally not a foreign key: analytics rows may outlive or precede
-- identity.users rows and must never block account lifecycle operations.
CREATE TABLE platform.product_events (
event_id uuid PRIMARY KEY,
event_name varchar(64) NOT NULL,
event_version smallint NOT NULL DEFAULT 1,
anonymous_id uuid NOT NULL,
user_id uuid,
session_id uuid NOT NULL,
client_ts timestamptz NOT NULL,
server_ts timestamptz NOT NULL DEFAULT now(),
app_version varchar(32) NOT NULL,
platform varchar(16) NOT NULL,
os_version varchar(32) NOT NULL,
props jsonb NOT NULL DEFAULT '{}'::jsonb,
CONSTRAINT ck_product_events_name CHECK (event_name ~ '^[a-z][a-z0-9_]{1,63}$'),
CONSTRAINT ck_product_events_version CHECK (event_version > 0),
CONSTRAINT ck_product_events_platform CHECK (platform IN ('android', 'ios')),
CONSTRAINT ck_product_events_app_version CHECK (char_length(btrim(app_version)) BETWEEN 1 AND 32),
CONSTRAINT ck_product_events_os_version CHECK (char_length(btrim(os_version)) BETWEEN 1 AND 32),
CONSTRAINT ck_product_events_props CHECK (jsonb_typeof(props) = 'object'),
CONSTRAINT ck_product_events_client_ts CHECK (
client_ts >= server_ts - interval '30 days'
AND client_ts <= server_ts + interval '1 day'
)
);
COMMENT ON TABLE platform.product_events IS
'Client analytics events (auth funnel v1); dedup by client-generated event_id, metrics windows use server_ts';
-- Funnel/metric queries: daily counts per event name.
CREATE INDEX ix_product_events_name_server_ts
ON platform.product_events (event_name, server_ts);
-- Per-subject dedup for conversion metrics (userId after login, anonymousId before).
CREATE INDEX ix_product_events_user_server_ts
ON platform.product_events (user_id, server_ts)
WHERE user_id IS NOT NULL;
CREATE INDEX ix_product_events_anon_server_ts
ON platform.product_events (anonymous_id, server_ts);
```
插入模式(接收端补写 `server_ts` 用列默认值即可,不由客户端传入):
```sql
INSERT INTO platform.product_events
(event_id, event_name, event_version, anonymous_id, user_id, session_id,
client_ts, app_version, platform, os_version, props)
VALUES (...)
ON CONFLICT (event_id) DO NOTHING;
-- 受影响行数 = 0 即 duplicate,计入去重命中率指标
```
### 2.3 分区建议(明确:v1 不分区)
- **不分区的理由**:第一迭代事件量极小;而 PostgreSQL 分区表的主键必须包含分区键,若按 `server_ts` 范围分区,PK 变为 `(event_id, server_ts)`——重试上报的同一事件会带着**不同的** `server_ts` 到达,去重唯一键即告失效,必须再引入应用层近期 eventId 缓存来补,复杂度不值。
- **触发条件**:单表超过约 5,000 万行、或需要按保留期批量清理时再改造。届时优先考虑「不分区 + 保留期删除(如保留 18 个月,按 `server_ts` 批量 DELETE)」;确要分区,同步设计应用层去重缓存(近 N 天 eventId 布隆过滤器/Redis set)兜住跨分区重复。
---
## 3. Flutter 采集模块设计
### 3.1 模块结构
`features/` 平级(埋点是横切基础设施,不是 feature):
```
lib/analytics/
analytics.dart # 门面导出:业务代码只 import 这一个
analytics_client.dart # AnalyticsClient:track()/identify()/reset()/flush()
event_context.dart # EventContext:组装公共属性(anonymousId/sessionId/appVersion/platform/osVersion)
session_tracker.dart # sessionId 生命周期:冷启动或后台超 30 分钟重新生成(WidgetsBindingObserver)
auth_analytics.dart # 11 个 auth_ 事件的类型安全封装(业务侧唯一允许的调用入口,杜绝手拼事件名/属性)
queue/
event_queue.dart # 抽象接口:append/peekBatch/removeBatch/size
prefs_event_queue.dart # shared_preferences 分段实现(v1)
upload/
event_uploader.dart # 批量上报、指数退避、at-least-once
```
关键契约:
- `AnalyticsClient.track(name, props)` **永不抛异常、永不 await 网络**——内部 try/catch 全吞并本地计数,埋点是旁路,任何失败不得影响业务流程。
- `identify(userId)` 在登录/恢复成功后调用,之后的事件自动带 `userId`;`reset()` 在退出后调用,只清 `userId`,**不清** `anonymousId` 与队列。
- `auth_analytics.dart` 提供如 `trackLoginSucceeded({required IdentifierType identifierType, required int durationMs})` 的强类型方法,属性名/枚举值编译期锁死,与字典 v1 一一对应。
依赖决策:`eventId` 需要 UUIDv7,当前 pubspec 无 uuid 能力。建议工单引入 `uuid` 包(^4,支持 v7)——auth 流程的 `Idempotency-Key` 同样需要它,一举两得;若依赖审批不过,退路是自实现 UUIDv7(约 30 行,`Random.secure` + 毫秒时间戳)。
### 3.2 存储选型:为什么是 shared_preferences,存什么
当前应用**只有 `shared_preferences`** 可用(已核对 pubspec.yaml);sqflite 未引入,追加式文件需要 path_provider 定位文档目录,也未引入。第一迭代不为埋点扩依赖面,用 shared_preferences 承载,理由:
- 事件属于**非敏感数据**(隐私红线在采集侧已挡住 password/token/账号原文),存普通本地存储合规。**token 类敏感数据继续禁入 shared_preferences**——它们属于 M1 将引入的安全存储(flutter_secure_storage/Keychain/Keystore),与事件队列物理隔离,本模块任何 key 不得存凭证。
- 事件量小(身份漏斗每会话 < 10 条),500 条 × ~0.5 KB ≈ 250 KB,在 shared_preferences(Android 上是整读整写的 XML/DataStore)可接受范围内。
`EventQueue` 做成接口,M2 起若 sqflite/path_provider 进入依赖集,换实现不动调用方。
### 3.3 分段队列方案(避免整队列重写)
朴素方案「一个 key 存整个 JSON 数组」每次 track 要重写全量字符串,500 条时是 O(n) 放大。改为**分段(segment)**:
| Key | 内容 |
| --- | --- |
| `pb.analytics.anonymousId` | 设备匿名 ID(UUID,首启生成,永不清除) |
| `pb.analytics.lastActiveAt` | 最近活跃时间(ISO 8601),用于 30 分钟会话超时判定 |
| `pb.analytics.segIndex` | JSON 数组:段 ID 有序列表(旧 → 新) |
| `pb.analytics.seg.<segId>` | JSON 数组:该段最多 20 条序列化事件 |
| `pb.analytics.droppedCount` | 本地累计丢弃计数(溢出淘汰 + 4xx 丢批),诊断用 |
- **写入**:track() 先进内存缓冲,追加到当前「开放段」并持久化该段(重写 ≤ 20 条,几 KB);段满 20 条即封段、开新段。
- **上限与淘汰**:总量上限 **500 条**(25 段)。超限时**丢最旧的整段**并累加 `droppedCount`——先到先丢,保住最新行为数据。
- **读取上报**:从最旧段起取事件拼批(单批 ≤ 50 条,即最多 2.5 段);**收到 202 后才删除对应段**(部分消费的段重写剩余部分),这就是 at-least-once——应用在响应到达前被杀,事件还在,重启后重发,服务端靠 `eventId` 去重。
### 3.4 上报时序(批量 / 退避 / at-least-once)
冲刷触发(四选一即触发):缓冲 ≥ 20 条;30 秒定时器;冷启动完成;应用进入后台(`AppLifecycleState.paused`,尽力冲刷不保证完成)。
```
track() ──► 内存缓冲 ──► 持久化到当前段(同步落盘,应用被杀不丢)
触发条件满足 ──────────────►│
取最旧 ≤50 条组批 ──► POST /api/v1/events
│ (有未过期 token 则带,否则匿名;绝不触发刷新)
┌──────────────┼──────────────────┬───────────────┐
▼ ▼ ▼ ▼
202 401(带了失效token) 400(整批被拒) 网络错误/5xx/429
删除该批本地段 去掉 Authorization 丢弃该批+ 保留本地,指数退避:
重置退避; 重试一次(仅一次) droppedCount++ 5s 起 ×2,上限 5min;
统计 rejected (不重试) 429 优先用 Retry-After
条数(不重试)
```
- 同一时刻最多一个在途上报请求(串行),天然保序,避免并发批间重复消费同段。
- 退避状态存内存即可,冷启动重置(冷启动本身会触发一次冲刷)。
### 3.5 与 auth 流程的挂接点
auth 功能页与仓储层是 M1 在建项(当前 `lib/` 尚无 auth feature),下表按开发计划 M1 架构(登录/注册页、ApiClient、AuthRepository、鉴权拦截、安全存储)指明每个事件的触发调用点,供 auth 工单实现时对号入座:
| # | 事件 | 挂接点(类/时机) |
| --- | --- | --- |
| 1 | `auth_register_started` | RegisterPage:任一输入框**首次**产生非空输入(页面 State 持一次性 flag,每次进入注册页的会话记一次);`entryPoint` 由路由来源传入 |
| 2 | `auth_register_succeeded` | AuthRepository.register:收到 code=0 且 token 已写入安全存储**之后**;`durationMs` = started 至此的耗时(started 时间戳由页面传给仓储调用) |
| 3 | `auth_register_failed` | 两处:RegisterPage 本地校验拦截提交时(`validation_error`);AuthRepository.register 异常分类处(按 §4 枚举映射业务码/HTTP 状态/网络异常) |
| 4 | `auth_login_succeeded` | AuthRepository.login:成功且 token 落安全存储后;**紧接着调用 `analytics.identify(userId)`**,顺序不可反(本事件自身要带上 userId) |
| 5 | `auth_login_failed` | AuthRepository.login 异常分类处 |
| 6 | `auth_token_refresh_succeeded` | TokenRefresher(单一刷新入口,被三类调用方使用):轮换成功且新 token 落安全存储后;`trigger` 由调用方传入(`proactive` 预刷新定时器 / `on_401` ApiClient 401 拦截器 / `restore` 启动恢复) |
| 7 | `auth_token_refresh_failed` | TokenRefresher 失败分支,`trigger` 同上 |
| 8 | `auth_logout` | SessionManager.logout:**本地凭证清除后**立即上报(不等服务端结果),`serverRevoked` = `POST /auth/logout` 调用结果;之后调用 `analytics.reset()`(事件本身要带退出前的 userId,顺序不可反) |
| 9 | `auth_session_restore_started` | 应用引导(main.dart bootstrap → SessionRestorer):安全存储中**存在** refresh token 才上报并进入恢复流程;无存量凭证不报任何 restore 事件 |
| 10 | `auth_session_restore_succeeded` | SessionRestorer:拿到有效 access token 且 `GET /api/v1/me` 成功后;随后 `identify(userId)` |
| 11 | `auth_session_restore_failed` | SessionRestorer 失败分支(用户被送回登录页时) |
两条全局规则:被动登出(刷新失败导致跳登录页)不报 `auth_logout`,由事件 7/11 覆盖;所有 track 调用点都不 await、不包裹业务 try/catch 之外的逻辑。
---
## 4. 事件字典终稿(v1)
> **字典版本:v1(2026-09-04)**。所有事件 `eventVersion = 1`。schema 变更须递增事件版本并在本字典追加条目,禁止原地改语义。
> 相对 05 号报告的裁剪(依据 ADR-004 仅账号密码):删除 `phone_taken`;`identifierType` 枚举仅 `username`(字段保留待扩展)。无短信/第三方登录相关枚举残留。
### 4.0 公共属性(所有事件必带,即 §1.2 `TrackedEvent` 顶层字段)
| 字段 | 类型 | 必填 | 示例 | 说明 |
| --- | --- | --- | --- | --- |
| `eventId` | UUID | 是 | `01920b7e-…` | 客户端 UUIDv7,去重键 |
| `eventName` | string | 是 | `auth_login_succeeded` | 本字典 11 个之一 |
| `eventVersion` | int | 是 | `1` | v1 固定 1 |
| `anonymousId` | UUID | 是 | `3f8a…` | 设备匿名标识 |
| `userId` | UUID | 否(可 null) | `9c21…` | 登录后填充 |
| `sessionId` | UUID | 是 | `b442…` | 冷启动/后台 30 分钟后重生成 |
| `clientTs` | ISO 8601 | 是 | `2026-09-04T10:12:03.120+08:00` | 客户端时间;`serverTs` 服务端补写,客户端不发 |
| `appVersion` | string | 是 | `1.0.0+12` | |
| `platform` | enum | 是 | `android` | `android` / `ios` |
| `osVersion` | string | 是 | `android-14` | 粗粒度主版本 |
隐私红线(05 号报告 1.3 节)全文有效:密码、凭证、手机号/邮箱/用户名原文、完整 IP、广告标识、原始报文/堆栈,任何事件任何字段禁止携带。
### 4.1 `auth_register_started`(注册)
| 属性 | 类型 | 必填 | 示例 | 说明 |
| --- | --- | --- | --- | --- |
| `entryPoint` | enum | 是 | `login_page_link` | `launch` / `login_page_link` |
### 4.2 `auth_register_succeeded`(注册,漏斗事件)
| 属性 | 类型 | 必填 | 示例 | 说明 |
| --- | --- | --- | --- | --- |
| `durationMs` | int | 是 | `41250` | started → succeeded 耗时 |
### 4.3 `auth_register_failed`(注册)
| 属性 | 类型 | 必填 | 示例 | 说明 |
| --- | --- | --- | --- | --- |
| `failureReason` | enum | 是 | `username_taken` | 见下方枚举 |
| `errorCode` | string | 否 | `A0102` | 业务错误码,本地拦截/网络错误时为空 |
| `httpStatus` | int | 否 | `409` | 无响应时为空 |
| `attemptSeq` | int | 是 | `2` | 本注册会话第几次尝试 |
`failureReason` 枚举(v1):`validation_error``username_taken``weak_password``rate_limited``network_error``server_error`
(~~`phone_taken`~~ 已删除——ADR-004 首版无手机号注册。)
### 4.4 `auth_login_succeeded`(登录,漏斗事件)
| 属性 | 类型 | 必填 | 示例 | 说明 |
| --- | --- | --- | --- | --- |
| `identifierType` | enum | 是 | `username` | v1 仅 `username`;字段保留待手机号/邮箱扩展 |
| `durationMs` | int | 是 | `1830` | 提交 → 成功耗时 |
### 4.5 `auth_login_failed`(登录)
| 属性 | 类型 | 必填 | 示例 | 说明 |
| --- | --- | --- | --- | --- |
| `identifierType` | enum | 是 | `username` | 同上 |
| `failureReason` | enum | 是 | `invalid_credentials` | 见下方枚举 |
| `errorCode` | string | 否 | `A0201` | |
| `httpStatus` | int | 否 | `401` | |
| `attemptSeq` | int | 是 | `1` | |
`failureReason` 枚举(v1):`invalid_credentials`(不区分账号不存在与密码错,与接口防枚举一致)、`account_locked``rate_limited``validation_error``network_error``server_error`
### 4.6 `auth_token_refresh_succeeded`(会话维持)
| 属性 | 类型 | 必填 | 示例 | 说明 |
| --- | --- | --- | --- | --- |
| `trigger` | enum | 是 | `on_401` | `proactive` / `on_401` / `restore` |
### 4.7 `auth_token_refresh_failed`(会话维持)
| 属性 | 类型 | 必填 | 示例 | 说明 |
| --- | --- | --- | --- | --- |
| `trigger` | enum | 是 | `restore` | 同上 |
| `failureReason` | enum | 是 | `refresh_revoked` | `refresh_expired` / `refresh_revoked`(含轮换重放被拒,M1 验收观测点)/ `network_error` / `server_error` |
| `errorCode` | string | 否 | `A0301` | |
| `httpStatus` | int | 否 | `401` | |
### 4.8 `auth_logout`(退出)
| 属性 | 类型 | 必填 | 示例 | 说明 |
| --- | --- | --- | --- | --- |
| `serverRevoked` | bool | 是 | `true` | `POST /auth/logout` 是否成功;事件在本地凭证清除后上报,不等服务端 |
### 4.9 `auth_session_restore_started`(恢复)
无专有属性(`props` 为空对象)。仅当安全存储存在 refresh token 时上报。
### 4.10 `auth_session_restore_succeeded`(恢复)
| 属性 | 类型 | 必填 | 示例 | 说明 |
| --- | --- | --- | --- | --- |
| `durationMs` | int | 是 | `920` | 恢复流程耗时 |
| `usedRefresh` | bool | 是 | `true` | 是否经历了 token 刷新 |
### 4.11 `auth_session_restore_failed`(恢复)
| 属性 | 类型 | 必填 | 示例 | 说明 |
| --- | --- | --- | --- | --- |
| `failureReason` | enum | 是 | `refresh_expired` | `refresh_expired` / `refresh_revoked` / `network_error` / `server_error` |
| `errorCode` | string | 否 | `A0301` | |
| `httpStatus` | int | 否 | `401` | |
---
## 5. 数据质量验收清单
事件链路上线时逐项验证;§5.2 的对账 SQL 沉淀为每日巡检(未来实验前置条件「与服务端日志交叉核对」的日常化)。真值来源:`identity.auth_sessions` 的会话族结构(登录开新族、刷新在族内轮换、退出写 `revoke_reason`)与 `identity.users.created_at`
### 5.1 上线验收项(一次性)
| # | 验收项 | 方法 | 通过标准 |
| --- | --- | --- | --- |
| 1 | 幂等去重生效 | 手工重放同一批(同 `eventId`)两次 | 第二次全部 `duplicate`,表内仅一行 |
| 2 | `serverTs` 覆盖率 | `SELECT count(*) FROM platform.product_events WHERE server_ts IS NULL` | 恒为 0(列 NOT NULL DEFAULT 保证,查询作双保险) |
| 3 | 白名单剥离与红线拒绝 | 构造带未知字段 / 带 `password` 字段的事件上报 | 前者字段被剥离且计数告警,后者整条 rejected(`forbidden_field`);库内 props 无红线字段(SQL 见 5.2.4) |
| 4 | at-least-once 不丢 | 飞行模式下操作登录流程 → 杀进程 → 重启联网 | 事件补报到库,无重复 |
| 5 | 匿名上报与 401 降级 | 未登录状态上报;带过期 token 上报 | 前者 202;后者 401 后客户端去头重试成功 |
| 6 | 事件丢失率 | 端上 `droppedCount` 抽样 + 5.2 对账偏差 | 丢失率 < 5%(实验前置条件阈值) |
| 7 | 整批限制 | 51 条 / >64KB 请求 | 400,客户端丢批不重试 |
### 5.2 对账 SQL(每日巡检,偏差 > 5% 告警)
统计窗口均为 UTC 日界(与指标口径一致)。注意:事件经本地队列有分钟级延迟,`server_ts` 与会话创建时刻可能跨日,单日偏差告警建议观察连续 2 日,7 天滚动窗口偏差是更稳的告警口径。
**5.2.1 登录成功对账**:`auth_login_succeeded` 事件数 vs 服务端新建会话族数(登录开新 `token_family_id`;排除注册当场创建的会话族)。
```sql
WITH family_first AS (
SELECT DISTINCT ON (token_family_id) token_family_id, user_id, created_at
FROM identity.auth_sessions
ORDER BY token_family_id, created_at
),
api_logins AS (
SELECT date_trunc('day', ff.created_at AT TIME ZONE 'UTC') AS day, count(*) AS api_cnt
FROM family_first ff
JOIN identity.users u ON u.id = ff.user_id
WHERE ff.created_at - u.created_at > interval '60 seconds' -- 排除注册即建的首个会话族
GROUP BY 1
),
tracked AS (
SELECT date_trunc('day', server_ts AT TIME ZONE 'UTC') AS day, count(*) AS evt_cnt
FROM platform.product_events
WHERE event_name = 'auth_login_succeeded'
GROUP BY 1
)
SELECT coalesce(a.day, t.day) AS day,
coalesce(api_cnt, 0) AS api_cnt,
coalesce(evt_cnt, 0) AS evt_cnt,
round(abs(coalesce(evt_cnt, 0) - coalesce(api_cnt, 0))::numeric
/ greatest(coalesce(api_cnt, 0), 1) * 100, 2) AS diff_pct -- > 5 告警
FROM api_logins a
FULL JOIN tracked t USING (day)
ORDER BY day;
```
**5.2.2 注册成功对账**:`auth_register_succeeded` vs `identity.users` 当日新建数。
```sql
SELECT coalesce(u.day, t.day) AS day, coalesce(api_cnt, 0) AS api_cnt,
coalesce(evt_cnt, 0) AS evt_cnt,
round(abs(coalesce(evt_cnt, 0) - coalesce(api_cnt, 0))::numeric
/ greatest(coalesce(api_cnt, 0), 1) * 100, 2) AS diff_pct
FROM (SELECT date_trunc('day', created_at AT TIME ZONE 'UTC') AS day, count(*) AS api_cnt
FROM identity.users GROUP BY 1) u
FULL JOIN (SELECT date_trunc('day', server_ts AT TIME ZONE 'UTC') AS day, count(*) AS evt_cnt
FROM platform.product_events
WHERE event_name = 'auth_register_succeeded' GROUP BY 1) t USING (day)
ORDER BY day;
```
**5.2.3 刷新成功对账**:`auth_token_refresh_succeeded` vs 会话轮换数(`rotated_at` 落在当日的会话行)。
```sql
SELECT coalesce(s.day, t.day) AS day, coalesce(api_cnt, 0) AS api_cnt,
coalesce(evt_cnt, 0) AS evt_cnt
FROM (SELECT date_trunc('day', rotated_at AT TIME ZONE 'UTC') AS day, count(*) AS api_cnt
FROM identity.auth_sessions WHERE rotated_at IS NOT NULL GROUP BY 1) s
FULL JOIN (SELECT date_trunc('day', server_ts AT TIME ZONE 'UTC') AS day, count(*) AS evt_cnt
FROM platform.product_events
WHERE event_name = 'auth_token_refresh_succeeded' GROUP BY 1) t USING (day)
ORDER BY day;
```
**5.2.4 隐私红线扫描**:props 中不得出现红线字段(每日跑,命中即 P1 处理并清洗)。
```sql
SELECT event_name, k AS prop_key, count(*) AS hits
FROM platform.product_events
CROSS JOIN LATERAL jsonb_object_keys(props) AS k
WHERE server_ts >= now() - interval '1 day'
AND k ~* 'password|token|secret|phone|mobile|email|credential|idfa|gaid'
GROUP BY 1, 2;
-- 期望恒为空集;另将全量 key 分布与字典 v1 白名单比对,发现未知 key 说明服务端剥离逻辑失效
```
**5.2.5 服务端技术指标**(接收端 Micrometer 计数器,上线即带):`/api/v1/events` 请求量、整批拒绝率、逐条 rejected 率(按 reason 分)、去重命中率、白名单剥离字段计数。去重命中率长期 > 10% 提示客户端删除本地队列的时机有 bug。
---
## 附:工单拆分建议
1. **后端**:`V2` 迁移 + `/api/v1/events` 接收端(白名单校验、限流、逐条结果)+ 5.2.5 技术指标 —— 依赖 M0 Flyway 基线。
2. **Flutter**:`lib/analytics/` 模块(队列 + 上报器 + 门面),可先于 auth 功能独立交付并用假事件自测。
3. **Flutter**:auth 工单按 §3.5 挂接 11 个事件(依赖工单 2 与 M1 auth 实现)。
4. **数据**:5.2 对账 SQL 入 `patbond-doc/docs/database/` 参考查询 + 告警巡检接入。
@@ -0,0 +1,158 @@
# 14 第一波「工程基线」里程碑证据档案与提交清单
- 编制人:EvidenceQA(Evidence Collector)
- 快照时间:2026-09-04(所有 git 输出均为本刻快照)
- 方法:patbond-api 全程只读(ls/grep/git 只读/读文件,未运行任何构建);patbond-flutter 复跑三条门禁命令做独立验证;patbond-doc 安装 mkdocs 并运行 strict 构建(产物输出到 scratchpad,仓库未落任何文件)。
- 对照基线:`06-evidence-audit.md` 问题清单(B1/B2、M1-M5、m1-m4)、`07-backend-baseline-report.md``08-flutter-theme-report.md``patbond-doc/docs/architecture/decisions.md`(ADR-001~005)。
---
## 0. 重大事实更正:「三仓均未提交」已不成立
任务前提是三仓改动均未提交,实测不符:
| 仓库 | 分支 | 本地最新提交 | 远端(origin)最新 | 工作区 |
| --- | --- | --- | --- | --- |
| patbond-api | dev | `c7ddaec 重构底层框架`(2026-09-03 17:59:56 +0800,Lixi20) | `c7ddaec`(一致,**已推送**) | 干净(`git status --porcelain` 空) |
| patbond-doc | main | `ca8cb72 完善开发文档`(2026-09-03 18:00:52 +0800,含 decisions.md 63 行 + mkdocs.yml 2 行) | `ca8cb72`(一致,**已推送**) | 干净 |
| patbond-flutter | dev | `2601da3 update README.md` | `2601da3`(一致) | **7 个修改 + 7 个未跟踪文件待提交**(见第 3 节) |
即:第一波后端改动与 ADR 文档已由某人(git 作者 Lixi20)在 2026-09-03 傍晚提交并推送;`c7ddaec``git show --stat`(24 个文件,+955/-99)与 07 报告的改动清单逐一吻合(4 pom、6 Java 源、2 sample、mvnw 三件套、5 个测试文件、Readme、测试专用 application.yml、.gitignore)。**真正待提交的只剩 patbond-flutter。**
---
## 1. 问题闭环核对(对照 06 审计清单,逐项附本刻证据)
| # | 问题 | 状态 | 本刻证据 |
| --- | --- | --- | --- |
| B1 | 后端零测试、CI 门禁空转 | **部分闭环** | 三模块均有 `src/test`;`grep -rc "@Test"` 计 3+1+9+1+7=**21 个用例**(ApiResponseTest 3、UserApplicationTests 1、UserControllerTest 9、AuthApplicationTests 1、AuthControllerTest 7);三个 pom 均有 `spring-boot-starter-test`(common:30、user:40、auth:40)。07 报告附 `./mvnw clean test` BUILD SUCCESS 输出(21 测试 0 失败)。仍缺:Testcontainers 集成测试、契约测试、安全测试(后续工单) |
| B2 | 内存用户 + 不可验证 token + `/internal` 裸奔 | **未动(计划内后续工单)** | `UserService.java:21-23` 仍为 `AtomicLong` + 两个 `ConcurrentHashMap`;`AuthService.java:50` 仍为 `UUID.randomUUID()`;`UserController.java:18` 仍映射 `/internal/users`;全仓 grep 无 `spring-boot-starter-security`/`SecurityFilterChain`(输出:NO SECURITY CONFIG) |
| M1 | auth→user 调用链错误状态码折叠 | **未动(计划内后续工单)** | `AuthService.java:61``ResponseStatusException(HttpStatus.BAD_REQUEST, ...)`;07 报告冒烟实测「错密码登录 → HTTP 500」,并已用 `AuthControllerTest.loginPropagatesFeignExceptionUnhandled` 把现状钉为基线 |
| M2 | common 强制全体引入 AMQP/Feign/LB/Nacos | **已修复** | `patbond-common/pom.xml` 依赖仅剩 `jakarta.validation-api`(compile)+ `spring-boot-starter-test`(test scope);全仓 `grep -rn "nacos\|amqp\|loadbalancer" --include="pom.xml"` 零命中(输出:NO NACOS/AMQP/LB IN POMS) |
| M3 | 无 CI 配置 + 无 Maven Wrapper | **部分闭环** | Wrapper 已入库:`mvnw``mvnw.cmd``.mvn/wrapper/maven-wrapper.properties` 存在且在 `c7ddaec` 提交内。CI 载体仍缺:`ls -a` 无 .github/.gitlab-ci/Jenkinsfile(输出:NO CI CONFIG) |
| M4 | API Readme 启动步骤不完整 | **已修复** | `patbond-api/Readme.md`:16 行 Wrapper 说明、21/27 行 `./mvnw clean test`(含 JAVA_HOME=JDK17 指引)、32-39 行 sample→yml 复制命令、46-48 行 `-pl patbond-common install` + 两个 `spring-boot:run`、71 行禁止向 sample 提交密钥 |
| M5 | 数据库 bootstrap 单文件混装 fixture 凭据 | **未动(后续工单)** | `patbond-doc/docs/database/` 仍仅 `patbond_postgresql.sql` 单文件;第 1269 行仍有 `accounts use the password: Patbond@123`;Flyway 拆分(工单 T1)未启动 |
| m1 | Flutter 唯一冒烟测试断言写死演示数据 | **未动** | `test/widget_test.dart:18-19` 仍断言 `'北京 · 朝阳区'``'28°C 晴'`;该文件不在本次改动清单 |
| m2 | Flutter README 校验命令缺 `--output=none` | **未修** | `patbond-flutter/README.md:34` 仍为 `dart format --set-exit-if-changed lib test`(会改写文件)。注意:README 当前的未提交改动只是在运行段加了一行 `flutter clean`(`git diff README.md` 全文仅此一处),并未修此项 |
| m3 | 演示数据持久化展示字符串 + 22 处 Unsplash URL | **未动(计划内,逐页替换时清除)** | `lib/data/demo_data.dart` 不在改动清单,'2小时前'/'1.2km' 等仍在 |
| m4 | flutter 工作区不干净 | **性质变化** | 审计时仅 ` M README.md`;现为第一波交付的 7 修改 + 7 新增,即本档案第 3 节要落账的对象。README 的用户自有改动仍混在其中 |
**闭环率**:任务预期应修复的 4 项全部兑现——M2 已修复、M4 已修复、M3 的 Wrapper 半边已修复、B1 已部分闭环(21 测试)。全部 11 项口径:2 项全闭(M2、M4),2 项部分(B1、M3),7 项未动——其中 B2/M1/M5/m1/m3 属已排期后续工单,**m2 是一处一行即可修的文档问题,两波交付均未顺手处理,建议纳入下一波**。
---
## 2. 文档门禁(mkdocs build --strict):通过
- 安装:`pip install --user mkdocs` → mkdocs 1.6.1(Python 3.14,`~/.local/bin/mkdocs`)。此前 `which mkdocs` / `python3 -m mkdocs` 均无,门禁属首次打通。
- 执行(产物指向 scratchpad,避免污染仓库):
```text
$ cd patbond-doc && mkdocs build --strict -d <scratchpad>/mkdocs-site
INFO - Cleaning site directory
INFO - Building documentation to directory: .../mkdocs-site
INFO - Documentation built in 0.08 seconds
EXIT=0
```
- **exit 0,零 warning,strict 模式下导航与链接均无报错**;无错误清单需要移交。
- 仓库卫生:构建后 `ls -d site` 确认仓库内无 site/ 目录,`git status` 仍为空。mkdocs.yml 三级导航(index / development-plan / decisions)与 06 审计的静态核对结论一致,本次为动态实证。
- 注意:mkdocs 装在 `~/.local`,CI 环境需自行安装;若后续文档引入主题/插件(如 material),需同步补 requirements 文件——当前 mkdocs.yml 用内置 readthedocs 主题,无额外依赖。
---
## 3. 三仓提交清单
### 3.1 patbond-api —— 本刻无待提交内容(快照口径,提交前必须重新生成)
`git status --porcelain` 为空;dev 分支与 origin/dev 同在 `c7ddaec`。第一波改动已随 `c7ddaec 重构底层框架` 提交并推送,无需再操作。
磁盘上存在但已被正确忽略(不得入库):
| 路径 | 处置 | 依据 |
| --- | --- | --- |
| `patbond-auth/src/main/resources/application.yml``patbond-user/.../application.yml` | 应忽略(本地真实配置,sample 模式) | `.gitignore` 白名单规则,`git status --ignored` 确认 `!!` |
| `patbond-{common,user,auth}/target/` | 应忽略(构建产物) | 同上 |
| `.idea/` | 应忽略(IDE 配置) | 同上,且 `git ls-files` 无 .idea 条目 |
**警示**:该仓有开发 agent 正在继续开发,本清单仅为快照;开发波次完成后需重新 `git status` 生成新清单再提交。既成事实备注:`重构底层框架` 这条信息无语义前缀(更贴切的应为 `refactor: 升级 Spring Boot 3.5、移除 Nacos、补齐 21 个测试与 Maven Wrapper`),但该提交已推送远端,不建议改写历史;后续新提交请回归中文语义前缀约定。
### 3.2 patbond-doc —— 本刻无待提交内容
`git status --porcelain`(含 --ignored)为空;main 与 origin/main 同在 `ca8cb72 完善开发文档`(ADR-001~005 的 decisions.md + mkdocs.yml 导航,已推送)。既成事实备注:更贴切的信息应为 `docs: 新增 ADR-001~005 架构决策记录`,同样不建议改写已推送历史。本次 strict 构建未在仓库产生任何文件。
### 3.3 patbond-flutter —— 14 个文件待处置(唯一真正待提交的仓库)
`git status --porcelain -uall` 全量清单与逐文件处置:
| 文件 | 状态 | 处置 | 说明 |
| --- | --- | --- | --- |
| `README.md` | M | **用户自决** | 未提交改动仅一行:运行段新增 `flutter clean`(diff 已核,无其他内容)。是用户本人的改动,不并入第一波提交;若用户决定保留,建议连同 m2 的 `--output=none` 修复一起单独提交 |
| `lib/core/theme/app_theme.dart` | M | 应提交 | ADR-005 主题重写(珊瑚橙 token 体系) |
| `lib/features/create/create_page.dart` | M | 应提交 | 硬编码色 → token |
| `lib/features/home/home_page.dart` | M | 应提交 | 同上 |
| `lib/features/pets/pets_page.dart` | M | 应提交 | 同上 |
| `lib/features/profile/profile_page.dart` | M | 应提交 | 同上 |
| `lib/widgets/common.dart` | M | 应提交 | 同上 |
| `lib/core/widgets/brand_mark.dart` | ?? | 应提交 | 新增认证基础组件 ×5 |
| `lib/core/widgets/app_text_field.dart` | ?? | 应提交 | 〃 |
| `lib/core/widgets/primary_button.dart` | ?? | 应提交 | 〃 |
| `lib/core/widgets/inline_error_banner.dart` | ?? | 应提交 | 〃 |
| `lib/core/widgets/auth_scaffold.dart` | ?? | 应提交 | 〃 |
| `test/core/widgets/primary_button_test.dart` | ?? | 应提交 | 组件 widget 测试 ×2 文件(6 用例) |
| `test/core/widgets/app_text_field_test.dart` | ?? | 应提交 | 〃 |
已忽略、确认不入库:`build/``.dart_tool/``.flutter-plugins-dependencies``android/local.properties`、iOS/Android 生成文件等(`git status --ignored` 全部 `!!`,规则健全)。
**提交前门禁已独立复跑(2026-09-04,非转述 08 报告)**:
```text
$ dart format --output=none --set-exit-if-changed lib test → Formatted 22 files (0 changed), exit 0
$ flutter analyze → No issues found! (ran in 0.7s)
$ flutter test → 00:01 +7: All tests passed! (5 组件测试 + 1 导航冒烟 + 1)
```
**建议提交信息**(排除 README.md 后一条提交):
```
feat: 迁移珊瑚橙主题体系并新增认证基础组件(ADR-005)
- app_theme.dart 重写为语义 token(AppColors/AppRadius),落地 primaryStrong/error 等 AA 对比度色
- 五个页面与公共组件的旧靛蓝硬编码色值全部替换为 token,仅换色不动布局
- 新增 BrandMark/AppTextField/PrimaryButton/InlineErrorBanner/AuthScaffold 及 6 个 widget 测试
- 门禁:dart format(0 changed)/flutter analyze(0 issues)/flutter test(7 passed)
```
**建议 api/doc 提交信息**:本刻两仓无待提交内容,上表既成事实备注已给出应然写法,供后续波次遵循;api 下一波提交信息待其开发完成后按实际改动拟定(中文语义前缀 refactor/feat)。
---
## 4. 里程碑证据索引(iteration-1-reports/ 01-08)
| 报告 | 角色 | 核心验收证据 / 结论 |
| --- | --- | --- |
| `01-pm-task-breakdown.md` | Senior Project Manager | M0 + 登录纵切 8 任务的工单化分解;M0 的 Flyway 化与第 8 节任务 1 合并为工单 T1(仅 identity/media schema);明确社区/AI/预约不在本迭代 |
| `02-dev-technical-assessment.md` | Senior Developer | 实读三仓源码的现状盘点与选型建议,是 ADR-001(升 Boot 3)/ADR-002(移除 Nacos)的技术论证来源 |
| `03-reality-check.md` | Reality Checker | 总评 **NEEDS WORK**;确认文档描述诚实、无夸大;两个环境硬阻塞(无 Nacos 实例、数据库不可验证)+ 默认 JDK 26 与基线 17 不符——前者已被 ADR-002 从根上消除,后者由 07 报告以 `JAVA_HOME=/usr/lib/jvm/java-17-openjdk` 方案落地 |
| `04-ui-login-design-spec.md` | UI Designer | 登录/注册 UI 规范;裁决双视觉分歧、定义 token 体系与 `primaryStrong #D6431A``error #D0342C`(AA 对比度),即 ADR-005 补充规范的正文 |
| `05-experiment-tracking-plan.md` | Experiment Tracker | 身份漏斗(注册→登录→me→退出)埋点事件规范与实验规划;实现依赖后端契约冻结,尚未落码 |
| `06-evidence-audit.md` | EvidenceQA | 开工前基线:B1/B2 两 Blocker、M1-M5、m1-m4 共 11 项问题(全部附文件+行号证据);第 5 节给出登录纵切验收所需的完整证据清单(测试输出/curl transcript/psql/截图),仍是 M1 里程碑验收的执行标准 |
| `07-backend-baseline-report.md` | Senior Developer | 第一波后端交付:Boot 2.7.18→**3.5.16**、Cloud 2025.0.3、移除 Nacos(ADR-001/002)、common 瘦身(M2)、Maven Wrapper(M3 半)、README 重写(M4);**21 测试 BUILD SUCCESS** 原始输出;两服务干净配置启动 + 端到端冒烟(注册/登录/internal 均通,错密码 500 为已知 M1 遗留);进程已清理。→ 已作为 `c7ddaec` 提交并推送 |
| `08-flutter-theme-report.md` | Frontend Developer | 第一波 Flutter 交付:ADR-005 主题迁移(旧→新 token 映射表)、5 个认证基础组件、6 个组件测试;门禁三命令全绿(format 0 changed / analyze 0 issues / **flutter test +7**)——本档案 2026-09-04 复跑复现同样结果。→ 尚未提交,见 3.3 清单 |
**里程碑口径**:第一波「工程基线」的两份交付(07/08)验收证据齐全且经独立复核;ADR 文档门禁(mkdocs --strict)本次首次实证通过。里程碑归档条件已满足,唯一未落账动作是 patbond-flutter 的提交。
---
## 5. 移交后续波次的未闭环清单
1. **B2 三件套**(数据库持久化 + JWT + `/internal` 鉴权)——M1 里程碑主体,验收按 06 报告第 5 节证据清单执行。
2. **M1 错误状态码折叠**——统一异常契约工单;现有 `loginPropagatesFeignExceptionUnhandled` 测试在改造时按新契约改写。
3. **M3 后半:CI 载体**——`./mvnw clean test` 与 Flutter 三命令已可直接作为门禁命令,缺执行载体;文档门禁可加 `pip install mkdocs && mkdocs build --strict`
4. **M5 / 工单 T1**——bootstrap SQL 拆 Flyway baseline 与种子数据,验收含 `grep -c "Patbond@123"` = 0。
5. **m2**——`patbond-flutter/README.md:34``--output=none`,一行改动,建议随用户 README 自决一并处理。
6. **m1/m3**——widget_test 演示数据断言与 demo 展示字符串,随登录页/真实数据接入清除。
7. `AuthTokenResponse.expiresAt` 无时区 LocalDateTime(07 报告遗留 4),随 token 重构处理。
---
- 本次动作留痕:安装 mkdocs 至 `~/.local`(pip --user);mkdocs 产物写入 scratchpad(`mkdocs-site/`,会话级临时目录);flutter 门禁复跑仅触碰 git 忽略的构建缓存。三仓的受跟踪文件零修改、零提交、无遗留进程。
@@ -0,0 +1,67 @@
# 15 Git 收尾与工作流建章报告
- 执行人:Git Workflow Master
- 日期:2026-09-04
- 范围:patbond-flutter / patbond-api 两笔功能提交 + patbond-doc 工作流规范一笔提交。**三仓均只 commit 未 push**(推送由用户执行),各仓本地领先 origin 1 个提交。
---
## 1. 提交一:patbond-flutter `af002ed`dev
`feat: 迁移珊瑚橙主题体系并新增认证基础组件(ADR-005)`
**提交前门禁实跑(2026-09-04,非转述)**
```text
dart format --output=none --set-exit-if-changed lib test → Formatted 22 files (0 changed)exit 0
flutter analyze → No issues found! (ran in 1.1s)
flutter test → 00:02 +7: All tests passed!
```
**git show --stat 核对**13 files changed, +500/-49,与 14 号档案 3.3 节清单逐一一致——
- M ×6`lib/core/theme/app_theme.dart`+126 段主题重写)、create/home/pets/profile 四页 + `lib/widgets/common.dart`token 替换)
- A ×7`lib/core/widgets/` 下 app_text_field / auth_scaffold / brand_mark / inline_error_banner / primary_button 五组件 + `test/core/widgets/` 两个测试文件
- **README.md 未入库**(用户自有改动,保持 ` M` 未提交状态,由用户自决)
## 2. 提交二:patbond-api `bd20adc`dev
`feat: 用户 UUID 持久化与统一异常契约(Flyway baseline / UUIDv7 / 错误码透传)`
**提交前门禁实跑**`JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./mvnw clean test`**BUILD SUCCESS**,四模块(patbond-api 聚合 / common / user / auth)全部 SUCCESS,合计 37 测试 0 失败 0 错误(含 Testcontainers postgres:16 集成测试,auth 末段 12 测试输出留档于会话)。总耗时 25.5s。
**入库前卫生核对**`git status --ignored` 确认 `.idea/``patbond-{user,auth}/src/main/resources/application.yml`(本地真实配置)、三个 `target/` 全部为 `!!` 忽略态,未入暂存区;配置仅 `application.yml.sample` 入库。
**git show --stat 核对**28 files changed, +1265/-158,与 10 号报告改动面完全吻合——
- A ×14`V1__identity_media_baseline.sql`277 行)、`db/dev/afterMigrate__dev_seed.sql`、UserRepository、UuidV7+测试)、ErrorCode/BusinessException、两个 GlobalExceptionHandler、ApiErrorDecoder/FeignConfig+测试)、TestcontainersConfiguration、UserPersistenceIntegrationTest
- M ×14Readme、user pomjdbc/flyway/pg/testcontainers 依赖)、UserService/UserController、三个 common DTO、auth 的 DTO/Service、application.yml.sample、既有测试改写
## 3. 提交三:patbond-doc `027876a`main
`docs: 增加 Git 工作流规范`
- 新增 `docs/development/git-workflow.md`43 行),`mkdocs.yml` 导航挂到「开发文档」组下(+1 行)。
- **门禁实跑**`mkdocs build --strict -d <scratchpad>` → exit 0,零 warning,产物写入会话 scratchpad,仓库内无 site/。
- git show --stat2 files changed, +44。
**规范要点**(一页以内,可执行):
1. **分支模型**api/flutter 以 dev 为集成分支、小步直提;doc 直提 main;跨多天/破坏性/多人并行时才开短命 `feat|fix/<主题>` 分支。
2. **提交信息**:中文 + `feat/fix/refactor/docs/test/chore` 前缀;正文列表写验收证据(门禁输出结论);引用 ADR 编号的既有惯例成文固化,附真实示例。
3. **禁止事项**:敏感配置/构建产物不入库(提交前核对暂存清单);共享分支不 force push(个人分支用 `--force-with-lease`);**已推送 Flyway 迁移不可变、只增不改**(呼应开发计划 4.3);不改写已推送历史。
4. **CI 衔接**:按仓库分列提交前必跑命令表(api`JAVA_HOME=jdk17 ./mvnw clean test`flutter 三命令;doc`mkdocs build --strict`),未来 CI 载体(审计 M3 后半)原样采用。
## 4. 三仓最终 git status
| 仓库 | 分支 | 相对 origin | 工作区 |
| --- | --- | --- | --- |
| patbond-api | dev | **领先 1**`bd20adc` | 干净 |
| patbond-flutter | dev | **领先 1**`af002ed` | 仅 ` M README.md`(按约保留给用户) |
| patbond-doc | main | **领先 1**`027876a` | 干净 |
## 5. 卫生留痕
- 未执行任何 push;未动三仓之外的文件(本报告除外)。
- Maven `target/``./mvnw clean` 清除;`docker ps` 无遗留容器(Testcontainers/ryuk 自动回收);mkdocs 产物在 scratchpad;无后台进程遗留。
- 待用户动作:三仓各 push 一次;patbond-flutter README.md 自决(建议顺手补 m2 的 `--output=none` 一并单独提交)。
@@ -0,0 +1,46 @@
# 第一迭代进展看板
> 目标:真实登录纵切(注册 → 登录 → 获取当前用户 → 退出),依据[开发实施计划](../../development-plan.md)第 8 节。
> 更新日期:2026-09-04。本页是团队共享的进度事实来源,每波工作交付后更新。
## 当前状态一览
| 状态 | 内容 |
| --- | --- |
| ✅ 已完成 | 开工分析(报告 01-06)、工程基线(第一波)、持久化纵切(第二波)、Git 工作流建章 |
| 🔜 下一步 | 第三波:JWT + refresh 会话 → `/internal` 鉴权 → OpenAPI 冻结 → Flutter 登录页拼装 → 端到端用例 |
| ⚠️ 未闭环 | token 仍为随机串(表已就绪)、`/internal` 无鉴权、CI 载体缺失、UI 待修 FIX-1/FIX-2、Flutter README 门禁参数(m2 |
## 已完成(附提交)
**第一波:工程基线**
- 后端升级 Spring Boot 3.5.16 / JDK 17,移除 NacosADR-001/002),common 瘦身,Maven Wrapper,测试 0 → 21`patbond-api@c7ddaec`,报告 07)。
- Flutter 迁移珊瑚橙主题(ADR-005),新增 5 个认证组件 + 6 个 widget 测试(`patbond-flutter@af002ed`,报告 08)。
- ADR-001~005 入档(`patbond-doc@ca8cb72`,见[技术决策记录](../../../architecture/decisions.md))。
**第二波:持久化纵切**
- Flyway V1 baselineidentity/media)、用户 UUIDv7 持久化到 PostgreSQL、统一异常与错误码透传(修复错误码折叠),测试 21 → 37,全部经 Testcontainers 验证(`patbond-api@bd20adc`,报告 10)。
- 独立复核确认第一波声明属实(报告 11);UI 设计 QA + 登录/注册/Splash 组装稿(报告 12);埋点工程规范含 OpenAPI/DDL 草案(报告 13);mkdocs 门禁打通(报告 14)。
- Git 工作流规范入档(`patbond-doc@027876a`,见 [Git 工作流规范](../../git-workflow.md)),报告 15。
## 下一步(第三波,未启动)
1. **T4 JWT + refresh 会话**:按 ADR-003access 15 分钟 / refresh 30 天轮换 / 多设备),`identity.auth_sessions` 表已随 V1 就绪;同时保护 `/internal/**`、整改 `expiresAt` 时区。
2. **T6a OpenAPI 冻结**:错误码契约(报告 10)+ token 字段定型后出契约文档。
3. **Flutter 登录页拼装**:照报告 12 组装稿实现,顺带修 FIX-1(促销卡渐变对比度)、FIX-2helperStyle)、m2README 门禁参数);接入 `--dart-define` 注入 API 地址。
4. **端到端用例**:注册 → 登录 → 获取当前用户 → 退出,进 CI。
启动条件已满足(持久化纵切完成 + 复核无否决)。
## 环境与构建(新成员必读)
- 后端构建:`JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./mvnw clean test`(本机默认 JDK 版本过高,必须显式指 17;需 Docker 供 Testcontainers 起 postgres:16)。
- Flutter 门禁:`dart format --output=none --set-exit-if-changed lib test` / `flutter analyze` / `flutter test`
- 文档门禁:`mkdocs build --strict`
- 测试数据库策略见 ADR-006:自动化测试一律 Testcontainers,个人手动联调用本机 PostgreSQL(环境变量注入连接信息)。
## 报告目录
01-06 为开工前六角色分析(任务分解/技术评估/现状核实/UI 规范/埋点规划/质量审计),07-08 第一波交付,09-15 第二波交付与复核。文件名编号即时间顺序,各报告首节均有摘要。
+18
View File
@@ -5,5 +5,23 @@ nav:
- 首页: index.md
- 开发文档:
- 开发实施计划: development/development-plan.md
- Git 工作流规范: development/git-workflow.md
- 第一迭代:
- 进展看板: development/iterations/iteration-1/index.md
- 01 任务分解: development/iterations/iteration-1/01-pm-task-breakdown.md
- 02 技术评估: development/iterations/iteration-1/02-dev-technical-assessment.md
- 03 现状核实: development/iterations/iteration-1/03-reality-check.md
- 04 登录 UI 设计规范: development/iterations/iteration-1/04-ui-login-design-spec.md
- 05 埋点规划: development/iterations/iteration-1/05-experiment-tracking-plan.md
- 06 质量审计: development/iterations/iteration-1/06-evidence-audit.md
- 07 后端基线改造报告: development/iterations/iteration-1/07-backend-baseline-report.md
- 08 Flutter 主题迁移报告: development/iterations/iteration-1/08-flutter-theme-report.md
- 09 任务板更新: development/iterations/iteration-1/09-pm-board-update.md
- 10 持久化纵切报告: development/iterations/iteration-1/10-backend-persistence-report.md
- 11 第一波复核: development/iterations/iteration-1/11-reality-recheck.md
- 12 UI QA 与登录组装稿: development/iterations/iteration-1/12-ui-design-qa-and-assembly.md
- 13 埋点实现规范: development/iterations/iteration-1/13-tracking-implementation-spec.md
- 14 里程碑证据档案: development/iterations/iteration-1/14-evidence-milestone-dossier.md
- 15 Git 收尾报告: development/iterations/iteration-1/15-git-workflow-report.md
- 架构:
- 技术决策记录: architecture/decisions.md