dingdanquanliucheng/backend/app/services/config_service.py

216 lines
8.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""系统配置服务模块
负责系统配置项的查询和更新。
提供数据库优先、默认值兜底的读取策略,支持对关键配置项的值校验。
所有写操作均会记录审计日志。
被调用方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_CONFIGSbootstrap 阶段初始化的默认配置列表。
"""
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: 配置键名。
异常:
键名为空时抛出 AppExceptionPARAM_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: 配置值。
异常:
值不合法时抛出 AppExceptionPARAM_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", "pending_logistics"}:
raise AppException(code=ErrorCode.PARAM_ERROR, message="欠款生成模式仅支持 shipped 或 pending_logistics", 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()