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, '');