卡系統(tǒng)源碼核心解析:支付回調(diào)與卡密自動發(fā)貨設(shè)計)
簡介這是一份面向網(wǎng)站開發(fā)者和運(yùn)營者的發(fā)卡系統(tǒng)網(wǎng)站源碼屬于修復(fù)版完整項目涵蓋虛擬商品自動售賣所需的核心流程。壓縮包共375個文件大小3.53MB其中以188個PHP文件為主干負(fù)責(zé)業(yè)務(wù)邏輯與接口處理44個CSS和32個JS用于前端界面與交互45個PNG和38張JPG構(gòu)成頁面素材另有10個SQL數(shù)據(jù)庫腳本便于初始化部署。源碼目錄清晰包含商品管理、訂單處理、支付集成、用戶管理、安全防護(hù)、日志記錄等模塊并配有安裝目錄和后臺管理界面。通過該源碼可理解自動發(fā)卡業(yè)務(wù)流程學(xué)習(xí)ajax異步處理、API接口對接、定時任務(wù)清理過期訂單等項目設(shè)計思路也適合作為PHP項目結(jié)構(gòu)與二次開發(fā)的實戰(zhàn)范例。修復(fù)版已針對安全漏洞和穩(wěn)定性進(jìn)行優(yōu)化部署時需遵守法律法規(guī)確保商品合法性與用戶數(shù)據(jù)安全。已有1187人學(xué)習(xí)下載對于需要快速搭建或定制發(fā)卡平臺的開發(fā)者這份資源提供了可直接參考的完整框架。1. 發(fā)卡系統(tǒng)能干什么一次支付系統(tǒng)自動吐出卡密A同學(xué)做虛擬商品線上售賣每天手動把會員碼、軟件授權(quán)碼發(fā)給買家半夜還要爬起來回消息。后來他把卡密庫存全部搬進(jìn)這套發(fā)卡系統(tǒng)網(wǎng)站源碼買家在下單頁選好商品、完成支付支付網(wǎng)關(guān)的回調(diào)一到系統(tǒng)自動從卡密池取出一張貨在訂單頁展示給買家同時通過站內(nèi)消息或郵件補(bǔ)發(fā)一次。這套源碼就是干這件事的商品管理、卡密批量導(dǎo)入、自動發(fā)貨、訂單查詢、支付回調(diào)處理全部通過網(wǎng)站頁面完成。適合個人開發(fā)者、小微團(tuán)隊做軟件授權(quán)、會員兌換碼、課程邀請碼這類虛擬商品的自動化售賣也適合想學(xué) PHP 支付閉環(huán)和庫存扣減的開發(fā)者拿來練手。2. 技術(shù)棧與目錄結(jié)構(gòu)入口文件到數(shù)據(jù)庫的全貌一套發(fā)卡系統(tǒng)的技術(shù)選型決定了它能不能在低配服務(wù)器上跑得穩(wěn)也決定了二次開發(fā)時你能改動哪些層。這一章先把這份源碼里最外圍的技術(shù)組合、目錄劃分和數(shù)據(jù)表關(guān)系理清楚后面分析核心鏈路時每段代碼都能對應(yīng)到具體文件和表。2.1 技術(shù)組合PHP 7.4 ThinkPHP 6 MySQL 5.7 Redis 6這套源碼是典型的 PHP 單體應(yīng)用。選 PHP 而不是 Node 或 Go原因很直接發(fā)卡系統(tǒng)的主流使用場景是個人服務(wù)器或低配虛擬主機(jī)PHP-FPM 的部署成本和運(yùn)行開銷最低??蚣軐用嬗玫氖侵髁?MVC 框架路由、ORM、模型事件這些基礎(chǔ)設(shè)施都有現(xiàn)成實現(xiàn)不用自己造輪子。Redis 在這里的角色很關(guān)鍵支付回調(diào)冪等、防并發(fā)超賣、異步隊列消費(fèi)者都要靠它。組件常見版本項目里干什么PHP-FPM7.4運(yùn)行 PHP 代碼處理 HTTP 請求ThinkPHP 66.xMVC 框架路由、ORM、模板引擎MySQL5.7商品、卡密池、訂單核心數(shù)據(jù)的持久化Redis6.x回調(diào)冪等鎖、庫存扣減鎖、異步任務(wù)隊列這套組合里最值得關(guān)注的是 Redis 承擔(dān)了“鎖”的職責(zé)。MySQL 的事務(wù)鎖能保證數(shù)據(jù)一致性但支付回調(diào)這種高頻率、可能重試的入口先用 Redis 做一次冪等攔截能減少大量無謂的數(shù)據(jù)庫行鎖等待。2.2 目錄結(jié)構(gòu)按業(yè)務(wù)模塊而不是按技術(shù)分層源碼解壓后的目錄劃分很清晰沒有把代碼全塞在控制器里。你可以先看app/controller找到 HTTP 入口再進(jìn)app/service看真正的業(yè)務(wù)邏輯。目錄樹大概長這樣faka/ ├─ app/ │ ├─ controller/ │ │ ├─ Index.php # 商城首頁與購買流程 │ │ ├─ Goods.php # 商品詳情與價格展示 │ │ ├─ Order.php # 下單、支付跳轉(zhuǎn) │ │ └─ Notify.php # 支付回調(diào)統(tǒng)一入口 │ ├─ model/ │ │ ├─ Goods.php # 商品模型 │ │ ├─ Cards.php # 卡密池模型 │ │ └─ Order.php # 訂單模型 │ ├─ service/ │ │ ├─ CardService.php # 卡密入庫、發(fā)貨、狀態(tài)流轉(zhuǎn) │ │ └─ PayService.php # 支付通道適配、驗簽 │ └─ common.php # 公共函數(shù)金額格式化、驗簽 ├─ config/ │ ├─ app.php # 應(yīng)用基礎(chǔ)配置 │ ├─ database.php # 數(shù)據(jù)庫連接配置 │ └─ route.php # 路由規(guī)則 ├─ public/ │ ├─ index.php # 應(yīng)用唯一入口 │ └─ static/ # CSS/JS/圖片等靜態(tài)資源 ├─ runtime/ # 日志、緩存、編譯模板 └─ .env.example # 環(huán)境變量配置模板service層的存在是這個目錄設(shè)計里最值得學(xué)習(xí)的地方??刂破髦回?fù)責(zé)收參數(shù)、調(diào)服務(wù)、返回響應(yīng)卡密的入庫、扣減、發(fā)貨這些核心邏輯全部收斂到CardService里支付回調(diào)統(tǒng)一走Notify控制器并轉(zhuǎn)發(fā)到PayService。這樣如果以后要對接新支付通道只需要擴(kuò)展PayService不用去動訂單和卡密的核心邏輯。2.3 數(shù)據(jù)庫設(shè)計五張表撐起整個發(fā)卡流程數(shù)據(jù)表只有五張核心表字段也不算多。這種精簡設(shè)計由發(fā)卡業(yè)務(wù)本身決定商品、卡密、訂單、回調(diào)日志、配置五個域幾乎覆蓋了全部業(yè)務(wù)場景。表名核心字段作用goodsid, name, price, stock, sold_count, status商品的基本信息和庫存總量cardsid, goods_id, card_key, card_value, a_status, sold_order_no, sold_time卡密池每行是一張卡ordersorder_no, goods_id, pay_amount, channel, u_status, pay_time, card_id訂單與支付狀態(tài)流轉(zhuǎn)pay_logorder_no, notify_raw, notify_time, is_handle支付回調(diào)原始數(shù)據(jù)留痕configkey, value站點(diǎn)名稱、支付密鑰等鍵值配置cards表和orders表之間的關(guān)聯(lián)值得注意。cards里的a_status用于標(biāo)識卡密狀態(tài)0 待售、1 已售、2 鎖定、3 作廢。當(dāng)一張卡被賣出它的sold_order_no會寫入對應(yīng)的訂單號同時orders表記錄card_id這種雙向關(guān)聯(lián)讓對賬變得很簡單。orders表的u_status則維護(hù)訂單自身狀態(tài)pending 待支付、paid 已支付、refunded 已退款。兩張表通過card_id和sold_order_no互相索引避免在發(fā)貨時再去遍歷待售卡密。3. 核心模塊拆解一張卡密從入庫到售出的完整鏈路發(fā)卡系統(tǒng)最核心的鏈路是管理員批量導(dǎo)入卡密用戶下單支付支付回調(diào)觸發(fā)后系統(tǒng)從卡密池取一張標(biāo)記為已售并返回給買家。整個過程里最危險的兩個點(diǎn)是重復(fù)入庫和并發(fā)超賣。這一章把這條鏈路的每一段代碼按執(zhí)行順序拆開講清楚每段代碼為什么那么寫。3.1 卡密批量入庫事務(wù)包裹用內(nèi)存哈希做去重運(yùn)營人員拿到一批卡密后通常是一段文本或一個 Excel 文件。導(dǎo)入接口做的事情本質(zhì)上就是按行拆分、去空、查重、批量插入。源碼里CardService的導(dǎo)入方法大致如下public function importCards(int $goodsId, array $cards): int { $goods Goods::find($goodsId); if (!$goods) { throw new \Exception(商品不存在); } // 先做內(nèi)存去重避免重復(fù)卡密進(jìn)入下一輪 $newList []; foreach ($cards as $item) { $cd trim($item); if ($cd ) { continue; } if (in_array($cd, $newList, true)) { continue; } $newList[] $cd; } if (empty($newList)) { throw new \Exception(沒有有效的卡密數(shù)據(jù)); } Db::transaction(function () use ($goodsId, $newList, $goods) { // 查一次庫里已有卡密防止重復(fù)導(dǎo)入 $exist Db::name(cards) -where(goods_id, $goodsId) -column(card_key); $existMap array_flip($exist); $insert []; foreach ($newList as $card) { if (isset($existMap[$card])) { continue; } $insert[] [ goods_id $goodsId, card_key $card, a_status 0, create_time time(), ]; } if (!empty($insert)) { Db::name(cards)-insertAll($insert); // 同步更新商品的庫存余量 $goods-stock count($insert); $goods-save(); } }); return count($insert); }導(dǎo)入這段代碼把“數(shù)據(jù)清洗”和“落庫”拆成了兩步。第一步先用內(nèi)存in_array去重這一步其實可以省略只靠 MySQL 的唯一索引也能攔住重復(fù)但先用 PHP 做一次過濾能顯著減少第二步insertAll的數(shù)據(jù)量。第二步用事務(wù)包裹再把庫里已有的card_key拉出來做一次對比確保同一商品下不會出現(xiàn)重復(fù)卡密。這里有個我在實際項目中常注意的點(diǎn)如果是批量導(dǎo)入幾萬條卡密insertAll可能超過 SQL 最大包大小穩(wěn)妥的做法是按 1000 條一組分多次插入。3.2 下單創(chuàng)建訂單顯示庫存但不下手鎖庫存很多發(fā)卡系統(tǒng)初始版本會在用戶點(diǎn)擊購買時就把庫存減掉這會導(dǎo)致大量“拍下不支付”的垃圾訂單霸占庫存。這套源碼的思路是下單時只創(chuàng)建一個 pending 狀態(tài)的訂單不扣減卡密池真正的庫存扣減動作放到支付成功以后。public function createOrder(int $goodsId): string { $goods Goods::lock(true)-find($goodsId); if (!$goods || $goods-status ! 1) { throw new \Exception(商品已下架); } // 這里讀的是商品表里的 stock 字段而不是實際統(tǒng)計卡密表 if ($goods-stock 0) { throw new \Exception(庫存不足); } $orderNo date(YmdHis) . mt_rand(1000, 9999); Db::name(orders)-insert([ order_no $orderNo, goods_id $goodsId, pay_amount $goods-price, u_status pending, create_time time(), ]); return $orderNo; }這段代碼里用lock(true)讀取商品行是為了保證并發(fā)下單時讀到的stock是準(zhǔn)確的避免兩個用戶同時看到一個商品還有最后 1 件庫存。但請注意這里并沒有真正把stock減掉訂單只是停留在 pending 狀態(tài)。這種設(shè)計的取舍很明確不支付就不會占用庫存對真實買家更友好。壞處是極端情況下會有輕微超賣比如 100 個人同時下單最后 1 件但只要支付回調(diào)里卡密扣減邏輯做對了多的訂單會進(jìn)入缺貨退款流程不會把一張卡發(fā)給兩個人。3.3 支付回調(diào)與自動發(fā)貨Redis 冪等鍵加 MySQL 行鎖支付回調(diào)是發(fā)卡系統(tǒng)的命門。支付網(wǎng)關(guān)會異步通知你的服務(wù)器而且通知可能發(fā)生多次如果回調(diào)處理不做好冪等和并發(fā)控制就會出現(xiàn)一張訂單發(fā)兩次貨、兩張訂單搶同一張卡的情況。源碼在Notify控制器里做的處理邏輯如下public function payNotify(array $params): bool { $orderNo $params[order_no]; // Redis 冪等鍵同一訂單即使收到多次回調(diào)也只有一個能進(jìn)入發(fā)貨流程 $lockKey notify_lock_ . $orderNo; if (!Redis::set($lockKey, 1, [nx, ex 120])) { return true; } Db::transaction(function () use ($orderNo) { // 行鎖訂單防止兩個并發(fā)回調(diào)同時讀到未支付狀態(tài) $order Db::name(orders) -where(order_no, $orderNo) -lock(true) -find(); if (!$order || $order[u_status] paid) { return; } // 從卡密池取最早一張待售卡片同樣加行鎖 $card Db::name(cards) -where(goods_id, $order[goods_id]) -where(a_status, 0) -order(id asc) -lock(true) -find(); if (!$card) { // 庫存不足訂單標(biāo)記為缺貨狀態(tài)進(jìn)入人工退款流程 Db::name(orders)-where(id, $order[id])-update([ u_status refunded, ]); throw new \Exception(卡密庫存不足請人工介入); } Db::name(cards)-where(id, $card[id])-update([ a_status 1, sold_order_no $orderNo, sold_time time(), ]); Db::name(orders)-where(id, $order[id])-update([ u_status paid, pay_time time(), card_id $card[id], ]); }); return true; }這里有兩個攔截重復(fù)發(fā)貨的機(jī)制缺一不可。第一層是 Redis 的set nx ex同一訂單號第一次回調(diào)會成功設(shè)置鍵后續(xù)的重試回調(diào)在 Redis 這一層就被擋掉了壓根不會進(jìn)數(shù)據(jù)庫業(yè)務(wù)邏輯。第二層是 MySQL 事務(wù)里的lock(true)即使 Redis 抖動或鍵過期導(dǎo)致兩個回調(diào)同時進(jìn)入事務(wù)訂單行鎖也能保證只有一個事務(wù)能讀到 pending 狀態(tài)并進(jìn)行發(fā)貨另一個讀到u_status paid后直接退出。真正取出卡密用的是where(a_status, 0)加lock(true)并發(fā)場景下后一個事務(wù)會被阻塞等前一個事務(wù)提交后它再讀取時這張卡的a_status已經(jīng)變成 1查詢條件命中不到就不會出現(xiàn)兩張訂單發(fā)出同一張卡的慘案。3.4 訂單查卡與補(bǔ)發(fā)用 card_id 關(guān)聯(lián)而非狀態(tài)篩選買家支付成功后跳回訂單詳情頁前端需要立即展示卡密。如果每次查看都去卡密池里篩選a_status 0那就會拿到另外一張未售卡這是典型的邏輯錯誤。正確做法是通過訂單表里已經(jīng)寫入的card_id關(guān)聯(lián)查詢$order Db::name(orders) -alias(o) -join(cards c, o.card_id c.id) -where(o.order_no, $orderNo) -field(o.order_no, o.pay_amount, c.card_key, c.card_value) -find();這段查詢的意義不在于 SQL 本身有多復(fù)雜而是它體現(xiàn)了一個關(guān)鍵約定訂單和卡密一旦綁定就通過外鍵關(guān)系固化下來后續(xù)所有查看和補(bǔ)發(fā)動作都基于這個綁定關(guān)系而不是去卡密池里重新找。訂單詳情、郵件補(bǔ)發(fā)、后臺售后查詢?nèi)繌?fù)用這一個查詢邏輯就能保證買家在任意時刻看到的都是當(dāng)初分配給自己的那張卡。4. 避坑指南支付回調(diào)和并發(fā)場景的幾個現(xiàn)場發(fā)卡系統(tǒng)看起來功能簡單實際跑起來最容易翻車的地方全在支付回調(diào)、庫存扣減和數(shù)據(jù)安全這幾個點(diǎn)。這一章整理了四個真實項目里最容易踩到的坑每一條都按現(xiàn)象、原因、解決三步說清楚。4.1 重復(fù)回調(diào)導(dǎo)致同一訂單發(fā)出兩張卡密現(xiàn)象買家只下一單付了一次款卻收到了兩條帶卡密的郵件或站內(nèi)信。原因支付網(wǎng)關(guān)為了保證通知送達(dá)會按照一定頻率重試回調(diào)直到你的服務(wù)器返回表示“我處理成功了”的響應(yīng)。如果回調(diào)處理里沒有做冪等控制每一次重試都會執(zhí)行一遍完整的發(fā)貨邏輯自然就發(fā)了兩張卡。解決在回調(diào)入口加 Redis 冪等鍵用set nx保證同一個訂單號只有第一次請求能真正進(jìn)入發(fā)貨流程后續(xù)請求直接返回成功。同時數(shù)據(jù)庫里的訂單狀態(tài)也要做二次校驗u_status一旦變成paid后續(xù)事務(wù)讀到就直接退出。兩套機(jī)制配合能擋住 Redis 鍵意外失效時的極端情況。4.2 高并發(fā)下超賣最后一張卡被兩個訂單同時買走現(xiàn)象商品庫存只剩最后 1 張兩個買家同時支付成功系統(tǒng)卻只找到 1 張卡另一個訂單被迫人工退款。原因兩張訂單的支付回調(diào)幾乎同時進(jìn)入發(fā)貨邏輯都執(zhí)行了where a_status 0的查詢在沒有行鎖的情況下都拿到了同一張卡然后分別標(biāo)記為已售。解決取卡密的 SQL 必須使用SELECT ... FOR UPDATE即框架里的lock(true)讓并發(fā)事務(wù)排隊。前一個事務(wù)取走卡并提交后后一個事務(wù)再執(zhí)行查詢時已經(jīng)取不到待售卡會走缺貨退款的分支。注意鎖的范圍要精確到卡密池的行不要在商品表上鎖整個庫存否則并發(fā)性能會非常難看。4.3 回調(diào)地址被偽造卡密被人批量刷走現(xiàn)象后臺日志里出現(xiàn)大量訂單都走同一個回調(diào)地址訂單沒有真實支付記錄但卡密全部被標(biāo)記為已售。原因回調(diào)接口沒有做簽名驗證攻擊者構(gòu)造一個包含訂單號的請求直接訪問回調(diào)地址服務(wù)器誤以為支付成功就執(zhí)行了發(fā)貨邏輯。解決支付網(wǎng)關(guān)每次回調(diào)會帶簽名參數(shù)把所有回調(diào)參數(shù)按字母序排序拼上商戶密鑰后計算簽名再用hash_equals和回調(diào)里的簽名比對。hash_equals是專門用來做字符串比較的函數(shù)比直接更防時序攻擊private function verifySign(array $params, string $sign): bool { // 1. 剔除簽名本身按字母序排序 ksort($params); // 2. 拼接成 query string 形式再拼上商戶密鑰 $str urldecode(http_build_query($params)); $secret $this-config-getPaySecret(); // 3. 用 hash_equals 做常量時間比較 return hash_equals(md5($str . $secret), $sign); }這套代碼的關(guān)鍵在排序和拼接規(guī)則必須和支付網(wǎng)關(guān)的簽名規(guī)則完全一致。一般支付平臺文檔里都會寫明簽名算法照著實現(xiàn)一遍后建議先跑一遍平臺提供的模擬回調(diào)工具確認(rèn)驗簽通過再上線。4.4 卡密明文入庫數(shù)據(jù)庫泄露等于全部資產(chǎn)裸奔現(xiàn)象服務(wù)器被拖庫后攻擊者直接拿著card_key字段去變現(xiàn)所有庫存瞬間歸零。原因卡密字段在數(shù)據(jù)庫里是明文存儲沒有做任何加密或脫敏處理。解決入庫前用openssl_encrypt對卡密做對稱加密密鑰存到config表或.env里。展示和補(bǔ)發(fā)時再解密// 入庫時加密 $encrypted openssl_encrypt($cardKey, AES-256-CBC, $secretKey, 0, $iv); // 展示時解密 $plainCard openssl_decrypt($encrypted, AES-256-CBC, $secretKey, 0, $iv);這樣即使數(shù)據(jù)庫文件整個泄露攻擊者拿到的也只是一堆密文沒有應(yīng)用密鑰就無法還原出真實卡密。代價是后臺管理員搜索卡密時會失去“全文模糊查詢”的能力只能在加密前記錄一段不可逆哈希用于精確匹配。這個取舍在虛擬商品交易場景里是值得的。5. 運(yùn)行與部署從源碼到一臺服務(wù)器前面四章把代碼邏輯講透了接下來要解決的問題是這份源碼拿到手本地怎么跑起來線上怎么配置。這一步對新手來說最容易卡住主要卡在三個點(diǎn)PHP 擴(kuò)展缺失、偽靜態(tài)沒配、.env配置不對。這一章按順序走一遍。5.1 本地運(yùn)行最小依賴和初始化步驟發(fā)卡系統(tǒng)的運(yùn)行環(huán)境非常常規(guī)只要 PHP 7.4 以上版本開啟pdo_mysql、redis、openssl、fileinfo這幾個擴(kuò)展即可。用你習(xí)慣的集成環(huán)境或 Docker 都行。我一般按下面這個順序初始化項目# 1. 進(jìn)入項目根目錄安裝依賴 composer install # 2. 復(fù)制環(huán)境配置模板并編輯 cp .env.example .env # 3. 啟動 ThinkPHP 自帶的開發(fā)服務(wù)器 php think run.env文件里要重點(diǎn)填對四組信息數(shù)據(jù)庫連接地址和賬號、Redis 連接地址、支付網(wǎng)關(guān)的商戶號和密鑰。第一次運(yùn)行php think run后訪問http://127.0.0.1:8000應(yīng)該能看到商城首頁后臺入口一般在/admin路徑首次進(jìn)入會跳轉(zhuǎn)初始化頁面讓你設(shè)置管理員賬號和密碼。5.2 Nginx 配置與偽靜態(tài)讓所有路由都進(jìn) index.php本地開發(fā)服務(wù)器不用配偽靜態(tài)但線上部署到 Nginx 時必須配。這套源碼的入口文件在public/index.php所有請求都通過這個文件分發(fā)到不同控制器。Nginx 配置的核心是把不存在的文件請求 rewrite 到index.php上server { listen 80; server_name your-domain.com; root /var/www/faka/public; index index.php index.html; location / { # 文件不存在時交給 index.php 處理 if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s/$1 last; } } location ~ ^/index\.php { fastcgi_pass 127.0.0.1:9000; fastcgi_param SCRIPT_FILENAME $document_root/index.php; include fastcgi_params; } location ~* \.(jpg|jpeg|png|css|js|ico)$ { expires 7d; } }這段配置里最關(guān)鍵的if (!-e $request_filename)是判斷請求的路徑是不是真實存在的文件像/admin、/order/detail這類路由在文件系統(tǒng)里并不存在需要 rewrite 到index.php里由框架解析。而靜態(tài)資源請求比如/static/css/app.css是真實文件必須直接返回不能走 PHP 解析否則會白白損耗性能。5.3 上線前必須改的幾個配置項上線部署和本地跑通有一個顯著區(qū)別本地用的是默認(rèn)密鑰和測試支付參數(shù)線上必須全部換成自己的。我每次上線前會強(qiáng)制過一遍以下清單.env里的PAY_SECRET換成至少 32 位隨機(jī)字符串不能用源碼自帶的默認(rèn)值數(shù)據(jù)庫連接不推薦使用數(shù)據(jù)庫 root 賬號單獨(dú)創(chuàng)建一個業(yè)務(wù)賬號并只授予業(yè)務(wù)庫表權(quán)限支付回調(diào)地址要改成線上域名并且暫時關(guān)閉支付網(wǎng)關(guān)的沙箱模式APP_DEBUG設(shè)為false防止異常信息直接暴露給訪客runtime目錄要保證 PHP 進(jìn)程有寫入權(quán)限否則日志和緩存寫不進(jìn)去排查問題時什么線索都看不到配置完成后建議先做一次小額真實支付確認(rèn)回調(diào)、發(fā)貨、訂單詳情展示全部正常再開始放量。這一步跳過會怎么樣大概率上線第一天就被各種訂單問題淹沒而且很難從日志里定位到原因。6. 二次開發(fā)把同步發(fā)貨改成異步隊列處理如果只是把卡密直接存在數(shù)據(jù)庫里同步發(fā)貨這個系統(tǒng)已經(jīng)能跑。可一旦你接入短信發(fā)送或者物流單號推送這種外部渠道同步發(fā)卡就會暴露問題支付網(wǎng)關(guān)的等待時間有限如果回調(diào)接口里多做幾次第三方請求網(wǎng)關(guān)會認(rèn)為你處理超時而反復(fù)重試反而容易產(chǎn)生重復(fù)發(fā)貨的隱患。常見的改造思路是把發(fā)貨動作推進(jìn) Redis 隊列讓回調(diào)只負(fù)責(zé)“確認(rèn)支付并標(biāo)記已支付”真正的卡密取出和推送交給后臺消費(fèi)者異步執(zhí)行。我一般會加一個簡單的隊列// 支付回調(diào)里在訂單標(biāo)記為 paid 后只做一件事推送任務(wù) Redis::lpush(faka:dispatch_queue, json_encode([ order_no $orderNo, goods_id $order[goods_id], ])); // 后臺消費(fèi)者腳本里從隊列里取出訂單號再發(fā)貨 while ($payload Redis::brpop(faka:dispatch_queue, 5)) { $orderNo json_decode($payload[1], true)[order_no]; $card CardService::dispatch($orderNo); }這樣改造后支付回調(diào)的耗時大幅縮短網(wǎng)關(guān)認(rèn)定的“處理成功”在毫秒級內(nèi)就能返回。真正的取卡、發(fā)短信、發(fā)郵件全部挪到消費(fèi)者里做如果消費(fèi)者掛了任務(wù)繼續(xù)留在隊列里服務(wù)恢復(fù)后還能補(bǔ)處理不會丟單。驗證異步化改造是否成功有個很簡單的辦法同時開兩個請求模擬同一訂單號的重復(fù)支付回調(diào)觀察在 Redis 冪等鍵有效期間訂單只標(biāo)記一次paid隊列里只有一條任務(wù)記錄消費(fèi)者也只發(fā)出一次貨。我從那次改完隊列之后每次動發(fā)貨鏈路都會強(qiáng)制自己在測試環(huán)境先跑一遍這個雙回調(diào)測試。這套源碼的支付回調(diào)主鏈路依然可以保持同步方式使用但理解了隊列化改造的思路以后加新渠道就不會被回調(diào)超時卡住。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取