# -*- coding: utf-8 -*- """ 管理端审核路由 —— 申请列表、详情、批准、拒绝。 端点清单: - GET /api/admin/applications — 查询申请列表(可按 status 过滤) - GET /api/admin/applications/{id} — 查询申请详情 + 候选匹配 - POST /api/admin/applications/{id}/approve — 批准申请 - POST /api/admin/applications/{id}/reject — 拒绝申请 所有端点要求 JWT + site_admin 或 tenant_admin 角色。 """ from __future__ import annotations import logging from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Query, status from app.auth.dependencies import CurrentUser from app.database import get_connection from app.middleware.permission import require_permission from app.schemas.xcx_auth import ( ApplicationResponse, ApproveRequest, MatchCandidate, RejectRequest, ) from app.services.application import approve_application, reject_application from app.services.matching import find_candidates logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/admin", tags=["管理端审核"]) # ── 管理端需要 site_admin 或 tenant_admin 权限 ───────────── # require_permission() 不检查具体 permission code, # 但会验证 status=approved;管理端路由额外在依赖中检查角色。 # 设计文档要求 site_admin / tenant_admin 角色, # 这里通过检查 CurrentUser.roles 实现。 def _require_admin(): """ 管理端依赖:要求用户 status=approved 且角色包含 site_admin 或 tenant_admin。 复用 require_permission()(无具体权限码 → 仅检查 approved), 再额外校验角色列表。 """ async def _dependency( user: CurrentUser = Depends(require_permission()), ) -> CurrentUser: admin_roles = {"site_admin", "tenant_admin"} if not admin_roles.intersection(user.roles): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="需要管理员权限(site_admin 或 tenant_admin)", ) return user return _dependency # ── GET /api/admin/applications ─────────────────────────── @router.get("/applications", response_model=list[ApplicationResponse]) async def list_applications( status_filter: Optional[str] = Query( None, alias="status", description="按状态过滤:pending / approved / rejected" ), user: CurrentUser = Depends(_require_admin()), ): """查询申请列表,可按 status 过滤。""" conn = get_connection() try: with conn.cursor() as cur: if status_filter: cur.execute( """ SELECT id, site_code, applied_role_text, status, review_note, created_at::text, reviewed_at::text FROM auth.user_applications WHERE status = %s ORDER BY created_at DESC """, (status_filter,), ) else: cur.execute( """ SELECT id, site_code, applied_role_text, status, review_note, created_at::text, reviewed_at::text FROM auth.user_applications ORDER BY created_at DESC """ ) rows = cur.fetchall() finally: conn.close() return [ ApplicationResponse( id=r[0], site_code=r[1], applied_role_text=r[2], status=r[3], review_note=r[4], created_at=r[5], reviewed_at=r[6], ) for r in rows ] # ── GET /api/admin/applications/{id} ───────────────────── @router.get("/applications/{application_id}") async def get_application_detail( application_id: int, user: CurrentUser = Depends(_require_admin()), ): """查询申请详情 + 候选匹配。""" conn = get_connection() try: with conn.cursor() as cur: cur.execute( """ SELECT id, user_id, site_code, site_id, applied_role_text, phone, employee_number, status, review_note, created_at::text, reviewed_at::text FROM auth.user_applications WHERE id = %s """, (application_id,), ) row = cur.fetchone() finally: conn.close() if row is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="申请不存在", ) app_data = { "id": row[0], "user_id": row[1], "site_code": row[2], "site_id": row[3], "applied_role_text": row[4], "phone": row[5], "employee_number": row[6], "status": row[7], "review_note": row[8], "created_at": row[9], "reviewed_at": row[10], } # 查找候选匹配 candidates_raw = await find_candidates( site_id=app_data["site_id"], phone=app_data["phone"], employee_number=app_data["employee_number"], ) candidates = [MatchCandidate(**c) for c in candidates_raw] return { "application": app_data, "candidates": [c.model_dump() for c in candidates], } # ── POST /api/admin/applications/{id}/approve ──────────── @router.post("/applications/{application_id}/approve", response_model=ApplicationResponse) async def approve( application_id: int, body: ApproveRequest, user: CurrentUser = Depends(_require_admin()), ): """批准申请:分配角色 + 可选绑定。""" result = await approve_application( application_id=application_id, reviewer_id=user.user_id, role_id=body.role_id, binding=body.binding, review_note=body.review_note, ) return ApplicationResponse(**result) # ── POST /api/admin/applications/{id}/reject ───────────── @router.post("/applications/{application_id}/reject", response_model=ApplicationResponse) async def reject( application_id: int, body: RejectRequest, user: CurrentUser = Depends(_require_admin()), ): """拒绝申请:记录拒绝原因。""" result = await reject_application( application_id=application_id, reviewer_id=user.user_id, review_note=body.review_note, ) return ApplicationResponse(**result)