使用spring-ai-alibaba重构项目
This commit is contained in:
7
pom.xml
7
pom.xml
@@ -13,6 +13,7 @@
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>AI-Interview</name>
|
||||
<description>AI-Interview</description>
|
||||
<!-- TODO: 考虑删除空的元数据元素 -->
|
||||
<url/>
|
||||
<licenses>
|
||||
<license/>
|
||||
@@ -56,6 +57,7 @@
|
||||
<artifactId>spring-boot-starter-webflux</artifactId> <!-- 用于 WebClient -->
|
||||
</dependency>
|
||||
|
||||
<!-- TODO: 考虑删除已注释的依赖 -->
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>org.springframework.ai</groupId>-->
|
||||
<!-- <artifactId>spring-ai-openai-spring-boot-starter</artifactId>-->
|
||||
@@ -72,6 +74,7 @@
|
||||
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- TODO: 检查内存相关依赖是否都需要 -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud.ai</groupId>
|
||||
<artifactId>spring-ai-alibaba-starter-memory</artifactId>
|
||||
@@ -100,7 +103,7 @@
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- TODO: 考虑删除已注释的依赖 -->
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>com.alibaba</groupId>-->
|
||||
<!-- <artifactId>dashscope-sdk-java</artifactId>-->
|
||||
@@ -198,11 +201,13 @@
|
||||
<build>
|
||||
<finalName>ai-interview</finalName>
|
||||
<plugins>
|
||||
<!-- TODO: 检查Spring Boot插件版本是否与父POM一致 -->
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<version>3.5.0</version>
|
||||
</plugin>
|
||||
<!-- TODO: 考虑在正式环境中启用测试 -->
|
||||
<!-- maven 打包时跳过测试 -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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> T getBean(Class<T> beanClass) {
|
||||
2
src/main/java/com/qingqiu/interview/common/utils/TreeUtil.java → src/main/java/com/qingqiu/interview/common/utils/TreeUtils.java
Executable file → Normal file
2
src/main/java/com/qingqiu/interview/common/utils/TreeUtil.java → src/main/java/com/qingqiu/interview/common/utils/TreeUtils.java
Executable file → Normal file
@@ -7,7 +7,7 @@ import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class TreeUtil {
|
||||
public class TreeUtils {
|
||||
|
||||
/**
|
||||
* 通用树形结构构建方法
|
||||
@@ -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))
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
100
src/main/java/com/qingqiu/interview/config/OpenAiChatConfig.java
Normal file
100
src/main/java/com/qingqiu/interview/config/OpenAiChatConfig.java
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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<InterviewReportResponse> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
@@ -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
|
||||
) {
|
||||
}
|
||||
60
src/main/java/com/qingqiu/interview/service/DashboardService.java
Executable file → Normal file
60
src/main/java/com/qingqiu/interview/service/DashboardService.java
Executable file → Normal file
@@ -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<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));
|
||||
* @program: ai-interview
|
||||
* @description: 工作台接口
|
||||
* @author: huangpeng
|
||||
* @create: 2025-11-07 14:54
|
||||
**/
|
||||
public interface DashboardService {
|
||||
|
||||
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());
|
||||
}
|
||||
DashboardStatsResponse getDashboardStats();
|
||||
}
|
||||
|
||||
@@ -54,8 +54,17 @@ public interface InterviewService extends IService<InterviewSession> {
|
||||
|
||||
/**
|
||||
* 获取面试报告
|
||||
*
|
||||
* @param sessionId
|
||||
* @return
|
||||
*/
|
||||
InterviewReportResponse getInterviewReport(String sessionId);
|
||||
|
||||
/**
|
||||
* 读取pdf文件数据
|
||||
*
|
||||
* @param file 文件
|
||||
* @return 文件内容
|
||||
*/
|
||||
String readPdfFile(MultipartFile file) throws IOException;
|
||||
}
|
||||
|
||||
149
src/main/java/com/qingqiu/interview/service/QuestionClassificationService.java
Executable file → Normal file
149
src/main/java/com/qingqiu/interview/service/QuestionClassificationService.java
Executable file → Normal file
@@ -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 com.qingqiu.interview.entity.Question;
|
||||
import com.qingqiu.interview.entity.ai.QuestionClassificationAiRes;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class QuestionClassificationService {
|
||||
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
private final ChatClient chatClient;
|
||||
|
||||
/**
|
||||
* 使用AI对题目进行分类
|
||||
*/
|
||||
public List<Question> classifyQuestions(String rawContent) {
|
||||
log.info("开始使用AI分类题目,内容长度: {}", rawContent.length());
|
||||
* @program: ai-interview
|
||||
* @description: 题型分类
|
||||
* @author: huangpeng
|
||||
* @create: 2025-11-06 19:59
|
||||
**/
|
||||
public interface QuestionClassificationService {
|
||||
|
||||
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;
|
||||
}
|
||||
List<QuestionClassificationAiRes.Item> classifyQuestions(String rawContent);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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<InterviewQuestionProgress> 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<InterviewQuestionProgress> 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()))
|
||||
|
||||
@@ -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")) {
|
||||
|
||||
|
||||
@@ -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<InterviewSessionMapper, In
|
||||
public InterviewSession startInterview(MultipartFile file, InterviewStartRequest dto) throws IOException {
|
||||
// 1. 创建并保存会话主记录
|
||||
String sessionId = UUID.randomUUID().toString().replace("-", "");
|
||||
String resumeContent = parseResume(file);
|
||||
String resumeContent = readPdfFile(file);
|
||||
InterviewSession session = new InterviewSession();
|
||||
|
||||
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());
|
||||
// 1. 获取简历解析器
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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<QuestionMapper, Question> implements QuestionService {
|
||||
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 IQuestionCategoryService questionCategoryService;
|
||||
|
||||
private final ChatClient chatClient;
|
||||
|
||||
/**
|
||||
* 分页查询题库
|
||||
*/
|
||||
@@ -96,19 +106,19 @@ public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> i
|
||||
.orElseThrow(() -> new IllegalArgumentException("不支持的文件类型: " + fileExtension));
|
||||
|
||||
String content = parser.parse(file.getInputStream());
|
||||
List<Question> questionsFromAi = classificationService.classifyQuestions(content);
|
||||
List<QuestionClassificationAiRes.Item> 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<QuestionMapper, Question> i
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void useAiCheckQuestionData() {
|
||||
// // 查询数据库
|
||||
// List<Question> questions = questionMapper.selectList(
|
||||
// new LambdaQueryWrapper<Question>()
|
||||
// .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<Question> questions = questionMapper.selectList(
|
||||
new LambdaQueryWrapper<Question>()
|
||||
.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<String> list = Arrays.asList(s.split(","));
|
||||
// TODO: 检查这些注释代码是否可以删除
|
||||
// JSONObject parse = JSONObject.parse(chat);
|
||||
// JSONArray questionsIds = parse.getJSONArray("questions");
|
||||
// List<Long> list = questionsIds.toList(Long.class);
|
||||
// questionMapper.delete(
|
||||
// new LambdaQueryWrapper<Question>()
|
||||
// .notIn(Question::getId, list)
|
||||
// );
|
||||
questionMapper.delete(
|
||||
new LambdaQueryWrapper<Question>()
|
||||
.notIn(Question::getId, list)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -155,25 +167,71 @@ public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> i
|
||||
}
|
||||
JSONObject jsonObject = new JSONObject();
|
||||
jsonObject.put("data", jsonArray);
|
||||
return String.format("""
|
||||
你是一个数据清洗与去重专家。你的任务是从以下文本列表中,识别出语义相同或高度相似的条目,并为每一组相似条目筛选出一个最规范、最简洁的代表性版本。
|
||||
|
||||
【去重规则】
|
||||
1. **语义核心优先**:忽略无关的修饰词、标点符号、后缀(如“:2”)、大小写和空格。
|
||||
2. **合并同类项**:将表达同一主题或问题的文本归为一组。
|
||||
3. **选择标准**:从每一组中,选出那个最完整、最简洁、且没有多余符号(如序号、特殊后缀)的版本作为代表。如果两个版本质量相当,优先选择更短的那个。
|
||||
4. **保留原意**:确保选出的代表版本没有改变原文本的核心含义。
|
||||
请按照下述格式返回,已被剔除掉的数据无需返回
|
||||
{
|
||||
"questions": [1, 2, 3, .....]
|
||||
}
|
||||
分类规则:
|
||||
return PromptTemplate.builder()
|
||||
.renderer(
|
||||
StTemplateRenderer.builder()
|
||||
.startDelimiterToken('<')
|
||||
.endDelimiterToken('>')
|
||||
.build()
|
||||
)
|
||||
.template("""
|
||||
请对以下题库JSON数据进行智能去重处理。
|
||||
|
||||
## 任务说明
|
||||
识别并移除语义相似或表达意思基本相同的重复题目,只保留每个独特题目的一个版本。
|
||||
|
||||
## 语义相似度判断标准
|
||||
1. 核心意思相同:即使表述不同,但考察的知识点和答案逻辑一致
|
||||
2. 同义替换:使用同义词、近义词但意思不变的题目
|
||||
3. 句式变换:主动被动语态转换、疑问词替换等句式变化
|
||||
4. 冗余表述:增加了无关修饰词但核心内容相同的题目
|
||||
|
||||
## 处理规则
|
||||
- 对语义相似的题目组,只保留其中一条数据
|
||||
- 保留原则:选择表述最清晰、最完整的那条
|
||||
- 如果难以判断,保留ID较小或创建时间较早的那条
|
||||
|
||||
## 输出要求
|
||||
1. 只返回JSON,不要其他解释文字
|
||||
2. 请严格按照API接口形式返回,不要返回任何额外的文字内容,包括'```json```'!!!!
|
||||
3. 请严格按照网络接口的形式返回JSON数据!!!
|
||||
【请处理以下数据列表】:
|
||||
%s
|
||||
""", jsonObject.toJSONString());
|
||||
{
|
||||
"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())) {
|
||||
questionCategories = questionCategoryService.batchFindByAncestorIdsUnion(dto.getCategoryIds());
|
||||
if (CollectionUtil.isNotEmpty(questionCategories)) {
|
||||
treeList = TreeUtil.buildTree(
|
||||
treeList = TreeUtils.buildTree(
|
||||
questionCategories,
|
||||
QuestionCategory::getId,
|
||||
QuestionCategory::getParentId,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user