ApplyGarbledDbRepairAll.java 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. import java.nio.charset.StandardCharsets;
  2. import java.nio.file.Files;
  3. import java.nio.file.Path;
  4. import java.nio.file.Paths;
  5. import java.sql.Connection;
  6. import java.sql.DriverManager;
  7. import java.sql.PreparedStatement;
  8. import java.sql.ResultSet;
  9. import java.sql.SQLException;
  10. import java.sql.Statement;
  11. import java.util.ArrayList;
  12. import java.util.List;
  13. /**
  14. * Apply garbled-text repair SQL to master + all active tenant DBs.
  15. * Usage: java -cp mysql-connector-j.jar;. ApplyGarbledDbRepairAll
  16. */
  17. public class ApplyGarbledDbRepairAll {
  18. private static final String MASTER_HOST = getenv("DB_HOST", "cq-cdb-8fjmemkb.sql.tencentcdb.com");
  19. private static final int MASTER_PORT = Integer.parseInt(getenv("DB_PORT", "27220"));
  20. private static final String MASTER_USER = getenv("DB_USER", "root");
  21. private static final String MASTER_PASSWORD = getenv("DB_PASSWORD", "Ylrz_1q2w3e4r5t6y");
  22. private static final String MASTER_DB = getenv("DB_NAME", "ylrz_saas");
  23. private static final Path JAVA_ROOT = Paths.get(System.getProperty("user.dir")).getParent();
  24. private static final Path[] TENANT_SQL = {
  25. JAVA_ROOT.resolve("fs-agent/src/main/resources/db/migration/tenant/V20260724_01__lobster_industry_corpus_garbled_repair.sql"),
  26. JAVA_ROOT.resolve("fs-agent/src/main/resources/db/migration/tenant/V20260724_02__billing_and_menu_comment_repair.sql"),
  27. };
  28. private static final Path[] MASTER_SQL = {
  29. JAVA_ROOT.resolve("sql/master/V20260724_01__lobster_industry_corpus_garbled_repair.sql"),
  30. JAVA_ROOT.resolve("sql/fix_tenant_sys_menu_other_parent.sql"),
  31. };
  32. public static void main(String[] args) throws Exception {
  33. Class.forName("com.mysql.cj.jdbc.Driver");
  34. System.out.println("=== Garbled DB repair (master + tenants) ===");
  35. try (Connection master = open(MASTER_HOST, MASTER_PORT, MASTER_DB, MASTER_USER, MASTER_PASSWORD)) {
  36. for (Path p : MASTER_SQL) {
  37. applyFile(master, "[MASTER]", p);
  38. }
  39. verifyMasterMenus(master);
  40. List<Long> tenantIds = loadActiveTenantIds(master);
  41. System.out.println("\nActive tenants: " + tenantIds.size());
  42. int ok = 0;
  43. int fail = 0;
  44. for (Long tenantId : tenantIds) {
  45. TenantDb tenant = loadTenant(master, tenantId);
  46. if (tenant == null) {
  47. System.out.println("[SKIP] tenant " + tenantId);
  48. continue;
  49. }
  50. System.out.println("\n[TENANT " + tenantId + "] " + tenant.database);
  51. try (Connection conn = open(tenant.host, tenant.port, tenant.database, tenant.user, tenant.password)) {
  52. for (Path p : TENANT_SQL) {
  53. applyFile(conn, " ", p);
  54. }
  55. verifyTenant(conn);
  56. ok++;
  57. } catch (Exception e) {
  58. System.out.println(" [ERR] " + e.getMessage());
  59. e.printStackTrace(System.out);
  60. fail++;
  61. }
  62. }
  63. System.out.println("\n=== SUMMARY ===");
  64. System.out.println("Tenants OK: " + ok + ", FAIL: " + fail);
  65. if (fail > 0) {
  66. System.exit(1);
  67. }
  68. }
  69. }
  70. private static void applyFile(Connection conn, String prefix, Path path) throws Exception {
  71. if (!Files.exists(path)) {
  72. System.out.println(prefix + "[SKIP missing] " + path.getFileName());
  73. return;
  74. }
  75. String sql = stripLineComments(readUtf8(path));
  76. System.out.println(prefix + "[APPLY] " + path.getFileName());
  77. for (String stmt : splitStatements(sql)) {
  78. if (stmt.isBlank()) {
  79. continue;
  80. }
  81. try (Statement st = conn.createStatement()) {
  82. st.execute(stmt);
  83. } catch (SQLException e) {
  84. String msg = e.getMessage() != null ? e.getMessage() : "";
  85. if (msg.contains("Unknown column") || msg.contains("doesn't exist")) {
  86. System.out.println(prefix + "[SKIP stmt] " + truncate(stmt) + " -> " + msg);
  87. } else {
  88. throw e;
  89. }
  90. }
  91. }
  92. }
  93. private static String truncate(String stmt) {
  94. String oneLine = stmt.replaceAll("\\s+", " ").trim();
  95. return oneLine.length() > 80 ? oneLine.substring(0, 80) + "..." : oneLine;
  96. }
  97. private static List<String> splitStatements(String sql) {
  98. List<String> list = new ArrayList<>();
  99. StringBuilder sb = new StringBuilder();
  100. boolean inSingle = false;
  101. for (int i = 0; i < sql.length(); i++) {
  102. char c = sql.charAt(i);
  103. if (c == '\'') {
  104. inSingle = !inSingle;
  105. sb.append(c);
  106. continue;
  107. }
  108. if (c == ';' && !inSingle) {
  109. list.add(sb.toString().trim());
  110. sb.setLength(0);
  111. continue;
  112. }
  113. sb.append(c);
  114. }
  115. if (sb.length() > 0) {
  116. list.add(sb.toString().trim());
  117. }
  118. return list;
  119. }
  120. private static void verifyMasterMenus(Connection conn) throws SQLException {
  121. try (Statement st = conn.createStatement();
  122. ResultSet rs = st.executeQuery(
  123. "SELECT menu_id, menu_name FROM tenant_sys_menu WHERE menu_id IN (3008,35300) ORDER BY menu_id")) {
  124. System.out.println("\n[MASTER verify tenant_sys_menu]");
  125. while (rs.next()) {
  126. System.out.println(" " + rs.getLong(1) + " = " + rs.getString(2));
  127. }
  128. }
  129. }
  130. private static void verifyTenant(Connection conn) throws SQLException {
  131. int badMenu = scalar(conn,
  132. "SELECT COUNT(*) FROM sys_menu WHERE menu_name LIKE '%?%' OR HEX(menu_name) LIKE '3F3F%'");
  133. System.out.println(" garbled sys_menu=" + badMenu);
  134. if (!columnExists(conn, "lobster_sales_corpus", "customer_question")) {
  135. System.out.println(" garbled industry_seed=skipped(no corpus columns)");
  136. return;
  137. }
  138. int badCorpus = scalar(conn,
  139. "SELECT COUNT(*) FROM lobster_sales_corpus WHERE source_type='industry_seed' "
  140. + "AND (customer_question LIKE '%?%' OR sales_answer LIKE '%?%')");
  141. System.out.println(" garbled industry_seed=" + badCorpus);
  142. }
  143. private static boolean columnExists(Connection conn, String table, String column) throws SQLException {
  144. try (ResultSet rs = conn.getMetaData().getColumns(conn.getCatalog(), null, table, column)) {
  145. return rs.next();
  146. }
  147. }
  148. private static int scalar(Connection conn, String sql) throws SQLException {
  149. try (Statement st = conn.createStatement(); ResultSet rs = st.executeQuery(sql)) {
  150. rs.next();
  151. return rs.getInt(1);
  152. }
  153. }
  154. private static String stripLineComments(String sql) {
  155. StringBuilder out = new StringBuilder();
  156. for (String line : sql.split("\\R")) {
  157. int idx = line.indexOf("--");
  158. if (idx >= 0) {
  159. line = line.substring(0, idx);
  160. }
  161. out.append(line).append('\n');
  162. }
  163. return out.toString();
  164. }
  165. private static String readUtf8(Path path) throws Exception {
  166. byte[] bytes = Files.readAllBytes(path);
  167. String sql = new String(bytes, StandardCharsets.UTF_8);
  168. if (!sql.isEmpty() && sql.charAt(0) == '\uFEFF') {
  169. sql = sql.substring(1);
  170. }
  171. return sql;
  172. }
  173. private static List<Long> loadActiveTenantIds(Connection master) throws SQLException {
  174. List<Long> ids = new ArrayList<>();
  175. try (Statement st = master.createStatement();
  176. ResultSet rs = st.executeQuery("SELECT id FROM tenant_info WHERE status = 1 ORDER BY id")) {
  177. while (rs.next()) {
  178. ids.add(rs.getLong(1));
  179. }
  180. }
  181. return ids;
  182. }
  183. private static TenantDb loadTenant(Connection master, long tenantId) throws SQLException {
  184. try (PreparedStatement ps = master.prepareStatement(
  185. "SELECT db_url, db_account, db_pwd FROM tenant_info WHERE id = ? AND status = 1 LIMIT 1")) {
  186. ps.setLong(1, tenantId);
  187. try (ResultSet rs = ps.executeQuery()) {
  188. if (!rs.next()) {
  189. return null;
  190. }
  191. TenantDb t = new TenantDb();
  192. String url = rs.getString("db_url");
  193. String body = url.substring("jdbc:mysql://".length());
  194. int slash = body.indexOf('/');
  195. String hostPort = body.substring(0, slash);
  196. String db = body.substring(slash + 1).replaceAll("\\?.*", "");
  197. int colon = hostPort.indexOf(':');
  198. t.host = colon >= 0 ? hostPort.substring(0, colon) : hostPort;
  199. t.port = colon >= 0 ? Integer.parseInt(hostPort.substring(colon + 1)) : 3306;
  200. t.database = db;
  201. t.user = rs.getString("db_account");
  202. t.password = rs.getString("db_pwd");
  203. return t;
  204. }
  205. }
  206. }
  207. private static Connection open(String host, int port, String db, String user, String password)
  208. throws SQLException {
  209. return DriverManager.getConnection(
  210. "jdbc:mysql://" + host + ":" + port + "/" + db
  211. + "?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&allowMultiQueries=true",
  212. user, password);
  213. }
  214. private static String getenv(String key, String def) {
  215. String v = System.getenv(key);
  216. return v != null && !v.isBlank() ? v : def;
  217. }
  218. private static final class TenantDb {
  219. String host;
  220. int port;
  221. String database;
  222. String user;
  223. String password;
  224. }
  225. }