实验室设备管理系统APP开发实战:从SQLite建表到借用审批闭环

发布时间:2026/10/9 14:15:51
实验室设备管理系统APP开发实战:从SQLite建表到借用审批闭环
简介这是实验室设备管理系统APP的完整源码工程包面向毕业设计、课程设计、工程实训、大作业及竞赛项目等场景。项目包含移动端页面、后端接口和数据库脚本代码经运行验证功能正常可直接复现也便于在此基础上扩展新功能。压缩包内同时附有设计说明文档可为论文撰写与答辩展示提供参考。整个包体共78个文件大小约54.72MB。文件类型上45张png界面截图可直观核对页面效果15个html页面与3个js脚本负责展示与交互4个css文件控制系统样式4个php接口与sql数据库支撑后端逻辑和数据存储另有docx设计文档与xml配置等辅助说明目录结构清晰按模块定位方便。目前已有36人浏览学习。资料包含完整源码、工程文件、数据库脚本、设计文档及界面截图既可用于复现运行也能帮助理解前后端联调与管理系统设计思路。如遇使用问题可随时联系作者获得帮助适合需要在短时间内完成高质量管理系统项目的开发者与在校生。1. 实验室设备管理系统APP.zip一个能直接上台演示的完整闭环拿到写满“毕设课设实训大作业竞赛”字样的“实验室设备管理系统APP.zip”第一反应通常是怀疑这种打包好的项目里面到底是能跑的源码还是套壳PPT我的经验是只要压缩包结构完整、目录里没有乱码它通常能跑起来但能不能在答辩现场撑住五分钟取决于你怎么理解它的需求边界。这个系统解决的真实问题是实验室里“设备在哪、谁借走了、该不该催还、这个季度哪类设备最忙”这几件小事适合想用最短时间拿到可演示成果的学生也适合实训课和竞赛组队时做快速起步的基线项目。下面按我改造这种项目的顺序从需求、建库、写界面到避坑、演示一次讲完。2. 需求边界先立住设备管理不是 OA越收敛越好演示很多课设项目翻车不是因为代码少而是因为功能多到没法自圆其说。实验室设备管理系统APP的核心是“台账 流转”不是考勤、不是公告、不是聊天。你需要在写第一行代码前把设备的状态流和角色权限定死剩下的界面都是这些规则的投影。这一章先把“管什么”和“谁在管”说清楚后面所有建表和写接口都围绕这张边界来。2.1 设备状态机与借用状态机两个状态字段管住整条业务链设备本身要有状态借用单也要有状态两者不能混成一个字段。设备状态描述“这个东西能不能被借”借用单状态描述“这笔申请进行到哪一步”。设备侧我用四个枚举值0在库、1已借出、2维修中、3报废。借用单侧用五个枚举值0待审批、1已通过、2已拒绝、3已借出、4已归还。借用单是“已通过”还是“已借出”我建议分开存。“已通过”表示审批过了但人还没来领设备一旦领走设备状态置为1借用单状态置为3。归还时两者同步改设备回到0借用单变4。维修和报废不走借用单直接在设备台账里改状态但要在备注里写原因不然盘点时对不上账。逾期不单独存一个状态而是查询时算出来。多存一个“逾期中”状态意味着写代码的人必须在每处状态更新时维护它漏一处数据就脏一处。常见做法是查询时用日期函数比对省掉一个状态却换来正确的“待催还列表”很划算。SELECT d.device_name, b.borrower, b.expected_return_time, CAST(julianday(now) - julianday(b.expected_return_time) AS INTEGER) AS overdue_days FROM borrow_record b JOIN device d ON d.id b.device_id WHERE b.approve_status 3 AND b.actual_return_time AND b.expected_return_time date(now) ORDER BY overdue_days DESC;这段SQL的意思是从借用单里找出“已借出但没归还、且预期归还时间早于今天”的记录。approve_status 3是已借出actual_return_time 表示还没还julianday相减的结果转成整数就是逾期天数。查询比状态维护可靠演示时打开这个列表所有该催的设备一目了然。2.2 三种角色的权限矩阵学生、老师、管理员的界面差异课程设计里最容易出现的问题是所有人登录进去看到一模一样的界面这会让评审老师觉得你没有理解“管理”二字。我的习惯是至少分三种角色学生、实验室老师、管理员。学生只负责“查设备、借设备、看自己的记录”实验室老师负责审批学生的借用申请、维护设备台账、执行盘点管理员额外拥有用户管理和全量数据导出权限。功能点学生实验室老师管理员设备检索是是是发起借用是是否借用审批否是是设备录入/编辑否是是盘点否是是统计报表否是是用户管理否否是权限矩阵一旦定下来前端就可以按角色隐藏菜单项后端在每次操作时校验角色值。不需要做复杂的权限框架登录成功后在内存里保存一个role整数每次跳页面或调用接口前判断一下即可。这样写省时间答辩时也能讲清楚“我做了访问控制”。2.3 选型要“保守”本地 SQLite 优先后端按需再加我看到很多同学一上来就奔着MySQL加后端接口去结果被环境配置卡了两天。实验室设备管理这个场景最大的特点就是单点、轻量、演示优先。一个实验室的设备存量撑死几百条借用记录一年也就几千条SQLite完全扛得住而且APK扔到任何一台模拟器上都能直接跑不依赖外部服务。数据方案优点缺点适合场景纯 Android SQLite单机可跑演示零依赖不支持多端协同课设、实训Android 本机Spring Boot后端有真正的服务端分层能回答并发问题需要解决模拟器访问宿主机、网络权限毕设、竞赛Android 云数据库演示时不用开电脑需要账号配置现场网络不可控网络有保障的竞赛我一般建议把SQLite版本先跑通让整个业务流程闭环再决定要不要在外部包一层后端。评审老师问“为什么不用MySQL”你回答“设备管理是单实验室、轻并发场景本地数据库降低部署成本数据量上来后可以平滑迁移到服务端”这个说法既稳又真实。3. 从建表到跑通借用归还Android 端最小可复现工程需求边界立住后动手顺序我习惯是先建表再写数据库访问层然后写界面和业务方法最后配图表。数据库结构是地基表设计错了后面所有查询都别扭。下面的建表语句、Helper、Adapter和事务写法都是我在改造类似项目时反复用到的最小可复现方案照抄能跑理解后能改。3.1 设备台账、借用单、用户表三张表的设计与建表 SQL实验室设备管理系统APP里最关键的就是这三张表。device存设备静态信息和当前状态borrow_record存每一次借用申请的完整流转app_user存账号和角色。设备编号device_code必须唯一它是扫码盘点的识别依据也是演示时展示“资产编号”概念的抓手。CREATE TABLE device ( id INTEGER PRIMARY KEY AUTOINCREMENT, device_code TEXT NOT NULL UNIQUE, device_name TEXT NOT NULL, category TEXT DEFAULT 通用, location TEXT DEFAULT , custodian TEXT DEFAULT , status INTEGER DEFAULT 0, buy_date TEXT DEFAULT , warranty_date TEXT DEFAULT , remark TEXT DEFAULT ); CREATE TABLE borrow_record ( id INTEGER PRIMARY KEY AUTOINCREMENT, device_id INTEGER NOT NULL, borrower TEXT NOT NULL, borrow_time TEXT NOT NULL, expected_return_time TEXT NOT NULL, actual_return_time TEXT DEFAULT , approve_status INTEGER DEFAULT 0, approver TEXT DEFAULT , note TEXT DEFAULT , FOREIGN KEY (device_id) REFERENCES device(id) ); CREATE TABLE app_user ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_code TEXT NOT NULL UNIQUE, user_name TEXT NOT NULL, password TEXT NOT NULL, role INTEGER DEFAULT 0 );状态字段我统一用整数不用字符串。字符串“在库”“已借出”写起来直观但排序、统计、条件查询都会变别扭而且容易写错字导致数据对不上。整数枚举在代码里写好映射函数即可。日期字段全用TEXT存YYYY-MM-DD HH:mm:ssSQLite的日期函数能直接处理这种格式省去类型转换的麻烦。3.2 SQLiteOpenHelper 与种子数据第一次启动就能演示数据库访问层不需要引入任何第三方框架Android自带的SQLiteOpenHelper就够了。关键在onCreate里执行建表SQL后插入几条种子数据保证用户第一次打开APP就能看到内容而不是面对一个空列表发呆。这对演示来说几乎是必须的。public class DBHelper extends SQLiteOpenHelper { public static final String DB_NAME lab_device.db; public static final int DB_VERSION 1; public DBHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } Override public void onCreate(SQLiteDatabase db) { db.execSQL(CREATE_DEVICE_SQL); db.execSQL(CREATE_BORROW_SQL); db.execSQL(CREATE_USER_SQL); // 种子数据保证首次启动就有可演示的设备与账号 insertSeedData(db); } Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // 课设阶段最省事的迁移方案直接重建 db.execSQL(DROP TABLE IF EXISTS borrow_record); db.execSQL(DROP TABLE IF EXISTS device); db.execSQL(DROP TABLE IF EXISTS app_user); onCreate(db); } }DB_VERSION是个容易忽略的参数。以后你改了表结构比如给device加一个purchase_price字段只改建表语句不会生效必须把DB_VERSION加1并处理onUpgrade。课设阶段直接删表重建最省事但要清楚代价是旧数据全丢。真项目里应该用ALTER TABLE迁移这个区别能在答辩时讲出来就是个加分点。插入种子数据时custodian字段不要写具体人名建议存用户编号如U001与app_user.user_code对应。这样既能演示“责任人可追溯”又避免了硬编码个人信息数据规范上也更说得通。示例设备至少覆盖三类电子测量类、计算机类、通用工具类统计报表的饼图柱状图才有内容可画。3.3 借用审批的事务处理两行更新的原子性借用流程最怕出现“审批通过了但设备状态还是可借”的脏数据。学生提交借用申请时要插入一条借用单同时把设备状态改为“锁定”管理员审批通过时同样要改两个地方。这两个写操作必须放在同一个事务里要么都成功要么都回滚。public long submitBorrow(int deviceId, String borrower, String expectReturn) { SQLiteDatabase db dbHelper.getWritableDatabase(); db.beginTransaction(); long recordId -1L; try { ContentValues bv new ContentValues(); bv.put(device_id, deviceId); bv.put(borrower, borrower); bv.put(borrow_time, now()); bv.put(expected_return_time, expectReturn); bv.put(approve_status, 0); // 待审批 recordId db.insert(borrow_record, null, bv); ContentValues dv new ContentValues(); dv.put(status, 1); // 设备进入已借出/锁定状态 int rows db.update(device, dv, id? AND status0, new String[]{String.valueOf(deviceId)}); if (rows ! 1) { throw new IllegalStateException(设备已被占用本次借用提交失败); } db.setTransactionSuccessful(); return recordId; } catch (Exception e) { return -1L; } finally { db.endTransaction(); } }这段代码里最关键的是update语句中的AND status0。它不是装饰而是一个乐观锁只有当设备当前确实是在库状态时更新才会返回影响行数1如果设备已经被别人借走rows会是0抛异常回滚事务。这是并发控制里最朴素也最不容易写错的一种方式比先查询再判断的“两步走”可靠得多因为在查询和更新之间可能已经有人改了数据。归还流程和审批通过流程原理一样把approve_status改成对应值同时把device.status改回0也用事务包住。审批拒绝则只改借用单状态不动设备状态因为设备本来就没被锁定或已经被解锁。每次操作前想一下“有哪些表需要同步变”这个习惯能避免绝大多数数据不一致问题。3.4 统计报表GROUP BY 查询与图表库的 x 轴格式化统计是答辩时的视觉亮点。常见做法是做一个“设备分类占比”柱状图或饼图数据来源就是一个GROUP BY查询。SQL很简单但图表库的接入有两个高频坑一个是忘记给x轴配格式化器导致显示角标数字一个是数据为空时图表区域空白。String sql SELECT category, COUNT(*) AS cnt FROM device GROUP BY category ORDER BY cnt DESC; SQLiteDatabase db dbHelper.getReadableDatabase(); Cursor c db.rawQuery(sql, null); ArrayListString labels new ArrayList(); ArrayListFloat values new ArrayList(); while (c.moveToNext()) { labels.add(c.getString(0)); values.add(c.getFloat(1)); } c.close(); // 图表库的高频坑x轴必须配格式化器否则显示 0/1/2 而不是分类名 BarDataSet dataSet new BarDataSet(toBarEntries(labels, values), 设备数量); chart.getXAxis().setValueFormatter(new IndexAxisValueFormatter(labels)); chart.setData(new BarData(dataSet)); chart.invalidate();getFloat(1)取的是COUNT(*)的结果把它转成图表库需要的float数组。IndexAxisValueFormatter的作用是把x轴的下标映射成分类名称漏掉这一行柱状图下方只会出现0、1、2这类角标演示时观感很降分。chart.invalidate()用来触发重绘改完数据后不调用它界面可能不刷新。数据为空时先判空再setData或者直接显示一个“暂无数据”的TextView避免图表区域留白引发误解。4. 避坑指南跑通与翻车只隔了这五个细节这类项目的大部分问题不是业务逻辑难而是环境、权限、数据刷新这些细节在关键时刻冒出来。我把改造和测试过程中最常见的五类翻车现场按“现象、原因、解决”写清楚你在自己的机器上跑通后照着排查能省下大量时间。4.1 模拟器连不上本机服务10.0.2.2 与明文流量权限现象本地起了一个测试接口手机浏览器能访问APP里一请求就超时。原因有两个一是Android模拟器里的localhost指向模拟器自己要访问开发电脑必须用10.0.2.2二是Android 9之后默认禁止明文HTTP流量直接请求会抛异常。解决开发环境把服务地址统一写成http://10.0.2.2:8080/并在AndroidManifest.xml的application节点加android:usesCleartextTraffictrue。注意这个属性只应该留在开发调试阶段正式打包时去掉。真机调试时模拟器地址要换成电脑在局域网里的IP而且手机和电脑必须在同一个网段。4.2 RecyclerView 点击错位position 陷阱与 View Tag现象列表滚动后再点击某个设备弹出来的是另一台设备的信息。原因onBindViewHolder里把position直接赋给了点击事件而notifyDataSetChanged触发列表刷新后复用的ViewHolder持有的position已经变化点击事件却还拿着旧的闭包值。解决在onBindViewHolder里把设备ID通过holder.itemView.setTag(item.getId())存起来点击时从view.getTag()取值。基于ID操作而不是基于列表序号操作这是RecyclerView开发里必须养成的习惯。代码看起来只是多了一行但能少掉一个最隐蔽的逻辑错误。4.3 数据库文件导出后是 0KB 或找不到现象想从模拟器里把数据库文件拉出来查看文件管理器里看不到adb pull出来是个0字节文件。原因Android 10之后的分区存储机制应用数据库属于应用私有数据不在共享存储里文件管理器和普通adb pull路径都够不到。解决不要直接拉/data/data/包名/databases/下的文件先改用adb shell run-as 包名 cat databases/lab_device.db lab_device.db或者更简单的做法——在APP里写一个“导出备份”按钮把数据库文件通过FileProvider分享出去。这既是开发调试的实用功能也能在答辩时作为“数据备份方案”展示一举两得。4.4 图表一片空白或 x 轴显示角标现象柱状图区域存在但没有任何柱子或者柱子有了但x轴下方显示0、1、2而不是分类名。原因前者通常是数据源为空时没有判空图表库收到空数据后放弃绘制后者是没配IndexAxisValueFormatter。解决setData之前先判断values.size() 0为空就隐藏图表并显示空态文案。x轴格式化器的配置务必保留同时调用chart.invalidate()。图表库版本不同类名略有差异但“x轴必须显式绑定标签列表”这一点在所有主流库上都是通用的。4.5 打好的 APK 提示“应用未安装”现象调试阶段一直用Android Studio直接运行没问题生成正式APK传到模拟器上安装却提示未安装。原因debug签名的APK已经装过再用release签名装同一个包名系统认为签名不一致拒绝覆盖安装。解决先卸载旧包再装新包。要彻底解决就在build.gradle里配置固定的release签名文件让调试和发布用同一个keystore后续升级就不会再踩这个坑。每次发布前把versionCode加1versionName同步更新这个习惯能避免很多安装和升级的怪问题。5. 演示与交付把“能用”讲成“会做”代码跑通只完成了一半另一半是现场演示和材料交付。很多项目功能完整但演示散乱讲功能像念购物清单。这一章给你一套可复用的演示节奏和文档归档清单重点是让评审老师五分钟内建立起“这个系统是完整闭环”的印象。5.1 五分钟演示脚本按业务故事线串起功能演示最忌“想到哪个点哪个”。更有效的结构是跟着一笔借用业务走登录、查设备、发起借用、审批、归还、看报表一个故事走完所有核心功能都覆盖到。时间环节操作要点0:00-0:40登录与角色切换先用学生账号登录展示首页布局和数据概览0:40-1:50设备检索与详情输入关键词搜索展示设备状态、位置、责任人1:50-3:00借用发起与审批学生提交借用申请切换管理员账号审批通过3:00-3:40归还流程将设备归还验证设备状态回到“在库”3:40-4:30统计报表展示分类占比和借用频次排行4:30-5:00延伸展示演示盘点入口或数据导出备份功能切换账号这个动作不要跳过它最能体现你做了角色权限设计。在演示前先把种子账号准备好密码统一设置为简单值减少现场输入的出错可能。如果现场网络不稳定提前把数据都放在本地确保不依赖后端也能走完全流程。5.2 正式打包和文档归档别让代码成为唯一的交付物课程设计和毕业设计除了能跑的代码还需要一份能自洽的说明文档。我习惯的归档结构是README.md放项目简介和运行方法docs/需求说明.md写功能列表和角色权限docs/数据库设计.md放表结构说明和ER图screenshots/放每个核心页面的截图标注操作流程最后附一份演示视频或者演示脚本。打包时注意三个检查点一是统一签名别用debug包直接交二是给APP换一个正式的图标和应用名称默认的机器人图标会拉低完成度三是关掉调试日志输出用BuildConfig.DEBUG把日志包一层正式包保持干净。5.3 评审老师最常问的三个问题与应答分寸“为什么用SQLite不用MySQL”是出镜率最高的问题。回答方向是“这个场景单实验室、轻并发SQLite零部署、数据文件可备份完全满足需求如果扩展到多个实验室跨校区使用我会把数据层换成服务端MySQL接口层已经预留”。这样说既承认了选型又展示了扩展思路。“并发借用怎么办”按3.3里的乐观锁回答提交借用时通过status0条件更新设备状态影响行数不是1就回滚从数据库层面杜绝了同一台设备被重复借出的可能。如果想让答案更完整可以补一句“高并发场景会引入设备表版本号做乐观锁或者把借用操作放到服务端队列里”。“数据丢了怎么办”对应备份机制。展示数据库导出按钮说明可以定期把lab_device.db导出为备份文件生产环境则部署MySQL并做定时备份。每个问题的回答都遵循“当前方案 为什么够用 如何扩展”的结构篇幅不长但显得有工程判断。6. 进阶给设备列表加上扫码盘点现场演示的加分技巧当核心流程都稳定后我想推荐一个性价比极高的进阶功能扫码盘点。打印机把每台设备的device_code生成二维码贴在设备上APP里用摄像头扫一下自动将该设备标记为“已盘点”。这比在列表里手动搜索更真实也更贴合“实验室设备管理”这个场景竞赛评审时是一个容易讲清楚的亮点。实现思路并不复杂扫描回调拿到二维码里的设备编号去数据库查对应设备ID已扫过的设备放进一个内存集合去重同时更新设备表里的last_check_time字段。盘点结束后用一个查询把“盘点时间早于本次批次开始时间”的设备列为盘点差异导出成报告。public void onScanResult(String rawText) { if (rawText null || rawText.trim().isEmpty()) return; Device d deviceDao.getByCode(rawText.trim()); if (d null || scannedIds.contains(d.getId())) return; scannedIds.add(d.getId()); deviceDao.updateLastCheckTime(d.getId(), System.currentTimeMillis()); runOnUiThread(() - adapter.addScannedItem(d)); }这段代码里scannedIds是HashSetInteger用来防止同一台设备重复扫码导致统计出错。runOnUiThread必须有因为扫码回调里的线程不是UI线程直接操作Adapter会崩溃。盘点差异查询的SQL写法和2.1里的逾期查询类似本质是“筛选出未出现在某次集合中的数据”。扫码模块集成时最容易踩的坑是预览画面过曝或对焦慢。我自己调扫码模块时固定把焦点模式改为手动、曝光模式改为手动预览分辨率固定在640x480这个习惯救过不少次现场演示——有些演示环境灯光特别亮自动曝光会让二维码白成一片。希望这次整理的方案能帮到你把路线规划好逐个模块做扎实这类项目跑通一次对你理解完整的工程闭环会有很大帮助。本文还有配套的精品资源点击获取