""" 领导团队管理 API(role=leader 或 admin 可访问) 路由前缀:/api/team 接口列表: GET /api/team/members 查看本部门及下级成员 GET /api/team/members/{user_id} 查看成员详情 PATCH /api/team/members/{user_id}/permissions 修改成员上传权限 GET /api/team/audit-logs 查看本部门及下级操作日志 """ from typing import List, Optional import asyncpg from fastapi import APIRouter, Depends, HTTPException, Query, status from admin.schemas import AuditLogItem, AuditLogListResponse, TeamMemberItem, UserPermissionUpdate from core.dependencies import get_db, get_current_user from core.permissions import get_managed_dept_ids, is_subordinate from models.user import User from services.audit_service import AuditService from utils.helpers import BaseResponse from logger.logging import get_logger logger = get_logger(__name__) team_router = APIRouter(prefix="/api/team", tags=["领导团队管理"]) async def _require_leader_or_admin(user: User) -> User: """只有 leader 或 admin 才能访问团队管理接口。""" if user.role not in ("leader", "admin"): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="仅部门领导或管理员可访问团队管理功能", ) return user # ───────────────────────────────────────────── # 成员列表 # ───────────────────────────────────────────── @team_router.get("/members", response_model=BaseResponse, summary="查看管辖范围内的成员") async def list_team_members( page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), current_user: User = Depends(get_current_user), conn: asyncpg.Connection = Depends(get_db), ): await _require_leader_or_admin(current_user) dept_ids = await get_managed_dept_ids(conn, current_user) if not dept_ids: return BaseResponse(code=200, msg="ok", data={"total": 0, "items": []}) offset = (page - 1) * page_size total = await conn.fetchval( """ SELECT COUNT(*) FROM user_list WHERE enterprise_id = $1 AND department_id = ANY($2::int[]) AND id != $3 """, current_user.enterprise_id, dept_ids, current_user.id, ) rows = await conn.fetch( """ SELECT u.id, u.username, u.display_name, u.department_id, d.name AS department_name, u.role, u.is_active, u.allow_kb_upload, u.created_at FROM user_list u LEFT JOIN department d ON d.id = u.department_id WHERE u.enterprise_id = $1 AND u.department_id = ANY($2::int[]) AND u.id != $3 ORDER BY u.department_id, u.id LIMIT $4 OFFSET $5 """, current_user.enterprise_id, dept_ids, current_user.id, page_size, offset, ) items = [TeamMemberItem(**dict(r)).model_dump() for r in rows] return BaseResponse(code=200, msg="ok", data={"total": int(total or 0), "items": items}) @team_router.get("/members/{user_id}", response_model=BaseResponse, summary="查看成员详情") async def get_team_member( user_id: int, current_user: User = Depends(get_current_user), conn: asyncpg.Connection = Depends(get_db), ): await _require_leader_or_admin(current_user) if not await is_subordinate(conn, current_user, user_id): raise HTTPException(status_code=403, detail="无权查看该成员,不在管辖范围内") row = await conn.fetchrow( """ SELECT u.id, u.username, u.display_name, u.department_id, d.name AS department_name, u.role, u.is_active, u.allow_kb_upload, u.created_at FROM user_list u LEFT JOIN department d ON d.id = u.department_id WHERE u.id = $1 """, user_id, ) if not row: raise HTTPException(status_code=404, detail="用户不存在") return BaseResponse(code=200, msg="ok", data=TeamMemberItem(**dict(row)).model_dump()) # ───────────────────────────────────────────── # 修改成员权限 # ───────────────────────────────────────────── @team_router.patch( "/members/{user_id}/permissions", response_model=BaseResponse, summary="设置下属成员知识库上传权限", ) async def update_member_permission( user_id: int, body: UserPermissionUpdate, current_user: User = Depends(get_current_user), conn: asyncpg.Connection = Depends(get_db), ): await _require_leader_or_admin(current_user) if not await is_subordinate(conn, current_user, user_id): raise HTTPException(status_code=403, detail="越级操作:该用户不在您的管辖范围内") await conn.execute( "UPDATE user_list SET allow_kb_upload = $1 WHERE id = $2", body.allow_kb_upload, user_id, ) # 审计日志 target_row = await conn.fetchrow( "SELECT department_id FROM user_list WHERE id = $1", user_id ) await AuditService.write( conn, enterprise_id=current_user.enterprise_id or 0, actor_id=current_user.id, action="permission_change", target_user_id=user_id, department_id=current_user.department_id, metadata={ "field": "allow_kb_upload", "new": body.allow_kb_upload, "by_role": current_user.role, }, ) return BaseResponse( code=200, msg="权限已更新", data={"user_id": user_id, "allow_kb_upload": body.allow_kb_upload}, ) # ───────────────────────────────────────────── # 审计日志(部门范围) # ───────────────────────────────────────────── @team_router.get( "/audit-logs", response_model=BaseResponse, summary="查看管辖部门内的操作日志", ) async def list_team_audit_logs( page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), action: Optional[str] = Query(None, description="操作类型过滤"), actor_id: Optional[int] = Query(None, description="指定操作者"), kb_id: Optional[int] = Query(None, description="指定知识库"), current_user: User = Depends(get_current_user), conn: asyncpg.Connection = Depends(get_db), ): await _require_leader_or_admin(current_user) dept_ids = await get_managed_dept_ids(conn, current_user) # admin 传 None 查全量;leader 传 dept_ids filter_depts = None if current_user.role == "admin" else dept_ids rows, total = await AuditService.list_logs( conn, current_user.enterprise_id, department_ids=filter_depts, actor_id=actor_id, kb_id=kb_id, action=action, page=page, page_size=page_size, ) items = [AuditLogItem(**r).model_dump() for r in rows] return BaseResponse( code=200, msg="ok", data=AuditLogListResponse(total=total, items=items).model_dump(), )