|
@@ -3280,6 +3280,11 @@ insertSysConfigSeed("gift_membership_pay_price", "198000", "送会员商品达
|
|
|
createServiceConfigTable();
|
|
createServiceConfigTable();
|
|
|
migrateScatteredCfConfigs();
|
|
migrateScatteredCfConfigs();
|
|
|
seedCfServiceConfig();
|
|
seedCfServiceConfig();
|
|
|
|
|
+
|
|
|
|
|
+ // 迁移413: service_config 收编 product_config(W0.2 双配置中心合并)
|
|
|
|
|
+ // 幂等:product_key 列/索引已存在则跳过;product_config 表不存在时数据并入为空跑
|
|
|
|
|
+ addServiceConfigProductKeyColumn();
|
|
|
|
|
+ migrateProductConfigToServiceConfig();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private void migrateHealthDimensionsTable() {
|
|
private void migrateHealthDimensionsTable() {
|
|
@@ -13680,6 +13685,135 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
|
|
|
return idx > 0 ? key.substring(0, idx) : "general";
|
|
return idx > 0 ? key.substring(0, idx) : "general";
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // ==================== 迁移413:W0.2 双配置中心合并(service_config 收编 product_config) ====================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 迁移413-1:service_config 增加 product_key 列(product 侧配置关联键;NULL = 全局/CF 通用参数)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 幂等:service_config 不存在 / 列已存在 / 索引已存在均跳过,可重复执行。
|
|
|
|
|
+ */
|
|
|
|
|
+ private void addServiceConfigProductKeyColumn() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (!tableExists("service_config")) {
|
|
|
|
|
+ log.warn("迁移413: service_config 表不存在(迁移412 未执行?),跳过 product_key 列/索引");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (columnExists("service_config", "product_key")) {
|
|
|
|
|
+ log.info("迁移413: service_config.product_key 已存在,跳过加列");
|
|
|
|
|
+ } else {
|
|
|
|
|
+ ensureColumn("service_config", "product_key",
|
|
|
|
|
+ "VARCHAR(64) DEFAULT NULL COMMENT '关联产品标识(product 侧配置;NULL=全局/CF 通用参数)'");
|
|
|
|
|
+ log.info("迁移413: 已为 service_config 添加 product_key 列");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!indexExists("service_config", "idx_product_key")) {
|
|
|
|
|
+ jdbcTemplate.execute("ALTER TABLE service_config ADD KEY idx_product_key (product_key)");
|
|
|
|
|
+ log.info("迁移413: 已创建 service_config.idx_product_key 索引");
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.warn("迁移413: 添加 product_key 列/索引失败: {}", e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 迁移413-2:把 product_config 表数据并入 service_config(product_key 填对应产品标识)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>product_key 标识规则(按 product_config 实际列优先级取第一个存在的列):</p>
|
|
|
|
|
+ * <ol>
|
|
|
|
|
+ * <li>{@code product_key}(字符串产品标识)</li>
|
|
|
|
|
+ * <li>{@code product_code}(产品编码)</li>
|
|
|
|
|
+ * <li>{@code product_id}(产品主键,CAST 为字符串;唯一索引冲突风险由 idempotentKey 拼接规避)</li>
|
|
|
|
|
+ * <li>group_key(计划文档 §3.6.6 中唯一保证存在的关联字段,作为兜底产品标识)</li>
|
|
|
|
|
+ * <li>以上皆缺 → {@code default}</li>
|
|
|
|
|
+ * </ol>
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>config_key 写入规则:product 侧键统一加 {@code product.{productKey}.} 前缀
|
|
|
|
|
+ * (见 {@link com.etotem.cfc.constant.ProductConfigKeys}),保证 config_key 全局唯一
|
|
|
|
|
+ * (service_config 唯一键 uk_config_key 保留不动,W0.2 不改 W0.1 种子与索引)。</p>
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>幂等:本仓库基线(origin/cfclub / 测试库)不存在 product_config 表,迁移为空跑;
|
|
|
|
|
+ * 若目标环境存在该表,按「service_config 中不存在同 key 才插入」幂等,INSERT IGNORE 兜底唯一键冲突。</p>
|
|
|
|
|
+ */
|
|
|
|
|
+ private void migrateProductConfigToServiceConfig() {
|
|
|
|
|
+ if (!tableExists("product_config")) {
|
|
|
|
|
+ log.info("迁移413: product_config 表不存在(本基线无该表),跳过数据并入(幂等空跑)");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ String groupCol = firstExistingColumn("product_config", "group_key", "groupKey");
|
|
|
|
|
+ String keyCol = firstExistingColumn("product_config", "config_key", "configKey");
|
|
|
|
|
+ String valueCol = firstExistingColumn("product_config", "config_value", "configValue");
|
|
|
|
|
+ String typeCol = firstExistingColumn("product_config", "value_type", "valueType");
|
|
|
|
|
+ String descCol = firstExistingColumn("product_config", "description");
|
|
|
|
|
+ String editableCol = firstExistingColumn("product_config", "editable");
|
|
|
|
|
+ String upByCol = firstExistingColumn("product_config", "updated_by", "updatedBy");
|
|
|
|
|
+ String createdAtCol = firstExistingColumn("product_config", "created_at", "createdAt");
|
|
|
|
|
+ String updatedAtCol = firstExistingColumn("product_config", "updated_at", "updatedAt");
|
|
|
|
|
+ if (keyCol == null) {
|
|
|
|
|
+ log.warn("迁移413: product_config 缺少配置键列(config_key/configKey),无法并入,跳过");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 产品标识表达式(优先级:product_key → product_code → product_id → group_key → 'default')
|
|
|
|
|
+ // 统一加 p. 前缀限定,避免与 service_config 同名列歧义
|
|
|
|
|
+ String productKeyExpr = null;
|
|
|
|
|
+ for (String c : new String[]{"product_key", "product_code"}) {
|
|
|
|
|
+ if (columnExists("product_config", c)) {
|
|
|
|
|
+ productKeyExpr = "NULLIF(TRIM(p.`" + c + "`), '')";
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (productKeyExpr == null && columnExists("product_config", "product_id")) {
|
|
|
|
|
+ productKeyExpr = "CAST(p.`product_id` AS CHAR)";
|
|
|
|
|
+ }
|
|
|
|
|
+ if (productKeyExpr == null && groupCol != null) {
|
|
|
|
|
+ productKeyExpr = "NULLIF(TRIM(p.`" + groupCol + "`), '')";
|
|
|
|
|
+ }
|
|
|
|
|
+ if (productKeyExpr == null) {
|
|
|
|
|
+ productKeyExpr = "'" + com.etotem.cfc.constant.ProductConfigKeys.DEFAULT_PRODUCT_KEY + "'";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ StringBuilder sql = new StringBuilder("INSERT IGNORE INTO service_config ")
|
|
|
|
|
+ .append("(group_key, config_key, config_value, value_type, description, editable, product_key, updated_by, created_at, updated_at) ")
|
|
|
|
|
+ .append("SELECT ");
|
|
|
|
|
+ sql.append("'").append(com.etotem.cfc.constant.ProductConfigKeys.PRODUCT_GROUP_KEY).append("', ");
|
|
|
|
|
+ // config_key = product.{productKey}.{原key}(保证全局唯一;截断到 128 防超长)
|
|
|
|
|
+ sql.append("LEFT(CONCAT('").append(com.etotem.cfc.constant.ProductConfigKeys.PRODUCT_KEY_PREFIX)
|
|
|
|
|
+ .append("', COALESCE(").append(productKeyExpr).append(", '")
|
|
|
|
|
+ .append(com.etotem.cfc.constant.ProductConfigKeys.DEFAULT_PRODUCT_KEY).append("'), '.', p.`")
|
|
|
|
|
+ .append(keyCol).append("`), 128), ");
|
|
|
|
|
+ sql.append(valueCol != null ? "p.`" + valueCol + "`, " : "NULL, ");
|
|
|
|
|
+ sql.append(typeCol != null ? "COALESCE(NULLIF(TRIM(p.`" + typeCol + "`), ''), 'string'), " : "'string', ");
|
|
|
|
|
+ sql.append(descCol != null ? "LEFT(p.`" + descCol + "`, 255), " : "NULL, ");
|
|
|
|
|
+ sql.append(editableCol != null ? "COALESCE(p.`" + editableCol + "`, 1), " : "1, ");
|
|
|
|
|
+ sql.append("LEFT(COALESCE(").append(productKeyExpr).append(", '")
|
|
|
|
|
+ .append(com.etotem.cfc.constant.ProductConfigKeys.DEFAULT_PRODUCT_KEY).append("'), 64), ");
|
|
|
|
|
+ sql.append(upByCol != null ? "p.`" + upByCol + "`, " : "NULL, ");
|
|
|
|
|
+ sql.append(createdAtCol != null ? "COALESCE(p.`" + createdAtCol + "`, NOW()), " : "NOW(), ");
|
|
|
|
|
+ sql.append(updatedAtCol != null ? "COALESCE(p.`" + updatedAtCol + "`, NOW()) " : "NOW() ");
|
|
|
|
|
+ sql.append("FROM product_config p WHERE NOT EXISTS "
|
|
|
|
|
+ + "(SELECT 1 FROM service_config s WHERE s.config_key = CONCAT('"
|
|
|
|
|
+ + com.etotem.cfc.constant.ProductConfigKeys.PRODUCT_KEY_PREFIX
|
|
|
|
|
+ + "', COALESCE(").append(productKeyExpr).append(", '")
|
|
|
|
|
+ .append(com.etotem.cfc.constant.ProductConfigKeys.DEFAULT_PRODUCT_KEY).append("'), '.', p.`")
|
|
|
|
|
+ .append(keyCol).append("`))");
|
|
|
|
|
+
|
|
|
|
|
+ int rows = jdbcTemplate.update(sql.toString());
|
|
|
|
|
+ log.info("迁移413: product_config → service_config 并入完成,新增 {} 行", rows);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.warn("迁移413: product_config 数据并入失败(跳过,可重试):{}", e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 返回 product_config 中第一个实际存在的列名(蛇形优先,兼容驼峰),全部缺失返回 null */
|
|
|
|
|
+ private String firstExistingColumn(String table, String... candidates) {
|
|
|
|
|
+ for (String c : candidates) {
|
|
|
|
|
+ if (columnExists(table, c)) {
|
|
|
|
|
+ return c;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private String cfValueTypeOf(String value) {
|
|
private String cfValueTypeOf(String value) {
|
|
|
if (value == null) {
|
|
if (value == null) {
|
|
|
return "string";
|
|
return "string";
|