From 5cc63615345fc30cb342a336292d7decdffea98f Mon Sep 17 00:00:00 2001 From: Lixi20 Date: Mon, 14 Sep 2026 11:19:13 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20v0.4.0=20=E5=8F=91=E5=B8=83=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=20+=20=E5=8A=9F=E8=83=BD=E6=B8=85=E5=8D=95=20M3.5=20?= =?UTF-8?q?=E8=8A=82=20+=20=E5=8F=91=E5=B8=83=20E2E=20=E5=9B=9E=E5=BD=92?= =?UTF-8?q?=E6=8A=A5=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - releases.md 新增 v0.4.0(M3.5 体验补齐):三仓 tag、门禁 8 项证据、 首次经 PR 流程发布的操作记录与流程验证结论、期间安全事件索引 - checklist 变更登记:**回归清单由两份改为四份**(M1/M2/M3/M3.5), 原则「每个引入对外端点的迭代都应有对应 E2E 脚本并在此后每次发布回归」 - feature-checklist 新增第 13 节(M3.5 共 13 条) - 08 号报告:E2E 四份 42/42 场景 234 断言零失败、契约偏差 0; 另实证零迁移在存量库升级路径(Flyway: No migration necessary) Co-Authored-By: Claude Fable 5 --- docs/development/feature-checklist.md | 24 +- .../08-release-e2e-regression.md | 349 ++++++++++++++++++ docs/development/releases.md | 60 +++ mkdocs.yml | 1 + 4 files changed, 433 insertions(+), 1 deletion(-) create mode 100644 docs/development/iterations/iteration-3.5/08-release-e2e-regression.md diff --git a/docs/development/feature-checklist.md b/docs/development/feature-checklist.md index 920a8c5..0804c65 100644 --- a/docs/development/feature-checklist.md +++ b/docs/development/feature-checklist.md @@ -2,7 +2,7 @@ > 目的:直观呈现哪些功能**已完成且有自动化测试**、哪些**部分完成**、哪些**尚未开始**,方便针对性验证与回归。 > 维护约定:每波工单合入后由执行人更新本清单;状态以 `dev` 分支 + 门禁全绿为准。 -> 最后更新:2026-09-10(M3 收官:patbond-api `8089c06` 334 测试、patbond-flutter `0e87413` 502 测试,均门禁全绿;E2E 烟囱 14/14 契约偏差 0;M2 条目见第 7~9 节,M3 见第 10~12 节) +> 最后更新:2026-09-14(**v0.4.0 发布**:patbond-api `3cd8005` 379 测试、patbond-flutter `fbcd734` 597 测试,均门禁全绿;E2E 四份 42/42 场景零失败、契约偏差 0;M2 条目见第 7~9 节,M3 见第 10~12 节,M3.5 见第 13 节) 图例:✅ 已完成且已测试 | 🟡 部分完成/有已知限制 | ⬜ 未开始 @@ -220,3 +220,25 @@ curl -s -i http://127.0.0.1:8082/internal/users/by-username/demo_user | head -1 | 逐卡 Feed 曝光 | ⬜ | ADR-020 否决(量级测算 7~14 个月击穿分区阈值 + 无背压);留 backlog 待 M4+ 服务端下发日志 | | Android 真机验证(M2 两项 + M3 四项) | 🟡 | 步骤全部备齐在[真机验证清单](device-verification.md);桌面 `platform=linux` 整批 400 属契约内,落库只能真机验 | | `eventVersion` 口径定型 | ⬜ | 契约描述可两读、服务端不校验、客户端硬编码 1(E2E 观察项 2) | + +--- + +# M3.5 体验补齐(v0.4.0) + +## 13. 用户资料与头像 + +| 功能 | 状态 | 自动化测试 | 说明 | +| --- | --- | --- | --- | +| 中文本地化(Material 内置组件) | ✅ | widget 测试断言中文渲染 | 此前从未配 `flutter_localizations`,Flutter 静默回退英文;顺带 `datePickerTheme` 上品牌色(只复用已审计色对) | +| 日期录入共享层 `pickAppDate`(7 处调用点收口) | ✅ | 断言 `DatePickerDialog.firstDate/lastDate` 防后续放宽 | 保留手输铅笔 + 表单行「今天」快捷(原生 picker 无法注入弹窗内动作);月份网格未做(🟡 遗留) | +| `GET /api/v1/me` 补 nickname/avatarUrl | ✅ | 契约矩阵 + E2E | **不做 username 回退**(回退只在客户端展示层做一层,避免编辑页预填后固化) | +| `PATCH /api/v1/me`(昵称 + 头像,三态) | ✅ | 全响应矩阵 5 格 + E2E 三态专项 | 键缺省=不改 / 显式 null=清空 / 给值=设置;纯空白昵称 400;空 patch 400;昵称长度按**码点**计(32 emoji 通过、33 拒) | +| 用户头像上传(`purpose=user_avatar`) | ✅ | E2E 两步上传 + 字节一致 | `avatarAssetId` 只写不读,「有头像」等价 `avatarUrl != null` | +| 宠物头像读写(`purpose=pet_avatar`) | ✅ | 契约矩阵 + E2E | **权限按本次碰了哪些字段定档**:仅头像=WRITE(owner+caregiver)、含资料字段=MANAGE、混合取更严 | +| `GET /api/v1/me/community-stats` | ✅ | 2 格矩阵 + E2E(含草稿不计、他人帖不串号) | 读侧实时聚合(`SUM(like_count)`),无新冗余列;本人 published 且未软删,自赞计入 | +| 资料页真实化 + 编辑页 | ✅ | 四态 + 展示名回退双路 + PATCH 载荷断言 | 176 行硬编码 demo 退役;四项统计全部真实 | +| 首页问候语真实化 | ✅ | widget 用例反向钉住「保留项仍在」 | 其余首页 demo 按 **ADR-022 刻意保留**并在代码标注去向(天气/位置待外部服务、圈子=话题属 ADR-018 剪出、促销卡属 M5) | +| 花费卡显示实际月份 + 数据卡可点提示 | ✅ | `monthlyExpenseCardLabel` 纯函数单测 | 同年「9 月花费」、跨年「2026/12 花费」、非法串回退「本月花费」 | +| 契约 v1.4.0 冻结 + 四模块快照同步 | ✅ | 矩阵 181 格、mutation 自证 | 对 v1.3.0 纯增量;11 格红转绿 | +| 「我的收藏与草稿」列表页 | 🟡 | — | 后端与仓库层已就位,缺 2 页面 + 导航(建议独立 S~M 工单) | +| 真机项(头像上传弱网、头像缓存) | 🟡 | — | 步骤已登记[真机验证清单](device-verification.md) M3.5 节 | diff --git a/docs/development/iterations/iteration-3.5/08-release-e2e-regression.md b/docs/development/iterations/iteration-3.5/08-release-e2e-regression.md new file mode 100644 index 0000000..185b5c5 --- /dev/null +++ b/docs/development/iterations/iteration-3.5/08-release-e2e-regression.md @@ -0,0 +1,349 @@ +# 08 v0.4.0 发布前 E2E 回归门禁 + +> 作者:QA(Frontend Developer 角色执行) +> 日期:2026-09-14 +> 任务:v0.4.0 发布 checklist 第 2 步「compose 全栈起,跑 E2E 烟囱脚本,全场景 PASS」 +> 输入基线:patbond-api dev@`3cd8005`(379 测试)、patbond-flutter dev@`6945436`(597 测试)、 +> 契约 `openapi.yaml` **v1.4.0**(32 路径 / 45 操作 / 75 schema) +> 提交:`test_e2e_m35_manual.dart` 新增,已推 `origin/dev`(见 §7) +> 结论先行:**门禁 PASS。四份脚本 42/42 场景全绿(M1 7/7、M2 11/11、M3 14/14、M3.5 新写 10/10), +> 共 234 条断言零失败;契约偏差 **0**;M3.5 新增对外面 8 项逐项验证通过;三门禁命令全绿 +> (analyze 0 issue、format 0 changed、flutter test 597 passed)。patbond-api 与 patbond-doc +> 代码/契约零改动。** + +--- + +## 1. 结论表:四份脚本场景通过数 + +| 脚本 | 迭代 | 场景 | 通过 | 断言数 | 退出码 | 判定 | +| --- | --- | --- | --- | --- | --- | --- | +| `test_e2e_manual.dart` | M1 | 7 | **7/7** | 7 | 0 | ✅ PASS | +| `test_e2e_m2_manual.dart` | M2 | 11 | **11/11** | 44 | 0 | ✅ PASS | +| `test_e2e_m3_manual.dart` | M3 | 14 | **14/14** | 88 | 0 | ✅ PASS | +| `test_e2e_m35_manual.dart` | **M3.5(本次新写)** | 10 | **10/10** | 95 | 0 | ✅ PASS | +| **合计** | | **42** | **42/42** | **234** | | ✅ **PASS** | + +- **契约偏差数:0**。四份脚本对 v1.4.0 冻结契约的每一处断言(状态码、业务码、字段键集合、 + 可空性、枚举、错误谱)均命中,无一处需要"以实现为准"地放宽期望。 +- **失败项:无**。M3.5 脚本首次运行即 10/10 通过(未出现需要调整断言或重试的场景)。 +- 三份既有回归脚本**逐字未改**——回归价值恰在于脚本本身不动。 + +### 执行顺序与理由 + +依次串行 M1 → M2 → M3 → M3.5,不并行。M3 场景 10/11 对 Feed 做**全量翻页前后计数比对** +(`idsAfter.length == idsBefore.length - 1`),任何并发发帖都会污染该断言;M3.5 场景 10 也要发帖。 +串行是这两个断言成立的前提。 + +--- + +## 2. M3.5 新增对外面逐项验证结果 + +任务列出的 8 项必覆盖点,逐项对照: + +| # | 要求 | 覆盖场景 | 结果 | 关键证据 | +| --- | --- | --- | --- | --- | +| 1 | 注册 → `GET /me` 断言 nickname 为 null(未设)、avatarUrl 为 null | [1/10] | ✅ | 全新账号 `nickname=null avatarUrl=null`,两键**恒在**;同时断言 Me 形态在冻结的 6 字段内 | +| 2 | `PATCH /me` 设昵称 → 回读一致 | [3/10] | ✅ | PATCH 回显与 `GET /me` 回读**逐字段一致**(userId/username/phone/createdAt/avatarUrl 五字段逐一比对),证明两动词同一 `Me` 形态 | +| 3 | 三态:只带 nickname 不影响头像 / 显式 `null` 清空生效 / 空 patch 400 / 纯空白 400 | [5/10] | ✅ | 见 §2.1 展开 | +| 4 | 昵称边界:1 可、32 可、33 拒、**32 emoji 按码点计应通过** | [6/10] | ✅ | 见 §2.2 展开 | +| 5 | 用户头像两步上传(user_avatar) → 挂载 → avatarUrl 可下载且字节一致 → 响应**不含** avatarAssetId | [4/10] | ✅ | 见 §2.3 展开 | +| 6 | 宠物头像(pet_avatar) → 挂载 → 详情/列表 avatarUrl 可用;错 purpose 被拒 | [8/10] + [9/10] | ✅ | 见 §2.4 展开。**错 purpose 实测为 404/40405**(与契约一致,非 42203) | +| 7 | `GET /me/community-stats`:空 0/0 → 发帖 → 自赞 → 1/1 → 软删归零 | [10/10] | ✅ | 见 §2.5 展开 | +| 8 | purpose 白名单外(`id_card`)创建上传被拒 | [2/10] | ✅ | `400/40000`,message 回显白名单三值 | + +`/internal` 侧按任务要求**未测**(内部端点不入公网契约)。 + +### 2.1 `PATCH /api/v1/me` 三态与错误谱(场景 5,13 条断言) + +| 请求 | 期望 | 实测 | 说明 | +| --- | --- | --- | --- | +| `{nickname: "豆豆"}`(头像已在) | 200,头像不变 | 200,`avatarUrl` 仍非 null | **键缺省 = 不改**:只带昵称的 patch 不碰头像 | +| `{nickname: null}` | 200,昵称清空、头像留存 | 200,`nickname=null`、`avatarUrl` 非 null | **显式 null = 清空**,且只作用于携带的键;回读复核已持久化 | +| `{avatarAssetId: null}` | 200,`avatarUrl` 回落 null | 200,`avatarUrl=null` | 头像清除生效 | +| `{}` | 400/40000 | 400/40000 | message:`请至少提交一个可更新字段:nickname 或 avatarAssetId`——不静默 200 | +| `{nickname: " "}` | 400/40000 | 400/40000 | **纯空白不隐式清空**,清空只留显式 null 一条路 | +| `{nickname: ""}` | 400/40000 | 400/40000 | 空串与纯空白同判 | +| `{bio: "…"}` | 400/40000 | 400/40000 | 只带未声明字段 = 空 patch(`bio` 列存在但 M3.5 未开放读写) | +| `{avatarAssetId: "not-a-uuid"}` | 400/40000 | 400/40000 | 参数错先于引用校验 | +| 坏 JSON(`{"nickname": `) | 400/40000 | 400/40000 | 非法 body | +| 无 token | 401/40101 | 401/40101 | 写入口同样强制鉴权 | +| 同 body 连发两次 | 两次 200 且状态一致 | 两次 200,nickname 与"有头像"布尔一致 | 无 `Idempotency-Key`,天然幂等 | + +每一处 `PATCH` 成功响应都跑了 `assertMeShape`:**键集合不超出冻结 6 字段**,且**不含 `avatarAssetId`**。 + +### 2.2 昵称边界——码点计而非 UTF-16 长度(场景 6,7 条断言) + +| 昵称 | 码点 | UTF-16 长度 | 期望 | 实测 | +| --- | --- | --- | --- | --- | +| `柴` | 1 | 1 | 200 | ✅ 200 | +| `猫`×32 | 32 | 32 | 200 | ✅ 200 | +| **`🐕`×32** | **32** | **64** | **200** | ✅ **200** | +| `猫`×33 | 33 | 33 | 400/40000 | ✅ 400/40000 | +| `🐕`×33 | 33 | 66 | 400/40000 | ✅ 400/40000 | +| `" 豆豆 "` | — | — | 200 且存为 `豆豆` | ✅ btrim 对齐 | + +**32 emoji 昵称通过是这一格的核心**:若应用层按 `String.length()` 校验,UTF-16 长度 64 会被误拒, +而 PostgreSQL `char_length` 数的是码点、数据库本可存下。实测通过 + 回读逐字一致 +(`runes.length == 32`,无截断),证明校验确实按码点。上边界对 CJK 与 emoji 两种字符集行为一致。 + +### 2.3 用户头像端到端(场景 4,9 条断言) + +链路:`POST /media/uploads`(purpose=**user_avatar**) → 预签名 PUT 直传 MinIO → +`POST /complete`(→ready) → `PATCH /me {avatarAssetId}` → `GET `。 + +- `avatarUrl` 由 null 转为**现签预签名 GET**,携带 `X-Amz-Signature` SigV4 query 族。 +- objectKey 前缀随 purpose:`patbond-media/user_avatar/2026/09/{assetId}`——证明 §4 的前缀规则生效。 +- **预签名 GET 取回 344 字节,与上传逐字节一致**(`ListEquality` 全等比对)。 +- `GET /me` 再取一次 `avatarUrl` 仍可下载 → **每次响应现签**(客户端不得持久化)。 +- **同一对象去掉签名直访 → 403**:桶保持私有,头像不靠公开读。 +- 挂头像时 `nickname` 未被改动(三态交叉验证)。 +- 挂载/清空/初始三态的响应**均不含 `avatarAssetId`**。 + +### 2.4 宠物头像(场景 8 + 9,20 条断言) + +| 断言 | 结果 | +| --- | --- | +| 建档响应 `avatarUrl` 键恒在且为 `null`;**不含** `avatarAssetId` | ✅ | +| `PATCH /pets/{id} {version, avatarAssetId}` 仅头像字段 → 200(WRITE 档) | ✅ | +| `version` 0→1(头像与资料共用同一把乐观锁) | ✅ | +| 只带 avatarAssetId 未动 `name`/`species` | ✅ | +| **详情** `GET /pets/{id}` 的 avatarUrl 可下载且字节一致 | ✅ | +| **列表** `GET /pets` 的 avatarUrl 可下载且字节一致(两处各自现签、均有效) | ✅ | +| 列表项同样不外露 `avatarAssetId` | ✅ | +| `{avatarAssetId: null}` 显式清除 → `avatarUrl=null`,回读复核 | ✅ | + +asset 校验矩阵(场景 9): + +| 引用的 asset | 期望 | 实测 | message | +| --- | --- | --- | --- | +| 错用途 `user_avatar`(跨域头像不通用) | 404/40405 | ✅ | `该媒体资源的用途不是 pet_avatar,不能作为宠物头像` | +| 错用途 `post_image` | 404/40405 | ✅ | 同上 | +| 幽灵 assetId | 404/40405 | ✅ | `媒体资源不存在` | +| 他人(B)的 asset | 404/40405 | ✅ | `媒体资源不存在`(防枚举合并) | +| 本人 `pet_avatar` 但仍 `uploading` | 422/42203 | ✅ | `媒体尚未就绪` | +| 缺 `version`(只带 avatarAssetId) | 400/40000 | ✅ | `version 不能为空`——即便只改头像仍必填 | + +**副作用复核**:以上 6 次失败的 PATCH 之后,宠物 `avatarUrl` 仍为 null 且 `version` 未 +1 +——失败路径零副作用。 + +> **任务提出的"错 purpose 以实现为准(40405 或 42203)"已定论:实测 404/40405**,与 +> `openapi.yaml` v1.4.0 的 `paths./api/v1/pets/{petId}.patch` 404 描述及 03 号报告 §2.6 +> 完全一致。42203 只留给"用途相符但状态未就绪"。**契约与实现无偏差。** + +用户侧同构矩阵(场景 7,6 条断言):幽灵 / 他人(B) / 错用途 `post_image` / 错用途 `pet_avatar` +四路均 404/40405,且**幽灵 id 与他人 asset 的响应体逐字节一致**(不能据差异探出 id 是真的); +本人 `user_avatar` 但 `uploading` → 422/42203(用**真未直传**的 asset,非 SQL 造数据)。 +5 次失败后头像未被改动。 + +### 2.5 `GET /api/v1/me/community-stats`(场景 10,14 条断言) + +| 步骤 | 期望 | 实测 | +| --- | --- | --- | +| 无 token | 401/40101(唯一错误谱) | ✅ `401/40101` | +| A 全新账号(无帖) | 200,`{0, 0}`,**永不 404** | ✅ `{"receivedLikeCount":0,"publishedPostCount":0}` | +| 形态 | 恰两字段,非 null | ✅ 无多余键 | +| 发布 1 帖 | `{0, 1}` | ✅ | +| **A 自赞该帖** | `{1, 1}`(自赞**计入**) | ✅ `{"receivedLikeCount":1,"publishedPostCount":1}` | +| 对账 | 帖详情 `likeCount` == `receivedLikeCount` | ✅ 两处均为 1,**口径对得上** | +| 再建 1 草稿 | `{1, 1}`(**草稿不计**) | ✅ 作品数仍 1 | +| 再发布 1 帖 | `{1, 2}`(多帖求和) | ✅ | +| B 发布并自赞一帖 | A 视角仍 `{1, 2}`(**他人帖不串号**) | ✅ 主体恒为 token 里的调用者 | +| **软删被赞的那帖** | `{0, 1}`(获赞**归零**) | ✅ `{"receivedLikeCount":0,"publishedPostCount":1}` | + +超出任务要求补测的两格:**草稿不计** 与 **他人帖不串号**——前者是 §2.5 口径表最容易实现错的一格, +后者证明"路径上没有 userId"确实等价于主体隔离。 + +--- + +## 3. 契约偏差数:0 + +逐面核对 `patbond-doc/docs/api/openapi.yaml` v1.4.0,脚本断言与契约声明零分歧: + +| 契约条目 | 声明 | 实测 | +| --- | --- | --- | +| `info.version` | 1.4.0 | — | +| 路径 / 操作 / schema 计数 | 32 / 45 / 75 | ✅ 与基线一致(机械计数复核) | +| `Me.required` | `[userId, username, nickname, avatarUrl, createdAt]` | ✅ 五键恒在;`phone` 可选可空 | +| `Me` 不含 `avatarAssetId` | 响应一律不外露 | ✅ 6 处响应(GET 初始/PATCH 回显/挂载后/清空后 等)逐一断言 | +| `Pet` 加 `avatarUrl` 进 required | 键恒在、值可空 | ✅ 创建/详情/列表三处 | +| `Pet` 不含 `avatarAssetId` | 只写不读 | ✅ 三处断言 | +| `UpdateMeRequest` 三态 | 缺省/null/给值 | ✅ 三态各一格 | +| `PATCH /me` 400 谱 | 空 patch、长度越界、纯空白/空串、非法 UUID、坏 JSON | ✅ 6 格全中 | +| `PATCH /me` 401/404/422 | 40101 / 40405 / 42203 | ✅ 全中 | +| `PATCH /pets/{petId}` 新增 404/40405、422/42203 | asset 四态 + 未就绪 | ✅ 6 格全中 | +| `CommunityStats` | 两 int64、required 非 nullable、空数据 0、永不 404 | ✅ 全中 | +| `CreateMediaUploadRequest.purpose` 枚举 | `[post_image, user_avatar, pet_avatar]`,外值 400/40000 | ✅ 三值放行 + `id_card` 被拒 | + +--- + +## 4. 环境记录 + +| 项 | 值 | +| --- | --- | +| 日期 | 2026-09-14 | +| patbond-api | `dev@3cd8005`(工作树干净,零改动) | +| patbond-doc | `dev@38d9e97`(契约零改动;本报告为新增文件) | +| patbond-flutter | `dev@6945436` + 新增 `test_e2e_m35_manual.dart` | +| 契约 | `openapi.yaml` v1.4.0 — **32 路径 / 45 操作 / 75 schema**(机械计数复核一致) | +| Docker | 29.7.2;Docker Compose 5.5.1 | +| Dart SDK | 3.12.2 (stable) | +| Flutter | 3.44.6 (stable),framework `ee80f08bbf` | +| JDK(打包) | `JAVA_HOME=/usr/lib/jvm/java-17-openjdk`(17.0.20.1) | + +### 六容器实况 + +| 容器 | 镜像 | 状态 | 端口 | +| --- | --- | --- | --- | +| `patbond-postgres-1` | `postgres:18`(18.6) | Up (healthy) | 不发布(仅容器网) | +| `patbond-minio-1` | `minio/minio:RELEASE.2025-04-22T22-12-26Z` | Up (healthy) | 9000 | +| `patbond-auth-1` | `patbond-auth`(本地构建) | Up | 8081 | +| `patbond-user-1` | `patbond-user` | Up | 8082 | +| `patbond-pet-1` | `patbond-pet` | Up | 8083 | +| `patbond-community-1` | `patbond-community` | Up | 8084 | + +MinIO 镜像 tag 与集成测试的 Testcontainer 钉同一版本(三环境零分叉,ADR-016)。 + +### 起停命令 + +```bash +cd <你的工作区>/patbond-api +JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./mvnw -DskipTests package # exit 0 +docker compose up -d --build # 等 postgres/minio healthy +# …跑四份脚本… +docker compose down +``` + +### 附带取证:M3.5 的"零 Flyway 迁移"在真实升级路径上成立 + +本次 compose 起在**已存在的 `pgdata` volume** 上(非全新库),user 服务(迁移唯一所有者)日志: + +``` +o.f.core.internal.command.DbValidate : Successfully validated 5 migrations +o.f.core.internal.command.DbMigrate : Current version of schema "public": 5 +o.f.core.internal.command.DbMigrate : Schema "public" is up to date. No migration necessary. +``` + +即 M3.5 相对 M3 **未新增任何 Flyway 版本**(仍停在 V5,V6 留给后续真正建表的迭代), +ADR-022 的"零迁移"前提在**存量库升级**场景下得到实证,而不只是全新安装。 +auth / pet / community 三服务不跑 Flyway(无迁移输出),与设计一致。 + +### 配置侧复核(03 号报告 §4 的部署警告) + +03 号报告提醒:若环境的 `application.yml` 显式写了 `allowed-purposes: post_image`,头像上传会 400。 +compose 挂载的是 `patbond-user/src/main/resources/application.yml.sample`,实测该文件第 59 行为 +`allowed-purposes: post_image,user_avatar,pet_avatar` ✅;pet 服务已补齐 +`PATBOND_MINIO_PUBLIC_ENDPOINT/ACCESS_KEY/SECRET_KEY`(compose 第 107 行), +故宠物侧 `avatarUrl` 才可能非 null——场景 8 的下载成功即其端到端证明。 + +--- + +## 5. 门禁三命令输出 + +```bash +cd <你的工作区>/patbond-flutter +``` + +### 5.1 `flutter analyze` + +``` +Analyzing patbond-flutter... +No issues found! (ran in 2.1s) +``` +退出码 **0**。(新增的根目录脚本也在分析范围内——`flutter analyze` 覆盖整仓 `.dart`。) + +### 5.2 `dart format --set-exit-if-changed lib test` + +``` +Formatted 161 files (0 changed) in 0.66 seconds. +``` +退出码 **0**。 + +补充:该命令的范围是 `lib test`,**不覆盖仓库根目录的四份 E2E 脚本**。为免留下格式债, +另跑了一次显式检查,四份并列脚本均已 format-clean: + +``` +$ dart format --output=none --set-exit-if-changed \ + test_e2e_manual.dart test_e2e_m2_manual.dart \ + test_e2e_m3_manual.dart test_e2e_m35_manual.dart +Formatted 4 files (0 changed) in 0.06 seconds. +``` +退出码 **0**。(新脚本首版曾 1 changed,已 `dart format` 归一;formatter 拆出的一处 +无花括号 `if` 已补花括号,避免 `curly_braces_in_flow_control_structures` 风格债。 +归一后**重跑脚本复核仍 10/10**,即上表证据出自最终提交的文件。) + +### 5.3 `flutter test` + +``` +00:30 +597 ~2: All tests passed! +``` +退出码 **0** — **597 passed**、2 skipped、0 failed,与基线 597 **逐格一致**(本次未新增单测: +E2E 脚本是独立可执行的手动脚本,不进 `flutter test` 反应堆)。 + +--- + +## 6. 失败项与判定 + +**无失败项。** + +| 类别 | 数量 | 说明 | +| --- | --- | --- | +| 真实回归 | **0** | M1/M2/M3 三份既有脚本 32/32 场景全绿,M3.5 新增字段未破坏任何既有断言 | +| 契约偏差 | **0** | 见 §3 | +| 需放宽的断言 | **0** | 含任务预留的"错 purpose 以实现为准"一格——实测与契约一致(40405) | +| 环境/配置问题 | **0** | §4 两处部署风险点均实测已闭环 | + +**判定:v0.4.0 发布 checklist 第 2 步 PASS。** + +### 本次门禁未覆盖的范围(不构成 FAIL,按既定方案挂起) + +- **真机四项**(`development/device-verification.md`):弱网上传头像、预签名 URL 过期后重取、 + caregiver 账号改宠物头像、资料页获赞数与帖子详情点赞数对账。本脚本纯 `dart:io HttpClient` + 跑后端契约面,不驱动 Flutter UI,故这四项仍按 M3 起的方案 A 挂起。 + 其中"获赞数对账"的**后端口径**已在场景 10 证明(详情 `likeCount` == `receivedLikeCount`), + 真机侧待验的只剩 UI 呈现。 +- **caregiver 分档**(viewer 改头像 403 / caregiver 夹带混合 403):需要第二账号建立 + `pet_owners` 照护关系,M2/M3 脚本均未铺该前置,本次亦未铺;后端已有 + `PetAvatarIntegrationTest` 三段断言覆盖(03 号报告 §2.4)。 +- **并发** `PATCH /me` **列级 UPDATE**:由后端 `MeProfileIntegrationTest.concurrentDisjointPatchesBothSurvive` + 以 `CyclicBarrier` 覆盖,单线程脚本不复现,本次以"重放幂等"作为可观测替代。 +- `/internal/**`:按任务要求不测(不入公网契约)。 + +--- + +## 7. 脚本落位与提交 + +| 项 | 值 | +| --- | --- | +| 新增文件 | `<你的工作区>/patbond-flutter/test_e2e_m35_manual.dart`(仓库**根目录**,与前三份并列,**不在 `test/`**) | +| 行数 | 1166 行,10 场景 / 95 断言 | +| 依赖 | 无(纯 `dart:io HttpClient` + `dart:convert`,不引 `collection` 包——`ListEquality` 内联,与 M3 脚本同先例) | +| 运行 | `cd <你的工作区>/patbond-flutter && dart run test_e2e_m35_manual.dart` | +| 提交 | `dev` 分支(`main` 受保护禁直推) | + +### 脱敏纪律(与 M3 脚本一致) + +- **token**:仅打印前 20 字符 + `...`。 +- **预签名 URL**:`redactSignedUrl()` 保留 scheme/host/port/path,**签名 query 整体替换为 + ``**——objectKey 前缀(`user_avatar/2026/09/…`)仍可读,便于核对 purpose + 前缀规则,而签名族一字不落盘。 +- **Idempotency-Key**:日志中以 `` 占位,不打印真实 UUID。 +- 超长昵称按"前 4 码点 + 码点数/UTF-16 长度"概述,不整条刷屏。 + +### 硬约束遵守情况 + +- `patbond-api`:`git status --porcelain` **空**,HEAD 仍 `3cd8005` — 代码/契约零改动,仅起 compose。 +- `patbond-doc`:契约 `openapi.yaml` 零改动,HEAD 仍 `38d9e97`;本报告为新增文件, + **只写不提交**(随后统一入档);`mkdocs.yml` **未动**。 +- `patbond-flutter`:仅新增 1 个文件,无既有文件改动。 + +--- + +## 8. 交接给发布 checklist 后续步骤 + +1. **第 2 步可勾**:42/42 场景、234 断言、契约偏差 0、门禁三命令全绿。 +2. 本报告入档时需挂 `mkdocs.yml` 导航(本次按纪律未动)。 +3. 建议把 `test_e2e_m35_manual.dart` 与前三份一同写进发布 checklist 的常设回归清单—— + 下个迭代的发布门禁应跑**四份**而非三份。 +4. 真机四项与 caregiver 分档仍挂起,见 §6;若 v0.4.0 定位为可发布版本, + 建议在 `device-verification.md` 明确登记"头像相关四项待真机补验",避免遗忘。 +5. 收尾已执行 `docker compose down`(volume 保留,未 `-v`)。 diff --git a/docs/development/releases.md b/docs/development/releases.md index 6671191..a9d57ff 100644 --- a/docs/development/releases.md +++ b/docs/development/releases.md @@ -6,6 +6,66 @@ --- +## v0.4.0 — M3.5 体验补齐(2026-09-14) + +**首次经 PR 流程发布**(v0.3.0 为直推 main,分支保护启用前)。 + +### 三仓 tag + +| 仓库 | tag | 提交 | 内容 | +| --- | --- | --- | --- | +| patbond-api | `v0.4.0` | `3cd8005` | 用户资料读写 + 宠物头像 + 社区统计;379 测试;契约 v1.4.0 快照 | +| patbond-flutter | `v0.4.0` | `fbcd734` | 中文本地化 + 日期录入收口 + 资料页/编辑页 + 头像接线;597 测试;四份 E2E 脚本 | +| patbond-doc | `v0.4.0` | 本记录所在提交 | 契约 v1.4.0 + M3.5 报告 01~08 + 安全事件复盘 + 服务器暴露面清单 | + +### 版本内容(相对 v0.3.0 的增量) + +- **用户资料**:`GET /me` 补 `nickname`/`avatarUrl`;新增 `PATCH /me`(三态:键缺省=不改 / 显式 null=清空 / 给值=设置) +- **头像**:用户与宠物头像上传(复用 M3 的 MinIO 两步上传),`purpose` 白名单 +`user_avatar`/+`pet_avatar` +- **社区统计**:`GET /me/community-stats`(获赞总数 + 作品数,读侧实时聚合,无新冗余列) +- **客户端体验**:中文本地化(此前从未配置 `flutter_localizations`,Flutter 静默回退英文)、日期录入收口共享层(7 处调用点 + 手输 + 「今天」快捷)、资料页 demo 退役、花费卡显示实际月份 +- **契约**:openapi.yaml **v1.4.0**(32 路径/45 操作/75 schema),**对 v1.3.0 纯增量**(无字段删改/类型变更/必填收紧,已写入 `info.description`) +- **零 Flyway 迁移**(ADR-022):所需列 V1/V3/V5 早已存在,`purpose` 白名单为配置项 +- **决策**:ADR-022 + +### 发布门禁证据 + +| 门禁项 | 结果 | 证据 | +| --- | --- | --- | +| 全量测试 | ✅ | api **379** / flutter **597**,双绿 | +| **E2E 回归(四份,同环境串行)** | ✅ | M1 7/7 + M2 11/11 + M3 14/14 + **M3.5 10/10** = **42/42 场景、234 断言零失败** — [08 号报告](iterations/iteration-3.5/08-release-e2e-regression.md) | +| 契约偏差 | ✅ **0** | 逐场景对照 v1.4.0;契约计数机械复核 32/45/75 | +| 契约向后兼容 | ✅ | v1.3.0→v1.4.0 纯增量,v1.3.0 客户端无需改动 | +| 契约一致性矩阵 | ✅ | 173→**181 格**,11 格「未声明字段即漂移」的红全部转绿,mutation 三处注毒自证 | +| **零迁移在存量库上实证** | ✅ | compose 起于既有 `pgdata` volume,Flyway 日志 `Current version: 5 / No migration necessary.` | +| CI(push + **pull_request**) | ✅ | api 5m31s / 6m42s;flutter 3m46s / 3m44s;doc 2m58s | +| 凭证防泄漏 | ✅ | `check-secrets.sh --all` exit 0 | + +### 发布操作记录 + +1. **首次走 PR 流程**(分支保护生效后):两仓各建 `dev → main` PR(标题 `v0.4.0 M3.5 体验补齐`),等 `pull_request` 状态检查转绿后以 **Fast-forward only** 合并 → **main == dev、零合并提交、历史保持线性**。 +2. **流程本身也被验证了**:v0.3.0 是直推 main,「PR + 状态检查」这套机制从未真实走过。本次确认:`pull_request` 触发器会跑、分支保护确实要求该检查通过、PR 会自动跟随 dev 新提交重新检查(发布途中 E2E 脚本提交推入 dev,PR 随即重跑)。 +3. **CI 从安全事件恢复后的首次完整验证**:见下方「期间事件」。 + +### 期间事件:Gitea gitconfig 注入(已闭环) + +本迭代期间发生一次安全事件,CI 全面中断约一天。完整复盘见 [07 号报告](iterations/iteration-3.5/07-security-incident-20260911.md),纪律固化见新建的[服务器暴露面清单](server-exposure.md)。要点:攻击**未达成代码执行**、三仓代码经核对**未被篡改**、**无系统层入侵**;处置后外部验证四项通过。 + +### checklist 变更(下次发布适用) + +- **回归清单由两份改为四份**:M1/M2/M3/M3.5。原则是「每个引入对外端点的迭代都应有对应的 E2E 脚本,并在此后每次发布回归」。 +- 发布前若 dev 仍有提交推入,PR 检查会重跑——**以最终 HEAD 的检查结果为准**,不要用旧的绿色状态放行。 + +### 已知遗留 + +**真机验证**:M2 两项 + M3 四项 + M3.5 新增(头像上传弱网、头像缓存)——步骤全部备齐在[真机验证清单](device-verification.md),待设备到位。 + +**功能遗留**:「我的收藏与草稿」列表页(后端就绪,缺 2 页面)、月份网格选择器、`SegmentedButton` 主题债、`widthPx/heightPx` 恒 null、`eventVersion` 口径未定型、uploading 超时清理任务、429 限流、话题/关注列表/作者主页(ADR-018 剪出)。 + +**跨迭代技术债**:access token 黑名单、`/internal` 改 mTLS。 + +**待评估**:文档站公开可访问是否加访问控制(见服务器暴露面清单)。 + ## v0.3.0 — M3 社区(2026-09-10) **首次正式发布**,发布流程首次演练。 diff --git a/mkdocs.yml b/mkdocs.yml index 3525384..1903946 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -105,6 +105,7 @@ nav: - 05 资料页与头像 UI: development/iterations/iteration-3.5/05-profile-avatar-ui.md - 06 M3.5 收口: development/iterations/iteration-3.5/06-wave2-closure.md - 07 安全事件复盘: development/iterations/iteration-3.5/07-security-incident-20260911.md + - 08 发布 E2E 回归: development/iterations/iteration-3.5/08-release-e2e-regression.md - API: - 契约说明: api/index.md - 架构: