feat:支付宝支付证书模式

This commit is contained in:
dzc committed 2026-11-10 11:32:21 +08:00
1 parent c226b532de
commit ba02133449
12 files changed
+867 -157

No files matched your search

+45 -1
View File
@@ -10,6 +10,7 @@ app-plus:
# 网页/移动应用:聚合支付宝 WAP 签名及付款通知;原 App 继续使用此组。
ali-pay-config:
app-id: ${ALIPAY_WEB_APP_ID}
sign-mode: KEY # 省略时默认 KEY;证书模式示例见下文
app-private-key: ${ALIPAY_WEB_APP_PRIVATE_KEY}
alipay-public-key: ${ALIPAY_WEB_PUBLIC_KEY}
base-callback-url: ${THIRD_PUBLIC_BASE_URL} # 网关公网地址,包含实际 /api 子路径
@@ -18,6 +19,7 @@ app-plus:
# 小程序及历史小程序交易凭据:当前 H5 新下单不读取此组;已有相关流水时保留原凭据。
ali-pay-mini-config:
app-id: ${ALIPAY_MINI_APP_ID}
sign-mode: KEY # 每组应用独立选择 KEY 或 CERT
app-private-key: ${ALIPAY_MINI_APP_PRIVATE_KEY}
alipay-public-key: ${ALIPAY_MINI_PUBLIC_KEY}
base-callback-url: ${THIRD_PUBLIC_BASE_URL}
@@ -26,6 +28,7 @@ app-plus:
# 第三方应用独立预留,当前不参与 App/聚合新下单;不用时整组省略。
# ali-pay-third-party-config:
# app-id: ${ALIPAY_THIRD_PARTY_APP_ID}
# sign-mode: KEY
# app-private-key: ${ALIPAY_THIRD_PARTY_APP_PRIVATE_KEY}
# alipay-public-key: ${ALIPAY_THIRD_PARTY_PUBLIC_KEY}
# base-callback-url: ${THIRD_PUBLIC_BASE_URL}
@@ -43,7 +46,7 @@ app-plus:
`h5-base-url` 只接受 HTTPS 基础地址,不能带查询参数、Hash 或用户信息。可以包含实际部署子路径;H5 的路由 base 需对应配置。
支付宝按网页/移动应用、小程序、第三方应用分类保存配套 AppID、应用私钥和支付宝公钥。新支付宝聚合付款使用现有 `ali-pay-config` 的 `alipay.trade.wap.pay` 手机网站支付能力,产品码为 `QUICK_WAP_WAY`,不再先取网页 OAuth 身份,也不调用 `alipay.trade.create`。网页/移动应用需要已开通手机网站支付及实际直付通商户收单权限,切换凭据不会自动开通产品。没有新增 `ali-pay-h5-config`。原 App 默认配置与 `app.pay` 路径保持兼容。聚合支付宝旧 OAuth、`alipay.trade.create` 和 H5 `tradeNO` 调起代码已删除。
支付宝按网页/移动应用、小程序、第三方应用分类保存配套 AppID、应用私钥及支付宝公钥或证书。新支付宝聚合付款使用现有 `ali-pay-config` 的 `alipay.trade.wap.pay` 手机网站支付能力,产品码为 `QUICK_WAP_WAY`,不再先取网页 OAuth 身份,也不调用 `alipay.trade.create`。网页/移动应用需要已开通手机网站支付及实际直付通商户收单权限,切换凭据不会自动开通产品。没有新增 `ali-pay-h5-config`。原 App 默认配置与 `app.pay` 路径保持兼容。聚合支付宝旧 OAuth、`alipay.trade.create` 和 H5 `tradeNO` 调起代码已删除。
新支付单在已有 `mch_pay_setting_json` 中保存网页应用的非敏感 `alipayAppId`,查单、关单、退款、退款查询及结算/分账按这个 AppID 选择原应用;支付与退款回调先按已登记 AppID 验签,再校验是否与支付单一致。历史小程序流水仍按其快照选择原小程序凭据;旧快照没有该标识时继续使用原网页/移动应用。只启用微信的商户不要求支付宝配置。更新凭据或切换下单应用后需重启 third 的全部实例。本次不增加数据库字段,不改写历史订单。
@@ -53,6 +56,47 @@ H5 只使用项目根目录一个 `.env`,变量命名沿用参考 App:开发
后端仅为配置的 H5 精确 Origin 开放匿名商户、创建、上下文和状态 JSON 接口的跨域访问,不开放凭据跨域;其他接口保持既有安全规则。
### 支付宝密钥与证书模式
三组应用分别支持 `KEY` 和 `CERT`,未提供 `sign-mode` 时使用 `KEY`。上述配置展示普通公钥方式,网页/App 应用切换证书时采用以下配置。小程序及第三方应用使用各自配置组的同名字段,允许三组应用混用两种模式。
```yaml
app-plus:
pay:
ali-pay-config:
app-id: ${ALIPAY_WEB_APP_ID}
sign-mode: CERT
app-private-key: ${ALIPAY_WEB_APP_PRIVATE_KEY}
app-cert-path: ${ALIPAY_WEB_APP_CERT_PATH}
alipay-public-cert-path: ${ALIPAY_WEB_PUBLIC_CERT_PATH}
alipay-root-cert-path: ${ALIPAY_WEB_ROOT_CERT_PATH}
# 过渡期保留原支付宝公钥,通知证书验签失败后使用此公钥再次校验。
# 全新证书应用或过渡结束后可省略本项。
alipay-public-key: ${ALIPAY_WEB_PUBLIC_KEY:}
base-callback-url: ${THIRD_PUBLIC_BASE_URL}
pay-callback-url: /third/third-app-pay/app/pay-callback
refund-callback-url: /third/third-app-pay/app/refund-callback
```
| 配置项 | KEY 要求 | CERT 要求 |
| --- | --- | --- |
| `app-id` | 必填,当前应用标识 | 必填,当前应用标识 |
| `app-private-key` | 必填,应用私钥 | 必填,与应用公钥证书配套的私钥 |
| `alipay-public-key` | 必填,支付宝公钥 | 可选,当前应用原公钥用于过渡期通知 |
| `app-cert-path` | 可省略 | 必填,应用公钥证书绝对路径 |
| `alipay-public-cert-path` | 可省略 | 必填,支付宝公钥证书绝对路径 |
| `alipay-root-cert-path` | 可省略 | 必填,支付宝根证书绝对路径,可含多个根证书 |
三份证书采用 PEM 格式,并以只读文件挂载到每个 thirdparty 容器或服务器。例如应用证书挂载至 `/opt/certs/alipay/appCertPublicKey.crt`,配置项填写容器内部绝对路径。运行用户须具备目录访问和证书读取权限,密钥和证书材料由部署环境管理。
统一组件按已登记 AppID 延迟初始化,在同一次初始化中读取证书内容并固定客户端、模式和通知公钥。配置模式、私钥、公钥或证书文件更新后重启全部 thirdparty 实例,新的进程重新读取凭据。证书缺失、路径不可读或内容无法解析时反馈对应配置错误,初始化失败的实例可以在修正配置或文件后重试。
支付请求使用 RSA2、UTF-8 和 JSON。服务端支付、查询、退款、结算分账、商户资料及图片请求按模式执行普通或证书接口。App 沿用 `sdkExecute`,WAP 沿用 `pageExecute(request, "GET")`,应用证书及根证书序列号由 SDK 自动填入本地签名参数。
支付、退款及订阅通知按已登记 `app_id` 选择验签凭据。CERT 优先使用固定证书公钥,签名不匹配且配置了原公钥时再执行过渡期校验。每次验签使用独立参数副本,原始签名字段保留用于后续处理。配置和证书初始化失败按异常反馈。支付单固定应用、商户、金额和重复通知的业务检查继续执行。
发布先部署兼容现有 KEY 配置的代码,再逐应用完成支付宝开放平台的证书配置、部署三份文件、设置 `sign-mode: CERT` 并重启全部实例。切换期间保留原支付宝公钥,历史通知处理完成后可移除该项并再次重启。集中联调应覆盖支付及通知、查单、关闭和撤销、退款及退款查询、结算分账、商户资料与图片上传,并检查历史订单仍使用其固定 AppID。
## 平台配置
- 微信沿用服务商统一公众号 OAuth 与 JSAPI 下单配置,确认公众号网页授权域名对应现有后端 OAuth 回调域名。
@@ -0,0 +1,88 @@
package com.bsd.pjcloud.thirdparty.config;
import cn.hutool.core.util.StrUtil;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.AlipayRequest;
import com.alipay.api.AlipayResponse;
import com.alipay.api.internal.util.AlipaySignature;
import java.util.HashMap;
import java.util.Map;
/**
* 一个支付宝应用在本次进程生命周期内固定的 SDK 客户端、签名模式及通知验签凭据。
* 请求执行与模式保存在同一快照中,避免更新配置后用旧客户端执行另一种模式的请求。
*/
public final class AlipayClientContext {
/** 已初始化且可复用的支付宝 SDK 客户端,由统一支持组件创建。 */
private final AlipayClient sdkClient;
/** 创建客户端时确定的签名模式,后续配置变更通过重启生效。 */
private final AppPlusPayConfig.AliPaySignMode signMode;
/** 当前通知验签公钥,CERT 模式从首次初始化读取的支付宝证书内容提取。 */
private final String verificationPublicKey;
/** CERT 模式明确保留的旧支付宝公钥,空值表示仅采用当前证书验签。 */
private final String legacyPublicKey;
/**
* 固定同一应用的 SDK 与验签凭据,调用前由支持组件完成配置和证书校验。
*/
AlipayClientContext(AlipayClient sdkClient, AppPlusPayConfig.AliPaySignMode signMode, String verificationPublicKey, String legacyPublicKey) {
this.sdkClient = sdkClient;
this.signMode = signMode;
this.verificationPublicKey = verificationPublicKey;
this.legacyPublicKey = legacyPublicKey;
}
/**
* 发起服务端请求,按固定模式选择普通或证书执行接口,保留请求中的业务及授权参数。
* SDK、网络异常交回业务层按原有查单、重试和幂等规则处理。
*/
public <T extends AlipayResponse> T execute(AlipayRequest<T> request) throws AlipayApiException {
if (signMode == AppPlusPayConfig.AliPaySignMode.CERT) {
return sdkClient.certificateExecute(request);
}
return sdkClient.execute(request);
}
/** 本地生成 App 支付签名参数;证书序列号由 SDK 从固定证书配置自动填充。 */
public <T extends AlipayResponse> T sdkExecute(AlipayRequest<T> request) throws AlipayApiException {
return sdkClient.sdkExecute(request);
}
/** 本地生成网页支付地址或表单,httpMethod 沿用调用方的 GET/POST 语义。 */
public <T extends AlipayResponse> T pageExecute(AlipayRequest<T> request, String httpMethod) throws AlipayApiException {
return sdkClient.pageExecute(request, httpMethod);
}
/**
* 验证本应用通知,证书签名不匹配时可使用明确保留的旧公钥再次校验。
* 每次交给 SDK 独立参数副本,保留原始 sign、sign_type 供后续验证及业务处理。
*/
boolean verifySignature(Map<String, String> params) {
if (params == null || StrUtil.isBlank(params.get("sign"))) {
return false;
}
if (verifyWithPublicKey(params, verificationPublicKey)) {
return true;
}
return signMode == AppPlusPayConfig.AliPaySignMode.CERT && StrUtil.isNotBlank(legacyPublicKey)
&& verifyWithPublicKey(params, legacyPublicKey);
}
/**
* 使用可信配置中的公钥执行 RSA2 验签,签名内容或长度异常均按本次验签失败处理。
* 配置及证书初始化在调用前完成,其异常不会进入过渡期补充验签。
*/
private boolean verifyWithPublicKey(Map<String, String> params, String publicKey) {
try {
return AlipaySignature.rsaCheckV1(new HashMap<>(params), publicKey, "UTF-8", "RSA2");
} catch (AlipayApiException exception) {
return false;
}
}
}
@@ -0,0 +1,142 @@
package com.bsd.pjcloud.thirdparty.config;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayConfig;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.internal.util.AntCertificationUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
/**
* thirdparty 支付宝应用的统一初始化、配置校验及通知验签入口。
* 三组已登记应用分别延迟初始化,成功后固定客户端与凭据,更新配置及证书须重启全部实例。
*/
@Component
@RequiredArgsConstructor
public class AlipayClientSupport {
/** 已登记的网页/App、小程序及第三方应用配置,来源于 app-plus.pay。 */
private final AppPlusPayConfig appPlusPayConfig;
/** 按已登记 AppID 保存成功初始化的快照,仅在同步获取方法中读写,数量受三组配置限制。 */
private final Map<String, AlipayClientContext> applicationClients = new HashMap<>();
/**
* 校验指定模式的必填凭据;实际证书文件只在客户端首次初始化时读取和解析。
* 供支付前置检查复用,CERT 模式的旧支付宝公钥为可选项。
*/
public static void validateConfiguration(AppPlusPayConfig.AliPayConfig config) {
if (config == null || StrUtil.hasBlank(config.getAppId(), config.getAppPrivateKey())) {
throw new IllegalStateException("支付宝应用配置不完整,请配置 app-id 和 app-private-key");
}
if (config.getSignMode() == null) {
throw new IllegalStateException("支付宝签名模式不能为空,请配置 KEY 或 CERT");
}
if (config.getSignMode() == AppPlusPayConfig.AliPaySignMode.KEY && StrUtil.isBlank(config.getAlipayPublicKey())) {
throw new IllegalStateException("支付宝 KEY 模式缺少 alipay-public-key");
}
if (config.getSignMode() == AppPlusPayConfig.AliPaySignMode.CERT
&& StrUtil.hasBlank(config.getAppCertPath(), config.getAlipayPublicCertPath(), config.getAlipayRootCertPath())) {
throw new IllegalStateException("支付宝 CERT 模式须配置 app-cert-path、alipay-public-cert-path 和 alipay-root-cert-path");
}
}
/**
* 获取已登记应用的固定运行快照;未知、重复 AppID 或非登记配置拒绝使用。
* 同步初始化保证并发首次访问只发布一个完整实例,初始化失败时允许修正后重试。
*/
public synchronized AlipayClientContext getClient(AppPlusPayConfig.AliPayConfig config) throws AlipayApiException {
String appId = config == null ? null : config.getAppId();
if (StrUtil.isBlank(appId) || appPlusPayConfig.findAliPayConfig(appId) != config) {
throw new IllegalStateException("支付宝应用未唯一登记,请检查对应配置组的 app-id");
}
AlipayClientContext client = applicationClients.get(appId);
if (client == null) {
// 初始化全程使用独立配置副本,使 SDK 参数、模式和通知公钥属于同一个运行快照。
AppPlusPayConfig.AliPayConfig snapshot = BeanUtil.copyProperties(config, AppPlusPayConfig.AliPayConfig.class);
snapshot.setAppId(appId);
validateConfiguration(snapshot);
client = createClient(snapshot);
applicationClients.put(appId, client);
}
return client;
}
/**
* 使用明确选定的应用验证通知;配置异常交回调用方,不能将初始化失败当作旧公钥匹配。
*/
public boolean verifyCallback(AppPlusPayConfig.AliPayConfig config, Map<String, String> params) throws AlipayApiException {
return config != null && params != null && config.getAppId() != null
&& config.getAppId().equals(params.get("app_id")) && getClient(config).verifySignature(params);
}
/**
* 以指定应用创建 SDK 和通知验签快照;CERT 使用同一批读取内容初始化两者,避免文件轮换时错配。
* 旧支付宝公钥仅保存到通知快照,CERT 的 SDK 由证书建立响应验签信任。
*/
private AlipayClientContext createClient(AppPlusPayConfig.AliPayConfig config) throws AlipayApiException {
AlipayConfig sdkConfig = new AlipayConfig();
sdkConfig.setServerUrl("https://openapi.alipay.com/gateway.do");
sdkConfig.setAppId(config.getAppId());
sdkConfig.setPrivateKey(config.getAppPrivateKey());
sdkConfig.setFormat("json");
sdkConfig.setCharset("UTF-8");
sdkConfig.setSignType("RSA2");
String verificationPublicKey = config.getAlipayPublicKey();
String legacyPublicKey = null;
if (config.getSignMode() == AppPlusPayConfig.AliPaySignMode.CERT) {
String appCertificate = readCertificate(config.getAppCertPath(), "应用公钥证书");
String alipayCertificate = readCertificate(config.getAlipayPublicCertPath(), "支付宝公钥证书");
String rootCertificate = readCertificate(config.getAlipayRootCertPath(), "支付宝根证书");
sdkConfig.setAppCertPath(config.getAppCertPath());
sdkConfig.setAppCertContent(appCertificate);
sdkConfig.setAlipayPublicCertPath(config.getAlipayPublicCertPath());
sdkConfig.setAlipayPublicCertContent(alipayCertificate);
sdkConfig.setRootCertPath(config.getAlipayRootCertPath());
sdkConfig.setRootCertContent(rootCertificate);
verificationPublicKey = Base64.getEncoder().encodeToString(AntCertificationUtil.getCertFromContent(alipayCertificate).getPublicKey().getEncoded());
legacyPublicKey = config.getAlipayPublicKey();
} else {
sdkConfig.setAlipayPublicKey(config.getAlipayPublicKey());
}
return new AlipayClientContext(new DefaultAlipayClient(sdkConfig), config.getSignMode(), verificationPublicKey, legacyPublicKey);
}
/**
* 从服务器绝对路径一次读取 PEM 证书并确认包含可解析的 X.509 证书,根证书支持证书集合。
* 错误仅描述证书类型和操作,避免把文件内容或底层异常中的凭据传出。
*/
private String readCertificate(String configuredPath, String certificateLabel) {
try {
Path path = Path.of(configuredPath);
if (!path.isAbsolute() || !Files.isRegularFile(path) || !Files.isReadable(path)) {
throw new IllegalStateException(certificateLabel + "须为可读取的本地文件绝对路径");
}
String content = Files.readString(path, StandardCharsets.UTF_8);
try (ByteArrayInputStream input = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8))) {
if (CertificateFactory.getInstance("X.509").generateCertificates(input).isEmpty()) {
throw new IllegalStateException(certificateLabel + "文件未包含有效的 X.509 证书");
}
}
return content;
} catch (IOException | InvalidPathException exception) {
throw new IllegalStateException(certificateLabel + "读取失败,请检查路径及文件权限");
} catch (CertificateException exception) {
throw new IllegalStateException(certificateLabel + "解析失败,请提供有效的 PEM 格式 X.509 证书");
}
}
}
@@ -34,8 +34,8 @@ public class AppPlusPayConfig {
* @return 已校验凭据、公网基础地址和支付通知路径的现有 ali-pay-config
*/
public AliPayConfig requireAliPayWebPayConfig() {
if (AliPayConfig == null || StrUtil.hasBlank(AliPayConfig.getAppId(), AliPayConfig.getAppPrivateKey(),
AliPayConfig.getAlipayPublicKey(), AliPayConfig.getBaseCallbackUrl())) {
AlipayClientSupport.validateConfiguration(AliPayConfig);
if (StrUtil.isBlank(AliPayConfig.getBaseCallbackUrl())) {
throw new IllegalStateException("支付宝手机网站支付配置不完整,请配置网页/移动应用 app-plus.pay.ali-pay-config");
}
AliPayConfig config = findAliPayConfig(AliPayConfig.getAppId());
@@ -167,23 +167,45 @@ public class AppPlusPayConfig {
}
/** 支付宝应用的签名配置模式,名称直接绑定配置中心的 sign-mode。 */
public enum AliPaySignMode {
/** 普通公钥模式,应用私钥签名、支付宝公钥验签,兼容省略模式的历史配置。 */
KEY,
/** 证书模式,使用应用证书、支付宝公钥证书与根证书,服务端请求走证书执行接口。 */
CERT
}
/** 同一支付宝应用的完整凭据与回调配置,不允许只替换 AppID 而混用其他应用密钥。 */
@Data
public static class AliPayConfig {
/** 支付宝开放平台的应用 ID,必须与当前组的应用私钥及支付宝公钥配套。 */
/** 支付宝开放平台应用 ID,须与本组私钥及公钥或证书配套。 */
private String appId;
/** 签名凭据模式,省略时沿用 KEY;更新后须重启全部 thirdparty 实例。 */
private AliPaySignMode signMode = AliPaySignMode.KEY;
/**
* 应用私钥
*/
private String appPrivateKey;
/**
* 支付公钥
* KEY 模式必填的支付宝公钥;CERT 模式可保留原公钥,专供过渡期通知补充验签。
*/
private String alipayPublicKey;
/** CERT 模式必填的应用公钥证书绝对路径,文件在应用首次使用时读取并固定到内存。 */
private String appCertPath;
/** CERT 模式必填的支付宝公钥证书绝对路径,用于响应及异步通知验签。 */
private String alipayPublicCertPath;
/** CERT 模式必填的支付宝根证书文件绝对路径,允许一个文件包含多份根证书。 */
private String alipayRootCertPath;
/**
* 基础回调地址
*/
@@ -8,9 +8,7 @@ import com.bsd.pjcloud.common.core.exception.ErrorCodes;
import com.bsd.pjcloud.common.core.exception.I18nBizException;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.alipay.api.AlipayClient;
import com.alipay.api.AlipayConfig;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.AlipayApiException;
import com.alipay.api.domain.AlipayTradeWapPayModel;
import com.alipay.api.domain.SettleDetailInfo;
import com.alipay.api.domain.SettleInfo;
@@ -22,6 +20,8 @@ import com.alipay.api.response.AlipayTradeCloseResponse;
import com.alipay.api.response.AlipayTradeQueryResponse;
import com.bsd.pjcloud.common.core.constant.enums.YesNoEnum;
import com.bsd.pjcloud.thirdparty.config.AppPlusPayConfig;
import com.bsd.pjcloud.thirdparty.config.AlipayClientContext;
import com.bsd.pjcloud.thirdparty.config.AlipayClientSupport;
import com.bsd.pjcloud.thirdparty.constant.miniplatform.AppPlusPlatformTypeEnum;
import com.bsd.pjcloud.thirdparty.constant.pay.ThirdPayOrderStatusEnum;
import com.bsd.pjcloud.thirdparty.entity.pay.ThirdPayOrderEntity;
@@ -103,6 +103,9 @@ public class ThirdAggregateAliPayServiceImpl implements ThirdAggregateAliPayServ
*/
private final ThirdAppPlusAliPayService thirdAppPlusAliPayService;
/** 网页/App 应用的双模式客户端入口,与原 App、退款及商户管理复用同一应用快照。 */
private final AlipayClientSupport alipayClientSupport;
/** 聚合子订单创建前校验支付宝直付通关系归属、绑定状态和抽佣比例。 */
@Autowired(required = false)
private ThirdMchFzRelationService mchFzRelationService;
@@ -111,11 +114,6 @@ public class ThirdAggregateAliPayServiceImpl implements ThirdAggregateAliPayServ
@Autowired
private StringEncryptor stringEncryptor;
/**
* 网页/移动应用的支付客户端,延迟初始化并在聚合下单请求间复用。
*/
private volatile AlipayClient alipayClient;
/**
* 签名 WAP GET 请求并先落本地待支付流水;真正的渠道建单在浏览器访问官方网关时发生。
* 复用直付通全额结算快照,不要求或伪造 buyer_id/open_id;生成地址不能当作支付宝已受理。
@@ -218,38 +216,17 @@ public class ThirdAggregateAliPayServiceImpl implements ThirdAggregateAliPayServ
}
/**
* 获取延迟初始化的支付宝客户端。
* 获取延迟初始化且固定签名模式的支付宝网页支付上下文。
*
* @return 使用现有网页/移动应用 AppID 与配套密钥初始化的支付客户端
* @return 使用现有网页/移动应用凭据初始化的客户端和模式快照
*/
protected synchronized AlipayClient getAlipayClient() {
if (alipayClient == null) {
try {
alipayClient = createAlipayClient(appPlusPayConfig.requireAliPayWebPayConfig());
} catch (Exception exception) {
throw new I18nBizException(ErrorCodes.THIRD_ALIPAY_CLIENT_INIT_FAIL);
}
protected AlipayClientContext getAlipayClient() {
try {
return alipayClientSupport.getClient(appPlusPayConfig.requireAliPayWebPayConfig());
} catch (AlipayApiException exception) {
log.error("支付宝网页应用客户端初始化失败,errorCode:{}", exception.getErrCode());
throw new I18nBizException(ErrorCodes.THIRD_ALIPAY_CLIENT_INIT_FAIL);
}
return alipayClient;
}
/**
* 仅以调用方明确指定的服务端配置创建 SDK 客户端,不在两类应用之间自动降级。
*
* @param config 当前调用使用的完整应用凭据
* @return 对应应用的客户端
* @throws Exception SDK 初始化失败时由上层转换为统一业务错误
*/
private AlipayClient createAlipayClient(AppPlusPayConfig.AliPayConfig config) throws Exception {
AlipayConfig sdkConfig = new AlipayConfig();
sdkConfig.setServerUrl("https://openapi.alipay.com/gateway.do");
sdkConfig.setAppId(config.getAppId());
sdkConfig.setPrivateKey(config.getAppPrivateKey());
sdkConfig.setFormat("json");
sdkConfig.setAlipayPublicKey(config.getAlipayPublicKey());
sdkConfig.setCharset("UTF-8");
sdkConfig.setSignType("RSA2");
return new DefaultAlipayClient(sdkConfig);
}
/**
@@ -8,9 +8,6 @@ import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.AlipayConfig;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.domain.AlipayTradeAppPayModel;
import com.alipay.api.domain.AlipayTradeCancelModel;
import com.alipay.api.domain.AlipayTradeCloseModel;
@@ -29,7 +26,6 @@ import com.alipay.api.domain.SettleDetailInfo;
import com.alipay.api.domain.SettleInfo;
import com.alipay.api.domain.SettleConfirmExtendParams;
import com.alipay.api.domain.SettleExtendParams;
import com.alipay.api.internal.util.AlipaySignature;
import com.alipay.api.request.AlipayTradeAppPayRequest;
import com.alipay.api.request.AlipayTradeCancelRequest;
import com.alipay.api.request.AlipayTradeCloseRequest;
@@ -54,6 +50,8 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.bsd.pjcloud.common.core.exception.ErrorCodes;
import com.bsd.pjcloud.common.core.exception.I18nBizException;
import com.bsd.pjcloud.thirdparty.config.AppPlusPayConfig;
import com.bsd.pjcloud.thirdparty.config.AlipayClientContext;
import com.bsd.pjcloud.thirdparty.config.AlipayClientSupport;
import com.bsd.pjcloud.thirdparty.constant.fz.ThirdMchFzRelationStatusEnum;
import com.bsd.pjcloud.thirdparty.constant.miniplatform.AppPlusPlatformTypeEnum;
import com.bsd.pjcloud.thirdparty.constant.pay.ThirdPayOrderStatusEnum;
@@ -86,7 +84,6 @@ import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
@@ -141,14 +138,6 @@ public class ThirdAppPlusAliPayServiceImpl implements ThirdAppPlusAliPayService
private static final List<String> ALIPAY_REFUND_NOT_FOUND_CODES = List.of(
"ACQ.REFUND_NOT_EXIST", "ACQ.TRADE_NOT_EXIST");
/**
* 当前应用配置对应的支付宝客户端;延迟初始化后由所有支付请求复用。
*/
private volatile AlipayClient alipayClient;
/** 非默认支付宝应用的 SDK 客户端,按已登记 AppID 缓存;仅在同步方法内读写。 */
private final Map<String, AlipayClient> applicationClients = new HashMap<>();
/**
* 支付宝应用级配置,来源于现有 app-plus.pay 配置中心。
*/
@@ -159,6 +148,9 @@ public class ThirdAppPlusAliPayServiceImpl implements ThirdAppPlusAliPayService
*/
private final ThirdPayOrderService thirdPayOrderService;
/** 支付宝双模式客户端与通知验签入口,跨 App、聚合收银及商户管理统一复用应用快照。 */
private final AlipayClientSupport alipayClientSupport;
/**
* 支付宝直付通分账订单服务;使用现有 third_fz_order 表保存两阶段请求快照。
*/
@@ -192,43 +184,19 @@ public class ThirdAppPlusAliPayServiceImpl implements ThirdAppPlusAliPayService
private ThirdPaymentOrderReservationService paymentOrderReservationService;
/**
* 构建支付宝 SDK 客户端配置,不输出或复制任何密钥到日志。
* 获取默认网页/App 应用的双模式客户端上下文。
*
* @param aliPayConfig 已按支付单 AppID 选定的服务端应用配置
* @return 指定应用的支付宝 SDK 配置
* <p>保留 protected 测试替换入口;生产实例由统一组件按应用延迟初始化并复用。</p>
*
* @return 已固定签名模式、SDK 和通知验签凭据的应用上下文
*/
private AlipayConfig getAlipayConfig(AppPlusPayConfig.AliPayConfig aliPayConfig) {
if (aliPayConfig == null || StrUtil.hasBlank(aliPayConfig.getAppId(), aliPayConfig.getAppPrivateKey(), aliPayConfig.getAlipayPublicKey())) {
throw new I18nBizException(ErrorCodes.THIRD_ALIPAY_APP_CONFIG_INCOMPLETE);
protected AlipayClientContext getAlipayClient() {
try {
return alipayClientSupport.getClient(appPlusPayConfig.getAliPayConfig());
} catch (AlipayApiException exception) {
log.error("支付宝默认应用客户端初始化失败,errorCode:{}", exception.getErrCode());
throw new I18nBizException(ErrorCodes.THIRD_ALIPAY_CLIENT_INIT_FAIL);
}
AlipayConfig alipayConfig = new AlipayConfig();
alipayConfig.setServerUrl("https://openapi.alipay.com/gateway.do");
alipayConfig.setAppId(aliPayConfig.getAppId());
alipayConfig.setPrivateKey(aliPayConfig.getAppPrivateKey());
alipayConfig.setFormat("json");
alipayConfig.setAlipayPublicKey(aliPayConfig.getAlipayPublicKey());
alipayConfig.setCharset("UTF-8");
alipayConfig.setSignType("RSA2");
return alipayConfig;
}
/**
* 获取延迟初始化的支付宝客户端。
*
* <p>方法保留为 protected,便于单元测试替换为 Mock 客户端;生产环境只会创建一次。</p>
*
* @return 可用的支付宝客户端
*/
protected synchronized AlipayClient getAlipayClient() {
if (alipayClient == null) {
try {
alipayClient = new DefaultAlipayClient(getAlipayConfig(appPlusPayConfig.getAliPayConfig()));
} catch (Exception e) {
log.error("初始化支付宝客户端失败", e);
throw new I18nBizException(ErrorCodes.THIRD_ALIPAY_CLIENT_INIT_FAIL);
}
}
return alipayClient;
}
/**
@@ -258,30 +226,26 @@ public class ThirdAppPlusAliPayServiceImpl implements ThirdAppPlusAliPayService
* @param settingJson 创建时保存的商户和应用标识快照
* @return 对应应用的客户端
*/
private AlipayClient getOrderAlipayClient(String settingJson) {
private AlipayClientContext getOrderAlipayClient(String settingJson) {
return getAlipayClient(resolveOrderAliPayConfig(settingJson));
}
/**
* 缓存已登记应用的客户端;默认应用沿用原方法,保留 App 链路及现有测试扩展点。
* 按原支付应用获取统一双模式上下文;默认应用沿用原方法,保留测试扩展点。
*
* @param config 从可信服务端配置选出的完整应用凭据
* @return 对应 AppID 的 SDK 客户端
* @return 对应 AppID 的固定客户端及验签凭据快照
*/
protected synchronized AlipayClient getAlipayClient(AppPlusPayConfig.AliPayConfig config) {
protected AlipayClientContext getAlipayClient(AppPlusPayConfig.AliPayConfig config) {
if (config == appPlusPayConfig.getAliPayConfig()) {
return getAlipayClient();
}
AlipayClient client = applicationClients.get(config.getAppId());
if (client == null) {
try {
client = new DefaultAlipayClient(getAlipayConfig(config));
applicationClients.put(config.getAppId(), client);
} catch (Exception exception) {
throw new I18nBizException(ErrorCodes.THIRD_ALIPAY_CLIENT_INIT_FAIL);
}
try {
return alipayClientSupport.getClient(config);
} catch (AlipayApiException exception) {
log.error("支付宝应用客户端初始化失败,appId:{},errorCode:{}", config.getAppId(), exception.getErrCode());
throw new I18nBizException(ErrorCodes.THIRD_ALIPAY_CLIENT_INIT_FAIL);
}
return client;
}
/**
@@ -1850,7 +1814,7 @@ public class ThirdAppPlusAliPayServiceImpl implements ThirdAppPlusAliPayService
public ThirdALiPayCallbackVo miniSubscribeCallback(Map<String, String> params, String body, Map<String, String> headers) {
try {
AppPlusPayConfig.AliPayConfig config = resolveCallbackAliPayConfig(params.get("app_id"));
boolean flag = config != null && AlipaySignature.rsaCheckV1(params, config.getAlipayPublicKey(), "UTF-8", "RSA2");
boolean flag = alipayClientSupport.verifyCallback(config, params);
if (!flag) return null;
String bizContent = params.get("biz_content");
String msgMethod = params.get("msg_method");
@@ -1997,7 +1961,7 @@ public class ThirdAppPlusAliPayServiceImpl implements ThirdAppPlusAliPayService
}
/**
* 使用应用支付宝公钥对支付回调执行 RSA2 验签。
* 按已登记应用的固定公钥或证书快照对支付、退款回调执行 RSA2 验签。
*
* @param params 支付宝表单回调参数
* @return 验签通过时返回 {@code true}
@@ -2005,8 +1969,7 @@ public class ThirdAppPlusAliPayServiceImpl implements ThirdAppPlusAliPayService
protected boolean verifyPayCallbackSignature(Map<String, String> params) {
try {
AppPlusPayConfig.AliPayConfig callbackConfig = resolveCallbackAliPayConfig(params.get("app_id"));
return callbackConfig != null && AlipaySignature.rsaCheckV1(params,
callbackConfig.getAlipayPublicKey(), "UTF-8", "RSA2");
return alipayClientSupport.verifyCallback(callbackConfig, params);
} catch (Exception exception) {
log.warn("|{}|支付宝支付回调验签异常,payNo:{}", TAG, params.get("out_trade_no"));
return false;
@@ -2014,7 +1977,7 @@ public class ThirdAppPlusAliPayServiceImpl implements ThirdAppPlusAliPayService
}
/**
* 按回调 app_id 选取已登记应用的支付宝公钥;验签后仍须匹配订单固定 AppID。
* 按回调 app_id 选取已登记应用的完整配置;验签后仍须匹配订单固定 AppID。
*
* @param appId 支付宝回调中的应用 ID
* @return 应用 ID 唯一匹配时返回对应配置,未知应用返回 {@code null}
@@ -32,6 +32,7 @@ import com.bsd.pjcloud.hard.api.feign.RemoteHardOrderService;
import com.bsd.pjcloud.supermarket.api.feign.RemoteSmHotelOrderService;
import com.bsd.pjcloud.supermarket.vo.SmHotelOrderPayInfoVo;
import com.bsd.pjcloud.thirdparty.config.AppPlusPayConfig;
import com.bsd.pjcloud.thirdparty.config.AlipayClientSupport;
import com.bsd.pjcloud.thirdparty.constant.miniplatform.AppPlusPlatformLoginErrorEnum;
import com.bsd.pjcloud.thirdparty.constant.miniplatform.AppPlusPlatformPayType;
import com.bsd.pjcloud.thirdparty.constant.miniplatform.AppPlusPlatformTypeEnum;
@@ -1016,12 +1017,12 @@ public class ThirdAppPlusServiceImpl implements ThirdAppPlusService {
}
/**
* 校验现有支付宝应用配置满足预创建、回调验签和回调地址拼接要求。
* 按签名模式校验支付宝应用凭据,并检查订单码预创建所需的回调地址。
*/
private void validateAliPayApplicationConfig() {
AppPlusPayConfig.AliPayConfig config = appPlusPayConfig.getAliPayConfig();
if (config == null || StrUtil.hasBlank(config.getAppId(), config.getAppPrivateKey(), config.getAlipayPublicKey(),
config.getBaseCallbackUrl(), config.getPayCallbackUrl())) {
AlipayClientSupport.validateConfiguration(config);
if (StrUtil.hasBlank(config.getBaseCallbackUrl(), config.getPayCallbackUrl())) {
throw new I18nBizException(ErrorCodes.THIRD_ALIPAY_APP_CONFIG_INCOMPLETE);
}
}
@@ -3,10 +3,7 @@ package com.bsd.pjcloud.thirdparty.service.impl.pay;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.alipay.api.AlipayClient;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayConfig;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.FileItem;
import com.alipay.api.domain.AlipayTradeRoyaltyRelationBatchqueryModel;
import com.alipay.api.domain.AlipayTradeRoyaltyRelationBindModel;
@@ -33,6 +30,8 @@ import com.bsd.pjcloud.common.core.constant.enums.YesNoEnum;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.data.tenant.TenantContextHolder;
import com.bsd.pjcloud.thirdparty.config.AppPlusPayConfig;
import com.bsd.pjcloud.thirdparty.config.AlipayClientContext;
import com.bsd.pjcloud.thirdparty.config.AlipayClientSupport;
import com.bsd.pjcloud.thirdparty.constant.fz.ThirdMchFzRelationSceneEnum;
import com.bsd.pjcloud.thirdparty.constant.fz.ThirdMchFzRelationStatusEnum;
import com.bsd.pjcloud.thirdparty.constant.miniplatform.AppPlusPlatformPayType;
@@ -150,6 +149,9 @@ public class ThirdMchFzRelationServiceImpl implements ThirdMchFzRelationService
*/
private final RemoteMchService remoteMchService;
/** 商户资料、图片及分账关系请求统一使用的支付宝双模式客户端入口。 */
private final AlipayClientSupport alipayClientSupport;
/**
* 查询支付配置下的一对多关系,接收方账号只返回脱敏值。
*
@@ -1823,25 +1825,14 @@ public class ThirdMchFzRelationServiceImpl implements ThirdMchFzRelationService
}
/**
* 动态创建平台统一支付宝客户端;测试实现可替换客户端以验证渠道响应边界。
* 获取平台应用的固定双模式客户端;测试实现可替换上下文以验证渠道响应边界。
*
* @param config 已解析的平台支付宝账号配置
* @return 使用指定平台账号创建的支付宝 SDK 客户端
* @return 指定平台账号的客户端与签名模式快照
* @throws AlipayApiException SDK 配置无法初始化时抛出
*/
protected AlipayClient client(AppPlusPayConfig.AliPayConfig config) throws AlipayApiException {
if (config == null || StrUtil.hasBlank(config.getAppId(), config.getAppPrivateKey(), config.getAlipayPublicKey())) {
throw new IllegalStateException("支付宝平台账号配置不完整");
}
AlipayConfig sdk = new AlipayConfig();
sdk.setServerUrl("https://openapi.alipay.com/gateway.do");
sdk.setAppId(config.getAppId());
sdk.setPrivateKey(config.getAppPrivateKey());
sdk.setFormat("json");
sdk.setAlipayPublicKey(config.getAlipayPublicKey());
sdk.setCharset("UTF-8");
sdk.setSignType("RSA2");
return new DefaultAlipayClient(sdk);
protected AlipayClientContext client(AppPlusPayConfig.AliPayConfig config) throws AlipayApiException {
return alipayClientSupport.getClient(config);
}
/**
@@ -0,0 +1,474 @@
package com.bsd.pjcloud.thirdparty.config;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.internal.util.AlipaySignature;
import com.alipay.api.internal.util.AntCertificationUtil;
import com.alipay.api.request.AlipayTradeAppPayRequest;
import com.alipay.api.request.AlipayTradeQueryRequest;
import com.alipay.api.request.AlipayTradeWapPayRequest;
import com.alipay.api.response.AlipayTradeQueryResponse;
import org.bouncycastle.x509.X509V3CertificateGenerator;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.context.properties.bind.BindException;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
import javax.security.auth.x500.X500Principal;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/** 支付宝模式绑定、执行分流、证书快照及旧公钥通知兼容的离线回归。 */
class AlipayClientSupportTest {
/** 运行时生成的应用证书及配套私钥,各用例复用以减少 RSA 密钥生成开销。 */
private static CertificateMaterial appMaterial;
/** 运行时生成的支付宝测试公钥证书,用于真实 RSA2 通知验签。 */
private static CertificateMaterial alipayMaterial;
/** 运行时生成的根证书,SDK 据此计算根证书序列号。 */
private static CertificateMaterial rootMaterial;
/** 旧公钥使用不同签名长度,覆盖首次验签抛出签名长度异常后的兼容路径。 */
private static KeyPair legacyKeys;
/** 每个测试独立的证书目录,由 JUnit 在测试结束后清理。 */
@TempDir
Path certificateDirectory;
/** 当前测试的三组应用配置容器。 */
private AppPlusPayConfig applications;
/** 默认网页/App 应用配置,按用例切换 KEY 或 CERT。 */
private AppPlusPayConfig.AliPayConfig config;
/** 当前测试的进程级客户端快照缓存。 */
private AlipayClientSupport support;
/** 所有证书和私钥均在测试进程中生成,仓库只保存生成逻辑。 */
@BeforeAll
static void generateTestCertificates() throws Exception {
appMaterial = certificate("Application Test", 1);
alipayMaterial = certificate("Alipay Test", 2);
rootMaterial = certificate("Root Test", 3);
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(1024);
legacyKeys = generator.generateKeyPair();
}
/** 默认建立完整 KEY 配置,覆盖省略 sign-mode 的历史部署方式。 */
@BeforeEach
void setUp() {
applications = new AppPlusPayConfig();
config = new AppPlusPayConfig.AliPayConfig();
config.setAppId("test-web-app");
config.setAppPrivateKey(privateKey(appMaterial.keys()));
config.setAlipayPublicKey(publicKey(alipayMaterial.keys()));
config.setBaseCallbackUrl("https://pay.example.com");
config.setPayCallbackUrl("/third/third-app-pay/app/pay-callback");
applications.setAliPayConfig(config);
support = new AlipayClientSupport(applications);
}
/** 三组配置真实绑定各自模式,省略模式继续使用 KEY。 */
@Test
void configurationShouldBindIndependentModesAndPaths() {
Map<String, Object> values = Map.of(
"app-plus.pay.ali-pay-config.app-id", "web",
"app-plus.pay.ali-pay-mini-config.app-id", "mini",
"app-plus.pay.ali-pay-mini-config.sign-mode", "CERT",
"app-plus.pay.ali-pay-mini-config.app-cert-path", "/certs/app.crt",
"app-plus.pay.ali-pay-mini-config.alipay-public-cert-path", "/certs/alipay.crt",
"app-plus.pay.ali-pay-mini-config.alipay-root-cert-path", "/certs/root.crt",
"app-plus.pay.ali-pay-third-party-config.app-id", "third",
"app-plus.pay.ali-pay-third-party-config.sign-mode", "KEY");
AppPlusPayConfig bound = new Binder(new MapConfigurationPropertySource(values))
.bind("app-plus.pay", AppPlusPayConfig.class).get();
assertEquals(AppPlusPayConfig.AliPaySignMode.KEY, bound.getAliPayConfig().getSignMode());
assertEquals(AppPlusPayConfig.AliPaySignMode.CERT, bound.getAliPayMiniConfig().getSignMode());
assertEquals(AppPlusPayConfig.AliPaySignMode.KEY, bound.getAliPayThirdPartyConfig().getSignMode());
assertEquals("/certs/app.crt", bound.getAliPayMiniConfig().getAppCertPath());
assertEquals("/certs/alipay.crt", bound.getAliPayMiniConfig().getAlipayPublicCertPath());
assertEquals("/certs/root.crt", bound.getAliPayMiniConfig().getAlipayRootCertPath());
}
/** 拼错模式必须由配置绑定明确拒绝。 */
@Test
void unknownModeShouldFailConfigurationBinding() {
Binder binder = new Binder(new MapConfigurationPropertySource(Map.of(
"app-plus.pay.ali-pay-config.sign-mode", "UNKNOWN")));
assertThrows(BindException.class, () -> binder.bind("app-plus.pay", AppPlusPayConfig.class));
}
/** KEY 配置独立要求普通公钥,CERT 必填项按三份证书路径校验。 */
@Test
void requiredCredentialsShouldFollowConfiguredMode() {
config.setAlipayPublicKey(null);
assertThrows(IllegalStateException.class, () -> AlipayClientSupport.validateConfiguration(config));
config.setSignMode(AppPlusPayConfig.AliPaySignMode.CERT);
assertThrows(IllegalStateException.class, () -> AlipayClientSupport.validateConfiguration(config));
config.setSignMode(null);
assertThrows(IllegalStateException.class, () -> AlipayClientSupport.validateConfiguration(config));
}
/** KEY 服务端调用沿用普通执行接口,证书接口保持零调用。 */
@Test
void keyServerRequestShouldUseOrdinaryExecute() throws Exception {
AlipayClient sdk = mock(AlipayClient.class);
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
AlipayTradeQueryResponse response = new AlipayTradeQueryResponse();
when(sdk.execute(request)).thenReturn(response);
AlipayClientContext context = new AlipayClientContext(sdk, AppPlusPayConfig.AliPaySignMode.KEY, null, null);
assertSame(response, context.execute(request));
verify(sdk).execute(request);
verify(sdk, never()).certificateExecute(request);
}
/** CERT 服务端调用始终选择证书执行接口,避免 SDK 的 CERT_EXECUTE_ERROR。 */
@Test
void certificateServerRequestShouldUseCertificateExecute() throws Exception {
AlipayClient sdk = mock(AlipayClient.class);
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
AlipayTradeQueryResponse response = new AlipayTradeQueryResponse();
when(sdk.certificateExecute(request)).thenReturn(response);
AlipayClientContext context = new AlipayClientContext(sdk, AppPlusPayConfig.AliPaySignMode.CERT, null, null);
assertSame(response, context.execute(request));
verify(sdk).certificateExecute(request);
verify(sdk, never()).execute(request);
}
/** 证书请求失败保留原异常,业务层据此执行原有未知结果和重试逻辑。 */
@Test
void certificateRequestFailureShouldPropagateWithoutAnotherSubmission() throws Exception {
AlipayClient sdk = mock(AlipayClient.class);
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
AlipayApiException failure = new AlipayApiException("TEST_NETWORK_ERROR", "test");
when(sdk.certificateExecute(request)).thenThrow(failure);
AlipayClientContext context = new AlipayClientContext(sdk, AppPlusPayConfig.AliPaySignMode.CERT, null, null);
assertSame(failure, assertThrows(AlipayApiException.class, () -> context.execute(request)));
verify(sdk, never()).execute(request);
}
/** CERT 无普通公钥时可通过网页支付前置检查,并在两种本地签名中自动带入证书序列号。 */
@Test
void certificateModeShouldSignAppAndWapWithoutOrdinaryPublicKey() throws Exception {
enableCertificateMode(config);
assertSame(config, applications.requireAliPayWebPayConfig());
AlipayClientContext context = support.getClient(config);
String appParameters = appParameters(context);
AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest();
request.setBizContent("{}");
String wapParameters = context.pageExecute(request, "GET").getBody();
String appSerial = "app_cert_sn=" + AntCertificationUtil.getCertSN(appMaterial.certificate());
String rootSerial = "alipay_root_cert_sn=" + AntCertificationUtil.getRootCertSN(rootMaterial.pem());
assertTrue(appParameters.contains(appSerial));
assertTrue(appParameters.contains(rootSerial));
assertTrue(wapParameters.startsWith("https://openapi.alipay.com/gateway.do?"));
assertTrue(wapParameters.contains(appSerial));
assertTrue(wapParameters.contains(rootSerial));
}
/** 省略模式的旧配置无需证书文件,即可执行 App 和 WAP 本地签名。 */
@Test
void defaultKeyModeShouldKeepLocalSigningWithoutCertificateParameters() throws Exception {
AlipayClientContext context = support.getClient(config);
AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest();
request.setBizContent("{}");
assertFalse(appParameters(context).contains("app_cert_sn="));
assertFalse(context.pageExecute(request, "GET").getBody().contains("alipay_root_cert_sn="));
}
/** 普通公钥通知可使用不可修改参数集合,原始签名字段保留。 */
@Test
void keyNotificationShouldVerifyWithoutChangingOriginalParameters() throws Exception {
Map<String, String> params = notification(alipayMaterial.keys());
assertTrue(support.verifyCallback(config, params));
assertTrue(params.containsKey("sign"));
assertEquals("RSA2", params.get("sign_type"));
}
/** 支付宝公钥证书提取出的公钥可以验证真实 RSA2 通知。 */
@Test
void certificateNotificationShouldVerifyUsingCertificatePublicKey() throws Exception {
enableCertificateMode(config);
assertTrue(support.verifyCallback(config, notification(alipayMaterial.keys())));
}
/** 旧签名长度与当前证书不同,第一次验签异常后仍用完整参数进行旧公钥复核。 */
@Test
void certificateNotificationShouldSupportExplicitLegacyKeyWithFreshParameters() throws Exception {
enableCertificateMode(config);
config.setAlipayPublicKey(publicKey(legacyKeys));
Map<String, String> params = notification(legacyKeys);
Map<String, String> original = new HashMap<>(params);
assertTrue(support.verifyCallback(config, params));
assertEquals(original, params);
}
/** 旧公钥缺省时旧签名被拒绝,金额篡改在开启兼容后同样被拒绝。 */
@Test
void missingLegacyKeyAndTamperedNotificationShouldBeRejected() throws Exception {
enableCertificateMode(config);
assertFalse(support.verifyCallback(config, notification(legacyKeys)));
AlipayClientSupport withLegacyKey = new AlipayClientSupport(applications);
config.setAlipayPublicKey(publicKey(legacyKeys));
Map<String, String> tampered = new HashMap<>(notification(legacyKeys));
tampered.put("total_amount", "200.00");
assertFalse(withLegacyKey.verifyCallback(config, tampered));
}
/** 未知 AppID、错误配置组、缺失签名及重复登记 AppID 均被明确拒绝。 */
@Test
void callbackApplicationAndRegistrationShouldBeStrictlyMatched() throws Exception {
Map<String, String> params = new HashMap<>(notification(alipayMaterial.keys()));
params.put("app_id", "unknown-app");
assertFalse(support.verifyCallback(applications.findAliPayConfig("unknown-app"), params));
assertFalse(support.verifyCallback(config, params));
assertFalse(support.verifyCallback(config, Map.of("app_id", config.getAppId())));
AppPlusPayConfig.AliPayConfig duplicate = new AppPlusPayConfig.AliPayConfig();
duplicate.setAppId(config.getAppId());
applications.setAliPayMiniConfig(duplicate);
assertThrows(IllegalStateException.class, () -> support.getClient(config));
}
/** 每份证书缺失均指出对应类型,失败实例不进入缓存,修正路径后可再次初始化。 */
@Test
void eachMissingCertificateShouldFailAndAllowCorrectedInitialization() throws Exception {
enableCertificateMode(config);
String missing = certificateDirectory.resolve("missing.crt").toString();
String original = config.getAppCertPath();
config.setAppCertPath(missing);
assertTrue(assertThrows(IllegalStateException.class, () -> support.getClient(config)).getMessage().contains("应用公钥证书"));
config.setAppCertPath(original);
original = config.getAlipayPublicCertPath();
config.setAlipayPublicCertPath(missing);
assertTrue(assertThrows(IllegalStateException.class, () -> support.getClient(config)).getMessage().contains("支付宝公钥证书"));
config.setAlipayPublicCertPath(original);
original = config.getAlipayRootCertPath();
config.setAlipayRootCertPath(missing);
assertTrue(assertThrows(IllegalStateException.class, () -> support.getClient(config)).getMessage().contains("支付宝根证书"));
config.setAlipayRootCertPath(original);
assertTrue(appParameters(support.getClient(config)).contains("app_cert_sn="));
}
/** 三种证书的格式错误均在初始化阶段明确反馈。 */
@Test
void malformedCertificateShouldBeRejectedForEveryRole() throws Exception {
enableCertificateMode(config);
Path malformed = certificateDirectory.resolve("malformed.crt");
Files.writeString(malformed, "invalid-certificate", StandardCharsets.UTF_8);
String original = config.getAppCertPath();
config.setAppCertPath(malformed.toString());
assertTrue(assertThrows(IllegalStateException.class, () -> support.getClient(config)).getMessage().contains("应用公钥证书"));
config.setAppCertPath(original);
original = config.getAlipayPublicCertPath();
config.setAlipayPublicCertPath(malformed.toString());
assertTrue(assertThrows(IllegalStateException.class, () -> support.getClient(config)).getMessage().contains("支付宝公钥证书"));
config.setAlipayPublicCertPath(original);
config.setAlipayRootCertPath(malformed.toString());
assertTrue(assertThrows(IllegalStateException.class, () -> support.getClient(config)).getMessage().contains("支付宝根证书"));
}
/** 相对路径或目录不能被当作本地证书文件使用。 */
@Test
void relativePathAndDirectoryShouldBeRejected() throws Exception {
enableCertificateMode(config);
config.setAppCertPath("relative/app.crt");
assertThrows(IllegalStateException.class, () -> support.getClient(config));
config.setAppCertPath(certificateDirectory.toString());
assertThrows(IllegalStateException.class, () -> support.getClient(config));
}
/** 即使旧公钥签名有效,证书初始化失败也按配置异常向上传递。 */
@Test
void invalidCertificateMustNotBeHiddenByValidLegacySignature() throws Exception {
enableCertificateMode(config);
config.setAlipayPublicKey(publicKey(legacyKeys));
config.setAlipayPublicCertPath(certificateDirectory.resolve("missing.crt").toString());
assertThrows(IllegalStateException.class, () -> support.verifyCallback(config, notification(legacyKeys)));
}
/** 并发首次读取发布同一个完整客户端快照。 */
@Test
void concurrentInitializationShouldReuseOneApplicationContext() throws Exception {
enableCertificateMode(config);
var executor = Executors.newFixedThreadPool(4);
try {
List<Callable<AlipayClientContext>> tasks = new ArrayList<>();
for (int index = 0; index < 8; index++) {
tasks.add(() -> support.getClient(config));
}
var results = executor.invokeAll(tasks);
AlipayClientContext first = results.get(0).get();
for (var result : results) {
assertSame(first, result.get());
}
} finally {
executor.shutdownNow();
}
}
/** 已缓存的模式固定至重启,新实例才采纳 CERT 配置。 */
@Test
void modeChangeShouldTakeEffectOnlyForNewSupportInstance() throws Exception {
AlipayClientContext original = support.getClient(config);
enableCertificateMode(config);
assertSame(original, support.getClient(config));
assertFalse(appParameters(original).contains("app_cert_sn="));
AlipayClientContext restarted = new AlipayClientSupport(applications).getClient(config);
assertNotSame(original, restarted);
assertTrue(appParameters(restarted).contains("app_cert_sn="));
}
/** 本地证书轮换不改变已初始化通知公钥,新实例重新读取并报告异常文件。 */
@Test
void cachedVerificationKeyShouldSurviveCertificateFileReplacement() throws Exception {
enableCertificateMode(config);
support.getClient(config);
Files.writeString(Path.of(config.getAlipayPublicCertPath()), "invalid-certificate", StandardCharsets.UTF_8);
assertTrue(support.verifyCallback(config, notification(alipayMaterial.keys())));
AlipayClientSupport restarted = new AlipayClientSupport(applications);
assertThrows(IllegalStateException.class, () -> restarted.getClient(config));
}
/** 三组应用可同时使用不同模式,缓存和通知公钥按各自 AppID 隔离。 */
@Test
void mixedApplicationGroupsShouldKeepIndependentContexts() throws Exception {
AppPlusPayConfig.AliPayConfig mini = new AppPlusPayConfig.AliPayConfig();
mini.setAppId("test-mini-app");
mini.setAppPrivateKey(privateKey(appMaterial.keys()));
enableCertificateMode(mini);
applications.setAliPayMiniConfig(mini);
AppPlusPayConfig.AliPayConfig third = new AppPlusPayConfig.AliPayConfig();
third.setAppId("test-third-app");
third.setAppPrivateKey(privateKey(appMaterial.keys()));
third.setAlipayPublicKey(publicKey(legacyKeys));
applications.setAliPayThirdPartyConfig(third);
assertFalse(appParameters(support.getClient(config)).contains("app_cert_sn="));
assertTrue(appParameters(support.getClient(mini)).contains("app_cert_sn="));
assertFalse(appParameters(support.getClient(third)).contains("app_cert_sn="));
assertNotSame(support.getClient(config), support.getClient(mini));
assertNotSame(support.getClient(mini), support.getClient(third));
Map<String, String> thirdNotification = notification(third.getAppId(), legacyKeys);
assertTrue(support.verifyCallback(third, thirdNotification));
assertFalse(support.verifyCallback(mini, thirdNotification));
}
/** 将指定应用切换为无旧公钥的证书模式,文件全部写入本用例临时目录。 */
private void enableCertificateMode(AppPlusPayConfig.AliPayConfig target) throws Exception {
target.setSignMode(AppPlusPayConfig.AliPaySignMode.CERT);
target.setAlipayPublicKey(null);
target.setAppCertPath(Files.writeString(certificateDirectory.resolve("app.crt"), appMaterial.pem()).toString());
target.setAlipayPublicCertPath(Files.writeString(certificateDirectory.resolve("alipay.crt"), alipayMaterial.pem()).toString());
target.setAlipayRootCertPath(Files.writeString(certificateDirectory.resolve("root.crt"), rootMaterial.pem()).toString());
}
/** 使用真实 SDK 在内存中生成 App 支付参数,调用过程不访问支付宝网关。 */
private String appParameters(AlipayClientContext context) throws Exception {
AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest();
request.setBizContent("{}");
return context.sdkExecute(request).getBody();
}
/** 构造默认应用通知,方便核对当前证书与旧公钥的信任范围。 */
private Map<String, String> notification(KeyPair signingKeys) throws Exception {
return notification(config.getAppId(), signingKeys);
}
/** 使用运行时测试私钥生成真实 RSA2 通知,并返回不可修改集合以检查验签副作用。 */
private Map<String, String> notification(String appId, KeyPair signingKeys) throws Exception {
Map<String, String> params = new HashMap<>();
params.put("app_id", appId);
params.put("out_trade_no", "test-payment");
params.put("total_amount", "100.00");
params.put("sign_type", "RSA2");
String content = AlipaySignature.getSignCheckContentV1(new HashMap<>(params));
params.put("sign", AlipaySignature.rsaSign(content, privateKey(signingKeys), "UTF-8", "RSA2"));
return Map.copyOf(params);
}
/** 生成短期自签名 RSA 证书,复用 SDK 已依赖的 BouncyCastle API。 */
@SuppressWarnings("deprecation")
private static CertificateMaterial certificate(String commonName, long serial) throws Exception {
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(2048);
KeyPair keys = generator.generateKeyPair();
X509V3CertificateGenerator certificateGenerator = new X509V3CertificateGenerator();
X500Principal principal = new X500Principal("CN=" + commonName);
certificateGenerator.setIssuerDN(principal);
certificateGenerator.setSubjectDN(principal);
certificateGenerator.setSerialNumber(BigInteger.valueOf(serial));
certificateGenerator.setNotBefore(Date.from(Instant.now().minusSeconds(60)));
certificateGenerator.setNotAfter(Date.from(Instant.now().plusSeconds(86400)));
certificateGenerator.setPublicKey(keys.getPublic());
certificateGenerator.setSignatureAlgorithm("SHA256withRSA");
X509Certificate certificate = certificateGenerator.generate(keys.getPrivate());
String encoded = Base64.getMimeEncoder(64, new byte[]{'\n'}).encodeToString(certificate.getEncoded());
return new CertificateMaterial(keys, certificate, "-----BEGIN CERTIFICATE-----\n" + encoded + "\n-----END CERTIFICATE-----\n");
}
/** 将运行时测试私钥编码为 SDK 支持的 PKCS#8 Base64 字符串。 */
private static String privateKey(KeyPair keys) {
return Base64.getEncoder().encodeToString(keys.getPrivate().getEncoded());
}
/** 将运行时测试公钥编码为 SDK 支持的 X.509 Base64 字符串。 */
private static String publicKey(KeyPair keys) {
return Base64.getEncoder().encodeToString(keys.getPublic().getEncoded());
}
/**
* 仅在测试进程内保存的证书材料。
* @param keys 运行时生成的 RSA 密钥对
* @param certificate 对应的短期自签名证书
* @param pem 临时证书文件内容
*/
private record CertificateMaterial(KeyPair keys, X509Certificate certificate, String pem) {
}
}
@@ -4,7 +4,8 @@ import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.alipay.api.AlipayClient;
import com.bsd.pjcloud.thirdparty.config.AlipayClientContext;
import com.bsd.pjcloud.thirdparty.config.AlipayClientSupport;
import com.alipay.api.domain.AlipayTradeWapPayModel;
import com.alipay.api.internal.util.json.JSONWriter;
import com.alipay.api.request.AlipayTradeWapPayRequest;
@@ -49,8 +50,9 @@ class ThirdAggregateAliPayServiceImplTest {
/**
* 网页/移动应用支付 SDK 客户端 Mock,仅用于捕获聚合交易创建请求。
*/
/** 网页/App 应用上下文 Mock,仅验证 WAP 本地签名入口。 */
@Mock
private AlipayClient alipayClient;
private AlipayClientContext alipayClient;
/**
* 第三方支付订单服务 Mock,用于验证子订单快照和保存失败补偿。
@@ -354,9 +356,9 @@ class ThirdAggregateAliPayServiceImplTest {
private static final class TestableThirdAggregateAliPayService extends ThirdAggregateAliPayServiceImpl {
/**
* 当前测试方法共享的支付宝 SDK 客户端 Mock。
* 当前测试方法共享的支付宝双模式上下文 Mock。
*/
private final AlipayClient testAlipayClient;
private final AlipayClientContext testAlipayClient;
/**
* 创建可替换 SDK 客户端的支付宝聚合服务。
@@ -369,18 +371,18 @@ class ThirdAggregateAliPayServiceImplTest {
private TestableThirdAggregateAliPayService(AppPlusPayConfig appPlusPayConfig,
ThirdPayOrderService thirdPayOrderService,
ThirdAppPlusAliPayService thirdAppPlusAliPayService,
AlipayClient testAlipayClient) {
super(appPlusPayConfig, thirdPayOrderService, thirdAppPlusAliPayService);
AlipayClientContext testAlipayClient) {
super(appPlusPayConfig, thirdPayOrderService, thirdAppPlusAliPayService, new AlipayClientSupport(appPlusPayConfig));
this.testAlipayClient = testAlipayClient;
}
/**
* 返回测试注入的支付宝 SDK 客户端。
* 返回测试注入的支付宝双模式上下文。
*
* @return 支付宝 SDK 客户端 Mock
* @return 支付宝双模式上下文 Mock
*/
@Override
protected AlipayClient getAlipayClient() {
protected AlipayClientContext getAlipayClient() {
return testAlipayClient;
}
}
@@ -3,7 +3,8 @@ package com.bsd.pjcloud.thirdparty.service.impl.appplus;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.json.JSONUtil;
import com.alipay.api.AlipayClient;
import com.bsd.pjcloud.thirdparty.config.AlipayClientContext;
import com.bsd.pjcloud.thirdparty.config.AlipayClientSupport;
import com.alipay.api.domain.AlipayTradeAppPayModel;
import com.alipay.api.domain.AlipayTradeCloseModel;
import com.alipay.api.domain.AlipayTradeOrderSettleModel;
@@ -96,12 +97,13 @@ class ThirdAppPlusAliPayServiceImplTest {
@Mock
private ThirdPayOrderService thirdPayOrderService;
/** 网页/App 应用的双模式上下文 Mock,业务断言继续核对原始 SDK 请求内容。 */
@Mock
private AlipayClient alipayClient;
private AlipayClientContext alipayClient;
/** 小程序应用客户端,与网页/移动客户端分开,防止测试掩盖应用路由错误。 */
@Mock
private AlipayClient miniAlipayClient;
private AlipayClientContext miniAlipayClient;
/**
* 分账订单服务 Mock,用于保存支付宝确认结算、平台抽佣和退分账快照。
@@ -324,7 +326,7 @@ class ThirdAppPlusAliPayServiceImplTest {
appPlusPayConfig.getAliPayMiniConfig().setAlipayPublicKey(Base64.getEncoder().encodeToString(miniKeys.getPublic().getEncoded()));
appPlusPayConfig.getAliPayConfig().setAlipayPublicKey(Base64.getEncoder().encodeToString(webKeys.getPublic().getEncoded()));
String privateKey = Base64.getEncoder().encodeToString(miniKeys.getPrivate().getEncoded());
ThirdAppPlusAliPayServiceImpl realService = new ThirdAppPlusAliPayServiceImpl(appPlusPayConfig, thirdPayOrderService);
ThirdAppPlusAliPayServiceImpl realService = new ThirdAppPlusAliPayServiceImpl(appPlusPayConfig, thirdPayOrderService, new AlipayClientSupport(appPlusPayConfig));
Map<String, String> params = successCallbackParams();
params.put("app_id", appPlusPayConfig.getAliPayMiniConfig().getAppId());
params.put("sign_type", "RSA2");
@@ -1322,10 +1324,11 @@ class ThirdAppPlusAliPayServiceImplTest {
private static final class TestableThirdAppPlusAliPayService extends ThirdAppPlusAliPayServiceImpl {
private final AlipayClient testAlipayClient;
/** 网页/App 应用上下文 Mock,替代渠道调用。 */
private final AlipayClientContext testAlipayClient;
/** 小程序场景专用客户端 Mock,用于确认操作没有误用旧应用客户端。 */
private final AlipayClient testMiniAlipayClient;
private final AlipayClientContext testMiniAlipayClient;
/** 被测小程序 AppID,仅用于为已解析的应用选择对应 Mock。 */
private final String testMiniAppId;
@@ -1335,21 +1338,22 @@ class ThirdAppPlusAliPayServiceImplTest {
/** 创建保持原 App 扩展点、同时区分网页/移动与小程序客户端的测试服务。 */
private TestableThirdAppPlusAliPayService(AppPlusPayConfig appPlusPayConfig, ThirdPayOrderService thirdPayOrderService,
AlipayClient testAlipayClient, AlipayClient testMiniAlipayClient) {
super(appPlusPayConfig, thirdPayOrderService);
AlipayClientContext testAlipayClient, AlipayClientContext testMiniAlipayClient) {
super(appPlusPayConfig, thirdPayOrderService, new AlipayClientSupport(appPlusPayConfig));
this.testAlipayClient = testAlipayClient;
this.testMiniAlipayClient = testMiniAlipayClient;
this.testMiniAppId = appPlusPayConfig.getAliPayMiniConfig().getAppId();
}
/** 返回网页/App 应用上下文 Mock,保留默认应用路由断言。 */
@Override
protected AlipayClient getAlipayClient() {
protected AlipayClientContext getAlipayClient() {
return testAlipayClient;
}
/** 按生产解析方法选出的 AppID 返回独立 Mock,保留对支付单应用路由的真实校验。 */
@Override
protected AlipayClient getAlipayClient(AppPlusPayConfig.AliPayConfig config) {
protected AlipayClientContext getAlipayClient(AppPlusPayConfig.AliPayConfig config) {
return testMiniAppId.equals(config.getAppId()) ? testMiniAlipayClient : super.getAlipayClient(config);
}
@@ -1,7 +1,8 @@
package com.bsd.pjcloud.thirdparty.service.impl.pay;
import cn.hutool.json.JSONUtil;
import com.alipay.api.AlipayClient;
import com.bsd.pjcloud.thirdparty.config.AlipayClientContext;
import com.bsd.pjcloud.thirdparty.config.AlipayClientSupport;
import com.alipay.api.domain.AlipayTradeRoyaltyRelationBindModel;
import com.alipay.api.domain.AntMerchantExpandIndirectZftModifyModel;
import com.alipay.api.domain.RoyaltyEntity;
@@ -76,9 +77,10 @@ class ThirdMchFzRelationServiceImplTest {
@Mock
private RemoteMchService remoteMchService;
/** 支付宝 SDK 客户端 Mock,用于验证网关成功与业务结果码的组合语义。 */
/** 支付宝双模式上下文 Mock,用于验证网关成功与业务结果码的组合语义。 */
/** 商户资料和分账关系使用的双模式上下文 Mock。 */
@Mock
private AlipayClient alipayClient;
private AlipayClientContext alipayClient;
/** 被测统一分账关系服务。 */
private ThirdMchFzRelationServiceImpl service;
@@ -790,8 +792,8 @@ class ThirdMchFzRelationServiceImplTest {
/** 固定返回 Mock 支付宝客户端的关系服务测试实现。 */
private static final class TestableThirdMchFzRelationServiceImpl extends ThirdMchFzRelationServiceImpl {
/** 当前测试共享的支付宝 SDK 客户端 Mock。 */
private final AlipayClient testAlipayClient;
/** 当前测试共享的支付宝双模式上下文 Mock。 */
private final AlipayClientContext testAlipayClient;
/**
* 创建可替换支付宝客户端的关系服务。
@@ -801,21 +803,21 @@ class ThirdMchFzRelationServiceImplTest {
* @param profitsharingService 微信分账 SDK Mock
* @param appPlusPayConfig 平台支付配置
* @param remoteMchService 商户内部接口 Mock
* @param testAlipayClient 支付宝 SDK 客户端 Mock
* @param testAlipayClient 支付宝双模式上下文 Mock
*/
private TestableThirdMchFzRelationServiceImpl(ThirdMchFzRelationMapper relationMapper,
ThirdPaymentTaskEnqueuer taskEnqueuer,
ProfitsharingService profitsharingService,
AppPlusPayConfig appPlusPayConfig,
RemoteMchService remoteMchService,
AlipayClient testAlipayClient) {
super(relationMapper, taskEnqueuer, profitsharingService, appPlusPayConfig, remoteMchService);
AlipayClientContext testAlipayClient) {
super(relationMapper, taskEnqueuer, profitsharingService, appPlusPayConfig, remoteMchService, new AlipayClientSupport(appPlusPayConfig));
this.testAlipayClient = testAlipayClient;
}
/** 返回测试注入的支付宝 SDK 客户端,避免发起真实渠道请求。 */
/** 返回测试注入的支付宝双模式上下文,避免发起真实渠道请求。 */
@Override
protected AlipayClient client(AppPlusPayConfig.AliPayConfig config) {
protected AlipayClientContext client(AppPlusPayConfig.AliPayConfig config) {
return testAlipayClient;
}
}