搜尋開發指南

試試「多租戶」、「啟動」或「outbox」。
搜尋僅使用本站文件,不會傳送至外部服務。

文件目錄

NEXUSPRO / HANDBOOK

工作流與審批

了解 approval role、workflow run、stage action、review inbox 與長流程 worker 的資料與授權邊界。

內容核對 2026-09-28·圖文指南

本章目標與前置條件

讀完後,你應能區分「誰可以簽」的 approval role、「一張申請跑到哪」的 run,以及「誰在何時做了什麼」的 action,並能追蹤 review inbox 到資料庫投影。前置條件是熟悉 UUID、狀態機、授權範圍與 idempotency。

本章涵蓋兩套彼此獨立的審批引擎:表單審批(form_* 表,在 tenant transaction 內同步推進)與 JML 長流程(workflow_* 表,由 HR 事件開啟、Orchestrator/Temporal 編排)。兩者共用 approval role 與 review inbox,但資料表、動作契約與非同步邊界都不同。

功能現況

區塊後端已核實能力前端現況
Approval roleslist/get/create/update、departments lookup、applicant preview;binding subject_type 為 account、org_unit、org_unit_manager、position、user_group,resolver 為 direct_manager、department_manager、static_bindings、applicant/workspace/approval-roles(features/approval-roles)有角色編輯、綁定對象與解析預覽
表單審批POST /v1/forms/:formInstanceId/submit、approve、reject、return、withdraw、admin-cancel,以及 GET …/workflow、POST …/workflow/preview;同步引擎寫 form_runs、form_stage_instances、form_actionsfeatures/forms 負責送出、撤回、管理員取消與路徑預覽;審核在 features/reviews
JML runsGET /v1/workflow-runs、GET /v1/workflow-runs/:workflowRunId 與 approve、reject、return、withdraw;沒有建立端點,run 只由 HR 事件開啟;run domain 有 stage、current stage、execution summary前端沒有任何 /v1/workflow-runs 消費者
Review inboxGET /v1/workflows/reviews 合併 form/JML 卡片(source、box);POST /v1/workflows/reviews/bulk-action 只接受表單項目(form_id)/notifications(features/reviews)有 inbox、detail、bulk 結果與 sidebar badge;清單固定帶 source=form,JML 卡片與 JML 待辦數不會出現
審核交接規劃中:契約草案待審(docs/plans/2026-09-21-review-handover-contract.md),目前沒有任何交接路由只有 ReviewHandoverPanel 的 design-system 預覽,未接入 /notifications
Engine/workerengine.Approve(表單與 JML 共用)、engine.Compare(表單條件)與 role resolution;JML 的 outbox worker 啟動/完成處理與 Temporal adapter僅能以 live API 驗證;前端 fixture 明確與 live cache 隔離
JML/長流程已落地定義 hr.onboarding.v1、hr.offboarding.v1;TEMPORAL_ENABLED 與 WORKFLOW_JML_AUTO_START_ENABLED 預設皆為 false無(見 JML runs)

核心概念

Approval role 不會授予 IAM 權限

Approval role 是 workflow routing 定義,以 binding(account、org_unit、position、user_group 等)或 resolver 規則解析簽核人;它不建立 IAM grant。role 更新與 preview 要在同一個 workflow domain contract 內驗證,避免把「可被指定為簽核人」誤當成「可操作此頁」。被指派後,審核人可透過任務關係處理「那一筆」任務(見下方兩道防線),但這不等於取得頁面或全域 workflow.action.* 權限。

worker 的租戶巡檢會以 role_defaults.go 補齊預設角色;其中 hr_unit、it_unit 等 static_bindings 角色預設為 inactive,未綁定並啟用前,引用它們的 approver 關卡會解析失敗(workflow_stage_unavailable)。

Run、stage、action 是不同事實

run 保存整個流程狀態;stage 是到達過的關卡及當時的 role snapshot;action 是 actor 對 stage 的決策事件。兩套引擎各有一組資料表:表單是 form_runs、form_stage_instances、form_actions(關卡型別 approver、notify、condition),JML 是 workflow_runs、workflow_stage_instances、workflow_actions(approver、notify)。權限、過期與 assignee 檢查都不能由前端代替,但兩者的動作契約不同:

