FR-112 · 任務真相收回規劃頁、流程圖降為走向設計 · 需求討論稿 · 2026-09-19(v1 · 待審)

任務在規劃頁上開,流程圖只管先後順序

目前要替一個查核項目多開一個任務,唯一的辦法是打開流程圖編輯器、從工具列拖一個方塊出來——PM 不會用、也不該被要求會用。本案把「新增任務」放回專案規劃頁,並把新開的任務預設接成平行(誰先做都行、全部做完這個查核項目才算過);流程圖編輯器降級為純粹的「走向設計工具」,只管先後與分支,不再是填任務資料的地方。

方向已拍板 4 項(決策者 2026-09-19) 待決策 D1–D6 跨 3 repo:jedi-flow-engine/BE/FE 前身:2026-06-17 雙向同步診斷(未拍板) 🔴 實查發現引擎缺口:合流閘道不等兄弟分支
§1

分工概述(30 秒版)

整案一句話:任務的所有資料以專案規劃頁為準存進資料庫,流程圖只留「先後與分支」這一件事;規劃頁補上「新增任務」按鈕,新任務預設掛成平行分支,該查核項目要全部任務做完才算通過。

給非工程讀者的三個名詞:

  • 查核項目(AO):一條法規控制項底下再細分的檢查點,規劃頁左邊樹狀清單點到的最小單位。每個查核項目底下掛一串任務。
  • 流程圖(BPMN):用方塊與箭頭畫出來的工作流程。方塊叫 userTask(一個方塊=一個任務),箭頭表示先後順序,菱形的叫「閘道」——平行閘道表示「這幾條同時開跑」、互斥閘道表示「二選一」。
  • 任務資料表(job_executions):資料庫裡真正存任務的地方——名稱、描述、負責人、狀態、證據、問卷、檢測工具設定都在這裡。
階段 做什麼 產出 完成怎麼判定(決策者親手檢查法)
① 流程圖改圖工具 在共用套件裡補三支能力:把流程重排成標準平行結構、在合流閘道前掛一條新分支、合流閘道要等所有兄弟分支都完成才放行 jedi-flow-engine 新增/修正的函式 + 單元測試 跑套件測試:給一張「兩條平行分支、合流後還有一個任務」的流程圖,完成第一條分支後,合流後的任務不可以變成進行中;兩條都完成才可以
② 後端新增/刪除任務 新增任務端點不再要求前端先給流程圖節點編號——後端自己在流程圖上補節點、接線、回寫任務編號;刪除任務時把節點從流程圖上拿掉並把線接回去 BE app service + repo 改動 + 存檔驗證 用 Swagger 直接打新增任務端點(不帶 job_element_uid)→ 回 200;去資料庫看該查核項目的流程圖 XML,多了一個方塊、且方塊上帶著新任務的編號
③ 規劃頁按鈕 任務區塊補「新增任務」與「刪除任務」;新任務展開就是既有的任務表單 FE ProjectPlanningView.vue 規劃頁點任一查核項目 → 按「新增任務」→ 填名稱存檔 → 重新整理仍在 → 打開流程圖編輯器,看得到這個新方塊,而且是與原任務並排(平行)不是串在後面
④ 流程圖編輯器瘦身 屬性面板只留「名稱」;存檔不再由前端逐一打新增/更新任務端點(改由後端存檔時處理) FE WorkflowSetupEditor.vue 打開編輯器點一個方塊 → 右邊只剩名稱欄,沒有任務類型/問卷/設備/部門;拖一個新方塊出來存檔 → 回規劃頁看得到一筆新的空任務(名稱是你在圖上打的那個)
⑤ 驗收與回歸 專案啟動、輪次推進、批次完成、任務佇列四條既有動線在平行結構下重跑 e2e 回歸(測試 repo) 起一個新專案 → 規劃頁加兩個任務 → 兩個都完成 → 該查核項目變「已完成」;只完成一個時不可以變完成

依賴:①是②的地基(②要呼叫①補的改圖函式);③④在②完成後可平行;⑤等③④都驗收過才做。


§2

為什麼要做這件事

現在的樣子

專案規劃頁(ProjectPlanningView.vue)的任務區塊可以改任務的每一個欄位——名稱、描述、指南、類型、負責人、部門、設備、問卷、檢測工具參數,存檔走 saveTask()(FE ProjectPlanningView.vue:1772)。唯獨不能新增,也不能刪除——實查該檔案,找不到任何 addTask / deleteTask 之類的函式。

要多開一個任務,PM 得:打開流程圖編輯器(openBpmnEditor(),同檔 :1923,開新分頁到 /workflow/workflow-setup)→ 從左邊工具列拖一個方塊出來 → 手動接線 → 在右邊屬性面板填一堆欄位 → 存檔。

🔴 這條動線對 PM 是不合理的要求

流程圖編輯器是工程師與流程設計者的工具。要 PM 為了「多派一件事給同仁」而去學拖方塊、接箭頭、分辨平行閘道與互斥閘道,等於把工具的複雜度轉嫁給使用者。

過去為什麼會走成這樣:兩份真相在互相覆蓋

任務的同一份資料現在存在兩個地方:

