Compare commits
2 Commits
021bf311c1
...
aaaac95b53
| Author | SHA1 | Date | |
|---|---|---|---|
| aaaac95b53 | |||
| 4a5dc61a02 |
@@ -35,11 +35,15 @@ async def _run_alembic_migrations() -> None:
|
||||
- 既有但无 alembic_version(历史 DB):先 stamp head 基线,再 upgrade(no-op)
|
||||
"""
|
||||
async with db_manager.engine.begin() as conn:
|
||||
has_alembic = await conn.execute(text("SELECT to_regclass('public.alembic_version')")).scalar()
|
||||
# 注意括号:await 优先级低于属性访问,await conn.execute(...).scalar()
|
||||
# 实际是 await (conn.execute(...).scalar())——会在协程对象上调 .scalar()
|
||||
# 直接 AttributeError。必须 (await conn.execute(...)).scalar()。
|
||||
# 曾因漏括号让启动迁移自诞生起一次都没跑通过(2026-09-26 事故根因)。
|
||||
has_alembic = (await conn.execute(text("SELECT to_regclass('public.alembic_version')"))).scalar()
|
||||
if not has_alembic:
|
||||
table_count = await conn.execute(
|
||||
table_count = (await conn.execute(
|
||||
text("SELECT count(*) FROM information_schema.tables WHERE table_schema='public' AND table_name <> 'alembic_version'")
|
||||
).scalar()
|
||||
)).scalar()
|
||||
if table_count and table_count > 0:
|
||||
logger.info("检测到既有 DB 未纳入 alembic 管理,自动 stamp head 作为基线")
|
||||
await asyncio.to_thread(_alembic_stamp_head)
|
||||
@@ -184,6 +188,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:
|
||||
@@ -196,6 +245,12 @@ async def init_database(keep_connected: bool = True):
|
||||
"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)
|
||||
if perm_map is None:
|
||||
@@ -223,7 +278,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:
|
||||
|
||||
Reference in New Issue
Block a user