certflow.main 源代码

# src/certflow/main.py
"""CertFlow 主入口

桌面应用启动入口:初始化日志、数据库管理器、状态推断与主窗口,
并进入 Qt 事件循环。
"""

from __future__ import annotations

import os
import subprocess
import sys
from pathlib import Path

from dotenv import load_dotenv
from loguru import logger
from PySide6 import QtCore
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QApplication

from certflow.config.settings import cfg
from certflow.controllers.base_controller import BaseController
from certflow.utils.database import DatabaseManager, NutstoreWebDAVConfig
from certflow.utils.logger import setup_logger
from certflow.views.main_window import MainWindow


[文档] def setup_webdav_config() -> NutstoreWebDAVConfig | None: """从环境变量加载WebDAV配置 Returns: NutstoreWebDAVConfig | None: 当环境变量 ``NUTSTORE_WEBDAV_URL``、 ``NUTSTORE_WEBDAV_USER``、``NUTSTORE_WEBDAV_PASSWORD`` 均存在时返回 配置对象,否则返回 None。 """ load_dotenv() # 确保加载.env文件 url = os.getenv("NUTSTORE_WEBDAV_URL") username = os.getenv("NUTSTORE_WEBDAV_USER") password = os.getenv("NUTSTORE_WEBDAV_PASSWORD") if all([url, username, password]): from certflow.utils.database import NutstoreWebDAVConfig logger.info("检测到WebDAV配置,将启用远程同步功能") return NutstoreWebDAVConfig( url=url, username=username, password=password, remote_path=os.getenv("NUTSTORE_REMOTE_PATH", "CertFlow/backups"), keep_backups=int(os.getenv("NUTSTORE_KEEP_BACKUPS", "10")), ) return None
def _maybe_seed_base_data(db_manager: DatabaseManager) -> None: """若基础字典底座(材质/型号压力/口径)为空,则自动播种。 仅在新空白数据库场景下触发(业务数据缺失不影响,步骤均 upsert)。 播种失败不阻断应用启动;详细录入效果可通过 VALVE_SEED_DEBUG / VALVE_DEBUG 环境变量开启(subprocess 输出与最终计数会写入日志)。 设计要点:型号参数字典只播种「型号→压力」,标准号留空——标准号以合格证 打印指令回填最准确,底座不应写入可能不准的通用标准号(避免导入期标黄误报)。 """ from certflow.models.caliber_mapping import CaliberMapping from certflow.models.material_grade import MaterialGrade from certflow.models.model_param_mapping import ModelParamMapping session = db_manager.get_session() try: base_empty = ( session.query(MaterialGrade).count() == 0 or session.query(ModelParamMapping).count() == 0 or session.query(CaliberMapping).count() == 0 ) finally: session.close() if not base_empty: logger.info("基础字典底座已存在,跳过自动播种") return debug = bool(os.getenv("VALVE_SEED_DEBUG") or os.getenv("VALVE_DEBUG")) logger.info("检测到空白数据库,开始自动填充基础字典底座(型号→压力 / 口径 / 材质)...") root = Path(__file__).resolve().parents[2] cmd = [sys.executable, str(root / "scripts" / "data" / "seed_base_data.py"), "--skip-standard"] if debug: cmd.append("--debug") # 子进程输出为 UTF-8;显式注入 PYTHONIOENCODING 保证跨平台(尤其 Windows GBK locale) # 输出编码一致,避免 subprocess 内部读取线程按 GBK 解码 UTF-8 字节而抛 UnicodeDecodeError。 seed_env = {**os.environ, "PYTHONIOENCODING": "utf-8", "PYTHONUTF8": "1"} proc = subprocess.run( cmd, cwd=str(root), capture_output=True, env=seed_env, ) stdout = proc.stdout.decode("utf-8", errors="replace") if proc.stdout else "" stderr = proc.stderr.decode("utf-8", errors="replace") if proc.stderr else "" if stdout: for line in stdout.rstrip().splitlines(): (logger.info if debug else logger.debug)(f"[seed-base] {line}") if proc.returncode != 0: logger.warning( f"基础字典底座自动播种失败(exit={proc.returncode}),应用将继续启动:\n{stderr[-1500:]}" ) else: logger.info("基础字典底座自动播种完成")
[文档] def init_database_manager() -> DatabaseManager: """初始化数据库管理器(支持WebDAV同步) Returns: DatabaseManager: 已初始化(必要时从远程同步)的数据库管理器实例。 """ webdav_config = setup_webdav_config() # 创建数据库管理器 db_manager = DatabaseManager(webdav_config=webdav_config, auto_backup_before_sync=True) # 初始化数据库(如果配置了WebDAV,自动从远程同步) # 注意:这里只调用一次 init_db db_manager.init_db( create_tables=True, sync_from_webdav=bool(webdav_config), # 有配置就启用同步 force_sync=False, # 不强制覆盖现有数据库 ) # 空白数据库自动填充基础字典底座(非阻断) _maybe_seed_base_data(db_manager) # 安全网:若自动播种后仍为空(如 seed 子进程失败),给出 seed 提示(只读,不自动 seed) db_manager.warn_if_base_data_empty() return db_manager
[文档] def check_data_sources() -> None: """启动前检查关键数据源是否存在,缺失仅记 warning(不弹窗、不阻断启动)。 - 销售计划文件(SALES_PLAN_PATH):缺失则提示用户将其放到该路径。 - 基线库(database/baseline.db 与 database/samples/baseline.sample.db): 均缺失则提示放入本地基线 C 或接受新建空白库(详细提示由 db-reset 给出)。 """ from certflow.config.settings import SALES_PLAN_PATH from certflow.utils.path_utils import get_project_root # 销售计划文件 if SALES_PLAN_PATH: p = Path(SALES_PLAN_PATH) if not p.is_absolute(): p = get_project_root() / p if not p.is_file(): logger.warning( f"销售计划文件缺失: {p}\n" f" 请将销售计划 Excel 放到上述路径(或在 .env 中修改 " f"VALVE_SALES_PLAN_PATH 指向正确文件),否则销售计划相关功能不可用。" ) else: logger.warning( "未配置销售计划路径(VALVE_SALES_PLAN_PATH / paths.sales_plan.primary)," "销售计划相关功能将不可用。" ) # 基线库(仅提示,详细处置见 certflow db-reset) # 路径统一从 config/paths.yaml 读取(paths.database.baseline_local / baseline_sample), # 消除硬编码,与 db-reset / start.sh 口径一致。 root = get_project_root() baseline_c = root / cfg("paths.database.baseline_local", "database/baseline.db") baseline_d = root / cfg("paths.database.baseline_sample", "database/samples/baseline.sample.db") if not baseline_c.is_file() and not baseline_d.is_file(): logger.warning( f"未找到任何基线库:本地基线 C ({baseline_c}) 与样本基线 D ({baseline_d}) 均缺失。\n" f" 如需历史参考数据,请将基线库放到 {baseline_c}(或配置 Git LFS 拉取样本库);\n" f" 否则首次启动将新建空白库并自动播种基础字典底座。" )
[文档] def main() -> None: """主函数 解析配置、初始化日志与数据库、构建主窗口并进入 Qt 事件循环。 Examples: >>> from certflow.main import main >>> main() # 启动桌面应用 """ # 从配置读取 DEBUG 模块黑名单 debug_modules: list[str] = cfg("logging.debug_modules", []) # 初始化日志 setup_logger(debug_modules=debug_modules) # 启动前数据源检查(缺失仅 warning,不弹窗、不阻断) check_data_sources() # 过滤 Shiboken _pythonToCppCopy 噪声(Qt 内部通过 C++ fprintf(stderr) 输出, # 绕过 Python sys.stderr wrapper,故用环境变量 QT_LOGGING_RULES 抑制) # 原先由 start.sh 的 `| grep -v` 管道过滤,但管道缓冲会导致控制台日志延迟/丢失 os.environ.setdefault("QT_LOGGING_RULES", "*.debug=false") # 仅抑制 Shiboken 的 _pythonToCppCopy 警告,保留其他 Qt 日志 _existing_rules = os.environ.get("QT_LOGGING_RULES", "") if "shiboken" not in _existing_rules.lower(): os.environ["QT_LOGGING_RULES"] = ( _existing_rules + ";shiboken.warning=false" if _existing_rules else "shiboken.warning=false" ) # 初始化状态推断模块(从 config.yaml 加载规则) from certflow.utils.status_inference import StatusInference StatusInference.init_from_config() # ---- 高 DPI 适配 ---- # 启用 Qt 内置高 DPI 缩放,使控件/字体/行高在不同屏幕缩放(125%/150%)下 # 都按 devicePixelRatio 一致缩放,根除「同份代码在不同显示器上行高变高/错位」 # 的问题(之前未开启时 resizeRowsToContents 与 setFixedHeight 的像素度量不一致)。 # 尊重环境变量:设 QT_AUTO_SCALE_FACTOR=1 走自动感知,或显式 QT_SCALE_FACTOR=1.25 # 可固定缩放比;二者皆未设时回退到框架默认缩放。 if not os.environ.get("QT_SCALE_FACTOR") and not os.environ.get("QT_AUTO_SCALE_FACTOR"): os.environ.setdefault("QT_AUTO_SCALE_FACTOR", "1") # AA_EnableHighDpiScaling / AA_UseHighDpiPixmaps 在 Qt6 已废弃(高 DPI 默认开启), # 设置会触发 DeprecationWarning 且无效果;仅在 Qt5 下保留以保持旧行为。 if QtCore.qVersion().startswith("5"): if not QApplication.testAttribute(Qt.ApplicationAttribute.AA_EnableHighDpiScaling): QApplication.setAttribute(Qt.ApplicationAttribute.AA_EnableHighDpiScaling, True) QApplication.setAttribute(Qt.ApplicationAttribute.AA_UseHighDpiPixmaps, True) else: # Qt6:高 DPI 默认启用;统一缩放比取整策略,避免同份代码在不同显示器行高错位。 QApplication.setHighDpiScaleFactorRoundingPolicy( Qt.HighDpiScaleFactorRoundingPolicy.PassThrough ) # 创建应用 app = QApplication(sys.argv) # 初始化数据库管理器(会自动创建表和同步) db_manager = init_database_manager() # 获取数据库会话 session = db_manager.get_session() # 设置全局共享数据库管理器(让所有控制器自动使用) BaseController.set_shared_db_manager(db_manager) # 构建配置字典(使用 _cfg 读取) config = { "print": cfg("print", {}), "printer": cfg("printer", {}), "templates": cfg("templates", {}), "print_templates": cfg("print_templates", {}), "background": cfg("background", {}), } # 初始化设置控制器(主题/语言/自动保存) from certflow.controllers.settings_controller import SettingsController settings_controller = SettingsController() # 创建主窗口(注入设置控制器) window = MainWindow( session, config, db_manager=db_manager, settings_controller=settings_controller ) window.show() # 运行应用 exit_code = app.exec() # 清理资源 session.close() db_manager.close() BaseController.clear_shared_db_manager() sys.exit(exit_code)
if __name__ == "__main__": main()