Commit f3db296ca46698c3171b15d6b739976e4b9d9837
1 parent
2449a3c0
完善订单Webhook事件通知:按订单应用定向回调,新增签名与失败重试机制,并补充后台指派/转单通知触发
Showing
8 changed files
with
127 additions
and
26 deletions
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); | ... | ... |