Android图书管理系统:SQLite数据库设计与借阅流程实现指南
2026/9/11 16:49:04 网站建设 项目流程

简介:这是一份面向安卓初学者的毕业设计完整源码资源,基于安卓平台开发大学生图书管理系统,覆盖学生用户端与管理员端,实现图书查询、预约借阅、挂失归还、罚款交纳、读者信息管理、图书信息管理以及分级管理员权限等功能,业务模块完整,适合作为课程设计、毕业设计或项目实战的参考方案。资源打包为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 和 FloatingActionButton,recyclerview是书目列表的核心组件,okhttpgson是为第五章预留的网络能力。

工程目录保持 Android Studio 默认结构即可,MainActivity作为应用入口,负责承载书架列表的 Fragment 或者直接放一个 RecyclerView 的 Activity。不需要一开始就引入 MVVM 全家桶,ViewModel + LiveData 在数据量不大的管理系统里会让代码更绕。

2.3 界面导航结构:学生入口和管理员入口怎么切

图书管理系统 App 不能只有一个页面,至少要包含“登录/角色选择 → 书目列表 → 图书详情 → 借阅登记 → 归还确认”五个界面。常见做法是:首屏放两个大的 Button,一个“学生入口”,一个“管理员入口”。学生进入后能查书、借书、查看自己的借阅记录;管理员进入后能新增图书、修改库存、查看所有借阅记录。

首屏布局不需要复杂,核心是让状态切换简单:

<LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:gravity="center"> <Button android:id="@+id/btn_student" android:text="学生入口" /> <Button android:id="@+id/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_datedue_dateTEXT类型保存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_infoisbn字段加了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.Adapter<BookAdapter.BookViewHolder> { private List<Book> bookList; private OnBorrowClickListener listener; public BookAdapter(List<Book> 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 List<Book> searchBooks(String keyword) { List<Book> 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_infoavailable_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(); } }

beginTransactionsetTransactionSuccessful的组合是 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类,类里写两个方法:fetchBookListFromRemotesyncLocalData。在毕业设计阶段,这两个方法可以先返回假数据,或者留一个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(); List<Book> remoteBooks = gson.fromJson(json, new TypeToken<List<Book>>(){}.getType()); runOnUiThread(() -> updateBookList(remoteBooks)); } });

10.0.2.2是 Android 模拟器访问宿主机 localhost 的特殊地址,换成真机调试时必须改成电脑在局域网中的 IP。enqueue是异步回调,不能在子线程里直接更新 UI,所以用runOnUiThread切回主线程。GsonTypeToken写法是为了告诉解析器目标是List<Book>,而不是单个 Book 对象。

5.3 状态同步:借阅状态以哪端为准

引入网络后,最大的坑是“借阅状态到底是本地数据库说了算还是服务端说了算”。设计原则是:单一的权威数据源。如果服务端是主库,那么本地 SQLite 只能算缓存,借书操作必须请求服务端成功后,才能改本地的available_copiesborrow_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_idstudent_id加联合唯一索引,并限定status = 0时不能重复插入。毕业设计里是否做这个限制取决于题目描述,一般不做也不会被扣分,但你要知道自己系统当前的逻辑边界在哪里。

6.3 空态与图标:给答辩演示加分的小细节

功能全部跑通后,界面体验是拉开分数差距的地方。两个位置最值得补:一个是书目列表为空时显示“暂无图书,请管理员添加”,另一个是借阅成功后用 Toast 提示“借阅成功,请在 30 天内归还”。图书封面的 URL 如果请求不到就不要占用线程,直接用默认的灰色书本图标替代。

更进阶的一步是给每本图书标记“馆藏位置”,比如“A区-03架-第2层”。这个字段在数据库设计时已经预留了location,界面里展示出来会让评委觉得你考虑了实体图书管理的真实场景,而不仅仅是在写一张表的增删改查。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询