Files
zTools2/app/database.py
zikai 9af28f41b4 refactor: 清理死代码/提前失败/日志/高内聚低耦合
死代码移除:
- whiteboard_hub.py: 移除未引用的 reset_hub 单例重置函数
- tunnel_service.py: 移除未引用的 is_port_allowed (逻辑已在 sftp_server 内联)
- tunnel_session_dao.py: 移除未引用的 get_active_by_port
- pdf_job_dao.py: 移除未用 datetime 导入
- pdf_converter.py: 移除未用 shutil 导入
- pdf_service.py: 移除未用 PdfSubmitResponse 导入 + _do_convert 内未用 hashlib 导入
- upload_html.py: 移除未用 escape 导入 (JS 侧自有 escapeHtml)
- pdf_controller.py: 移除 _resolve_cookie 内未用 cfg 局部变量

提前失败/分层修复:
- database.py init_db_schema: 建表后用 inspector 校验既有表列与模型一致,
  缺列即抛 RuntimeError (fail-fast on schema drift), 避免运行期才暴露
- whiteboard_dao.get_or_create: 仅 IntegrityError 才回滚重读, 其他异常向上抛
  (原 except Exception 会掩盖 schema/连接等真实故障)
- pdf_service.admin_delete/_safe_delete_file: 改用 PdfJobDAO.delete /
  UploadedFileDAO.delete, 不再直接操作 job_dao.db / file_dao.db (修复分层契约:
  DAO 头注释声明 service 不直接操作 session)
- PdfJobDAO 新增 delete(job) 方法

日志补全 (8 处 silent catch):
- whiteboard_hub.py disconnect/close_board 关闭 ws: logger.debug
- whiteboard_controller _safe_send/_safe_close: logger.debug
- sftp_server _close_tunnel_dao/读用户名: logger.debug
- sftp_server validate_public_key: logger.warning (auth 路径, 避免静默失败)

文档:
- 新增 docs/routes.md, docs/configuration.md, docs/error-handling.md
- README.md 精简为简介/结构/外部依赖/apache2 配置/Ubuntu 安装/docs 链接
2026-07-28 11:34:35 +08:00

93 lines
2.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""数据库引擎、Session 与 Declarative Base。
引擎采用懒初始化:首次访问 `get_engine()` 时才创建连接池,
避免 import 时的副作用,并支持 `dispose()` 后重新加载配置。
"""
from __future__ import annotations
from collections.abc import Generator
from sqlalchemy import create_engine
from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
from .config import get_settings
_engine: create_engine | None = None
_session_local: sessionmaker | None = None
def get_engine() -> create_engine:
"""返回全局 Engine 实例;首次调用时懒创建。"""
global _engine, _session_local
if _engine is None:
s = get_settings()
_engine = create_engine(
s.db_url(),
pool_pre_ping=True,
pool_size=s.database.pool_size,
pool_recycle=s.database.pool_recycle,
future=True,
)
_session_local = sessionmaker(
bind=_engine, autoflush=False, autocommit=False, future=True,
)
return _engine
def dispose_engine() -> None:
"""关闭连接池并清除缓存,下次访问时重新创建(配合 reload_settings 使用)。"""
global _engine, _session_local
if _engine is not None:
_engine.dispose()
_engine = None
_session_local = None
def get_session_local() -> sessionmaker:
"""返回全局 SessionLocal确保 Engine 已初始化。"""
get_engine()
return _session_local # type: ignore[return-value]
class Base(DeclarativeBase):
pass
def get_db() -> Generator[Session, None, None]:
"""FastAPI 依赖:为每个请求产出一个 Session。"""
db = get_session_local()()
try:
yield db
finally:
db.close()
def init_db_schema() -> None:
"""按需建表幂等并校验既有表列与模型一致fail-fast on schema drift
先导入 models 注册映射create_all 用 IF NOT EXISTS 仅补缺失的表;
随后对每张已存在的表检查模型声明的列是否齐全,缺列即抛 RuntimeError
避免运行期才以晦涩的 OperationalError 暴露 schema 漂移。
"""
from sqlalchemy import inspect
from . import models # noqa: F401
engine = get_engine()
Base.metadata.create_all(bind=engine)
inspector = inspect(engine)
missing: list[str] = []
for table, mapper in Base.registry.mappers.items():
if not inspector.has_table(table):
continue
db_cols = {c["name"] for c in inspector.get_columns(table)}
for model_col in mapper.columns.keys():
if model_col not in db_cols:
missing.append(f"{table}.{model_col}")
if missing:
raise RuntimeError(
"数据库 schema 与模型不一致,缺少列: " + ", ".join(missing)
+ "。请执行 sql/schema.sql 或迁移脚本更新表结构。"
)