| | |
| | | @Slf4j |
| | | @Component |
| | | public class MerchantOnboardingSchemaInitializer implements ApplicationRunner { |
| | | /** DDL 等锁上限:拿不到就放弃这一列,绝不把整个服务拖住。 */ |
| | | private static final int DDL_LOCK_WAIT_TIMEOUT_SECONDS = 5; |
| | | private final JdbcTemplate jdbc; |
| | | |
| | | public MerchantOnboardingSchemaInitializer(JdbcTemplate jdbc) { this.jdbc = jdbc; } |
| | |
| | | columns.put("subject_cert_begin_date", "char(8) default null"); |
| | | columns.put("subject_cert_end_date", "char(8) default null"); |
| | | columns.put("initial_bank_card_id", "bigint default null"); |
| | | // 结算凭证图(对公=开户许可证、对私/个体工商户=银行卡卡号面)。进件时要上报通道, |
| | | // 而进件在通道侧配置失败后需要能**重新提交**;不存这一列就没法复用商户填过的图, |
| | | // 只能让商户把整张表重填一遍——实际就是为了这个原因反复重填过。 |
| | | columns.put("settlement_card_front_url", "varchar(512) default null"); |
| | | columns.put("provider_huifu_id", "varchar(32) default null"); |
| | | columns.put("provider_apply_no", "varchar(32) default null"); |
| | | columns.put("provider_response_code", "varchar(32) default null"); |
| | |
| | | columns.put("capability_response_message", "varchar(512) default null"); |
| | | columns.put("capability_verified_at", "datetime default null"); |
| | | for (Map.Entry<String, String> column : columns.entrySet()) { |
| | | if (!hasColumn("merchant_onboarding_application", column.getKey())) jdbc.execute("alter table merchant_onboarding_application add column " + column.getKey() + " " + column.getValue()); |
| | | if (!hasColumn("merchant_onboarding_application", column.getKey())) addColumn("merchant_onboarding_application", column.getKey(), column.getValue()); |
| | | } |
| | | Map<String, String> bankCardColumns = new LinkedHashMap<>(); |
| | | bankCardColumns.put("provider_token_no", "varchar(32) default null"); |
| | |
| | | bankCardColumns.put("card_no_encrypted", "varchar(1024) default null"); |
| | | bankCardColumns.put("branch_code", "varchar(12) default null"); |
| | | for (Map.Entry<String, String> column : bankCardColumns.entrySet()) { |
| | | if (!hasColumn("merchant_bank_card", column.getKey())) jdbc.execute("alter table merchant_bank_card add column " + column.getKey() + " " + column.getValue()); |
| | | if (!hasColumn("merchant_bank_card", column.getKey())) addColumn("merchant_bank_card", column.getKey(), column.getValue()); |
| | | } |
| | | } catch (Exception exception) { |
| | | log.warn("Merchant onboarding schema initialization failed", exception); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 加列,但**绝不无限期等锁**。 |
| | | * |
| | | * <p>`ALTER TABLE` 需要排他元数据锁。只要表上还有一个没提交的事务(哪怕它已经读完、 |
| | | * {@code trx_query} 为空),DDL 就会一直 PENDING;而 MySQL 默认 `lock_wait_timeout` 是 |
| | | * 数万秒,于是 DDL 会一直挂着,**同时把该表上所有后续查询全部排队**, |
| | | * 最终借光连接池、整个后台不可用。2026-09-15 就是这样瘫掉的: |
| | | * |
| | | * <pre> |
| | | * conn 201 SHARED_READ(GRANTED) 空闲未提交事务,已持有 280 秒 |
| | | * conn 202 EXCLUSIVE(PENDING) alter table ... add column settlement_card_front_url |
| | | * 其余会话 SHARED_READ(PENDING) 全部排队 |
| | | * </pre> |
| | | * |
| | | * <p>所以这里显式把等待上限压到几秒:拿不到锁就放弃这一列,记一条日志, |
| | | * 让应用照常启动(缺列只影响重试功能,不影响整个服务)。SQL 里用 |
| | | * {@code SET SESSION} 而非驱动参数,避免依赖 JDBC URL 是否带 allowMultiQueries。 |
| | | */ |
| | | private void addColumn(String tableName, String columnName, String definition) { |
| | | try { |
| | | jdbc.execute("set session lock_wait_timeout = " + DDL_LOCK_WAIT_TIMEOUT_SECONDS); |
| | | jdbc.execute("alter table " + tableName + " add column " + columnName + " " + definition); |
| | | log.info("Merchant onboarding schema: added {}.{}", tableName, columnName); |
| | | } catch (Exception exception) { |
| | | log.warn("Merchant onboarding schema: could not add {}.{} within {}s (table busy); it will be retried on the next restart. cause={}", |
| | | tableName, columnName, DDL_LOCK_WAIT_TIMEOUT_SECONDS, exception.getClass().getSimpleName()); |
| | | } |
| | | } |
| | | |
| | | private boolean hasColumn(String tableName, String columnName) throws Exception { |
| | | try (Connection connection = jdbc.getDataSource().getConnection(); ResultSet result = connection.getMetaData().getColumns(connection.getCatalog(), null, tableName, columnName)) { |
| | | if (result.next()) return true; |