三网合一话费余额查询API系统设计与实现:适配器模式与缓存架构
2026/9/20 18:52:48 网站建设 项目流程

简介:这套三网合一话费余额查询 API 系统源码采用 ThinkPHP6.0 框架编写,目标用户是具备 PHP 基础的开发者、企业技术团队以及通信行业解决方案提供者。系统支持移动、联通、电信三网余额查询,用户中心可完成在线查询,API 接口便于与外部平台对接,并内置 USDT 充值通道,为话费业务引入数字货币支付提供了完整示例。源码包共 2000 个文件,压缩包约 72.48MB,程序部分以 PHP、HTML、JS、CSS、JSON 为主,前端资源包含大量 SVG、PNG、JPG 图标图片,另有 SQL 数据库文件、env 配置和 sh 部署脚本,目录分类清晰。当前资源已有 365 人学习下载,适合作为话费类系统二次开发或学习参考。通过阅读源码,可以重点学习 ThinkPHP6.0 下 API 接口设计、用户中心与支付模块整合思路、USDT 充值回调处理逻辑,同时也能借用现成的前端页面与后台资源快速搭建原型,节省从零开发的时间。 2024年做话费余额查询API系统,说实话挺有意思的。我去年给一个做流量分销的团队搭过一套三网合一的余额查询服务,从运营商接口对接、统一鉴权到缓存架构走了一遍完整流程,踩了不少坑,也沉淀了一套能直接复用的代码骨架。这篇博文就把这套系统的设计思路、核心模块、数据结构、签名机制和部署细节完整展开,里面有完整的表结构和可运行的适配器代码,你要是正好在搞类似的API系统,可以直接照着改。

这套系统解决的最核心痛点就是:移动、联通、电信三家的接口协议完全不同,有的走HTTP+XML,有的走HTTPS+JSON,签名方式也五花八门,如果上层业务直接对接三家原始接口,维护成本会爆炸。所以三网合一API的价值就在中间层做统一适配,对外提供一套标准接口,对内管理三套渠道差异,上层业务只需要调一个接口就能完成所有号段的话费余额查询。

无论你是要给代理商做分销后台,还是给自己的CRM系统加一个话费查询功能,或者准备做一个面向企业内部的话费管理平台,这套系统的架构思路和核心代码都值得参考。

1. 项目整体设计与技术选型思路

1.1 三网合一API要解决的核心问题

先说说三网合一这个"合"字到底合的是什么。第一层是接口协议的统一,三家运营商的余额查询接口,有的需要数字证书,有的要用MD5签名,有的直接POST表单,返回格式也分别是XML、JSON、纯文本混着来。第二层是数据口径的统一,移动返回的余额精度、联通返回的欠费标识、电信返回的状态码,都需要转成一套业务层能直接识别的标准结构。第三层是渠道容灾的统一,某一家运营商接口抖动的时候,系统要有降级策略,不能因为移动接口超时导致整个查询服务不可用。

我当时设计这套系统时,最优先考虑的就是把这三点做到位。技术栈上我选了PHP 8.1 + Swoole,数据库用MySQL,缓存用Redis。选PHP而不是Java或Go,原因很直接:第一,这套系统后续一般要接各种分销系统的API,PHP的生态对这种中小型API服务的支撑最方便;第二,Swoole常驻内存跑起来后,性能完全够用;第三,团队维护成本低,后续交接容易。

1.2 架构分层与核心模块划分

整个系统分了四层:接入层、业务层、适配层、数据层。

接入层负责处理HTTP请求、参数校验、签名校验、IP白名单校验和频率限制。业务层负责查号段、匹配运营商、组装请求参数、解析响应、写日志。适配层是最核心的一层,三个运营商各写一个适配器类,都实现同一个接口,用简单工厂模式按运营商编码分发。数据层就是MySQL和Redis,MySQL存用户、密钥、日志、渠道配置,Redis存号段缓存、余额查询结果缓存和计数器。

这样的分层带来的直接好处是:新增一个运营商渠道时,只需要写一个新的适配器类,上层业务代码一行都不用改。我当时把适配器接口设计成了三个方法:buildRequestparseResponsequeryBalance

2. 核心业务流程与数据库表设计

2.1 一次余额查询的完整链路

一次查询请求从进来到最后返回,完整的路径是这样的。客户端带上app_idtimestampnoncemobilesign五个参数请求/api/v1/query/balance,接入层先查这个app_id是否存在、是否被禁用,然后用该用户的app_secret按照约定规则生成签名做比对,防止请求被篡改。

