資產狀態機
這篇寫給想知道「某個功能現在是什麼狀態、該去哪裡查真相」的人。
這個系統有很多地方都在問「這筆資料現在是什麼狀態」,但狀態欄位分散在很多張表、很多層。本頁整理各功能的狀態各自該去哪裡查,以及「作廢」「還原」這類狀態轉換實際上動了什麼。
先記住一句話
系統裡沒有一個「萬用狀態欄位」管所有東西。每個功能有自己的狀態,但同一個功能的狀態應該只有一個地方說了算——這篇就是列出「這個功能的狀態,真正該去哪裡查」。
各功能的狀態,該去哪裡查?
| 功能 | 狀態定義在哪裡 | 值大致有哪些 | 白話說明 |
|---|---|---|---|
| 申請單 | ApplicationStatus | 草稿/待處理/處理中/已完成/已作廢(另外還有簽署、附件、發票三個獨立小狀態) | 一張申請單同時有「主狀態」跟三個附屬小狀態,查的時候要先分清楚問的是哪一個 |
| 權狀申購 | 沿用 ApplicationStatus,不再自己另外推算 | 同上 | 以前這裡自己算過一次「這張算不算完成」,跟申請單本身的狀態對不上;後來已經改成直接讀申請單狀態 |
| 權狀修改(詳情頁/查詢清單) | 同樣沿用 ApplicationStatus | 草稿/非草稿 | 這裡只在意「還是草稿」還是「不是草稿」,細節仍以申請單狀態為準 |
| 綜合查詢列表 | 顯示邏輯呼叫 ApplicationStatus 的轉換方法,不自己重複判斷 | 同上 | 以前這裡自己又寫了一份判斷邏輯轉成顯示文字,容易跟正式狀態兜不起來 |
| 晉塔/安奉資料預填 | PrefillStatus | 待處理/處理中/已完成/已作廢 | 這是「預填」這個小功能自己的狀態,跟晉塔本身、跟申請單都是不同的東西,不要混著查 |
| 新購資料預填 | PrefillStatus(另一組值) | 待處理/已匯入/已逾期 | 名字跟上面那個很像(都可能叫 pending),但意思完全不同,這組是舊站資料匯入用的暫存區狀態 |
| 晉塔/安奉通報 | NotificationStatus | 待處理/進行中/已完成/逾期/已取消 | 通報是「現場工作通知」,狀態跟申請單、跟預填都不是同一件事 |
| 管理費催繳 | ProductUseStatusHelper 即時算出來,不是存表裡固定不變的值 | 依到期天數換算出的階段 | 這裡刻意不存死值,每次查詢當下重新算,避免資料變舊卻沒人更新 |
| 無塔位暫厝 | 自己一個狀態欄位 | 進行中/已完成/已作廢 | 只有無塔位暫厝這個小功能自己在用 |
橫向共用、大部分功能都會參考的兩個權威:
- 使用狀態(
ProductUseStatusHelper):這個位置目前是未使用/已晉塔使用中/已遷出。 - 位置狀態(
PositionStatus):這個位置目前是空位/已售出/已預約/鎖定等。
為什麼要特別講這件事?
因為以前這些狀態常常各自手刻一份判斷邏輯,久了就會跟正式資料兜不起來——例如畫面顯示「已完成」,但實際上申請單根本還在處理中。
現在的做法是:同一個功能的狀態顯示,全部改成呼叫同一份權威邏輯,不再各自重複判斷。如果之後又發現某個畫面自己另外寫了一套判斷邏輯,那就是舊問題重新長出來,應該把它改成呼叫對應的權威,而不是繼續各自維護。
以前哪些畫面自己手刻、後來收斂到哪
下面這幾個地方,以前都各自寫過一份狀態判斷,後來陸續收斂掉。列出來是讓你知道「這些坑填過了」,之後要是又冒出類似的自算邏輯,照同樣方式收斂即可。
| 以前哪裡自己算 | 出過什麼問題 | 現在收斂到 | 現行落點(2026-07-01 對照 develop) |
|---|---|---|---|
| 權狀申購的完成徽章 | 自己推一套 completed,跟申請單本身狀態對不上 | 直接讀申請單 f_status、不再自算 | CertificatePurchaseAppService.DeriveApplicationStatus(services/Implementations/Product/CertificatePurchaseAppService.cs:576,已改成單行 passthrough);前端 stores/certificate/certificate-editor-store.ts:69 + components/certificate/CertificatePurchaseEditor.vue:329-343 |
| 綜合查詢列表 | 自己又寫一份狀態文字轉換 | 委派 ApplicationStatus.Main.ToLabel | services/Implementations/ComprehensiveQuery/ComprehensiveQueryService.cs:298(投影先設 null)+ :413-414(回填 ToLabel) |
| 產品使用狀態 | ProductService 本地一份 switch | 委派 ProductUseStatusHelper | services/Implementations/Product/ProductService.cs:255 |
| 晉塔/安奉通報狀態 | 前端自己一份 STATUS_MAP | 後端補 NotificationStatus + options 端點,前端改吃端點 | 狀態常數 repos/Constants/NotificationStatus.cs:18;前端 pages/query/enshrinement-notification.vue:59-62 改用 useNotificationStatusOptions() |
| 晉塔預填/新購預填 | 前端各手刻,而且兩個都叫 pending、語意其實不同 | 後端補 PrefillStatus(兩變體) | repos/Constants/PrefillStatus.cs:16(Enshrine:22/Purchase:71 兩子類) |
| 無塔位暫厝 | FStatus 沒有 enum | 補 TemporaryStorageStatus | 常數 repos/Constants/TemporaryStorageStatus.cs:16;三個寫入點 services/Implementations/TemporaryStorage/TemporaryStorageCrudService.cs:164(建立)/:249(完成)/:311(作廢) |
| 繳交情況徽章 | 直接拿存死的 f_markD 顯示,還把「到期」誤標成「預繳」 | 改吃後端即時推導的期別 stage | services/Mappings/CertificateModificationMappingProfile.cs:72(PayStatus 改 Ignore(),改由 QueryById 即時算);前端 components/certificate/CertificateManagementFeeTable.vue:81-94 只做顏色映射 |
還有一個「暫厝模式」,原本以為散在四個地方、要收斂成一份,後來查下去發現「散四層」這個前提根本不成立(其中一層其實是管理費關係人的自由文字、不是模式欄),所以沒有硬收——這也是個提醒:收斂前先確認「重複」是不是真的重複。
有些狀態舊站就有、有些是這套系統新訂的
判斷一個狀態「權威可不可信」有個快捷方式:看它舊站有沒有對應。
- 舊站就有、這套忠實沿用的:位置狀態、使用狀態、管理費的
f_markD、產品有效/作廢。這幾個ProductUseStatusHelper/PositionStatus是逐字對齊舊站的,基本可信,只要別的地方別再自己手刻一份就好。(唯一例外:f_markD的「規則」對,但直接信 DB 存死的值會過期,所以催繳階段改成每次查詢即時算,見上面。) - 舊站沒有、這套新訂的:申請單四維狀態、晉塔/新購預填、通報
field_status、無塔位暫厝、暫厝模式。這幾個沒有舊站可參照,正是最容易各自手刻、最會 drift 的一批——上面那張收斂清單裡的問題,幾乎全出在這批。
作廢與還原:三層要分清楚
「作廢」跟「還原」不是同一件事在不同地方重複發生,而是三個完全不同層次的動作,各自動的東西也不一樣:
| 層次 | 在辦什麼 | 實際上動了什麼 | 位置會不會變 |
|---|---|---|---|
| 第一層:權狀作廢 | 轉讓/繼承的持有人作廢、補發/換裝的權狀號作廢,連動退費 | 改持有人、改權狀號、管理費軟刪 | 不動位置 |
| 第二層:晉塔/安奉還原 | 正式入位前反悔、要撤銷這次晉塔/安奉 | 清掉使用人綁定,撤銷入位標記 | 不動位置(這是「已使用」狀態內部的還原,不是把位置放回空位) |
| 第三層:購買退貨作廢 | 整張權狀連同購買一起作廢 | 產品標記作廢(ProductStatus.Voided),退回可售 | 位置從「已售出」放回「空位」 |
這三層之所以要分清楚,是因為「位置從已售出變回空位」只會發生在第三層(購買退貨,經 VoidApplicationManager)。
如果在處理晉塔還原(第二層)時位置卻被放回空位,或反過來購買退貨時位置沒有真的釋放,都代表兩層被搞混了——先查清楚現在到底是哪一層的作廢,不要照抄另一層的邏輯。
已經正式入位的權狀,不能直接退貨
第二層還原只適用在「還沒正式入位之前」。如果權狀已經正式完成晉塔/安奉,購買退貨(第三層)會直接被擋下來,訊息是「此權狀已完成晉塔安奉,請先完成遷出再退貨」。
也就是說,已經入位的權狀要走退貨,必須先辦遷出(把人遷出去,這是晉塔流程的逆操作,屬於正式業務流程,不是第二層那種「反悔還原」),遷出完成後才輪得到第三層退貨。看到「已晉塔卻不能退貨」不是 bug,是刻意的先後順序卡控。
三層各自動到什麼、落點在哪
| 要追什麼 | 唯一該看的寫入點(2026-07-01 對照 develop) |
|---|---|
| 第三層「已售出→空位」(唯一會改位置的一層) | 唯一寫入點 VoidApplicationManager.ReleasePositionAsync(services/DomainServices/Shared/VoidApplicationManager.cs:188)。⚠️ 這方法早期文件裡叫 ReleasePositionToVacantAsync、現已改名;而且它會看這筆是不是換位——換位是退回「預約」不是「空位」(:204 只處理已售、:208-210 分流空位/預約)。購買退貨把產品標作廢 f_status='7' 在同檔 :115。整條 void 分流從 CompleteAsync(services/Implementations/ApplicationAction/ApplicationActionService.cs:1010-1016 的 voidItems 那支)進 VoidApplicationManager |
| 位置變「已售出」的唯一來源 | SubmitApplicationManager.UpdatePositionToSoldAsync(services/DomainServices/Shared/SubmitApplicationManager.cs:542)。要追「位置怎麼從空位變已售、又怎麼變回來」,就這個點跟上面那個對看 |
| 判斷產品是不是被作廢 | 別處用的是裸字元 '7'(退貨作廢)/'3'(換裝作廢)比對,例如 services/Implementations/Customers/CustomerService.cs:188-189/:344-345 排除作廢產品——這也是為什麼第二層/第三層搞混時,會出現「作廢產品還掛在名下」 |
查 bug 時先問哪一句?
| 看到的症狀 | 先問 |
|---|---|
| 某個功能顯示的狀態文字跟預期不同 | 這個畫面是不是自己另外寫了一份判斷邏輯,而不是呼叫上面表格對應的權威? |
| 管理費催繳的到期天數看起來過期了 | 這是即時算出來的,不是存死的值;先查算法而不是查資料庫裡存的舊值 |
| 作廢後位置狀態不對 | 先確認這次作廢是三層裡的哪一層,位置只有第三層(購買退貨)才會變 |
| 晉塔還原後位置變成空位,覺得怪 | 通常是把第二層(晉塔還原)跟第三層(購買退貨)搞混了 |
| 兩個地方都叫「pending」但意思好像不一樣 | 很可能是晉塔預填跟新購預填,這兩組狀態剛好都用了同一個英文字,但語意完全不同 |
相關概念
- 申請單異動從建立到完成怎麼運作 — 申請單本身的建立/送出/處理存檔/完成流程,以及各種異動完成時實際套用了什麼
- 晉塔流程 — 晉塔/安奉的完整業務流程,本頁「第二層還原」就是這個流程的逆操作
本次驗證範圍
本頁於 2026-07-01 對照現行程式碼整理:
ApplicationStatusProductUseStatusHelperPositionStatusPrefillStatusNotificationStatusTemporaryStorageStatusVoidApplicationManagerProductStatus
本頁以現行程式碼為準,不把討論串當長期權威。本次未重新查詢 grace2,因此不宣稱資料庫個別存量資料已逐筆重驗。
2026-07-01 補充:把原本只散在 GitHub 追蹤 issue(#205 狀態機統整)裡的幾塊搬進本頁,目標是讓本頁足以取代該 issue——新增「以前哪些畫面自己手刻、後來收斂到哪」的收斂清單(A1~T-A8 各項 + 現行落點)、「哪些狀態舊站就有/哪些是新訂」的判斷法,以及三層作廢的唯一寫入點落點。落點行號由 subagent 讀現行程式碼核對;核對時發現舊 issue 的 ReleasePositionToVacantAsync 已改名 ReleasePositionAsync、Sold 寫入點行號已漂,均以現行碼為準更正。