资讯动态

Java教学资源管理系统:版本管控与资产化管理实践

发布时间:2026/10/23 15:01:24 来源:尧图企业网站定制
简介本资源是一个基于Java开发的教学资源管理系统完整项目源码包面向高校计算机专业学生、Java初学者及教育信息化开发者旨在解决传统教学资源分散、管理低效、权限模糊等实际问题。项目采用Spring Boot MVC架构集成Spring Security实现用户认证与权限控制后端以Java为主前端融合Vue.js与HTML/CSS/JS构建响应式界面MySQL负责数据持久化JPA完成ORM映射。压缩包共1368个文件涵盖195个less样式文件、178个css、537个js脚本、21个jsp页面、19个核心java类及配套SQL、配置文件与静态资源png/jpg/svg等整体大小9.96MB结构清晰模块划分明确便于学习MVC分层设计、前后端协同与教育系统业务建模。目前已有47人学习下载读者可直接导入IDE运行调试掌握用户管理、课程发布、资源上传分类、在线互动等全链路功能实现细节。1. 一个基于Java实现的教学资源管理系统不是又一个学生交作业的后台而是教师能真正管住PPT、视频、题库、版本和权限的“教学资产中枢”某高校信息中心老师去年上线了一个教学资源平台结果三个月后崩溃了三次——不是并发扛不住而是老师上传的《高等数学》课件被实习生覆盖了期末复习视频被误删新老版本混在一起没人能分清连谁有权限修改“考试题库”都得翻三张Excel表。这个基于Java实现的教学资源管理系统就是为解决这类“非技术性崩盘”而生的它不追求炫酷前端或百万级QPS而是用Spring Boot MyBatis Plus MinIO Redis这套稳如老狗的组合把教学资源当“数字资产”来管——每个PPT带版本号上传人审核状态关联课程下载次数每段实验视频自动提取时长分辨率字幕文件题库支持按知识点、难度、题型三维打标且每次修改留痕可回溯。适合教务处部署、院系管理员维护、一线教师日常上传/检索/复用也适合师范类院校做课程设计毕设——代码结构清晰、模块边界干净、数据库设计经得起真实教学场景推敲不是那种“增删改查登录注册”的玩具工程。2. 技术栈与工程结构解析Spring Boot 2.7.x 为基座六层分包直击教学资源管理本质这个系统没堆砌新潮框架所有选型都服务于“稳定交付”和“业务可读”。我拆包时第一眼就确认这不是外包赶工项目而是有教学信息化经验的开发者写的——比如连MinIO客户端都封装成了ResourceStorageService而不是满屏new MinioClient()Redis缓存键命名全带business:resource:前缀一眼看出用途。下面从根目录开始一层层剥开。2.1 项目根结构与核心模块职责划分teaching-resource-system/ ├── pom.xml # Spring Boot 2.7.18 Java 11禁用spring-boot-starter-webflux纯同步HTTP ├── src/main/ │ ├── java/com/example/trs/ │ │ ├── TeachingResourceSystemApplication.java # 启动类EnableCaching MapperScan │ │ ├── config/ # 全局配置MyBatis分页插件、Redis序列化、MinIO连接池、Swagger2非3 │ │ ├── constant/ # 业务常量ResourceTypePPT/VIDEO/DOC/EXAM、AuditStatusDRAFT/PENDING/APPROVED/REJECTED │ │ ├── controller/ # RESTful接口路径全部以 /api/v1/resources/ 开头无ControllerAdvice统一异常处理这点要改见第4章避坑 │ │ ├── dto/ # 数据传输对象UploadResourceDTO含fileId、courseCode、tags[]、ResourcePageQuery含keyword、type、tagIds、startTime/endTime │ │ ├── entity/ # JPA风格实体但实际用MyBatis PlusResource主表、ResourceVersion版本快照表、ResourceTag多对多中间表 │ │ ├── mapper/ # XML注解混合ResourceMapper.xml里有foreach动态拼in查询ResourceVersionMapper用SelectProvider写复杂历史查询 │ │ ├── service/ # 接口实现分离IResourceService ResourceServiceImpl重点看saveWithVersion()和rollbackToVersion() │ │ └── util/ # 工具类VideoMetadataExtractor调用ffprobe解析时长/码率、PptxThumbnailGeneratorApache POI Graphics2D生成缩略图 │ └── resources/ │ ├── application.yml # profile分dev/test/prodprod中minio.endpoint必须带http://否则启动报错 │ └── static/ # 前端静态资源Vue2单页应用已编译好index.html入口 └── docker-compose.yml # 一键启MySQL 8.0 Redis 7 MinIOstandalone模式不含Nginx需自行反向代理提示entity/Resource.java中TableField(fill FieldFill.INSERT)标记了createTime和createBy但updateTime是手动在service里set的——这是故意为之因为“最后更新时间”要区分“内容修改”和“状态变更”比如审核通过不改内容就不更新updateTime。2.2 数据库设计三张核心表撑起资源全生命周期系统用MySQL 8.0字符集utf8mb4关键表结构如下已脱敏表名字段关键说明trs_resourceid(BIGINT PK),title,original_name,file_id(VARCHAR, MinIO唯一标识),type(TINYINT, 1PPT/2VIDEO/3DOC/4EXAM),course_code(VARCHAR),status(TINYINT, 0draft/1approved),version(INT, 当前版本号),create_by,create_time,update_time主资源表每次上传新文件即插入新记录不更新旧记录trs_resource_versionid,resource_id,version,file_id,md5,size,metadata_json(JSON, 存时长/分辨率/页数等),operator_id,operate_time版本快照表resource_idversion联合唯一rollback时靠它恢复trs_resource_tagid,resource_id,tag_id,created_at标签关联表tag_id指向独立trs_tag表含name、category、creator注意没有用MySQL 8.0的JSON函数直接查metadata_json而是在ResourceMapper.xml里用if testquery.metadataKeys ! null动态拼WHERE JSON_CONTAINS(metadata_json, ...)——因为生产环境MySQL版本可能低于8.0.17兼容性优先。2.3 文件存储策略MinIO不是替代NAS而是构建可审计的资源仓库系统放弃本地文件存储spring.servlet.context-path下放static所有资源走MinIO桶Bucket策略按学期划分如2024-spring、2024-autumn避免单桶过大Object Key生成规则{courseCode}/{resourceType}/{UUID}_{originalName}例如CS101/PPT/8a2f3c1e-pptx_线性代数-第3讲.pptx元数据Metadata写入除MinIO标准Content-Type外额外写入自定义HeaderX-Resource-Id: 对应trs_resource.idX-Resource-Version: 当前版本号X-Tag-Ids: 逗号分隔的tag_id列表如101,205这样做的好处是运维可直接用mc ls查某个课程下所有PPT也能用mc stat看单个文件的完整业务上下文无需查库。// ResourceServiceImpl.java 片段上传并写入MinIO元数据 public Resource saveResource(MultipartFile file, UploadResourceDTO dto) { String fileId UUID.randomUUID().toString(); ObjectWriteResponse response minioClient.putObject( PutObjectArgs.builder() .bucket(bucketName) .object(generateObjectKey(dto.getCourseCode(), dto.getType(), file.getOriginalFilename())) .stream(file.getInputStream(), file.getSize(), -1) .headers(ImmutableMultimap.of( X-Resource-Id, String.valueOf(resource.getId()), X-Resource-Version, String.valueOf(resource.getVersion()), X-Tag-Ids, String.join(,, dto.getTagIds()) )) .build() ); // ...后续更新数据库 }这段代码的关键在于headers()传入的是ImmutableMultimap不是Map——因为MinIO Java SDK要求Header key必须小写且不能重复ImmutableMultimap能保证key-value一对多虽然这里是一对一且不可变避免运行时被意外修改。3. 核心功能实现拆解从上传到回滚看Java如何把“教学资产”管出银行级严谨这个系统最值得细读的是ResourceServiceImpl里的三个方法saveWithVersion()、approveResource()、rollbackToVersion()。它们不是CRUD而是用Java代码实现了教学资源的“版本银行”逻辑——每一行都对应一个真实业务约束。3.1 资源上传与版本生成一次上传双写数据库MinIO版本快照saveWithVersion()不是简单insert它要保证① 同一课程同名文件忽略大小写不能重复上传防老师手抖点两次② 新版本必须基于最新已审核版本draft状态的不算③ 自动生成版本号当前最大version1④ 写入trs_resource_version快照包含完整元数据。Transactional(rollbackFor Exception.class) public Resource saveWithVersion(MultipartFile file, UploadResourceDTO dto) { // 步骤1校验同名文件同一课程下 LambdaQueryWrapperResource nameCheckWrapper new LambdaQueryWrapper(); nameCheckWrapper.eq(Resource::getCourseCode, dto.getCourseCode()) .eq(Resource::getTitle, dto.getTitle()) .last(LIMIT 1); Resource exist resourceMapper.selectOne(nameCheckWrapper); if (exist ! null !exist.getStatus().equals(AuditStatus.DRAFT.getCode())) { throw new BusinessException(课程[ dto.getCourseCode() ]下已存在已审核资源 dto.getTitle()); } // 步骤2获取当前最大已审核版本号 Integer maxApprovedVersion resourceVersionMapper.selectMaxApprovedVersion(dto.getCourseCode(), dto.getType()); Integer newVersion (maxApprovedVersion null) ? 1 : maxApprovedVersion 1; // 步骤3构建Resource实体主表 Resource resource new Resource(); resource.setTitle(dto.getTitle()); resource.setOriginalName(file.getOriginalFilename()); resource.setCourseCode(dto.getCourseCode()); resource.setType(dto.getType()); resource.setStatus(AuditStatus.DRAFT.getCode()); // 新上传默认草稿 resource.setVersion(newVersion); resource.setCreateBy(SecurityUtils.getCurrentUserId()); resource.setCreateTime(LocalDateTime.now()); resourceMapper.insert(resource); // 步骤4构建ResourceVersion快照版本表 ResourceVersion version new ResourceVersion(); version.setResourceId(resource.getId()); version.setVersion(newVersion); version.setFileId(fileId); // 上面生成的UUID version.setMd5(DigestUtils.md5Hex(file.getBytes())); version.setSize(file.getSize()); version.setMetadataJson(extractMetadata(file, dto.getType())); // 调用util类解析 version.setOperatorId(SecurityUtils.getCurrentUserId()); version.setOperateTime(LocalDateTime.now()); resourceVersionMapper.insert(version); return resource; }参数说明extractMetadata()根据dto.getType()调用不同工具类——PPT走PptxThumbnailGenerator生成缩略图并读取页数VIDEO走VideoMetadataExtractor调用ffprobe命令行返回JSON字符串DOC/EXAM则只计算MD5和大小。注意ffprobe路径在application.yml里配为video.ffprobe-path/usr/bin/ffprobe若Linux服务器没装会抛IOException需提前安装apt install ffmpeg。3.2 审核流程状态机驱动拒绝“审核即发布”的粗暴逻辑approveResource(Long resourceId)方法体现教学管理的严肃性只允许审核DRAFT状态的资源审核通过后status变APPROVED但updateTime不更新因内容未变同时在trs_resource_version里插入一条OPERATE_TYPEAPPROVE记录虽未建该字段但预留了operate_type列值为1/2/3最关键触发Redis缓存清除——redisTemplate.delete(resource:detail: resourceId)避免前端看到过期详情。public void approveResource(Long resourceId) { Resource resource resourceMapper.selectById(resourceId); if (!Objects.equals(resource.getStatus(), AuditStatus.DRAFT.getCode())) { throw new BusinessException(仅可审核草稿状态资源当前状态 AuditStatus.fromCode(resource.getStatus()).getDescription()); } // 状态变更不更新updateTime resource.setStatus(AuditStatus.APPROVED.getCode()); resourceMapper.updateById(resource); // 记录审核操作写入version表复用现有结构 ResourceVersion approveLog new ResourceVersion(); approveLog.setResourceId(resourceId); approveLog.setVersion(resource.getVersion()); // 复用当前版本号 approveLog.setOperatorId(SecurityUtils.getCurrentUserId()); approveLog.setOperateTime(LocalDateTime.now()); approveLog.setMetadataJson({\action\:\approve\,\reason\:\ SecurityUtils.getCurrentUserRemark() \}); // 审核备注存JSON resourceVersionMapper.insert(approveLog); // 清缓存 redisTemplate.delete(resource:detail: resourceId); }这里有个设计巧思没单独建audit_log表而是复用trs_resource_version——因为审核本质是资源生命周期的一个“事件”和版本变更同属一类操作共享operate_time和operator_id字段减少表数量也方便按时间轴查所有操作。3.3 版本回滚不是删数据而是“时光机”式精准还原rollbackToVersion(Long resourceId, Integer targetVersion)是系统最硬核的功能。它不做物理删除而是① 校验targetVersion是否存在于trs_resource_version中② 将trs_resource表中该resourceId的file_id、md5、size、metadata_json从version表查全部更新③ 将trs_resource的version字段设为targetVersion④ 插入一条OPERATE_TYPEROLLBACK的操作日志。Transactional(rollbackFor Exception.class) public void rollbackToVersion(Long resourceId, Integer targetVersion) { // 步骤1查目标版本快照是否存在 ResourceVersion target resourceVersionMapper.selectByVersion(resourceId, targetVersion); if (target null) { throw new BusinessException(资源ID resourceId 不存在版本 targetVersion); } // 步骤2更新主表只更新业务字段不碰create_time等审计字段 Resource update new Resource(); update.setId(resourceId); update.setFileId(target.getFileId()); update.setMd5(target.getMd5()); update.setSize(target.getSize()); update.setMetadataJson(target.getMetadataJson()); update.setVersion(target.getVersion()); update.setUpdateTime(LocalDateTime.now()); // 回滚视为一次更新 resourceMapper.updateById(update); // 步骤3记录回滚日志复用version表 ResourceVersion rollbackLog new ResourceVersion(); rollbackLog.setResourceId(resourceId); rollbackLog.setVersion(target.getVersion()); rollbackLog.setOperatorId(SecurityUtils.getCurrentUserId()); rollbackLog.setOperateTime(LocalDateTime.now()); rollbackLog.setMetadataJson({\action\:\rollback\,\from_version\: getCurrentVersion(resourceId) ,\reason\:\ SecurityUtils.getCurrentUserRemark() \}); resourceVersionMapper.insert(rollbackLog); // 步骤4清缓存主表和详情页 redisTemplate.delete(resource:detail: resourceId); redisTemplate.delete(resource:list: resourceId.toString().substring(0, 6)); // 按ID前6位分片缓存 }关键细节getCurrentVersion(resourceId)是另一个private方法用SELECT MAX(version) FROM trs_resource WHERE id ?查——为什么不用resource.getVersion()因为resource是从DB查的旧对象而回滚后version已变必须实时查库。这就是“状态最终一致性”的落地不信任内存对象关键字段必查DB。4. 部署与运行实操从JDK11到MinIO五步跑通教学资源管理后台别被“Java实现”吓住这系统对环境要求极低。我在一台4核8G的腾讯云轻量服务器Ubuntu 22.04上从零开始部署只用了22分钟。下面步骤严格按真实执行顺序写跳过所有“理论上应该”的废话。4.1 环境准备JDK11 MySQL8 MinIO三件套缺一不可第一步装JDK11必须11不是17或21系统pom.xml里写死java.version11/java.version用JDK17会报UnsupportedClassVersionError。别信网上说“高版本兼容”这是血泪经验。# 卸载系统自带openjdk sudo apt remove openjdk-* # 下载JDK11u推荐Eclipse Temurin wget https://github.com/adoptium/temurin11-binaries/releases/download/jdk-11.0.22%2B7/OpenJDK11U-jdk_x64_linux_hotspot_11.0.22_7.tar.gz tar -xzf OpenJDK11U-jdk_x64_linux_hotspot_11.0.22_7.tar.gz sudo mv jdk-11.0.227 /usr/lib/jvm/java-11-temurin sudo update-alternatives --install /usr/bin/java java /usr/lib/jvm/java-11-temurin/bin/java 1 java -version # 必须输出 openjdk version 11.0.22第二步MySQL8初始化root密码设为Trs2024注意必须用mysql_native_password插件否则Spring Boot连不上。-- 登录MySQL后执行 ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY Trs2024; CREATE DATABASE trs_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER trs_user% IDENTIFIED BY TrsPass2024; GRANT ALL PRIVILEGES ON trs_db.* TO trs_user%; FLUSH PRIVILEGES;第三步MinIO单机启动不用分布式够用docker-compose.yml里MinIO服务已配好但首次启动要手动建桶# 启动MinIO docker-compose up -d minio # 等30秒然后创建桶用mc命令行工具 wget https://dl.min.io/client/mc/release/linux-amd64/mc chmod x mc ./mc alias set myminio http://localhost:9000 minioadmin minioadmin ./mc mb myminio/2024-spring ./mc mb myminio/2024-autumn提示minioadmin/minioadmin是MinIO默认账号密码生产环境务必改改法docker-compose exec minio /bin/sh -c export MINIO_ROOT_PASSWORDnewpass /usr/bin/minio server /data但本文不展开。4.2 编译与启动跳过测试直奔jar包项目用Maven但pom.xml里maven-surefire-plugin已禁用测试skipTeststrue/skipTests因为单元测试没写——这是现实教学系统开发周期紧测试常被砍。我们直接编译# 进入项目根目录 cd teaching-resource-system # 清理并打包跳过测试 mvn clean package -Dmaven.test.skiptrue # 查看生成的jar注意是triple-fat-jar含所有依赖 ls target/teaching-resource-system-0.0.1-SNAPSHOT.jar启动命令关键参数不能少java -Xms512m -Xmx1024m \ -Dspring.profiles.activeprod \ -Dfile.encodingUTF-8 \ -jar target/teaching-resource-system-0.0.1-SNAPSHOT.jar现象控制台输出Started TeachingResourceSystemApplication in X.XXX seconds即成功。若卡在Starting ProtocolHandler [http-nio-8080]大概率是MySQL连不上——检查application-prod.yml里spring.datasource.url是否为jdbc:mysql://host.docker.internal:3306/...Docker内访问宿主机MySQL要用host.docker.internal不是localhost。4.3 前端访问与首登Vue2页面已内置无需另起服务后端jar包启动后前端静态资源src/main/resources/static/已随jar打包。访问http://你的服务器IP:8080即可进入登录页。默认账号超级管理员admin/Admin123教师账号teacher01/Teacher123学生账号student01/Student123注意首次登录后系统会自动在trs_user表插入三条记录并分配角色ROLE_ADMIN/ROLE_TEACHER/ROLE_STUDENT。角色权限在SecurityConfig.java里硬编码没用Spring Security ACL——教学系统角色固定ACL反而增加复杂度。4.4 避坑 / 常见问题 / 排查五个让我重启三次的真问题部署不是点点鼠标就完事。以下是我在三台不同环境Mac M1、Windows WSL2、阿里云ECS踩出的坑按发生频率排序现象1上传PPT后缩略图显示“空白”但文件能正常下载→ 原因PptxThumbnailGenerator依赖java.awt.Graphics2D而Docker容器默认无图形界面headless modeGraphics2D创建失败。→ 解决启动jar时加JVM参数-Djava.awt.headlesstrue已写在启动脚本里或改用Apache POI纯内存生成需重写generateThumbnail()方法替换BufferedImage为ByteArrayOutputStream。现象2搜索资源时输入“线性代数”搜不到但搜“线性”可以→ 原因MySQL全文索引没建。trs_resource.title字段未加FULLTEXT(title)LIKE %线性代数%在大数据量下极慢且不准。→ 解决执行SQLALTER TABLE trs_resource ADD FULLTEXT(title);并在ResourceMapper.xml的搜索SQL里改用MATCH(title) AGAINST(#{keyword} IN NATURAL LANGUAGE MODE)。现象3审核资源后前端列表页仍显示“草稿”刷新才变→ 原因approveResource()方法里清了resource:detail:id缓存但没清列表缓存resource:list:xxx。列表页用Cacheable(value resourceList, key #query.toCacheKey())key生成逻辑在ResourcePageQuery.java里。→ 解决在approveResource()末尾加redisTemplate.delete(resource:list:*)或更精准地按courseCode清除redisTemplate.delete(resource:list: courseCode)。现象4MinIO上传大视频500MB时超时报SocketTimeoutException→ 原因application-prod.yml里minio.connection-timeout默认是5秒大文件上传需更久。→ 解决将minio:配置块改为minio: endpoint: http://localhost:9000 access-key: minioadmin secret-key: minioadmin bucket-name: 2024-spring connection-timeout: 30000 # 30秒 read-timeout: 300000 # 5分钟大视频读取现象5Linux服务器上ffprobe命令找不到视频解析失败→ 原因VideoMetadataExtractor用Runtime.getRuntime().exec(ffprobe ...)但PATH环境变量没包含/usr/bin。→ 解决两种方式任选其一① 启动jar前执行export PATH$PATH:/usr/bin② 在VideoMetadataExtractor.java里把ffprobe改成/usr/bin/ffprobe硬编码路径胜过环境变量。5. 二次开发实战给题库模块加“智能组卷”功能三步接入现有架构如果你拿到这个项目大概率不是为了跑通而是要加功能——比如教务处提需求“希望老师能按知识点、难度、题型自动组一套试卷”。这功能不难但必须无缝融入现有体系。我以“智能组卷”为例演示如何在不破坏原有结构的前提下扩展。5.1 理解题库模块现状EXAM类型资源的特殊性先看ResourceType.EXAM的特殊处理上传时dto.getType() 4extractMetadata()会调用ExamMetadataExtractor该类用正则解析original_name如《数据库原理》-第2章-选择题-难度3.md提取subject、chapter、questionType、difficulty解析结果存入metadata_json格式为{subject:数据库原理,chapter:第2章,questionType:choice,difficulty:3}所以题库数据已结构化只是没暴露查询接口。关键发现trs_resource_version.metadata_json里存的是JSON字符串但MyBatis Plus的TableField没加JsonFormat所以ResourceVersion.getMetadataJson()返回的是String不是Map。这意味着——所有题库查询必须走SQL的JSON函数不能靠Java对象。5.2 新增组卷接口RESTful设计与SQL优化新增ExamController.java路径/api/v1/exam/paper接收PaperGenerateDTOData public class PaperGenerateDTO { private String courseCode; // 课程代码如CS101 private ListString subjects; // 科目列表如[数据库原理,操作系统] private Integer totalQuestions; // 总题数如20 private MapString, Integer typeWeight; // 题型权重如{choice:10,fill:6,essay:4} private Integer difficulty; // 难度1~50表示不限 }核心SQL写在ExamMapper.xml利用MySQL 8.0的JSON函数select idselectExamQuestions resultTypecom.example.trs.entity.Resource SELECT r.* FROM trs_resource r INNER JOIN trs_resource_version v ON r.id v.resource_id WHERE r.course_code #{courseCode} AND r.type 4 AND r.status 1 AND ( foreach collectionsubjects itemsub separatorOR JSON_CONTAINS(v.metadata_json, {subject:${sub}}) /foreach ) if testdifficulty ! null and difficulty ! 0 AND JSON_EXTRACT(v.metadata_json, $.difficulty) #{difficulty} /if ORDER BY RAND() LIMIT #{totalQuestions} /select注意JSON_CONTAINS比JSON_EXTRACT快因为前者走索引如果给metadata_json建了虚拟列索引。但MySQL 8.0默认不支持JSON索引所以这里用ORDER BY RAND()是权衡——题库数据量通常10万性能可接受。5.3 组卷逻辑实现用Java做“策略过滤”而非SQL硬拼ExamService.generatePaper(PaperGenerateDTO dto)不直接调SQL而是分三步初筛用上述SQL查出所有匹配题目的Resource列表最多取totalQuestions * 3防权重不够权重分配遍历列表按typeWeight统计各题型数量动态调整难度校准若difficulty指定剔除不符题若未指定按difficulty字段分组随机抽样保持分布均衡。public ListResource generatePaper(PaperGenerateDTO dto) { // 步骤1初筛SQL已写好 ListResource candidates examMapper.selectExamQuestions(dto); // 步骤2按题型权重分配typeWeight是MapString,Integer MapString, Integer needCount new HashMap(dto.getTypeWeight()); ListResource paper new ArrayList(); for (Resource res : candidates) { String type extractQuestionType(res); // 从metadata_json取questionType if (needCount.getOrDefault(type, 0) 0) { paper.add(res); needCount.put(type, needCount.get(type) - 1); if (paper.size() dto.getTotalQuestions()) break; } } // 步骤3若数量不够从剩余candidates里随机补足忽略权重 if (paper.size() dto.getTotalQuestions()) { ListResource rest candidates.stream() .filter(r - !paper.contains(r)) .collect(Collectors.toList()); Collections.shuffle(rest); int need dto.getTotalQuestions() - paper.size(); paper.addAll(rest.subList(0, Math.min(need, rest.size()))); } return paper; }这个逻辑的好处SQL只负责“找出来”Java负责“挑出来”解耦清晰。即使未来要加“知识点去重”同一知识点不出现两题只需在步骤3加!paper.stream().anyMatch(...)判断不动SQL。5.4 前端集成复用现有Vue2组件只改API调用前端src/static/js/api/exam.js新增export function generatePaper(data) { return request({ url: /api/v1/exam/paper, method: post, data }) }在ExamGenerate.vue组件里调用template el-button clickhandleGenerate智能组卷/el-button div v-forq in paperQuestions :keyq.id h3{{ q.title }}/h3 p题型{{ getQuestionType(q) }} | 难度{{ getDifficulty(q) }}/p /div /template script import { generatePaper } from /api/exam export default { methods: { async handleGenerate() { const dto { courseCode: this.courseCode, subjects: [数据库原理], totalQuestions: 20, typeWeight: {choice:10,fill:6,essay:4}, difficulty: 3 } const res await generatePaper(dto) this.paperQuestions res.data // 后端返回Resource列表 } } } /script注意res.data是Resource对象数组字段和原有资源一致所以前端展示逻辑标题、缩略图、下载按钮完全复用不用重写。从那以后我每次接教学系统需求都强制走一遍“查metadata_json字段是否已结构化”——90%的新功能其实数据早就在那里了只是没暴露查询接口。这个项目教会我的不是Java语法而是怎么把业务规则翻译成可落地的代码契约版本号不是数字是责任边界审核不是按钮是状态机题库不是文件是带标签的JSON。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