最近一直在做信息管理系统,前阵子接了一个模拟的养老院项目,最后用 Python 搭配 Vue 把整套系统设计和实现出来了。后端主要用 Django,也认真对比过 Flask 的轻量方案,整个开发过程都在 PyCharm 里完成。这类管理系统表面看只是登记老人资料,真正动手做才发现护理记录、床位分配、费用结算、家属探访这些模块环环相扣,没提前设计好数据模型很容易写到一半就返工。这篇文章就把从需求分析、数据模型、后端 API、前端页面到联调部署的过程完整梳理一遍,正在做类似系统的开发者可以参考落地。
1. 养老院信息管理系统到底要管什么:需求梳理与数据模型设计
1.1 核心业务流程拆解
养老院的日常运营比想象中繁琐得多。老人入住首先要登记完整档案,分配床位;护理员每天要记录老人的体温、血压、用药;财务每个月要按床位费和护理等级生成账单;前台还要管理家属来访。如果不做系统,全凭Excel和微信群,数据一多就各种对不上。
我按照角色把业务拆成了几个核心模块:
- 老人档案:姓名、性别、出生日期、入住时间、床位号、紧急联系人、健康状态、医保信息。
- 床位管理:楼层、房间号、床位号、状态,床位状态直接影响老人入住和费用计算。
- 护理记录:每天多次记录体温、血压、用药、护理内容,还要关联操作护理员。
- 排班管理:护理员每天负责哪些老人、值班班次,方便追溯责任。
- 费用管理:床位费、护理费、餐费按月生成账单,支持查看未缴和已缴状态。
- 访客登记:来访人信息、被访老人、进出时间。
表面看每个模块都是独立的增删改查,实际上数据关系非常紧密。老人要挂在床位上,护理记录必须关联老人和护理员,账单又依赖床位状态和护理等级。如果一开始没把关系理清楚,后面每加一个功能都要打补丁。
1.2 数据库实体关系设计
我设计表结构时遵循尽量简单、查询高效的原则。核心表有这几张:老人表、房间床位表、员工表、健康档案表、护理记录表、费用表和访客表。
具体关系是这样的:
- 老人表和房间床位表是单向的一对一关系,因为一个固定床位只对应一位老人。我在老人表里放room_id外键,避免维护中间关联表。如果要换床,直接更新这个外键就行。
- 员工表通过护理记录表和老人表产生多对多关系,排班本质上就是员工和老人的对应关系。
- 健康档案表和老人表是一对多关系,每次体检都会追加一条记录。
- 费用表和老人表是一对多,每月可以生成多条独立账单。
拿护理记录表举例,核心字段设计成这样:
class CareRecord(models.Model): elderly = models.ForeignKey('elderly.Elderly', on_delete=models.CASCADE, verbose_name='老人') staff = models.ForeignKey('staff.Staff', on_delete=models.CASCADE, verbose_name='护理员') record_time = models.DateTimeField(auto_now_add=True, verbose_name='记录时间') temperature = models.FloatField(verbose_name='体温') blood_pressure = models.CharField(max_length=20, verbose_name='血压') medication = models.CharField(max_length=200, blank=True, verbose_name='用药') note = models.TextField(blank=True, verbose_name='备注')有几个字段设计的坑需要特别注意。时间字段必须用DateTimeField而不用字符串,否则后面做统计排序会非常痛苦。外键的on_delete策略也要想清楚,老人虽然退住但历史护理记录不能删,所以我用CASCADE关联老人表,但房间表用了SET_NULL,防止房间删除导致老人数据丢失。
开发阶段我先用SQLite,部署时切到MySQL。Django的ORM把数据库细节屏蔽了,迁移命令一跑,表结构自动同步,省了手写SQL的麻烦。
2. 后端框架选型:Django还是Flask,以及为什么最终选了Django
2.1 Django与Flask的核心差异对比
标题里写的django flask,说明很多人在做这类系统时都会纠结两个框架选哪个。我给这两种选择做了个直接对比:
| 维度 | Django | Flask |
|---|---|---|
| 自带功能 | ORM、Admin后台、认证、迁移全都有 | 只给最基本的请求路由,其余靠扩展插件 |
| 工程结构 | 有默认的项目结构,App拆分清晰 | 结构完全自由,适合小项目 |
| 数据库迁移 | 内置migrations,管理方便 | 需要额外使用Flask-Migrate |
| 第三方生态 | Django REST Framework非常成熟 | 要靠Flask-SQLAlchemy等插件组合 |
| 学习曲线 | 有一定学习成本但按部就班 | 上手简单但工程化要自己搭 |
| 适合场景 | 多模块后台管理系统 | 轻量接口服务、小微应用 |
养老院管理系统有老人、护理、费用、访客这些清晰模块,角色还分管理员、护理员、财务、前台,用Django的“全家桶”思路最合适。路由、ORM、Admin后台、认证通通不需要自己拼,模式固定,后面交给同事接手也容易看懂。
Flask也不是不行。如果只做十几个接口的轻量API,Flask确实更灵活,但信息管理系统要长期迭代,数据模型复杂,还是需要Django这种自带约束的框架。
2.2 结合项目规模做选型决定
为什么我会从项目规模角度考虑?因为养老院管理系统不是做一个算法演示,而是要支撑真实日常操作,稳定性和可维护性比开发速度重要得多。老人档案、护理记录都是需要长期留存的数据,字段多关系多,Django的ORM在迁移和查询优化上更稳,不容易出现隐蔽的bug。
PyCharm对Django有专门的工程支持,这也是一个重要因素。PyCharm能直接识别Django项目结构,创建项目时直接生成settings、urls、views这些基础文件;manage.py命令也做了集成,直接从右上角运行按钮就能执行。如果换Flask,PyCharm只能把它当普通Python项目,很多针对Django的调试和模板功能都用不上。
所以最终定的技术栈是:
- 后端:Python 3.11 + Django 4.2 + Django REST Framework
- 前端:Vue 3 + Vite + Element Plus + Axios
- 开发环境:PyCharm Professional
- 数据库:SQLite开发环境/MySQL 8生产环境
这个组合正好对应标题里的技术栈,每一步都有明确理由,不是顺手选的。
3. 用Django实现后端API:模型、序列化器与视图的落地过程
3.1 数据模型类与迁移
工程开始先在PyCharm里创建虚拟环境和Django项目。项目名我用了simulation_elderly作为模拟项目代称,命令行操作如下:
django-admin startproject elderly_system cd elderly_system python manage.py startapp elderly python manage.py startapp care python manage.py startapp billing python manage.py startapp staff拆成多个App而不是把模型全部堆到一个文件里,目的是让职责清晰。老人档案和房间管理的逻辑集中在elderly App,护理记录在care App,费用在billing App。每个App对应一个业务域,改一个模块不容易动到其他代码。
老人核心模型的写法:
from django.db import models class Room(models.Model): floor = models.IntegerField(verbose_name='楼层') room_no = models.IntegerField(verbose_name='房间号') bed_no = models.IntegerField(verbose_name='床位号', default=1) status = models.CharField(max_length=10, default='empty', verbose_name='状态') class Meta: db_table = 'room' ordering = ['floor', 'room_no', 'bed_no'] def __str__(self): return f"{self.floor}层{self.room_no}号-{self.bed_no}床" class Elderly(models.Model): name = models.CharField(max_length=50, verbose_name='姓名') gender = models.CharField(max_length=2, choices=[('M', '男'), ('F', '女')], verbose_name='性别') birth_date = models.DateField(verbose_name='出生日期') room = models.OneToOneField(Room, on_delete=models.SET_NULL, null=True, blank=True, verbose_name='床位') contact_person = models.CharField(max_length=50, verbose_name='紧急联系人') contact_phone = models.CharField(max_length=20, verbose_name='联系电话') status = models.CharField(max_length=10, default='active', verbose_name='状态') class Meta: db_table = 'elderly' verbose_name = '老人档案'写完后执行迁移:
python manage.py makemigrations python manage.py migrate迁移这一步最容易出的问题就是字段没有设置默认值或者没有允许为空,导致makemigrations报错。所以写模型时就要提前把这些参数想好,不要留着让后续迁移补充,那样每次都会要求交互输入。
3.2 DRF序列化器和视图开发
模型写完就要对外提供接口。Django REST Framework(DRF)在这里承担了序列化、反序列化和生成接口文档的任务,安装后注册到settings的INSTALLED_APPS里。
老人档案的序列化器这么写:
from rest_framework import serializers from .models import Elderly class ElderlySerializer(serializers.ModelSerializer): room_info = serializers.SerializerMethodField() class Meta: model = Elderly fields = ['id', 'name', 'gender', 'birth_date', 'room', 'room_info', 'contact_person', 'contact_phone', 'status'] def get_room_info(self, obj): if obj.room: return { 'id': obj.room.id, 'floor': obj.room.floor, 'room_no': obj.room.room_no, 'bed_no': obj.room.bed_no, } return NoneSerializerMethodField用于返回外键对象拼成的自定义字段,前端拿到的数据结构更符合展示需求,不用自己再去关联查询。视图用ModelViewSet一行一套CRUD:
from rest_framework import viewsets, permissions from .models import Elderly from .serializers import ElderlySerializer class ElderlyViewSet(viewsets.ModelViewSet): queryset = Elderly.objects.select_related('room').all() serializer_class = ElderlySerializer permission_classes = [permissions.IsAuthenticated]select_related主要用来解决N+1查询。列表页返回多条老人数据时,如果每条都再查一次房间表,接口性能会非常慢。用select_related提前把关联表查出来,前端渲染效率明显提升。
路由配置用DefaultRouter注册:
from rest_framework.routers import DefaultRouter from .views import ElderlyViewSet router = DefaultRouter() router.register('elderly', ElderlyViewSet, basename='elderly')这样list、detail、create、update、delete全部自动生成,前端通过HTTP直接访问。DRF自带的可浏览API页面配合调试非常高效。
分页在settings里配置:
REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'PAGE_SIZE': 10, }不分页的话老人列表数据稍微一多,响应时间就会直线上升,前端菜单都不流畅。
3.3 认证权限配置
管理系统必须分清角色权限。我在员工模型里增加了role字段,取值有admin、nurse、front_desk三种。管理员能增删改所有数据,护理员只能添加和查看护理记录,前台只能操作访客登记和老人列表查询。
DRF默认配置加上TokenAuthentication:
REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': [ 'rest_framework.authentication.TokenAuthentication', ], ... }前端登录成功后拿到token,后续每次请求在Header里带Authorization。通过修改不同ViewSet的permission_classes就能控制不同接口的角色访问。
护理记录这种接口,我只允许登录护士角色写入:
from rest_framework.permissions import IsAuthenticated class CareRecordViewSet(viewsets.ModelViewSet): permission_classes = [IsAuthenticated] def create(self, request, *args, **kwargs): data = request.data.copy() data['staff'] = request.user.staff.id serializer = self.get_serializer(data=data) serializer.is_valid(raise_exception=True) serializer.save() return Response(serializer.data, status=201)这个做法很关键:操作员信息不从前端传,直接从request.user取,防止有人伪造提交改成别人的护理记录。
4. Vue前端如何与后端对接:页面组件、axios封装与权限拦截
4.1 前端项目结构和路由设计
前端使用Vite初始化Vue 3项目,目录结构:
src/ api/ router/ store/ views/ Dashboard.vue ElderlyList.vue ElderlyDetail.vue RoomManage.vue CareRecord.vue BillList.vue Login.vue App.vue main.js路由在创建时就加了meta信息,用于权限拦截。管理员可以访问全部页面,护理员只允许访问护理相关页面。这个逻辑写到router.beforeEach前置守卫里:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.path === '/login') { next() } else if (!token) { next('/login') } else { const role = store.state.userRole if (to.meta.role && to.meta.role !== role) { next('/403') } else { next() } } })这样即使护理员手动修改地址栏进入费用管理页面,还是会被挡在门外。前端权限只是体验优化,真正可靠的防线始终是后端接口校验。
4.2 axios请求封装与跨域代理
axios必须做统一封装,不能每个页面每次都写完整地址。我的做法是创建api/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'] = `Token ${token}` } return config }, error => Promise.reject(error)) service.interceptors.response.use( response => response.data, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') window.location.href = '/login' } return Promise.reject(error) } ) export default service开发阶段最容易踩的坑是跨域。Vite默认端口是5173,Django是8000,直接请求会触发CORS。我在vite.config.js里配置代理:
export default { server: { proxy: { '/api': { target: 'http://127.0.0.1:8000', changeOrigin: true, } } } }前端所有/api开头的请求都转发到Django,完全绕开浏览器跨域限制,生产环境由Nginx做同样的事情。
4.3 核心业务页面实现要点
登录页接收用户名密码,POST接口返回token和角色信息,存入localStorage后跳转首页。老人列表页用Element Plus的el-table展示,查询条件由el-form提供姓名、房号输入框,查询时带上page参数请求后端分页数据。
房间管理页面我用卡片而不是表格展示,因为前台更关心哪些床位是空的,卡片状态色块更直观。实现逻辑是请求房间列表,根据status字段显示“空床/已入住/维修”三种状态,点击空床卡片可以弹窗直接关联入住老人。
护理记录页是典型的子表操作。选择老人后下拉框查询该老人的历史护理记录,新增表单里体温、血压、用药是必填项,提交时把elderly_id传POST接口。这个操作我在后端配置了权限,只有角色为nurse的员工才能提交,前端登录后也根据角色隐藏掉新增按钮,双重控制。
整体来看Vue部分的开发重点是组件拆分和状态共享,把老人详情拆成独立组件后,老人列表、健康档案、护理记录三个页面都能复用,不会出现一份代码复制三遍的糟糕情况。
5. 前后端联调中踩过的坑与解决方案
5.1 时间字段与中文编码问题
联调第一个坑就是时间格式。DRF默认返回ISO格式的时间字符串,比如2025-01-20T15:30:00,而Element Plus日期组件默认只接收2025-01-20。如果不做转换,后端DateTimeField会直接报错。我的解决办法是前端提交前把组件绑定值格式化为带T的字符串,或者直接用dayjs转换。
中文编码问题在开发环境用SQLite完全无感,一旦切到MySQL就暴露了。建库时如果没指定utf8mb4,所有中文存储都会变成问号。我在Django的settings里做了显式声明:
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'elderly_system', 'USER': 'root', 'PASSWORD': 'password', 'HOST': '127.0.0.1', 'PORT': '3306', 'OPTIONS': { 'charset': 'utf8mb4', } } }这个问题如果不提前处理,等上线数据写进去再改就要做数据迁移,特别麻烦。
5.2 图片上传与静态资源处理
老人档案需要上传头像和体检报告,这里涉及到文件上传。后端用ImageField或FileField存储到MEDIA_ROOT,再通过URL访问。需要检查settings里是否配置了MEDIA_URL和MEDIA_ROOT,还要在urls.py加一行路由:
from django.conf import settings from django.conf.urls.static import static urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)漏掉这一步文件会上传成功但无法访问,前端显示破图。上传请求必须使用FormData,同时前端手动设置Content-Type,浏览器会自动补上multipart boundary,手动设置反而会出问题。
我在模拟上传逻辑时,测试数据用的是一张临时占位图,上传后访问路径正常返回。
5.3 部署:PyCharm调试配合Nginx和Gunicorn
系统开发完成后要做模拟部署验证。我的方案是:Nginx承担反向代理和静态文件服务,Gunicorn跑Django后端,Vue构建后的dist目录作为前端静态文件。
Nginx关键配置:
location /api { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { root /var/www/dist; try_files $uri $uri/ /index.html; }Vue使用history路由后,刷新页面URL会请求到Nginx,但Nginx没有对应文件,所以必须用try_files把请求fallback到index.html,否则页面一刷新就404。
Django侧设置ALLOWED_HOSTS为部署域名,DEBUG设为False。如果不关闭DEBUG,错误页面信息会暴露到线上环境。
PyCharm在联调时的作用是远程同步。我把本地代码通过PyCharm的Deploy功能同步到测试环境,改一处代码点一次同步,省去手动敲scp命令的时间。
6. PyCharm让这个项目开发效率提升的一些实操配置
6.1 虚拟环境和解释器配置
PyCharm里建项目第一步就是配Python解释器。我在Settings -> Project -> Python Interpreter中新建Virtualenv,路径选择项目内的.venv,解释器用Python 3.11。之后所有依赖都安装在这个环境里,系统Python不受污染。
使用Django模板创建项目后,PyCharm右上角直接显示manage.py运行入口,点击下拉框就能执行makemigrations、migrate、runserver这些命令,不需要在Terminal里手输完整路径。
新开的Terminal默认激活虚拟环境,这个功能在Settings里打开选项“Activate virtualenv”即可,避免每次手动source。
6.2 调试与数据库工具的使用
Debug模式是排查问题的利器。前端调用一个接口报500,先在后端代码的ViewSet方法加断点,然后用Debug模式启动Django,前端再次请求就会停在断点处。此时可以检查request.data是否包含了正确字段,ORM查询返回了什么数据。
PyCharm Professional内置数据库工具,连接MySQL或SQLite后直接在IDE里打开表,查看字段和数据。遇到前端报错时,我先打开数据库跑一遍查询,确认数据层没问题,再回到后端代码找逻辑bug,这种顺序能快速缩小问题范围。
6.3 高频快捷键与代码模板
几个真正的效率快捷键,做项目期间非常常用:
- Ctrl + Shift + Alt + T:重构菜单,重命名变量时把所有引用同步更新。
- Ctrl + Alt + L:一键格式化代码,统一全项目的缩进和引号风格。
- Ctrl + Shift + F:全局搜索,找URL配置、API调用点非常快。
- Live Template:预置一个Django View模板,输入viewset再按Tab直接展开代码块。
- Postfix:输入.for再按Tab自动生成for循环。
内置的Local History也很有用。某次我改序列化器把字段名写错了,导致前端字段全被过滤,后来用Local History恢复修改前的版本,省了不少时间。
工作区开启“Watching”功能后,本地改代码自动同步到测试环境,模拟协作部署时非常高效。这个功能适合单人项目或小团队,多人场景还是用版本控制系统更稳妥。
后期跟着这个项目成长,值得扩展智能门禁对接、健康数据大屏、护工排班自动生成这些功能。技术栈不管怎么变,信息管理系统的底层逻辑都是数据模型要清晰,页面要围绕真实业务流程来组织。用PyCharm做这类系统,体验比传统编辑器组合好很多,关键是把环境、调试、数据库工具都用起来。