做了一个“粤畅游”旅游推荐系统,技术栈选了Python后端+Vue前端,后端同时用到Django和Flask,开发环境是PyCharm。这套组合做下来,基本把Python全栈开发的主流程都走了一遍:数据建模、接口设计、推荐逻辑、前后端联调、打包部署。这篇文章把整个项目的思路、实现细节、踩坑点完整记录下来,给正在做类似旅游推荐系统、或者想用Django+Flask+Vue做毕设/练手项目的朋友一个能直接照搬的参考。
先说明一下我的方案定位:Django负责主体业务和核心API,Flask负责独立的推荐服务。这个分工不是拍脑袋定的,后面我会详细解释原因。整个系统实现的内容包括景点展示、城市筛选、类型标签筛选、热门推荐、个性化推荐、景点详情、收藏功能、路线推荐,以及后台数据管理,基本涵盖了一个旅游类小程序/Web端常见的功能闭环。
1. 技术选型与系统架构
1.1 为什么同时用Django和Flask
很多人在刚接触这套技术栈的时候会纠结:到底选Django还是Flask?我的结论是:如果项目够复杂,两个一起用并不冲突,甚至更合理。Django的强项是全家桶,自带ORM、Admin后台、认证体系、表单校验,做一个需要管理后台的业务系统非常省事。Flask的强项是轻、灵活,适合做单一职责的服务。
在这个项目里,Django承担的是主业务后端:景点数据管理、用户注册登录、收藏和评论功能、提供前端展示所用的全部REST接口。Flask则单独跑一个推荐引擎服务,接收用户行为数据,计算推荐结果,再通过HTTP接口把结果抛给Django层。为什么要拆出来?因为推荐算法后续可能会迭代,比如从简单热度榜升级成协同过滤甚至是基于Embedding的向量召回,用Flask隔离出来之后,改动推荐逻辑不影响主站业务的稳定。
实际上你也会发现,Django跑推荐也不是不行,但每次改推荐代码都要过一遍Django工程的中间件、路由、配置,试错成本高。Flask服务几十行代码就能起一个接口,部署的时候单独挂端口,主站挂了推荐接口依然可以降级返回热门榜单,可用性上更灵活。
1.2 Django和Flask通信的方式
两个后端之间直接通过HTTP调用,Django的View里用requests库调Flask服务,这种方式最直观,也最容易排查问题。
Django侧的核心调用逻辑大概是这样的:
import requests from django.conf import settings def get_recommendations(user_id, city=None, limit=10): flask_url = f"{settings.FLASK_SERVICE_URL}/api/recommend" params = {"user_id": user_id, "city": city or "", "limit": limit} try: resp = requests.get(flask_url, params=params, timeout=3) if resp.status_code == 200: return resp.json().get("items", []) except requests.exceptions.Timeout: return [] return []Flask侧对应的推荐接口就是一个标准的JSON返回,把推荐结果包装成统一结构。
from flask import Flask, request, jsonify import redis app = Flask(__name__) cache = redis.Redis(host="127.0.0.1", port=6379, db=1) @app.route("/api/recommend", methods=["GET"]) def recommend(): user_id = request.args.get("user_id", type=int, default=0) city = request.args.get("city", default="") limit = request.args.get("limit", type=int, default=10) # 先从缓存取推荐结果,取不到再实时计算 cache_key = f"rec:{user_id}:{city}:{limit}" cached = cache.get(cache_key) if cached: return jsonify({"items": eval(cached), "source": "cache"}) items = compute_recommendations(user_id, city, limit) cache.setex(cache_key, 300, str(items)) return jsonify({"items": items, "source": "realtime"})这里加了一个Redis缓存,推荐结果缓存5分钟,极大降低了推荐计算对数据库的压力。注意eval只在内部可信场景用,生产环境建议换成json.loads。
1.3 前端框架选型和目录结构
前端用了Vue 3 + Vue Router + Pinia + Element Plus。选择Vue而不是传统模板渲染,核心原因在于这个系统的交互复杂度:景点列表要实时筛选、推荐结果要异步刷新、用户收藏要即时反馈,用Vue组件化开发可以把这些状态管理得清清楚楚。
推荐的分层架构是Vue页面组件调用Pinia Store,Store里封装统一的Axios请求方法,API层单独抽离一个目录。目录结构如下:
travel-frontend/ ├── src/ │ ├── api/ │ │ ├── attractions.js # 景点相关接口 │ │ ├── recommend.js # 推荐相关接口 │ │ └── user.js # 用户相关接口 │ ├── assets/ │ ├── components/ │ │ ├── AttractionCard.vue # 景点卡片组件 │ │ ├── CityFilter.vue # 城市筛选栏 │ │ └── RatingStars.vue # 评分星星组件 │ ├── router/ │ │ └── index.js │ ├── stores/ │ │ ├── userStore.js │ │ └── attractionStore.js │ ├── views/ │ │ ├── HomeView.vue │ │ ├── ListView.vue │ │ ├── DetailView.vue │ │ └── RecommendView.vue │ ├── utils/ │ │ └── request.js # Axios统一封装 │ ├── App.vue │ └── main.js └── package.json后端Django工程结构相对标准:
travel_backend/ ├── manage.py ├── config/ │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── apps/ │ ├── attractions/ # 景点模块 │ ├── users/ # 用户模块 │ ├── orders/ # 收藏/评论模块 │ └── recommend/ # 推荐相关的Django层(调用Flask) ├── static/ └── templates/2. 数据库设计与推荐逻辑实现
2.1 核心数据表设计
一个旅游推荐系统的数据模型,核心是景点表、用户表、收藏表、评论表、用户行为表。下面给出最关键的几个表结构,用的是Django模型定义。
# apps/attractions/models.py from django.db import models class Attraction(models.Model): name = models.CharField(max_length=128, verbose_name="景点名称") city = models.CharField(max_length=32, db_index=True, verbose_name="城市") district = models.CharField(max_length=64, blank=True, verbose_name="所在区域") category = models.CharField(max_length=32, db_index=True, verbose_name="类型") cover_url = models.URLField(verbose_name="封面图") images = models.TextField(blank=True, verbose_name="图集JSON") price = models.DecimalField(max_digits=8, decimal_places=2, default=0, verbose_name="门票价格") rating = models.FloatField(default=0, verbose_name="评分") rating_count = models.IntegerField(default=0, verbose_name="评分数") popularity = models.FloatField(default=0, verbose_name="热度值") tags = models.CharField(max_length=255, blank=True, verbose_name="标签") keywords = models.TextField(blank=True, verbose_name="搜索关键词") intro = models.TextField(verbose_name="简介") lat = models.FloatField(default=0, verbose_name="纬度") lng = models.FloatField(default=0, verbose_name="经度") created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = "attractions" ordering = ["-popularity"]这里我要重点说明几个字段设计的考虑:
首先,city和category都设置了db_index=True,因为列表页最常见的查询就是按城市、按类型筛选。如果不加索引,数据量上了几万条之后,筛选接口会明显变慢。
其次,images字段直接存JSON字符串而不是单独建一张图片表,原因是景点图片只是展示用途,不需要跟业务表做关联查询,JSON字符串读取方便,也省去联表操作。类似的做法在项目初期完全够用,后续如果需要做图片审核、标签化管理再拆表。
用户表用Django自带的User扩展一个Profile表存偏好信息。
# apps/users/models.py from django.db import models from django.contrib.auth.models import User class UserProfile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile") preferred_city = models.CharField(max_length=32, blank=True, verbose_name="常去城市") visit_days = models.IntegerField(default=1, verbose_name="预计游玩天数") budget = models.IntegerField(default=500, verbose_name="预算上限") interests = models.TextField(blank=True, verbose_name="兴趣标签,逗号分隔")收藏和用户行为表是推荐算法的核心数据来源。
# apps/orders/models.py class Favorite(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="favorites") attraction = models.ForeignKey("attractions.Attraction", on_delete=models.CASCADE, related_name="favorited_by") created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = "favorites" unique_together = ("user", "attraction") class UserAction(models.Model): user = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL) attraction = models.ForeignKey("attractions.Attraction", on_delete=models.CASCADE) action_type = models.CharField(max_length=16, db_index=True) # view / fav / rating / search score = models.FloatField(default=1.0) timestamp = models.DateTimeField(auto_now_add=True)UserAction这张表是整个推荐系统的数据基础,每次用户浏览、收藏、评分,前端都会调用接口记录一条行为。刚开始可能觉得多余,但等到要做个性化推荐的时候,你会发现没有行为数据寸步难行。
2.2 推荐算法的三个层次
这个系统的推荐算法我分了三层实现,每一层都有明确的降级策略。
第一层:热门榜推荐
热门榜是最基础、永远兜底的推荐策略。热度值不直接等于浏览量,而是加权计算。
hot_score = 浏览量 * 0.4 + 收藏量 * 0.3 + 评分人数 * 0.1 + 平均评分 * 2.0这个公式里的系数是经验值,可以根据实际运营数据调节。实现上可以在每次用户浏览、收藏时触发热度值更新,也可以定时用脚本重算。
def update_popularity(attraction_id): attraction = Attraction.objects.get(pk=attraction_id) views = UserAction.objects.filter(attraction_id=attraction_id, action_type="view").count() favs = Favorite.objects.filter(attraction_id=attraction_id).count() ratings = UserAction.objects.filter(attraction_id=attraction_id, action_type="rating").count() avg_rating = attraction.rating if attraction.rating_count else 0 popularity = views * 0.4 + favs * 0.3 + ratings * 0.1 + avg_rating * 2.0 Attraction.objects.filter(pk=attraction_id).update(popularity=popularity)第二层:基于标签匹配的推荐
当用户注册时选择了兴趣标签,比如“历史文化”“自然风光”“亲子乐园”,系统根据这些标签去匹配景点。
具体的匹配逻辑是把用户的兴趣标签和景点Tags做交集计算,对匹配度高的景点加权排序。代码实现如下:
def recommend_by_tags(user_profile, limit=10): if not user_profile or not user_profile.interests: return [] interest_tags = [t.strip() for t in user_profile.interests.split(",") if t.strip()] attractions = Attraction.objects.all() scored = [] for att in attractions: att_tags = [t.strip() for t in att.tags.split(",") if t.strip()] match_count = len(set(interest_tags) & set(att_tags)) if match_count > 0: score = match_count * 2 + att.popularity scored.append((score, att)) scored.sort(key=lambda x: x[0], reverse=True) return [att for _, att in scored[:limit]]这个写法虽然简单,但有一个性能隐患:每次都要全表扫描。景点数据量在几千条以内问题不大,如果以后扩展到几十万条,必须改成数据库侧匹配或者用倒排索引,这个要注意。
第三层:基于用户行为的协同过滤
协同过滤是推荐系统的经典算法。我在这版系统里实现了基于物品的协同过滤(ItemCF),逻辑是:找出用户最近浏览/收藏过的景点,找到其他也浏览过这些景点的用户,再找到这些用户浏览过的其他景点,按共现次数排序推荐。
核心的计算步骤可以讲清楚,便于面试或者答辩时解释原理。
# Flask服务的实现 def item_cf(user_id, limit=10): # 1. 获取该用户最近的交互物品 user_attractions = get_user_recent_actions(user_id, top_n=10) sim_scores = {} # 2. 遍历每个物品,找出喜欢该物品的用户 for att_id in user_attractions: similar_users = get_users_actions_by_attraction(att_id) # 3. 遍历这些用户,找出他们喜欢的其他物品 for uid in similar_users: other_attractions = get_user_recent_actions(uid, top_n=10) for other_id in other_attractions: if other_id == att_id: continue sim_scores[other_id] = sim_scores.get(other_id, 0) + 1 sorted_items = sorted(sim_scores.items(), key=lambda x: x[1], reverse=True) return [item_id for item_id, _ in sorted_items[:limit]]这个版本的逻辑很原始,没有做归一化、没有惩罚热门物品,但作为初学者理解和实现是足够的。后续优化方向可以是引入Jaccard相似度、对热门物品降权加惩罚项1/log(1 + popularity)、加入时间衰减因子等。
2.3 Django ORM操作要点
开发过程中Django ORM有几个细节要特别记住。
删除对象用的是.delete()方法,这个方法有坑:如果模型定义了on_delete=models.CASCADE,删一个对象会级联删除关联数据。比如删除一个景点,所有关联的收藏和用户行为都会一并删除。如果只想删景点的推荐状态而不想删行为记录,一定要先清理关联。
# 正确删除单个对象 attraction = Attraction.objects.get(pk=1) attraction.delete() # 批量删除要小心,返回的是元组 (删除总数, 各表删除数明细) deleted_count, details = Attraction.objects.filter(city="广州").delete()另外,Django查询结果默认是惰性的,filter()并不会真的执行数据库查询,只有遍历或者调用list()、len()等操作时才触达数据库。调试时要区分QuerySet和真正的数据列表:
# QuerySet是惰性的,不是列表 queryset = Attraction.objects.filter(city="广州") print(type(queryset)) # <class 'django.db.models.query.QuerySet'> # 如果需要转换成列表,用 list() attractions = list(queryset)3. Vue前端开发实战
3.1 开发环境搭建与工程初始化
前端开发环境,我用的组合是Node.js 18 LTS + Vue CLI(或者Vite)+ Vue 3。这里要提醒新人,直接使用npm create vue@latest初始化项目,比手动配置Webpack省心得多。
如果用Vite创建项目,执行这几步:
npm create vite@latest travel-frontend -- --template vue cd travel-frontend npm install npm install vue-router@4 pinia axios element-plus npm run devElement Plus的引入有两种方式:完整引入和按需引入。新手建议先用完整引入,省去配置插件的麻烦,后续优化性能再考虑按需:
// main.js import { createApp } from 'vue' import { createPinia } from 'pinia' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' import App from './App.vue' import router from './router' const app = createApp(App) app.use(createPinia()) app.use(router) app.use(ElementPlus) app.mount('#app')关于Axios封装,我要强调一个重点:一定要统一处理BaseURL、请求超时、错误提示和Token注入,不要在每一个页面组件里直接axios.get。封装好之后,每个组件里调用会清爽很多。
// utils/request.js import axios from 'axios' import { ElMessage } from 'element-plus' const request = axios.create({ baseURL: '/api', timeout: 8000 }) // 请求拦截器:加Token request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) // 响应拦截器:统一错误提示 request.interceptors.response.use( response => response.data, error => { ElMessage.error(error.response?.data?.message || '网络异常') return Promise.reject(error) } ) export default request统一封装的好处是后期如果要改鉴权方案、加日志上报、处理Token过期自动刷新,只需要动一个文件,所有页面全部生效。
3.2 路由与页面组件设计
路由设计上,这个系统的前端有四个核心页面:首页推荐、景点列表、景点详情、个人中心。
// router/index.js import { createRouter, createWebHistory } from 'vue-router' const router = createRouter({ history: createWebHistory(), routes: [ { path: '/', component: () => import('../views/HomeView.vue') }, { path: '/attractions', component: () => import('../views/ListView.vue') }, { path: '/attraction/:id', component: () => import('../views/DetailView.vue') }, { path: '/recommend', component: () => import('../views/RecommendView.vue') }, { path: '/user', component: () => import('../views/UserView.vue') } ] })路由的懒加载用了动态import,这样首屏不会一次性把所有页面都下载下来,加载速度会快很多。
在景点列表页,核心是筛选条件与请求参数的联动。当用户选择城市、类型、排序方式时,组件内部通过watch监听变化,触发数据重新获取。
<template> <div class="list-page"> <CityFilter :city.sync="query.city" /> <CategoryFilter :category.sync="query.category" /> <SortBar :sort.sync="query.sort" /> <div v-loading="loading" class="attraction-grid"> <AttractionCard v-for="item in list" :key="item.id" :data="item" @click="goDetail(item.id)" /> </div> <el-pagination v-model:current-page="query.page" :page-size="query.pageSize" :total="total" layout="prev, pager, next" /> </div> </template>一个很容易踩的坑:v-for的:key不要用索引下标,必须用唯一ID。因为列表渲染后如果数据顺序变了,Vue靠key做节点复用,用index会导致详情页打开错误的数据。
3.3 接口调用与数据渲染
景点详情页的接口调用,需要重点处理两个问题:URL参数获取和页面加载态。
<script setup> import { ref, onMounted } from 'vue' import { useRoute } from 'vue-router' import { getAttractionDetail, recordView } from '../api/attractions' const route = useRoute() const detail = ref(null) const loading = ref(true) onMounted(async () => { const id = route.params.id try { detail.value = await getAttractionDetail(id) // 浏览行为记录,用于推荐算法 recordView(id) } finally { loading.value = false } }) </script>这里有个细节:recordView是异步操作但不需要等待结果,收藏和浏览记录可以参考埋点的思路,做到“不影响主流程”。如果后期并发大了,可以对行为上报做批量合并,前端攒一批数据然后再发一个接口,减少请求次数,这个后续可以优化。
推荐页面的话,后端返回推荐结果后,前端用卡片流展示。真实场景里用户体验很依赖图片加载速度,所以我在卡片图片上做了懒加载:
<el-image :src="item.cover_url" fit="cover" lazy class="attraction-cover" />图片懒加载看似小细节,但在列表页二三十张图片一次性加载的场景下,首屏速度至少提升一半。
4. 联调、部署与问题排查
4.1 前后端联调与代理配置
前后端分离开发时,联调阶段最常见的问题就是跨域。Django后端默认不开启CORS,Vue前端开发服务器默认跑在5173端口,两者端口不同,必出跨域错。
开发环境最简单的方案是用Vite的代理,把前端请求代理到后端地址,这样浏览器侧看到的请求是同源的,可以完美避开跨域。配置在vite.config.js中:
export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { '/api': { target: 'http://127.0.0.1:8000', changeOrigin: true } } } })我把Axios的baseURL设置成了/api,配合Vite代理转发,开发阶段完全不用管CORS。但如果前端是独立部署的,那就必须在Django里配置跨域了:
# settings.py 中安装 django-cors-headers 后的配置 INSTALLED_APPS = [ # ... 'corsheaders', ] MIDDLEWARE = [ # ... 'corsheaders.middleware.CorsMiddleware', 'django.middleware.common.CommonMiddleware', ] CORS_ALLOW_ALL_ORIGINS = False # 生产环境建议关闭 CORS_ALLOWED_ORIGINS = [ "http://localhost:5173", "https://travel.example.com", ]区分一下:开发阶段用开发代理,生产阶段用CORS白名单,两件事不冲突,都配置好就行。
4.2 PyCharm中的配置与调试
PyCharm是这套开发流程的主力IDE。建议从官网下载社区版,如果你所在的公司或学校有正版授权,可以考虑Professional版。社区版配合插件功能已经能覆盖Vue和Django的主要开发场景。
关于版本的坑,有两点我要专门提醒:
一是Python解释器版本,建议直接用Python 3.9或3.10,不要用太高或太低的版本。Django 4.x要求Python 3.8以上,3.11、3.12也能跑,但个别第三方库的兼容性跟不上。稳妥起见用3.10。
二是PyCharm里跑Django的时候,项目根路径和manage.py路径的配置。很多人启动项目报ModuleNotFoundError,就是因为运行配置里的“Working directory”没有正确指向manage.py所在目录。
配置调试的方法:
- 点击右上角运行配置下拉框,选择“Edit Configurations”
- 新增一个“Django Server”配置,Name随意
- 在“Working directory”里选择后端工程根目录
- 在“Parameters”里填入
runserver 0.0.0.0:8000 - 这样点Debug按钮,可以直接在PyCharm里断点调试Python代码
前端Vue的调试也需要在PyCharm中配置。PyCharm Professional自带Node.js插件,社区版可以通过安装“Vue.js”插件获得Vue文件语法高亮。运行时直接在Terminal里敲npm run dev就够了,开发服务器热更新很好用。
4.3 项目部署要点
Django和Flask的部署,我分别说一下。
Django部署最规范的方式是:Nginx + Gunicorn + Django。Gunicorn是Python的WSGI服务器,比Django自带的runserver稳定得多,适合生产环境。
pip install gunicorn # 启动命令,3个worker进程 gunicorn config.wsgi:application -w 3 -b 127.0.0.1:8000Flask推荐服务部署方式差不多,也可以直接用Gunicorn:
pip install gunicorn gunicorn app:app -w 2 -b 127.0.0.1:5001Flask程序内部,app:app意思是导入app.py文件中的app对象。如果Flask的入口文件名不是app.py,对应调整模块名即可。
数据库方面,开发阶段用SQLite足够,部署上线换成MySQL。Django切数据库很方便,只要改settings.py里的DATABASES配置,然后执行:
python manage.py makemigrations python manage.py migrate但要注意:SQLite和MySQL的字段类型有些差异,比如JSONField在MySQL需要5.7以上,DecimalField精度在不同数据库下行为可能不同。所以建议项目早期就建好生产数据库的迁移,不要拖到上线前再切,不然数据迁移会非常痛苦。
4.4 常见问题速查表
开发这套系统过程中,我把踩过的坑整理成了一张速查表,分享出来供大家参考。
| 问题 | 现象 | 解决方案 |
|---|---|---|
| 中文乱码 | Django返回JSON含中文显示\uXXXX | 配置JSON_AS_ASCII = False或使用JsonResponse的json_dumps_params |
| 跨域请求失败 | Vue请求后端报CORS错误 | 开发环境配置Vite代理,生产配置CORS白名单 |
| 数据库迁移冲突 | makemigrations提示已有同ID迁移文件 | 删除migrations目录下冲突文件(保留000_init.py),重新生成 |
| 端口被占用 | 启动runserver提示Address already in use | Linux执行lsof -i:8000查占用进程并kill |
| ORM误删数据 | 删除景点后收藏全部丢失 | 定义外键时慎重设置on_delete,生产数据先备份 |
| Vue打包后路径错误 | 静态资源404 | vite.config.js中设置base: './' |
| PyCharm终端报ModuleNotFoundError | 没有激活虚拟环境 | 终端里先执行source venv/bin/activate |
| Element Plus按需引入不生效 | 样式缺失 | 检查是否安装了unplugin-vue-components及相关插件 |
| axios请求返回字符串而非对象 | 响应拦截器处理不当 | 检查后端Content-Type是否为application/json |
| Flask接口数据乱码 | 返回中文变??? | Flask设置app.config['JSON_AS_ASCII'] = False或指定UTF-8 |
针对第一个问题再补充一下:Django 3.x之后JsonResponse默认会把中文转成ASCII码返回,浏览器能正常显示,但调试时很难看。建议在settings.py里这样配置:
import json from django.core.serializers.json import DjangoJSONEncoder # 在响应时手动指定 response = JsonResponse(data, json_dumps_params={'ensure_ascii': False})关于PyCharm安装Python库包,有人在热词里问“pycharm怎么安装pandas包”一类的问题。其实不需要在PyCharm里手动安装,直接在Terminal里执行pip install pandas即可,安装完成之后PyCharm会自动识别虚拟环境中的包。如果安装慢,可以使用国内镜像源:
pip install pandas -i https://pypi.tuna.tsinghua.edu.cn/simple5. 项目演进与更多可能性
这个系统的基础版本做完之后,还有几个方向可以继续演进,我这里按照成本从低到高排个序。
先是最容易做的:在现有Flask推荐服务中加入Redis缓存后,再接入定时更新逻辑。比如每天凌晨根据前一天的浏览和收藏记录,用离线任务重新计算热度榜,并把结果预热到Redis,这样推荐接口的响应时间能稳定控制在100ms以内。
然后是推荐算法升级。现有ItemCF没有考虑时间衰减,一个用户三个月前看的景点的权重和三天前看到的完全相同。可以改造评分函数:
def time_decay(days): return 1 / (1 + days * 0.1)将交互时间做指数衰减,近期的行为权重更高,推荐结果会更符合用户当前口味。这是经典推荐系统从“能用”到“好用”的关键一步。
架构层面,如果系统数据量继续增长,Django侧可以做读写分离、接口缓存;Flask推荐服务可以改用FastAPI做异步;前端可以做服务端渲染提升SEO。但这些都是后话,项目初版能把全流程跑通、数据流清晰、代码结构规范,已经胜过很多“技术上堆得高但逻辑一团糟”的系统。
根据我自己的实操经验,做这类全栈项目的核心思路是:先把数据模型和接口约定定清楚,再分配前后端开发任务,联调效率会高很多。我就是一开始急着写前端页面,结果后端接口返参结构反复调整,改了好几次才稳定下来。如果重新做,我会先和后端定一个OpenAPI文档,用Apifox或Swagger把每个接口的入参出参固定住,开发体验会舒服很多。
再分享一个关于推荐效果调优的体会:不要迷信算法,先把数据进行可视化。把用户的浏览、收藏、城市分布做几张图表出来,你会发现推荐不准的原因往往是数据稀疏或者行为采集不全,而不是算法不够高级。把埋点做好、把行为数据记录完整,比把协同过滤做得复杂得多管用。这也是为什么我在上文反复强调UserAction的重要性。
最后建议各位,项目源码一定用Git管理,每个功能模块做完就提交一次。这套系统虽然不大,但Django和Flask之间、后端和前端之间、业务逻辑和推荐逻辑之间,一旦串起来改动难免牵一发动全身。有版本控制兜底,改坏了随时可以回退,这一步省不了。