388 lines
15 KiB
Python
388 lines
15 KiB
Python
"""
|
||
企业后台管理 API(需 role=admin,与主站共用 JWT:/api/auth/login)
|
||
"""
|
||
import asyncpg
|
||
from typing import Optional
|
||
|
||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||
|
||
from admin.schemas import (
|
||
AdminUserCreate,
|
||
AdminUserListItem,
|
||
AdminUserListResponse,
|
||
AdminUserUpdate,
|
||
AuditLogItem,
|
||
AuditLogListResponse,
|
||
DepartmentCreate,
|
||
DepartmentResponse,
|
||
DepartmentSetLeader,
|
||
DepartmentUpdate,
|
||
EnterpriseResponse,
|
||
EnterpriseUpdate,
|
||
UserPermissionUpdate,
|
||
)
|
||
from core.dependencies import get_db, get_current_admin_user
|
||
from models.user import User
|
||
from services.admin_user_service import AdminUserService
|
||
from services.audit_service import AuditService
|
||
from services.department_service import DepartmentService
|
||
from services.enterprise_service import EnterpriseService
|
||
from utils.helpers import BaseResponse
|
||
|
||
admin_router = APIRouter(prefix="/api/admin", tags=["后台管理"])
|
||
|
||
|
||
@admin_router.get("/enterprise", response_model=BaseResponse, summary="当前企业信息")
|
||
async def get_enterprise(
|
||
admin: User = Depends(get_current_admin_user),
|
||
conn: asyncpg.Connection = Depends(get_db),
|
||
):
|
||
if admin.enterprise_id is None:
|
||
raise HTTPException(status_code=400, detail="用户未关联企业")
|
||
row = await EnterpriseService.get_by_id(conn, admin.enterprise_id)
|
||
if not row:
|
||
raise HTTPException(status_code=404, detail="企业不存在")
|
||
return BaseResponse(
|
||
code=200,
|
||
msg="ok",
|
||
data=EnterpriseResponse(**row).model_dump(),
|
||
)
|
||
|
||
|
||
@admin_router.put("/enterprise", response_model=BaseResponse, summary="更新企业信息")
|
||
async def update_enterprise(
|
||
body: EnterpriseUpdate,
|
||
admin: User = Depends(get_current_admin_user),
|
||
conn: asyncpg.Connection = Depends(get_db),
|
||
):
|
||
if admin.enterprise_id is None:
|
||
raise HTTPException(status_code=400, detail="用户未关联企业")
|
||
row = await EnterpriseService.update_profile(
|
||
conn,
|
||
admin.enterprise_id,
|
||
name=body.name,
|
||
ai_display_name=body.ai_display_name,
|
||
)
|
||
if not row:
|
||
raise HTTPException(status_code=404, detail="企业不存在")
|
||
return BaseResponse(code=200, msg="更新成功", data=EnterpriseResponse(**row).model_dump())
|
||
|
||
|
||
@admin_router.get("/departments", response_model=BaseResponse, summary="部门列表")
|
||
async def list_departments(
|
||
admin: User = Depends(get_current_admin_user),
|
||
conn: asyncpg.Connection = Depends(get_db),
|
||
):
|
||
rows = await DepartmentService.list_by_enterprise(conn, admin.enterprise_id)
|
||
items = [
|
||
DepartmentResponse(
|
||
id=r["id"],
|
||
enterprise_id=r["enterprise_id"],
|
||
name=r["name"],
|
||
parent_id=r["parent_id"],
|
||
leader_user_id=r.get("leader_user_id"),
|
||
leader_name=r.get("leader_name"),
|
||
created_at=r["created_at"],
|
||
).model_dump()
|
||
for r in rows
|
||
]
|
||
return BaseResponse(code=200, msg="ok", data={"items": items})
|
||
|
||
|
||
@admin_router.post("/departments", response_model=BaseResponse, summary="创建部门")
|
||
async def create_department(
|
||
body: DepartmentCreate,
|
||
admin: User = Depends(get_current_admin_user),
|
||
conn: asyncpg.Connection = Depends(get_db),
|
||
):
|
||
if body.parent_id is not None:
|
||
parent = await DepartmentService.get_by_id(conn, body.parent_id, admin.enterprise_id)
|
||
if not parent:
|
||
raise HTTPException(status_code=400, detail="上级部门不存在")
|
||
try:
|
||
row = await DepartmentService.create(
|
||
conn, admin.enterprise_id, body.name, body.parent_id
|
||
)
|
||
return BaseResponse(
|
||
code=200,
|
||
msg="创建成功",
|
||
data=DepartmentResponse(
|
||
id=row["id"],
|
||
enterprise_id=row["enterprise_id"],
|
||
name=row["name"],
|
||
parent_id=row["parent_id"],
|
||
leader_user_id=None,
|
||
created_at=row["created_at"],
|
||
).model_dump(),
|
||
)
|
||
except asyncpg.UniqueViolationError:
|
||
raise HTTPException(status_code=400, detail="同企业下部门名称已存在")
|
||
|
||
|
||
@admin_router.put("/departments/{dept_id}", response_model=BaseResponse, summary="更新部门")
|
||
async def update_department(
|
||
dept_id: int,
|
||
body: DepartmentUpdate,
|
||
admin: User = Depends(get_current_admin_user),
|
||
conn: asyncpg.Connection = Depends(get_db),
|
||
):
|
||
if body.parent_id is not None:
|
||
parent = await DepartmentService.get_by_id(conn, body.parent_id, admin.enterprise_id)
|
||
if not parent:
|
||
raise HTTPException(status_code=400, detail="上级部门不存在")
|
||
row = await DepartmentService.update(
|
||
conn, dept_id, admin.enterprise_id, name=body.name, parent_id=body.parent_id
|
||
)
|
||
if not row:
|
||
raise HTTPException(status_code=404, detail="部门不存在")
|
||
return BaseResponse(
|
||
code=200,
|
||
msg="更新成功",
|
||
data=DepartmentResponse(
|
||
id=row["id"],
|
||
enterprise_id=row["enterprise_id"],
|
||
name=row["name"],
|
||
parent_id=row["parent_id"],
|
||
leader_user_id=row.get("leader_user_id"),
|
||
created_at=row["created_at"],
|
||
).model_dump(),
|
||
)
|
||
|
||
|
||
@admin_router.delete("/departments/{dept_id}", response_model=BaseResponse, summary="删除部门")
|
||
async def delete_department(
|
||
dept_id: int,
|
||
admin: User = Depends(get_current_admin_user),
|
||
conn: asyncpg.Connection = Depends(get_db),
|
||
):
|
||
err = await DepartmentService.delete(conn, dept_id, admin.enterprise_id)
|
||
if err:
|
||
raise HTTPException(status_code=400, detail=err)
|
||
return BaseResponse(code=200, msg="删除成功", data=None)
|
||
|
||
|
||
@admin_router.get("/users", response_model=BaseResponse, summary="用户列表")
|
||
async def list_users(
|
||
page: int = Query(1, ge=1),
|
||
page_size: int = Query(20, ge=1, le=100),
|
||
username: Optional[str] = Query(None, description="用户名(模糊)"),
|
||
email: Optional[str] = Query(None, description="邮箱(模糊)"),
|
||
phone: Optional[str] = Query(None, description="手机号(模糊)"),
|
||
display_name: Optional[str] = Query(None, description="显示名(模糊)"),
|
||
department_id: Optional[int] = Query(None, description="按部门 ID 精确筛选"),
|
||
admin: User = Depends(get_current_admin_user),
|
||
conn: asyncpg.Connection = Depends(get_db),
|
||
):
|
||
if department_id is not None:
|
||
d = await DepartmentService.get_by_id(conn, department_id, admin.enterprise_id)
|
||
if not d:
|
||
raise HTTPException(status_code=400, detail="部门不存在")
|
||
rows, total = await AdminUserService.list_users(
|
||
conn,
|
||
admin.enterprise_id,
|
||
page,
|
||
page_size,
|
||
username=username,
|
||
email=email,
|
||
phone=phone,
|
||
display_name=display_name,
|
||
department_id=department_id,
|
||
)
|
||
items = [AdminUserListItem(**r).model_dump() for r in rows]
|
||
return BaseResponse(
|
||
code=200,
|
||
msg="ok",
|
||
data=AdminUserListResponse(total=total, items=items).model_dump(),
|
||
)
|
||
|
||
|
||
@admin_router.post("/users", response_model=BaseResponse, summary="创建用户")
|
||
async def create_user(
|
||
body: AdminUserCreate,
|
||
admin: User = Depends(get_current_admin_user),
|
||
conn: asyncpg.Connection = Depends(get_db),
|
||
):
|
||
if body.department_id is not None:
|
||
d = await DepartmentService.get_by_id(conn, body.department_id, admin.enterprise_id)
|
||
if not d:
|
||
raise HTTPException(status_code=400, detail="部门不存在")
|
||
try:
|
||
row = await AdminUserService.create_user(conn, admin.enterprise_id, body)
|
||
return BaseResponse(
|
||
code=200,
|
||
msg="创建成功",
|
||
data=AdminUserListItem(**row).model_dump(),
|
||
)
|
||
except ValueError as e:
|
||
raise HTTPException(status_code=400, detail=str(e))
|
||
|
||
|
||
@admin_router.get("/users/{user_id}", response_model=BaseResponse, summary="用户详情")
|
||
async def get_user(
|
||
user_id: int,
|
||
admin: User = Depends(get_current_admin_user),
|
||
conn: asyncpg.Connection = Depends(get_db),
|
||
):
|
||
row = await AdminUserService.get_user(conn, admin.enterprise_id, user_id)
|
||
if not row:
|
||
raise HTTPException(status_code=404, detail="用户不存在")
|
||
return BaseResponse(
|
||
code=200,
|
||
msg="ok",
|
||
data=AdminUserListItem(**row).model_dump(),
|
||
)
|
||
|
||
|
||
@admin_router.put("/users/{user_id}", response_model=BaseResponse, summary="更新用户")
|
||
async def update_user(
|
||
user_id: int,
|
||
body: AdminUserUpdate,
|
||
admin: User = Depends(get_current_admin_user),
|
||
conn: asyncpg.Connection = Depends(get_db),
|
||
):
|
||
unset = body.model_dump(exclude_unset=True)
|
||
if "department_id" in unset and unset["department_id"] is not None:
|
||
d = await DepartmentService.get_by_id(conn, unset["department_id"], admin.enterprise_id)
|
||
if not d:
|
||
raise HTTPException(status_code=400, detail="部门不存在")
|
||
try:
|
||
row = await AdminUserService.update_user(conn, admin, user_id, body)
|
||
if not row:
|
||
raise HTTPException(status_code=404, detail="用户不存在")
|
||
return BaseResponse(
|
||
code=200,
|
||
msg="更新成功",
|
||
data=AdminUserListItem(**row).model_dump(),
|
||
)
|
||
except ValueError as e:
|
||
raise HTTPException(status_code=400, detail=str(e))
|
||
|
||
|
||
@admin_router.delete("/users/{user_id}", response_model=BaseResponse, summary="删除用户")
|
||
async def delete_user(
|
||
user_id: int,
|
||
admin: User = Depends(get_current_admin_user),
|
||
conn: asyncpg.Connection = Depends(get_db),
|
||
):
|
||
try:
|
||
ok = await AdminUserService.delete_user(conn, admin, user_id)
|
||
if not ok:
|
||
raise HTTPException(status_code=404, detail="用户不存在")
|
||
return BaseResponse(code=200, msg="删除成功", data=None)
|
||
except ValueError as e:
|
||
raise HTTPException(status_code=400, detail=str(e))
|
||
except asyncpg.ForeignKeyViolationError:
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail="该用户仍存在关联数据(如会话、知识库归属等),无法直接删除,请先禁用账号",
|
||
)
|
||
|
||
|
||
# ────────────────────────────────────────────────────────────
|
||
# 部门负责人绑定(admin 专用)
|
||
# ────────────────────────────────────────────────────────────
|
||
|
||
@admin_router.put(
|
||
"/departments/{dept_id}/leader",
|
||
response_model=BaseResponse,
|
||
summary="设置/清除部门负责人",
|
||
)
|
||
async def set_department_leader(
|
||
dept_id: int,
|
||
body: DepartmentSetLeader,
|
||
admin: User = Depends(get_current_admin_user),
|
||
conn: asyncpg.Connection = Depends(get_db),
|
||
):
|
||
# 若指定了 user_id,确认该用户属于本企业且是 leader
|
||
if body.leader_user_id is not None:
|
||
u = await conn.fetchrow(
|
||
"SELECT id, role, enterprise_id FROM user_list WHERE id = $1 AND enterprise_id = $2",
|
||
body.leader_user_id, admin.enterprise_id,
|
||
)
|
||
if not u:
|
||
raise HTTPException(status_code=400, detail="用户不存在或不属于本企业")
|
||
if u["role"] not in ("leader", "admin"):
|
||
raise HTTPException(status_code=400, detail="只能将 role=leader 或 admin 的用户设为负责人")
|
||
row = await DepartmentService.set_leader(conn, dept_id, admin.enterprise_id, body.leader_user_id)
|
||
if not row:
|
||
raise HTTPException(status_code=404, detail="部门不存在")
|
||
return BaseResponse(code=200, msg="设置成功", data=DepartmentResponse(
|
||
id=row["id"],
|
||
enterprise_id=row["enterprise_id"],
|
||
name=row["name"],
|
||
parent_id=row["parent_id"],
|
||
leader_user_id=row.get("leader_user_id"),
|
||
).model_dump())
|
||
|
||
|
||
# ────────────────────────────────────────────────────────────
|
||
# 用户上传权限开关(admin 可设置任意用户)
|
||
# ────────────────────────────────────────────────────────────
|
||
|
||
@admin_router.patch(
|
||
"/users/{user_id}/permissions",
|
||
response_model=BaseResponse,
|
||
summary="设置用户知识库上传权限",
|
||
)
|
||
async def set_user_permission(
|
||
user_id: int,
|
||
body: UserPermissionUpdate,
|
||
admin: User = Depends(get_current_admin_user),
|
||
conn: asyncpg.Connection = Depends(get_db),
|
||
):
|
||
target = await conn.fetchrow(
|
||
"SELECT id FROM user_list WHERE id = $1 AND enterprise_id = $2",
|
||
user_id, admin.enterprise_id,
|
||
)
|
||
if not target:
|
||
raise HTTPException(status_code=404, detail="用户不存在")
|
||
await conn.execute(
|
||
"UPDATE user_list SET allow_kb_upload = $1 WHERE id = $2",
|
||
body.allow_kb_upload, user_id,
|
||
)
|
||
await AuditService.write(
|
||
conn,
|
||
enterprise_id=admin.enterprise_id or 0,
|
||
actor_id=admin.id,
|
||
action="permission_change",
|
||
target_user_id=user_id,
|
||
metadata={"field": "allow_kb_upload", "new": body.allow_kb_upload, "by": "admin"},
|
||
)
|
||
return BaseResponse(code=200, msg="权限已更新", data={"allow_kb_upload": body.allow_kb_upload})
|
||
|
||
|
||
# ────────────────────────────────────────────────────────────
|
||
# 全企业审计日志(admin 专用)
|
||
# ────────────────────────────────────────────────────────────
|
||
|
||
@admin_router.get(
|
||
"/audit-logs",
|
||
response_model=BaseResponse,
|
||
summary="查看全企业知识库操作日志",
|
||
)
|
||
async def list_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="操作者 user_id"),
|
||
kb_id: Optional[int] = Query(None, description="知识库 ID"),
|
||
admin: User = Depends(get_current_admin_user),
|
||
conn: asyncpg.Connection = Depends(get_db),
|
||
):
|
||
rows, total = await AuditService.list_logs(
|
||
conn,
|
||
admin.enterprise_id,
|
||
department_ids=None, # admin 查全量
|
||
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(),
|
||
)
|