feat: 益选 OCR 订单处理系统初始提交
- 智能供应商识别(蓉城易购/烟草/杨碧月/通用) - 百度 OCR 表格识别集成 - 规则引擎(列映射/数据清洗/单位转换/规格推断) - 条码映射管理与云端同步(Gitea REST API) - 云端同步支持:条码映射、供应商配置、商品资料、采购模板 - 拖拽一键处理(图片→OCR→Excel→合并) - 191 个单元测试 - 移除无用的模板管理功能 - 清理 IDE 产物目录 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
"""
|
||||
配置管理模块
|
||||
-----------
|
||||
提供统一的配置加载、访问和保存功能。
|
||||
"""
|
||||
|
||||
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())
|
||||
Reference in New Issue
Block a user