
简介面向个人站长与开发者的码支付系统源码定位为免签约、三网免挂的支付对接方案解决原生码支付依赖云端、回调延迟、额度受限等问题适合具备PHP与服务器运维基础的读者部署或二次开发。包体共476个文件、约17.55MB以109个PHP业务文件为核心搭配76个CSS与72个JS构建前台交互另有gif/png图片资源及SQL、pem等配置部署文件目录结构完整便于快速搭建。功能上支持微信商业码、H5开关、语音提示、邮箱提醒及支付宝H5备注全部回调走本地任务去除云端限制有效提升支付通知速度同时附带后台入口与默认账号可直接登录管理面板测试。目前已有8237人学习下载适合需要搭建个人免签约支付平台或研究码支付实现的读者参考。1. 码支付源码、三网免挂、本地回调这套支付方案到底在解决什么问题做个人项目接支付最烦的不是生成收款码而是用户到底付没付钱这件事不可控。用码支付这类聚合收款方案回调通知默认走第三方中转高峰期延迟十几秒、偶尔丢单。把码支付源码自己部署到服务器配合三网免挂监听端和本地回调等于把支付成功这个信号从别人手里挪回自己手里时延到秒级订单能和本地数据库对上账。文章按部署顺序写先讲三网免挂和本地回调在链路里各干哪段活再说 PHP 源码落地的环境、建表、Nginx 三关给出本地回调接口的签名、验签、幂等完整实现最后用 curl 模拟回调验证链路点出时钟、金额、响应体三个翻车点。适合自己维护网站、小程序、游戏服、想用个人收款码收小额入账的开发者以及要把回调接进内部系统的运维和全栈工程师。2. 码支付三网免挂的链路机制从扫码支付到本地回调通知先把链路里的角色分清用户手机上的微信、支付宝是收钱入口监听端是一台常开的 Android 设备码支付服务端是处理订单状态的中枢商户服务器是你自己部署源码和回调接口的地方。三网免挂解决的是服务端怎么知道钱到了本地回调解决的是服务端知道了以后怎么可靠地告诉你。2.1 免挂的真正含义通知监听代替前台挂机码支付方案里收款确认依赖监听端。传统做法是让手机保持亮屏、App 保持前台微信或支付宝收到钱后监听端截屏或读无障碍节点识别金额这就是挂机。挂机的毛病很直接屏幕亮一整晚伤屏耗电进程被系统回收一次就漏单一通电话进来把 App 挤掉后面一晚上都收不到钱。三网免挂把识别逻辑从前台界面挪到系统通知栏。监听端注册成 NotificationListenerService微信、支付宝在收款成功时弹通知系统会把通知分发给所有已授权的监听服务——这时候 App 在前台还是后台、有没有熄屏都不影响通知到达。所谓三网指监听端跑在移动、联通、电信的手机数据网络上长连接在 4G/5G 和数据漫游之间切换时要做断线鉴权重连免挂就是说这套机制不再依赖屏幕常亮和前台进程。Android 端一个最小监听实现大致是这样public class PayListener extends NotificationListenerService { Override public void onNotificationPosted(StatusBarNotification sbn) { String pkg sbn.getPackageName(); if (!com.tencent.mm.equals(pkg) !com.eg.android.AlipayGphone.equals(pkg)) { return; } Bundle extras sbn.getNotification().extras; String title extras.getString(Notification.EXTRA_TITLE); String text extras.getString(Notification.EXTRA_TEXT); if (text ! null text.contains(收款)) { Matcher m Pattern.compile(([\\d.])元).matcher(text); if (m.find()) { report(pkg, m.group(1)); } } } }代码先按包名过滤只处理微信和支付宝的通知再从 extras 里拿标题和正文微信收款通知正文通常形如微信支付凭证 / 收款金额0.01 元。正则抓出金额后交给 report() 上报。两个细节要注意一是 Android 8.0 之后要动态请求通知监听权限用户必须手动去系统设置里打开通知使用权否则 onNotificationPosted 永远不会被调二是部分国产 ROM 会强杀后台进程需要在系统管家里把监听端的自启动和后台运行放行这一步是免挂能免到多彻底的分水岭。2.2 监听端的通知解析与上报协议解析完通知后监听端并不把整个通知文本丢给服务端而是按约定上报四个字段渠道类型 typealipay/wechat/qqpay、金额 money、设备标识 device_id、本地时间戳。服务端拿到上报后先去待支付订单里找等额订单——这里带出一个业务约束同一商户同一时刻金额相同的待支付订单不能堆太多否则会撞单。常见做法是下单时给订单号加随机尾巴或者把商品定价从分上做开让同金额撞车概率降下来。上报接口本身是 HTTP POST 加简单鉴权客户端带固定 token服务端校验 token 和来源。我一般会把上报接口和对外开放的下单 API 分开跑避免监听端流量把商户 API 日志刷爆。上报请求到达后服务端按渠道 金额 待支付三个条件命中订单命中后立刻把订单状态改成已支付再进入回调环节。如果上报金额在订单表里查不到匹配项说明用户付了钱但商户后台没对应订单这类孤儿记录要单独落表事后人工核对。2.3 服务端收到上报后的回调时序订单确认后码支付服务端向商户下单时预留的 notify_url 发起 HTTP 回调完整时序如下用户扫码支付完成微信/支付宝弹出支付成功通知。监听端抓到通知解析出渠道和金额POST 上报到码支付服务端。服务端匹配待支付订单标记已支付记录实付金额和渠道。服务端向商户服务器的 notify_url 发起异步回调参数携带订单号、金额、签名。商户接口验签、核对订单金额、改本地订单状态返回 success。服务端收到 success停止对该订单的重试。本地回调的含义就在第 4 步notify_url 指向的是你自己服务器上的接口回调请求由你部署的码支付服务端直接发起中间不进任何第三方中继。链路短的直接好处是回调来源可控、签名规则可查、重试节奏由自己这台服务器说了算。这也意味着码支付服务端的回调模块必须把日志开全后面排错全靠它。提示如果只用第三方提供的公共托管服务回调地址规则和重试节奏由对方定丢单了你既看不了服务端日志也干预不了重试间隔。这也是很多人愿意自己部署一份源码的根本原因。3. 码支付 PHP 源码的本地部署环境、建表与 Nginx 启动拿到手的码支付源码绝大多数是 PHP 源码也有 Java 和 Node 版本但个人部署成本最低、后面改签名最顺手的还是 PHP MySQL 这套。目标环境我一般用 Debian/CentOS PHP 7.4 MySQL 5.7内存 1GB 的小机器跑这个绰绰有余。3.1 拿到源码后先看这三样东西解压压缩包后别急着传服务器先在本地过三样install.sql 或 codepay.sql 建表脚本确认里面有没有订单表config.php 或 .env 配置文件看数据库连接、商户密钥、notify_url 写在哪个文件入口文件位置是原生 PHP 按模块分目录还是 ThinkPHP 这类框架的 public 目录结构。我一般会先在本地对所有 PHP 文件做一次语法检查传输损坏的压缩包很容易在某个文件上冒出 parse error等传上服务器再查就多绕一圈find . -name *.php -exec php -l {} \; | grep -v No syntax errorsfind 把目录下所有 .php 文件逐个交给 php -l 做语法检查grep -v 反选掉正常的 No syntax errors 行剩下输出的就是有问题的文件。这一步在本地做完服务器上就不会遇到页面白屏、日志里只有 PHP Parse error这类最基础的问题。3.2 数据库初始化与支付订单表设计建库、导表结构可以一口气做完mysql -uroot -p -e CREATE DATABASE IF NOT EXISTS codepay DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; mysql -uroot -p codepay install.sqlutf8mb4 是为了让商品名这类中文和特殊字符都能落库。订单表是整个源码的核心一个能扛住回调幂等的表结构大致是这样CREATE TABLE pay_order ( id int(11) NOT NULL AUTO_INCREMENT, out_trade_no varchar(64) NOT NULL COMMENT 商户订单号, trade_no varchar(64) DEFAULT NULL COMMENT 码支付交易号, pid int(11) NOT NULL COMMENT 商户ID, type varchar(10) NOT NULL COMMENT 支付渠道 alipay/wechat/qqpay, name varchar(64) NOT NULL DEFAULT COMMENT 商品名称, money decimal(10,2) NOT NULL COMMENT 订单金额(元), trade_status tinyint(1) NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付, create_time datetime DEFAULT CURRENT_TIMESTAMP, pay_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_out_trade_no (out_trade_no), KEY idx_pid_status (pid, trade_status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;out_trade_no 上的唯一索引是回调幂等的底牌第 4 章的实现要反复用到它。调试初期pid trade_status 联合索引同样重要——监听端上报一笔钱服务端要快速筛出该商户下所有待支付订单再在中间匹配金额一致的那条没有这个索引订单量一大匹配就是全表扫描。3.3 Nginx 与 PHP-FPM 的最小配置源码放到 /var/www/codepay 后给站点写一个最小 Nginx 配置server { listen 80; server_name pay.example.com; root /var/www/codepay/public; # 换成你源码的实际入口目录 index index.php index.html; location / { try_files $uri $uri/ /index.php?s$uri; } location ~ \.php$ { fastcgi_pass unix:/run/php/php7.4-fpm.sock; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } }try_files 把不存在的静态文件路径统一交给 index.php 处理框架路由才能接管请求fastcgi_pass 指向 PHP-FPM 的 socket路径里的 7.4 要换成你实际装的 PHP 版本。配完重启 Nginx 和 PHP-FPM确认 config.php 里的数据库账号密码能连上访问一次首页看到商户后台登录页环境就算通了。这个时候先别急着配置支付参数把服务器时间同步确认一遍timedatectl set-ntp true时钟偏移会在后面回调验签时变成最隐蔽的故障源。4. 本地回调接口的实现签名算法、验签与幂等处理回调接口是整套源码里故障率最高的模块。用户支付成功后码支付服务端向 notify_url 发请求你的接口要做三件事验签确认请求是真的、核对订单确认金额对得上、落库改状态保证不会重复发货。做完之后返回一个 success 字符串。4.1 回调参数约定与签名计算方式码支付回调是 POST 表单格式常见参数如下表参数含义示例pid商户 ID1000trade_no码支付平台交易号202501011200001out_trade_no商户订单号202501010001type支付渠道alipay / wechat / qqpayname商品名称测试商品money支付金额元0.01trade_status交易状态TRADE_SUCCESSsign签名32 位小写 md5通用的签名规则是除了 sign 本身把所有参数按参数名 ASCII 升序排列拼成 keyvaluekeyvalue 的字符串末尾直接接商户密钥再做 md5。不同码支付源码的签名细节有差异有的只拼 value 不拼 key有的密钥要再包一层 md5拿到的源码里给了官方 SDK 就以 SDK 的拼接方式为准。一个可用的签名函数function makeSign(array $params, string $key): string { unset($params[sign]); ksort($params); $str ; foreach ($params as $k $v) { if ($v || $v null) continue; $str . $k . . $v . ; } return md5(rtrim($str, ) . $key); }ksort 做升序排序空值跳过不参与签名rtrim 去掉末尾多余的 然后拼接密钥做 md5。这里要特别强调money 参与签名时必须是原样字符串绝不能先转 float 再拼。0.01 用 (float) 处理后再拼接在不同 PHP 版本下可能变成 0.01 或 0.010一旦和网关侧的签名串不一致验签立刻失败。4.2 回调接口的完整 PHP 实现一个能直接放进业务里的 notify.php 大致长这样?php require config.php; require lib/sign.php; $params $_POST; if (empty($params[sign]) || empty($params[out_trade_no])) { exit(fail); } $calc makeSign($params, $config[merchant_key]); if (!hash_equals($calc, $params[sign])) { error_log([notify] sign bad: . json_encode($params, JSON_UNESCAPED_UNICODE)); exit(fail); } $pdo new PDO($config[dsn], $config[db_user], $config[db_pass]); $stmt $pdo-prepare( UPDATE pay_order SET trade_status 1, trade_no ?, type ?, name ?, pay_time NOW() WHERE out_trade_no ? AND trade_status 0 AND money ? ); $stmt-execute([ $params[trade_no], $params[type], $params[name], $params[out_trade_no], $params[money], ]); if ($stmt-rowCount() 0) { // 订单首次确认在这里触发发货、开通账号等业务动作 sendGoods($params[out_trade_no]); } exit(success);处理顺序是刻意的先验签签名不对直接 fail让网关按自己的策略决定是否重试验签通过后用 UPDATE 语句把金额核对直接写进 WHERE 条件订单号对上、状态还是待支付、金额完全一致才允许更新。rowCount 返回 0 有两种情况一是订单不存在或金额不符说明参数配错或者有人伪造回调二是这张订单之前已经处理过本次是重复回调。两种情况都返回 success因为对网关而言它关心的是这单还要不要继续重试再纠结下去只会制造更多重复通知。4.3 网关重试机制与幂等设计码支付服务端对没有返回 success 的订单一般会按 1 分钟、3 分钟、10 分钟的间隔逐步拉长重试总窗口能到 24 小时。这意味着同一笔订单可能被重复回调几十次接口不做幂等就会重复发货。上面 SQL 里的 AND trade_status 0 就是幂等屏障第一笔事务把状态改成 1 之后后续所有重复回调的 UPDATE 都影响 0 行sendGoods 永远不会执行第二次。还有两个配套实践。第一回调处理里不要同步做慢操作发货、发邮件这类动作要么在 UPDATE 后异步触发要么丢进队列因为网关的 HTTP 超时通常只有 5 秒左右你处理 10 秒网关那端早就判定超时开始下一次重试了。第二把验签日志和发货日志分开记排查重复发货时先看哪一次 UPDATE 影响行数非 0再看发货动作实际触发几次两个日志一对就知道问题出在幂等屏障之前还是之后。5. 本地回调链路验证与排错模拟回调、日志定位和三个高频坑环境配好、接口写完最后一公里是验证。最直接的手段是绕过用户扫码环节自己往 notify.php 发一个构造好的回调请求。5.1 用 curl 模拟网关回调验证全链路假设商户密钥是 abc1234567890def先在命令行算出签名echo -n money0.01name测试商品out_trade_no20250101001pid1000trade_noMOCK123trade_statusTRADE_SUCCESStypealipayabc1234567890def | md5sumecho -n 不输出换行字符串末尾接商户密钥md5sum 输出的 32 位小写值就是 sign。然后发起回调请求curl -s -X POST http://127.0.0.1/notify.php \ --data-urlencode pid1000 \ --data-urlencode out_trade_no20250101001 \ --data-urlencode trade_noMOCK123 \ --data-urlencode typealipay \ --data-urlencode name测试商品 \ --data-urlencode money0.01 \ --data-urlencode trade_statusTRADE_SUCCESS \ --data-urlencode sign上一步算出的md5值--data-urlencode 会自动 URL 编码中文和特殊字符和真实网关行为一致。收到 success 后回库里确认mysql -ucodepay -p codepay -e SELECT trade_status, pay_time FROM pay_order WHERE out_trade_no20250101001\Gtrade_status 变成 1 且 pay_time 有值说明验签、SQL 更新、返回体三个环节全通。这套 curl 模板换个参数名就能复用到任意支付网关的回调测试。5.2 回调日志要记的四个字段回调接口里加一行结构日志排错效率翻倍error_log(implode(|, [ date(Y-m-d H:i:s), $params[out_trade_no] ?? , $params[money] ?? , $ok ? SIGN_OK : SIGN_BAD ]) . \n, 3, /var/log/codepay_notify.log);时间、订单号、金额、验签结果四个字段足够定位 90% 的回调问题。完整参数按需再展开日志文件按天切分。5.3 三个高频坑时钟偏移、金额精度、响应体脏字符时钟偏移签名带时间戳或网关校验时间窗时服务器时间差几分钟就会时好时坏。用 date 看系统时间然后 timedatectl set-ntp true 让系统自动同步。金额精度回调里的 money 是元字符串验签、比较、落库全程不要经过 float直接塞进 SQL 的 WHERE 条件交给 MySQL 的 decimal 比较浮点误差从源头杜绝。响应体脏字符成功响应必须原样输出 success 四个字母。PHP 若用 ? 结束并在后面留了换行这些字符会被带进响应体导致网关判定失败继续重试。根治办法是所有回调文件去掉 ? 结束标签让文件以最后一行代码的换行符收尾。本文还有配套的精品资源点击获取