1. 这个系统要解决的问题:共享实验室的“排队焦虑”与“资产黑箱”
先聊个场景。某高校或某公司的公共实验室,设备十几台、工位几十个,以前靠一张Excel表或者微信群接龙排实验。你要用精密仪器得提前一周去登记,去了发现被占着;设备坏了也没人记录;耗材领用更是糊涂账。做一个基于Python的实验室预约系统,本质上就是把这张Excel表变成一套线上化的状态机——让每台设备、每个工位、每段时间都有明确的“空闲/已被预订/使用中/维护中”标签,同时把设备租赁、耗材管理这些附属业务一并纳入统一流程。
这个项目标题“django-flask基于python实验室预约系统 实验室设备租赁管理系统pycharm -Vue”,拆开看包含三条核心线索:一是后端由Django和Flask双框架组合承担;二是业务范围覆盖预约和租赁两大块;三是前端用Vue实现,开发环境基于PyCharm。这套组合很典型,不是生产级超大平台,而是适合课程设计、毕业设计、中小型实验室内部管理系统的标准架构。
适合谁来参考?三类人:第一类是正在做毕设或课设的学生,需要一个功能完整、架构清晰的实战项目;第二类是实验室管理员或课题组负责人,想花少量成本把预约流程线上化;第三类是刚入门的Web开发者,想弄明白Django、Flask和Vue三者如何配合。这篇文章会从数据库设计讲到前后端联调,再到部署中容易踩的坑,尽量把每个关键点都说透。
2. 技术选型复盘:为什么是Django + Flask双后端,而不是只用其中一个
2.1 Django负责“重”的部分:认证、权限和后台管理
Python的Web框架里,Django最大的优势是“全家桶”式的内置能力:ORM、Admin后台、认证系统、CSRF防护、Session管理都是现成的。做实验室预约系统,最头疼的是两类账号——学生/教师用户和管理员,不同的角色对应不同的操作权限。如果用Flask单独做,用户表、角色表、权限校验、Session维护全部要手写,工作量不小。
Django的User模型加Group权限机制可以直接改造:用户表继承AbstractUser,加上学号、工号、所属院系等字段;权限用Django自带的Permission体系,管理员走Django Admin后台维护设备档案和预约规则,普通用户走自定的预约前端页面。这样可以把精力集中在业务逻辑上,而不是重复造用户认证的轮子。
实际操作中我是这样分工的:Django作为主后端,承担用户认证、管理员后台、预约订单的创建与状态流转、设备档案管理这些核心业务。内部 REST API 用 Django REST Framework(DRF)提供,前端Vue通过 axios 请求这些接口。这样做的好处是开发效率高,因为 Django 的 ORM 和 Admin 直接覆盖了80%的后台需求,另外 DRF 自带序列化、分页和权限控制,接口落地很快。
2.2 Flask承担“轻”的部分:设备监测、微信回调等独立服务
Flask在这个项目里不是拿来和Django打架的,而是负责几个独立的小服务。最典型的是设备状态回传接口——实验室里的某些设备会定期上报开关机状态、使用时长、故障码等信息,这类轻量级接口用Flask写一个单独进程跑起来,只需要几个路由,不依赖Django的整套中间件和配置,维护成本低很多。
另外,如果系统要接企业微信或钉钉的通知推送(预约成功提醒、设备归还提醒),回调服务也可以丢给Flask处理。两个框架跑在不同的端口,通过HTTP互相调用,互不干扰。我在项目里的实际做法是:Django跑在8000端口,Flask跑在9000端口,两者通过Redis做数据中转,Flask收到的设备状态消息写入Redis,Django的Celery定时任务再从Redis取数据入库。
2.3 PyCharm在开发流程里的实际作用
说实话,项目标题里特意带上“pycharm”,大概率是课程设计或毕设的环境要求。不过PyCharm的Professional版对Django和Flask的支持确实好:直接创建Django项目、一键运行manage.py命令、自带数据库工具、内置终端和调试器。前端Vue部分建议在PyCharm里装一个Vue.js插件,虽然前端调试我更习惯用VS Code,但全栈项目都在一个IDE里打开,切换上下文确实方便。
实际开发中有一个小建议:PyCharm里要把虚拟环境配对好,File > Settings > Project > Python Interpreter里选择项目根目录下的venv,否则运行Django时会提示找不到模块。多人协作时,每个人clone项目后第一件事就是创建虚拟环境并安装requirements.txt,然后在PyCharm里切到对应解释器,这一步能省掉大部分环境配置问题。
2.4 前端技术栈:Vue 2还是Vue 3
这个系统用Vue来做前端SPA(单页面应用)。如果是从Vue 2时代就开始做的项目,沿用Vue 2 + Vue Router + Vuex + Element UI可能更稳;如果是从零开始新建,建议直接用Vue 3 + Vite + Pinia + Element Plus。我在这个项目里考虑到组件生态和团队熟悉度,选择了Vue 2 + Element UI,这并不是说Vue 3不好,而是Element UI的表格、表单、日期选择器组件非常贴合后台管理系统的需求,开箱即用。
前端通过代理方式解决跨域问题:Vue的webpack-dev-server把/api前缀的请求代理到http://localhost:8000,线上部署的时候再用Nginx做反向代理。这样前后端分离部署,开发调试都很顺畅。
3. 数据库模型设计:预约、设备、用户三者如何关联,字段怎么定
3.1 用户表的定制:不只是用户名和密码
Django默认的User表只有username、password、email等基础字段,做实验室系统必须扩展。继承AbstractUser之后新增字段:user_type(区分管理员、教师、学生)、student_id(学号或工号)、phone(手机号)、department(所属院系)。这些字段要提前定好,因为后续预约记录、租赁订单都要关联到用户信息,如果后面再改表结构,迁移数据会非常痛苦。
from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): USER_TYPES = ( ('student', '学生'), ('teacher', '教师'), ('admin', '管理员'), ) user_type = models.CharField(max_length=10, choices=USER_TYPES, default='student') student_id = models.CharField(max_length=20, unique=True, null=True, blank=True) phone = models.CharField(max_length=11, blank=True) department = models.CharField(max_length=50, blank=True) class Meta: db_table = 'lab_user'这里要注意unique=True放在学号上,避免同一个学生注册多个账号。但学号在管理员账号中并没有,所以必须设置null=True, blank=True,否则管理员创建时会报完整性错误。
3.2 设备表:预约与租赁共享同一种资源模型
实验室的设备有两个维度:预约使用(按时段使用)和租赁外出(按天带走)。这两种业务本质都是“设备在某个时间区间内不能被其他人使用”,区别只在于计费方式和归还校验。因此设备表可以统一设计,用status字段区分当前状态。
class Equipment(models.Model): name = models.CharField(max_length=100) # 设备名称 code = models.CharField(max_length=20, unique=True) # 设备编号,例如 EQUIP-001 location = models.CharField(max_length=100) # 存放位置 description = models.TextField(blank=True) # 设备说明 total_count = models.IntegerField(default=1) # 同型号设备总数 available_count = models.IntegerField(default=1) # 当前可用数量 price_hour = models.DecimalField(max_digits=8, decimal_places=2, default=0) # 按时租赁价格 price_day = models.DecimalField(max_digits=8, decimal_places=2, default=0) # 按天租赁价格 status = models.CharField(max_length=10, choices=( ('available', '可用'), ('maintenance', '维护中'), ('disabled', '已下架'), ), default='available') create_time = models.DateTimeField(auto_now_add=True)有几个字段要特别说明。total_count与available_count是两个概念:总量不变,可用数量会随预约和租赁动态增减。同型号设备有多台,比如有5台万用表,预约或租赁时不是锁定某台具体设备,而是锁定“可用数量”,真正领用时再分配具体哪一台。price_hour和price_day分别支持下单时自动计算押金和租金。
3.3 预约单与租赁单:两张表还是一种订单模型
这是整个系统设计最容易纠结的地方。我的方案是拆成两种订单表,因为它们的状态流转和计费逻辑差异很大。
预约单字段设计:
class Reservation(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='预约人') equipment = models.ForeignKey(Equipment, on_delete=models.CASCADE, verbose_name='设备') start_time = models.DateTimeField(verbose_name='开始时间') end_time = models.DateTimeField(verbose_name='结束时间') purpose = models.TextField(blank=True, verbose_name='实验目的') status = models.CharField(max_length=10, choices=( ('pending', '待审核'), ('approved', '已通过'), ('rejected', '已驳回'), ('cancelled', '已取消'), ('completed', '已使用'), ('expired', '已过期'), ), default='pending') create_time = models.DateTimeField(auto_now_add=True) class Meta: db_table = 'lab_reservation'这里状态机是整个流程的核心:用户提交预约 -> 管理员审核通过 -> 到时间后设备状态变为“使用中” -> 用完归还后标记“已完成”。如果用户没有按时到场,系统定时任务自动把状态改为“已过期”,把设备释放出来。审核环节是否需要,取决于实验室的管理制度,有些实验室允许预约后直接生效,那么就把pending和approved合并。
租赁单字段设计与预约单大部分相同,额外增加deposit(押金)、rent_fee(租金)、expect_return_time(预计归还时间)、actual_return_time(实际归还时间)、damage_status(归还时设备损坏情况记录)。押金金额可以在用户下单时根据设备价值自动计算,也可以由管理员填写。
我单独给租赁单建了LeaseOrder模型,目的是租金计算清晰。比如设备按天计费,用户租用3天,订单创建时锁定status='ongoing',归还日管理员确认设备完好后,把status变为finished,系统自动计算超时费用。
3.4 时间段冲突检测:数据库层怎么防“一设备多约”
系统上线后最容易遇到的并发问题就是:同一台设备在同一时间段被两个人成功下单。解决方案不是在视图函数里用if判断是否冲突——两个请求同时到达时,判断结果可能都是“空闲”,然后都写入数据库,这属于经典的并发竞态条件。正确做法是利用数据库的唯一约束来兜底。
给预约表加唯一约束,锁定“设备+时间区间”不重叠:
class Meta: constraints = [ models.UniqueConstraint( fields=['equipment', 'start_time', 'end_time'], name='unique_equipment_time_slot' ) ]但这个方案太严格:如果时间段完全不重叠,那就可能允许同一台设备在同一小时被分成两段预约,而用户实际使用可能超时导致冲突。所以更严谨的做法是在视图函数中使用select_for_update()对设备行加锁,事务内检查时间段是否重叠,再创建订单:
from django.db import transaction @transaction.atomic def create_reservation(request, equipment_id): equipment = Equipment.objects.select_for_update().get(pk=equipment_id) start_time = request.data['start_time'] end_time = request.data['end_time'] conflict = Reservation.objects.filter( equipment=equipment, status__in=['pending', 'approved', 'ongoing'], start_time__lt=end_time, end_time__gt=start_time ).exists() if conflict: return JsonResponse({'code': 1, 'msg': '该时间段已被预约'}) Reservation.objects.create(...)select_for_update()会在事务期间锁定设备行,第二个请求必须等第一个事务提交后才能读取,因此不会出现并发下单成功的情况。这个细节是“能跑”和“跑得对”的分水岭,在课设答辩或代码评审时,这一条足够证明你确实考虑过并发问题。
4. 核心功能模块拆解:从用户注册到管理员审核到设备状态回传
4.1 注册/登录模块
使用Django自带的认证扩展,前端提交用户名、密码、学号手机号等信息,后端用DRF的CreateAPIView创建用户。登录接口返回token,前端把token存到localStorage,后续每次请求在Authorization头里带上。密码存储交给Django的make_password处理,这个尽量不要自己去实现哈希,Django内置的PBKDF2算法比手写的简单方案可靠得多。
有个体验优化点:密码强度校验可以从前端和后台双重校验。前端Element UI的el-form里配置validator规则,后端再用django.contrib.auth.password_validation校验,避免有人设置太弱的密码。
4.2 设备展示与筛选
首页给用户看到的是一张设备列表卡片,支持按设备名称、设备类型、存放位置筛选。前端通过axios请求GET /api/equipment/list获取设备数据,后端用DRF的ModelViewSet+filter_backends+SearchFilter支持搜索参数。为了让前端响应快,列表接口加上分页,每一次加载15条,滚动到底部再加载下一页。
class EquipmentViewSet(viewsets.ModelViewSet): queryset = Equipment.objects.all() serializer_class = EquipmentSerializer filter_backends = [filters.SearchFilter] search_fields = ['name', 'code', 'location']如果用户只看“可用”设备,接口再支持一个?status=available的过滤参数。设备缩略图可以用ImageField上传,然后前端用el-image组件展示。这一块没什么技术难点,重点是查询效率:如果设备超过几千条,别用objects.all()直接返回,加一个Paginator或者 DRF 分页,否则页面会卡住。
4.3 预约下单流程
用户点“预约”,弹出一个对话框,包含日期选择、时间段选择、实验目的输入。日期和时间段选择是这个流程最值得打磨的地方——很多实验室一秒一段(或半小时一段),要让用户选的是“开始时间”和“结束时间”,而不是“选一个日期”。我前端用的是el-date-picker的datetimerange类型,一次选好起止时间,后端再做时间格式解析。
提交后创建Reservation对象,状态为pending。前端提示“提交成功,等待管理员审核”。如果实验室不需要审核,可以在配置表里加一个need_review的字段,管理员在后台可以开启或关闭。预约成功后给用户弹一个二维码或预约编号,用户到实验室时出示预约编号,管理员核对后确认到场。
4.4 管理员审核与设备分配
管理员在后台看到待审核的预约列表,点击“通过”或“驳回”。通过之后,系统自动减少该设备的available_count,并生成一条使用记录。如果设备在预约时段被其他用户申请,系统会在审核通过时自动检查冲突并拒绝通过。
管理员的设备管理页面用Django Admin或者自定的Vue页面都可行。我用Django Admin做设备档案维护,字段比较多的时候,list_display、list_filter、search_fields这几个属性配置好,后台用起来非常顺手。设备图片的预览需要在admin.py里加一个display_image方法,返回HTML格式的img标签。
4.5 租赁下单与归还流程
租赁流程比预约多几步。用户提交租赁申请时除了选设备和时间段,还要填写预计使用用途和押金支付方式。管理员确认后,用户到实验室取设备,管理员点击“已出库”,系统把设备状态改为on_rent,available_count减一。归还时管理员检查设备外观和功能,录入damage_status,如果正常则点击“已归还”,系统计算租金。
租金计算规则:按照天数 x price_day,超过预计归还时间的部分按隔夜计费或者按小时折算,取决于你定义的计算规则。我在项目里用了简单规则:超时不足一天的按一天算,同时每天封顶租金不超过设备价值的30%。这个业务规则写在后端服务模块里,单独一个calculate_rent_fee(order)函数,便于维护和测试。
4.6 Flask服务:设备状态回传与通知推送
这一节讲Flask服务的具体实现。新建一个device_status_service工程,里面用Flask提供两个接口:/report_status接收设备上报状态,/health_check用于健康检查。设备状态上报的数据格式约定为JSON:设备编号、温度、状态码(正常/告警/故障)、时间戳。Flask解析后直接把原始数据写入Redis的List结构,Django的Celery任务每分钟取一次批量入库。
from flask import Flask, request import redis app = Flask(__name__) r = redis.Redis(host='localhost', port=6379, decode_responses=True) @app.route('/report_status', methods=['POST']) def report_status(): data = request.get_json() device_code = data.get('device_code') status = data.get('status') if not device_code or not status: return {'code': 1, 'msg': '参数不完整'}, 400 r.lpush('device_status_queue', str(data)) return {'code': 0, 'msg': 'ok'} if __name__ == '__main__': app.run(host='0.0.0.0', port=9000)为什么用Redis中转而不是Flask直接写数据库?原因是设备上报频率可能很高,一次实验课的传感器设备可能每秒上报一个状态点,如果每个点都写MySQL,数据库压力很大。Redis先缓冲,Django定时批量写入,既能降低数据库负载,又能保证数据不丢。整套链路是:设备 -> Flask -> Redis -> Django Celery -> MySQL,每一个环节都很好替换和扩展。
5. 前端Vue页面结构与接口对接经验
5.1 页面路由和布局
前端SPA按功能模块组织成几个页面:登录/注册页、设备列表页、预约表单页、我的预约页、租赁管理页、管理后台页。Vue Router使用动态路由:未登录用户只能访问登录页和设备列表,登录后根据user_type把管理路由加上。
const routes = [ { path: '/login', component: Login }, { path: '/equipment', component: EquipmentList }, { path: '/my/reservations', component: MyReservations, meta: { requiresAuth: true } }, { path: '/admin', component: AdminDashboard, meta: { requiresAuth: true, requiresAdmin: true } }, ]路由守卫里做全局拦截:如果没有token直接跳转登录页;如果有token且是管理员但访问了用户页面,也重定向到首页。这个逻辑虽然简单,但实际项目中拦截住80%的未授权访问,后端接口仍然要校验权限,前端拦截只是体验优化,权限安全最终落在后端。
5.2 axios封装与拦截器
请求封装是必要的。每台接口地址、token、错误码处理总不能写20遍。封装一个request.js:
import axios from 'axios' const service = axios.create({ baseURL: '/api', timeout: 10000 }) service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'JWT ' + token } return config }) service.interceptors.response.use( response => { const res = response.data if (res.code !== 0) { alert(res.msg) return Promise.reject(new Error(res.msg)) } return res }, error => { if (error.response.status === 401) { localStorage.removeItem('token') router.push('/login') } return Promise.reject(error) } )一个关键问题:token过期失效。后端返回401时,前端一定要清掉本地token并跳转登录页,否则用户会停留在一个看似登录实则无权请求的状态里,所有操作都静默失败,体验极差。
5.3 预约表单的日期校验
前端做一个时间段选择器的时候,要防止用户选出非法时间:开始时间必须大于当前时间,结束时间必须大于开始时间,且总时长不超过可预约最大时长(比如单次不超过4小时)。这些校验既在前端做一遍,也在后端做一遍,双保险。后端校验有个好处:即使用户绕过前端直接调API,也占不到便宜。
5.4 管理后台的表格操作
管理后台用Element UI的el-table展示预约列表,列的写法很简单,但要注意几个细节:
- 状态列用
el-tag显示不同颜色:pending为黄色、approved为绿色、rejected为红色、completed为蓝色。用户扫一眼就能看出当前进度。 - 操作列根据状态动态渲染按钮:pending状态显示“通过/驳回”;approved状态显示“标记使用”;completed不能做任何操作。
- 表格底部默认显示分页,
@current-change事件绑定换页请求。
后端给管理员的列表接口要返回总条数和当前页数据。数据量大时,前端不要一次性加载全部,而是每页15到20条,避免DOM节点过多导致页面卡顿。这个体验优化在课设答辩时很加分。
6. 开发中踩过的坑:并发、时区、外键删除与定时任务
6.1 时区问题:默认时间不显示,预约记录差了8小时
Django默认的TIME_ZONE是UTC,如果不改,用户在中国时区预约下午3点,数据库存的是UTC时间,前端展示直接变成晚上11点。解决方法是把settings.py中的TIME_ZONE改为'Asia/Shanghai',并将USE_TZ设为False。
简单解释一下:USE_TZ=True时Django会把所有时间按UTC存储,显示时按当前时区转换,但这要求前端和后端都严格处理时区,一旦有一环漏了就会出现“时间看起来对不上”的怪毛病。对于纯国内使用的实验室系统,关掉USE_TZ以本地时间存储,时间逻辑最直观。
6.2 外键删除策略:设备删不掉怎么办
设备表被预约单和租赁单外键引用,如果直接删除设备,数据库会报ProtectedError。有两种处理方式:一是把ForeignKey的on_delete设为CASCADE,但这样会把历史订单一起删掉,显然不合适;二是设备下线不删除,把状态改为disabled,保留全部历史记录,这也是管理系统的常见做法。
我在项目里选择第二种,并给管理员的删除按钮加了二次确认:“确认下架该设备吗?下架后不可预约但历史订单保留。”
6.3 定时任务:如何自动取消过期预约和释放设备
用户预约了设备但没来,设备被白白锁定。解决办法是Celery的定时任务每10分钟扫描一次预约表,把approved且结束时间已过的预约改为expired,同时把ongoing且超出结束时间一定时长的预约自动标记为“超时”,并释放对应设备的可用数量。
注意这里不能用“结束时间小于当前时间”一个条件就想当然地认为设备可以释放——因为用户可能正在超时使用,贸然释放会导致设备被重复预约。我采用的规则是:结束时间超过30分钟仍未操作“确认归还”,系统才自动释放并通知管理员。这个30分钟的宽限期是可配置的。
6.4 前后端联调里最烦的问题:跨域、Header丢失、JSON转译错误
开发模式下Vue跑在8080端口,Django跑在8000端口,跨域必须配置。用django-cors-headers这个库,在INSTALLED_APPS里加corsheaders,中间件放到CommonMiddleware之前,然后设置CORS_ALLOWED_ORIGINS为['http://localhost:8080']。
如果不想用这个库,也可以在Vue的vue.config.js里配置devServer.proxy:
devServer: { proxy: { '/api': { target: 'http://localhost:8000', changeOrigin: true } } }这样前端请求/api/xxx时会自动转发到Django,浏览器端不出现跨域问题。Header丢失的情况多半是后端CORS_ALLOW_HEADERS没把Authorization加进去,前端设置了token但后端收不到,接口一直返回401,这个坑排查了半天才找到,后来直接把CORS_ALLOW_ALL_HEADERS = True设为true省心。
JSON转译错误最常见的是日期时间格式:Django的模型序列化默认把DateTimeField转成"2025-06-01T14:30:00Z"这种ISO字符串,前端new Date()能解析,但如果直接把整个字符串塞进el-date-picker会报警告。我的做法是在前端做一次格式化函数,把ISO字符串转成YYYY-MM-DD HH:mm:ss格式再展示,免得Element UI的组件对时间格式敏感。
7. 项目代码结构组织与线下演示的准备工作
7.1 后端目录怎么安排
一个Django项目按业务模块拆分目录,不要把所有视图写在一个views.py里。我的建议结构是:
lab_project/ ├── manage.py ├── config/ # 项目配置(settings.py,urls.py) ├── apps/ │ ├── users/ # 用户管理 │ ├── equipment/ # 设备管理 │ ├── reservation/ # 预约管理 │ ├── lease/ # 租赁管理 │ └── notifications/ # 通知推送 ├── static/ └── templates/每个app内部用views.py存放视图,serializers.py存放DRF序列化器,models.py存放数据库模型,urls.py路由。这样结构清晰,答辩或代码审查时导师一眼就能看出你理解了模块化开发。
7.2 Flask服务怎么融入整体项目
Flask服务放在项目根目录下的services/device_sync文件夹里,单独维护app.py、config.py,不依赖Django的模型。两个进程之间通过Redis传递消息,不会出现循环引用问题。启动时先启动Django,再启动Flask,最后启动Celery worker,我写了一个start_all.sh脚本一键启动所有服务。
7.3 演示数据准备技巧
演示系统时最容易翻车的是“没数据”和“时间过期”。提前在后台准备一批演示数据:5条不同状态的学生账号、10台设备(包含可用、维护中、已下架三种状态)、若干条预约和租赁订单。时间数据要以当前时间为基准动态生成,比如预约单的开始时间是今天14:00、结束时间是明天16:00,这样演示时打开页面就能看到“待审核”“已通过”“已过期”等状态并存的效果。
另外,在演示前一定检查系统时间与当前时间是否一致,避免出现“所有预约都过期”的尴尬。我吃过一次亏,演示电脑的系统时间慢了3天,结果管理后台里全是已过期状态,整整花了5分钟才找到原因。
8. 部署到服务器:Nginx + Gunicorn + Supervisor的完整流程
8.1 后端生产环境配置
开发调试可以用python manage.py runserver,但生产环境必须用Gunicorn启动Django服务:
gunicorn config.wsgi:application -w 4 -b 127.0.0.1:8000-w 4表示4个worker进程,每个worker处理并发请求,具体worker数可以按CPU核心数调整。Flask服务则直接用Gunicorn或uWSGI启动,绑定9000端口。
8.2 Nginx反向代理配置
Nginx负责两件事:静态文件服务和API反向代理。前端Vue执行npm run build后生成dist文件夹,Nginx把根目录指向dist,location /api转发到Django,location /status转发到Flask服务:
server { listen 80; server_name your_server_ip; root /var/www/lab_frontend/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /status/ { proxy_pass http://127.0.0.1:9000; } }配置里的try_files $uri $uri/ /index.html;很重要。Vue Router的history模式在刷新某个子路由页面时,Nginx默认会返回404,这一行把所有找不到的路径都指回index.html,由前端路由接管。
8.3 Supervisor守护进程
服务在服务器上不能只靠手动nohup挂着,我用Supervisor管理Django、Flask和Celery三个进程。配置一个lab_system.ini:
[program:lab_django] command=/var/www/lab_project/venv/bin/gunicorn config.wsgi:application -w 4 -b 127.0.0.1:8000 directory=/var/www/lab_project autostart=true autorestart=true [program:lab_flask] command=/var/www/lab_project/venv/bin/gunicorn app:app -w 2 -b 127.0.0.1:9000 directory=/var/www/lab_project/services/device_sync autostart=true autorestart=true [program:lab_celery] command=/var/www/lab_project/venv/bin/celery -A config worker --loglevel=info directory=/var/www/lab_project autostart=true autorestart=true部署中有一个很容易踩的坑:Django的DEBUG没有关,导致页面报错时直接把堆栈信息返回给用户,非常不安全。生产环境务必将DEBUG = False配置到settings.py,同时配置ALLOWED_HOSTS加上服务器的域名或IP。
8.4 数据库备份方案
实验室管理系统里有预约记录和设备数据,丢数据比丢代码更麻烦。最实用的备份方案是定时用mysqldump备份数据库,再配合cron任务每天凌晨导出一次SQL文件,保留最近7天的备份:
0 2 * * * mysqldump -u root lab_system > /backup/lab_system_$(date +%Y%m%d).sql find /backup -name "*.sql" -mtime +7 -delete有条件的可以再写一个脚本把SQL文件同步到对象存储或另一台服务器,做到异地备份。课设项目可能不需要这么复杂,但养成备份习惯会让你在真正跑生产时从容很多。
9. 测试方案:哪些接口必须写单元测试
代码写完不是结束,跑一遍测试才能真正验证逻辑。Django项目里推荐用pytest-django写单元测试。至少要覆盖这几类关键接口:
- 注册接口:正常注册、重复用户名、密码过短,3个用例。
- 登录接口:正确密码返回token、错误密码返回401。
- 预约创建:时间段冲突、设备不存在、设备已下架,这3个边界条件必须测。
- 审核通过:被审核的预约单是否会减少设备可用数量,以及冲突情况下的拒绝逻辑。
- 租赁归还:押金计算、超时费计算、损坏状态记录。
为什么优先测这些?因为它们背后是状态机和并发安全的核心。界面偶尔出个bug可以修补,但预约冲突和时间计算错误会导致实际使用者的损失。
10. 从开发完成到持续运维:一些个人心得
第一次做这类全栈项目,我最大的体会是:系统写得再漂亮,不如流程设计得合理。管理员端、用户端、设备数据的实时同步,靠代码只能解决技术问题,真正的难点在于业务规则怎么定义——要不要审核、超时多久释放、租金上限怎么算,这些规则一定是和实验室实际管理人员反复沟通才能定下来的。技术上完全可以在开发前做原型,但业务规则最好提前问清楚。
另外一个建议是数据库字段设计时,尽量加create_time和update_time两个时间字段。这在初期看不出作用,但是当你想排查“某个订单是谁在什么时间改的状态”时,这两个字段能节省大量排查精力。Django的auto_now_add=True和auto_now=True分别搞定创建与更新,成本很低,收益却很大。
最后再说一个小技巧:前端打包生成的生产包和后端API服务,在线上部署前一定先做一轮“流程穿测”——用普通用户身份从注册、预约、管理员审批、使用归还这整个流程走一遍,每个环节留个截图或录屏。这既是验收的手段,也是答辩或汇报时的素材。我自己见过很多项目代码写得不错,但演示时没有按真实业务流程走,结果中途跳错,印象分大打折扣。穿测跑顺了,后续所有工作都会顺畅很多。