news 2026/10/17 4:55:35

医疗设备维修管理系统实战:Django+Vue3全栈开发

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
医疗设备维修管理系统实战:Django+Vue3全栈开发

设备科老师傅最怕的不是设备坏,而是坏完之后只剩一张手写单。一台进口CT机从报修到恢复运行,中间谁接的单、谁去修的、用了什么备件、花了多少钱,这些信息要是全靠人工追,维修效率根本提不上来。所以我做了这套医院医疗设备维修管理系统,技术栈很直接:后端 python-django 提供接口,前端 vue3 负责交互界面。它解决的也就是三件事:把设备台账管起来,把维修工单流转起来,把维修成本和备件数据沉淀下来。这篇文章我会把从架构选型到数据库设计、从后端接口到前端页面、再到部署避坑的完整过程都拆开讲,适合正在做类似管理系统的开发者参考,也适合医院信息科的朋友了解这套东西内部到底是怎么运转的。

我不是第一次做管理系统,但医疗设备维修这个场景比普通的后台管理要特殊不少。设备种类杂、价值高、维修链条长,且涉及的角色至少有报修人、维修工程师、设备科管理员这几类。如果按普通 CRUD 的思路做,最后一定会被真实业务需求打脸。后面我会详细说说为什么选 Django 和 Vue 3 这套组合,以及建模的时候到底哪些细节最容易被忽略。

1. 项目整体设计与技术选型逻辑

1.1 医院设备维修场景的特殊性

医疗设备管理系统不能简单理解成“给设备建个档案再记维修记录”。真实场景里,一台设备可能同时涉及三个状态维度:设备自身状态(在用、停机、报废)、维修工单状态(待派单、维修中、已完成)、保养计划状态(正常、即将到期、已逾期)。这三个维度的数据交织在一起,才是设备科真正关心的“这台机器到底能不能用”。

另一个特点是角色权限差异极大。临床科室的人只关心“报修了没、修好了没”,维修工程师关心的是“派给我的单子有哪些、备件够不够”,设备科管理员则要看到全院设备台账、维修费用统计、供应商响应时间这些宏观数据。所以系统从一开始就不能只做一个“管理员总览”的页面,而是要把功能拆到角色维度上。

还有一个很容易被忽略的点:医疗设备涉及大量附件,比如采购合同扫描件、维修验收单、工程师上门处理照片。这些文件看起来不起眼,但真到了设备报废审计或者效益分析的时候,缺一张纸都能让你翻档案翻到怀疑人生。所以文件上传和附件关联功能从一开始就得设计进去,而不是后面打补丁。

好工具、坏工具的差别往往不在功能多少,而在流程是否闭环。报修、派单、接单、维修、验收、回访、数据统计,这一条链上任何一个环节断掉,系统就变成了一个“电子台账”,失去了管理意义。所以我的整体设计思路是:先把工单状态机定死,再做外围功能。状态机是这套系统的脊柱,脊柱歪了,后面全白搭。

1.2 为什么后端选 Django,前端选 Vue 3

后端框架里可选的不算少,我最终选 Django,主要看中这几点:

Django 自带 Admin、ORM、认证、序列化等一整套设施,对于“管理系统”这类以数据增删改查为核心的业务,开发效率确实高。尤其是 ORM,配合 model 定义就能直接生成数据库表结构,省掉了大量手写 SQL 的时间。另一个关键点是 Django 自带的后台管理界面,在开发阶段可以直接用它来维护测试数据、管理用户权限,项目还没铺前端之前,设备科的老师就已经能通过 Admin 录设备数据了,这对前期需求确认帮助特别大。

有人会担心 Django 太重、性能不如某些异步框架。但医院设备维修管理系统的并发量其实很有限,核心是几百号人的内部使用场景,QPS 压力不大。真正重要的是开发效率和可维护性,Django 在这一类内部管理项目里是相当稳妥的选择。

