☰
免签支付系统搭建实战:从个人收款码到运营级支付网关
2026/10/2 14:46:08 网站建设 项目流程

简介:即刻云支付是一套面向个人站长、独立开发者和中小团队的运营级个人免签支付系统,基于PHP构建,集成了多用户管理、订单轮询、实时二维码生成等功能,可有效弥补个人无法直接申请官方支付接口、收款渠道受限的痛点。整包共1351个文件,压缩后约290.83MB,核心包括PHP服务端源码、Java安卓端源码、易语言多用户监控程序、数据库SQL文件,以及用于前端界面和交互的JS、CSS、PNG等素材,同时包含编译后的jar、class、exe组件,覆盖从后端服务到客户端展示的完整闭环。已有552人学习下载,适合具备一定PHP基础、需要快速搭建个人收款通道或进行支付系统二次开发的用户;其中SQL文件可直接初始化数据库,便于快速跑通整套流程。整套源码提供可直接部署的免签支付方案,安卓端支持实时生成收款二维码,配套登录器与PC客户端可对接支付宝、微信等渠道,监控模块支持轮询查单与多用户管理,目录结构清晰完整,可作为商业项目底码,也便于逐模块学习支付流程和定制开发。

1. 从个人免签到运营级:这套源码到底在解决什么问题

做独立开发那几年,最头疼的不是写业务,而是收钱。没有企业资质,签不了微信支付、支付宝的官方通道;挂别人的聚合支付,抽成高、结算又不稳。身边不少人最后都绕回同一条路:用个人收款码,配一套“即刻云支付系统源码”这种运营级个人免签支付系统,自己搭一个支付网关。

这类源码包一般同时带上服务端支付系统、监控服务、安卓监控端三块,解决的产品问题可以拆成四层:用户扫码支付、到账自动识别、订单回调通知、运营管理。说白了就是让一个个人收款码拥有接近官方支付的体验。它的真实难度不在“支付”两个字,而在“免签加稳定”四个字。免签意味着没有官方异步通知,你必须自己从监控端拿到“钱到了”的证据;稳定意味着监控掉线一天,损失的不只是订单,是客户信任。

这套东西适合三类人:有自有业务、想省下通道费的独立开发者;做二开或集成的小团队;以及想研究支付系统架构的开发者。接下来的内容按我实际部署和改源码的顺序写,尽量把参数、命令、坑都摆出来。

2. 免签支付系统的核心链路:四段时序和三处选型

免签支付和官方支付最大的区别在于:官方支付是支付渠道主动回调你,免签支付是监控端被动感知到账后主动上报。这一被动,整个支付系统架构就得做大量补偿设计。先看一条完整请求链路,再聊三处关键选型。

2.1 从支付成功到订单完成:一次完整请求的时序

一次正常支付,数据是这样流动的:

  1. 用户在商户站点下单,请求服务端生成订单,订单号关联到这笔待支付记录。
  2. 服务端返回一个收款码地址,通常是微信或支付宝的个人收款码,也可能是动态生成的一次性二维码。
  3. 用户扫码后完成付款,此时微信或支付宝的钱包账单里多了一笔款,通知栏也会弹一条到账消息。
  4. 安卓监控端通过通知监听、截屏识别或辅助功能捕获到“收款金额”这条信息。
  5. 监控端把金额、渠道、时间、监控端设备号上报给服务端。
  6. 服务端校验金额和订单状态,把订单从待支付改成已支付。
  7. 服务端按商户提交的 notify_url 发起异步通知,商户接口返回 success 后停止重试。
  8. 商户在自己的业务侧完成发货、加积分、充值等动作。

注意第 5 步到第 7 步,这里有两个容易翻车的点。第一,监控上报是“可能重复”的,通知栏弹一次、截屏又识别一次,服务端必须按流水号去重。第二,回调通知是“可能失败”的,商户接口超时、返回非 success,服务端要有重试队列,否则就会出现客户付了钱但没发卡的情况。我见过的生产事故,十有八九都出在这两步。

