From 9f4db073a82d86af49e8507d64460b68298c54b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E6=A3=AE=E6=9E=97?= Date: Tue, 26 May 2026 10:05:14 +0800 Subject: [PATCH 1/5] =?UTF-8?q?docs(readme):=20=E5=A2=9E=E5=8A=A0=20AI=20?= =?UTF-8?q?=E6=8E=A5=E5=85=A5=E5=8A=A9=E6=89=8B=E6=AE=B5=EF=BC=8C=E5=BC=95?= =?UTF-8?q?=E5=AF=BC=E5=88=B0=20jeepay-skills=20=E4=BB=93=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在「SDK 资源」之后补一节「AI 接入助手」,列出独立的 jeepay-skills 仓库(https://github.com/jeequan/jeepay-skills)。让用 Claude / Cursor / ChatGPT 等 AI 工具接入 Jeepay 的开发者能从 Jeepay 主仓库 README 一步 找到配套的接入助手;不用 AI 的开发者也可以直接把它当结构化的接入 指南文档浏览。 --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 2401cfd4..6668eddd 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,14 @@ Jeepay 已提供 Java、Python SDK,以及 PHP 对接 Demo: - SDK 下载地址: - Java SDK 仓库: +## AI 接入助手 + +如果你用 Claude / Cursor / ChatGPT 等 AI 工具接入 Jeepay,可以使用 [Jeepay Skills](https://github.com/jeequan/jeepay-skills) —— 把签名陷阱、字段大小写、wayCode 选择等 AI 容易踩坑的细节系统化整理,让 AI 一次接对。 + +- 仓库地址: +- 支持 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) From 9f64c3eb8a47073f563d8c1d379de22885a7a8c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E6=A3=AE=E6=9E=97?= Date: Thu, 23 Apr 2026 11:48:13 +0800 Subject: [PATCH 2/5] =?UTF-8?q?fix(install):=20git=20clone=20=E6=BA=90?= =?UTF-8?q?=E7=A0=81=E5=90=8E=E5=8A=A0=20require=5Fok=EF=BC=8C=E9=81=BF?= =?UTF-8?q?=E5=85=8D=20tag=20=E4=B8=8D=E5=AD=98=E5=9C=A8=E8=A2=AB=E5=90=8E?= =?UTF-8?q?=E7=BB=AD=E6=AD=A5=E9=AA=A4=E5=90=9E=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit V3.2.8 tag 未同步到 gitee 时,git clone --branch V3.2.8 失败,脚本继续 执行到 [3] 才以 'cd: /jeepayhomes/sources/jeepay/docs/install: No such file or directory' / '复制 MySQL 配置 my.cnf' 爆错,误导用户以为是 MySQL 步骤问题。补上 require_ok 后 clone 失败立刻退出并打印明确原因与规避 方式(jeepayRef=master 覆盖)。 --- docs/install/install.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/install/install.sh b/docs/install/install.sh index 542d68e0..65e25d05 100644 --- a/docs/install/install.sh +++ b/docs/install/install.sh @@ -411,6 +411,7 @@ jeepayRef=${jeepayRef:-V3.2.7} 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文件目录 From 0b10193536435ddfa043c33d3a73dad0fb044fa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E6=A3=AE=E6=9E=97?= Date: Sun, 24 May 2026 10:16:49 +0800 Subject: [PATCH 3/5] =?UTF-8?q?fix(conf):=20=E9=BB=98=E8=AE=A4=20MQ=20?= =?UTF-8?q?=E5=88=87=E5=88=B0=20rocketMQ=20=E5=90=8E=E6=B3=A8=E9=87=8A?= =?UTF-8?q?=E6=8E=89=20spring.activemq=20=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 默认 isys.mq.vender 已是 rocketMQ,但 conf 下 4 份 application.yml 里 spring.activemq.* 仍保持启用状态。由于 activemq starter 是 compile scope,Spring Boot 会真去解析这段配置并起 ConnectionFactory 连接默认的 activemq5:61616 / 127.0.0.1:61616,failover 协议会一直 后台重连,产生噪音和资源占用,也容易让阅读者误以为系统在用 activeMQ。 按已有 rabbitmq / rocketmq(spring 段) 注释惯例,把这段也注释掉, 并在标题注里说明:默认走 rocketMQ,如需切回 activeMQ 请同步改 isys.mq.vender。涉及文件:conf/{payment,manager,merchant,devCommons/config}/application.yml。 refs #105 --- conf/devCommons/config/application.yml | 20 ++++++++++---------- conf/manager/application.yml | 20 ++++++++++---------- conf/merchant/application.yml | 20 ++++++++++---------- conf/payment/application.yml | 20 ++++++++++---------- 4 files changed, 40 insertions(+), 40 deletions(-) diff --git a/conf/devCommons/config/application.yml b/conf/devCommons/config/application.yml index 5159fc74..fc0c25f8 100644 --- a/conf/devCommons/config/application.yml +++ b/conf/devCommons/config/application.yml @@ -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: diff --git a/conf/manager/application.yml b/conf/manager/application.yml index 9076575b..1a01b51e 100644 --- a/conf/manager/application.yml +++ b/conf/manager/application.yml @@ -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: diff --git a/conf/merchant/application.yml b/conf/merchant/application.yml index 9b7349fe..48bf86c4 100644 --- a/conf/merchant/application.yml +++ b/conf/merchant/application.yml @@ -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: diff --git a/conf/payment/application.yml b/conf/payment/application.yml index 52a959bf..c21250e2 100644 --- a/conf/payment/application.yml +++ b/conf/payment/application.yml @@ -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: From b20b680bea072d7a31fea1856c0d5be6f485270c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E6=A3=AE=E6=9E=97?= Date: Sun, 24 May 2026 16:16:51 +0800 Subject: [PATCH 4/5] =?UTF-8?q?fix(alipay):=20=E5=8A=A0=E5=9B=BA=E6=8E=88?= =?UTF-8?q?=E6=9D=83=E5=9B=9E=E8=B0=83=20state=20=E8=A7=A3=E6=9E=90=20+=20?= =?UTF-8?q?=E5=BC=82=E6=AD=A5=E9=80=9A=E7=9F=A5=E8=A1=A5=E4=B8=9A=E5=8A=A1?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=E4=BA=A4=E5=8F=89=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复 #81(感谢 @xiaowuDev 提交的 PR 描述定位到这两处问题)。 1) 授权回调 state 参数解析鲁棒性 - 原 AlipayBizController.redirectAppToAppAuth / appToAppAuthCallback 直接 split("_")[0]/[1],没做长度、空值、空串校验,异常或被构造的 state 会触发 ArrayIndexOutOfBoundsException / 空指针,回调链路出现 500。 - 抽出 AlipayKit.parseIsvAndMchAppIdState(String) 工具方法:用 indexOf 而非 split(避免 mchAppId 内部含 "_" 时被切碎),任一段为空都返回 null。 - 两个回调入口改用工具方法 + null 校验 + mchAppService.getById 结果空校验, 抛出 BizException 由现有异常处理统一展示给用户。 - 顺手清理 redirectAppToAppAuth 里两行无效代码(getSandbox() 返回值未使用、 isvNo 局部变量从未读取)。 2) 异步通知业务字段交叉校验 - 原 AlipayChannelNoticeService.doNotice 验签通过后直接信任 jsonParams, 仅取了 trade_no / buyer_id / trade_status 来设置 ChannelRetMsg。 验签通过仅证明请求来自支付宝并由本商户密钥签名,不保证回调内容就是 这笔本地订单。多商户配置切换 / 跨订单签名穿越 / 沙箱与生产串扰 等场景下,仍可能出现 out_trade_no / 金额 / app_id 与本地订单上下文 不一致的回调,按现有逻辑会把这笔"看似合法"的回调直接当成功来回写订单。 - 在验签通过后、设置 ChannelRetMsg 前补一道校验: out_trade_no 必须等于 payOrder.payOrderId、 app_id 必须等于商户配置里的 appId、 total_amount(元)必须等于本地金额(分→元,用 BigDecimal compareTo 比对避免 scale 差异)。 - 任一字段不一致:记 ERROR 日志(带具体不匹配的字段与双方值,便于排查)+ 返回 SUCCESS 给支付宝阻断重试 8 次浪费资源 + 不更新订单状态。 - ChannelState 用 UNKNOWN 而非 CONFIRM_FAIL,避免被下游误判为"已确认失败" 从而触发退款补偿逻辑。 --- .../alipay/AlipayChannelNoticeService.java | 33 +++++++++++++++++++ .../jeepay/pay/channel/alipay/AlipayKit.java | 26 +++++++++++++++ .../alipay/ctrl/AlipayBizController.java | 22 ++++++++++--- 3 files changed, 77 insertions(+), 4 deletions(-) diff --git a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/alipay/AlipayChannelNoticeService.java b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/alipay/AlipayChannelNoticeService.java index a37dd645..bcfbbbb8 100644 --- a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/alipay/AlipayChannelNoticeService.java +++ b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/alipay/AlipayChannelNoticeService.java @@ -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; /* @@ -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()){ // 获取支付参数 @@ -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{ @@ -93,6 +98,7 @@ public ChannelRetMsg doNotice(HttpServletRequest request, Object params, PayOrde alipaySignType = alipayParams.getSignType(); alipayPublicCert = alipayParams.getAlipayPublicCert(); alipayPublicKey = alipayParams.getAlipayPublicKey(); + expectedAppId = alipayParams.getAppId(); } // 获取请求参数 @@ -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"); diff --git a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/alipay/AlipayKit.java b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/alipay/AlipayKit.java index ea90a1a4..ba4f6f5f 100644 --- a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/alipay/AlipayKit.java +++ b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/alipay/AlipayKit.java @@ -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; /* * 【支付宝】支付通道工具包 @@ -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 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; diff --git a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/alipay/ctrl/AlipayBizController.java b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/alipay/ctrl/AlipayBizController.java index 54abec57..e00d4bb0 100644 --- a/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/alipay/ctrl/AlipayBizController.java +++ b/jeepay-payment/src/main/java/com/jeequan/jeepay/pay/channel/alipay/ctrl/AlipayBizController.java @@ -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; @@ -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 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){ @@ -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 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); From 376f1ae30120deb474a9a277eaff1190e7e3ef19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E6=A3=AE=E6=9E=97?= Date: Wed, 27 May 2026 16:29:23 +0800 Subject: [PATCH 5/5] =?UTF-8?q?chore(release):=20=E6=92=9E=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E5=8F=B7=E5=88=B0=20V3.2.9=20+=20=E8=A1=A5=20V3.2.9?= =?UTF-8?q?=20changelog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - version.md:v3.2.8_20260422 → v3.2.9_20260527 - docs/install/install.sh / config.sh:jeepayRef 默认值跟进到 V3.2.9 - upgrade.md:新增 V3.2.9 段,说明本版本主要内容: * 支付宝授权回调 state 解析鲁棒性 * 支付宝异步通知补业务字段交叉校验 * install.sh git clone 失败硬退出 * conf/* 里 spring.activemq 段统一注释掉 * README 加 AI 接入助手段 --- docs/install/config.sh | 2 +- docs/install/install.sh | 6 +++--- upgrade.md | 22 ++++++++++++++++++++++ version.md | 2 +- 4 files changed, 27 insertions(+), 5 deletions(-) diff --git a/docs/install/config.sh b/docs/install/config.sh index ba6c89f8..02dfe2a4 100644 --- a/docs/install/config.sh +++ b/docs/install/config.sh @@ -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" diff --git a/docs/install/install.sh b/docs/install/install.sh index b6a4e3dc..9dddbfb4 100644 --- a/docs/install/install.sh +++ b/docs/install/install.sh @@ -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 } @@ -402,10 +402,10 @@ 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).... " diff --git a/upgrade.md b/upgrade.md index b9a1c7b7..92163b8f 100644 --- a/upgrade.md +++ b/upgrade.md @@ -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。 diff --git a/version.md b/version.md index eac51776..940f619c 100644 --- a/version.md +++ b/version.md @@ -1 +1 @@ -v3.2.8_20260422 +v3.2.9_20260527