ARTICLE DETAIL

资讯详情

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

聚合收银台源码拆解:支付路由、回调分发与订单状态机实战

聚合收银台源码拆解:支付路由、回调分发与订单状态机实战 简介星益云聚合收银台系统源码是一套面向商家与开发者的多支付渠道整合方案支持银行卡、支付宝、微信、电子钱包等聚合收款并涵盖实时交易处理、数据统计、风险监控及主流收银硬件适配。源码以PHP为核心包内共637个文件包含305个PHP脚本、48个HTML页面、46个JS与29个CSS样式以及77个GIF、52个PNG等界面素材另附SQL数据库文件和ThinkPHP框架相关配置整体压缩包仅15.81MB目录结构清晰便于按模块定位和二次开发。截至目前已有196人学习。开发者可借此快速搭建聚合收银台原型学习支付接口对接、交易流水管理、前端收银界面设计及安全加密实现商家也能通过开源代码定制专属收银流程结合销售数据分析优化运营决策。资源附带环境配置与基础目录说明适合具备一定PHP基础的中级开发者作为支付类项目参考。1. 聚合收银台源码拆开看其实是一套支付路由聚合收银台这类源码下载下来第一反应是当作“多支付渠道收银页面”来用但拆完星益云这套之后你会发现它真正值钱的部分不在UI而在“支付路由 回调分发 订单状态机”这一整条链路。所谓聚合收银台本质是把微信支付、支付宝、云闪付等渠道的差异收口到一个统一接口商户前端只管跳转/扫码渠道差异全部由服务端消化。合适谁自营商城、多门店收款、外包项目中需要同时接两三个支付渠道又不想维护多套对接代码的团队都适合直接拿它做底座。但别急着把代码跑起来——先看路由配置和回调处理这才是决定这套系统上线后会不会掉单的关键。2. 模块划分与部署参数先弄懂这套支付路由是怎么转起来的2.1 收银台系统的模块构成与选型理由拆这套源码时我的习惯是先列模块清单。聚合收银台通常由五个部分组成收银页/API入口、下单服务、渠道驱动、异步回调接收器、商户管理后台。星益云的目录结构基本也按这个思路组织app/下分controller/、model/、service/service/里每一个支付渠道对应一个 driver 文件比如wechat_driver.php、alipay_driver.php。这样的设计有个明显好处新增渠道时主流程代码一行都不用动只加一个 driver 和对应配置。选型理由值得多说一句。聚合收银台的难点从来不是“调通一个渠道”而是“多个渠道并存时怎么保证状态一致”。所以这套源码在 service 层之上做了一层统一订单状态机所有渠道的订单都落在orders表里由状态字段流转而不是让每个渠道各自维护一套订单状态。这种设计在二次开发时非常友好——对接外部业务系统只需要查你这边的orders表不需要理解微信和支付宝各自的异步通知数据结构。配套环境我按常见的 PHP 项目来处理PHP 7.4、Nginx 1.18、MySQL 5.7 或 8.0、Redis 可选用于缓存渠道配置和做二维码短链。这些参数在config/下的配置文件中都有占位填好再启动。Nginx 伪静态要额外注意下面单独说。2.2 部署环境参数与伪静态配置先给出一份环境参数表这是我能稳定跑通这套源码的组合直接抄即可组件建议版本/参数说明PHP7.4开启pdo_mysql、openssl、curl扩展支付签名和回调验签依赖 opensslNginx1.18配置 thinkphp 兼容伪静态前端控制器模式必须配 rewriteMySQL5.7字符集utf8mb4订单号与回调数据含中文时避免乱码Redis5.0可选用于缓存二维码短链和渠道 access_tokenPHP 内存限制memory_limit 128M生成二维码图片时偶尔需要额外内存Nginx 伪静态配置我一般这么写注意try_files那段是这套 PHP 框架路由生效的关键server { listen 80; server_name your-domain.com; root /var/www/xingyiyun/public; index index.php index.html; location / { try_files $uri $uri/ /index.php?s$uri$args; } location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } }try_files的核心作用是把所有非文件请求交给index.php处理这样下单接口/api/order/create这类路由才能被框架正确分发。如果你拿到源码后在浏览器里访问下单接口返回 404九成是这个配置没生效。PHP 这边的php.ini里还要确保openssl扩展开启否则后面渠道签名校验会直接报“Call to undefined function”页面白屏没提示。2.3 数据库初始化与渠道配置字段源码包里通常会附带install.sql或database.sql导入后重点看两张表orders和payment_channel。orders表字段我整理了一份对接时要心里有数字段名类型含义order_novarchar(32)业务订单号渠道侧也叫out_trade_nochannelvarchar(16)渠道代码wxpay/alipay/cloudpayamountdecimal(10,2)订单金额单位元statusvarchar(16)pending/paid/closed/refundedcallback_urlvarchar(255)商户自定义异步回调地址可空expire_atint(11)二维码过期时间戳paid_atint(11)支付成功时间戳trade_novarchar(64)渠道侧交易单号渠道配置在config/payment.php或后台的“支付设置”页面里维护核心参数是每个渠道的商户号、AppID、API 密钥和回调地址。这里有个容易忽略的点callback_url不一定全局统一建议在下单参数里允许商户传入否则一个平台接了多个商户时回调只能全部打到同一个地址业务侧没法区分。这套源码在这一点上留了参数位是个加分项。3. 支付流程实现下单、回调、状态机怎么拼出闭环3.1 下单接口从参数校验到生成支付二维码聚合收银台的下单接口是整套系统的门面。前端页面展示一个收款二维码背后调用的是POST /api/order/create参数只有三个核心total_fee金额、channel渠道、return_url支付成功后的跳转页。我这里给出一个简化版本思路和源码一致性很高public function createOrder($params) { if (!isset($params[total_fee]) || !is_numeric($params[total_fee])) { return $this-fail(金额参数异常); } // 金额统一转成分避免浮点运算误差 $totalFee intval($params[total_fee] * 100); if ($totalFee 0) { return $this-fail(金额必须大于0); } $order[order_no] date(YmdHis) . mt_rand(1000, 9999); $order[channel] $params[channel]; // wxpay / alipay $order[amount] $totalFee / 100; $order[status] pending; $order[expire_at] time() 120; // 二维码120秒有效 // 调用渠道driver生成支付链接 $driver DriverFactory::create($params[channel]); // 根据渠道选择实现 $payUrl $driver-prepay($order); // 返回二维码内容URL $this-orderModel-insert($order); return $this-success([ pay_url $payUrl, order_no $order[order_no], expire_at $order[expire_at], ]); }这段代码有几个参数值得较真。total_fee前端传的是元服务端主动转成分原因是微信和支付宝的接口金额单位都是分而且用字符串传输如果在 PHP 里用浮点数计算金额0.58 这类数字会出现精度误差最终导致渠道侧校验金额不通过或者入账金额错一分钱。expire_at设置 120 秒是折中值——太短用户来不及掏手机扫码太长又容易产生“用户扫了但已经过期”的脏订单实际项目里我一般调到 120180 秒。DriverFactory::create()是这套系统中最重要的调度点。源码里每个渠道 driver 都实现了同一个接口prepay()负责生成支付链接verify()负责验签refund()负责退款。下单、回调、退款这三个动作全部走统一接口这是聚合方案的可扩展性所在。3.2 二维码生成与过期控制下单接口返回pay_url后前端怎么把它变成二维码常见的做法是前端用qrcode.js生成但如果你需要服务端直接输出二维码图片源码里通常用的是phpqrcode库调用方式如下public function qrcode($orderNo) { $order $this-orderModel-findByOrderNo($orderNo); if (!$order || $order[status] ! pending) { throw new \RuntimeException(订单不存在或已过期); } if (time() $order[expire_at]) { // 状态流转为 closed标记二维码作废 $this-orderModel-updateStatus($order[id], closed); throw new \RuntimeException(二维码已过期请刷新); } include_once ROOT_PATH . /lib/phpqrcode/phpqrcode.php; \QRcode::png($order[pay_url], false, QR_ECLEVEL_L, 6); }过期判定放在生成二维码时而不是前端是个容易忽略但很重要的细节。原因是前端时钟不可信用户手机时间快了 2 分钟他扫的二维码就应该还在有效期内这个判断必须放在服务端以expire_at与服务器当前时间为准。另外\QRcode::png()的第一个参数是二维码内容这里传的是pay_url不是订单号渠道返回的pay_url已经包含了必要的支付参数扫码后直接调起收银台。3.3 异步通知处理与订单状态机支付结果同步页面跳转并不可靠——用户付完款可能直接关掉浏览器所以这套系统的核心链路是“异步回调 主动查询兜底”。所有渠道都支持服务端异步通知支付完成后往你的回调地址推送结果。回调处理函数是这套源码里我建议逐行读的部分public function notify() { $payload file_get_contents(php://input); $data json_decode($payload, true); if (!$data) { return fail; } $sign $data[sign] ?? ; unset($data[sign], $data[sign_type]); $driver DriverFactory::create($data[channel] ?? ); if (!$driver-verify($data, $sign)) { // 记录日志并返回失败渠道会再次推送 \Log::error(callback sign verify failed, $data); return fail; } $order $this-orderModel-findByOrderNo($data[out_trade_no]); if ($order $order[status] pending) { $this-orderModel-markPaid($order[id], $data[trade_no]); // 触发商户自定义业务回调 $this-notifyMerchant($order); } return success; }回调函数里的幂等保护是防止“多次入账”的防线。渠道对异步通知是有重试机制的假设网络抖动导致第一次回调处理成功但返回超时渠道会重新推送同一笔订单如果没有pending状态这个判断条件同一笔订单可能被处理两次商户侧就会出现重复发货。在数据库层面orders表上一般还会给order_no加唯一索引双重保证。状态机流转是这个系统的“还原点”拉平看就三条线当前状态触发动作目标状态pending用户扫码支付成功/回调到达paidpending二维码超过expire_at且主动查询未支付closedpaid商户发起退款且渠道退款成功refunded这里建议你把markPaid和notifyMerchant分开。前者只改订单状态后者负责通知业务系统比如开放平台对接的下游系统。如果一个系统在通知商户失败时把订单状态也回滚了你就会遇到“用户明明付了钱订单还是待支付”的诡异问题。源码里这层分离做得不错业务二次开发时保持这个拆分习惯。4. 常见问题与排查五种必踩的坑4.1 二维码扫出来是乱码或打不开现象前端展示的二维码能生成但手机扫码提示“链接无效”或直接打开一个显示乱码的页面。原因最常见的是下单接口返回的pay_url里有中文参数或者pay_url本身被框架的 URL 编码处理了一遍二维码里存的不是完整可访问的链接。另一个高频原因是 Nginx 伪静态配置没配好pay_url指向的路由直接返回了 HTML 错误页二维码内容反而变成了“错误页面编码后的文本”。解决扫码前先手动把pay_url放到浏览器地址栏访问确认能正常跳转到收银台。如果截图里有index.php?s这样的路径说明伪静态没生效回去检查 2.2 的try_files配置。如果参数里有中文需要在下单代码里对pay_url做urlencode编码后再返回。4.2 微信/支付宝异步回调一直收不到现象用户支付成功了渠道后台能查到交易但本地订单状态一直不变日志里没有任何回调记录。原因渠道的异步通知只发往公网可访问的 HTTPS 地址很多人在本地联调时把回调地址写成了http://localhost/callback自然收不到。另一种情况是回调地址填对了但你的服务器防火墙限制了渠道服务器 IP回调包被拦在入口。解决回调地址必须填公网可访问的域名且建议强制 HTTPS渠道侧配置的地址要和config里的callback_url完全一致。排查时在回调函数入口第一行写\Log::debug(notify raw, file_get_contents(php://input));然后到渠道后台手动触发一次“重发通知”如果日志里有记录但验签失败问题在签名如果连日志都没有问题在网络层。4.3 用户已支付但订单状态没变把同步跳转当成了支付结果现象用户支付成功并跳回商户页面页面显示支付完成但数据库订单状态是pending后台对账时发现少了这笔。原因这是示例代码最容易埋雷的地方。前端体验页跳转依赖的是渠道的同步通知return_url但同步通知是不保证送达的用户支付后直接关掉浏览器同步跳转压根不会发生。正确实现必须依赖异步通知notify_url同步跳转只做页面展示。解决把markPaid的控制权完全交给异步回调同步跳转页只查询订单状态做展示不修改数据库。补单机制也必须配合写一个定时脚本每 5 分钟拉一遍所有pending状态的订单向渠道查询真实支付状态已支付的主动补标记。4.4 金额字段类型不对对账差一分钱现象渠道账单显示金额是 0.58 元本地订单表里显示 0.57999998后台对账怎么都对不上。原因建表时amount用了float或者doublePHP 里又直接拿浮点数存入。浮点数在二进制下无法精确表示 0.58数据库存储和渠道侧字符串传参之间就产生了微小误差。解决这是血泪教训金额字段一律用decimal(10,2)应用层全部用字符串操作。下单参数接收时转成分为整数处理入库再转回元字符串。对账脚本里比较金额也不能用统一转成“分”为单位再比。4.5 解压源码包时提示“压缩文件末端”错误现象源码下载完Windows 自带的右键解压弹窗报“不可预料的压缩文件末端”或者要求输入密码但资源介绍里没给密码。原因这种情况常见于 zip 包带了伪加密标记或目录里包含中文文件名和 Unix 权限位Windows 自带解压工具对这类 zip 兼容性较差。项目资源发布时常在打包环节加了多余的标记位不是包坏了。解决优先用 7-Zip 打开先“测试归档完整性”能正常列出文件名说明包体没问题。若提示需要密码但资源页没有密码说明大概率是伪加密用 7-Zip 直接打开后把文件拖出来即可。如果拖出来某个子文件损坏再单独重新下载不用反复解压整个包。5. 对账脚本与二次开发扩展点从能跑变成能上线5.1 交易对账流程渠道账单与本地订单差异处理上线后的第一周最容易出问题的地方是“对账”不是支付链路。支付链路有问题会立刻暴露但对账差异是累积到每天业务结束后才暴露的处理成本高得多。在这套源码基础上做对账脚本我建议按“拉单→比对→查缺”三步走。拉单阶段从各渠道后台下载每日交易账单格式通常是 CSV。比对这个环节SQL 这么写-- 按渠道日期聚合本地已支付订单金额 SELECT channel, DATE(FROM_UNIXTIME(paid_at)) AS pay_date, COUNT(*) AS local_count, SUM(amount) AS local_amount FROM orders WHERE status paid AND paid_at BETWEEN 2025-01-01 AND 2025-01-02 GROUP BY channel, DATE(FROM_UNIXTIME(paid_at));这段 SQL 的作用是出一份“本地订单汇总表”然后按同样的维度和渠道账单做差额比对。差异只有三种可能本地有订单但渠道账单没有下单但渠道侧未支付成功状态可能是pending渠道账单有交易但本地没有对应order_no用户在渠道侧直接付款订单没落在本地表里金额不一致有可能牵扯退款或渠道手续费。处理逻辑上第一种要主动查渠道订单接口确认支付失败就置为closed第二种要第一时间查回调日志大概率是回调通知丢失手动补单即可。第三种要先看退款表把已退款订单剔除后再对比仍对不上就导出明细人工核对。这套源码的对账思路是留下payment_log表所有渠道交互都记录原始请求和响应断言对账差异生成时能直接索引到原始报文。5.2 新增支付渠道的改造思路driver 模式的边界业务跑起来后大概率会遇到新需求——加一个银行聚合渠道或者数字人民币渠道。基于这套源码的 driver 模式改造路径是清晰的新建一个service/drivers/xxx_driver.php实现prepay()、verify()、refund()三个方法然后在DriverFactory的create()里注册新渠道代码。具体实现时最花时间的是prepay()里的参数拼装。不同渠道对这个方法的要求差异很大微信需要notify_url和openid支付宝需要subject和timeout_express新渠道的参数要仔细看对方接入文档。我的经验是先在 driver 里把prepay()收到订单数据后做一次全量日志记录格式用json_encode这样对比渠道侧收到的参数和文档要求能一眼看出差异。数据库侧只需要在payment_channel表里增加一条配置记录orders表不用改——通用字段channel、callback_url、trade_no已经覆盖了新渠道。这是这套聚合设计给我留下的最好印象它把渠道差异锁死在 driver 内部业务层无感知。5.3 开放平台对接如何把聚合收银台能力开放给第三方系统有些场景下你的收银台不只要给自己用还要开放给下游开发者。比如你在做一个 SaaS 平台商户各有一套系统不能让他们都直接访问你的数据库。这时候需要在这套源码上包装一层开放接口核心是“下单权限控制”和“回调转发”两个点。下单权限控制常规做法是给第三方系统分配app_id和app_secret外部系统下单时携带app_id和一个签名串签名规则是把参数按 key 排序后拼接app_secret做 MD5。这个模式常见于各大支付开放平台源码里虽然没有现成的中间件但改造不难在createOrder入口处加一层 filter 即可。回调转发是另一个容易被忽略的点外部系统下单时在你的接口里传入它自己的callback_url但你传给微信的notify_url仍是你自己的地址。收到微信回调后先更新本地订单再以 HTTP POST 方式把渠道原始报文转发给外部系统的callback_url转发失败要有重试队列。这层设计在不信任第三方系统时特别有用——第三方网络不稳定不影响你把订单状态维护正确。6. 本地联调技巧沙箱环境 三个自检必经步骤拿到这套源码后千万别直接拿正式商户号联调有一次我图省事用正式号测结果产生了一笔真实扣款退款的流程又折腾了三天。正确的做法是先确认源码里的渠道配置支持沙箱模式微信有商户平台测试号支付宝有沙箱环境新渠道也基本都有测试网关。沙箱联调在config/payment.php里把sandbox字段置为true然后填入测试商户号和密钥。支付宝沙箱调起支付后会跳到一个专门的测试收银台页面输入预设的买家账号即可完成支付整个流程和正式环境几乎一致唯一的差别是金额不会真实扣款。我在本地联调时习惯用 0.01、0.02、0.03 三笔小额测试每笔都覆盖不同渠道验证完了直接跑对账 SQL看金额是否一致。联调顺序我建议强制走三步缺一不可。第一步测下单接口确认返回pay_url用扫码工具确认二维码内容能打开。第二步测异步回调支付完成后看回调日志确认日志里出现了notify received且verify结果为 true然后查库确认status从pending变成了paid。第三步测掉单补单把orders表里statuspending的订单的expire_at改为过去时间跑一次主动查询脚本确认能正确标记为closed或补单为paid。最后一条排查技巧给到回调疑难杂症。回调验签失败是联调时最磨人的问题我的习惯是在verify()方法里临时把接收到的原始报文和重新生成的签名字符串都写到日志里两边逐字符对比。绝大部分验签失败的原因是签名时用的字段顺序不对——微信要求按 ASCII 码排序后拼接支付宝则是按文档给的顺序拼如果某个字段多传或漏传签名就会对不上。从那以后我每次部署这类收银台系统都强制自己走一遍“沙箱下单→扫码支付→回调成功→对账无误”四步自检确认链路全通之后才敢换正式参数。这套源码整体上给的扩展空间和状态机设计都够用踩坑的地方基本集中在部署细节和回调处理上按前面说的来能省不少上线后的操心时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表