项目简介
本项目是一个完整的外卖点餐系统,采用前后端分离架构:
- 后端:Spring Boot 3.2.5 + MyBatis-Plus + Redis + RabbitMQ + Neo4j + 支付宝沙箱
- 前端:Vue 3 + Element Plus + Pinia + Vite
核心业务覆盖:用户注册登录、菜品浏览、购物车管理、下单结算、支付宝支付、订单状态实时同步、支付成功邮件通知等完整闭环。系统实现了基于用户行为的个性化推荐、基于图数据库的购物车共现推荐、以及基于消息队列的全链路异步解耦。
核心技术亮点
一、RabbitMQ 异步消息架构
系统使用 RabbitMQ 实现了 5 大业务场景的异步解耦,构建了完整的消息驱动架构。
1. Exchange / Queue 全景
| Exchange(交换机) | Queue(队列) | Routing Key | 业务场景 |
|---|---|---|---|
login_code_exchange |
login_code_mail |
login.code.mail |
登录验证码异步邮件发送 |
order_pay_exchange |
order_pay_queue |
order.pay |
支付成功异步邮件通知 |
order_delay_exchange |
order_delay_queue |
order.delay |
订单超时延迟取消(TTL + 死信) |
order_cancel_exchange |
order_cancel_queue |
order.cancel |
超时订单实际取消执行 |
behavior_exchange |
user_behavior_queue |
behavior.save |
用户行为异步采集与偏好更新 |
2. 消息流转机制
┌─────────────────────────────────────────────────────────────────────┐
│ RabbitMQ 消息流转全景 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 【用户行为采集链路】 │
│ 前端行为上报 → BehaviorController → BehaviorServiceImpl │
│ ↓ │
│ behavior_exchange ──behavior.save──→ user_behavior_queue │
│ ↓ │
│ UserBehaviorConsumer 消费: │
│ Step1: 写入 user_behavior_log 行为日志 │
│ Step2: 更新 user_food_preference 偏好分值(加权累加) │
│ Step3: 清除 Redis 推荐缓存 │
│ ↓ │
│ ⚡ 降级策略:MQ不可用时自动降级为同步处理 │
│ │
│ 【订单超时取消链路(TTL + 死信队列)】 │
│ 下单成功 → 发送 OrderDelayMessage(30min TTL) │
│ ↓ │
│ order_delay_queue (TTL=1800s, 死信转发至 order_cancel_exchange) │
│ ↓ 30分钟后 │
│ 若订单未支付 → 死信路由到 order_cancel_queue │
│ ↓ │
│ OrderCancelConsumer 消费 → 自动取消订单(status=3) │
│ │
│ 【支付成功通知链路】 │
│ 支付宝回调 → OrderPayProducer → order_pay_queue │
│ ↓ │
│ OrderPayConsumer 消费 → 构建 HTML 邮件 → 发送给用户 │
│ │
│ 【验证码邮件链路】 │
│ 登录/注册 → CodeMailConsumer → 异步发送验证码邮件 │
│ │
└─────────────────────────────────────────────────────────────────────┘
3. 降级容错机制
| 链路 | 降级策略 | 说明 |
|---|---|---|
| 行为上报 | MQ不可用 → 同步处理 | 直接在当前线程执行日志写入+偏好更新+缓存清除 |
| 邮件发送 | MQ投递失败 → 同步发送 | 保证邮件必达,不阻塞主流程 |
| 订单超时 | MQ连接失败 → 不触发 | 订单依赖前端状态轮询兜底 |
| 全局异常 | 消费端异常仅记日志 | 不影响其他消息处理 |
4. 关键配置说明
ruby
spring:
rabbitmq:
host: 127.0.0.1
port: 5672
username: admin
password: 123456
listener:
simple:
concurrency: 1 # 最小消费者数
max-concurrency: 3 # 最大消费者数
acknowledge-mode: auto # 自动确认
prefetch: 10 # 每次预取消息数
publisher-confirm-type: correlated # 发布确认
publisher-returns: true # 发布返回
5. 核心 Consumer 实现
| Consumer | 监听队列 | 核心职责 |
|---|---|---|
UserBehaviorConsumer |
user_behavior_queue |
消费行为消息,写日志+更新偏好+清缓存 |
OrderPayConsumer |
order_pay_queue |
消费支付成功消息,构建HTML邮件并发送 |
OrderCancelConsumer |
order_cancel_queue |
消费超时取消消息,自动将订单置为已取消 |
CodeMailConsumer |
login_code_mail |
消费验证码消息,异步发送验证码邮件 |
二、MySQL 推荐引擎
系统基于 MySQL 构建了 用户行为权重推荐引擎,通过多维度用户行为数据生成个性化推荐。
1. 行为权重模型
系统为每种用户行为类型定义了权重值,用于计算用户对菜品的偏好分值:
| 行为类型 | 类型编码 | 权重值 | 说明 |
|---|---|---|---|
| 浏览 (BROWSE) | 1 | 0.3 | 轻度兴趣,权重最低 |
| 加购 (CART) | 4 | 1.5 | 中等意愿 |
| 收藏 (FAVORITE) | 2 | 3.0 | 强偏好意愿 |
| 下单 (ORDER) | 3 | 8.0 | 真实消费行为,权重最高 |
偏好分值计算公式:
用户对菜品A的偏好分 = Σ(每次行为类型对应权重)
例如:用户先后浏览了菜品A(+0.3)、加购了菜品A(+1.5)、最终下单了菜品A(+8.0),则最终偏好分 = 0.3 + 1.5 + 8.0 = 9.8
2. 核心数据表
| 数据表 | 用途 | 关键字段 |
|---|---|---|
user_behavior_log |
用户行为日志(原始记录) | user_id, food_id, behavior_type, create_time |
user_food_preference |
用户菜品偏好分(聚合结果) | user_id, food_id, score, update_time |
resfood |
菜品基础信息 | fid, fname, realprice, category, spicy_level |
order_item |
订单明细(菜品销量统计) | order_id, fid, num |
3. 推荐算法流程
┌─────────────────────────────────────────────────────────────────────┐
│ MySQL 个性化推荐流程 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Step1: 缓存查询 │
│ ┌─────────────────────┐ │
│ │ Redis GET │ → 命中 → 直接返回(TTL=2h) │
│ │ recommend:v2:food: │ │
│ │ user:{userId} │ → 未命中 → 进入 Step2 │
│ └─────────────────────┘ │
│ │
│ Step2: 用户画像判断 │
│ ┌─────────────────────┐ │
│ │ 查询 user_food_ │ │
│ │ preference 表 │ → 有数据 → 老用户,offset=0 │
│ │ COUNT(*) │ → 无数据 → 新用户,计算哈希偏移 │
│ └─────────────────────┘ │
│ ↓ │
│ 新用户哈希偏移算法: │
│ offset = Math.abs(hash(userId)) % maxOffset │
│ (maxOffset = min(3, totalFoods - limit)) │
│ → 保证不同新用户看到不同的热销菜品组合 │
│ │
│ Step3: MySQL 推荐查询(核心 SQL) │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ SELECT f.fid, f.fname, f.realprice, f.fphoto, │ │
│ │ IFNULL(p.score, 0) AS score │ │
│ │ FROM resfood f │ │
│ │ LEFT JOIN user_food_preference p │ │
│ │ ON f.fid = p.food_id AND p.user_id = #{userId} │ │
│ │ LEFT JOIN (菜品总销量子查询) s ON f.fid = s.fid │ │
│ │ ORDER BY │ │
│ │ CASE │ │
│ │ WHEN 用户有偏好 AND p.score IS NULL THEN -1 -- 没点过排后 │ │
│ │ ELSE IFNULL(p.score, 0) │ │
│ │ END DESC, │ │
│ │ 总销量 DESC, │ │
│ │ fid ASC │ │
│ │ LIMIT #{offset}, #{limit} │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ Step4: 缓存写入 │
│ ┌─────────────────────┐ │
│ │ Redis SET │ → 写入缓存供下次使用 │
│ │ recommend:v2:food: │ (缓存异常不影响返回) │
│ │ user:{userId} │ │
│ └─────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
4. 两种推荐模式对比
| 维度 | 个性化推荐 (/recommend/list) |
全局热销推荐 (/recommend/hot) |
|---|---|---|
| 数据源 | user_food_preference + resfood + order_item |
resfood + order_item + resorderitem |
| 排序规则 | 偏好分 DESC → 总销量 DESC → fid ASC | 总销量 DESC → fid ASC |
| 目标用户 | 已登录用户(有行为数据) | 新用户 / 冷启动 |
| 缓存 Key | recommend:v2:food:user:{userId} |
recommend:hot |
| 降级策略 | 新用户哈希偏移取不同热销组合 | 直接按总销量排序 |
5. API 接口
| 接口 | 方法 | 说明 |
|---|---|---|
/recommend/list?userId=&size= |
GET | 个性化推荐,按用户偏好分排序 |
/recommend/hot?limit= |
GET | 全局热销推荐,按总销量排序 |
/behavior/add |
POST | 上报用户行为(异步入MQ) |
三、Neo4j 图数据库推荐
系统引入 Neo4j 图数据库,构建了 购物车双引擎推荐 能力,基于用户历史订单共现关系和菜品属性规则实现智能搭配推荐。
OFTEN_BOUGHT_WITH 共现关系:
共现 = 在同一个购物车 / 同一个订单里面,一起出现过 。 比如很多人的购物车同时放:素炒莴笋丝 + 炖鸡,这两个商品共同出现,就建立这条关系。

