Compare commits
10
Commits
master
..
3e4c0645ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e4c0645ad | ||
|
|
11a7c8aaea | ||
|
|
e691dfaa95 | ||
|
|
a6f684c8b7 | ||
|
|
0413013f24 | ||
|
|
9595ad92ee | ||
|
|
6678c2b8c0 | ||
|
|
b434520eff | ||
|
|
5b6f7d6a2a | ||
|
|
c8d0584f11 |
@@ -0,0 +1,4 @@
|
||||
# 行尾规范:文本文件统一 LF,Windows 批处理保留 CRLF
|
||||
* text=auto eol=lf
|
||||
*.bat text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
@@ -44,16 +44,17 @@ lpt-be/src/main/java/com/guo/learningprogresstracker/
|
||||
- entity 使用 `@TableName`、`@TableField` 映射数据库字段。
|
||||
- mapStruct 承载 DTO 转换。
|
||||
|
||||
## API 端点总览(32 个)
|
||||
## API 端点总览(44 个)
|
||||
|
||||
| 模块 | 端点数 | 端点 |
|
||||
|------|--------|------|
|
||||
| 学习会话 | 9 | `POST /study-sessions/{taskNum}/start`, `PUT .../pause`, `PUT .../resume`, `PUT .../end`, `POST .../fragments`, `GET .../history`, `PUT .../expectation`, `GET .../expectation`, `GET .../report-draft` |
|
||||
| 标准思维导图 | 6 | `GET /review/standard-mind-map/{taskNum}`, `POST .../regenerate`, `PUT ...`, `POST .../recall`, `POST .../find-node`, `GET .../recall-records` |
|
||||
| 复习模块 | 6 | `GET /review/feed`, `GET /review/task/{taskNum}`, `GET /review/report/{id}`, `GET /review/fragment/{id}`, `GET /review/standard-mind-map/recall-records/{recordId}`, `GET /review/tasks` |
|
||||
| 任务管理 | 6 | `GET /tasks`, `POST /tasks`, `PUT /tasks/{taskNum}`, `DELETE /tasks/{taskNum}`, `GET /tasks/priority-weights`, `PUT /tasks/priority-weights` |
|
||||
| 应用场景 | 4 | `GET/POST/PUT/DELETE /tasks/{taskNum}/applications[/{id}]` |
|
||||
| 登录认证 | 2 | `POST /login`, `POST /logout` |
|
||||
| 学习会话 | 11 | `PUT /study-sessions/{sessionNum}/expectation`, `GET /study-sessions/{sessionNum}/expectation`, `GET /study-sessions/{sessionNum}`, `POST /study-sessions/{sessionNum}/study-sessions/continue`, `POST /study-sessions/{sessionNum}/study-sessions/pause`, `POST /study-sessions/{sessionNum}/study-sessions/ended`, `GET /study-sessions/{sessionNum}/all-fragments`, `GET /study-sessions/{sessionNum}/report-draft`, `GET /study-sessions/tasks/{taskNum}/fragments`, `GET /study-sessions/tasks/{taskNum}/reports`, `GET /study-sessions/active` |
|
||||
| 任务管理 | 13 | `GET/POST /tasks`, `GET/PUT/DELETE /tasks/{taskId}`, `GET/PUT /tasks/priority-weights`, `GET/POST /tasks/{taskNum}/applications`, `PUT/DELETE /tasks/applications/{id}`, `GET /tasks/{taskNum}/study-sessions/start-or-continue`, `GET /tasks/{taskNum}/not-ended-study-session` |
|
||||
| 复习与导图 | 13 | `GET /review/feed`, `GET /review/tasks`, `GET /review/tasks/{taskNum}`, `GET /review/task/{taskNum}`, `GET /review/report/{id}`, `GET /review/fragment/{id}`, `GET /review/standard-mind-map/{taskNum}`, `POST /review/standard-mind-map/{taskNum}/regenerate`, `PUT /review/standard-mind-map/{taskNum}`, `POST /review/standard-mind-map/{taskNum}/recall`, `POST /review/standard-mind-map/{taskNum}/find-node`, `GET /review/standard-mind-map/{taskNum}/recall-records`, `GET /review/standard-mind-map/recall-records/{recordId}` |
|
||||
| 学习残片 | 3 | `POST /report-fragments`, `PUT /report-fragments/{id}`, `GET /report-fragments/session/{sessionNum}` |
|
||||
| 工具 | 1 | `GET /utils/fetch-title?url=...` |
|
||||
| 测试 | 1 | `GET /test/{id}` |
|
||||
|
||||
## 数据库(10 个核心表)
|
||||
|
||||
@@ -73,6 +74,7 @@ lpt-be/src/main/java/com/guo/learningprogresstracker/
|
||||
- 业务错误:`CommonResult.error(msg)`,code=400,HTTP 200。
|
||||
- 未登录:`GlobalExceptionHandler.handleNotLogin()`,HTTP 401 + code=401。
|
||||
- 参数校验失败:`MethodArgumentNotValidException`,code=400。
|
||||
- 用户可见错误文案必须口语化、可理解,避免“无法生成”“不存在”等技术化表述;技术细节写入日志。例如无学习报告时应提示“这个任务还没开始学习哦,学习后产生学习报告后再来吧”,而不是“没有学习报告,无法生成思维导图”。
|
||||
|
||||
## DTO 转换
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.guo.learningprogresstracker.entity.CommonResult;
|
||||
import com.guo.learningprogresstracker.exception.AppException;
|
||||
import com.guo.learningprogresstracker.exception.ErrorParameterException;
|
||||
import com.guo.learningprogresstracker.exception.NotFindEntitiesException;
|
||||
import com.guo.learningprogresstracker.exception.OperationFailedException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -21,16 +22,25 @@ public class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler({ErrorParameterException.class})
|
||||
public CommonResult errorParameterException(ErrorParameterException ex) {
|
||||
log.warn("参数异常: {}", ex.getMessage(), ex);
|
||||
return CommonResult.error(ex.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler({NotFindEntitiesException.class})
|
||||
public CommonResult notFindEntitiesException(NotFindEntitiesException ex) {
|
||||
log.warn("数据不存在: {}", ex.getMessage(), ex);
|
||||
return CommonResult.error(ex.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler({OperationFailedException.class})
|
||||
public CommonResult operationFailedException(OperationFailedException ex) {
|
||||
log.warn("操作失败: {}", ex.getMessage(), ex);
|
||||
return CommonResult.error(ex.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler({AppException.class})
|
||||
public CommonResult AppException(AppException ex) {
|
||||
log.error("业务异常: {}", ex.getMessage(), ex);
|
||||
return CommonResult.serverError(ex.getMessage());
|
||||
}
|
||||
|
||||
@@ -49,7 +59,7 @@ public class GlobalExceptionHandler {
|
||||
@ExceptionHandler(Exception.class)
|
||||
public CommonResult Exception(Exception ex) {
|
||||
log.error("系统异常", ex);
|
||||
return CommonResult.error(ex.getMessage());
|
||||
return CommonResult.error("操作没有成功,请稍后再试");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ public class ReviewController {
|
||||
private final StandardMindMapService standardMindMapService;
|
||||
|
||||
/**
|
||||
* 获取复习 feed,合并报告和残片按时间倒序
|
||||
* 获取复习 feed,仅返回学习残片(首页滚动条使用)
|
||||
*/
|
||||
@GetMapping("/feed")
|
||||
public CommonResult<List<ReviewFeedItem>> getReviewFeed(
|
||||
|
||||
+12
-1
@@ -1,6 +1,7 @@
|
||||
package com.guo.learningprogresstracker.controller;
|
||||
|
||||
import com.google.protobuf.ServiceException;
|
||||
import com.guo.learningprogresstracker.dto.request.AbortStudySessionRequest;
|
||||
import com.guo.learningprogresstracker.dto.request.EndedStudySessionRequest;
|
||||
import com.guo.learningprogresstracker.dto.request.UpsertExpectationRequest;
|
||||
import com.guo.learningprogresstracker.dto.response.StudySessionResponse;
|
||||
@@ -57,7 +58,7 @@ public class StudySessionController {
|
||||
* 通过sessionNum获取一个【学习会话】
|
||||
*/
|
||||
@GetMapping("/{sessionNum}")
|
||||
public CommonResult<StudySessionResponse> getStudySessionBySessionNum(@NotEmpty(message = "sessionNum不可为空") @PathVariable String sessionNum) throws ErrorParameterException {
|
||||
public CommonResult<StudySessionResponse> getStudySessionBySessionNum(@NotEmpty(message = "请提供学习会话编号") @PathVariable String sessionNum) throws ErrorParameterException {
|
||||
// 通过 service 层获取,包含归属校验
|
||||
StudySessionResponse response = studySessionsServiceImpl.getStudySessionBySessionNum(sessionNum);
|
||||
return CommonResult.success(response);
|
||||
@@ -93,6 +94,16 @@ public class StudySessionController {
|
||||
return message != null ? CommonResult.success(message) : CommonResult.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 误操作结束一个【学习会话】:需输入确认语,严格零数据(删除会话与预期),仅未产生学习残片时可用
|
||||
*/
|
||||
@PostMapping("/{sessionNum}/study-sessions/abort")
|
||||
public CommonResult<Void> abortStudySession(@PathVariable("sessionNum") String sessionNum,
|
||||
@Valid @RequestBody AbortStudySessionRequest request) throws ErrorParameterException {
|
||||
studySessionsServiceImpl.abortStudySession(sessionNum, request.getConfirmation());
|
||||
return CommonResult.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取【学习会话】所有的学习残片数据,以列表返回
|
||||
*/
|
||||
|
||||
@@ -132,7 +132,7 @@ public class TaskController {
|
||||
* 开始或继续一个【学习会话】
|
||||
*/
|
||||
@GetMapping("/{taskNum}/study-sessions/start-or-continue")
|
||||
public CommonResult<StudySessionResponse> startOrContinueStudySession(@NotEmpty(message = "taskNum不可为空")
|
||||
public CommonResult<StudySessionResponse> startOrContinueStudySession(@NotEmpty(message = "请提供任务编号")
|
||||
@PathVariable String taskNum) throws NotFindEntitiesException, ServiceException {
|
||||
StudySessionResponse response = studySessionsServiceImpl.startOrContinueStudySession(taskNum);
|
||||
return CommonResult.success(response);
|
||||
@@ -143,7 +143,7 @@ public class TaskController {
|
||||
* 通过学习任务num获取该任务的未结束会话
|
||||
*/
|
||||
@GetMapping("/{taskNum}/not-ended-study-session")
|
||||
public CommonResult<StudySessionResponse> getNotEndedStudySessionByTaskNum(@NotEmpty(message = "taskNum不可为空")
|
||||
public CommonResult<StudySessionResponse> getNotEndedStudySessionByTaskNum(@NotEmpty(message = "请提供任务编号")
|
||||
@PathVariable String taskNum) throws ErrorParameterException {
|
||||
StudySessionResponse response = studySessionsServiceImpl.getNotEndedStudySessionByTaskNum(taskNum);
|
||||
return CommonResult.success(response);
|
||||
|
||||
@@ -5,7 +5,7 @@ import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 复习滚动 feed 条目,合并展示学习报告和残片
|
||||
* 复习滚动 feed 条目;首页仅使用残片,任务详情仍可同时包含报告和残片
|
||||
*/
|
||||
@Data
|
||||
public class ReviewFeedItem {
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.guo.learningprogresstracker.dto.request;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 误操作结束学习会话的请求体
|
||||
*/
|
||||
@Data
|
||||
public class AbortStudySessionRequest {
|
||||
|
||||
@NotBlank(message = "请填写确认语")
|
||||
private String confirmation;
|
||||
}
|
||||
+1
-1
@@ -16,6 +16,6 @@ public class CreateFragmentsRequest {
|
||||
/**
|
||||
* 残片内容,学习内容的描述
|
||||
*/
|
||||
@NotBlank(message = "啊?无字天书?")
|
||||
@NotBlank(message = "请填写学习内容")
|
||||
private String content;
|
||||
}
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ public class CreateTaskApplicationRequest {
|
||||
|
||||
private String taskNum;
|
||||
|
||||
@NotBlank(message = "应用项目标题不可为空")
|
||||
@NotBlank(message = "请填写应用项目标题")
|
||||
private String title;
|
||||
|
||||
private String description;
|
||||
|
||||
+1
-1
@@ -8,6 +8,6 @@ import lombok.Data;
|
||||
*/
|
||||
@Data
|
||||
public class EndedStudySessionRequest {
|
||||
@NotBlank(message = "啊?搞无字天书是吧?报告内容不可为空")
|
||||
@NotBlank(message = "请填写学习报告内容")
|
||||
private String content;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import lombok.Data;
|
||||
@Data
|
||||
public class RecallCompareRequest {
|
||||
|
||||
@NotBlank(message = "回忆大纲不可为空")
|
||||
@NotBlank(message = "请先填写回忆大纲")
|
||||
private String recallOutline;
|
||||
|
||||
/** 复习起点节点路径(以 / 分隔),null 为任务维度 */
|
||||
|
||||
@@ -15,16 +15,16 @@ import lombok.Data;
|
||||
public class TaskRequest {
|
||||
|
||||
public interface Create{}
|
||||
@Null(message = "创建时不可指定任务ID",groups = Ops.CreateG.class)
|
||||
@NotNull(message = "更新时必须指定任务ID",groups = Ops.UpdateG.class)
|
||||
@Null(message = "创建任务时不需要填写任务编号",groups = Ops.CreateG.class)
|
||||
@NotNull(message = "缺少要更新的任务信息,请刷新后重试",groups = Ops.UpdateG.class)
|
||||
private Integer id;
|
||||
/**
|
||||
* 学习任务的名称
|
||||
*/
|
||||
@NotEmpty(message = "必须指定非空的任务名称", groups = Ops.CreateG.class)
|
||||
@NotEmpty(message = "请填写任务名称", groups = Ops.CreateG.class)
|
||||
private String taskName;
|
||||
|
||||
@NotEmpty(message = "必须指定非空的任务描述", groups = Ops.CreateG.class)
|
||||
@NotEmpty(message = "请填写任务描述", groups = Ops.CreateG.class)
|
||||
private String taskDescription;
|
||||
|
||||
/**
|
||||
@@ -35,41 +35,41 @@ public class TaskRequest {
|
||||
/**
|
||||
* 用户设置的任务紧急性
|
||||
*/
|
||||
@NotNull(message = "必须指定【任务紧急性】指标", groups = Ops.CreateG.class)
|
||||
@Min(value = 0,message = "urgency参数值必须大于等于0")
|
||||
@Max(value = 5,message = "urgency参数值必须小于等于5")
|
||||
@NotNull(message = "请设置任务紧急性", groups = Ops.CreateG.class)
|
||||
@Min(value = 0,message = "任务紧急性不能小于 0")
|
||||
@Max(value = 5,message = "任务紧急性不能大于 5")
|
||||
private Integer urgency;
|
||||
|
||||
/**
|
||||
* 用户设置的任务重要性
|
||||
*/
|
||||
@NotNull(message = "必须指定【任务重要性】指标", groups = Ops.CreateG.class)
|
||||
@Min(value = 0,message = "importance参数值必须大于等于0")
|
||||
@Max(value = 5,message = "importance参数值必须小于等于5")
|
||||
@NotNull(message = "请设置任务重要性", groups = Ops.CreateG.class)
|
||||
@Min(value = 0,message = "任务重要性不能小于 0")
|
||||
@Max(value = 5,message = "任务重要性不能大于 5")
|
||||
private Integer importance;
|
||||
|
||||
/**
|
||||
* 任务的内容难度
|
||||
*/
|
||||
@NotNull(message = "必须指定【内容难度】指标", groups = Ops.CreateG.class)
|
||||
@Min(value = 0,message = "contentDifficulty参数值必须大于等于0")
|
||||
@Max(value = 5,message = "contentDifficulty参数值必须小于等于5")
|
||||
@NotNull(message = "请设置内容难度", groups = Ops.CreateG.class)
|
||||
@Min(value = 0,message = "内容难度不能小于 0")
|
||||
@Max(value = 5,message = "内容难度不能大于 5")
|
||||
private Integer contentDifficulty;
|
||||
|
||||
/**
|
||||
* 任务的未来价值
|
||||
*/
|
||||
@NotNull(message = "必须指定【未来价值】指标", groups = Ops.CreateG.class)
|
||||
@Min(value = 0,message = "futureValue参数值必须大于等于0")
|
||||
@Max(value = 5,message = "futureValue参数值必须小于等于5")
|
||||
@NotNull(message = "请设置未来价值", groups = Ops.CreateG.class)
|
||||
@Min(value = 0,message = "未来价值不能小于 0")
|
||||
@Max(value = 5,message = "未来价值不能大于 5")
|
||||
private Integer futureValue;
|
||||
|
||||
/**
|
||||
* 用户对任务的主观优先级
|
||||
*/
|
||||
@NotNull(message = "必须指定【主观优先级】指标", groups = Ops.CreateG.class)
|
||||
@Min(value = 0,message = "subjectivePriority参数值必须大于等于0")
|
||||
@Max(value = 5,message = "subjectivePriority参数值必须小于等于5")
|
||||
@NotNull(message = "请设置主观优先级", groups = Ops.CreateG.class)
|
||||
@Min(value = 0,message = "主观优先级不能小于 0")
|
||||
@Max(value = 5,message = "主观优先级不能大于 5")
|
||||
private Integer subjectivePriority;
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -11,6 +11,6 @@ public class UpdateFragmentsRequest {
|
||||
/**
|
||||
* 残片内容,学习内容的描述
|
||||
*/
|
||||
@NotBlank(message = "啊?无字天书?")
|
||||
@NotBlank(message = "请填写学习内容")
|
||||
private String content;
|
||||
}
|
||||
|
||||
+1
-1
@@ -9,6 +9,6 @@ import lombok.Data;
|
||||
@Data
|
||||
public class UpdateStandardMindMapRequest {
|
||||
|
||||
@NotBlank(message = "思维导图大纲不可为空")
|
||||
@NotBlank(message = "请填写思维导图大纲")
|
||||
private String outline;
|
||||
}
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import lombok.Data;
|
||||
@Data
|
||||
public class UpdateTaskApplicationRequest {
|
||||
|
||||
@NotBlank(message = "应用项目标题不可为空")
|
||||
@NotBlank(message = "请填写应用项目标题")
|
||||
private String title;
|
||||
|
||||
private String description;
|
||||
|
||||
+1
-1
@@ -9,6 +9,6 @@ import lombok.Data;
|
||||
@Data
|
||||
public class UpsertExpectationRequest {
|
||||
|
||||
@NotBlank(message = "学习预期不可为空")
|
||||
@NotBlank(message = "请填写学习预期")
|
||||
private String description;
|
||||
}
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 任务标准思维导图:由内置规则或 AI 从学习报告/残片生成,用户可修改
|
||||
* 任务标准思维导图:由内置规则或 AI 从学习报告生成,用户可修改
|
||||
*/
|
||||
@TableName(value = "review_standard_mind_maps")
|
||||
@Data
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.guo.learningprogresstracker.service;
|
||||
|
||||
import com.guo.learningprogresstracker.entity.StudyReportFragmentsEntity;
|
||||
import com.guo.learningprogresstracker.entity.StudyReportsEntity;
|
||||
import com.guo.learningprogresstracker.entity.TaskApplicationEntity;
|
||||
import com.guo.learningprogresstracker.entity.TaskEntity;
|
||||
@@ -25,15 +24,13 @@ public interface MindMapAiClient {
|
||||
* 根据学习数据生成标准思维导图根节点
|
||||
*
|
||||
* @param task 学习任务
|
||||
* @param reports 该任务的全部学习报告
|
||||
* @param fragments 该任务的全部学习残片
|
||||
* @param reports 该任务的全部学习报告
|
||||
* @param applications 该任务的应用场景(可选)
|
||||
* @param clientHint 前端已有的大纲文本(可选,用于 AI 续写而非全量生成)
|
||||
* @return 标准思维导图的根节点;若无可生成数据则返回 {@link Optional#empty()}
|
||||
*/
|
||||
Optional<MindMapNode> generate(TaskEntity task,
|
||||
List<StudyReportsEntity> reports,
|
||||
List<StudyReportFragmentsEntity> fragments,
|
||||
List<TaskApplicationEntity> applications,
|
||||
String clientHint);
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import java.util.List;
|
||||
public interface ReviewService {
|
||||
|
||||
/**
|
||||
* 获取复习 feed 列表,合并报告和残片按时间倒序
|
||||
* 获取复习 feed 列表,仅返回学习残片并按时间倒序
|
||||
*/
|
||||
List<ReviewFeedItem> getReviewFeed(int limit, String mode);
|
||||
|
||||
|
||||
@@ -17,4 +17,9 @@ public interface StudyExpectationsService {
|
||||
* 查询学习会话的学习预期,不存在时返回 null
|
||||
*/
|
||||
StudyExpectationsEntity getBySessionNum(String sessionNum);
|
||||
|
||||
/**
|
||||
* 删除学习会话的学习预期(误操作结束时会话不留任何数据)
|
||||
*/
|
||||
void deleteBySessionNum(String sessionNum);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,11 @@ public interface StudySessionsService extends IService<StudySessionsEntity> {
|
||||
|
||||
String endedStudySession(String sessionNum, String content) throws ErrorParameterException;
|
||||
|
||||
/**
|
||||
* 误操作结束学习会话:需输入确认语,严格零数据(删除会话与预期),仅未产生学习残片时可用
|
||||
*/
|
||||
void abortStudySession(String sessionNum, String confirmation) throws ErrorParameterException;
|
||||
|
||||
void pauseStudySession(String sessionNum, LocalDateTime endTime) throws ErrorParameterException, ServiceException;
|
||||
|
||||
void continueStudySession(String sessionNum) throws ErrorParameterException, ServiceException;
|
||||
|
||||
@@ -82,10 +82,10 @@ public class AiServiceClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用 lpt-ai 从学习数据生成思维导图大纲。
|
||||
* 调用 lpt-ai 从学习报告生成思维导图大纲。
|
||||
*/
|
||||
public Optional<String> generateMindMap(String taskName, String taskDescription,
|
||||
List<String> reports, List<String> fragments) {
|
||||
List<String> reports) {
|
||||
if (!isConfigured()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
@@ -93,8 +93,7 @@ public class AiServiceClient {
|
||||
Map<String, Object> params = Map.of(
|
||||
"taskName", taskName,
|
||||
"taskDescription", taskDescription != null ? taskDescription : "",
|
||||
"reports", reports != null ? reports : List.of(),
|
||||
"fragments", fragments != null ? fragments : List.of()
|
||||
"reports", reports != null ? reports : List.of()
|
||||
);
|
||||
|
||||
Optional<JsonNode> result = submitAndWait("generate-mind-map", params);
|
||||
|
||||
+11
-37
@@ -1,6 +1,5 @@
|
||||
package com.guo.learningprogresstracker.service.impl;
|
||||
|
||||
import com.guo.learningprogresstracker.entity.StudyReportFragmentsEntity;
|
||||
import com.guo.learningprogresstracker.entity.StudyReportsEntity;
|
||||
import com.guo.learningprogresstracker.entity.TaskApplicationEntity;
|
||||
import com.guo.learningprogresstracker.entity.TaskEntity;
|
||||
@@ -16,12 +15,12 @@ import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 内置规则引擎:从学习报告和残片提取关键词并组织为树形思维导图。
|
||||
* 内置规则引擎:从学习报告提取内容并组织为树形思维导图。
|
||||
* <p>规则策略:</p>
|
||||
* <ol>
|
||||
* <li>根节点 = 任务名称</li>
|
||||
* <li>一级分支 = 按会话(日期+报告摘要)分组</li>
|
||||
* <li>二级分支 = 该会话下的残片标题</li>
|
||||
* <li>二级分支 = 该会话下的报告</li>
|
||||
* <li>附加分支"应用场景" = 任务应用场景(如有)</li>
|
||||
* <li>去重:标准化后标题对比,合并内容相似的节点</li>
|
||||
* <li>引用追溯:每个节点携带 sourceType / sourceId</li>
|
||||
@@ -46,10 +45,9 @@ public class BuiltinMindMapGenerator implements MindMapAiClient {
|
||||
@Override
|
||||
public Optional<MindMapNode> generate(TaskEntity task,
|
||||
List<StudyReportsEntity> reports,
|
||||
List<StudyReportFragmentsEntity> fragments,
|
||||
List<TaskApplicationEntity> applications,
|
||||
String clientHint) {
|
||||
if (reports.isEmpty() && fragments.isEmpty()) {
|
||||
if (reports == null || reports.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@@ -58,34 +56,21 @@ public class BuiltinMindMapGenerator implements MindMapAiClient {
|
||||
|
||||
// 按 session 分组
|
||||
Map<String, List<StudyReportsEntity>> reportsBySession = reports.stream()
|
||||
.filter(r -> r.getSessionNum() != null)
|
||||
.collect(Collectors.groupingBy(StudyReportsEntity::getSessionNum));
|
||||
Map<String, List<StudyReportFragmentsEntity>> fragmentsBySession = fragments.stream()
|
||||
.collect(Collectors.groupingBy(StudyReportFragmentsEntity::getSessionNum));
|
||||
|
||||
// 合并所有 session
|
||||
Set<String> allSessions = new LinkedHashSet<>();
|
||||
allSessions.addAll(reportsBySession.keySet());
|
||||
allSessions.addAll(fragmentsBySession.keySet());
|
||||
for (Map.Entry<String, List<StudyReportsEntity>> entry : reportsBySession.entrySet()) {
|
||||
List<StudyReportsEntity> sessReports = entry.getValue();
|
||||
|
||||
for (String sessionNum : allSessions) {
|
||||
List<StudyReportsEntity> sessReports = reportsBySession.getOrDefault(sessionNum, List.of());
|
||||
List<StudyReportFragmentsEntity> sessFragments = fragmentsBySession.getOrDefault(sessionNum, List.of());
|
||||
|
||||
// 会话分支标题:取第一条报告的前 60 字作为摘要,或直接写"学习记录"
|
||||
String sessionTitle;
|
||||
if (!sessReports.isEmpty()) {
|
||||
String firstReport = sessReports.get(0).getContent();
|
||||
sessionTitle = truncate(firstReport, MAX_TITLE_LENGTH);
|
||||
if (sessReports.get(0).getCreatedTime() != null) {
|
||||
sessionTitle = formatDate(sessReports.get(0).getCreatedTime()) + " " + sessionTitle;
|
||||
}
|
||||
} else {
|
||||
sessionTitle = "学习记录 " + (sessFragments.isEmpty() ? "" : formatDate(sessFragments.get(0).getCreatedTime()));
|
||||
// 会话分支标题:取第一条报告的前 60 字作为摘要
|
||||
StudyReportsEntity firstReport = sessReports.get(0);
|
||||
String sessionTitle = truncate(firstReport.getContent(), MAX_TITLE_LENGTH);
|
||||
if (firstReport.getCreatedTime() != null) {
|
||||
sessionTitle = formatDate(firstReport.getCreatedTime()) + " " + sessionTitle;
|
||||
}
|
||||
|
||||
MindMapNode sessionNode = new MindMapNode(sessionTitle);
|
||||
|
||||
// 报告作为子节点
|
||||
for (StudyReportsEntity report : sessReports) {
|
||||
String content = report.getContent();
|
||||
if (content == null || content.isBlank()) continue;
|
||||
@@ -96,17 +81,6 @@ public class BuiltinMindMapGenerator implements MindMapAiClient {
|
||||
sessionNode.getChildren().add(reportNode);
|
||||
}
|
||||
|
||||
// 残片作为子节点
|
||||
for (StudyReportFragmentsEntity frag : sessFragments) {
|
||||
String content = frag.getContent();
|
||||
if (content == null || content.isBlank()) continue;
|
||||
MindMapNode fragNode = new MindMapNode(truncate(content, MAX_TITLE_LENGTH));
|
||||
fragNode.setNotes(content);
|
||||
fragNode.setSourceType("FRAGMENT");
|
||||
fragNode.setSourceId(frag.getId());
|
||||
sessionNode.getChildren().add(fragNode);
|
||||
}
|
||||
|
||||
root.getChildren().add(sessionNode);
|
||||
}
|
||||
|
||||
|
||||
+3
-10
@@ -1,6 +1,5 @@
|
||||
package com.guo.learningprogresstracker.service.impl;
|
||||
|
||||
import com.guo.learningprogresstracker.entity.StudyReportFragmentsEntity;
|
||||
import com.guo.learningprogresstracker.entity.StudyReportsEntity;
|
||||
import com.guo.learningprogresstracker.entity.TaskApplicationEntity;
|
||||
import com.guo.learningprogresstracker.entity.TaskEntity;
|
||||
@@ -40,32 +39,26 @@ public class RemoteAiMindMapClient implements MindMapAiClient {
|
||||
@Override
|
||||
public Optional<MindMapNode> generate(TaskEntity task,
|
||||
List<StudyReportsEntity> reports,
|
||||
List<StudyReportFragmentsEntity> fragments,
|
||||
List<TaskApplicationEntity> applications,
|
||||
String clientHint) {
|
||||
if (!isAvailable()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
// 提取报告和残片的内容文本
|
||||
// 提取学习报告的内容文本
|
||||
List<String> reportTexts = reports.stream()
|
||||
.map(StudyReportsEntity::getContent)
|
||||
.filter(c -> c != null && !c.isBlank())
|
||||
.collect(Collectors.toList());
|
||||
List<String> fragmentTexts = fragments.stream()
|
||||
.map(StudyReportFragmentsEntity::getContent)
|
||||
.filter(c -> c != null && !c.isBlank())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (reportTexts.isEmpty() && fragmentTexts.isEmpty()) {
|
||||
if (reportTexts.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
Optional<String> optOutline = aiServiceClient.generateMindMap(
|
||||
task.getTaskName(),
|
||||
task.getTaskDescription(),
|
||||
reportTexts,
|
||||
fragmentTexts
|
||||
reportTexts
|
||||
);
|
||||
|
||||
if (optOutline.isEmpty() || optOutline.get().isBlank()) {
|
||||
|
||||
@@ -16,6 +16,7 @@ import com.guo.learningprogresstracker.mapper.StudySessionsMapper;
|
||||
import com.guo.learningprogresstracker.mapper.TasksMapper;
|
||||
import com.guo.learningprogresstracker.service.ReviewService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@@ -29,6 +30,7 @@ import java.util.stream.Stream;
|
||||
/**
|
||||
* 复习模块 Service 实现
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ReviewServiceImpl implements ReviewService {
|
||||
@@ -53,13 +55,6 @@ public class ReviewServiceImpl implements ReviewService {
|
||||
}
|
||||
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 " + safeLimit));
|
||||
|
||||
List<StudyReportFragmentsEntity> fragments = random
|
||||
? studyReportFragmentsMapper.selectList(Wrappers.<StudyReportFragmentsEntity>lambdaQuery()
|
||||
.last("ORDER BY RAND() LIMIT " + safeLimit))
|
||||
@@ -67,7 +62,7 @@ public class ReviewServiceImpl implements ReviewService {
|
||||
.orderByDesc(StudyReportFragmentsEntity::getCreatedTime)
|
||||
.last("LIMIT " + safeLimit));
|
||||
|
||||
List<ReviewFeedItem> items = mergeAndConvert(reports, fragments);
|
||||
List<ReviewFeedItem> items = mergeAndConvert(List.of(), fragments);
|
||||
if (random) {
|
||||
Collections.shuffle(items);
|
||||
}
|
||||
@@ -82,14 +77,11 @@ public class ReviewServiceImpl implements ReviewService {
|
||||
private List<ReviewFeedItem> getSmartFeed(int safeLimit) {
|
||||
int candidateLimit = Math.min(safeLimit * SMART_CANDIDATE_MULTIPLIER, 500);
|
||||
|
||||
List<StudyReportsEntity> reports = studyReportsMapper.selectList(
|
||||
Wrappers.<StudyReportsEntity>lambdaQuery()
|
||||
.last("ORDER BY RAND() LIMIT " + candidateLimit));
|
||||
List<StudyReportFragmentsEntity> fragments = studyReportFragmentsMapper.selectList(
|
||||
Wrappers.<StudyReportFragmentsEntity>lambdaQuery()
|
||||
.last("ORDER BY RAND() LIMIT " + candidateLimit));
|
||||
|
||||
List<ReviewFeedItem> candidates = mergeAndConvert(reports, fragments);
|
||||
List<ReviewFeedItem> candidates = mergeAndConvert(List.of(), fragments);
|
||||
if (candidates.size() <= safeLimit) {
|
||||
Collections.shuffle(candidates);
|
||||
return candidates;
|
||||
@@ -224,13 +216,19 @@ public class ReviewServiceImpl implements ReviewService {
|
||||
@Override
|
||||
public StudyReportsEntity getReportDetail(int id) throws NotFindEntitiesException {
|
||||
return Optional.ofNullable(studyReportsMapper.selectById(id))
|
||||
.orElseThrow(() -> new NotFindEntitiesException("学习报告[" + id + "]不存在"));
|
||||
.orElseThrow(() -> {
|
||||
log.warn("学习报告[{}]不存在", id);
|
||||
return new NotFindEntitiesException("这份学习报告不存在或已被删除");
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public StudyReportFragmentsEntity getFragmentDetail(int id) throws NotFindEntitiesException {
|
||||
return Optional.ofNullable(studyReportFragmentsMapper.selectById(id))
|
||||
.orElseThrow(() -> new NotFindEntitiesException("学习残片[" + id + "]不存在"));
|
||||
.orElseThrow(() -> {
|
||||
log.warn("学习残片[{}]不存在", id);
|
||||
return new NotFindEntitiesException("这条学习残片不存在或已被删除");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -321,7 +319,8 @@ public class ReviewServiceImpl implements ReviewService {
|
||||
private void ensureTaskExists(String taskNum) throws NotFindEntitiesException {
|
||||
if (!StringUtils.hasText(taskNum) || !tasksMapper.exists(
|
||||
Wrappers.<TaskEntity>lambdaQuery().eq(TaskEntity::getTaskNum, taskNum))) {
|
||||
throw new NotFindEntitiesException("任务[" + taskNum + "]不存在");
|
||||
log.warn("任务[{}]不存在", taskNum);
|
||||
throw new NotFindEntitiesException("这个任务不存在或已被删除");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+22
-16
@@ -36,7 +36,6 @@ public class StandardMindMapServiceImpl implements StandardMindMapService {
|
||||
private final ReviewRecallRecordMapper recallRecordMapper;
|
||||
private final TasksMapper tasksMapper;
|
||||
private final StudyReportsMapper studyReportsMapper;
|
||||
private final StudyReportFragmentsMapper studyReportFragmentsMapper;
|
||||
private final TaskApplicationMapper taskApplicationMapper;
|
||||
private final StudySessionsMapper studySessionsMapper;
|
||||
|
||||
@@ -86,7 +85,8 @@ public class StandardMindMapServiceImpl implements StandardMindMapService {
|
||||
ensureTaskExists(taskNum);
|
||||
ReviewStandardMindMapEntity existing = queryByTaskNum(taskNum);
|
||||
if (existing == null) {
|
||||
throw new NotFindEntitiesException("标准思维导图尚不存在,无法增量更新");
|
||||
log.warn("任务[{}]标准思维导图尚不存在,无法增量更新", taskNum);
|
||||
throw new NotFindEntitiesException("还没有生成过标准思维导图,请先完整生成一次哦");
|
||||
}
|
||||
// 防并发生成
|
||||
AtomicBoolean lock = generatingTasks.computeIfAbsent(taskNum, k -> new AtomicBoolean(false));
|
||||
@@ -201,7 +201,8 @@ public class StandardMindMapServiceImpl implements StandardMindMapService {
|
||||
try {
|
||||
resultJson = objectMapper.writeValueAsString(result);
|
||||
} catch (Exception e) {
|
||||
throw new OperationFailedException("对比结果序列化失败");
|
||||
log.error("任务[{}]对比结果序列化失败", taskNum, e);
|
||||
throw new OperationFailedException("对比结果解析失败了,请稍后再试");
|
||||
}
|
||||
|
||||
// 5. 保存回忆记录
|
||||
@@ -405,7 +406,10 @@ public class StandardMindMapServiceImpl implements StandardMindMapService {
|
||||
@Override
|
||||
public ReviewRecallRecordEntity getRecallRecord(Integer recordId) throws NotFindEntitiesException {
|
||||
return Optional.ofNullable(recallRecordMapper.selectById(recordId))
|
||||
.orElseThrow(() -> new NotFindEntitiesException("回忆记录[" + recordId + "]不存在"));
|
||||
.orElseThrow(() -> {
|
||||
log.warn("回忆记录[{}]不存在", recordId);
|
||||
return new NotFindEntitiesException("这条回忆记录不存在或已被删除");
|
||||
});
|
||||
}
|
||||
|
||||
// ============ 内部方法 ============
|
||||
@@ -415,7 +419,8 @@ public class StandardMindMapServiceImpl implements StandardMindMapService {
|
||||
Wrappers.<TaskEntity>lambdaQuery().eq(TaskEntity::getTaskNum, taskNum).last("LIMIT 1"));
|
||||
|
||||
if (task == null) {
|
||||
throw new OperationFailedException("任务[" + taskNum + "]不存在");
|
||||
log.warn("任务[{}]不存在,无法生成思维导图", taskNum);
|
||||
throw new OperationFailedException("这个任务不存在或已被删除");
|
||||
}
|
||||
|
||||
// 收集学习数据
|
||||
@@ -428,14 +433,12 @@ public class StandardMindMapServiceImpl implements StandardMindMapService {
|
||||
List<StudyReportsEntity> reports = sessionNums.isEmpty() ? List.of()
|
||||
: studyReportsMapper.selectList(Wrappers.<StudyReportsEntity>lambdaQuery()
|
||||
.in(StudyReportsEntity::getSessionNum, sessionNums));
|
||||
List<StudyReportFragmentsEntity> fragments = sessionNums.isEmpty() ? List.of()
|
||||
: studyReportFragmentsMapper.selectList(Wrappers.<StudyReportFragmentsEntity>lambdaQuery()
|
||||
.in(StudyReportFragmentsEntity::getSessionNum, sessionNums));
|
||||
List<TaskApplicationEntity> applications = taskApplicationMapper.selectList(
|
||||
Wrappers.<TaskApplicationEntity>lambdaQuery().eq(TaskApplicationEntity::getTaskNum, taskNum));
|
||||
|
||||
if (reports.isEmpty() && fragments.isEmpty()) {
|
||||
throw new OperationFailedException("任务[" + taskNum + "]没有学习报告或残片,无法生成思维导图");
|
||||
if (reports.isEmpty()) {
|
||||
log.warn("任务[{}]没有学习报告,无法生成思维导图", taskNum);
|
||||
throw new OperationFailedException("这个任务还没开始学习哦,学习后产生学习报告后再来吧");
|
||||
}
|
||||
|
||||
// 优先选 AI 客户端(非 BUILTIN),其次内置生成器
|
||||
@@ -445,10 +448,11 @@ public class StandardMindMapServiceImpl implements StandardMindMapService {
|
||||
.orElse(null);
|
||||
|
||||
if (client == null) {
|
||||
throw new OperationFailedException("没有可用的思维导图生成器");
|
||||
log.warn("任务[{}]没有可用的思维导图生成器", taskNum);
|
||||
throw new OperationFailedException("思维导图暂时生成不了,请稍后再试");
|
||||
}
|
||||
|
||||
Optional<MindMapNode> optRoot = client.generate(task, reports, fragments, applications, null);
|
||||
Optional<MindMapNode> optRoot = client.generate(task, reports, applications, null);
|
||||
// AI 生成失败时尝试降级到内置生成器
|
||||
if (optRoot.isEmpty() && !"BUILTIN".equals(client.generatorName())) {
|
||||
log.info("AI 思维导图生成失败,降级到内置生成器");
|
||||
@@ -456,11 +460,12 @@ public class StandardMindMapServiceImpl implements StandardMindMapService {
|
||||
.filter(c -> "BUILTIN".equals(c.generatorName()) && c.isAvailable())
|
||||
.findFirst().orElse(null);
|
||||
if (fallback != null) {
|
||||
optRoot = fallback.generate(task, reports, fragments, applications, null);
|
||||
optRoot = fallback.generate(task, reports, applications, null);
|
||||
}
|
||||
}
|
||||
if (optRoot.isEmpty()) {
|
||||
throw new OperationFailedException("思维导图生成失败");
|
||||
log.warn("任务[{}]思维导图生成失败,已尝试全部生成器", taskNum);
|
||||
throw new OperationFailedException("思维导图生成失败了,请稍后再试");
|
||||
}
|
||||
|
||||
MindMapNode root = optRoot.get();
|
||||
@@ -483,7 +488,7 @@ public class StandardMindMapServiceImpl implements StandardMindMapService {
|
||||
entity.setGenerator(client.generatorName());
|
||||
entity.setGeneratorVersion("1.0");
|
||||
entity.setSourceReportCount(reports.size());
|
||||
entity.setSourceFragmentCount(fragments.size());
|
||||
entity.setSourceFragmentCount(0);
|
||||
entity.setGeneratedTime(LocalDateTime.now());
|
||||
|
||||
if (create) {
|
||||
@@ -497,7 +502,8 @@ public class StandardMindMapServiceImpl implements StandardMindMapService {
|
||||
private void ensureTaskExists(String taskNum) throws NotFindEntitiesException {
|
||||
if (!StringUtils.hasText(taskNum) || !tasksMapper.exists(
|
||||
Wrappers.<TaskEntity>lambdaQuery().eq(TaskEntity::getTaskNum, taskNum))) {
|
||||
throw new NotFindEntitiesException("任务[" + taskNum + "]不存在");
|
||||
log.warn("任务[{}]不存在", taskNum);
|
||||
throw new NotFindEntitiesException("这个任务不存在或已被删除");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+11
-1
@@ -8,11 +8,13 @@ import com.guo.learningprogresstracker.mapper.StudyExpectationsMapper;
|
||||
import com.guo.learningprogresstracker.mapper.StudySessionsMapper;
|
||||
import com.guo.learningprogresstracker.service.StudyExpectationsService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 学习预期服务实现
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class StudyExpectationsServiceImpl implements StudyExpectationsService {
|
||||
@@ -26,7 +28,8 @@ public class StudyExpectationsServiceImpl implements StudyExpectationsService {
|
||||
Wrappers.<StudySessionsEntity>lambdaQuery()
|
||||
.eq(StudySessionsEntity::getSessionNum, sessionNum));
|
||||
if (!sessionExists) {
|
||||
throw new ErrorParameterException("学习会话[" + sessionNum + "]不存在");
|
||||
log.warn("学习会话[{}]不存在", sessionNum);
|
||||
throw new ErrorParameterException("这次学习会话不存在或已结束");
|
||||
}
|
||||
|
||||
StudyExpectationsEntity existing = getBySessionNum(sessionNum);
|
||||
@@ -49,4 +52,11 @@ public class StudyExpectationsServiceImpl implements StudyExpectationsService {
|
||||
.eq(StudyExpectationsEntity::getSessionNum, sessionNum)
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteBySessionNum(String sessionNum) {
|
||||
studyExpectationsMapper.delete(
|
||||
Wrappers.<StudyExpectationsEntity>lambdaQuery()
|
||||
.eq(StudyExpectationsEntity::getSessionNum, sessionNum));
|
||||
}
|
||||
}
|
||||
|
||||
+6
-2
@@ -12,6 +12,7 @@ import com.guo.learningprogresstracker.mapper.StudySessionsMapper;
|
||||
import com.guo.learningprogresstracker.service.StudyReportFragmentsService;
|
||||
import com.guo.learningprogresstracker.mapper.StudyReportFragmentsMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -20,6 +21,7 @@ import java.util.List;
|
||||
/**
|
||||
* 针对表【study_report_fragments(记录学习过程中的学习内容报告残片)】的数据库操作Service实现
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class StudyReportFragmentsServiceImpl extends ServiceImpl<StudyReportFragmentsMapper, StudyReportFragmentsEntity>
|
||||
@@ -35,7 +37,8 @@ public class StudyReportFragmentsServiceImpl extends ServiceImpl<StudyReportFrag
|
||||
Wrappers.lambdaQuery(StudySessionsEntity.class)
|
||||
.eq(StudySessionsEntity::getSessionNum, entity.getSessionNum()));
|
||||
if (session == null) {
|
||||
throw new NotFindEntitiesException(String.format("未能找到学习会话sessionNum[%s]", entity.getSessionNum()));
|
||||
log.warn("未能找到学习会话sessionNum[{}]", entity.getSessionNum());
|
||||
throw new NotFindEntitiesException("这次学习会话不存在或已结束");
|
||||
}
|
||||
this.save(entity);
|
||||
}
|
||||
@@ -44,7 +47,8 @@ public class StudyReportFragmentsServiceImpl extends ServiceImpl<StudyReportFrag
|
||||
public void updateFragments(Integer id, UpdateFragmentsRequest request) throws NotFindEntitiesException {
|
||||
StudyReportFragmentsEntity existing = this.getById(id);
|
||||
if (existing == null) {
|
||||
throw new NotFindEntitiesException(String.format("未能找到学习残片id[%d]", id));
|
||||
log.warn("未能找到学习残片id[{}]", id);
|
||||
throw new NotFindEntitiesException("这条学习残片不存在或已被删除");
|
||||
}
|
||||
StudyReportFragmentsEntity entity = FragmentsConvert.MAPPER.toFragmentsEntity(request);
|
||||
entity.setId(id);
|
||||
|
||||
+58
-12
@@ -40,6 +40,11 @@ public class StudySessionsServiceImpl extends ServiceImpl<StudySessionsMapper, S
|
||||
|
||||
public static final int WORK_DURATION = 25;
|
||||
|
||||
/**
|
||||
* 误操作结束的确认语:用户必须输入完全一致的内容才能零数据关闭会话
|
||||
*/
|
||||
public static final String MISOPERATION_CONFIRMATION = "我误操作导致开启了本次学习";
|
||||
|
||||
private final TasksServiceImpl tasksServiceImpl;
|
||||
private final StudyReportFragmentsServiceImpl studyReportFragmentsServiceImpl;
|
||||
private final StudyReportFragmentsMapper studyReportFragmentsMapper;
|
||||
@@ -50,7 +55,10 @@ public class StudySessionsServiceImpl extends ServiceImpl<StudySessionsMapper, S
|
||||
public StudySessionResponse startOrContinueStudySession(String taskNum) throws NotFindEntitiesException, ServiceException {
|
||||
TaskEntity taskEntity = tasksServiceImpl.getOneOpt(
|
||||
Wrappers.lambdaQuery(TaskEntity.class).eq(TaskEntity::getTaskNum, taskNum))
|
||||
.orElseThrow(() -> new NotFindEntitiesException(String.format("[%s]不存在", taskNum)));
|
||||
.orElseThrow(() -> {
|
||||
log.warn("[{}]不存在", taskNum);
|
||||
return new NotFindEntitiesException("这个任务不存在或已被删除");
|
||||
});
|
||||
|
||||
StudySessionsEntity session = this.getOneOpt(Wrappers.<StudySessionsEntity>lambdaQuery()
|
||||
.eq(StudySessionsEntity::getTaskNum, taskNum)
|
||||
@@ -88,10 +96,10 @@ public class StudySessionsServiceImpl extends ServiceImpl<StudySessionsMapper, S
|
||||
public void pauseStudySession(String sessionNum, LocalDateTime endTime) throws ErrorParameterException, ServiceException {
|
||||
StudySessionsEntity studySessionsEntity = this.getOneOpt(Wrappers.lambdaQuery(StudySessionsEntity.class)
|
||||
.eq(StudySessionsEntity::getSessionNum, sessionNum))
|
||||
.orElseThrow(() -> new ErrorParameterException("会话[" + sessionNum + "]不存在"));
|
||||
.orElseThrow(() -> sessionNotFound(sessionNum));
|
||||
if (StudySessionStateEnum.PAUSED.name().equals(studySessionsEntity.getSessionState())) {
|
||||
log.error("会话[" + studySessionsEntity.getSessionNum() + "]重复暂停");
|
||||
throw new ErrorParameterException("会话[" + sessionNum + "]已经暂停,请勿重复暂停!");
|
||||
throw new ErrorParameterException("这次学习会话已经暂停了,不用重复暂停哦");
|
||||
}
|
||||
studySessionsEntity.calculatePointerPosition();
|
||||
studySessionsEntity.pausedStudySession(endTime);
|
||||
@@ -104,7 +112,7 @@ public class StudySessionsServiceImpl extends ServiceImpl<StudySessionsMapper, S
|
||||
public String endedStudySession(String sessionNum, String content) throws ErrorParameterException {
|
||||
StudySessionsEntity studySessionsEntity = this.getOneOpt(Wrappers.lambdaQuery(StudySessionsEntity.class)
|
||||
.eq(StudySessionsEntity::getSessionNum, sessionNum))
|
||||
.orElseThrow(() -> new ErrorParameterException("会话[" + sessionNum + "]不存在"));
|
||||
.orElseThrow(() -> sessionNotFound(sessionNum));
|
||||
boolean wasOngoing = StudySessionStateEnum.ONGOING.name().equals(studySessionsEntity.getSessionState());
|
||||
studySessionsEntity.endedStudySession();
|
||||
this.updateById(studySessionsEntity);
|
||||
@@ -117,14 +125,42 @@ public class StudySessionsServiceImpl extends ServiceImpl<StudySessionsMapper, S
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@Override
|
||||
@Transactional
|
||||
public void abortStudySession(String sessionNum, String confirmation) throws ErrorParameterException {
|
||||
StudySessionsEntity studySessionsEntity = this.getOneOpt(Wrappers.lambdaQuery(StudySessionsEntity.class)
|
||||
.eq(StudySessionsEntity::getSessionNum, sessionNum))
|
||||
.orElseThrow(() -> sessionNotFound(sessionNum));
|
||||
if (StudySessionStateEnum.ENDED.name().equals(studySessionsEntity.getSessionState())) {
|
||||
log.warn("会话[{}]已结束,不能误操作结束", sessionNum);
|
||||
throw new ErrorParameterException("这次学习会话已经结束,不能再误操作结束了");
|
||||
}
|
||||
if (!MISOPERATION_CONFIRMATION.equals(confirmation == null ? "" : confirmation.trim())) {
|
||||
log.warn("会话[{}]误操作结束的确认语不正确", sessionNum);
|
||||
throw new ErrorParameterException("确认语输入不正确,请核对后重新输入");
|
||||
}
|
||||
long fragmentCount = studyReportFragmentsMapper.selectCount(Wrappers.lambdaQuery(StudyReportFragmentsEntity.class)
|
||||
.eq(StudyReportFragmentsEntity::getSessionNum, sessionNum));
|
||||
if (fragmentCount > 0) {
|
||||
log.warn("会话[{}]已产生{}条学习残片,不能误操作结束", sessionNum, fragmentCount);
|
||||
throw new ErrorParameterException("这次学习已经产生了学习残片,不能按误操作结束了哦");
|
||||
}
|
||||
// 严格零数据:学习预期与会话本身一并删除,系统视角下会话从未发生
|
||||
studyExpectationsService.deleteBySessionNum(sessionNum);
|
||||
this.removeById(studySessionsEntity.getId());
|
||||
log.info("会话[{}]误操作结束,未产生任何学习数据", sessionNum);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StudySessionResponse getNotEndedStudySessionByTaskNum(String taskNum) throws ErrorParameterException {
|
||||
TaskEntity task = tasksServiceImpl.getOneOpt(Wrappers.lambdaQuery(TaskEntity.class)
|
||||
.eq(TaskEntity::getTaskNum, taskNum))
|
||||
.orElseThrow(() -> new ErrorParameterException("任务[" + taskNum + "]不存在"));
|
||||
.orElseThrow(() -> taskNotFound(taskNum));
|
||||
StudySessionsDto dto = Optional.ofNullable(studyReportsMapper.getNotEndedStudySessionDtoByTaskNum(taskNum))
|
||||
.orElseThrow(() -> new ErrorParameterException("任务[" + taskNum + "]不存在进行中或暂停中的会话"));
|
||||
.orElseThrow(() -> {
|
||||
log.warn("任务[{}]不存在进行中或暂停中的会话", taskNum);
|
||||
return new ErrorParameterException("这个任务当前没有进行中或已暂停的学习会话");
|
||||
});
|
||||
return StudySessionConvert.MAPPER.toStudySessionResponse(dto);
|
||||
}
|
||||
|
||||
@@ -136,7 +172,7 @@ public class StudySessionsServiceImpl extends ServiceImpl<StudySessionsMapper, S
|
||||
.eq(StudyReportFragmentsEntity::getSessionNum, sessionNum))
|
||||
.stream().map(StudyReportFragmentsEntity::getContent).collect(Collectors.toCollection(ArrayList::new));
|
||||
} else {
|
||||
throw new ErrorParameterException("会话[" + sessionNum + "]不存在");
|
||||
throw sessionNotFound(sessionNum);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,13 +181,13 @@ public class StudySessionsServiceImpl extends ServiceImpl<StudySessionsMapper, S
|
||||
public void continueStudySession(String sessionNum) throws ErrorParameterException, ServiceException {
|
||||
StudySessionsEntity studySessionsEntity = this.getOneOpt(Wrappers.lambdaQuery(StudySessionsEntity.class)
|
||||
.eq(StudySessionsEntity::getSessionNum, sessionNum))
|
||||
.orElseThrow(() -> new ErrorParameterException("会话[" + sessionNum + "]不存在"));
|
||||
.orElseThrow(() -> sessionNotFound(sessionNum));
|
||||
if (StudySessionStateEnum.ONGOING.name().equals(studySessionsEntity.getSessionState())) {
|
||||
log.error("会话[" + studySessionsEntity.getSessionNum() + "]重复开始");
|
||||
throw new ErrorParameterException("会话[" + sessionNum + "]已经开始,请勿重复开始!");
|
||||
throw new ErrorParameterException("这次学习会话已经开始了,不用重复开始哦");
|
||||
} else if (StudySessionStateEnum.ENDED.name().equals(studySessionsEntity.getSessionState())) {
|
||||
log.error("会话[" + studySessionsEntity.getSessionNum() + "]处于结束状态,不可开始该会话!");
|
||||
throw new ErrorParameterException("会话[" + sessionNum + "]处于结束状态,不可开始该会话!");
|
||||
throw new ErrorParameterException("这次学习会话已经结束,无法再次开始");
|
||||
}
|
||||
studySessionsEntity.continueStudySession();
|
||||
this.updateById(studySessionsEntity);
|
||||
@@ -163,7 +199,7 @@ public class StudySessionsServiceImpl extends ServiceImpl<StudySessionsMapper, S
|
||||
public StudySessionResponse getStudySessionBySessionNum(String sessionNum) throws ErrorParameterException {
|
||||
StudySessionsEntity session = this.getOneOpt(Wrappers.lambdaQuery(StudySessionsEntity.class)
|
||||
.eq(StudySessionsEntity::getSessionNum, sessionNum))
|
||||
.orElseThrow(() -> new ErrorParameterException("会话[" + sessionNum + "]不存在"));
|
||||
.orElseThrow(() -> sessionNotFound(sessionNum));
|
||||
return StudySessionConvert.MAPPER.toStudySessionResponse(session);
|
||||
}
|
||||
|
||||
@@ -174,7 +210,7 @@ public class StudySessionsServiceImpl extends ServiceImpl<StudySessionsMapper, S
|
||||
public String generateReportDraft(String sessionNum) throws ErrorParameterException {
|
||||
StudySessionsEntity session = this.getOneOpt(Wrappers.lambdaQuery(StudySessionsEntity.class)
|
||||
.eq(StudySessionsEntity::getSessionNum, sessionNum))
|
||||
.orElseThrow(() -> new ErrorParameterException("会话[" + sessionNum + "]不存在"));
|
||||
.orElseThrow(() -> sessionNotFound(sessionNum));
|
||||
|
||||
ArrayList<String> fragments = getAllFragments(sessionNum);
|
||||
if (fragments.isEmpty()) {
|
||||
@@ -262,4 +298,14 @@ public class StudySessionsServiceImpl extends ServiceImpl<StudySessionsMapper, S
|
||||
return response;
|
||||
}
|
||||
|
||||
private ErrorParameterException sessionNotFound(String sessionNum) {
|
||||
log.warn("会话[{}]不存在", sessionNum);
|
||||
return new ErrorParameterException("这次学习会话不存在或已结束");
|
||||
}
|
||||
|
||||
private ErrorParameterException taskNotFound(String taskNum) {
|
||||
log.warn("任务[{}]不存在", taskNum);
|
||||
return new ErrorParameterException("这个任务不存在或已被删除");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -60,7 +60,8 @@ public class TasksServiceImpl extends ServiceImpl<TasksMapper, TaskEntity>
|
||||
|
||||
if (this.exists(Wrappers.lambdaQuery(TaskEntity.class)
|
||||
.eq(TaskEntity::getTaskName, task.getTaskName()))) {
|
||||
throw new ErrorParameterException(String.format("任务名[%s]重复", task.getTaskName()));
|
||||
log.warn("任务名[{}]重复", task.getTaskName());
|
||||
throw new ErrorParameterException("已经有同名任务了,换个任务名称吧");
|
||||
}
|
||||
|
||||
task.setTaskNum(GenerateNumTool.generateNum("TASK"));
|
||||
@@ -132,7 +133,10 @@ public class TasksServiceImpl extends ServiceImpl<TasksMapper, TaskEntity>
|
||||
@Override
|
||||
public TaskApplicationEntity updateApplication(Integer id, UpdateTaskApplicationRequest request) throws NotFindEntitiesException {
|
||||
TaskApplicationEntity existing = Optional.ofNullable(taskApplicationMapper.selectById(id))
|
||||
.orElseThrow(() -> new NotFindEntitiesException("应用场景[" + id + "]不存在"));
|
||||
.orElseThrow(() -> {
|
||||
log.warn("应用场景[{}]不存在", id);
|
||||
return new NotFindEntitiesException("这个应用场景不存在或已被删除");
|
||||
});
|
||||
existing.setTitle(request.getTitle());
|
||||
existing.setDescription(request.getDescription());
|
||||
existing.setResourceUrl(request.getResourceUrl());
|
||||
@@ -144,14 +148,18 @@ public class TasksServiceImpl extends ServiceImpl<TasksMapper, TaskEntity>
|
||||
@Override
|
||||
public void deleteApplication(Integer id) throws NotFindEntitiesException {
|
||||
TaskApplicationEntity existing = Optional.ofNullable(taskApplicationMapper.selectById(id))
|
||||
.orElseThrow(() -> new NotFindEntitiesException("应用场景[" + id + "]不存在"));
|
||||
.orElseThrow(() -> {
|
||||
log.warn("应用场景[{}]不存在", id);
|
||||
return new NotFindEntitiesException("这个应用场景不存在或已被删除");
|
||||
});
|
||||
taskApplicationMapper.deleteById(existing.getId());
|
||||
}
|
||||
|
||||
private void ensureTaskExists(String taskNum) throws NotFindEntitiesException {
|
||||
if (!StringUtils.hasText(taskNum) || !tasksMapper.exists(
|
||||
Wrappers.<TaskEntity>lambdaQuery().eq(TaskEntity::getTaskNum, taskNum))) {
|
||||
throw new NotFindEntitiesException("任务[" + taskNum + "]不存在");
|
||||
log.warn("任务[{}]不存在", taskNum);
|
||||
throw new NotFindEntitiesException("这个任务不存在或已被删除");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ public class CalculatedPriorityTool {
|
||||
|
||||
/**
|
||||
* 使用系统默认权重计算加权优先级。
|
||||
* 急迫性 0.35 / 重要性 0.25 / 内容难度 0.20 / 未来价值 0.10 / 主观优先级 0.10
|
||||
* 紧急性 0.35 / 重要性 0.25 / 内容难度 0.20 / 未来价值 0.10 / 主观优先级 0.10
|
||||
*/
|
||||
public static Double calculatedPriority(PriorityDto priorityDto) {
|
||||
return calculatedPriority(priorityDto, UserPriorityWeightsEntity.defaults());
|
||||
|
||||
+6
-14
@@ -62,13 +62,13 @@ class ReviewServiceImplTest {
|
||||
void getReviewFeed_smartMode_prefersLowRecallRatioContent() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
StudyReportsEntity oldLowMastery = new StudyReportsEntity();
|
||||
StudyReportFragmentsEntity oldLowMastery = new StudyReportFragmentsEntity();
|
||||
oldLowMastery.setId(1);
|
||||
oldLowMastery.setSessionNum("S_LOW");
|
||||
oldLowMastery.setContent("很久没复习且掌握度低");
|
||||
oldLowMastery.setCreatedTime(now.minusDays(60));
|
||||
|
||||
StudyReportsEntity freshHighMastery = new StudyReportsEntity();
|
||||
StudyReportFragmentsEntity freshHighMastery = new StudyReportFragmentsEntity();
|
||||
freshHighMastery.setId(2);
|
||||
freshHighMastery.setSessionNum("S_HIGH");
|
||||
freshHighMastery.setContent("刚学完且掌握度高");
|
||||
@@ -93,8 +93,7 @@ class ReviewServiceImplTest {
|
||||
highRatioRecord.setRecallRatio(1.0);
|
||||
highRatioRecord.setCreatedTime(now);
|
||||
|
||||
when(studyReportsMapper.selectList(any())).thenReturn(List.of(oldLowMastery, freshHighMastery));
|
||||
when(studyReportFragmentsMapper.selectList(any())).thenReturn(List.of());
|
||||
when(studyReportFragmentsMapper.selectList(any())).thenReturn(List.of(oldLowMastery, freshHighMastery));
|
||||
when(studySessionsMapper.selectList(any())).thenReturn(List.of(sessionLow, sessionHigh));
|
||||
when(tasksMapper.selectList(any())).thenReturn(List.of(taskLow, taskHigh));
|
||||
when(reviewRecallRecordMapper.selectList(any())).thenReturn(List.of(highRatioRecord));
|
||||
@@ -116,15 +115,9 @@ class ReviewServiceImplTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void getReviewFeed_shouldApplyFinalLimitAfterMerge() {
|
||||
StudyReportsEntity report = new StudyReportsEntity();
|
||||
report.setId(1);
|
||||
report.setSessionNum("S1");
|
||||
report.setContent("report");
|
||||
report.setCreatedTime(LocalDateTime.now());
|
||||
|
||||
void getReviewFeed_shouldReturnFragmentsOnly() {
|
||||
StudyReportFragmentsEntity fragment = new StudyReportFragmentsEntity();
|
||||
fragment.setId(2);
|
||||
fragment.setId(1);
|
||||
fragment.setSessionNum("S1");
|
||||
fragment.setContent("fragment");
|
||||
fragment.setCreatedTime(LocalDateTime.now().minusMinutes(1));
|
||||
@@ -137,7 +130,6 @@ class ReviewServiceImplTest {
|
||||
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));
|
||||
@@ -145,7 +137,7 @@ class ReviewServiceImplTest {
|
||||
List<ReviewFeedItem> items = reviewService.getReviewFeed(1, "recent");
|
||||
|
||||
assertEquals(1, items.size());
|
||||
assertEquals("REPORT", items.get(0).getSourceType());
|
||||
assertEquals("FRAGMENT", items.get(0).getSourceType());
|
||||
assertEquals("Task", items.get(0).getTaskName());
|
||||
}
|
||||
|
||||
|
||||
+1
-3
@@ -36,8 +36,6 @@ class StandardMindMapServiceImplTest {
|
||||
@Mock
|
||||
private StudyReportsMapper studyReportsMapper;
|
||||
@Mock
|
||||
private StudyReportFragmentsMapper studyReportFragmentsMapper;
|
||||
@Mock
|
||||
private TaskApplicationMapper taskApplicationMapper;
|
||||
@Mock
|
||||
private StudySessionsMapper studySessionsMapper;
|
||||
@@ -189,7 +187,7 @@ class StandardMindMapServiceImplTest {
|
||||
private StandardMindMapServiceImpl createService() {
|
||||
StandardMindMapServiceImpl s = new StandardMindMapServiceImpl(
|
||||
standardMindMapMapper, recallRecordMapper,
|
||||
tasksMapper, studyReportsMapper, studyReportFragmentsMapper,
|
||||
tasksMapper, studyReportsMapper,
|
||||
taskApplicationMapper, studySessionsMapper,
|
||||
List.of(mockAiClient), objectMapper, aiServiceClient);
|
||||
return s;
|
||||
|
||||
+103
@@ -4,9 +4,11 @@ import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.guo.learningprogresstracker.entity.StudyReportsEntity;
|
||||
import com.guo.learningprogresstracker.entity.StudySessionsEntity;
|
||||
import com.guo.learningprogresstracker.enums.StudySessionStateEnum;
|
||||
import com.guo.learningprogresstracker.exception.ErrorParameterException;
|
||||
import com.guo.learningprogresstracker.mapper.StudyReportFragmentsMapper;
|
||||
import com.guo.learningprogresstracker.mapper.StudyReportsMapper;
|
||||
import com.guo.learningprogresstracker.mapper.StudySessionsMapper;
|
||||
import com.guo.learningprogresstracker.service.StudyExpectationsService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -17,6 +19,7 @@ import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -41,10 +44,15 @@ class StudySessionsServiceImplTest {
|
||||
@Mock
|
||||
private StudySessionsMapper studySessionsMapper;
|
||||
|
||||
@Mock
|
||||
private StudyExpectationsService studyExpectationsService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// stub updateById,避免调用真实的 getBaseMapper()
|
||||
doReturn(true).when(studySessionsService).updateById(any());
|
||||
// stub removeById,避免调用真实的 getBaseMapper()
|
||||
doReturn(true).when(studySessionsService).removeById(any(Serializable.class));
|
||||
}
|
||||
|
||||
private StudySessionsEntity createSession(String state, int minutesAgo) {
|
||||
@@ -210,4 +218,99 @@ class StudySessionsServiceImplTest {
|
||||
|
||||
verify(studySessionsService).updateById(session);
|
||||
}
|
||||
|
||||
// ============ 误操作结束(零数据关闭) ============
|
||||
|
||||
/**
|
||||
* 场景11:确认语正确且未产生残片 → 删除预期与会话,零数据关闭
|
||||
*/
|
||||
@Test
|
||||
void abortStudySession_success_shouldDeleteEverything() throws Exception {
|
||||
StudySessionsEntity session = createSession(StudySessionStateEnum.ONGOING.name(), 5);
|
||||
session.setId(1);
|
||||
mockGetOneOpt(session);
|
||||
doReturn(0L).when(studyReportFragmentsMapper).selectCount(any());
|
||||
|
||||
studySessionsService.abortStudySession("SESSION_TEST", StudySessionsServiceImpl.MISOPERATION_CONFIRMATION);
|
||||
|
||||
verify(studyExpectationsService).deleteBySessionNum("SESSION_TEST");
|
||||
verify(studySessionsService).removeById(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景12:确认语不正确 → 抛异常,不删除任何数据
|
||||
*/
|
||||
@Test
|
||||
void abortStudySession_wrongConfirmation_shouldThrow() {
|
||||
StudySessionsEntity session = createSession(StudySessionStateEnum.ONGOING.name(), 5);
|
||||
mockGetOneOpt(session);
|
||||
|
||||
ErrorParameterException ex = assertThrows(ErrorParameterException.class, () ->
|
||||
studySessionsService.abortStudySession("SESSION_TEST", "我不是确认语"));
|
||||
|
||||
assertEquals("确认语输入不正确,请核对后重新输入", ex.getMessage());
|
||||
verify(studyExpectationsService, never()).deleteBySessionNum(any());
|
||||
verify(studySessionsService, never()).removeById(any());
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景13:确认语首尾空白应被忽略,仍视为正确
|
||||
*/
|
||||
@Test
|
||||
void abortStudySession_confirmationWithSpaces_shouldSucceed() throws Exception {
|
||||
StudySessionsEntity session = createSession(StudySessionStateEnum.PAUSED.name(), 5);
|
||||
session.setId(2);
|
||||
mockGetOneOpt(session);
|
||||
doReturn(0L).when(studyReportFragmentsMapper).selectCount(any());
|
||||
|
||||
studySessionsService.abortStudySession("SESSION_TEST",
|
||||
" " + StudySessionsServiceImpl.MISOPERATION_CONFIRMATION + " ");
|
||||
|
||||
verify(studyExpectationsService).deleteBySessionNum("SESSION_TEST");
|
||||
verify(studySessionsService).removeById(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景14:已产生学习残片 → 抛异常,不能误操作结束
|
||||
*/
|
||||
@Test
|
||||
void abortStudySession_hasFragments_shouldThrow() {
|
||||
StudySessionsEntity session = createSession(StudySessionStateEnum.ONGOING.name(), 5);
|
||||
mockGetOneOpt(session);
|
||||
doReturn(1L).when(studyReportFragmentsMapper).selectCount(any());
|
||||
|
||||
ErrorParameterException ex = assertThrows(ErrorParameterException.class, () ->
|
||||
studySessionsService.abortStudySession("SESSION_TEST", StudySessionsServiceImpl.MISOPERATION_CONFIRMATION));
|
||||
|
||||
assertEquals("这次学习已经产生了学习残片,不能按误操作结束了哦", ex.getMessage());
|
||||
verify(studyExpectationsService, never()).deleteBySessionNum(any());
|
||||
verify(studySessionsService, never()).removeById(any());
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景15:会话已结束 → 抛异常
|
||||
*/
|
||||
@Test
|
||||
void abortStudySession_endedSession_shouldThrow() {
|
||||
StudySessionsEntity session = createSession(StudySessionStateEnum.ENDED.name(), 5);
|
||||
mockGetOneOpt(session);
|
||||
|
||||
ErrorParameterException ex = assertThrows(ErrorParameterException.class, () ->
|
||||
studySessionsService.abortStudySession("SESSION_TEST", StudySessionsServiceImpl.MISOPERATION_CONFIRMATION));
|
||||
|
||||
assertEquals("这次学习会话已经结束,不能再误操作结束了", ex.getMessage());
|
||||
verify(studySessionsService, never()).removeById(any());
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景16:会话不存在 → 抛异常
|
||||
*/
|
||||
@Test
|
||||
void abortStudySession_sessionNotFound_shouldThrow() {
|
||||
doReturn(Optional.empty()).when(studySessionsService).getOneOpt(any(Wrapper.class));
|
||||
|
||||
assertThrows(ErrorParameterException.class, () ->
|
||||
studySessionsService.abortStudySession("NOT_EXIST", StudySessionsServiceImpl.MISOPERATION_CONFIRMATION));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user