申請單異動從建立到完成怎麼運作
這篇寫給第一次查申請單異動的人。
先不要急著 grep。先判斷問題發生在哪一段:建立、送出、處理存檔,還是完成。
先記住一句話
建立 = 選要辦什麼
送出 = 確認這張單能不能成立
處理存檔 = 把細節補齊,必要時先建立後續流程要用的資料
完成 = 真的把結果寫進權狀、使用人、管理費、通報或歷史紀錄所以申請單不是一按送出就全部生效。
建立時只是選「這張單要辦哪些異動」。
送出時只代表「這張申請單可以成立」。
處理存檔時,系統才會把一些後面流程需要先看到的資料建出來。
完成時,才真的套用異動結果。
整體流程
1. 建立申請
→ 查權狀
→ 下拉框只顯示目前可辦的異動
→ 前端做第一層提醒與禁用
2. 送出申請
→ 後端重新檢查互斥、順序、狀態與必填
→ 通過後建立 pending 申請單
3. 處理存檔
→ 詳情頁補細節
→ 晉塔、遷出、預繳這類需要提早被其他流程看到的資料,會在這裡先 materialize
4. 完成申請
→ 後端最後守門
→ 缺資料就擋
→ 通過後才真的改正式資料1. 建立時:下拉框為什麼有些項目不能選?
建立畫面的下拉框不是單純讀一張選項表。
目前會先跑 ModificationValidationManager.GetAvailableModificationTypesAsync,大致做四件事:
- 看產品類型。
- 塔位只顯示塔位可辦的異動。
- 蓮位只顯示蓮位可辦的異動。
- 禮儀只顯示禮儀可辦的異動。
- 排除後端目前不接受的類型。
- 有些資料庫裡有 code,但送出後端不支援,畫面就不要讓人選到死路。
- 看這張權狀有沒有 pending / processing 的申請。
- 如果已有進行中的申請,會依卡控規則把互斥項目從下拉框移除或禁用。
- 回傳幾個前端需要的提示欄位。
soloOnly:只能單獨辦。firstOnly:只能排第一順位。systemChecks:人工把關提示。isEnshrined:前端判斷是否可以自動加存放證補發。
三張設定表分工
| 表 | 白話說明 |
|---|---|
tb_modification_type | 異動項目字典:名稱、產品類型、是否只能單辦、是否只能第一順位 |
tb_modification_control_rule | 歷史/在途卡控:這張權狀已經有某種進行中異動時,哪些異動不能再選 |
tb_modification_system_check | 人工提醒清單:顯示給經辦看,不是後端硬卡控 |
最容易誤會的是 system_check。
它是提醒,不是守門。
真正會擋下來的是後端 method,例如 ValidateItemsAsync 和 ValidateProductStatusAsync。
2. 前端選項卡控:畫面會先擋,但不能只靠畫面
前端主要用 useModificationGating 判斷某個選項能不能選。
它會看:
- 後端回來的
blockedTypeIds - 目前同一權狀已選了哪些項目
soloOnlyfirstOnly
建立頁逐列下拉和批次異動 modal 都共用這個 composable,避免兩個畫面各寫一套規則。
自動加項
目前有兩種常見自動加項。
| 使用者選了什麼 | 系統可能自動加什麼 | 條件 |
|---|---|---|
| 繼承、遺失補發 | 存放證補發 | 權狀已晉塔,而且存放證補發沒有被卡控 |
| 晉塔/安奉 | 預繳管理費申請 | 查不到可綁定的管理費,而且預繳項目可選 |
這些自動加項只是幫使用者少點幾下。
使用者仍可手動刪除。
後端送出時也會再驗一次,所以前端漏擋不代表資料可以進系統。
3. 送出時:為什麼後端還要再擋一次?
因為前端只能改善操作體驗,不能當資料安全邊界。
真正送出時會進 ApplicationCreateService.SubmitApplicationAsync,後端會重新守門。
目前送出端重點檢查:
- 非本人不得查詢要確認紙本簽署。
- 同一權狀的異動組合要再跑
ValidateItemsAsync。 - 客戶層級互斥要擋。
- 每個權狀狀態要再跑
ValidateProductStatusAsync。 - 晉塔/安奉如果已帶使用人明細,就要檢查管理費與日期。
- 管理費退費要確認真的有可退費期數。
- 同一權狀不可有重複進行中的申請,晉塔/安奉例外。
- 同一次送出,同一權狀不可重複選同一個異動類型。
同一權狀常見互斥
| 不可同時選 | 白話原因 |
|---|---|
| 轉讓 + 繼承 | 持有人變更方向不同,不能同張權狀同時辦 |
| 換位 + 換裝 | 一個動位置,一個動規格,語意衝突 |
| 退費 + 預繳 | 帳務方向相反 |
| 晉塔/安奉 + 換裝 | 一邊準備入位,一邊改規格,不能混在同一權狀同時做 |
狀態卡控
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。
完成端的責任是:
- 確認申請單還能完成。
- 對需要完成端生效的異動做最後 guard。
- 套用真正的業務副作用。
- 標記申請單與 item 為 completed。
- 寫入狀態紀錄與必要歷史資料。
結果型異動通常在完成時生效
| 異動 | 完成時做什麼 |
|---|---|
| 名條變更 | 更新 enshrinement 使用人姓名,寫異動紀錄 |
| 蓮位重刻 | 建歷史快照,更新牌位內容,寫異動紀錄 |
| 執行人變更 | 更新產品上的執行人欄位 |
| 使用人變更 | 更新產品上的使用人欄位 |
| 履約日期 | 更新履約日期與狀態 |
| 繳費狀態 | 更新繳款資料 |
| 轉讓/繼承 | 寫 transfers,改持有人,寫異動紀錄 |
| 權狀補發 | 取新號或更新權狀資料,寫異動紀錄 |
| 非本人不得查詢 | 建立查詢限制資料 |
白完成是什麼?
白完成就是:
申請單被標記完成了,
但真正該改的業務資料沒有改。這通常比直接報錯更危險,因為使用者會以為事情已經辦完。
目前完成端有 ValidateWhiteCompletionGuards,會擋下幾類缺資料的異動:
| 類型 | 缺什麼會被擋 |
|---|---|
| 執行人變更 | 缺新執行人姓名,且不是不指定 |
| 使用人變更 | 缺新使用人姓名,且不是不指定 |
| 履約日期 | 缺有效履約日期 |
| 繳費狀態 | 缺繳款碼 |
| 名條變更 | 缺原姓名或新姓名 |
| 蓮位重刻 | 缺新牌位內容 |
這裡的原則很簡單:
完成後才會生效的異動,完成前就一定要檢查資料夠不夠。
資料不夠,就 Fail,不要跳過。7. 查 bug 時先問哪一句?
不要一開始就問「哪個檔案壞了」。
先問:
問題發生在哪一段?| 看到的症狀 | 優先查哪裡 |
|---|---|
| 下拉框沒有某個異動 | GetAvailableModificationTypesAsync、useModificationGating |
| 畫面能選,但送出被擋 | SubmitApplicationAsync、ValidateItemsAsync、ValidateProductStatusAsync |
| 批次異動和逐列選擇行為不同 | AppCreateBatchModifyModal.vue 是否共用 useModificationGating |
| 送出後通報頁沒資料 | 先確認是否已進詳情頁填使用人/日期並儲存 |
| 詳情頁保存後欄位不見 | ApplicationItemDetailWriter、tb_application_items.f_nameplate_users_json |
| 處理存檔後沒產生通報或業務表資料 | UpdateItemAsync 後段的 materialize 分流 |
| 完成被擋 | CompleteAsync 的完成端 guard |
| 完成成功但正式資料沒變 | ApplyChangeTypeEffectsAsync 與各 Apply*Async |
| 權狀修改查詢清單是舊資料 | ComprehensiveQueryService |
| 權狀修改編輯頁是舊資料 | CertificateModificationAppService.QueryByIdAsync |
| 已遷出但不能退費 | 同時查 move_outs 和 management_fees.f_auth_no |
| 附件看得到但不能上傳 | 前端顯示權限與 MediaController.Upload 後端授權是否同一套 |
8. 幾個已知陷阱
8.1 system_check 不是卡控
tb_modification_system_check 是人工提醒,不是後端硬擋。
如果要查「為什麼真的被擋」,看 ValidateItemsAsync、ValidateProductStatusAsync、SubmitApplicationAsync 或完成端 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
遷出流程本身不應直接把管理費軟刪。
如果管理費突然不見,先查:
management_fees.x_boolmanagement_fees.mod_dttb_syslog- 相關退費申請的操作時間
很多時候是會計退費按鈕即時軟刪,不是遷出 materialize 做的。
8.8 權狀修改查詢清單和編輯頁不是同一條讀路徑
清單讀 ComprehensiveQueryService。
編輯頁讀 CertificateModificationAppService.QueryByIdAsync。
所以「清單正確但編輯頁錯」或「編輯頁正確但清單錯」都可能發生,要分開查。
8.9 dropdown 空白通常是三層不一致
常見原因:
- 申請單 JSON 存的是英文 code。
- materialize 時沒有轉成 DB 期待的中文 optionName。
- 權狀修改編輯頁 dropdown 用 optionName 比對。
另一種原因是前端沒有 fetch 對應 category 的 options。
DB 有值但畫面空白時,不要只查資料,也要查 option 是否載入。
9. 資料真相表
| 階段 | 主要資料位置 |
|---|---|
| 建立中 | 前端 store |
| 送出後、處理前 | tb_applications、tb_application_items |
| 詳情保存後 | tb_application_items.f_nameplate_users_json |
| 晉塔/安奉處理後 | enshrinements、tb_enshrinement_notification、management_fees |
| 遷出處理後 | move_outs |
| 預繳處理後 | management_fees |
| 完成後 | products、enshrinements、transfers、modifications、各類歷史表 |
| 權狀修改查詢 | ComprehensiveQueryService 組出的查詢結果 |
| 權狀修改編輯 | CertificateModificationAppService.QueryByIdAsync |
10. 工程位置速查
| 想查什麼 | 入口 |
|---|---|
| 下拉框可選項目 | ModificationValidationManager.GetAvailableModificationTypesAsync |
| 同權狀組合卡控 | ModificationValidationManager.ValidateItemsAsync |
| 權狀狀態卡控 | ModificationValidationManager.ValidateProductStatusAsync |
| 送出主流程 | ApplicationCreateService.SubmitApplicationAsync |
| 前端下拉卡控 | useModificationGating |
| 前端自動加項 | application-create-store.ts 的 toggleModificationType、ensurePrepayForEnshrinement |
| 詳情頁保存 | ApplicationActionService.UpdateItemAsync |
| 詳情欄位寫回 item JSON | ApplicationItemDetailWriter.ApplyDetail |
| 晉塔/安奉 materialize | ApplicationActionService.MaterializeEnshrinementAsync |
| 遷出 materialize | MoveoutApplicationManager.MaterializeMoveoutAsync |
| 預繳 materialize | ApplicationActionService.MaterializePrepayFeesAsync |
| 完成主流程 | ApplicationActionService.CompleteAsync |
| 白完成 guard | ApplicationActionService.ValidateWhiteCompletionGuards |
| 完成端套用異動 | ApplicationActionService.ApplyChangeTypeEffectsAsync |
| 通報完成正式入位 | EnshrinementNotificationService.CompleteNotificationAsync |
| 權狀修改清單 | ComprehensiveQueryService |
| 權狀修改編輯頁 | CertificateModificationAppService.QueryByIdAsync |
本次驗證範圍
本頁於 2026-06-30 對照現行程式碼與測試整理:
ModificationValidationManagerApplicationCreateServiceapplication-create-store.tsuseModificationGatingAppCreateStep2.vueAppCreateBatchModifyModal.vueApplicationActionServiceApplicationItemDetailWriterMoveoutApplicationManagerEnshrinementNotificationServiceapplication-modification-debuggingskill 的除錯路徑
本頁以現行程式碼、前端入口、測試與除錯 skill 路徑為準,不把討論串當長期權威。
本次未重新查詢 grace2,因此不宣稱資料庫個別存量資料已逐筆重驗。