实现网银直连:PHP+EXE自动回调与订单对账系统解析

📅 发布时间:2026/9/16 9:41:14
实现网银直连:PHP+EXE自动回调与订单对账系统解析
简介这套支付宝包装网银/网银网关支付系统面向中小企业站长与个人开发者旨在用自有PHP服务端配套EXE监控客户端替代高手续费的第三方支付平台解决网站资金周转困难和商品成本过高的问题。系统实现原生网银直连、订单自动查询与零延迟回调支持商户管理、交易管理、通道管理、账号管理及自动轮询可全天候无人值守完成即时到账。压缩包约202.12MB内含PHP交易管理源码、EXE监控客户端程序及配置、说明文档PHP源码用于服务端业务逻辑EXE程序负责PC端订单监控便于在Windows服务器上快速部署和二次开发。目前已有112人关注学习适合具备PHP基础、希望搭建安全稳定支付通道的技术人员。通过这套源码可同时获得管理端与监控端的完整实现降低支付运维成本提升网站收款自动化程度。1. 为什么这么多站点回头找“网银直连”支付网关软件要解决的真实问题做电商站和充值平台的都知道接入第三方支付要过审、要交保证金、每笔还被抽成中小站长最难受的是结算周期压资金。所以当一套号称“支付宝网银网关软件”的源码出现在眼前时很多人第一反应是“这不就是外挂吗”实际上拆完这套东西你会发现它就是一个把“人工盯支付宝订单 → 手动确认打款 → 再给网站回执”这个过程自动化掉的系统。PHP 交易管理系统管商户和订单PC 端 EXE 监控程序管查询和通知两边通过 HTTP 回调协同。它能做到 24 小时无人值守核心价值是替代第三方支付渠道的“支付结果通知”环节而不是绕过支付工具本身。适合已经有业务流量、想用支付宝原声网银收单但不想被渠道费吃掉的团队也适合做支付二清研究的开发者。2. 交易管理系统PHP的模块拆解与数据表设计2.1 系统架构总览这套系统的部署形态是“PHP 服务端 EXE 客户端”。PHP 服务端跑在 Linux 或 Windows 的 Web 环境里负责商户管理、订单录入、通道账号维护、接收 EXE 推送的支付结果并回调商户。EXE 客户端装在一台能访问支付宝网关的 Windows 机器上定时或轮询查询支付订单状态一旦发现已支付就把订单号、金额、交易流水号 POST 回 PHP 服务端。我在实际部署中会把 PHP 服务端和 EXE 客户端分两台机器跑PHP 只做 API不承担轮询任务。这样即使 EXE 所在的网络机故障Web 服务也能正常接收商户下单请求订单先落到库等 EXE 恢复后再补齐状态通知。2.2 核心数据表结构先看最关键的几张表。商户表、订单表、通道表、账号表这四张表构成了整个支付系统的骨架。下面是精简后的 MySQL 建表语句-- 商户表 CREATE TABLE merchant ( id int(11) NOT NULL AUTO_INCREMENT, mch_id varchar(32) NOT NULL COMMENT 商户号, mch_name varchar(64) NOT NULL, api_key varchar(64) NOT NULL COMMENT API签名密钥, notify_url varchar(255) NOT NULL COMMENT 回调地址, ip_whitelist varchar(255) DEFAULT COMMENT IP白名单,逗号分隔, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 1启用 0禁用, PRIMARY KEY (id), UNIQUE KEY uk_mch_id (mch_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单表 CREATE TABLE orders ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(64) NOT NULL COMMENT 商户订单号, mch_id varchar(32) NOT NULL, amount decimal(10,2) NOT NULL COMMENT 订单金额, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已关闭 3已退款, channel_id int(11) NOT NULL COMMENT 通道ID, account_id int(11) NOT NULL COMMENT 使用的支付宝账号ID, trade_no varchar(64) DEFAULT COMMENT 支付宝流水号, notify_flag tinyint(4) NOT NULL DEFAULT 0 COMMENT 是否已通知商户, create_time datetime NOT NULL, pay_time datetime DEFAULT NULL, update_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_status (status), KEY idx_account (account_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 通道表 CREATE TABLE channel ( id int(11) NOT NULL AUTO_INCREMENT, channel_name varchar(32) NOT NULL COMMENT 通道名称, bank_code varchar(16) NOT NULL COMMENT 银行代码, sort_order int(11) NOT NULL DEFAULT 0, status tinyint(4) NOT NULL DEFAULT 1, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 账号表 CREATE TABLE pay_account ( id int(11) NOT NULL AUTO_INCREMENT, channel_id int(11) NOT NULL COMMENT 所属通道, alipay_account varchar(64) NOT NULL COMMENT 支付宝登录账号, cookie_token text COMMENT 登录凭证或网关Token, max_amount decimal(10,2) NOT NULL DEFAULT 0 COMMENT 单笔限额, today_amount decimal(10,2) NOT NULL DEFAULT 0 COMMENT 今日已累计, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 1可用 0不可用, PRIMARY KEY (id), KEY idx_channel (channel_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;订单表里的notify_flag很关键它标记了是否已经通知过商户。EXE 循环查询时会把已支付订单先更新状态再调回调接口。如果通知失败notify_flag保持 0系统会进入下一轮补单逻辑而不是直接丢弃。2.3 商户 API 签名与 IP 白名单商户接入时服务端会生成一个mch_id和api_key。商户下单请求必须用这个api_key对参数排序后做 MD5 签名。PHP 侧验签逻辑不能只比对签名还要校验 IP 和订单金额是否在允许范围内function verifySign($params, $apiKey) { unset($params[sign]); ksort($params); $str urldecode(http_build_query($params)) . key . $apiKey; $sign md5($str); return hash_equals($sign, $_POST[sign]); } // 验签后还要做白名单校验 function checkIpWhitelist($mchIp, $whitelist) { if (empty($whitelist)) return false; $list explode(,, $whitelist); return in_array($mchIp, $list); }这里有个细节下单参数里不能带channel_id让服务端根据金额权重自动选通道。因为如果暴露通道参数商户可以直接指定某个通道导致账号负载不均甚至把限额已经用完的账号暴露出去。服务端选通道的常见做法是先按单笔限额匹配再按今日累计排序取最小负载的那个账号。3. 客户端 EXE 监控程序的工作原理与轮询策略3.1 为什么用 EXE 而不是 PHP 常驻进程很多人问PHP 不是也能写常驻脚本吗为什么监控端要单独做一个 EXE我的理解是PHP 的 web 生命周期不适合做高频轮询每次启停进程的资源和请求上下文都是浪费而 EXE 是原生 Windows 程序可以直接调用支付宝网银控件或本地加密证书还能把浏览器会话保持在一个独立进程里。EXE 内部通常开多个线程每个线程负责一个支付宝账号的轮询内存管理比 PHP 的进程模型更可控。EXE 和 PHP 服务端的通信很简单就是 POST 一个 JSON。EXE 查询到订单后向http://你的域名/index.php/api/notify发送通知PHP 接收后更新库并回调商户。这样即使 EXE 和 PHP 不同机器也可以通过网络联调。3.2 轮询算法的设计轮询不是越频繁越好。早期版本有人把间隔设成 1 秒结果支付宝账号当天就被风控了多笔订单异常。我通常建议轮询间隔按账号的活跃度动态调整刚下单的订单 5 秒查一次超过 2 分钟的订单 15 秒查一次超过 10 分钟的订单 30 秒查一次。EXE 侧维护一个待查询订单队列订单按创建时间排序每次从最早的一批开始查。下面是一个模拟轮询调度逻辑的伪代码import time from queue import PriorityQueue pending_queue PriorityQueue() # 元素为 (next_query_time, order_no, query_count) def order_paid(order_no): # 模拟收到已支付结果 pass def polling_loop(): while True: now time.time() # 拿到最早需要查询的订单 if pending_queue.empty(): time.sleep(1) continue next_time, order_no, query_count pending_queue.get() if next_time now: pending_queue.put((next_time, order_no, query_count)) time.sleep(0.5) continue status query_alipay(order_no) if status PAID: order_paid(order_no) elif query_count 20: # 指数退避最多重试20次 interval min(30, 5 * (query_count 1)) pending_queue.put((time.time() interval, order_no, query_count 1)) else: # 超时未支付标记关闭 close_order(order_no)代码里的 PriorityQueue 是为了避免每次都扫描全队取最短延时的那一个。query_count控制最大查询次数防止无限轮询把订单变成死单。3.3 全自动回调的 PHP 处理当 EXE 查询到订单已支付会向 PHP 发送order_no、trade_no、amount、alipay_account。PHP 端处理回调的核心是幂等同一个订单无论通知多少次最终商户只会收到一次成功通知而且金额必须等于订单金额否则不处理并告警。public function handleNotify() { $orderNo $_POST[order_no] ?? ; $tradeNo $_POST[trade_no] ?? ; $amount $_POST[amount] ?? 0; $order DB::table(orders)-where(order_no, $orderNo)-first(); if (!$order || $order-status ! 0) { exit(OK); // 已经处理过直接返回 } // 金额比对防止低额订单被高额支付后利用 if (bccomp($order-amount, $amount, 2) ! 0) { file_put_contents(/var/log/pay_mismatch.log, date(Y-m-d H:i:s) . $orderNo mismatch\n, FILE_APPEND); exit(FAIL); } // 更新订单 DB::table(orders)-where(id, $order-id) -update([status 1, trade_no $tradeNo, pay_time date(Y-m-d H:i:s)]); // 通知商户 notifyMerchant($order-mch_id, $order-order_no, $order-amount); exit(OK); }bccomp做金额比较避免浮点误差。商户通知失败时需要把notify_flag置 0并写进重试表由另一个定时任务每隔 10 分钟重推最多重推 5 次。3.4 客户端日志与健康检查EXE 的稳定直接决定了支付成功率。看日志时主要关注三类关键行QUERY_START、QUERY_SUCCESS、NOTIFY_SEND。如果QUERY_SUCCESS数量占总查询量低于 95%说明账号被风控或网络环境不稳定。如果NOTIFY_SEND之后没有对应NOTIFY_ACK说明 PHP 端回调处理异常需要查 PHP 的 error_log 和 Nginx 的 access log。我一般在 EXE 配置里加一个大文件日志转储单文件超过 50MB 自动切分。因为轮询日志增长非常快不切分的话半个月就能占用几个 GB 磁盘。4. 从“被动回调”到“主动补单”订单状态机与异常对账4.1 订单状态机系统里订单状态必须闭环否则容易出现“钱收了但订单没发货”的纠纷。完整的状态迁移如下状态触发场景目标状态0 待支付商户下单成功1 已支付 / 2 已关闭1 已支付EXE 查询到已支付或支付宝主动通知3 已退款2 已关闭超时未支付或被商户取消无3 已退款商户退款接口触发或人工对账退款无在代码实现上所有状态更新都放到一个带锁的 service 方法里防止并发回调把订单重复发货public function changeOrderStatus($orderNo, $targetStatus, $lockKey ) { $lock Redis::get(lock:order: . $orderNo); if ($lock) return false; Redis::setex(lock:order: . $orderNo, 10, 1); try { $order DB::table(orders)-where(order_no, $orderNo)-first(); if (!$order) return false; // 根据当前状态判断是否允许迁移 $allowMap [ 0 [1, 2], 1 [3], ]; if (!in_array($targetStatus, $allowMap[$order-status] ?? [])) return false; // 执行更新 return DB::table(orders)-where(id, $order-id)-update([status $targetStatus]); } finally { Redis::del(lock:order: . $orderNo); } }4.2 主动补单的三种场景第一EXE 查询到支付成功但 PHP 进程挂了导致订单状态没有更新。这种情况恢复后 EXE 要能从日志里回放“待确认交易”重新推送一次。第二支付宝网银页面跳转或回调丢失。用户在银行侧已经扣款但 EXE 那一次查询请求超时了。所以 EXE 要有一个“在途订单”列表只要订单没被显式关闭就继续按退避策略查询。第三订单金额不一致。比如用户实际付了 100.01 元订单是 100 元这时候系统不能自动确认而是进入待人工审核表。我在实际运营中遇到过多次这种“多付一分钱”的情况基本都是银行的协议或支付页面显示问题。4.3 定时对账脚本无论 EXE 多自动化每周跑一次对账都是必须的。最直接的方式是导出支付宝账单 CSV然后和本地订单表比对。下面是 PHP 脚本的核心片段public function checkAgainstAlipay($csvFile) { $fp fopen($csvFile, r); $line 1; $mismatch []; while (($data fgetcsv($fp)) ! false) { if ($line 4) { $line; continue; } // 跳过表头 $alipayOrderNo $data[0]; // 支付宝订单号 $localOrderNo $data[1]; // 商户订单号 $amount $data[7]; // 金额 $status $data[9]; // 交易状态 $local DB::table(orders)-where(order_no, $localOrderNo)-first(); if (!$local) { $mismatch[] 本地无单: $localOrderNo; continue; } if (bccomp($local-amount, $amount, 2) ! 0) { $mismatch[] 金额不一致: $localOrderNo 本地{$local-amount} 支付宝$amount; } if ($status 交易成功 $local-status ! 1) { $mismatch[] 状态不一致: $localOrderNo 本地{$local-status} 支付宝成功; } } if (!empty($mismatch)) { file_put_contents(/var/log/pay_recon_ . date(Ymd) . .txt, implode(\n, $mismatch), FILE_APPEND); } }4.4 区分“网关超时”和“订单不存在”EXE 查询时支付宝网关可能返回三种结果支付成功、支付中、订单不存在。其中“订单不存在”有两种原因一是订单号录入错误二是超过了支付宝网银的查询时间窗口。我建议 EXE 对“订单不存在”不做自动关闭只记录日志然后把这个订单转移到人工复核列表。因为有些银行的网银跳转延迟支付宝侧可能暂时还没建立订单等几秒再查就有了。5. 部署细节与运行排错从 Nginx 配置到 Windows 服务化5.1 PHP 环境要求与 Nginx 配置要点这套系统对 PHP 版本要求不高7.4 到 8.2 都能跑但必须开启curl、pdo_mysql、bcmath扩展。Nginx 配置里要注意两个点一是client_max_body_size不能设太小EXE 回调时可能携带 cookie 数据二是 PHP-FPM 的request_terminate_timeout要调大因为回调商户接口时如果商户响应慢会导致 PHP 进程卡住。server { listen 80; server_name pay.example.com; client_max_body_size 2m; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { include fastcgi_params; fastcgi_pass unix:/run/php/php7.4-fpm.sock; fastcgi_param PHP_VALUE request_terminate_timeout60s; fastcgi_read_timeout 60s; } }try_files到index.php是为了支持框架式路由。如果你拿到的源码是原生 PHP 文件则不需要这个配置直接指向文件即可。5.2 数据库并发连接控制EXE 轮询的频率越高数据库连接消耗越快。PHP-FPM 默认pm.max_children只有 10 的话回调一打过来就全堵住了。我在部署时按 8GB 内存的机器做如下调整pm dynamic pm.max_children 40 pm.start_servers 10 pm.min_spare_servers 5 pm.max_spare_servers 20另外所有 SQL 必须走预处理防止回调用法注入。EXE 发送的order_no是数据库索引但也要用prepare绑定参数不要直接拼接字符串。5.3 将 EXE 注册为 Windows 服务监控 EXE 是 24 小时运行的程序不能每次开机手动双击。常见的做法是用 NSSM 把它注册成服务nssm install PayMonitor C:\prog\PayMonitor\monitor.exe nssm set PayMonitor AppParameters --configC:\prog\PayMonitor\config.ini nssm set PayMonitor AppRotateFiles 1 nssm set PayMonitor RotationBytes 52428800 nssm start PayMonitorRotationBytes设置为 5242880050MB日志轮转不用自己写代码。服务失败时NSSM 可以自动重启nssm set PayMonitor AppExit Default Restart nssm set PayMonitor RestartDelay 50005.4 常见问题排查最常出现的问题是时区不一致。MySQL 的time_zone如果是默认的 UTC而 PHP 用的date_default_timezone_set(Asia/Shanghai)那么订单时间就会差 8 小时对账脚本永远对不上。强制统一在 PHP 初始化时设置date_default_timezone_set(Asia/Shanghai); $pdo-exec(SET time_zone 08:00);另一个坑是 Windows 防火墙默认拦截 EXE 对外访问导致查询超时。在服务端看到大量超时日志时先在 EXE 机器上执行ping pay.example.com curl http://pay.example.com/api/health如果 curl 通但 EXE 不通八成是防火墙规则只允许了浏览器进程手动添加一项允许 TCP 出站即可。6. 压测验证与运营中的几个坑6.1 回调接口的并发压测回调接口虽然是内网调用但也要防住商户直接刷接口。用 Apache Bench 模拟 EXE 的并发回调验证 PHP 接口是否扛得住ab -n 2000 -c 100 -p notify.json -T application/json \ -H Authorization: Bearer test_key \ http://pay.example.com/index.php/api/notifynotify.json里放一个测试订单号压测前先在数据库插入 100 条待支付订单。压测后看 DB 里status1的记录是否都正确以及是否存在重复更新。如果出现死锁优先检查订单表的索引策略。6.2 运营中的三个常见坑第一个坑是频繁修改支付宝账号的登录凭证会导致次日轮询全部失败。很多网银接口会校验会话环境同一个账号在多个 IP 间切换后EXE 查询会直接被拒绝。解决办法是给每个账号固定一个登录出口EXE 在启动时校验凭证有效性失效时发告警通知而不是静默重试。第二个坑是订单金额校验不严被刷单。我之前接手过一个被薅的系统原因是回调接口只验订单号不验金额。攻击者先下单 1 元再通过伪造回调构造任意金额的付款成功通知。压测验证时特意把金额比对放到状态更新之前并且用bccomp而不是。第三个坑是服务器时间偏差导致验签失败。支付宝 APP 支付用 RSA 验签时如果服务器时间快慢超过 5 分钟会直接验签失败。部署一套 NTP 时间同步并在监控里加上时间偏移检查ntpdate -u ntp.aliyun.com6.3 通过日志验证系统稳定性上线后每天只需看两个指标一是notify_flag从 0 变 1 的订单占比正常情况应该接近 100%二是 EXE 日志中QUERY_SUCCESS的总耗时。如果平均耗时超过 800ms说明轮询间隔太短账号可能已经被限流。此时将轮询间隔翻倍观察 24 小时直到平均耗时回落到 200ms 以内。对于日订单量在 5000 笔以下的小团队这个方案在成本和可控性上远比接第三方支付要好。本文还有配套的精品资源点击获取