diff --git a/pom.xml b/pom.xml index 8a39d30..7e2ea8d 100755 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 3.4.10-SNAPSHOT + 3.5.0 com.qingqiu diff --git a/src/main/java/com/qingqiu/interview/ai/enums/LLMProvider.java b/src/main/java/com/qingqiu/interview/ai/enums/LLMProvider.java new file mode 100644 index 0000000..82aaa77 --- /dev/null +++ b/src/main/java/com/qingqiu/interview/ai/enums/LLMProvider.java @@ -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); + } +} diff --git a/src/main/java/com/qingqiu/interview/ai/factory/AIClientFactory.java b/src/main/java/com/qingqiu/interview/ai/factory/AIClientFactory.java index 8c18ad0..aea32c2 100755 --- a/src/main/java/com/qingqiu/interview/ai/factory/AIClientFactory.java +++ b/src/main/java/com/qingqiu/interview/ai/factory/AIClientFactory.java @@ -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(); } diff --git a/src/main/java/com/qingqiu/interview/ai/factory/AIClientManager.java b/src/main/java/com/qingqiu/interview/ai/factory/AIClientManager.java index 470265b..263c79f 100755 --- a/src/main/java/com/qingqiu/interview/ai/factory/AIClientManager.java +++ b/src/main/java/com/qingqiu/interview/ai/factory/AIClientManager.java @@ -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 factories; + private final Map factories; - public AIClientManager(Map factories) { - this.factories = factories; + public AIClientManager(List 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(); } diff --git a/src/main/java/com/qingqiu/interview/ai/factory/DeepSeekClientFactory.java b/src/main/java/com/qingqiu/interview/ai/factory/DeepSeekClientFactory.java index 406c118..73ffce2 100755 --- a/src/main/java/com/qingqiu/interview/ai/factory/DeepSeekClientFactory.java +++ b/src/main/java/com/qingqiu/interview/ai/factory/DeepSeekClientFactory.java @@ -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; + } } diff --git a/src/main/java/com/qingqiu/interview/ai/factory/QwenClientFactory.java b/src/main/java/com/qingqiu/interview/ai/factory/QwenClientFactory.java index 66633bd..6d6a5d8 100755 --- a/src/main/java/com/qingqiu/interview/ai/factory/QwenClientFactory.java +++ b/src/main/java/com/qingqiu/interview/ai/factory/QwenClientFactory.java @@ -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; + } } diff --git a/src/main/java/com/qingqiu/interview/ai/service/impl/QwenClientServiceImpl.java b/src/main/java/com/qingqiu/interview/ai/service/impl/QwenClientServiceImpl.java index b243de8..6a15725 100755 --- a/src/main/java/com/qingqiu/interview/ai/service/impl/QwenClientServiceImpl.java +++ b/src/main/java/com/qingqiu/interview/ai/service/impl/QwenClientServiceImpl.java @@ -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(); diff --git a/src/main/java/com/qingqiu/interview/common/utils/TreeUtil.java b/src/main/java/com/qingqiu/interview/common/utils/TreeUtil.java index faa48fd..f343a8a 100755 --- a/src/main/java/com/qingqiu/interview/common/utils/TreeUtil.java +++ b/src/main/java/com/qingqiu/interview/common/utils/TreeUtil.java @@ -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 { } } } + } diff --git a/src/main/java/com/qingqiu/interview/controller/DashboardController.java b/src/main/java/com/qingqiu/interview/controller/DashboardController.java index 36ac9b3..9881e0e 100755 --- a/src/main/java/com/qingqiu/interview/controller/DashboardController.java +++ b/src/main/java/com/qingqiu/interview/controller/DashboardController.java @@ -12,7 +12,7 @@ import org.springframework.web.bind.annotation.RestController; * 仪表盘数据统计接口 */ @RestController -@RequestMapping("/api/v1/dashboard") +@RequestMapping("/dashboard") @RequiredArgsConstructor public class DashboardController { diff --git a/src/main/java/com/qingqiu/interview/controller/InterviewController.java b/src/main/java/com/qingqiu/interview/controller/InterviewController.java index acb9056..5fa1e41 100755 --- a/src/main/java/com/qingqiu/interview/controller/InterviewController.java +++ b/src/main/java/com/qingqiu/interview/controller/InterviewController.java @@ -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 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); } /** diff --git a/src/main/java/com/qingqiu/interview/controller/InterviewQuestionProgressController.java b/src/main/java/com/qingqiu/interview/controller/InterviewQuestionProgressController.java index 31356dd..69cfa78 100755 --- a/src/main/java/com/qingqiu/interview/controller/InterviewQuestionProgressController.java +++ b/src/main/java/com/qingqiu/interview/controller/InterviewQuestionProgressController.java @@ -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 { diff --git a/src/main/java/com/qingqiu/interview/controller/QuestionCategoryController.java b/src/main/java/com/qingqiu/interview/controller/QuestionCategoryController.java index 86113fe..7ae6143 100755 --- a/src/main/java/com/qingqiu/interview/controller/QuestionCategoryController.java +++ b/src/main/java/com/qingqiu/interview/controller/QuestionCategoryController.java @@ -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> getTreeList() { - try { - List list = questionCategoryService.getTreeList(); - return R.success(list); - } catch (Exception e) { - log.error("获取分类树列表失败", e); - return R.error("获取分类树列表失败"); - } + List list = questionCategoryService.getTreeList(); + return R.success(list); + } + + @GetMapping("/question-tree-list") + public R> getQuestionTreeList() { +// List list = questionCategoryService.getQuestionTreeList(); + return R.success(); } /** @@ -98,10 +101,10 @@ public class QuestionCategoryController { /** * 更新分类 */ - @PutMapping("/{id}") - public R update(@PathVariable Long id, @Validated @RequestBody QuestionCategoryDTO dto) { + @PostMapping("/update") + public R update(@RequestBody QuestionCategoryDTO dto) { try { - questionCategoryService.updateCategory(id, dto); + questionCategoryService.updateCategory(dto); return R.success(); } catch (RuntimeException e) { log.error("更新分类失败", e); diff --git a/src/main/java/com/qingqiu/interview/controller/QuestionController.java b/src/main/java/com/qingqiu/interview/controller/QuestionController.java index 1482b63..21f85ba 100755 --- a/src/main/java/com/qingqiu/interview/controller/QuestionController.java +++ b/src/main/java/com/qingqiu/interview/controller/QuestionController.java @@ -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> getTreeListCategory(@RequestBody QuestionOptionsDTO dto) { + return R.success(questionService.getTreeListCategory(dto)); + } } diff --git a/src/main/java/com/qingqiu/interview/dto/InterviewStartRequest.java b/src/main/java/com/qingqiu/interview/dto/InterviewStartRequest.java index 7c39425..f3a9e00 100755 --- a/src/main/java/com/qingqiu/interview/dto/InterviewStartRequest.java +++ b/src/main/java/com/qingqiu/interview/dto/InterviewStartRequest.java @@ -1,14 +1,22 @@ 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 { @NotBlank(message = "候选人姓名不能为空") private String candidateName; - + + + private List selectedNodes; + + @NotBlank(message = "面试类型不能为空") + private String model; // 简历文件通过MultipartFile单独传递 diff --git a/src/main/java/com/qingqiu/interview/dto/QuestionCategoryDTO.java b/src/main/java/com/qingqiu/interview/dto/QuestionCategoryDTO.java index 3412196..2f8ee66 100755 --- a/src/main/java/com/qingqiu/interview/dto/QuestionCategoryDTO.java +++ b/src/main/java/com/qingqiu/interview/dto/QuestionCategoryDTO.java @@ -27,6 +27,10 @@ public class QuestionCategoryDTO { @NotNull(message = "状态不能为空") private Integer state; + private String ancestor; + + private Integer level; + /** * 父分类名称(用于前端显示) */ diff --git a/src/main/java/com/qingqiu/interview/dto/QuestionOptionsDTO.java b/src/main/java/com/qingqiu/interview/dto/QuestionOptionsDTO.java new file mode 100644 index 0000000..27a7d42 --- /dev/null +++ b/src/main/java/com/qingqiu/interview/dto/QuestionOptionsDTO.java @@ -0,0 +1,17 @@ +package com.qingqiu.interview.dto; + +import lombok.Data; + +import java.util.List; + + + +@Data +public class QuestionOptionsDTO { + + /** 分类id */ + private List categoryIds; + /** 难度 */ + private String difficulty; + +} diff --git a/src/main/java/com/qingqiu/interview/dto/QuestionPageParams.java b/src/main/java/com/qingqiu/interview/dto/QuestionPageParams.java index d62ec7c..f5d8528 100755 --- a/src/main/java/com/qingqiu/interview/dto/QuestionPageParams.java +++ b/src/main/java/com/qingqiu/interview/dto/QuestionPageParams.java @@ -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; } diff --git a/src/main/java/com/qingqiu/interview/entity/Question.java b/src/main/java/com/qingqiu/interview/entity/Question.java index a0d7e29..985fe23 100755 --- a/src/main/java/com/qingqiu/interview/entity/Question.java +++ b/src/main/java/com/qingqiu/interview/entity/Question.java @@ -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; diff --git a/src/main/java/com/qingqiu/interview/entity/QuestionCategory.java b/src/main/java/com/qingqiu/interview/entity/QuestionCategory.java index 4dcf849..bfd90c3 100755 --- a/src/main/java/com/qingqiu/interview/entity/QuestionCategory.java +++ b/src/main/java/com/qingqiu/interview/entity/QuestionCategory.java @@ -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; + /** *

* 题型分类 @@ -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; /** diff --git a/src/main/java/com/qingqiu/interview/mapper/QuestionCategoryMapper.java b/src/main/java/com/qingqiu/interview/mapper/QuestionCategoryMapper.java index a00648e..2b02a3c 100755 --- a/src/main/java/com/qingqiu/interview/mapper/QuestionCategoryMapper.java +++ b/src/main/java/com/qingqiu/interview/mapper/QuestionCategoryMapper.java @@ -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; /** *

@@ -12,5 +15,5 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper; * @since 2025-09-08 */ public interface QuestionCategoryMapper extends BaseMapper { - + List batchFindByAncestorIdsUnion(@Param("searchIds") List searchIds); } diff --git a/src/main/java/com/qingqiu/interview/mapper/QuestionMapper.java b/src/main/java/com/qingqiu/interview/mapper/QuestionMapper.java index 019dde9..b5b995e 100755 --- a/src/main/java/com/qingqiu/interview/mapper/QuestionMapper.java +++ b/src/main/java/com/qingqiu/interview/mapper/QuestionMapper.java @@ -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 selectByContent(@Param("content") String content); - List countByCategory(); + List countByCategory(); + + Page queryPage(@Param("page") Page page, @Param("params") QuestionPageParams params); + } diff --git a/src/main/java/com/qingqiu/interview/service/IQuestionCategoryService.java b/src/main/java/com/qingqiu/interview/service/IQuestionCategoryService.java index 16a5f42..6678500 100755 --- a/src/main/java/com/qingqiu/interview/service/IQuestionCategoryService.java +++ b/src/main/java/com/qingqiu/interview/service/IQuestionCategoryService.java @@ -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 { /** * 更新分类 */ - void updateCategory(Long id, QuestionCategoryDTO dto); + void updateCategory(QuestionCategoryDTO dto); /** * 删除分类 @@ -64,43 +62,14 @@ public interface IQuestionCategoryService extends IService { */ List searchByName(String name); - /** - * 获取某个分类的所有子孙分类 - */ - List 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 batchFindByAncestorIdsUnion(List searchIds); - /** - * 获取指定层级的分类 - */ - List getCategoriesByLevel(Integer level); - /** - * 获取启用的分类树 - */ - List getEnabledTreeList(); - - /** - * 根据父ID获取子分类 - */ - List getChildrenByParentId(Long parentId); } diff --git a/src/main/java/com/qingqiu/interview/service/QuestionClassificationService.java b/src/main/java/com/qingqiu/interview/service/QuestionClassificationService.java index d7e2ac2..f4881fb 100755 --- a/src/main/java/com/qingqiu/interview/service/QuestionClassificationService.java +++ b/src/main/java/com/qingqiu/interview/service/QuestionClassificationService.java @@ -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 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); diff --git a/src/main/java/com/qingqiu/interview/service/QuestionService.java b/src/main/java/com/qingqiu/interview/service/QuestionService.java index eae9abc..670a6d4 100755 --- a/src/main/java/com/qingqiu/interview/service/QuestionService.java +++ b/src/main/java/com/qingqiu/interview/service/QuestionService.java @@ -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 { - private final QuestionMapper questionMapper; - private final QuestionClassificationService classificationService; - private final List documentParserList; // This will be injected by Spring - private final LlmService llmService; + Page getQuestionPage(QuestionPageParams params); - /** - * 分页查询题库 - */ - public Page getQuestionPage(QuestionPageParams params) { - log.info("分页查询题库,当前页: {}, 每页数量: {}", params.getCurrent(), params.getSize()); - return questionMapper.selectPage( - Page.of(params.getCurrent(), params.getSize()), - new LambdaQueryWrapper() - .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 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 questions = questionMapper.selectList( - new LambdaQueryWrapper() - .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 list = questionsIds.toList(Long.class); - questionMapper.delete( - new LambdaQueryWrapper() - .notIn(Question::getId, list) - ); - } - - @NotNull - private static String getPrompt(List 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 getTreeListCategory(QuestionOptionsDTO dto); } diff --git a/src/main/java/com/qingqiu/interview/service/impl/QuestionCategoryServiceImpl.java b/src/main/java/com/qingqiu/interview/service/impl/QuestionCategoryServiceImpl.java index 22b7468..d07674c 100755 --- a/src/main/java/com/qingqiu/interview/service/impl/QuestionCategoryServiceImpl.java +++ b/src/main/java/com/qingqiu/interview/service/impl/QuestionCategoryServiceImpl.java @@ -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 implements IQuestionCategoryService { + + private final QuestionService questionService; + @Override public List getTreeList() { List allCategories = getAllValidCategories(); @@ -76,21 +84,21 @@ public class QuestionCategoryServiceImpl extends ServiceImpl categoryIdsToDelete = getAllCategoryIdsToDelete(id); + + if (CollectionUtil.isEmpty(categoryIdsToDelete)) { + return; } - checkChildrenExists(id); - removeById(id); + // 2. 删除所有相关分类 + this.removeByIds(categoryIdsToDelete); - log.info("删除分类成功:{}", id); + // 3. 删除关联的题目数据 + questionService.remove( + new LambdaQueryWrapper() + .in(Question::getCategoryId, categoryIdsToDelete) + ); } @Override @@ -173,42 +187,6 @@ public class QuestionCategoryServiceImpl extends ServiceImpl getAllDescendants(Long parentId) { - List allCategories = getAllValidCategories(); - return findDescendants(allCategories, parentId); - } - - @Override - @Transactional(rollbackFor = Exception.class) - public void batchUpdateState(Long parentId, Integer state) { - List 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 allCategories = getAllValidCategories(); - Map categoryMap = allCategories.stream() - .collect(Collectors.toMap(QuestionCategory::getId, category -> category)); - - List 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 batchFindByAncestorIdsUnion(List searchIds) { + return baseMapper.batchFindByAncestorIdsUnion(searchIds); } - @Override - public List getCategoriesByLevel(Integer level) { - List allCategories = getAllValidCategories(); - - return allCategories.stream() - .filter(category -> level.equals(category.getLevel())) - .sorted(Comparator.comparingInt(QuestionCategory::getSort)) - .collect(Collectors.toList()); - } - - @Override - public List getEnabledTreeList() { - List allCategories = getAllValidCategories(); - - List enabledCategories = allCategories.stream() - .filter(category -> CommonStateEnum.ENABLED.getCode().equals(category.getState())) - .collect(Collectors.toList()); - - return buildCategoryTree(enabledCategories); - } - - @Override - public List getChildrenByParentId(Long parentId) { - List allCategories = getAllValidCategories(); - - return allCategories.stream() - .filter(category -> parentId.equals(category.getParentId())) - .sorted(Comparator.comparingInt(QuestionCategory::getSort)) - .collect(Collectors.toList()); - } // ============ 私有方法 ============ private List getAllValidCategories() { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(QuestionCategory::getDeleted, CommonConstant.ZERO) - .orderByAsc(QuestionCategory::getSort); - return list(wrapper); +// LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); +// wrapper.eq(QuestionCategory::getDeleted, CommonConstant.ZERO) +// .orderByAsc(QuestionCategory::getSort); + return list( + new LambdaQueryWrapper() + .orderByDesc(QuestionCategory::getSort) + .orderByDesc(QuestionCategory::getCreatedTime) + ); } private List buildCategoryTree(List categories) { @@ -361,16 +280,6 @@ public class QuestionCategoryServiceImpl extends ServiceImpl allCategories = getAllValidCategories(); - boolean hasChildren = allCategories.stream() - .anyMatch(category -> parentId.equals(category.getParentId())); - - if (hasChildren) { - throw new RuntimeException("存在子分类,无法删除"); - } - } - private List findDescendants(List allCategories, Long parentId) { List descendants = new ArrayList<>(); @@ -384,4 +293,29 @@ public class QuestionCategoryServiceImpl extends ServiceImpl getAllCategoryIdsToDelete(Long parentId) { + List result = new ArrayList<>(); + result.add(parentId); + + // 查找直接子分类 + List children = this.list( + new LambdaQueryWrapper() + .eq(QuestionCategory::getParentId, parentId) + ); + + // 递归查找所有子分类 + for (QuestionCategory child : children) { + result.addAll(getAllCategoryIdsToDelete(child.getId())); + } + + return result; + } } diff --git a/src/main/java/com/qingqiu/interview/service/impl/QuestionServiceImpl.java b/src/main/java/com/qingqiu/interview/service/impl/QuestionServiceImpl.java new file mode 100644 index 0000000..69241be --- /dev/null +++ b/src/main/java/com/qingqiu/interview/service/impl/QuestionServiceImpl.java @@ -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 implements QuestionService { + private final QuestionMapper questionMapper; + private final QuestionClassificationService classificationService; + private final List documentParserList; // This will be injected by Spring + private final LlmService llmService; + private final IQuestionCategoryService questionCategoryService; + + /** + * 分页查询题库 + */ + @Override + public Page 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 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 questions = questionMapper.selectList( + new LambdaQueryWrapper() + .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 list = questionsIds.toList(Long.class); + questionMapper.delete( + new LambdaQueryWrapper() + .notIn(Question::getId, list) + ); + } + + + @NotNull + private static String getPrompt(List 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 getTreeListCategory(QuestionOptionsDTO dto) { + if (StringUtils.isNoneBlank(dto.getDifficulty()) && dto.getDifficulty().equals("ALL")) { + dto.setDifficulty(null); + } + + // 获取分类树列表 + List treeList = questionCategoryService.getTreeList(); + List 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 questionList = list( + new LambdaQueryWrapper() + .in(CollectionUtil.isNotEmpty(dto.getCategoryIds()), Question::getCategoryId, + dto.getCategoryIds()) + .eq(StringUtils.isNotBlank(dto.getDifficulty()), Question::getDifficulty, dto.getDifficulty()) + .eq(Question::getDeleted, 0) + ); + + // 转换为VO对象并整合题目数据 + List 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 convertToVOListWithQuestions( + List categoryList, + List questionList) { + + if (CollectionUtil.isEmpty(categoryList)) { + return List.of(); + } + + // 按分类ID分组题目数据 + Map> 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> 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 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 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 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; + } + +} diff --git a/src/main/java/com/qingqiu/interview/service/llm/LlmService.java b/src/main/java/com/qingqiu/interview/service/llm/LlmService.java index ebf288d..21cf61a 100755 --- a/src/main/java/com/qingqiu/interview/service/llm/LlmService.java +++ b/src/main/java/com/qingqiu/interview/service/llm/LlmService.java @@ -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); } diff --git a/src/main/java/com/qingqiu/interview/service/llm/qwen/QwenService.java b/src/main/java/com/qingqiu/interview/service/llm/qwen/QwenService.java index 9dacdb2..a9cf224 100755 --- a/src/main/java/com/qingqiu/interview/service/llm/qwen/QwenService.java +++ b/src/main/java/com/qingqiu/interview/service/llm/qwen/QwenService.java @@ -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 aiSessionLogs = aiSessionLogMapper.selectList( new LambdaQueryWrapper() @@ -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); -// } } /** diff --git a/src/main/java/com/qingqiu/interview/vo/QuestionAndCategoryTreeListVO.java b/src/main/java/com/qingqiu/interview/vo/QuestionAndCategoryTreeListVO.java new file mode 100644 index 0000000..d9228ff --- /dev/null +++ b/src/main/java/com/qingqiu/interview/vo/QuestionAndCategoryTreeListVO.java @@ -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 children; + + private Integer count; + +} diff --git a/src/main/resources/mapper/QuestionCategoryMapper.xml b/src/main/resources/mapper/QuestionCategoryMapper.xml index 972cbf7..5f9254d 100755 --- a/src/main/resources/mapper/QuestionCategoryMapper.xml +++ b/src/main/resources/mapper/QuestionCategoryMapper.xml @@ -2,4 +2,16 @@ + diff --git a/src/main/resources/mapper/QuestionMapper.xml b/src/main/resources/mapper/QuestionMapper.xml index be5f22e..138feda 100755 --- a/src/main/resources/mapper/QuestionMapper.xml +++ b/src/main/resources/mapper/QuestionMapper.xml @@ -5,14 +5,14 @@ SELECT * FROM question - WHERE category IN + WHERE category_name IN #{category} @@ -40,11 +40,34 @@ + + \ No newline at end of file