前端选 Vue 3,除了生态成熟之外,还有一个很实际的原因:Vue 的单文件组件开发方式对于这种表单密集、表格密集的管理系统非常友好。加上组合式 API 之后,逻辑复用比之前方便很多。配合 Element Plus 这类组件库,设备台账表格、工单表单、数据统计卡片这些界面都能快速搭出来。

前后端分离是我这次坚持的架构。Django 只提供 JSON 接口,Vue 3 单独跑前端服务,开发时用 Vite 代理解决跨域,部署时用 Nginx 统一反向代理。这条路线的好处是分工清晰,接口文档一出来,前端后端可以并行开发。

1.3 整体架构怎么串起来

这套系统在逻辑上分四层:

  • 数据层:PostgreSQL(生产环境)或者 SQLite(开发环境),通过 Django ORM 访问。
  • 接口层:Django REST Framework 提供标准 RESTful API,用 JWT 做身份认证。
  • 前端层:Vue 3 + Vite + Pinia + Vue Router 单页应用。
  • 部署层:Gunicorn 运行 Django 服务,Nginx 托管前端静态文件并反向代理后端接口。

从客户端到服务端的请求链路大概是:浏览器访问 Nginx 托管的 Vue 页面,页面内通过 axios 发起请求到 /api 路径,Nginx 把 /api 前缀的请求转发给 Gunicorn 启动的 Django 应用,Django 处理完业务逻辑后返回 JSON 数据,前端拿到数据后渲染页面。

这个链路里有几个细节值得注意。第一是 JWT 的 token 存储,我放在 localStorage 里,每次请求通过拦截器自动加在 Authorization 头里。第二是文件上传,直接走 Django 的 media 路径,Nginx 再单独配置一个 /media 的静态路径映射,避免让后端容器扛静态文件压力。第三是跨域,开发环境靠 Vite proxy 解决,生产环境因为 Nginx 统一入口,跨域问题基本不存在。

2. 功能模块拆解与数据库建模

2.1 功能模块到底有哪些

我把系统功能拆成了六大模块:设备台账管理、维修工单管理、备件库存管理、保养计划管理、统计报表、系统管理。这六个模块不是并列关系,而是围绕“设备全生命周期”串起来的。

设备台账是底座,一台设备从采购入库开始就有了唯一编号、名称、型号、所属科室、安装位置、启用日期、质保期这些信息。维修工单挂在设备下面,每一张工单记录一次故障报修和维修过程。备件库存则跟工单关联,工程师在维修时可以登记用了哪些备件,扣减库存的同时也把备件成本记录到了这台设备头上。保养计划是另外一条线,按设备类型和保养周期生成到期提醒。

统计报表从三个角度切入:全院各科室的设备维修费用排行、设备的故障率排行、维修工程师的工作量统计。管理员可以在一个页面上看到本月新增报修多少单、已完成多少单、平均响应时间有多长,这些数据能直接用来做供应商考核和设备更新评估。

2.2 核心数据模型设计

数据模型是整个系统最需要花心思的地方。我先把几个核心模型列出来,再说为什么这样设计。

设备表:

class Equipment(models.Model): code = models.CharField(max_length=64, unique=True, verbose_name="设备编号") name = models.CharField(max_length=128, verbose_name="设备名称") category = models.ForeignKey( "EquipmentCategory", on_delete=models.PROTECT, verbose_name="设备分类" ) department = models.ForeignKey( "Department", on_delete=models.PROTECT, verbose_name="所属科室" ) location = models.CharField(max_length=128, verbose_name="安装位置") status = models.CharField( max_length=16, choices=EquipmentStatus.choices, default=EquipmentStatus.NORMAL, verbose_name="设备状态", ) purchase_date = models.DateField(null=True, blank=True, verbose_name="启用日期") warranty_expire = models.DateField(null=True, blank=True, verbose_name="质保到期日") manufacturer = models.CharField(max_length=128, blank=True, verbose_name="生产厂家") created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = "equipment" verbose_name = "设备档案"

