diff --git a/xinnengyuan/.gitignore b/xinnengyuan/.gitignore
index ac541149..2adafeb0 100644
--- a/xinnengyuan/.gitignore
+++ b/xinnengyuan/.gitignore
@@ -53,3 +53,4 @@ logs/
docs
/file
.idea/
+chat-memory/
diff --git a/xinnengyuan/ruoyi-admin/src/main/resources/application.yml b/xinnengyuan/ruoyi-admin/src/main/resources/application.yml
index 33f043de..5221d0de 100644
--- a/xinnengyuan/ruoyi-admin/src/main/resources/application.yml
+++ b/xinnengyuan/ruoyi-admin/src/main/resources/application.yml
@@ -262,8 +262,6 @@ springdoc:
packages-to-scan: org.dromara.ctr
- group: 15.无人机模块
packages-to-scan: org.dromara.drone
- - group: 20.代码生成模块
- packages-to-scan: org.dromara.generator
- group: 16.app模块
packages-to-scan: org.dromara.app
- group: 17.材料设备模块
@@ -272,18 +270,16 @@ springdoc:
packages-to-scan: org.dromara.out
- group: 19.消息模块
packages-to-scan: org.dromara.message
+ - group: 20.代码生成模块
+ packages-to-scan: org.dromara.generator
- group: 21.分标策划模块
packages-to-scan: org.dromara.tender
- group: 22.大屏模块
packages-to-scan: org.dromara.bigscreen
- - group: 22.投标管理模块
- packages-to-scan: org.dromara.bidding
- group: 23.GPS定位模块
packages-to-scan: org.dromara.gps
- group: 24.招标模块
packages-to-scan: org.dromara.tender
- - group: 29.app版本模块
- packages-to-scan: org.dromara.app
- group: 25.数据迁移模块
packages-to-scan: org.dromara.transferData
- group: 26.netty消息模块
@@ -292,6 +288,12 @@ springdoc:
packages-to-scan: org.dromara.xzd
- group: 28.车辆模块
packages-to-scan: org.dromara.vehicle
+ - group: 29.app版本模块
+ packages-to-scan: org.dromara.app
+ - group: 30.AI 模块
+ packages-to-scan: org.dromara.ai
+ - group: 31.投标管理模块
+ packages-to-scan: org.dromara.bidding
# knife4j的增强配置,不需要增强可以不配
knife4j:
enable: true
diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/pom.xml b/xinnengyuan/ruoyi-modules/ruoyi-system/pom.xml
index 37510d55..6349ff66 100644
--- a/xinnengyuan/ruoyi-modules/ruoyi-system/pom.xml
+++ b/xinnengyuan/ruoyi-modules/ruoyi-system/pom.xml
@@ -33,6 +33,11 @@
com.alibaba.cloud.ai
spring-ai-alibaba-starter-dashscope
+
+ com.esotericsoftware
+ kryo
+ 5.6.2
+
diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/advisor/CustomLoggerAdvisor.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/advisor/CustomLoggerAdvisor.java
new file mode 100644
index 00000000..cb38adb4
--- /dev/null
+++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/advisor/CustomLoggerAdvisor.java
@@ -0,0 +1,61 @@
+package org.dromara.ai.advisor;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.ai.chat.client.ChatClientMessageAggregator;
+import org.springframework.ai.chat.client.ChatClientRequest;
+import org.springframework.ai.chat.client.ChatClientResponse;
+import org.springframework.ai.chat.client.advisor.api.CallAdvisor;
+import org.springframework.ai.chat.client.advisor.api.CallAdvisorChain;
+import org.springframework.ai.chat.client.advisor.api.StreamAdvisor;
+import org.springframework.ai.chat.client.advisor.api.StreamAdvisorChain;
+import reactor.core.publisher.Flux;
+
+/**
+ * 自定义日志拦截器
+ *
+ * @author lilemy
+ * @date 2025-11-04 10:15
+ */
+@Slf4j
+public class CustomLoggerAdvisor implements CallAdvisor, StreamAdvisor {
+
+ private void before(ChatClientRequest request) {
+ log.info("AI 请求参数:{}", request.prompt());
+ }
+
+ private void observeAfter(ChatClientResponse response) {
+ if (response.chatResponse() != null) {
+ log.info("AI 响应结果:{}", response.chatResponse().getResult().getOutput().getText());
+ }
+ }
+
+ @Override
+ public ChatClientResponse adviseCall(ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain) {
+ before(chatClientRequest);
+ ChatClientResponse chatClientResponse = callAdvisorChain.nextCall(chatClientRequest);
+ observeAfter(chatClientResponse);
+ return chatClientResponse;
+ }
+
+ @Override
+ public Flux adviseStream(ChatClientRequest chatClientRequest, StreamAdvisorChain streamAdvisorChain) {
+ before(chatClientRequest);
+ Flux chatClientResponseFlux = streamAdvisorChain.nextStream(chatClientRequest);
+ return (new ChatClientMessageAggregator()).aggregateChatClientResponse(chatClientResponseFlux, this::observeAfter);
+ }
+
+ /**
+ * Return the name of the advisor.
+ *
+ * @return the advisor name.
+ */
+ @Override
+ public String getName() {
+ return this.getClass().getSimpleName();
+ }
+
+ @Override
+ public int getOrder() {
+ return 0;
+ }
+}
diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/chat/DashScopeChat.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/chat/DashScopeChat.java
new file mode 100644
index 00000000..b7ea1383
--- /dev/null
+++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/chat/DashScopeChat.java
@@ -0,0 +1,117 @@
+package org.dromara.ai.chat;
+
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.ai.advisor.CustomLoggerAdvisor;
+import org.dromara.ai.chatmemory.FileBasedChatMemory;
+import org.dromara.ai.domain.AIChatMemory;
+import org.dromara.ai.service.IAIChatMemoryService;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
+import org.springframework.ai.chat.memory.ChatMemory;
+import org.springframework.ai.chat.model.ChatModel;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Flux;
+
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * @author lilemy
+ * @date 2025-11-04 09:34
+ */
+@Slf4j
+@Component
+public class DashScopeChat {
+
+ @Resource
+ private SimpleChat simpleChat;
+
+ @Resource
+ private IAIChatMemoryService chatMemoryService;
+
+ private final ChatClient chatClient;
+
+ private static final String DEFAULT_PROMPT = "你是一个博学的智能聊天助手,请根据用户提问回答!";
+
+ private static final String DEFAULT_FILE_DIR = System.getProperty("user.dir") + "/chat-memory";
+
+ public DashScopeChat(ChatModel dashScopeChatModel) {
+ // 初始化基于文件的对话记忆
+ ChatMemory chatMemory = new FileBasedChatMemory(DEFAULT_FILE_DIR);
+ chatClient = ChatClient.builder(dashScopeChatModel)
+ .defaultSystem(DEFAULT_PROMPT)
+ .defaultAdvisors(
+ MessageChatMemoryAdvisor.builder(chatMemory).build(),
+ // 自定义日志输出
+ new CustomLoggerAdvisor()
+ ).build();
+ }
+
+ /**
+ * AI 对话,流式输出
+ *
+ * @param message 用户输入
+ * @param chatId 会话id
+ * @return 流式输出结果
+ */
+ public Flux doChatStream(String message, String chatId) {
+ Long userId = LoginHelper.getUserId();
+ boolean isFirst = StringUtils.isBlank(chatId);
+ if (StringUtils.isBlank(chatId)) {
+ // 构建新的会话id
+ chatId = UUID.randomUUID().toString();
+ }
+ String finalChatId = chatId;
+ return chatClient
+ .prompt()
+ .user(message)
+ .advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, finalChatId))
+ .stream()
+ .content()// 收集所有 token,生成完整回复
+ .collectList()
+ .flatMapMany(tokens -> {
+ String aiResponse = String.join("", tokens);
+ if (isFirst) {
+ // 异步生成标题
+ generateChatTitleAsync(finalChatId, message, aiResponse, userId);
+ }
+ // 返回完整的流结果
+ return Flux.fromIterable(tokens);
+ });
+ }
+
+ /**
+ * 异步生成标题
+ *
+ * @param chatId 会话id
+ * @param userMessage 用户输入
+ * @param aiResponse AI回复
+ * @param userId 用户id
+ */
+ private void generateChatTitleAsync(String chatId, String userMessage, String aiResponse, Long userId) {
+ CompletableFuture.runAsync(() -> {
+ try {
+ // 构建生成标题的提示词
+ String prompt = String.format("""
+ 请为下面这段用户与AI的对话生成一个简短的标题(不超过10个字):
+ 用户:%s
+ AI:%s
+ """, userMessage, aiResponse);
+ String title = simpleChat.doChat(prompt);
+ log.info("用户:{} 生成标题成功:{} -> {}", userId, chatId, title);
+ // 保存对话数据
+ AIChatMemory memory = new AIChatMemory();
+ memory.setUserId(userId);
+ memory.setFileName(chatId);
+ memory.setFirstQuestion(title);
+ chatMemoryService.save(memory);
+ } catch (Exception e) {
+ log.error("生成标题失败", e);
+ }
+ });
+ }
+
+}
diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/chat/SimpleChat.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/chat/SimpleChat.java
new file mode 100644
index 00000000..4f2dbdff
--- /dev/null
+++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/chat/SimpleChat.java
@@ -0,0 +1,36 @@
+package org.dromara.ai.chat;
+
+import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
+import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.stereotype.Component;
+
+/**
+ * @author lilemy
+ * @date 2025-11-04 15:26
+ */
+@Component
+public class SimpleChat {
+
+ private final ChatClient dashScopeChatClient;
+
+ public SimpleChat(ChatClient.Builder chatClientBuilder) {
+ this.dashScopeChatClient = chatClientBuilder
+ // 设置 ChatClient 中 ChatModel 的 Options 参数
+ .defaultOptions(
+ DashScopeChatOptions.builder()
+ .withTopP(0.7)
+ .build()
+ )
+ .build();
+ }
+
+ /**
+ * AI 对话
+ *
+ * @param message 用户输入
+ * @return 响应结果
+ */
+ public String doChat(String message) {
+ return dashScopeChatClient.prompt(message).call().content();
+ }
+}
diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/chatmemory/FileBasedChatMemory.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/chatmemory/FileBasedChatMemory.java
new file mode 100644
index 00000000..82368afd
--- /dev/null
+++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/chatmemory/FileBasedChatMemory.java
@@ -0,0 +1,87 @@
+package org.dromara.ai.chatmemory;
+
+import com.esotericsoftware.kryo.Kryo;
+import com.esotericsoftware.kryo.io.Input;
+import com.esotericsoftware.kryo.io.Output;
+import lombok.extern.slf4j.Slf4j;
+import org.objenesis.strategy.StdInstantiatorStrategy;
+import org.springframework.ai.chat.memory.ChatMemory;
+import org.springframework.ai.chat.messages.Message;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 基于文件持久化的对话记忆
+ */
+@Slf4j
+public class FileBasedChatMemory implements ChatMemory {
+
+ private final String BASE_DIR;
+ private static final Kryo kryo = new Kryo();
+
+ static {
+ kryo.setRegistrationRequired(false);
+ // 设置实例化策略
+ kryo.setInstantiatorStrategy(new StdInstantiatorStrategy());
+ }
+
+ // 构造对象时,指定文件保存目录
+ public FileBasedChatMemory(String dir) {
+ this.BASE_DIR = dir;
+ File baseDir = new File(dir);
+ if (!baseDir.exists()) {
+ baseDir.mkdirs();
+ }
+ }
+
+ @Override
+ public void add(String conversationId, List messages) {
+ List conversationMessages = getOrCreateConversation(conversationId);
+ conversationMessages.addAll(messages);
+ saveConversation(conversationId, conversationMessages);
+ }
+
+ @Override
+ public List get(String conversationId) {
+ return getOrCreateConversation(conversationId);
+ }
+
+ @Override
+ public void clear(String conversationId) {
+ File file = getConversationFile(conversationId);
+ if (file.exists()) {
+ file.delete();
+ }
+ }
+
+ private List getOrCreateConversation(String conversationId) {
+ File file = getConversationFile(conversationId);
+ List messages = new ArrayList<>();
+ if (file.exists()) {
+ try (Input input = new Input(new FileInputStream(file))) {
+ messages = kryo.readObject(input, ArrayList.class);
+ } catch (IOException e) {
+ log.error("读取对话失败", e);
+ }
+ }
+ return messages;
+ }
+
+ private void saveConversation(String conversationId, List messages) {
+ File file = getConversationFile(conversationId);
+ try (Output output = new Output(new FileOutputStream(file))) {
+ kryo.writeObject(output, messages);
+ } catch (IOException e) {
+ log.error("保存对话失败", e);
+ }
+ }
+
+ private File getConversationFile(String conversationId) {
+ return new File(BASE_DIR, conversationId + ".kryo");
+ }
+}
diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/controller/AIChatController.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/controller/AIChatController.java
new file mode 100644
index 00000000..79396579
--- /dev/null
+++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/controller/AIChatController.java
@@ -0,0 +1,49 @@
+package org.dromara.ai.controller;
+
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.ai.chat.DashScopeChat;
+import org.dromara.ai.chatmemory.FileBasedChatMemory;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.springframework.ai.chat.messages.Message;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import reactor.core.publisher.Flux;
+
+import java.util.List;
+
+/**
+ * @author lilemy
+ * @date 2025-10-23 11:32
+ */
+@Slf4j
+@Validated
+@RestController
+@RequestMapping("/ai/chat")
+public class AIChatController {
+
+ @Resource
+ private DashScopeChat dashScopeChat;
+
+ /**
+ * ChatClient 流式调用
+ */
+ @GetMapping("/stream")
+ public Flux streamChat(String query, String chatId, HttpServletResponse response) {
+ response.setCharacterEncoding("UTF-8");
+ return dashScopeChat.doChatStream(query, chatId);
+ }
+
+ /**
+ * 对话记录
+ */
+ @GetMapping("/history")
+ public R> getChatHistory(String chatId) {
+ FileBasedChatMemory memory = new FileBasedChatMemory(System.getProperty("user.dir") + "/chat-memory");
+ return R.ok(memory.get(chatId));
+ }
+}
diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/controller/AIChatMemoryController.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/controller/AIChatMemoryController.java
new file mode 100644
index 00000000..1aebef79
--- /dev/null
+++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/controller/AIChatMemoryController.java
@@ -0,0 +1,81 @@
+package org.dromara.ai.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.RequiredArgsConstructor;
+import org.dromara.ai.domain.dto.AIChatMemoryQueryReq;
+import org.dromara.ai.domain.vo.AIChatMemoryVo;
+import org.dromara.ai.service.IAIChatMemoryService;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.excel.utils.ExcelUtil;
+import org.dromara.common.log.annotation.Log;
+import org.dromara.common.log.enums.BusinessType;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.web.core.BaseController;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * AI 对话记录信息
+ *
+ * @author lilemy
+ * @date 2025-11-04
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/ai/chatMemory")
+public class AIChatMemoryController extends BaseController {
+
+ private final IAIChatMemoryService aiChatMemoryService;
+
+ /**
+ * 查询AI 对话记录信息列表
+ */
+ @SaCheckPermission("ai:chatMemory:list")
+ @GetMapping("/list")
+ public TableDataInfo list(AIChatMemoryQueryReq req, PageQuery pageQuery) {
+ return aiChatMemoryService.queryPageList(req, pageQuery);
+ }
+
+ /**
+ * 导出AI 对话记录信息列表
+ */
+ @SaCheckPermission("ai:chatMemory:export")
+ @Log(title = "AI 对话记录信息", businessType = BusinessType.EXPORT)
+ @PostMapping("/export")
+ public void export(AIChatMemoryQueryReq req, HttpServletResponse response) {
+ List list = aiChatMemoryService.queryList(req);
+ ExcelUtil.exportExcel(list, "AI 对话记录信息", AIChatMemoryVo.class, response);
+ }
+
+ /**
+ * 获取AI 对话记录信息详细信息
+ *
+ * @param id 主键
+ */
+ @SaCheckPermission("ai:chatMemory:query")
+ @GetMapping("/{id}")
+ public R getInfo(@NotNull(message = "主键不能为空")
+ @PathVariable Long id) {
+ return R.ok(aiChatMemoryService.queryById(id));
+ }
+
+ /**
+ * 删除AI 对话记录信息
+ *
+ * @param ids 主键串
+ */
+ @SaCheckPermission("ai:chatMemory:remove")
+ @Log(title = "AI 对话记录信息", businessType = BusinessType.DELETE)
+ @DeleteMapping("/{ids}")
+ public R remove(@NotEmpty(message = "主键不能为空")
+ @PathVariable Long[] ids) {
+ return toAjax(aiChatMemoryService.deleteWithValidByIds(List.of(ids), true));
+ }
+}
diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/controller/AIController.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/controller/AIController.java
deleted file mode 100644
index 099bb993..00000000
--- a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/controller/AIController.java
+++ /dev/null
@@ -1,51 +0,0 @@
-package org.dromara.ai.controller;
-
-import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
-import jakarta.servlet.http.HttpServletResponse;
-import org.springframework.ai.chat.client.ChatClient;
-import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
-import org.springframework.validation.annotation.Validated;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.bind.annotation.RestController;
-import reactor.core.publisher.Flux;
-
-/**
- * @author lilemy
- * @date 2025-10-23 11:32
- */
-@Validated
-@RestController
-@RequestMapping("/ai")
-public class AIController {
-
- private static final String DEFAULT_PROMPT = "你是一个博学的智能聊天助手,请根据用户提问回答!";
-
- private final ChatClient dashScopeChatClient;
-
- public AIController(ChatClient.Builder chatClientBuilder) {
- this.dashScopeChatClient = chatClientBuilder
- .defaultSystem(DEFAULT_PROMPT)
- // 实现 Logger 的 Advisor
- .defaultAdvisors(
- new SimpleLoggerAdvisor()
- )
- // 设置 ChatClient 中 ChatModel 的 Options 参数
- .defaultOptions(
- DashScopeChatOptions.builder()
- .withTopP(0.7)
- .build()
- )
- .build();
- }
-
- /**
- * ChatClient 流式调用
- */
- @GetMapping("/stream/chat")
- public Flux streamChat(@RequestParam(value = "query", defaultValue = "你好,很高兴认识你,能简单介绍一下自己吗?") String query, HttpServletResponse response) {
- response.setCharacterEncoding("UTF-8");
- return dashScopeChatClient.prompt(query).stream().content();
- }
-}
diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/domain/AIChatMemory.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/domain/AIChatMemory.java
new file mode 100644
index 00000000..111e39e1
--- /dev/null
+++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/domain/AIChatMemory.java
@@ -0,0 +1,52 @@
+package org.dromara.ai.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.mybatis.core.domain.BaseEntity;
+
+import java.io.Serial;
+
+/**
+ * AI 对话记录信息对象 ai_chat_memory
+ *
+ * @author lilemy
+ * @date 2025-11-04
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("ai_chat_memory")
+public class AIChatMemory extends BaseEntity {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 主键
+ */
+ @TableId(value = "id")
+ private Long id;
+
+ /**
+ * 用户id
+ */
+ private Long userId;
+
+ /**
+ * 文件名
+ */
+ private String fileName;
+
+ /**
+ * 第一条问题
+ */
+ private String firstQuestion;
+
+ /**
+ * 备注
+ */
+ private String remark;
+
+
+}
diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/domain/dto/AIChatMemoryQueryReq.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/domain/dto/AIChatMemoryQueryReq.java
new file mode 100644
index 00000000..0b35c56a
--- /dev/null
+++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/domain/dto/AIChatMemoryQueryReq.java
@@ -0,0 +1,32 @@
+package org.dromara.ai.domain.dto;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * @author lilemy
+ * @date 2025-11-04 15:19
+ */
+@Data
+public class AIChatMemoryQueryReq implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = -4090176451164739134L;
+
+ /**
+ * 用户id
+ */
+ private Long userId;
+
+ /**
+ * 文件名
+ */
+ private String fileName;
+
+ /**
+ * 第一条问题
+ */
+ private String firstQuestion;
+}
diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/domain/vo/AIChatMemoryVo.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/domain/vo/AIChatMemoryVo.java
new file mode 100644
index 00000000..44a06efb
--- /dev/null
+++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/domain/vo/AIChatMemoryVo.java
@@ -0,0 +1,58 @@
+package org.dromara.ai.domain.vo;
+
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+import org.dromara.ai.domain.AIChatMemory;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+
+/**
+ * AI 对话记录信息视图对象 ai_chat_memory
+ *
+ * @author lilemy
+ * @date 2025-11-04
+ */
+@Data
+@ExcelIgnoreUnannotated
+@AutoMapper(target = AIChatMemory.class)
+public class AIChatMemoryVo implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 主键
+ */
+ @ExcelProperty(value = "主键")
+ private Long id;
+
+ /**
+ * 用户id
+ */
+ @ExcelProperty(value = "用户id")
+ private Long userId;
+
+ /**
+ * 文件名
+ */
+ @ExcelProperty(value = "文件名")
+ private String fileName;
+
+ /**
+ * 第一条问题
+ */
+ @ExcelProperty(value = "第一条问题")
+ private String firstQuestion;
+
+ /**
+ * 备注
+ */
+ @ExcelProperty(value = "备注")
+ private String remark;
+
+
+}
diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/mapper/AIChatMemoryMapper.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/mapper/AIChatMemoryMapper.java
new file mode 100644
index 00000000..30660f47
--- /dev/null
+++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/mapper/AIChatMemoryMapper.java
@@ -0,0 +1,15 @@
+package org.dromara.ai.mapper;
+
+import org.dromara.ai.domain.AIChatMemory;
+import org.dromara.ai.domain.vo.AIChatMemoryVo;
+import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
+
+/**
+ * AI 对话记录信息Mapper接口
+ *
+ * @author lilemy
+ * @date 2025-11-04
+ */
+public interface AIChatMemoryMapper extends BaseMapperPlus {
+
+}
diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/service/IAIChatMemoryService.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/service/IAIChatMemoryService.java
new file mode 100644
index 00000000..9c513751
--- /dev/null
+++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/service/IAIChatMemoryService.java
@@ -0,0 +1,54 @@
+package org.dromara.ai.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import org.dromara.ai.domain.AIChatMemory;
+import org.dromara.ai.domain.dto.AIChatMemoryQueryReq;
+import org.dromara.ai.domain.vo.AIChatMemoryVo;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * AI 对话记录信息Service接口
+ *
+ * @author lilemy
+ * @date 2025-11-04
+ */
+public interface IAIChatMemoryService extends IService{
+
+ /**
+ * 查询AI 对话记录信息
+ *
+ * @param id 主键
+ * @return AI 对话记录信息
+ */
+ AIChatMemoryVo queryById(Long id);
+
+ /**
+ * 分页查询AI 对话记录信息列表
+ *
+ * @param bo 查询条件
+ * @param pageQuery 分页参数
+ * @return AI 对话记录信息分页列表
+ */
+ TableDataInfo queryPageList(AIChatMemoryQueryReq bo, PageQuery pageQuery);
+
+ /**
+ * 查询符合条件的AI 对话记录信息列表
+ *
+ * @param bo 查询条件
+ * @return AI 对话记录信息列表
+ */
+ List queryList(AIChatMemoryQueryReq bo);
+
+ /**
+ * 校验并批量删除AI 对话记录信息信息
+ *
+ * @param ids 待删除的主键集合
+ * @param isValid 是否进行有效性校验
+ * @return 是否删除成功
+ */
+ Boolean deleteWithValidByIds(Collection ids, Boolean isValid);
+}
diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/service/impl/AIChatMemoryServiceImpl.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/service/impl/AIChatMemoryServiceImpl.java
new file mode 100644
index 00000000..1b0cdaa1
--- /dev/null
+++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/ai/service/impl/AIChatMemoryServiceImpl.java
@@ -0,0 +1,90 @@
+package org.dromara.ai.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.dromara.ai.domain.AIChatMemory;
+import org.dromara.ai.domain.dto.AIChatMemoryQueryReq;
+import org.dromara.ai.domain.vo.AIChatMemoryVo;
+import org.dromara.ai.mapper.AIChatMemoryMapper;
+import org.dromara.ai.service.IAIChatMemoryService;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.springframework.stereotype.Service;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * AI 对话记录信息Service业务层处理
+ *
+ * @author lilemy
+ * @date 2025-11-04
+ */
+@Service
+public class AIChatMemoryServiceImpl extends ServiceImpl
+ implements IAIChatMemoryService {
+
+ /**
+ * 查询AI 对话记录信息
+ *
+ * @param id 主键
+ * @return AI 对话记录信息
+ */
+ @Override
+ public AIChatMemoryVo queryById(Long id) {
+ return baseMapper.selectVoById(id);
+ }
+
+ /**
+ * 分页查询AI 对话记录信息列表
+ *
+ * @param req 查询条件
+ * @param pageQuery 分页参数
+ * @return AI 对话记录信息分页列表
+ */
+ @Override
+ public TableDataInfo queryPageList(AIChatMemoryQueryReq req, PageQuery pageQuery) {
+ LambdaQueryWrapper lqw = buildQueryWrapper(req);
+ Page result = baseMapper.selectVoPage(pageQuery.build(), lqw);
+ return TableDataInfo.build(result);
+ }
+
+ /**
+ * 查询符合条件的AI 对话记录信息列表
+ *
+ * @param req 查询条件
+ * @return AI 对话记录信息列表
+ */
+ @Override
+ public List queryList(AIChatMemoryQueryReq req) {
+ LambdaQueryWrapper lqw = buildQueryWrapper(req);
+ return baseMapper.selectVoList(lqw);
+ }
+
+ private LambdaQueryWrapper buildQueryWrapper(AIChatMemoryQueryReq req) {
+ LambdaQueryWrapper lqw = Wrappers.lambdaQuery();
+ lqw.orderByDesc(AIChatMemory::getId);
+ lqw.eq(req.getUserId() != null, AIChatMemory::getUserId, req.getUserId());
+ lqw.like(StringUtils.isNotBlank(req.getFileName()), AIChatMemory::getFileName, req.getFileName());
+ lqw.eq(StringUtils.isNotBlank(req.getFirstQuestion()), AIChatMemory::getFirstQuestion, req.getFirstQuestion());
+ return lqw;
+ }
+
+ /**
+ * 校验并批量删除AI 对话记录信息信息
+ *
+ * @param ids 待删除的主键集合
+ * @param isValid 是否进行有效性校验
+ * @return 是否删除成功
+ */
+ @Override
+ public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) {
+ if (isValid) {
+ //TODO 做一些业务上的校验,判断是否需要校验
+ }
+ return baseMapper.deleteByIds(ids) > 0;
+ }
+}
diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/manager/recognizermanager/enums/RecognizerTypeEnum.java b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/manager/recognizermanager/enums/RecognizerTypeEnum.java
index 38f0a4d6..7546d070 100644
--- a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/manager/recognizermanager/enums/RecognizerTypeEnum.java
+++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/manager/recognizermanager/enums/RecognizerTypeEnum.java
@@ -13,17 +13,17 @@ import java.util.stream.Collectors;
@Getter
public enum RecognizerTypeEnum {
- WEARING_ALL("穿戴安全帽反光衣", "wearingall", ""),
- NO_EQUIPMENT("没穿安全帽反光衣", "noequipment", "1"),
- NO_HELMET("有反光衣没安全帽", "nohelmet", ""),
- NO_VEST("有安全帽没反光衣", "novest", ""),
- SMOKE("吸烟", "smoke", "3"),
- FIRE("火焰", "fire", "16"),
- SMOGGY("烟雾", "smoggy", ""),
- PANEL("光伏板", "solar", ""),
- BRACKET("光伏板支架", "bracket", ""),
- COLUMN("光伏板桩", "column", ""),
- HOLE("光伏板孔", "hole", "");
+ WEARING_ALL("穿戴安全帽反光衣", "wearingall", "1"),
+ NO_EQUIPMENT("没穿安全帽反光衣", "noequipment", "2"),
+ NO_HELMET("有反光衣没安全帽", "nohelmet", "3"),
+ NO_VEST("有安全帽没反光衣", "novest", "4"),
+ SMOKE("吸烟", "smoke", "5"),
+ FIRE("火焰", "fire", "6"),
+ SMOGGY("烟雾", "smoggy", "7"),
+ PANEL("光伏板", "solar", "8"),
+ BRACKET("光伏板支架", "bracket", "9"),
+ COLUMN("光伏板桩", "column", "10"),
+ HOLE("光伏板孔", "hole", "11");
private final String text;
diff --git a/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/resources/mapper/ai/AIChatMemoryMapper.xml b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/resources/mapper/ai/AIChatMemoryMapper.xml
new file mode 100644
index 00000000..a5329f24
--- /dev/null
+++ b/xinnengyuan/ruoyi-modules/ruoyi-system/src/main/resources/mapper/ai/AIChatMemoryMapper.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/xinnengyuan/script/sql/xinnengyuan.sql b/xinnengyuan/script/sql/xinnengyuan.sql
index c2b7bae2..19c7b5a6 100644
--- a/xinnengyuan/script/sql/xinnengyuan.sql
+++ b/xinnengyuan/script/sql/xinnengyuan.sql
@@ -1904,21 +1904,52 @@ create table mat_warehouse
-- 菜单 SQL
-insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
-values(1983374316882939905, '物资仓库', '1953994827229114369', '1', 'warehouse', 'materials/warehouse/index', 1, 0, 'C', '0', '0', 'materials:warehouse:list', '#', 103, 1, sysdate(), null, null, '物资仓库菜单');
+insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible,
+ status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
+values (1983374316882939905, '物资仓库', '1953994827229114369', '1', 'warehouse', 'materials/warehouse/index', 1, 0,
+ 'C', '0', '0', 'materials:warehouse:list', '#', 103, 1, sysdate(), null, null, '物资仓库菜单');
-- 按钮 SQL
-insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
-values(1983374316882939906, '物资仓库查询', 1983374316882939905, '1', '#', '', 1, 0, 'F', '0', '0', 'materials:warehouse:query', '#', 103, 1, sysdate(), null, null, '');
+insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible,
+ status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
+values (1983374316882939906, '物资仓库查询', 1983374316882939905, '1', '#', '', 1, 0, 'F', '0', '0',
+ 'materials:warehouse:query', '#', 103, 1, sysdate(), null, null, '');
-insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
-values(1983374316882939907, '物资仓库新增', 1983374316882939905, '2', '#', '', 1, 0, 'F', '0', '0', 'materials:warehouse:add', '#', 103, 1, sysdate(), null, null, '');
+insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible,
+ status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
+values (1983374316882939907, '物资仓库新增', 1983374316882939905, '2', '#', '', 1, 0, 'F', '0', '0',
+ 'materials:warehouse:add', '#', 103, 1, sysdate(), null, null, '');
-insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
-values(1983374316882939908, '物资仓库修改', 1983374316882939905, '3', '#', '', 1, 0, 'F', '0', '0', 'materials:warehouse:edit', '#', 103, 1, sysdate(), null, null, '');
+insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible,
+ status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
+values (1983374316882939908, '物资仓库修改', 1983374316882939905, '3', '#', '', 1, 0, 'F', '0', '0',
+ 'materials:warehouse:edit', '#', 103, 1, sysdate(), null, null, '');
-insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
-values(1983374316882939909, '物资仓库删除', 1983374316882939905, '4', '#', '', 1, 0, 'F', '0', '0', 'materials:warehouse:remove', '#', 103, 1, sysdate(), null, null, '');
+insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible,
+ status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
+values (1983374316882939909, '物资仓库删除', 1983374316882939905, '4', '#', '', 1, 0, 'F', '0', '0',
+ 'materials:warehouse:remove', '#', 103, 1, sysdate(), null, null, '');
+
+insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible,
+ status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
+values (1983374316882939910, '物资仓库导出', 1983374316882939905, '5', '#', '', 1, 0, 'F', '0', '0',
+ 'materials:warehouse:export', '#', 103, 1, sysdate(), null, null, '');
+
+
+DROP TABLE IF EXISTS ai_chat_memory;
+CREATE TABLE `ai_chat_memory`
+(
+ `id` bigint not null auto_increment comment '主键',
+ `user_id` bigint not null comment '用户id',
+ `file_name` varchar(64) not null comment '文件名',
+ `first_question` varchar(128) not null comment '第一条问题',
+ `remark` varchar(255) null comment '备注',
+ `create_by` bigint null comment '创建者',
+ `update_by` bigint null comment '更新者',
+ `create_dept` bigint null comment '创建部门',
+ `create_time` datetime default CURRENT_TIMESTAMP null comment '创建时间',
+ `update_time` datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '更新时间',
+ PRIMARY KEY (`id`) USING BTREE,
+ INDEX `idx_user_id` (`user_id` ASC) USING BTREE comment '用户id'
+) comment = 'AI 对话记录信息';
-insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
-values(1983374316882939910, '物资仓库导出', 1983374316882939905, '5', '#', '', 1, 0, 'F', '0', '0', 'materials:warehouse:export', '#', 103, 1, sysdate(), null, null, '');