From 4a5dc61a02974bf920e89fd076570ba0fb98f3a3 Mon Sep 17 00:00:00 2001 From: chenjw28 <792430652@qq.com> Date: Sat, 26 Sep 2026 23:11:30 +0800 Subject: [PATCH] =?UTF-8?q?fix(init=5Fdb):=20=E5=90=AF=E5=8A=A8=E8=BF=81?= =?UTF-8?q?=E7=A7=BB=E5=A4=B1=E8=B4=A5=E4=B8=8D=E5=86=8D=E9=9D=99=E9=BB=98?= =?UTF-8?q?=E2=80=94=E2=80=94logger.exception=20=E5=B8=A6=20traceback=20+?= =?UTF-8?q?=20schema=20=E8=A6=86=E7=9B=96=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 2026-09-26 事故复盘:AUTO_MIGRATE=true 但启动迁移在 stamp 之前就抛异常, except 里 logger.error(f"{e}") 丢掉了 traceback,docker logs 只剩一句 "数据库初始化失败",prod 缺 stp_files.product_id 等列导致上传全挂, 无从定位。两个修复: - 初始化异常改用 logger.exception,完整 traceback 落日志 - 新增启动期 _verify_schema_coverage:模型声明的 32 张表全部列必须真实 存在于 DB,缺失即 error 点名(只查缺不查多,遗留列不报);迁移后自动 执行,只读、自身异常不影响启动 已对生产库实跑验证:32 张表全部通过。 Co-Authored-By: Claude Code --- src/shared/database/init_db.py | 55 +++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/src/shared/database/init_db.py b/src/shared/database/init_db.py index 9bd5126..c0c9a4c 100644 --- a/src/shared/database/init_db.py +++ b/src/shared/database/init_db.py @@ -184,6 +184,51 @@ async def create_admin_user(session): logger.info(f"创建了管理员账户: {settings.ADMIN_USERNAME}") +async def _verify_schema_coverage() -> None: + """启动期 schema 校验:模型声明的表/列必须真实存在于 DB。 + + 背景(2026-09-26 事故):AUTO_MIGRATE 启动迁移曾静默失败,prod 库缺 + stp_files.product_id 等列,上传全挂两天无启动日志线索。此校验保证 + 『迁移被跳过 / stamp 基线掩盖未应用增量』这类漂移在启动时即被点名。 + + 只查缺、不查多:DB 里的遗留列/表(如 users.is_superuser)是历史产物,不管。 + 发现缺失只记 error 不抛——服务照常起,但 docker logs 必有醒目线索。 + """ + import moldinsight.models # noqa: F401 + import inventory.models # noqa: F401 + from shared.models.base import Base + + async with db_manager.engine.connect() as conn: + rows = (await conn.execute(text( + "SELECT table_name, column_name FROM information_schema.columns " + "WHERE table_schema='public'" + ))).all() + db_cols: dict = {} + for tn, cn in rows: + db_cols.setdefault(tn, set()).add(cn) + + missing = [] + for tname, table in Base.metadata.tables.items(): + present = db_cols.get(tname) + if present is None: + missing.append(f"整表缺失: {tname}") + continue + absent = {c.name for c in table.columns} - present + if absent: + missing.append(f"{tname} 缺列: {sorted(absent)}") + + if missing: + logger.error( + "schema 校验失败:模型声明了 %d 处 DB 缺失(迁移链与实际 schema 不一致," + "请人工执行 alembic upgrade head 或核对基线):%s", + len(missing), "; ".join(missing), + ) + else: + logger.info( + "schema 校验通过:模型 %d 张表的全部列均存在于 DB", len(Base.metadata.tables) + ) + + async def init_database(keep_connected: bool = True): """初始化数据库""" try: @@ -195,6 +240,12 @@ async def init_database(keep_connected: bool = True): "AUTO_MIGRATE=false:跳过启动期 alembic 迁移," "schema 由部署流程单点执行(alembic CLI 或 python -m shared.database.init_db)" ) + + # 迁移后校验(只读、不抛):迁移链与实际 schema 脱节时在启动日志直接点名 + try: + await _verify_schema_coverage() + except Exception: + logger.exception("schema 校验自身异常(不影响启动流程)") async with db_manager.session() as session: perm_map = await init_permissions(session) @@ -223,7 +274,9 @@ async def init_database(keep_connected: bool = True): return True except Exception as e: - logger.error(f"数据库初始化失败: {e}") + # logger.exception 而非 error(f"{e}"):吞掉 traceback 曾让启动迁移静默 + # 失败两天无从排查(2026-09-26 事故) + logger.exception(f"数据库初始化失败: {e}") print(f"数据库初始化失败: {e}") return False finally: