huoyan-enterprise/backend/admin/router.py

388 lines
15 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.

"""
企业后台管理 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(),
)