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 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 listWebhooks(int page, int size, String sortField, String sortOrder) { QueryWrapper countWrapper = new QueryWrapper<>(); Long total = webhookConfigMapper.selectCount(countWrapper); QueryWrapper 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 records = webhookConfigMapper.selectList(listWrapper); Map 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 events) { // 将事件列表包装为 Map 存入 JSONB Map 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 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 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 payload) { // 查询所有已启用的 Webhook QueryWrapper wrapper = new QueryWrapper<>(); wrapper.eq("enabled", true); List allWebhooks = webhookConfigMapper.selectList(wrapper); // 过滤出订阅了该事件的 Webhook List matched = allWebhooks.stream() .filter(wh -> isEventSubscribed(wh, event)) .toList(); if (matched.isEmpty()) { return; } // 构建推送 payload Map 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 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; } }