简介:一套基于Java Web的研究生实验室管理系统项目包,面向Java Web学习者、课程设计或毕业设计者。系统覆盖教师端与学生端完整功能:教师可查看学生历次汇报与代码、按时间排序审核、创建限时会议、发起讨论并审批请假;学生可创建项目、上传代码并设置公开状态、提交本周汇报、查看公开代码与历史会议记录。这些模块基本涵盖研究生实验室日常管理的主要场景。资源以zip压缩包形式提供,共246个文件,含62个Java源码、62个class编译文件、54个jar依赖库、24个JSP页面、16个XML及properties配置,包体大小30.63MB;源码承载核心业务逻辑,class与jar便于部署运行,JSP负责页面交互,XML和properties用于项目配置,目录结构清晰。目前已有589人浏览学习,可作为完整项目参考,帮助读者理解Java Web分层开发、角色权限与会话管理等典型实现,并便于在此基础上扩展会议通知、消息提醒、数据统计等功能。
1. 当实验室管理还停留在微信接龙和 Excel 共享表格
我带过的高校实验室,有一多半的管理状态是这样的:设备借还要在群里喊一声,谁拿了没人记得;周会纪要在共享盘里存了十几个版本;导师问"这个月设备利用率多少",我得打开五个 Excel 才能拼出一个大概。研究生三年级的师兄走后,他名下那些实验项目管理权限和资料就是一团乱麻。这些痛点在两三年前有套系统就能解决,但要自己写一套完整可落地的 JavaWeb 实验室管理系统,需要用到的功能清单和踩坑记录远比想象中多。这篇文章我想拆的,就是一套基于 JavaWeb 的研究生实验室管理系统,功能覆盖设备预约借用、实验项目进度跟踪、实验室周会/组会会议记录管理、成员权限分配。适合正在做毕业设计、或者实验室确实需要数字化管理工具的开发者参考,技术栈是现在很多高校仍在用的 SSM(Spring + Spring MVC + MyBatis)配上 JSP 前端,老项目维护和二次开发都能接得住。
2. 技术选型与数据库设计:先把地基打稳,功能才不会空中楼阁
2.1 为什么选 SSM 而不是 Spring Boot
先把这个最容易被问的问题讲清楚。近几年新项目大多用 Spring Boot,但实验室管理系统这类高校内部项目,真要动手做的时候你会发现师兄师姐留下的代码几乎都是 SSM 骨架,而且部署环境多数是老实验室的服务器,JDK 版本卡在 1.7 或 1.8,Tomcat 还是 8.0。我一般会建议先确认目标环境再决定框架。
如果从零开始且部署环境可控,Spring Boot 确实省事——内嵌 Tomcat、自动配置、不用写一堆 XML。但我在这套系统里还是选了 SSM 搭配 Maven 多模块结构,理由很简单:实验室管理系统要对接的旧数据库脚本、以及后续可能要拆分的移动端接口,SSM 的分层结构在团队协作时边界更清晰。
实际开发时,用户管理、设备管理、会议管理三个模块的代码规模并不大,SSM 的 XML 配置虽然显得繁琐,但出了问题可以精准定位到是哪一层的问题。Spring Boot 那种自动配置在出故障时反而因为封装太深,对新手不太友好。
2.2 数据库设计:四张核心表怎么定字段
这是整套系统里最需要静下心做对的部分。我见过不止一个项目翻车在设备表没有设计"状态变更记录",导致后来想统计设备利用率时数据全是空的。核心表我最终定为六张,但根基是下面四张:
-- 用户表:区分学生、教师、管理员三种角色 CREATE TABLE `t_user` ( `user_id` INT PRIMARY KEY AUTO_INCREMENT, `username` VARCHAR(32) UNIQUE NOT NULL COMMENT '登录名', `password` VARCHAR(64) NOT NULL COMMENT 'MD5加密存储', `real_name` VARCHAR(32) NOT NULL COMMENT '真实姓名', `role` TINYINT NOT NULL DEFAULT 2 COMMENT '角色: 0-管理员 1-教师 2-学生', `lab_id` INT DEFAULT NULL COMMENT '所属实验室', `phone` VARCHAR(16) DEFAULT NULL, `email` VARCHAR(64) DEFAULT NULL, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 设备表:asset_no 是线下物理编号 CREATE TABLE `t_equipment` ( `equip_id` INT PRIMARY KEY AUTO_INCREMENT, `asset_no` VARCHAR(32) NOT NULL COMMENT '资产编号', `equip_name` VARCHAR(64) NOT NULL, `category` VARCHAR(32) DEFAULT NULL COMMENT '设备分类', `status` TINYINT DEFAULT 0 COMMENT '0-空闲 1-借出 2-维修中 3-报废', `location` VARCHAR(64) COMMENT '存放位置', `lab_id` INT NOT NULL, `purchase_date` DATE, `price` DECIMAL(10,2) ); -- 设备借用记录表:归还时间是关键索引 CREATE TABLE `t_borrow_record` ( `record_id` INT PRIMARY KEY AUTO_INCREMENT, `equip_id` INT NOT NULL, `user_id` INT NOT NULL, `borrow_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `due_time` DATETIME NOT NULL COMMENT '应还时间', `return_time` DATETIME DEFAULT NULL COMMENT '实际归还时间', `status` TINYINT DEFAULT 0 COMMENT '0-借用中 1-已归还 2-逾期', `remark` VARCHAR(255) ); -- 会议记录表:记录会议纪要和任务分配 CREATE TABLE `t_meeting` ( `meeting_id` INT PRIMARY KEY AUTO_INCREMENT, `meeting_title` VARCHAR(128) NOT NULL, `meeting_date` DATE NOT NULL, `location` VARCHAR(64), `host_id` INT COMMENT '主持人用户ID', `lab_id` INT NOT NULL, `summary` TEXT COMMENT '会议纪要正文', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP );这套结构的设计点在两个地方:一是t_user里的lab_id外键,直接决定了后面权限过滤时只用一条 SQL 就能拉出"本实验室的设备",不用跨表关联三次;二是t_borrow_record的due_time和return_time分开存,而不是只存一个状态字段——因为逾期判断要拿return_time IS NULL AND due_time < NOW()来查,状态字段在系统跑一段时间后很容易和数据不一致,需要定时任务去更新。参数注意:TINYINT类型的角色和状态字段别改成枚举字符串,Java 代码里写常量比对即可,数据库层面省空间,查询性能也更好。
2.3 项目目录结构:Maven 骨架与三层架构落位
很多新手拿到这类系统源码直接就开始改包名,结果改到一半发现 XML 里面配的扫描路径不对,项目起不来。我先给出一份我在模拟项目X里实际用的目录结构,这套结构在 SSM 项目里算比较标准的写法。
lab-manage/ ├── pom.xml ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/example/lab/ │ │ │ ├── controller/ # Spring MVC 控制层 │ │ │ │ ├── UserController.java │ │ │ │ ├── EquipmentController.java │ │ │ │ ├── MeetingController.java │ │ │ │ └── BorrowRecordController.java │ │ │ ├── service/ # 业务接口 │ │ │ │ └── impl/ # 业务实现 │ │ │ ├── dao/ # MyBatis Mapper 接口 │ │ │ ├── entity/ # 实体类 │ │ │ ├── interceptor/ # 登录/权限拦截器 │ │ │ └── util/ # 工具类(MD5、日期处理等) │ │ ├── resources/ │ │ │ ├── mapper/ # MyBatis XML 映射文件 │ │ │ ├── spring/ │ │ │ │ ├── spring-dao.xml │ │ │ │ ├── spring-service.xml │ │ │ │ └── spring-mvc.xml │ │ │ └── jdbc.properties # 数据库连接配置 │ │ └── webapp/ │ │ ├── WEB-INF/ │ │ │ ├── web.xml │ │ │ └── views/ # JSP 页面 │ │ ├── static/ # CSS/JS/图片 │ │ └── upload/ # 文件上传目录这里要强调spring-mvc.xml、spring-dao.xml、spring-service.xml三个配置文件的分工:spring-dao.xml只管数据源、SqlSessionFactory 和 Mapper 扫描;spring-service.xml管事务和 Service 实现类扫描;spring-mvc.xml只扫描 controller 包,并且配视图解析器。有个血泪经验是如果让 spring-mvc.xml 扫描了 @Service 注解,会导致事务配置失效,因为 Spring 容器出现了两份 Service 实例。判断方法很简单:启动日志里看到 "Bean named 'xxxService' is already defined in file" 就说明扫描路径交叉了。
3. 核心功能拆解:设备预约、项目进度、会议管理三个硬骨头
3.1 设备借用与归还:状态机设计比 CRUD 更重要
设备管理模块功能看起来是增删改查,实际上最关键的逻辑是"状态流转"。我把状态分成空闲、已预约、借出、维修维护、报废五种,其中"已预约"和"借出"要严格区分。预约只是占了一个时间段,设备物理上还在实验室;借出代表设备已经离开存放位置。从数据库层面看,t_equipment.status只是当前快照,真正的预约信息存在独立的预约表里,这样才不会出现两个人都预约了同一台设备的情况。
业务层处理借用申请的流程是:前端提交借用申请,携带设备 ID、预计使用时间区间、用途说明,Service 层先调checkAvailability(equipId, startTime, endTime)方法查该设备在时间区间内是否有冲突预约,没有冲突才创建申请并设置状态为待审核。教师或管理员审核通过后,状态改为已预约。实际取设备时再点击"确认借出",状态改为借出。归还时检查设备完好即可确认归还,然后更新状态为空闲。这种拆法保证了每一个动作都有记录可追踪,出了纠纷能查到底。
3.2 实验项目进度管理:从立项到结题的四级状态
研究生实验室和公司项目最大的不同在于,项目周期动辄半年一年,中间的人员流动很大。所以项目模块我设计成:项目立项、中期检查、结题验收、归档关闭四个状态,每个状态变更都要求附带说明。
这块要特别处理的是"项目成员中途退出"的情况。一开始我把成员关系设计成项目表外键直接关联用户表,后来发现某学校项目有个 A 同学中途转导师了,他在项目里提交的实验数据就全断了关联。后来的解决方式是单独建一张t_project_member中间表,记录成员加入时间和退出时间,项目进行中的最新成员集合用exit_time IS NULL过滤,这样历史数据始终保留,成员变更也有迹可循。
项目状态变更的接口里有个容易疏忽的逻辑:当项目从"进行中"改为"已结题"时,要校验该项目名下挂着的设备借用记录是否已经全部归还,如果还有未还设备,则禁止结题。这个约束在数据库层面没有外键能直接实现,只能在 Service 层人为判断——写的时候加个 try-catch 把业务异常抛给全局异常处理器,返回友好提示。
3.3 会议管理:周会纪要与任务分配
会议管理是本系统的亮点模块。研究生实验室的周会频率很高,一次周会可能牵扯出几十条待办。我实现的逻辑是:创建会议记录时,除了填写会议主题、日期、地点、出席人员,还要支持在会议纪要正文下方添加"待办事项列表",每个待办可以指派给指定成员,设置截止时间。
这里比较妙的设计是:待办事项变更后要在t_meeting_issue表里记录一条改动历史,包括操作人、操作时间、状态变化。这样下次开会时可以直接展开上一次会议的待办事项逐条核对。从技术角度来说,这是一个简单的状态机加审计表,但对使用者来说体验非常好——它彻底解决了"上次周会说了什么"和"那些事办得怎么样"这两个高频提问。
会议模块的另一个技术点是文件上传。每次会议可能附带 PPT、PDF 或者 Word 文档,我建议把文件上传与会议记录做成分步操作:先创建会议记录,拿到会议 ID,再上传附件,附件表存储原始文件名和磁盘路径,文件重命名规则用"会议ID_时间戳_原文件名"。这么做是为了避免 JavaWeb 上传文件时的中文文件名乱码问题—— CommonMultipartResolver 解析 multipart 请求时,如果前端没有设置 UTF-8 编码,中文文件名极容易变成乱码。后文避坑章节还会专门提这一点。
4. 关键代码落地:从 Mapper 到业务层的完整实现
4.1 实体类与 MyBatis 映射:驼峰命名和字段映射的坑
后端实体类我直接给出一个典型的Equipment实体,这里面有一个很容易踩的坑:Java 属性命名使用驼峰(比如equipName),数据库字段用下划线(equip_name),MyBatis 如果没开驼峰映射,查询结果全是 null。
public class Equipment { private Integer equipId; private String assetNo; private String equipName; private String category; private Integer status; private String location; private Integer labId; private Date purchaseDate; private BigDecimal price; // getter/setter 省略 }对应 Mapper 接口:
public interface EquipmentMapper { List<Equipment> selectByLabId(@Param("labId") Integer labId); int insertEquipment(Equipment equipment); int updateStatus(@Param("equipId") Integer equipId, @Param("status") Integer status); List<Equipment> selectAvailable(@Param("labId") Integer labId, @Param("startTime") Date startTime, @Param("endTime") Date endTime); }XML 映射文件里我建议显式写resultMap而不是依赖自动映射,因为自动映射在关联查询时很容易出问题:
<resultMap id="EquipmentResultMap" type="Equipment"> <id property="equipId" column="equip_id"/> <result property="assetNo" column="asset_no"/> <result property="equipName" column="equip_name"/> <result property="category" column="category"/> <result property="status" column="status"/> <result property="location" column="location"/> <result property="labId" column="lab_id"/> <result property="purchaseDate" column="purchase_date"/> <result property="price" column="price"/> </resultMap>注意purchase_date是 DATE 类型,映射到 Java 的Date没问题,但如果数据库字段是DATETIME,建议在实体里用java.util.Date或者java.time.LocalDateTime,别用java.sql.Date——后者的精度只有日期,查询时光标落在System.currentTimeMillis()附近的记录可能对不上。参数说明:@Param注解在 Mapper 接口方法参数多于一个时必须加,否则 MyBatis 无法识别参数名,报BindingException错误。
4.2 会议室预约冲突检测:区间重叠判断的逻辑
会议室预约比设备借用更敏感,因为一天之内同一个会议室不能被占两次。核心冲突判断 SQL 如下:
SELECT COUNT(*) FROM t_meeting_room_reservation WHERE room_id = #{roomId} AND status = 1 -- 1表示已确认预约 AND ( (start_time < #{endTime} AND end_time > #{startTime}) OR (start_time >= #{startTime} AND start_time < #{endTime}) )这段 SQL 覆盖了四种重叠情况:预约区间完全包含新区间、新区间完全包含预约区间、预约区间的开始落在新区间内、预约区间的结束落在新区间内。只要计数大于 0,就拒绝本次预约。
对应的 Service 层逻辑:
public boolean checkRoomAvailability(Integer roomId, Date startTime, Date endTime) { // 先校验时间参数合法性 if (startTime.after(endTime)) { throw new IllegalArgumentException("开始时间必须早于结束时间"); } // 校验是否预约未来时间段 if (startTime.before(new Date())) { throw new IllegalArgumentException("不能预约过去的时间"); } int count = meetingRoomReservationMapper.countConflict( roomId, startTime, endTime); return count == 0; }这段代码有意识地把时间合法性和冲突检测分开,而不是一次 SQL 搞定。原因在于业务上的提示信息要分类型——参数不合法和已被预约是两类完全不同的报错,合并处理会让用户看到"该时间段已被预约"但实际是传参有误,这种误导性的报错在学校项目里经常被骂。另外,每周固定组会这种周期性预约,我一般建议在业务层单独处理,不要把复杂表达式塞进这个checkRoomAvailability方法里。
4.3 文件上传与下载:解析 multipart 请求的前后配置
Spring MVC 处理文件上传需要在spring-mvc.xml里配置文件解析器,这一步漏了前端传过来的文件永远都是 null。
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <property name="maxUploadSize" value="52428800"/> <property name="maxInMemorySize" value="1048576"/> <property name="defaultEncoding" value="UTF-8"/> </bean>这里三个参数的作用分别是:maxUploadSize限制单个请求最大 50MB,maxInMemorySize控制在内存中暂存的上限(超过 1MB 就落盘到临时文件),defaultEncoding强制 UTF-8,中文文件名就靠这一行避免乱码。Controller 层接收文件的写法是:
@PostMapping("/upload") public String uploadMeetingFile(@RequestParam("file") MultipartFile file, @RequestParam("meetingId") Integer meetingId, HttpServletRequest request) { if (file.isEmpty()) { return "error"; } // 使用 UUID 重命名文件,避免中文和空格问题 String originalFilename = file.getOriginalFilename(); String extName = originalFilename.substring(originalFilename.lastIndexOf(".")); String newFileName = UUID.randomUUID().toString().replace("-", "") + extName; // 保存路径按月份归档,防止单个目录文件过多 String monthPath = new SimpleDateFormat("yyyyMM").format(new Date()); String realPath = request.getServletContext().getRealPath("/upload/") + monthPath; File destDir = new File(realPath); if (!destDir.exists()) { destDir.mkdirs(); } file.transferTo(new File(realPath + File.separator + newFileName)); // 保存文件记录到数据库 meetingFileMapper.insertFile(meetingId, originalFilename, newFileName, monthPath); return "success"; }需要强调的是,getOriginalFilename()返回的可能是带全路径的浏览器兼容写法(旧版 IE 会传完整路径),稳妥做法是这里裁掉路径部分。另外真实部署到 Linux 服务器时,request.getServletContext().getRealPath("/upload/")拿到的路径在 Tomcat 的 webapps 目录下,如果重启部署会清空目录,所以严谨做法是把上传目录配置成外部绝对路径,在jdbc.properties里写死。这两点我都在系统交付时给实验室管理员写进了部署文档,免得运维阶段出幺蛾子。
5. 部署上线与避坑指南:环境配置、中文乱码和依赖冲突
5.1 本地环境搭建:JDK 1.8 + Tomcat 8.5 + MySQL 5.7
这套 SSM 项目我推荐的环境组合是 JDK 1.8、Tomcat 8.5、MySQL 5.7。不是不能用更高版本,而是这些版本的组合踩坑最少,网上可查的解决方案也最多。数据库初始化脚本执行完后,先手动用 Navicat 或命令行验证数据表能正常查询,再启动 Tomcat 部署项目。
环境配置里比较容易忽视的是jdbc.properties里的连接参数:
jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/lab_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=yourpasswordserverTimezone=Asia/Shanghai这一条在 MySQL 5.7 以上版本常见,不设置会报时区错误。但更隐蔽的问题是如果 MySQL 本身初始化时的字符集不是 utf8mb4,即使连接串加了characterEncoding=utf8,中文还是会乱码。我一般建议建库时执行:
CREATE DATABASE lab_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;5.2 常见问题一:数据库驱动版本和连接失败
现象是项目启动后执行第一条 SQL 就报ClassNotFoundException: com.mysql.jdbc.Driver,或者Access denied for user 'root'@'localhost'。
原因基本是两个方向:一是 Maven 依赖里 MySQL 驱动的版本和数据库版本不匹配,低版本驱动连不上高版本数据库;二是账号密码或权限配置问题。解决方式:MySQL 8.x 必须用com.mysql.cj.jdbc.Driver,核心依赖要改版本;账号问题执行ALTER USER 'root'@'localhost' IDENTIFIED BY 'newpassword';并检查是否有远程连接权限。我在模拟项目X里就遇到过一次驱动包被 Maven 漏下导致编译能过运行时报错的情况,排查方式是看本地 Maven 仓库里mysql-connector-java的 jar 包是否存在,如果不存在强制mvn dependency:resolve。
5.3 常见问题二:页面中文乱码
现象是页面显示中文全是问号或乱码,如果数据库里存的也是乱码,那就是从上到下的编码链都有问题。
解决要按三层排查:第一层是 JSP 页面头部的<%@ page contentType="text/html;charset=UTF-8" language="java" %>是否声明;第二层是 web.xml 里是否配置了 Spring MVC 的 CharacterEncodingFilter;第三层是 MySQL 的表结构和连接串字符集是否为 utf8mb4。三层都对了,乱码问题基本消失。值得警惕的是偶尔有某个字段还是乱码,那多半是数据库那张表建表时单独定义为 latin1,处理方式是ALTER TABLE t_xxx CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;。
5.4 常见问题三:MyBatis Mapper 绑定异常
现象是启动时报Invalid bound statement (not found): com.example.lab.dao.EquipmentMapper.selectByLabId。
原因十有八九是 XML 映射文件没有放在和 Mapper 接口相同的包路径下,或者spring-dao.xml里的mapperLocations配置路径不对。我习惯的做法是把 XML 放在src/main/resources/mapper目录下,然后在spring-dao.xml里配置:
<property name="mapperLocations" value="classpath:mapper/*.xml"/>这样 Mapper 接口用注解 @Mapper 扫描注册,XML 文件与接口的同名规则不再要求包路径一致。注意如果同时配置了<mapper package>扫描注解模式的接口,又配置 XML 扫描,要确保路径不冲突,否则会报重复绑定。
5.5 常见问题四:Maven 依赖冲突
现象是编译能过,但 Tomcat 启动时抛NoSuchMethodError或者ClassCastException,一看日志发现是 Spring 版本不一致。
这类问题的排查废话不说,直接建议:在 pom.xml 中对全部 Spring 相关依赖统一版本号,用<properties>定义变量;另一个高频冲突点是一套老系统里出现了两组 servlet-api 和 jsp-api 的依赖,Tomcat 自带的版本和 Maven 拉下来的版本互相打架。解决办法是依赖里把 servlet-api 的 scope 设为provided,让它只在编译期存在,运行时用 Tomcat 的。这个坑容易让人在启动日志里翻很久。
6. 进阶技巧:把权限控制和数据可视化加进现有系统
基础功能跑通之后,实验室管理系统真正"好用"和"能看"的分水岭在两条:一是权限控制是否精确到了操作级别,二是管理页面是否有数据图表支撑。
先看权限控制,SSM 项目里最常见的方案是拦截器配合注解。定义一个@RequireRole注解,标注在 Controller 方法上:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RequireRole { int value() default 2; // 0管理员 1教师 2学生 }拦截器里取当前登录用户角色,数值越小权限越高:
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if (!(handler instanceof HandlerMethod)) { return true; // 静态资源直接放行 } HandlerMethod method = (HandlerMethod) handler; RequireRole requireRole = method.getMethodAnnotation(RequireRole.class); if (requireRole == null) { return true; // 没有权限注解就不拦截 } Object loginUser = request.getSession().getAttribute("loginUser"); if (loginUser == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return false; } User user = (User) loginUser; if (user.getRole() > requireRole.value()) { request.setAttribute("errorMsg", "权限不足"); request.getRequestDispatcher("/WEB-INF/views/error.jsp").forward(request, response); return false; } return true; }这段代码的关键判断点是user.getRole() > requireRole.value(),因为角色数值越小权限越高。这里有个习惯问题——有的项目角色定义相反(0 学生、1 教师),换项目时容易惯性写反导致学生能删设备。我的习惯是在整个项目里只用"数值越小权限越高"这一种约定,并且全员都要在注释里写明。
数据可视化我用的方案是 Chart.js,JSP 页面引入静态 js 文件,Controller 返回 JSON,前端 Ajax 拉数据再渲染图表。典型的设备使用率统计可以按周聚合:
public Map<String, Object> getEquipmentUsage(Integer labId) { List<Map<String, Object>> usageList = borrowRecordMapper.selectWeeklyUsage(labId); Map<String, Object> result = new HashMap<>(); for (Map<String, Object> item : usageList) { String week = item.get("week_start").toString(); Long total = (Long) item.get("total_hours"); result.put(week, total); } return result; }前端 Ajax 收到数据后用 Chart.js 画折线图:
$.ajax({ url: '/lab/equipment/usage', dataType: 'json', success: function(data) { var labels = Object.keys(data); var values = Object.values(data); new Chart(document.getElementById('usageChart'), { type: 'line', data: { labels: labels, datasets: [{ label: '设备使用时长(小时)', data: values }] } }); } });第三个我比较推荐的进阶技巧是邮件提醒。设备借用前一天发邮件提醒归还,会议开始前半小时发邮件提醒参会。实现方式简单,用javax.mail包封装一个MailUtil,配合java.util.Timer或者 Spring 的@Scheduled定时任务每天扫一次t_borrow_record表里due_time在次日且return_time为空的记录,发送提醒邮件。定量来说,配合数据图表,管理员打开首页扫一眼就能知道哪些设备闲置率高、哪些学生经常逾期归还,导师开组会的时候直接照着系统数据分配资源,比开会拍脑袋高效得多。
说个实际的教训:我接手的第一套实验室系统在没有任何权限拦截的情况下裸跑了三个月,某天一个学生用户直接访问了删除设备的 URL,把服务器上的设备数据清空了大半。从那以后我交付的每个系统都强制走一遍权限自检清单——先登录学生账号,逐一访问管理员的功能 URL,确认都被拦截器挡下来,这一步不落下。希望这篇拆解对你有点用,动手搭的时候少走一段弯路。
本文还有配套的精品资源,点击获取