Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
H
HYH.APSJ
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
佟礼
HYH.APSJ
Commits
fec8084d
Commit
fec8084d
authored
Jul 07, 2026
by
Tong Li
Browse files
Options
Browse Files
Download
Plain Diff
Merge remote-tracking branch 'origin/AI'
parents
8396797c
ffea8fc4
Hide whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
2612 additions
and
11 deletions
+2612
-11
pom.xml
pom.xml
+12
-1
CorsConfig.java
src/main/java/com/aps/config/CorsConfig.java
+10
-9
LLMConfig.java
src/main/java/com/aps/config/LLMConfig.java
+39
-0
DiagnosisController.java
src/main/java/com/aps/controller/DiagnosisController.java
+993
-0
PageController.java
src/main/java/com/aps/controller/PageController.java
+19
-0
LLMClient.java
src/main/java/com/aps/llm/LLMClient.java
+248
-0
ChromosomeData.java
src/main/java/com/aps/model/ChromosomeData.java
+63
-0
MachineOption.java
src/main/java/com/aps/model/MachineOption.java
+25
-0
Operation.java
src/main/java/com/aps/model/Operation.java
+63
-0
OperationInfo.java
src/main/java/com/aps/model/OperationInfo.java
+19
-0
ChromosomeParser.java
src/main/java/com/aps/parser/ChromosomeParser.java
+319
-0
application.yml
src/main/resources/application.yml
+7
-1
index.html
src/main/resources/templates/index.html
+795
-0
No files found.
pom.xml
View file @
fec8084d
...
...
@@ -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>
...
...
src/main/java/com/aps/config/CorsConfig.java
View file @
fec8084d
...
...
@@ -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
src/main/java/com/aps/config/LLMConfig.java
0 → 100644
View file @
fec8084d
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
;
}
src/main/java/com/aps/controller/DiagnosisController.java
0 → 100644
View file @
fec8084d
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
();
}
}
}
src/main/java/com/aps/controller/PageController.java
0 → 100644
View file @
fec8084d
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"
;
}
}
src/main/java/com/aps/llm/LLMClient.java
0 → 100644
View file @
fec8084d
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
);
}
}
src/main/java/com/aps/model/ChromosomeData.java
0 → 100644
View file @
fec8084d
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
;
}
src/main/java/com/aps/model/MachineOption.java
0 → 100644
View file @
fec8084d
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
;
}
src/main/java/com/aps/model/Operation.java
0 → 100644
View file @
fec8084d
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
;
}
src/main/java/com/aps/model/OperationInfo.java
0 → 100644
View file @
fec8084d
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
;
}
src/main/java/com/aps/parser/ChromosomeParser.java
0 → 100644
View file @
fec8084d
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
;
}
}
}
src/main/resources/application.yml
View file @
fec8084d
...
...
@@ -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
:
# 时区配置
...
...
src/main/resources/templates/index.html
0 → 100644
View file @
fec8084d
<!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
,
1
fr
));
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>
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment