新能源汽车推荐系统 - 亲手搭的全链路实践
写这套项目时,我想把"看车、选车、对比、收藏、点评"这一整条链路串起来,让它既能跑通功能,又方便大家落地和二次改造。下面按个人实际体验带大家过一遍。
项目概览
- 前端:React + Vite + TypeScript + Tailwind。组件化拆分卡片、详情、对比、偏好引导、后台面板,样式轻量又易改。
- 后端:Flask + SQLAlchemy + Flask-Migrate + MySQL,JWT 鉴权。(auth/users/vehicles/recommend/favorites/reviews/history/compare/admin/media),接口清晰。
- 媒体:本地静态目录
car-backend/media+/media/<path>路由,序列化时返回绝对 URL,前端拿来即用。 - 推荐与行为:偏好驱动 + 热门/相似推荐;收藏、对比(限 4 辆)、评分/评论、浏览历史全链路打通。
- 数据:车辆表支持
image_url+images多图 banner。我抓了多品牌的实车风景图并写入数据库,详情页一进来就有大图氛围感。
推荐算法与应用
- 热门推荐(未登录兜底):先给你看大家都在看的,排序参考浏览量 + 评分权重(示例:
score = rating_avg * log(review_count + 1) + view_count),保证游客也有好内容。 - 偏好驱动(登录态):你填了预算/品牌/车型/续航,我们就按"离你最近"的顺序给;如果你没填偏好,就回退热门,不会让页面空着。
- 相似推荐(详情页):点进一辆车,右侧推和它"气质相近"的,主要看车型类型、价格差、续航差做相似度,方便横向对比。
- 行为信号(可扩展):收藏/浏览/评分可以做协同过滤,目前先用规则 + 偏好,后续想接入 CF 也很容易(recommend 模块是独立的)。
- 数据闭环:浏览会累计,评分/评论提升互动,收藏/对比沉淀偏好;后台能看到热门和评分分布,用来调权重、做 A/B 时很直观。
系统展示
-
首页/推荐 :卡片列表 + 筛选区,展示品牌、价格、续航、评分、浏览量。可截图对比"未登录热门"与"登录后个性化"。

-
车辆详情:多图 banner(含风景大图)、价格/续航/加速/时速/座位、评分、收藏、对比、评论、点赞。可截主图和缩略图条。


- 对比页 :最多 4 辆车的参数并排,包括价格/续航/加速/电池/座位等,适合做横向对比截图。


- 收藏与历史:列表/卡片式展示,能看到最近浏览和收藏的车。

- 管理后台:概览数据(用户数/车辆数/评论数/浏览量)、热门车辆排行榜、评分分布、车型分布,车辆/用户/评论管理。适合截仪表盘和表格。

