Commit fec8084d authored by Tong Li's avatar Tong Li

Merge remote-tracking branch 'origin/AI'

parents 8396797c ffea8fc4
......@@ -23,6 +23,7 @@
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
......@@ -62,7 +63,10 @@
<artifactId>velocity-engine-core</artifactId>
<version>2.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- 数据库驱动 -->
<dependency>
<groupId>mysql</groupId>
......@@ -117,6 +121,13 @@
<artifactId>ortools-java</artifactId>
<version>9.7.2996</version>
</dependency>
<!-- HTTP客户端 (用于调用LLM API) -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.14</version>
</dependency>
</dependencies>
<build>
......
......@@ -11,15 +11,15 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*") // 修改这里
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
// @Override
// public void addCorsMappings(CorsRegistry registry) {
// registry.addMapping("/**")
// .allowedOriginPatterns("*") // 修改这里
// .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
// .allowedHeaders("*")
// .allowCredentials(true)
// .maxAge(3600);
// }
@Bean
public CorsFilter corsFilter() {
......@@ -35,5 +35,6 @@ public class CorsConfig implements WebMvcConfigurer {
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
\ No newline at end of file
package com.aps.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* LLM配置类
*/
@Data
@Component
@ConfigurationProperties(prefix = "llm")
public class LLMConfig {
/**
* API密钥
*/
private String apiKey = "";
/**
* API基础URL
*/
private String baseUrl = "https://api.deepseek.com/v1";
/**
* 模型名称
*/
private String model = "deepseek-chat";
/**
* 温度参数
*/
private Double temperature = 0.3;
/**
* 最大token数
*/
private Integer maxTokens = 4000;
}
package com.aps.controller;
import com.aps.llm.LLMClient;
import com.aps.model.ChromosomeData;
import com.aps.model.MachineOption;
import com.aps.model.Operation;
import com.aps.parser.ChromosomeParser;
import com.aps.service.plan.SceneService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Collectors;
/**
* 诊断API控制器
*/
@RestController
@RequestMapping("/api")
@CrossOrigin(origins = "*")
public class DiagnosisController {
@Autowired
private LLMClient llmClient;
private static final String UPLOAD_DIR = "uploads";
@Autowired
private SceneService sceneService;
// 移除成员变量,改用Session存储
// 注意:ChromosomeParser不再使用@Autowired注入,而是每次请求创建新实例
/**
* 获取会话数据(从Session中)
*/
@SuppressWarnings("unchecked")
private Map<String, Object> getSessionData(javax.servlet.http.HttpSession session) {
Map<String, Object> data = (Map<String, Object>) session.getAttribute("sessionData");
if (data == null) {
data = new HashMap<>();
session.setAttribute("sessionData", data);
}
return data;
}
/**
* 获取对话历史(从Session中)
*/
@SuppressWarnings("unchecked")
private List<Map<String, String>> getConversationHistory(javax.servlet.http.HttpSession session) {
List<Map<String, String>> history = (List<Map<String, String>>) session.getAttribute("conversationHistory");
if (history == null) {
history = new ArrayList<>();
session.setAttribute("conversationHistory", history);
}
return history;
}
/**
* 获取当前用户上传的文件列表(从Session中)
*/
@SuppressWarnings("unchecked")
private List<String> getUserUploadedFiles(javax.servlet.http.HttpSession session) {
List<String> files = (List<String>) session.getAttribute("userUploadedFiles");
if (files == null) {
files = new ArrayList<>();
session.setAttribute("userUploadedFiles", files);
}
return files;
}
/**
* 保存当前用户上传的文件列表(到Session中)
*/
private void saveUserUploadedFiles(javax.servlet.http.HttpSession session, List<String> files) {
session.setAttribute("userUploadedFiles", files);
}
/**
* 清空会话数据
*/
private void clearSessionData(javax.servlet.http.HttpSession session) {
session.removeAttribute("sessionData");
session.removeAttribute("conversationHistory");
session.removeAttribute("userUploadedFiles");
}
/**
* 获取可用的JSON文件列表(仅显示当前用户上传的文件)
*/
@GetMapping("/files")
public ResponseEntity<Map<String, Object>> getAvailableFiles(javax.servlet.http.HttpSession session) {
try {
// 从Session中获取当前用户上传的文件列表
List<String> userFiles = getUserUploadedFiles(session);
if (userFiles.isEmpty()) {
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("files", new ArrayList<>());
return ResponseEntity.ok(response);
}
File uploadDir = new File(UPLOAD_DIR);
List<Map<String, Object>> fileList = new ArrayList<>();
for (String fileName : userFiles) {
File file = new File(uploadDir, fileName);
if (file.exists() && file.isFile()) {
Map<String, Object> fileInfo = new HashMap<>();
fileInfo.put("name", file.getName());
fileInfo.put("path", file.getAbsolutePath());
fileInfo.put("size", file.length());
fileInfo.put("lastModified", file.lastModified());
fileList.add(fileInfo);
}
}
// 按修改时间排序,最新的在前
fileList.sort((a, b) ->
Long.compare((Long) b.get("lastModified"), (Long) a.get("lastModified")));
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("files", fileList);
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> error = new HashMap<>();
error.put("success", false);
error.put("message", e.getMessage());
return ResponseEntity.status(500).body(error);
}
}
/**
* 上传JSON文件(绑定到当前用户Session)
*/
@PostMapping("/upload")
public ResponseEntity<Map<String, Object>> uploadFile(@RequestParam("file") MultipartFile file,
javax.servlet.http.HttpSession session) {
try {
// 创建上传目录
Path uploadPath = Paths.get(UPLOAD_DIR);
if (!Files.exists(uploadPath)) {
Files.createDirectories(uploadPath);
}
// 保存文件
String fileName = file.getOriginalFilename();
if (fileName == null || fileName.isEmpty()) {
throw new IllegalArgumentException("文件名不能为空");
}
// 确保文件名以.json结尾
if (!fileName.toLowerCase().endsWith(".json")) {
fileName += ".json";
}
Path filePath = uploadPath.resolve(fileName);
Files.write(filePath, file.getBytes());
// 将文件添加到当前用户的文件列表中
List<String> userFiles = getUserUploadedFiles(session);
if (!userFiles.contains(fileName)) {
userFiles.add(fileName);
saveUserUploadedFiles(session, userFiles);
}
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("fileName", fileName);
response.put("filePath", filePath.toString());
response.put("message", "文件上传成功");
return ResponseEntity.ok(response);
} catch (IOException e) {
Map<String, Object> error = new HashMap<>();
error.put("success", false);
error.put("message", "文件上传失败: " + e.getMessage());
return ResponseEntity.status(500).body(error);
}
}
/**
* 加载并解析JSON文件
*/
@PostMapping("/load")
public ResponseEntity<Map<String, Object>> loadFile(@RequestBody Map<String, String> request,
javax.servlet.http.HttpSession session) {
try {
String filePath = request.get("filePath");
if (filePath == null || filePath.isEmpty()) {
throw new IllegalArgumentException("文件路径不能为空");
}
// 创建新的Parser实例(避免多用户数据共享)
ChromosomeParser parser = new ChromosomeParser();
// 解析数据
parser.loadFromFile(filePath);
// 获取基本信息和指标
Map<String, Object> basicInfo = parser.getBasicInfo();
Map<String, Object> metrics = parser.calculateMetrics();
String summary = parser.generateSummary();
// 将Parser实例存储到Session中,供后续操作使用
Map<String, Object> sessionData = getSessionData(session);
sessionData.put("parser", parser); // ← 存储Parser实例
sessionData.put("filePath", filePath);
sessionData.put("summary", summary);
sessionData.put("basicInfo", basicInfo);
sessionData.put("metrics", metrics);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("basicInfo", basicInfo);
response.put("metrics", metrics);
response.put("message", "数据加载成功");
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> error = new HashMap<>();
error.put("success", false);
error.put("message", "数据加载失败: " + e.getMessage());
return ResponseEntity.status(500).body(error);
}
}
/**
* 加载并解析JSON文件
*/
private Map<String, Object> loadChromosomeFile(String sceneId,javax.servlet.http.HttpSession session) {
try {
if (sceneId == null || sceneId.isEmpty()) {
throw new IllegalArgumentException("场景ID不能为空");
}
File file =sceneService.getCurrentChromosomeFile(sceneId);
String filePath = file.getPath();
if (filePath == null || filePath.isEmpty()) {
throw new IllegalArgumentException("文件路径不能为空");
}
// 创建新的Parser实例(避免多用户数据共享)
ChromosomeParser parser = new ChromosomeParser();
// 解析数据
parser.loadFromFile(filePath);
// 获取基本信息和指标
Map<String, Object> basicInfo = parser.getBasicInfo();
Map<String, Object> metrics = parser.calculateMetrics();
String summary = parser.generateSummary();
// 将Parser实例存储到Session中,供后续操作使用
Map<String, Object> sessionData = getSessionData(session);
sessionData.put("parser", parser); // ← 存储Parser实例
sessionData.put("filePath", filePath);
sessionData.put("summary", summary);
sessionData.put("basicInfo", basicInfo);
sessionData.put("metrics", metrics);
return sessionData;
} catch (Exception e) {
Map<String, Object> error = new HashMap<>();
error.put("success", false);
error.put("message", "数据加载失败: " + e.getMessage());
return error;
}
}
/**
* 执行AI诊断
*/
@PostMapping("/diagnoseChromosome")
public ResponseEntity<Map<String, Object>> diagnoseChromosome(@RequestBody Map<String, Object> params,javax.servlet.http.HttpSession session) {
try {
// 从参数中获取sceneId
String sceneId = (String) params.get("sceneId");
// 校验sceneId是否存在
if (sceneId == null || sceneId.isEmpty()) {
throw new IllegalArgumentException("场景ID不能为空");
}
Map<String, Object> sessionData = loadChromosomeFile(sceneId,session);
if (!sessionData.containsKey("summary")) {
throw new IllegalStateException("请先加载数据");
}
// 从Session中获取Parser实例
ChromosomeParser parser = (ChromosomeParser) sessionData.get("parser");
if (parser == null) {
throw new IllegalStateException("解析器未初始化,请重新加载数据");
}
String summary = (String) sessionData.get("summary");
// 调用AI诊断
String rulesContext = "无特定规则约束。\n\n" +
"请作为APS(高级计划与排程)系统专家,基于您的专业知识对以下排产方案进行自由诊断分析。\n\n" +
"您可以从以下角度进行分析:\n" +
"- 整体评估\n" +
"- 瓶颈识别\n" +
"- 效率分析\n" +
"- 优化建议\n" +
"- 风险评估\n";
List<String> diagnosisFocus = Arrays.asList("全面分析");
String report = llmClient.diagnoseSchedule(summary, rulesContext, diagnosisFocus);
// 保存诊断报告(使用Session隔离)
sessionData.put("diagnosisReport", report);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("report", report);
response.put("message", "诊断完成");
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> error = new HashMap<>();
error.put("success", false);
error.put("message", "诊断失败: " + e.getMessage());
e.printStackTrace();
return ResponseEntity.status(500).body(error);
}
}
/**
* 执行AI诊断
*/
@PostMapping("/diagnose")
public ResponseEntity<Map<String, Object>> diagnose(javax.servlet.http.HttpSession session) {
try {
Map<String, Object> sessionData = getSessionData(session);
if (!sessionData.containsKey("summary")) {
throw new IllegalStateException("请先加载数据");
}
// 从Session中获取Parser实例
ChromosomeParser parser = (ChromosomeParser) sessionData.get("parser");
if (parser == null) {
throw new IllegalStateException("解析器未初始化,请重新加载数据");
}
String summary = (String) sessionData.get("summary");
// 调用AI诊断
String rulesContext = "无特定规则约束。\n\n" +
"请作为APS(高级计划与排程)系统专家,基于您的专业知识对以下排产方案进行自由诊断分析。\n\n" +
"您可以从以下角度进行分析:\n" +
"- 整体评估\n" +
"- 瓶颈识别\n" +
"- 效率分析\n" +
"- 优化建议\n" +
"- 风险评估\n";
List<String> diagnosisFocus = Arrays.asList("全面分析");
String report = llmClient.diagnoseSchedule(summary, rulesContext, diagnosisFocus);
// 保存诊断报告(使用Session隔离)
sessionData.put("diagnosisReport", report);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("report", report);
response.put("message", "诊断完成");
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> error = new HashMap<>();
error.put("success", false);
error.put("message", "诊断失败: " + e.getMessage());
e.printStackTrace();
return ResponseEntity.status(500).body(error);
}
}
/**
* 交互式提问
*/
@PostMapping("/ask")
public ResponseEntity<Map<String, Object>> askQuestion(@RequestBody Map<String, String> request,
javax.servlet.http.HttpSession session) {
try {
String question = request.get("question");
if (question == null || question.isEmpty()) {
throw new IllegalArgumentException("问题不能为空");
}
// 获取会话数据和对话历史(使用Session隔离)
Map<String, Object> sessionData = getSessionData(session);
List<Map<String, String>> conversationHistory = getConversationHistory(session);
// 添加用户问题到对话历史
conversationHistory.add(createMessage("user", question));
String answer;
// 如果有加载数据,使用上下文信息;否则进行通用对话
if (sessionData.containsKey("summary")) {
String contextInfo = (String) sessionData.get("summary");
answer = llmClient.interactiveChat(conversationHistory, contextInfo);
} else {
// 没有加载数据时,进行通用APS对话
answer = llmClient.generalChat(conversationHistory);
}
// 添加AI回答到对话历史
conversationHistory.add(createMessage("assistant", answer));
// 限制对话历史长度,避免超出token限制(保留最近10轮对话)
if (conversationHistory.size() > 20) {
conversationHistory = conversationHistory.subList(conversationHistory.size() - 20, conversationHistory.size());
}
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("answer", answer);
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> error = new HashMap<>();
error.put("success", false);
error.put("message", "回答失败: " + e.getMessage());
return ResponseEntity.status(500).body(error);
}
}
/**
* 清空对话历史
*/
@PostMapping("/clear-chat")
public ResponseEntity<Map<String, Object>> clearChat(javax.servlet.http.HttpSession session) {
// 使用Session隔离,只清空当前用户的对话历史
List<Map<String, String>> conversationHistory = getConversationHistory(session);
conversationHistory.clear();
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "对话历史已清空");
return ResponseEntity.ok(response);
}
/**
* 优化排产方案
*/
@PostMapping("/optimize")
public ResponseEntity<Map<String, Object>> optimize(javax.servlet.http.HttpSession session) {
try {
Map<String, Object> sessionData = getSessionData(session);
if (!sessionData.containsKey("summary")) {
throw new IllegalStateException("请先加载数据");
}
// 从Session中获取Parser实例
ChromosomeParser parser = (ChromosomeParser) sessionData.get("parser");
if (parser == null) {
throw new IllegalStateException("解析器未初始化,请重新加载数据");
}
String summary = (String) sessionData.get("summary");
String filePath = (String) sessionData.get("filePath");
// 获取当前解析器数据
Map<String, Object> basicInfo = parser.getBasicInfo();
Map<String, Object> metrics = parser.calculateMetrics();
Map<Integer, ChromosomeParser.MachineStats> oldStats = parser.getMachineUsageStats();
// 提取旧负载数据
List<Integer> oldLoads = oldStats.values().stream()
.map(ChromosomeParser.MachineStats::getCount)
.collect(Collectors.toList());
// 第1步:让AI分析并给出优化策略和参数建议
String optimizationPrompt = buildOptimizationPrompt(summary, basicInfo, metrics, oldLoads);
List<Map<String, String>> messages = new ArrayList<>();
messages.add(createMessage("system", "你是一个专业的APS排产优化专家,擅长分析和优化生产调度方案。"));
messages.add(createMessage("user", optimizationPrompt));
String optimizationAdvice = llmClient.chatCompletion(messages, 0.3, 2500, null);
// 第2步:从AI建议中提取优化参数
OptimizationParams params = extractOptimizationParams(optimizationAdvice, oldLoads);
// 第3步:根据AI建议的参数执行优化算法
List<Integer> newMachineSelection = performLoadBalancingOptimization(parser, params);
List<Integer> newOperationSequence = optimizeOperationSequence(parser, params);
// 验证优化方案
if (!validateOptimization(parser, newMachineSelection)) {
throw new RuntimeException("优化方案验证失败");
}
// 保存优化后的文件
String outputFilePath = saveOptimizedFile(parser, filePath, newMachineSelection, newOperationSequence, optimizationAdvice);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "优化完成");
response.put("outputFile", outputFilePath);
response.put("optimizationAdvice", optimizationAdvice);
response.put("newMachineSelection", newMachineSelection);
response.put("optimizationStrategy", params.getStrategy());
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> error = new HashMap<>();
error.put("success", false);
error.put("message", "优化失败: " + e.getMessage());
e.printStackTrace();
return ResponseEntity.status(500).body(error);
}
}
/**
* 构建优化提示词
*/
private String buildOptimizationPrompt(String summary, Map<String, Object> basicInfo,
Map<String, Object> metrics, List<Integer> oldLoads) {
StringBuilder prompt = new StringBuilder();
prompt.append("你是APS(高级计划与排程)系统专家。\n\n");
prompt.append("## 当前排产方案数据摘要\n");
prompt.append(summary).append("\n\n");
prompt.append("## 任务要求\n");
prompt.append("请分析当前排产方案的问题,并给出具体的优化策略和参数建议。\n\n");
prompt.append("### 当前问题\n");
prompt.append(String.format("- 工序总数: %s\n", basicInfo.get("operationCount")));
prompt.append(String.format("- 使用机器数: %s\n", metrics.get("totalMachinesUsed")));
if (!oldLoads.isEmpty()) {
prompt.append(String.format("- 最大负载: %d 工序\n", Collections.max(oldLoads)));
prompt.append(String.format("- 最小负载: %d 工序\n", Collections.min(oldLoads)));
int avgLoad = oldLoads.stream().mapToInt(Integer::intValue).sum() / oldLoads.size();
prompt.append(String.format("- 平均负载: %d 工序\n", avgLoad));
}
prompt.append("\n### 请给出以下优化建议\n");
prompt.append("1. **优化策略**: 从以下策略中选择最适合的一个:\n");
prompt.append(" - BALANCED: 负载均衡优先(适合负载不均的情况)\n");
prompt.append(" - EFFICIENCY: 效率优先(适合追求最短完工时间)\n");
prompt.append(" - HYBRID: 混合策略(平衡负载和效率)\n\n");
prompt.append("2. **负载均衡权重** (0-1之间): \n");
prompt.append(" - 如果选择BALANCED策略,建议0.8-1.0\n");
prompt.append(" - 如果选择EFFICIENCY策略,建议0.3-0.5\n");
prompt.append(" - 如果选择HYBRID策略,建议0.6-0.7\n\n");
prompt.append("3. **工序排序策略**: \n");
prompt.append(" - SPT: 最短处理时间优先\n");
prompt.append(" - LPT: 最长处理时间优先\n");
prompt.append(" - EDD: 最早交期优先\n");
prompt.append(" - FCFS: 先来先服务(保持原序)\n\n");
prompt.append("4. **具体操作步骤**: 详细说明应该如何优化\n\n");
prompt.append("### 输出格式\n");
prompt.append("请用中文详细说明优化策略,包括:\n");
prompt.append("- 当前存在的主要问题\n");
prompt.append("- 推荐的优化策略及原因\n");
prompt.append("- 预期的改善效果\n\n");
prompt.append("**重要**: 请在回答中明确说明你选择的策略、权重和排序方法。");
return prompt.toString();
}
/**
* 执行负载均衡优化
*/
private List<Integer> performLoadBalancingOptimization(ChromosomeParser parser, OptimizationParams params) {
List<Operation> operations = parser.getGlobalOperations();
// 构建机器可选映射
Map<Integer, List<MachineOption>> machineOptionsMap = new HashMap<>();
for (int i = 0; i < operations.size(); i++) {
Operation op = operations.get(i);
if (op.getMachineOptions() != null) {
machineOptionsMap.put(i, op.getMachineOptions());
}
}
// 统计每台机器的可用次数
Map<Integer, Integer> machineAvailability = new HashMap<>();
for (List<MachineOption> options : machineOptionsMap.values()) {
for (MachineOption option : options) {
Integer machineId = option.getMachineId();
if (machineId != null) {
machineAvailability.put(machineId,
machineAvailability.getOrDefault(machineId, 0) + 1);
}
}
}
// 根据策略调整优化目标
Map<Integer, Integer> machineLoad = new HashMap<>();
for (Integer machineId : machineAvailability.keySet()) {
machineLoad.put(machineId, 0);
}
List<Integer> newMachineSelection = new ArrayList<>();
double balanceWeight = params.getBalanceWeight();
for (int i = 0; i < operations.size(); i++) {
List<MachineOption> options = machineOptionsMap.get(i);
if (options == null || options.isEmpty()) {
newMachineSelection.add(1); // 默认选择第一个
continue;
}
// 根据平衡权重计算得分
int bestOptionIndex = 1;
double bestScore = Double.MAX_VALUE;
for (int j = 0; j < options.size(); j++) {
Integer machineId = options.get(j).getMachineId();
if (machineId != null) {
int currentLoad = machineLoad.getOrDefault(machineId, 0);
// 综合得分 = 负载得分 * 权重 + 效率得分 * (1-权重)
double loadScore = currentLoad * balanceWeight;
// 使用工序的runtime作为效率指标(如果没有则用默认值1.0)
Operation currentOp = operations.get(i);
double runtime = currentOp.getRuntime() != null ? currentOp.getRuntime() : 1.0;
double efficiencyScore = runtime * (1 - balanceWeight);
double totalScore = loadScore + efficiencyScore;
if (totalScore < bestScore) {
bestScore = totalScore;
bestOptionIndex = j + 1; // 从1开始计数
}
}
}
newMachineSelection.add(bestOptionIndex);
// 更新机器负载
Integer selectedMachineId = options.get(bestOptionIndex - 1).getMachineId();
if (selectedMachineId != null) {
machineLoad.put(selectedMachineId, machineLoad.get(selectedMachineId) + 1);
}
}
return newMachineSelection;
}
/**
* 优化工序排序
*/
private List<Integer> optimizeOperationSequence(ChromosomeParser parser, OptimizationParams params) {
List<Operation> operations = parser.getGlobalOperations();
String strategy = params.getSequencingStrategy();
// 如果选择FCFS,保持原序
if ("FCFS".equals(strategy)) {
return parser.getRawData().getOperationSequencing();
}
// 创建索引列表
List<Integer> indices = new ArrayList<>();
for (int i = 0; i < operations.size(); i++) {
indices.add(i);
}
// 根据策略排序
switch (strategy) {
case "SPT": // 最短处理时间优先
indices.sort((a, b) -> {
double timeA = getProcessingTime(operations.get(a));
double timeB = getProcessingTime(operations.get(b));
return Double.compare(timeA, timeB);
});
break;
case "LPT": // 最长处理时间优先
indices.sort((a, b) -> {
double timeA = getProcessingTime(operations.get(a));
double timeB = getProcessingTime(operations.get(b));
return Double.compare(timeB, timeA);
});
break;
case "EDD": // 最早交期优先(使用订单编码排序作为替代)
indices.sort((a, b) -> {
String orderA = operations.get(a).getOrderCode();
String orderB = operations.get(b).getOrderCode();
if (orderA == null) return 1;
if (orderB == null) return -1;
return orderA.compareTo(orderB);
});
break;
default:
// 默认保持原序
return parser.getRawData().getOperationSequencing();
}
// 将索引转换为工序ID
List<Integer> newSequence = new ArrayList<>();
for (Integer index : indices) {
Operation op = operations.get(index);
if (op.getId() != null) {
newSequence.add(op.getId());
}
}
return newSequence;
}
/**
* 获取工序的处理时间
*/
private double getProcessingTime(Operation operation) {
// 使用runtime作为处理时间
if (operation.getRuntime() != null) {
return operation.getRuntime();
}
return 1.0; // 默认值
}
/**
* 验证优化方案
*/
private boolean validateOptimization(ChromosomeParser parser, List<Integer> newMachineSelection) {
List<Operation> operations = parser.getGlobalOperations();
// 检查长度
if (newMachineSelection.size() != operations.size()) {
return false;
}
// 检查值范围
for (int i = 0; i < newMachineSelection.size(); i++) {
Integer selectionValue = newMachineSelection.get(i);
Operation op = operations.get(i);
if (op.getMachineOptions() != null) {
int optionCount = op.getMachineOptions().size();
if (selectionValue < 1 || selectionValue > optionCount) {
return false;
}
}
}
return true;
}
/**
* 验证优化后数据的完整性
*/
private void validateOptimizationData(ChromosomeParser parser, ChromosomeData originalData,
List<Integer> newMachineSelection, List<Integer> newOperationSequence) {
System.out.println("\n========== 优化数据完整性验证 ==========");
// 1. 验证工序数量
int originalOpCount = originalData.getGlobalOpList() != null ? originalData.getGlobalOpList().size() : 0;
int optimizedOpCount = parser.getGlobalOperations().size();
System.out.println(String.format("✓ 原始工序数: %d", originalOpCount));
System.out.println(String.format("✓ 优化后工序数: %d", optimizedOpCount));
if (originalOpCount != optimizedOpCount) {
throw new RuntimeException(String.format("工序数量不匹配!原始: %d, 优化后: %d", originalOpCount, optimizedOpCount));
}
// 2. 验证机器选择数组长度
int machineSelectionSize = newMachineSelection.size();
System.out.println(String.format("✓ 机器选择数组长度: %d", machineSelectionSize));
if (machineSelectionSize != optimizedOpCount) {
throw new RuntimeException(String.format("机器选择数组长度不匹配!期望: %d, 实际: %d", optimizedOpCount, machineSelectionSize));
}
// 3. 验证工序排序数组长度
int sequenceSize = newOperationSequence.size();
System.out.println(String.format("✓ 工序排序数组长度: %d", sequenceSize));
if (sequenceSize != optimizedOpCount) {
throw new RuntimeException(String.format("工序排序数组长度不匹配!期望: %d, 实际: %d", optimizedOpCount, sequenceSize));
}
// 4. 验证机器选择的合法性
List<Operation> operations = parser.getGlobalOperations();
int invalidSelections = 0;
for (int i = 0; i < newMachineSelection.size(); i++) {
Integer selectionValue = newMachineSelection.get(i);
Operation op = operations.get(i);
if (op.getMachineOptions() != null && !op.getMachineOptions().isEmpty()) {
int optionCount = op.getMachineOptions().size();
if (selectionValue < 1 || selectionValue > optionCount) {
invalidSelections++;
System.err.println(String.format("✗ 工序%d的机器选择无效: %d (可选范围: 1-%d)",
op.getId(), selectionValue, optionCount));
}
}
}
if (invalidSelections > 0) {
throw new RuntimeException(String.format("发现%d个无效的机器选择!", invalidSelections));
}
System.out.println("✓ 所有机器选择均合法");
// 5. 验证工序ID的唯一性
Set<Integer> uniqueIds = new HashSet<>(newOperationSequence);
if (uniqueIds.size() != newOperationSequence.size()) {
throw new RuntimeException("工序排序中存在重复的工序ID!");
}
System.out.println("✓ 工序排序中无重复ID");
// 6. 统计信息
System.out.println(String.format("\n========== 优化效果统计 =========="));
System.out.println(String.format("• 总工序数: %d", optimizedOpCount));
System.out.println(String.format("• 优化前版本: %d", originalData.getVersion()));
System.out.println(String.format("• 优化后版本: %d", originalData.getVersion() + 1));
System.out.println("==========================================\n");
}
/**
* 保存优化后的文件
*/
private String saveOptimizedFile(ChromosomeParser parser, String originalFilePath, List<Integer> newMachineSelection,
List<Integer> newOperationSequence, String optimizationAdvice) throws Exception {
// 读取原始数据
ChromosomeData originalData = parser.getRawData();
// 生成输出文件名
String baseName = originalFilePath;
if (baseName.contains(".")) {
baseName = baseName.substring(0, baseName.lastIndexOf("."));
}
String outputFilePath = baseName + "_optimized.json";
// 读取原始文件的完整JSON内容(保留所有字段)
com.fasterxml.jackson.databind.ObjectMapper objectMapper = new com.fasterxml.jackson.databind.ObjectMapper();
com.fasterxml.jackson.databind.JsonNode rootNode = objectMapper.readTree(new File(originalFilePath));
// 创建可变的ObjectNode
com.fasterxml.jackson.databind.node.ObjectNode objectNode = (com.fasterxml.jackson.databind.node.ObjectNode) rootNode;
// 只更新需要修改的字段(纯排产数据,不包含AI建议)
objectNode.set("machineSelection", objectMapper.valueToTree(newMachineSelection));
objectNode.set("operationSequencing", objectMapper.valueToTree(newOperationSequence));
// 版本号+1
Integer version = objectNode.has("version") ? objectNode.get("version").asInt() : 0;
objectNode.put("version", version + 1);
// 添加AI优化标记(仅标记是否经过AI优化,不保存建议文本)
objectNode.put("optimized_by_ai", true);
// 写入文件(不使用pretty printer,保持与原始文件相同的紧凑格式)
objectMapper.writeValue(new File(outputFilePath), objectNode);
// 验证数据完整性
validateOptimizationData(parser, originalData, newMachineSelection, newOperationSequence);
// 打印文件大小对比
File originalFile = new File(originalFilePath);
File optimizedFile = new File(outputFilePath);
long originalSize = originalFile.length();
long optimizedSize = optimizedFile.length();
double ratio = (double)optimizedSize / originalSize * 100;
System.out.println(String.format("\n========== 文件大小对比 =========="));
System.out.println(String.format("• 原始文件大小: %.2f MB (%d bytes)", originalSize / (1024.0 * 1024.0), originalSize));
System.out.println(String.format("• 优化后文件大小: %.2f MB (%d bytes)", optimizedSize / (1024.0 * 1024.0), optimizedSize));
System.out.println(String.format("• 大小比例: %.2f%%", ratio));
System.out.println(String.format("• 差异: %+.2f MB (%+.2f%%)",
(optimizedSize - originalSize) / (1024.0 * 1024.0), ratio - 100));
System.out.println("==========================================\n");
return outputFilePath;
}
/**
* 辅助方法:创建消息对象
*/
private Map<String, String> createMessage(String role, String content) {
Map<String, String> message = new HashMap<>();
message.put("role", role);
message.put("content", content);
return message;
}
/**
* 优化参数类
*/
private static class OptimizationParams {
private String strategy = "HYBRID"; // BALANCED, EFFICIENCY, HYBRID
private double balanceWeight = 0.7; // 负载均衡权重 0-1
private String sequencingStrategy = "FCFS"; // SPT, LPT, EDD, FCFS
public String getStrategy() { return strategy; }
public void setStrategy(String strategy) { this.strategy = strategy; }
public double getBalanceWeight() { return balanceWeight; }
public void setBalanceWeight(double balanceWeight) { this.balanceWeight = balanceWeight; }
public String getSequencingStrategy() { return sequencingStrategy; }
public void setSequencingStrategy(String sequencingStrategy) { this.sequencingStrategy = sequencingStrategy; }
}
/**
* 从AI建议中提取优化参数
*/
private OptimizationParams extractOptimizationParams(String aiAdvice, List<Integer> oldLoads) {
OptimizationParams params = new OptimizationParams();
// 默认策略:根据负载情况自动选择
if (!oldLoads.isEmpty()) {
int maxLoad = Collections.max(oldLoads);
int minLoad = Collections.min(oldLoads);
double loadVariance = (double)(maxLoad - minLoad) / maxLoad;
if (loadVariance > 0.3) {
// 负载差异大,选择均衡策略
params.setStrategy("BALANCED");
params.setBalanceWeight(0.9);
} else if (loadVariance < 0.1) {
// 负载已经很均衡,选择效率策略
params.setStrategy("EFFICIENCY");
params.setBalanceWeight(0.4);
} else {
// 中等差异,混合策略
params.setStrategy("HYBRID");
params.setBalanceWeight(0.65);
}
}
// 尝试从AI建议中解析策略(简单关键词匹配)
String adviceLower = aiAdvice.toLowerCase();
if (adviceLower.contains("balanced") || adviceLower.contains("均衡")) {
params.setStrategy("BALANCED");
params.setBalanceWeight(0.85);
} else if (adviceLower.contains("efficiency") || adviceLower.contains("效率")) {
params.setStrategy("EFFICIENCY");
params.setBalanceWeight(0.4);
} else if (adviceLower.contains("hybrid") || adviceLower.contains("混合")) {
params.setStrategy("HYBRID");
params.setBalanceWeight(0.65);
}
// 尝试解析工序排序策略
if (adviceLower.contains("spt") || adviceLower.contains("最短处理时间")) {
params.setSequencingStrategy("SPT");
} else if (adviceLower.contains("lpt") || adviceLower.contains("最长处理时间")) {
params.setSequencingStrategy("LPT");
} else if (adviceLower.contains("edd") || adviceLower.contains("最早交期")) {
params.setSequencingStrategy("EDD");
}
return params;
}
/**
* 下载优化后的文件
*/
@GetMapping("/download")
public ResponseEntity<Resource> downloadFile(@RequestParam String filePath) {
try {
File file = new File(filePath);
if (!file.exists()) {
return ResponseEntity.notFound().build();
}
Resource resource = new FileSystemResource(file);
String fileName = file.getName();
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"")
.contentType(MediaType.APPLICATION_JSON)
.body(resource);
} catch (Exception e) {
e.printStackTrace();
return ResponseEntity.status(500).build();
}
}
}
package com.aps.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
/**
* 页面控制器
*/
@Controller
public class PageController {
/**
* 首页
*/
@GetMapping("/")
public String index() {
return "index";
}
}
package com.aps.llm;
import com.aps.config.LLMConfig;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 大语言模型客户端
*/
@Component
public class LLMClient {
@Autowired
private LLMConfig config;
private final ObjectMapper objectMapper = new ObjectMapper();
/**
* 调用聊天完成API
*/
public String chatCompletion(List<Map<String, String>> messages) throws Exception {
return chatCompletion(messages, null, null, null);
}
/**
* 调用聊天完成API(带自定义参数)
*/
public String chatCompletion(List<Map<String, String>> messages,
Double temperature,
Integer maxTokens,
String model) throws Exception {
CloseableHttpClient httpClient = HttpClients.createDefault();
try {
// 构建请求体
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("model", model != null ? model : config.getModel());
requestBody.put("messages", messages);
requestBody.put("temperature", temperature != null ? temperature : config.getTemperature());
requestBody.put("max_tokens", maxTokens != null ? maxTokens : config.getMaxTokens());
String jsonBody = objectMapper.writeValueAsString(requestBody);
// 创建HTTP POST请求
HttpPost httpPost = new HttpPost(config.getBaseUrl() + "/chat/completions");
httpPost.setHeader("Content-Type", "application/json");
httpPost.setHeader("Authorization", "Bearer " + config.getApiKey());
httpPost.setEntity(new StringEntity(jsonBody, StandardCharsets.UTF_8));
// 设置超时
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(30000)
.setSocketTimeout(60000)
.build();
httpPost.setConfig(requestConfig);
// 执行请求
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
int statusCode = response.getStatusLine().getStatusCode();
String responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
if (statusCode != 200) {
throw new RuntimeException("API调用失败: HTTP " + statusCode + ", 响应: " + responseBody);
}
// 解析响应
JsonNode rootNode = objectMapper.readTree(responseBody);
JsonNode choicesNode = rootNode.path("choices");
if (choicesNode.isArray() && choicesNode.size() > 0) {
return choicesNode.get(0).path("message").path("content").asText();
} else {
throw new RuntimeException("API响应格式错误: " + responseBody);
}
}
} finally {
httpClient.close();
}
}
/**
* 诊断排产计划
*/
public String diagnoseSchedule(String scheduleSummary,
String rulesContext,
List<String> diagnosisFocus) throws Exception {
StringBuilder focusText = new StringBuilder();
if (diagnosisFocus != null && !diagnosisFocus.isEmpty()) {
focusText.append("\n\n请重点关注以下方面:\n");
for (String focus : diagnosisFocus) {
focusText.append("- ").append(focus).append("\n");
}
}
String systemPrompt = buildSystemPrompt(rulesContext, focusText.toString());
List<Map<String, String>> messages = new ArrayList<>();
messages.add(createMessage("system", systemPrompt));
messages.add(createMessage("user", "请诊断以下排产计划:\n\n" + scheduleSummary));
return chatCompletion(messages);
}
/**
* 构建系统提示词
*/
private String buildSystemPrompt(String rulesContext, String focusText) {
return String.format(
"你是一个APS(高级计划与排程)系统专家,需要用通俗易懂的语言帮助用户理解排产方案。\n\n" +
"## 你的任务\n" +
"基于提供的排产计划数据,进行诊断分析,用简单明了的语言指出问题、瓶颈和优化机会。\n\n" +
"## 重要要求:使用中文术语\n" +
"在诊断报告中,所有技术参数必须使用中文名称,禁止直接使用英文变量名!\n\n" +
"### 术语对照表(请使用左侧中文名称)\n" +
"machineSelection = 机器分配方案\n" +
"operationSequencing = 工序排序方案\n" +
"globalOpList = 工序详细信息列表\n" +
"machineOptions = 可选机器列表\n" +
"machineId = 机器ID\n" +
"equipName = 设备名称\n" +
"orderCode = 订单编码\n" +
"productName = 产品名称\n" +
"quantity = 生产数量\n" +
"runtime = 运行时间\n" +
"singleOut = 单件产出\n" +
"Makespan = 最大完工时间\n" +
"Load Balance = 负载均衡\n" +
"Bottleneck = 瓶颈资源\n\n" +
"## 输出格式要求(非常重要)\n" +
"1. 使用简洁的段落,避免过多符号和复杂格式\n" +
"2. 使用简单的编号(1. 2. 3.),不要用复杂的标记符号\n" +
"3. 语言要通俗易懂,就像跟普通工人解释一样\n" +
"4. 避免使用Markdown的复杂语法(如加粗、表格等)\n" +
"5. 每个要点独立成段,清晰明了\n" +
"6. 不要使用✅等特殊符号\n\n" +
"## Chromosome数据结构说明\n" +
"本次诊断的数据采用Chromosome双层编码格式:\n" +
"1. 机器分配方案:每个位置表示对应工序选择第几个可选机器。例如:[1, 1, 1, ...] 表示所有工序都选择第1个可选机器\n" +
"2. 工序排序方案:表示工序的加工优先级顺序。例如:[2534, 587, 1690, ...] 表示优先加工工序2534,其次587,然后1690\n\n" +
"## 诊断内容要求\n" +
"请按以下结构输出诊断结果:\n\n" +
"一、整体评价\n" +
"用2-3句话简单说明这个排产方案怎么样,打分(满分100分)\n\n" +
"二、主要优点\n" +
"列出2-3个做得好的地方\n\n" +
"三、存在的问题\n" +
"列出2-4个需要注意的问题,用通俗语言解释\n\n" +
"四、优化建议\n" +
"给出3-5条具体可行的改进建议,每条建议简单说明怎么做\n\n" +
"五、总结\n" +
"用1-2句话总结,告诉用户这个方案是否可以接受,是否需要优化\n\n" +
"## 注意事项\n" +
"1. 不要误判未分配机器:机器分配方案中所有值都是有效的机器索引\n" +
"2. 关注负载均衡:如果所有工序都分配到同一台机器,这是严重问题\n" +
"3. 基于数据说话:引用具体的工序数、机器数、负载统计等数据\n" +
"4. 严格使用中文术语:所有技术参数必须使用上述对照表中的中文名称\n" +
"5. 语言要通俗易懂,避免专业术语堆砌\n" +
"6. 格式要简洁美观,方便阅读\n\n" +
"请基于数据进行客观分析,给出专业但易懂的建议。\n" +
"%s",
rulesContext
);
}
/**
* 创建消息对象
*/
private Map<String, String> createMessage(String role, String content) {
Map<String, String> message = new HashMap<>();
message.put("role", role);
message.put("content", content);
return message;
}
/**
* 交互式对话
*/
public String interactiveChat(List<Map<String, String>> conversationHistory,
String contextInfo) throws Exception {
String systemPrompt = "你是一个APS排产诊断助手,帮助用户分析和优化排产方案。\n\n" +
"## 你的能力\n" +
"1. 诊断排产计划的问题和瓶颈\n" +
"2. 提供优化建议和改进方案\n" +
"3. 处理机器检修等突发情况\n" +
"4. 对比不同排产方案\n" +
"5. 回答用户关于排产的疑问\n\n" +
"## 当前上下文\n" +
contextInfo + "\n\n" +
"## 对话原则\n" +
"- 基于数据和规则进行分析\n" +
"- 给出具体、可操作的建议\n" +
"- 保持专业和友好的语气\n" +
"- 如果信息不足,主动询问用户\n";
List<Map<String, String>> messages = new ArrayList<>();
messages.add(createMessage("system", systemPrompt));
messages.addAll(conversationHistory);
return chatCompletion(messages);
}
/**
* 通用对话(无需加载数据)
*/
public String generalChat(List<Map<String, String>> conversationHistory) throws Exception {
String systemPrompt = "你是一个专业的APS(高级计划与排程)系统专家助手。\n\n" +
"## 你的角色\n" +
"你可以帮助用户解答关于APS排产系统的各种问题,包括:\n" +
"1. APS系统的基本概念和原理\n" +
"2. 生产排程的优化方法\n" +
"3. 机器调度和资源分配策略\n" +
"4. 工序排序的最佳实践\n" +
"5. 负载均衡和瓶颈分析\n" +
"6. 排产方案的评估标准\n\n" +
"## 对话原则\n" +
"- 用通俗易懂的语言解释专业概念\n" +
"- 给出具体、实用的建议\n" +
"- 保持专业和友好的语气\n" +
"- 如果问题超出APS领域,礼貌地告知用户\n" +
"- 鼓励用户提供更多背景信息以便给出更精准的建议\n";
List<Map<String, String>> messages = new ArrayList<>();
messages.add(createMessage("system", systemPrompt));
messages.addAll(conversationHistory);
return chatCompletion(messages);
}
}
package com.aps.model;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
import java.util.Map;
/**
* Chromosome排产方案数据模型
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class ChromosomeData {
/**
* 生成类型
*/
@JsonProperty("generateType")
private String generateType;
/**
* 版本号
*/
@JsonProperty("version")
private Integer version;
/**
* 机器分配方案 - 每个位置表示对应工序选择第几个可选机器(从1开始)
*/
@JsonProperty("machineSelection")
private List<Integer> machineSelection;
/**
* 工序排序方案 - 表示工序的加工优先级顺序
*/
@JsonProperty("operationSequencing")
private List<Integer> operationSequencing;
/**
* 全局工序列表
*/
@JsonProperty("globalOpList")
private List<OperationInfo> globalOpList;
/**
* AI优化标记
*/
@JsonProperty("optimized_by_ai")
private Boolean optimizedByAi;
/**
* AI优化建议
*/
@JsonProperty("optimization_advice")
private String optimizationAdvice;
/**
* 其他扩展字段
*/
private Map<String, Object> additionalProperties;
}
package com.aps.model;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 机器选项
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class MachineOption {
/**
* 机器ID
*/
@JsonProperty("machineId")
private Integer machineId;
/**
* 设备名称
*/
@JsonProperty("equipName")
private String equipName;
}
package com.aps.model;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
/**
* 工序详细信息
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class Operation {
/**
* 工序ID
*/
@JsonProperty("id")
private Integer id;
/**
* 订单编码
*/
@JsonProperty("orderCode")
private String orderCode;
/**
* 订单ID
*/
@JsonProperty("orderId")
private String orderId;
/**
* 产品名称
*/
@JsonProperty("productName")
private String productName;
/**
* 生产数量
*/
@JsonProperty("quantity")
private Integer quantity;
/**
* 运行时间
*/
@JsonProperty("runtime")
private Double runtime;
/**
* 单件产出
*/
@JsonProperty("singleOut")
private Double singleOut;
/**
* 可选机器列表
*/
@JsonProperty("machineOptions")
private List<MachineOption> machineOptions;
}
package com.aps.model;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 工序信息
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class OperationInfo {
/**
* 工序详细信息
*/
@JsonProperty("op")
private Operation op;
}
package com.aps.parser;
import com.aps.model.ChromosomeData;
import com.aps.model.MachineOption;
import com.aps.model.Operation;
import com.aps.model.OperationInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Component;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Chromosome排产方案解析器
*/
@Component
public class ChromosomeParser {
private ChromosomeData rawData;
private String filePath;
private final ObjectMapper objectMapper = new ObjectMapper();
/**
* 从文件加载数据
*/
public ChromosomeData loadFromFile(String filePath) throws IOException {
File file = new File(filePath);
if (!file.exists()) {
throw new IOException("文件不存在: " + filePath);
}
this.rawData = objectMapper.readValue(file, ChromosomeData.class);
this.filePath = filePath;
return this.rawData;
}
/**
* 获取原始数据
*/
public ChromosomeData getRawData() {
return rawData;
}
/**
* 从JSON字符串加载数据
*/
public ChromosomeData loadFromJson(String jsonContent) throws IOException {
this.rawData = objectMapper.readValue(jsonContent, ChromosomeData.class);
return this.rawData;
}
/**
* 获取基本信息
*/
public Map<String, Object> getBasicInfo() {
if (rawData == null) {
throw new IllegalStateException("未加载数据");
}
Map<String, Object> info = new HashMap<>();
info.put("generateType", rawData.getGenerateType());
info.put("version", rawData.getVersion());
info.put("operationCount", rawData.getGlobalOpList() != null ? rawData.getGlobalOpList().size() : 0);
info.put("machineSelectionLength", rawData.getMachineSelection() != null ? rawData.getMachineSelection().size() : 0);
info.put("operationSequenceLength", rawData.getOperationSequencing() != null ? rawData.getOperationSequencing().size() : 0);
return info;
}
/**
* 获取全局工序列表
*/
public List<Operation> getGlobalOperations() {
if (rawData == null || rawData.getGlobalOpList() == null) {
return Collections.emptyList();
}
return rawData.getGlobalOpList().stream()
.map(OperationInfo::getOp)
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
/**
* 获取机器使用统计
*/
public Map<Integer, MachineStats> getMachineUsageStats() {
if (rawData == null) {
throw new IllegalStateException("未加载数据");
}
List<Integer> machineSelection = rawData.getMachineSelection();
List<Operation> operations = getGlobalOperations();
Map<Integer, MachineStats> stats = new HashMap<>();
for (int i = 0; i < machineSelection.size() && i < operations.size(); i++) {
Integer selectionValue = machineSelection.get(i);
Operation op = operations.get(i);
if (op.getMachineOptions() != null && !op.getMachineOptions().isEmpty()) {
// machineSelection的值从1开始,需要转换为0-based索引
if (selectionValue >= 1 && selectionValue <= op.getMachineOptions().size()) {
int actualIndex = selectionValue - 1;
Integer machineId = op.getMachineOptions().get(actualIndex).getMachineId();
if (machineId != null) {
stats.computeIfAbsent(machineId, k -> new MachineStats())
.addOperation(op.getId());
}
}
}
}
return stats;
}
/**
* 计算关键指标
*/
public Map<String, Object> calculateMetrics() {
List<Operation> operations = getGlobalOperations();
Map<Integer, MachineStats> machineStats = getMachineUsageStats();
Map<String, Object> metrics = new HashMap<>();
metrics.put("totalOperations", operations.size());
metrics.put("uniqueOrders", getUniqueOrders().size());
metrics.put("uniqueMachines", getUniqueMachines().size());
metrics.put("totalMachinesUsed", machineStats.size());
// 计算机器负载统计
List<Integer> machineLoads = machineStats.values().stream()
.map(MachineStats::getCount)
.collect(Collectors.toList());
if (!machineLoads.isEmpty()) {
double avgLoad = machineLoads.stream().mapToInt(Integer::intValue).average().orElse(0.0);
int maxLoad = Collections.max(machineLoads);
int minLoad = Collections.min(machineLoads);
// 计算标准差
double variance = machineLoads.stream()
.mapToDouble(load -> Math.pow(load - avgLoad, 2))
.average()
.orElse(0.0);
double stdDev = Math.sqrt(variance);
// 负载均衡率
double balanceRatio = maxLoad > 0 ? (double) minLoad / maxLoad : 1.0;
metrics.put("avgMachineLoad", avgLoad);
metrics.put("machineLoadStd", stdDev);
metrics.put("maxMachineLoad", maxLoad);
metrics.put("minMachineLoad", minLoad);
metrics.put("loadBalanceRatio", balanceRatio);
}
return metrics;
}
/**
* 获取唯一订单集合
*/
public Set<String> getUniqueOrders() {
return getGlobalOperations().stream()
.map(Operation::getOrderId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
}
/**
* 获取唯一机器集合
*/
public Set<Integer> getUniqueMachines() {
return getGlobalOperations().stream()
.flatMap(op -> op.getMachineOptions() != null ? op.getMachineOptions().stream() : Stream.empty())
.map(MachineOption::getMachineId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
}
/**
* 生成数据摘要文本
*/
public String generateSummary() {
Map<String, Object> basicInfo = getBasicInfo();
Map<String, Object> metrics = calculateMetrics();
List<Integer> machineSelection = rawData.getMachineSelection();
List<Integer> operationSequence = rawData.getOperationSequencing();
StringBuilder sb = new StringBuilder();
sb.append("================================================================================\n");
sb.append("APS排产方案基本信息 (Chromosome格式)\n");
sb.append("================================================================================\n");
sb.append(String.format("生成类型: %s\n", basicInfo.get("generateType")));
sb.append(String.format("版本号: %s\n", basicInfo.get("version")));
sb.append(String.format("工序总数: %s\n", basicInfo.get("operationCount")));
sb.append(String.format("机器分配方案长度: %s\n", basicInfo.get("machineSelectionLength")));
sb.append(String.format("工序排序方案长度: %s\n", basicInfo.get("operationSequenceLength")));
sb.append("\n");
sb.append("================================================================================\n");
sb.append("关键指标\n");
sb.append("================================================================================\n");
sb.append(String.format("唯一订单数: %s\n", metrics.get("uniqueOrders")));
sb.append(String.format("唯一机器数: %s\n", metrics.get("uniqueMachines")));
sb.append(String.format("实际使用机器数: %s\n", metrics.get("totalMachinesUsed")));
if (metrics.containsKey("machineLoadStd")) {
sb.append(String.format("平均机器负载: %.2f 工序/机器\n", metrics.get("avgMachineLoad")));
sb.append(String.format("机器负载标准差: %.2f\n", metrics.get("machineLoadStd")));
sb.append(String.format("最大负载: %s 工序\n", metrics.get("maxMachineLoad")));
sb.append(String.format("最小负载: %s 工序\n", metrics.get("minMachineLoad")));
sb.append(String.format("负载均衡率: %.2f%%\n", (Double) metrics.get("loadBalanceRatio") * 100));
}
// 添加机器分配统计
Map<Integer, MachineStats> machineStats = getMachineUsageStats();
if (!machineStats.isEmpty()) {
sb.append("\n");
sb.append("================================================================================\n");
sb.append("机器分配统计\n");
sb.append("================================================================================\n");
sb.append(String.format("已分配工序的机器数: %d\n", machineStats.size()));
sb.append("\n机器使用情况 (前10台):\n");
machineStats.entrySet().stream()
.sorted((Map.Entry<Integer, MachineStats> e1, Map.Entry<Integer, MachineStats> e2) ->
e2.getValue().getCount() - e1.getValue().getCount())
.limit(10)
.forEach(entry -> {
sb.append(String.format(" - 机器ID %d: %d个工序\n", entry.getKey(), entry.getValue().getCount()));
});
}
// 添加双层编码说明
sb.append("\n");
sb.append("================================================================================\n");
sb.append("双层编码结构说明\n");
sb.append("================================================================================\n");
sb.append("1. 机器分配方案 (machineSelection): 每个位置表示对应工序选择第几个可选机器\n");
sb.append(" - 例如: [1, 2, 1, ...] 表示工序0选择第1个可选机器,工序1选择第2个可选机器...\n");
sb.append(" - 值从1开始计数,需要转换为0-based索引访问machineOptions\n");
sb.append("\n");
sb.append("2. 工序排序方案 (operationSequencing): 表示工序的加工优先级顺序\n");
if (operationSequence != null && !operationSequence.isEmpty()) {
String firstFive = operationSequence.subList(0, Math.min(5, operationSequence.size())).stream()
.map(String::valueOf)
.collect(Collectors.joining(", "));
sb.append(String.format(" - 例如: [%s, ...]\n", firstFive));
if (operationSequence.size() >= 3) {
sb.append(String.format(" - 表示优先加工工序%d, 其次工序%d, 然后工序%d, ...\n",
operationSequence.get(0), operationSequence.get(1), operationSequence.get(2)));
}
}
// 添加前10个工序样例
List<Operation> operations = getGlobalOperations();
if (!operations.isEmpty()) {
sb.append("\n");
sb.append("================================================================================\n");
sb.append("工序样例 (前10个,含机器分配)\n");
sb.append("================================================================================\n");
int limit = Math.min(10, operations.size());
for (int i = 0; i < limit; i++) {
Operation op = operations.get(i);
String assignedMachine = "未分配";
if (i < machineSelection.size()) {
Integer selectionValue = machineSelection.get(i);
if (op.getMachineOptions() != null && !op.getMachineOptions().isEmpty()
&& selectionValue >= 1 && selectionValue <= op.getMachineOptions().size()) {
int actualIndex = selectionValue - 1;
MachineOption option = op.getMachineOptions().get(actualIndex);
assignedMachine = option.getEquipName() != null ?
option.getEquipName() : "机器ID:" + option.getMachineId();
}
}
sb.append(String.format("%d. 工序ID: %d, 订单: %s, 产品: %s, 数量: %d, 分配机器: %s\n",
i + 1,
op.getId() != null ? op.getId() : 0,
op.getOrderCode() != null ? op.getOrderCode() : "N/A",
op.getProductName() != null ? op.getProductName() : "N/A",
op.getQuantity() != null ? op.getQuantity() : 0,
assignedMachine));
}
}
return sb.toString();
}
/**
* 机器统计数据内部类
*/
public static class MachineStats {
private int count = 0;
private List<Integer> operations = new ArrayList<>();
public void addOperation(Integer opId) {
count++;
operations.add(opId);
}
public int getCount() {
return count;
}
public List<Integer> getOperations() {
return operations;
}
}
}
......@@ -86,7 +86,13 @@ mybatis-plus:
id-type: auto # 主键自增策略
table-underline: true
capital-mode: false
# LLM API配置 (DeepSeek)
llm:
api-key: sk-56843e79389346dfb2f8e0ccb8c76365
base-url: https://api.deepseek.com/v1
model: deepseek-chat
temperature: 0.3
max-tokens: 4000
# 应用配置
app:
# 时区配置
......
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>APS排产方案AI诊断系统</title>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
background: white;
border-radius: 15px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
font-size: 32px;
margin-bottom: 10px;
}
.header p {
font-size: 16px;
opacity: 0.9;
}
.content {
padding: 30px;
}
.section {
margin-bottom: 30px;
padding: 20px;
border: 2px solid #e0e0e0;
border-radius: 10px;
transition: all 0.3s;
}
.section:hover {
border-color: #667eea;
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.1);
}
.section-title {
font-size: 24px;
color: #667eea;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #667eea;
}
.file-list {
display: grid;
gap: 10px;
margin-top: 15px;
}
.file-item {
padding: 15px;
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
display: flex;
justify-content: space-between;
align-items: center;
}
.file-item:hover {
background: #e9ecef;
transform: translateX(5px);
}
.file-item.selected {
background: #667eea;
color: white;
border-color: #667eea;
}
.btn {
padding: 12px 30px;
border: none;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: all 0.3s;
font-weight: bold;
margin: 5px;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.btn-success {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
color: white;
}
.btn-success:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(17, 153, 142, 0.4);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none !important;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 15px;
margin-top: 15px;
}
.info-card {
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
border-left: 4px solid #667eea;
}
.info-card label {
font-size: 12px;
color: #6c757d;
display: block;
margin-bottom: 5px;
}
.info-card value {
font-size: 20px;
font-weight: bold;
color: #495057;
}
.report-container {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
margin-top: 15px;
max-height: 500px;
overflow-y: auto;
white-space: pre-wrap;
line-height: 1.8;
font-size: 14px;
}
/* Markdown 渲染样式 */
.report-container h1,
.report-container h2,
.report-container h3 {
margin-top: 20px;
margin-bottom: 10px;
color: #333;
}
.report-container h1 {
font-size: 24px;
border-bottom: 2px solid #667eea;
padding-bottom: 10px;
}
.report-container h2 {
font-size: 20px;
color: #667eea;
}
.report-container h3 {
font-size: 18px;
}
.report-container ul,
.report-container ol {
margin-left: 20px;
margin-bottom: 15px;
}
.report-container li {
margin-bottom: 8px;
}
.report-container strong {
color: #764ba2;
}
.report-container code {
background: #e9ecef;
padding: 2px 6px;
border-radius: 4px;
font-size: 13px;
}
.report-container pre {
background: #2d2d2d;
color: #f8f8f2;
padding: 15px;
border-radius: 8px;
overflow-x: auto;
margin: 15px 0;
}
.report-container pre code {
background: none;
padding: 0;
}
.chat-container {
margin-top: 15px;
}
.chat-messages {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
min-height: 300px;
max-height: 500px;
overflow-y: auto;
margin-bottom: 15px;
}
.message {
margin-bottom: 15px;
padding: 12px;
border-radius: 8px;
animation: fadeIn 0.3s;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.message.user {
background: #667eea;
color: white;
margin-left: 50px;
}
.message.ai {
background: white;
border: 1px solid #dee2e6;
margin-right: 50px;
}
.chat-input {
display: flex;
gap: 10px;
}
.chat-input input {
flex: 1;
padding: 12px;
border: 2px solid #dee2e6;
border-radius: 8px;
font-size: 14px;
}
.chat-input input:focus {
outline: none;
border-color: #667eea;
}
.loading {
text-align: center;
padding: 20px;
color: #667eea;
}
.spinner {
border: 3px solid #f3f3f3;
border-top: 3px solid #667eea;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.alert {
padding: 15px;
border-radius: 8px;
margin-bottom: 15px;
}
.alert-success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.alert-error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.upload-area {
border: 2px dashed #667eea;
border-radius: 8px;
padding: 30px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
margin-top: 15px;
}
.upload-area:hover {
background: #f8f9fa;
border-color: #764ba2;
}
.hidden {
display: none;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🚀 APS排产方案AI诊断系统</h1>
<p>基于大语言模型的智能排产分析与优化平台</p>
</div>
<div class="content">
<!-- 文件选择区域 -->
<div class="section" id="fileSection">
<h2 class="section-title">📂 选择排产方案文件</h2>
<div id="fileAlert"></div>
<div class="upload-area" onclick="document.getElementById('fileInput').click()">
<p>📤 点击上传JSON文件或从下方列表选择</p>
<input type="file" id="fileInput" accept=".json" style="display: none;" onchange="handleFileUpload(event)">
</div>
<div id="fileListContainer" style="margin-top: 20px;">
<h3 style="margin-bottom: 10px;">可用文件列表:</h3>
<div id="fileList" class="file-list">
<div class="loading">
<div class="spinner"></div>
<p>加载中...</p>
</div>
</div>
</div>
<div style="margin-top: 20px; text-align: center;">
<button class="btn btn-primary" onclick="loadSelectedFile()" id="loadBtn" disabled>
加载选中文件
</button>
</div>
</div>
<!-- 数据信息区域 -->
<div class="section hidden" id="infoSection">
<h2 class="section-title">📊 排产方案基本信息</h2>
<div id="basicInfo" class="info-grid"></div>
<div style="margin-top: 20px; text-align: center;">
<button class="btn btn-success" onclick="startDiagnosis()" id="diagnoseBtn">
开始AI诊断
</button>
<button class="btn btn-primary" onclick="startOptimization()" id="optimizeBtn" style="display: none;">
🚀 AI智能优化排产
</button>
</div>
</div>
<!-- 诊断报告区域 -->
<div class="section hidden" id="reportSection">
<h2 class="section-title">📋 AI诊断报告</h2>
<div id="reportContent" class="report-container"></div>
<div style="margin-top: 20px; text-align: center;">
<button class="btn btn-success" onclick="downloadOptimizedFile()" id="downloadBtn" style="display: none;">
📥 下载优化后的方案
</button>
</div>
</div>
<!-- 对话区域 -->
<div class="section hidden" id="chatSection">
<h2 class="section-title">💬 与AI助手对话</h2>
<div class="chat-container">
<div id="chatMessages" class="chat-messages"></div>
<div class="chat-input">
<input type="text" id="questionInput" placeholder="输入您的问题..."
onkeypress="if(event.key==='Enter') askQuestion()">
<button class="btn btn-primary" onclick="askQuestion()">发送</button>
</div>
</div>
</div>
</div>
</div>
<script>
let selectedFile = null;
let currentFilePath = null;
let optimizedFilePath = null; // 存储优化文件路径
// 页面加载时获取文件列表
window.onload = function() {
loadFileList();
};
// 加载文件列表
async function loadFileList() {
try {
const response = await fetch('/api/files');
const data = await response.json();
if (data.success) {
displayFileList(data.files);
} else {
showAlert('fileAlert', 'error', '加载文件列表失败: ' + data.message);
}
} catch (error) {
showAlert('fileAlert', 'error', '网络错误: ' + error.message);
}
}
// 显示文件列表
function displayFileList(files) {
const fileListDiv = document.getElementById('fileList');
if (!files || files.length === 0) {
fileListDiv.innerHTML = '<p style="text-align: center; color: #6c757d;">暂无可用文件,请上传JSON文件</p>';
return;
}
fileListDiv.innerHTML = files.map((file, index) => {
// 转义文件路径中的特殊字符,防止 JavaScript 语法错误
const escapedPath = file.path.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
return `
<div class="file-item" onclick="selectFile('${escapedPath}', this)" data-path="${file.path}">
<div>
<strong>${file.name}</strong>
<br>
<small style="color: #6c757d;">
${(file.size / 1024).toFixed(1)} KB |
${new Date(file.lastModified).toLocaleString('zh-CN')}
</small>
</div>
</div>
`}).join('');
}
// 选择文件
function selectFile(filePath, element) {
selectedFile = filePath;
// 移除其他选中状态
document.querySelectorAll('.file-item').forEach(item => {
item.classList.remove('selected');
});
// 添加选中状态
element.classList.add('selected');
// 启用加载按钮
document.getElementById('loadBtn').disabled = false;
}
// 处理文件上传
async function handleFileUpload(event) {
console.log('File upload triggered:', event);
const file = event.target.files[0];
console.log('Selected file:', file);
if (!file) {
console.log('No file selected');
return;
}
showAlert('fileAlert', 'success', '正在上传: ' + file.name);
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
showAlert('fileAlert', 'success', '文件上传成功!');
loadFileList(); // 刷新文件列表
} else {
showAlert('fileAlert', 'error', '上传失败: ' + data.message);
}
} catch (error) {
console.error('Upload error:', error);
showAlert('fileAlert', 'error', '上传错误: ' + error.message);
}
// 清空文件输入,允许重复上传同一文件
event.target.value = '';
}
// 加载选中的文件
async function loadSelectedFile() {
if (!selectedFile) {
alert('请先选择一个文件');
return;
}
currentFilePath = selectedFile;
try {
const response = await fetch('/api/load', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ filePath: selectedFile })
});
const data = await response.json();
if (data.success) {
displayBasicInfo(data.basicInfo, data.metrics);
document.getElementById('infoSection').classList.remove('hidden');
showAlert('fileAlert', 'success', '数据加载成功!');
// 滚动到信息区域
document.getElementById('infoSection').scrollIntoView({ behavior: 'smooth' });
} else {
showAlert('fileAlert', 'error', '加载失败: ' + data.message);
}
} catch (error) {
showAlert('fileAlert', 'error', '加载错误: ' + error.message);
}
}
// 显示基本信息
function displayBasicInfo(basicInfo, metrics) {
const infoDiv = document.getElementById('basicInfo');
infoDiv.innerHTML = `
<div class="info-card">
<label>生成类型</label>
<value>${basicInfo.generateType || 'N/A'}</value>
</div>
<div class="info-card">
<label>版本号</label>
<value>${basicInfo.version || 'N/A'}</value>
</div>
<div class="info-card">
<label>工序总数</label>
<value>${basicInfo.operationCount || 0}</value>
</div>
<div class="info-card">
<label>唯一订单数</label>
<value>${metrics.uniqueOrders || 0}</value>
</div>
<div class="info-card">
<label>唯一机器数</label>
<value>${metrics.uniqueMachines || 0}</value>
</div>
<div class="info-card">
<label>实际使用机器数</label>
<value>${metrics.totalMachinesUsed || 0}</value>
</div>
${metrics.loadBalanceRatio ? `
<div class="info-card">
<label>负载均衡率</label>
<value>${(metrics.loadBalanceRatio * 100).toFixed(2)}%</value>
</div>
` : ''}
`;
}
// 开始诊断
async function startDiagnosis() {
const diagnoseBtn = document.getElementById('diagnoseBtn');
diagnoseBtn.disabled = true;
diagnoseBtn.textContent = '诊断中...';
try {
const response = await fetch('/api/diagnose', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (data.success) {
displayReport(data.report);
document.getElementById('reportSection').classList.remove('hidden');
document.getElementById('chatSection').classList.remove('hidden');
document.getElementById('optimizeBtn').style.display = 'inline-block';
// 滚动到报告区域
document.getElementById('reportSection').scrollIntoView({ behavior: 'smooth' });
} else {
alert('诊断失败: ' + data.message);
}
} catch (error) {
alert('诊断错误: ' + error.message);
} finally {
diagnoseBtn.disabled = false;
diagnoseBtn.textContent = ' 开始AI诊断';
}
}
// 显示诊断报告(支持 Markdown)
function displayReport(report) {
const reportDiv = document.getElementById('reportContent');
// 使用 marked.js 渲染 Markdown
reportDiv.innerHTML = marked.parse(report);
}
// 提问
async function askQuestion() {
const input = document.getElementById('questionInput');
const question = input.value.trim();
if (!question) return;
// 显示用户问题
addMessage('user', question);
input.value = '';
// 显示加载状态
const loadingId = addMessage('ai', '<div class="spinner"></div><p>AI正在思考...</p>');
try {
const response = await fetch('/api/ask', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ question: question })
});
const data = await response.json();
// 移除加载消息
document.getElementById(loadingId).remove();
if (data.success) {
addMessage('ai', data.answer);
} else {
addMessage('ai', '❌ 回答失败: ' + data.message);
}
} catch (error) {
document.getElementById(loadingId).remove();
addMessage('ai', '❌ 网络错误: ' + error.message);
}
}
// 添加消息
function addMessage(type, content) {
const messagesDiv = document.getElementById('chatMessages');
const messageId = 'msg-' + Date.now();
const messageDiv = document.createElement('div');
messageDiv.id = messageId;
messageDiv.className = `message ${type}`;
messageDiv.innerHTML = content;
messagesDiv.appendChild(messageDiv);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
return messageId;
}
// 显示提示
function showAlert(containerId, type, message) {
const container = document.getElementById(containerId);
container.innerHTML = `<div class="alert alert-${type}">${message}</div>`;
setTimeout(() => {
container.innerHTML = '';
}, 5000);
}
// 开始优化
async function startOptimization() {
const optimizeBtn = document.getElementById('optimizeBtn');
optimizeBtn.disabled = true;
optimizeBtn.textContent = '优化中...';
try {
const response = await fetch('/api/optimize', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (data.success) {
// 存储优化文件路径
optimizedFilePath = data.outputFile;
// 将优化结果显示在诊断报告区域
const reportContent = document.getElementById('reportContent');
const optimizationHtml = `
<div style="border-top: 3px solid #11998e; padding-top: 20px; margin-top: 30px;">
<h1>🚀 AI优化建议</h1>
${marked.parse(data.optimizationAdvice || '优化完成')}
<div style="background: #d4edda; padding: 15px; border-radius: 8px; margin-top: 20px; border-left: 4px solid #28a745;">
<strong>✅ 优化文件已生成!</strong><br>
<small style="color: #155724;">${data.outputFile}</small>
</div>
</div>
`;
reportContent.innerHTML += optimizationHtml;
// 显示下载按钮
document.getElementById('downloadBtn').style.display = 'inline-block';
// 滚动到优化结果区域
reportContent.scrollIntoView({ behavior: 'smooth' });
alert('优化完成!请查看报告底部的优化建议,并下载优化后的方案文件。');
} else {
alert('优化失败: ' + data.message);
}
} catch (error) {
alert('优化错误: ' + error.message);
} finally {
optimizeBtn.disabled = false;
optimizeBtn.textContent = '🚀 AI智能优化排产';
}
}
// 下载优化后的文件
async function downloadOptimizedFile() {
if (!optimizedFilePath) {
alert('请先执行优化操作');
return;
}
try {
const response = await fetch('/api/download?filePath=' + encodeURIComponent(optimizedFilePath));
if (response.ok) {
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = optimizedFilePath.split('\\').pop().split('/').pop(); // 提取文件名
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} else {
alert('下载失败');
}
} catch (error) {
alert('下载错误: ' + error.message);
}
}
</script>
</body>
</html>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment