使用spring-ai-alibaba重构项目

This commit is contained in:
2025-11-24 13:37:37 +08:00
parent 5fb4ed754c
commit 80dcb23bbc
21 changed files with 514 additions and 450 deletions

View File

@@ -13,6 +13,7 @@
<version>0.0.1-SNAPSHOT</version> <version>0.0.1-SNAPSHOT</version>
<name>AI-Interview</name> <name>AI-Interview</name>
<description>AI-Interview</description> <description>AI-Interview</description>
<!-- TODO: 考虑删除空的元数据元素 -->
<url/> <url/>
<licenses> <licenses>
<license/> <license/>
@@ -56,6 +57,7 @@
<artifactId>spring-boot-starter-webflux</artifactId> <!-- 用于 WebClient --> <artifactId>spring-boot-starter-webflux</artifactId> <!-- 用于 WebClient -->
</dependency> </dependency>
<!-- TODO: 考虑删除已注释的依赖 -->
<!-- <dependency>--> <!-- <dependency>-->
<!-- <groupId>org.springframework.ai</groupId>--> <!-- <groupId>org.springframework.ai</groupId>-->
<!-- <artifactId>spring-ai-openai-spring-boot-starter</artifactId>--> <!-- <artifactId>spring-ai-openai-spring-boot-starter</artifactId>-->
@@ -72,6 +74,7 @@
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId> <artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
</dependency> </dependency>
<!-- TODO: 检查内存相关依赖是否都需要 -->
<dependency> <dependency>
<groupId>com.alibaba.cloud.ai</groupId> <groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-memory</artifactId> <artifactId>spring-ai-alibaba-starter-memory</artifactId>
@@ -100,7 +103,7 @@
<artifactId>spring-boot-starter-data-redis</artifactId> <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency> </dependency>
<!-- TODO: 考虑删除已注释的依赖 -->
<!-- <dependency>--> <!-- <dependency>-->
<!-- <groupId>com.alibaba</groupId>--> <!-- <groupId>com.alibaba</groupId>-->
<!-- <artifactId>dashscope-sdk-java</artifactId>--> <!-- <artifactId>dashscope-sdk-java</artifactId>-->
@@ -198,11 +201,13 @@
<build> <build>
<finalName>ai-interview</finalName> <finalName>ai-interview</finalName>
<plugins> <plugins>
<!-- TODO: 检查Spring Boot插件版本是否与父POM一致 -->
<plugin> <plugin>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId> <artifactId>spring-boot-maven-plugin</artifactId>
<version>3.5.0</version> <version>3.5.0</version>
</plugin> </plugin>
<!-- TODO: 考虑在正式环境中启用测试 -->
<!-- maven 打包时跳过测试 --> <!-- maven 打包时跳过测试 -->
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>

View File

@@ -1,15 +0,0 @@
package com.qingqiu.interview.annotation;
import java.lang.annotation.*;
/**
* <h1></h1>
*
* @author qingqiu
* @date 2025/9/18 12:58
*/
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AiChatLog {
}

View File

