PMFlow/backend/app/api/users_api/routes_bulk.py

243 lines
7.5 KiB
Python

from __future__ import annotations
from .shared import *
from .helpers_roles import _normalize_role_input, _pack_roles
from .helpers_user import _replace_user_projects, _serialize_user
from .schemas import BulkAssignUsersRequest, BulkDeleteUsersRequest
@router.delete("/{user_id}")
async def delete_user(
user_id: int,
tenant_id: int = Depends(get_tenant_id),
current_user: User = Depends(require_roles("BOSS", "ADMIN")),
db: AsyncSession = Depends(get_db),
):
"""删除用户(有未完成任务时禁止)。"""
if int(current_user.id) == user_id:
raise HTTPException(status_code=400, detail="Cannot delete yourself")
result = await db.execute(
select(User).where(and_(User.tenant_id == tenant_id, User.id == user_id))
)
user = result.scalar_one_or_none()
if not user:
raise HTTPException(status_code=404, detail="User not found")
incomplete_count_result = await db.execute(
select(func.count(Task.id)).where(
and_(
Task.tenant_id == tenant_id,
Task.assignee_id == user_id,
func.lower(Task.status).notin_(COMPLETED_TASK_STATUSES),
)
)
)
incomplete_count = int(incomplete_count_result.scalar() or 0)
if incomplete_count > 0:
raise HTTPException(
status_code=400,
detail=f"User has {incomplete_count} incomplete tasks",
)
before_data = user.to_dict()
await AIPMOKernelService.invalidate_world_model_cache_for_user(
db=db,
tenant_id=tenant_id,
user_id=user_id,
)
await db.execute(
delete(UserSkillBinding).where(
and_(
UserSkillBinding.tenant_id == tenant_id,
UserSkillBinding.user_id == user_id,
)
)
)
await db.execute(
delete(UserWorkload).where(
and_(
UserWorkload.tenant_id == tenant_id,
UserWorkload.user_id == user_id,
)
)
)
await db.execute(
delete(UserProject).where(
and_(
UserProject.tenant_id == tenant_id,
UserProject.user_id == user_id,
)
)
)
await db.delete(user)
await log_operation(
db,
tenant_id=tenant_id,
user_id=int(current_user.id),
project_id=None,
entity_type="user",
entity_id=str(user_id),
action="delete",
before_data=before_data,
after_data=None,
)
await db.commit()
return {"message": "User deleted successfully"}
@router.post("/bulk-delete")
async def bulk_delete_users(
body: BulkDeleteUsersRequest,
tenant_id: int = Depends(get_tenant_id),
current_user: User = Depends(require_roles("BOSS", "ADMIN")),
db: AsyncSession = Depends(get_db),
):
"""批量删除用户(带未完成任务保护)。"""
if not body.user_ids:
raise HTTPException(status_code=400, detail="user_ids required")
deleted: list[int] = []
skipped: list[dict[str, Any]] = []
for user_id in sorted(set(body.user_ids)):
if int(current_user.id) == user_id:
skipped.append({"user_id": user_id, "reason": "Cannot delete yourself"})
continue
user = (
await db.execute(
select(User).where(
and_(
User.tenant_id == tenant_id,
User.id == user_id,
)
)
)
).scalar_one_or_none()
if not user:
skipped.append({"user_id": user_id, "reason": "User not found"})
continue
incomplete_count_result = await db.execute(
select(func.count(Task.id)).where(
and_(
Task.tenant_id == tenant_id,
Task.assignee_id == user_id,
func.lower(Task.status).notin_(COMPLETED_TASK_STATUSES),
)
)
)
incomplete_count = int(incomplete_count_result.scalar() or 0)
if incomplete_count > 0:
skipped.append(
{
"user_id": user_id,
"reason": f"User has {incomplete_count} incomplete tasks",
}
)
continue
await AIPMOKernelService.invalidate_world_model_cache_for_user(
db=db,
tenant_id=tenant_id,
user_id=user_id,
)
await db.execute(
delete(UserSkillBinding).where(
and_(
UserSkillBinding.tenant_id == tenant_id,
UserSkillBinding.user_id == user_id,
)
)
)
await db.execute(
delete(UserWorkload).where(
and_(
UserWorkload.tenant_id == tenant_id,
UserWorkload.user_id == user_id,
)
)
)
await db.execute(
delete(UserProject).where(
and_(
UserProject.tenant_id == tenant_id,
UserProject.user_id == user_id,
)
)
)
await db.delete(user)
deleted.append(user_id)
await db.commit()
return {"deleted_count": len(deleted), "deleted_user_ids": deleted, "skipped": skipped}
@router.post("/bulk-assign")
async def bulk_assign_users(
body: BulkAssignUsersRequest,
tenant_id: int = Depends(get_tenant_id),
current_user: User = Depends(require_roles("BOSS", "ADMIN")),
db: AsyncSession = Depends(get_db),
):
"""批量分配项目/角色。"""
user_ids = sorted(set(body.user_ids or []))
if not user_ids:
raise HTTPException(status_code=400, detail="user_ids required")
if body.role is None and body.project_ids is None:
raise HTTPException(status_code=400, detail="role or project_ids required")
updated = 0
skipped: list[dict[str, Any]] = []
for user_id in user_ids:
user = (
await db.execute(
select(User).where(
and_(
User.tenant_id == tenant_id,
User.id == user_id,
)
)
)
).scalar_one_or_none()
if not user:
skipped.append({"user_id": user_id, "reason": "User not found"})
continue
before_data = user.to_dict()
if body.role is not None:
if not _has_any_role(_enum_value(current_user.role), "BOSS"):
skipped.append({"user_id": user_id, "reason": "Only BOSS can bulk assign roles"})
continue
try:
user.role = _pack_roles(body.role)
except ValueError as exc:
skipped.append({"user_id": user_id, "reason": str(exc)})
continue
if body.project_ids is not None:
await _replace_user_projects(
db,
tenant_id=tenant_id,
user_id=user_id,
project_ids=body.project_ids,
)
await log_operation(
db,
tenant_id=tenant_id,
user_id=int(current_user.id),
project_id=None,
entity_type="user",
entity_id=str(user_id),
action="bulk_assign",
before_data=before_data,
after_data=user.to_dict(),
remark=f"role={body.role}, projects={body.project_ids}",
)
updated += 1
await db.commit()
return {"updated_count": updated, "skipped": skipped}