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