@@ -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;
/**
* <h1>
* ai聊天的切面
* </h1>
*
* @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;
}
}

View File

@@ -6,12 +6,12 @@ import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Component @Component
public class SpringApplicationContextUtil implements ApplicationContextAware { public class SpringApplicationContextUtils implements ApplicationContextAware {
private static ApplicationContext applicationContext; private static ApplicationContext applicationContext;
@Override @Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringApplicationContextUtil.applicationContext = applicationContext; SpringApplicationContextUtils.applicationContext = applicationContext;
} }
public static <T> T getBean(Class<T> beanClass) { public static <T> T getBean(Class<T> beanClass) {

View File

@@ -7,7 +7,7 @@ import java.util.Map;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class TreeUtil { public class TreeUtils {
/** /**
* 通用树形结构构建方法 * 通用树形结构构建方法

View File

@@ -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))
)
);
}
}

View File

@@ -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.DashScopeChatModel;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
import com.qingqiu.interview.common.constants.ChatConstant; import com.qingqiu.interview.common.constants.ChatConstant;
import io.netty.channel.ChannelOption;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor; import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.memory.MessageWindowChatMemory; import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.ai.chat.model.ChatModel; 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.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary; 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相关配置类 * DashScope相关配置类
@@ -45,21 +36,11 @@ public class DashScopeConfig {
@Value("${spring.ai.dashscope.chat.options.model}") @Value("${spring.ai.dashscope.chat.options.model}")
private String dashScopeChatModelName; 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) @Resource(name = ChatConstant.MYSQL_CHAT_MEMORY_BEAN_NAME)
private MessageWindowChatMemory mysqlChatMemory; private MessageWindowChatMemory mysqlChatMemory;
@Resource(name = ChatConstant.REDIS_CHAT_MEMORY_BEAN_NAME)
private MessageWindowChatMemory redisChatMemory;
/** /**
* 创建DashScopeApi Bean实例 * 创建DashScopeApi Bean实例
* 配置了HTTP客户端连接参数包括连接超时和响应超时时间 * 配置了HTTP客户端连接参数包括连接超时和响应超时时间
@@ -100,42 +81,6 @@ public class DashScopeConfig {
.build(); .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模型 * 创建默认的聊天客户端Bean实例使用DashScope模型
@@ -171,47 +116,5 @@ public class DashScopeConfig {
.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();
}
/**
* 创建全局默认的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))
)
);
}
} }

View File

@@ -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();
}
}

View File

@@ -13,6 +13,7 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.List; import java.util.List;
/** /**
@@ -122,4 +123,11 @@ public class InterviewController {
public R<InterviewReportResponse> getInterviewReportDetail(@PathVariable String sessionId) { public R<InterviewReportResponse> getInterviewReportDetail(@PathVariable String sessionId) {
return R.success(interviewService.getInterviewReport(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);
}
} }

View File

@@ -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<Item> questions) {}
}

View File

@@ -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
) {
}

View File

@@ -1,59 +1,15 @@
package com.qingqiu.interview.service; package com.qingqiu.interview.service;
import com.qingqiu.interview.mapper.InterviewSessionMapper;
import com.qingqiu.interview.mapper.QuestionMapper;
import com.qingqiu.interview.dto.DashboardStatsResponse; import com.qingqiu.interview.dto.DashboardStatsResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.time.LocalDate; /**
import java.time.format.DateTimeFormatter; * @program: ai-interview
import java.util.List; * @description: 工作台接口
import java.util.Map; * @author: huangpeng
import java.util.stream.Collectors; * @create: 2025-11-07 14:54
import java.util.stream.IntStream; **/
public interface DashboardService {
@Service DashboardStatsResponse getDashboardStats();
@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<DashboardStatsResponse.DailyStat> recentStats = sessionMapper.countRecentInterviews(7);
stats.setRecentInterviewStats(fillMissingDates(recentStats, 7));
return stats;
}
/**
* 填充最近几天内没有面试数据的日期补0
*/
private List<DashboardStatsResponse.DailyStat> fillMissingDates(List<DashboardStatsResponse.DailyStat> existingStats, int days) {
Map<String, Long> 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());
}
} }

View File

@@ -54,8 +54,17 @@ public interface InterviewService extends IService<InterviewSession> {
/** /**
* 获取面试报告 * 获取面试报告
*
* @param sessionId * @param sessionId
* @return * @return
*/ */
InterviewReportResponse getInterviewReport(String sessionId); InterviewReportResponse getInterviewReport(String sessionId);
/**
* 读取pdf文件数据
*
* @param file 文件
* @return 文件内容
*/
String readPdfFile(MultipartFile file) throws IOException;
} }

View File

@@ -1,147 +1,18 @@
package com.qingqiu.interview.service; 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 com.qingqiu.interview.entity.Question;
import com.qingqiu.interview.entity.ai.QuestionClassificationAiRes;
import java.util.List; import java.util.List;
@Slf4j /**
@Service * @program: ai-interview
@RequiredArgsConstructor * @description: 题型分类
public class QuestionClassificationService { * @author: huangpeng
* @create: 2025-11-06 19:59
**/
public interface QuestionClassificationService {
List<QuestionClassificationAiRes.Item> classifyQuestions(String rawContent);
private final ObjectMapper objectMapper = new ObjectMapper();
private final ChatClient chatClient;
/**
* 使用AI对题目进行分类
*/
public List<Question> 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<Question> parseAiResponse(String aiResponse) {
List<Question> 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<Question> fallbackParsing(String content) {
log.warn("使用降级解析策略");
List<Question> 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;
}
} }

