ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于Android的图书馆自习室座位预定APP设计与实现

基于Android的图书馆自习室座位预定APP设计与实现 刚做完一个基于Android的图书馆自习室座位预定APP趁热把设计和实现过程捋一遍。这个项目的场景很简单高校图书馆座位紧张很多学生靠“人肉占座”抢位置管理员清理起来又麻烦又容易起冲突。APP要解决的就是座位资源的在线预约、状态实时可见、到馆签到和超时释放让占座变成“先约先得”减少无谓的排队和争吵。整个项目包含Android客户端、数据库设计和接口联调虽然规模不大但该踩的坑一样没少。这篇文章会把需求分析、技术选型、数据库设计、核心模块实现、UI细节和调试心得全部摊开讲适合正在做类似课设或想练原生Android开发的同学参考。1. 项目概述与需求定位1.1 这个APP到底在解决什么具体问题图书馆自习室的座位管理本质是“稀缺资源分配”问题。每天早晨开馆前门口排长队开馆后大家冲进去放本书占座到了中午有人出去吃饭座位仍然被书包占着想学习的人找不到位置。管理员如果强行收书又容易引发矛盾。所以这个APP的核心价值不是“做一个漂亮的界面”而是把座位资源变成可查询、可预约、可释放的数字化资源。具体来说需要实现这样几个能力用户能查看某一天、某个时段、某个楼层的座位占用情况。用户能选择空闲座位提交预约预约成功后这个座位在该时段内被锁定。用户到馆后可以签到签到后座位使用权归用户如果不签到超过规定时间自动释放。用户能主动取消预约或者结束当前使用。管理员能看到所有预约记录并对恶意占座、超时未签到的记录进行清理。这套流程走通之后占座行为就变成了有约定、有记录、可追溯的预约行为。哪怕偶尔有人迟到系统也会按规则释放座位管理员只需要处理少数异常情况。1.2 功能边界与MVP版本取舍我在设计之初也差点把功能铺得过大比如想做在线支付、积分系统、图形化座位热力图、消息推送。后来发现对于一次课程设计或者一个中小型项目来说这些都不是必选项。MVP版本应该只保留最核心的一条链路注册登录 → 浏览座位 → 预约 → 签到 → 取消/释放。具体功能优先级我列了一张表开发时按这个顺序推进优先级功能模块说明P0登录注册学号/工号作为唯一身份标识P0座位大厅按日期、时段、楼层筛选座位P0提交预约选择空闲座位生成预约记录P0我的预约查看预约记录支持取消P0签到/释放到馆签到使用结束主动释放P1管理员界面查看全场状态强制释放座位P1签到超时自动释放定时任务扫描超时记录P2收藏常用座位方便经常在同一区域学习的用户P2二维码签到扫描桌贴二维码快速签到P2功能我建议放到二期再做因为二维码签到需要预先打印桌贴并采集座位编码会牵扯到线下物料准备不是单纯写代码能解决的问题。MVP版本先用“到馆点击签到”按钮配合GPS或WiFi判断是否在图书馆附近虽然精度一般但逻辑上足够闭环。1.3 目标用户与使用场景拆解这个APP的使用者主要有两类学生/教师以及图书馆管理员。普通用户的使用场景可以拆成四个出门前在宿舍打开APP看一下上午三四节时段有没有空位有就预约。路上确认预约成功顺便看看目标区域在几楼、哪个门进最近。到馆签到后安心坐下不用再翻包找校园卡。离开用完点击释放座位给下一位同学腾出资源。管理员的使用场景则集中在高峰期查看哪些座位被预约但人没来哪些用户频繁爽约必要时强制释放。功能拆到这里结论就清晰了项目的技术核心不是复杂的算法而是“座位状态的实时准确”和“预约记录的可靠保存”。只要这两点做到位APP就成功了80%。2. 技术选型与工程搭建2.1 为什么选择Android原生开发而不是小程序或跨端方案项目标题里明确写了“基于Android”所以原生开发是主线。我在规划时也认真对比过微信小程序、Uniapp和Flutter最终仍然选择Android原生理由有三点。第一原生能更自然地使用系统能力。比如座位预约经常需要检查定位权限、发送签到通知、读取日历提醒。原生API对这些能力的控制粒度最细权限流程也最规范。第二数据同步和后台任务更可靠。图书馆场景容易出现弱网环境原生App可以利用WorkManager、AlarmManager做定时任务比如超时释放提醒。小程序受到平台限制做这类长周期任务会比较别扭。第三学习价值高。如果目的是完成课设或者给简历增加项目经验Android原生项目能更清楚地展示Activity、Fragment、RecyclerView、Room、网络请求这些核心知识点。跨端框架虽然开发快但面试时容易被追问底层原理反而难以深入。当然原生开发的缺点也明显开发周期稍长需要适配不同屏幕和Android版本。对于这个项目来说可控的代价换来的是稳定性和学习深度我认为是划算的。2.2 技术栈与依赖配置整个项目的技术栈我控制在常见范围内不追求新潮主打稳定模块技术选型说明开发语言Kotlin官方推荐空安全特性减少崩溃IDEAndroid Studio建议使用稳定版不用预览版UI框架XML Layout RecyclerView传统View体系资料多、排查容易网络层Retrofit OkHttp处理登录、预约接口数据持久化Room本地缓存用户信息和预约记录异步处理ViewModel LiveData避免内存泄漏生命周期友好本地存储SharedPreferences保存登录Token和用户信息依赖注入Koin轻量适合小项目快速搭建图片加载Coil加载座位示意图体积小build.gradle里的核心依赖可以这样配置dependencies { implementation androidx.core:core-ktx:1.10.1 implementation androidx.appcompat:appcompat:1.6.1 implementation com.google.android.material:material:1.9.0 implementation androidx.constraintlayout:constraintlayout:2.1.4 implementation androidx.room:room-runtime:2.5.2 kapt androidx.room:room-compiler:2.5.2 implementation androidx.room:room-ktx:2.5.2 implementation com.squareup.retrofit2:retrofit:2.9.0 implementation com.squareup.retrofit2:converter-gson:2.9.0 implementation com.squareup.okhttp3:logging-interceptor:4.11.0 implementation org.koin:koin-android:3.4.0 implementation androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1 implementation io.coil-kt:coil:2.4.0 }版本号不需要刻意追新能用、稳定、网上遇到问题能搜到解法才是关键。比如我用Retrofit 2.9.0是因为它是2.x的稳定版本虽然3.x已经发布但迁移成本对这个项目没必要。2.3 工程目录结构与基础架构项目采用MVVM模式包结构如下com.example.libraryseat/ ├── data/ │ ├── db/ // Room数据库实体、DAO │ ├── model/ // 数据模型User, Seat, Reservation等 │ ├── repository/ // 数据仓库屏蔽数据来源 │ └── remote/ // Retrofit接口定义与网络数据源 ├── ui/ │ ├── login/ // 登录页面 │ ├── main/ // 主页面Activity │ ├── seat/ // 座位大厅包含筛选和RecyclerView │ ├── reservation/ // 我的预约 │ └── admin/ // 管理员页面 ├── utils/ // 日期格式化、状态映射等工具 └── App.kt // Application入口初始化Koin初学者容易犯的错是把所有代码堆在Activity里短期内很方便但一旦增加预约记录、管理员界面Activity就会膨胀到上千行。MVVM模式配合LiveData可以让界面只负责显示数据业务逻辑放在ViewModel中数据源变化时界面自动刷新逻辑清晰很多。实际上如果只做课设不引入Koin这种依赖注入框架也可以。直接在Application里写一个ServiceLocator作为容器虽然看起来“土”但胜在直观。我在这篇文章里用Koin是为了让代码结构更接近工业级项目同时避免手写单例导致测试困难。2.4 Android Studio环境准备环境版本上建议使用Android Studio 2023.2及以上版本编译SDK版本用34minSdk用23覆盖Android 6.0到最新版本targetSdk用34。minSdk选择23有一个考虑Android 6.0引入了运行时权限机制如果minSdk低于23处理定位、存储权限时会多出不少兼容分支。而当前绝大多数在校学生的手机都在Android 8.0以上所以minSdk 23既够用又不让代码变得复杂。初始化工程时记得在gradle.properties中启用AndroidXandroid.useAndroidXtrue android.enableJetifiertrue第二个属性主要用于兼容老库如果我们一开始就全部使用AndroidX库可以不开jetifier。但是勾上它能在老旧第三方库上省去很多麻烦所以我通常保持它开着。3. 数据库与数据模型设计3.1 核心表结构规划座位预约系统的数据模型可以拆成几张表表之间尽量不要有冗余字段避免更新时出现数据不一致。我设计的基础表包括user表用户基础信息。CREATE TABLE user ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_no TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, name TEXT NOT NULL, role INTEGER NOT NULL DEFAULT 0, -- 0普通用户 1管理员 created_at INTEGER NOT NULL );floor表楼层信息用于座位大厅的筛选。CREATE TABLE floor ( id INTEGER PRIMARY KEY AUTOINCREMENT, building_name TEXT NOT NULL, floor_number INTEGER NOT NULL, seat_count INTEGER NOT NULL );seat表座位主数据。一个座位属于一个楼层有一个编号比如A101-32。CREATE TABLE seat ( id INTEGER PRIMARY KEY AUTOINCREMENT, floor_id INTEGER NOT NULL, seat_no TEXT NOT NULL, seat_type INTEGER NOT NULL DEFAULT 0, -- 0普通 1靠窗 2电源 3安静 status INTEGER NOT NULL DEFAULT 0, -- 0可用 1已约 2维护 UNIQUE (floor_id, seat_no) );reservation表预约记录也是整个系统的核心表。状态字段需要完整表达预约的生命周期。CREATE TABLE reservation ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, seat_id INTEGER NOT NULL, date TEXT NOT NULL, -- 预约日期格式 yyyy-MM-dd time_slot INTEGER NOT NULL, -- 时段编号比如 1表示08:00-10:00 status INTEGER NOT NULL, -- 0待签到 1已签到 2已结束 3已取消 4爽约 created_at INTEGER NOT NULL, expired_at INTEGER NOT NULL, checked_at INTEGER );time_slot表定义全天有哪些时段。CREATE TABLE time_slot ( id INTEGER PRIMARY KEY AUTOINCREMENT, slot_index INTEGER NOT NULL UNIQUE, start_time TEXT NOT NULL, end_time TEXT NOT NULL );签到记录表可以先不做因为在reservation表里已经有复选框字段。但如果将来要统计“用户来了又马上走”等行为单独建一张签到日志表会更方便。MVP阶段保留设计的余地就好。3.2 座位状态与预约冲突检测座位本身有一个静态状态可用/维护中。而“是否被预约”是某个时间段下的动态状态。同一个座位在早上是空闲的在下午可能已被约。所以座位页面展示时必须把“日期 时段 座位”三者联合查询。判断一个座位是否可约SQL核心逻辑如下SELECT COUNT(*) FROM reservation WHERE seat_id ? AND date ? AND time_slot ? AND status IN (0, 1);status为0表示待签到1表示已签到。只要存在这两种状态之一说明这个座位在对应时段已被占用不能再预约。用户取消预约后status变成3座位立刻释放。这里有个关键细节同一个用户也不能在同一时段预约两个座位。所以在提交预约前除了查询座位冲突还要查询用户是否已有预约。SELECT COUNT(*) FROM reservation WHERE user_id ? AND date ? AND time_slot ? AND status IN (0, 1);这两个查询看起来简单但并发场景下容易出现双人同时抢座。解决办法是给数据库增加唯一约束例如对reservation表增加一个复合唯一索引 (seat_id, date, time_slot)并且只对status为0和1的记录生效。这事在MySQL里可以用生成列实现在SQLite里稍微麻烦一点。我实际项目中是在接口层用事务加行锁处理先锁座位行再查询并插入预约记录避免并发穿透。3.3 Room数据库与SharedPreferences的职责划分项目采用Room作为本地缓存但本地不是主要数据源。后端接口返回的数据会先写入Room再通过LiveData通知UI更新。这样做的好处是离线时仍能查看最近的座位状态网络恢复后自动刷新。Room的实体类和DAO是配合使用的实体类示例Entity(tableName reservation) data class ReservationEntity( PrimaryKey(autoGenerate true) val id: Long 0, ColumnInfo(name user_id) val userId: Long, ColumnInfo(name seat_id) val seatId: Long, val date: String, ColumnInfo(name time_slot) val timeSlot: Int, val status: Int, ColumnInfo(name created_at) val createdAt: Long, ColumnInfo(name expired_at) val expiredAt: Long )SharedPreferences则只用来保存登录状态等轻量数据比如token、userId、用户姓名。不要把预约数据塞进SharedPreferences读写频繁、数据量大会让主线程卡顿。4. 核心功能模块的实现4.1 登录注册与会话保持登录逻辑比较简单用户输入学号和密码后端校验通过后返回Token客户端将Token存到SharedPreferences。后续每个请求都在Header里带上Token。需要注意的一点密码绝对不能明文存储也不能明文传输。客户端这边不做加密的话至少在传输层用HTTPS。本地SharedPreferences只能存Token不存密码。后端数据库中存的也应该是密码的哈希值比如BCrypt。登录页面建议监听回车键并自动跳转用户体验会好很多。另外一定要处理登录按钮的防重复点击我见过很多同学连点导致三个重复请求的问题可以在ViewModel里加一个isLoading标志位请求期间禁用按钮并显示进度条。4.2 座位大厅筛选联动座位大厅是APP的门面核心交互是“选日期 选时段 选楼层 选座位”。我把日期做成横向滚动的列表时段做成Tab楼层做成下拉框。实现时日期列表用RecyclerView横向布局每一格显示“周一 04-22”。点击日期后需要重新请求该日期下的座位数据。时段切换时页面要立即刷新不能等到用户再次点查询。楼层切换同样如此。这三个筛选条件彼此独立任意一个变化都要触发刷新。我建议把它们统一为三个LiveData字段在ViewModel中用MediatorLiveData合并这三个数据源任意一个变化就调用刷新方法。具体到代码上val queryTrigger MediatorLiveDataAny().apply { addSource(selectedDate, { value it }) addSource(selectedTimeSlot, { value it }) addSource(selectedFloor, { value it }) }然后观察queryTrigger每次有变化就重新加载座位列表。这样做比在界面上一处处调用refresh()更干净也天然支持了浏览过程中快速切换筛选。4.3 预约流程与状态机设计座位预约的状态流转是项目里最容易出逻辑漏洞的地方。我定义的完整生命周期如下预约成功状态为“待签到”此时座位已被锁定。到馆签到状态变为“已签到”此时座位正式归该用户使用。使用结束状态变为“已结束”座位释放。取消预约只能在“待签到”状态取消状态变为“已取消”座位释放。超时未签到从预约时间开始算超过30分钟未签到状态变为“爽约”座位释放。用文字描述状态机其实比画复杂的图更直观。关键点在于每个状态变更都要记录时间戳并且所有状态变更都必须在后端完成客户端不能直接改状态。在客户端UI上座位网格需要感知这些状态。比如你看到座位是绿色的点击后弹出确认框提交成功变成红色。如果你点击的是红色座位Toast会提示“该时段已被预约”。如果座位是灰色则会提示“座位维护中请选其他位置”。整个预约流程放在一个PlaceReservationWorker中处理步骤为校验用户登录状态。向服务器发起预约请求。服务器执行座位锁检查和插入预约记录。返回成功后刷新座位列表与我的预约。设置一个本地提醒预约时间前15分钟通知用户签到。4.4 我的预约列表与取消操作“我的预约”页面使用RecyclerView展示卡片。每张卡片包含日期、时段、座位号、状态、操作按钮。操作按钮根据状态变化待签到显示“取消预约”按钮字色为红色。已签到显示“结束使用”按钮字色为绿色。已结束/已取消/爽约不显示任何按钮卡片降低透明度。取消预约时要弹出确认对话框因为误点会导致座位被释放。确认后调用接口成功后本地更新卡片状态同时座位大厅里这个座位重新变绿。如果想让体验更好可以在“待签到”状态下增加倒计时显示还剩多少时间可以签到。这个倒计时不需要每秒刷新用Handler延迟到指定时间点更新一次就行避免无谓的CPU消耗。4.5 管理员端异常处理功能管理员界面可以设计成一个独立的入口普通用户看不到。管理员登录后进入一个按楼层展示的总览页面能看到所有座位的实时状态。出现“人来了但系统显示已约”这类问题或者在高峰期强制释放某个被长期占用的座位时管理员可以点击座位进行强制变更。这个功能不需要做得太复杂核心就是提供座位状态列表 变更接口。管理员权限的判断在前端只是隐藏入口真正权限控制必须由后端根据role字段完成防止普通用户直接调用接口越权。5. 关键UI与代码实现细节5.1 座位网格的RecyclerView适配器写法座位大厅的座位网格用RecyclerView实现布局管理器是GridLayoutManager每行4列。如果座位数量多还可以考虑StaggeredGridLayoutManager但座位通常是规整的格子用Grid更合适。适配器的核心是处理好“点击事件”和“状态颜色”。我写了一个内部类SeatViewHolderclass SeatAdapter( private val onSeatClick: (Seat) - Unit ) : RecyclerView.AdapterSeatAdapter.SeatViewHolder() { private val items: MutableListSeatUI mutableListOf() fun submitList(list: ListSeatUI) { items.clear() items.addAll(list) notifyDataSetChanged() } class SeatViewHolder(view: View) : RecyclerView.ViewHolder(view) { val seatNo: TextView view.findViewById(R.id.tv_seat_no) val root: View view.findViewById(R.id.root_seat) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SeatViewHolder { val view LayoutInflater.from(parent.context) .inflate(R.layout.item_seat, parent, false) return SeatViewHolder(view) } override fun getItemCount(): Int items.size override fun onBindViewHolder(holder: SeatViewHolder, position: Int) { val item items[position] holder.seatNo.text item.seatNo holder.root.setBackgroundResource(item.bgRes) holder.root.setOnClickListener { onSeatClick(item.seat) } } }注意submitList里我用了notifyDataSetChanged()如果数据量大可以改成DiffUtil但对图书馆规模的数据量来说全量刷新完全够用。如果用了DiffUtil反而让代码复杂不少收益不明显。5.2 加载进度条与异步状态处理座位数据加载是网络请求必须使用异步。我采用ViewModel LiveData的方式在加载开始前把isLoading置为true结束时置为false。页面上方的进度条根据isLoading显示或隐藏。进度条可以用Material的LinearProgressIndicator更轻量也能设置动画。在布局中它默认是不可见的等到加载完成再隐藏viewModel.seatList.observe(viewLifecycleOwner) { result - if (result.isLoading) { binding.progressBar.show() } else { binding.progressBar.hide() adapter.submitList(result.data) } }有一个很容易被忽略的细节加载状态要和筛选条件联动。比如用户正在快速切换时段上一次请求还没回来下一次请求已经发出。如果不做控制旧请求可能覆盖新请求的结果页面展示错乱。我采用的做法是给每个请求增加一个请求序号只有最新序号的结果才能更新UI。private var requestId 0 fun loadSeats(date: String, slot: Int, floorId: Int) { val currentId requestId viewModelScope.launch { val result repository.getSeats(date, slot, floorId) if (currentId requestId) { _seatList.value result } } }这个套路简单有效比用协程的Job.cancel()更不容易出问题。因为网络请求发出去后无法真正取消用请求序号做年龄判定是最稳妥的。5.3 座位状态颜色与资源管理座位状态的展示我统一用颜色区分避免用户被文字干扰。颜色定义在res/values/colors.xml里状态颜色含义空闲#4CAF50绿色可预约已约#F44336红色已被占用维护中#9E9E9E灰色不可选我的预约#2196F3蓝色当前用户已约这里有一个设计细节在座位状态颜色上要区分“已约”和“我的预约”否则用户会误以为自己的座位不可用。所以颜色需要多一个蓝色状态而且要在座位图上更醒目。状态映射我写了一个util函数避免在Adapter里到处写if elsefun SeatStatus.toBackgroundRes(): Int { return when (this) { SeatStatus.AVAILABLE - R.drawable.bg_seat_available SeatStatus.BOOKED - R.drawable.bg_seat_booked SeatStatus.IN_MAINTENANCE - R.drawable.bg_seat_maintenance SeatStatus.MY_BOOKING - R.drawable.bg_seat_my_booking } }如果座位上有电源插座或靠窗属性可以在座位编号旁边放一个小圆点作为标识不需要额外文字。这类信息应该在座位详情弹窗中展示清楚。5.4 日期选择器的横向滑动优化日期列表是功能里的一个重要控件但不能为了炫酷做得太复杂。我用RecyclerView实现每条item是一个TextView。默认选中当天通过setSelected和背景色表示选中状态。日期数据在进入页面时生成最近7天包含星期几和日期。有个小技巧生成日期数据时不要用SimpleDateFormat在线程里反复创建建议定义一个全局格式器或者用DateTimeFormatter。在高频刷新列表时这能减少不必要的对象创建和GC卡顿。6. 调试实录与避坑指南6.1 Android Studio编译依赖问题的排查开发过程中最常见的一类报错就是依赖项解析失败。典型的错误信息像这样Could not determine the dependencies of task :app:compileDebugJavaWithJavac.我遇到这种情况时的排查顺序是先看是不是网络问题Could not resolve all task dependencies往往是因为依赖下载不完整或仓库源连接超时。检查build.gradle依赖版本号是否写错比如Room 2.5.2写成2.5.3但在某些仓库中该版本未同步。尝试File - Sync Project with Gradle Files如果还不行就Build - Clean Project再Rebuild Project。最后可以考虑gradlew --refresh-dependencies强制刷新缓存。如果错误出现在某个第三方库中通常是版本冲突用gradlew :app:dependencies查看依赖树。还有一个很低级但常犯的错误Kotlin项目里配置kapt时忘记加kotlin-kapt插件导致Room注解处理器不生效。这也是编译错误的高发原因。我建议直接在主模块的plugins块中显式声明id kotlin-kapt。6.2 真机调试中明文HTTP请求被拦截这个坑几乎所有人都遇过后端接口是http://192.168.1.100:8080/api/在模拟器上跑得好好的换到真机就报Cleartext HTTP traffic not permitted。原因从Android 9开始系统默认禁止应用使用明文HTTP协议。解决方法有几种最简单的方案在AndroidManifest.xml的application标签上加android:usesCleartextTraffictrue这个方案适合开发阶段。正式上线时如果必须用HTTP更好的做法是配置网络安全策略只允许特定域名走明文其他域名走HTTPS。另外真机上访问局域网接口要确保手机和电脑在同一个WiFi下并且防火墙放行了8080端口。很多时候问题不在Android而在电脑防火墙没开端口。我习惯先在后端机器上用另一个手机浏览器访问一下接口地址确认能通再排查App代码。6.3 时间同步与预约状态不一致问题预约系统对时间非常敏感。如果客户端和后端时间不一致会出现预约列表里显示“待签到”但实际上已经超过签到截止时间的问题。比如手机时间慢了两分钟用户以为还能签到后端却已经判定爽约。解决办法是所有时间校验以后端服务器时间为准客户端只负责展示。具体操作是后端在每个接口响应的Header中返回Server-Timestamp客户端在登录和请求数据的间隙校准一次本地时间偏移量。在执行签到或取消操作时客户端可以用这个偏移量做倒计时显示但真正判断能否操作必须由后端完成。在APP内部倒计时组件不要依赖System.currentTimeMillis()因为用户可能手动改系统时间。应该统一用一个TimeProvider类内部维护偏移量所有代码都从这个类取当前时间。这样即使服务器时间校准也只改一处。6.4 另外几个让我印象深刻的坑第一个坑是RecyclerView的item复用导致状态残留。座位列表中某些item设置了透明背景滚动时复用了之前“已约”状态的item背景导致显示错乱。解决办法是在Adapter的onBindViewHolder中无条件设置背景和点击监听不要把设置逻辑包在if分支里。第二个坑是加载数据时快速滑动列表导致图片/文字闪烁。如果不需要异步加载图片可以不处理但如果用Coil加载座位示意图需要保证占位图一致。否则因为item复用图片加载完成时会闪动。第三个坑是预约成功后的页面刷新。用户提交预约后如果只更新了“我的预约”页面而没有刷新座位大厅用户返回座位列表时会发现刚预约的座位还是绿色造成困惑。正确做法是把预约结果通过ViewModel共享给两个页面或者使用本地广播通知座位列表刷新。6.5 后端接口联调的小建议虽然是Android项目但联调阶段如果后端是自己写的建议先把接口返回格式统一。我使用的统一结构是{ code: 0, message: success, data: {} }客户端在Retrofit的GsonConverter层解析code字段非0时抛出业务异常并在UI层提示。不要在成功回调用if (data null)来判断是否出错因为后端异常时data字段可能是空的规范统一后前端处理逻辑就干净很多。接口文档用Swagger/OpenAPI维护哪怕只是给自己看也要把每个字段的含义写清楚。联调过程中最高频的问题就是字段名拼写不一致比如前端用userId后端返回studentNo。如果格式统一、文档齐全这类问题能少一半。收尾一点实际体会整个项目做下来我最大的感受是座位预约系统的难点不在界面有多炫而在状态流转和并发控制。只要你把“座位状态”和“预约记录”这两张表设计清楚所有功能都会顺理成章。开发过程中不要着急写代码先花半天把状态机画出来把每个状态之间的转移条件写清楚后面写代码基本就是照着翻译。最后再分享一个小技巧预约时段不建议拆得太细30分钟或2小时为最小单位比较合适太细会把座位切成一堆没人用的碎片。这个设计决策在初期可能看不出问题等到用户量上来你会发现它对整体利用率的帮助比想象中更大。
返回列表