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, _translate_user_integrity_error, _create_user_with_defaults @router.post("/") async def create_user( user_data: UserCreate, tenant_id: int = Depends(get_tenant_id), current_user: User = Depends(require_roles("OWNER", "REVIEWER")), db: AsyncSession = Depends(get_db), ): """创建用户(自动初始化 user_workloads)。""" try: user = await _create_user_with_defaults( db, tenant_id=tenant_id, user_data=user_data, actor_id=int(current_user.id), commit=True, ) except ValueError as exc: await db.rollback() raise HTTPException(status_code=400, detail=str(exc)) except IntegrityError as exc: await db.rollback() raise HTTPException(status_code=400, detail=_translate_user_integrity_error(exc)) except Exception: await db.rollback() raise workloads, skills, projects = await _collect_user_enrichment(db, tenant_id, [user]) return _serialize_user(user, workloads, skills, projects) @router.put("/{user_id}") @router.patch("/{user_id}") async def update_user( user_id: int, user_data: UserUpdate, tenant_id: int = Depends(get_tenant_id), current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """更新用户信息。""" 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") current_role = _enum_value(current_user.role) is_self = int(current_user.id) == user_id is_manager = _has_any_role(current_role, *MANAGER_ROLE_SET) if not is_self and not is_manager: raise HTTPException(status_code=403, detail="No permission") if is_self and not is_manager: # 普通成员仅可修改自身基础资料。 if user_data.role is not None or user_data.status is not None or user_data.is_active is not None: raise HTTPException(status_code=403, detail="Members can only edit own profile fields") if user_data.project_ids is not None: raise HTTPException(status_code=403, detail="Members cannot change project assignments") before_data = user.to_dict() if user_data.name is not None: user.name = user_data.name.strip() if user_data.email is not None: user.email = user_data.email.strip() if user_data.phone_number is not None: from ...services.phone_auth_service import normalize_phone phone_payload = normalize_phone(user_data.phone_country_code, user_data.phone_number) user.phone_country_code = phone_payload["phone_country_code"] user.phone_number = phone_payload["phone_number"] user.phone_e164 = phone_payload["phone_e164"] user.phone_verified_at = user.phone_verified_at or now_cn_naive() if user_data.assigned_domains is not None: user.assigned_domains = user_data.assigned_domains if user_data.workload is not None: user.workload = max(0, min(200, int(user_data.workload))) if user_data.status is not None: user.status = _enum_value(user_data.status) user.is_active = user.status == UserStatus.ACTIVE.value if user_data.is_active is not None: user.is_active = bool(user_data.is_active) if not user.is_active: user.status = UserStatus.OFFLINE.value elif user.status == UserStatus.OFFLINE.value: user.status = UserStatus.ACTIVE.value if user_data.role is not None and is_manager: try: user.role = _pack_roles(user_data.role) except ValueError as exc: raise HTTPException(status_code=400, detail=str(exc)) if user_data.project_ids is not None: await _replace_user_projects( db, tenant_id=tenant_id, user_id=user_id, project_ids=user_data.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="update", before_data=before_data, after_data=user.to_dict(), ) try: await db.commit() await db.refresh(user) except IntegrityError as exc: await db.rollback() raise HTTPException(status_code=400, detail=_translate_user_integrity_error(exc)) workloads, skills, projects = await _collect_user_enrichment(db, tenant_id, [user]) return _serialize_user(user, workloads, skills, projects) @router.post("/{user_id}/reset-password") async def reset_password( user_id: int, body: UserResetPasswordRequest, tenant_id: int = Depends(get_tenant_id), current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """重置用户密码。""" 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") current_role = _enum_value(current_user.role) is_manager = _has_any_role(current_role, *MANAGER_ROLE_SET) if not is_manager and int(current_user.id) != user_id: raise HTTPException(status_code=403, detail="No permission") try: user.password_hash = AuthService.hash_password(body.new_password) except ValueError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc user.updated_at = now_cn_naive() 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="reset_password", before_data=None, after_data={"updated_at": user.updated_at.isoformat()}, ) await db.commit() return {"message": "Password reset successfully"} @router.patch("/{user_id}/status") async def patch_user_status( user_id: int, body: UserStatusPatchRequest, tenant_id: int = Depends(get_tenant_id), current_user: User = Depends(require_roles("OWNER", "REVIEWER")), db: AsyncSession = Depends(get_db), ): """禁用/启用用户,禁用时清空任务指派并归零负载。""" 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") before_data = user.to_dict() user.is_active = body.is_active user.status = UserStatus.ACTIVE.value if body.is_active else UserStatus.OFFLINE.value if not body.is_active: user.workload = 0 await db.execute( update(Task) .where( and_( Task.tenant_id == tenant_id, Task.assignee_id == user_id, ) ) .values(assignee_id=None, updated_at=now_cn_naive()) ) workload_result = await db.execute( select(UserWorkload).where( and_( UserWorkload.tenant_id == tenant_id, UserWorkload.user_id == user_id, ) ) ) workload = workload_result.scalar_one_or_none() if not workload: workload = UserWorkload(tenant_id=tenant_id, user_id=user_id) db.add(workload) workload.current_assigned_hours = 0.0 workload.workload_ratio = 0.0 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="status_patch", before_data=before_data, after_data=user.to_dict(), ) await db.commit() await db.refresh(user) return {"message": "Status updated", "user": _serialize_user(user)}