dingdanquanliucheng/backend/app/services/config_service.py

216 lines
8.8 KiB
Python
Raw Normal View History

"""系统配置服务模块
负责系统配置项的查询和更新
提供数据库优先默认值兜底的读取策略支持对关键配置项的值校验
所有写操作均会记录审计日志
被调用方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
2026-05-14 13:51:06 +08:00
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_keyconfig_valueconfig_nameremarkstatus 的字典
被调用方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)
2026-05-14 13:51:06 +08:00
def _map_config(self, config) -> dict:
"""将数据库配置对象转换为 API 返回用的字典。
参数
config: SQLAlchemy 的配置模型对象
返回
包含 config_keyconfig_valueconfig_nameremarkstatus 的字典
"""
return {
"config_key": config.config_key,
"config_value": config.config_value,
"config_name": config.config_name,
"remark": config.remark,
"status": config.status,
}
2026-05-14 13:51:06 +08:00
config_service = ConfigService()