三合一收款码PHP源码原理与实现:从UA识别到支付路由的聚合支付方案
简介:这是新款支付宝、微信、QQ三合一收款码在线生成源码,面向需要整合多平台收款的商家、店主及前端开发者,用来解决同时出示多个收款码造成的收银繁琐问题。压缩包共44个文件,以HTML页面结构、CSS样式、JavaScript交互脚本及图片/字体资源为主,整体仅13.46MB,部署轻量;完全基于前端运行,无需后台服务器,商家在本地即可快速合成支付宝、微信、QQ三平台收款码,降低使用成本。内置多款模板,可按店铺风格自由挑选外观,同时图片与字体资源齐全,便于二次修改设计;JS脚本负责表单校验与二维码生成,处理过程在本地完成,避免支付信息外传,兼顾效率与隐私。已有1460人学习/下载,无论是独立网店还是实体门店,都能借此提升收款效率;前端初学者也能从清晰的目录结构中学习到完整的二维码生成与页面交互实战案例。
1. 新款三合一收款码源码:为什么“一张码扫天下”的背后是支付路由的取舍
线下商户收银台上最尴尬的画面,是支付宝、微信、QQ三张收款码并排贴在一起,顾客扫码时总要先判断自己要扫哪张。所谓“三合一收款码在线生成源码”,做的就是把这三种渠道的收款码合并成一个入口:扫一张码,由程序判断你是从支付宝、微信还是QQ点开的,再决定展示哪个平台的收款码。对开发者来说,这套源码解决的不只是“少贴一张码”,而是聚合支付的落地路由问题。需要先说明白:一张静态图片不可能同时被三个App识别为收款码,市面上能跑通的三合一,核心都是“动态中转页 + UA识别”。这篇文章会从原理、数据库设计、生成接口、模板改造一直写到避坑和进阶回调,适合要用 PHP 快速搭收款码聚合站的个人开发者和给小商户做解决方案的同行。
2. 三合一收款码的原理与选型:静态合并、动态跳转与中转页面怎么选
2.1 三合一收款码的三种主流实现方式:从“三张码贴一起”到“UA 跳转”
拿“三合一”这个需求来说,贴一起的最笨实现是拿图片编辑器把三张收款码横着拼到一张图里,然后打印出来。用户扫码时,微信扫到自己收款码,支付宝也扫到自己收款码,但问题很实在:图片分辨率受限于打印尺寸,任何一张二维码都容易超出识别容错边界;用户长按“识别图中二维码”时,微信只会识别它自己认识的码,支付宝码可能直接提示“无法识别”。我早年给朋友做过一次,他反馈顾客扫了半天扫不出来。这种静态合并方案只适合做展示,不适合当收款入口。
第二种是动态跳转:服务器生成一个二维码,扫码后先落到你的域名,程序读取 User-Agent,判断到是支付宝客户端就跳到支付宝收款码对应的图片页面,微信就跳微信对应的图片页面。这里的“跳”不是跳到qr.alipay.com这种外部链接,而是跳到你自己项目内展示该渠道收款码的落地页,因为微信内置浏览器会拦截任何跳转去打开支付宝的支付链接,反过来说支付宝也拦截微信支付链接。所以可靠做法是提前把三个渠道的收款码图片分别生成或上传到服务器,扫码后只展示对应图片,让用户长按识别自己App里的收款码。
第三种是中转页:不管什么UA,统一打开一个包含三个按钮和三个收款码的H5页面,用户手动选“我用的支付宝”再识别对应码。好处是程序逻辑最简单,坏处是多一步选择,顾客体验打折。现在标题里说的“在线生成源码”,大多数实现是第二种,部分带模板的会把第二种和第三种结合:电脑访问时显示三码合一的聚合选择页,手机访问时按UA自动选中。
提示:我推荐默认做“动态跳转 + 兜底选择页”,后面第3章的代码也按这个思路写。
2.2 运行环境与依赖选型:PHP 源码里怎么搭配二维码库和模板引擎
三合一收款码源码在网络上流传的大多是 PHP 版本,原因很朴素:这类工具一般部署在虚拟主机或小云服务器上,PHP 的部署门槛最低,不需要编译扩展,把文件传上去就能跑。如果你要手动搭建这套源码,环境建议是:Nginx/Apache + PHP 7.2+ + MySQL 5.7+。低版本 PHP 上有些写法会报错,比如展开数组、箭头函数等,建议直接用 PHP 7.4 或 8.0 起步,能少踩不少兼容性坑。
二维码生成库的选型上,PHP 圈最常见的库是phpqrcode,依赖 GD 扩展。类库里只有一个主要的QRcode类,生成 PNG 文件很方便。模板引擎我建议不要引入 Smarty/Twig 这种重量级方案,因为收款页的模板就那么几行,原生 PHP 的 include 或者字符串替换完全够用,还少一层缓存失效的坑。“内置多款模板”在源码里的体现,一般就是一个templates目录下放几个子目录,每个子目录一套 HTML+CSS。模板切换用参数传入模板ID,而不是运行时动态改代码。
数据库方面,如果订单量不大,SQLite 也能撑住,但国内这套源码的常见版本用的是 MySQL,因为部署更通用。订单表的作用是记录每次生成操作,避免同一个二维码链接无法追溯金额和渠道。如果只是做一个纯展示工具,甚至可以不建库,直接把参数加密进订单号里,但那样没法统计使用情况,所以我还是建议建表。
2.3 内置模板的渲染机制:模板变量、替换规则与多模板并存
“内置多款模板”本质上是一套模板语言的最小实现。以我常用的目录结构为例,每个模板是一个文件夹,里面包含index.php、style.css、logo.png。index.php里不写业务逻辑,只写收款页的界面,通过一个约定好的$data数组读取数据。$data里有收款码图片地址、金额、订单号、商户备注、背景色等。
渲染时,程序会根据 URL 里的template=red参数找到对应目录,加载里面的 HTML 片段并替换占位符。模板语言不需要多高级,关键是变量命名要统一。比如金额就用{amount},二维码图片就用{qrcode},标题就用{title}。因为模板文件本身是 PHP 文件,我一般会限制模板里只能读取$data里的键,不允许模板代码直接查询数据库或执行系统命令,否则模板就变成了一台提权入口。安全边界先守住,再谈花哨。
模板参数表写模板时直接照这个约定来:
| 变量名 | 含义 | 示例 |
|---|---|---|
{order_no} | 订单号 | 20250101123456 |
{amount} | 金额 | 19.90 |
{title} | 收款备注 | 购买会员 |
{qrcode} | 该渠道收款码图片地址 | /uploads/order_123/alipay.png |
{template_css} | 模板样式地址 | /templates/red/style.css |
3. 用 PHP 把三合一收款码源码跑起来:表结构、生成接口与跳转路由
3.1 目录结构与数据库表设计:先定订单表还是先定模板表
拿到一套源码后,我会先把目录结构过一遍,不要急着传服务器。常见的最小结构是:/lib放二维码库,/templates放模板,/uploads放生成的二维码图,/pay.php是扫码后的落地页,/generator.php是后台生成入口。可以先建订单表,因为生成接口和落地页都依赖它。SQL 如下:
CREATE TABLE `pay_order` ( `id` int(11) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL, `amount` decimal(10,2) NOT NULL DEFAULT '0.00', `channel` varchar(10) NOT NULL DEFAULT '', `template_id` varchar(20) NOT NULL DEFAULT 'default', `title` varchar(100) NOT NULL DEFAULT '', `alipay_img` varchar(255) NOT NULL DEFAULT '', `wechat_img` varchar(255) NOT NULL DEFAULT '', `qq_img` varchar(255) NOT NULL DEFAULT '', `status` tinyint(1) NOT NULL DEFAULT '0', `created_at` datetime NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;核心是order_no作为业务主键,channel用来记录扫码后的最终渠道,alipay_img、wechat_img、qq_img保存三个渠道的收款码图片路径。为什么不直接存一个联合二维码?因为后续要支持按渠道展示,三个路径分别存才能在不同客户端下取对应图。status字段在纯静态收款码场景下可以忽略,对接支付接口后用来标记是否已支付。
3.2 核心生成接口:PHP 代码生成收款码与数据返回
看生成接口的逻辑。用户在前台填金额、选择模板、填备注,提交到generator.php,程序先生成订单号,再分别为支付宝、微信、QQ三个渠道生成二维码图片。这里我使用常见的phpqrcode库,先把每个渠道的收款码链接转成项目内可访问的 PNG 文件:
<?php // generator.php 片段,仅保留核心逻辑 require_once __DIR__ . '/lib/phpqrcode/qrlib.php'; function createQrFile($text, $outDir, $fileName) { if (!is_dir($outDir)) { mkdir($outDir, 0755, true); } $filePath = $outDir . '/' . $fileName; // QR_ECLEVEL_L 表示7%容错率,8是每个模块像素大小,2是边框 QRcode::png($text, $filePath, QR_ECLEVEL_L, 8, 2); return $filePath; } $orderNo = date('YmdHis') . mt_rand(1000, 9999); $amount = round(floatval($_POST['amount'] ?? 0), 2); $title = trim(strip_tags($_POST['title'] ?? '')); // 以个人收款码地址为例,分别生成三个渠道的二维码 $alipayFile = createQrFile('https://qr.alipay.com/' . ($_POST['alipay_code'] ?? ''), 'uploads/' . $orderNo, 'alipay.png'); $wechatFile = createQrFile('https://qr.wechatpay.cn/' . ($_POST['wechat_code'] ?? ''), 'uploads/' . $orderNo, 'wechat.png'); $qqFile = createQrFile('https://i.qq.com/' . ($_POST['qq_code'] ?? ''), 'uploads/' . $orderNo, 'qq.png'); // 写库并跳转到预览页 $query = http_build_query(['order_no' => $orderNo]); header('Location: /preview.php?' . $query);这里的关键参数是QR_ECLEVEL_L。线上付款场景我建议用容错率 L,而不是更高的 H。高容错会让二维码模块点更密,手机在弱光环境下反而不容易识别;低容错在遇到轻微污损时可能扫不出。8 是每个模块的像素宽,2 是白边,这两个值配合打印尺寸调整。代码把三个渠道的支付地址写在生成参数里,实际开发中应该从后台配置读取,不要把密钥拼在表单里。
3.3 UA 识别与跳转路由:区分支付宝、微信与 QQ 的浏览器环境
接下来是整套源码的命脉,pay.php。用户扫了生成出来的二维码后,请求会落到这个文件。它要做两件事:根据 URL 订单号查出记录;再根据$_SERVER['HTTP_USER_AGENT']判断客户端,决定展示哪个渠道的收款码图片。
<?php // pay.php 跳转/渲染逻辑 $orderNo = $_GET['order_no'] ?? ''; $order = getOrderByNo($orderNo); // 查库函数,返回上面表结构的一行 if (!$order) { exit('订单不存在或已被清理'); } $ua = $_SERVER['HTTP_USER_AGENT']; if (stripos($ua, 'AlipayClient') !== false) { $image = $order['alipay_img']; } elseif (stripos($ua, 'MicroMessenger') !== false) { $image = $order['wechat_img']; } elseif (stripos($ua, 'MQQBrowser') !== false || stripos($ua, 'QQ/') !== false) { $image = $order['qq_img']; } else { // 电脑浏览器或未知来源,展示聚合选择页 $image = $order['alipay_img']; // 默认展示支付宝,需要聚合页可自行加判断 } $data = [ 'order_no' => $orderNo, 'amount' => $order['amount'], 'title' => $order['title'], 'qrcode' => $image, ]; renderTemplate($order['template_id'], $data);UA 判断不是穷举,只取三个平台客户端的稳定标识:支付宝是AlipayClient,微信是MicroMessenger,QQ 内置浏览器是MQQBrowser或 UA 里带QQ/。注意,iOS 端手机QQ打开链接的 UA 和安卓不完全一样,但一定有一个QQ标识;如果你发现某台手机判不进 QQ 分支,抓一次 UA 字符串补进条件里就好。这段代码没有直接 redirect 到支付链接,而是渲染本地图片,这样微信不会出现“已停止访问该网页”的风险提示。
4. 内置多款模板的改造实战:从换肤到自定义收款页
4.1 模板目录组织与配置驱动:把模板变成可选套餐
模板要能在线切换,就不能把样式写死在pay.php里。我一般把每个模板拆成一个文件夹,内部只放界面相关文件。目录像这样:
/templates /default index.php style.css preview.png /red index.php style.css /dark index.php style.csstemplate_id就是文件夹名。程序里维护一个模板配置数组,定义每个模板的显示名称、页面背景色、是否启用。配置驱动的好处是,以后新增模板只需要放文件夹,再在配置里加一行,不需要改支付逻辑。
<?php // config/templates.php return [ 'default' => ['name' => '简约白', 'enabled' => true], 'red' => ['name' => '中国红', 'enabled' => true], 'dark' => ['name' => '暗夜蓝', 'enabled' => false], ];这里enabled字段是给后台列表用的,避免把还没做好的模板暴露给用户。页面里展示模板选择时,循环这个配置输出单选框即可。强调一点:模板 ID 作为 URL 参数传入时,一定要做白名单校验,不能直接拼路径。曾经有源码直接把template参数拼进include语句,被人读掉了配置文件,这是典型的模板注入漏洞。用配置数组做一层映射,不存在的 ID 就回落到default。
4.2 收款页渲染示例:模板标签里嵌入二维码和金额
每个模板的index.php不直接使用$_GET,而是从一个统一的渲染入口拿$data。以default模板为例,最小化的收款页面是这样的:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>收款:{title}</title> <link rel="stylesheet" href="/templates/default/style.css"> </head> <body> <div class="pay-box"> <h2>{title}</h2> <p class="amount">¥{amount}</p> <img src="{qrcode}" alt="收款二维码" class="qrcode"> <p class="tip">请使用对应App扫码支付</p> </div> </body> </html>这里的{title}、{amount}、{qrcode}都是占位符。渲染函数把它们替换成真实数据。注意,我在页面里只有一个{qrcode}变量,因为服务端已经根据 UA 选好渠道,前端不需要再展示三个码,这样能减少顾客认知负担。要是想做成手动选择版,就在聚合分支里返回一个带三个按钮的模板,这是进阶选项。
渲染函数长这样:
<?php function renderTemplate($templateId, $data) { $config = require __DIR__ . '/config/templates.php'; if (!isset($config[$templateId]) || !$config[$templateId]['enabled']) { $templateId = 'default'; } $file = __DIR__ . '/templates/' . $templateId . '/index.php'; $content = file_get_contents($file); foreach ($data as $key => $value) { $content = str_replace('{' . $key . '}', htmlspecialchars($value, ENT_QUOTES), $content); } echo $content; }用str_replace做模板替换是因为模板简单,不引入额外迁移成本。htmlspecialchars必须加,否则用户提交的标题里有引号或脚本标记时,页面会被注入XSS。file_get_contents配合白名单模板 ID 是安全的,但如果哪天你决定支持动态模板名,一定要先走配置映射这关。
4.3 动态替换金额、备注、头像的参数约定
三合一收款码在线生成,常见的使用场景是生成后分享给顾客。为了让每个订单不依赖固定写死的金额,URL 里需要约定参数名。我习惯用amount、title、template、bg四个参数。amount用 decimal(10,2) 接收,页面显示前做一次number_format($amount, 2),避免浮点误差;title做长度限制和标签过滤;bg作为模板内的小主题色,直接让模板文件里的 CSS 读取,不在全局替换变量中开放。
比如生成链接的示例是:
/pay.php?order_no=20250101123456而生成二维码前,后台会拼一个带参数的预览链接,这组参数只作为初始值,真正展示时以订单表为准。这里有个容易踩的坑:有人把金额直接放在二维码链接里,顾客扫码后改URL里的金额,页面显示变了,但实际支付时指向的收款码图片并没有变。所以必须服务端落库,生成时就锁定。你的源码里如果看到“金额从URL读取且同时直接输出收款图片”,那就要小心,这属于伪动态收款,实际收款码还是固定金额。
我给的实现里,{amount}只是展示在页面上的数字,收款码图片在生成时已经固定。做到“顾客改金额也能按新金额收款”,需要对接支付宝当面付、微信Native支付这类官方接口,这属于进阶能力,最后第6章再说。
5. 三合一收款码的常见问题与避坑排查:从扫码不识别到回调失效
5.1 扫码后打开一个网页而不是收款码,或者被微信拦截
现象:顾客用微信扫三合一码,跳到一个没有意义的网页,或者微信提示“已停止访问该网址”。
原因:很多新手实现里把扫码后的跳转地址直接指向支付宝的qr.alipay.com链接,微信识别到这是外部支付域名,直接拦。还有人把二维码内容生成成了https://qr.alipay.com/xxx,让微信误以为你要诱导支付。
解决:二维码内容必须是你自己的域名落地页地址,例如https://yourdomain.com/pay.php?order_no=xxx。落地页在服务端根据 UA 展示提前生成好的对应渠道收款码图片,而不是 redirect 到对方域名。这样微信看到的只是你域名下的一张图片页面,不涉及支付URL跳转,拦截阈值低很多。另外,域名一定要备案,国内服务器如果未备案,主流应用都会在扫码后直接拦截,这不是代码问题。
5.2 支付宝/微信/QQ 扫码后提示风险或“请复制链接到浏览器打开”
现象:扫码后能打开,但底部弹条提示风险,或者微信里无法长按识别图片。
原因:新域名第一次被扫时,微信风控会试探性拦截;另外收款页没有 HTTPS,或者使用了自签名证书;长按识别图片在微信里本身就是受限功能,如果页面图片过小或二维码库生成时容错率太高,也会让长按识别失败。
解决:域名必须配 HTTPS,用免费证书就行。二维码图片在页面里固定为 240px 以上;生成二维码的时候不要用QR_ECLEVEL_H,改成 L 或 M,保证模块清晰。如果要进一步减少风险提示,可以在页面上加入“如果无法识别,请点击放大”的功能,图片点击后全屏预览,长按成功率更高。
5.3 金额固定无法修改,修改链接中的 amount 参数后页面仍然显示旧金额
现象:用户把pay.php?order_no=...&amount=100里的金额改成 1,页面数字变了,但实际扫码支付的还是原来的金额。
原因:这是源码设计问题。收款码图片在生成时已经根据当时金额生成了静态图片,URL 参数只是显示值,并不会重新生成图片。很多在线生成源码就是这种“假动态”。
解决:要么接受这个设计,把金额作为生成参数在二维码生成时就锁定,保证用户不能改;要么对接官方支付 API,用动态金额二维码。就三合一收款码本身来说,个人收款码天生不支持动态金额,所以我在模板页会加一行说明“请按页面金额支付”,并隐藏 URL 里的金额参数拼接。如果你想做真实动态金额,最后第6章的官方接口方案才是出路。
5.4 模板切换后样式不变,修改模板文件也没有反应
现象:后台把默认模板改为红色模板,前台预览还是老样子;直接改 style.css 后刷新页面,样式没变化。
原因:前端浏览器缓存了 CSS,模板加载路径带了固定文件名,没有加版本号;另外如果模板渲染函数里对模板 ID 做了白名单,但是配置文件的enabled没有打开新模板,就会回退到 default。
解决:在模板入口引用样式时加版本参数,比如/templates/red/style.css?v=20250101,修改文件时同步更新版本号;或者在服务器端把 CSS 过期时间设为最短。代码层面,检查渲染函数里是否有enabled判断,把旧配置数组里的false改成true。还有一个土办法:给预览页 URL 加一个时间戳参数,绕过浏览器缓存,但不能用于线上长期使用。
5.5 数据库连接报错或二维码库没有启用 GD 扩展
现象:部署到虚拟主机后,打开生成接口报“Call to undefined function imagecreatetruecolor”,或数据库连接失败。
原因:二维码库phpqrcode依赖 GD 扩展;数据库配置文件和实际 MySQL 账号不匹配,也可能 PHP 版本太高导致旧的 mysql 函数失效。
解决:先登入服务器执行php -m | grep gd确认 GD 是否加载,没加载就安装php-gd扩展。数据库报错时打开源码里的config/db.php,核对 host、端口、库名、账号、密码。特别注意旧源码里可能用了mysql_connect这种 PHP 5 时代的函数,如果跑在 PHP 7+ 上必然报错,需要改为 MySQLi 或 PDO。我在接手旧源码时,通常第一步就把所有mysql_*函数替换成mysqli_*,避免逐个排查。
6. 进阶:让三合一收款码从“能扫码”到“能记账”:回调通知与稳定性验证
如果要让三合一收款码不只是“贴个码”,而是能记录谁付了多少钱,就得从静态二维码走向官方支付接口。常见做法是:三合一页面只负责展示支付入口,点击“立即支付”后请求后端创建订单,后端调用支付宝当面付或微信Native支付接口,拿到支付二维码再渲染到原来的模板里。支付完成后,支付宝/微信服务器会发异步通知到你的回调地址,你需要校验签名并更新订单状态。这就是“支付宝回调”在一套源码里真正起作用的地方。一个最小化的回调接收片段(以支付宝为例)可以这样写:
// notify.php 片段 $post = $_POST; if (verifySign($post)) { // 校验支付宝签名 $orderNo = $post['out_trade_no']; if ($post['trade_status'] === 'TRADE_SUCCESS') { updateOrderStatus($orderNo, 1); } echo 'success'; // 告诉支付宝别重发 }这段代码不复杂,但签名校验一定不能省略。不校验回调就直接更新订单,别人伪造一个请求就能把订单标记成已支付,那是线上事故。调试回调时不要只依赖日志,我用的最土但最有效的办法是让回调地址把完整内容写到本地文件,然后拿着支付宝沙箱去触发,再对着文档核对字段。
二维码稳定性验证也有技巧。我见过太多次“我本地扫没问题,顾客手机扫不出”的翻车。我的习惯是:拿到新模板或新生成二维码后,至少用三个不同品牌的手机、分别在亮光和暗光环境扫一遍;打印出来用普通A4纸再扫,别只用手机屏幕测。二维码生成参数用 8 倍大小和 L 容错率基本能覆盖大多数屏幕,但如果是打印使用,模块要调到 10 以上。容错率并非越高越好,太高会让码点变得更密,打印小尺寸时反而一塌糊涂。希望这些来自一线踩坑的经验能帮你少走弯路,也希望你的收款码项目能少一点玄学、多一点确定性。
本文还有配套的精品资源,点击获取