refactor(payment): 交易调试仅保留签名组参,去掉直调支付后门

管理端 develop 不再调用 NormalPayService;真实支付由前端模拟商户请求走 /unipay/pay。
同步放宽直传模式 method 校验,路由模式由 PayRouteService 显式校验。
This commit is contained in:
DaxPay Dev
2026-07-13 09:41:46 +08:00
parent 60083867dd
commit 0cdbc1412d
16 changed files with 88 additions and 151 deletions

View File

@@ -1,9 +1,9 @@
package cn.daxpay.open.payment.web.develop.controller;
package cn.daxpay.open.payment.admin.controller.develop;
import cn.daxpay.open.payment.unipay.param.assist.GenerateAuthUrlParam;
import cn.daxpay.open.payment.unipay.result.assist.AuthResult;
import cn.daxpay.open.payment.unipay.result.assist.AuthUrlResult;
import cn.daxpay.open.payment.web.develop.service.DevelopAuthService;
import cn.daxpay.open.payment.admin.service.develop.DevelopAuthService;
import cn.daxpay.open.platform.core.annotation.PermCode;
import cn.daxpay.open.platform.core.rest.Res;
import cn.daxpay.open.platform.core.rest.result.Result;

View File

@@ -1,9 +1,9 @@
package cn.daxpay.open.payment.web.develop.controller;
package cn.daxpay.open.payment.admin.controller.develop;
import cn.daxpay.open.payment.web.develop.param.DevelopSignParam;
import cn.daxpay.open.payment.web.develop.param.DevelopVerifyParam;
import cn.daxpay.open.payment.web.develop.result.DevelopSignResult;
import cn.daxpay.open.payment.web.develop.service.DevelopSignService;
import cn.daxpay.open.payment.admin.param.develop.DevelopSignParam;
import cn.daxpay.open.payment.admin.param.develop.DevelopVerifyParam;
import cn.daxpay.open.payment.admin.result.develop.DevelopSignResult;
import cn.daxpay.open.payment.admin.service.develop.DevelopSignService;
import cn.daxpay.open.platform.core.annotation.PermCode;
import cn.daxpay.open.platform.core.rest.Res;
import cn.daxpay.open.platform.core.rest.result.Result;

View File

@@ -1,9 +1,8 @@
package cn.daxpay.open.payment.web.develop.controller;
package cn.daxpay.open.payment.admin.controller.develop;
import cn.daxpay.open.payment.web.develop.param.DevelopParam;
import cn.daxpay.open.payment.web.develop.result.DevelopPayResult;
import cn.daxpay.open.payment.web.develop.result.DevelopSignResult;
import cn.daxpay.open.payment.web.develop.service.DevelopTradeService;
import cn.daxpay.open.payment.admin.param.develop.DevelopParam;
import cn.daxpay.open.payment.admin.result.develop.DevelopSignResult;
import cn.daxpay.open.payment.admin.service.develop.DevelopTradeService;
import cn.daxpay.open.payment.masterdata.constants.provider.result.PayProviderMethodResult;
import cn.daxpay.open.payment.unipay.param.trade.pay.NormalPayParam;
import cn.daxpay.open.platform.core.annotation.PermCode;
@@ -24,6 +23,8 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/// 交易开发调试(管理)
///
/// 仅提供签名与元数据辅助, 真实支付由前端模拟商户请求调用 `/unipay/pay`
@PermCode(menuCode = "develop:trade")
@Tag(name = "交易开发调试服务")
@RestController
@@ -40,13 +41,6 @@ public class DevelopTradeController {
return Res.ok(developTradeService.sign(param));
}
@PermCode(code = "pay", nameCn = "支付", nameEn = "Pay")
@Operation(summary = "支付调试(真实发起)")
@PostMapping("/pay")
public Result<DevelopPayResult> pay(@RequestBody DevelopParam<NormalPayParam> param) {
return Res.ok(developTradeService.pay(param));
}
@PermCode(code = "view", nameCn = "查看", nameEn = "View")
@Operation(summary = "已启用渠道支付方式目录")
@GetMapping("/method-directory")

View File

