
简介这是一份面向安卓初学者的毕业设计完整源码资源基于安卓平台开发大学生图书管理系统覆盖学生用户端与管理员端实现图书查询、预约借阅、挂失归还、罚款交纳、读者信息管理、图书信息管理以及分级管理员权限等功能业务模块完整适合作为课程设计、毕业设计或项目实战的参考方案。资源打包为RAR压缩格式共2000个文件约52.76MB核心包括Java源码、XML界面布局、PNG/JPG图片资源、JSON与properties配置、MySQL数据库SQL脚本并额外提供项目结构说明、代码讲解视频、软件配置流程视频和运行环境工具便于从零搭建运行。已有430人学习下载无论初次接触安卓还是计划快速完成系统设计都可借助配套视频和源码理解登录鉴权、数据库交互和前后台联动等关键环节从界面到数据库的落地实现也便于二次开发缩短开发调试时间。1. 一个能跑起来的 Android 图书管理系统到底卡在哪图书管理系统是计算机专业最经典的毕业设计命题之一但把同样的题目落到 Android App 上难点并不是“会不会写 SQL”而是“能不能在手机里把业务流程走通”。大学生图书管理系统 App 和网上遍地都是的 Web 管理系统最大的区别在于它要处理离线状态下的数据缓存、触摸交互下的列表渲染、以及一台手机上“学生借书、管理员管书”的双角色逻辑。拿一个只有后台 CRUD 经验的模板改 UI几乎没办法在答辩现场演示完整个借阅流程。本文围绕这个标题按“技术选型 → 数据库建模 → 核心业务实现 → 数据同步 → 验证与答辩”这条线展开所有代码都以能跑通为最低标准。适合正在做毕业设计、课程设计的同学也适合想快速搭一套本地优先的 Android 业务应用的开发者参考。2. 技术选型与工程结构Android 端的图书管理先分清“本地库”和“服务端”2.1 选型逻辑App 端的“管理”和后台的“管理”是两回事Web 端的图书管理系统一个 Tomcat 加 MySQL 就能把所有逻辑集中在服务器上跑。但 Android 端天然要考虑“没网能不能用”“数据存哪”“多端怎么同步”这三个问题。一般我们做这类毕业设计有两种常见路线第一种是纯本地方案所有数据存在手机 SQLite 里界面直接读写数据库App 就是一个完整的单机管理系统。这种方案实现简单、演示稳定缺点是换一台手机数据就没了且无法体现“联网”能力。第二种是客户端 服务端方案Android 只做 UI 和请求业务和数据都在后端接口里常见的配套是 Spring Boot MySQL。这种方案更像真实项目但工作量翻倍如果时间紧张很容易写到一半烂尾。我的建议是选“本地优先、接口预留”的混合结构核心借阅流程在本地 SQLite 完成同时预留一个数据接口层如果后续要加后端只需要替换数据仓库实现UI 层完全不用动。对毕业设计答辩来说这个设计能解释清楚“为什么你选择了 SQLite”又能回答“以后怎么扩展”。维度纯本地 SQLite本地 后端接口开发周期短中长演示稳定性高不依赖网络依赖模拟器/真机网络答辩加分点架构简单清晰体现前后端分离、接口设计数据安全弱中推荐场景时间紧张时间充裕且想冲刺优秀2.2 Android Studio 工程骨架包名、依赖与最小目录打开 Android Studio 新建项目时最好选“Empty Views Activity”而不是直接用 Compose。原因很简单图书管理系统这种界面以列表和表单为主的 App传统 View 体系的参考资料更多遇到问题容易搜到答案。包名建议取com.example.libsys不要用中文包名否则后面 Build 容易报错。build.gradle里建议加入以下依赖dependencies { implementation androidx.appcompat:appcompat:1.6.1 implementation com.google.android.material:material:1.9.0 implementation androidx.recyclerview:recyclerview:1.3.0 implementation androidx.cardview:cardview:1.0.0 implementation com.squareup.okhttp3:okhttp:4.11.0 implementation com.google.code.gson:gson:2.10.1 }这些依赖覆盖了 UI 组件、网络请求和 JSON 解析三块。appcompat保证旧版本 Android 的兼容性material提供 Toolbar 和 FloatingActionButtonrecyclerview是书目列表的核心组件okhttp和gson是为第五章预留的网络能力。工程目录保持 Android Studio 默认结构即可MainActivity作为应用入口负责承载书架列表的 Fragment 或者直接放一个 RecyclerView 的 Activity。不需要一开始就引入 MVVM 全家桶ViewModel LiveData 在数据量不大的管理系统里会让代码更绕。2.3 界面导航结构学生入口和管理员入口怎么切图书管理系统 App 不能只有一个页面至少要包含“登录/角色选择 → 书目列表 → 图书详情 → 借阅登记 → 归还确认”五个界面。常见做法是首屏放两个大的 Button一个“学生入口”一个“管理员入口”。学生进入后能查书、借书、查看自己的借阅记录管理员进入后能新增图书、修改库存、查看所有借阅记录。首屏布局不需要复杂核心是让状态切换简单LinearLayout android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical android:gravitycenter Button android:idid/btn_student android:text学生入口 / Button android:idid/btn_admin android:text管理员入口 / /LinearLayout这里没有用两个 Fragment 嵌套是因为“角色切换”属于顶层逻辑用 Activity 跳转比 Fragment 替换更直观。btn_student跳到StudentMainActivitybtn_admin跳到AdminMainActivity两个 Activity 各自持有一份数据访问对象。这样即使后面逻辑写乱了也不会互相牵连。3. 数据库设计图书表、学生表、借阅表的三表建模3.1 三张表的字段定义与关系图书管理系统的核心是数据建模。不要把“学生借书”简单理解成往一张表里插一条记录而是要拆成三张表图书表、学生表、借阅表。图书表存的是书目信息和库存总量学生表存的是学生身份信息借阅表是两张表的关系表记录“谁借了哪本书、什么时候借的、还了没有”。三张表的设计能避免数据冗余例如同一本书被两个人借走不需要在图书表里存两行只需要在借阅表里插两条记录即可。CREATE TABLE book_info ( book_id INTEGER PRIMARY KEY AUTOINCREMENT, book_name TEXT NOT NULL, author TEXT, publisher TEXT, isbn TEXT UNIQUE, total_copies INTEGER DEFAULT 1, available_copies INTEGER DEFAULT 1, location TEXT ); CREATE TABLE student_info ( student_id TEXT PRIMARY KEY, student_name TEXT NOT NULL, major TEXT, phone TEXT ); CREATE TABLE borrow_record ( record_id INTEGER PRIMARY KEY AUTOINCREMENT, book_id INTEGER NOT NULL, student_id TEXT NOT NULL, borrow_date TEXT NOT NULL, due_date TEXT NOT NULL, return_date TEXT, status INTEGER DEFAULT 0, FOREIGN KEY (book_id) REFERENCES book_info(book_id), FOREIGN KEY (student_id) REFERENCES student_info(student_id) );status字段用 0 和 1 表示“未还”和“已还”比删除记录更安全。borrow_date和due_date用TEXT类型保存yyyy-MM-dd格式的字符串比用时间戳更直观排序时也按字典序生效。available_copies是核心字段借出时减一归还时加一它代表这本书目前还能不能借。3.2 用 SQLiteOpenHelper 封装创建与升级Android 里操作 SQLite 不能用原生的openOrCreateDatabase裸写正规做法是继承SQLiteOpenHelper把建表逻辑集中在onCreate里。这样 App 第一次启动时自动建表后续需要加字段时只需要提高版本号并写onUpgrade。public class LibraryDbHelper extends SQLiteOpenHelper { private static final String DB_NAME library.db; private static final int DB_VERSION 1; public LibraryDbHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } Override public void onCreate(SQLiteDatabase db) { db.execSQL(CREATE TABLE IF NOT EXISTS book_info ( book_id INTEGER PRIMARY KEY AUTOINCREMENT, book_name TEXT NOT NULL, author TEXT, publisher TEXT, isbn TEXT UNIQUE, total_copies INTEGER DEFAULT 1, available_copies INTEGER DEFAULT 1, location TEXT)); db.execSQL(CREATE TABLE IF NOT EXISTS student_info ( student_id TEXT PRIMARY KEY, student_name TEXT NOT NULL, major TEXT, phone TEXT)); db.execSQL(CREATE TABLE IF NOT EXISTS borrow_record ( record_id INTEGER PRIMARY KEY AUTOINCREMENT, book_id INTEGER NOT NULL, student_id TEXT NOT NULL, borrow_date TEXT NOT NULL, due_date TEXT NOT NULL, return_date TEXT, status INTEGER DEFAULT 0, FOREIGN KEY(book_id) REFERENCES book_info(book_id), FOREIGN KEY(student_id) REFERENCES student_info(student_id))); } Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL(DROP TABLE IF EXISTS borrow_record); db.execSQL(DROP TABLE IF EXISTS student_info); db.execSQL(DROP TABLE IF EXISTS book_info); onCreate(db); } }这里execSQL一次只能执行一条语句不能把三张表的建表 SQL 合并成一个字符串否则在 SQLite 里会报语法错误。onUpgrade直接删表重建是毕业设计里可以接受的方案因为演示数据不需要长久保留如果是正式产品应该用ALTER TABLE增量更新。3.3 预置数据与库存扣减的边界条件App 装到模拟器上数据库是空的不能让评委看到空荡荡的界面。常见做法是在onCreate建表之后判断表内行数如果为 0 就插入几条预设的图书和学生数据。这个逻辑放在LibraryDbHelper里再合适不过因为每次 App 冷启动都会经过这里。数据预置时要注意一个坑onCreate拿到的SQLiteDatabase对象不能直接执行查询要先通过getReadableDatabase()拿到同一个实例后再查。另外book_info的isbn字段加了UNIQUE约束预置数据时如果重复插入第二次会抛SQLiteConstraintException所以插入前先查一遍。SQLiteDatabase db this.getWritableDatabase(); Cursor cursor db.rawQuery(SELECT COUNT(*) FROM book_info, null); cursor.moveToFirst(); if (cursor.getInt(0) 0) { ContentValues values new ContentValues(); values.put(book_name, Android 开发艺术探索); values.put(author, 任玉刚); values.put(total_copies, 5); values.put(available_copies, 5); db.insert(book_info, null, values); } cursor.close();库存扣减的边界条件是整个系统的第一道保险当available_copies小于 1 时借阅按钮必须置灰或者点击后提示“库存不足”。这层判断放在 UI 层只能防手滑真正的防线应该在数据库操作层先查询再更新两步合在一个同步方法里避免极端情况下出现并发问题。4. 核心业务图书检索、借阅登记与归还流程的实现4.1 RecyclerView 展示书目列表Adapter 与 ViewHolder书目列表是这个 App 出现频率最高的界面。RecyclerView 的写法固定但繁琐核心是 Adapter 里维护数据集合并给每个 item 绑定事件。public class BookAdapter extends RecyclerView.AdapterBookAdapter.BookViewHolder { private ListBook bookList; private OnBorrowClickListener listener; public BookAdapter(ListBook bookList, OnBorrowClickListener listener) { this.bookList bookList; this.listener listener; } Override public BookViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_book, parent, false); return new BookViewHolder(view); } Override public void onBindViewHolder(BookViewHolder holder, int position) { Book book bookList.get(position); holder.tvName.setText(book.getBookName()); holder.tvAuthor.setText(book.getAuthor()); holder.tvAvailable.setText(可借 book.getAvailableCopies() / book.getTotalCopies() 本); holder.btnBorrow.setOnClickListener(v - { if (listener ! null) { listener.onBorrowClick(book); } }); } Override public int getItemCount() { return bookList.size(); } static class BookViewHolder extends RecyclerView.ViewHolder { TextView tvName, tvAuthor, tvAvailable; Button btnBorrow; BookViewHolder(View itemView) { super(itemView); tvName itemView.findViewById(R.id.tv_book_name); tvAuthor itemView.findViewById(R.id.tv_book_author); tvAvailable itemView.findViewById(R.id.tv_available); btnBorrow itemView.findViewById(R.id.btn_borrow); } } }onBindViewHolder里给btnBorrow设置的监听器会在点击时把当前 item 对应的Book对象回调出去。注意这里没有在BookViewHolder里处理业务而是通过接口交给 Activity目的是让 Adapter 保持纯净方便后续替换数据源。4.2 检索功能LIKE 模糊查询与分类过滤学生查书最大的需求就是“搜书名”。SQLite 的LIKE语句支持%通配符Android 里用rawQuery传参时要注意写成?占位符不能直接把用户输入拼接进 SQL 里否则有注入风险。public ListBook searchBooks(String keyword) { ListBook result new ArrayList(); SQLiteDatabase db dbHelper.getReadableDatabase(); Cursor cursor db.rawQuery( SELECT * FROM book_info WHERE book_name LIKE ? OR author LIKE ?, new String[]{% keyword %, % keyword %}); while (cursor.moveToNext()) { Book book new Book(); book.setBookId(cursor.getInt(cursor.getColumnIndexOrThrow(book_id))); book.setBookName(cursor.getString(cursor.getColumnIndexOrThrow(book_name))); book.setAuthor(cursor.getString(cursor.getColumnIndexOrThrow(author))); book.setAvailableCopies(cursor.getInt(cursor.getColumnIndexOrThrow(available_copies))); result.add(book); } cursor.close(); return result; }getColumnIndexOrThrow是 Android 里比较安全的取值方式不会因为字段拼写错误返回 -1 导致后续空指针。如果搜索框里什么也不输入就点击搜索查询语句等于LIKE %%会查出全部数据这符合直觉不需要额外处理。4.3 借阅与归还流程事务保证库存扣减与记录插入一致借书的完整动作有两步往borrow_record插一条记录同时把book_info的available_copies减一。如果只做两步操作中间任何一步失败都会造成数据不一致。正确的做法是放到一个事务里执行。public boolean borrowBook(int bookId, String studentId) { SQLiteDatabase db dbHelper.getWritableDatabase(); db.beginTransaction(); try { Cursor cursor db.rawQuery( SELECT available_copies FROM book_info WHERE book_id ?, new String[]{String.valueOf(bookId)}); cursor.moveToFirst(); int available cursor.getInt(0); cursor.close(); if (available 0) { return false; } ContentValues record new ContentValues(); record.put(book_id, bookId); record.put(student_id, studentId); record.put(borrow_date, getToday()); record.put(due_date, getTodayAfterDays(30)); record.put(status, 0); db.insert(borrow_record, null, record); db.execSQL(UPDATE book_info SET available_copies available_copies - 1 WHERE book_id ?, new String[]{String.valueOf(bookId)}); db.setTransactionSuccessful(); return true; } finally { db.endTransaction(); } }beginTransaction和setTransactionSuccessful的组合是 Android SQLite 的标准事务写法。注意必须先判断库存再插入记录顺序反了会在库存为 0 时产生一条无效借阅记录。getToday()和getTodayAfterDays(30)是要自己实现的日期工具方法用SimpleDateFormat格式化成yyyy-MM-dd字符串即可。归还流程是借书的逆操作必须同时完成“更新借阅记录状态”和“库存加一”两步同样需要事务保护。还有一个细节归还时应该校验这条记录是不是该学生的可以在UPDATE语句里加AND student_id ?条件。4.4 用 ContentProvider 还是直接访问数据库很多毕业设计参考代码会在 DAO 层上面再加一层 ContentProvider这是照搬“教材里的通讯录示例”。如果我们的 App 不需要给其他应用提供图书数据不需要系统级的跨进程访问用SQLiteOpenHelper加一个BookDao类就够了。ContentProvider 的适用场景是“数据要被别的 App 读”比如系统的联系人模块。图书管理系统作为一个独立 App自己读自己的数据库没有任何必要启 ContentProvider。如果答辩时被问到可以说“这里没有跨应用共享数据的需求所以直接用 SQLiteOpenHelper 封装数据层后续如果要暴露接口给教务处系统再把 DAO 迁移到 ContentProvider”。这个回答比盲目使用更体现你对 Android 组件的理解。5. 网络层与数据同步图书系统从“单机 App”到“接口对接”5.1 什么时候需要服务端接口如果图书管理 App 只在一台模拟器上跑那永远不需要网络。但这个系统叫“管理系统”意味着存在“管理员导入书目”和“学生查询书目”两个角色真实场景里一台手机不可能同时承担所有角色。当答辩老师问“你这里的数据备份怎么办”的时候就要有一个能接住问题的网络层设计。常见做法不一定是真的写完整个后端而是留好接口层。一般会定义一个ApiService类类里写两个方法fetchBookListFromRemote和syncLocalData。在毕业设计阶段这两个方法可以先返回假数据或者留一个TODO关键是通过接口层的抽象让 UI 代码不必改动就能对接未来后端。5.2 OkHttp 请求图书列表GET 与 JSON 解析假设后端接口已经就绪地址为http://10.0.2.2:8080/api/books用 OkHttp 发 GET 请求并解析 JSON。OkHttpClient client new OkHttpClient(); Request request new Request.Builder() .url(http://10.0.2.2:8080/api/books) .build(); client.newCall(request).enqueue(new Callback() { Override public void onFailure(Call call, IOException e) { runOnUiThread(() - Toast.makeText(context, 网络请求失败, Toast.LENGTH_SHORT).show()); } Override public void onResponse(Call call, Response response) throws IOException { String json response.body().string(); Gson gson new Gson(); ListBook remoteBooks gson.fromJson(json, new TypeTokenListBook(){}.getType()); runOnUiThread(() - updateBookList(remoteBooks)); } });10.0.2.2是 Android 模拟器访问宿主机 localhost 的特殊地址换成真机调试时必须改成电脑在局域网中的 IP。enqueue是异步回调不能在子线程里直接更新 UI所以用runOnUiThread切回主线程。Gson的TypeToken写法是为了告诉解析器目标是ListBook而不是单个 Book 对象。5.3 状态同步借阅状态以哪端为准引入网络后最大的坑是“借阅状态到底是本地数据库说了算还是服务端说了算”。设计原则是单一的权威数据源。如果服务端是主库那么本地 SQLite 只能算缓存借书操作必须请求服务端成功后才能改本地的available_copies和borrow_record。如果网络请求失败就提示“当前网络不可用请稍后重试”不能自动在本地把库存减一。否则用户借了一本书单机 App 显示可借数量减少了服务端却完全不知道这次借阅的存在后续所有统计都会错。5.4 离线兜底本地缓存与弱网体验演示过程中可能出现模拟器断网的情况所以需要设计“本地数据先展示远程数据后刷新”的策略。进入书目列表时先从 SQLite 查一次立即展示然后发异步请求等服务端返回后用新列表覆盖本地两者 book_id 一致的记录更新库存字段不一致的记录做新增处理。这样即使断网也能看到上一次同步的数据只是库存可能不是最新的。对答辩演示来说这种体验远比一个加载失败白屏的界面要好也体现你对真实业务场景有过考虑。6. 调试、验证与进阶从能跑到能答辩的检查清单6.1 用 adb 查看模拟器数据库内容开发中最容易踩的坑是“界面报错但不知道为什么”这时候最好的方法不是加日志而是直接拉数据库看数据。打开终端先确认设备连接adb shell run-as com.example.libsys ls /data/data/com.example.libsys/databases/run-as只能调试包使用release包没有这个权限。如果列表里有library.db就可以用下面命令拉出来看adb exec-out run-as com.example.libsys cat /data/data/com.example.libsys/databases/library.db /tmp/library.db拉出来的库用 SQLite 浏览器打开直接查book_info表里available_copies的值。这个技巧在答辩前检查数据非常高效不用在代码里到处打Log.d。6.2 三个必须测的边界场景任何图书管理系统跑演示的时候评委最喜欢问“借完了怎么办”。下边三个场景建议挨个测一遍对着表格检查 App 的表现测试场景操作预期结果库存为 0 时借阅找到可借数量为 0 的图书点击借阅提示“库存不足”借阅记录不产生重复借同一本书同一学号连续借两本同一图书第二本成功因为库存足够但只能产生两条记录归还未借的书在无借阅记录状态下点击归还提示“无借阅记录”库存不变如果第二项业务要求“一个学生同时只能借一本相同的书”那要在borrow_record里给book_id和student_id加联合唯一索引并限定status 0时不能重复插入。毕业设计里是否做这个限制取决于题目描述一般不做也不会被扣分但你要知道自己系统当前的逻辑边界在哪里。6.3 空态与图标给答辩演示加分的小细节功能全部跑通后界面体验是拉开分数差距的地方。两个位置最值得补一个是书目列表为空时显示“暂无图书请管理员添加”另一个是借阅成功后用 Toast 提示“借阅成功请在 30 天内归还”。图书封面的 URL 如果请求不到就不要占用线程直接用默认的灰色书本图标替代。更进阶的一步是给每本图书标记“馆藏位置”比如“A区-03架-第2层”。这个字段在数据库设计时已经预留了location界面里展示出来会让评委觉得你考虑了实体图书管理的真实场景而不仅仅是在写一张表的增删改查。本文还有配套的精品资源点击获取