項目表單(/v1/forms/:formInstanceId/…)JML(/v1/workflow-runs/:workflowRunId/…)
動作submit、approve、reject、return、withdraw、admin-cancelapprove、reject、return、withdraw
過期防線body revision CAS,不符回 stale_versionbody stage_instance_id 必須是目前 active 關卡,否則回 workflow_stage_not_active
必要輸入Idempotency-Key;reject、return、admin-cancel 需非空 commentIdempotency-Key(1–128 字元);comment 不得為 null;withdraw 不得帶 stage_instance_id
撤回withdraw 只限申請人(workflow_not_applicant);admin-cancel 是 tenant-wide 管理動作,結果為 withdrawnwithdraw 是 tenant-wide 管理動作,不比對發起人
狀態draft → in_review → approved/rejected/returned/withdrawn;returned 可再 submitpending_start → running → completed;reject、withdraw → cancelled;return → returned;啟動失敗 → start_failed

兩邊的 comment 上限都是 1000 字元。JML 詞彙中的 compensating 目前不會寫入:reject 與 withdraw 直接以補償語義進 cancelled,已完成的關卡不回滾。

一個 action 的兩道防線

route registry 先做授權判定。持有精確 permission point(如 workflow.action.approve)者依 grant 與 scope 放行;沒有 grant 時,只有列在 review_task_authz.go 的 reviewTaskKind 裡的任務路由會改走任務關係判定(在 identity、explicit deny 與 elevation 之後):

  • 表單 approve、reject、return:actor 必須是目前 active approver 關卡上 pending 的 assignee;讀取表單詳情只需是申請人或曾經/目前的參與者。
  • JML approve、reject、return:run 必須是 running,且 actor 是當前 approver 關卡的 pending assignee;讀取 run 詳情只需曾被指派於該 run。
  • GET /v1/workflows/reviews 與 bulk-action 對已登入者放行,由 handler 依 actor 過濾並逐項授權;inbox 的 can_act 與表單 detail 的 permissions 用同一套判定即時計算。

service 在 transaction 內再鎖定 run/stage,確認 current stage、active status、assignee 與 command fingerprint。上一關、非 assignee 或已終態的 request 不是「空結果」,應回傳可辨識錯誤(如 workflow_not_assignee、workflow_stage_not_active、workflow_action_conflict、invalid_state)。

冪等回放與可見性重查

同一個 Idempotency-Key 配同一個 body 重送會回放第一次的結果;body 不同回 idempotency_conflict(409)。在任務關係路徑上,key 不能取代目前的可見性:回放只允許原 actor、原資源、原動詞的已持久化命令,而且表單會先重查 actor 仍可見該表單,JML 會先重查 actor 仍被指派於該 run。bulk 以父 key 導出每一項的子 key 逐項授權,已快取的結果也會重新授權;被撤銷的項目回 failed,不回傳快取的表單。

請求鏈與非同步邊界

共同原則:業務 fact 與它引發的 outbox 事件在同一個 domain transaction 保存,worker 在 transaction 外投遞;通知、HR transition 或 attendance effect 沿著明確事件/adapter 邊界處理,不在一個 tenant transaction 內同步呼叫另一個 domain service。兩套引擎的邊界不同,不要用同一張圖理解。

表單審批:同步引擎

text
POST /v1/forms/:id/submit 等動作
  → 交易外:解析 approval role、評估 condition、計算下一關
  → tenant transaction:鎖定 + revision CAS
      ├─ form_runs、form_stage_instances、form_actions
      ├─ form_command_replays
      └─ outbox:form.approval.pending、form.approval.result、form.notified 與考勤 effect 事件
  → 交易提交後:worker 的 notification、attendance handler 投遞事件
  → GET /v1/workflows/reviews 讀到新的關卡狀態

表單動作先在交易外解析角色、評估條件並算出下一關,再於同一個 tenant transaction 內鎖定 instance、以 revision CAS 提交關卡、action、outbox 事件與 replay 紀錄;沒有任何 worker 或 Temporal 參與關卡推進。例外是請假類表單:submit 時經 LeaveAdmission port 在獨立的 attendance transaction 預占,表單交易失敗時補償撤銷。

JML:HR 事件驅動的長流程

架構流程圖 · Mermaid

正在繪製架構圖…

查看圖表原始碼
flowchart LR
  HR["HR 交易:hr.employee.created 或 status_changed"] -- "outbox" --> HRWorker["worker:HR handler"]
  HRWorker -- "WORKFLOW_JML_AUTO_START_ENABLED" --> Start["workflow_runs:pending_start + workflow.run.start_requested"]
  Start --> RunWorker["worker:runStartRequestedHandler"]
  RunWorker --> Orch["Orchestrator.Start:Temporal 或 in-process fake"]
  Orch --> Projection["activity 寫 projection:stage、assignee、run status"]
  Projection --> Reads["GET /v1/workflows/reviews、GET /v1/workflow-runs"]
  API["POST /v1/workflow-runs/:id/approve 等"] -- "交易內" --> Actions["workflow_actions"]
  API -- "交易外 Signal" --> Orch
  Projection -- "終態" --> Completed["outbox:workflow.run.completed"]

