搜尋開發指南

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

文件目錄

NEXUSPRO / HANDBOOK

新增業務功能實戰

以「草稿審核流程預覽」為例,沿著已存在的實作,走完契約、資料存取、後端、前端與驗證。

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

本章目標

需求是:送出草稿前,先讓申請人看到預計經過的關卡與審核人。 預覽不能修改草稿、扣除額度、建立關卡或投遞事件,也不能保證日後一定依同一路徑執行。

此功能在目前前後端原始碼中已實作。本章是從既有實作學習新增功能的方法,不是指示你重複建立端點,也不是宣告已完成部署驗收。

前置條件

閱讀後端規範,能開啟前後端原始碼。若要實際聯調,另需已授權的環境、可登入帳號、正確租戶與本人可見的 draft 或 returned 表單;本站不提供這些敏感資料。

步驟一先定契約

在 api/openapi.yaml 找到實際 operation:

text
POST /v1/forms/{formInstanceId}/workflow/preview
operationId: workflowPreviewFormStages
Content-Type: application/json
Request body: {}

formInstanceId 是表單 UUID。權限不寫在 OpenAPI:handler 以 route.Point("workflow.run", "read").ResourceID("formInstanceId").Page("workflow.form") 註冊,快照在 internal/route/testdata/registry.golden,資源識別綁定表單 ID。完整可見性仍由 service 控制:self 範圍只能看到自己參與的表單;授權範圍為 tenant 或 all 時,formPrincipal 會設 TenantWideRead,loadVisible 便略過參與者可見性檢查。

契約決定原因
body 必須是封閉空物件 {}只讀已保存欄位,不讓未驗證輸入進入條件計算
不要求 Idempotency-Key預覽不保存業務狀態
只允許 draft、returned已進流程的表單應查看實際歷程
不可判定時回 complete: false缺欄位不能猜測成條件不成立
planned、skipped、undetermined預計結果與已發生的流程狀態使用不同詞彙

回應核心欄位是 form_instance_id、definition_version、revision、evaluated_at、complete、stages 與 warnings。關卡 label 已是完整顯示文字,前端不要自行用 condition_result 拼接。

新增端點要一起改的登記

本例沒有新增權限點、錯誤碼、env 或 migration,但多了一條路由,所以除了 OpenAPI 與 handler,還改了三份封閉清單;任一漏改,make contract 都會失敗:

  • internal/route/testdata/registry.golden:路由與權限宣告快照,沒有自動更新旗標,要手動改。
  • tests/contract/openapi_test.go 的 trackedOpenAPIOperations:OpenAPI 允許的 path/method 清單。
  • tests/contract/operator_surface_test.go:已註冊路由的精確集合。

交付時另要在 Apifox 套件 29010 建場景,執行方式見測試策略。

步驟二確認資料庫需求

本功能沿用既有 form runtime aggregate,不需要為「預覽結果」新增資料表或 migration。閱讀既有 000044_form_runtime.sql 理解儲存結構,再看 db/queries/form_runtime.sql:

sql
-- 既有查詢摘錄:db/queries/form_runtime.sql
-- name: GetFormInstance :one
SELECT * FROM public.form_instances WHERE tenant_id= @tenant_id AND id= @id;

service 的 load 在 WithinTenant 內呼叫 tx.Get 取得 aggregate;loadVisible 再檢查呼叫者可見性。不能省略租戶與可見性檢查,直接按 ID 查表。

若你的新功能確實新增儲存欄位,才依序新增 migration、更新 db/queries、執行 make generate(sqlc、mapper-gen 與 go generate),再更新 Repository 契約;同時在 db/migrations/FROZEN.sha256 追加新檔 hash,並把 tests/integration/postgres/migrations_test.go 的 latestMigrationVersion 改成新版本,否則契約或整合測試會失敗。本案例選擇不保存預覽,正是為了避免讓「可能發生的路徑」污染已發生的事實。

步驟三在正確分層實作

internal/api/v1/form_runtime.go 的 handler 負責解析 principal、UUID 與封閉 body,再呼叫 PreviewWorkflow。HTTP 層不自行計算審核人。

internal/service/form/instance/preview.go 的核心順序:

  1. loadVisible 讀取已保存且可見的 aggregate。
  2. 拒絕非 draft/returned 狀態。
  3. 解碼 definition,透過 sample 取得本輪唯一時間。
  4. 以目前申請人的 employee 身分解析關卡。
  5. 在記憶體副本上行走條件與審核關卡;不保存副作用。

預覽型別與 planned/skipped/undetermined 常數在 internal/domain/form/runtime_preview.go。審核人解析沿用從 submit 路徑 advance() 抽出的 Service.stageAccounts(stages.go),條件求值沿用同一個 condition();預覽與 submit 共用解析邏輯,不另寫平行實作。

架構流程圖 · Mermaid

正在繪製架構圖…

查看圖表原始碼
flowchart TD
  A["讀取可見草稿"] --> B{"狀態可預覽?"}
  B -- 否 --> C["409 invalid_state"]
  B -- 是 --> D["單次時間取樣 + 關卡解析"]
  D --> E{"條件資料完整?"}
  E -- 否 --> F["undetermined / complete false"]
  E -- 是 --> G["計算預計路徑"]
  G --> H["回傳 stages + warnings"]

預覽不放寬 submit。即使可以帶著 workflow_stage_unavailable warning 顯示預覽,真正送出仍可能拒絕。

步驟四前端串接

