資產狀態機

這篇寫給想知道「某個功能現在是什麼狀態、該去哪裡查真相」的人。
這個系統有很多地方都在問「這筆資料現在是什麼狀態」,但狀態欄位分散在很多張表、很多層。本頁整理各功能的狀態各自該去哪裡查,以及「作廢」「還原」這類狀態轉換實際上動了什麼。

先記住一句話

系統裡沒有一個「萬用狀態欄位」管所有東西。每個功能有自己的狀態,但同一個功能的狀態應該只有一個地方說了算——這篇就是列出「這個功能的狀態,真正該去哪裡查」。

各功能的狀態,該去哪裡查?

功能狀態定義在哪裡值大致有哪些白話說明
申請單ApplicationStatus草稿/待處理/處理中/已完成/已作廢(另外還有簽署、附件、發票三個獨立小狀態)一張申請單同時有「主狀態」跟三個附屬小狀態,查的時候要先分清楚問的是哪一個
權狀申購沿用 ApplicationStatus,不再自己另外推算同上以前這裡自己算過一次「這張算不算完成」,跟申請單本身的狀態對不上;後來已經改成直接讀申請單狀態
權狀修改(詳情頁/查詢清單)同樣沿用 ApplicationStatus草稿/非草稿這裡只在意「還是草稿」還是「不是草稿」,細節仍以申請單狀態為準
綜合查詢列表顯示邏輯呼叫 ApplicationStatus 的轉換方法,不自己重複判斷同上以前這裡自己又寫了一份判斷邏輯轉成顯示文字,容易跟正式狀態兜不起來
晉塔/安奉資料預填PrefillStatus待處理/處理中/已完成/已作廢這是「預填」這個小功能自己的狀態,跟晉塔本身、跟申請單都是不同的東西,不要混著查
新購資料預填PrefillStatus(另一組值)待處理/已匯入/已逾期名字跟上面那個很像(都可能叫 pending),但意思完全不同,這組是舊站資料匯入用的暫存區狀態
晉塔/安奉通報NotificationStatus待處理/進行中/已完成/逾期/已取消通報是「現場工作通知」,狀態跟申請單、跟預填都不是同一件事
管理費催繳ProductUseStatusHelper 即時算出來,不是存表裡固定不變的值依到期天數換算出的階段這裡刻意不存死值,每次查詢當下重新算,避免資料變舊卻沒人更新
無塔位暫厝自己一個狀態欄位進行中/已完成/已作廢只有無塔位暫厝這個小功能自己在用

橫向共用、大部分功能都會參考的兩個權威:

  • 使用狀態ProductUseStatusHelper):這個位置目前是未使用/已晉塔使用中/已遷出。
  • 位置狀態PositionStatus):這個位置目前是空位/已售出/已預約/鎖定等。

為什麼要特別講這件事?

因為以前這些狀態常常各自手刻一份判斷邏輯,久了就會跟正式資料兜不起來——例如畫面顯示「已完成」,但實際上申請單根本還在處理中。

現在的做法是:同一個功能的狀態顯示,全部改成呼叫同一份權威邏輯,不再各自重複判斷。如果之後又發現某個畫面自己另外寫了一套判斷邏輯,那就是舊問題重新長出來,應該把它改成呼叫對應的權威,而不是繼續各自維護。

以前哪些畫面自己手刻、後來收斂到哪

下面這幾個地方,以前都各自寫過一份狀態判斷,後來陸續收斂掉。列出來是讓你知道「這些坑填過了」,之後要是又冒出類似的自算邏輯,照同樣方式收斂即可。

以前哪裡自己算出過什麼問題現在收斂到現行落點(2026-07-01 對照 develop)
權狀申購的完成徽章自己推一套 completed,跟申請單本身狀態對不上直接讀申請單 f_status、不再自算CertificatePurchaseAppService.DeriveApplicationStatusservices/Implementations/Product/CertificatePurchaseAppService.cs:576,已改成單行 passthrough);前端 stores/certificate/certificate-editor-store.ts:69 + components/certificate/CertificatePurchaseEditor.vue:329-343
綜合查詢列表自己又寫一份狀態文字轉換委派 ApplicationStatus.Main.ToLabelservices/Implementations/ComprehensiveQuery/ComprehensiveQueryService.cs:298(投影先設 null)+ :413-414(回填 ToLabel)
產品使用狀態ProductService 本地一份 switch委派 ProductUseStatusHelperservices/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:16Enshrine:22Purchase:71 兩子類)
無塔位暫厝FStatus 沒有 enumTemporaryStorageStatus常數 repos/Constants/TemporaryStorageStatus.cs:16;三個寫入點 services/Implementations/TemporaryStorage/TemporaryStorageCrudService.cs:164(建立)/:249(完成)/:311(作廢)
繳交情況徽章直接拿存死的 f_markD 顯示,還把「到期」誤標成「預繳」改吃後端即時推導的期別 stageservices/Mappings/CertificateModificationMappingProfile.cs:72PayStatusIgnore(),改由 QueryById 即時算);前端 components/certificate/CertificateManagementFeeTable.vue:81-94 只做顏色映射

還有一個「暫厝模式」,原本以為散在四個地方、要收斂成一份,後來查下去發現「散四層」這個前提根本不成立(其中一層其實是管理費關係人的自由文字、不是模式欄),所以沒有硬收——這也是個提醒:收斂前先確認「重複」是不是真的重複。

有些狀態舊站就有、有些是這套系統新訂的

