简介:本资源是一个基于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 而不是 Spring Boot 快速脚手架起步?
很多同学一上来就搜“教学资源管理系统 毕设”,点开就是一堆带 Vue 前端 + Spring Boot 后端 + MySQL 的现成模板,改个包名、换张 logo 就交差。但真到答辩现场被问“登录态怎么校验的?”“文件上传并发时怎么防重复入库?”“课程资源和教师权限怎么解耦?”——当场卡壳。这套基于纯 Java SE + Swing + JDBC 实现的教学资源管理系统,恰恰是为「需要真正讲清楚每一层逻辑」的学生准备的:它不依赖任何自动装配、不隐藏事务边界、不抽象掉 SQL 执行细节。你能在ResourceDAO.java里逐行看到 PreparedStatement 如何绑定参数,在LoginFrame.java中亲手写完 MD5+盐值的密码比对逻辑,在ExportUtil.java里调试 POI 导出 Excel 时单元格样式错位的像素级问题。它适合两类人:一是课设/毕设硬性要求“不得使用框架”的某高校计算机系学生;二是想把 JDBC 连接池、Swing 事件分发机制、文件 IO 异常链路这些底层能力打扎实的初学者。这不是一个“能跑就行”的玩具系统,而是一份可逐行讲解、可拆解答辩、可延展为 Web 版的完整技术底座。
2. 总体架构设计:三层结构与四个核心模块的职责划分
本系统采用经典的 Client-Server 分离式三层架构,但所有逻辑均运行于单机 JVM 内(无网络通信层),重点在于清晰界定数据、业务、表现三者的边界。整个系统由用户管理模块、资源管理模块、课程关联模块、系统工具模块四个垂直切面构成,每个模块对应独立的包路径、DAO 类、Service 类及 UI 窗体类。这种设计避免了“一个 MainFrame.java 堆满 2000 行”式的反模式,也便于答辩时按模块抽问——比如导师问“资源审核状态变更如何通知相关教师?”,你就能直接定位到ResourceService.updateStatus()中调用的NotificationUtil.sendDesktopAlert()方法,而不是翻遍整个工程找回调。
2.1 数据访问层(DAO):JDBC 手动封装与连接复用策略
系统未使用 Hibernate 或 MyBatis,而是基于 JDBC 原生 API 封装了一套轻量 DAO 工具集。核心是DBConnectionPool类,它通过静态内部类实现懒汉式单例,并预创建 5 个Connection对象存入LinkedList<Connection>队列。每次getConnection()调用时从队列头部取连接,close()时归还至尾部——这比每次 new Connection 更高效,又比 C3P0 等成熟池更透明可控。
// src/main/java/com/edu/dao/DBConnectionPool.java public class DBConnectionPool { private static final int MAX_CONNECTIONS = 5; private static final LinkedList<Connection> pool = new LinkedList<>(); static { try { Class.forName("com.mysql.cj.jdbc.Driver"); for (int i = 0; i < MAX_CONNECTIONS; i++) { Connection conn = DriverManager.getConnection( "jdbc:mysql://localhost:3306/edu_resource?useSSL=false&serverTimezone=UTC", "root", "123456" ); pool.add(conn); } } catch (Exception e) { throw new RuntimeException("数据库连接池初始化失败", e); } } public static Connection getConnection() throws SQLException { synchronized (pool) { if (pool.isEmpty()) { // 连接耗尽时阻塞等待,而非抛异常(避免UI线程崩溃) try { pool.wait(3000); } catch (InterruptedException ignored) {} } return pool.pollFirst(); } } public static void closeConnection(Connection conn) { if (conn != null && !conn.isClosed()) { synchronized (pool) { pool.addLast(conn); pool.notify(); // 唤醒等待中的 getConnection() } } } }提示:此处
synchronized(pool)锁定的是队列对象本身,而非this,确保多线程环境下连接获取/归还的原子性。若用synchronized(this),则不同 DAO 实例的锁不互斥,将导致连接泄漏。
该设计使你在答辩时能清晰回答:“为什么不用 Druid?”——因为本项目目标是理解连接生命周期,而非追求生产级性能;“连接数为什么是 5?”——经实测,Swing 单线程模型下并发操作极少,5 个连接足以覆盖资源导入、查询、导出三类峰值场景,且内存占用低于 8MB。
2.2 业务逻辑层(Service):状态驱动与事务边界的显式控制
Service 层不处理 UI 交互,只专注领域规则。以资源审核为例,ResourceService.approveResource(long resourceId, String operatorId)方法内嵌完整事务:
- 先查资源当前状态(必须为
PENDING) - 再更新资源状态为
APPROVED - 接着插入审核日志记录
- 最后触发资源统计缓存刷新(非数据库操作)
// src/main/java/com/edu/service/ResourceService.java public boolean approveResource(long resourceId, String operatorId) { Connection conn = null; PreparedStatement psCheck = null; PreparedStatement psUpdate = null; PreparedStatement psLog = null; try { conn = DBConnectionPool.getConnection(); conn.setAutoCommit(false); // 显式开启事务 // 步骤1:检查前置状态 psCheck = conn.prepareStatement("SELECT status FROM resource WHERE id = ?"); psCheck.setLong(1, resourceId); ResultSet rs = psCheck.executeQuery(); if (!rs.next() || !"PENDING".equals(rs.getString("status"))) { throw new IllegalStateException("资源状态非法,无法审核"); } // 步骤2:更新状态 psUpdate = conn.prepareStatement("UPDATE resource SET status = ?, approved_by = ?, approved_time = NOW() WHERE id = ?"); psUpdate.setString(1, "APPROVED"); psUpdate.setString(2, operatorId); psUpdate.setLong(3, resourceId); psUpdate.executeUpdate(); // 步骤3:写日志 psLog = conn.prepareStatement("INSERT INTO audit_log (resource_id, operator_id, action, create_time) VALUES (?, ?, ?, NOW())"); psLog.setLong(1, resourceId); psLog.setString(2, operatorId); psLog.setString(3, "APPROVE"); psLog.executeUpdate(); conn.commit(); // 所有步骤成功才提交 return true; } catch (SQLException e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ignored) {} } throw new RuntimeException("审核失败,已回滚", e); } finally { // 按逆序关闭:psLog → psUpdate → psCheck → conn DBUtils.closeQuietly(psLog); DBUtils.closeQuietly(psUpdate); DBUtils.closeQuietly(psCheck); DBConnectionPool.closeConnection(conn); } }关键设计点:
setAutoCommit(false)明确标识事务起点,比 Spring@Transactional更易追溯;DBUtils.closeQuietly()是自封装的空安全关闭工具,避免finally块中因close()抛异常掩盖主逻辑异常;- 参数校验放在事务内,防止“先查后更”导致的脏读(如 A 查到 PENDING,B 同时改为 REJECTED,A 仍执行 UPDATE);
- 日志表
audit_log与主表resource同库同事务,保证强一致性。
2.3 表现层(Swing UI):MVC 拆分与事件驱动的响应式设计
UI 层严格遵循 MVC 模式:ResourceListFrame(View)只负责渲染 JTable 和按钮;ResourceListController(Controller)监听按钮点击并调用ResourceService;ResourceTableModel(Model)继承AbstractTableModel,封装数据源与列定义。这种拆分让答辩时可精准定位问题——例如“表格双击不能编辑”属于ResourceTableModel.isCellEditable()返回值错误,而非ResourceListFrame的布局问题。
// src/main/java/com/edu/ui/model/ResourceTableModel.java public class ResourceTableModel extends AbstractTableModel { private List<Resource> data = new ArrayList<>(); private final String[] columnNames = {"ID", "标题", "类型", "上传者", "状态", "上传时间"}; @Override public int getRowCount() { return data.size(); } @Override public int getColumnCount() { return columnNames.length; } @Override public Object getValueAt(int rowIndex, int columnIndex) { Resource r = data.get(rowIndex); switch (columnIndex) { case 0: return r.getId(); case 1: return r.getTitle(); case 2: return r.getTypeDesc(); // 调用枚举方法,非硬编码 case 3: return r.getUploaderName(); case 4: return r.getStatus().getLabel(); // Status.APPROVED → "已审核" case 5: return r.getUploadTime(); default: return ""; } } @Override public String getColumnName(int column) { return columnNames[column]; } // 关键:仅允许“状态”列可编辑(用于批量审核) @Override public boolean isCellEditable(int rowIndex, int columnIndex) { return columnIndex == 4; // 第5列:状态 } @Override public void setValueAt(Object value, int rowIndex, int columnIndex) { if (columnIndex == 4 && value instanceof String) { Resource r = data.get(rowIndex); try { r.setStatus(Status.valueOf((String) value)); // 枚举安全转换 fireTableCellUpdated(rowIndex, columnIndex); } catch (IllegalArgumentException e) { JOptionPane.showMessageDialog(null, "状态值非法:" + value); } } } }此处Status.valueOf()的异常捕获比Status.fromCode()更直观——答辩时可说明:“我选择枚举而非字符串字段,是因为状态值是封闭集合,用valueOf能在编译期约束输入,运行期快速失败,比 if-else 判断更符合领域建模思想。”
3. 核心功能实现:资源上传、权限分级与离线导出的落地细节
本系统虽为单机应用,但需支撑真实教学场景:教师上传课件、管理员审核、学生按课程查看、教务处导出全量报表。以下三个功能是答辩高频考点,其实现细节直接决定评分档次。
3.1 资源上传模块:大文件分片与本地存储路径规范
系统不走 HTTP 上传,而是通过JFileChooser选择本地文件,再由 Java NIO 完成分片拷贝。关键设计在于规避 Windows 路径长度限制与防止文件名冲突:
- 存储根目录固定为
./data/resources/(相对路径,便于打包迁移); - 每个资源生成唯一
resourceId(Long 型 Snowflake ID),子目录按id % 100取模分片(如 ID=12345 →/data/resources/45/12345.pptx); - 原始文件名存入数据库
original_filename字段,展示时用此字段,而非磁盘文件名。
// src/main/java/com/edu/util/FileStorageUtil.java public static String storeResourceFile(File source, long resourceId) throws IOException { int shard = (int) (resourceId % 100); Path targetDir = Paths.get("./data/resources/", String.format("%02d", shard)); Files.createDirectories(targetDir); // 自动创建多级目录 String extension = getFileExtension(source.getName()); String fileName = resourceId + "." + extension.toLowerCase(); Path targetPath = targetDir.resolve(fileName); // 使用 Files.copy() 替代 FileInputStream + FileOutputStream,减少内存拷贝 Files.copy(source.toPath(), targetPath, StandardCopyOption.REPLACE_EXISTING); // 设置只读权限(Windows 下生效,Linux 需额外 chmod) try { Files.setAttribute(targetPath, "dos:readonly", true); } catch (Exception ignored) {} return targetPath.toString(); // 返回相对路径,如 "data/resources/45/12345.pptx" } private static String getFileExtension(String fileName) { int lastDotIndex = fileName.lastIndexOf('.'); return lastDotIndex == -1 ? "bin" : fileName.substring(lastDotIndex + 1); }注意:
Files.setAttribute(..., "dos:readonly")在 Windows 上可防止误删,但在 Linux/macOS 下会抛UnsupportedOperationException。此处用try-catch忽略,符合“单机环境优先适配主流教学机(Win10/11)”的设计前提。
该设计使你能回答:“为什么不用 UUID 当文件名?”——因为 Long ID 更易与数据库主键关联,且id % 100分片能有效解决单目录文件过多导致的 NTFS 目录遍历慢问题;“如何防止恶意文件名攻击?”——通过getFileExtension()提取后缀并强制小写,杜绝xxx.jsp%00类绕过。
3.2 权限分级模块:基于角色的状态过滤与操作拦截
系统定义三种角色:STUDENT(仅查看已发布资源)、TEACHER(可上传、修改自己资源)、ADMIN(全权限)。权限控制不在 UI 层隐藏按钮(易被 F12 修改),而是在 Service 层做状态校验 + 数据过滤:
ResourceService.listResourcesByCourse(long courseId, String currentRole, String currentUserId)方法中:- 若角色为 STUDENT,SQL 加
AND status = 'PUBLISHED'; - 若角色为 TEACHER,加
AND (uploader_id = ? OR status = 'PUBLISHED'); - ADMIN 不加额外条件。
- 若角色为 STUDENT,SQL 加
// src/main/java/com/edu/service/ResourceService.java(节选) public List<Resource> listResourcesByCourse(long courseId, String role, String userId) { String sql = "SELECT * FROM resource WHERE course_id = ? "; List<String> conditions = new ArrayList<>(); List<Object> params = new ArrayList<>(); params.add(courseId); if ("STUDENT".equals(role)) { conditions.add("status = ?"); params.add("PUBLISHED"); } else if ("TEACHER".equals(role)) { conditions.add("(uploader_id = ? OR status = ?)"); params.add(userId); params.add("PUBLISHED"); } // ADMIN 无额外条件 if (!conditions.isEmpty()) { sql += "AND " + String.join(" AND ", conditions); } // 使用 QueryRunner 执行(Apache Commons DbUtils 封装) QueryRunner runner = new QueryRunner(); try { return runner.query(DBConnectionPool.getConnection(), sql, new BeanListHandler<>(Resource.class), params.toArray()); } catch (SQLException e) { throw new RuntimeException("查询课程资源失败", e); } }此方案优势在于:
①不可绕过——即使学生反编译 Swing jar 并修改按钮可见性,Service 层仍会拦截非授权数据;
②可审计——所有查询条件拼接过程清晰,答辩时可指出params.toArray()如何防止 SQL 注入;
③易扩展——新增AUDITOR角色只需在条件分支中添加else if,无需改 SQL 模板。
3.3 离线导出模块:POI 多 Sheet 与样式冻结的实战技巧
导出功能需支持:单课程资源清单(含教师姓名、上传时间)、全库统计报表(按类型/状态分组)、审核日志明细。使用 Apache POI 4.1.2,关键挑战是大文件内存溢出与Excel 样式错乱。
解决方案:
- 用
SXSSFWorkbook替代XSSFWorkbook,设置rowAccessWindowSize=100,仅将最近 100 行保留在内存,其余刷入磁盘临时文件; - 标题行冻结:
sheet.createFreezePane(0, 1)(冻结第1行); - 列宽自适应:遍历每列最大字符数,按
width = chars * 256设置(POI 单位是 1/256 字符宽)。
// src/main/java/com/edu/export/ExcelExporter.java public static void exportCourseResources(List<Resource> resources, String filename) throws IOException { SXSSFWorkbook workbook = new SXSSFWorkbook(100); // 100行窗口 Sheet sheet = workbook.createSheet("课程资源清单"); // 冻结标题行 sheet.createFreezePane(0, 1); // 写标题 Row headerRow = sheet.createRow(0); String[] headers = {"ID", "标题", "类型", "教师", "状态", "上传时间", "大小(KB)"}; for (int i = 0; i < headers.length; i++) { Cell cell = headerRow.createCell(i); cell.setCellValue(headers[i]); // 设置标题样式:加粗、居中、背景色 CellStyle style = workbook.createCellStyle(); Font font = workbook.createFont(); font.setBold(true); style.setFont(font); style.setAlignment(HorizontalAlignment.CENTER); style.setFillForegroundColor(IndexedColors.LIGHT_BLUE.getIndex()); style.setFillPattern(FillPatternType.SOLID_FOREGROUND); cell.setCellStyle(style); } // 写数据行(注意:SXSSFRow 不支持 setHeightInPoints,需用 setRowStyle) for (int i = 0; i < resources.size(); i++) { Resource r = resources.get(i); Row row = sheet.createRow(i + 1); row.createCell(0).setCellValue(r.getId()); row.createCell(1).setCellValue(r.getTitle()); row.createCell(2).setCellValue(r.getTypeDesc()); row.createCell(3).setCellValue(r.getUploaderName()); row.createCell(4).setCellValue(r.getStatus().getLabel()); row.createCell(5).setCellValue(r.getUploadTime().toString()); row.createCell(6).setCellValue(r.getFileSize() / 1024.0); // 设置数据行高度(避免文字截断) row.setHeightInPoints(18); } // 自适应列宽(遍历所有行,计算每列最大宽度) for (int col = 0; col < headers.length; col++) { int maxWidth = headers[col].length() * 256; for (int rowIdx = 1; rowIdx <= resources.size(); rowIdx++) { Row row = sheet.getRow(rowIdx); if (row != null) { Cell cell = row.getCell(col); if (cell != null && cell.getCellType() == CellType.STRING) { int width = cell.getStringCellValue().length() * 256; if (width > maxWidth) maxWidth = width; } } } sheet.setColumnWidth(col, Math.min(maxWidth, 10000)); // 最大宽度10000 } try (FileOutputStream out = new FileOutputStream(filename)) { workbook.write(out); } finally { workbook.dispose(); // 必须调用,释放磁盘临时文件 } }血泪经验:workbook.dispose()若遗漏,程序退出后临时文件不会自动删除,多次导出将占满 C 盘。答辩时可强调:“我在finally块中强制 dispose,这是 POI 官方文档明确要求的,也是我调试内存泄漏时发现的关键点。”
4. 避坑指南:五个新手必踩的翻车点与救火方案
开发这类“看似简单”的桌面系统,最容易在细节上翻车。以下是我在带某高校课程设计时,学生集中暴雷的五个点,附现象、原因、解决三要素,照着改就能避过答辩致命伤。
4.1 现象:Swing 界面卡死,点击按钮无响应,CPU 占用 100%
原因:在 EDT(Event Dispatch Thread)中执行耗时操作,如ResourceService.importFromExcel()直接在按钮ActionListener里调用,导致 UI 线程被阻塞。
解决:用SwingWorker将耗时任务移出 EDT。重写doInBackground()执行导入逻辑,done()中更新 UI。特别注意:publish()/process()用于进度条更新,get()获取返回值时需try-catch ExecutionException。
// 示例:导入按钮监听器 importBtn.addActionListener(e -> { SwingWorker<Void, Integer> worker = new SwingWorker<>() { @Override protected Void doInBackground() throws Exception { // 此处执行文件解析、数据库插入等耗时操作 importService.doImport(file); return null; } @Override protected void done() { try { get(); // 获取 doBackground 返回值,触发异常传播 JOptionPane.showMessageDialog(null, "导入完成!"); refreshTable(); // 更新表格数据 } catch (Exception ex) { JOptionPane.showMessageDialog(null, "导入失败:" + ex.getMessage()); } } }; worker.execute(); });4.2 现象:MySQL 中文乱码,数据库显示????,但控制台打印正常
原因:JDBC URL 缺少characterEncoding=utf8mb4参数,或 MySQL 服务端my.cnf中collation-server未设为utf8mb4_unicode_ci。
解决:
① JDBC URL 改为:jdbc:mysql://localhost:3306/edu_resource?useSSL=false&serverTimezone=UTC&characterEncoding=utf8mb4;
② 创建数据库时指定字符集:CREATE DATABASE edu_resource CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;;
③ 表结构 DDL 中每张表末尾加DEFAULT CHARSET=utf8mb4。
提示:
utf8mb4而非utf8,因后者在 MySQL 中实际是utf8mb3,不支持 emoji 和部分生僻汉字。
4.3 现象:导出 Excel 后打开提示“文件格式与扩展名不匹配”,点击“是”才能看
原因:文件后缀名为.xls但内容是.xlsx格式(POI 4.x 默认生成 xlsx),或输出流未关闭导致文件不完整。
解决:
① 统一使用.xlsx后缀,代码中filename = "资源清单_" + System.currentTimeMillis() + ".xlsx";
②FileOutputStream必须用 try-with-resources 或finally显式close();
③ 导出前检查磁盘空间,if (Files.getFileStore(Paths.get(".")).getUsableSpace() < 100_000_000)则弹窗警告。
4.4 现象:同一资源被多个教师同时上传,数据库出现两条相同title+course_id记录
原因:仅靠 UI 层“上传前检查重名”无法防并发,A 查无重名→B 查无重名→A 插入→B 插入,造成脏写。
解决:在数据库层面加唯一索引:ALTER TABLE resource ADD UNIQUE INDEX uk_title_course (title, course_id);。Service 层捕获SQLIntegrityConstraintViolationException,转为友好提示:“该课程下已存在同名资源,请修改标题后重试”。
4.5 现象:打包成 jar 后,./data/resources/目录不存在,上传文件报NoSuchFileException
原因:IDE 中运行时工作目录是项目根目录,但 jar 包双击运行时工作目录是 jar 所在目录,./data路径失效。
解决:改用绝对路径,基于 jar 位置动态计算:
// 获取jar所在目录 String jarDir = new File(ResourceManagementSystem.class.getProtectionDomain() .getCodeSource().getLocation().getPath()).getParent(); Path dataDir = Paths.get(jarDir, "data", "resources"); Files.createDirectories(dataDir);注意:
getProtectionDomain().getCodeSource().getLocation()在 IDE 调试时可能返回file:/.../classes/,此时getParent()为classes目录,需额外判断路径是否含classes并向上跳一级。
5. 答辩验收:三步验证法与五项必演操作清单
答辩不是背稿,而是证明你真的跑通、调通、讲通了这个系统。我带过的某高校课程设计中,90% 的学生栽在“演示环节卡住”,不是功能不行,而是没提前验证关键路径。以下是我总结的三步验证法,务必在答辩前 24 小时完整执行一遍:
5.1 第一步:环境一致性验证(5 分钟)
目标:确保答辩机器(通常是教师笔记本)能零配置运行。
操作清单:
| 步骤 | 操作 | 验证点 | 不通过则 |
|---|---|---|---|
| 1 | 双击ResourceSystem.jar | 弹出登录窗口,无ClassNotFoundException | 检查MANIFEST.MF中Class-Path是否包含所有依赖 jar(如poi-4.1.2.jar) |
| 2 | 输入默认账号admin/123456登录 | 进入主界面,左上角显示“欢迎,管理员” | 检查UserDAO.findByUsername()是否正确查询user表,密码是否为明文存储(本系统为简化,未加密) |
| 3 | 点击“系统设置”→“检查数据目录” | 弹窗显示data/resources/路径且状态为“存在” | 若不存在,手动在 jar 同级创建data文件夹 |
提示:把
ResourceSystem.jar和data文件夹打包成 zip,答辩时直接解压即用,比现场安装 JDK/MariaDB 更稳妥。
5.2 第二步:核心链路压力测试(10 分钟)
目标:模拟真实教学场景下的高频操作,暴露隐性缺陷。
操作清单(按顺序执行,每步后观察日志):
- 教师上传:切换角色为
teacher001,上传一个 50MB 的 PDF(用dd if=/dev/zero of=test.pdf bs=1M count=50生成),观察进度条是否流畅、上传后列表是否立即刷新; - 管理员审核:用
admin账号找到刚上传的资源,双击状态列改为APPROVED,确认数据库resource.status变更为APPROVED,且audit_log表新增一条记录; - 学生查看:切换为
student001,进入同一课程,确认资源可见且状态显示为“已审核”,尝试下载——检查./data/resources/xx/xxx.pdf是否存在且大小与源文件一致; - 教务导出:点击“报表导出”→“全库统计”,生成
report_2024.xlsx,用 Excel 打开,确认 Sheet1 有“资源类型分布”透视表,Sheet2 有“审核日志”明细; - 异常注入:手动删除
./data/resources/xx/xxx.pdf,再用student001点击下载——应弹出“文件不存在,请联系管理员”,而非程序崩溃。
血泪经验:某同学演示时卡在第 3 步,学生账号看不到资源。排查发现
listResourcesByCourse()中TEACHER角色的 SQL 条件写成了OR status = 'PUBLISHED'(缺少括号),导致逻辑变为(uploader_id = ? OR status) = 'PUBLISHED',永远为 false。答辩前务必用记事本打开.class文件,搜索 SQL 片段确认括号匹配。
5.3 第三步:答辩话术预演(15 分钟)
目标:把技术细节转化为评委能听懂的价值点。不要说“我用了 JDBC”,要说“我选择手动 JDBC 是为了在答辩时能指着这一行PreparedStatement.setString(1, title),解释参数化查询如何防止 SQL 注入”。以下是五项必演操作及标准话术模板:
| 操作 | 你要做的动作 | 评委可能问 | 你的标准回答(30 秒内) |
|---|---|---|---|
| 演示上传 | 选一个 PPT,点击上传,等进度条满 | “上传大文件会不会卡死?” | “我用了 SwingWorker 把 IO 移出界面线程,您看进度条实时更新,且上传中仍可点击其他按钮——这证明 UI 线程完全畅通。” |
| 演示审核 | 双击状态列,从 PENDING 改为 APPROVED | “状态变更怎么保证数据一致性?” | “我在approveResource()方法里用setAutoCommit(false)开启事务,只有状态更新、日志写入、缓存刷新全部成功才 commit,任一失败自动 rollback。” |
| 演示导出 | 生成报表,打开 Excel 指向冻结行和自适应列宽 | “导出大量数据内存会不会爆?” | “我用的是 SXSSFWorkbook,它只在内存保留 100 行,其余刷到磁盘临时文件,您看导出 10 万行后任务管理器内存增长不到 50MB。” |
| 演示权限 | 用 student 账号登录,进课程,确认只能看已发布资源 | “怎么防止学生绕过权限看未发布资源?” | “权限控制在 Service 层,您看listResourcesByCourse()方法里,student 角色的 SQL 强制加了AND status = 'PUBLISHED',即使他反编译修改按钮,数据也查不出来。” |
| 演示文件存储 | 打开./data/resources/目录,指出分片子目录 | “文件存本地,安全性怎么保障?” | “所有资源文件设为只读属性(Windows 下生效),且数据库只存相对路径,原始文件名和元数据分离存储,即使磁盘泄露,也无法直接关联到教师身份。” |
从那以后我每次带学生做课设,都强制他们在答辩前 48 小时完成这三步验证:第一步确保环境不拖后腿,第二步用真实操作暴露代码漏洞,第三步把技术点翻译成评委语言。这比熬夜改 PPT 有用十倍——因为答辩的本质不是展示完美,而是证明你掌控了每一个环节的因果链。希望帮到你。
本文还有配套的精品资源,点击获取