XuqmGroup-Server/xuqm-log-service/src/main/java/com/xuqm/log/service/WebhookService.java

179 行
7.1 KiB
Java

package com.xuqm.log.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.xuqm.log.dto.WebhookRequest;
import com.xuqm.log.dto.WebhookResponse;
import com.xuqm.log.entity.LogIssueEntity;
import com.xuqm.log.entity.LogWebhookEntity;
import com.xuqm.log.repository.LogWebhookRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
@Service
public class WebhookService {
private static final Logger log = LoggerFactory.getLogger(WebhookService.class);
private static final String COOLDOWN_KEY_PREFIX = "log:webhook:cooldown:";
private final LogWebhookRepository webhookRepository;
private final StringRedisTemplate redisTemplate;
private final ObjectMapper objectMapper;
private final HttpClient httpClient;
public WebhookService(LogWebhookRepository webhookRepository,
StringRedisTemplate redisTemplate,
ObjectMapper objectMapper) {
this.webhookRepository = webhookRepository;
this.redisTemplate = redisTemplate;
this.objectMapper = objectMapper;
this.httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofMillis(3000))
.build();
}
public void checkAndNotify(LogIssueEntity issue) {
List<LogWebhookEntity> webhooks = webhookRepository.findByAppKeyAndEnabledTrue(issue.getAppKey());
for (LogWebhookEntity webhook : webhooks) {
if (!matchesEventType(webhook.getEvents(), issue.getType())) {
continue;
}
String cooldownKey = COOLDOWN_KEY_PREFIX + webhook.getId() + ":" + issue.getFingerprint();
Boolean acquired = redisTemplate.opsForValue()
.setIfAbsent(cooldownKey, "1", Duration.ofSeconds(webhook.getCooldownSec()));
if (Boolean.TRUE.equals(acquired)) {
sendWebhook(webhook, issue);
} else {
log.debug("Webhook cooldown active: webhookId={}, fingerprint={}", webhook.getId(), issue.getFingerprint());
}
}
}
private void sendWebhook(LogWebhookEntity webhook, LogIssueEntity issue) {
try {
Map<String, Object> body = Map.of(
"event", "issue.new",
"appKey", issue.getAppKey(),
"issue", Map.of(
"fingerprint", issue.getFingerprint(),
"type", issue.getType(),
"title", issue.getTitle(),
"count", issue.getCount(),
"lastSeenAt", issue.getLastSeenAt().toString(),
"platform", issue.getPlatform() != null ? issue.getPlatform() : "",
"appVersion", issue.getAppVersion() != null ? issue.getAppVersion() : ""
)
);
String jsonBody = objectMapper.writeValueAsString(body);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(webhook.getUrl()))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.timeout(Duration.ofMillis(5000))
.build();
httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenAccept(response -> {
if (response.statusCode() >= 200 && response.statusCode() < 300) {
log.info("Webhook sent successfully: webhookId={}, status={}", webhook.getId(), response.statusCode());
} else {
log.warn("Webhook returned non-2xx: webhookId={}, status={}", webhook.getId(), response.statusCode());
}
})
.exceptionally(ex -> {
log.error("Webhook send failed: webhookId={}, url={}", webhook.getId(), webhook.getUrl(), ex);
return null;
});
} catch (Exception e) {
log.error("Failed to serialize webhook body: webhookId={}", webhook.getId(), e);
}
}
private boolean matchesEventType(String eventsJson, String eventType) {
try {
@SuppressWarnings("unchecked")
List<String> events = objectMapper.readValue(eventsJson, List.class);
return events.contains(eventType);
} catch (Exception e) {
return false;
}
}
@Transactional(readOnly = true)
public List<WebhookResponse> listWebhooks(String appKey) {
return webhookRepository.findByAppKey(appKey).stream()
.map(this::toResponse)
.toList();
}
@Transactional
public WebhookResponse createWebhook(WebhookRequest request) {
LogWebhookEntity entity = new LogWebhookEntity();
entity.setAppKey(request.appKey());
entity.setUrl(request.url());
entity.setCooldownSec(request.cooldownSec() > 0 ? request.cooldownSec() : 3600);
entity.setEnabled(true);
entity.setCreatedAt(LocalDateTime.now());
try {
entity.setEvents(objectMapper.writeValueAsString(request.events()));
} catch (Exception e) {
entity.setEvents("[]");
}
entity = webhookRepository.save(entity);
return toResponse(entity);
}
@Transactional
public WebhookResponse updateWebhook(Long id, WebhookRequest request) {
LogWebhookEntity entity = webhookRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Webhook not found: " + id));
entity.setAppKey(request.appKey());
entity.setUrl(request.url());
entity.setCooldownSec(request.cooldownSec() > 0 ? request.cooldownSec() : 3600);
try {
entity.setEvents(objectMapper.writeValueAsString(request.events()));
} catch (Exception e) {
// keep existing
}
entity = webhookRepository.save(entity);
return toResponse(entity);
}
@Transactional
public void deleteWebhook(Long id) {
webhookRepository.deleteById(id);
}
private WebhookResponse toResponse(LogWebhookEntity entity) {
List<String> events;
try {
events = objectMapper.readValue(entity.getEvents(), List.class);
} catch (Exception e) {
events = List.of();
}
return new WebhookResponse(
entity.getId(), entity.getAppKey(), entity.getUrl(),
events, entity.getCooldownSec(), entity.isEnabled(),
entity.getCreatedAt()
);
}
}