diff --git a/pom.xml b/pom.xml index 38ddfbe..97b42c9 100644 --- a/pom.xml +++ b/pom.xml @@ -44,11 +44,10 @@ spring-boot-starter-data-redis - com.baomidou mybatis-plus-spring-boot3-starter - 3.5.5 + 3.5.10.1 com.baomidou diff --git a/src/main/java/com/ai/app/SpringAiAppApplication.java b/src/main/java/com/ai/app/SpringAiAppApplication.java index 98c12e9..43799de 100644 --- a/src/main/java/com/ai/app/SpringAiAppApplication.java +++ b/src/main/java/com/ai/app/SpringAiAppApplication.java @@ -1,9 +1,11 @@ package com.ai.app; +import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication +@MapperScan(basePackages = "com.ai.app.mapper") public class SpringAiAppApplication { public static void main(String[] args) { diff --git a/src/main/java/com/ai/app/config/AIConfig.java b/src/main/java/com/ai/app/config/AIConfig.java index 85cfeb5..6cdbdb8 100644 --- a/src/main/java/com/ai/app/config/AIConfig.java +++ b/src/main/java/com/ai/app/config/AIConfig.java @@ -1,6 +1,7 @@ package com.ai.app.config; import com.ai.app.constant.SystemConstants; +import com.ai.app.tools.CourseTools; import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; @@ -78,7 +79,7 @@ public class AIConfig { } /** - * dashScopeChatClient对话客户端 + * 游戏对话客户端 * * @param dashScopeChatModel 千问max大模型 * @param simpleLoggerAdvisor 日志Advisor @@ -101,6 +102,32 @@ public class AIConfig { .build(); } + /** + * 智能客服chat client + * + * @param dashScopeChatModel 千问模型 + * @param simpleLoggerAdvisor 日志增强 + * @param messageChatMemoryAdvisor 对话记忆 + * @return: org.springframework.ai.chat.client.ChatClient + * @author kai.liu + * @date: 2025/9/4 14:35 + */ + @Bean + public ChatClient serviceDashScopeChatClient(DashScopeChatModel dashScopeChatModel, + SimpleLoggerAdvisor simpleLoggerAdvisor, + MessageChatMemoryAdvisor messageChatMemoryAdvisor, + CourseTools courseTools) { + return ChatClient + //模型 + .builder(dashScopeChatModel) + //系统提示词 + .defaultSystem(SystemConstants.SERVICE_SYSTEM_PROMPT) + //环绕增强 + .defaultAdvisors(simpleLoggerAdvisor, messageChatMemoryAdvisor) + .defaultTools(courseTools) + .build(); + } + /** * deepseek 对话客户端 * 响应太慢,不建议使用 diff --git a/src/main/java/com/ai/app/config/RedisChatMemoryRepositor.java b/src/main/java/com/ai/app/config/RedisChatMemoryRepositor.java deleted file mode 100644 index 536d9cc..0000000 --- a/src/main/java/com/ai/app/config/RedisChatMemoryRepositor.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.ai.app.config; - -import com.ai.app.service.AIChatMessageService; -import org.springframework.ai.chat.memory.ChatMemoryRepository; -import org.springframework.ai.chat.messages.Message; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -import java.util.List; - -/** - * @describe - * @Author kai.liu - * @Date 2025/8/29 9:01 - */ -@Component -public class RedisChatMemoryRepositor implements ChatMemoryRepository { - - @Autowired - private AIChatMessageService aiChatMessageService; - - @Override - public List findConversationIds() { - return aiChatMessageService.getConversationIds(); - } - - @Override - public List findByConversationId(String conversationId) { - return aiChatMessageService.getMessage(conversationId); - } - - @Override - public void saveAll(String conversationId, List messages) { - aiChatMessageService.saveAll(conversationId, messages); - } - - @Override - public void deleteByConversationId(String conversationId) { - aiChatMessageService.delete(conversationId); - } -} diff --git a/src/main/java/com/ai/app/controller/DashScopeController.java b/src/main/java/com/ai/app/controller/DashScopeController.java index eda07f2..eb3e713 100644 --- a/src/main/java/com/ai/app/controller/DashScopeController.java +++ b/src/main/java/com/ai/app/controller/DashScopeController.java @@ -1,7 +1,6 @@ package com.ai.app.controller; import com.ai.app.constant.ChatTypeEnum; -import com.ai.app.constant.SystemConstants; import com.ai.app.dto.MessageDTO; import com.ai.app.service.ChatHistoryService; import org.springframework.ai.chat.client.ChatClient; @@ -37,6 +36,10 @@ public class DashScopeController { @Qualifier("gameDashScopeChatClient") private ChatClient gameDashScopeChatClient; + @Autowired + @Qualifier("serviceDashScopeChatClient") + private ChatClient serviceDashScopeChatClient; + @Autowired private ChatHistoryService chatHistoryService; @@ -107,4 +110,23 @@ public class DashScopeController { .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, chatId)) .stream().content(); } + + /** + * it培训机构智能客服 + * + * @param prompt 用户提示词 + * @param chatId 对话ID + * @return: reactor.core.publisher.Flux + * @author kai.liu + * @date: 2025/9/4 14:51 + */ + @RequestMapping(value = "/service", produces = "text/stream;charset=UTF-8") + public Flux service(String prompt, String chatId) { + chatHistoryService.saveHistoryChatId(ChatTypeEnum.CHAT_PDF.type, chatId); + return serviceDashScopeChatClient + .prompt() + .user(prompt) + .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, chatId)) + .stream().content(); + } } diff --git a/src/main/java/com/ai/app/entity/Course.java b/src/main/java/com/ai/app/entity/Course.java new file mode 100644 index 0000000..2629434 --- /dev/null +++ b/src/main/java/com/ai/app/entity/Course.java @@ -0,0 +1,59 @@ +package com.ai.app.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import java.io.Serializable; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + *

+ * 学科表 + *

+ * + * @author huge + * @since 2025-03-08 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@TableName("course") +public class Course implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 主键 + */ + @TableId(value = "id", type = IdType.AUTO) + private Integer id; + + /** + * 学科名称 + */ + private String name; + + /** + * 学历背景要求:0-无,1-初中,2-高中、3-大专、4-本科以上 + */ + private Integer edu; + + /** + * 课程类型:编程、设计、自媒体、其它 + */ + private String type; + + /** + * 课程价格 + */ + private Long price; + + /** + * 学习时长,单位: 天 + */ + private Integer duration; + + +} diff --git a/src/main/java/com/ai/app/entity/CourseReservation.java b/src/main/java/com/ai/app/entity/CourseReservation.java new file mode 100644 index 0000000..8ddfe5b --- /dev/null +++ b/src/main/java/com/ai/app/entity/CourseReservation.java @@ -0,0 +1,56 @@ +package com.ai.app.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import java.io.Serializable; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + *

+ * + *

+ * + * @author huge + * @since 2025-03-08 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@TableName("course_reservation") +public class CourseReservation implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(value = "id", type = IdType.AUTO) + private Integer id; + + /** + * 预约课程 + */ + private String course; + + /** + * 学生姓名 + */ + private String studentName; + + /** + * 联系方式 + */ + private String contactInfo; + + /** + * 预约校区 + */ + private String school; + + /** + * 备注 + */ + private String remark; + + +} diff --git a/src/main/java/com/ai/app/entity/Msg.java b/src/main/java/com/ai/app/entity/Msg.java new file mode 100644 index 0000000..988f193 --- /dev/null +++ b/src/main/java/com/ai/app/entity/Msg.java @@ -0,0 +1,37 @@ +package com.ai.app.entity; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.ai.chat.messages.*; + +import java.util.List; +import java.util.Map; + +@NoArgsConstructor +@AllArgsConstructor +@Data +public class Msg { + MessageType messageType; + String text; + Map metadata; + List toolCalls; + + public Msg(Message message) { + this.messageType = message.getMessageType(); + this.text = message.getText(); + this.metadata = message.getMetadata(); + if (message instanceof AssistantMessage am) { + this.toolCalls = am.getToolCalls(); + } + } + + public Message toMessage() { + return switch (messageType) { + case SYSTEM -> new SystemMessage(text); + case USER -> UserMessage.builder().text(text).media(List.of()).metadata(metadata).build(); + case ASSISTANT -> new AssistantMessage(text, metadata, toolCalls, List.of()); + default -> throw new IllegalArgumentException("Unsupported message type: " + messageType); + }; + } +} diff --git a/src/main/java/com/ai/app/entity/School.java b/src/main/java/com/ai/app/entity/School.java new file mode 100644 index 0000000..26568be --- /dev/null +++ b/src/main/java/com/ai/app/entity/School.java @@ -0,0 +1,44 @@ +package com.ai.app.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import java.io.Serializable; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + *

+ * 校区表 + *

+ * + * @author huge + * @since 2025-03-08 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Accessors(chain = true) +@TableName("school") +public class School implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 主键 + */ + @TableId(value = "id", type = IdType.AUTO) + private Integer id; + + /** + * 校区名称 + */ + private String name; + + /** + * 校区所在城市 + */ + private String city; + + +} diff --git a/src/main/java/com/ai/app/mapper/CourseMapper.java b/src/main/java/com/ai/app/mapper/CourseMapper.java new file mode 100644 index 0000000..a1733cf --- /dev/null +++ b/src/main/java/com/ai/app/mapper/CourseMapper.java @@ -0,0 +1,16 @@ +package com.ai.app.mapper; + +import com.ai.app.entity.Course; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + *

