Commit f3db296ca46698c3171b15d6b739976e4b9d9837

Authored by shaofan
1 parent 2449a3c0

完善订单Webhook事件通知:按订单应用定向回调,新增签名与失败重试机制,并补充后台指派/转单通知触发

src/main/java/com/diligrp/rider/service/WebhookService.java
1 1 package com.diligrp.rider.service;
2 2  
  3 +import com.diligrp.rider.entity.Orders;
  4 +
3 5 /**
4 6 * Webhook 推送服务
5 7 * 订单状态变更时调用此服务通知接入方
... ... @@ -13,6 +15,8 @@ public interface WebhookService {
13 15 */
14 16 void send(String event, Long bizId, String payload);
15 17  
  18 + void sendOrderEvent(Orders order, String event, String payload);
  19 +
16 20 /** 重试失败的 Webhook */
17 21 void retry(Long logId);
18 22 }
... ...
src/main/java/com/diligrp/rider/service/impl/AdminRiderServiceImpl.java
... ... @@ -17,8 +17,11 @@ import com.diligrp.rider.mapper.*;
17 17 import com.diligrp.rider.service.AdminRiderService;
18 18 import com.diligrp.rider.service.DeliveryOrderService;
19 19 import com.diligrp.rider.service.RiderHoldLimitService;
  20 +import com.diligrp.rider.service.WebhookService;
20 21 import com.diligrp.rider.vo.DeliveryOrderCreateVO;
  22 +import com.fasterxml.jackson.databind.ObjectMapper;
21 23 import lombok.RequiredArgsConstructor;
  24 +import lombok.extern.slf4j.Slf4j;
22 25 import org.springframework.stereotype.Service;
23 26 import org.springframework.transaction.annotation.Transactional;
24 27 import org.springframework.util.DigestUtils;
... ... @@ -31,6 +34,7 @@ import java.util.Map;
31 34 import java.util.Set;
32 35 import java.util.stream.Collectors;
33 36  
  37 +@Slf4j
