移动钱包这个词现在听着挺"互联网"的,但真把时间轴拉长看,你会发现它最早、也最扎实的那一批落地形态,压根没跑在数据网络上,而是长在通信网络的信令和短信通道里。USSD、短信、SIM卡工具包,这三样东西撑起了大量早期移动钱包的全部门面。我带过几个这类项目,也从零写过 USSD 菜单树、对过 SMSC 的账、被字符集坑到半夜改代码。这篇文章就把这套体系的骨架拆开讲:它为什么这么设计、USSD 到底跑在哪条通道上、会话状态怎么管、账务核心和代理网络怎么接、以及真上手时会踩哪些坑。适合三类人看:做支付/钱包产品的、做电信增值业务对接的、以及想搞明白"为什么很多地方的钱包看起来这么土却这么好用"的技术同学。哪怕你只会写点 Python、对电信协议一无所知,也能看懂大半,因为我会尽量用生活化的类比把信令层的事说清楚。
1. 为什么钱包会长在通信网络上:设备、账户、信任三条链路
这个问题我被人问过很多次,问法通常是"你们为什么不用 App,非要用那么难用的星号井号"。答案不在技术偏好,而在约束条件。移动钱包要成立,必须同时解决三件事:用户手上得有个能交互的终端、系统得能确认"你是谁"、钱得有个能进能出的池子。在智能手机和数据资费普及之前,这三件事在通信网络里全部都有现成答案,而且是几十年沉淀下来的、覆盖到毛细血管的答案。
1.1 终端现实:功能机的交互能力被严重低估
很多人默认"功能机=不能做业务",这是错的。一台十几年前的功能机,具备完整的键盘输入、屏幕显示、短信收发、以及通过 SIM 卡运行的小应用能力。它缺的只是图形界面和数据连接,但支付这种业务,90% 的交互其实就是"选一个菜单项、输一串数字、看一眼结果"。这恰好是文本菜单能覆盖的形状。
我做第一版钱包的时候做过统计,目标用户群里智能机占比不到三成,剩下七成里有一半是键盘机,还有一部分是连浏览器都没有的极简机型。你要覆盖这批人,只有两条路:要么打电话给客服人工下单,成本高到不可能;要么用通信网络自带的文本交互通道。USSD 就是为这个场景存在的——它不需要装任何东西,不需要流量,拨一串星号井号的短码就能拉起一个菜单,任何一台能打电话的手机都支持。这个"零安装、零流量、零门槛"的特性,在推广初期的价值远超任何交互体验上的先进。
再补一句实操感受:功能机用户的输入习惯和触屏用户完全不同。他们习惯用数字键选菜单("按 1 转账,按 2 查询"),不习惯滚动;习惯短句子,超过一屏的文字会直接放弃;对"确认/取消"的语义极其敏感,一步走错就会挂断重来。这些习惯直接决定了后面菜单树的设计原则,第 2 章会细讲。
1.2 账户现实:手机号是最便宜的身份凭证
做金融业务最贵的一块是 KYC(了解你的客户)。你要确认一个人真实存在、能联系上、且这个身份是唯一的。在很多市场,手机号是唯一一个已经完成实名登记、覆盖率极高、且天然唯一的标识。这意味着:MSISDN(手机号码)可以直接当作钱包主键,用户注册时不需要再提交一堆材料,运营商侧的身份信息已经完成了第一层校验。
对比一下 App 路线:用户下载、注册、填手机号、收验证码、设密码、绑卡,每一步都是流失点。而通信通道的钱包,用户拨个短码,系统从信令里直接拿到他的号码,连"我是谁"都不用问。这个体验差异在获客成本上是数量级的。
从系统设计角度,这带来一个很重要的约束:手机号即账户,账户即身份,三者强绑定。好处是简单,坏处是换号等于换账户,且号码回收(运营商把停用号码重新放号)会带来账户归属风险。我后面在第 4 章会讲怎么用"号码 + 首次激活时间 + 设备指纹"做二次校验来缓解这个问题。
1.3 结算现实:话费账户是最早的"电子钱包"
第三个条件最容易被忽略:钱怎么进去。在银行卡渗透率低的市场,普通人手里是现金,而现金要变成电子余额,需要一个遍布街角的"充值点"。运营商为了卖话费,早就建好了这张网——小卖部、报刊亭、代理商,都能收现金、给你账户加钱。这套网络的存在,让钱包业务可以零成本复用:用户给代理商现金,代理商给他的钱包账户充值,钱从"话费余额"这个池子里挪到"钱包余额"。
这里有个关键设计叫话费账户与钱包账户的隔离。早期有些方案图省事,直接把钱包余额挂在话费余额上,结果出了大问题:话费余额会被套餐抵扣、会被有效期清零、会被运营商侧的各种规则影响,根本不适合承载金融余额。正确做法是两套账,中间用充值/提现两条通道连接,钱包侧独立记账、独立对账。这个教训我吃过一次,用户投诉"我明明充了钱,怎么打电话打没了",排查半天发现是账没隔离。
把三条链路串起来看,逻辑就很清楚了:通信网络提供了**终端(功能机 + USSD)、身份(MSISDN)、资金入口(代理充值网络)**这三样基础设施,钱包业务只是在这套基础设施上叠了一层业务逻辑。这就是它"长在通信网络上"的根本原因——不是技术选择,是基础设施决定的路径。
2. USSD:把功能机变成交易终端的信令通道
这一章是全文技术密度最高的部分,我会把 USSD 的通道位置、会话模型、以及由此带来的工程约束讲透。搞明白这一层,你再看任何通信通道的钱包,都能一眼看出它的设计取舍。
2.1 USSD 到底跑在哪条通道上
先纠正一个常见误解:USSD 不是短信,也不是数据业务。它走的是信令通道,具体来说是基于七号信令(SS7)体系里的 MAP 协议。用户拨*123#之后,手机会把这个请求通过基站送到核心网,由 HLR(归属位置寄存器)识别出这是一条 USSD 请求,转给 USSD 网关(也叫 USSDC),网关再通过 HTTP/XML 或私有协议把请求转给你的业务系统。
这个路径决定了 USSD 的几个硬特性,我列个表对比一下,方便你建立直觉:
| 特性 | USSD | 短信(SMS) | 数据业务(HTTP) |
|---|---|---|---|
| 承载通道 | 信令(SS7/MAP) | 信令(存储转发) | 用户面(分组数据) |
| 是否需要流量 | 否 | 否 | 是 |
| 交互形态 | 实时会话,一问一答 | 单向投递 | 请求响应 |
| 典型响应时限 | 10~20 秒 | 秒级到分钟级不定 | 取决于网络 |
| 是否需要安装 | 否 | 否 | 需要 |
| 会话状态 | 网关侧维护 | 无 | 应用侧维护 |
关键差异在"实时会话"这一条:USSD 是一次会话,用户按下发送后,屏幕进入等待状态,业务系统必须在超时窗口内(通常 10 到 20 秒,各运营商配置不同)返回下一屏内容,否则会话被网络侧强制释放,用户看到的是"请求超时"。这意味着你的后端处理链路不能有长耗时操作——查余额可以,但如果你在菜单里做一次跨系统的转账,必须在超时窗口内完成或者先返回"处理中"。
我踩过的最典型的一个坑:某次账务系统在做日终批处理,锁表锁了几十秒,结果那段时间所有 USSD 交易全部超时,用户端看到的是"网络错误",但实际交易在后台已经成功了。这引出了后面反复强调的原则:通道超时不等于交易失败,两者必须解耦,靠幂等和查询来兜底。
2.2 会话模型:无状态协议如何撑起有状态业务
USSD 协议本身是无状态的:每一次请求都携带一个会话 ID、用户号码、本次输入的字符串,以及一个"是否新会话"的标志。业务系统要自己维护一棵菜单树和用户当前所处的位置。这个模型和 HTTP 很像,你可以把它理解为"信令层的短连接"。
网关给业务系统的报文通常长这样,不同厂商字段名会变,但结构大同小异:
POST /ussd/gateway { "sessionId": "a1b2c3d4-...", "msisdn": "86xxxxxxxxxxx", "ussdString": "*123*2*1#", "newRequest": true, "networkCode": "op-x" }newRequest为 true 时,说明是用户刚拨入的第一帧,ussdString里带的是完整短码;为 false 时,ussdString里通常只有用户本次按下的那个数字。业务系统返回的是一段文本加一个动作标识:CON表示"继续会话,等用户输入",END表示"结束会话"。
用生活类比:这就像你去银行柜台,柜员每次只问你一个问题,你答完他就把之前所有信息记在小本子上,然后问下一个问题。USSD 网关就是那个柜员,小本子就是你的会话状态存储。
状态存在哪?两种做法。一是放 Redis,key 为 sessionId,带 TTL(通常设 90 到 120 秒,略长于网络超时,避免用户还在等你先把状态删了);二是放关系库,适合强审计要求的场景,但要加过期清理任务。我一般用 Redis 做主存 + 交易流水落库,兼顾性能和可追溯。
注意:会话超时后用户重新拨入,sessionId 会变,之前的菜单位置全部丢失,用户会回到首页。所以关键操作(比如输金额之后)不要指望"用户还能回到上一步",每一步都要给明确的"返回"和"取消"选项。
2.3 菜单树设计的工程约束
菜单树是 USSD 钱包的门面,也是最容易被低估的设计工作。我总结了几条硬约束,都是踩出来的:
第一,层级不超过三层。用户每进一层都要重新建立上下文,超过三层,完成率断崖式下跌。实测数据是:一层菜单完成率约 70%,两层约 50%,三层约 30%,四层基本没人走完。所以高频操作(查余额、转账、充值)必须放在第一层。
第二,每屏不超过 160 个字符。这个数字和短信单条容量接近不是巧合,都是 GSM 时代的产物。超了会被网关截断,用户看到的是一句半截的话,非常糟糕。做法是留出 20 个字符的余量,正文控制在 140 以内。
第三,输入校验必须前置。用户输金额,你要在服务端校验是不是数字、有没有超限、余额够不够,然后把错误提示放在同一屏里让他重输,而不是先结束会话再发短信通知。后者体验极差。
第四,每个动作都要幂等。用户可能因为屏幕没反应连按两次,也可能网络重传。同一个 sessionId 下的同一个确认动作,重复到达时只能处理一次。做法是用"sessionId + 步骤号 + 用户输入哈希"生成幂等键。
最后提醒一个细节:不同运营商对*和#的解析、对输入长度的限制、对特殊字符(比如空格、中文标点)的容忍度都不一样。上线前必须在每个目标运营商网络上逐一真机测试,别只在模拟网关上跑通就发版。
3. 短信与 STK:通知通道与卡内安全区
USSD 负责"用户主动发起",短信负责"系统主动触达",这两者是一对搭档。而 STK(SIM 卡工具包)则是另一种思路:把一部分逻辑和密钥塞进 SIM 卡,让卡本身成为一个安全执行环境。这两块经常被混在一起讲,其实职责完全不同。
3.1 短信的角色分工:通知、回执、兜底
短信在钱包体系里绝不是"发个广告"那么简单,它承担三个关键职责:
交易凭证。USSD 会话以END结束,屏幕上只给一句话,用户拿不到可保存的凭据。这时候补发一条短信,写明金额、对方、时间、流水号,就是法律意义上的交易记录。很多市场的监管要求也明确规定了这一点。
异步结果通知。前面说过,USSD 有超时限制,复杂交易只能"先返回处理中,结果另发"。短信就是那个异步回执通道。这里要注意:短信是存储转发的,延迟可能是几秒也可能是几分钟,所以文案里不能写"预计 X 秒内到账",写"到账后将另行通知"更稳妥。
兜底通道。用户手机不支持 USSD、或者当时在通话中无法使用 USSD 时,可以用短信指令做交易(发送固定格式到指定号码)。这条通道效率低、安全性差,但作为兜底手段有价值。我一般建议只保留查询类指令,交易类一律走 USSD 或 App。
短信通道的技术要点在于与 SMSC 的对接。上行(用户发给系统)走 MO 短信,下行走 MT 短信。批量下发要做限速,运营商侧一般有 TPS 限制,超了会被丢包甚至封通道。长短信要注意拼接:超过 70 个中文字符会被拆成多条,接收端靠 UDH 头拼接,但部分老年机和某些网关不支持,会出现乱序。所以关键通知控制在 70 字以内,把流水号这类关键信息放在最前面。
3.2 STK 与 SIM 卡作为安全载体
STK 的思路完全不同:它在 SIM 卡里跑一个 Java Card 或原生小应用,手机开机后菜单里会多出一项(通常叫"我的钱包"之类),用户点进去是一个卡内菜单。这个菜单能做的事包括显示文本、获取用户输入、发短信、甚至发起加密运算。
它最大的价值是密钥不出卡。交易的签名、PIN 校验都可以在卡内完成,手机侧和网络侧都碰不到明文密钥。这比"App 里存密钥"的安全性高一个量级,比"USSD 明文传输"更是天壤之别。早期很多钱包要求用户设置一个 4 到 6 位的钱包 PIN,这个 PIN 的校验逻辑就在卡里,连输错三次锁卡——这个机制和银行卡的 PIN 是同一个原理。
代价也很明显:卡应用的开发、测试、发卡、更新全部要过运营商和卡商的流程,周期以月计,成本高;而且卡内存储和算力有限,能做的事不多。所以 STK 通常只承载最核心的安全环节(PIN 校验 + 交易签名),业务逻辑还是放在后台。
3.3 OTA 与卡应用生命周期
卡应用装上去之后要更新怎么办?总不能把用户的卡收回来。答案是OTA(空中下载),通过短信通道(或 BIP 通道)把更新包下发到卡里,卡内的引导程序负责安装。这套机制很成熟,但实际操作中有几个坑:
一是下发成功率。用户关机、信号差、卡被拔出,都会导致下发失败。要做重试队列,通常尝试三到五次,跨越几天。二是版本兼容。不同批次、不同厂商的卡,引导程序版本不同,更新包要分版本打。三是回滚。更新包万一有 bug 把卡搞挂了,用户连电话都打不了,这是重大事故。所以必须做 A/B 分区和失败自动回滚。
我在一个项目里遇到过更新包把卡内菜单搞乱的情况,用户点进去一片空白。最后是靠 OTA 下发一个修复包解决的,但那两天客服电话被打爆。从此之后我们的规矩是:任何卡应用更新,先在小批量测试卡上跑满一周,再灰度 1%,观察两周,才全量。
4. 钱包平台的系统骨架:接入、账务与对账
讲完通道层,往上走一层看系统架构。通信通道的钱包平台,骨架其实和互联网支付平台高度相似,差异主要在接入层和风控层。我用一张分层表把职责说清楚:
| 层次 | 主要职责 | 关键设计点 |
|---|---|---|
| 接入层 | 承接 USSD、短信、App、代理商终端等多通道 | 通道抽象、统一报文、会话管理 |
| 业务层 | 转账、充值、提现、缴费等用例编排 | 幂等、状态机、限额 |
| 账务核心 | 记账、余额、试算平衡 | 复式记账、事务、并发控制 |
| 清结算 | 与运营商、银行、代理商的资金划拨 | 对账、差错处理、T+1 结算 |
| 风控 | 反欺诈、反洗钱、限额 | 规则引擎、名单、行为模型 |
4.1 接入层的多通道抽象
接入层的核心设计原则是:业务层不应该知道交易是从哪个通道来的。USSD、短信、App、代理商 POS,最终都要被归一化成同一种内部请求格式,包含操作类型、金额、币种、付款方、收款方、幂等键、通道标识。
为什么要做这层抽象?因为通道会一直变。今天是 USSD,明天加 App,后天加扫码,如果不抽象,每加一个通道就要改一遍业务代码,改一次错一次。我习惯的做法是定义一个ChannelAdapter接口,每个通道实现三个方法:解析入站报文、渲染出站响应、处理异步回调。业务层只依赖接口。
这里有个容易被忽略的点:不同通道的能力不一样。USSD 能实时交互但有时限,短信单向无交互,App 能做富交互。所以接口设计里要允许"能力协商"——比如 USSD 通道不支持传图片,那就返回纯文本;App 通道可以返回结构化卡片。做法是在适配器里声明能力集,业务层根据能力集选择响应形态。
4.2 账务核心与幂等设计
账务这块,一句话原则:不要在余额字段上做加减,要做复式记账。每一笔交易生成两条分录(借和贷),余额由分录汇总得出(或由分录驱动更新,但必须可回溯)。这样做的好处是任何时刻都能"从流水重算余额",出了差错能定位到具体哪一笔,而不是面对一个对不上的数字发呆。
幂等的实现我之前提过一次,这里展开说。通信通道的重复请求来源特别多:USSD 网络重传、用户连按、短信网关重复投递、超时后用户重试。做法是给每笔交易分配一个业务幂等键,规则是:
- USSD 交易:
sessionId + 步骤号 + 操作类型 - 短信交易:
msisdn + 短信中心流水号 - App 交易:客户端生成的
requestId
幂等键写入一张唯一索引表,插入成功才继续处理,插入冲突直接返回上次的结果。注意要返回上次的结果而不是报错,否则用户重试还是失败。
并发控制方面,同一个账户的余额变更必须串行化。我的做法是按账户 ID 哈希分片加锁,锁粒度到账户级,避免全表锁。如果交易量再大,就用"余额预扣 + 异步落账"的两阶段模式。
4.3 代理网络与现金进出
这是通信通道钱包最"土"也最关键的一块。前面说过,用户的钱是现金,要变成电子余额,需要代理商收现金、系统加余额;反过来提现,用户找代理商,系统扣余额、代理商付现金。
系统上要做三件事:代理商管理(层级、额度、佣金)、现金流水(每笔充提都要有代理商维度的记录)、流动性管理(代理商手里的现金和电子余额都要够,不然做不了业务)。我见过代理商因为当天收的现金太多、没及时存银行,导致第二天没钱给用户提现,业务直接停摆。所以后台要能看到每个代理商的实时头寸,低于阈值就预警。
佣金结算一般是 T+1 或按周,要防止代理商刷单。常见作弊手法是"自己给自己充值再提现"刷佣金,防法是加规则:同一身份证下的多个号码之间的充提不计佣金、单日佣金上限、异常模式识别。
5. 从零搭一个可跑通的 USSD 钱包 Demo
理论讲完,来点能动手的。这一章我带你搭一个最小可用的 USSD 钱包骨架:模拟网关、会话状态机、账务记账、完整交易链路。代码用 Python 写,跑在本地就行,不需要真的接运营商网络。
5.1 会话状态机的实现
先定义状态。菜单树本质是一个有限状态机,每个状态对应一屏内容,用户输入触发状态迁移。我用一个字典描述整棵树,方便以后改文案不用动代码:
MENU = { "root": { "text": "欢迎使用钱包\n1.查询余额\n2.转账\n3.充值\n0.退出", "next": {"1": "balance", "2": "transfer_target", "3": "topup_amount", "0": None}, }, "balance": { "text": "您的余额为 {balance} 元\n0.返回首页", "next": {"0": "root"}, }, "transfer_target": { "text": "请输入对方手机号", "input": "target", "next": {"*": "transfer_amount"}, }, "transfer_amount": { "text": "请输入转账金额,当前收款人 {target}", "input": "amount", "next": {"*": "transfer_confirm"}, }, "transfer_confirm": { "text": "确认向 {target} 转账 {amount} 元?\n1.确认 2.取消", "next": {"1": "transfer_done", "2": "root"}, }, "transfer_done": { "text": "转账已提交,流水号 {trace}\n结果将短信通知", "next": {}, }, }这段结构里input字段表示该状态需要收集用户自由输入,next的键是用户输入的分支。*是一个占位符,表示"任意有效输入"。实际项目里还会有校验函数、错误提示分支,这里为了可读性省略了。
会话存储用 Redis 或者本地字典都行,Demo 阶段我用内存字典,key 是 sessionId:
SESSIONS = {} def handle(session_id, msisdn, ussd_string, is_new): if is_new: SESSIONS[session_id] = {"state": "root", "ctx": {}, "msisdn": msisdn} sess = SESSIONS.get(session_id) if not sess: return "END", "会话已过期,请重新拨入" node = MENU[sess["state"]] raw = ussd_string.strip() if not is_new and "input" in node: sess["ctx"][node["input"]] = raw key = "*" else: key = raw.lstrip("*").split("*")[-1] if raw.startswith("*") else raw nxt = node["next"].get(key) if nxt is None: SESSIONS.pop(session_id, None) return "END", node["text"].format(**sess["ctx"], balance=get_balance(msisdn)) sess["state"] = nxt return "CON", render(MENU[nxt]["text"], sess["ctx"], msisdn)这里的render负责把{balance}、{target}这类占位符填上。注意balance不是存在会话里的,而是每次现查,因为余额随时会变,缓存住就是事故。
5.2 报文与状态表设计
真实对接时,网关会给你一个 HTTP 接口,你返回的内容就是屏幕上的字。所以上面handle函数的返回值(动作 + 文本)直接对应网关协议里的CON/END。
状态表要落库,方便排障。我建议至少记这几列:
| 字段 | 说明 |
|---|---|
| session_id | 会话唯一标识 |
| msisdn | 用户号码 |
| current_state | 当前菜单状态 |
| ctx_json | 已收集的上下文(JSON) |
| created_at / updated_at | 创建与更新时间 |
| release_reason | 结束原因(正常、超时、错误) |
有了这张表,用户投诉"我操作到一半消失了",你能立刻查出是超时还是异常,不用靠猜。这个表也是复盘菜单设计的数据来源——哪个状态停留时间最长、哪个状态退出率最高,一目了然。
5.3 交易链路串联与测试
最后一环是把菜单动作接到账务上。转账的完整链路是:会话确认 → 生成幂等键 → 校验(余额、限额、对方号码有效性)→ 记账(借付款方、贷收款方)→ 返回流水号 → 异步发短信通知。
校验部分我列几个必须要做的:
- 号码格式校验,且不能转给自己
- 金额必须为正数、最多两位小数、不超过单笔限额
- 余额必须足够,且考虑在途交易(已提交未落账的要冻结)
- 日累计限额、月累计限额
- 收款方账户状态正常(未冻结、未注销)
记账用事务包起来,两条分录加一条流水,要么全成要么全败。幂等键在事务开始前插入,冲突就返回原结果。
测试阶段我强烈建议做两件事:一是故障注入,故意让账务系统慢 15 秒,看会话超时后的表现是不是符合预期;二是重复投递测试,把同一个确认请求连发五次,看是不是只扣一次钱。这两个测试能抓出 80% 的上线事故。
6. 踩坑实录:字符集、超时、重复扣款与欺诈
这章是我这些年攒下来的"血泪账",都是文档里不会写、但一定会遇到的问题。按发生频率排序。
6.1 字符集与编码的坑
USSD 和短信在 GSM 体系里默认用的是GSM 7-bit 默认字母表,这个表里只有 128 个字符,包含基本拉丁字母、数字、常用符号,不含中文。你要显示中文,就得用 UCS2 编码,容量从 160 字符掉到 70 字符。这个差异导致两个后果:一是菜单文案的实际可用长度比你以为的短一半;二是如果网关配置和业务系统不一致,用户会看到一堆问号或者乱码。
我遇到过最诡异的一次:同一段文案,在 A 运营商网络上正常,在 B 运营商网络上中文全变成方框。排查了两天才发现是 B 的网关默认按 7-bit 转发,没有做编码标识。解决办法是在报文里显式声明编码,并且把文案做降级——如果检测到不支持中文,自动切英文。
还有一个坑是特殊符号。用户输入金额时可能输全角数字、可能输逗号、可能输空格。别指望前端拦,UI 就一个文本框,什么都可能进来。一定要在服务端做归一化:全角转半角、去掉千分位、只保留数字和小数点。
6.2 重复提交与幂等
前面提过幂等设计,这里讲两个真实场景。场景一:用户输完金额,屏幕卡住没反应,他以为没成功,挂断重拨,又操作了一遍。这时候 sessionId 变了,你的幂等键如果只按 sessionId 生成,就拦不住,用户被扣两次钱。所以幂等键要加入"业务维度"——同一付款方、同一收款方、同一金额、五分钟内视为重复。
场景二:短信通道重复投递。短信中心的存储转发机制在网络拥塞时会重试,同一条上行短信可能到达两次。这时候靠短信中心流水号(如果网关透传了)做幂等;没透传就只能靠"号码 + 内容 + 时间窗"。
提示:幂等不是"防重复请求",是"重复请求返回同样结果"。用户重试时期望看到的是"你的转账已成功,流水号 XXX",而不是"重复请求"这种冷冰冰的报错。
6.3 欺诈面与风控
通信通道的钱包有几个特有的攻击面:
SIM 卡克隆与补卡攻击。攻击者冒充用户去营业厅补卡,拿到新卡后就能收短信、接 USSD 回调,直接接管账户。防法是补卡后强制冷静期(比如 24 小时内禁止大额交易)、补卡事件实时同步到风控、大额交易要求二次验证(比如输入钱包 PIN 或回答预设问题)。
短信伪造与拦截。短信本身没有强认证,伪造发送方号码在技术上是有可能的。所以绝对不能把短信作为唯一的交易确认手段。交易确认必须在服务端完成(USSD 会话里确认,或 App 里确认),短信只做通知。
社工诈骗。冒充客服打电话,让用户拨某个短码、输某个"验证码"。这类攻击技术手段防不住,只能靠产品设计:任何合法流程都不应该让用户把 PIN 告诉别人;短码只从官方渠道公布;转账确认页要显示收款方姓名(脱敏),让用户有机会发现异常。
风控规则我一般按三层配:静态名单(黑名单、涉案号码)、动态限额(新户低限额,用久了提额)、行为模型(异地、深夜、高频、整数金额)。规则引擎要能热更新,出了新手法当天就能加规则,不用等发版。
7. 通信通道钱包的边界与演进
写到这儿,可能有人会问:这套东西现在还有意义吗?智能机都普及了。我的看法是,它的价值不在于替代 App,而在于覆盖 App 覆盖不到的那部分场景和人。
三类场景它依然是最优解。第一类是极端低端设备和极端网络环境,没数据、没电、信号只有两格的地方,USSD 还能用。第二类是应急通道,App 挂了、网络故障、用户在国外没开数据,USSD 是最后的兜底。第三类是特定人群的强需求,比如不识字或识字有限的用户,纯数字菜单比图形界面更友好。
从我参与过的项目看,成熟的方案基本都是混合架构:App 做主力、USSD 做兜底、短信做通知、代理商网络做现金出入口。四者共用一套账务核心和风控,通道只是入口。这个架构的好处是,新通道进来不用重做业务,老通道也不用因为新通道而废弃。
至于 USSD 本身的技术演进,方向主要在两端:一端是网关侧的协议现代化,很多厂商已经把老式的私有协议换成了标准 HTTP/JSON,对接成本大幅降低;另一端是和即时通讯类通道的融合,把菜单交互搬到更现代的载体上。但底层那套"手机号即身份、信令即通道、代理即网点"的逻辑,短期内不会变,因为它解决的是基础设施问题,不是交互体验问题。
最后分享一个我自己的判断标准:评估一个市场要不要上通信通道钱包,别只看智能机渗透率,要看三个数——数据资费占收入比例、银行卡持有率、线下充值网点密度。这三个数决定的是用户"能不能用得起、能不能绑得上、能不能充得进"。前两个高、后一个低的地方,USSD 钱包的生命周期会比你想的长得多。我在实际项目里用这个标准做过几次取舍,比拍脑袋靠谱。