"""CertFlow 配置管理 - 所有值从 config.yaml / .env 读取,零硬编码
提供模块级配置常量(直接通过 _cfg() 读取 YAML)和工具函数.
"""
import os
import sys
from pathlib import Path
from typing import Any
import yaml
from loguru import logger
# ============================================================
# 路径辅助函数
# ============================================================
def _app_base_dir() -> Path:
"""获取应用程序基础目录
自动检测运行环境,兼容 PyInstaller 打包后的可执行文件和开发环境.
Returns:
Path: 应用程序根目录路径
- 打包环境: 可执行文件所在目录
- 开发环境: 项目根目录(向上查找4级父目录)
"""
if getattr(sys, "frozen", False):
return Path(sys.executable).parent
return Path(__file__).resolve().parent.parent.parent.parent
BASE_DIR = _app_base_dir()
def _resolve_path(path_value: Any, base: Path = BASE_DIR) -> Path:
"""解析路径,将相对路径转换为绝对路径
支持相对路径(相对于BASE_DIR)和绝对路径两种形式.
Args:
path_value: 待解析的路径值,可以是字符串或Path对象
base: 基准目录,默认为应用程序根目录
Returns:
Path: 解析后的绝对路径
- 空值或None: 返回 base/output
- 绝对路径: 直接返回
- 相对路径: 相对于base拼接后返回
"""
if not path_value:
return base / "output"
if not isinstance(path_value, str):
path_value = str(path_value)
p = Path(path_value)
if p.is_absolute():
return p
return base / p
# ============================================================
# YAML 配置读取(惰性加载 + 缓存)
# ============================================================
_config_cache: dict | None = None
def _load_config() -> dict:
"""加载 config.yaml 为原始字典
使用惰性加载策略,仅在首次调用时加载配置并缓存.
Returns:
dict: 配置字典,包含所有YAML配置数据
"""
global _config_cache
if _config_cache is None:
from certflow.config.loader import ConfigLoader
loader = ConfigLoader()
root = loader.load()
_config_cache = root.model_dump(exclude_none=False)
return _config_cache
def _cfg(key: str, default: Any = None) -> Any:
"""从 YAML 配置读取嵌套键值
支持使用点号分隔的路径访问嵌套配置,如 "paths.sales_plan.primary".
Args:
key: 点号分隔的配置键路径
default: 键不存在时返回的默认值
Returns:
Any: 配置值,如果路径不存在则返回默认值
Examples:
>>> _cfg("app.name")
"CertFlow"
>>> _cfg("paths.database.dir", "database")
"database"
"""
config = _load_config()
parts = key.split(".")
value: Any = config
for part in parts:
if isinstance(value, dict):
value = value.get(part)
if value is None:
return default
else:
return default
return value
def _env(key: str) -> str:
"""读取环境变量值
Args:
key: 环境变量名称
Returns:
str: 环境变量的值,不存在时返回空字符串
"""
return os.getenv(key, "")
[文档]
def get_config_value(key: str, env_var: str | None = None, default: Any = None) -> Any:
"""获取配置值,优先级: 环境变量 > YAML > 传入默认值
提供统一的配置读取接口,优先使用环境变量,其次使用YAML配置,
最后使用代码中的默认值.
Args:
key: YAML配置中的键路径
env_var: 环境变量名称,可选
default: 默认值
Returns:
Any: 获取到的配置值
Examples:
>>> get_config_value("paths.sales_plan.primary", "VALVE_SALES_PLAN_PATH")
"/path/to/sales/plan.xlsx"
"""
if env_var:
value = _env(env_var)
if value:
return value
value = _cfg(key)
if value is not None:
return value
return default
# ============================================================
# 应用信息
# ============================================================
APP_NAME: str = _cfg("app.name", "CertFlow")
"""应用程序名称"""
APP_VERSION: str = _cfg("app.version", "0.0.1")
"""应用程序版本号"""
APP_YEAR: int = _cfg("app.year", 2026)
"""版权年份"""
APP_ORGANIZATION: str = _cfg("app.organization", "CertFlow Team")
"""组织名称"""
APP_DEBUG: bool = _cfg("app.debug", False)
"""是否启用调试模式"""
# ============================================================
# 目录路径
# ============================================================
CONFIG_DIR: Path = BASE_DIR / "config"
"""配置文件目录"""
DATABASE_DIR: Path = _resolve_path(_cfg("paths.database.dir", "database"))
"""数据库文件目录"""
TEMP_DIR: Path = _resolve_path(_cfg("paths.temp.base", "temp"))
"""临时文件根目录"""
LOGS_DIR: Path = _resolve_path(_cfg("paths.logs", "logs"))
"""日志文件目录"""
RESOURCES_DIR: Path = _resolve_path(_cfg("paths.resources", "resources"))
"""资源文件目录"""
UI_CONFIG_FILE: Path = CONFIG_DIR / "ui.yaml"
"""UI配置文件路径"""
# 数据库
DATABASE_PATH: Path = DATABASE_DIR / str(_cfg("paths.database.name", "certflow.db"))
"""SQLite数据库文件完整路径"""
DATABASE_URL: str = f"sqlite:///{DATABASE_PATH}"
"""数据库连接URL"""
# ============================================================
# 文件路径(环境变量优先)
# ============================================================
SALES_PLAN_PATH: str | None = get_config_value("paths.sales_plan.primary", "VALVE_SALES_PLAN_PATH")
"""销售计划主文件路径(优先使用环境变量)"""
SALES_PLAN_NETWORK: str | None = get_config_value(
"paths.sales_plan.network", "VALVE_SALES_PLAN_NETWORK"
)
"""销售计划网络路径"""
SALES_PLAN_LOCAL_COPY: str | None = get_config_value(
"paths.sales_plan.local_copy", "VALVE_SALES_PLAN_LOCAL_COPY"
)
"""销售计划本地副本路径"""
SALES_PLAN_MASTER_FILE: str | None = get_config_value(
"paths.sales_plan.master_file", "VALVE_MASTER_ORDER_FILE"
)
"""主订单文件路径"""
SALES_PLAN_MASTER_SHEET: str | None = _cfg("paths.sales_plan.master_sheet")
"""主订单工作表名称"""
QUALIFIED_LIST_PATH: str | None = get_config_value(
"paths.qualified_list.primary", "VALVE_QUALIFIED_LIST_PATH"
)
"""合格清单文件路径"""
QUALIFIED_LIST_XLSM_ORIGINAL: str | None = get_config_value(
"paths.qualified_list.xlsm_original", ""
)
"""原始合格清单XLSM文件路径"""
QUALIFIED_LIST_SHEET: str | None = _cfg("paths.qualified_list.sheet")
"""合格清单工作表名称"""
NAMEPLATE_DB_PATH: str | None = get_config_value(
"paths.nameplate_db.primary", "VALVE_NAMEPLATE_DB_PATH"
)
"""铭牌数据库路径"""
NAMEPLATE_DB_BACKUP: str | None = _cfg("paths.nameplate_db.backup")
"""铭牌数据库备份路径"""
QUALITY_CERT_FOREIGN_DIR: str | None = get_config_value(
"paths.quality_cert.foreign_dir", "VALVE_QUALITY_CERT_FOREIGN_DIR"
)
"""外协质量证书目录"""
QUALITY_CERT_SELF_MADE_DIR: str | None = _cfg("paths.quality_cert.self_made_dir")
"""自制质量证书目录"""
QUALITY_CERT_REPORT_DIR: str | None = _cfg("paths.quality_cert.report_dir")
"""质量证书报告目录"""
TRANSFER_DOCS_NETWORK: str | None = get_config_value(
"paths.transfer_docs.network", "VALVE_TRANSFER_DOCS_NETWORK"
)
"""交接文档网络路径"""
TRANSFER_DOCS_LOCAL: str | None = get_config_value(
"paths.transfer_docs.local", "VALVE_TRANSFER_DOCS_LOCAL"
)
"""交接文档本地路径"""
CERT_HISTORY_PATH: str | None = get_config_value(
"paths.backup.cert_history", "VALVE_CERT_BACKUP_PATH"
)
"""证书历史备份路径"""
# ============================================================
# 输出目录
# ============================================================
OUTPUT_BASE: Path = _resolve_path(_cfg("paths.output.base", "output"))
"""输出文件根目录"""
OUTPUT_TEST_REPORT: Path = OUTPUT_BASE / str(_cfg("paths.output.test_report", "reports"))
"""试压报告输出目录"""
OUTPUT_QUALITY_CERT: Path = OUTPUT_BASE / str(_cfg("paths.output.quality_cert", "certificates"))
"""质量证书输出目录"""
OUTPUT_CERTIFICATE_IMAGES: Path = OUTPUT_BASE / str(
_cfg("paths.output.certificate_images", "images")
)
"""证书图片输出目录"""
OUTPUT_NAMEPLATE: Path = OUTPUT_BASE / str(_cfg("paths.output.nameplate", "nameplates"))
"""铭牌输出目录"""
OUTPUT_BACKUPS: Path = OUTPUT_BASE / str(_cfg("paths.output.backups", "backups"))
"""备份文件输出目录"""
# 图片资源路径
NAMEPLATE_IMAGE_PATH: str | None = get_config_value(
"paths.images.nameplate", "VALVE_NAMEPLATE_IMAGE_PATH"
)
"""铭牌图片路径"""
NAMEPLATE_IMAGE_BACKUP: str | None = _cfg("paths.images.nameplate_backup")
"""铭牌图片备份路径"""
# Excel 排版配置
ROW_HEIGHT_TITLE: Any = _cfg("paths.excel_layout.row_height.title", 20)
"""Excel标题行高度"""
ROW_HEIGHT_HEADER: Any = _cfg("paths.excel_layout.row_height.header", 25)
"""Excel表头行高度"""
ROW_HEIGHT_DATA: Any = _cfg("paths.excel_layout.row_height.data", 18)
"""Excel数据行高度"""
ROW_HEIGHT_SUMMARY: Any = _cfg("paths.excel_layout.row_height.summary", 22)
"""Excel汇总行高度"""
# ============================================================
# 编号规则配置
# ============================================================
NUMBERING_PREFIX: str = _cfg("numbering.prefix", "V")
"""编号前缀(V2)"""
NUMBERING_FORMAT: str = _cfg("numbering.format", "{prefix}{year}{month}{seq:03d}{suffix}")
"""编号格式模板(V2)"""
NUMBERING_SUFFIXES: dict = _cfg("numbering.suffixes", {})
"""编号后缀映射(V2)"""
NUMBERING_AUTO_NUMBER_MODE: dict | None = _cfg("numbering.auto_number_mode")
"""自动编号模式配置(V2)"""
NUMBERING_SUFFIX_RULES: list = _cfg("numbering.suffix_rules", [])
"""编号后缀规则列表(V2)"""
# 旧接口兼容: certificate 段(V1 编号配置)
CERTIFICATE_PREFIX: str = _cfg("certificate.prefix", NUMBERING_PREFIX)
"""证书编号前缀(V1兼容)"""
CERTIFICATE_DATE_FORMAT: str = _cfg("certificate.date_format", "%Y%m%d")
"""证书日期格式(V1兼容)"""
CERTIFICATE_FORMAT_TEMPLATE: str = _cfg("certificate.format_template", "{prefix}{date}{seq:04d}")
"""证书编号格式模板(V1兼容)"""
# ============================================================
# 颜色映射
# ============================================================
VBA_COLORS: dict = _cfg("color_mapping.vba_colors", {})
"""VBA颜色索引到颜色的映射"""
FONT_COLOR_TO_SHIPPING: dict = _cfg("color_mapping.font_color_to_shipping", {})
"""字体颜色到发货状态的映射"""
BG_COLOR_TO_STATUS: dict = _cfg("color_mapping.bg_color_to_status", {})
"""背景颜色到状态的映射"""
THEME_FONT_COLORS: dict = _cfg("color_mapping.theme_font_colors", {})
"""THEME 字体颜色到状态的映射"""
# ============================================================
# 重复检查 / 唯一标识规则
# ============================================================
DUPLICATE_CHECK_FIELDS: list = _cfg("duplicate_check.fields", [])
"""重复检查字段列表"""
UNIQUE_KEY_SEPARATOR: str = _cfg("unique_key.separator", "|")
"""唯一键字段分隔符"""
UNIQUE_KEY_DATE_TO_SERIAL: bool = _cfg("unique_key.date_to_serial", True)
"""是否将日期转换为序列号"""
UNIQUE_KEY_FIELD_FALLBACKS: dict = _cfg("unique_key.key_field_fallbacks", {})
"""唯一键字段回退映射"""
# ============================================================
# 排序规则
# ============================================================
SORT_RULES_KEYS: list = _cfg("sort_rules.keys", [])
"""排序规则键列表"""
SORT_KEYS: list[str] = [k.get("field", "") for k in SORT_RULES_KEYS] if SORT_RULES_KEYS else []
"""排序字段名列表(向后兼容)"""
# ============================================================
# 分组规则
# ============================================================
GROUP_BY_FIELDS: list = _cfg("group_by.fields", [])
"""分组字段列表"""
GROUP_BY_FIELDS_NO_PROJECT: list = _cfg("group_by.fields_no_project", [])
"""非项目数据的分组字段列表"""
GROUP_BY_SUMMARY_FIELD: str = _cfg("group_by.summary_field", "数量")
"""分组汇总字段名称"""
GROUP_BY_NUMBER_FORMAT: str = _cfg("group_by.number_format", "编号:{:03d}")
"""组内编号格式"""
# ============================================================
# 导出配置
# ============================================================
EXPORT_HYPERLINK_COLUMN: int = _cfg("export.hyperlink_column", 44)
"""超链接所在列号"""
EXPORT_HYPERLINK_REF_PREFIX: str = _cfg("export.hyperlink_ref_prefix", "AR")
"""超链接引用前缀"""
EXPORT_HYPERLINK_WIDTH: int = _cfg("export.hyperlink_width", 20)
"""超链接列宽度(字符数)"""
EXPORT_SUMMARY_LABEL: str = _cfg("export.summary.label", "合计")
"""汇总行标签文本"""
EXPORT_SUMMARY_LABEL_COLUMN: int = _cfg("export.summary.label_column", 1)
"""汇总标签所在列号"""
EXPORT_SUMMARY_BG_COLOR: str = _cfg("export.summary.bg_color", "E0E0E0")
"""汇总行背景颜色"""
EXPORT_SUMMARY_COL_RANGE: int = _cfg("export.summary.col_range", 25)
"""汇总列范围"""
EXPORT_SUMMARY_SUM_COLUMNS: list = _cfg("export.summary.sum_columns", [])
"""需要求和的列列表"""
# ============================================================
# 销售计划配置
# ============================================================
SALE_PLAN_COLUMNS: dict = _cfg("sales_plan.column_mapping.direct", {})
"""销售计划列映射字典"""
COLUMN_ALIASES: dict = _cfg("sales_plan.column_mapping.aliases", {})
"""列别名映射"""
SALES_PLAN_HEADER_ROW: int = _cfg("sales_plan.header_row", 2)
"""销售计划表头行号(从1开始)"""
SALES_PLAN_DATA_START_ROW: int = _cfg("sales_plan.data_start_row", 3)
"""销售计划数据起始行号(从1开始)"""
SALES_PLAN_SHEET_PATTERN: str = _cfg("sales_plan.sheet_pattern", "{month}月")
"""销售计划工作表名称匹配模式"""
SALES_PLAN_HEADER_KEYWORDS: list = _cfg("sales_plan.header_keywords", [])
"""表头识别关键词列表"""
REQUIRED_FIELDS: list = _cfg("sales_plan.required_fields", [])
"""销售计划必填字段列表"""
STYLED_IMPORT_COLUMNS: list = _cfg("sales_plan.styled_import_columns", [])
"""带格式导入时保留的列列表"""
# ============================================================
# 合格证清单配置
# ============================================================
QUALIFIED_LIST_HEADER_ROW: int = _cfg("qualified_list.header_row", 2)
"""合格清单表头行号(从1开始)"""
QUALIFIED_LIST_DATA_START_ROW: int = _cfg("qualified_list.data_start_row", 3)
"""合格清单数据起始行号(从1开始)"""
QUALIFIED_LIST_REQUIRED_FIELDS: list = _cfg("qualified_list.required_fields", [])
"""合格清单必填字段列表"""
# ============================================================
# 数据导入配置
# ============================================================
IMPORT_MAPPING_PREPROCESSOR: dict | None = _cfg("import_mapping.preprocessor")
"""导入映射预处理器配置"""
IMPORT_MAPPING_COLUMN_MAPPING: list = _cfg("import_mapping.column_mapping", [])
"""导入列映射规则列表"""
IMPORT_MAPPING_DEFAULT_VALUES: dict = _cfg("import_mapping.default_values", {})
"""导入默认值映射"""
# ============================================================
# 试压报告配置
# ============================================================
TEST_REPORT_COMPANY_NAME: str = _cfg("test_report.company_name", "")
"""试压报告公司名称"""
TEST_REPORT_FORM_CODE: str = _cfg("test_report.form_code", "")
"""试压报告表单代码"""
# ============================================================
# 日志配置
# ============================================================
_log_level_raw: Any = _cfg("logging.level", "INFO")
LOG_LEVEL: str = str(_log_level_raw) if _log_level_raw else "INFO"
"""日志级别(DEBUG/INFO/WARNING/ERROR/CRITICAL)"""
LOG_ROTATION: str = _cfg("logging.rotation", "10 MB")
"""日志轮转条件(如"10 MB"或"1 day")"""
LOG_RETENTION: str = _cfg("logging.retention", "30 days")
"""日志保留时间(如"30 days")"""
LOG_COMPRESSION: str = _cfg("logging.compression", "zip")
"""日志压缩格式"""
LOG_FORMAT: str = _cfg("logging.format", "{time} | {level} | {name}:{function}:{line} - {message}")
"""日志格式字符串"""
# 处理环境变量占位符
if LOG_LEVEL.startswith("${"):
LOG_LEVEL = LOG_LEVEL.replace("${LOG_LEVEL:", "").rstrip("}") or "INFO"
# ============================================================
# 打印机配置
# ============================================================
DEFAULT_PRINTER: str | None = get_config_value("printer.default", "VALVE_DEFAULT_PRINTER")
"""默认打印机名称"""
SKIP_PRINTING: bool = _env("VALVE_SKIP_PRINTING") == "true"
"""是否跳过打印(用于测试)"""
# ============================================================
# 模板文件路径
# ============================================================
TEMPLATES: dict = _cfg("templates", {})
"""模板配置字典"""
def _get_template(path_key: str) -> Path:
"""获取模板文件路径
优先从顶层templates配置读取,回退到V1的paths.templates配置.
Args:
path_key: 模板键名,如"certificate_chinese"
Returns:
Path: 模板文件的绝对路径
"""
val = TEMPLATES.get(path_key, "")
if val:
return _resolve_path(val)
v1_templates = _cfg("paths.templates", {})
return _resolve_path(v1_templates.get(path_key, ""))
TEMPLATE_CERTIFICATE_CHINESE: Path = _get_template("certificate_chinese")
"""中文合格证模板路径"""
TEMPLATE_CERTIFICATE_BILINGUAL: Path = _get_template("certificate_bilingual")
"""中英文合格证模板路径"""
TEMPLATE_CERTIFICATE_RUSSIAN: Path = _get_template("certificate_russian")
"""俄文合格证模板路径"""
TEMPLATE_TEST_REPORT_SMALL: Path = _get_template("test_report_small")
"""小型试压报告模板路径"""
TEMPLATE_TEST_REPORT_MEDIUM: Path = _get_template("test_report_medium")
"""中型试压报告模板路径"""
TEMPLATE_QUALITY_CERT_FILE: Path = _get_template("quality_cert_file")
"""质量证书模板文件路径"""
# ============================================================
# 图片背景
# ============================================================
IMAGE_BG_CHINESE: str = _cfg("image_backgrounds.chinese", "")
"""中文版证书背景图片路径"""
IMAGE_BG_BILINGUAL: str = _cfg("image_backgrounds.bilingual", "")
"""中英文双语版证书背景图片路径"""
IMAGE_BG_RUSSIAN: str = _cfg("image_backgrounds.russian", "")
"""俄文版证书背景图片路径"""
# ============================================================
# 数据库配置
# ============================================================
DATABASE_TYPE: str = _cfg("database.type", "sqlite")
"""数据库类型(sqlite/mysql/postgresql)"""
DATABASE_ECHO: bool = _cfg("database.echo", False)
"""是否打印SQL语句"""
DATABASE_POOL_SIZE: int = _cfg("database.pool_size", 5)
"""数据库连接池大小"""
DATABASE_ACCESS_ENABLED: bool = _cfg("database.access.enabled", False)
"""是否启用Access数据库"""
DATABASE_ACCESS_PRIMARY: str = _cfg("database.access.primary", "")
"""Access主数据库路径"""
DATABASE_ACCESS_BACKUP: str = _cfg("database.access.backup", "")
"""Access备份数据库路径"""
# ============================================================
# 环境信息
# ============================================================
ENV_COMPUTER_NAME: str | None = get_config_value("environment.computer_name", "COMPUTERNAME")
"""当前计算机名称"""
ENV_USERNAME: str | None = get_config_value("environment.username", "USERNAME")
"""当前用户名"""
# ============================================================
# 颜色值映射
# ============================================================
COLORS_YELLOW: int = _cfg("colors.yellow", 65535)
"""黄色的RGB整数值"""
COLORS_BLUE: int = _cfg("colors.blue", 16711680)
"""蓝色的RGB整数值"""
COLORS_RED: int = _cfg("colors.red", 255)
"""红色的RGB整数值"""
COLORS_GREEN: int = _cfg("colors.green", 65280)
"""绿色的RGB整数值"""
COLORS_LIGHT_BLUE: int = _cfg("colors.light_blue", 15773696)
"""浅蓝色的RGB整数值"""
# ============================================================
# Excel 库选择
# ============================================================
USE_XLWINGS: bool = _env("VALVE_USE_XLWINGS") == "true"
"""是否使用xlwings库操作Excel"""
USE_OPENPYXL: bool = _env("VALVE_USE_OPENPYXL") == "true"
"""是否使用openpyxl库操作Excel"""
# ============================================================
# 导入去重配置(V1 兼容 — 现从 IDGenerator 获取唯一键字段)
# ============================================================
# UNIQUE_KEY_FIELDS 和 DEDUP_KEYS 已迁移至 IDGenerator.get_all_unique_key_fields()
# 如需保持向后兼容,请通过以下方式获取:
# from certflow.handlers.id_generator import IDGenerator
# UNIQUE_KEY_FIELDS = IDGenerator.get_all_unique_key_fields()
# DEDUP_KEYS = UNIQUE_KEY_FIELDS
# DEDUPLICATION_STRATEGY: str = _cfg("import_deduplication.strategy", "skip")
# """去重策略(skip/overwrite)"""
UPDATE_ON_DUPLICATE: bool = _cfg("import_deduplication.update_on_duplicate", True)
"""重复时是否检测变更并更新 SalePlan 字段"""
MONITORED_FIELDS: list = _cfg(
"import_deduplication.monitored_fields",
[
"product_name",
"product_model",
"product_spec",
"quantity",
"customer",
"project_name",
"plan_date",
],
)
"""导入时监控变更的字段列表"""
# ============================================================
# 确保输出目录存在
# ============================================================
for _dir in [
OUTPUT_TEST_REPORT,
OUTPUT_QUALITY_CERT,
OUTPUT_CERTIFICATE_IMAGES,
OUTPUT_NAMEPLATE,
OUTPUT_BACKUPS,
]:
_dir.mkdir(parents=True, exist_ok=True)
LOGS_DIR.mkdir(parents=True, exist_ok=True)
DATABASE_DIR.mkdir(parents=True, exist_ok=True)
# ============================================================
# 排序规则(完整版,从 YAML 动态读取)
# ============================================================
[文档]
def get_sort_rules() -> list[dict[str, str]]:
"""获取完整排序规则列表
Returns:
List[Dict[str, str]]: 排序规则列表,每个元素包含field和order字段
Examples:
>>> get_sort_rules()
[{"field": "order_number", "order": "asc"}, {"field": "amount", "order": "desc"}]
"""
keys: list = _cfg("sort_rules.keys", [])
return [{"field": str(k.get("field", "")), "order": str(k.get("order", "asc"))} for k in keys]
# ============================================================
# 分组与排序配置(V3 新增)
# ============================================================
# 分组配置
GROUPING_KEYS: list = _cfg("grouping.group_keys", [])
"""分组键列表(英文字段名)"""
GROUPING_KEYS_NO_PROJECT: list = _cfg("grouping.group_keys_no_project", [])
"""无项目名称时的分组键列表(英文字段名)"""
# 分组序号配置
GROUPING_USE_GLOBAL_PREFIX: bool = _cfg("grouping.numbering.use_global_prefix", True)
GROUPING_PREFIX_FORMAT: str = _cfg("grouping.numbering.prefix_format", "G{:03d}")
GROUPING_SEQ_FORMAT: str = _cfg("grouping.numbering.seq_format", "{:03d}")
GROUPING_SEPARATOR: str = _cfg("grouping.numbering.separator", "-")
GROUP_INDEX_FIELD: str = _cfg("grouping.numbering.group_index_field", "_group_index")
# 排序配置(组内排序)
SORTING_KEYS: list = _cfg("sorting.keys", [])
"""组内排序键列表,每个元素包含 field 和 order"""
# 提取排序字段名列表(供 Sorter 使用)
SORTING_FIELD_NAMES: list = [k.get("field", "") for k in SORTING_KEYS if k.get("field")]
"""排序字段名列表(按优先级顺序)"""
# ============================================================
# 颜色映射配置
# ============================================================
FONT_COLOR_STATUS_MAP: dict = _cfg("color_mapping.font_colors", {})
BG_COLOR_STATUS_MAP: dict = _cfg("color_mapping.bg_colors", {})
# ============================================================
# 已完成工单回填配置
# ============================================================
COMPLETED_ORDERS_BACKFILL: dict = _cfg("completed_orders_backfill", {})
# ============================================================
# 字段默认值
# ============================================================
[文档]
def get_field_default(field_name: str, default: str | None = None) -> Any:
"""获取证书字段的默认值
Args:
field_name: 字段名称
default: 默认值(字段不存在时返回)
Returns:
Any: 字段默认值
"""
defaults: dict = _cfg("certificate.defaults", {})
return defaults.get(field_name, default)
# ============================================================
# 列名工具
# ============================================================
[文档]
def get_column_by_alias(excel_column: str) -> str | None:
"""通过别名查找目标列名
在列别名映射中查找给定的Excel列名,返回对应的目标字段名.
Args:
excel_column: Excel中的列名或别名
Returns:
Optional[str]: 目标字段名,未找到时返回None
Examples:
>>> get_column_by_alias("订单号")
"order_number"
>>> get_column_by_alias("客户名称")
"customer_name"
"""
aliases: dict = COLUMN_ALIASES or {}
for target, alias_list in aliases.items():
if excel_column in alias_list:
return str(target)
return None
# ============================================================
# UI 配置快捷方法
# ============================================================
[文档]
def get_ui_window_title() -> str:
"""获取UI窗口标题
Returns:
str: 窗口标题,默认为"CertFlow"
"""
return str(_cfg("ui.window.title", "CertFlow"))
[文档]
def get_ui_table_alternating_colors() -> bool:
"""获取UI表格是否使用交替行颜色
Returns:
bool: 是否启用交替行颜色,默认为True
"""
return bool(_cfg("ui.table.alternating_row_colors", True))
# ============================================================
# 目录确保
# ============================================================
[文档]
def ensure_directories() -> None:
"""创建所有必需的目录
遍历所有配置的目录路径,确保它们存在.
包括数据库目录、临时文件目录、日志目录、资源目录及其子目录.
"""
for dir_path in [DATABASE_DIR, TEMP_DIR, LOGS_DIR, RESOURCES_DIR]:
dir_path.mkdir(parents=True, exist_ok=True)
temp_subdirs = [
str(_cfg("paths.temp.excel", "excel")),
str(_cfg("paths.temp.reports", "reports")),
str(_cfg("paths.temp.images", "images")),
str(_cfg("paths.temp.scans", "scans")),
]
for subdir in temp_subdirs:
(TEMP_DIR / subdir).mkdir(exist_ok=True)
template_base: str = str(_cfg("paths.templates.base", "data/templates"))
template_dir = BASE_DIR / template_base
template_dir.mkdir(parents=True, exist_ok=True)
logger.info("All directories verified/created successfully")
# ============================================================
# 配置重载
# ============================================================
[文档]
def reload_config() -> None:
"""强制重新加载 YAML 配置
清除配置缓存,下次读取时会重新加载配置文件.
用于运行时动态更新配置.
"""
global _config_cache
_config_cache = None
_load_config()
logger.info("Configuration reloaded")
# ============================================================
# 用户配置管理(userconfig.yaml 读写)
# ============================================================
_user_config_cache: dict | None = None
def _get_user_config() -> dict:
"""加载 userconfig.yaml(惰性缓存)
读取用户配置文件,用于存储用户偏好设置和导入历史.
Returns:
Dict: 用户配置字典,文件不存在时返回空字典
"""
global _user_config_cache
if _user_config_cache is None:
user_config_path = BASE_DIR / "config" / "userconfig.yaml"
if user_config_path.exists():
with open(user_config_path, encoding="utf-8") as f:
_user_config_cache = yaml.safe_load(f) or {}
else:
_user_config_cache = {}
return _user_config_cache # type: ignore[return-value]
def _save_user_config(data: dict) -> None:
"""保存 userconfig.yaml 并刷新缓存
Args:
data: 要保存的用户配置数据
"""
global _user_config_cache
user_config_path = BASE_DIR / "config" / "userconfig.yaml"
with open(user_config_path, "w", encoding="utf-8") as f:
yaml.dump(data, f, allow_unicode=True, default_flow_style=False)
_user_config_cache = data
[文档]
def get_recent_files() -> list[dict]:
"""获取最近打开的文件列表
Returns:
List[Dict]: 最近文件列表,每个元素包含path和last_used字段
"""
return _get_user_config().get("recent_files", [])
[文档]
def add_recent_file(file_path: str) -> None:
"""添加文件到最近打开列表
如果文件已存在,会移到列表开头.
Args:
file_path: 文件路径
"""
from datetime import datetime
user_config = _get_user_config()
recent_files: list = user_config.get("recent_files", [])
recent_files = [f for f in recent_files if f.get("path") != file_path]
recent_files.insert(
0, {"path": file_path, "last_used": datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
)
user_config["recent_files"] = recent_files[:10]
_save_user_config(user_config)
[文档]
def get_user_preference(key: str, default: Any = None) -> Any:
"""获取用户偏好设置
Args:
key: 偏好设置的键名
default: 默认值
Returns:
Any: 偏好设置值
"""
return _get_user_config().get("preferences", {}).get(key, default)
[文档]
def get_skipped_columns(file_path: str) -> list[str]:
"""获取导入时跳过的列列表
Args:
file_path: 文件路径
Returns:
List[str]: 跳过的列名列表
"""
user_config = _get_user_config()
import_history = user_config.get("import_history", {})
return import_history.get(file_path, {}).get("skipped_columns", [])
[文档]
def save_skipped_columns(file_path: str, skipped_columns: list[str]) -> None:
"""保存导入时跳过的列配置
Args:
file_path: 文件路径
skipped_columns: 跳过的列名列表
"""
user_config = _get_user_config()
user_config.setdefault("import_history", {}).setdefault(file_path, {})
user_config["import_history"][file_path]["skipped_columns"] = skipped_columns
_save_user_config(user_config)
logger.debug(f"已保存跳过列: {file_path} -> {skipped_columns}")
[文档]
def get_import_config(file_path: str) -> dict[str, Any]:
"""获取文件的导入配置
Args:
file_path: 文件路径
Returns:
Dict[str, Any]: 导入配置字典
"""
return _get_user_config().get("import_history", {}).get(file_path, {})
[文档]
def save_import_config(file_path: str, config: dict[str, Any]) -> None:
"""保存文件的导入配置
Args:
file_path: 文件路径
config: 导入配置字典
"""
user_config = _get_user_config()
user_config.setdefault("import_history", {}).setdefault(file_path, {}).update(config)
_save_user_config(user_config)
logger.debug(f"已保存导入配置: {file_path}")
[文档]
def get_recent_import_files(max_count: int = 5) -> list[dict]:
"""获取最近导入的文件列表
Args:
max_count: 最大返回数量,默认为5
Returns:
List[Dict]: 最近导入文件列表
"""
recent: list = _get_user_config().get("recent_import_files", [])
return recent[:max_count]
[文档]
def add_recent_import_file(file_path: str, config: dict[str, Any] | None = None) -> None:
"""添加文件到最近导入列表
Args:
file_path: 文件路径
config: 导入配置(可选)
"""
from datetime import datetime
user_config = _get_user_config()
recent: list = user_config.get("recent_import_files", [])
recent = [f for f in recent if f.get("path") != file_path]
record: dict[str, Any] = {
"path": file_path,
"name": Path(file_path).name,
"last_used": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
}
if config:
record["config"] = {
"sheet_name": config.get("sheet_name"),
"header_row": config.get("header_row"),
"data_start_row": config.get("data_start_row"),
"skip_rows": config.get("skip_rows", 0),
"column_mapping": config.get("column_mapping", {}),
}
recent.insert(0, record)
user_config["recent_import_files"] = recent[:10]
_save_user_config(user_config)
logger.debug(f"已记录最近文件: {file_path}")
[文档]
def get_file_import_config(file_path: str) -> dict[str, Any]:
"""获取文件的导入配置(优先使用最近文件中的配置)
Args:
file_path: 文件路径
Returns:
Dict[str, Any]: 导入配置字典
"""
for item in get_recent_import_files():
if item.get("path") == file_path:
return item.get("config", {})
return _get_user_config().get("file_sheet_configs", {}).get(file_path, {})
[文档]
def clear_recent_files() -> None:
"""清空最近文件列表"""
user_config = _get_user_config()
user_config["recent_import_files"] = []
_save_user_config(user_config)
logger.info("已清空最近文件列表")
[文档]
def save_sheet_config(file_path: str, sheet_configs: dict[str, Any]) -> None:
"""保存工作簿内的工作表配置
Args:
file_path: Excel文件路径
sheet_configs: 工作表配置字典,键为工作表名称,值为配置内容
"""
user_config = _get_user_config()
user_config.setdefault("file_sheet_configs", {})[file_path] = sheet_configs
_save_user_config(user_config)
[文档]
def ensure_ui_config() -> None:
"""确保UI配置文件存在
如果UI配置文件不存在,创建带有默认配置的ui.yaml文件.
"""
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
if not UI_CONFIG_FILE.exists():
_create_default_ui_config()
logger.info(f"已创建默认UI配置文件: {UI_CONFIG_FILE}")
def _create_default_ui_config() -> None:
"""创建默认的UI配置文件
生成包含主窗口配置、导航按钮、页面配置、样式定义的默认UI配置.
"""
default_config = {
"main_window": {
"title": "{app_name} - {app_version}",
"min_width": 1280,
"min_height": 800,
"default_width": 1440,
"default_height": 900,
},
"title_bar": {
"text": "{app_name} - 合格证打印管理系统",
"font_size": 20,
"font_weight": "bold",
"padding": 15,
"background_color": "#2c7be5",
"text_color": "white",
"alignment": "center",
},
"nav_buttons": [
{
"name": "import",
"text": "导入销售计划",
"icon": "",
"tooltip": "导入Excel销售计划文件",
"page_index": 1,
"style": {"padding": "20px", "font_size": "16px", "min_width": "150px"},
},
{
"name": "print",
"text": "打印合格证",
"icon": "",
"tooltip": "打印合格证",
"page_index": None,
"style": {"padding": "20px", "font_size": "16px", "min_width": "150px"},
},
{
"name": "report",
"text": "生成报告",
"icon": "",
"tooltip": "生成统计报告",
"page_index": None,
"style": {"padding": "20px", "font_size": "16px", "min_width": "150px"},
},
{
"name": "log",
"text": "查看日志",
"icon": "",
"tooltip": "查看系统日志",
"page_index": 2,
"style": {"padding": "20px", "font_size": "16px", "min_width": "150px"},
},
],
"welcome_page": {
"title": {
"text": "欢迎使用{app_name}",
"font_size": 24,
"margin": "50px",
"alignment": "center",
},
"description": {
"text": "请选择要执行的操作",
"font_size": 14,
"margin": "20px",
"alignment": "center",
},
},
"import_page": {
"title": {
"text": "销售计划导入",
"font_size": 18,
"margin": "20px",
"font_weight": "bold",
"alignment": "center",
},
"import_button": {
"text": "选择Excel文件导入",
"style": {"padding": "15px", "font_size": "14px"},
"file_filter": "Excel文件 (*.xlsx *.xls)",
},
"result_area": {"placeholder": "导入结果将显示在这里...", "read_only": True},
"back_button": {"text": "返回首页", "style": {"padding": "10px"}},
},
"log_page": {
"title": {
"text": "系统日志",
"font_size": 18,
"margin": "20px",
"font_weight": "bold",
"alignment": "center",
},
"back_button": {"text": "返回首页", "style": {"padding": "10px"}},
"max_lines": 1000,
"auto_scroll": True,
},
"status_bar": {"default_message": "就绪", "timeout_ms": 3000},
"styles": {
"global": "QMainWindow {\n background-color: #f5f5f5;\n}\n",
"button": {
"default": (
"QPushButton {\n background-color: #2c7be5;\n color: white;\n"
" border: none;\n border-radius: 5px;\n}\n"
"QPushButton:hover {\n background-color: #1c68c5;\n}\n"
"QPushButton:pressed {\n background-color: #1557a3;\n}\n"
)
},
"text_edit": (
"QTextEdit {\n border: 1px solid #ddd;\n border-radius: 5px;\n"
" padding: 10px;\n font-family: monospace;\n}\n"
),
},
}
with open(UI_CONFIG_FILE, "w", encoding="utf-8") as f:
yaml.dump(default_config, f, allow_unicode=True, default_flow_style=False, indent=2)