From 6c2bcc59b2a14eb63d98c16ab767293862b60289 Mon Sep 17 00:00:00 2001 From: Cheng Zhou Date: Mon, 29 Jun 2026 16:25:04 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E6=95=B4=E8=B4=A6=E5=8F=B7=E6=B3=A8?= =?UTF-8?q?=E5=86=8C=E4=B8=8E=E5=88=A0=E9=99=A4=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/v1/admin_users.py | 62 --- backend/app/api/v1/auth.py | 4 +- backend/app/api/v1/router.py | 3 +- backend/app/api/v1/users.py | 15 +- backend/app/crud/user.py | 41 +- backend/app/schemas/user.py | 9 - frontend/src/api/admin.ts | 12 - frontend/src/api/users.ts | 2 +- frontend/src/content/authProtocol.ts | 2 +- frontend/src/locales/zh-CN.ts | 20 +- frontend/src/router/index.ts | 7 - frontend/src/views/Login.vue | 2 +- frontend/src/views/Register.vue | 6 +- .../src/views/admin/AdminUserApproval.vue | 405 ------------------ frontend/src/views/admin/Users.vue | 43 +- 15 files changed, 44 insertions(+), 589 deletions(-) delete mode 100644 backend/app/api/v1/admin_users.py delete mode 100644 frontend/src/views/admin/AdminUserApproval.vue diff --git a/backend/app/api/v1/admin_users.py b/backend/app/api/v1/admin_users.py deleted file mode 100644 index cec261d5..00000000 --- a/backend/app/api/v1/admin_users.py +++ /dev/null @@ -1,62 +0,0 @@ -import uuid - -from fastapi import APIRouter, Depends, HTTPException, Query, status -from sqlalchemy.ext.asyncio import AsyncSession - -from app.core.deps import get_db_session, is_system_admin, require_roles -from app.crud import user as user_crud -from app.models.user import User, UserStatus -from app.schemas.user import AdminUserListResponse, UserAdminReviewRequest, UserResponse - -router = APIRouter(prefix="/users") - - -@router.get("/", response_model=AdminUserListResponse) -async def list_users_for_review( - status_filter: UserStatus = Query(default=UserStatus.PENDING, alias="status"), - db: AsyncSession = Depends(get_db_session), - current_user=Depends(require_roles(["ADMIN"])), -) -> AdminUserListResponse: - users = await user_crud.list_users_by_status(db, status=status_filter) - return AdminUserListResponse(items=list(users), total=len(users)) - - -async def _get_review_user(db: AsyncSession, user_id: uuid.UUID) -> User: - user = await user_crud.get_by_id(db, user_id) - if not user: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在") - if is_system_admin(user): - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="管理员账号不允许审核") - return user - - -@router.post("/{user_id}/approve", response_model=UserResponse) -async def approve_user( - user_id: uuid.UUID, - review: UserAdminReviewRequest, - db: AsyncSession = Depends(get_db_session), - current_user=Depends(require_roles(["ADMIN"])), -) -> UserResponse: - if review.action != "approve": - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="非法操作") - user = await _get_review_user(db, user_id) - if user.status != UserStatus.PENDING: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="仅允许审核待审核用户") - user = await user_crud.approve_user(db, user, admin_id=current_user.id) - return user - - -@router.post("/{user_id}/reject", response_model=UserResponse) -async def reject_user( - user_id: uuid.UUID, - review: UserAdminReviewRequest, - db: AsyncSession = Depends(get_db_session), - current_user=Depends(require_roles(["ADMIN"])), -) -> UserResponse: - if review.action not in ("reject", "approve"): - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="非法操作") - user = await _get_review_user(db, user_id) - if user.status != UserStatus.PENDING: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="仅允许审核待审核用户") - user = await user_crud.reject_user(db, user, admin_id=current_user.id) - return user diff --git a/backend/app/api/v1/auth.py b/backend/app/api/v1/auth.py index a25cb3cc..d177b48d 100644 --- a/backend/app/api/v1/auth.py +++ b/backend/app/api/v1/auth.py @@ -116,8 +116,8 @@ async def register( existing = await user_crud.get_by_email(db, payload.email) if existing: raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="邮箱已注册") - await user_crud.create_pending_user(db, payload) - return {"message": "注册成功,等待管理员审核"} + await user_crud.create_registered_user(db, payload) + return {"message": "注册成功,请登录"} @router.get("/login-key", response_model=LoginKeyResponse) diff --git a/backend/app/api/v1/router.py b/backend/app/api/v1/router.py index 76ae40e2..8d65b234 100644 --- a/backend/app/api/v1/router.py +++ b/backend/app/api/v1/router.py @@ -1,11 +1,10 @@ from fastapi import APIRouter -from app.api.v1 import auth, users, admin_users, studies, sites, members, attachments, audit_logs, dashboard, subjects, visits, aes, finance_dashboard, fees_contracts, drug_shipments, material_equipments, project_milestones, startup, precautions, subject_histories, subject_pds, study_subject_pds, faq_categories, faqs, documents, etmf, overview, notifications, monitoring_visit_issues, api_permissions, permission_monitoring, permission_templates, system_permissions, study_active_roles +from app.api.v1 import auth, users, studies, sites, members, attachments, audit_logs, dashboard, subjects, visits, aes, finance_dashboard, fees_contracts, drug_shipments, material_equipments, project_milestones, startup, precautions, subject_histories, subject_pds, study_subject_pds, faq_categories, faqs, documents, etmf, overview, notifications, monitoring_visit_issues, api_permissions, permission_monitoring, permission_templates, system_permissions, study_active_roles api_router = APIRouter() api_router.include_router(auth.router, prefix="/auth", tags=["auth"]) -api_router.include_router(admin_users.router, prefix="/admin", tags=["admin"]) api_router.include_router(users.router, prefix="/users", tags=["users"]) api_router.include_router(studies.router, prefix="/studies", tags=["studies"]) api_router.include_router(overview.router, prefix="/studies/{study_id}", tags=["overview"]) diff --git a/backend/app/api/v1/users.py b/backend/app/api/v1/users.py index 99b16bed..dec364aa 100644 --- a/backend/app/api/v1/users.py +++ b/backend/app/api/v1/users.py @@ -1,6 +1,7 @@ import uuid from fastapi import APIRouter, Depends, HTTPException, Query, status +from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession from app.core.deps import get_db_session, is_system_admin, require_roles @@ -97,4 +98,16 @@ async def delete_user( status_code=status.HTTP_400_BAD_REQUEST, detail="该账号仍在项目成员中,请先在项目成员配置中移除后再删除", ) - await user_crud.delete_user(db, db_user) + if await user_crud.user_has_retained_history(db, db_user.id): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="该账号已有审计或权限访问记录,请停用账号以保留历史追溯", + ) + try: + await user_crud.delete_user(db, db_user) + except IntegrityError as exc: + await db.rollback() + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="该账号已有业务记录,请停用账号以保留历史追溯", + ) from exc diff --git a/backend/app/crud/user.py b/backend/app/crud/user.py index f3fec7d3..e792a6b5 100644 --- a/backend/app/crud/user.py +++ b/backend/app/crud/user.py @@ -13,6 +13,8 @@ from app.core.config import ( PROTECTED_ADMIN_FULL_NAME, ) from app.core.security import hash_password +from app.models.audit_log import AuditLog +from app.models.permission_access_log import PermissionAccessLog from app.models.study_member import StudyMember from app.models.user import User, UserStatus from app.schemas.user import UserCreate, UserRegisterRequest, UserUpdate @@ -53,8 +55,8 @@ async def create_user( return user -async def create_pending_user(db: AsyncSession, user_in: UserRegisterRequest) -> User: - return await create_user(db, UserCreate(**user_in.model_dump()), status=UserStatus.PENDING) +async def create_registered_user(db: AsyncSession, user_in: UserRegisterRequest) -> User: + return await create_user(db, UserCreate(**user_in.model_dump()), status=UserStatus.ACTIVE) async def update_user(db: AsyncSession, user: User, user_in: UserUpdate) -> User: @@ -150,6 +152,19 @@ async def count_active_admins(db: AsyncSession) -> int: return int(result.scalar_one() or 0) +async def user_has_retained_history(db: AsyncSession, user_id: uuid.UUID) -> bool: + audit_count = await db.scalar( + select(func.count()).select_from(AuditLog).where(AuditLog.operator_id == user_id) + ) + if int(audit_count or 0) > 0: + return True + + permission_log_count = await db.scalar( + select(func.count()).select_from(PermissionAccessLog).where(PermissionAccessLog.user_id == user_id) + ) + return int(permission_log_count or 0) > 0 + + async def list_users_by_status( db: AsyncSession, status: UserStatus | None = None, skip: int = 0, limit: int = 100 ) -> Sequence[User]: @@ -189,25 +204,3 @@ async def delete_user(db: AsyncSession, user: User) -> None: await db.execute(delete(StudyMember).where(StudyMember.user_id == user.id)) await db.delete(user) await db.commit() - - -async def approve_user(db: AsyncSession, user: User, admin_id: uuid.UUID) -> User: - await db.execute( - update(User) - .where(User.id == user.id) - .values(status=UserStatus.ACTIVE, approved_by=admin_id, approved_at=func.now()) - ) - await db.commit() - await db.refresh(user) - return user - - -async def reject_user(db: AsyncSession, user: User, admin_id: uuid.UUID) -> User: - await db.execute( - update(User) - .where(User.id == user.id) - .values(status=UserStatus.REJECTED, approved_by=admin_id, approved_at=func.now()) - ) - await db.commit() - await db.refresh(user) - return user diff --git a/backend/app/schemas/user.py b/backend/app/schemas/user.py index 25a3ba36..f4b42bc2 100644 --- a/backend/app/schemas/user.py +++ b/backend/app/schemas/user.py @@ -78,15 +78,6 @@ class UserResponse(UserRead): pass -class UserAdminReviewRequest(BaseModel): - action: Literal["approve", "reject"] - - -class AdminUserListResponse(BaseModel): - items: list[UserResponse] - total: int - - class UserSelfUpdate(_PasswordValidator): full_name: Optional[str] = None clinical_department: Optional[str] = None diff --git a/frontend/src/api/admin.ts b/frontend/src/api/admin.ts index 353e77d4..e69de29b 100644 --- a/frontend/src/api/admin.ts +++ b/frontend/src/api/admin.ts @@ -1,12 +0,0 @@ -import type { AxiosResponse } from "axios"; -import { apiGet, apiPost } from "./axios"; -import type { AdminUserListResponse, UserInfo, UserStatus } from "../types/api"; - -export const listPendingUsers = (status: UserStatus = "PENDING"): Promise> => - apiGet("/api/v1/admin/users/", { params: { status } }); - -export const approveUser = (userId: string): Promise> => - apiPost(`/api/v1/admin/users/${userId}/approve`, { action: "approve" }); - -export const rejectUser = (userId: string): Promise> => - apiPost(`/api/v1/admin/users/${userId}/reject`, { action: "reject" }); diff --git a/frontend/src/api/users.ts b/frontend/src/api/users.ts index b3f416f2..fdbcef2f 100644 --- a/frontend/src/api/users.ts +++ b/frontend/src/api/users.ts @@ -20,4 +20,4 @@ export const updateUser = ( apiPatch(`/api/v1/users/${userId}`, payload); export const deleteUser = (userId: string) => - apiDelete(`/api/v1/users/${userId}`); + apiDelete(`/api/v1/users/${userId}`, { suppressErrorMessage: true }); diff --git a/frontend/src/content/authProtocol.ts b/frontend/src/content/authProtocol.ts index 85bf866a..eea379c9 100644 --- a/frontend/src/content/authProtocol.ts +++ b/frontend/src/content/authProtocol.ts @@ -52,7 +52,7 @@ export const privacyPolicySections: AuthProtocolSection[] = [ { title: "2. 数据使用与最小必要", paragraphs: [ - "平台仅在账号注册审核、身份识别、权限分配、项目协同、风险追踪、审计核查和系统安全维护所需范围内使用相关数据。", + "平台仅在账号注册、身份识别、权限分配、项目协同、风险追踪、审计核查和系统安全维护所需范围内使用相关数据。", "用户应遵循最小必要原则录入、查看、导出和传播数据,不得上传与当前项目管理无关的个人信息、受试者信息或其他敏感资料。", ], }, diff --git a/frontend/src/locales/zh-CN.ts b/frontend/src/locales/zh-CN.ts index 338998bc..366ce370 100644 --- a/frontend/src/locales/zh-CN.ts +++ b/frontend/src/locales/zh-CN.ts @@ -329,9 +329,9 @@ export const TEXT = { footer: "© 2025 CTMS Enterprise. 专业临床研究团队支持。", authFailed: "认证失败,请检查您的凭据", registerTitle: "创建账号", - registerDesc: "提交后需管理员审核通过方可登录", - registerButton: "提交注册", - registerSuccess: "注册成功,等待管理员审核", + registerDesc: "完成邮箱验证后即可创建可用账号", + registerButton: "完成注册", + registerSuccess: "注册成功,请登录", backToLogin: "已有账号?返回登录", passwordRuleMin: "密码至少 8 位", passwordRuleMix: "需包含字母和数字", @@ -775,18 +775,6 @@ export const TEXT = { siteRequired: "中心级目录需选择分中心", }, }, - adminUserApproval: { - title: "注册审核", - subtitle: "仅显示待审核账号,审核通过后即可登录", - createdAt: "注册时间", - approve: "审核通过", - reject: "拒绝", - loadFailed: "加载待审核用户失败", - approveSuccess: "已通过审核", - approveFailed: "审核失败", - rejectSuccess: "已拒绝", - rejectFailed: "拒绝失败", - }, adminUsers: { title: "账号管理(系统登录账号)", subtitle: "账号用于登录系统,本身不具备项目或角色权限", @@ -994,9 +982,7 @@ export const TEXT = { CTA: "CTA", }, userStatus: { - PENDING: "待审核", ACTIVE: "启用", - REJECTED: "已拒绝", DISABLED: "已禁用", }, subjectStatus: { diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 45d1eca8..121495e2 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -14,7 +14,6 @@ import StudyHome from "../views/StudyHome.vue"; import FaqDetail from "../views/FaqDetail.vue"; import AuditLogs from "../views/admin/AuditLogs.vue"; import AdminUsers from "../views/admin/Users.vue"; -import AdminUserApproval from "../views/admin/AdminUserApproval.vue"; import AdminProjects from "../views/admin/Projects.vue"; import AdminSites from "../views/admin/Sites.vue"; import PermissionManagement from "../views/admin/PermissionManagement.vue"; @@ -334,12 +333,6 @@ const routes: RouteRecordRaw[] = [ component: AdminUsers, meta: { title: TEXT.menu.accountManagement, requiresAdmin: true }, }, - { - path: "user-approval", - name: "AdminUserApproval", - component: AdminUserApproval, - meta: { title: TEXT.modules.adminUserApproval.title, requiresAdmin: true }, - }, { path: "projects", name: "AdminProjects", diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue index 577b9f0f..5b78e099 100644 --- a/frontend/src/views/Login.vue +++ b/frontend/src/views/Login.vue @@ -300,7 +300,7 @@ const onSubmit = async () => { } else if (status === 429) { loginError.value = { title: "操作过于频繁", message: "登录尝试次数过多,请稍后再试" }; } else if (status === 403 || detail.includes("未激活") || detail.includes("审核") || detail.includes("pending")) { - loginError.value = { title: "账号待审核", message: "您的账号尚未通过管理员审批,请耐心等待或联系管理员" }; + loginError.value = { title: "账号不可用", message: "请联系系统管理员确认账号状态" }; } else if (detail.includes("锁定") || detail.includes("locked") || detail.includes("disabled")) { loginError.value = { title: "账号已被锁定", message: "请联系系统管理员解锁账号" }; } else if (status === 401) { diff --git a/frontend/src/views/Register.vue b/frontend/src/views/Register.vue index 2ff2665a..96d16cec 100644 --- a/frontend/src/views/Register.vue +++ b/frontend/src/views/Register.vue @@ -109,9 +109,9 @@ -

