Commit f3ec2e0c6d225caaa8f17a95eeb2ee9355512476

Authored by shaofan
1 parent 548c125f

完善开放平台Webhook配置校验,新增支持事件列表接口

src/main/java/com/diligrp/rider/controller/PlatformOpenAppController.java
@@ -16,6 +16,7 @@ import lombok.RequiredArgsConstructor; @@ -16,6 +16,7 @@ import lombok.RequiredArgsConstructor;
16 import org.springframework.web.bind.annotation.*; 16 import org.springframework.web.bind.annotation.*;
17 17
18 import java.util.List; 18 import java.util.List;
  19 +import java.util.Map;
19 20
20 /** 21 /**
21 * 平台后台:开放平台应用管理 22 * 平台后台:开放平台应用管理
@@ -74,12 +75,18 @@ public class PlatformOpenAppController { @@ -74,12 +75,18 @@ public class PlatformOpenAppController {
74 /** 更新 Webhook 配置 */ 75 /** 更新 Webhook 配置 */
75 @PostMapping("/updateWebhook") 76 @PostMapping("/updateWebhook")
76 public Result<Void> updateWebhook(@RequestParam Long appId, 77 public Result<Void> updateWebhook(@RequestParam Long appId,
77 - @RequestParam String webhookUrl,  
78 - @RequestParam String webhookEvents) { 78 + @RequestParam(required = false) String webhookUrl,
  79 + @RequestParam(required = false) String webhookEvents) {
79 openAppService.updateWebhook(appId, webhookUrl, webhookEvents); 80 openAppService.updateWebhook(appId, webhookUrl, webhookEvents);
80 return Result.success(); 81 return Result.success();
81 } 82 }
82 83
  84 + /** 支持订阅的 Webhook 事件 */
  85 + @GetMapping("/webhook/events")
  86 + public Result<List<Map<String, Object>>> webhookEvents() {
  87 + return Result.success(openAppService.supportedWebhookEvents());
  88 + }
  89 +
