feat: complete review workflow support

This commit is contained in:
2026-06-20 09:16:20 +08:00
parent 81543969d8
commit f170e85297
20 changed files with 718 additions and 50 deletions
@@ -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<List<ReviewFeedItem>> 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<StudyReportFragmentsEntity> getFragmentDetail(@PathVariable int id) throws NotFindEntitiesException {
return CommonResult.success(reviewService.getFragmentDetail(id));
}
/**
* 获取指定任务的应用场景列表
*/
@GetMapping("/applications/{taskNum}")
public CommonResult<List<ReviewApplicationEntity>> getApplications(@PathVariable String taskNum) throws NotFindEntitiesException {
return CommonResult.success(reviewService.getApplications(taskNum));
}
/**
* 创建应用场景
*/
@PostMapping("/applications")
public CommonResult<ReviewApplicationEntity> createApplication(
@Valid @RequestBody CreateReviewApplicationRequest request) throws NotFindEntitiesException {
return CommonResult.success(reviewService.createApplication(request));
}
/**
* 更新应用场景
*/
@PutMapping("/applications/{id}")
public CommonResult<ReviewApplicationEntity> updateApplication(
@PathVariable Integer id,
@Valid @RequestBody UpdateReviewApplicationRequest request) throws NotFindEntitiesException {
return CommonResult.success(reviewService.updateApplication(id, request));
}
/**
* 删除应用场景
*/
@DeleteMapping("/applications/{id}")
public CommonResult<Void> deleteApplication(@PathVariable Integer id) throws NotFindEntitiesException {
reviewService.deleteApplication(id);
return CommonResult.success();
}
/**
* 获取指定任务的思维导图
*/
@GetMapping("/mind-map/{taskNum}")
public CommonResult<ReviewMindMapEntity> getMindMap(@PathVariable String taskNum) throws NotFindEntitiesException {
return CommonResult.success(reviewService.getMindMap(taskNum));
}
/**
* 创建或更新指定任务的思维导图
*/
@PutMapping("/mind-map/{taskNum}")
public CommonResult<ReviewMindMapEntity> upsertMindMap(
@PathVariable String taskNum,
@Valid @RequestBody UpsertReviewMindMapRequest request) throws NotFindEntitiesException {
return CommonResult.success(reviewService.upsertMindMap(taskNum, request));
}
}
@@ -46,7 +46,7 @@ public class TaskController {
@PostMapping
@Operation(summary = "添加新任务")
public CommonResult<String> 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")
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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);
}
}
@@ -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<ReviewApplicationEntity> {
}
@@ -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<ReviewMindMapEntity> {
}
@@ -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<ReviewFeedItem> getReviewFeed(int limit);
List<ReviewFeedItem> getReviewFeed(int limit, String mode);
/**
* 获取所有任务的复习统计数据
@@ -42,4 +47,34 @@ 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;
/**
* 获取指定任务的思维导图
*/
ReviewMindMapEntity getMindMap(String taskNum) throws NotFindEntitiesException;
/**
* 创建或更新指定任务的思维导图
*/
ReviewMindMapEntity upsertMindMap(String taskNum, UpsertReviewMindMapRequest request) throws NotFindEntitiesException;
}
@@ -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<ReviewFeedItem> getReviewFeed(int limit) {
List<StudyReportsEntity> reports = studyReportsMapper.selectList(
Wrappers.<StudyReportsEntity>lambdaQuery()
public List<ReviewFeedItem> getReviewFeed(int limit, String mode) {
int safeLimit = normalizeLimit(limit);
boolean random = RANDOM_MODE.equalsIgnoreCase(mode);
List<StudyReportsEntity> reports = random
? studyReportsMapper.selectList(Wrappers.<StudyReportsEntity>lambdaQuery()
.last("ORDER BY RAND() LIMIT " + safeLimit))
: studyReportsMapper.selectList(Wrappers.<StudyReportsEntity>lambdaQuery()
.orderByDesc(StudyReportsEntity::getCreatedTime)
.last("LIMIT " + limit));
.last("LIMIT " + safeLimit));
List<StudyReportFragmentsEntity> fragments = studyReportFragmentsMapper.selectList(
Wrappers.<StudyReportFragmentsEntity>lambdaQuery()
List<StudyReportFragmentsEntity> fragments = random
? studyReportFragmentsMapper.selectList(Wrappers.<StudyReportFragmentsEntity>lambdaQuery()
.last("ORDER BY RAND() LIMIT " + safeLimit))
: studyReportFragmentsMapper.selectList(Wrappers.<StudyReportFragmentsEntity>lambdaQuery()
.orderByDesc(StudyReportFragmentsEntity::getCreatedTime)
.last("LIMIT " + limit));
.last("LIMIT " + safeLimit));
return mergeAndConvert(reports, fragments);
List<ReviewFeedItem> 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<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);
return reviewMindMapMapper.selectOne(
Wrappers.<ReviewMindMapEntity>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.<TaskEntity>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<ReviewTaskStats> statsList) {
Map<String, ReviewTaskStats> statsByTaskNum = statsList.stream()
.filter(item -> StringUtils.hasText(item.getTaskNum()))
@@ -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 '复习模块:任务思维导图';