
做养老院信息管理系统的项目前后花了差不多两个月。刚开始接触需求时我以为是做一个普通的增删改查后台后来去一家社区养老机构蹲了半天才发现这里的业务远比想象中复杂老人的基础档案、护理记录、床位分配、健康指标、用药提醒、亲属探访还有每个月的费用结算每一块都不能出错。老人家属打电话来问今天血压怎么样护理员如果不能三秒钟内调出记录那就说明系统设计是失败的。今天把整个设计和实现过程梳理一遍尤其讲讲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后台能快速生成内部数据维护入口做原型和测试阶段太方便了。所以这个项目最终选了DjangoFlask也不是不行但更适合更小的单业务场景。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_length50, verbose_name姓名) gender models.CharField(max_length10, verbose_name性别) birth_date models.DateField(verbose_name出生日期) id_card models.CharField(max_length18, uniqueTrue, verbose_name身份证号) admission_date models.DateField(verbose_name入住日期) nursing_level models.CharField(max_length20, verbose_name护理等级) health_status models.TextField(blankTrue, verbose_name健康状况) allergies models.TextField(blankTrue, verbose_name过敏药物) bed models.OneToOneField( bed.BedInfo, on_deletemodels.SET_NULL, nullTrue, blankTrue, related_nameelder_info, verbose_name床位 ) contact_name models.CharField(max_length50, verbose_name紧急联系人) contact_phone models.CharField(max_length20, verbose_name联系人电话) emergency_contact models.CharField(max_length50, blankTrue, verbose_name备用联系人) created_at models.DateTimeField(auto_now_addTrue, verbose_name创建时间) updated_at models.DateTimeField(auto_nowTrue, 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项目搭建与核心API3.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-hansTIME_ZONE改成Asia/ShanghaiUSE_I18N和USE_TZ保持默认。这里有个细节数据库里如果存的是UTC时间展示时再转本地时间可以避免服务器时区不同导致时间错乱。3.2 核心Model与序列化器设计Model就按照前面的表结构来写。我在elder这个App的models.py里定义ElderInfo类字段用CharField、DateField、ForeignKey等定义。ForeignKey关联床位表时设置on_deletemodels.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里定义ElderSerializerMeta里指定modelElderInfofields里列明需要暴露的字段比如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增加corsheadersMIDDLEWARE里增加CorsMiddleware放在最前面然后设置CORS_ALLOWED_ORIGINS为前端地址比如http://localhost:8080同时设置CORS_ALLOW_CREDENTIALS为True。如果前端用axios的withCredentials携带cookie后端必须配置这个参数且不能用CORS_ALLOW_ALL_ORIGINSTrue因为带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里也可以设置charsetutf8mb4这样ORM写入的数据就不会乱码。如果MySQL里表的默认字符集是latin1那段加上utf8mb4的数据也会乱所以建库和建表都要统一。时间字段的坑从开发到部署都有。Django默认使用UTC如果settings里USE_TZTrue数据库里存的是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设为FalseALLOWED_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; } }如果服务器配置低并发量也不大其实DjangoSQLite也不是不能用但我建议还是MySQL因为后期数据量增长后SQLite的锁问题会越来越明显查询性能也会下降。做完这个项目再回头看我最大的体会是这类管理系统不在于技术栈多新而在于把业务细节想清楚把每个操作闭环做好。护理员录入一条记录管理员能看到统计报表家属能查看老人动态这三个环节打通了系统才真正有生命力。我自己在第一次设计时太关注表结构和接口数量反而忽略了护理员每天的实际使用场景后来重新梳理需求才把交互理顺。如果准备做类似项目建议先花时间蹲在业务现场看别人怎么工作再动手写代码。数据字段的设计一定是在实际观察后才定下来的而不是一开始拍脑袋。希望这些经验能帮准备做养老院管理系统的同学少踩几个坑。