"""系统配置服务模块 负责系统配置项的查询和更新。 提供数据库优先、默认值兜底的读取策略,支持对关键配置项的值校验。 所有写操作均会记录审计日志。 被调用方:configs 路由(配置查询、配置更新接口)。 """ from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.orm import Session from backend.app.core.error_codes import ErrorCode from backend.app.core.exceptions import AppException from backend.app.repositories.config_repository import ConfigRepository from backend.app.services.audit_service import audit_service from backend.app.services.bootstrap import DEFAULT_CONFIGS class ConfigService: """系统配置业务服务。 依赖: - ConfigRepository:配置数据的读写操作。 - AuditService:操作审计日志记录。 - DEFAULT_CONFIGS:bootstrap 阶段初始化的默认配置列表。 """ def __init__(self) -> None: self.repository = ConfigRepository() self.default_configs = {item["config_key"]: item for item in DEFAULT_CONFIGS} def get_config(self, config_key: str, session: Session | None = None) -> dict: """查询单个配置项。 优先从数据库读取,若数据库无数据则从默认配置中兜底返回。 参数: config_key: 配置键名。 session: 数据库会话,为 None 时直接返回默认值。 返回: 包含 config_key、config_value、config_name、remark、status 的字典。 被调用方:configs 路由(配置查询接口)。 """ self._ensure_config_key(config_key) if session is not None: try: config = self.repository.get_by_key(session, config_key) if config is not None: return self._map_config(config) except SQLAlchemyError as exc: raise AppException(code=ErrorCode.INTERNAL_ERROR, message="数据库异常", status_code=500) from exc default_item = self.default_configs.get(config_key) if default_item is None: raise AppException(code=ErrorCode.NOT_FOUND, message="配置不存在", status_code=404) return { "config_key": default_item["config_key"], "config_value": default_item["config_value"], "config_name": default_item["config_name"], "remark": None, "status": 1, } def update_config(self, config_key: str, payload: dict, session: Session | None = None) -> dict: """更新或新建配置项。 若配置已存在则更新,不存在则创建。写操作会记录审计日志。 针对特定配置键有值校验(如欠款生成模式、超时小时数等)。 参数: config_key: 配置键名。 payload: 配置数据字典,必含 config_value。 session: 数据库会话,不可为 None。 返回: 包含配置各字段及 updated 标志的字典。 被调用方:configs 路由(配置更新接口)。 """ self._ensure_config_key(config_key) self._ensure_config_value(config_key, payload["config_value"]) config_name = (payload.get("config_name") or self.default_configs.get(config_key, {}).get("config_name") or config_key).strip() if session is not None: try: config = self.repository.get_by_key(session, config_key) if config is None: config = self.repository.create_config( session, { "config_key": config_key, "config_value": payload["config_value"].strip(), "config_name": config_name, "remark": payload.get("remark"), "status": payload.get("status", 1), }, ) audit_service.write_log( session, { "operate_type": "config_create", "biz_type": "system_config", "biz_id": config.id, "before_value": None, "after_value": self._map_config(config), "remark": f"新增配置 {config_key}", }, ) else: before_snapshot = self._map_config(config) self.repository.update_config( config, { "config_value": payload["config_value"].strip(), "config_name": config_name, "remark": payload.get("remark"), "status": payload.get("status", 1), }, ) audit_service.write_log( session, { "operate_type": "config_update", "biz_type": "system_config", "biz_id": config.id, "before_value": before_snapshot, "after_value": self._map_config(config), "remark": f"更新配置 {config_key}", }, ) session.commit() return { "config_key": config.config_key, "config_value": config.config_value, "config_name": config.config_name, "remark": config.remark, "status": config.status, "updated": True, } except AppException: session.rollback() raise except SQLAlchemyError as exc: session.rollback() raise AppException(code=ErrorCode.INTERNAL_ERROR, message="数据库异常", status_code=500) from exc raise AppException(code=ErrorCode.INTERNAL_ERROR, message="数据库连接不可用", status_code=500) def _ensure_config_key(self, config_key: str) -> None: """校验配置键名不为空。 参数: config_key: 配置键名。 异常: 键名为空时抛出 AppException(PARAM_ERROR)。 """ if not config_key.strip(): raise AppException(code=ErrorCode.PARAM_ERROR, message="配置键不能为空", status_code=400) def _ensure_config_value(self, config_key: str, config_value: str) -> None: """校验配置值的合法性,针对特定配置键有专用校验规则。 校验规则: - arrears_generate_mode:仅允许 'shipped' 或 'delivered'。 - logistics_timeout_hours / inactive_customer_days:必须为正整数。 - inactive_customer_amount_threshold:必须为非负数。 参数: config_key: 配置键名。 config_value: 配置值。 异常: 值不合法时抛出 AppException(PARAM_ERROR)。 """ value = config_value.strip() if not value: raise AppException(code=ErrorCode.PARAM_ERROR, message="配置值不能为空", status_code=400) if config_key == "arrears_generate_mode" and value not in {"shipped", "delivered"}: raise AppException(code=ErrorCode.PARAM_ERROR, message="欠款生成模式仅支持 shipped 或 delivered", status_code=400) if config_key in {"logistics_timeout_hours", "inactive_customer_days"}: if not value.isdigit() or int(value) <= 0: raise AppException(code=ErrorCode.PARAM_ERROR, message="配置值必须为正整数", status_code=400) if config_key == "inactive_customer_amount_threshold": try: amount = float(value) except ValueError as exc: raise AppException(code=ErrorCode.PARAM_ERROR, message="金额阈值格式错误", status_code=400) from exc if amount < 0: raise AppException(code=ErrorCode.PARAM_ERROR, message="金额阈值不能小于 0", status_code=400) def _map_config(self, config) -> dict: """将数据库配置对象转换为 API 返回用的字典。 参数: config: SQLAlchemy 的配置模型对象。 返回: 包含 config_key、config_value、config_name、remark、status 的字典。 """ return { "config_key": config.config_key, "config_value": config.config_value, "config_name": config.config_name, "remark": config.remark, "status": config.status, } config_service = ConfigService()