判斷一個狀態「權威可不可信」有個快捷方式:看它舊站有沒有對應

  • 舊站就有、這套忠實沿用的:位置狀態、使用狀態、管理費的 f_markD、產品有效/作廢。這幾個 ProductUseStatusHelperPositionStatus 是逐字對齊舊站的,基本可信,只要別的地方別再自己手刻一份就好。(唯一例外:f_markD 的「規則」對,但直接信 DB 存死的值會過期,所以催繳階段改成每次查詢即時算,見上面。)
  • 舊站沒有、這套新訂的:申請單四維狀態、晉塔/新購預填、通報 field_status、無塔位暫厝、暫厝模式。這幾個沒有舊站可參照,正是最容易各自手刻、最會 drift 的一批——上面那張收斂清單裡的問題,幾乎全出在這批。

作廢與還原:三層要分清楚

「作廢」跟「還原」不是同一件事在不同地方重複發生,而是三個完全不同層次的動作,各自動的東西也不一樣:

層次在辦什麼實際上動了什麼位置會不會變
第一層:權狀作廢轉讓/繼承的持有人作廢、補發/換裝的權狀號作廢,連動退費改持有人、改權狀號、管理費軟刪不動位置
第二層:晉塔/安奉還原正式入位反悔、要撤銷這次晉塔/安奉清掉使用人綁定,撤銷入位標記不動位置(這是「已使用」狀態內部的還原,不是把位置放回空位)
第三層:購買退貨作廢整張權狀連同購買一起作廢產品標記作廢(ProductStatus.Voided),退回可售位置從「已售出」放回「空位」

這三層之所以要分清楚,是因為「位置從已售出變回空位」只會發生在第三層(購買退貨,經 VoidApplicationManager)。

如果在處理晉塔還原(第二層)時位置卻被放回空位,或反過來購買退貨時位置沒有真的釋放,都代表兩層被搞混了——先查清楚現在到底是哪一層的作廢,不要照抄另一層的邏輯。

已經正式入位的權狀,不能直接退貨

第二層還原只適用在「還沒正式入位之前」。如果權狀已經正式完成晉塔/安奉,購買退貨(第三層)會直接被擋下來,訊息是「此權狀已完成晉塔安奉,請先完成遷出再退貨」。

也就是說,已經入位的權狀要走退貨,必須先辦遷出(把人遷出去,這是晉塔流程的逆操作,屬於正式業務流程,不是第二層那種「反悔還原」),遷出完成後才輪得到第三層退貨。看到「已晉塔卻不能退貨」不是 bug,是刻意的先後順序卡控。

三層各自動到什麼、落點在哪

要追什麼唯一該看的寫入點(2026-07-01 對照 develop)
第三層「已售出→空位」(唯一會改位置的一層)唯一寫入點 VoidApplicationManager.ReleasePositionAsyncservices/DomainServices/Shared/VoidApplicationManager.cs:188)。⚠️ 這方法早期文件裡叫 ReleasePositionToVacantAsync、現已改名;而且它會看這筆是不是換位——換位是退回「預約」不是「空位」(:204 只處理已售、:208-210 分流空位/預約)。購買退貨把產品標作廢 f_status='7' 在同檔 :115。整條 void 分流從 CompleteAsyncservices/Implementations/ApplicationAction/ApplicationActionService.cs:1010-1016voidItems 那支)進 VoidApplicationManager
位置變「已售出」的唯一來源SubmitApplicationManager.UpdatePositionToSoldAsyncservices/DomainServices/Shared/SubmitApplicationManager.cs:542)。要追「位置怎麼從空位變已售、又怎麼變回來」,就這個點跟上面那個對看
判斷產品是不是被作廢別處用的是裸字元 '7'(退貨作廢)/'3'(換裝作廢)比對,例如 services/Implementations/Customers/CustomerService.cs:188-189:344-345 排除作廢產品——這也是為什麼第二層/第三層搞混時,會出現「作廢產品還掛在名下」

查 bug 時先問哪一句?

看到的症狀先問
某個功能顯示的狀態文字跟預期不同這個畫面是不是自己另外寫了一份判斷邏輯,而不是呼叫上面表格對應的權威?
管理費催繳的到期天數看起來過期了這是即時算出來的,不是存死的值;先查算法而不是查資料庫裡存的舊值
作廢後位置狀態不對先確認這次作廢是三層裡的哪一層,位置只有第三層(購買退貨)才會變
晉塔還原後位置變成空位,覺得怪通常是把第二層(晉塔還原)跟第三層(購買退貨)搞混了
兩個地方都叫「pending」但意思好像不一樣很可能是晉塔預填跟新購預填,這兩組狀態剛好都用了同一個英文字,但語意完全不同

相關概念

本次驗證範圍

本頁於 2026-07-01 對照現行程式碼整理:

  • ApplicationStatus
  • ProductUseStatusHelper
  • PositionStatus
  • PrefillStatus
  • NotificationStatus
  • TemporaryStorageStatus
  • VoidApplicationManager
  • ProductStatus

本頁以現行程式碼為準,不把討論串當長期權威。本次未重新查詢 grace2,因此不宣稱資料庫個別存量資料已逐筆重驗。

2026-07-01 補充:把原本只散在 GitHub 追蹤 issue(#205 狀態機統整)裡的幾塊搬進本頁,目標是讓本頁足以取代該 issue——新增「以前哪些畫面自己手刻、後來收斂到哪」的收斂清單(A1~T-A8 各項 + 現行落點)、「哪些狀態舊站就有/哪些是新訂」的判斷法,以及三層作廢的唯一寫入點落點。落點行號由 subagent 讀現行程式碼核對;核對時發現舊 issue 的 ReleasePositionToVacantAsync 已改名 ReleasePositionAsync、Sold 寫入點行號已漂,均以現行碼為準更正。