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