34 38 @Service
35 39 @RequiredArgsConstructor
36 40 public class AdminRiderServiceImpl implements AdminRiderService {
... ... @@ -42,6 +46,8 @@ public class AdminRiderServiceImpl implements AdminRiderService {
42 46 private final AdminScopeGuard adminScopeGuard;
43 47 private final DeliveryOrderService deliveryOrderService;
44 48 private final RiderHoldLimitService riderHoldLimitService;
  49 + private final WebhookService webhookService;
  50 + private final ObjectMapper objectMapper;
45 51  
46 52 @Override
47 53 public void add(AdminRiderAddDTO dto, Long cityId) {
... ... @@ -221,6 +227,7 @@ public class AdminRiderServiceImpl implements AdminRiderService {
221 227 }
222 228 int updated = ordersMapper.update(null, wrapper);
223 229 if (updated == 0) throw new BizException("指派失败,请重试");
  230 + notifyOrderEvent(orderId, "order.dispatched");
224 231 }
225 232  
226 233 @Override
... ... @@ -248,6 +255,9 @@ public class AdminRiderServiceImpl implements AdminRiderService {
248 255 }
249 256 int updated = ordersMapper.update(null, wrapper);
250 257 if (updated == 0) throw new BizException("操作失败,请重试");
  258 + if (trans == 1) {
  259 + notifyOrderEvent(orderId, "order.transferred");
  260 + }
251 261 }
252 262  
253 263 @Override
... ... @@ -266,6 +276,24 @@ public class AdminRiderServiceImpl implements AdminRiderService {
266 276 deliveryOrderService.cancelByAdmin(order);
267 277 }
268 278  
  279 + private void notifyOrderEvent(Long orderId, String event) {
  280 + try {
  281 + Orders order = ordersMapper.selectById(orderId);
  282 + if (order == null || order.getAppKey() == null || order.getAppKey().isBlank()) return;
  283 + Map<String, Object> payload = new HashMap<>();
  284 + payload.put("event", event);
  285 + payload.put("outOrderNo", order.getOutOrderNo());
  286 + payload.put("deliveryOrderId", order.getId());
  287 + payload.put("orderNo", order.getOrderNo());
  288 + payload.put("status", order.getStatus());
  289 + payload.put("riderId", order.getRiderId());
  290 + payload.put("timestamp", System.currentTimeMillis() / 1000);
  291 + webhookService.sendOrderEvent(order, event, objectMapper.writeValueAsString(payload));
  292 + } catch (Exception e) {
  293 + log.warn("后台订单事件通知失败 orderId={} event={}", orderId, event, e);
  294 + }
  295 + }
  296 +
269 297 private String encryptPass(String pass) {
270 298 return DigestUtils.md5DigestAsHex(pass.getBytes(StandardCharsets.UTF_8));
271 299 }
... ...
src/main/java/com/diligrp/rider/service/impl/DeliveryOrderServiceImpl.java
... ... @@ -303,7 +303,7 @@ public class DeliveryOrderServiceImpl implements DeliveryOrderService {
303 303 payload.put("status", order.getStatus());
304 304 payload.put("timestamp", System.currentTimeMillis() / 1000);
305 305 String json = objectMapper.writeValueAsString(payload);
306   - webhookService.send(event, order.getId(), json);
  306 + webhookService.sendOrderEvent(order, event, json);
307 307 } catch (Exception e) {
308 308 log.warn("通知回调失败 orderId={}", order.getId(), e);
309 309 }
... ...
src/main/java/com/diligrp/rider/service/impl/DispatchServiceImpl.java
... ... @@ -346,7 +346,7 @@ public class DispatchServiceImpl implements DispatchService {
346 346 payload.put("riderId", order.getRiderId());
347 347 payload.put("dispatchRiderId", order.getDispatchRiderId());
348 348 payload.put("timestamp", System.currentTimeMillis() / 1000);
349   - webhookService.send(event, orderId, objectMapper.writeValueAsString(payload));
  349 + webhookService.sendOrderEvent(order, event, objectMapper.writeValueAsString(payload));
350 350 } catch (Exception e) {
351 351 log.warn("派单事件通知失败 orderId={} event={}", orderId, event, e);
352 352 }
... ...
src/main/java/com/diligrp/rider/service/impl/RefundServiceImpl.java
... ... @@ -70,6 +70,7 @@ public class RefundServiceImpl implements RefundService {
70 70 ordersMapper.update(null, new LambdaUpdateWrapper<Orders>()
71 71 .eq(Orders::getId, orderId)
72 72 .set(Orders::getStatus, 7));
  73 + order.setStatus(7);
73 74  
74 75 // 写退款记录
75 76 OrderRefundRecord record = new OrderRefundRecord();
... ... @@ -115,12 +116,14 @@ public class RefundServiceImpl implements RefundService {
115 116 ordersMapper.update(null, new LambdaUpdateWrapper<Orders>()
116 117 .eq(Orders::getId, record.getOid())
117 118 .set(Orders::getStatus, 8));
  119 + order.setStatus(8);
118 120 notifyRefundEvent(order, "order.refund_success");
119 121 } else {
120 122 // 退款拒绝 → 订单状态改为退款拒绝
121 123 ordersMapper.update(null, new LambdaUpdateWrapper<Orders>()
122 124 .eq(Orders::getId, record.getOid())
123 125 .set(Orders::getStatus, 9));
  126 + order.setStatus(9);
124 127 notifyRefundEvent(order, "order.refund_reject");
125 128 }
126 129 }
... ... @@ -148,7 +151,7 @@ public class RefundServiceImpl implements RefundService {
148 151 payload.put("orderNo", order.getOrderNo());
149 152 payload.put("status", order.getStatus());
150 153 payload.put("timestamp", System.currentTimeMillis() / 1000);
151   - webhookService.send(event, order.getId(), objectMapper.writeValueAsString(payload));
  154 + webhookService.sendOrderEvent(order, event, objectMapper.writeValueAsString(payload));
152 155 } catch (Exception e) {
153 156 log.warn("退款事件通知失败 orderId={}", order.getId(), e);
154 157 }
... ...
src/main/java/com/diligrp/rider/service/impl/RiderOrderServiceImpl.java
... ... @@ -256,7 +256,7 @@ public class RiderOrderServiceImpl implements RiderOrderService {
256 256 payload.put("status", order.getStatus());
257 257 payload.put("riderId", order.getRiderId());
258 258 payload.put("timestamp", System.currentTimeMillis() / 1000);
259   - webhookService.send(event, orderId, objectMapper.writeValueAsString(payload));
  259 + webhookService.sendOrderEvent(order, event, objectMapper.writeValueAsString(payload));
260 260 } catch (Exception e) {
261 261 log.warn("订单事件通知失败 orderId={} event={}", orderId, event, e);
262 262 }
... ... @@ -414,7 +414,7 @@ public class RiderOrderServiceImpl implements RiderOrderService {
414 414  
415 415 if (order.getStatus() == 3) {
416 416 // status=3(已接单,未取货):直接回抢单池,无需审批
417   - ordersMapper.update(null, new LambdaUpdateWrapper<Orders>()
  417 + int updated = ordersMapper.update(null, new LambdaUpdateWrapper<Orders>()
418 418 .eq(Orders::getId, orderId)
419 419 .eq(Orders::getRiderId, riderId)
420 420 .eq(Orders::getStatus, 3)
... ... @@ -426,6 +426,8 @@ public class RiderOrderServiceImpl implements RiderOrderService {
426 426 .set(Orders::getRiderIncome, BigDecimal.ZERO)
427 427 .set(Orders::getSubstationIncome, BigDecimal.ZERO)
428 428 .set(Orders::getTransTime, now));
  429 + if (updated == 0) throw new BizException(980, "操作失败");
  430 + notifyOrderEvent(orderId, "order.transferred");
429 431 } else {
430 432 // status=4(取货后配送中):需分站审批,设为申请中
431 433 ordersMapper.update(null, new LambdaUpdateWrapper<Orders>()
... ...
src/main/java/com/diligrp/rider/service/impl/WebhookServiceImpl.java
... ... @@ -3,13 +3,17 @@ package com.diligrp.rider.service.impl;
3 3 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4 4 import com.fasterxml.jackson.databind.ObjectMapper;
5 5 import com.diligrp.rider.entity.OpenApp;
  6 +import com.diligrp.rider.entity.Orders;
6 7 import com.diligrp.rider.entity.WebhookLog;
7 8 import com.diligrp.rider.mapper.OpenAppMapper;
  9 +import com.diligrp.rider.mapper.OrdersMapper;
8 10 import com.diligrp.rider.mapper.WebhookLogMapper;
9 11 import com.diligrp.rider.service.WebhookService;
  12 +import com.diligrp.rider.util.SignUtil;
10 13 import lombok.RequiredArgsConstructor;
11 14 import lombok.extern.slf4j.Slf4j;
12 15 import org.springframework.scheduling.annotation.Async;
  16 +import org.springframework.scheduling.annotation.Scheduled;
13 17 import org.springframework.stereotype.Service;
14 18  
15 19 import java.net.URI;
... ... @@ -18,6 +22,7 @@ import java.net.http.HttpRequest;
18 22 import java.net.http.HttpResponse;
19 23 import java.time.Duration;
20 24 import java.util.List;
  25 +import java.util.UUID;
21 26  
22 27 @Slf4j
23 28 @Service
... ... @@ -25,6 +30,7 @@ import java.util.List;
25 30 public class WebhookServiceImpl implements WebhookService {
26 31  
27 32 private final OpenAppMapper openAppMapper;
  33 + private final OrdersMapper ordersMapper;
28 34 private final WebhookLogMapper webhookLogMapper;
29 35 private final ObjectMapper objectMapper;
30 36  
... ... @@ -35,47 +41,102 @@ public class WebhookServiceImpl implements WebhookService {
35 41 @Override
36 42 @Async
37 43 public void send(String event, Long bizId, String payload) {
38   - // 查找订阅该事件的所有应用
39   - List<OpenApp> apps = openAppMapper.selectList(
40   - new LambdaQueryWrapper<OpenApp>().eq(OpenApp::getStatus, 1));
41   -
42   - for (OpenApp app : apps) {
43   - if (app.getWebhookUrl() == null || app.getWebhookUrl().isBlank()) continue;
44   - if (!isSubscribed(app.getWebhookEvents(), event)) continue;
45   - doSend(app, event, bizId, payload, 0);
  44 + Orders order = ordersMapper.selectById(bizId);
  45 + if (order == null) {
  46 + log.warn("Webhook 发送跳过,订单不存在 bizId={} event={}", bizId, event);
  47 + return;
46 48 }
  49 + sendOrderEvent(order, event, payload);
  50 + }
  51 +
  52 + @Override
  53 + @Async
  54 + public void sendOrderEvent(Orders order, String event, String payload) {
  55 + if (order == null || order.getAppKey() == null || order.getAppKey().isBlank()) return;
  56 + OpenApp app = openAppMapper.selectOne(new LambdaQueryWrapper<OpenApp>()
  57 + .eq(OpenApp::getAppKey, order.getAppKey())
  58 + .eq(OpenApp::getStatus, 1)
  59 + .last("LIMIT 1"));
  60 + if (app == null) return;
  61 + if (!isSubscribed(app.getWebhookEvents(), event)) return;
  62 +
  63 + String url = order.getCallbackUrl();
  64 + if (url == null || url.isBlank()) {
  65 + url = app.getWebhookUrl();
  66 + }
  67 + if (url == null || url.isBlank()) return;
  68 +
  69 + doSend(app, url, event, order.getId(), payload, 0);
47 70 }
48 71  
49 72 @Override
50 73 public void retry(Long logId) {
51 74 WebhookLog log = webhookLogMapper.selectById(logId);
52 75 if (log == null || log.getStatus() == 1) return;
53   - OpenApp app = openAppMapper.selectById(log.getAppId());
  76 + retry(log);
  77 + }
  78 +
  79 + @Scheduled(fixedDelay = 300_000)
  80 + public void retryFailedWebhooks() {
  81 + List<WebhookLog> logs = webhookLogMapper.selectList(new LambdaQueryWrapper<WebhookLog>()
  82 + .eq(WebhookLog::getStatus, 0)
  83 + .lt(WebhookLog::getRetryCount, 5)
  84 + .orderByAsc(WebhookLog::getCreateTime)
  85 + .last("LIMIT 20"));
  86 + for (WebhookLog log : logs) {
  87 + retry(log);
  88 + }
  89 + }
  90 +
  91 + private void retry(WebhookLog webhookLog) {
  92 + OpenApp app = openAppMapper.selectById(webhookLog.getAppId());
54 93 if (app == null) return;
55   - doSend(app, log.getEvent(), log.getBizId(), log.getPayload(), log.getRetryCount() + 1);
  94 + String url = webhookLog.getUrl();
  95 + if (url == null || url.isBlank()) {
  96 + url = app.getWebhookUrl();
  97 + }
  98 + if (url == null || url.isBlank()) return;
  99 + SendResult result = sendHttp(app, url, webhookLog.getEvent(), webhookLog.getPayload());
  100 + webhookLog.setUrl(url);
  101 + webhookLog.setResponseCode(result.responseCode());
  102 + webhookLog.setResponseBody(trimResponseBody(result.responseBody()));
  103 + webhookLog.setStatus(result.status());
  104 + webhookLog.setRetryCount((webhookLog.getRetryCount() == null ? 0 : webhookLog.getRetryCount()) + 1);
  105 + webhookLogMapper.updateById(webhookLog);
56 106 }
57 107  
58   - private void doSend(OpenApp app, String event, Long bizId, String payload, int retryCount) {
  108 + private void doSend(OpenApp app, String url, String event, Long bizId, String payload, int retryCount) {
  109 + SendResult result = sendHttp(app, url, event, payload);
59 110 WebhookLog webhookLog = new WebhookLog();
60 111 webhookLog.setAppId(app.getId());
61 112 webhookLog.setEvent(event);
62 113 webhookLog.setBizId(bizId);
63   - webhookLog.setUrl(app.getWebhookUrl());
  114 + webhookLog.setUrl(url);
64 115 webhookLog.setPayload(payload);
65 116 webhookLog.setRetryCount(retryCount);
66 117 webhookLog.setCreateTime(System.currentTimeMillis() / 1000);
  118 + webhookLog.setResponseCode(result.responseCode());
  119 + webhookLog.setResponseBody(trimResponseBody(result.responseBody()));
  120 + webhookLog.setStatus(result.status());
  121 + webhookLogMapper.insert(webhookLog);
  122 + }
67 123  
  124 + private SendResult sendHttp(OpenApp app, String url, String event, String payload) {
68 125 int responseCode = 0;
69 126 String responseBody = "";
70 127 int status = 0;
71 128  
72 129 try {
  130 + String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
  131 + String nonce = UUID.randomUUID().toString().replace("-", "");
73 132 HttpRequest request = HttpRequest.newBuilder()
74   - .uri(URI.create(app.getWebhookUrl()))
  133 + .uri(URI.create(url))
75 134 .header("Content-Type", "application/json")
76 135 .header("X-App-Key", app.getAppKey())
77 136 .header("X-Event", event)
78   - .header("X-Timestamp", String.valueOf(System.currentTimeMillis() / 1000))
  137 + .header("X-Timestamp", timestamp)
  138 + .header("X-Nonce", nonce)
  139 + .header("X-Sign", SignUtil.sign(app.getAppKey(), timestamp, nonce, app.getAppSecret()))
79 140 .POST(HttpRequest.BodyPublishers.ofString(payload))
80 141 .timeout(Duration.ofSeconds(10))
81 142 .build();
... ... @@ -83,19 +144,22 @@ public class WebhookServiceImpl implements WebhookService {
83 144 HttpResponse<String> response = HTTP_CLIENT.send(request,
84 145 HttpResponse.BodyHandlers.ofString());
85 146 responseCode = response.statusCode();
86   - responseBody = response.body();
  147 + responseBody = response.body() == null ? "" : response.body();
87 148 if (responseCode == 200) status = 1;
88 149 } catch (Exception e) {
89 150 log.warn("Webhook 发送失败 appId={} event={} err={}", app.getId(), event, e.getMessage());
90   - responseBody = e.getMessage();
  151 + responseBody = e.getMessage() == null ? "" : e.getMessage();
91 152 }
  153 + return new SendResult(responseCode, responseBody, status);
  154 + }
92 155  
93   - webhookLog.setResponseCode(responseCode);
94   - webhookLog.setResponseBody(responseBody.length() > 500 ? responseBody.substring(0, 500) : responseBody);
95   - webhookLog.setStatus(status);
96   - webhookLogMapper.insert(webhookLog);
  156 + private String trimResponseBody(String responseBody) {
  157 + if (responseBody == null) return "";
  158 + return responseBody.length() > 500 ? responseBody.substring(0, 500) : responseBody;
97 159 }
98 160  
  161 + private record SendResult(int responseCode, String responseBody, int status) {}
  162 +
99 163 /** 检查应用是否订阅了某事件 */
100 164 private boolean isSubscribed(String webhookEvents, String event) {
101 165 if (webhookEvents == null || webhookEvents.isBlank()) return false;
... ...
src/main/java/com/diligrp/rider/task/OrderScheduleTask.java
... ... @@ -123,7 +123,7 @@ public class OrderScheduleTask {
123 123 payload.put("status", 10);
124 124 payload.put("reason", "超时无人接单,系统自动取消");
125 125 payload.put("timestamp", System.currentTimeMillis() / 1000);
126   - webhookService.send("order.cancelled", order.getId(),
  126 + webhookService.sendOrderEvent(order, "order.cancelled",
127 127 objectMapper.writeValueAsString(payload));
128 128 } catch (Exception e) {
129 129 log.warn("取消通知失败 orderId={}", order.getId(), e);
... ...