注册申请已提交

-

待管理员审核通过后方可登录系统。

-

管理员审核通过前,请勿重复提交注册申请。

+

注册成功

+

账号已启用,可以立即登录系统。

+

请使用刚注册的邮箱和密码登录。

diff --git a/frontend/src/views/admin/AdminUserApproval.vue b/frontend/src/views/admin/AdminUserApproval.vue deleted file mode 100644 index 97a6e2a1..00000000 --- a/frontend/src/views/admin/AdminUserApproval.vue +++ /dev/null @@ -1,405 +0,0 @@ - - - - - diff --git a/frontend/src/views/admin/Users.vue b/frontend/src/views/admin/Users.vue index cdecea62..1885bdf8 100644 --- a/frontend/src/views/admin/Users.vue +++ b/frontend/src/views/admin/Users.vue @@ -29,18 +29,6 @@ {{ TEXT.enums.userStatus.ACTIVE }} -
-
- - - - -
-
- {{ pendingCount }} - {{ TEXT.enums.userStatus.PENDING }} -
-
@@ -70,9 +58,7 @@
- -
@@ -189,23 +175,11 @@ const auth = useAuthStore(); let keywordSearchTimer: ReturnType | null = null; const activeCount = computed(() => allUsers.value.filter(u => u.status === 'ACTIVE').length); -const pendingCount = computed(() => allUsers.value.filter(u => u.status === 'PENDING').length); const disabledCount = computed(() => allUsers.value.filter(u => u.status === 'DISABLED').length); -const statusType = (status: string) => { - switch (status) { - case "ACTIVE": return "success"; - case "PENDING": return "warning"; - case "REJECTED": return "danger"; - default: return "info"; - } -}; - const statusLabel = (status: string) => { switch (status) { case "ACTIVE": return TEXT.enums.userStatus.ACTIVE; - case "PENDING": return TEXT.enums.userStatus.PENDING; - case "REJECTED": return TEXT.enums.userStatus.REJECTED; case "DISABLED": return TEXT.enums.userStatus.DISABLED; default: return status || TEXT.common.fallback; } @@ -378,7 +352,7 @@ onBeforeUnmount(() => {