diff --git a/app/database.py b/app/database.py index bffaac4..9ab9777 100644 --- a/app/database.py +++ b/app/database.py @@ -445,7 +445,10 @@ class DatabaseManager: mysql_type = self.TYPE_MAPPING.get(col_type, 'VARCHAR(255)') column_defs.append(f'`{col}` {mysql_type}') - sql = f"CREATE TABLE IF NOT EXISTS `{table_name}` ({', '.join(column_defs)}) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" + sql = ( + f"CREATE TABLE IF NOT EXISTS `{table_name}` ({', '.join(column_defs)}) " + "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ) with self.get_connection() as conn: with conn.cursor() as cursor: diff --git a/app/services/cell_data.py b/app/services/cell_data.py index 24eea67..484c964 100644 --- a/app/services/cell_data.py +++ b/app/services/cell_data.py @@ -396,7 +396,7 @@ class CellDataProcessor: `功率` varchar(100) DEFAULT NULL, `网络` varchar(20) DEFAULT NULL, KEY `CGI` (`CGI`) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci """ ) @@ -548,9 +548,17 @@ def copy_celldata_tables_to_capacity(app_config: AppConfig, logger=None) -> int: logger.info("CellData 数据库中没有表,跳过复制") return 0 + target_charset, target_collation = _database_charset_and_collation(cap_conn) copied = 0 for table in tables: - rows = _copy_one_table(cd_conn, cap_conn, table, logger) + rows = _copy_one_table( + cd_conn, + cap_conn, + table, + target_charset, + target_collation, + logger, + ) if rows >= 0: copied += 1 cap_conn.commit() @@ -571,7 +579,28 @@ def _list_tables(conn) -> list[str]: return [list(row.values())[0] for row in cur.fetchall()] -def _copy_one_table(src_conn, dst_conn, table: str, logger=None) -> int: +def _database_charset_and_collation(conn) -> tuple[str, str]: + with conn.cursor() as cur: + cur.execute( + "SELECT @@character_set_database AS `charset_name`, " + "@@collation_database AS `collation_name`" + ) + row = cur.fetchone() + charset = str(row["charset_name"]) + collation = str(row["collation_name"]) + if not re.fullmatch(r"[A-Za-z0-9_]+", charset) or not re.fullmatch(r"[A-Za-z0-9_]+", collation): + raise RuntimeError("目标数据库字符集或排序规则名称无效") + return charset, collation + + +def _copy_one_table( + src_conn, + dst_conn, + table: str, + target_charset: str, + target_collation: str, + logger=None, +) -> int: with src_conn.cursor() as cur: cur.execute(f"SHOW CREATE TABLE `{table}`") row = cur.fetchone() @@ -584,6 +613,10 @@ def _copy_one_table(src_conn, dst_conn, table: str, logger=None) -> int: with dst_conn.cursor() as cur: cur.execute(f"DROP TABLE IF EXISTS `{table}`") cur.execute(create_sql) + cur.execute( + f"ALTER TABLE `{table}` CONVERT TO CHARACTER SET {target_charset} " + f"COLLATE {target_collation}" + ) if count == 0: if logger: diff --git a/db_init/celldata.cellinfo.sql b/db_init/celldata.cellinfo.sql index 683ecdf..0f03ba8 100644 --- a/db_init/celldata.cellinfo.sql +++ b/db_init/celldata.cellinfo.sql @@ -14,4 +14,4 @@ CREATE TABLE IF NOT EXISTS `cellinfo` ( `功率` varchar(100) DEFAULT NULL, `网络` varchar(20) DEFAULT NULL, KEY `CGI` (`CGI`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/db_init/celldata.sector.sql b/db_init/celldata.sector.sql index e133f87..a32baec 100644 --- a/db_init/celldata.sector.sql +++ b/db_init/celldata.sector.sql @@ -11,4 +11,4 @@ CREATE TABLE IF NOT EXISTS `sector` ( `站型` varchar(50) DEFAULT NULL, `网络` varchar(20) DEFAULT NULL, KEY `CGI` (`CGI`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/db_init/celldata.sector_band_ref.sql b/db_init/celldata.sector_band_ref.sql index cfe3791..3573976 100644 --- a/db_init/celldata.sector_band_ref.sql +++ b/db_init/celldata.sector_band_ref.sql @@ -9,7 +9,7 @@ CREATE TABLE IF NOT EXISTS `sector_band_ref` ( `PLMN` varchar(16) DEFAULT NULL, `制式` varchar(20) DEFAULT NULL, `频段` varchar(20) DEFAULT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; INSERT INTO `sector_band_ref` (`网络`,`频点下限`,`频点上限`,`PLMN`,`制式`,`频段`) VALUES ('5G',4000,99999,NULL,'4.9G','4.9G'), diff --git a/docs/project_context.md b/docs/project_context.md index 3852cad..9eff29a 100644 --- a/docs/project_context.md +++ b/docs/project_context.md @@ -1112,3 +1112,10 @@ CellData 处理日志细化(`app/services/cell_data.py`): - 全脚本审计覆盖 32 个表标识:配置输入、脚本内建表和线上实际表之间无剩余大小写错配/冲突,且按 157 条语句执行顺序检查无“表尚未创建便引用”和未加反引号的表名。 - 修复镜像 `capacityrepost-web:c054b48` 已部署并重启 Web;MySQL 和旧实例未重启。线上以同一数据库执行第 126、127 条核心查询通过,移动/广电临时校验结果分别为 23,227/23,107 行,临时表已删除。 - 自动调度保持启用,正式任务 `20260806_170846` 已启动;因完整处理耗时较长,后续结果由用户自行观察。 + +## 2026-08-06:统一 CellData 与容量库排序规则 + +- 第 136 条 SQL 的 1267 错误来自跨表 CGI 关联:容量结果表使用 `utf8mb4_unicode_ci`,CellData 复制来的 `sector/cellinfo` 保留源表 `utf8mb4_0900_ai_ci`;第 136-139 条四个 CGI/NCGI 关联均有同类风险。 +- `copy_celldata_tables_to_capacity` 在目标空表创建后、插入前读取目标数据库的 `@@character_set_database/@@collation_database` 并执行 `ALTER TABLE ... CONVERT`,复制表不再继承不兼容的源排序规则;字符集和排序规则名称限定为字母数字下划线。 +- `CellDataProcessor`、`DatabaseManager` 和三份 `db_init` 建表 SQL 均显式使用 `utf8mb4_unicode_ci`,避免 MySQL 8 在只写 `DEFAULT CHARSET=utf8mb4` 时回落到 `utf8mb4_0900_ai_ci`。 +- 线上 `celldata` 与 `CapacityReport` 两库的 `sector/cellinfo` 已迁移为 `utf8mb4_unicode_ci`;4G/5G 结果表分别关联 sector/cellinfo 的四组真实查询均通过。自动重试任务 `20260806_180436` 保持运行,未重启容器。