申請單異動從建立到完成怎麼運作

這篇寫給第一次查申請單異動的人。
先不要急著 grep。先判斷問題發生在哪一段:建立、送出、處理存檔,還是完成。

先記住一句話

建立 = 選要辦什麼
送出 = 確認這張單能不能成立
處理存檔 = 把細節補齊,必要時先建立後續流程要用的資料
完成 = 真的把結果寫進權狀、使用人、管理費、通報或歷史紀錄

所以申請單不是一按送出就全部生效。

建立時只是選「這張單要辦哪些異動」。
送出時只代表「這張申請單可以成立」。
處理存檔時,系統才會把一些後面流程需要先看到的資料建出來。
完成時,才真的套用異動結果。

整體流程

1. 建立申請
   → 查權狀
   → 下拉框只顯示目前可辦的異動
   → 前端做第一層提醒與禁用
 
2. 送出申請
   → 後端重新檢查互斥、順序、狀態與必填
   → 通過後建立 pending 申請單
 
3. 處理存檔
   → 詳情頁補細節
   → 晉塔、遷出、預繳這類需要提早被其他流程看到的資料,會在這裡先 materialize
 
4. 完成申請
   → 後端最後守門
   → 缺資料就擋
   → 通過後才真的改正式資料

1. 建立時:下拉框為什麼有些項目不能選?

建立畫面的下拉框不是單純讀一張選項表。

目前會先跑 ModificationValidationManager.GetAvailableModificationTypesAsync,大致做四件事:

  1. 看產品類型。
    • 塔位只顯示塔位可辦的異動。
    • 蓮位只顯示蓮位可辦的異動。
    • 禮儀只顯示禮儀可辦的異動。
  2. 排除後端目前不接受的類型。
    • 有些資料庫裡有 code,但送出後端不支援,畫面就不要讓人選到死路。
  3. 看這張權狀有沒有 pending / processing 的申請。
    • 如果已有進行中的申請,會依卡控規則把互斥項目從下拉框移除或禁用。
  4. 回傳幾個前端需要的提示欄位。
    • soloOnly:只能單獨辦。
    • firstOnly:只能排第一順位。
    • systemChecks:人工把關提示。
    • isEnshrined:前端判斷是否可以自動加存放證補發。

三張設定表分工

白話說明
tb_modification_type異動項目字典:名稱、產品類型、是否只能單辦、是否只能第一順位
tb_modification_control_rule歷史/在途卡控:這張權狀已經有某種進行中異動時,哪些異動不能再選
tb_modification_system_check人工提醒清單:顯示給經辦看,不是後端硬卡控

最容易誤會的是 system_check

它是提醒,不是守門。
真正會擋下來的是後端 method,例如 ValidateItemsAsyncValidateProductStatusAsync

2. 前端選項卡控:畫面會先擋,但不能只靠畫面

前端主要用 useModificationGating 判斷某個選項能不能選。

它會看:

  • 後端回來的 blockedTypeIds
  • 目前同一權狀已選了哪些項目
  • soloOnly
  • firstOnly

建立頁逐列下拉和批次異動 modal 都共用這個 composable,避免兩個畫面各寫一套規則。

自動加項

目前有兩種常見自動加項。

使用者選了什麼系統可能自動加什麼條件
繼承、遺失補發存放證補發權狀已晉塔,而且存放證補發沒有被卡控
晉塔/安奉預繳管理費申請查不到可綁定的管理費,而且預繳項目可選

這些自動加項只是幫使用者少點幾下。

使用者仍可手動刪除。
後端送出時也會再驗一次,所以前端漏擋不代表資料可以進系統。

3. 送出時:為什麼後端還要再擋一次?

因為前端只能改善操作體驗,不能當資料安全邊界。

真正送出時會進 ApplicationCreateService.SubmitApplicationAsync,後端會重新守門。

