From 80dcb23bbc43190f35a491f34a5c2ced8c26eec3 Mon Sep 17 00:00:00 2001 From: qingqiu <1764183241@qq.com> Date: Mon, 24 Nov 2025 13:37:37 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BD=BF=E7=94=A8spring-ai-alibaba=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E9=A1=B9=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 7 +- .../interview/annotation/AiChatLog.java | 15 -- .../interview/aspect/AiChatLogAspect.java | 71 -------- ...ava => SpringApplicationContextUtils.java} | 4 +- .../utils/{TreeUtil.java => TreeUtils.java} | 2 +- .../common/utils/WebClientUtils.java | 46 +++++ .../interview/config/DashScopeConfig.java | 101 +---------- .../interview/config/OpenAiChatConfig.java | 100 +++++++++++ .../controller/InterviewController.java | 8 + .../ai/QuestionClassificationAiRes.java | 22 +++ .../entity/ai/QuestionDeduplicationAiRes.java | 18 ++ .../interview/service/DashboardService.java | 74 ++------ .../interview/service/InterviewService.java | 9 + .../QuestionClassificationService.java | 165 ++---------------- .../service/impl/DashboardServiceImpl.java | 61 +++++++ .../service/impl/InterviewAiServiceImpl.java | 5 + .../impl/InterviewChatServiceImpl.java | 2 + .../service/impl/InterviewServiceImpl.java | 6 +- .../QuestionClassificationServiceImpl.java | 77 ++++++++ .../service/impl/QuestionServiceImpl.java | 160 +++++++++++------ src/main/resources/application.yml | 11 +- 21 files changed, 514 insertions(+), 450 deletions(-) delete mode 100644 src/main/java/com/qingqiu/interview/annotation/AiChatLog.java delete mode 100644 src/main/java/com/qingqiu/interview/aspect/AiChatLogAspect.java rename src/main/java/com/qingqiu/interview/common/utils/{SpringApplicationContextUtil.java => SpringApplicationContextUtils.java} (88%) mode change 100755 => 100644 rename src/main/java/com/qingqiu/interview/common/utils/{TreeUtil.java => TreeUtils.java} (98%) mode change 100755 => 100644 create mode 100644 src/main/java/com/qingqiu/interview/common/utils/WebClientUtils.java create mode 100644 src/main/java/com/qingqiu/interview/config/OpenAiChatConfig.java create mode 100644 src/main/java/com/qingqiu/interview/entity/ai/QuestionClassificationAiRes.java create mode 100644 src/main/java/com/qingqiu/interview/entity/ai/QuestionDeduplicationAiRes.java mode change 100755 => 100644 src/main/java/com/qingqiu/interview/service/DashboardService.java mode change 100755 => 100644 src/main/java/com/qingqiu/interview/service/QuestionClassificationService.java create mode 100644 src/main/java/com/qingqiu/interview/service/impl/DashboardServiceImpl.java create mode 100644 src/main/java/com/qingqiu/interview/service/impl/QuestionClassificationServiceImpl.java diff --git a/pom.xml b/pom.xml index 3a6b3ad..ac00b31 100755 --- a/pom.xml +++ b/pom.xml @@ -13,6 +13,7 @@ 0.0.1-SNAPSHOT AI-Interview AI-Interview + @@ -56,6 +57,7 @@ spring-boot-starter-webflux + @@ -72,6 +74,7 @@ spring-ai-alibaba-starter-dashscope + com.alibaba.cloud.ai spring-ai-alibaba-starter-memory @@ -100,7 +103,7 @@ spring-boot-starter-data-redis - + @@ -198,11 +201,13 @@ ai-interview + org.springframework.boot spring-boot-maven-plugin 3.5.0 + org.apache.maven.plugins diff --git a/src/main/java/com/qingqiu/interview/annotation/AiChatLog.java b/src/main/java/com/qingqiu/interview/annotation/AiChatLog.java deleted file mode 100644 index fb32e51..0000000 --- a/src/main/java/com/qingqiu/interview/annotation/AiChatLog.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.qingqiu.interview.annotation; - -import java.lang.annotation.*; - -/** - *

- * - * @author qingqiu - * @date 2025/9/18 12:58 - */ -@Documented -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -public @interface AiChatLog { -} diff --git a/src/main/java/com/qingqiu/interview/aspect/AiChatLogAspect.java b/src/main/java/com/qingqiu/interview/aspect/AiChatLogAspect.java deleted file mode 100644 index 07f0e05..0000000 --- a/src/main/java/com/qingqiu/interview/aspect/AiChatLogAspect.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.qingqiu.interview.aspect; - -import com.qingqiu.interview.dto.ChatDTO; -import com.qingqiu.interview.entity.AiSessionLog; -import com.qingqiu.interview.service.IAiSessionLogService; -import com.qingqiu.interview.vo.ChatVO; -import jakarta.annotation.Resource; -import org.apache.commons.lang3.StringUtils; -import org.aspectj.lang.ProceedingJoinPoint; -import org.aspectj.lang.annotation.Around; -import org.aspectj.lang.annotation.Aspect; -import org.aspectj.lang.annotation.Pointcut; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -/** - *

- * ai聊天的切面 - *

- * - * @author qingqiu - * @date 2025/9/18 13:00 - */ -@Aspect -@Component -public class AiChatLogAspect { - - @Resource - private IAiSessionLogService aiSessionLogService; - - public AiChatLogAspect() { - - } - - @Pointcut("@annotation(com.qingqiu.interview.annotation.AiChatLog)") - public void logPointCut() { - } - - @Around("logPointCut()") - @Transactional(rollbackFor = Exception.class) - public Object around(ProceedingJoinPoint point) throws Throwable { - - Object[] args = point.getArgs(); - ChatDTO arg = (ChatDTO) args[0]; - if (StringUtils.isNoneBlank(arg.getSessionId())) { - AiSessionLog userSessionLog = new AiSessionLog(); - userSessionLog - .setRole(arg.getRole()) - .setDataType(arg.getDataType()) - .setContent(arg.getContent()) - .setToken(arg.getSessionId()) - ; - aiSessionLogService.save(userSessionLog); - } - - - Object result = point.proceed(); - - ChatVO chatVO = (ChatVO) result; - if (StringUtils.isNotBlank(chatVO.getSessionId())) { - AiSessionLog aiSessionLog = new AiSessionLog(); - aiSessionLog - .setRole(chatVO.getRole()) - .setContent(chatVO.getContent()) - .setToken(chatVO.getSessionId()) - ; - aiSessionLogService.save(aiSessionLog); - } - return result; - } -} diff --git a/src/main/java/com/qingqiu/interview/common/utils/SpringApplicationContextUtil.java b/src/main/java/com/qingqiu/interview/common/utils/SpringApplicationContextUtils.java old mode 100755 new mode 100644 similarity index 88% rename from src/main/java/com/qingqiu/interview/common/utils/SpringApplicationContextUtil.java rename to src/main/java/com/qingqiu/interview/common/utils/SpringApplicationContextUtils.java index 04e28d6..877f775 --- a/src/main/java/com/qingqiu/interview/common/utils/SpringApplicationContextUtil.java +++ b/src/main/java/com/qingqiu/interview/common/utils/SpringApplicationContextUtils.java @@ -6,12 +6,12 @@ import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Component; @Component -public class SpringApplicationContextUtil implements ApplicationContextAware { +public class SpringApplicationContextUtils implements ApplicationContextAware { private static ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - SpringApplicationContextUtil.applicationContext = applicationContext; + SpringApplicationContextUtils.applicationContext = applicationContext; } public static T getBean(Class beanClass) { diff --git a/src/main/java/com/qingqiu/interview/common/utils/TreeUtil.java b/src/main/java/com/qingqiu/interview/common/utils/TreeUtils.java old mode 100755 new mode 100644 similarity index 98% rename from src/main/java/com/qingqiu/interview/common/utils/TreeUtil.java rename to src/main/java/com/qingqiu/interview/common/utils/TreeUtils.java index f343a8a..4d5b21f --- a/src/main/java/com/qingqiu/interview/common/utils/TreeUtil.java +++ b/src/main/java/com/qingqiu/interview/common/utils/TreeUtils.java @@ -7,7 +7,7 @@ import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; -public class TreeUtil { +public class TreeUtils { /** * 通用树形结构构建方法 diff --git a/src/main/java/com/qingqiu/interview/common/utils/WebClientUtils.java b/src/main/java/com/qingqiu/interview/common/utils/WebClientUtils.java new file mode 100644 index 0000000..182bd4d --- /dev/null +++ b/src/main/java/com/qingqiu/interview/common/utils/WebClientUtils.java @@ -0,0 +1,46 @@ +package com.qingqiu.interview.common.utils; + + +import io.netty.channel.ChannelOption; +import org.springframework.http.client.ReactorClientHttpRequestFactory; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.web.client.RestClient; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.netty.http.client.HttpClient; + +import java.time.Duration; + +/** + * @program: ai-interview + * @description: + * @author: huangpeng + * @create: 2025-11-06 20:04 + **/ +public class WebClientUtils { + + /** + * 创建全局默认的WebClient Bean实例 + * 配置了连接超时和响应超时时间 + * + * @return WebClient实例 + */ + public static WebClient.Builder getWebClientBuilder() { + HttpClient httpClient = HttpClient.create() + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 30000) // 连接超时 + .responseTimeout(Duration.ofMillis(30000)); // 读取超时 + + return WebClient.builder() + .clientConnector(new ReactorClientHttpConnector(httpClient)) + ; + } + + public static RestClient.Builder getRestClientBuilder() { + return RestClient.builder() + .requestFactory( + new ReactorClientHttpRequestFactory( + HttpClient.create() + .responseTimeout(Duration.ofMinutes(30)) + ) + ); + } +} diff --git a/src/main/java/com/qingqiu/interview/config/DashScopeConfig.java b/src/main/java/com/qingqiu/interview/config/DashScopeConfig.java index 31cb174..31b22e0 100755 --- a/src/main/java/com/qingqiu/interview/config/DashScopeConfig.java +++ b/src/main/java/com/qingqiu/interview/config/DashScopeConfig.java @@ -5,28 +5,19 @@ import com.alibaba.cloud.ai.dashscope.api.DashScopeResponseFormat; import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; import com.qingqiu.interview.common.constants.ChatConstant; -import io.netty.channel.ChannelOption; import jakarta.annotation.Resource; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor; import org.springframework.ai.chat.memory.MessageWindowChatMemory; import org.springframework.ai.chat.model.ChatModel; -import org.springframework.ai.openai.OpenAiChatModel; -import org.springframework.ai.openai.OpenAiChatOptions; -import org.springframework.ai.openai.api.OpenAiApi; -import org.springframework.ai.openai.api.ResponseFormat; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; -import org.springframework.http.client.ReactorClientHttpRequestFactory; -import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.web.client.RestClient; -import org.springframework.web.reactive.function.client.WebClient; -import reactor.netty.http.client.HttpClient; -import java.time.Duration; +import static com.qingqiu.interview.common.utils.WebClientUtils.getRestClientBuilder; +import static com.qingqiu.interview.common.utils.WebClientUtils.getWebClientBuilder; /** * DashScope相关配置类 @@ -45,21 +36,11 @@ public class DashScopeConfig { @Value("${spring.ai.dashscope.chat.options.model}") private String dashScopeChatModelName; - @Value("${spring.ai.openai.chat.options.model}") - private String openAiChatModelName; - @Value("${spring.ai.openai.base-url}") - private String openAiBaseUrl; - - @Value("${spring.ai.openai.api-key}") - private String openAiApiKey; @Resource(name = ChatConstant.MYSQL_CHAT_MEMORY_BEAN_NAME) private MessageWindowChatMemory mysqlChatMemory; - @Resource(name = ChatConstant.REDIS_CHAT_MEMORY_BEAN_NAME) - private MessageWindowChatMemory redisChatMemory; - /** * 创建DashScopeApi Bean实例 * 配置了HTTP客户端连接参数,包括连接超时和响应超时时间 @@ -100,42 +81,6 @@ public class DashScopeConfig { .build(); } - /** - * 创建OpenAI聊天模型Bean实例(主模型) - * 配置了最大完成token数和响应格式为JSON Schema - * - * @return ChatModel实例 - */ - @Bean(name = ChatConstant.OPEN_AI_CHAT_MODEL_BEAN_NAME) - @Primary - public ChatModel openAiChatModel() { - return OpenAiChatModel.builder() - .openAiApi( - OpenAiApi.builder() - .apiKey(openAiApiKey) - .baseUrl(openAiBaseUrl) - .webClientBuilder(getWebClientBuilder()) - .restClientBuilder(getRestClientBuilder()) - .build() - ) - .defaultOptions( - OpenAiChatOptions.builder() - .model(openAiChatModelName) - .maxCompletionTokens(ChatConstant.MAX_COMPLETION_TOKENS) - - .responseFormat( - ResponseFormat.builder() - .type(ResponseFormat.Type.JSON_SCHEMA) - .jsonSchema( - ResponseFormat.JsonSchema - .builder() - .build() - ).build() - ) - .build() - ) - .build(); - } /** * 创建默认的聊天客户端Bean实例(使用DashScope模型) @@ -171,47 +116,5 @@ public class DashScopeConfig { .build(); } - /** - * 创建基于MySQL存储聊天历史的OpenAI聊天客户端Bean实例 - * 配置了消息内存管理和日志记录功能 - * - * @return ChatClient实例 - */ - @Bean(name = ChatConstant.OPEN_AI_CHAT_CLIENT_BEAN_NAME) - public ChatClient openAiChatClient() { - return ChatClient - .builder(openAiChatModel()) - .defaultAdvisors( - MessageChatMemoryAdvisor.builder(mysqlChatMemory).build(), - new SimpleLoggerAdvisor() - ) - .build(); - } - - /** - * 创建全局默认的WebClient Bean实例 - * 配置了连接超时和响应超时时间 - * - * @return WebClient实例 - */ - public WebClient.Builder getWebClientBuilder() { - HttpClient httpClient = HttpClient.create() - .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) // 连接超时 - .responseTimeout(Duration.ofMillis(10000)); // 读取超时 - - return WebClient.builder() - .clientConnector(new ReactorClientHttpConnector(httpClient)) - ; - } - - private RestClient.Builder getRestClientBuilder() { - return RestClient.builder() - .requestFactory( - new ReactorClientHttpRequestFactory( - HttpClient.create() - .responseTimeout(Duration.ofMinutes(5)) - ) - ); - } } \ No newline at end of file diff --git a/src/main/java/com/qingqiu/interview/config/OpenAiChatConfig.java b/src/main/java/com/qingqiu/interview/config/OpenAiChatConfig.java new file mode 100644 index 0000000..8ee996f --- /dev/null +++ b/src/main/java/com/qingqiu/interview/config/OpenAiChatConfig.java @@ -0,0 +1,100 @@ +package com.qingqiu.interview.config; + + +import com.qingqiu.interview.common.constants.ChatConstant; +import jakarta.annotation.Resource; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; +import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor; +import org.springframework.ai.chat.memory.MessageWindowChatMemory; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.ai.openai.OpenAiChatOptions; +import org.springframework.ai.openai.api.OpenAiApi; +import org.springframework.ai.openai.api.ResponseFormat; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +import static com.qingqiu.interview.common.utils.WebClientUtils.getRestClientBuilder; +import static com.qingqiu.interview.common.utils.WebClientUtils.getWebClientBuilder; + +/** + * @program: ai-interview + * @description: openai聊天配置 + * @author: huangpeng + * @create: 2025-11-06 20:03 + **/ +@Configuration +public class OpenAiChatConfig { + + + @Value("${spring.ai.openai.chat.options.model}") + private String openAiChatModelName; + + @Value("${spring.ai.openai.base-url}") + private String openAiBaseUrl; + + @Value("${spring.ai.openai.api-key}") + private String openAiApiKey; + + @Resource(name = ChatConstant.MYSQL_CHAT_MEMORY_BEAN_NAME) + private MessageWindowChatMemory mysqlChatMemory; + + + /** + * 创建OpenAI聊天模型Bean实例(主模型) + * 配置了最大完成token数和响应格式为JSON Schema + * + * @return ChatModel实例 + */ + @Bean(name = ChatConstant.OPEN_AI_CHAT_MODEL_BEAN_NAME) + @Primary + public ChatModel openAiChatModel() { + return OpenAiChatModel.builder() + .openAiApi( + OpenAiApi.builder() + .apiKey(openAiApiKey) + .baseUrl(openAiBaseUrl) + .webClientBuilder(getWebClientBuilder()) + .restClientBuilder(getRestClientBuilder()) + .build() + ) + .defaultOptions( + OpenAiChatOptions.builder() + .model(openAiChatModelName) + .maxCompletionTokens(ChatConstant.MAX_COMPLETION_TOKENS) + + .responseFormat( + ResponseFormat.builder() + .type(ResponseFormat.Type.JSON_SCHEMA) + .jsonSchema( + ResponseFormat.JsonSchema + .builder() + .build() + ).build() + ) + .build() + ) + .build(); + } + + + /** + * 创建基于MySQL存储聊天历史的OpenAI聊天客户端Bean实例 + * 配置了消息内存管理和日志记录功能 + * + * @return ChatClient实例 + */ + @Bean(name = ChatConstant.OPEN_AI_CHAT_CLIENT_BEAN_NAME) + public ChatClient openAiChatClient() { + return ChatClient + .builder(openAiChatModel()) + .defaultAdvisors( + MessageChatMemoryAdvisor.builder(mysqlChatMemory).build(), + new SimpleLoggerAdvisor() + ) + .build(); + } +} diff --git a/src/main/java/com/qingqiu/interview/controller/InterviewController.java b/src/main/java/com/qingqiu/interview/controller/InterviewController.java index 863b78d..4d67985 100644 --- a/src/main/java/com/qingqiu/interview/controller/InterviewController.java +++ b/src/main/java/com/qingqiu/interview/controller/InterviewController.java @@ -13,6 +13,7 @@ import org.springframework.context.annotation.Lazy; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; +import java.io.IOException; import java.util.List; /** @@ -122,4 +123,11 @@ public class InterviewController { public R getInterviewReportDetail(@PathVariable String sessionId) { return R.success(interviewService.getInterviewReport(sessionId)); } + + @PostMapping("/read-pdf") + public R readPdf(@RequestParam MultipartFile file) throws IOException { + String readPdfFile = interviewService.readPdfFile(file); + log.info("resume content: {}", readPdfFile); + return R.success(readPdfFile); + } } diff --git a/src/main/java/com/qingqiu/interview/entity/ai/QuestionClassificationAiRes.java b/src/main/java/com/qingqiu/interview/entity/ai/QuestionClassificationAiRes.java new file mode 100644 index 0000000..8742073 --- /dev/null +++ b/src/main/java/com/qingqiu/interview/entity/ai/QuestionClassificationAiRes.java @@ -0,0 +1,22 @@ +package com.qingqiu.interview.entity.ai; + + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * @program: ai-interview + * @description: 题型分类ai返回 + * @author: huangpeng + * @create: 2025-11-06 20:08 + **/ +public class QuestionClassificationAiRes { + + public record Item(@JsonProperty("content") String content, + @JsonProperty("category") String category, + @JsonProperty("difficulty") String difficulty, + @JsonProperty("tags") String tags) {} + + public record Wrapper(@JsonProperty("questions")List questions) {} +} diff --git a/src/main/java/com/qingqiu/interview/entity/ai/QuestionDeduplicationAiRes.java b/src/main/java/com/qingqiu/interview/entity/ai/QuestionDeduplicationAiRes.java new file mode 100644 index 0000000..7d800a2 --- /dev/null +++ b/src/main/java/com/qingqiu/interview/entity/ai/QuestionDeduplicationAiRes.java @@ -0,0 +1,18 @@ +package com.qingqiu.interview.entity.ai; + + +import com.fasterxml.jackson.annotation.JsonProperty; +import redis.clients.jedis.graph.Statistics; + +import java.util.List; + +/** + * @program: ai-interview + * @description: 题目去重AI返回结果 + * @author: huangpeng + * @create: 2025-11-07 10:37 + **/ +public record QuestionDeduplicationAiRes( + @JsonProperty("questionIds") String questionIds +) { +} diff --git a/src/main/java/com/qingqiu/interview/service/DashboardService.java b/src/main/java/com/qingqiu/interview/service/DashboardService.java old mode 100755 new mode 100644 index 7704a3a..67966d1 --- a/src/main/java/com/qingqiu/interview/service/DashboardService.java +++ b/src/main/java/com/qingqiu/interview/service/DashboardService.java @@ -1,59 +1,15 @@ -package com.qingqiu.interview.service; - -import com.qingqiu.interview.mapper.InterviewSessionMapper; -import com.qingqiu.interview.mapper.QuestionMapper; -import com.qingqiu.interview.dto.DashboardStatsResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -@Service -@RequiredArgsConstructor -public class DashboardService { - - private final QuestionMapper questionMapper; - private final InterviewSessionMapper sessionMapper; - - public DashboardStatsResponse getDashboardStats() { - DashboardStatsResponse stats = new DashboardStatsResponse(); - - // 1. 获取核心KPI - stats.setTotalQuestions(questionMapper.selectCount(null)); - stats.setTotalInterviews(sessionMapper.selectCount(null)); - - // 2. 获取题库分类统计 - stats.setQuestionCategoryStats(questionMapper.countByCategory()); - - // 3. 获取最近7天的面试统计,并补全没有数据的日期 - List recentStats = sessionMapper.countRecentInterviews(7); - stats.setRecentInterviewStats(fillMissingDates(recentStats, 7)); - - return stats; - } - - /** - * 填充最近几天内没有面试数据的日期,补0 - */ - private List fillMissingDates(List existingStats, int days) { - Map statsMap = existingStats.stream() - .collect(Collectors.toMap(DashboardStatsResponse.DailyStat::getDate, DashboardStatsResponse.DailyStat::getCount)); - - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); - - return IntStream.range(0, days) - .mapToObj(i -> LocalDate.now().minusDays(i)) - .map(date -> { - String dateString = date.format(formatter); - long count = statsMap.getOrDefault(dateString, 0L); - return new DashboardStatsResponse.DailyStat(dateString, count); - }) - .sorted((d1, d2) -> d1.getDate().compareTo(d2.getDate())) // 按日期升序排序 - .collect(Collectors.toList()); - } -} +package com.qingqiu.interview.service; + + +import com.qingqiu.interview.dto.DashboardStatsResponse; + +/** + * @program: ai-interview + * @description: 工作台接口 + * @author: huangpeng + * @create: 2025-11-07 14:54 + **/ +public interface DashboardService { + + DashboardStatsResponse getDashboardStats(); +} diff --git a/src/main/java/com/qingqiu/interview/service/InterviewService.java b/src/main/java/com/qingqiu/interview/service/InterviewService.java index 10ed8d2..db260bb 100644 --- a/src/main/java/com/qingqiu/interview/service/InterviewService.java +++ b/src/main/java/com/qingqiu/interview/service/InterviewService.java @@ -54,8 +54,17 @@ public interface InterviewService extends IService { /** * 获取面试报告 + * * @param sessionId * @return */ InterviewReportResponse getInterviewReport(String sessionId); + + /** + * 读取pdf文件数据 + * + * @param file 文件 + * @return 文件内容 + */ + String readPdfFile(MultipartFile file) throws IOException; } diff --git a/src/main/java/com/qingqiu/interview/service/QuestionClassificationService.java b/src/main/java/com/qingqiu/interview/service/QuestionClassificationService.java old mode 100755 new mode 100644 index a7f213b..ff9800f --- a/src/main/java/com/qingqiu/interview/service/QuestionClassificationService.java +++ b/src/main/java/com/qingqiu/interview/service/QuestionClassificationService.java @@ -1,147 +1,18 @@ -package com.qingqiu.interview.service; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.qingqiu.interview.common.constants.ChatConstant; -import com.qingqiu.interview.common.utils.UUIDUtils; -import com.qingqiu.interview.entity.Question; -import jakarta.annotation.Resource; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.ai.chat.client.ChatClient; -import org.springframework.ai.chat.memory.ChatMemory; -import org.springframework.stereotype.Service; - -import java.util.ArrayList; -import java.util.List; - -@Slf4j -@Service -@RequiredArgsConstructor -public class QuestionClassificationService { - - - private final ObjectMapper objectMapper = new ObjectMapper(); - - private final ChatClient chatClient; - - /** - * 使用AI对题目进行分类 - */ - public List classifyQuestions(String rawContent) { - log.info("开始使用AI分类题目,内容长度: {}", rawContent.length()); - - String prompt = buildClassificationPrompt(rawContent); - String aiResponse = chatClient.prompt() - .user(prompt) - .call() - .content(); - - log.info("AI分类响应: {}", aiResponse); - - assert aiResponse != null; - return parseAiResponse(aiResponse); - } - - private String buildClassificationPrompt(String content) { - return """ - 请分析以下面试题内容,将其分类并提取信息。请严格按照以下JSON格式返回结果: - - { - "questions": [ - { - "content": "题目内容", - "category": "分类(如:Java基础、Spring框架、数据库、算法、系统设计等)", - "difficulty": "难度(Easy、Medium、Hard)", - "tags": "相关标签,用逗号分隔" - } - ] - } - - 分类规则: - 1. category应该是具体的技术领域,如:Java基础、Spring框架、MySQL、Redis、算法与数据结构、系统设计、网络协议等 - 2. difficulty根据题目复杂度判断:Easy(基础概念)、Medium(实际应用)、Hard(深入原理或复杂场景) - 3. tags包含更细粒度的标签,如:多线程、JVM、事务、索引等 - 4. 如果内容包含多个独立的题目,请分别提取 - 5. 只返回JSON,不要其他解释文字 - - 待分析内容: - """ + content; - } - - private List parseAiResponse(String aiResponse) { - List questions = new ArrayList<>(); - - try { - // 清理响应,移除可能的markdown标记 - String cleanResponse = aiResponse.trim(); - if (cleanResponse.startsWith("```json")) { - cleanResponse = cleanResponse.substring(7); - } - if (cleanResponse.endsWith("```")) { - cleanResponse = cleanResponse.substring(0, cleanResponse.length() - 3); - } - cleanResponse = cleanResponse.trim(); - - JsonNode rootNode = objectMapper.readTree(cleanResponse); - JsonNode questionsNode = rootNode.get("questions"); - - if (questionsNode != null && questionsNode.isArray()) { - for (JsonNode questionNode : questionsNode) { - Question question = new Question() - .setContent(getTextValue(questionNode, "content")) - .setCategoryName(getTextValue(questionNode, "category")) - .setDifficulty(getTextValue(questionNode, "difficulty")) - .setTags(getTextValue(questionNode, "tags")); - - if (isValidQuestion(question)) { - questions.add(question); - } - } - } - - log.info("成功解析出 {} 个题目", questions.size()); - - } catch (JsonProcessingException e) { - log.error("解析AI响应失败: {}", e.getMessage()); - log.error("原始响应: {}", aiResponse); - - // 降级处理:如果AI返回格式不正确,尝试简单分割 - questions.addAll(fallbackParsing(aiResponse)); - } - - return questions; - } - - private String getTextValue(JsonNode node, String fieldName) { - JsonNode fieldNode = node.get(fieldName); - return fieldNode != null ? fieldNode.asText("") : ""; - } - - private boolean isValidQuestion(Question question) { - return question.getContent() != null && !question.getContent().trim().isEmpty() - && question.getCategoryName() != null && !question.getCategoryName().trim().isEmpty(); - } - - private List fallbackParsing(String content) { - log.warn("使用降级解析策略"); - List questions = new ArrayList<>(); - - // 简单的降级策略:按行分割,每行作为一个题目 - String[] lines = content.split("\n"); - for (String line : lines) { - line = line.trim(); - if (!line.isEmpty() && line.length() > 10) { // 过滤太短的内容 - Question question = new Question() - .setContent(line) - .setCategoryName("未分类") - .setDifficulty("Medium") - .setTags("待分类"); - questions.add(question); - } - } - - return questions; - } -} +package com.qingqiu.interview.service; + + +import com.qingqiu.interview.entity.Question; +import com.qingqiu.interview.entity.ai.QuestionClassificationAiRes; + +import java.util.List; + +/** + * @program: ai-interview + * @description: 题型分类 + * @author: huangpeng + * @create: 2025-11-06 19:59 + **/ +public interface QuestionClassificationService { + + List classifyQuestions(String rawContent); +} diff --git a/src/main/java/com/qingqiu/interview/service/impl/DashboardServiceImpl.java b/src/main/java/com/qingqiu/interview/service/impl/DashboardServiceImpl.java new file mode 100644 index 0000000..baacdb2 --- /dev/null +++ b/src/main/java/com/qingqiu/interview/service/impl/DashboardServiceImpl.java @@ -0,0 +1,61 @@ +package com.qingqiu.interview.service.impl; + +import com.qingqiu.interview.mapper.InterviewSessionMapper; +import com.qingqiu.interview.mapper.QuestionMapper; +import com.qingqiu.interview.dto.DashboardStatsResponse; +import com.qingqiu.interview.service.DashboardService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +@Service +@RequiredArgsConstructor +public class DashboardServiceImpl implements DashboardService { + + private final QuestionMapper questionMapper; + private final InterviewSessionMapper sessionMapper; + + @Override + public DashboardStatsResponse getDashboardStats() { + DashboardStatsResponse stats = new DashboardStatsResponse(); + + // 1. 获取核心KPI + stats.setTotalQuestions(questionMapper.selectCount(null)); + stats.setTotalInterviews(sessionMapper.selectCount(null)); + + // 2. 获取题库分类统计 + stats.setQuestionCategoryStats(questionMapper.countByCategory()); + + // 3. 获取最近7天的面试统计,并补全没有数据的日期 + List recentStats = sessionMapper.countRecentInterviews(7); + stats.setRecentInterviewStats(fillMissingDates(recentStats, 7)); + + return stats; + } + + /** + * 填充最近几天内没有面试数据的日期,补0 + */ + private List fillMissingDates(List existingStats, int days) { + Map statsMap = existingStats.stream() + .collect(Collectors.toMap(DashboardStatsResponse.DailyStat::getDate, DashboardStatsResponse.DailyStat::getCount)); + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + return IntStream.range(0, days) + .mapToObj(i -> LocalDate.now().minusDays(i)) + .map(date -> { + String dateString = date.format(formatter); + long count = statsMap.getOrDefault(dateString, 0L); + return new DashboardStatsResponse.DailyStat(dateString, count); + }) + .sorted((d1, d2) -> d1.getDate().compareTo(d2.getDate())) // 按日期升序排序 + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/qingqiu/interview/service/impl/InterviewAiServiceImpl.java b/src/main/java/com/qingqiu/interview/service/impl/InterviewAiServiceImpl.java index 268b2af..3abc2d6 100644 --- a/src/main/java/com/qingqiu/interview/service/impl/InterviewAiServiceImpl.java +++ b/src/main/java/com/qingqiu/interview/service/impl/InterviewAiServiceImpl.java @@ -60,6 +60,7 @@ public class InterviewAiServiceImpl implements InterviewAiService { String jobRequirements, String resumeContent, int count) { + // TODO: 考虑删除这些注释掉的旧代码实现 // String skillsStr = String.join(", ", skills); // String prompt = String.format( // "你是一位专业的软件开发岗位技术面试官。" + @@ -83,6 +84,7 @@ public class InterviewAiServiceImpl implements InterviewAiService { .advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, sessionId)) .call() .entity(QuestionAiRes.Wrapper.class); + // TODO: 考虑删除这些注释掉的旧代码实现 // String content = openAiChatClient // .prompt(aiInterviewerPrompt) // .advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, sessionId)) @@ -120,6 +122,7 @@ public class InterviewAiServiceImpl implements InterviewAiService { .entity(QuestionAiRes.Wrapper.class); assert entity != null; return entity.questions(); + // TODO: 考虑删除这些注释掉的旧代码实现 // String skillsStr = String.join(", ", skills); // // 2. 构建发送给AI的提示 // String prompt = String.format(""" @@ -152,6 +155,7 @@ public class InterviewAiServiceImpl implements InterviewAiService { @Override public EvaluateAiRes evaluateAnswer(String sessionId, String question, String userAnswer, List context) { + // TODO: 考虑删除这些注释掉的旧代码实现 // 构建上下文历史 // String history = context.stream() // .map(p -> String.format("Q: %s\nA: %s", p.getQuestionContent(), p.getUserAnswer())) @@ -190,6 +194,7 @@ public class InterviewAiServiceImpl implements InterviewAiService { @Override public InterviewReportAiRes generateFinalReport(InterviewSession session, List progressList) { + // TODO: 考虑删除这些注释掉的旧代码实现 // String transcript = progressList.stream() // .map(p -> String.format("问题: %s\n回答: %s\nAI评分: %.1f\nAI反馈: %s\n", // p.getQuestionContent(), p.getUserAnswer(), p.getScore(), p.getFeedback())) diff --git a/src/main/java/com/qingqiu/interview/service/impl/InterviewChatServiceImpl.java b/src/main/java/com/qingqiu/interview/service/impl/InterviewChatServiceImpl.java index 7363cd1..63e9fef 100644 --- a/src/main/java/com/qingqiu/interview/service/impl/InterviewChatServiceImpl.java +++ b/src/main/java/com/qingqiu/interview/service/impl/InterviewChatServiceImpl.java @@ -28,11 +28,13 @@ import java.io.IOException; public class InterviewChatServiceImpl implements InterviewChatService { private final DocumentParserManager documentParserManager; + @Override public void startInterview(MultipartFile resume, InterviewStartRequest request) throws IOException { log.info("开始新面试会话,当前模式: {}, 候选人: {}, 默认AI模型: {}", request.getModel(), request.getCandidateName(), AIStrategyConstant.QWEN); // 1. 解析简历 String resumeContent = parseResume(resume); + // TODO: 检查这个空if语句是否需要实现逻辑或删除 // 判断是否使用本地题库 if (request.getModel().equals("local")) { diff --git a/src/main/java/com/qingqiu/interview/service/impl/InterviewServiceImpl.java b/src/main/java/com/qingqiu/interview/service/impl/InterviewServiceImpl.java index 3f8ab93..a92014a 100644 --- a/src/main/java/com/qingqiu/interview/service/impl/InterviewServiceImpl.java +++ b/src/main/java/com/qingqiu/interview/service/impl/InterviewServiceImpl.java @@ -2,7 +2,6 @@ package com.qingqiu.interview.service.impl; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.io.file.FileNameUtil; -import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; @@ -70,7 +69,7 @@ public class InterviewServiceImpl extends ServiceImpl classifyQuestions(String rawContent) { + log.info("开始使用AI分类题目,内容长度: {}", rawContent.length()); + + + QuestionClassificationAiRes.Wrapper entity = chatClient.prompt() + .user(buildClassificationPrompt(rawContent)) + .call() + .entity(QuestionClassificationAiRes.Wrapper.class); + assert entity != null; + return entity.questions(); + } + + private String buildClassificationPrompt(String content) { + + PromptTemplate prompt = PromptTemplate.builder() + .renderer(StTemplateRenderer.builder().startDelimiterToken('<').endDelimiterToken('>').build()) + .template( + """ + 请分析以下面试题内容,将其分类并提取信息。请严格按照以下JSON格式返回结果: + + { + "questions": [ + { + "content": "题目内容", + "category": "分类(如:Java基础、Spring框架、数据库、算法、系统设计等)", + "difficulty": "难度(Easy、Medium、Hard)", + "tags": "相关标签,用逗号分隔" + } + ] + } + + 分类规则: + 1. category应该是具体的技术领域,如:Java基础、Spring框架、MySQL、Redis、算法与数据结构、系统设计、网络协议等 + 2. difficulty根据题目复杂度判断:Easy(基础概念)、Medium(实际应用)、Hard(深入原理或复杂场景) + 3. tags包含更细粒度的标签,如:多线程、JVM、事务、索引等 + 4. 如果内容包含多个独立的题目,请分别提取 + 5. 只返回JSON,不要其他解释文字 + + 待分析内容: + + """ + ) + .build(); + return prompt.render(Map.of("content", content)); + } +} diff --git a/src/main/java/com/qingqiu/interview/service/impl/QuestionServiceImpl.java b/src/main/java/com/qingqiu/interview/service/impl/QuestionServiceImpl.java index 84e1584..0c78052 100644 --- a/src/main/java/com/qingqiu/interview/service/impl/QuestionServiceImpl.java +++ b/src/main/java/com/qingqiu/interview/service/impl/QuestionServiceImpl.java @@ -1,28 +1,36 @@ package com.qingqiu.interview.service.impl; +import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.collection.CollectionUtil; +import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.qingqiu.interview.common.enums.LLMProvider; import com.qingqiu.interview.common.constants.CommonConstant; -import com.qingqiu.interview.common.utils.TreeUtil; +import com.qingqiu.interview.common.enums.LLMProvider; +import com.qingqiu.interview.common.utils.TreeUtils; import com.qingqiu.interview.dto.QuestionOptionsDTO; import com.qingqiu.interview.dto.QuestionPageParams; import com.qingqiu.interview.entity.Question; import com.qingqiu.interview.entity.QuestionCategory; +import com.qingqiu.interview.entity.ai.QuestionClassificationAiRes; +import com.qingqiu.interview.entity.ai.QuestionDeduplicationAiRes; import com.qingqiu.interview.mapper.QuestionMapper; import com.qingqiu.interview.service.IQuestionCategoryService; -import com.qingqiu.interview.service.QuestionClassificationService; import com.qingqiu.interview.service.QuestionService; import com.qingqiu.interview.service.parser.DocumentParser; import com.qingqiu.interview.vo.QuestionAndCategoryTreeListVO; +import jakarta.annotation.Resource; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.prompt.PromptTemplate; +import org.springframework.ai.template.TemplateRenderer; +import org.springframework.ai.template.st.StTemplateRenderer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; @@ -38,10 +46,12 @@ import java.util.stream.Collectors; @RequiredArgsConstructor(onConstructor_ = {@Autowired, @Lazy}) public class QuestionServiceImpl extends ServiceImpl implements QuestionService { private final QuestionMapper questionMapper; - private final QuestionClassificationService classificationService; + private final QuestionClassificationServiceImpl classificationService; private final List documentParserList; // This will be injected by Spring private final IQuestionCategoryService questionCategoryService; + private final ChatClient chatClient; + /** * 分页查询题库 */ @@ -96,19 +106,19 @@ public class QuestionServiceImpl extends ServiceImpl i .orElseThrow(() -> new IllegalArgumentException("不支持的文件类型: " + fileExtension)); String content = parser.parse(file.getInputStream()); - List questionsFromAi = classificationService.classifyQuestions(content); + List items = classificationService.classifyQuestions(content); int newQuestionsCount = 0; - for (Question question : questionsFromAi) { + for (QuestionClassificationAiRes.Item item : items) { try { - validateQuestion(question.getContent(), null); - questionMapper.insert(question); + validateQuestion(item.content(), null); + questionMapper.insert(BeanUtil.toBean(item, Question.class)); newQuestionsCount++; } catch (IllegalArgumentException e) { - log.warn("跳过重复题目: {}", question.getContent()); + log.warn("跳过重复题目: {}", item.content()); } } - log.info("成功导入 {} 个新题目,跳过 {} 个重复题目。", newQuestionsCount, questionsFromAi.size() - newQuestionsCount); + log.info("成功导入 {} 个新题目,跳过 {} 个重复题目。", newQuestionsCount, items.size() - newQuestionsCount); } /** @@ -117,30 +127,32 @@ public class QuestionServiceImpl extends ServiceImpl i @Override @Transactional(rollbackFor = Exception.class) public void useAiCheckQuestionData() { -// // 查询数据库 -// List questions = questionMapper.selectList( -// new LambdaQueryWrapper() -// .orderByDesc(Question::getCreatedTime) -// ); -// // 组装prompt -// if (CollectionUtil.isEmpty(questions)) { -// return; -// } -// String prompt = getPrompt(questions); -// log.info("发送内容: {}", prompt); -// // 验证token上下文长度 -// Integer promptTokens = llmService.getPromptTokens(prompt); -// log.info("当前prompt长度: {}", promptTokens); -// String chat = llmService.chat(prompt, LLMProvider.DEEPSEEK); -// // 调用AI -// log.info("AI返回内容: {}", chat); + // 查询数据库 + List questions = questionMapper.selectList( + new LambdaQueryWrapper() + .orderByDesc(Question::getCreatedTime) + ); + // 组装prompt + if (CollectionUtil.isEmpty(questions)) { + return; + } + String prompt = getPrompt(questions); + + + QuestionDeduplicationAiRes entity = chatClient.prompt().user(prompt).call().entity(QuestionDeduplicationAiRes.class); + assert entity != null; + // 调用AI + log.info("AI返回内容: {}", JSONObject.toJSONString(entity)); + String s = entity.questionIds(); + List list = Arrays.asList(s.split(",")); + // TODO: 检查这些注释代码是否可以删除 // JSONObject parse = JSONObject.parse(chat); // JSONArray questionsIds = parse.getJSONArray("questions"); // List list = questionsIds.toList(Long.class); -// questionMapper.delete( -// new LambdaQueryWrapper() -// .notIn(Question::getId, list) -// ); + questionMapper.delete( + new LambdaQueryWrapper() + .notIn(Question::getId, list) + ); } @@ -155,25 +167,71 @@ public class QuestionServiceImpl extends ServiceImpl i } JSONObject jsonObject = new JSONObject(); jsonObject.put("data", jsonArray); - return String.format(""" - 你是一个数据清洗与去重专家。你的任务是从以下文本列表中,识别出语义相同或高度相似的条目,并为每一组相似条目筛选出一个最规范、最简洁的代表性版本。 - - 【去重规则】 - 1. **语义核心优先**:忽略无关的修饰词、标点符号、后缀(如“:2”)、大小写和空格。 - 2. **合并同类项**:将表达同一主题或问题的文本归为一组。 - 3. **选择标准**:从每一组中,选出那个最完整、最简洁、且没有多余符号(如序号、特殊后缀)的版本作为代表。如果两个版本质量相当,优先选择更短的那个。 - 4. **保留原意**:确保选出的代表版本没有改变原文本的核心含义。 - 请按照下述格式返回,已被剔除掉的数据无需返回 - { - "questions": [1, 2, 3, .....] - } - 分类规则: - 1. 只返回JSON,不要其他解释文字 - 2. 请严格按照API接口形式返回,不要返回任何额外的文字内容,包括'```json```'!!!! - 3. 请严格按照网络接口的形式返回JSON数据!!! - 【请处理以下数据列表】: - %s - """, jsonObject.toJSONString()); + + return PromptTemplate.builder() + .renderer( + StTemplateRenderer.builder() + .startDelimiterToken('<') + .endDelimiterToken('>') + .build() + ) + .template(""" + 请对以下题库JSON数据进行智能去重处理。 + + ## 任务说明 + 识别并移除语义相似或表达意思基本相同的重复题目,只保留每个独特题目的一个版本。 + + ## 语义相似度判断标准 + 1. 核心意思相同:即使表述不同,但考察的知识点和答案逻辑一致 + 2. 同义替换:使用同义词、近义词但意思不变的题目 + 3. 句式变换:主动被动语态转换、疑问词替换等句式变化 + 4. 冗余表述:增加了无关修饰词但核心内容相同的题目 + + ## 处理规则 + - 对语义相似的题目组,只保留其中一条数据 + - 保留原则:选择表述最清晰、最完整的那条 + - 如果难以判断,保留ID较小或创建时间较早的那条 + + ## 输出要求 + 1. 只返回JSON,不要其他解释文字 + 2. 请严格按照API接口形式返回,不要返回任何额外的文字内容,包括'```json```'!!!! + 3. 请严格按照网络接口的形式返回JSON数据!!! + { + "questionIds": "1, 2, 3" # 请返回保留数据的id + } + + ## 特殊说明 + - 注意区分真正重复和只是题型相似的题目 + - 对于选择题,要同时考虑题干和选项的语义相似度 + - 保留题目版本的完整性 + + 请处理以下JSON数据: + + """) + .build() + .render(Map.of("data", jsonObject.toJSONString())) + ; + + // TODO: 检查这些注释代码是否可以删除 - 这是旧的prompt模板实现 +// return String.format(""" +// 你是一个数据清洗与去重专家。你的任务是从以下文本列表中,识别出语义相同或高度相似的条目,并为每一组相似条目筛选出一个最规范、最简洁的代表性版本。 +// +// 【去重规则】 +// 1. **语义核心优先**:忽略无关的修饰词、标点符号、后缀(如“:2”)、大小写和空格。 +// 2. **合并同类项**:将表达同一主题或问题的文本归为一组。 +// 3. **选择标准**:从每一组中,选出那个最完整、最简洁、且没有多余符号(如序号、特殊后缀)的版本作为代表。如果两个版本质量相当,优先选择更短的那个。 +// 4. **保留原意**:确保选出的代表版本没有改变原文本的核心含义。 +// 请返回数据的id,已被剔除掉的数据无需返回,格式如下: +// { +// "questions": [1, 2, 3, .....] +// } +// 分类规则: +// 1. 只返回JSON,不要其他解释文字 +// 2. 请严格按照API接口形式返回,不要返回任何额外的文字内容,包括'```json```'!!!! +// 3. 请严格按照网络接口的形式返回JSON数据!!! +// 【请处理以下数据列表】: +// %s +// """, jsonObject.toJSONString()); } /** @@ -209,7 +267,7 @@ public class QuestionServiceImpl extends ServiceImpl i if (CollectionUtil.isNotEmpty(dto.getCategoryIds())) { questionCategories = questionCategoryService.batchFindByAncestorIdsUnion(dto.getCategoryIds()); if (CollectionUtil.isNotEmpty(questionCategories)) { - treeList = TreeUtil.buildTree( + treeList = TreeUtils.buildTree( questionCategories, QuestionCategory::getId, QuestionCategory::getParentId, diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 6e58163..e4d810a 100755 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -37,6 +37,7 @@ spring: min-idle: 0 max-wait: -1ms +# TODO: 考虑删除已注释的配置 # ai: # openai: # api-key: sk-faaa2a1b485442ccbf115ff1271a3480 @@ -44,6 +45,14 @@ spring: # chat: # options: # model: deepseek-chat +logging: + level: + org: + springframework: + ai: + chat: + client: + advisor: debug mybatis-plus: configuration: map-underscore-to-camel-case: true @@ -52,4 +61,4 @@ mybatis-plus: db-config: logic-delete-field: deleted # 全局逻辑删除字段名 logic-delete-value: 1 # 逻辑已删除值。可选,默认值为 1 - logic-not-delete-value: 0 # 逻辑未删除值。可选,默认值为 0 \ No newline at end of file + logic-not-delete-value: 0 # 逻辑未删除值。可选,默认值为 0