run 沒有建立端點,只由 HR 事件開啟:hr.employee.created 開 hr.onboarding.v1(人資單位 approver → 直屬主管 notify),轉為 offboarded 的 hr.employee.status_changed 開 hr.offboarding.v1(直屬主管 approver → 人資單位 approver → 資訊單位 notify)。WORKFLOW_JML_AUTO_START_ENABLED 預設 false,關閉時 HR handler 只記 log、不開 run;TEMPORAL_ENABLED 預設 false,關閉時 API 與 worker 都裝載 in-process fake orchestrator,只同步推進關卡,沒有長流程、重試與逾時保證。

JML 動作不寫 outbox:workflow_actions 在交易內寫入後,API process 在交易外直接呼叫 Orchestrator.Signal,成功後以獨立短交易記下 signal_sent_at。引擎不可用時回 workflow_orchestrator_unavailable(503),此時動作已落地,以同一個 Idempotency-Key 重送會走回放並補送 signal;引擎沒有該 execution 時回 workflow_execution_not_found(409)。

approver 關卡解不出簽核人不會被略過(notify 解不出人才記 warn 並繼續):在 Temporal 路徑上,第一關失敗時 run 進 start_failed,後續關卡則停住並以 timer 退避重試,期間仍可撤回。退回後 run 為 returned 且原關已結束,目前沒有重新送出的端點,只能撤回。JML 關卡指派不發通知(只發 workflow.run.start_requested 與 workflow.run.completed);兩個已落地定義都是 HR 事實先發生,workflow.run.completed 目前只記 log,不需要 HR 寫入。

實作步驟與示例

  1. 先確認是 role governance、run read、form review 或 action command,查 internal/route/testdata/registry.golden(或各 register*Routes 宣告)的 permission/risk/tenant_wide/page;api/openapi.yaml 只描述 wire 形狀,不含這些欄位。新增任務路由要同步登記 reviewTaskKind。
  2. 新增 stage type、action 或 status 時,先改 domain vocabulary(表單在 internal/domain/form,JML 在 internal/domain/workflow/run.go),再同步 DB CHECK、OpenAPI enum、service engine、前端 zod schema 與 contract test。
  3. 新增 approval role binding 時,驗證被綁定對象可解析、snapshot 有界、沒有把 IAM 權限混入 workflow。
  4. action 寫入後以 PostgreSQL projection 驗證 review inbox、badge、detail 與下一 stage;不要只看 worker log。
http
GET /v1/workflows/reviews?box=pending&page=1&page_size=20
POST /v1/workflows/reviews/bulk-action
Idempotency-Key: <每次批次請求唯一值;重試沿用同一值>
Content-Type: application/json

{"action":"approve","items":[{"form_id":"<form-instance-id>","revision":7,"comment":null}]}
http
POST /v1/workflow-runs/<workflow-run-id>/approve
Idempotency-Key: <每個動作唯一值;重試沿用同一值>
Content-Type: application/json

{"stage_instance_id":"<current-stage-instance-id>","comment":""}

上例只展示 wire 形狀,ID、revision 與 stage 必須使用當前受控資料。bulk 只接受表單項目:1–50 筆、form_id 不得重複,reject/return 需非空 comment;JML 卡片要逐筆呼叫 /v1/workflow-runs/:workflowRunId/approve 等端點並帶目前的 stage_instance_id。驗證成功 action 後,應核對 response 的逐項結果、run status、stage status、audit 與 badge;若一筆 stale,不應讓其他可成功項目一起回滾。5xx 會讓整批回應失敗,但已完成的項目不回滾;以同一個 key 重送會沿用各項已記錄的結果並續跑其餘項目。

驗證矩陣

