Compare commits
4 Commits
704ea4ab7b
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b357fbb93 | |||
| a384bbfd16 | |||
| 7f24d65d76 | |||
| d14b46d007 |
0
.gitignore
vendored
Normal file → Executable file
0
.gitignore
vendored
Normal file → Executable file
@@ -1,135 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="dashboard-container">
|
|
||||||
<!-- 欢迎横幅 -->
|
|
||||||
<el-card shadow="never" class="welcome-banner">
|
|
||||||
<div class="welcome-content">
|
|
||||||
<div class="welcome-text">
|
|
||||||
<h2>欢迎回来!</h2>
|
|
||||||
<p>准备好开始您的下一次模拟面试了吗?在这里管理您的题库,不断提升面试技巧。</p>
|
|
||||||
</div>
|
|
||||||
<img src="/src/assets/dashboard-hero.svg" alt="仪表盘插图" class="welcome-illustration" />
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<!-- 功能导航 -->
|
|
||||||
<div class="feature-grid">
|
|
||||||
<router-link to="/interview" class="feature-card-link">
|
|
||||||
<el-card shadow="hover" class="feature-card">
|
|
||||||
<div class="card-content">
|
|
||||||
<el-icon class="card-icon" style="background-color: #ecf5ff; color: #409eff;"><ChatLineRound /></el-icon>
|
|
||||||
<div class="text-content">
|
|
||||||
<h3>开始模拟面试</h3>
|
|
||||||
<p>上传简历,与AI进行实战演练</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</router-link>
|
|
||||||
|
|
||||||
<router-link to="/question-bank" class="feature-card-link">
|
|
||||||
<el-card shadow="hover" class="feature-card">
|
|
||||||
<div class="card-content">
|
|
||||||
<el-icon class="card-icon" style="background-color: #f0f9eb; color: #67c23a;"><MessageBox /></el-icon>
|
|
||||||
<div class="text-content">
|
|
||||||
<h3>题库管理</h3>
|
|
||||||
<p>新增、编辑和导入您的面试题库</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</router-link>
|
|
||||||
|
|
||||||
<router-link to="/history" class="feature-card-link">
|
|
||||||
<el-card shadow="hover" class="feature-card">
|
|
||||||
<div class="card-content">
|
|
||||||
<el-icon class="card-icon" style="background-color: #fdf6ec; color: #e6a23c;"><Finished /></el-icon>
|
|
||||||
<div class="text-content">
|
|
||||||
<h3>面试历史</h3>
|
|
||||||
<p>查看过往的面试记录与AI复盘报告</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
// 导入Element Plus图标
|
|
||||||
import { ChatLineRound, MessageBox, Finished } from '@element-plus/icons-vue';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* 仪表盘容器 */
|
|
||||||
.dashboard-container {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 欢迎横幅 */
|
|
||||||
.welcome-banner {
|
|
||||||
border: none;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.welcome-content {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.welcome-text h2 {
|
|
||||||
font-size: 1.8em;
|
|
||||||
margin-top: 0;
|
|
||||||
color: #303133;
|
|
||||||
}
|
|
||||||
|
|
||||||
.welcome-text p {
|
|
||||||
color: #606266;
|
|
||||||
font-size: 1.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.welcome-illustration {
|
|
||||||
width: 200px;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 功能网格布局 */
|
|
||||||
.feature-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-card-link {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-card .card-content {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 20px;
|
|
||||||
transition: transform 0.3s, box-shadow 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-card:hover {
|
|
||||||
transform: translateY(-5px);
|
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-icon {
|
|
||||||
font-size: 32px;
|
|
||||||
padding: 15px;
|
|
||||||
border-radius: 50%;
|
|
||||||
margin-right: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-content h3 {
|
|
||||||
margin: 0 0 8px 0;
|
|
||||||
color: #303133;
|
|
||||||
font-size: 1.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-content p {
|
|
||||||
margin: 0;
|
|
||||||
color: #909399;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
7
pom.xml
Normal file → Executable file
7
pom.xml
Normal file → Executable file
@@ -5,7 +5,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-parent</artifactId>
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
<version>3.4.10-SNAPSHOT</version>
|
<version>3.5.0</version>
|
||||||
<relativePath/> <!-- lookup parent from repository -->
|
<relativePath/> <!-- lookup parent from repository -->
|
||||||
</parent>
|
</parent>
|
||||||
<groupId>com.qingqiu</groupId>
|
<groupId>com.qingqiu</groupId>
|
||||||
@@ -43,6 +43,11 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-web</artifactId>
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- aop和aspect -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-aop</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-webflux</artifactId> <!-- 用于 WebClient -->
|
<artifactId>spring-boot-starter-webflux</artifactId> <!-- 用于 WebClient -->
|
||||||
|
|||||||
0
sql/.idea/.gitignore
generated
vendored
Normal file → Executable file
0
sql/.idea/.gitignore
generated
vendored
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/AiInterviewApplication.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/AiInterviewApplication.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/ai/entity/Message.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/ai/entity/Message.java
Normal file → Executable file
4
src/main/java/com/qingqiu/interview/ai/factory/AIClientFactory.java
Normal file → Executable file
4
src/main/java/com/qingqiu/interview/ai/factory/AIClientFactory.java
Normal file → Executable file
@@ -1,7 +1,11 @@
|
|||||||
package com.qingqiu.interview.ai.factory;
|
package com.qingqiu.interview.ai.factory;
|
||||||
|
|
||||||
|
import com.qingqiu.interview.common.enums.LLMProvider;
|
||||||
import com.qingqiu.interview.ai.service.AIClientService;
|
import com.qingqiu.interview.ai.service.AIClientService;
|
||||||
|
|
||||||
public interface AIClientFactory {
|
public interface AIClientFactory {
|
||||||
AIClientService createAIClient();
|
AIClientService createAIClient();
|
||||||
|
|
||||||
|
// 支持的提供商
|
||||||
|
LLMProvider getSupportedProvider();
|
||||||
}
|
}
|
||||||
|
|||||||
22
src/main/java/com/qingqiu/interview/ai/factory/AIClientManager.java
Normal file → Executable file
22
src/main/java/com/qingqiu/interview/ai/factory/AIClientManager.java
Normal file → Executable file
@@ -1,24 +1,32 @@
|
|||||||
package com.qingqiu.interview.ai.factory;
|
package com.qingqiu.interview.ai.factory;
|
||||||
|
|
||||||
|
import com.qingqiu.interview.common.enums.LLMProvider;
|
||||||
import com.qingqiu.interview.ai.service.AIClientService;
|
import com.qingqiu.interview.ai.service.AIClientService;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class AIClientManager {
|
public class AIClientManager {
|
||||||
|
|
||||||
private final Map<String, AIClientFactory> factories;
|
private final Map<LLMProvider, AIClientFactory> factories;
|
||||||
|
|
||||||
public AIClientManager(Map<String, AIClientFactory> factories) {
|
public AIClientManager(List<AIClientFactory> strategies) {
|
||||||
this.factories = factories;
|
this.factories = strategies.stream()
|
||||||
|
.collect(Collectors.toMap(
|
||||||
|
AIClientFactory::getSupportedProvider,
|
||||||
|
Function.identity()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
public AIClientService getClient(String aiType) {
|
public AIClientService getClient(LLMProvider provider) {
|
||||||
String factoryName = aiType + "ClientFactory";
|
// String factoryName = aiType + "ClientFactory";
|
||||||
AIClientFactory factory = factories.get(factoryName);
|
AIClientFactory factory = factories.get(provider);
|
||||||
if (factory == null) {
|
if (factory == null) {
|
||||||
throw new IllegalArgumentException("不支持的AI type: " + aiType);
|
throw new IllegalArgumentException("不支持的AI type: " + provider);
|
||||||
}
|
}
|
||||||
return factory.createAIClient();
|
return factory.createAIClient();
|
||||||
}
|
}
|
||||||
|
|||||||
6
src/main/java/com/qingqiu/interview/ai/factory/DeepSeekClientFactory.java
Normal file → Executable file
6
src/main/java/com/qingqiu/interview/ai/factory/DeepSeekClientFactory.java
Normal file → Executable file
@@ -1,5 +1,6 @@
|
|||||||
package com.qingqiu.interview.ai.factory;
|
package com.qingqiu.interview.ai.factory;
|
||||||
|
|
||||||
|
import com.qingqiu.interview.common.enums.LLMProvider;
|
||||||
import com.qingqiu.interview.ai.service.AIClientService;
|
import com.qingqiu.interview.ai.service.AIClientService;
|
||||||
import com.qingqiu.interview.ai.service.impl.DeepSeekClientServiceImpl;
|
import com.qingqiu.interview.ai.service.impl.DeepSeekClientServiceImpl;
|
||||||
import com.qingqiu.interview.common.utils.SpringApplicationContextUtil;
|
import com.qingqiu.interview.common.utils.SpringApplicationContextUtil;
|
||||||
@@ -11,4 +12,9 @@ public class DeepSeekClientFactory implements AIClientFactory{
|
|||||||
public AIClientService createAIClient() {
|
public AIClientService createAIClient() {
|
||||||
return SpringApplicationContextUtil.getBean(DeepSeekClientServiceImpl.class);
|
return SpringApplicationContextUtil.getBean(DeepSeekClientServiceImpl.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LLMProvider getSupportedProvider() {
|
||||||
|
return LLMProvider.DEEPSEEK;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
6
src/main/java/com/qingqiu/interview/ai/factory/QwenClientFactory.java
Normal file → Executable file
6
src/main/java/com/qingqiu/interview/ai/factory/QwenClientFactory.java
Normal file → Executable file
@@ -1,5 +1,6 @@
|
|||||||
package com.qingqiu.interview.ai.factory;
|
package com.qingqiu.interview.ai.factory;
|
||||||
|
|
||||||
|
import com.qingqiu.interview.common.enums.LLMProvider;
|
||||||
import com.qingqiu.interview.ai.service.AIClientService;
|
import com.qingqiu.interview.ai.service.AIClientService;
|
||||||
import com.qingqiu.interview.ai.service.impl.QwenClientServiceImpl;
|
import com.qingqiu.interview.ai.service.impl.QwenClientServiceImpl;
|
||||||
import com.qingqiu.interview.common.utils.SpringApplicationContextUtil;
|
import com.qingqiu.interview.common.utils.SpringApplicationContextUtil;
|
||||||
@@ -11,4 +12,9 @@ public class QwenClientFactory implements AIClientFactory{
|
|||||||
public AIClientService createAIClient() {
|
public AIClientService createAIClient() {
|
||||||
return SpringApplicationContextUtil.getBean(QwenClientServiceImpl.class);
|
return SpringApplicationContextUtil.getBean(QwenClientServiceImpl.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LLMProvider getSupportedProvider() {
|
||||||
|
return LLMProvider.QWEN;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
0
src/main/java/com/qingqiu/interview/ai/service/AIClientService.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/ai/service/AIClientService.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/ai/service/impl/DeepSeekClientServiceImpl.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/ai/service/impl/DeepSeekClientServiceImpl.java
Normal file → Executable file
2
src/main/java/com/qingqiu/interview/ai/service/impl/QwenClientServiceImpl.java
Normal file → Executable file
2
src/main/java/com/qingqiu/interview/ai/service/impl/QwenClientServiceImpl.java
Normal file → Executable file
@@ -4,6 +4,7 @@ import com.alibaba.dashscope.aigc.generation.Generation;
|
|||||||
import com.alibaba.dashscope.aigc.generation.GenerationParam;
|
import com.alibaba.dashscope.aigc.generation.GenerationParam;
|
||||||
import com.alibaba.dashscope.aigc.generation.GenerationResult;
|
import com.alibaba.dashscope.aigc.generation.GenerationResult;
|
||||||
import com.alibaba.dashscope.common.Message;
|
import com.alibaba.dashscope.common.Message;
|
||||||
|
import com.alibaba.dashscope.common.ResponseFormat;
|
||||||
import com.alibaba.dashscope.exception.ApiException;
|
import com.alibaba.dashscope.exception.ApiException;
|
||||||
import com.alibaba.dashscope.exception.InputRequiredException;
|
import com.alibaba.dashscope.exception.InputRequiredException;
|
||||||
import com.alibaba.dashscope.exception.NoApiKeyException;
|
import com.alibaba.dashscope.exception.NoApiKeyException;
|
||||||
@@ -42,6 +43,7 @@ public class QwenClientServiceImpl extends AIClientService {
|
|||||||
.model(QWEN_PLUS_LATEST) // 可根据需要更换模型
|
.model(QWEN_PLUS_LATEST) // 可根据需要更换模型
|
||||||
.messages(messages)
|
.messages(messages)
|
||||||
.resultFormat(GenerationParam.ResultFormat.MESSAGE)
|
.resultFormat(GenerationParam.ResultFormat.MESSAGE)
|
||||||
|
.responseFormat(ResponseFormat.builder().type(ResponseFormat.JSON_OBJECT).build())
|
||||||
.apiKey(apiKey)
|
.apiKey(apiKey)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
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 {
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.qingqiu.interview.aspect;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>
|
||||||
|
* ai聊天的切面
|
||||||
|
* </h1>
|
||||||
|
*
|
||||||
|
* @author qingqiu
|
||||||
|
* @date 2025/9/18 13:00
|
||||||
|
*/
|
||||||
|
@Aspect
|
||||||
|
@Component
|
||||||
|
public class AiChatLogAspect {
|
||||||
|
|
||||||
|
public AiChatLogAspect() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Pointcut("@annotation(com.qingqiu.interview.annotation.AiChatLog)")
|
||||||
|
public void logPointCut() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Around("logPointCut()")
|
||||||
|
public Object around(ProceedingJoinPoint point) throws Throwable {
|
||||||
|
Object result = point.proceed();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
0
src/main/java/com/qingqiu/interview/common/constants/AIStrategyConstant.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/common/constants/AIStrategyConstant.java
Normal file → Executable file
17
src/main/java/com/qingqiu/interview/common/constants/CommonConstant.java
Executable file
17
src/main/java/com/qingqiu/interview/common/constants/CommonConstant.java
Executable file
@@ -0,0 +1,17 @@
|
|||||||
|
package com.qingqiu.interview.common.constants;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>公共常量</h1>
|
||||||
|
* @author huangpeng
|
||||||
|
* @date 2025/9/11 09:30
|
||||||
|
*/
|
||||||
|
public class CommonConstant {
|
||||||
|
|
||||||
|
public static final Integer ZERO = 0;
|
||||||
|
public static final Integer ONE = 1;
|
||||||
|
public static final Long ROOT_PARENT_ID = 0L;
|
||||||
|
public static final Integer MAX_TOKEN = 64000;
|
||||||
|
public static final BigDecimal DEFAULT_TRUNCATE_RATIO = new BigDecimal("0.1");
|
||||||
|
}
|
||||||
0
src/main/java/com/qingqiu/interview/common/constants/QwenModelConstant.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/common/constants/QwenModelConstant.java
Normal file → Executable file
63
src/main/java/com/qingqiu/interview/common/enums/CommonStateEnum.java
Executable file
63
src/main/java/com/qingqiu/interview/common/enums/CommonStateEnum.java
Executable file
@@ -0,0 +1,63 @@
|
|||||||
|
package com.qingqiu.interview.common.enums;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1></h1>
|
||||||
|
*
|
||||||
|
* @author huangpeng
|
||||||
|
* @date 2025/9/11 09:49
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor
|
||||||
|
public enum CommonStateEnum {
|
||||||
|
/**
|
||||||
|
* 禁用状态
|
||||||
|
*/
|
||||||
|
DISABLED(0, "禁用"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启用状态
|
||||||
|
*/
|
||||||
|
ENABLED(1, "启用"),
|
||||||
|
;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态码
|
||||||
|
*/
|
||||||
|
private final Integer code;
|
||||||
|
|
||||||
|
private final String value;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据状态码获取枚举
|
||||||
|
*/
|
||||||
|
public static CommonStateEnum getByCode(Integer code) {
|
||||||
|
if (code == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (CommonStateEnum state : values()) {
|
||||||
|
if (state.getCode().equals(code)) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据标识获取枚举
|
||||||
|
*/
|
||||||
|
public static CommonStateEnum getByValue(String value) {
|
||||||
|
if (value == null || value.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (CommonStateEnum state : values()) {
|
||||||
|
if (state.getValue().equalsIgnoreCase(value)) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.qingqiu.interview.common.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1></h1>
|
||||||
|
*
|
||||||
|
* @author qingqiu
|
||||||
|
* @date 2025/9/18 16:43
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum DocumentParserProvider {
|
||||||
|
|
||||||
|
PDF("pdf"),
|
||||||
|
MARKDOWN("md"),
|
||||||
|
|
||||||
|
;
|
||||||
|
|
||||||
|
private final String code;
|
||||||
|
|
||||||
|
DocumentParserProvider(String code) {
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DocumentParserProvider fromCode(String code) {
|
||||||
|
for (DocumentParserProvider provider : values()) {
|
||||||
|
if (provider.getCode().equals(code)) {
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("Unknown provider: " + code);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.qingqiu.interview.common.enums;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public enum LLMProvider {
|
||||||
|
|
||||||
|
OPEN_AI("openai"),
|
||||||
|
CLAUDE("claude"),
|
||||||
|
GEMINI("gemini"),
|
||||||
|
DEEPSEEK("deepSeek"),
|
||||||
|
OLLAMA("ollama"),
|
||||||
|
QWEN("qwen"),
|
||||||
|
;
|
||||||
|
|
||||||
|
private final String code;
|
||||||
|
|
||||||
|
LLMProvider(String code) {
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LLMProvider fromCode(String code) {
|
||||||
|
for (LLMProvider provider : values()) {
|
||||||
|
if (provider.getCode().equals(code)) {
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("Unknown provider: " + code);
|
||||||
|
}
|
||||||
|
}
|
||||||
0
src/main/java/com/qingqiu/interview/common/ex/ApiException.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/common/ex/ApiException.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/common/ex/GlobalErrorHandler.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/common/ex/GlobalErrorHandler.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/common/res/IErrorCode.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/common/res/IErrorCode.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/common/res/R.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/common/res/R.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/common/res/ResultCode.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/common/res/ResultCode.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/common/service/HttpService.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/common/service/HttpService.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/common/service/impl/HttpServiceImpl.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/common/service/impl/HttpServiceImpl.java
Normal file → Executable file
15
src/main/java/com/qingqiu/interview/common/utils/AIUtils.java
Normal file → Executable file
15
src/main/java/com/qingqiu/interview/common/utils/AIUtils.java
Normal file → Executable file
@@ -2,6 +2,10 @@ package com.qingqiu.interview.common.utils;
|
|||||||
|
|
||||||
import com.alibaba.dashscope.common.Message;
|
import com.alibaba.dashscope.common.Message;
|
||||||
import com.alibaba.dashscope.common.Role;
|
import com.alibaba.dashscope.common.Role;
|
||||||
|
import com.alibaba.dashscope.tokenizers.Tokenizer;
|
||||||
|
import com.alibaba.dashscope.tokenizers.TokenizerFactory;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class AIUtils {
|
public class AIUtils {
|
||||||
|
|
||||||
@@ -23,4 +27,15 @@ public class AIUtils {
|
|||||||
public static Message createSystemMessage(String prompt) {
|
public static Message createSystemMessage(String prompt) {
|
||||||
return createMessage(Role.SYSTEM.getValue(), prompt);
|
return createMessage(Role.SYSTEM.getValue(), prompt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取prompt的token数
|
||||||
|
* @param prompt 输入
|
||||||
|
* @return tokens
|
||||||
|
*/
|
||||||
|
public static Integer getPromptTokens(String prompt) {
|
||||||
|
Tokenizer tokenizer = TokenizerFactory.qwen();
|
||||||
|
List<Integer> integers = tokenizer.encodeOrdinary(prompt);
|
||||||
|
return integers.size();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
0
src/main/java/com/qingqiu/interview/common/utils/SpringApplicationContextUtil.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/common/utils/SpringApplicationContextUtil.java
Normal file → Executable file
63
src/main/java/com/qingqiu/interview/common/utils/TreeUtil.java
Executable file
63
src/main/java/com/qingqiu/interview/common/utils/TreeUtil.java
Executable file
@@ -0,0 +1,63 @@
|
|||||||
|
package com.qingqiu.interview.common.utils;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public class TreeUtil {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用树形结构构建方法
|
||||||
|
*/
|
||||||
|
public static <T, ID> List<T> buildTree(List<T> list,
|
||||||
|
Function<T, ID> idGetter,
|
||||||
|
Function<T, ID> parentIdGetter,
|
||||||
|
Function<T, List<T>> childrenSetter,
|
||||||
|
ID rootParentId) {
|
||||||
|
if (list == null || list.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按父ID分组
|
||||||
|
Map<ID, List<T>> parentMap = list.stream()
|
||||||
|
.collect(Collectors.groupingBy(parentIdGetter));
|
||||||
|
|
||||||
|
// 设置子节点
|
||||||
|
list.forEach(item -> {
|
||||||
|
List<T> children = parentMap.get(idGetter.apply(item));
|
||||||
|
if (children != null && !children.isEmpty()) {
|
||||||
|
childrenSetter.apply(item).addAll(children);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 返回根节点
|
||||||
|
return parentMap.get(rootParentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扁平化树形结构
|
||||||
|
*/
|
||||||
|
public static <T> List<T> flattenTree(List<T> tree, Function<T, List<T>> childrenGetter) {
|
||||||
|
List<T> result = new ArrayList<>();
|
||||||
|
flattenTreeRecursive(tree, childrenGetter, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> void flattenTreeRecursive(List<T> nodes,
|
||||||
|
Function<T, List<T>> childrenGetter,
|
||||||
|
List<T> result) {
|
||||||
|
if (nodes == null) return;
|
||||||
|
|
||||||
|
for (T node : nodes) {
|
||||||
|
result.add(node);
|
||||||
|
List<T> children = childrenGetter.apply(node);
|
||||||
|
if (children != null && !children.isEmpty()) {
|
||||||
|
flattenTreeRecursive(children, childrenGetter, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
0
src/main/java/com/qingqiu/interview/config/DashScopeConfig.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/config/DashScopeConfig.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/config/JacksonConfig.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/config/JacksonConfig.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/config/MyBatisPlusConfig.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/config/MyBatisPlusConfig.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/config/WebClientConfig.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/config/WebClientConfig.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/controller/AiSessionLogController.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/controller/AiSessionLogController.java
Normal file → Executable file
@@ -0,0 +1,36 @@
|
|||||||
|
package com.qingqiu.interview.controller;
|
||||||
|
|
||||||
|
import com.qingqiu.interview.common.res.R;
|
||||||
|
import com.qingqiu.interview.dto.ChatDTO;
|
||||||
|
import com.qingqiu.interview.dto.InterviewStartRequest;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>AI聊天控制器</h1>
|
||||||
|
*
|
||||||
|
* @author qingqiu
|
||||||
|
* @date 2025/9/18 12:11
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/chat")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ChatController {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建聊天
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@PostMapping("/send")
|
||||||
|
public R<?> createChat(@RequestBody ChatDTO dto) {
|
||||||
|
return R.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/interview/create")
|
||||||
|
public R<?> createInterview(@RequestParam("resume") MultipartFile resume,
|
||||||
|
@Validated @ModelAttribute InterviewStartRequest request) {
|
||||||
|
return R.success();
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src/main/java/com/qingqiu/interview/controller/DashboardController.java
Normal file → Executable file
2
src/main/java/com/qingqiu/interview/controller/DashboardController.java
Normal file → Executable file
@@ -12,7 +12,7 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
* 仪表盘数据统计接口
|
* 仪表盘数据统计接口
|
||||||
*/
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/dashboard")
|
@RequestMapping("/dashboard")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class DashboardController {
|
public class DashboardController {
|
||||||
|
|
||||||
|
|||||||
13
src/main/java/com/qingqiu/interview/controller/InterviewController.java
Normal file → Executable file
13
src/main/java/com/qingqiu/interview/controller/InterviewController.java
Normal file → Executable file
@@ -1,20 +1,24 @@
|
|||||||
package com.qingqiu.interview.controller;
|
package com.qingqiu.interview.controller;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
import com.qingqiu.interview.dto.*;
|
import com.qingqiu.interview.dto.*;
|
||||||
import com.qingqiu.interview.entity.InterviewSession;
|
import com.qingqiu.interview.entity.InterviewSession;
|
||||||
import com.qingqiu.interview.service.InterviewService;
|
import com.qingqiu.interview.service.InterviewService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
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.io.IOException;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 面试流程相关接口
|
* 面试流程相关接口
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/interview")
|
@RequestMapping("/interview")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class InterviewController {
|
public class InterviewController {
|
||||||
|
|
||||||
@@ -27,8 +31,11 @@ public class InterviewController {
|
|||||||
public ApiResponse<InterviewResponse> startInterview(
|
public ApiResponse<InterviewResponse> startInterview(
|
||||||
@RequestParam("resume") MultipartFile resume,
|
@RequestParam("resume") MultipartFile resume,
|
||||||
@Validated @ModelAttribute InterviewStartRequest request) throws IOException {
|
@Validated @ModelAttribute InterviewStartRequest request) throws IOException {
|
||||||
InterviewResponse response = interviewService.startInterview(resume, request);
|
// InterviewResponse response = interviewService.startInterview(resume, request);
|
||||||
return ApiResponse.success(response);
|
log.info("接收到的数据: {}", JSONObject.toJSONString(request));
|
||||||
|
InterviewResponse interviewResponse = new InterviewResponse();
|
||||||
|
interviewResponse.setSessionId(UUID.randomUUID().toString().replace("-", ""));
|
||||||
|
return ApiResponse.success(interviewResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
2
src/main/java/com/qingqiu/interview/controller/InterviewQuestionProgressController.java
Normal file → Executable file
2
src/main/java/com/qingqiu/interview/controller/InterviewQuestionProgressController.java
Normal file → Executable file
@@ -19,7 +19,7 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
* @since 2025-08-30
|
* @since 2025-08-30
|
||||||
*/
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/interview-question-progress")
|
@RequestMapping("/interview-question-progress")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class InterviewQuestionProgressController {
|
public class InterviewQuestionProgressController {
|
||||||
|
|
||||||
|
|||||||
162
src/main/java/com/qingqiu/interview/controller/QuestionCategoryController.java
Executable file
162
src/main/java/com/qingqiu/interview/controller/QuestionCategoryController.java
Executable file
@@ -0,0 +1,162 @@
|
|||||||
|
package com.qingqiu.interview.controller;
|
||||||
|
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import com.qingqiu.interview.common.res.R;
|
||||||
|
import com.qingqiu.interview.dto.QuestionCategoryDTO;
|
||||||
|
import com.qingqiu.interview.dto.QuestionCategoryPageParams;
|
||||||
|
import com.qingqiu.interview.entity.QuestionCategory;
|
||||||
|
import com.qingqiu.interview.service.IQuestionCategoryService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/question-category")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class QuestionCategoryController {
|
||||||
|
|
||||||
|
@Lazy
|
||||||
|
private final IQuestionCategoryService questionCategoryService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分类树列表
|
||||||
|
*/
|
||||||
|
@GetMapping("/tree-list")
|
||||||
|
public R<List<QuestionCategory>> getTreeList() {
|
||||||
|
List<QuestionCategory> list = questionCategoryService.getTreeList();
|
||||||
|
return R.success(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/question-tree-list")
|
||||||
|
public R<List<QuestionCategory>> getQuestionTreeList() {
|
||||||
|
// List<QuestionCategory> list = questionCategoryService.getQuestionTreeList();
|
||||||
|
return R.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分类选项
|
||||||
|
*/
|
||||||
|
@GetMapping("/options")
|
||||||
|
public R<List<QuestionCategory>> getOptions() {
|
||||||
|
try {
|
||||||
|
List<QuestionCategory> options = questionCategoryService.getOptions();
|
||||||
|
return R.success(options);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("获取分类选项失败", e);
|
||||||
|
return R.error("获取分类选项失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分类详情
|
||||||
|
*/
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public R<QuestionCategory> getDetail(@PathVariable Long id) {
|
||||||
|
try {
|
||||||
|
QuestionCategory category = questionCategoryService.getCategoryDetail(id);
|
||||||
|
return R.success(category);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("获取分类详情失败", e);
|
||||||
|
return R.error("获取分类详情失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询分类
|
||||||
|
*/
|
||||||
|
@GetMapping("/page")
|
||||||
|
public R<Page<QuestionCategory>> getPage(QuestionCategoryPageParams query) {
|
||||||
|
try {
|
||||||
|
Page<QuestionCategory> pageR = questionCategoryService.getCategoryPage(query);
|
||||||
|
return R.success(pageR);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("分页查询分类失败", e);
|
||||||
|
return R.error("分页查询分类失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建分类
|
||||||
|
*/
|
||||||
|
@PostMapping
|
||||||
|
public R<Long> create(@Validated @RequestBody QuestionCategoryDTO dto) {
|
||||||
|
try {
|
||||||
|
Long id = questionCategoryService.createCategory(dto);
|
||||||
|
return R.success(id);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
log.error("创建分类失败", e);
|
||||||
|
return R.error(e.getMessage());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("创建分类失败", e);
|
||||||
|
return R.error("创建分类失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新分类
|
||||||
|
*/
|
||||||
|
@PostMapping("/update")
|
||||||
|
public R<Void> update(@RequestBody QuestionCategoryDTO dto) {
|
||||||
|
try {
|
||||||
|
questionCategoryService.updateCategory(dto);
|
||||||
|
return R.success();
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
log.error("更新分类失败", e);
|
||||||
|
return R.error(e.getMessage());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("更新分类失败", e);
|
||||||
|
return R.error("更新分类失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除分类
|
||||||
|
*/
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public R<Void> delete(@PathVariable Long id) {
|
||||||
|
try {
|
||||||
|
questionCategoryService.deleteCategory(id);
|
||||||
|
return R.success();
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
log.error("删除分类失败", e);
|
||||||
|
return R.error(e.getMessage());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("删除分类失败", e);
|
||||||
|
return R.error("删除分类失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新分类状态
|
||||||
|
*/
|
||||||
|
@PatchMapping("/{id}/state")
|
||||||
|
public R<Void> updateState(@PathVariable Long id, @RequestParam Integer state) {
|
||||||
|
try {
|
||||||
|
questionCategoryService.updateState(id, state);
|
||||||
|
return R.success();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("更新分类状态失败", e);
|
||||||
|
return R.error("更新分类状态失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索分类
|
||||||
|
*/
|
||||||
|
@GetMapping("/search")
|
||||||
|
public R<List<QuestionCategory>> search(@RequestParam String name) {
|
||||||
|
try {
|
||||||
|
List<QuestionCategory> res = questionCategoryService.searchByName(name);
|
||||||
|
return R.success(res);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("搜索分类失败", e);
|
||||||
|
return R.error("搜索分类失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/main/java/com/qingqiu/interview/controller/QuestionController.java
Normal file → Executable file
10
src/main/java/com/qingqiu/interview/controller/QuestionController.java
Normal file → Executable file
@@ -3,21 +3,24 @@ package com.qingqiu.interview.controller;
|
|||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.qingqiu.interview.common.res.R;
|
import com.qingqiu.interview.common.res.R;
|
||||||
import com.qingqiu.interview.dto.ApiResponse;
|
import com.qingqiu.interview.dto.ApiResponse;
|
||||||
|
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.service.QuestionService;
|
import com.qingqiu.interview.service.QuestionService;
|
||||||
|
import com.qingqiu.interview.vo.QuestionAndCategoryTreeListVO;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
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.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 题库管理相关接口
|
* 题库管理相关接口
|
||||||
*/
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/question")
|
@RequestMapping("/question")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class QuestionController {
|
public class QuestionController {
|
||||||
|
|
||||||
@@ -76,4 +79,9 @@ public class QuestionController {
|
|||||||
questionService.useAiCheckQuestionData();
|
questionService.useAiCheckQuestionData();
|
||||||
return R.success();
|
return R.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/tree-list-category")
|
||||||
|
public R<List<QuestionAndCategoryTreeListVO>> getTreeListCategory(@RequestBody QuestionOptionsDTO dto) {
|
||||||
|
return R.success(questionService.getTreeListCategory(dto));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
0
src/main/java/com/qingqiu/interview/dto/ApiResponse.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/dto/ApiResponse.java
Normal file → Executable file
26
src/main/java/com/qingqiu/interview/dto/ChatDTO.java
Normal file
26
src/main/java/com/qingqiu/interview/dto/ChatDTO.java
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package com.qingqiu.interview.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1></h1>
|
||||||
|
*
|
||||||
|
* @author qingqiu
|
||||||
|
* @date 2025/9/18 12:54
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Accessors(chain = true)
|
||||||
|
public class ChatDTO {
|
||||||
|
|
||||||
|
/** 会话id */
|
||||||
|
private String sessionId;
|
||||||
|
/** 调用模型 */
|
||||||
|
private String aiModel;
|
||||||
|
/** 输入内容 */
|
||||||
|
private String content;
|
||||||
|
/** 0 普通会话 1 面试会话 */
|
||||||
|
private Integer dataType;
|
||||||
|
/** 角色类型:user/assistant/system */
|
||||||
|
private String role;
|
||||||
|
}
|
||||||
0
src/main/java/com/qingqiu/interview/dto/ChatRequest.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/dto/ChatRequest.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/dto/DashboardStatsResponse.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/dto/DashboardStatsResponse.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/dto/InterviewReportResponse.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/dto/InterviewReportResponse.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/dto/InterviewResponse.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/dto/InterviewResponse.java
Normal file → Executable file
8
src/main/java/com/qingqiu/interview/dto/InterviewStartRequest.java
Normal file → Executable file
8
src/main/java/com/qingqiu/interview/dto/InterviewStartRequest.java
Normal file → Executable file
@@ -1,8 +1,11 @@
|
|||||||
package com.qingqiu.interview.dto;
|
package com.qingqiu.interview.dto;
|
||||||
|
|
||||||
|
import com.qingqiu.interview.vo.QuestionAndCategoryTreeListVO;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public class InterviewStartRequest {
|
public class InterviewStartRequest {
|
||||||
|
|
||||||
@@ -10,6 +13,11 @@ public class InterviewStartRequest {
|
|||||||
private String candidateName;
|
private String candidateName;
|
||||||
|
|
||||||
|
|
||||||
|
private List<QuestionAndCategoryTreeListVO> selectedNodes;
|
||||||
|
|
||||||
|
@NotBlank(message = "面试类型不能为空")
|
||||||
|
private String model;
|
||||||
|
|
||||||
|
|
||||||
// 简历文件通过MultipartFile单独传递
|
// 简历文件通过MultipartFile单独传递
|
||||||
}
|
}
|
||||||
|
|||||||
0
src/main/java/com/qingqiu/interview/dto/PageBaseParams.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/dto/PageBaseParams.java
Normal file → Executable file
38
src/main/java/com/qingqiu/interview/dto/QuestionCategoryDTO.java
Executable file
38
src/main/java/com/qingqiu/interview/dto/QuestionCategoryDTO.java
Executable file
@@ -0,0 +1,38 @@
|
|||||||
|
package com.qingqiu.interview.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1></h1>
|
||||||
|
*
|
||||||
|
* @author huangpeng
|
||||||
|
* @date 2025/9/11 09:39
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class QuestionCategoryDTO {
|
||||||
|
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@NotBlank(message = "分类名称不能为空")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@NotNull(message = "父级分类ID不能为空")
|
||||||
|
private Long parentId;
|
||||||
|
|
||||||
|
@NotNull(message = "排序不能为空")
|
||||||
|
private Integer sort;
|
||||||
|
|
||||||
|
@NotNull(message = "状态不能为空")
|
||||||
|
private Integer state;
|
||||||
|
|
||||||
|
private String ancestor;
|
||||||
|
|
||||||
|
private Integer level;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 父分类名称(用于前端显示)
|
||||||
|
*/
|
||||||
|
private String parentName;
|
||||||
|
}
|
||||||
47
src/main/java/com/qingqiu/interview/dto/QuestionCategoryPageParams.java
Executable file
47
src/main/java/com/qingqiu/interview/dto/QuestionCategoryPageParams.java
Executable file
@@ -0,0 +1,47 @@
|
|||||||
|
package com.qingqiu.interview.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1></h1>
|
||||||
|
*
|
||||||
|
* @author huangpeng
|
||||||
|
* @date 2025/9/11 09:40
|
||||||
|
*/
|
||||||
|
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@Data
|
||||||
|
@Accessors(chain = true)
|
||||||
|
public class QuestionCategoryPageParams extends PageBaseParams{
|
||||||
|
/**
|
||||||
|
* 分类名称(模糊查询)
|
||||||
|
*/
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态(0:禁用,1:启用)
|
||||||
|
*/
|
||||||
|
private Integer state;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 父级分类ID
|
||||||
|
*/
|
||||||
|
private Long parentId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 层级
|
||||||
|
*/
|
||||||
|
private Integer level;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否包含子分类
|
||||||
|
*/
|
||||||
|
private Boolean includeChildren = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否只返回启用状态的分类
|
||||||
|
*/
|
||||||
|
private Boolean onlyEnabled = false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.qingqiu.interview.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class QuestionOptionsDTO {
|
||||||
|
|
||||||
|
/** 分类id */
|
||||||
|
private List<Long> categoryIds;
|
||||||
|
/** 难度 */
|
||||||
|
private String difficulty;
|
||||||
|
|
||||||
|
}
|
||||||
4
src/main/java/com/qingqiu/interview/dto/QuestionPageParams.java
Normal file → Executable file
4
src/main/java/com/qingqiu/interview/dto/QuestionPageParams.java
Normal file → Executable file
@@ -8,7 +8,9 @@ import lombok.experimental.Accessors;
|
|||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
@Accessors(chain = true)
|
@Accessors(chain = true)
|
||||||
public class QuestionPageParams extends PageBaseParams{
|
public class QuestionPageParams extends PageBaseParams {
|
||||||
|
|
||||||
private String content;
|
private String content;
|
||||||
|
|
||||||
|
private Long categoryId;
|
||||||
}
|
}
|
||||||
|
|||||||
0
src/main/java/com/qingqiu/interview/dto/QuestionProgressPageParams.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/dto/QuestionProgressPageParams.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/dto/SessionHistoryResponse.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/dto/SessionHistoryResponse.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/dto/SessionRequest.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/dto/SessionRequest.java
Normal file → Executable file
10
src/main/java/com/qingqiu/interview/entity/AiSessionLog.java
Normal file → Executable file
10
src/main/java/com/qingqiu/interview/entity/AiSessionLog.java
Normal file → Executable file
@@ -5,6 +5,7 @@ import lombok.Data;
|
|||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.experimental.Accessors;
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ import java.time.LocalDateTime;
|
|||||||
@TableName("ai_session_log")
|
@TableName("ai_session_log")
|
||||||
public class AiSessionLog implements Serializable {
|
public class AiSessionLog implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
@TableId(value = "id", type = IdType.AUTO)
|
@TableId(value = "id", type = IdType.AUTO)
|
||||||
@@ -32,6 +34,11 @@ public class AiSessionLog implements Serializable {
|
|||||||
*/
|
*/
|
||||||
private String role;
|
private String role;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据类型 0 普通会话 1 面试会话
|
||||||
|
*/
|
||||||
|
private Integer dataType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 输入内容
|
* 输入内容
|
||||||
*/
|
*/
|
||||||
@@ -54,5 +61,8 @@ public class AiSessionLog implements Serializable {
|
|||||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||||
private LocalDateTime updatedTime;
|
private LocalDateTime updatedTime;
|
||||||
|
|
||||||
|
@TableLogic
|
||||||
|
private Integer deleted;
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
0
src/main/java/com/qingqiu/interview/entity/InterviewEvaluation.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/entity/InterviewEvaluation.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/entity/InterviewMessage.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/entity/InterviewMessage.java
Normal file → Executable file
9
src/main/java/com/qingqiu/interview/entity/InterviewQuestionProgress.java
Normal file → Executable file
9
src/main/java/com/qingqiu/interview/entity/InterviewQuestionProgress.java
Normal file → Executable file
@@ -33,6 +33,15 @@ public class InterviewQuestionProgress {
|
|||||||
@TableField("question_content")
|
@TableField("question_content")
|
||||||
private String questionContent;
|
private String questionContent;
|
||||||
|
|
||||||
|
/** 问题序号 */
|
||||||
|
private Integer questionIndex;
|
||||||
|
|
||||||
|
/** 答题耗时(秒) */
|
||||||
|
private Long timeTaken;
|
||||||
|
|
||||||
|
/** 详细评估信息 */
|
||||||
|
private String evaluationDetails;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 面试会话ID
|
* 面试会话ID
|
||||||
*/
|
*/
|
||||||
|
|||||||
8
src/main/java/com/qingqiu/interview/entity/InterviewSession.java
Normal file → Executable file
8
src/main/java/com/qingqiu/interview/entity/InterviewSession.java
Normal file → Executable file
@@ -33,6 +33,14 @@ public class InterviewSession implements Serializable {
|
|||||||
|
|
||||||
@TableField("extracted_skills")
|
@TableField("extracted_skills")
|
||||||
private String extractedSkills;
|
private String extractedSkills;
|
||||||
|
@TableField("interview_type")
|
||||||
|
private String interviewType;
|
||||||
|
|
||||||
|
@TableField("estimated_duration")
|
||||||
|
private Integer estimatedDuration;
|
||||||
|
|
||||||
|
@TableField("current_question_id")
|
||||||
|
private Long currentQuestionId;
|
||||||
|
|
||||||
@TableField("ai_model")
|
@TableField("ai_model")
|
||||||
private String aiModel;
|
private String aiModel;
|
||||||
|
|||||||
7
src/main/java/com/qingqiu/interview/entity/Question.java
Normal file → Executable file
7
src/main/java/com/qingqiu/interview/entity/Question.java
Normal file → Executable file
@@ -19,8 +19,11 @@ public class Question {
|
|||||||
@TableField("content")
|
@TableField("content")
|
||||||
private String content;
|
private String content;
|
||||||
|
|
||||||
@TableField("category")
|
@TableField("category_id")
|
||||||
private String category;
|
private Long categoryId;
|
||||||
|
|
||||||
|
@TableField("category_name")
|
||||||
|
private String categoryName;
|
||||||
|
|
||||||
@TableField("difficulty")
|
@TableField("difficulty")
|
||||||
private String difficulty;
|
private String difficulty;
|
||||||
|
|||||||
58
src/main/java/com/qingqiu/interview/entity/QuestionCategory.java
Normal file → Executable file
58
src/main/java/com/qingqiu/interview/entity/QuestionCategory.java
Normal file → Executable file
@@ -1,14 +1,14 @@
|
|||||||
package com.qingqiu.interview.entity;
|
package com.qingqiu.interview.entity;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.*;
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.io.Serializable;
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.experimental.Accessors;
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>
|
* <p>
|
||||||
* 题型分类
|
* 题型分类
|
||||||
@@ -25,7 +25,7 @@ public class QuestionCategory implements Serializable {
|
|||||||
|
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
@TableId(value = "id", type = IdType.AUTO)
|
@TableId(type = IdType.AUTO)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,16 +33,60 @@ public class QuestionCategory implements Serializable {
|
|||||||
*/
|
*/
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上级id
|
||||||
|
*/
|
||||||
|
private Long parentId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 层级
|
||||||
|
*/
|
||||||
|
private Integer level;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上级序列
|
||||||
|
*/
|
||||||
|
private String ancestor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 排序
|
* 排序
|
||||||
*/
|
*/
|
||||||
private Integer sort;
|
private Integer sort;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态 0 禁用 1 启用
|
||||||
|
*/
|
||||||
|
private Integer state;
|
||||||
|
|
||||||
|
@TableField(fill = FieldFill.INSERT)
|
||||||
private LocalDateTime createdTime;
|
private LocalDateTime createdTime;
|
||||||
|
|
||||||
|
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||||
private LocalDateTime updatedTime;
|
private LocalDateTime updatedTime;
|
||||||
|
@TableLogic
|
||||||
private Integer deleted;
|
private Integer deleted;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 子分类列表(非数据库字段)
|
||||||
|
*/
|
||||||
|
@TableField(exist = false)
|
||||||
|
private List<QuestionCategory> children;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 子分类数量(非数据库字段)
|
||||||
|
*/
|
||||||
|
@TableField(exist = false)
|
||||||
|
private Integer childrenCount;
|
||||||
|
/**
|
||||||
|
* 父分类名称(非数据库字段,用于显示)
|
||||||
|
*/
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String parentName;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
0
src/main/java/com/qingqiu/interview/mapper/AiSessionLogMapper.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/mapper/AiSessionLogMapper.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/mapper/InterviewEvaluationMapper.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/mapper/InterviewEvaluationMapper.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/mapper/InterviewMessageMapper.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/mapper/InterviewMessageMapper.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/mapper/InterviewQuestionProgressMapper.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/mapper/InterviewQuestionProgressMapper.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/mapper/InterviewSessionMapper.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/mapper/InterviewSessionMapper.java
Normal file → Executable file
5
src/main/java/com/qingqiu/interview/mapper/QuestionCategoryMapper.java
Normal file → Executable file
5
src/main/java/com/qingqiu/interview/mapper/QuestionCategoryMapper.java
Normal file → Executable file
@@ -2,6 +2,9 @@ package com.qingqiu.interview.mapper;
|
|||||||
|
|
||||||
import com.qingqiu.interview.entity.QuestionCategory;
|
import com.qingqiu.interview.entity.QuestionCategory;
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>
|
* <p>
|
||||||
@@ -12,5 +15,5 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|||||||
* @since 2025-09-08
|
* @since 2025-09-08
|
||||||
*/
|
*/
|
||||||
public interface QuestionCategoryMapper extends BaseMapper<QuestionCategory> {
|
public interface QuestionCategoryMapper extends BaseMapper<QuestionCategory> {
|
||||||
|
List<QuestionCategory> batchFindByAncestorIdsUnion(@Param("searchIds") List<Long> searchIds);
|
||||||
}
|
}
|
||||||
|
|||||||
8
src/main/java/com/qingqiu/interview/mapper/QuestionMapper.java
Normal file → Executable file
8
src/main/java/com/qingqiu/interview/mapper/QuestionMapper.java
Normal file → Executable file
@@ -1,6 +1,9 @@
|
|||||||
package com.qingqiu.interview.mapper;
|
package com.qingqiu.interview.mapper;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import com.qingqiu.interview.dto.DashboardStatsResponse;
|
||||||
|
import com.qingqiu.interview.dto.QuestionPageParams;
|
||||||
import com.qingqiu.interview.entity.Question;
|
import com.qingqiu.interview.entity.Question;
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
@@ -18,6 +21,9 @@ public interface QuestionMapper extends BaseMapper<Question> {
|
|||||||
|
|
||||||
Question selectByContent(@Param("content") String content);
|
Question selectByContent(@Param("content") String content);
|
||||||
|
|
||||||
List<com.qingqiu.interview.dto.DashboardStatsResponse.CategoryStat> countByCategory();
|
List<DashboardStatsResponse.CategoryStat> countByCategory();
|
||||||
|
|
||||||
|
Page<Question> queryPage(@Param("page") Page<Question> page, @Param("params") QuestionPageParams params);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
29
src/main/java/com/qingqiu/interview/service/ChatService.java
Normal file
29
src/main/java/com/qingqiu/interview/service/ChatService.java
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package com.qingqiu.interview.service;
|
||||||
|
|
||||||
|
import com.qingqiu.interview.dto.ChatDTO;
|
||||||
|
import com.qingqiu.interview.dto.InterviewStartRequest;
|
||||||
|
import com.qingqiu.interview.vo.ChatVO;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1></h1>
|
||||||
|
*
|
||||||
|
* @author qingqiu
|
||||||
|
* @date 2025/9/18 12:45
|
||||||
|
*/
|
||||||
|
public interface ChatService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建普通会话
|
||||||
|
* @return sessionId
|
||||||
|
*/
|
||||||
|
ChatVO createChat(ChatDTO dto);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建面试会话
|
||||||
|
* @param resume 简历
|
||||||
|
* @param request 面试信息
|
||||||
|
* @return sessionId
|
||||||
|
*/
|
||||||
|
String createInterviewChat(MultipartFile resume, InterviewStartRequest request);
|
||||||
|
}
|
||||||
0
src/main/java/com/qingqiu/interview/service/DashboardService.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/service/DashboardService.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/service/IAiSessionLogService.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/service/IAiSessionLogService.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/service/IInterviewQuestionProgressService.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/service/IInterviewQuestionProgressService.java
Normal file → Executable file
61
src/main/java/com/qingqiu/interview/service/IQuestionCategoryService.java
Normal file → Executable file
61
src/main/java/com/qingqiu/interview/service/IQuestionCategoryService.java
Normal file → Executable file
@@ -1,7 +1,12 @@
|
|||||||
package com.qingqiu.interview.service;
|
package com.qingqiu.interview.service;
|
||||||
|
|
||||||
import com.qingqiu.interview.entity.QuestionCategory;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.baomidou.mybatisplus.extension.service.IService;
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
import com.qingqiu.interview.dto.QuestionCategoryDTO;
|
||||||
|
import com.qingqiu.interview.dto.QuestionCategoryPageParams;
|
||||||
|
import com.qingqiu.interview.entity.QuestionCategory;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>
|
* <p>
|
||||||
@@ -12,5 +17,59 @@ import com.baomidou.mybatisplus.extension.service.IService;
|
|||||||
* @since 2025-09-08
|
* @since 2025-09-08
|
||||||
*/
|
*/
|
||||||
public interface IQuestionCategoryService extends IService<QuestionCategory> {
|
public interface IQuestionCategoryService extends IService<QuestionCategory> {
|
||||||
|
/**
|
||||||
|
* 获取分类树列表
|
||||||
|
*/
|
||||||
|
List<QuestionCategory> getTreeList();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分类选项(用于下拉选择)
|
||||||
|
*/
|
||||||
|
List<QuestionCategory> getOptions();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建分类
|
||||||
|
*/
|
||||||
|
Long createCategory(QuestionCategoryDTO dto);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新分类
|
||||||
|
*/
|
||||||
|
void updateCategory(QuestionCategoryDTO dto);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除分类
|
||||||
|
*/
|
||||||
|
void deleteCategory(Long id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新分类状态
|
||||||
|
*/
|
||||||
|
void updateState(Long id, Integer state);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分类详情
|
||||||
|
*/
|
||||||
|
QuestionCategory getCategoryDetail(Long id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询分类
|
||||||
|
*/
|
||||||
|
Page<QuestionCategory> getCategoryPage(QuestionCategoryPageParams query);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据名称搜索分类
|
||||||
|
*/
|
||||||
|
List<QuestionCategory> searchByName(String name);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查分类名称是否重复
|
||||||
|
*/
|
||||||
|
boolean checkNameExists(String name, Long parentId, Long excludeId);
|
||||||
|
|
||||||
|
List<QuestionCategory> batchFindByAncestorIdsUnion(List<Long> searchIds);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.qingqiu.interview.service;
|
||||||
|
|
||||||
|
import com.qingqiu.interview.dto.InterviewStartRequest;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1></h1>
|
||||||
|
*
|
||||||
|
* @author qingqiu
|
||||||
|
* @date 2025/9/18 16:37
|
||||||
|
*/
|
||||||
|
public interface InterviewChatService {
|
||||||
|
|
||||||
|
void startInterview(MultipartFile resume, InterviewStartRequest request) throws IOException;
|
||||||
|
}
|
||||||
87
src/main/java/com/qingqiu/interview/service/InterviewService.java
Normal file → Executable file
87
src/main/java/com/qingqiu/interview/service/InterviewService.java
Normal file → Executable file
@@ -63,6 +63,12 @@ public class InterviewService {
|
|||||||
|
|
||||||
// 1. 解析简历
|
// 1. 解析简历
|
||||||
String resumeContent = parseResume(resume);
|
String resumeContent = parseResume(resume);
|
||||||
|
// 判断是否AI出题
|
||||||
|
if (request.getModel().equals("local")) {
|
||||||
|
if (CollectionUtil.isEmpty(request.getSelectedNodes())) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// 2. 创建会话 并发送AI请求 让其从题库中智能抽题
|
// 2. 创建会话 并发送AI请求 让其从题库中智能抽题
|
||||||
@@ -212,40 +218,8 @@ public class InterviewService {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 导入题库(使用AI自动分类)
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取会话历史
|
|
||||||
*/
|
|
||||||
public SessionHistoryResponse getSessionHistory(String sessionId) {
|
|
||||||
InterviewSession session = sessionMapper.selectBySessionId(sessionId);
|
|
||||||
if (session == null) {
|
|
||||||
throw new IllegalArgumentException("会话不存在: " + sessionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<InterviewMessage> messages = messageMapper.selectBySessionIdOrderByOrder(sessionId);
|
|
||||||
List<SessionHistoryResponse.MessageDto> messageDtos = messages.stream()
|
|
||||||
.map(msg -> new SessionHistoryResponse.MessageDto()
|
|
||||||
.setMessageType(msg.getMessageType())
|
|
||||||
.setSender(msg.getSender())
|
|
||||||
.setContent(msg.getContent())
|
|
||||||
.setMessageOrder(msg.getMessageOrder())
|
|
||||||
.setCreatedTime(msg.getCreatedTime()))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
return new SessionHistoryResponse()
|
|
||||||
.setSessionId(sessionId)
|
|
||||||
.setCandidateName(session.getCandidateName())
|
|
||||||
.setAiModel(session.getAiModel())
|
|
||||||
.setStatus(session.getStatus())
|
|
||||||
.setTotalQuestions(session.getTotalQuestions())
|
|
||||||
.setCurrentQuestionIndex(session.getCurrentQuestionIndex())
|
|
||||||
.setCreatedTime(session.getCreatedTime())
|
|
||||||
.setMessages(messageDtos);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String parseResume(MultipartFile resume) throws IOException {
|
private String parseResume(MultipartFile resume) throws IOException {
|
||||||
String fileExtension = getFileExtension(resume.getOriginalFilename());
|
String fileExtension = getFileExtension(resume.getOriginalFilename());
|
||||||
@@ -539,55 +513,6 @@ public class InterviewService {
|
|||||||
""", session.getResumeContent(), historyBuilder.toString());
|
""", session.getResumeContent(), historyBuilder.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
private InterviewResponse generateNextQuestion(InterviewSession session) {
|
|
||||||
try {
|
|
||||||
// 1. 解析出AI选择的题目ID列表
|
|
||||||
List<Long> selectedQuestionIds = objectMapper.readValue(session.getSelectedQuestionIds(), new com.fasterxml.jackson.core.type.TypeReference<List<Long>>() {
|
|
||||||
});
|
|
||||||
|
|
||||||
// 2. 获取下一个问题的索引
|
|
||||||
int nextQuestionIndex = session.getCurrentQuestionIndex(); // 数据库中存的是已回答问题的数量
|
|
||||||
if (nextQuestionIndex >= selectedQuestionIds.size()) {
|
|
||||||
return finishInterview(session); // 如果没有更多问题,则结束面试
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 获取下一个问题的ID并从数据库查询
|
|
||||||
Long nextQuestionId = selectedQuestionIds.get(nextQuestionIndex);
|
|
||||||
Question nextQuestion = questionMapper.selectById(nextQuestionId);
|
|
||||||
if (nextQuestion == null) {
|
|
||||||
log.error("无法找到ID为 {} 的问题,跳过此问题。", nextQuestionId);
|
|
||||||
// 更新会话状态并尝试下一个问题
|
|
||||||
session.setCurrentQuestionIndex(nextQuestionIndex + 1);
|
|
||||||
sessionMapper.updateById(session);
|
|
||||||
return generateNextQuestion(session); // 递归调用以获取再下一个问题
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 更新会话状态(当前问题索引+1)
|
|
||||||
session.setCurrentQuestionIndex(nextQuestionIndex + 1);
|
|
||||||
sessionMapper.updateById(session);
|
|
||||||
|
|
||||||
// 5. 生成并保存AI的提问消息
|
|
||||||
String questionContent = String.format("好的,下一个问题是:%s", nextQuestion.getContent());
|
|
||||||
int messageOrder = messageMapper.selectMaxOrderBySessionId(session.getSessionId()) + 1;
|
|
||||||
saveMessage(session.getSessionId(), InterviewMessage.MessageType.QUESTION.name(),
|
|
||||||
InterviewMessage.Sender.AI.name(), questionContent, nextQuestion.getId(), messageOrder);
|
|
||||||
|
|
||||||
// 6. 返回响应
|
|
||||||
return new InterviewResponse()
|
|
||||||
.setSessionId(session.getSessionId())
|
|
||||||
.setMessage(questionContent)
|
|
||||||
.setMessageType(InterviewMessage.MessageType.QUESTION.name())
|
|
||||||
.setSender(InterviewMessage.Sender.AI.name())
|
|
||||||
.setCurrentQuestionIndex(session.getCurrentQuestionIndex())
|
|
||||||
.setTotalQuestions(session.getTotalQuestions())
|
|
||||||
.setStatus(InterviewSession.Status.ACTIVE.name());
|
|
||||||
|
|
||||||
} catch (JsonProcessingException e) {
|
|
||||||
log.error("解析会话中的题目ID列表失败", e);
|
|
||||||
return finishInterview(session); // 解析失败则直接结束面试
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取所有面试会话列表
|
* 获取所有面试会话列表
|
||||||
|
|||||||
6
src/main/java/com/qingqiu/interview/service/QuestionClassificationService.java
Normal file → Executable file
6
src/main/java/com/qingqiu/interview/service/QuestionClassificationService.java
Normal file → Executable file
@@ -82,7 +82,7 @@ public class QuestionClassificationService {
|
|||||||
for (JsonNode questionNode : questionsNode) {
|
for (JsonNode questionNode : questionsNode) {
|
||||||
Question question = new Question()
|
Question question = new Question()
|
||||||
.setContent(getTextValue(questionNode, "content"))
|
.setContent(getTextValue(questionNode, "content"))
|
||||||
.setCategory(getTextValue(questionNode, "category"))
|
.setCategoryName(getTextValue(questionNode, "category"))
|
||||||
.setDifficulty(getTextValue(questionNode, "difficulty"))
|
.setDifficulty(getTextValue(questionNode, "difficulty"))
|
||||||
.setTags(getTextValue(questionNode, "tags"));
|
.setTags(getTextValue(questionNode, "tags"));
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ public class QuestionClassificationService {
|
|||||||
|
|
||||||
private boolean isValidQuestion(Question question) {
|
private boolean isValidQuestion(Question question) {
|
||||||
return question.getContent() != null && !question.getContent().trim().isEmpty()
|
return question.getContent() != null && !question.getContent().trim().isEmpty()
|
||||||
&& question.getCategory() != null && !question.getCategory().trim().isEmpty();
|
&& question.getCategoryName() != null && !question.getCategoryName().trim().isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Question> fallbackParsing(String content) {
|
private List<Question> fallbackParsing(String content) {
|
||||||
@@ -126,7 +126,7 @@ public class QuestionClassificationService {
|
|||||||
if (!line.isEmpty() && line.length() > 10) { // 过滤太短的内容
|
if (!line.isEmpty() && line.length() > 10) { // 过滤太短的内容
|
||||||
Question question = new Question()
|
Question question = new Question()
|
||||||
.setContent(line)
|
.setContent(line)
|
||||||
.setCategory("未分类")
|
.setCategoryName("未分类")
|
||||||
.setDifficulty("Medium")
|
.setDifficulty("Medium")
|
||||||
.setTags("待分类");
|
.setTags("待分类");
|
||||||
questions.add(question);
|
questions.add(question);
|
||||||
|
|||||||
171
src/main/java/com/qingqiu/interview/service/QuestionService.java
Normal file → Executable file
171
src/main/java/com/qingqiu/interview/service/QuestionService.java
Normal file → Executable file
@@ -1,177 +1,30 @@
|
|||||||
package com.qingqiu.interview.service;
|
package com.qingqiu.interview.service;
|
||||||
|
|
||||||
import cn.hutool.core.collection.CollectionUtil;
|
|
||||||
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.plugins.pagination.Page;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
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.mapper.QuestionMapper;
|
import com.qingqiu.interview.entity.QuestionCategory;
|
||||||
import com.qingqiu.interview.service.llm.LlmService;
|
import com.qingqiu.interview.vo.QuestionAndCategoryTreeListVO;
|
||||||
import com.qingqiu.interview.service.parser.DocumentParser;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Slf4j
|
public interface QuestionService extends IService<Question> {
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class QuestionService {
|
|
||||||
|
|
||||||
private final QuestionMapper questionMapper;
|
Page<Question> getQuestionPage(QuestionPageParams params);
|
||||||
private final QuestionClassificationService classificationService;
|
|
||||||
private final List<DocumentParser> documentParserList; // This will be injected by Spring
|
|
||||||
private final LlmService llmService;
|
|
||||||
|
|
||||||
/**
|
void addQuestion(Question question);
|
||||||
* 分页查询题库
|
|
||||||
*/
|
|
||||||
public Page<Question> getQuestionPage(QuestionPageParams params) {
|
|
||||||
log.info("分页查询题库,当前页: {}, 每页数量: {}", params.getCurrent(), params.getSize());
|
|
||||||
return questionMapper.selectPage(
|
|
||||||
Page.of(params.getCurrent(), params.getSize()),
|
|
||||||
new LambdaQueryWrapper<Question>()
|
|
||||||
.like(StringUtils.isNotBlank(params.getContent()), Question::getContent, params.getContent())
|
|
||||||
.orderByDesc(Question::getCreatedTime)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
void updateQuestion(Question question);
|
||||||
* 新增题目,并进行重复校验
|
|
||||||
*/
|
|
||||||
public void addQuestion(Question question) {
|
|
||||||
validateQuestion(question.getContent(), null);
|
|
||||||
log.info("新增题目: {}", question.getContent());
|
|
||||||
questionMapper.insert(question);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
void deleteQuestion(Long id);
|
||||||
* 更新题目,并进行重复校验
|
|
||||||
*/
|
|
||||||
public void updateQuestion(Question question) {
|
|
||||||
validateQuestion(question.getContent(), question.getId());
|
|
||||||
log.info("更新题目ID: {}", question.getId());
|
|
||||||
questionMapper.updateById(question);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
void importQuestionsFromFile(MultipartFile file) throws IOException;
|
||||||
* 删除题目
|
|
||||||
*/
|
|
||||||
public void deleteQuestion(Long id) {
|
|
||||||
log.info("删除题目ID: {}", id);
|
|
||||||
questionMapper.deleteById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
void useAiCheckQuestionData();
|
||||||
* AI批量导入题库,并进行去重
|
|
||||||
*/
|
|
||||||
public void importQuestionsFromFile(MultipartFile file) throws IOException {
|
|
||||||
log.info("开始从文件导入题库: {}", file.getOriginalFilename());
|
|
||||||
String fileExtension = getFileExtension(file.getOriginalFilename());
|
|
||||||
DocumentParser parser = documentParserList.stream()
|
|
||||||
.filter(p -> p.getSupportedType().equals(fileExtension))
|
|
||||||
.findFirst()
|
|
||||||
.orElseThrow(() -> new IllegalArgumentException("不支持的文件类型: " + fileExtension));
|
|
||||||
|
|
||||||
String content = parser.parse(file.getInputStream());
|
List<QuestionAndCategoryTreeListVO> getTreeListCategory(QuestionOptionsDTO dto);
|
||||||
List<Question> questionsFromAi = classificationService.classifyQuestions(content);
|
|
||||||
|
|
||||||
int newQuestionsCount = 0;
|
|
||||||
for (Question question : questionsFromAi) {
|
|
||||||
try {
|
|
||||||
validateQuestion(question.getContent(), null);
|
|
||||||
questionMapper.insert(question);
|
|
||||||
newQuestionsCount++;
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
log.warn("跳过重复题目: {}", question.getContent());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.info("成功导入 {} 个新题目,跳过 {} 个重复题目。", newQuestionsCount, questionsFromAi.size() - newQuestionsCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 调用AI检查题库中的数据是否重复
|
|
||||||
*/
|
|
||||||
@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);
|
|
||||||
// 调用AI
|
|
||||||
log.info("AI返回内容: {}", chat);
|
|
||||||
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)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
private static String getPrompt(List<Question> questions) {
|
|
||||||
JSONArray jsonArray = new JSONArray();
|
|
||||||
for (Question question : questions) {
|
|
||||||
JSONObject jsonObject = new JSONObject();
|
|
||||||
jsonObject.put("id", question.getId());
|
|
||||||
jsonObject.put("content", question.getContent());
|
|
||||||
jsonArray.add(jsonObject);
|
|
||||||
}
|
|
||||||
JSONObject jsonObject = new JSONObject();
|
|
||||||
jsonObject.put("data", jsonArray);
|
|
||||||
return String.format("""
|
|
||||||
请对以下数据进行重复校验,如果题目内容相似,请只保留1条数据,并返回对应数据的id。请严格按照以下JSON格式返回结果:
|
|
||||||
|
|
||||||
{
|
|
||||||
"questions": [1, 2, 3, .....]
|
|
||||||
}
|
|
||||||
|
|
||||||
分类规则:
|
|
||||||
1. 只返回JSON,不要其他解释文字
|
|
||||||
2. 请严格按照API接口形式返回,不要返回任何额外的文字内容,包括'```json```'!!!!
|
|
||||||
3. 请严格按照网络接口的形式返回JSON数据!!!
|
|
||||||
数据如下:
|
|
||||||
%s
|
|
||||||
""", jsonObject.toJSONString());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 校验题目内容是否重复
|
|
||||||
*
|
|
||||||
* @param content 题目内容
|
|
||||||
* @param currentId 当前题目ID,更新时传入,用于排除自身
|
|
||||||
*/
|
|
||||||
private void validateQuestion(String content, Long currentId) {
|
|
||||||
Question existingQuestion = questionMapper.selectByContent(content);
|
|
||||||
if (existingQuestion != null && (currentId == null || !existingQuestion.getId().equals(currentId))) {
|
|
||||||
throw new IllegalArgumentException("题目内容已存在,请勿重复添加。");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getFileExtension(String fileName) {
|
|
||||||
if (fileName == null || fileName.lastIndexOf('.') == -1) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
0
src/main/java/com/qingqiu/interview/service/impl/AiSessionLogServiceImpl.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/service/impl/AiSessionLogServiceImpl.java
Normal file → Executable file
@@ -0,0 +1,96 @@
|
|||||||
|
package com.qingqiu.interview.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollectionUtil;
|
||||||
|
import com.alibaba.dashscope.common.Message;
|
||||||
|
import com.alibaba.dashscope.common.Role;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.qingqiu.interview.common.enums.LLMProvider;
|
||||||
|
import com.qingqiu.interview.ai.factory.AIClientManager;
|
||||||
|
import com.qingqiu.interview.common.utils.AIUtils;
|
||||||
|
import com.qingqiu.interview.dto.ChatDTO;
|
||||||
|
import com.qingqiu.interview.dto.InterviewStartRequest;
|
||||||
|
import com.qingqiu.interview.entity.AiSessionLog;
|
||||||
|
import com.qingqiu.interview.service.ChatService;
|
||||||
|
import com.qingqiu.interview.service.IAiSessionLogService;
|
||||||
|
import com.qingqiu.interview.vo.ChatVO;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import static com.qingqiu.interview.common.constants.CommonConstant.DEFAULT_TRUNCATE_RATIO;
|
||||||
|
import static com.qingqiu.interview.common.constants.CommonConstant.MAX_TOKEN;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1></h1>
|
||||||
|
*
|
||||||
|
* @author qingqiu
|
||||||
|
* @date 2025/9/18 12:56
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ChatServiceImpl implements ChatService {
|
||||||
|
|
||||||
|
private final AIClientManager aiClientManager;
|
||||||
|
|
||||||
|
private IAiSessionLogService aiSessionLogService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChatVO createChat(ChatDTO dto) {
|
||||||
|
LLMProvider llmProvider = LLMProvider.fromCode(dto.getAiModel());
|
||||||
|
List<Message> messages = new ArrayList<>();
|
||||||
|
AtomicInteger tokens = new AtomicInteger();
|
||||||
|
// 如果会话id不为空 则从数据库中获取会话记录
|
||||||
|
if (dto.getSessionId() != null) {
|
||||||
|
List<AiSessionLog> list = aiSessionLogService.list(
|
||||||
|
new LambdaQueryWrapper<AiSessionLog>()
|
||||||
|
.eq(AiSessionLog::getToken, dto.getSessionId())
|
||||||
|
.eq(AiSessionLog::getDataType, dto.getDataType())
|
||||||
|
.orderByAsc(AiSessionLog::getCreatedTime)
|
||||||
|
);
|
||||||
|
if (CollectionUtil.isNotEmpty(list)) {
|
||||||
|
messages = list.stream().map(data -> {
|
||||||
|
tokens.getAndAdd(AIUtils.getPromptTokens(data.getContent()));
|
||||||
|
return AIUtils.createMessage(data.getRole(), data.getContent());
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
if (CollectionUtil.isEmpty( messages)) {
|
||||||
|
messages = new ArrayList<>();
|
||||||
|
}
|
||||||
|
messages.add(AIUtils.createMessage(dto.getRole(), dto.getContent()));
|
||||||
|
List<Message> finalMessage = new ArrayList<>();
|
||||||
|
// 剪切 10%的消息
|
||||||
|
if (tokens.get() > MAX_TOKEN) {
|
||||||
|
BigDecimal size = new BigDecimal(String.valueOf(messages.size()));
|
||||||
|
size = size.multiply(DEFAULT_TRUNCATE_RATIO).setScale(0, RoundingMode.HALF_UP);
|
||||||
|
for (int i = size.intValue(); i < messages.size(); i++) {
|
||||||
|
finalMessage.add(messages.get(i));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
finalMessage = messages;
|
||||||
|
}
|
||||||
|
String res = aiClientManager.getClient(llmProvider).chatCompletion(finalMessage);
|
||||||
|
|
||||||
|
|
||||||
|
return ChatVO.builder()
|
||||||
|
.role(Role.ASSISTANT.getValue())
|
||||||
|
.sessionId(dto.getSessionId())
|
||||||
|
.content(res)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String createInterviewChat(MultipartFile resume, InterviewStartRequest request) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package com.qingqiu.interview.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.io.file.FileNameUtil;
|
||||||
|
import com.qingqiu.interview.common.constants.AIStrategyConstant;
|
||||||
|
import com.qingqiu.interview.common.enums.DocumentParserProvider;
|
||||||
|
import com.qingqiu.interview.dto.InterviewStartRequest;
|
||||||
|
import com.qingqiu.interview.service.InterviewChatService;
|
||||||
|
import com.qingqiu.interview.service.parser.DocumentParser;
|
||||||
|
import com.qingqiu.interview.service.parser.DocumentParserManager;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1></h1>
|
||||||
|
*
|
||||||
|
* @author qingqiu
|
||||||
|
* @date 2025/9/18 16:38
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor(onConstructor_ = {@Autowired, @Lazy})
|
||||||
|
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);
|
||||||
|
// 判断是否使用本地题库
|
||||||
|
if (request.getModel().equals("local")) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String parseResume(MultipartFile resume) throws IOException {
|
||||||
|
// 获取文件扩展名
|
||||||
|
String extName = FileNameUtil.extName(resume.getOriginalFilename());
|
||||||
|
// 1. 获取简历解析器
|
||||||
|
DocumentParser parser = documentParserManager.getParser(DocumentParserProvider.fromCode(extName));
|
||||||
|
// 2. 解析简历
|
||||||
|
return parser.parse(resume.getInputStream());
|
||||||
|
}
|
||||||
|
}
|
||||||
0
src/main/java/com/qingqiu/interview/service/impl/InterviewQuestionProgressServiceImpl.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/service/impl/InterviewQuestionProgressServiceImpl.java
Normal file → Executable file
303
src/main/java/com/qingqiu/interview/service/impl/QuestionCategoryServiceImpl.java
Normal file → Executable file
303
src/main/java/com/qingqiu/interview/service/impl/QuestionCategoryServiceImpl.java
Normal file → Executable file
@@ -1,10 +1,30 @@
|
|||||||
package com.qingqiu.interview.service.impl;
|
package com.qingqiu.interview.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollectionUtil;
|
||||||
|
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.constants.CommonConstant;
|
||||||
|
import com.qingqiu.interview.common.enums.CommonStateEnum;
|
||||||
|
import com.qingqiu.interview.dto.QuestionCategoryDTO;
|
||||||
|
import com.qingqiu.interview.dto.QuestionCategoryPageParams;
|
||||||
|
import com.qingqiu.interview.entity.Question;
|
||||||
import com.qingqiu.interview.entity.QuestionCategory;
|
import com.qingqiu.interview.entity.QuestionCategory;
|
||||||
import com.qingqiu.interview.mapper.QuestionCategoryMapper;
|
import com.qingqiu.interview.mapper.QuestionCategoryMapper;
|
||||||
import com.qingqiu.interview.service.IQuestionCategoryService;
|
import com.qingqiu.interview.service.IQuestionCategoryService;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.qingqiu.interview.service.QuestionService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.BeanUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>
|
* <p>
|
||||||
@@ -14,7 +34,288 @@ import org.springframework.stereotype.Service;
|
|||||||
* @author huangpeng
|
* @author huangpeng
|
||||||
* @since 2025-09-08
|
* @since 2025-09-08
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
|
@RequiredArgsConstructor(onConstructor_ = {@Autowired, @Lazy})
|
||||||
public class QuestionCategoryServiceImpl extends ServiceImpl<QuestionCategoryMapper, QuestionCategory> implements IQuestionCategoryService {
|
public class QuestionCategoryServiceImpl extends ServiceImpl<QuestionCategoryMapper, QuestionCategory> implements IQuestionCategoryService {
|
||||||
|
|
||||||
|
private final QuestionService questionService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<QuestionCategory> getTreeList() {
|
||||||
|
List<QuestionCategory> allCategories = getAllValidCategories();
|
||||||
|
return buildCategoryTree(allCategories);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<QuestionCategory> getOptions() {
|
||||||
|
List<QuestionCategory> allCategories = getAllValidCategories();
|
||||||
|
|
||||||
|
return allCategories.stream()
|
||||||
|
.filter(category -> CommonStateEnum.ENABLED.getCode().equals(category.getState()))
|
||||||
|
.sorted(Comparator.comparingInt(QuestionCategory::getSort))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public Long createCategory(QuestionCategoryDTO dto) {
|
||||||
|
// 检查名称是否重复
|
||||||
|
if (checkNameExists(dto.getName(), dto.getParentId(), null)) {
|
||||||
|
throw new RuntimeException("同一层级下分类名称不能重复");
|
||||||
|
}
|
||||||
|
|
||||||
|
validateParentCategory(dto.getParentId());
|
||||||
|
|
||||||
|
QuestionCategory category = new QuestionCategory();
|
||||||
|
BeanUtils.copyProperties(dto, category);
|
||||||
|
|
||||||
|
calculateLevelAndPath(category, dto.getParentId());
|
||||||
|
|
||||||
|
// 保存分类
|
||||||
|
save(category);
|
||||||
|
|
||||||
|
// 更新路径(需要ID)
|
||||||
|
updateCategoryPathAfterSave(category, dto.getParentId());
|
||||||
|
|
||||||
|
log.info("创建分类成功:{}", category);
|
||||||
|
return category.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void updateCategory(QuestionCategoryDTO dto) {
|
||||||
|
QuestionCategory category = getById(dto.getId());
|
||||||
|
if (category == null) {
|
||||||
|
throw new RuntimeException("分类不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查名称是否重复(排除自身)
|
||||||
|
if (checkNameExists(dto.getName(), category.getParentId(), dto.getId())) {
|
||||||
|
throw new RuntimeException("同一层级下分类名称不能重复");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否修改了父分类
|
||||||
|
// if (!category.getParentId().equals(dto.getParentId())) {
|
||||||
|
// throw new RuntimeException("不支持直接修改父分类,请使用移动分类功能");
|
||||||
|
// }
|
||||||
|
|
||||||
|
BeanUtils.copyProperties(dto, category);
|
||||||
|
category.setUpdatedTime(LocalDateTime.now());
|
||||||
|
updateById(category);
|
||||||
|
|
||||||
|
log.info("更新分类成功:{}", category);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void deleteCategory(Long id) {
|
||||||
|
// 1. 查找所有需要删除的分类ID(包括子分类)
|
||||||
|
List<Long> categoryIdsToDelete = getAllCategoryIdsToDelete(id);
|
||||||
|
|
||||||
|
if (CollectionUtil.isEmpty(categoryIdsToDelete)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 删除所有相关分类
|
||||||
|
this.removeByIds(categoryIdsToDelete);
|
||||||
|
|
||||||
|
// 3. 删除关联的题目数据
|
||||||
|
questionService.remove(
|
||||||
|
new LambdaQueryWrapper<Question>()
|
||||||
|
.in(Question::getCategoryId, categoryIdsToDelete)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void updateState(Long id, Integer state) {
|
||||||
|
QuestionCategory category = new QuestionCategory();
|
||||||
|
category.setId(id);
|
||||||
|
category.setState(state);
|
||||||
|
category.setUpdatedTime(LocalDateTime.now());
|
||||||
|
updateById(category);
|
||||||
|
|
||||||
|
log.info("更新分类状态成功:id={}, state={}", id, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public QuestionCategory getCategoryDetail(Long id) {
|
||||||
|
QuestionCategory category = getById(id);
|
||||||
|
if (category != null && !CommonConstant.ONE.equals(category.getDeleted())) {
|
||||||
|
// 设置父分类名称
|
||||||
|
if (!CommonConstant.ROOT_PARENT_ID.equals(category.getParentId())) {
|
||||||
|
QuestionCategory parent = getById(category.getParentId());
|
||||||
|
if (parent != null) {
|
||||||
|
category.setParentName(parent.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return category;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Page<QuestionCategory> getCategoryPage(QuestionCategoryPageParams query) {
|
||||||
|
return page(
|
||||||
|
Page.of(query.getCurrent(), query.getSize()),
|
||||||
|
new LambdaQueryWrapper<QuestionCategory>()
|
||||||
|
.like(StringUtils.hasText(query.getName()), QuestionCategory::getName, query.getName())
|
||||||
|
.eq(QuestionCategory::getState, query.getState())
|
||||||
|
.or(Objects.nonNull(query.getParentId()), wrapper -> {
|
||||||
|
wrapper.eq(QuestionCategory::getParentId, query.getParentId())
|
||||||
|
.or()
|
||||||
|
.apply("find_in_set({0}, ancestor)", query.getParentId())
|
||||||
|
;
|
||||||
|
})
|
||||||
|
.orderByDesc(QuestionCategory::getSort)
|
||||||
|
.orderByDesc(QuestionCategory::getCreatedTime)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<QuestionCategory> searchByName(String name) {
|
||||||
|
if (!StringUtils.hasText(name)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<QuestionCategory> allCategories = getAllValidCategories();
|
||||||
|
|
||||||
|
return allCategories.stream()
|
||||||
|
.filter(category -> category.getName().toLowerCase().contains(name.toLowerCase()))
|
||||||
|
.sorted(Comparator.comparingInt(QuestionCategory::getSort))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean checkNameExists(String name, Long parentId, Long excludeId) {
|
||||||
|
List<QuestionCategory> allCategories = getAllValidCategories();
|
||||||
|
|
||||||
|
return allCategories.stream()
|
||||||
|
.filter(category -> category.getName().equals(name))
|
||||||
|
.filter(category -> parentId.equals(category.getParentId()))
|
||||||
|
.anyMatch(category -> excludeId == null || !excludeId.equals(category.getId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<QuestionCategory> batchFindByAncestorIdsUnion(List<Long> searchIds) {
|
||||||
|
return baseMapper.batchFindByAncestorIdsUnion(searchIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============ 私有方法 ============
|
||||||
|
|
||||||
|
private List<QuestionCategory> getAllValidCategories() {
|
||||||
|
// LambdaQueryWrapper<QuestionCategory> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
// wrapper.eq(QuestionCategory::getDeleted, CommonConstant.ZERO)
|
||||||
|
// .orderByAsc(QuestionCategory::getSort);
|
||||||
|
return list(
|
||||||
|
new LambdaQueryWrapper<QuestionCategory>()
|
||||||
|
.orderByDesc(QuestionCategory::getSort)
|
||||||
|
.orderByDesc(QuestionCategory::getCreatedTime)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<QuestionCategory> buildCategoryTree(List<QuestionCategory> categories) {
|
||||||
|
if (CollectionUtil.isEmpty(categories)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按父ID分组
|
||||||
|
Map<Long, List<QuestionCategory>> parentIdMap = categories.stream()
|
||||||
|
.collect(Collectors.groupingBy(QuestionCategory::getParentId));
|
||||||
|
|
||||||
|
// 设置子节点并计算子节点数量
|
||||||
|
categories.forEach(category -> {
|
||||||
|
List<QuestionCategory> children = parentIdMap.get(category.getId());
|
||||||
|
if (!CollectionUtil.isEmpty(children)) {
|
||||||
|
category.setChildren(children);
|
||||||
|
category.setChildrenCount(children.size());
|
||||||
|
children.sort(Comparator.comparingInt(QuestionCategory::getSort));
|
||||||
|
} else {
|
||||||
|
category.setChildrenCount(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 返回根节点
|
||||||
|
return parentIdMap.getOrDefault(CommonConstant.ROOT_PARENT_ID, Collections.emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateParentCategory(Long parentId) {
|
||||||
|
if (!CommonConstant.ROOT_PARENT_ID.equals(parentId)) {
|
||||||
|
QuestionCategory parentCategory = getById(parentId);
|
||||||
|
if (parentCategory == null || CommonConstant.ONE.equals(parentCategory.getDeleted())) {
|
||||||
|
throw new RuntimeException("父分类不存在或已被删除");
|
||||||
|
}
|
||||||
|
if (CommonStateEnum.DISABLED.getCode().equals(parentCategory.getState())) {
|
||||||
|
throw new RuntimeException("父分类已被禁用,无法创建子分类");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void calculateLevelAndPath(QuestionCategory category, Long parentId) {
|
||||||
|
if (CommonConstant.ROOT_PARENT_ID.equals(parentId)) {
|
||||||
|
category.setLevel(1);
|
||||||
|
} else {
|
||||||
|
QuestionCategory parentCategory = getById(parentId);
|
||||||
|
category.setLevel(parentCategory.getLevel() + 1);
|
||||||
|
|
||||||
|
if (category.getLevel() > 5) {
|
||||||
|
throw new RuntimeException("分类层级过深,最多支持5级分类");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void updateCategoryPathAfterSave(QuestionCategory category, Long parentId) {
|
||||||
|
if (!CommonConstant.ROOT_PARENT_ID.equals(parentId)) {
|
||||||
|
QuestionCategory parentCategory = getById(parentId);
|
||||||
|
String newPath = parentCategory.getAncestor() + "," + category.getId();
|
||||||
|
category.setAncestor(newPath);
|
||||||
|
updateById(category);
|
||||||
|
} else {
|
||||||
|
category.setAncestor(String.valueOf(category.getId()));
|
||||||
|
updateById(category);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private List<QuestionCategory> findDescendants(List<QuestionCategory> allCategories, Long parentId) {
|
||||||
|
List<QuestionCategory> descendants = new ArrayList<>();
|
||||||
|
|
||||||
|
allCategories.stream()
|
||||||
|
.filter(category -> parentId.equals(category.getParentId()))
|
||||||
|
.forEach(category -> {
|
||||||
|
descendants.add(category);
|
||||||
|
descendants.addAll(findDescendants(allCategories, category.getId()));
|
||||||
|
});
|
||||||
|
|
||||||
|
return descendants;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 递归获取所有需要删除的分类ID(包括子分类)
|
||||||
|
*
|
||||||
|
* @param parentId 父分类ID
|
||||||
|
* @return 所有需要删除的分类ID列表
|
||||||
|
*/
|
||||||
|
private List<Long> getAllCategoryIdsToDelete(Long parentId) {
|
||||||
|
List<Long> result = new ArrayList<>();
|
||||||
|
result.add(parentId);
|
||||||
|
|
||||||
|
// 查找直接子分类
|
||||||
|
List<QuestionCategory> children = this.list(
|
||||||
|
new LambdaQueryWrapper<QuestionCategory>()
|
||||||
|
.eq(QuestionCategory::getParentId, parentId)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 递归查找所有子分类
|
||||||
|
for (QuestionCategory child : children) {
|
||||||
|
result.addAll(getAllCategoryIdsToDelete(child.getId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,382 @@
|
|||||||
|
package com.qingqiu.interview.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollectionUtil;
|
||||||
|
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.dto.QuestionOptionsDTO;
|
||||||
|
import com.qingqiu.interview.dto.QuestionPageParams;
|
||||||
|
import com.qingqiu.interview.entity.Question;
|
||||||
|
import com.qingqiu.interview.entity.QuestionCategory;
|
||||||
|
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.llm.LlmService;
|
||||||
|
import com.qingqiu.interview.service.parser.DocumentParser;
|
||||||
|
import com.qingqiu.interview.vo.QuestionAndCategoryTreeListVO;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor(onConstructor_ = {@Autowired, @Lazy})
|
||||||
|
public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> implements QuestionService {
|
||||||
|
private final QuestionMapper questionMapper;
|
||||||
|
private final QuestionClassificationService classificationService;
|
||||||
|
private final List<DocumentParser> documentParserList; // This will be injected by Spring
|
||||||
|
private final LlmService llmService;
|
||||||
|
private final IQuestionCategoryService questionCategoryService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询题库
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Page<Question> getQuestionPage(QuestionPageParams params) {
|
||||||
|
log.info("分页查询题库,当前页: {}, 每页数量: {}", params.getCurrent(), params.getSize());
|
||||||
|
return questionMapper.queryPage(
|
||||||
|
Page.of(params.getCurrent(), params.getSize()),
|
||||||
|
params
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增题目,并进行重复校验
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void addQuestion(Question question) {
|
||||||
|
validateQuestion(question.getContent(), null);
|
||||||
|
log.info("新增题目: {}", question.getContent());
|
||||||
|
questionMapper.insert(question);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新题目,并进行重复校验
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void updateQuestion(Question question) {
|
||||||
|
validateQuestion(question.getContent(), question.getId());
|
||||||
|
log.info("更新题目ID: {}", question.getId());
|
||||||
|
questionMapper.updateById(question);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除题目
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void deleteQuestion(Long id) {
|
||||||
|
log.info("删除题目ID: {}", id);
|
||||||
|
questionMapper.deleteById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI批量导入题库,并进行去重
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void importQuestionsFromFile(MultipartFile file) throws IOException {
|
||||||
|
log.info("开始从文件导入题库: {}", file.getOriginalFilename());
|
||||||
|
String fileExtension = getFileExtension(file.getOriginalFilename());
|
||||||
|
DocumentParser parser = documentParserList.stream()
|
||||||
|
.filter(p -> p.getSupportedType().equals(fileExtension))
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("不支持的文件类型: " + fileExtension));
|
||||||
|
|
||||||
|
String content = parser.parse(file.getInputStream());
|
||||||
|
List<Question> questionsFromAi = classificationService.classifyQuestions(content);
|
||||||
|
|
||||||
|
int newQuestionsCount = 0;
|
||||||
|
for (Question question : questionsFromAi) {
|
||||||
|
try {
|
||||||
|
validateQuestion(question.getContent(), null);
|
||||||
|
questionMapper.insert(question);
|
||||||
|
newQuestionsCount++;
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
log.warn("跳过重复题目: {}", question.getContent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.info("成功导入 {} 个新题目,跳过 {} 个重复题目。", newQuestionsCount, questionsFromAi.size() - newQuestionsCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用AI检查题库中的数据是否重复
|
||||||
|
*/
|
||||||
|
@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);
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private static String getPrompt(List<Question> questions) {
|
||||||
|
JSONArray jsonArray = new JSONArray();
|
||||||
|
for (Question question : questions) {
|
||||||
|
JSONObject jsonObject = new JSONObject();
|
||||||
|
jsonObject.put("id", question.getId());
|
||||||
|
jsonObject.put("content", question.getContent());
|
||||||
|
jsonArray.add(jsonObject);
|
||||||
|
}
|
||||||
|
JSONObject jsonObject = new JSONObject();
|
||||||
|
jsonObject.put("data", jsonArray);
|
||||||
|
return String.format("""
|
||||||
|
你是一个数据清洗与去重专家。你的任务是从以下文本列表中,识别出语义相同或高度相似的条目,并为每一组相似条目筛选出一个最规范、最简洁的代表性版本。
|
||||||
|
|
||||||
|
【去重规则】
|
||||||
|
1. **语义核心优先**:忽略无关的修饰词、标点符号、后缀(如“:2”)、大小写和空格。
|
||||||
|
2. **合并同类项**:将表达同一主题或问题的文本归为一组。
|
||||||
|
3. **选择标准**:从每一组中,选出那个最完整、最简洁、且没有多余符号(如序号、特殊后缀)的版本作为代表。如果两个版本质量相当,优先选择更短的那个。
|
||||||
|
4. **保留原意**:确保选出的代表版本没有改变原文本的核心含义。
|
||||||
|
请按照下述格式返回,已被剔除掉的数据无需返回
|
||||||
|
{
|
||||||
|
"questions": [1, 2, 3, .....]
|
||||||
|
}
|
||||||
|
分类规则:
|
||||||
|
1. 只返回JSON,不要其他解释文字
|
||||||
|
2. 请严格按照API接口形式返回,不要返回任何额外的文字内容,包括'```json```'!!!!
|
||||||
|
3. 请严格按照网络接口的形式返回JSON数据!!!
|
||||||
|
【请处理以下数据列表】:
|
||||||
|
%s
|
||||||
|
""", jsonObject.toJSONString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验题目内容是否重复
|
||||||
|
*
|
||||||
|
* @param content 题目内容
|
||||||
|
* @param currentId 当前题目ID,更新时传入,用于排除自身
|
||||||
|
*/
|
||||||
|
private void validateQuestion(String content, Long currentId) {
|
||||||
|
Question existingQuestion = questionMapper.selectByContent(content);
|
||||||
|
if (existingQuestion != null && (!existingQuestion.getId().equals(currentId))) {
|
||||||
|
throw new IllegalArgumentException("题目内容已存在,请勿重复添加。");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getFileExtension(String fileName) {
|
||||||
|
if (fileName == null || fileName.lastIndexOf('.') == -1) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<QuestionAndCategoryTreeListVO> getTreeListCategory(QuestionOptionsDTO dto) {
|
||||||
|
if (StringUtils.isNoneBlank(dto.getDifficulty()) && dto.getDifficulty().equals("ALL")) {
|
||||||
|
dto.setDifficulty(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取分类树列表
|
||||||
|
List<QuestionCategory> treeList = questionCategoryService.getTreeList();
|
||||||
|
List<QuestionCategory> questionCategories = new ArrayList<>();
|
||||||
|
|
||||||
|
if (CollectionUtil.isNotEmpty(dto.getCategoryIds())) {
|
||||||
|
questionCategories = questionCategoryService.batchFindByAncestorIdsUnion(dto.getCategoryIds());
|
||||||
|
if (CollectionUtil.isNotEmpty(questionCategories)) {
|
||||||
|
treeList = TreeUtil.buildTree(
|
||||||
|
questionCategories,
|
||||||
|
QuestionCategory::getId,
|
||||||
|
QuestionCategory::getParentId,
|
||||||
|
QuestionCategory::getChildren,
|
||||||
|
CommonConstant.ROOT_PARENT_ID
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有题目列表
|
||||||
|
List<Question> questionList = list(
|
||||||
|
new LambdaQueryWrapper<Question>()
|
||||||
|
.in(CollectionUtil.isNotEmpty(dto.getCategoryIds()), Question::getCategoryId,
|
||||||
|
dto.getCategoryIds())
|
||||||
|
.eq(StringUtils.isNotBlank(dto.getDifficulty()), Question::getDifficulty, dto.getDifficulty())
|
||||||
|
.eq(Question::getDeleted, 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 转换为VO对象并整合题目数据
|
||||||
|
List<QuestionAndCategoryTreeListVO> voList = convertToVOListWithQuestions(treeList, questionList);
|
||||||
|
|
||||||
|
// 设置根节点的题目总数
|
||||||
|
if (CollectionUtil.isNotEmpty(voList)) {
|
||||||
|
Integer i = calcCount(voList);
|
||||||
|
log.info("根节点题目总数: {}", i);
|
||||||
|
QuestionAndCategoryTreeListVO rootVO = new QuestionAndCategoryTreeListVO();
|
||||||
|
rootVO.setId(0L);
|
||||||
|
rootVO.setName("全部题目");
|
||||||
|
rootVO.setType("root");
|
||||||
|
rootVO.setChildren(voList);
|
||||||
|
rootVO.setCount(i);
|
||||||
|
return List.of(rootVO);
|
||||||
|
}
|
||||||
|
|
||||||
|
return voList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将QuestionCategory列表转换为QuestionAndCategoryTreeListVO列表,并整合题目数据
|
||||||
|
*
|
||||||
|
* @param categoryList 分类列表
|
||||||
|
* @param questionList 题目列表
|
||||||
|
* @return QuestionAndCategoryTreeListVO列表
|
||||||
|
*/
|
||||||
|
private List<QuestionAndCategoryTreeListVO> convertToVOListWithQuestions(
|
||||||
|
List<QuestionCategory> categoryList,
|
||||||
|
List<Question> questionList) {
|
||||||
|
|
||||||
|
if (CollectionUtil.isEmpty(categoryList)) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按分类ID分组题目数据
|
||||||
|
Map<Long, List<Question>> questionsByCategoryId = questionList.stream()
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.filter(question -> question.getCategoryId() != null)
|
||||||
|
.collect(Collectors.groupingBy(Question::getCategoryId));
|
||||||
|
|
||||||
|
return categoryList.stream()
|
||||||
|
.map(category -> convertToVOWithQuestions(category, questionsByCategoryId))
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将单个QuestionCategory转换为QuestionAndCategoryTreeListVO,并整合题目数据
|
||||||
|
*
|
||||||
|
* @param category 分类对象
|
||||||
|
* @param questionsByCategoryId 按分类ID分组的题目数据
|
||||||
|
* @return QuestionAndCategoryTreeListVO对象
|
||||||
|
*/
|
||||||
|
private QuestionAndCategoryTreeListVO convertToVOWithQuestions(
|
||||||
|
QuestionCategory category,
|
||||||
|
Map<Long, List<Question>> questionsByCategoryId) {
|
||||||
|
|
||||||
|
if (category == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建VO对象
|
||||||
|
QuestionAndCategoryTreeListVO vo = new QuestionAndCategoryTreeListVO();
|
||||||
|
|
||||||
|
// 复制基本属性
|
||||||
|
vo.setId(category.getId());
|
||||||
|
vo.setName(category.getName());
|
||||||
|
vo.setType("category");
|
||||||
|
vo.setCount(0);
|
||||||
|
|
||||||
|
// 处理子节点(包括子分类和题目)
|
||||||
|
List<QuestionAndCategoryTreeListVO> childrenVOs = new ArrayList<>();
|
||||||
|
|
||||||
|
// 先处理子分类
|
||||||
|
if (CollectionUtil.isNotEmpty(category.getChildren())) {
|
||||||
|
for (QuestionCategory childCategory : category.getChildren()) {
|
||||||
|
QuestionAndCategoryTreeListVO childVO = convertToVOWithQuestions(childCategory, questionsByCategoryId);
|
||||||
|
if (childVO != null) {
|
||||||
|
childrenVOs.add(childVO);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 再处理当前分类下的题目
|
||||||
|
List<Question> questionsInCategory = questionsByCategoryId.getOrDefault(category.getId(), List.of());
|
||||||
|
|
||||||
|
if (CollectionUtil.isNotEmpty(questionsInCategory)) {
|
||||||
|
for (Question question : questionsInCategory) {
|
||||||
|
QuestionAndCategoryTreeListVO questionVO = convertQuestionToVO(question);
|
||||||
|
if (questionVO != null) {
|
||||||
|
childrenVOs.add(questionVO);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置子节点
|
||||||
|
if (CollectionUtil.isNotEmpty(childrenVOs)) {
|
||||||
|
vo.setChildren(childrenVOs);
|
||||||
|
}
|
||||||
|
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将Question转换为QuestionAndCategoryTreeListVO
|
||||||
|
*
|
||||||
|
* @param question 题目对象
|
||||||
|
* @return QuestionAndCategoryTreeListVO对象
|
||||||
|
*/
|
||||||
|
private QuestionAndCategoryTreeListVO convertQuestionToVO(Question question) {
|
||||||
|
if (question == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
QuestionAndCategoryTreeListVO vo = new QuestionAndCategoryTreeListVO();
|
||||||
|
vo.setId(question.getId());
|
||||||
|
// 使用题目内容作为名称,可以根据需要修改
|
||||||
|
vo.setName(question.getContent());
|
||||||
|
// 题目下面没有子节点
|
||||||
|
vo.setChildren(List.of());
|
||||||
|
vo.setType("question");
|
||||||
|
vo.setCount(0); // 题目节点没有子节点,count设为0
|
||||||
|
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer calcCount(List<QuestionAndCategoryTreeListVO> voList) {
|
||||||
|
Integer count = 0;
|
||||||
|
if (CollectionUtil.isNotEmpty(voList)) {
|
||||||
|
for (QuestionAndCategoryTreeListVO vo : voList) {
|
||||||
|
Integer currCount = 0;
|
||||||
|
if (vo.getType().equals("question")) {
|
||||||
|
count++;
|
||||||
|
currCount++;
|
||||||
|
}
|
||||||
|
if (CollectionUtil.isNotEmpty(vo.getChildren())) {
|
||||||
|
Integer i = calcCount(vo.getChildren());
|
||||||
|
count += i;
|
||||||
|
currCount += i;
|
||||||
|
}
|
||||||
|
vo.setCount(currCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.qingqiu.interview.service.impl.parser;
|
||||||
|
|
||||||
|
import com.qingqiu.interview.common.enums.DocumentParserProvider;
|
||||||
|
import com.qingqiu.interview.service.parser.DocumentParser;
|
||||||
|
import com.qingqiu.interview.service.parser.DocumentParserManager;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1></h1>
|
||||||
|
*
|
||||||
|
* @author qingqiu
|
||||||
|
* @date 2025/9/18 16:45
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class DocumentParserManagerImpl implements DocumentParserManager {
|
||||||
|
private final Map<DocumentParserProvider, DocumentParser> factories;
|
||||||
|
|
||||||
|
public DocumentParserManagerImpl(List<DocumentParser> strategies) {
|
||||||
|
this.factories = strategies.stream()
|
||||||
|
.collect(Collectors.toMap(
|
||||||
|
DocumentParser::getSupportedProvider,
|
||||||
|
Function.identity()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DocumentParser getParser(DocumentParserProvider provider) {
|
||||||
|
DocumentParser parser = factories.get(provider);
|
||||||
|
if (parser == null) {
|
||||||
|
throw new IllegalArgumentException("不支持的AI type: " + provider);
|
||||||
|
}
|
||||||
|
return parser;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.qingqiu.interview.service.parser;
|
package com.qingqiu.interview.service.impl.parser;
|
||||||
|
|
||||||
|
import com.qingqiu.interview.common.enums.DocumentParserProvider;
|
||||||
|
import com.qingqiu.interview.service.parser.DocumentParser;
|
||||||
import org.commonmark.node.Node;
|
import org.commonmark.node.Node;
|
||||||
import org.commonmark.parser.Parser;
|
import org.commonmark.parser.Parser;
|
||||||
import org.commonmark.renderer.text.TextContentRenderer;
|
import org.commonmark.renderer.text.TextContentRenderer;
|
||||||
@@ -9,7 +11,7 @@ import java.io.InputStream;
|
|||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
|
|
||||||
@Service("mdParser")
|
@Service("mdParser")
|
||||||
public class MarkdownParserService implements DocumentParser {
|
public class MarkdownParserServiceImpl implements DocumentParser {
|
||||||
|
|
||||||
private final Parser parser = Parser.builder().build();
|
private final Parser parser = Parser.builder().build();
|
||||||
private final TextContentRenderer renderer = TextContentRenderer.builder().build();
|
private final TextContentRenderer renderer = TextContentRenderer.builder().build();
|
||||||
@@ -28,5 +30,10 @@ public class MarkdownParserService implements DocumentParser {
|
|||||||
public String getSupportedType() {
|
public String getSupportedType() {
|
||||||
return "md";
|
return "md";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DocumentParserProvider getSupportedProvider() {
|
||||||
|
return DocumentParserProvider.MARKDOWN;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
package com.qingqiu.interview.service.parser;
|
package com.qingqiu.interview.service.impl.parser;
|
||||||
|
|
||||||
|
|
||||||
|
import com.qingqiu.interview.common.enums.DocumentParserProvider;
|
||||||
|
import com.qingqiu.interview.service.parser.DocumentParser;
|
||||||
import org.apache.pdfbox.Loader;
|
import org.apache.pdfbox.Loader;
|
||||||
import org.apache.pdfbox.cos.COSDocument;
|
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.fdf.FDFDocument;
|
|
||||||
import org.apache.pdfbox.text.PDFTextStripper;
|
import org.apache.pdfbox.text.PDFTextStripper;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ import java.io.InputStream;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
@Service("pdfParser")
|
@Service("pdfParser")
|
||||||
public class PdfParserService implements DocumentParser {
|
public class PdfParserServiceImpl implements DocumentParser {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析 PDF 文档内容
|
* 解析 PDF 文档内容
|
||||||
@@ -53,5 +53,10 @@ public class PdfParserService implements DocumentParser {
|
|||||||
public String getSupportedType() {
|
public String getSupportedType() {
|
||||||
return "pdf"; // 返回支持的文档类型
|
return "pdf"; // 返回支持的文档类型
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DocumentParserProvider getSupportedProvider() {
|
||||||
|
return DocumentParserProvider.PDF;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
12
src/main/java/com/qingqiu/interview/service/llm/LlmService.java
Normal file → Executable file
12
src/main/java/com/qingqiu/interview/service/llm/LlmService.java
Normal file → Executable file
@@ -1,5 +1,7 @@
|
|||||||
package com.qingqiu.interview.service.llm;
|
package com.qingqiu.interview.service.llm;
|
||||||
|
|
||||||
|
import com.qingqiu.interview.common.enums.LLMProvider;
|
||||||
|
|
||||||
public interface LlmService {
|
public interface LlmService {
|
||||||
|
|
||||||
|
|
||||||
@@ -9,6 +11,7 @@ public interface LlmService {
|
|||||||
* @return ai回复
|
* @return ai回复
|
||||||
*/
|
*/
|
||||||
String chat(String prompt);
|
String chat(String prompt);
|
||||||
|
String chat(String prompt, LLMProvider provider);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 与模型进行多轮对话
|
* 与模型进行多轮对话
|
||||||
@@ -18,6 +21,15 @@ public interface LlmService {
|
|||||||
*/
|
*/
|
||||||
String chat(String prompt, String token);
|
String chat(String prompt, String token);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 与模型进行多轮对话 指定模型
|
||||||
|
* @param prompt 提示词
|
||||||
|
* @param model 模型名称
|
||||||
|
* @param token 会话token
|
||||||
|
* @return ai回复
|
||||||
|
*/
|
||||||
|
String chat(String prompt, String token, LLMProvider provider);
|
||||||
|
|
||||||
Integer getPromptTokens(String prompt);
|
Integer getPromptTokens(String prompt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
84
src/main/java/com/qingqiu/interview/service/llm/qwen/QwenService.java
Normal file → Executable file
84
src/main/java/com/qingqiu/interview/service/llm/qwen/QwenService.java
Normal file → Executable file
@@ -7,8 +7,8 @@ import com.alibaba.dashscope.common.Role;
|
|||||||
import com.alibaba.dashscope.tokenizers.Tokenizer;
|
import com.alibaba.dashscope.tokenizers.Tokenizer;
|
||||||
import com.alibaba.dashscope.tokenizers.TokenizerFactory;
|
import com.alibaba.dashscope.tokenizers.TokenizerFactory;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.qingqiu.interview.common.enums.LLMProvider;
|
||||||
import com.qingqiu.interview.ai.factory.AIClientManager;
|
import com.qingqiu.interview.ai.factory.AIClientManager;
|
||||||
import com.qingqiu.interview.common.constants.AIStrategyConstant;
|
|
||||||
import com.qingqiu.interview.entity.AiSessionLog;
|
import com.qingqiu.interview.entity.AiSessionLog;
|
||||||
import com.qingqiu.interview.mapper.AiSessionLogMapper;
|
import com.qingqiu.interview.mapper.AiSessionLogMapper;
|
||||||
import com.qingqiu.interview.service.llm.LlmService;
|
import com.qingqiu.interview.service.llm.LlmService;
|
||||||
@@ -42,7 +42,7 @@ public class QwenService implements LlmService {
|
|||||||
public String chat(String prompt) {
|
public String chat(String prompt) {
|
||||||
// log.info("开始调用API....");
|
// log.info("开始调用API....");
|
||||||
// long l = System.currentTimeMillis();
|
// long l = System.currentTimeMillis();
|
||||||
return aiClientManager.getClient(AIStrategyConstant.DEEPSEEK).chatCompletion(prompt);
|
return chat(prompt, LLMProvider.DEEPSEEK);
|
||||||
// GenerationParam param = GenerationParam.builder()
|
// GenerationParam param = GenerationParam.builder()
|
||||||
// .model(DEEPSEEK_3) // 可根据需要更换模型
|
// .model(DEEPSEEK_3) // 可根据需要更换模型
|
||||||
// .messages(Collections.singletonList(createMessage(Role.USER.getValue(), prompt)))
|
// .messages(Collections.singletonList(createMessage(Role.USER.getValue(), prompt)))
|
||||||
@@ -63,8 +63,52 @@ public class QwenService implements LlmService {
|
|||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String chat(String prompt, LLMProvider provider) {
|
||||||
|
return aiClientManager.getClient(provider).chatCompletion(prompt);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String chat(String prompt, String token) {
|
public String chat(String prompt, String token) {
|
||||||
|
return chat(prompt, token, LLMProvider.DEEPSEEK);
|
||||||
|
|
||||||
|
// // 调用AI模型
|
||||||
|
// try {
|
||||||
|
// log.info("开始调用API....");
|
||||||
|
// long l = System.currentTimeMillis();
|
||||||
|
// GenerationParam param = GenerationParam.builder()
|
||||||
|
// .model(DEEPSEEK_3_1) // 可根据需要更换模型
|
||||||
|
// .messages(messages)
|
||||||
|
// .resultFormat(GenerationParam.ResultFormat.MESSAGE)
|
||||||
|
// .apiKey(apiKey)
|
||||||
|
// .build();
|
||||||
|
//
|
||||||
|
// GenerationResult result = generation.call(param);
|
||||||
|
// log.info("调用成功,耗时: {} ms", System.currentTimeMillis() - l);
|
||||||
|
// String aiResponse = result.getOutput().getChoices().get(0).getMessage().getContent();
|
||||||
|
// log.debug("响应结果: {}", aiResponse);
|
||||||
|
// // 存储用户提问
|
||||||
|
// AiSessionLog userLog = new AiSessionLog();
|
||||||
|
// userLog.setToken(token);
|
||||||
|
// userLog.setRole(Role.USER.getValue());
|
||||||
|
// userLog.setContent(prompt);
|
||||||
|
// aiSessionLogMapper.insert(userLog);
|
||||||
|
//
|
||||||
|
// // 存储AI回复
|
||||||
|
// AiSessionLog aiLog = new AiSessionLog();
|
||||||
|
// aiLog.setToken(token);
|
||||||
|
// aiLog.setRole(Role.ASSISTANT.getValue());
|
||||||
|
// aiLog.setContent(aiResponse);
|
||||||
|
// aiSessionLogMapper.insert(aiLog);
|
||||||
|
//
|
||||||
|
// return aiResponse;
|
||||||
|
// } catch (ApiException | NoApiKeyException | InputRequiredException e) {
|
||||||
|
// throw new RuntimeException("调用AI服务失败", e);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String chat(String prompt, String token, LLMProvider provider) {
|
||||||
// 根据token查询会话记录
|
// 根据token查询会话记录
|
||||||
List<AiSessionLog> aiSessionLogs = aiSessionLogMapper.selectList(
|
List<AiSessionLog> aiSessionLogs = aiSessionLogMapper.selectList(
|
||||||
new LambdaQueryWrapper<AiSessionLog>()
|
new LambdaQueryWrapper<AiSessionLog>()
|
||||||
@@ -113,7 +157,7 @@ public class QwenService implements LlmService {
|
|||||||
createMessage(Role.USER.getValue(), prompt)
|
createMessage(Role.USER.getValue(), prompt)
|
||||||
);
|
);
|
||||||
|
|
||||||
String aiResponse = aiClientManager.getClient(AIStrategyConstant.DEEPSEEK).chatCompletion(messages);
|
String aiResponse = aiClientManager.getClient(provider).chatCompletion(messages);
|
||||||
// 存储用户提问
|
// 存储用户提问
|
||||||
AiSessionLog userLog = new AiSessionLog();
|
AiSessionLog userLog = new AiSessionLog();
|
||||||
userLog.setToken(token);
|
userLog.setToken(token);
|
||||||
@@ -128,40 +172,6 @@ public class QwenService implements LlmService {
|
|||||||
aiLog.setContent(aiResponse);
|
aiLog.setContent(aiResponse);
|
||||||
aiSessionLogMapper.insert(aiLog);
|
aiSessionLogMapper.insert(aiLog);
|
||||||
return aiResponse;
|
return aiResponse;
|
||||||
|
|
||||||
// // 调用AI模型
|
|
||||||
// try {
|
|
||||||
// log.info("开始调用API....");
|
|
||||||
// long l = System.currentTimeMillis();
|
|
||||||
// GenerationParam param = GenerationParam.builder()
|
|
||||||
// .model(DEEPSEEK_3_1) // 可根据需要更换模型
|
|
||||||
// .messages(messages)
|
|
||||||
// .resultFormat(GenerationParam.ResultFormat.MESSAGE)
|
|
||||||
// .apiKey(apiKey)
|
|
||||||
// .build();
|
|
||||||
//
|
|
||||||
// GenerationResult result = generation.call(param);
|
|
||||||
// log.info("调用成功,耗时: {} ms", System.currentTimeMillis() - l);
|
|
||||||
// String aiResponse = result.getOutput().getChoices().get(0).getMessage().getContent();
|
|
||||||
// log.debug("响应结果: {}", aiResponse);
|
|
||||||
// // 存储用户提问
|
|
||||||
// AiSessionLog userLog = new AiSessionLog();
|
|
||||||
// userLog.setToken(token);
|
|
||||||
// userLog.setRole(Role.USER.getValue());
|
|
||||||
// userLog.setContent(prompt);
|
|
||||||
// aiSessionLogMapper.insert(userLog);
|
|
||||||
//
|
|
||||||
// // 存储AI回复
|
|
||||||
// AiSessionLog aiLog = new AiSessionLog();
|
|
||||||
// aiLog.setToken(token);
|
|
||||||
// aiLog.setRole(Role.ASSISTANT.getValue());
|
|
||||||
// aiLog.setContent(aiResponse);
|
|
||||||
// aiSessionLogMapper.insert(aiLog);
|
|
||||||
//
|
|
||||||
// return aiResponse;
|
|
||||||
// } catch (ApiException | NoApiKeyException | InputRequiredException e) {
|
|
||||||
// throw new RuntimeException("调用AI服务失败", e);
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
4
src/main/java/com/qingqiu/interview/service/parser/DocumentParser.java
Normal file → Executable file
4
src/main/java/com/qingqiu/interview/service/parser/DocumentParser.java
Normal file → Executable file
@@ -1,5 +1,7 @@
|
|||||||
package com.qingqiu.interview.service.parser;
|
package com.qingqiu.interview.service.parser;
|
||||||
|
|
||||||
|
import com.qingqiu.interview.common.enums.DocumentParserProvider;
|
||||||
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
|
||||||
public interface DocumentParser {
|
public interface DocumentParser {
|
||||||
@@ -15,5 +17,7 @@ public interface DocumentParser {
|
|||||||
* @return "pdf", "md", etc.
|
* @return "pdf", "md", etc.
|
||||||
*/
|
*/
|
||||||
String getSupportedType();
|
String getSupportedType();
|
||||||
|
|
||||||
|
DocumentParserProvider getSupportedProvider();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.qingqiu.interview.service.parser;
|
||||||
|
|
||||||
|
import com.qingqiu.interview.common.enums.DocumentParserProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1></h1>
|
||||||
|
*
|
||||||
|
* @author qingqiu
|
||||||
|
* @date 2025/9/18 16:42
|
||||||
|
*/
|
||||||
|
public interface DocumentParserManager {
|
||||||
|
|
||||||
|
DocumentParser getParser(DocumentParserProvider provider);
|
||||||
|
}
|
||||||
22
src/main/java/com/qingqiu/interview/vo/ChatVO.java
Normal file
22
src/main/java/com/qingqiu/interview/vo/ChatVO.java
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package com.qingqiu.interview.vo;
|
||||||
|
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1></h1>
|
||||||
|
*
|
||||||
|
* @author qingqiu
|
||||||
|
* @date 2025/9/18 12:56
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Accessors
|
||||||
|
@Builder
|
||||||
|
public class ChatVO {
|
||||||
|
|
||||||
|
private String sessionId;
|
||||||
|
private String content;
|
||||||
|
/** 角色 */
|
||||||
|
private String role;
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.qingqiu.interview.vo;
|
||||||
|
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
@Accessors(chain = true)
|
||||||
|
public class QuestionAndCategoryTreeListVO implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
/**
|
||||||
|
* category:分类
|
||||||
|
* question:问题
|
||||||
|
*/
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
private List<QuestionAndCategoryTreeListVO> children;
|
||||||
|
|
||||||
|
private Integer count;
|
||||||
|
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user