From f170e85297dac73268ee946a4ad16c04eaf7cc04 Mon Sep 17 00:00:00 2001
From: cat_shark <1716967236@qq.com>
Date: Sat, 20 Jun 2026 09:16:20 +0800
Subject: [PATCH] feat: complete review workflow support
---
pom.xml | 5 +
.../controller/ReviewController.java | 69 ++++++-
.../controller/TaskController.java | 2 +-
.../CreateReviewApplicationRequest.java | 20 ++
.../UpdateReviewApplicationRequest.java | 17 ++
.../request/UpsertReviewMindMapRequest.java | 16 ++
.../entity/ReviewApplicationEntity.java | 35 ++++
.../entity/ReviewMindMapEntity.java | 32 ++++
.../enums/ReviewApplicationStatusEnum.java | 35 ++++
.../mapper/ReviewApplicationMapper.java | 9 +
.../mapper/ReviewMindMapMapper.java | 9 +
.../service/ReviewService.java | 37 +++-
.../service/impl/ReviewServiceImpl.java | 142 +++++++++++++-
..._add_review_applications_and_mind_maps.sql | 36 ++++
.../DataSourceTest.java | 13 +-
.../controller/TaskControllerTest.java | 52 ++----
.../service/impl/ReviewServiceImplTest.java | 175 ++++++++++++++++++
src/test/resources/application.yml | 26 +++
.../resources/db/migration/R__test_seed.sql | 37 ++++
.../org.mockito.plugins.MockMaker | 1 +
20 files changed, 718 insertions(+), 50 deletions(-)
create mode 100644 src/main/java/com/guo/learningprogresstracker/dto/request/CreateReviewApplicationRequest.java
create mode 100644 src/main/java/com/guo/learningprogresstracker/dto/request/UpdateReviewApplicationRequest.java
create mode 100644 src/main/java/com/guo/learningprogresstracker/dto/request/UpsertReviewMindMapRequest.java
create mode 100644 src/main/java/com/guo/learningprogresstracker/entity/ReviewApplicationEntity.java
create mode 100644 src/main/java/com/guo/learningprogresstracker/entity/ReviewMindMapEntity.java
create mode 100644 src/main/java/com/guo/learningprogresstracker/enums/ReviewApplicationStatusEnum.java
create mode 100644 src/main/java/com/guo/learningprogresstracker/mapper/ReviewApplicationMapper.java
create mode 100644 src/main/java/com/guo/learningprogresstracker/mapper/ReviewMindMapMapper.java
create mode 100644 src/main/resources/db/migration/V20260620_1__add_review_applications_and_mind_maps.sql
create mode 100644 src/test/java/com/guo/learningprogresstracker/service/impl/ReviewServiceImplTest.java
create mode 100644 src/test/resources/application.yml
create mode 100644 src/test/resources/db/migration/R__test_seed.sql
create mode 100644 src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
diff --git a/pom.xml b/pom.xml
index e1179eb..4537716 100644
--- a/pom.xml
+++ b/pom.xml
@@ -52,6 +52,11 @@
mockito-junit-jupiter
test
+
+ com.h2database
+ h2
+ test
+
org.flywaydb
flyway-core
diff --git a/src/main/java/com/guo/learningprogresstracker/controller/ReviewController.java b/src/main/java/com/guo/learningprogresstracker/controller/ReviewController.java
index c177c92..e36e998 100644
--- a/src/main/java/com/guo/learningprogresstracker/controller/ReviewController.java
+++ b/src/main/java/com/guo/learningprogresstracker/controller/ReviewController.java
@@ -2,11 +2,17 @@ package com.guo.learningprogresstracker.controller;
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.CommonResult;
+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.exception.NotFindEntitiesException;
-import com.guo.learningprogresstracker.service.impl.ReviewServiceImpl;
+import com.guo.learningprogresstracker.service.ReviewService;
+import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@@ -22,15 +28,16 @@ import java.util.List;
@RequiredArgsConstructor
public class ReviewController {
- private final ReviewServiceImpl reviewService;
+ private final ReviewService reviewService;
/**
* 获取复习 feed,合并报告和残片按时间倒序
*/
@GetMapping("/feed")
public CommonResult> getReviewFeed(
- @RequestParam(defaultValue = "30") int limit) {
- return CommonResult.success(reviewService.getReviewFeed(limit));
+ @RequestParam(defaultValue = "30") int limit,
+ @RequestParam(defaultValue = "recent") String mode) {
+ return CommonResult.success(reviewService.getReviewFeed(limit, mode));
}
/**
@@ -72,4 +79,58 @@ public class ReviewController {
public CommonResult getFragmentDetail(@PathVariable int id) throws NotFindEntitiesException {
return CommonResult.success(reviewService.getFragmentDetail(id));
}
+
+ /**
+ * 获取指定任务的应用场景列表
+ */
+ @GetMapping("/applications/{taskNum}")
+ public CommonResult> getApplications(@PathVariable String taskNum) throws NotFindEntitiesException {
+ return CommonResult.success(reviewService.getApplications(taskNum));
+ }
+
+ /**
+ * 创建应用场景
+ */
+ @PostMapping("/applications")
+ public CommonResult createApplication(
+ @Valid @RequestBody CreateReviewApplicationRequest request) throws NotFindEntitiesException {
+ return CommonResult.success(reviewService.createApplication(request));
+ }
+
+ /**
+ * 更新应用场景
+ */
+ @PutMapping("/applications/{id}")
+ public CommonResult updateApplication(
+ @PathVariable Integer id,
+ @Valid @RequestBody UpdateReviewApplicationRequest request) throws NotFindEntitiesException {
+ return CommonResult.success(reviewService.updateApplication(id, request));
+ }
+
+ /**
+ * 删除应用场景
+ */
+ @DeleteMapping("/applications/{id}")
+ public CommonResult deleteApplication(@PathVariable Integer id) throws NotFindEntitiesException {
+ reviewService.deleteApplication(id);
+ return CommonResult.success();
+ }
+
+ /**
+ * 获取指定任务的思维导图
+ */
+ @GetMapping("/mind-map/{taskNum}")
+ public CommonResult getMindMap(@PathVariable String taskNum) throws NotFindEntitiesException {
+ return CommonResult.success(reviewService.getMindMap(taskNum));
+ }
+
+ /**
+ * 创建或更新指定任务的思维导图
+ */
+ @PutMapping("/mind-map/{taskNum}")
+ public CommonResult upsertMindMap(
+ @PathVariable String taskNum,
+ @Valid @RequestBody UpsertReviewMindMapRequest request) throws NotFindEntitiesException {
+ return CommonResult.success(reviewService.upsertMindMap(taskNum, request));
+ }
}
diff --git a/src/main/java/com/guo/learningprogresstracker/controller/TaskController.java b/src/main/java/com/guo/learningprogresstracker/controller/TaskController.java
index ca03507..617ee06 100644
--- a/src/main/java/com/guo/learningprogresstracker/controller/TaskController.java
+++ b/src/main/java/com/guo/learningprogresstracker/controller/TaskController.java
@@ -46,7 +46,7 @@ public class TaskController {
@PostMapping
@Operation(summary = "添加新任务")
public CommonResult addTask(@RequestBody @Validated({Ops.CreateG.class}) TaskRequest taskRequest) throws ErrorParameterException {
- return CommonResult.success(tasksService.addTask(taskRequest));
+ return CommonResult.success("请求成功", tasksService.addTask(taskRequest));
}
@Operation(summary = "任务详情API")
diff --git a/src/main/java/com/guo/learningprogresstracker/dto/request/CreateReviewApplicationRequest.java b/src/main/java/com/guo/learningprogresstracker/dto/request/CreateReviewApplicationRequest.java
new file mode 100644
index 0000000..249e07a
--- /dev/null
+++ b/src/main/java/com/guo/learningprogresstracker/dto/request/CreateReviewApplicationRequest.java
@@ -0,0 +1,20 @@
+package com.guo.learningprogresstracker.dto.request;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+@Data
+public class CreateReviewApplicationRequest {
+
+ @NotBlank(message = "任务编号不可为空")
+ private String taskNum;
+
+ @NotBlank(message = "应用项目标题不可为空")
+ private String title;
+
+ private String description;
+
+ private String resourceUrl;
+
+ private String status;
+}
diff --git a/src/main/java/com/guo/learningprogresstracker/dto/request/UpdateReviewApplicationRequest.java b/src/main/java/com/guo/learningprogresstracker/dto/request/UpdateReviewApplicationRequest.java
new file mode 100644
index 0000000..2f1fb76
--- /dev/null
+++ b/src/main/java/com/guo/learningprogresstracker/dto/request/UpdateReviewApplicationRequest.java
@@ -0,0 +1,17 @@
+package com.guo.learningprogresstracker.dto.request;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+@Data
+public class UpdateReviewApplicationRequest {
+
+ @NotBlank(message = "应用项目标题不可为空")
+ private String title;
+
+ private String description;
+
+ private String resourceUrl;
+
+ private String status;
+}
diff --git a/src/main/java/com/guo/learningprogresstracker/dto/request/UpsertReviewMindMapRequest.java b/src/main/java/com/guo/learningprogresstracker/dto/request/UpsertReviewMindMapRequest.java
new file mode 100644
index 0000000..d2401b2
--- /dev/null
+++ b/src/main/java/com/guo/learningprogresstracker/dto/request/UpsertReviewMindMapRequest.java
@@ -0,0 +1,16 @@
+package com.guo.learningprogresstracker.dto.request;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+@Data
+public class UpsertReviewMindMapRequest {
+
+ @NotBlank(message = "思维导图标题不可为空")
+ private String title;
+
+ @NotBlank(message = "思维导图内容不可为空")
+ private String content;
+
+ private String contentFormat;
+}
diff --git a/src/main/java/com/guo/learningprogresstracker/entity/ReviewApplicationEntity.java b/src/main/java/com/guo/learningprogresstracker/entity/ReviewApplicationEntity.java
new file mode 100644
index 0000000..a17d8f2
--- /dev/null
+++ b/src/main/java/com/guo/learningprogresstracker/entity/ReviewApplicationEntity.java
@@ -0,0 +1,35 @@
+package com.guo.learningprogresstracker.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+
+@TableName(value = "review_applications")
+@Data
+public class ReviewApplicationEntity extends BaseEntity implements Serializable {
+
+ @TableId(value = "id", type = IdType.AUTO)
+ private Integer id;
+
+ @TableField(value = "task_num")
+ private String taskNum;
+
+ @TableField(value = "title")
+ private String title;
+
+ @TableField(value = "description")
+ private String description;
+
+ @TableField(value = "resource_url")
+ private String resourceUrl;
+
+ @TableField(value = "status")
+ private String status;
+
+ @TableField(exist = false)
+ private static final long serialVersionUID = 1L;
+}
diff --git a/src/main/java/com/guo/learningprogresstracker/entity/ReviewMindMapEntity.java b/src/main/java/com/guo/learningprogresstracker/entity/ReviewMindMapEntity.java
new file mode 100644
index 0000000..c92d2e4
--- /dev/null
+++ b/src/main/java/com/guo/learningprogresstracker/entity/ReviewMindMapEntity.java
@@ -0,0 +1,32 @@
+package com.guo.learningprogresstracker.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+
+@TableName(value = "review_mind_maps")
+@Data
+public class ReviewMindMapEntity extends BaseEntity implements Serializable {
+
+ @TableId(value = "id", type = IdType.AUTO)
+ private Integer id;
+
+ @TableField(value = "task_num")
+ private String taskNum;
+
+ @TableField(value = "title")
+ private String title;
+
+ @TableField(value = "content")
+ private String content;
+
+ @TableField(value = "content_format")
+ private String contentFormat;
+
+ @TableField(exist = false)
+ private static final long serialVersionUID = 1L;
+}
diff --git a/src/main/java/com/guo/learningprogresstracker/enums/ReviewApplicationStatusEnum.java b/src/main/java/com/guo/learningprogresstracker/enums/ReviewApplicationStatusEnum.java
new file mode 100644
index 0000000..c0ad172
--- /dev/null
+++ b/src/main/java/com/guo/learningprogresstracker/enums/ReviewApplicationStatusEnum.java
@@ -0,0 +1,35 @@
+package com.guo.learningprogresstracker.enums;
+
+import lombok.AllArgsConstructor;
+
+import java.util.Arrays;
+import java.util.Locale;
+
+@AllArgsConstructor
+public enum ReviewApplicationStatusEnum {
+ TODO("TODO", "待应用"),
+ DOING("DOING", "应用中"),
+ DONE("DONE", "已完成");
+
+ private final String code;
+ private final String description;
+
+ public String getCode() {
+ return code;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public static ReviewApplicationStatusEnum fromCodeOrDefault(String code) {
+ if (code == null || code.trim().isEmpty()) {
+ return TODO;
+ }
+ String normalized = code.trim().toUpperCase(Locale.ROOT);
+ return Arrays.stream(values())
+ .filter(status -> status.code.equals(normalized))
+ .findFirst()
+ .orElse(TODO);
+ }
+}
diff --git a/src/main/java/com/guo/learningprogresstracker/mapper/ReviewApplicationMapper.java b/src/main/java/com/guo/learningprogresstracker/mapper/ReviewApplicationMapper.java
new file mode 100644
index 0000000..f689a48
--- /dev/null
+++ b/src/main/java/com/guo/learningprogresstracker/mapper/ReviewApplicationMapper.java
@@ -0,0 +1,9 @@
+package com.guo.learningprogresstracker.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.guo.learningprogresstracker.entity.ReviewApplicationEntity;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface ReviewApplicationMapper extends BaseMapper {
+}
diff --git a/src/main/java/com/guo/learningprogresstracker/mapper/ReviewMindMapMapper.java b/src/main/java/com/guo/learningprogresstracker/mapper/ReviewMindMapMapper.java
new file mode 100644
index 0000000..8de4fae
--- /dev/null
+++ b/src/main/java/com/guo/learningprogresstracker/mapper/ReviewMindMapMapper.java
@@ -0,0 +1,9 @@
+package com.guo.learningprogresstracker.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.guo.learningprogresstracker.entity.ReviewMindMapEntity;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface ReviewMindMapMapper extends BaseMapper {
+}
diff --git a/src/main/java/com/guo/learningprogresstracker/service/ReviewService.java b/src/main/java/com/guo/learningprogresstracker/service/ReviewService.java
index 4a4e1af..d64a0d3 100644
--- a/src/main/java/com/guo/learningprogresstracker/service/ReviewService.java
+++ b/src/main/java/com/guo/learningprogresstracker/service/ReviewService.java
@@ -2,6 +2,11 @@ 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;
import com.guo.learningprogresstracker.exception.NotFindEntitiesException;
@@ -16,7 +21,7 @@ public interface ReviewService {
/**
* 获取复习 feed 列表,合并报告和残片按时间倒序
*/
- List getReviewFeed(int limit);
+ List getReviewFeed(int limit, String mode);
/**
* 获取所有任务的复习统计数据
@@ -42,4 +47,34 @@ public interface ReviewService {
* 获取残片详情
*/
StudyReportFragmentsEntity getFragmentDetail(int id) throws NotFindEntitiesException;
+
+ /**
+ * 获取指定任务的应用场景列表
+ */
+ List 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;
+
+ /**
+ * 获取指定任务的思维导图
+ */
+ ReviewMindMapEntity getMindMap(String taskNum) throws NotFindEntitiesException;
+
+ /**
+ * 创建或更新指定任务的思维导图
+ */
+ ReviewMindMapEntity upsertMindMap(String taskNum, UpsertReviewMindMapRequest request) throws NotFindEntitiesException;
}
diff --git a/src/main/java/com/guo/learningprogresstracker/service/impl/ReviewServiceImpl.java b/src/main/java/com/guo/learningprogresstracker/service/impl/ReviewServiceImpl.java
index af70420..ceddb5b 100644
--- a/src/main/java/com/guo/learningprogresstracker/service/impl/ReviewServiceImpl.java
+++ b/src/main/java/com/guo/learningprogresstracker/service/impl/ReviewServiceImpl.java
@@ -3,11 +3,19 @@ package com.guo.learningprogresstracker.service.impl;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
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.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;
import com.guo.learningprogresstracker.mapper.StudySessionsMapper;
@@ -31,24 +39,42 @@ import java.util.stream.Stream;
@RequiredArgsConstructor
public class ReviewServiceImpl implements ReviewService {
+ private static final int DEFAULT_LIMIT = 30;
+ private static final int MAX_LIMIT = 100;
+ private static final String RANDOM_MODE = "random";
+ private static final String DEFAULT_MIND_MAP_FORMAT = "TEXT";
+
private final StudyReportsMapper studyReportsMapper;
private final StudyReportFragmentsMapper studyReportFragmentsMapper;
private final StudySessionsMapper studySessionsMapper;
private final TasksMapper tasksMapper;
+ private final ReviewApplicationMapper reviewApplicationMapper;
+ private final ReviewMindMapMapper reviewMindMapMapper;
@Override
- public List getReviewFeed(int limit) {
- List reports = studyReportsMapper.selectList(
- Wrappers.lambdaQuery()
+ public List getReviewFeed(int limit, String mode) {
+ int safeLimit = normalizeLimit(limit);
+ boolean random = RANDOM_MODE.equalsIgnoreCase(mode);
+
+ List reports = random
+ ? studyReportsMapper.selectList(Wrappers.lambdaQuery()
+ .last("ORDER BY RAND() LIMIT " + safeLimit))
+ : studyReportsMapper.selectList(Wrappers.lambdaQuery()
.orderByDesc(StudyReportsEntity::getCreatedTime)
- .last("LIMIT " + limit));
+ .last("LIMIT " + safeLimit));
- List fragments = studyReportFragmentsMapper.selectList(
- Wrappers.lambdaQuery()
+ List fragments = random
+ ? studyReportFragmentsMapper.selectList(Wrappers.lambdaQuery()
+ .last("ORDER BY RAND() LIMIT " + safeLimit))
+ : studyReportFragmentsMapper.selectList(Wrappers.lambdaQuery()
.orderByDesc(StudyReportFragmentsEntity::getCreatedTime)
- .last("LIMIT " + limit));
+ .last("LIMIT " + safeLimit));
- return mergeAndConvert(reports, fragments);
+ List items = mergeAndConvert(reports, fragments);
+ if (random) {
+ Collections.shuffle(items);
+ }
+ return items.stream().limit(safeLimit).collect(Collectors.toList());
}
@Override
@@ -119,6 +145,77 @@ public class ReviewServiceImpl implements ReviewService {
.orElseThrow(() -> new NotFindEntitiesException("学习残片[" + id + "]不存在"));
}
+ @Override
+ public List getApplications(String taskNum) throws NotFindEntitiesException {
+ ensureTaskExists(taskNum);
+ return reviewApplicationMapper.selectList(
+ Wrappers.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);
+ return reviewMindMapMapper.selectOne(
+ Wrappers.lambdaQuery()
+ .eq(ReviewMindMapEntity::getTaskNum, taskNum)
+ .last("LIMIT 1"));
+ }
+
+ @Override
+ public ReviewMindMapEntity upsertMindMap(String taskNum, UpsertReviewMindMapRequest request) throws NotFindEntitiesException {
+ ensureTaskExists(taskNum);
+ ReviewMindMapEntity entity = getMindMap(taskNum);
+ boolean create = entity == null;
+ if (create) {
+ entity = new ReviewMindMapEntity();
+ entity.setTaskNum(taskNum);
+ }
+ entity.setTitle(request.getTitle());
+ entity.setContent(request.getContent());
+ entity.setContentFormat(normalizeMindMapFormat(request.getContentFormat()));
+ if (create) {
+ reviewMindMapMapper.insert(entity);
+ } else {
+ reviewMindMapMapper.updateById(entity);
+ }
+ return entity;
+ }
+
/**
* 将报告和残片合并转换为 ReviewFeedItem 列表,按创建时间倒序排列
*/
@@ -197,6 +294,35 @@ public class ReviewServiceImpl implements ReviewService {
return stats;
}
+ private int normalizeLimit(int limit) {
+ if (limit <= 0) {
+ return DEFAULT_LIMIT;
+ }
+ return Math.min(limit, MAX_LIMIT);
+ }
+
+ private void ensureTaskExists(String taskNum) throws NotFindEntitiesException {
+ if (!StringUtils.hasText(taskNum) || !tasksMapper.exists(
+ Wrappers.lambdaQuery().eq(TaskEntity::getTaskNum, taskNum))) {
+ throw new NotFindEntitiesException("任务[" + taskNum + "]不存在");
+ }
+ }
+
+ private String normalizeApplicationStatus(String status) {
+ return ReviewApplicationStatusEnum.fromCodeOrDefault(status).getCode();
+ }
+
+ private String normalizeMindMapFormat(String contentFormat) {
+ if (!StringUtils.hasText(contentFormat)) {
+ return DEFAULT_MIND_MAP_FORMAT;
+ }
+ String normalized = contentFormat.trim().toUpperCase(Locale.ROOT);
+ if (!Set.of("TEXT", "MERMAID", "JSON").contains(normalized)) {
+ return DEFAULT_MIND_MAP_FORMAT;
+ }
+ return normalized;
+ }
+
private void fillReviewTaskStats(List statsList) {
Map statsByTaskNum = statsList.stream()
.filter(item -> StringUtils.hasText(item.getTaskNum()))
diff --git a/src/main/resources/db/migration/V20260620_1__add_review_applications_and_mind_maps.sql b/src/main/resources/db/migration/V20260620_1__add_review_applications_and_mind_maps.sql
new file mode 100644
index 0000000..37d452b
--- /dev/null
+++ b/src/main/resources/db/migration/V20260620_1__add_review_applications_and_mind_maps.sql
@@ -0,0 +1,36 @@
+create table review_applications
+(
+ id int auto_increment comment 'id无业务含义'
+ primary key,
+ task_num varchar(255) not null comment '任务编码',
+ title varchar(255) not null comment '应用项目标题',
+ description text null comment '应用项目描述',
+ resource_url varchar(1024) null comment '相关链接',
+ status varchar(30) not null default 'TODO' comment '状态:TODO/DOING/DONE',
+ created_time datetime not null comment '创建时间',
+ created_by varchar(255) null,
+ last_modified_time datetime null,
+ last_modified_by varchar(255) null,
+ device_info varchar(50) null comment '操作者设备类型',
+ deleted int default 0 not null comment '逻辑删除符',
+ index idx_review_applications_task_num (task_num)
+)
+ comment '复习模块:学习内容可应用项目';
+
+create table review_mind_maps
+(
+ id int auto_increment comment 'id无业务含义'
+ primary key,
+ task_num varchar(255) not null comment '任务编码',
+ title varchar(255) not null comment '思维导图标题',
+ content text not null comment '思维导图内容',
+ content_format varchar(30) not null default 'TEXT' comment '内容格式:TEXT/MERMAID/JSON',
+ created_time datetime not null comment '创建时间',
+ created_by varchar(255) null,
+ last_modified_time datetime null,
+ last_modified_by varchar(255) null,
+ device_info varchar(50) null comment '操作者设备类型',
+ deleted int default 0 not null comment '逻辑删除符',
+ unique key uk_review_mind_maps_task_num (task_num)
+)
+ comment '复习模块:任务思维导图';
diff --git a/src/test/java/com/guo/learningprogresstracker/DataSourceTest.java b/src/test/java/com/guo/learningprogresstracker/DataSourceTest.java
index bec6ac9..174a528 100644
--- a/src/test/java/com/guo/learningprogresstracker/DataSourceTest.java
+++ b/src/test/java/com/guo/learningprogresstracker/DataSourceTest.java
@@ -1,7 +1,9 @@
package com.guo.learningprogresstracker;
+import cn.dev33.satoken.stp.StpUtil;
import com.guo.learningprogresstracker.entity.TestTableEntity;
import com.guo.learningprogresstracker.service.impl.TestTableServiceImpl;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@@ -19,6 +21,11 @@ public class DataSourceTest {
@Autowired
TestTableServiceImpl testTableService;
+ @BeforeEach
+ void setUp() {
+ StpUtil.login("1", "test_driver");
+ }
+
@Test
public void testAdd(){
TestTableEntity testTableEntity = new TestTableEntity();
@@ -30,8 +37,12 @@ public class DataSourceTest {
@Test
public void testDelete(){
+ TestTableEntity testTableEntity = new TestTableEntity();
+ testTableEntity.setIdName("guo_test_delete");
+ testTableService.save(testTableEntity);
+
HashMap stringStringHashMap = new HashMap<>();
- stringStringHashMap.put("id_name", "guo_test");
+ stringStringHashMap.put("id_name", "guo_test_delete");
boolean delete = testTableService.removeByMap(stringStringHashMap);
assertTrue(delete,"删除idName为guo_test的数据失败");
diff --git a/src/test/java/com/guo/learningprogresstracker/controller/TaskControllerTest.java b/src/test/java/com/guo/learningprogresstracker/controller/TaskControllerTest.java
index 289d1d1..656fccc 100644
--- a/src/test/java/com/guo/learningprogresstracker/controller/TaskControllerTest.java
+++ b/src/test/java/com/guo/learningprogresstracker/controller/TaskControllerTest.java
@@ -18,8 +18,6 @@ import org.springframework.test.web.servlet.MockMvc;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@@ -45,12 +43,12 @@ class TaskControllerTest {
@BeforeEach
void setUp() {
- StpUtil.login("测试用户-创建任务", "test_driver");
+ StpUtil.login("1", "test_driver");
}
@Test
void addTask() throws Exception {
- TaskRequest taskRequest = getTaskRequest();
+ TaskRequest taskRequest = getTaskRequest("测试任务名称-" + System.nanoTime());
log.info("testInfo");
String s = jacksonObjectMapper.writeValueAsString(taskRequest);
System.out.println("Token Value: " + StpUtil.getTokenValue());
@@ -63,30 +61,16 @@ class TaskControllerTest {
}
- private static TaskRequest getTaskRequest() {
- TaskRequest taskRequest = mock(TaskRequest.class);
-
- when(taskRequest.getId()).thenReturn(null);
- // 设置学习任务的名称
- when(taskRequest.getTaskName()).thenReturn("测试任务名称");
-
- // 设置学习材料的存储URL
- when(taskRequest.getMaterialUrl()).thenReturn("http://example.com/material");
-
- // 设置用户设置的任务紧急性
- when(taskRequest.getUrgency()).thenReturn(5);
-
- // 设置用户设置的任务重要性
- when(taskRequest.getImportance()).thenReturn(3);
-
- // 设置任务的内容难度
- when(taskRequest.getContentDifficulty()).thenReturn(4);
-
- // 设置任务的未来价值
- when(taskRequest.getFutureValue()).thenReturn(4);
-
- // 设置用户对任务的主观优先级
- when(taskRequest.getSubjectivePriority()).thenReturn(1);
+ private static TaskRequest getTaskRequest(String taskName) {
+ TaskRequest taskRequest = new TaskRequest();
+ taskRequest.setTaskName(taskName);
+ taskRequest.setTaskDescription("测试任务描述");
+ taskRequest.setMaterialUrl("http://example.com/material");
+ taskRequest.setUrgency(5);
+ taskRequest.setImportance(3);
+ taskRequest.setContentDifficulty(4);
+ taskRequest.setFutureValue(4);
+ taskRequest.setSubjectivePriority(1);
return taskRequest;
}
@@ -108,10 +92,8 @@ class TaskControllerTest {
@Test
void updateTask() throws Exception {
- TaskRequest taskRequest = mock(TaskRequest.class);
- when(taskRequest.getTaskName()).thenReturn("测试数据名称");
- when(taskRequest.getMaterialUrl()).thenReturn("http://example.com/material");
- when(taskRequest.getId()).thenReturn(1);
+ TaskRequest taskRequest = getTaskRequest("测试数据名称");
+ taskRequest.setId(1);
String s = jacksonObjectMapper.writeValueAsString(taskRequest);
mockMvc.perform(put("/tasks/"+taskRequest.getId())
@@ -126,9 +108,9 @@ class TaskControllerTest {
@Test
void deleteTask() throws Exception {
- CommonResult commonResult = taskController.addTask(getTaskRequest());
+ CommonResult commonResult = taskController.addTask(getTaskRequest("待删除任务-" + System.nanoTime()));
assertEquals(commonResult.getCode(),200);
- TaskEntity taskEntity = tasksServiceImpl.getOne(Wrappers.lambdaQuery().eq(TaskEntity::getTaskNum, commonResult.getMessage()));
+ TaskEntity taskEntity = tasksServiceImpl.getOne(Wrappers.lambdaQuery().eq(TaskEntity::getTaskNum, commonResult.getData()));
assertNotNull(taskEntity,"未能找到新创建的任务实体");
mockMvc.perform(delete("/tasks/"+taskEntity.getId())
.header("satoken",StpUtil.getTokenValue()))
@@ -145,4 +127,4 @@ class TaskControllerTest {
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data").exists());
}
-}
\ No newline at end of file
+}
diff --git a/src/test/java/com/guo/learningprogresstracker/service/impl/ReviewServiceImplTest.java b/src/test/java/com/guo/learningprogresstracker/service/impl/ReviewServiceImplTest.java
new file mode 100644
index 0000000..555deb7
--- /dev/null
+++ b/src/test/java/com/guo/learningprogresstracker/service/impl/ReviewServiceImplTest.java
@@ -0,0 +1,175 @@
+package com.guo.learningprogresstracker.service.impl;
+
+import com.baomidou.mybatisplus.core.MybatisConfiguration;
+import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
+import com.guo.learningprogresstracker.dto.ReviewFeedItem;
+import com.guo.learningprogresstracker.dto.request.CreateReviewApplicationRequest;
+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.mapper.ReviewApplicationMapper;
+import com.guo.learningprogresstracker.mapper.ReviewMindMapMapper;
+import com.guo.learningprogresstracker.mapper.StudyReportFragmentsMapper;
+import com.guo.learningprogresstracker.mapper.StudyReportsMapper;
+import com.guo.learningprogresstracker.mapper.StudySessionsMapper;
+import com.guo.learningprogresstracker.mapper.TasksMapper;
+import org.apache.ibatis.builder.MapperBuilderAssistant;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class ReviewServiceImplTest {
+
+ @BeforeAll
+ static void initTableInfo() {
+ MybatisConfiguration configuration = new MybatisConfiguration();
+ TableInfoHelper.initTableInfo(new MapperBuilderAssistant(configuration, ""), StudyReportsEntity.class);
+ TableInfoHelper.initTableInfo(new MapperBuilderAssistant(configuration, ""), StudyReportFragmentsEntity.class);
+ TableInfoHelper.initTableInfo(new MapperBuilderAssistant(configuration, ""), StudySessionsEntity.class);
+ TableInfoHelper.initTableInfo(new MapperBuilderAssistant(configuration, ""), TaskEntity.class);
+ TableInfoHelper.initTableInfo(new MapperBuilderAssistant(configuration, ""), ReviewApplicationEntity.class);
+ TableInfoHelper.initTableInfo(new MapperBuilderAssistant(configuration, ""), ReviewMindMapEntity.class);
+ }
+
+ @InjectMocks
+ private ReviewServiceImpl reviewService;
+
+ @Mock
+ private StudyReportsMapper studyReportsMapper;
+
+ @Mock
+ private StudyReportFragmentsMapper studyReportFragmentsMapper;
+
+ @Mock
+ private StudySessionsMapper studySessionsMapper;
+
+ @Mock
+ private TasksMapper tasksMapper;
+
+ @Mock
+ private ReviewApplicationMapper reviewApplicationMapper;
+
+ @Mock
+ private ReviewMindMapMapper reviewMindMapMapper;
+
+ @Test
+ void getReviewFeed_shouldApplyFinalLimitAfterMerge() {
+ StudyReportsEntity report = new StudyReportsEntity();
+ report.setId(1);
+ report.setSessionNum("S1");
+ report.setContent("report");
+ report.setCreatedTime(LocalDateTime.now());
+
+ StudyReportFragmentsEntity fragment = new StudyReportFragmentsEntity();
+ fragment.setId(2);
+ fragment.setSessionNum("S1");
+ fragment.setContent("fragment");
+ fragment.setCreatedTime(LocalDateTime.now().minusMinutes(1));
+
+ StudySessionsEntity session = new StudySessionsEntity();
+ session.setSessionNum("S1");
+ session.setTaskNum("T1");
+
+ TaskEntity task = new TaskEntity();
+ task.setTaskNum("T1");
+ task.setTaskName("Task");
+
+ when(studyReportsMapper.selectList(any())).thenReturn(List.of(report));
+ when(studyReportFragmentsMapper.selectList(any())).thenReturn(List.of(fragment));
+ when(studySessionsMapper.selectList(any())).thenReturn(List.of(session));
+ when(tasksMapper.selectList(any())).thenReturn(List.of(task));
+
+ List items = reviewService.getReviewFeed(1, "recent");
+
+ assertEquals(1, items.size());
+ assertEquals("REPORT", items.get(0).getSourceType());
+ assertEquals("Task", items.get(0).getTaskName());
+ }
+
+ @Test
+ void createApplication_shouldUseDefaultStatusWhenStatusInvalid() throws Exception {
+ when(tasksMapper.exists(any())).thenReturn(true);
+ when(reviewApplicationMapper.insert(any())).thenAnswer(invocation -> {
+ ReviewApplicationEntity entity = invocation.getArgument(0);
+ entity.setId(7);
+ return 1;
+ });
+
+ CreateReviewApplicationRequest request = new CreateReviewApplicationRequest();
+ request.setTaskNum("T1");
+ request.setTitle("Build a demo");
+ request.setStatus("invalid");
+
+ ReviewApplicationEntity entity = reviewService.createApplication(request);
+
+ assertEquals(7, entity.getId());
+ assertEquals(ReviewApplicationStatusEnum.TODO.getCode(), entity.getStatus());
+ verify(reviewApplicationMapper).insert(any(ReviewApplicationEntity.class));
+ }
+
+ @Test
+ void upsertMindMap_shouldCreateWhenMissing() throws Exception {
+ when(tasksMapper.exists(any())).thenReturn(true);
+ when(reviewMindMapMapper.selectOne(any())).thenReturn(null);
+ when(reviewMindMapMapper.insert(any())).thenAnswer(invocation -> {
+ ReviewMindMapEntity entity = invocation.getArgument(0);
+ entity.setId(3);
+ return 1;
+ });
+
+ UpsertReviewMindMapRequest request = new UpsertReviewMindMapRequest();
+ request.setTitle("Map");
+ request.setContent("root");
+ request.setContentFormat("mermaid");
+
+ ReviewMindMapEntity entity = reviewService.upsertMindMap("T1", request);
+
+ assertNotNull(entity.getId());
+ assertEquals("T1", entity.getTaskNum());
+ assertEquals("MERMAID", entity.getContentFormat());
+ verify(reviewMindMapMapper).insert(any(ReviewMindMapEntity.class));
+ }
+
+ @Test
+ void upsertMindMap_shouldUpdateExisting() throws Exception {
+ ReviewMindMapEntity existing = new ReviewMindMapEntity();
+ existing.setId(5);
+ existing.setTaskNum("T1");
+ existing.setTitle("Old");
+ existing.setContent("old");
+ existing.setContentFormat("TEXT");
+
+ when(tasksMapper.exists(any())).thenReturn(true);
+ when(reviewMindMapMapper.selectOne(any())).thenReturn(existing);
+
+ UpsertReviewMindMapRequest request = new UpsertReviewMindMapRequest();
+ request.setTitle("New");
+ request.setContent("new");
+ request.setContentFormat("json");
+
+ ReviewMindMapEntity entity = reviewService.upsertMindMap("T1", request);
+
+ assertEquals(5, entity.getId());
+ assertEquals("New", entity.getTitle());
+ assertEquals("JSON", entity.getContentFormat());
+ verify(reviewMindMapMapper).updateById(existing);
+ }
+}
diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml
new file mode 100644
index 0000000..9a00708
--- /dev/null
+++ b/src/test/resources/application.yml
@@ -0,0 +1,26 @@
+spring:
+ datasource:
+ driver-class-name: org.h2.Driver
+ url: jdbc:h2:mem:lpt_test;MODE=MySQL;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE;NON_KEYWORDS=USER;DB_CLOSE_DELAY=-1
+ username: sa
+ password:
+ flyway:
+ enabled: true
+ locations: classpath:db/migration
+ baseline-on-migrate: true
+ jackson:
+ time-zone: UTC
+ date-format: yyyy-MM-dd'T'HH:mm:ss'Z'
+
+mybatis-plus:
+ configuration:
+ log-impl: org.apache.ibatis.logging.nologging.NoLoggingImpl
+
+sa-token:
+ token-name: satoken
+ timeout: 2592000
+ active-timeout: -1
+ is-concurrent: true
+ is-share: true
+ token-style: uuid
+ is-log: false
diff --git a/src/test/resources/db/migration/R__test_seed.sql b/src/test/resources/db/migration/R__test_seed.sql
new file mode 100644
index 0000000..97799fa
--- /dev/null
+++ b/src/test/resources/db/migration/R__test_seed.sql
@@ -0,0 +1,37 @@
+insert into user (id, user_name, user_password, created_time, deleted)
+values ('1', 'admin', '$2a$10$skdiSwut4R9pAvt2ZK5ct.6QNualPyJGiaxngFObdLrpAxRnzehBS', now(), 0);
+
+insert into tasks (
+ id,
+ task_num,
+ task_name,
+ task_description,
+ urgency,
+ importance,
+ content_difficulty,
+ future_value,
+ subjective_priority,
+ calculated_priority,
+ material_url,
+ last_learning_status,
+ created_by,
+ created_time,
+ deleted
+)
+values (
+ 1,
+ 'TASK_TEST',
+ '测试任务名称',
+ '测试任务描述',
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 'http://example.com/material',
+ '未开始',
+ '1',
+ now(),
+ 0
+);
diff --git a/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000..fdbd0b1
--- /dev/null
+++ b/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1 @@
+mock-maker-subclass