逸轩小微支付系统源码拆解:支付闭环、回调验签与上线避坑 简介面向微信/支付宝服务商的小微商户进件支付系统2022更新修复版全开源源码包适合有PHP开发基础、需要快速搭建或二次开发聚合支付平台的开发者与中小企业。系统覆盖小微商户进件、支付渠道配置、订单管理等核心环节源码完整开放便于结合实际业务深度定制。压缩包整体约195.6MB上游未提供文件明细暂不细分文件类型核心代码以PHP为主已包含部署所需的基础配置说明与安装引导内容。目前已有376人学习/下载适合作为支付系统二次开发的学习蓝本或生产环境部署参考。需注意运行环境要求PHP 7.2Redis扩展与MySQL 5.7及以上下载后对照配置要求调整服务器环境可显著减少部署与排错成本。1. 一套2022年更新修复过的逸轩小微支付系统源码凭什么现在还能用去年给一个做小超市的朋友搞收银台需要一套能被顾客扫码、能挂在柜台上亮着二维码、还要能随时看订单记录的支付系统。找来找去最后落到这套名称很长的东西上2022更新修复版本逸轩小微支付系统源码全开源版。名字长但核心就四个字源码、开源。它是一套PHP写的支付系统数据库是MySQL。朋友的第一反应是2022年的东西现在用会不会太老我的原话是支付系统的核心不在框架新旧而在支付链路是否闭环。这套源码把“生成订单、展示二维码、异步回调、订单状态落库、查单补单”这条闭环完整做出来了而且因为代码没有过度封装新手把文件拖进本地就能一步步改。适合两类人一是中小商户想快速搭一个独立收银台二是刚入行的开发者想拿真实支付流程练手。这篇文章就把这套源码从头拆到尾——从表结构怎么设计到本地怎么跑起来、回调验签怎么写再到上线前需要提前避开的那些坑。2. 逸轩小微支付系统里的订单链路与核心表设计从支付单生成到回调落库老支付系统最值得看的不是页面而是数据怎么流转。这一章先跟着一笔订单从头走到尾再落地看三张表最后说清楚状态机为什么能让重复通知失效。2.1 一笔支付单的完整生命周期预生成订单、扫码支付、异步回调、终态更新先梳理主流程。用户打开收银台时系统做的第一件事不是调起支付而是在本地插入一笔订单订单号、商户号、实付金额、状态待支付都落进pay_order表。随后后端把订单号和金额拼成支付平台的预支付参数生成一个二维码链接。用户扫码后微信或支付宝唤起确认支付。确认完成后支付平台会向系统配置的异步通知地址发起回调。收到回调服务端先验签名再比对订单金额确认无误后把订单状态改成已支付并记录支付平台流水号。最后用户前端可以有个查询动作看到订单变成“已支付”。这里有个新手常踩的坑把同步跳转当作状态依据。用户扫完码浏览器会跳回商户页面跳转URL里带着支付结果参数很多人图省事直接在这个入口更新订单状态。结果用户没等页面跳转就关掉浏览器订单永远卡在待支付。真实链路里同步跳转只负责给用户看一眼不做据信状态必须由异步回调兜底。这套2022修复版在回调处理里专门加固了验签逻辑和状态处理顺序这也是“更新修复”这个后缀背后最实在的部分。从开发角度看这条链路里最值钱的是两个动作一是生成订单时就要把status设为0并且订单号必须全局唯一二是回调处理进程里所有“改状态”的SQL都要带上AND status0。后面第4章会展开讲为什么。2.2 三张核心表商户表、订单表、支付日志表怎么设计老代码里藏着哪些讲究拆开这套源码后你会发现页面文件很多但真正决定支付闭环不出错的其实就三张表。我把它们的结构整理成了一份通用版本。注意这不是原包逐字段抄录而是这类系统在常见实现里最稳定的形态。CREATE TABLE merchant_info ( id int(11) NOT NULL AUTO_INCREMENT, appid varchar(32) NOT NULL COMMENT 平台分配的AppID, merchant_no varchar(32) NOT NULL COMMENT 商户号, secret_key varchar(64) NOT NULL COMMENT API密钥, status tinyint(1) DEFAULT 1 COMMENT 1启用 0禁用, create_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_merchant_no (merchant_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE pay_order ( id int(11) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 系统内订单号, platform_order_no varchar(64) DEFAULT NULL COMMENT 支付平台流水号, merchant_no varchar(32) NOT NULL COMMENT 所属商户号, amount decimal(10,2) NOT NULL COMMENT 用户实付金额, status tinyint(1) NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已关闭 3异常, notify_url varchar(255) DEFAULT NULL COMMENT 异步通知地址, create_time datetime DEFAULT NULL, pay_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_merchant_status (merchant_no,status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE pay_log ( id int(11) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL, trace_content text COMMENT 请求或回调的原始报文, trace_type varchar(16) NOT NULL COMMENT notify/query/refund, add_time datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;三张表各干各的。merchant_info存接入方信息一套系统里可能有多个商户号每个商户号有自己的密钥。pay_order是主订单表所有支付相关状态的变更都在这里。pay_log是审计表存请求和回调的原始报文出了问题能翻出黑匣子。几个值得注意的细节金额字段用decimal(10,2)绝对不要用float否则月底对账时会冒出一分钱的差价。订单号建了唯一索引这不是可有可无是并发条件下防止重复订单的下限。pay_log.trace_content用text类型存原始报文虽然占空间但当你需要排查“平台到底发来了什么”时它是唯一的后悔药。status字段只用四个数字0待支付、1已支付、2已关闭、3异常。老代码这种克制很见功底对比一些新系统用字符串如success、failure数字状态更不容易写错。2.3 回调与状态机的映射重复通知为什么不会让你多收钱微信和支付宝的异步通知会在一段时间内重复发送而且不保证顺序。这套系统里处理重复的逻辑特别简单只有当当前状态是0待支付时才允许把订单改成1已支付。我把它抽成一个典型代码片段if ((int)$order[status] 1) { // 已经被处理过直接返回success不再执行业务逻辑 exit(success); } // 更新订单状态条件里必须带 status0 $db-query( UPDATE pay_order SET status1, platform_order_no{$platformNo}, pay_timeNOW() WHERE order_no{$orderNo} AND status0 ); if ($db-affected_rows() ! 1) { // 并发或重复通知导致更新不到记录日志 logTrace($orderNo, duplicate_notify); exit(success); }这里的精妙之处在 UPDATE 语句本身。即使两个回调同时进入数据库层的行锁也能保证只有一个请求的affected_rows为 1另一个拿到 0自然不会再做一遍加余额动作。如果业务里还给商户账户加钱了这个“加钱”动作必须和“订单状态更新”放在同一个事务里不能分开。分开意味着状态改成功了但钱没到账或者钱到了但订单状态没改——这种不一致比丢单更难排查。状态机到这里只是最简单的一层实际还包含“2已关闭”“3异常”。这两个状态不需要从支付平台反向感知而是由本地定时任务决定超过一定时间没回调且查单失败订单从0置为2或3。所以“只信回调”还不够下一节开始讲部署部署完你会更清楚补单为什么存在。3. 把全开源版跑在本地PHP 7.4、数据库导入、回调地址暴露到公网这一章解决“怎么让它转起来”。我不推荐直接用生产环境试错先在本地把配置弄明白再上公网。3.1 环境与依赖清单我为什么坚持用 PHP 7.4 而不是 8.x2022年这代源码底层常见是基于 ThinkPHP 5 或 CodeIgniter 3 一类的老框架。这类框架对 PHP 8 的兼容性并不可靠最典型的问题是方括号语法、魔术方法提示不兼容导致白屏或者函数未定义。我做这套系统时统一使用 PHP 7.4 MySQL 5.7 或 MariaDB 10.3。注意 PHP 7.4 官方已停止维护但在这种场景里稳定运行比版本新鲜更重要。装好之后先确认扩展。打开终端执行php -v php -m | grep -E curl|openssl|pdo_mysql|mbstring|json|gd如果php -m输出的列表里缺了 curl 或 openssl支付签名和回调的SSL验证会直接趴窝。缺 pdo_mysql 数据库连不上缺 mbstring 中文乱码缺 json 可能连 OpenAI 风格的新接口都用不了这个不一定看你接到哪个支付渠道。总之五个扩展一个不能少。使用 Linux 面板的在 PHP 设置里把这几个扩展勾上然后重载Windows 本地用 phpStudy 或 Laragon 的装完记得在 php.ini 里确认extension_dir路径和curl.cainfo路径。尤其是curl.cainfo漏配时回调验签阶段 openssl 会报unable to get local issuer certificate导致验签直接失败。3.2 导入数据库并修改配置文件五个关键项改错一个就白屏这套源码的包里一般会带一份.sql文件。用命令行导入比在 phpMyAdmin 里导入更稳定尤其当 SQL 文件超过 20MB 时图形界面经常超时。mysql -uroot -p -e CREATE DATABASE IF NOT EXISTS yixuan_pay DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; mysql -uroot -p yixuan_pay /path/to/yixuan_pay.sql第一行创建数据库第二行导入数据。utf8mb4是必须的否则生僻字和 emoji 会变成问号。导入成功后用编辑器打开数据库配置文件。ThinkPHP 类源码通常在application/database.php原生 PHP 源码通常是一个config.php。把数据库地址、用户名、密码、库名改成本地的。除了数据库支付相关配置一般集中在一个数组里?php // config/pay.php return [ appid 你的appid, mch_id 你的商户号, key 32位API密钥, notify_url https://你的域名/api/notify, log_path /var/www/yixuan-pay/runtime/logs/, ];五个配置项缺一不可。appid是支付平台分配的应用IDmch_id是商户号key是API密钥notify_url是异步通知的入口log_path是日志目录。注意key不要写成大写常量老代码里如果把它写成打包里的默认值等于向全互联网公开了密钥。另外注意看根目录入口文件的位置。很多老源码的 Web 入口在public/index.php虚拟主机需要把站点根目录指到public否则会暴露框架目录文件。如果服务器不支持改根目录就需要在public下放一个.htaccess或 Nginx 伪静态。3.3 用 Nginx 伪静态和公网穿透让回调地址能真正敲开门这套系统如果带着index.php访问支付回调地址会变成https://域名/index.php?s/api/notify。支付平台对这类带路由入口的URL容忍度较低而且你的部分拦截逻辑可能只针对/api/notify。因此需要一条伪静态规则把/api/notify直接转发到入口文件。Apache 下把.htaccess放到public目录Nginx 下在站点配置里加一段location / { if (!-e $request_filename) { rewrite ^/(.*)$ /index.php?s/$1 last; break; } }rewrite的作用是把凡是不存在的文件路径都交给index.php?s/$1处理。last表示终止当前规则集重新走一遍 location 匹配。配完nginx -s reload后访问/api/notify应该能正确到达控制器。回调地址必须公网可访问。本地调试时最简单的做法是使用内网穿透把本地端口暴露为公网 HTTPS 地址我用过最顺手的还是 ngrok./ngrok http 8080启动后会出现一个https://xxxx.ngrok.io的地址。把notify_url填成https://xxxx.ngrok.io/api/notify。然后去支付平台后台把授权回调域名也改成它。这里有一个血泪经验支付平台会缓存你首次配置的域名如果你先用 http 测试之后再改成 https前几次回调可能仍走 http导致证书验签失败。所以从第一秒起就直接用 https 地址不要来回切。如果你在公司内网TCP 协议的 frp 也能承担同样角色总之原则是回调地址必须稳定、公网可达、带合法证书。4. 在“2022修复版”里学最值钱的回调验签签名顺序、金额统一、自动补单这一章是我认为整套系统含金量最高的区域。很多支付系统的翻车不在下单而在回调验签。4.1 微信支付V2签名为什么容易翻车排序、空值、字符集这套源码大概率走的是微信支付V2接口。V2签名算法是 MD5核心是“参数名ASCII字典序排序、排除空值和 sign 本身、最后拼接 key 做 MD5”。看似简单实际总有人栽跟头。function makeSign(array $params, string $key): string { ksort($params); $stringA ; foreach ($params as $k $v) { if ($v || in_array($k, [sign, key])) { continue; } $stringA . $k . . $v . ; } $stringSignTemp $stringA . key . $key; return strtoupper(md5($stringSignTemp)); }注意两点$v 是严格判断。如果从请求里解析的参数值是null用$v 会把null判成空但拼接时null会变成空字符串问题不大但如果用in_array处理时$v是数组则会悄悄变成字符串Array签名永远对不上。另一个坑是字符集平台回调里的中文参数比如attach如果你的程序以 ISO-8859-1 读取再原样拼进签名MD5 结果必然和平台不一致。所以拿到回调内容后第一件事是把所有值显式转成 UTF-8。支付宝这边又不同用的是 RSA2 验签需要加载支付宝公钥。老代码常因公钥字符串里异常混入换行或空格导致openssl_verify返回 false。处理方式是把公钥里BEGIN和END行去掉拼接成一行再去掉所有空格。4.2 回调验签的标准顺序先验商户再验签最后改状态我见过太多代码把金额校验放在验签前面结果攻击者随便伪造一个请求就能慢慢探测你的订单金额。正确的顺序是先认身份再验签名然后查订单再处理重复最后才对金额并落库。一个典型的回调处理函数如下function handleNotify(array $params) { $merchant getMerchantByAppid($params[appid]); if (!$merchant) { logTrace(unknown_appid, json_encode($params)); exit(success); } if (!verifySign($params, $merchant[secret_key])) { logTrace(sign_error, json_encode($params)); exit(fail); // 让平台稍后重试 } $order getOrderByOrderNo($params[out_trade_no]); if (!$order || $order[merchant_no] ! $merchant[merchant_no]) { logTrace(order_not_match, json_encode($params)); exit(fail); } if ((int)$order[status] 1) { exit(success); } if ((int)$params[total_fee] ! (int)round($order[amount] * 100)) { logTrace(amount_not_match, pay . $params[total_fee] . db . $order[amount]); exit(fail); } // 到这里才更新状态 finishOrder($order[order_no], $params[transaction_id]); }第一步按appid查出商户查不到说明这个请求根本不是发给你的直接返回success不让平台反复重试。第二步验签不通过就返回fail让平台重试。第三步查订单并比对商户号防止一个平台的某个商户号混到另一个商户号下。第四步是幂等拦截状态已是已支付就确认成功。第五步才做金额比对这里用整数分比较避免浮点误差。最后统一调finishOrder把订单状态更新和加余额放在同一个事务里。注意exit(fail)和exit(success)的大小写。支付平台文档要求返回小写字符串多一个空格都算失败。很多老源码在这里写echo success; die;没问题但如果你用了框架的响应输出框架可能额外输出换行符导致平台误判。直接用exit(success)最干净。4.3 补单任务平台回调丢了订单必须靠主动查询捞回来异步通知即使配上重试机制也会在连续失败多次后停止。所以不能用“回调没来就永远待支付”。这套系统上线后我总会加一个定时任务扫描“创建超过10分钟仍待支付”的订单主动调用支付平台的查单接口确认真实状态。?php // 每5分钟执行一次检测10分钟前创建的待支付订单 $startTime date(Y-m-d H:i:s, strtotime(-10 minutes)); $orders $db-query( SELECT order_no, merchant_no FROM pay_order WHERE status0 AND create_time {$startTime} )-fetchAll(); foreach ($orders as $order) { $result queryPlatformOrder($order[order_no]); if ($result[trade_state] SUCCESS) { finishOrder($order[order_no], $result[transaction_id]); logTrace($order[order_no], repair_success); } }这里有两个要点。第一查单接口要记录到pay_log这样你能看到哪些单是被补单程序修复的对账时心里有数。第二补单完成时不要自己伪造回调参数也不要重复实现一套订单更新逻辑而是复用和回调处理同一个finishOrder。如果两处逻辑分离早晚会出现一处有事务一处没有的差异。定时任务建议每5分钟一次频率再高容易触发支付平台风控。放到 crontab 里就是*/5 * * * * /usr/bin/php /var/www/yixuan-pay/scripts/order_repair.php /var/www/yixuan-pay/logs/repair.log 21的意思是追加输出21把标准错误也写到同一个日志文件排错时能看清到底有没有执行成功。5. 上线时常见的5个翻车现场逸轩小微支付系统排查手册这一章直接给结论。每一段按“现象 → 原因 → 解决”的顺序展开都是我真金白银踩过或看别人踩过的。5.1 回调地址一直是 index.php 入口订单支付了却一直不动现象用户扫码支付成功但页面刷新后订单还是待支付后台日志里没有任何回调记录。原因Nginx 没有配置伪静态回调 URL 是https://域名/index.php?s/api/notify。平台确实发起了请求但路由解析时不能正确匹配到控制器或者返回的状态码不是可识别的成功标志平台重试几次就放弃了。解决按第3.3节配置 rewrite然后重启 PHP-FPMsystemctl restart php-fpm。接着在浏览器里直接访问回调地址看能否出现一个空白页或返回字符串如果能正常路由再重新在支付平台后台修改一次回调地址强制平台刷新缓存。5.2 回调日志显示金额比对失败支付平台返回的 total_fee 是分数据库是元现象pay_log里记录着amount_not_match pay100 db1.00用户已经付了钱系统却认为金额不对拒回fail平台反复重试。原因微信回调的total_fee是以“分”为单位的整数数据库表里存的是以“元”为单位的decimal(10,2)代码里直接写if ($params[total_fee] $order[amount])100和1.00在 PHP 的弱比较下也会相等但如果你用了强比较就永远不成立。另一种隐蔽情况是 ORM 把amount读成字符串1.00浮点运算后强转 int 的时机不对导致比较结果不统一。解决统一在比较前转成整数分(int)$params[total_fee] ! (int)round($order[amount] * 100)。这里用round是为了防止数据库浮点精度问题虽然decimal本身精确但 PHP 的$order[amount]有时会被 ORM 转成 string直接强转成 int 会把1.00变成1。正确的顺序是先做乘法再四舍五入最后强转。5.3 同一笔订单被回调两次商户余额被加了两次现象用户只付了一次款后台账目却出现两条入账记录总额翻倍。原因回调处理没有做幂等。第一次回调进来订单状态从0改成1并加余额第二次回调进来代码没有检查状态又加了一次余额。虽然第2章讲了带AND status0的更新方式但有些人在写“给商户账户加余额”时没有和订单状态更新放在同一个事务里。事务回滚时状态更新回滚了余额却已提交最后表现为“钱加了状态没改”看起来像重复加钱。解决把两个操作包进同一个事务并且用UPDATE pay_order SET status1 ... WHERE status0的受影响行数来判断是否继续加余额。如果受影响行数为 0直接exit(success)。另外在商户钱包流水表里给order_no建唯一索引这是最后一层保险。有了唯一索引即使代码逻辑漏了数据库也会拒绝重复插入。5.4 生产环境日志目录写不进任何内容权限还是open_basedir的锅现象支付回调日志、错误日志全部空白但是程序运行正常。原因PHP-FPM 的运行用户是www而日志目录的属主是root:root权限 755www用户无法创建文件。还有一种情况是 PHP 配置文件限制了open_basedir只允许脚本访问某些目录日志目录不在白名单内PHP 的file_put_contents会静默失败或者抛出一个Permission denied但被框架吞掉。解决检查日志目录的属主和权限至少执行chown -R www:www /var/www/yixuan-pay/runtime/ chmod -R 755 /var/www/yixuan-pay/runtime/然后在php.ini里找到open_basedir把日志目录加进去重新加载 PHP-FPM。注意如果使用了宝塔面板面板本身会重设权限你需要确认在“网站-设置-防跨站攻击”里没有把runtime目录拦在外面。5.5 回调地址配置成 HTTPS 但证书是自签名平台回调直接被拒现象本地内网穿透测试一切正常换上线域名后日志里没有任何回调请求。原因支付平台的服务器会验证回调地址的 HTTPS 证书是否可信。自签名证书、过期证书、或者证书链不完整都会导致平台在 SSL 握手阶段就拒绝连接你的程序一个字符都收不到。另外回调地址不能是裸 IP必须是域名。解决上线前给域名配上受信任的 CA 证书。用 Let’s Encrypt 就能解决或者用云厂商的免费证书。配好后在支付平台后台把回调地址重新保存一次。保存后平台一般不会立刻生效常见会有1到5分钟缓存不要反复改。测试时可以用curl -I https://你的域名/api/notify看返回状态是否是 200并检查证书签发机构。6. 上生产前用 0.01 元订单把验证路径走通回调日志与对账脚本最后这一章不是总结是一套我每次都要做的验证路径。你按这个顺序走一遍能躲开大部分“看起来能跑、一上线就挂”的问题。6.1 第一遍手工测试付款金额改成 0.01登录后台或者直接改数据库把测试商品的金额改成 0.01 元然后走完整流程创建订单 → 扫码 → 支付 → 观察回调日志 → 查看pay_order表状态。这个动作看起来笨却是最直接的。0.01 元能暴露环境配置问题、回调地址问题、验签参数问题比什么单元测试都管用。支付成功后立刻查看订单状态SELECT order_no, status, amount, platform_order_no, pay_time FROM pay_order ORDER BY id DESC LIMIT 5;如果status1整个主链路通了。如果还是 0去看回调日志。6.2 第二遍看回调日志确认每一个请求都真实记录把日志落到统一目录后用tail实时观察回调入口是否命中tail -f /var/www/yixuan-pay/runtime/logs/notify.log正常你会看到类似sign_verify_success的记录。如果看到sign_error把trace_content里的原始报文和本地生成的签名放进同一个脚本里对拍别用肉眼。这一步能快速区分是签名算法问题还是参数解析问题。6.3 第三遍写一个对账脚本别等月底才承认自己没对账我吃过一次亏上线一个月后平台对账单出来了差了一笔钱查了半天发现是某个订单在回调到来前订单号被手误改动。后来我养成了一个习惯每天凌晨跑一次对账脚本。逻辑非常简单从支付平台后台下载前一天的交易账单与本地订单表中pay_time在前一天且status1的订单做差集比对。差集不为空就告警。下面是一个 PHP 命令行脚本的骨架?php $remoteOrders downloadRemoteBill($date); // 返回订单号金额数组 $localOrders $db-query(SELECT order_no, amount FROM pay_order WHERE status1 AND pay_time BETWEEN {$date} 00:00:00 AND {$date} 23:59:59)-fetchAll(); $diff []; foreach ($localOrders as $local) { if (!isset($remoteOrders[$local[order_no]])) { $diff[] $local[order_no]; } } if ($diff) { error_log(对账差异: . implode(,, $diff)); }这个脚本没有任何高深技术但它能让你在月底不被财务追着跑。只要差异不为空要么是平台漏了报要么是你本地多了单不管哪一种都值得立刻查。处理完差异后把pay_log里对应的repair_success和手工调用的finishOrder全部检查一遍确认无遗漏。最后说一个我的习惯每次改完签名逻辑我不会只测一种支付方式而是把微信和支付宝各跑一遍 0.01 元订单因为两家的验签算法、参数名、回调重试策略完全不同。老系统里的“2022更新修复版”靠谱不靠谱不在于它是不是新而在于你有没有把回调验证的顺序、幂等事务、补单任务、每日对账这四件事做扎实。做完这四件事你再去接任何支付渠道都不会慌。希望帮到你。本文还有配套的精品资源点击获取