| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653 |
- """系统配置 API"""
- import re
- from typing import Optional, List
- from fastapi import APIRouter, Body, Depends
- from sqlalchemy.orm import Session
- from pydantic import BaseModel, Field, ConfigDict
- from app.core.database import get_db
- from app.core.auth import get_current_user
- from app.core.response import success_response, error_response
- from app.core.utils import generate_id
- from app.models.models import BizSystemConfig
- from app.crud.base import CRUDBase, get_model_attr
- router = APIRouter(prefix="/api/web/admin/system-config", tags=["系统配置"])
- # 运营端「系统设置」仅存以下键(其余配置走 biz_system_config 但不在该界面维护)
- _SETTING_KEY_META = {
- "auto_cancel_minutes": {
- "response_key": "autoCancelMinutes",
- "default": 30,
- "min": 5,
- "max": 1440,
- "config_type": "number",
- "config_name": "订单自动取消时间",
- "config_group": "order",
- "description": "超时未支付自动取消订单(分钟)",
- },
- "default_region_expected_delivery_minutes": {
- "response_key": "defaultRegionExpectedDeliveryMinutes",
- "default": 600,
- "min": 1,
- "max": 10080,
- "config_type": "number",
- "config_name": "区域未配置时的默认预计配送时长",
- "config_group": "order",
- "description": "分配配送员时,若区域未设置预计配送(分钟),则配送截止时间=当前时间+本值(分钟)",
- },
- "auto_confirm_receive_days": {
- "response_key": "autoConfirmReceiveDays",
- "default": 7,
- "min": 1,
- "max": 365,
- "config_type": "number",
- "config_name": "自动确认收货期限",
- "config_group": "order",
- "description": "商品送达后,客户超过该天数未确认收货则系统自动确认(天)",
- },
- "customer_service_phone": {
- "response_key": "servicePhone",
- "default": "",
- "max_len": 32,
- "config_type": "string",
- "config_name": "客服电话",
- "config_group": "contact",
- "description": "小程序一键拨号展示的客服电话",
- },
- }
- # 运营端「系统设置」页涉及的配置键:首次读取/定时任务读取前若缺失则插入默认值,保证均以 biz_system_config 为准
- _WEB_SETTINGS_KEYS = (
- "auto_cancel_minutes",
- "default_region_expected_delivery_minutes",
- "auto_confirm_receive_days",
- "customer_service_phone",
- )
- def ensure_web_system_settings_rows(db: Session, *, commit: bool = True) -> None:
- """若缺失则插入默认行(与 _SETTING_KEY_META 一致),便于统一从数据库读取。"""
- seeded = False
- for config_key in _WEB_SETTINGS_KEYS:
- if system_config_crud.get_by_key(db, config_key):
- continue
- meta = _SETTING_KEY_META[config_key]
- if meta["config_type"] == "string":
- val = "" if meta.get("default") in (None, "") else str(meta["default"])
- else:
- val = str(int(meta["default"]))
- system_config_crud.create_or_update(
- db,
- config_key,
- val,
- config_type=meta["config_type"],
- config_name=meta["config_name"],
- config_group=meta["config_group"],
- description=meta["description"],
- create_by="system",
- create_name="系统",
- commit=False,
- )
- seeded = True
- if seeded and commit:
- db.commit()
- def _parse_int_setting(raw: Optional[str], default: int) -> int:
- if raw is None or str(raw).strip() == "":
- return default
- try:
- return int(float(raw))
- except (TypeError, ValueError):
- return default
- class OrderSettingsUpdate(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- auto_cancel_minutes: Optional[int] = Field(None, alias="autoCancelMinutes", ge=5, le=1440)
- default_region_expected_delivery_minutes: Optional[int] = Field(
- None, alias="defaultRegionExpectedDeliveryMinutes", ge=1, le=10080
- )
- auto_confirm_receive_days: Optional[int] = Field(
- None, alias="autoConfirmReceiveDays", ge=1, le=365
- )
- class ContactSettingsUpdate(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- service_phone: Optional[str] = Field(None, alias="servicePhone", max_length=32)
- class SystemSettingsUpdateBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- order: Optional[OrderSettingsUpdate] = None
- contact: Optional[ContactSettingsUpdate] = None
- class SystemConfigCRUD(CRUDBase[BizSystemConfig]):
- """系统配置CRUD"""
- def __init__(self):
- super().__init__(BizSystemConfig)
- def get_by_key(self, db: Session, config_key: str) -> Optional[BizSystemConfig]:
- """根据配置键获取配置"""
- return db.query(self.model).filter(
- self.model.config_key == config_key,
- self.model.is_del == 0
- ).first()
- def create_or_update(
- self,
- db: Session,
- config_key: str,
- config_value: str,
- config_type: str = "string",
- config_name: str = None,
- config_group: str = None,
- description: str = None,
- is_active: int = 1,
- create_by: str = None,
- create_name: str = None,
- update_by: str = None,
- commit: bool = True,
- ) -> BizSystemConfig:
- """创建或更新配置"""
- existing = db.query(self.model).filter(self.model.config_key == config_key).first()
- if existing:
- existing.config_value = config_value
- existing.config_type = config_type
- if config_name:
- existing.config_name = config_name
- if config_group:
- existing.config_group = config_group
- if description:
- existing.description = description
- existing.is_active = is_active
- if hasattr(existing, "is_del"):
- existing.is_del = 0
- if update_by is not None:
- existing.update_by = update_by
- if commit:
- db.commit()
- db.refresh(existing)
- else:
- db.flush()
- return existing
- else:
- data = {
- "id": generate_id(),
- "config_key": config_key,
- "config_value": config_value,
- "config_type": config_type,
- "config_name": config_name,
- "config_group": config_group,
- "description": description,
- "is_active": is_active,
- "create_by": create_by,
- "create_name": create_name
- }
- obj = self.model()
- for key, value in data.items():
- attr_name = get_model_attr(obj, key)
- if attr_name:
- setattr(obj, attr_name, value)
- if not obj.id:
- obj.id = generate_id()
- db.add(obj)
- if commit:
- db.commit()
- db.refresh(obj)
- else:
- db.flush()
- return obj
- def batch_update_status(
- self,
- db: Session,
- ids: List[str],
- is_active: int
- ) -> int:
- """批量更新状态"""
- count = db.query(self.model).filter(
- self.model.id.in_(ids)
- ).update({"is_active": is_active}, synchronize_session=False)
- db.commit()
- return count
- system_config_crud = SystemConfigCRUD()
- def _clamp_to_meta(value: int, meta: dict) -> int:
- return max(meta["min"], min(meta["max"], value))
- def _read_setting_int(db: Session, config_key: str) -> int:
- meta = _SETTING_KEY_META[config_key]
- row = system_config_crud.get_by_key(db, config_key)
- if not row:
- return meta["default"]
- parsed = _parse_int_setting(row.config_value, meta["default"])
- return _clamp_to_meta(parsed, meta)
- def _read_setting_str(db: Session, config_key: str) -> str:
- meta = _SETTING_KEY_META[config_key]
- default = meta.get("default", "")
- max_len = int(meta.get("max_len", 255))
- row = system_config_crud.get_by_key(db, config_key)
- if not row or row.config_value is None:
- s = "" if default is None else str(default)
- return s.strip()[:max_len]
- return str(row.config_value).strip()[:max_len]
- def _normalize_service_phone(value: Optional[str]) -> str:
- if value is None:
- return ""
- s = str(value).strip()
- if len(s) > 32:
- raise ValueError("客服电话长度不能超过32个字符")
- if not s:
- return ""
- if not re.fullmatch(r"[\d\-\+\s\(\)]+", s):
- raise ValueError("客服电话仅支持数字及 - + ( ) 与空格")
- return s
- def get_auto_cancel_minutes_for_job(db: Session) -> int:
- """供定时任务读取:线上单未支付自动取消的分钟数。"""
- ensure_web_system_settings_rows(db, commit=True)
- return _read_setting_int(db, "auto_cancel_minutes")
- def get_auto_confirm_receive_days_for_job(db: Session) -> int:
- """供定时任务读取:送达后自动确认收货的天数。"""
- ensure_web_system_settings_rows(db, commit=True)
- return _read_setting_int(db, "auto_confirm_receive_days")
- def get_default_region_expected_delivery_minutes(db: Session) -> int:
- """区域未配置预计配送时长时,分配配送员弹窗使用的默认分钟数。"""
- ensure_web_system_settings_rows(db, commit=True)
- return _read_setting_int(db, "default_region_expected_delivery_minutes")
- def read_customer_service_phone_for_mini(db: Session) -> str:
- """小程序端展示的客服电话(已清洗长度)。"""
- ensure_web_system_settings_rows(db, commit=True)
- return _read_setting_str(db, "customer_service_phone")
- @router.get("/settings", response_model=dict)
- def get_system_settings(
- db: Session = Depends(get_db),
- current_user: dict = Depends(get_current_user)
- ):
- """运营端系统设置 — 数据来自 biz_system_config(配送时效预警请在「区域」中配置)"""
- ensure_web_system_settings_rows(db, commit=True)
- order = {
- "autoCancelMinutes": _read_setting_int(db, "auto_cancel_minutes"),
- "defaultRegionExpectedDeliveryMinutes": _read_setting_int(
- db, "default_region_expected_delivery_minutes"
- ),
- "autoConfirmReceiveDays": _read_setting_int(db, "auto_confirm_receive_days"),
- }
- contact = {
- "servicePhone": _read_setting_str(db, "customer_service_phone"),
- }
- return success_response({
- "order": order,
- "contact": contact,
- })
- def _parse_drm_from_payload_order(order_raw: Optional[dict]) -> Optional[int]:
- """从原始 JSON 的 order 中解析 defaultRegionExpectedDeliveryMinutes(兼容解析异常)。"""
- if not isinstance(order_raw, dict):
- return None
- raw_v = order_raw.get("defaultRegionExpectedDeliveryMinutes")
- if raw_v is None:
- return None
- try:
- return int(float(raw_v))
- except (TypeError, ValueError):
- return None
- @router.put("/settings", response_model=dict)
- def update_system_settings(
- payload: dict = Body(...),
- db: Session = Depends(get_db),
- current_user: dict = Depends(get_current_user)
- ):
- """更新运营端系统设置(按模块部分更新);三条配置同一事务提交,避免只落库两条。"""
- try:
- body = SystemSettingsUpdateBody.model_validate(payload)
- except Exception as e:
- return error_response(400, f"参数无效: {str(e)}")
- if body.order is None and body.contact is None:
- return error_response(400, "请至少选择一项设置内容")
- uid = current_user.get("user_id")
- uname = current_user.get("real_name") or current_user.get("username") or ""
- order_raw = payload.get("order") if isinstance(payload.get("order"), dict) else None
- try:
- ensure_web_system_settings_rows(db, commit=False)
- updated = False
- if body.order is not None and body.order.auto_cancel_minutes is not None:
- k = "auto_cancel_minutes"
- meta = _SETTING_KEY_META[k]
- v = _clamp_to_meta(body.order.auto_cancel_minutes, meta)
- system_config_crud.create_or_update(
- db,
- k,
- str(v),
- config_type=meta["config_type"],
- config_name=meta["config_name"],
- config_group=meta["config_group"],
- description=meta["description"],
- create_by=uid,
- create_name=uname,
- update_by=uid,
- commit=False,
- )
- updated = True
- if body.order is not None and body.order.auto_confirm_receive_days is not None:
- k_acr = "auto_confirm_receive_days"
- meta_acr = _SETTING_KEY_META[k_acr]
- v_acr = _clamp_to_meta(body.order.auto_confirm_receive_days, meta_acr)
- system_config_crud.create_or_update(
- db,
- k_acr,
- str(v_acr),
- config_type=meta_acr["config_type"],
- config_name=meta_acr["config_name"],
- config_group=meta_acr["config_group"],
- description=meta_acr["description"],
- create_by=uid,
- create_name=uname,
- update_by=uid,
- commit=False,
- )
- updated = True
- if body.order is not None:
- k_drm = "default_region_expected_delivery_minutes"
- meta_drm = _SETTING_KEY_META[k_drm]
- drm = body.order.default_region_expected_delivery_minutes
- if drm is None:
- drm = _parse_drm_from_payload_order(order_raw)
- if drm is None:
- drm = _read_setting_int(db, k_drm)
- v_drm = _clamp_to_meta(int(drm), meta_drm)
- system_config_crud.create_or_update(
- db,
- k_drm,
- str(v_drm),
- config_type=meta_drm["config_type"],
- config_name=meta_drm["config_name"],
- config_group=meta_drm["config_group"],
- description=meta_drm["description"],
- create_by=uid,
- create_name=uname,
- update_by=uid,
- commit=False,
- )
- updated = True
- if body.contact is not None and body.contact.service_phone is not None:
- try:
- phone_norm = _normalize_service_phone(body.contact.service_phone)
- except ValueError as e:
- return error_response(400, str(e))
- k = "customer_service_phone"
- meta = _SETTING_KEY_META[k]
- system_config_crud.create_or_update(
- db,
- k,
- phone_norm,
- config_type=meta["config_type"],
- config_name=meta["config_name"],
- config_group=meta["config_group"],
- description=meta["description"],
- create_by=uid,
- create_name=uname,
- update_by=uid,
- commit=False,
- )
- updated = True
- if not updated:
- return error_response(400, "没有可保存的字段")
- db.commit()
- return success_response({"message": "保存成功"})
- except Exception as e:
- db.rollback()
- return error_response(500, f"保存失败: {str(e)}")
- class SystemConfigCreateRequest(BaseModel):
- """创建系统配置请求"""
- config_key: str = Field(..., description="配置键")
- config_value: str = Field(..., description="配置值")
- config_type: str = Field("string", description="配置类型: string, number, boolean, json")
- config_name: str = Field(..., description="配置名称")
- config_group: str = Field(None, description="配置分组")
- description: str = Field(None, description="描述")
- is_active: int = Field(1, description="是否启用")
- @router.post("/create", response_model=dict)
- def create_config(
- request: SystemConfigCreateRequest,
- db: Session = Depends(get_db),
- current_user: dict = Depends(get_current_user)
- ):
- """创建系统配置"""
- try:
- # 检查配置键是否已存在
- existing = system_config_crud.get_by_key(db, request.config_key)
- if existing:
- return error_response(400, "配置键已存在")
- config = system_config_crud.create(
- db=db,
- data={
- "id": generate_id(),
- "config_key": request.config_key,
- "config_value": request.config_value,
- "config_type": request.config_type,
- "config_name": request.config_name,
- "config_group": request.config_group,
- "description": request.description,
- "is_active": request.is_active,
- "create_by": current_user.get("user_id"),
- "create_name": current_user.get("real_name")
- }
- )
- return success_response({
- "id": config.id,
- "message": "配置创建成功"
- })
- except Exception as e:
- return error_response(500, f"创建配置失败: {str(e)}")
- @router.post("/update/{config_id}", response_model=dict)
- def update_config(
- config_id: str,
- request: SystemConfigCreateRequest,
- db: Session = Depends(get_db),
- current_user: dict = Depends(get_current_user)
- ):
- """更新系统配置"""
- try:
- config = system_config_crud.get(db, config_id)
- if not config:
- return error_response(404, "配置不存在")
- if config.is_del == 1:
- return error_response(404, "配置已删除")
- # 如果修改了配置键,检查是否冲突
- if request.config_key != config.config_key:
- existing = system_config_crud.get_by_key(db, request.config_key)
- if existing:
- return error_response(400, "配置键已存在")
- config.config_key = request.config_key
- config.config_value = request.config_value
- config.config_type = request.config_type
- config.config_name = request.config_name
- config.config_group = request.config_group
- config.description = request.description
- config.is_active = request.is_active
- db.commit()
- db.refresh(config)
- return success_response({"message": "配置更新成功"})
- except Exception as e:
- return error_response(500, f"更新配置失败: {str(e)}")
- @router.get("/list", response_model=dict)
- def get_config_list(
- page: int = 1,
- page_size: int = 20,
- config_name: str = None,
- config_group: str = None,
- is_active: int = None,
- db: Session = Depends(get_db),
- current_user: dict = Depends(get_current_user)
- ):
- """获取系统配置列表"""
- try:
- query = db.query(system_config_crud.model).filter(system_config_crud.model.is_del == 0)
- if config_name:
- query = query.filter(system_config_crud.model.config_name.like(f"%{config_name}%"))
- if config_group:
- query = query.filter(system_config_crud.model.config_group == config_group)
- if is_active is not None:
- query = query.filter(system_config_crud.model.is_active == is_active)
- total = query.count()
- items = query.order_by(system_config_crud.model.config_group, system_config_crud.model.create_at.desc()).offset((page - 1) * page_size).limit(page_size).all()
- return success_response({
- "list": [_to_response(item) for item in items],
- "total": total,
- "page": page,
- "page_size": page_size
- })
- except Exception as e:
- return error_response(500, f"获取配置列表失败: {str(e)}")
- @router.get("/detail/{config_id}", response_model=dict)
- def get_config_detail(
- config_id: str,
- db: Session = Depends(get_db),
- current_user: dict = Depends(get_current_user)
- ):
- """获取配置详情"""
- try:
- config = system_config_crud.get(db, config_id)
- if not config:
- return error_response(404, "配置不存在")
- if config.is_del == 1:
- return error_response(404, "配置已删除")
- return success_response(_to_response(config))
- except Exception as e:
- return error_response(500, f"获取配置详情失败: {str(e)}")
- @router.get("/get/{config_key}", response_model=dict)
- def get_config_by_key(
- config_key: str,
- db: Session = Depends(get_db)
- ):
- """根据配置键获取配置值"""
- try:
- config = system_config_crud.get_by_key(db, config_key)
- if not config or config.is_del == 1:
- return error_response(404, "配置不存在")
- return success_response({
- "key": config.config_key,
- "value": config.config_value,
- "type": config.config_type
- })
- except Exception as e:
- return error_response(500, f"获取配置失败: {str(e)}")
- @router.post("/batch-status", response_model=dict)
- def batch_update_status(
- body: dict,
- db: Session = Depends(get_db),
- current_user: dict = Depends(get_current_user)
- ):
- """批量更新配置状态"""
- try:
- ids = body.get("ids", [])
- is_active = body.get("is_active")
- if not isinstance(ids, list) or len(ids) == 0:
- return error_response(400, "ids不能为空")
- if is_active is None:
- return error_response(400, "is_active不能为空")
- count = system_config_crud.batch_update_status(db, ids, is_active)
- return success_response({"count": count, "message": "批量更新成功"})
- except Exception as e:
- return error_response(500, f"批量更新失败: {str(e)}")
- @router.delete("/delete/{config_id}", response_model=dict)
- def delete_config(
- config_id: str,
- db: Session = Depends(get_db),
- current_user: dict = Depends(get_current_user)
- ):
- """删除配置"""
- try:
- config = system_config_crud.get(db, config_id)
- if not config:
- return error_response(404, "配置不存在")
- config.is_del = 1
- db.commit()
- return success_response({"message": "删除成功"})
- except Exception as e:
- return error_response(500, f"删除配置失败: {str(e)}")
- def _to_response(config: BizSystemConfig) -> dict:
- """转换为响应格式"""
- return {
- "id": config.id,
- "config_key": config.config_key,
- "config_value": config.config_value,
- "config_type": config.config_type,
- "config_name": config.config_name,
- "config_group": config.config_group,
- "description": config.description,
- "is_active": config.is_active,
- "create_at": str(config.create_at) if config.create_at else None,
- "update_at": str(config.update_at) if config.update_at else None
- }
|