简介:ARYA云支付1.1Java版是一套基于Java开发的聚合支付源码,核心解决支付宝个人码转银行卡、免签支付与多渠道聚合等实际业务需求,适合个人开发者、中小团队以及需要快速搭建支付系统的技术人员参考。系统自带完整的搭建教程、部署文档和使用说明,目录划分清晰,前后端代码齐备。压缩包共包含2000个文件,解压后约182.28MB,其中以1350个JavaScript脚本、275个HTML页面、145个CSS样式为主,配合142个Java核心类、XML配置文件及少量说明文档,整体覆盖前端页面、后端逻辑与系统配置。目前已有141人学习下载。开发者可直接阅读完整源码,掌握聚合支付接口调用、免签回调处理、安全验证机制以及多支付通道切换等关键实现;同时可基于现有工程按业务场景二次开发,调整功能模块并重新打包部署,是理解支付系统底层设计、提升工程能力的高价值参考。
1. 支付源码里的另一条路:个码收单加码转卡,ARYA云支付 1.1 Java 版拆解
支付宝收单接口不是想签就能签的,商户资质、费率、结算周期会卡住不少起步团队。做支付系统的圈子里,绕过官方签约的免签方案一直在流传,“码转卡”就是其中一种:用户扫你的个人收款码,资金先进你的支付宝,系统再把对应款项转给订单归属方。这套 zip 里就是一份 Java 版聚合支付源码——ARYA云支付 1.1,后端是 Java,管理后台是 Bootstrap 风格界面,包里附带搭建教程、部署文档和使用说明。这篇把源码拆开讲:码转卡链路怎么落地、异步通知怎么验签、用户扫码后掉单查哪里、二开从哪里下手。适合做聚合收单、代收结算、私域卖货系统的 Java 开发者,也适合想研究免签支付原理的人。
2. 先看懂通道逻辑:码转卡、免签支付与聚合收单的源码结构
2.1 三种收单形态,为什么有人选“个码免签”
做支付对接的人第一步就是选通道形态。我按实际项目里常见的三种路子做一个对比,别看都是“扫码付款”,接入成本和后续维护完全是三码事。
| 收单形态 | 接入门槛 | 费率与结算 | 典型场景 | 回调成熟度 |
|---|---|---|---|---|
| 官方扫码支付(当面付) | 需要企业资质、完成签约 | 官方费率,T+1 自动结算 | 正规电商、线下门店 | 官方异步通知,文档齐全 |
| 聚合服务商动态码 | 服务商进件,商户逐个审核 | 阶梯费率,分润结算 | 多商户 SaaS 平台 | 有官方 API,流程偏重 |
| 个人码免签 | 几乎零门槛 | 走码主账户,结算自己处理 | 私域收单、快收通道、码转卡 | 需要自己维护回调与对账状态 |
ARYA 云支付走的是第三行。它的产品逻辑一句话能说清:平台方准备一个(或多个)支付宝个人收款码,用户下单后展示码,用户扫码付款后资金进入码主账户,平台通过某种方式感知到账,再把这笔钱在系统里标记为已支付,最后统一结算到商户绑定的银行卡。这就是标题里“个码转卡转账”四个字的来源。
这套设计的价值在于绕开了签约门槛,代价是把本该由官方通道负责的事情全部拉回来自建:到账感知、订单状态维护、资金结算、差错处理,一个都不能少。源码里最值钱的不是某个炫技算法,而是这套“免签收单 + 转卡结算”的完整闭环。看懂闭环之后,不管你是拿它做二开还是只当学习样板,都不会迷路。
2.2 源码的模块拆解:收银台、商户后台、订单引擎、回调服务
根据摘要和包里能看到的文件布局,这套系统大致由六块组成。我按调用顺序理一遍:
- 收银台模块:用户端下单页,负责创建订单、生成支付宝收款码、轮询订单状态并跳转结果页。这块是用户唯一能看到的部分。
- 商户后台模块:管理商户、收款码、结算银行卡、订单查询和账单下载,也就是 Bootstrap 那套界面的核心功能区。项目正文里那几个 CSS 文件,antui-all.css、style.css、bootstrap.min.css,基本能确认后台前端是 Bootstrap 全家桶。
- 支付通道层:封装与支付宝的交互,包括生成二维码链接、发起转账、处理异步通知。1.1 版本主要对接支付宝,聚合能力说的是以后可以横向接微信、银联这些。
- 订单引擎:订单创建、状态流转、超时关闭、回调状态更新。这块是支付系统的命根子,后面讲状态机时专门展开。
- 结算服务:这个是“码转卡”的核心落点,把已支付订单汇总,按商户维度做转账记录,触发银行卡转账并回写结算状态。
- 系统管理:管理员登录、操作员权限、字典配置、系统参数、日志查看。
2.3 技术栈推演与包目录观感
这类 Java 支付项目我拆过好几个,ARYA 的技术栈属于典型的中型单体支付系统,没有花架子。根据源码文件名和 Java 版本特征推断,大概率是这套组合:
- JDK 1.8,Spring Boot 或 Spring MVC + MyBatis
- MySQL 5.7 以上,存储订单、商户、结算流水
- Redis 做二维码缓存和短时效数据,部分版本也可能没接
- 支付宝官方 SDK:alipay-sdk-java,负责签名、验签、转账接口调用
- 前端 Bootstrap 3/4,后台页面静态资源直接放在包里
解压之后你大概率会看到这样的目录骨架,具体类名以你手里的 zip 为准,但结构大差不差:
arya-pay/ ├── src/main/java │ ├── controller/ # 收银台、后台、回调三个入口 │ ├── service/ # 订单服务、结算服务、通道服务 │ ├── mapper/ # MyBatis 数据层 │ ├── entity/ # 订单、商户、码、结算流水实体 │ └── config/ # 数据源、拦截器、支付参数配置 ├── src/main/resources │ ├── application.yml # 核心配置,数据库、Redis、支付参数 │ ├── mapper/*.xml # SQL 映射 │ └── static/ # 后台静态资源,antui、bootstrap 都在这里 ├── docs/ # 搭建教程与部署文档 ├── sql/ │ └── arya_pay.sql # 初始化脚本 └── pom.xml注意一个容易忽略的点:项目正文里列出来一堆 bootstrap.min.css 和 style.css 重复项,说明打包时前端资源被多次打包进不同目录,不代表系统有多个前端。看后台页面的时候,认准 static 目录下实际被 admin 模板引用的那套就行。我一般拿到源码先不看业务代码,先把静态资源目录和数据库 SQL 过一眼,能快速判断这个项目的完整度和作者习惯。
这套技术栈选的没什么问题,都是支付系统里的常见方案。MyBatis 做订单状态更新时方便写原子 SQL,Redis 缓存二维码短链接能抗住用户快速扫码的场景,Spring Boot 把配置集中在 application.yml 里也好排查。接下来直接把它跑起来。
3. 部署到能跑:数据库、配置文件和启动验证
3.1 环境准备清单
不要一上来就改代码,先把运行环境对齐。1.1 版本是偏 Java 8 时代的项目,高版本 JDK 经常会遇到反射或字节码层面的兼容问题。环境建议按下面这张表准备:
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8.0_2xx | 以 pom.xml 里 java.version 为准,别直接用 JDK 17 |
| MySQL | 5.7 或 8.0 | 注意 8.0 的驱动名和时区参数不一样 |
| Redis | 5.x 以上 | 如果配置里没启用可以暂时不开 |
| Maven | 3.6 以上 | 打 jar/war 包用 |
| Nginx | 1.18 以上 | 用于域名转发,回调接口必须要公网可访问 |
这里有个容易翻车的细节:如果你用的是 MySQL 8.0,驱动类要改成com.mysql.cj.jdbc.Driver,而 5.7 时代的老项目里写的是com.mysql.jdbc.Driver。不改的话启动直接报ClassNotFoundException。我一般先把这两个环境的差异确认好再动手,省得启动失败后回头猜配置。
3.2 导入数据库与初始化配置
解压 zip 后,在 sql 目录下找到初始化脚本,正常情况下一个文件就能建完所有表。导入命令用重定向最省事:
mysql -u root -p -h 127.0.0.1 --default-character-set=utf8mb4 < arya_pay.sql导入成功后,用 Navicat 或命令行连上去检查,至少能看到orders、merchant、pay_code、settlement这几类表。如果缺表,或者某个表是空的,多半是 SQL 脚本没完整执行,重新导一遍。
接着改配置文件。老版本项目可能是jdbc.properties,新一点的是application.yml,两个文件的作用一样,把下面这段按实际情况替换:
spring: datasource: url: jdbc:mysql://127.0.0.1:3306/arya_pay?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 你的数据库密码 driver-class-name: com.mysql.jdbc.Driver redis: host: 127.0.0.1 port: 6379 password: pay: alipay: appId: 你的支付宝应用APPID privateKey: 你的应用私钥 alipayPublicKey: 支付宝公钥,注意不是应用公钥 notifyUrl: http://你的域名/notify/alipay returnUrl: http://你的域名/result这里把三个最关键的参数讲透:
privateKey是你自己生成并保存在本地的应用私钥,用来给请求签名;alipayPublicKey是支付宝公钥,用来验签支付宝推送的通知。这两个搞反是最常见的低级错误,我见过不下五次。notifyUrl是支付宝异步通知的接收地址,必须填公网能直接访问到的地址,而且要是 http/https 可以正常请求到的域名,不能填 IP 加端口,支付宝校验时对这种地址大概率直接拒绝。本地联调阶段往往会在这里卡住,后文避坑章节单独说。
3.3 启动后端服务
配置改完,先 Maven 打包再启动。如果是 Spring Boot 项目,标准流程这样走:
# 在项目根目录执行,跳过测试减少意外失败 mvn clean package -DskipTests # 进入target目录启动 cd target nohup java -jar arya-pay-1.1.0.jar --spring.profiles.active=prod > /logs/pay.log 2>&1 & # 确认服务起来了 tail -f /logs/pay.log如果是传统的 war 包项目,就把打出来的 war 文件扔进 Tomcat 的 webapps 目录,启动 Tomcat 后看日志。判断启动成功的标志不是“端口起来了”,而是日志里出现数据源初始化完成、MyBatis mapper 加载这些关键行。
登录后台时要注意,这类系统后台路径一般是/admin或/manager,初始账号密码在部署文档里有,登录后第一件事改密码。不要跳过这一步,支付系统的后台如果保持默认口令,相当于把资金流水公示给全网。后台登录进去后,先建一个测试商户,配置一个收款码,生成一笔测试订单,能跑到这一步说明部署链路基本通了。
3.4 验证“下单→展示码→回调”的最小闭环
跑通的最短验证路径是:
- 商户后台创建收款码,关联一个支付宝个人码。
- 复制收银台地址,模拟用户看到下单页。
- 用自己的支付宝扫生成的码,真实付一笔 0.1 元。
- 回到商户后台看订单状态是否从“待支付”变成“已支付”。
如果第 4 步状态没变,先别急着怀疑业务代码,按日志优先排查。打开支付日志,搜索订单号,看有没有收到支付宝的异步通知记录。多数情况是通知根本没到你服务,这个坑下面有专门一节。走到这一步,你已经把整套源码从静态代码变成了运行中的系统,接下来的核心是业务链路里的细节。
4. 核心业务流:订单状态机、支付宝异步通知与码转卡结算
4.1 从下单到支付的状态流转
支付系统的代码可以写得乱,但订单状态不能模糊。ARYA 这类免签方案的订单状态机比官方支付多一个“待结算”状态,因为资金进的是码主账户,需要系统主动触发转账。
我按通用设计还原一下这张订单表的状态设计:
CREATE TABLE `orders` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '平台订单号', `merchant_id` bigint(20) NOT NULL COMMENT '归属商户', `amount` decimal(10,2) NOT NULL COMMENT '订单金额', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0待支付 1已支付 2已结算 3已关闭 4结算失败', `channel` varchar(16) DEFAULT 'alipay' COMMENT '支付渠道', `callback_time` datetime DEFAULT NULL COMMENT '回调时间', `pay_time` datetime DEFAULT NULL COMMENT '实际支付时间', `create_time` datetime NOT NULL, PRIMARY KEY (`id`), KEY `idx_order_no` (`order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;整个生命周期链路是这样的:用户下单后状态是 0,等待支付宝异步通知;收到通知并验签通过后,状态变成 1;结算服务扫描已支付订单,发起转账,成功后状态变成 2;如果转账失败,状态进入 4,需要人工介入或自动重试。
这里有一个容易踩坑的设计点:状态变更必须用条件更新,不能先查出来在 Java 内存里改完再 update。原因很简单,支付回调引擎是不定线程数的,同一笔订单可能同时被通知回调、管理员手动标记、定时补偿任务读取。用无条件 update 一定会出现状态覆盖。正确写法应该是UPDATE orders SET status = 1 WHERE order_no = ? AND status = 0这种带前置条件的原子操作,扫到的行数为 0 就说明订单已经被处理过,直接返回成功。
4.2 支付宝异步通知的验签与幂等处理
支付宝异步通知是整个系统中风险最集中的入口。100% 的支付系统安全问题都出在“无条件信任通知内容”这个环节。通知参数可以伪造,签名是不能伪造的,所以第一步永远是对参数验签:
@PostMapping("/notify/alipay") public String handleAlipayNotify(HttpServletRequest request) throws AlipaySignatureException { // 把支付宝异步通知的参数取出来,转成 Map Map<String, String> params = new HashMap<>(); Map<String, String[]> requestParams = request.getParameterMap(); for (String name : requestParams.keySet()) { String[] values = requestParams.get(name); params.put(name, String.join(",", values)); } // 验签:RSA2 算法必须保持一致,公钥必须用支付宝公钥 boolean signVerified = AlipaySignature.rsaCheckV1(params, alipayPublicKey, "UTF-8", "RSA2"); if (!signVerified) { log.error("异步通知验签失败,直接拒绝:{}", params); return "failure"; } // 只处理成功状态的交易,其他状态直接 ACK,避免重复计算 String tradeStatus = params.get("trade_status"); if (!"TRADE_SUCCESS".equals(tradeStatus)) { return "success"; } // 幂等更新:只有待支付状态才能变更为已支付 String orderNo = params.get("out_trade_no"); int updated = orderMapper.updatePaidIfPaying(orderNo); if (updated > 0) { orderMapper.updateCallbackInfo(orderNo, params.get("trade_no"), new Date()); } return "success"; }参数说明:rsaCheckV1的第一个参数是支付宝推送的原始参数集合,签名校验算法必须用RSA2,这是目前支付宝强制要求的。out_trade_no是平台生成的订单号,trade_no是支付宝交易号,回调信息里两个都要存,后续对账靠支付宝交易号去找支付宝侧记录。
这段代码里最关键的战略性习惯是“先验签、再审状态、再条件更新”这个顺序,任何一步不满足都直接返回。支付宝的通知机制很直白:你返回success它认为你处理完了,返回其他任何字符串都会在接下来的 24 小时内按递增间隔重试,最长 7 次。如果业务处理成功但因为代码异常返回了failure,支付宝会重推几次,而你的幂等逻辑挡住了重复处理,这反而是补偿机制在兜底,不是坏事。
4.3 码转卡结算的记账逻辑
订单标记为已支付之后,资金还躺在收款码的支付宝账户里,要把这笔钱结算给商户绑定的银行卡。这就是“码转卡”的最后一跳。
结算服务一般按商户维度做批次汇总,避免每单都发起一笔转账。常见的聚合策略是:每分钟扫描一次已支付但未结算的订单,按商户分组,每个商户累加金额生成一条结算流水,然后调用支付宝转账接口或者人工打款。转账涉及敏感接口,实现上必须保留完整的操作日志:
public void settleMerchant(Long merchantId) { // 先把可结算订单汇总出来,避免转账过程中订单状态变化 List<Order> orders = orderMapper.listSettling(merchantId); BigDecimal totalAmount = orders.stream() .map(Order::getAmount) .reduce(BigDecimal.ZERO, BigDecimal::add); // 生成结算流水,状态为处理中 Settlement settlement = new Settlement(); settlement.setMerchantId(merchantId); settlement.setAmount(totalAmount); settlement.setStatus(0); settlementMapper.insert(settlement); // 调用转账渠道,逐笔处理失败时单独标记 boolean transferResult = transferService.transfer(merchantId, totalAmount); if (transferResult) { settlementMapper.markSuccess(settlement.getId()); orderMapper.markSettled(merchantId); } else { settlementMapper.markFailed(settlement.getId()); // 失败流水保留,等重试或人工核对 } }这里强调一个“先记账、再转账”的顺序。很多二开者上来就调转账,转账成功后才想起来写流水,一旦渠道返回超时但实际到账,流水缺失导致对账永远对不平。正确的做法是转账前先生成一条待确认流水,转账结果再回写流水状态,这样不管渠道返回什么,账面上都有迹可循。
5. 上线排查避坑:回调丢失、掉单、重复通知与数据错乱
5.1 支付宝异步通知就是进不来
现象:用户扫码付款成功了,订单状态一直是待支付,后台日志里根本搜不到notify相关的访问记录。
原因:最常见的有三个。第一,notifyUrl填的是内网地址或 IP 地址,支付宝的服务器根本路由不到;第二,服务器防火墙或云安全组没放行对应端口;第三,填了域名但该域名还没备案或没解析到当前服务器,支付宝请求直接超时放弃。
解决:先把notifyUrl改成一个浏览器能直接访问的线上地址,再用 curl 模拟一个 POST 请求测通接口,确认接口能返回success。测试时注意,支付宝要求接口的响应时间不能太长,业务处理逻辑超过 3 秒会被判定超时,真正的回调进来时只会更慢,所以回调入口只做状态更新,不做重活。
5.2 钱扣了但系统没单,掉单怎么查
现象:用户反馈“我明明付款了,订单还是待支付”,而且日志里确实没有收到支付宝通知。
原因:这类掉单大多出在异步通知丢失。支付宝异步通知依赖公网链路,偶尔会有网络抖动导致通知没送达,这不常见也不是小概率,跑量之后总会遇到。另外一个隐蔽原因是用户在扫码之后支付环境异常,比如支付宝内部拦截了收银台的跳转,导致回调延迟了很久。
解决:不要只依赖被动通知。我给一个基本兜底方案:定期轮询支付宝订单查询接口,超过 30 秒还处于待支付的订单主动向支付宝发起查询,查到已支付就直接更新本地状态。代码留在最后一章。掉单问题的根因是“被动等待”,而生产环境必须把主动性握在自己手里。
5.3 重复通知导致金额重复上账
现象:订单只付了一笔,后台却出现了两条支付流水,或者商户余额增加了两倍。
原因:支付宝异步通知有重推机制,如果业务代码先查订单状态再在 Java 层判断“已支付就返回”,两个线程同时进来就能绕过判断。另外人工手动补单和自动补偿任务同时跑,也会重复处理。
解决:全部改成条件更新。任何状态流转都用UPDATE ... WHERE status = 期望的旧状态这种原子操作,更新行数为 0 说明已被处理,直接忽略。这条规则写进代码评审标准里,比任何分布式锁都便宜可靠。
5.4 数据库中文乱码和订单时间差八小时
现象:后台商户名称显示乱码,订单创建时间比实际时间早或晚 8 个小时。
原因:数据库表和连接串的字符集不一致。老项目初始化脚本里常用utf8,而 MySQL 8.0 默认是utf8mb4,两者在 emoji 和生僻字场景下表现完全不同。时间错乱则是因为 JDBC 连接串没有指定时区,服务器时区与实际时区偏差。
解决:连接串里显式加上characterEncoding=utf8mb4和serverTimezone=Asia/Shanghai,同时把表和库的字符集统一改为utf8mb4。改完还要重启服务,因为连接池在启动时就已经按旧参数建立了连接。别用 Navicat 里的可视化删改,直接执行一条 ALTER 语句全库转换最干净。
5.5 验签一直提示失败
现象:日志里明确打印出验签失败,回调参数看起来正常,但rsaCheckV1就是不通过。
原因:九成是公钥配置错误。很多人把“应用公钥”填到了alipayPublicKey的位置,但验签必须用“支付宝公钥”,这两个完全不一样。支付宝开放平台后台的应用信息页面里能看到两个公钥,一个是你上传的应用公钥,一个是支付宝生成的支付宝公钥,复制机会不小。
解决:找回正确的支付宝公钥后,先把配置里填错的公钥删掉,重启服务后重新用真实回调测试一次。同时确认算法字符串是RSA2而不是RSA,这两个算法对应不同的签名方式,验签时写错会 100% 失败。如果回调参数里有中文且服务端用ISO-8859-1解析了,也会造成签名原文不一致,控制台启动后第一件事就是把请求编码确认成UTF-8。
6. 二开进阶:上线前先加一个主动查单补偿任务
接支付通道的人有个共识:回调是补丁,主动查询才是主路径。ARYA 源码里即使已经有掉单补偿,我也建议按自己的业务节奏重写一版。下面这个定时任务用 Spring 的@Scheduled实现,60 秒扫一次超过 30 秒仍然待支付的订单,逐个向支付宝发起交易查询:
@Component public class OrderCompensateTask { private static final int PAYING_TIMEOUT_SECONDS = 30; private static final int QUERY_BATCH_SIZE = 50; @Scheduled(fixedDelay = 60000) public void compensatePayingOrders() { // 只捞状态为待支付、且创建时间超过30秒的单子,避免刚下单就被查询 List<Order> payingOrders = orderMapper.listTimeoutPaying(PAYING_TIMEOUT_SECONDS, QUERY_BATCH_SIZE); if (payingOrders.isEmpty()) { return; } for (Order order : payingOrders) { try { // 调用支付宝查询接口,查真实交易状态 AlipayTradeQueryResponse response = alipayService.query(order.getOrderNo()); if (response == null || !response.isSuccess()) { continue; } // 查到已支付就做幂等更新,不理会重复处理 if ("TRADE_SUCCESS".equals(response.getTradeStatus())) { int updated = orderMapper.updatePaidIfPaying(order.getOrderNo()); if (updated > 0) { log.info("补偿任务将订单置为已支付:{}", order.getOrderNo()); } } } catch (Exception e) { // 单笔查询失败不影响整批,吞掉异常留着下次扫描再试 log.error("补偿查询异常,订单号:{}", order.getOrderNo(), e); } } } }参数说明:PAYING_TIMEOUT_SECONDS设 30 秒是留出用户实际扫码支付的时间,太短会导致用户还没付完就被查一次,产生无意义的渠道调用;QUERY_BATCH_SIZE限制每批扫描数量,防止积压大量异常订单时把线程全部占满。频率上 60 秒一次已经足够,支付系统的补偿任务不是越频繁越好,支付宝接口有流量限制,频繁轮询还可能触发风控。
日志是这类的灵魂。你会依赖日志来判断是“查询失败”还是“查询成功但更新失败”,所以每笔都要打订单号。生产环境里把日志级别调到 INFO,单独输出到独立文件,方便出问题时按订单号 grep。
补上这段之后,整条链路才算闭合:主动查询兜底被动回调;回调正常时秒级完成状态更新;回调丢失时最多晚 60 秒被补偿任务捞回来。两套机制同时工作,互相掩护,这就是支付系统没有玄学、只有兜底的真实写照。那年我上线第一个支付通道,凌晨一点接到商户电话说钱扣了订单还没到账,翻日志查了一整夜,最后发现是回调网络抖动丢了一个通知,数据库里那笔单子就那么干躺着。从那以后我接任何支付通道,第一件事就是先把主动查单任务写好,再谈上线。ARYA 这套源码跑起来之后,你也先把这条兜底加上。希望帮到你。
本文还有配套的精品资源,点击获取