Stripe-Paytype-Express
會員在結帳時勾選「記住這張卡」之後,下次結帳畫面上完全看不到卡號輸入框,直接就能一鍵扣款,這個「免輸入卡號」的體驗背後,mweb 前台跟 PaymentMiddleware(以下簡稱 PMW)之間到底怎麼互相配合,才能既讓使用者方便,又不會讓卡片資訊外流。整體會先看 PMW 端 Stripe 外掛怎麼判斷「這次要用哪一種方式扣款」,再往前台看這些關鍵欄位究竟是怎麼被組出來、又是怎麼存回資料庫的。
PMW 收到 mweb 呼叫後,StripePlugin.Pay() 會依照這次請求帶的 ExtendInfo 內容依序判斷要走哪一種付款情境,命中即停,不會同時符合兩種。全文的卡片顏色都對應同一套語意,先認識這三個顏色,後面看圖表會更快上手:
payment_method AND customer_id?is_reuse_payment_method == true?Pay() L112-124 — 先檢查 PaymentMethod 與 CustomerId 是否都非空 → 情境三;否則檢查 IsReusePaymentMethod == true → 情境二;都不符合則呼叫 strategy.Pay(request) → 情境一。行動錢包(Apple Pay / Google Pay)在最前面已被攔截,走獨立的 ProcessMobileWalletPayment,不在此三情境之列。
💳 三種付款情境詳解
情境一:單純 Pay(未綁卡 / 一般結帳)
strategy.Pay()觸發條件
DirectCharge 流程(一般商店)
1. POST /v1/payment_methods ← 明碼卡號,換取 pm_xxx 2. POST /v1/payment_intents ← 帶 pm_xxx、金額、幣別、confirm=true confirmation_method=automatic confirm=true application_fee_amount=...
DestinationCharge 流程(子帳號分潤商店,多一步)
1. POST /v1/payment_methods ← 用主帳號,不帶 Stripe-Account Header 2. GET /v1/accounts/{sub_account} ← 取子帳號 statement_descriptor 3. POST /v1/payment_intents ← transfer_data[destination]、on_behalf_of
回應結果
| Stripe status | PMW ReturnCode | mweb 行為 |
|---|---|---|
succeeded | 0000 | 付款成功,訂單繼續走完後續 Processor |
requires_action | 2003 | 回傳 3D 驗證 URL,訂單進入 WaitingTo3DAuth |
| Stripe ApiException(卡被拒等) | 3000 | 取消訂單,退還積點 / 券 / 購物金 |
response = await strategy.Pay(request),由 DirectChargePaymentFlowStrategy 或 DestinationChargePaymentFlowStrategy 處理實際 API 呼叫。情境二:首次綁卡(記住信用卡)
RememberPaymentMethodProcess觸發條件
API 呼叫序列
// StripePlugin.ReusePaymentMethod() 先查詢 Customer 0. GET /v1/customers/search?query=name:"{shop_id}_{member_id}"// RememberPaymentMethodProcess.Process()
- POST /v1/payment_methods ← 明碼卡號建立 PM
- POST /v1/payment_intents ← setup_future_usage=off_session(固定帶)
若 PaymentIntent.status 為 succeeded 或 requires_action:
├─ customers.data.Count == 1(已有 Customer)
│ 3a. POST /v1/payment_methods/{id}/attach ← 綁到既有 Customer
└─ customers.data.Count == 0(尚無 Customer)
3b. POST /v1/customers ← 建立並帶 payment_method 綁定
其他狀態(付款失敗)→ 不執行綁卡,直接回傳
回應結果
| PaymentIntent status | 綁卡動作 | ReturnCode |
|---|---|---|
succeeded | attach 或 create customer | 0000 |
requires_action | attach 或 create customer | 2003 |
| 其他失敗狀態 | 不綁卡 | 依失敗結果回傳 |
status is "requires_action" or "succeeded" 才會進入 switch(context.CustomersSearch.data.Count);Count 為 1 走 AttachMethod,為 0 走 CreateCustomer,其餘(大於 1)視為異常僅記 log 不處理,確保「一會員一 Customer」的資料原則。情境三:舊卡復用
ReusePaymentMethodPaymentIntentProcess觸發條件
API 呼叫序列
// StripePlugin.ReusePaymentMethod() 先驗證 Customer 1. GET /v1/customers/search?query=name:"{shop_id}_{member_id}"// ReusePaymentMethodPaymentIntentProcess.Process()
若 customer_id 存在於 search 結果中:
2. POST /v1/payment_intents ← 直接帶已存的 payment_method Token
customer={customer_id}
off_session=true ← 有帶則嘗試跳過 3D
否則:
throw new NotSupportedException(“Illegal Customer”) ← HTTP 500
回應結果
| 情況 | 行為 | 結果 |
|---|---|---|
customer_id 存在於 Search 結果 | 正常執行 PaymentIntent | 依 status 走 0000 / 2003 |
customer_id 不存在於 Search 結果 | throw NotSupportedException | HTTP 500 |
| Stripe ApiException(Token 失效等) | 捕捉例外 | 3000 |
context.CustomersSearch.data.Any(x => x.id == context.CustomerId) 防止使用者竄改 customer_id 冒用他人已綁定的卡;驗證通過才呼叫 Strategy.PaymentIntentAsync(request, null)(不覆寫 setup_future_usage)。三種情境的觸發依據、涉及的 Process 類別與 Stripe API 呼叫次數整理如下:
| 情境 | 決定欄位 | 負責類別 | DirectCharge API 數 | DestinationCharge API 數 |
|---|---|---|---|---|
| ① 單純 Pay | 皆空 | strategy.Pay() | 2 | 3 |
| ② 首次綁卡 | is_reuse_payment_method=true | RememberPaymentMethodProcess | 4 | 5 |
| ③ 舊卡復用 | payment_method + customer_id | ReusePaymentMethodPaymentIntentProcess | 2 | 3 |
情境二、情境三命中後,實際扣款與後續動作都委派給對應的 IReusePaymentMethodProcess 實作類別執行,以下說明兩者各自負責的職責與存在原因。
RememberPaymentMethodProcess
負責「用明碼卡號完成這一次付款,同時把這張卡的 Token 記到會員的 Stripe Customer 底下」,讓下次結帳可以直接用 Token 免輸入卡號(即情境三的前置準備)。
- 建立 PaymentMethod:呼叫
strategy.CreatePaymentMethodAsync(),用使用者這次輸入的明碼卡號向 Stripe 換取一次性的pm_xxxToken。 - 建立扣款意圖:呼叫
strategy.PaymentIntentAsync(request, "off_session"),並固定帶入setup_future_usage=off_session,等於跟 Stripe 講「這張卡之後我還會在使用者不在場的情況下扣款」,Stripe 才會允許之後做靜默扣款。 - 判斷是否要綁卡:只有
PaymentIntent.status為succeeded或requires_action(代表這張卡本身有效)時才進行綁定;若付款直接失敗,這張卡不值得留下,直接略過。 - 依 Customer 是否存在分流:已有 Customer(
data.Count == 1)就呼叫/payment_methods/{id}/attach把新卡掛到既有 Customer 底下;尚無 Customer(data.Count == 0)就呼叫/customers直接建立 Customer 並帶入卡片完成綁定。
data.Count 保證一個會員在同一子帳號下只會有一個 Stripe Customer,避免重複建立造成之後舊卡復用查詢時出現多筆歧義資料。ReusePaymentMethodPaymentIntentProcess
負責「驗證使用者傳入的 customer_id 是否真的屬於自己,通過後才用既有的 payment_method Token 直接扣款」,不再重新輸入卡號、不再重新建立 PaymentMethod。
- 合法性檢查(防冒用):用外層
StripePlugin.ReusePaymentMethod()事先查好的CustomersSearch結果,執行data.Any(x => x.id == context.CustomerId),確認前端傳來的customer_id確實存在於「這個會員」名下的 Stripe Customer 清單中。 - 驗證失敗直接擋下:若傳入的
customer_id對不上,代表可能是竄改請求、冒用他人已綁定的卡,直接throw NotSupportedException("Illegal Customer"),讓外層拋出 HTTP 500,不會呼叫任何扣款 API。 - 驗證通過才扣款:呼叫
strategy.PaymentIntentAsync(request, null)(第二參數傳null,代表不覆寫setup_future_usage,因為卡片早已在情境二綁定過),直接帶已存的payment_methodToken 與customer_id建立並確認 PaymentIntent。 - 省略建卡流程:與情境一 / 二不同,這裡完全不呼叫
/v1/payment_methods,因為 Token 早就存在,只需一支/v1/payment_intentsAPI 即可完成扣款。
payment_method/customer_id 參數盜用他人已存的信用卡。CompleteForNewCartV2 進入 ThirdPartyProcess Pipeline 後,customer_id / payment_method 並不是由前端直接送出明碼,而是 mweb 依「快速結帳識別碼 identity」在後端撈出,並在付款完成後把 Stripe 回傳的新 Token 寫回資料庫,形成一個完整的閉環:
PayTypeExpressProcessorMemberId + ShopId + PayProfileType 查出該會員「預設」的 PayTypeExpress 記錄,把它的 Identity 塞進 context.ThirdPartyPaymentInfo.ExtendInfo["identity"](此時卡號等機敏資料尚未載入,只有識別碼)。var payTypeExpressEntity = _payTypeExpressService
.GetDefaultPayTypeExpress(memberId, payProfileType, shopId, gatewayType);
extendInfo.Add(“identity”, payTypeExpressEntity.Identity);
context.ThirdPartyPaymentInfo.ExtendInfo = extendInfo;
ArrangePayTypeExpressInfoProcessoridentity 到 PayTypeExpress 資料表查出同一筆記錄,反序列化其 Info(JSON)欄位,取得內含 customer_id/payment_method 的機敏資訊,包成 paytype_express_info 放回 context,準備交給 PMW。var sameIdentity = payTypeExpressEntity.Single(i => i.Identity == identity); var payTypeExpressInfo = _payTypeExpressService.GetPayTypeExpressInfo(sameIdentity);context.ThirdPartyPaymentInfo.ExtendInfo = new Dictionary<string, object>
{
{ “identity”, sameIdentity.Identity },
{ “paytype_express_info”, payTypeExpressInfo.ExtendInfo } // 內含 customer_id / payment_method
};
StripePayChannelService.GetPayExtendInfo()ThirdPartyPayApiProcessor 呼叫 TradesOrderPaymentService.ProcessPayment() 時,實際呼叫此方法組出要 POST 給 PMW /api/v1/Pay/CreditCardOnce_Stripe/{tgCode} 的 Body。這裡才是 customer_id / payment_method 真正被放進送往 PMW 請求的地方,來源就是上一步的 paytype_express_info。// 定期購自動成單:從 RegularOrderCheckoutInfo JSON 取值(另一條路徑) extendInfo.Add("customer_id", stripeRegularOrderCheckOurInfo.CustomerId); extendInfo.Add("payment_method", stripeRegularOrderCheckOurInfo.PaymentMethod);// 一般快速結帳:優先從 paytype_express_info 取值
var payTypeExpressInfo = thirdPartyPaymentInfo[“paytype_express_info”].ObjToDictionary<object>();
payTypeExpressInfo.TryGetValue(“customer_id”, out object customerId);
payTypeExpressInfo.TryGetValue(“payment_method”, out object paymentMethod);extendInfo.Add(“customer_id”, customerId); // → 對應 PMW 情境三判斷欄位
extendInfo.Add(“payment_method”, paymentMethod); // → 對應 PMW 情境三判斷欄位
paytype_express_info(沒有 identity、沒有存過卡),就不會帶這兩個欄位,PMW 端 StripePlugin.Pay() 判斷落到情境一或情境二。ChangeExtendInfoAfterPaymentResultThirdPartyPayResponseEntity 會帶回新建立的 customer_id / payment_method(見前面「情境二」的 attach / create customer 結果)。mweb 收到後先暫存於 context.ThirdPartyPaymentInfo.ExtendInfo,供同一次請求後續 Processor 使用。if (paymentResult.ExtendInfo["payment_method"] != null && paymentResult.ExtendInfo["customer_id"] != null)
{
context.ThirdPartyPaymentInfo.ExtendInfo = new Dictionary<string, object>
{
{ "payment_method", paymentResult.ExtendInfo["payment_method"] },
{ "customer_id", paymentResult.ExtendInfo["customer_id"] }
};
}
AfterOrderProcessorcustomer_id / payment_method 包成 PayTypeExpressInfoForStripeEntity 序列化成 JSON,寫入(新增或更新)PayTypeExpress 資料表的 Info 欄位,並清快取。下次結帳時 Step 1、2 就能撈到這筆記錄,形成「首次明碼輸入 → 綁卡 → 之後都用 Token 復用」的閉環。context.ThirdPartyPaymentInfo.ExtendInfo.TryGetValue("customer_id", out object customer);
context.ThirdPartyPaymentInfo.ExtendInfo.TryGetValue("payment_method", out object paymentMethod);
stripeData.ExtendInfo = new PayTypeExpressInfoForStripeEntity
{
Customer = customer?.ToString(),
PaymentMethod = paymentMethod?.ToString(),
Country = context.CreditCardInfo.IssueCountryCode
};
_payTypeExpressService.CreatePayTypeExpress(payTypeExpressCurrent); // 或 UpdatePayTypeExpress()
_payTypeExpressService.RemovePayTypeExpressCache(memberId, payProfileType, shopId);
identity,真正的 Stripe Token 只存在 mweb 後端資料庫(下一分頁的 PayTypeExpress_Info)與 PMW/Stripe 之間,降低外洩風險,同時讓使用者能用「上次的卡」快速結帳而不必重新輸入卡號。aaa53.png 這張 DB 查詢截圖是 PayTypeExpress 資料表的真實資料,完整欄位定義與 Info 欄位的 JSON 結構整理如下,方便日後查表對照。
🗂️ 資料表完整欄位
bigint,主鍵,流水號bigint,商店序號int,會員序號varchar,付款方式類型,Stripe 固定為 CreditCardOnce_Stripevarchar,金流服務商,Stripe 走 PMW 固定為 PaymentMiddlewarevarchar,「這張已存卡」的識別碼(一長串亂碼),前端/Processor 用它指定要用哪張卡,本身不含卡號或 Tokenbit,是否為該會員此金流的預設卡,決定 GetDefaultPayTypeExpress() 撈到哪一筆nvarchar,JSON 字串,機敏內容本體,結構見下方🧬 Info 欄位 JSON 結構(Stripe)
PayTypeExpress_Info 反序列化為 PayTypeExpressCreditCardEntity<PayTypeExpressInfoForStripeEntity>,外層是卡片顯示用資訊,ExtendInfo 才是真正要帶給 PMW 的付款憑證:
{
"Issuer": null, // 發卡銀行(Stripe 多為 null)
"Association": "Visa", // 發卡組織:Visa / MasterCard / UnionPay...
"No": "************6274", // 卡號(僅顯示末四碼,其餘遮罩)
"Month": "08", // 有效月份
"Year": "27", // 有效年份
"ExtendInfo": {
"customer_id": "cus_xxxxxxxxxxxx", // ← 帶給 PMW 情境三判斷欄位
"payment_method": "pm_xxxxxxxxxxxx", // ← 帶給 PMW 情境三判斷欄位
"country": "TW" // 發卡行國家
}
}
Issuer/Association/No/Month/Year/ExtendInfo)+ BE/PayTypeExpress/PayTypeExpressInfoForStripeEntity.cs(ExtendInfo 內的 customer_id/payment_method/country,皆以 [JsonProperty] 對應小寫底線命名)。📋 實際資料範例(節錄自 DB 查詢結果)
| 欄位 | 範例值 |
|---|---|
| PayTypeExpress_Id | 189518 |
| PayTypeExpress_ShopId | 17 |
| PayTypeExpress_MemberId | 1598546 |
| PayTypeExpress_PayProfileType | CreditCardOnce_Stripe |
| PayTypeExpress_PaymentServiceProvider | PaymentMiddleware |
| PayTypeExpress_Identity | 9E160DB1471373603EF57A8F9455BCD0BEDA2C86DCE77BD31... |
| PayTypeExpress_IsDefault | 1 |
| PayTypeExpress_Info | {"Issuer":null,"Association":"Visa","No":"************6274","Mo...(同上方 JSON 結構,末端截斷) |
identity,真正的 Stripe Token 只存在 mweb 後端資料庫(PayTypeExpress_Info)與 PMW/Stripe 之間,降低外洩風險,同時讓使用者能用「上次的卡」快速結帳而不必重新輸入卡號。情境判斷的核心程式碼,位於 nineyi.payment.middleware → Plugins/NineYi.PaymentMiddleware.Plugins.Stripe/StripePlugin.cs:
public async Task<PaymentResponseEntity...> Pay(request, headers, payMethod) { var strategy = GetPaymentFlowStrategy(request.ExtendInfo.StripePaymentFlow); // 行動錢包(Apple Pay / Google Pay)另外處理,不屬於三情境 if (_mobileWalletMethods.Contains(payMethod)) return await strategy.ProcessMobileWalletPayment(request); var context = new ReusePaymentMethodEntity { ...Strategy = strategy, Request = request }; // 情境三:舊卡復用 — payment_method 與 customer_id 都有值 if (!string.IsNullOrWhiteSpace(request.ExtendInfo.PaymentMethod) && !string.IsNullOrWhiteSpace(request.ExtendInfo.CustomerId)) { response = await ReusePaymentMethod(context, ReusePaymentMethodPaymentIntent); } // 情境二:首次綁卡 — is_reuse_payment_method == true else if (request.ExtendInfo.IsReusePaymentMethod == true) { response = await ReusePaymentMethod(context, RememberPaymentMethod); } // 情境一:單純 Pay — 兩者皆非 else { response = await strategy.Pay(request); } return GetThirdPartyPayResponseEntity(request, response, context); }
ReusePaymentMethod() 內部會先呼叫 CustomersSearchAsync 查出該會員({shop_id}_{member_id})在 Stripe 的 Customer 記錄,再交給對應的 IReusePaymentMethodProcess(情境二為 RememberPaymentMethodProcess,情境三為 ReusePaymentMethodPaymentIntentProcess)執行後續動作。
第一次消費、這個會員在這間商店這個付款方式下 PayTypeExpress 表完全是空的時候,identity 是從哪冒出來的?答案是:伺服器端自己用「這次剛輸入的卡片明細」現算一組 SHA256 雜湊值,算完立刻寫入 DB,並不是從任何既有資料查出來的
觸發點:AfterOrderProcessor(成立訂單後動作)
AfterOrderProcessor 掛在幾乎所有付款完成流程(信用卡、ApplePay、ATM、LinePay…)的 Pipeline 尾端,付款成功、訂單成立後才會執行:
// AfterOrderProcessor.cs:330-390 // 沒勾選記住信用卡則整段略過 if (context.RememberCreditCardNo == false) return; // 查詢會員在此商店/付款方式下,DB 目前已有的記住卡片清單 var payTypeExpressList = this._payTypeExpressService.GetPayTypeExpressList(memberId, payProfileType, shopId); // ← 第一次消費時,這裡查出來是空 List // 取得「這次刷卡」的卡片識別碼 var cardIdentity = this.GetCreditCardIdentity(context); var payTypeExpressCurrent = payTypeExpressList.FirstOrDefault(x => x.Identity == cardIdentity && x.PaymentServiceProvider == paymentServiceProvider); // 空 List 一定找不到,進入「新增」分支 if (payTypeExpressCurrent == null) { payTypeExpressCurrent = new PayTypeExpressEntity { ShopId = shopId, MemberId = memberId, PayProfileType = payProfileType, PaymentServiceProvider = paymentServiceProvider, IsDefault = true, Identity = this.GetPayTypeExpressIdentity(context), // ← 現算出來的新 identity Info = this.GetPayTypeExpressInfo(context) }; this._payTypeExpressService.CreatePayTypeExpress(payTypeExpressCurrent); // ← 真正 INSERT this._payTypeExpressService.RemovePayTypeExpressCache(memberId, payProfileType, shopId); }
🧮 GetPayTypeExpressIdentity():identity 的真正計算公式
// AfterOrderProcessor.cs:610-641 private string GetPayTypeExpressIdentity(PayProcessContextEntity context) { var shopId = context.ShoppingCartV2.ShopId; var memberId = Convert.ToInt32(context.MemberId); string creditCardIdentity; switch (context.PayProfileType) { case CreditCardOnce: case CreditCardInstallment: // NCCC / TapPay:用 TapPay SDK 回傳的識別碼 + 到期日 var identificationCode = context.TapPayCardInfo?.IdentificationCode ?? context.PaymentInfo?.CreditCardInfo?.CardCode; var creditCardExpiryDate = context.TapPayCardInfo?.ExpiryDate ?? context.PaymentInfo?.CreditCardInfo?.ExpiryDate; creditCardIdentity = $"{identificationCode}_{creditCardExpiryDate}"; break; default: // Stripe / CheckoutDotCom / KPay:直接用「這次使用者剛輸入的卡號 + 到期日」 creditCardIdentity = $"{context.CreditCardInfo.CreditCardNo}_{context.CreditCardInfo.CreditCardDate}"; break; } var result = $"{shopId}_{memberId}_{creditCardIdentity}"; return result.ToSHA256(); // ← 最終的 identity 字串 }
identity = SHA256(shopId_memberId_卡號_到期日);NCCC/TapPay 通道 → identity = SHA256(shopId_memberId_TapPay識別碼_到期日)。輸入完全來自使用者「這次結帳畫面上親自輸入」的卡片明細,不依賴任何既有 DB 資料。🗝️ 為什麼這樣設計:用卡片特徵值做冪等 key
identity 會對得上,藉此判斷「是不是同一張已記住的卡」,而不必額外維護一組自增序號或額外的比對機制。🔁 兩個時機的對照
(DB 無資料)
SHA256(shopId_memberId_卡號/識別碼_到期日),並由 AfterOrderProcessor 在訂單成立後立刻呼叫 CreatePayTypeExpress 寫入 DB(DB 已有記錄)
PayTypeExpress 表撈出上次算好、存好的那筆 Identity,經 Shopping 服務 PayTypeExpressProcessor 回填給前端,前端原樣帶回,mweb 端再用它反查機敏資料(見「🔑 mweb 如何取得 customer_id / payment_method」分頁)GetCreditCardIdentity 610 行前、GetPayTypeExpressIdentity 610-641 行、新增分支 365-390 行)+ WebStore/DA/WebStoreDBV2/Repositories/PayTypeExpressRepository.cs(CreatePayTypeExpress 102-120 行,真正執行 INSERT 的地方)。前面幾個分頁都是站在 mweb(nineyi.webstore.mobilewebmall)這個 repo 裡面看事情,但實際上結帳頁 identity 的產生與回填,橫跨了三個各自獨立部署的服務。這個分頁把「使用者打開結帳頁」到「送出付款」這一整趟旅程,按實際呼叫順序完整串起來,釐清 is_reuse_payment_method/identity/ExtendInfo["identity"] 到底是誰、在哪個服務、哪一支程式碼裡被組裝跟傳遞的。
🧭 三個服務各自的角色
C:\91APP\ShoppingGET api/checkout、api/checkout/info),負責組出畫面要顯示的所有資料,包含「記住這張卡」checkbox 狀態與已記住卡片摘要C:\91APP\Cart\cart2\nine1.cartapi/checkouts/get(給 Shopping 讀快照)與 api/checkouts/complete(送出付款,轉呼叫 mweb)nineyi.webstore.mobilewebmallPayTypeExpress 表的地方,對外入口是 tradesOrderLite/CompleteForNewCartV21️⃣ 開啟結帳頁:GET api/checkout?checkoutUniqueKey=...(Shopping 服務)
CheckoutController.Get → CheckoutService.GetCartFromP2Async 依序跑 12 段 Processor Pipeline(ProcessorDefinitionCenter.GetCheckoutProcessorLayers),其中兩段是本次追蹤的重點:
// Stage 1:GetCheckoutProcessor 呼叫 Cart 服務讀 Redis 快照 GetCheckoutProcessor → Cart api/checkouts/get(純讀 Redis,快照是建立 checkout 時寫入的) // Stage 6:決定「記住這張卡」checkbox 要不要顯示 GetPayProcessDataProcessor → 讀 ShopDefault.IsRememberCreditCard(PaymentMiddleWare 閘道設定) → 組出 IsEnabledRememberCreditCardNo // Stage 7:若已啟用記住卡,撈出上次記住的卡片摘要 PayTypeExpressProcessor(Shopping 版本) → PayTypeExpressService.GetDefaultPayTypeExpressesAsync(...) (gateway type = "PaymentMiddleWare") → 組出 PaymentMiddlewareCreditCardInfoEntity(含 Identity) → 塞進 context.Data.DisplayCreditCard.PaymentMiddlewareCreditCardInfo
IGetCheckoutResponseEntity(前端 types.ts:782-839),內含 isEnabledRememberCreditCardNo 與 displayCreditCard。api/checkout/info 是完全不同的另一支 API,只回傳海外配送聲明/訂購須知/金物流顯示樣式三個純顯示欄位,跟金流、記住信用卡完全無關;真正跟 checkbox、卡片摘要有關的是 api/checkout?checkoutUniqueKey=...(即 getCheckout)。2️⃣ 使用者按下送出:api/checkouts/complete(Cart 服務)
前端把畫面上(可能是使用者剛輸入的新卡,也可能是「記住的卡」原樣帶回的)PaymentMiddlewareCreditCardInfo.Identity 送回 Cart 服務。Cart 端真正的閉環發生在:
// GetPayDataProcessor.cs:560-624 AssignPaymentMiddlewareCreditCardInfo(requestCreditCardInfo, payProcessContext) { // 把前端回傳的 Identity 寫入 mweb 認得的欄位 payProcessContext.ThirdPartyPaymentInfo.ExtendInfo["identity"] = requestCreditCardInfo.Identity; }
payProcessContext,其型別正是 mweb 的 PayProcessContextEntity——也就是這份文件從頭到尾在討論的那個 context。組好之後,Cart 服務呼叫 mweb 的 tradesOrderLite/CompleteForNewCartV2,正式進入本文件其他分頁描述的付款流程。🔁 五步驟收斂圖
GetCheckoutProcessor 呼叫 Cart 讀 Redis 快照,組出畫面資料GetPayProcessDataProcessor + PayTypeExpressProcessor 決定 checkbox 狀態與卡片摘要(含 DB 撈出的 Identity)Identity(新卡或舊卡皆然)原封不動送到 Cart api/checkouts/completeGetPayDataProcessor.AssignPaymentMiddlewareCreditCardInfo 把 Identity 寫入 payProcessContext.ThirdPartyPaymentInfo.ExtendInfo["identity"]tradesOrderLite/CompleteForNewCartV2,mweb ArrangePayTypeExpressInfoProcessor 用 thirdPartyPaymentInfo.ExtendInfo["identity"] 反查 DB 機敏資料;若是全新卡片(DB 查無資料),則由 AfterOrderProcessor 現算新的 identity 並寫回 DB(詳見「🌱 首次消費 identity 從何而來」分頁)PayTypeExpressProcessor/付款相關 Entity,彼此不是互相呼叫同一份程式碼,而是各自讀同一份 DB 設定(ShopDefault.IsRememberCreditCard)、傳遞同一個字串欄位(identity)來達成邏輯一致;一旦要修改「記住信用卡」相關邏輯,必須同時檢查三個 repo,而不是只改 mweb 這一份。

