228 lines
8.1 KiB
Python
228 lines
8.1 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, _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)}
|