243 lines
7.5 KiB
Python
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}
|
|
|