費(fèi)原理與實(shí)現(xiàn):預(yù)付費(fèi)實(shí)時(shí)扣費(fèi)鏈路詳解)
簡介華為內(nèi)部技術(shù)文檔《OCS計(jì)費(fèi)原理與實(shí)現(xiàn)排版后》面向電信計(jì)費(fèi)研發(fā)、系統(tǒng)運(yùn)維及通信專業(yè)讀者系統(tǒng)闡述在線計(jì)費(fèi)系統(tǒng)的原理與落地實(shí)現(xiàn)。內(nèi)容涵蓋計(jì)費(fèi)系統(tǒng)從離線到實(shí)時(shí)的演進(jìn)脈絡(luò)詳細(xì)解釋TAG、業(yè)務(wù)用量、費(fèi)用項(xiàng)、產(chǎn)品、訂購、入帳關(guān)系等核心概念并完整梳理事件捕獲、事件處理、生成賬單、結(jié)算的計(jì)費(fèi)流程。實(shí)現(xiàn)部分涉及計(jì)費(fèi)引擎、數(shù)據(jù)庫、消息中間件及安全機(jī)制等關(guān)鍵組件可幫助讀者建立從原理到實(shí)際部署的整體認(rèn)知。文檔源自華為技術(shù)有限公司內(nèi)部資料保留原始章節(jié)結(jié)構(gòu)與流程圖說明對于梳理實(shí)時(shí)計(jì)費(fèi)系統(tǒng)的業(yè)務(wù)邏輯與關(guān)鍵節(jié)點(diǎn)非常有幫助。資源包共1個(gè)文件為doc格式文檔壓縮包大小1.87MB排版后的正文便于閱讀。目前已有297人學(xué)習(xí)下載適合需要深入理解OCS機(jī)制或開展內(nèi)部培訓(xùn)的工程師參考。1. OCS計(jì)費(fèi)原理與實(shí)現(xiàn)為什么預(yù)付費(fèi)用戶離不開這條實(shí)時(shí)扣費(fèi)鏈路客服坐席被用戶質(zhì)問余額明明還有38.6元為什么10元的流量包訂購失敗。查了一圈發(fā)現(xiàn)賬務(wù)系統(tǒng)里的余額和OCS實(shí)時(shí)余額差了正好10元——這筆錢在離線賬本里還沒扣但OCS已經(jīng)把它預(yù)占走了。OCS計(jì)費(fèi)原理與實(shí)現(xiàn)講的就是這條實(shí)時(shí)扣費(fèi)鏈路用戶每次上網(wǎng)、打電話、發(fā)短信網(wǎng)元都會(huì)先向OCS申請配額OCS完成鑒權(quán)、批價(jià)、扣費(fèi)后才放行業(yè)務(wù)。這套系統(tǒng)適合預(yù)付費(fèi)用戶的余額實(shí)時(shí)控制、套餐流量和時(shí)長的準(zhǔn)實(shí)時(shí)扣減以及需要賬務(wù)聯(lián)動(dòng)同步的業(yè)務(wù)線。下面按位置、原理、落表、踩坑、驗(yàn)證的順序展開新手能照著建表寫邏輯熟手可以對著排查思路核對邊界。2. OCS在計(jì)費(fèi)域里的位置離線計(jì)費(fèi)撐不住的地方在線計(jì)費(fèi)來兜底2.1 離線計(jì)費(fèi)為什么跟不上預(yù)付費(fèi)話單延遲的代價(jià)離線計(jì)費(fèi)的鏈路是網(wǎng)絡(luò)設(shè)備在會(huì)話結(jié)束后生成CDR話單經(jīng)過采集、清洗、批價(jià)進(jìn)入賬務(wù)系統(tǒng)最后在出賬日統(tǒng)一結(jié)算。這條鏈路的延遲通常以分鐘甚至小時(shí)計(jì)。對后付費(fèi)用戶來說延遲一個(gè)月都沒問題對預(yù)付費(fèi)用戶來說延遲幾分鐘就可能讓余額耗盡的用戶繼續(xù)享受服務(wù)形成欠費(fèi)風(fēng)險(xiǎn)。更麻煩的是用戶通過營業(yè)廳或App查詢余額時(shí)看到的是賬務(wù)系統(tǒng)里的“滯后數(shù)字”和實(shí)際可用的實(shí)時(shí)余額對不上投訴就來了。OCS把流程倒過來會(huì)話開始前網(wǎng)元必須先向OCS發(fā)起鑒權(quán)計(jì)費(fèi)請求拿到配額才允許業(yè)務(wù)使用。配額用完網(wǎng)元再次申請申請不到業(yè)務(wù)被切斷。這樣“用多少錢由OCS現(xiàn)場決定”的模式才是在線計(jì)費(fèi)的正解。OCS與網(wǎng)元之間走的是Diameter Credit-Control協(xié)議Ro接口承載在線計(jì)費(fèi)請求。網(wǎng)元發(fā)出的請求叫CCRCredit-Control RequestOCS返回的叫CCACredit-Control Answer。CCR/CCA里的“CC-Request-Type”字段區(qū)分這是一次會(huì)話的初始請求、中間更新還是最終結(jié)束這套協(xié)議棧是3GPP標(biāo)準(zhǔn)定義的自研實(shí)現(xiàn)時(shí)只要把該字段和配額字段對清楚就能和設(shè)備廠商聯(lián)調(diào)。離線與在線的關(guān)鍵差異可以用一張表說清維度離線計(jì)費(fèi)在線計(jì)費(fèi)計(jì)費(fèi)時(shí)機(jī)會(huì)話結(jié)束后會(huì)話開始前與過程中數(shù)據(jù)來源CDR話單計(jì)費(fèi)請求/應(yīng)答超額控制出賬后追繳配額用盡即斷余額查詢滯后賬本實(shí)時(shí)余額典型場景后付費(fèi)月結(jié)預(yù)付費(fèi)語音、流量2.2 OCS、GRS、RCS在生產(chǎn)網(wǎng)里的配合方式在運(yùn)營商內(nèi)部的生產(chǎn)培訓(xùn)教材里常把OCS、GRS、RCS這些網(wǎng)元縮寫畫在同一張鏈路上。很多新人被縮寫嚇住實(shí)際上它們之間的交互最終都是同一個(gè)動(dòng)作網(wǎng)元發(fā)配額申請OCS回配額結(jié)果。OCS前面接的是各類業(yè)務(wù)網(wǎng)關(guān)——語音的、流量的、消息的OCS后面接的是賬務(wù)系統(tǒng)、客服系統(tǒng)和批價(jià)規(guī)則庫。一次流量上網(wǎng)會(huì)話的交互大概是這樣PGW收到用戶上網(wǎng)請求發(fā)現(xiàn)是預(yù)付費(fèi)用戶先不直接放行而是向OCS發(fā)一個(gè)Initial CCR里面帶用戶標(biāo)識(shí)、業(yè)務(wù)標(biāo)識(shí)、請求的流量大小。OCS經(jīng)過批價(jià)發(fā)現(xiàn)用戶套餐里還有2GB月度流量于是回一個(gè)CCA帶“本次授權(quán)可以使用1GB有效期600秒”。PGW放行1GB。用戶跑完900MB后PGW再發(fā)Update CCROCS繼續(xù)授權(quán)下一段。這中間網(wǎng)元和OCS之間沒有任何人工賬單參與完全是機(jī)器對話?,F(xiàn)網(wǎng)實(shí)現(xiàn)里OCS內(nèi)部控制一般分成四個(gè)域批價(jià)引擎負(fù)責(zé)把業(yè)務(wù)使用量換算成金額余額中心負(fù)責(zé)統(tǒng)籌賬戶下的現(xiàn)金、贈(zèng)送、套餐額度配額管理器負(fù)責(zé)決定本次下發(fā)多少配額、有效期多長話單網(wǎng)關(guān)負(fù)責(zé)把會(huì)話結(jié)束后的使用明細(xì)轉(zhuǎn)成標(biāo)準(zhǔn)CDR送給賬務(wù)系統(tǒng)。GRS、RCS這類縮寫要么是圍繞OCS的配置/規(guī)則管理能力要么是會(huì)話與資源狀態(tài)統(tǒng)計(jì)能力不必一個(gè)一個(gè)背抓住“批價(jià)余額配額”這條主線就夠了。2.3 哪些業(yè)務(wù)必須交給OCS判斷標(biāo)準(zhǔn)不是所有業(yè)務(wù)都要塞進(jìn)OCS。我一般按三條標(biāo)準(zhǔn)判斷一是業(yè)務(wù)是否需要“事前實(shí)時(shí)放行”。比如VoLTE通話、流量上網(wǎng)、短信發(fā)送用戶對實(shí)時(shí)性要求高且存在透支風(fēng)險(xiǎn)必須在線計(jì)費(fèi)。二是是否存在“配額”概念。凡是可以按時(shí)間、流量、條數(shù)切分成一段一段使用的資源都適合OCS一次性點(diǎn)播內(nèi)容更適合按次計(jì)費(fèi)也可以走OCS的靈活配額。三是是否需要余額聯(lián)動(dòng)。如果業(yè)務(wù)要保證用戶余額與可用資源強(qiáng)一致OCS就是正確選擇。一個(gè)反例企業(yè)專線月結(jié)業(yè)務(wù)用戶按合同月底統(tǒng)一付款業(yè)務(wù)是長期在線的沒有“配額”概念硬套OCS的話要么每個(gè)會(huì)話都下發(fā)超大配額要么頻繁中斷體驗(yàn)很差。這種走傳統(tǒng)離線計(jì)費(fèi)更合適。所以O(shè)CS是工具不是所有計(jì)費(fèi)問題的銀彈。判斷清楚了再進(jìn)入原理層。3. OCS計(jì)費(fèi)核心原理配額、會(huì)話和余額倒扣的三層模型3.1 配額是實(shí)時(shí)計(jì)費(fèi)的最小單位余額不能直接當(dāng)令牌發(fā)OCS不把“余額”直接下發(fā)給網(wǎng)元。原因很實(shí)際網(wǎng)元是網(wǎng)絡(luò)設(shè)備它只認(rèn)“能用多少時(shí)間/流量”不認(rèn)“賬上有多少錢”。而且余額是總賬如果網(wǎng)元拿到總賬就直接放行用戶很可能把余額一次耗盡、甚至超額等到話單回來已經(jīng)晚了。所以O(shè)CS要把余額轉(zhuǎn)成配額Quota。配額的含義是“本次會(huì)話中網(wǎng)元最多可以用掉的量”。它可以按時(shí)間秒、按流量字節(jié)、按費(fèi)用金額或按業(yè)務(wù)特定單位來定義。一次授權(quán)通常包含三要素配額大小本次允許用多少時(shí)間/流量/金額配額類型time、total-octets、input-octets、output-octets、service-specific有效期Validity Time在這個(gè)時(shí)間內(nèi)配額有效過了時(shí)間網(wǎng)元必須停用并用完的部分歸還。配額的粒度是OCS調(diào)優(yōu)的關(guān)鍵。給大了用戶可能透支給小了網(wǎng)元頻繁發(fā)請求信令壓力大。常見做法是先按套餐周期估算可用量再按“單次會(huì)話最多使用量”切分。比如一個(gè)用戶套餐里有10GB月流量OCS一般不會(huì)一次下發(fā)10GB而是按1GB或2GB一段一段給每段都帶有效期。這樣即使用戶余額突然被管控下一段配額也發(fā)不出去。3.2 會(huì)話計(jì)費(fèi)的三個(gè)動(dòng)作授權(quán)、更新、釋放一個(gè)典型的OCS會(huì)話跑三次交互。第一次是Initial Request初始授權(quán)。網(wǎng)元發(fā)現(xiàn)新會(huì)話向OCS申請配額。OCS做的事情依次是識(shí)別用戶、檢查用戶狀態(tài)、查可用余額/套餐余量、按資費(fèi)批價(jià)、預(yù)占費(fèi)用、下發(fā)配額并記錄會(huì)話狀態(tài)。預(yù)占不是真的扣掉而是把“這筆錢可能消耗掉”標(biāo)記出來避免后續(xù)其他會(huì)話重復(fù)花。第二次是Update Request中間更新。用戶在配額快用完時(shí)網(wǎng)元會(huì)先上報(bào)“上一段已經(jīng)用了多少”O(jiān)CS根據(jù)實(shí)際使用量結(jié)算上一段、再?zèng)Q定下一段批多少。如果余額不足OCS可以下Final-Unit-Indication告訴網(wǎng)元“這是最后一次授權(quán)”用完就停。有些實(shí)現(xiàn)里網(wǎng)元也會(huì)主動(dòng)在配額沒過期但業(yè)務(wù)空閑時(shí)發(fā)更新請求把未用的配額還回來。第三次是Termination Request最終釋放。會(huì)話結(jié)束網(wǎng)元上報(bào)最終使用量。OCS計(jì)算實(shí)際費(fèi)用對比之前預(yù)占的金額多退少補(bǔ)關(guān)閉會(huì)話狀態(tài)。這里的“預(yù)占-確認(rèn)-回沖”就是余額倒扣的基本姿態(tài)。如果只做“結(jié)束再扣”會(huì)話期間的透支持續(xù)存在OCS就退化成離線計(jì)費(fèi)了。會(huì)話狀態(tài)的流轉(zhuǎn)是OCS實(shí)現(xiàn)里最容易亂的部分。每個(gè)會(huì)話在OCS側(cè)都有一個(gè)狀態(tài)常見是IDLE沒有會(huì)話、AUTHORIZED已授權(quán)、UPDATING更新中、TERMINATED已結(jié)束。網(wǎng)元每發(fā)一個(gè)請求OCS都先校驗(yàn)“當(dāng)前狀態(tài)是否允許這個(gè)請求”。比如一個(gè)Initial請求如果出現(xiàn)在AUTHORIZED狀態(tài)說明網(wǎng)元重復(fù)初始化應(yīng)該直接拒絕而不是重復(fù)給配額。狀態(tài)流轉(zhuǎn)表是OCS開發(fā)里必畫的圖不畫你會(huì)在第5章的時(shí)序坑里栽跟頭。3.3 余額模型與扣費(fèi)優(yōu)先級(jí)贈(zèng)送、套餐、現(xiàn)金誰先扣OCS的余額不是單一大字段而是一個(gè)帶優(yōu)先級(jí)的余額集合。一個(gè)賬戶下面通常掛多種余額現(xiàn)金余額、贈(zèng)送余額通常有有效期、套餐余額按流量/分鐘/條數(shù)計(jì)??圪M(fèi)時(shí)要按規(guī)則排序。我常用的優(yōu)先級(jí)排序是定向贈(zèng)送余額比如“夜間流量包”先扣因?yàn)橛行诙滩挥靡矔?huì)過期套餐內(nèi)余額月度流量/語音分鐘其次通用贈(zèng)送余額充話費(fèi)贈(zèng)送的錢再扣最后扣現(xiàn)金余額。每個(gè)檔位扣到不足時(shí)剩余部分從下一檔繼續(xù)扣而不是“要么全扣要么不扣”。這個(gè)邏輯叫余額分?jǐn)侽CS在預(yù)占時(shí)也要按分?jǐn)偨Y(jié)果去鎖額度。實(shí)現(xiàn)時(shí)余額表里的每一條記錄都要關(guān)系到一個(gè)配額記錄否則賬實(shí)無法核對。贈(zèng)送余額和套餐余額的過期處理也是OCS的常見矛盾點(diǎn)。比如用戶有一筆贈(zèng)送金30天后過期扣費(fèi)優(yōu)先級(jí)排在套餐后面結(jié)果套餐一直夠用贈(zèng)送金過期了用戶投訴“白送的為什么一句提示都沒有”。實(shí)現(xiàn)上可以在有效期到期前做提醒但提醒端到端的延遲是另一套體系不是OCS單點(diǎn)能解決的。在OCS內(nèi)部能做的是過期結(jié)算時(shí)生成賬單記錄供賬務(wù)系統(tǒng)后續(xù)處理。4. OCS計(jì)費(fèi)實(shí)現(xiàn)四張表、三個(gè)扣費(fèi)動(dòng)作和一個(gè)最小接口閉環(huán)4.1 四張核心表的結(jié)構(gòu)定義OCS實(shí)現(xiàn)不管上層多花哨落庫基本離不開四張表賬戶表、余額表、配額記錄表、話單表。下面是我常用的一套MySQL風(fēng)格建表金額和流量都存整數(shù)避免浮點(diǎn)誤差。CREATE TABLE acc_account ( acct_id BIGINT PRIMARY KEY, brand_id INT NOT NULL, credit_class SMALLINT NOT NULL DEFAULT 0, status TINYINT NOT NULL DEFAULT 1, created_at DATETIME NOT NULL ) ENGINEInnoDB; CREATE TABLE acct_balance ( balance_id BIGINT PRIMARY KEY, acct_id BIGINT NOT NULL, balance_type TINYINT NOT NULL COMMENT 1現(xiàn)金 2贈(zèng)送 3套餐, amount BIGINT NOT NULL DEFAULT 0 COMMENT 單位分, expire_date DATE NULL, status TINYINT NOT NULL DEFAULT 1, INDEX idx_acct (acct_id, status) ) ENGINEInnoDB; CREATE TABLE quota_record ( quota_id BIGINT PRIMARY KEY, session_id VARCHAR(64) NOT NULL, acct_id BIGINT NOT NULL, granted_units BIGINT NOT NULL COMMENT 本次授權(quán)量, used_units BIGINT NOT NULL DEFAULT 0, validity_time INT NOT NULL COMMENT 有效期秒, reserved_amount BIGINT NOT NULL COMMENT 預(yù)占金額分, status TINYINT NOT NULL COMMENT 1預(yù)占 2確認(rèn) 3釋放, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, UNIQUE KEY uk_session (session_id) ) ENGINEInnoDB; CREATE TABLE call_detail ( cdr_id BIGINT PRIMARY KEY, session_id VARCHAR(64) NOT NULL, acct_id BIGINT NOT NULL, rating_group INT NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, total_units BIGINT NOT NULL, total_amount BIGINT NOT NULL, UNIQUE KEY uk_session_start (session_id, start_time) ) ENGINEInnoDB;參數(shù)說明amount和total_amount都用“分”存儲(chǔ)不要用decimal也不要直接用元status字段的數(shù)字含義要在代碼里用枚舉類固化不能散落在業(yè)務(wù)邏輯里。quota_record里加了session_id唯一索引這是話單去重和配額防重放的第一道防線。call_detail表額外加了(session_id, start_time)唯一鍵后面對重復(fù)話單很管用。4.2 扣費(fèi)代碼的落地順序預(yù)占、確認(rèn)、回沖扣費(fèi)邏輯我一般拆成三個(gè)函數(shù)grant_quota做預(yù)占confirm_charge做確認(rèn)release_refund做回沖。下面是一段可讀性優(yōu)先的Python偽代碼重點(diǎn)看順序def grant_quota(acct_id, session_id, requested_units, tariff): with transaction() as tx: # 1. 鎖定余額行防止并發(fā)把同一筆錢批給兩個(gè)會(huì)話 bal tx.select_for_update( SELECT balance_id, amount FROM acct_balance WHERE acct_id%s AND balance_type1 AND status1 ORDER BY expire_date LIMIT 1, acct_id) # 2. 批價(jià)把請求的單位換算成預(yù)占金額 cost rating(tariff, requested_units) if bal.amount cost: granted bal.amount # 余額不足時(shí)按余額折算 cost rating(tariff, granted) else: granted requested_units # 3. 預(yù)占余額不是真的扣走而是先從余額里減掉 tx.update(UPDATE acct_balance SET amountamount-%s WHERE balance_id%s AND amount%s, cost, bal.balance_id, cost) # 4. 落配額記錄 tx.insert(INSERT INTO quota_record (session_id, acct_id, granted_units, reserved_amount, status) VALUES (%s,%s,%s,%s,1), session_id, acct_id, granted, cost) return granted, costconfirm_charge在會(huì)話結(jié)束時(shí)調(diào)用根據(jù)最終使用量重算費(fèi)用把真實(shí)費(fèi)用寫進(jìn)賬本再把預(yù)占差額回沖給余額def confirm_charge(session_id, used_units): with transaction() as tx: quota tx.select_for_update( SELECT * FROM quota_record WHERE session_id%s, session_id) actual_cost rating(quota.tariff, used_units) # 回沖差額預(yù)占的錢減掉實(shí)際用的錢退回余額 refund quota.reserved_amount - actual_cost if refund 0: tx.update(UPDATE acct_balance SET amountamount%s WHERE balance_id%s, refund, quota.balance_id) tx.update(UPDATE quota_record SET status2, used_units%s WHERE session_id%s, used_units, session_id) tx.insert(INSERT INTO call_detail (...) VALUES (...))注意grant_quota里第3步的UPDATE帶上了amount%s條件這是余額不被打穿的最后一道防線。第1步的select_for_update解決并發(fā)問題但即便鎖阻塞了條件更新的寫法也能兜住余額為負(fù)的情況。這段代碼里最關(guān)鍵的是第1步的select_for_update和第3步的“余額足夠才扣”條件。前者解決并發(fā)后者保證余額不會(huì)因?yàn)槌绦騜ug扣成負(fù)數(shù)。rating()函數(shù)在真實(shí)項(xiàng)目里會(huì)拿到資費(fèi)表、優(yōu)惠疊加規(guī)則和周期用量邏輯會(huì)復(fù)雜得多但扣費(fèi)的骨架不變。4.3 授權(quán)接口的字段設(shè)計(jì)與應(yīng)答碼OCS的對外接口在標(biāo)準(zhǔn)里是Diameter但在私有實(shí)現(xiàn)里很多團(tuán)隊(duì)會(huì)封裝成HTTP或Java接口。無論哪種字段都要貼近標(biāo)準(zhǔn)語義因?yàn)榫W(wǎng)元對接時(shí)是按標(biāo)準(zhǔn)字段來的。方向關(guān)鍵字段說明請求session_id網(wǎng)元生成的會(huì)話ID同一會(huì)話所有消息都帶請求cc_request_typeINITIAL/UPDATE/TERMINATION請求subscription_id用戶標(biāo)識(shí)常是MSISDN或IMSI請求requested_units本次申請的配額量請求used_units更新/結(jié)束時(shí)上報(bào)的上段使用量請求rating_group業(yè)務(wù)類型如語音/流量/短信應(yīng)答result_code2001成功4012余額不足4011用戶不存在應(yīng)答granted_units本次授權(quán)量應(yīng)答validity_time單位秒超過后配額失效應(yīng)答final_unit_indication標(biāo)記這是最后的配額應(yīng)答碼的設(shè)計(jì)對網(wǎng)元行為影響很大。4012BALANCE_NOT_ENOUGH應(yīng)答后網(wǎng)元應(yīng)該立即中斷會(huì)話而不是自己憑感覺繼續(xù)。如果把余額不足回成2001但granted_units0網(wǎng)元行為會(huì)不統(tǒng)一排障時(shí)到處燒香。這塊我建議在聯(lián)調(diào)階段就把每個(gè)應(yīng)答碼對應(yīng)的網(wǎng)元行為表簽死。5. OCS計(jì)費(fèi)落地避坑指南五個(gè)讓賬實(shí)不符的典型坑5.1 并發(fā)扣費(fèi)把余額扣成負(fù)數(shù)現(xiàn)象兩個(gè)會(huì)話同時(shí)請求配額第一個(gè)扣完余額還剩5元第二個(gè)也按5元批了出去結(jié)果余額變-3元。原因業(yè)務(wù)代碼先“查余額”再“扣余額”中間沒有加鎖兩個(gè)請求都讀到舊的余額值各自認(rèn)為自己那筆錢足夠。解決所有余額扣減必須走select_for_update或樂觀鎖版本號(hào)??蹨pSQL要帶余額條件例如UPDATE ... SET amountamount-5 WHERE balance_id1 AND amount5。余額不足返回4012不給配額。查當(dāng)前余額的SQL和扣減SQL之間不要留空隙鎖的粒度要鎖到余額行不是鎖賬戶。5.2 配額下發(fā)太大錢被預(yù)占?jí)毫艘徽宫F(xiàn)象用戶充了50元一次性全被預(yù)占只打了一分鐘電話剩下49元多被鎖住其他業(yè)務(wù)都顯示余額不足。原因Initial時(shí)直接把賬戶余額當(dāng)作配額發(fā)出去沒有按單次會(huì)話上限和有效期控制。預(yù)占雖然是臨時(shí)的但預(yù)占期間這部分錢不能另作他用。解決配額下發(fā)達(dá)到一個(gè)上限比如單次會(huì)話最多預(yù)占10元或1GBValidity Time設(shè)600秒到期網(wǎng)元必須更新或釋放。配額給多還有一個(gè)副作用一旦用戶資金被管控過大的預(yù)占會(huì)讓余額凍結(jié)時(shí)間變長對賬時(shí)審計(jì)被問好幾輪。給配額記錄表加created_at和updated_at回沖時(shí)效性一目了然。5.3 話單重復(fù)上報(bào)同一筆通話扣了兩次現(xiàn)象用戶打了一次電話余額被扣兩筆查話單有兩條相同起止時(shí)間的記錄。原因網(wǎng)元超時(shí)重傳Termination CCR兩次都進(jìn)入confirm_charge而話單表沒有針對業(yè)務(wù)維度的唯一約束。解決call_detail表建唯一索引字段是(session_id, start_time)confirm_charge里在insert話單時(shí)捕獲duplicate插入如果發(fā)現(xiàn)同一session已經(jīng)結(jié)算過第二次直接返回成功不再次扣費(fèi)。話單去重不能只靠業(yè)務(wù)流水號(hào)因?yàn)榫W(wǎng)元重傳時(shí)可能生成新的流水號(hào)。最可靠的是業(yè)務(wù)維度session_id配合start_time。如果還要防跨天就把start_time精確到秒并拼接成yyyyMMddHHmmss有UTC偏移量再加上偏移量。5.4 釋放請求先于更新請求到達(dá)回沖亂套現(xiàn)象用戶的配額還沒更新完會(huì)話就釋放了回沖金額按最終使用量算多退了幾塊錢。原因OCS沒有對請求做狀態(tài)機(jī)校驗(yàn)。網(wǎng)絡(luò)里消息亂序是常態(tài)釋放和更新可能同時(shí)在路上。解決每個(gè)CCR請求帶Sequence NumberOCS側(cè)按sessionsequence排序只接受序號(hào)連續(xù)且狀態(tài)允許的消息。狀態(tài)機(jī)先判斷再執(zhí)行扣款避免舊請求覆蓋新狀態(tài)。這種坑最惡心的地方是它不報(bào)錯(cuò)只把余額算錯(cuò)對賬的時(shí)候才暴露。所以配額記錄表一定要留status和update_count字段每次更新都疊加上去最后用update_count判斷消息是否缺失。5.5 跨網(wǎng)元時(shí)鐘不一致扣費(fèi)時(shí)長算錯(cuò)現(xiàn)象一端記錄的通話時(shí)長是320秒另一端是350秒差價(jià)導(dǎo)致扣費(fèi)不一致。原因OCS按會(huì)話開始/結(jié)束時(shí)間計(jì)算時(shí)長而這個(gè)時(shí)間是網(wǎng)元在各自本地時(shí)鐘下寫的。不同網(wǎng)元之間時(shí)鐘偏差可能達(dá)到秒級(jí)甚至分鐘級(jí)。解決OCS側(cè)統(tǒng)計(jì)時(shí)長以O(shè)CS接收Termination的時(shí)間減去OCS接收Initial的時(shí)間不依賴業(yè)務(wù)側(cè)上報(bào)的起止時(shí)間同時(shí)全網(wǎng)網(wǎng)元統(tǒng)一NTP同步。若無法統(tǒng)一話單里記錄時(shí)間偏移量由OCS校正。排查這類問題最簡單的方法是抓兩臺(tái)設(shè)備日志里的時(shí)間戳對比。讓網(wǎng)元在CCR里帶上接入網(wǎng)元的時(shí)間OCS回CCA時(shí)把這個(gè)時(shí)間回顯聯(lián)調(diào)階段一眼就能看到時(shí)差。6. 用模擬會(huì)話驗(yàn)證OCS計(jì)費(fèi)從配額下發(fā)到話單落庫的校驗(yàn)方法最后把第4章的邏輯串成一個(gè)可跑通的模擬腳本適合在改完表結(jié)構(gòu)或扣費(fèi)算法后快速驗(yàn)證“余額變動(dòng)對不對”。def simulate_call(acct_id, duration_seconds): session_id uuid4().hex # 1. 發(fā)起授權(quán)拿到配額 granted, reserved grant_quota(acct_id, session_id, duration_seconds, VOICE_TARIFF) print(授權(quán)配額:, granted, 預(yù)占金額:, reserved) # 2. 模擬業(yè)務(wù)跑了一半網(wǎng)元更新用量 used granted // 2 confirm_charge(session_id, used) # 3. 業(yè)務(wù)結(jié)束網(wǎng)元上報(bào)完整用量并釋放 confirm_charge(session_id, duration_seconds) # 4. 校驗(yàn)余額 初始余額 - 實(shí)際費(fèi)用 balance query_balance(acct_id) assert balance initial_balance - rating(duration_seconds)腳本的四個(gè)步驟對應(yīng)第3章的授權(quán)、更新、釋放第4章的三段偽代碼可以直接插入。輸出結(jié)果里重點(diǎn)看三處預(yù)占金額、回沖金額、最終余額是否與實(shí)際費(fèi)用一致。如果中間某步結(jié)果和預(yù)期差一分錢先查是不是預(yù)占沒回沖再查是不是話單重復(fù)插入。我自己的習(xí)慣是每次改配額算法或資費(fèi)規(guī)則先用這個(gè)腳本跑三組用例——正常用完、余額不足、中途主動(dòng)釋放配額——每組跑完都對賬余額。三組都過了才提交代碼。這套模擬驗(yàn)證擋住過不少黑匣子問題多數(shù)賬實(shí)不符的bug并不是資費(fèi)算錯(cuò)而是預(yù)占和回沖的時(shí)序問題。只要把“會(huì)話有狀態(tài)、扣費(fèi)有鎖、話單有唯一鍵、配額有有效期”這四件事做到位OCS的實(shí)時(shí)扣費(fèi)鏈路就立得住了。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取