简介:这是一套面向网站开发者与电商运营人员的多支付集成源码,集中解决网页端接入QQ支付和支付宝支付时的接口对接、订单生成与回调处理等问题。资源共219个文件,压缩包约3.9MB,以PHP业务逻辑、JavaScript前端交互、CSS样式与PNG/JPG等页面素材为主,另含SQL数据库脚本及配置文件,便于本地部署和二次开发。已有314人学习下载。内容涵盖商户注册、支付订单生成、二维码/H5拉起、回调验签、订单状态更新等完整实现链路,并特别注重安全性、异常处理与跨浏览器兼容性。对于需要快速搭建支付演示环境或理解主流支付流程的开发者,是一份可直接参考和学习的基础源码包。
1. 收银台页面先落地,再谈支付对接
拿到这份Pay_html_QQ支付_payment支付_Alipay_pay源码_,第一反应不是去翻支付接口文档,而是先把它当成一组静态页面资源来看。资源包里的 app.min.css、amazeui.min.css、bootstrap.css、main.css、animate.min.css、style.css,再加上对应的 HTML 页面,构成了一个完整的收银台前端骨架。也就是说,这套源码解决的是「支付页面长什么样、表单怎么提交、二维码区域怎么展示」这一层,而真正和 QQ 钱包、支付宝服务器通信的部分,需要你用自己的后端去承接。对做聚合支付的团队来说,这正好是最省时间的起点:视觉、交互、静态资源全部就绪,你只需要把下单接口的返回结果填进预留的容器里。本文会把前端资源逐个拆开讲,再给出对接 QQ 支付和 Alipay 当面付的后端流程与代码示例,最后用抓包思路验证回调链路是否真的通了。适合正在搭网站收银台、需要快速集成多种支付渠道的开发者阅读。
2. 静态资源组里藏着页面骨架:CSS 分工与 HTML 结构
2.1 先分清三套 UI 框架的职责
资源包里的 CSS 文件可以分成三组。第一组是全局框架层,包括 bootstrap.css、bootstrap.min.css、amazeui.min.css,这三套决定了页面的栅格系统、按钮、表单、弹窗等基础组件样式。第二组是业务样式层,包括 app.css、app.min.css、main.css,这类文件通常由项目开发者自己维护,里面定义的是支付页特有的布局,比如订单金额展示区、支付方式 Tab 切换、二维码容器、结果页的成败图标。第三组是动效辅助层,animate.min.css 负责按钮 hover、弹窗出现、二维码 loading 等过渡动画。
实战里最常见的问题是把三套框架混用导致样式冲突。bootstrap 的.btn和 amazeui 的.am-btn虽然都定义了按钮,但优先级和圆角半径不同。我一般会这样处理:全局布局只保留 bootstrap,amazeui 只引它独有的am-前缀组件,业务样式的类名统一用pay-前缀,从命名空间上避开覆盖。style.css 这个文件名太通用,通常放的是页面级覆写,建议打开后先搜body和.container,看有没有把框架的默认间距改掉。
2.2 收银台的 HTML 骨架长什么样
支付页面的核心区域一般分为三段:订单信息区、支付方式选择区、二维码/表单提交区。下面的结构是这套源码里最常见的组合方式:
<div class="container pay-checkout"> <div class="order-panel"> <h3 class="order-title">订单确认</h3> <div class="order-amount"> <span class="amount-label">应付金额</span> <strong class="amount-value" id="payAmount">function validateAndSubmit() { var amountDom = document.getElementById('payAmount'); var amountText = amountDom.getAttribute('data-amount'); var channel = document.querySelector('.channel-tabs li.active').getAttribute('data-channel'); var orderNo = document.getElementById('orderNo').innerText.split(':')[1]; if (!/^[0-9]+(\.[0-9]{1,2})?$/.test(amountText)) { alert('金额格式不合法,仅支持两位小数'); return false; } var amountFen = Math.round(parseFloat(amountText) * 100); if (amountFen <= 0) { alert('金额必须大于0'); return false; } document.getElementById('orderIdInput').value = orderNo; document.getElementById('channelInput').value = channel; document.getElementById('amountInput').value = amountFen; document.getElementById('payForm').submit(); return true; }这里把金额转成最小单位「分」再提交,是为了后端签名时与各支付平台保持一致,避免因浮点数换算产生差异。例如一笔 199.00 元的订单,前端传 19900,后端签名原串也按分处理,与 QQ 支付和支付宝的金额单位约定统一。正则里的[0-9]+(\.[0-9]{1,2})?允许 0.01 到任意整数加两位小数的范围,但不允许.5、1.这类边界写法,这类格式错误在用户手输金额时经常出现,提前拦截能减少后端无效请求。
3. 对接 QQ 支付:从商户参数到 Native 下单
3.1 商户平台要拿到哪几样东西
QQ 钱包在 web 端常用的支付方式是 Native 扫码。接入前需要先到腾讯开放平台完成商户入驻,审核通过后获得mch_id(商户号)和appid(开放平台应用 ID),然后在商户平台生成 API 密钥api_key。这个 api_key 是回调验签和下单签名共用的对称密钥,一旦泄露,攻击者可以伪造回调通知。建议单独申请一个 API 密钥,不要用登录后台的密码。
QQ 支付的下单接口地址是https://qpay.qq.com/cgi-bin/pay/qpay_unified_order.cgi,采用 XML 格式交互。这里需要区分:文章摘要里提到的 QQ 账号体系直接支付,属于 JSAPI 或 H5 场景,而资源包里这种带二维码容器的收银台页面,最贴近的是 Native 扫码支付——页面展示二维码,用户用 QQ 扫码后唤起钱包完成支付。
3.2 统一下单请求的组装与签名
后端收到前端表单提交的channel=qq后,需要组装统一下单参数并做 MD5 签名。以下代码以 PHP 为例,这也是这套资源最常见的使用场景:
public function createQqOrder($orderId, $amountFen, $body, $notifyUrl) { $params = [ 'appid' => $this->qqAppId, 'mch_id' => $this->qqMchId, 'nonce_str' => $this->generateNonceStr(32), 'body' => $body, 'out_trade_no'=> $orderId, 'fee_type' => 'CNY', 'total_fee' => intval($amountFen), 'spbill_create_ip' => $this->clientIp, 'trade_type' => 'NATIVE', 'notify_url' => $notifyUrl, ]; $params['sign'] = $this->signParams($params, $this->qqApiKey); $xml = $this->toXml($params); $response = $this->postXml('https://qpay.qq.com/cgi-bin/pay/qpay_unified_order.cgi', $xml); $result = $this->fromXml($response); if ($result['return_code'] === 'SUCCESS' && $result['result_code'] === 'SUCCESS') { return $result['code_url']; // 二维码内容 } throw new \Exception('QQ统一下单失败: ' . $result['err_code_des']); }签名函数signParams的规则是:先对参数按字典序升序排列,拼成key1=value1&key2=value2的字符串,末尾追加&key=你的api_key,再对整串做 MD5,结果转大写。这个规则和支付宝、微信支付略有不同——微信支付在新版本里改用 HMAC-SHA256,而 QQ 钱包仍以 MD5 为主。需要注意:参数值不为空的才参与签名,sign字段本身不参与。
trade_type传NATIVE时,支付平台返回code_url,这个值不是二维码图片本身,而是一串链接。前端拿到后需要把它生成二维码图片,常见的做法是用qrcode.js这类库转成 data URL 再填入img标签的src。不要尝试让后端去调第三方二维码生成接口,延迟高且不稳定。
3.3 回调验签与订单状态流转
用户扫码支付成功后,QQ 钱包会向notify_url发送异步通知,内容同样是 XML。这里最容易踩的坑是:没有先判断return_code就直接验签,导致签名通过但业务上其实失败了。正确的处理顺序是:
public function handleQqNotify() { $xml = file_get_contents('php://input'); $data = $this->fromXml($xml); // 第一步:检查通信层状态 if ($data['return_code'] !== 'SUCCESS') { return '<xml><return_code>FAIL</return_code><return_msg>通信失败</return_msg></xml>'; } // 第二步:验签 $sign = $data['sign']; unset($data['sign']); $checkSign = $this->signParams($data, $this->qqApiKey); if ($checkSign !== $sign) { return '<xml><return_code>FAIL</return_code><return_msg>验签失败</return_msg></xml>'; } // 第三步:检查业务结果 if ($data['result_code'] !== 'SUCCESS') { return 'success'; } // 第四步:幂等处理,比对金额与订单状态 $order = OrderModel::findByTradeNo($data['out_trade_no']); if (!$order || $order->status !== 0) { return 'success'; } if (intval($data['total_fee']) !== $order->amount_fen) { return '<xml><return_code>FAIL</return_code><return_msg>金额不一致</return_msg></xml>'; } $order->status = 1; $order->transaction_id = $data['transaction_id']; $order->paid_at = date('Y-m-d H:i:s'); $order->save(); // 第五步:必须返回 success,否则平台会重复通知 return 'success'; }回调地址必须是公网可访问的 HTTPS 或 HTTP 地址,并且不能带自定义 header 鉴权,否则支付平台无法推送。调试阶段可以在入口处先把原始 XML 写入日志文件,因为后续验签失败时你不知道平台实际发了什么。
4. Alipay 集成:密钥体系、当面付下单与异步通知验签
4.1 支付宝的 RSA2 密钥与普通 MD5 签名不在一个量级
支付宝开放平台的签名机制和 QQ 支付完全不同。QQ 支付用的是对称密钥 MD5,支付宝用的是非对称 RSA2,具体是 SHA256WithRSA。商户需要在开放平台上生成应用私钥app_private_key和应用公钥app_public_key,同时把应用公钥上传给支付宝,支付宝会给你一个平台公钥alipay_public_key,这个平台公钥才是验签时用的。注意:上传的是应用公钥,不是应用私钥;私钥一旦泄露,任何人都可以伪造支付请求。
下单接口方面,页面扫码场景对应的是「当面付 precreate」,接口名alipay.trade.precreate。请求格式是 JSON 字符串,整体作为biz_content参数,外层再包一层公共参数。公共参数里的sign_type必须写RSA2,如果误写成RSA(SHA1),支付宝会直接拒绝请求。
4.2 当面付 precreate 下单代码示例
以下用 Python 请求库来演示,因为很多做数据分析的人会把支付回调接到 Python 服务里做二次处理:
import json import time import requests from urllib.parse import urlencode def alipay_precreate(order_id, amount_yuan, subject): biz_content = { "out_trade_no": order_id, "total_amount": amount_yuan, # 字符串,单位是元 "subject": subject, "timeout_express": "30m" } common_params = { "app_id": ALIPAY_APP_ID, "method": "alipay.trade.precreate", "format": "JSON", "charset": "utf-8", "sign_type": "RSA2", "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"), "version": "1.0", "biz_content": json.dumps(biz_content, ensure_ascii=False) } # 参数按 key 升序排列后拼接,需要注意这里公共参数的 sign 字段本身不参与 sign_str = "&".join(f"{k}={common_params[k]}" for k in sorted(common_params.keys())) common_params["sign"] = sign_with_rsa2(sign_str, APP_PRIVATE_KEY) gateway = "https://openapi.alipay.com/gateway.do?" + urlencode(common_params) resp = requests.get(gateway) result = resp.json()["alipay_trade_precreate_response"] if result["code"] == "10000": qr_code = result["qr_code"] # 这个值可以直接丢给前端生成二维码 return qr_code else: raise RuntimeError(f"下单失败: {result.get('sub_msg')}")注意total_amount单位是元,且是字符串类型,这是支付宝和 QQ 支付最大的差异。同一个业务系统同时接两家时,后端存储统一用分,出参时各自转换:给 QQ 支付转成整数分传给total_fee,给支付宝转成字符串元传给total_amount。最容易出错的点是把 QQ 支付的分直接当成支付宝的元传过去,导致一笔 199 元的订单变成 19900 元。timeout_express建议设置,否则默认是 15 天,二维码失效后用户扫码会提示订单已关闭但页面没有及时刷新。
4.3 异步通知的验签时序
支付宝的异步通知是 POST 表单格式,application/x-www-form-urlencoded编码。验签时需要把所有参数(除了sign和sign_type)按 key 升序拼接,然后用支付宝平台公钥做 SHA256WithRSA 验签。代码逻辑如下:
from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import padding def verify_alipay_notify(params: dict) -> bool: sign = params.pop("sign", "") params.pop("sign_type", None) raw_string = "&".join(f"{k}={params[k]}" for k in sorted(params.keys())) public_key = load_alipay_public_key() try: public_key.verify( base64.b64decode(sign), raw_string.encode("utf-8"), padding.PKCS1v15(), hashes.SHA256() ) return True except Exception: return False验签和业务处理是两件事。验签通过只代表这条通知确实来自支付宝,不代表订单金额正确、不代表订单号存在。验签通过后,还要检查trade_status是否为TRADE_SUCCESS,同时比对total_amount和out_trade_no是否和本地订单一致。TRADE_FINISHED虽然也表示交易完成,但当面付场景下一般只处理TRADE_SUCCESS,否则重复发货的问题很容易出现。业务处理完成后直接输出字符串success,注意不要加引号、换行、BOM 头,否则支付宝会认为通知失败并继续重发。
| 对比维度 | QQ 支付 Native | Alipay 当面付 |
|---|---|---|
| 参数名 | qpay_unified_order.cgi | alipay.trade.precreate |
| 签名算法 | MD5(对称) | RSA2/SHA256(非对称) |
| 金额单位 | 整数分 | 字符串元 |
| 通知格式 | XML | application/x-www-form-urlencoded |
| 成功应答 | XML 的 return_code=SUCCESS | 纯文本 success |
| 二维码来源 | code_url 字段 | qr_code 字段 |
这张表建议贴到项目 README 里,联调时对照着看,能省掉大量因为单位、格式不一致产生的问题。
5. 聚合收银台的分发逻辑与页面轮询状态
5.1 用 channel 参数统一路由
前端表单提交channel=qq或channel=alipay后,后端网关应该先根据 channel 字段做分发,而不是让页面感知到具体支付平台的存在。分发逻辑的核心是一个简单的工厂模式,控制器只负责拿到统一的返回结构:
$channel = $_POST['channel']; $orderId = $_POST['order_id']; $amountFen = intval($_POST['amount']); $factory = new PayChannelFactory(); $pay = $factory->make($channel); // 返回 QqPay 或 Alipay 实例 $result = $pay->createOrder($orderId, $amountFen); // 统一返回给前端 echo json_encode([ 'code' => 0, 'data' => [ 'qr_content' => $result['qr_code'], // 二维码原始值 'channel' => $channel, 'order_id' => $orderId ] ]);这样做的好处是:以后要加微信支付或者银联,只需要新增一个实现类,前端的收银台页面几乎不用改动,只扩展channel-tabs的li节点即可。二维码容器接收qr_content后用 qrcode.js 生成图片,而不是让后端返回图片 URL,避免后端缓存导致二维码过期后还显示旧图。
5.2 二维码过期与轮询机制
用户扫码之后到回调到达可能有几秒的延迟,页面需要轮询后端查询订单状态,不能干等。常见做法是每 3 秒请求一次订单查询接口,连续查 20 次仍未支付就提示用户二维码已失效。这是这套资源里最容易做成轮询卡死的地方——轮询不能同时存在多条定时器链:
var pollTimer = null; var pollCount = 0; function startPolling(orderNo) { stopPolling(); pollCount = 0; pollTimer = setInterval(function () { pollCount++; fetch('/api/pay/query?order_id=' + orderNo, { credentials: 'include' }) .then(function (res) { return res.json(); }) .then(function (data) { if (data.data.status === 'paid') { stopPolling(); window.location.href = '/pay/success?order_id=' + orderNo; } else if (pollCount > 20) { stopPolling(); document.getElementById('qrStatus').innerText = '二维码已过期,请刷新页面重试'; } }) .catch(function () { stopPolling(); document.getElementById('qrStatus').innerText = '网络异常,请检查连接'; }); }, 3000); } function stopPolling() { if (pollTimer) { clearInterval(pollTimer); pollTimer = null; } }轮询接口/api/pay/query在查库时要注意:本地订单状态是在异步通知里更新的,如果用户支付成功但回调延迟,轮询可能查到「待支付」状态。因此查询接口不能只读本地库,还要在查不到已支付记录时主动调用 QQ 钱包或支付宝的订单查询接口,以支付平台的结果为准。而查询频率要克制,3 秒一次是相对安全的间隔,1 秒一次容易触发支付平台的频率限制,导致接口返回限流错误。pollCount > 20时页面还不能直接引导刷新,要先把二维码区域清空,避免用户扫一个已经作废的码,支付后订单关单。
5.3 支付结果页的区分与回跳
window.location.href跳到/pay/success之后,不能完全信任这个 URL 参数。结果页需要再调用一次查询接口,拿到订单状态后才显示「支付成功」,否则用户手动改order_id就能看到别人的订单状态。回跳这个动作最好与浏览器前进后退解耦——从支付结果页返回到收银台时,上一步的轮询定时器必须已经被清理,这就是startPolling里先调用stopPolling的原因。
6. 回调链路不触发时,用抓包和日志定位问题
支付对接里最折磨人的场景:前台扫码付了钱,后台订单状态纹丝不动,而代码检查了无数遍没发现语法错误。这类问题绝大多数出在回调链路,而不是下单环节。这里给出一套可复现的排查思路。
第一步,确认回调地址是否公网可达。可以用curl -X POST -d '<xml><return_code>SUCCESS</return_code></xml>' https://你的域名/notify/qq先做一次模拟推送,看服务端是否返回预期响应。如果返回 502 或连接超时,说明 Nginx 或防火墙拦截了支付平台的 IP 段,需要放行。第二步,查看 Web 服务器访问日志,确认支付平台是否真的请求到回调地址。QQ 钱包和支付宝都有重试机制,如果日志里完全没有任何记录,说明通知根本没发出来,重点检查下单时notify_url参数是否写错,或者该 URL 在商户后台被重置过。第三步,在回调入口第一行记录原始报文,包括 header 和 body:
file_put_contents('/tmp/pay_notify.log', date('Y-m-d H:i:s') . PHP_EOL . file_get_contents('php://input') . PHP_EOL . json_encode($_POST) . PHP_EOL, FILE_APPEND );这个日志会暴露大部分问题:支付宝的回调如果走 form 表单提交,$_POST里有数据;QQ 支付的回调是 XML 原始体,只能从php://input读。很多人在 QQ 支付回调里去读$_POST,得到空数组后直接判定验签失败,其实只是读错了数据源。最后验证验签逻辑时,把sign字段打印出来,用手边的工具独立算一遍签名,对比平台推送的值是否一致。要注意以下几个坑:签名拼接时值为空的参数必须剔除;QQ 支付签名结果是大写 MD5,支付宝验签是 base64 解码后交给 RSA2 验证;调试阶段别开 PHP 的display_errors,返回给支付平台的响应体里多了Warning或Fatal,平台会认为处理失败,从而反复重试,把问题从「收不到通知」变成「接口被打爆」。
本文还有配套的精品资源,点击获取