2.2 监控端的三条技术路线:通知监听、截屏识别、辅助功能

拿到到账信息是整条链路的地基。常见三条路,各有取舍:

方案实现方式到账延迟误报率功耗说明
通知监听NotificationListenerService 读通知栏文案1 秒内低低主流方案,但部分国产 ROM 会杀通知权限
截屏识别MediaProjection 截屏后 OCR 金额2 到 5 秒中高兼容性最好,兜底必备
无障碍辅助AccessibilityService 读界面节点1 到 3 秒低中能拿更多信息,但对系统版本敏感

我一般建议主备结合:通知监听为主,截屏识别兜底。平时用通知通道,延迟低、耗电少;一旦通知权限被系统回收或者微信通知被折叠,截屏兜底还能把单子救回来。千万别只做一条路,哪怕手机是测试机,也不敢保证 ROM 不抽风。

这里还有一个选型细节要注意:OCR 识别如果放在安卓端,识别速度和准确率受机型影响很大;放在服务端,每张截图都要上传,流量和存储成本会涨。折中做法是安卓端先用轻量模板匹配,简单判断截图里有没有“收款”字样,再决定要不要上传做全量 OCR,能省下七成以上无效计算。

2.3 订单状态机与回调幂等:不设计好这里迟早翻车

服务端订单表至少要这几个字段:订单号 order_no、支付渠道 channel、金额 amount、状态 status、回调地址 notify_url、回调次数 notify_count、最后回调时间 notify_time。状态流转一般是:

CREATED -> PAID -> NOTIFIED -> DONE

加上一个超时分支:CREATED 超过一定时间未支付,进入 CLOSED,CLOSED 后不允许再被上报支付成功。

回调幂等是硬约束。同一笔订单在重试机制下会收到多次通知,商户那边如果每次回调都执行一次加积分操作,那就是事故。常见做法是给订单表加一个唯一键uk_order_no,状态从 PAID 改成 NOTIFIED 时用带条件的 UPDATE 语句,只有上一状态是 PAID 时才更新成功。重复回调进来时,服务端直接返回成功标记,不再触发商户接口。

UPDATE orders SET status = 'NOTIFIED', notify_time = NOW() WHERE order_no = ? AND status = 'PAID';

这行 SQL 保证了幂等:只有第一次回调能把 PAID 改成 NOTIFIED,后面的重复通知全被挡掉。很多老源码在这里偷懒,直接用普通 UPDATE,结果并发重试时同一订单状态被覆盖,血泪经验,别省这一行条件判断。

3. 在 Linux 服务器上跑通服务端:解压、配置、模拟一单

拿到源码后第一件事不是改业务,是把这个支付系统先在服务器上跑通。常见这类 PHP 源码包解压后有这几个目录:入口在 public 目录,服务端逻辑在 application 或 app 目录,数据库 SQL 文件在根目录的 install.sql 里,安卓源码在 android 目录。下面按最小闭环来。

3.1 环境选型:PHP 版本、MySQL、Redis 怎么配更稳

这类免签支付系统服务端大多是 PHP 写的,有些框架老一点,有些自研。我的建议是:PHP 用 7.4 或 8.0,别追新,也千万别用 5.6。老源码在 PHP 7.4 下兼容性最稳,到了 8.1 以后容易踩到 deprecated 报错,特别是字符串函数和数组相关逻辑。数据库选 MySQL 5.7 或 MariaDB 10.4,Redis 用 6.x,主要做队列、缓存和分布式锁。

# 以 Ubuntu 20.04 为例 apt install -y nginx mysql-server redis-server php7.4-fpm php7.4-mysql php7.4-redis php7.4-curl php7.4-gd php7.4-mbstring # 解压源码到站点目录 unzip jike-cloud-payment.zip -d /www/wwwroot/pay # 给运行目录写权限 chown -R www-data:www-data /www/wwwroot/pay chmod -R 755 /www/wwwroot/pay/storage

PHP 7.4 的 opcache 建议开启,opcache.enable=1,支付接口的 QPS 能提一截。但注意改完 PHP 配置要重启 php-fpm,systemctl restart php7.4-fpm,否则配置不生效。