存在哪 存了什麼 誰在寫
資料庫 job_executions(+ task_assignees 等關聯表) 名稱、描述、指南、類型、狀態、時間、負責人、部門、設備、問卷、檢測工具 規劃頁存檔、後端流程引擎
流程圖 XML 的 camunda 屬性 name、workDescription、guide、actionType、actionInfo、devices、departments、toolParams、completionMode、job_execution_uid 流程圖編輯器存檔、後端 _sync_task_to_template_xml 回寫

對照表在 FE docs/guides/grc-page-data-models.md:40-47。

兩邊互相覆蓋的具體路徑(實查程式碼):

  1. 規劃頁 → 流程圖:任務存檔後,後端 _sync_task_to_template_xml()(infra/flow_control/repository/flow_control_job_repo_impl.py:401)把名稱/描述/指南/類型/問卷/工具參數寫回流程圖 XML 的 camunda 屬性。這是單向的,方向正確。
  2. 流程圖 → 規劃頁:編輯器存檔時 onSaveClick()(FE WorkflowSetupEditor.vue:402)先存 XML,再呼叫 syncJobExecutions()(同檔 :489)——逐一走訪圖上每個方塊,有 job_execution_uid 就打 PUT 更新既有任務、沒有就打 POST 新建,然後 patchJobExecutionUids()(同檔 :443)把新任務編號寫回 XML、再存一次 XML、再重新載入圖。

第 2 條是問題所在:編輯器只有五個欄位(name / workDescription / guide / actionType / actionInfo),但它送出的 PUT 會覆蓋規劃頁在同一筆任務上設好的東西。程式碼裡留著防禦註解可以佐證這件事真的發生過——syncJobExecutions() 裡寫著「只在 actionInfo 非空時帶 surveys⋯避免把規劃頁設的問卷誤刪」「不帶 approval_required⋯不誤蓋規劃頁審查旗標」(FE WorkflowSetupEditor.vue:525-531)。

這些防禦註解就是「兩份真相」的病徵

每加一個規劃頁專屬的欄位,就得在編輯器同步邏輯裡補一條「這欄不要蓋」的例外。例外會愈來愈多,而漏掉一條的症狀是資料靜靜消失、沒有錯誤訊息。

歷史文件 docs/analysis/2026-06-17-ao-statement-key-mismatch-and-task-bpmn-sync.md 第二節當時就爬出這個落差,結論停在「需 user 確認任務粒度後再實作雙向同步」,一直沒拍板。本案就是那次待拍板事項的落地——而且方向與當時討論的「雙向同步」相反:不做雙向,改成單一真相。

決策者這次裁定的方向

四條已拍板(2026-09-19)

  1. 規劃頁為主、流程圖為輔。任務的業務資料真相在 job_executions;流程圖 XML 只負責兩件事——拓樸(誰先誰後、誰跟誰平行),以及用 job_execution_uid 屬性指回是哪一筆任務。名稱寫進圖裡只是為了讓圖看得懂,是顯示副本,由後端單向寫入。
  2. 規劃頁加「新增任務」,新增後流程預設改成平行式:開始 → 分叉 → 全部任務並排 → 合流 → 結束。該查核項目要所有任務都完成才算通過。
  3. 流程圖編輯器降為純流程走向設計。可以改線(拉成一條龍、加二選一分支);也保留在圖上拖新方塊——存檔時後端發現有方塊沒對應任務,就建一筆空任務並回寫編號。屬性面板只留名稱,細節回規劃頁填。前端不再自己跑兩次存檔。
  4. 刪除任務由規劃頁做,後端負責從圖上移掉節點並把線接回去。

§3

業界怎麼做

這個「流程圖只描述走向、業務資料另存」的分法不是本案發明的,是流程引擎領域的通則。

Camunda / Flowable(流程引擎)

流程定義(BPMN 檔)是不可變的部署物——部署後有版號,改了就是新版本,已在跑的案子繼續用舊版。真正的業務資料放在「流程變數」與外部業務表,不寫進 BPMN 檔本身。

BPMN 上的 userTask 屬性主要是路由用(誰該收到這件事、什麼條件往哪走),不是拿來存「這個任務的負責人叫什麼、他交了幾份證據」。

對本案的啟示:把任務欄位塞進 camunda 屬性,一開始就偏離了這個工具的設計意圖。

Asana / Jira 這類任務工具

子任務預設彼此不相依——建了就都可以開始做,沒有「要等前一個」這回事。要有先後關係,得另外手動設定「阻擋/被阻擋」。

母任務的完成判定就是「子任務全部完成」。

對本案的啟示:決策者裁的「新增後預設平行、全部完成才算過」與這個直覺一致。使用者建兩個任務時,心裡想的就是「這兩件事都要做」,不是「先做A再做B」。

兩邊合起來就是本案的形狀:任務像 Asana(預設互不相依、全做完才算過),流程圖像 Camunda(只管走向、不存業務資料)。


§4

現況盤點

3涉及 repo

10寫進流程圖的 camunda 屬性

2引擎缺口落點(同一個 bug 兩處)

後端(compliance-manager-be)