这里我用on_delete=models.PROTECT而不是 CASCADE,原因很简单:设备档案属于基础数据,不能被维修工单或者保养记录连带删除。手一抖删错一台设备,历史数据全没了,这是运维事故。

维修工单表:

class RepairOrder(models.Model): order_no = models.CharField(max_length=32, unique=True, verbose_name="工单编号") equipment = models.ForeignKey( Equipment, on_delete=models.PROTECT, related_name="repair_orders", verbose_name="设备" ) reporter = models.ForeignKey( User, on_delete=models.PROTECT, related_name="reported_orders", verbose_name="报修人" ) assignee = models.ForeignKey( User, on_delete=models.PROTECT, null=True, blank=True, related_name="assigned_orders", verbose_name="维修工程师" ) defect_desc = models.TextField(verbose_name="故障描述") status = models.CharField( max_length=16, choices=RepairStatus.choices, default=RepairStatus.PENDING, verbose_name="工单状态" ) priority = models.CharField( max_length=8, choices=Priority.choices, default=Priority.MEDIUM, verbose_name="优先级" ) report_time = models.DateTimeField(auto_now_add=True, verbose_name="报修时间") accept_time = models.DateTimeField(null=True, blank=True, verbose_name="接单时间") complete_time = models.DateTimeField(null=True, blank=True, verbose_name="完成时间") repair_result = models.TextField(blank=True, verbose_name="维修结果") cost = models.DecimalField(max_digits=12, decimal_places=2, default=0, verbose_name="维修费用") class Meta: db_table = "repair_order" verbose_name = "维修工单" ordering = ["-report_time"]

工单编号我用了自定义生成逻辑,格式是WO + 年月日 + 当日序号,例如WO20250416001。这个编号虽然可以用数据库自增 ID 代替,但纸面流转和口头沟通时,可读性强的编号要比纯数字 ID 好用太多。当时我就是用 Redis 的 incr 计数,每天零点重置,避免在高频报修时出现并发重复号。

备件领用记录表:

class SparePartUsage(models.Model): order = models.ForeignKey(RepairOrder, on_delete=models.CASCADE, related_name="spare_usage") spare_part = models.ForeignKey(SparePart, on_delete=models.PROTECT) quantity = models.PositiveIntegerField(verbose_name="领用数量") unit_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="当时单价") created_at = models.DateTimeField(auto_now_add=True)

这里最关键的字段是unit_price,它不是从备件表里读取当前价格,而是把维修当时的单价快照存储下来。为什么?因为备件采购价格会变,如果只关联备件 ID,等月底统计维修成本的时候,价格已经被后来的采购单覆盖了。这种“历史快照”的思想在很多财务场景里都适用,代价是多存一个字段,收益是历史数据永远可还原。

2.3 权限设计:三类角色的数据边界

我在前面提到权限要分角色设计,这里具体展开。系统里我建了三个角色分组:报修人、维修工程师、设备科管理员。

报修人只有创建工单和查看自己发起的工单的权限,看不到别人的维修费用、备件领用等敏感信息。维修工程师可以看到派给自己的工单,也能查看全院设备台账,但不能修改设备档案。管理员拥有全部权限,包括设备档案维护、工单指派、备件入库、数据统计导出。

在 Django 里我用Group和Permission配合完成这个设计。每个 ViewSet 根据用户的 group 来动态决定 queryset 范围。例如维修工单列表,如果用户是普通工程师,只返回assignee=request.user的工单;如果是管理员,返回全部。这种数据级的权限控制,不能只靠前端菜单隐藏,否则接口被人猜到就能越权访问,安全隐患非常大。

2.4 工单状态机:最容易做烂的地方

维修工单的状态流转是我在设计阶段花时间最多的地方。最开始我图省事,直接用一个status字段管理,前端按钮自己想怎么跳就怎么跳。结果测试的时候发现,工单可以从“已完成”直接跳回“待派单”,也能从“待接单”跳过工程师直接变成“已完成”。这种混乱在演示时直接把设备科的老师看愣了。

