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

220 lines
7.7 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.

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.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)}