package com.ruoyi.payment.application.service;
|
|
import com.alibaba.fastjson.JSON;
|
import com.ruoyi.payment.domain.event.PaymentSuccessEvent;
|
import com.ruoyi.payment.domain.model.BizCallbackLog;
|
import com.ruoyi.payment.domain.model.PaymentOrder;
|
import com.ruoyi.payment.domain.model.PaymentTransaction;
|
import com.ruoyi.payment.infrastructure.config.BusinessCallbackConfig;
|
import com.ruoyi.payment.infrastructure.persistence.mapper.BizCallbackLogMapper;
|
import com.ruoyi.payment.infrastructure.util.SignUtil;
|
import com.ruoyi.payment.infrastructure.util.SnowflakeIdGenerator;
|
import com.ruoyi.payment.interfaces.callback.PaymentCallback;
|
import lombok.extern.slf4j.Slf4j;
|
import org.apache.http.client.methods.CloseableHttpResponse;
|
import org.apache.http.client.methods.HttpPost;
|
import org.apache.http.entity.StringEntity;
|
import org.apache.http.impl.client.CloseableHttpClient;
|
import org.apache.http.impl.client.HttpClients;
|
import org.apache.http.util.EntityUtils;
|
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.context.ApplicationContext;
|
import org.springframework.scheduling.annotation.Async;
|
import org.springframework.stereotype.Service;
|
import org.springframework.util.StringUtils;
|
|
import java.time.LocalDateTime;
|
import java.util.HashMap;
|
import java.util.Map;
|
import java.util.UUID;
|
|
/**
|
* 业务回调服务
|
*
|
* 支持三种回调方式:
|
* 1. Spring事件:发布 PaymentSuccessEvent 事件(推荐用于jar包集成)
|
* 2. 接口回调:调用所有实现了 PaymentCallback 接口的Bean(推荐用于jar包集成)
|
* 3. HTTP回调:向指定URL发送POST请求(用于独立部署或跨服务调用)
|
*
|
* @author ruoyi
|
*/
|
@Slf4j
|
@Service
|
public class BizCallbackService {
|
|
@Autowired
|
private BizCallbackLogMapper bizCallbackLogMapper;
|
|
@Autowired
|
private BusinessCallbackConfig businessCallbackConfig;
|
|
@Autowired
|
private ApplicationContext applicationContext;
|
|
/**
|
* 触发业务回调(异步)
|
*
|
* 优先级:
|
* 1. 发布Spring事件(同步,在同一事务中)
|
* 2. 调用PaymentCallback接口实现(同步,在同一事务中)
|
* 3. HTTP回调(异步,支持重试)
|
*/
|
@Async
|
public void triggerCallback(PaymentOrder order, PaymentTransaction transaction) {
|
log.info("触发业务回调,订单ID: {}, bizOrderId: {}", order.getId(), order.getBizOrderId());
|
|
// 方式1: 发布Spring事件(jar包集成时,主项目可以监听此事件)
|
publishPaymentEvent(order, transaction);
|
|
// 方式2: 调用PaymentCallback接口实现(jar包集成时,主项目实现此接口)
|
invokeCallbackInterface(order, transaction);
|
|
// 方式3: HTTP回调(独立部署时使用,需要配置callbackUrl)
|
if (StringUtils.hasText(order.getCallbackUrl())) {
|
executeHttpCallback(order, transaction);
|
} else {
|
log.info("未配置HTTP回调URL,跳过HTTP回调");
|
}
|
}
|
|
/**
|
* 发布支付成功事件
|
*/
|
private void publishPaymentEvent(PaymentOrder order, PaymentTransaction transaction) {
|
try {
|
PaymentSuccessEvent event = new PaymentSuccessEvent(this, order, transaction);
|
applicationContext.publishEvent(event);
|
log.info("发布支付成功事件,订单ID: {}", order.getId());
|
} catch (Exception e) {
|
log.error("发布支付事件失败", e);
|
}
|
}
|
|
/**
|
* 调用PaymentCallback接口实现
|
*/
|
private void invokeCallbackInterface(PaymentOrder order, PaymentTransaction transaction) {
|
try {
|
Map<String, PaymentCallback> callbacks = applicationContext.getBeansOfType(PaymentCallback.class);
|
if (callbacks.isEmpty()) {
|
log.info("未找到PaymentCallback接口实现,跳过接口回调");
|
return;
|
}
|
|
for (Map.Entry<String, PaymentCallback> entry : callbacks.entrySet()) {
|
try {
|
log.info("调用PaymentCallback: {}", entry.getKey());
|
entry.getValue().onPaymentSuccess(order, transaction);
|
} catch (Exception e) {
|
log.error("调用PaymentCallback失败: {}", entry.getKey(), e);
|
}
|
}
|
} catch (Exception e) {
|
log.error("调用PaymentCallback接口失败", e);
|
}
|
}
|
|
/**
|
* 执行HTTP回调
|
*/
|
private void executeHttpCallback(PaymentOrder order, PaymentTransaction transaction) {
|
log.info("执行HTTP回调,订单ID: {}, 回调URL: {}", order.getId(), order.getCallbackUrl());
|
|
// 构造回调参数
|
Map<String, Object> callbackData = new HashMap<>();
|
callbackData.put("tradeId", transaction.getId());
|
callbackData.put("orderId", order.getId());
|
callbackData.put("bizOrderId", order.getBizOrderId());
|
callbackData.put("channel", order.getChannel());
|
callbackData.put("amount", order.getAmount());
|
callbackData.put("currency", order.getCurrency());
|
callbackData.put("status", order.getStatus());
|
callbackData.put("channelTradeNo", order.getChannelTradeNo());
|
callbackData.put("paidAt", order.getPaidAt() != null ? order.getPaidAt().toString() : null);
|
callbackData.put("subject", order.getSubject());
|
callbackData.put("description", order.getDescription());
|
|
String payload = JSON.toJSONString(callbackData);
|
|
// 生成签名
|
String nonce = UUID.randomUUID().toString().replace("-", "");
|
String timestamp = String.valueOf(System.currentTimeMillis());
|
String signData = payload + nonce + timestamp;
|
String signature = SignUtil.hmacSha256(signData, businessCallbackConfig.getCallbackSignSecret());
|
|
// 记录回调日志
|
BizCallbackLog callbackLog = new BizCallbackLog();
|
callbackLog.setId(SnowflakeIdGenerator.generateId());
|
callbackLog.setOrderId(order.getId());
|
callbackLog.setTransactionId(transaction.getId());
|
callbackLog.setCallbackUrl(order.getCallbackUrl());
|
callbackLog.setPayload(payload);
|
callbackLog.setSuccess(false);
|
callbackLog.setRetryCount(0);
|
callbackLog.setCreatedAt(LocalDateTime.now());
|
bizCallbackLogMapper.insert(callbackLog);
|
|
// 执行HTTP回调
|
executeHttpRequest(callbackLog, payload, signature, nonce, timestamp, 0);
|
}
|
|
/**
|
* 执行HTTP回调请求
|
*/
|
private void executeHttpRequest(BizCallbackLog callbackLog, String payload, String signature,
|
String nonce, String timestamp, int retryCount) {
|
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
|
HttpPost httpPost = new HttpPost(callbackLog.getCallbackUrl());
|
|
// 设置请求头
|
httpPost.setHeader("Content-Type", "application/json");
|
httpPost.setHeader("X-Signature", signature);
|
httpPost.setHeader("X-Nonce", nonce);
|
httpPost.setHeader("X-Timestamp", timestamp);
|
|
// 设置请求体
|
httpPost.setEntity(new StringEntity(payload, "UTF-8"));
|
|
// 发送请求
|
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
|
int statusCode = response.getStatusLine().getStatusCode();
|
String responseBody = EntityUtils.toString(response.getEntity(), "UTF-8");
|
|
log.info("业务回调响应,状态码: {}, 响应: {}", statusCode, responseBody);
|
|
// 更新回调日志
|
callbackLog.setHttpStatus(statusCode);
|
callbackLog.setResponse(responseBody);
|
callbackLog.setRetryCount(retryCount);
|
callbackLog.setLastRetryAt(LocalDateTime.now());
|
|
if (statusCode >= 200 && statusCode < 300) {
|
callbackLog.setSuccess(true);
|
bizCallbackLogMapper.update(callbackLog);
|
log.info("业务回调成功,订单ID: {}", callbackLog.getOrderId());
|
} else {
|
callbackLog.setSuccess(false);
|
bizCallbackLogMapper.update(callbackLog);
|
|
// 重试逻辑
|
scheduleRetry(callbackLog, payload, signature, nonce, timestamp, retryCount);
|
}
|
}
|
} catch (Exception e) {
|
log.error("业务回调异常,订单ID: {}", callbackLog.getOrderId(), e);
|
|
// 更新失败记录
|
callbackLog.setSuccess(false);
|
callbackLog.setResponse("异常: " + e.getMessage());
|
callbackLog.setRetryCount(retryCount);
|
callbackLog.setLastRetryAt(LocalDateTime.now());
|
bizCallbackLogMapper.update(callbackLog);
|
|
// 重试逻辑
|
scheduleRetry(callbackLog, payload, signature, nonce, timestamp, retryCount);
|
}
|
}
|
|
/**
|
* 安排重试
|
*/
|
private void scheduleRetry(BizCallbackLog callbackLog, String payload, String signature,
|
String nonce, String timestamp, int retryCount) {
|
int maxRetry = businessCallbackConfig.getCallbackRetryMaxCount();
|
if (retryCount >= maxRetry) {
|
log.warn("业务回调重试次数已达上限,订单ID: {}", callbackLog.getOrderId());
|
return;
|
}
|
|
int[] intervals = businessCallbackConfig.getRetryIntervalsArray();
|
int nextRetry = retryCount + 1;
|
int delayMinutes = nextRetry < intervals.length ? intervals[nextRetry] : intervals[intervals.length - 1];
|
|
log.info("安排业务回调重试,订单ID: {}, 第{}次重试,延迟{}分钟",
|
callbackLog.getOrderId(), nextRetry, delayMinutes);
|
|
// TODO: 使用定时任务或延迟队列实现重试
|
// 这里简化处理,实际应使用 @Scheduled 或消息队列
|
}
|
|
/**
|
* 手工重发回调
|
*/
|
public void resendCallback(Long orderId) {
|
// TODO: 实现手工重发逻辑
|
log.info("手工重发业务回调,订单ID: {}", orderId);
|
}
|
}
|