83 /** Webhook 推送日志 */ 90 /** Webhook 推送日志 */
84 @GetMapping("/webhook/logs") 91 @GetMapping("/webhook/logs")
85 public Result<List<WebhookLog>> webhookLogs(@RequestParam Long appId, 92 public Result<List<WebhookLog>> webhookLogs(@RequestParam Long appId,
src/main/java/com/diligrp/rider/service/OpenAppService.java
@@ -3,6 +3,7 @@ package com.diligrp.rider.service; @@ -3,6 +3,7 @@ package com.diligrp.rider.service;
3 import com.diligrp.rider.entity.OpenApp; 3 import com.diligrp.rider.entity.OpenApp;
4 4
5 import java.util.List; 5 import java.util.List;
  6 +import java.util.Map;
6 7
7 public interface OpenAppService { 8 public interface OpenAppService {
8 /** 创建应用,自动生成 AppKey/AppSecret,cityId 为必填(租户隔离) */ 9 /** 创建应用,自动生成 AppKey/AppSecret,cityId 为必填(租户隔离) */
@@ -15,6 +16,8 @@ public interface OpenAppService { @@ -15,6 +16,8 @@ public interface OpenAppService {
15 void setStatus(Long appId, int status); 16 void setStatus(Long appId, int status);
16 /** 更新 Webhook 配置 */ 17 /** 更新 Webhook 配置 */
17 void updateWebhook(Long appId, String webhookUrl, String webhookEvents); 18 void updateWebhook(Long appId, String webhookUrl, String webhookEvents);
  19 + /** 支持的 Webhook 事件列表 */
  20 + List<Map<String, Object>> supportedWebhookEvents();
18 /** 根据 AppKey 获取应用(用于签名验证) */ 21 /** 根据 AppKey 获取应用(用于签名验证) */
19 OpenApp getByAppKey(String appKey); 22 OpenApp getByAppKey(String appKey);
20 /** 验证签名 */ 23 /** 验证签名 */
src/main/java/com/diligrp/rider/service/impl/OpenAppServiceImpl.java
@@ -2,6 +2,9 @@ package com.diligrp.rider.service.impl; @@ -2,6 +2,9 @@ package com.diligrp.rider.service.impl;
2 2
3 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 3 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; 4 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
  5 +import com.fasterxml.jackson.core.JsonProcessingException;
  6 +import com.fasterxml.jackson.core.type.TypeReference;
  7 +import com.fasterxml.jackson.databind.ObjectMapper;
5 import com.diligrp.rider.common.exception.BizException; 8 import com.diligrp.rider.common.exception.BizException;
6 import com.diligrp.rider.entity.OpenApp; 9 import com.diligrp.rider.entity.OpenApp;
7 import com.diligrp.rider.mapper.OpenAppMapper; 10 import com.diligrp.rider.mapper.OpenAppMapper;
@@ -10,17 +13,50 @@ import com.diligrp.rider.util.SignUtil; @@ -10,17 +13,50 @@ import com.diligrp.rider.util.SignUtil;
10 import lombok.RequiredArgsConstructor; 13 import lombok.RequiredArgsConstructor;
11 import org.springframework.stereotype.Service; 14 import org.springframework.stereotype.Service;
12 15
  16 +import java.net.URI;
  17 +import java.net.URISyntaxException;
  18 +import java.util.ArrayList;
  19 +import java.util.LinkedHashMap;
  20 +import java.util.LinkedHashSet;
13 import java.util.List; 21 import java.util.List;
  22 +import java.util.Map;
  23 +import java.util.Set;
14 24
15 @Service 25 @Service
16 @RequiredArgsConstructor 26 @RequiredArgsConstructor
17 public class OpenAppServiceImpl implements OpenAppService { 27 public class OpenAppServiceImpl implements OpenAppService {
18 28
19 private final OpenAppMapper openAppMapper; 29 private final OpenAppMapper openAppMapper;
  30 + private final ObjectMapper objectMapper;
  31 +
  32 + private static final List<Map<String, Object>> SUPPORTED_WEBHOOK_EVENTS = List.of(
  33 + event("订单创建", "order.created", 2, "外部系统推单成功,订单进入待接单"),
  34 + event("已派单", "order.dispatched", 3, "系统自动派单或后台指派骑手"),
  35 + event("骑手接单", "order.accepted", 3, "骑手主动抢单成功"),
  36 + event("骑手到店", "order.arrived_shop", 3, "骑手到达取货点"),
  37 + event("骑手取件", "order.picking", 4, "骑手取件并开始配送"),
  38 + event("订单完成", "order.completed", 6, "订单配送完成"),
  39 + event("订单取消", "order.cancelled", 10, "订单未接单前取消"),
  40 + event("订单转单", "order.transferred", 2, "转单审核通过,订单重新待接单")
  41 + );
  42 +
  43 + private static final Set<String> SUPPORTED_EVENT_VALUES = Set.of(
  44 + "*",
  45 + "order.created",
  46 + "order.dispatched",
  47 + "order.accepted",
  48 + "order.arrived_shop",
  49 + "order.picking",
  50 + "order.completed",
  51 + "order.cancelled",
  52 + "order.transferred"
  53 + );
20 54
21 @Override 55 @Override
22 public OpenApp create(String appName, Long cityId, Long storeId, String webhookUrl, String webhookEvents, String remark) { 56 public OpenApp create(String appName, Long cityId, Long storeId, String webhookUrl, String webhookEvents, String remark) {
23 if (cityId == null || cityId < 1) throw new BizException("请选择关联城市/租户"); 57 if (cityId == null || cityId < 1) throw new BizException("请选择关联城市/租户");
  58 + String normalizedUrl = normalizeWebhookUrl(webhookUrl);
  59 + String normalizedEvents = normalizeWebhookEvents(webhookEvents);
24 OpenApp app = new OpenApp(); 60 OpenApp app = new OpenApp();
25 app.setAppName(appName); 61 app.setAppName(appName);
26 app.setAppKey(SignUtil.generateAppKey()); 62 app.setAppKey(SignUtil.generateAppKey());
@@ -28,8 +64,8 @@ public class OpenAppServiceImpl implements OpenAppService { @@ -28,8 +64,8 @@ public class OpenAppServiceImpl implements OpenAppService {
28 app.setCityId(cityId); 64 app.setCityId(cityId);
29 app.setStoreId(storeId != null ? storeId : 0L); 65 app.setStoreId(storeId != null ? storeId : 0L);
30 app.setStatus(1); 66 app.setStatus(1);
31 - app.setWebhookUrl(webhookUrl);  
32 - app.setWebhookEvents(webhookEvents); 67 + app.setWebhookUrl(normalizedUrl);
  68 + app.setWebhookEvents(normalizedEvents);
33 app.setRemark(remark); 69 app.setRemark(remark);
34 app.setCreateTime(System.currentTimeMillis() / 1000); 70 app.setCreateTime(System.currentTimeMillis() / 1000);
35 openAppMapper.insert(app); 71 openAppMapper.insert(app);
@@ -64,10 +100,19 @@ public class OpenAppServiceImpl implements OpenAppService { @@ -64,10 +100,19 @@ public class OpenAppServiceImpl implements OpenAppService {
64 100
65 @Override 101 @Override
66 public void updateWebhook(Long appId, String webhookUrl, String webhookEvents) { 102 public void updateWebhook(Long appId, String webhookUrl, String webhookEvents) {
  103 + OpenApp app = openAppMapper.selectById(appId);
  104 + if (app == null) throw new BizException("应用不存在");
  105 + String normalizedUrl = normalizeWebhookUrl(webhookUrl);
  106 + String normalizedEvents = normalizeWebhookEvents(webhookEvents);
67 openAppMapper.update(null, new LambdaUpdateWrapper<OpenApp>() 107 openAppMapper.update(null, new LambdaUpdateWrapper<OpenApp>()
68 .eq(OpenApp::getId, appId) 108 .eq(OpenApp::getId, appId)
69 - .set(OpenApp::getWebhookUrl, webhookUrl)  
70 - .set(OpenApp::getWebhookEvents, webhookEvents)); 109 + .set(OpenApp::getWebhookUrl, normalizedUrl)
  110 + .set(OpenApp::getWebhookEvents, normalizedEvents));
  111 + }
  112 +
  113 + @Override
  114 + public List<Map<String, Object>> supportedWebhookEvents() {
  115 + return SUPPORTED_WEBHOOK_EVENTS;
71 } 116 }
72 117
73 @Override 118 @Override
@@ -83,4 +128,70 @@ public class OpenAppServiceImpl implements OpenAppService { @@ -83,4 +128,70 @@ public class OpenAppServiceImpl implements OpenAppService {
83 if (app == null || app.getStatus() != 1) return false; 128 if (app == null || app.getStatus() != 1) return false;
84 return SignUtil.verify(appKey, timestamp, nonce, sign, app.getAppSecret()); 129 return SignUtil.verify(appKey, timestamp, nonce, sign, app.getAppSecret());
85 } 130 }
  131 +
  132 + private String normalizeWebhookUrl(String webhookUrl) {
  133 + if (webhookUrl == null || webhookUrl.isBlank()) {
  134 + return "";
  135 + }
  136 + String url = webhookUrl.trim();
  137 + try {
  138 + URI uri = new URI(url);
  139 + String scheme = uri.getScheme();
  140 + if (scheme == null || !("http".equalsIgnoreCase(scheme) || "https".equalsIgnoreCase(scheme))
  141 + || uri.getHost() == null || uri.getHost().isBlank()) {
  142 + throw new BizException("Webhook 回调地址必须是有效的 http/https URL");
  143 + }
  144 + } catch (URISyntaxException e) {
  145 + throw new BizException("Webhook 回调地址格式错误");
  146 + }
  147 + return url;
  148 + }
  149 +
  150 + private String normalizeWebhookEvents(String webhookEvents) {
  151 + if (webhookEvents == null || webhookEvents.isBlank()) {
  152 + return "[]";
  153 + }
  154 + List<String> events;
  155 + try {
  156 + events = objectMapper.readValue(webhookEvents, new TypeReference<List<String>>() {});
  157 + } catch (Exception e) {
  158 + throw new BizException("订阅事件必须是 JSON 字符串数组");
  159 + }
  160 + if (events == null || events.isEmpty()) {
  161 + return "[]";
  162 + }
  163 +
  164 + LinkedHashSet<String> normalized = new LinkedHashSet<>();
  165 + for (String event : events) {
  166 + if (event == null || event.isBlank()) {
  167 + continue;
  168 + }
  169 + String value = event.trim();
  170 + if (!SUPPORTED_EVENT_VALUES.contains(value)) {
  171 + throw new BizException("不支持的 Webhook 事件:" + value);
  172 + }
  173 + normalized.add(value);
  174 + }
  175 + if (normalized.isEmpty()) {
  176 + return "[]";
  177 + }
  178 + if (normalized.contains("*")) {
  179 + normalized.clear();
  180 + normalized.add("*");
  181 + }
  182 + try {
  183 + return objectMapper.writeValueAsString(new ArrayList<>(normalized));
  184 + } catch (JsonProcessingException e) {
  185 + throw new BizException("订阅事件保存失败");
  186 + }
  187 + }
  188 +
  189 + private static Map<String, Object> event(String label, String value, Integer status, String description) {
  190 + Map<String, Object> item = new LinkedHashMap<>();
  191 + item.put("label", label);
  192 + item.put("value", value);
  193 + item.put("status", status);
  194 + item.put("description", description);
  195 + return item;
  196 + }
86 } 197 }