一張 9.9 元的團購券在收銀臺掃了三次都沒能核銷成功,而排隊隊伍已經(jīng)不耐煩地嘆了四次氣——這是門店核銷鏈路斷裂的典型現(xiàn)場。問題的根源不在于“有沒有核銷功能”,而在于核銷動作背后的碼狀態(tài)、權(quán)限校驗和冪等保障有沒有做到位。

重新理解核銷:不是掃碼就完事

核銷的本質(zhì)是將一筆預(yù)先創(chuàng)建的權(quán)益(券、訂單兌換碼、活動資格)標(biāo)記為“已使用”,同時阻止它被再次使用。對于門店小程序,這個動作通常發(fā)生在顧客出示核銷碼、店員掃碼或手動輸入的瞬間。但除了客戶端的碼,你必須同時處理以下三個環(huán)節(jié):

  • 憑證生成策略:用什么標(biāo)識一段待核銷權(quán)益?是一次性券碼還是可多次核銷的次卡?碼是否需要加密?
  • 核銷終端權(quán)限:誰可以核銷?是否限定門店、店員、設(shè)備?
  • 狀態(tài)機與并發(fā)控制:同一碼被兩名店員同時掃到、網(wǎng)絡(luò)重試造成重復(fù)請求時,怎么保證只核銷一次?

只有把這三個環(huán)節(jié)串起來,核銷才不會變成“人工喊?!被颉笆潞髮~才發(fā)現(xiàn)多核了”的災(zāi)難。

因此,在實現(xiàn)上你通常面對兩條路徑:接入微信支付商家券核銷能力,或自建通用核銷碼系統(tǒng)。前者與微信支付訂單強綁定,適合有支付商戶號、希望核銷即記賬的場景;后者靈活度更高,可以對接積分兌換、活動邀請碼等非支付類權(quán)益。下面以使用最頻繁的“自建核銷碼 + 門店員工掃碼終端”為例,給出完整的落地步驟。

自建核銷的實現(xiàn)鏈路

一個典型的核銷鏈路包含券碼生成、核銷掃碼、后端驗證與狀態(tài)變更三個階段。

1. 生成可驗證的核銷碼

