Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,14 @@ Jeepay 已提供 Java、Python SDK,以及 PHP 对接 Demo:
- SDK 下载地址:<https://doc.jeequan.com/#/integrate/open/api/116>
- Java SDK 仓库:<https://github.com/jeequan/jeepay-sdk-java>

## AI 接入助手

如果你用 Claude / Cursor / ChatGPT 等 AI 工具接入 Jeepay,可以使用 [Jeepay Skills](https://github.com/jeequan/jeepay-skills) —— 把签名陷阱、字段大小写、wayCode 选择等 AI 容易踩坑的细节系统化整理,让 AI 一次接对。

- 仓库地址:<https://github.com/jeequan/jeepay-skills>
- 支持 Claude Code / Cursor / Cline / 网页 AI(claude.ai · ChatGPT · 通义千问 · 文心一言)
- 也可作为纯文档浏览,不用 AI 也能直接看接入指南

## 项目地址

- 服务端:[GitHub](https://github.com/jeequan/jeepay) · [Gitee](https://gitee.com/jeequan/jeepay) · [GitCode](https://gitcode.com/jeequantech/jeepay)
Expand Down
20 changes: 10 additions & 10 deletions conf/devCommons/config/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,16 @@ spring:
timeout: 1000
password:

# #activeMQ配置 ( 注意: activeMQ配置项需在spring的下级 )
activemq:
broker-url: failover:(tcp://127.0.0.1:61616?wireFormat.maxInactivityDuration=0) #连接地址
in-memory: false # Jeepay项目不可使用内存模式, 需要连接多个消费者。
user: system # activeMQ默认无需账密认证。 打开认证:activemq.xml添加simpleAuthenticationPlugin标签,账密在credentials.properties文件。
password: manager
pool:
enabled: true
max-connections: 10
idle-timeout: 30000 # 空闲的连接过期时间,默认为30秒
# #activeMQ配置 ( 注意: activeMQ配置项需在spring的下级。默认 MQ 已切换到 rocketMQ,此段保留作参考;如需切回 activeMQ,请同步修改 isys.mq.vender )
# activemq:
# broker-url: failover:(tcp://127.0.0.1:61616?wireFormat.maxInactivityDuration=0) #连接地址
# in-memory: false # Jeepay项目不可使用内存模式, 需要连接多个消费者。
# user: system # activeMQ默认无需账密认证。 打开认证:activemq.xml添加simpleAuthenticationPlugin标签,账密在credentials.properties文件。
# password: manager
# pool:
# enabled: true
# max-connections: 10
# idle-timeout: 30000 # 空闲的连接过期时间,默认为30秒
#
# #rabbitmq配置 ( 注意: rabbitmq配置项需在spring的下级 )
# rabbitmq:
Expand Down
20 changes: 10 additions & 10 deletions conf/manager/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,16 @@ spring:
timeout: 1000
password:

# #activeMQ配置 ( 注意: activeMQ配置项需在spring的下级 )
activemq:
broker-url: failover:(tcp://activemq5:61616?wireFormat.maxInactivityDuration=0) #连接地址
in-memory: false # Jeepay项目不可使用内存模式, 需要连接多个消费者。
user: system # activeMQ默认无需账密认证。 打开认证:activemq.xml添加simpleAuthenticationPlugin标签,账密在credentials.properties文件。
password: manager
pool:
enabled: true
max-connections: 10
idle-timeout: 30000 # 空闲的连接过期时间,默认为30秒
# #activeMQ配置 ( 注意: activeMQ配置项需在spring的下级。默认 MQ 已切换到 rocketMQ,此段保留作参考;如需切回 activeMQ,请同步修改 isys.mq.vender )
# activemq:
# broker-url: failover:(tcp://activemq5:61616?wireFormat.maxInactivityDuration=0) #连接地址
# in-memory: false # Jeepay项目不可使用内存模式, 需要连接多个消费者。
# user: system # activeMQ默认无需账密认证。 打开认证:activemq.xml添加simpleAuthenticationPlugin标签,账密在credentials.properties文件。
# password: manager
# pool:
# enabled: true
# max-connections: 10
# idle-timeout: 30000 # 空闲的连接过期时间,默认为30秒
#
# #rabbitmq配置 ( 注意: rabbitmq配置项需在spring的下级 )
# rabbitmq:
Expand Down
20 changes: 10 additions & 10 deletions conf/merchant/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,16 @@ spring:
timeout: 1000
password:

# #activeMQ配置 ( 注意: activeMQ配置项需在spring的下级 )
activemq:
broker-url: failover:(tcp://activemq5:61616?wireFormat.maxInactivityDuration=0) #连接地址
in-memory: false # Jeepay项目不可使用内存模式, 需要连接多个消费者。
user: system # activeMQ默认无需账密认证。 打开认证:activemq.xml添加simpleAuthenticationPlugin标签,账密在credentials.properties文件。
password: manager
pool:
enabled: true
max-connections: 10
idle-timeout: 30000 # 空闲的连接过期时间,默认为30秒
# #activeMQ配置 ( 注意: activeMQ配置项需在spring的下级。默认 MQ 已切换到 rocketMQ,此段保留作参考;如需切回 activeMQ,请同步修改 isys.mq.vender )
# activemq:
# broker-url: failover:(tcp://activemq5:61616?wireFormat.maxInactivityDuration=0) #连接地址
# in-memory: false # Jeepay项目不可使用内存模式, 需要连接多个消费者。
# user: system # activeMQ默认无需账密认证。 打开认证:activemq.xml添加simpleAuthenticationPlugin标签,账密在credentials.properties文件。
# password: manager
# pool:
# enabled: true
# max-connections: 10
# idle-timeout: 30000 # 空闲的连接过期时间,默认为30秒
#
# #rabbitmq配置 ( 注意: rabbitmq配置项需在spring的下级 )
# rabbitmq:
Expand Down
20 changes: 10 additions & 10 deletions conf/payment/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,16 @@ spring:
timeout: 1000
password:

# #activeMQ配置 ( 注意: activeMQ配置项需在spring的下级 )
activemq:
broker-url: failover:(tcp://activemq5:61616?wireFormat.maxInactivityDuration=0) #连接地址
in-memory: false # Jeepay项目不可使用内存模式, 需要连接多个消费者。
user: system # activeMQ默认无需账密认证。 打开认证:activemq.xml添加simpleAuthenticationPlugin标签,账密在credentials.properties文件。
password: manager
pool:
enabled: true
max-connections: 10
idle-timeout: 30000 # 空闲的连接过期时间,默认为30秒
# #activeMQ配置 ( 注意: activeMQ配置项需在spring的下级。默认 MQ 已切换到 rocketMQ,此段保留作参考;如需切回 activeMQ,请同步修改 isys.mq.vender )
# activemq:
# broker-url: failover:(tcp://activemq5:61616?wireFormat.maxInactivityDuration=0) #连接地址
# in-memory: false # Jeepay项目不可使用内存模式, 需要连接多个消费者。
# user: system # activeMQ默认无需账密认证。 打开认证:activemq.xml添加simpleAuthenticationPlugin标签,账密在credentials.properties文件。
# password: manager
# pool:
# enabled: true
# max-connections: 10
# idle-timeout: 30000 # 空闲的连接过期时间,默认为30秒
#
# #rabbitmq配置 ( 注意: rabbitmq配置项需在spring的下级 )
# rabbitmq:
Expand Down
2 changes: 1 addition & 1 deletion docs/install/config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ mysql_pwd="jeepaydb123456"
# 若使用自行构建的 arm64 RocketMQ 镜像,请同时覆盖 rocketmqImage 并将下行改为 linux/arm64:
# rocketmqPlatform="linux/amd64"

# 源码 ref:install.sh 默认锁到 V3.2.8 release tag。如需切到其他 tag 或 master,
# 源码 ref:install.sh 默认锁到 V3.2.9 release tag。如需切到其他 tag 或 master,
# 取消以下注释并改为目标 ref;确保该 ref 下的 SQL / broker.conf.template / nginx.conf /
# conf/* 与所选业务镜像版本兼容。
# jeepayRef="master"
Expand Down
7 changes: 4 additions & 3 deletions docs/install/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ show_usage() {
nginxImage / managerImage / merchantImage 镜像覆盖
paymentImage 镜像覆盖
rocketmqPlatform RocketMQ 平台(默认 linux/amd64)
jeepayRef 源码 tag,默认 V3.2.8
jeepayRef 源码 tag,默认 V3.2.9
uiRelease 前端静态资源 release(默认 V3.0.0)
EOF
}
Expand Down Expand Up @@ -402,15 +402,16 @@ paymentImage=${paymentImage:-swr.cn-south-1.myhuaweicloud.com/jeepay/jeepay-paym
# 其他镜像(mysql/redis/nginx)已具备多架构 manifest,不再强制 platform。
rocketmqPlatform=${rocketmqPlatform:-linux/amd64}

# 源码 ref:默认锁到 V3.2.8 release tag,保证 git clone 下来的 SQL / broker.conf.template /
# 源码 ref:默认锁到 V3.2.9 release tag,保证 git clone 下来的 SQL / broker.conf.template /
# nginx.conf / conf/* 与业务镜像(3.2.0 业务镜像与 V3.2.x 配置兼容)契合,不受 master 后续演进影响。
# 如需用最新 master 或其他 tag,安装前导出 jeepayRef=xxx 覆盖即可(透传给 git clone --branch)。
jeepayRef=${jeepayRef:-V3.2.8}
jeepayRef=${jeepayRef:-V3.2.9}

# 第2步:拉取项目源代码 || 拉取脚本文件
echo "[2] 拉取项目源代码文件 (ref=$jeepayRef).... "
cd $rootDir/sources
git clone --branch "$jeepayRef" --depth 1 https://gitee.com/jeequan/jeepay.git
require_ok $? "拉取 jeepay 源码 (ref=$jeepayRef)。若 tag 尚未推送到 gitee,请改用 jeepayRef=master 覆盖或稍后重试。"
echo "[2] Done. "

#源码中install.sh文件目录
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,18 @@
import com.jeequan.jeepay.core.model.params.alipay.AlipayConfig;
import com.jeequan.jeepay.core.model.params.alipay.AlipayIsvParams;
import com.jeequan.jeepay.core.model.params.alipay.AlipayNormalMchParams;
import com.jeequan.jeepay.core.utils.AmountUtil;
import com.jeequan.jeepay.pay.channel.AbstractChannelNoticeService;
import com.jeequan.jeepay.pay.model.MchAppConfigContext;
import com.jeequan.jeepay.pay.rqrs.msg.ChannelRetMsg;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.MutablePair;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;

import jakarta.servlet.http.HttpServletRequest;
import java.math.BigDecimal;
import java.util.Map;

/*
Expand Down Expand Up @@ -75,6 +78,7 @@ public ChannelRetMsg doNotice(HttpServletRequest request, Object params, PayOrde
//配置参数获取
Byte useCert = null;
String alipaySignType, alipayPublicCert, alipayPublicKey = null;
String expectedAppId; // 业务字段交叉校验用
if(mchAppConfigContext.isIsvsubMch()){

// 获取支付参数
Expand All @@ -83,6 +87,7 @@ public ChannelRetMsg doNotice(HttpServletRequest request, Object params, PayOrde
alipaySignType = alipayParams.getSignType();
alipayPublicCert = alipayParams.getAlipayPublicCert();
alipayPublicKey = alipayParams.getAlipayPublicKey();
expectedAppId = alipayParams.getAppId();

}else{

Expand All @@ -93,6 +98,7 @@ public ChannelRetMsg doNotice(HttpServletRequest request, Object params, PayOrde
alipaySignType = alipayParams.getSignType();
alipayPublicCert = alipayParams.getAlipayPublicCert();
alipayPublicKey = alipayParams.getAlipayPublicKey();
expectedAppId = alipayParams.getAppId();
}

// 获取请求参数
Expand All @@ -113,6 +119,33 @@ public ChannelRetMsg doNotice(HttpServletRequest request, Object params, PayOrde
throw ResponseException.buildText("ERROR");
}

// 业务字段交叉校验:验签通过仅证明请求来自支付宝并由本商户密钥签名,
// 不保证回调内容就是这笔本地订单。多商户配置切换 / 跨订单签名穿越 / 沙箱与生产串扰
// 等场景下,仍可能出现 out_trade_no、金额、app_id 与本地订单上下文不一致的回调。
// 任一字段不一致 → 记 ERROR 日志 + 返回 SUCCESS 阻断支付宝重试 8 次 + 不更新订单状态。
String notifyOutTradeNo = jsonParams.getString("out_trade_no");
String notifyAppId = jsonParams.getString("app_id");
String notifyTotalAmount = jsonParams.getString("total_amount");
String expectedTotalAmount = AmountUtil.convertCent2Dollar(payOrder.getAmount());
boolean amountMatch;
try {
amountMatch = new BigDecimal(notifyTotalAmount).compareTo(new BigDecimal(expectedTotalAmount)) == 0;
} catch (NumberFormatException e) {
amountMatch = false;
}
if (!StringUtils.equals(notifyOutTradeNo, payOrder.getPayOrderId())
|| !StringUtils.equals(notifyAppId, expectedAppId)
|| !amountMatch) {
log.error("支付宝异步回调业务字段不匹配,已拒绝处理。out_trade_no=[{}] expected=[{}]; app_id=[{}] expected=[{}]; total_amount=[{}] expected=[{}]",
notifyOutTradeNo, payOrder.getPayOrderId(),
notifyAppId, expectedAppId,
notifyTotalAmount, expectedTotalAmount);
ChannelRetMsg dropped = new ChannelRetMsg();
dropped.setResponseEntity(textResp("SUCCESS"));
dropped.setChannelState(ChannelRetMsg.ChannelState.UNKNOWN);
return dropped;
}

//验签成功后判断上游订单状态
ResponseEntity okResponse = textResp("SUCCESS");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
import com.jeequan.jeepay.pay.model.MchAppConfigContext;
import com.jeequan.jeepay.pay.service.ConfigContextQueryService;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;

/*
* 【支付宝】支付通道工具包
Expand Down Expand Up @@ -108,6 +110,30 @@ public static String appendErrCode(String code, String subCode){
return StringUtils.defaultIfEmpty(subCode, code); //优先: subCode
}

/**
* 解析支付宝授权回调中拼接的 state 参数(格式:ISVNO_MCHAPPID)。
*
* 历史上 controller 直接写 split("_")[0]/[1],没做边界校验:
* 空串、不含 "_"、或者 "_" 在首尾的输入都会触发 ArrayIndexOutOfBoundsException
* 或得到空字符串,进而被恶意构造的请求触发回调链路 500。
*
* 使用 indexOf 取第一个 "_" 切分,而不是 split,避免 mchAppId 内部含 "_" 时被切碎。
*
* @return ImmutablePair(isvNo, mchAppId);解析失败返回 null,由调用方决定如何提示
*/
public static Pair<String, String> parseIsvAndMchAppIdState(String state) {
if (StringUtils.isEmpty(state)) {
return null;
}
int idx = state.indexOf('_');
// idx <= 0:没有 "_" 或 "_" 在最前面(isvNo 为空)
// idx >= state.length() - 1:" _" 在最后(mchAppId 为空)
if (idx <= 0 || idx >= state.length() - 1) {
return null;
}
return ImmutablePair.of(state.substring(0, idx), state.substring(idx + 1));
}

public static String appendErrMsg(String msg, String subMsg){

String result = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import com.jeequan.jeepay.service.impl.SysConfigService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
Expand Down Expand Up @@ -73,10 +74,16 @@ public class AlipayBizController extends AbstractCtrl {
@RequestMapping("/redirectAppToAppAuth/{isvAndMchAppId}")
public void redirectAppToAppAuth(@PathVariable("isvAndMchAppId") String isvAndMchAppId) throws IOException {

String isvNo = isvAndMchAppId.split("_")[0];
Pair<String, String> parsed = AlipayKit.parseIsvAndMchAppIdState(isvAndMchAppId);
if (parsed == null) {
throw new BizException("授权参数非法:isvAndMchAppId 格式应为 ISVNO_MCHAPPID");
}
String isvNo = parsed.getLeft();

AlipayIsvParams alipayIsvParams = (AlipayIsvParams) configContextQueryService.queryIsvParams(isvNo, CS.IF_CODE.ALIPAY);
alipayIsvParams.getSandbox();
if (alipayIsvParams == null) {
throw new BizException("ISV [" + isvNo + "] 未配置支付宝参数");
}

String oauthUrl = AlipayConfig.PROD_APP_TO_APP_AUTH_URL;
if(alipayIsvParams.getSandbox() != null && alipayIsvParams.getSandbox() == CS.YES){
Expand All @@ -101,10 +108,17 @@ public String appToAppAuthCallback() {

if(StringUtils.isNotEmpty(isvAndMchAppId) && StringUtils.isNotEmpty(appAuthCode)){
isAlipaySysAuth = false;
String isvNo = isvAndMchAppId.split("_")[0];
String mchAppId = isvAndMchAppId.split("_")[1];

Pair<String, String> parsed = AlipayKit.parseIsvAndMchAppIdState(isvAndMchAppId);
if (parsed == null) {
throw new BizException("授权回调 state 格式非法:应为 ISVNO_MCHAPPID");
}
String mchAppId = parsed.getRight();

MchApp mchApp = mchAppService.getById(mchAppId);
if (mchApp == null) {
throw new BizException("商户应用 [" + mchAppId + "] 不存在");
}

MchAppConfigContext mchAppConfigContext = configContextQueryService.queryMchInfoAndAppInfo(mchApp.getMchNo(), mchAppId);
AlipayClientWrapper alipayClientWrapper = configContextQueryService.getAlipayClientWrapper(mchAppConfigContext);
Expand Down
22 changes: 22 additions & 0 deletions upgrade.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,3 +246,25 @@
5. 修复: mysql8 / redis6 容器添加 --network-alias,解决应用按 hostname 解析失败(UnknownHostException)
6. 修复: install.sh 自动把 application.yml datasource 密码 rootroot 替换为 mysql_pwd,消除 Access denied
7. 修复: 移除 codegen 未用的 spring-context 依赖(消除 CVE-2024-38820 / CVE-2025-22233)

[v3.2.9_20260527]
> 安全 / 健壮性补丁版本:业务代码改动集中在支付宝渠道的安全加固和 install 脚本
> 健壮性,与 V3.2.8 业务镜像基本兼容;其中支付宝异步通知验签升级建议受影响
> 部署重新构建并发布 jeepay-payment 镜像。
1. 修复: 支付宝授权回调 state 参数解析鲁棒性——AlipayBizController.redirectAppToAppAuth /
appToAppAuthCallback 不再直接 split("_")[0]/[1],抽出 AlipayKit.parseIsvAndMchAppIdState
工具方法做长度 / 空值校验,配 mchAppService.getById 空校验,异常或被构造的 state
不再触发 ArrayIndexOutOfBoundsException / NPE。
2. 修复: 支付宝异步通知补业务字段交叉校验——AlipayChannelNoticeService.doNotice
在验签通过后再校验 out_trade_no / app_id / total_amount 三字段与本地订单一致,
消除多商户 / 沙箱与生产串扰 / 跨订单签名穿越场景下"看似合法但内容不匹配"通知
误处理风险。任一字段不一致 → 记 ERROR 日志 + 返回 SUCCESS 阻断重试 + 不更新订单状态。
3. 修复: install.sh 在 git clone 源码后加 require_ok 断言,tag 不存在或拉取失败
时立即硬退出,不再出现"实际是 git clone 失败但报到下游 MySQL 配置步骤"的误报。
4. 优化: conf/{payment,manager,merchant,devCommons/config}/application.yml 里
spring.activemq 段统一注释掉。默认 isys.mq.vender 已切到 rocketMQ 后,activemq
starter 仍会去连 activemq5:61616 重试,本次清理后启动日志不再有此类后台连接噪音。
5. 文档: README 增加"AI 接入助手"段,指向 jeepay-skills 仓库
(https://github.com/jeequan/jeepay-skills),供使用 Claude / Cursor / ChatGPT
等 AI 工具接入 Jeepay 的开发者。
6. 优化: install.sh 默认 jeepayRef 由 V3.2.8 跟进到 V3.2.9。
2 changes: 1 addition & 1 deletion version.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v3.2.8_20260422
v3.2.9_20260527
Loading