mirror of
https://gitee.com/dromara/dax-pay
synced 2026-08-12 07:25:39 +08:00
refactor(demo): 测试回调从 unipay 迁移到 demo 模块
- 删除 unipay 模块 TestCallbackController(测试代码不应进生产模块) - demo 模块新增 callback: TestCallbackController(验签+记录) / TestCallbackRecord / TestCallbackStore - daxpay-demo pom 增 daxpay-payment-core 依赖(提供 PaySignUtil 验签)
This commit is contained in:
@@ -42,6 +42,13 @@
|
||||
<artifactId>capability-audit-log</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 支付域内核(提供 PaySignUtil,用于测试回调验签演示;传递 common-config 含 PlatformConfigProperties) -->
|
||||
<dependency>
|
||||
<groupId>cn.daxpay.open</groupId>
|
||||
<artifactId>daxpay-payment-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
package cn.daxpay.open.demo.callback.controller;
|
||||
|
||||
import cn.daxpay.open.demo.callback.result.TestCallbackRecord;
|
||||
import cn.daxpay.open.demo.callback.service.TestCallbackStore;
|
||||
import cn.daxpay.open.payment.common.util.PaySignUtil;
|
||||
import cn.daxpay.open.platform.common.config.properties.PlatformConfigProperties;
|
||||
import cn.daxpay.open.platform.common.json.util.JacksonUtil;
|
||||
import cn.daxpay.open.platform.core.annotation.IgnoreAuth;
|
||||
import cn.daxpay.open.platform.core.rest.Res;
|
||||
import cn.daxpay.open.platform.core.rest.result.Result;
|
||||
import cn.hutool.core.lang.UUID;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
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;
|
||||
import tools.jackson.core.type.TypeReference;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/// # 测试回调接受控制器
|
||||
///
|
||||
/// 模拟商户端接收平台出站通知([DaxNoticeResult] 签名 JSON),
|
||||
/// 用于支付/退款回调链路的联调与验证。
|
||||
///
|
||||
/// 平台出站通知经 [SystemHttpSignedSender] 用平台私钥 RSA 签名后 POST 到商户配置的 notifyUrl,
|
||||
/// 本端用平台公钥验签并暂存记录, 供 `GET /test/callback/list` 查看联调结果。
|
||||
///
|
||||
/// Ack 规则: 接收端点(notify/pay/refund)返回固定 "SUCCESS"
|
||||
/// (平台要求 HTTP 2xx 且 body 等于 SUCCESS, 忽略大小写)。
|
||||
/// 管理端点(list/clear)走统一 [Result] 格式, 供人工查看。
|
||||
///
|
||||
/// 鉴权: `/test/**` 已在白名单, 类上叠加 `@IgnoreAuth` 双保险。
|
||||
@Slf4j
|
||||
@IgnoreAuth
|
||||
@Tag(name = "测试商户回调接收控制器")
|
||||
@RestController
|
||||
@RequestMapping("/test/callback")
|
||||
@RequiredArgsConstructor
|
||||
public class TestCallbackController {
|
||||
|
||||
private final PlatformConfigProperties platformConfigProperties;
|
||||
private final TestCallbackStore store;
|
||||
|
||||
/// 通用通知接收入口(支付/退款均可)
|
||||
@Operation(summary = "通用通知接收")
|
||||
@PostMapping("/notify")
|
||||
public String notify(@RequestBody String body) {
|
||||
return handle(body);
|
||||
}
|
||||
|
||||
/// 支付通知接收(语义端点, 商户可将应用 notifyUrl 指向此)
|
||||
@Operation(summary = "支付通知接收")
|
||||
@PostMapping("/pay")
|
||||
public String pay(@RequestBody String body) {
|
||||
return handle(body);
|
||||
}
|
||||
|
||||
/// 退款通知接收(语义端点)
|
||||
@Operation(summary = "退款通知接收")
|
||||
@PostMapping("/refund")
|
||||
public String refund(@RequestBody String body) {
|
||||
return handle(body);
|
||||
}
|
||||
|
||||
/// 拉取最近的回调接收记录(最新在前)
|
||||
@Operation(summary = "拉取回调接收记录")
|
||||
@GetMapping("/list")
|
||||
public Result<List<TestCallbackRecord>> list() {
|
||||
return Res.ok(store.list());
|
||||
}
|
||||
|
||||
/// 清空全部回调接收记录
|
||||
@Operation(summary = "清空调回接收记录")
|
||||
@PostMapping("/clear")
|
||||
public Result<Void> clear() {
|
||||
store.clear();
|
||||
return Res.ok();
|
||||
}
|
||||
|
||||
/// 统一处理: 验签 → 解析 → 暂存 → 返回 SUCCESS
|
||||
private String handle(String body) {
|
||||
log.info("收到测试回调通知: {}", body);
|
||||
// 用平台公钥验签
|
||||
String publicKey = platformConfigProperties.getKeyConfig().getPublicKey();
|
||||
boolean verifyResult = PaySignUtil.verify(body, publicKey);
|
||||
log.info("回调签名验证结果: {}", verifyResult);
|
||||
|
||||
// 解析报文, 提取关键字段(event 区分支付/退款, data 内业务号等)
|
||||
String event = null;
|
||||
String mchNo = null;
|
||||
String appId = null;
|
||||
String bizNo = null;
|
||||
String amount = null;
|
||||
try {
|
||||
Map<String, Object> map = JacksonUtil.toBean(body, new TypeReference<Map<String, Object>>() {});
|
||||
event = (String) map.get("event");
|
||||
mchNo = (String) map.get("mchNo");
|
||||
appId = (String) map.get("appId");
|
||||
Object dataObj = map.get("data");
|
||||
if (dataObj instanceof Map<?, ?> dataMap) {
|
||||
bizNo = pickFirst(dataMap, "tradeNo", "outTradeNo", "refundNo", "outRefundNo", "orderNo");
|
||||
amount = pickFirst(dataMap, "amount", "totalAmount", "payAmount", "refundAmount");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 解析失败不影响 Ack, 仅记录原始报文供排查
|
||||
log.warn("解析通知报文失败, 仅记录原始报文", e);
|
||||
}
|
||||
|
||||
TestCallbackRecord record = new TestCallbackRecord()
|
||||
.setId(UUID.randomUUID().toString(true))
|
||||
.setEvent(event)
|
||||
.setBizType(resolveBizType(event))
|
||||
.setMchNo(mchNo)
|
||||
.setAppId(appId)
|
||||
.setBizNo(bizNo)
|
||||
.setAmount(amount)
|
||||
.setVerifyResult(verifyResult)
|
||||
.setReceiveTime(OffsetDateTime.now())
|
||||
.setRawBody(StrUtil.sub(body, 0, 2000));
|
||||
store.add(record);
|
||||
return "SUCCESS";
|
||||
}
|
||||
|
||||
/// 按 key 顺序取首个非空值
|
||||
private String pickFirst(Map<?, ?> map, String... keys) {
|
||||
for (String key : keys) {
|
||||
Object val = map.get(key);
|
||||
if (val != null) {
|
||||
return String.valueOf(val);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 由事件码推断业务类型(pay / refund / unknown)
|
||||
private String resolveBizType(String event) {
|
||||
if (StrUtil.isBlank(event)) {
|
||||
return "unknown";
|
||||
}
|
||||
if (event.startsWith("pay")) {
|
||||
return "pay";
|
||||
}
|
||||
if (event.startsWith("refund")) {
|
||||
return "refund";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package cn.daxpay.open.demo.callback.result;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
/// # 测试回调接收记录
|
||||
///
|
||||
/// 模拟商户端接收平台出站通知([DaxNoticeResult] 签名 JSON)后生成的记录,
|
||||
/// 暂存于内存供联调时通过 `/test/callback/list` 查看。
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Schema(title = "测试回调接收记录")
|
||||
public class TestCallbackRecord {
|
||||
|
||||
/// 记录 ID
|
||||
@Schema(description = "记录ID")
|
||||
private String id;
|
||||
|
||||
/// 通知事件码(如 pay.success / refund.success)
|
||||
@Schema(description = "通知事件码")
|
||||
private String event;
|
||||
|
||||
/// 业务类型(pay / refund / unknown, 由 event 前缀解析)
|
||||
@Schema(description = "业务类型")
|
||||
private String bizType;
|
||||
|
||||
/// 商户号
|
||||
@Schema(description = "商户号")
|
||||
private String mchNo;
|
||||
|
||||
/// 应用ID
|
||||
@Schema(description = "应用ID")
|
||||
private String appId;
|
||||
|
||||
/// 业务号(优先取 tradeNo, 其次 outTradeNo / refundNo / outRefundNo)
|
||||
@Schema(description = "业务号")
|
||||
private String bizNo;
|
||||
|
||||
/// 金额(分, 如报文携带)
|
||||
@Schema(description = "金额(分)")
|
||||
private String amount;
|
||||
|
||||
/// 签名验证结果
|
||||
@Schema(description = "验签结果")
|
||||
private boolean verifyResult;
|
||||
|
||||
/// 接收时间(UTC)
|
||||
@Schema(description = "接收时间(UTC)")
|
||||
private OffsetDateTime receiveTime;
|
||||
|
||||
/// 原始报文(截断保留, 便于排查)
|
||||
@Schema(description = "原始报文")
|
||||
private String rawBody;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package cn.daxpay.open.demo.callback.service;
|
||||
|
||||
import cn.daxpay.open.demo.callback.result.TestCallbackRecord;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||
|
||||
/// # 测试回调接收记录内存存储
|
||||
///
|
||||
/// 演示场景下的回调接收记录暂存地, 进程内存、非持久化。
|
||||
///
|
||||
/// 设计取舍:
|
||||
/// - 仅用于 demo 联调, 不做持久化(重启即清空)
|
||||
/// - 有界容量 100 条, 超出后丢弃最旧记录, 避免内存膨胀
|
||||
/// - 使用 {@link ConcurrentLinkedDeque} 保证并发安全
|
||||
@Slf4j
|
||||
@Component
|
||||
public class TestCallbackStore {
|
||||
|
||||
/// 最大保留条数
|
||||
private static final int MAX_SIZE = 100;
|
||||
|
||||
private final ConcurrentLinkedDeque<TestCallbackRecord> deque = new ConcurrentLinkedDeque<>();
|
||||
|
||||
/// 追加一条接收记录, 超出容量时淘汰最旧记录
|
||||
public void add(TestCallbackRecord record) {
|
||||
deque.offerFirst(record);
|
||||
while (deque.size() > MAX_SIZE) {
|
||||
// 超出容量, 移除最旧(队尾)
|
||||
TestCallbackRecord removed = deque.pollLast();
|
||||
if (removed == null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 返回全部记录(最新的在前)
|
||||
public List<TestCallbackRecord> list() {
|
||||
return new ArrayList<>(deque);
|
||||
}
|
||||
|
||||
/// 清空全部记录
|
||||
public void clear() {
|
||||
deque.clear();
|
||||
log.info("测试回调接收记录已清空");
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package cn.daxpay.open.payment.unipay.trade.controller;
|
||||
|
||||
import cn.daxpay.open.platform.core.annotation.IgnoreAuth;
|
||||
import cn.daxpay.open.platform.common.config.properties.PlatformConfigProperties;
|
||||
import cn.daxpay.open.payment.common.util.PaySignUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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;
|
||||
|
||||
/// # 测试回调接受控制器
|
||||
///
|
||||
@Slf4j
|
||||
@Tag(name = "测试商户回调接收控制器")
|
||||
@RestController
|
||||
@RequestMapping("/test/callback")
|
||||
@RequiredArgsConstructor
|
||||
@IgnoreAuth
|
||||
public class TestCallbackController {
|
||||
|
||||
@Operation(summary = "notify")
|
||||
@PostMapping("/notify")
|
||||
public String notify(@RequestBody String data){
|
||||
log.info("notify:{}",data);
|
||||
var licenseKey = SpringUtil.getBean(PlatformConfigProperties.class).getKeyConfig();
|
||||
boolean verify = PaySignUtil.verify(data, licenseKey.getPublicKey());
|
||||
log.info("回调签名认证: :{}",verify);
|
||||
return "success";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user