一、项目简介
旅游平台常见的痛点是信息多、选择难:用户需要在城市、预算、旅行天数、主题偏好之间反复比较。本文实现一个可运行的 Python 全栈项目--智能旅游推荐系统,通过用户输入的目的地、主题、季节、预算和天数,结合用户收藏行为与相似用户偏好,自动推荐合适景点,并支持收藏与行程保存。
项目不是单纯的推荐算法 Demo,而是完整的全栈应用:后端提供注册登录、Token 鉴权、景点库、协同过滤推荐、收藏、行程接口;前端使用 Vue 3 构建交互页面;数据库使用 SQLite 存储用户、景点、收藏和行程数据。
二、技术栈
- 后端:FastAPI、SQLAlchemy、Pydantic、Passlib
- 数据库:SQLite
- 前端:Vue 3、Vite、Fetch API
- 认证:密码哈希 + Bearer Token
- 部署运行:Uvicorn + Vite Dev Server
前端主技术栈为 Vue 3/Vite,适合扩展为组件化单页应用。
三、系统架构
text
用户浏览器
│
│ Vue 3 页面:注册、登录、推荐、收藏、行程
▼
FastAPI REST API
│
├── auth.py:密码哈希、Token 创建、鉴权依赖
├── crud.py:用户、景点、推荐、收藏、行程业务逻辑
├── schemas.py:请求和响应结构
└── models.py:SQLAlchemy ORM 模型
▼
SQLite 数据库 travel.db
系统采用前后端分离架构。前端登录成功后把 Token 保存到 localStorage,后续访问推荐、收藏、行程接口时通过 Authorization: Bearer <token> 请求头完成鉴权。
四、功能模块
-
用户模块
- 用户注册
- 用户登录
- 登录状态恢复
- 退出登录
- Token 鉴权
-
景点库模块
- 初始化示例景点数据
- 按城市查询
- 按关键词搜索景点名称、主题和标签
-
智能推荐模块
- 支持城市、主题、季节、预算、旅行天数
- 基于用户收藏行为构建用户-景点交互数据
- 使用用户-用户协同过滤挖掘相似用户偏好
- 结合景点主题、城市、标签做物品相似度补充
- 支持城市、主题、季节、预算和游玩时长约束
- 推荐接口需要登录后访问
-
收藏模块
- 登录用户收藏景点
- 查询个人收藏列表
-
行程模块
- 将推荐结果保存为个人行程
- 查询个人行程列表
五、数据库/数据模型设计
系统核心数据表如下:
| 表名 | 说明 | 关键字段 |
|---|---|---|
| users | 用户表 | username、email、password_hash |
| tokens | 登录令牌表 | token、user_id、created_at |
| attractions | 景点表 | name、city、theme、tags、price、rating |
| favorites | 收藏表 | user_id、attraction_id |
| itineraries | 行程表 | user_id、title、city、days、budget、attraction_ids |
models.py 中定义了 ORM 模型,例如景点模型:
python
class Attraction(Base):
__tablename__ = "attractions"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(100), index=True, nullable=False)
city = Column(String(50), index=True, nullable=False)
theme = Column(String(80), index=True, nullable=False)
tags = Column(String(255), default="")
season = Column(String(80), default="四季")
price = Column(Float, default=0)
rating = Column(Float, default=4.0)
duration_hours = Column(Float, default=2.0)
description = Column(Text, default="")
六、后端接口设计
| 方法 | 路径 | 鉴权 | 说明 |
|---|---|---|---|
| POST | /api/auth/register |
否 | 注册并返回 Token |
| POST | /api/auth/login |
否 | 登录并返回 Token |
| GET | /api/auth/me |
是 | 获取当前用户 |
| POST | /api/auth/logout |
是 | 退出登录 |
| GET | /api/attractions |
否 | 景点查询 |
| POST | /api/recommendations |
是 | 生成个性化推荐 |
| GET | /api/favorites |
是 | 获取收藏 |
| POST | /api/favorites |
是 | 添加收藏 |
| GET | /api/itineraries |
是 | 获取行程 |
| POST | /api/itineraries |
是 | 保存行程 |
鉴权依赖位于 auth.py:
python
def get_current_user(authorization: str = Header(default=""), db: Session = Depends(get_db)):
if not authorization.startswith("Bearer "):
raise HTTPException(status_code=401, detail="缺少认证令牌")
raw_token = authorization.replace("Bearer ", "", 1).strip()
token = db.query(models.Token).filter(models.Token.token == raw_token).first()
if not token:
raise HTTPException(status_code=401, detail="令牌无效或已退出登录")
return db.query(models.User).filter(models.User.id == token.user_id).first()
七、前端页面设计
前端采用 Vue 3 单页应用结构:
- 登录/注册卡片:支持用户认证与登录状态展示。
- 推荐条件表单:填写城市、主题、季节、预算、天数。
- 推荐结果区:卡片式展示推荐景点,可一键收藏。
- 景点库区:支持关键词搜索。
- 我的行程区:登录后展示个人保存的行程。
API 请求统一封装在 frontend/src/api.js 中:
javascript
async function request(path, options = {}) {
const headers = { 'Content-Type': 'application/json', ...(options.headers || {}) }
const token = getToken()
if (token) headers.Authorization = `Bearer ${token}`
const res = await fetch(`${API_BASE}${path}`, { ...options, headers })
const data = await res.json().catch(() => ({}))
if (!res.ok) throw new Error(data.detail || '请求失败')
return data
}
八、核心代码讲解
1. 密码哈希与 Token 登录
系统不保存明文密码,而是使用 Passlib 对密码进行哈希:
python
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def hash_password(password: str) -> str:
return pwd_context.hash(password)
def verify_password(password: str, password_hash: str) -> bool:
return pwd_context.verify(password, password_hash)
登录成功后创建随机 Token,并写入数据库。前端保存 Token,后续访问受保护接口时自动携带。
2. 协同过滤推荐算法
推荐算法位于 crud.py。将用户收藏记录视为隐式反馈,构建"用户-景点"交互矩阵,再通过 Jaccard 相似度计算用户之间的相似程度,优先推荐相似用户收藏过、当前用户尚未收藏的景点。同时,系统会结合景点主题、城市和标签计算物品相似度,用来缓解示例数据量较小时的冷启动和稀疏问题:
python
def recommend(db: Session, query: schemas.RecommendQuery, user_id: int):
favorites = db.query(models.Favorite).all()
user_favorites = {}
popularity = {}
for favorite in favorites:
user_favorites.setdefault(favorite.user_id, set()).add(favorite.attraction_id)
popularity[favorite.attraction_id] = popularity.get(favorite.attraction_id, 0) + 1
current_liked = user_favorites.get(user_id, set())
similar_users = {}
for other_user_id, other_liked in user_favorites.items():
if other_user_id == user_id:
continue
similarity = _jaccard(current_liked, other_liked)
if similarity > 0:
similar_users[other_user_id] = similarity
随后系统会对候选景点计算综合得分:相似用户贡献越高、与当前用户已收藏景点越相似、全站收藏热度越高,排名越靠前。最后再根据城市、季节、预算和每日可游玩时长过滤结果,返回最适合的景点组合。对于没有收藏记录的新用户,系统会自动使用全站收藏热度、景点评分和查询条件作为冷启动兜底。
3. Vue 登录状态处理
前端启动时会尝试读取本地 Token 并请求 /api/auth/me:
javascript
async function restoreSession() {
if (!getToken()) return
try {
user.value = await api.me()
await loadItineraries()
} catch {
clearToken()
}
}
如果 Token 无效,则清除本地登录状态,避免前端显示错误用户信息。
九、部署与运行步骤
项目代码位于 project/ 目录。
1. 启动后端
bash
cd project/backend
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
默认后端地址:http://127.0.0.1:8000。首次启动会自动创建 SQLite 数据库并写入示例景点。
2. 启动前端
bash
cd project/frontend
npm install
npm run dev
默认 Vite 地址通常是 http://127.0.0.1:5173。如果后端地址不同,可设置:
bash
export VITE_API_BASE=http://127.0.0.1:8000
npm run dev
3. 体验流程
- 注册新用户或使用页面默认示例信息注册。
- 登录后选择城市、主题偏好、旅行季节、预算和天数。
- 点击"生成智能推荐"。
- 收藏喜欢的景点。
- 将当前推荐结果保存为行程。
十、项目总结
本文完成了一个围绕旅游场景的 Python 全栈项目:后端使用 FastAPI 提供 REST API,SQLite 存储核心数据,Vue 3/Vite 构建前端页面,并实现了完整的用户注册、登录、Token 鉴权、协同过滤景点推荐、收藏和行程管理。
这个项目适合作为全栈练习模板继续扩展,例如:
- 接入真实景点和地图数据;
- 扩展矩阵分解、向量召回或深度学习推荐模型;
- 增加酒店、交通、美食模块;
- 增加管理员后台维护景点库;
- 使用 Docker Compose 部署前后端。
通过该项目,可以系统掌握 FastAPI 后端接口、SQLite 数据建模、Vue 3 前端状态管理以及前后端认证协作的完整开发流程。