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("OWNER", "REVIEWER")), 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("OWNER", "REVIEWER")), 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("OWNER", "REVIEWER")), 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), "OWNER"): skipped.append({"user_id": user_id, "reason": "Only OWNER 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}