# 盤點 ②：專案成員角色制與流程綁定現況

> 2026-10-06 探脈產出，唯讀盤點，供討論稿引用。`$S` = `.venv/lib/python3.11/site-packages`。

## 1. 成員角色資料

- **表與 ORM**：表在 jedi_task_platform 套件內，主專案沒有 model。五張表的 role 欄位定義相同：`String(20)`、default `viewer`。
  - `project_participants`：主鍵 project_id + user_id。`$S/jedi_task_platform/participant/infra/model/project_participant.py`
  - `control_group_participants`：project_id + group_id + user_id
  - `project_group_participants`：同上
  - `project_control_participants`：project_id + group_id + control_id + user_id
  - `process_participants`：project_id + process_id + user_id
- **Enum 有兩份，內容相同**：`common/enum/participant_enum.py:12-18` 和 `$S/jedi_task_platform/participant/common/participant_enum.py:10-14`。註解寫明角色是「決定系統行為（通知、待辦）」。
  - manager：收「待管理」，可管理參與人員
  - reviewer：收「待審核」通知
  - auditor：收「待稽核判定」
  - viewer：純瀏覽，沒有待辦
- **角色字串也硬寫在**：`app/oscal/service/excel_parser/validators.py:9`、`$S/jedi_task_platform/participant/api/serializers/project_participant.py:13,18`、`process_participant.py:15,20`、`$S/jedi_compliance_audit/api/serializers/project.py:77`、`$S/jedi_task_platform/task/app/service/task_execution_service.py:30`。建專案時建立者自動補為 manager：`app/project/service/project_start_app_service.py:166-176`。

## 2. 硬編角色判定清單

欄位說明：「fb」代表走 control→group→project 回退；「精確」代表只認專案層那一列。M=manager、R=reviewer、A=auditor、V=viewer、Any=任一參與者。