View File

@@ -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<DashboardStatsResponse.DailyStat> recentStats = sessionMapper.countRecentInterviews(7);
stats.setRecentInterviewStats(fillMissingDates(recentStats, 7));
return stats;
}
/**
* 填充最近几天内没有面试数据的日期补0
*/
private List<DashboardStatsResponse.DailyStat> fillMissingDates(List<DashboardStatsResponse.DailyStat> existingStats, int days) {
Map<String, Long> 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());
}
}

View File

@@ -60,6 +60,7 @@ public class InterviewAiServiceImpl implements InterviewAiService {
String jobRequirements, String jobRequirements,
String resumeContent, String resumeContent,
int count) { int count) {
// TODO: 考虑删除这些注释掉的旧代码实现
// String skillsStr = String.join(", ", skills); // String skillsStr = String.join(", ", skills);
// String prompt = String.format( // String prompt = String.format(
// "你是一位专业的软件开发岗位技术面试官。" + // "你是一位专业的软件开发岗位技术面试官。" +
@@ -83,6 +84,7 @@ public class InterviewAiServiceImpl implements InterviewAiService {
.advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, sessionId)) .advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, sessionId))
.call() .call()
.entity(QuestionAiRes.Wrapper.class); .entity(QuestionAiRes.Wrapper.class);
// TODO: 考虑删除这些注释掉的旧代码实现
// String content = openAiChatClient // String content = openAiChatClient
// .prompt(aiInterviewerPrompt) // .prompt(aiInterviewerPrompt)
// .advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, sessionId)) // .advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, sessionId))
@@ -120,6 +122,7 @@ public class InterviewAiServiceImpl implements InterviewAiService {
.entity(QuestionAiRes.Wrapper.class); .entity(QuestionAiRes.Wrapper.class);
assert entity != null; assert entity != null;
return entity.questions(); return entity.questions();
// TODO: 考虑删除这些注释掉的旧代码实现
// String skillsStr = String.join(", ", skills); // String skillsStr = String.join(", ", skills);
// // 2. 构建发送给AI的提示 // // 2. 构建发送给AI的提示
// String prompt = String.format(""" // String prompt = String.format("""
@@ -152,6 +155,7 @@ public class InterviewAiServiceImpl implements InterviewAiService {
@Override @Override
public EvaluateAiRes evaluateAnswer(String sessionId, String question, String userAnswer, List<InterviewQuestionProgress> context) { public EvaluateAiRes evaluateAnswer(String sessionId, String question, String userAnswer, List<InterviewQuestionProgress> context) {
// TODO: 考虑删除这些注释掉的旧代码实现
// 构建上下文历史 // 构建上下文历史
// String history = context.stream() // String history = context.stream()
// .map(p -> String.format("Q: %s\nA: %s", p.getQuestionContent(), p.getUserAnswer())) // .map(p -> String.format("Q: %s\nA: %s", p.getQuestionContent(), p.getUserAnswer()))
@@ -190,6 +194,7 @@ public class InterviewAiServiceImpl implements InterviewAiService {
@Override @Override
public InterviewReportAiRes generateFinalReport(InterviewSession session, List<InterviewQuestionProgress> progressList) { public InterviewReportAiRes generateFinalReport(InterviewSession session, List<InterviewQuestionProgress> progressList) {
// TODO: 考虑删除这些注释掉的旧代码实现
// String transcript = progressList.stream() // String transcript = progressList.stream()
// .map(p -> String.format("问题: %s\n回答: %s\nAI评分: %.1f\nAI反馈: %s\n", // .map(p -> String.format("问题: %s\n回答: %s\nAI评分: %.1f\nAI反馈: %s\n",
// p.getQuestionContent(), p.getUserAnswer(), p.getScore(), p.getFeedback())) // p.getQuestionContent(), p.getUserAnswer(), p.getScore(), p.getFeedback()))

View File

@@ -28,11 +28,13 @@ import java.io.IOException;
public class InterviewChatServiceImpl implements InterviewChatService { public class InterviewChatServiceImpl implements InterviewChatService {
private final DocumentParserManager documentParserManager; private final DocumentParserManager documentParserManager;
@Override @Override
public void startInterview(MultipartFile resume, InterviewStartRequest request) throws IOException { public void startInterview(MultipartFile resume, InterviewStartRequest request) throws IOException {
log.info("开始新面试会话,当前模式: {}, 候选人: {}, 默认AI模型: {}", request.getModel(), request.getCandidateName(), AIStrategyConstant.QWEN); log.info("开始新面试会话,当前模式: {}, 候选人: {}, 默认AI模型: {}", request.getModel(), request.getCandidateName(), AIStrategyConstant.QWEN);
// 1. 解析简历 // 1. 解析简历
String resumeContent = parseResume(resume); String resumeContent = parseResume(resume);
// TODO: 检查这个空if语句是否需要实现逻辑或删除
// 判断是否使用本地题库 // 判断是否使用本地题库
if (request.getModel().equals("local")) { if (request.getModel().equals("local")) {

View File

@@ -2,7 +2,6 @@ package com.qingqiu.interview.service.impl;
import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.io.file.FileNameUtil; import cn.hutool.core.io.file.FileNameUtil;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject; import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@@ -70,7 +69,7 @@ public class InterviewServiceImpl extends ServiceImpl<InterviewSessionMapper, In
public InterviewSession startInterview(MultipartFile file, InterviewStartRequest dto) throws IOException { public InterviewSession startInterview(MultipartFile file, InterviewStartRequest dto) throws IOException {
// 1. 创建并保存会话主记录 // 1. 创建并保存会话主记录
String sessionId = UUID.randomUUID().toString().replace("-", ""); String sessionId = UUID.randomUUID().toString().replace("-", "");
String resumeContent = parseResume(file); String resumeContent = readPdfFile(file);
InterviewSession session = new InterviewSession(); InterviewSession session = new InterviewSession();
session.setSessionId(sessionId); session.setSessionId(sessionId);
@@ -400,7 +399,8 @@ public class InterviewServiceImpl extends ServiceImpl<InterviewSessionMapper, In
} }
private String parseResume(MultipartFile resume) throws IOException { @Override
public String readPdfFile(MultipartFile resume) throws IOException {
// 获取文件扩展名 // 获取文件扩展名
String extName = FileNameUtil.extName(resume.getOriginalFilename()); String extName = FileNameUtil.extName(resume.getOriginalFilename());
// 1. 获取简历解析器 // 1. 获取简历解析器

View File

@@ -0,0 +1,77 @@
package com.qingqiu.interview.service.impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qingqiu.interview.entity.Question;
import com.qingqiu.interview.entity.ai.QuestionClassificationAiRes;
import com.qingqiu.interview.service.QuestionClassificationService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.template.st.StTemplateRenderer;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Slf4j
@Service
@RequiredArgsConstructor
public class QuestionClassificationServiceImpl implements QuestionClassificationService {
private final ChatClient chatClient;
/**
* 使用AI对题目进行分类
*/
@Override
public List<QuestionClassificationAiRes.Item> 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不要其他解释文字
待分析内容:
<content>
"""
)
.build();
return prompt.render(Map.of("content", content));
}
}

View File

@@ -1,28 +1,36 @@
package com.qingqiu.interview.service.impl; package com.qingqiu.interview.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.collection.CollectionUtil;
import com.alibaba.cloud.ai.dashscope.api.DashScopeApi;
import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject; import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; 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.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.QuestionOptionsDTO;
import com.qingqiu.interview.dto.QuestionPageParams; import com.qingqiu.interview.dto.QuestionPageParams;
import com.qingqiu.interview.entity.Question; import com.qingqiu.interview.entity.Question;
import com.qingqiu.interview.entity.QuestionCategory; 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.mapper.QuestionMapper;
import com.qingqiu.interview.service.IQuestionCategoryService; import com.qingqiu.interview.service.IQuestionCategoryService;
import com.qingqiu.interview.service.QuestionClassificationService;
import com.qingqiu.interview.service.QuestionService; import com.qingqiu.interview.service.QuestionService;
import com.qingqiu.interview.service.parser.DocumentParser; import com.qingqiu.interview.service.parser.DocumentParser;
import com.qingqiu.interview.vo.QuestionAndCategoryTreeListVO; import com.qingqiu.interview.vo.QuestionAndCategoryTreeListVO;
import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull; 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.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -38,10 +46,12 @@ import java.util.stream.Collectors;
@RequiredArgsConstructor(onConstructor_ = {@Autowired, @Lazy}) @RequiredArgsConstructor(onConstructor_ = {@Autowired, @Lazy})
public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> implements QuestionService { public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> implements QuestionService {
private final QuestionMapper questionMapper; private final QuestionMapper questionMapper;
private final QuestionClassificationService classificationService; private final QuestionClassificationServiceImpl classificationService;
private final List<DocumentParser> documentParserList; // This will be injected by Spring private final List<DocumentParser> documentParserList; // This will be injected by Spring
private final IQuestionCategoryService questionCategoryService; private final IQuestionCategoryService questionCategoryService;
private final ChatClient chatClient;
/** /**
* 分页查询题库 * 分页查询题库
*/ */
@@ -96,19 +106,19 @@ public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> i
.orElseThrow(() -> new IllegalArgumentException("不支持的文件类型: " + fileExtension)); .orElseThrow(() -> new IllegalArgumentException("不支持的文件类型: " + fileExtension));
String content = parser.parse(file.getInputStream()); String content = parser.parse(file.getInputStream());
List<Question> questionsFromAi = classificationService.classifyQuestions(content); List<QuestionClassificationAiRes.Item> items = classificationService.classifyQuestions(content);
int newQuestionsCount = 0; int newQuestionsCount = 0;
for (Question question : questionsFromAi) { for (QuestionClassificationAiRes.Item item : items) {
try { try {
validateQuestion(question.getContent(), null); validateQuestion(item.content(), null);
questionMapper.insert(question); questionMapper.insert(BeanUtil.toBean(item, Question.class));
newQuestionsCount++; newQuestionsCount++;
} catch (IllegalArgumentException e) { } 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<QuestionMapper, Question> i
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void useAiCheckQuestionData() { public void useAiCheckQuestionData() {
// // 查询数据库 // 查询数据库
// List<Question> questions = questionMapper.selectList( List<Question> questions = questionMapper.selectList(
// new LambdaQueryWrapper<Question>() new LambdaQueryWrapper<Question>()
// .orderByDesc(Question::getCreatedTime) .orderByDesc(Question::getCreatedTime)
// ); );
// // 组装prompt // 组装prompt
// if (CollectionUtil.isEmpty(questions)) { if (CollectionUtil.isEmpty(questions)) {
// return; return;
// } }
// String prompt = getPrompt(questions); String prompt = getPrompt(questions);
// log.info("发送内容: {}", prompt);
// // 验证token上下文长度
// Integer promptTokens = llmService.getPromptTokens(prompt); QuestionDeduplicationAiRes entity = chatClient.prompt().user(prompt).call().entity(QuestionDeduplicationAiRes.class);
// log.info("当前prompt长度: {}", promptTokens); assert entity != null;
// String chat = llmService.chat(prompt, LLMProvider.DEEPSEEK); // 调用AI
// // 调用AI log.info("AI返回内容: {}", JSONObject.toJSONString(entity));
// log.info("AI返回内容: {}", chat); String s = entity.questionIds();
List<String> list = Arrays.asList(s.split(","));
// TODO: 检查这些注释代码是否可以删除
// JSONObject parse = JSONObject.parse(chat); // JSONObject parse = JSONObject.parse(chat);
// JSONArray questionsIds = parse.getJSONArray("questions"); // JSONArray questionsIds = parse.getJSONArray("questions");
// List<Long> list = questionsIds.toList(Long.class); // List<Long> list = questionsIds.toList(Long.class);
// questionMapper.delete( questionMapper.delete(
// new LambdaQueryWrapper<Question>() new LambdaQueryWrapper<Question>()
// .notIn(Question::getId, list) .notIn(Question::getId, list)
// ); );
} }
@@ -155,25 +167,71 @@ public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> i
} }
JSONObject jsonObject = new JSONObject(); JSONObject jsonObject = new JSONObject();
jsonObject.put("data", jsonArray); jsonObject.put("data", jsonArray);
return String.format("""
你是一个数据清洗与去重专家。你的任务是从以下文本列表中,识别出语义相同或高度相似的条目,并为每一组相似条目筛选出一个最规范、最简洁的代表性版本。
【去重规则】 return PromptTemplate.builder()
1. **语义核心优先**:忽略无关的修饰词、标点符号、后缀(如“:2”、大小写和空格。 .renderer(
2. **合并同类项**:将表达同一主题或问题的文本归为一组。 StTemplateRenderer.builder()
3. **选择标准**:从每一组中,选出那个最完整、最简洁、且没有多余符号(如序号、特殊后缀)的版本作为代表。如果两个版本质量相当,优先选择更短的那个。 .startDelimiterToken('<')
4. **保留原意**:确保选出的代表版本没有改变原文本的核心含义。 .endDelimiterToken('>')
请按照下述格式返回,已被剔除掉的数据无需返回 .build()
{ )
"questions": [1, 2, 3, .....] .template("""
} 请对以下题库JSON数据进行智能去重处理。
分类规则:
1. 只返回JSON不要其他解释文字 ## 任务说明
2. 请严格按照API接口形式返回不要返回任何额外的文字内容包括'```json```'!!!! 识别并移除语义相似或表达意思基本相同的重复题目,只保留每个独特题目的一个版本。
3. 请严格按照网络接口的形式返回JSON数据
【请处理以下数据列表】: ## 语义相似度判断标准
%s 1. 核心意思相同:即使表述不同,但考察的知识点和答案逻辑一致
""", jsonObject.toJSONString()); 2. 同义替换:使用同义词、近义词但意思不变的题目
3. 句式变换:主动被动语态转换、疑问词替换等句式变化
4. 冗余表述:增加了无关修饰词但核心内容相同的题目
## 处理规则
- 对语义相似的题目组,只保留其中一条数据
- 保留原则:选择表述最清晰、最完整的那条
- 如果难以判断保留ID较小或创建时间较早的那条
## 输出要求
1. 只返回JSON不要其他解释文字
2. 请严格按照API接口形式返回不要返回任何额外的文字内容包括'```json```'!!!!
3. 请严格按照网络接口的形式返回JSON数据
{
"questionIds": "1, 2, 3" # 请返回保留数据的id
}
## 特殊说明
- 注意区分真正重复和只是题型相似的题目
- 对于选择题,要同时考虑题干和选项的语义相似度
- 保留题目版本的完整性
请处理以下JSON数据
<data>
""")
.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<QuestionMapper, Question> i
if (CollectionUtil.isNotEmpty(dto.getCategoryIds())) { if (CollectionUtil.isNotEmpty(dto.getCategoryIds())) {
questionCategories = questionCategoryService.batchFindByAncestorIdsUnion(dto.getCategoryIds()); questionCategories = questionCategoryService.batchFindByAncestorIdsUnion(dto.getCategoryIds());
if (CollectionUtil.isNotEmpty(questionCategories)) { if (CollectionUtil.isNotEmpty(questionCategories)) {
treeList = TreeUtil.buildTree( treeList = TreeUtils.buildTree(
questionCategories, questionCategories,
QuestionCategory::getId, QuestionCategory::getId,
QuestionCategory::getParentId, QuestionCategory::getParentId,

View File

@@ -37,6 +37,7 @@ spring:
min-idle: 0 min-idle: 0
max-wait: -1ms max-wait: -1ms
# TODO: 考虑删除已注释的配置
# ai: # ai:
# openai: # openai:
# api-key: sk-faaa2a1b485442ccbf115ff1271a3480 # api-key: sk-faaa2a1b485442ccbf115ff1271a3480
@@ -44,6 +45,14 @@ spring:
# chat: # chat:
# options: # options:
# model: deepseek-chat # model: deepseek-chat
logging:
level:
org:
springframework:
ai:
chat:
client:
advisor: debug
mybatis-plus: mybatis-plus:
configuration: configuration:
map-underscore-to-camel-case: true map-underscore-to-camel-case: true