修改AI面试相关内容
This commit is contained in:
2
pom.xml
2
pom.xml
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.4.10-SNAPSHOT</version>
|
||||
<version>3.5.0</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>com.qingqiu</groupId>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.qingqiu.interview.ai.enums;
|
||||
|
||||
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 String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public static LLMProvider fromCode(String code) {
|
||||
for (LLMProvider provider : values()) {
|
||||
if (provider.getCode().equals(code)) {
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Unknown provider: " + code);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
package com.qingqiu.interview.ai.factory;
|
||||
|
||||
import com.qingqiu.interview.ai.enums.LLMProvider;
|
||||
import com.qingqiu.interview.ai.service.AIClientService;
|
||||
|
||||
public interface AIClientFactory {
|
||||
AIClientService createAIClient();
|
||||
|
||||
// 支持的提供商
|
||||
LLMProvider getSupportedProvider();
|
||||
}
|
||||
|
||||
@@ -1,24 +1,32 @@
|
||||
package com.qingqiu.interview.ai.factory;
|
||||
|
||||
import com.qingqiu.interview.ai.enums.LLMProvider;
|
||||
import com.qingqiu.interview.ai.service.AIClientService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class AIClientManager {
|
||||
|
||||
private final Map<String, AIClientFactory> factories;
|
||||
private final Map<LLMProvider, AIClientFactory> factories;
|
||||
|
||||
public AIClientManager(Map<String, AIClientFactory> factories) {
|
||||
this.factories = factories;
|
||||
public AIClientManager(List<AIClientFactory> strategies) {
|
||||
this.factories = strategies.stream()
|
||||
.collect(Collectors.toMap(
|
||||
AIClientFactory::getSupportedProvider,
|
||||
Function.identity()
|
||||
));
|
||||
}
|
||||
|
||||
public AIClientService getClient(String aiType) {
|
||||
String factoryName = aiType + "ClientFactory";
|
||||
AIClientFactory factory = factories.get(factoryName);
|
||||
public AIClientService getClient(LLMProvider provider) {
|
||||
// String factoryName = aiType + "ClientFactory";
|
||||
AIClientFactory factory = factories.get(provider);
|
||||
if (factory == null) {
|
||||
throw new IllegalArgumentException("不支持的AI type: " + aiType);
|
||||
throw new IllegalArgumentException("不支持的AI type: " + provider);
|
||||
}
|
||||
return factory.createAIClient();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.qingqiu.interview.ai.factory;
|
||||
|
||||
import com.qingqiu.interview.ai.enums.LLMProvider;
|
||||
import com.qingqiu.interview.ai.service.AIClientService;
|
||||
import com.qingqiu.interview.ai.service.impl.DeepSeekClientServiceImpl;
|
||||
import com.qingqiu.interview.common.utils.SpringApplicationContextUtil;
|
||||
@@ -11,4 +12,9 @@ public class DeepSeekClientFactory implements AIClientFactory{
|
||||
public AIClientService createAIClient() {
|
||||
return SpringApplicationContextUtil.getBean(DeepSeekClientServiceImpl.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LLMProvider getSupportedProvider() {
|
||||
return LLMProvider.DEEPSEEK;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.qingqiu.interview.ai.factory;
|
||||
|
||||
import com.qingqiu.interview.ai.enums.LLMProvider;
|
||||
import com.qingqiu.interview.ai.service.AIClientService;
|
||||
import com.qingqiu.interview.ai.service.impl.QwenClientServiceImpl;
|
||||
import com.qingqiu.interview.common.utils.SpringApplicationContextUtil;
|
||||
@@ -11,4 +12,9 @@ public class QwenClientFactory implements AIClientFactory{
|
||||
public AIClientService createAIClient() {
|
||||
return SpringApplicationContextUtil.getBean(QwenClientServiceImpl.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LLMProvider getSupportedProvider() {
|
||||
return LLMProvider.QWEN;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.alibaba.dashscope.aigc.generation.Generation;
|
||||
import com.alibaba.dashscope.aigc.generation.GenerationParam;
|
||||
import com.alibaba.dashscope.aigc.generation.GenerationResult;
|
||||
import com.alibaba.dashscope.common.Message;
|
||||
import com.alibaba.dashscope.common.ResponseFormat;
|
||||
import com.alibaba.dashscope.exception.ApiException;
|
||||
import com.alibaba.dashscope.exception.InputRequiredException;
|
||||
import com.alibaba.dashscope.exception.NoApiKeyException;
|
||||
@@ -42,6 +43,7 @@ public class QwenClientServiceImpl extends AIClientService {
|
||||
.model(QWEN_PLUS_LATEST) // 可根据需要更换模型
|
||||
.messages(messages)
|
||||
.resultFormat(GenerationParam.ResultFormat.MESSAGE)
|
||||
.responseFormat(ResponseFormat.builder().type(ResponseFormat.JSON_OBJECT).build())
|
||||
.apiKey(apiKey)
|
||||
.build();
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package com.qingqiu.interview.common.utils;
|
||||
|
||||
import java.util.*;
|
||||
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;
|
||||
|
||||
@@ -56,4 +59,5 @@ public class TreeUtil {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
* 仪表盘数据统计接口
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/dashboard")
|
||||
@RequestMapping("/dashboard")
|
||||
@RequiredArgsConstructor
|
||||
public class DashboardController {
|
||||
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
package com.qingqiu.interview.controller;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.qingqiu.interview.dto.*;
|
||||
import com.qingqiu.interview.entity.InterviewSession;
|
||||
import com.qingqiu.interview.service.InterviewService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 面试流程相关接口
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/interview")
|
||||
@RequestMapping("/interview")
|
||||
@RequiredArgsConstructor
|
||||
public class InterviewController {
|
||||
|
||||
@@ -27,8 +31,11 @@ public class InterviewController {
|
||||
public ApiResponse<InterviewResponse> startInterview(
|
||||
@RequestParam("resume") MultipartFile resume,
|
||||
@Validated @ModelAttribute InterviewStartRequest request) throws IOException {
|
||||
InterviewResponse response = interviewService.startInterview(resume, request);
|
||||
return ApiResponse.success(response);
|
||||
// InterviewResponse response = interviewService.startInterview(resume, request);
|
||||
log.info("接收到的数据: {}", JSONObject.toJSONString(request));
|
||||
InterviewResponse interviewResponse = new InterviewResponse();
|
||||
interviewResponse.setSessionId(UUID.randomUUID().toString().replace("-", ""));
|
||||
return ApiResponse.success(interviewResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,7 +19,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
* @since 2025-08-30
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/interview-question-progress")
|
||||
@RequestMapping("/interview-question-progress")
|
||||
@RequiredArgsConstructor
|
||||
public class InterviewQuestionProgressController {
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ 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.*;
|
||||
|
||||
@@ -16,10 +17,11 @@ import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/question-category")
|
||||
@RequestMapping("/question-category")
|
||||
@RequiredArgsConstructor
|
||||
public class QuestionCategoryController {
|
||||
|
||||
@Lazy
|
||||
private final IQuestionCategoryService questionCategoryService;
|
||||
|
||||
/**
|
||||
@@ -27,13 +29,14 @@ public class QuestionCategoryController {
|
||||
*/
|
||||
@GetMapping("/tree-list")
|
||||
public R<List<QuestionCategory>> getTreeList() {
|
||||
try {
|
||||
List<QuestionCategory> list = questionCategoryService.getTreeList();
|
||||
return R.success(list);
|
||||
} catch (Exception e) {
|
||||
log.error("获取分类树列表失败", e);
|
||||
return R.error("获取分类树列表失败");
|
||||
}
|
||||
|
||||
@GetMapping("/question-tree-list")
|
||||
public R<List<QuestionCategory>> getQuestionTreeList() {
|
||||
// List<QuestionCategory> list = questionCategoryService.getQuestionTreeList();
|
||||
return R.success();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -98,10 +101,10 @@ public class QuestionCategoryController {
|
||||
/**
|
||||
* 更新分类
|
||||
*/
|
||||
@PutMapping("/{id}")
|
||||
public R<Void> update(@PathVariable Long id, @Validated @RequestBody QuestionCategoryDTO dto) {
|
||||
@PostMapping("/update")
|
||||
public R<Void> update(@RequestBody QuestionCategoryDTO dto) {
|
||||
try {
|
||||
questionCategoryService.updateCategory(id, dto);
|
||||
questionCategoryService.updateCategory(dto);
|
||||
return R.success();
|
||||
} catch (RuntimeException e) {
|
||||
log.error("更新分类失败", e);
|
||||
|
||||
@@ -3,21 +3,24 @@ 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.ApiResponse;
|
||||
import com.qingqiu.interview.dto.QuestionOptionsDTO;
|
||||
import com.qingqiu.interview.dto.QuestionPageParams;
|
||||
import com.qingqiu.interview.entity.Question;
|
||||
import com.qingqiu.interview.service.QuestionService;
|
||||
import com.qingqiu.interview.vo.QuestionAndCategoryTreeListVO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 题库管理相关接口
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/question")
|
||||
@RequestMapping("/question")
|
||||
@RequiredArgsConstructor
|
||||
public class QuestionController {
|
||||
|
||||
@@ -76,4 +79,9 @@ public class QuestionController {
|
||||
questionService.useAiCheckQuestionData();
|
||||
return R.success();
|
||||
}
|
||||
|
||||
@PostMapping("/tree-list-category")
|
||||
public R<List<QuestionAndCategoryTreeListVO>> getTreeListCategory(@RequestBody QuestionOptionsDTO dto) {
|
||||
return R.success(questionService.getTreeListCategory(dto));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package com.qingqiu.interview.dto;
|
||||
|
||||
import com.qingqiu.interview.vo.QuestionAndCategoryTreeListVO;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class InterviewStartRequest {
|
||||
|
||||
@@ -10,6 +13,11 @@ public class InterviewStartRequest {
|
||||
private String candidateName;
|
||||
|
||||
|
||||
private List<QuestionAndCategoryTreeListVO> selectedNodes;
|
||||
|
||||
@NotBlank(message = "面试类型不能为空")
|
||||
private String model;
|
||||
|
||||
|
||||
// 简历文件通过MultipartFile单独传递
|
||||
}
|
||||
|
||||
@@ -27,6 +27,10 @@ public class QuestionCategoryDTO {
|
||||
@NotNull(message = "状态不能为空")
|
||||
private Integer state;
|
||||
|
||||
private String ancestor;
|
||||
|
||||
private Integer level;
|
||||
|
||||
/**
|
||||
* 父分类名称(用于前端显示)
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -8,7 +8,9 @@ import lombok.experimental.Accessors;
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Accessors(chain = true)
|
||||
public class QuestionPageParams extends PageBaseParams{
|
||||
public class QuestionPageParams extends PageBaseParams {
|
||||
|
||||
private String content;
|
||||
|
||||
private Long categoryId;
|
||||
}
|
||||
|
||||
@@ -19,8 +19,11 @@ public class Question {
|
||||
@TableField("content")
|
||||
private String content;
|
||||
|
||||
@TableField("category")
|
||||
private String category;
|
||||
@TableField("category_id")
|
||||
private Long categoryId;
|
||||
|
||||
@TableField("category_name")
|
||||
private String categoryName;
|
||||
|
||||
@TableField("difficulty")
|
||||
private String difficulty;
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
package com.qingqiu.interview.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 题型分类
|
||||
@@ -26,7 +25,7 @@ public class QuestionCategory implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,9 @@ package com.qingqiu.interview.mapper;
|
||||
|
||||
import com.qingqiu.interview.entity.QuestionCategory;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
@@ -12,5 +15,5 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
* @since 2025-09-08
|
||||
*/
|
||||
public interface QuestionCategoryMapper extends BaseMapper<QuestionCategory> {
|
||||
|
||||
List<QuestionCategory> batchFindByAncestorIdsUnion(@Param("searchIds") List<Long> searchIds);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package com.qingqiu.interview.mapper;
|
||||
|
||||
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 org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
@@ -18,6 +21,9 @@ public interface QuestionMapper extends BaseMapper<Question> {
|
||||
|
||||
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);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package com.qingqiu.interview.service;
|
||||
|
||||
import cn.hutool.db.PageResult;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
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 com.baomidou.mybatisplus.extension.service.IService;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -37,7 +35,7 @@ public interface IQuestionCategoryService extends IService<QuestionCategory> {
|
||||
/**
|
||||
* 更新分类
|
||||
*/
|
||||
void updateCategory(Long id, QuestionCategoryDTO dto);
|
||||
void updateCategory(QuestionCategoryDTO dto);
|
||||
|
||||
/**
|
||||
* 删除分类
|
||||
@@ -64,43 +62,14 @@ public interface IQuestionCategoryService extends IService<QuestionCategory> {
|
||||
*/
|
||||
List<QuestionCategory> searchByName(String name);
|
||||
|
||||
/**
|
||||
* 获取某个分类的所有子孙分类
|
||||
*/
|
||||
List<QuestionCategory> getAllDescendants(Long parentId);
|
||||
|
||||
/**
|
||||
* 批量更新分类状态(包含子孙分类)
|
||||
*/
|
||||
void batchUpdateState(Long parentId, Integer state);
|
||||
|
||||
/**
|
||||
* 获取分类的完整路径名称
|
||||
*/
|
||||
String getFullPathName(Long categoryId);
|
||||
|
||||
/**
|
||||
* 检查分类名称是否重复
|
||||
*/
|
||||
boolean checkNameExists(String name, Long parentId, Long excludeId);
|
||||
|
||||
/**
|
||||
* 移动分类(修改父分类)
|
||||
*/
|
||||
void moveCategory(Long id, Long newParentId);
|
||||
List<QuestionCategory> batchFindByAncestorIdsUnion(List<Long> searchIds);
|
||||
|
||||
/**
|
||||
* 获取指定层级的分类
|
||||
*/
|
||||
List<QuestionCategory> getCategoriesByLevel(Integer level);
|
||||
|
||||
/**
|
||||
* 获取启用的分类树
|
||||
*/
|
||||
List<QuestionCategory> getEnabledTreeList();
|
||||
|
||||
/**
|
||||
* 根据父ID获取子分类
|
||||
*/
|
||||
List<QuestionCategory> getChildrenByParentId(Long parentId);
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ public class QuestionClassificationService {
|
||||
for (JsonNode questionNode : questionsNode) {
|
||||
Question question = new Question()
|
||||
.setContent(getTextValue(questionNode, "content"))
|
||||
.setCategory(getTextValue(questionNode, "category"))
|
||||
.setCategoryName(getTextValue(questionNode, "category"))
|
||||
.setDifficulty(getTextValue(questionNode, "difficulty"))
|
||||
.setTags(getTextValue(questionNode, "tags"));
|
||||
|
||||
@@ -112,7 +112,7 @@ public class QuestionClassificationService {
|
||||
|
||||
private boolean isValidQuestion(Question question) {
|
||||
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) {
|
||||
@@ -126,7 +126,7 @@ public class QuestionClassificationService {
|
||||
if (!line.isEmpty() && line.length() > 10) { // 过滤太短的内容
|
||||
Question question = new Question()
|
||||
.setContent(line)
|
||||
.setCategory("未分类")
|
||||
.setCategoryName("未分类")
|
||||
.setDifficulty("Medium")
|
||||
.setTags("待分类");
|
||||
questions.add(question);
|
||||
|
||||
@@ -1,177 +1,30 @@
|
||||
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.service.IService;
|
||||
import com.qingqiu.interview.dto.QuestionOptionsDTO;
|
||||
import com.qingqiu.interview.dto.QuestionPageParams;
|
||||
import com.qingqiu.interview.entity.Question;
|
||||
import com.qingqiu.interview.mapper.QuestionMapper;
|
||||
import com.qingqiu.interview.service.llm.LlmService;
|
||||
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 com.qingqiu.interview.entity.QuestionCategory;
|
||||
import com.qingqiu.interview.vo.QuestionAndCategoryTreeListVO;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class QuestionService {
|
||||
public interface QuestionService extends IService<Question> {
|
||||
|
||||
private final QuestionMapper questionMapper;
|
||||
private final QuestionClassificationService classificationService;
|
||||
private final List<DocumentParser> documentParserList; // This will be injected by Spring
|
||||
private final LlmService llmService;
|
||||
Page<Question> getQuestionPage(QuestionPageParams params);
|
||||
|
||||
/**
|
||||
* 分页查询题库
|
||||
*/
|
||||
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 addQuestion(Question question);
|
||||
|
||||
/**
|
||||
* 新增题目,并进行重复校验
|
||||
*/
|
||||
public void addQuestion(Question question) {
|
||||
validateQuestion(question.getContent(), null);
|
||||
log.info("新增题目: {}", question.getContent());
|
||||
questionMapper.insert(question);
|
||||
}
|
||||
void updateQuestion(Question question);
|
||||
|
||||
/**
|
||||
* 更新题目,并进行重复校验
|
||||
*/
|
||||
public void updateQuestion(Question question) {
|
||||
validateQuestion(question.getContent(), question.getId());
|
||||
log.info("更新题目ID: {}", question.getId());
|
||||
questionMapper.updateById(question);
|
||||
}
|
||||
void deleteQuestion(Long id);
|
||||
|
||||
/**
|
||||
* 删除题目
|
||||
*/
|
||||
public void deleteQuestion(Long id) {
|
||||
log.info("删除题目ID: {}", id);
|
||||
questionMapper.deleteById(id);
|
||||
}
|
||||
void importQuestionsFromFile(MultipartFile file) throws IOException;
|
||||
|
||||
/**
|
||||
* 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));
|
||||
void useAiCheckQuestionData();
|
||||
|
||||
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检查题库中的数据是否重复
|
||||
*/
|
||||
@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();
|
||||
}
|
||||
List<QuestionAndCategoryTreeListVO> getTreeListCategory(QuestionOptionsDTO dto);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.qingqiu.interview.service.impl;
|
||||
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.db.PageResult;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
@@ -9,11 +8,16 @@ 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.mapper.QuestionCategoryMapper;
|
||||
import com.qingqiu.interview.service.IQuestionCategoryService;
|
||||
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.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
@@ -32,7 +36,11 @@ import java.util.stream.Collectors;
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor(onConstructor_ = {@Autowired, @Lazy})
|
||||
public class QuestionCategoryServiceImpl extends ServiceImpl<QuestionCategoryMapper, QuestionCategory> implements IQuestionCategoryService {
|
||||
|
||||
private final QuestionService questionService;
|
||||
|
||||
@Override
|
||||
public List<QuestionCategory> getTreeList() {
|
||||
List<QuestionCategory> allCategories = getAllValidCategories();
|
||||
@@ -76,21 +84,21 @@ public class QuestionCategoryServiceImpl extends ServiceImpl<QuestionCategoryMap
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateCategory(Long id, QuestionCategoryDTO dto) {
|
||||
QuestionCategory category = getById(id);
|
||||
public void updateCategory(QuestionCategoryDTO dto) {
|
||||
QuestionCategory category = getById(dto.getId());
|
||||
if (category == null) {
|
||||
throw new RuntimeException("分类不存在");
|
||||
}
|
||||
|
||||
// 检查名称是否重复(排除自身)
|
||||
if (checkNameExists(dto.getName(), category.getParentId(), id)) {
|
||||
if (checkNameExists(dto.getName(), category.getParentId(), dto.getId())) {
|
||||
throw new RuntimeException("同一层级下分类名称不能重复");
|
||||
}
|
||||
|
||||
// 检查是否修改了父分类
|
||||
if (!category.getParentId().equals(dto.getParentId())) {
|
||||
throw new RuntimeException("不支持直接修改父分类,请使用移动分类功能");
|
||||
}
|
||||
// if (!category.getParentId().equals(dto.getParentId())) {
|
||||
// throw new RuntimeException("不支持直接修改父分类,请使用移动分类功能");
|
||||
// }
|
||||
|
||||
BeanUtils.copyProperties(dto, category);
|
||||
category.setUpdatedTime(LocalDateTime.now());
|
||||
@@ -102,15 +110,21 @@ public class QuestionCategoryServiceImpl extends ServiceImpl<QuestionCategoryMap
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteCategory(Long id) {
|
||||
QuestionCategory category = getById(id);
|
||||
if (category == null) {
|
||||
throw new RuntimeException("分类不存在");
|
||||
// 1. 查找所有需要删除的分类ID(包括子分类)
|
||||
List<Long> categoryIdsToDelete = getAllCategoryIdsToDelete(id);
|
||||
|
||||
if (CollectionUtil.isEmpty(categoryIdsToDelete)) {
|
||||
return;
|
||||
}
|
||||
|
||||
checkChildrenExists(id);
|
||||
removeById(id);
|
||||
// 2. 删除所有相关分类
|
||||
this.removeByIds(categoryIdsToDelete);
|
||||
|
||||
log.info("删除分类成功:{}", id);
|
||||
// 3. 删除关联的题目数据
|
||||
questionService.remove(
|
||||
new LambdaQueryWrapper<Question>()
|
||||
.in(Question::getCategoryId, categoryIdsToDelete)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -173,42 +187,6 @@ public class QuestionCategoryServiceImpl extends ServiceImpl<QuestionCategoryMap
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<QuestionCategory> getAllDescendants(Long parentId) {
|
||||
List<QuestionCategory> allCategories = getAllValidCategories();
|
||||
return findDescendants(allCategories, parentId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void batchUpdateState(Long parentId, Integer state) {
|
||||
List<QuestionCategory> descendants = getAllDescendants(parentId);
|
||||
descendants.forEach(category -> {
|
||||
category.setState(state);
|
||||
category.setUpdatedTime(LocalDateTime.now());
|
||||
});
|
||||
|
||||
updateBatchById(descendants);
|
||||
log.info("批量更新分类状态成功:parentId={}, state={}, count={}", parentId, state, descendants.size());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFullPathName(Long categoryId) {
|
||||
List<QuestionCategory> allCategories = getAllValidCategories();
|
||||
Map<Long, QuestionCategory> categoryMap = allCategories.stream()
|
||||
.collect(Collectors.toMap(QuestionCategory::getId, category -> category));
|
||||
|
||||
List<String> pathNames = new ArrayList<>();
|
||||
QuestionCategory current = categoryMap.get(categoryId);
|
||||
|
||||
while (current != null) {
|
||||
pathNames.add(current.getName());
|
||||
current = categoryMap.get(current.getParentId());
|
||||
}
|
||||
|
||||
Collections.reverse(pathNames);
|
||||
return String.join("/", pathNames);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkNameExists(String name, Long parentId, Long excludeId) {
|
||||
@@ -221,81 +199,22 @@ public class QuestionCategoryServiceImpl extends ServiceImpl<QuestionCategoryMap
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void moveCategory(Long id, Long newParentId) {
|
||||
QuestionCategory category = getById(id);
|
||||
if (category == null) {
|
||||
throw new RuntimeException("分类不存在");
|
||||
public List<QuestionCategory> batchFindByAncestorIdsUnion(List<Long> searchIds) {
|
||||
return baseMapper.batchFindByAncestorIdsUnion(searchIds);
|
||||
}
|
||||
|
||||
if (category.getParentId().equals(newParentId)) {
|
||||
throw new RuntimeException("新父分类与当前父分类相同");
|
||||
}
|
||||
|
||||
validateParentCategory(newParentId);
|
||||
|
||||
// 检查名称是否重复
|
||||
if (checkNameExists(category.getName(), newParentId, id)) {
|
||||
throw new RuntimeException("目标父分类下已存在相同名称的分类");
|
||||
}
|
||||
|
||||
// 更新父分类
|
||||
category.setParentId(newParentId);
|
||||
|
||||
// 重新计算层级和路径
|
||||
QuestionCategory newParent = getById(newParentId);
|
||||
if (CommonConstant.ROOT_PARENT_ID.equals(newParentId)) {
|
||||
category.setLevel(1);
|
||||
category.setAncestor(String.valueOf(category.getId()));
|
||||
} else {
|
||||
category.setLevel(newParent.getLevel() + 1);
|
||||
category.setAncestor(newParent.getAncestor() + "," + category.getId());
|
||||
}
|
||||
|
||||
category.setUpdatedTime(LocalDateTime.now());
|
||||
updateById(category);
|
||||
|
||||
log.info("移动分类成功:id={}, newParentId={}", id, newParentId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<QuestionCategory> getCategoriesByLevel(Integer level) {
|
||||
List<QuestionCategory> allCategories = getAllValidCategories();
|
||||
|
||||
return allCategories.stream()
|
||||
.filter(category -> level.equals(category.getLevel()))
|
||||
.sorted(Comparator.comparingInt(QuestionCategory::getSort))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<QuestionCategory> getEnabledTreeList() {
|
||||
List<QuestionCategory> allCategories = getAllValidCategories();
|
||||
|
||||
List<QuestionCategory> enabledCategories = allCategories.stream()
|
||||
.filter(category -> CommonStateEnum.ENABLED.getCode().equals(category.getState()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return buildCategoryTree(enabledCategories);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<QuestionCategory> getChildrenByParentId(Long parentId) {
|
||||
List<QuestionCategory> allCategories = getAllValidCategories();
|
||||
|
||||
return allCategories.stream()
|
||||
.filter(category -> parentId.equals(category.getParentId()))
|
||||
.sorted(Comparator.comparingInt(QuestionCategory::getSort))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// ============ 私有方法 ============
|
||||
|
||||
private List<QuestionCategory> getAllValidCategories() {
|
||||
LambdaQueryWrapper<QuestionCategory> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(QuestionCategory::getDeleted, CommonConstant.ZERO)
|
||||
.orderByAsc(QuestionCategory::getSort);
|
||||
return list(wrapper);
|
||||
// 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) {
|
||||
@@ -361,16 +280,6 @@ public class QuestionCategoryServiceImpl extends ServiceImpl<QuestionCategoryMap
|
||||
}
|
||||
}
|
||||
|
||||
private void checkChildrenExists(Long parentId) {
|
||||
List<QuestionCategory> allCategories = getAllValidCategories();
|
||||
boolean hasChildren = allCategories.stream()
|
||||
.anyMatch(category -> parentId.equals(category.getParentId()));
|
||||
|
||||
if (hasChildren) {
|
||||
throw new RuntimeException("存在子分类,无法删除");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private List<QuestionCategory> findDescendants(List<QuestionCategory> allCategories, Long parentId) {
|
||||
List<QuestionCategory> descendants = new ArrayList<>();
|
||||
@@ -384,4 +293,29 @@ public class QuestionCategoryServiceImpl extends ServiceImpl<QuestionCategoryMap
|
||||
|
||||
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.ai.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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.qingqiu.interview.service.llm;
|
||||
|
||||
import com.qingqiu.interview.ai.enums.LLMProvider;
|
||||
|
||||
public interface LlmService {
|
||||
|
||||
|
||||
@@ -9,6 +11,7 @@ public interface LlmService {
|
||||
* @return ai回复
|
||||
*/
|
||||
String chat(String prompt);
|
||||
String chat(String prompt, LLMProvider provider);
|
||||
|
||||
/**
|
||||
* 与模型进行多轮对话
|
||||
@@ -18,6 +21,15 @@ public interface LlmService {
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ import com.alibaba.dashscope.common.Role;
|
||||
import com.alibaba.dashscope.tokenizers.Tokenizer;
|
||||
import com.alibaba.dashscope.tokenizers.TokenizerFactory;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.qingqiu.interview.ai.enums.LLMProvider;
|
||||
import com.qingqiu.interview.ai.factory.AIClientManager;
|
||||
import com.qingqiu.interview.common.constants.AIStrategyConstant;
|
||||
import com.qingqiu.interview.entity.AiSessionLog;
|
||||
import com.qingqiu.interview.mapper.AiSessionLogMapper;
|
||||
import com.qingqiu.interview.service.llm.LlmService;
|
||||
@@ -42,7 +42,7 @@ public class QwenService implements LlmService {
|
||||
public String chat(String prompt) {
|
||||
// log.info("开始调用API....");
|
||||
// long l = System.currentTimeMillis();
|
||||
return aiClientManager.getClient(AIStrategyConstant.DEEPSEEK).chatCompletion(prompt);
|
||||
return chat(prompt, LLMProvider.DEEPSEEK);
|
||||
// GenerationParam param = GenerationParam.builder()
|
||||
// .model(DEEPSEEK_3) // 可根据需要更换模型
|
||||
// .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
|
||||
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查询会话记录
|
||||
List<AiSessionLog> aiSessionLogs = aiSessionLogMapper.selectList(
|
||||
new LambdaQueryWrapper<AiSessionLog>()
|
||||
@@ -113,7 +157,7 @@ public class QwenService implements LlmService {
|
||||
createMessage(Role.USER.getValue(), prompt)
|
||||
);
|
||||
|
||||
String aiResponse = aiClientManager.getClient(AIStrategyConstant.DEEPSEEK).chatCompletion(messages);
|
||||
String aiResponse = aiClientManager.getClient(provider).chatCompletion(messages);
|
||||
// 存储用户提问
|
||||
AiSessionLog userLog = new AiSessionLog();
|
||||
userLog.setToken(token);
|
||||
@@ -128,40 +172,6 @@ public class QwenService implements LlmService {
|
||||
aiLog.setContent(aiResponse);
|
||||
aiSessionLogMapper.insert(aiLog);
|
||||
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);
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -2,4 +2,16 @@
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.qingqiu.interview.mapper.QuestionCategoryMapper">
|
||||
|
||||
<select id="batchFindByAncestorIdsUnion" resultType="com.qingqiu.interview.entity.QuestionCategory">
|
||||
SELECT DISTINCT qc.*
|
||||
FROM question_category qc
|
||||
INNER JOIN (
|
||||
<foreach collection="searchIds" item="searchId" separator="UNION ALL">
|
||||
SELECT #{searchId} as search_id
|
||||
</foreach>
|
||||
) AS search_values
|
||||
ON FIND_IN_SET(search_values.search_id, qc.ancestor) > 0
|
||||
OR qc.id = search_values.search_id
|
||||
WHERE qc.deleted = 0
|
||||
</select>
|
||||
</mapper>
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
<select id="selectByCategory" resultType="com.qingqiu.interview.entity.Question">
|
||||
SELECT *
|
||||
FROM question
|
||||
WHERE category = #{category} AND deleted = 0
|
||||
WHERE category_name = #{category} AND deleted = 0
|
||||
ORDER BY created_time DESC
|
||||
</select>
|
||||
|
||||
<select id="selectByCategories" resultType="com.qingqiu.interview.entity.Question">
|
||||
SELECT *
|
||||
FROM question
|
||||
WHERE category IN
|
||||
WHERE category_name IN
|
||||
<foreach collection="categories" item="category" open="(" separator="," close=")">
|
||||
#{category}
|
||||
</foreach>
|
||||
@@ -23,7 +23,7 @@
|
||||
<select id="selectRandomByCategories" resultType="com.qingqiu.interview.entity.Question">
|
||||
SELECT *
|
||||
FROM question
|
||||
WHERE category IN
|
||||
WHERE category_name IN
|
||||
<foreach collection="categories" item="category" open="(" separator="," close=")">
|
||||
#{category}
|
||||
</foreach>
|
||||
@@ -40,11 +40,34 @@
|
||||
</select>
|
||||
|
||||
<select id="countByCategory" resultType="com.qingqiu.interview.dto.DashboardStatsResponse$CategoryStat">
|
||||
SELECT category as name, COUNT(*) as value
|
||||
SELECT category_name as name, COUNT(*) as value
|
||||
FROM question
|
||||
WHERE deleted = 0
|
||||
GROUP BY category
|
||||
GROUP BY category_name
|
||||
ORDER BY value DESC
|
||||
</select>
|
||||
<select id="queryPage" resultType="com.qingqiu.interview.entity.Question">
|
||||
select q.id,
|
||||
q.content,
|
||||
q.category_id,
|
||||
q.category_name,
|
||||
q.difficulty,
|
||||
q.tags,
|
||||
q.created_time,
|
||||
q.updated_time,
|
||||
q.deleted
|
||||
from question q
|
||||
inner join question_category qc on q.category_id = qc.id
|
||||
where q.deleted = 0
|
||||
<if test="params.content != null">
|
||||
and q.content like concat('%', #{params.content}, '%')
|
||||
</if>
|
||||
<if test="params.categoryId != null">
|
||||
and (find_in_set(#{params.categoryId}, qc.ancestor)
|
||||
or q.category_id = #{params.categoryId})
|
||||
</if>
|
||||
order by q.created_time desc
|
||||
</select>
|
||||
|
||||
|
||||
</mapper>
|
||||
Reference in New Issue
Block a user