Procházet zdrojové kódy

feat(duty): register admin and public duty APIs with encrypt exclude

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu před 2 měsíci
rodič
revize
36fc7f286c

+ 107 - 0
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))

+ 37 - 0
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)

+ 2 - 1
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:

+ 5 - 0
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)