feat(payment): 新增开发调试接口与菜单

- 新增 develop 签名调试接口 /admin/develop/sign/{gen,verify}
- 新增 develop 支付调试接口 /admin/develop/trade/{sign,pay}
- 新增 DevelopParam/DevelopSignParam/DevelopVerifyParam 参数类
- 新增 DevelopSignResult/DevelopPayResult 结果类
- 新增 DevelopSignService/DevelopTradeService 服务
- iam_perm_menu 新增开发调试目录及支付/签名调试菜单
This commit is contained in:
DaxPay Dev
2026-06-23 23:49:32 +08:00
parent e3fd2d65d7
commit 7f34c9db57
10 changed files with 297 additions and 0 deletions

View File

@@ -144,6 +144,9 @@ INSERT INTO "public"."iam_perm_menu" VALUES (4040116, 401, 'payment:platform:pro
INSERT INTO "public"."iam_perm_menu" VALUES (204, 2, 'demos:artemis', 'admin', 'ArtemisDemo', '消息队列演示', 'Message Queue Demo', 'menu.demos.artemis', 'lucide:send', 'f', 'f', '/demos/artemis/ArtemisDemo', '/demos/artemis', NULL, 4, 'f', 't', 'f', 0, NULL, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-06-18 00:00:00+00', '2026-06-18 00:00:00+00');
INSERT INTO "public"."iam_perm_menu" VALUES (900001, 304, 'iam:social:config', 'admin', 'SocialConfig', '三方平台登录', 'Third-party Platform Login', 'menu.system.config.social', 'lucide:share-2', 'f', 'f', 'views/iam/social/social-config', '/system/config/social', NULL, 5, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO "public"."iam_perm_menu" VALUES (205, 2, 'demos:channel', 'admin', 'ChannelDemo', '通道连通性演示', 'Channel Connectivity Demo', 'menu.demos.channel', 'lucide:radio-tower', 'f', 'f', '/demos/channel/ChannelDemo', '/demos/channel', NULL, 5, 'f', 't', 'f', 0, NULL, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-06-23 00:00:00+00', '2026-06-23 00:00:00+00');
INSERT INTO "public"."iam_perm_menu" VALUES (8, NULL, 'develop', 'admin', 'Develop', '开发调试', 'Develop', 'menu.develop', 'lucide:wrench', 'f', 'f', NULL, '/develop', NULL, 5, 'f', 't', 'f', 1, 1, 0, 'f', 'catalog', NULL, NULL, NULL, NULL, NULL, NULL, '2026-06-23 16:00:00+00', '2026-06-23 16:00:00+00');
INSERT INTO "public"."iam_perm_menu" VALUES (801, 8, 'payment:develop:trade', 'admin', 'DevelopTrade', '支付调试', 'Payment Develop', 'menu.develop.trade', 'lucide:credit-card', 'f', 'f', '/payment/develop/trade/DevelopTrade', '/develop/trade', NULL, 1, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-06-23 16:00:00+00', '2026-06-23 16:00:00+00');
INSERT INTO "public"."iam_perm_menu" VALUES (802, 8, 'payment:develop:sign', 'admin', 'DevelopSign', '签名调试', 'Sign Develop', 'menu.develop.sign', 'lucide:file-signature', 'f', 'f', '/payment/develop/sign/DevelopSign', '/develop/sign', NULL, 2, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-06-23 16:00:00+00', '2026-06-23 16:00:00+00');
-- ----------------------------
-- Primary Key structure for table iam_perm_menu

View File

@@ -0,0 +1,41 @@
package cn.daxpay.open.payment.admin.controller.develop;
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;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/// 签名调试(管理)
@PermCode(menuCode = "payment:develop:sign")
@Tag(name = "签名调试服务")
@RestController
@RequestMapping("/admin/develop/sign")
@RequiredArgsConstructor
public class DevelopSignController {
private final DevelopSignService developSignService;
@PermCode(code = "view", nameCn = "查看", nameEn = "View")
@Operation(summary = "生成签名")
@PostMapping("/gen")
public Result<DevelopSignResult> sign(@RequestBody DevelopSignParam param) {
return Res.ok(developSignService.sign(param));
}
@PermCode(code = "view", nameCn = "查看", nameEn = "View")
@Operation(summary = "验签")
@PostMapping("/verify")
public Result<Boolean> verify(@RequestBody DevelopVerifyParam param) {
return Res.ok(developSignService.verify(param));
}
}

View File

@@ -0,0 +1,42 @@
package cn.daxpay.open.payment.admin.controller.develop;
import cn.daxpay.open.payment.admin.param.develop.DevelopParam;
import cn.daxpay.open.payment.admin.result.develop.DevelopPayResult;
import cn.daxpay.open.payment.admin.result.develop.DevelopSignResult;
import cn.daxpay.open.payment.admin.service.develop.DevelopTradeService;
import cn.daxpay.open.payment.unipay.param.trade.pay.PayParam;
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;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/// 交易开发调试(管理)
@PermCode(menuCode = "payment:develop:trade")
@Tag(name = "交易开发调试服务")
@RestController
@RequestMapping("/admin/develop/trade")
@RequiredArgsConstructor
public class DevelopTradeController {
private final DevelopTradeService developTradeService;
@PermCode(code = "sign", nameCn = "签名", nameEn = "Sign")
@Operation(summary = "支付参数签名")
@PostMapping("/sign")
public Result<DevelopSignResult> sign(@RequestBody DevelopParam<PayParam> param) {
return Res.ok(developTradeService.sign(param));
}
@PermCode(code = "pay", nameCn = "支付", nameEn = "Pay")
@Operation(summary = "支付调试(真实发起)")
@PostMapping("/pay")
public Result<DevelopPayResult> pay(@RequestBody DevelopParam<PayParam> param) {
return Res.ok(developTradeService.pay(param));
}
}

View File

@@ -0,0 +1,23 @@
package cn.daxpay.open.payment.admin.param.develop;
import cn.daxpay.open.payment.unipay.param.MerchantPaymentCommonParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
/// # 开发调试参数包装
///
/// 包装业务参数与调试用手填私钥, 私钥仅用于本次签名生成, 不入库
@Data
@Accessors(chain = true)
@Schema(title = "开发调试参数包装")
public class DevelopParam<T extends MerchantPaymentCommonParam> {
/// 业务参数
@Schema(description = "业务参数")
private T param;
/// 生成签名使用的私钥(PEM 格式, 调试页手填, 不入库)
@Schema(description = "生成签名使用的私钥(PEM格式)")
private String privateKey;
}

View File

@@ -0,0 +1,20 @@
package cn.daxpay.open.payment.admin.param.develop;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
/// # 签名调试-生成签名参数
@Data
@Accessors(chain = true)
@Schema(title = "签名调试-生成签名参数")
public class DevelopSignParam {
/// 待签名 JSON 字符串
@Schema(description = "待签名JSON字符串")
private String json;
/// 私钥(PEM 格式)
@Schema(description = "私钥(PEM格式)")
private String privateKey;
}

View File

@@ -0,0 +1,24 @@
package cn.daxpay.open.payment.admin.param.develop;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
/// # 签名调试-验签参数
@Data
@Accessors(chain = true)
@Schema(title = "签名调试-验签参数")
public class DevelopVerifyParam {
/// 待验签 JSON 字符串(不含 sign 字段)
@Schema(description = "待验签JSON字符串(不含sign字段)")
private String json;
/// 签名值
@Schema(description = "签名值")
private String sign;
/// 公钥(PEM 格式)
@Schema(description = "公钥(PEM格式)")
private String publicKey;
}

View File

@@ -0,0 +1,25 @@
package cn.daxpay.open.payment.admin.result.develop;
import cn.daxpay.open.payment.unipay.result.trade.pay.PayResult;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
/// # 支付调试结果
@Data
@Accessors(chain = true)
@Schema(title = "支付调试结果")
public class DevelopPayResult {
/// 发送的请求体 JSON
@Schema(description = "请求体JSON")
private String requestBody;
/// 签名信息(传入私钥时返回)
@Schema(description = "签名信息")
private DevelopSignResult signInfo;
/// 支付结果
@Schema(description = "支付结果")
private PayResult payResult;
}

View File

@@ -0,0 +1,20 @@
package cn.daxpay.open.payment.admin.result.develop;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
/// # 签名调试结果
@Data
@Accessors(chain = true)
@Schema(title = "签名调试结果")
public class DevelopSignResult {
/// 待签名原文(键值拼接串)
@Schema(description = "待签名原文")
private String signStr;
/// 签名值
@Schema(description = "签名值")
private String sign;
}

View File

@@ -0,0 +1,40 @@
package cn.daxpay.open.payment.admin.service.develop;
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;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/// # 签名调试服务
///
/// 提供参数签名生成与验签功能, 便于商户对接联调
@Slf4j
@Service
@RequiredArgsConstructor
public class DevelopSignService {
/// 生成签名
public DevelopSignResult sign(DevelopSignParam param) {
// 待签名原文(扁平化排序后的键值拼接串)
String signStr = JsonSignStrUtil.buildSignStr(param.getJson());
// 使用私钥生成签名
String sign = RsaSignUtil.sign(signStr, param.getPrivateKey());
return new DevelopSignResult().setSignStr(signStr).setSign(sign);
}
/// 验签
public boolean verify(DevelopVerifyParam param) {
String signStr = JsonSignStrUtil.buildSignStr(param.getJson());
try {
return RsaSignUtil.verify(signStr, param.getSign(), param.getPublicKey());
} catch (Exception e) {
// 验签失败不抛异常, 返回 false 便于前端展示
log.warn("验签失败: {}", e.getMessage());
return false;
}
}
}

View File

@@ -0,0 +1,59 @@
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.DevelopPayResult;
import cn.daxpay.open.payment.admin.result.develop.DevelopSignResult;
import cn.daxpay.open.payment.common.context.PaymentScopeManager;
import cn.daxpay.open.payment.common.util.ObjectSignStrUtil;
import cn.daxpay.open.payment.common.util.PaySignUtil;
import cn.daxpay.open.payment.pay.service.NormalPayService;
import cn.daxpay.open.payment.unipay.param.trade.pay.PayParam;
import cn.daxpay.open.payment.unipay.result.trade.pay.PayResult;
import cn.daxpay.open.platform.common.json.util.JsonUtil;
import cn.hutool.core.util.StrUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/// # 交易开发调试服务
///
/// 复用普通支付核心服务 NormalPayService, 在管理端以指定商户/应用身份真实发起一笔支付,
/// 便于联调验证通道配置与签名是否正确
@Slf4j
@Service
@RequiredArgsConstructor
public class DevelopTradeService {
private final NormalPayService normalPayService;
private final PaymentScopeManager paymentScopeManager;
/// 生成支付参数签名(与正式签名逻辑一致)
public DevelopSignResult sign(DevelopParam<PayParam> param) {
// 签名串(与 PaySignUtil 内部一致)
String signStr = ObjectSignStrUtil.buildSignStr(param.getParam());
// 签名值
String sign = PaySignUtil.sign(param.getParam(), param.getPrivateKey());
return new DevelopSignResult().setSignStr(signStr).setSign(sign);
}
/// 支付调试(真实发起)
public DevelopPayResult pay(DevelopParam<PayParam> param) {
PayParam payParam = param.getParam();
// 记录发送的请求体
String requestBody = JsonUtil.toJsonStr(payParam);
// 签名信息(传入私钥时生成, 便于核对)
DevelopSignResult signInfo = null;
if (StrUtil.isNotBlank(param.getPrivateKey())) {
signInfo = this.sign(param);
}
// 在支付作用域内执行, 初始化商户上下文, 使 mchNo/appId 等字段自动填充
PayResult payResult = paymentScopeManager.executeWithScope(
payParam.getMchNo(), payParam.getAppId(),
() -> normalPayService.pay(payParam)
);
return new DevelopPayResult()
.setRequestBody(requestBody)
.setSignInfo(signInfo)
.setPayResult(payResult);
}
}