Nginx 配置要指向 public 目录。这类入口设计是为了避免用户直接访问源码目录,把 index.php 和数据库配置都锁在外面。伪静态规则一定要配,否则路由全部 404。

server { listen 80; server_name pay.yourdomain.com; root /www/wwwroot/pay/public; index index.php; location / { try_files $uri $uri/ /index.php?s=$uri; } location ~ \.php$ { fastcgi_pass unix:/run/php/php7.4-fpm.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } }

这段配置里最关键的是try_files,它把所有非真实文件请求都交给 index.php 处理,这套支付路由才能正常工作。如果忘记配,访问/api/order/create会直接 404,而且这类报错日志里很难看出来是路由问题还是代码问题。

3.2 配置文件里的十个关键项:密钥、回调地址、监控端 token

跑通前先把配置文件过一遍。PHP 源码通常在.env或config.php里,下面这些项是必调项,按顺序对着检查:

配置项作用建议值
DB_HOST / DB_NAME / DB_USER / DB_PWD数据库连接按实际填写
REDIS_HOST / REDIS_PORT队列和缓存的地址127.0.0.1 / 6379
app_secret服务端与商户端签名的密钥生成 32 位随机字符串
monitor_token监控端上传的鉴权 token和 app_secret 分开单独一个
notify_retry_times异步回调最大重试次数3 到 5 次
notify_retry_interval每次重试间隔秒数1, 5, 15
order_expire_time订单支付超时时间300 秒
amount_check是否开启金额严格校验true
log_level日志等级debug(上线后改 error)
qrcode_cache_time二维码缓存时间120 秒

app_secret 和 monitor_token 一定要分两个值。很多人贪省事共用一个 token,结果安卓端源码泄露后整个服务端签名体系跟着暴露。monitor_token 只在监控端和服务端之间用,泄露了可以单独换,不用动商户端签名。

3.3 用命令行模拟一笔完整订单:验证闭环是否通了

在接安卓端之前,先用 curl 模拟一遍完整流程。这步能确认服务端代码本身没毛病,再出问题就锁定在监控端。

# 第一步:创建一笔 0.01 元订单 curl -X POST http://127.0.0.1/api/order/create \ -H "Content-Type: application/json" \ -d '{"amount":"0.01","notify_url":"http://yourdomain.com/notify.php"}' # 返回里拿到 order_no,比如 20241101001 # 第二步:模拟监控端上报到账 curl -X POST http://127.0.0.1/api/notify/up \ -H "Content-Type: application/json" \ -d '{"monitor_token":"你自己配置的token","order_no":"20241101001","amount":"0.01","channel":"wechat"}' # 第三步:观察服务端日志里有没有商户回调记录 tail -f /www/wwwroot/pay/storage/logs/notify.log

第三步的日志里应该能看到商户接口请求记录,如果商户接口返回 success,订单状态会直接变 NOTIFIED。这里最常踩的坑是金额校验,创建订单传 0.01,上报也传 0.01,但服务端内部算金额时用了浮点比较,0.01 在浮点里是不精确的。建议在源码里确认金额比较是否用整数分,也就是amount存的是分而不是元,接口层再转换,能少很多玄学问题。

4. 安卓监控源码改造:通知到服务端上报的最后一公里

服务端跑通后,重头戏在安卓端。这个环节最磨人,因为同一个 App 在不同品牌手机上行为完全不一样。不要指望它开箱即用,手机厂商的后台限制才是真正的黑匣子。

4.1 用 NotificationListenerService 接住到账通知

通知监听是延迟最低的方案,也是这套源码的默认主通道。核心代码一般在 android 工程里的一个服务类,继承 NotificationListenerService,重写 onNotificationPosted 回调。

