docs: 迁入第一迭代过程报告并建立进展看板
- 新增 development/iterations/iteration-1/:15 份角色报告 + 进展看板(已完成/未闭环/下一步),作为双人协作的进度事实来源 - 新增 ADR-006:测试与交付容器化策略(Testcontainers / 交付 Docker 包 / 本机库仅个人联调) - Git 工作流规范补充:敏感信息只进忽略文件或 sample、测试数据不入库、测试代码限标准测试目录 - 门禁:mkdocs build --strict 通过(零警告)
This commit is contained in:
@@ -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.0,Java 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`:extensions(pgcrypto/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 节允许)。
|
||||
|
||||
### 任务 2:patbond-user 接 PostgreSQL Repository,UUID 用户持久化
|
||||
|
||||
- **涉及现有文件**:重写 `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、HikariCP(starter 自带)。**这些依赖只加在 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 并 trim;nickname 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 token:JWT**,建议 `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-user:Flyway 迁移可在空库执行 + Repository 落库/唯一冲突/失败锁定测试(真实 PG 容器,第 9 节要求)。
|
||||
- patbond-auth:JWT 签发/过期/篡改验证单测;Controller 层 mock UserClient。
|
||||
- 纵切集成:register → login → 携带 token 访问受保护端点 → refresh(旧 refresh 复用被拒)→ logout(session 撤销后 refresh 不可用)——对应 M1 全部验收标准。
|
||||
- **覆盖门禁**:每接口至少成功/参数错/401/409/重放五类(第 9 节)。
|
||||
|
||||
### 任务 6:OpenAPI + Flutter API Client
|
||||
|
||||
- **建议契约先行(design-first)**:手写 `patbond-doc/docs/api/openapi.yaml`(OpenAPI 3.0),只含第一批 auth/me 端点,团队评审后冻结,再实现两端。代码生成文档的备选是 springdoc——注意 Boot 2.7 只能用 springdoc 1.7.x(2.x 需要 Boot 3),又一个升级加分项。
|
||||
- **Flutter 客户端**:端点只有 5-6 个,**建议手写 dio client + 手写 DTO(`json_serializable` 可选)**,不引 openapi-generator(dart-dio 生成器的产物风格重、定制成本高,等接口上量再评估)。契约一致性靠任务 5 的契约测试兜底。
|
||||
- **新建**:`lib/core/network/api_client.dart`(dio 实例、baseUrl 环境区分、`ApiResponse<T>` 信封解包、错误码映射)。
|
||||
|
||||
### 任务 7:Flutter 登录页、安全 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`:注入 Bearer;401 时单飞(single-flight)refresh——并发请求排队等同一次刷新,刷新失败清 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.yml(env 注入)、test profile 可离线启动 [解 B3]
|
||||
2. 任务 1 + 2 + 3:Flyway baseline → UUID 持久化 → 统一异常与校验(同 PR 链,先冻结对外契约) [解 B1/B4/B5]
|
||||
3. 任务 4:JWT + refresh 轮换 + /internal 保护 + 登录失败锁定 [解 B2]
|
||||
4. 任务 5:Testcontainers 集成测试(随 2/3/4 增量补,不后置)
|
||||
5. 任务 6:openapi.yaml 评审冻结
|
||||
6. 任务 7:Flutter 登录纵切(依赖 5 的冻结契约)
|
||||
7. 任务 8:后端 E2E 进 CI 门禁;Flutter integration_test 本地跑通
|
||||
```
|
||||
|
||||
测试(任务 5)实际应与 2-4 步同 PR 交付,此处单列仅表示依赖关系。
|
||||
Reference in New Issue
Block a user