| 檔:行 | 守的動作 | 允許角色 | 查法 |
|---|---|---|---|
| **定義** common/authz/project.py:28,43 | assert_project_manager 本體 | M | fb |
| common/authz/project.py:47,72 | assert_project_role_fallback 本體 | allowed_roles | fb |
| common/authz/project.py:75,110 | assert_project_role 本體 | allowed_roles（預設 M） | 精確 |
| common/authz/project.py:118 | assert_project_participant 本體 | Any | fb |
| common/authz/workflow.py:32 | workflow 參與者守門（沒有登入情境就放行） | Any | 專案層 |
| common/authz/workflow.py:50,71 | 完成／退回任務 | M 或該任務被指派人 | 專案層 |
| common/authz/ssp.py:91-132 | SspPermissionChecker 的 require_participant / require_manager（寫入還要求 AP 可編輯） | Any / M | 精確 |
| **flow_control** app/flow_control/service/project_service.py:383 | 更新專案 | M | fb |
| project_service.py:578-582 | 刪除專案：非成員回 404；是否 manager 決定能否刪 | owner 或 M | fb+精確 |
| project_service.py:629 | 批次刪除專案 | owner 或 M | 精確 |
| project_service.py:676-679 | 專案啟動通知裡的角色標籤（不是守門） | — | — |
| job_service.py:150（呼叫點 212/261/337） | 新增／刪除／更新任務 | M | fb |
| job_service.py:159（呼叫點 300） | 列任務 | Any | fb |
| job_service.py:185 | 取單一任務 | Any | fb |
| job_batch_complete_service.py:60-61 | 批次完成任務：manager 可略過被指派人檢查 | Any 可進，M 不限指派 | fb |
| assessment_plan_app_service.py:236（呼叫點 209/549/561/592/706） | AP 編輯：審查控制項、受評對象、tasks、parties | A、M | 精確 |
| job_import_service.py:120（呼叫點 477） | 確認匯入任務 | M | 精確 |
| job_import_service.py:135 | 匯入前檢查計畫屬於本專案 | Any | fb |
| project_read_guard.py:22 | 專案讀取守門 | Any | fb |
| workflow_xml_sync_service.py:142-157（呼叫點 96） | 流程編輯器存檔（會增刪任務） | M | fb |
| **project_summary_report** project_summary_report_history_service.py:33（呼叫點 41/78） | 報告歷史讀取、還原 | Any | fb |
| project_summary_report_service.py:45（呼叫點 53/115） | 建立／讀取摘要報告 | Any | fb |
| **cloud_integration** drive_sync_admin_service.py:150（呼叫點 136） | 初始化專案雲端資料夾 | M | 精確 |
| drive_project_verify_service.py:160 | 驗證雲端硬碟 | M | 精確 |
| **oscal（SSP）** ssp_document_pool_service.py:59,149,187 | 讀文件池 | Any | 精確 |
| ssp_document_pool_service.py:65,119,157,164,195,202 | 寫文件池 | M＋AP 可編輯 | 精確 |
| ssp_inventory_items_app_service.py:66 / 75,102,128 | inventory 讀 / 寫 | Any / M | 精確 |
| ssp_resources_app_service.py:33 / 38,45,51 | resources 讀 / 寫 | Any / M | 精確 |
| ssp_system_characteristic_app_service.py:46 / 54 | 系統特性讀 / 寫 | Any / M | 精確 |
| ssp_leveraged_app_service.py:67 / 75,102,132 | leveraged 讀 / 寫 | Any / M | 精確 |
| ssp_components_app_service.py:60 / 71,103,133 | components 讀 / 寫 | Any / M | 精確 |
| ssp_party_app_service.py:69 / 78,99,115 | parties 讀 / 寫 | Any / M | 精確 |
| ssp_control_implementation_service.py:71,96,157,911 / 79,846,866,883,889,924 | 控制實作讀 / 寫 | Any / M | 精確 |
| ssp_control_impl_import_service.py:104,220,336 / 379 | 控制實作匯出、驗證、預覽 / 寫入 | Any / M | 精確 |
| ssp_excel_import_app_service.py:798 / 788 | Excel 匯入預覽 / 寫入 | Any / M | 精確 |
| ssp_docx_import_app_service.py:604 / 594 | docx 匯入預覽 / 寫入 | Any / M | 精確 |
| export/ssp_export_app_service.py:80 | SSP 匯出 | Any | 精確 |
| module_frame/service/ssp_import_template_app_service.py:283 | 為 SSP 產生匯入範本 | Any | 精確 |
| **flow_engine** stage_advance_service.py:93 | 讀階段 banner | Any（四值全列） | 精確 |
| stage_advance_service.py:130 | banner 上的「可推進」旗標 | main_roles 或 M | 精確 |
| stage_advance_service.py:307 | 推進階段 | main_roles 或 M | 精確 |
| stage_advance_service.py:318 | 強制推進（force，略過前置條件） | M | 精確 |
| stage_rollback_service.py:96 | 階段回退 | M | 精確 |
| job_force_start_service.py:115（呼叫點 60） | 強制開始任務：預檢 | Any | fb |
| job_force_start_service.py:127（呼叫點 67） | 強制開始任務：執行 | M | fb |
| workflow_execution_service.py:132-140 | 依流程實例反查專案後驗參與者 | Any | 專案層 |
| workflow_execution_service.py:142-146 → job_operator_guard.py:39,46 | 任務完成／退回 | M 或被指派人 | 專案層 |
| job_evidence_service.py:61,82 | 列佐證 | Any | 專案層 |
| job_evidence_service.py:97,163,188 | 新增／修改／刪除佐證 | Any | 專案層 |
| **core/plugins** _file_ownership.py:175 | 輪次檔案存取 | A、M | 精確 |
| _file_ownership.py:277 / 279 | 任務檔案寫 / 讀 | M 或被指派人 / Any | 專案層 |
| _file_ownership.py:310-312 | 證據分類批次檔案寫 / 讀 | M / Any | 精確 |
| file_upload.py:124 / 132 | 佐證檔案讀 / 刪 | Any / M 或被指派人 | 專案層 |
| evidence_classification.py:79-88 | is_project_manager | M | 精確 |
| evidence_classification.py:90-97 | is_any_project_manager（任一專案是 manager） | M | 精確 |
| evidence_classification.py:101-113 | is_project_participant | Any | fb |
| compliance_audit.py:80-93、participant.py:91-107、infra/survey/adapters.py:46-65 | 套件守門 adapter，轉發給上面的 canonical | — | — |
| **$S/jedi_compliance_audit** review_service.py:14,50（呼叫點 89/102/116/130） | 標記／取消已審閱（控制項、AO） | M、R | fb |
| audit_service.py:42（呼叫點 97/108/130/144） | 判定、新增／修改／刪除 finding | A、M | 精確 |
| audit_round_app_service.py:132（呼叫點 240/411/567/823/855/880） | 建輪次、發動稽核、結案、三種回退 | M | 精確 |
| audit_round_app_service.py:132（呼叫點 472/504/613） | 開始稽核、定案、發動複驗 | A、M | 精確 |
| audit_round_app_service.py:139 | 輪次讀取 | Any | 精確 |
| assessment_result_app_service.py:113 / 120 | AR 寫 / 讀 | A、M / Any | 精確 |
| poam_app_service.py:92 / 98 | POA&M 寫 / 讀 | M / Any | 精確 |
| **$S/jedi_task_platform** project_participant_service.py:96,118,137 | 新增／修改／刪除參與者 | M | fb |
| project_participant_service.py:49,161 | 列參與者、選單 | Any | fb |
| project_control_participant_service.py:44,66 | 控制項參與者選單、繼承清單 | Any | fb |
| task_assignee_service.py:102,188,224 | 新增／修改／刪除任務指派 | M | fb（含 control_id） |
| task_assignee_service.py:88,93 | 讀指派 | Any | fb / 專案層 |
| task_execution_service.py:57（呼叫點 72） | 開始任務執行 | M | 精確 |
| task_execution_service.py:147 | 準備階段任務進度讀取 | Any | 精確 |
| job_comment_service.py:45 | 任務留言 | Any | 專案層 |
| **$S/jedi_survey** task_survey_guard.py:58（writer：question_answer_service.py:120,255,407、question_answer_history_service.py:82） | 填答、存檔、修改答案、還原歷史 | 被指派人或 M | 專案層 |
| task_survey_guard.py:77 | 問卷讀取 | Any | 專案層 |
| task_survey_guard.py:87（task_survey_service.py:112,296） | 更新問卷、批次匯入題目 | M | fb |
| **$S/jedi_evidence_classification** evidence_batch_service.py:1476（呼叫點 412/513/542/552/577/669/732/839/996） | 建批次、上傳、刪檔、封存、歸檔、清除、刪批次、寫 run 狀態、分類 | M | 精確 |
| **$S/jedi_detection** detection_orchestration_service.py:209,913,969,1061,1090,1159,1189,1532 / 1736 | 偵測任務操作 / 讀 | M 或被指派人 / Any | 專案層 |