1. 图数据模型
┌─────────────────────────────────────────────────────────────────────┐
│ Neo4j 图谱模型 │
│ 以 "素炒莴笋丝" 为中心的图关系示例 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ (UserNode) (FoodNode) │
│ ┌─────────────────┐ ┌─────────────────────┐ │
│ │ user_id: "4" │ │ fid: 5 │ │
│ │ username:"123d" │──ORDERED─────→│ fname: "素炒莴笋丝" │ │
│ └─────────────────┘ orderCount:2 │ realprice: 12.00 │ │
│ ↓ lastOrderTime │ category: "COLD" │ │
│ │ :2026-08-02 │ spicyLevel: 0 │ │
│ │ │ totalSales: 12 │ │
│ │ └─────────────────────┘ │
│ │ ↑ │
│ │ │ │
│ │ ORDERED {count:1} │ │
│ └─────────────────────────────────────┘ │
│ │
│ (UserNode) ↑ │
│ ┌─────────────────┐ │ │
│ │ user_id: "2" │ ORDERED {count:1} │ │
│ │ username:"123" │───────────────────────────┘ │
│ └─────────────────┘ │
│ │
│ │
│ 素炒莴笋丝 的共现关系(OFTEN_BOUGHT_WITH) │
│ ───────────────────────────────────────── │
│ │
│ (FoodNode) (FoodNode) │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ fid: 20 │ │ fid: 17 │ │
│ │ fname: "清炒油麦菜" │──OFTEN_BOUGHT──→│ fname: "西红柿蛋汤" │ │
│ │ category: "COLD" │ _WITH │ category: "SOUP" │ │
│ │ spicyLevel: 0 │ count: 3 │ spicyLevel: 0 │ │
│ │ totalSales: 8 │ │ totalSales: 15 │ │
│ └─────────────────────┘ └─────────────────────┘ │
│ ↑ ↑ │
│ │ │ │
│ │ OFTEN_BOUGHT_WITH {count: 2} │ │
│ └──────────────────────────────────┘ │
│ │
│ (FoodNode) (FoodNode) │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ fid: 16 │ │ fid: 26 │ │
│ │ fname: "蒜蓉西兰花" │──OFTEN_BOUGHT──→│ fname: "紫菜蛋花汤" │ │
│ │ category: "COLD" │ _WITH │ category: "SOUP" │ │
│ │ spicyLevel: 0 │ count: 2 │ spicyLevel: 0 │ │
│ │ totalSales: 6 │ │ totalSales: 10 │ │
│ └─────────────────────┘ └─────────────────────┘ │
│ │
│
└─────────────────────────────────────────────────────────────────────┘
节点类型:
| 节点标签 | 主键属性 | 业务含义 | 核心属性 |
|---|---|---|---|
UserNode |
user_id |
用户节点 | username |
FoodNode |
fid |
菜品节点 | fname, realprice, fphoto, category, spicyLevel, totalSales |
关系类型:
| 关系类型 | 方向 | 含义 | 属性 |
|---|---|---|---|
ORDERED |
UserNode → FoodNode | 用户下单关系 | orderCount, lastOrderTime |
OFTEN_BOUGHT_WITH |
FoodNode → FoodNode | 菜品共现关系 | count(共现次数) |
2. 购物车双引擎推荐
CartRecommendServiceImpl 实现了两个独立的推荐引擎,分别产出不同推荐理由的菜品:
┌─────────────────────────────────────────────────────────────────────┐
│ 购物车双引擎推荐架构 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 输入:购物车菜品列表 + 用户ID │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 引擎一:规则推荐(基于菜品属性匹配) │ │
│ │ │ │
│ │ 原理:从 Neo4j 查询购物车菜品的 category 和 spicyLevel │ │
│ │ 规则优先级: │ │
│ │ ① 有热菜无主食 → 推荐主食 (STAPLE) │ │
│ │ ② 有辣菜无汤/饮品 → 优先推荐汤品 (SOUP),其次饮品 (DRINK) │ │
│ │ ③ 有主食无汤品 → 推荐汤 (SOUP) │ │
│ │ ④ 热菜+主食+辣,无凉菜 → 推荐凉菜 (COLD) │ │
│ │ ⑤ 兜底 → 推荐销量最高的主食或汤 │ │
│ │ │ │
│ │ Cypher 示例: │ │
│ │ MATCH (f:FoodNode {category: $category}) │ │
│ │ WHERE NOT f.fid IN $excludeFids │ │
│ │ RETURN f.fid, f.fname, f.realprice, f.totalSales │ │
│ │ ORDER BY f.totalSales DESC LIMIT 1 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 引擎二:共现推荐(基于图数据库关系遍历) │ │
│ │ │ │
│ │ 原理:从购物车菜品出发,沿 OFTEN_BOUGHT_WITH 关系遍历 │ │
│ │ 排序:共现次数(coScore) DESC → 菜品销量(baseSales) DESC │ │
│ │ 降级:无共现数据时回退到热销推荐 │ │
│ │ │ │
│ │ Cypher: │ │
│ │ MATCH (cart:FoodNode)-[r:OFTEN_BOUGHT_WITH]->(rec:FoodNode) │ │
│ │ WHERE cart.fid IN $excludeFids │ │
│ │ AND NOT rec.fid IN $excludeFids │ │
│ │ RETURN rec.fid, rec.fname, rec.totalSales AS baseSales, │ │
│ │ sum(r.count) AS coScore │ │
│ │ ORDER BY coScore DESC, baseSales DESC LIMIT 1 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ 输出:两个推荐菜品(不同引擎产出,保证不重复) │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ ruleBased: { fname: "蛋炒饭", tip: "搭配主食更美味" } │ │
│ │ coOccurBased: { fname: "酸辣汤", tip: "经常和这些菜一起下单"}│ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
3. 图谱数据初始化与同步
启动时自动初始化 (Neo4jDataInitializer):
项目启动
↓
清空 Neo4j 全部节点与关系(MATCH (n) DETACH DELETE n)
↓
同步 MySQL 用户表 → MERGE 写入 UserNode
↓
同步 MySQL 菜品表 → MERGE 写入 FoodNode(含 category, spicyLevel, totalSales)
↓
聚合已支付订单 → 构建 ORDERED 关系(orderCount, lastOrderTime)
↓
统计菜品共现 → 构建 OFTEN_BOUGHT_WITH 关系(count = 共现次数)
↓
图谱就绪,推荐服务可用
运行时全量同步 (Neo4jSyncController):
| 接口 | 方法 | 说明 |
|---|---|---|
/sync/neo4j/all |
POST | 全量同步 MySQL 数据至 Neo4j(幂等写入) |
/cart-recommend/sync-co-occurrence |
POST | 手动触发重建菜品共现关系 |
4. 三种推荐策略对比
| 维度 | MySQL 个性化推荐 | Neo4j 规则推荐 | Neo4j 共现推荐 |
|---|---|---|---|
| 数据源 | user_food_preference | FoodNode (category) | OFTEN_BOUGHT_WITH 关系 |
| 算法核心 | 行为权重累加 + 偏好分排序 | 品类补缺规则引擎 | 图遍历 + 共现次数排序 |
| 输入 | 用户ID | 购物车菜品列表 | 购物车菜品列表 |
| 输出 | 4-10 个推荐菜品 | 1 个规则匹配菜品 | 1 个共现匹配菜品 |
| 适用场景 | 首页个性化推荐 | 购物车搭配推荐 | 购物车搭配推荐 |
| 优势 | 实时反映用户偏好 | 可解释性强 | 发现隐藏关联 |
技术栈
后端技术栈
| 技术 | 版本 | 说明 |
|---|---|---|
| Spring Boot | 3.2.5 | 核心框架 |
| MyBatis-Plus | 3.5.5 | ORM 框架 |
| Spring Security | - | 安全框架 |
| MySQL | 8.x | 关系型数据库(用户/订单/菜品/偏好) |
| Redis | - | 缓存(推荐结果 + Session) |
| RabbitMQ | - | 消息队列(5大业务场景异步解耦) |
| Neo4j | - | 图数据库(共现关系 + 购物车推荐) |
| Spring Mail | - | 邮件发送 |
| 支付宝 SDK | 4.8.73 | 沙箱支付 |
| Hutool | 5.8.27 | 工具类库 |
| Knife4j | 4.4.0 | API 文档 |
| JWT | 0.12.6 | Token 认证 |
| OpenFeign | - | 服务间调用 |
| Caffeine | 3.1.8 | 本地缓存 |
前端技术栈
| 技术 | 版本 | 说明 |
|---|---|---|
| Vue | 3.4.27 | 前端框架 |
| Vue Router | 4.3.2 | 路由 |
| Pinia | 2.1.7 | 状态管理 |
| Element Plus | 2.7.6 | UI 组件库 |
| Axios | 1.7.2 | HTTP 请求 |
| Vite | 5.2.11 | 构建工具 |
| Day.js | 1.11.11 | 日期处理 |
功能特性
用户端
- 用户注册 / 登录(Session + JWT 认证)
- 菜品浏览与分类筛选
- 购物车增删改查
- 购物车智能搭配推荐(规则引擎 + 共现推荐双引擎)
- 下单结算
- 支付宝沙箱支付
- 订单状态实时轮询(2秒/次)
- 支付成功邮件通知
- 个性化菜品推荐(基于行为权重的偏好推荐)
- 全局热销推荐(用于新用户冷启动)
- 订单评价
系统特性
- RabbitMQ 全链路异步解耦:5 个 Exchange/Queue 覆盖行为采集、邮件通知、订单超时等核心场景
- 订单超时自动取消:TTL + 死信队列实现 30 分钟未支付自动取消
- MySQL 行为权重推荐引擎:浏览(0.3) → 加购(1.5) → 收藏(3.0) → 下单(8.0) 加权聚合
- Neo4j 图共现推荐:基于用户历史订单构建菜品共现关系,实现购物车智能搭配
- 支付兜底机制:支付宝回调丢失时,主动查询订单状态同步
- 降级容错:MQ 不可用时自动降级为同步处理,核心业务不受影响
- Redis 双层缓存:推荐结果缓存 + Session 持久化
- 雪花算法:唯一订单号生成
- 全局异常处理:统一错误响应
- 单元测试覆盖:45+ 单元测试用例
项目结构
take-out/ # 后端项目根目录
├── src/
│ ├── main/
│ │ ├── java/org/example/
│ │ │ ├── common/ # 通用类
│ │ │ │ ├── context/ # 用户上下文
│ │ │ │ ├── exception/ # 全局异常处理
│ │ │ │ ├── result/ # 响应结果枚举
│ │ │ │ ├── util/
│ │ │ │ │ ├── BehaviorWeight.java # 行为权重常量
│ │ │ │ │ └── SnowflakeIdWorker.java # 雪花ID生成
│ │ │ │ └── R.java # 统一响应封装
│ │ │ ├── config/ # 配置类
│ │ │ │ ├── RabbitMQConfig.java # MQ 5个Exchange/Queue定义
│ │ │ │ ├── RabbitTemplateConfig.java # MQ序列化配置
│ │ │ │ ├── RedisConfig.java # Redis配置
│ │ │ │ ├── SecurityConfig.java # 安全配置
│ │ │ │ └── ...
│ │ │ ├── controller/ # 控制层
│ │ │ │ ├── RecommendController.java # MySQL推荐接口
│ │ │ │ ├── CartRecommendController.java # Neo4j购物车推荐
│ │ │ │ ├── BehaviorController.java # 行为上报接口
│ │ │ │ ├── Neo4jSyncController.java # 图谱同步接口
│ │ │ │ ├── OrderController.java # 订单控制器
│ │ │ │ ├── PayController.java # 支付控制器
│ │ │ │ └── ...
│ │ │ ├── dto/ # 数据传输对象
│ │ │ │ ├── RecommendFoodVO.java # 推荐菜品VO
│ │ │ │ ├── CartRecommendRequest.java # 购物车推荐请求
│ │ │ │ ├── CartRecommendResponse.java # 购物车推荐响应
│ │ │ │ └── ...
│ │ │ ├── entity/ # 实体类
│ │ │ │ ├── UserBehaviorLog.java # 行为日志实体
│ │ │ │ ├── UserFoodPreference.java # 用户偏好实体
│ │ │ │ ├── Orders.java / OrderItem.java
│ │ │ │ └── ...
│ │ │ ├── listener/ # MQ 消费者
│ │ │ │ ├── UserBehaviorConsumer.java # 行为消费→偏好更新
│ │ │ │ ├── OrderPayConsumer.java # 支付成功邮件消费
│ │ │ │ ├── OrderCancelConsumer.java # 超时订单取消消费
│ │ │ │ └── CodeMailConsumer.java # 验证码邮件消费
│ │ │ ├── mapper/ # MyBatis Mapper
│ │ │ │ ├── ResFoodMapper.java # 推荐核心SQL
│ │ │ │ └── ...
│ │ │ ├── mq/ # MQ 消息定义
│ │ │ │ ├── consumer/
│ │ │ │ │ └── UserBehaviorConsumer.java
│ │ │ │ └── dto/
│ │ │ │ ├── BehaviorMessage.java # 行为消息
│ │ │ │ ├── OrderPayMessage.java # 支付消息
│ │ │ │ ├── OrderDelayMessage.java # 延迟消息
│ │ │ │ └── CodeMailMessage.java # 验证码消息
│ │ │ ├── service/ # 业务层
│ │ │ │ ├── RecommendService.java # MySQL推荐接口
│ │ │ │ ├── CartRecommendService.java # Neo4j购物车推荐接口
│ │ │ │ ├── BehaviorService.java # 行为采集接口
│ │ │ │ ├── impl/
│ │ │ │ │ ├── RecommendServiceImpl.java # MySQL推荐实现
│ │ │ │ │ ├── CartRecommendServiceImpl.java # Neo4j推荐实现
│ │ │ │ │ ├── BehaviorServiceImpl.java # 行为采集实现
│ │ │ │ │ └── ...
│ │ │ │ └── ...
│ │ │ ├── takeout/ # Neo4j 专用
│ │ │ │ ├── config/
│ │ │ │ │ └── Neo4jDataInitializer.java # 图谱初始化
│ │ │ │ └── neo4j/
│ │ │ │ ├── entity/
│ │ │ │ │ ├── FoodNode.java # 菜品节点
│ │ │ │ │ ├── UserNode.java # 用户节点
│ │ │ │ │ └── OrderedRelation.java # 下单关系
│ │ │ │ └── repository/
│ │ │ │ ├── FoodNodeRepository.java
│ │ │ │ └── UserNodeRepository.java
│ │ │ └── TakeOutApplication.java # 启动类
│ │ └── resources/
│ │ ├── application.yml # 主配置(含MQ/Neo4j配置)
│ │ ├── application-dev.yml # 开发环境配置
│ │ ├── application-prod.yml # 生产环境配置
│ │ └── mapper/
│ │ └── ResFoodMapper.xml # 推荐核心SQL映射
│ └── test/
│ └── java/org/example/
│ ├── service/impl/
│ │ └── MailServiceImplTest.java
│ ├── mq/
│ │ └── OrderNotifyProducerTest.java
│ └── MailIntegrationTest.java
└── pom.xml
快速开始
环境要求
- JDK 17+
- Node.js 18+
- MySQL 8.x
- Redis 6.x+
- RabbitMQ 3.x+
- Neo4j 5.x+
1. 数据库准备
创建数据库并导入表结构:
ruby
CREATE DATABASE IF NOT EXISTS res DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE res;
-- 执行建表脚本(项目中提供的 SQL 文件)
核心数据表结构(推荐相关):
ruby
-- 用户行为日志表
CREATE TABLE user_behavior_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id VARCHAR(32) NOT NULL,
food_id BIGINT NOT NULL,
behavior_type INT NOT NULL,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user_food (user_id, food_id)
);
-- 用户菜品偏好分表
CREATE TABLE user_food_preference (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id VARCHAR(32) NOT NULL,
food_id BIGINT NOT NULL,
score DECIMAL(10,2) DEFAULT 0,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
is_deleted TINYINT DEFAULT 0,
UNIQUE KEY uk_user_food (user_id, food_id, is_deleted)
);
2. 配置文件
修改 src/main/resources/application-dev.yml:
ruby
spring:
datasource:
url: jdbc:mysql://localhost:3306/res?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: your_password
data:
redis:
host: localhost
port: 6379
password:
database: 0
rabbitmq:
host: 127.0.0.1
port: 5672
username: admin
password: 123456
virtual-host: /
neo4j:
uri: bolt://127.0.0.1:7687
authentication:
username: neo4j
password: your_neo4j_password
mail:
host: smtp.163.com
port: 465
username: your_email@163.com
password: your_email_auth_code
alipay:
app-id: your_alipay_app_id
merchant-private-key: your_private_key
alipay-public-key: your_public_key
notify-url: your_cpolar_domain/pay/notify.action
3. 启动后端
ruby
cd take-out
mvn clean install -DskipTests
mvn spring-boot:run
后端服务启动后访问:
- API 地址:
http://localhost:8080 - API 文档:
http://localhost:8080/doc.html - 推荐接口:
GET /recommend/list、GET /recommend/hot - 购物车推荐:
POST /cart-recommend/recommend - 行为上报:
POST /behavior/add - 图谱同步:
POST /sync/neo4j/all
4. 启动前端
ruby
cd take-out-web
npm install
npm run dev
核心业务流程
用户行为驱动推荐闭环
ruby
┌──────────────────────────────────────────────────────────────────────┐
│ 行为驱动推荐闭环 │
├──────────────────────────────────────────────────────────────────────┤
│ │
│ ① 用户浏览菜品 → 前端上报 behaviorType=1(浏览) │
│ ↓ │
│ ② BehaviorController → BehaviorServiceImpl.reportBehavior() │
│ ↓ │
│ ③ 投递 MQ → behavior_exchange → user_behavior_queue │
│ ↓ │
│ ④ UserBehaviorConsumer 消费: │
│ - 写入 user_behavior_log (行为日志) │
│ - 更新 user_food_preference (偏好分 += 0.3) │
│ - 清除 Redis 推荐缓存 │
│ ↓ │
│ ⑤ 用户请求推荐 → RecommendController.list() │
│ ↓ │
│ ⑥ 缓存未命中 → 查询 MySQL (LEFT JOIN preference 表) │
│ ↓ │
│ ⑦ 返回最新偏好分排序的推荐列表 → 写入缓存 │
│ │
│ 💡 每一次用户行为都会实时更新推荐结果! │
│ │
└──────────────────────────────────────────────────────────────────────┘
下单支付全流程
ruby
用户选菜 → 加入购物车 → 购物车推荐搭配(Neo4j 双引擎)
↓
提交订单 → 投递 MQ 延迟消息(30min TTL)
↓
调用支付宝下单接口 → 支付
↓
支付宝异步回调 notify.action → 更新订单状态
↓
投递 MQ 支付消息 → OrderPayConsumer → 发送支付成功邮件
↓
前端轮询订单状态 → 自动更新
↓
30分钟内未支付 → 死信队列触发 → OrderCancelConsumer 自动取消
邮件通知链路
ruby
登录/注册 → 投递 login_code_mail 队列 → CodeMailConsumer → 发送验证码邮件
↓
支付成功 → 投递 order_pay_queue → OrderPayConsumer → 发送支付成功HTML邮件
↓
MQ 投递失败 → 降级为同步发送 → 保证邮件必达
测试
单元测试
ruby
# 运行所有单元测试
mvn test
仅运行邮件服务测试
mvn test -Dtest="MailServiceImplTest"
仅运行 MQ 生产者测试
mvn test -Dtest="OrderNotifyProducerTest"
邮件测试接口
| 接口 | 说明 |
|---|---|
GET /mail/test/config |
查看邮件服务状态 |
GET /mail/test/simple |
发送简单文本邮件 |
GET /mail/test/html |
发送 HTML 邮件 |
GET /mail/test/order |
发送模拟订单邮件 |
GET /mail/test/realOrder?orderNo=xxx |
发送真实订单邮件 |
GET /mail/test/mq |
测试 MQ 链路 |
推荐接口测试
ruby
# 个性化推荐(userId 可从响应中获取)
curl "http://localhost:8080/recommend/list?userId=1&size=4"
全局热销推荐
curl "http://localhost:8080/recommend/hot?limit=10"
购物车双引擎推荐
curl -X POST "http://localhost:8080/cart-recommend/recommend"
-H "Content-Type: application/json"
-d '{"userId":"1","cartItems":[{"fid":1,"num":2},{"fid":3,"num":1}]}'
触发图谱全量同步
curl -X POST "http://localhost:8080/sync/neo4j/all"
手动重建共现关系
curl -X POST "http://localhost:8080/cart-recommend/sync-co-occurrence"
上报用户行为(异步入MQ)
curl -X POST "http://localhost:8080/behavior/add"
-H "Content-Type: application/json"
-d '{"userId":"1","foodId":5,"behaviorType":4}'
关键设计说明
RabbitMQ 消息可靠性保障
| 机制 | 实现 | 说明 |
|---|---|---|
| 发布确认 | publisher-confirm-type: correlated |
消息成功到达 Broker 后回调确认 |
| 消息返回 | publisher-returns: true |
消息无法路由时返回生产者 |
| 自动重队列 | default-requeue-rejected: false |
拒绝消息不自动重入队列,避免无限循环 |
| 消费确认 | acknowledge-mode: auto |
自动确认,简化消费端逻辑 |
| TTL + 死信 | 延迟队列设置 x-message-ttl + x-dead-letter-exchange |
实现订单超时自动取消 |
MySQL 推荐缓存策略
- 缓存 Key :
recommend:v2:food:user:{userId}(个性化)、recommend:hot(热销) - TTL:2 小时(推荐数据不需要实时更新)
- 缓存更新:用户行为上报后自动清除缓存,下次请求重新查库
- 缓存保护:所有 Redis 操作 try-catch,缓存异常不影响主流程
Neo4j 推荐降级策略
- Neo4j 连接失败时,
CartRecommendServiceImpl返回 null,前端不显示推荐窗口 - 共现查询无结果时,降级为按品类热销推荐
- 启动时 Neo4j 不可用不影响 MySQL 推荐和其他核心业务
支付兜底机制
由于支付宝回调可能因网络原因丢失(如 cpolar 内网穿透不稳定),系统实现了主动兜底逻辑:
- 在支付回调接口中,下单后立即查询支付宝订单状态
- 在订单状态轮询接口中,主动查询支付宝获取订单真实状态
- 若发现已支付,立即更新本地订单并触发邮件通知
常见问题
Q: 启动时 RabbitMQ 连接失败怎么办?
RabbitMQ 连接失败时,系统会自动降级,不影响核心业务。但以下功能将不可用:
- 异步邮件通知(降级为同步发送)
- 订单超时自动取消
- 行为异步采集(降级为同步处理)
建议:
- 确保 RabbitMQ 服务正常运行
- 检查
application.yml和application-dev.yml中的 RabbitMQ 配置 - 或暂时移除
spring-boot-starter-amqp依赖
Q: Neo4j 连接失败怎么办?
Neo4j 连接失败时:
- MySQL 推荐(
/recommend/list、/recommend/hot)不受影响 - 购物车推荐(
/cart-recommend/recommend)将返回空数据 - 系统启动时 Neo4j 数据初始化会跳过,不阻塞启动
Q: 如何初始化 Neo4j 图谱数据?
有两种方式:
- 自动初始化 :项目启动时
Neo4jDataInitializer会自动执行全量同步 - 手动同步 :调用
POST /sync/neo4j/all接口触发全量同步
Q: 如何更新菜品共现关系?
共现关系 (OFTEN_BOUGHT_WITH) 是基于历史订单预计算的静态关系,需要手动触发更新:
- 调用
POST /cart-recommend/sync-co-occurrence接口重建 - 或重新启动项目触发
Neo4jDataInitializer
Q: 推荐缓存如何清除?
有三种方式:
- 用户行为上报时自动清除对应用户的推荐缓存
- 缓存 2 小时 TTL 到期后自动失效
- 可通过 Redis 命令手动删除
recommend:v2:food:user:*