Files
Neo-ZQYY/apps/backend/app/routers/admin_applications.py

215 lines
6.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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)