核銷碼不能是可被猜中的自增數(shù)字。你至少需要生成一個帶隨機部分、能關(guān)聯(lián)內(nèi)部券記錄的標(biāo)識符。常見做法是生成 12–20 位的字母數(shù)字混合碼,或者直接生成二維碼(更適合掃碼槍和攝像頭)。碼背后對應(yīng)的數(shù)據(jù)模型通常包括:

  • code:用戶可見的核銷碼串(例如 NXK7-2WPA-9LQ4
  • voucher_id:內(nèi)部券記錄主鍵
  • statusunused / used / expired / refunded
  • max_usageused_count:支持次卡場景
  • expire_at:過期時間
  • store_id / binding_user_id:可選,限制核銷門店或綁定用戶

核銷碼生成時,推薦在前端以二維碼形式展示,同時在人眼可讀位置展示短碼,便于手動輸入。二維碼可以直接用 voucher_idcode 作為內(nèi)容,無需額外加密,因為安全性必須依賴后續(xù)的服務(wù)端校驗。

2. 店員端掃碼與請求

門店員工使用小程序內(nèi)的“核銷”入口調(diào)起掃碼界面。微信小程序提供了 wx.scanCode API,可以讀取二維碼內(nèi)容。掃碼獲得碼字符串后,小程序攜帶三個關(guān)鍵參數(shù)向后端發(fā)起核銷請求:

  • code:掃描到的碼字符串
  • cashier_id:當(dāng)前登錄店員 ID
  • store_id:所屬門店 ID(可從店員信息獲?。?/li>

示例請求片段:

// 門店小程序內(nèi)店員掃碼后調(diào)用
const res = await wx.scanCode({ scanType: 'qrCode' });
if (res.result) {
  wx.request({
    url: 'https://your-api.domain/api/v1/redeem',
    method: 'POST',
    data: {
      code: res.result,
      cashier_id: getCurrentCashierId(),
      store_id: getCurrentStoreId()
    },
    success: handleRedeemResponse
  });
}

3. 后端核銷接口的原子校驗

這是防錯核心。接口必須在一次數(shù)據(jù)庫事務(wù)內(nèi)完成以下檢查并更新狀態(tài),否則并發(fā)核銷就會溜過去。

以 Node.js + PostgreSQL 為例,關(guān)鍵不是選什么語言,而是 SQL 層面用“比較并更新”避免競爭:

-- 使用“比較與交換”確保只更新一條 still-unused 記錄
UPDATE vouchers
SET status = 'used',
    used_at = NOW(),
    used_by = :cashier_id,
    used_store = :store_id
WHERE code = :code
  AND status = 'unused'
  AND expire_at > NOW()
  AND (restrict_store_id IS NULL OR restrict_store_id = :store_id);

然后判斷受影響行數(shù):

  • 返回 1 → 核銷成功,正常返回券信息。
  • 返回 0 → 需要進(jìn)一步查詢 code 是否存在、是否已使用、是否過期或門店不符,返回明確錯誤碼,小程序端據(jù)此展示“已核銷”“已過期”“該券不能在當(dāng)前門店使用”等不同提示。

對于次卡等可多次核銷的憑證,把 WHERE status = 'unused' 換成 WHERE used_count < max_usage,并在同一事務(wù)內(nèi)原子增加 used_count

如果你的系統(tǒng)已經(jīng)集成了 Redis,也可以使用 SETNX 或 Lua 腳本配合數(shù)據(jù)庫進(jìn)行鎖協(xié)調(diào),但最終狀態(tài)必須以數(shù)據(jù)庫為準(zhǔn)——Redis 只能作為性能層,不能作為唯一狀態(tài)源。

容易踩的四個坑和行動準(zhǔn)則

即便流程跑通,以下細(xì)節(jié)仍會讓核銷系統(tǒng)在真實門店環(huán)境中翻車。

1. 網(wǎng)絡(luò)重試導(dǎo)致的重復(fù)核銷 掃碼請求超時后,收銀員習(xí)慣再次掃碼。如果后端未做好冪等處理,就會產(chǎn)生重復(fù)核銷。正確的做法是讓核銷接口冪等:同一個 code 在狀態(tài)已經(jīng)是 used 時,第二次請求直接返回“已核銷”的成功狀態(tài)(或?qū)iT的 ALREADY_REDEEMED 錯誤碼),而不報系統(tǒng)錯誤,避免店員反復(fù)嘗試。

2. 二維碼偽造與盜用 不能把 voucher_id 明文當(dāng)作核銷憑據(jù)后,就依賴二維碼不可猜測來保安全。在顧客端展示核銷碼時,可以生成短期動態(tài)碼或附帶簽名——但復(fù)雜度會增加。對于大多數(shù)中小門店,提供手動核銷確認(rèn)機制更經(jīng)濟:店員掃碼后看到券面信息(比如金額、商品名),必須由店員滑動確認(rèn)或顧客輸入確認(rèn)密碼,防止快速掃碼盜刷。此外,對所有核銷操作記錄完整日志(操作人、時間、設(shè)備、門店),出現(xiàn)糾紛時可追溯。

3. 核銷后發(fā)生退款 如果一筆已核銷的券需要退款,必須設(shè)計“先撤銷核銷再退款”還是“保留核銷記錄,新增退款補償”的流程。前者需要券再次變?yōu)榭捎茫笳邉t保留審計線索。一般建議財務(wù)上走退款支付補單,而券狀態(tài)保持 used 并標(biāo)記 refunded,避免一張券被多次使用。你的核銷接口需要預(yù)留與退款中心聯(lián)動的鉤子。

4. 離線核銷的誘惑 在地下車庫、弱網(wǎng)環(huán)境下,收銀臺可能想先記錄碼,等網(wǎng)絡(luò)恢復(fù)再提交。不要做真正的離線核銷——讓店員拍下碼或記錄碼串,網(wǎng)絡(luò)恢復(fù)后手動輸入并請求核銷,仍然是實時走服務(wù)端校驗,只是操作節(jié)奏變了。真正的本地預(yù)核銷會導(dǎo)致碼狀態(tài)不一致,最終需要大量人工對賬。

行動建議:按復(fù)雜度選型

  • 如果你已經(jīng)在用微信支付,優(yōu)先使用「商家券」能力的核銷接口。它內(nèi)建了防重、退款聯(lián)動和消息觸達(dá),你只需要在店員端調(diào)用相應(yīng)的核銷接口,避免自建狀態(tài)機。
  • 如果同時存在非支付類權(quán)益(積分兌換、抽獎領(lǐng)取),則必須自建核銷碼系統(tǒng)。此時務(wù)必把原子狀態(tài)更新、冪等性和權(quán)限校驗作為第一版就上線的硬指標(biāo),而不是迭代中“再補”。
  • 多門店場景:在核銷時綁定 store_id,并在后臺報表中區(qū)分門店,這樣既能做績效統(tǒng)計,也能發(fā)現(xiàn)某門店異常核銷突增的情況。

門店小程序核銷做得不好,損失的不是服務(wù)器資源,而是收銀臺前立等可取的信任。把核銷當(dāng)成一個需要事務(wù)保障、角色驗證和審計追蹤的數(shù)據(jù)操作,而不是一個簡單的掃碼 UI,你的技術(shù)選擇自然就會收斂到上述方案上。

← 上一篇 仿站制作:從低效手動復(fù)制到結(jié)構(gòu)化提取的完整路徑 下一篇 → 預(yù)約小程序開發(fā):從混亂到有序,你需要避開的四個坑