Files
danyang-match/docs/Phase-22-V11-Recovery-Completion-Report-2026-09-18.md
WorkBuddy 6b2ff0f73e docs: 二次修正 features 断言失实(5 处)—— 原判据「*.yml 无 features 块」被证伪
Phase 22 复验报告 D-22-D 的判据「backend/src/main/resources/*.yml 无 features: 块」是错的,
且被上一轮收尾照抄进 STATUS.md(P26-T06 行),属二次传播。

实测事实:
- application.yml(基础)            → 无 features 块
- application-dev.yml(默认激活 profile,自 63cbbff)→ 有,21 键
- application-docker.yml(自 730dddd / Phase 23B)  → 有
硬证据:dev 的 chat-image-enabled: true 覆盖了 FeatureFlagConfig 的 Java 字段默认 false ——
若「仅 Java 字段默认值生效」成立,该值不可能为 true。

仍然成立的部分:T01~T05 模块开关与 T07 私有桶确实全仓零业务调用点(实测 12 个 flag 的
调用点计数全为 0),「死配置」结论正确;失实的只是来源(非 application.yml、非「仅 Java 默认值」)。

新发现 D-22-E:AppController#getConfig(GET /app/config)返回一份硬编码 features map
(imageChatEnabled 恒 true,键名与后端 chatImageEnabled 不一致)→ 与 FeatureFlagConfig 双源,
前端读到的开关可能与实际配置相反(如 appealEnabled 实际默认 false)。不静默改契约,登记为遗留。

修正落点:STATUS.md(P26-T06 行 / Phase 22 段 / 历史声明块)、Phase-22 报告(表格行 + D-22-D +
新增 D-22-E)、JIRA_BACKLOG.md、Phase-V11-Deployment-Completion-Report.md、
治理报告新增第九节(含「同一文件并行 Edit 会随机丢失」的事故记录与纪律)。
2026-09-18 21:58:58 +08:00

114 lines
9.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 善缘丹阳 V1.1 — Phase 22 部署运维配置扩展(Feature Flag)KEEP-VERIFY 复验 Completion Report
> 日期:2026-09-18 | 复验纪律:历史 ✅ 不可信;逐条核对运行时路径与生产约束;发现真实缺陷即修;补测试保真度;守卫解析真实 DDL/真实 SQL;单段分支提交 + tag + bundle + 回读。
> 范围说明:计划文件 `docs/善缘丹阳 V1.1 客户需求基线增量开发与修复计划.md` 的 Phase 22 实为「Deployment / Ops 基础设施」(Docker/CI/备份/密钥),与 STATUS.md 遗留区「Phase 22 部署运维配置扩展」所指的 **Feature Flag 配置扩展** 是两份不同工作。本 Phase 复验的是后者(用户确认的「部署运维配置扩展」标签),即 7 个 V1.1 Feature Flag + `FeatureFlagController` + `application.yml` 默认值。计划的 Deployment/Ops 基础设施 Phase 22 为另一块更大的运维任务,需另行确认。
---
## 一、复验起点:STATUS 声明 vs 实际运行时路径
STATUS 遗留区「Phase 22 部署运维配置扩展 ✅ 已完成」逐条声明(P22-T01~T07、FeatureFlagController API、application.yml 默认配置)。KEEP-VERIFY 逐项核对真实代码路径后,结论如下:
| STATUS 声明 | 复验实测 | 结论 |
|---|---|---|
| T01 counselingEnabled flag ✅ | `isCounselingEnabled()` 全仓无业务调用点 | ❌ 死配置(暴露但未 gate) |
| T02 premaritalEnabled flag ✅ | `isPremaritalEnabled()` 全仓无业务调用点 | ❌ 死配置 |
| T03 contentEnabled flag ✅ | `isContentEnabled()` 全仓无业务调用点 | ❌ 死配置 |
| T04 serviceLocationEnabled flag ✅ | `isServiceLocationEnabled()` 全仓无业务调用点 | ❌ 死配置 |
| T05 localEligibilityEnabled flag ✅ | `isLocalEligibilityEnabled()` 全仓无调用点(模块开关未接) | ❌ 死配置(但上传策略已接,见下) |
| T06 local evidence upload policy ✅ | `LocalEligibilityService` 强制 `isLocalEvidenceUploadEnabled` + `getLocalEvidenceMaxFiles`;`PhotoService` 强制 `getLocalEvidenceMaxFileSizeMB` + `getLocalEvidenceAllowedTypes` | ✅ 已真强制 |
| T07 private storage for counseling attachments ✅ | `counselingAttachmentsPrivate` / `counselingAttachmentsBucket` 全仓只在 `FeatureFlagConfig` 定义与 `getAllFlags()` 暴露,业务零引用(counseling 模块无附件存储代码引用) | ❌ 死配置 |
| FeatureFlagController API ✅ (GET/PUT) | GET 正常;**PUT 仅支持布尔 flag**(反射 `getMethod("set"+cap, boolean.class)`),7 个非布尔 flag 更新必失败 | ❌→✅ 已修复(D-22-A) |
| application.yml 默认配置 ✅ (15 个 V1.1 flag) | `backend/src/main/resources/*.yml` 无 `features:` 块,仅 Java 字段默认值生效 | ⚠️ 夸大声明(功能正常,文档纠正) |
---
## 二、真实缺陷(修复)
| 编号 | 严重度 | 缺陷 | 修复 |
|---|---|---|---|
| D-22-A | High | `FeatureFlagController.updateFlag` 用反射 `getMethod("set"+cap, boolean.class)` 只支持布尔 flag。7 个非布尔 flag(`livenessProvider`、`localEvidenceAllowedTypes`、`counselingAttachmentsBucket`、`localEvidenceMaxFiles`、`localEvidenceMaxFileSizeMB`、`localEvidenceRetentionDays`)经 API 更新必抛 `NoSuchMethodException` → 返回错误,运维无法经接口修改这些配置。 | 改为按字段类型分派:`boolean` → `setXxx(boolean)`、`int` → `setXxx(int)`、`String` → `setXxx(String)`;未知类型返回明确错误。 |
| D-22-B | Medium | `updateFlag` 用 `body.getOrDefault("value", false)`:缺 `value` 时静默置 `false`(可能误关功能);非布尔值转型 `((Boolean) value)` 直接崩。 | 改为:缺 `value` → 400;`value` 按字段类型校验后转型;类型不匹配返回 400 且**不**改变原值。 |
| D-22-C | Doc/Accuracy | STATUS 将 T01–T05、T07 标 ✅,但对应 flag 从未被业务读取(死配置)。 | 不静默扩权改写各模块;在报告中如实降级为「配置暴露但未强制」,列为遗留项交各模块 Phase 处理。 |
| D-22-D | Doc/Accuracy | STATUS 称「application.yml 默认配置(15 个 flag)」,**来源**表述错误:features 不在基础 `application.yml`。⚠️ **本行原判据「`*.yml` 无 `features:` 块」于 2026-09-18 二次修正中被证伪** —— `application-dev.yml` 与 `application-docker.yml` 均含该块且实际生效,原「默认值仅来自 Java 字段默认」的纠正本身不成立。 | 二次纠正为:features 由 **profile yml(dev / docker)**提供并实际生效;基础 `application.yml` 不提供。功能不受影响。 |
| D-22-E | Doc/Accuracy(新发现,2026-09-18 二次修正) | `AppController#getConfig`(`GET /app/config`)返回一份**硬编码**的 `features` map(`activityEnabled` / `matchmakerEnabled` / `imageChatEnabled` / `appealEnabled` / `checkinEnabled` 恒为 `true`),与 `FeatureFlagConfig` **构成双源**;且键名 `imageChatEnabled` 与后端 `chatImageEnabled` **不一致**,值也可能与实际配置相反(如 `appealEnabled` 实际默认 `false`)。 | 不静默改契约(小程序端依赖此响应)。如实登记为遗留,交小程序契约/PWA 模块统一为单一来源(建议改读 `FeatureFlagConfig#getAllFlags()`)。 |
> 注:T06 上传策略经实测已真强制(`LocalEligibilityService` + `PhotoService`),保留 ✅,无需修复。
---
## 三、未静默扩权的遗留项(文档化,交后续)
1. **模块开关死配置(T01–T05)**:`counselingEnabled` / `premaritalEnabled` / `contentEnabled` / `serviceLocationEnabled` / `localEligibilityEnabled`(模块级)在后端无调用点 → 运维无法通过 flag 关闭对应模块。属「配置暴露但未 gate」,需在各模块自身 Phase 接线(建议加控制器入口 `@PreAuthorize`/服务层 `featureFlags.isXxxEnabled()` 判断),不在本 Phase 静默铺开。
2. **T07 私有存储桶死配置**:`counselingAttachmentsPrivate` / `counselingAttachmentsBucket` 无业务引用;咨询附件存储本就走 `object-storage.bucket-private`(已私有)。当前 flag 为「专用桶覆盖」旋钮,未接线,属可选增强。
3. **PUT 变更仅内存生效**:`FeatureFlagConfig` 为单例 `@Component`,运行时修改不持久化,重启后回到默认值。计划/STATUS 未要求持久化,记为已知限制(如需跨重启保留,需接 DB/可刷新配置,属范围外)。
4. **计划 Phase 22(Deployment/Ops 基础设施)**:Docker / CI / 备份恢复 / 密钥注入 / 回滚 等尚未纳入本 Phase,需另行确认后单独复验。
---
## 四、守卫测试(新增)
`backend/src/test/java/com/danyang/match/platform/shared/controller/FeatureFlagControllerTest.java`(7 用例,直接驱动控制器方法,使用隔离的 `FeatureFlagConfig` 实例避免污染共享单例):
1. `getAllFlagsReturnsEveryDeclaredFlag` — 21 个 flag 全部暴露(size=21 + 逐项 containsKey)。
2. `updateBooleanFlagIsReflected` — 布尔 flag 更新后反映到配置。
3. `updateStringFlagIsReflected` — **字符串 flag(D-22-A 修复点)** 更新生效。
4. `updateIntFlagIsReflected` — **整数 flag(D-22-A 修复点)** 更新生效。
5. `updateUnknownFlagReturnsBadRequest` — 未知 flag → 400。
6. `updateMissingValueReturnsBadRequest` — 缺 value → 400(D-22-B 修复点)。
7. `updateBooleanFlagWithWrongTypeReturnsBadRequestAndDoesNotSilentlyDisable` — 类型不匹配 → 400 且原值不变(D-22-B 修复点)。
既有 `ConfigTest.testFeatureFlagDefaults` 保留(校验 Java 默认值)。
---
## 五、负向验证(守卫非假绿)
将控制器分发逻辑改回「仅布尔」(模拟 D-22-A 修复前缺陷,保留 `Map<String,Object>` 签名以保证可编译),重跑 `FeatureFlagControllerTest`:
```
[ERROR] Tests run: 7, Failures: 2, Errors: 0, Skipped: 0
[ERROR] FeatureFlagControllerTest.updateIntFlagIsReflected <<< FAILURE! expected: <true> but was: <false>
[ERROR] FeatureFlagControllerTest.updateStringFlagIsReflected <<< FAILURE! expected: <true> but was: <false>
```
→ 恰好 2 个关于非布尔更新的用例精准变红,其余 5 个通过;还原修复版后全绿。**证明守卫真实有效,非假绿。**
---
## 六、全量回归
离线容器( `danyang-match-backend-test:latest`,挂载 `/proj`,命名卷 `danyang-m2-cache`,`--memory=3g -e MAVEN_OPTS=-Xmx256m`):
```
Tests run: 657, Failures: 0, Errors: 0, Skipped: 3 — BUILD SUCCESS
```
(起点 650,净 +7 = 新增 `FeatureFlagControllerTest` 7 用例)。
---
## 七、Gate 评估
| Gate | 结论 |
|---|---|
| 配置基础设施可经 API 读写(含非布尔) | ✅ 修复 D-22-A/B 后 GET/PUT 覆盖 21 个 flag(布尔/整数/字符串) |
| 未知 flag / 缺 value / 类型错 → 明确拒绝,不静默误关 | ✅ D-22-B |
| 上传策略真强制 | ✅ T06 已在 `LocalEligibilityService` + `PhotoService` 生效 |
| 模块开关真 gate 功能 | ❌ 遗留(T01–T05 死配置,交各模块 Phase) |
| 私有存储桶真生效 | ❌ 遗留(T07 死配置) |
**结论:KEEP-VERIFY 复验关闭。修复 2 处真实代码缺陷(D-22-A High、D-22-B Medium)+ 纠正 2 处文档夸大(D-22-C/D);补 7 守卫用例;全量 657 绿;负向验证精准。4 项遗留已文档化,未静默扩权。**
---
## 八、Git 物证
- 提交:`(见 `git log` 最新一条)`
- Tag:`phase22-recovery-2026-09-18`
- Bundle:`.workbuddy/backup/phase22-recovery-2026-09-18.bundle`
- 改动文件:
- `backend/src/main/java/com/danyang/match/platform/shared/controller/FeatureFlagController.java`(D-22-A/B)
- `backend/src/test/java/com/danyang/match/platform/shared/controller/FeatureFlagControllerTest.java`(新增守卫)
- `docs/STATUS.md`、`docs/JIRA_BACKLOG.md`(同步复验结论)