feat(flags): PUT /operator/feature-flags 真持久化(V048)

此前 updateFlag 只改 FeatureFlagConfig 内存单例的字段:重启即回默认值、
多实例互不一致,表现为「后台显示已关闭,一次发布后功能又回来了」。

- V048 新建 feature_flag_overrides(flag_key 主键;值存字符串、回读按字段
  声明类型解析;updated_by 记审计)。updated_by 必须建 uuid 而非 varchar,
  否则 UUIDStringTypeHandler 以 uuid 发参数会在 PG 抛
  operator does not exist: character varying = uuid(同 V046 的 actor_id)。
- FeatureFlagOverride/Mapper/Service:upsert 走「先 UPDATE、0 行再 INSERT」,
  不用 PG 的 ON CONFLICT(H2 测试库不认,同 IdempotencyMapper 取舍)。
- FeatureFlagConfig.reloadFromPersistence() 挂 @PostConstruct:该时机晚于
  @ConfigurationProperties 绑定,故 DB 覆盖压得住 yml 默认值。
- PUT 先改内存再落库(内存改动失败即 400,不写库);新增 DELETE 恢复默认。
- 覆盖值无法解析时启动 fail-fast,不静默回退默认(否则「配了不生效」)。

守卫 FeatureFlagPersistenceGuardTest 11 例,核心是「另起实例重新加载后改动仍在」
(重启等价路径),并自检 @PostConstruct 未被摘掉。
负向验证:注入不落库 + 摘钩子 -> 恰好 5 例变红,其余不受影响;还原 md5 一致。

全量回归 700 / 0 / 0 / 3(起点 689,净 +11);UI 契约门禁 18/18。
This commit is contained in:
WorkBuddy
2026-09-19 17:47:16 +08:00
parent 018626935a
commit d605c04707
12 changed files with 752 additions and 46 deletions
+13
View File
@@ -633,3 +633,16 @@ CREATE TABLE review_items (
CREATE UNIQUE INDEX uk_idempotency_scope
ON idempotency_records (actor_id, endpoint, idempotency_key);
-- V048: Feature Flag 运行期覆盖持久化
-- 生产 updated_by 是 uuid(UUIDStringTypeHandler 以 uuid 类型发送参数,
-- 建成 varchar 会在 PG 上报 operator does not exist,同 V046 的 actor_id);
-- H2 侧沿用本库既有约定的 VARCHAR(36)。
CREATE TABLE feature_flag_overrides (
flag_key VARCHAR(64) PRIMARY KEY,
flag_value VARCHAR(255) NOT NULL,
value_type VARCHAR(16) NOT NULL,
updated_by VARCHAR(36),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
CONSTRAINT chk_flag_value_type CHECK (value_type IN ('boolean', 'int', 'string'))
);