后来我把状态流转收敛到一个独立的方法里,所有状态变更必须走这个方法校验合法性:

class RepairOrder(models.Model): # ...字段定义... def transition_to(self, new_status, user=None): allowed = self.ALLOWED_TRANSITIONS.get(self.status) if allowed and new_status in allowed: self.status = new_status else: raise ValueError(f"不能从 {self.status} 流转到 {new_status}")
ALLOWED_TRANSITIONS 的定义就像一张地图,写清楚每个状态能到哪个状态: 待派单 -> 待接单、已取消 待接单 -> 维修中、已转派 维修中 -> 待验收、已暂停 待验收 -> 已完成、维修中 已完成 -> 已关闭 已取消 -> 已关闭 这个状态机的好处是,无论前端怎么乱点,后端都会拦住非法的状态变化,同时给前端提供明确的错误提示。后来我还加了一个操作日志模型,每次状态变更都记录操作人、旧状态、新状态和操作时间。这样万一出了纠纷,比如临床科室说“我们没收到维修完成的通知”,直接拉日志出来看是谁在什么时间点了完成按钮,责任清清楚楚。 ## 3. 后端实现:Django 接口与业务逻辑 ### 3.1 项目初始化与依赖清单 项目初始化我是按 Django 标准流程做的。虚拟环境建好之后,装了这几个核心依赖: ```text django>=4.2 djangorestframework django-cors-headers djangorestframework-simplejwt django-filter Pillow openpyxl

其中django-filter用于设备列表的筛选,比如按设备状态筛选、按科室筛选,不用自己手写一堆 if 判断;Pillow用于处理上传的设备照片缩略图;openpyxl用于导出 Excel 报表。JWT 认证用的是simplejwt,配置上之后的路由注册也比较简单。

这里要提醒一个 Django 版本选择的坑。如果项目要部署在比较老的服务器上,Python 版本可能比较低,Django 4.2 LTS 需要 Python 3.8 以上,Django 5.0 则需要 3.10 以上。我当时在部署环境里就被 Python 版本卡过一次,老老实实换到 3.10 才解决。所以项目初始化之前先确认服务器上的 Python 版本,比什么都重要。

3.2 序列化器的设计:嵌套字段一次性给全

DRF 的序列化器是接口的核心。我习惯在列表接口里直接返回关联对象的名称字段,避免前端为了显示一个科室名称又单独发一次请求。比如设备列表序列化器:

class EquipmentSerializer(serializers.ModelSerializer): category_name = serializers.CharField(source="category.name", read_only=True) department_name = serializers.CharField(source="department.name", read_only=True) class Meta: model = Equipment fields = [ "id", "code", "name", "category", "category_name", "department", "department_name", "location", "status", "manufacturer", "warranty_expire", ]

这里的source参数能直接拿到关联对象的属性,序列化输出里就会多出category_name和department_name。前端表格组件里直接绑定这两个字段渲染,不需要再做数据转换。

对于维修工单列表,我也用同样的思路,把设备名称、报修人姓名、工程师姓名全部带上:

class RepairOrderSerializer(serializers.ModelSerializer): equipment_name = serializers.CharField(source="equipment.name", read_only=True) reporter_name = serializers.CharField(source="reporter.username", read_only=True) assignee_name = serializers.CharField(source="assignee.username", read_only=True, default=None) class Meta: model = RepairOrder fields = [ "id", "order_no", "equipment", "equipment_name", "reporter", "reporter_name", "assignee", "assignee_name", "defect_desc", "status", "priority", "report_time", "accept_time", "complete_time", "repair_result", "cost", ]

嵌套信息一次给全的代价是查询性能。所以对应的 ViewSet 里一定要提前用select_related把外键对象加载到内存,否则每返回一条工单,ORM 就要发三次 SQL 去查设备、报修人、工程师,列表一长,数据库直接被 N+1 查询打爆。

