首页 / 资讯中心 / 文章详情

基于Android的高校教室预约管理系统:从需求到并发冲突处理实战复盘

基于Android的高校教室预约管理系统:从需求到并发冲突处理实战复盘 ★ FEATURED ARTICLE
又到毕业设计答辩季找我聊的计算机专业同学里十个想做系统的有八个会问同一个问题“基于Android的高校教室预约管理系统这种题目到底能不能做出深度”我的回答是能但前提是你别把它做成一个普通CRUD。很多同学以为预约系统就是“发布教室列表 提交预约表单”结果做到中期才发现难点全在审批流转、冲突处理、签到和权限控制上。这篇我把之前带过的一套高校教室预约管理系统完整复盘一遍从需求拆解、技术选型、预约冲突检测、Android端核心实现到踩坑实录尽量写成可以直接“抄作业”的经验尤其适合正在准备这个选题的同学参考。1. 先搞清楚这套系统到底要解决什么需求拆解与功能边界1.1 高校教室预约的现实痛点我在带项目之前先让同学去学校教务那边蹲了一个下午观察真实的借教室流程。结论很简单大部分高校还在用“纸质登记 人工沟通 办公室跑腿”的方式管理教室。学生办社团活动要借教室需要先看课表、找空档、再找管理员确认管理员还得去翻不同课程的占用情况最后手写登记。遇到期末复习周大量空教室没人统一管理学生找不到地方自习而教室管理员也说不清哪间教室此刻到底空不空。“基于Android的高校教室预约管理系统”要解决的不是“把教室列表搬到手机上”这么简单而是要把教室空闲状态、预约申请、管理员审批、扫码签到、违约处罚整条链路全部打通。作为一个毕业设计这个题目的优势在于它天然包含多角色权限、时间段冲突、状态流转、统计报表这些能讲出层次的模块中期答辩和最终答辩都不愁没内容说。1.2 功能边界第一版只做六件事真正动手之前我们列了一个功能边界表明确“做什么”和“不做什么”。模块具体功能面向角色教室信息管理教学楼、楼层、教室编号、容量、类型、设备状态维护系统管理员时段预约按日期和课时段提交预约查看空闲教室学生、教师审批流程预约单提交后由对应管理员审批可驳回并填写原因管理员签到与违约扫码签到、暂离释放、超时未签到自动标记违约学生、教师统计报表教室利用率、预约高峰时段、违约排行管理员系统管理用户管理、角色权限、黑名单、学期设置超级管理员项目开始时很容易犯的错是“什么都想做”自动排课、智能推荐教室、在线缴费、门禁联动。我们把这些全部砍掉。原因很简单第一版要的是闭环从预约到签到再到统计一条链路能完整跑通。毕业设计答辩老师最在意的是你有没有把一个核心问题讲透而不是功能列表有多长。自动推荐、门禁联动这些可以在项目文档的“后续扩展”里写作为思考方向的补充而不是作为第一版范围。当时我是这样和同学说的你这个系统如果能在“同一间教室、同一个时间段、面对两个同时提交的预约请求”时保证只有一个成功就已经超过市面上很多只做了界面的演示项目了。事实证明光这个冲突处理就值得单独写进论文的一章。2. 技术选型毕业设计最稳的组合不是最新而是最合适2.1 Android端为什么选原生Java MVVM而不是跨平台框架很多同学一上来就问能不能用Flutter或者uni-app我的建议很直接题目里明确写了“基于Android”就老老实实做Android原生。这不是排斥跨平台技术而是毕业设计这个场景里原生方案有不可替代的优势你可以讲Activity生命周期、Fragment通信、RecyclerView性能优化、权限动态申请、Service后台任务答辩老师想听的那些Android知识点全都能落到实处。用跨平台框架反而容易陷入“框架替你做完了所有事却说不出底层原理”的尴尬。Android端我们用的是Java。现在Android Studio官方推荐Kotlin但不少高校课程还在教Java再加上后端也是Java同学切换起来成本更低。架构上采用MVVM模式用ViewModel LiveData来承载UI数据网络层用Retrofit OkHttp本地缓存用SharedPreferences存储登录状态和少量配置不引入过于复杂的数据绑定框架。数据绑定对于毕业设计不是必需品反而会增加排错难度。网络接口定义大致是这种风格public interface ApiService { POST(user/login) CallResultLoginVO login(Body LoginRequest request); GET(room/available) CallResultListRoomVO getAvailableRooms(Query(date) String date, Query(slotId) int slotId); POST(booking) CallResultLong createBooking(Header(Authorization) String token, Body BookingRequest request); POST(booking/sign) CallResultVoid signIn(Header(Authorization) String token, Body SignRequest request); }Retrofit的好处是接口定义直观、注解解析成熟、和OkHttp配合方便而且网上案例极多遇到问题很容易搜到答案。对于时间紧张的毕设项目来说可维护性和可查错性比所谓“性能极致”重要得多。2.2 后端与数据库怎么搭才能撑住演示不翻车后端我们用的是Spring Boot 2.x MyBatis-Plus MySQL 5.7 Redis 5.0权限控制用JWT实现无状态认证。Spring Boot的好处是生态完善、默认配置多、启动快MyBatis-Plus比原生MyBatis少写大量重复SQL尤其在分页和条件查询上省时间Redis在这里不是摆设而是真的有用途缓存教室列表、记录预约热度、存储黑名单过期时间都能在答辩时讲出实际意义。为什么不引入Spring Cloud、消息队列这类重量级组件因为教室预约系统的并发量根本到不了微服务的级别。强行引入反而会让答辩变成大型“给自己挖坑”现场老师随便问一个服务治理问题你很难圆回来。毕业设计的技术栈应该遵循一个原则每个组件都能说清楚它解决了什么具体问题而不是为了显得“高级”去堆砌。2.3 数据链路一次预约请求的完整旅程我把一次预约请求的完整链路画成文字流程方便同学理解整个系统在干嘛学生打开App输入学号和密码登录接口返回JWT令牌客户端保存到本地学生选择日期前端调用“查询空闲教室”接口后端先查Redis缓存未命中再查MySQL并将结果回填缓存学生选中教室和时间段提交预约请求请求头携带JWT后端先做JWT鉴权和角色校验确认用户不是黑名单状态后端开启数据库事务先检测该教室在目标时间段的占用情况再尝试插入预约记录预约成功后预约状态为待审批同时发送消息通知到Android端的消息中心管理员在管理端看到待审批列表点击通过或驳回审批通过后学生端收到状态变化推送届时到教室扫码签到。这个链路每个环节都会有同学出问题JWT过期没处理、缓存和数据库不一致、事务没加注解、审批后消息没实时刷新。后面我一个个说。3. 预约冲突检测与状态机全系统最容易出bug的地方3.1 时间片划分与数据库表设计教室预约系统的核心是“时间片”。不是让用户随便填一个起止时间而是把一天切成固定课时段。例如上午1-2节、3-4节、5-6节、7-8节、晚上9-10节共五个时段。每个时段之间有默认的休息间隔这样既能简化冲突检测又贴合高校实际课表。数据库里核心表有三张。教室表存教室基础信息预约表存业务数据时间片占用表负责解决冲突。CREATE TABLE room ( id BIGINT PRIMARY KEY AUTO_INCREMENT, room_no VARCHAR(20) NOT NULL, building VARCHAR(50), floor INT, capacity INT, room_type VARCHAR(20), status TINYINT DEFAULT 1 ); CREATE TABLE booking ( id BIGINT PRIMARY KEY AUTO_INCREMENT, booking_no VARCHAR(32) NOT NULL, room_id BIGINT NOT NULL, book_date DATE NOT NULL, slot_id INT NOT NULL, applicant_id BIGINT NOT NULL, purpose VARCHAR(200), status TINYINT NOT NULL, create_time DATETIME, update_time DATETIME, sign_in_time DATETIME, sign_out_time DATETIME ); CREATE TABLE room_schedule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, room_id BIGINT NOT NULL, book_date DATE NOT NULL, slot_id INT NOT NULL, book_id BIGINT, status TINYINT DEFAULT 0, UNIQUE KEY uk_room_date_slot (room_id, book_date, slot_id) );讲解时用了个生活类比让同学理解房间就像酒店客房日期和时段就像某一天的一个时间窗口。同一间房同一个晚上不能同时订给两拨客人。为了避免“两个人同时下单抢最后一间房”酒店系统不会先查一遍再下单而是在数据库层面用唯一约束保证只有一个订单能成功。room_schedule表上的唯一索引uk_room_date_slot就是这个“一房一晚一单”的数据库约束。3.2 并发冲突唯一约束 事务兜底很多初版方案是这样写的先查询booking表里有没有同教室同时段且状态为已通过的记录没有就插入。这种做法在低并发下没问题但一旦两个请求同时提交两个事务都查不到对方然后都插入成功就产生了冲突预约。答辩老师如果问“你怎么保证并发安全”答不上来就会很被动。正确做法是强制性先占时间片再写预约单。整个流程放在同一个事务方法里Transactional(rollbackFor Exception.class) public Long createBooking(BookingRequest request) { RoomSchedule schedule new RoomSchedule(); schedule.setRoomId(request.getRoomId()); schedule.setBookDate(request.getBookDate()); schedule.setSlotId(request.getSlotId()); try { roomScheduleMapper.insert(schedule); } catch (DuplicateKeyException ex) { throw new BusinessException(该教室该时段已被占用); } Booking booking new Booking(); booking.setRoomId(request.getRoomId()); booking.setBookDate(request.getBookDate()); booking.setSlotId(request.getSlotId()); booking.setApplicantId(request.getApplicantId()); booking.setStatus(BookingStatus.PENDING); bookingMapper.insert(booking); schedule.setBookId(booking.getId()); roomScheduleMapper.updateById(schedule); return booking.getId(); }第一次插入room_schedule时如果唯一索引冲突说明该教室该时段已经有人占用。数据库直接抛出DuplicateKeyException业务层捕获后返回友好提示。这样无论并发多少最终只有一个请求能插入成功。这个方案的关键在于检查动作不在应用层而在数据库索引层从根本上堵住了“先查后插”的并发漏洞。3.3 预约状态机从申请到违约的六种状态预约不是“提交了就完事”它有完整的状态流转。我们设计了五个核心状态外加一个违约标记状态触发动作下一步状态说明待审批学生提交预约已通过 / 已驳回提交后管理员审批已通过管理员点击通过已签到 / 已取消可以取消或到场签到已驳回管理员点击驳回结束结束已签到学生扫码签到已暂离 / 已完成签到后算正式使用已取消用户在开始前取消结束超过截止时间不可取消违约未签到或超时未离场结束计入黑名单统计状态字段全放在后端管理Android端只是展示和触发。每做一次状态变更后端都要校验“当前状态是否允许这个动作”。例如“已驳回”状态不允许再签到“已取消”状态不允许再审批。这种设计可以避免App端被绕过用户就算抓包改了请求后端也能拦住。这里有一个容易遗漏的点取消预约的截止时间。我们规定预约开始时间前1小时允许用户主动取消开始前15分钟内允许签到开始后30分钟仍未签到系统自动将状态改为违约。自动状态变更不能用闹钟实现而是通过定时任务扫描数据库里的“待开始且未签到”的预约记录统一更新状态。毕业设计阶段用Spring的定时任务就够了。4. Android端核心页面实现预约列表、扫码签到与消息提醒4.1 预约列表下拉刷新、分页与加载态Android端最容易被老师打开的就是“我的预约”页面。很多同学只做一个无加载态、无空态、无分页的静态列表演示时一旦没数据就显得很糙。我们的做法是用SwipeRefreshLayout做下拉刷新用RecyclerView展示分页数据页面必须区分“加载中”“加载失败”“暂无数据”三种状态。分页拉取时后端接口设计成标准的分页参数GET(booking/my) CallResultPageResultBookingVO getMyBookings( Header(Authorization) String token, Query(page) int page, Query(size) int size);ViewModel中保存当前页码和是否正在加载的状态下拉刷新时重置页码为1加载更多时页码加1。每次请求前显示底部加载动画请求完成后隐藏。这个交互细节看着不起眼但在答辩演示时很加分因为老师会看到你考虑了真实使用场景而不是只会调接口。我还特意让同学在列表项上显示了预约状态的颜色标签待审批是黄色、已通过是绿色、已驳回是红色、已签到是蓝色、违约是灰色。颜色一出来整个界面的信息层次就清楚了。这个细节在UI评分上比花里胡哨的动画有用得多。4.2 扫码签到ZXing集成与教室二维码约定扫码签到是整个系统最有“物联网感”的功能。我们的方案是每间教室在Room表里维护一个固定的二维码内容内容是以下字符串约定BOOKING:教室编号:日期:时间段学生到教室后用App扫教室门口贴的二维码解析后与当前预约单匹配如果状态为已通过且当前时间在签到窗口内则签到成功状态改为已签到。二维码生成用ZXing库的QRCodeWriterAndroid端扫码也直接集成ZXing的扫一扫模块。这里要提醒的是权限问题从Android 6.0开始相机权限必须在运行时动态申请从Android 10开始目标SDK版本如果较高还需要考虑摄像头权限的精细化说明。我们采用的是在扫码页面进入时申请权限用户拒绝时引导去设置页开启而不是直接闪退。扫码页面还加了一个“手动输入预约码”的入口这个入口不是为了偷懒而是为了应对“教室二维码模糊、光线太暗、模拟器没有摄像头”这些真实场景。每次扫码和手动输入都会在本地埋一条日志方便排错。4.3 消息提醒WebSocket与本地轮询的取舍预约审批通过后学生端需要及时收到通知。这里有两个方向WebSocket长连接和HTTP短轮询。我最终带同学用的是短轮询原因很实际WebSocket虽然实时性强但要处理连接建立、心跳保活、断线重连、前台后台切换等一堆问题稍不留神就成了深坑。教室预约系统的通知频率根本不高审批结果也不是秒级发生的事五分钟一次的轮询完全够用。具体实现是Android端用WorkManager启动一个周期性的后台任务每隔五分钟请求一次“未读消息接口”如果有新消息通过Notification发送系统通知。代码非常简短PeriodicWorkRequest workRequest new PeriodicWorkRequest.Builder(NotifyWorker.class, 15, TimeUnit.MINUTES) .build(); WorkManager.getInstance(context).enqueueUniquePeriodicWork( notify_work, ExistingPeriodicWorkPolicy.KEEP, workRequest);虽然微信等IM场景必须用长连接但毕业设计里能把这个取舍理由讲清楚比盲目堆技术更能体现工程判断力。5. 权限控制与统计报表让管理员真正能管事5.1 三套角色的接口鉴权方案系统里有学生、教师、管理员、超级管理员四类登录身份本质上是三大权限级别。后端没有给每个接口手动写if判断而是用Spring Security的过滤器链统一解析JWT再把角色信息塞进请求上下文。我画了一个简单的角色权限表角色可访问模块说明学生查看教室、提交预约、我的预约、扫码签到、个人违约记录主流程角色教师学生全部功能并额外具备批量预约权限可代班级申请管理员审批预约、修改教室信息、查看统计数据、拉黑用户核心管理角色超级管理员全部功能管理管理员账号和学期设置最高权限接口层的写法很直接在需要限制的接口上加一个自定义注解AOP切面统一判断当前JWT中的角色是否匹配。这样等于把权限判断收敛到了一个地方出现越权问题时只查一处即可。Android端根据登录接口返回的role字段控制页面入口但真正的防越权完全依赖后端这一点在文档里必须写清楚也是答辩老师喜欢追问的点。5.2 管理后台审批、排课导入与黑名单管理端我们没有单独做一个Web系统而是在Android App里设置了一个“管理入口”通过角色字段动态显示。这个做法对毕设来说足够了省去双端开发的成本同时还能体现出你对角色动态路由的理解。审批页面的核心是“批量操作”和“冲突提示”。管理员看到待审批列表后可以通过详情页查看该教室该时段的当前占用情况也可以直接快速通过。审批通过时后端会再次检查时间片是否仍然空闲防止两个管理员同时操作一张单子。这里我们又用到了room_schedule的唯一索引审批时不是简单地把预约状态改为已通过而是先尝试更新room_schedule的book_id和status失败则说明该时段已被其他单子占用审批不能通过。黑名单的规则是违约次数达到3次该用户7天内无法发起新的预约。Redis里存一个黑名单key过期时间设为7天。每次预约提交前先查Redis这是一个值得在论文里单独写小节的功能点。5.3 数据可视化MPAndroidChart画教室利用率统计报表是很多同学习惯性忽略的模块但老师非常喜欢看。原因很简单它直接证明你收集的数据有闭环价值。我们用的是MPAndroidChart这个开源图表库在Android端画了两张图教室周利用率柱状图以及预约高峰时段折线图。柱状图的数据来自后端统计接口。SQL大致是SELECT room_id, COUNT(DISTINCT CASE WHEN status IN (1, 2, 3, 4) THEN CONCAT(book_date, _, slot_id) END) AS used_slots, COUNT(DISTINCT CONCAT(book_date, _, slot_id)) AS total_slots FROM booking WHERE book_date BETWEEN #{startDate} AND #{endDate} GROUP BY room_id;这个统计口径有一点坑预约单即使被驳回也不能算进“教室被占用”所以统计时必须过滤掉驳回和取消状态。但“是否占用”和“是否成功履约”是两个概念时间片被占用了即使后来取消那段时间也不能再给其他人预约。为此统计教室利用率时我建议直接查room_schedule表而不是booking表因为room_schedule才真正记录了“占用”状态跟业务结果无关。这个细节如果不注意图表数据就会偏大或偏小演示时容易被老师一眼看穿。6. 踩坑实录这五个问题几乎每个同学都会遇到6.1 Android 9之后访问不到HTTP接口项目第一次联调同学发现App请求后端接口一直报错“CLEARTEXT communication to ... not permitted by network security policy”。这不是代码逻辑问题而是Android 9API 28开始系统默认禁止明文HTTP流量。后端如果是本地局域网IP或者http协议就会被拦下来。解决办法有两种。一个是在AndroidManifest.xml的application标签里设置application android:usesCleartextTraffictrue ... 另一个更规范的做法是配置网络安全配置文件允许指定域名使用明文。毕设演示阶段直接用第一种最简单但要在文档里说明生产环境应使用HTTPSusesCleartextTraffic只用于开发调试。这个解释一出来反而成了加分项。6.2 后端接收不到LocalDateTimeJSON反序列化格式不一致前端传2025-06-10 14:00:00给后端Spring Boot后端却报错Cannot deserialize value of typejava.time.LocalDateTimefrom String。这个问题很经典因为Spring Boot默认没有注册JavaTimeModule或者默认格式和前端不一致。解决方式是在application.yml里配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8同时在后端实体类的LocalDateTime字段上加上统一格式注解也可以但更建议全局配置避免每个字段都要写一遍。这个坑让人印象深刻因为当时我们前端传的是字符串后端以为是时间戳两边对不上调试了整整一个下午。6.3 并发压测时预约成功的记录莫名消失我们自己写了一个小脚本模拟20个并发请求提交同一间教室的同一时段预约结果发现有几条请求后端返回了“预约成功”但数据库里查不到对应记录。排查之后发现原因有两个一是事务方法内部虽然抛了DuplicateKeyException但外层代码没有捕获该异常Spring的事务回滚把整个业务都回滚了包括已经插入成功的唯一一条记录二是应用层先做了“查询是否有冲突”的预检查预检查通过后进入事务这中间存在并发窗口。修复方案是把“预检查 插入”收拢到一个带有数据库唯一索引的单事务里并显式捕获DuplicateKeyException转换成业务提示。并发冲突检测这种问题靠应用层查库永远有窗口只有让数据库做裁判才可靠。把这个排错过程写进论文里比写八百字原理更有说服力。6.4 模拟器扫码永远识别不了真机一测就崩一开始我们只在Android模拟器里测扫码功能发现模拟器的虚拟摄像头对着电脑屏幕上的二维码怎么都识别不出来。后来换到真机又出现扫码页面黑屏或闪退。排查后发现是两个问题一是模拟器虚拟摄像头的画面源设置不对需要把摄像头设置为模拟场景而不是桌面二是真机上的相机预览尺寸和ZXing分析回调的分辨率不匹配导致某些机型无法正常回调。我们的处理方式是在扫码页面增加“手动输入预约码”调试入口所有二维码都支持数字编码回退输入同时真机崩溃问题通过统一指定预览尺寸为640x480解决。毕设演示最好提前准备一台已经装好App的真机别等到答辩现场才连电脑调试。6.5 数据库时区差8小时统计图表数据对不上统计报表里的预约记录日期总是显示成前一天或者按小时统计时数据整体偏移。问题根因是MySQL连接串里没有指定时区而服务器系统时区是UTC导致数据库读写把时间往后/往前转换了8小时。解决办法是在JDBC连接串里明确指定jdbc:mysql://localhost:3306/classroom_booking?serverTimezoneAsia/ShanghaiuseUnicodetruecharacterEncodingutf8useSSLfalseallowPublicKeyRetrievaltrue另外还有一个细节日期字段如果用DATE类型存的是“日期”不受时区干扰但如果用DATETIME或TIMESTAMP就必须统一时区。我们内部约定所有请求参数里的时间字符串都采用yyyy-MM-dd HH:mm:ss格式后端统一按东八区解析数据库也统一东八区避免出现“App显示正常但报表错误”这种难查的诡异问题。最后说一点个人体会把这个项目从零到一做完之后我最大的感受是毕业设计不怕题目小就怕逻辑闭环断在半路。预约冲刺、审批流转、签到违约、统计可视化这一套闭环跑通之后你会发现自己对Android权限、网络请求、后端事务、数据库索引这些零散知识的理解突然就串成了一条线。如果后面时间富余我个人比较推荐的扩展方向是在room_schedule基础上增加一个“智能推荐教室”的接口根据历史预约数据计算高峰期教室偏好或者把签到记录导出生成课程考勤表。这些扩展都不需要推翻现有表结构直接在原系统上叠加即可。做这类系统别追求一步到位先把主流程做稳再谈优化和花样。
阅读完成 · 觉得有帮助?
咨询建站