feat(复习):标准思维导图生成与回忆对比
- 新增 review_standard_mind_maps / review_recall_records 数据表 - BuiltinMindMapGenerator:从报告/残片/应用场景规则生成标准导图 - MindMapAiClient 接口 + RemoteAiMindMapClient 预留 - StandardMindMapService:生成/编辑/重新生成/回忆对比 - 对比算法:标题归一化 + Bigram Jaccard 模糊匹配 - ReviewController 新增 6 个端点 - MindMapTreeTool / MindMapNode / CompareResult 工具类 - 完整单元测试(10 个,全部通过) - 更新 docs/review-module-design.md
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
package com.guo.learningprogresstracker.service;
|
||||
|
||||
import com.guo.learningprogresstracker.entity.StudyReportFragmentsEntity;
|
||||
import com.guo.learningprogresstracker.entity.StudyReportsEntity;
|
||||
import com.guo.learningprogresstracker.entity.TaskApplicationEntity;
|
||||
import com.guo.learningprogresstracker.entity.TaskEntity;
|
||||
import com.guo.learningprogresstracker.utils.MindMapNode;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 思维导图 AI 生成客户端抽象。
|
||||
* <p>后续接入真实 AI 时实现此接口,通过 Spring {@code @Profile} / {@code @ConditionalOnProperty} 切换。
|
||||
* 当前由内置规则生成器 {@code BuiltinMindMapGenerator} 充当。</p>
|
||||
*/
|
||||
public interface MindMapAiClient {
|
||||
|
||||
/**
|
||||
* 是否可用(例如:API Key 已配置)
|
||||
*/
|
||||
boolean isAvailable();
|
||||
|
||||
/**
|
||||
* 根据学习数据生成标准思维导图根节点
|
||||
*
|
||||
* @param task 学习任务
|
||||
* @param reports 该任务的全部学习报告
|
||||
* @param fragments 该任务的全部学习残片
|
||||
* @param applications 该任务的应用场景(可选)
|
||||
* @param clientHint 前端已有的大纲文本(可选,用于 AI 续写而非全量生成)
|
||||
* @return 标准思维导图的根节点;若无可生成数据则返回 {@link Optional#empty()}
|
||||
*/
|
||||
Optional<MindMapNode> generate(TaskEntity task,
|
||||
List<StudyReportsEntity> reports,
|
||||
List<StudyReportFragmentsEntity> fragments,
|
||||
List<TaskApplicationEntity> applications,
|
||||
String clientHint);
|
||||
|
||||
/**
|
||||
* 获取生成器标识(如 {@code BUILTIN} / {@code AI-4.5})
|
||||
*/
|
||||
String generatorName();
|
||||
}
|
||||
@@ -2,10 +2,7 @@ package com.guo.learningprogresstracker.service;
|
||||
|
||||
import com.guo.learningprogresstracker.dto.ReviewFeedItem;
|
||||
import com.guo.learningprogresstracker.dto.ReviewTaskStats;
|
||||
import com.guo.learningprogresstracker.dto.request.CreateReviewApplicationRequest;
|
||||
import com.guo.learningprogresstracker.dto.request.UpdateReviewApplicationRequest;
|
||||
import com.guo.learningprogresstracker.dto.request.UpsertReviewMindMapRequest;
|
||||
import com.guo.learningprogresstracker.entity.ReviewApplicationEntity;
|
||||
import com.guo.learningprogresstracker.entity.ReviewMindMapEntity;
|
||||
import com.guo.learningprogresstracker.entity.StudyReportFragmentsEntity;
|
||||
import com.guo.learningprogresstracker.entity.StudyReportsEntity;
|
||||
@@ -51,26 +48,6 @@ public interface ReviewService {
|
||||
*/
|
||||
StudyReportFragmentsEntity getFragmentDetail(int id) throws NotFindEntitiesException;
|
||||
|
||||
/**
|
||||
* 获取指定任务的应用场景列表
|
||||
*/
|
||||
List<ReviewApplicationEntity> getApplications(String taskNum) throws NotFindEntitiesException;
|
||||
|
||||
/**
|
||||
* 创建应用场景
|
||||
*/
|
||||
ReviewApplicationEntity createApplication(CreateReviewApplicationRequest request) throws NotFindEntitiesException;
|
||||
|
||||
/**
|
||||
* 更新应用场景
|
||||
*/
|
||||
ReviewApplicationEntity updateApplication(Integer id, UpdateReviewApplicationRequest request) throws NotFindEntitiesException;
|
||||
|
||||
/**
|
||||
* 删除应用场景
|
||||
*/
|
||||
void deleteApplication(Integer id) throws NotFindEntitiesException;
|
||||
|
||||
/**
|
||||
* 获取指定任务的思维导图
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.guo.learningprogresstracker.service;
|
||||
|
||||
import com.guo.learningprogresstracker.entity.ReviewRecallRecordEntity;
|
||||
import com.guo.learningprogresstracker.entity.ReviewStandardMindMapEntity;
|
||||
import com.guo.learningprogresstracker.exception.NotFindEntitiesException;
|
||||
import com.guo.learningprogresstracker.exception.OperationFailedException;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 标准思维导图服务。
|
||||
* <p>负责标准导图的按需生成、查询、用户编辑,以及用户回忆导图的对比。</p>
|
||||
*/
|
||||
public interface StandardMindMapService {
|
||||
|
||||
/**
|
||||
* 获取指定任务的标准思维导图,不存在时自动生成。
|
||||
*/
|
||||
ReviewStandardMindMapEntity getOrGenerate(String taskNum) throws NotFindEntitiesException, OperationFailedException;
|
||||
|
||||
/**
|
||||
* 强制重新生成指定任务的标准思维导图。
|
||||
*/
|
||||
ReviewStandardMindMapEntity regenerate(String taskNum) throws NotFindEntitiesException, OperationFailedException;
|
||||
|
||||
/**
|
||||
* 用户手动编辑标准思维导图(用缩进大纲文本替换)。
|
||||
*/
|
||||
ReviewStandardMindMapEntity updateByOutline(String taskNum, String outline) throws NotFindEntitiesException;
|
||||
|
||||
/**
|
||||
* 用户提交回忆大纲,与标准导图对比并返回结果。
|
||||
*
|
||||
* @param recallOutline 用户回忆的缩进大纲文本
|
||||
* @return 对比记录实体(compareResult 字段包含对比树 JSON)
|
||||
*/
|
||||
ReviewStandardMindMapEntity recallCompare(String taskNum, String recallOutline) throws NotFindEntitiesException, OperationFailedException;
|
||||
|
||||
/**
|
||||
* 获取该任务的所有回忆对比记录。
|
||||
*/
|
||||
List<ReviewRecallRecordEntity> listRecallRecords(String taskNum) throws NotFindEntitiesException;
|
||||
|
||||
/**
|
||||
* 获取单条对比记录详情。
|
||||
*/
|
||||
ReviewRecallRecordEntity getRecallRecord(Integer recordId) throws NotFindEntitiesException;
|
||||
}
|
||||
@@ -3,12 +3,17 @@ package com.guo.learningprogresstracker.service;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.guo.learningprogresstracker.dto.TaskInfo;
|
||||
import com.guo.learningprogresstracker.dto.request.CreateTaskApplicationRequest;
|
||||
import com.guo.learningprogresstracker.dto.request.TaskRequest;
|
||||
import com.guo.learningprogresstracker.dto.request.UpdateTaskApplicationRequest;
|
||||
import com.guo.learningprogresstracker.dto.response.TaskInfoResponse;
|
||||
import com.guo.learningprogresstracker.entity.TaskApplicationEntity;
|
||||
import com.guo.learningprogresstracker.entity.TaskEntity;
|
||||
import com.guo.learningprogresstracker.exception.ErrorParameterException;
|
||||
import com.guo.learningprogresstracker.exception.NotFindEntitiesException;
|
||||
|
||||
import java.rmi.ServerException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author guo
|
||||
@@ -26,4 +31,12 @@ public interface TasksService extends IService<TaskEntity> {
|
||||
void updateTask(String taskId, TaskRequest updatedTask);
|
||||
|
||||
void deleteTask(String taskId) throws ServerException;
|
||||
|
||||
List<TaskApplicationEntity> getApplications(String taskNum) throws NotFindEntitiesException;
|
||||
|
||||
TaskApplicationEntity createApplication(CreateTaskApplicationRequest request) throws NotFindEntitiesException;
|
||||
|
||||
TaskApplicationEntity updateApplication(Integer id, UpdateTaskApplicationRequest request) throws NotFindEntitiesException;
|
||||
|
||||
void deleteApplication(Integer id) throws NotFindEntitiesException;
|
||||
}
|
||||
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
package com.guo.learningprogresstracker.service.impl;
|
||||
|
||||
import com.guo.learningprogresstracker.entity.StudyReportFragmentsEntity;
|
||||
import com.guo.learningprogresstracker.entity.StudyReportsEntity;
|
||||
import com.guo.learningprogresstracker.entity.TaskApplicationEntity;
|
||||
import com.guo.learningprogresstracker.entity.TaskEntity;
|
||||
import com.guo.learningprogresstracker.service.MindMapAiClient;
|
||||
import com.guo.learningprogresstracker.utils.MindMapNode;
|
||||
import com.guo.learningprogresstracker.utils.MindMapTreeTool;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 内置规则引擎:从学习报告和残片提取关键词并组织为树形思维导图。
|
||||
* <p>规则策略:</p>
|
||||
* <ol>
|
||||
* <li>根节点 = 任务名称</li>
|
||||
* <li>一级分支 = 按会话(日期+报告摘要)分组</li>
|
||||
* <li>二级分支 = 该会话下的残片标题</li>
|
||||
* <li>附加分支"应用场景" = 任务应用场景(如有)</li>
|
||||
* <li>去重:标准化后标题对比,合并内容相似的节点</li>
|
||||
* <li>引用追溯:每个节点携带 sourceType / sourceId</li>
|
||||
* </ol>
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class BuiltinMindMapGenerator implements MindMapAiClient {
|
||||
|
||||
private static final int MAX_TITLE_LENGTH = 60;
|
||||
|
||||
@Override
|
||||
public boolean isAvailable() {
|
||||
return true; // 始终可用
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generatorName() {
|
||||
return "BUILTIN";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<MindMapNode> generate(TaskEntity task,
|
||||
List<StudyReportsEntity> reports,
|
||||
List<StudyReportFragmentsEntity> fragments,
|
||||
List<TaskApplicationEntity> applications,
|
||||
String clientHint) {
|
||||
if (reports.isEmpty() && fragments.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
MindMapNode root = new MindMapNode(task.getTaskName() != null ? task.getTaskName() : "学习任务");
|
||||
root.setNotes(task.getTaskDescription() != null ? task.getTaskDescription() : "");
|
||||
|
||||
// 按 session 分组
|
||||
Map<String, List<StudyReportsEntity>> reportsBySession = reports.stream()
|
||||
.collect(Collectors.groupingBy(StudyReportsEntity::getSessionNum));
|
||||
Map<String, List<StudyReportFragmentsEntity>> fragmentsBySession = fragments.stream()
|
||||
.collect(Collectors.groupingBy(StudyReportFragmentsEntity::getSessionNum));
|
||||
|
||||
// 合并所有 session
|
||||
Set<String> allSessions = new LinkedHashSet<>();
|
||||
allSessions.addAll(reportsBySession.keySet());
|
||||
allSessions.addAll(fragmentsBySession.keySet());
|
||||
|
||||
for (String sessionNum : allSessions) {
|
||||
List<StudyReportsEntity> sessReports = reportsBySession.getOrDefault(sessionNum, List.of());
|
||||
List<StudyReportFragmentsEntity> sessFragments = fragmentsBySession.getOrDefault(sessionNum, List.of());
|
||||
|
||||
// 会话分支标题:取第一条报告的前 60 字作为摘要,或直接写"学习记录"
|
||||
String sessionTitle;
|
||||
if (!sessReports.isEmpty()) {
|
||||
String firstReport = sessReports.get(0).getContent();
|
||||
sessionTitle = truncate(firstReport, MAX_TITLE_LENGTH);
|
||||
if (sessReports.get(0).getCreatedTime() != null) {
|
||||
sessionTitle = formatDate(sessReports.get(0).getCreatedTime()) + " " + sessionTitle;
|
||||
}
|
||||
} else {
|
||||
sessionTitle = "学习记录 " + (sessFragments.isEmpty() ? "" : formatDate(sessFragments.get(0).getCreatedTime()));
|
||||
}
|
||||
|
||||
MindMapNode sessionNode = new MindMapNode(sessionTitle);
|
||||
|
||||
// 报告作为子节点
|
||||
for (StudyReportsEntity report : sessReports) {
|
||||
String content = report.getContent();
|
||||
if (content == null || content.isBlank()) continue;
|
||||
MindMapNode reportNode = new MindMapNode(truncate(content, MAX_TITLE_LENGTH));
|
||||
reportNode.setNotes(content);
|
||||
reportNode.setSourceType("REPORT");
|
||||
reportNode.setSourceId(report.getId());
|
||||
sessionNode.getChildren().add(reportNode);
|
||||
}
|
||||
|
||||
// 残片作为子节点
|
||||
for (StudyReportFragmentsEntity frag : sessFragments) {
|
||||
String content = frag.getContent();
|
||||
if (content == null || content.isBlank()) continue;
|
||||
MindMapNode fragNode = new MindMapNode(truncate(content, MAX_TITLE_LENGTH));
|
||||
fragNode.setNotes(content);
|
||||
fragNode.setSourceType("FRAGMENT");
|
||||
fragNode.setSourceId(frag.getId());
|
||||
sessionNode.getChildren().add(fragNode);
|
||||
}
|
||||
|
||||
root.getChildren().add(sessionNode);
|
||||
}
|
||||
|
||||
// 应用场景分支(如有)
|
||||
if (applications != null && !applications.isEmpty()) {
|
||||
MindMapNode appNode = new MindMapNode("应用场景");
|
||||
for (TaskApplicationEntity app : applications) {
|
||||
if (app.getTitle() == null || app.getTitle().isBlank()) continue;
|
||||
MindMapNode child = new MindMapNode(app.getTitle());
|
||||
child.setNotes(app.getDescription() != null ? app.getDescription() : "");
|
||||
child.setSourceType("APPLICATION");
|
||||
child.setSourceId(app.getId());
|
||||
appNode.getChildren().add(child);
|
||||
}
|
||||
if (!appNode.getChildren().isEmpty()) {
|
||||
root.getChildren().add(appNode);
|
||||
}
|
||||
}
|
||||
|
||||
// 去重:同级 title 标准化后合并
|
||||
deduplicateChildren(root);
|
||||
|
||||
log.info("BuiltinMindMapGenerator: 为任务[{}]生成导图,共 {} 个节点,{} 层",
|
||||
task.getTaskNum(), MindMapTreeTool.countNodes(root), MindMapTreeTool.maxDepth(root));
|
||||
return Optional.of(root);
|
||||
}
|
||||
|
||||
/** 同级节点按标准化标题去重 */
|
||||
private void deduplicateChildren(MindMapNode node) {
|
||||
if (node.getChildren() == null || node.getChildren().isEmpty()) return;
|
||||
|
||||
Map<String, MindMapNode> seen = new LinkedHashMap<>();
|
||||
for (MindMapNode child : node.getChildren()) {
|
||||
String key = normalize(child.getTitle());
|
||||
seen.merge(key, child, (a, b) -> {
|
||||
// 保留 notes 较长的那个
|
||||
if (b.getNotes() != null && b.getNotes().length() > (a.getNotes() != null ? a.getNotes().length() : 0)) {
|
||||
a.setNotes(b.getNotes());
|
||||
}
|
||||
// 合并子节点
|
||||
if (b.getChildren() != null) {
|
||||
a.getChildren().addAll(b.getChildren());
|
||||
}
|
||||
return a;
|
||||
});
|
||||
}
|
||||
node.setChildren(new ArrayList<>(seen.values()));
|
||||
|
||||
for (MindMapNode child : node.getChildren()) {
|
||||
deduplicateChildren(child);
|
||||
}
|
||||
}
|
||||
|
||||
private static String normalize(String s) {
|
||||
if (s == null) return "";
|
||||
return s.replaceAll("[\\s 、,。!?:;()\\[\\]{},.!?:;()\\-—/\\\\|]", "")
|
||||
.toLowerCase(Locale.ROOT)
|
||||
.trim();
|
||||
}
|
||||
|
||||
private static String truncate(String s, int maxLen) {
|
||||
if (s == null) return "";
|
||||
if (s.length() <= maxLen) return s;
|
||||
return s.substring(0, maxLen) + "…";
|
||||
}
|
||||
|
||||
private static String formatDate(LocalDateTime dt) {
|
||||
if (dt == null) return "";
|
||||
return dt.format(DateTimeFormatter.ofPattern("MM-dd"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.guo.learningprogresstracker.service.impl;
|
||||
|
||||
import com.guo.learningprogresstracker.entity.StudyReportFragmentsEntity;
|
||||
import com.guo.learningprogresstracker.entity.StudyReportsEntity;
|
||||
import com.guo.learningprogresstracker.entity.TaskApplicationEntity;
|
||||
import com.guo.learningprogresstracker.entity.TaskEntity;
|
||||
import com.guo.learningprogresstracker.service.MindMapAiClient;
|
||||
import com.guo.learningprogresstracker.utils.MindMapNode;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 远程 AI 思维导图生成客户端(预留实现)。
|
||||
* <p>通过 {@code lpt.ai.endpoint} / {@code lpt.ai.api-key} 配置;未配置时不可用,
|
||||
* 上层服务会回退到 {@link BuiltinMindMapGenerator}。</p>
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "lpt.ai")
|
||||
@Setter
|
||||
public class RemoteAiMindMapClient implements MindMapAiClient {
|
||||
|
||||
/** AI 服务端点 */
|
||||
private String endpoint;
|
||||
/** API Key */
|
||||
private String apiKey;
|
||||
/** 模型名称 */
|
||||
private String model = "claude-fable-5";
|
||||
/** 超时秒数 */
|
||||
private int timeoutSeconds = 60;
|
||||
|
||||
private boolean enabled = false;
|
||||
|
||||
@PostConstruct
|
||||
void init() {
|
||||
enabled = apiKey != null && !apiKey.isBlank() && endpoint != null && !endpoint.isBlank();
|
||||
if (enabled) {
|
||||
log.info("RemoteAiMindMapClient 已启用: endpoint={}, model={}", endpoint, model);
|
||||
} else {
|
||||
log.info("RemoteAiMindMapClient 未配置,将使用内置生成器");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAvailable() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generatorName() {
|
||||
return "AI-" + model;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<MindMapNode> generate(TaskEntity task,
|
||||
List<StudyReportsEntity> reports,
|
||||
List<StudyReportFragmentsEntity> fragments,
|
||||
List<TaskApplicationEntity> applications,
|
||||
String clientHint) {
|
||||
if (!enabled) {
|
||||
return Optional.empty();
|
||||
}
|
||||
// TODO: 调用远程 AI API,构造 prompt 发送 reports/fragments
|
||||
// RestClient.create().post().uri(endpoint).body(prompt).retrieve()
|
||||
log.warn("RemoteAiMindMapClient: 远程 AI 调用尚未实现,请配置 API Key 并实现请求逻辑");
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
@@ -4,20 +4,15 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.guo.learningprogresstracker.dto.ReviewFeedItem;
|
||||
import com.guo.learningprogresstracker.dto.ReviewTaskStats;
|
||||
import com.guo.learningprogresstracker.dto.request.CreateReviewApplicationRequest;
|
||||
import com.guo.learningprogresstracker.dto.request.UpdateReviewApplicationRequest;
|
||||
import com.guo.learningprogresstracker.dto.request.UpsertReviewMindMapRequest;
|
||||
import com.guo.learningprogresstracker.entity.ReviewApplicationEntity;
|
||||
import com.guo.learningprogresstracker.entity.ReviewMindMapEntity;
|
||||
import com.guo.learningprogresstracker.entity.StudyReportFragmentsEntity;
|
||||
import com.guo.learningprogresstracker.entity.StudyReportsEntity;
|
||||
import com.guo.learningprogresstracker.entity.StudySessionsEntity;
|
||||
import com.guo.learningprogresstracker.entity.TaskEntity;
|
||||
import com.guo.learningprogresstracker.enums.ReviewApplicationStatusEnum;
|
||||
import com.guo.learningprogresstracker.enums.ReviewMindMapFileFormatEnum;
|
||||
import com.guo.learningprogresstracker.enums.ReviewMindMapParseStatusEnum;
|
||||
import com.guo.learningprogresstracker.exception.NotFindEntitiesException;
|
||||
import com.guo.learningprogresstracker.mapper.ReviewApplicationMapper;
|
||||
import com.guo.learningprogresstracker.mapper.ReviewMindMapMapper;
|
||||
import com.guo.learningprogresstracker.mapper.StudyReportFragmentsMapper;
|
||||
import com.guo.learningprogresstracker.mapper.StudyReportsMapper;
|
||||
@@ -60,7 +55,6 @@ public class ReviewServiceImpl implements ReviewService {
|
||||
private final StudyReportFragmentsMapper studyReportFragmentsMapper;
|
||||
private final StudySessionsMapper studySessionsMapper;
|
||||
private final TasksMapper tasksMapper;
|
||||
private final ReviewApplicationMapper reviewApplicationMapper;
|
||||
private final ReviewMindMapMapper reviewMindMapMapper;
|
||||
private final MindMapFileParser mindMapFileParser = new MindMapFileParser(new ObjectMapper());
|
||||
|
||||
@@ -158,48 +152,6 @@ public class ReviewServiceImpl implements ReviewService {
|
||||
.orElseThrow(() -> new NotFindEntitiesException("学习残片[" + id + "]不存在"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ReviewApplicationEntity> getApplications(String taskNum) throws NotFindEntitiesException {
|
||||
ensureTaskExists(taskNum);
|
||||
return reviewApplicationMapper.selectList(
|
||||
Wrappers.<ReviewApplicationEntity>lambdaQuery()
|
||||
.eq(ReviewApplicationEntity::getTaskNum, taskNum)
|
||||
.orderByDesc(ReviewApplicationEntity::getLastModifiedTime)
|
||||
.orderByDesc(ReviewApplicationEntity::getCreatedTime));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReviewApplicationEntity createApplication(CreateReviewApplicationRequest request) throws NotFindEntitiesException {
|
||||
ensureTaskExists(request.getTaskNum());
|
||||
ReviewApplicationEntity entity = new ReviewApplicationEntity();
|
||||
entity.setTaskNum(request.getTaskNum());
|
||||
entity.setTitle(request.getTitle());
|
||||
entity.setDescription(request.getDescription());
|
||||
entity.setResourceUrl(request.getResourceUrl());
|
||||
entity.setStatus(normalizeApplicationStatus(request.getStatus()));
|
||||
reviewApplicationMapper.insert(entity);
|
||||
return entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReviewApplicationEntity updateApplication(Integer id, UpdateReviewApplicationRequest request) throws NotFindEntitiesException {
|
||||
ReviewApplicationEntity existing = Optional.ofNullable(reviewApplicationMapper.selectById(id))
|
||||
.orElseThrow(() -> new NotFindEntitiesException("应用场景[" + id + "]不存在"));
|
||||
existing.setTitle(request.getTitle());
|
||||
existing.setDescription(request.getDescription());
|
||||
existing.setResourceUrl(request.getResourceUrl());
|
||||
existing.setStatus(normalizeApplicationStatus(request.getStatus()));
|
||||
reviewApplicationMapper.updateById(existing);
|
||||
return existing;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteApplication(Integer id) throws NotFindEntitiesException {
|
||||
ReviewApplicationEntity existing = Optional.ofNullable(reviewApplicationMapper.selectById(id))
|
||||
.orElseThrow(() -> new NotFindEntitiesException("应用场景[" + id + "]不存在"));
|
||||
reviewApplicationMapper.deleteById(existing.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReviewMindMapEntity getMindMap(String taskNum) throws NotFindEntitiesException {
|
||||
ensureTaskExists(taskNum);
|
||||
@@ -381,10 +333,6 @@ public class ReviewServiceImpl implements ReviewService {
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeApplicationStatus(String status) {
|
||||
return ReviewApplicationStatusEnum.fromCodeOrDefault(status).getCode();
|
||||
}
|
||||
|
||||
private String normalizeMindMapFormat(String contentFormat) {
|
||||
if (!StringUtils.hasText(contentFormat)) {
|
||||
return DEFAULT_MIND_MAP_FORMAT;
|
||||
|
||||
+359
@@ -0,0 +1,359 @@
|
||||
package com.guo.learningprogresstracker.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.guo.learningprogresstracker.entity.*;
|
||||
import com.guo.learningprogresstracker.exception.NotFindEntitiesException;
|
||||
import com.guo.learningprogresstracker.exception.OperationFailedException;
|
||||
import com.guo.learningprogresstracker.mapper.*;
|
||||
import com.guo.learningprogresstracker.service.MindMapAiClient;
|
||||
import com.guo.learningprogresstracker.service.StandardMindMapService;
|
||||
import com.guo.learningprogresstracker.utils.CompareResult;
|
||||
import com.guo.learningprogresstracker.utils.MindMapNode;
|
||||
import com.guo.learningprogresstracker.utils.MindMapTreeTool;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 标准思维导图服务实现
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class StandardMindMapServiceImpl implements StandardMindMapService {
|
||||
|
||||
private final ReviewStandardMindMapMapper standardMindMapMapper;
|
||||
private final ReviewRecallRecordMapper recallRecordMapper;
|
||||
private final TasksMapper tasksMapper;
|
||||
private final StudyReportsMapper studyReportsMapper;
|
||||
private final StudyReportFragmentsMapper studyReportFragmentsMapper;
|
||||
private final TaskApplicationMapper taskApplicationMapper;
|
||||
private final StudySessionsMapper studySessionsMapper;
|
||||
|
||||
private final List<MindMapAiClient> aiClients;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
private static final String GENERATOR_BUILTIN = "BUILTIN";
|
||||
private static final String GENERATOR_USER = "USER";
|
||||
private static final String MATCH_STATUS_MATCHED = "MATCHED";
|
||||
private static final String MATCH_STATUS_MISSED = "MISSED";
|
||||
|
||||
// ============ 查询与生成 ============
|
||||
|
||||
@Override
|
||||
public ReviewStandardMindMapEntity getOrGenerate(String taskNum) throws NotFindEntitiesException, OperationFailedException {
|
||||
ensureTaskExists(taskNum);
|
||||
ReviewStandardMindMapEntity existing = queryByTaskNum(taskNum);
|
||||
return existing != null ? existing : doGenerate(taskNum);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReviewStandardMindMapEntity regenerate(String taskNum) throws NotFindEntitiesException, OperationFailedException {
|
||||
ensureTaskExists(taskNum);
|
||||
return doGenerate(taskNum);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public ReviewStandardMindMapEntity updateByOutline(String taskNum, String outline) throws NotFindEntitiesException {
|
||||
ensureTaskExists(taskNum);
|
||||
ReviewStandardMindMapEntity existing = queryByTaskNum(taskNum);
|
||||
if (existing == null) {
|
||||
existing = new ReviewStandardMindMapEntity();
|
||||
existing.setTaskNum(taskNum);
|
||||
}
|
||||
|
||||
// 解析大纲为树节点
|
||||
MindMapNode root = MindMapTreeTool.parseOutline(outline);
|
||||
String contentJson = MindMapTreeTool.toJson(root, objectMapper);
|
||||
|
||||
existing.setTitle(root.getTitle() != null ? root.getTitle() : "思维导图");
|
||||
existing.setContent(contentJson);
|
||||
existing.setOutline(outline);
|
||||
existing.setGenerator(GENERATOR_USER);
|
||||
existing.setGeneratorVersion(null);
|
||||
existing.setSummary("用户编辑,共 " + MindMapTreeTool.countNodes(root) + " 个节点");
|
||||
existing.setGeneratedTime(LocalDateTime.now());
|
||||
|
||||
if (existing.getId() == null) {
|
||||
standardMindMapMapper.insert(existing);
|
||||
} else {
|
||||
standardMindMapMapper.updateById(existing);
|
||||
}
|
||||
return existing;
|
||||
}
|
||||
|
||||
// ============ 回忆对比 ============
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public ReviewStandardMindMapEntity recallCompare(String taskNum, String recallOutline)
|
||||
throws NotFindEntitiesException, OperationFailedException {
|
||||
ensureTaskExists(taskNum);
|
||||
|
||||
// 1. 获取标准导图(自动生成)
|
||||
ReviewStandardMindMapEntity standard = getOrGenerate(taskNum);
|
||||
|
||||
// 2. 解析标准树和用户回忆树
|
||||
MindMapNode standardRoot = MindMapTreeTool.fromJson(standard.getContent(), objectMapper);
|
||||
MindMapNode recallRoot = MindMapTreeTool.parseOutline(recallOutline);
|
||||
|
||||
// 3. 执行对比
|
||||
CompareResult result = compareTrees(standardRoot, recallRoot);
|
||||
|
||||
// 4. 序列化对比结果
|
||||
String resultJson;
|
||||
try {
|
||||
resultJson = objectMapper.writeValueAsString(result);
|
||||
} catch (Exception e) {
|
||||
throw new OperationFailedException("对比结果序列化失败");
|
||||
}
|
||||
|
||||
// 5. 保存回忆记录
|
||||
ReviewRecallRecordEntity record = new ReviewRecallRecordEntity();
|
||||
record.setTaskNum(taskNum);
|
||||
record.setStandardMapId(standard.getId());
|
||||
record.setRecallContent(recallOutline);
|
||||
record.setCompareResult(resultJson);
|
||||
record.setRecallRatio(result.getRecallRatio());
|
||||
record.setMatchedCount(result.getMatchedCount());
|
||||
record.setMissedCount(result.getMissedCount());
|
||||
record.setExtraCount(result.getExtraCount());
|
||||
recallRecordMapper.insert(record);
|
||||
|
||||
log.info("回忆对比: taskNum={}, recallRatio={}, matched={}, missed={}, extra={}",
|
||||
taskNum, result.getRecallRatio(), result.getMatchedCount(),
|
||||
result.getMissedCount(), result.getExtraCount());
|
||||
|
||||
return standard;
|
||||
}
|
||||
|
||||
// ============ 对比算法核心 ============
|
||||
|
||||
/**
|
||||
* 两颗树的节点级对比算法。
|
||||
*/
|
||||
CompareResult compareTrees(MindMapNode standardRoot, MindMapNode recallRoot) {
|
||||
CompareResult result = new CompareResult();
|
||||
|
||||
// 展平标准树
|
||||
List<MindMapNode> standardFlat = MindMapTreeTool.flatten(standardRoot);
|
||||
// 展平回忆树(排除根节点本身)
|
||||
List<MindMapNode> recallFlat = MindMapTreeTool.flatten(recallRoot).stream()
|
||||
.filter(n -> n != recallRoot)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 构建回忆节点标题 → 节点映射(标准化后)
|
||||
Map<String, MindMapNode> recallTitleMap = new LinkedHashMap<>();
|
||||
for (MindMapNode node : recallFlat) {
|
||||
recallTitleMap.merge(normalize(node.getTitle()), node, (a, b) -> a);
|
||||
}
|
||||
|
||||
// 标注标准树
|
||||
int matched = 0, missed = 0;
|
||||
for (MindMapNode node : standardFlat) {
|
||||
if (node == standardRoot) continue; // 跳过根节点
|
||||
String key = normalize(node.getTitle());
|
||||
boolean found = recallTitleMap.containsKey(key);
|
||||
if (found) {
|
||||
node.setNotes(MATCH_STATUS_MATCHED + "|" + (node.getNotes() != null ? node.getNotes() : ""));
|
||||
matched++;
|
||||
} else {
|
||||
// 尝试模糊匹配
|
||||
found = fuzzyMatch(node.getTitle(), recallTitleMap);
|
||||
if (found) {
|
||||
node.setNotes(MATCH_STATUS_MATCHED + "|" + (node.getNotes() != null ? node.getNotes() : ""));
|
||||
matched++;
|
||||
} else {
|
||||
node.setNotes(MATCH_STATUS_MISSED + "|" + (node.getNotes() != null ? node.getNotes() : ""));
|
||||
missed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 找出额外节点(用户在回忆中新增的、标准树中没有的)
|
||||
Set<String> standardNormTitles = standardFlat.stream()
|
||||
.map(n -> normalize(n.getTitle()))
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
List<CompareResult.FlatNode> extraNodes = new ArrayList<>();
|
||||
for (MindMapNode node : recallFlat) {
|
||||
String key = normalize(node.getTitle());
|
||||
if (!standardNormTitles.contains(key)) {
|
||||
CompareResult.FlatNode flat = new CompareResult.FlatNode();
|
||||
flat.setTitle(node.getTitle());
|
||||
flat.setPath(node.getTitle()); // 简化路径
|
||||
extraNodes.add(flat);
|
||||
}
|
||||
}
|
||||
|
||||
int total = matched + missed;
|
||||
result.setMatchedTree(standardRoot);
|
||||
result.setExtraNodes(extraNodes);
|
||||
result.setMatchedCount(matched);
|
||||
result.setMissedCount(missed);
|
||||
result.setExtraCount(extraNodes.size());
|
||||
result.setRecallRatio(total > 0 ? (double) matched / total : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ============ 回忆记录查询 ============
|
||||
|
||||
@Override
|
||||
public List<ReviewRecallRecordEntity> listRecallRecords(String taskNum) throws NotFindEntitiesException {
|
||||
ensureTaskExists(taskNum);
|
||||
return recallRecordMapper.selectList(
|
||||
Wrappers.<ReviewRecallRecordEntity>lambdaQuery()
|
||||
.eq(ReviewRecallRecordEntity::getTaskNum, taskNum)
|
||||
.orderByDesc(ReviewRecallRecordEntity::getCreatedTime));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReviewRecallRecordEntity getRecallRecord(Integer recordId) throws NotFindEntitiesException {
|
||||
return Optional.ofNullable(recallRecordMapper.selectById(recordId))
|
||||
.orElseThrow(() -> new NotFindEntitiesException("回忆记录[" + recordId + "]不存在"));
|
||||
}
|
||||
|
||||
// ============ 内部方法 ============
|
||||
|
||||
private ReviewStandardMindMapEntity doGenerate(String taskNum) throws OperationFailedException {
|
||||
TaskEntity task = tasksMapper.selectOne(
|
||||
Wrappers.<TaskEntity>lambdaQuery().eq(TaskEntity::getTaskNum, taskNum).last("LIMIT 1"));
|
||||
|
||||
if (task == null) {
|
||||
throw new OperationFailedException("任务[" + taskNum + "]不存在");
|
||||
}
|
||||
|
||||
// 收集学习数据
|
||||
List<String> sessionNums = studySessionsMapper.selectList(
|
||||
Wrappers.<StudySessionsEntity>lambdaQuery()
|
||||
.eq(StudySessionsEntity::getTaskNum, taskNum)
|
||||
.select(StudySessionsEntity::getSessionNum))
|
||||
.stream().map(StudySessionsEntity::getSessionNum).collect(Collectors.toList());
|
||||
|
||||
List<StudyReportsEntity> reports = sessionNums.isEmpty() ? List.of()
|
||||
: studyReportsMapper.selectList(Wrappers.<StudyReportsEntity>lambdaQuery()
|
||||
.in(StudyReportsEntity::getSessionNum, sessionNums));
|
||||
List<StudyReportFragmentsEntity> fragments = sessionNums.isEmpty() ? List.of()
|
||||
: studyReportFragmentsMapper.selectList(Wrappers.<StudyReportFragmentsEntity>lambdaQuery()
|
||||
.in(StudyReportFragmentsEntity::getSessionNum, sessionNums));
|
||||
List<TaskApplicationEntity> applications = taskApplicationMapper.selectList(
|
||||
Wrappers.<TaskApplicationEntity>lambdaQuery().eq(TaskApplicationEntity::getTaskNum, taskNum));
|
||||
|
||||
if (reports.isEmpty() && fragments.isEmpty()) {
|
||||
throw new OperationFailedException("任务[" + taskNum + "]没有学习报告或残片,无法生成思维导图");
|
||||
}
|
||||
|
||||
// 找可用的 AI 客户端
|
||||
MindMapAiClient client = aiClients.stream()
|
||||
.filter(MindMapAiClient::isAvailable)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
if (client == null) {
|
||||
throw new OperationFailedException("没有可用的思维导图生成器");
|
||||
}
|
||||
|
||||
Optional<MindMapNode> optRoot = client.generate(task, reports, fragments, applications, null);
|
||||
if (optRoot.isEmpty()) {
|
||||
throw new OperationFailedException("思维导图生成失败");
|
||||
}
|
||||
|
||||
MindMapNode root = optRoot.get();
|
||||
String contentJson = MindMapTreeTool.toJson(root, objectMapper);
|
||||
String outline = MindMapTreeTool.toFullOutline(root);
|
||||
int nodeCount = MindMapTreeTool.countNodes(root);
|
||||
int depth = MindMapTreeTool.maxDepth(root);
|
||||
|
||||
ReviewStandardMindMapEntity entity = queryByTaskNum(taskNum);
|
||||
boolean create = entity == null;
|
||||
if (create) {
|
||||
entity = new ReviewStandardMindMapEntity();
|
||||
entity.setTaskNum(taskNum);
|
||||
}
|
||||
|
||||
entity.setTitle(root.getTitle() != null ? root.getTitle() : "思维导图");
|
||||
entity.setContent(contentJson);
|
||||
entity.setOutline(outline);
|
||||
entity.setSummary("共 " + nodeCount + " 个节点,最大层级 " + depth);
|
||||
entity.setGenerator(client.generatorName());
|
||||
entity.setGeneratorVersion("1.0");
|
||||
entity.setSourceReportCount(reports.size());
|
||||
entity.setSourceFragmentCount(fragments.size());
|
||||
entity.setGeneratedTime(LocalDateTime.now());
|
||||
|
||||
if (create) {
|
||||
standardMindMapMapper.insert(entity);
|
||||
} else {
|
||||
standardMindMapMapper.updateById(entity);
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
private void ensureTaskExists(String taskNum) throws NotFindEntitiesException {
|
||||
if (!StringUtils.hasText(taskNum) || !tasksMapper.exists(
|
||||
Wrappers.<TaskEntity>lambdaQuery().eq(TaskEntity::getTaskNum, taskNum))) {
|
||||
throw new NotFindEntitiesException("任务[" + taskNum + "]不存在");
|
||||
}
|
||||
}
|
||||
|
||||
private ReviewStandardMindMapEntity queryByTaskNum(String taskNum) {
|
||||
return standardMindMapMapper.selectOne(
|
||||
Wrappers.<ReviewStandardMindMapEntity>lambdaQuery()
|
||||
.eq(ReviewStandardMindMapEntity::getTaskNum, taskNum)
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
|
||||
// ============ 标题归一化 ============
|
||||
|
||||
/**
|
||||
* 标准化标题用于对比:去空格、去标点、转小写。
|
||||
*/
|
||||
static String normalize(String s) {
|
||||
if (s == null) return "";
|
||||
// 先移除内部对比标记前缀,再清理标点
|
||||
String result = s.replaceAll("^(?:MATCHED|MISSED)\\|", "");
|
||||
result = result.replaceAll("[\\s 、,。!?:;()\\[\\]{},.!?:;()\\-—/\\\\|]", "");
|
||||
return result.toLowerCase(Locale.ROOT).trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 模糊匹配:计算字符 bigram Jaccard 相似度
|
||||
*/
|
||||
static boolean fuzzyMatch(String title, Map<String, MindMapNode> recallTitleMap) {
|
||||
if (title == null || title.isBlank()) return false;
|
||||
String norm = normalize(title);
|
||||
Set<String> bigrams = bigramSet(norm);
|
||||
if (bigrams.isEmpty()) return false;
|
||||
|
||||
for (String recallKey : recallTitleMap.keySet()) {
|
||||
Set<String> recallBigrams = bigramSet(recallKey);
|
||||
if (recallBigrams.isEmpty()) continue;
|
||||
// Jaccard
|
||||
Set<String> intersection = new HashSet<>(bigrams);
|
||||
intersection.retainAll(recallBigrams);
|
||||
Set<String> union = new HashSet<>(bigrams);
|
||||
union.addAll(recallBigrams);
|
||||
double similarity = (double) intersection.size() / union.size();
|
||||
if (similarity >= 0.6) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static Set<String> bigramSet(String s) {
|
||||
Set<String> set = new HashSet<>();
|
||||
for (int i = 0; i < s.length() - 1; i++) {
|
||||
set.add(s.substring(i, i + 2));
|
||||
}
|
||||
return set;
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,18 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.guo.learningprogresstracker.dto.PriorityDto;
|
||||
import com.guo.learningprogresstracker.dto.TaskInfo;
|
||||
import com.guo.learningprogresstracker.dto.request.CreateTaskApplicationRequest;
|
||||
import com.guo.learningprogresstracker.dto.request.TaskRequest;
|
||||
import com.guo.learningprogresstracker.dto.request.UpdateTaskApplicationRequest;
|
||||
import com.guo.learningprogresstracker.dto.response.TaskInfoResponse;
|
||||
import com.guo.learningprogresstracker.entity.TaskApplicationEntity;
|
||||
import com.guo.learningprogresstracker.entity.TaskEntity;
|
||||
import com.guo.learningprogresstracker.enums.TaskApplicationStatusEnum;
|
||||
import com.guo.learningprogresstracker.exception.ErrorParameterException;
|
||||
import com.guo.learningprogresstracker.exception.NotFindEntitiesException;
|
||||
import com.guo.learningprogresstracker.mapStruct.RequestConvert;
|
||||
import com.guo.learningprogresstracker.mapStruct.TaskConvert;
|
||||
import com.guo.learningprogresstracker.mapper.TaskApplicationMapper;
|
||||
import com.guo.learningprogresstracker.service.TasksService;
|
||||
import com.guo.learningprogresstracker.mapper.TasksMapper;
|
||||
import com.guo.learningprogresstracker.utils.CalculatedPriorityTool;
|
||||
@@ -18,6 +24,10 @@ import com.guo.learningprogresstracker.utils.GenerateNumTool;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 针对表【tasks(存储学习任务的基本信息,包括优先级的多维度计算)】的数据库操作Service实现
|
||||
@@ -30,6 +40,8 @@ public class TasksServiceImpl extends ServiceImpl<TasksMapper, TaskEntity>
|
||||
|
||||
private final TasksMapper tasksMapper;
|
||||
|
||||
private final TaskApplicationMapper taskApplicationMapper;
|
||||
|
||||
@Override
|
||||
public Page<TaskInfo> taskList(Integer pageNum, Integer pageSize) {
|
||||
Page<TaskEntity> page = tasksMapper.selectPage(new Page<TaskEntity>(pageNum, pageSize),
|
||||
@@ -86,4 +98,57 @@ public class TasksServiceImpl extends ServiceImpl<TasksMapper, TaskEntity>
|
||||
}
|
||||
this.removeById(taskId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TaskApplicationEntity> getApplications(String taskNum) throws NotFindEntitiesException {
|
||||
ensureTaskExists(taskNum);
|
||||
return taskApplicationMapper.selectList(
|
||||
Wrappers.<TaskApplicationEntity>lambdaQuery()
|
||||
.eq(TaskApplicationEntity::getTaskNum, taskNum)
|
||||
.orderByDesc(TaskApplicationEntity::getLastModifiedTime)
|
||||
.orderByDesc(TaskApplicationEntity::getCreatedTime));
|
||||
}
|
||||
|
||||
@Override
|
||||
public TaskApplicationEntity createApplication(CreateTaskApplicationRequest request) throws NotFindEntitiesException {
|
||||
ensureTaskExists(request.getTaskNum());
|
||||
TaskApplicationEntity entity = new TaskApplicationEntity();
|
||||
entity.setTaskNum(request.getTaskNum());
|
||||
entity.setTitle(request.getTitle());
|
||||
entity.setDescription(request.getDescription());
|
||||
entity.setResourceUrl(request.getResourceUrl());
|
||||
entity.setStatus(normalizeApplicationStatus(request.getStatus()));
|
||||
taskApplicationMapper.insert(entity);
|
||||
return entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TaskApplicationEntity updateApplication(Integer id, UpdateTaskApplicationRequest request) throws NotFindEntitiesException {
|
||||
TaskApplicationEntity existing = Optional.ofNullable(taskApplicationMapper.selectById(id))
|
||||
.orElseThrow(() -> new NotFindEntitiesException("应用场景[" + id + "]不存在"));
|
||||
existing.setTitle(request.getTitle());
|
||||
existing.setDescription(request.getDescription());
|
||||
existing.setResourceUrl(request.getResourceUrl());
|
||||
existing.setStatus(normalizeApplicationStatus(request.getStatus()));
|
||||
taskApplicationMapper.updateById(existing);
|
||||
return existing;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteApplication(Integer id) throws NotFindEntitiesException {
|
||||
TaskApplicationEntity existing = Optional.ofNullable(taskApplicationMapper.selectById(id))
|
||||
.orElseThrow(() -> new NotFindEntitiesException("应用场景[" + id + "]不存在"));
|
||||
taskApplicationMapper.deleteById(existing.getId());
|
||||
}
|
||||
|
||||
private void ensureTaskExists(String taskNum) throws NotFindEntitiesException {
|
||||
if (!StringUtils.hasText(taskNum) || !tasksMapper.exists(
|
||||
Wrappers.<TaskEntity>lambdaQuery().eq(TaskEntity::getTaskNum, taskNum))) {
|
||||
throw new NotFindEntitiesException("任务[" + taskNum + "]不存在");
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeApplicationStatus(String status) {
|
||||
return TaskApplicationStatusEnum.fromCodeOrDefault(status).getCode();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user