简介:这是一套面向Java后端开发者与支付系统二次开发者的全开源聚合支付四方支付系统源码,基于Jeepay架构实现,可帮助团队快速搭建自有支付网关,解决多渠道对接与统一路由问题。资源包共387个文件,以322个java源码为核心业务实现,辅以28个xml配置、7个yml与7个txt说明文件,另含sql建表脚本、ftl模板及少量html页面,压缩包约6.8MB,结构完整便于导入IDE直接研读。系统支持微信服务商与普通商户V2/V3接口、支付宝RSA与RSA2签名、云闪付服务商接口,支付网关可自动路由并支持分布式部署与高并发场景;管理端涵盖运营平台与商户系统,权限由Spring Security管控,订单通知借助MQ保证高可用与消息可达,支付渠道参数配置界面可自动化生成,前后端分离架构利于二次开发。目前已有816人学习下载,适合需要研究支付网关设计、渠道对接与分布式交易链路的开发者参考借鉴。
1. 从一份 jeepay 聚合支付系统压缩包说起:四方支付到底在解决什么问题
很多做 Java 后端的同学第一次接触「四方支付」这个词,是在某个资源站看到一份叫jeepay聚合支付四方支付系统.zip的包,下载下来解压一看,一堆 Maven 模块、Spring Boot 启动类、还有一堆渠道对接的配置。它到底是个什么东西?简单说,jeepay 是一套用 Java 写的开源聚合支付系统,核心价值是把微信、支付宝、云闪付这些三方支付渠道统一收口,对外提供一套标准的下单、退款、回调接口,让商户只对接一次就能同时支持多个渠道。而「四方」指的是介于商户和官方渠道之间的服务商角色——它不直接持有支付牌照,而是通过聚合多家三方通道,给中小商户提供更灵活的结算和技术接入。
这套系统能解决的真实痛点很具体:一个电商团队要同时接微信和支付宝,如果各自对接,签名、验签、回调重试、对账逻辑要写两套,后期加一个渠道就是一次重构。jeepay 这类聚合支付系统把这些差异封装在渠道适配层里,业务代码只面对统一的支付网关接口。它适合谁?适合需要自建支付中台的后端团队、做 SaaS 多商户分账的产品、以及想研究支付系统架构的 Java 工程师。下面我按「跑起来 → 看懂结构 → 接一个渠道 → 避坑 → 进阶」的顺序,把这份包背后的东西拆开讲清楚。
2. 把 jeepay 在本地跑起来:环境、建库与启动顺序
拿到压缩包后别急着改代码,先把最小可运行环境搭出来。jeepay 是典型的 Spring Boot 多模块项目,依赖 MySQL 和 Redis,前端分管理端和商户端两套。这一章的目标是让你在本地看到登录页、能进管理后台,这是后面所有调试的前提。
2.1 环境准备与依赖版本确认
先确认本机 Java 环境。jeepay 主流版本基于 JDK 8 或 JDK 11,用java -version看一眼,别用 JDK 17 直接上,Spring Boot 老版本在 17 上会有反射相关的启动报错。Maven 用 3.6 以上即可。数据库 MySQL 建议 5.7 或 8.0,Redis 用 5.x 以上。
# 确认基础环境,版本不对后面全是玄学问题 java -version # 期望 1.8 或 11 mvn -v # 期望 3.6+ mysql --version # 期望 5.7 / 8.0 redis-server --version这几条命令不是走过场。我见过太多「启动失败怎么解决」的求助,最后查出来是 JDK 版本和依赖不匹配。参数上唯一要注意的是 MySQL 8 的时区,连接串里必须带serverTimezone=Asia/Shanghai,否则启动时连接池初始化就会抛时区异常。
2.2 建库、导入 SQL 与修改连接配置
解压后找到docs或sql目录,里面通常有初始化脚本。jeepay 一般分两个库:一个管配置和商户(如jeepay_manager),一个管订单和支付流水(如jeepay_order),具体以包内脚本为准。
# 建库并导入,库名以包内 SQL 文件实际命名为准 mysql -uroot -p -e "CREATE DATABASE jeepay DEFAULT CHARACTER SET utf8mb4;" mysql -uroot -p jeepay < docs/jeepay.sql导入完成后改配置文件。jeepay 的配置分散在各模块的application.yml里,重点是数据源、Redis 和端口。下面是一段典型的数据源配置,参数含义我写在注释里。
spring: datasource: url: jdbc:mysql://127.0.0.1:3306/jeepay?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver redis: host: 127.0.0.1 port: 6379 database: 0serverTimezone不写会报时区错;useSSL=false本地调试省去证书麻烦;Redis 的database建议单独用一个库号,避免和你本机其他项目的缓存键冲突。改完配置,按「先启动 manager 管理端,再启动 merchant 商户端,最后启动 payment 支付网关」的顺序跑,因为网关启动时会去读管理端写入的渠道配置。
2.3 启动顺序与首次登录验证
用 Maven 分别启动各模块,或者直接在 IDE 里跑各自的Application主类。启动日志里看到Started XxxApplication且没有Connection refused就算成功。
# 在项目根目录,按模块逐个启动(模块名以实际 pom 为准) mvn -pl jeepay-manager spring-boot:run mvn -pl jeepay-merchant spring-boot:run mvn -pl jeepay-payment spring-boot:run启动后浏览器访问管理端地址,默认账号密码一般在 SQL 脚本的t_sys_user表里,常见是admin加一个初始密码。能登进去、能看到菜单,说明环境通了。这一步别跳过,后面接渠道时如果回调不通,你得先排除「是不是环境本身就没起来」这个变量。端口冲突是高频翻车点,三个模块默认端口如果撞了,改server.port即可。
3. 看懂 jeepay 的模块分层:聚合支付的核心抽象在哪
环境跑通只是第一步,真正决定你能不能改得动这套系统的,是理解它的分层。jeepay 的代码结构其实回答了一个问题:多个支付渠道的差异,到底被隔离在哪一层。看懂这个,你加渠道、改签名、排查回调才有方向。
3.1 渠道适配层:统一接口与具体实现的分离
聚合支付的核心抽象是「支付渠道接口」。系统会定义一个统一的接口,比如下单、退款、查单、回调解析这几个方法,然后微信、支付宝各写一个实现类。业务层调用时只依赖接口,不关心底层是哪个渠道。
// 渠道统一接口的典型形态(方法名以实际代码为准) public interface IPayChannelService { // 统一下单,入参是内部订单模型,返回渠道下单结果 String pay(PayOrderRQ rq); // 退款 String refund(RefundOrderRQ rq); // 解析渠道异步回调,转成内部统一模型 String parseNotify(String channelCode, HttpServletRequest request); }逻辑说明:PayOrderRQ是内部统一的下单请求对象,渠道实现类负责把它翻译成微信或支付宝要求的字段格式,再拼签名发出去。参数上,channelCode是渠道标识,用来路由到具体实现。这样设计的好处是,新增一个渠道只需要实现接口并注册,业务代码零改动。你要排查「某个渠道下单失败」,第一站就是找到对应的实现类,看它拼参数和签名的逻辑。
3.2 订单状态机:支付流水为什么不能随便改状态
支付系统最怕状态错乱。jeepay 里订单有明确的状态流转:待支付、支付中、支付成功、已退款等。状态变更通常集中在 service 层,且带乐观锁或条件更新,防止并发回调把状态改花。
-- 状态更新一般带前置条件,避免重复回调覆盖 UPDATE t_pay_order SET state = 2, update_time = NOW() WHERE order_id = ? AND state = 1;这条 SQL 的关键在AND state = 1。如果回调重复到达,第二次执行时state已经是 2,影响行数为 0,就不会重复处理。这是支付系统防重复的常见做法。你如果自己写业务,千万别用「先查再改」的两步操作,并发下必然出问题。理解这一点,你就明白为什么对账和回调日志这么重要——它们是状态机的黑匣子。
3.3 配置驱动:渠道参数为什么放在数据库而不是配置文件
jeepay 把渠道的商户号、密钥、证书路径这些放在数据库表里,通过管理端界面配置,而不是写死在 yml。原因是四方支付场景下,一个系统要服务多个商户、多个渠道,配置是动态的。
| 配置项 | 存放位置 | 修改后是否需重启 |
|---|---|---|
| 渠道商户号 | 数据库渠道表 | 否,读取时实时查 |
| 渠道密钥 | 数据库渠道表 | 否 |
| 数据源连接 | application.yml | 是 |
| Redis 地址 | application.yml | 是 |
这张表说明一个原则:会随业务变的放数据库,环境相关的放配置文件。你接渠道时如果发现改了密钥不生效,先确认是不是有缓存——jeepay 通常会把渠道配置缓存到 Redis,改完要在管理端点一下刷新,或者等缓存过期。这是新手最容易踩的坑之一。
4. 对接一个真实渠道:从配置到回调联调的完整链路
前面都是铺垫,这一章是真正动手的部分。我们以「新增一个渠道并跑通一笔下单到回调」为主线,把配置、下单、回调、验签串起来。不同渠道细节不同,但链路是一致的。
4.1 在管理端配置渠道参数
登录管理端,找到渠道配置菜单,新增一个渠道。需要填的核心参数包括:渠道编码、商户号、应用 ID、私钥、公钥、回调地址。回调地址必须是外网可访问的,本地调试常用内网穿透工具把本地端口映射出去,注意这里只做技术联调,映射的是你自己的开发服务。
配置时几个参数要特别小心:私钥格式(是 PKCS8 还是 PKCS1)、公钥是平台公钥还是应用公钥、签名类型(RSA2 还是 MD5)。填错任何一个,下单时都会返回签名错误。我的习惯是配完先点「测试连接」或类似按钮,没有的话就直接下一笔最小金额订单验证。
4.2 发起一笔下单请求并观察日志
配置好后,用商户端的收银台或直接调接口发起下单。下面是一个模拟内部下单请求的代码片段,展示业务层怎么调用统一接口。
// 业务层下单:只面对统一接口,不关心具体渠道 PayOrderRQ rq = new PayOrderRQ(); rq.setMchNo("M0001"); // 商户号 rq.setAmount(1L); // 金额,单位分,1 表示 1 分钱 rq.setChannelCode("WXPAY"); // 渠道编码,路由到微信实现类 rq.setNotifyUrl("https://your-domain/notify"); // 异步回调地址 String payOrderId = payOrderService.createOrder(rq);逻辑说明:amount单位是分,这是支付系统的通用约定,别传成元,否则金额差 100 倍。channelCode决定路由到哪个渠道实现。notifyUrl是渠道异步通知你支付结果的地址。下单成功后,日志里会打印渠道返回的预支付信息(比如二维码链接或调起参数)。如果这一步失败,重点看日志里的渠道返回码和返回描述,那是最直接的线索。
4.3 回调验签与幂等处理
用户支付完成后,渠道会异步回调你的notifyUrl。这一步是整个链路最容易出问题的地方。回调处理要做三件事:验签、幂等、返回成功标识。
// 回调处理的核心步骤 public String handleNotify(String channelCode, HttpServletRequest request) { // 1. 验签:用渠道公钥验证回调参数签名,防止伪造 boolean valid = channelService.verify(channelCode, request); if (!valid) { return "FAIL"; // 验签失败,让渠道重试或告警 } // 2. 幂等:根据渠道订单号查本地订单,已处理则直接返回成功 String channelOrderNo = request.getParameter("out_trade_no"); if (orderService.isProcessed(channelOrderNo)) { return "SUCCESS"; } // 3. 更新订单状态并落库 orderService.markPaid(channelOrderNo); return "SUCCESS"; }逻辑说明:验签用渠道提供的公钥,这一步不能省,否则任何人都能伪造回调把你的订单改成已支付。幂等靠渠道订单号判断,重复回调直接返回成功,避免渠道一直重试。返回给渠道的字符串必须是渠道约定的成功标识(微信是 SUCCESS,支付宝是 success),返回错了渠道会持续重试,日志会被刷爆。参数上,out_trade_no是你下单时传给渠道的商户订单号,用它来关联本地订单。
5. 避坑与排查:jeepay 落地时最容易翻车的五个地方
这一章是我和同行踩过的血泪经验汇总,每条按「现象 → 原因 → 解决」写,你遇到问题时可以直接对号入座。
现象一:启动报时区或连接池初始化失败。原因:MySQL 8 连接串缺少serverTimezone,或驱动类名用了老版本com.mysql.jdbc.Driver。解决:连接串加serverTimezone=Asia/Shanghai,驱动换成com.mysql.cj.jdbc.Driver。
现象二:改了渠道密钥但不生效。原因:渠道配置被缓存到 Redis,管理端改了数据库但缓存没刷新。解决:在管理端找刷新缓存入口,或手动删掉对应 Redis key,重启网关模块也能强制重载。
现象三:回调一直收不到,订单停在支付中。原因:notifyUrl外网不可达,或回调地址被网关拦截。解决:先用工具从外网访问你的回调地址确认可达,再检查网关有没有对回调路径做鉴权拦截——回调接口必须放行,不能要求登录态。
现象四:金额对不上,差 100 倍。原因:下单时金额单位传成了元,而系统按分处理。解决:统一用分作为金额单位,前端展示时再除以 100,别在传输层做转换。
现象五:重复回调导致订单状态被覆盖或重复发货。原因:回调处理没有幂等判断,或状态更新没有前置条件。解决:按渠道订单号做幂等,状态更新 SQL 带AND state = 待支付条件,影响行数为 0 就跳过后续业务。
提示:支付系统的日志要打全,尤其是渠道请求报文、返回报文、回调原始参数。出问题时这些日志就是后悔药,没有它们只能靠猜。
6. 进阶:用对账和压测验证你的聚合支付系统是否真的可靠
跑通一笔订单不代表系统可靠。真正上线前,我会做两件事:对账和压测。对账是拿渠道的账单文件和本地订单逐笔比对,找出「渠道成功但本地没更新」或「本地成功但渠道没有」的差异单。jeepay 一般有对账模块,核心逻辑是下载渠道账单、解析、和本地流水按订单号匹配。
// 对账核心:按订单号比对本地与渠道流水 for (ChannelBill bill : channelBills) { PayOrder local = orderDao.selectByOrderId(bill.getOrderId()); if (local == null) { // 本地无此单,可能是掉单,需要补单或告警 alertService.raise("本地缺失订单: " + bill.getOrderId()); } else if (local.getState() != PAID && bill.isSuccess()) { // 渠道成功本地未更新,典型的回调丢失,需要主动补状态 orderService.markPaid(bill.getOrderId()); } }逻辑说明:对账是支付系统的最后一道防线,回调可能因为网络问题丢失,但对账能兜底。参数上,账单文件的格式各渠道不同,解析时要处理编码和分隔符差异。压测则用 JMeter 或 wrk 对下单接口打流量,重点观察数据库连接池和 Redis 的瓶颈,以及高并发下订单号生成有没有重复。我自己的习惯是,任何支付相关的改动,上线前必须跑一遍对账脚本,确认没有差异单才敢发。这套东西不难,难的是坚持做。希望帮到你。
本文还有配套的精品资源,点击获取