|
|
@@ -49,9 +49,65 @@ public class DatabaseInitializer implements CommandLineRunner {
|
|
|
@Resource
|
|
|
private CfLegacyLedgerMigrationService cfLegacyLedgerMigrationService;
|
|
|
|
|
|
+ @Resource
|
|
|
+ private com.etotem.cfc.service.MiniAppConfigService miniAppConfigService;
|
|
|
+
|
|
|
@Value("${app.init-test-data:false}")
|
|
|
private boolean initTestData;
|
|
|
|
|
|
+ // ===== 多小程序配置(迁移465 播种 default 行时从 yml wechat:* 读取初始值)=====
|
|
|
+ @Value("${wechat.appid:wx5ba8038ef16fb245}")
|
|
|
+ private String wechatAppid;
|
|
|
+
|
|
|
+ @Value("${wechat.mch-id:}")
|
|
|
+ private String wechatMchId;
|
|
|
+
|
|
|
+ @Value("${wechat.mch-serial-no:}")
|
|
|
+ private String wechatMchSerialNo;
|
|
|
+
|
|
|
+ @Value("${wechat.private-key-path:}")
|
|
|
+ private String wechatPrivateKeyPath;
|
|
|
+
|
|
|
+ @Value("${wechat.notify-url:}")
|
|
|
+ private String wechatNotifyUrl;
|
|
|
+
|
|
|
+ @Value("${wechat.login-url:}")
|
|
|
+ private String wechatLoginUrl;
|
|
|
+
|
|
|
+ @Value("${wechat.phone-url:}")
|
|
|
+ private String wechatPhoneUrl;
|
|
|
+
|
|
|
+ @Value("${wechat.test-mode:false}")
|
|
|
+ private boolean wechatTestMode;
|
|
|
+
|
|
|
+ @Value("${wechat.fulfillment-enabled:true}")
|
|
|
+ private boolean wechatFulfillmentEnabled;
|
|
|
+
|
|
|
+ @Value("${wechat.virtual-pay-env:0}")
|
|
|
+ private int wechatVirtualPayEnv;
|
|
|
+
|
|
|
+ @Value("${wechat.offer-id:}")
|
|
|
+ private String wechatOfferId;
|
|
|
+
|
|
|
+ @Value("${wechat.alert-template-id:}")
|
|
|
+ private String wechatAlertTemplateId;
|
|
|
+
|
|
|
+ @Value("${wechat.promotion-tier-template-id:}")
|
|
|
+ private String wechatPromotionTierTemplateId;
|
|
|
+
|
|
|
+ @Value("${wechat.env-version:release}")
|
|
|
+ private String wechatEnvVersion;
|
|
|
+
|
|
|
+ @Value("${wechat.wxacode-login-page:pages/login/scan-login}")
|
|
|
+ private String wechatWxacodeLoginPage;
|
|
|
+
|
|
|
+ /** 默认小程序 appid(users/appid 回填兜底值,与 yml apps.default.app-id 一致) */
|
|
|
+ @Value("${wechat.apps.default.app-id:wx5ba8038ef16fb245}")
|
|
|
+ private String defaultAppIdFallback;
|
|
|
+
|
|
|
+ // 运行时读取的 appId(由 MiniAppProperties 优先;yml 未配置时回退上面)
|
|
|
+ // 注:weChatService 的 @Value wechat.appid 已由 Spring 绑定;此处仅做 DDL 回填的兜底常量。
|
|
|
+
|
|
|
@Override
|
|
|
public void run(String... args) throws Exception {
|
|
|
log.info("开始初始化数据库...");
|
|
|
@@ -65,6 +121,13 @@ public class DatabaseInitializer implements CommandLineRunner {
|
|
|
// 3. 执行数据库迁移(添加新列)
|
|
|
runMigrations();
|
|
|
|
|
|
+ // 迁移完成后刷新多小程序配置缓存(@PostConstruct 时表可能尚未建出, 此处补齐 DB 播种行)
|
|
|
+ try {
|
|
|
+ miniAppConfigService.reload();
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("迁移后刷新多小程序配置缓存失败(不阻塞): {}", e.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
// 4. 初始化食品相关表(DDL,始终执行)
|
|
|
initFoodTables();
|
|
|
|
|
|
@@ -3491,6 +3554,139 @@ insertSysConfigSeed("gift_membership_pay_price", "198000", "送会员商品达
|
|
|
|
|
|
// 迁移464: 创建 P3 跨案例聚合承载表 attribution_case_strategies(T15 跨案例聚合,S-cfc-119 guhe.p3.lowSideAggregate)
|
|
|
createAttributionCaseStrategyTable();
|
|
|
+
|
|
|
+ // 迁移465: 多小程序配置表 miniapp_config + 播种 default 行(多小程序方案 §2.1,密钥只存 env 引用名)
|
|
|
+ migrateMiniAppConfig();
|
|
|
+
|
|
|
+ // 迁移466: users / product_orders / package_orders 加 appid 列 + users 唯一索引(OID 防串号)
|
|
|
+ migrateMultiAppDataDimensions();
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 迁移465: 多小程序配置表 miniapp_config + 播种 default 行。
|
|
|
+ *
|
|
|
+ * <p>多小程序对接架构方案 §2.1:密钥类字段只存「env 引用名」不存明文;
|
|
|
+ * default 行为 NULL 引用名(回落 yml {@code wechat.*} 既有取值)。
|
|
|
+ * 幂等:{@code CREATE TABLE IF NOT EXISTS} + {@code INSERT IGNORE}(按
|
|
|
+ * mp_code/app_id 唯一键去重),可重复执行不报 Duplicate。</p>
|
|
|
+ */
|
|
|
+ private void migrateMiniAppConfig() {
|
|
|
+ try {
|
|
|
+ jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS miniapp_config ("
|
|
|
+ + "id BIGINT AUTO_INCREMENT PRIMARY KEY, "
|
|
|
+ + "mp_code VARCHAR(32) NOT NULL UNIQUE COMMENT '小程序路由标识(稳定短码,路由/回调/日志用),如 default/xiaoyi/zhishao', "
|
|
|
+ + "display_name VARCHAR(64) COMMENT '展示名,如 CFC主小程序', "
|
|
|
+ + "app_id VARCHAR(32) NOT NULL UNIQUE COMMENT '微信 appid(区分配置的自然键)', "
|
|
|
+ + "secret_ref VARCHAR(128) COMMENT 'secret 的 env 引用名(如 WECHAT_SECRET_XIAOYI);NULL=回落 wechat.secret', "
|
|
|
+ + "mch_id VARCHAR(32) COMMENT '微信支付商户号;NULL=不启用支付', "
|
|
|
+ + "mch_key_ref VARCHAR(128) COMMENT 'mch-key 的 env 引用名', "
|
|
|
+ + "api_v3_key_ref VARCHAR(128) COMMENT 'api-v3-key 的 env 引用名', "
|
|
|
+ + "mch_serial_no VARCHAR(64) COMMENT '商户 API 证书序列号', "
|
|
|
+ + "private_key_path VARCHAR(256) COMMENT 'apiclient_key.pem classpath 路径(每 appid 一份)', "
|
|
|
+ + "notify_url VARCHAR(256) COMMENT '支付回调 URL(可带 /{mpCode} 路径)', "
|
|
|
+ + "login_url VARCHAR(256) DEFAULT 'https://api.weixin.qq.com/sns/jscode2session', "
|
|
|
+ + "phone_url VARCHAR(256) DEFAULT 'https://api.weixin.qq.com/wxa/business/getuserphonenumber', "
|
|
|
+ + "test_mode TINYINT(1) DEFAULT 0 COMMENT '测试模式:1=跳过微信 API', "
|
|
|
+ + "fulfillment_enabled TINYINT(1) DEFAULT 1, "
|
|
|
+ + "virtual_pay_enabled TINYINT(1) DEFAULT 0, "
|
|
|
+ + "appkey_ref VARCHAR(128) COMMENT '虚拟支付现网 appkey 的 env 引用名', "
|
|
|
+ + "sandbox_appkey_ref VARCHAR(128) COMMENT '虚拟支付沙箱 appkey 的 env 引用名', "
|
|
|
+ + "virtual_pay_env TINYINT(1) DEFAULT 0 COMMENT '0=现网 1=沙箱', "
|
|
|
+ + "virtual_pay_token_ref VARCHAR(128) COMMENT '虚拟支付消息推送 Token 的 env 引用名', "
|
|
|
+ + "offer_id VARCHAR(32) COMMENT '虚拟支付应用 ID(每个小程序专属)', "
|
|
|
+ + "alert_template_id VARCHAR(64) COMMENT '订阅消息模板 ID', "
|
|
|
+ + "promotion_tier_template_id VARCHAR(64) COMMENT '促销档位模板 ID', "
|
|
|
+ + "env_version VARCHAR(16) DEFAULT 'release' COMMENT '小程序码环境:dev/test/release', "
|
|
|
+ + "wxacode_login_page VARCHAR(128) DEFAULT 'pages/login/scan-login', "
|
|
|
+ + "enabled TINYINT(1) DEFAULT 1, "
|
|
|
+ + "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, "
|
|
|
+ + "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "
|
|
|
+ + "INDEX idx_app_id (app_id) "
|
|
|
+ + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='多小程序配置表(密钥只存 env 引用名,不存明文)'");
|
|
|
+ log.info("迁移465: miniapp_config 表已就绪");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("迁移465: miniapp_config 建表失败(忽略,依赖 schema.sql 兜底): {}", e.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 播种 default 行:从 yml wechat: 读初始值,INSERT IGNORE 幂等
|
|
|
+ try {
|
|
|
+ int seeded = jdbcTemplate.update(
|
|
|
+ "INSERT IGNORE INTO miniapp_config "
|
|
|
+ + "(mp_code, display_name, app_id, secret_ref, mch_id, mch_key_ref, api_v3_key_ref, "
|
|
|
+ + "mch_serial_no, private_key_path, notify_url, login_url, phone_url, test_mode, "
|
|
|
+ + "fulfillment_enabled, virtual_pay_enabled, appkey_ref, sandbox_appkey_ref, "
|
|
|
+ + "virtual_pay_env, virtual_pay_token_ref, offer_id, alert_template_id, "
|
|
|
+ + "promotion_tier_template_id, env_version, wxacode_login_page, enabled) "
|
|
|
+ + "VALUES ('default', 'CFC主小程序', ?, NULL, ?, NULL, NULL, ?, ?, ?, ?, ?, ?, ?, "
|
|
|
+ + "0, NULL, NULL, ?, NULL, ?, ?, ?, ?, ?, 1)",
|
|
|
+ wechatAppid, wechatMchId, wechatMchSerialNo, wechatPrivateKeyPath, wechatNotifyUrl,
|
|
|
+ wechatLoginUrl, wechatPhoneUrl, wechatTestMode ? 1 : 0, wechatFulfillmentEnabled ? 1 : 0,
|
|
|
+ wechatVirtualPayEnv, wechatOfferId, wechatAlertTemplateId,
|
|
|
+ wechatPromotionTierTemplateId, wechatEnvVersion, wechatWxacodeLoginPage);
|
|
|
+ log.info("迁移465: miniapp_config 播种 default 行结果={} (0=已存在跳过, 1=新插入)", seeded);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("迁移465: miniapp_config 播种 default 行失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 迁移466: 数据层多小程序隔离(T03)—— users/product_orders/package_orders 加 appid 列,
|
|
|
+ * 同时建立 (openid, appid) 唯一索引(决策 2:先查重后建;冲突只告警不阻塞)。
|
|
|
+ *
|
|
|
+ * <p>参考:docs/system_design.md §5、§6 P3、§7 T03。</p>
|
|
|
+ */
|
|
|
+ private void migrateMultiAppDataDimensions() {
|
|
|
+ // 1) 对 orders 加列
|
|
|
+ ensureColumn("product_orders", "appid", "VARCHAR(32) NULL COMMENT '小程序 appid(订单数据隔离维度)'");
|
|
|
+ ensureColumn("package_orders", "appid", "VARCHAR(32) NULL COMMENT '小程序 appid(套餐订单数据隔离维度)'");
|
|
|
+
|
|
|
+ // 2) users 加列并回填存量默认为当前 default appid(保持既有账号归属)
|
|
|
+ ensureColumn("users", "appid",
|
|
|
+ "VARCHAR(32) NULL COMMENT '小程序 appid(多小程序数据隔离维度,默认=default app)'");
|
|
|
+ try {
|
|
|
+ int updated = jdbcTemplate.update(
|
|
|
+ "UPDATE users SET appid = ? WHERE appid IS NULL OR TRIM(appid)='' OR LENGTH(TRIM(appid))=0",
|
|
|
+ defaultAppIdFallback != null ? defaultAppIdFallback.trim() : "wx5ba8038ef16fb245");
|
|
|
+ log.info("迁移466: users.appid 回填默认={},影响行数={}", defaultAppIdFallback, updated);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("迁移466: users.appid 回填失败(可能列不存在或类型不兼容): {}", e.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3) 查重:同 (openid, appid) 出现多条 → 告警(决策2:不阻塞)
|
|
|
+ try {
|
|
|
+ List<Map<String, Object>> dupRows = jdbcTemplate.queryForList(
|
|
|
+ "SELECT openid, appid, COUNT(*) AS cnt FROM users "
|
|
|
+ + "WHERE openid IS NOT NULL AND TRIM(openid) <> '' "
|
|
|
+ + "GROUP BY openid, appid HAVING cnt > 1 LIMIT 20");
|
|
|
+ if (!dupRows.isEmpty()) {
|
|
|
+ log.warn("迁移466: users 表在 {} 个 (openid, appid) 组存在重复记录,将跳过唯一索引建表:"
|
|
|
+ , dupRows.size());
|
|
|
+ for (Map<String, Object> row : dupRows) {
|
|
|
+ log.warn(" - openid={}, appid={}, count={}",
|
|
|
+ row.get("openid"), row.get("appid"), row.get("cnt"));
|
|
|
+ }
|
|
|
+ log.warn("请人工核查这些重复 openid 的归属后,再手动执行: ALTER TABLE users ADD UNIQUE INDEX uk_openid_appid(openid, appid)");
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ // 若查重的查询因表结构/列缺失报错,忽略并继续(后续建唯一索引步骤会被 catch 捕获)
|
|
|
+ log.debug("迁移466: 查重查询失败(继续): {}", e.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4) 尝试建唯一索引(IF NOT EXISTS 幂等;有冲突会抛 Duplicate key exception)
|
|
|
+ try {
|
|
|
+ jdbcTemplate.execute("ALTER TABLE users ADD UNIQUE INDEX uk_openid_appid (openid, appid)");
|
|
|
+ log.info("迁移466: users.uk_openid_appid 唯一索引创建成功");
|
|
|
+ } catch (Exception e) {
|
|
|
+ String msg = e.getMessage();
|
|
|
+ if (msg != null && (msg.contains("Duplicate") || msg.contains("already exists")
|
|
|
+ || msg.toLowerCase().contains("duplicate"))) {
|
|
|
+ log.warn("迁移466: users.uk_openid_appid 唯一索引建失败(存在同 appid 内重复 openid,请人工处理): {}", msg);
|
|
|
+ } else {
|
|
|
+ // 其他未知错误,仅 warn 保留应用启动能力;后续可重试迁移
|
|
|
+ log.warn("迁移466: users 唯一索引建失败(非幂等失败, 暂不重试): {}", msg);
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/**
|