fix(feature-flags): close Phase 22 KEEP-VERIFY — 2 real defects + doc overclaims
KEEP-VERIFY re-audit of the Feature Flag configuration extension (STATUS's "Phase 22 部署运维配置扩展"): marked ✅ but contained real code defects and overstated completion. Real defects fixed: - D-22-A (High): FeatureFlagController.updateFlag used reflection `getMethod("set"+cap, boolean.class)` and only supported boolean flags. 7 non-boolean flags (livenessProvider, localEvidenceAllowedTypes, counselingAttachmentsBucket, localEvidenceMaxFiles, localEvidenceMaxFileSizeMB, localEvidenceRetentionDays) failed to update via the API. Now dispatches by field type (boolean/int/String). - D-22-B (Medium): `body.getOrDefault("value", false)` silently set false when value was missing and crashed on non-boolean casts. Now returns 400 on missing value / type mismatch and leaves the original value unchanged. Doc overclaims corrected (no silent scope creep): - T01-T05 module-toggle flags (isCounselingEnabled etc.) have zero business call sites -> dead config (exposed but not gating). Left for each module's own phase to wire; documented as residual. - T07 private-storage bucket flags have zero references; storage already uses object-storage.bucket-private. Documented as residual. - STATUS claimed "application.yml defaults (15 flags)"; yml has no `features:` block, defaults are Java field defaults. Functionally fine, docs corrected. T06 upload policy verified actually enforced in LocalEligibilityService + PhotoService (kept ✅). Guard tests: new FeatureFlagControllerTest (7 cases: 21 keys exposed, boolean/ string/int update reflected, unknown flag / missing value / wrong type -> 400). Negative validation: reverting to boolean-only dispatch made exactly 2 update cases fail red, then green after restore — proves guards are real. Full regression: 657 tests / 0 failures / 0 errors / 3 skipped (baseline 650, +7).
This commit is contained in:
+45
-8
@@ -34,20 +34,57 @@ public class FeatureFlagController {
|
||||
/**
|
||||
* 更新单个 Feature Flag
|
||||
* PUT /api/v1/operator/feature-flags/{flagName}
|
||||
* Body: { "value": true }
|
||||
* Body: { "value": <boolean|int|string> }
|
||||
*
|
||||
* <p>支持布尔 / 整数 / 字符串三类 flag(此前仅支持布尔,导致 7 个非布尔 flag
|
||||
* 经 API 更新必失败)。未知 flag 或缺失 value 返回 400,且不会静默把功能置为关闭。
|
||||
*/
|
||||
@PutMapping("/{flagName}")
|
||||
public ApiResponse<Map<String, Object>> updateFlag(
|
||||
@AuthenticationPrincipal OperatorPrincipal principal,
|
||||
@PathVariable String flagName,
|
||||
@RequestBody Map<String, Boolean> body) {
|
||||
boolean value = body.getOrDefault("value", false);
|
||||
// 使用反射更新对应字段
|
||||
@RequestBody Map<String, Object> body) {
|
||||
if (!featureFlagConfig.getAllFlags().containsKey(flagName)) {
|
||||
return ApiResponse.error(
|
||||
com.danyang.match.platform.shared.enums.ApiResponseCode.BAD_REQUEST,
|
||||
"未知的 Feature Flag: " + flagName);
|
||||
}
|
||||
if (!body.containsKey("value")) {
|
||||
return ApiResponse.error(
|
||||
com.danyang.match.platform.shared.enums.ApiResponseCode.BAD_REQUEST,
|
||||
"请求体必须包含 value 字段");
|
||||
}
|
||||
Object value = body.get("value");
|
||||
try {
|
||||
java.lang.reflect.Method setter = featureFlagConfig.getClass()
|
||||
.getMethod("set" + capitalize(flagName), boolean.class);
|
||||
setter.invoke(featureFlagConfig, value);
|
||||
return ApiResponse.ok(Map.of("flagName", flagName, "value", value, "updated", true));
|
||||
java.lang.reflect.Field field = featureFlagConfig.getClass().getDeclaredField(flagName);
|
||||
Class<?> type = field.getType();
|
||||
if (type == boolean.class || type == Boolean.class) {
|
||||
boolean b = ((Boolean) value);
|
||||
featureFlagConfig.getClass()
|
||||
.getMethod("set" + capitalize(flagName), boolean.class)
|
||||
.invoke(featureFlagConfig, b);
|
||||
} else if (type == int.class || type == Integer.class) {
|
||||
int i = ((Number) value).intValue();
|
||||
featureFlagConfig.getClass()
|
||||
.getMethod("set" + capitalize(flagName), int.class)
|
||||
.invoke(featureFlagConfig, i);
|
||||
} else if (type == String.class) {
|
||||
featureFlagConfig.getClass()
|
||||
.getMethod("set" + capitalize(flagName), String.class)
|
||||
.invoke(featureFlagConfig, String.valueOf(value));
|
||||
} else {
|
||||
return ApiResponse.error(
|
||||
com.danyang.match.platform.shared.enums.ApiResponseCode.BAD_REQUEST,
|
||||
"不支持的 Flag 类型: " + type.getSimpleName());
|
||||
}
|
||||
return ApiResponse.ok(Map.of(
|
||||
"flagName", flagName,
|
||||
"value", featureFlagConfig.getAllFlags().get(flagName),
|
||||
"updated", true));
|
||||
} catch (ClassCastException e) {
|
||||
return ApiResponse.error(
|
||||
com.danyang.match.platform.shared.enums.ApiResponseCode.BAD_REQUEST,
|
||||
"value 类型与 Flag " + flagName + " 不匹配: " + e.getMessage());
|
||||
} catch (Exception e) {
|
||||
return ApiResponse.error(
|
||||
com.danyang.match.platform.shared.enums.ApiResponseCode.BAD_REQUEST,
|
||||
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
package com.danyang.match.platform.shared.controller;
|
||||
|
||||
import com.danyang.match.platform.shared.config.FeatureFlagConfig;
|
||||
import com.danyang.match.platform.shared.dto.ApiResponse;
|
||||
import com.danyang.match.platform.shared.security.principal.OperatorPrincipal;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Phase 22(部署运维配置扩展 — Feature Flag)真实守卫测试。
|
||||
*
|
||||
* <p>背景(KEEP-VERIFY 复验发现):
|
||||
* <ul>
|
||||
* <li>D-22-A(High):原 {@code updateFlag} 仅支持布尔 flag(反射 {@code getMethod("set"+cap, boolean.class)}),
|
||||
* 7 个非布尔 flag(livenessProvider / localEvidenceAllowedTypes / counselingAttachmentsBucket /
|
||||
* localEvidenceMaxFiles / localEvidenceMaxFileSizeMB / localEvidenceRetentionDays)经 API 更新必失败。</li>
|
||||
* <li>D-22-B(Medium):原 {@code body.getOrDefault("value", false)} 缺 value 时静默置 false(可能误关功能),
|
||||
* 且非布尔类型转型会崩。</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>本测试直接驱动控制器方法(与 BannerOperatorPhase19IntegrationTest 同一装配模式),
|
||||
* 使用隔离的 {@link FeatureFlagConfig} 实例,避免污染共享单例影响其他读取默认 flag 的测试。
|
||||
*/
|
||||
@SpringBootTest
|
||||
@ActiveProfiles("test")
|
||||
@Transactional
|
||||
class FeatureFlagControllerTest {
|
||||
|
||||
@MockBean
|
||||
private StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
private FeatureFlagConfig fixture;
|
||||
private FeatureFlagController controller;
|
||||
private OperatorPrincipal principal;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
fixture = new FeatureFlagConfig();
|
||||
controller = new FeatureFlagController(fixture);
|
||||
principal = new OperatorPrincipal("00000000-0000-0000-0000-000000000099");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAllFlagsReturnsEveryDeclaredFlag() {
|
||||
ApiResponse<Map<String, Object>> resp = controller.getAllFlags();
|
||||
assertTrue(resp.isSuccess());
|
||||
Map<String, Object> flags = resp.getData();
|
||||
// 21 个 flag 必须全部暴露,否则运维看不到/改不了某个配置(含此前仅布尔可更新的死区)
|
||||
assertEquals(21, flags.size());
|
||||
for (String key : new String[]{
|
||||
"activityEnabled", "matchmakerEnabled", "chatImageEnabled", "appealEnabled",
|
||||
"counselingEnabled", "premaritalEnabled", "contentEnabled", "serviceLocationEnabled",
|
||||
"localEligibilityEnabled", "referralEnabled", "mutualConsentEnabled", "serviceProgramEnabled",
|
||||
"livenessEnabled", "livenessProvider", "localEvidenceUploadEnabled",
|
||||
"localEvidenceMaxFiles", "localEvidenceMaxFileSizeMB", "localEvidenceAllowedTypes",
|
||||
"localEvidenceRetentionDays", "counselingAttachmentsPrivate", "counselingAttachmentsBucket"
|
||||
}) {
|
||||
assertTrue(flags.containsKey(key), "缺少暴露的 flag: " + key);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateBooleanFlagIsReflected() {
|
||||
ApiResponse<Map<String, Object>> resp = controller.updateFlag(principal, "counselingEnabled", Map.of("value", false));
|
||||
assertTrue(resp.isSuccess());
|
||||
assertFalse(fixture.isCounselingEnabled());
|
||||
assertEquals(false, fixture.getAllFlags().get("counselingEnabled"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateStringFlagIsReflected() {
|
||||
ApiResponse<Map<String, Object>> resp = controller.updateFlag(principal, "livenessProvider", Map.of("value", "third-party"));
|
||||
assertTrue(resp.isSuccess());
|
||||
assertEquals("third-party", fixture.getLivenessProvider());
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateIntFlagIsReflected() {
|
||||
ApiResponse<Map<String, Object>> resp = controller.updateFlag(principal, "localEvidenceMaxFiles", Map.of("value", 3));
|
||||
assertTrue(resp.isSuccess());
|
||||
assertEquals(3, fixture.getLocalEvidenceMaxFiles());
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateUnknownFlagReturnsBadRequest() {
|
||||
ApiResponse<Map<String, Object>> resp = controller.updateFlag(principal, "noSuchFlag", Map.of("value", true));
|
||||
assertFalse(resp.isSuccess());
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateMissingValueReturnsBadRequest() {
|
||||
ApiResponse<Map<String, Object>> resp = controller.updateFlag(principal, "counselingEnabled", Map.of());
|
||||
assertFalse(resp.isSuccess());
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateBooleanFlagWithWrongTypeReturnsBadRequestAndDoesNotSilentlyDisable() {
|
||||
ApiResponse<Map<String, Object>> resp = controller.updateFlag(principal, "counselingEnabled", Map.of("value", "not-a-bool"));
|
||||
assertFalse(resp.isSuccess());
|
||||
assertTrue(fixture.isCounselingEnabled(), "类型不匹配不应静默把功能置为关闭");
|
||||
}
|
||||
}
|
||||
@@ -178,7 +178,8 @@
|
||||
| Phase 18 申诉复验 | 已完成(2026-09-17) | 见 `Phase-18-V11-Recovery-Completion-Report-2026-09-17.md`;修复 **8 处真实缺陷**(**D1(Critical,必崩级盲区)测试库缺 `appeals` 表 + `AppealServiceTest` 9 例中 6 例 `assertTrue(true)` 假绿 → 申诉真实 DB 路径零覆盖**;**D2(High,只在 PG 上炸)`escalateAppeal` 向 `operatorId` 发用户通知撞 `notifications.user_id → users(id)` FK → 升级事务整体回滚**;**D3(High,只在 PG 上炸)`appealType` 未校验生产 `chk_appeal_type` 白名单**;D4 escalated 死胡同(前后端均无法再决策);D5 `processAppeal` 先读后写并发竞态;D6 三处查询漏 `deleted_at` 过滤;D7 硬编码 `resolved`;D8 证据字段契约 `evidenceFileIds`);补 `appeals` 表并移出白名单入 `REQUIRED_IN_TEST_SCHEMA`;新增原子认领 `markReviewingIfProcessable`;新增 `AppealPhase18IntegrationTest`(15 用例),删除假绿 `AppealServiceTest`(9 用例);负向验证(误伤其他处置 → Gate 用例精准变红并还原);后端 **494 tests 全绿**;admin-web `vue-tsc` EXITCODE=0;**Phase 19 尚未获准** |
|
||||
| Phase 19 统一运营后台 + 用户端公益服务页面复验 | 已完成(2026-09-17) | 见 `Phase-19-V11-Recovery-Completion-Report-2026-09-17.md`;修复 **25 处真实缺陷**(**D20 服务站公告能力整条不可达:Service + V042 表都在、全仓零端点**;**D21 服务站 `create/update` 越权写:类级 `service_location.view` 兜底,`service_location.manage` 是从未使用的死权限**;D22 该控制器未纳入权限覆盖守卫;**D2/D3 Banner 校验 500 + 软删 Banner 仍上首页**;**D5/D6 Content 发布死路 + 分类停用不可恢复**;D10~D14 ServiceProgram 审批死路/草稿泄露/成员数恒 0/退出未实现/撞唯一键;D9/D23 测试库缺 `content_categories` 与 `service_location_announcements` → 零覆盖;D18 预约 IDOR;D24 迁移基线未跟进 V042);补 V042 公告表 + 运营端五端点 + 用户端只读端点 + 后台公告管理 UI + 小程序 U50 公告区块;新增 3 组守卫(24+5+5 用例)并把服务站运营端收入权限覆盖扫描;负向验证 3 注入 → 4 用例精准变红;后端 **602 tests 全绿**;admin-web `vue-tsc`/`vite build` 双绿;小程序 `tsc` EXIT=0;ui-tests 14/14;**Phase 20 尚未获准** |
|
||||
| Phase 20 Analytics 数据统计与管理驾驶舱复验 | 已完成(2026-09-17) | 见 `Phase-20-V11-Recovery-Completion-Report-2026-09-17.md`;修复 **20 处真实缺陷**(**D1 实名人数用 `profile_status != 'rejected'` 冒充 → 把"资料还在填"算成已实名**;**D3 Chat Started 直接数会话行数 → 系统通知计入**;**D4 Match 不分来源 → 10 对互选被当 10 对成功引用**;**D5 活动漏斗全部硬编码 0 + 一行把"用户数"当"活动数"的死代码**;**D6 Banner 曝光/点击硬编码 0**;**D12 系统性软删缺陷:`BaseEntity.deletedAt` 无 `@TableLogic` + 全局无逻辑删除配置 → `selectCount` 把软删数据全部计入且无任何报错**,修复 45 处统计调用(含 2 轮复验补扫出的 5 处次级路径);**D15 三套独立实现** → 抽 `AnalyticsMetricsCalculator` 统一口径;**D13/权限 授予脱节:`data_viewer` 空壳角色 + `platform_admin` 被 `id >= '20000000-…'` 条件漏掉 V1.0 老权限段 → 整个模块除 super_admin 外全员 403**;**D14 `AnalyticsServiceTest` = 5×`assertTrue(true)` 假绿 → 删除**;D20 `getDailySummary` 无边界校验;前端 `AnalyticsView.vue` 未接 `/v11-dashboard`);建立指标字典 `MetricCatalog`(36 指标 × 12 字段)+ `docs/ANALYTICS_METRICS.md`;新增 V043 授权(**刻意排除 `organization_admin`** —— 相关表无组织维度,无范围全量数据即越权);新增守卫 `MetricCatalogGuardTest`(11) / `AnalyticsMetricsPhase20IntegrationTest`(21) / `AnalyticsSoftDeleteGuardTest`(4,结构性) / `AnalyticsPermissionGrantTest`(6);**跨阶段修复 X1~X3**(**注销写入 `user_identities` 状态域的 `'failed'` → 违反 V017 `chk_elig_verify_status` → 真实 PG 上账号注销 100% 不可用**;`checkCanWithdraw` 资格拦截死代码;测试库补两条 CHECK 后暴露 `ActivityPhase14Test` 夹具违规)并新增 `LocalEligibilityStatusVocabularyGuardTest`(9);负向验证两轮 7 注入 → 7 用例精准变红并还原(`md5sum` 逐字节一致);后端 **648 tests 全绿**;admin-web `vue-tsc` EXIT=0;**Phase 21 已复验关闭(2026-09-18,650 tests 全绿)** |
|
||||
| Phase 21 账户生命周期增量复验 | 已完成(2026-09-18) | 见 `docs/Phase-21-V11-Recovery-Completion-Report-2026-09-18.md`;KEEP-VERIFY 复验发现 STATUS 标✅ 但含 **6 处真实缺陷 + 假绿测试**:① `AuthService.checkAccountStatus` 不拒 `CLOSED` → 注销后仍可登录(Gate ❌→✅);② `notifyActiveMatches` 为 TODO 桩 → 注销未关闭活跃 Match/会话(Gate ❌→✅,已实现关闭 Match + `chatService.closeConversationForMatch`);③ `cancelPendingActivityRegistrations` 为 TODO 桩 → 未来活动报名未关闭(Gate ❌→✅);④ `anonymizeServiceHistory` 已实现却**从未被 `closeAccount` 调用(死代码)** → 个人/咨询正文暴露(Gate ❌→✅,已接线 + 脱敏身份证占位符 + `counseling_followups` 正文);⑤ `closeAccount` 在 blockers 处硬抛异常使 `cancelXxx` 永远不可达(死代码矛盾 → 信息性日志 + 继续清理);⑥ 删除 7×`assertTrue(true)` 假绿 → 替换为 11 用例真实 DB 集成测试 + 3 用例登录守卫;跨模块防线(推荐引擎/匹配资格/Capability 已拒非 NORMAL)确认有效;后端 **650 tests 全绿**(起点 648,净 +2);**Phase 22 尚未获准** |
|
||||
| Phase 21 账户生命周期增量复验 | 已完成(2026-09-18) | 见 `docs/Phase-21-V11-Recovery-Completion-Report-2026-09-18.md`;KEEP-VERIFY 复验发现 STATUS 标✅ 但含 **6 处真实缺陷 + 假绿测试**:① `AuthService.checkAccountStatus` 不拒 `CLOSED` → 注销后仍可登录(Gate ❌→✅);② `notifyActiveMatches` 为 TODO 桩 → 注销未关闭活跃 Match/会话(Gate ❌→✅,已实现关闭 Match + `chatService.closeConversationForMatch`);③ `cancelPendingActivityRegistrations` 为 TODO 桩 → 未来活动报名未关闭(Gate ❌→✅);④ `anonymizeServiceHistory` 已实现却**从未被 `closeAccount` 调用(死代码)** → 个人/咨询正文暴露(Gate ❌→✅,已接线 + 脱敏身份证占位符 + `counseling_followups` 正文);⑤ `closeAccount` 在 blockers 处硬抛异常使 `cancelXxx` 永远不可达(死代码矛盾 → 信息性日志 + 继续清理);⑥ 删除 7×`assertTrue(true)` 假绿 → 替换为 11 用例真实 DB 集成测试 + 3 用例登录守卫;跨模块防线(推荐引擎/匹配资格/Capability 已拒非 NORMAL)确认有效;后端 **650 tests 全绿**(起点 648,净 +2);**Phase 22 已复验关闭(2026-09-18,657 tests 全绿)** |
|
||||
| Phase 22 部署运维配置扩展(Feature Flag)复验 | 已完成(2026-09-18) | 见 `docs/Phase-22-V11-Recovery-Completion-Report-2026-09-18.md`;KEEP-VERIFY 复验发现 STATUS 标✅ 但含 **2 处真实代码缺陷 + 2 处文档夸大**:① D-22-A(High)`FeatureFlagController.updateFlag` 仅支持布尔 flag(反射 `getMethod("set"+cap, boolean.class)`)→ 7 个非布尔 flag 经 API 更新必失败,已改为按字段类型分派 boolean/int/String;② D-22-B(Medium)`body.getOrDefault("value", false)` 缺 value 静默置 false + 非布尔转型崩,已改为缺 value/类型错→400 且不改原值;T01–T05 模块开关(`isCounselingEnabled` 等)全仓无业务调用点 → 死配置(暴露未 gate,遗留交各模块 Phase);T06 上传策略实测已在 `LocalEligibilityService`+`PhotoService` 真强制 ✅;T07 私有桶零引用 → 死配置(遗留);yml 无 `features:` 块、默认值实为 Java 字段默认(文档纠正);新增 `FeatureFlagControllerTest`(7 用例)+ 负向验证(仅布尔缺陷版恰 2 用例变红);后端 **657 tests 全绿**(起点 650,净 +7);**Phase 23 尚未获准** |
|
||||
| UI-CLEANUP-20260913 | 已完成 | 废弃 UI 代码、冲突规范、旧计划和无效验收证据已清理;`ui-document-baseline-contracts` 锁定唯一视觉入口 |
|
||||
| UI-ADMIN-20260913 | 已完成(待桌面端目视复验) | 管理端全局框架与工作台已按规格交付;18 modules + 22 routes,vue-tsc 与 vite build 全绿 |
|
||||
| UI-MINIAPP-20260913 | 已完成 | 小程序 11 个核心页面已重构;11/11 页 TS-only,2026-09-16 通过用户验收 |
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
# 善缘丹阳 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)」,但 yml 无 `features:` 块。 | 在报告/STATUS 中纠正:默认值来自 `FeatureFlagConfig` 的 Java 字段默认值,非 yml。功能不受影响。 |
|
||||
|
||||
> 注: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`(同步复验结论)
|
||||
+17
-2
@@ -2,9 +2,9 @@
|
||||
|
||||
> **2026-09-17 当前有效结论:`V1.1 INCREMENTAL RECOVERY PHASE 20 COMPLETE / UI REDESIGN ACCEPTED BY USER / GIT CLEAN`。**
|
||||
>
|
||||
> 阶段 0(在途收口)、阶段 1(小程序 C 组 + discover 筛选重设计)、阶段 2(管理端 Vue 新框架适配)、阶段 3(CapabilityService 独立化)、阶段 4(双端总验收)全部完成。三端构建通过、**13/13 契约全绿**、核心 11 页面**全部 TS-only(11/11)**。**UI 重设计首批 11 页已于 2026-09-16 通过用户验收(结论:基本达标)**;`/me/*` 前后端契约同轮收敛,17 个 404 调用已恢复。**Phase 08(Like / Match / Block)、Phase 09(Text Chat)、Phase 10(Image & Chat Safety)、Phase 11(Notification)、Phase 12(Relationship Progress)、Phase 13(公益红娘转介)、Phase 13A(婚恋咨询与婚前辅导)、Phase 14(多类型公益活动)、Phase 15(活动报名、审核与签到)、Phase 16(活动互选与双方开启沟通确认)、Phase 17(举报与风控)、Phase 18(申诉)、Phase 19(统一运营后台 + 用户端公益服务页面)、Phase 20(Analytics 数据统计与管理驾驶舱)复验已关闭**,十四轮共修复 130+ 处真实缺陷(含 3 处隐藏编译缺陷),并各自补齐测试保真度与守卫。**Phase 21 起需用户确认后启动。**
|
||||
> 阶段 0(在途收口)、阶段 1(小程序 C 组 + discover 筛选重设计)、阶段 2(管理端 Vue 新框架适配)、阶段 3(CapabilityService 独立化)、阶段 4(双端总验收)全部完成。三端构建通过、**13/13 契约全绿**、核心 11 页面**全部 TS-only(11/11)**。**UI 重设计首批 11 页已于 2026-09-16 通过用户验收(结论:基本达标)**;`/me/*` 前后端契约同轮收敛,17 个 404 调用已恢复。**Phase 08(Like / Match / Block)、Phase 09(Text Chat)、Phase 10(Image & Chat Safety)、Phase 11(Notification)、Phase 12(Relationship Progress)、Phase 13(公益红娘转介)、Phase 13A(婚恋咨询与婚前辅导)、Phase 14(多类型公益活动)、Phase 15(活动报名、审核与签到)、Phase 16(活动互选与双方开启沟通确认)、Phase 17(举报与风控)、Phase 18(申诉)、Phase 19(统一运营后台 + 用户端公益服务页面)、Phase 20(Analytics 数据统计与管理驾驶舱)复验已关闭**,十四轮共修复 130+ 处真实缺陷(含 3 处隐藏编译缺陷),并各自补齐测试保真度与守卫。**Phase 21、22 复验已关闭(2026-09-18);Phase 23 起需用户确认后启动。**
|
||||
|
||||
最后更新: 2026-09-17 — 分支 `main`(`feat-ui-v11` 保留为冗余锚点)
|
||||
最后更新: 2026-09-18 — 分支 `main`(`feat-ui-v11` 保留为冗余锚点)
|
||||
|
||||
## 当前唯一有效状态
|
||||
|
||||
@@ -569,6 +569,21 @@ MinIO: localhost:9003 (凭证由本地环境变量提供)
|
||||
| 报告 | `docs/Phase-21-V11-Recovery-Completion-Report-2026-09-18.md` |
|
||||
| 遗留(未静默扩权) | ① 红娘服务进行中拦截:数据模型无 `matchmaker_services` 表,仍硬编码 0(TODO);② 数据保留"配置+人工审核"删除流程属产品决策未实现;③ 隐私设置接口仍为 TODO 桩 |
|
||||
|
||||
### Phase 22 — 部署运维配置扩展(Feature Flag)✅(KEEP-VERIFY 复验关闭,2026-09-18)
|
||||
| 项 | 复验结论 |
|
||||
|------|------|
|
||||
| 范围 | STATUS 遗留区「Phase 22 部署运维配置扩展」= 7 个 V1.1 Feature Flag + `FeatureFlagController` + 默认值(计划文件 Phase 22 实为 Deployment/Ops 基础设施,另属一块任务,未纳入本 Phase) |
|
||||
| D-22-A(High) | `updateFlag` 仅支持布尔 flag(反射 `getMethod("set"+cap, boolean.class)`)→ 7 个非布尔 flag 经 API 更新必失败;已改为按字段类型分派 boolean/int/String |
|
||||
| D-22-B(Medium) | `body.getOrDefault("value", false)` 缺 value 静默置 false + 非布尔转型崩;已改为缺 value/类型错→400 且不改原值 |
|
||||
| T01–T05 模块开关 | 复验发现 `isCounselingEnabled` / `isPremaritalEnabled` / `isContentEnabled` / `isServiceLocationEnabled` / `isLocalEligibilityEnabled`(模块级)全仓无业务调用点 → **死配置**(暴露但未 gate);不静默扩权,留交各模块 Phase 接线 |
|
||||
| T06 上传策略 | 实测已在 `LocalEligibilityService`(启用开关 + 数量上限)+ `PhotoService`(大小 + 类型)**真强制** ✅ |
|
||||
| T07 私有存储桶 | `counselingAttachmentsPrivate` / `Bucket` 全仓零引用 → **死配置**(存储本就走 `object-storage.bucket-private`);可选增强,遗留 |
|
||||
| yml 声明夸大 | STATUS 称「application.yml 默认配置(15 flag)」,实际 yml 无 `features:` 块,默认值来自 Java 字段默认值;功能不受影响,文档纠正 |
|
||||
| 守卫测试 | 新增 `FeatureFlagControllerTest`(7 用例:21 键全暴露 / 布尔·字符串·整数更新生效 / 未知 flag·缺 value·类型错→400);负向验证(仅布尔缺陷版)恰 2 用例精准变红,还原全绿 |
|
||||
| 全量回归 | **657 tests / 0 failures / 0 errors / 3 skipped(起点 650,净 +7)** |
|
||||
| 报告 | `docs/Phase-22-V11-Recovery-Completion-Report-2026-09-18.md` |
|
||||
| 遗留(未静默扩权) | ① T01–T05 模块开关死配置;② T07 私有桶死配置;③ PUT 仅内存生效(不持久化,计划未要求);④ 计划 Phase 22 Deployment/Ops 基础设施尚未复验 |
|
||||
|
||||
**Phase 20(Analytics数据统计扩展)— 已完成** ✅
|
||||
|
||||
### Phase 22 — 部署运维配置扩展 ✅ 已完成
|
||||
|
||||
Reference in New Issue
Block a user