目前送出端重點檢查:

  1. 非本人不得查詢要確認紙本簽署。
  2. 同一權狀的異動組合要再跑 ValidateItemsAsync
  3. 客戶層級互斥要擋。
  4. 每個權狀狀態要再跑 ValidateProductStatusAsync
  5. 晉塔/安奉如果已帶使用人明細,就要檢查管理費與日期。
  6. 管理費退費要確認真的有可退費期數。
  7. 同一權狀不可有重複進行中的申請,晉塔/安奉例外。
  8. 同一次送出,同一權狀不可重複選同一個異動類型。

同一權狀常見互斥

不可同時選白話原因
轉讓 + 繼承持有人變更方向不同,不能同張權狀同時辦
換位 + 換裝一個動位置,一個動規格,語意衝突
退費 + 預繳帳務方向相反
晉塔/安奉 + 換裝一邊準備入位,一邊改規格,不能混在同一權狀同時做

狀態卡控

ValidateProductStatusAsync 是建立與送出共用的狀態守門。

常見規則:

異動會被擋的情況
全部異動權狀已作廢、位置鎖定
晉塔/安奉未選位、禮儀產品、已晉塔
換位/換裝未選位、已晉塔
遷出尚未晉塔/安奉
存放證補發尚未晉塔
名條變更尚未晉塔
蓮位重刻尚未安奉
退貨作廢已晉塔時不能直接退,要先遷出
預繳管理費目前沒有額外狀態前置

4. 處理存檔:不是完成,但有些正式資料會先建立

申請送出後,多數資料還只是申請單 item 的 JSON。

使用者進詳情頁按「處理」並儲存時,會進 ApplicationActionService.UpdateItemAsync
這裡會先用 ApplicationItemDetailWriter 把明細寫回 tb_application_items

接著,部分異動會在處理存檔時先 materialize。

什麼叫 materialize?

白話說,就是:

把申請單 JSON 裡的暫存資料,轉成真正業務表裡的資料。

不是每種異動都在同一個時間點 materialize。

判斷原則是:

後面流程需要先看到的,就在處理存檔時先建立。
只有完成後才該生效的,就留到完成時再套用。

處理存檔時會先建立的常見資料

異動處理存檔時做什麼為什麼要提早
晉塔/安奉建或更新 pending enshrinement,並建立 pending 通報現場通報需要先看到
遷出建或更新 move_outs 與相關通報資料遷出日期、使用人與後續退費需要先能查
預繳管理費建或更新 management_fees晉塔明細要能選到可綁定的管理費

這類流程的 guard 也應該落在處理存檔端。
也就是說,既然資料在處理存檔時建立,必填檢查也要在這裡擋,不應等到完成才發現資料不完整。

5. 晉塔/安奉通報:為什麼送出後通報頁不一定立刻有資料?

晉塔/安奉要特別分清楚:

申請單 = 行政流程
通報 = 現場工作通知

目前流程是:

建立晉塔/安奉申請
→ 送出後維持 pending
→ 到申請詳情頁填使用人、日期、管理費
→ 儲存處理資料
→ 系統建立 pending enshrinement
→ 系統建立 pending 通報
→ 現場完成通報
→ 主要晉塔/安奉才正式入位並配發使用者編號

所以「申請送出」不等於「已經有通報」。
要到詳情資料完整並儲存後,才會建立通報。

暫厝通報和主要通報不同

類型完成後代表什麼
暫厝通報只代表暫厝工作完成,不會正式入位
主要晉塔/安奉通報正式入位,enshrinements.f_in 會變成 '1',並配發使用者編號

如果看到新建晉塔沒有使用者編號,先不要判定 bug。
使用者編號是在主要通報完成時配發,不是申請單送出或申請單完成時配發。

6. 完成:缺資料不能白完成

完成時會進 ApplicationActionService.CompleteAsync

完成端的責任是:

  1. 確認申請單還能完成。
  2. 對需要完成端生效的異動做最後 guard。
  3. 套用真正的業務副作用。
  4. 標記申請單與 item 為 completed。
  5. 寫入狀態紀錄與必要歷史資料。

