diff --git a/init.sql b/init.sql new file mode 100644 index 0000000..718011a --- /dev/null +++ b/init.sql @@ -0,0 +1,38 @@ +-- 创建数据库 (如果不存在) +CREATE DATABASE IF NOT EXISTS `exam` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +USE `exam`; + +-- 1. 题库表 (question) +CREATE TABLE IF NOT EXISTS `question` ( + `id` INT AUTO_INCREMENT PRIMARY KEY COMMENT '题目ID', + `title` TEXT NOT NULL COMMENT '题目内容', + `type` INT NOT NULL COMMENT '题目类型: 1-选择题, 2-简答题', + `answer` TEXT COMMENT '学生答案参考(仅简答题)', + `answer_a` VARCHAR(255) COMMENT '选项A(仅选择题)', + `answer_b` VARCHAR(255) COMMENT '选项B(仅选择题)', + `answer_c` VARCHAR(255) COMMENT '选项C(仅选择题)', + `answer_d` VARCHAR(255) COMMENT '选项D(仅选择题)', + `r_answer` TEXT COMMENT '正确答案', + `score` INT NOT NULL DEFAULT 0 COMMENT '题目分值' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='题库表'; + +-- 2. 试卷表 (exam_paper) +CREATE TABLE IF NOT EXISTS `exam_paper` ( + `id` INT AUTO_INCREMENT PRIMARY KEY COMMENT '试卷自增ID', + `uid` VARCHAR(64) NOT NULL UNIQUE COMMENT '试卷唯一标识(UUID等)', + `title` VARCHAR(255) COMMENT '试卷标题', + `select` TEXT COMMENT '选择题集合或相关配置', + `content` TEXT COMMENT '简答题集合或相关内容' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='生成的试卷表'; + +-- 3. 答题记录表/批改记录表 (exam_back) +CREATE TABLE IF NOT EXISTS `exam_back` ( + `id` INT AUTO_INCREMENT PRIMARY KEY COMMENT '答题记录自增ID', + `uid` VARCHAR(64) NOT NULL UNIQUE COMMENT '答题记录唯一标识', + `examId` VARCHAR(64) NOT NULL COMMENT '关联的试卷UID', + `title` VARCHAR(255) COMMENT '试卷标题(冗余防丢)', + `name` VARCHAR(100) NOT NULL COMMENT '做题人姓名', + `content` JSON COMMENT '包含题目和所填答案的JSON数组', + `score` VARCHAR(50) COMMENT '总得分/批改状态' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='学生答卷与批改表'; \ No newline at end of file diff --git a/questions_data.sql b/questions_data.sql new file mode 100644 index 0000000..9ac2d21 --- /dev/null +++ b/questions_data.sql @@ -0,0 +1,120 @@ +-- 题库数据插入脚本 +-- 共100道题,选择题70道(分值2-3分),简答题30道(分值4-5分) +-- 组合示例:14道选择题(3分)+6道简答题(5分)=42+30=72分;20道选择题(3分)+8道简答题(5分)=60+40=100分 + +USE exam; + +-- 清空已有数据(可选) +-- TRUNCATE TABLE question; + +-- 选择题(70道)- 35道3分,35道2分 +INSERT INTO question (title, type, answer_a, answer_b, answer_c, answer_d, r_answer, score) VALUES +('Java中,以下哪个关键字用于定义类?', 1, 'class', 'interface', 'extends', 'implements', 'A', 3), +('Python中,列表推导式的语法是?', 1, '[x for x in iterable]', '(x for x in iterable)', '{x for x in iterable}', '', 'A', 2), +('HTTP协议默认使用的端口号是?', 1, '80', '8080', '443', '3306', 'A', 3), +('MySQL中,用于删除表的命令是?', 1, 'DELETE TABLE', 'DROP TABLE', 'REMOVE TABLE', 'CLEAR TABLE', 'B', 2), +('CSS中,用于设置背景颜色的属性是?', 1, 'background-color', 'bgcolor', 'color', 'background', 'A', 3), +('JavaScript中,以下哪个不是原始数据类型?', 1, 'String', 'Number', 'Object', 'Boolean', 'C', 2), +('Spring Boot中,主类上的核心注解是?', 1, '@SpringApplication', '@SpringBootApplication', '@EnableAutoConfiguration', '@ComponentScan', 'B', 3), +('Git中,用于提交代码的命令是?', 1, 'git push', 'git commit', 'git add', 'git pull', 'B', 2), +('Linux中,查看当前目录的命令是?', 1, 'ls', 'cd', 'pwd', 'dir', 'C', 3), +('HTML中,用于定义超链接的标签是?', 1, '', '', '', '', 'B', 2), +('Java中,String类的equals方法用于?', 1, '比较引用', '比较内容', '赋值', '连接字符串', 'B', 3), +('数据库中,ACID特性不包括?', 1, 'Atomicity', 'Consistency', 'Isolation', 'Distribution', 'D', 2), +('Python中,len函数的作用是?', 1, '计算长度', '计算总和', '计算平均值', '计算最大值', 'A', 3), +('Redis是一种?', 1, '关系型数据库', '内存数据库', '文档数据库', '图数据库', 'B', 2), +('Docker中,用于构建镜像的命令是?', 1, 'docker run', 'docker build', 'docker pull', 'docker push', 'B', 3), +('Java中,以下哪个集合类是线程安全的?', 1, 'ArrayList', 'HashMap', 'Vector', 'HashSet', 'C', 2), +('Maven中,用于定义项目依赖的文件是?', 1, 'build.xml', 'pom.xml', 'config.xml', 'project.xml', 'B', 3), +('JavaScript中,===运算符的作用是?', 1, '赋值', '相等比较', '严格相等比较', '不等于', 'C', 2), +('Linux中,查看文件内容的命令是?', 1, 'open', 'cat', 'view', 'show', 'B', 3), +('HTTP状态码404表示?', 1, '服务器错误', '未授权', '未找到', '重定向', 'C', 2), +('Java中,final关键字不能用于修饰?', 1, '类', '方法', '变量', '接口', 'D', 3), +('Python中,def关键字用于?', 1, '定义变量', '定义函数', '定义类', '定义模块', 'B', 2), +('MySQL中,主键的特点是?', 1, '可以为空', '可以重复', '唯一且非空', '自动生成', 'C', 3), +('Vue.js中,用于数据绑定的指令是?', 1, 'v-if', 'v-for', 'v-model', 'v-bind', 'C', 2), +('Nginx主要用于?', 1, '数据库服务器', 'Web服务器/反向代理', '缓存服务器', '消息队列', 'B', 3), +('Java中,异常处理的关键字不包括?', 1, 'try', 'catch', 'throw', 'error', 'D', 2), +('CSS中,盒模型不包括?', 1, 'content', 'padding', 'border', 'layer', 'D', 3), +('Spring中,依赖注入的方式不包括?', 1, '构造器注入', 'Setter注入', '接口注入', '字段注入', 'C', 2), +('Git中,创建新分支的命令是?', 1, 'git branch', 'git checkout', 'git switch', '以上都可以', 'D', 3), +('Python中,导入模块的关键字是?', 1, 'include', 'import', 'using', 'require', 'B', 2), +('数据库中,用于排序的关键字是?', 1, 'GROUP BY', 'ORDER BY', 'SORT BY', 'ARRANGE BY', 'B', 3), +('JavaScript中,数组的push方法作用是?', 1, '删除末尾元素', '在末尾添加元素', '在开头添加元素', '删除开头元素', 'B', 2), +('Linux中,修改文件权限的命令是?', 1, 'chown', 'chmod', 'chgrp', 'perm', 'B', 3), +('HTML5中,用于本地存储的API是?', 1, 'cookie', 'localStorage', 'session', 'cache', 'B', 2), +('Java中,接口可以包含?', 1, '构造方法', '静态方法(Java8+)', '实例变量', '普通方法体(Java7)', 'B', 3), +('MongoDB是一种?', 1, '关系型数据库', '文档型NoSQL数据库', '键值对数据库', '列族数据库', 'B', 2), +('Kafka主要用于?', 1, '数据存储', '消息队列/流处理', '负载均衡', '缓存', 'B', 3), +('Python中,列表和元组的区别是?', 1, '列表有序,元组无序', '列表可变,元组不可变', '列表可以重复,元组不可以', '没有区别', 'B', 2), +('CSS中,Flex布局的主轴方向默认是?', 1, 'row', 'column', 'row-reverse', 'column-reverse', 'A', 3), +('Java中,synchronized关键字用于?', 1, '继承', '多态', '线程同步', '异常处理', 'C', 2), +('MySQL中,JOIN操作的作用是?', 1, '删除数据', '更新数据', '连接多表查询', '插入数据', 'C', 3), +('React中,用于状态管理的Hook是?', 1, 'useEffect', 'useState', 'useContext', 'useReducer', 'B', 2), +('Linux中,查找文件的命令是?', 1, 'grep', 'find', 'locate', 'search', 'B', 3), +('HTTP状态码500表示?', 1, '客户端错误', '服务器内部错误', '未授权', '禁止访问', 'B', 2), +('Java中,abstract类不能?', 1, '有构造方法', '有抽象方法', '直接实例化', '有成员变量', 'C', 3), +('Python中,__init__方法是?', 1, '构造函数', '析构函数', '普通方法', '静态方法', 'A', 2), +('Elasticsearch主要用于?', 1, '关系存储', '全文搜索', '图计算', '事务处理', 'B', 3), +('Git中,合并分支的命令是?', 1, 'git combine', 'git merge', 'git join', 'git integrate', 'B', 2), +('CSS中,z-index属性用于?', 1, '水平定位', '垂直定位', '层级控制', '透明度控制', 'C', 3), +('Spring Boot中,application.yml用于?', 1, '编写业务代码', '配置应用程序', '定义数据模型', '编写测试', 'B', 2), +('JavaScript中,typeof null返回?', 1, '"null"', '"undefined"', '"object"', '"number"', 'C', 3), +('Redis的数据类型不包括?', 1, 'String', 'List', 'Table', 'Set', 'C', 2), +('Docker中,用于查看运行容器的命令是?', 1, 'docker images', 'docker ps', 'docker list', 'docker show', 'B', 3), +('MySQL中,索引的作用是?', 1, '增加存储空间', '提高查询效率', '保证数据完整性', '加密数据', 'B', 2), +('Vue.js中,v-if和v-show的区别是?', 1, '没有区别', 'v-if是条件渲染,v-show是CSS切换', 'v-if更快', 'v-show只能用于div', 'B', 3), +('Linux中,压缩文件的命令是?', 1, 'zip', 'tar', 'gzip', '以上都可以', 'D', 2), +('Java中, transient关键字的作用是?', 1, '序列化时忽略该字段', '线程安全', '常量定义', '同步锁', 'A', 3), +('Python的装饰器是?', 1, '一种设计模式', '语法糖,用于修改函数', '一种数据结构', '一种算法', 'B', 2), +('Nginx配置文件的默认路径是?', 1, '/etc/nginx/nginx.conf', '/usr/local/nginx/conf/nginx.conf', 'A和B都可能', '/var/nginx/nginx.conf', 'C', 3), +('React中,props的作用是?', 1, '存储组件内部状态', '父组件向子组件传递数据', '事件处理', '路由跳转', 'B', 2), +('SQL中,GROUP BY用于?', 1, '排序', '分组聚合', '筛选', '连接表', 'B', 3), +('Java中,volatile关键字用于?', 1, '声明常量', '保证变量可见性', '序列化', '异常处理', 'B', 2), +('CSS中,@media用于?', 1, '导入样式', '定义动画', '响应式布局', '字体设置', 'C', 3), +('Git中,撤销上次提交的命令是?', 1, 'git reset', 'git revert', 'git undo', 'git rollback', 'A', 2), +('Spring Cloud中,服务注册的组件是?', 1, 'Zuul', 'Eureka', 'Ribbon', 'Hystrix', 'B', 3), +('Python中,lambda表达式是?', 1, '匿名函数', '装饰器', '生成器', '迭代器', 'A', 2), +('Linux中,查看内存使用情况的命令是?', 1, 'df', 'du', 'free', 'top', 'C', 3), +('HTTP是无状态协议,解决会话保持的技术是?', 1, 'Cookie/Session', 'WebSocket', 'TCP', 'UDP', 'A', 2), +('Java中,默认的类加载器是?', 1, 'Bootstrap ClassLoader', 'Extension ClassLoader', 'Application ClassLoader', 'User ClassLoader', 'C', 3), +('Node.js是基于什么语言开发的?', 1, 'Python', 'Java', 'C++', 'JavaScript', 'D', 2), +('Java中,==和equals的区别是?', 1, '没有区别', '==比较引用,equals比较内容', 'equals比较引用,==比较内容', '都比较内容', 'B', 3), +('Python中,字典的键可以是?', 1, '列表', '字典', '元组', '集合', 'C', 2), +('MySQL中,事务的隔离级别不包括?', 1, 'READ UNCOMMITTED', 'READ COMMITTED', 'WRITE COMMITTED', 'SERIALIZABLE', 'C', 3), +('JavaScript中,闭包的作用是?', 1, '提高性能', '实现封装和私有变量', '简化代码', '减少内存', 'B', 2), +('Linux中,grep命令的作用是?', 1, '查找文件', '过滤文本', '压缩文件', '排序文本', 'B', 3), +('Spring中,@Autowired的作用是?', 1, '声明Bean', '自动注入依赖', '配置属性', '定义切面', 'B', 2); + +-- 简答题(30道)- 20道5分,10道4分 +INSERT INTO question (title, type, answer, r_answer, score) VALUES +('简述Java中ArrayList和LinkedList的区别及适用场景。', 2, '', 'ArrayList基于动态数组实现,查询快O(1)、增删慢O(n),适合随机访问多的场景;LinkedList基于双向链表实现,增删快O(1)、查询慢O(n),适合频繁插入删除的场景。', 5), +('什么是数据库事务的ACID特性?请分别解释。', 2, '', 'ACID包括:原子性(Atomicity)事务要么全完成要么全不完成;一致性(Consistency)事务前后数据完整性一致;隔离性(Isolation)事务之间互不干扰;持久性(Durability)事务完成后数据永久保存。', 5), +('解释Spring框架中的IOC和AOP概念。', 2, '', 'IOC(控制反转)是将对象创建和依赖关系管理交给Spring容器,降低耦合;AOP(面向切面编程)将横切关注点如日志、事务从业务逻辑中分离,通过代理实现功能增强。', 5), +('简述Redis的五种基本数据类型及其使用场景。', 2, '', 'String:缓存、计数器;List:消息队列、时间线;Set:去重、集合运算;ZSet:排行榜;Hash:存储对象属性。', 5), +('什么是RESTful API?它有哪些特点?', 2, '', 'RESTful是一种软件架构风格,使用URL定位资源,HTTP动词(GET/POST/PUT/DELETE)操作资源。特点:无状态、统一接口、可缓存、分层系统。', 4), +('解释Java中的多线程同步机制有哪些?', 2, '', 'synchronized关键字、ReentrantLock显式锁、volatile保证可见性、Atomic原子类、Concurrent并发集合、Semaphore信号量等。', 5), +('MySQL中索引失效的常见情况有哪些?', 2, '', '使用!=或<>、对列进行函数操作、LIKE以%开头、类型隐式转换、OR条件无索引、复合索引未用左前缀、IS NOT NULL等。', 5), +('简述TCP三次握手的过程。', 2, '', '第一次:客户端发送SYN;第二次:服务端回复SYN+ACK;第三次:客户端回复ACK。目的是确认双方收发能力正常,同步初始序列号。', 4), +('什么是JVM内存模型?主要包括哪些区域?', 2, '', 'JVM内存模型定义多线程共享变量的访问规则。运行时数据区包括:堆、方法区、虚拟机栈、本地方法栈、程序计数器。', 5), +('简述Vue.js的生命周期钩子函数。', 2, '', '创建前/后:beforeCreate/created;挂载前/后:beforeMount/mounted;更新前/后:beforeUpdate/updated;销毁前/后:beforeUnmount/unmounted。', 5), +('什么是Docker?它解决了什么问题?', 2, '', 'Docker是容器化平台,将应用及其依赖打包为镜像,实现一次构建到处运行。解决了环境不一致、部署复杂、资源利用率低等问题。', 4), +('解释Git的工作流程和常用分支策略。', 2, '', '工作区→暂存区→本地仓库→远程仓库。常见策略:Git Flow(master/develop/feature/release/hotfix分支)或GitHub Flow(主干开发)。', 5), +('什么是微服务架构?它的优缺点是什么?', 2, '', '将单体应用拆分为小型、独立部署的服务。优点:技术异构、独立扩展、故障隔离;缺点:分布式复杂性、运维成本高、数据一致性难保证。', 5), +('简述HTTPS的加密流程。', 2, '', '客户端请求证书→服务端发送证书(含公钥)→客户端验证证书→生成随机对称密钥→用公钥加密发送→双方用对称密钥加密通信。', 5), +('Java 8引入了哪些重要新特性?', 2, '', 'Lambda表达式、Stream API、Optional类、新日期时间API、默认方法、方法引用、函数式接口、接口中的静态方法等。', 4), +('什么是数据库的范式?请列举前三种。', 2, '', '1NF:属性原子性;2NF:消除部分依赖;3NF:消除传递依赖。目的是减少数据冗余,保证数据一致性。', 5), +('解释消息队列MQ的主要作用和使用场景。', 2, '', '作用:异步处理、应用解耦、流量削峰、日志处理。场景:订单处理、秒杀系统、日志收集、通知推送等。', 5), +('简述Java垃圾回收的基本原理。', 2, '', 'JVM自动回收不再使用的对象内存。常用算法:标记-清除、复制、标记-整理。现代JVM采用分代收集:新生代用复制,老年代用标记-整理。', 5), +('什么是CSS盒模型?标准盒模型和IE盒模型有何区别?', 2, '', '盒模型包含content、padding、border、margin。标准盒模型width/height只含content;IE盒模型包含content+padding+border。通过box-sizing切换。', 4), +('解释OAuth 2.0的授权流程。', 2, '', '四种模式:授权码模式(最安全)、简化模式、密码凭证、客户端凭证。主流授权码流程:请求授权→获取code→换取token→访问资源。', 5), +('什么是设计模式?请列举三种常用创建型模式。', 2, '', '设计模式是解决特定问题的最佳实践。创建型模式:单例模式、工厂模式、建造者模式、原型模式。', 5), +('简述Nginx的负载均衡策略有哪些?', 2, '', '轮询(默认)、权重(weight)、IP哈希(ip_hash)、最少连接(least_conn)、一致性哈希等。', 4), +('什么是WebSocket?与HTTP有什么区别?', 2, '', 'WebSocket是全双工通信协议,建立连接后可双向实时通信。HTTP是请求-响应模式、无状态、单向。WebSocket适合实时应用如聊天、股票行情。', 5), +('解释Spring Boot的自动配置原理。', 2, '', '@EnableAutoConfiguration通过classpath扫描依赖,根据META-INF/spring.factories中定义的自动配置类,结合条件注解如@ConditionalOnClass进行条件装配。', 5), +('MySQL中InnoDB和MyISAM存储引擎的区别?', 2, '', 'InnoDB:支持事务、行级锁、外键、MVCC,适合高并发;MyISAM:表级锁、全文索引、速度快,适合读多写少。', 5), +('什么是前端路由?React Router的工作原理是什么?', 2, '', '前端路由在不刷新页面的情况下切换视图。React Router通过history API监听URL变化,匹配Route组件渲染对应组件,实现SPA导航。', 4), +('简述分布式系统中的CAP理论。', 2, '', 'CAP指一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)。分布式系统无法同时满足三者,最多满足两个,通常是AP或CP。', 5), +('什么是线程池?Java中如何创建线程池?', 2, '', '线程池复用线程减少创建销毁开销。通过Executors工厂方法或ThreadPoolExecutor构造函数创建,参数包括核心线程数、最大线程数、队列、拒绝策略等。', 5), +('解释跨域问题产生的原因及解决方案。', 2, '', '浏览器同源策略限制不同源请求。方案:CORS(服务端设置响应头)、JSONP(只支持GET)、代理服务器、Nginx反向代理、WebSocket等。', 4), +('简述Kafka的基本架构和核心概念。', 2, '', 'Kafka是分布式流处理平台。核心概念:Producer生产者、Consumer消费者、Broker服务器、Topic主题、Partition分区、Offset偏移量、Replication副本。', 5); diff --git a/src/main/java/com/baobaot/exam/Controller/AdminController.java b/src/main/java/com/baobaot/exam/Controller/AdminController.java new file mode 100644 index 0000000..89374c2 --- /dev/null +++ b/src/main/java/com/baobaot/exam/Controller/AdminController.java @@ -0,0 +1,105 @@ +package com.baobaot.exam.Controller; + +import com.baobaot.exam.Service.adminService; +import com.baobaot.exam.Service.makeExamService; +import com.baobaot.exam.dao.examPaper; +import com.baobaot.exam.dao.question; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +@Controller +public class AdminController { + + @Autowired + private adminService adminService; + + @Autowired + private makeExamService makeExamService; + + @GetMapping("/admin") + public String adminRedirect() { + return "redirect:/admin/exam"; + } + + @GetMapping("/admin/exam") + public String adminPage(Model model) { + List questions = adminService.list(); + model.addAttribute("questions", questions); + return "admin"; + } + + @PostMapping("/admin/add/choice") + public String addChoiceQuestion(@RequestParam String title, + @RequestParam Integer type, + @RequestParam(required = false) String answer_a, + @RequestParam(required = false) String answer_b, + @RequestParam(required = false) String answer_c, + @RequestParam(required = false) String answer_d, + @RequestParam String r_answer, + @RequestParam Integer score) { + adminService.addChoiceQuestion(title, type, answer_a, answer_b, answer_c, answer_d, r_answer, score); + return "redirect:/admin/exam"; + } + + @PostMapping("/admin/add/essay") + public String addEssayQuestion(@RequestParam String title, + @RequestParam Integer type, + @RequestParam(required = false) String answer, + @RequestParam(required = false) String r_answer, + @RequestParam Integer score) { + adminService.addEssayQuestion(title, type, answer, r_answer, score); + return "redirect:/admin/exam"; + } + + @GetMapping("/admin/query/type") + public String queryByType(@RequestParam Integer type, Model model) { + List questions = adminService.getQuestionsByType(type); + model.addAttribute("questions", questions); + model.addAttribute("queryType", "类型: " + type); + return "admin"; + } + + @GetMapping("/admin/query/score") + public String queryByScore(@RequestParam Integer score, Model model) { + List questions = adminService.getQuestionsByScore(score); + model.addAttribute("questions", questions); + model.addAttribute("queryScore", "分数: " + score); + return "admin"; + } + + @PostMapping("/admin/delete") + public String deleteByTitle(@RequestParam String title) { + adminService.deleteQuestionByTitle(title); + return "redirect:/admin/exam"; + } + + @GetMapping("/admin/mkexam") + public String makeExamPage(Model model) { + return "mkexam"; + } + + @PostMapping("/admin/mkexam/create") + @ResponseBody + public examPaper createExam(@RequestParam(required = false) String title) { + return makeExamService.createExamPaper(title); + } + + @GetMapping("/admin/mkexam/detail/{uid}") + @ResponseBody + public Map getExamDetail(@PathVariable String uid) { + return makeExamService.getExamPaperDetail(uid); + } + + @GetMapping("/admin/listexam") + public String listExamPage(@RequestParam(required = false) String title, Model model) { + List examPapers = makeExamService.getExamList(title); + model.addAttribute("examPapers", examPapers); + model.addAttribute("searchTitle", title); + return "listexam"; + } +} diff --git a/src/main/java/com/baobaot/exam/Controller/CorrectController.java b/src/main/java/com/baobaot/exam/Controller/CorrectController.java new file mode 100644 index 0000000..d35c612 --- /dev/null +++ b/src/main/java/com/baobaot/exam/Controller/CorrectController.java @@ -0,0 +1,147 @@ +package com.baobaot.exam.Controller; + +import com.baobaot.exam.Service.CorrectService; +import com.baobaot.exam.Service.ExamAnswerService; +import com.baobaot.exam.Service.makeExamService; +import com.baobaot.exam.dao.examBack; +import com.baobaot.exam.dto.AnswerItem; +import com.baobaot.exam.dto.CorrectResult; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 批改控制器 + */ +@Controller +@RequestMapping("/admin/correct") +public class CorrectController { + + @Autowired + private CorrectService correctService; + + @Autowired + private ExamAnswerService examAnswerService; + + @Autowired + private makeExamService makeExamService; + + /** + * 批改首页 - 搜索界面 + */ + @GetMapping("") + public String correctPage() { + return "correct"; + } + + /** + * 获取试卷标题列表(下拉菜单用) + */ + @GetMapping("/titles") + @ResponseBody + public Map getExamTitles() { + Map result = new HashMap<>(); + + try { + List titles = correctService.getExamTitleList(); + result.put("success", true); + result.put("data", titles); + } catch (Exception e) { + result.put("success", false); + result.put("message", e.getMessage()); + } + + return result; + } + + /** + * 搜索答题记录 + * @param name 做题人姓名(可选) + * @param title 试卷标题(可选) + */ + @GetMapping("/search") + @ResponseBody + public Map searchAnswers( + @RequestParam(required = false) String name, + @RequestParam(required = false) String title) { + Map result = new HashMap<>(); + + try { + List answers = correctService.searchAnswers(name, title); + result.put("success", true); + result.put("data", answers); + result.put("count", answers.size()); + } catch (Exception e) { + result.put("success", false); + result.put("message", e.getMessage()); + } + + return result; + } + + /** + * 获取答题详情(用于批改) + * @param answerUid 答题记录UID + */ + @GetMapping("/detail/{answerUid}") + @ResponseBody + public Map getAnswerDetail(@PathVariable String answerUid) { + Map result = new HashMap<>(); + + try { + CorrectResult detail = correctService.getAnswerDetailForCorrect(answerUid); + result.put("success", true); + result.put("data", detail); + } catch (Exception e) { + result.put("success", false); + result.put("message", e.getMessage()); + } + + return result; + } + + /** + * 提交批改结果 + */ + @PostMapping("/submit/{answerUid}") + @ResponseBody + public Map submitCorrect( + @PathVariable String answerUid, + @RequestBody Map essayScores) { + Map result = new HashMap<>(); + + try { + // essayScores: key = "q_" + qid, value = 得分 + int totalScore = correctService.submitCorrect(answerUid, essayScores); + result.put("success", true); + result.put("message", "批改完成"); + result.put("totalScore", totalScore); + } catch (Exception e) { + result.put("success", false); + result.put("message", e.getMessage()); + } + + return result; + } + + /** + * 打印页面 - 纯文本试卷答案和分数 + * @param answerUid 答题记录UID + */ + @GetMapping("/print/{answerUid}") + public String printPage(@PathVariable String answerUid, Model model) { + try { + CorrectResult detail = correctService.getAnswerDetailForCorrect(answerUid); + model.addAttribute("data", detail); + return "print"; + } catch (Exception e) { + model.addAttribute("error", e.getMessage()); + return "print"; + } + } +} diff --git a/src/main/java/com/baobaot/exam/Controller/ExamAnswerController.java b/src/main/java/com/baobaot/exam/Controller/ExamAnswerController.java new file mode 100644 index 0000000..228f090 --- /dev/null +++ b/src/main/java/com/baobaot/exam/Controller/ExamAnswerController.java @@ -0,0 +1,183 @@ +package com.baobaot.exam.Controller; + +import com.baobaot.exam.Service.ExamAnswerService; +import com.baobaot.exam.Service.makeExamService; +import com.baobaot.exam.dao.examBack; +import com.baobaot.exam.dao.examPaper; +import com.baobaot.exam.dto.AnswerItem; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Controller +public class ExamAnswerController { + + @Autowired + private makeExamService makeExamService; + + @Autowired + private ExamAnswerService examAnswerService; + + /** + * 答题页面 - 根据试卷UID显示答题界面 + */ + @GetMapping("/{examUid}") + public String answerPage(@PathVariable String examUid, Model model) { + // 获取试卷详情 + Map examDetail = makeExamService.getExamPaperDetail(examUid); + + if (examDetail == null) { + model.addAttribute("error", "试卷不存在"); + return "error"; + } + + model.addAttribute("examUid", examUid); + model.addAttribute("title", examDetail.get("title") != null ? examDetail.get("title") : "未命名试卷"); + model.addAttribute("choiceQuestions", examDetail.get("choiceQuestions")); + model.addAttribute("essayQuestions", examDetail.get("essayQuestions")); + model.addAttribute("choiceCount", examDetail.get("choiceCount")); + model.addAttribute("essayCount", examDetail.get("essayCount")); + model.addAttribute("totalScore", examDetail.get("totalScore")); + + return "answer"; + } + + /** + * 用于接收前端传来的答卷数据的 DTO 内部类 + */ + public static class SubmitRequest { + private String name; + private Map answers; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Map getAnswers() { + return answers; + } + + public void setAnswers(Map answers) { + this.answers = answers; + } + } + + /** + * 提交答案 + */ + @PostMapping("/{examUid}/submit") + @ResponseBody + public Map submitAnswer(@PathVariable String examUid, + @RequestBody SubmitRequest request) { + Map result = new HashMap<>(); + + try { + String name = request.getName(); + Map answers = request.getAnswers(); + + // 验证试卷是否存在 + examPaper paper = makeExamService.getExamPaperByUid(examUid); + if (paper == null) { + result.put("success", false); + result.put("message", "试卷不存在"); + return result; + } + + // 验证姓名 + if (name == null || name.trim().isEmpty()) { + result.put("success", false); + result.put("message", "请输入做题人姓名"); + return result; + } + + // 调用服务提交答案 + String answerUid = examAnswerService.submitAnswer(examUid, name, answers); + + result.put("success", true); + result.put("message", "提交成功"); + result.put("answerUid", answerUid); + + } catch (Exception e) { + result.put("success", false); + result.put("message", e.getMessage()); + } + + return result; + } + + /** + * 查询答题记录详情(根据答题记录UID) + */ + @GetMapping("/answer/{answerUid}") + @ResponseBody + public Map getAnswerDetail(@PathVariable String answerUid) { + Map result = new HashMap<>(); + + try { + examBack back = examAnswerService.getAnswerByUid(answerUid); + if (back == null) { + result.put("success", false); + result.put("message", "答题记录不存在"); + return result; + } + + // 解析答案内容 + List answers = examAnswerService.parseAnswerContent(back.getContent()); + + result.put("success", true); + result.put("uid", back.getUid()); + result.put("examId", back.getExamId()); + result.put("name", back.getName()); + result.put("answers", answers); + result.put("submitTime", back.getId()); // 如果有时间字段可以替换 + + } catch (Exception e) { + result.put("success", false); + result.put("message", e.getMessage()); + } + + return result; + } + + /** + * 查询某试卷的所有答题记录 + */ + @GetMapping("/{examUid}/answers") + @ResponseBody + public Map getAnswersByExam(@PathVariable String examUid) { + Map result = new HashMap<>(); + + try { + // 验证试卷是否存在 + examPaper paper = makeExamService.getExamPaperByUid(examUid); + if (paper == null) { + result.put("success", false); + result.put("message", "试卷不存在"); + return result; + } + + List answers = examAnswerService.getAnswersByExamId(examUid); + + result.put("success", true); + result.put("examUid", examUid); + result.put("examTitle", paper.getTitle()); + result.put("count", answers.size()); + result.put("answers", answers); + + } catch (Exception e) { + result.put("success", false); + result.put("message", e.getMessage()); + } + + return result; + } +} diff --git a/src/main/java/com/baobaot/exam/Controller/ExamController.java b/src/main/java/com/baobaot/exam/Controller/ExamController.java new file mode 100644 index 0000000..07a7d85 --- /dev/null +++ b/src/main/java/com/baobaot/exam/Controller/ExamController.java @@ -0,0 +1,60 @@ +package com.baobaot.exam.Controller; + +import com.baobaot.exam.Service.makeExamService; +import com.baobaot.exam.dao.examPaper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; + +@RestController +@RequestMapping("/exam") +public class ExamController { + + @Autowired + private makeExamService makeExamService; + + /** + * 创建试卷 + */ + @PostMapping("/create") + public ResponseEntity> createExam(@RequestParam(required = false) String title) { + Map result = new HashMap<>(); + try { + examPaper paper = makeExamService.createExamPaper(title); + result.put("success", true); + result.put("uid", paper.getUid()); + result.put("title", paper.getTitle()); + result.put("select", paper.getSelect()); + result.put("content", paper.getContent()); + return ResponseEntity.ok(result); + } catch (Exception e) { + result.put("success", false); + result.put("message", e.getMessage()); + return ResponseEntity.badRequest().body(result); + } + } + + /** + * 根据UID查询试卷 + */ + @GetMapping("/get/{uid}") + public ResponseEntity> getExam(@PathVariable String uid) { + Map result = new HashMap<>(); + examPaper paper = makeExamService.getExamPaperByUid(uid); + if (paper != null) { + result.put("success", true); + result.put("uid", paper.getUid()); + result.put("title", paper.getTitle()); + result.put("select", paper.getSelect()); + result.put("content", paper.getContent()); + return ResponseEntity.ok(result); + } else { + result.put("success", false); + result.put("message", "试卷不存在"); + return ResponseEntity.notFound().build(); + } + } +} diff --git a/src/main/java/com/baobaot/exam/Service/CorrectService.java b/src/main/java/com/baobaot/exam/Service/CorrectService.java new file mode 100644 index 0000000..56cdac6 --- /dev/null +++ b/src/main/java/com/baobaot/exam/Service/CorrectService.java @@ -0,0 +1,42 @@ +package com.baobaot.exam.Service; + +import com.baobaot.exam.dao.examBack; +import com.baobaot.exam.dto.CorrectResult; + +import java.util.List; +import java.util.Map; + +/** + * 批改服务接口 + */ +public interface CorrectService { + + /** + * 搜索答题记录 + * @param name 做题人姓名(可选) + * @param title 试卷标题(可选) + * @return 答题记录列表 + */ + List searchAnswers(String name, String title); + + /** + * 获取所有试卷标题列表(去重) + * @return 试卷标题列表 + */ + List getExamTitleList(); + + /** + * 获取答题详情(用于批改) + * @param answerUid 答题记录UID + * @return 批改详情(包含题目、答案、选择题自动批改结果) + */ + CorrectResult getAnswerDetailForCorrect(String answerUid); + + /** + * 提交批改结果 + * @param answerUid 答题记录UID + * @param essayScores 简答题得分 Map + * @return 总得分 + */ + int submitCorrect(String answerUid, Map essayScores); +} diff --git a/src/main/java/com/baobaot/exam/Service/ExamAnswerService.java b/src/main/java/com/baobaot/exam/Service/ExamAnswerService.java new file mode 100644 index 0000000..98b7e93 --- /dev/null +++ b/src/main/java/com/baobaot/exam/Service/ExamAnswerService.java @@ -0,0 +1,43 @@ +package com.baobaot.exam.Service; + +import com.baobaot.exam.dao.examBack; +import com.baobaot.exam.dto.AnswerItem; + +import java.util.List; +import java.util.Map; + +public interface ExamAnswerService { + + /** + * 提交答案 + * @param examUid 试卷UID + * @param name 做题人姓名 + * @param answers 答案Map (key: q_题目ID, value: 答案) + * @return 答题记录UID + */ + String submitAnswer(String examUid, String name, Map answers); + + /** + * 根据答题记录UID查询答案 + */ + examBack getAnswerByUid(String uid); + + /** + * 根据试卷UID查询所有答题记录 + */ + List getAnswersByExamId(String examId); + + /** + * 解析答题记录的content JSON + * @param content examBack.content 字段的JSON字符串 + * @return 答案项列表 + */ + List parseAnswerContent(String content); + + /** + * 根据答题记录UID获取解析后的答案列表 + * @param uid 答题记录UID + * @return 答案项列表 + */ + List getParsedAnswersByUid(String uid); +} diff --git a/src/main/java/com/baobaot/exam/Service/impl/CorrectServiceImpl.java b/src/main/java/com/baobaot/exam/Service/impl/CorrectServiceImpl.java new file mode 100644 index 0000000..ada3b2b --- /dev/null +++ b/src/main/java/com/baobaot/exam/Service/impl/CorrectServiceImpl.java @@ -0,0 +1,249 @@ +package com.baobaot.exam.Service.impl; + +import com.baobaot.exam.Service.CorrectService; +import com.baobaot.exam.Service.ExamAnswerService; +import com.baobaot.exam.Service.makeExamService; +import com.baobaot.exam.dao.examBack; +import com.baobaot.exam.dao.question; +import com.baobaot.exam.dto.AnswerItem; +import com.baobaot.exam.dto.CorrectDetailItem; +import com.baobaot.exam.dto.CorrectResult; +import com.baobaot.exam.mapper.examBackMapper; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.util.*; + +@Service +public class CorrectServiceImpl implements CorrectService { + + @Autowired + private examBackMapper examBackMapper; + + @Autowired + private ExamAnswerService examAnswerService; + + @Autowired + private makeExamService makeExamService; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public List searchAnswers(String name, String title) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + // 根据姓名搜索(模糊匹配) + if (StringUtils.hasText(name)) { + wrapper.like(examBack::getName, name); + } + + // 根据试卷标题搜索(精确匹配) + if (StringUtils.hasText(title)) { + wrapper.eq(examBack::getTitle, title); + } + + // 按ID降序,最新的在前面 + wrapper.orderByDesc(examBack::getId); + + return examBackMapper.selectList(wrapper); + } + + @Override + public List getExamTitleList() { + // 获取所有不重复的试卷标题(使用Java去重,更可靠) + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.isNotNull(examBack::getTitle); + wrapper.ne(examBack::getTitle, ""); // 排除空字符串 + + List list = examBackMapper.selectList(wrapper); + + // 使用Set去重,然后排序 + Set titleSet = new HashSet<>(); + for (examBack back : list) { + if (StringUtils.hasText(back.getTitle())) { + titleSet.add(back.getTitle()); + } + } + + List titles = new ArrayList<>(titleSet); + Collections.sort(titles); // 按字母顺序排序 + return titles; + } + + @Override + public CorrectResult getAnswerDetailForCorrect(String answerUid) { + // 1. 获取答题记录 + examBack back = examAnswerService.getAnswerByUid(answerUid); + if (back == null) { + throw new RuntimeException("答题记录不存在"); + } + + // 2. 获取试卷详情 + Map examDetail = makeExamService.getExamPaperDetail(back.getExamId()); + if (examDetail == null) { + throw new RuntimeException("试卷不存在"); + } + + // 3. 解析答案(包含得分) + List answerItems = examAnswerService.parseAnswerContent(back.getContent()); + Map answerItemMap = new HashMap<>(); + for (AnswerItem item : answerItems) { + answerItemMap.put(item.getQid(), item); + } + + // 4. 获取题目列表 + @SuppressWarnings("unchecked") + List choiceQuestions = (List) examDetail.get("choiceQuestions"); + @SuppressWarnings("unchecked") + List essayQuestions = (List) examDetail.get("essayQuestions"); + + // 5. 构建批改详情 + List detailItems = new ArrayList<>(); + int choiceScore = 0; + int essayTotalScore = 0; + int exmaid = 1; + + // 处理选择题(自动批改) + for (question q : choiceQuestions) { + AnswerItem answerItem = answerItemMap.get(q.getId()); + String userAnswer = answerItem != null ? answerItem.getAnswer() : ""; + String correctAnswer = q.getRAnswer(); + boolean isCorrect = userAnswer.equalsIgnoreCase(correctAnswer); + int score = isCorrect ? q.getScore() : 0; + choiceScore += score; + + CorrectDetailItem item = CorrectDetailItem.builder() + .exmaid(exmaid++) + .qid(q.getId()) + .type(1) + .title(q.getTitle()) + .score(q.getScore()) + .userAnswer(userAnswer) + .correctAnswer(correctAnswer) + .autoCorrect(true) + .autoScore(score) + .isCorrect(isCorrect) + .build(); + detailItems.add(item); + } + + // 处理简答题(需要人工批改) + int essayScore = 0; + for (question q : essayQuestions) { + AnswerItem answerItem = answerItemMap.get(q.getId()); + String userAnswer = answerItem != null ? answerItem.getAnswer() : ""; + // 从 answerItem 中获取得分(如果已批改) + Integer itemScore = answerItem != null ? answerItem.getScore() : null; + essayTotalScore += q.getScore(); + if (itemScore != null) { + essayScore += itemScore; + } + + CorrectDetailItem item = CorrectDetailItem.builder() + .exmaid(exmaid++) + .qid(q.getId()) + .type(2) + .title(q.getTitle()) + .score(q.getScore()) + .userAnswer(userAnswer) + .correctAnswer(q.getRAnswer()) + .autoCorrect(false) + .autoScore(itemScore != null ? itemScore : 0) + .isCorrect(null) + .build(); + detailItems.add(item); + } + + // 6. 判断是否已批改(exam_back.score字段不为空表示已批改) + boolean isCorrected = back.getScore() != null && !back.getScore().isEmpty(); + int finalScore = isCorrected ? (choiceScore + essayScore) : choiceScore; + + return CorrectResult.builder() + .answerUid(answerUid) + .studentName(back.getName()) + .examUid(back.getExamId()) + .examTitle((String) examDetail.get("title")) + .choiceScore(choiceScore) + .essayTotalScore(essayTotalScore) + .totalScore(choiceScore + essayTotalScore) + .isCorrected(isCorrected) + .finalScore(finalScore) + .questions(detailItems) + .build(); + } + + @Override + public int submitCorrect(String answerUid, Map essayScores) { + // 1. 获取答题记录 + examBack back = examAnswerService.getAnswerByUid(answerUid); + if (back == null) { + throw new RuntimeException("答题记录不存在"); + } + + // 2. 获取试卷详情以计算选择题得分 + Map examDetail = makeExamService.getExamPaperDetail(back.getExamId()); + if (examDetail == null) { + throw new RuntimeException("试卷不存在"); + } + + // 3. 解析答案 + List answerItems = examAnswerService.parseAnswerContent(back.getContent()); + Map answerItemMap = new HashMap<>(); + for (AnswerItem item : answerItems) { + answerItemMap.put(item.getQid(), item); + } + + // 4. 计算选择题得分并设置简答题得分 + @SuppressWarnings("unchecked") + List choiceQuestions = (List) examDetail.get("choiceQuestions"); + @SuppressWarnings("unchecked") + List essayQuestions = (List) examDetail.get("essayQuestions"); + + int choiceScore = 0; + int essayScore = 0; + + // 设置选择题得分 + for (question q : choiceQuestions) { + AnswerItem item = answerItemMap.get(q.getId()); + if (item == null) continue; + + String userAnswer = item.getAnswer(); + if (userAnswer != null && userAnswer.equalsIgnoreCase(q.getRAnswer())) { + item.setScore(q.getScore()); + choiceScore += q.getScore(); + } else { + item.setScore(0); + } + } + + // 设置简答题得分 + for (question q : essayQuestions) { + String key = "q_" + q.getId(); + Integer score = essayScores.get(key); + if (score != null) { + AnswerItem item = answerItemMap.get(q.getId()); + if (item != null) { + item.setScore(score); + essayScore += score; + } + } + } + + // 5. 计算总分 + int totalScore = choiceScore + essayScore; + + // 6. 更新答题记录(更新content和score) + try { + back.setContent(objectMapper.writeValueAsString(answerItems)); + } catch (Exception e) { + throw new RuntimeException("保存答案得分失败: " + e.getMessage()); + } + back.setScore(String.valueOf(totalScore)); + examBackMapper.updateById(back); + + return totalScore; + } +} diff --git a/src/main/java/com/baobaot/exam/Service/impl/ExamAnswerServiceImpl.java b/src/main/java/com/baobaot/exam/Service/impl/ExamAnswerServiceImpl.java new file mode 100644 index 0000000..aa5d228 --- /dev/null +++ b/src/main/java/com/baobaot/exam/Service/impl/ExamAnswerServiceImpl.java @@ -0,0 +1,138 @@ +package com.baobaot.exam.Service.impl; + +import com.baobaot.exam.Service.ExamAnswerService; +import com.baobaot.exam.Service.makeExamService; +import com.baobaot.exam.dao.examBack; +import com.baobaot.exam.dao.examPaper; +import com.baobaot.exam.dao.question; +import com.baobaot.exam.dto.AnswerItem; +import com.baobaot.exam.mapper.examBackMapper; +import com.baobaot.exam.util.UidGenerator; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +public class ExamAnswerServiceImpl implements ExamAnswerService { + + @Autowired + private makeExamService makeExamService; + + @Autowired + private examBackMapper examBackMapper; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public String submitAnswer(String examUid, String name, Map answers) { + try { + // 获取试卷题目 + Map examDetail = makeExamService.getExamPaperDetail(examUid); + if (examDetail == null) { + throw new RuntimeException("试卷不存在"); + } + + @SuppressWarnings("unchecked") + List choiceQuestions = (List) examDetail.get("choiceQuestions"); + @SuppressWarnings("unchecked") + List essayQuestions = (List) examDetail.get("essayQuestions"); + + // 构建答案JSON,格式: [{"exmaid": 1, "qid": 3, "answer": "A"}] + List answerList = new ArrayList<>(); + + // 处理选择题答案 + int exmaid = 1; // 试卷中的题目序号,从1开始 + for (question q : choiceQuestions) { + AnswerItem item = AnswerItem.builder() + .exmaid(exmaid++) + .qid(q.getId()) + .answer(answers.getOrDefault("q_" + q.getId(), "")) + .build(); + answerList.add(item); + } + + // 处理简答题答案 + for (question q : essayQuestions) { + AnswerItem item = AnswerItem.builder() + .exmaid(exmaid++) + .qid(q.getId()) + .answer(answers.getOrDefault("q_" + q.getId(), "")) + .build(); + answerList.add(item); + } + + // 生成唯一UID + String uid = UidGenerator.generateUniqueUid(this::isAnswerUidExists); + + // 获取试卷标题 + examPaper paper = makeExamService.getExamPaperByUid(examUid); + String examTitle = paper != null ? paper.getTitle() : ""; + + // 保存到数据库 + examBack back = examBack.builder() + .uid(uid) + .examId(examUid) + .title(examTitle) + .name(name.trim()) + .content(objectMapper.writeValueAsString(answerList)) + .build(); + + examBackMapper.insert(back); + + return uid; + } catch (Exception e) { + throw new RuntimeException("提交失败: " + e.getMessage(), e); + } + } + + @Override + public examBack getAnswerByUid(String uid) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(examBack::getUid, uid); + return examBackMapper.selectOne(wrapper); + } + + @Override + public List getAnswersByExamId(String examId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(examBack::getExamId, examId); + return examBackMapper.selectList(wrapper); + } + + /** + * 检查答题记录UID是否已存在 + */ + private boolean isAnswerUidExists(String uid) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(examBack::getUid, uid); + return examBackMapper.selectCount(wrapper) > 0; + } + + @Override + public List parseAnswerContent(String content) { + try { + if (content == null || content.trim().isEmpty()) { + return new ArrayList<>(); + } + return objectMapper.readValue(content, new TypeReference>() {}); + } catch (Exception e) { + throw new RuntimeException("解析答案内容失败: " + e.getMessage(), e); + } + } + + @Override + public List getParsedAnswersByUid(String uid) { + examBack back = getAnswerByUid(uid); + if (back == null) { + return new ArrayList<>(); + } + return parseAnswerContent(back.getContent()); + } +} diff --git a/src/main/java/com/baobaot/exam/dto/AnswerItem.java b/src/main/java/com/baobaot/exam/dto/AnswerItem.java new file mode 100644 index 0000000..440f812 --- /dev/null +++ b/src/main/java/com/baobaot/exam/dto/AnswerItem.java @@ -0,0 +1,37 @@ +package com.baobaot.exam.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 答题记录中的单个答案项 + * 对应 exam_back.content JSON 数组中的每个元素 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AnswerItem { + + /** + * 试卷中的题目序号(第几题) + */ + private Integer exmaid; + + /** + * 题目在题库中的ID + */ + private Integer qid; + + /** + * 用户提交的答案 + */ + private String answer; + + /** + * 该题得分(批改后填充) + */ + private Integer score; +} diff --git a/src/main/java/com/baobaot/exam/dto/CorrectDetailItem.java b/src/main/java/com/baobaot/exam/dto/CorrectDetailItem.java new file mode 100644 index 0000000..9826d4d --- /dev/null +++ b/src/main/java/com/baobaot/exam/dto/CorrectDetailItem.java @@ -0,0 +1,66 @@ +package com.baobaot.exam.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 批改详情中的单个题目项 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CorrectDetailItem { + + /** + * 试卷中的题目序号 + */ + private Integer exmaid; + + /** + * 题目在题库中的ID + */ + private Integer qid; + + /** + * 题目类型: 1-选择题, 2-简答题 + */ + private Integer type; + + /** + * 题目内容 + */ + private String title; + + /** + * 题目分值 + */ + private Integer score; + + /** + * 用户提交的答案 + */ + private String userAnswer; + + /** + * 正确答案(选择题)/参考答案(简答题) + */ + private String correctAnswer; + + /** + * 是否自动批改(选择题为true) + */ + private Boolean autoCorrect; + + /** + * 自动批改得分(选择题) + */ + private Integer autoScore; + + /** + * 是否正确(选择题) + */ + private Boolean isCorrect; +} diff --git a/src/main/java/com/baobaot/exam/dto/CorrectResult.java b/src/main/java/com/baobaot/exam/dto/CorrectResult.java new file mode 100644 index 0000000..3dc19ec --- /dev/null +++ b/src/main/java/com/baobaot/exam/dto/CorrectResult.java @@ -0,0 +1,69 @@ +package com.baobaot.exam.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 批改结果DTO + * 用于返回批改页面的完整数据 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CorrectResult { + + /** + * 答题记录UID + */ + private String answerUid; + + /** + * 做题人姓名 + */ + private String studentName; + + /** + * 试卷UID + */ + private String examUid; + + /** + * 试卷标题 + */ + private String examTitle; + + /** + * 选择题自动批改得分 + */ + private Integer choiceScore; + + /** + * 简答题总分值 + */ + private Integer essayTotalScore; + + /** + * 试卷总分 + */ + private Integer totalScore; + + /** + * 是否已批改 + */ + private Boolean isCorrected; + + /** + * 最终得分(已批改时显示) + */ + private Integer finalScore; + + /** + * 题目详情列表 + */ + private List questions; +} diff --git a/src/main/java/com/baobaot/exam/util/UidGenerator.java b/src/main/java/com/baobaot/exam/util/UidGenerator.java new file mode 100644 index 0000000..22fd45d --- /dev/null +++ b/src/main/java/com/baobaot/exam/util/UidGenerator.java @@ -0,0 +1,37 @@ +package com.baobaot.exam.util; + +import java.security.SecureRandom; + +/** + * 生成16位随机UID工具类 + * 包含A-Z, a-z, 0-9字符 + */ +public class UidGenerator { + + private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + private static final int UID_LENGTH = 16; + private static final SecureRandom random = new SecureRandom(); + + /** + * 生成16位随机UID + */ + public static String generateUid() { + StringBuilder sb = new StringBuilder(UID_LENGTH); + for (int i = 0; i < UID_LENGTH; i++) { + int index = random.nextInt(CHARACTERS.length()); + sb.append(CHARACTERS.charAt(index)); + } + return sb.toString(); + } + + /** + * 生成唯一的UID(需传入检查唯一性的接口) + */ + public static String generateUniqueUid(java.util.function.Function existsChecker) { + String uid; + do { + uid = generateUid(); + } while (existsChecker.apply(uid)); + return uid; + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..f58ac92 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,9 @@ +spring: + application: + name: exam + datasource: + url: jdbc:mysql://localhost:3306/exam +# url: jdbc:mysql://192.168.1.56:3306/exam?useSSL=true&requireSSL=true&serverTimezone=UTC + username: root + password: 521707 + driver-class-name: com.mysql.cj.jdbc.Driver \ No newline at end of file diff --git a/src/main/resources/templates/admin.html b/src/main/resources/templates/admin.html new file mode 100644 index 0000000..368791f --- /dev/null +++ b/src/main/resources/templates/admin.html @@ -0,0 +1,388 @@ + + + + + 题库管理系统 + + + +
+

题库管理系统

+ + +
+

添加题目

+
+ + +
+ + +
+
+ +
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+ +
+
+ + +
+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+ + +
+ + +
+

+ 题目列表 + + +

+ +
+ + + +

暂无题目数据

+
+ + + + + + + + + + + + + + + + + + + + + + + + +
ID题目类型选项/学生答案正确答案分值操作
+ 选择题 + 简答题 + +
+
+
+
+ +
+
+
+ + - + +
+ + +
+
+
+
+ + + + diff --git a/src/main/resources/templates/answer.html b/src/main/resources/templates/answer.html new file mode 100644 index 0000000..bf54f96 --- /dev/null +++ b/src/main/resources/templates/answer.html @@ -0,0 +1,417 @@ + + + + + + 在线答题 + + + +
+ +
+ + +
+ +
+

试卷标题

+
+
+
选择题
+
0道
+
+
+
简答题
+
0道
+
+
+
总分
+
100分
+
+
+
+ + +
+
+ + +
+ + + +
+
+ 1 + 题目内容 + 3分 + 选择题 +
+
+ + + + +
+
+ + +
+
+ 1 + 题目内容 + 5分 + 简答题 +
+
+ +
+
+ + +
+ +
+
+
+
+ + + + + + + diff --git a/src/main/resources/templates/correct.html b/src/main/resources/templates/correct.html new file mode 100644 index 0000000..8590c5a --- /dev/null +++ b/src/main/resources/templates/correct.html @@ -0,0 +1,705 @@ + + + + + + 批改试卷 + + + +
+ ← 返回管理后台 +

批改试卷

+ + +
+

搜索答题记录

+
+
+ + +
+
+ + +
+ +
+ + + + +
+ + + + + + +
+ + + + + + + diff --git a/src/main/resources/templates/listexam.html b/src/main/resources/templates/listexam.html new file mode 100644 index 0000000..12bbe22 --- /dev/null +++ b/src/main/resources/templates/listexam.html @@ -0,0 +1,389 @@ + + + + + 查看试卷列表 + + + +
+

试卷管理与浏览

+ + + +
+

试卷列表

+ + +
+
+ + +
+ + 重置 +
+ +
+ 暂无试卷记录,点击上方"创建试卷"生成一份吧 +
+ + + + + + + + + + + + + + + + + + +
IDUID试卷标题操作
+
+ + 前往答题 +
+
+
+
+ + + + + + + diff --git a/src/main/resources/templates/mkexam.html b/src/main/resources/templates/mkexam.html new file mode 100644 index 0000000..bd0dcca --- /dev/null +++ b/src/main/resources/templates/mkexam.html @@ -0,0 +1,536 @@ + + + + + 创建试卷 + + + +
+

创建试卷

+ + + + +
+

生成新试卷

+
+

系统将自动从题库中随机选择题目的7:3比例组合(选择题:简答题),总分100分

+
+
+ + +
+
+ +
+
+
+
+ + +
+
+

正在生成试卷...

+
+ + + + + + +
+ + + + diff --git a/src/main/resources/templates/print.html b/src/main/resources/templates/print.html new file mode 100644 index 0000000..2974226 --- /dev/null +++ b/src/main/resources/templates/print.html @@ -0,0 +1,300 @@ + + + + + + 试卷答案 + + + + + + + +
+ + +
+ +
试卷标题
+ + +
+
+ 姓名: + +
+
+ 答题记录UID: + +
+
+ 日期: + +
+
+ + +
+
+
选择题得分
+
+
+
+
简答题得分
+
+
+
+
总得分
+
+
+
+ + +
+
+
+ 第1题 + 选择题 + 5分 +
+
题目内容
+ +
+ +
+
+ 答案: + + +
+
+ 正确答案: + +
+
+ 得分: + + ✓ 正确 + ✗ 错误 +
+
+ + +
+
+ 答案: + +
+
+ 得分: + +
+
+
+
+
+ + +
+ +