mirror of
https://gitee.com/dromara/dax-pay
synced 2026-08-12 23:45:39 +08:00
feat(notice): 新增商户出站通知管线(任务/重试/协议SPI)
统一订单级与应用级 webhook,接入支付/退款终态与易支付协议发送,并提供管理端任务查询与手动重发。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -62,7 +62,7 @@ public class EasyPayOrder extends MchBaseEntity implements ToResult<EasyPayOrder
|
||||
/// 已退款金额(元)
|
||||
private BigDecimal refundMoney;
|
||||
|
||||
/// 异步通知地址(本期仅落库不发送)
|
||||
/// 异步通知地址(由 EasyPayNoticeSender 出站推送,不写内核订单 notifyUrl)
|
||||
private String notifyUrl;
|
||||
|
||||
/// 同步跳转
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
package cn.daxpay.open.plugin.easypay.notice;
|
||||
|
||||
import cn.daxpay.open.payment.trade.notice.entity.MchNoticeTask;
|
||||
import cn.daxpay.open.payment.trade.notice.protocol.NoticeProtocolSender;
|
||||
import cn.daxpay.open.platform.common.json.util.JacksonUtil;
|
||||
import cn.daxpay.open.platform.core.enums.pay.notice.NoticeProtocolEnum;
|
||||
import cn.daxpay.open.plugin.easypay.dao.EasyPayOrderManager;
|
||||
import cn.daxpay.open.plugin.easypay.entity.EasyPayOrder;
|
||||
import cn.daxpay.open.plugin.easypay.enums.EasyPayApiVersionEnum;
|
||||
import cn.daxpay.open.plugin.easypay.result.api.v1.EasyPayCallbackV1Result;
|
||||
import cn.daxpay.open.plugin.easypay.result.api.v2.EasyPayCallbackV2Result;
|
||||
import cn.daxpay.open.plugin.easypay.service.config.EasyPayCredentialService;
|
||||
import cn.daxpay.open.plugin.easypay.util.EasyPayUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.core.util.URLUtil;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import tools.jackson.core.type.TypeReference;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Objects;
|
||||
import java.util.TreeMap;
|
||||
|
||||
/// # 易支付协议出站发送器
|
||||
///
|
||||
/// content_mode=ref:content 存 EasyPayOrder.id;发送时实时组装 V1/V2 GET 回调
|
||||
/// Ack:HTTP 2xx 且 body=SUCCESS(忽略大小写)
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class EasyPayNoticeSender implements NoticeProtocolSender {
|
||||
|
||||
private static final DateTimeFormatter NORM =
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.of("Asia/Shanghai"));
|
||||
|
||||
private final EasyPayOrderManager easyPayOrderManager;
|
||||
private final EasyPayCredentialService easyPayCredentialService;
|
||||
|
||||
@Override
|
||||
public String protocol() {
|
||||
return NoticeProtocolEnum.EASY_PAY.getCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public NoticeSendResult send(MchNoticeTask task) {
|
||||
NoticeSendResult result = new NoticeSendResult();
|
||||
Long easyPayOrderId = JSONUtil.parseObj(task.getContent()).getLong("id");
|
||||
if (easyPayOrderId == null) {
|
||||
return result.setSuccess(false).setErrorMsg("easy pay ref missing id");
|
||||
}
|
||||
EasyPayOrder order = easyPayOrderManager.findByIdNotTenant(easyPayOrderId).orElse(null);
|
||||
if (order == null) {
|
||||
return result.setSuccess(false).setErrorMsg("easy pay order not found: " + easyPayOrderId);
|
||||
}
|
||||
if (Objects.equals(order.getApiVersion(), EasyPayApiVersionEnum.V1.getCode())) {
|
||||
return sendV1(task, order, result);
|
||||
}
|
||||
return sendV2(task, order, result);
|
||||
}
|
||||
|
||||
private NoticeSendResult sendV1(MchNoticeTask task, EasyPayOrder order, NoticeSendResult result) {
|
||||
var credential = easyPayCredentialService.getAndCheck(order.getPid());
|
||||
var callback = new EasyPayCallbackV1Result()
|
||||
.setPid(order.getPid())
|
||||
.setTradeNo(order.getTradeNo())
|
||||
.setOutTradeNo(order.getOutTradeNo())
|
||||
.setType(order.getType())
|
||||
.setName(order.getName())
|
||||
.setMoney(order.getMoney() == null ? null : order.getMoney().toPlainString())
|
||||
.setTradeStatus("TRADE_SUCCESS")
|
||||
.setParam(order.getParam())
|
||||
.setSignType("MD5");
|
||||
// 仅一次 MD5 签名(修复商业版重复 setSign)
|
||||
callback.setSign(EasyPayUtil.signByMd5(callback, credential.getMd5Key()));
|
||||
return doGet(task.getUrl(), callback, result);
|
||||
}
|
||||
|
||||
private NoticeSendResult sendV2(MchNoticeTask task, EasyPayOrder order, NoticeSendResult result) {
|
||||
var credential = easyPayCredentialService.getAndCheck(order.getPid());
|
||||
var callback = new EasyPayCallbackV2Result()
|
||||
.setPid(order.getPid())
|
||||
.setTradeNo(order.getTradeNo())
|
||||
.setOutTradeNo(order.getOutTradeNo())
|
||||
.setApiTradeNo(order.getApiTradeNo())
|
||||
.setType(order.getType())
|
||||
.setTradeStatus("TRADE_SUCCESS")
|
||||
.setAddTime(order.getAddTime() == null ? null : NORM.format(order.getAddTime().toInstant()))
|
||||
.setEndTime(order.getEndTime() == null ? null : NORM.format(order.getEndTime().toInstant()))
|
||||
.setName(order.getName())
|
||||
.setMoney(order.getMoney() == null ? null : order.getMoney().toPlainString())
|
||||
.setParam(order.getParam())
|
||||
.setBuyer(order.getBuyer())
|
||||
.setTimestamp(String.valueOf(System.currentTimeMillis() / 1000))
|
||||
.setSignType("RSA");
|
||||
callback.setSign(EasyPayUtil.signByRsa(callback, credential.getPlatformPrivateKey()));
|
||||
return doGet(task.getUrl(), callback, result);
|
||||
}
|
||||
|
||||
private NoticeSendResult doGet(String baseUrl, Object callback, NoticeSendResult result) {
|
||||
String body = null;
|
||||
Integer httpStatus = null;
|
||||
try {
|
||||
TreeMap<String, String> map = JacksonUtil.toBean(JacksonUtil.toJson(callback),
|
||||
new TypeReference<TreeMap<String, String>>() {});
|
||||
String query = URLUtil.buildQuery(map, StandardCharsets.UTF_8);
|
||||
String fullUrl = baseUrl.contains("?") ? baseUrl + "&" + query : baseUrl + "?" + query;
|
||||
result.setRequestDigest(StrUtil.sub(fullUrl, 0, 500));
|
||||
HttpResponse response = HttpUtil.createGet(fullUrl).timeout(15000).execute();
|
||||
httpStatus = response.getStatus();
|
||||
body = response.body();
|
||||
} catch (Exception e) {
|
||||
log.error("易支付通知发送失败, url={}", baseUrl, e);
|
||||
return result.setSuccess(false).setHttpStatus(httpStatus).setErrorMsg(e.getMessage());
|
||||
}
|
||||
result.setHttpStatus(httpStatus);
|
||||
boolean ack = httpStatus != null && httpStatus >= 200 && httpStatus < 300
|
||||
&& StrUtil.equalsIgnoreCase(StrUtil.trim(body), "SUCCESS");
|
||||
result.setSuccess(ack);
|
||||
if (!ack) {
|
||||
result.setErrorMsg(StrUtil.blankToDefault(StrUtil.sub(body, 0, 300), "httpStatus=" + httpStatus));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package cn.daxpay.open.plugin.easypay.result.api.v1;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/// # 易支付 V1 回调报文
|
||||
///
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class EasyPayCallbackV1Result implements Serializable {
|
||||
|
||||
@JsonProperty("pid")
|
||||
private Integer pid;
|
||||
|
||||
@JsonProperty("trade_no")
|
||||
private String tradeNo;
|
||||
|
||||
@JsonProperty("out_trade_no")
|
||||
private String outTradeNo;
|
||||
|
||||
@JsonProperty("type")
|
||||
private String type;
|
||||
|
||||
@JsonProperty("name")
|
||||
private String name;
|
||||
|
||||
@JsonProperty("money")
|
||||
private String money;
|
||||
|
||||
@JsonProperty("trade_status")
|
||||
private String tradeStatus;
|
||||
|
||||
@JsonProperty("param")
|
||||
private String param;
|
||||
|
||||
@JsonProperty("sign")
|
||||
private String sign;
|
||||
|
||||
@JsonProperty("sign_type")
|
||||
private String signType;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package cn.daxpay.open.plugin.easypay.result.api.v2;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/// # 易支付 V2 回调报文
|
||||
///
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Schema(title = "易支付V2回调报文")
|
||||
public class EasyPayCallbackV2Result implements Serializable {
|
||||
|
||||
@JsonProperty("pid")
|
||||
private Integer pid;
|
||||
|
||||
@JsonProperty("trade_no")
|
||||
private String tradeNo;
|
||||
|
||||
@JsonProperty("out_trade_no")
|
||||
private String outTradeNo;
|
||||
|
||||
@JsonProperty("api_trade_no")
|
||||
private String apiTradeNo;
|
||||
|
||||
@JsonProperty("type")
|
||||
private String type;
|
||||
|
||||
@JsonProperty("trade_status")
|
||||
private String tradeStatus;
|
||||
|
||||
@JsonProperty("addtime")
|
||||
private String addTime;
|
||||
|
||||
@JsonProperty("endtime")
|
||||
private String endTime;
|
||||
|
||||
@JsonProperty("name")
|
||||
private String name;
|
||||
|
||||
@JsonProperty("money")
|
||||
private String money;
|
||||
|
||||
@JsonProperty("param")
|
||||
private String param;
|
||||
|
||||
@JsonProperty("buyer")
|
||||
private String buyer;
|
||||
|
||||
@JsonProperty("timestamp")
|
||||
private String timestamp;
|
||||
|
||||
@JsonProperty("sign")
|
||||
private String sign;
|
||||
|
||||
@JsonProperty("sign_type")
|
||||
private String signType;
|
||||
}
|
||||
@@ -176,7 +176,7 @@ public class EasyPayPayV2Service {
|
||||
payParam.setOpenId(openId);
|
||||
payParam.setAuthCode(authCode);
|
||||
payParam.setClientIp(order.getClientIp());
|
||||
payParam.setNotifyUrl(order.getNotifyUrl());
|
||||
// 易支付 notifyUrl 仅落在协议单,不写内核订单,避免与 system 协议双发
|
||||
payParam.setReturnUrl(order.getReturnUrl());
|
||||
payParam.setAttach(order.getParam());
|
||||
payParam.setSource(easyPayAssistService.sourceCode());
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
package cn.daxpay.open.plugin.easypay.strategy;
|
||||
|
||||
import cn.daxpay.open.payment.strategy.plugin.AbsPayPluginStrategy;
|
||||
import cn.daxpay.open.payment.trade.notice.command.NoticeDispatchCommand;
|
||||
import cn.daxpay.open.payment.trade.notice.service.NoticeDispatcher;
|
||||
import cn.daxpay.open.payment.trade.order.entity.RefundOrder;
|
||||
import cn.daxpay.open.payment.trade.order.entity.PayTrade;
|
||||
import cn.daxpay.open.platform.common.json.util.JacksonUtil;
|
||||
import cn.daxpay.open.platform.core.enums.pay.notice.NoticeContentModeEnum;
|
||||
import cn.daxpay.open.platform.core.enums.pay.notice.NoticeEventEnum;
|
||||
import cn.daxpay.open.platform.core.enums.pay.notice.NoticeProtocolEnum;
|
||||
import cn.daxpay.open.platform.core.enums.pay.trade.TradeSourceEnum;
|
||||
import cn.daxpay.open.plugin.easypay.entity.EasyPayOrder;
|
||||
import cn.daxpay.open.plugin.easypay.service.order.EasyPayOrderService;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/// # 易支付插件生命周期策略
|
||||
@@ -20,14 +29,16 @@ import java.util.Objects;
|
||||
public class EasyPayPluginStrategy implements AbsPayPluginStrategy {
|
||||
|
||||
private final EasyPayOrderService easyPayOrderService;
|
||||
private final NoticeDispatcher noticeDispatcher;
|
||||
|
||||
/// 支付成功:回写协议单状态
|
||||
/// 支付成功:回写协议单状态并注册易支付出站通知
|
||||
@Override
|
||||
public void paySuccess(PayTrade trade) {
|
||||
if (!Objects.equals(TradeSourceEnum.EASY_PAY.getCode(), trade.getSource())) {
|
||||
return;
|
||||
}
|
||||
easyPayOrderService.paySuccess(trade);
|
||||
EasyPayOrder easyPayOrder = easyPayOrderService.paySuccess(trade);
|
||||
registerEasyPayNotice(easyPayOrder);
|
||||
}
|
||||
|
||||
/// 关单:一期仅日志
|
||||
@@ -48,4 +59,28 @@ public class EasyPayPluginStrategy implements AbsPayPluginStrategy {
|
||||
long amount = refundOrder.getAmount() == null ? 0L : refundOrder.getAmount();
|
||||
easyPayOrderService.refundSuccess(trade, amount);
|
||||
}
|
||||
|
||||
/// 注册易支付协议出站(content 仅存 id 指针)
|
||||
private void registerEasyPayNotice(EasyPayOrder easyPayOrder) {
|
||||
if (easyPayOrder == null || StrUtil.isBlank(easyPayOrder.getNotifyUrl())) {
|
||||
log.info("易支付订单无需回调, outTradeNo={}",
|
||||
easyPayOrder == null ? null : easyPayOrder.getOutTradeNo());
|
||||
return;
|
||||
}
|
||||
String content = JacksonUtil.toJson(Map.of(
|
||||
"id", easyPayOrder.getId(),
|
||||
"pid", easyPayOrder.getPid() == null ? 0 : easyPayOrder.getPid(),
|
||||
"remark", "ref-only; payload assembled at send time"
|
||||
));
|
||||
noticeDispatcher.dispatch(new NoticeDispatchCommand()
|
||||
.setMchNo(easyPayOrder.getMchNo())
|
||||
.setAppId(easyPayOrder.getAppId())
|
||||
.setEvent(NoticeEventEnum.PAY_SUCCESS.getCode())
|
||||
.setBizId(easyPayOrder.getId())
|
||||
.setBizNo(easyPayOrder.getTradeNo() != null ? easyPayOrder.getTradeNo() : easyPayOrder.getOutTradeNo())
|
||||
.setProtocol(NoticeProtocolEnum.EASY_PAY)
|
||||
.setContentMode(NoticeContentModeEnum.REF)
|
||||
.setContentOrRef(content)
|
||||
.setProtocolNotifyUrl(easyPayOrder.getNotifyUrl()));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user