元件 現況(實查) 本案動作
api/flow_control/routes/job_route.py:170 ProjectJobCreateResource 新增任務端點還在(POST /grc/project/<uid>/assessment-object/<ao_uid>/jobs),註冊在 api/flow_control/__init__.py:62,117;現在只有流程圖編輯器在呼叫 端點保留,讓它接受不帶節點編號的呼叫
api/flow_control/serializers/job.py:322 job_element_uid 欄位 allow_none=True, load_default=None——schema 本來就允許不帶 不改;改的是下游的 repo
infra/.../flow_control_job_repo_impl.py:673 create_job() 開頭 if not job_element_uid: return None(:698)——沒帶節點編號就默默回 None,路由層轉成 return_response(False, None),前端看不出發生什麼事 🔴 核心改動:沒帶編號時改成「後端自己在圖上插節點」,插完再往下走原邏輯
同檔 create_job() 的冪等防護(:729) 同一流程下同一 template_job_id 已有任務 → 更新後回傳、不重建。防編輯器重複 POST 保留。後端自己插節點時編號必定是新的,不衝突
同檔 create_job() 的初始狀態判定(:750) 該流程下沒有未完成的任務 → 新任務開 PROCESSING;否則 TODO ⚠️ 平行結構下這個判定要重看,見下方風險 R3
同檔 _sync_task_to_template_xml()(:401) 規劃頁存檔後單向回寫圖:定位方式「先找 job_execution_uid 屬性、退而求其次比對節點 id」;屬性不存在會建立;同時寫主表與翻譯表(檔內註解記著只寫主表會讓編輯器永遠讀到舊值) 保留且成為唯一的圖寫入路徑;寫入欄位範圍可收斂(見 D4)
同檔 delete_job()(:779) 刪任務時清五張關聯表,再呼叫 BpmnUtils.remove_user_task_from_xml() 從圖上移節點(註解寫「前端存檔通常已移除,這裡兜底」) 從「兜底」升格為主要路徑——規劃頁刪除就是走這裡
同檔 _compute_can_revert_job()(:350-396) 判「這個任務可不可以退回」:len(all_jobs)==1 直接可以;否則取 get_next_jobs() 的第 0 筆看是不是進行中 ⚠️ 平行結構下「下一個任務」有多筆,只看第 0 筆會誤判,見風險 R4
app/flow_control/service/job_service.py:117 create_job() @transaction,第一件事 _require_manager(project_uid)(:94,走 assert_project_manager);接著處理問卷 snapshot、task_survey 笛卡爾積、檢測工具綁定與參數加密 守門與後續處理都沿用,不動
app/flow_engine/service/workflow_execution_service.py:602 complete_job() 完成任務後用 get_next_jobs() 找下一批。沒有下一批時(:677)檢查整條流程還有沒有非 COMPLETED/CANCEL 的任務,全完成才把流程標 COMPLETED(:687)——已經是「全部完成才通過」的語意 這段正確,不動。要修的是有下一批時的判定(見引擎缺口)
同檔 _assert_round_not_frozen_for_workflow()(:155) 輪次凍結後不可退回任務(目前只擋 revert_job,:794) 新增任務要不要受同一道 gate 擋,是 D1
app/flow_control/service/prep_job_generation_service.py:101 generate() 專案啟動時:每個查核項目 clone 一份流程範本 → 建一個流程實例 → 建一筆任務,節點編號取 _first_user_task_id()(:93,正規表示式抓第一個 userTask 的 id,:54),再 inject_job_execution_uid() 把任務編號注回圖 ⚠️ 「只抓第一個」在單任務範本下正確;若日後範本本身就是多任務,這裡會漏建。本案不改(範本仍是單任務),但要記進風險 R5
app/flow_engine/service/stage_advance_service.py 輪次推進用 _peek_next_stage_code_via_jobs()(:463)+_is_terminal_user_task()(:508)判下一階段 不在本案範圍——它走的是輪次主流程的圖,不是查核項目底下的收證據流程。要在驗收時確認兩者確實不共用同一張圖
app/flow_engine/service/stage_rollback_service.py:183 _find_job_by_stage_code() 靠節點上的階段代碼找任務,與任務數無關 同上,不在範圍
app/flow_control/service/job_batch_complete_service.py:86 批次完成:逐筆取 template_job_id 呼叫 complete_job() 不改程式,但是風險集中點——批次完成會連續觸發引擎缺口,見風險 R1
infra/readmodel/tasks/vw_user_job_queue.py 任務佇列讀模型,只是把 template_job_id 當一般欄位存著(:77),沒有順序假設 不受影響

共用套件(jedi-flow-engine,現版 1.1.2)