@@ -1,4 +1,4 @@
package cn.daxpay.open.payment.web.develop.param;
package cn.daxpay.open.payment.admin.param.develop;
import cn.daxpay.open.payment.unipay.param.MerchantPaymentCommonParam;
import io.swagger.v3.oas.annotations.media.Schema;

View File

@@ -1,4 +1,4 @@
package cn.daxpay.open.payment.web.develop.param;
package cn.daxpay.open.payment.admin.param.develop;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

View File

@@ -1,4 +1,4 @@
package cn.daxpay.open.payment.web.develop.param;
package cn.daxpay.open.payment.admin.param.develop;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

View File

@@ -1,4 +1,4 @@
package cn.daxpay.open.payment.web.develop.result;
package cn.daxpay.open.payment.admin.result.develop;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

View File

@@ -1,4 +1,4 @@
package cn.daxpay.open.payment.web.develop.service;
package cn.daxpay.open.payment.admin.service.develop;
import cn.daxpay.open.payment.core.assist.AuthSessionStore;
import cn.daxpay.open.payment.core.assist.ChannelAuthService;

View File

@@ -1,8 +1,8 @@
package cn.daxpay.open.payment.web.develop.service;
package cn.daxpay.open.payment.admin.service.develop;
import cn.daxpay.open.payment.web.develop.param.DevelopSignParam;
import cn.daxpay.open.payment.web.develop.param.DevelopVerifyParam;
import cn.daxpay.open.payment.web.develop.result.DevelopSignResult;
import cn.daxpay.open.payment.admin.param.develop.DevelopSignParam;
import cn.daxpay.open.payment.admin.param.develop.DevelopVerifyParam;
import cn.daxpay.open.payment.admin.result.develop.DevelopSignResult;
import cn.daxpay.open.payment.common.util.JsonSignStrUtil;
import cn.daxpay.open.platform.core.util.RsaSignUtil;
import lombok.RequiredArgsConstructor;

View File

@@ -0,0 +1,57 @@
package cn.daxpay.open.payment.admin.service.develop;
import cn.daxpay.open.payment.admin.param.develop.DevelopParam;
import cn.daxpay.open.payment.admin.result.develop.DevelopSignResult;
import cn.daxpay.open.payment.common.util.ObjectSignStrUtil;
import cn.daxpay.open.payment.common.util.PaySignUtil;
import cn.daxpay.open.payment.masterdata.constants.provider.result.PayProviderMethodResult;
import cn.daxpay.open.payment.masterdata.constants.provider.service.PayProviderMethodService;
import cn.daxpay.open.payment.merchant.service.route.support.PayRouteStrategyCapabilitySupport;
import cn.daxpay.open.payment.unipay.param.trade.pay.NormalPayParam;
import cn.daxpay.open.platform.core.rest.dto.LabelValue;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
/// # 交易开发调试服务
///
/// 仅提供组参辅助与签名能力, **不发起真实交易**。
/// 真实支付由管理端调试页模拟商户 HTTP 请求调用 `/unipay/pay` 完成,
/// 与正式对接走同一入口, 避免 admin 内部直调支付核心形成后门。
@Slf4j
@Service
@RequiredArgsConstructor
public class DevelopTradeService {
private final PayProviderMethodService payProviderMethodService;
private final PayRouteStrategyCapabilitySupport payRouteStrategyCapabilitySupport;
/// 生成支付参数签名(与正式签名逻辑一致)
///
/// 调用方需在 param 中自备 reqTime / nonceStr 等公共字段, 本方法只负责签名串与签名值。
public DevelopSignResult sign(DevelopParam<NormalPayParam> param) {
// 签名串(与 PaySignUtil 内部一致)
String signStr = ObjectSignStrUtil.buildSignStr(param.getParam());
// 签名值
String sign = PaySignUtil.sign(param.getParam(), param.getPrivateKey());
return new DevelopSignResult().setSignStr(signStr).setSign(sign);
}
/// 已启用渠道支付方式目录(供调试页支付方式下拉)
public List<PayProviderMethodResult> listMethodDirectory() {
return payProviderMethodService.listDirectoryFlat();
}
/// 传值模式:商户全部启用通道商户候选, provider 为空返回全部,
/// 非空按支付渠道过滤(如 wechat 返回所有声明支持微信的产品通道商户, 含官方与三方聚合通道)
public List<LabelValue> listChannelMchCandidates(String mchNo, String provider) {
return payRouteStrategyCapabilitySupport.listDirectChannelMchCandidates(mchNo, provider);
}
/// 传值模式:按通道商户(产品)返回全部启用支付能力候选
public List<LabelValue> listCapabilityCandidates(String channelMchNo) {
return payRouteStrategyCapabilitySupport.listDirectCapabilityCandidates(channelMchNo);
}
}