結果型異動通常在完成時生效

異動完成時做什麼
名條變更更新 enshrinement 使用人姓名,寫異動紀錄
蓮位重刻建歷史快照,更新牌位內容,寫異動紀錄
執行人變更更新產品上的執行人欄位
使用人變更更新產品上的使用人欄位
履約日期更新履約日期與狀態
繳費狀態更新繳款資料
轉讓/繼承寫 transfers,改持有人,寫異動紀錄
權狀補發取新號或更新權狀資料,寫異動紀錄
非本人不得查詢建立查詢限制資料

白完成是什麼?

白完成就是:

申請單被標記完成了,
但真正該改的業務資料沒有改。

這通常比直接報錯更危險,因為使用者會以為事情已經辦完。

目前完成端有 ValidateWhiteCompletionGuards,會擋下幾類缺資料的異動:

類型缺什麼會被擋
執行人變更缺新執行人姓名,且不是不指定
使用人變更缺新使用人姓名,且不是不指定
履約日期缺有效履約日期
繳費狀態缺繳款碼
名條變更缺原姓名或新姓名
蓮位重刻缺新牌位內容

這裡的原則很簡單:

完成後才會生效的異動,完成前就一定要檢查資料夠不夠。
資料不夠,就 Fail,不要跳過。

7. 查 bug 時先問哪一句?

不要一開始就問「哪個檔案壞了」。
先問:

問題發生在哪一段?
看到的症狀優先查哪裡
下拉框沒有某個異動GetAvailableModificationTypesAsyncuseModificationGating
畫面能選,但送出被擋SubmitApplicationAsyncValidateItemsAsyncValidateProductStatusAsync
批次異動和逐列選擇行為不同AppCreateBatchModifyModal.vue 是否共用 useModificationGating
送出後通報頁沒資料先確認是否已進詳情頁填使用人/日期並儲存
詳情頁保存後欄位不見ApplicationItemDetailWritertb_application_items.f_nameplate_users_json
處理存檔後沒產生通報或業務表資料UpdateItemAsync 後段的 materialize 分流
完成被擋CompleteAsync 的完成端 guard
完成成功但正式資料沒變ApplyChangeTypeEffectsAsync 與各 Apply*Async
權狀修改查詢清單是舊資料ComprehensiveQueryService
權狀修改編輯頁是舊資料CertificateModificationAppService.QueryByIdAsync
已遷出但不能退費同時查 move_outsmanagement_fees.f_auth_no
附件看得到但不能上傳前端顯示權限與 MediaController.Upload 後端授權是否同一套

8. 幾個已知陷阱

8.1 system_check 不是卡控

tb_modification_system_check 是人工提醒,不是後端硬擋。

如果要查「為什麼真的被擋」,看 ValidateItemsAsyncValidateProductStatusAsyncSubmitApplicationAsync 或完成端 guard。

8.2 晉塔明細空白送出,不一定是 bug

建立申請時可以只選「晉塔/安奉」。
詳細使用人、日期、管理費可以留到詳情頁處理。

如果 submit 時已帶了使用人明細,後端才會檢查每位有效使用人是否有管理費與日期。

8.3 蓮位安奉不一定要有姓名

蓮位安奉的有效使用人不一定只看姓名。

只要有稱位、暫厝日期、主要日期、管理費等實質資料,就可能是有效資料。
不要把「姓名非空」當成唯一判準。

8.4 ParseUsers()ParseNotifList() 用途不同

ParseUsers() 是純粹解析使用人。
ParseNotifList() 是依日期產生通報事件。

如果 guard 要檢查「有沒有使用人缺管理費」,要用 ParseUsers()
如果用 ParseNotifList(),沒有日期的使用人可能被漏掉,guard 會被繞過。

8.5 同一位使用人可以同時有暫厝和主要通報

同一位使用人可能同時有:

  • 暫厝日期
  • 主要晉塔/安奉日期