元件 現況(實查) 本案動作
bpmn_uilts.py:276 get_next_jobs() 走訪下一批節點:遇到任務直接收;遇到互斥閘道依條件挑一條(比對不中走預設並發 warning);遇到平行閘道(:326)把所有出線對應的任務全部收進來 平行展開已支援,不必改
bpmn_uilts.py:213 get_next_elements() 只看一條出線的目標節點是什麼類型(任務/互斥閘道/平行閘道/子流程) 不改
bpmn_uilts.py:501 append_user_task_to_xml() 靜態方法,在結束事件之前插一個新方塊——docstring 明寫「接線邏輯(線性流程)」,是把 最後節點 → 結束 改成 最後節點 → 新方塊 → 結束 🔴 這支只會串成一條龍,本案要的是掛平行分支 → 需新增函式(見 D2)
bpmn_uilts.py:574 inject_job_execution_uid() 把任務編號寫進指定節點的 camunda 屬性,已存在就覆蓋 直接沿用
bpmn_uilts.py:613 remove_user_task_from_xml() 移除節點並把線接回去(前 → 節點 → 後 變成 前 → 後) 沿用;但平行分支被刪到剩一條時要不要把閘道也收掉,是新增的邏輯
bpmn_generator.py:145 create_bpmn_with_user_tasks() 已經能產標準平行結構:開始 → 分叉閘道 → N 個方塊並排 → 合流閘道 → 結束(:216 起逐一建方塊與連線) 可直接當「重排成標準平行」的參考實作或直接重用
bpmn_uilts.py:391 get_job_execution_order() 深度優先走訪出順序 平行結構下「順序」意義不大,但不會壞
bpmn_uilts.py:447 get_jobs_by_layer() 廣度優先分層 同上

前端(compliance-manager-fe)

元件 現況(實查) 本案動作
ProjectPlanningView.vue:2931 起 任務區塊 逐筆展開面板,欄位齊全;空狀態只顯示「沒有任務」文案,沒有新增按鈕(:2943) 🔴 補「新增任務」;空狀態改成可以從這裡開第一筆
ProjectPlanningView.vue:1772 saveTask() 存單筆任務,含名稱必填、檢測工具必選、逐列參數必填、重複分派檢查 不動;新增任務走同一支存檔
ProjectPlanningView.vue:1923 openBpmnEditor() 開新分頁到 /workflow/workflow-setup,帶範本編號/流程實例編號/專案編號/查核項目編號 保留;按鈕文案可改成「編輯流程走向」以符合新定位
WorkflowSetupEditor.vue:402 onSaveClick() 存 XML → syncJobExecutions() → 有新任務就 patchJobExecutionUids() → 再存一次 XML → 重新載入圖 🔴 砍掉後三步,只留第一步存 XML;新任務由後端在存檔時建
WorkflowSetupEditor.vue:489 syncJobExecutions() 逐節點打 PUT/POST;失敗會 toast(註解記著 CM-1163 曾經整個吞掉) 整支移除
WorkflowSetupEditor.vue:443 patchJobExecutionUids() 用 DOMParser 直接改 XML 字串寫回任務編號 整支移除(改由後端回寫)
WorkflowSetupEditor.vue:635 onSaveTaskProperties() +屬性面板(:1032-1045) 面板有名稱/工作描述/指南/任務類型四欄(另有設備、部門屬性一起寫進圖) 🔴 只留名稱(見 D4)
TaskSetupView.vue:524 也會開 /workflow/workflow-setup 要一起確認改動後行為正常
FlowTemplateEditorView.vue 流程範本管理,是另一套系統 明確不在範圍

§5

目標設計

原則:一件事只有一個地方說了算

這件事 真相在哪 另一邊是什麼
任務叫什麼、描述、指南、類型、負責人、部門、設備、問卷、工具參數、狀態 資料庫 job_executions +關聯表 圖上的 name 是顯示副本,由後端單向寫;其餘屬性不再寫也不再讀
誰先誰後、誰跟誰平行、有沒有二選一分支 流程圖 XML 資料庫不存拓樸
圖上這個方塊是哪一筆任務 圖上的 job_execution_uid 屬性(後端寫) —

🔴 判準一句話:圖上的東西只有「拓樸」和「指回任務的編號」是真的,其他都是印出來給人看的。

任何一次改動只要讓某個欄位「從圖上讀回來當真值用」,就等於把第二份真相請回來。

後端新增任務:兩種入口,同一支端點

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
sequenceDiagram
    participant PM as PM(規劃頁)
    participant API as 後端端點<br/>job_route.py:170
    participant SVC as 任務服務<br/>job_service.py:117
    participant REPO as 任務倉儲<br/>repo_impl.py:673
    participant XML as 流程圖改圖<br/>jedi-flow-engine
    participant DB as 資料庫

    PM->>API: 按「新增任務」(只帶名稱,不帶節點編號)
    API->>SVC: create_job(ao_uid, name, job_element_uid=None)
    SVC->>SVC: 檢查是不是專案 manager
    SVC->>REPO: create_job(...)
    Note over REPO: 現況:沒帶編號就 return None<br/>本案:改成往下走
    REPO->>DB: 查這個查核項目的流程範本與流程實例
    REPO->>XML: 依現有拓樸決定插法<br/>(重排成平行 / 掛新分支)
    XML-->>REPO: 新 XML + 新節點編號
    REPO->>DB: 建 job_executions 一筆
    REPO->>XML: inject_job_execution_uid(新節點, 新任務編號)
    REPO->>DB: 寫回範本 XML(主表+翻譯表)
    REPO-->>SVC: 任務資料
    SVC-->>API: 任務資料
    API-->>PM: 200,任務面板出現、預設展開待填
圖 1 — 規劃頁新增任務的端到端時序

兩種入口走同一支端點,差別只在有沒有帶 job_element_uid:

