简介:运用Python与协同过滤算法构建的电影推荐系统,采用Vue实现前后端分离,并集成Django与MySQL,是一套面向计算机相关专业学生、适用于毕业设计与推荐算法入门实践的完整可运行项目。压缩包共688个文件,约13.01MB,主要包含38个Python源码、33个Vue组件、162个JavaScript脚本、CSS样式及SVG图标,另有SQL数据库脚本、Markdown说明文档与论文文档,代码结构覆盖前端页面、后端接口和数据库初始化。目前已有202人学习浏览,项目代码经测试运行成功后才上传,附有安装与运行脚本,功能模块完整,含管理员和用户两种角色。通过该资源可获得完整电影推荐系统源码、数据库文件、论文及配套文档,电影分类、电影信息、评分、评论与收藏等模块均已实现,协同过滤算法逻辑清晰,便于在此基础上修改扩展,也可直接作为课题演示或课设作业。
1. 为什么用协同过滤做电影推荐,还要坚持前后端分离
如果你只是写一个“给用户推荐几部电影”的演示,最省事的做法是后端模板渲染,页面直接塞进一个 SQLite 文件里。但真实业务场景里,电影推荐系统要面对的是用户行为日志、候选集更新、多端共用推荐接口、以及后续接入实时特征,这种情况下“推荐算法”和“展示层”必须解耦。标题里提到的这个项目,本质上是一套完整的工程化教学样板:Python 负责协同过滤算法与推荐接口,Vue 负责交互展示,前后端通过 HTTP/JSON 通信,SQL 文件负责初始化数据,论文和文档说明则把算法选型、系统设计、测试结果串成一条可交付的链路。
很多人拿到这类项目时容易卡在三个地方:协同过滤的相似度矩阵怎么算、推荐接口返回的数据结构如何设计、Vue 怎么处理跨域和 token。这三个问题恰好就是前后端分离架构下的核心实践点。这篇文章直接按这三条线展开,落到可以跑的代码、可以调的参数、以及你真正部署时会踩的坑。适合的目标人群是有 Python 和基础 SQL 经验、想把推荐算法从脚本变成一个 Web 服务的开发者;如果你只是想在简历上写“协同过滤”,那也得先搞清楚 UserCF 和 ItemCF 在工程上的取舍。
2. 协同过滤算法原理与相似度计算选型
2.1 基于用户与基于物品的协同过滤:先决定你的场景
协同过滤的核心假设是:过去有相似偏好的人,未来也会有相似偏好。基于用户的协同过滤(UserCF)先找“与我兴趣相似的用户”,再把那些用户看过而我没看过的电影推荐给我;基于物品的协同过滤(ItemCF)则先找“与我曾看过电影相似的电影”,它的相似不是内容上的相似,而是“被同一批人同时喜欢”的关系。在电影推荐场景中,用户数量通常远远大于电影数量,而且评分/行为的分布非常稀疏。UserCF 需要在每次推荐时实时计算用户之间的相似度,当用户规模达到百万级时,这个计算代价会变得不可接受。ItemCF 可以预先离线计算物品相似度矩阵,把相似关系固化下来,在线推荐时只需要查询目标用户的行为历史对应的 item 邻域即可。所以如果你的系统面向海量注册用户,ItemCF 是更常见的工程选择;如果是课程设计或中小规模实验,UserCF 更直观,也更容易讲清楚算法细节。标题里只写了“协同过滤算法”,没有限定具体变体,下面我会以 UserCF 为主线做完整实现,同时给出切换到 ItemCF 的关键修改点。
2.2 相似度度量:皮尔逊相关系数 vs 余弦相似度
在计算用户相似度之前,得先把用户对电影的评分/行为表示成向量。假设有 5 部电影,用户 A 的评分向量是 [5, 3, 0, 4, 0],用户 B 是 [4, 0, 0, 5, 2],0 表示未看。余弦相似度直接计算两个向量夹角的余弦值,它不关注用户整体评分是偏严还是偏松。皮尔逊相关系数则在余弦相似度基础上减去了用户的平均评分,可以消除“评分尺度”差异。比如 A 习惯打 3-5 分,B 习惯打 1-3 分,虽然他们口味相同,但余弦相似度会被整体分差拉低,皮尔逊则在中心化后能更准确反映偏好趋势。在 MovieLens 这类显式评分数值上,皮尔逊通常表现更好;在点击、收藏、播放这类隐式行为上,数据往往只有 0/1,余弦相似度更常用。下面代码里我会把两种度量都实现出来,通过一个参数切换,方便你在自己的数据上对比效果。
2.3 用 Python 实现用户相似度并生成推荐候选集
import numpy as np import pandas as pd def load_ratings(sql_path='ratings.csv'): # 实际项目中,数据一般从 MySQL 的 user_movie_rating 表读取 # 这里用 CSV 演示,字段与 SQL 表保持一致 df = pd.read_csv(sql_path) return df def normalize_ratings(df): # 按用户减去平均分,消除评分尺度影响 df['mean'] = df.groupby('user_id')['rating'].transform('mean') df['norm'] = df['rating'] - df['mean'] return df def user_similarity_matrix(df, method='pearson'): # 构造 用户-电影 矩阵,行为 user_id,列为 movie_id pivot = df.pivot_table(index='user_id', columns='movie_id', values='norm').fillna(0) if method == 'cosine': # 余弦相似度 denom = np.linalg.norm(pivot.values, axis=1, keepdims=True) sim = (pivot @ pivot.T) / (denom @ denom.T) else: # 皮尔逊相关系数:标准化后的向量点积就是相关系数 sim = pivot @ pivot.T np.fill_diagonal(sim.values, 0) return sim, pivot这段代码的逻辑是:先用transform('mean')把每个用户的评分均值算出来,形成一个新列;再用pivot_table把数据展开成二维矩阵,缺失值填 0。皮尔逊相关系数在中心化之后,点积结果恰好等于相关系数;如果你需要严格的分母归一化,可以再除以向量模长。np.fill_diagonal把用户自己和自己的相似度置为 0,避免推荐结果包含自己。参数说明:method='cosine'时适合隐式反馈数据,method='pearson'适合显式评分。实际装置中,这个矩阵可能非常稀疏,建议先做一次“只看过至少 5 部电影的用户”过滤,否则相似度会被长尾用户干扰。推荐候选集的生成见下面代码:
def recommend_for_user(sim_matrix, norm_df, target_user, top_k_users=10, top_n_movies=10): # 找和目标用户最相似的 K 个用户 sim_scores = sim_matrix.loc[target_user].sort_values(ascending=False).head(top_k_users) # 取这些用户看过但目标用户没看过的电影 target_history = set(norm_df[norm_df['user_id'] == target_user]['movie_id']) candidates = {} for other_user, sim_score in sim_scores.items(): other_history = norm_df[norm_df['user_id'] == other_user] for _, row in other_history.iterrows(): if row['movie_id'] not in target_history: # 加权累加:相似度 * 该用户的偏好程度 candidates[row['movie_id']] = candidates.get(row['movie_id'], 0) + sim_score * row['norm'] ranked = sorted(candidates.items(), key=lambda x: x[1], reverse=True)[:top_n_movies] return [movie_id for movie_id, _ in ranked]推荐分数的累加方式是“相似度乘以该用户对电影的中心化评分”,这比单纯的“统计相似用户看过次数”更细粒度,因为一个 5 分好评和一个 3 分普通评价对推荐的贡献应该不同。top_k_users和top_n_movies是两个最值得调的参数:k 太小,推荐结果受个别相似用户主导;k 太大,边缘用户把分数稀释。经验值是用户规模的 1%~5%,你可以直接做成 API 参数,方便后续调优。
2.4 回到工程:算法模块与 Web 服务如何划分
常见做法是把算法部分封装成一个独立的 Python 包,不直接写在任何路由文件里。比如recommender/user_cf.py负责相似度矩阵计算与推荐候选生成,recommender/item_cf.py负责物品相似度;app.py只负责接收 HTTP 请求,调用算法模块,返回 JSON。这样做的目的是,算法评估时可以单独跑脚本,不依赖 Flask 或 Vue;生产环境里也可以把推荐计算放到 Celery 异步任务里,Web 服务只读 Redis 缓存。下面接口设计中,我会把相似度矩阵的更新设计成“启动时计算 + 定时缓存”,不在每个请求里重新计算。
3. 使用 Flask 实现前后端分离推荐接口
3.1 为什么选 Flask 而不是 FastAPI 或 Django
在“前后端分离”这个项目里,你需要的是一个轻量、能快速暴露 JSON 接口、同时允许你自由组织算法代码的 Web 框架。Django 自带 ORM 和 Admin,但重量级较重;FastAPI 支持异步和类型提示,性能更好,但很多教程里给出的协同过滤代码都是同步的,FastAPI 的优势并不能充分发挥。Flask 的优势在于“零约束”:你可以在同一个进程里既算矩阵又提供接口,也可以把矩阵计算拆出去。对于课程设计或中小型推荐系统,Flask 是最稳妥的选择。如果你确实想用 FastAPI,后面的路由和响应结构几乎不用改,只需把装饰器替换为@app.get和@app.post,再加上 Pydantic 模型做参数校验即可。
3.2 定义推荐接口的数据契约
前后端分离最重要的一件事就是先定好 JSON 数据结构,再写具体代码。下面给出一个推荐的电影接口响应示例:
{ "code": 0, "message": "success", "data": { "user_id": 1, "items": [ { "movie_id": 101, "title": "肖申克的救赎", "score": 4.8, "reason": "与你相似的用户 17, 23 看过" } ] } }这里用code: 0表示成功,非 0 为错误码。reason字段给前端展示“推荐理由”用,既能让页面显得更有说服力,又方便你调试推荐结果是否符合直觉。前端拿到这个 JSON 后直接渲染卡片,不需要知道后端是怎么算的。接口路径设计我一般这样约定:GET /api/recommend/<int:user_id>获取推荐;GET /api/movies/<int:movie_id>获取电影详情;POST /api/rate提交用户评分。这样实现 Vue 路由时可以直接映射。
3.3 用 Flask 暴露协同过滤结果
from flask import Flask, jsonify, request from flask_cors import CORS from recommender.user_cf import load_ratings, user_similarity_matrix, recommend_for_user import pandas as pd app = Flask(__name__) CORS(app, resources={r"/api/*": {"origins": "*"}}) # 启动时加载并计算相似度矩阵 RATINGS_DF = load_ratings('data/ratings.csv') RATINGS_DF = RATINGS_DF[RATINGS_DF.groupby('user_id')['user_id'].transform('size') > 5] SIM_MATRIX, PIVOT = user_similarity_matrix(RATINGS_DF, method='pearson') @app.route('/api/recommend/<int:user_id>', methods=['GET']) def recommend(user_id): if user_id not in SIM_MATRIX.index: return jsonify({'code': 1, 'message': 'user not found', 'data': None}), 404 movie_ids = recommend_for_user(SIM_MATRIX, RATINGS_DF, user_id) movies = [] for mid in movie_ids: movies.append({'movie_id': mid, 'title': f'movie_{mid}', 'score': 0.0}) return jsonify({'code': 0, 'message': 'success', 'data': {'user_id': user_id, 'items': movies}}) @app.route('/api/rate', methods=['POST']) def rate_movie(): payload = request.get_json() user_id = payload.get('user_id') movie_id = payload.get('movie_id') rating = payload.get('rating') if not all([user_id, movie_id, rating]): return jsonify({'code': 2, 'message': 'invalid params', 'data': None}), 400 # 实际项目中这里写入 MySQL,并触发缓存失效 return jsonify({'code': 0, 'message': 'success', 'data': None}) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=False)CORS(app, resources={...})配置了允许跨域访问。开发时 Vue 跑在localhost:8080,Flask 跑在localhost:5000,如果不配置 CORS,浏览器会直接拦截请求。注意我把相似度矩阵放在模块全局变量里,每次启动只算一次;但用户新增评分后矩阵不会更新,所以实际的系统里应该在rate_movie接口写库后删除 Redis 缓存,让后台任务重算矩阵。这里没有使用debug=True,因为 Debug 模式会启用源码热加载,在推荐场景中会让你每次请求都重新检查代码而影响性能。
3.4 相似度矩阵更新与性能瓶颈
当用户数到达上万时,user_similarity_matrix里的点积计算需要的内存和耗时都会剧增。一种缓解办法是计算相似矩阵前先过滤掉行为少于 5 条的用户,上面代码已经做了;另一种更工业化的思路是使用 Spark 或 Faiss 做近邻搜索,但在这个项目里没必要。你可以把计算好的矩阵用np.save或pandas.to_pickle存到本地文件,启动时打个时间戳检查是否有新评分,如果没有新数据就直接加载文件。推荐接口本身要控制在 200ms 内,如果超过了,优先检查相似度矩阵计算是否泄漏到请求路径中。
4. Vue 前端实现电影推荐展示与前后端联调
4.1 Vue 项目初始化与代理配置
前端部分用 Vue 3 与 Vite 是当前最常见的组合,但如果你是照着传统的课程设计在做,Vue 2 + Element UI 仍然可以跑。这里我按 Vue 3 + Vite 来写,组件结构与 Vue 2 差别不大。创建项目的命令是:
npm create vite@latest movie-front -- --template vue cd movie-front npm install npm install axios element-plus安装完成后,在项目根目录创建一个.env.development文件写入VITE_API_BASE_URL=/api。Vite 的开发服务器需要配置代理,让/api开头的请求转发到 Flask 端口。这样 Vue 前端里可以直接写axios.get('/recommend/1'),不会出现跨域报错。配置文件vite.config.js关键部分如下:
import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], server: { port: 8080, proxy: { '/api': { target: 'http://localhost:5000', changeOrigin: true, // 后端接口本身就没有 /api 前缀时,可以做重写 // rewrite: (path) => path.replace(/^\/api/, '') } } } })这段配置让 Vite 在开发阶段把/api/recommend/1代理到http://localhost:5000/api/recommend/1。注意如果 Flask 里路由写的是/api/recommend/<id>,就不需要 rewrite;如果 Flask 写的是/recommend/<id>,你就需要把 rewrite 打开。很多前后端分离联调失败,就是因为少配了代理或没有理解代理的转发路径。
4.2 Vue 页面调用推荐接口并处理 token
实际的项目标题里提到“前后端分离 + SQL 文件”,但并没有特别写“登录”。实际系统一般会带一个简单登录注册,这时候就需要在 Vue 里处理 token。常见做法是用户登录后后端返回 JWT,前端存到 localStorage,每次请求通过 axios 拦截器把 token 放到请求头中。
import axios from 'axios' import { ElMessage } from 'element-plus' const service = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 10000 }) service.interceptors.request.use(config => { const token = localStorage.getItem('access_token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) service.interceptors.response.use( response => { const res = response.data if (res.code !== 0) { ElMessage.error(res.message) return Promise.reject(new Error(res.message)) } return res }, error => { ElMessage.error('网络异常') return Promise.reject(error) } )这段代码在请求发起前读取 localStorage 中的access_token;响应拦截器根据code字段判断业务是否成功。这样recommend页面组件中直接调用service.get('/recommend/1')即可。这个 axios 封装写好后,整个项目所有页面都复用一套 token 处理逻辑,比在每个组件里手动写Authorization头干净得多。如果你后端使用的是 Flask-JWT-Extended,返回的通常包含access_token与refresh_token,前端在 401 时需要用 refresh_token 刷新令牌,这里不做展开,但你需要知道在拦截器里 401 分支写刷新逻辑。
4.3 电影推荐卡片组件的实现
Vue 页面结构通常分成:顶部导航、推荐列表、评分弹窗。下面是最核心的一个推荐列表组件:
<template> <div class="movie-recommend"> <el-row :gutter="16"> <el-col :span="6" v-for="item in movieList" :key="item.movie_id"> <el-card> <h4>{{ item.title }}</h4> <p>推荐分:{{ item.score.toFixed(2) }}</p> <p class="reason">{{ item.reason }}</p> <el-button type="primary" @click="handleRate(item)">评分</el-button> </el-card> </el-col> </el-row> </div> </template> <script setup> import { ref, onMounted } from 'vue' import { service } from '../utils/request' const movieList = ref([]) async function loadRecommend(userId) { const res = await service.get(`/recommend/${userId}`) movieList.value = res.data.items } function handleRate(item) { // 打开评分弹窗,提交后刷新列表 console.log('rate movie', item.movie_id) } onMounted(() => { const userId = localStorage.getItem('user_id') || 1 loadRecommend(userId) }) </script>这里使用的是 Vue 3 的<script setup>语法。注意item.score.toFixed(2)假设 score 是数字类型,后端返回时不要把它序列化成字符串。播放 m3u8 之类的热词跟本场景没关系,不需要在电影推荐界面硬凑视频播放功能;如果你是做电影网站,想要放映室资源,可以在电影详情页单独加一个 video 组件,但不要影响推荐主流程。
4.4 跨域与 token 的常见坑
开发和部署环境里最容易翻车的是三件事:第一,代理配了但后端 CORS 也开着,导致重复的跨域头;第二,token 放在 localStorage 里存在 XSS 风险,但你需要在论文里写清楚它的利弊;第三,前端打包后放在 Nginx 中,Nginx 没有把/api的请求反向代理到 Flask,页面能打开但数据加载不出来。下面是生产环境常见 Nginx 配置片段:
server { listen 80; server_name your-domain.com; root /var/www/movie-front/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:5000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }try_files $uri $uri/ /index.html这一行是 Vue Router 的 history 模式必需配置,否则刷新页面时会 404。proxy_pass末尾带不带/含义不同:http://127.0.0.1:5000;会把原始 URI 完整转发,而http://127.0.0.1:5000/;会去掉/api前缀。你要根据自己的后端路由决定,否则会出现路径多一段或少一段的问题。
5. SQL 文件结构、数据初始化与推荐效果验证
5.1 SQL 文件里需要哪几张表
一个电影推荐系统的 SQL 文件至少要包含用户表、电影表、评分表。如果是基于 Tag 的冷启动策略,还可以加电影类型表和用户偏好表。下面给出 MySQL 建表语句的骨架:
CREATE DATABASE IF NOT EXISTS movie_db DEFAULT CHARSET utf8mb4; USE movie_db; CREATE TABLE t_user ( user_id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password_hash VARCHAR(200) NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB; CREATE TABLE t_movie ( movie_id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(200) NOT NULL, genres VARCHAR(200), release_year INT, avg_score DECIMAL(3,1) DEFAULT 0.0 ) ENGINE=InnoDB; CREATE TABLE t_rating ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, movie_id INT NOT NULL, rating TINYINT NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_user_movie (user_id, movie_id), CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES t_user(user_id), CONSTRAINT fk_movie FOREIGN KEY (movie_id) REFERENCES t_movie(movie_id) ) ENGINE=InnoDB;你需要把用户评分动作从算法演示扩展为一个真实的表。UNIQUE KEY uk_user_movie (user_id, movie_id)非常重要,它保证一个用户对同一部电影只能有一条评分记录,后端的INSERT ... ON DUPLICATE KEY UPDATE可以直接复用。TINYINT适合 1-5 分的评分范围,DECIMAL(3,1)用来缓存平均分。索引方面,评分表需要建立(user_id)和(movie_id)的普通索引,否则推荐算法读取数据时会发生全表扫描,在几十万条评分记录上就没法看了。
5.2 用 SQL 文件生成初始化评分数据
实际项目中,SQL 文件里除了建表语句,还需要准备一部分模拟数据,才能让前后端一启动就有推荐效果。Twitter 或 GitHub 上常见的处理方式是导入 MovieLens 的 ratings.csv 后,用LOAD DATA INFILE导入到 t_rating 中。但课程设计里往往要求直接提供一个movies.sql,可以用下面这样的方式构造:
INSERT INTO t_movie (movie_id, title, genres, release_year) VALUES (1, '肖申克的救赎', 'Drama', 1994), (2, '这个杀手不太冷', 'Action|Crime|Drama', 1994), (3, '阿甘正传', 'Drama|Romance', 1994); INSERT INTO t_rating (user_id, movie_id, rating) VALUES (1, 1, 5), (1, 2, 4), (2, 1, 4), (2, 3, 5), (3, 2, 5), (3, 3, 4);这样构造数据的好处是可以直接人工验算推荐结果。比如用户 1 看过了电影 1 和 2,UserCF 找到与它相似的用户 2,因为用户 2 给电影 3 打了 5 分,系统就会把电影 3 推荐给用户 1。这样的推荐结果在论文里解释时也更通顺,你能画一张简单的表格说明每个相似用户对候选电影的贡献分怎么出来的。注意 SQL 文件里的评分分布要尽量模拟长尾现象:少数热门电影有很多人评,大多数冷门电影只有零星评分,否则推荐算法就没太大发挥空间。
5.3 后端如何读取 MySQL 数据而不是 CSV
前面的 Python 代码演示用了read_csv,真正接 MySQL 时需要改为 SQLAlchemy 读库。你可以在database.py里封装一个:
from sqlalchemy import create_engine, text import pandas as pd DB_URI = 'mysql+pymysql://root:password@127.0.0.1:3306/movie_db?charset=utf8mb4' engine = create_engine(DB_URI) def load_ratings_from_db(): query = text(""" SELECT user_id, movie_id, rating FROM t_rating WHERE rating IS NOT NULL """) return pd.read_sql(query, engine)如果你的 SQL 文件里存储的是 1-5 分,rating列可以直接使用;如果是隐式行为(0/1),你要在加载时决定是否把 1 转换成分数。参数说明:create_engine的第一个参数是连接串,pymysql是 Python 连接 MySQL 的驱动,需要pip install pymysql sqlalchemy。使用text()查询能避免 SQL 注入风险,但这里的查询条件全部是固定的,没有用户参数,所以不需要额外做参数化绑定。
5.4 验证推荐系统效果的两个常用指标
如果你只是把推荐结果展示出来,论文里还可以附上离线评估:把评分数据集按 8:2 切分,训练集计算相似度矩阵,测试集里移除每个用户最近的几条行为,用推荐排序是否覆盖被移除的电影来评估。通常用 Precision@K 和 Recall@K 这两个指标。下面单独写一段评估数据和推荐排序的比对逻辑:
def precision_recall_k(predicted, held_out, k=10): recommended = set(predicted[:k]) real = set(held_out) inter = recommended & real precision = len(inter) / k recall = len(inter) / len(real) if len(real) > 0 else 0 return precision, recall当k增大时,召回率一般会上升,精确率下降。如果你看到精确率一直很低,先检查是不是用户行为过于稀疏,或者冷门电影根本没有机会被推荐。这时候一个常见补救办法是给相似度矩阵做“热门商品降权”,也叫做 Inverse User Frequency,即在计算相似度时给热门电影赋予更低的权重。你可以在算法模块里加一个参数popularity_penalty,把热门电影的中心化评分乘以一个小于 1 的系数。这个调参过程可以写进论文的实验章节,比堆各种深度学习模型要务实得多。
5.5 让 SQL、推荐、Vue 在一条链路上跑通
在一台开发机上把完整的项目拉起来,步骤是这样:先执行movie_db.sql初始化数据库;然后编辑database.py里的数据库账号和密码;运行python app.py,看到 Flask 启动日志;再npm run dev启动 Vue;浏览器打开http://localhost:8080。如果页面报错,按顺序查四层:第一层看浏览器 Network 里请求返回的 HTTP 状态码是 401 还是 500;第二层看 Flask 控制台有没有异常堆栈;第三层看数据库是否能连上,执行SELECT COUNT(*) FROM t_rating;;第四层看 Vite 代理有没有生效,直接在浏览器地址栏输入http://localhost:8080/api/recommend/1,如果这里返回的是index.html,说明代理没生效。最后一层往往最容易忽略:Vue Router 会把/api/recommend/1当成前端路由处理,必须确保代理规则在前面优先匹配。另外再补充一个约定:项目里的文档说明和论文应该把 SQL 文件提交进来,而不是只贴部分建表语句,这样你拿到项目后可以一次性复现数据环境,省掉很多手工造数据的时间。当你把相似度计算、Flask 接口、Vue 展示、MySQL 存储串联起来之后,再回头改任何一个环节的细节,比如把 UserCF 换成 ItemCF,或者把相似度从皮尔逊换成余弦,就只动半个算法文件,剩下的接口和前端完全可以复用。这套“算法逻辑独立、接口契约固定、前端只管渲染”的架构,才是标题中前后端分离真正想要传达的工程价值。
本文还有配套的精品资源,点击获取