From 41bd423be00ef3c0cbcd5674798194369c46228f Mon Sep 17 00:00:00 2001 From: Cheng Zhou Date: Tue, 26 May 2026 14:46:01 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=9F=E4=B8=80=E9=A1=B9=E7=9B=AE=E8=A7=92?= =?UTF-8?q?=E8=89=B2=E5=B1=95=E7=A4=BA=E4=B8=8E=E9=A2=84=E8=AE=BE=E6=96=87?= =?UTF-8?q?=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../20260514_01_add_permission_templates.py | 20 ++--- .../versions/20260521_01_remove_qa_role.py | 4 +- .../20260522_01_update_role_template_copy.py | 58 +++++++++++++++ backend/app/api/v1/sites.py | 2 +- .../tests/test_migrated_endpoints_batch3.py | 12 +-- backend/tests/test_migration_safety.py | 20 +++++ backend/tests/test_permission_templates.py | 2 +- .../components/ApiEndpointPermissions.test.ts | 20 +++++ .../src/components/ApiEndpointPermissions.vue | 5 +- .../components/PermissionAccessLogs.test.ts | 12 +++ .../src/components/PermissionAccessLogs.vue | 24 ++---- .../PermissionTemplateSelector.test.ts | 4 + .../components/PermissionTemplateSelector.vue | 11 +-- .../composables/useRoleTemplateMeta.test.ts | 21 ++++++ .../src/composables/useRoleTemplateMeta.ts | 74 +++++++++++++++++++ frontend/src/content/authProtocol.ts | 4 +- frontend/src/locales/zh-CN.ts | 6 +- frontend/src/views/StudyHome.test.ts | 17 +++++ frontend/src/views/StudyHome.vue | 6 +- .../views/admin/PermissionManagement.test.ts | 43 ++++++++--- .../src/views/admin/PermissionManagement.vue | 62 ++++++---------- .../src/views/admin/ProjectMembers.test.ts | 13 +++- frontend/src/views/admin/ProjectMembers.vue | 36 +++++---- .../views/documents/DocumentDetail.test.ts | 20 +++++ .../src/views/documents/DocumentDetail.vue | 10 ++- 25 files changed, 384 insertions(+), 122 deletions(-) create mode 100644 backend/alembic/versions/20260522_01_update_role_template_copy.py create mode 100644 frontend/src/composables/useRoleTemplateMeta.test.ts create mode 100644 frontend/src/composables/useRoleTemplateMeta.ts create mode 100644 frontend/src/views/StudyHome.test.ts create mode 100644 frontend/src/views/documents/DocumentDetail.test.ts diff --git a/backend/alembic/versions/20260514_01_add_permission_templates.py b/backend/alembic/versions/20260514_01_add_permission_templates.py index f64ce94a..a526f8ee 100644 --- a/backend/alembic/versions/20260514_01_add_permission_templates.py +++ b/backend/alembic/versions/20260514_01_add_permission_templates.py @@ -88,40 +88,40 @@ def _insert_system_templates() -> None: templates = [ ( "3078efe6-60d6-4623-b7b3-862aac0084cd", - "项目经理", - "项目管理员,拥有所有权限", + "PM", + "项目负责人,统筹项目全局,协调进度、资源与关键决策。", "PM", "PM", '{"PM": {"subjects:create": true, "subjects:list": true, "subjects:read": true, "subjects:update": true, "subjects:delete": true, "visits:create": true, "visits:list": true, "visits:read": true, "visits:update": true, "visits:delete": true, "risk_issues:create": true, "risk_issues:list": true, "risk_issues:read": true, "risk_issues:update": true, "risk_issues:delete": true, "finance_contracts:create": true, "finance_contracts:list": true, "finance_contracts:read": true, "finance_contracts:update": true, "finance_contracts:delete": true, "fees_contracts:create": true, "fees_contracts:list": true, "fees_contracts:read": true, "fees_contracts:update": true, "fees_contracts:delete": true, "fees_payments:create": true, "fees_payments:update": true, "fees_payments:delete": true, "project_members:create": true, "project_members:list": true, "project_members:candidates": true, "project_members:update": true, "project_members:delete": true, "sites:create": true, "sites:list": true, "sites:read": true, "sites:update": true, "sites:delete": true, "ethics:create": true, "ethics:list": true, "ethics:read": true, "ethics:update": true, "ethics:delete": true, "feasibility:create": true, "feasibility:list": true, "feasibility:read": true, "feasibility:update": true, "feasibility:delete": true, "budget:create": true, "budget:list": true, "budget:read": true, "budget:update": true, "budget:delete": true, "timeline:create": true, "timeline:list": true, "timeline:read": true, "timeline:update": true, "timeline:delete": true, "permissions:read": true, "permissions:update": true, "overview:read": true, "monitoring_issues:create": true, "monitoring_issues:list": true, "monitoring_issues:read": true, "monitoring_issues:update": true, "monitoring_issues:delete": true, "monitoring_issues:close": true, "monitoring_issues:history": true, "monitoring_audit:create": true, "monitoring_audit:read": true, "monitoring_audit:update": true, "monitoring_audit:delete": true, "drug_shipments:create": true, "drug_shipments:list": true, "drug_shipments:read": true, "drug_shipments:update": true, "drug_shipments:delete": true, "materials:create": true, "materials:list": true, "materials:read": true, "materials:update": true, "materials:delete": true, "subject_pds:create": true, "subject_pds:list": true, "subject_pds:read": true, "subject_pds:update": true, "audit_logs:list": true, "audit_logs:read": true, "audit_logs:export": true, "knowledge_notes:create": true, "knowledge_notes:list": true, "knowledge_notes:read": true, "knowledge_notes:update": true, "knowledge_notes:delete": true, "subject_history:list": true, "subject_history:read": true, "subject_history:timeline": true, "subject_history:export": true, "subject_history:search": true, "milestones:list": true, "milestones:update": true, "attachments:create": true, "attachments:read": true, "attachments:update": true, "attachments:delete": true, "fees_attachments:create": true, "fees_attachments:read": true, "fees_attachments:delete": true, "faq:create": true, "faq:read": true, "faq:update": true, "faq:delete": true, "faq_category:create": true, "faq_category:read": true, "faq_category:update": true, "faq_category:delete": true, "faq_reply:create": true, "faq_reply:delete": true, "dashboard:read": true, "subject_histories:create": true, "subject_histories:list": true, "subject_histories:read": true, "subject_histories:update": true, "subject_histories:delete": true, "material_equipments:create": true, "material_equipments:list": true, "material_equipments:read": true, "material_equipments:update": true, "material_equipments:delete": true, "documents:create": true, "documents:read": true, "documents:update": true, "documents:delete": true}}', ), ( "a17e6fd5-f0c6-4723-b166-6c25a595e28d", - "临床研究协调员", - "数据输入和日常管理人员", + "CRA", + "负责各中心临床监查执行,跟进现场质量、数据和问题闭环。", "CRA", "CRA", '{"CRA": {"subjects:create": true, "subjects:list": true, "subjects:read": true, "subjects:update": true, "subjects:delete": false, "visits:create": true, "visits:list": true, "visits:read": true, "visits:update": true, "visits:delete": false, "risk_issues:create": true, "risk_issues:list": true, "risk_issues:read": true, "risk_issues:update": true, "risk_issues:delete": false, "finance_contracts:create": false, "finance_contracts:list": true, "finance_contracts:read": true, "finance_contracts:update": false, "finance_contracts:delete": false, "fees_contracts:create": false, "fees_contracts:list": true, "fees_contracts:read": true, "fees_contracts:update": false, "fees_contracts:delete": false, "fees_payments:create": false, "fees_payments:update": false, "fees_payments:delete": false, "project_members:create": false, "project_members:list": false, "project_members:candidates": false, "project_members:update": false, "project_members:delete": false, "sites:create": false, "sites:list": true, "sites:read": true, "sites:update": false, "sites:delete": false, "ethics:create": false, "ethics:list": true, "ethics:read": true, "ethics:update": false, "ethics:delete": false, "feasibility:create": false, "feasibility:list": true, "feasibility:read": true, "feasibility:update": false, "feasibility:delete": false, "budget:create": false, "budget:list": true, "budget:read": true, "budget:update": false, "budget:delete": false, "timeline:create": false, "timeline:list": true, "timeline:read": true, "timeline:update": false, "timeline:delete": false, "permissions:read": false, "permissions:update": false, "overview:read": true, "monitoring_issues:create": true, "monitoring_issues:list": true, "monitoring_issues:read": true, "monitoring_issues:update": true, "monitoring_issues:delete": false, "monitoring_issues:close": true, "monitoring_issues:history": true, "monitoring_audit:create": true, "monitoring_audit:read": true, "monitoring_audit:update": true, "monitoring_audit:delete": false, "drug_shipments:create": false, "drug_shipments:list": true, "drug_shipments:read": true, "drug_shipments:update": false, "drug_shipments:delete": false, "materials:create": false, "materials:list": true, "materials:read": true, "materials:update": false, "materials:delete": false, "subject_pds:create": true, "subject_pds:list": true, "subject_pds:read": true, "subject_pds:update": true, "audit_logs:list": false, "audit_logs:read": false, "audit_logs:export": false, "knowledge_notes:create": true, "knowledge_notes:list": true, "knowledge_notes:read": true, "knowledge_notes:update": true, "knowledge_notes:delete": false, "subject_history:list": true, "subject_history:read": true, "subject_history:timeline": true, "subject_history:export": true, "subject_history:search": true, "milestones:list": true, "milestones:update": false, "attachments:create": true, "attachments:read": true, "attachments:update": true, "attachments:delete": true, "fees_attachments:create": true, "fees_attachments:read": true, "fees_attachments:delete": true, "faq:create": false, "faq:read": true, "faq:update": false, "faq:delete": false, "faq_category:create": false, "faq_category:read": true, "faq_category:update": false, "faq_category:delete": false, "faq_reply:create": true, "faq_reply:delete": false, "dashboard:read": true, "subject_histories:create": true, "subject_histories:list": true, "subject_histories:read": true, "subject_histories:update": true, "subject_histories:delete": false, "material_equipments:create": true, "material_equipments:list": true, "material_equipments:read": true, "material_equipments:update": true, "material_equipments:delete": false, "documents:create": true, "documents:read": true, "documents:update": true, "documents:delete": false}}', ), ( "f45fbfda-faed-4b83-b4a8-24ca4011894b", - "访视员", - "访视和参与者管理人员", + "PV", + "负责药物警戒相关工作,跟踪安全性事件并支持风险评估。", "PV", "PV", '{"PV": {"subjects:create": false, "subjects:list": true, "subjects:read": true, "subjects:update": false, "subjects:delete": false, "visits:create": false, "visits:list": true, "visits:read": true, "visits:update": false, "visits:delete": false, "risk_issues:create": true, "risk_issues:list": true, "risk_issues:read": true, "risk_issues:update": true, "risk_issues:delete": false, "finance_contracts:create": false, "finance_contracts:list": false, "finance_contracts:read": false, "finance_contracts:update": false, "finance_contracts:delete": false, "fees_contracts:create": false, "fees_contracts:list": false, "fees_contracts:read": false, "fees_contracts:update": false, "fees_contracts:delete": false, "fees_payments:create": false, "fees_payments:update": false, "fees_payments:delete": false, "project_members:create": false, "project_members:list": false, "project_members:candidates": false, "project_members:update": false, "project_members:delete": false, "sites:create": false, "sites:list": true, "sites:read": true, "sites:update": false, "sites:delete": false, "ethics:create": false, "ethics:list": true, "ethics:read": true, "ethics:update": false, "ethics:delete": false, "feasibility:create": false, "feasibility:list": true, "feasibility:read": true, "feasibility:update": false, "feasibility:delete": false, "budget:create": false, "budget:list": false, "budget:read": false, "budget:update": false, "budget:delete": false, "timeline:create": false, "timeline:list": true, "timeline:read": true, "timeline:update": false, "timeline:delete": false, "permissions:read": false, "permissions:update": false, "overview:read": true, "monitoring_issues:create": false, "monitoring_issues:list": true, "monitoring_issues:read": true, "monitoring_issues:update": false, "monitoring_issues:delete": false, "monitoring_issues:close": false, "monitoring_issues:history": true, "monitoring_audit:create": true, "monitoring_audit:read": true, "monitoring_audit:update": true, "monitoring_audit:delete": false, "drug_shipments:create": false, "drug_shipments:list": false, "drug_shipments:read": false, "drug_shipments:update": false, "drug_shipments:delete": false, "materials:create": false, "materials:list": false, "materials:read": false, "materials:update": false, "materials:delete": false, "subject_pds:create": false, "subject_pds:list": true, "subject_pds:read": true, "subject_pds:update": false, "audit_logs:list": false, "audit_logs:read": false, "audit_logs:export": false, "knowledge_notes:create": true, "knowledge_notes:list": true, "knowledge_notes:read": true, "knowledge_notes:update": true, "knowledge_notes:delete": false, "subject_history:list": true, "subject_history:read": true, "subject_history:timeline": true, "subject_history:export": false, "subject_history:search": true, "milestones:list": true, "milestones:update": false, "attachments:create": true, "attachments:read": true, "attachments:update": false, "attachments:delete": false, "fees_attachments:create": false, "fees_attachments:read": true, "fees_attachments:delete": false, "faq:create": false, "faq:read": true, "faq:update": false, "faq:delete": false, "faq_category:create": false, "faq_category:read": true, "faq_category:update": false, "faq_category:delete": false, "faq_reply:create": true, "faq_reply:delete": false, "dashboard:read": true, "subject_histories:create": false, "subject_histories:list": true, "subject_histories:read": true, "subject_histories:update": false, "subject_histories:delete": false, "material_equipments:create": false, "material_equipments:list": true, "material_equipments:read": true, "material_equipments:update": false, "material_equipments:delete": false, "documents:create": false, "documents:read": true, "documents:update": false, "documents:delete": false}}', ), ( "b43d06e4-2ee2-4e80-b733-85964d738f40", - "医学审核", - "医学审核人员", + "QA", + "负责医学审核与稽查,关注质量风险、合规性和医学一致性。", "MEDICAL_REVIEW", "MEDICAL_REVIEW", '{"MEDICAL_REVIEW": {"subjects:create": false, "subjects:list": true, "subjects:read": true, "subjects:update": false, "subjects:delete": false, "visits:create": false, "visits:list": true, "visits:read": true, "visits:update": false, "visits:delete": false, "risk_issues:create": true, "risk_issues:list": true, "risk_issues:read": true, "risk_issues:update": true, "risk_issues:delete": false, "finance_contracts:create": false, "finance_contracts:list": false, "finance_contracts:read": false, "finance_contracts:update": false, "finance_contracts:delete": false, "fees_contracts:create": false, "fees_contracts:list": false, "fees_contracts:read": false, "fees_contracts:update": false, "fees_contracts:delete": false, "fees_payments:create": false, "fees_payments:update": false, "fees_payments:delete": false, "project_members:create": false, "project_members:list": false, "project_members:candidates": false, "project_members:update": false, "project_members:delete": false, "sites:create": false, "sites:list": true, "sites:read": true, "sites:update": false, "sites:delete": false, "ethics:create": false, "ethics:list": true, "ethics:read": true, "ethics:update": false, "ethics:delete": false, "feasibility:create": false, "feasibility:list": true, "feasibility:read": true, "feasibility:update": false, "feasibility:delete": false, "budget:create": false, "budget:list": false, "budget:read": false, "budget:update": false, "budget:delete": false, "timeline:create": false, "timeline:list": false, "timeline:read": false, "timeline:update": false, "timeline:delete": false, "permissions:read": false, "permissions:update": false, "overview:read": true, "monitoring_issues:create": false, "monitoring_issues:list": true, "monitoring_issues:read": true, "monitoring_issues:update": false, "monitoring_issues:delete": false, "monitoring_issues:close": false, "monitoring_issues:history": true, "monitoring_audit:create": true, "monitoring_audit:read": true, "monitoring_audit:update": true, "monitoring_audit:delete": false, "drug_shipments:create": false, "drug_shipments:list": false, "drug_shipments:read": false, "drug_shipments:update": false, "drug_shipments:delete": false, "materials:create": false, "materials:list": false, "materials:read": false, "materials:update": false, "materials:delete": false, "subject_pds:create": false, "subject_pds:list": true, "subject_pds:read": true, "subject_pds:update": false, "audit_logs:list": false, "audit_logs:read": false, "audit_logs:export": false, "knowledge_notes:create": true, "knowledge_notes:list": true, "knowledge_notes:read": true, "knowledge_notes:update": true, "knowledge_notes:delete": false, "subject_history:list": true, "subject_history:read": true, "subject_history:timeline": true, "subject_history:export": false, "subject_history:search": true, "milestones:list": true, "milestones:update": false, "attachments:create": false, "attachments:read": true, "attachments:update": false, "attachments:delete": false, "fees_attachments:create": false, "fees_attachments:read": false, "fees_attachments:delete": false, "faq:create": false, "faq:read": true, "faq:update": false, "faq:delete": false, "faq_category:create": false, "faq_category:read": true, "faq_category:update": false, "faq_category:delete": false, "faq_reply:create": true, "faq_reply:delete": false, "dashboard:read": true, "subject_histories:create": false, "subject_histories:list": true, "subject_histories:read": true, "subject_histories:update": false, "subject_histories:delete": false, "material_equipments:create": false, "material_equipments:list": true, "material_equipments:read": true, "material_equipments:update": false, "material_equipments:delete": false, "documents:create": false, "documents:read": true, "documents:update": false, "documents:delete": false}}', ), ( "1e38cffb-a7f2-4243-a31c-1a4f8ce66a05", - "物资管理员", - "物资和设备管理人员", + "CTA", + "负责合同、药品及相关项目事务管理,保障执行支持与物资协同。", "IMP", "IMP", '{"IMP": {"subjects:create": false, "subjects:list": true, "subjects:read": true, "subjects:update": false, "subjects:delete": false, "visits:create": false, "visits:list": true, "visits:read": true, "visits:update": false, "visits:delete": false, "risk_issues:create": false, "risk_issues:list": true, "risk_issues:read": true, "risk_issues:update": false, "risk_issues:delete": false, "finance_contracts:create": false, "finance_contracts:list": true, "finance_contracts:read": true, "finance_contracts:update": false, "finance_contracts:delete": false, "fees_contracts:create": false, "fees_contracts:list": true, "fees_contracts:read": true, "fees_contracts:update": false, "fees_contracts:delete": false, "fees_payments:create": false, "fees_payments:update": false, "fees_payments:delete": false, "project_members:create": false, "project_members:list": false, "project_members:candidates": false, "project_members:update": false, "project_members:delete": false, "sites:create": false, "sites:list": true, "sites:read": true, "sites:update": false, "sites:delete": false, "ethics:create": false, "ethics:list": false, "ethics:read": false, "ethics:update": false, "ethics:delete": false, "feasibility:create": false, "feasibility:list": false, "feasibility:read": false, "feasibility:update": false, "feasibility:delete": false, "budget:create": false, "budget:list": true, "budget:read": true, "budget:update": false, "budget:delete": false, "timeline:create": false, "timeline:list": false, "timeline:read": false, "timeline:update": false, "timeline:delete": false, "permissions:read": false, "permissions:update": false, "overview:read": true, "monitoring_issues:create": false, "monitoring_issues:list": false, "monitoring_issues:read": false, "monitoring_issues:update": false, "monitoring_issues:delete": false, "monitoring_issues:close": false, "monitoring_issues:history": false, "monitoring_audit:create": false, "monitoring_audit:read": false, "monitoring_audit:update": false, "monitoring_audit:delete": false, "drug_shipments:create": true, "drug_shipments:list": true, "drug_shipments:read": true, "drug_shipments:update": true, "drug_shipments:delete": false, "materials:create": true, "materials:list": true, "materials:read": true, "materials:update": true, "materials:delete": false, "subject_pds:create": false, "subject_pds:list": false, "subject_pds:read": false, "subject_pds:update": false, "audit_logs:list": false, "audit_logs:read": false, "audit_logs:export": false, "knowledge_notes:create": false, "knowledge_notes:list": true, "knowledge_notes:read": true, "knowledge_notes:update": false, "knowledge_notes:delete": false, "subject_history:list": false, "subject_history:read": false, "subject_history:timeline": false, "subject_history:export": false, "subject_history:search": false, "milestones:list": true, "milestones:update": false, "attachments:create": false, "attachments:read": true, "attachments:update": false, "attachments:delete": false, "fees_attachments:create": false, "fees_attachments:read": false, "fees_attachments:delete": false, "faq:create": false, "faq:read": true, "faq:update": false, "faq:delete": false, "faq_category:create": false, "faq_category:read": true, "faq_category:update": false, "faq_category:delete": false, "faq_reply:create": false, "faq_reply:delete": false, "dashboard:read": true, "subject_histories:create": false, "subject_histories:list": true, "subject_histories:read": true, "subject_histories:update": false, "subject_histories:delete": false, "material_equipments:create": false, "material_equipments:list": true, "material_equipments:read": true, "material_equipments:update": false, "material_equipments:delete": false, "documents:create": false, "documents:read": true, "documents:update": false, "documents:delete": false}}', diff --git a/backend/alembic/versions/20260521_01_remove_qa_role.py b/backend/alembic/versions/20260521_01_remove_qa_role.py index 1407e4ff..6e6cc8b1 100644 --- a/backend/alembic/versions/20260521_01_remove_qa_role.py +++ b/backend/alembic/versions/20260521_01_remove_qa_role.py @@ -41,14 +41,14 @@ def upgrade() -> None: DELETE FROM permission_template_versions WHERE template_id IN ( SELECT id FROM permission_templates - WHERE category = 'QA' OR recommended_roles = 'QA' OR permissions ? 'QA' + WHERE category = 'QA' OR recommended_roles = 'QA' OR (permissions::jsonb ? 'QA') ) """ ) op.execute( """ DELETE FROM permission_templates - WHERE category = 'QA' OR recommended_roles = 'QA' OR permissions ? 'QA' + WHERE category = 'QA' OR recommended_roles = 'QA' OR (permissions::jsonb ? 'QA') """ ) op.execute( diff --git a/backend/alembic/versions/20260522_01_update_role_template_copy.py b/backend/alembic/versions/20260522_01_update_role_template_copy.py new file mode 100644 index 00000000..37cf5c58 --- /dev/null +++ b/backend/alembic/versions/20260522_01_update_role_template_copy.py @@ -0,0 +1,58 @@ +"""update role template copy + +Revision ID: 20260522_01 +Revises: 20260521_01 +Create Date: 2026-05-22 09:00:00.000000 + +""" + +from typing import Sequence, Union + +from alembic import op + + +revision: str = "20260522_01" +down_revision: Union[str, None] = "20260521_01" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +ROLE_TEMPLATE_COPY = { + "PM": ("PM", "项目负责人,统筹项目全局,协调进度、资源与关键决策。"), + "CRA": ("CRA", "负责各中心临床监查执行,跟进现场质量、数据和问题闭环。"), + "IMP": ("CTA", "负责合同、药品及相关项目事务管理,保障执行支持与物资协同。"), + "MEDICAL_REVIEW": ("QA", "负责医学审核与稽查,关注质量风险、合规性和医学一致性。"), + "PV": ("PV", "负责药物警戒相关工作,跟踪安全性事件并支持风险评估。"), +} + +PREVIOUS_ROLE_TEMPLATE_COPY = { + "PM": ("项目经理", "项目管理员,拥有所有权限"), + "CRA": ("临床研究协调员", "数据输入和日常管理人员"), + "IMP": ("物资管理员", "物资和设备管理人员"), + "MEDICAL_REVIEW": ("医学审核", "医学审核人员"), + "PV": ("访视员", "访视和参与者管理人员"), +} + + +def _update_role_copy(category: str, name: str, description: str) -> None: + op.execute( + f""" + UPDATE permission_templates + SET name = '{name}', + description = '{description}', + updated_at = NOW() + WHERE category = '{category}' + AND template_type = 'ROLE' + AND is_system = true + """ + ) + + +def upgrade() -> None: + for category, (name, description) in ROLE_TEMPLATE_COPY.items(): + _update_role_copy(category, name, description) + + +def downgrade() -> None: + for category, (name, description) in PREVIOUS_ROLE_TEMPLATE_COPY.items(): + _update_role_copy(category, name, description) diff --git a/backend/app/api/v1/sites.py b/backend/app/api/v1/sites.py index a9f1d6a8..e061a26f 100644 --- a/backend/app/api/v1/sites.py +++ b/backend/app/api/v1/sites.py @@ -79,7 +79,7 @@ async def create_site( module="sites", action="read", description="查询中心列表", - default_roles=["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP", "QA"], + default_roles=["PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"], ) async def list_sites( study_id: uuid.UUID, diff --git a/backend/tests/test_migrated_endpoints_batch3.py b/backend/tests/test_migrated_endpoints_batch3.py index 1fb55d7e..a831593d 100644 --- a/backend/tests/test_migrated_endpoints_batch3.py +++ b/backend/tests/test_migrated_endpoints_batch3.py @@ -317,35 +317,35 @@ async def test_subject_pds_list_with_permission(db_session: AsyncSession): @pytest.mark.asyncio async def test_audit_logs_list_with_permission(db_session: AsyncSession): - """验证有权限的QA可以查询审计日志列表""" + """验证有权限的PM可以查询审计日志列表""" study_id = uuid.uuid4() perm = ApiEndpointPermission( study_id=study_id, - role="QA", + role="PM", endpoint_key="GET:/studies/{study_id}/audit-logs", allowed=True, ) db_session.add(perm) await db_session.commit() - allowed = await role_has_api_permission(db_session, study_id, "QA", "GET:/studies/{study_id}/audit-logs") + allowed = await role_has_api_permission(db_session, study_id, "PM", "GET:/studies/{study_id}/audit-logs") assert allowed is True @pytest.mark.asyncio async def test_audit_logs_export_with_permission(db_session: AsyncSession): - """验证有权限的QA可以导出审计日志""" + """验证有权限的PM可以导出审计日志""" study_id = uuid.uuid4() perm = ApiEndpointPermission( study_id=study_id, - role="QA", + role="PM", endpoint_key="POST:/studies/{study_id}/audit-logs/export", allowed=True, ) db_session.add(perm) await db_session.commit() - allowed = await role_has_api_permission(db_session, study_id, "QA", "POST:/studies/{study_id}/audit-logs/export") + allowed = await role_has_api_permission(db_session, study_id, "PM", "POST:/studies/{study_id}/audit-logs/export") assert allowed is True diff --git a/backend/tests/test_migration_safety.py b/backend/tests/test_migration_safety.py index 66a08db2..9c2621cc 100644 --- a/backend/tests/test_migration_safety.py +++ b/backend/tests/test_migration_safety.py @@ -24,3 +24,23 @@ def test_migration_state_check_script_exists(): assert "studies" in source assert "subjects" in source assert "monitoring_visit_issues" in source + + +def test_remove_qa_role_migration_casts_json_permissions_for_key_lookup(): + source = Path("alembic/versions/20260521_01_remove_qa_role.py").read_text(encoding="utf-8") + + assert "permissions ? 'QA'" not in source + assert "permissions::jsonb ? 'QA'" in source + + +def test_role_template_copy_migration_updates_display_copy_without_qa_role_key(): + source = Path("alembic/versions/20260522_01_update_role_template_copy.py").read_text(encoding="utf-8") + + assert "项目负责人,统筹项目全局,协调进度、资源与关键决策。" in source + assert "负责各中心临床监查执行,跟进现场质量、数据和问题闭环。" in source + assert "负责合同、药品及相关项目事务管理,保障执行支持与物资协同。" in source + assert "负责医学审核与稽查,关注质量风险、合规性和医学一致性。" in source + assert "负责药物警戒相关工作,跟踪安全性事件并支持风险评估。" in source + assert '"IMP": ("CTA"' in source + assert '"MEDICAL_REVIEW": ("QA"' in source + assert "category = 'QA'" not in source diff --git a/backend/tests/test_permission_templates.py b/backend/tests/test_permission_templates.py index 6912dd72..58651f83 100644 --- a/backend/tests/test_permission_templates.py +++ b/backend/tests/test_permission_templates.py @@ -166,7 +166,7 @@ async def test_list_templates_excludes_qa_system_preset(db_session): permissions={"QA": {"subjects:read": True}}, ) pm_template = PermissionTemplate( - name="项目经理", + name="PM", template_type=TemplateType.ROLE, is_system=True, category="PM", diff --git a/frontend/src/components/ApiEndpointPermissions.test.ts b/frontend/src/components/ApiEndpointPermissions.test.ts index fe54c5d1..62ba4928 100644 --- a/frontend/src/components/ApiEndpointPermissions.test.ts +++ b/frontend/src/components/ApiEndpointPermissions.test.ts @@ -8,6 +8,15 @@ import type { ApiEndpointPermissionsResponse } from "@/types/api"; vi.mock("@/api/projectPermissions", () => ({ fetchApiOperations: vi.fn().mockResolvedValue({ data: { operations: [] } }), + fetchPermissionTemplates: vi.fn().mockResolvedValue({ + data: [ + { category: "PM", name: "PM", description: "项目负责人,统筹项目全局,协调进度、资源与关键决策。" }, + { category: "CRA", name: "CRA", description: "负责各中心临床监查执行,跟进现场质量、数据和问题闭环。" }, + { category: "IMP", name: "CTA", description: "负责合同、药品及相关项目事务管理,保障执行支持与物资协同。" }, + { category: "MEDICAL_REVIEW", name: "QA", description: "负责医学审核与稽查,关注质量风险、合规性和医学一致性。" }, + { category: "PV", name: "PV", description: "负责药物警戒相关工作,跟踪安全性事件并支持风险评估。" }, + ], + }), })); describe("ApiEndpointPermissions.vue", () => { @@ -70,6 +79,17 @@ describe("ApiEndpointPermissions.vue", () => { expect(source).toContain("updatedMatrix[role][operation_key] = allowed"); }); + it("renders role headers from template display names while keeping matrix role keys", () => { + const source = readFileSync(resolve(__dirname, "./ApiEndpointPermissions.vue"), "utf8"); + + expect(source).toContain("useRoleTemplateMeta"); + expect(source).toContain(':label="roleLabel(role)"'); + expect(source).toContain(':key="role"'); + expect(source).toContain("isOperationAllowed(row.operation_key, role)"); + expect(source).toContain("onPermissionChange(row.operation_key, role, val)"); + expect(source).not.toContain(':label="role"'); + }); + it("filters endpoints by search text", async () => { const wrapper = mount(ApiEndpointPermissions, { props: { diff --git a/frontend/src/components/ApiEndpointPermissions.vue b/frontend/src/components/ApiEndpointPermissions.vue index 4164d1d5..e4edab56 100644 --- a/frontend/src/components/ApiEndpointPermissions.vue +++ b/frontend/src/components/ApiEndpointPermissions.vue @@ -55,7 +55,7 @@ @@ -81,6 +81,7 @@ import { fetchApiOperations } from "@/api/projectPermissions"; import { isApiPermissionAllowed } from "@/utils/apiPermissionValue"; import { useAuthStore } from "@/store/auth"; import { isSystemAdmin } from "@/utils/roles"; +import { useRoleTemplateMeta } from "@/composables/useRoleTemplateMeta"; interface Operation { operation_key: string; @@ -103,6 +104,7 @@ const props = defineProps(); const emit = defineEmits(); const auth = useAuthStore(); +const { roleLabel, loadRoleTemplates } = useRoleTemplateMeta(); const isAdmin = computed(() => isSystemAdmin(auth.user)); const isRoleEditable = (role: string) => { if (role === "ADMIN") return false; @@ -267,6 +269,7 @@ const onPermissionChange = (operation_key: string, role: string, allowed: boolea onMounted(() => { loadOperations(); + loadRoleTemplates(); }); defineExpose({ searchText }); diff --git a/frontend/src/components/PermissionAccessLogs.test.ts b/frontend/src/components/PermissionAccessLogs.test.ts index f70228db..7d86b488 100644 --- a/frontend/src/components/PermissionAccessLogs.test.ts +++ b/frontend/src/components/PermissionAccessLogs.test.ts @@ -36,6 +36,18 @@ describe("PermissionAccessLogs", () => { expect(source).not.toContain('QA: "QA"'); }); + it("uses permission template names for role filters and log lines", () => { + const source = readSource(); + + expect(source).toContain("useRoleTemplateMeta"); + expect(source).toContain("const { roleLabel, roleOptionsFor, loadRoleTemplates } = useRoleTemplateMeta();"); + expect(source).toContain("const roleFilterOptions = computed(() => roleOptionsFor(ROLE_FILTER_KEYS));"); + expect(source).toContain('v-for="role in roleFilterOptions"'); + expect(source).toContain("role=${roleLabel(row.role)}"); + expect(source).not.toContain("const ROLE_LABELS"); + expect(source).not.toContain('label="医学审核" value="MEDICAL_REVIEW"'); + }); + it("uses narrow metric cards without helper subtitles", () => { const source = readSource(); diff --git a/frontend/src/components/PermissionAccessLogs.vue b/frontend/src/components/PermissionAccessLogs.vue index c369ea1f..ee4572e6 100644 --- a/frontend/src/components/PermissionAccessLogs.vue +++ b/frontend/src/components/PermissionAccessLogs.vue @@ -12,12 +12,7 @@ @change="onFilterChange" /> - - - - - - + @@ -157,11 +152,13 @@ import type { SecurityAccessLogsResponse, } from "@/types/api"; import { Search as SearchIcon } from "@element-plus/icons-vue"; +import { useRoleTemplateMeta } from "@/composables/useRoleTemplateMeta"; const props = withDefaults(defineProps<{ showSecurityLog?: boolean }>(), { showSecurityLog: false, }); const showSecurityLog = computed(() => props.showSecurityLog); +const { roleLabel, roleOptionsFor, loadRoleTemplates } = useRoleTemplateMeta(); const MetricIconVisit = () => h("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, [ h("path", { d: "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" }), @@ -222,14 +219,8 @@ const filters = reactive({ allowed: undefined as boolean | undefined, }); -const ROLE_LABELS: Record = { - ADMIN: "管理员", - PM: "项目负责人", - CRA: "CRA", - PV: "PV", - MEDICAL_REVIEW: "医学审核", - IMP: "药品管理", -}; +const ROLE_FILTER_KEYS = ["ADMIN", "PM", "CRA", "PV", "MEDICAL_REVIEW", "IMP"]; +const roleFilterOptions = computed(() => roleOptionsFor(ROLE_FILTER_KEYS)); const SECURITY_AUTH_LABELS: Record = { ANONYMOUS: "匿名", @@ -243,8 +234,6 @@ const formatTerminalTime = (iso: string) => { return `${date.getFullYear()}/${pad(date.getMonth() + 1)}/${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`; }; -const roleLabel = (role: string) => ROLE_LABELS[role] || role; - const formatIpLocation = (row: AccessLogItem) => { const parts = [row.ip_province, row.ip_city, row.ip_isp].filter(Boolean); return parts.length ? parts.join(" / ") : row.ip_location; @@ -437,7 +426,8 @@ const downloadSecurityLog = () => { downloadLogFile("security-event-audit.log", securityTerminalLines.value); }; -onMounted(() => { +onMounted(async () => { + await loadRoleTemplates(); refresh(); startRealtimePolling(); }); diff --git a/frontend/src/components/PermissionTemplateSelector.test.ts b/frontend/src/components/PermissionTemplateSelector.test.ts index 2e7c9cf1..976b11cf 100644 --- a/frontend/src/components/PermissionTemplateSelector.test.ts +++ b/frontend/src/components/PermissionTemplateSelector.test.ts @@ -20,6 +20,10 @@ describe("PermissionTemplateSelector", () => { it("reads current project permissions by role key, not by role label", () => { const source = readSource(); + expect(source).toContain('{{ template.name }}'); + expect(source).toContain("refreshKey?: number;"); + expect(source).toContain("watch(() => props.refreshKey, loadTemplates);"); + expect(source).not.toContain("ROLE_LABELS"); expect(source).toContain("template.category && props.currentPermissions[template.category]"); expect(source).toContain("enabledPercent(template.category)"); expect(source).toContain("countCurrentEnabled(template.category)"); diff --git a/frontend/src/components/PermissionTemplateSelector.vue b/frontend/src/components/PermissionTemplateSelector.vue index db2bf526..7c3f2515 100644 --- a/frontend/src/components/PermissionTemplateSelector.vue +++ b/frontend/src/components/PermissionTemplateSelector.vue @@ -19,7 +19,7 @@
{{ roleIcon(template.category) }}
- {{ template.category ? (ROLE_LABELS[template.category] || template.name) : template.name }} + {{ template.name }} {{ template.description }}
自定义 @@ -62,7 +62,7 @@