在 features/forms/api.ts 找到 useGetFormWorkflowPreview,再看它的呼叫點 features/forms/FormsPage.tsx:依後端回傳的 permissions.canSubmit(草稿與退回待重送)決定是否請求預覽,並以 formInstanceId 濾掉 keepPreviousData 保留的上一張結果,再經 _components/FormModal.tsx 傳給 WorkflowPanel.tsx 轉成 view-model。WorkflowPanel.tsx 本身不呼叫 hook。

  • SWR key 包含身分、表單 ID 與 revision;儲存後 revision 前進就觸發重取。
  • transport 先把 snake_case 轉成 camelCase,再用嚴格的 zod schema 驗證;未公告的 warning code 視為契約違反而拒收。
  • 回應 revision 落後時只重取一次;仍落後就報錯,不讓舊流程蓋住新草稿。
  • undetermined 顯示「填寫後決定」,不是「審核失敗」。
  • 前端的 fixture 分支(development 且 NEXT_PUBLIC_MSW=true)不是線上資料,驗收需辨別當次模式。

從畫面到資料的閱讀順序:

text
features/forms/_components/WorkflowPanel.tsx     (view-model,只接收資料)
    ← features/forms/_components/FormModal.tsx
    ← features/forms/FormsPage.tsx                (呼叫 useGetFormWorkflowPreview)
    ← features/forms/api.ts / useGetFormWorkflowPreview
    ← POST /v1/forms/{formInstanceId}/workflow/preview
    ← internal/api/v1/form_runtime.go             (formWorkflowPreview)
    ← internal/service/form/instance/preview.go

預期結果與驗證

以下為目前存在的聚焦測試入口。本輪只核實測試內容與路徑,未執行業務測試;不要把預期結果讀成已跑過的報告。

bash
# 工作目錄:nexus-pro-be-plus
go test ./internal/api/v1 -run '^TestFormWorkflowPreviewBodyIsAClosedEmptyObject$' -count=1
go test ./tests/unit/forminstance -run '^TestPreview' -count=1
# 真 PostgreSQL(testcontainers,需要 Docker)
REQUIRE_INTEGRATION=1 go test ./tests/integration/postgres -run '^TestFormPreviewWritesNothing$' -count=1

tests/unit/forminstance 不在 make unit 內,所以 ci-core/ci-local 都不會跑這組 TestPreview*;改到表單流程時,要用上面的命令、go test ./tests/unit/... 或 make test 另外執行。

bash
# 工作目錄:nexus-pro-web-plus
pnpm test:run features/forms/workflow-preview.contract.test.tsx
場景預期可觀察結果
{} 請求handler 呼叫 service 並回傳預覽
缺 body、null、未知 keyHTTP 400,且不進入 service
已送出的狀態409,呼叫端轉向讀歷程
無法看到的草稿不揭露資料;service 以 NotFound 處理
條件欄位未填undetermined、complete: false、對應 warning
完整路徑計算回傳預計關卡,aggregate/events/quota 保持不變
resolver 基礎設施故障error 上拋,不轉成正常空資料
回應 revision 過舊前端最多重取一次,仍過舊則報錯

TestPreviewWalksTheWholePathWithoutWriting 會比較計算前後 aggregate,並確認沒有 events/quota 變動;也注入次微秒殘值驗證時間精度。這比只斷言 err == nil 更有力。TestFormPreviewWritesNothing 則在真 PostgreSQL 上確認 aggregate、workflow 投影與 outbox 在預覽前後完全一致。

常見錯誤與交付缺口

不要把這個端點和「未建立草稿前的模板預覽」混用;後者沒有相同的 instance 身分。不把預覽狀態當作已簽核的歷程,不因預覽成功而保證 submit 成功。新增端點時只改 OpenAPI 與 handler、漏掉三份路由登記,make contract 會失敗。

相關文件

回到後端規範的交付門檢查缺少的證據,測試分層與 Apifox 執行方式見測試策略;遇到差異時依常見問題排查。

內容來源與核實範圍

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

  • nexus-pro-be-plus@4cdf9822(本機工作區已讀,不宣稱工作區全淨)
  • nexus-pro-be-plus/api/openapi.yaml
  • nexus-pro-be-plus/internal/api/v1/form_runtime.go
  • nexus-pro-be-plus/internal/api/v1/form_workflow_preview_test.go
  • nexus-pro-be-plus/internal/route/testdata/registry.golden
  • nexus-pro-be-plus/tests/contract/openapi_test.go
  • nexus-pro-be-plus/tests/contract/operator_surface_test.go
  • nexus-pro-be-plus/internal/domain/form/runtime_preview.go
  • nexus-pro-be-plus/internal/service/form/instance/preview.go
  • nexus-pro-be-plus/internal/service/form/instance/stages.go
  • nexus-pro-be-plus/internal/service/form/instance/service.go
  • nexus-pro-be-plus/internal/service/form/instance/definition.go
  • nexus-pro-be-plus/db/migrations/000044_form_runtime.sql
  • nexus-pro-be-plus/db/queries/form_runtime.sql
  • nexus-pro-be-plus/tests/unit/forminstance/preview_test.go
  • nexus-pro-be-plus/tests/integration/postgres/form_preview_test.go
  • nexus-pro-be-plus/memory/dev-log/2026-09-18-form.md
  • nexus-pro-be-plus/memory/dev-log/2026-09-24.md
  • nexus-pro-web-plus@85b7de0(本機工作區已讀,不宣稱工作區全淨)
  • nexus-pro-web-plus/features/forms/api.ts
  • nexus-pro-web-plus/features/forms/FormsPage.tsx
  • nexus-pro-web-plus/features/forms/_components/FormModal.tsx
  • nexus-pro-web-plus/features/forms/workflow-preview.contract.test.tsx
  • nexus-pro-web-plus/features/forms/_components/WorkflowPanel.tsx
NexusPro 開發指南以程式碼為準 · 以驗證為據