wlzboy
5 天以前 06a17c236d4cb9b8da75fce43af938cb7ea510bf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
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);
    }
}