From 14252e9dc51a06ee8eed6e1c5f3286f102ddcf71 Mon Sep 17 00:00:00 2001 From: Nixevol Date: Fri, 26 Jun 2026 17:26:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=89=8D=E7=BD=AE=E6=A3=80=E6=9F=A5?= =?UTF-8?q?=E8=A1=A5=E5=85=A8=E5=BA=93=E4=B8=8D=E5=AD=98=E5=9C=A8=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=88=9B=E5=BB=BA=EF=BC=88CREATE=20DATABASE=EF=BC=89?= =?UTF-8?q?=EF=BC=8C=E4=B8=8D=E5=86=8D=E4=BE=9D=E8=B5=96=E5=BA=93=E5=B7=B2?= =?UTF-8?q?=E5=AD=98=E5=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/db_init.py | 50 ++++++++++++++++++++++++++++++++++++----- db_init/README.md | 8 ++++--- docs/project_context.md | 7 ++++++ 3 files changed, 57 insertions(+), 8 deletions(-) diff --git a/app/db_init.py b/app/db_init.py index a8e9cab..6a214d4 100644 --- a/app/db_init.py +++ b/app/db_init.py @@ -1,12 +1,13 @@ -"""数据库前置检查:确保各库「必须存在的结构表」已建好,缺表则按 db_init/ 下的初始化 SQL 自动创建。 +"""数据库前置检查:先确保各「库」存在(不存在则自动创建),再确保各库「必须存在的结构表」已建好。 约定(见 db_init/README.md): -- 初始化 SQL 放在 BASE_DIR/db_init/,文件名形如 ``<库标识>.<表名>.sql``。 +- 建库:对 celldata 与 capacityreport(仅直连 MySQL 时) 两个目标库执行 CREATE DATABASE IF NOT EXISTS, + 库名以用户配置为准;连接时若库不存在不会报错(先用不带 database 的连接建库)。 +- 建表:初始化 SQL 放在 BASE_DIR/db_init/,文件名形如 ``<库标识>.<表名>.sql``,仅当目标表「不存在」时执行, + 因此 sector_band_ref 这类带预设数据的表只在首次创建时写入,绝不覆盖用户自定义。 - 库标识 -> 实际连接(库名以用户配置为准,不写死): celldata -> AppConfig.cell_data.mysql(CellData 库,始终直连 MySQL) capacityreport -> AppConfig.mysql(主仓库,仅当 warehouse_type == 'mysql' 直连时检查) -- 仅当目标表「不存在」时执行对应 SQL;已存在则跳过整文件, - 因此 sector_band_ref 这类带预设数据的表只在首次创建时写入,绝不覆盖用户自定义。 - 全过程 best-effort:单个库/单张表失败只记录日志并继续,不阻断启动或处理流程。 """ from __future__ import annotations @@ -20,6 +21,9 @@ from app.config import BASE_DIR, AppConfig, MySQLConfig DB_INIT_DIR = BASE_DIR / "db_init" +# 需要确保存在的目标库(建库不依赖是否有建表 SQL;主库表为动态生成,故无建表文件但仍需建库) +DB_KEYS = ("celldata", "capacityreport") + def _log(logger, message: str) -> None: if logger is not None: @@ -42,6 +46,40 @@ def _target_mysql(app_config: AppConfig, db_key: str) -> Optional[MySQLConfig]: return None +def _ensure_database(mysql: MySQLConfig) -> None: + """库不存在则创建(用不带 database 的连接执行 CREATE DATABASE IF NOT EXISTS)。""" + safe_name = str(mysql.dbname).replace("`", "") + conn = pymysql.connect( + host=mysql.host, + port=mysql.port, + user=mysql.user, + password=mysql.passwd, + charset="utf8mb4", + cursorclass=pymysql.cursors.Cursor, + autocommit=True, + ) + try: + with conn.cursor() as cursor: + cursor.execute( + f"CREATE DATABASE IF NOT EXISTS `{safe_name}` " + f"CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci" + ) + finally: + conn.close() + + +def _ensure_databases(app_config: AppConfig, logger=None) -> None: + """确保所有目标库存在(best-effort)。""" + for db_key in DB_KEYS: + mysql = _target_mysql(app_config, db_key) + if mysql is None or not mysql.dbname: + continue + try: + _ensure_database(mysql) + except Exception as exc: # noqa: BLE001 + _log(logger, f"[前置检查] 确保库 {db_key}({mysql.dbname}@{mysql.host}:{mysql.port}) 失败:{exc}") + + def _connect(mysql: MySQLConfig): return pymysql.connect( host=mysql.host, @@ -86,7 +124,9 @@ def _discover() -> dict[str, list[tuple[str, Path]]]: def ensure_required_tables(app_config: AppConfig, logger=None) -> None: - """检查各库必须存在的表,缺则按初始化 SQL 建好。失败不抛出(仅记录日志)。""" + """先确保各库存在(不存在则创建),再检查各库必须存在的表,缺则按初始化 SQL 建好。失败不抛出。""" + _ensure_databases(app_config, logger) + groups = _discover() for db_key, items in groups.items(): mysql = _target_mysql(app_config, db_key) diff --git a/db_init/README.md b/db_init/README.md index b38eb0c..c8663a2 100644 --- a/db_init/README.md +++ b/db_init/README.md @@ -2,7 +2,8 @@ 本目录存放各数据库「**必须存在的结构表**」的初始化 SQL。程序启动时、以及每次执行 CellData 脚本前,会由 `app/db_init.py` 的 `ensure_required_tables()` 自动检查: -**某张必需表不存在时,就执行对应的初始化 SQL 把它建好**,从而避免运行过程中因缺表报错。 +**① 库(schema)不存在时自动创建**(celldata 与 capacityreport,库名以配置为准); +**② 某张必需表不存在时,执行对应的初始化 SQL 把它建好**,从而避免运行过程中因缺库/缺表报错。 ## 文件命名规则 @@ -26,7 +27,8 @@ CellData 脚本前,会由 `app/db_init.py` 的 `ensure_required_tables()` 自 - 因此带预设数据的表(如 `sector_band_ref`)**只在首次创建时写入预设**,之后你在库里 对它的任何增改都会被保留,不会被覆盖或重置。 - 全过程 best-effort:单库连不上 / 单表初始化失败只记日志,不会中断启动或数据处理。 -- 前提:**数据库本身需已存在**(本机制只建表,不建库)。 +- **建库**:连接前先用不带 database 的连接执行 `CREATE DATABASE IF NOT EXISTS`,故库不存在也能自愈; + 前提是 MySQL 账号有建库权限(root 默认有)。Metrix 模式下主库走平台 API,不在此建库。 ## 当前清单 @@ -38,7 +40,7 @@ CellData 脚本前,会由 `app/db_init.py` 的 `ensure_required_tables()` 自 | `celldata.sector_band_ref.sql` | `sector_band_ref` | 频段特征库(频点区间+PLMN→制式/频段),含预设 10 条规则,供逆推用,可自行增改。 | ### capacityreport(主仓库) -当前**无需前置建表**:主仓库的原始表(`4G_UD`/`5G_UD`)、结果表(`4G_结果表`/`5G_结果表`) +**会自动建库**(直连 MySQL 模式下),但**无需前置建表**:主仓库的原始表(`4G_UD`/`5G_UD`)、结果表(`4G_结果表`/`5G_结果表`) 以及从 CellData 复制过来的 `sector`/`cellinfo`,都由导入流程 / `ReportScript.sql` / 跨库复制以 `DROP TABLE IF EXISTS` + `CREATE` 动态生成,结构随源数据字段而定, 不宜在此预先固定结构(会与动态建表冲突)。如确有需要,按上面的命名规则新增 diff --git a/docs/project_context.md b/docs/project_context.md index 0f85566..cd04df2 100644 --- a/docs/project_context.md +++ b/docs/project_context.md @@ -1024,6 +1024,13 @@ CellData 处理日志细化(`app/services/cell_data.py`): - 准确率(MCP 实测,重叠 66001):网络 100%、制式 97.88%(排除 3DMM 即 100%)、频段 97.6%、带宽 99.75%、站型 99.94%、**物理站 99.59%、扇区 99.20%**;残差为不可还原的人工差异(室分多载波编号、700M 个别人工编号、源数据制表符等)。详见 `docs/sector_inference_research.md`。 - 关键坑:MySQL 字符串字面量会吞掉未知转义的反斜杠,正则字面量需写 `\\(`(.sql 文件)/ JSON 调用需 `\\\\(`;ICU 正则字符类内的 `[ ]` 易误判,统一把名称中的 `[]` 转 `()` 后只处理圆括号。 +## 2026-06-26:前置检查补全「库不存在自动创建」 + +- 问题:原 db_init 只建表不建库,库(schema)不存在时连接 `database=dbname` 直接失败,库始终建不出来。 +- 修复:`app/db_init.py` 新增 `_ensure_database`(用**不带 database** 的连接执行 `CREATE DATABASE IF NOT EXISTS \`库\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`)与 `_ensure_databases`(遍历 `DB_KEYS=(celldata, capacityreport)`,主库仅直连 MySQL 模式);`ensure_required_tables` 开头先建库再建表。建库走独立库清单**不依赖建表 SQL**(主库无建表文件但仍需建库)。库名以配置为准、反引号去冲突;best-effort 不阻断。 +- 调用链不变:main 启动 lifespan + execute_celldata_script 前均会触发,故启动/手动上传/容量处理前库都会自愈。需 MySQL 账号有建库权限(root 默认有);Metrix 模式主库走平台不在此建。 +- 校验:py_compile 通过;MCP 验证 `CREATE DATABASE IF NOT EXISTS` 语法/权限有效(建临时库→确认→删除)。db_init/README.md 同步更正(原写"只建表不建库")。 + ## 2026-06-26:修复看板问题清单固定列重叠(透明背景透出下层列) - 现象:问题小区清单横向滚动时,右固定列「问题」与滚动区「日均流量」列视觉重叠。