diff --git a/manager/src/main/java/org/jiayunet/controller/MentorMockChatController.java b/manager/src/main/java/org/jiayunet/controller/MentorMockChatController.java new file mode 100644 index 0000000..cf215e0 --- /dev/null +++ b/manager/src/main/java/org/jiayunet/controller/MentorMockChatController.java @@ -0,0 +1,29 @@ +package org.jiayunet.controller; + +import lombok.AllArgsConstructor; +import org.jiayunet.pojo.dto.mentor.MentorMockChatDto; +import org.jiayunet.service.MentorMockChatService; +import org.springframework.web.bind.annotation.*; + +/** + * 导师模拟对话接口 + * + * @author zk + */ +@RestController +@RequestMapping("/public/mentor") +@AllArgsConstructor +public class MentorMockChatController { + + private final MentorMockChatService mentorMockChatService; + + /** + * 生成导师模拟对话 + * + * @param mentorId 导师ID + */ + @GetMapping("/mockChat") + public MentorMockChatDto generateMockChat(@RequestParam Long mentorId) { + return mentorMockChatService.generate(mentorId); + } +} diff --git a/manager/src/main/java/org/jiayunet/mapper/MentorMockChatMapper.java b/manager/src/main/java/org/jiayunet/mapper/MentorMockChatMapper.java new file mode 100644 index 0000000..c4beef5 --- /dev/null +++ b/manager/src/main/java/org/jiayunet/mapper/MentorMockChatMapper.java @@ -0,0 +1,12 @@ +package org.jiayunet.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.jiayunet.pojo.po.MentorMockChat; + +/** + * 导师模拟对话Mapper + * + * @author zk + */ +public interface MentorMockChatMapper extends BaseMapper { +} diff --git a/manager/src/main/java/org/jiayunet/pojo/dto/mentor/MentorMockChatDto.java b/manager/src/main/java/org/jiayunet/pojo/dto/mentor/MentorMockChatDto.java new file mode 100644 index 0000000..2159fee --- /dev/null +++ b/manager/src/main/java/org/jiayunet/pojo/dto/mentor/MentorMockChatDto.java @@ -0,0 +1,29 @@ +package org.jiayunet.pojo.dto.mentor; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 导师模拟对话DTO + * + * @author zk + */ +@Data +public class MentorMockChatDto { + + /** 对话消息列表 */ + private List messages; + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class Message { + /** 角色:user=用户,mentor=导师 */ + private String role; + /** 消息内容 */ + private String content; + } +} diff --git a/manager/src/main/java/org/jiayunet/pojo/po/MentorMockChat.java b/manager/src/main/java/org/jiayunet/pojo/po/MentorMockChat.java new file mode 100644 index 0000000..e2cd4ae --- /dev/null +++ b/manager/src/main/java/org/jiayunet/pojo/po/MentorMockChat.java @@ -0,0 +1,43 @@ +package org.jiayunet.pojo.po; + +import com.baomidou.mybatisplus.annotation.*; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.Data; + +import java.time.Instant; +import java.util.List; + +/** + * 导师模拟对话表(bg_mentor_mock_chat) + *

缓存AI生成的导师模拟对话,按导师ID唯一

+ * + * @author zk + */ +@Data +@TableName(value = "bg_mentor_mock_chat", autoResultMap = true) +public class MentorMockChat { + + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** 导师ID */ + private Long mentorId; + + /** 对话消息列表(JSON数组) */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List messages; + + /** 创建时间 */ + private Instant createTime; + + /** + * 对话消息项 + */ + @Data + public static class ChatMessage { + /** 角色:user=用户,mentor=导师 */ + private String role; + /** 消息内容 */ + private String content; + } +} diff --git a/manager/src/main/java/org/jiayunet/service/MentorMockChatService.java b/manager/src/main/java/org/jiayunet/service/MentorMockChatService.java new file mode 100644 index 0000000..28c12d0 --- /dev/null +++ b/manager/src/main/java/org/jiayunet/service/MentorMockChatService.java @@ -0,0 +1,175 @@ +package org.jiayunet.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.fasterxml.jackson.core.type.TypeReference; +import lombok.extern.slf4j.Slf4j; +import org.jiayunet.ai.AiChatAbility; +import org.jiayunet.ai.AiResponseCleanTool; +import org.jiayunet.mapper.MentorMockChatMapper; +import org.jiayunet.pojo.dto.mentor.MentorMockChatDto; +import org.jiayunet.pojo.po.MentorMockChat; +import org.jiayunet.remote.jsxq.JsxqClient; +import org.jiayunet.remote.jsxq.pojo.dto.MentorFullInfoDto; +import org.jiayunet.remote.jsxq.pojo.dto.TeacherTalkDto; +import org.jiayunet.tool.HttpTool; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; + +import java.time.Instant; +import java.util.List; + +/** + * 导师模拟对话生成服务 + *

依赖:JsxqClient(获取导师信息和话题)、AiChatAbility(AI生成对话)、MentorMockChatMapper(缓存入库)

+ *

使用表:bg_mentor_mock_chat(查询/写入缓存)

+ * + * @author zk + */ +@Slf4j +@Service +public class MentorMockChatService { + + @Autowired + private JsxqClient jsxqClient; + + @Autowired + private AiChatAbility aiChatAbility; + + @Autowired + private MentorMockChatMapper mentorMockChatMapper; + + /** + * 获取导师模拟对话 + *

1. 查库,有缓存直接返回 2. 无缓存则获取导师信息+随机话题 3. AI生成 4. 入库 5. 返回

+ * + * @param mentorId 导师ID + * @return 模拟对话 + */ + public MentorMockChatDto generate(Long mentorId) { + // 1. 查缓存 + MentorMockChat existing = mentorMockChatMapper.selectOne(new LambdaQueryWrapper().eq(MentorMockChat::getMentorId, mentorId)); + if (existing != null) { + MentorMockChatDto dto = new MentorMockChatDto(); + dto.setMessages(existing.getMessages().stream() + .map(m -> new MentorMockChatDto.Message(m.getRole(), m.getContent())) + .toList()); + return dto; + } + + // 2. 获取导师信息 + MentorFullInfoDto mentorInfo = jsxqClient.selectMentorFullInfo(mentorId); + Assert.notNull(mentorInfo, "导师不存在"); + + // 3. 获取话题列表(无话题时仅用导师资料生成) + List talks = jsxqClient.selectTalk(mentorInfo.getId()); + + // 4. 构建prompt,调用AI生成 + String systemPrompt = buildSystemPrompt(); + String userMessage = buildUserMessage(mentorInfo, talks); + String aiResponse = aiChatAbility.chat(systemPrompt, userMessage); + String json = AiResponseCleanTool.clean(aiResponse); + + // 5. 解析 + List chatMessages; + try { + chatMessages = HttpTool.objectMapper.readValue(json, new TypeReference<>() {}); + } catch (Exception e) { + log.warn("模拟对话AI返回解析失败: {}", json, e); + throw new RuntimeException("模拟对话生成失败", e); + } + + // 6. 入库 + MentorMockChat record = new MentorMockChat(); + record.setMentorId(mentorId); + record.setMessages(chatMessages); + record.setCreateTime(Instant.now()); + mentorMockChatMapper.insert(record); + + // 7. 返回 + MentorMockChatDto dto = new MentorMockChatDto(); + dto.setMessages(chatMessages.stream() + .map(m -> new MentorMockChatDto.Message(m.getRole(), m.getContent())) + .toList()); + return dto; + } + + /** + * 构建系统提示词 + */ + private String buildSystemPrompt() { + return """ + 你是一个对话生成器。根据提供的导师资料和话题信息,生成一段求职者与导师之间的模拟对话。 + + 要求: + 1. 对话轮数:2~3轮(随机),每轮由一条用户消息+一条导师消息组成 + 2. 用户消息:简短口语化,10~30字,带有真实困惑或追问 + 3. 导师消息:字数浮动,有长有短,短的10~30字,长的40~80字,体现专业性 + 4. 对话内容必须与导师的真实职业背景和话题方向强相关 + 5. 语气自然,像真实的即时通讯对话,不要书面化 + 6. 用户是求职者/应届生身份,导师根据自己的行业经验给出建议 + + 返回JSON数组格式: + [{"role":"user","content":"..."},{"role":"mentor","content":"..."},...] + + 只返回JSON,不要任何额外说明。"""; + } + + /** + * 构建用户消息(导师资料+全部话题信息) + */ + private String buildUserMessage(MentorFullInfoDto mentorInfo, List talks) { + StringBuilder sb = new StringBuilder(); + sb.append("【导师资料】\n"); + + MentorFullInfoDto.MentorInfo info = mentorInfo.getMentorInfo(); + if (info != null) { + if (info.getNickName() != null) sb.append("昵称:").append(info.getNickName()).append("\n"); + if (info.getIntroduce() != null) sb.append("介绍:").append(info.getIntroduce()).append("\n"); + if (info.getEverCompany() != null) sb.append("曾任公司:").append(info.getEverCompany()).append("\n"); + } + + // 认证信息 + MentorFullInfoDto.Verification v = mentorInfo.getVerification(); + if (v != null) { + if (v.getWorking() != null) { + sb.append("公司:").append(v.getWorking().getCompanyName()) + .append(" 职位:").append(v.getWorking().getPosition()) + .append(" 从业").append(v.getWorking().getTerm()).append("年\n"); + } else if (v.getStaff() != null) { + sb.append("院校:").append(v.getStaff().getSchoolName()) + .append(" 职位:").append(v.getStaff().getPosition()).append("\n"); + } else if (v.getStudent() != null) { + sb.append("院校:").append(v.getStudent().getSchoolName()) + .append(" 专业:").append(v.getStudent().getMajor()).append("\n"); + } else if (v.getCareer() != null) { + sb.append("机构:").append(v.getCareer().getAgencyName()) + .append(" 职位:").append(v.getCareer().getPosition()) + .append(" 从业").append(v.getCareer().getTerm()).append("年\n"); + } + } + + // 标签 + List labels = mentorInfo.getLabelRelations(); + if (labels != null && !labels.isEmpty()) { + sb.append("标签:"); + labels.forEach(l -> sb.append(l.getLabelName()).append("、")); + sb.setLength(sb.length() - 1); + sb.append("\n"); + } + + // 全部话题 + if (talks != null && !talks.isEmpty()) { + sb.append("\n【话题列表】\n"); + for (int i = 0; i < talks.size(); i++) { + TeacherTalkDto talk = talks.get(i); + sb.append(i + 1).append(". ").append(talk.getTitle()); + if (talk.getContent() != null) sb.append("(").append(talk.getContent()).append(")"); + if (talk.getTags() != null) sb.append(" [").append(talk.getTags()).append("]"); + sb.append("\n"); + } + } + + return sb.toString(); + } +}