## 3. 流程引擎綁定

- **main_role 存在哪**：不是獨立欄位，是 BPMN XML 裡 UserTask 的 camunda extension property `name="main_role"`。值可以是單一角色，也可以逗號分隔多個。
  - 設計器範本：`compliance.flow_templates.bpmn_xml`（text）。DEV 現有 manager 35、auditor 12、reviewer 11。
  - 實例範本：`compliance.workflow_templates.xml`（text）。DEV 現有 manager 2250、auditor 2165、reviewer 7。
  - 值域只有 manager / reviewer / auditor，沒看到 viewer。
- **`stage_objects.default_main_roles`**：`compliance.stage_objects`，JSONB，ORM 在 `infra/flow_engine/models/stage_object.py:26`。DEV 現值：

  | code | default_main_roles |
  |---|---|
  | ap_authoring | ["auditor"] |
  | audit | ["auditor"] |
  | planning | ["manager"] |
  | poam | ["manager"] |
  | review | ["reviewer"] |
  | task_execution | ["manager"] |

- **`_resolve_main_roles`**（`app/flow_engine/service/stage_advance_service.py:584-591`）：先讀當前 UserTask properties 的 `main_role`，有值就逗號切分；沒值才退回 `stage_object.default_main_roles`。比對對象是 `_get_user_role`（`:593-601`），只查專案層 participant 那一列，不做 fb。manager 一律可以覆寫（`:130`、`:307`）。
- **FE 選角色的地方**：`compliance-manager-fe/src/views/flow-template/FlowTemplateEditorView.vue` `:125-130` roleOptions 寫死四值（含 viewer）；`:277` 讀 main_role；`:371-377` 未設定時自動帶入 stage 的 `default_main_roles[0]`；`:384` 寫回 XML；`:589-602` 存檔驗證每個 UserTask 必有 main_role；`:1111-1119` 選單 UI。`FlowTemplatePreviewDialog.vue:64-77,168` 預覽顯示角色。`service/StageService.js:24` API 回傳 `main_roles`。

