diff --git a/api/ai-agent/.gitignore b/api/ai-agent/.gitignore new file mode 100644 index 00000000..5ff6309b --- /dev/null +++ b/api/ai-agent/.gitignore @@ -0,0 +1,38 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/api/ai-agent/Dockerfile b/api/ai-agent/Dockerfile new file mode 100644 index 00000000..e290e49c --- /dev/null +++ b/api/ai-agent/Dockerfile @@ -0,0 +1,7 @@ +FROM openjdk:17-jdk-slim + +WORKDIR /app + +COPY ./target/erp-api-2.12.0.jar erp-api.jar + +CMD ["java", "-Duser.timezone=Asia/Shanghai", "-jar", "erp-api.jar"] \ No newline at end of file diff --git a/api/ai-agent/pom.xml b/api/ai-agent/pom.xml new file mode 100644 index 00000000..74bd26a0 --- /dev/null +++ b/api/ai-agent/pom.xml @@ -0,0 +1,154 @@ + + 4.0.0 + + + + + + + + cn.qihangerp.api + api + 2.12.0 + + + ai-agent + jar + 0.1.0 + ai-agent + http://maven.apache.org + + + 17 + UTF-8 + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + + + + org.springframework.cloud + spring-cloud-starter-openfeign + 4.0.0 + + + + + + + + + + + + + + cn.qihangerp.core + security + 1.0 + + + com.squareup.okhttp3 + okhttp + 4.12.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + mysql + mysql-connector-java + 8.0.33 + + + com.baomidou + mybatis-plus-spring-boot3-starter + 3.5.5 + + + org.apache.poi + poi-ooxml + 5.2.5 + + + cn.qihangerp.service + service + 2.0.0 + + + + + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/api/ai-agent/src/main/java/cn/qihangerp/erp/AiAgent.java b/api/ai-agent/src/main/java/cn/qihangerp/erp/AiAgent.java new file mode 100644 index 00000000..e5ddd0ef --- /dev/null +++ b/api/ai-agent/src/main/java/cn/qihangerp/erp/AiAgent.java @@ -0,0 +1,31 @@ +package cn.qihangerp.erp; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +//import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.cloud.client.loadbalancer.LoadBalanced; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.web.client.RestTemplate; + +//@EnableDiscoveryClient +//@MapperScan("cn.qihangerp.oms.mapper") +@EnableFeignClients(basePackages = "cn.qihangerp.erp") +@EnableDiscoveryClient +@ComponentScan(basePackages={"cn.qihangerp"}) +@SpringBootApplication +public class AiAgent { + public static void main( String[] args ) + { + System.out.println( "Hello ai-agent!" ); + SpringApplication.run(AiAgent.class, args); + + } + @Bean + @LoadBalanced + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} diff --git a/api/ai-agent/src/main/java/cn/qihangerp/erp/config/MybatisPlusConfig.java b/api/ai-agent/src/main/java/cn/qihangerp/erp/config/MybatisPlusConfig.java new file mode 100644 index 00000000..80d391dd --- /dev/null +++ b/api/ai-agent/src/main/java/cn/qihangerp/erp/config/MybatisPlusConfig.java @@ -0,0 +1,21 @@ +package cn.qihangerp.erp.config; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +@Configuration +@MapperScan({"cn.qihangerp.mapper","cn.qihangerp.module.mapper","cn.qihangerp.mapper"}) +public class MybatisPlusConfig { + @Primary + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); //注意使用哪种数据库 + return interceptor; + } +} diff --git a/api/ai-agent/src/main/java/cn/qihangerp/erp/controller/HomeController.java b/api/ai-agent/src/main/java/cn/qihangerp/erp/controller/HomeController.java new file mode 100644 index 00000000..ed3183bd --- /dev/null +++ b/api/ai-agent/src/main/java/cn/qihangerp/erp/controller/HomeController.java @@ -0,0 +1,39 @@ +package cn.qihangerp.erp.controller; + + +import lombok.AllArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestTemplate; + +@AllArgsConstructor +@RestController +public class HomeController { + +// @Resource +// private EchoService echoService; + @Autowired + private RestTemplate restTemplate; + @Autowired + private RedisTemplate redisTemplate; + + @GetMapping("/") + public String home(){ + return "{'code':0,'msg':'oms-api请通过api访问'}"; + } + + + + @GetMapping(value = "/echo-rest") + public String rest() { + return restTemplate.getForObject("http://tao-oms/test/na", String.class); + } +// @GetMapping(value = "/echo-feign") +// public String feign() { +// String token = "Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjdkOTBmN2EzLWUwNWQtNDkxNy04NjIzLTU1OGRhNGY3NjE3NiJ9._Oukm9b0P1WvcOywLdhs6_BOt_6mRSF41Q6f4fBm_DGUkPR86Qg1tqyRTM5ouTR2Xz46IRuRAVez8Wcl3NIlwg"; +// +// return echoService.echo(token); +// } +} \ No newline at end of file diff --git a/api/ai-agent/src/main/java/cn/qihangerp/erp/controller/SseController.java b/api/ai-agent/src/main/java/cn/qihangerp/erp/controller/SseController.java new file mode 100644 index 00000000..5fcc19c6 --- /dev/null +++ b/api/ai-agent/src/main/java/cn/qihangerp/erp/controller/SseController.java @@ -0,0 +1,174 @@ +package cn.qihangerp.erp.controller; + +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +@Slf4j +@RestController +@RequestMapping("/sse") +public class SseController { + + private static final Map emitters = new ConcurrentHashMap<>(); + private final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1); + + @GetMapping(value = "/connect", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public SseEmitter connect(@RequestParam String clientId) { + SseEmitter emitter = new SseEmitter(Long.MAX_VALUE); + emitters.put(clientId, emitter); + + // 设置超时处理 + emitter.onTimeout(() -> emitters.remove(clientId)); + emitter.onCompletion(() -> emitters.remove(clientId)); + + // 发送连接成功消息 + try { + emitter.send(SseEmitter.event() + .name("connected") + .data("连接成功")); + } catch (IOException e) { + emitters.remove(clientId); + } + + // 定期发送心跳 + executorService.scheduleAtFixedRate(() -> { + try { + if (emitters.containsKey(clientId)) { + emitters.get(clientId).send(SseEmitter.event() + .name("heartbeat") + .data("ping")); + } + } catch (IOException e) { + emitters.remove(clientId); + } + }, 30, 30, TimeUnit.SECONDS); + + return emitter; + } + + @GetMapping("/send") + public String sendMessage(@RequestParam String clientId, @RequestParam String message) { + log.info("=============来新消息了!"); + SseEmitter emitter = emitters.get(clientId); + if (emitter != null) { + try { + // 调用opencode接口获取回复 + String response = callOpencodeApi(message); + + emitter.send(SseEmitter.event() + .name("message") + .data(response)); + return "消息发送成功"; + } catch (Exception e) { + emitters.remove(clientId); + return "消息发送失败"; + } + } + return "客户端不存在"; + } + + private String callOpencodeApi(String message) throws Exception { + // 创建HTTP客户端 + HttpClient client = HttpClient.newHttpClient(); + + // 1. 创建新会话 + HttpRequest createSessionRequest = HttpRequest.newBuilder() + .uri(URI.create("http://localhost:14967/session")) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString("{}")) + .build(); + + HttpResponse createSessionResponse = client.send(createSessionRequest, HttpResponse.BodyHandlers.ofString()); + String sessionId = parseSessionId(createSessionResponse.body()); + + // 2. 构建消息请求体 + JSONObject requestBody = new JSONObject(); + JSONArray parts = new JSONArray(); + JSONObject part = new JSONObject(); + part.put("type", "text"); + part.put("text", message); + parts.add(part); + requestBody.put("parts", parts); + + // 3. 向会话发送消息 + HttpRequest sendMessageRequest = HttpRequest.newBuilder() + .uri(URI.create("http://localhost:14967/session/" + sessionId + "/message")) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(requestBody.toJSONString())) + .build(); + + // 发送请求并获取响应 + HttpResponse response = client.send(sendMessageRequest, HttpResponse.BodyHandlers.ofString()); + + // 解析响应,提取AI回复 + return parseAIResponse(response.body()); + } + + private String parseSessionId(String responseBody) { + // 简单解析JSON,提取sessionId + // 实际项目中建议使用JSON库 + int idIndex = responseBody.indexOf("\"id\":\""); + if (idIndex != -1) { + int start = idIndex + 6; + int end = responseBody.indexOf("\"", start); + if (end != -1) { + return responseBody.substring(start, end); + } + } + return ""; + } + + private String parseAIResponse(String responseBody) { + log.info("=================AI回复=========="); + log.info(responseBody); + try { + // 解析响应,提取AI回复 + JSONObject jsonObject = JSONObject.parseObject(responseBody); + if (jsonObject.containsKey("info")) { + JSONArray parts = jsonObject.getJSONArray("parts"); + for (int i = 0; i < parts.size(); i++) { + JSONObject part = parts.getJSONObject(i); + if (part.containsKey("text")) { + return part.getString("text"); + } + } + + } + } catch (Exception e) { + e.printStackTrace(); + } + return "无法获取AI回复"; + } + + @GetMapping("/disconnect") + public String disconnect(@RequestParam String clientId) { + SseEmitter emitter = emitters.remove(clientId); + if (emitter != null) { + emitter.complete(); + return "断开连接成功"; + } + return "客户端不存在"; + } + + @GetMapping("/status") + public String getStatus() { + return "当前连接数: " + emitters.size(); + } +} \ No newline at end of file diff --git a/api/ai-agent/src/main/java/cn/qihangerp/erp/feign/EchoService.java b/api/ai-agent/src/main/java/cn/qihangerp/erp/feign/EchoService.java new file mode 100644 index 00000000..d74ee6ea --- /dev/null +++ b/api/ai-agent/src/main/java/cn/qihangerp/erp/feign/EchoService.java @@ -0,0 +1,12 @@ +//package cn.qihangerp.oms.feign; +// +//import org.springframework.cloud.openfeign.FeignClient; +//import org.springframework.web.bind.annotation.GetMapping; +//import org.springframework.web.bind.annotation.RequestHeader; +// +// +//@FeignClient(name = "open-api") +//public interface EchoService { +// @GetMapping(value = "/test/na") +// String echo(@RequestHeader(name = "Authorization",required = true) String Token); +//} diff --git a/api/ai-agent/src/main/java/cn/qihangerp/erp/feign/OpenApiService.java b/api/ai-agent/src/main/java/cn/qihangerp/erp/feign/OpenApiService.java new file mode 100644 index 00000000..6da16b62 --- /dev/null +++ b/api/ai-agent/src/main/java/cn/qihangerp/erp/feign/OpenApiService.java @@ -0,0 +1,66 @@ +package cn.qihangerp.erp.feign; + +import com.alibaba.fastjson2.JSONObject; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +@FeignClient(name = "oms-api") +public interface OpenApiService { + @GetMapping(value = "/dou/order/get_detail") + JSONObject getDouOrderDetail(@RequestParam String orderId); + + @GetMapping(value = "/dou/refund/get_detail") + JSONObject getDouRefundDetail(@RequestParam String id); + /** + * 抖店发货 + * @param Token + * @return + */ +// @GetMapping(value = "/dou/ship/order_ship") +// JSONObject shipDouOrder(@RequestHeader(name = "Authorization",required = true) String Token, @RequestBody DouOrderShipBo bo); + +// @GetMapping(value = "/dou/ship/order_ship_multi_pack") +// JSONObject shipDouOrderMultiPack(@RequestHeader(name = "Authorization",required = true) String Token, @RequestBody DouOrderShipMultiPackBo bo); + + + @GetMapping(value = "/jd/order/get_detail") + JSONObject getJdOrderDetail(@RequestParam Long orderId,@RequestParam Integer vc); + + @GetMapping(value = "/jd/refund/get_detail") + JSONObject getJdRefundDetail(@RequestParam Long refundId,@RequestParam Integer vc); + + @GetMapping(value = "/pdd/order/get_detail") + JSONObject getPddOrderDetail(@RequestParam String sn); + + @GetMapping(value = "/pdd/refund/get_detail") + JSONObject getPddRefundDetail(@RequestParam Long id); + + /** + * 淘宝发货 + * @param Token + * @return + */ +// @GetMapping(value = "/tao/ship/order_ship") +// JSONObject shipTaoOrder(@RequestHeader(name = "Authorization",required = true) String Token, @RequestBody TaoOrderShipBo bo); + + @GetMapping(value = "/tao/order/get_detail") + JSONObject getTaoOrderDetail(@RequestParam String tid); + + @GetMapping(value = "/tao/refund/get_detail") + JSONObject getTaoRefundDetail(@RequestParam Long refundId); + + @GetMapping(value = "/wei/order/get_detail") + JSONObject getWeiOrderDetail(@RequestParam String orderId); + + @GetMapping(value = "/wei/refund/get_detail") + JSONObject getWeiRefundDetail(@RequestParam String afterSaleOrderId); + + /** + * 微信小店发货 + * @param Token + * @return + */ +// @GetMapping(value = "/wei/ship/order_ship") +// JSONObject shipWeiOrder(@RequestHeader(name = "Authorization",required = true) String Token, @RequestBody WeiOrderShipBo bo); +} diff --git a/api/ai-agent/src/main/java/cn/qihangerp/erp/serviceImpl/DeepSeekService.java b/api/ai-agent/src/main/java/cn/qihangerp/erp/serviceImpl/DeepSeekService.java new file mode 100644 index 00000000..4dddf36f --- /dev/null +++ b/api/ai-agent/src/main/java/cn/qihangerp/erp/serviceImpl/DeepSeekService.java @@ -0,0 +1,365 @@ +//package cn.qihangerp.erp.serviceImpl; +// +//import com.fasterxml.jackson.databind.ObjectMapper; +//import jakarta.annotation.PostConstruct; +//import okhttp3.*; +//import org.slf4j.Logger; +//import org.slf4j.LoggerFactory; +//import org.springframework.beans.factory.annotation.Value; +//import org.springframework.stereotype.Service; +//import java.io.IOException; +//import java.util.*; +//import java.util.concurrent.TimeUnit; +// +//@Service +//public class DeepSeekService { +// +// private static final Logger log = LoggerFactory.getLogger(DeepSeekService.class); +// private static final MediaType JSON = MediaType.get("application/json; charset=utf-8"); +// +// @Value("${deepseek.api.key}") +// private String apiKey; +// +// @Value("${deepseek.api.endpoint:https://api.deepseek.com/v1/chat/completions}") +// private String apiEndpoint; +// +// @Value("${deepseek.api.model:deepseek-chat}") +// private String model; +// +// private OkHttpClient okHttpClient; +// private final ObjectMapper objectMapper; +// +// // 缓存最近一次成功的分析结果 +// private Map cachedAnalysis = new HashMap<>(); +// +// public DeepSeekService(ObjectMapper objectMapper) { +// this.objectMapper = objectMapper; +// } +// +// @PostConstruct +// public void init() { +// // 配置具有重试和连接池功能的OkHttpClient +// this.okHttpClient = new OkHttpClient.Builder() +// .connectTimeout(15, TimeUnit.SECONDS) // 连接超时 +// .readTimeout(30, TimeUnit.SECONDS) // 读取超时 +// .writeTimeout(15, TimeUnit.SECONDS) // 写入超时 +// .connectionPool(new ConnectionPool(5, 5, TimeUnit.MINUTES)) // 连接池 +// .addInterceptor(new RetryInterceptor(3)) // 自定义重试拦截器 +// .addInterceptor(new LoggingInterceptor()) // 日志拦截器 +// .build(); +// } +// +// /** +// * 调用DeepSeek API - 带有Spring Retry重试机制 +// */ +//// @Retryable( +//// value = {IOException.class, RuntimeException.class}, +//// maxAttempts = 3, +//// backoff = @Backoff(delay = 1000, multiplier = 2, maxDelay = 10000) +//// ) +// public Map analyzeData(Map formattedData, String analysisType) { +// String cacheKey = generateCacheKey(formattedData, analysisType); +// +// try { +// // 1. 构建请求体 +// String requestBody = buildRequestBody(formattedData, analysisType); +// RequestBody body = RequestBody.create(requestBody, JSON); +// +// // 2. 构建请求 +// Request request = new Request.Builder() +// .url(apiEndpoint) +// .header("Authorization", "Bearer " + apiKey) +// .header("Content-Type", "application/json") +// .post(body) +// .build(); +// +// // 3. 执行请求并处理响应 +// try (Response response = okHttpClient.newCall(request).execute()) { +// if (!response.isSuccessful()) { +// handleErrorResponse(response, cacheKey); +// } +// +// String responseBody = response.body().string(); +// Map result = parseResponse(responseBody, analysisType); +// +// // 缓存成功的结果 +// cacheSuccessfulResult(cacheKey, result); +// return result; +// } +// +// } catch (Exception e) { +// log.error("调用DeepSeek API失败,尝试使用缓存或降级方案", e); +// return getFallbackAnalysis(cacheKey, analysisType); +// } +// } +// +// /** +// * 为补货建议优化的专用方法 +// */ +// public Map generateReplenishmentSuggestions(Map inventoryData) { +// try { +// String prompt = buildReplenishmentPrompt(inventoryData); +// +// Map requestBody = Map.of( +// "model", model, +// "messages", List.of( +// Map.of("role", "system", "content", +// "你是一个经验丰富的电商库存管理专家,擅长制定补货策略。"), +// Map.of("role", "user", "content", prompt) +// ), +// "temperature", 0.2, +// "max_tokens", 1500, +// "response_format", Map.of("type", "json_object") +// ); +// +// String jsonBody = objectMapper.writeValueAsString(requestBody); +// +// Request request = new Request.Builder() +// .url(apiEndpoint) +// .header("Authorization", "Bearer " + apiKey) +// .post(RequestBody.create(jsonBody, JSON)) +// .build(); +// +// // 设置更短的超时时间,因为补货建议需要快速响应 +// OkHttpClient quickClient = okHttpClient.newBuilder() +// .readTimeout(15, TimeUnit.SECONDS) +// .build(); +// +// try (Response response = quickClient.newCall(request).execute()) { +// if (response.isSuccessful()) { +// String responseBody = response.body().string(); +// return parseReplenishmentResponse(responseBody); +// } else { +// // 如果API失败,使用本地算法生成补货建议 +// return generateLocalReplenishmentSuggestions(inventoryData); +// } +// } +// +// } catch (Exception e) { +// log.warn("AI补货建议失败,使用本地算法", e); +// return generateLocalReplenishmentSuggestions(inventoryData); +// } +// } +// +// /** +// * 本地补货算法 - 服务降级方案 +// */ +// private Map generateLocalReplenishmentSuggestions(Map inventoryData) { +// List> products = (List>) inventoryData.get("data"); +// List> suggestions = new ArrayList<>(); +// int totalQuantity = 0; +// double estimatedCost = 0.0; +// +// for (Map product : products) { +// String status = (String) product.get("inventoryStatus"); +// +// // 只处理需要补货的产品 +// if ("HEALTHY".equals(status) || "OVERSTOCK".equals(status)) { +// continue; +// } +// +// int currentStock = (int) product.getOrDefault("currentStock", 0); +// int safetyStock = (int) product.getOrDefault("safetyStock", 100); +// double avgDailySales = ((Integer) product.getOrDefault("avgDailySales", 10)).doubleValue(); +// double coverDays = (double) product.getOrDefault("coverDays", 0.0); +// +// Map suggestion = new HashMap<>(); +// suggestion.put("product_id", product.get("productId")); +// suggestion.put("product_name", product.get("productName")); +// +// // 本地补货逻辑 +// int suggestedQty; +// String urgency; +// +// if (currentStock <= 0) { +// suggestedQty = (int) (avgDailySales * 30); +// urgency = "紧急"; +// } else if (coverDays < 3) { +// suggestedQty = (int) (avgDailySales * 15 - currentStock); +// urgency = "高"; +// } else if (currentStock < safetyStock) { +// suggestedQty = safetyStock * 2 - currentStock; +// urgency = "中"; +// } else { +// suggestedQty = (int) (avgDailySales * 7); +// urgency = "低"; +// } +// +// suggestedQty = Math.max(suggestedQty, 10); +// totalQuantity += suggestedQty; +// +// suggestion.put("suggested_quantity", suggestedQty); +// suggestion.put("urgency", urgency); +// suggestion.put("reason", "本地算法计算"); +// suggestion.put("expected_cover_days", suggestedQty / Math.max(avgDailySales, 1)); +// +// suggestions.add(suggestion); +// } +// +// return Map.of( +// "success", true, +// "source", "local_algorithm", +// "replenishment_list", suggestions, +// "total_replenishment_quantity", totalQuantity, +// "analysis_summary", "基于本地规则生成的补货建议", +// "recommendations", List.of( +// "建议优先处理标记为'紧急'的商品", +// "此为本地降级方案,AI分析恢复后将提供更精确建议" +// ) +// ); +// } +// +// /** +// * 自定义重试拦截器 +// */ +// private static class RetryInterceptor implements Interceptor { +// private final int maxRetries; +// +// public RetryInterceptor(int maxRetries) { +// this.maxRetries = maxRetries; +// } +// +// @Override +// public Response intercept(Chain chain) throws IOException { +// Request request = chain.request(); +// Response response = null; +// IOException exception = null; +// +// // 重试逻辑 +// for (int retryCount = 0; retryCount <= maxRetries; retryCount++) { +// try { +// response = chain.proceed(request); +// +// // 只有服务器错误(5xx)或特定客户端错误才重试 +// if (response.isSuccessful() || +// (response.code() != 503 && response.code() != 429 && response.code() != 408)) { +// return response; +// } +// +// log.warn("API请求失败,状态码: {}, 重试: {}/{}", +// response.code(), retryCount, maxRetries); +// +// // 关闭响应体 +// response.close(); +// +// } catch (IOException e) { +// exception = e; +// log.warn("网络异常,重试: {}/{}", retryCount, maxRetries, e); +// } +// +// // 如果不是最后一次重试,等待一段时间 +// if (retryCount < maxRetries) { +// try { +// // 指数退避:1s, 2s, 4s... +// long waitTime = (long) Math.pow(2, retryCount) * 1000; +// Thread.sleep(waitTime); +// } catch (InterruptedException e) { +// Thread.currentThread().interrupt(); +// throw new IOException("重试被中断", e); +// } +// } +// } +// +// if (exception != null) { +// throw exception; +// } +// +// return response; +// } +// } +// +// /** +// * 日志拦截器 +// */ +// private static class LoggingInterceptor implements Interceptor { +// @Override +// public Response intercept(Chain chain) throws IOException { +// Request request = chain.request(); +// long startTime = System.currentTimeMillis(); +// +// log.debug("发送请求: {} {}", request.method(), request.url()); +// +// Response response; +// try { +// response = chain.proceed(request); +// } catch (IOException e) { +// long duration = System.currentTimeMillis() - startTime; +// log.error("请求失败: {} {} - 耗时: {}ms", +// request.method(), request.url(), duration, e); +// throw e; +// } +// +// long duration = System.currentTimeMillis() - startTime; +// log.info("收到响应: {} {} - 状态: {} - 耗时: {}ms", +// request.method(), request.url(), response.code(), duration); +// +// return response; +// } +// } +// +// /** +// * 错误处理 +// */ +// private void handleErrorResponse(Response response, String cacheKey) throws IOException { +// int code = response.code(); +// String errorBody = response.body() != null ? response.body().string() : "无错误详情"; +// +// log.error("DeepSeek API错误响应: 状态码={}, 错误信息={}", code, errorBody); +// +// // 根据错误类型处理 +// if (code == 401) { +// throw new RuntimeException("API密钥无效或已过期"); +// } else if (code == 429) { +// throw new RuntimeException("请求频率超限,请稍后重试"); +// } else if (code == 503) { +// // 服务不可用,尝试使用缓存 +// if (cachedAnalysis.containsKey(cacheKey)) { +// log.info("服务不可用,使用缓存结果"); +// throw new ServiceUnavailableException("服务不可用,已返回缓存结果"); +// } +// throw new RuntimeException("DeepSeek服务暂时不可用,请稍后重试"); +// } else { +// throw new RuntimeException(String.format("API请求失败: %d - %s", code, errorBody)); +// } +// } +// +// /** +// * 服务降级:获取缓存或基础分析 +// */ +// private Map getFallbackAnalysis(String cacheKey, String analysisType) { +// // 1. 首先尝试缓存 +// if (cachedAnalysis.containsKey(cacheKey)) { +// log.info("使用缓存的AI分析结果"); +// Map cached = (Map) cachedAnalysis.get(cacheKey); +// cached.put("source", "cached"); +// return cached; +// } +// +// // 2. 返回基础分析模板 +// log.info("返回基础分析模板"); +// return Map.of( +// "success", false, +// "source", "fallback_template", +// "message", "AI分析服务暂时不可用", +// "basic_analysis", Map.of( +// "suggestion", "建议检查库存水平,重点关注缺货商品", +// "generated_at", new Date() +// ), +// "recommendations", List.of( +// "1. 优先处理库存为0的商品", +// "2. 检查日销量高但库存低的商品", +// "3. AI服务恢复后重新获取详细分析" +// ) +// ); +// } +// +// // 其他辅助方法(buildRequestBody, parseResponse等)保持原有逻辑 +// // ... +//} +// +//// 自定义异常类 +//class ServiceUnavailableException extends RuntimeException { +// public ServiceUnavailableException(String message) { +// super(message); +// } +//} \ No newline at end of file diff --git a/api/ai-agent/src/main/java/cn/qihangerp/erp/serviceImpl/InventorySalesAnalyzer.java b/api/ai-agent/src/main/java/cn/qihangerp/erp/serviceImpl/InventorySalesAnalyzer.java new file mode 100644 index 00000000..ef4c5154 --- /dev/null +++ b/api/ai-agent/src/main/java/cn/qihangerp/erp/serviceImpl/InventorySalesAnalyzer.java @@ -0,0 +1,273 @@ +package cn.qihangerp.erp.serviceImpl; + +import com.fasterxml.jackson.annotation.JsonProperty; +import okhttp3.*; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.util.*; +import java.util.stream.Collectors; + +public class InventorySalesAnalyzer { + + // 配置你的 DeepSeek API 信息 + private static final String API_KEY = "sk-e1f3aecc45e44eca9451d5a659a4bc91"; + private static final String API_URL = "https://api.deepseek.com/v1/chat/completions"; + + private static final MediaType JSON = MediaType.get("application/json; charset=utf-8"); + private static final OkHttpClient client = new OkHttpClient.Builder() + .connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS) + .readTimeout(60, java.util.concurrent.TimeUnit.SECONDS) + .build(); + private static final ObjectMapper objectMapper = new ObjectMapper(); + + // 你的数据 + private static final String INVENTORY_JSON = "[{\"id\":1,\"goods_title\":\"雷士照明led吸顶灯灯芯替换圆形灯板节能灯芯冷光高显护眼健康\",\"sku_name\":\"白光12W\",\"stock_num\":12},{\"id\":2,\"goods_title\":\"雷士照明led吸顶灯灯芯替换圆形灯板节能灯芯冷光高显护眼健康\",\"sku_name\":\"白光18W\",\"stock_num\":12},{\"id\":3,\"goods_title\":\"雷士照明led吸顶灯灯芯替换圆形灯板节能灯芯冷光高显护眼健康\",\"sku_name\":\"白光24W\",\"stock_num\":12},{\"id\":4,\"goods_title\":\"雷士照明led吸顶灯灯芯替换圆形灯板节能灯芯冷光高显护眼健康\",\"sku_name\":\"双色36W\",\"stock_num\":12}]"; + + private static final String SALES_JSON = "[{\"order_num\":\"1\",\"sku_id\":1,\"count\":1,\"item_amount\":29.32,\"order_time\":\"2025-05-24 23:19:51\"},{\"order_num\":\"1\",\"sku_id\":3,\"count\":1,\"item_amount\":29.32,\"order_time\":\"2025-05-24 23:19:51\"},{\"order_num\":\"1\",\"sku_id\":1,\"count\":1,\"item_amount\":29.32,\"order_time\":\"2025-05-24 23:19:51\"},{\"order_num\":\"1\",\"sku_id\":2,\"count\":1,\"item_amount\":29.32,\"order_time\":\"2025-05-24 23:19:51\"},{\"order_num\":\"1\",\"sku_id\":4,\"count\":1,\"item_amount\":29.32,\"order_time\":\"2025-05-24 23:19:51\"},{\"order_num\":\"1\",\"sku_id\":1,\"count\":1,\"item_amount\":29.32,\"order_time\":\"2025-05-24 23:19:51\"},{\"order_num\":\"1\",\"sku_id\":1,\"count\":1,\"item_amount\":29.32,\"order_time\":\"2025-05-24 23:19:51\"},{\"order_num\":\"1\",\"sku_id\":3,\"count\":1,\"item_amount\":29.32,\"order_time\":\"2025-05-24 23:19:51\"},{\"order_num\":\"1\",\"sku_id\":1,\"count\":1,\"item_amount\":29.32,\"order_time\":\"2025-05-24 23:19:51\"},{\"order_num\":\"1\",\"sku_id\":1,\"count\":1,\"item_amount\":29.32,\"order_time\":\"2025-05-24 23:19:51\"},{\"order_num\":\"1\",\"sku_id\":2,\"count\":1,\"item_amount\":29.32,\"order_time\":\"2025-05-24 23:19:51\"}]"; + + public static void main(String[] args) { + try { + System.out.println("开始分析库存与销售数据...\n"); + + // 1. 解析数据 + List inventoryList = parseInventoryData(); + List salesList = parseSalesData(); + + // 2. 分析数据并生成报告 + String analysisResult = analyzeInventoryAndSales(inventoryList, salesList); + + System.out.println("=== AI 分析报告 ===\n"); + System.out.println(analysisResult); + + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 核心分析方法 + */ + public static String analyzeInventoryAndSales(List inventory, + List sales) throws IOException { + + // 1. 数据预处理:按 SKU ID 关联库存和销售数据 + Map analysisMap = new HashMap<>(); + + // 初始化库存数据 + for (InventoryItem item : inventory) { + SkuAnalysis analysis = new SkuAnalysis(); + analysis.id = item.id; + analysis.goodsTitle = item.goodsTitle; + analysis.skuName = item.skuName; + analysis.stockNum = item.stockNum; + analysisMap.put(item.id, analysis); + } + + // 统计销售数据 + for (SalesOrder order : sales) { + if (analysisMap.containsKey(order.skuId)) { + SkuAnalysis analysis = analysisMap.get(order.skuId); + analysis.totalSales += order.count; + analysis.totalRevenue += order.itemAmount; + analysis.orderCount++; + + // 记录销售时间(用于趋势分析) + analysis.salesTimes.add(order.orderTime); + } + } + + // 2. 计算关键指标 + for (SkuAnalysis analysis : analysisMap.values()) { + // 计算日均销量(假设数据是最近30天的) + analysis.dailyAvgSales = analysis.totalSales / 30.0; + + // 计算可售天数 + if (analysis.dailyAvgSales > 0) { + analysis.daysOfSupply = analysis.stockNum / analysis.dailyAvgSales; + } else { + analysis.daysOfSupply = 999; // 无销售 + } + + // 判断库存状态 + analysis.stockStatus = determineStockStatus(analysis.stockNum, analysis.dailyAvgSales); + } + + // 3. 构建 AI 分析提示词 + String prompt = buildAnalysisPrompt(analysisMap); + + // 4. 调用 DeepSeek API + return callDeepSeekAPI(prompt); + } + + /** + * 构建 AI 分析提示词 + */ + private static String buildAnalysisPrompt(Map analysisMap) { + StringBuilder prompt = new StringBuilder(); + + prompt.append("你是一名专业的电商库存管理专家。请分析以下 LED 灯具产品的库存与销售数据,并提供专业的分析报告和建议:\n\n"); + + prompt.append("=== 数据概览 ===\n"); + prompt.append("产品名称:雷士照明 LED 吸顶灯灯芯\n"); + prompt.append("分析时间:").append(new Date()).append("\n\n"); + + prompt.append("=== 详细数据 ===\n"); + prompt.append(String.format("%-8s %-12s %-8s %-8s %-12s %-10s %-15s\n", + "SKU ID", "规格", "库存量", "总销量", "总销售额", "可售天数", "库存状态")); + prompt.append("-".repeat(80)).append("\n"); + + for (SkuAnalysis analysis : analysisMap.values()) { + prompt.append(String.format("%-8d %-12s %-8d %-8d %-12.2f %-10.1f %-15s\n", + analysis.id, + analysis.skuName, + analysis.stockNum, + analysis.totalSales, + analysis.totalRevenue, + analysis.daysOfSupply, + analysis.stockStatus + )); + } + + prompt.append("\n=== 分析要求 ===\n"); + prompt.append("请基于以上数据,提供以下分析:\n"); + prompt.append("1. **库存健康度分析**:评估每个SKU的库存状况,识别缺货风险\n"); + prompt.append("2. **销售表现分析**:分析各规格产品的销售情况,找出畅销款和滞销款\n"); + prompt.append("3. **补货建议**:\n"); + prompt.append(" - 哪些SKU需要立即补货?建议补货数量?\n"); + prompt.append(" - 哪些SKU库存过多?建议如何清理?\n"); + prompt.append(" - 建议的安全库存水平\n"); + prompt.append("4. **运营建议**:基于销售模式,给出采购、促销或产品组合建议\n\n"); + + prompt.append("请以专业报告格式回复,包含具体数据和理由。"); + + return prompt.toString(); + } + + /** + * 调用 DeepSeek API + */ + private static String callDeepSeekAPI(String prompt) throws IOException { + // 构建请求体 + Map requestBody = new HashMap<>(); + requestBody.put("model", "deepseek-chat"); + requestBody.put("messages", Arrays.asList( + Map.of("role", "user", "content", prompt) + )); + requestBody.put("temperature", 0.3); // 降低随机性,使分析更稳定 + requestBody.put("max_tokens", 2000); + + String jsonBody = objectMapper.writeValueAsString(requestBody); + + // 创建请求 + Request request = new Request.Builder() + .url(API_URL) + .header("Authorization", "Bearer " + API_KEY) + .header("Content-Type", "application/json") + .post(RequestBody.create(jsonBody, JSON)) + .build(); + + // 发送请求(带重试机制) + for (int attempt = 0; attempt < 3; attempt++) { + try (Response response = client.newCall(request).execute()) { + if (response.isSuccessful()) { + String responseBody = response.body().string(); + return extractContentFromResponse(responseBody); + } else if (response.code() == 429 || response.code() >= 500) { + // 频率限制或服务器错误,等待后重试 + System.out.println("请求失败,状态码: " + response.code() + ",等待重试..."); + Thread.sleep(2000 * (attempt + 1)); + continue; + } else { + throw new IOException("API请求失败: " + response.code() + " - " + response.message()); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("请求被中断", e); + } + } + + throw new IOException("API请求失败,已重试3次"); + } + + /** + * 从 API 响应中提取内容 + */ + private static String extractContentFromResponse(String responseBody) throws IOException { + Map responseMap = objectMapper.readValue(responseBody, + new TypeReference>() {}); + + List> choices = (List>) responseMap.get("choices"); + if (choices != null && !choices.isEmpty()) { + Map choice = choices.get(0); + Map message = (Map) choice.get("message"); + return (String) message.get("content"); + } + + return "未获取到有效回复"; + } + + /** + * 判断库存状态 + */ + private static String determineStockStatus(int stock, double dailySales) { + if (stock == 0) return "缺货"; + if (dailySales == 0) return "滞销"; + + double daysOfSupply = stock / dailySales; + + if (daysOfSupply < 7) return "急需补货"; + if (daysOfSupply < 14) return "需要补货"; + if (daysOfSupply < 30) return "库存正常"; + if (daysOfSupply < 60) return "库存偏高"; + return "库存积压"; + } + + // 数据解析方法 + private static List parseInventoryData() throws IOException { + return objectMapper.readValue(INVENTORY_JSON, + new TypeReference>() {}); + } + + private static List parseSalesData() throws IOException { + return objectMapper.readValue(SALES_JSON, + new TypeReference>() {}); + } + + // 数据类定义 + static class InventoryItem { + public int id; + @JsonProperty("goods_title") + public String goodsTitle; + @JsonProperty("sku_name") + public String skuName; + @JsonProperty("stock_num") + public int stockNum; + } + + static class SalesOrder { + @JsonProperty("order_num") + public String orderNum; + @JsonProperty("sku_id") + public int skuId; + public int count; + @JsonProperty("item_amount") + public double itemAmount; + @JsonProperty("order_time") + public String orderTime; + } + + static class SkuAnalysis { + public int id; + public String goodsTitle; + public String skuName; + public int stockNum; + public int totalSales = 0; + public double totalRevenue = 0.0; + public int orderCount = 0; + public double dailyAvgSales = 0.0; + public double daysOfSupply = 0.0; + public String stockStatus = "未知"; + public List salesTimes = new ArrayList<>(); + } +} \ No newline at end of file diff --git a/api/ai-agent/src/main/resources/application.yml b/api/ai-agent/src/main/resources/application.yml new file mode 100644 index 00000000..0715caf0 --- /dev/null +++ b/api/ai-agent/src/main/resources/application.yml @@ -0,0 +1,101 @@ +server: + port: 8084 +spring: + application: + name: ai-agent + cloud: + loadbalancer: + nacos: + enabled: true + nacos: + # serverAddr: 127.0.0.1:8848 + discovery: + server-addr: 127.0.0.1:8848 +# username: nacos +# password: nacos + + data: + # redis 配置 + redis: + # 地址 + # host: 8.130.98.215 + host: 127.0.0.1 + # 端口,默认为6379 + port: 6379 + # 数据库索引 + database: 0 + # 密码 + # password: 123321 + # 连接超时时间 + timeout: 10s + lettuce: + pool: + # 连接池中的最小空闲连接 + min-idle: 0 + # 连接池中的最大空闲连接 + max-idle: 8 + # 连接池的最大数据库连接数 + max-active: 8 + # #连接池最大阻塞等待时间(使用负值表示没有限制) + max-wait: -1ms + datasource: + driverClassName: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://127.0.0.1:3306/qihang-erp?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 + username: root + password: Andy_123 + hikari: + maximum-pool-size: 10 + min-idle: 5 + connection-timeout: 30000 + idle-timeout: 600000 + max-lifetime: 1800000 +# kafka: +# bootstrap-servers: localhost:9092 +# producer: +# batch-size: 16384 #批量大小 +# acks: -1 #应答级别:多少个分区副本备份完成时向生产者发送ack确认(可选0、1、all/-1) +# retries: 10 # 消息发送重试次数 +# # transaction-id-prefix: tx_1 #事务id前缀 +# buffer-memory: 33554432 +# key-serializer: org.apache.kafka.common.serialization.StringSerializer +# value-serializer: org.apache.kafka.common.serialization.StringSerializer +# properties: +# linger: +# ms: 2000 #提交延迟 +# # partitioner: #指定分区器 +# # class: com.example.kafkademo.config.CustomizePartitioner +# consumer: +# group-id: testGroup #默认的消费组ID +# enable-auto-commit: true #是否自动提交offset +# auto-commit-interval: 2000 #提交offset延时 +# # 当kafka中没有初始offset或offset超出范围时将自动重置offset +# # earliest:重置为分区中最小的offset; +# # latest:重置为分区中最新的offset(消费分区中新产生的数据); +# # none:只要有一个分区不存在已提交的offset,就抛出异常; +# auto-offset-reset: latest +# max-poll-records: 500 #单次拉取消息的最大条数 +# key-deserializer: org.apache.kafka.common.serialization.StringDeserializer +# value-deserializer: org.apache.kafka.common.serialization.StringDeserializer +# properties: +# session: +# timeout: +# ms: 120000 # 消费会话超时时间(超过这个时间 consumer 没有发送心跳,就会触发 rebalance 操作) +# request: +# timeout: +# ms: 18000 # 消费请求的超时时间 + + + + + +mybatis-plus: + mapper-locations: classpath*:mapper/**/*Mapper.xml + type-aliases-package: cn.qihangerp.oms.domain;cn.qihangerp.module.domain;cn.qihangerp.security.entity; +# configuration: +# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开启sql日志 + +deepseek: + api: + key: + endpoint: https://api.deepseek.com/chat/completions + model: deepseek-chat \ No newline at end of file diff --git a/api/gateway/src/main/resources/application.yaml b/api/gateway/src/main/resources/application.yaml index 15aa0ac3..1c5c56cc 100644 --- a/api/gateway/src/main/resources/application.yaml +++ b/api/gateway/src/main/resources/application.yaml @@ -37,10 +37,10 @@ spring: filters: - StripPrefix=2 - - id: open_api_route - uri: lb://open-api + - id: ai_agent_route + uri: lb://ai-agent predicates: - - Path=/api/open-api/** + - Path=/api/ai-agent/** filters: - StripPrefix=2 diff --git a/api/pom.xml b/api/pom.xml index 2bebb8de..29f27631 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -21,6 +21,7 @@ http://maven.apache.org gateway + ai-agent erp-api sys-api oms-api diff --git a/vue/src/views/index.vue b/vue/src/views/index.vue index a7731dfb..a231b9f0 100644 --- a/vue/src/views/index.vue +++ b/vue/src/views/index.vue @@ -1,164 +1,616 @@