ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

OCS计费原理与实现:预付费实时扣费链路详解

OCS计费原理与实现:预付费实时扣费链路详解 简介华为内部技术文档《OCS计费原理与实现排版后》面向电信计费研发、系统运维及通信专业读者系统阐述在线计费系统的原理与落地实现。内容涵盖计费系统从离线到实时的演进脉络详细解释TAG、业务用量、费用项、产品、订购、入帐关系等核心概念并完整梳理事件捕获、事件处理、生成账单、结算的计费流程。实现部分涉及计费引擎、数据库、消息中间件及安全机制等关键组件可帮助读者建立从原理到实际部署的整体认知。文档源自华为技术有限公司内部资料保留原始章节结构与流程图说明对于梳理实时计费系统的业务逻辑与关键节点非常有帮助。资源包共1个文件为doc格式文档压缩包大小1.87MB排版后的正文便于阅读。目前已有297人学习下载适合需要深入理解OCS机制或开展内部培训的工程师参考。1. OCS计费原理与实现为什么预付费用户离不开这条实时扣费链路客服坐席被用户质问余额明明还有38.6元为什么10元的流量包订购失败。查了一圈发现账务系统里的余额和OCS实时余额差了正好10元——这笔钱在离线账本里还没扣但OCS已经把它预占走了。OCS计费原理与实现讲的就是这条实时扣费链路用户每次上网、打电话、发短信网元都会先向OCS申请配额OCS完成鉴权、批价、扣费后才放行业务。这套系统适合预付费用户的余额实时控制、套餐流量和时长的准实时扣减以及需要账务联动同步的业务线。下面按位置、原理、落表、踩坑、验证的顺序展开新手能照着建表写逻辑熟手可以对着排查思路核对边界。2. OCS在计费域里的位置离线计费撑不住的地方在线计费来兜底2.1 离线计费为什么跟不上预付费话单延迟的代价离线计费的链路是网络设备在会话结束后生成CDR话单经过采集、清洗、批价进入账务系统最后在出账日统一结算。这条链路的延迟通常以分钟甚至小时计。对后付费用户来说延迟一个月都没问题对预付费用户来说延迟几分钟就可能让余额耗尽的用户继续享受服务形成欠费风险。更麻烦的是用户通过营业厅或App查询余额时看到的是账务系统里的“滞后数字”和实际可用的实时余额对不上投诉就来了。OCS把流程倒过来会话开始前网元必须先向OCS发起鉴权计费请求拿到配额才允许业务使用。配额用完网元再次申请申请不到业务被切断。这样“用多少钱由OCS现场决定”的模式才是在线计费的正解。OCS与网元之间走的是Diameter Credit-Control协议Ro接口承载在线计费请求。网元发出的请求叫CCRCredit-Control RequestOCS返回的叫CCACredit-Control Answer。CCR/CCA里的“CC-Request-Type”字段区分这是一次会话的初始请求、中间更新还是最终结束这套协议栈是3GPP标准定义的自研实现时只要把该字段和配额字段对清楚就能和设备厂商联调。离线与在线的关键差异可以用一张表说清维度离线计费在线计费计费时机会话结束后会话开始前与过程中数据来源CDR话单计费请求/应答超额控制出账后追缴配额用尽即断余额查询滞后账本实时余额典型场景后付费月结预付费语音、流量2.2 OCS、GRS、RCS在生产网里的配合方式在运营商内部的生产培训教材里常把OCS、GRS、RCS这些网元缩写画在同一张链路上。很多新人被缩写吓住实际上它们之间的交互最终都是同一个动作网元发配额申请OCS回配额结果。OCS前面接的是各类业务网关——语音的、流量的、消息的OCS后面接的是账务系统、客服系统和批价规则库。一次流量上网会话的交互大概是这样PGW收到用户上网请求发现是预付费用户先不直接放行而是向OCS发一个Initial CCR里面带用户标识、业务标识、请求的流量大小。OCS经过批价发现用户套餐里还有2GB月度流量于是回一个CCA带“本次授权可以使用1GB有效期600秒”。PGW放行1GB。用户跑完900MB后PGW再发Update CCROCS继续授权下一段。这中间网元和OCS之间没有任何人工账单参与完全是机器对话。现网实现里OCS内部控制一般分成四个域批价引擎负责把业务使用量换算成金额余额中心负责统筹账户下的现金、赠送、套餐额度配额管理器负责决定本次下发多少配额、有效期多长话单网关负责把会话结束后的使用明细转成标准CDR送给账务系统。GRS、RCS这类缩写要么是围绕OCS的配置/规则管理能力要么是会话与资源状态统计能力不必一个一个背抓住“批价余额配额”这条主线就够了。2.3 哪些业务必须交给OCS判断标准不是所有业务都要塞进OCS。我一般按三条标准判断一是业务是否需要“事前实时放行”。比如VoLTE通话、流量上网、短信发送用户对实时性要求高且存在透支风险必须在线计费。二是是否存在“配额”概念。凡是可以按时间、流量、条数切分成一段一段使用的资源都适合OCS一次性点播内容更适合按次计费也可以走OCS的灵活配额。三是是否需要余额联动。如果业务要保证用户余额与可用资源强一致OCS就是正确选择。一个反例企业专线月结业务用户按合同月底统一付款业务是长期在线的没有“配额”概念硬套OCS的话要么每个会话都下发超大配额要么频繁中断体验很差。这种走传统离线计费更合适。所以OCS是工具不是所有计费问题的银弹。判断清楚了再进入原理层。3. OCS计费核心原理配额、会话和余额倒扣的三层模型3.1 配额是实时计费的最小单位余额不能直接当令牌发OCS不把“余额”直接下发给网元。原因很实际网元是网络设备它只认“能用多少时间/流量”不认“账上有多少钱”。而且余额是总账如果网元拿到总账就直接放行用户很可能把余额一次耗尽、甚至超额等到话单回来已经晚了。所以OCS要把余额转成配额Quota。配额的含义是“本次会话中网元最多可以用掉的量”。它可以按时间秒、按流量字节、按费用金额或按业务特定单位来定义。一次授权通常包含三要素配额大小本次允许用多少时间/流量/金额配额类型time、total-octets、input-octets、output-octets、service-specific有效期Validity Time在这个时间内配额有效过了时间网元必须停用并用完的部分归还。配额的粒度是OCS调优的关键。给大了用户可能透支给小了网元频繁发请求信令压力大。常见做法是先按套餐周期估算可用量再按“单次会话最多使用量”切分。比如一个用户套餐里有10GB月流量OCS一般不会一次下发10GB而是按1GB或2GB一段一段给每段都带有效期。这样即使用户余额突然被管控下一段配额也发不出去。3.2 会话计费的三个动作授权、更新、释放一个典型的OCS会话跑三次交互。第一次是Initial Request初始授权。网元发现新会话向OCS申请配额。OCS做的事情依次是识别用户、检查用户状态、查可用余额/套餐余量、按资费批价、预占费用、下发配额并记录会话状态。预占不是真的扣掉而是把“这笔钱可能消耗掉”标记出来避免后续其他会话重复花。第二次是Update Request中间更新。用户在配额快用完时网元会先上报“上一段已经用了多少”OCS根据实际使用量结算上一段、再决定下一段批多少。如果余额不足OCS可以下Final-Unit-Indication告诉网元“这是最后一次授权”用完就停。有些实现里网元也会主动在配额没过期但业务空闲时发更新请求把未用的配额还回来。第三次是Termination Request最终释放。会话结束网元上报最终使用量。OCS计算实际费用对比之前预占的金额多退少补关闭会话状态。这里的“预占-确认-回冲”就是余额倒扣的基本姿态。如果只做“结束再扣”会话期间的透支持续存在OCS就退化成离线计费了。会话状态的流转是OCS实现里最容易乱的部分。每个会话在OCS侧都有一个状态常见是IDLE没有会话、AUTHORIZED已授权、UPDATING更新中、TERMINATED已结束。网元每发一个请求OCS都先校验“当前状态是否允许这个请求”。比如一个Initial请求如果出现在AUTHORIZED状态说明网元重复初始化应该直接拒绝而不是重复给配额。状态流转表是OCS开发里必画的图不画你会在第5章的时序坑里栽跟头。3.3 余额模型与扣费优先级赠送、套餐、现金谁先扣OCS的余额不是单一大字段而是一个带优先级的余额集合。一个账户下面通常挂多种余额现金余额、赠送余额通常有有效期、套餐余额按流量/分钟/条数计。扣费时要按规则排序。我常用的优先级排序是定向赠送余额比如“夜间流量包”先扣因为有效期短不用也会过期套餐内余额月度流量/语音分钟其次通用赠送余额充话费赠送的钱再扣最后扣现金余额。每个档位扣到不足时剩余部分从下一档继续扣而不是“要么全扣要么不扣”。这个逻辑叫余额分摊OCS在预占时也要按分摊结果去锁额度。实现时余额表里的每一条记录都要关系到一个配额记录否则账实无法核对。赠送余额和套餐余额的过期处理也是OCS的常见矛盾点。比如用户有一笔赠送金30天后过期扣费优先级排在套餐后面结果套餐一直够用赠送金过期了用户投诉“白送的为什么一句提示都没有”。实现上可以在有效期到期前做提醒但提醒端到端的延迟是另一套体系不是OCS单点能解决的。在OCS内部能做的是过期结算时生成账单记录供账务系统后续处理。4. OCS计费实现四张表、三个扣费动作和一个最小接口闭环4.1 四张核心表的结构定义OCS实现不管上层多花哨落库基本离不开四张表账户表、余额表、配额记录表、话单表。下面是我常用的一套MySQL风格建表金额和流量都存整数避免浮点误差。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现金 2赠送 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 本次授权量, used_units BIGINT NOT NULL DEFAULT 0, validity_time INT NOT NULL COMMENT 有效期秒, reserved_amount BIGINT NOT NULL COMMENT 预占金额分, status TINYINT NOT NULL COMMENT 1预占 2确认 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;参数说明amount和total_amount都用“分”存储不要用decimal也不要直接用元status字段的数字含义要在代码里用枚举类固化不能散落在业务逻辑里。quota_record里加了session_id唯一索引这是话单去重和配额防重放的第一道防线。call_detail表额外加了(session_id, start_time)唯一键后面对重复话单很管用。4.2 扣费代码的落地顺序预占、确认、回冲扣费逻辑我一般拆成三个函数grant_quota做预占confirm_charge做确认release_refund做回冲。下面是一段可读性优先的Python伪代码重点看顺序def grant_quota(acct_id, session_id, requested_units, tariff): with transaction() as tx: # 1. 锁定余额行防止并发把同一笔钱批给两个会话 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. 批价把请求的单位换算成预占金额 cost rating(tariff, requested_units) if bal.amount cost: granted bal.amount # 余额不足时按余额折算 cost rating(tariff, granted) else: granted requested_units # 3. 预占余额不是真的扣走而是先从余额里减掉 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在会话结束时调用根据最终使用量重算费用把真实费用写进账本再把预占差额回冲给余额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) # 回冲差额预占的钱减掉实际用的钱退回余额 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解决并发问题但即便锁阻塞了条件更新的写法也能兜住余额为负的情况。这段代码里最关键的是第1步的select_for_update和第3步的“余额足够才扣”条件。前者解决并发后者保证余额不会因为程序bug扣成负数。rating()函数在真实项目里会拿到资费表、优惠叠加规则和周期用量逻辑会复杂得多但扣费的骨架不变。4.3 授权接口的字段设计与应答码OCS的对外接口在标准里是Diameter但在私有实现里很多团队会封装成HTTP或Java接口。无论哪种字段都要贴近标准语义因为网元对接时是按标准字段来的。方向关键字段说明请求session_id网元生成的会话ID同一会话所有消息都带请求cc_request_typeINITIAL/UPDATE/TERMINATION请求subscription_id用户标识常是MSISDN或IMSI请求requested_units本次申请的配额量请求used_units更新/结束时上报的上段使用量请求rating_group业务类型如语音/流量/短信应答result_code2001成功4012余额不足4011用户不存在应答granted_units本次授权量应答validity_time单位秒超过后配额失效应答final_unit_indication标记这是最后的配额应答码的设计对网元行为影响很大。4012BALANCE_NOT_ENOUGH应答后网元应该立即中断会话而不是自己凭感觉继续。如果把余额不足回成2001但granted_units0网元行为会不统一排障时到处烧香。这块我建议在联调阶段就把每个应答码对应的网元行为表签死。5. OCS计费落地避坑指南五个让账实不符的典型坑5.1 并发扣费把余额扣成负数现象两个会话同时请求配额第一个扣完余额还剩5元第二个也按5元批了出去结果余额变-3元。原因业务代码先“查余额”再“扣余额”中间没有加锁两个请求都读到旧的余额值各自认为自己那笔钱足够。解决所有余额扣减必须走select_for_update或乐观锁版本号。扣减SQL要带余额条件例如UPDATE ... SET amountamount-5 WHERE balance_id1 AND amount5。余额不足返回4012不给配额。查当前余额的SQL和扣减SQL之间不要留空隙锁的粒度要锁到余额行不是锁账户。5.2 配额下发太大钱被预占压了一整夜现象用户充了50元一次性全被预占只打了一分钟电话剩下49元多被锁住其他业务都显示余额不足。原因Initial时直接把账户余额当作配额发出去没有按单次会话上限和有效期控制。预占虽然是临时的但预占期间这部分钱不能另作他用。解决配额下发达到一个上限比如单次会话最多预占10元或1GBValidity Time设600秒到期网元必须更新或释放。配额给多还有一个副作用一旦用户资金被管控过大的预占会让余额冻结时间变长对账时审计被问好几轮。给配额记录表加created_at和updated_at回冲时效性一目了然。5.3 话单重复上报同一笔通话扣了两次现象用户打了一次电话余额被扣两笔查话单有两条相同起止时间的记录。原因网元超时重传Termination CCR两次都进入confirm_charge而话单表没有针对业务维度的唯一约束。解决call_detail表建唯一索引字段是(session_id, start_time)confirm_charge里在insert话单时捕获duplicate插入如果发现同一session已经结算过第二次直接返回成功不再次扣费。话单去重不能只靠业务流水号因为网元重传时可能生成新的流水号。最可靠的是业务维度session_id配合start_time。如果还要防跨天就把start_time精确到秒并拼接成yyyyMMddHHmmss有UTC偏移量再加上偏移量。5.4 释放请求先于更新请求到达回冲乱套现象用户的配额还没更新完会话就释放了回冲金额按最终使用量算多退了几块钱。原因OCS没有对请求做状态机校验。网络里消息乱序是常态释放和更新可能同时在路上。解决每个CCR请求带Sequence NumberOCS侧按sessionsequence排序只接受序号连续且状态允许的消息。状态机先判断再执行扣款避免旧请求覆盖新状态。这种坑最恶心的地方是它不报错只把余额算错对账的时候才暴露。所以配额记录表一定要留status和update_count字段每次更新都叠加上去最后用update_count判断消息是否缺失。5.5 跨网元时钟不一致扣费时长算错现象一端记录的通话时长是320秒另一端是350秒差价导致扣费不一致。原因OCS按会话开始/结束时间计算时长而这个时间是网元在各自本地时钟下写的。不同网元之间时钟偏差可能达到秒级甚至分钟级。解决OCS侧统计时长以OCS接收Termination的时间减去OCS接收Initial的时间不依赖业务侧上报的起止时间同时全网网元统一NTP同步。若无法统一话单里记录时间偏移量由OCS校正。排查这类问题最简单的方法是抓两台设备日志里的时间戳对比。让网元在CCR里带上接入网元的时间OCS回CCA时把这个时间回显联调阶段一眼就能看到时差。6. 用模拟会话验证OCS计费从配额下发到话单落库的校验方法最后把第4章的逻辑串成一个可跑通的模拟脚本适合在改完表结构或扣费算法后快速验证“余额变动对不对”。def simulate_call(acct_id, duration_seconds): session_id uuid4().hex # 1. 发起授权拿到配额 granted, reserved grant_quota(acct_id, session_id, duration_seconds, VOICE_TARIFF) print(授权配额:, granted, 预占金额:, reserved) # 2. 模拟业务跑了一半网元更新用量 used granted // 2 confirm_charge(session_id, used) # 3. 业务结束网元上报完整用量并释放 confirm_charge(session_id, duration_seconds) # 4. 校验余额 初始余额 - 实际费用 balance query_balance(acct_id) assert balance initial_balance - rating(duration_seconds)脚本的四个步骤对应第3章的授权、更新、释放第4章的三段伪代码可以直接插入。输出结果里重点看三处预占金额、回冲金额、最终余额是否与实际费用一致。如果中间某步结果和预期差一分钱先查是不是预占没回冲再查是不是话单重复插入。我自己的习惯是每次改配额算法或资费规则先用这个脚本跑三组用例——正常用完、余额不足、中途主动释放配额——每组跑完都对账余额。三组都过了才提交代码。这套模拟验证挡住过不少黑匣子问题多数账实不符的bug并不是资费算错而是预占和回冲的时序问题。只要把“会话有状态、扣费有锁、话单有唯一键、配额有有效期”这四件事做到位OCS的实时扣费链路就立得住了。希望帮到你。本文还有配套的精品资源点击获取
返回列表