feat: 系统设置新增通报时间配置 + 亮灯表30天滚动窗口 + 相关人员文案统一 + 周报数据补全
- 亮灯表从自然月改为30天滚动窗口(30/60天阈值),解决月底覆盖断崖 - 系统设置新增「定时通报时间」选择器,支局长可配置每日企微通报时刻 - 新增 SystemConfig 键值表 + scheduler_manager APScheduler 运行时重调度 - 拜访记录/导入模板/弹窗表单统一「同访人员」→「相关人员」 - 仪表盘周报数据补全:companion_names_resolved、visitor_name/phone 等字段 - 新增 weekly_report_template.xlsx 模板文件 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
"""System configuration API — get/set runtime settings like notification time."""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from app.database import get_db
|
||||
from app.middleware.auth import get_current_user, require_director
|
||||
from app.models.system_config import SystemConfig
|
||||
from app.services.scheduler_manager import reschedule_daily_check
|
||||
|
||||
router = APIRouter(prefix="/system-config", tags=["SystemConfig"])
|
||||
|
||||
DEFAULT_NOTIFICATION_TIME = "17:30"
|
||||
|
||||
|
||||
def _parse_time(time_str: str) -> tuple[int, int]:
|
||||
"""Parse 'HH:MM' string to (hour, minute). Raises ValueError on bad input."""
|
||||
parts = time_str.strip().split(":")
|
||||
if len(parts) != 2:
|
||||
raise ValueError("时间格式必须为 HH:MM")
|
||||
h, m = int(parts[0]), int(parts[1])
|
||||
if not (0 <= h <= 23 and 0 <= m <= 59):
|
||||
raise ValueError("小时 0-23,分钟 0-59")
|
||||
return h, m
|
||||
|
||||
|
||||
async def _get_config(db: AsyncSession, key: str) -> str | None:
|
||||
row = await db.get(SystemConfig, key)
|
||||
return row.value if row else None
|
||||
|
||||
|
||||
async def _set_config(db: AsyncSession, key: str, value: str):
|
||||
row = await db.get(SystemConfig, key)
|
||||
if row:
|
||||
row.value = value
|
||||
else:
|
||||
db.add(SystemConfig(key=key, value=value))
|
||||
await db.commit()
|
||||
|
||||
|
||||
async def get_notification_time(db: AsyncSession) -> str:
|
||||
"""Read notification_time from DB, falling back to default."""
|
||||
val = await _get_config(db, "notification_time")
|
||||
return val if val else DEFAULT_NOTIFICATION_TIME
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_config(db: AsyncSession = Depends(get_db)):
|
||||
"""Return all system config as {key: value} dict."""
|
||||
result = await db.execute(select(SystemConfig))
|
||||
rows = result.scalars().all()
|
||||
config = {r.key: r.value for r in rows}
|
||||
# Ensure notification_time always has a value
|
||||
if "notification_time" not in config:
|
||||
config["notification_time"] = DEFAULT_NOTIFICATION_TIME
|
||||
return config
|
||||
|
||||
|
||||
@router.put("/{key}")
|
||||
async def update_config(
|
||||
key: str,
|
||||
body: dict,
|
||||
current_user: dict = Depends(require_director),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Update a single config key. Only directors can change settings."""
|
||||
value = body.get("value", "")
|
||||
valid_keys = {"notification_time"}
|
||||
|
||||
if key not in valid_keys:
|
||||
raise HTTPException(status_code=400, detail=f"不支持的配置项: {key}")
|
||||
|
||||
if key == "notification_time":
|
||||
try:
|
||||
h, m = _parse_time(value)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
# Persist to DB
|
||||
await _set_config(db, key, value)
|
||||
# Reschedule the APScheduler job
|
||||
await reschedule_daily_check(h, m)
|
||||
return {"key": key, "value": value, "scheduled": f"{h:02d}:{m:02d}"}
|
||||
|
||||
await _set_config(db, key, value)
|
||||
return {"key": key, "value": value}
|
||||
Reference in New Issue
Block a user