简介:这是一套面向支付系统开发者与二次开发者的H5十四合一代付系统源码,基于PHP7.4与MySQL5.7环境运行,代码全开源,适合需要搭建聚合支付平台或进行功能定制的技术人员参考使用。资源包共112个文件,包含25个php核心程序文件、6个html页面、5个js脚本、2个css样式表以及1个sql数据库文件,另配有31张png与25张jpg图片素材,压缩包约9.46MB,结构完整便于部署。系统内置美团、京东、拼多多、滴滴、携程、抖音、淘宝、得物、飞猪、猫眼等14套模板,支持代理分成、用户提现、推广下级、商品上架审核及后台运营大屏,支付接口可对接总后台或由用户自行配置。本次更新修复了分享卡片显示异常、远程资源加载缓慢等问题,并将分享卡片配置集成至后台,支持自定义文字与图片。已有225人学习下载,适合具备一定PHP基础、希望快速搭建或二次开发代付平台的开发者参考。
1. 十四合一代付系统到底合了什么:从 H5 收银台到二次开发边界
很多人第一次听到「十四合一代付系统」会以为是十四个支付通道简单堆在一起,实际拆开看,它合的是十四类代付业务场景:单笔代付、批量代付、定时代付、审核后付、API 直连代付、H5 收银台代付、余额代付、佣金结算代付、退款回退、多商户分账代付、异步回调通知、对账文件生成、风控拦截、二次开发接口。这套源码的价值不在通道数量,而在于它把「商户下单 → 平台审核 → 通道扣款 → 回调通知 → 对账核销」这条链路做成了可插拔的模块,你拿到手后能按自己的业务改风控规则、换通道适配器、加商户层级。
适合谁看:手里有支付牌照或聚合支付资质的团队、需要给商户做代付结算的平台方、想学支付系统架构的后端工程师。不适合想直接上线跑资金的人——代付涉及资金安全,源码只是骨架,合规和风控得自己补。H5 端在这套系统里承担的是商户操作台和用户收银台两个角色,前者用 uniapp 封装成 H5 能同时指向两个域名做灰度,后者要处理 iOS 下载文件变预览、input 聚焦键盘顶起页面这些移动端老问题。下面按「先跑通最小闭环,再改通道,最后避坑」的顺序拆。
2. 把源码跑起来:环境、数据库与 H5 收银台最小闭环
2.1 环境选型与宝塔部署的取舍
这套源码常见做法是 PHP 后端 + MySQL + Redis + Nginx,前端 H5 用 Vue 或 uniapp 打包。服务器系统我一般选 CentOS 7.9 或 Ubuntu 22.04,宝塔面板能省掉装 Nginx、PHP、MySQL 的时间,但要注意宝塔默认装的 PHP 版本可能和源码要求的扩展不匹配。源码里通常需要bcmath、gd、redis、curl、openssl这几个扩展,缺一个代付签名就会报错。
部署前先确认三件事:PHP 版本(常见 7.4 或 8.0,看源码 composer.json)、MySQL 版本(5.7 够用,8.0 注意密码加密方式)、Redis 是否开启持久化。宝塔里建站点时把运行目录指向public,伪静态选thinkphp或laravel规则,具体看框架。数据库导入用 phpMyAdmin 或命令行都行,导入后检查config/database.php里的连接信息。
# 宝塔 SSH 里检查 PHP 扩展,缺哪个装哪个 php -m | grep -E 'bcmath|gd|redis|curl|openssl' # 导入数据库,注意替换库名和用户名 mysql -u root -p payment_db < /www/wwwroot/payment/sql/install.sql # 给 runtime 和 upload 目录写权限,否则代付回调日志写不进去 chmod -R 755 /www/wwwroot/payment/runtime chmod -R 755 /www/wwwroot/payment/public/upload参数说明:payment_db是源码里默认库名,导入前先在宝塔数据库页面建好同名库;runtime目录存的是代付请求日志和回调记录,权限不对会出现「下单成功但查不到订单」的玄学问题。Redis 配置在.env或config/cache.php,代付的幂等锁和验证码都靠它,别用文件缓存代替。
2.2 十四合一代付的数据库表结构与核心字段
代付系统的表设计决定了你能不能二次开发。常见核心表有merchant(商户)、order(代付订单)、channel(通道配置)、callback_log(回调日志)、settlement(结算记录)、risk_rule(风控规则)。order表里几个字段必须盯紧:order_no平台订单号、out_trade_no商户订单号、channel_code通道标识、amount金额(分)、fee手续费、status状态(0 待审核 1 处理中 2 成功 3 失败 4 退回)、notify_url回调地址、notify_status通知状态。
二次开发时最容易翻车的是金额单位。源码里如果amount存的是分,你前端传元就会差 100 倍;有些通道要求传元,适配器里得做转换。状态机也要看清,代付和代收不同,代付失败后资金退回是异步的,status=3不代表钱已经回来,得看settlement表。
-- 查一笔代付订单的完整链路,排查卡在哪个状态 SELECT o.order_no, o.out_trade_no, o.amount, o.fee, o.status, o.channel_code, o.notify_status, c.channel_name FROM `order` o LEFT JOIN channel c ON o.channel_code = c.code WHERE o.out_trade_no = '你的商户订单号'; -- 查回调日志,确认通道是否通知过 SELECT * FROM callback_log WHERE order_no = '平台订单号' ORDER BY id DESC LIMIT 5;逻辑说明:第一段 SQL 把订单和通道配置关联,能看出用的是哪家通道、通知状态是否成功;第二段查回调日志,如果通道通知了但notify_status=0,说明你的回调处理逻辑有 bug,常见是签名验证失败或返回内容不是通道要求的格式。
2.3 H5 收银台跑通第一笔代付
H5 收银台是商户或用户发起代付的入口。用 uniapp 封装 H5 时,如果要做灰度或双域名,可以在manifest.json里配h5.router.base,再在 Nginx 做两个 server 块指向同一套静态资源。跑通第一笔代付的步骤:配置一个测试通道 → 建测试商户 → 生成 API 密钥 → 用 Postman 或 curl 调代付接口 → 看回调。
# 调代付下单接口,参数按源码文档替换 curl -X POST 'https://你的域名/api/pay/create' \ -H 'Content-Type: application/json' \ -d '{ "merchant_no": "M10001", "out_trade_no": "TEST20250101001", "amount": 100, "bank_card": "6222020200112233445", "bank_name": "工商银行", "account_name": "张三", "notify_url": "https://你的域名/notify/test", "sign": "按源码签名规则生成" }'参数说明:amount单位看源码,多数是分;sign签名规则通常在app/common/library/Sign.php或类似文件里,常见是参数按 key 排序后拼key=商户密钥做 MD5 或 HMAC-SHA256。notify_url必须是公网可访问的,本地开发用内网穿透工具临时映射,但注意别把测试回调地址带到生产。
提示:第一笔代付建议用通道的沙箱环境,没有沙箱就用最小金额(比如 1 分)跑,确认回调、对账、状态流转都正常再放大金额。
3. 二次开发改哪里:通道适配器、风控规则与 H5 双域名封装
3.1 新增一个代付通道适配器的完整步骤
十四合一代付系统的扩展性体现在通道适配器上。常见目录结构是app/channel/driver/下每个通道一个类,实现pay()、query()、notify()、transfer()几个方法。新增通道时不要改核心逻辑,照着已有通道复制一份改。
<?php // app/channel/driver/NewChannel.php namespace app\channel\driver; class NewChannel { protected $config; public function __construct($config) { $this->config = $config; // 从 channel 表读的商户号、密钥、网关地址 } // 代付下单,返回通道订单号和原始响应 public function transfer($order) { $params = [ 'mch_id' => $this->config['mch_id'], 'out_trade_no'=> $order['order_no'], 'amount' => $order['amount'], // 注意单位,通道要元就 /100 'bank_card' => $order['bank_card'], 'notify_url' => $this->config['notify_url'], ]; $params['sign'] = $this->sign($params); $result = $this->httpPost($this->config['gateway'], $params); // 统一返回格式,核心逻辑只认 code/msg/data return [ 'code' => $result['ret_code'] == '0000' ? 1 : 0, 'msg' => $result['ret_msg'], 'data' => ['channel_order_no' => $result['channel_order_no']], ]; } // 签名,按通道文档改 protected function sign($params) { ksort($params); $str = urldecode(http_build_query($params)) . '&key=' . $this->config['key']; return strtoupper(md5($str)); } protected function httpPost($url, $data) { $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => http_build_query($data), CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, ]); $res = curl_exec($ch); curl_close($ch); return json_decode($res, true); } }逻辑说明:适配器只负责和通道通信,不碰订单状态。核心逻辑调transfer()拿到code=1后把订单置为处理中,等notify()回调再改成功或失败。参数说明:$this->config从channel表按channel_code读,新增通道时在后台加一条记录填商户号和密钥;amount单位转换是高频坑,通道要元就除以 100,要分就原样传。
3.2 风控规则怎么加才不影响正常代付
代付风控比代收更敏感,因为直接出钱。常见规则:单笔限额、单日累计限额、同一收款卡短时间多次、商户余额不足拦截、黑名单卡号。源码里风控通常在app/common/library/Risk.php或独立risk_rule表配。加规则时用「先记录后拦截」策略,别一上来就硬拦,否则正常商户会被误伤。
// 在代付下单前调用风控检查 public function check($order) { // 规则1:单笔限额,从 risk_rule 表读 $limit = Db::name('risk_rule')->where('type', 'single_limit')->value('value'); if ($order['amount'] > $limit * 100) { return ['pass' => false, 'msg' => '单笔超限']; } // 规则2:同一卡号 10 分钟内超过 3 次 $count = Db::name('order') ->where('bank_card', $order['bank_card']) ->where('create_time', '>', time() - 600) ->count(); if ($count >= 3) { return ['pass' => false, 'msg' => '该卡号操作过于频繁']; } // 规则3:商户余额是否够代付加手续费 $balance = Db::name('merchant')->where('merchant_no', $order['merchant_no'])->value('balance'); if ($balance < $order['amount'] + $order['fee']) { return ['pass' => false, 'msg' => '余额不足']; } return ['pass' => true]; }参数说明:single_limit存的是元,比较时乘 100 转分;create_time用时间戳,查询范围 600 秒。风控规则建议做成后台可配,别写死在代码里,否则每次调阈值都要发版。注意风控拦截的订单要记日志,方便商户申诉时查。
3.3 uniapp 封装 H5 指向两个域名的配置
热词里「uniapp 封装 h5 如何指向 2 个域名」是真实需求,常见于灰度发布或主备切换。做法是在manifest.json的h5节点配router.base,然后打包两份,Nginx 按域名分发。如果要在运行时动态切,可以用环境变量注入。
// config.js 根据当前域名返回不同 API 地址 const host = window.location.host; const apiMap = { 'pay.example.com': 'https://api1.example.com', 'pay-backup.example.com': 'https://api2.example.com', }; export const API_BASE = apiMap[host] || 'https://api1.example.com'; // 请求封装里用 API_BASE import axios from 'axios'; import { API_BASE } from './config'; const request = axios.create({ baseURL: API_BASE, timeout: 10000 });逻辑说明:window.location.host拿到当前访问域名,映射到对应 API 地址,这样同一份 H5 代码部署到两个域名能指向不同后端。参数说明:apiMap的 key 要和 Nginxserver_name一致;如果 H5 嵌在 App 里,window.location.host可能为空,得用plus.runtime或原生注入的方式传域名。
注意:H5 在 iOS 上下载文件变成预览是 Safari 的限制,代付对账文件下载建议用
window.open或后端返回 base64 让前端转 Blob 下载,别直接给文件 URL。
4. 代付系统避坑:回调、金额与状态机的血泪经验
4.1 回调通知收不到或重复处理
现象:通道显示代付成功,但平台订单还是处理中,或者同一笔回调被处理两次导致重复加款。原因:回调地址公网不可达、签名验证失败、回调处理没有幂等锁。解决:先用curl从外网访问回调地址确认通;签名验证失败看参数是否被 URL 编码影响;幂等用 Redis 锁,key 用order_no,处理前setnx,处理完删掉。
// 回调入口加幂等锁 $lockKey = 'notify_lock_' . $orderNo; if (!Redis::setnx($lockKey, 1)) { exit('success'); // 已处理过,直接返回成功避免通道重试 } Redis::expire($lockKey, 300); // ... 处理业务逻辑 Redis::del($lockKey);4.2 金额单位不一致导致多付或少付
现象:商户传 100 元,实际代付 1 元或 10000 元。原因:前端传元、后端存分、通道要元,三层单位没统一。解决:约定所有内部计算用分,只在调通道适配器时按通道文档转换,转换处加注释和单元测试。
4.3 状态机乱改导致对账对不上
现象:订单状态从处理中直接跳成功,但通道实际失败,对账时资金缺口。原因:回调处理没校验通道返回的最终状态,或者人工在后台乱改状态。解决:状态流转只允许通过回调或主动查询接口改,后台改状态要记操作日志,对账文件每天跑一次比对。
4.4 商户密钥泄露被刷代付
现象:商户密钥硬编码在前端或日志里,被人拿到后伪造代付请求。原因:密钥存在 H5 代码或runtime日志。解决:密钥只存后端,前端不碰;日志里签名和密钥脱敏;给商户加 IP 白名单。
4.5 数据库连接数打满
现象:代付高峰期报「too many connections」。原因:每个请求都新建数据库连接,或者慢查询堆积。解决:用连接池,检查order表的out_trade_no和create_time有没有索引,回调处理里的查询加缓存。
5. 进阶:用对账文件反查代付漏洞与二次开发验收清单
代付系统上线后,最有效的验证手段不是看订单列表,而是跑对账。通道一般提供对账文件下载接口或定时推送,格式常见 CSV 或 TXT。写一个脚本每天拉取,和本地order表比对,重点看三类差异:通道成功但本地处理中、通道失败但本地成功、金额不一致。
# 对账脚本核心逻辑,按通道文件格式调整 import csv from datetime import datetime def reconcile(channel_file, db_orders): diff = [] with open(channel_file, 'r', encoding='utf-8') as f: reader = csv.DictReader(f) for row in reader: order_no = row['out_trade_no'] channel_status = row['status'] # 通道侧状态 channel_amount = int(float(row['amount']) * 100) local = db_orders.get(order_no) if not local: diff.append(('本地缺失', order_no)) continue if channel_status == 'SUCCESS' and local['status'] != 2: diff.append(('通道成功本地未成功', order_no)) if channel_status == 'FAIL' and local['status'] == 2: diff.append(('通道失败本地成功', order_no)) if channel_amount != local['amount']: diff.append(('金额不一致', order_no)) return diff参数说明:channel_file是通道对账文件路径,db_orders是从数据库查的订单字典,key 用out_trade_no。差异结果要人工复核,尤其是「通道失败本地成功」这种,可能是回调伪造或状态机 bug。对账脚本建议每天凌晨跑,结果发到运维群。
二次开发验收清单:新增通道能否正常下单、查询、回调;风控规则是否可后台配置且生效;H5 双域名切换后 API 地址是否正确;回调幂等锁是否生效;对账脚本能否跑出差异;日志里有没有明文密钥。这几项过了,这套十四合一代付系统才算真正能接业务。
我自己踩过最深的坑是回调幂等没做,通道重试三次,商户被加了三次钱,最后人工追回。从那以后我习惯在回调入口第一行就加锁,宁可多写十行代码,也别留这种后悔药都没得吃的漏洞。希望帮到你。
本文还有配套的精品资源,点击获取