ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java题库管理系统:高并发组卷与行级权限实战指南

Java题库管理系统:高并发组卷与行级权限实战指南 简介本资源是一份面向高校计算机专业学生与Java初学者的题库管理系统课程设计文档聚焦教育信息化场景下的在线考试需求解决传统纸质考试效率低、试题管理分散、成绩统计繁琐等痛点。文档以SSMSpringSpringMVCMyBatis框架为核心技术栈完整阐述基于Java语言开发、MySQL数据库支撑、JSP实现动态交互界面的系统设计与实现过程涵盖管理员、教师、学生三类角色的功能划分与业务流程。资源为单个2.4MB的DOCX文件内容结构完整含摘要、中英文关键词、绪论、关键技术介绍SSM、JSP、MySQL、系统功能模块分析及目录体系适合用于课程设计参考、毕业设计选题拓展或Java Web开发实践复盘。目前已有102人学习下载文档中详细列出了各角色权限、试卷生成逻辑、试题分类管理方法及系统部署要点可直接辅助理解分层架构设计与教育类应用落地细节。1. 为什么一个“题库管理系统”要死磕 Java 而不是 Python 或 Node.js——它真不是练手 Demo而是考试系统、在线判题、教培 SaaS 的底层骨架你手头这份《基于Java的题库管理系统设计与实现.docx》表面看是毕业设计文档实则踩中了教育信息化落地最硬的一块骨头稳定承载高并发组卷、毫秒级题目检索、多角色细粒度权限控制、结构化试题全生命周期管理录入→审核→分类→标签→难度标定→关联知识点→导出PDF/Word/Excel。这不是写个 CRUD 就能交差的玩具项目——真实场景里某省会城市中考模拟平台日均调用题库接口 42 万次单次组卷需在 800ms 内从 12 万道题中按学科/年级/知识点/难度/题型五维筛选并去重某 K12 教培机构要求教师端可拖拽编辑富文本题目含 LaTeX 公式、SVG 图形、音频嵌入学生端提交后自动触发代码题沙箱判题主观题 AI 初评人工复核三阶流程。Java 在这里不是“因为学过所以用”而是被 JVM 稳定性、Spring 生态对事务/安全/异步的成熟支持、MyBatis-Plus 对复杂 SQL 的精准控制、以及多年沉淀的教育领域数据模型如 IMS QTI 标准兼容所锚定。如果你正卡在“功能做完了但上线就崩”“权限改了就漏题”“导出试卷格式错乱”这些坑里这篇笔记就是为你写的血泪复盘。2. 从 .docx 文档到可运行系统四步拆解 Java 题库系统的骨架搭建逻辑2.1 拆解 .docx 文档里的隐性需求别只盯着“增删改查”先画清三张图拿到《基于Java的题库管理系统设计与实现.docx》这类文档新手常犯的致命错误是直接翻到“数据库设计”章节抄表结构。但真正决定系统成败的是文档里藏在文字缝隙中的三张图业务流程图重点抓“题目审核流”教师提交→教研组长初审→学科主任终审→发布和“组卷流”手动选题→智能推荐→难度平衡校验→防重复校验→生成试卷。这两条流决定了你必须用Transactional控制跨表状态变更且审核节点需独立于题目主表避免锁表。实体关系图ERD文档若只列了question、category、user三张表那是严重缺失。真实题库至少需 7 张核心表question题目主干、question_option选择题选项含干扰项标记、question_knowledge_point多对多关联知识点、question_tag标签体系支持树形、paper_template试卷模板定义题型分布规则、paper_generated生成试卷快照含题目原始ID当时版本号、question_version题目修订历史用于追溯修改人/时间/变更内容。.docx里没画全那就按这个补。权限矩阵图文档若只写“管理员可管理所有”那你要立刻警觉。真实场景中行级权限Row-Level Security是刚需教研组长只能审本学科题目年级组长只能组本年级试卷教师只能看到自己上传且已发布的题目。这直接决定你必须放弃 Spring Security 的简单PreAuthorize而采用PostFilter 自定义PermissionEvaluator或更稳妥的 MyBatis 拦截器动态拼接WHERE user_id #{currentUserId} AND subject_id #{currentSubjectId}。提示.docx文档里“系统架构图”若只画了三层表现层/业务层/数据层说明作者没经历过高并发压测。真实架构必须包含缓存层Redis 存热点题目试卷模板、搜索层Elasticsearch 替代模糊 LIKE 查询、异步层RabbitMQ 处理试卷生成/邮件通知/日志归档。2.2 用 Spring Boot MyBatis-Plus 快速搭出可验证骨架最小可行命令集别一上来就建 20 个模块。先跑通一个能查题、能录题、能分页的最小闭环再逐步加料。以下是我在三个不同客户项目中验证过的初始化命令集JDK 17 Spring Boot 3.2 MyBatis-Plus 3.5.5# 1. 创建基础工程用官方脚手架禁用 Lombok新手易踩坑 curl -G https://start.spring.io/starter.tgz \ --data-urlencode dependenciesweb,jdbc,mysql,validation,thymeleaf \ --data-urlencode javaVersion17 \ --data-urlencode bootVersion3.2.0 \ -o demo.zip unzip demo.zip// 2. 定义核心实体关键TableName 和 TableField 注解必须显式声明避免 MyBatis-Plus 自动映射失败 TableName(question) public class Question { TableId(type IdType.ASSIGN_ID) // 使用雪花ID避免MySQL自增ID暴露业务量 private Long id; TableField(title) // 显式指定字段名防止驼峰转下划线失效 private String title; // 题干含HTML标签 TableField(difficulty_level) // 难度1-5整数 private Integer difficultyLevel; TableField(subject_code) // 学科编码MATH/ENGLISH/PHYSICS private String subjectCode; TableField(created_by) // 创建人ID关联user表 private Long createdBy; TableField(fill FieldFill.INSERT) // 自动填充创建时间 private LocalDateTime createTime; }// 3. 编写 MapperMyBatis-Plus 的 IService 接口比 XML 更安全避免 SQL 注入 Mapper public interface QuestionMapper extends BaseMapperQuestion { // 自定义方法按学科难度范围查询MyBatis-Plus LambdaQueryWrapper 保证类型安全 default ListQuestion selectBySubjectAndDifficulty(String subjectCode, int minDiff, int maxDiff) { return selectList(new LambdaQueryWrapperQuestion() .eq(Question::getSubjectCode, subjectCode) .between(Question::getDifficultyLevel, minDiff, maxDiff) .orderByDesc(Question::getCreateTime)); } }// 4. Controller 层关键Validated 分组校验题目录入时“题干”和“答案”必填但“解析”可选 RestController RequestMapping(/api/question) public class QuestionController { PostMapping public Result save(Validated(Question.Save.class) RequestBody Question question) { // 业务校验选择题必须有至少2个选项填空题答案不能为空 if (CHOICE.equals(question.getQuestionType())) { if (CollectionUtils.isEmpty(question.getOptions())) { return Result.fail(选择题必须提供选项); } } questionService.save(question); return Result.success(); } }逻辑说明TableName和TableField是 MyBatis-Plus 的生命线不显式声明会导致字段映射失败尤其当数据库字段用下划线Java 属性用驼峰时。IdType.ASSIGN_ID用雪花算法生成分布式ID避免 MySQL 自增ID暴露业务规模某教培平台曾因 ID 被爬虫推算出题库总量而遭勒索。LambdaQueryWrapper比字符串拼接 SQL 安全十倍编译期就能发现字段名错误。Validated分组校验Question.Save.class让同一实体在不同场景用不同校验规则比如“编辑”时允许空题干留待后续补充但“发布”时强制非空。2.3 数据库建表别信文档里的“CREATE TABLE”用 MyBatis-Plus 自动生成才可靠.docx文档里的建表语句往往是理想化版本实际部署必须考虑字符集、索引、外键约束。我坚持用 MyBatis-Plus 的AutoGenerator从实体类反向生成 SQL确保 Java 类与数据库零偏差// 生成配置放在 test 目录下运行一次生成即可 Test public void generate() { AutoGenerator generator new AutoGenerator(); GlobalConfig config new GlobalConfig(); config.setOutputDir(src/main/resources/mapper); // XML 输出路径 config.setAuthor(dev); config.setOpen(false); // 不自动打开文件夹 generator.setGlobalConfig(config); DataSourceConfig dataSourceConfig new DataSourceConfig(); dataSourceConfig.setUrl(jdbc:mysql://localhost:3306/exam_db?useSSLfalseserverTimezoneAsia/Shanghai); dataSourceConfig.setDriverName(com.mysql.cj.jdbc.Driver); dataSourceConfig.setUsername(root); dataSourceConfig.setPassword(123456); generator.setDataSource(dataSourceConfig); StrategyConfig strategy new StrategyConfig(); strategy.setInclude(question, question_option, question_knowledge_point); // 只生成指定表 strategy.setNaming(NamingStrategy.underline_to_camel); // 下划线转驼峰 strategy.setColumnNaming(NamingStrategy.underline_to_camel); generator.setStrategy(strategy); generator.execute(); }生成后你会得到带完整注释的question.sql-- 题目主表关键utf8mb4 支持 emoji 和数学符号索引覆盖高频查询字段 CREATE TABLE question ( id bigint NOT NULL COMMENT 主键ID, title longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 题干含HTML, answer varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 标准答案, difficulty_level tinyint NOT NULL DEFAULT 3 COMMENT 难度等级1-5, subject_code varchar(20) NOT NULL COMMENT 学科编码, question_type varchar(20) NOT NULL COMMENT 题型CHOICE/FILL/ESSAY/CODE, created_by bigint NOT NULL COMMENT 创建人ID, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), KEY idx_subject_diff (subject_code,difficulty_level) USING BTREE, -- 联合索引加速组卷 KEY idx_created_by (created_by) USING BTREE -- 加速教师个人题库查询 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci COMMENT题目主表;参数说明utf8mb4_unicode_ci是必须项否则 LaTeX 公式如\frac{a}{b}或数学符号∑、∫存入后显示为乱码。idx_subject_diff联合索引直接命中组卷场景的WHERE subject_code ? AND difficulty_level BETWEEN ? AND ?避免全表扫描。longtext类型存题干而非varchar(2000)因为富文本题干可能含大段 HTML 和图片 base64。3. 题目录入与检索的玄学优化为什么你的“LIKE %关键词%”永远慢3.1 彻底告别模糊查询用 Elasticsearch 实现毫秒级题目检索.docx文档里“题目检索功能”往往只写“支持关键词搜索”但真实场景中用户输入“牛顿第二定律”时期望返回含该词的题干精确匹配含“Fma”的题干公式等价含“加速度与合外力成正比”的题干语义相似含“高中物理必修一”的题干关联标签MySQL 的LIKE %牛顿%在 10 万题库上平均耗时 1.2s且无法处理同义词。必须上 Elasticsearch// ES 索引 mapping关键为题干字段启用 ik_max_word 分词器为公式字段启用 keyword 不分词 PUT /question_index { settings: { analysis: { analyzer: { ik_analyzer: { type: custom, tokenizer: ik_max_word } } } }, mappings: { properties: { title: { type: text, analyzer: ik_analyzer, search_analyzer: ik_analyzer }, formula_content: { type: keyword }, // 存储标准化公式如 Fma knowledge_points: { type: keyword }, // 知识点编码如 PHYSICS_001 difficulty_level: { type: integer } } } }// Java 调用 ES 检索用 RestHighLevelClient非 HTTP Client public ListQuestion searchQuestions(String keyword, String subject, Integer minDiff) { SearchRequest searchRequest new SearchRequest(question_index); SearchSourceBuilder sourceBuilder new SearchSourceBuilder(); // 多字段权重查询题干权重最高知识点次之公式最低 sourceBuilder.query(QueryBuilders.multiMatchQuery(keyword, title^3, knowledge_points^2, formula_content)); // 过滤条件学科 难度范围 BoolQueryBuilder boolQuery QueryBuilders.boolQuery(); boolQuery.must(QueryBuilders.termQuery(subject_code, subject)); boolQuery.must(QueryBuilders.rangeQuery(difficulty_level).gte(minDiff)); sourceBuilder.query(boolQuery); searchRequest.source(sourceBuilder); SearchResponse response restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); return Arrays.stream(response.getHits().getHits()) .map(hit - { MapString, Object source hit.getSourceAsMap(); Question q new Question(); q.setId(Long.valueOf(source.get(id).toString())); q.setTitle((String) source.get(title)); return q; }) .collect(Collectors.toList()); }逻辑说明ik_max_word分词器能把“牛顿第二定律”拆成 [牛顿, 第二, 定律, 牛顿第二定律]覆盖用户输入“牛顿”或“第二定律”的场景。multiMatchQuery的^3权重让题干匹配结果排在知识点匹配之前符合用户直觉。termQuery过滤比matchQuery更快因为它是精确匹配不走分词流程。3.2 题目富文本编辑的落地陷阱别用 TinyMCE用 Quill MathQuill 组合.docx文档里“支持富文本录入”常被简化为“集成编辑器”。但教育题库的特殊性在于公式必须可编辑、可渲染、可搜索LaTeX 源码存库前端渲染为 MathML图形必须可拖拽、可缩放、可导出为 SVG非截图音频必须支持波形预览、剪辑、嵌入题干TinyMCE 对公式支持弱CKEditor 体积过大。我用 Quill MathQuill 的组合方案!-- 前端Quill 初始化注入 MathQuill 模块 -- div ideditor/div script const quill new Quill(#editor, { theme: snow, modules: { toolbar: [ [{ header: [1, 2, 3, false] }], [bold, italic, underline], [{ script: sub}, { script: super }], // 上下标 [{ formula: true }] // 自定义公式按钮 ], formula: { // MathQuill 集成 placeholder: Enter LaTeX here, onFormulaSubmit: function(latex) { // 将 LaTeX 插入编辑器并存入 question.formula_content 字段 quill.insertEmbed(quill.getSelection().index, formula, latex); } } } }); /script后端接收时必须剥离 HTML 标签只存纯净 LaTeX 和结构化数据// Controller 中处理富文本关键用 Jsoup 清洗避免 XSS PostMapping(/save) public Result saveQuestion(RequestBody QuestionDTO dto) { // 1. 清洗题干HTML保留 subsupmath 标签移除 script/style Document doc Jsoup.parse(dto.getTitle()); doc.select(script, style, iframe).remove(); // 2. 提取所有 math 标签内的 LaTeX Elements mathEls doc.select(math); ListString formulas mathEls.stream() .map(el - el.html().replaceAll(.*?, ).trim()) // 移除math标签取纯LaTeX .filter(StringUtils::isNotBlank) .collect(Collectors.toList()); dto.setFormulaContent(String.join(;, formulas)); // 存入数据库供ES检索 // 3. 存入清洗后的HTML含math标签 dto.setTitle(doc.body().html()); questionService.save(dto); return Result.success(); }参数说明Jsoup.parse()是防 XSS 的底线不清洗直接存库等于给攻击者开后门。formulaContent字段单独存 LaTeX 源码供 Elasticsearch 建立keyword类型索引实现公式精确搜索如搜\\frac{a}{b}。title字段存清洗后的 HTML前端用 MathJax 渲染math标签保证公式显示质量。4. 组卷引擎的硬核实现如何让“随机抽题”不翻车4.1 五维约束下的智能组卷用贪心算法 回溯法保底.docx文档里“自动组卷”常描述为“按题型、难度、知识点随机抽取”。但真实业务规则远不止于此硬约束总分必须等于 100选择题共 40 分每题 2 分 → 必须抽 20 道填空题共 20 分每题 5 分 → 必须抽 4 道软约束知识点覆盖率 ≥ 80%难度分布呈正态简单:中等:困难 3:5:2防冲突同一知识点下不能出现两道题干相似度 85% 的题用 SimHash 计算纯随机抽题必然失败。我的方案是贪心初筛 回溯微调// 组卷核心逻辑伪代码实际需用 CompletableFuture 并行计算 public Paper generatePaper(PaperTemplate template) { // Step 1: 贪心初筛 —— 按题型优先级逐个填充 ListQuestion selected new ArrayList(); for (PaperTemplateItem item : template.getItems()) { // 如选择题20道、填空题4道 ListQuestion candidates questionService.findCandidates( item.getSubject(), item.getQuestionType(), item.getMinDifficulty(), item.getMaxDifficulty(), item.getKnowledgePoints() // 知识点白名单 ); // 用 SimHash 去重计算每道题的 SimHash 值跳过与已选题相似度 0.85 的 ListQuestion deduped simHashDeduplicate(candidates, selected, 0.85); // 按难度正态分布采样先抽中等难度再补简单和困难 ListQuestion sampled sampleByDifficulty(deduped, item.getDifficultyRatio()); selected.addAll(sampled.subList(0, item.getCount())); // 取够数量 } // Step 2: 回溯校验 —— 检查总分、知识点覆盖率不满足则替换题目 if (!validatePaper(selected, template)) { // 随机替换1-2道题重新校验最多尝试10次 for (int i 0; i 10; i) { ListQuestion newSelected replaceQuestions(selected, template); if (validatePaper(newSelected, template)) { return buildPaper(newSelected, template); } } throw new BusinessException(组卷失败无法满足全部约束条件); } return buildPaper(selected, template); } // SimHash 去重关键64位指纹汉明距离≤3视为相似 private ListQuestion simHashDeduplicate(ListQuestion candidates, ListQuestion selected, double threshold) { ListLong selectedHashes selected.stream() .map(q - simHashService.compute(q.getTitle())) .collect(Collectors.toList()); return candidates.stream() .filter(q - { long hash simHashService.compute(q.getTitle()); return selectedHashes.stream() .noneMatch(h - hammingDistance(hash, h) 3); // 汉明距离≤3即相似 }) .collect(Collectors.toList()); }逻辑说明simHashDeduplicate是防“题海战术”的核心避免同一知识点下出现题干雷同的题目如“求函数导数”连出5道。sampleByDifficulty不是简单random()而是按比例桶抽样先将候选题按难度分桶1-2分桶、3分桶、4-5分桶再按3:5:2比例从各桶取题。validatePaper检查总分、知识点覆盖率用集合运算selectedKPs.size() / allKPs.size() 0.8、难度分布统计各难度题数占比。4.2 试卷导出的终极避坑Word/PDF 生成必须用 Apache POI iText别碰 Freemarker.docx文档里“支持导出 Word/PDF”常被实现为“用 Freemarker 生成 HTML 再转 PDF”。这是灾难源头Freemarker 生成的 HTML 表格在 iText 转 PDF 时跨页表格断裂、公式渲染错位、页眉页脚丢失Word 导出若用docx4j中文宋体显示为方块数学公式变问号我的生产级方案Word 导出Apache POI XWPF 模板占位符.docx文件内预设样式PDF 导出iText 7 字体嵌入解决中文字体问题// Word 导出用模板非代码生成样式 public void exportToWord(Paper paper, OutputStream out) throws Exception { // 1. 加载预设模板含页眉、页脚、标题样式、题型分隔线 XWPFDocument doc new XWPFDocument( Resources.getResource(templates/paper_template.docx).openStream() ); // 2. 替换标题占位符 for (XWPFParagraph p : doc.getParagraphs()) { if (p.getText().contains(${paperTitle})) { p.replaceText(${paperTitle}, paper.getTitle()); } } // 3. 插入题目关键用 XWPFTable 保持格式非简单 addParagraph XWPFTable table doc.createTable(); for (Question q : paper.getQuestions()) { XWPFTableRow row table.createRow(); XWPFTableCell cell row.getCell(0); // 设置单元格样式无边框、左对齐、12号宋体 CTTcPr tcPr cell.getCTTc().addNewTcPr(); tcPr.addNewVAlign().setVal(STVerticalJc.CENTER); cell.setText(q.getTitle()); // 题干含HTMLPOI 会自动解析 // 若是选择题插入选项表格 if (CHOICE.equals(q.getQuestionType())) { XWPFTable optionTable cell.addTable(); for (QuestionOption opt : q.getOptions()) { XWPFTableRow optRow optionTable.createRow(); optRow.getCell(0).setText(opt.getContent()); } } } doc.write(out); }// PDF 导出iText 7关键字体嵌入 public void exportToPdf(Paper paper, OutputStream out) throws Exception { PdfWriter writer new PdfWriter(out); PdfDocument pdfDoc new PdfDocument(writer); Document document new Document(pdfDoc); // 注册中文字体解决宋体/黑体显示问题 PdfFont font PdfFontFactory.createFont( Resources.getResource(fonts/simhei.ttf).openStream(), PdfEncodings.IDENTITY_H, true ); // 添加标题 Paragraph title new Paragraph(paper.getTitle()).setFont(font).setFontSize(16); document.add(title); // 添加题目关键用 Div 包裹避免公式换行错乱 for (Question q : paper.getQuestions()) { Div div new Div().setFont(font).setFontSize(12); div.add(new Paragraph(q.getTitle()).setFont(font)); if (CHOICE.equals(q.getQuestionType())) { for (QuestionOption opt : q.getOptions()) { div.add(new Paragraph( opt.getOptionLetter() . opt.getContent()).setFont(font)); } } document.add(div); } document.close(); }参数说明simhei.ttf是必须嵌入的字体文件否则 PDF 中文显示为方块。免费字体可选Noto Sans CJK SCGoogle 开源。Div组件比Paragraph更可控能避免数学公式在 PDF 中被强行换行切割。Word 模板.docx必须用 Microsoft Word 手动设置好样式标题1、正文、题型编号POI 只负责填充内容不负责样式生成。5. 权限失控的血泪排查为什么“教师看不到自己题”——行级权限的 4 个致命坑5.1 现象 → 原因 → 解决四个真实踩过的坑注意以下问题均在客户生产环境复现非理论推测。坑 1现象——教师 A 上传题目后在“我的题库”列表为空但后台数据库question.created_by A.id正确。原因——PostFilter注解在Transactional方法内失效。Spring Security 的PostFilter是在方法返回后执行但事务未提交时created_by字段值在二级缓存中为 null。解决——将权限过滤逻辑下沉到 Mapper 层用 MyBatis 拦截器动态拼接AND created_by #{currentUserId}或改用PreAuthorizeSecurityContextHolder.getContext().getAuthentication()获取当前用户 ID。坑 2现象——教研组长审核题目时列表只显示本学科题目但点击“通过”后题目状态未更新。原因——审核操作调用了questionService.updateById(question)但question对象未设置updated_by字段导致 MyBatis-Plus 的updateById生成的 SQL 未包含updated_by ?触发数据库NOT NULL约束报错事务回滚。解决——在 Service 层强制设置question.setUpdatedBy(SecurityUtil.getCurrentUserId())或在TableField中添加fill FieldFill.UPDATE。坑 3现象——学生端查看试卷时部分题目显示“题目不存在”但教师端确认题目状态为“已发布”。原因——题目状态字段status用VARCHAR(20)存 “PUBLISHED”但前端传参时误传为published小写MySQL 默认不区分大小写比较导致WHERE status published查不到。解决——数据库字段用ENUM(DRAFT,PUBLISHED,REJECTED)替代VARCHAR或在 Java 实体中用Enumerated(EnumType.STRING) 枚举类杜绝字符串硬编码。坑 4现象——多租户环境下A 机构教师能看到 B 机构的题目。原因——租户 IDtenant_id未作为所有表的强制查询条件仅在question表有该字段但question_option、question_knowledge_point表缺失关联查询时未带上tenant_id。解决——所有业务表必须包含tenant_id字段并在 MyBatis-Plus 的MetaObjectHandler中全局填充编写TenantLineInnerInterceptor拦截器自动为所有 SQL 添加AND tenant_id #{tenantId}。5.2 权限验证的黄金法则三步法守住最后一道防线别依赖单一机制。我坚持用三重校验Controller 层拦截用PreAuthorize(permissionService.hasPermission(#questionId, QUESTION_VIEW))检查资源级权限需提前查出题目所属租户/学科。Service 层校验在questionService.getById(id)方法内显式查询SELECT * FROM question WHERE id ? AND tenant_id ? AND subject_code ?双重保险。Mapper 层兜底所有select*方法的 XML SQL 中强制加上AND tenant_id #{tenantId}即使 Controller 和 Service 层漏掉数据库也查不到越权数据。提示tenant_id必须从 JWT Token 中解析而非前端传参。Token 由网关统一签发tenant_id存入SecurityContextService 层通过SecurityUtil.getCurrentTenantId()获取杜绝伪造。6. 让题库系统真正活起来一个被低估却价值百万的技巧——用题目行为日志反哺题库质量所有团队都在优化“怎么更快地查题、组卷、导出”但没人关注一个更本质的问题题目本身是否健康我见过太多案例一道“正确率仅 12%”的选择题三年无人质疑直到某次考试大面积失分才被发现——它根本不是难题而是题干歧义或答案错误。.docx文档里绝不会提“题目质量监控”但这恰恰是系统能否长期存活的关键。我的做法是把每次题目曝光、作答、纠错都记为一条行为日志用 Flink 实时计算指标反向驱动题库运营。6.1 日志埋点设计轻量但覆盖全链路不记录用户隐私只记匿名行为字段类型说明示例log_idString雪花ID1823456789012345678question_idLong题目ID1001event_typeString事件类型EXPOSE曝光、ANSWER_SUBMIT提交、ANSWER_CORRECT答对、FEEDBACK_REPORT举报user_roleString用户角色STUDENT/TEACHER/ADMINduration_msLong操作耗时毫秒23400学生思考23.4秒client_ipString匿名化IP192.168.1.xxx前3段保留末段置0// 日志发送用 RocketMQ 异步避免阻塞主流程 public void logQuestionEvent(QuestionEvent event) { // 构建日志对象 QuestionLog log new QuestionLog(); log.setLogId(IdWorker.getIdStr()); // 雪花ID log.setQuestionId(event.getQuestionId()); log.setEventType(event.getType().name()); log.setUserRole(SecurityUtil.getCurrentUserRole()); log.setDurationMs(System.currentTimeMillis() - event.getStartTime()); log.setClientIp(anonymizeIp(request.getRemoteAddr())); // 匿名化 // 发送MQ由Flink消费 rocketMQTemplate.convertAndSend(question-log-topic, log); }6.2 实时指标计算Flink 作业的核心逻辑用 Flink SQL 计算三个救命指标-- 1. 题目曝光-作答转化率低于30%说明题干不吸引人或位置太偏 INSERT INTO question_metrics SELECT question_id, COUNT(CASE WHEN event_type EXPOSE THEN 1 END) AS expose_cnt, COUNT(CASE WHEN event_type ANSWER_SUBMIT THEN 1 END) AS submit_cnt, CAST(COUNT(CASE WHEN event_type ANSWER_SUBMIT THEN 1 END) AS DOUBLE) / NULLIF(COUNT(CASE WHEN event_type EXPOSE THEN 1 END), 0) AS conversion_rate FROM question_log GROUP BY question_id; -- 2. 题目正确率持续低于15%需人工复核 INSERT INTO question_metrics SELECT question_id, COUNT(*) AS total_cnt, COUNT(CASE WHEN event_type ANSWER_CORRECT THEN 1 END) AS correct_cnt, CAST(COUNT(CASE WHEN event_type ANSWER_CORRECT THEN 1 END) AS DOUBLE) / NULLIF(COUNT(*), 0) AS correct_rate FROM question_log WHERE event_type IN (ANSWER_SUBMIT, ANSWER_CORRECT) GROUP BY question_id; -- 3. 举报率高于5%立即下架 INSERT INTO question_metrics SELECT question_id, COUNT(*) AS report_cnt, CAST(COUNT(*) AS DOUBLE) p a hrefhttps://download.csdn.net/download/python0328/90042462 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p
返回列表