做养老院信息管理系统的项目,前后花了差不多两个月。刚开始接触需求时,我以为是做一个普通的增删改查后台,后来去一家社区养老机构蹲了半天,才发现这里的业务远比想象中复杂:老人的基础档案、护理记录、床位分配、健康指标、用药提醒、亲属探访,还有每个月的费用结算,每一块都不能出错。老人家属打电话来问今天血压怎么样,护理员如果不能三秒钟内调出记录,那就说明系统设计是失败的。今天把整个设计和实现过程梳理一遍,尤其讲讲Python和Vue前后端分离的架构怎么做,Django和Flask怎么选,以及PyCharm里最容易踩的坑。
1. 项目背景与技术选型解析
1.1 养老院管理系统到底要解决什么
这个项目出发点是解决养老机构日常运营中的信息碎片化问题。我调研时发现,大多数养老院的老人档案还停留在纸质表格阶段,护理记录要么写在交接本上,要么散落在几个Excel文件里;家属询问老人今天吃了什么、有没有按时服药、最近血压波动情况时,护理员往往要翻好几份记录才能答上来。更麻烦的是床位变更、转房、退住这些操作经常是口头通知,月底一核对数据就对不上。所以系统的核心价值,是把散落在纸面和脑子里的信息,统一收进一个可查询、可追溯、可预警的数字化平台里。
具体功能上,一个能真正跑起来的养老院管理系统至少要覆盖老人档案、床位分配、护理流程记录、健康档案、用药提醒、探访预约、费用结算和系统权限管理这几块。老人档案不是简单存一个身份证号,还要包含入住时间、紧急联系人、既往病史、过敏药物、护理等级;护理记录需要按时间轴记下来,便于追溯;健康档案要能记录血压、血糖的定期测量值,最好还能生成趋势图;用药提醒是给护理员看的,什么时间该给哪位老人喂什么药,系统要能提前提示。这些需求不是我拍脑袋想出来的,每一个都是从实际运营场景里倒推出来的。如果你拿这个项目做毕业设计或者个人作品集,把这些业务模块讲清楚,评委或面试官最容易认可的就是需求分析是否真正落地。
1.2 为什么选Python + Vue,而不是Java或PHP
技术选型这件事,我认真比过好几条路线。Java配合Spring Boot确实是大厂主流,但一个人开发时从环境搭建到编码实现的周期会拉长不少,Spring Boot的依赖注入、配置管理都要花精力熟悉。用PHP写后台配合Laravel也有不少成功案例,但PHP在API开发上不如Python顺手,尤其是后续想扩展数据分析、机器学习的健康风险预测等功能时,Python的生态优势非常明显。所以我最终定下来用Python做后端,Vue做前端,整个链路很清晰:Python负责业务逻辑和数据接口,Vue负责页面展示和交互,两边通过JSON格式通信,开发效率很高。
后端框架在Django和Flask之间我犹豫了很久。Flask轻量灵活,一个main.py就能写几十个接口,适合非常小的项目;但养老院管理系统的业务量并不小,老人、护理、床位、费用、健康档案、探访、用户权限一大堆模型要管理,如果逻辑全堆在一个文件里,后面维护非常痛苦。Django自带ORM、Admin后台、用户认证中间件,而且是天然的应用模块化结构,一个App对应一个业务域,代码组织非常清晰。Django还有一个隐藏好处:自带Admin后台能快速生成内部数据维护入口,做原型和测试阶段太方便了。所以这个项目最终选了Django,Flask也不是不行,但更适合更小的单业务场景。
1.3 PyCharm在开发中的作用与配置要点
用PyCharm做开发工具,图的是它的集成调试和虚拟环境管理。专业版自带Django支持,可以不敲命令行直接点运行按钮启动开发服务器,调试断点时能直接看到请求对象的全部字段,定位问题快很多。另一点是PyCharm管理虚拟环境很方便,创建项目时选好解释器,它会自动生成venv目录,之后pip安装的依赖都装进这个环境里,不会污染系统Python。我建议新手也一定要用虚拟环境,否则不同项目之间的包版本冲突会让你怀疑人生。像这个项目要装Django、djangorestframework、djangorestframework-simplejwt、django-cors-headers、pymysql、Pillow,如果全装到全局环境,大概率会破坏电脑上其他项目的依赖。
PyCharm配置Django项目有几个要点需要记住。第一是设置解释器,在File -> Settings -> Project -> Python Interpreter里选已经创建好的venv;第二是如果使用MySQL,配置好Database工具窗口,方便直接查看表结构和跑SQL,当然用Navicat也行;第三是运行配置,在Run/Debug Configurations里选择Django server,指定settings模块,这样按一下快捷键就能启动服务。新手最容易遇到的坑是本地连MySQL时报ModuleNotFoundError: No module named 'MySQLdb',其实就是因为没装pymysql,且没在项目__init__.py里写import pymysql; pymysql.install_as_MySQLdb()。这种细节网上很多教程不会提,但一旦踩到就会卡住半天,我在后面的常见问题里还会专门整理。
2. 系统整体设计与数据库建模
2.1 功能模块规划:从角色出发而不是表格出发
很多人在设计管理系统时会犯一个错误:一上来就想着建多少张表,把所有字段罗列出来,然后再去想页面该长什么样。这样做的结果往往是表之间关系冗余,功能跟用户的操作习惯了脱节。我习惯先从角色出发,搞清楚谁会使用这个系统,他每天要做什么。养老院系统里实际使用的人主要是三类:系统管理员、一线护理员、以及偶尔来看信息的家属。管理员关注基础数据维护和费用核算;护理员关注今天要看护哪些老人、要执行什么护理任务、老人状态有没有异常;家属关注的是老人的生活状况和探访安排。
从这三个角色出发,功能模块自然就出来了。管理员端包括员工账号管理、老人入退住登记、床位分配与调换、价格设置与费用流水、数据报表统计;护理员端包括今日任务列表、护理记录录入、健康数据记录、用药执行确认、请假与换班申请;家属端做得简单些,就是查看老人日常动态和提交探访预约。整个页面采用左右布局,左侧是菜单栏,右侧是主内容区。菜单先按角色做权限过滤,这样每个用户登录后只能看到自己关心的部分,界面不臃肿,安全性也更好控制。还有一个容易忽略的点:这些模块之间不是孤立的。比如护理员录入护理记录后,管理员能在报表中看到某一位老人的护理频次,家属能在动态页面看到护理摘要。所以设计时要把数据流向画清楚,而不是每个页面各自为政。
2.2 数据库表结构设计细节
数据库设计是这个项目的核心基础,表建不好,后面全乱。我梳理的核心表大概有八张:用户表、角色表、老人信息表、床位表、护理记录表、健康档案表、探访预约表、费用流水表。老人信息表不是简单把联系人电话塞进去就行,我做了两个字段分别存紧急联系人姓名和关系,与亲属联系方式拆开,因为实际场景中子女电话和紧急联系人往往不是同一个人。床位表要和楼栋、楼层、房间做关联,我用building、floor、room_number三个字段,每张床位有唯一编号和状态:0表示空闲,1表示占用,2表示维修。老人表里放一个bed_id外键,这样查房时能直接关联出老人名字,护理员操作也直观。
具体建表时我使用了Django的ORM模型,没有手写SQL。以下是一个简化版的老人信息模型示意,实际开发时可以根据需求增加字段:
from django.db import models class ElderInfo(models.Model): name = models.CharField(max_length=50, verbose_name='姓名') gender = models.CharField(max_length=10, verbose_name='性别') birth_date = models.DateField(verbose_name='出生日期') id_card = models.CharField(max_length=18, unique=True, verbose_name='身份证号') admission_date = models.DateField(verbose_name='入住日期') nursing_level = models.CharField(max_length=20, verbose_name='护理等级') health_status = models.TextField(blank=True, verbose_name='健康状况') allergies = models.TextField(blank=True, verbose_name='过敏药物') bed = models.OneToOneField( 'bed.BedInfo', on_delete=models.SET_NULL, null=True, blank=True, related_name='elder_info', verbose_name='床位' ) contact_name = models.CharField(max_length=50, verbose_name='紧急联系人') contact_phone = models.CharField(max_length=20, verbose_name='联系人电话') emergency_contact = models.CharField(max_length=50, blank=True, verbose_name='备用联系人') created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间') class Meta: db_table = 'elder_info' ordering = ['-created_at']护理等级单独拎出来,是因为养老院的定价和排班基本都跟随护理等级走,自理、半失能、失能老人的护理频次完全不同,这也是后续做费用统计和任务生成的重要依据。护理记录表则记录老人ID、护理员ID、护理类型(口腔护理、翻身拍背、排泄护理等)、护理内容、备注、记录时间,并按时间倒序查询,方便看护史追溯。费用流水表要记录费用类型、金额、发生时间、支付状态、关联老人和经办人,月底结算时直接按老人和时间段汇总。
2.3 前端Vue项目结构与路由规划
前端我用的是Vue 2.7配合Element UI,因为这套组合文档多、组件全、社区解决方案丰富,开发管理后台可以省很多事。Vue CLI创建项目后,我在src目录下划分了api、assets、components、router、store、views六个目录。api目录里每个业务模块一个文件,比如elder.js、nurse.js、health.js,里面全部是封装好的请求函数;views目录按页面划分,比如Login.vue、Dashboard.vue、ElderList.vue、ElderEdit.vue、NurseRecord.vue。这样目录结构和后端接口能一一对应,找人找代码非常直接。
路由规划上,使用vue-router的嵌套路由:根路由是登录页,登录成功后进入布局组件Layout,然后在Layout下挂载dashboard、elder、nurse、health、bed、fee等子路由。因为涉及权限,我加了全局前置守卫,在beforeEach里检查是否有token,没有token就重定向到登录页;有token再根据用户角色动态过滤可以访问的菜单。这里有个容易被忽略的点:前端只能做展示层面的权限控制,真正的安全校验一定要在后端做,否则别人直接调接口就能绕过。所以前端权限只是提升体验,后端Django的权限校验才是最后一道防线。前后端联调时最容易发现的问题是Element UI的表单字段名和后端模型字段名不一致导致提交失败,这个需要一开始就统一命名规范。
3. 后端实现:Django项目搭建与核心API
3.1 Django项目创建与全局配置
我在PyCharm里创建Django项目时,命令其实很简单:django-admin startproject elderly_site,然后在项目根目录下创建App,比如python manage.py startapp elder、startapp nurse、startapp bed、startapp fee。一个App管一个业务域,这样Django的migration也能分离,后续调整表结构时不容易牵连。Django的默认数据库是SQLite3,开发和演示阶段完全够用,但部署或真实使用建议换成MySQL,因为并发写入和容量更好。我本地开发用的就是MySQL,配置方式是在settings.py里修改DATABASES参数,指定ENGINE为django.db.backends.mysql,然后填上数据库名、用户名、密码、主机和端口。
全局配置有些细节值得讲一下。settings.py里ALLOWED_HOSTS在开发阶段可以留空或写['*'],但部署时必须改成具体域名,否则会暴露Host头攻击风险。INSTALLED_APPS里要加入rest_framework、corsheaders以及自己创建的App模块。因为前后端分离,后端不返回HTML页面,只返回JSON,所以TEMPLATES不用大改。LANGUAGE_CODE建议改成'zh-hans',TIME_ZONE改成'Asia/Shanghai',USE_I18N和USE_TZ保持默认。这里有个细节:数据库里如果存的是UTC时间,展示时再转本地时间,可以避免服务器时区不同导致时间错乱。
3.2 核心Model与序列化器设计
Model就按照前面的表结构来写。我在elder这个App的models.py里定义ElderInfo类,字段用CharField、DateField、ForeignKey等定义。ForeignKey关联床位表时设置on_delete=models.SET_NULL并允许为空,因为老人退住后床位要释放。每个模型类的Meta里可以设置db_table为可读性更高的表名,ordering按创建时间倒序。Django的ORM会自动生成migration文件,开发时执行python manage.py makemigrations和python manage.py migrate就能建表。这个环节我的心得是:外键关系一定要想清楚,尤其床位和老人是一对一还是多对一。我这里用的是OneToOneField,因为一个床位同一时刻只能住一位老人,这样查房时直接bed.elder_info就能拿到老人,逻辑非常清晰。
数据要转成JSON返回前端,我用的是Django REST Framework的ModelSerializer。在elder的serializers.py里定义ElderSerializer,Meta里指定model=ElderInfo,fields里列明需要暴露的字段,比如id、name、gender、birth_date、admission_date、nursing_level、bed_no等。外键字段如果直接返回ID,前端还得再调一个接口去查床位编号,所以我选择了使用SerializerMethodField返回详细的床位对象,这样前端一次拿到完整数据,少一次网络请求。页面列表需要分页,DRF内置了PageNumberPagination,只需要在settings里配置DEFAULT_PAGINATION_CLASS即可。一次真实的接口返回结构通常是这样:
{ "code": 0, "message": "success", "data": { "total": 120, "items": [ { "id": 1, "name": "张奶奶", "gender": "女", "bed_no": "A-02-01", "nursing_level": "半失能", "contact_phone": "138****1234" } ] } }3.3 登录认证与权限控制
管理系统的登录认证我用的是JWT方案,具体是djangorestframework-simplejwt这个库。安装后在settings.py里配置REST_FRAMEWORK的DEFAULT_AUTHENTICATION_CLASSES为JWTAuthentication,然后配置SIMPLE_JWT的过期时间,比如ACCESS_TOKEN_LIFETIME设成8小时,REFRESH_TOKEN_LIFETIME设成7天。用户模型我选择用Django自带的User再扩展一个Profile模型,通过OneToOneField关联,Profile里存角色role、手机号、头像等额外信息,这比完全自定义User模型要省事不少。调用登录接口时,后端验证用户名密码,返回access和refresh两个token,前端存到localStorage里,每次请求时在Authorization头里带上Bearer token。
权限控制方面,我在视图函数上用DRF的permission_classes装饰器。例如is_staff可以访问所有接口,is_nurse只能访问自己相关的护理记录接口。具体实现时通过request.user.profile.role来区分角色,也可以自定义一个IsAdminOrReadOnly权限类,继承BasePermission,在has_permission方法里判断用户角色和请求方法。这种做法很灵活,而且序列化时还能根据角色过滤字段。比如护理员可以录入护理记录但不能修改费用,管理员可以管理基础数据。这里需要特别提醒:JWT的token在过期前无法主动作废,用户修改密码或角色变更后旧的token仍然有效。如果对安全要求高,需要实现token黑名单或者简单的强制下线逻辑。
4. 前端Vue核心功能实现与联调实录
4.1 axios封装与请求拦截器
前端和后端通信,我统一用axios。这里重点讲如何封装一个通用的request.js模块。先创建一个axios实例,设置baseURL为http://localhost:8000/api,再设置timeout为10000。然后在请求拦截器里从localStorage取出token,设置到headers的Authorization字段。响应拦截器里做两件事:如果返回的HTTP状态码是401,说明token过期或未登录,就清掉本地token并跳转到登录页;如果业务code不等于0,就用Element UI的Message组件提示后端返回的错误信息。核心代码大致如下:
import axios from 'axios' import { Message } from 'element-ui' import router from '@/router' const service = axios.create({ baseURL: process.env.VUE_APP_BASE_URL, timeout: 10000 }) service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }, error => { return Promise.reject(error) }) service.interceptors.response.use(response => { const res = response.data if (res.code !== 0) { Message.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) } return res }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') router.push('/login') } Message.error(error.message || '网络异常') return Promise.reject(error) }) export default servicebaseURL最好写成环境变量,开发时指向本地Django服务,部署时指向线上后端域名。用Vue CLI时可以放在.env.development和.env.production文件中,通过process.env.VUE_APP_BASE_URL读取。之前有次联调问题就是因为baseURL写死,项目从台式机挪到笔记本上跑,后端IP变了,前端所有请求全部报错,排查半天才发现是配置写死。养成从一开始就用环境变量的习惯,后面会省很多事。
4.2 核心页面实现思路与代码要点
举几个有代表性的页面说实现思路。
老人列表页:顶部是筛选栏,包含姓名、护理等级、入住日期范围;中间是表格,列有老人姓名、性别、年龄、床位号、护理等级、紧急联系人;右侧操作按钮有查看详情和编辑。数据来源是在created钩子里调用ElderAPI.getList(),参数带上分页和筛选条件。表格数据返回后,直接用Element UI的el-table绑定期;年龄字段由birth_date动态计算,封装一个工具函数根据出生日期算周岁。详情页用抽屉el-drawer展示老人所有信息,并放一个进入护理记录列表的入口。这个页面的难点在于筛选条件和分页参数要和后端接口约定好,例如page、size、search、nursing_level这些参数名必须一致,否则后端收不到参数。
护理记录页面:选择一位老人后,默认展示最近30天的护理记录时间线,每条记录卡片上显示护理员姓名、护理类型、内容、时间。新增记录时打开浮动表单,护理类型用el-select选择,内容用textarea填写,提交时调POST接口,成功后刷新列表。这个页面是护理员使用频率最高的,交互上要尽量减少点击次数,比如在老人详情页直接放一个"新增护理记录"按钮,省去找老人的步骤。Element UI的日期时间选择器返回时间默认是ISO格式,后端Django解析没问题,但要小心不是所有UI组件都能自动处理。
登录页面:账号密码输入框加登录按钮,点击后调登录接口,返回的token存到localStorage,再根据用户角色跳转不同首页。我遇到过的小坑是登录成功后刷新页面,Vuex中的状态会丢失,页面白屏。解决方案是在router.beforeEach里检查store里的用户信息,如果为空但token存在,就调一次获取用户信息的接口重新拉取。
4.3 CORS跨域与联调问题
前后端分离开发,最难缠的就是跨域。我在Django后端使用django-cors-headers解决。安装后,在settings.py里INSTALLED_APPS增加corsheaders,MIDDLEWARE里增加CorsMiddleware放在最前面,然后设置CORS_ALLOWED_ORIGINS为前端地址,比如http://localhost:8080,同时设置CORS_ALLOW_CREDENTIALS为True。如果前端用axios的withCredentials携带cookie,后端必须配置这个参数,且不能用CORS_ALLOW_ALL_ORIGINS=True,因为带cookie时无法用通配符,这是浏览器规范的限制。
如果不想每次部署都改动Django的CORS配置,也可以反过来用前端开发服务器做代理。在vue.config.js里配置devServer的proxy,把/api的请求转发到http://localhost:8000,这样前端页面发请求时同源,不会触发CORS。这种方法上线时由Nginx统一处理,后端甚至可以完全不开CORS。两种方案我都实测过,最终在开发阶段用代理,部署阶段用Nginx反向代理,后端CORS关闭,因为生产环境同域名访问不需要跨域。下面是开发阶段的代理配置片段:
module.exports = { devServer: { port: 8080, proxy: { '/api': { target: 'http://localhost:8000', changeOrigin: true } } } }5. 常见问题与排查技巧实录
5.1 PyCharm环境配置与Python包依赖
大多数第一次跑这个项目的人都会挂在环境步骤上。首先Python版本建议用3.8以上,某些依赖对低版本支持不好。创建项目时最好新建虚拟环境,不要直接用全局Python。在PyCharm里选择Virtualenv Environment,指定基础解释器,这样项目会自带一个venv目录。然后用终端执行pip install -r requirements.txt,我把项目依赖都写在这个文件里,包括django、djangorestframework、djangorestframework-simplejwt、django-cors-headers、pymysql、Pillow等。Pillow用于处理后台上传图片,比如老人头像,没有它Django的ImageField会报错。
依赖装完还要执行数据库迁移,python manage.py makemigrations和python manage.py migrate。如果你用的是MySQL,别忘了先建好数据库,并把settings里的用户名密码改成自己的。有个特别常见的坑是MySQL 8.0以上版本默认使用caching_sha2_password认证方式,pymysql老版本连不上,解决办法是安装pymysql大于1.0.0版本,或者干脆用mysqlclient,官方文档支持更好。我第一次搭环境时被这个问题卡了很久,所以特别拿出来提醒。
5.2 数据库中文乱码与时间字段相关
中文乱码问题,多数出在MySQL字符集配置上。建库时就要指定utf8mb4,例如CREATE DATABASE elderly DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci。Django的settings.py里DATABASES的OPTIONS里也可以设置charset='utf8mb4',这样ORM写入的数据就不会乱码。如果MySQL里表的默认字符集是latin1,那段加上utf8mb4的数据也会乱,所以建库和建表都要统一。
时间字段的坑从开发到部署都有。Django默认使用UTC,如果settings里USE_TZ=True,数据库里存的是UTC时间,前端展示时如果不做转换就会少8个小时。我在Django里统一把datetime字段在序列化时转为本地时间显示,前端只要直接用字符串显示即可。还有创建时间和更新时间建议用auto_now_add和auto_now,这两个字段Django会自动维护,不要自己去赋值,不然每次更新数据都要手动改,最容易遗漏。我在项目初期就因为手动维护updated_at字段,导致修改数据后时间不更新,排查了很久。
5.3 联调时出现401、404、500的排查顺序
前后端联调时,报错类型基本是三类。401,大部分原因是token没带上或token过期。先打开浏览器开发者工具,看请求头里有没有Authorization字段,如果没有,检查axios拦截器是否写了token获取逻辑;如果字段有,再看token是否过期。404,先确认URL路径和请求方式是否正确,比如Django的path里写的是elder-info,前端请求写成elderInfo,肯定404。500,则把Django的调试信息打开,先看控制台里报什么异常,或者用print/log把关键变量打出来,定位到具体行。我习惯写接口时先把异常用try-except包一层并打出日志,虽然上线时不能暴露详情,但开发时对排查问题帮助很大。
代理和跨域问题再多说一句:如果你用了Vue的代理方案,出现请求404,很可能是proxy配置里target写错端口,或者pathRewrite把路径改写错了。用vue.config.js代理时,路径是没有进到后端的,一定要确认实际请求拿到的URL和Django的路由匹配。有个快速排查方法:在浏览器Network面板里看请求URL,如果已经是http://localhost:8000/api/...说明代理生效,否则检查proxy配置。
5.4 部署上线的建议
项目上线时,不建议再用python manage.py runserver,这个服务器性能太弱只能调试用。我用的是gunicorn配合nginx,流程是:后端代码跑在gunicorn上,绑定127.0.0.1:8000,前端打包成静态文件交给nginx托管,nginx再把/api开头的请求反代到gunicorn。这样动静分离,前端访问静态文件很快,后端接口走反向代理,也没有跨域问题。部署前记得把settings.py里的DEBUG设为False,ALLOWED_HOSTS填上域名,然后执行python manage.py collectstatic收集Django的静态文件。数据库迁移和数据初始化要在服务器上再执行一遍。
前端路由如果使用history模式,刷新子页面时nginx会404,需要在nginx配置里加上try_files $uri $uri/ /index.html;。如果路由使用hash模式就不会有这个问题,但URL看着不美观。部署完后用curl测试后端接口通不通,再访问前端页面,全部走通就算上线了。这里给一个简单的nginx配置参考:
server { listen 80; server_name your-domain.com; root /var/www/elderly_frontend; index index.html; location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }如果服务器配置低,并发量也不大,其实Django+SQLite也不是不能用,但我建议还是MySQL,因为后期数据量增长后SQLite的锁问题会越来越明显,查询性能也会下降。
做完这个项目再回头看,我最大的体会是这类管理系统不在于技术栈多新,而在于把业务细节想清楚,把每个操作闭环做好。护理员录入一条记录,管理员能看到统计报表,家属能查看老人动态,这三个环节打通了,系统才真正有生命力。我自己在第一次设计时太关注表结构和接口数量,反而忽略了护理员每天的实际使用场景,后来重新梳理需求才把交互理顺。如果准备做类似项目,建议先花时间蹲在业务现场看别人怎么工作,再动手写代码。数据字段的设计,一定是在实际观察后才定下来的,而不是一开始拍脑袋。希望这些经验能帮准备做养老院管理系统的同学少踩几个坑。