e4d62df7e3
- 智能供应商识别(蓉城易购/烟草/杨碧月/通用) - 百度 OCR 表格识别集成 - 规则引擎(列映射/数据清洗/单位转换/规格推断) - 条码映射管理与云端同步(Gitea REST API) - 云端同步支持:条码映射、供应商配置、商品资料、采购模板 - 拖拽一键处理(图片→OCR→Excel→合并) - 191 个单元测试 - 移除无用的模板管理功能 - 清理 IDE 产物目录 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
176 lines
6.7 KiB
Python
176 lines
6.7 KiB
Python
"""
|
|
配置管理模块
|
|
-----------
|
|
提供统一的配置加载、访问和保存功能。
|
|
"""
|
|
|
|
import os
|
|
import configparser
|
|
from typing import Dict, List, Optional, Any
|
|
|
|
from dotenv import load_dotenv
|
|
from ..core.utils.log_utils import get_logger
|
|
from .defaults import DEFAULT_CONFIG
|
|
|
|
# 加载 .env 文件
|
|
load_dotenv()
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
class ConfigManager:
|
|
"""
|
|
配置管理类,负责加载和保存配置
|
|
单例模式确保全局只有一个配置实例
|
|
"""
|
|
_instance = None
|
|
|
|
def __new__(cls, config_file=None):
|
|
"""单例模式实现"""
|
|
if cls._instance is None:
|
|
cls._instance = super(ConfigManager, cls).__new__(cls)
|
|
cls._instance._init(config_file)
|
|
return cls._instance
|
|
|
|
def _init(self, config_file):
|
|
"""初始化配置管理器"""
|
|
self.config_file = config_file or 'config.ini'
|
|
self.config = configparser.ConfigParser()
|
|
self.load_config()
|
|
|
|
def load_config(self) -> None:
|
|
"""
|
|
加载配置文件,如果不存在则创建默认配置
|
|
API 密钥优先从环境变量 (.env) 读取
|
|
"""
|
|
if not os.path.exists(self.config_file):
|
|
self.create_default_config()
|
|
else:
|
|
try:
|
|
# 先读取现有配置
|
|
self.config.read(self.config_file, encoding='utf-8')
|
|
|
|
# 检查是否有缺失的配置项,只添加缺失的项
|
|
for section, options in DEFAULT_CONFIG.items():
|
|
if not self.config.has_section(section):
|
|
self.config.add_section(section)
|
|
|
|
for option, value in options.items():
|
|
if not self.config.has_option(section, option):
|
|
self.config.set(section, option, value)
|
|
|
|
# API 密钥优先从环境变量读取
|
|
self._override_from_env()
|
|
|
|
# 保存更新后的配置
|
|
self.save_config()
|
|
logger.info(f"已加载并更新配置文件: {self.config_file}")
|
|
except Exception as e:
|
|
logger.error(f"加载配置文件时出错: {e}")
|
|
logger.info("使用默认配置")
|
|
self.create_default_config(save=False)
|
|
|
|
def _override_from_env(self) -> None:
|
|
"""从环境变量覆盖敏感配置"""
|
|
env_mapping = {
|
|
('API', 'api_key'): 'BAIDU_API_KEY',
|
|
('API', 'secret_key'): 'BAIDU_SECRET_KEY',
|
|
('Gitea', 'token'): 'GITEA_TOKEN',
|
|
}
|
|
for (section, option), env_key in env_mapping.items():
|
|
env_val = os.getenv(env_key, '').strip()
|
|
if env_val:
|
|
self.config.set(section, option, env_val)
|
|
|
|
def create_default_config(self, save: bool = True) -> None:
|
|
"""创建默认配置"""
|
|
for section, options in DEFAULT_CONFIG.items():
|
|
if not self.config.has_section(section):
|
|
self.config.add_section(section)
|
|
|
|
for option, value in options.items():
|
|
self.config.set(section, option, value)
|
|
|
|
if save:
|
|
self.save_config()
|
|
logger.info(f"已创建默认配置文件: {self.config_file}")
|
|
|
|
def save_config(self) -> None:
|
|
"""保存配置到文件(API 密钥不写入文件)"""
|
|
try:
|
|
# 保存前临时清空 API 密钥,避免写入文件
|
|
saved_keys = {}
|
|
for option in ('api_key', 'secret_key'):
|
|
saved_keys[option] = self.config.get('API', option, fallback='')
|
|
self.config.set('API', option, '')
|
|
|
|
with open(self.config_file, 'w', encoding='utf-8') as f:
|
|
self.config.write(f)
|
|
|
|
# 恢复内存中的值
|
|
for option, val in saved_keys.items():
|
|
self.config.set('API', option, val)
|
|
|
|
logger.info(f"配置已保存到: {self.config_file}")
|
|
except Exception as e:
|
|
logger.error(f"保存配置文件时出错: {e}")
|
|
|
|
def get(self, section: str, option: str, fallback: Any = None) -> Any:
|
|
"""获取配置值"""
|
|
return self.config.get(section, option, fallback=fallback)
|
|
|
|
def getint(self, section: str, option: str, fallback: int = 0) -> int:
|
|
"""获取整数配置值"""
|
|
return self.config.getint(section, option, fallback=fallback)
|
|
|
|
def getfloat(self, section: str, option: str, fallback: float = 0.0) -> float:
|
|
"""获取浮点数配置值"""
|
|
return self.config.getfloat(section, option, fallback=fallback)
|
|
|
|
def getboolean(self, section: str, option: str, fallback: bool = False) -> bool:
|
|
"""获取布尔配置值"""
|
|
return self.config.getboolean(section, option, fallback=fallback)
|
|
|
|
def get_list(self, section: str, option: str, fallback: str = "", delimiter: str = ",") -> List[str]:
|
|
"""获取列表配置值(逗号分隔的字符串转为列表)"""
|
|
value = self.get(section, option, fallback)
|
|
return [item.strip() for item in value.split(delimiter) if item.strip()]
|
|
|
|
def update(self, section: str, option: str, value: Any) -> None:
|
|
"""更新配置选项"""
|
|
if not self.config.has_section(section):
|
|
self.config.add_section(section)
|
|
|
|
self.config.set(section, option, str(value))
|
|
logger.debug(f"更新配置: [{section}] {option} = {value}")
|
|
|
|
def get_path(self, section: str, option: str, fallback: str = "", create: bool = False) -> str:
|
|
"""
|
|
获取路径配置并确保它是一个有效的绝对路径
|
|
如果create为True,则自动创建该目录
|
|
"""
|
|
from pathlib import Path
|
|
path_str = self.get(section, option, fallback)
|
|
path = Path(path_str)
|
|
|
|
if not path.is_absolute():
|
|
# 相对路径,转为绝对路径(相对于项目根目录)
|
|
path = Path(os.getcwd()) / path
|
|
|
|
if create:
|
|
try:
|
|
# 智能判断是文件还是目录
|
|
# 如果有后缀名则认为是文件,创建其父目录
|
|
if path.suffix:
|
|
directory = path.parent
|
|
if not directory.exists():
|
|
directory.mkdir(parents=True, exist_ok=True)
|
|
logger.info(f"已创建父目录: {directory}")
|
|
else:
|
|
# 否则认为是目录路径
|
|
if not path.exists():
|
|
path.mkdir(parents=True, exist_ok=True)
|
|
logger.info(f"已创建目录: {path}")
|
|
except Exception as e:
|
|
logger.error(f"创建目录失败: {path}, 错误: {e}")
|
|
|
|
return str(path.absolute()) |