资讯动态

基于Django+Flask的校园体育器材租赁管理系统实现

发布时间:2026/10/8 16:15:07 来源:尧图企业网站定制
校园体育器材租赁这件事听起来就是个普通的管理系统可真要动手做的时候你会发现需求远比想象中琐碎器材种类多、数量不一致、借还时间要卡准、逾期要提醒、库存不能超借还要区分学生、教师、管理员三种身份。我做完这套基于 Python 的 Django Flask 校园体育器材租赁管理系统之后最大的感受是选对框架组合能省掉一半的折腾。这篇文章就把我整个项目的需求拆解、技术选型、数据库设计、核心代码、前后端协作和部署经验完整写出来无论你是课程设计、毕业设计还是学校体育部真的想上一套管理系统都能直接参考。1. 先盘清楚需求这个系统到底要解决什么问题1.1 校园体育器材管理的真实痛点很多学校的体育器材管理停留在纸质登记阶段。学生借一个篮球管理员在台账上记一笔还球的时候再划掉全靠自觉和记忆。遇到体育课集中借用七八个学生同时排队登记管理员手忙脚乱台账漏记、错记是常事。更麻烦的是器材状态没法实时掌握——哪只球已经漏气哪副羽毛球拍线断了谁借的球逾期一周没还这些问题在纸质系统里几乎无法追踪。所以这个项目要解决的核心问题不是“做一个网页”而是把器材从入库到报废的全生命周期管起来。每一个器材的当前状态、在谁手里、应该在什么时候归还都要能一查便知。系统还要辅助管理员做决策哪类器材使用频率最高、哪个学生频繁逾期、哪些器材该维护了这些数据如果靠人工翻台账统计一个月都理不清。这个系统的目标用户有三类普通学生和教师负责查器材、在线申请借用器材管理员负责审核、登记借出和归还、处理逾期系统管理员负责维护器材基础数据、管理用户权限。三类用户的操作权限和界面都不同设计时要明确区分。1.2 角色划分与核心业务流程我把整个系统划分为三种角色学生浏览器材列表、查看可借数量、提交租赁申请、查看自己的租赁记录、在线续借或归还体育部管理员审核租赁申请、确认借出、确认归还、登记器材损坏、查看逾期列表、器材入库与维护超级管理员管理用户账号、分配角色权限、配置器材分类、数据统计与导出核心业务流程是一条完整的租赁闭环学生浏览器材 → 提交租赁申请 → 管理员审核 → 学生取货借出→ 使用中 → 归还 → 管理员验收 → 订单完结。如果到期未还系统自动把订单状态改为逾期并通知管理员跟进。这个流程里最容易出问题的是“申请”和“借出”被当成一回事。很多简化版系统一提交就扣库存学生乱申请占着器材不取导致真正想借的人借不到。我的方案是把订单拆成多个状态节点每个节点都有对应的操作人这样责任链条清楚也方便后续统计各环节的耗时。1.3 功能清单从“能用”到“好用”基础功能按照租赁业务闭环来规划器材管理分类管理、器材新增/编辑/下架、库存数量维护、器材状态标识可借/已借出/维修中/损坏/报废用户管理注册、登录、角色分配、个人资料维护、借还记录查询租赁管理申请、审核、借出、归还、逾期判定、续借处理统计报表器材借出频次排行、用户借阅排行、逾期统计、器材损耗报表消息提醒借出成功通知、临近归还提醒、逾期警告目前用站内消息实现后续可以接邮件或企业微信升维思考一步这套系统还可以扩展预约功能热门器材按时间段预约避免集中挤兑。我在实际设计订单表时预留了 time_slot 相关的字段位置后续想做时间段预约可以直接扩展不用改动表结构。2. 技术选型为什么是 Django Flask 而不是单一框架2.1 Django后台管理与业务建模的第一选择Django 最吸引人的地方是“全家桶”式的开发体验。自带 ORM、Admin 后台、认证系统、表单处理、模板引擎、CSRF 防护几乎把 Web 开发中最常见的基础设施都给你备好了。对于校园器材租赁这种以数据管理为核心的系统Django 的后台可以直接生成一套可用的增删改查界面管理员管理器材、查看订单记录基本不用额外写前端就能满足大部分日常操作。Django ORM 的模型设计能力在这个项目里发挥了很大作用。器材、订单、用户之间的关系比较清晰用 Django 的 models 定义好外键和索引后查询既方便性能也可靠。Admin 后台还可以通过简单的注册配置让超级管理员直接在线维护数据库记录这一点对非开发的运维人员非常友好。Django 内置的用户认证系统直接解决了我的一大块工作量。登录、登出、session 管理、密码加密都是成熟的实现我要做的只是扩展 User 模型加入角色字段再自定义几组权限。如果从零手写这些功能至少要增加两千行代码。2.2 Flask轻量 API 层的灵活补充Flask 在这个项目里扮演的角色是 API 服务层。为什么已经有了全功能的 Django 还要加一个 Flask原因在于Django 的模板页面适合给电脑端管理员用但学生端的需求更偏向移动端——在手机上快速查器材、提交借还。Django 模板做移动端适配虽然也能做但前后端耦合太紧后续如果想开发小程序或者 App就得把 Django 的接口重新梳理一遍。Flask 则可以在项目里独立开出一套 /api/ 路径的 RESTful 接口默认返回 JSON 数据。学生端页面放在 Django 里渲染其中数据请求通过 Ajax 打到 Flask 的 API。后续如果团队决定开发微信小程序前端直接调用 Flask 接口就行Django 的模板页面可以完全不动。这就是“业务后台”和“数据接口”分离的好处。Flask 本身的轻量性也符合这个定位我只需要定义几个 Blueprint 模块和路由函数处理 JSON 序列化和简单的跨域配置代码量非常可控。整个 Flask 应用我写了 300 行左右就覆盖了器材查询、租赁操作、历史记录三大组接口。2.3 和 FastAPI 对比为什么这个项目没选它最近热门的 FastAPI 确实是做 API 层的优秀选择性能好、自带文档、自动校验数据我在其他项目里也经常用它。但这个项目最终没有把 Flask 换成 FastAPI主要原因是团队和后续维护者的技术栈。校园系统往往会被下一年接手的同学继续开发Flask 的学习曲线更平缓网上参考资料也多对刚接触 Python Web 开发的初学者更友好。另外一点Flask 的生态和 Django 配合起来更成熟。网上有很多现成的 Django Flask 混合项目骨架部署方案比如用 Nginx 同时代理两个 Gunicorn 服务踩坑记录也更多。FastAPI 虽然好但它依赖 Pydantic v2版本更新较快对于求稳的校园项目而言成熟稳定比“先进”更重要。不过如果从一开始就明确系统只做前后端分离不做模板渲染后端只提供 API那 FastAPI 完全值得选。这个项目是模板API 混合模式Flask 是更稳妥的中间方案。2.4 Django 与 Flask 如何在一个项目里共处两个框架共存听起来复杂实际操作并不难。我的项目目录结构是这样的backend/ ├── manage.py ├── sports_equipment_system/ # Django 项目配置目录 │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── apps/ │ ├── accounts/ # 用户角色 App │ ├── equipment/ # 器材管理 App │ └── rental/ # 租赁订单 App ├── flask_api/ │ ├── app.py # Flask 入口 │ ├── blueprints/ │ │ ├── equipment_api.py │ │ └── rental_api.py │ └── database.py # 数据库连接模块 ├── templates/ # Django 模板 ├── static/ # 静态资源 └── requirements.txtDjango 负责 MVC 中的 Model、后台管理、模板渲染Flask 只负责面向 JSON 的 API。两者共享同一个数据库。Django 侧通过 ORM 访问数据Flask 侧用 SQLAlchemy 或直接 PyMySQL 访问同一张表。部署时用不同的端口跑两个服务Nginx 按路径前缀分发/api/ 开头的请求转给 Flask其余请求转给 Django。3. 数据模型与业务状态机设计3.1 器材信息模型不是每样器材只有“一件”设计器材表的时候最容易犯的错是把一个器材对象对应成“一件实物”。实际上篮球这种器材可能有 50 个每一个都是独立资产可能有不同的购买批次和磨损状态。但 50 个篮球如果建 50 条记录订单表的外键就非常臃肿归还时还要逐一确认是哪一只。我的方案是引入“器材类别 库存数量”的模式。Equipment 代表一类器材用 total_count 记录总数用 available_count 记录当前可借数量。EquipmentItem 是更细一层的实体记录具体每一件器材的编号和状态但这一层在很多基础需求下可以不建。考虑到系统上线初期管理员录入工作量我最终只用了 Equipment 类别模型用数量的增减来表达借还变化。Equipment 模型示例from django.db import models class EquipmentCategory(models.Model): name models.CharField(max_length50, uniqueTrue, verbose_name器材类别) description models.TextField(blankTrue, verbose_name描述) created_at models.DateTimeField(auto_now_addTrue) class Meta: verbose_name 器材类别 def __str__(self): return self.name class Equipment(models.Model): STATUS_CHOICES [ (available, 可借用), (borrowed, 全部借出), (maintenance, 维修中), (retired, 报废), ] category models.ForeignKey( EquipmentCategory, on_deletemodels.PROTECT, related_nameequipments, verbose_name类别 ) name models.CharField(max_length100, verbose_name器材名称) code models.CharField(max_length30, uniqueTrue, verbose_name器材编号) total_count models.PositiveIntegerField(default1, verbose_name库存总量) available_count models.PositiveIntegerField(default1, verbose_name当前可借量) location models.CharField(max_length100, blankTrue, verbose_name存放位置) status models.CharField(max_length20, choicesSTATUS_CHOICES, defaultavailable, verbose_name状态) created_at models.DateTimeField(auto_now_addTrue) updated_at models.DateTimeField(auto_nowTrue) class Meta: verbose_name 器材 ordering [-created_at] def __str__(self): return f{self.name}({self.code})category 字段的 on_delete 用了 PROTECT 而不是 CASCADE是因为器材类别被器材引用时不允许被直接删除必须先把该类别下的器材转移或下架。这类业务保护在真实系统中非常重要如果默认用 CASCADE管理员误删一个类别整批器材记录会连带消失。3.2 租赁订单模型从借到还的完整闭环租赁订单是系统的核心表所有业务流程都围绕它展开。我设计订单时重点关注几个问题订单号怎么生成、状态怎么流转、押金和租金怎么算、逾期怎么判定。订单号我采用时间戳加随机数的方案形如 R202405171430001234生成逻辑本身很简单但不依赖数据库自增主键避免多进程下序号冲突。订单状态我用了一段连续的状态机class RentalOrder(models.Model): STATUS_CHOICES [ (pending, 待审核), (approved, 待取货), (active, 租赁中), (returned, 已归还), (overdue, 已逾期), (cancelled, 已取消), ] order_no models.CharField(max_length32, uniqueTrue, verbose_name订单号) user models.ForeignKey( accounts.CustomUser, on_deletemodels.CASCADE, related_namerental_orders, verbose_name借用人 ) equipment models.ForeignKey( Equipment, on_deletemodels.PROTECT, related_namerental_orders, verbose_name器材 ) quantity models.PositiveIntegerField(default1, verbose_name借用数量) start_time models.DateTimeField(verbose_name借出时间) due_time models.DateTimeField(verbose_name应归还时间) actual_return_time models.DateTimeField(nullTrue, blankTrue, verbose_name实际归还时间) deposit models.DecimalField(max_digits8, decimal_places2, default0, verbose_name押金) rent_fee models.DecimalField(max_digits8, decimal_places2, default0, verbose_name租金) status models.CharField(max_length20, choicesSTATUS_CHOICES, defaultpending, verbose_name状态) remark models.CharField(max_length255, blankTrue, verbose_name备注) created_at models.DateTimeField(auto_now_addTrue) updated_at models.DateTimeField(auto_nowTrue) class Meta: verbose_name 租赁订单 ordering [-created_at]状态流转的规则我用了一个元组字典来管理只允许合法跳转。比如 “pending” 只能跳到 “approved” 或 “cancelled”不能直接跳到 “returned”。这样可以防止逻辑混乱后端在更新状态前先校验当前状态是否允许目标状态。3.3 库存扣减的并发问题select_for_update 的必要性这是很多校园项目不做但实际必须考虑的问题。两个学生同时提交借用申请同时执行“检查 available_count 然后减一”的逻辑在并发场景下可能导致库存变成负数。数据库层面需要行锁来保证原子性。Django ORM 的解决方案很简单在事务里使用 select_for_updatefrom django.db import transaction transaction.atomic def create_rental_order(user, equipment_id, quantity, due_time): equipment Equipment.objects.select_for_update().get(idequipment_id) if equipment.available_count quantity: raise ValueError(库存不足) equipment.available_count - quantity equipment.save() order RentalOrder.objects.create(...) return orderselect_for_update 会在数据库层面锁定这条器材记录直到事务提交或回滚。第二个请求必须等第一个请求完成才能执行库存检查从根源上杜绝超借。代价是并发性能下降但对校园系统完全没有影响因为同一时刻操作同一器材的请求量级根本不大。4. Django 核心模块实现从零搭出一个可用的管理后台4.1 项目初始化与结构Django 项目的初始化直接使用官方命令pip install django djangorestframework flask flask-cors pymysql django-admin startproject sports_equipment_system . python manage.py startapp accounts python manage.py startapp equipment python manage.py startapp rentalsettings.py 里需要重点配置的几项把三个自定义 App 加入 INSTALLED_APPS配置数据库连接设置登录跳转地址和语言时区。如果要用 MySQL需要在文件里额外安装 PyMySQL 并且配置import pymysql pymysql.install_as_MySQLdb()数据库连接配置DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: sports_equipment_db, USER: root, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, OPTIONS: {charset: utf8mb4}, } }的时候特别提醒一点字符集一定要显式指定 utf8mb4。默认的 utf8 字符集存不了表情符号而现在的用户昵称、备注里出现 emoji 的概率很高一旦出现就会插入报错排查起来还以为是代码问题。4.2 用户角色与权限控制用户模型我选择继承 Django 自带的 AbstractUser再加上一个 role 字段from django.contrib.auth.models import AbstractUser from django.db import models class CustomUser(AbstractUser): ROLE_CHOICES [ (student, 学生), (teacher, 教师), (admin, 管理员), (superadmin, 超级管理员), ] role models.CharField(max_length20, choicesROLE_CHOICES, defaultstudent, verbose_name角色) student_id models.CharField(max_length20, blankTrue, verbose_name学号/工号) phone models.CharField(max_length20, blankTrue, verbose_name联系电话) class Meta: verbose_name 用户 def is_admin_user(self): return self.role in [admin, superadmin]权限控制用 Django 的 Group 和 Permission 组合实现。我在项目里先建了三个 Group分别对应学生、管理员、超级管理员然后把自定义权限绑定到对应组。视图层使用 login_required 和自定义的 role_required 装饰器控制访问。这个装饰器用起来很顺手from functools import wraps from django.http import HttpResponseForbidden def role_required(*roles): def decorator(view_func): wraps(view_func) def _wrapped_view(request, *args, **kwargs): if not request.user.is_authenticated: from django.shortcuts import redirect return redirect(login) if request.user.role not in roles: return HttpResponseForbidden(无权限访问) return view_func(request, *args, **kwargs) return _wrapped_view return decorator # 用法示例 role_required(admin, superadmin) def equipment_list(request): ...4.3 器材管理模块增删改查与状态流转器材管理是管理员最频繁使用的模块。列表页需要支持按名称、类别、状态筛选还要显示当前库存和可借数量。视图函数写起来并不复杂关键是筛选条件的组合from django.shortcuts import render from django.core.paginator import Paginator from django.db.models import Q from .models import Equipment, EquipmentCategory role_required(admin, superadmin) def equipment_manage(request): queryset Equipment.objects.select_related(category).all() name request.GET.get(name, ) category request.GET.get(category, ) status request.GET.get(status, ) if name: queryset queryset.filter(name__icontainsname) if category: queryset queryset.filter(category_idint(category)) if status: queryset queryset.filter(statusstatus) paginator Paginator(queryset, 10) page_number request.GET.get(page, 1) page_obj paginator.get_page(page_number) return render(request, equipment/manage.html, { page_obj: page_obj, categories: EquipmentCategory.objects.all(), status_choices: Equipment.STATUS_CHOICES, })器材新增和编辑直接复用 Django ModelForm表单校验交给框架处理from django import forms from .models import Equipment class EquipmentForm(forms.ModelForm): class Meta: model Equipment fields [category, name, code, total_count, location, status] widgets { name: forms.TextInput(attrs{class: form-control}), code: forms.TextInput(attrs{class: form-control}), total_count: forms.NumberInput(attrs{class: form-control, min: 1}), location: forms.TextInput(attrs{class: form-control}), category: forms.Select(attrs{class: form-control}), status: forms.Select(attrs{class: form-control}), } def clean_available_count(self): # 库存总量不能小于已借出数量 ...经验提示修改器材库存数量时一定要做校验。如果某器材当前有 5 个被借出管理员把 total_count 改成 3那么 available_count 就直接变成负数了。我在表单的 clean 方法里增加了相关校验逻辑前端的提示是“库存总量不能小于当前借出数量”。4.4 租赁下单与归还流程实现租赁下单的逻辑前面已经展示过部分代码。归还流程比下单更复杂因为涉及归还时间计算、逾期判定、库存回补、押金结算from datetime import datetime from django.utils import timezone from django.db import transaction transaction.atomic def handle_return(order_id, user): order RentalOrder.objects.select_for_update().get(idorder_id, useruser) if order.status not in [active, overdue]: raise ValueError(当前状态不可归还) now timezone.now() order.actual_return_time now order.status returned # 逾期则计算额外费用 if now order.due_time: overdue_days (now - order.due_time).days overdue_fee order.rent_fee * overdue_days * 0.05 order.remark f逾期{overdue_days}天逾期费用{overdue_fee:.2f}元 order.save() # 库存回补 equipment Equipment.objects.select_for_update().get(idorder.equipment_id) equipment.available_count min(equipment.total_count, equipment.available_count order.quantity) equipment.status available if equipment.available_count 0 else equipment.status equipment.save() return order归还的时候库存回补也要注意上限。available_count 加上归还数量后不能超过 total_count否则说明之前数据有问题。我在代码里用 min 函数做了一层兜底虽然正常逻辑走不到这里但数据异常时不会出现可借数量大于库存总量的荒谬情况。逾期状态的自动判定我采用定时任务实现。用 Django 的 manage.py 自定义命令配合系统 crontab 每半小时执行一次from django.core.management.base import BaseCommand from django.utils import timezone from rental.models import RentalOrder class Command(BaseCommand): help 将超过应归还时间的订单置为逾期 def handle(self, *args, **options): now timezone.now() updated RentalOrder.objects.filter( statusactive, due_time__ltnow ).update(statusoverdue) self.stdout.write(f{updated} 条订单已标记逾期)5. Flask API 层给二次开发和其他客户端留一条路5.1 接口设计思路RESTful 风格Flask API 层按照 RESTful 风格设计资源名用名词操作通过 HTTP 方法区分。我主要提供了以下几组接口方法路径功能GET/api/equipment获取器材列表支持分页、筛选GET/api/equipment/id获取器材详情POST/api/rental/apply提交租赁申请POST/api/rental/return提交归还申请GET/api/orders/my查询当前用户的租赁记录POST/api/auth/login用户登录换取 tokenPOST/api/auth/register用户注册接口全部返回 JSON 格式统一的结构是{ code: 0, message: success, data: {} }code 为 0 表示成功非 0 表示业务错误码。前端不管是用模板还是别的方式调用只要判断 code 就行。手机端同学如果想开发小程序这个接口文档直接可以对接前提是接口层做一下 token 身份校验。目前由于 Django 模板的 session 机制和 Flask 的接口不是同一套会话体系Flask 侧的接口采用的是简单的 token 方案用户调用登录接口后Flask 生成一个随机 token 存数据库后续请求通过 Authorization 头传递。5.2 数据读取方式直接连同一个数据库Flask 和 Django 共享同一个 MySQL 数据库。Flask 侧我用 PyMySQL 直连没有引入 SQLAlchemy。原因很简单接口层只有十几张表的查询和写入SQLAlchemy 的 ORM 映射在这个场景下没有带来足够好处反而增加了包依赖和配置工作量。数据库连接模块import pymysql DB_CONFIG { host: 127.0.0.1, port: 3306, user: root, password: your_password, database: sports_equipment_db, charset: utf8mb4, cursorclass: pymysql.cursors.DictCursor, } def get_conn(): return pymysql.connect(**DB_CONFIG)这里有个小坑pymysql 的 connections 默认不自动提交事务需要对 autocommit 做设置否则执行 INSERT 和 UPDATE 语句后不生效。我是在连接配置里加了 autocommitTrue如果忘了你就会很奇怪“数据写进去了刷新又没了”。5.3 一个完整的 Flask 接口示例器材列表接口的完整代码from flask import Blueprint, request, jsonify from database import get_conn equipment_bp Blueprint(equipment, __name__) equipment_bp.route(/api/equipment, methods[GET]) def list_equipment(): page int(request.args.get(page, 1)) per_page int(request.args.get(per_page, 10)) keyword request.args.get(keyword, ) category_id request.args.get(category_id, ) conn get_conn() cursor conn.cursor() where [] params [] if keyword: where.append(name LIKE %s) params.append(f%{keyword}%) if category_id: where.append(category_id %s) params.append(category_id) where_sql AND .join(where) if where else 11 offset (page - 1) * per_page cursor.execute( fSELECT id, name, code, total_count, available_count, location, status fFROM equipment_equipment WHERE {where_sql} LIMIT %s OFFSET %s, [*params, per_page, offset] ) items cursor.fetchall() cursor.execute( fSELECT COUNT(*) AS total FROM equipment_equipment WHERE {where_sql}, params ) total cursor.fetchone()[total] cursor.close() conn.close() return jsonify({ code: 0, message: success, data: { list: items, page: page, per_page: per_page, total: total, } })这个接口注意两点一是表具体名称来自 Django 的 app_label modelequipment App 下的 Equipment 模型默认表名就是 equipment_equipment别想当然写错一旦写错就会报“table doesnt exist”。二是查询中文关键字时如果出现乱码大概率是 MySQL 连接字符集和表字符集不一致确认两边都统一为 utf8mb4 就好。6. 前端页面与交互细节6.1 模板选型与整体布局前端我用了 Django 模板 Bootstrap 5 jQuery CDN 的组合。没有引入 Vue 或 React主要原因是 Django 模板已经能完成大部分渲染工作避免过度设计。Bootstrap 负责响应式布局学生用手机访问时界面自动适配电脑端用卡片和表格展示视觉上清晰干净。整体布局采用左侧导航加右侧内容区的经典后台结构。左侧导航用 Bootstrap 的 Offcanvas 组件在手机端折叠电脑端常驻。导航项目按用户角色动态渲染学生看“器材列表”“我的租赁”“提交申请”管理员看“器材管理”“订单审核”“归还登记”“逾期列表”超级管理员额外看“用户管理”“数据统计”。模板继承让整个项目的前端维护非常省心。base.html 里定义好导航和公共样式子模板只覆盖 content 块{% extends base.html %} {% block title %}器材列表{% endblock %} {% block content %} ... {% endblock %}6.2 列表页的筛选与分页器材列表页的筛选区我放在页面顶部包含搜索框、类别下拉框、状态下拉框三个控件。搜索默认用表单 GET 提交和 Django 视图的查询参数对应这样用户可以直接通过 URL 分享筛选结果比如 /equipment/?name篮球category1。分页用 Bootstrap 的 Pagination 样式Django 端的 Paginator 已经处理好了 page_obj模板里渲染页码即可。我的分页逻辑是只显示当前页前后两页和首尾页避免器材种类几百个时页码滚列表过长。Ajax 交互主要集中在“申请借用”和“归还”两个操作。点击按钮后前端先做表单校验数量不能为 0、归还时间必须大于当前时间然后 POST 到 Flask 的 API根据返回的 code 显示成功或失败提示。这样避免了 Django 表单提交整页刷新体验更接近原生应用。6.3 借还操作的表单校验与提示前端校验和后端校验要配合。前端校验是为了快速响应后端校验才是安全底线。我在“申请借用”表单里加入了几个规则数量必须为正整数、数量不能超过该器材当前可借量、选择的使用时长范围为 1 到 72 小时。这些规则在前端用 jQuery Validation 实现后端在 Django 视图和 Flask 接口里各写了一遍同样的校验。有一个交互细节很值得提归还操作必须弹窗二次确认。因为我实际使用中遇到过管理员误点归还按钮、把还没还的器材提前归库的情况导致库存数据不准。二次确认弹窗里明确展示“器材名称、借用数量、应归还时间、当前状态”管理员确认无误后才提交。7. 部署上线Nginx Gunicorn 双服务协同7.1 服务器环境准备部署服务器我用的是一台 2 核 4G 的 CentOS 云服务器跑校园系统绰绰有余。上线流程从安装 Python 环境开始服务器自带 Python 3.6但有些依赖需要更高版本所以我用 pyenv 安装了 Python 3.9 并设置为虚拟环境。依赖安装直接用pip install -r requirements.txt python manage.py makemigrations python manage.py migrate python manage.py collectstatic python manage.py createsuperusercollectstatic 是静态文件收集这步很容易被忽略。Django 开发模式会自动代理静态文件生产环境不会必须把 static 目录下所有文件收集到一个统一目录交给 Nginx 直接托管。7.2 Gunicorn 启动两个服务Django 和 Flask 分别用 Gunicorn 启动。我的启动命令# Django 服务监听 8000 端口 gunicorn sports_equipment_system.wsgi:application \ --workers 3 \ --bind 127.0.0.1:8000 \ --timeout 60 # Flask API 服务监听 5000 端口 gunicorn -w 2 -b 127.0.0.1:5000 flask_api.app:app两个服务都建议用 systemd 托管这样开机自启、异常自动重启、日志集中管理都有了。我之前偷懒用了 nohup 后台运行结果服务器重启一次服务就丢了远程还没法立即登上去拉起来非常被动。写成 systemd 服务后systemctl restart sports-django 一行命令搞定。7.3 Nginx 反向代理配置Nginx 的核心配置文件如下server { listen 80; server_name your-campus-domain.com; client_max_body_size 10M; # 静态文件直接由 Nginx 服务 location /static/ { alias /opt/sports_equipment/staticfiles/; } location /media/ { alias /opt/sports_equipment/media/; } # API 请求全部转发到 Flask location /api/ { proxy_pass http://127.0.0.1:5000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 其余请求转发到 Django location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }配置完成后 nginx -t 检查语法然后 nginx -s reload 生效。特别强调一点proxy_pass 后边的地址写 127.0.0.1:8000 时末尾不要加斜杠加了斜杠会导致 URL 路径被替换找半天都找不出问题。7.4 静态文件与媒体文件的坑上线阶段最容易耗时间的不是代码而是静态文件 404。Django 项目的 Admin 后台模板、Bootstrap 样式、图片动辄几十上百个文件如果 settings.py 里的 STATIC_ROOT 配置不对collectstatic 就会收集不完整。我检查的顺序是# 确认 settings.py STATIC_URL /static/ STATIC_ROOT /opt/sports_equipment/staticfiles # 收集静态文件 python manage.py collectstatic --noinput # 检查目录文件数量 ls -l /opt/sports_equipment/staticfiles如果 Nginx 直接服务静态文件后还是 404先 curl 一下 http://127.0.0.1/static/admin/css/base.css 看返回结果逐层排查是 Django 没收集还是 Nginx 路径错误。8. 我实际踩过的坑和排查思路8.1 时区问题订单逾期时间总是差 8 小时上线第二天管理员反馈订单的应归还时间总比提交时选的晚 8 小时。查了一圈发现是 Django 时区设置问题。settings.py 里 USE_TZTrueTIME_ZONE 默认是 UTC而前端页面提交的本地时间Django 存进数据库时按 UTC 转换取出来显示又转回 UTC时间就“漂移”了。解决方案是把 TIME_ZONE 和 USE_TZ 设置为USE_TZ True TIME_ZONE Asia/Shanghai然后更关键的步骤已有历史订单里的时间全部是 UTC 存储的需要对数据做一次时间转换修正否则新老数据的对比永远是错的。真实教训就是数据库统一存 UTC 没关系但写业务逻辑判断逾期、计算时长时一定要在入口处把本地时间转换为 timezone-aware 对象再入库from django.utils import timezone # 前端传入的 start_time 是 naive datetime start timezone.make_aware(start_time, timezone.get_current_timezone())8.2 库存超卖并发下单导致的负库存项目内测阶段让几个同学同时抢同一件器材结果库存从 5 变成了 -1。一开始我以为是前端校验被绕过后来复现发现是并发问题。两个请求同时读到 available_count1都判断可以借出然后各自减一就变成 -1。解决就是用前面说过的 select_for_update 加事务。这个坑不实际并发压测很难发现普通的单用户调试根本不会暴露。提醒所有做类似库存系统的同学只要涉及数量扣减一定要考虑行锁。8.3 CSRF 校验失败学生注册功能上线后有同学反馈注册提交后被拦截日志报 CSRF verification failed。原因是我的注册页面模板里表单没有加 {% csrf_token %}。Django 对 POST 请求默认启用 CSRF 防护模板表单少加这个隐藏字段所有 POST 都会被拦。排查方法很简单打开页面源码搜索“csrfmiddlewaretoken”看有没有这个 hidden input。没有就把 {% csrf_token %} 加进 form 标签内部。Flask API 侧的接口不需要也不能依赖 Django 的 CSRF 机制。前端调用 Flask 接口时用 token 认证没有 token 的匿名请求只开放注册和器材查询接口其他接口全部返回 401。8.4 常见问题速查表问题现象排查方向解决方案静态文件 404Nginx 路径、STATIC_ROOT检查 collectstatic 目录、路径结尾斜杠中文乱码数据库连接字符集统一 utf8mb4数据写入无变化autocommit 关闭设置 autocommitTrue时间相差 8 小时USE_TZ 与 TIME_ZONE统一 Asia/Shanghai转换 naive datetime库存负数并发未锁行select_for_update 事务CSRF 拦截模板缺少 csrf_token表单加 {% csrf_token %}502 Bad GatewayGunicorn 服务未启动systemctl status 检查服务进程MySQL 连接数报警未使用连接池引入连接池或限制连接生命周期项目实施下来我最深的感受是这个系统真正的难点不是“把 Django 和 Flask 跑起来”而是把业务流程理清楚、把边界情况想到位。库存会不会超卖、时间会不会乱、权限有没有漏洞、状态能不能乱跳这些问题比写多少页面都重要。你照着这篇文章的架构去复现核心流程都能跑通如果再把我踩的这些坑提前避开整个上线过程会顺利得多。最后分享一个小建议器材租赁系统做完后试着把“预约”功能加上那个模块才是真正拉开普通设计和优秀设计差距的地方。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