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 callbacks = applicationContext.getBeansOfType(PaymentCallback.class); if (callbacks.isEmpty()) { log.info("未找到PaymentCallback接口实现,跳过接口回调"); return; } for (Map.Entry 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 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); } }