
简介这是一份基于Python打造的实验室设备管理系统完整项目面向实验员、教师或管理员覆盖设备入库、借用归还、维护提醒与使用统计等日常管理场景。项目包含约2000个文件压缩包大小24.45MB主体为py源码与pyc编译文件同时包含数据库交互、Web界面渲染、国际化多语言模板、前端样式脚本及部署配置等类型目录结构较为完整便于按模块研读与实践。目前已有144人学习下载。系统在设计上贯穿关系型数据库建模、Web框架路由、用户认证与权限控制、表单校验、CRUD接口、日志与异常处理、设备借用频次统计等关键知识点适合作为Python Web全栈开发的学习项目。读者可从源码结构、资源配置与功能逻辑中掌握从后端数据模型到前端交互的完整实现思路也可参考其中的报告统计与部署运维方案为扩展更多实验室管理需求打下基础。1. “python实验室设备管理系统.zip”到底是什么拆包前先想清楚自己要什么拿到一个 python实验室设备管理系统.zip先别急着解压跑起来。这个压缩包解决的是一个很具体的问题实验室里几百台设备还在用 Excel 登记、借用靠口头、月底盘点靠人肉对账这套系统能把台账、借还、维修、报废和统计装进一个 Web 界面。它是 Python 写的最常见的形态是 Flask 加 SQLite 或 MySQL配一个管理员后台和登录借还页。你要是正在做课程设计、想在公司内网搭一套轻量资产工具或者想拿真实业务练手 Python Web 开发这个方向都值得拆开看一遍。按我的经验先验证它能不能跑再决定是改造还是照着重写。2. 接手前的第一件事目录结构、技术栈与数据库表设计怎么定压缩包解压后第一时间别点 run.py 看窗口先花十分钟把包里有什么摸清楚。我见过太多下载源码后直接双击启动、下一秒闪退就开始骂“代码有问题”的情况其实大部分闪退都是环境问题。先看结构能省掉后面大半的排错时间。2.1 拿到 zip 先做四个检查比马上写代码重要先确认包里的文件是不是完整再决定要不要在它基础上改。这一步决定了后面几天的效率。unzip python实验室设备管理系统.zip -d lab_system cd lab_system pwd ls -la find . -maxdepth 2 -type f | sort | head -30第一行把压缩包解压到lab_system目录避免和当前目录的其它文件混在一起第二行进去第三行确认当前路径和控制台工作目录第四行列出两层以内的所有文件快速看结构。拿到文件列表后按四个点核对入口文件常见名字是app.py、run.py、manage.py。没有入口文件的项目基本没法直接跑。依赖声明看有没有requirements.txt有就说明导库是可控的没有的话后面你会在 ImportError 里逐个补。数据库初始化有些项目自带init_db.py或create_db.py有些靠db.create_all()自动建表还有些给你一份lab.sql让你自己导入 MySQL。注意区分清楚。说明文档有没有 README 或者说明文档。文档里一般写了 Python 版本、默认管理员账号、启动顺序。如果这个压缩包里一个入口文件都没有也没带任何数据库脚本那它大概率是个残缺包或从黑匣子拷贝出的半成品别浪费时间猜结构直接按后面第 2.3 节的三张表重写主体反而更快。2.2 技术栈选择为什么最稳妥的组合是 Flask SQLAlchemy SQLite你可能会在包里看到 Flask、Django、FastAPI 三种框架写的版本。选型不是越新越好而是要和你自己的维护能力匹配。这套系统的典型使用场景是内网几十个人用、数据量不大、业务逻辑集中在设备借还上Flask 比 Django 轻得多。对比点FlaskDjango上手成本一个文件就能起服务适合快速验证项目管理命令多结构固定数据层SQLAlchemy 自己配灵活ORM 内置但换数据库要改配置功能重量登录、后台、导出都要自己接库自带 Admin 和认证体系这个系统的适配度合适改动面小偏重适合大型业务系统数据库方面SQLite 和 MySQL 也不是对立关系而是不同阶段的选择。单机演示、内网几十人用SQLite 完全够如果预期并发写操作很多或者要长期积累历史数据再迁 MySQL。迁移成本也不高后面第 5.3 节会专门讲什么时候该迁。依赖清单建议整理成下面这种形式Flask2.2 Flask-SQLAlchemy3.0 Flask-Login0.6 openpyxl3.1 qrcode7.4 Pillow10.0Flask-SQLAlchemy3.x 依赖 SQLAlchemy 2.x写法上比旧版多了db.session.get()这些方法后面代码默认按这个版本写。openpyxl专门处理 Excel 导入导出qrcode加Pillow用来生成设备二维码这套组合基本覆盖了设备管理系统的全部功能边界。安装环境时用pip install -r requirements.txt一次装齐如果你在用 VSCode 写代码记得先选对 Python 解释器再装包避免装到了全局环境里项目却找不到。2.3 三大核心表users、devices、borrow_records 的设计要点不管压缩包里原来的代码怎么写的设备管理系统的核心逃不出三张表用户表、设备表、借用记录表。先看这三张表的设计基本就能判断一个源码包的质量。from datetime import datetime from flask_sqlalchemy import SQLAlchemy db SQLAlchemy() class User(db.Model): __tablename__ users id db.Column(db.Integer, primary_keyTrue) username db.Column(db.String(64), uniqueTrue, nullableFalse) password_hash db.Column(db.String(256), nullableFalse) role db.Column(db.String(16), nullableFalse, defaultuser) # admin/user class Device(db.Model): __tablename__ devices id db.Column(db.Integer, primary_keyTrue) sn db.Column(db.String(64), uniqueTrue, indexTrue) # 资产编号 name db.Column(db.String(128), nullableFalse) category db.Column(db.String(64), indexTrue) # 光谱仪/离心机/示波器 location db.Column(db.String(128)) # 存放房间和工位 status db.Column(db.String(16), nullableFalse, defaultidle) purchase_date db.Column(db.Date) class BorrowRecord(db.Model): __tablename__ borrow_records id db.Column(db.Integer, primary_keyTrue) device_id db.Column(db.Integer, db.ForeignKey(devices.id)) user_id db.Column(db.Integer, db.ForeignKey(users.id)) borrow_time db.Column(db.DateTime, defaultdatetime.utcnow) due_time db.Column(db.DateTime) # 应归还时间 return_time db.Column(db.DateTime) # 实际归还时间 is_overdue db.Column(db.Boolean, defaultFalse)设备表只存静态属性和当前状态借用记录表存每一次借出的生命周期两边通过外键关联。sn是资产编号和自然主键id分开因为盘点、贴二维码、Excel 导入时用户看到的是sn不是数据库自增 id。category和location建立索引是因为统计报表基本都按这两个字段分组加了索引以后聚合查询会快很多。借用记录里的due_time和return_time分开存超时判断直接拿两者差值就行不用再倒推规则。这里最关键的字段是Device.status我把它设计成字符串而不是布尔值因为它要表达的状态不只是“可借/不可借”两种具体怎么转下一章展开。3. 把设备台账做成能用的系统状态机、借用归还与权限控制的落地代码设备和图书的最大区别是它还有维修、报废、校准这些生命周期状态。只做一个“借出/归还”页面那不叫设备管理系统叫借条登记工具。这一章直接写状态机怎么设计以及借还接口和权限控制怎么落地。3.1 设备状态机为什么不能用“可用/不可用”两个状态硬扛我见过不少源码把设备状态设计成is_available一个布尔字段前端勾一下“可用”就行。等到设备送修的时候问题就来了维修中的设备不能借但修完之后要能再上架如果只有一个布尔值你用什么区分“在库里”和“维修中”总不能修一次就删掉一条设备记录再重建一次。正确做法是用一组固定的状态字符串把状态流转限制在下面这张表里当前状态允许动作目标状态idle 空闲借出borrowed 已借出borrowed 已借出归还idle 空闲idle / borrowed送修repairing 维修中repairing 维修中维修完成上架idle 空闲idle / borrowed / repairing报废scrapped 已报废状态机的好处是业务规则能被代码强制约束。比如“维修中的设备不能直接被借出”在接口里就是一行状态判断而如果用布尔值你得靠前端隐藏按钮来避免误操作后端完全不知道发生了什么事。内网系统最怕的就是数据对不上账状态字段一旦被随意写三个月后盘点你会疯掉。实现时建议直接用字符串常量不用 Python Enum因为状态值要存进数据库字符串在 SQL 里查起来直观也方便迁移。代码里把状态名定义为模块级常量避免每次手写字符串拼错。3.2 借用归还接口状态校验、事务边界与超时计算借出接口的核心是两个动作插入一条借用记录把设备状态改成已借出。这两个动作必须在同一个事务里提交否则会出现“设备显示可借但记录已经存在”或者反过来“记录没了设备被锁死”的脏数据。from datetime import datetime, timedelta from flask import jsonify, request from flask_login import current_user, login_required app.route(/api/borrow, methods[POST]) login_required def borrow_device(): data request.get_json(forceTrue) or {} device db.session.get(Device, data.get(device_id)) if device is None or device.status ! idle: return jsonify({ok: False, msg: 设备不存在或当前不可借}), 400 record BorrowRecord( device_iddevice.id, user_idcurrent_user.id, borrow_timedatetime.utcnow(), due_timedatetime.utcnow() timedelta(days7) ) device.status borrowed db.session.add(record) db.session.commit() return jsonify({ok: True, record_id: record.id})db.session.get(Device, data.get(device_id))是 SQLAlchemy 2.x 推荐的主键查询写法比老的Device.query.get()更明确。request.get_json(forceTrue)的意思是即使请求头没写Content-Type: application/json也尝试把 body 当 JSON 解析方便调试。due_time用 UTC 时间加上 7 天作为默认借期这里用timedelta(days7)如果你想做时长配置把这个值挪到系统设置表里别写死在代码里。归还接口要处理超时判断app.route(/api/return, methods[POST]) login_required def return_device(): data request.get_json(forceTrue) or {} record BorrowRecord.query.filter_by( device_iddata.get(device_id), return_timeNone ).first() if record is None: return jsonify({ok: False, msg: 未找到未归还记录}), 400 now datetime.utcnow() record.return_time now if record.due_time and now record.due_time: record.is_overdue True record.device.status idle db.session.commit() return jsonify({ok: True, overdue: record.is_overdue})归还时不要只按 record_id 查而是按“设备 未归还”过滤这样即使同一个人在界面重复点了两次归还也不会误伤另一条记录。is_overdue在归还瞬间计算并落库而不是每次报表都现场算时间差这样历史记录保持稳定不会因为后来改规则而全部变化。这里有个容易忽略的点BorrowRecord.query.filter_by(return_timeNone)查的是 SQL 的NULL所以写记录时千万别把空字符串放进return_time空字符串和NULL在 SQL 里是两个东西。3.3 权限控制装饰器管住管理员页面的最低成本写法设备管理系统至少要分管理员和普通用户两种角色。普通用户只能借还和查看台账管理员才能改设备信息、审核报废、导入 Excel。权限控制最低成本的方案是写一个装饰器统一挡在路由函数外面。from functools import wraps def admin_required(fn): wraps(fn) def wrapper(*args, **kwargs): if not current_user.is_authenticated: return jsonify({ok: False, msg: 请先登录}), 401 if current_user.role ! admin: return jsonify({ok: False, msg: 没有管理员权限}), 403 return fn(*args, **kwargs) return wrapper配合 Flask-Login 的login_required一起用在视图函数上顺序有讲究app.route(/admin/devices, methods[GET, POST]) login_required admin_required def admin_devices(): # 只有管理员能访问 ...装饰器自下而上包装所以login_required在顶层先执行匿名用户最先被挡下然后才到admin_required检查角色。反过来写的话未登录用户会先被你的角色装饰器拦截此时current_user还是一个匿名对象你可能拿不到role属性直接抛异常。这个顺序问题我在初稿翻过车注意别看代码能跑就忽略它。权限控制还有一层容易漏装饰器管“谁能调接口”函数体里要管“这个状态能不能做”。比如普通用户被挡住了管理员能不能把已报废的设备改成维修中这是业务校验不是权限校验得在函数里再判断一次。两层各管各的缺了业务校验状态机就会变成摆设。4. 让数据流转起来Excel 导入导出、盘点与统计报表的实现思路设备台账建起来之后真正的日常工作发生在数据搬运上管理员要把老台账从 Excel 里导进系统盘点时要对着实物清点月底要给领导出报表。这三件事做不好系统就会沦为摆设。4.1 openpyxl 批量导入设备模板校验、错误行回显与重复跳过成百上千台的旧设备不可能让管理员在网页上一个一个新增。常见做法是提供一份 Excel 模板管理员按列填好名称、编号、分类、存放地然后整表导入。用openpyxl读文件核心逻辑是逐行校验而不是逐行入库。from openpyxl import load_workbook def import_devices(file_path): wb load_workbook(file_path, read_onlyTrue, data_onlyTrue) ws wb.active errors, ok_count [], 0 for row_idx, row in enumerate(ws.iter_rows(min_row2, values_onlyTrue), start2): sn, name, category, location row[0], row[1], row[2], row[3] if not sn or not name: errors.append(f第{row_idx}行缺少必填项) continue if Device.query.filter_by(snsn).first() is not None: errors.append(f第{row_idx}行资产编号重复: {sn}) continue db.session.add(Device(snsn, namename, categorycategory, locationlocation)) ok_count 1 db.session.commit() return ok_count, errorsread_onlyTrue让 openpyxl 以流式方式读文件遇到几千行的 Excel 也不会撑爆内存data_onlyTrue表示取单元格的计算结果而不是公式本身防止你读到A1B1这种公式串。iter_rows(min_row2, values_onlyTrue)从第二行开始读默认第一行是表头。每条记录先在内存里做完整性校验和重复校验成功进 session不成功把行号记进 errors。最后统一 commit这样如果中间有几百条错误最多回滚一次而不是每行开一个事务。这里给你一个建议导入接口暂时不要做“部分成功部分失败”的复杂逻辑最稳的做法是一次性检查全部通过再写入有任何错误行就整批返回错误列表让管理员在 Excel 里改好再重新传。对设备台账这种低频操作安全优先于效率。4.2 设备二维码生成与扫码盘点把“数资产”变成对着实物核对设备管理的最终目的是知道“资产在不在、在哪、谁在用”。每个月最累的就是盘点一个人拿纸质表去实验室一台台对另一个人在旁边喊编号。二维码能让这个流程快十倍。每台设备生成一个二维码贴上后盘点时扫码就能确认实物与系统一致。import qrcode def generate_qr(device): content fDEV-{device.sn} img qrcode.make(content, box_size8, border2) img.save(fqr/{device.sn}.png)二维码内容不要只放数据库 id要放资产编号sn因为盘点的人拿到码后是直接看内容的不是扫出来再查系统。box_size8表示每个黑色模块 8 像素值越大打印出来越清晰border2是白边宽度太窄的话扫码可能识别不出来。生成后按sn命名文件打印时直接按资产编号归档方便找。盘点页面的逻辑不复杂扫码后把sn传到接口系统查设备是否存在、状态是否符合预期再返回确认信息。盘点结束导出差异列表把“系统有但没扫到”和“扫到但系统没有”的两拨设备列出来。注意这里有个执行细节盘点接口要记录盘点人和盘点时间否则下次盘点时你根本不知道上一次是什么时候盘的审计不好做。4.3 报表与统计一条聚合 SQL 撑起设备台账看板统计报表最常用的是两张按分类看设备分布按部门看借用次数。前者了解资产结构后者判断使用强度。SQL 写起来很简单关键是别在 Python 里手动分组。SELECT category, status, COUNT(*) AS device_count FROM devices GROUP BY category, status ORDER BY category;这条 SQL 按“分类 状态”分组跑出来的结果直接就是一个矩阵光谱仪、离心机、示波器各有多少台在借、在修、空闲。不用把数据全部加载进来再在 Python 里做循环统计数据库才是干这活的。统计借用次数时要连用户拿部门信息SELECT u.department, COUNT(br.id) AS borrow_count FROM borrow_records br JOIN users u ON u.id br.user_id GROUP BY u.department ORDER BY borrow_count DESC;在 SQLAlchemy 里执行原生 SQL 用db.session.execute(db.text(sql))拿到的结果集可以直接传给模板渲染成表格。报表不需要做得太花哨能导出 CSV 给领导看一眼就够了。记得给大结果集加日期范围条件比如br.borrow_time :start否则历史数据越攒越多报表会越来越慢。5. 五个高频避坑从 Windows 路径到 SQLite 并发开发时最容易翻车的点这一章的坑我基本都亲自踩过而且是那种“代码看着没问题一跑就出事”的类型。按出现频率排序前三条尤其值得注意。5.1 现象run.py 闪退数据库文件“消失”双击run.py后窗口一闪就没了你打开项目目录看不到lab.db但在其它目录却能找到一个空的数据库文件。原因是 Windows 下双击运行时工作目录是脚本所在目录吗不一定。你用 PyCharm 或 VSCode 的终端跑很正常但双击或用计划任务启动时当前工作目录可能完全不一样相对路径sqlite:///lab.db就建到了别处。解决方式是用绝对路径拼数据库地址以项目文件所在目录为基准import os BASE_DIR os.path.dirname(os.path.abspath(__file__)) app.config[SQLALCHEMY_DATABASE_URI] fsqlite:///{os.path.join(BASE_DIR, lab.db)}这里__file__是当前 Python 文件的绝对路径os.path.dirname取目录再拼上lab.db数据库文件永远落在项目根目录。注意sqlite:///后面跟绝对路径时是三个斜杠。这个习惯在 Linux 下用 systemd 启动时同样重要因为你永远不知道守护进程会把工作目录切到哪里。5.2 现象jsonify 返回 borrow_time 直接 TypeError接口返回借用记录时borrow_time是datetime对象Flask 的jsonify默认不认识它直接抛TypeError: Object of type datetime is not JSON serializable。原因不复杂JSON 格式里没有日期时间类型必须转成字符串。但我见过不少代码在这个坑前选择“不用 datetime改存字符串”这是给后面挖坑。正确做法是序列化时统一转成 ISO 格式字符串from datetime import datetime, date def json_default(obj): if isinstance(obj, (datetime, date)): return obj.isoformat() raise TypeError(fType {type(obj)} not serializable) # Flask 2.2 设置全局 JSON 编码器 app.json.default json_default设置一次以后所有接口里的datetime和date都会自动变成2025-01-01T10:00:0000:00这种字符串前端拿到后new Date(...)直接解析。比在每个视图函数里手动调isoformat()干净得多。注意如果你用datetime.utcnow()生成的是 naive 时间不带时区后缀前端解析时按本地时区理解跨时区场景要格外小心内网系统一般无所谓。5.3 现象多用户同时借还时 SQLite 报 database is locked系统刚上线时只有管理员一个人用大家都觉得挺快。等学生开始集中借设备接口开始偶尔抛database is locked。原因在于 SQLite 的写锁是整个数据库级别的两个写请求同时到达后到的会直接失败而不是排队等待。Flask 默认每请求一个 session并发一上来必然撞车。解决方案分两步走开启 WAL 日志模式让读写互不阻塞同时把 busy_timeout 调大from sqlalchemy import event from sqlalchemy.engine import Engine event.listens_for(Engine, connect) def set_sqlite_pragma(dbapi_connection, connection_record): cursor dbapi_connection.cursor() cursor.execute(PRAGMA journal_modeWAL) cursor.execute(PRAGMA busy_timeout5000) cursor.close()WAL 模式把写操作改成追加日志读操作可以并发进行busy_timeout5000表示写请求遇到锁时最多等 5 秒而不是立刻报错。内网三五十人规模这套配置完全跑得住。如果以后并发继续涨把数据库 URI 从sqlite:///lab.db换成mysqlpymysql://user:passwordlocalhost/lab_db模型代码基本不用改因为 SQLAlchemy 帮你挡掉了多数方言差异。别一开始就上 MySQLSQLite 能让你在开发期少配置很多东西。5.4 现象普通用户绕过前端直接调接口把设备状态改掉前端把“报废”按钮只渲染给管理员普通用户看不到页面入口。但你拿浏览器开发者工具分析一下请求地址照样能POST /api/scrap。如果视图函数里没做角色校验这请求就成功了。解决方式是后端权限校验不能依赖页面必须在每个写接口上同时加装饰器和业务校验app.route(/api/scrap, methods[POST]) login_required admin_required def scrap_device(): device db.session.get(Device, request.json.get(device_id)) if device.status scrapped: return jsonify({ok: False, msg: 设备已是报废状态}), 400 device.status scrapped db.session.commit() return jsonify({ok: True})装饰器解决“谁能调”函数体里的状态判断解决“能不能调”。两个层次缺一不可。这也是很多源码包最弱的地方前端隐藏了按钮后端却没有对应校验看起来功能正常实际上安全边界形同虚设。拿到别人的源码后我一般会全局搜一下app.route逐个看有没有漏掉权限装饰器。5.5 现象导出的文件拿到 Windows 打开全是乱码用 pandas 或 csv 模块导出中文 CSV在 Mac 上打开一切正常发到 Windows 上用 Excel 打开就乱码。原因不是代码坏了是csv默认用 UTF-8 编码而 Windows 的 Excel 默认按 GBK 解析。解决方式是在写入文件时明确指定utf-8-sig编码import csv with open(devices_export.csv, w, encodingutf-8-sig, newline) as f: writer csv.writer(f) writer.writerow([资产编号, 名称, 分类, 状态]) # 逐行写设备数据utf-8-sig会在文件开头写入一个 BOM 头Excel 看到 BOM 就知道这是 UTF-8 编码。如果导出的是 xlsx 而不是 csv用openpyxl保存即可不涉及这个编码问题。顺带提醒一句有人在调试时发现 xlsx 文件打不开试着用解压工具打开发现里面是 XML 文件——这是正常现象xlsx 本身就是个 zip 容器千万别因为这个去“修复”文件。6. 进阶收尾用 Flask-Admin 快速搭管理后台再挂一个健康检查脚本设备管理系统的管理页面其实很套路列表、搜索、编辑、删除。如果压缩包里没有现成的后台页面与其一个模板一个模板手写不如先用 Flask-Admin 把 CRUD 页面搭出来省掉一半重复劳动。6.1 用 Flask-Admin 少写一半 CRUD 页面from flask_admin import Admin from flask_admin.contrib.sqla import ModelView admin Admin(app, name实验室设备管理, template_modebootstrap4) admin.add_view(ModelView(Device, db.session)) admin.add_view(ModelView(BorrowRecord, db.session))注册之后后台自动生成设备的列表、新增、编辑、删除页面还自带按字段筛选和搜索。设备管理这种内部工具管理端功能不是核心价值数据准确才是所以借助现成框架把后台撑起来把精力留给借还流程和盘点逻辑。需要给 ModelView 加权限时可以重写is_accessible方法做角色判断但要注意覆盖的不只是访问还有增删改的方法名否则可能出现“看得到入口但操作不了”的尴尬。6.2 挂一个健康检查接口定期确认服务活着系统部署到内网服务器后服务和数据库任何一方挂掉都要等用户报障才知道太被动。最简单的是一个健康检查接口app.route(/health) def health(): try: db.session.execute(db.text(SELECT 1)) return jsonify({status: ok}) except Exception: return jsonify({status: error}), 500db.text(SELECT 1)是 SQLAlchemy 2.x 推荐的原生 SQL 包装方式比直接传字符串更安全。这个接口可以配到监控脚本里每分钟请求一次失败就告警。顺便在返回值里加个设备总数监控时顺带看数据有没有异常增长。6.3 上线前照这张检查单选做一遍别信“能登录就算成功”检查项操作方式登录与注销管理员和普通用户各测一遍借还流程空闲设备借出再对已借设备发起借出超时标记手动把 due_time 改到过去归还后确认 is_overdue批量导入故意错一行确认错误行号和原因能回显二维码打印一张扫码内容与资产编号一致统计报表分组结果与原始记录逐条对账我最早写这类系统时状态字段用了“可用/不可用”两个布尔值盘点时发现同一台设备在表里出现了三种状态写法统计报表全是乱的。后来强制固定状态枚举并把每次状态变更写进一条变更日志才真正把账对平。这也是我拿到别人源码时最先看状态字段和权限装饰器的原因。希望你在这个项目上少踩我踩过的坑一次把状态机设计好后面能省很多事。希望帮到你。本文还有配套的精品资源点击获取