3.3 视图与权限控制实战

我用了 DRF 的ModelViewSet配合router管理接口,代码量最少,且自动生成标准 CRUD 接口。但数据权限的过滤需要在get_queryset里处理:

class RepairOrderViewSet(viewsets.ModelViewSet): serializer_class = RepairOrderSerializer permission_classes = [IsAuthenticated] def get_queryset(self): qs = ( RepairOrder.objects .select_related("equipment", "reporter", "assignee") .all() ) if self.request.user.groups.filter(name="维修工程师").exists(): qs = qs.filter(assignee=self.request.user) return qs.order_by("-report_time")

这样写之后,普通工程师访问工单列表,只能看到assignee是自己的工单。管理员不在这个条件里,所以看全部数据。

另外一个细节是创建工单时order_no的生成逻辑,不能放在序列化器里直接让前端传过来,不然伪造数据太容易了。我在 ViewSet 的perform_create里生成:

def perform_create(self, serializer): serializer.save(reporter=self.request.user, order_no=generate_order_no())

这样后面做权限审计,报修人是谁永远以服务端数据为准,前端传的reporter字段直接被忽略掉。这个做法也推荐给做类似系统的朋友,凡是创建数据时涉及当前登录用户的字段,一律在perform_create里赋值,不要在序列化器里接受用户传入。

3.4 文件上传与数据导出

文件上传这类的需求看起来不难,但坑不少。首先是存储路径,我在settings.py里配置了:

MEDIA_ROOT = os.path.join(BASE_DIR, "media") MEDIA_URL = "/media/"

然后为维修工单加一个附件模型,每个附件关联到工单 ID。上传接口用 Django 自带的FileSystemStorage,限制文件类型和后缀。医疗场景经常出现拍照上传,所以我把图片格式做了压缩处理,避免设备科拍一张 10MB 的照片把服务器磁盘塞满。

数据导出我用的openpyxl。最开始导出的 Excel 文件中文列名都正常,但设备科反馈在 WPS 里打开没问题,Excel 打开居然乱码。排查了半天,发现是 CSV 导出时编码用了 UTF-8,而 Excel 默认按 GBK 解析老版本 CSV。后来我直接改走openpyxl生成.xlsx,一下绕开了所有编码问题。所以在中文环境下做导出功能,建议直接用 Excel 格式,别折腾 CSV。

4. 前端实现:Vue 3 页面与交互细节

4.1 Vue 3 项目搭建与工程结构

前端我用 Vite 脚手架创建 Vue 3 项目,选了组合式 API 的写法。工程目录大致是:

src/ api/ # 接口请求封装 assets/ # 静态资源 components/ # 通用组件 router/ # 路由配置 stores/ # Pinia 状态管理 views/ # 页面组件 equipment/ # 设备台账 repair/ # 维修工单 spare/ # 备件管理 dashboard/ # 统计报表

技术选型上,状态管理用了 Pinia,路由用了 Vue Router,UI 组件库选了 Element Plus。Element Plus 在表格、表单、弹窗这三大件上的完成度很高,管理系统开发基本绕不开它。

页面结构上我是这么安排的:左侧是菜单栏,右侧是内容区。菜单项根据用户角色动态生成,管理员能看到全部菜单,工程师只能看到工单处理和自己相关的页面。这个动态菜单除了前端路由守卫控制之外,后端接口也做了权限校验,双重保险才稳妥。

4.2 axios 封装与 Token 处理

axios 封装看起来简单,但要注意的细节不少。我在request.js里做了请求拦截器和响应拦截器:

