<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Payment on Tarragon</title><link>https://tarrragon.github.io/blog/tags/payment/</link><description>Recent content in Payment on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Fri, 10 Jul 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/payment/index.xml" rel="self" type="application/rss+xml"/><item><title>16 種支付渠道、4 種行為分類 — 分層 enum：保真層與行為層的粒度分工</title><link>https://tarrragon.github.io/blog/work-log/dart_payment_dual_layer_enum/</link><pubDate>Fri, 10 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/work-log/dart_payment_dual_layer_enum/</guid><description>&lt;blockquote>
&lt;p>&lt;strong>觸發場景&lt;/strong>：POS 專案的支付方式建模。後端 API 的支付 type 是 1 到 16 的完整列舉（現金、錢包、信用卡、支付寶、微信、Touch&amp;rsquo;n Go、各種商城資產……），前端 UI 的行為分歧卻只有四種：要不要找零、限不限會員、要不要驗證支付結果
&lt;strong>疑問來源&lt;/strong>：一個 enum 做 16 個成員、每個掛行為謂詞？還是做 4 個大類、序列化時想辦法？兩個方向都彆扭
&lt;strong>整理目的&lt;/strong>：記下「分類系統的粒度由消費者決定、多種消費者就分層」的建模方式
&lt;strong>本文邊界&lt;/strong>：素材是該專案現行的 payment model；三層結構（16 → 4 → 3）是這個 domain 的結果、分層的推導方式可遷移&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="兩個消費者兩種粒度需求">兩個消費者、兩種粒度需求&lt;/h2>
&lt;p>支付方式這個概念在系統裡有兩類消費者，粒度需求相反：&lt;/p>
&lt;p>&lt;strong>序列化要無損。&lt;/strong> 後端的 &lt;code>type&lt;/code> 欄位是 1 到 16 的完整列舉，而且不只結帳請求用——&lt;code>payment_logs&lt;/code> 記錄實際扣款渠道時，會回到細分等級（商城 TC、商城 UC、USDN）。前端若把 16 種壓成 4 大類存，資料回不去了：兩筆 log 一筆走支付寶一筆走微信，壓縮後都是「第三方支付」，對帳時無從區分。&lt;/p>
&lt;p>&lt;strong>行為分流要粗。&lt;/strong> UI 關心的問題是「要不要開找零輸入」「這個支付方式非會員能不能選」「結帳前要不要跑驗證」——這些行為在 16 種渠道上高度重複：七種第三方支付的答案全部相同。把行為謂詞掛在 16 個成員上，是 16 × N 個決策、其中大半是複製貼上，新增渠道時要重答一整排實際上沒有分歧的問題。&lt;/p>
&lt;p>單一 enum 不管選哪個粒度，都是犧牲其中一個消費者。&lt;/p>
&lt;h2 id="分層解法channel-保真type-承行為category-是橋">分層解法：channel 保真、type 承行為、category 是橋&lt;/h2>
&lt;p>這個專案的做法是兩個 enum 各司其職：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">/// 後端的支付方式 type（1~16 完整對應，無損）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="n">enum&lt;/span> &lt;span class="n">PaymentChannel&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="n">cash&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">walletLegacy&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">creditCard&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">alipay&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">wechatPay&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">touchNGo&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="n">payNow&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">favePay&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">grabPay&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">vnPay&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">mallTC&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">mallUC&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">mallUSDN&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="n">visaUCard&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">exchangeUSDN&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">exchangeUSDT&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">...;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="c1">/// 對應的大分類
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">PaymentType&lt;/span> &lt;span class="kd">get&lt;/span> &lt;span class="n">category&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="k">switch&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="n">cash&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="n">PaymentType&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">cash&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="n">walletLegacy&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="n">mallTC&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="n">mallUC&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="n">PaymentType&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">wallet&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="n">creditCard&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="n">visaUCard&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="n">PaymentType&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">creditCard&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="n">alipay&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="n">wechatPay&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="n">PaymentType&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">thirdPartyPayment&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="p">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">&lt;span class="c1">/// 前端行為分流的四大類
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="n">enum&lt;/span> &lt;span class="n">PaymentType&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl"> &lt;span class="n">cash&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">wallet&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">creditCard&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">thirdPartyPayment&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">unknown&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl"> &lt;span class="kt">bool&lt;/span> &lt;span class="kd">get&lt;/span> &lt;span class="n">requiresChange&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="k">switch&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="n">cash&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl"> &lt;span class="kt">bool&lt;/span> &lt;span class="kd">get&lt;/span> &lt;span class="n">requiresMemberLogin&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="k">switch&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="n">wallet&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl"> &lt;span class="kt">bool&lt;/span> &lt;span class="kd">get&lt;/span> &lt;span class="n">isMemberOnly&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="k">switch&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="n">wallet&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>category&lt;/code> 這座橋用 exhaustive switch 實作，買到的是&lt;strong>編譯期的完整性保證&lt;/strong>：後端加了第 17 種渠道、前端 enum 補成員的那一刻，switch 不涵蓋就編譯失敗——「新渠道歸哪類」這個決策無法被遺忘。行為謂詞同樣全部用 exhaustive switch，&lt;code>PaymentType&lt;/code> 加成員時每個行為問題都會被編譯器逐一逼答。&lt;/p>
&lt;p>實際上還有第三層：&lt;code>CheckoutPattern&lt;/code>（三種結帳流程——標準、後端請款、POS 請款）由 &lt;code>PaymentType.checkoutPattern&lt;/code> 衍生。三層的粒度各自對應一種消費者：&lt;strong>序列化要 16、UI 行為要 4、結帳流程要 3&lt;/strong>——每一層都是「這個消費者眼中真正有分歧的數量」。&lt;/p>
&lt;h2 id="保真層承載的行為層放不下的知識">保真層承載的、行為層放不下的知識&lt;/h2>
&lt;p>分層之後，後端行為的細節知識有了正確的歸宿——它們屬於個別渠道、不屬於大類：&lt;/p>
&lt;ul>
&lt;li>&lt;code>walletLegacy&lt;/code> 是會員錢包的總入口：會員不指定動用哪種資產、後端依 TC → UC → USDN 順序混合扣款、&lt;code>payment_logs&lt;/code> 回實際扣到的細分渠道&lt;/li>
&lt;li>部分商城資產不開放單獨結帳（&lt;code>legacyWalletMergedChannels&lt;/code>）、只供 walletLegacy 動用，因此不列入 POS 的支付選項&lt;/li>
&lt;li>交易所資產的餘額目前拿不到，前端對這兩個 channel 直接放行、由後端在實際扣款時裁決&lt;/li>
&lt;/ul>
&lt;p>這些知識若硬塞進 4 大類的行為層，wallet 那一類會長出「有些成員其實……」的例外註解——例外是粒度選錯的訊號。放在 channel 層，每條知識就是它所屬成員的屬性或註解，自然原子。&lt;/p>
&lt;h2 id="判準與對照">判準與對照&lt;/h2>
&lt;p>收束成可操作的一句：&lt;strong>分類系統的粒度不是自己的屬性、是消費者的屬性&lt;/strong>。消費者只有一種，單一 enum 就夠；消費者多種且粒度需求不同，分層、層間用 exhaustive switch 衍生。判斷分幾層的方式是列消費者：這個 case 是「序列化、UI 行為、結帳流程」三個消費者、所以三層。&lt;/p>
&lt;p>反向的對照組在同族案例裡有兩個：&lt;a href="https://tarrragon.github.io/blog/work-log/flutter_exception_error_category_invariant/" data-link-title="Exception 型別綁 ErrorCategory 的建構不變式 — 以及合法需求撞上不變式的時刻" data-link-desc="把「錯誤代碼必須屬於對應分類」做成建構期不變式，錯誤分類錯亂會變成測試失敗而不是靜默混亂；同一批修復出現三種形態——換對值、換精確值、以及改繼承逃離約束。第三種是分類學本身的訊號：一個 domain 的錯誤天生橫跨技術分類時，分類軸跟階層軸不正交。">一個 &lt;code>ReadingStatus&lt;/code> enum 混裝閱讀狀態、書籍格式、書籍來源三種概念&lt;/a>所引的分類軸不正交問題——那是把多個&lt;strong>正交軸&lt;/strong>壓進一個 enum；本文的 16 vs 4 則是同一個軸的&lt;strong>不同粒度&lt;/strong>。前者的修法是拆軸、本文的修法是分層，訊號都是例外與重複開始增生。&lt;/p>
&lt;h2 id="相關閱讀">相關閱讀&lt;/h2>
&lt;ul>
&lt;li>分類軸不正交的姊妹篇：&lt;a href="https://tarrragon.github.io/blog/work-log/flutter_exception_error_category_invariant/" data-link-title="Exception 型別綁 ErrorCategory 的建構不變式 — 以及合法需求撞上不變式的時刻" data-link-desc="把「錯誤代碼必須屬於對應分類」做成建構期不變式，錯誤分類錯亂會變成測試失敗而不是靜默混亂；同一批修復出現三種形態——換對值、換精確值、以及改繼承逃離約束。第三種是分類學本身的訊號：一個 domain 的錯誤天生橫跨技術分類時，分類軸跟階層軸不正交。">Exception 型別綁 ErrorCategory 的建構不變式&lt;/a>——軸錯了分層救不了、要先拆軸&lt;/li>
&lt;li>同專案的 model 分工：&lt;a href="https://tarrragon.github.io/blog/work-log/dart_pos_item_four_lifecycle_models/" data-link-title="同一個品項、四個 model — value object 什麼時候該升級成 entity" data-link-desc="同一個業務概念要不要拆成多個 model、value object 什麼時候該升級成 entity——判準是操作需不需要 identity-based 回寫。以 POS 品項從點選、掛單、結算到歷史訂單的四階段模型為例，含 snapshot 與 live reference 的凍結時機。">同一個品項、四個 model&lt;/a>——那篇是生命週期軸的分模型、本文是粒度軸的分層，同一個「一個結構不硬撐多種語意」的原則&lt;/li>
&lt;li>概念地基：&lt;a href="https://tarrragon.github.io/blog/ddd/entity-vs-value-object/" data-link-title="entity 與 value object 的判準" data-link-desc="同一個業務概念該建成 entity 還是 value object：判準是「操作需不需要 identity-based 回寫」、而不是概念重要性或有沒有 id 可填。含判準隨生命週期重問的交棒時機、value object 的語意封閉、枚舉分層。">entity 與 value object 的判準&lt;/a> 的枚舉分層段——分類值也是 value object 建模的一部分&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<blockquote>
<p><strong>觸發場景</strong>：POS 專案的支付方式建模。後端 API 的支付 type 是 1 到 16 的完整列舉（現金、錢包、信用卡、支付寶、微信、Touch&rsquo;n Go、各種商城資產……），前端 UI 的行為分歧卻只有四種：要不要找零、限不限會員、要不要驗證支付結果
<strong>疑問來源</strong>：一個 enum 做 16 個成員、每個掛行為謂詞？還是做 4 個大類、序列化時想辦法？兩個方向都彆扭
<strong>整理目的</strong>：記下「分類系統的粒度由消費者決定、多種消費者就分層」的建模方式
<strong>本文邊界</strong>：素材是該專案現行的 payment model；三層結構（16 → 4 → 3）是這個 domain 的結果、分層的推導方式可遷移</p></blockquote>
<hr>
<h2 id="兩個消費者兩種粒度需求">兩個消費者、兩種粒度需求</h2>
<p>支付方式這個概念在系統裡有兩類消費者，粒度需求相反：</p>
<p><strong>序列化要無損。</strong> 後端的 <code>type</code> 欄位是 1 到 16 的完整列舉，而且不只結帳請求用——<code>payment_logs</code> 記錄實際扣款渠道時，會回到細分等級（商城 TC、商城 UC、USDN）。前端若把 16 種壓成 4 大類存，資料回不去了：兩筆 log 一筆走支付寶一筆走微信，壓縮後都是「第三方支付」，對帳時無從區分。</p>
<p><strong>行為分流要粗。</strong> UI 關心的問題是「要不要開找零輸入」「這個支付方式非會員能不能選」「結帳前要不要跑驗證」——這些行為在 16 種渠道上高度重複：七種第三方支付的答案全部相同。把行為謂詞掛在 16 個成員上，是 16 × N 個決策、其中大半是複製貼上，新增渠道時要重答一整排實際上沒有分歧的問題。</p>
<p>單一 enum 不管選哪個粒度，都是犧牲其中一個消費者。</p>
<h2 id="分層解法channel-保真type-承行為category-是橋">分層解法：channel 保真、type 承行為、category 是橋</h2>
<p>這個專案的做法是兩個 enum 各司其職：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">/// 後端的支付方式 type（1~16 完整對應，無損）
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="n">enum</span> <span class="n">PaymentChannel</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="n">cash</span><span class="p">,</span> <span class="n">walletLegacy</span><span class="p">,</span> <span class="n">creditCard</span><span class="p">,</span> <span class="n">alipay</span><span class="p">,</span> <span class="n">wechatPay</span><span class="p">,</span> <span class="n">touchNGo</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="n">payNow</span><span class="p">,</span> <span class="n">favePay</span><span class="p">,</span> <span class="n">grabPay</span><span class="p">,</span> <span class="n">vnPay</span><span class="p">,</span> <span class="n">mallTC</span><span class="p">,</span> <span class="n">mallUC</span><span class="p">,</span> <span class="n">mallUSDN</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="n">visaUCard</span><span class="p">,</span> <span class="n">exchangeUSDN</span><span class="p">,</span> <span class="n">exchangeUSDT</span><span class="p">,</span> <span class="p">...;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="c1">/// 對應的大分類
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span>  <span class="n">PaymentType</span> <span class="kd">get</span> <span class="n">category</span> <span class="o">=&gt;</span> <span class="k">switch</span> <span class="p">(</span><span class="k">this</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="n">cash</span> <span class="o">=&gt;</span> <span class="n">PaymentType</span><span class="p">.</span><span class="n">cash</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="n">walletLegacy</span> <span class="o">||</span> <span class="n">mallTC</span> <span class="o">||</span> <span class="n">mallUC</span> <span class="o">||</span> <span class="p">...</span> <span class="o">=&gt;</span> <span class="n">PaymentType</span><span class="p">.</span><span class="n">wallet</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="n">creditCard</span> <span class="o">||</span> <span class="n">visaUCard</span> <span class="o">=&gt;</span> <span class="n">PaymentType</span><span class="p">.</span><span class="n">creditCard</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="n">alipay</span> <span class="o">||</span> <span class="n">wechatPay</span> <span class="o">||</span> <span class="p">...</span> <span class="o">=&gt;</span> <span class="n">PaymentType</span><span class="p">.</span><span class="n">thirdPartyPayment</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="p">...</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">  <span class="p">};</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="c1">/// 前端行為分流的四大類
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="c1"></span><span class="n">enum</span> <span class="n">PaymentType</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">  <span class="n">cash</span><span class="p">,</span> <span class="n">wallet</span><span class="p">,</span> <span class="n">creditCard</span><span class="p">,</span> <span class="n">thirdPartyPayment</span><span class="p">,</span> <span class="n">unknown</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl">  <span class="kt">bool</span> <span class="kd">get</span> <span class="n">requiresChange</span> <span class="o">=&gt;</span> <span class="k">switch</span> <span class="p">(</span><span class="k">this</span><span class="p">)</span> <span class="p">{</span> <span class="n">cash</span> <span class="o">=&gt;</span> <span class="kc">true</span><span class="p">,</span> <span class="p">...</span> <span class="p">};</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">  <span class="kt">bool</span> <span class="kd">get</span> <span class="n">requiresMemberLogin</span> <span class="o">=&gt;</span> <span class="k">switch</span> <span class="p">(</span><span class="k">this</span><span class="p">)</span> <span class="p">{</span> <span class="n">wallet</span> <span class="o">=&gt;</span> <span class="kc">true</span><span class="p">,</span> <span class="p">...</span> <span class="p">};</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">  <span class="kt">bool</span> <span class="kd">get</span> <span class="n">isMemberOnly</span> <span class="o">=&gt;</span> <span class="k">switch</span> <span class="p">(</span><span class="k">this</span><span class="p">)</span> <span class="p">{</span> <span class="n">wallet</span> <span class="o">=&gt;</span> <span class="kc">true</span><span class="p">,</span> <span class="p">...</span> <span class="p">};</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>category</code> 這座橋用 exhaustive switch 實作，買到的是<strong>編譯期的完整性保證</strong>：後端加了第 17 種渠道、前端 enum 補成員的那一刻，switch 不涵蓋就編譯失敗——「新渠道歸哪類」這個決策無法被遺忘。行為謂詞同樣全部用 exhaustive switch，<code>PaymentType</code> 加成員時每個行為問題都會被編譯器逐一逼答。</p>
<p>實際上還有第三層：<code>CheckoutPattern</code>（三種結帳流程——標準、後端請款、POS 請款）由 <code>PaymentType.checkoutPattern</code> 衍生。三層的粒度各自對應一種消費者：<strong>序列化要 16、UI 行為要 4、結帳流程要 3</strong>——每一層都是「這個消費者眼中真正有分歧的數量」。</p>
<h2 id="保真層承載的行為層放不下的知識">保真層承載的、行為層放不下的知識</h2>
<p>分層之後，後端行為的細節知識有了正確的歸宿——它們屬於個別渠道、不屬於大類：</p>
<ul>
<li><code>walletLegacy</code> 是會員錢包的總入口：會員不指定動用哪種資產、後端依 TC → UC → USDN 順序混合扣款、<code>payment_logs</code> 回實際扣到的細分渠道</li>
<li>部分商城資產不開放單獨結帳（<code>legacyWalletMergedChannels</code>）、只供 walletLegacy 動用，因此不列入 POS 的支付選項</li>
<li>交易所資產的餘額目前拿不到，前端對這兩個 channel 直接放行、由後端在實際扣款時裁決</li>
</ul>
<p>這些知識若硬塞進 4 大類的行為層，wallet 那一類會長出「有些成員其實……」的例外註解——例外是粒度選錯的訊號。放在 channel 層，每條知識就是它所屬成員的屬性或註解，自然原子。</p>
<h2 id="判準與對照">判準與對照</h2>
<p>收束成可操作的一句：<strong>分類系統的粒度不是自己的屬性、是消費者的屬性</strong>。消費者只有一種，單一 enum 就夠；消費者多種且粒度需求不同，分層、層間用 exhaustive switch 衍生。判斷分幾層的方式是列消費者：這個 case 是「序列化、UI 行為、結帳流程」三個消費者、所以三層。</p>
<p>反向的對照組在同族案例裡有兩個：<a href="/blog/work-log/flutter_exception_error_category_invariant/" data-link-title="Exception 型別綁 ErrorCategory 的建構不變式 — 以及合法需求撞上不變式的時刻" data-link-desc="把「錯誤代碼必須屬於對應分類」做成建構期不變式，錯誤分類錯亂會變成測試失敗而不是靜默混亂；同一批修復出現三種形態——換對值、換精確值、以及改繼承逃離約束。第三種是分類學本身的訊號：一個 domain 的錯誤天生橫跨技術分類時，分類軸跟階層軸不正交。">一個 <code>ReadingStatus</code> enum 混裝閱讀狀態、書籍格式、書籍來源三種概念</a>所引的分類軸不正交問題——那是把多個<strong>正交軸</strong>壓進一個 enum；本文的 16 vs 4 則是同一個軸的<strong>不同粒度</strong>。前者的修法是拆軸、本文的修法是分層，訊號都是例外與重複開始增生。</p>
<h2 id="相關閱讀">相關閱讀</h2>
<ul>
<li>分類軸不正交的姊妹篇：<a href="/blog/work-log/flutter_exception_error_category_invariant/" data-link-title="Exception 型別綁 ErrorCategory 的建構不變式 — 以及合法需求撞上不變式的時刻" data-link-desc="把「錯誤代碼必須屬於對應分類」做成建構期不變式，錯誤分類錯亂會變成測試失敗而不是靜默混亂；同一批修復出現三種形態——換對值、換精確值、以及改繼承逃離約束。第三種是分類學本身的訊號：一個 domain 的錯誤天生橫跨技術分類時，分類軸跟階層軸不正交。">Exception 型別綁 ErrorCategory 的建構不變式</a>——軸錯了分層救不了、要先拆軸</li>
<li>同專案的 model 分工：<a href="/blog/work-log/dart_pos_item_four_lifecycle_models/" data-link-title="同一個品項、四個 model — value object 什麼時候該升級成 entity" data-link-desc="同一個業務概念要不要拆成多個 model、value object 什麼時候該升級成 entity——判準是操作需不需要 identity-based 回寫。以 POS 品項從點選、掛單、結算到歷史訂單的四階段模型為例，含 snapshot 與 live reference 的凍結時機。">同一個品項、四個 model</a>——那篇是生命週期軸的分模型、本文是粒度軸的分層，同一個「一個結構不硬撐多種語意」的原則</li>
<li>概念地基：<a href="/blog/ddd/entity-vs-value-object/" data-link-title="entity 與 value object 的判準" data-link-desc="同一個業務概念該建成 entity 還是 value object：判準是「操作需不需要 identity-based 回寫」、而不是概念重要性或有沒有 id 可填。含判準隨生命週期重問的交棒時機、value object 的語意封閉、枚舉分層。">entity 與 value object 的判準</a> 的枚舉分層段——分類值也是 value object 建模的一部分</li>
</ul>
]]></content:encoded></item></channel></rss>