
简介一套聚焦油卡与三网话费充值场景的系统源码面向具备PHP基础的中高级开发者可用于快速搭建或二次开发线上充值平台覆盖中国移动、联通、电信三大运营商的话费充值与油卡充值业务可应用于独立直充站点或分销系统等场景。压缩包70.48MB共2000个文件以php源码为后端核心搭配html、js、css构建H5前端界面sql脚本用于数据库初始化md文档提供使用说明另有json/xml配置、日志及测试文件便于排错与二次开发。目前已有861人学习下载。完整源码中展示了前后端分离架构、支付接口集成、异步充值任务处理、HTTPS加密与防注入等安全措施、异常捕获与日志记录等关键实现同时包含composer依赖、单元测试等工程化配置适合作为真实充值系统的研究样本也可直接作为毕业设计或商业项目的起点。1. 充值系统的核心不在支付而在订单状态机这套三网话费与油卡充值系统源码拆开来看并不是一个「支付工具」而是一套围绕订单生命周期运转的状态机。话费充值要覆盖移动、联通、电信三网油卡充值又要对接不同油企的卡密渠道每个上游返回结果的时间、协议都不一样真正难的部分是把这些不可控的外部依赖统一成一套内部可追踪的订单模型。适合谁想快速搭建在线充值平台的技术团队、做校园或企业集采话费油卡业务的运维人员以及准备对老项目做前后端分离改造的开发者。H5 在这里不只是做响应式页面它决定了用户从扫码到支付完成的整个交互路径是否顺畅。下文从页面、订单、数据库到验证逐层拆。2. H5 充值页到后端预下单前端链路怎么接2.1 充值场景里 H5 不只是自适应布局摘要里把 H5 定位为“响应式前端界面”实际落地时它承担的事比“能自适应”多得多。首先是入口问题用户在微信、支付宝或者普通浏览器里打开充值页前端需要识别环境并决定支付方式——微信内用 JSAPI 拉起支付支付宝内用 alipay 的 H5 协议普通浏览器只能走扫码或网银跳转。其次是弱网体验充值页不能依赖整包加载一个五六 MB 的 SPA常见做法是把页面按视图拆成独立入口核心充值表单用原生 HTML 加轻量脚本实现后台管理端才引用重量级框架。这套源码的前端目录里出现了 bootstrap.min.css、style.css、summernote-bs3.css说明后台管理界面采用 Bootstrap 3 体系Summernote 用于富文本编辑比如配置公告、价格说明页面。业务前端则可以单独维护一套更轻的 H5 页面不做首屏全量加载。给一个最小充值表单的请求链路示例form idrechargeForm input typetext namephone placeholder手机号 maxlength11 / select nameamount option value5050元/option option value100100元/option /select input typehidden namerecharge_type valuephone / button typesubmit立即充值/button /form script document.getElementById(rechargeForm).addEventListener(submit, function (e) { e.preventDefault(); var phone document.querySelector([namephone]).value; var amount document.querySelector([nameamount]).value; var payload { phone: phone, amount: amount, recharge_type: phone, request_id: Date.now().toString(36) Math.random().toString(36).slice(2, 8) }; fetch(/api/recharge/preorder, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify(payload) }).then(function (res) { return res.json(); }).then(function (data) { if (data.code 0) { window.location.href /paypage/ data.order_no; } else { alert(data.message); } }); }); /script这段代码的作用是在表单提交时阻止默认跳转把用户输入的手机号、金额、充值类型组装成 JSON 发给后端预下单接口。重点看request_id字段它是由时间戳加随机串生成的客户端幂等标识避免用户双击按钮或网络重试导致同一笔请求被后端处理两次。后端收到预下单请求后会先校验手机号格式、查询商品价格、创建待支付订单然后返回order_no给前端跳转收银台。这个环节的关键参数就是recharge_type和request_id前者决定走话费还是油卡通道后者决定能否安全地重复提交。2.2 轮询订单状态代替 WebSocket 回调用户完成支付后系统需要告知前端“充值成功还是失败”。由于充值渠道的回调通常是异步的最短也要几秒到几十秒充值页最稳妥的做法不是等一次性响应而是做订单状态轮询。WebSocket 在这种场景性价比不高因为用户可能停留在页面很久长连接占用大量资源而轮询只要控制好频率和退出条件即可。var maxTimes 20; var count 0; function queryOrderStatus(orderNo) { if (count maxTimes) { // 超过轮询上限提示用户稍后查看消息 return; } fetch(/api/order/status?order_no orderNo, { method: GET }) .then(function (res) { return res.json(); }) .then(function (data) { count 1; if (data.code 0) { var status data.data.status; if (status success) { // 跳转到成功页 } else if (status failed) { // 跳转到失败页 } else { setTimeout(function () { queryOrderStatus(orderNo); }, 3000); } } else { setTimeout(function () { queryOrderStatus(orderNo); }, 3000); } }); }这里把轮询间隔设为三秒上限二十次也就是最多观察六十秒。话费充值在正常情况下三十秒内能拿到运营商结果油卡卡密渠道可能需要更久所以超过上限后不能直接判失败而是引导用户到订单列表页或关注后续短信通知。参数maxTimes和轮询间隔要根据上游渠道的平均响应时间调整写死在页面里不灵活可以把这两个值放到后端接口配置里下发前端动态获取。2.3 收银台账单页的降级处理H5 页面在户外弱网环境下很容易出现 fetch 请求超时此时要有一个降级策略本地先渲染“支付处理中”的占位页用navigator.onLine监听网络变化恢复后自动重新同步状态。盲目地在超时后立刻刷新订单状态接口可能造成重复查询但对订单结果没有副作用真正的风险在于用户多次点击“确认支付”导致重复下单所以前端必须把提交按钮置灰并在本地标记request_id已使用。这套源码里的充值页核心改造点就在这个位置保证用户输入一次、提交一次、等待结果不在前端做任何重试充值动作。3. 订单状态机与异步充值链路3.1 为什么必须把订单状态建模成一串状态位上游渠道返回充值结果的时间不可控导致订单不可能由一个同步接口从头执行到尾。常见做法是定义一个内部状态流转表待支付、支付确认、提交上游、充值成功、充值失败、退款处理、退款完成。每一步对应订单表里的status字段值同时关联一张流水表记录每次状态变更。这样做的直接收益是任何一笔订单都能回答“现在到哪一步了”和“之前经历过什么”出了问题可以按流水回放。以 PHP 后端为例订单状态更新最简单的实现是封装一个状态变更函数function changeOrderStatus($orderNo, $newStatus, $remark ) { $pdo getPdo(); $stmt $pdo-prepare( UPDATE orders SET status :status, updated_at :time WHERE order_no :order_no ); $stmt-execute([ :status $newStatus, :time date(Y-m-d H:i:s), :order_no $orderNo ]); // 写入状态变更流水 $stmtLog $pdo-prepare( INSERT INTO order_logs (order_no, from_status, to_status, remark, created_at) VALUES (:order_no, :from_status, :to_status, :remark, :time) ); $stmtLog-execute([ :order_no $orderNo, :from_status previous_status, :to_status $newStatus, :remark $remark, :time date(Y-m-d H:i:s) ]); return true; }代码里的参数说明status字段建议用 tinyint 或 varchar 存状态码不要直接存中文避免索引失效和编码问题order_logs表记录变更前状态、变更后状态和备注失败重试、上游回调延迟、管理员人工介入都以流水形式留痕。实际生产中changeOrderStatus还要加一个乐观锁条件比如WHERE order_no ? AND status ?防止并发请求把状态覆盖回旧值。3.2 异步处理方案crontab 扫描还是消息队列话费充值接口调用通常需要请求上游供应商 HTTP 接口这个过程可能耗时两三秒甚至更久如果放在支付回调里同步执行用户在收银台会等很久而且一旦 PHP-FPM 执行超时请求被中断订单会卡在“已支付但未充值”的中间状态。成熟的方案是把“支付成功”和“提交上游”两个动作解耦。对于日均千级别的充值系统直接用 crontab 加订单状态扫描就够了不需要引入 RabbitMQ 这类重组件。每三十秒执行一次任务把状态为待充值的订单捞出来逐条调用上游接口并更新状态function scanPendingOrders($limit 50) { $pdo getPdo(); $stmt $pdo-prepare( SELECT order_no, recharge_account, amount, channel_id FROM orders WHERE status :status LIMIT :limit ); $stmt-execute([ :status 2, // 待充值 :limit $limit ]); $orders $stmt-fetchAll(); foreach ($orders as $order) { $result submitToUpstream($order); if ($result[code] 0) { changeOrderStatus($order[order_no], 3, 提交上游成功); } else { changeOrderStatus($order[order_no], 4, 提交上游失败 . $result[msg]); } } }扫描任务有两个关键参数limit和任务执行频率。limit设得太小订单积压时充值速度跟不上设得太大单个进程阻塞时间过长可能影响其他任务。执行频率决定充值的时效性话费场景三十秒一次基本无感油卡卡密渠道如果涉及面值批次可以放宽到一分钟一次。选择 crontab 方案的前提是保证同一时间只有一个扫描任务在跑通常在入口加一个进程锁文件防止重复执行导致同一订单被同时提交两次。3.3 上游接口对接的签名与回调校验对接三网话费上游时供应商一般要求请求参数按字典序排序后拼接密钥做签名。这是一个高频踩坑点排序算法不一致、签名串编码不一致、时间戳偏差过大都会导致鉴权失败。以常见的 MD5 签名方式为例function buildSign($params, $secretKey) { ksort($params); $str ; foreach ($params as $key $value) { if ($value ! $value ! null) { $str . $key . . $value . ; } } $str . key . $secretKey; return strtoupper(md5($str)); }该函数的逻辑是把参数按 key 升序排列排除空值和 null拼接成a1b2key密钥的形式后计算 MD5 并转大写。需要注意ksort默认按字符串排序与供应商文档里“按参数名 ASCII 升序”一致部分渠道要求每个参数的值不进行 URL 编码而另一些要求先编码再拼接这必须对照具体接口文档测试。回调校验是反向操作收到上游回调后去掉签名本身对剩余参数重复同样的拼接和计算比对结果是否相等同时校验金额是否与本地订单一致防止伪造回调把失败订单改成成功。本文还有配套的精品资源点击获取