本地 RAG 知识库
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

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;
}
}