You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
204 lines
7.1 KiB
204 lines
7.1 KiB
package com.wok.supportbot.service;
|
|
|
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
|
import com.wok.supportbot.common.SortUtils;
|
|
import com.wok.supportbot.dao.WebhookConfigMapper;
|
|
import com.wok.supportbot.entity.WebhookConfig;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
import org.springframework.scheduling.annotation.Async;
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.web.client.RestClient;
|
|
|
|
import java.util.*;
|
|
|
|
/**
|
|
* Webhook 管理服务
|
|
* 提供 Webhook 的增删改查及事件推送功能。
|
|
*/
|
|
@Slf4j
|
|
@Service
|
|
public class WebhookService {
|
|
|
|
@Autowired
|
|
private WebhookConfigMapper webhookConfigMapper;
|
|
|
|
/** Webhook 列表排序字段白名单(前端 colKey -> 数据库列名),防 SQL 注入 */
|
|
private static final Map<String, String> WEBHOOK_SORT_COLUMNS = Map.of(
|
|
"name", "name",
|
|
"url", "url",
|
|
"enabled", "enabled",
|
|
"createTime", "create_time");
|
|
|
|
/**
|
|
* 分页查询 Webhook 列表
|
|
*
|
|
* @param page 页码(从1开始)
|
|
* @param size 每页条数
|
|
* @return 包含 records/total/page/size/pages 的结果
|
|
*/
|
|
public Map<String, Object> listWebhooks(int page, int size, String sortField, String sortOrder) {
|
|
QueryWrapper<WebhookConfig> countWrapper = new QueryWrapper<>();
|
|
Long total = webhookConfigMapper.selectCount(countWrapper);
|
|
|
|
QueryWrapper<WebhookConfig> listWrapper = new QueryWrapper<>();
|
|
String sortColumn = SortUtils.resolveColumn(sortField, WEBHOOK_SORT_COLUMNS, "create_time");
|
|
if (SortUtils.isAscending(sortOrder)) {
|
|
listWrapper.orderByAsc(sortColumn);
|
|
} else {
|
|
listWrapper.orderByDesc(sortColumn);
|
|
}
|
|
listWrapper.last("LIMIT " + size + " OFFSET " + (long) (page - 1) * size);
|
|
List<WebhookConfig> records = webhookConfigMapper.selectList(listWrapper);
|
|
|
|
Map<String, Object> result = new HashMap<>();
|
|
result.put("records", records);
|
|
result.put("total", total);
|
|
result.put("page", page);
|
|
result.put("size", size);
|
|
result.put("pages", (total + size - 1) / size);
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* 创建 Webhook
|
|
*
|
|
* @param userId 所属用户ID
|
|
* @param name 名称
|
|
* @param url 回调 URL
|
|
* @param events 订阅的事件列表
|
|
* @return 新创建的 WebhookConfig
|
|
*/
|
|
public WebhookConfig createWebhook(Long userId, String name, String url, List<String> events) {
|
|
// 将事件列表包装为 Map 存入 JSONB
|
|
Map<String, Object> eventsMap = new HashMap<>();
|
|
eventsMap.put("list", events != null ? events : new ArrayList<>());
|
|
|
|
WebhookConfig config = WebhookConfig.builder()
|
|
.userId(userId)
|
|
.name(name)
|
|
.url(url)
|
|
.events(eventsMap)
|
|
.enabled(true)
|
|
.secret(UUID.randomUUID().toString().replace("-", ""))
|
|
.build();
|
|
|
|
webhookConfigMapper.insert(config);
|
|
log.info("创建 Webhook: name={}, url={}", name, url);
|
|
return config;
|
|
}
|
|
|
|
/**
|
|
* 更新 Webhook
|
|
*
|
|
* @param id Webhook ID
|
|
* @param name 名称(null 则不更新)
|
|
* @param url 回调 URL(null 则不更新)
|
|
* @param events 订阅事件列表(null 则不更新)
|
|
* @param enabled 是否启用(null 则不更新)
|
|
* @return 更新后的 WebhookConfig
|
|
*/
|
|
public WebhookConfig updateWebhook(Long id, String name, String url, List<String> events, Boolean enabled) {
|
|
WebhookConfig existing = webhookConfigMapper.selectById(id);
|
|
if (existing == null) {
|
|
throw new IllegalArgumentException("Webhook 不存在,ID:" + id);
|
|
}
|
|
|
|
if (name != null) {
|
|
existing.setName(name);
|
|
}
|
|
if (url != null) {
|
|
existing.setUrl(url);
|
|
}
|
|
if (events != null) {
|
|
Map<String, Object> eventsMap = new HashMap<>();
|
|
eventsMap.put("list", events);
|
|
existing.setEvents(eventsMap);
|
|
}
|
|
if (enabled != null) {
|
|
existing.setEnabled(enabled);
|
|
}
|
|
|
|
webhookConfigMapper.updateById(existing);
|
|
log.info("更新 Webhook: id={}", id);
|
|
return webhookConfigMapper.selectById(id);
|
|
}
|
|
|
|
/**
|
|
* 逻辑删除 Webhook
|
|
*
|
|
* @param id Webhook ID
|
|
*/
|
|
public void deleteWebhook(Long id) {
|
|
WebhookConfig existing = webhookConfigMapper.selectById(id);
|
|
if (existing == null) {
|
|
throw new IllegalArgumentException("Webhook 不存在,ID:" + id);
|
|
}
|
|
webhookConfigMapper.deleteById(id);
|
|
log.info("删除 Webhook: id={}, name={}", id, existing.getName());
|
|
}
|
|
|
|
/**
|
|
* 异步触发事件推送
|
|
* 查询所有匹配事件的已启用 Webhook,逐个 POST 推送。
|
|
* 失败只记录日志,不抛异常。
|
|
*
|
|
* @param event 事件名称,如 "document.processed" / "feedback.negative"
|
|
* @param payload 事件数据
|
|
*/
|
|
@Async
|
|
public void triggerEvent(String event, Map<String, Object> payload) {
|
|
// 查询所有已启用的 Webhook
|
|
QueryWrapper<WebhookConfig> wrapper = new QueryWrapper<>();
|
|
wrapper.eq("enabled", true);
|
|
List<WebhookConfig> allWebhooks = webhookConfigMapper.selectList(wrapper);
|
|
|
|
// 过滤出订阅了该事件的 Webhook
|
|
List<WebhookConfig> matched = allWebhooks.stream()
|
|
.filter(wh -> isEventSubscribed(wh, event))
|
|
.toList();
|
|
|
|
if (matched.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
// 构建推送 payload
|
|
Map<String, Object> body = new HashMap<>();
|
|
body.put("event", event);
|
|
body.put("timestamp", System.currentTimeMillis());
|
|
body.put("data", payload != null ? payload : new HashMap<>());
|
|
|
|
RestClient restClient = RestClient.create();
|
|
|
|
for (WebhookConfig webhook : matched) {
|
|
try {
|
|
restClient.post()
|
|
.uri(webhook.getUrl())
|
|
.header("Content-Type", "application/json")
|
|
.body(body)
|
|
.retrieve()
|
|
.toBodilessEntity();
|
|
log.info("Webhook 推送成功: name={}, event={}", webhook.getName(), event);
|
|
} catch (Exception e) {
|
|
log.error("Webhook 推送失败: name={}, url={}, event={}, error={}",
|
|
webhook.getName(), webhook.getUrl(), event, e.getMessage());
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 判断 Webhook 是否订阅了指定事件
|
|
*/
|
|
@SuppressWarnings("unchecked")
|
|
private boolean isEventSubscribed(WebhookConfig webhook, String event) {
|
|
Map<String, Object> events = webhook.getEvents();
|
|
if (events == null || !events.containsKey("list")) {
|
|
return false;
|
|
}
|
|
Object listObj = events.get("list");
|
|
if (listObj instanceof List<?> list) {
|
|
return list.contains(event);
|
|
}
|
|
return false;
|
|
}
|
|
}
|