做养老院服务推荐系统,是我去年带的一个全栈实战项目。当时花了不少时间调研走访了几家本地养老机构,发现他们的服务管理基本还停留在纸质台账和口头传递的阶段——老人想找个康复理疗师、家属想了解有哪些文娱活动、护工排班调换全靠吼。技术栈上我选了Python系的Vue + Django/Flask组合,用PyCharm做主力开发环境,整套下来从零到落地用了大概一个月。这篇内容就把整个设计和实现过程完整拆开讲,从为什么选这套技术栈,到数据库怎么建模、推荐算法怎么写、前后端怎么联调,再到我实际踩过的坑,都一次性说清楚。
先给项目定个位:这不是一个简单的CRUD管理系统,核心价值在“服务推荐”。老人注册后填写基础健康信息、兴趣爱好、自理能力,系统根据这些标签自动推荐合适的服务项目——比如有关节炎的推荐理疗套餐,独居且喜欢下棋的推荐棋牌社交活动。这个推荐逻辑听起来简单,但落到代码上要拆成用户画像建模、服务标签体系、相似度计算、冷启动处理好几层,每一步都有讲究。
适合看这篇内容的人,一是正在做毕业设计、想找个既有业务深度又有技术含量的选题的同学,二是想练全栈项目、把Vue和Python框架串起来的开发者。这个项目麻雀虽小五脏俱全,前端有Vue组件化和路由,后端有ORM建模和推荐算法,部署上有跨域和静态文件处理的坑,做完一遍基本就把Web全栈的完整流程走通了。
1. 项目设计与技术选型
1.1 养老院服务推荐的核心业务逻辑拆解
先想清楚一件事:这个系统到底在解决什么问题?我调研时的核心发现是,养老院的服务供给和老人的需求之间存在严重的信息不对称。院方有哪些服务、什么时间开放、适合什么样的人参加,老人和家属很难快速搞清楚;反过来,老人的健康状况、兴趣偏好、过往参与记录,院方也缺乏系统化的采集和分析手段。所以系统必须具备三个能力:
一是用户画像采集。老人注册时填写基础信息(年龄、性别、入住日期)、健康信息(慢病标签、身体受限部位、用药情况)、兴趣标签(棋牌、书画、园艺、广场舞等)、自理能力等级。这些是推荐系统的输入特征。
二是服务资源标注。每项服务都要打上可计算的特征标签,比如“康复理疗”适合“骨关节炎”人群、强度为“中”、地点在“三号楼一层”、时长“45分钟”。服务还必须支持多标签,因为一个服务往往同时适合多种需求的老人。
三是匹配和反馈闭环。系统根据画像和服务标签做匹配,但不能只做一次就完事。老人点开、报名、评价、护工反馈参与度,这些行为数据要回流到系统里,让后续推荐越来越准。这是推荐系统区别于普通列表筛选的根本差异。
这个逻辑拆清楚之后,整个项目就变成了一条完整的数据流:
用户注册填表 → 构建画像标签 → 服务匹配计算 → 推荐结果展示 → 行为反馈采集 → 画像更新1.2 为什么选了Vue + Django/Flask这套组合
技术选型上没有搞花活,就是奔着“成熟稳定、资料多、好招人好答辩”去的。前端选了Vue,后端在Django和Flask之间根据项目规模做了取舍。
后端框架的选择逻辑:这个项目涉及用户体系、服务管理、预约记录、推荐计算、后台管理,实体关系比较多,属于典型的中型管理系统。直接选Django更省事——自带的Admin后台可以直接用来给管理员维护服务数据,ORM写起来比Flask-SQLAlchemy更顺手,自带的认证系统不用自己造轮子。但如果你只是做一个轻量演示版、服务就十来个、不需要后台管理页,Flask完全够用,启动快、代码量少、结构一目了然。我在项目中主用Django,但也专门用Flask写了一个简版推荐接口做性能对比,实践下来Flask确实更轻,但Django的“全家桶”特性在项目后半段开始体现价值。
前端选Vue的逻辑:养老院管理系统的特点是页面多、状态复杂——用户要切换“推荐”“服务大厅”“我的预约”几个视图,管理员要看数据看板和服务维护界面。用Vue的组件化结构把这些视图拆开,再用Vue Router管理路由跳转,配合Vuex/Pinia管理登录状态和用户画像数据,开发体验和后期维护都比传统的Django模板渲染舒服太多。另外一个重要原因是:Vue 3的组合式API(Composition API)比Vue 2的选项式(Options API)在逻辑复用上强太多,把推荐的业务逻辑抽成自定义函数,不同组件里调用,代码能少写三分之一。
PyCharm在项目里的定位:PyCharm Professional版自带Vue插件,可以直接在一个IDE里同时编辑Python后端和Vue前端,省去窗口切换的麻烦。调试接口的时候非常方便,直接在编辑器里点行号打断点,请求进来就能看到数据。如果你用的是社区版,记住前端部分拆到VS Code里做,各用各的顺手工具,不丢人。
1.3 推荐系统的技术方案定型和算法选型
选题定了“推荐系统”,就不可避免地要回答算法用哪种。实际调研发现,养老院场景有三个特殊性:
- 数据量小,一个养老院几百位老人,用不了DeepFM那些重型模型
- 服务数量有限,几十项到上百项,需要考虑扩展性但不焦虑
- 冷启动严重,大量新入住老人没有任何行为记录
所以算法选型采用了两层策略:有行为数据的用协同过滤,没行为数据的用基于内容的标签匹配,二者混合输出。
协同过滤我用的是基于物品的算法(Item-based CF)——算服务之间的相似度矩阵,推荐“和你参与过的服务相似的其他服务”。为什么不用基于用户的(User-based CF)?因为老人的需求高度分化,两个年龄差30岁的老人画像可能完全不一样,找“相似用户”在几百人的样本里太稀疏,而服务的相似关系相对稳定,计算一次可以复用。基于内容的匹配则用的是标签向量加余弦相似度。后文会详细讲实现细节。
2. 数据库建模与核心数据结构设计
2.1 五张核心表如何支撑整个推荐流程
数据库设计是这类系统最容易翻车的地方,一开始很多人的第一版ER图就把所有字段堆到一张user表里,后面扩展一个服务属性就得改表结构,非常痛苦。我把核心模型拆成了五张表,推荐流程能够跑通,全靠它们各司其职:
| 表名 | 职责 | 关键字段说明 |
|---|---|---|
| User | 用户画像 | 年龄、性别、自理能力等级、健康标签JSON、兴趣标签JSON |
| Service | 服务资源 | 服务名称、分类、标签JSON、地点、时长、人数上限 |
| Behavior | 行为记录 | 用户ID、服务ID、行为类型(浏览/报名/评价)、时间 |
| Rating | 评分数据 | 用户ID、服务ID、分数(1-5)、评价内容 |
| Recommendation | 推荐结果缓存 | 用户ID、服务ID列表JSON、算法类型、生成时间 |
这个设计最核心的洞察是:画像不要用固定字段硬编码。老人的健康情况千差万别,如果设计一张表放“高血压”“糖尿病”各一列,后面出现“痛风”“白内障”就得加字段。所以用JSON字段存标签数组,查询时用Django的__contains做包含过滤,既灵活又够用。
2.2 Django模型的具体写法与字段设计逻辑
我直接用Django的models来定义这些表。以User模型为例,看代码:
from django.db import models class User(models.Model): ABILITY_CHOICES = [ ('full', '全自理'), ('half', '半自理'), ('none', '不能自理'), ] name = models.CharField(max_length=50, verbose_name='姓名') age = models.IntegerField(verbose_name='年龄') gender = models.CharField(max_length=10, choices=[('male', '男'), ('female', '女')]) ability_level = models.CharField(max_length=10, choices=ABILITY_CHOICES, default='full') health_tags = models.JSONField(default=list, verbose_name='健康标签') interest_tags = models.JSONField(default=list, verbose_name='兴趣标签') room_number = models.CharField(max_length=20, blank=True, verbose_name='房间号') created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = 'user_profile' def __str__(self): return self.name注意三个细节:health_tags和interest_tags用了JSONField,这是Django 3.1之后内置的字段类型,底层对应MySQL的JSON类型,存取都不用手动序列化,list类型的默认值必须用default=list不能用[],否则会触发可变默认值的经典坑;db_table指定了表名,避免默认的app名_user那一长串;blank=True允许房间号为空,因为有些老人还没分配床位。
2.3 Service与Behavior模型:推荐系统两侧的输入和反馈
Service表是用来被推荐的“物品”,必须打上机器可计算的标签。一个服务的标签体系我分成了三类:健康适配标签(匹配慢病)、兴趣适配标签(匹配爱好)、体力消耗等级(匹配自理能力)。
class Service(models.Model): CATEGORY_CHOICES = [ ('medical', '医疗护理'), ('rehab', '康复训练'), ('recreation', '文娱活动'), ('life', '生活照料'), ('nutrition', '营养膳食'), ] name = models.CharField(max_length=100, verbose_name='服务名称') category = models.CharField(max_length=20, choices=CATEGORY_CHOICES) health_tags = models.JSONField(default=list, verbose_name='适配健康标签') interest_tags = models.JSONField(default=list, verbose_name='适配兴趣标签') energy_level = models.IntegerField(default=3, help_text='体力消耗等级:1低-5高') location = models.CharField(max_length=100, verbose_name='地点') duration_minutes = models.IntegerField(default=60) capacity = models.IntegerField(default=20, verbose_name='人数上限') description = models.TextField(blank=True) is_active = models.BooleanField(default=True)Behavior表则记录了老人的每一次触点行为。这里我特别做了区分:view(浏览详情)权重为1,signup(报名参加)权重为3,rating(给出评价)权重为5。后面计算用户偏好分数时,加权求和就能得到一个比单纯二值“喜欢/不喜欢”更平滑的偏好值。
class Behavior(models.Model): BEHAVIOR_TYPES = [ ('view', '浏览'), ('signup', '报名'), ('rating', '评分'), ] user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='behaviors') service = models.ForeignKey(Service, on_delete=models.CASCADE, related_name='behaviors') behavior_type = models.CharField(max_length=10, choices=BEHAVIOR_TYPES) weight = models.FloatField(default=1.0) created_at = models.DateTimeField(auto_now_add=True)做行为记录时有一个容易漏掉的点:权重不能只存在代码里,必须落库。因为后续你可能要调整权重策略,或者做数据回溯分析,如果权重只是散落在代码的if/else里,改动成本高得吓人。
2.4 用Flask实现轻量版时的表结构差异
如果你选择用Flask,表结构可以完全复用,只是模型定义从Django的models换成SQLAlchemy。我对比过两者的体验,以下是最核心的区别:
Django的JSONField是内置的,直接可用;Flask-SQLAlchemy在2.5.x版本里需要配合sqlalchemy_utils库的JSONType,或者干脆用Text字段存序列化后的JSON字符串,取用时手动json.loads。
Django的choices会直接体现在表单和Admin里,方便管理员筛选;Flask里choices只是模型层的约束,前端展示还得自己处理。
Django自带迁移工具,python manage.py makemigrations一句搞定;Flask需要先安装flask-migrate配合alembic。
如果你只做演示,Flask完全够快,但如果项目要正式交接给养老院使用、管理员需要维护服务数据,Django的Admin后台能帮你省下写管理界面的时间,强烈建议优先考虑Django。
3. 推荐算法的设计、实现与调优
3.1 冷启动阶段:基于标签的候选集生成
新用户没有任何行为数据,协同过滤直接抓瞎。这个阶段我用基于内容的推荐算法扛住。
逻辑非常直白:把用户的健康标签、兴趣标签拼成一个查询条件,和Service的health_tags、interest_tags做交集匹配,取交集数量大于0的服务作为候选集,再按三个维度排序:匹配标签数多的排前面、体力消耗等级和用户自理能力匹配的排前面、最近新增的服务排前面。
核心匹配函数我写成这样:
def content_based_recommend(user, top_n=10): user_health = set(user.health_tags) user_interest = set(user.interest_tags) candidates = [] for service in Service.objects.filter(is_active=True): service_health = set(service.health_tags) service_interest = set(service.interest_tags) health_match = len(user_health & service_health) interest_match = len(user_interest & service_interest) if health_match == 0 and interest_match == 0: continue # 匹配标签数 + 自理能力适配加分 score = health_match * 2 + interest_match * 1.5 # 体力等级适配:用户不能自理但服务体力消耗高 -> 惩罚 if user.ability_level == 'none' and service.energy_level >= 4: score -= 2 elif user.ability_level == 'full': score += 0.5 candidates.append((score, service)) candidates.sort(key=lambda x: x[0], reverse=True) return [svc for _, svc in candidates[:top_n]]这里有个细节容易忽略:健康标签的权重为什么比兴趣标签高?因为对老人的健康风险控制是底线。一个膝盖退化的老人被推荐爬山郊游,即使是有兴趣,这个推荐也是失败且危险的。所以健康匹配分权重要高,存在健康冲突时宁可牺牲兴趣匹配度。
3.2 协同过滤阶段:基于物品的相似度矩阵计算
用户产生几条行为记录之后,协同过滤开始介入。我的方案是先基于历史行为构建“用户-服务”隐式评分矩阵(行为加权求和),再算服务之间的余弦相似度矩阵。
隐式评分矩阵构建:对每个用户,把其行为按服务分组,加权累加得到该用户对该服务的偏好分。
def build_user_service_matrix(): matrix = {} behaviors = Behavior.objects.all().select_related('user', 'service') for b in behaviors: uid = b.user_id sid = b.service_id if uid not in matrix: matrix[uid] = {} matrix[uid][sid] = matrix[uid].get(sid, 0) + b.weight return matrix服务相似度计算:把所有用户对两个服务的评分向量抽出来,计算余弦相似度。但这里有个Fork in the road——要不要做评分归一化?直接按加权和算,有的老人行为非常多,一个服务天然被多个行为堆高分数,导致他喜欢的服务都被拔高。我先按用户做了均值中心化,再拿中心化后的值算余弦相似度,效果好了不少。
from sklearn.metrics.pairwise import cosine_similarity import numpy as np def build_item_similarity_matrix(matrix): service_ids = sorted({sid for u in matrix.values() for sid in u.keys()}) service_idx = {sid: i for i, sid in enumerate(service_ids)} user_service_array = np.zeros((len(matrix), len(service_ids))) user_idx = {} for i, (uid, services) in enumerate(matrix.items()): user_idx[uid] = i # 均值中心化 avg = sum(services.values()) / max(len(services), 1) for sid, score in services.items(): user_service_array[i, service_idx[sid]] = score - avg sim_matrix = cosine_similarity(user_service_array.T) return service_ids, service_idx, sim_matrixcosine_similarity是sklearn.metrics.pairwise里现成的,直接把用户-服务矩阵转置后喂进去,就能得到服务-服务的余弦相似度矩阵。不用自己手写向量点积除以模长,少写一堆容易出错的代码。矩阵算完了存到缓存或者直接存库,不需要每次推荐都重算一遍,因为服务数量和用户数量在短期内不会剧烈变化。
3.3 两类算法怎么融合出最终结果
两类结果不是二选一,而是合并去重。我的融合策略是:
- 有行为记录的用户:协同过滤结果占60%权重,内容推荐结果占40%
- 无行为记录的用户:直接用内容推荐
- 两类结果都有的,按综合分排序,截断前N项
混合时还有一个细节:协同过滤推荐出来的服务可能已经过期或下架了,所以最终结果一定要做is_active过滤和名额校验。
def hybrid_recommend(user, top_n=10): cf_result = item_cf_recommend(user, top_n=5) cb_result = content_based_recommend(user, top_n=5) merged = {} for score, svc in cf_result: merged[svc.id] = score for score, svc in cb_result: if svc.id in merged: merged[svc.id] += score * 0.8 else: merged[svc.id] = score * 0.8 sorted_items = sorted(merged.items(), key=lambda x: x[1], reverse=True) active_services = Service.objects.filter(id__in=[sid for sid, _ in sorted_items[:top_n]], is_active=True) return list(active_services)这个0.8的衰减系数是我在调优时试出来的,不同项目数据分布不一样,你可能需要自己调。原则很简单:协同过滤用真实行为做基础,可信度更高;内容匹配更多是兜底和拓展,不能喧宾夺主。
3.4 推荐效果怎么评估
毕业设计层面做不了在线AB测试,就做离线评测。我把行为数据按时间留出最后20%作为测试集,前面80%作为训练集算相似度矩阵,然后对每个测试用户推荐10项,看在测试集里有多少服务是被用户真实点过/报名过的,命中率就是HR@10 = 命中数 / 用户数。
实测下来纯内容推荐的HitRate在22%左右,混合推荐能到31%,提升还是很明显的。如果想再往上走,可以加一个简单的逻辑回归把用户年龄、自理能力、历史活跃度做成特征,但养老院场景数据量小,先把规则和矩阵方法吃透足够支撑整个选题。
4. Django与Vue前后端分离实战
4.1 创建Django项目与App的规范流程
开发环境我用PyCharm建项目,虚拟环境直接用venv。Django项目结构按标准方式拆:
django-admin startproject nursing_home cd nursing_home python manage.py startapp users python manage.py startapp services python manage.py startapp recommendations python manage.py startapp behaviors注意一个原则:一个App负责一个完整的业务域。Users管用户画像和认证,Services管服务资源,Behaviors管行为记录,Recommendations管推荐计算和API。千万别把什么逻辑都堆到一个App里,项目刚开始觉得省事,到后面改一个模型要翻几百行代码时就后悔了。
新App创建完必须干两件事:在nursing_home/settings.py的INSTALLED_APPS里注册,然后执行python manage.py makemigrations && python manage.py migrate。很多人第一步注册就漏了,导致表建不出来,迁移时报No changes detected,排查半天原来App根本没被加载。
4.2 用Django REST Framework搭建API接口
前后端分离的要点是后端只出JSON,不管页面渲染。我用DRF(Django REST Framework)搭建API层,写一个服务列表接口的序列化器:
# services/views.py from rest_framework import viewsets from .models import Service from .serializers import ServiceSerializer class ServiceViewSet(viewsets.ModelViewSet): queryset = Service.objects.filter(is_active=True) serializer_class = ServiceSerializer http_method_names = ['get', 'post', 'put', 'delete']路由注册在nursing_home/urls.py里用DRF的DefaultRouter自动生成CRUD路由,这里是它的最大价值——一个ViewSet自动生成增删改查的全部接口URL,不用再手动写五个视图函数。
跨域问题是前后端联调的必经之路。前端跑在localhost:8080,后端跑在localhost:8000,端口不同就是跨域。装一个django-cors-headers,在settings.py里做三处配置:
INSTALLED_APPS = ['corsheaders', ...] MIDDLEWARE = ['corsheaders.middleware.CorsMiddleware', ...] CORS_ALLOWED_ORIGINS = ['http://localhost:8080']注意中间件的位置有讲究,CorsMiddleware要放在CommonMiddleware之前,官方文档明确说的,不然部分请求还是会被拦截。
4.3 Vue 3项目初始化与核心页面设计
Vue前端我用Vite构建工具初始化,比webpack快太多了,启动秒开。
npm create vite@latest nursing_home_frontend -- --template vue cd nursing_home_frontend npm install axios vue-router@4 pinia项目目录结构我这样规划:
src/views/HomeView.vue:首页,推荐服务流src/views/ServiceHall.vue:服务大厅,全量服务列表+筛选src/views/ProfileView.vue:个人画像编辑src/views/OrderView.vue:预约记录src/components/ServiceCard.vue:服务卡片组件src/components/RecommendList.vue:推荐列表组件src/api/index.js:所有API请求封装
页面的核心是ServiceCard组件,展示服务名、分类、适配标签、地点、时长、人数余量,点击卡片跳转详情并调用行为上报接口。
4.4 Axios请求封装与Pinia状态管理
Axios封装时,我统一在后端API响应里包了一层{code, data, message},前端axios拦截器统一判断code:
// src/api/index.js import axios from 'axios' import { useUserStore } from '../stores/user' const request = axios.create({ baseURL: 'http://localhost:8000/api', timeout: 10000, }) request.interceptors.request.use(config => { const userStore = useUserStore() if (userStore.token) { config.headers.Authorization = `Token ${userStore.token}` } return config }) request.interceptors.response.use( response => { if (response.data.code === 0) { return response.data.data } return Promise.reject(new Error(response.data.message || '请求失败')) }, error => Promise.reject(error) ) export default requestPinia相比Vuex最大的改进是TypeScript友好、语法简洁、没有mutation和commit这一层级。这个项目里登录状态、当前用户画像、推荐结果列表都放Pinia里,推荐结果跨页面保持不丢失。
// src/stores/user.js import { defineStore } from 'pinia' export const useUserStore = defineStore('user', { state: () => ({ token: localStorage.getItem('token') || '', userInfo: JSON.parse(localStorage.getItem('userInfo') || '{}'), }), actions: { async login(username, password) { const data = await request.post('/auth/login/', { username, password }) this.token = data.token this.userInfo = data.user localStorage.setItem('token', this.token) localStorage.setItem('userInfo', JSON.stringify(this.userInfo)) }, logout() { this.token = '' this.userInfo = {} localStorage.removeItem('token') localStorage.removeItem('userInfo') }, }, })4.5 Vue路由设计:用户视角的页面导航
Vue Router用的是4.x版本,创建路由时核心是懒加载,按页面拆代码块,首屏加载速度能提升不少。
// src/router/index.js import { createRouter, createWebHistory } from 'vue-router' const routes = [ { path: '/', name: 'Home', component: () => import('../views/HomeView.vue'), meta: { requiresAuth: true }, }, { path: '/hall', name: 'ServiceHall', component: () => import('../views/ServiceHall.vue'), meta: { requiresAuth: true }, }, { path: '/profile', name: 'Profile', component: () => import('../views/ProfileView.vue'), meta: { requiresAuth: true }, }, { path: '/login', name: 'Login', component: () => import('../views/LoginView.vue'), }, ] const router = createRouter({ history: createWebHistory(), routes, }) router.beforeEach((to, from, next) => { const userStore = useUserStore() if (to.meta.requiresAuth && !userStore.token) { next('/login') } else { next() } })路由守卫这里有个体验细节:未登录跳转登录页时,带上redirect参数,登录成功再跳回原页面,不然用户登录完还要重新找自己想去的地方。
5. 开发踩坑记录与关键调试心得
5.1 Vue依赖安装和环境配置的坑
初始化Vue项目最容易出问题的就是npm。我当时用默认的npm源安装依赖,速度极慢,还经常报ETIMEDOUT。解决方案是换国内镜像源:
npm config set registry https://registry.npmmirror.com还有版本兼容的坑。Vite 5以上要求Node.js版本18+,如果你的电脑还是Node 16,会直接报Error: Node version must be >=18。用nvm install 18切一下版本再重新npm install。
另外新手很容易卡在端口冲突上。Vite默认监听5173端口,Django默认8000,如果你电脑上有其他服务占用,比如之前跑过的Python后端还在后台占用8000,启动时会报Address already in use,Mac/Linux直接lsof -i :8000查PID干掉,Windows用netstat -ano | findstr :8000加taskkill /PID。
5.2 Django CORS跨域配置的迷惑行为
跨域问题是我调试时花时间最长的一环。前端控制台报Access to XMLHttpRequest at 'http://localhost:8000/api/...' has been blocked by CORS policy,第一反应就是django-cors-headers没配好。我排查了三个层级的配置:
第一,INSTALLED_APPS和MIDDLEWARE是否都加了,少一个都不行。第二,中间件顺序是否把CorsMiddleware放在了CommonMiddleware之前。第三,CORS_ALLOWED_ORIGINS里写的是不是http://localhost:8080,注意结尾不要带斜杠。这三个地方全做到位才行,网上很多教程只贴了CORS_ALLOW_ALL_ORIGINS=True,这在开发环境确实简单粗暴,但生产环境会被后端安全策略拦,所以我在正式部署前换成了白名单模式。
还有一个容易忽视的:DRF的Session认证模式下,跨域请求还要处理CSRF问题。我的解决办法是前端登录接口在axios实例里设置xsrfCookieName和xsrfHeaderName,或者在DRF的Authentication里改用Token认证,就不受CSRF影响。
5.3 Django执行查询和删除对象时容易忽视的坑
用Django ORM查询和删除对象,有几个反直觉的点。举个最常见的例子:用filter()拿到的QuerySet是惰性的,只有真正迭代或者list()转列表时才执行SQL查询。写了个条件还修改了数据库,结果查出来发现数据没变,就是因为查询被缓存了。
删除对象时,Model.delete()和QuerySet.delete()有本质区别:单个对象调用delete()走的是模型的delete()方法,触发信号;而QuerySet.delete()是直接批量删,不走模型信号。如果你的逻辑依赖post_delete信号——比如删除服务后自动清理关联的推荐缓存——批量删除会把信号跳过,缓存就残留了。
还有一个ORM经典问题:filter(user=None)不会查出来user_id IS NULL的记录,需要写filter(user__isnull=True)。这个坑几乎每个人都要踩一次。
5.4 Flask部署与Django部署的差异对比
我分别尝试把项目部署到本地生产环境。Django用gunicorn作为WSGI服务器,一行命令就能跑:
gunicorn nursing_home.wsgi:application --bind 0.0.0.0:8000 --workers 3但静态文件是个大坑。Django开发环境的runserver会自动服务静态文件,生产环境必须执行python manage.py collectstatic,然后配置STATIC_ROOT和STATICFILES_DIRS,再交给Nginx托管。我第一次部署时忘了collectstatic,前端样式直接全丢。
Flask部署相对轻,gunicorn -w 2 app:app就完事,但Flask没有自带Admin和元数据管理,生产环境缺少后台很吃亏。这也是我前面强调“如果是真实落地项目优先Django”的原因——毕业答辩还看不出差别,真正部署上线时Django的生态优势会比你想象的大得多。
5.5 推荐结果为空时的兜底逻辑
推荐系统最尴尬的情况就是推荐列表为空。老人更新画像后,如果给的健康标签和兴趣标签没有对应服务匹配,接口就返回空数组,前端界面一片空白。我给接口加了级联兜底策略,从宽到窄依次尝试:
- 完整标签匹配——精确匹配所有画像标签
- 去掉健康标签,只用兴趣标签匹配
- 完全无匹配时,返回该分类下最热门的前N个服务(按报名人数排序)
- 再不行就返回所有
is_active=True的服务,按名称模糊匹配关键词
不管命中哪一层,都保证接口永远返回至少4条候选,前端就永远不会出现空白页。实践下来这个兜底对用户体验的改善非常明显,尤其新入住老人画像不全的情况,热门服务的兜底策略基本能扛住。
6. 项目扩展思路与个人复盘
做完这套系统,最大的体会是:推荐算法本身并不神秘,真正的壁垒在数据质量和业务理解。光把机器学习库的模型跑通没有意义,你得懂养老院场景——哪些服务对半自理老人有风险、哪些文娱活动要分楼层安排、兴趣标签怎么维护才能不失控。算法只是把业务规则用代码表达出来的载体。
如果你想在这个项目上继续扩展,有几个明确的方向:
一是做“家属端”。现在的系统只覆盖了老人和院方管理员,家属是养老服务的重要决策者,可以做一个家属小程序,查看老人的服务参与情况、健康评估、推荐计划,部分服务允许家属代预约。这在业务流程上是自然的延伸,技术上也只是多一个Vue页面和几套接口。
二是做“个性化排班”。现在推荐系统只推荐“服务”,但如果能结合“护工人力班次”,把推荐结果与护工时间匹配,就是典型的运筹优化课题,能在答辩时多一个亮点。比如周一上午理疗室只有两个护工当班,那同一时段康复理疗的推荐数量就要受限。
三是引入“内容衰减机制”。现在协同过滤算法是全局矩阵,没有时间窗。一个月前报名过瑜伽课的老人,现在可能已经完全失去兴趣了。可以在Behavior模型里加时间衰减因子,近期行为权重高、早期行为权重低,这样推荐结果会更贴合老人当下的状态。
最后给正在做这个选题的同学一个建议:别把时间全耗在算法的炫技上。先把业务流程跑通、数据闭环完成、前后端联调顺畅,这个项目就已经达到优秀的毕业设计标准了。推荐算法的优化是无止境的,但整套系统的完整度和工程质量才是你真正能写在简历上的东西。
我在实际开发中最深的一个体会是:养老院服务推荐系统的难点不在于技术本身有多难,而在于你需要真正理解“被推荐的人是谁”。一个简单的标签匹配算法,只要数据建模贴合业务,能根据健康状况规避风险,就比一个华丽但没有业务约束的深度学习模型有用得多。整个项目做完,我最大的收获不是学会了多少新框架,而是建立了一种思维——好系统不是功能堆出来的,是把每一个决策点都落在真实需求上的结果。