From 36fc7f286c0170700daccfcd4a78ec9ed250fe82 Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Fri, 14 Aug 2026 17:12:40 +0800 Subject: [PATCH] feat(duty): register admin and public duty APIs with encrypt exclude Co-authored-by: Cursor --- backend/app/api/duty.py | 107 +++++++++++++++++++++++++++++++++ backend/app/api/public_duty.py | 37 ++++++++++++ backend/app/core/config.py | 3 +- backend/app/main.py | 5 ++ 4 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 backend/app/api/duty.py create mode 100644 backend/app/api/public_duty.py diff --git a/backend/app/api/duty.py b/backend/app/api/duty.py new file mode 100644 index 000000000..c60fc41fa --- /dev/null +++ b/backend/app/api/duty.py @@ -0,0 +1,107 @@ +"""值班排班 · 管理端 API""" +from __future__ import annotations + +from datetime import date, datetime +from typing import Optional + +from fastapi import APIRouter, Depends, Query +from sqlalchemy.orm import Session + +from app.core.database import get_db +from app.core.menu_guard import require_admin_or_super_admin +from app.core.response import error_response, success_response +from app.crud import user as user_crud +from app.schemas.duty import DutyDaySaveBody, DutyLinkBody +from app.services import duty_schedule as duty_svc +from app.services.duty_schedule import DutyBizError + +router = APIRouter(prefix="/web/admin/duty", tags=["值班排班"]) + + +def _enrich_assign_names(db: Session, body: DutyDaySaveBody) -> DutyDaySaveBody: + """指定用户时补全 personName(用 nick_name)。""" + for slot in body.slots: + for item in slot.assignments or []: + if item.source != "assign": + continue + uid = (item.userId or "").strip() + if not uid: + continue + if (item.personName or "").strip(): + continue + u = user_crud.get_user(db, uid) + if u: + item.personName = (u.nick_name or "").strip() or uid + return body + + +@router.get("") +def month_summary( + year: int = Query(..., ge=2000, le=2100), + month: int = Query(..., ge=1, le=12), + _user: dict = Depends(require_admin_or_super_admin), + db: Session = Depends(get_db), +): + tenant_no = _user.get("tenant_no", "") or "" + data = duty_svc.get_month_summary(db, tenant_no, year, month) + return success_response(data) + + +@router.get("/day") +def day_detail( + dutyDate: str = Query(..., description="YYYY-MM-DD"), + _user: dict = Depends(require_admin_or_super_admin), + db: Session = Depends(get_db), +): + tenant_no = _user.get("tenant_no", "") or "" + try: + d = datetime.strptime(dutyDate.strip(), "%Y-%m-%d").date() + except ValueError: + return error_response(400, "日期格式无效") + return success_response(duty_svc.get_day_detail(db, tenant_no, d)) + + +@router.post("/day") +def save_day( + body: DutyDaySaveBody, + _user: dict = Depends(require_admin_or_super_admin), + db: Session = Depends(get_db), +): + tenant_no = _user.get("tenant_no", "") or "" + try: + body = _enrich_assign_names(db, body) + data = duty_svc.save_day(db, tenant_no, _user, body) + return success_response(data, message="保存成功") + except DutyBizError as exc: + return error_response(400, str(exc)) + + +@router.post("/day/{day_id}/link") +def set_link( + day_id: str, + body: DutyLinkBody, + _user: dict = Depends(require_admin_or_super_admin), + db: Session = Depends(get_db), +): + tenant_no = _user.get("tenant_no", "") or "" + user_id = _user.get("user_id") or _user.get("id", "") + try: + data = duty_svc.set_link_status(db, tenant_no, day_id, body.linkStatus, operator_id=user_id) + return success_response(data, message="已更新链接状态") + except DutyBizError as exc: + return error_response(400, str(exc)) + + +@router.delete("/assignments/{assignment_id}") +def delete_assignment( + assignment_id: str, + _user: dict = Depends(require_admin_or_super_admin), + db: Session = Depends(get_db), +): + tenant_no = _user.get("tenant_no", "") or "" + user_id = _user.get("user_id") or _user.get("id", "") + try: + duty_svc.soft_delete_assignment(db, tenant_no, assignment_id, operator_id=user_id) + return success_response(None, message="已删除") + except DutyBizError as exc: + return error_response(400, str(exc)) diff --git a/backend/app/api/public_duty.py b/backend/app/api/public_duty.py new file mode 100644 index 000000000..e85c98df9 --- /dev/null +++ b/backend/app/api/public_duty.py @@ -0,0 +1,37 @@ +"""值班排班 · 公开抢班 API(免登录)""" +from __future__ import annotations + +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session + +from app.core.database import get_db +from app.core.response import error_response, success_response +from app.schemas.duty import DutyGrabBody +from app.services import duty_schedule as duty_svc +from app.services.duty_schedule import DutyBizError + +router = APIRouter(prefix="/public/duty", tags=["值班排班-公开"]) + + +@router.get("/{token}") +def public_page(token: str, db: Session = Depends(get_db)): + try: + data = duty_svc.get_public_page(db, token) + return success_response(data) + except DutyBizError as exc: + msg = str(exc) + if "无效" in msg: + raise HTTPException(status_code=404, detail=msg) + return error_response(400, msg) + + +@router.post("/{token}/grab") +def public_grab(token: str, body: DutyGrabBody, db: Session = Depends(get_db)): + try: + data = duty_svc.grab(db, token, body.shiftType, body.personName) + return success_response(data, message="报名成功") + except DutyBizError as exc: + msg = str(exc) + if "无效" in msg: + raise HTTPException(status_code=404, detail=msg) + return error_response(400, msg) diff --git a/backend/app/core/config.py b/backend/app/core/config.py index fe72d9c49..baa47690e 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -178,7 +178,8 @@ class Settings(BaseSettings): "/api/public/pl/inbound," "/api/public/pl/outbound," "/api/public/db/inventory," - "/api/public/db/vehicle-plates" + "/api/public/db/vehicle-plates," + "/api/public/duty" ) class Config: diff --git a/backend/app/main.py b/backend/app/main.py index c0deabd0e..54b2e63f0 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -18,6 +18,7 @@ from app.models import models # noqa: F401 import app.models.bx_models # noqa: F401 import app.models.db_models # noqa: F401 import app.models.pl_models # noqa: F401 +import app.models.duty_models # noqa: F401 from app.api import ( public_router, menu_router, @@ -38,6 +39,8 @@ from app.api.pl.public_io_order import ( ) from app.api.db.public_inventory import router as public_db_inventory_router from app.api.db.public_vehicles import router as public_db_vehicles_router +from app.api.duty import router as duty_router +from app.api.public_duty import router as public_duty_router @asynccontextmanager @@ -161,6 +164,7 @@ app.include_router(public_pl_inbound_router, prefix=settings.API_V1_PREFIX) app.include_router(public_pl_outbound_router, prefix=settings.API_V1_PREFIX) app.include_router(public_db_inventory_router, prefix=settings.API_V1_PREFIX) app.include_router(public_db_vehicles_router, prefix=settings.API_V1_PREFIX) +app.include_router(public_duty_router, prefix=settings.API_V1_PREFIX) app.include_router(menu_router, prefix=settings.API_V1_PREFIX) app.include_router(admin_router, prefix=settings.API_V1_PREFIX) app.include_router(workflow_router, prefix=settings.API_V1_PREFIX) @@ -169,6 +173,7 @@ app.include_router(crm_router, prefix=settings.API_V1_PREFIX) app.include_router(bx_router, prefix=settings.API_V1_PREFIX) app.include_router(db_router, prefix=settings.API_V1_PREFIX) app.include_router(pl_router, prefix=settings.API_V1_PREFIX) +app.include_router(duty_router, prefix=settings.API_V1_PREFIX) app.include_router(monitor_router, prefix=settings.API_V1_PREFIX)