diff --git a/src/main/java/com/example/exam/exam/dal/ExamErrorQuestion.java b/src/main/java/com/example/exam/exam/dal/ExamErrorQuestion.java new file mode 100644 index 0000000..b813800 --- /dev/null +++ b/src/main/java/com/example/exam/exam/dal/ExamErrorQuestion.java @@ -0,0 +1,126 @@ +package com.example.exam.exam.dal; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 错题集 + * + * @author pengchen + */ + +@TableName(value = "exam_error_question", autoResultMap = true) +@Accessors(chain = true) +@Data +public class ExamErrorQuestion { + + + @TableId + private String id; + /** + * 试题id + */ + private String quId; + + /** + * 任务ID + */ + private String taskId; + + /** + * 章节名称 + */ + private String chapteridDictText; + /** + * 题型名称 + */ + private String subjectName; + + /** + * 题型难度(0:简单,1:一般,2:困难) + */ + private Integer quLevel; + + /** + * 试题内容(带样式:

下列表格123

\n\n\n\n\) + */ + private String content; + + /** + * 试题内容(纯文本) + */ + private String contentText; + + /** + * 解析(带样式) + */ + private String analysis; + + /** + * c语言参考答案 + */ + private String answer; + + + /** + * 知识点 + */ + private String pointNames; + /** + * 关键字 + */ + private String keywords; + + /** + * 课程类别 + */ + private String courseName; + + /** + * 专业分类 + */ + private String specialtyName; + /** + * 数据库名 + */ + private String tname; + + /** + * 试题答案 + */ + @TableField(exist = false) + private List answerList; + + /** + * 试题文件 + */ + @TableField(exist = false) + private List fileUploads; + + /** + * 试题判分 + */ + @TableField(exist = false) + private ExamQuestionScore questionScores; + + /** + * 试题关键字 + */ + @TableField(exist = false) + private List questionKeywords; + + private LocalDateTime createTime; + + private String creator; + /** + * 租户id + */ + private long tenantId; + +} diff --git a/src/main/java/com/example/exam/exam/mapper/ExamErrorQuestionMapper.java b/src/main/java/com/example/exam/exam/mapper/ExamErrorQuestionMapper.java new file mode 100644 index 0000000..bd011d4 --- /dev/null +++ b/src/main/java/com/example/exam/exam/mapper/ExamErrorQuestionMapper.java @@ -0,0 +1,27 @@ +package com.example.exam.exam.mapper; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.exam.exam.dal.ExamErrorQuestion; +import com.example.exam.exam.dal.ExamQuestion; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +/** + * 错题集Mapper接口 + * + * @author pengchen + */ +@Mapper +public interface ExamErrorQuestionMapper extends BaseMapper +{ + + default ExamErrorQuestion selectByIdAndUserId(String quId, String userId, String tenantId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(ExamErrorQuestion::getQuId, quId); + wrapper.eq(ExamErrorQuestion::getCreator, userId); + wrapper.eq(ExamErrorQuestion::getTenantId, tenantId); + return selectOne(wrapper); + } +} diff --git a/src/main/java/com/example/exam/exam/service/error/ExamErrorQuestionServiceImpl.java b/src/main/java/com/example/exam/exam/service/error/ExamErrorQuestionServiceImpl.java new file mode 100644 index 0000000..b101158 --- /dev/null +++ b/src/main/java/com/example/exam/exam/service/error/ExamErrorQuestionServiceImpl.java @@ -0,0 +1,32 @@ +package com.example.exam.exam.service.error; + + +import com.example.exam.exam.dal.*; +import com.example.exam.exam.mapper.*; +import jakarta.annotation.Resource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 错题集Service业务层处理 + * + * @author pengchen + */ +@Service +public class ExamErrorQuestionServiceImpl implements IExamErrorQuestionService +{ + @Resource + ExamErrorQuestionMapper examErrorQuestionMapper; + + @Override + public int insertExamErrorQuestion(ExamErrorQuestion examErrorQuestion) { + return examErrorQuestionMapper.insert(examErrorQuestion); + } + + @Override + public boolean isExamErrorQuestion(String quId, String userId, String tenantId) { + return examErrorQuestionMapper.selectByIdAndUserId(quId, userId, tenantId) != null; + } +} diff --git a/src/main/java/com/example/exam/exam/service/error/IExamErrorQuestionService.java b/src/main/java/com/example/exam/exam/service/error/IExamErrorQuestionService.java new file mode 100644 index 0000000..9f87b31 --- /dev/null +++ b/src/main/java/com/example/exam/exam/service/error/IExamErrorQuestionService.java @@ -0,0 +1,21 @@ +package com.example.exam.exam.service.error; + + +import com.example.exam.exam.dal.ExamErrorQuestion; + +/** + * 错题集Service接口 + * + * @author pengchen + */ +public interface IExamErrorQuestionService { + /** + * 新增错题集数据 + */ + public int insertExamErrorQuestion(ExamErrorQuestion examErrorQuestion); + + /** + * 判断错题是否存在 + */ + boolean isExamErrorQuestion(String quId, String userId, String tenantId); +} diff --git a/src/main/java/com/example/exam/exam/utils/error_question/ErrorQuestion.java b/src/main/java/com/example/exam/exam/utils/error_question/ErrorQuestion.java new file mode 100644 index 0000000..fb46986 --- /dev/null +++ b/src/main/java/com/example/exam/exam/utils/error_question/ErrorQuestion.java @@ -0,0 +1,36 @@ +package com.example.exam.exam.utils.error_question; + +import com.example.exam.exam.controller.auto.vo.StuInfoVo; +import com.example.exam.exam.dal.ExamErrorQuestion; +import com.example.exam.exam.dal.ExamQuestion; +import com.example.exam.exam.dal.SystemTenant; +import com.example.exam.exam.utils.snowflake.SnowflakeId; +import org.springframework.beans.BeanUtils; + +import java.time.LocalDateTime; + +public class ErrorQuestion { + + public static ExamErrorQuestion insertErrorQuestion(String questionId, + ExamQuestion examQuestion, + String taskId, + SystemTenant systemTenant, + StuInfoVo stuInfoVo, + int isError) { + // 判断是否进入错题集 + if (isError != 0) { + // 如果不是全对得话进入错题集 + System.out.println("进入错题集" + questionId); + ExamErrorQuestion examErrorQuestion = new ExamErrorQuestion(); + BeanUtils.copyProperties(examQuestion, examErrorQuestion); + examErrorQuestion.setTaskId(taskId); + SnowflakeId idWorker = new SnowflakeId(0, 31); + examErrorQuestion.setId(idWorker.nextIdStr()); + examErrorQuestion.setTenantId(systemTenant.getId()); + examErrorQuestion.setCreator(String.valueOf(stuInfoVo.getStuId())); + examErrorQuestion.setCreateTime(LocalDateTime.now()); + return examErrorQuestion; + } + return null; + } +} diff --git a/src/main/java/com/example/exam/exam/utils/snowflake/SnowflakeId.java b/src/main/java/com/example/exam/exam/utils/snowflake/SnowflakeId.java new file mode 100644 index 0000000..1564697 --- /dev/null +++ b/src/main/java/com/example/exam/exam/utils/snowflake/SnowflakeId.java @@ -0,0 +1,180 @@ +package com.example.exam.exam.utils.snowflake; + +import java.time.Instant; + +/** + * 雪花算法 ID 生成器(64bit) + * 结构:1bit 符号位 | 41bit 时间戳(ms) | 5bit 数据中心 | 5bit 工作节点 | 12bit 序列 + * 默认 epoch:2025-01-01T00:00:00Z + */ +public class SnowflakeId { + + // ---- 位宽定义 ---- + private static final long WORKER_ID_BITS = 5L; // 0-31 + private static final long DATACENTER_ID_BITS = 5L; // 0-31 + private static final long SEQUENCE_BITS = 12L; // 0-4095 + + private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS); // 31 + private static final long MAX_DATACENTER_ID = ~(-1L << DATACENTER_ID_BITS); // 31 + private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS); // 4095 + + private static final long WORKER_ID_SHIFT = SEQUENCE_BITS; // 12 + private static final long DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS; // 17 + private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS; // 22 + + // ---- 配置 ---- + private final long epoch; // 自定义纪元(毫秒) + private final long datacenterId; // 0..31 + private final long workerId; // 0..31 + + // ---- 运行时状态 ---- + private long lastTimestamp = -1L; // 上次生成的时间戳 + private long sequence = 0L; // 当前毫秒内的序列 + + /** + * 构造器 + * @param datacenterId 0..31 + * @param workerId 0..31 + * @param epochMillis 自定义纪元(毫秒),建议固定且小于当前时间 + */ + public SnowflakeId(long datacenterId, long workerId, long epochMillis) { + if (datacenterId < 0 || datacenterId > MAX_DATACENTER_ID) { + throw new IllegalArgumentException("datacenterId out of range: 0.." + MAX_DATACENTER_ID); + } + if (workerId < 0 || workerId > MAX_WORKER_ID) { + throw new IllegalArgumentException("workerId out of range: 0.." + MAX_WORKER_ID); + } + this.datacenterId = datacenterId; + this.workerId = workerId; + this.epoch = epochMillis; + } + + /** + * 使用默认纪元(2020-01-01T00:00:00Z) + */ + public SnowflakeId(long datacenterId, long workerId) { + this(datacenterId, workerId, Instant.parse("2020-01-01T00:00:00Z").toEpochMilli()); + } + + /** + * 生成下一个 ID(线程安全) + */ + public synchronized long nextId() { + long now = currentTime(); + + // 处理时钟回拨 + if (now < lastTimestamp) { + long offset = lastTimestamp - now; + // 小回拨:尝试等待一个回拨差值的时长 + if (offset <= 5) { + sleepMs(offset); + now = currentTime(); + if (now < lastTimestamp) { + // 仍小于:采用“借用序列”的容错策略(把时间当作 lastTimestamp) + now = lastTimestamp; + } + } else { + // 大回拨:直接使用 lastTimestamp(保序)或抛出异常 + // 为了高可用,这里选择保序;如需严格,可改为抛出异常 + now = lastTimestamp; + } + } + + if (now == lastTimestamp) { + // 同一毫秒内自增序列 + sequence = (sequence + 1) & SEQUENCE_MASK; + if (sequence == 0) { + // 序列溢出,等待到下一毫秒 + now = waitUntilNextMillis(lastTimestamp); + } + } else { + // 新毫秒,序列重置为 0(也可随机起步以降低热点) + sequence = 0L; + } + + lastTimestamp = now; + + long timestampPart = (now - epoch) << TIMESTAMP_SHIFT; + long datacenterPart = (datacenterId << DATACENTER_ID_SHIFT); + long workerPart = (workerId << WORKER_ID_SHIFT); + long sequencePart = sequence; + + return timestampPart | datacenterPart | workerPart | sequencePart; + } + + /** + * 生成字符串形式的 ID(十进制) + */ + public String nextIdStr() { + return Long.toUnsignedString(nextId()); + } + + /** + * 解析:从 ID 中还原出时间戳(绝对毫秒) + */ + public long extractTimestamp(long id) { + long ts = (id >>> TIMESTAMP_SHIFT) + epoch; + return ts; + } + + /** + * 解析:数据中心 ID + */ + public long extractDatacenterId(long id) { + return (id >>> DATACENTER_ID_SHIFT) & MAX_DATACENTER_ID; + } + + /** + * 解析:工作节点 ID + */ + public long extractWorkerId(long id) { + return (id >>> WORKER_ID_SHIFT) & MAX_WORKER_ID; + } + + /** + * 解析:序列 + */ + public long extractSequence(long id) { + return id & SEQUENCE_MASK; + } + + private long waitUntilNextMillis(long lastTs) { + long ts = currentTime(); + while (ts <= lastTs) { + ts = currentTime(); + } + return ts; + } + + private long currentTime() { + return System.currentTimeMillis(); + } + + private void sleepMs(long ms) { + try { + Thread.sleep(ms); + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + } + } + + // ---- 简易单例 ---- + private static class Holder { + // 默认:数据中心 0,工作节点 0,可按需修改 + private static final SnowflakeId INSTANCE = new SnowflakeId(0, 0); + } + + /** 获取全局单例(如项目只需一个生成器时使用) */ + public static SnowflakeId getInstance() { + return Holder.INSTANCE; + } + + // ---- DEMO ---- + public static void main(String[] args) { + SnowflakeId gen = new SnowflakeId(1, 3); // 数据中心=1,工作节点=3 + for (int i = 0; i < 5; i++) { + long id = gen.nextId(); + System.out.println(id + " ts=" + Instant.ofEpochMilli(gen.extractTimestamp(id))); + } + } +}
1