ARTICLE DETAIL

资讯详情

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

Python Flask实现的高校教室报修管理平台:工单流转与资源台账全解析

Python Flask实现的高校教室报修管理平台:工单流转与资源台账全解析 教室报修这个事看起来小做起来头疼。我接到这个需求时的背景是这样的高校教学楼几十间教室设备坏了靠口头通知、纸质登记维修师傅跑上跑下管理员坐在办公室里根本不知道哪个教室还没修、修到哪一步了。于是就有了这个基于 Python Flask 的高校教室报修资源管理平台。它要解决的不只是“报修登记”而是把教室资源、设备台账、报修工单、维修人员调度串成一条完整链路让每一次报修都有迹可循、让管理员的统计从“翻本子”变成“看页面”。这篇文章我会把整个项目从需求拆解、技术选型、模块实现到部署上线的过程完整复盘一遍重点讲清楚每个设计决策背后的原因和踩过的坑适合正在做课程设计、毕业设计或者想拿 Flask 练一个完整业务系统的同学参考。项目本身不复杂但麻雀虽小五脏俱全三种角色权限、资源管理、工单流转、统计看板这些逻辑搞明白后换到任何管理类系统都能套用。1. 项目概述与需求拆解1.1 报修管理的现实痛点高校教室报修这个场景最典型的状态是这样的一门课上到一半投影仪突然坏了学生代表跑去找楼下管理员管理员拿出一个皱巴巴的本子记下来然后打电话联系维修师傅。维修师傅根据本子上的信息挨个教室跑结果有的教室已经临时换课没人来有的设备其实只是没通电真正需要维修的反而被排在后面。我接手项目后先去蹲点了两所学校的教室管理员办公室蹲完的感受是问题不在“没人报修”而在“报修信息失真的链路太长”。纸质登记的信息缺失率非常高故障描述写成“投影坏了”四个字根本不包含教室编号、设备型号、报修时间、紧急程度。等维修师傅看到本子时间已经过去半天信息还要靠猜。平台要解决的核心痛点可以拆成三条第一条是信息结构化报修必须关联具体教室、具体设备、具体故障现象第二条是流程可追踪每一张报修单从提交到处理完毕的每一步操作都要有记录第三条是管理可统计管理员期末写设备维护报告时能从系统直接拉出每个月报修次数、故障设备排名、平均维修时长这些数据。1.2 角色设计与业务流程梳理我把系统按使用角色划分成三类每一个角色只做自己职责范围内的事情避免权限交叉导致混乱。角色核心权限典型操作普通用户学生/教师提交报修、查看本人工单、评价反馈选择教室与设备、填写故障描述、提交后跟踪状态维修人员查看分派给自己的工单、更新维修进度接单、填写维修备注、标记完成并说明处理结果管理员管理教室/设备/用户/全部工单审核报修、派单给维修工、查看统计报表、维护资源台账报修单的生命周期我最终定义为“五状态循环”待审核 → 已派单 → 维修中 → 已完成 → 已评价。每个状态变化的动作都很明确用户在首页填表提交系统进入“待审核”管理员看到新报修后指定维修工状态改为“已派单”维修工点击开始维修变为“维修中”维修工提交处理结果后变为“已完成”此时用户端可以填写评价变为“已评价”。这里有一个容易被忽略的设计重点状态流转不能允许倒退。比如维修中之后不能直接跳回待审核否则维修工误操作会把整个流程搞乱。我在代码里是通过一个状态映射表来约束合法迁移路径这是一个在答辩时会被频繁追问的设计点提前想清楚会加分。1.3 功能边界控制我见过很多同类项目失败不是因为功能太少而是因为功能太多。有人一上来就想加在线支付、短信通知、地图定位结果开发量翻了数倍核心流程反而没跑通。这次我给自己定的原则是把“报修闭环”做透其他一律排到二期。所以第一版砍掉了以下功能自动排班、维修评价打分权重、消息推送、微信小程序端。只保留一个看板页面的简单统计因为管理员确实需要快速看到“今天还有几单没处理”。砍功能不等于不做接口预留。我在数据库设计时为用户表预留了一份 role 字段为后续接入企业微信通知留了一个 message_push 扩展位这样二期接入时不需要改动表结构。2. 技术选型与整体架构设计2.1 为什么是 Flask 而不是 Django选 Flask 不选 Django很多人第一反应是“Django 更重更全”。这个说法对但不完全。对于这个项目核心决策因素有三个第一系统规模属于校园级用户同时在线量不大并发峰值可能不超过几十个人Flask 这种轻量框架完全够用不需要 Django 自带的重型 ORM 和 Admin 后台第二Flask 的 Jinja2 模板和原生路由写法非常直观即使初学者也能快速理解请求是怎么进来的、页面是怎么渲染出去的这对后续维护和课程设计展示都很友好第三Flask 的第三方扩展生态虽然是“按需选装”但最基本的 flask-sqlalchemy、flask-wtf、flask-login 都有成熟稳定的版本组合起来并不比 Django 缺东西。Flask 被质疑最多的一点是“没有固定的项目结构”但从另一个角度看这也意味着我可以根据项目实际规模去定制目录。后面会详细讲我采用的蓝图结构它在表达能力上完全够用。2.2 数据库选型与表关系设计数据库我在开发阶段使用 SQLite部署到生产环境换成 MySQL。原因比较现实SQLite 零配置、单文件、方便本地调试适合初期把业务逻辑跑通但它对并发写入支持较弱校园正式使用中会出现多个维修工同时更新工单的情况所以生产环境必须换 MySQL。表结构我设计了五张核心表用户表、教室表、设备表、报修单表、操作日志表。先看最简单的三张基础表。用户表字段id、username、password_hash、real_name、roleuser/worker/admin、phone、created_at。密码绝不存明文用 werkzeug.security 提供的 generate_password_hash 加密。教室表字段id、name如“教学楼A-301”、building所属楼宇、floor楼层、capacity座位数、status可用/停用/维修中。设备表字段id、classroom_id外键关联教室、equipment_type投影仪/电脑/音响/空调、model型号、asset_no资产编号、purchase_date采购日期、status。报修单表是整个业务的核心字段较多下面单列说明。报修单表字段id、repair_no工单号年月日序列号、user_id报修人、classroom_id、equipment_id可空因为有时是整个教室灯管坏了、fault_desc故障描述、fault_type故障分类、priority紧急程度、status五状态之一、assigned_worker_id派单维修工、admin_id审核人、created_at、updated_at、solved_at完成时间、feedback用户评价。操作日志表用来记录每一次状态变更的详情id、repair_id、operator_id、action提交/审核/派单/开始维修/完成/评价、remark、operated_at。这五张表的关系用一句话概括用户表公告报修单教室表关联设备表报修单挂在教室和设备之下并绑定操作全过程。在用 SQLAlchemy 定义模型时外键关系不要偷懒只写字段不写 relationship后续做前端联动筛选会非常麻烦。2.3 项目目录结构与蓝图划分Flask 项目最忌讳把所有路由写在一个 app.py 里。我见过太多初学者最后改一个功能要滚动半天找函数这次我按业务域拆成三个蓝图目录结构如下repair-platform/ ├── run.py # 启动入口 ├── config.py # 配置分离 ├── requirements.txt # 依赖清单 ├── app/ │ ├── __init__.py # 应用工厂 │ ├── models.py # 所有数据模型 │ ├── forms.py # 表单校验 │ ├── auth/ # 登录注册蓝图 │ ├── resource/ # 教室/设备管理蓝图 │ ├── repair/ # 报修核心流程蓝图 │ ├── dashboard/ # 统计看板蓝图 ├── templates/ │ ├── base.html # 公共模板 │ ├── auth/ # 登录注册页面 │ ├── resource/ # 资源管理页面 │ ├── repair/ # 报修相关页面 │ └── dashboard/ # 统计页面 └── static/ ├── css/ ├── js/ └── uploads/ # 上传图片目录若有启动方式用应用工厂模式在app/__init__.py中创建 app 实例并注册蓝图run.py只负责读取配置并调用工厂方法。这样做的好处是测试时可以直接 import app 而不会重复初始化。3. 核心功能实现详解3.1 登录与会话权限控制权限控制是整个系统安全的基础。我用 Flask 的 session 配合装饰器实现没有引入重量级的 flask-login因为本项目角色逻辑简单自实现反而更好理解。核心代码如下from functools import wraps from flask import session, redirect, url_for, flash def login_required(f): wraps(f) def wrapper(*args, **kwargs): if user_id not in session: flash(请先登录后再操作, warning) return redirect(url_for(auth.login)) return f(*args, **kwargs) return wrapper def role_required(*roles): def decorator(f): wraps(f) def wrapper(*args, **kwargs): if user_id not in session: return redirect(url_for(auth.login)) if session.get(role) not in roles: flash(没有权限执行此操作, danger) return redirect(url_for(main.index)) return f(*args, **kwargs) return wrapper return decorator使用方式很直接维修工接口加role_required(worker, admin)管理员接口只用role_required(admin)。这里有个我踩过的坑Flask 的 session 默认通过 cookie 存储SECRET_KEY如果不配置每次重启服务后 session 都会失效用户必须重新登录。所以 config.py 里一定要写一个固定的、足够随机的 SECRET_KEY。装饰器的顺序也值得注意如果多个装饰器叠加login_required必须放在外层、路由装饰器app.route放在最外层否则路由判断先于登录判断执行未登录用户会被放行到视图函数内部才被拦截虽然逻辑上还是能拦下来但 URL 行为会变得很怪。3.2 教室资源台账管理教室资源模块本质上是一个基础数据维护页面。管理员登录后可以新建教室、停用教室、添加设备、修改资产的归属状态。这块功能从实现难度上看不复杂但它是报修流程的“数据地基”。我花了比较大的心思在页面联动上。提交报修表单时用户先选教学楼再选楼层的教室列表最后根据该教室下的设备列表选择故障设备。这三级联动如果不用 Ajax每次刷新页面都会丢失已选状态体验很差。我的方案是三个独立接口返回 JSONapp.route(/api/classrooms_by_building/building) def classrooms_by_building(building): rooms Classroom.query.filter_by(buildingbuilding, statusactive).all() return jsonify([{id: r.id, name: r.name} for r in rooms])前端用 jQuery 的$.getJSON监听下拉框 change 事件依次填充下一级列表。这里有一个细节设备列表可能出现“全部设备已正常”的情况所以下拉框的第一项永远是“不指定具体设备”对应数据库里 equipment_id 为 NULL用于整间教室的维修场景。设备状态在报修完成后应该被自动联动更新。比如一台投影仪报修完成且标记为“已更换灯泡”它的状态应该从“故障”变为“可用”。这个我是在报修单完成的时候额外做了一次equipment.status active的更新而不是让管理员手动再去设备管理页改减少了一个容易遗漏的人工环节。3.3 报修工单核心流转报修流程是整个平台的重头戏我把它拆成了五个子场景创建工单、审核派单、维修处理、完成反馈、状态时间线。创建工单的视图接收前端表单 POST 数据先做基础校验比如故障描述不能为空、选定的教室必须存在。校验通过后生成工单号格式是BX 年月日 三位序号例如BX20250112001。生成逻辑很简单查当天最大序号加一避免并发时重号最简单的方式是在序号字段上建唯一索引如果插入报唯一冲突就重试一次。审核派单是管理员的专属操作界面。页面上默认只显示待审核列表每条报修单旁边有一个“派单”按钮点击后弹出一个维修工下拉选择框。核心视图如下repair_bp.route(/repair/int:repair_id/assign, methods[POST]) role_required(admin) def assign_repair(repair_id): repair RepairOrder.query.get_or_404(repair_id) worker_id request.form.get(worker_id, typeint) worker User.query.get(worker_id) if not worker or worker.role ! worker: flash(请选择有效的维修人员, danger) return redirect(url_for(repair.repair_detail, repair_idrepair_id)) repair.assigned_worker_id worker_id repair.status assigned repair.admin_id session[user_id] db.session.commit() flash(派单成功, success) return redirect(url_for(repair.repair_detail, repair_idrepair.id))维修工登录后看到的是“我的工单”列表分为待处理和已处理两组。点击开始维修调用一个接口更新状态为 processing点击完成则需要填写处理结果字段并选择该设备是否恢复正常。两个动作分别对应状态机里的合法迁移不允许跨状态操作。用户在个人中心可以看到自己的历史工单和当前进度状态为已完成时可以提交反馈填写的内容写入 feedback 字段同时状态变为 evaluated。为了让整个工单流转的每一步都清晰可见操作日志表在每一个动作发生时都会插入一条记录渲染到详情页的底部时间线上。这个设计在答辩时是亮点因为“能证明系统流程完整”比“功能能跑”更有说服力。3.4 统计看板模块统计看板是给管理员做“滚动管控”用的不需要做得多花哨关键是数据准确和刷新及时。我实现了四个核心指标待处理工单总数、本月新增报修数、平均维修完成时长、故障设备排行 Top 5。前两项直接用 SQLAlchemy 的 count 和 created_at 时间过滤实现平均维修时长则是查询所有已完成工单中solved_at - created_at的平均值。故障设备排行需要 group by equipment_id 然后按 count 倒序注意 equipment 为空整间教室报修的数据要过滤掉。图表展示我没有用 ECharts 而是用最简单的纯 CSS 表格水平条因为这类校内系统用户只关心数字不关心炫酷交互。图表库往往要引入大体积的 JS 文件在校园网环境下加载速度反而更慢得不偿失。4. 实操过程与关键代码4.1 环境准备与项目初始化从零开始复现这个项目第一步是确认环境。我的建议是 Python 3.10 及以上版本不要用 3.6 以下的老版本因为很多 Flask 扩展已经不再兼容旧版语法。安装依赖用 pip一行命令搞定pip install flask flask-sqlalchemy flask-wtf python-dotenv这里额外说一句flask-wtf 不是只用来做表单验证的。它的 CSRF 保护功能非常关键默认每个 POST 表单都需要带{% csrf_token() %}才能通过校验能有效防止跨站请求伪造。如果不喜欢写模板标签也可以通过app.config[WTF_CSRF_ENABLED] False全局关闭但我不建议在生产环境这么干。创建虚拟环境是必须的操作不要图省事直接装在系统 Python 全局环境里面。不同项目依赖版本互相污染的问题等你要部署第二个 Flask 项目时就会深刻体会到。推荐 python -m venv 创建虚拟环境具体命令如下python -m venv venv source venv/bin/activate # Linux/macOS venv\Scripts\activate # Windows4.2 数据模型定义与建库数据模型我用 Flask-SQLAlchemy 定义以设备表和报修单表为例实际代码可以这样写from flask_sqlalchemy import SQLAlchemy from datetime import datetime db SQLAlchemy() class Equipment(db.Model): __tablename__ equipment id db.Column(db.Integer, primary_keyTrue) classroom_id db.Column(db.Integer, db.ForeignKey(classroom.id), nullableFalse) equipment_type db.Column(db.String(32), nullableFalse) model db.Column(db.String(64)) asset_no db.Column(db.String(64), uniqueTrue) status db.Column(db.String(16), defaultactive) # active/breakdown/inactive created_at db.Column(db.DateTime, defaultdatetime.now) classroom db.relationship(Classroom, backrefdb.backref(equipments, lazydynamic)) class RepairOrder(db.Model): __tablename__ repair_order id db.Column(db.Integer, primary_keyTrue) repair_no db.Column(db.String(32), uniqueTrue, nullableFalse, indexTrue) user_id db.Column(db.Integer, db.ForeignKey(user.id), nullableFalse) classroom_id db.Column(db.Integer, db.ForeignKey(classroom.id), nullableFalse) equipment_id db.Column(db.Integer, db.ForeignKey(equipment.id), nullableTrue) fault_desc db.Column(db.Text, nullableFalse) fault_type db.Column(db.String(16)) priority db.Column(db.String(8), defaultnormal) # low/normal/urgent status db.Column(db.String(16), defaultpending) assigned_worker_id db.Column(db.Integer, db.ForeignKey(user.id), nullableTrue) admin_id db.Column(db.Integer, db.ForeignKey(user.id), nullableTrue) created_at db.Column(db.DateTime, defaultdatetime.now) updated_at db.Column(db.DateTime, defaultdatetime.now, onupdatedatetime.now) solved_at db.Column(db.DateTime, nullableTrue) feedback db.Column(db.Text, nullableTrue) reporter db.relationship(User, foreign_keys[user_id], backrefreported_repairs) worker db.relationship(User, foreign_keys[assigned_worker_id]) classroom db.relationship(Classroom) equipment db.relationship(Equipment)定义好模型后初始化库表的操作是在命令行执行from app import db, create_app app create_app() with app.app_context(): db.create_all()create_all()只负责创建缺失的表不会更新已有的表结构。开发阶段如果改了模型字段最直接的方式是删掉数据库文件重新创建或者用 Flask-Migrate 做正规迁移。如果后面要长期维护建议从第一天就接上 Flask-Migrate否则表结构改到第三次时你会后悔。4.3 表单校验与信息发布用户提交报修是最需要做数据校验的环节。我没有手写一堆 if 判断而是用 flask-wtf 的 FlaskForm 声明式写法from flask_wtf import FlaskForm from wtforms import StringField, TextAreaField, SelectField, IntegerField from wtforms.validators import DataRequired, Length class RepairForm(FlaskForm): classroom_id SelectField(所在教室, coerceint, validators[DataRequired()]) equipment_id SelectField(故障设备, coerceint, validators[DataOptional()]) fault_type SelectField(故障分类, choices[(projector, 投影仪问题), (network, 网络故障), (power, 供电问题), (other, 其他)], validators[DataRequired()]) fault_desc TextAreaField(故障描述, validators[DataRequired(), Length(max500)]) priority SelectField(紧急程度, choices[(low, 一般), (normal, 中等), (urgent, 紧急)], defaultnormal)wtforms 的组件会自动渲染 HTML 并进行字段类型转义。SelectField 用coerceint可以把表单提交的字符串转换为 int如果转换失败直接抛校验错误不需要手动处理 ValueError。发布报修后的信息回显也很重要。用户提交成功后页面不会跳转到列表而是留在详情页并把用户刚才填的描述原样显示在故障描述区这样用户可以自己检查有没有写错。这个体验细节是蹲点的时候观察出来的——很多用户根本不知道自己提交的描述在管理员那边是什么样子。4.4 模板页面搭建与前端联动Jinja2 模板是 Flask 的原生渲染方式。base.html 里我统一做了三部分顶部导航栏、左侧菜单根据角色显示不同菜单项、中间内容区。所有子页面只需要继承 base 并覆盖 content block。以报修列表的模板片段为例{% extends base.html %} {% block content %} div classcard div classcard-header 我的报修记录 a href{{ url_for(repair.create) }} classbtn btn-primary btn-sm float-end新建报修/a /div table classtable table-striped thead tr th工单号/th th教室/th th设备/th th状态/th th提交时间/th th操作/th /tr /thead tbody {% for repair in repairs.items %} tr td{{ repair.repair_no }}/td td{{ repair.classroom.name }}/td td{{ repair.equipment.equipment_type if repair.equipment else 整间教室 }}/td td{{ status_map[repair.status] }}/td td{{ repair.created_at.strftime(%Y-%m-%d %H:%M) }}/td tda href{{ url_for(repair.detail, repair_idrepair.id) }}查看/a/td /tr {% endfor %} /tbody /table /div {% endblock %}前端联动之前提过的下拉框三级筛选这里用到一个小技巧页面加载时先获取教学楼列表教师选择变化后触发 ajax 请求对应楼层与教室而不是一次性把全部数据塞进一个下拉框。原因是教室多的学校有上百间全量加载不仅慢用户找起来也费劲。状态显示我维护了一个 Python 字典映射status_map { pending: 待审核, assigned: 已派单, processing: 维修中, completed: 已完成, evaluated: 已评价 }模板里直接用status_map[repair.status]显示中文数据库里存英文枚举值。这个映射表的好处是扩展新状态只需要改一处比如以后加“驳回”状态只需要在映射里加一行。4.5 本地运行与冒烟测试所有代码写完后的第一轮自测我按照真实业务流程从头到尾走了一遍管理员登录并创建教室和设备 → 普通用户注册并提交报修 → 管理员审核并派单 → 维修工接单并更新进度 → 用户评价反馈。启动开发服务器的命令很简单python run.py默认访问http://127.0.0.1:5000。这里要提醒一点Flask 自带的开发服务器app.run(debugTrue)只适合本地调试一定不要直接拿它做正式服务。开发模式下 debug 为 True 时会开启交互式调试器任何访问者都可能拿到 Python 执行环境这是极大的安全隐患。冒烟测试阶段我发现了一个很有代表性的事情用户提交报修时选择“整个教室”而不指定设备但在教师列表页做筛选查询时用了 INNER JOIN 把没有设备的报修单全部过滤掉了。这就是典型的 join 使用场景错误应该用 LEFT OUTER JOIN 保留报修表的所有记录设备信息为空时显示“整间教室”。这个 bug 让我明白写查询时先想清楚“主表是哪张、要不要保留无匹配行”再动笔写 join。5. 常见问题与排查实录5.1 问题速查表开发过程中我记录了不少典型报错整理成速查表很多是刚接触 Flask 的同学一定会碰到的。现象最常见原因解决办法浏览器显示 Internal Server Error 且无提示debug 未开启无法看到堆栈先设置app.config[DEBUG] True修完后必须关闭表单提交后提示 CSRF token missingFlaskForm 需要 CSRF 令牌模板中加{{ csrf_token() }}或在表单内加{{ form.hidden_tag() }}登录后跳转正常但刷新就退出登录SECRET_KEY 配置缺失或每次启动随机在 config.py 固定 SECRET_KEY数据库是乱码MySQL 数据库编码非 utf8mb4建库时指定CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ciSQLite 报 database is locked并发写入冲突开发时可设置connect_args{timeout: 15}生产换 MySQL上传图片访问返回 403图片目录不在 static 下或权限不足确认上传目录挂载、检查目录权限时间差 8 小时datetime.now() 与 utcnow 混淆统一用datetime.now()数据库配置时区为 Asia/Shanghai这里挑几个详细展开。5.2 SQLite 锁与并发写入开发阶段我用 SQLite第一次压测时发现维修工同时点了两次“开始维修”页面直接报database is locked。原因很简单SQLite 同一时间只允许一个写事务两个并发请求同时写入就会冲突。解决思路有三个层次。最轻量的是给 SQLAlchemy 连接配置一个超时参数SQLALCHEMY_ENGINE_OPTIONS { connect_args: {timeout: 10} }这只解决“等待”问题如果请求量再大还是不行正路是生产环境换 MySQL还有一个被很多人忽略的优化是事务保持短小不要在事务中间去做耗时的外部请求或者渲染模板。我曾经为了省事在同一个事务里既更新了工单状态又调用了一个外部的 Python 脚本做数据清洗结果那个脚本运行了三秒钟期间所有其他写请求全部排队超时。把外部调用挪出事务之后问题立刻消失。5.3 时间显示时区问题很多初学者在报修单详情页看到的时间比实际时间晚了 8 小时这是因为建表时用了datetime.utcnow而用户在中国时区。Flask 默认以 UTC 存时间前端显示时再转本地时间也可以但容易处处遗漏。我的处理方式是简单粗暴数据库里所有时间字段统一用datetime.now()存本地时间前端模板直接显示字符串。如果以后要部署到不同时区的服务器再迁移到 UTC 存储也不迟但校园系统基本不会跨时区。5.4 文件上传路径的经典坑如果把报修现场照片上传功能加上一定要提前规划文件名和路径。我最开始直接使用了用户原始文件名结果两个用户上传了同名照片后一个覆盖了前一个。修复方案是给文件名加 uuid 前缀import uuid from werkzeug.utils import secure_filename ext filename.rsplit(., 1)[-1].lower() new_filename f{uuid.uuid4().hex}.{ext}secure_filename会过滤掉路径和特殊字符避免像../../etc/passwd这样危险的文件名。上传目录只保存新文件名原始文件名存入数据库字段。另外上传目录必须放在static/uploads下并且给目录授予可读权限否则浏览器访问图片会 403。5.5 开发服务器端口被占用address already in use这个报错我也反复遇到过尤其是跑单元测试和开发服务器来回切换的时候。排查命令# Linux/macOS lsof -i:5000 # Windows 管理员权限下 netstat -ano | findstr :5000找到占用进程后按 PID 杀掉即可。如果是在容器里跑记得端口映射暴露配置要与进程监听端口一致。这里顺带说一句Flask 启动时如果设置app.run(host0.0.0.0)端口默认还是 5000不是常见的 80最终访问 URL 要加端口号。6. 部署上线与后续演进6.1 gunicorn 与 nginx 的生产部署本地跑通只是第一步真正上线要解决三个问题服务进程管理、反向代理、静态文件分离。我采用 gunicorn nginx 的组合在 Ubuntu 云服务器上的部署步骤大致如下pip install gunicorn gunicorn -w 4 -b 127.0.0.1:8000 run:app-w 4是启动 4 个 worker 进程一个简单的估算方式是服务器 CPU 核心数乘以 2 1。千万记得把 worker 数设得比数据库连接池大一些不然高并发下来回握手会把数据库拖垮。nginx 配置要点是处理好/的 proxy_pass 和/static/的 aliasserver { listen 80; server_name repair.example.edu.cn; location /static/ { alias /data/repair-platform/static/; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }再用 systemd 给 gunicorn 配一个服务托管保证服务器重启后系统自动拉起不需要人工登服务器敲命令。gunicorn 本身不推荐直接用 root 用户跑新建一个专用账户比较稳妥。6.2 数据备份与日常维护校园系统的数据量虽然不大但报修工单是重要的资产管理依据备份不能省。SQLite 阶段直接拷贝文件即可最好定时用 cron 执行0 2 * * * cp /data/repair-platform/data.db /data/backup/repair_$(date \%Y\%m\%d).dbMySQL 阶段用 mysqldump 做逻辑备份建议每周全量备份一次每天增量备份一次。恢复流程最好实际演练一遍确保备份文件是完整的、可用的。我见过太多人配了备份任务但从来没试过恢复等到真出问题时才发现备份脚本早因为密码变更失效了。运维监控方面最简单的方式是写一个健康检查脚本定时请求/health返回 JSON 状态码探测失败就触发告警。这个接口实现容易但价值极高。6.3 可扩展方向思考这个平台跑通之后后续值得做的扩展方向我觉得有三个。第一是消息通知链路的打通。现在用户登录系统后才知道维修进度太被动。可以接入企业微信机器人或者钉钉群机器人在报修单到达关键状态时推送一条通知学校内部系统用群机器人比短信成本低很多一条 webhook 就能搞定。第二是设备二维码扫码报修。给每台设备贴上一张二维码扫描后自动带入教室号、设备号用户只需要填故障描述信息准确性会大幅提高。这个功能本质上是给报修表单加一个初始参数对后端改动很小但对用户体验的提升立竿见影。第三是维修耗材库存联动。维修工填写“更换灯泡”之后系统自动从耗材库存中扣减一个灯泡库存低于阈值时提醒管理员补货。这个模块独立性强不影响现有表结构只要新增一个耗材表和消耗流水表就行。我个人在实际操作中最大的体会是这类校园级系统最怕的不是技术不够而是流程没有想清楚就急着写代码。这次我花在蹲点访谈和状态设计上的时间占了总开发时间的三成后面写代码几乎是一路顺风。另外还有个实用小技巧开发启动后先建一个演示数据脚本一键生成模拟的教室、设备、用户和各状态报修单这样边开发边能看到真实效果而不是对着空数据库发愁。等要答辩或给管理员演示的时候这份演示数据也能直接派上用场。
返回列表