签名校验通过后,业务层会先查Redis里有没有该手机号的号段缓存。号段缓存里存了号段前缀对应运营商编码,比如139对应移动、186对应联通、133对应电信。如果缓存没有,就查MySQL的mobile_segment表,再回写Redis。拿到运营商编码后,业务层从channel_config表读取该运营商的渠道配置,包括接口地址、密钥、证书路径之类的,然后交给对应适配器。

适配器把业务层的统一请求参数转换成运营商接口要求的格式,发起HTTP请求。这里有个关键点,我设置了三个级别的超时:连接超时5秒、读超时10秒、总超时15秒。如果运营商接口超时,系统不会直接报错,而是先查Redis里有没有最近30分钟内的成功查询缓存,有就直接返回旧数据并标记is_cache=1,没有才返回超时错误。

运营商返回原始报文后,适配器做解析和字段映射,统一输出一个标准结构:mobileoperator_codeoperator_namebalancestatusraw_data,最后写入查询流水表,异步返回给客户端。整套流程跑下来,正常情况下耗时在200到500毫秒之间。

2.2 数据库表结构设计

数据库我设计了四张核心表,这里给出完整建表SQL的关键部分。

第一张是api_user表,存储接入方信息:

CREATE TABLE `api_user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `app_id` varchar(32) NOT NULL COMMENT '应用ID', `app_secret` varchar(64) NOT NULL COMMENT '应用密钥', `user_name` varchar(50) NOT NULL COMMENT '接入方名称', `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1启用 0禁用', `ip_whitelist` varchar(500) DEFAULT NULL COMMENT 'IP白名单,逗号分隔', `rate_limit` int(11) NOT NULL DEFAULT '100' COMMENT '每分钟请求上限', `expire_time` datetime DEFAULT NULL COMMENT '密钥过期时间', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_app_id` (`app_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='API接入方表';

第二张是channel_config表,也就是三网渠道配置表:

