From 36d1edbed4e0ce8d31ec72f92516c355fbbe9465 Mon Sep 17 00:00:00 2001
From: yutao <2930275373@qq.com>
Date: Mon, 28 Apr 2025 21:34:32 +0800
Subject: [PATCH] LYT
---
user/UserUtils.java | 75 +++++++
user/book/controller/UserBookController.java | 79 +++++++
user/book/dto/UserBookDTO.java | 79 +++++++
user/book/entity/UserBook.java | 80 ++++++++
user/book/mapper/UserBookMapper.java | 18 ++
user/book/service/UserBookService.java | 41 ++++
.../service/impl/UserBookServiceImpl.java | 192 ++++++++++++++++++
user/exam/controller/UserExamController.java | 65 ++++++
user/exam/dto/UserExamDTO.java | 84 ++++++++
user/exam/dto/request/UserExamReqDTO.java | 42 ++++
user/exam/dto/response/UserExamRespDTO.java | 36 ++++
user/exam/entity/UserExam.java | 85 ++++++++
user/exam/mapper/UserExamMapper.java | 37 ++++
user/exam/service/UserExamService.java | 50 +++++
.../service/impl/UserExamServiceImpl.java | 116 +++++++++++
15 files changed, 1079 insertions(+)
create mode 100644 user/UserUtils.java
create mode 100644 user/book/controller/UserBookController.java
create mode 100644 user/book/dto/UserBookDTO.java
create mode 100644 user/book/entity/UserBook.java
create mode 100644 user/book/mapper/UserBookMapper.java
create mode 100644 user/book/service/UserBookService.java
create mode 100644 user/book/service/impl/UserBookServiceImpl.java
create mode 100644 user/exam/controller/UserExamController.java
create mode 100644 user/exam/dto/UserExamDTO.java
create mode 100644 user/exam/dto/request/UserExamReqDTO.java
create mode 100644 user/exam/dto/response/UserExamRespDTO.java
create mode 100644 user/exam/entity/UserExam.java
create mode 100644 user/exam/mapper/UserExamMapper.java
create mode 100644 user/exam/service/UserExamService.java
create mode 100644 user/exam/service/impl/UserExamServiceImpl.java
diff --git a/user/UserUtils.java b/user/UserUtils.java
new file mode 100644
index 0000000..7341166
--- /dev/null
+++ b/user/UserUtils.java
@@ -0,0 +1,75 @@
+package com.yf.exam.modules.user;
+
+// 导入自定义的 API 错误信息类
+import com.yf.exam.core.api.ApiError;
+// 导入自定义的服务异常类
+import com.yf.exam.core.exception.ServiceException;
+// 导入系统用户登录信息响应数据传输对象
+import com.yf.exam.modules.sys.user.dto.response.SysUserLoginDTO;
+// 导入 Shiro 安全工具类
+import org.apache.shiro.SecurityUtils;
+
+/**
+ * 用户静态工具类,提供获取当前登录用户信息的静态方法。
+ * @author bool
+ */
+public class UserUtils {
+
+ /**
+ * 获取当前登录用户的ID。
+ *
+ * @param throwable 一个布尔值,指示在获取用户 ID 失败时是否抛出异常。
+ * 若为 true,则在失败时抛出 ServiceException 异常;
+ * 若为 false,则在失败时返回 null。
+ * @return 当前登录用户的 ID,如果获取失败且 throwable 为 false,则返回 null。
+ */
+ public static String getUserId(boolean throwable){
+ try {
+ // 从 Shiro 的 Subject 中获取当前用户的主身份信息,并转换为 SysUserLoginDTO 对象,然后获取用户 ID
+ return ((SysUserLoginDTO) SecurityUtils.getSubject().getPrincipal()).getId();
+ }catch (Exception e){
+ if(throwable){
+ // 若 throwable 为 true,获取用户 ID 失败时抛出服务异常
+ throw new ServiceException(ApiError.ERROR_10010002);
+ }
+ // 若 throwable 为 false,获取用户 ID 失败时返回 null
+ return null;
+ }
+ }
+
+ /**
+ * 判断当前登录用户是否为管理员。
+ *
+ * @param throwable 一个布尔值,指示在获取用户信息失败时是否抛出异常。
+ * 若为 true,则在失败时抛出 ServiceException 异常;
+ * 若为 false,则在失败时返回 false。
+ * @return 若当前登录用户的角色列表包含 "sa",则返回 true;否则返回 false。
+ * 若获取用户信息失败且 throwable 为 false,也返回 false。
+ */
+ public static boolean isAdmin(boolean throwable){
+ try {
+ // 从 Shiro 的 Subject 中获取当前用户的主身份信息,并转换为 SysUserLoginDTO 对象
+ SysUserLoginDTO dto = ((SysUserLoginDTO) SecurityUtils.getSubject().getPrincipal());
+ // 判断用户的角色列表是否包含 "sa"
+ return dto.getRoles().contains("sa");
+ }catch (Exception e){
+ if(throwable){
+ // 若 throwable 为 true,获取用户信息失败时抛出服务异常
+ throw new ServiceException(ApiError.ERROR_10010002);
+ }
+ }
+
+ // 若获取用户信息失败且 throwable 为 false,返回 false
+ return false;
+ }
+
+ /**
+ * 获取当前登录用户的 ID,默认在获取失败时会抛出异常。
+ * 该方法调用了 getUserId(boolean throwable) 方法,并将 throwable 参数设置为 true。
+ *
+ * @return 当前登录用户的 ID,如果获取失败则抛出 ServiceException 异常。
+ */
+ public static String getUserId(){
+ return getUserId(true);
+ }
+}
diff --git a/user/book/controller/UserBookController.java b/user/book/controller/UserBookController.java
new file mode 100644
index 0000000..9ff16d0
--- /dev/null
+++ b/user/book/controller/UserBookController.java
@@ -0,0 +1,79 @@
+package com.yf.exam.modules.user.book.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.yf.exam.core.api.ApiRest;
+import com.yf.exam.core.api.controller.BaseController;
+import com.yf.exam.core.api.dto.BaseIdRespDTO;
+import com.yf.exam.core.api.dto.BaseIdsReqDTO;
+import com.yf.exam.core.api.dto.PagingReqDTO;
+import com.yf.exam.modules.user.book.dto.UserBookDTO;
+import com.yf.exam.modules.user.book.service.UserBookService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ *
+ * 错题本控制器,处理与错题本相关的HTTP请求
+ *
+ *
+ * @author 聪明笨狗
+ * @since 2020-05-27 17:56
+ */
+@Api(tags={"错题本"})
+@RestController
+@RequestMapping("/exam/api/user/wrong-book")
+public class UserBookController extends BaseController {
+
+ /**
+ * 注入错题本服务类,用于处理错题本相关的业务逻辑
+ */
+ @Autowired
+ private UserBookService baseService;
+
+ /**
+ * 批量删除错题本记录
+ * @param reqDTO 包含要删除记录ID列表的请求对象
+ * @return 操作结果的统一响应对象
+ */
+ @ApiOperation(value = "批量删除")
+ @RequestMapping(value = "/delete", method = { RequestMethod.POST})
+ public ApiRest delete(@RequestBody BaseIdsReqDTO reqDTO) {
+ // 根据传入的ID列表删除对应的错题本记录
+ baseService.removeByIds(reqDTO.getIds());
+ // 返回操作成功的响应
+ return super.success();
+ }
+
+ /**
+ * 分页查找错题本记录
+ * @param reqDTO 包含分页信息和查询条件的请求对象
+ * @return 包含分页结果的统一响应对象
+ */
+ @ApiOperation(value = "分页查找")
+ @RequestMapping(value = "/paging", method = { RequestMethod.POST})
+ public ApiRest> paging(@RequestBody PagingReqDTO reqDTO) {
+ // 调用服务层方法进行分页查询,并将结果转换为UserBookDTO对象
+ IPage page = baseService.paging(reqDTO);
+ // 返回包含分页结果的成功响应
+ return super.success(page);
+ }
+
+ /**
+ * 查找下一个错题记录,每次最多返回200条数据
+ * @param reqDTO 包含考试ID和当前题目ID的请求对象
+ * @return 包含下一个题目ID的统一响应对象
+ */
+ @ApiOperation(value = "查找列表")
+ @RequestMapping(value = "/next", method = { RequestMethod.POST})
+ public ApiRest nextQu(@RequestBody UserBookDTO reqDTO) {
+ // 调用服务层方法查找下一个错题的题目ID
+ String quId = baseService.findNext(reqDTO.getExamId(), reqDTO.getQuId());
+ // 将下一个题目ID封装到响应对象中并返回成功响应
+ return super.success(new BaseIdRespDTO(quId));
+ }
+}
diff --git a/user/book/dto/UserBookDTO.java b/user/book/dto/UserBookDTO.java
new file mode 100644
index 0000000..96e4cad
--- /dev/null
+++ b/user/book/dto/UserBookDTO.java
@@ -0,0 +1,79 @@
+package com.yf.exam.modules.user.book.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ *
+ * 错题本请求类,用于在不同层之间传输错题本相关的数据。
+ * 该类包含了错题本记录的基本信息,如考试ID、用户ID、题目ID等。
+ *
+ *
+ * @author 聪明笨狗
+ * @since 2020-05-27 17:56
+ */
+@Data
+@ApiModel(value="错题本", description="错题本")
+public class UserBookDTO implements Serializable {
+
+ // 序列化版本号,用于保证序列化和反序列化过程中类的版本一致性
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 错题本记录的唯一标识ID
+ */
+ @ApiModelProperty(value = "ID", required=true)
+ private String id;
+
+ /**
+ * 关联的考试ID,表明该错题属于哪次考试
+ */
+ @ApiModelProperty(value = "考试ID", required=true)
+ private String examId;
+
+ /**
+ * 关联的用户ID,表明该错题属于哪个用户
+ */
+ @ApiModelProperty(value = "用户ID", required=true)
+ private String userId;
+
+ /**
+ * 关联的题目ID,表明具体是哪道题目做错了
+ */
+ @ApiModelProperty(value = "题目ID", required=true)
+ private String quId;
+
+ /**
+ * 该错题加入错题本的时间
+ */
+ @ApiModelProperty(value = "加入时间", required=true)
+ private Date createTime;
+
+ /**
+ * 该错题最近一次出错的时间
+ */
+ @ApiModelProperty(value = "最近错误时间", required=true)
+ private Date updateTime;
+
+ /**
+ * 该错题累计出错的次数
+ */
+ @ApiModelProperty(value = "错误时间", required=true)
+ private Integer wrongCount;
+
+ /**
+ * 错题的题目标题,用于快速识别题目内容
+ */
+ @ApiModelProperty(value = "题目标题", required=true)
+ private String title;
+
+ /**
+ * 错题在错题本中的排序序号
+ */
+ @ApiModelProperty(value = "错题序号", required=true)
+ private Integer sort;
+}
diff --git a/user/book/entity/UserBook.java b/user/book/entity/UserBook.java
new file mode 100644
index 0000000..a36fb6a
--- /dev/null
+++ b/user/book/entity/UserBook.java
@@ -0,0 +1,80 @@
+package com.yf.exam.modules.user.book.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 com.baomidou.mybatisplus.extension.activerecord.Model;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ *
+ * 错题本实体类,对应数据库表 `el_user_book`,用于存储用户错题相关信息。
+ * 该类继承自 MyBatis-Plus 的 `Model` 类,可使用其提供的 ActiveRecord 功能。
+ *
+ *
+ * @author 聪明笨狗
+ * @since 2020-05-27 17:56
+ */
+@Data
+@TableName("el_user_book")
+public class UserBook extends Model {
+
+ // 序列化版本号,确保序列化和反序列化过程中类版本的一致性
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 错题本记录的唯一标识 ID,在插入数据时由系统自动分配。
+ */
+ @TableId(value = "id", type = IdType.ASSIGN_ID)
+ private String id;
+
+ /**
+ * 关联的考试 ID,表明该错题所属的考试。
+ */
+ @TableField("exam_id")
+ private String examId;
+
+ /**
+ * 关联的用户 ID,表明该错题所属的用户。
+ */
+ @TableField("user_id")
+ private String userId;
+
+ /**
+ * 关联的题目 ID,表明具体是哪道题目被做错。
+ */
+ @TableField("qu_id")
+ private String quId;
+
+ /**
+ * 错题被加入错题本的时间,记录首次出错时间。
+ */
+ @TableField("create_time")
+ private Date createTime;
+
+ /**
+ * 该错题最近一次出错的时间,反映错题的最新错误情况。
+ */
+ @TableField("update_time")
+ private Date updateTime;
+
+ /**
+ * 该错题累计出错的次数,用于统计错题的出错频率。
+ */
+ @TableField("wrong_count")
+ private Integer wrongCount;
+
+ /**
+ * 错题的题目标题,方便用户快速识别错题内容。
+ */
+ private String title;
+
+ /**
+ * 错题在错题本中的排序序号,用于对错题进行排序展示。
+ */
+ private Integer sort;
+
+}
diff --git a/user/book/mapper/UserBookMapper.java b/user/book/mapper/UserBookMapper.java
new file mode 100644
index 0000000..68af834
--- /dev/null
+++ b/user/book/mapper/UserBookMapper.java
@@ -0,0 +1,18 @@
+package com.yf.exam.modules.user.book.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.yf.exam.modules.user.book.entity.UserBook;
+
+/**
+ *
+ * 错题本Mapper接口,用于与数据库中错题本相关表进行数据交互。
+ * 该接口继承自MyBatis-Plus的BaseMapper接口,可直接使用BaseMapper提供的基础CRUD操作方法。
+ *
+ *
+ * @author 聪明笨狗
+ * @since 2020-05-27 17:56
+ */
+public interface UserBookMapper extends BaseMapper {
+ // 若需要自定义数据库操作方法,可在此处添加接口方法声明
+ // 对应的SQL语句可以在XML文件中实现,或者使用@Select、@Insert等注解
+}
diff --git a/user/book/service/UserBookService.java b/user/book/service/UserBookService.java
new file mode 100644
index 0000000..b4c0f74
--- /dev/null
+++ b/user/book/service/UserBookService.java
@@ -0,0 +1,41 @@
+package com.yf.exam.modules.user.book.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.yf.exam.core.api.dto.PagingReqDTO;
+import com.yf.exam.modules.user.book.dto.UserBookDTO;
+import com.yf.exam.modules.user.book.entity.UserBook;
+
+/**
+ *
+ * 错题本业务类接口,定义了错题本相关的业务操作方法。
+ * 该接口继承自 MyBatis-Plus 的 IService 接口,可使用其提供的基础服务方法。
+ *
+ *
+ * @author 聪明笨狗
+ * @since 2020-05-27 17:56
+ */
+public interface UserBookService extends IService {
+
+ /**
+ * 分页查询错题本数据
+ * @param reqDTO 包含分页信息和查询条件的请求对象,泛型为 UserBookDTO
+ * @return 包含分页结果的 UserBookDTO 对象集合,封装在 IPage 中
+ */
+ IPage paging(PagingReqDTO reqDTO);
+
+ /**
+ * 将指定题目加入错题本
+ * @param examId 关联的考试 ID,标识该错题所属的考试
+ * @param quId 关联的题目 ID,标识具体是哪道题目
+ */
+ void addBook(String examId, String quId);
+
+ /**
+ * 查找当前错题的下一个错题
+ * @param examId 关联的考试 ID,限定查找范围为该考试内的错题
+ * @param quId 当前错题的题目 ID,基于此查找下一个错题
+ * @return 下一个错题的题目 ID,如果不存在则可能返回 null
+ */
+ String findNext(String examId, String quId);
+}
diff --git a/user/book/service/impl/UserBookServiceImpl.java b/user/book/service/impl/UserBookServiceImpl.java
new file mode 100644
index 0000000..38d8b48
--- /dev/null
+++ b/user/book/service/impl/UserBookServiceImpl.java
@@ -0,0 +1,192 @@
+package com.yf.exam.modules.user.book.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.TypeReference;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.yf.exam.core.api.dto.PagingReqDTO;
+import com.yf.exam.modules.qu.entity.Qu;
+import com.yf.exam.modules.qu.service.QuService;
+import com.yf.exam.modules.user.UserUtils;
+import com.yf.exam.modules.user.book.dto.UserBookDTO;
+import com.yf.exam.modules.user.book.entity.UserBook;
+import com.yf.exam.modules.user.book.mapper.UserBookMapper;
+import com.yf.exam.modules.user.book.service.UserBookService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+/**
+ *
+ * 错题本服务实现类,实现了错题本相关的业务逻辑。
+ *
+ *
+ * @author 聪明笨狗
+ * @since 2020-05-27 17:56
+ */
+@Service
+public class UserBookServiceImpl extends ServiceImpl implements UserBookService {
+
+ /**
+ * 注入题目服务类,用于获取题目相关信息
+ */
+ @Autowired
+ private QuService quService;
+
+ /**
+ * 分页查询用户错题本记录
+ * @param reqDTO 包含分页信息和查询条件的请求对象
+ * @return 包含分页结果的错题本记录 DTO 对象
+ */
+ @Override
+ public IPage paging(PagingReqDTO reqDTO) {
+
+ // 创建分页对象,指定当前页码和每页显示数量
+ Page query = new Page<>(reqDTO.getCurrent(), reqDTO.getSize());
+
+ // 构建查询条件包装器
+ QueryWrapper wrapper = new QueryWrapper<>();
+ // 只查询当前用户的错题记录
+ wrapper.lambda().eq(UserBook::getUserId, UserUtils.getUserId(true));
+
+ // 获取查询参数
+ UserBookDTO params = reqDTO.getParams();
+ if (params != null) {
+ // 如果标题参数不为空,添加模糊查询条件
+ if (!StringUtils.isEmpty(params.getTitle())) {
+ wrapper.lambda().like(UserBook::getTitle, params.getTitle());
+ }
+
+ // 如果考试 ID 参数不为空,添加精确查询条件
+ if (!StringUtils.isEmpty(params.getExamId())) {
+ wrapper.lambda().eq(UserBook::getExamId, params.getExamId());
+ }
+ }
+
+ // 执行分页查询,获取错题本实体分页数据
+ IPage page = this.page(query, wrapper);
+ // 将实体分页数据转换为 DTO 分页数据
+ IPage pageData = JSON.parseObject(JSON.toJSONString(page), new TypeReference>(){});
+ return pageData;
+ }
+
+ /**
+ * 将错题添加到用户错题本中
+ * @param examId 考试 ID
+ * @param quId 题目 ID
+ */
+ @Override
+ public void addBook(String examId, String quId) {
+
+ // 构建查询条件,查找该用户在本次考试中该题目的错题记录
+ QueryWrapper wrapper = new QueryWrapper<>();
+ wrapper.lambda()
+ .eq(UserBook::getUserId, UserUtils.getUserId())
+ .eq(UserBook::getExamId, examId)
+ .eq(UserBook::getQuId, quId);
+
+ // 查找已有的错题信息
+ UserBook book = this.getOne(wrapper, false);
+
+ // 获取题目信息
+ Qu qu = quService.getById(quId);
+
+ if (book == null) {
+ // 如果该错题记录不存在,则创建新的错题记录
+ book = new UserBook();
+ book.setExamId(examId);
+ book.setUserId(UserUtils.getUserId());
+ book.setTitle(qu.getContent());
+ book.setQuId(quId);
+ book.setWrongCount(1);
+ // 获取当前考试中用户错题的最大排序值,并加 1 作为新记录的排序值
+ Integer maxSort = this.findMaxSort(examId, UserUtils.getUserId());
+ book.setSort(maxSort + 1);
+
+ // 保存新的错题记录
+ this.save(book);
+ } else {
+ // 如果该错题记录已存在,错误次数加 1
+ book.setWrongCount(book.getWrongCount() + 1);
+ // 更新错题记录
+ this.updateById(book);
+ }
+ }
+
+ /**
+ * 查找当前错题的下一个错题的题目 ID
+ * @param examId 考试 ID
+ * @param quId 当前错题的题目 ID
+ * @return 下一个错题的题目 ID,如果不存在则返回 null
+ */
+ @Override
+ public String findNext(String examId, String quId) {
+
+ // 初始化排序值为一个较大值
+ Integer sort = 999999;
+
+ if (!StringUtils.isEmpty(quId)) {
+ // 构建查询条件,查找当前错题记录
+ QueryWrapper wrapper = new QueryWrapper<>();
+ wrapper.lambda()
+ .eq(UserBook::getUserId, UserUtils.getUserId())
+ .eq(UserBook::getExamId, examId)
+ .eq(UserBook::getQuId, quId);
+ // 按排序值降序排序
+ wrapper.last(" ORDER BY `sort` DESC");
+
+ // 获取当前错题记录
+ UserBook last = this.getOne(wrapper, false);
+ if (last != null) {
+ // 如果找到当前错题记录,获取其排序值
+ sort = last.getSort();
+ }
+ }
+
+ // 构建查询条件,查找排序值小于当前错题的下一个错题记录
+ QueryWrapper wrapper = new QueryWrapper<>();
+ wrapper.lambda()
+ .eq(UserBook::getUserId, UserUtils.getUserId())
+ .eq(UserBook::getExamId, examId)
+ .lt(UserBook::getSort, sort);
+ // 按排序值降序排序
+ wrapper.last(" ORDER BY `sort` DESC");
+
+ // 获取下一个错题记录
+ UserBook next = this.getOne(wrapper, false);
+ if (next != null) {
+ // 如果找到下一个错题记录,返回其题目 ID
+ return next.getQuId();
+ }
+
+ return null;
+ }
+
+ /**
+ * 查找指定考试中用户错题的最大排序值
+ * @param examId 考试 ID
+ * @param userId 用户 ID
+ * @return 最大排序值,如果没有记录则返回 0
+ */
+ private Integer findMaxSort(String examId, String userId) {
+
+ // 构建查询条件,查找指定考试中用户的错题记录
+ QueryWrapper wrapper = new QueryWrapper<>();
+ wrapper.lambda()
+ .eq(UserBook::getExamId, examId)
+ .eq(UserBook::getUserId, userId);
+ // 按排序值降序排序
+ wrapper.last(" ORDER BY `sort` DESC");
+
+ // 获取排序值最大的错题记录
+ UserBook book = this.getOne(wrapper, false);
+ if (book == null) {
+ // 如果没有记录,返回 0
+ return 0;
+ }
+ // 返回最大排序值
+ return book.getSort();
+ }
+}
diff --git a/user/exam/controller/UserExamController.java b/user/exam/controller/UserExamController.java
new file mode 100644
index 0000000..ae03687
--- /dev/null
+++ b/user/exam/controller/UserExamController.java
@@ -0,0 +1,65 @@
+package com.yf.exam.modules.user.exam.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.yf.exam.core.api.ApiRest;
+import com.yf.exam.core.api.controller.BaseController;
+import com.yf.exam.core.api.dto.PagingReqDTO;
+import com.yf.exam.modules.user.exam.dto.request.UserExamReqDTO;
+import com.yf.exam.modules.user.exam.dto.response.UserExamRespDTO;
+import com.yf.exam.modules.user.exam.service.UserExamService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ *
+ * 考试记录控制器,处理与用户考试记录相关的 HTTP 请求。
+ * 该控制器提供了分页查询考试记录的接口。
+ *
+ *
+ * @author 聪明笨狗
+ * @since 2020-09-21 15:13
+ */
+@Api(tags={"考试记录"})
+@RestController
+@RequestMapping("/exam/api/user/exam")
+public class UserExamController extends BaseController {
+
+ /**
+ * 注入用户考试记录服务类,用于处理考试记录相关的业务逻辑。
+ */
+ @Autowired
+ private UserExamService baseService;
+
+ /**
+ * 分页查找考试记录
+ * @param reqDTO 包含分页信息和查询条件的请求对象,泛型为 UserExamReqDTO
+ * @return 包含分页结果的统一响应对象,分页结果为 UserExamRespDTO 类型
+ */
+ @ApiOperation(value = "分页查找")
+ @RequestMapping(value = "/paging", method = { RequestMethod.POST})
+ public ApiRest> paging(@RequestBody PagingReqDTO reqDTO) {
+ // 调用服务层的分页查询方法,获取分页后的考试记录
+ IPage page = baseService.paging(reqDTO);
+ // 返回包含分页结果的成功响应
+ return super.success(page);
+ }
+
+ /**
+ * 分页查找当前用户的考试记录
+ * @param reqDTO 包含分页信息和查询条件的请求对象,泛型为 UserExamReqDTO
+ * @return 包含分页结果的统一响应对象,分页结果为 UserExamRespDTO 类型
+ */
+ @ApiOperation(value = "分页查找")
+ @RequestMapping(value = "/my-paging", method = { RequestMethod.POST})
+ public ApiRest> myPaging(@RequestBody PagingReqDTO reqDTO) {
+ // 调用服务层的分页查询方法,获取当前用户分页后的考试记录
+ IPage page = baseService.myPaging(reqDTO);
+ // 返回包含分页结果的成功响应
+ return super.success(page);
+ }
+}
diff --git a/user/exam/dto/UserExamDTO.java b/user/exam/dto/UserExamDTO.java
new file mode 100644
index 0000000..e6d915d
--- /dev/null
+++ b/user/exam/dto/UserExamDTO.java
@@ -0,0 +1,84 @@
+// 声明该类所在的包,此包用于存放用户考试相关的数据传输对象
+package com.yf.exam.modules.user.exam.dto;
+
+// 导入自定义的字典注解,用于处理数据字典映射
+import com.yf.exam.core.annon.Dict;
+// 导入 Swagger 相关注解,用于生成 API 文档
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+// 导入 Lombok 的 Data 注解,自动生成 getter、setter、equals、hashCode 和 toString 方法
+import lombok.Data;
+// 导入日期类,用于表示时间相关的属性
+import java.util.Date;
+// 导入序列化接口,使该类的对象可以被序列化和反序列化
+import java.io.Serializable;
+
+/**
+ *
+ * 考试记录数据传输类,用于在不同层(如控制层、服务层、数据访问层)之间传输考试记录相关的数据。
+ * 该类封装了考试记录的基本信息,如用户ID、考试ID、考试次数、最高分数等。
+ *
+ *
+ * @author 聪明笨狗
+ * @since 2020-09-21 15:13
+ */
+// 使用 Lombok 的 Data 注解,简化代码,自动生成常用方法
+@Data
+// 使用 Swagger 的 ApiModel 注解,为 API 文档提供该类的描述信息
+@ApiModel(value="考试记录", description="考试记录")
+public class UserExamDTO implements Serializable {
+
+ // 序列化版本号,用于保证序列化和反序列化过程中类的版本一致性,避免版本不一致导致的错误
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 考试记录的唯一标识ID,用于在数据库中唯一确定一条考试记录。
+ */
+ private String id;
+
+ /**
+ * 参加考试的用户ID,该字段为必填项,用于关联参加考试的用户。
+ */
+ @ApiModelProperty(value = "用户ID", required=true)
+ private String userId;
+
+ /**
+ * 关联的考试ID,该字段为必填项。
+ * @Dict 注解用于从 `el_exam` 表中根据 `id` 字段获取对应的 `title` 作为字典文本,
+ * 方便在前端展示考试的标题信息。
+ */
+ @Dict(dictTable = "el_exam", dicText = "title", dicCode = "id")
+ @ApiModelProperty(value = "考试ID", required=true)
+ private String examId;
+
+ /**
+ * 用户参加该考试的次数,该字段为必填项,用于统计用户参加特定考试的频率。
+ */
+ @ApiModelProperty(value = "考试次数", required=true)
+ private Integer tryCount;
+
+ /**
+ * 用户参加该考试获得的最高分数,该字段为必填项,用于记录用户在该考试中的最佳成绩。
+ */
+ @ApiModelProperty(value = "最高分数", required=true)
+ private Integer maxScore;
+
+ /**
+ * 用户是否通过该考试,该字段为必填项,用于判断用户在考试中的最终结果。
+ */
+ @ApiModelProperty(value = "是否通过", required=true)
+ private Boolean passed;
+
+ /**
+ * 考试记录的创建时间,记录该考试记录首次被创建的时间。
+ */
+ @ApiModelProperty(value = "创建时间")
+ private Date createTime;
+
+ /**
+ * 考试记录的更新时间,记录该考试记录最后一次被修改的时间。
+ */
+ @ApiModelProperty(value = "更新时间")
+ private Date updateTime;
+
+}
diff --git a/user/exam/dto/request/UserExamReqDTO.java b/user/exam/dto/request/UserExamReqDTO.java
new file mode 100644
index 0000000..594eeb7
--- /dev/null
+++ b/user/exam/dto/request/UserExamReqDTO.java
@@ -0,0 +1,42 @@
+package com.yf.exam.modules.user.exam.dto.request;
+
+// 导入 UserExamDTO 类,作为当前类的父类
+import com.yf.exam.modules.user.exam.dto.UserExamDTO;
+// 导入 Swagger 注解,用于 API 文档生成
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+// 导入 Lombok 的 Data 注解,自动生成 getter、setter、toString 等方法
+import lombok.Data;
+
+/**
+ *
+ * 考试记录数据传输类,用于封装考试记录相关的请求数据。
+ * 该类继承自 UserExamDTO,可复用其属性,同时添加了额外的请求所需属性。
+ *
+ *
+ * @author 聪明笨狗
+ * @since 2020-09-21 15:13
+ */
+// 使用 Lombok 的 Data 注解,自动生成常用方法
+@Data
+// 使用 Swagger 的 ApiModel 注解,为 API 文档提供模型描述
+@ApiModel(value="考试记录", description="考试记录")
+public class UserExamReqDTO extends UserExamDTO {
+
+ // 序列化版本号,用于保证序列化和反序列化过程中类的版本一致性
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 考试名称,用于标识具体的考试,在请求中为必填项。
+ */
+ // 使用 Swagger 的 ApiModelProperty 注解,为 API 文档描述该属性
+ @ApiModelProperty(value = "考试名称", required=true)
+ private String title;
+
+ /**
+ * 参考人员的真实姓名,用于关联参加考试的人员,在请求中为必填项。
+ */
+ // 使用 Swagger 的 ApiModelProperty 注解,为 API 文档描述该属性
+ @ApiModelProperty(value = "人员名称", required=true)
+ private String realName;
+}
diff --git a/user/exam/dto/response/UserExamRespDTO.java b/user/exam/dto/response/UserExamRespDTO.java
new file mode 100644
index 0000000..982a836
--- /dev/null
+++ b/user/exam/dto/response/UserExamRespDTO.java
@@ -0,0 +1,36 @@
+package com.yf.exam.modules.user.exam.dto.response;
+
+import com.yf.exam.modules.user.exam.dto.UserExamDTO;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ *
+ * 考试记录响应数据传输类,用于封装返回给前端的考试记录相关信息。
+ * 该类继承自 UserExamDTO,复用其属性,同时添加了额外的响应所需属性。
+ *
+ *
+ * @author 聪明笨狗
+ * @since 2020-09-21 15:13
+ */
+@Data
+@ApiModel(value="考试记录", description="考试记录")
+public class UserExamRespDTO extends UserExamDTO {
+
+ // 序列化版本号,用于保证序列化和反序列化过程中类的版本一致性
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 考试名称,标识具体的考试,该信息在响应中为必填项。
+ */
+ @ApiModelProperty(value = "考试名称", required=true)
+ private String title;
+
+ /**
+ * 参考人员的真实姓名,用于关联参加考试的人员,该信息在响应中为必填项。
+ */
+ @ApiModelProperty(value = "人员名称", required=true)
+ private String realName;
+
+}
diff --git a/user/exam/entity/UserExam.java b/user/exam/entity/UserExam.java
new file mode 100644
index 0000000..b1104ae
--- /dev/null
+++ b/user/exam/entity/UserExam.java
@@ -0,0 +1,85 @@
+package com.yf.exam.modules.user.exam.entity;
+
+// 导入 MyBatis-Plus 主键类型注解,用于指定主键生成策略
+import com.baomidou.mybatisplus.annotation.IdType;
+// 导入 MyBatis-Plus 表字段注解,用于指定实体类属性与数据库表字段的映射关系
+import com.baomidou.mybatisplus.annotation.TableField;
+// 导入 MyBatis-Plus 表主键注解,用于指定实体类的主键属性
+import com.baomidou.mybatisplus.annotation.TableId;
+// 导入 MyBatis-Plus 表名注解,用于指定实体类对应的数据库表名
+import com.baomidou.mybatisplus.annotation.TableName;
+// 导入 MyBatis-Plus 活动记录模型类,实体类继承该类可使用活动记录模式操作数据库
+import com.baomidou.mybatisplus.extension.activerecord.Model;
+// 导入 Lombok 的 Data 注解,自动生成 getter、setter、equals、hashCode 和 toString 方法
+import lombok.Data;
+// 导入日期类,用于表示时间相关的属性
+import java.util.Date;
+
+/**
+ *
+ * 考试记录实体类,对应数据库中的 `el_user_exam` 表,用于封装考试记录的相关信息。
+ * 该类继承自 MyBatis-Plus 的 Model 类,可使用活动记录模式进行数据库操作。
+ *
+ *
+ * @author 聪明笨狗
+ * @since 2020-09-21 15:13
+ */
+// 使用 Lombok 的 Data 注解,简化代码,自动生成常用方法
+@Data
+// 使用 MyBatis-Plus 的 TableName 注解,指定该实体类对应的数据库表名为 `el_user_exam`
+@TableName("el_user_exam")
+public class UserExam extends Model {
+
+ // 序列化版本号,用于保证序列化和反序列化过程中类的版本一致性
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 考试记录的唯一标识 ID,对应数据库表中的 `id` 字段。
+ * 使用 MyBatis-Plus 的 TableId 注解指定主键,采用 ASSIGN_ID 策略自动生成 ID。
+ */
+ @TableId(value = "id", type = IdType.ASSIGN_ID)
+ private String id;
+
+ /**
+ * 用户 ID,对应数据库表中的 `user_id` 字段,用于关联参加考试的用户。
+ */
+ @TableField("user_id")
+ private String userId;
+
+ /**
+ * 考试 ID,对应数据库表中的 `exam_id` 字段,用于关联具体的考试。
+ */
+ @TableField("exam_id")
+ private String examId;
+
+ /**
+ * 考试次数,对应数据库表中的 `try_count` 字段,记录用户参加该考试的次数。
+ */
+ @TableField("try_count")
+ private Integer tryCount;
+
+ /**
+ * 最高分数,对应数据库表中的 `max_score` 字段,记录用户参加该考试获得的最高分数。
+ */
+ @TableField("max_score")
+ private Integer maxScore;
+
+ /**
+ * 是否通过,对应数据库表中未显式指定字段名(默认按属性名映射),
+ * 记录用户是否通过该考试。
+ */
+ private Boolean passed;
+
+ /**
+ * 创建时间,对应数据库表中的 `create_time` 字段,记录该考试记录的创建时间。
+ */
+ @TableField("create_time")
+ private Date createTime;
+
+ /**
+ * 更新时间,对应数据库表中的 `update_time` 字段,记录该考试记录的更新时间。
+ */
+ @TableField("update_time")
+ private Date updateTime;
+
+}
diff --git a/user/exam/mapper/UserExamMapper.java b/user/exam/mapper/UserExamMapper.java
new file mode 100644
index 0000000..24aa5c5
--- /dev/null
+++ b/user/exam/mapper/UserExamMapper.java
@@ -0,0 +1,37 @@
+package com.yf.exam.modules.user.exam.mapper;
+
+// 导入 MyBatis-Plus 基础 Mapper 接口,提供基本的数据库 CRUD 操作
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+// 导入 MyBatis-Plus 分页查询结果接口,用于封装分页查询结果
+import com.baomidou.mybatisplus.core.metadata.IPage;
+// 导入 MyBatis-Plus 分页对象,用于设置分页查询的参数
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+// 导入考试记录请求数据传输对象,用于封装查询条件
+import com.yf.exam.modules.user.exam.dto.request.UserExamReqDTO;
+// 导入考试记录响应数据传输对象,用于封装查询结果
+import com.yf.exam.modules.user.exam.dto.response.UserExamRespDTO;
+// 导入考试记录实体类,对应数据库中的考试记录表
+import com.yf.exam.modules.user.exam.entity.UserExam;
+// 导入 MyBatis 参数注解,用于在 SQL 语句中引用方法参数
+import org.apache.ibatis.annotations.Param;
+
+/**
+ *
+ * 考试记录Mapper接口,用于定义与考试记录相关的数据库操作方法。
+ * 该接口继承自 MyBatis-Plus 的 BaseMapper 接口,可使用其提供的基础数据库操作方法。
+ *
+ *
+ * @author 聪明笨狗
+ * @since 2020-09-21 15:13
+ */
+public interface UserExamMapper extends BaseMapper {
+
+ /**
+ * 分页查询我的考试记录
+ * @param page 分页对象,包含当前页码、每页显示数量等分页信息
+ * @param query 考试记录请求数据传输对象,包含查询条件,如考试名称、参考人员姓名等
+ * @return 包含分页结果的考试记录响应数据传输对象集合,封装在 IPage 中
+ */
+ IPage paging(Page page, @Param("query") UserExamReqDTO query);
+
+}
diff --git a/user/exam/service/UserExamService.java b/user/exam/service/UserExamService.java
new file mode 100644
index 0000000..4c80bf4
--- /dev/null
+++ b/user/exam/service/UserExamService.java
@@ -0,0 +1,50 @@
+package com.yf.exam.modules.user.exam.service;
+
+// 导入 MyBatis-Plus 分页元数据接口,用于封装分页查询结果
+import com.baomidou.mybatisplus.core.metadata.IPage;
+// 导入 MyBatis-Plus 扩展服务接口,提供基础的 CRUD 操作
+import com.baomidou.mybatisplus.extension.service.IService;
+// 导入自定义的分页请求数据传输对象,用于封装分页查询的请求参数
+import com.yf.exam.core.api.dto.PagingReqDTO;
+// 导入考试记录请求数据传输对象,用于封装考试记录查询的具体条件
+import com.yf.exam.modules.user.exam.dto.request.UserExamReqDTO;
+// 导入考试记录响应数据传输对象,用于封装考试记录查询的返回结果
+import com.yf.exam.modules.user.exam.dto.response.UserExamRespDTO;
+// 导入考试记录实体类,对应数据库中的考试记录数据
+import com.yf.exam.modules.user.exam.entity.UserExam;
+
+/**
+ *
+ * 考试记录业务类,定义了与考试记录相关的业务操作接口。
+ * 该接口继承自 MyBatis-Plus 的 IService 接口,可使用其提供的基础服务方法。
+ *
+ *
+ * @author 聪明笨狗
+ * @since 2020-09-21 15:13
+ */
+public interface UserExamService extends IService {
+
+ /**
+ * 分页查询考试记录数据
+ * @param reqDTO 包含分页信息和考试记录查询条件的请求对象,泛型为 UserExamReqDTO
+ * @return 包含分页结果的考试记录响应对象集合,封装在 IPage 中
+ */
+ IPage paging(PagingReqDTO reqDTO);
+
+ /**
+ * 分页查询当前用户的考试记录数据
+ * @param reqDTO 包含分页信息和考试记录查询条件的请求对象,泛型为 UserExamReqDTO
+ * @return 包含分页结果的考试记录响应对象集合,封装在 IPage 中
+ */
+ IPage myPaging(PagingReqDTO reqDTO);
+
+ /**
+ * 考试完成后,将用户的考试成绩信息加入到考试记录中
+ * 如果该用户针对此考试已有记录,则更新相关信息;若不存在,则创建新记录。
+ * @param userId 参加考试的用户 ID
+ * @param examId 考试的 ID
+ * @param score 用户本次考试获得的分数
+ * @param passed 用户是否通过本次考试
+ */
+ void joinResult(String userId, String examId, Integer score, boolean passed);
+}
diff --git a/user/exam/service/impl/UserExamServiceImpl.java b/user/exam/service/impl/UserExamServiceImpl.java
new file mode 100644
index 0000000..1d0062a
--- /dev/null
+++ b/user/exam/service/impl/UserExamServiceImpl.java
@@ -0,0 +1,116 @@
+package com.yf.exam.modules.user.exam.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.yf.exam.core.api.dto.PagingReqDTO;
+import com.yf.exam.modules.user.UserUtils;
+import com.yf.exam.modules.user.exam.dto.request.UserExamReqDTO;
+import com.yf.exam.modules.user.exam.dto.response.UserExamRespDTO;
+import com.yf.exam.modules.user.exam.entity.UserExam;
+import com.yf.exam.modules.user.exam.mapper.UserExamMapper;
+import com.yf.exam.modules.user.exam.service.UserExamService;
+import org.springframework.stereotype.Service;
+
+import java.util.Date;
+
+/**
+ *
+ * 考试记录业务实现类,实现了 UserExamService 接口,处理考试记录相关的业务逻辑。
+ * 继承自 MyBatis-Plus 的 ServiceImpl 类,可使用其提供的基础服务方法。
+ *
+ *
+ * @author 聪明笨狗
+ * @since 2020-09-21 15:13
+ */
+@Service
+public class UserExamServiceImpl extends ServiceImpl implements UserExamService {
+
+ /**
+ * 分页查询考试记录
+ * @param reqDTO 包含分页信息和查询条件的请求对象,泛型为 UserExamReqDTO
+ * @return 包含分页结果的 UserExamRespDTO 对象集合,封装在 IPage 中
+ */
+ @Override
+ public IPage paging(PagingReqDTO reqDTO) {
+ // 调用 Mapper 层的分页查询方法,将请求的分页信息和查询参数传入
+ // 并将查询结果转换为包含 UserExamRespDTO 的分页数据
+ IPage pageData = baseMapper.paging(reqDTO.toPage(), reqDTO.getParams());
+ return pageData;
+ }
+
+ /**
+ * 分页查询当前用户的考试记录
+ * @param reqDTO 包含分页信息和查询条件的请求对象,泛型为 UserExamReqDTO
+ * @return 包含分页结果的 UserExamRespDTO 对象集合,封装在 IPage 中
+ */
+ @Override
+ public IPage myPaging(PagingReqDTO reqDTO) {
+ // 获取请求中的查询参数
+ UserExamReqDTO params = reqDTO.getParams();
+
+ // 若查询参数为空,则创建一个新的 UserExamReqDTO 对象
+ if(params == null){
+ params = new UserExamReqDTO();
+ }
+
+ // 设置当前用户的 ID 到查询参数中
+ params.setUserId(UserUtils.getUserId());
+
+ // 调用 Mapper 层的分页查询方法,将请求的分页信息和更新后的查询参数传入
+ // 并将查询结果转换为包含 UserExamRespDTO 的分页数据
+ IPage pageData = baseMapper.paging(reqDTO.toPage(), params);
+ return pageData;
+ }
+
+ /**
+ * 记录用户考试结果,更新或创建考试记录
+ * @param userId 用户 ID
+ * @param examId 考试 ID
+ * @param score 用户本次考试得分
+ * @param passed 用户是否通过本次考试
+ */
+ @Override
+ public void joinResult(String userId, String examId, Integer score, boolean passed) {
+ // 构建查询条件,根据用户 ID 和考试 ID 查询考试记录
+ QueryWrapper wrapper = new QueryWrapper<>();
+ wrapper.lambda().eq(UserExam::getUserId, userId)
+ .eq(UserExam::getExamId, examId);
+
+ // 根据查询条件获取一条考试记录
+ UserExam record = this.getOne(wrapper, false);
+ if(record == null){
+ // 若记录不存在,则创建一条新的考试记录
+ record = new UserExam();
+ // 设置记录创建时间
+ record.setCreateTime(new Date());
+ // 设置记录更新时间
+ record.setUpdateTime(new Date());
+ // 设置用户 ID
+ record.setUserId(userId);
+ // 设置考试 ID
+ record.setExamId(examId);
+ // 设置最高分数为本次考试得分
+ record.setMaxScore(score);
+ // 设置是否通过考试
+ record.setPassed(passed);
+ // 保存新的考试记录
+ this.save(record);
+ return;
+ }
+
+ // 修复低分数不加入统计问题,增加考试次数
+ record.setTryCount(record.getTryCount() + 1);
+ // 更新记录更新时间
+ record.setUpdateTime(new Date());
+
+ // 若本次考试得分高于之前的最高分数,则更新最高分数和是否通过考试状态
+ if(record.getMaxScore() < score){
+ record.setMaxScore(score);
+ record.setPassed(passed);
+ }
+
+ // 根据记录 ID 更新考试记录
+ this.updateById(record);
+ }
+}