入口 帶 job_element_uid 嗎 後端做什麼
規劃頁按「新增任務」 不帶 自己在圖上插節點、接線、回寫任務編號
流程圖編輯器拖一個新方塊、存檔 帶(前端存 XML 時圖上已有這個節點) 沿用現況:直接用這個編號建任務、把編號回寫圖上

保留第二種入口是決策者明示的

在圖上拖方塊仍然可以開任務——只是屬性面板只能填名稱,其餘細節回規劃頁補。存檔後後端掃圖,發現沒有對應任務的 userTask 就建一筆空任務並回寫編號。

插節點規則:看現在的圖長什麼樣決定怎麼插

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
flowchart TB
    subgraph A["情境一:只有一個任務(專案啟動後的預設樣子)"]
      direction LR
      A1([開始]) --> A2[任務 1] --> A3([結束])
      A3 -.新增任務 2.-> A4[" "]
      A5([開始]) --> A6{{分叉}}
      A6 --> A7[任務 1]
      A6 --> A8[任務 2 新]
      A7 --> A9{{合流}}
      A8 --> A9
      A9 --> A10([結束])
    end

    subgraph B["情境二:已是標準平行(再加一個)"]
      direction LR
      B1([開始]) --> B2{{分叉}}
      B2 --> B3[任務 1]
      B2 --> B4[任務 2]
      B3 --> B5{{合流}}
      B4 --> B5
      B5 --> B6([結束])
      B6 -.新增任務 3.-> B7[" "]
      B8{{分叉}} --> B9[任務 1]
      B8 --> B10[任務 2]
      B8 --> B11[任務 3 新]
      B9 --> B12{{合流}}
      B10 --> B12
      B11 --> B12
    end

    subgraph C["情境三:使用者手動排成一條龍(不動他的順序)"]
      direction LR
      C1([開始]) --> C2[任務 1] --> C3[任務 2] --> C4{{合流}} --> C5([結束])
      C4 -.新增任務 3.-> C6[" "]
      C7[任務 1] --> C8[任務 2] --> C9{{合流}}
      C10{{分叉}} --> C11[任務 3 新] --> C9
    end
圖 2 — 三種情境下新增任務後的拓樸變化

規則寫成白話:

  1. 圖上只有一個任務(專案啟動後的預設) → 重排成標準平行:開始 → 分叉 → 兩個任務並排 → 合流 → 結束。可直接重用 bpmn_generator.py:145 的產圖邏輯。
  2. 圖上已經是標準平行結構 → 在分叉閘道上多接一條出線、在合流閘道上多接一條入線,新方塊掛中間。
  3. 使用者已經在編輯器手動排成一條龍或加了二選一分支 → 不動他排好的順序,把新任務掛成「合流前的一條新平行分支」。若圖上沒有合流閘道,就在結束事件前補一個。這是 D2,需拍板。

為什麼不是「一律重排成標準平行」

使用者若刻意把任務排成「先做 A 才能做 B」,一按新增就被重排回全平行,等於系統擅自推翻他的設計,而且沒有任何提示。這會是很難解釋的行為。

刪除任務:移節點、接線、必要時收掉閘道

規劃頁按刪除 → 後端 delete_job()(repo_impl.py:778)清關聯表 → 呼叫改圖函式移節點:

  • 一般情形:前 → 節點 → 後 接回 前 → 後(現有 remove_user_task_from_xml() 就在做這件事)。
  • 平行分支被刪到只剩一條時:分叉與合流閘道只剩單進單出,留著不會壞但圖很醜 → 順手把兩個閘道也收掉、接回線性。這是新增邏輯。
  • 刪到一個任務都不剩:圖變成 開始 → 結束。要不要擋(至少留一個任務)屬產品判斷,列在 D6。

流程圖存檔時的驗證

編輯器存 XML 時,後端要在寫進資料庫之前檢查三件事,任一不過就退回並指出哪個節點有問題:

檢查 為什麼 不過的症狀(如果不擋)
圖上每個 job_execution_uid 都查得到對應任務 使用者可能複製貼上方塊,連帶複製了別人的任務編號 兩個方塊指向同一筆任務,完成其中一個另一個跟著變狀態
沒有孤兒任務(資料庫有、圖上沒有對應節點) 使用者在圖上刪了方塊 任務永遠停在待辦、永遠不會被推進,而且看不出原因
每個任務節點從開始事件走得到 使用者接線接錯、方塊沒連上 完成前一個任務時引擎找不到它,流程卡住

孤兒任務的處理方式(刪掉?標成取消?擋下存檔?)需拍板,併入 D6。

編輯器鎖定範圍

還能做 不能做
改線(把平行拉成一條龍、加二選一分支、調整順序) 填任務類型、問卷、設備、部門、工具參數(回規劃頁)
拖新方塊進來(只填名稱) 直接改任務的負責人與狀態
刪方塊 —

屬性面板留哪些欄位是 D4。


§6

🔴 實查發現的引擎缺口

合流閘道不會等兄弟分支——第一條分支完成,合流後面的任務就被啟動