推荐系统核心代码
- 后端媒体路由:本地
/media直出,安全校验避免越权。
python
# car-backend/app/blueprints/media.py
@bp.get("/media/<path:filename>")
def serve_media(filename: str):
media_root = Path(current_app.config["MEDIA_ROOT"]).resolve()
requested = (media_root / filename).resolve()
if not requested.exists() or not requested.is_relative_to(media_root):
abort(404)
rel_path = requested.relative_to(media_root)
return send_from_directory(str(media_root), rel_path.as_posix())
- 推荐相似接口:按车型/价格/续航相似度排序。
python
# car-backend/app/blueprints/recommend.py
@bp.get("/similar/<int:vehicle_id>")
def similar(vehicle_id: int):
target = Vehicle.query.get_or_404(vehicle_id)
candidates = Vehicle.query.filter(Vehicle.id != vehicle_id, Vehicle.status == "active")
vehicles = sorted(
candidates,
key=lambda v: (
0 if v.vehicle_type == target.vehicle_type else 1,
abs(v.price - target.price),
abs(v.range_km - target.range_km),
),
)
return success([vehicle_to_dict(v) for v in vehicles])
- 推荐接口(热门 + 偏好):未登录按热门,登录后按预算/品牌/车型/续航综合打分。
python
# car-backend/app/blueprints/recommend.py
@bp.get("/hot")
def hot():
vehicles = Vehicle.query.filter_by(status="active")\
.order_by(desc(Vehicle.view_count)).limit(8).all()
return success([vehicle_to_dict(v) for v in vehicles])
@bp.get("/personal")
@jwt_required(optional=True)
def personal():
ident = get_jwt_identity()
user_id = int(ident) if ident else None
query = Vehicle.query.filter_by(status="active")
if not user_id:
vehicles = query.order_by(desc(Vehicle.rating_avg * Vehicle.rating_count + Vehicle.view_count)).limit(6).all()
return success([vehicle_to_dict(v) for v in vehicles])
pref = UserPreference.query.filter_by(user_id=user_id).first()
if not pref:
vehicles = query.order_by(desc(Vehicle.rating_avg * Vehicle.rating_count + Vehicle.view_count)).limit(6).all()
return success([vehicle_to_dict(v) for v in vehicles])
scored = []
for v in query.all():
score = 0
if pref.budget_min <= v.price <= pref.budget_max:
score += 40
else:
diff = min(abs(v.price - pref.budget_min), abs(v.price - pref.budget_max))
score += max(0, 40 - (diff / 50000) * 10)
if pref.preferred_brands and v.brand in pref.preferred_brands:
score += 20
if pref.vehicle_types and v.vehicle_type in pref.vehicle_types:
score += 20
score += 15 if v.range_km >= pref.range_requirement else (v.range_km / pref.range_requirement) * 15
score += (float(v.rating_avg) / 5 if v.rating_avg else 0) * 5
scored.append((score, v))
scored.sort(key=lambda x: x[0], reverse=True)
return success([vehicle_to_dict(v) for _, v in scored[:6]])
- 前端车辆详情多图:
images数组 + 缩略图/全屏浏览。
tsx
// car-front/src/components/ImageGallery.tsx
<div className="relative aspect-[4/3] rounded-2xl overflow-hidden bg-gray-100 group">
<img src={images[currentIndex]} onClick={() => setIsFullscreen(true)} className="w-full h-full object-cover" />
{/* 左右箭头 + 计数 */}
</div>
{isFullscreen && (
<div className="fixed inset-0 bg-black z-50 flex items-center justify-center">
<img src={images[currentIndex]} className="max-w-full max-h-[90vh] object-contain" />
{/* 缩略条 + 翻页按钮 */}
</div>
)}
快速启动项目的脚本
bash
# 1) 一键脚本(推荐)
./start.sh start
# 默认:后端 http://localhost:5555,前端 http://localhost:3000
# 2) 手动(如需)
cd car-backend && python3 -m venv venv && source venv/bin/activate
pip install -r requirements.txt
flask db upgrade
FLASK_APP=run.py flask run --host=0.0.0.0 --port=5555
cd ../car-front && npm install && npm run dev -- --host --port 3000
实测下来,一键脚本就能把 venv、依赖、迁移、端口都搞定;如果你想分步调试,手动流程也很顺畅。
目录与重要文件
car-front/src:前端源代码(组件、样式、API 封装)。car-backend/app:后端、模型、序列化、配置。car-backend/media/vehicles:车辆图片存储(已含多张风景实拍图)。start.sh:安装依赖 + 启动前后端的便捷脚本。
API 与数据约定
- 路由前缀
/api,统一响应{ code, message, data }。 - 车辆字段:
imageUrl(主封面),images(数组,含主封面 + 额外图),price单位元,rangekm。 - 媒体访问:直接使用后端返回的绝对 URL,或手动拼接
/media/vehicles/xxx.jpg。
配置与扩展
- 数据库:环境变量
DB_USER/DB_PASSWORD/DB_HOST/DB_PORT/DB_NAME。 - 媒体目录:
MEDIA_ROOT可改为挂载盘/CDN 同步目录;MEDIA_URL_PATH可自定义前缀。 - 安全:JWT 鉴权,管理员路由校验;生产建议收紧 CORS、加限流/反代静态。
- 性能:车辆查询索引(品牌/车型/价格/续航/类型);热门/推荐可加短缓存;媒体可前置 CDN/Nginx。
写在最后
- 这套代码更偏"能跑、易改、易扩展",你可以直接用,也可以把推荐/样式/数据源替换成自己的。
- 有反馈或想法欢迎留言交流,大家一起把"看车选车"这件小事做得更顺手。