## 4. 通知／待辦依角色決定對象

| 檔:行 | 用途 | 角色 |
|---|---|---|
| infra/readmodel/tasks/job_batch_complete_query.py:232（get_batch_notification_data） | 批次完成後寄「請審閱」 | M、R |
| infra/readmodel/detection/detection_job_notify_query.py:150（_resolve_targets） | 偵測任務完成通知 | M、R |
| infra/readmodel/audit/auditor_dashboard_query.py:58（get_auditor_ap_list） | 我的稽核待辦清單 | A、M |
| app/flow_control/service/project_service.py:659-700（_notify_project_started） | 專案啟動通知：發給所有參與者，角色只當信中標籤 | 全部 |
| $S/jedi_task_platform/participant/app/service/participant_role_service.py:86-140（get_reviewers_for_task） | 找控制項審核人，順序 control→group→project | R |

`get_reviewers_for_task` 在主專案沒有找到呼叫點。

## 5. 進行中的流程實例

- 會快照，而且是整份 XML 快照。每個 workflow_execution 透過 `template_id` 指向一列自己專屬的 `workflow_templates`（帶 `source_template_uid`，從設計器範本複製出來），main_role 就在那列的 `xml` 裡。
- 實例上的 `workflow_template_xml` 是指向 `workflow_template.xml` 的 association_proxy（`$S/jedi_flow_engine/infra/models/workflow_execution.py:57`），stage 推進時從這裡讀（`stage_advance_service.py:562`）。
- DEV 數量：PROCESSING 1150 筆，對應 1150 份各自獨立的範本；TODO 59427 筆；COMPLETED 67 筆。
- `default_main_roles` 不會快照，每次即時讀 stage_objects。
- 結論：翻譯 main_role 時必須連 `workflow_templates.xml`（實例那一份）一起改，或者在 `_resolve_main_roles` 做讀取時轉換。只改 `flow_templates.bpmn_xml` 不會影響已建立的實例。

## 6. control→group→project 回退

一句話：判斷某人在某個控制項或群組上的角色時，先找控制項層的指派，沒有就找群組層，再沒有才用專案層；回傳找到的第一個角色，所以下層指派會蓋過上層。

- 實作：`$S/jedi_task_platform/participant/app/service/participant_role_service.py:34-85`（get_user_role）。
- `assert_project_manager`、`assert_project_role_fallback`、`assert_project_participant` 都走這一套；只有傳了 group_id / control_id 才會往下查（`common/authz/project.py:28-44`）。
- `assert_project_role` 只認專案層那一列，不回退（`common/authz/project.py:75-90` 有註明兩者不可互相冒充）。
- 守門裡實際有傳 control_id 的，只看到 task_assignee_service。