實查 app/flow_engine/service/workflow_execution_service.py:602 complete_job():完成一個任務後拿 get_next_jobs()(:668)得到下一批,接著在 :702 起的 for job in next_jobs: 迴圈裡,把每一個都直接設成 PROCESSING——中間沒有任何「兄弟分支都完成了嗎」的檢查。

同一個缺口在兩個地方:另一支 complete_main_workflow_job()(同檔 :469,輪次推進在用,stage_advance_service.py:382 呼叫)的 :569 迴圈是一模一樣的寫法。兩處都要修,不能只修一處。

而 get_next_jobs()(jedi-flow-engine/.../bpmn_uilts.py:276)遇到平行閘道時(:326)是把該閘道所有出線的任務都收進來——它分不出手上這個閘道是「分叉」還是「合流」,兩者在 BPMN 裡都是 parallelGateway,差別只在入線與出線的數量。

成立條件:合流閘道後面還有 userTask。以本案要產的標準結構(分叉 → N 個任務 → 合流 → 結束)來說,合流後面是結束事件、get_next_jobs() 回空,走的是 :686 那條「檢查整條流程還有沒有未完成任務」的分支——那條是對的,全完成才標 COMPLETED。所以本案的預設結構不會立刻踩到。

但本案會放大它:

  1. 決策者明示編輯器保留改線能力。使用者只要在合流之後接一個「主管覆核」之類的任務,缺口立刻成立——第一個同仁交完證據,覆核任務就跳成進行中,另外兩個同仁根本還沒開始做。
  2. 批次完成(job_batch_complete_service.py:86)會連續呼叫 complete_job(),一次完成多個平行任務,把這個時序放到最大。
  3. 現在之所以沒人回報,是因為現況每個查核項目只有一個任務(prep_job_generation_service.py:170 只抓第一個 userTask 建一筆),平行結構幾乎不存在,缺口沒有被觸發的機會。

要不要在本案一併修,是 D3。


§7

待決策

D1 — 流程啟動後、輪次凍結了,還能不能新增任務?

現在的凍結 gate(workflow_execution_service.py:155 _assert_round_not_frozen_for_workflow)只擋「退回任務」這一個動作(:794),新增任務完全沒有經過它。

要考慮的是:稽核輪次一旦離開規劃期(launch_audit 之後),任務清單就是這一輪的稽核範圍;中途追加任務等於改變已經在跑的稽核範圍,證據完整性與報表都會受影響。但實務上「稽核到一半發現漏了一項」也確實會發生。

建議:本期不可以——新增任務只在規劃期開放,凍結後按鈕停用並顯示原因(照 FR-110 的做法,停用+留住提示,不彈 toast)。「稽核中補任務」牽涉補件流程與報表口徑,另開一期處理。

D2 — 使用者已經把流程排成一條龍,這時在規劃頁新增任務,要掛哪裡?

三個選項:①一律重排成標準平行(推翻使用者排的順序);②掛在最後一個任務後面、繼續串成一條龍(符合「接在後面」的直覺,但與「新增預設平行」的拍板方向相反);③在合流前掛一條新的平行分支(保留使用者排好的順序,新任務與那整條串並行)。

建議:選③。理由是它同時滿足兩件事——不動使用者刻意排的順序,又維持「新增的任務預設不相依」這個拍板原則。圖上沒有合流閘道時,就在結束事件前補一個。

D3 — 合流閘道等待修正要不要納入本案?

缺口本身是既有的,不是本案造成的。但本案讓平行結構從「幾乎不存在」變成「預設樣貌」,並且明示保留改線能力——不修的話,使用者只要在合流後接一個覆核任務就會踩到,而症狀(覆核任務提早跳成進行中)看起來像是資料錯亂,不像流程設定問題,會很難查。

修法方向:complete_job() 把下一批任務設為進行中之前,先判斷這個節點的入線是不是來自合流閘道;是的話檢查該閘道所有入線分支的任務是否都已完成/取消,沒有就跳過不啟動。判斷邏輯適合放在 jedi-flow-engine(那裡才看得到拓樸)。

建議:納入,排在階段①(流程圖改圖工具)一起做。它與改圖函式同屬套件層、同一個人一棒做完最省;分開做則後面那一棒要重新把整張圖的拓樸語意讀一遍。

D4 — 流程圖編輯器的屬性面板留哪些欄位?

現在面板有名稱、工作描述、指南、任務類型四欄(WorkflowSetupEditor.vue:1032-1045),另外還把設備與部門寫進圖的 camunda 屬性。決策者裁定「只留名稱」。

要確認的是:工作描述與指南要不要一起留?留著的好處是在圖上就看得到這個方塊在做什麼;壞處是它們又變成第二份真相,規劃頁改了描述、圖上沒改,兩邊不一致。

建議:只留名稱,照拍板執行。描述與指南從面板拿掉,圖上既有的屬性讀取路徑一併停用。理由是「在圖上看得到描述」這個好處,可以靠後端單向把名稱寫得更完整達成,不值得為它保留一條雙向寫入路徑。

D5 — 既有專案的圖上帶著完整的 camunda 屬性,要不要清掉?

現有專案的流程圖 XML 裡已經有 workDescription / guide / actionType / actionInfo / devices / departments / toolParams / completionMode 這些屬性(repo_impl.py:401 一路寫進去的)。

