Files
ctms/backend/app/models/user.py
T
Cheng Zhou 5b97ea32ab 移除用户系统级角色,权限完全迁移至项目级管理
- 用户注册/创建不再需要选择角色,账号管理只管资料和状态
- 移除前后端所有系统级 role 字段的暴露,改用 is_admin 标识管理员
- PM(项目负责人)角色自动拥有全部项目权限且不可修改
- 系统管理员在项目中的成员身份不可被删除
- 管理界面全面美化

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 15:52:17 +08:00

77 lines
2.3 KiB
Python

from __future__ import annotations
from typing import Optional
import enum
import uuid
from datetime import datetime
from sqlalchemy import DateTime, Enum, ForeignKey, String, func
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base_class import Base
class UserRole(str, enum.Enum):
ADMIN = "ADMIN"
PM = "PM"
CRA = "CRA"
PV = "PV"
MEDICAL_REVIEW = "MEDICAL_REVIEW"
IMP = "IMP"
QA = "QA"
class UserStatus(str, enum.Enum):
PENDING = "PENDING"
ACTIVE = "ACTIVE"
REJECTED = "REJECTED"
DISABLED = "DISABLED"
class User(Base):
__tablename__ = "users"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
email: Mapped[str] = mapped_column(String(255), unique=True, index=True, nullable=False)
password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
full_name: Mapped[str] = mapped_column(String(255), nullable=False)
role: Mapped[UserRole] = mapped_column(Enum(UserRole, name="user_role"), nullable=False)
clinical_department: Mapped[str] = mapped_column(String(255), nullable=False)
status: Mapped[UserStatus] = mapped_column(
Enum(UserStatus, name="user_status"),
nullable=False,
server_default=UserStatus.PENDING.value,
default=UserStatus.PENDING,
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
server_default=func.now(),
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
server_default=func.now(),
onupdate=func.now(),
)
approved_by: Mapped[Optional[uuid.UUID]] = mapped_column(
UUID(as_uuid=True),
ForeignKey("users.id"),
nullable=True,
)
approved_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
avatar_url: Mapped[Optional[str]] = mapped_column(String(500), nullable=True)
@property
def is_active(self) -> bool: # compatibility helper for existing checks
return self.status == UserStatus.ACTIVE
@property
def is_admin(self) -> bool:
return self.role == UserRole.ADMIN
@property
def username(self) -> str: # backward compatibility for existing UI copy
return self.email