這會產生兩個事件。
兩個事件可以共用同一筆 enshrinement,但會有不同 notification。

8.6 遷出不要只看 enshrinements.f_in

本系統遷出後,enshrinements.f_in 仍可能維持 '1'

要判斷是否已遷出,要看 move_outs 是否有有效資料。
所以「已入位/已遷出」這類畫面狀態,不能只看 f_in

8.7 管理費消失,先查 syslog

遷出流程本身不應直接把管理費軟刪。

如果管理費突然不見,先查:

  1. management_fees.x_bool
  2. management_fees.mod_dt
  3. tb_syslog
  4. 相關退費申請的操作時間

很多時候是會計退費按鈕即時軟刪,不是遷出 materialize 做的。

8.8 權狀修改查詢清單和編輯頁不是同一條讀路徑

清單讀 ComprehensiveQueryService
編輯頁讀 CertificateModificationAppService.QueryByIdAsync

所以「清單正確但編輯頁錯」或「編輯頁正確但清單錯」都可能發生,要分開查。

8.9 dropdown 空白通常是三層不一致

常見原因:

  1. 申請單 JSON 存的是英文 code。
  2. materialize 時沒有轉成 DB 期待的中文 optionName。
  3. 權狀修改編輯頁 dropdown 用 optionName 比對。

另一種原因是前端沒有 fetch 對應 category 的 options。
DB 有值但畫面空白時,不要只查資料,也要查 option 是否載入。

9. 資料真相表

階段主要資料位置
建立中前端 store
送出後、處理前tb_applicationstb_application_items
詳情保存後tb_application_items.f_nameplate_users_json
晉塔/安奉處理後enshrinementstb_enshrinement_notificationmanagement_fees
遷出處理後move_outs
預繳處理後management_fees
完成後productsenshrinementstransfersmodifications、各類歷史表
權狀修改查詢ComprehensiveQueryService 組出的查詢結果
權狀修改編輯CertificateModificationAppService.QueryByIdAsync

10. 工程位置速查

想查什麼入口
下拉框可選項目ModificationValidationManager.GetAvailableModificationTypesAsync
同權狀組合卡控ModificationValidationManager.ValidateItemsAsync
權狀狀態卡控ModificationValidationManager.ValidateProductStatusAsync
送出主流程ApplicationCreateService.SubmitApplicationAsync
前端下拉卡控useModificationGating
前端自動加項application-create-store.tstoggleModificationTypeensurePrepayForEnshrinement
詳情頁保存ApplicationActionService.UpdateItemAsync
詳情欄位寫回 item JSONApplicationItemDetailWriter.ApplyDetail
晉塔/安奉 materializeApplicationActionService.MaterializeEnshrinementAsync
遷出 materializeMoveoutApplicationManager.MaterializeMoveoutAsync
預繳 materializeApplicationActionService.MaterializePrepayFeesAsync
完成主流程ApplicationActionService.CompleteAsync
白完成 guardApplicationActionService.ValidateWhiteCompletionGuards
完成端套用異動ApplicationActionService.ApplyChangeTypeEffectsAsync
通報完成正式入位EnshrinementNotificationService.CompleteNotificationAsync
權狀修改清單ComprehensiveQueryService
權狀修改編輯頁CertificateModificationAppService.QueryByIdAsync

本次驗證範圍

本頁於 2026-06-30 對照現行程式碼與測試整理:

  • ModificationValidationManager
  • ApplicationCreateService
  • application-create-store.ts
  • useModificationGating
  • AppCreateStep2.vue
  • AppCreateBatchModifyModal.vue
  • ApplicationActionService
  • ApplicationItemDetailWriter
  • MoveoutApplicationManager
  • EnshrinementNotificationService
  • application-modification-debugging skill 的除錯路徑

本頁以現行程式碼、前端入口、測試與除錯 skill 路徑為準,不把討論串當長期權威。

本次未重新查詢 grace2,因此不宣稱資料庫個別存量資料已逐筆重驗。