资讯动态

基于Flask+Vue+MySQL的设备报修管理系统实战与部署详解

发布时间:2026/10/9 12:43:16 来源:尧图企业网站定制
电气企业的设备报修管理听起来是个老掉牙的流程电话吼一嗓子、纸质单子填一遍、维修师傅凭记忆满厂跑。我去年接手一家电气成套设备厂的内部系统改造把这一套全换成了基于 Flask Vue MySQL 的 ERP 设备报修管理系统前后端各归各位报修单子直接在手机和电脑上流转。今天把这套东西从需求拆解、数据库设计、接口实现、前端页面到部署上线和踩过的坑完整拉出来讲一遍给准备做企业后端管理系统、或者想用 Python 写业务系统的朋友做个参考。这套系统不是什么高并发火箭它解决的是最实际的三个问题报修信息不落地、维修过程不可控、维修数据没法统计。技术栈选型也很直接——Flask 轻量灵活写 CRUD 和业务接口效率高MySQL 稳定便宜企业内网部署成本低Vue 做管理后台交互流畅Element Plus 现成组件能省大量前端工作量。下面直接进入正文我会把项目整体思路、核心功能实现和踩过的坑一一拆开讲。1. 项目背景与需求拆解1.1 为什么电气企业需要一个独立的设备报修系统电气企业的设备和普通办公室设备不一样产线上一台绕线机坏了停一分钟就是损失。很多厂的报修流程还停留在“找设备科的人口头说一声什么时候修好靠催促”。这种做法有几个硬伤第一报修的人说不清楚设备型号和故障现象维修工现场看了才发现工具零件没带齐第二维修进度没人跟踪设备科整天接电话真正干活的师傅被催得团团转第三月底做设备故障率统计、备件消耗分析的时候发现一张纸质记录都没有全靠老员工拍脑袋。这套系统要做的就是把这些环节全部线上化员工扫码或登录系统提交报修单设备管理员审核后派单给对应维修工维修工接单、到场、填写处理结果使用部门确认后归档。每一步都有时间戳和操作人记录既规范了流程又为后续数据分析留下了原始数据。1.2 用户角色和核心流程我在项目启动前和车间主任、设备科长聊了好几次最后整理出四类核心角色权限划分很清楚普通员工报修人创建报修单、查看自己提交的工单进度、确认维修结果。设备管理员管理设备台账、审核报修单、派单给维修工、处理维修结果。维修工查看待接单、更新维修状态、填写维修记录和更换备件信息。系统管理员部门经理查看所有工单、统计报表、配置用户账号和基础数据。流程上肯定不能一提交就直接到维修工手里中间必须有人审核。因为很多报修单写的不清楚维修工接到单以后发现“故障描述坏了”根本没法准备工具。所以我的设计是报修人提交 - 设备管理员初审必要时退回补充信息 - 派单 - 维修工接单 - 到场维修 - 填写结果 - 报修人确认 - 工单归档。整个状态机相对简单但足够覆盖实际场景。1.3 技术选型为什么是 Flask 而不是 FastAPI现在一提 Python 后端很多人第一反应是 FastAPI性能确实好异步支持也强。但选 Flask 有现实考量团队里其他系统都是 Flask 写的维护成本最低这套 ERP 模块不追求高并发内网也就几十个人同时用Flask-SQLAlchemy 生态成熟配合 Flask-JWT-Extended、Flask-CORS 这些扩展开发速度很快。FastAPI 的学习曲线和文档风格更适合新项目从头长周期维护而对这个报修系统Flask 的“约定优于配置”能让项目在一周内跑通核心流程。前端选 Vue 3 Element Plus 也没什么悬念。管理后台就是一堆表单、表格、状态标签Element Plus 的表单校验和弹窗组件能直接满足需求。Vue 的响应式数据绑定让工单状态实时刷新很舒服组件化开发也让报修表单、工单卡片这些模块容易被复用。2. 系统架构与数据库设计2.1 前后端分离的整体架构这套系统采用前后端分离架构前端是 Vue 3 单页应用负责页面展示和交互后端是 Flask 提供 RESTful API负责业务逻辑和数据存取。前端通过 Axios 发送 JSON 请求后端返回统一格式的 JSON 数据。生产环境部署时Nginx 同时担任静态文件服务器和 API 反向代理解决跨域问题的同时还能做一层负载均衡。整体模块划分我就不贴那种复杂的架构图了脑子里过一遍就清楚前端Vue 3 Vite Element Plus ECharts Pinia Vue Router。后端Flask 2.3 Flask-SQLAlchemy Flask-Migrate Flask-JWT-Extended Flask-CORS。数据库MySQL 8.0使用 InnoDB 引擎utf8mb4 字符集。这样的好处是前端的路由守卫和后端的 JWT 校验双重控制权限前端只管交互后端只管数据和流程调试问题的时候不用在一个大项目里找半天。2.2 核心数据表设计数据库设计是整个系统里最值得花时间的部分。我建了六张核心表外加两张辅助表。列一下主要字段和设计思路用户表t_user字段类型说明idbigint主键自增usernamevarchar(50)登录名唯一索引password_hashvarchar(255)使用 Werkzeug 哈希不存明文real_namevarchar(50)姓名roletinyint1-员工 2-管理员 3-维修工 4-经理department_idint所属部门外键statustinyint1启用 0停用设备台账表t_device字段类型说明idbigint主键device_codevarchar(50)设备编号像“CSSB-2024-001”device_namevarchar(100)设备名称category_idint设备分类绕线机、测试台等locationvarchar(100)安装位置purchase_datedate购置日期statustinyint1-在用 2-停机 3-报废报修单表t_repair_order这是核心中的核心字段比较多列主要的id主键order_no工单编号格式“BX20250101001”device_id外键关联设备report_user_id报修人report_content故障描述必填这里可以做文本长度校验防止“坏了”这类无效描述report_time报修时间audit_status审核状态0待审核 1通过 2退回assign_user_id指派的维修工repair_status维修状态0待接单 1维修中 2待验收 3已完成 4已归档repair_result维修结果描述finish_time维修完成时间confirm_status用户确认状态0待确认 1已确认priority优先级1普通 2紧急维修记录表t_repair_record每一条工单可以有多次维修动作字段包括工单ID、操作人、操作说明、工时、备件消耗、操作时间。还有部门表、设备分类表这些辅助表不展开。所有表都加了 created_at 和 updated_at 时间字段方便排查和统计。2.3 状态机设计怎样避免流程乱套报修工单的整个生命周期用一个 status 字段是控制不住的所以我把状态拆成了审核状态、维修状态、确认状态三个维度分别用独立的字段掌控。这样做的原因是企业管理流程中审核和维修是并行推进的报修单可能刚提交、审核过了但维修还在进行中也可能维修完成了使用部门迟迟不确认。如果把所有状态塞进一个字段写代码的时候到处都是 if-else状态组合一多直接崩溃。所以三个维度各自独立流转审核状态0待审核 - 1通过 / 2退回。维修状态0待接单 - 1维修中 - 2待验收 - 3已完成 - 4已归档。确认状态0待确认 - 1已确认。后端接口每次更新工单时判断当前用户角色和操作类型校验允许的状态变迁。比如维修工不能把待验收的工单直接改成已完成必须先等报修人确认。虽然状态机不算复杂但明确之后前端按钮的显示和后端接口的权限判断都清晰了。3. 核心功能实现与后端接口开发3.1 后端项目结构与初始化Flask 项目我习惯按照业务模块组织目录而不是把所有路由堆在 app.py 里。这个项目的结构大概是repair_system/ ├── app/ │ ├── __init__.py # 应用工厂 │ ├── models.py # 所有模型定义 │ ├── api/ │ │ ├── auth.py # 登录、用户认证接口 │ │ ├── device.py # 设备管理接口 │ │ ├── order.py # 报修工单接口 │ │ ├── record.py # 维修记录接口 │ │ └── stats.py # 统计报表接口 │ ├── utils/ │ │ ├── decorators.py # 权限装饰器 │ │ └── response.py # 统一返回格式 ├── config.py # 配置文件 ├── run.py # 启动入口 └── requirements.txt应用工厂模式的好处是测试和生产环境可以灵活切换配置。config.py 里分开发、测试、生产三个配置类数据库链接、JWT 密钥、Token 过期时间都放配置文件里。数据库连接配置有个容易踩坑的点如果直接用默认的 utf8MySQL 里表情符号存不进去。虽然在报修系统里用表情符号的场景不多但防一手统一设置为 utf8mb4SQLALCHEMY_DATABASE_URI mysqlpymysql://root:passwordlocalhost:3306/repair_db?charsetutf8mb4还有 SQLAlchemy 的连接池配置默认 pool_size 是 5如果公司内部并发稍微上来就会出现连接等待超时。我一般会调大一点SQLALCHEMY_ENGINE_OPTIONS { pool_size: 10, pool_recycle: 3600, pool_pre_ping: True, }pool_pre_ping 这个参数特别重要MySQL 默认 wait_timeout 是 8 小时长时间不活跃的连接会被数据库主动断开如果不做 pre_ping 检查第二天上班第一波请求就会报 “MySQL server has gone away”。3.2 用户认证与权限控制实现认证这块我用的 JWT不是 Session。因为前后端分离后JWT 可以在请求头 Authorization 里带Nginx 不会拦截也方便将来做成移动端接口。JWT 流程不复杂登录时校验用户名密码通过后生成 token前端存到 localStorage 或 Pinia 里每次请求 Axios 拦截器自动加上 Header。具体实现依赖 Flask-JWT-Extended。登录接口核心代码大概长这样auth_bp.route(/login, methods[POST]) def login(): data request.get_json() username data.get(username, ).strip() password data.get(password, ).strip() user User.query.filter_by(usernameusername).first() if user is None or user.status ! 1: return error_response(账号不存在或已停用) if not check_password_hash(user.password_hash, password): return error_response(密码错误) access_token create_access_token( identitystr(user.id), additional_claims{role: user.role, real_name: user.real_name} ) return success_response({token: access_token, role: user.role, real_name: user.real_name})权限控制不是只在前端隐藏按钮后端必须也校验。我写了一个装饰器从 JWT 的 additional_claims 里取出角色判断是否允许访问当前接口def role_required(*allowed_roles): def wrapper(fn): wraps(fn) def decorated(*args, **kwargs): claims get_jwt() role claims.get(role) if role not in allowed_roles: return error_response(没有权限访问, 403) return fn(*args, **kwargs) return decorated return wrapper实际调用时比如设备管理接口只有管理员和经理能操作就在路由上直接加role_required(2, 4)。这个装饰器帮我省了很多重复的权限校验代码后面加新接口的时候直接复用。3.3 报修单接口的完整实现报修单是整个系统的业务核心接口不能只是简单的 CRUD还要处理状态流转、通知提醒、数据校验。我按业务场景拆了几个接口创建报修单用户提交报修表单时必填设备、故障描述、优先级。后端校验设备是否存在、描述长度是否不少于 10 个字然后生成工单编号、创建记录。生成编号我用的是 MySQL 里取系统时间加序列的方式但为了避免并发冲突直接用当前时间戳加随机四位字符order_no BX datetime.now().strftime(%Y%m%d%H%M%S) str(randint(1000, 9999))这样即使同一秒内有两张单子撞号概率也很低且工单号生成独立于数据库自增ID方便业务方从编号里读懂时间。创建报修单的核心代码如下order_bp.route(/create, methods[POST]) jwt_required() def create_order(): user_id get_jwt_identity() data request.get_json() device_id data.get(device_id) report_content data.get(report_content, ).strip() priority int(data.get(priority, 1)) if len(report_content) 10: return error_response(故障描述至少填写10个字) device Device.query.get(device_id) if device is None or device.status 3: return error_response(设备不存在或已报废) order RepairOrder( order_nogenerate_order_no(), device_iddevice_id, report_user_iduser_id, report_contentreport_content, prioritypriority, audit_status0, repair_status0, confirm_status0 ) db.session.add(order) db.session.commit() return success_response({order_id: order.id, order_no: order.order_no})这里有个细节设备状态如果是报废状态就不应该允许提交报修我在前端也做了同样的校验但后端必须兜底。所有校验逻辑以后端为准前端校验只是提高用户体验。派单接口设备管理员点击派单按钮选择维修工后端把维修工 ID 写入 assign_user_id同时把审计状态改为通过并把维修状态改为待接单。这个操作涉及到用户提交审核所以我的接口设计是“审核并派单”一步完成避免管理员先审核后再进入另一个派单页面。order_bp.route(/assign, methods[POST]) jwt_required() role_required(2, 4) def assign_order(): data request.get_json() order_id data.get(order_id) assign_user_id data.get(assign_user_id) order RepairOrder.query.get(order_id) if order is None: return error_response(工单不存在) assign_user User.query.get(assign_user_id) if assign_user is None or assign_user.role ! 3: return error_response(所选用户不是维修工) order.audit_status 1 order.assign_user_id assign_user_id order.repair_status 0 db.session.commit() return success_response(派单成功)如果审核不通过管理员可以退回需要填写退回原因前端弹窗收集退回意见后端存到 remark 字段报修人在详情页能看见。维修进度更新维修工在自己工单列表点击“开始维修”前端调用接口把 repair_status 从 0 改成 1维修完成后填写维修结果调用接口改成 2待验收。这两个操作本质都是更新状态我合并成同一个接口传参 status 和 resultorder_bp.route(/update_status, methods[POST]) jwt_required() role_required(3, 2, 4) def update_repair_status(): data request.get_json() order_id data.get(order_id) new_status int(data.get(repair_status)) result data.get(repair_result, ).strip() order RepairOrder.query.get(order_id) if order is None: return error_response(工单不存在) if get_jwt()[role] 3 and order.assign_user_id ! int(get_jwt_identity()): return error_response(只能操作派给自己工单) if new_status 1 and order.repair_status 0: order.repair_status 1 elif new_status 2 and order.repair_status 1: if not result: return error_response(维修结果不能为空) order.repair_status 2 order.repair_result result order.finish_time datetime.now() else: return error_response(非法的状态流转) db.session.commit() return success_response(更新成功)这里我特别加了判断维修工只能更新自己名下的工单防止越权。状态流转也不是随意乱跳必须从 0 到 1 再到 2不合法的跳转直接拒绝。确认验收与归档报修人看到维修状态变成 2待验收后可以点击“确认完成”后端将 confirm_status 改为 1repair_status 改为 3。到了这一步工单基本就闭环了不需要再自动归档管理员可以手动归档或系统每天晚上自动把超过 30 天的已完成工单置为归档状态。3.4 维修记录与备件消耗维修工在维修过程中可能会换轴承、换接触器这些备件信息不记录的话月底设备科根本不知道钱花在哪。我设计了维修记录子表维修工每次更新状态的时候也可以同时提交一条记录包含备件名称、数量、费用。这样报表里可以直接按备件维度汇总支出。接口设计成批量提交一个工单可以一次添加多条记录record_bp.route(/add, methods[POST]) jwt_required() def add_record(): data request.get_json() order_id data.get(order_id) records data.get(records, []) # 批量插入 for r in records: db.session.add(RepairRecord( order_idorder_id, operator_idget_jwt_identity(), part_namer.get(part_name), quantityr.get(quantity, 1), costr.get(cost, 0), descriptionr.get(description, ) )) db.session.commit() return success_response(保存成功)实际开发中前端是弹出一个表格允许添加多行提交时一次性 JSON 数组传给后端。这样比逐条 POST 效率高也符合维修工一次填完几个备件的习惯。3.5 列表查询接口分页、筛选与排序报修单列表是企业系统里最常被访问的页面接口必须支持状态筛选、时间范围、模糊搜索和分页。Flask-SQLAlchemy 的 query 配合 filter 和 order_by 很容易实现order_bp.route(/list, methods[GET]) jwt_required() def order_list(): page request.args.get(page, 1, typeint) per_page request.args.get(per_page, 10, typeint) status request.args.get(status, typeint) keyword request.args.get(keyword, ).strip() start_date request.args.get(start_date) end_date request.args.get(end_date) query RepairOrder.query if status is not None: query query.filter(RepairOrder.repair_status status) if keyword: query query.join(Device).filter(Device.device_name.like(f%{keyword}%)) if start_date: query query.filter(RepairOrder.report_time start_date) if end_date: query query.filter(RepairOrder.report_time end_date) pagination query.order_by(RepairOrder.report_time.desc()).paginate( pagepage, per_pageper_page, error_outFalse ) # 把查询结果转换为字典列表 items [order.to_dict() for order in pagination.items] return success_response({ list: items, total: pagination.total, page: page, per_page: per_page })有一个小细节Flask-SQLAlchemy 的paginate在 page 越界时默认报 404我设置了error_outFalse这样前端请求一个不存在的页码也能正常返回空列表不至于页面崩溃。order 的to_dict()方法是我在模型里定义的用来把 ORM 对象转成可 JSON 序列化的字典同时把外键关联的设备名称、报修人姓名等冗余信息一起带出来避免前端再逐个查询。这种“一次性查全”的做法对管理后台的体验提升很明显。4. 前端页面设计与 Vue 实现4.1 前端工程结构和路由设计前端采用 Vite 创建 Vue 3 工程用 Pinia 管理用户状态用 Vue Router 控制页面跳转。目录结构大概这样src/ ├── api/ # axios 封装和接口定义 │ ├── request.js # axios 实例 │ └── order.js # 报修单相关接口 ├── router/ # 路由配置 ├── stores/ # Pinia 状态 ├── views/ │ ├── Login.vue │ ├── Dashboard.vue # 仪表盘 │ ├── DeviceList.vue │ ├── OrderCreate.vue │ ├── OrderList.vue │ └── OrderDetail.vue └── components/ ├── DeviceSelect.vue └── StatusTag.vue路由守卫很关键所有页面除了登录页都需要 JWT Token 才能访问。在 Router 的beforeEach里统一判断router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (!token to.path ! /login) { next(/login) } else { next() } })没有权限的用户即使手动改 URL 跳到其它页面也会被弹回登录页。后端接口再校验一遍双保险。4.2 Axios 封装与统一错误处理所有接口调用都走同一个 axios 实例好处是拦截器统一处理 Token 过期、报错提示这些琐碎的事。我的 request.js 核心逻辑import axios from axios import { ElMessage } from element-plus import router from /router const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer ${token} } return config }) service.interceptors.response.use( response { const res response.data if (res.code ! 0) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res.data }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } else { ElMessage.error(error.message) } return Promise.reject(error) } )注意一个细节我自己定义的响应格式是{code: 0, data: ..., message: ok}code 为 0 表示成功。axios 拦截器里先把数据解包了页面里调用接口就直接拿到 data 部分少一层.data.data的嵌套代码看着舒服很多。4.3 报修表单页面报修表单页面是员工用得最多的页面一定要简洁字段不能多。设备用下拉选择器故障描述用 textarea至少要 10 个字加一个字数校验。如果企业有多个厂区设备数量上千台直接下拉选会很难找我在 DeviceSelect 组件里做了一个远程搜索输入关键字实时从后端接口拉设备列表template el-select v-modeldeviceId filterable remote :remote-methodsearchDevice placeholder请输入名称或编号搜索设备 changehandleDeviceChange el-option v-foritem in devices :keyitem.id :labelitem.device_code - item.device_name :valueitem.id / /el-select /template远程搜索的接口后端写成device_bp.route(/search, methods[GET]) jwt_required() def search_device(): keyword request.args.get(keyword, ).strip() devices Device.query.filter( or_(Device.device_name.like(f%{keyword}%), Device.device_code.like(f%{keyword}%)) ).limit(20).all() return success_response([d.to_dict() for d in devices])limit 20 防止输入一个“1”把所有设备全查出来前端搜索体验很关键。4.4 工单列表与状态标签工单列表页用 Element Plus 的 el-table 展示列包括工单号、设备名称、报修人、状态、优先级、报修时间、操作。状态显示我单独做了一个 StatusTag 组件根据维修状态显示不同颜色的标签template el-tag :typetypeMap[status]{{ textMap[status] }}/el-tag /template script setup const props defineProps({ status: Number }) const typeMap [info, primary, warning, success, info] const textMap [待接单, 维修中, 待验收, 已完成, 已归档] /script列表的筛选区域放在表格上方按状态选择、按时间范围选择、按关键词搜索。方便设备管理员快速查看待派单的工单。4.5 仪表盘统计页统计页面用 ECharts 画两张图一张是近 30 天工单数量趋势折线图一张是设备故障类型占比饼图再加上几个关键 KPI 卡片待处理工单数、本周完成数、平均维修时长。数据全部从后端统计接口取不会在页面里自己聚合运算。后端统计接口的关键 SQL 逻辑大概这样用 ORM 写法trend db.session.query( func.date_format(RepairOrder.report_time, %Y-%m-%d).label(day), func.count(RepairOrder.id) ).filter( RepairOrder.report_time datetime.now() - timedelta(days30) ).group_by(day).all()因为 MySQL 的 group by 在 ONLY_FULL_GROUP_BY 模式下要求 select 的字段必须出现在 group by 里或使用聚合函数我故意在 label 里给日期取了别名再用别名分组这样即使只按日期分组不报错。实际开发中MySQL 版本不同SQL 模式有差异ORM 生成 SQL 之前可以先在数据库客户端里跑一遍确认。5. 部署上线与实战调优5.1 开发环境搭建先从零开始配一遍环境。MySQL 安装大家比我熟Windows 上装 8.0 就是把安装包下载下来注意选择 Server only字符集选 utf8mb4。装完后在命令行初始化密码然后创建数据库和用户CREATE DATABASE repair_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER repairlocalhost IDENTIFIED BY Repair123; GRANT ALL PRIVILEGES ON repair_db.* TO repairlocalhost; FLUSH PRIVILEGES;后端用 venv 创建虚拟环境requirements.txt 里把 Flask、PyMySQL、Flask-SQLAlchemy、Flask-JWT-Extended、Flask-CORS、python-dotenv 列好连上数据库后直接跑初始化脚本建表python -m venv venv source venv/bin/activate # Linux pip install -r requirements.txt python run.py前端用 Vite 开发服务器需要在 vite.config.js 里配置代理把 /api 开头的请求转发到 Flask 的 5000 端口这样开发时也不会有跨域问题server: { proxy: { /api: { target: http://localhost:5000, changeOrigin: true } } }注意Flask 里的接口路由我统一加了前缀/api这样前端代理和后端蓝图的 prefix 正好能对上。5.2 生产环境部署waitress 还是 gunicornLinux 服务器上首选 gunicorn多 worker 模式能利用多核 CPU。启动命令gunicorn -w 4 -b 127.0.0.1:5000 run:app-w 4 表示启动 4 个 worker 进程内部请求量不大4 足够了。如果项目运行在 Windows Server 上gunicorn 不能直接用可以换 waitress命令类似。设置成 systemd 服务保证开机自启、崩溃自动重启这一套就别省了。Nginx 的配置核心是反向代理和静态文件服务server { listen 80; server_name repair.example.com; location / { root /opt/repair_system/dist; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:5000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }Vue 是单页应用前端路由是 history 模式直接刷新某个子路径会出现 404所以必须加try_files $uri $uri/ /index.html;。这个坑很多人第一次部署都遇到过提一嘴。5.3 MySQL 配置调优报修系统数据量不算大但并发查询还是需要一把好的索引。核心表的索引设计报修单表(repair_status)单列索引按状态筛选工单列表。报修单表(device_id)外键索引关联查询设备时加速。报修单表(report_time)索引按时间范围查询。工单编号唯一索引保证生成编号不重复。MySQL 的 my.cnf 里调整 innodb_buffer_pool_size如果内存有 8G可以设为 2G减少磁盘 IO。这不是什么高深调优但很多人默认配置都不改导致后期查询慢才来找原因。5.4 接口性能优化经验列表接口如果毫秒级响应就不用瞎优化但有一个地方容易慢设备名称和报修人姓名的冗余查询。如果每条工单都用 ORM 去索引关联表N1 查询问题就出现了。我在模型to_dict()里直接用属性访问关联对象听起来没问题但列表查询时 SQLAlchemy 默认懒加载遍历 100 条工单就会额外执行 100 次设备查询和 100 次用户查询。解决方法是查询的时候用joinedload或contains_eager预先加载from sqlalchemy.orm import joinedload query RepairOrder.query.options( joinedload(RepairOrder.device), joinedload(RepairOrder.report_user) )这样一次 JOIN 就把关联对象带到内存里to_dict()访问属性的时候不再发出新的 SQL。这算是 SQLAlchemy 优化里最立竿见影的一条了强烈推荐。6. 常见问题与避坑实录6.1 前端跨域问题开发环境下用 Vite 代理很省事但如果有人直接把前端 dev server 和后端 Flask 端口分别打开访问浏览器就会报跨域错误。解决办法有两个前端代理或者后端加 Flask-CORS。我两个都做了开发时用代理生产 Nginx 处理Flask-CORS 只作为兜底。建议不要把 CORS 配置成*允许所有域名内网系统也尽量限定允许来源。6.2 JWT Token 过期后的用户体验Token 过期后用户在某个页面点按钮接口返回 401axios 拦截器直接清掉本地 token 并跳到登录页。这个逻辑是没问题但用户体验不太好用户正在填一个很长的报修单突然跳走了表单内容全丢。我的做法是在拦截器里对 401 先判断当前路由如果是表单页先用 Element Plus 的 ElMessageBox 弹窗提示“登录已过期请重新登录”然后再跳转。同时报修表单组件在 mounted 时把草稿存到 localStorage用户重新登录后可以一键恢复上一次未提交的内容。这个小功能比较讨巧实际使用中师傅们很买账。6.3 时间字段的时区问题Flask 在读写 MySQL 的 DATETIME 字段时默认用的是服务器本地时间。如果服务器时区设成 UTC而业务方在中国就会出现所有工单时间比实际早 8 小时。我统一在数据库连接 URL 里加了init_command或者直接在 MySQL 里设置时区SET time_zone 08:00;同时Python 端在模型定义时间字段时直接用datetime.datetime.now()不要用datetime.utcnow()。前者取的是系统本地时间后者是 UTC 时间。这个坑一踩就是所有列表的时间全乱排查半天才发现是时区问题。6.4 MySQL 8.0 认证插件连接失败PyMySQL 连接 MySQL 8.0 时如果报Authentication plugin caching_sha2_password cannot be loaded是因为 MySQL 8 默认使用了新的认证插件PyMySQL 新版本是支持的。遇到这个报错先执行ALTER USER repairlocalhost IDENTIFIED WITH mysql_native_password BY Repair123;或者直接升级 PyMySQL 到最新版本。开发环境建议用 Docker 装 MySQL 8.0.44注意挂载数据卷否则容器删了数据全丢。6.5 Vue 项目构建后 index.html 404 或刷新 404这个问题在 Nginx 部署那节已经提到了核心就是try_files。还有一个相关问题是打包后的静态资源路径如果部署在子路径下比如/erp/Vite 构建时要设置base: /erp/否则资源 404。我们内网部署一般就根路径问题不大。6.6 报修单状态不同步导致按钮错乱前端页面按钮是根据 repair_status 渲染的如果运维手改数据库或者后端状态更新用了不同的字段前端就会显示错乱。所以在项目里我用三个字段并行控制状态后端接口只允许状态机规定的跳转不合法的操作直接返回错误。这样即使有人手动改数据库前端也能通过后端接口的校验保护流转逻辑不被彻底破坏。7. 项目扩展与个人心得这套系统上线之后实际使用反馈还是很正面的。设备科的人终于不用整天接电话维修工也不再靠微信群里翻记录。我最深的体会是做企业内部系统技术只是基础关键是要把业务流程吃透跟车间的人聊明白他们到底需要什么。一开始我想把报修单设计成很“规范”的模式损坏照片、音频备注、严重等级评分结果老员工根本不买账嫌填起来麻烦。后来我改成除了故障描述必填其他字段全部可选表单一屏能填完上报率立刻上来了。如果再往外扩展这个系统可以加的东西很多设备二维码张贴扫码直达报修表单微信公众号或企业微信模板消息工单状态变更自动推送设备保养计划定期生成巡检任务维修备件库存管理和采购系统对接自动补货。底层数据库结构已经预留了扩展空间加模块不需要推倒重来。最后分享一个开发技巧我习惯把所有表的 model 类单独放一个 models.py 文件但不要在这里面写太多业务逻辑只保留to_dict()这样的序列化方法。业务判断全部放接口层这样后面加测试也好写多个视图函数之间也不容易互相影响。你如果现在正准备做个管理后台可以从这套结构开始起步先把用户、工单这两张核心表吃透再慢慢丰富模块比一开始就追求大而全要稳得多。

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

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

免费获取报价 →
↑