
简介这是一套面向网站开发者与电商运营人员的多支付集成源码集中解决网页端接入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虽然都定义了按钮但优先级和圆角半径不同。我一般会这样处理全局布局只保留 bootstrapamazeui 只引它独有的am-前缀组件业务样式的类名统一用pay-前缀从命名空间上避开覆盖。style.css 这个文件名太通用通常放的是页面级覆写建议打开后先搜body和.container看有没有把框架的默认间距改掉。2.2 收银台的 HTML 骨架长什么样支付页面的核心区域一般分为三段订单信息区、支付方式选择区、二维码/表单提交区。下面的结构是这套源码里最常见的组合方式div classcontainer pay-checkout div classorder-panel h3 classorder-title订单确认/h3 div classorder-amount span classamount-label应付金额/span strong classamount-value idpayAmount>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 统一下单请求的组装与签名后端收到前端表单提交的channelqq后需要组装统一下单参数并做 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的规则是先对参数按字典序升序排列拼成key1value1key2value2的字符串末尾追加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 xmlreturn_codeFAIL/return_codereturn_msg通信失败/return_msg/xml; } // 第二步验签 $sign $data[sign]; unset($data[sign]); $checkSign $this-signParams($data, $this-qqApiKey); if ($checkSign ! $sign) { return xmlreturn_codeFAIL/return_codereturn_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 xmlreturn_codeFAIL/return_codereturn_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如果误写成RSASHA1支付宝会直接拒绝请求。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_asciiFalse) } # 参数按 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 支付 NativeAlipay 当面付参数名qpay_unified_order.cgialipay.trade.precreate签名算法MD5对称RSA2/SHA256非对称金额单位整数分字符串元通知格式XMLapplication/x-www-form-urlencoded成功应答XML 的 return_codeSUCCESS纯文本 success二维码来源code_url 字段qr_code 字段这张表建议贴到项目 README 里联调时对照着看能省掉大量因为单位、格式不一致产生的问题。5. 聚合收银台的分发逻辑与页面轮询状态5.1 用 channel 参数统一路由前端表单提交channelqq或channelalipay后后端网关应该先根据 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 xmlreturn_codeSUCCESS/return_code/xml https://你的域名/notify/qq先做一次模拟推送看服务端是否返回预期响应。如果返回 502 或连接超时说明 Nginx 或防火墙拦截了支付平台的 IP 段需要放行。第二步查看 Web 服务器访问日志确认支付平台是否真的请求到回调地址。QQ 钱包和支付宝都有重试机制如果日志里完全没有任何记录说明通知根本没发出来重点检查下单时notify_url参数是否写错或者该 URL 在商户后台被重置过。第三步在回调入口第一行记录原始报文包括 header 和 bodyfile_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平台会认为处理失败从而反复重试把问题从「收不到通知」变成「接口被打爆」。本文还有配套的精品资源点击获取