選項:①寫 migration 掃全部範本清掉這些屬性;②不清,改成讀的時候一律忽略。

建議:不清、讀時忽略。理由有三:一是清除 migration 要改動所有租戶的範本 XML,改壞了沒有回頭路(XML 沒有欄位層級的版本控制);二是這些屬性留著不會造成行為差異——只要沒有任何程式碼讀它,它就只是一段被忽略的文字;三是清除的唯一好處是「看起來乾淨」,不值得承擔改壞既有專案流程的風險。但 _sync_task_to_template_xml() 要收斂成只寫 name,否則舊屬性會被持續刷新、永遠不會自然凋零。

D6 — 邊界情形怎麼處理?

三個小問題併成一項:①一個查核項目可不可以一個任務都沒有(刪到空);②存檔時發現孤兒任務(資料庫有、圖上沒有)要刪掉、標取消還是擋下存檔;③一個查核項目最多幾個任務(要不要設上限)。

建議:①可以是零,規劃頁空狀態本來就已經有畫面了,強制留一個反而會讓「這項不需要做」無法表達;②擋下存檔並指出是哪一筆任務,不自動刪——自動刪掉使用者已經填了半天的任務是不可逆的,而擋下來只是多按一次;③不設上限,但超過一定數量(例如 20)在規劃頁給提示,因為平行分支太多在圖上會擠成一團、難以閱讀。


§8

影響面與風險

編號 風險 實查結果 嚴重度 怎麼處理
R1 合流閘道不等兄弟分支(見上節) ✅ 成立,workflow_execution_service.py:702 與 :569 兩處迴圈皆無等待檢查;bpmn_uilts.py:326 平行閘道無條件展開全部出線 高(本案放大) 見 D3
R2 批次完成連續觸發引擎缺口 ✅ job_batch_complete_service.py:86 逐筆呼叫 complete_job(),中間沒有彙總判定 中(依附 R1) R1 修好即消失;驗收時要專門測「一次完成多個平行任務」
R3 新任務初始狀態判定在平行結構下不對 ✅ repo_impl.py:750:該流程下沒有未完成任務 → 新任務開 PROCESSING、否則 TODO。平行結構下所有分支應該都可以開始做,但依現行邏輯第二個之後建的都會是 TODO(排隊) 中 平行分支上的任務建立時應一律 PROCESSING;判定要改成看「這個節點的前一個節點是不是分叉閘道/開始事件」
R4 「可不可以退回」判定只看下一批的第 0 筆 ✅ repo_impl.py:394:next_jobs[0] 硬取第一筆;另 :375 有 len(all_jobs)==1 → True 的捷徑,多任務後這條捷徑失效 中 平行結構下要改成「所有下一批任務都還沒開始才可以退回」;同時 len(all_jobs)==1 那條捷徑要重新檢視
R5 專案啟動只建第一個任務 ✅ prep_job_generation_service.py:93 _first_user_task_id() 用正規表示式抓第一個 userTask(:54),每個查核項目只建一筆任務 低(本案不改範本) 本案範本仍是單任務,不受影響。但若日後把範本本身做成多任務,這裡會靜默漏建——要在該函式旁留一句註解說明這個前提
R6 輪次推進/回退走的是另一張圖,可能誤判成受影響 ⚠️ stage_advance_service.py:85 與 stage_rollback_service.py:144 都在讀 workflow_template_xml,需驗收時實查確認它們讀的是輪次主流程的圖、不是查核項目底下的收證據流程 待驗證 列為階段⑤的必測項;若真的共用同一張圖,本案範圍要重新評估
R7 範本 XML 是可翻譯欄位,只寫主表會讓編輯器讀到舊值 ✅ repo_impl.py:495 起的檔內註解記著這個坑:規劃頁走原生 SQL 讀主表、編輯器走倉儲的語系備援讀翻譯表,只更新主表會讓編輯器永遠看到舊圖 高(已知坑) 後端所有寫圖路徑(新增插節點、刪除移節點)都必須比照現況同時寫主表與翻譯表
R8 前端移除兩次存檔後,圖與任務短暫不一致 現況 onSaveClick() 存兩次 XML 是為了把新任務編號寫回圖 低 改成後端在同一個交易內完成(插節點+建任務+回寫編號+寫圖),前端只存一次、回傳新 XML 讓前端重新載入
R9 檢測工具型任務的參數加密路徑 job_service.py:163 起的註解寫「新建路徑不經倉儲的 BPMN 回寫,但仍必須加密」 低 新增任務改走同一支 create_job(),加密路徑不變;但要驗收「規劃頁新增的檢測工具任務,參數確實有加密落庫」
R10 套件改動會影響其他使用者 jedi-flow-engine 目前版本 1.1.2,是共用套件 中 依規範:改動範圍與其他使用者風險要在實作計畫中明列、等決策者點頭;開發期走 poetry 路徑相依,發版另外等指示

§9

階段拆分草案

這是草案,不是派工單

各階段的驗收條件寫在下面,但實際開卡、切棒、派工要等決策者發令。跨 3 repo,各 repo 分開 commit。

階段① — 流程圖改圖工具(jedi-flow-engine)

