系統(tǒng)PHP接入運營商二要素核驗實戰(zhàn))
1. 為什么要在O2O票務(wù)系統(tǒng)里接入運營商二要素核驗做過O2O票務(wù)的人都知道這個行業(yè)最頭疼的不是高并發(fā)搶票也不是座位鎖定而是實名制合規(guī)。一場演唱會、一趟景區(qū)直通車、一次線下劇本殺只要涉及真實身份入場就繞不開“人證合一”這道坎。早些年大家用短信驗證碼糊弄一下后來監(jiān)管收緊必須做身份證實名核驗再后來發(fā)現(xiàn)光有身份證號還不夠——號碼可能是買來的、借來的、甚至是從某些渠道批量搞來的于是運營商二要素核驗就成了票務(wù)系統(tǒng)里繞不過去的一環(huán)。所謂運營商二要素說白了就是姓名手機(jī)號拿去和運營商數(shù)據(jù)庫比對確認(rèn)這個手機(jī)號是不是登記在這個人名下。相比身份證二要素姓名身份證號運營商二要素的優(yōu)勢在于手機(jī)號是活的是人在用的能過濾掉大量“身份證是真的但人不對”的情況。對于票務(wù)場景尤其是需要現(xiàn)場核銷、入場檢票的O2O業(yè)務(wù)這個核驗環(huán)節(jié)直接決定了黃牛能不能批量囤票、代搶能不能繞過實名。我接手這個項目的時候系統(tǒng)已經(jīng)有一套身份證實名認(rèn)證但黃牛依然能通過“收身份證收手機(jī)號”的方式繞過。問題出在兩點一是身份證核驗只驗號碼真?zhèn)尾或灣钟腥硕鞘謾C(jī)號沒有做歸屬核驗隨便填一個能收短信的號就能過。后來我們決定接入海宇運營商二要素V即時版用PHP做數(shù)據(jù)工程層的改造把核驗鏈路重新梳理了一遍。這篇文章就把整個改造過程拆開講包括為什么選這個接口、PHP側(cè)怎么設(shè)計、踩了哪些坑、以及最后怎么把合規(guī)體驗做順。適合誰看如果你是PHP后端、票務(wù)系統(tǒng)開發(fā)者、或者正在做實名合規(guī)改造的技術(shù)負(fù)責(zé)人這篇內(nèi)容可以直接抄作業(yè)。如果你只是好奇“運營商二要素到底怎么玩”也能從里面看到完整的落地邏輯。2. 海宇運營商二要素V即時版的核心機(jī)制與選型考量2.1 運營商二要素到底核驗什么先把這個東西講透。運營商二要素核驗輸入是姓名和手機(jī)號輸出通常是三種狀態(tài)一致、不一致、無記錄。一致代表這個手機(jī)號在運營商側(cè)登記的機(jī)主姓名和你傳入的姓名匹配不一致代表號碼存在但機(jī)主不是這個人無記錄代表號碼不存在或者運營商沒有收錄。這里有個細(xì)節(jié)很多人搞混運營商二要素不是查身份證它查的是手機(jī)號的實名登記信息。也就是說如果一個人用別人的身份證辦了手機(jī)號那這個手機(jī)號的登記姓名就是別人的你拿本人的姓名去核驗結(jié)果就是“不一致”。這恰恰是票務(wù)場景需要的——我們要確認(rèn)“買票的人”和“用手機(jī)的人”是同一個。海宇這個V即時版相比普通版最大的區(qū)別是即時性。普通版可能是異步回調(diào)或者有緩存延遲V即時版是同步返回毫秒級響應(yīng)。對于票務(wù)系統(tǒng)來說用戶點“提交訂單”之后你不可能讓他等幾秒鐘再告訴他核驗結(jié)果必須是實時反饋。這也是我們選V即時版的核心原因。2.2 為什么不用身份證二要素或者三要素這里要解釋一下選型邏輯。身份證二要素姓名身份證號的核驗成本低但問題是它只驗證“這個身份證號是不是這個人的”不驗證“這個人是不是在用這個手機(jī)號”。黃牛完全可以拿一批真實身份證信息配上虛擬手機(jī)號繞過身份證二要素。三要素姓名身份證號手機(jī)號當(dāng)然更嚴(yán)格但成本高而且很多票務(wù)場景不需要這么重。比如景區(qū)門票、展覽票用戶可能只是臨時買一張你讓他填身份證號手機(jī)號姓名體驗太重轉(zhuǎn)化率會掉。運營商二要素剛好卡在中間比身份證二要素多了一層手機(jī)號歸屬驗證比三要素輕量成本也可控。我們實測下來運營商二要素能攔掉大約70%以上的黃牛批量下單因為黃牛手里的手機(jī)號大多是養(yǎng)號或者虛擬號登記姓名和買票姓名對不上。剩下30%是真人代搶那種只能靠限購和風(fēng)控策略補。2.3 海宇V即時版的接口特性海宇這個接口是HTTP POST返回JSON。核心參數(shù)就三個name、mobile、key或者簽名。V即時版的響應(yīng)時間實測在200-500ms之間取決于運營商側(cè)的網(wǎng)絡(luò)。返回碼里1000代表一致1001代表不一致1002代表無記錄其他是異常。有一點要注意運營商二要素的核驗結(jié)果不是100%準(zhǔn)確的。因為運營商數(shù)據(jù)更新有延遲比如一個人剛過戶手機(jī)號運營商側(cè)可能還沒同步這時候核驗就會失敗。所以我們在業(yè)務(wù)層做了容錯如果返回“無記錄”允許用戶走人工審核通道而不是直接拒絕。這個后面會細(xì)講。3. PHP數(shù)據(jù)工程層的架構(gòu)設(shè)計與核驗鏈路改造3.1 整體鏈路怎么走原來的票務(wù)下單鏈路是這樣的用戶選票 - 填實名信息 - 提交訂單 - 身份證二要素核驗 - 生成訂單 - 支付。改造之后我們在“提交訂單”和“生成訂單”之間插入了運營商二要素核驗并且把核驗結(jié)果和訂單狀態(tài)綁定。具體鏈路用戶提交訂單攜帶姓名、手機(jī)號、身份證號PHP側(cè)先做本地格式校驗姓名長度、手機(jī)號正則、身份證校驗位調(diào)用海宇V即時版接口傳入姓名手機(jī)號根據(jù)返回碼決定下一步1000核驗通過繼續(xù)生成訂單1001核驗不通過返回錯誤提示要求用戶確認(rèn)信息1002無記錄進(jìn)入人工審核隊列訂單狀態(tài)置為“待審核”其他記錄日志降級到身份證二要素核驗允許下單但標(biāo)記風(fēng)險這個鏈路的關(guān)鍵在于不能因為運營商核驗失敗就完全阻斷下單否則會誤傷真實用戶。我們的策略是“核驗通過直接放行核驗不通過攔截?zé)o記錄轉(zhuǎn)人工”這樣既保證合規(guī)又不影響正常轉(zhuǎn)化。3.2 PHP側(cè)的類設(shè)計我設(shè)計了一個OperatorVerifier類專門負(fù)責(zé)運營商二要素核驗。這個類不依賴框架純PHP方便在不同項目里復(fù)用。核心方法就一個verify($name, $mobile)返回一個結(jié)構(gòu)化的結(jié)果對象。class OperatorVerifier { private $apiUrl; private $apiKey; private $timeout 3; private $retryTimes 1; public function __construct($apiUrl, $apiKey) { $this-apiUrl $apiUrl; $this-apiKey $apiKey; } public function verify($name, $mobile) { $params [ name $name, mobile $mobile, key $this-apiKey, ]; $result $this-request($params); return $this-parseResult($result); } private function request($params) { $ch curl_init(); curl_setopt($ch, CURLOPT_URL, $this-apiUrl); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, $this-timeout); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); $response curl_exec($ch); $error curl_error($ch); curl_close($ch); if ($error) { return [code -1, msg $error]; } return json_decode($response, true); } private function parseResult($result) { if (!isset($result[code])) { return [status error, msg 接口返回異常]; } switch ($result[code]) { case 1000: return [status pass, msg 核驗一致]; case 1001: return [status fail, msg 姓名與手機(jī)號不一致]; case 1002: return [status unknown, msg 無記錄轉(zhuǎn)人工]; default: return [status error, msg 接口異常 . $result[code]]; } } }這個類里我加了timeout和retryTimes因為運營商接口偶爾會超時。超時之后不能直接判失敗而是要走降級邏輯。retryTimes設(shè)1次避免重試太多拖慢整體響應(yīng)。3.3 數(shù)據(jù)庫表設(shè)計核驗記錄必須落庫一是為了審計二是為了后續(xù)風(fēng)控分析。我設(shè)計了一張operator_verify_log表字段名類型說明idbigint自增主鍵order_idvarchar(32)關(guān)聯(lián)訂單號namevarchar(50)姓名脫敏存儲mobilevarchar(11)手機(jī)號verify_statustinyint0待核驗 1一致 2不一致 3無記錄 4異常verify_codevarchar(10)接口返回碼verify_msgvarchar(100)接口返回信息request_timedatetime請求時間response_timedatetime響應(yīng)時間created_atdatetime創(chuàng)建時間姓名和手機(jī)號要做脫敏姓名只存姓星號手機(jī)號存前3后4。這是合規(guī)的基本要求別問為什么問就是踩過坑。4. 實操過程從零接入海宇V即時版4.1 申請接口與配置第一步是找海宇申請接口權(quán)限。需要提供企業(yè)資質(zhì)、使用場景說明審核通過后會拿到apiUrl和apiKey。這里有個經(jīng)驗申請的時候把使用場景寫清楚是“票務(wù)實名核驗”不要寫“用戶注冊驗證”因為票務(wù)場景的審核通過率更高而且后續(xù)如果被抽查場景一致也省事。拿到key之后不要硬編碼在代碼里。我用的是環(huán)境變量配置文件的方式// config/operator.php return [ api_url env(OPERATOR_API_URL, ), api_key env(OPERATOR_API_KEY, ), timeout 3, retry 1, ];.env文件里配置實際值這樣不同環(huán)境測試、生產(chǎn)可以切換也避免key泄露。4.2 核驗邏輯的完整實現(xiàn)在訂單提交的控制器里我這樣調(diào)用public function submitOrder(Request $request) { $name $request-input(name); $mobile $request-input(mobile); $idCard $request-input(id_card); // 本地格式校驗 if (!preg_match(/^1[3-9]\d{9}$/, $mobile)) { return response()-json([code 400, msg 手機(jī)號格式錯誤]); } // 先做身份證二要素原有邏輯 $idVerify $this-idCardVerifier-verify($name, $idCard); if ($idVerify[status] ! pass) { return response()-json([code 400, msg 身份證核驗失敗]); } // 再做運營商二要素 $operatorVerifier new OperatorVerifier( config(operator.api_url), config(operator.api_key) ); $result $operatorVerifier-verify($name, $mobile); // 記錄日志 $this-logVerify($request-input(order_id), $name, $mobile, $result); switch ($result[status]) { case pass: // 核驗通過繼續(xù)下單 return $this-createOrder($request); case fail: return response()-json([code 400, msg 姓名與手機(jī)號不匹配請確認(rèn)后重試]); case unknown: // 轉(zhuǎn)人工審核 return $this-createPendingOrder($request); default: // 接口異常降級處理 return $this-createOrderWithRiskFlag($request); } }這里的關(guān)鍵是降級邏輯。如果海宇接口掛了或者超時不能直接讓用戶下不了單而是允許下單但標(biāo)記風(fēng)險后續(xù)人工抽檢。這個策略是我們和法務(wù)、運營一起定的既保證合規(guī)底線又不影響業(yè)務(wù)連續(xù)性。4.3 人工審核隊列的處理對于返回“無記錄”的訂單我們建了一個人工審核隊列。運營后臺可以看到這些訂單手動核對用戶信息。審核通過后訂單狀態(tài)從“待審核”變?yōu)椤耙汛_認(rèn)”用戶可以繼續(xù)支付。這個隊列的處理時效我們定的是2小時內(nèi)因為票務(wù)場景用戶等不了太久。如果2小時沒審核訂單自動取消釋放庫存。這個邏輯用PHP的隊列任務(wù)實現(xiàn)每10分鐘跑一次檢查超時訂單。// 隊列任務(wù)檢查超時未審核訂單 public function handle() { $timeout now()-subHours(2); $orders Order::where(status, pending_review) -where(created_at, , $timeout) -get(); foreach ($orders as $order) { $order-status cancelled; $order-cancel_reason 人工審核超時; $order-save(); // 釋放庫存 $this-releaseStock($order); } }4.4 核驗結(jié)果的緩存策略運營商二要素核驗是要花錢的每次調(diào)用都有成本。為了省錢我們對同一用戶做了短期緩存同一個姓名手機(jī)號24小時內(nèi)只核驗一次。緩存用Rediskey是operator_verify:{md5(namemobile)}value是核驗結(jié)果過期時間24小時。$cacheKey operator_verify: . md5($name . $mobile); $cached Redis::get($cacheKey); if ($cached) { return json_decode($cached, true); } $result $operatorVerifier-verify($name, $mobile); Redis::setex($cacheKey, 86400, json_encode($result));這個策略實測能省30%-40%的核驗成本因為很多用戶會在短時間內(nèi)重復(fù)下單比如選座猶豫、支付失敗重試。但要注意如果用戶修改了手機(jī)號或姓名緩存要失效否則會用到舊結(jié)果。5. 常見問題與排查技巧實錄5.1 接口超時怎么辦海宇V即時版雖然叫“即時”但運營商側(cè)偶爾會抽風(fēng)超時率大概在1%-2%。我們的處理是超時后重試1次如果還超時走降級邏輯允許下單但標(biāo)記風(fēng)險。這里有個坑不要設(shè)置太長的timeout。我一開始設(shè)了10秒結(jié)果用戶等10秒才拿到“核驗失敗”體驗極差。后來改成3秒超時就降級用戶幾乎無感。5.2 返回“無記錄”的比例太高如果發(fā)現(xiàn)大量訂單返回1002先檢查手機(jī)號是不是虛擬號段。虛擬號段比如170、171、165等在運營商側(cè)登記信息往往不完整容易返回?zé)o記錄。我們的做法是對虛擬號段單獨處理直接轉(zhuǎn)人工審核不調(diào)用接口省成本也省時間。另外新辦的手機(jī)號也可能返回?zé)o記錄因為運營商數(shù)據(jù)同步有延遲。這種情況只能靠人工審核兜底。5.3 姓名中有生僻字導(dǎo)致核驗失敗生僻字是運營商二要素的老大難問題。有些生僻字在運營商側(cè)存儲的是拼音或者編碼你傳漢字過去就匹配不上。我們的解決方案是對生僻字做轉(zhuǎn)碼處理先嘗試傳漢字如果返回不一致再嘗試傳拼音。這個邏輯寫在OperatorVerifier里作為兜底策略。if ($result[status] fail $this-hasRareChar($name)) { $pinyinName $this-convertToPinyin($name); $result $this-verify($pinyinName, $mobile); }5.4 并發(fā)調(diào)用導(dǎo)致接口限流海宇接口有QPS限制具體多少要看合同。我們一開始沒注意大促的時候并發(fā)調(diào)用直接把接口打限流了返回大量異常。后來加了令牌桶限流用Redis實現(xiàn)控制在合同約定的QPS以內(nèi)。$bucketKey operator_verify_bucket; $bucket Redis::get($bucketKey) ?: 0; if ($bucket $maxQps) { // 超過限流走降級 return $this-degrade(); } Redis::incr($bucketKey); Redis::expire($bucketKey, 1);5.5 常見問題速查表問題現(xiàn)象可能原因排查方法解決方案接口返回超時運營商側(cè)網(wǎng)絡(luò)波動查看curl_error日志重試1次超時降級大量返回1002虛擬號段或新號統(tǒng)計手機(jī)號號段分布虛擬號段轉(zhuǎn)人工姓名核驗不一致生僻字或姓名順序?qū)Ρ扔脩糨斎牒瓦\營商返回嘗試拼音轉(zhuǎn)碼接口返回限流QPS超限查看接口返回碼加令牌桶限流緩存導(dǎo)致結(jié)果錯誤用戶修改了信息檢查緩存key是否包含姓名手機(jī)號修改后清除緩存6. 合規(guī)體驗優(yōu)化的幾個關(guān)鍵決策6.1 用戶提示文案怎么寫核驗失敗的時候提示文案很關(guān)鍵。寫“姓名與手機(jī)號不匹配”用戶會懵寫“請確認(rèn)手機(jī)號是否本人實名”用戶才知道怎么改。我們的文案是您填寫的手機(jī)號與姓名不匹配請確認(rèn)手機(jī)號是否為本人實名登記。如果剛過戶請等待24小時后再試。這樣既解釋了原因又給了解決方案還降低了客服壓力。6.2 核驗失敗后的重試策略用戶核驗失敗后不能讓他無限重試否則會被黃牛用來試探。我們的策略是同一用戶24小時內(nèi)最多核驗3次超過就鎖定需要聯(lián)系客服解鎖。這個限制寫在Redis里key是用戶IDvalue是核驗次數(shù)。6.3 和風(fēng)控系統(tǒng)的聯(lián)動運營商二要素核驗結(jié)果會同步到風(fēng)控系統(tǒng)。如果同一個手機(jī)號關(guān)聯(lián)了多個不同姓名或者同一個姓名關(guān)聯(lián)了多個手機(jī)號風(fēng)控系統(tǒng)會標(biāo)記為高風(fēng)險后續(xù)下單直接攔截。這個聯(lián)動是通過消息隊列實現(xiàn)的核驗完成后發(fā)一條消息到風(fēng)控隊列風(fēng)控系統(tǒng)異步處理。6.4 數(shù)據(jù)脫敏與存儲合規(guī)前面提到過姓名和手機(jī)號必須脫敏存儲。我們的做法是核驗日志里只存脫敏后的信息原始信息不落庫。如果后續(xù)需要審計通過訂單號關(guān)聯(lián)到用戶表用戶表里的信息是加密存儲的。加密用AES-256密鑰存在KMS里不在代碼里硬編碼。7. 實際運行效果與后續(xù)擴(kuò)展方向這套改造上線之后黃牛批量下單量下降了約75%人工審核隊列每天大概200-300單審核通過率在60%左右也就是說有40%的“無記錄”訂單確實是風(fēng)險訂單。整體下單轉(zhuǎn)化率只下降了不到2%因為大部分真實用戶都能通過核驗少數(shù)無記錄的走人工審核也能完成下單。后續(xù)我們計劃做兩件事一是把運營商二要素和活體檢測結(jié)合對高風(fēng)險訂單要求人臉核驗二是把核驗結(jié)果和用戶信用分綁定信用分高的用戶可以直接跳過核驗提升體驗。這兩個方向都在測試中等跑通了再分享。我個人在實際操作中的體會是運營商二要素不是萬能的但它是票務(wù)合規(guī)里性價比最高的一環(huán)。關(guān)鍵是要把降級邏輯和人工審核做好不能因為核驗失敗就一刀切拒絕用戶。另外緩存和限流一定要做否則成本會失控。最后再分享一個小技巧把核驗日志和訂單日志分開存儲核驗日志可以定期歸檔訂單日志要長期保留這樣既滿足審計要求又不會讓主庫壓力太大。