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,
|
||||
|
||||
Reference in New Issue
Block a user