ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Flask的虚拟实验室设备租赁管理系统设计与实现

基于Flask的虚拟实验室设备租赁管理系统设计与实现 只要你接触过高校实验室的设备管理大概都会遇到同样的场景设备台账靠Excel、借用靠微信群接龙、归还靠打电话提醒。这套以“python基于flask的虚拟实验室设备租赁管理系统”为题的Web项目就是把这些线下流程搬到线上学生在线预约设备教师或管理员在线审核系统自动记录借还时间、计算逾期、维护设备库存顺带把设备档案和借用历史全部沉淀下来。我这两年帮人改过不少这类课设和毕设代码也实际带过实习生做类似的实验室管理系统今天就把这套系统的设计与实现完整拆一遍重点讲清楚每一步为什么这么做以及在实操中容易踩的坑。这类系统的难度其实不高难的是把业务规则理清楚。Flask本身非常轻量不强制你用什么数据库、什么ORM、什么扩展但正因为灵活新手容易把代码写成“路由大杂烩”。所以下面我会按项目设计的完整链路来讲从需求梳理、数据库设计到核心模块实现、问题排查再到最后部署上线每个环节都会附上可以直接抄的代码和经验。1. 项目整体设计与思路拆解1.1 先把业务边界说清楚接到这个题目第一步不是写代码而是把“虚拟实验室设备租赁”这几个字拆开看清楚。这里的“虚拟实验室”在多数场景下指的是线上实验室管理平台设备本质上还是实际存在的仪器设备只是预约、审核、借还这些动作全部虚拟化、线上化。有的学校也会把一些纯软件类资源比如授权License、仿真软件账号当“设备”来管理逻辑上是一样的都有数量、有可用状态、有借用周期。从角色角度看一个完整的系统至少要有三类用户学生浏览设备、提交租赁申请、查看个人借用记录、申请归还。教师/实验员审核学生申请、确认发放设备、登记归还信息、管理自己所负责的设备。管理员用户管理、设备分类与设备档案管理、所有订单的查询与统计、处理异常比如设备损坏、逾期强扣。核心业务流程也非常清晰学生检索设备 - 提交预约申请 - 教师/管理员审核 - 审核通过后学生线下领取设备 - 到期归还 - 系统更新库存与状态。如果到期未还系统要能标记逾期并计算逾期费用。再往细了说还可能有设备维修登记、故障上报、日志审计等等。把这些业务边界理顺之后你会发现一个很重要的结论这个系统本质上是围绕“租赁订单状态”运转的。谁能看到什么、能操作什么、设备库存怎么变化全部取决于订单当前处于哪个状态。所以设计阶段的重心要放在数据模型和状态流转上而不是急着写页面。1.2 为什么选Flask这套技术栈选择Flask而不是Django对这个场景来说是很合理的选择。Flask的核心卖点是“小”一个跑起来很基础的应用就几个文件对前端路由、数据库表结构的控制权完全在自己手里。做课设或毕设答辩时导师问“这个路由为什么这么写”“这个装饰器起什么作用”你能讲得很清楚这在答辩时是加分项。而Django把ORM、Admin后台、认证体系全给你配好了自己写的业务代码反而不多讲原理时容易露怯。另外从实用角度讲Flask周边的扩展完全可以按需组合Flask-SQLAlchemy管数据库Flask-Login管登录态Flask-WTF管表单校验Flask-Migrate管表结构迁移。这套组合在中小型管理系统里几乎成了事实标准社区资料多、坑也都被踩平了非常适合这种单机即可运行、后续又能平滑扩展的项目。需要提醒的是Flask 3.x 和 Flask 2.x 在一些细节上有差异比如 app.route 的配置方式、before_request 的处理网上大量教程还是老的写法。如果你用的是最新版遇到不兼容的问题先查一下官方文档的升级说明不要盲目复制粘贴旧代码。2. 数据库设计与核心模型2.1 数据表结构规划数据库设计是这套系统最关键的部分。我见过很多半途而废的项目问题基本都出在“表关系”没理清后面写业务逻辑越写越乱。这个系统需要几张核心表我直接给出一版经过实践验证的模型。用户表存登录账号、密码哈希、角色、姓名等信息。角色字段我用简单的字符串因为它只有三个固定值不需要额外建表。from flask_sqlalchemy import SQLAlchemy from flask_login import UserMixin from werkzeug.security import generate_password_hash, check_password_hash from datetime import datetime db SQLAlchemy() class User(UserMixin, db.Model): __tablename__ user id db.Column(db.Integer, primary_keyTrue) username db.Column(db.String(64), uniqueTrue, nullableFalse, indexTrue) password_hash db.Column(db.String(128), nullableFalse) role db.Column(db.String(16), nullableFalse, defaultstudent) # admin / teacher / student real_name db.Column(db.String(32)) student_no db.Column(db.String(32)) # 学号学生角色时使用 created_at db.Column(db.DateTime, defaultdatetime.now) def set_password(self, password): self.password_hash generate_password_hash(password) def check_password(self, password): return check_password_hash(self.password_hash, password)注意密码永远不要明文存储。werkzeug.security 自带的哈希方案足够应付这个场景不用自己去写加密函数。设备表存设备基础信息、分类、库存数量和当前可用数量。这里刻意把 total_quantity 和 available_quantity 拆开目的是让“总数”和“当前可借数”分开维护。有人会觉得用一个字段 plus 一个状态字段就行但那样处理“部分借出”的场景会非常痛苦。class Equipment(db.Model): __tablename__ equipment id db.Column(db.Integer, primary_keyTrue) name db.Column(db.String(128), nullableFalse) model db.Column(db.String(128)) # 型号 category_id db.Column(db.Integer, db.ForeignKey(category.id), indexTrue) total_quantity db.Column(db.Integer, nullableFalse, default1) available_quantity db.Column(db.Integer, nullableFalse, default1) location db.Column(db.String(128)) # 存放位置 status db.Column(db.String(16), defaultavailable) # available / maintenance / disabled image_url db.Column(db.String(256)) # 设备图片路径 description db.Column(db.Text) created_at db.Column(db.DateTime, defaultdatetime.now)设备分类表可以单独建一张也可以直接用字符串我建议单独建表。原因很实际分类以后要扩展字段比如负责人、存放地点时会方便很多而且管理端做一个分类下拉框也很自然。租赁订单表这张表是整个系统的核心。它把用户、设备、租赁时间、状态串在一起所有业务操作都围绕它展开。class RentalOrder(db.Model): __tablename__ rental_order id db.Column(db.Integer, primary_keyTrue) order_no db.Column(db.String(32), uniqueTrue, nullableFalse) # 订单号如 20250101153000123 user_id db.Column(db.Integer, db.ForeignKey(user.id), indexTrue) equipment_id db.Column(db.Integer, db.ForeignKey(equipment.id), indexTrue) quantity db.Column(db.Integer, nullableFalse, default1) start_date db.Column(db.Date, nullableFalse) # 计划开始日期 planned_return_date db.Column(db.Date, nullableFalse) # 计划归还日期 actual_return_date db.Column(db.Date) # 实际归还日期未归还时为 None status db.Column(db.String(16), nullableFalse, defaultpending) # pending 待审核 / approved 已通过 / rejected 已驳回 / borrowed 已借出 # returned 已归还 / overdue 已逾期 / cancelled 已取消 remark db.Column(db.String(256)) # 申请备注或驳回原因 created_at db.Column(db.DateTime, defaultdatetime.now) user db.relationship(User, backreforders) equipment db.relationship(Equipment, backreforders)这里有几个设计细节值得强调第一日期字段全部用 Date 而不是 DateTime。设备租赁的粒度是一天不是具体到某个时分秒用 Date 可以避免“23:59归还还算不算逾期”这种无谓的争论。第二订单号 order_no 手动生成而不是自增 id。因为 id 是连续的很容易被人猜测订单总量手动生成规则化的订单号比如“日期用户id随机数”观感更好排查问题也方便。第三外键字段都加了 indexTrue。订单表经常按 user_id、equipment_id 查询如果不加索引数据量几百条可能没感觉上万条之后查询时间就会肉眼可见地增长。2.2 租赁状态的流转模型状态管理是我最想重点讲的部分因为大部分逻辑bug都出在这里。我上面定义了7个状态但它们之间不是任意转换的必须有一个明确的流转规则。建议在项目里直接写一个状态转移表作为前期设计的参照也方便答辩时展示你的逻辑严谨性。当前状态可执行操作目标状态触发角色/方式pending通过审核approved教师 / 管理员pending驳回申请rejected教师 / 管理员approved线下领取borrowed教师 / 管理员确认发放borrowed按时归还returned教师 / 管理员登记归还borrowed超过计划归还日overdue系统自动标记 / 手动标记overdue归还设备returned教师 / 管理员登记归还pending用户主动取消cancelled学生本人approved用户取消 / 管理员作废cancelled学生本人 / 管理员这张表的核心思想是系统的每一个操作函数都只允许当前状态下的特定角色执行特定动作。比如学生提交申请后看得到自己的订单是 pending但“通过审核”这个按钮绝不能渲染在学生的页面上即便学生通过构造请求直接访问审核接口后端也要做第二次校验。很多新手只控制前端按钮的显示后端不校验这是很大的安全隐患。逾期状态不需要一个后台进程来“定时改库”。可以在每次查询订单时用日期做实时判断如果 borrowed 状态且 planned_return_date 小于今天在展示时显示为“已逾期”并在数据库里同步把 status 改成 overdue。这样不依赖定时任务逻辑也更简单。3. 核心模块的实现细节3.1 用户登录与权限控制登录认证我直接用 Flask-Login它能处理 session 逻辑、记住我功能、current_user 全局变量。核心配置非常简单from flask_login import LoginManager, login_user, login_required, current_user, logout_user login_manager LoginManager(app) login_manager.login_view auth.login # 未登录时跳转到登录页面的端点 login_manager.user_loader def load_user(user_id): return db.session.get(User, int(user_id))登录视图函数也直白app.route(/login, methods[GET, POST]) def login(): if request.method POST: username request.form.get(username) password request.form.get(password) user User.query.filter_by(usernameusername).first() if user and user.check_password(password): login_user(user) return redirect(url_for(index)) flash(用户名或密码错误) return render_template(login.html)权限控制我习惯用自定义装饰器来做。Flask 的 login_required 只能判断“是否登录”判断不了“角色对不对”所以自己加一层非常必要from functools import wraps from flask_login import current_user def role_required(*roles): def wrapper(f): wraps(f) def decorated_function(*args, **kwargs): if not current_user.is_authenticated: flash(请先登录) return redirect(url_for(auth.login)) if current_user.role not in roles: flash(没有权限执行该操作) return redirect(url_for(index)) return f(*args, **kwargs) return decorated_function return wrapper使用方式很直观app.route(/order/int:order_id/approve, methods[POST]) login_required role_required(admin, teacher) def approve_order(order_id): ...前后端双校验非常重要。光有后端校验前端不隐藏按钮用户体验差用户点了半天发现没权限光有前端隐藏后端不拦截懂点技术的同学直接拿 POST 工具就能越权调用接口。这个坑我见过太多次一定要两边都做。3.2 设备信息管理与图片上传设备管理模块就是一个标准的 CRUD列表、新增、编辑、删除、详情。列表页面要有分页和筛选我建议直接用 Flask-SQLAlchemy 的 paginate 方法省去手动算偏移量app.route(/equipment) login_required def equipment_list(): page request.args.get(page, 1, typeint) keyword request.args.get(keyword, ).strip() category_id request.args.get(category_id, typeint) query Equipment.query if keyword: query query.filter(Equipment.name.like(f%{keyword}%)) if category_id: query query.filter(Equipment.category_id category_id) pagination query.order_by(Equipment.id.desc()).paginate( pagepage, per_page10, error_outFalse ) return render_template(equipment/list.html, paginationpagination)这里有个小细节request.args.get 的第二个参数可以直接传 typeintFlask 会帮你做类型转换转失败就返回默认值不用自己写 try-except。设备图片上传是这个模块里比较容易踩坑的地方。热词里专门提到了“附件路径错误”这个问题几乎每个做 Flask 上传功能的同学都会遇到。核心原因有三个第一Windows 下os.path.join拼接出来的是反斜杠路径而浏览器 URL 用的是正斜杠直接拼到 HTML 的 src 属性里就会出现路径错误。第二图片保存到了本地某个绝对路径但 Flask 的静态文件映射规则没有覆盖到那个目录。第三部署到服务器后通过 Nginx 或反向代理配置了额外的静态目录但本地开发时并没有这个映射。我推荐的解决方案是统一用 pathlib 拼接路径并把所有上传文件放到静态目录下的 uploads 子目录from pathlib import Path import uuid UPLOAD_FOLDER Path(app.root_path) / static / uploads app.config[UPLOAD_FOLDER] str(UPLOAD_FOLDER) # 确保目录存在 UPLOAD_FOLDER.mkdir(parentsTrue, exist_okTrue) app.route(/equipment/add, methods[POST]) admin_required def add_equipment(): file request.files.get(image) if file and file.filename ! : ext file.filename.rsplit(., 1)[-1].lower() if ext not in {jpg, jpeg, png, gif}: flash(图片格式不支持) return redirect(url_for(equipment_list)) filename f{uuid.uuid4().hex}.{ext} file.save(UPLOAD_FOLDER / filename) # 模板里通过 url_for(static, filenameuploads/ filename) 访问这段代码解决了三个关键问题文件名用 uuid 重命名避免中文名和特殊字符导致 URL 编码问题也防止同名文件互相覆盖。扩展名做白名单校验防止用户上传 exe 等危险文件。保存路径和访问路径统一映射到 static/uploads部署时也只用把这个目录同步过去即可。3.3 预约、审核与库存扣减预约流程是核心中的核心。学生选择设备、填写租赁时间和数量系统要完成几件事校验时间是否合法、校验库存是否充足、创建订单、锁定对应库存。这里最大的坑是并发超借。如果两个学生在同一瞬间提交同一设备ID的订单而库存只剩1台程序完全有可能让两个订单都通过校验最终导致实际借出数量大于库存。解决办法是数据库行锁。app.route(/order/create, methods[POST]) login_required def create_order(): equipment_id request.form.get(equipment_id, typeint) quantity request.form.get(quantity, typeint) start_date datetime.strptime(request.form.get(start_date), %Y-%m-%d).date() end_date datetime.strptime(request.form.get(planned_return_date), %Y-%m-%d).date() if start_date date.today(): flash(开始日期不能早于今天) return redirect(request.referrer or url_for(index)) if end_date start_date: flash(归还日期必须晚于开始日期) return redirect(request.referrer or url_for(index)) if quantity 0: flash(数量不合法) return redirect(request.referrer or url_for(index)) # 加悲观锁读取当前设备防止并发超借 equipment Equipment.query.filter_by(idequipment_id).with_for_update().first() if not equipment: flash(设备不存在) return redirect(url_for(index)) if equipment.available_quantity quantity: flash(f库存不足当前可借数量为 {equipment.available_quantity}) return redirect(request.referrer or url_for(index)) # 生成唯一订单号时间戳 用户id 随机数 order_no datetime.now().strftime(%Y%m%d%H%M%S) str(current_user.id).zfill(4) str(random.randint(100, 999)) order RentalOrder( order_noorder_no, user_idcurrent_user.id, equipment_idequipment_id, quantityquantity, start_datestart_date, planned_return_dateend_date, statuspending ) db.session.add(order) db.session.commit() flash(预约申请已提交请等待审核) return redirect(url_for(order_detail, order_idorder.id))注意到一个关键设计创建订单时并不扣减库存审核通过时才扣减。为什么因为如果学生提交申请就扣库存三个人同时申请同一台设备其中两个被驳回后库存才恢复体验很差而且“待审核”状态怎么与库存对应会很复杂。正确做法是申请阶段只创建订单不管库存审核通过时再校验并扣减库存。这样库存的语义非常清晰——永远代表“当前真正可以借走”的数量。审核通过的操作也要加锁并校验库存因为从申请提交到审核通过之间可能隔了很久库存早就变了app.route(/order/int:order_id/approve, methods[POST]) login_required role_required(admin, teacher) def approve_order(order_id): order RentalOrder.query.get_or_404(order_id) if order.status ! pending: flash(订单状态已变化无法审核) return redirect(url_for(order_detail, order_idorder.id)) equipment Equipment.query.filter_by(idorder.equipment_id).with_for_update().first() if equipment.available_quantity order.quantity: flash(库存不足无法通过审核) return redirect(url_for(order_detail, order_idorder.id)) equipment.available_quantity - order.quantity order.status approved db.session.commit() flash(审核已通过库存已锁定) return redirect(url_for(order_detail, order_idorder.id))使用 with_for_update() 时需要注意它必须在事务内生效。SQLAlchemy 默认会在 commit 时开启事务所以这段代码是有效的。但是如果你开了 autocommit 模式或者把 db.session 放在别的线程里使用行锁可能不会按预期工作这一点在并发量大的生产环境中尤其要小心。3.4 归还与逾期处理归还操作相对简单学生到实验室还设备教师/管理员点击“登记归还”恢复库存写入实际归还日期app.route(/order/int:order_id/return, methods[POST]) login_required role_required(admin, teacher) def return_order(order_id): order RentalOrder.query.get_or_404(order_id) if order.status not in (borrowed, overdue): flash(当前状态不可归还) return redirect(url_for(order_detail, order_idorder.id)) equipment Equipment.query.get(order.equipment_id) equipment.available_quantity order.quantity order.status returned order.actual_return_date date.today() # 计算是否逾期逾期则记录逾期天数可以另建表也可以简单记录在 remark 里 if order.planned_return_date date.today(): overdue_days (date.today() - order.planned_return_date).days order.remark f逾期归还逾期{overdue_days}天 db.session.commit() flash(归还登记成功) return redirect(url_for(order_detail, order_idorder.id))逾期标记不必启动后台任务。我的做法是写一个辅助函数在查询订单列表、展示订单详情时都会调用def check_and_mark_overdue(order): if order.status borrowed and order.planned_return_date date.today(): order.status overdue db.session.commit() return order.status然后在所有展示订单的地方先调用一下这个函数。这样逻辑的触发点是“有人查看”数据能及时更新又不需要部署 cron 或 Celery。对课设级别的系统来说这种“懒更新”策略完全够用而且逻辑很好理解。前端展示时可以配合JS做实时倒计时比如剩余天数等于 planned_return_date 减去今天。但注意 JS 的 Date 和 Python 的日期一定要统一时区不要前端用的本地时区、后端用的 UTC两者差八个小时很容易在“还剩几天”上显示出错。我建议后端直接算好剩余天数传给模板前端只负责展示最省心。4. 常见问题与排查技巧实录4.1 文件上传后图片不显示路径到底错在哪这个问题在开发阶段、部署阶段都可能遇到我在前面讲了解决方案这里再把排查思路整理成速查表。遇到图片不显示按顺序检查检查项说明文件是否真的保存到了目标目录先到服务器/本地磁盘确认 uploads 目录下有没有这个文件模板中的 src 是否拼写正确用浏览器 F12 看 Network 里图片资源返回的是 200 还是 404Flask 静态目录映射默认只映射 /static 路径检查你的目录层级Windows 反斜杠问题确认数据库里存的 url 是正斜杠还是反斜杠存反斜杠的改成 url 相对路径部署后 Nginx 反向代理如果用了 Nginx需要在 location /static 中配置 alias 指向实际目录常见的一个低级错误是文件保存成功数据库里存的是static/uploads/xxx.jpg但模板里写的是url_for(static, filenameuploads/xxx.jpg)最后生成的URL是/static/uploads/xxx.jpg数据库里的字符串根本没被使用。所以设计的时候要想清楚数据库存的是“相对 static 目录的路径”还是“完整 URL 的路径”前后端约定好别混用。4.2 两个学生同时申请库存被超借我前面用 with_for_update() 解决了这个问题但很多人会问为什么普通查询 加个 if 判断不行因为两个并发请求可能同时读到 available_quantity 1然后同时通过 if 判断接着同时执行减一操作最后两个订单都成功库存变成 -1。行锁的实质是让第二个请求等待第一个请求提交事务后再读取数据这时它读到的库存已经是0if 判断自然就拦截了。用 with_for_update() 时还有一个注意点加了行锁的查询必须尽快提交事务否则会长时间占用数据库连接影响整体性能。所以审核函数里不要在加锁之后做太多耗时的操作比如网络请求、文件写入应该先锁、再校验、马上改库存、提交一气呵成。4.3 前后端传参类型错乱Flask 的 request.form 拿到的值默认全部是字符串数值类型必须自己做转换。这也是热词里提到的“flask查看从客户端获取的变量数据类型”的痛点。我推荐统一用 request.form.get(quantity, typeint) 这种写法。因为如果客户端传了非数字typeint 会直接让返回值为默认值 None 或你给的默认值不需要 try-except。但要注意typeint 转换失败时并不报错而是返回默认值所以后续判断要考虑到这一点不要直接拿 None 和数字比较。模板传参也有类似问题。Jinja2 里渲染数字直接写{{ order.quantity }}是没问题的但如果把变量塞进 JS 代码里一定要用 tojson 过滤器防止字符串被组合成非法 JS。比如const orderData {{ order | tojson }};用 tojson 会自动把 Python 对象转成合法 JSON比手写 JSON.stringify 可靠得多。4.4 状态不同步按钮乱显示有同学反馈订单都逾期了页面上还显示“归还”按钮点击后又报错“当前状态不可归还”。这种问题本质是前端渲染的状态和数据库里的状态不一致。方向有两个第一个是用我前面说的懒更新函数在路由里统一先调用 check_and_mark_overdue 再渲染页面第二个是前端模板里不要用复杂的嵌套判断而是把状态映射统一放在后端处理。比如可以给 Order 模型加一个属性property def status_display(self): mapping { pending: 待审核, approved: 已通过, rejected: 已驳回, borrowed: 已借出, returned: 已归还, overdue: 已逾期, cancelled: 已取消 } return mapping.get(self.status, self.status)展示按钮的逻辑也建议后端统一给出比如给订单加一个 can_return 属性视图函数里根据状态计算好模板里只判断这一个属性避免模板里写一堆“如果什么什么且什么什么”的条件不仅难读而且容易漏情况。5. 部署与演示的准备5.1 本地运行环境搭建这个项目大家基本都用虚拟环境操作很简单但容易忽略 requirements.txt 的管理。项目收尾前在项目目录执行pip freeze requirements.txt生成依赖清单。注意这里会包含当前环境里所有已安装的库如果虚拟环境里混入了不相关的包建议手动清理一下 requirements.txt只保留项目真正用到的。要不然导师在另一台机器上pip install -r requirements.txt时会装很多无关包体验很不好。另外如果你的 Flask 版本比较高注意app.run(debugTrue)不要用于生产环境。Flask 自带的开发服务器性能很差并发稍高就卡住生产环境一定要换 GunicornLinux或 waitressWindows。5.2 简单部署思路部署部分至少要有基本的思路答辩时老师很可能会问。最标准的方案是Flask应用由 Gunicorn 启动Nginx 做反向代理和静态文件服务。Gunicorn 启动命令示例gunicorn -w 4 -b 127.0.0.1:8000 wsgi:app这里的 wsgi 是你的项目入口文件名app 是 Flask 实例名。注意 -w 是 worker 数量一般不要少于2也不要超过CPU核数太多。Nginx 反向代理配置核心片段server { listen 80; server_name your-domain.com; location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /static/ { alias /path/to/your/project/static/; } }静态文件的 location 一定要配置否则用户访问 /static/xxx 时请求会反向代理到 Flask 应用去处理Flask 的开发服务器能处理但 Gunicorn 默认不处理静态文件图片样式全会挂。这个问题在好几位同学的部署记录里出现过部署阶段的“附件路径错误”大多就是这么来的。5.3 演示前的准备工作最后说点答辩演示的实战经验。第一一定准备一套完整的演示数据包括不同角色的账号、几种不同状态的订单、一张带图片的设备记录。第二演示前把所有浏览器缓存清一遍把 Flask 应用重启一次保证页面干净不报错。第三提前想好“如果现场网络不好怎么办”如果系统部署在本地服务器确保演示机可以访问内网地址。我个人在做这种管理系统时还会额外准备五张左右的页面截图配一段2分钟的操作录屏万一现场出问题放录像也能把流程讲清楚。这不是偷懒而是给自己留后路现场设备环境不可控的情况太多了。6. 写在最后的一些体会这套系统做下来最大的感受是它并不需要什么高深的技术难在把业务逻辑理清楚并把规则落到数据模型和代码上。Flask 给了你很大的自由但这种自由也要求你自己约束自己。我记得有一次帮学生调代码问题出在他下了两张“状态流转”的定义完全不同——前端以 status 等于 approved 判断显示按钮后端却以为 approved 是“已借出”结果学生点了半天没反应审核通过之后界面也一直不变。这种问题靠调试很难发现因为代码语法完全没错就是语义不一致。所以做这类项目我建议你把状态定义、角色权限、库存变化的规则先用表格写清楚再动笔写代码。这个工作表面上多花了半小时实际上能帮你省下后面无数排查问题的时间。另外就是记得多用 locals() 或者 flask shell 去测试模型层不要什么都靠页面验证页面报错信息往往比模型层晚半步。如果做完基础功能还有精力值得扩展的方向包括添加实验课程与设备绑定的模块、生成借用率统计报表、增加邮件提醒归还功能。这些方向都会让系统在复杂度上更上一个台阶但底层逻辑仍然不变——把规则定义清楚让每个操作在正确的边界内发生。
返回列表