Compare commits
27 Commits
task-1
...
81543969d8
| Author | SHA1 | Date | |
|---|---|---|---|
| 81543969d8 | |||
| 2cf3533141 | |||
| d0c78ceb28 | |||
| d7e1d9a10c | |||
| a64daacc71 | |||
| 591dc89e53 | |||
| c7eb448e33 | |||
| d5835cdf55 | |||
| 487b38e8b0 | |||
| e1cd5d5b82 | |||
| 6bd6b00518 | |||
| aadf1911ce | |||
| 82948e3ff9 | |||
| fe6983cb69 | |||
| 27cb1b21fb | |||
| 4b2bd5a47a | |||
| b6bc587ea2 | |||
| a92465dfc0 | |||
| 78a24e8394 | |||
| 1fc41f1443 | |||
| 24d871b430 | |||
| 950c91791f | |||
| f797a712db | |||
| a932bf52ca | |||
| 4b26ed46ec | |||
| 8409dc58ab | |||
| 0beaed8121 |
@@ -0,0 +1,47 @@
|
||||
# 项目规范
|
||||
|
||||
## 数据库迁移(Flyway Migration)
|
||||
|
||||
### 命名规则
|
||||
- 脚本格式:`V{YYYYMMDD}_{序号}__{描述}.sql`
|
||||
- 日期必须使用**实际编写日期**,不得使用过去的日期
|
||||
- 序号从 1 开始,同一天多个脚本递增
|
||||
- 描述使用下划线分隔的英文短语
|
||||
|
||||
### 核心原则
|
||||
- **不可变性**:已执行的迁移脚本永远不得修改
|
||||
- **只增不减**:数据库变更只能通过新增迁移脚本实现
|
||||
- **向后兼容**:新脚本应兼容已有数据
|
||||
|
||||
### 操作规范
|
||||
- 删除表:创建新迁移脚本,使用 `DROP TABLE IF EXISTS`
|
||||
- 修改表结构:使用 `ALTER TABLE` 语句
|
||||
- 新增表:创建新迁移脚本,使用 `CREATE TABLE`
|
||||
|
||||
## 框架机制
|
||||
|
||||
### 行级数据隔离(多租户拦截器)
|
||||
|
||||
- **配置类:** `MybatisPlusConfig` 注册 `TenantLineInnerInterceptor`
|
||||
- **租户字段:** `created_by`(每个业务表的创建人字段)
|
||||
- **租户值来源:** `StpUtil.getLoginIdAsString()`(当前登录用户)
|
||||
- **自动注入:** 所有 SELECT/UPDATE/DELETE 语句自动追加 `WHERE created_by = #{当前用户}`
|
||||
- **归属校验:** 查询单条记录时拦截器自动校验 `created_by`,非本人数据直接返回空
|
||||
- **排除表:** `user`、`flyway_schema_history`、`databasechangelog`、`databasechangeloglock`
|
||||
|
||||
### 自动填充(MetaObjectHandler)
|
||||
|
||||
- **created_by / updated_by:** 插入/更新时自动填充为当前登录用户
|
||||
- **created_time / updated_time:** 插入/更新时自动填充当前时间
|
||||
|
||||
### 认证鉴权(Sa-Token)
|
||||
|
||||
- **会话管理:** 基于 `StpUtil` 的登录/登出/会话查询
|
||||
- **权限校验:** `@SaCheckPermission` 注解式权限控制
|
||||
- **未登录处理:** 全局异常处理器捕获 `NotLoginException` 返回 401
|
||||
|
||||
## 编码规范
|
||||
|
||||
### 数据访问层
|
||||
- **优先使用纯 MyBatis-Plus Java API**(`Wrappers.<T>lambdaQuery()`、`selectList`、`selectById` 等),避免手写 XML SQL
|
||||
- 复杂查询通过 `Wrappers` 链式构建条件,必要时使用 `.apply()` 拼接原生 SQL 片段
|
||||
@@ -129,6 +129,13 @@
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- jBCrypt (独立 BCrypt 实现,无 Spring Security 依赖) -->
|
||||
<dependency>
|
||||
<groupId>org.mindrot</groupId>
|
||||
<artifactId>jbcrypt</artifactId>
|
||||
<version>0.4</version>
|
||||
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
@@ -142,7 +149,7 @@
|
||||
<forceJavacCompilerUse>true</forceJavacCompilerUse>
|
||||
<source>17</source>
|
||||
<target>17</target>
|
||||
<compilerArgs>--enable-preview</compilerArgs>
|
||||
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
---
|
||||
name: comment-cleanup
|
||||
description: 提交代码前优化 Java 注释,移除冗余描述性注释,保留功能性注释和业务上下文注释。
|
||||
metadata:
|
||||
short-description: 清理 Java 代码中的冗余注释
|
||||
---
|
||||
|
||||
# Comment Cleanup
|
||||
|
||||
在代码提交前,扫描变更的 Java 文件,清理冗余注释,保留有价值的注释。
|
||||
|
||||
## 触发条件
|
||||
|
||||
用户要求清理注释、优化注释、提交前检查注释时使用。
|
||||
|
||||
## 工作流程
|
||||
|
||||
1. 定位待清理的 Java 文件(通常是 `git diff` 中变更的文件)
|
||||
2. 逐文件扫描注释,按规则分类处理
|
||||
3. 执行清理(删除或改造)
|
||||
4. 输出清理报告
|
||||
|
||||
## 注释分类规则
|
||||
|
||||
### 移除:类型 A — 代码语义重复注释
|
||||
|
||||
代码本身已经清晰表达意图时,删除多余的行内注释。
|
||||
|
||||
```java
|
||||
// ❌ 移除:代码已经说清楚了
|
||||
// 转换报告
|
||||
List<ReviewFeedItem> reportItems = reports.stream().map(r -> toFeedItem(r)).collect(Collectors.toList());
|
||||
|
||||
// ❌ 移除:合并逻辑一眼就能看出
|
||||
// 合并并按创建时间倒序
|
||||
return Stream.concat(reportItems.stream(), fragmentItems.stream())
|
||||
.sorted(Comparator.comparing(ReviewFeedItem::getCreatedTime).reversed())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// ❌ 移除:查询目的从变量名已可知
|
||||
// 先查该任务下的所有 sessionNum
|
||||
List<String> sessionNums = studySessionsMapper.selectList(...)
|
||||
```
|
||||
|
||||
**判断标准:** 如果删掉注释后,一个熟悉 Java/Spring 的开发者看代码没有任何困惑,就该移除。
|
||||
|
||||
**例外:** 方法级的 Javadoc(`/** ... */`)即使与代码重复,也保留,因为它服务于 IDE 提示和文档生成。
|
||||
|
||||
### 移除:类型 B — 框架机制注释
|
||||
|
||||
框架隐式行为(拦截器、MetaObjectHandler、AOP 等)的说明不在代码中重复标注,而是在 `AGENTS.md` 的"框架机制"章节集中描述。代码中的此类注释一律移除。
|
||||
|
||||
```java
|
||||
// ❌ 移除:框架机制已在 AGENTS.md 中说明
|
||||
// created_by 条件由 TenantLineInnerInterceptor 自动注入
|
||||
List<StudyReportsEntity> reports = studyReportsMapper.selectList(...)
|
||||
|
||||
// ❌ 移除
|
||||
// 拦截器自动校验归属
|
||||
return Optional.ofNullable(studyReportsMapper.selectById(id)) ...
|
||||
|
||||
// ❌ 移除
|
||||
// created_by 由 MetaObjectHandler 自动填充
|
||||
studySessionsServiceImpl.save(...)
|
||||
```
|
||||
|
||||
### 移除:类型 D — 空注释 / 无信息量注释
|
||||
|
||||
```java
|
||||
// ❌ 空 Javadoc
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@TableField(value = "created_time")
|
||||
private LocalDateTime createdTime;
|
||||
|
||||
// ❌ 纯标注作者(无版本/日期等有价值信息时)
|
||||
/**
|
||||
* @author guo
|
||||
*/
|
||||
public class GlobalExceptionHandler { ... }
|
||||
|
||||
// ❌ 重复 HTTP 状态码(CommonResult.error 已隐含 400)
|
||||
//code:400
|
||||
return CommonResult.error(ex.getMessage());
|
||||
```
|
||||
|
||||
### 保留:类型 C — 字段/类 Javadoc
|
||||
|
||||
字段 Javadoc 描述数据含义,类 Javadoc 描述模块职能。
|
||||
|
||||
**字段 Javadoc:** 保留,说明字段的业务含义。
|
||||
|
||||
```java
|
||||
// ✅ 保留:字段含义对理解数据模型有帮助
|
||||
/**
|
||||
* 账号-登录用
|
||||
*/
|
||||
@TableField(value = "user_name")
|
||||
private String userName;
|
||||
```
|
||||
|
||||
**类 Javadoc:** 保留,但只描述类的职能,不包含实现技术细节。
|
||||
|
||||
```java
|
||||
// ❌ 移除:实现技术属于实现细节,不属于类描述
|
||||
/** 复习模块 Service 实现(纯 MyBatis-Plus Java API) */
|
||||
|
||||
// ✅ 保留:只描述职能
|
||||
/** 复习模块 Service 实现 */
|
||||
```
|
||||
|
||||
### 改造:类型 E — 枚举注释
|
||||
|
||||
枚举中的中文含义注释应迁移到枚举类的专用字段中,而非用注释标注。
|
||||
|
||||
```java
|
||||
// ❌ 改造前:用注释标注含义
|
||||
public enum Strategy {
|
||||
// 创建组
|
||||
CREATE("C"),
|
||||
// 更新组
|
||||
UPDATE("U");
|
||||
}
|
||||
|
||||
// ✅ 改造后:用字段存储含义
|
||||
public enum Strategy {
|
||||
CREATE("C", "创建组"),
|
||||
UPDATE("U", "更新组");
|
||||
|
||||
private final String code;
|
||||
private final String label;
|
||||
|
||||
Strategy(String code, String label) {
|
||||
this.code = code;
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
public String getCode() { return code; }
|
||||
public String getLabel() { return label; }
|
||||
}
|
||||
```
|
||||
|
||||
如果枚举已有 `label`/`desc` 等字段,则直接删除注释。
|
||||
|
||||
**常量注释保留不变:**
|
||||
```java
|
||||
// ✅ 常量注释保留
|
||||
// 最大重试次数
|
||||
private static final int MAX_RETRY = 3;
|
||||
```
|
||||
|
||||
### 保留:类型 F — TODO / FIXME 注释
|
||||
|
||||
```java
|
||||
// ✅ 保留
|
||||
//todo 参数传递未加密
|
||||
```
|
||||
|
||||
## 执行命令
|
||||
|
||||
```bash
|
||||
# 获取变更的 Java 文件
|
||||
git diff --cached --name-only --diff-filter=ACMR -- '*.java'
|
||||
|
||||
# 对每个文件逐行扫描,按上述规则处理
|
||||
```
|
||||
|
||||
## 输出格式
|
||||
|
||||
清理完成后,输出简要报告:
|
||||
|
||||
```
|
||||
✅ 清理完成,共处理 N 个文件:
|
||||
- 移除冗余注释 X 处
|
||||
- 改造枚举注释 Y 处(需人工确认新增字段)
|
||||
- 保留注释 Z 处(字段Javadoc/TODO)
|
||||
```
|
||||
@@ -0,0 +1,6 @@
|
||||
schema_version: v1
|
||||
interface:
|
||||
display_name: Java 注释清理
|
||||
short_description: 清理 Java 代码中的冗余注释,保留功能性注释
|
||||
default_prompt: |
|
||||
使用 $comment-cleanup 扫描变更的 Java 文件,移除冗余的描述性注释,保留框架机制注释、字段 Javadoc 和 TODO 注释。
|
||||
@@ -15,41 +15,33 @@ import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
|
||||
/**
|
||||
* @author guo
|
||||
*/
|
||||
@RestControllerAdvice
|
||||
@Slf4j
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler({ErrorParameterException.class})
|
||||
public CommonResult errorParameterException(ErrorParameterException ex) {
|
||||
//code:400
|
||||
return CommonResult.error(ex.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler({NotFindEntitiesException.class})
|
||||
public CommonResult notFindEntitiesException(NotFindEntitiesException ex) {
|
||||
//code:400
|
||||
return CommonResult.error(ex.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler({AppException.class})
|
||||
public CommonResult AppException(AppException ex) {
|
||||
//code:500
|
||||
return CommonResult.serverError(ex.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public CommonResult MyMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
|
||||
BindingResult bindingResult = ex.getBindingResult();
|
||||
//code:400
|
||||
return CommonResult.error(bindingResult.getFieldError().getDefaultMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(NotLoginException.class)
|
||||
public CommonResult handleNotLogin(NotLoginException e, HttpServletRequest req, HttpServletResponse res) {
|
||||
//code:401
|
||||
res.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||
return new CommonResult<>(HttpStatus.UNAUTHORIZED.value(), "未登录,请重新登录", null);
|
||||
}
|
||||
@@ -57,8 +49,7 @@ public class GlobalExceptionHandler {
|
||||
@ExceptionHandler(Exception.class)
|
||||
public CommonResult Exception(Exception ex) {
|
||||
log.error("系统异常", ex);
|
||||
//code:400
|
||||
return CommonResult.error(ex.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,13 @@ import jakarta.validation.groups.Default;
|
||||
|
||||
/**
|
||||
* 全局数据验证分组
|
||||
* GlobalValidationGroup -> OpenGroups ->ops
|
||||
* @author guo
|
||||
*/
|
||||
public interface Ops {
|
||||
// 创建组
|
||||
interface CreateG extends Default {}
|
||||
interface CreateG extends Default {
|
||||
}
|
||||
|
||||
// 更新组
|
||||
interface UpdateG {}
|
||||
interface UpdateG {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,21 +10,16 @@ import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* @author guo
|
||||
*/
|
||||
@Configuration
|
||||
public class JacksonConfig {
|
||||
@Bean
|
||||
public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) {
|
||||
ObjectMapper mapper = builder.build();
|
||||
|
||||
// 注册自定义序列化器
|
||||
SimpleModule module = new SimpleModule();
|
||||
module.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer());
|
||||
mapper.registerModule(module);
|
||||
|
||||
// 关闭序列化为时间戳(使用字符串格式)
|
||||
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
||||
|
||||
return mapper;
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.guo.learningprogresstracker.config;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import net.sf.jsqlparser.expression.StringValue;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* MyBatis-Plus 多租户配置
|
||||
* 利用 TenantLineInnerInterceptor 自动在 SELECT/UPDATE/DELETE 语句中
|
||||
* 注入 WHERE created_by = #{当前登录用户},实现行级数据隔离。
|
||||
*/
|
||||
@Configuration
|
||||
public class MybatisPlusConfig {
|
||||
|
||||
/**
|
||||
* 不需要租户过滤的表(仅排除不含 created_by 的系统表)
|
||||
*/
|
||||
private static final List<String> EXCLUDE_TABLES = Arrays.asList(
|
||||
"user",
|
||||
"flyway_schema_history",
|
||||
"databasechangelog",
|
||||
"databasechangeloglock"
|
||||
);
|
||||
|
||||
@Bean
|
||||
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
|
||||
@Override
|
||||
public Expression getTenantId() {
|
||||
return new StringValue(StpUtil.getLoginIdAsString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTenantIdColumn() {
|
||||
return "created_by";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean ignoreTable(String tableName) {
|
||||
return EXCLUDE_TABLES.contains(tableName.toLowerCase());
|
||||
}
|
||||
}));
|
||||
return interceptor;
|
||||
}
|
||||
}
|
||||
@@ -13,9 +13,6 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* @author Administrator
|
||||
*/
|
||||
@Profile("dev")
|
||||
@Configuration
|
||||
@Slf4j
|
||||
@@ -24,11 +21,9 @@ public class WebMvcDevConfig implements WebMvcConfigurer {
|
||||
|
||||
private final CorsProperties corsProperties;
|
||||
|
||||
// 注册拦截器
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(new SaInterceptor(handle -> {
|
||||
// OPTIONS 请求直接放行
|
||||
if ("OPTIONS".equalsIgnoreCase(SaHolder.getRequest().getMethod())) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -30,10 +30,8 @@ public class WebMvcProdConfig implements WebMvcConfigurer {
|
||||
.allowCredentials(true);
|
||||
}
|
||||
|
||||
// 注册拦截器
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
// 注册 Sa-Token 拦截器,校验规则为 StpUtil.checkLogin() 登录校验。
|
||||
registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin()))
|
||||
.addPathPatterns("/**")
|
||||
.excludePathPatterns("/login");
|
||||
|
||||
-3
@@ -9,8 +9,6 @@ import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
public class LocalDateTimeSerializer extends StdSerializer<LocalDateTime> {
|
||||
|
||||
// 修改为ISO 8601格式,末尾带Z,表示UTC时区
|
||||
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'");
|
||||
|
||||
public LocalDateTimeSerializer() {
|
||||
@@ -22,7 +20,6 @@ public class LocalDateTimeSerializer extends StdSerializer<LocalDateTime> {
|
||||
if (value == null) {
|
||||
gen.writeNull();
|
||||
} else {
|
||||
// 这里假设LocalDateTime是UTC时间,直接格式化并加Z
|
||||
String formattedDate = value.format(formatter);
|
||||
gen.writeString(formattedDate);
|
||||
}
|
||||
|
||||
@@ -13,9 +13,6 @@ import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
|
||||
/**
|
||||
* @author guo
|
||||
*/
|
||||
@CrossOrigin
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.guo.learningprogresstracker.controller;
|
||||
|
||||
import com.guo.learningprogresstracker.dto.ReviewFeedItem;
|
||||
import com.guo.learningprogresstracker.dto.ReviewTaskStats;
|
||||
import com.guo.learningprogresstracker.entity.CommonResult;
|
||||
import com.guo.learningprogresstracker.entity.StudyReportFragmentsEntity;
|
||||
import com.guo.learningprogresstracker.entity.StudyReportsEntity;
|
||||
@@ -32,6 +33,22 @@ public class ReviewController {
|
||||
return CommonResult.success(reviewService.getReviewFeed(limit));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务维度的复习统计汇总
|
||||
*/
|
||||
@GetMapping("/tasks")
|
||||
public CommonResult<List<ReviewTaskStats>> getReviewTaskStats() {
|
||||
return CommonResult.success(reviewService.getReviewTaskStats());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定任务的复习统计汇总
|
||||
*/
|
||||
@GetMapping("/tasks/{taskNum}")
|
||||
public CommonResult<ReviewTaskStats> getReviewTaskStats(@PathVariable String taskNum) {
|
||||
return CommonResult.success(reviewService.getReviewTaskStats(taskNum));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定任务下的所有报告和残片
|
||||
*/
|
||||
|
||||
+6
-11
@@ -1,13 +1,10 @@
|
||||
package com.guo.learningprogresstracker.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.google.protobuf.ServiceException;
|
||||
import com.guo.learningprogresstracker.dto.request.EndedStudySessionRequest;
|
||||
import com.guo.learningprogresstracker.dto.response.StudySessionResponse;
|
||||
import com.guo.learningprogresstracker.entity.CommonResult;
|
||||
import com.guo.learningprogresstracker.entity.StudySessionsEntity;
|
||||
import com.guo.learningprogresstracker.exception.ErrorParameterException;
|
||||
import com.guo.learningprogresstracker.mapStruct.StudySessionConvert;
|
||||
import com.guo.learningprogresstracker.service.impl.StudySessionsServiceImpl;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
@@ -15,8 +12,8 @@ import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 学习会话控制层
|
||||
@@ -39,11 +36,9 @@ public class StudySessionController {
|
||||
*/
|
||||
@GetMapping("/{sessionNum}")
|
||||
public CommonResult<StudySessionResponse> getStudySessionBySessionNum(@NotEmpty(message = "sessionNum不可为空") @PathVariable String sessionNum) throws ErrorParameterException {
|
||||
StudySessionsEntity studySessionsEntity = studySessionsServiceImpl.getOneOpt(Wrappers.lambdaQuery(StudySessionsEntity.class)
|
||||
.eq(StudySessionsEntity::getSessionNum, sessionNum))
|
||||
.orElseThrow(() -> new ErrorParameterException("会话[" + sessionNum + "]不存在"));
|
||||
StudySessionResponse studySessionResponse = StudySessionConvert.MAPPER.toStudySessionResponse(studySessionsEntity);
|
||||
return CommonResult.success(studySessionResponse);
|
||||
// 通过 service 层获取,包含归属校验
|
||||
StudySessionResponse response = studySessionsServiceImpl.getStudySessionBySessionNum(sessionNum);
|
||||
return CommonResult.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,8 +67,8 @@ public class StudySessionController {
|
||||
@PostMapping("/{sessionNum}/study-sessions/ended")
|
||||
public CommonResult<Void> endedStudySession(@PathVariable("sessionNum") String sessionNum,
|
||||
@Valid @RequestBody EndedStudySessionRequest request) throws ErrorParameterException {
|
||||
studySessionsServiceImpl.endedStudySession(sessionNum, request.getContent());
|
||||
return CommonResult.success();
|
||||
String message = studySessionsServiceImpl.endedStudySession(sessionNum, request.getContent());
|
||||
return message != null ? CommonResult.success(message) : CommonResult.success();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+24
-6
@@ -1,16 +1,17 @@
|
||||
package com.guo.learningprogresstracker.controller;
|
||||
|
||||
import com.guo.learningprogresstracker.dto.request.CreateFragmentsRequest;
|
||||
import com.guo.learningprogresstracker.dto.request.UpdateFragmentsRequest;
|
||||
import com.guo.learningprogresstracker.entity.CommonResult;
|
||||
import com.guo.learningprogresstracker.entity.StudyReportFragmentsEntity;
|
||||
import com.guo.learningprogresstracker.exception.NotFindEntitiesException;
|
||||
import com.guo.learningprogresstracker.service.impl.StudyReportFragmentsServiceImpl;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 学习残片控制层
|
||||
@@ -24,12 +25,29 @@ public class reportFragmentsController {
|
||||
|
||||
/**
|
||||
* 创建学习残片
|
||||
* @param request
|
||||
* @return
|
||||
*/
|
||||
@PostMapping
|
||||
public CommonResult<Void> createFragments(@Valid @RequestBody CreateFragmentsRequest request) throws NotFindEntitiesException {
|
||||
studyReportFragmentsServiceImpl.createFragments(request);
|
||||
return CommonResult.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新学习残片
|
||||
*/
|
||||
@PutMapping("/{id}")
|
||||
public CommonResult<Void> updateFragments(@PathVariable Integer id,
|
||||
@Valid @RequestBody UpdateFragmentsRequest request) throws NotFindEntitiesException {
|
||||
studyReportFragmentsServiceImpl.updateFragments(id, request);
|
||||
return CommonResult.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定会话的所有学习残片
|
||||
*/
|
||||
@GetMapping("/session/{sessionNum}")
|
||||
public CommonResult<List<StudyReportFragmentsEntity>> getFragmentsBySession(@PathVariable String sessionNum) {
|
||||
List<StudyReportFragmentsEntity> fragments = studyReportFragmentsServiceImpl.getFragmentsBySession(sessionNum);
|
||||
return CommonResult.success(fragments);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.guo.learningprogresstracker.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 任务维度的复习统计数据。
|
||||
*/
|
||||
@Data
|
||||
public class ReviewTaskStats {
|
||||
private String taskNum;
|
||||
private String taskName;
|
||||
private long reportCount;
|
||||
private long fragmentCount;
|
||||
private double effectiveTime;
|
||||
private long sessionCount;
|
||||
private double todayEffectiveTime;
|
||||
private double weekEffectiveTime;
|
||||
private double avgEffectiveTime;
|
||||
private double avgEffectivenessRatio;
|
||||
}
|
||||
@@ -2,9 +2,6 @@ package com.guo.learningprogresstracker.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @author guo
|
||||
*/
|
||||
@Data
|
||||
public class TaskInfo {
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.guo.learningprogresstracker.dto.request;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @author guo
|
||||
*/
|
||||
@Data
|
||||
public class UpdateFragmentsRequest {
|
||||
/**
|
||||
* 残片内容,学习内容的描述
|
||||
*/
|
||||
@NotBlank(message = "啊?无字天书?")
|
||||
private String content;
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
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 java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 存储每次学习开始前的预期
|
||||
* @TableName study_expectations
|
||||
*/
|
||||
@TableName(value ="study_expectations")
|
||||
@Data
|
||||
public class StudyExpectationsEntity extends BaseEntity implements Serializable {
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@TableId(value = "expectation_id", type = IdType.AUTO)
|
||||
private Integer expectationId;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@TableField(value = "session_id")
|
||||
private Integer sessionId;
|
||||
|
||||
/**
|
||||
* 学习预期的详细描述
|
||||
*/
|
||||
@TableField(value = "description")
|
||||
private String description;
|
||||
|
||||
|
||||
@TableField(exist = false)
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
||||
@@ -123,7 +123,6 @@ public class StudySessionsEntity extends BaseEntity implements Serializable {
|
||||
public void pausedStudySession(LocalDateTime endTime){
|
||||
if (this.getSessionState().equals(StudySessionStateEnum.PAUSED.name())) {
|
||||
log.warn("不应出现的情况:暂停了一个状态为【{}】的学习会话({})", this.getSessionState(), this.getSessionNum());
|
||||
//
|
||||
} else {
|
||||
this.setEndTime(ObjectUtils.isEmpty(endTime) ? LocalDateTime.now() : endTime);
|
||||
this.setActualTime(Duration.between(this.startTime, this.endTime).toSeconds());
|
||||
@@ -133,6 +132,11 @@ public class StudySessionsEntity extends BaseEntity implements Serializable {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 有效学习时间的最小阈值(秒),低于此值不计入总学习时间
|
||||
*/
|
||||
private static final double MIN_EFFECTIVE_TIME_SECONDS = 10 * 60;
|
||||
|
||||
/**
|
||||
* 结束会话
|
||||
*/
|
||||
@@ -141,7 +145,12 @@ public class StudySessionsEntity extends BaseEntity implements Serializable {
|
||||
log.warn("不应出现的情况:结束了一个状态为【{}】的学习会话",this.getSessionState());
|
||||
}else {
|
||||
if (this.getSessionState().equals(StudySessionStateEnum.ONGOING.name())) {
|
||||
this.pausedStudySession(endTime);
|
||||
this.pausedStudySession(LocalDateTime.now());
|
||||
}
|
||||
if (this.getEffectiveTime() < MIN_EFFECTIVE_TIME_SECONDS) {
|
||||
log.info("会话[{}]有效学习时间({}秒)不足10分钟,不计入总学习时间", this.getSessionNum(), this.getEffectiveTime());
|
||||
this.setEffectiveTime(0);
|
||||
this.setEffectivenessRatio(0);
|
||||
}
|
||||
this.setSessionState(StudySessionStateEnum.ENDED.name());
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
package com.guo.learningprogresstracker.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import java.io.Serializable;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 存储用户-任务之间的对应关系
|
||||
* @TableName user_task
|
||||
*/
|
||||
@TableName(value ="user_task")
|
||||
@Data
|
||||
public class UserTaskEntity extends BaseEntity implements Serializable {
|
||||
|
||||
@TableId
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 用户id
|
||||
*/
|
||||
@TableField(value = "user_id")
|
||||
private Integer userId;
|
||||
|
||||
/**
|
||||
* 任务id
|
||||
*/
|
||||
@TableField(value = "task_id")
|
||||
private Integer taskId;
|
||||
|
||||
|
||||
@TableField(exist = false)
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.guo.learningprogresstracker.mapStruct;
|
||||
|
||||
import com.guo.learningprogresstracker.dto.request.CreateFragmentsRequest;
|
||||
import com.guo.learningprogresstracker.dto.request.UpdateFragmentsRequest;
|
||||
import com.guo.learningprogresstracker.entity.StudyReportFragmentsEntity;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.ReportingPolicy;
|
||||
@@ -11,4 +12,6 @@ public interface FragmentsConvert {
|
||||
FragmentsConvert MAPPER = Mappers.getMapper(FragmentsConvert.class);
|
||||
|
||||
StudyReportFragmentsEntity toFragmentsEntity(CreateFragmentsRequest request);
|
||||
|
||||
StudyReportFragmentsEntity toFragmentsEntity(UpdateFragmentsRequest request);
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
package com.guo.learningprogresstracker.mapper;
|
||||
|
||||
import com.guo.learningprogresstracker.dto.ReviewFeedItem;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 复习模块自定义查询 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface ReviewMapper {
|
||||
|
||||
/**
|
||||
* 获取最近的学习报告和残片,合并排序后返回
|
||||
*/
|
||||
List<ReviewFeedItem> selectReviewFeed(@Param("limit") int limit);
|
||||
|
||||
/**
|
||||
* 获取指定任务下的所有报告和残片,按时间倒序
|
||||
*/
|
||||
List<ReviewFeedItem> selectTaskReview(@Param("taskNum") String taskNum);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package com.guo.learningprogresstracker.mapper;
|
||||
|
||||
import com.guo.learningprogresstracker.entity.StudyExpectationsEntity;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* @author guo
|
||||
* @description 针对表【study_expectations(存储每次学习开始前的预期)】的数据库操作Mapper
|
||||
* @createDate 2024-06-09 15:28:48
|
||||
* @Entity com.guo.learningprogresstracker.entity.StudyExpectationsEntity
|
||||
*/
|
||||
@Mapper
|
||||
public interface StudyExpectationsMapper extends BaseMapper<StudyExpectationsEntity> {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package com.guo.learningprogresstracker.mapper;
|
||||
|
||||
import com.guo.learningprogresstracker.entity.UserTaskEntity;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* @author guo
|
||||
* @description 针对表【user_task(存储用户-任务之间的对应关系)】的数据库操作Mapper
|
||||
* @createDate 2024-06-09 15:28:48
|
||||
* @Entity com.guo.learningprogresstracker.entity.UserTaskEntity
|
||||
*/
|
||||
@Mapper
|
||||
public interface UserTaskMapper extends BaseMapper<UserTaskEntity> {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.guo.learningprogresstracker.service;
|
||||
|
||||
import com.guo.learningprogresstracker.dto.ReviewFeedItem;
|
||||
import com.guo.learningprogresstracker.dto.ReviewTaskStats;
|
||||
import com.guo.learningprogresstracker.entity.StudyReportFragmentsEntity;
|
||||
import com.guo.learningprogresstracker.entity.StudyReportsEntity;
|
||||
import com.guo.learningprogresstracker.exception.NotFindEntitiesException;
|
||||
@@ -17,6 +18,16 @@ public interface ReviewService {
|
||||
*/
|
||||
List<ReviewFeedItem> getReviewFeed(int limit);
|
||||
|
||||
/**
|
||||
* 获取所有任务的复习统计数据
|
||||
*/
|
||||
List<ReviewTaskStats> getReviewTaskStats();
|
||||
|
||||
/**
|
||||
* 获取指定任务的复习统计数据
|
||||
*/
|
||||
ReviewTaskStats getReviewTaskStats(String taskNum);
|
||||
|
||||
/**
|
||||
* 获取指定任务下的所有报告和残片
|
||||
*/
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.guo.learningprogresstracker.service;
|
||||
|
||||
import com.guo.learningprogresstracker.entity.StudyExpectationsEntity;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
|
||||
/**
|
||||
* @author guo
|
||||
* @description 针对表【study_expectations(存储每次学习开始前的预期)】的数据库操作Service
|
||||
* @createDate 2024-06-09 15:28:48
|
||||
*/
|
||||
public interface StudyExpectationsService extends IService<StudyExpectationsEntity> {
|
||||
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
package com.guo.learningprogresstracker.service;
|
||||
|
||||
import com.guo.learningprogresstracker.dto.request.CreateFragmentsRequest;
|
||||
import com.guo.learningprogresstracker.dto.request.UpdateFragmentsRequest;
|
||||
import com.guo.learningprogresstracker.entity.StudyReportFragmentsEntity;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.guo.learningprogresstracker.exception.NotFindEntitiesException;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author guo
|
||||
* @description 针对表【study_report_fragments(记录学习过程中的学习内容报告残片)】的数据库操作Service
|
||||
@@ -13,4 +16,8 @@ import com.guo.learningprogresstracker.exception.NotFindEntitiesException;
|
||||
public interface StudyReportFragmentsService extends IService<StudyReportFragmentsEntity> {
|
||||
|
||||
void createFragments(CreateFragmentsRequest request) throws NotFindEntitiesException;
|
||||
|
||||
void updateFragments(Integer id, UpdateFragmentsRequest request) throws NotFindEntitiesException;
|
||||
|
||||
List<StudyReportFragmentsEntity> getFragmentsBySession(String sessionNum);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ public interface StudySessionsService extends IService<StudySessionsEntity> {
|
||||
|
||||
ArrayList<String> getAllFragments(String sessionNum) throws ErrorParameterException;
|
||||
|
||||
void endedStudySession(String sessionNum, String content) throws ErrorParameterException;
|
||||
String endedStudySession(String sessionNum, String content) throws ErrorParameterException;
|
||||
|
||||
void pauseStudySession(String sessionNum, LocalDateTime endTime) throws ErrorParameterException, ServiceException;
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.guo.learningprogresstracker.service;
|
||||
|
||||
import com.guo.learningprogresstracker.entity.UserTaskEntity;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
|
||||
/**
|
||||
* @author guo
|
||||
* @description 针对表【user_task(存储用户-任务之间的对应关系)】的数据库操作Service
|
||||
* @createDate 2024-06-09 15:28:48
|
||||
*/
|
||||
public interface UserTaskService extends IService<UserTaskEntity> {
|
||||
|
||||
}
|
||||
@@ -1,18 +1,28 @@
|
||||
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.entity.StudyReportFragmentsEntity;
|
||||
import com.guo.learningprogresstracker.entity.StudyReportsEntity;
|
||||
import com.guo.learningprogresstracker.entity.StudySessionsEntity;
|
||||
import com.guo.learningprogresstracker.entity.TaskEntity;
|
||||
import com.guo.learningprogresstracker.exception.NotFindEntitiesException;
|
||||
import com.guo.learningprogresstracker.mapper.ReviewMapper;
|
||||
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 com.guo.learningprogresstracker.service.ReviewService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.time.DayOfWeek;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* 复习模块 Service 实现
|
||||
@@ -21,18 +31,80 @@ import java.util.Optional;
|
||||
@RequiredArgsConstructor
|
||||
public class ReviewServiceImpl implements ReviewService {
|
||||
|
||||
private final ReviewMapper reviewMapper;
|
||||
private final StudyReportsMapper studyReportsMapper;
|
||||
private final StudyReportFragmentsMapper studyReportFragmentsMapper;
|
||||
private final StudySessionsMapper studySessionsMapper;
|
||||
private final TasksMapper tasksMapper;
|
||||
|
||||
@Override
|
||||
public List<ReviewFeedItem> getReviewFeed(int limit) {
|
||||
return reviewMapper.selectReviewFeed(limit);
|
||||
List<StudyReportsEntity> reports = studyReportsMapper.selectList(
|
||||
Wrappers.<StudyReportsEntity>lambdaQuery()
|
||||
.orderByDesc(StudyReportsEntity::getCreatedTime)
|
||||
.last("LIMIT " + limit));
|
||||
|
||||
List<StudyReportFragmentsEntity> fragments = studyReportFragmentsMapper.selectList(
|
||||
Wrappers.<StudyReportFragmentsEntity>lambdaQuery()
|
||||
.orderByDesc(StudyReportFragmentsEntity::getCreatedTime)
|
||||
.last("LIMIT " + limit));
|
||||
|
||||
return mergeAndConvert(reports, fragments);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ReviewTaskStats> getReviewTaskStats() {
|
||||
List<TaskEntity> tasks = tasksMapper.selectList(
|
||||
Wrappers.<TaskEntity>lambdaQuery()
|
||||
.orderByDesc(TaskEntity::getCalculatedPriority));
|
||||
|
||||
List<ReviewTaskStats> stats = tasks.stream()
|
||||
.map(this::newTaskStats)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
fillReviewTaskStats(stats);
|
||||
return stats;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReviewTaskStats getReviewTaskStats(String taskNum) {
|
||||
TaskEntity task = tasksMapper.selectList(
|
||||
Wrappers.<TaskEntity>lambdaQuery()
|
||||
.eq(TaskEntity::getTaskNum, taskNum)
|
||||
.last("LIMIT 1"))
|
||||
.stream()
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
ReviewTaskStats stats = newTaskStats(taskNum, task == null ? null : task.getTaskName());
|
||||
fillReviewTaskStats(Collections.singletonList(stats));
|
||||
return stats;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ReviewFeedItem> getTaskReview(String taskNum) {
|
||||
return reviewMapper.selectTaskReview(taskNum);
|
||||
List<String> sessionNums = studySessionsMapper.selectList(
|
||||
Wrappers.<StudySessionsEntity>lambdaQuery()
|
||||
.eq(StudySessionsEntity::getTaskNum, taskNum)
|
||||
.select(StudySessionsEntity::getSessionNum))
|
||||
.stream()
|
||||
.map(StudySessionsEntity::getSessionNum)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (sessionNums.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<StudyReportsEntity> reports = studyReportsMapper.selectList(
|
||||
Wrappers.<StudyReportsEntity>lambdaQuery()
|
||||
.in(StudyReportsEntity::getSessionNum, sessionNums)
|
||||
.orderByDesc(StudyReportsEntity::getCreatedTime));
|
||||
|
||||
List<StudyReportFragmentsEntity> fragments = studyReportFragmentsMapper.selectList(
|
||||
Wrappers.<StudyReportFragmentsEntity>lambdaQuery()
|
||||
.in(StudyReportFragmentsEntity::getSessionNum, sessionNums)
|
||||
.orderByDesc(StudyReportFragmentsEntity::getCreatedTime));
|
||||
|
||||
return mergeAndConvert(reports, fragments);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -46,4 +118,168 @@ public class ReviewServiceImpl implements ReviewService {
|
||||
return Optional.ofNullable(studyReportFragmentsMapper.selectById(id))
|
||||
.orElseThrow(() -> new NotFindEntitiesException("学习残片[" + id + "]不存在"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 将报告和残片合并转换为 ReviewFeedItem 列表,按创建时间倒序排列
|
||||
*/
|
||||
private List<ReviewFeedItem> mergeAndConvert(List<StudyReportsEntity> reports,
|
||||
List<StudyReportFragmentsEntity> fragments) {
|
||||
Set<String> allSessionNums = Stream.concat(
|
||||
reports.stream().map(StudyReportsEntity::getSessionNum),
|
||||
fragments.stream().map(StudyReportFragmentsEntity::getSessionNum)
|
||||
).collect(Collectors.toSet());
|
||||
|
||||
if (allSessionNums.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Map<String, String> sessionToTaskMap = studySessionsMapper.selectList(
|
||||
Wrappers.<StudySessionsEntity>lambdaQuery()
|
||||
.in(StudySessionsEntity::getSessionNum, allSessionNums)
|
||||
.select(StudySessionsEntity::getSessionNum, StudySessionsEntity::getTaskNum))
|
||||
.stream()
|
||||
.collect(Collectors.toMap(
|
||||
StudySessionsEntity::getSessionNum,
|
||||
StudySessionsEntity::getTaskNum,
|
||||
(a, b) -> a));
|
||||
|
||||
Set<String> taskNums = new HashSet<>(sessionToTaskMap.values());
|
||||
Map<String, String> taskNameMap = tasksMapper.selectList(
|
||||
Wrappers.<TaskEntity>lambdaQuery()
|
||||
.in(TaskEntity::getTaskNum, taskNums)
|
||||
.select(TaskEntity::getTaskNum, TaskEntity::getTaskName))
|
||||
.stream()
|
||||
.collect(Collectors.toMap(
|
||||
TaskEntity::getTaskNum,
|
||||
TaskEntity::getTaskName,
|
||||
(a, b) -> a));
|
||||
|
||||
List<ReviewFeedItem> reportItems = reports.stream().map(r -> {
|
||||
ReviewFeedItem item = new ReviewFeedItem();
|
||||
item.setId(r.getId());
|
||||
item.setSessionNum(r.getSessionNum());
|
||||
item.setSourceType("REPORT");
|
||||
item.setContent(r.getContent());
|
||||
item.setCreatedTime(r.getCreatedTime());
|
||||
String taskNum = sessionToTaskMap.get(r.getSessionNum());
|
||||
item.setTaskNum(taskNum);
|
||||
item.setTaskName(taskNum != null ? taskNameMap.get(taskNum) : null);
|
||||
return item;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
List<ReviewFeedItem> fragmentItems = fragments.stream().map(f -> {
|
||||
ReviewFeedItem item = new ReviewFeedItem();
|
||||
item.setId(f.getId());
|
||||
item.setSessionNum(f.getSessionNum());
|
||||
item.setSourceType("FRAGMENT");
|
||||
item.setContent(f.getContent());
|
||||
item.setCreatedTime(f.getCreatedTime());
|
||||
String taskNum = sessionToTaskMap.get(f.getSessionNum());
|
||||
item.setTaskNum(taskNum);
|
||||
item.setTaskName(taskNum != null ? taskNameMap.get(taskNum) : null);
|
||||
return item;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
return Stream.concat(reportItems.stream(), fragmentItems.stream())
|
||||
.sorted(Comparator.comparing(ReviewFeedItem::getCreatedTime,
|
||||
Comparator.nullsLast(Comparator.reverseOrder())))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private ReviewTaskStats newTaskStats(TaskEntity task) {
|
||||
return newTaskStats(task.getTaskNum(), task.getTaskName());
|
||||
}
|
||||
|
||||
private ReviewTaskStats newTaskStats(String taskNum, String taskName) {
|
||||
ReviewTaskStats stats = new ReviewTaskStats();
|
||||
stats.setTaskNum(taskNum);
|
||||
stats.setTaskName(taskName);
|
||||
return stats;
|
||||
}
|
||||
|
||||
private void fillReviewTaskStats(List<ReviewTaskStats> statsList) {
|
||||
Map<String, ReviewTaskStats> statsByTaskNum = statsList.stream()
|
||||
.filter(item -> StringUtils.hasText(item.getTaskNum()))
|
||||
.collect(Collectors.toMap(
|
||||
ReviewTaskStats::getTaskNum,
|
||||
item -> item,
|
||||
(a, b) -> a,
|
||||
LinkedHashMap::new));
|
||||
|
||||
if (statsByTaskNum.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<StudySessionsEntity> sessions = studySessionsMapper.selectList(
|
||||
Wrappers.<StudySessionsEntity>lambdaQuery()
|
||||
.in(StudySessionsEntity::getTaskNum, statsByTaskNum.keySet()));
|
||||
|
||||
LocalDateTime todayStart = LocalDate.now().atStartOfDay();
|
||||
LocalDateTime weekStart = LocalDate.now().with(DayOfWeek.MONDAY).atStartOfDay();
|
||||
Map<String, String> sessionToTaskNum = new HashMap<>();
|
||||
Map<String, Double> effectivenessRatioSum = new HashMap<>();
|
||||
|
||||
for (StudySessionsEntity session : sessions) {
|
||||
ReviewTaskStats stats = statsByTaskNum.get(session.getTaskNum());
|
||||
if (stats == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(session.getSessionNum())) {
|
||||
sessionToTaskNum.put(session.getSessionNum(), session.getTaskNum());
|
||||
}
|
||||
|
||||
double effectiveTime = session.getEffectiveTime();
|
||||
stats.setSessionCount(stats.getSessionCount() + 1);
|
||||
stats.setEffectiveTime(stats.getEffectiveTime() + effectiveTime);
|
||||
effectivenessRatioSum.merge(session.getTaskNum(), session.getEffectivenessRatio(), Double::sum);
|
||||
|
||||
LocalDateTime startTime = session.getStartTime();
|
||||
if (startTime != null && !startTime.isBefore(todayStart)) {
|
||||
stats.setTodayEffectiveTime(stats.getTodayEffectiveTime() + effectiveTime);
|
||||
}
|
||||
if (startTime != null && !startTime.isBefore(weekStart)) {
|
||||
stats.setWeekEffectiveTime(stats.getWeekEffectiveTime() + effectiveTime);
|
||||
}
|
||||
}
|
||||
|
||||
statsByTaskNum.forEach((taskNum, stats) -> {
|
||||
if (stats.getSessionCount() > 0) {
|
||||
stats.setAvgEffectiveTime(stats.getEffectiveTime() / stats.getSessionCount());
|
||||
stats.setAvgEffectivenessRatio(effectivenessRatioSum.getOrDefault(taskNum, 0D) / stats.getSessionCount());
|
||||
}
|
||||
});
|
||||
|
||||
fillReportAndFragmentCount(sessionToTaskNum, statsByTaskNum);
|
||||
}
|
||||
|
||||
private void fillReportAndFragmentCount(Map<String, String> sessionToTaskNum,
|
||||
Map<String, ReviewTaskStats> statsByTaskNum) {
|
||||
if (sessionToTaskNum.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Set<String> sessionNums = sessionToTaskNum.keySet();
|
||||
List<StudyReportsEntity> reports = studyReportsMapper.selectList(
|
||||
Wrappers.<StudyReportsEntity>lambdaQuery()
|
||||
.in(StudyReportsEntity::getSessionNum, sessionNums)
|
||||
.select(StudyReportsEntity::getSessionNum));
|
||||
for (StudyReportsEntity report : reports) {
|
||||
ReviewTaskStats stats = statsByTaskNum.get(sessionToTaskNum.get(report.getSessionNum()));
|
||||
if (stats != null) {
|
||||
stats.setReportCount(stats.getReportCount() + 1);
|
||||
}
|
||||
}
|
||||
|
||||
List<StudyReportFragmentsEntity> fragments = studyReportFragmentsMapper.selectList(
|
||||
Wrappers.<StudyReportFragmentsEntity>lambdaQuery()
|
||||
.in(StudyReportFragmentsEntity::getSessionNum, sessionNums)
|
||||
.select(StudyReportFragmentsEntity::getSessionNum));
|
||||
for (StudyReportFragmentsEntity fragment : fragments) {
|
||||
ReviewTaskStats stats = statsByTaskNum.get(sessionToTaskNum.get(fragment.getSessionNum()));
|
||||
if (stats != null) {
|
||||
stats.setFragmentCount(stats.getFragmentCount() + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
package com.guo.learningprogresstracker.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.guo.learningprogresstracker.entity.StudyExpectationsEntity;
|
||||
import com.guo.learningprogresstracker.service.StudyExpectationsService;
|
||||
import com.guo.learningprogresstracker.mapper.StudyExpectationsMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* @author guo
|
||||
* @description 针对表【study_expectations(存储每次学习开始前的预期)】的数据库操作Service实现
|
||||
* @createDate 2024-06-09 15:28:48
|
||||
*/
|
||||
@Service
|
||||
public class StudyExpectationsServiceImpl extends ServiceImpl<StudyExpectationsMapper, StudyExpectationsEntity>
|
||||
implements StudyExpectationsService{
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
+31
-14
@@ -3,6 +3,7 @@ package com.guo.learningprogresstracker.service.impl;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.guo.learningprogresstracker.dto.request.CreateFragmentsRequest;
|
||||
import com.guo.learningprogresstracker.dto.request.UpdateFragmentsRequest;
|
||||
import com.guo.learningprogresstracker.entity.StudyReportFragmentsEntity;
|
||||
import com.guo.learningprogresstracker.entity.StudySessionsEntity;
|
||||
import com.guo.learningprogresstracker.exception.NotFindEntitiesException;
|
||||
@@ -13,30 +14,46 @@ import com.guo.learningprogresstracker.mapper.StudyReportFragmentsMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author guo
|
||||
* @description 针对表【study_report_fragments(记录学习过程中的学习内容报告残片)】的数据库操作Service实现
|
||||
* @createDate 2024-06-09 15:28:48
|
||||
*/
|
||||
* 针对表【study_report_fragments(记录学习过程中的学习内容报告残片)】的数据库操作Service实现
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class StudyReportFragmentsServiceImpl extends ServiceImpl<StudyReportFragmentsMapper, StudyReportFragmentsEntity>
|
||||
implements StudyReportFragmentsService{
|
||||
implements StudyReportFragmentsService {
|
||||
|
||||
private final StudySessionsMapper studySessionsMapper;
|
||||
|
||||
@Override
|
||||
public void createFragments(CreateFragmentsRequest request) throws NotFindEntitiesException {
|
||||
StudyReportFragmentsEntity entity = FragmentsConvert.MAPPER.toFragmentsEntity(request);
|
||||
if (studySessionsMapper.exists(Wrappers.lambdaQuery(StudySessionsEntity.class)
|
||||
.eq(StudySessionsEntity::getSessionNum,entity.getSessionNum()))) {
|
||||
this.save(entity);
|
||||
}else {
|
||||
throw new NotFindEntitiesException(String.format("未能找到学习会话sessionNum[%s]",entity.getSessionNum()));
|
||||
StudySessionsEntity session = studySessionsMapper.selectOne(
|
||||
Wrappers.lambdaQuery(StudySessionsEntity.class)
|
||||
.eq(StudySessionsEntity::getSessionNum, entity.getSessionNum()));
|
||||
if (session == null) {
|
||||
throw new NotFindEntitiesException(String.format("未能找到学习会话sessionNum[%s]", entity.getSessionNum()));
|
||||
}
|
||||
this.save(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
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));
|
||||
}
|
||||
StudyReportFragmentsEntity entity = FragmentsConvert.MAPPER.toFragmentsEntity(request);
|
||||
entity.setId(id);
|
||||
this.updateById(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<StudyReportFragmentsEntity> getFragmentsBySession(String sessionNum) {
|
||||
return this.list(
|
||||
Wrappers.lambdaQuery(StudyReportFragmentsEntity.class)
|
||||
.eq(StudyReportFragmentsEntity::getSessionNum, sessionNum)
|
||||
.orderByAsc(StudyReportFragmentsEntity::getCreatedTime));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+22
-10
@@ -27,9 +27,7 @@ import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author guo
|
||||
* @description 针对表【study_sessions(记录每次学习会话的具体数据)】的数据库操作Service实现
|
||||
* @createDate 2024-06-09 15:28:48
|
||||
* 针对表【study_sessions(记录每次学习会话的具体数据)】的数据库操作Service实现
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@@ -38,6 +36,7 @@ public class StudySessionsServiceImpl extends ServiceImpl<StudySessionsMapper, S
|
||||
implements StudySessionsService {
|
||||
|
||||
public static final int WORK_DURATION = 25;
|
||||
|
||||
private final TasksServiceImpl tasksServiceImpl;
|
||||
private final StudyReportFragmentsServiceImpl studyReportFragmentsServiceImpl;
|
||||
private final StudyReportFragmentsMapper studyReportFragmentsMapper;
|
||||
@@ -94,21 +93,28 @@ public class StudySessionsServiceImpl extends ServiceImpl<StudySessionsMapper, S
|
||||
|
||||
|
||||
@Override
|
||||
public void endedStudySession(String sessionNum, String content) throws ErrorParameterException {
|
||||
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 + "]不存在"));
|
||||
boolean wasOngoing = StudySessionStateEnum.ONGOING.name().equals(studySessionsEntity.getSessionState());
|
||||
studySessionsEntity.endedStudySession();
|
||||
this.updateById(studySessionsEntity);
|
||||
// 创建学习报告
|
||||
StudyReportsEntity studyReportsEntity = new StudyReportsEntity();
|
||||
studyReportsEntity.setSessionNum(sessionNum);
|
||||
studyReportsEntity.setContent(content);
|
||||
studyReportsMapper.insert(studyReportsEntity);
|
||||
if (wasOngoing && studySessionsEntity.getEffectiveTime() == 0) {
|
||||
return "本次有效学习时间不足10分钟,不计入总学习时间";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StudySessionResponse getNotEndedStudySessionByTaskNum(String taskNum) throws ErrorParameterException {
|
||||
TaskEntity task = tasksServiceImpl.getOneOpt(Wrappers.lambdaQuery(TaskEntity.class)
|
||||
.eq(TaskEntity::getTaskNum, taskNum))
|
||||
.orElseThrow(() -> new ErrorParameterException("任务[" + taskNum + "]不存在"));
|
||||
StudySessionsDto dto = Optional.ofNullable(studyReportsMapper.getNotEndedStudySessionDtoByTaskNum(taskNum))
|
||||
.orElseThrow(() -> new ErrorParameterException("任务[" + taskNum + "]不存在进行中或暂停中的会话"));
|
||||
return StudySessionConvert.MAPPER.toStudySessionResponse(dto);
|
||||
@@ -118,7 +124,6 @@ public class StudySessionsServiceImpl extends ServiceImpl<StudySessionsMapper, S
|
||||
public ArrayList<String> getAllFragments(String sessionNum) throws ErrorParameterException {
|
||||
if (this.exists(Wrappers.lambdaQuery(StudySessionsEntity.class)
|
||||
.eq(StudySessionsEntity::getSessionNum, sessionNum))) {
|
||||
// todo guo 后续可在此补充更高级的整理方式
|
||||
return studyReportFragmentsMapper.selectList(Wrappers.lambdaQuery(StudyReportFragmentsEntity.class)
|
||||
.eq(StudyReportFragmentsEntity::getSessionNum, sessionNum))
|
||||
.stream().map(StudyReportFragmentsEntity::getContent).collect(Collectors.toCollection(ArrayList::new));
|
||||
@@ -142,8 +147,15 @@ public class StudySessionsServiceImpl extends ServiceImpl<StudySessionsMapper, S
|
||||
studySessionsEntity.continueStudySession();
|
||||
this.updateById(studySessionsEntity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 sessionNum 获取学习会话
|
||||
*/
|
||||
public StudySessionResponse getStudySessionBySessionNum(String sessionNum) throws ErrorParameterException {
|
||||
StudySessionsEntity session = this.getOneOpt(Wrappers.lambdaQuery(StudySessionsEntity.class)
|
||||
.eq(StudySessionsEntity::getSessionNum, sessionNum))
|
||||
.orElseThrow(() -> new ErrorParameterException("会话[" + sessionNum + "]不存在"));
|
||||
return StudySessionConvert.MAPPER.toStudySessionResponse(session);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -19,34 +19,29 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.rmi.ServerException;
|
||||
|
||||
/**
|
||||
* @author guo
|
||||
* @description 针对表【tasks(存储学习任务的基本信息,包括优先级的多维度计算)】的数据库操作Service实现
|
||||
* @createDate 2024-06-09 15:28:48
|
||||
*/
|
||||
* 针对表【tasks(存储学习任务的基本信息,包括优先级的多维度计算)】的数据库操作Service实现
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class TasksServiceImpl extends ServiceImpl<TasksMapper, TaskEntity>
|
||||
implements TasksService{
|
||||
|
||||
implements TasksService {
|
||||
|
||||
private final TasksMapper tasksMapper;
|
||||
|
||||
@Override
|
||||
public Page<TaskInfo> taskList(Integer pageNum, Integer pageSize) {
|
||||
Page<TaskEntity> page = tasksMapper.selectPage(new Page<TaskEntity>(pageNum, pageSize),
|
||||
Wrappers.lambdaQuery(TaskEntity.class).orderByDesc(TaskEntity::getCalculatedPriority));
|
||||
Wrappers.lambdaQuery(TaskEntity.class)
|
||||
.orderByDesc(TaskEntity::getCalculatedPriority));
|
||||
Page<TaskInfo> response = TaskConvert.MAPPER.toTaskInfoPage(page);
|
||||
// todo 需要补充TaskInfo中的lastLearningStatus字段信息
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String addTask(TaskRequest taskRequest) throws ErrorParameterException {
|
||||
TaskEntity task= TaskConvert.MAPPER.taskRequestToTaskEntity(taskRequest);
|
||||
TaskEntity task = TaskConvert.MAPPER.taskRequestToTaskEntity(taskRequest);
|
||||
|
||||
if (this.exists(Wrappers.lambdaQuery(TaskEntity.class)
|
||||
.eq(TaskEntity::getTaskName, task.getTaskName()))) {
|
||||
@@ -63,13 +58,15 @@ public class TasksServiceImpl extends ServiceImpl<TasksMapper, TaskEntity>
|
||||
@Override
|
||||
public TaskInfoResponse getTask(String taskId) {
|
||||
TaskEntity taskEntity = this.getById(taskId);
|
||||
TaskInfoResponse taskInfoResponse = TaskConvert.MAPPER.taskEntityToTaskInfoResponse(taskEntity);
|
||||
|
||||
return taskInfoResponse;
|
||||
return TaskConvert.MAPPER.taskEntityToTaskInfoResponse(taskEntity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateTask(String taskId, TaskRequest updatedTask) {
|
||||
TaskEntity existingTask = this.getById(taskId);
|
||||
if (existingTask == null) {
|
||||
throw new IllegalArgumentException("任务不存在: " + taskId);
|
||||
}
|
||||
TaskEntity taskEntity = TaskConvert.MAPPER.taskRequestToTaskEntity(updatedTask);
|
||||
int id;
|
||||
try {
|
||||
@@ -78,21 +75,15 @@ public class TasksServiceImpl extends ServiceImpl<TasksMapper, TaskEntity>
|
||||
throw new IllegalArgumentException("任务ID格式错误: " + taskId);
|
||||
}
|
||||
taskEntity.setId(id);
|
||||
boolean updated = this.updateById(taskEntity);
|
||||
if (!updated) {
|
||||
throw new IllegalArgumentException("任务不存在或更新失败: " + taskId);
|
||||
}
|
||||
|
||||
this.updateById(taskEntity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteTask(String taskId) throws ServerException {
|
||||
boolean b = this.removeById(taskId);
|
||||
if(!b){
|
||||
throw new ServerException("未能正常删除任务");
|
||||
public void deleteTask(String taskId) {
|
||||
TaskEntity existingTask = this.getById(taskId);
|
||||
if (existingTask == null) {
|
||||
throw new IllegalArgumentException("任务不存在: " + taskId);
|
||||
}
|
||||
this.removeById(taskId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.guo.learningprogresstracker.exception.AppException;
|
||||
import com.guo.learningprogresstracker.service.UserService;
|
||||
import com.guo.learningprogresstracker.mapper.UserMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.mindrot.jbcrypt.BCrypt;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
@@ -23,12 +24,13 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, UserEntity>
|
||||
public String authenticate(String username, String password) throws AppException {
|
||||
|
||||
UserEntity userEntity = this.getOneOpt(Wrappers.<UserEntity>lambdaQuery()
|
||||
.eq(UserEntity::getUserName, username)
|
||||
.eq(UserEntity::getUserPassword, password)).orElseThrow(() -> new AppException("账号或密码错误!"));
|
||||
return userEntity.getId();
|
||||
.eq(UserEntity::getUserName, username))
|
||||
.orElseThrow(() -> new AppException("账号或密码错误!"));
|
||||
|
||||
if (!BCrypt.checkpw(password, userEntity.getUserPassword())) {
|
||||
throw new AppException("账号或密码错误!");
|
||||
}
|
||||
|
||||
return userEntity.getId();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
package com.guo.learningprogresstracker.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.guo.learningprogresstracker.entity.UserTaskEntity;
|
||||
import com.guo.learningprogresstracker.service.UserTaskService;
|
||||
import com.guo.learningprogresstracker.mapper.UserTaskMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* @author guo
|
||||
* @description 针对表【user_task(存储用户-任务之间的对应关系)】的数据库操作Service实现
|
||||
* @createDate 2024-06-09 15:28:48
|
||||
*/
|
||||
@Service
|
||||
public class UserTaskServiceImpl extends ServiceImpl<UserTaskMapper, UserTaskEntity>
|
||||
implements UserTaskService{
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -4,8 +4,6 @@ import com.guo.learningprogresstracker.dto.PriorityDto;
|
||||
|
||||
/**
|
||||
* 加权优先级计算工具
|
||||
*
|
||||
* @author guo
|
||||
*/
|
||||
public class CalculatedPriorityTool {
|
||||
|
||||
|
||||
@@ -7,16 +7,11 @@ import java.util.Date;
|
||||
|
||||
/**
|
||||
* 用于生成编码的工具
|
||||
*
|
||||
* @author guo
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public class GenerateNumTool {
|
||||
private static int getNextSequence() {
|
||||
// 获取当前时间的毫秒级时间戳
|
||||
long timestamp = System.currentTimeMillis();
|
||||
|
||||
// 使用时间戳的最后6位作为序列号
|
||||
return (int) (timestamp % 1_000_000);
|
||||
}
|
||||
|
||||
@@ -29,22 +24,14 @@ public class GenerateNumTool {
|
||||
*/
|
||||
public static String generateNum(String prefix, String separator) {
|
||||
String currentDate = new SimpleDateFormat("yyyyMMdd").format(new Date());
|
||||
|
||||
// 唯一的后缀
|
||||
int sequence = getNextSequence();
|
||||
|
||||
// 返回完整的 taskNum
|
||||
return prefix + separator + currentDate + separator + sequence;
|
||||
}
|
||||
|
||||
public static String generateNum(String prefix) {
|
||||
String separator = "-";
|
||||
String currentDate = new SimpleDateFormat("yyyyMMdd").format(new Date());
|
||||
|
||||
// 唯一的后缀
|
||||
int sequence = getNextSequence();
|
||||
|
||||
// 返回完整的 taskNum
|
||||
return prefix + separator + currentDate + separator + sequence;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- 删除不再需要的 user_task 表
|
||||
DROP TABLE IF EXISTS user_task;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE `user` MODIFY COLUMN `user_password` varchar(60) NOT NULL COMMENT '账号密码(BCrypt哈希)';
|
||||
@@ -1,55 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis-org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.guo.learningprogresstracker.mapper.ReviewMapper">
|
||||
|
||||
<select id="selectReviewFeed" resultType="com.guo.learningprogresstracker.dto.ReviewFeedItem">
|
||||
SELECT
|
||||
combined.id,
|
||||
combined.session_num AS sessionNum,
|
||||
combined.source_type AS sourceType,
|
||||
combined.content,
|
||||
combined.created_time AS createdTime,
|
||||
t.task_num AS taskNum,
|
||||
t.task_name AS taskName
|
||||
FROM (
|
||||
SELECT r.id, r.session_num, r.content, r.created_time, 'REPORT' AS source_type
|
||||
FROM study_reports r
|
||||
WHERE r.deleted = 0
|
||||
UNION ALL
|
||||
SELECT f.id, f.session_num, f.content, f.created_time, 'FRAGMENT' AS source_type
|
||||
FROM study_report_fragments f
|
||||
WHERE f.deleted = 0
|
||||
) combined
|
||||
JOIN study_sessions ss ON combined.session_num = ss.session_num AND ss.deleted = 0
|
||||
JOIN tasks t ON ss.task_num = t.task_num AND t.deleted = 0
|
||||
ORDER BY combined.created_time DESC
|
||||
LIMIT #{limit}
|
||||
</select>
|
||||
|
||||
<select id="selectTaskReview" resultType="com.guo.learningprogresstracker.dto.ReviewFeedItem">
|
||||
SELECT
|
||||
combined.id,
|
||||
combined.session_num AS sessionNum,
|
||||
combined.source_type AS sourceType,
|
||||
combined.content,
|
||||
combined.created_time AS createdTime,
|
||||
t.task_num AS taskNum,
|
||||
t.task_name AS taskName
|
||||
FROM (
|
||||
SELECT r.id, r.session_num, r.content, r.created_time, 'REPORT' AS source_type
|
||||
FROM study_reports r
|
||||
WHERE r.deleted = 0
|
||||
UNION ALL
|
||||
SELECT f.id, f.session_num, f.content, f.created_time, 'FRAGMENT' AS source_type
|
||||
FROM study_report_fragments f
|
||||
WHERE f.deleted = 0
|
||||
) combined
|
||||
JOIN study_sessions ss ON combined.session_num = ss.session_num AND ss.deleted = 0
|
||||
JOIN tasks t ON ss.task_num = t.task_num AND t.deleted = 0
|
||||
WHERE t.task_num = #{taskNum}
|
||||
ORDER BY combined.created_time DESC
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.guo.learningprogresstracker.mapper.StudyExpectationsMapper">
|
||||
|
||||
<resultMap id="BaseResultMap" type="com.guo.learningprogresstracker.entity.StudyExpectationsEntity">
|
||||
<id property="expectationId" column="expectation_id" jdbcType="INTEGER"/>
|
||||
<result property="sessionId" column="session_id" jdbcType="INTEGER"/>
|
||||
<result property="description" column="description" jdbcType="VARCHAR"/>
|
||||
<result property="createdTime" column="created_time" jdbcType="TIMESTAMP"/>
|
||||
<result property="createdBy" column="created_by" jdbcType="VARCHAR"/>
|
||||
<result property="lastModifiedTime" column="last_modified_time" jdbcType="TIMESTAMP"/>
|
||||
<result property="lastModifiedBy" column="last_modified_by" jdbcType="VARCHAR"/>
|
||||
</resultMap>
|
||||
|
||||
<sql id="Base_Column_List">
|
||||
expectation_id,session_id,description,
|
||||
created_time,created_by,last_modified_time,
|
||||
last_modified_by
|
||||
</sql>
|
||||
</mapper>
|
||||
@@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.guo.learningprogresstracker.mapper.UserTaskMapper">
|
||||
|
||||
<resultMap id="BaseResultMap" type="com.guo.learningprogresstracker.entity.UserTaskEntity">
|
||||
<result property="id" column="id" jdbcType="VARCHAR"/>
|
||||
<result property="userId" column="user_id" jdbcType="INTEGER"/>
|
||||
<result property="taskId" column="task_id" jdbcType="INTEGER"/>
|
||||
<result property="createdTime" column="created_time" jdbcType="TIMESTAMP"/>
|
||||
<result property="createdBy" column="created_by" jdbcType="VARCHAR"/>
|
||||
<result property="lastModifiedTime" column="last_modified_time" jdbcType="TIMESTAMP"/>
|
||||
<result property="lastModifiedBy" column="last_modified_by" jdbcType="VARCHAR"/>
|
||||
</resultMap>
|
||||
|
||||
<sql id="Base_Column_List">
|
||||
id,user_id,task_id,
|
||||
created_time,created_by,last_modified_time,
|
||||
last_modified_by
|
||||
</sql>
|
||||
</mapper>
|
||||
@@ -0,0 +1,301 @@
|
||||
package com.guo.learningprogresstracker.entity;
|
||||
|
||||
import com.guo.learningprogresstracker.enums.StudySessionStateEnum;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class StudySessionsEntityTest {
|
||||
|
||||
private StudySessionsEntity createOngoingSession(int minutesAgo) {
|
||||
StudySessionsEntity session = new StudySessionsEntity();
|
||||
session.setSessionNum("SESSION_TEST");
|
||||
session.setStartTime(LocalDateTime.now().minusMinutes(minutesAgo));
|
||||
session.setLastStartTime(LocalDateTime.now().minusMinutes(minutesAgo));
|
||||
session.setEffectiveTime(0);
|
||||
session.setActualTime(0);
|
||||
session.setSessionState(StudySessionStateEnum.ONGOING.name());
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景1:直接结束,有效时间 < 10分钟 → effectiveTime 应被清零
|
||||
*/
|
||||
@Test
|
||||
void endedSession_effectiveTimeLessThan10Min_shouldZeroOut() {
|
||||
StudySessionsEntity session = createOngoingSession(5);
|
||||
session.endedStudySession();
|
||||
|
||||
assertEquals(0, session.getEffectiveTime(), "有效时间不足10分钟应被清零");
|
||||
assertEquals(0, session.getEffectivenessRatio(), "有效时间比应被清零");
|
||||
assertEquals(StudySessionStateEnum.ENDED.name(), session.getSessionState());
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景2:直接结束,有效时间 = 10分钟(边界值)→ 保留(600 不 < 600)
|
||||
*/
|
||||
@Test
|
||||
void endedSession_effectiveTimeExactly10Min_shouldKeep() {
|
||||
StudySessionsEntity session = createOngoingSession(10);
|
||||
session.endedStudySession();
|
||||
|
||||
assertTrue(session.getEffectiveTime() >= 10 * 60, "有效时间等于10分钟应保留(阈值为 < 600)");
|
||||
assertEquals(StudySessionStateEnum.ENDED.name(), session.getSessionState());
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景3:直接结束,有效时间 > 10分钟 → effectiveTime 应保留
|
||||
*/
|
||||
@Test
|
||||
void endedSession_effectiveTimeMoreThan10Min_shouldKeep() {
|
||||
StudySessionsEntity session = createOngoingSession(15);
|
||||
session.endedStudySession();
|
||||
|
||||
assertTrue(session.getEffectiveTime() >= 15 * 60, "有效时间超过10分钟应保留");
|
||||
assertEquals(StudySessionStateEnum.ENDED.name(), session.getSessionState());
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景4:暂停后直接结束(未继续),暂停段 < 10分钟 → 清零
|
||||
*/
|
||||
@Test
|
||||
void endedSession_pausedThenEnded_lessThan10Min_shouldZeroOut() {
|
||||
StudySessionsEntity session = createOngoingSession(5);
|
||||
session.pausedStudySession(LocalDateTime.now());
|
||||
assertEquals(StudySessionStateEnum.PAUSED.name(), session.getSessionState());
|
||||
assertTrue(session.getEffectiveTime() > 0, "暂停后应有有效时间");
|
||||
|
||||
session.endedStudySession();
|
||||
|
||||
assertEquals(0, session.getEffectiveTime(), "暂停后结束,有效时间不足10分钟应清零");
|
||||
assertEquals(StudySessionStateEnum.ENDED.name(), session.getSessionState());
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景5:暂停后继续,再结束,总有效时间 < 10分钟 → 清零
|
||||
*/
|
||||
@Test
|
||||
void endedSession_pausedContinuedThenEnded_lessThan10Min_shouldZeroOut() throws Exception {
|
||||
StudySessionsEntity session = createOngoingSession(8);
|
||||
// 暂停(累加3分钟:从8分钟前到5分钟前)
|
||||
session.pausedStudySession(LocalDateTime.now().minusMinutes(5));
|
||||
|
||||
// 继续学习,模拟又学习了2分钟
|
||||
session.continueStudySession();
|
||||
session.setLastStartTime(LocalDateTime.now().minusMinutes(2));
|
||||
|
||||
session.endedStudySession();
|
||||
|
||||
// 总有效时间 ≈ 3+2 = 5分钟 < 10分钟 → 清零
|
||||
assertEquals(0, session.getEffectiveTime(), "总有效时间不足10分钟应清零");
|
||||
assertEquals(StudySessionStateEnum.ENDED.name(), session.getSessionState());
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景6:暂停后继续,再结束,总有效时间 > 10分钟 → 保留
|
||||
*/
|
||||
@Test
|
||||
void endedSession_pausedContinuedThenEnded_moreThan10Min_shouldKeep() throws Exception {
|
||||
StudySessionsEntity session = createOngoingSession(20);
|
||||
// 暂停(累加8分钟:从20分钟前到12分钟前)
|
||||
session.pausedStudySession(LocalDateTime.now().minusMinutes(12));
|
||||
|
||||
// 继续学习,模拟又学习了12分钟
|
||||
session.continueStudySession();
|
||||
session.setLastStartTime(LocalDateTime.now().minusMinutes(12));
|
||||
|
||||
session.endedStudySession();
|
||||
|
||||
// 总有效时间 ≈ 8+12 = 20分钟 > 10分钟 → 保留
|
||||
assertTrue(session.getEffectiveTime() > 0, "总有效时间超过10分钟应保留");
|
||||
assertEquals(StudySessionStateEnum.ENDED.name(), session.getSessionState());
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景7:多次暂停-继续,总有效时间 < 10分钟 → 清零
|
||||
*/
|
||||
@Test
|
||||
void endedSession_multiplePauseContinue_lessThan10Min_shouldZeroOut() throws Exception {
|
||||
StudySessionsEntity session = createOngoingSession(15);
|
||||
// 第一段:3分钟(15分钟前 → 12分钟前)
|
||||
session.pausedStudySession(LocalDateTime.now().minusMinutes(12));
|
||||
// 继续,模拟第二段学习3分钟(12分钟前 → 9分钟前)
|
||||
session.continueStudySession();
|
||||
session.setLastStartTime(LocalDateTime.now().minusMinutes(9));
|
||||
|
||||
// 第二段暂停,累加3分钟
|
||||
session.pausedStudySession(LocalDateTime.now().minusMinutes(6));
|
||||
// 继续,模拟第三段学习3分钟(6分钟前 → 3分钟前)
|
||||
session.continueStudySession();
|
||||
session.setLastStartTime(LocalDateTime.now().minusMinutes(3));
|
||||
|
||||
// 总计 3+3+3 = 9分钟 < 10分钟
|
||||
session.endedStudySession();
|
||||
|
||||
assertEquals(0, session.getEffectiveTime(), "多次暂停继续,总有效时间不足10分钟应清零");
|
||||
assertEquals(StudySessionStateEnum.ENDED.name(), session.getSessionState());
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景8:多次暂停-继续,总有效时间 > 10分钟 → 保留
|
||||
*/
|
||||
@Test
|
||||
void endedSession_multiplePauseContinue_moreThan10Min_shouldKeep() throws Exception {
|
||||
StudySessionsEntity session = createOngoingSession(25);
|
||||
// 第一段:5分钟(25分钟前 → 20分钟前)
|
||||
session.pausedStudySession(LocalDateTime.now().minusMinutes(20));
|
||||
// 继续,模拟第二段学习5分钟
|
||||
session.continueStudySession();
|
||||
session.setLastStartTime(LocalDateTime.now().minusMinutes(15));
|
||||
|
||||
// 第二段暂停,累加5分钟
|
||||
session.pausedStudySession(LocalDateTime.now().minusMinutes(10));
|
||||
// 继续,模拟第三段学习5分钟
|
||||
session.continueStudySession();
|
||||
session.setLastStartTime(LocalDateTime.now().minusMinutes(5));
|
||||
|
||||
// 总计 5+5+5 = 15分钟 > 10分钟
|
||||
session.endedStudySession();
|
||||
|
||||
assertTrue(session.getEffectiveTime() > 0, "多次暂停继续,总有效时间超过10分钟应保留");
|
||||
assertEquals(StudySessionStateEnum.ENDED.name(), session.getSessionState());
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景9:已结束的会话再次结束 → 不应重复处理
|
||||
*/
|
||||
@Test
|
||||
void endedSession_alreadyEnded_shouldNotChange() {
|
||||
StudySessionsEntity session = createOngoingSession(5);
|
||||
session.endedStudySession();
|
||||
double effectiveTimeAfterFirstEnd = session.getEffectiveTime();
|
||||
|
||||
session.endedStudySession();
|
||||
|
||||
assertEquals(effectiveTimeAfterFirstEnd, session.getEffectiveTime(), "已结束的会话不应重复处理");
|
||||
assertEquals(StudySessionStateEnum.ENDED.name(), session.getSessionState());
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景10:有效时间非常短(1分钟) → 清零
|
||||
*/
|
||||
@Test
|
||||
void endedSession_veryShortSession_shouldZeroOut() {
|
||||
StudySessionsEntity session = createOngoingSession(1);
|
||||
session.endedStudySession();
|
||||
|
||||
assertEquals(0, session.getEffectiveTime(), "极短会话的有效时间应被清零");
|
||||
assertEquals(0, session.getEffectivenessRatio());
|
||||
assertEquals(StudySessionStateEnum.ENDED.name(), session.getSessionState());
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景11:有效时间恰好超过阈值(11分钟) → 保留
|
||||
*/
|
||||
@Test
|
||||
void endedSession_justOverThreshold_shouldKeep() {
|
||||
StudySessionsEntity session = createOngoingSession(11);
|
||||
session.endedStudySession();
|
||||
|
||||
assertTrue(session.getEffectiveTime() > 0, "超过阈值的有效时间应保留");
|
||||
assertEquals(StudySessionStateEnum.ENDED.name(), session.getSessionState());
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景12:暂停期间不应计入有效时间,只计实际学习的段
|
||||
*/
|
||||
@Test
|
||||
void endedSession_pauseTimeNotCounted_shouldCalcCorrectly() throws Exception {
|
||||
StudySessionsEntity session = createOngoingSession(30);
|
||||
// 学习5分钟(30分钟前 → 25分钟前)
|
||||
session.pausedStudySession(LocalDateTime.now().minusMinutes(25));
|
||||
double firstSegment = session.getEffectiveTime();
|
||||
assertTrue(firstSegment >= 5 * 60 - 1 && firstSegment <= 5 * 60 + 1,
|
||||
"第一段有效时间应约5分钟,实际: " + firstSegment);
|
||||
|
||||
// 暂停20分钟(不应计入有效时间)
|
||||
|
||||
// 继续学习4分钟
|
||||
session.continueStudySession();
|
||||
session.setLastStartTime(LocalDateTime.now().minusMinutes(4));
|
||||
|
||||
session.endedStudySession();
|
||||
|
||||
// 有效时间应约为 5+4=9分钟 < 10分钟 → 清零
|
||||
assertEquals(0, session.getEffectiveTime(), "暂停期间不应计入有效时间,9分钟 < 10分钟阈值应清零");
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景13:暂停后继续,有效时间恰好在阈值边界 → 验证边界精度
|
||||
*/
|
||||
@Test
|
||||
void endedSession_borderlineWithPause_shouldHandleCorrectly() throws Exception {
|
||||
StudySessionsEntity session = createOngoingSession(20);
|
||||
// 第一段:6分钟(20分钟前 → 14分钟前)
|
||||
session.pausedStudySession(LocalDateTime.now().minusMinutes(14));
|
||||
|
||||
// 继续,模拟第二段学习5分钟
|
||||
session.continueStudySession();
|
||||
session.setLastStartTime(LocalDateTime.now().minusMinutes(5));
|
||||
|
||||
// 总计 6+5 = 11分钟 > 10分钟 → 保留
|
||||
session.endedStudySession();
|
||||
|
||||
assertTrue(session.getEffectiveTime() > 0, "总计11分钟应超过阈值被保留");
|
||||
assertEquals(StudySessionStateEnum.ENDED.name(), session.getSessionState());
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景14:ONGOING 状态直接结束,effectiveTime 为 0(刚开始就结束) → 0 < 600 → 清零
|
||||
*/
|
||||
@Test
|
||||
void endedSession_immediatelyEnded_shouldZeroOut() {
|
||||
StudySessionsEntity session = new StudySessionsEntity();
|
||||
session.setSessionNum("SESSION_INSTANT");
|
||||
session.setStartTime(LocalDateTime.now());
|
||||
session.setLastStartTime(LocalDateTime.now());
|
||||
session.setEffectiveTime(0);
|
||||
session.setActualTime(0);
|
||||
session.setSessionState(StudySessionStateEnum.ONGOING.name());
|
||||
|
||||
session.endedStudySession();
|
||||
|
||||
assertEquals(0, session.getEffectiveTime(), "刚开始就结束,有效时间应为0");
|
||||
assertEquals(StudySessionStateEnum.ENDED.name(), session.getSessionState());
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景15:暂停后直接结束(PAUSED 状态),有效时间 < 10分钟 → 清零
|
||||
*/
|
||||
@Test
|
||||
void endedSession_pausedThenDirectlyEnded_lessThan10Min_shouldZeroOut() {
|
||||
StudySessionsEntity session = createOngoingSession(7);
|
||||
// 暂停(累加7分钟有效时间)
|
||||
session.pausedStudySession(LocalDateTime.now());
|
||||
session.setEffectiveTime(7 * 60); // 精确设置为7分钟
|
||||
|
||||
// PAUSED 状态直接结束(不经过 ONGOING)
|
||||
session.endedStudySession();
|
||||
|
||||
assertEquals(0, session.getEffectiveTime(), "PAUSED 状态结束,7分钟 < 10分钟应清零");
|
||||
assertEquals(StudySessionStateEnum.ENDED.name(), session.getSessionState());
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景16:暂停后直接结束(PAUSED 状态),有效时间 > 10分钟 → 保留
|
||||
*/
|
||||
@Test
|
||||
void endedSession_pausedThenDirectlyEnded_moreThan10Min_shouldKeep() {
|
||||
StudySessionsEntity session = createOngoingSession(15);
|
||||
// 暂停(累加15分钟有效时间)
|
||||
session.pausedStudySession(LocalDateTime.now());
|
||||
|
||||
// PAUSED 状态直接结束
|
||||
session.endedStudySession();
|
||||
|
||||
assertTrue(session.getEffectiveTime() > 0, "PAUSED 状态结束,15分钟 > 10分钟应保留");
|
||||
assertEquals(StudySessionStateEnum.ENDED.name(), session.getSessionState());
|
||||
}
|
||||
}
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
package com.guo.learningprogresstracker.service.impl;
|
||||
|
||||
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.mapper.StudyReportFragmentsMapper;
|
||||
import com.guo.learningprogresstracker.mapper.StudyReportsMapper;
|
||||
import com.guo.learningprogresstracker.mapper.StudySessionsMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Spy;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
class StudySessionsServiceImplTest {
|
||||
|
||||
@Spy
|
||||
@InjectMocks
|
||||
private StudySessionsServiceImpl studySessionsService;
|
||||
|
||||
@Mock
|
||||
private StudyReportsMapper studyReportsMapper;
|
||||
|
||||
@Mock
|
||||
private StudyReportFragmentsMapper studyReportFragmentsMapper;
|
||||
|
||||
@Mock
|
||||
private StudySessionsMapper studySessionsMapper;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// stub updateById,避免调用真实的 getBaseMapper()
|
||||
doReturn(true).when(studySessionsService).updateById(any());
|
||||
}
|
||||
|
||||
private StudySessionsEntity createSession(String state, int minutesAgo) {
|
||||
StudySessionsEntity session = new StudySessionsEntity();
|
||||
session.setSessionNum("SESSION_TEST");
|
||||
session.setSessionState(state);
|
||||
session.setStartTime(LocalDateTime.now().minusMinutes(minutesAgo));
|
||||
session.setLastStartTime(LocalDateTime.now().minusMinutes(minutesAgo));
|
||||
session.setEffectiveTime(0);
|
||||
session.setActualTime(0);
|
||||
return session;
|
||||
}
|
||||
|
||||
private void mockGetOneOpt(StudySessionsEntity session) {
|
||||
doReturn(Optional.of(session)).when(studySessionsService).getOneOpt(any(Wrapper.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景1:ONGOING 状态,有效时间 < 10分钟 → 返回提示消息
|
||||
*/
|
||||
@Test
|
||||
void endedStudySession_ongoing_lessThan10Min_shouldReturnMessage() throws Exception {
|
||||
StudySessionsEntity session = createSession(StudySessionStateEnum.ONGOING.name(), 5);
|
||||
mockGetOneOpt(session);
|
||||
|
||||
String message = studySessionsService.endedStudySession("SESSION_TEST", "学习内容");
|
||||
|
||||
assertEquals("本次有效学习时间不足10分钟,不计入总学习时间", message);
|
||||
assertEquals(0, session.getEffectiveTime(), "effectiveTime 应被清零");
|
||||
assertEquals(StudySessionStateEnum.ENDED.name(), session.getSessionState());
|
||||
verify(studyReportsMapper).insert(any(StudyReportsEntity.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景2:ONGOING 状态,有效时间 > 10分钟 → 返回 null(正常结束)
|
||||
*/
|
||||
@Test
|
||||
void endedStudySession_ongoing_moreThan10Min_shouldReturnNull() throws Exception {
|
||||
StudySessionsEntity session = createSession(StudySessionStateEnum.ONGOING.name(), 15);
|
||||
mockGetOneOpt(session);
|
||||
|
||||
String message = studySessionsService.endedStudySession("SESSION_TEST", "学习内容");
|
||||
|
||||
assertNull(message, "有效时间超过10分钟不应返回提示");
|
||||
assertTrue(session.getEffectiveTime() > 0, "effectiveTime 应保留");
|
||||
assertEquals(StudySessionStateEnum.ENDED.name(), session.getSessionState());
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景3:PAUSED 状态,有效时间 < 10分钟 → 返回 null(PAUSED 不触发提示)
|
||||
*/
|
||||
@Test
|
||||
void endedStudySession_paused_lessThan10Min_shouldReturnNull() throws Exception {
|
||||
StudySessionsEntity session = createSession(StudySessionStateEnum.PAUSED.name(), 5);
|
||||
session.setEffectiveTime(5 * 60);
|
||||
mockGetOneOpt(session);
|
||||
|
||||
String message = studySessionsService.endedStudySession("SESSION_TEST", "学习内容");
|
||||
|
||||
assertNull(message, "PAUSED 状态结束不返回提示(用户已主动暂停)");
|
||||
assertEquals(0, session.getEffectiveTime(), "effectiveTime 仍应被实体层清零");
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景4:PAUSED 状态,有效时间 > 10分钟 → 返回 null,effectiveTime 保留
|
||||
*/
|
||||
@Test
|
||||
void endedStudySession_paused_moreThan10Min_shouldReturnNull() throws Exception {
|
||||
StudySessionsEntity session = createSession(StudySessionStateEnum.PAUSED.name(), 15);
|
||||
session.setEffectiveTime(15 * 60);
|
||||
mockGetOneOpt(session);
|
||||
|
||||
String message = studySessionsService.endedStudySession("SESSION_TEST", "学习内容");
|
||||
|
||||
assertNull(message);
|
||||
assertTrue(session.getEffectiveTime() > 0, "effectiveTime 应保留");
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景5:无论是否触发清零,学习报告都应正常创建
|
||||
*/
|
||||
@Test
|
||||
void endedStudySession_shouldAlwaysCreateReport() throws Exception {
|
||||
StudySessionsEntity session = createSession(StudySessionStateEnum.ONGOING.name(), 3);
|
||||
mockGetOneOpt(session);
|
||||
|
||||
studySessionsService.endedStudySession("SESSION_TEST", "测试报告内容");
|
||||
|
||||
verify(studyReportsMapper).insert(argThat(report ->
|
||||
"SESSION_TEST".equals(((StudyReportsEntity) report).getSessionNum())
|
||||
&& "测试报告内容".equals(((StudyReportsEntity) report).getContent())
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景6:ONGOING 状态,有效时间恰好 = 10分钟 → 有效时间保留(600 不 < 600)
|
||||
*/
|
||||
@Test
|
||||
void endedStudySession_ongoing_exactly10Min_shouldReturnNull() throws Exception {
|
||||
StudySessionsEntity session = createSession(StudySessionStateEnum.ONGOING.name(), 10);
|
||||
mockGetOneOpt(session);
|
||||
|
||||
String message = studySessionsService.endedStudySession("SESSION_TEST", "学习内容");
|
||||
|
||||
assertNull(message, "有效时间等于10分钟应保留(阈值为 < 600)");
|
||||
assertTrue(session.getEffectiveTime() >= 10 * 60, "effectiveTime 应保留");
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景7:会话不存在 → 抛出异常
|
||||
*/
|
||||
@Test
|
||||
void endedStudySession_sessionNotFound_shouldThrowException() {
|
||||
doReturn(Optional.empty()).when(studySessionsService).getOneOpt(any(Wrapper.class));
|
||||
|
||||
assertThrows(Exception.class, () ->
|
||||
studySessionsService.endedStudySession("NOT_EXIST", "内容"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景8:ONGOING 状态,有效时间极短(1分钟) → 返回提示消息
|
||||
*/
|
||||
@Test
|
||||
void endedStudySession_ongoing_veryShortTime_shouldReturnMessage() throws Exception {
|
||||
StudySessionsEntity session = createSession(StudySessionStateEnum.ONGOING.name(), 1);
|
||||
mockGetOneOpt(session);
|
||||
|
||||
String message = studySessionsService.endedStudySession("SESSION_TEST", "学习内容");
|
||||
|
||||
assertEquals("本次有效学习时间不足10分钟,不计入总学习时间", message);
|
||||
assertEquals(0, session.getEffectiveTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景9:ONGOING 状态,刚开始就结束(effectiveTime ≈ 0) → 返回提示消息
|
||||
*/
|
||||
@Test
|
||||
void endedStudySession_ongoing_immediatelyEnded_shouldReturnMessage() throws Exception {
|
||||
StudySessionsEntity session = new StudySessionsEntity();
|
||||
session.setSessionNum("SESSION_INSTANT");
|
||||
session.setSessionState(StudySessionStateEnum.ONGOING.name());
|
||||
session.setStartTime(LocalDateTime.now());
|
||||
session.setLastStartTime(LocalDateTime.now());
|
||||
session.setEffectiveTime(0);
|
||||
session.setActualTime(0);
|
||||
mockGetOneOpt(session);
|
||||
|
||||
String message = studySessionsService.endedStudySession("SESSION_INSTANT", "内容");
|
||||
|
||||
assertEquals("本次有效学习时间不足10分钟,不计入总学习时间", message);
|
||||
assertEquals(0, session.getEffectiveTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景10:确认 updateById 被调用(会话状态持久化)
|
||||
*/
|
||||
@Test
|
||||
void endedStudySession_shouldCallUpdateById() throws Exception {
|
||||
StudySessionsEntity session = createSession(StudySessionStateEnum.ONGOING.name(), 5);
|
||||
mockGetOneOpt(session);
|
||||
|
||||
studySessionsService.endedStudySession("SESSION_TEST", "内容");
|
||||
|
||||
verify(studySessionsService).updateById(session);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.guo.learningprogresstracker.utils;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mindrot.jbcrypt.BCrypt;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class BCryptPasswordGeneratorTest {
|
||||
|
||||
@Test
|
||||
void generatePasswordHash() {
|
||||
String password = System.getProperty("password", "admin");
|
||||
int rounds = Integer.parseInt(System.getProperty("rounds", "10"));
|
||||
|
||||
String hash = BCrypt.hashpw(password, BCrypt.gensalt(rounds));
|
||||
|
||||
System.out.println("BCrypt hash:");
|
||||
System.out.println(hash);
|
||||
System.out.println("SQL:");
|
||||
System.out.printf("UPDATE `user` SET `user_password` = '%s' WHERE `user_name` = 'admin';%n", hash);
|
||||
|
||||
assertTrue(BCrypt.checkpw(password, hash));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user