ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

高校图书馆自习室座位预约Android APP设计与实现

高校图书馆自习室座位预约Android APP设计与实现 1. 先说痛点为什么高校图书馆的自习室座位总是不够用做这个项目之前我先在校园里蹲了三天。准确说是在图书馆的自习区蹲了三天。早上七点半开馆门口已经排了四五十人八点十分左右一层的自习区基本满座八点半整栋楼的自习座位就剩不了几个。但有意思的是到了上午十点我数了一下靠窗那排二十个座位里至少有五个是空着的桌面上却放着书包、水杯、甚至一包没拆封的薯片。这就是图书馆座位管理最典型的矛盾座位不是不够是分配效率太低。占座成本几乎为零谁来得早谁就能占走了也不用释放其他人就算看到了空位也不敢坐——万一书包的主人只是去接水呢万一人家中午还会回来呢这种不确定性让真正的自习需求没法被满足。我当时和朋友聊这个想法他第一反应是这不就是个抢课系统吗后来又说淘宝上不是有现成的预约小程序吗。这些说法都没错但都忽略了一个关键场景图书馆座位预约不是单纯的抢它要解决的是座位状态的可信度和实时性问题。小程序和网页方案当然能做但考虑到图书馆内部Wi-Fi信号覆盖不均匀、学生手机型号五花八门、还有一部分人压根不习惯装一堆小程序我当时判断做一款原生Android APP是覆盖面最直接、体验最可控的方案。这个项目最终落地成了一套完整的客户端服务端系统Android端负责座位图展示、预约操作、签到倒计时、二维码核验后端负责座位状态管理、预约时段生成、信用分记录。整个项目从需求调研到真机测试前后花了大概两个多月中间踩了不少坑尤其是后台保活、并发预约、状态一致性这几个环节每个都值得单独拿出来讲。这篇文章就把整个项目的设计思路和实现过程完整梳理一遍分成需求、选型、客户端、服务端、踩坑五个部分。适合正在做课程设计、毕业设计的计算机专业同学也适合想了解预约类系统完整实现链路的后端和Android开发者。2. 需求调研学生、管理员和占座党三方博弈任何系统开发之前需求分析都是最重要的一步。我调研了大概30名学生和4位图书馆管理员整理了真实使用场景而不是凭空想象功能。2.1 三类角色的核心诉求我把参与方分成了三类每一类的诉求差异非常大角色核心诉求典型痛点普通自习学生快速找到空座、避免被占座转半天找不到座位看到空位不敢坐占座/长期学习者固定座位、不被中断离开一会儿座位被释放回来没地方坐图书馆管理员座位利用率可统计、管理成本低没法判断哪些座位长期闲置清理靠人工巡逻这三类诉求是互相冲突的。普通学生希望座位释放得快管理员希望有人巡查时能掌握准确数据长期占座那类用户则希望系统别那么死板。最终我确定的规则是预约成功后保留15分钟签到窗口签到后最长可连续使用4小时中途允许暂离两次、每次最多30分钟离开超过时限自动释放座位并扣除信用分。这套规则既照顾了临时需求也给了长期自习用户一定的容错空间。2.2 几个容易被忽略的非功能需求除了功能需求还有几个非功能需求在调研中被反复提及直接影响了后续的技术选型签到网络必须容忍弱网。图书馆地下层、楼梯间附近信号很差如果签到操作必须实时联网用户会疯掉。通知要及时。预约成功、临签到、暂离超时这些节点都不能靠用户自己盯着看系统必须主动推送。座位状态图要一眼能看懂。很多线下考研党不习惯看复杂图表颜色语义必须统一绿色空闲、红色占用、黄色暂离、灰色维护。管理端要能看到统计报表。虽然最初版本只做了简单的预约记录导出但数据库设计时必须预留统计字段否则后期加需求等于重做。这些非功能需求看起来不起眼但每一个都在后续开发中变成了具体的实现约束。比如容忍弱网这一条逼着我给客户端加了本地缓存和重试机制通知要及时这一条直接让我在Android端折腾了两天通知渠道适配。3. 技术选型Android原生、Spring Boot与MySQL的组合逻辑技术选型阶段我做了两轮对比一轮是客户端跨平台 vs 原生另一轮是后端框架和部署方案。3.1 客户端为什么放弃Flutter和Uniapp项目一开始我确实犹豫过要不要用Flutter。调研了一圈Flutter在UI表现力上很强座位状态图这类自定义视图做起来也很顺。但真正开始设计功能时几个问题让我最终回到了Android原生一是后台服务的稳定性。预约系统需要长时间驻留的倒计时服务、签到提醒、状态同步。Android原生的前台服务、通知栏、WorkManager这套体系是经过多年迭代的在国产ROM各种激进省电策略下仍然有一线生机。跨平台框架在后台保活这一块很难做到细粒度适配尤其是遇到MIUI、EMUI、ColorOS这种各自为政的ROM时基本是听天由命。二是二维码和系统级能力的调用。签到核验需要调用相机扫码虽然Flutter也有插件但原生CameraX的自定义扫码框、对焦策略、光线不足时的处理更可控。三是学习和排错成本。项目周期就两个多月我本职是后端开发Android端的知识储备并不算深厚。原生Android出了问题Stack Overflow上的资料最全踩坑成本最低。最终客户端定为Kotlin Jetpack全家桶ViewModel、LiveData、Room、WorkManager Retrofit CameraX。UI方面用了ConstraintLayout为主自定义View画座位图。3.2 后端轻量开局但结构要能扛后端没有选择微服务那一套那就是给自己找麻烦。最终用的是Spring Boot 2.7 MyBatis-Plus MySQL 8.0部署在一台4核8G的云服务器上这个配置对于单校图书馆几千人同时预约的规模来说绰绰有余。数据库层面选了MySQL而不是PostgreSQL说实话没有特别深刻的技术理由主要是团队熟悉、资料多。但我在设计座位表时借鉴了数据库锁的思路这个后面在并发预约一节详细讲。Redis当时没引入。一开始我觉得预约高峰期需要做缓存后来算了一笔账全校自习座位大概2000个预约请求峰值就算每秒200次MySQL加上连接池完全扛得住。引入Redis虽然能提升响应速度但带来了缓存一致性、过期策略、持久化一堆新问题。对于课程设计级别的项目保持简单反而是优势。3.3 整体架构图景系统分三层Android客户端包含用户模块登录、个人信息、信用分、座位模块实时座位图、预约操作台、签到模块倒计时、二维码展示/扫码、消息模块通知接收。后端服务RESTful API 定时任务处理超时释放、生成每日预约时段 统一的异常处理与返回码。数据层用户表、座位表、预约记录表、违规记录表、时段配置表。这里要特别强调一下Android端的包结构。我之前见过很多同学的项目所有代码堆在几个Activity里两三千行一个文件后面改需求时痛不欲生。这个项目的包结构按功能模块划分每个模块内部再按MVVM分层com.example.libraryseat ├── data/ // 数据层网络请求、本地数据库、仓库 │ ├── local/ │ ├── remote/ │ └── repository/ ├── ui/ // 视图层Activity、Fragment、Adapter │ ├── login/ │ ├── seat/ │ ├── reserve/ │ └── profile/ ├── service/ // 后台服务倒计时、通知 └── utils/ // 工具类这个分层看起来多花了一点功夫但后期调试问题的时候定位速度完全不一样。4. 客户端核心模块座位图、预约状态机与签到倒计时客户端是整个项目里工作量最大的部分前前后后写了大概一万行代码。这里挑三个最有代表性的模块详细讲实时座位图、预约状态机、签到倒计时的后台保活。4.1 实时座位图用自定义View还是WebView座位状态图是整个APP的门面。最初我考虑过直接加载HTML的Canvas方案用WebView展示开发速度快但交互体验和原生差距明显下拉刷新、点击响应、缩放流畅度都差一截。最终选择了自定义View实现。核心思路把图书馆的物理座位布局抽象成一个坐标系每张座位用一个矩形区域表示读取后端的座位状态列表在onDraw里根据状态填充不同颜色并监听onTouchEvent判断点击位置。这里有一个简单有效的做法——用座位ID作为Key把点击坐标命中测试转换成座位ID而不是用复杂的图形学算法。class SeatMapView JvmOverloads constructor( context: Context, attrs: AttributeSet? null ) : View(context, attrs) { private val seatRects mutableMapOfInt, RectF() // 座位ID - 矩形区域 private val seatStatus mutableMapOfInt, Int() // 座位ID - 状态 private var scale 1.0f override fun onDraw(canvas: Canvas) { super.onDraw(canvas) seatRects.forEach { (seatId, rect) - val paint Paint().apply { color when (seatStatus[seatId]) { STATUS_FREE - Color.parseColor(#4CAF50) STATUS_OCCUPIED - Color.parseColor(#F44336) STATUS_TEMP_LEAVE - Color.parseColor(#FFC107) STATUS_MAINTENANCE - Color.parseColor(#9E9E9E) else - Color.parseColor(#4CAF50) } style Paint.Style.FILL } // 座位矩形加上圆角视觉上更接近实体桌面 canvas.drawRoundRect(rect, 12f, 12f, paint) } } }座位图上的缩放和平移用Matrix处理手势监听用ScaleGestureDetector。这个模块踩的一个坑是坐标系转换View的宽高和实际布局的宽高在onMeasure之后才确定如果在init块里初始化座位矩形坐标拿到的是0值。必须在onSizeChanged里做一次初始化或者用View.post {}延迟到布局完成后再算。座位状态的下发策略也值得一提。最开始我用的是定时轮询每10秒拉一次全量状态高峰期流量大且状态更新有延迟。后来改成全量增量结合进页面先拉一次全量之后通过WebSocket或者轮询接口拉取增量变更。考虑到后端不复杂最终保留了轮询但把轮询周期拉长到15秒同时增加了一个预约成功后立即局部刷新的本地逻辑。用户体验上基本无感。4.2 预约状态机为什么不能简单用可约/不可约两个状态座位预约最容易被新手搞砸的地方是状态设计得太简单。很多初版方案就是空闲/占用两个布尔值结果一上线就出问题用户预约了但没来签到座位一直显示占用用户中途去吃饭座位被释放但人回来了用户提前结束自习座位状态没有同步更新。我最终设计了一个五状态机状态含义触发条件FREE空闲可约无预约记录或上一条记录已结束RESERVED已预约待签到用户提交预约系统锁定座位OCCUPIED使用中用户扫码/点击签到成功TEMP_LEAVE暂离中使用中用户申请暂离EXPIRED已释放签到超时或暂离超时座位解锁状态流转全部由服务端控制客户端只负责展示和发起操作。每个状态变更都生成一条记录写入预约历史表这样管理员查询用户行为时能看到完整的时序链。这里有一个非常重要的细节从RESERVED到FREE的超时释放不能靠用户端触发。用户把APP杀掉、手机没电、人走了客户端不可能主动通知服务器我超时了。所以释放逻辑必须放在服务端的定时任务里。我用的方案是Spring Boot的Scheduled注解每30秒扫描一次预约表找出所有reserve_time 15分钟 now且状态仍为RESERVED的记录批量置为EXPIRED。批量更新的SQL要带状态条件防止误释放刚签到的记录UPDATE reservation SET status EXPIRED, release_time NOW() WHERE status RESERVED AND reserve_time DATE_SUB(NOW(), INTERVAL 15 MINUTE)这个定时任务上线前我反复确认了一个问题服务器时间和手机时间不一致怎么办答案是不让客户端传时间所有时间判断一律以数据库时间为准。客户端只传操作指令不传时间戳避免各种时区和时钟偏差问题。4.3 签到倒计时通知渠道、前台服务与国产ROM的博弈签到环节的体验直接影响用户会不会卸载APP。流程是这样的用户预约成功后客户端启动一个15分钟倒计时在通知栏常驻显示剩余14分32秒后签到截止倒计时归零前5分钟和归零时各推送一次提醒。这里的技术难点不在倒计时本身——一个CountDownTimer就能解决——而在于Android系统的后台限制。从Android 8.0开始后台应用不能随意启动前台服务从Android 10开始后台读取定位等敏感操作受限各厂商ROM还会在用户杀掉APP后把服务和通知一并清掉。我的处理方案分三层第一层使用前台服务startForegroundService承载倒计时并绑定一个常驻通知。这样系统会认为这个服务是用户可见的在进程回收时优先级较高。第二层针对国产ROM的自启动管理后台耗电管理设置我做了适配引导页。检测到小米MIUI、华为EMUI/HarmonyOS、OPPOColorOS、vivoFuntouchOS时弹出说明页引导用户开启允许后台运行和允许自启动。这一步是纯经验积累Android原生系统上没有这些设置只能在代码里判断Build.MANUFACTURER然后跳到对应的设置页。第三层使用WorkManager作为兜底。就算前台服务被杀了WorkManager的持久化任务在下次APP被拉起时也能恢复状态——客户端启动后先检查本地数据库里有没有未完成签到的预约记录有的话立即恢复倒计时并重新创建通知。这算是一个主动恢复机制不再依赖系统的后台存活能力。实际测试下来这套三层方案在主流机型上的表现还不错。倒计时漏报率大约在5%以内主要集中在用户主动划掉最近任务的情况——这种场景下APP自身无解只能靠系统级的长按菜单恢复。如果你也在做类似功能我强烈建议把本地数据库持久化启动时状态恢复作为最后防线而不是把宝全押在服务保活上。5. 服务端设计接口约定、数据库锁与定时任务的三重保障服务端虽然代码量不如客户端但在数据一致性上的设计复杂度远超客户端。这一章讲三个核心问题接口怎么定、并发预约怎么防超卖、定时任务怎么写才不踩坑。5.1 RESTful接口定义与统一返回结构接口设计的原则是语义清晰、状态码统一、失败信息可读。整个项目定义了大约20个接口核心接口如下接口方法说明/api/user/registerPOST学号密码注册/api/user/loginPOST登录返回token/api/seat/listGET获取楼层座位状态列表/api/seat/reservePOST预约指定座位/api/seat/checkinPOST签到支持二维码扫码/api/seat/temp-leavePOST申请暂离/api/seat/releasePOST主动释放座位/api/reservation/historyGET预约历史/api/user/credit-scoreGET信用分查询统一返回结构用了最常见的code message data三件套成功时code为0失败时code为业务错误码。这里我定义错误码时踩过一个坑直接把HTTP状态码和业务码混在一起用结果客户端判断逻辑一团糟。后来明确规定HTTP状态码只表达网络和请求层面的问题200、401、500业务错误一律用返回体里的code表达比如1001表示座位已被预约1002表示今日预约次数已用完1003表示信用分不足。这样客户端拦截器只需要处理401继续登录其他错误码都走到业务逻辑层去判断。5.2 并发预约防止同一座位被两个人抢到座位预约的核心难点是并发控制。想象这个场景A和B同时看到5号座位是空闲的两个人同时点击预约。如果代码逻辑是先SELECT判断状态再UPDATE座位状态两个请求都能通过SELECT看到空闲状态然后先后执行UPDATE——最终结果是5号座位被两个人预约系统数据直接错乱。解决这个问题有三个层级我最终选了第二层第一层是悲观锁SELECT * FROM seat WHERE id ? FOR UPDATE锁住这一行直到事务提交。实现简单但在高并发下会产生锁等待而且需要事务包裹整个预约流程性能一般。第二层是条件更新CAS思路用一条UPDATE语句把座位状态从FREE改成RESERVED作为原子操作如果影响行数为1说明抢占成功否则说明该座位已经被别人预约。UPDATE seat SET status RESERVED, version version 1 WHERE id #{seatId} AND status FREE这条SQL由数据库的锁机制保证原子性不需要显式加锁性能好代码也简洁。影响行数为1才继续插入预约记录否则直接抛座位已被预约的异常。第三层是Redis分布式锁适合多实例部署的场景。单机部署用不上徒增复杂度。我用JMeter做了并发测试500个线程同时抢100个座位最终预约成功数恰好是100没有一例超卖也没有一例漏抢。条件更新方案在MySQL默认隔离级别下表现稳定。5.3 定时任务超时释放与每日时段的生成服务端有好几个定时任务最核心的是两个座位超时释放和每日预约时段初始化。超时释放的逻辑前面讲过用的Scheduled(fixedRate 30000)每30秒跑一次。这里要注意的是如果定时任务执行时间超过了间隔时间Spring默认会等到上一次执行完再开始下一次不会并发执行这点不用担心。但为了保险起见我在方法上加了一个基于数据库标记位的轻量级分布式锁——用一个配置表记录上次任务是否还在执行防止在极端情况下出现重叠执行。每日时段初始化是个容易忽略但必须做的任务。座位系统的预约时段不是24小时全天开放的而是按图书馆开馆时间比如8:00-22:00切成多个时段。我设计的时段是每4小时一个上午场8:00-12:00、下午场12:00-16:00、晚间场16:00-22:00。用户在晚上8点可以预约次日的任意时段。初始化逻辑是每日凌晨0点检查未来7天的时段配置是否存在不存在则批量插入。这里有一个隐蔽的bug如果定时任务当天挂了用户第二天打开APP会发现未来7天没有可预约时段所有预约入口直接404。所以我在查询时段配置的接口里加了一个兜底逻辑查不到配置时自动调用生成方法现场创建。这个兜底逻辑后来还真救了一次——一次服务器磁盘满了导致定时任务没跑但用户侧完全无感。6. 二维码功能与签到核验的实现细节二维码是整个签到链路里最有仪式感的部分也是让用户觉得这个APP不是山寨货的关键功能。这里分两个方向讲用户端生成二维码供管理员扫描以及用户扫描座位上的二维码完成签到。6.1 用户端二维码动态令牌而非用户ID最初版本我直接把用户学号放进二维码里管理员扫一下就能识别。结果测试时发现问题二维码被截屏转发别人也能冒充签到。后来改成了动态令牌方案。每次签到会话开始时服务端生成一个一次性token包含过期时间5分钟有效并把token关联到预约记录ID。二维码内容就是这个token管理员扫码后把token传到服务端兑换服务端校验token有效性和预约记录状态后完成签到。生成二维码用的是ZXing库核心代码很简单val barcodeEncoder MultiFormatWriter() val bitMatrix barcodeEncoder.encode( token, BarcodeFormat.QR_CODE, 400, 400 ) val bitmap Bitmap.createBitmap(400, 400, Bitmap.Config.RGB_565) for (x in 0 until 400) { for (y in 0 until 400) { bitmap.setPixel(x, y, if (bitMatrix[x, y]) Color.BLACK else Color.WHITE) } }这里有一个坑ZXing生成白色背景时如果直接在白色底图的ImageView上显示扫码时识别率会下降因为二维码的静区四周留白太小。我生成的二维码边距设了20像素并且放在白色CardView上保证静区充足。6.2 扫码签到CameraX与连续识别用户扫码签到的场景是每个座位旁边贴一个二维码内容为座位ID用户APP内扫一扫完成签到。CameraX的ImageAnalysis配合ZXing的QRCodeReader可以实现连续帧识别。需要注意两点一是分析回调的线程不能做耗时操作。ZXing识别一帧大约需要10-30毫秒如果直接在主线程分析会卡顿必须放在分析器自带的后台线程里。用ProcessCameraProvider绑定ImageAnalysis时设置setBackpressureStrategy(STRATEGY_KEEP_ONLY_LATEST)确保分析器只处理最新帧。二是识别框外区域的裁剪。CameraX默认分析整帧图像如果用户只把镜头对准二维码一角识别率会很低。我的方案是让用户在UI上对准一个方框然后把方框区域对应的imageProxy裁剪后再交给ZXingval cropRect Rect( (image.width * 0.25f).toInt(), (image.height * 0.25f).toInt(), (image.width * 0.75f).toInt(), (image.height * 0.75f).toInt() ) imageProxy.setCropRect(cropRect)这个优化把识别速度提升了大概40%尤其是光线一般的情况下效果明显。6.3 管理员端扫码枪还是手机摄像头管理员端我做了两个入口一个是用管理员手机登录后用摄像头扫描学生展示的二维码另一个是兼容USB扫码枪的方案——扫码枪本质上是模拟键盘输入焦点在输入框时直接填入字符串后自动回车。我只需要在管理员端的扫码签到页面隐藏一个输入框并监听回车事件就能兼容两种硬件方案。这里有个实际的运维经验很多图书馆其实更愿意用扫码枪因为手机摄像头在连续扫码时手累、对焦慢而扫码枪即扫即出体验远优于手机。系统从设计上兼容扫码枪是管理员愿意长期使用的关键因素。7. 真机测试与踩坑实录这些坑你大概率也会遇到项目从能跑到好用之间隔着一堆真机问题。这里把印象最深的几个坑完整记录下来基本都是常规文档里查不到的。7.1 通知渠道适配中国ROM的通知默认关闭陷阱Android 8.0引入通知渠道后我第一时间创建了IMPORTANCE_HIGH的通知渠道自测没问题。结果发给同学测试时两台小米手机一台华为手机都收不到签到提醒。查了一圈发现这些手机的系统设置里新安装APP的通知权限默认是关闭的必须手动开启。我加了权限引导页不算完更狠的坑是通知渠道的importance一旦创建就不可修改。如果用户第一次运行APP时渠道被系统强制降级后续代码里再怎么setImportance(IMPORTANCE_HIGH)都没用。这个问题的解决方案是在创建渠道后检查NotificationChannel.importance如果不在期望值提示用户去通知设置页手动调整或者干脆用不同的channelId重建渠道。7.2 时间显示时区与格式化的一堆小坑预约历史列表的时间格式化我用的是SimpleDateFormat(yyyy-MM-dd HH:mm)测试时发现服务器返回的2025-01-15T10:30:00被解析成了当地时间的凌晨。原因很简单后端返回的是ISO 8601格式带时区的字符串但后端把时间按UTC存储前端直接用本地时区解析相差8小时。最终的方案是后端统一返回long类型的时间戳毫秒客户端用java.time.Instant.ofEpochMilli(timestamp).atZone(ZoneId.systemDefault())转成本地时间。这个改动虽然简单但避免了所有时区歧义。如果你在项目里见过时间显示少了8小时的bug八成就是时间戳和字符串格式混用导致的。7.3 座位图在低端机上的渲染卡顿有台测试机是几年前的老款千元机打开座位图页面时帧率掉到个位数。排查后发现原因有两个一是每次onDraw都创建新的Paint对象这是大忌。正确做法是把Paint定义为成员变量初始化一次onDraw里复用。二是座位数量多一栋楼几百个座位每帧都重新计算矩形圆角路径开销太大。优化方案是座位矩形区域在onSizeChanged时预先计算好并缓存到ListPairRectF, PathonDraw里只做绘制不做计算。优化后低端机帧率恢复到50fps以上肉眼基本感觉不到卡顿。7.4 弱网环境下的请求失败与重试前面提到图书馆地下一层信号差预约请求很容易超时。Retrofit配合OkHttp的RetryOnConnectionFailure默认只会重试连接层但不会重试已经发出的HTTP请求。我封装了一个RetryInterceptor对幂等请求查询类、预约类做最多3次重试每次间隔500ms指数退避。这里必须强调释放座位、签到这类请求不能盲目重试否则可能出现重复操作导致状态错乱。我的做法是给每次请求生成一个requestId服务端根据requestId做幂等去重——同一requestId请求两次只处理一次。这个幂等设计其实是整个系统里最值得借鉴的一点。预约、签到、释放这些操作一旦用户点了客户端会认为已发送但网络层可能失败重试如果服务端不加幂等保护用户可能被扣两次信用分、或者收到两条签到成功的通知。8. 实测数据与运营层面的经验复盘项目在内部测试阶段跑了三个星期邀请了50名同学实际使用这里放一些真实数据和复盘结论。8.1 使用数据指标数据注册用户数327人测试期间日均预约请求约1200次签到成功率97.6%剩余失败来自网络超时和用户设备问题座位释放超时率约6.3%每天约120个预约超时未签到或未按时返回平均响应时间230ms预约接口浅层慢查询优化后并发预约测试500线程抢100座无一超卖签到成功率97.6%这个数据看起来不错但拆解后发现失败的2.4%里有约一半是用户已经在图书馆现场但手机网络差导致签到请求超时。这说明弱网优化做得还不够彻底后续可以考虑增加离线签到令牌方案——用户预约成功时本地预生成一个签到凭证允许在断网状态下出示给管理员由管理员端联网上传。这是从实际数据里发现的需求纯靠脑补是想不到的。8.2 信用分规则到底扣多少才合理信用分规则是我反复调参的地方。初始方案是超时未签到扣5分暂离超时扣3分主动释放不扣分。每位用户初始100分低于60分禁止预约3天。测试中发现一个bug有些用户预约后手机没电关机了根本不知道有预约醒来就被扣分体验极差。后来加了一个容错扣除信用分前先检查该用户的历史信用分如果此前30天内没有被扣过分第一次违规只警告不扣分。这条首次违规豁免规则让用户投诉率下降了很多。做这类系统一定要记住规则是给用户服务的不是用来惩罚用户的容错比惩罚更能提升长期使用意愿。8.3 图书馆管理员端的反馈管理员实际使用后的反馈集中在两点一是希望看到实时的座位热力图来辅助巡查二是希望导出Excel报表。第一个我做了简化版——按区域统计占用率用不同颜色的色块标记第二个因为数据库设计时已经在reservation表里存了library_id、floor、seat_no这些冗余字段导出报表只需要一条GROUP BY查询工作量不大。管理员提到的另一个细节让我印象很深他们希望某个座位被预约后在预约开始前30分钟系统自动把座位锁死这样他们巡检时看到锁死状态的座位就知道不用清理了。这个需求本质上是对已有状态机的扩展——给座位增加一个PRE_LOCK状态。从这点可以看出真实运营场景中的状态需求永远比开发初期的设计复杂。9. 如果重做一遍我会改变什么项目收尾阶段我做了一次复盘有几个决定如果重开会直接换掉。一是数据库表设计。当时为了省事把座位和座位区域放在同一张表里用area_code字段区分。后期做区域统计时SQL写得很别扭还出现了区域名变更遗漏更新的问题。正确做法是一开始就拆成area表和seat表用外键关联。二是通知系统。我用的是自研的本地通知轮询方案没有接厂商推送。测试阶段人数少没暴露问题但如果真上架几百人同时在线轮询的频率和通知的及时性都会成为瓶颈。重做的话我会直接用腾讯云推送或者极光推送这类成熟服务把精力集中在业务逻辑上。三是座位图的维护成本。自定义View虽然流畅但每次图书馆物理座位调整比如新增一排桌子都需要改代码里的座位坐标配置并重新发版。更优雅的方案是把座位坐标作为数据存到后端管理员在管理端通过拖拽配置客户端动态加载。这个功能我当时想做但没时间实际使用中已经出现了新座位在APP上看不到的情况只能临时改配置发版体验很糟糕。四是缺少Web管理端。当时觉得有个简单的管理页面就够了实际使用后发现管理员需要频繁查看预约记录、处理用户申诉手机上操作表格效率太低。如果重做我会用一个轻量级的Vue3Vite搭建管理后台复用现有API工作量大约一周但管理员的满意度会大幅提升。这些复盘结论其实比项目本身更有价值。做任何系统第一版都不可能完美关键是留好扩展空间比如数据库字段的可扩展性、状态机的可配置性、UI层的数据驱动。哪怕重构一次也比推倒重来强。最后分享一个开发过程中的小建议这类预约系统一定要从第一天就坚持服务端为状态权威的原则。所有状态变更都走服务端客户端只做展示和操作请求不要相信任何客户端本地状态。我在开发过程中有两次偷懒想用本地数据库暂存状态来减少网络请求结果都引发了数据不一致——一次是用户AB两个手机登录同一账号看到不同状态另一次是释放座位后本地状态没同步导致重复预约。这两次教训让我深刻体会到分布式系统里状态权威唯一是铁律碰都不能碰。如果你正在做类似的预约类项目或者打算用这个题目做毕业设计希望这篇文章能把你在需求分析、状态机设计、并发控制、Android保活这些关键环节上少走弯路。自习室座位预约看起来是个小项目但它把移动端开发、服务端并发、定时任务、用户体验全部串联起来了做完一遍你对整个软件开发链路的理解会上升一个台阶。
返回列表