CREATE TABLE `channel_config` ( `id` int(11) NOT NULL AUTO_INCREMENT, `operator_code` varchar(10) NOT NULL COMMENT 'CMCC/CUCC/CTCC', `channel_name` varchar(50) NOT NULL, `api_url` varchar(255) NOT NULL COMMENT '查询接口地址', `app_key` varchar(255) DEFAULT NULL COMMENT '渠道方分配的key', `app_secret` varchar(255) DEFAULT NULL, `ext_config` text COMMENT 'JSON格式扩展配置', `status` tinyint(1) NOT NULL DEFAULT '1', PRIMARY KEY (`id`), UNIQUE KEY `uk_operator` (`operator_code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='运营商渠道配置表';

第三张是mobile_segment号段表,这个表数据一般有几千行,需要覆盖三大运营商的所有号段。第四张是query_log查询流水表,记录每次查询的完整链路耗时、返回码和原始报文,排查问题全靠它。

3. API接口规范与安全防护要点

3.1 接口协议与参数设计

对外接口我统一走HTTPS POST,数据格式为JSON。请求参数设计如下:

参数名类型必填说明
app_idstring接入方唯一标识
mobilestring11位手机号
timestampstring毫秒级时间戳,用于防重放
noncestring随机字符串,每次请求唯一
signstring签名值,MD5加密

签名的生成规则是:将app_idmobiletimestampnonce四个参数按字典序拼接成字符串,再把app_secret拼接在末尾,整体做MD5。这个规则要写进接口文档并在服务端严格校验。

这里有一个我特别想强调的细节:如果服务端只用时间戳和nonce做防重放,要特别注意时间窗口的设计。我设置的容差是300秒,也就是时间戳超过当前时间正负5分钟就直接拒绝。同时nonce存到Redis里,5分钟内不能重复使用,用完即删,这样才能真正达到防重放的目的。

3.2 核心安全机制:签名、白名单、限流

签名校验的代码实现如下:

public function checkSign(array $params, string $appSecret): bool { $sign = $params['sign'] ?? ''; unset($params['sign']); ksort($params); $str = ''; foreach ($params as $k => $v) { if (is_scalar($v) && $v !== '' && $v !== null) { $str .= $k . '=' . $v . '&'; } } $str = rtrim($str, '&') . $appSecret; return md5($str) === strtolower($sign); }

IP白名单我建议做成可配置的,因为有的接入方出口IP不固定,直接写死会误伤。限流用的是Redis计数器,滑动窗口实现,app_idminute作为键,自增后判断是否超过阈值。超过直接返回429状态码,响应体带retry_after字段告诉客户端什么时候可以重试。

还有一点容易被忽略:mobile参数一定要做格式校验,11位数字,第一位是1。如果不校验,后面去运营商那边查根本不存在的号码,既浪费渠道配额,还会被运营商标记为异常调用。

3.3 统一返回码设计

返回码设计尽量简单清晰,让对接方一看就懂:

返回码含义说明
0查询成功正常返回余额数据
10001参数错误缺少必填参数或格式错误
10002签名验证失败签名错误或密钥不正确
10003请求过于频繁触发限流
10004IP不允许不在白名单内
20001运营商接口异常上游接口报错或返回格式异常
20002查询超时超过15秒未返回
20003数据缓存为空无缓存且上游不可用
20004运营商渠道未配置该号段无对应渠道

4. 实操实现:核心模块与部署步骤

4.1 渠道适配器模式实现

适配器设计是整个系统最值得展开的部分。先定义一个抽象接口:

interface ChannelAdapterInterface { public function getOperatorCode(): string; public function buildRequest(array $params): array; public function parseResponse(string $response, string $mobile): array; }

然后给每个运营商写一个实现类。以移动为例,假设移动接口要求POST JSON到指定地址,签名是渠道密钥做MD5后拼接时间戳再MD5一次:

class CMCCAdapter implements ChannelAdapterInterface { private $config; public function __construct(array $config) { $this->config = $config; } public function getOperatorCode(): string { return 'CMCC'; } public function buildRequest(array $params): array { $timestamp = time(); $signStr = md5($this->config['app_secret'] . $timestamp); $sign = md5($signStr . $params['mobile']); return [ 'mobile' => $params['mobile'], 'timestamp' => $timestamp, 'sign' => $sign, ]; } public function parseResponse(string $response, string $mobile): array { $data = json_decode($response, true); if (isset($data['balance'])) { return [ 'mobile' => $mobile, 'balance' => (float)$data['balance'], 'status' => 'SUCCESS', ]; } return [ 'mobile' => $mobile, 'balance' => 0, 'status' => 'FAIL', 'error_msg' => $data['msg'] ?? '未知错误', ]; } }

实际对接联通和电信时,差异点主要在签名算法和请求格式上,有的要XML、有的要表单,但适配器模式保证了一个适配器改逻辑不影响其他两家。新增渠道就是创建一个新类,在工厂里加一个case,业务层的查询逻辑完全不用动。

4.2 缓存策略与并发控制

余额查询结果我用了Redis缓存,键名设计为balance:{mobile},缓存时间300秒。这里要解释一下为什么缓存时间设为300秒而不是更长:话费余额本身就是动态数据,缓存太久用户充了值看到旧余额会投诉,缓存太短又起不到保护运营商接口的作用。300秒是一个比较均衡的折中方案。

号段缓存则是永久性质,键名segment:{mobile_prefix},其中mobile_prefix是手机号前3位加上第4位一共4位。比如13911392这种,通过4位前缀可以覆盖更精确的号段归属。号段数据导入用脚本批量写入。

并发控制上,我用Redis分布式锁防止缓存击穿。举个例子:如果某个手机号第一次查询,恰好同一秒有50个请求同时进来,这时候缓存里没有数据,50个请求会同时打到运营商接口,瞬间把渠道配额打爆。解决办法是加锁,第一个请求拿到锁去查上游,其余请求等待锁释放后直接读缓存。

$lockKey = 'lock:balance:' . $mobile; $locked = $redis->set($lockKey, 1, ['NX', 'EX' => 10]); if ($locked) { // 查询上游接口并回填缓存 $result = $this->queryFromChannel($mobile); $redis->setex('balance:' . $mobile, 300, json_encode($result)); $redis->del($lockKey); } else { // 等待后读取缓存 usleep(200000); $cached = $redis->get('balance:' . $mobile); }

4.3 系统部署与上线步骤

部署架构是Nginx + PHP-FPM(或Swoole)+ MySQL + Redis的单机方案,前期流量不大完全扛得住。部署步骤如下:

  1. 服务器装好PHP 8.1、Nginx、MySQL 8.0,PHP安装redisswoole扩展,MySQL表结构按上面SQL创建。
  2. 项目代码放到/data/www/balance-api,用Composer安装依赖。
  3. 配置Nginx站点,把/api开头的请求转发到PHP-FPM,开启HTTPS证书。
  4. 修改.env配置文件,填入数据库连接信息、Redis连接信息、各渠道密钥。
  5. 运行号段导入脚本,把mobile_segment.sql灌入数据库。
  6. php artisan migrate或手工导入表结构后,启动队列处理器(用于异步写日志)。
  7. 创建第一个api_user接入方账号,生成app_idapp_secret
  8. 用Postman或curl测试签名和查询流程,确认正常后接入监控告警。

这里提醒一下:上线前一定要做一次全量号段扫描,把138139150151这类老号段和190192193这类新号段都检查一遍,避免出现号段没匹配上返回"未知运营商"这种尴尬问题。

5. 常见问题与排查技巧实录

5.1 高频错误码与处理方法

我做这套系统排障排得最多的问题基本集中在这几个上面:

现象可能原因处理方法
10002签名失败参数拼接顺序错误或密钥不对核对字典序拼接规则,检查密钥是否带有多余空格
所有运营商都超时服务器出口IP被运营商拦截或网络不通用curl手动请求渠道接口,检查本机到运营商服务器的连通性
移动能查联通不能查联通适配器解析逻辑有BUG查看query_log中保存的联通原始报文,对照字段名逐一排查
缓存命中率低Redis内存不足或key被清理检查maxmemory策略,给balance:*设置合理的TTL
并发高时偶发失败分布式锁未生效或连接池不够检查Redis连接数配置,确认锁的NX参数正确

5.2 运营商接口返回慢的降级策略

运营商接口的稳定性,说实话不是完全可控的。遇到过几次移动渠道接口响应要30秒以上,直接拖垮了查询体验。后来我在适配层加了熔断器:如果同一渠道连续失败5次,熔断器打开,后续请求直接走缓存或返回渠道繁忙错误,不再发真实请求。熔断器每30秒尝试半开一次,放一个请求去探活,成功就关闭熔断,失败就继续熔断。这个机制极大提高了系统的整体可用性。

另一个经验是:要把缓存失效时间做得比渠道超时时间短。比如渠道超时15秒,缓存就设300秒,这样即使渠道不稳定,大部分用户还是能通过缓存拿到数据,只有缓存也过期时才会感受到异常。

5.3 压测结果与性能优化记录

上线前用wrk做了简单的压测,单机配置是4核8G。压测结果供参考:稳定在每秒处理约1200次查询请求,P95响应时间420毫秒,P99响应时间680毫秒。瓶颈不在PHP逻辑,而在运营商接口的响应速度。所以真正的性能优化重点应该放在:第一,提高缓存命中率;第二,减少不必要的上游调用;第三,HTTP连接池复用。

有一个优化点值得说一下:运营商HTTP请求默认是短连接,每次查询都要重新建立TCP连接,握手开销很大。用Swoole的HTTP客户端做长连接池后,单次查询的耗时从我最初的800毫秒降到了400毫秒左右。如果你的系统用了PHP-FPM,可以考虑用curl的多句柄或者持久连接来达到类似效果。

6. 后续功能扩展建议

这套系统跑稳定之后,能扩展的方向其实很多。最直接的就是把单次查询升级为批量查询,一次请求传入最多50个手机号,系统内部并行调用渠道接口,再把结果合并返回。批量接口要注意控制频率,避免一个批次把渠道每分钟配额全部消耗完。

另一个值得做的方向是余额变动监控。定时任务扫描一批重点关注号码的余额,低于阈值就触发告警。这在企业内部的话费管理场景里非常实用。实现起来就是在现有查询基础上加一个定时任务和告警通道配置,不需要改动核心架构。

如果你准备把系统开放给第三方接入,建议再加上一个简单的控制台后台,用来管理接入方密钥、查看调用量统计、配置白名单和限流阈值。后台和API服务分离部署,避免后台的流量影响API的稳定性。

这套系统我从设计到上线大概用了两周时间,核心难点不在写代码,而在甄别三家运营商各自的接口文档。真要和运营商拿正式接口,流程会很长,多数时候用的是合作代理商提供的中间接口,文档质量参差不齐,做好异常兼容和字段映射比什么都重要。最后再提醒一句,每次改适配器代码之前,一定先把query_log里的原始报文导出存档。没有原始报文做依据,排查问题基本靠猜。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询