恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
基于Android的图书管理系统App开发实战:Room建表与借还书事务解析
首页
资讯中心
/
基于Android的图书管理系统App开发实战:Room建表与借还书事务解析
基于Android的图书管理系统App开发实战:Room建表与借还书事务解析
发布时间:2026/9/12 22:25:35
简介这是一套完整的基于Android的大学生图书管理系统毕业设计源码面向计算机相关专业毕业生或Android开发学习者主要解决毕业设计选题与图书管理类App开发实践问题。内容覆盖学生端与管理端主要业务包括图书查询、预约、挂失以及学生用户管理、图书管理、借阅归还、罚款处理等模块代码结构清晰配有数据库SQL文件与配置运行视频适合用于课程设计、毕业论文实现或项目二次开发。整个资源包约52.76MB共2000个文件其中包含314个Java源文件、450个XML布局与配置、202个PNG图片、10个JSP页面以及SQL数据库脚本等涵盖从源码、界面资源到数据库与构建配置的主要组成部分。目前已有430人学习下载。资源还附带完整的配置教学视频、项目结构说明、代码讲解与运行流程演示能帮助使用者从环境搭建到最终运行全程跟随操作显著降低上手门槛是完成毕业设计类图书管理项目的实用参考。1. 基于Android的大学生图书管理系统App毕业设计真正的坑从建表才刚开始很多毕业生看到“基于Android的大学生图书管理系统App”这个题目第一反应是“图书增删改查这有什么难的”。等Android Studio里的项目跑起来才发现难点从来不在CRUD而在移动端特有的各种约束屏幕放不下复杂表格返回键按错了怎么办App退到后台再打开数据还在不在连点两次借书按钮会不会造成一书记两笔。它不像PC端那样有足够空间同时铺开列表和详情也不像网页端刷新一次就能从服务端重新拉数据。下面把数据库建表、登录检索、借还判定、答辩演示整条路径讲清楚适合已选Android方向但还没动手写代码的毕业设计动手党。2. 基于Android的图书管理系统App的数据库建模与Room选型2.1 为什么毕业设计优先选Room而不是原生SQLite图书管理系统的数据层可选方案有三条原生SQLite、LitePal、Room。对“源码型毕业设计”来说选型判据不是性能而是“答辩时能不能自圆其说出问题能不能快速定位”。Room把SQL写进Query注解编译期就做语法检查原生SQLite要运行到cursor.moveToNext()才会暴露问题这种错误在演示当天出现会非常被动。LitePal用对象映射简化操作但对Java注解处理依赖较深初次配置容易在compileOnly和implementation之间翻车。所以建议是Java Room annotationProcessor。配置Room的gradle依赖不算多一段代码就能引入implementation androidx.room:room-runtime:2.6.1 annotationProcessor androidx.room:room-compiler:2.6.1版本号要统一room-runtime和room-compiler如果一个写2.5.2一个写2.6.1编译期会报一堆找不到符号的错误。这里的版本只作示例实际以你的Android Studio创建项目时自动带出的版本为准不要盲目复制。三种方案在毕业设计里的横向对比对比项Room原生SQLiteLitePal编译期检查SQL有无无需要手写DAO是否否运行时崩溃风险低高中新手入门成本低中中单看这张表“把错误的SQL提前暴露”这件事Room做得最好这就是选它的核心理由。后面的同学如果答辩被问“为什么不用原生SQLite”可以直接回答原生SQLite的错误发生时机太晚对毕业设计这种短周期项目不友好。2.2 图书、学生、借阅记录三张表的字段与常见误区图书管理系统的核心不是“存储书”而是“管理书的状态”。字段可以拆成三类描述字段、库存字段、时间字段。描述字段对应书名、作者、ISBN库存字段对应total_count和available_count时间字段对应入馆时间、应还时间。下面的SQL在SQLite中可以直接执行是三张核心表的建表语句CREATE TABLE book ( book_id INTEGER PRIMARY KEY AUTOINCREMENT, isbn TEXT NOT NULL UNIQUE, title TEXT NOT NULL, author TEXT DEFAULT 佚名, category TEXT, total_count INTEGER DEFAULT 1, available_count INTEGER DEFAULT 1, location TEXT, create_time TEXT DEFAULT (datetime(now, localtime)) ); CREATE TABLE student ( student_no TEXT PRIMARY KEY, name TEXT NOT NULL, password TEXT NOT NULL, phone TEXT, max_borrow_count INTEGER DEFAULT 5 ); CREATE TABLE borrow_record ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_no TEXT NOT NULL, book_id INTEGER NOT NULL, borrow_time TEXT DEFAULT (datetime(now, localtime)), due_time TEXT, return_time TEXT, status INTEGER DEFAULT 0, FOREIGN KEY (student_no) REFERENCES student(student_no), FOREIGN KEY (book_id) REFERENCES book(book_id) );这里几个参数值得展开total_count代表馆藏总数available_count代表可借数还书时只增加available_count不要动total_countmax_borrow_count放到student表而不是写成全局常量是为了方便扩展不同年级不同限额due_time存的是“应还时间”字符串用yyyy-MM-dd HH:mm:ss格式不要在库里存“逾期7天”这种相对量status的0表示借阅中1表示已归还以后查历史借阅、逾期清单都不需要额外维护一张表。2.3 借阅记录和图书状态的联表查询怎么写答辩时被问得最多的一句话是“你怎么查出来谁借走了这本书”。如果把借阅记录直接挂在book表里一本书被借多次就会产生冗余。正确做法是用borrow_record做中间表通过外键关联student和book。以“查某本书当前借阅人”为例在Room的DAO里这样写Dao public interface BorrowRecordDao { Query(SELECT student.name, book.title, borrow_record.borrow_time, borrow_record.due_time FROM borrow_record INNER JOIN student ON borrow_record.student_no student.student_no INNER JOIN book ON borrow_record.book_id book.book_id WHERE book.title :title AND borrow_record.status 0) ListBorrowInfo findBorrowersByBookTitle(String title); }:title是Room的参数绑定执行时会替换SQL里的问号返回的BorrowInfo是一个普通Java类字段名要和查询列名严格一致。INNER JOIN只会返回借阅记录里存在的组合如果一本书从未被借过这条查询返回空列表但书本身还在图书列表页因为列表页走的另一条查询。这个区别在答辩演示时值得专门说一句能体现你对JOIN语义的理解。接着就引出一个常见的坑如果你在book表里存了“当前借阅人”字段那这本书被归还后这个字段要么置空要么保留旧值两种做法都会导致历史记录丢失。用borrow_record做关联表从设计上就避免了这套纠纷。3. 基于Android的图书管理系统App登录鉴权、图书检索与列表交互3.1 注册登录模块的密码处理与登录态保存登录模块要回应两个问题密码怎么存登录状态怎么记。密码用MD5加盐虽然按现在的标准看不算强但在单机版毕业设计里足够解释“我没有把明文密码直接落库”。盐可以用学号生成注册时把md5(password 学号)写入password字段下次登录用同一个盐计算后比对public static String md5WithSalt(String pwd, String salt) { String content pwd salt; try { MessageDigest digest MessageDigest.getInstance(MD5); byte[] bytes digest.digest(content.getBytes(StandardCharsets.UTF_8)); StringBuilder sb new StringBuilder(); for (byte b : bytes) { sb.append(String.format(%02x, b)); } return sb.toString(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(MD5 algorithm unavailable); } }%02x表示把字节格式化为两位十六进制x是小写输出。如果写成%X同一个密码会得到大写字符串登录比对时直接失败。这两行是小细节却是新手最容易翻车的地方。登录成功后的状态保持有三条路可走方案重启App后是否保留安全等级代码量SharedPreferences存学号保留低少SQLite存登录记录保留中中静态变量存当前用户不保留低最少毕业设计常见做法是第一种SharedPreferences保存学号每次冷启动读一次。注意不要把密码也存进去否则卸载后的残留文件里会暴露哈希答辩时解释不清楚。3.2 图书检索的SearchView与RecyclerView最小实现图书检索不需要引入第三方搜索框架SearchView加RecyclerView足够。搜索结果用模糊匹配覆盖书名、作者、ISBN三个维度DAO写法Query(SELECT * FROM book WHERE title LIKE % || :keyword || % OR author LIKE % || :keyword || % OR isbn LIKE % || :keyword || %) ListBook searchBooks(String keyword);LIKE在SQLite中默认不区分大小写对书名和作者足够用。两个%必须依靠||字符串连接符出现在SQL里Room不会自动补通配符。如果keyword为空串%%会匹配全表所以页面上要在keyword为空时直接走SELECT * FROM book避免一次多余查询。提示Query里的字符串连接一定要用SQLite的||写成加号会被当成数字加法搜索“001”时返回空列表排查半天都发现不了。这里建议把搜索限制在静态数据层。有同学会在TextWatcher里引入LiveData做观察订阅对几百条本地数据的单机App来说属于过度设计回调拿到列表后直接adapter.notifyDataSetChanged()就够。等数据量到几千条、过滤频繁增删item时再考虑DiffUtil或Paging 3那是另一种复杂度。3.3 列表Item的状态展示与详情页跳转参数列表每个item右侧放一个“可借/已借出”的TextView直接读available_count判断不额外查借阅表binding.tvStatus.setText(book.getAvailableCount() 0 ? 可借 : 已借出); binding.tvStatus.setTextColor(book.getAvailableCount() 0 ? Color.parseColor(#2E7D32) : Color.parseColor(#C62828));用三元表达式在Adapter里把文案和颜色一次设定不要在getView外通过Handler去刷新某一行。数据更新后调用adapter.notifyDataSetChanged()整表刷新几百条数据没有性能压力。跳转详情页时只传book_idIntent intent new Intent(activity, BookDetailActivity.class); intent.putExtra(book_id, book.getBookId()); activity.startActivity(intent);详情页onCreate里用getIntent().getIntExtra(book_id, -1)拿到ID再调用bookDao.findById(bookId)加载完整数据。传ID而不是传对象可以避免Serializable序列化带来的模板代码和版本不一致问题也符合“详情页数据自己取”的组件划分原则。4. 基于Android的图书管理系统App的借还书流程与逾期状态判定4.1 借书操作的事务边界先插入记录还是先扣库存借书动作拆开就是两步borrow_record表插入一条记录book表把available_count减一。最忌讳的是让这两步独立提交。举例点击借书后App在第一步成功、第二步失败用户看到的界面是库存没变借阅记录里却多了一条记录。给数据库包一层事务能规避这个问题db.runInTransaction(new Runnable() { Override public void run() { BorrowRecord record new BorrowRecord(studentNo, bookId); borrowRecordDao.insert(record); bookDao.decreaseAvailableCount(bookId); } });runInTransaction体内所有写操作共享同一个事务任何一个DAO方法抛出异常整个事务回滚两步都不会落库。顺序上先插记录再减库存解释为“借阅动作发生时记录作为审计数据必须存在”这一条也能在文档里写清楚。4.2 还书操作只更新不删除审计数据要留底部分同学会在还书时用delete from borrow_record把历史记录删掉。图书管理系统的还书业务和购物车不一样借阅历史要能够追溯。还书时应该做的是把status从0改成1同时把return_time写成当前时间Query(UPDATE borrow_record SET return_time datetime(now, localtime), status 1 WHERE id :recordId AND status 0) void returnBook(int recordId); Query(UPDATE book SET available_count available_count 1 WHERE book_id :bookId) void increaseAvailableCount(int bookId);status 0这个附加条件很关键。如果一条记录已经是还书状态第二次点击还书时第一条UPDATE的受影响行数为0后续不会重复增加可借数量。受影响行数正好可以用来判断“本次还书是否真的生效”而不是盲目地把available_count加一。字段值说明字段类型含义statusINTEGER0未还1已还borrow_timeTEXT借出时间入库时自动取当前时间due_timeTEXT应还时间借出时手动加上借期return_timeTEXT实际归还时间还书时写入4.3 逾期天数的计算与时间解析的坑逾期不用等到夜间任务去算。用户在“我的借阅”页面拉取记录时把每一条的due_time和当前时间做差需要展示“已逾期 N 天”时当场算不落库。这样省掉一个后台任务也避免设备时钟不准带来的脏数据SimpleDateFormat sdf new SimpleDateFormat(yyyy-MM-dd HH:mm:ss, Locale.CHINA); Date due sdf.parse(record.getDueTime()); long diff System.currentTimeMillis() - due.getTime(); long overdueDays diff / (1000 * 60 * 60 * 24); if (overdueDays 0) { statusText 已逾期 overdueDays 天; }diff / (1000 * 60 * 60 * 24)用的是整除直接把毫秒差转为整天数。如果写成(int)(diff / 86400000f)浮点除法会碰到边界问题23小时59分的差值会被算成0.99天强转int后变成0用户会看到“已逾期0天”这是个明显的前端误导。注意SimpleDateFormat线程不安全不要在AsyncTask或线程池里复用同一个静态实例解析due_time否则偶发解析出错误日期。最简单的规避是在每个调用点new实例或者改用java.time包下的LocalDateTime。4.4 防止连点造成重复借阅的两种手段连点问题在演示现场特别容易暴露鼠标点击快时两个点击事件间隔可能只有几十毫秒两个线程都判断完“可借”就会插入两条记录。手段一是在点击后立刻禁用按钮这只防同一页面手段二是给book表加version乐观锁防跨页面并发Query(UPDATE book SET available_count available_count - 1, version version 1 WHERE book_id :bookId AND available_count 0 AND version :version) int decreaseAvailableCount(int bookId, int version);这条UPDATE把“库存大于0”和“版本号一致”同时作为更新条件返回值是受影响行数。返回0说明其他操作已经抢先改过数据此时弹提示“库存刚被更新请重新确认”。把version字段初始化为0之后每次借还都让version加一本质上用乐观锁处理了并发写冲突。毕业设计不要求一定做到这一步但答辩时讲出“我用版本号防止了超借”算是一个实打实的亮点。5. 基于Android的图书管理系统App答辩前的边界场景验证与源码打包5.1 边界场景检查表提交前按下面这张表逐条走一遍这些场景覆盖了异常输入、重复操作和状态持久化基本是答辩现场最容易按出来的路径场景操作预期结果借满限额已借5本的学生再点借书弹窗拒绝按钮不可用空关键词搜索SearchView输入空格显示全部图书不崩溃连点借书按钮快速点击“借书”5次只产生一条借阅记录逾期图书展示系统日期超过due_time显示“已逾期N天”且标红杀进程重开Android Studio停止App再启动登录态保留直接进主页面检查时候的解释可以按“边界条件 - 预期行为 - 实际结果”组织每一行都可以转成论文测试章节的用例表写完直接放进“系统测试”一节。5.2 源码打包和演示前的目录清理提交源码前删掉build、.gradle和local.properties再压缩。build目录是编译产物local.properties里写了本机SDK路径留着会让源码包带上个人环境信息别人导入时反而要改。gradle文件夹中的wrapper要保留导师在别的机器上执行gradlew assembleDebug就能构建出可安装的APK避免出现“你那边能跑我这边跑不了”的局面。演示机提前用Build APK(s)生成一个release包装到真机上。如果只在模拟器上跑过答辩当天模拟器启动慢、内存不足都是不确定因素。真机装好后点击启动要直接进主页面不依赖Android Studio的连接。5.3 最后一个值得写进代码的小功能在MainActivity的onBackPressed里加一个“确认退出登录”的对话框这段逻辑能在答辩演示时展示任务栈处理Override public void onBackPressed() { new AlertDialog.Builder(this) .setMessage(确认退出登录) .setPositiveButton(退出, (dialog, which) - { getSharedPreferences(user, MODE_PRIVATE).edit().clear().apply(); moveTaskToBack(true); }) .setNegativeButton(取消, null) .show(); }moveTaskToBack(true)把任务栈退到后台而不是调用finish()再次打开App时栈还在不会重新执行启动页SharedPreferences清空后下次启动校验登录态时自然落入登录页。这两行代码把“退出登录”和“退出App”两种语义分开是答辩演示里容易让老师记住的实现细节。本文还有配套的精品资源点击获取