使用spring-ai-alibaba重构项目
This commit is contained in:
7
pom.xml
7
pom.xml
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
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) {
|
||||||
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.function.Function;
|
||||||
import java.util.stream.Collectors;
|
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.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))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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.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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
) {
|
||||||
|
}
|
||||||
74
src/main/java/com/qingqiu/interview/service/DashboardService.java
Executable file → Normal file
74
src/main/java/com/qingqiu/interview/service/DashboardService.java
Executable file → Normal 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;
|
* @program: ai-interview
|
||||||
|
* @description: 工作台接口
|
||||||
import java.time.LocalDate;
|
* @author: huangpeng
|
||||||
import java.time.format.DateTimeFormatter;
|
* @create: 2025-11-07 14:54
|
||||||
import java.util.List;
|
**/
|
||||||
import java.util.Map;
|
public interface DashboardService {
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.IntStream;
|
DashboardStatsResponse getDashboardStats();
|
||||||
|
}
|
||||||
@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));
|
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
165
src/main/java/com/qingqiu/interview/service/QuestionClassificationService.java
Executable file → Normal file
165
src/main/java/com/qingqiu/interview/service/QuestionClassificationService.java
Executable file → Normal 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.qingqiu.interview.entity.Question;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.qingqiu.interview.entity.ai.QuestionClassificationAiRes;
|
||||||
import com.qingqiu.interview.common.constants.ChatConstant;
|
|
||||||
import com.qingqiu.interview.common.utils.UUIDUtils;
|
import java.util.List;
|
||||||
import com.qingqiu.interview.entity.Question;
|
|
||||||
import jakarta.annotation.Resource;
|
/**
|
||||||
import lombok.RequiredArgsConstructor;
|
* @program: ai-interview
|
||||||
import lombok.extern.slf4j.Slf4j;
|
* @description: 题型分类
|
||||||
import org.springframework.ai.chat.client.ChatClient;
|
* @author: huangpeng
|
||||||
import org.springframework.ai.chat.memory.ChatMemory;
|
* @create: 2025-11-06 19:59
|
||||||
import org.springframework.stereotype.Service;
|
**/
|
||||||
|
public interface QuestionClassificationService {
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
List<QuestionClassificationAiRes.Item> classifyQuestions(String rawContent);
|
||||||
|
}
|
||||||
@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());
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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 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()))
|
||||||
|
|||||||
@@ -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")) {
|
||||||
|
|
||||||
|
|||||||
@@ -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. 获取简历解析器
|
||||||
|
|||||||
@@ -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;
|
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()
|
||||||
|
.renderer(
|
||||||
【去重规则】
|
StTemplateRenderer.builder()
|
||||||
1. **语义核心优先**:忽略无关的修饰词、标点符号、后缀(如“:2”)、大小写和空格。
|
.startDelimiterToken('<')
|
||||||
2. **合并同类项**:将表达同一主题或问题的文本归为一组。
|
.endDelimiterToken('>')
|
||||||
3. **选择标准**:从每一组中,选出那个最完整、最简洁、且没有多余符号(如序号、特殊后缀)的版本作为代表。如果两个版本质量相当,优先选择更短的那个。
|
.build()
|
||||||
4. **保留原意**:确保选出的代表版本没有改变原文本的核心含义。
|
)
|
||||||
请按照下述格式返回,已被剔除掉的数据无需返回
|
.template("""
|
||||||
{
|
请对以下题库JSON数据进行智能去重处理。
|
||||||
"questions": [1, 2, 3, .....]
|
|
||||||
}
|
## 任务说明
|
||||||
分类规则:
|
识别并移除语义相似或表达意思基本相同的重复题目,只保留每个独特题目的一个版本。
|
||||||
1. 只返回JSON,不要其他解释文字
|
|
||||||
2. 请严格按照API接口形式返回,不要返回任何额外的文字内容,包括'```json```'!!!!
|
## 语义相似度判断标准
|
||||||
3. 请严格按照网络接口的形式返回JSON数据!!!
|
1. 核心意思相同:即使表述不同,但考察的知识点和答案逻辑一致
|
||||||
【请处理以下数据列表】:
|
2. 同义替换:使用同义词、近义词但意思不变的题目
|
||||||
%s
|
3. 句式变换:主动被动语态转换、疑问词替换等句式变化
|
||||||
""", jsonObject.toJSONString());
|
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,
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -52,4 +61,4 @@ mybatis-plus:
|
|||||||
db-config:
|
db-config:
|
||||||
logic-delete-field: deleted # 全局逻辑删除字段名
|
logic-delete-field: deleted # 全局逻辑删除字段名
|
||||||
logic-delete-value: 1 # 逻辑已删除值。可选,默认值为 1
|
logic-delete-value: 1 # 逻辑已删除值。可选,默认值为 1
|
||||||
logic-not-delete-value: 0 # 逻辑未删除值。可选,默认值为 0
|
logic-not-delete-value: 0 # 逻辑未删除值。可选,默认值为 0
|
||||||
|
|||||||
Reference in New Issue
Block a user