本章目標與前置條件
讀完後,你應能從 template definition 追到 form instance,理解 revision、兩種 quota、attachment 與 workflow preview 的責任,知道請假表單怎麼把送審交給 attendance,並知道 fixture UI 不能代替 live API。前置條件是能閱讀 JSON schema、版本號、SWR mutation 與基本審批流程。
功能現況
| 區塊 | 後端已註冊的 API/規則 | 前端現況 |
|---|---|---|
| Designer | field-types、templates list/get/create/update(沒有刪除路由)、validate、publish、versions、published、quota-policy(GET/PUT)、data-sources | /workspace/forms 的 features/form-designer 有欄位(含 table 明細欄)、layout、condition、stage 編輯與 validation;沒有 quota 編輯,只讀 quota-policy、不呼叫 PUT |
| Runtime | drafts、建草稿前的 template preview、forms list/detail/definition、update/delete、submit、duplicate、workflow、workflow/preview、approve/reject/return/withdraw/admin-cancel、export、本人 quota 用量 | /forms 的 features/forms 有 list、modal、fields、template preview、workflow panel 與 action mutation |
| 附件 | /v1/attachments create/metadata/content,form adapter 串接 upload lifecycle | useUploadFormAttachment 先建 metadata,再上傳 content,保留 schema 檢查 |
| capability | attendance.leave、attendance.overtime、attendance.correction、attendance.leave_cancellation、attendance.overtime_cancellation;綁定隨 definition 保存 | designer 沒有 capability 綁定編輯 UI,features/forms/capabilityFields.ts 只是 designer 與申請端共用的 schema 詞彙;加班時數推導、銷假紀錄回顯、代理人員工編號帶入都在申請端 FormsPage |
| fixture boundary | development + NEXT_PUBLIC_MSW=true 時可使用 fixture;admin cancel fixture 明確拒絕偽造成功 | live mode 需 /v1/me 與後端;本文未把 fixture 當部署證據 |
核心概念
Definition schemaVersion=1
目前前端 schema 將 definition 拆成 fields、layouts、stages、entryStageId、capabilities 與 summaryFieldIds。field type(含 table)、condition operator、approval mode、security classification、data source key 等都是封閉詞彙;新增詞彙要同步 domain validator、OpenAPI、前端 schema 與 contract test。definition 在 DB 以 JSONB 保存,只檢查是 JSON object,詞彙沒有 DB constraint,後端 validator 是唯一把關。
查找值、明細表格與代理人欄位
- 查找欄位的值是 server lookup 回傳的
value。leave_types一律回假別 UUID(code只為歷史 definition 保留),attendance.leave送出時以 UUID 解析,送 code 會 422。 table欄位以table_columns定義 1–20 個子欄(文字、數值、日期、選項或核取方塊,不可巢狀,也不可有預設值、唯讀或跨欄參照);值是最多 100 列、以子欄 ID 為鍵的物件陣列,必填子欄在送出時才檢查。- 請假單種子另有代理人欄位:
proxy(employeeslookup)與唯讀的proxy_id;前端依所選代理人自動帶入員工編號(features/forms/proxyEmployeeNo.ts)。
Revision、quota 與 effect
Instance 每次保存有 revision,前端會把 revision 放入 workflow preview cache key,避免舊結果覆蓋新草稿。
這裡的 quota 是表單模板層級的「送單次數」配額,和假別天數餘額無關:送出時在表單交易內預留,quota_state 由 none 變 reserved,核准後為 consumed、其他終態為 released;額滿回 409 quota_exhausted(50600),quota_not_supported_for_template(50601)表示模板不支援送單次數限制。GET /v1/forms/templates/:formTemplateId/quota 的 reserved、consumed、remaining 由 server 判斷。假別的預扣與扣抵見假勤管理。
表單狀態和 attendance effect 狀態分開:核准後 effect_status 先是 pending,attendance 回報後才變 applied 或 failed(帶 effect_error_code),effectStatus 不能被簡化成 approved。
Preview 只計算,不寫業務狀態
POST /v1/forms/{formInstanceId}/workflow/preview 要求封閉空物件 {},service 只讀已保存的 draft/returned values,回傳 complete、stages、assignees、outcome 與 warnings。planned、skipped、undetermined 不是同義詞;資料不足時要保留 warning,不可猜成 false。建草稿前的 POST /v1/forms/templates/{formTemplateId}/preview 同樣只收 {},回傳 template、definition、applicant、預設值與同一套 workflow 預覽,不建立 instance。
請假申請的交接
請假表單(attendance.leave)送出時依序發生:
- 表單服務先以獨立 attendance 交易寫 leave admission,凍結政策版本與
hours_per_day;假別停用回 409leave_type_inactive,天制假別缺hours_per_day回 409leave_unit_conversion_unconfigured。 - 表單交易(tenant transaction 內的同步 PostgreSQL 狀態機)鎖定實例、比對 revision、預留送單次數 quota,並寫 outbox
form.leave.reservation_requested。交易失敗會補償刪除 admission;commit 結果不明時保留。 - worker 的 attendance effect 非同步預扣假別餘額,所以送出 API 回 200 時預扣可能尚未完成。之後核准發
form.leave.approved,拒絕、退回、撤回或管理員取消發form.leave.reservation_released。
一條請求鏈
正在繪製架構圖…
查看圖表原始碼
flowchart LR Designer["FormDesigner(/workspace/forms)"] --> Template["/v1/forms/templates"] Applicant["FormsPage(/forms)"] --> Draft["/v1/forms/drafts + /v1/forms"] Applicant --> Attach["/v1/attachments"] Draft --> Preview["workflow/preview"] Draft --> Submit["submit"] Submit --> Admission["請假單才有:leave admission"] Admission --> Tx["表單交易:quota 預留 + outbox"] Submit --> Tx Tx -- "reservation_requested" --> Effect["worker:attendance effect"] Tx --> Action["approve/reject/return/withdraw/admin-cancel"] Action -- "approved/reservation_released" --> Effect
Live API 的 features/forms/api.ts 刻意以 zod strict schema 解析 response(多數模組預設 .passthrough()),並將 cache key 以 tenant/account/fixture 狀態隔離。create、update、delete、submit、duplicate、withdraw、admin-cancel 成功後,共用的 useRevalidateFormLists 只重抓表單列表與 quota key;workflow preview 靠 key 內的 revision 自動重取,持久化 workflow 目前只在 admin-cancel 後手動 mutate。失敗時保留使用者輸入,不要以 fixture fallback 蓋掉錯誤。
實作步驟與示例
- 先鎖定是 template、instance、workflow 或 attachment 改動,再在
api/openapi.yaml寫清 wire contract 與 BREAKING 影響;路由權限點以internal/route/testdata/registry.golden為準。 - 改 definition 時先更新 field/condition/stage 的 domain validator,再更新 OpenAPI、service、handler 與前端 schema;definition 是 JSONB,詞彙不靠 migration 把關,已發布版本只能新增、不能改寫。
- 表單提交必須通過 server validation、quota、permission、revision/idempotency 與 capability mapping;前端只做即時提示。submit、approve、reject、return、withdraw、admin-cancel 都要
Idempotency-Key;reject、return、admin-cancel 要非空 comment(最多 1000 字);admin-cancel 另需 tenant-wide 的workflow.run.admin_cancel決策,否則 403;刪除草稿要帶?revision=,duplicate 則在 body 帶revision。 - workflow preview 必須用最新 draft revision;若回應落後,前端只重取一次,仍落後就顯示錯誤,而非展示舊 stage。
POST /v1/forms/00000000-0000-4000-8000-000000000001/workflow/preview
Content-Type: application/json
{}這個 UUID 僅為格式示例,不是可用資料。要驗證時,使用受控 tenant 內真實 draft,確認回應 revision 等於或追上草稿 revision,並逐一檢查 stage outcome、warning 與 assignee;不要把空 body 以外的草稿值塞入 preview endpoint。
驗證矩陣
| 驗證問題 | 程式碼/測試證據 | 仍需另外做的驗收 |
|---|---|---|
| definition 欄位/condition | features/form-designer/api.ts、validateFormDefinition 測試、internal/domain/form/validate_test.go、internal/domain/form/validation_table_test.go | live template validate/publish |
| runtime draft/revision | features/forms/create-draft.contract.test.tsx、features/forms/resume-draft.test.tsx、features/forms/list-revalidation.contract.test.tsx、internal/api/v1/form_test.go | 真實租戶保存與並行 revision |
| workflow/template preview | internal/api/v1/form_workflow_preview_test.go、internal/api/v1/form_template_preview_test.go、features/forms/workflow-preview.contract.test.tsx、features/forms/template-preview.contract.test.tsx | 後端 service、角色解析與資料庫投影 |
| actions/quota | features/forms/submitDraft.test.ts、features/forms/admin-cancel.contract.test.tsx、internal/api/v1/form_admin_cancel_test.go、internal/domain/form/quota_test.go、tests/unit/forminstance/events_test.go | 請假送出與核准的 effect 已有 2026-09-22 本機實測(見假勤管理);withdraw/reject/return 的 release 尚未實測 |
| table 欄位 | internal/api/v1/form_table_json_test.go、internal/service/form/instance/values_table_test.go、features/forms/table-values.test.ts | live 保存與送出 |
| attachment | features/forms/upload-identity.test.tsx、internal/api/v1/attachments_test.go | object storage、病毒掃描、內容下載 |
常見錯誤
- 直接修改已發布 version;已發布版本不可改寫,應改 template 草稿(PUT 帶 revision),再走 validate/publish 產生新 version。
- 用表單畫面自行算
remaining或把complete=false當作條件不成立。 - 把
quotaState(送單次數)當成假別餘額,或以為 designer 能設定 quota policy。 - 假別欄位送 code;
attendance.leave只接受 lookup 回傳的假別 UUID。 - 忘記把 draft revision 放入 cache key,導致 preview 看到上一版 stage。
- 以「fixture action 成功」宣稱後端 action 成功;admin cancel 在 fixture 中刻意不偽造 authority。
- 上傳 content 前跳過 metadata/identity 檢查,或把附件的 secret/object key 寫進文件與 log。