yeg
2026-09-15 621b51974f9baae62f81f523fa2ac8741dd304a5
talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantOnboardingSchemaInitializer.java
@@ -10,19 +10,63 @@
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
/** Keeps merchant onboarding additive columns available when Flyway is disabled in the deployment. */
/** Keeps merchant onboarding additive tables and columns available when Flyway is disabled in the deployment. */
@Slf4j
@Component
public class MerchantOnboardingSchemaInitializer implements ApplicationRunner {
    /** DDL 等锁上限:拿不到就放弃这一列,绝不把整个服务拖住。 */
    private static final int DDL_LOCK_WAIT_TIMEOUT_SECONDS = 5;
    /**
     * 进件新增的表,与迁移里的建表语句逐字对齐(V144)。
     *
     * <p>开发库(和任何 `spring.flyway.enabled=false` 的部署)不会自动补表:Flyway 不跑,而这个
     * 初始化器原来只会加列。结果是 V144 建的两张表在开发库永远不存在,`GET
     * /api/merchant/onboarding/status` 的第一个请求就以
     * `Table 'talent_card.merchant_onboarding_progress' doesn't exist` 500——测试库有 Flyway,
     * 看不出来。
     */
    private static final Map<String, String> TABLES = new LinkedHashMap<>();
    static {
        TABLES.put("merchant_onboarding_progress", """
                create table if not exists merchant_onboarding_progress (
                    application_id bigint not null primary key,
                    stages_json text not null,
                    last_synced_at datetime null,
                    sync_error varchar(512) not null default '',
                    sync_owner varchar(36) null,
                    sync_until datetime null,
                    updated_at datetime not null default current_timestamp on update current_timestamp
                )""");
        TABLES.put("merchant_onboarding_attempt", """
                create table if not exists merchant_onboarding_attempt (
                    id bigint not null auto_increment primary key,
                    application_id bigint not null,
                    step varchar(24) not null,
                    request_no varchar(32) not null,
                    request_date char(8) not null,
                    apply_no varchar(32) null,
                    status varchar(24) not null,
                    response_code varchar(32) null,
                    response_message varchar(512) null,
                    created_at datetime not null default current_timestamp,
                    unique key uk_onboarding_attempt_request (request_no),
                    key idx_onboarding_attempt_step (application_id, step, id)
                )""");
    }
    private final JdbcTemplate jdbc;
    public MerchantOnboardingSchemaInitializer(JdbcTemplate jdbc) { this.jdbc = jdbc; }
    static Map<String, String> tableDdl() { return Map.copyOf(TABLES); }
    @Override
    public void run(ApplicationArguments args) {
        try {
            for (Map.Entry<String, String> table : TABLES.entrySet()) {
                if (!hasTable(table.getKey())) createTable(table.getKey(), table.getValue());
            }
            Map<String, String> columns = new LinkedHashMap<>();
            columns.put("merchant_name", "varchar(128) default null");
            columns.put("merchant_short_name", "varchar(128) default null");
@@ -118,6 +162,29 @@
        }
    }
    /** 与 {@link #addColumn} 同样地失败即放弃:缺表只影响进件进度,不能让整个服务起不来。 */
    private void createTable(String tableName, String ddl) {
        try {
            jdbc.execute("set session lock_wait_timeout = " + DDL_LOCK_WAIT_TIMEOUT_SECONDS);
            jdbc.execute(ddl);
            log.info("Merchant onboarding schema: created table {}", tableName);
        } catch (Exception exception) {
            log.warn("Merchant onboarding schema: could not create {} within {}s (table busy); it will be retried on the next restart. cause={}",
                    tableName, DDL_LOCK_WAIT_TIMEOUT_SECONDS, exception.getClass().getSimpleName());
        }
    }
    private boolean hasTable(String tableName) throws Exception {
        try (Connection connection = jdbc.getDataSource().getConnection();
             ResultSet result = connection.getMetaData().getTables(connection.getCatalog(), null, tableName, new String[] { "TABLE" })) {
            if (result.next()) return true;
        }
        try (Connection connection = jdbc.getDataSource().getConnection();
             ResultSet result = connection.getMetaData().getTables(connection.getCatalog(), null, tableName.toUpperCase(), new String[] { "TABLE" })) {
            return result.next();
        }
    }
    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;