import axios from 'axios' import { ElMessage } from 'element-plus' import router from '@/router' const request = axios.create({ baseURL: '/api', timeout: 15000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('access_token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) request.interceptors.response.use( response => response.data, error => { if (error.response?.status === 401) { localStorage.removeItem('access_token') router.push('/login') } const detail = error.response?.data?.detail || '网络异常,请稍后重试' ElMessage.error(detail) return Promise.reject(error) } )

这里最关键的设计是把response.data直接返回。这意味着调用方拿到的永远是后端返回的业务数据,不需要每次在组件里多写一层.data.data。这个约定一旦确立,整个团队写接口调用的代码风格就非常统一。

还有一个处理 401 的细节。如果 token 过期,所有并发请求都会分别弹一次“网络异常”,体验比较糟糕。我的处理方式是在响应拦截器里加一个标记判断,如果已经弹过跳转登录框,就不要再重复弹。这个小改动虽然不起眼,但实际使用中的感知差异非常大。

4.3 核心页面:设备台账与工单流转

设备台账页是典型的主从表结构。主表是设备列表,每一行有设备编号、名称、所属科室、状态等字段。设备状态我用了标签组件展示,绿色代表正常,红色代表停机维修中,黄色代表待保养,一目了然。

点击某台设备的“详情”按钮,会弹出抽屉,里面显示设备基本信息、最近的维修记录、保养计划和附件列表。维修记录直接调用工单列表接口,按equipment_id过滤,时间倒序排列。这里有个技巧,抽屉打开时统一请求一次,不要把“详情”和“维修记录”拆成两个接口依次调用,否则会有明显的加载闪烁。

工单流转页是系统里交互最复杂的页面。我把它做成了三个 Tab:待处理、处理中、已完成。每个 Tab 里显示对应的工单卡片,卡片上有设备名称、报修时间、优先级、故障描述摘要。工程师在“待处理”Tab 里点击“接单”按钮,工单状态就变成维修中;维修完成后点击“完成”,弹出对话框让填写维修结果和实际费用,同时可以勾选本次用到的备件。

前端这部分的按钮状态必须跟状态机严格对齐。我直接把状态机的映射表也放在了前端代码里,根据当前工单状态动态渲染可用的操作按钮,用户永远看不到不合法的操作入口。这样既保证了交互友好,也不会因为状态错乱给后端接口制造麻烦。

4.4 数据可视化与报表页

统计报表页我一开始想上复杂图表库,后来评估了一下,用轻量方案就够。图表用的是 ECharts,通过 npm 引入。主要做三个图:按科室维修费用排行(柱状图)、设备月故障次数趋势(折线图)、工单状态分布(饼图)。

页面首屏加载时,并行发起三个统计接口,数据回来后填充图表配置。这里我踩过一个坑:Vue 3 里 ECharts 实例的销毁时机不对,导致页面切换后图表残留。后来我在组件onUnmounted钩子里手动调用chart.dispose(),问题就解决了。凡是用了外部类库操作 DOM 的场景,一定要记得在组件卸载时清理实例。

统计接口的数据精度也很重要。设备科要看的“维修费用”不能只算工程师填的维修费,还要加上备件费用。所以后端统计时我把RepairOrder.cost和关联的SparePartUsage费用汇总加在一起。这个口径要在后端确定好,前端不要自己拼数字,否则统计结果很容易对不上。

5. 常见问题与排查技巧实录

现象原因解决方案
前端请求报 CORS 错误开发环境前端端口和后端端口不同安装 django-cors-headers,配置 CORS_ALLOWED_ORIGINS,或直接用 Vite proxy
设备列表接口加载很慢没有使用 select_related在有外键的查询集上提前加载关联对象
上传图片后页面无法访问media 路由未在 Nginx 配置Nginx 里加 location /media/ 映射到 media 目录
工单列表出现重复数据分页参数或排序字段不稳定在 queryset 上固定 order_by("-report_time"),避免默认无序排序
token 过期后一直循环跳登录响应拦截器多个请求同时 401加标志位控制跳转逻辑,只处理一次
导出 Excel 日期显示为数字未设置单元格格式Satı列用 openpyxl 的 number_format 或转成字符串
后台管理中文显示乱码数据库编码不是 UTF-8创建数据库时指定 utf8mb4 字符集

在这张表之外,我再分享两个印象比较深刻的排查案例。

第一个是分页重复数据。测试时发现第二页第一条设备会重复出现在第一页,反复检查都找不到原因。最后发现是 queryset 没有指定排序,数据库返回顺序不稳定。这个问题的本质是:数据库表没有聚簇索引时,默认扫描顺序可能变化。修复方法就一行,给 queryset 加上order_by("-id"),问题立刻消失。做管理系统凡是涉及分页的,一定要给排序字段加唯一性兜底。

第二个是 JWT 认证配置遗漏。我一开始只配置了SIMPLE_JWT的密钥和过期时间,但忘了把django-cors-headers的中间件放在MIDDLEWARE列表里SessionMiddleware前面。结果前端带上了 Authorization 头之后请求依然 401,排查了很久才发现 cors 中间件对预检请求的处理有问题。这类中间件顺序问题,看官方文档最快,自己瞎猜往往浪费时间。

6. 部署与安全注意事项

6.1 生产环境部署流程

我的部署目标是单台云服务器,所以结构比较简单。Django 项目用 Gunicorn 跑在 8000 端口,Vue 前端构建成静态文件放在 Nginx 的 html 目录里,Nginx 监听 80 端口。关键配置如下:

server { listen 80; server_name your-domain.com; root /var/www/frontend; index index.html; location /api/ { proxy_pass http://127.0.0.1:8000/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /media/ { alias /var/www/backend/media/; } location / { try_files $uri $uri/ /index.html; } }

try_files那一行是 Vue 单页应用的关键。前端路由走 history 模式后,刷新/repair/123时 Nginx 找不到这个路径对应的静态文件,必须回退到index.html让 Vue Router 接管路由,否则刷新页面就是 404。

Django 那边生产环境要把DEBUG设为 False,ALLOWED_HOSTS配好域名和服务器 IP。静态资源收集用collectstatic,把 Django 自定义的静态文件也集中到一个目录里由 Nginx 托管。我用 Gunicorn 绑定了 4 个 worker——这个数字不是随便定的,参考值是 CPU 核心数乘以 2 再加 1。对这套管理系统的并发量来说,4 个 worker 完全够用。

6.2 必须注意的几个安全细节

医疗数据虽然不是公网上最敏感的类型,但涉及设备信息、维修记录和成本数据,安全上不能马虎。我做了这几件事:

第一,权限控制全部落在后端,前端的按钮隐藏只当体验优化,不当安全机制。第二,所有接口要求 JWT 认证,管理员的额外操作再叠加 Django Admin 的权限体系。第三,上传的文件做了类型校验和白名单限制,图片还会重新压缩生成新文件,防止恶意文件直接落地。第四,生产环境的数据库账号、Redis 密码、JWT 密钥全部从环境变量读取,不写死在代码仓库里。

还有一点容易被忽略:Django 的SECRET_KEY和生产密钥一定不能提交到 Git 仓库。就算仓库是私有的,万一哪天账户泄露,所有 token 都可以被伪造。我见过不止一个项目因为密钥泄露导致全站被拖库,这个教训用一次就够了。

6.3 部署之后还要做什么

系统上线不是终点,运维期的事情同样不少。首先数据库要定期备份,我用 crontab 每天凌晨拉一次pg_dump,保留最近 30 天的备份文件。其次 Nginx 的访问日志要配置切割,不然一年下来日志文件能把磁盘撑爆。另外 Django 的错误日志要单独记录下来,我通常把 stdout 和 stderr 重定向到日志文件,再用日志归档策略避免单文件无限增长。

前端构建产物版本管理也很重要。每次发布时保留上一版 dist 目录,万一新版本有问题,可以一键回滚。这个操作成本很低,但上线后的安全感提升很明显。

我在实际部署过程中还有一个体会:访问速度的问题,百分之八十出在网络环境和前端资源体积上,而不是后端性能。Vue 项目构建出来的 JS 文件如果太大,首屏加载就会被拖慢。我把路由用懒加载的方式拆开后,每个页面单独打包,首屏只加载需要的代码,加载速度提升了一倍左右。这个优化在管理系统里几乎必做,效果非常明显。

7. 写在最后的个人经验

做完这套系统,我最大的感触是:技术选型和代码实现只占整个项目的一部分,真正的难点在理解业务和守住流程。医院设备科的老师未必懂状态机、权限这些概念,但他们需要系统能告诉他们“这台设备维修到哪一步了”“这个季度哪个科室的设备维修花销最高”。开发者要做的,是把这些业务诉求翻译成数据结构、接口和页面流转。

如果你正在做类似的系统,我的建议是先画清楚状态流转图,再动数据库设计。工单状态一旦混乱,后面所有的统计报表都会跟着乱。其次,文件上传和附件关联一定别拖到最后再做,医疗设备维修场景里,维修照片、验收单、合同扫描件这些附件优先级非常高,后期补往往要改动数据库表结构,成本高得多。

这套系统我用 Django + Vue 3 的组合完整落地并上线运行,整体开发周期比预想的要短。现在再回头看,很多细节仍然有改进空间,比如消息通知完全可以接入企业微信或者短信,备件补货提醒也可以做得更智能。管理系统的价值从来不是一次到位,而是跟着业务一起迭代。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 4:55:34

CentOS 7 源码编译 VSFtpd:SSL/PAM/capabilities 定制实战

1. 项目概述:为什么在 CentOS 7 上坚持源码编译 VSFtpd?你点开这篇内容,大概率不是为了“装个 FTP 服务”这么简单——而是正卡在一个具体场景里:可能是某台生产环境的 CentOS 7 服务器不允许启用 EPEL 或第三方仓库,也…

作者头像 李华
网站建设 2026/10/17 4:55:33

汇编语言检测内存中SoftICE驻留痕迹的反调试实现

某开发者接到一个老式加密壳的维护任务,壳自身有反调试逻辑,但在装上某经典内核调试器SoftICE的环境里,程序总是被异常修改后照常运行,怎么打断点都拦不住。排查到最后,发现是壳的检测模块只查了用户态调试器相关入口&…

作者头像 李华
网站建设 2026/10/17 4:53:54

从幽灵窗口到 exit_reparent:X11 下安全拆解窗口树父子关系

1. 从一个“关不掉的窗口”说起我在维护一个自制轻量窗口管理器时,遇到一个特别反常的 Bug:窗口能正常创建、正常拖动、正常缩放,但只要一点关闭按钮,屏幕左上角就会留下一个无法操作的“幽灵窗口”,后台进程没退出&am…

作者头像 李华
网站建设 2026/10/17 4:53:53

J2EE订单履约系统:事务原子性与JMS解耦实战

简介:这是一套基于J2EE技术栈开发的完整在线图书销售系统——“云工厂-网上书城”,面向Java Web初学者与企业级应用实践者,用于深入理解多层架构设计、前后端协同及数据库集成等核心开发能力。资源包含588个文件,涵盖83个Java源码…

作者头像 李华
网站建设 2026/10/17 4:53:51

Spark Streaming foreachRDD 避坑指南:连接池、序列化与性能调优

1. 为什么foreachRDD是Spark Streaming里最容易踩坑的算子刚接触Spark Streaming那会儿,我一度以为foreachRDD就是个“把结果写到外部系统”的出口,跟RDD的foreach差不多,能跑就行。结果上线第一个实时统计任务,跑了不到两小时&am…

作者头像 李华
网站建设 2026/10/17 4:53:51

CTF MISC实战:Audacity频谱分析提取摩斯密码

取到青岑CTF MISC方向这道“美妙的音乐”时,我第一反应是赶紧戴上耳机完整听了一遍。前奏轻快,副歌也不跑调,甚至算得上比赛里少有的“好听”。但经验告诉我,MISC题目的背景音乐越正常,猫腻越藏在某个看不见的频段里。…

作者头像 李华