項目 內容
做什麼 ①新增「把流程重排成標準平行」函式(可重用 bpmn_generator.py:145 的產圖邏輯);②新增「在合流閘道前掛一條新分支」函式;③擴充 remove_user_task_from_xml(),平行分支剩一條時把閘道收掉;④修 get_next_jobs() 或在其上補一層合流等待判定(依 D3 定案);⑤全部補單元測試
不做什麼 不動 inject_job_execution_uid()、不動互斥閘道的條件判定邏輯
驗收條件 1. 單一任務的圖 → 呼叫重排函式 → 產出「開始→分叉→2任務→合流→結束」且兩個任務的編號都保留
2. 標準平行圖 → 掛新分支 → 分叉出線數與合流入線數都 +1
3. 三分支平行圖 → 刪到剩一條 → 閘道消失、接回線性
4. 合流後還有任務的圖:完成第一條分支 → 合流後的任務不得變進行中;兩條都完成 → 才變進行中
交付 套件原始碼改動 + 測試;不發版(開發期走路徑相依)

階段② — 後端新增/刪除與存檔驗證(compliance-manager-be)

項目 內容
做什麼 ①repo_impl.py:698 的 if not job_element_uid: return None 改成走插節點路徑;②依 D2 定案實作插法判斷;③刪除任務走 delete_job() 移節點;④_sync_task_to_template_xml() 依 D4/D5 收斂寫入欄位;⑤流程圖存檔時加三項驗證;⑥修 R3(初始狀態)與 R4(可否退回);⑦依 D1 加凍結 gate
不做什麼 不動守門(_require_manager)、不動問卷 snapshot/工具綁定/加密路徑、不動輪次推進與回退
驗收條件 1. Swagger 打新增任務端點不帶 job_element_uid → 200,資料庫查該範本 XML 多一個方塊且帶新任務編號
2. 同一個查核項目連續新增三次 → 圖是「分叉→3任務→合流」,三筆任務都是進行中(R3)
3. 刪除其中一筆 → 圖剩兩條分支,任務與五張關聯表都清乾淨
4. 刪到剩一筆 → 閘道收掉、接回線性
5. XML 主表與翻譯表都更新(R7):改完打開編輯器看得到新方塊
6. 輪次凍結後打新增端點 → 依 D1 定案回應
交付 BE 改動 + 走 ddd-compliance-reviewer 掃 diff

階段③ — 規劃頁新增/刪除按鈕(compliance-manager-fe)

項目 內容
做什麼 ①任務區塊補「新增任務」(空狀態與有任務時都要有);②每筆任務面板補刪除(帶確認);③新任務建立後自動展開、聚焦名稱欄;④凍結狀態按鈕停用+留住提示(照 FR-110 做法)
不做什麼 不動 saveTask()、不動檢測工具參數的逐列驗證
驗收條件 1. 規劃頁點查核項目 → 「新增任務」→ 填名稱存檔 → 重新整理仍在
2. 打開流程圖編輯器 → 新方塊與原任務並排(平行)不是串在後面
3. 刪除任務 → 確認後消失,重新整理不會回來
4. 凍結狀態下按鈕停用且看得到原因(不是無聲失效)

階段④ — 流程圖編輯器瘦身(compliance-manager-fe)

項目 內容
做什麼 ①onSaveClick() 砍成只存一次 XML;②移除 syncJobExecutions() 與 patchJobExecutionUids();③屬性面板依 D4 只留名稱;④存檔失敗時顯示後端回的驗證訊息(哪個節點有問題);⑤TaskSetupView.vue:524 進來的路徑一併驗
不做什麼 不動 FlowTemplateEditorView.vue(另一套系統)
驗收條件 1. 點方塊 → 右邊只剩名稱欄
2. 拖新方塊存檔 → 回規劃頁看得到一筆新的空任務,名稱是圖上打的那個
3. 刪方塊存檔 → 規劃頁對應任務消失
4. 故意製造孤兒(改 XML 刪掉某方塊但任務還在)→ 存檔被擋下且指得出是哪一筆

階段⑤ — 驗收與回歸(compliance-manager-test)

項目 內容
做什麼 四條既有動線在平行結構下重跑:專案啟動建任務、輪次推進、批次完成、任務佇列;補 e2e 情境
必測 1. 起新專案 → 規劃頁加兩個任務 → 只完成一個時該查核項目不可以變已完成;兩個都完成才可以
2. 批次完成一次勾兩個平行任務 → 狀態正確,不出現提早啟動(R2)
3. R6 實查:確認輪次推進/回退讀的圖與查核項目收證據流程的圖不是同一張
4. 檢測工具型任務從規劃頁新增 → 參數確實加密落庫(R9)

§10

這份文件的定位

你想知道 看哪份
為什麼要做、決策者裁了什麼、哪些還沒決定 本文件
過去診斷雙向同步缺口的原始爬梳 docs/analysis/2026-06-17-ao-statement-key-mismatch-and-task-bpmn-sync.md 第二節
流程圖屬性與 API 欄位的對照 FE docs/guides/grc-page-data-models.md:40-47
拍板後的完整設計與取捨 design.md(待寫,D1–D6 定案後)
每一棒做了什麼 handoff/FR-112-LOG.md(待建)