215 lines
6.6 KiB
Python
215 lines
6.6 KiB
Python
# -*- 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)
|