|
|
@@ -0,0 +1,145 @@
|
|
|
+package com.etotem.cfc.service;
|
|
|
+
|
|
|
+import com.etotem.cfc.dto.RecommendationResult;
|
|
|
+import com.fasterxml.jackson.databind.JsonNode;
|
|
|
+import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
+import com.fasterxml.jackson.databind.node.ArrayNode;
|
|
|
+import com.fasterxml.jackson.databind.node.ObjectNode;
|
|
|
+import org.slf4j.Logger;
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
+import org.springframework.http.HttpEntity;
|
|
|
+import org.springframework.http.ResponseEntity;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.web.client.RestTemplate;
|
|
|
+
|
|
|
+import javax.annotation.PostConstruct;
|
|
|
+import java.util.*;
|
|
|
+import java.util.concurrent.atomic.AtomicInteger;
|
|
|
+import java.util.concurrent.atomic.AtomicLong;
|
|
|
+
|
|
|
+/**
|
|
|
+ * LangGraph Python 服务网关
|
|
|
+ * 统一管理 Python 服务调用、熔断、Fallback
|
|
|
+ */
|
|
|
+@Service
|
|
|
+public class AiGateway {
|
|
|
+
|
|
|
+ private static final Logger log = LoggerFactory.getLogger(AiGateway.class);
|
|
|
+
|
|
|
+ @Value("${python.enabled:false}")
|
|
|
+ private boolean enabled;
|
|
|
+
|
|
|
+ @Value("${python.base-url:http://localhost:9000}")
|
|
|
+ private String baseUrl;
|
|
|
+
|
|
|
+ @Value("${python.timeout-ms:15000}")
|
|
|
+ private int timeoutMs;
|
|
|
+
|
|
|
+ @Value("${python.circuit-breaker.failure-threshold:3}")
|
|
|
+ private int failureThreshold;
|
|
|
+
|
|
|
+ @Value("${python.circuit-breaker.reset-timeout-ms:30000}")
|
|
|
+ private int resetTimeoutMs;
|
|
|
+
|
|
|
+ private final RestTemplate restTemplate = new RestTemplate();
|
|
|
+ private final ObjectMapper objectMapper = new ObjectMapper();
|
|
|
+
|
|
|
+ // 熔断器状态
|
|
|
+ private final AtomicInteger consecutiveFailures = new AtomicInteger(0);
|
|
|
+ private final AtomicLong lastFailureTime = new AtomicLong(0);
|
|
|
+ private volatile boolean circuitOpen = false;
|
|
|
+
|
|
|
+ @PostConstruct
|
|
|
+ public void init() {
|
|
|
+ if (enabled) {
|
|
|
+ log.info("AiGateway 已启用: baseUrl={}, timeout={}ms", baseUrl, timeoutMs);
|
|
|
+ } else {
|
|
|
+ log.info("AiGateway 已禁用, 所有请求走 Dify");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public boolean isEnabled() {
|
|
|
+ return enabled;
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean isCircuitOpen() {
|
|
|
+ if (!circuitOpen) return false;
|
|
|
+ // 检查是否到重置时间
|
|
|
+ if (System.currentTimeMillis() - lastFailureTime.get() > resetTimeoutMs) {
|
|
|
+ circuitOpen = false;
|
|
|
+ consecutiveFailures.set(0);
|
|
|
+ log.info("AiGateway 熔断器已半开");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void recordFailure() {
|
|
|
+ int failures = consecutiveFailures.incrementAndGet();
|
|
|
+ lastFailureTime.set(System.currentTimeMillis());
|
|
|
+ if (failures >= failureThreshold) {
|
|
|
+ circuitOpen = true;
|
|
|
+ log.warn("AiGateway 熔断器已打开 (连续{}次失败)", failures);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 调用 Python 推荐服务, 失败时返回 null (由调用方决定 fallback)
|
|
|
+ */
|
|
|
+ public List<RecommendationResult> recommend(Long userId, List<String> tags, List<String> types, Integer limit) {
|
|
|
+ if (!enabled || isCircuitOpen()) return null;
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 使用 ObjectMapper 构造 JSON 替代手动拼接
|
|
|
+ ObjectNode body = objectMapper.createObjectNode();
|
|
|
+ body.put("user_id", userId);
|
|
|
+ if (tags != null && !tags.isEmpty()) {
|
|
|
+ ArrayNode tagsArray = body.putArray("tags");
|
|
|
+ tags.forEach(tagsArray::add);
|
|
|
+ }
|
|
|
+ if (types != null && !types.isEmpty()) {
|
|
|
+ ArrayNode typesArray = body.putArray("types");
|
|
|
+ types.forEach(typesArray::add);
|
|
|
+ }
|
|
|
+ body.put("limit", limit != null ? limit : 5);
|
|
|
+
|
|
|
+ HttpEntity<String> entity = new HttpEntity<>(body.toString(), createJsonHeaders());
|
|
|
+ String url = baseUrl + "/api/v1/recommend";
|
|
|
+
|
|
|
+ ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
|
|
|
+
|
|
|
+ if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
|
|
|
+ JsonNode root = objectMapper.readTree(response.getBody());
|
|
|
+ JsonNode items = root.get("items");
|
|
|
+ if (items != null && items.isArray()) {
|
|
|
+ List<RecommendationResult> results = new ArrayList<>();
|
|
|
+ for (JsonNode item : items) {
|
|
|
+ RecommendationResult r = new RecommendationResult();
|
|
|
+ r.setType(item.get("type").asText());
|
|
|
+ r.setId(item.get("id").asLong());
|
|
|
+ r.setName(item.get("name").asText());
|
|
|
+ r.setDescription(item.has("description") ? item.get("description").asText() : "");
|
|
|
+ r.setReason(item.has("reason") ? item.get("reason").asText() : "");
|
|
|
+ r.setSource("langgraph");
|
|
|
+ results.add(r);
|
|
|
+ }
|
|
|
+ consecutiveFailures.set(0);
|
|
|
+ log.debug("AiGateway recommend 成功: {} 条", results.size());
|
|
|
+ return results;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("AiGateway recommend 调用失败: {}", e.getMessage());
|
|
|
+ recordFailure();
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private org.springframework.http.HttpHeaders createJsonHeaders() {
|
|
|
+ org.springframework.http.HttpHeaders headers = new org.springframework.http.HttpHeaders();
|
|
|
+ headers.setContentType(org.springframework.http.MediaType.APPLICATION_JSON);
|
|
|
+ return headers;
|
|
|
+ }
|
|
|
+}
|