FR-112 · 任務真相收回規劃頁、流程圖降為走向設計 · 需求討論稿 · 2026-09-19(v1 · 待審)
目前要替一個查核項目多開一個任務,唯一的辦法是打開流程圖編輯器、從工具列拖一個方塊出來——PM 不會用、也不該被要求會用。本案把「新增任務」放回專案規劃頁,並把新開的任務預設接成平行(誰先做都行、全部做完這個查核項目才算過);流程圖編輯器降級為純粹的「走向設計工具」,只管先後與分支,不再是填任務資料的地方。
整案一句話:任務的所有資料以專案規劃頁為準存進資料庫,流程圖只留「先後與分支」這一件事;規劃頁補上「新增任務」按鈕,新任務預設掛成平行分支,該查核項目要全部任務做完才算通過。
給非工程讀者的三個名詞:
userTask(一個方塊=一個任務),箭頭表示先後順序,菱形的叫「閘道」——平行閘道表示「這幾條同時開跑」、互斥閘道表示「二選一」。job_executions):資料庫裡真正存任務的地方——名稱、描述、負責人、狀態、證據、問卷、檢測工具設定都在這裡。| 階段 | 做什麼 | 產出 | 完成怎麼判定(決策者親手檢查法) |
|---|---|---|---|
| ① 流程圖改圖工具 | 在共用套件裡補三支能力:把流程重排成標準平行結構、在合流閘道前掛一條新分支、合流閘道要等所有兄弟分支都完成才放行 | jedi-flow-engine 新增/修正的函式 + 單元測試 | 跑套件測試:給一張「兩條平行分支、合流後還有一個任務」的流程圖,完成第一條分支後,合流後的任務不可以變成進行中;兩條都完成才可以 |
| ② 後端新增/刪除任務 | 新增任務端點不再要求前端先給流程圖節點編號——後端自己在流程圖上補節點、接線、回寫任務編號;刪除任務時把節點從流程圖上拿掉並把線接回去 | BE app service + repo 改動 + 存檔驗證 | 用 Swagger 直接打新增任務端點(不帶 job_element_uid)→ 回 200;去資料庫看該查核項目的流程圖 XML,多了一個方塊、且方塊上帶著新任務的編號 |
| ③ 規劃頁按鈕 | 任務區塊補「新增任務」與「刪除任務」;新任務展開就是既有的任務表單 | FE ProjectPlanningView.vue |
規劃頁點任一查核項目 → 按「新增任務」→ 填名稱存檔 → 重新整理仍在 → 打開流程圖編輯器,看得到這個新方塊,而且是與原任務並排(平行)不是串在後面 |
| ④ 流程圖編輯器瘦身 | 屬性面板只留「名稱」;存檔不再由前端逐一打新增/更新任務端點(改由後端存檔時處理) | FE WorkflowSetupEditor.vue |
打開編輯器點一個方塊 → 右邊只剩名稱欄,沒有任務類型/問卷/設備/部門;拖一個新方塊出來存檔 → 回規劃頁看得到一筆新的空任務(名稱是你在圖上打的那個) |
| ⑤ 驗收與回歸 | 專案啟動、輪次推進、批次完成、任務佇列四條既有動線在平行結構下重跑 | e2e 回歸(測試 repo) | 起一個新專案 → 規劃頁加兩個任務 → 兩個都完成 → 該查核項目變「已完成」;只完成一個時不可以變完成 |
依賴:①是②的地基(②要呼叫①補的改圖函式);③④在②完成後可平行;⑤等③④都驗收過才做。
專案規劃頁(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。
兩邊互相覆蓋的具體路徑(實查程式碼):
_sync_task_to_template_xml()(infra/flow_control/repository/flow_control_job_repo_impl.py:401)把名稱/描述/指南/類型/問卷/工具參數寫回流程圖 XML 的 camunda 屬性。這是單向的,方向正確。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)
job_executions;流程圖 XML 只負責兩件事——拓樸(誰先誰後、誰跟誰平行),以及用 job_execution_uid 屬性指回是哪一筆任務。名稱寫進圖裡只是為了讓圖看得懂,是顯示副本,由後端單向寫入。這個「流程圖只描述走向、業務資料另存」的分法不是本案發明的,是流程引擎領域的通則。
流程定義(BPMN 檔)是不可變的部署物——部署後有版號,改了就是新版本,已在跑的案子繼續用舊版。真正的業務資料放在「流程變數」與外部業務表,不寫進 BPMN 檔本身。
BPMN 上的 userTask 屬性主要是路由用(誰該收到這件事、什麼條件往哪走),不是拿來存「這個任務的負責人叫什麼、他交了幾份證據」。
對本案的啟示:把任務欄位塞進 camunda 屬性,一開始就偏離了這個工具的設計意圖。
子任務預設彼此不相依——建了就都可以開始做,沒有「要等前一個」這回事。要有先後關係,得另外手動設定「阻擋/被阻擋」。
母任務的完成判定就是「子任務全部完成」。
對本案的啟示:決策者裁的「新增後預設平行、全部完成才算過」與這個直覺一致。使用者建兩個任務時,心裡想的就是「這兩件事都要做」,不是「先做A再做B」。
兩邊合起來就是本案的形狀:任務像 Asana(預設互不相依、全做完才算過),流程圖像 Camunda(只管走向、不存業務資料)。
3涉及 repo
10寫進流程圖的 camunda 屬性
2引擎缺口落點(同一個 bug 兩處)
| 元件 | 現況(實查) | 本案動作 |
|---|---|---|
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),沒有順序假設 |
不受影響 |
| 元件 | 現況(實查) | 本案動作 |
|---|---|---|
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() |
廣度優先分層 | 同上 |
| 元件 | 現況(實查) | 本案動作 |
|---|---|---|
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 |
流程範本管理,是另一套系統 | 明確不在範圍 |
| 這件事 | 真相在哪 | 另一邊是什麼 |
|---|---|---|
| 任務叫什麼、描述、指南、類型、負責人、部門、設備、問卷、工具參數、狀態 | 資料庫 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,任務面板出現、預設展開待填
兩種入口走同一支端點,差別只在有沒有帶 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
規則寫成白話:
bpmn_generator.py:145 的產圖邏輯。為什麼不是「一律重排成標準平行」
使用者若刻意把任務排成「先做 A 才能做 B」,一按新增就被重排回全平行,等於系統擅自推翻他的設計,而且沒有任何提示。這會是很難解釋的行為。
規劃頁按刪除 → 後端 delete_job()(repo_impl.py:778)清關聯表 → 呼叫改圖函式移節點:
前 → 節點 → 後 接回 前 → 後(現有 remove_user_task_from_xml() 就在做這件事)。開始 → 結束。要不要擋(至少留一個任務)屬產品判斷,列在 D6。編輯器存 XML 時,後端要在寫進資料庫之前檢查三件事,任一不過就退回並指出哪個節點有問題:
| 檢查 | 為什麼 | 不過的症狀(如果不擋) |
|---|---|---|
圖上每個 job_execution_uid 都查得到對應任務 |
使用者可能複製貼上方塊,連帶複製了別人的任務編號 | 兩個方塊指向同一筆任務,完成其中一個另一個跟著變狀態 |
| 沒有孤兒任務(資料庫有、圖上沒有對應節點) | 使用者在圖上刪了方塊 | 任務永遠停在待辦、永遠不會被推進,而且看不出原因 |
| 每個任務節點從開始事件走得到 | 使用者接線接錯、方塊沒連上 | 完成前一個任務時引擎找不到它,流程卡住 |
孤兒任務的處理方式(刪掉?標成取消?擋下存檔?)需拍板,併入 D6。
| 還能做 | 不能做 |
|---|---|
| 改線(把平行拉成一條龍、加二選一分支、調整順序) | 填任務類型、問卷、設備、部門、工具參數(回規劃頁) |
| 拖新方塊進來(只填名稱) | 直接改任務的負責人與狀態 |
| 刪方塊 | — |
屬性面板留哪些欄位是 D4。
合流閘道不會等兄弟分支——第一條分支完成,合流後面的任務就被啟動
實查 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。所以本案的預設結構不會立刻踩到。
但本案會放大它:
job_batch_complete_service.py:86)會連續呼叫 complete_job(),一次完成多個平行任務,把這個時序放到最大。prep_job_generation_service.py:170 只抓第一個 userTask 建一筆),平行結構幾乎不存在,缺口沒有被觸發的機會。要不要在本案一併修,是 D3。
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)在規劃頁給提示,因為平行分支太多在圖上會擠成一團、難以閱讀。
| 編號 | 風險 | 實查結果 | 嚴重度 | 怎麼處理 |
|---|---|---|---|---|
| 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 路徑相依,發版另外等指示 |
這是草案,不是派工單
各階段的驗收條件寫在下面,但實際開卡、切棒、派工要等決策者發令。跨 3 repo,各 repo 分開 commit。
| 項目 | 內容 |
|---|---|
| 做什麼 | ①新增「把流程重排成標準平行」函式(可重用 bpmn_generator.py:145 的產圖邏輯);②新增「在合流閘道前掛一條新分支」函式;③擴充 remove_user_task_from_xml(),平行分支剩一條時把閘道收掉;④修 get_next_jobs() 或在其上補一層合流等待判定(依 D3 定案);⑤全部補單元測試 |
| 不做什麼 | 不動 inject_job_execution_uid()、不動互斥閘道的條件判定邏輯 |
| 驗收條件 | 1. 單一任務的圖 → 呼叫重排函式 → 產出「開始→分叉→2任務→合流→結束」且兩個任務的編號都保留 2. 標準平行圖 → 掛新分支 → 分叉出線數與合流入線數都 +1 3. 三分支平行圖 → 刪到剩一條 → 閘道消失、接回線性 4. 合流後還有任務的圖:完成第一條分支 → 合流後的任務不得變進行中;兩條都完成 → 才變進行中 |
| 交付 | 套件原始碼改動 + 測試;不發版(開發期走路徑相依) |
| 項目 | 內容 |
|---|---|
| 做什麼 | ①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 |
| 項目 | 內容 |
|---|---|
| 做什麼 | ①任務區塊補「新增任務」(空狀態與有任務時都要有);②每筆任務面板補刪除(帶確認);③新任務建立後自動展開、聚焦名稱欄;④凍結狀態按鈕停用+留住提示(照 FR-110 做法) |
| 不做什麼 | 不動 saveTask()、不動檢測工具參數的逐列驗證 |
| 驗收條件 | 1. 規劃頁點查核項目 → 「新增任務」→ 填名稱存檔 → 重新整理仍在 2. 打開流程圖編輯器 → 新方塊與原任務並排(平行)不是串在後面 3. 刪除任務 → 確認後消失,重新整理不會回來 4. 凍結狀態下按鈕停用且看得到原因(不是無聲失效) |
| 項目 | 內容 |
|---|---|
| 做什麼 | ①onSaveClick() 砍成只存一次 XML;②移除 syncJobExecutions() 與 patchJobExecutionUids();③屬性面板依 D4 只留名稱;④存檔失敗時顯示後端回的驗證訊息(哪個節點有問題);⑤TaskSetupView.vue:524 進來的路徑一併驗 |
| 不做什麼 | 不動 FlowTemplateEditorView.vue(另一套系統) |
| 驗收條件 | 1. 點方塊 → 右邊只剩名稱欄 2. 拖新方塊存檔 → 回規劃頁看得到一筆新的空任務,名稱是圖上打的那個 3. 刪方塊存檔 → 規劃頁對應任務消失 4. 故意製造孤兒(改 XML 刪掉某方塊但任務還在)→ 存檔被擋下且指得出是哪一筆 |
| 項目 | 內容 |
|---|---|
| 做什麼 | 四條既有動線在平行結構下重跑:專案啟動建任務、輪次推進、批次完成、任務佇列;補 e2e 情境 |
| 必測 | 1. 起新專案 → 規劃頁加兩個任務 → 只完成一個時該查核項目不可以變已完成;兩個都完成才可以 2. 批次完成一次勾兩個平行任務 → 狀態正確,不出現提早啟動(R2) 3. R6 實查:確認輪次推進/回退讀的圖與查核項目收證據流程的圖不是同一張 4. 檢測工具型任務從規劃頁新增 → 參數確實加密落庫(R9) |
| 你想知道 | 看哪份 |
|---|---|
| 為什麼要做、決策者裁了什麼、哪些還沒決定 | 本文件 |
| 過去診斷雙向同步缺口的原始爬梳 | 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(待建) |