拓冰建站拓冰建站
首页 / 资讯中心 / 正文

基于Java与Android的开放式实验室管理系统:MVC架构与排课冲突检测实践

简介面向高校实验室管理场景的Java毕业设计项目同时提供PC端与Android手机App覆盖管理员、教师、学生三类角色。核心功能包括实验室管理、实验设备增删改查、实验课排课与实验查询其中排课支持按学期、18周、班级错开安排并在填选教室时展示实验室占用情况。项目采用界面层、业务逻辑层、数据层三层分离的MVC设计思想服务器端与移动端分别可用MyEclipse/Eclipse/Idea和Eclipse/AndroidStudio导入运行数据通信结合XML与JSON格式。整个资源包943个文件、约20.75MB包含146个Java源文件、392个class文件、45个JSP页面、105个XML配置及SQL数据库脚本等配合Android客户端APK可直接对照调试。目前已有118人学习下载适合计算机专业学生用于毕业设计参考、前后端交互练习或实验室管理系统的二次开发。1. 高校开放式实验室管理要解决的不只是预约高校开放式实验室管理 App 这个题目表面上是做一个预约工具实际上要解决的是信息同步问题。管理员手里有一份实验室台账教师手里有一份排课表学生只能靠公告栏猜实验时间。三套数据各自为政就会出现两门课抢同一间实验室、设备已停用却仍在课表里、学生到了现场发现教室换了的连锁问题。这套基于 Java 和 MVC 设计思想开发的项目用 PC 端服务器加安卓手机端覆盖管理员、教师、学生三个角色把实验室管理、设备管理、实验课排课和实验查询放进同一条链路。对做毕业设计和安卓开发练习的人而言它最有价值的地方是完整展示了从 MySQL 数据层、Java 业务逻辑层到 Android 界面层的三层分离以及 XML 与 JSON 两种格式在接口通信中的实际分工。2. 从三层分离到MVC服务器端与客户端通信格式设计这套系统在客户端和服务器端都沿用了 MVC 设计思想。Android 端把 Activity 作为视图层数据模型和网络请求单独拆出来服务端按界面层、业务逻辑层、数据层三层分离每层只处理自己的事。实际拆项目时可以看到服务器端没有把所有代码堆在一个 Servlet 里而是按 Controller、Service、Dao 三个包组织Controller 负责拿到请求参数、调业务方法、输出响应Service 负责排课和占用判断这类规则Dao 负责拼 SQL 访问 MySQL。这个分层方式即使放到 Spring MVC 项目里思路也完全一致只是把手写 Servlet 换成了 Controller 注解所以先用这个结构讲原理面试或答辩时也更容易说清。2.1 界面层、业务逻辑层、数据层的边界三层分离最容易写歪的地方是边界。界面层只负责接收参数和渲染结果不应该出现 SQL数据层只负责数据库交互不应该包含权限判断业务逻辑层夹在中间承担排课、冲突检测、占用统计这些规则。比如管理员在 PC 端或安卓端点“删除设备”请求到达服务器后是 Controller 取得 deviceId 参数调用 DeviceService.delete(deviceId)Service 里判断该设备是否正在被某门实验课引用再让 DeviceDao 执行 DELETE。如果直接把 DELETE 语句写在 Servlet 里功能也能跑但排课要判断占用时就只能到处复制粘贴 SQL这是最常见的反面写法。从这个项目附带的内容看服务器端本质上就是一套传统 Java Web 应用可以在 MyEclipse、Eclipse 或 Idea 里运行。Android 客户端则是独立的安卓工程目录下能看到 StudentEditActivity、LabInfoEditActivity、DeviceEditActivity 这些类分别对应学生信息编辑、实验室信息编辑、设备信息编辑属于界面层组件。它们不直接访问数据库而是通过带参数的 URL 把请求发给服务器再由服务器返回 XML 或 JSON。这个约定是整个系统能跑通的前提。2.2 服务端 Controller 的请求与响应示例WebServlet(/lab/list) public class LabListServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String semester req.getParameter(semester); resp.setContentType(text/xml; charsetUTF-8); LabService service new LabService(); ListLab labList service.findAvailableLabs(semester); PrintWriter out resp.getWriter(); out.println(?xml version\1.0\ encoding\UTF-8\?); out.println(labs); for (Lab lab : labList) { out.println(lab id\ lab.getId() \ name\ lab.getName() \ capacity\ lab.getCapacity() \ location\ lab.getLocation() \/); } out.println(/labs); out.flush(); } }这里设置了 XML 响应头并逐行拼 XML。注意 item 的字段不要出现中文空格和换行否则 Android 端用 XmlPullParser 解析时容易拿到带换行的属性值。semester 参数是查询条件比如“2024-2025-1”service 层根据学期过滤出该学期排课涉及的实验室也可以不加学期参数直接返回全部实验室由前端分类。这种手拼 XML 的写法在老 Java Web 项目里很常见优点是依赖少缺点是容易漏转义。所以项目里同时引入了 JSON 用于单对象传输修改实验室或设备时服务器只需返回一个 JSON 对象包含 status 和 message客户端不用解析整张列表。2.3 XML 与 JSON 双格式的使用边界数据场景推荐格式原因查询多条记录如实验室列表、设备列表、排课列表XML记录集带嵌套关系解析时按标签定位单项数据返回如登录结果、添加实验后的状态JSON字段少解析成本低object 里能直接放对象错误信息与提示JSONmessage 字段单独存在客户端拿到后直接弹 Toast这个取舍和现在很多接口直接统一用 JSON 不一样。老式安卓项目里XML 解析用 XmlPullParser一次能拿到所有节点列表数据非常直观而 JSON 在 Android 里虽然用 org.json.JSONObject 也方便但处理嵌套数组时反而不如流式解析直观。项目原描述里明确写了“XML 格式用于传输查询的记录集json 格式用于传输单个的对象信息”这说明分类是刻意的列表走 XML详情和操作结果走 JSON。XmlPullParser parser Xml.newPullParser(); parser.setInput(new StringReader(xmlText)); int eventType parser.getEventType(); while (eventType ! XmlPullParser.END_DOCUMENT) { if (eventType XmlPullParser.START_TAG lab.equals(parser.getName())) { String labId parser.getAttributeValue(null, id); String labName parser.getAttributeValue(null, name); String capacity parser.getAttributeValue(null, capacity); // 加入 ArrayListLab } eventType parser.next(); }解析循环里通过 eventType 驱动遇到 START_TAG 且标签名为 lab 时用 getAttributeValue 按属性名取值。比较保险的写法是先把整段 XML 读成字符串确认不是错误页面后再交给 XmlPullParser否则服务器 500 时返回 HTML解析器会在第一行就抛 XmlPullParserException。这个坑在联调阶段几乎都会遇到。提示XML 和 JSON 的分工原则要在接口层固定下来否则安卓端同一个解析函数要写两套判断逻辑。2.4 HttpUtil 与安卓端请求封装安卓端所有网络请求最终都走到一个工具类里也就是原工程里能看到的 HttpUtil.class。实际源码会提供类似的封装一个 post(url, params) 方法内部用 HttpURLConnection 提交表单参数并统一设置连接超时和字符集。public class HttpUtil { private static final int CONNECT_TIMEOUT 10000; private static final int READ_TIMEOUT 15000; public static String post(String url, MapString, String params) { HttpURLConnection conn null; try { conn (HttpURLConnection) new URL(url).openConnection(); conn.setRequestMethod(POST); conn.setConnectTimeout(CONNECT_TIMEOUT); conn.setReadTimeout(READ_TIMEOUT); conn.setDoOutput(true); String body encodeParams(params); OutputStream os conn.getOutputStream(); os.write(body.getBytes(UTF-8)); os.close(); if (conn.getResponseCode() 200) { return readStream(conn.getInputStream()); } return null; } catch (Exception e) { e.printStackTrace(); return null; } finally { if (conn ! null) conn.disconnect(); } } }CONNECT_TIMEOUT 和 READ_TIMEOUT 分开设置是因为实验室列表接口偶尔会慢但连接阶段必须在 10 秒内完成读取阶段给到 15 秒避免弱网下小数据也超时。encodeParams 方法用 URLEncoder.encode 转义参数key 和 value 都做 UTF-8 编码。这个方法不处理 Cookie因为原项目的权限控制主要靠每次请求带 role 和 userId 实现登录态比较轻量。做毕业设计时如果不想引入第三方库这套封装完全够用。3. 管理员、教师、学生三端权限落地实验室/设备/排课的MySQL表设计与接口实现角色边界看起来简单落在数据库和接口层会影响很多东西。管理员的操作集中在实验室和设备两张表上教师的操作集中在排课表上学生的请求则是只读查询。权限可以不做非常复杂的拦截器而是在每个接口入口判断 role 参数但更好的做法是单独抽一层权限校验方法。3.1 三个角色的功能矩阵角色可操作模块典型操作管理员实验室、设备增删改查、设置可用状态教师排课、实验室占用填实验名、选教室、选时间学生实验查询查时间、地点、班级、主题从 Android 端类名也能看出这条链路DeviceEditActivity 负责设备编辑界面LabInfoEditActivity 负责实验室信息编辑界面CourseTeacherListActivity 和 CourseStudentListActivity 分别是教师端和学生端的实验课列表。服务器端收到不同请求后先识别角色再决定放行还是拒绝。这个设计在答辩时可以直接讲成“接口层的权限过滤”比把判断写进 SQL 里更容易解释。3.2 四张核心表的结构与建表语句CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, role TINYINT NOT NULL COMMENT 1管理员 2教师 3学生, real_name VARCHAR(32) ); CREATE TABLE lab ( id INT PRIMARY KEY AUTO_INCREMENT, lab_no VARCHAR(32) NOT NULL, lab_name VARCHAR(64) NOT NULL, capacity INT NOT NULL COMMENT 容纳人数, location VARCHAR(64), status TINYINT DEFAULT 1 COMMENT 1可用 0停用 ); CREATE TABLE device ( id INT PRIMARY KEY AUTO_INCREMENT, lab_id INT NOT NULL, device_no VARCHAR(32) NOT NULL, device_name VARCHAR(64) NOT NULL, device_type VARCHAR(32), status TINYINT DEFAULT 1 COMMENT 1正常 0报废, CONSTRAINT fk_device_lab FOREIGN KEY (lab_id) REFERENCES lab(id) ); CREATE TABLE course_schedule ( id INT PRIMARY KEY AUTO_INCREMENT, semester VARCHAR(32) NOT NULL COMMENT 学期如2024-2025-1, class_name VARCHAR(64) NOT NULL COMMENT 班级, start_week INT NOT NULL, end_week INT NOT NULL, weekday TINYINT NOT NULL COMMENT 1-7, lab_id INT NOT NULL, course_name VARCHAR(64) NOT NULL COMMENT 实验主题, teacher_id INT NOT NULL, student_count INT NOT NULL COMMENT 上课人数, CONSTRAINT fk_course_lab FOREIGN KEY (lab_id) REFERENCES lab(id) );course_schedule 里特意放 student_count是因为排课时需要拿它和 lab.capacity 比较教师选教室时能看到容量学生人数不能超过容纳人数。status 字段在两个模块里分别表示“实验室是否开放”“设备是否报废”查询时用 status1 过滤而不是直接删除记录这样能保留历史数据。外键在毕业设计项目里建议加上虽然会影响插入顺序但能防止排课表引用一个不存在的实验室。3.3 设备与实验室的增删改查实现public int saveDevice(Device device) { String sql INSERT INTO device(lab_id, device_no, device_name, device_type, status) VALUES(?,?,?,?,?); try (Connection conn DBHelper.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, device.getLabId()); ps.setString(2, device.getDeviceNo()); ps.setString(3, device.getDeviceName()); ps.setString(4, device.getDeviceType()); ps.setInt(5, device.getStatus()); return ps.executeUpdate(); } catch (SQLException e) { throw new ServiceException(保存设备失败, e); } }注意 try-with-resources 的写法Connection 和 PreparedStatement 用完后会自动关闭。这里把异常包装成 ServiceException 抛给上层而不是在 Dao 里 printStackTrace 后返回 0否则 Controller 拿不到失败原因。对应的 Android 端入口就是 DeviceEditActivity用户录入设备编号、名称、类型后点保存Activity 组装 Map 参数调用 HttpUtil.post再根据返回的 JSON 中 status 字段判断是否刷新列表。设备删除不能做成物理删除。常见做法是执行 UPDATE device SET status 0 WHERE id ?把状态置为报废否则某条排课记录已经关联了设备直接删除后列表出现空引用。管理员真正需要删除的场景很少绝大多数是“这台设备不能用了”。如果你在实现中遇到外键约束报错多半就是这里不做软删除导致的。3.4 实验室占用情况的查询SELECT lab.id, lab.lab_no, lab.capacity, (SELECT COUNT(*) FROM course_schedule cs WHERE cs.lab_id lab.id AND cs.semester ? AND cs.start_week ? AND cs.end_week ? AND cs.weekday ?) AS occupied_count FROM lab WHERE lab.status 1这是教师排课前最常用的查询传入 semester、周次、星期统计每个实验室在该时间点被占用几节课。occupied_count 为 0 的实验室可以选结果再按 capacity 过滤确保班级人数不超过容纳人数。注意 start_week ? AND end_week ? 这个区间重叠条件它覆盖了“新课程在课程中途插进来”的情况。如果这里写成 start_week ? OR end_week ?判断会漏掉本周仍然在上的课。4. 18周学期排课与冲突检测把“错开”变成可执行的算法排课功能是这套系统的重头戏。需求里说的是“一个学期一个学期地安排一个学期分为 18 周”所以表里不直接存日期而是存 semester、start_week、end_week、weekday。比如“2024-2025-1 学期软件 2101 班第 3 周到第 8 周每周三下午 5-6 节在 501 实验室上 Java 实验”。weekday 用 1 到 7 代表周一到周日。如果一栋实验室一天有多个时段建议再加一个 period 字段原项目没拆出来改造时可以补上。4.1 排课字段语义字段排课语义示例semester学期标识2024-2025-1start_week / end_week起始周和结束周第3周到第8周weekday星期几3 表示周三lab_id上课地点501实验室course_name实验主题Java实验student_count上课人数45“每个班级的排课需要错开”这句话落到数据结构上就是同一时间点、同一班级不能存在两条排课记录同一时间点、同一实验室也不能被两门课占用。这两个约束在插入前都要检查。一个学期 18 周是业务概念不是数据表特殊性数据库只存周次范围如果是单周课就可以 start_weekend_week3。4.2 排课冲突检测的核心方法public boolean hasConflict(CourseSchedule cs) { String sql SELECT COUNT(*) FROM course_schedule WHERE semester ? AND weekday ? AND start_week ? AND end_week ? AND (lab_id ? OR class_name ?); try (PreparedStatement ps DBHelper.getConnection().prepareStatement(sql)) { ps.setString(1, cs.getSemester()); ps.setInt(2, cs.getWeekday()); ps.setInt(3, cs.getEndWeek()); ps.setInt(4, cs.getStartWeek()); ps.setInt(5, cs.getLabId()); ps.setString(6, cs.getClassName()); ResultSet rs ps.executeQuery(); return rs.next() rs.getInt(1) 0; } catch (SQLException e) { throw new ServiceException(冲突检测失败, e); } }这串 SQL 是关键条件用 AND 连接start_week ? AND end_week ? 判断区间是否重叠参数分别传入新课程的结束周和开始周。后面的 OR 表示只要实验室或班级任意一个已经占用就认为有冲突。你可以拆成两个查询接口一个查教室占用、一个查班级课表但答辩时用一条 SQL 表达清楚会更有说服力。如果只检查 start_week 相等那“第 3 周到第 8 周”和“第 5 周到第 12 周”就检测不出来这是最容易被忽略的边界情况。4.3 批量排课与事务控制教师可能一次录入一个班级整学期的课程或者一次给多个班级排同一门实验课。这种场景不能用单个 insert而要用批量提交外面包事务。否则第二条数据插入失败时第一条已经落库排课表里出现残缺课表。public void batchAdd(ListCourseSchedule list) { String sql INSERT INTO course_schedule( semester, class_name, start_week, end_week, weekday, lab_id, course_name, teacher_id, student_count) VALUES(?,?,?,?,?,?,?,?,?); Connection conn DBHelper.getConnection(); PreparedStatement ps null; try { conn.setAutoCommit(false); ps conn.prepareStatement(sql); for (CourseSchedule cs : list) { ps.setString(1, cs.getSemester()); ps.setString(2, cs.getClassName()); ps.setInt(3, cs.getStartWeek()); ps.setInt(4, cs.getEndWeek()); ps.setInt(5, cs.getWeekday()); ps.setInt(6, cs.getLabId()); ps.setString(7, cs.getCourseName()); ps.setInt(8, cs.getTeacherId()); ps.setInt(9, cs.getStudentCount()); ps.addBatch(); } ps.executeBatch(); conn.commit(); } catch (SQLException e) { try { conn.rollback(); } catch (SQLException ex) { e.printStackTrace(); } throw new ServiceException(批量排课失败, e); } finally { DBHelper.close(conn, ps, null); } }setAutoCommit(false) 后executeBatch 提交的所有 insert 要么全部成功要么全部回滚。批量方法里同样要先调用冲突检测更好的做法是在循环里逐条检查 hasConflict任一冲突就中止并回滚。这个写法比逐条 insert 快也方便在 Controller 里统一返回“第 5 周周三与某班级冲突”的提示。4.4 教师端与学生端的列表场景从项目附带文件看客户端单独做了 CourseTeacherListActivity、CourseStudentListActivity 和 CourseTestTeacherListActivity。教师端打开 CourseTeacherListActivity 时请求 /course/teacherList?teacherIdxx服务器返回 XML 记录集包含自己排过的实验课列表学生端打开 CourseStudentListActivity 时请求 /course/studentList?className软件2101按班级名拉取课表。CourseTestTeacherListActivity 可以看作教师查看实验课对应考核或成绩的入口。Activity 负责发请求和渲染XML 标签与数据字段一一对应。这里有个实现细节学生端不传 userId 而是传 className因为排课表里没有 student 维度而是按班级排课。需求里“学生能查实验时间地点名称”完全可以通过 className 查询实现。如果非要从学生表出发关联班级就需要额外引入班级表复杂度会增加但对几个核心模块没有本质帮助。所以保留“班级名直接出现在排课表”的做法更贴合 18 周排课模型。5. 把源码导入Android Studio编译APK的细节与排错5.1 先分清编译产物与源码从项目文件列表里能看到 resources.ap_、MobileClient.apk、jarlist.cache、HttpUtil.class 等这些是 Eclipse 构建中间产物不是源码。真正要修改的是 src 目录下的 .java 文件和 res 目录下的资源文件。拿到完整源码后在 Android Studio 里新建一个 Empty Activity 项目把 java、res 和 AndroidManifest.xml 覆盖进去再修改 build.gradle 的 minSdk 和 compileSdk 即可。如果只拿到 class 文件那只能用于反编译参考不能直接编译。原资源说明里注明是完整源码所以导入时应直接找 src 目录而不是去改 class。5.2 联调时最容易踩的四个坑配置项常见问题建议做法AndroidManifest.xml没有 INTERNET 权限请求直接报 SocketException在 manifest 里加uses-permission android:nameandroid.permission.INTERNET/服务器地址模拟器访问本机用 10.0.2.2真机用局域网 IP把 ServerHost 写成可配置常量服务器编码XML/JSON 中文乱码Servlet 统一 setCharacterEncoding(UTF-8)JDK/Gradle 版本老项目导入新版 Studio 报 Unsupported major version按项目依赖选择 JDK 8 或 11不要无脑升到 17public class AppConfig { public static final String HOST 10.0.2.2; public static final int PORT 8080; public static final String BASE_URL http:// HOST : PORT /lab/; }模拟器里 10.0.2.2 指的是开发宿主机可以换成局域网 IP 后直接用手机测试。BASE_URL 决定了所有 HttpUtil 请求的前缀例如 /course/teacherList 会被拼成 /lab/course/teacherList调整时只改这个类不要在每个 Activity 里改字符串。5.3 验证排课冲突逻辑是否正确的自测方法把应用跑起来后先用管理员账号添加两个实验室用教师账号给“软件 2101”排第 3 到第 8 周周三再尝试排第 5 到第 12 周周三同一教室预期被拒绝换一个班级排同一教室预期成功。再用学生账号查询该班级课表看 XML 返回的时间地点是否与排课一致。最后把服务器端返回原始字符串打印到 Logcat确认不是 500 页面后再谈解析问题。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门