View File

@@ -55,6 +55,10 @@ public class PayRouteService implements PayRouteFacade {
resolveDirect(payParam);
return;
}
// 路由模式: 支付方式必填(Bean Validation 已放宽以兼容直定可空, 此处显式校验)
if (StrUtil.isBlank(payParam.getMethod())) {
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.route.error.methodRequired");
}
String appId = payParam.getAppId();
var bundle = loadBundle(appId);
if (bundle == null || bundle.getStrategy() == null) {

View File

@@ -55,9 +55,10 @@ public class NormalPayParam extends MerchantPaymentCommonParam {
private String product;
/// 支付方式编码
/// - 路由模式: 必填, 由路由引擎按 appId+method 匹配
/// - 直定模式(已传 channelMchNo+capability): 可空, 由 PayRouteService 按能力反推回填
/// @see PayMethodEnum
@Schema(description = "支付方式编码")
@NotBlank(message = "{validation.field.method.notBlank}")
@Schema(description = "支付方式编码(路由模式必填; 直定模式可空, 由能力反推)")
@Size(max = 32, message = "{validation.field.method.size}")
private String method;

View File

@@ -1,21 +0,0 @@
package cn.daxpay.open.payment.web.develop.result;
import cn.daxpay.open.payment.unipay.result.trade.pay.NormalPayResult;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
/// # 支付调试结果
@Data
@Accessors(chain = true)
@Schema(title = "支付调试结果")
public class DevelopPayResult {
/// 支付结果
@Schema(description = "支付结果")
private NormalPayResult payResult;
/// 响应签名(平台私钥签名, 可用于验证响应完整性)
@Schema(description = "响应签名")
private String sign;
}

View File

@@ -1,100 +0,0 @@
package cn.daxpay.open.payment.web.develop.service;
import cn.daxpay.open.payment.web.develop.param.DevelopParam;
import cn.daxpay.open.payment.web.develop.result.DevelopPayResult;
import cn.daxpay.open.payment.web.develop.result.DevelopSignResult;
import cn.daxpay.open.payment.common.runtime.PaymentContext;
import cn.daxpay.open.payment.common.service.MerchantContextLoader;
import cn.daxpay.open.payment.common.service.PaySignService;
import cn.daxpay.open.payment.common.util.ObjectSignStrUtil;
import cn.daxpay.open.payment.common.util.PaySignUtil;
import cn.daxpay.open.payment.masterdata.constants.provider.result.PayProviderMethodResult;
import cn.daxpay.open.payment.masterdata.constants.provider.service.PayProviderMethodService;
import cn.daxpay.open.payment.merchant.service.route.support.PayRouteStrategyCapabilitySupport;
import cn.daxpay.open.payment.core.trade.runtime.service.pay.NormalPayService;
import cn.daxpay.open.payment.unipay.param.trade.pay.NormalPayParam;
import cn.daxpay.open.payment.unipay.result.trade.pay.NormalPayResult;
import cn.daxpay.open.platform.common.config.properties.PlatformConfigProperties;
import cn.daxpay.open.platform.core.code.CommonErrorCode;
import cn.daxpay.open.platform.core.exception.BizInfoException;
import cn.daxpay.open.platform.core.rest.dto.LabelValue;
import cn.hutool.core.util.StrUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
/// # 交易开发调试服务
///
/// 复用普通支付核心服务 NormalPayService, 在管理端以指定商户/应用身份真实发起一笔支付,
/// 便于联调验证通道配置与签名是否正确
@Slf4j
@Service
@RequiredArgsConstructor
public class DevelopTradeService {
private final NormalPayService normalPayService;
private final PaymentContext paymentContext;
private final MerchantContextLoader merchantContextLoader;
private final PaySignService paySignService;
private final PlatformConfigProperties platformConfigProperties;
private final PayProviderMethodService payProviderMethodService;
private final PayRouteStrategyCapabilitySupport payRouteStrategyCapabilitySupport;
/// 生成支付参数签名(与正式签名逻辑一致)
public DevelopSignResult sign(DevelopParam<NormalPayParam> param) {
// 签名串(与 PaySignUtil 内部一致)
String signStr = ObjectSignStrUtil.buildSignStr(param.getParam());
// 签名值
String sign = PaySignUtil.sign(param.getParam(), param.getPrivateKey());
return new DevelopSignResult().setSignStr(signStr).setSign(sign);
}
/// 支付调试(真实发起)
///
/// 安全流程与正式支付一致:
/// 1. 使用传入的商户私钥对参数签名, 再用系统配置的商户公钥验签, 校验私钥是否正确
/// 2. 发起真实支付
/// 3. 使用平台私钥对支付结果签名, 便于客户端验证响应完整性
public DevelopPayResult pay(DevelopParam<NormalPayParam> param) {
NormalPayParam payParam = param.getParam();
// 私钥必填
if (StrUtil.isBlank(param.getPrivateKey())) {
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.error.assist.privateKeyEmpty");
}
// 在支付作用域内执行, 初始化商户身份(mchNo 进入线程上下文)
return paymentContext.runAs(() -> {
// 初始化商户身份(含状态校验)
merchantContextLoader.initMch(payParam.getMchNo());
// 私钥验证: 用传入私钥签名, 再用系统配置的商户公钥验签
String sign = PaySignUtil.sign(payParam, param.getPrivateKey());
payParam.setSign(sign);
paySignService.signVerify(payParam);
// 发起真实支付
NormalPayResult payResult = normalPayService.pay(payParam);
// 使用平台私钥对支付结果签名
String platformPrivateKey = platformConfigProperties.getKeyConfig().getPrivateKey();
String resultSign = PaySignUtil.sign(payResult, platformPrivateKey);
return new DevelopPayResult()
.setPayResult(payResult)
.setSign(resultSign);
});
}
/// 已启用渠道支付方式目录(供调试页支付方式下拉)
public List<PayProviderMethodResult> listMethodDirectory() {
return payProviderMethodService.listDirectoryFlat();
}
/// 传值模式:商户全部启用通道商户候选, provider 为空返回全部,
/// 非空按支付渠道过滤(如 wechat 返回所有声明支持微信的产品通道商户, 含官方与三方聚合通道)
public List<LabelValue> listChannelMchCandidates(String mchNo, String provider) {
return payRouteStrategyCapabilitySupport.listDirectChannelMchCandidates(mchNo, provider);
}
/// 传值模式:按通道商户(产品)返回全部启用支付能力候选
public List<LabelValue> listCapabilityCandidates(String channelMchNo) {
return payRouteStrategyCapabilitySupport.listDirectCapabilityCandidates(channelMchNo);
}
}

View File

@@ -1,6 +1,7 @@
{
"strategyNotFound": "Payment route strategy not configured for this app",
"noMatch": "No available payment product matched",
"methodRequired": "Pay method is required in route mode",
"providerRequired": "Pay brand is required when product is not specified",
"methodRequiredWhenProductSet": "Pay method is required when product is specified",
"basicProviderInvalid": "Invalid pay brand",

View File

@@ -1,6 +1,7 @@
{
"strategyNotFound": "应用未配置通道路由策略",
"noMatch": "未匹配到可用支付产品",
"methodRequired": "路由模式下支付方式不能为空",
"providerRequired": "未指定支付产品时,支付渠道不能为空",
"methodRequiredWhenProductSet": "指定支付产品时,支付方式不能为空",
"basicProviderInvalid": "支付渠道无效",