+ * 学科表 Mapper 接口 + *

+ * + * @author huge + * @since 2025-03-08 + */ +public interface CourseMapper extends BaseMapper { + +} diff --git a/src/main/java/com/ai/app/mapper/CourseReservationMapper.java b/src/main/java/com/ai/app/mapper/CourseReservationMapper.java new file mode 100644 index 0000000..6da5bc2 --- /dev/null +++ b/src/main/java/com/ai/app/mapper/CourseReservationMapper.java @@ -0,0 +1,17 @@ +package com.ai.app.mapper; + + +import com.ai.app.entity.CourseReservation; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + *

+ * Mapper 接口 + *

+ * + * @author huge + * @since 2025-03-08 + */ +public interface CourseReservationMapper extends BaseMapper { + +} diff --git a/src/main/java/com/ai/app/mapper/SchoolMapper.java b/src/main/java/com/ai/app/mapper/SchoolMapper.java new file mode 100644 index 0000000..81ad0d5 --- /dev/null +++ b/src/main/java/com/ai/app/mapper/SchoolMapper.java @@ -0,0 +1,16 @@ +package com.ai.app.mapper; + +import com.ai.app.entity.School; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + *

+ * 校区表 Mapper 接口 + *

+ * + * @author huge + * @since 2025-03-08 + */ +public interface SchoolMapper extends BaseMapper { + +} diff --git a/src/main/java/com/ai/app/query/CourseQuery.java b/src/main/java/com/ai/app/query/CourseQuery.java new file mode 100644 index 0000000..ee76063 --- /dev/null +++ b/src/main/java/com/ai/app/query/CourseQuery.java @@ -0,0 +1,24 @@ +package com.ai.app.query; + +import lombok.Data; +import org.springframework.ai.tool.annotation.ToolParam; + +import java.util.List; + +@Data +public class CourseQuery { + @ToolParam(required = false, description = "课程类型:编程、设计、自媒体、其它") + private String type; + @ToolParam(required = false, description = "学历要求:0-无、1-初中、2-高中、3-大专、4-本科及本科以上") + private Integer edu; + @ToolParam(required = false, description = "排序方式") + private List sorts; + + @Data + public static class Sort { + @ToolParam(required = false, description = "排序字段: price或duration") + private String field; + @ToolParam(required = false, description = "是否是升序: true/false") + private Boolean asc; + } +} \ No newline at end of file diff --git a/src/main/java/com/ai/app/service/ICourseReservationService.java b/src/main/java/com/ai/app/service/ICourseReservationService.java new file mode 100644 index 0000000..2234f9f --- /dev/null +++ b/src/main/java/com/ai/app/service/ICourseReservationService.java @@ -0,0 +1,16 @@ +package com.ai.app.service; + +import com.ai.app.entity.CourseReservation; +import com.baomidou.mybatisplus.extension.service.IService; + +/** + *

+ * 服务类 + *

+ * + * @author huge + * @since 2025-03-08 + */ +public interface ICourseReservationService extends IService { + +} diff --git a/src/main/java/com/ai/app/service/ICourseService.java b/src/main/java/com/ai/app/service/ICourseService.java new file mode 100644 index 0000000..5ea454d --- /dev/null +++ b/src/main/java/com/ai/app/service/ICourseService.java @@ -0,0 +1,16 @@ +package com.ai.app.service; + +import com.ai.app.entity.Course; +import com.baomidou.mybatisplus.extension.service.IService; + +/** + *

+ * 学科表 服务类 + *

+ * + * @author huge + * @since 2025-03-08 + */ +public interface ICourseService extends IService { + +} diff --git a/src/main/java/com/ai/app/service/ISchoolService.java b/src/main/java/com/ai/app/service/ISchoolService.java new file mode 100644 index 0000000..d54c5f2 --- /dev/null +++ b/src/main/java/com/ai/app/service/ISchoolService.java @@ -0,0 +1,16 @@ +package com.ai.app.service; + +import com.ai.app.entity.School; +import com.baomidou.mybatisplus.extension.service.IService; + +/** + *

+ * 校区表 服务类 + *

+ * + * @author huge + * @since 2025-03-08 + */ +public interface ISchoolService extends IService { + +} diff --git a/src/main/java/com/ai/app/service/impl/ChatHistoryServiceImpl.java b/src/main/java/com/ai/app/service/impl/ChatHistoryServiceImpl.java index 973e6b5..62201f1 100644 --- a/src/main/java/com/ai/app/service/impl/ChatHistoryServiceImpl.java +++ b/src/main/java/com/ai/app/service/impl/ChatHistoryServiceImpl.java @@ -7,7 +7,9 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * @describe @@ -18,10 +20,11 @@ import java.util.List; @Service public class ChatHistoryServiceImpl implements ChatHistoryService { - @Autowired - private RedisTemplateService redisTemplateService; + /*@Autowired + private RedisTemplateService redisTemplateService;*/ + + private final Map> chatStore = new HashMap<>(); - public static final String CHAT_TYPE_KEY = "ai:history:type:"; /** * 保存会话ID @@ -34,18 +37,11 @@ public class ChatHistoryServiceImpl implements ChatHistoryService { */ @Override public void saveHistoryChatId(String type, String chatId) { - String key = CHAT_TYPE_KEY + type; - List chatIds; - if (redisTemplateService.hasKey(key)) { - chatIds = (List) redisTemplateService.get(key); - } else { - chatIds = new ArrayList<>(); - } + List chatIds = chatStore.computeIfAbsent(type, k -> new ArrayList<>()); if (chatIds.contains(chatId)) { return; } chatIds.add(chatId); - redisTemplateService.set(key, chatIds); } /** @@ -58,10 +54,6 @@ public class ChatHistoryServiceImpl implements ChatHistoryService { */ @Override public List getHistoryChatIds(String type) { - String key = CHAT_TYPE_KEY + type; - if (redisTemplateService.hasKey(key)) { - return (List) redisTemplateService.get(key); - } - return List.of(); + return chatStore.getOrDefault(type, List.of()); } } diff --git a/src/main/java/com/ai/app/service/impl/CourseReservationServiceImpl.java b/src/main/java/com/ai/app/service/impl/CourseReservationServiceImpl.java new file mode 100644 index 0000000..9b07c1f --- /dev/null +++ b/src/main/java/com/ai/app/service/impl/CourseReservationServiceImpl.java @@ -0,0 +1,20 @@ +package com.ai.app.service.impl; + +import com.ai.app.entity.CourseReservation; +import com.ai.app.mapper.CourseReservationMapper; +import com.ai.app.service.ICourseReservationService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.stereotype.Service; + +/** + *

+ * 服务实现类 + *

+ * + * @author huge + * @since 2025-03-08 + */ +@Service +public class CourseReservationServiceImpl extends ServiceImpl implements ICourseReservationService { + +} diff --git a/src/main/java/com/ai/app/service/impl/CourseServiceImpl.java b/src/main/java/com/ai/app/service/impl/CourseServiceImpl.java new file mode 100644 index 0000000..6a69e1a --- /dev/null +++ b/src/main/java/com/ai/app/service/impl/CourseServiceImpl.java @@ -0,0 +1,20 @@ +package com.ai.app.service.impl; + +import com.ai.app.entity.Course; +import com.ai.app.mapper.CourseMapper; +import com.ai.app.service.ICourseService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.stereotype.Service; + +/** + *

+ * 学科表 服务实现类 + *

+ * + * @author huge + * @since 2025-03-08 + */ +@Service +public class CourseServiceImpl extends ServiceImpl implements ICourseService { + +} diff --git a/src/main/java/com/ai/app/service/impl/SchoolServiceImpl.java b/src/main/java/com/ai/app/service/impl/SchoolServiceImpl.java new file mode 100644 index 0000000..21a3c5d --- /dev/null +++ b/src/main/java/com/ai/app/service/impl/SchoolServiceImpl.java @@ -0,0 +1,21 @@ +package com.ai.app.service.impl; + + +import com.ai.app.entity.School; +import com.ai.app.mapper.SchoolMapper; +import com.ai.app.service.ISchoolService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.stereotype.Service; + +/** + *

+ * 校区表 服务实现类 + *

+ * + * @author huge + * @since 2025-03-08 + */ +@Service +public class SchoolServiceImpl extends ServiceImpl implements ISchoolService { + +} diff --git a/src/main/java/com/ai/app/tools/CourseTools.java b/src/main/java/com/ai/app/tools/CourseTools.java new file mode 100644 index 0000000..7bb09b4 --- /dev/null +++ b/src/main/java/com/ai/app/tools/CourseTools.java @@ -0,0 +1,64 @@ +package com.ai.app.tools; + +import com.ai.app.entity.Course; +import com.ai.app.entity.CourseReservation; +import com.ai.app.entity.School; +import com.ai.app.query.CourseQuery; +import com.ai.app.service.ICourseReservationService; +import com.ai.app.service.ICourseService; +import com.ai.app.service.ISchoolService; +import com.baomidou.mybatisplus.extension.conditions.query.QueryChainWrapper; +import lombok.RequiredArgsConstructor; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.ai.tool.annotation.ToolParam; +import org.springframework.stereotype.Component; + +import java.util.List; + +@RequiredArgsConstructor +@Component +public class CourseTools { + + private final ICourseService courseService; + private final ISchoolService schoolService; + private final ICourseReservationService reservationService; + + @Tool(description = "根据条件查询课程") + public List queryCourse(@ToolParam(description = "查询的条件", required = false) CourseQuery query) { + if (query == null) { + return courseService.list(); + } + QueryChainWrapper wrapper = courseService.query() + .eq(query.getType() != null, "type", query.getType()) // type = '编程' + .le(query.getEdu() != null, "edu", query.getEdu());// edu <= 2 + if (query.getSorts() != null && !query.getSorts().isEmpty()) { + for (CourseQuery.Sort sort : query.getSorts()) { + wrapper.orderBy(true, sort.getAsc(), sort.getField()); + } + } + return wrapper.list(); + } + + @Tool(description = "查询所有校区") + public List querySchool() { + return schoolService.list(); + } + + @Tool(description = "生成预约单,返回预约单号") + public Integer createCourseReservation( + @ToolParam(description = "预约课程") String course, + @ToolParam(description = "预约校区") String school, + @ToolParam(description = "学生姓名") String studentName, + @ToolParam(description = "联系电话") String contactInfo, + @ToolParam(description = "备注", required = false) String remark) { + CourseReservation reservation = new CourseReservation(); + reservation.setCourse(course); + reservation.setSchool(school); + reservation.setStudentName(studentName); + reservation.setContactInfo(contactInfo); + reservation.setRemark(remark); + reservationService.save(reservation); + + return reservation.getId(); + } +} diff --git a/src/main/java/com/ai/app/vo/MessageVO.java b/src/main/java/com/ai/app/vo/MessageVO.java new file mode 100644 index 0000000..5266316 --- /dev/null +++ b/src/main/java/com/ai/app/vo/MessageVO.java @@ -0,0 +1,27 @@ +package com.ai.app.vo; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.ai.chat.messages.Message; + +@NoArgsConstructor +@Data +public class MessageVO { + private String role; + private String content; + + public MessageVO(Message message) { + switch (message.getMessageType()) { + case USER: + role = "user"; + break; + case ASSISTANT: + role = "assistant"; + break; + default: + role = ""; + break; + } + this.content = message.getText(); + } +} diff --git a/src/main/java/com/ai/app/vo/Result.java b/src/main/java/com/ai/app/vo/Result.java new file mode 100644 index 0000000..74ec98f --- /dev/null +++ b/src/main/java/com/ai/app/vo/Result.java @@ -0,0 +1,24 @@ +package com.ai.app.vo; + +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class Result { + private Integer ok; + private String msg; + + private Result(Integer ok, String msg) { + this.ok = ok; + this.msg = msg; + } + + public static Result ok() { + return new Result(1, "ok"); + } + + public static Result fail(String msg) { + return new Result(0, msg); + } +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 7c90d5e..75d29ed 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -45,7 +45,7 @@ spring: options: model: deepseek-reasoner dashscope: - api-key: ${DASHSCOPE_API_KEY} + api-key: ${DASH_SCOPE_API_KEY} chat: options: model: qwen-max