20260911173133

This commit is contained in:
oneao committed 2026-09-11 17:31:34 +08:00
1 parent 3838ed74b1
commit f3ad727f1c
60 files changed
+4971 -6621

No files matched your search

@@ -0,0 +1,62 @@
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.Properties;
/**
* One-off migration: adds b_i18n varchar(150) null to dbo.s_power
* (i18n resource key for permission point names, see FMS新系统核心表结构设计.md 14.1).
* Idempotent: skips the ALTER TABLE when the column already exists.
*/
public final class AddSPowerI18nColumn {
public static void main(String[] args) throws Exception {
Path apiRoot = Path.of("").toAbsolutePath().normalize();
Path configPath = apiRoot.resolve("config/dbconfigs/g3hd.properties").normalize();
Properties properties = new Properties();
try (InputStream input = Files.newInputStream(configPath)) {
properties.load(input);
}
try (Connection connection = DriverManager.getConnection(
required(properties, "url"),
required(properties, "username"),
required(properties, "password"))) {
if (!"FMS".equalsIgnoreCase(connection.getCatalog())) {
throw new IllegalStateException("Refusing to alter database: " + connection.getCatalog());
}
try (Statement statement = connection.createStatement()) {
boolean exists;
try (ResultSet rows = statement.executeQuery(
"SELECT CASE WHEN COL_LENGTH(N'dbo.s_power', N'b_i18n') IS NULL THEN 0 ELSE 1 END")) {
rows.next();
exists = rows.getInt(1) == 1;
}
if (exists) {
System.out.println("SKIP|dbo.s_power.b_i18n already exists");
} else {
statement.executeUpdate("ALTER TABLE dbo.s_power ADD b_i18n varchar(150) NULL");
System.out.println("APPLIED|ALTER TABLE dbo.s_power ADD b_i18n varchar(150) NULL");
}
try (ResultSet rows = statement.executeQuery(
"SELECT c.name, t.name, c.max_length, c.is_nullable "
+ "FROM sys.columns c JOIN sys.types t ON t.user_type_id = c.user_type_id "
+ "WHERE c.object_id = OBJECT_ID(N'dbo.s_power') AND c.name = N'b_i18n'")) {
if (!rows.next()) throw new IllegalStateException("Column b_i18n not found after migration");
System.out.println("VERIFY|" + rows.getString(1) + "|" + rows.getString(2)
+ "(" + rows.getInt(3) + ")|nullable=" + rows.getInt(4));
}
}
}
}
private static String required(Properties properties, String key) {
String value = properties.getProperty(key);
if (value == null || value.isBlank()) throw new IllegalStateException("Missing property: " + key);
return value;
}
}
@@ -1,633 +0,0 @@
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
/**
* Reads the legacy SQL Server catalog and emits reviewed, schema-only FMS migrations.
* The generated SQL never connects back to the legacy database and never contains data.
*/
public final class GenerateBusinessMigrations {
private static final Set<String> EXCLUDED_TABLES = lowerSet(
"b_contact", "b_contact_type", "b_othercompany", "b_othercompany_bmfl",
"b_user", "b_user_power", "b_user_qx", "b_user_record",
"dtproperties", "log_datas"
);
private static final Set<String> TREE_TABLES = lowerSet(
"bc_pricelist_bmfl", "oa_title_bmfl"
);
private static final Set<String> EXCLUDED_VIEWS = lowerSet(
"v_autoidrule", "v_b_contact", "v_b_employee", "v_b_user_record"
);
private static final Set<String> BOOLEAN_COLUMNS = lowerSet(
"b_black", "b_canuse", "b_visible", "b_used", "b_default", "b_isdefault",
"b_finish", "b_sh", "b_bill", "b_zx", "b_toall", "b_reminder",
"b_othermachinelogin", "b_salesman", "b_docman", "b_dealman",
"b_visible_customer", "b_visible_supply", "b_levelvisible", "b_fromcustomer",
"b_logintime", "b_checkxy", "b_loginfee", "mx_finish", "mx_sh"
);
private static final Set<String> TRUE_DEFAULT_COLUMNS = lowerSet(
"b_canuse", "b_visible", "b_used"
);
private static final Set<String> UNICODE_HINTS = lowerSet(
"name", "memo", "bz", "addr", "address", "subject", "content", "info",
"desc", "caption", "department", "duty", "bank", "goods", "mark",
"remark", "taste", "habit", "situation", "place", "body"
);
private static final Map<String, String> BUSINESS_CODE_FIELDS = Map.ofEntries(
Map.entry("bc_khht", "b_htno"),
Map.entry("bs_business", "b_ywno"),
Map.entry("cw_account", "b_account"),
Map.entry("cw_bill", "b_ywno"),
Map.entry("cw_invoice", "b_ywno"),
Map.entry("cw_sfd", "b_ywno")
);
private static final Map<String, String> CHILD_PARENTS = Map.ofEntries(
Map.entry("b_othercompany_accountbank", "b_othercompany"),
Map.entry("bc_pricelist_sub", "bc_pricelist"),
Map.entry("bs_business_container", "bs_business"),
Map.entry("bs_business_cw", "bs_business"),
Map.entry("bs_business_hdbl", "bs_business"),
Map.entry("bs_business_transport", "bs_business"),
Map.entry("cw_billsub", "cw_bill"),
Map.entry("cw_invoicesub", "cw_invoice"),
Map.entry("cw_sfdsub", "cw_sfd"),
Map.entry("oa_notices_attach", "oa_notices"),
Map.entry("oa_noticessub", "oa_notices")
);
private static final Map<String, String> RENAMED_COLUMNS = Map.ofEntries(
Map.entry("bs_business_cw.mx_type_id", "mx_direction"),
Map.entry("bs_business_transport.mx_hstype_id", "mx_source_type"),
Map.entry("cw_bill.b_module_id", "b_direction"),
Map.entry("cw_bill.b_type_id", "b_document_type"),
Map.entry("cw_invoice.b_module_id", "b_direction"),
Map.entry("cw_invoice.b_type_id", "b_document_type"),
Map.entry("cw_sfd.b_module_id", "b_direction"),
Map.entry("cw_sfd.b_type_id", "b_document_type")
);
private record Column(
String name,
String type,
int maxLength,
int precision,
int scale,
boolean nullable,
int ordinal
) {}
private record Table(String name, List<Column> columns) {}
public static void main(String[] args) throws Exception {
Path apiRoot = Path.of("").toAbsolutePath().normalize();
Path sourceConfig = apiRoot.resolve("config/dbconfigs/G3HY2025.properties");
Path migrations = apiRoot.resolve("config/migrations");
Properties properties = new Properties();
try (InputStream input = Files.newInputStream(sourceConfig)) {
properties.load(input);
}
try (Connection connection = DriverManager.getConnection(
properties.getProperty("url"),
properties.getProperty("username"),
properties.getProperty("password")
)) {
Map<String, Table> tables = loadTables(connection);
writeDomain(migrations.resolve("016_create_base_tables.sql"), "基础资料", tables, "base");
writeDomain(
migrations.resolve("017_create_contract_file_tables.sql"),
"合同和文件资料",
tables,
"contract-file"
);
writeDomain(migrations.resolve("018_create_business_tables.sql"), "业务单据", tables, "business");
writeDomain(migrations.resolve("019_create_finance_tables.sql"), "财务", tables, "finance");
writeDomain(migrations.resolve("020_create_oa_tables.sql"), "OA", tables, "oa");
writeViews(migrations.resolve("021_create_business_views.sql"), connection);
writeVerification(migrations.resolve("verify_business_schema.sql"), tables, connection);
}
}
private static Map<String, Table> loadTables(Connection connection) throws Exception {
String sql = """
SELECT t.name AS table_name, c.name AS column_name, ty.name AS type_name,
c.max_length, c.precision, c.scale, c.is_nullable, c.column_id
FROM sys.tables AS t
INNER JOIN sys.schemas AS s ON s.schema_id = t.schema_id
INNER JOIN sys.columns AS c ON c.object_id = t.object_id
INNER JOIN sys.types AS ty ON ty.user_type_id = c.user_type_id
WHERE s.name = N'dbo' AND t.is_ms_shipped = 0
ORDER BY t.name, c.column_id
""";
Map<String, List<Column>> columnsByTable = new LinkedHashMap<>();
try (Statement statement = connection.createStatement(); ResultSet rows = statement.executeQuery(sql)) {
while (rows.next()) {
String tableName = rows.getString("table_name");
columnsByTable.computeIfAbsent(tableName, ignored -> new ArrayList<>()).add(new Column(
rows.getString("column_name"),
rows.getString("type_name"),
rows.getInt("max_length"),
rows.getInt("precision"),
rows.getInt("scale"),
rows.getBoolean("is_nullable"),
rows.getInt("column_id")
));
}
}
Map<String, Table> result = new LinkedHashMap<>();
for (Map.Entry<String, List<Column>> entry : columnsByTable.entrySet()) {
if (!includeTable(entry.getKey())) continue;
result.put(entry.getKey().toLowerCase(Locale.ROOT), new Table(entry.getKey(), entry.getValue()));
}
return result;
}
private static boolean includeTable(String tableName) {
String name = tableName.toLowerCase(Locale.ROOT);
if (EXCLUDED_TABLES.contains(name)) return false;
return name.startsWith("b_")
|| name.startsWith("bc_")
|| name.startsWith("bf_")
|| name.startsWith("bs_")
|| name.startsWith("cw_")
|| name.startsWith("oa_");
}
private static String domainOf(String tableName) {
String name = tableName.toLowerCase(Locale.ROOT);
if (name.startsWith("bc_") || name.startsWith("bf_")) return "contract-file";
if (name.startsWith("bs_")) return "business";
if (name.startsWith("cw_")) return "finance";
if (name.startsWith("oa_")) return "oa";
return "base";
}
private static void writeDomain(
Path target,
String title,
Map<String, Table> tables,
String domain
) throws Exception {
StringBuilder sql = new StringBuilder(header(title));
tables.values().stream()
.filter(table -> domainOf(table.name()).equals(domain))
.sorted(Comparator.comparing(Table::name, String.CASE_INSENSITIVE_ORDER))
.forEach(table -> sql.append(createTable(table)));
if (domain.equals("finance")) appendFinanceRelations(sql);
sql.append("\nCOMMIT TRANSACTION;\n");
Files.writeString(target, sql.toString(), StandardCharsets.UTF_8);
}
private static String header(String title) {
return """
/* Generated schema-only migration: %s. No legacy data is copied. */
SET NOCOUNT ON;
SET XACT_ABORT ON;
IF DB_NAME() <> N'FMS'
THROW 51501, N'本迁移只能在 FMS 目标数据库执行。', 1;
BEGIN TRANSACTION;
""".formatted(title);
}
private static String createTable(Table table) {
String lowerName = table.name().toLowerCase(Locale.ROOT);
if (TREE_TABLES.contains(lowerName)) return createTreeTable(table.name());
boolean child = hasColumn(table, "subid");
String primaryKey = child ? originalColumnName(table, "subid") : "b_id";
boolean sourceHasStringId = !child && table.columns().stream().anyMatch(column ->
column.name().equalsIgnoreCase("b_id") && isTextType(column.type())
);
String businessCode = businessCodeField(table, sourceHasStringId);
List<String> definitions = new ArrayList<>();
if (!child && !hasColumn(table, "b_id")) {
definitions.add(" b_id bigint NOT NULL");
}
for (Column column : table.columns()) {
if (isPluralIdColumn(column.name())) continue;
String targetName = targetColumnName(table, column.name());
String definition = " " + quote(targetName) + " " + targetType(table, column);
boolean requiredCode = businessCode != null && column.name().equalsIgnoreCase(businessCode);
boolean requiredKey = column.name().equalsIgnoreCase(primaryKey)
|| (!child && column.name().equalsIgnoreCase("b_id"));
if (BOOLEAN_COLUMNS.contains(column.name().toLowerCase(Locale.ROOT))) {
int defaultValue = TRUE_DEFAULT_COLUMNS.contains(column.name().toLowerCase(Locale.ROOT)) ? 1 : 0;
definition += " NOT NULL CONSTRAINT "
+ quote("DF_" + safeName(table.name()) + "_" + safeName(column.name()))
+ " DEFAULT (" + defaultValue + ")";
} else {
definition += requiredCode || requiredKey ? " NOT NULL" : column.nullable() ? " NULL" : " NOT NULL";
}
definitions.add(definition);
}
if (sourceHasStringId && businessCode != null && !hasColumn(table, businessCode)) {
definitions.add(" " + quote(businessCode) + " varchar(50) NOT NULL");
}
definitions.add(" CONSTRAINT " + quote("PK_" + safeName(table.name()))
+ " PRIMARY KEY (" + quote(primaryKey) + ")");
if (businessCode != null) {
definitions.add(" CONSTRAINT " + quote("UQ_" + safeName(table.name()) + "_" + businessCode)
+ " UNIQUE (" + quote(businessCode) + ")");
}
String parentTable = CHILD_PARENTS.get(lowerName);
if (child && parentTable != null && hasColumn(table, "father")) {
definitions.add(" CONSTRAINT " + quote("FK_" + safeName(table.name()) + "_father")
+ " FOREIGN KEY (" + quote(originalColumnName(table, "father")) + ") REFERENCES dbo."
+ quote(parentTable) + " (b_id)");
}
StringBuilder result = new StringBuilder();
result.append(" IF OBJECT_ID(N'dbo.").append(escape(table.name())).append("', N'U') IS NULL\n");
result.append(" BEGIN\n");
result.append(" CREATE TABLE dbo.").append(quote(table.name())).append(" (\n");
result.append(String.join(",\n", definitions)).append("\n );\n");
appendIndexes(result, table, primaryKey, businessCode);
result.append(" END;\n\n");
return result.toString();
}
private static String createTreeTable(String tableName) {
String safe = safeName(tableName);
return """
IF OBJECT_ID(N'dbo.%s', N'U') IS NULL
BEGIN
CREATE TABLE dbo.%s (
b_id bigint NOT NULL,
b_parent_id bigint NULL,
b_name nvarchar(150) NOT NULL,
b_level int NOT NULL CONSTRAINT %s DEFAULT (1),
b_path varchar(1000) NOT NULL,
b_xh int NOT NULL CONSTRAINT %s DEFAULT (0),
b_canuse int NOT NULL CONSTRAINT %s DEFAULT (1),
b_inputuser_id bigint NULL,
b_inputdatetime datetime2(0) NOT NULL CONSTRAINT %s DEFAULT (SYSDATETIME()),
b_updateuser_id bigint NULL,
b_updatedatetime datetime2(0) NULL,
CONSTRAINT %s PRIMARY KEY (b_id),
CONSTRAINT %s CHECK (b_parent_id IS NULL OR b_parent_id <> b_id),
CONSTRAINT %s FOREIGN KEY (b_parent_id) REFERENCES dbo.%s (b_id)
);
CREATE INDEX %s ON dbo.%s (b_parent_id, b_xh, b_id);
CREATE INDEX %s ON dbo.%s (b_path, b_xh, b_id);
END;
""".formatted(
escape(tableName), quote(tableName),
quote("DF_" + safe + "_level"), quote("DF_" + safe + "_xh"),
quote("DF_" + safe + "_canuse"), quote("DF_" + safe + "_inputdatetime"),
quote("PK_" + safe), quote("CK_" + safe + "_parent"), quote("FK_" + safe + "_parent"),
quote(tableName), quote("IX_" + safe + "_parent"), quote(tableName),
quote("IX_" + safe + "_path"), quote(tableName)
);
}
private static void appendIndexes(
StringBuilder sql,
Table table,
String primaryKey,
String businessCode
) {
Set<String> indexed = new LinkedHashSet<>();
for (Column column : table.columns()) {
String targetName = targetColumnName(table, column.name());
String lower = targetName.toLowerCase(Locale.ROOT);
if (lower.equals(primaryKey.toLowerCase(Locale.ROOT))) continue;
if (businessCode != null && lower.equals(businessCode.toLowerCase(Locale.ROOT))) continue;
if (lower.equals("father") || (lower.endsWith("_id") && !isAuditUserColumn(lower))) {
if (!indexed.add(lower)) continue;
String indexName = "IX_" + safeName(table.name()) + "_" + safeName(column.name());
sql.append(" CREATE INDEX ").append(quote(indexName)).append(" ON dbo.")
.append(quote(table.name())).append(" (").append(quote(targetName))
.append(", ").append(quote(primaryKey)).append(");\n");
}
}
}
private static String businessCodeField(Table table, boolean sourceHasStringId) {
String lowerName = table.name().toLowerCase(Locale.ROOT);
String configured = BUSINESS_CODE_FIELDS.get(lowerName);
if (configured != null) return configured;
if (hasColumn(table, "b_code")) return originalColumnName(table, "b_code");
return sourceHasStringId ? "b_code" : null;
}
private static String targetType(Table table, Column column) {
String name = column.name().toLowerCase(Locale.ROOT);
String sourceType = column.type().toLowerCase(Locale.ROOT);
String renameKey = table.name().toLowerCase(Locale.ROOT) + "." + name;
if (renameKey.equals("bs_business_cw.mx_type_id")
|| renameKey.equals("cw_bill.b_module_id")
|| renameKey.equals("cw_invoice.b_module_id")
|| renameKey.equals("cw_sfd.b_module_id")) {
return "varchar(20)";
}
if (renameKey.equals("bs_business_transport.mx_hstype_id")
|| renameKey.endsWith(".b_type_id")) {
return "varchar(50)";
}
if (name.equals("b_id") || name.equals("subid") || name.equals("father") || isReferenceId(name)) {
return "bigint";
}
if (BOOLEAN_COLUMNS.contains(name) || sourceType.equals("bit")) return "int";
if (name.endsWith("datetime") || name.endsWith("_datetime") || name.equals("b_inputdate")) {
return "datetime2(0)";
}
if (name.equals("b_inputtime")) return "time(0)";
if (name.equals("b_birthday") || name.endsWith("begindate") || name.endsWith("enddate")
|| name.endsWith("expiredate")) {
return "date";
}
if (sourceType.equals("text") || sourceType.equals("ntext")) return "nvarchar(max)";
if (sourceType.equals("image")) return "varbinary(max)";
if (sourceType.equals("datetime") || sourceType.equals("smalldatetime")) return "datetime2(0)";
if (sourceType.equals("float") || sourceType.equals("real")) return "decimal(18,6)";
if (sourceType.equals("money") || sourceType.equals("smallmoney")) return "decimal(19,4)";
if (sourceType.equals("decimal") || sourceType.equals("numeric")) {
return sourceType + "(" + column.precision() + "," + column.scale() + ")";
}
if (sourceType.equals("varchar") || sourceType.equals("char")
|| sourceType.equals("nvarchar") || sourceType.equals("nchar")) {
boolean unicode = sourceType.startsWith("n") || hasUnicodeHint(name);
String base = unicode ? "nvarchar" : "varchar";
int bytesPerChar = sourceType.startsWith("n") ? 2 : 1;
String length = column.maxLength() < 0 ? "max" : String.valueOf(column.maxLength() / bytesPerChar);
return base + "(" + length + ")";
}
if (sourceType.equals("binary") || sourceType.equals("varbinary")) {
String length = column.maxLength() < 0 ? "max" : String.valueOf(column.maxLength());
return sourceType + "(" + length + ")";
}
if (sourceType.equals("timestamp") || sourceType.equals("rowversion")) return "binary(8)";
if (sourceType.equals("uniqueidentifier")) return "bigint";
return sourceType;
}
private static boolean isReferenceId(String name) {
return name.endsWith("_id")
|| name.equals("userid")
|| name.equals("moduleid")
|| name.endsWith("userid")
|| name.endsWith("moduleid");
}
private static boolean isAuditUserColumn(String name) {
return name.equals("b_inputuser_id")
|| name.equals("b_updateuser_id")
|| name.equals("mx_inputuser_id")
|| name.equals("mx_updateuser_id");
}
private static boolean isPluralIdColumn(String name) {
String lower = name.toLowerCase(Locale.ROOT);
return lower.endsWith("_ids") || lower.equals("b_yw_ids") || lower.equals("b_bill_ids")
|| lower.equals("b_inv_ids");
}
private static boolean hasUnicodeHint(String name) {
for (String hint : UNICODE_HINTS) {
if (name.contains(hint)) return true;
}
return false;
}
private static boolean isTextType(String type) {
String lower = type.toLowerCase(Locale.ROOT);
return lower.equals("varchar") || lower.equals("nvarchar") || lower.equals("char")
|| lower.equals("nchar") || lower.equals("text") || lower.equals("ntext");
}
private static boolean hasColumn(Table table, String name) {
return table.columns().stream().anyMatch(column -> column.name().equalsIgnoreCase(name));
}
private static String originalColumnName(Table table, String name) {
return table.columns().stream()
.filter(column -> column.name().equalsIgnoreCase(name))
.findFirst()
.map(Column::name)
.orElse(name);
}
private static void appendFinanceRelations(StringBuilder sql) {
appendRelation(sql, "cw_bill_business", "b_bill_id", "cw_bill", "b_business_id", "bs_business");
appendRelation(sql, "cw_invoice_bill", "b_invoice_id", "cw_invoice", "b_bill_id", "cw_bill");
appendRelation(sql, "cw_invoice_business", "b_invoice_id", "cw_invoice", "b_business_id", "bs_business");
appendRelation(sql, "cw_sfd_business", "b_sfd_id", "cw_sfd", "b_business_id", "bs_business");
appendRelation(sql, "cw_sfd_invoice", "b_sfd_id", "cw_sfd", "b_invoice_id", "cw_invoice");
appendRelation(sql, "cw_sfd_bill", "b_sfd_id", "cw_sfd", "b_bill_id", "cw_bill");
}
private static void appendRelation(
StringBuilder sql,
String table,
String leftColumn,
String leftTable,
String rightColumn,
String rightTable
) {
String safe = safeName(table);
sql.append(" IF OBJECT_ID(N'dbo.").append(table).append("', N'U') IS NULL\n")
.append(" BEGIN\n")
.append(" CREATE TABLE dbo.").append(quote(table)).append(" (\n")
.append(" subid bigint NOT NULL,\n")
.append(" ").append(quote(leftColumn)).append(" bigint NOT NULL,\n")
.append(" ").append(quote(rightColumn)).append(" bigint NOT NULL,\n")
.append(" b_xh int NOT NULL CONSTRAINT ").append(quote("DF_" + safe + "_xh"))
.append(" DEFAULT (0),\n")
.append(" CONSTRAINT ").append(quote("PK_" + safe)).append(" PRIMARY KEY (subid),\n")
.append(" CONSTRAINT ").append(quote("UQ_" + safe)).append(" UNIQUE (")
.append(quote(leftColumn)).append(", ").append(quote(rightColumn)).append("),\n")
.append(" CONSTRAINT ").append(quote("FK_" + safe + "_left")).append(" FOREIGN KEY (")
.append(quote(leftColumn)).append(") REFERENCES dbo.").append(quote(leftTable)).append(" (b_id),\n")
.append(" CONSTRAINT ").append(quote("FK_" + safe + "_right")).append(" FOREIGN KEY (")
.append(quote(rightColumn)).append(") REFERENCES dbo.").append(quote(rightTable)).append(" (b_id)\n")
.append(" );\n")
.append(" END;\n\n");
}
private static void writeViews(Path target, Connection connection) throws Exception {
String query = """
SELECT v.name, sm.definition
FROM sys.views AS v
INNER JOIN sys.schemas AS s ON s.schema_id = v.schema_id
INNER JOIN sys.sql_modules AS sm ON sm.object_id = v.object_id
WHERE s.name = N'dbo'
ORDER BY v.name
""";
StringBuilder sql = new StringBuilder(header("业务视图"));
try (Statement statement = connection.createStatement(); ResultSet rows = statement.executeQuery(query)) {
while (rows.next()) {
String name = rows.getString("name");
if (!includeView(name)) continue;
String definition = rows.getString("definition");
if (name.equalsIgnoreCase("v_b_othercompany_bmfl")) {
definition = "CREATE VIEW dbo.v_b_othercompany_bmfl AS "
+ "SELECT b_id, b_name FROM dbo.b_othercompany_bmfl";
} else if (name.equalsIgnoreCase("v_b_feetype")) {
definition = "CREATE VIEW dbo.v_b_feetype AS SELECT "
+ "fee.b_id, fee.b_name, fee.b_name_e, fee.b_bz, fee.b_default, "
+ "fee.b_amount, fee.b_tax, fee.b_money_id, fee.b_inputuser_id, "
+ "fee.b_inputdatetime, fee.b_updateuser_id, fee.b_updatedatetime, money.b_hl "
+ "FROM dbo.b_feetype AS fee "
+ "LEFT JOIN dbo.b_money AS money ON money.b_id = fee.b_money_id";
}
definition = removeLegacyPluralIds(definition);
definition = renameLegacySemanticIds(name, definition);
sql.append(" IF OBJECT_ID(N'dbo.").append(escape(name)).append("', N'V') IS NOT NULL\n")
.append(" DROP VIEW dbo.").append(quote(name)).append(";\n")
.append(" EXEC sys.sp_executesql N'")
.append(escape(definition))
.append("';\n\n");
}
}
sql.append("COMMIT TRANSACTION;\n");
Files.writeString(target, sql.toString(), StandardCharsets.UTF_8);
}
private static boolean includeView(String viewName) {
String name = viewName.toLowerCase(Locale.ROOT);
if (EXCLUDED_VIEWS.contains(name)) return false;
return name.startsWith("v_b_") || name.startsWith("v_bc_")
|| name.startsWith("v_bs_") || name.startsWith("v_cw_")
|| name.equals("v_feetype");
}
private static String removeLegacyPluralIds(String definition) {
String result = definition;
for (String field : List.of("b_yw_ids", "b_bill_ids", "b_inv_ids")) {
result = result.replaceAll(
"(?i)(?:dbo\\.)?[A-Za-z0-9_]+\\." + field + "\\s*,\\s*",
""
);
result = result.replaceAll(
"(?i),\\s*(?:dbo\\.)?[A-Za-z0-9_]+\\." + field + "(?=\\s+FROM)",
""
);
}
return result;
}
private static String renameLegacySemanticIds(String viewName, String definition) {
String result = definition;
result = replaceIgnoreCase(result, "dbo.bs_business_cw.mx_type_id", "dbo.bs_business_cw.mx_direction");
result = replaceIgnoreCase(result, "dbo.bs_business_transport.mx_hstype_id", "dbo.bs_business_transport.mx_source_type");
result = replaceIgnoreCase(result, "dbo.cw_bill.b_module_id", "dbo.cw_bill.b_direction");
result = replaceIgnoreCase(result, "dbo.cw_bill.b_type_id", "dbo.cw_bill.b_document_type");
result = replaceIgnoreCase(result, "dbo.cw_invoice.b_module_id", "dbo.cw_invoice.b_direction");
result = replaceIgnoreCase(result, "dbo.cw_invoice.b_type_id", "dbo.cw_invoice.b_document_type");
result = replaceIgnoreCase(result, "dbo.cw_sfd.b_module_id", "dbo.cw_sfd.b_direction");
result = replaceIgnoreCase(result, "dbo.cw_sfd.b_type_id", "dbo.cw_sfd.b_document_type");
if (viewName.equalsIgnoreCase("v_bs_business_cw")
|| viewName.equalsIgnoreCase("v_bs_business_cw_yf")
|| viewName.equalsIgnoreCase("v_bs_business_cw_ys")) {
result = replaceIgnoreCase(result, "mx_type_id", "mx_direction");
}
if (viewName.equalsIgnoreCase("v_cw_account")) {
result = replaceIgnoreCase(result, "b_type_id", "b_direction");
result = replaceIgnoreCase(
result,
"a.b_account_id = dbo.cw_account.b_account",
"a.b_account_id = dbo.cw_account.b_id"
);
}
return result;
}
private static String replaceIgnoreCase(String value, String source, String replacement) {
return value.replaceAll("(?i)" + java.util.regex.Pattern.quote(source), replacement);
}
private static String targetColumnName(Table table, String sourceName) {
String key = table.name().toLowerCase(Locale.ROOT) + "." + sourceName.toLowerCase(Locale.ROOT);
return RENAMED_COLUMNS.getOrDefault(key, sourceName);
}
private static void writeVerification(
Path target,
Map<String, Table> tables,
Connection connection
) throws Exception {
int expectedTables = tables.size() + 6 + 5;
int expectedViews = 0;
try (Statement statement = connection.createStatement(); ResultSet rows = statement.executeQuery(
"SELECT name FROM sys.views WHERE is_ms_shipped = 0"
)) {
while (rows.next()) if (includeView(rows.getString(1))) expectedViews++;
}
String sql = """
SET NOCOUNT ON;
IF DB_NAME() <> N'FMS'
THROW 51510, N'验证脚本只能在 FMS 目标数据库执行。', 1;
SELECT
(SELECT COUNT(*) FROM sys.tables WHERE is_ms_shipped = 0) AS table_count,
(SELECT COUNT(*) FROM sys.views WHERE is_ms_shipped = 0) AS view_count,
%d AS generated_business_table_minimum,
%d AS generated_business_view_count;
SELECT OBJECT_SCHEMA_NAME(c.object_id) AS schema_name,
OBJECT_NAME(c.object_id) AS object_name,
c.name AS column_name,
TYPE_NAME(c.user_type_id) AS type_name
FROM sys.columns AS c
INNER JOIN sys.tables AS t ON t.object_id = c.object_id
WHERE t.is_ms_shipped = 0
AND (c.name = N'b_id' OR c.name = N'subid' OR c.name = N'father' OR c.name LIKE N'%%[_]id')
AND TYPE_NAME(c.user_type_id) <> N'bigint'
ORDER BY schema_name, object_name, c.column_id;
SELECT s.name AS schema_name, v.name AS view_name
FROM sys.views AS v
INNER JOIN sys.schemas AS s ON s.schema_id = v.schema_id
WHERE v.is_ms_shipped = 0 AND OBJECTPROPERTY(v.object_id, 'IsSchemaBound') IN (0, 1)
AND EXISTS (
SELECT 1 FROM sys.sql_expression_dependencies AS d
WHERE d.referencing_id = v.object_id AND d.referenced_id IS NULL
AND d.referenced_entity_name IS NOT NULL
)
ORDER BY s.name, v.name;
""".formatted(expectedTables, expectedViews);
Files.writeString(target, sql, StandardCharsets.UTF_8);
}
private static String quote(String identifier) {
return "[" + identifier.replace("]", "]]" ) + "]";
}
private static String escape(String value) {
return value.replace("'", "''");
}
private static String safeName(String value) {
return value.replaceAll("[^A-Za-z0-9_]", "_");
}
private static Set<String> lowerSet(String... values) {
Set<String> result = new HashSet<>();
for (String value : values) result.add(value.toLowerCase(Locale.ROOT));
return result;
}
}
@@ -0,0 +1,98 @@
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
/**
* One-off migration: simplifies s_power keys to bare usage permission codes.
* menu/module permission points drop the action suffix:
* module.s_i18n_type.read/.create/.update/.delete -> module.s_i18n_type
* b_action of the merged rows becomes 'access'; b_name is taken from s_module.b_name when available.
* Idempotent: old-style rows (b_type='module' with CRUD action) are merged only once.
*/
public final class SimplifyPowerKeys {
public static void main(String[] args) throws Exception {
Path apiRoot = Path.of("").toAbsolutePath().normalize();
Path configPath = apiRoot.resolve("config/dbconfigs/g3hd.properties").normalize();
Properties properties = new Properties();
try (InputStream input = Files.newInputStream(configPath)) {
properties.load(input);
}
try (Connection connection = DriverManager.getConnection(
properties.getProperty("url"),
properties.getProperty("username"),
properties.getProperty("password"))) {
if (!"FMS".equalsIgnoreCase(connection.getCatalog())) {
throw new IllegalStateException("Refusing to alter database: " + connection.getCatalog());
}
connection.setAutoCommit(false);
try {
List<String[]> oldRows = new ArrayList<>();
String findSql = "SELECT DISTINCT b_object_type, b_object_id FROM dbo.s_power "
+ "WHERE b_type = 'module' AND b_action <> 'access'";
try (Statement statement = connection.createStatement();
ResultSet rows = statement.executeQuery(findSql)) {
while (rows.next()) {
oldRows.add(new String[]{rows.getString(1), rows.getString(2)});
}
}
try (PreparedStatement delete = connection.prepareStatement(
"DELETE FROM dbo.s_power WHERE b_type = 'module' AND b_object_type = ? AND b_object_id = ?");
PreparedStatement insert = connection.prepareStatement(
"INSERT INTO dbo.s_power (b_id, b_name, b_i18n, b_type, b_object_type, b_object_id, b_action, b_canuse, b_xh) "
+ "VALUES (?, ?, NULL, 'module', ?, ?, 'access', 1, 0)")) {
for (String[] objectKey : oldRows) {
String objectType = objectKey[0];
String objectId = objectKey[1];
delete.setString(1, objectType);
delete.setString(2, objectId);
int removed = delete.executeUpdate();
String name = lookupModuleName(connection, objectId);
insert.setString(1, "module." + objectId);
insert.setString(2, name);
insert.setString(3, objectType);
insert.setString(4, objectId);
insert.executeUpdate();
System.out.println("MERGED|" + objectType + ":" + objectId
+ "|removed=" + removed + "|new=module." + objectId + "|name=" + name);
}
}
connection.commit();
System.out.println("COMMITTED|merged_objects=" + oldRows.size());
} catch (Exception exception) {
connection.rollback();
throw exception;
}
try (Statement statement = connection.createStatement();
ResultSet rows = statement.executeQuery(
"SELECT b_id, b_name, b_type, b_object_type, b_object_id, b_action, b_canuse "
+ "FROM dbo.s_power ORDER BY b_type, b_id")) {
while (rows.next()) {
System.out.println("ROW|" + rows.getString(1) + "|" + rows.getString(2)
+ "|type=" + rows.getString(3) + "|object=" + rows.getString(4) + ":" + rows.getString(5)
+ "|action=" + rows.getString(6) + "|canuse=" + rows.getInt(7));
}
}
}
}
private static String lookupModuleName(Connection connection, String moduleId) throws Exception {
try (PreparedStatement statement = connection.prepareStatement(
"SELECT b_name FROM dbo.s_module WHERE b_id = ?")) {
statement.setString(1, moduleId);
try (ResultSet rows = statement.executeQuery()) {
if (rows.next() && rows.getString(1) != null && !rows.getString(1).isBlank()) {
return rows.getString(1);
}
}
}
return moduleId;
}
}