ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

三网合一话费余额查询API系统设计与实现:适配器模式与缓存架构

2026/9/20 18:52:55 拓冰建站 浏览量
三网合一话费余额查询API系统设计与实现:适配器模式与缓存架构 简介这套三网合一话费余额查询 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系统可以直接照着改。这套系统解决的最核心痛点就是移动、联通、电信三家的接口协议完全不同有的走HTTPXML有的走HTTPSJSON签名方式也五花八门如果上层业务直接对接三家原始接口维护成本会爆炸。所以三网合一API的价值就在中间层做统一适配对外提供一套标准接口对内管理三套渠道差异上层业务只需要调一个接口就能完成所有号段的话费余额查询。无论你是要给代理商做分销后台还是给自己的CRM系统加一个话费查询功能或者准备做一个面向企业内部的话费管理平台这套系统的架构思路和核心代码都值得参考。1. 项目整体设计与技术选型思路1.1 三网合一API要解决的核心问题先说说三网合一这个合字到底合的是什么。第一层是接口协议的统一三家运营商的余额查询接口有的需要数字证书有的要用MD5签名有的直接POST表单返回格式也分别是XML、JSON、纯文本混着来。第二层是数据口径的统一移动返回的余额精度、联通返回的欠费标识、电信返回的状态码都需要转成一套业务层能直接识别的标准结构。第三层是渠道容灾的统一某一家运营商接口抖动的时候系统要有降级策略不能因为移动接口超时导致整个查询服务不可用。我当时设计这套系统时最优先考虑的就是把这三点做到位。技术栈上我选了PHP 8.1 Swoole数据库用MySQL缓存用Redis。选PHP而不是Java或Go原因很直接第一这套系统后续一般要接各种分销系统的APIPHP的生态对这种中小型API服务的支撑最方便第二Swoole常驻内存跑起来后性能完全够用第三团队维护成本低后续交接容易。1.2 架构分层与核心模块划分整个系统分了四层接入层、业务层、适配层、数据层。接入层负责处理HTTP请求、参数校验、签名校验、IP白名单校验和频率限制。业务层负责查号段、匹配运营商、组装请求参数、解析响应、写日志。适配层是最核心的一层三个运营商各写一个适配器类都实现同一个接口用简单工厂模式按运营商编码分发。数据层就是MySQL和RedisMySQL存用户、密钥、日志、渠道配置Redis存号段缓存、余额查询结果缓存和计数器。这样的分层带来的直接好处是新增一个运营商渠道时只需要写一个新的适配器类上层业务代码一行都不用改。我当时把适配器接口设计成了三个方法buildRequest、parseResponse、queryBalance。2. 核心业务流程与数据库表设计2.1 一次余额查询的完整链路一次查询请求从进来到最后返回完整的路径是这样的。客户端带上app_id、timestamp、nonce、mobile、sign五个参数请求/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_cache1没有才返回超时错误。运营商返回原始报文后适配器做解析和字段映射统一输出一个标准结构mobile、operator_code、operator_name、balance、status、raw_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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENTAPI接入方表;第二张是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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT运营商渠道配置表;第三张是mobile_segment号段表这个表数据一般有几千行需要覆盖三大运营商的所有号段。第四张是query_log查询流水表记录每次查询的完整链路耗时、返回码和原始报文排查问题全靠它。3. API接口规范与安全防护要点3.1 接口协议与参数设计对外接口我统一走HTTPS POST数据格式为JSON。请求参数设计如下参数名类型必填说明app_idstring是接入方唯一标识mobilestring是11位手机号timestampstring是毫秒级时间戳用于防重放noncestring是随机字符串每次请求唯一signstring是签名值MD5加密签名的生成规则是将app_id、mobile、timestamp、nonce四个参数按字典序拼接成字符串再把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_id加minute作为键自增后判断是否超过阈值。超过直接返回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位。比如1391、1392这种通过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的单机方案前期流量不大完全扛得住。部署步骤如下服务器装好PHP 8.1、Nginx、MySQL 8.0PHP安装redis、swoole扩展MySQL表结构按上面SQL创建。项目代码放到/data/www/balance-api用Composer安装依赖。配置Nginx站点把/api开头的请求转发到PHP-FPM开启HTTPS证书。修改.env配置文件填入数据库连接信息、Redis连接信息、各渠道密钥。运行号段导入脚本把mobile_segment.sql灌入数据库。用php artisan migrate或手工导入表结构后启动队列处理器用于异步写日志。创建第一个api_user接入方账号生成app_id和app_secret。用Postman或curl测试签名和查询流程确认正常后接入监控告警。这里提醒一下上线前一定要做一次全量号段扫描把138、139、150、151这类老号段和190、192、193这类新号段都检查一遍避免出现号段没匹配上返回未知运营商这种尴尬问题。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里的原始报文导出存档。没有原始报文做依据排查问题基本靠猜。本文还有配套的精品资源点击获取