class MonitorService : NotificationListenerService() { private val processed = LinkedHashSet<String>() override fun onNotificationPosted(sbn: StatusBarNotification?) { if (sbn == null) return val pkg = sbn.packageName if (pkg != "com.tencent.mm" && pkg != "com.eg.android.AlipayGphone") return val text = sbn.notification?.extras ?.getCharSequence(Notification.EXTRA_TEXT)?.toString() ?: return val reg = Regex("""(?:收款|到账)[^\d]*([0-9]+\.[0-9]{2})""") val match = reg.find(text) ?: return val amount = match.groupValues[1] val dedupeKey = "$pkg-${sbn.key}" if (!processed.add(dedupeKey)) return report(pkg, amount, dedupeKey) } private fun report(pkg: String, amount: String, dedupeKey: String) { // 用 WebSocket 或 HTTP 推送到服务端 // 上报内容包含 monitor_token、channel、amount、dedupeKey、ts } }

这代码里有三个参数是必调的。第一个是包名过滤,只处理微信和支付宝的通知,避免别的应用通知干扰。第二个是正则,微信文案一般是“微信支付收款0.01元”,支付宝一般是“支付宝到账0.01元”,所以用收款或到账两个词都能命中,金额用[0-9]+\.[0-9]{2}锚定两位小数。第三个是去重键,sbn.key是系统给每条通知的唯一标识,配合包名做成 dedupeKey,同一通知就算回调多次也只上报一次,这个键也是服务端幂等判断的兜底。

还要注意清单文件里的权限声明,BIND_NOTIFICATION_LISTENER_SERVICE是系统权限,必须让用户在系统设置里手动开启通知使用权,代码里要写好引导页。这个权限开不了,后面全白搭。

4.2 截屏识别兜底:当通知栏被系统干掉时

通知监听最大的敌人是国产 ROM。MIUI、EMUI、ColorOS 都有激进的后台清理策略,通知使用权可能被自动回收。此刻必须有截屏方案兜底。

截屏方案通常用 MediaProjection 拿屏幕数据,保存成 Bitmap 后做 OCR 识别。OCR 引擎可以选 Tesseract 这类轻量方案,但中文识别率一般;更推荐的做法是只做模板匹配,在截屏图上找“收款”或“到账”两个关键词附近的数字区域,再用本地小模型识别数字。这一步在安卓端做,能避免上传截图的隐私争议和流量开销。

val projection = mediaProjectionManager.getMediaProjection(resultCode, data) val imageReader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 2) // 拿到 Image 后转 Bitmap,再截取底部通知区域 // 对截取区域做二值化后交给 OCR 识别

截屏兜底的延迟比通知监听高不少,所以上报到服务端时要在 payload 里加一个source字段,标记这次上报来自 notify 还是 screenshot。服务端可以按 source 分别统计监控质量,如果发现截图上报占比过高,说明通知通道被系统掐了,需要提醒用户检查权限。

4.3 上报链路:长连接与心跳参数

监控端往服务端上报,常见做法是 WebSocket 长连接或 HTTP 轮询。WebSocket 延迟低,服务端实现可以用 Swoole 或 Workerman,PHP 环境兼容性更好的是 Workerman。HTTP 轮询最简单,每 2 到 3 秒拉一次,但延迟到账感知会慢,而且耗电。

上报数据格式建议统一走 JSON,字段名前后端对齐:

{ "monitor_token": "监控端专用token", "type": "wechat", "source": "notify", "amount": "0.01", "dedupe_key": "com.tencent.mm-1|state=null|...", "ts": 1730448000 }

心跳参数三件套:心跳间隔 30 秒,断线重连用指数退避,第一次重连等 2 秒,第二次 4 秒,最多等 60 秒。别做固定 5 秒重连,服务端一抖动,手机会产生连接风暴,把自己服务端打挂。

5. 运营级避坑:掉单、并发、限额与监控掉线的处置

跑通和稳定运营之间隔着无数暗坑。这一章写的都是我自己在线上遇到过的,按“现象、原因、解决”的结构来,每条都能直接对着查。

5.1 客户已付款但订单还是待支付:回调丢失的多米诺效应

现象:用户在微信里付了 0.01,监控端日志显示已经上报,服务端订单却一直停在待支付,商户那边也没收到回调。

原因:这链条上有三个可能断点。第一,监控端上报后服务端没有找到订单,常见是订单号传错或用金额匹配不到;第二,服务端把订单改成已支付后,商户回调地址网络不通或接口 500;第三,商户回调返回了内容,但服务端要求返回字符串 “success”,商户返回了 JSON,导致服务端认为回调失败并停止重试。

解决:断点逐个排查。先看监控端上报日志,确认 order_no 和金额;再看服务端 notify.log,确认服务端是否发起了回调;最后看商户接口日志,确认有没有收到请求。我在商户侧默认要求回调接口即使异常也要输出 “success”,付费逻辑用事务保证幂等,这样服务端只管状态推进,不会因为响应格式误判。

5.2 同一笔订单回调了五次,发卡发重复

现象:客户付一次款,收到两次充值。订单 notify_count 显示多次回调,商户业务侧没有做幂等。

原因:监控端通知监听推了一次,截图兜底又推了一次;而且服务端的更新语句没有加状态条件,导致重复上报把订单推进了多次回调。

解决:三处同时打补丁。监控端用 dedupe_key 去重;服务端订单状态更新必须带 WHERE 条件status = 'PAID';商户业务侧对 order_no 加唯一约束,重复回调直接返回旧的处理结果。这三层都做了,基本不可能重复发卡。

5.3 两笔同金额的订单,到账后串单

现象:A 和 B 都付了 0.01,A 的订单被标记已支付,但 B 的商户回调先收到,A 的商户没收到。

原因:部分老源码为了省事,监控端上报时只传金额不传订单号,服务端拿金额去匹配“待支付订单”。两笔同金额订单同时存在时,匹配逻辑返回了先查到的那个,导致串单。金额匹配本身就是不可靠的设计,只能作为兜底。

解决:扫码支付阶段就把订单号和收款码绑定,监控端上报时带上订单号或备注信息。我一般会做一层映射:每个订单生成时给收款码附加一个备注,微信或支付宝的收款记录里能带上这个备注,监控端把备注一起上报,服务端优先按备注定位订单,金额只做二次校验。

5.4 手机锁屏两小时后监控离线

现象:白天还好,晚上锁屏放一宿,第二天监控端显示离线,早上的订单全掉。

原因:国产 ROM 的后台限制。省电策略把 App 进程杀了,通知使用权被系统回收,或者 WiFi 在休眠时被断开,长连接跟着断。

解决:在 App 里做一个健康检查页,列出三项必须项:通知使用权是否开启、省电策略是否白名单、自启动是否允许。每次启动时引导用户检查,并用前台服务加startForeground把进程优先级拉高。服务端也要做监控端离线告警,超过 5 分钟没心跳就推送通知给运维人员,别等用户来投诉才处理。

6. 把到账延迟压进 5 秒:监控端到回调链路的性能调试

端到端延迟是这类系统最直观的体验指标。我习惯在链路里打四个时间点,T0 用户付款完成,T1 监控端捕获到通知,T2 上报到达服务端,T3 商户收到回调。目标分段如下:

锚点说明目标耗时
T0 到 T1监控端识别到账1 秒内
T1 到 T2上报网络传输2 秒内
T2 到 T3服务端回调商户2 秒内
端到端总延迟5 秒内

压测时重点看两个位置。第一个是上报链路是否复用了 TCP 连接,WebSocket 长连接天然复用,如果用 HTTP,务必用连接池,否则每笔订单都新建连接,晚高峰必炸。第二个是回调通知不能串行发,PHP 侧用 curl_multi 或丢到 Redis 队列里让 worker 并发消费,同一订单重试可以串行,不同订单必须并行。

我现在改监控端代码,第一件事不是看功能,而是加打点,把 T1 到 T3 的耗时全部打到日志里。宁可多写几行日志,也不让自己对着黑匣子猜哪一段慢了。这套系统跑起来容易,跑稳很难,但把监控和回调两段调顺之后,个人支付完全能接近官方通道的体验。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询