驗證問題程式碼/測試證據仍需另外做的驗收
role CRUD/previewinternal/service/workflow/role_service_test.go、features/approval-roles/api.test.ts、features/approval-roles/ApprovalRolesPage.test.tsxlive employee/department/group resolution
表單 actioninternal/service/workflow/engine/engine_test.go、internal/api/v1/form_admin_cancel_test.go、tests/unit/forminstance/bulk_test.go真實資料下的角色解析、條件與 effect
JML run/stage actioninternal/service/workflow/action_service_review_test.go、internal/platform/temporal/workflows/jml_test.go、tests/integration/postgres/workflow_jml_start_test.go、tests/integration/postgres/workflow_parity_test.go真 Temporal 下的啟動、重試與長流程
任務授權與回放internal/api/v1/review_task_authz_test.go、tests/integration/postgres/review_task_authz_test.go、tests/integration/postgres/review_task_http_test.golive 帳號的無 grant 審核與撤銷指派後的重送
review inboxinternal/api/v1/workflow_reviews.go、features/reviews/identity.test.tsx、features/reviews/useReviewSidebarBadges.test.tsxauthenticated API、projection freshness、跨帳號隔離
bulk partial successfeatures/reviews/api.ts、tests/unit/forminstance/bulk_test.go真實 stale/permission 混合批次
form workflow previewinternal/api/v1/form_workflow_preview_test.go、features/forms/workflow-preview.contract.test.tsxservice 的角色解析、條件與 DB revision

常見錯誤

  • 用 IAM role 取代 approval role,讓權限治理和流程路由耦合。
  • 把表單審批當成 Temporal 流程追查;表單關卡在 tenant transaction 內同步推進,Temporal 只負責 JML。
  • 把 Temporal status 當作頁面查詢結果,造成重整後與 PostgreSQL projection 不一致。
  • 只在 route 做 workflow.action.* 檢查,沒驗證 stage assignee、current stage 與 revision。
  • 以為撤銷 workflow.action.* grant 就能阻止審核;被指派人透過任務關係仍可處理自己的任務。
  • 對 JML 卡片呼叫 bulk-action,或新增任務路由卻沒登記 reviewTaskKind。
  • bulk action 遇一筆 stale 就整批 rollback;契約要求逐項結果,成功項不可因單筆失敗回滾。
  • 本機驗 JML 卻沒開 WORKFLOW_JML_AUTO_START_ENABLED,或把 TEMPORAL_ENABLED=false 的 fake 結果當成長流程驗收。
  • 看到 ReviewHandoverPanel 或前端 fixture inbox 就宣稱交接、worker/Temporal 已驗收;本文沒有執行長流程或部署驗證。

相關文件

內容來源與核實範圍

以下路徑相對於所列業務倉庫;核實層級為「已讀原始碼」,不是本輪業務測試或部署驗收。

  • nexus-pro-be-plus/internal/api/v1/workflow_roles.go
  • nexus-pro-be-plus/internal/api/v1/workflow_runs.go
  • nexus-pro-be-plus/internal/api/v1/workflow_actions.go
  • nexus-pro-be-plus/internal/api/v1/workflow_reviews.go
  • nexus-pro-be-plus/internal/api/v1/form_runtime.go
  • nexus-pro-be-plus/internal/api/v1/form_runtime_lists.go
  • nexus-pro-be-plus/internal/api/v1/review_task_authz.go
  • nexus-pro-be-plus/internal/authz/tasks.go
  • nexus-pro-be-plus/internal/repository/postgres/authz_tasks.go
  • nexus-pro-be-plus/internal/service/workflow/role_service.go
  • nexus-pro-be-plus/internal/service/workflow/role_defaults.go
  • nexus-pro-be-plus/internal/service/workflow/run_service.go
  • nexus-pro-be-plus/internal/service/workflow/action_service.go
  • nexus-pro-be-plus/internal/service/workflow/run_sequencing.go
  • nexus-pro-be-plus/internal/service/workflow/definitions.go
  • nexus-pro-be-plus/internal/service/form/instance/commands.go
  • nexus-pro-be-plus/internal/service/form/instance/bulk.go
  • nexus-pro-be-plus/internal/domain/workflow/run.go
  • nexus-pro-be-plus/internal/domain/workflow/approval_role.go
  • nexus-pro-be-plus/internal/platform/temporal/workflows/jml.go
  • nexus-pro-be-plus/internal/wiring/workflow.go
  • nexus-pro-be-plus/internal/worker/workflow.go
  • nexus-pro-be-plus/internal/worker/hr.go
  • nexus-pro-be-plus/internal/route/testdata/registry.golden
  • nexus-pro-be-plus/docs/plans/2026-09-21-review-handover-contract.md
  • nexus-pro-web-plus/features/approval-roles/api.ts
  • nexus-pro-web-plus/features/reviews/api.ts
  • nexus-pro-web-plus/features/reviews/_components/ReviewHandoverPanel.tsx
  • nexus-pro-web-plus/features/forms/api.ts
NexusPro 開發指南以程式碼為準 · 以驗證為據