基于AI图像识别与智能推荐的校园食堂评价系统研究 07-收藏与最近浏览模块

07-收藏与最近浏览模块

提示:本文档使用了颜色标注来突出重点内容:

  • 蓝色:文件路径和行号信息
  • 橙色:关键提示、重要注意和问题
  • 红色:抛出的问题

问题解答中的关键词语使用加粗标注。

学习这个模块时最好启动项目学习,这样会更直观。

模块说明

收藏与最近浏览模块提供用户个性化功能,帮助用户快速访问感兴趣的内容。主要包括:

  • 收藏功能:用户可以收藏喜欢的菜品,方便以后快速找到
  • 取消收藏:用户可以取消已收藏的内容
  • 收藏列表:查看自己收藏的所有内容
  • 最近浏览:系统自动记录用户最近查看的菜品,方便快速回访




与前面模块的关联

收藏与最近浏览模块是用户个性化功能,为用户提供便捷的内容访问方式:

1. 依赖关系

  • 依赖于01-用户模块
    • 收藏和浏览记录都需要用户ID,系统从Session中获取当前登录的用户信息(USER_SESSION
    • 只有登录用户才能收藏和查看浏览记录,未登录用户无法使用这些功能
    • 不同用户的收藏列表和浏览记录是独立的

关键代码示例:从Session获取用户ID

java 复制代码
// 文件:src/main/java/com/scfs/controller/FavoriteController.java
// 位置:第31-58行,addFavorite方法

@PostMapping("/add")
@ResponseBody
public Result addFavorite(@RequestParam Long dishId, HttpSession session) {
    try {
        // 从Session中获取当前登录用户
        User user = (User) session.getAttribute("USER_SESSION");
        if (user == null) {
            return new Result(false, 401, "请先登录", null);
        }
        Long userId = user.getUserId();
        if (userId == null) {
            return new Result(false, 401, "请先登录", null);
        }
        
        // 调用Service添加收藏
        boolean success = favoriteService.addFavorite(userId, dishId);
        if (success) {
            return new Result(true, "收藏成功");
        }
    } catch (Exception e) {
        return new Result(false, "收藏失败:" + e.getMessage());
    }
}
  • 依赖于02-数据展示模块
    • 最近浏览功能基于店铺详情页的访问
    • 用户查看店铺详情页中的菜品时,系统自动记录到最近浏览
    • 收藏列表和浏览记录中的内容,点击后会跳转到店铺详情页

关键代码示例:自动记录最近浏览

java 复制代码
// 文件:src/main/java/com/scfs/controller/DishController.java
// 位置:第158-167行,getDishDetail方法中记录浏览

@GetMapping("/get/{dishId}")
@ResponseBody
public Result getDishDetail(@PathVariable Long dishId, HttpSession session) {
    try {
        Dish dish = dishService.findById(dishId);
        if (dish != null) {
            // 记录最近浏览(如果用户已登录)
            try {
                User user = (User) session.getAttribute("USER_SESSION");
                if (user != null && user.getUserId() != null) {
                    recentViewService.recordView(user.getUserId(), dishId);  // 调用07-收藏与最近浏览模块
                }
            } catch (Exception e) {
                logger.warn("记录最近浏览失败", e);
            }
            return new Result(true, "获取菜品信息成功", dish);
        }
    }
}
  • 依赖于03-菜品管理模块
    • 收藏的对象是菜品(不是店铺)
    • 用户收藏的是03-菜品管理模块中的菜品
    • 收藏列表中显示的是菜品信息(名称、价格、图片等)

关键代码示例:收藏菜品

java 复制代码
// 文件:src/main/java/com/scfs/service/impl/FavoriteServiceImpl.java
// 位置:第26-44行,addFavorite方法

@Override
@Transactional
public boolean addFavorite(Long userId, Long dishId) {
    if (userId == null || dishId == null) {
        return false;
    }
    try {
        // 检查是否已收藏
        Favorite existing = favoriteMapper.findByUserIdAndDishId(userId, dishId);
        if (existing != null) {
            return true; // 已收藏,返回成功
        }
        // 添加收藏
        Favorite favorite = new Favorite(userId, dishId);
        return favoriteMapper.insert(favorite) > 0;
    } catch (Exception e) {
        e.printStackTrace();
        return false;
    }
}

2. 数据流转

收藏菜品时的数据流转

复制代码
用户在store-detail.jsp查看菜品列表(02-数据展示模块)
    ↓
用户点击某个菜品的收藏按钮
    ↓
前端JavaScript调用 /favorite/add 接口
    ↓
07-收藏与最近浏览模块的Controller从Session获取用户ID(01-用户模块)
    ↓
Controller调用Service添加收藏记录(用户ID + 菜品ID)
    ↓
Service调用Mapper插入favorite表
    ↓
返回成功,前端更新收藏按钮状态(灰色→黄色)

最近浏览的记录

复制代码
用户在store-detail.jsp查看某个菜品(02-数据展示模块)
    ↓
页面加载时调用 /dish/get/{dishId} 接口获取菜品详情
    ↓
03-菜品管理模块的Controller处理请求
    ↓
Controller在返回菜品详情前,调用RecentViewService.recordView()(07-收藏与最近浏览模块)
    ↓
Service记录或更新用户的浏览记录(用户ID + 菜品ID + 浏览时间)
    ↓
用户下次在个人中心查看"最近浏览"时,可以看到之前浏览过的菜品

3. 数据表关系

收藏表(favorite)

  • 存储用户ID和菜品ID的对应关系
  • 一个用户可以有多个收藏(一对多)
  • 一个菜品可以被多个用户收藏(一对多)

最近浏览表(recent_view)

  • 存储用户ID、菜品ID和浏览时间
  • 系统会自动更新浏览时间(同一用户多次浏览同一菜品时)
  • 可以设置保留最近N条浏览记录

4. 学习建议

  • 在学习本模块前

    • 建议先理解01-用户模块的Session管理(如何获取当前用户ID)
    • 理解02-数据展示模块的店铺详情页(收藏按钮在哪里)
    • 理解03-菜品管理模块的菜品数据结构(收藏的是什么)
  • 学习时可以结合

    • 实际操作收藏和取消收藏,观察按钮状态的变化
    • 查看个人中心的收藏列表和最近浏览列表
    • 查看数据库中的favorite表和recent_view
    • 观察收藏/浏览记录的创建时间和更新时间
    • 理解为什么需要这两个表(收藏和浏览的区别)
  • 学习本模块后

    • 可以思考如何优化收藏功能(分组、标签等)
    • 思考如何优化最近浏览(限制数量、定期清理等)
    • 理解用户个性化功能如何提升用户体验

5. 问题解答

Q1:收藏和浏览的区别是什么?为什么要分开存储?

A: 收藏和浏览有不同的业务含义和使用场景。

收藏(favorite)

  • 用户主动操作:用户点击"收藏"按钮才会记录
  • 表示用户兴趣:用户明确表示喜欢这个菜品
  • 长期保存:收藏记录会一直保存,直到用户取消收藏
  • 用途:用户可以快速找到自己喜欢的菜品

最近浏览(recent_view)

  • 系统自动记录:用户查看菜品详情时自动记录
  • 表示浏览历史:记录用户最近查看过的菜品
  • 有时间限制:通常只保留最近N条(如20条)
  • 用途:用户可以快速找到最近查看过的菜品

为什么要分开存储

  1. 业务逻辑不同:收藏是主动的,浏览是被动的
  2. 数据管理不同:收藏需要手动删除,浏览可以自动清理
  3. 查询需求不同:收藏列表按收藏时间排序,浏览列表按浏览时间排序
  4. 用户体验不同:收藏是"我喜欢",浏览是"我看过"

代码位置

java 复制代码
// 文件:src/main/java/com/scfs/service/impl/RecentViewServiceImpl.java
// 位置:第27-38行,recordView方法

@Override
@Transactional
public boolean recordView(Long userId, Long dishId) {
    if (userId == null || dishId == null) {
        return false;
    }
    try {
        RecentView recentView = new RecentView(userId, dishId);
        // 使用INSERT ... ON DUPLICATE KEY UPDATE,如果已存在则更新浏览时间
        return recentViewMapper.insertOrUpdate(recentView) > 0;
    } catch (Exception e) {
        e.printStackTrace();
        return false;
    }
}
Q2:最近浏览是如何自动记录的?

A: 在用户查看菜品详情时自动记录。

记录时机

  • 用户访问店铺详情页,查看某个菜品时
  • 系统在返回菜品详情前,自动调用recentViewService.recordView()

代码位置

java 复制代码
// 文件:src/main/java/com/scfs/controller/DishController.java
// 位置:第158-167行,getDishDetail方法

@GetMapping("/get/{dishId}")
@ResponseBody
public Result getDishDetail(@PathVariable Long dishId, HttpSession session) {
    try {
        Dish dish = dishService.findById(dishId);
        if (dish != null) {
            // 记录最近浏览(如果用户已登录)(第159-167行)
            try {
                User user = (User) session.getAttribute("USER_SESSION");
                if (user != null && user.getUserId() != null) {
                    recentViewService.recordView(user.getUserId(), dishId);
                }
            } catch (Exception e) {
                logger.warn("记录最近浏览失败", e);  // 记录失败不影响获取菜品信息
            }
            return new Result(true, "获取菜品信息成功", dish);
        }
    }
}

为什么用try-catch包裹

  • 记录浏览失败不应该影响获取菜品信息
  • 如果记录失败,只是没有记录浏览历史,不影响主要功能
Q3:如果用户多次浏览同一个菜品,会创建多条记录吗?

A: 不会,使用INSERT ... ON DUPLICATE KEY UPDATE更新浏览时间。

数据库设计

  • recent_view表有唯一索引:uk_user_dish (user_id, dish_id)
  • 同一用户多次浏览同一菜品,只会更新view_time字段,不会创建新记录

代码位置

java 复制代码
// 文件:src/main/java/com/scfs/mapper/RecentViewMapper.java
// 位置:第21-24行,insertOrUpdate方法

@Insert("INSERT INTO recent_view (user_id, dish_id, view_time) VALUES (#{userId}, #{dishId}, NOW()) " +
        "ON DUPLICATE KEY UPDATE view_time = NOW()")  // 第21-22行
@Options(useGeneratedKeys = true, keyProperty = "viewId", keyColumn = "view_id")
int insertOrUpdate(RecentView recentView);

工作原理

  1. 第一次浏览:插入新记录,view_time为当前时间
  2. 再次浏览:由于唯一索引冲突,执行UPDATE view_time = NOW()
  3. 结果:只有一条记录,但view_time更新为最新浏览时间

优点

  • 避免数据冗余
  • 浏览时间始终是最新的
  • 查询时按view_time排序,最近浏览的菜品排在前面

功能一:收藏菜品

功能说明

收藏功能是用户可以收藏喜欢的菜品,方便以后快速找到。

抛出问题:用户点击收藏按钮后,系统是怎么把收藏记录保存到数据库的?数据是怎么返回页面更新按钮状态的?

逐步追踪

第一步:找到用户操作的入口

用户在浏览店铺详情页(store-detail.jsp)时,会看到每个菜品卡片都有一个收藏按钮。这个按钮在my.js的displayDishList函数中生成:

javascript 复制代码
// 收藏按钮(所有登录用户可见)
var currentUserId = getCurrentUserId();
if (currentUserId) {
    html += '<button class="btn btn-primary btn-sm action-btn favorite-btn" id="favoriteBtn-' + dish.dishId + '" onclick="toggleDishFavorite(' + dish.dishId + ', event)" data-dish-id="' + dish.dishId + '">';
    html += '<span class="glyphicon glyphicon-star" id="favoriteIcon-' + dish.dishId + '"></span> <span id="favoriteText-' + dish.dishId + '">收藏</span>';
    html += '</button>';
}

位置src/main/webapp/js/my.js 第814-819行

当用户点击这个收藏按钮时,会触发toggleDishFavorite函数。

第二步:追踪前端JavaScript处理

用户点击收藏按钮,触发toggleDishFavorite函数:

javascript 复制代码
// [1] 切换收藏状态函数(数据流向:前端 → Controller)
function toggleDishFavorite(dishId, event) {
    // [2] 阻止事件冒泡
    if (event) {
        event.stopPropagation();
    }
    if (!dishId) {
        return;
    }
    
    // [3] 获取按钮和文字元素
    var btn = $('#favoriteBtn-' + dishId);
    var text = $('#favoriteText-' + dishId);
    // [4] 判断当前收藏状态
    var isFavorite = text.text() === '已收藏';
    // [5] 根据状态决定请求URL和操作类型
    var url = isFavorite ? getApiUrl('favorite', 'remove') : getApiUrl('favorite', 'add');
    var action = isFavorite ? '取消收藏' : '收藏';

    // [6] 禁用按钮,防止重复点击
    btn.prop('disabled', true);

    // [7] 发送AJAX POST请求(数据流向:前端 → Controller)
    $.ajax({
        url: url,
        type: 'POST',
        data: { dishId: dishId },  // 菜品ID
        // [8] 请求成功回调:处理后端返回的结果(数据流向:Controller → 前端)
        success: function (response) {
            if (response.success) {
                // [9] 更新按钮状态(已收藏/收藏)
                updateDishFavoriteButton(dishId, !isFavorite);
                var toastMsg = action + '成功';
            } else {
                // [10] 操作失败:显示错误信息
                alert(action + '失败:' + (response.message || '请稍后重试'));
            }
            // [11] 重新启用按钮
            btn.prop('disabled', false);
        },
        error: function () {
            alert(action + '失败,请稍后重试');
            btn.prop('disabled', false);
        }
    });
}

位置src/main/webapp/js/my.js 第2522-2559行

第三步:追踪到Controller层

根据请求URL/favorite/add/favorite/remove,找到FavoriteController的方法:

java 复制代码
// [1] 处理POST请求 /favorite/add(数据流向:前端 → Controller)
@PostMapping("/add")
@ResponseBody
public Result addFavorite(@RequestParam Long dishId, HttpSession session) {
    try {
        // [2] 从Session中获取当前登录用户(01-用户模块提供的USER_SESSION)
        User user = (User) session.getAttribute("USER_SESSION");
        if (user == null) {
            return new Result(false, 401, "请先登录", null);
        }
        Long userId = user.getUserId();
        
        // [3] 参数验证:检查菜品ID
        if (dishId == null) {
            return new Result(false, "菜品ID不能为空");
        }

        // [4] 调用Service层添加收藏(数据流向:Controller → Service)
        boolean success = favoriteService.addFavorite(userId, dishId);
        
        // [5] 根据Service返回结果构建响应(数据流向:Controller → 前端)
        if (success) {
            return new Result(true, "收藏成功");
        } else {
            return new Result(false, "收藏失败,可能已收藏过");
        }
    } catch (Exception e) {
        logger.error("添加收藏失败", e);
        return new Result(false, "添加收藏失败");
    }
}

// [1] 处理POST请求 /favorite/remove(数据流向:前端 → Controller)
@PostMapping("/remove")
@ResponseBody
public Result removeFavorite(@RequestParam Long dishId, HttpSession session) {
    try {
        // [2] 从Session中获取当前登录用户
        User user = (User) session.getAttribute("USER_SESSION");
        if (user == null) {
            return new Result(false, 401, "请先登录", null);
        }
        Long userId = user.getUserId();
        
        // [3] 参数验证:检查菜品ID
        if (dishId == null) {
            return new Result(false, "菜品ID不能为空");
        }

        // [4] 调用Service层取消收藏(数据流向:Controller → Service)
        boolean success = favoriteService.removeFavorite(userId, dishId);
        
        // [5] 根据Service返回结果构建响应(数据流向:Controller → 前端)
        if (success) {
            return new Result(true, "取消收藏成功");
        } else {
            return new Result(false, "取消收藏失败");
        }
    } catch (Exception e) {
        logger.error("取消收藏失败", e);
        return new Result(false, "取消收藏失败");
    }
}

位置src/main/java/com/scfs/controller/FavoriteController.java 第28-80行

第四步:追踪到Service层

Controller调用favoriteService.addFavorite()favoriteService.removeFavorite(),Service层实现:

java 复制代码
// [1] 添加收藏(数据流向:Service → Mapper)
@Override
public boolean addFavorite(Long userId, Long dishId) {
    try {
        // [2] 检查是否已收藏(避免重复收藏)
        Favorite existing = favoriteMapper.findByUserIdAndDishId(userId, dishId);
        if (existing != null) {
            return false;  // 已收藏,返回false
        }
        
        // [3] 创建收藏记录
        Favorite favorite = new Favorite();
        favorite.setUserId(userId);
        favorite.setDishId(dishId);
        favorite.setCreateTime(new Date());
        
        // [4] 插入数据库(数据流向:Service → Mapper)
        int result = favoriteMapper.insert(favorite);
        return result > 0;
    } catch (Exception e) {
        logger.error("添加收藏失败", e);
        return false;
    }
}

// [1] 取消收藏(数据流向:Service → Mapper)
@Override
public boolean removeFavorite(Long userId, Long dishId) {
    try {
        // [2] 删除收藏记录(数据流向:Service → Mapper)
        int result = favoriteMapper.deleteByUserIdAndDishId(userId, dishId);
        return result > 0;
    } catch (Exception e) {
        logger.error("取消收藏失败", e);
        return false;
    }
}

位置src/main/java/com/scfs/service/impl/FavoriteServiceImpl.java 第28-60行

第五步:追踪到Mapper层

Service调用favoriteMapper.insert()favoriteMapper.deleteByUserIdAndDishId(),Mapper层实现:

java 复制代码
// [1] 插入收藏记录到数据库(数据流向:Mapper → 数据库)
@Insert("INSERT INTO favorite (user_id, dish_id, create_time) " +
        "VALUES (#{userId}, #{dishId}, #{createTime})")
int insert(Favorite favorite);

// [2] 根据用户ID和菜品ID删除收藏记录(数据流向:Mapper → 数据库)
@Delete("DELETE FROM favorite WHERE user_id = #{userId} AND dish_id = #{dishId}")
int deleteByUserIdAndDishId(@Param("userId") Long userId, @Param("dishId") Long dishId);

位置src/main/java/com/scfs/mapper/FavoriteMapper.java 第28-35行

执行顺序说明

  • 添加收藏:先检查是否已收藏,如果未收藏则插入新记录
  • 取消收藏:直接删除收藏记录
  • 数据库执行SQL后,返回受影响的行数(数据流向:数据库 → Mapper → Service)
第六步:数据返回流程

完整的数据返回路径

  1. 数据库 → Mapper:数据库执行INSERT或DELETE语句,返回受影响的行数给Mapper
  2. Mapper → Service:Mapper返回受影响的行数给Service
  3. Service → Controller :Service判断行数大于0,返回true给Controller
  4. Controller处理
    • Controller封装成Result对象,successtruemessage为"收藏成功"或"取消收藏成功"
    • 返回给前端(数据流向:Controller → 前端)
  5. 前端处理
    • JavaScript接收到Result对象
    • 判断response.success是否为true
    • 如果成功,调用updateDishFavoriteButton(dishId, !isFavorite)更新按钮状态
    • 按钮文字从"收藏"变为"已收藏",或从"已收藏"变为"收藏"
    • 按钮样式也会相应更新(如星星图标填充颜色)

这样,用户点击收藏按钮后,系统就保存或删除收藏记录到数据库,返回成功提示,页面更新按钮状态,整个流程就完成了。

收藏操作完整流程时序图

数据库 FavoriteMapper FavoriteService FavoriteController JavaScript store-detail.jsp 用户 数据库 FavoriteMapper FavoriteService FavoriteController JavaScript store-detail.jsp 用户 alt [未收藏] [已收藏] alt [添加收藏] [取消收藏] [1] 点击收藏按钮 [2] 触发toggleDishFavorite() [3] 判断当前收藏状态 [4] 决定请求URL (/favorite/add 或 /favorite/remove) [5] 禁用按钮,防止重复点击 [6] AJAX POST /favorite/add (dishId) [7] 从Session获取用户信息 [8] 调用favoriteService.addFavorite() [9] 检查是否已收藏 [10] SELECT * FROM favorite WHERE user_id = ? AND dish_id = ? [11] 返回查询结果 [12] 返回Favorite对象(或null) [13] 创建Favorite对象 [14] 调用favoriteMapper.insert() [15] INSERT INTO favorite (user_id, dish_id, create_time) [16] 返回受影响行数(1) [17] 返回受影响行数 [18] 返回true [19] 返回Result(true, "收藏成功") [20] 更新按钮状态为"已收藏" [21] 返回false [22] 返回Result(false, "已收藏过") [23] 从Session获取用户信息 [24] 调用favoriteService.removeFavorite() [25] 调用favoriteMapper.deleteByUserIdAndDishId() [26] DELETE FROM favorite WHERE user_id = ? AND dish_id = ? [27] 返回受影响行数(1) [28] 返回受影响行数 [29] 返回true [30] 返回Result(true, "取消收藏成功") [31] 更新按钮状态为"收藏" [32] 重新启用按钮 [33] 显示更新后的按钮状态

为什么这样设计

为什么添加收藏前要检查是否已收藏?

  • 原因1:数据一致性

    • 避免重复插入相同的收藏记录
    • 保证数据库中的数据唯一性
    • 符合业务逻辑,一个用户对一个菜品只能收藏一次
  • 原因2:用户体验

    • 如果已收藏,可以给用户友好的提示
    • 避免用户重复点击导致的问题
    • 提升用户体验
  • 原因3:性能优化

    • 检查比直接插入更轻量

    • 可以提前发现重复操作

    • 减少不必要的数据库操作
      if (success) {
      return new Result(true, "收藏成功");
      } else {
      return new Result(false, "收藏失败");
      }
      } catch (Exception e) {
      e.printStackTrace();
      return new Result(false, "收藏失败:" + e.getMessage());
      }
      }

      位置src/main/java/com/scfs/controller/FavoriteController.java 第31-58行

      Controller接收请求后,先从Session中获取当前登录用户的ID。如果用户未登录,返回错误提示;如果已登录,调用favoriteService.addFavorite方法,传入用户ID和菜品ID。

      第四步:追踪到Service层

      FavoriteServiceFavoriteServiceImpl中实现。Service先检查用户是否已经收藏过这个菜品:

      java 复制代码
      @Override
      @Transactional
      public boolean addFavorite(Long userId, Long dishId) {
          if (userId == null || dishId == null) {
              return false;
          }
          try {
              // 检查是否已收藏
              Favorite existing = favoriteMapper.findByUserIdAndDishId(userId, dishId);
              if (existing != null) {
                  return true; // 已收藏,返回成功
              }
              // 添加收藏
              Favorite favorite = new Favorite(userId, dishId);
              return favoriteMapper.insert(favorite) > 0;
          } catch (Exception e) {
              e.printStackTrace();
              return false;
          }
      }

位置src/main/java/com/scfs/service/impl/FavoriteServiceImpl.java 第26-44行

如果已经收藏过,直接返回成功;如果未收藏,创建一个新的Favorite对象,调用favoriteMapper.insert方法插入数据库。

第五步:追踪到Mapper层

Service调用FavoriteMapperinsert方法操作数据库。Mapper使用@Insert注解定义SQL语句:

java 复制代码
@Insert("INSERT INTO favorite (user_id, dish_id) VALUES (#{userId}, #{dishId})")
@Options(useGeneratedKeys = true, keyProperty = "favoriteId", keyColumn = "favorite_id")
int insert(Favorite favorite);

位置src/main/java/com/scfs/mapper/FavoriteMapper.java 第20-22行

这个SQL语句直接插入favorite表中的一条记录,包含用户ID和菜品ID。数据库操作成功后,返回受影响的行数。

第六步:数据如何返回

数据库操作成功后,后端返回Result对象,successtruemessage为"收藏成功"。前端JavaScript接收到响应后,调用updateDishFavoriteButton函数更新按钮状态:

javascript 复制代码
function updateDishFavoriteButton(dishId, isFavorite) {
    var btn = $('#favoriteBtn-' + dishId);
    var icon = $('#favoriteIcon-' + dishId);
    var text = $('#favoriteText-' + dishId);
    
    if (isFavorite) {
        icon.removeClass('glyphicon-star').addClass('glyphicon-star-empty');
        text.text('已收藏');
        btn.removeClass('btn-primary').addClass('btn-warning');
    } else {
        icon.removeClass('glyphicon-star-empty').addClass('glyphicon-star');
        text.text('收藏');
        btn.removeClass('btn-warning').addClass('btn-primary');
    }
}

位置src/main/webapp/js/my.js 第2487-2507行

如果收藏成功,按钮文字从"收藏"变成"已收藏",按钮样式从蓝色变成黄色;如果取消收藏成功,按钮文字从"已收藏"变成"收藏",按钮样式从黄色变成蓝色。

这样,用户点击收藏按钮,数据就从页面发送到后端,保存到数据库,然后页面按钮状态更新,整个流程就完成了。


功能二:查看收藏列表

功能说明

收藏列表功能是用户可以查看自己收藏的所有菜品,方便快速访问。

抛出问题:用户进入用户中心查看收藏列表时,系统是怎么把收藏记录从数据库读取出来并展示到页面上的?

逐步追踪

第一步:找到用户操作的入口

用户在用户中心页面(user-center.jsp)点击"我的收藏"标签页,页面会自动调用loadMyFavorites函数加载收藏列表。

第二步:追踪前端JavaScript处理

在user-center.jsp中,找到loadMyFavorites函数:

javascript 复制代码
// [1] 加载我的收藏函数(数据流向:前端 → Controller)
function loadMyFavorites() {
    // [2] 发送AJAX GET请求获取收藏列表
    $.ajax({
        url: '${pageContext.request.contextPath}/user/getMyFavorites',
        type: 'GET',
        // [3] 请求成功回调:处理后端返回的结果(数据流向:Controller → 前端)
        success: function (response) {
            if (response.success) {
                // [4] 调用渲染函数,将收藏列表显示到页面
                renderFavoriteList(response.data);
            }
        }
    });
}

位置src/main/webapp/jsp/user-center.jsp 第353-364行

渲染函数renderFavoriteList(data)将后端返回的收藏列表渲染到页面:

javascript 复制代码
// [1] 渲染收藏列表函数
function renderFavoriteList(data) {
    const container = $('#favoriteList');
    container.empty();  // [2] 清空容器

    // [3] 判断是否有收藏记录
    if (data.length === 0) {
        container.html('<div class="empty-state">暂无收藏记录</div>');
        return;
    }

    // [4] 遍历收藏列表,为每个收藏项生成HTML
    data.forEach(function (item) {
        const html = '<div class="favorite-item">' +
            '<div class="item-info">' +
                '<h3>' + item.dishName + ' - ¥' + item.price + '</h3>' +
                '<p>' + item.storeName + '(' + item.canteenName + ')</p>' +
            '</div>' +
            '<div class="item-actions">' +
                '<button class="btn-primary" onclick="viewDish(' + item.dishId + ')">查看详情</button>' +
                '<button class="btn-danger" onclick="removeFavorite(' + item.dishId + ')">取消收藏</button>' +
            '</div>' +
        '</div>';
        container.append(html);
    });
}

位置src/main/webapp/jsp/user-center.jsp 第418-441行

第三步:追踪到Controller层

根据请求URL/user/getMyFavorites,找到UserControllergetMyFavorites方法:

java 复制代码
// [1] 处理GET请求 /user/getMyFavorites(数据流向:前端 → Controller)
@RequestMapping("/getMyFavorites")
@ResponseBody
public Result getMyFavorites(HttpServletRequest request) {
    try {
        // [2] 从Session中获取当前登录用户(01-用户模块提供的USER_SESSION)
        HttpSession session = request.getSession();
        User user = (User) session.getAttribute("USER_SESSION");
        if (user == null) {
            return new Result(false, 401, "请先登录", null);
        }
        Long userId = user.getUserId();
        
        // [3] 调用Service层获取收藏列表(数据流向:Controller → Service)
        List<Favorite> favorites = favoriteService.getUserFavorites(userId);
        // [4] 封装成Result对象返回(数据流向:Controller → 前端)
        return new Result(true, "获取收藏列表成功", favorites);
    } catch (Exception e) {
        logger.error("获取收藏列表失败", e);
        return new Result(false, "获取收藏列表失败:" + e.getMessage());
    }
}

位置src/main/java/com/scfs/controller/UserController.java 第238-259行

第四步:追踪到Service层

Controller调用favoriteService.getUserFavorites(userId),Service层实现:

java 复制代码
// [1] 获取用户收藏列表(数据流向:Service → Mapper)
@Override
public List<Favorite> getUserFavorites(Long userId) {
    if (userId == null) {
        return null;
    }
    try {
        // [2] 调用Mapper查询收藏列表(数据流向:Service → Mapper)
        return favoriteMapper.findByUserId(userId);
    } catch (Exception e) {
        e.printStackTrace();
        return null;
    }
}

位置src/main/java/com/scfs/service/impl/FavoriteServiceImpl.java 第60-71行

第五步:追踪到Mapper层

Service调用favoriteMapper.findByUserId(userId),Mapper层实现:

java 复制代码
// [1] 根据用户ID查询收藏列表(数据流向:Mapper → 数据库)
@Select("SELECT f.favorite_id AS favoriteId, f.user_id AS userId, f.dish_id AS dishId, " +
        "f.create_time AS createTime, " +
        "d.dish_name AS dishName, d.price, d.image_url AS imageUrl, " +
        "s.store_name AS storeName, c.canteen_name AS canteenName " +
        "FROM favorite f " +
        "LEFT JOIN dish d ON f.dish_id = d.dish_id " +
        "LEFT JOIN store s ON d.store_id = s.store_id " +
        "LEFT JOIN canteen c ON s.canteen_id = c.canteen_id " +
        "WHERE f.user_id = #{userId} " +
        "ORDER BY f.create_time DESC")
List<Favorite> findByUserId(@Param("userId") Long userId);

位置src/main/java/com/scfs/mapper/FavoriteMapper.java 第44-65行

执行顺序说明

  • FROM favorite f:从收藏表开始查询
  • LEFT JOIN dish d:关联菜品表,获取菜品信息
  • LEFT JOIN store s:关联店铺表,获取店铺信息
  • LEFT JOIN canteen c:关联食堂表,获取食堂信息
  • WHERE f.user_id = #{userId}:只查询当前用户的收藏记录
  • ORDER BY f.create_time DESC:按收藏时间倒序排列(最新的在前)
  • 数据库执行SQL查询后,返回List<Favorite>(数据流向:数据库 → Mapper → Service)
第六步:数据返回流程

完整的数据返回路径

  1. 数据库 → Mapper:数据库执行SELECT查询,通过JOIN关联查询获取收藏记录及关联的菜品、店铺、食堂信息,返回给Mapper
  2. Mapper → Service :Mapper将查询结果映射为List<Favorite>对象,返回给Service
  3. Service → Controller:Service直接返回收藏列表给Controller
  4. Controller处理
    • Controller封装成Result对象,successtruedata为收藏列表
    • 返回给前端(数据流向:Controller → 前端)
  5. 前端处理
    • JavaScript接收到Result对象
    • 判断response.success是否为true
    • 如果成功,调用renderFavoriteList(response.data)渲染收藏列表
    • renderFavoriteList()遍历收藏列表数组,为每个收藏项生成HTML,包含菜品名称、价格、店铺名称、食堂名称,以及"查看详情"和"取消收藏"按钮
    • 将HTML插入到页面的收藏列表容器中

这样,用户点击"我的收藏"标签页,系统就从数据库查询收藏记录,通过JOIN查询获取完整信息,然后渲染到页面上,整个流程就完成了。

收藏列表加载时序图

数据库 FavoriteMapper FavoriteService UserController JavaScript user-center.jsp 用户 数据库 FavoriteMapper FavoriteService UserController JavaScript user-center.jsp 用户 [1] 点击"我的收藏"标签页 [2] 触发loadMyFavorites() [3] AJAX GET /user/getMyFavorites [4] 从Session获取用户信息 [5] 调用favoriteService.getUserFavorites() [6] 调用favoriteMapper.findByUserId() [7] 执行SQL: SELECT f.*, d.*, s.*, c.* FROM favorite f LEFT JOIN dish d ON f.dish_id = d.dish_id LEFT JOIN store s ON d.store_id = s.store_id LEFT JOIN canteen c ON s.canteen_id = c.canteen_id WHERE f.user_id = ? ORDER BY f.create_time DESC [8] 返回收藏记录列表(包含关联信息) [9] 返回List<Favorite> [10] 返回List<Favorite> [11] 封装成Result对象 [12] 返回Result对象 (success, message, data=收藏列表) [13] 判断response.success [14] 调用renderFavoriteList(response.data) [15] 遍历收藏列表,生成HTML卡片 (包含菜品、店铺、食堂信息) [16] 将HTML插入到收藏列表容器 [17] 显示收藏列表

为什么这样设计

为什么使用LEFT JOIN关联查询,而不是分别查询?

  • 原因1:性能优化

    • 一次SQL查询获取所有需要的数据,减少数据库交互次数
    • 避免N+1查询问题(如果分别查询,需要1次查询收藏列表 + N次查询菜品信息)
    • 提升查询性能,减少网络传输开销
  • 原因2:数据完整性

    • 确保获取的数据是同一时刻的快照,避免数据不一致
    • 如果分别查询,可能在两次查询之间数据发生变化
    • 保证数据的准确性和一致性
  • 原因3:代码简洁

    • 一次查询获取所有数据,代码逻辑更简单
    • 减少Service层的处理逻辑
    • 便于维护和扩展

功能三:最近浏览记录

功能说明

最近浏览功能是系统自动记录用户查看过的菜品,方便用户快速回访。

抛出问题:用户浏览菜品详情页时,系统是怎么自动记录浏览记录的?浏览记录是怎么保存到数据库的?

逐步追踪

第一步:找到用户操作的入口

用户在店铺详情页点击某个菜品,或者在其他页面点击查看菜品详情,会跳转到菜品详情页(dish.jsp)。当页面加载时,系统会自动记录这次浏览。

第二步:追踪到Controller层

DishControllergetDishById方法中,当获取菜品详情时,会自动记录浏览记录:

java 复制代码
// [1] 在获取菜品详情时自动记录浏览(数据流向:Controller → Service)
@GetMapping("/{dishId}")
@ResponseBody
public Result getDishById(@PathVariable Long dishId, HttpSession session) {
    try {
        // [2] 获取菜品详情(省略其他代码)
        Dish dish = dishService.findById(dishId);
        
        // [3] 从Session中获取当前登录用户
        User user = (User) session.getAttribute("USER_SESSION");
        // [4] 如果用户已登录,自动记录浏览记录
        if (user != null && user.getUserId() != null) {
            try {
                // [5] 获取RecentViewService服务
                RecentViewService recentViewService = applicationContext.getBean(RecentViewService.class);
                // [6] 调用Service记录浏览(数据流向:Controller → Service)
                recentViewService.recordView(user.getUserId(), dishId);
            } catch (Exception e) {
                logger.warn("记录浏览历史失败", e);
                // 记录失败不影响菜品详情返回
            }
        }
        
        return new Result(true, "获取菜品详情成功", dish);
    } catch (Exception e) {
        logger.error("获取菜品详情失败", e);
        return new Result(false, "获取菜品详情失败");
    }
}

位置src/main/java/com/scfs/controller/DishController.java 第162行

关键点:浏览记录是自动记录的,不需要用户主动操作。当用户访问菜品详情页时,系统会自动记录这次浏览。

第三步:追踪到Service层

Controller调用recentViewService.recordView(userId, dishId),Service层实现:

java 复制代码
// [1] 记录浏览记录(数据流向:Service → Mapper)
@Override
@Transactional  // [2] 使用事务,确保数据一致性
public boolean recordView(Long userId, Long dishId) {
    // [3] 参数验证
    if (userId == null || dishId == null) {
        return false;
    }
    try {
        // [4] 创建浏览记录对象
        RecentView recentView = new RecentView(userId, dishId);
        // [5] 插入或更新浏览记录(数据流向:Service → Mapper)
        return recentViewMapper.insertOrUpdate(recentView) > 0;
    } catch (Exception e) {
        e.printStackTrace();
        return false;
    }
}

位置src/main/java/com/scfs/service/impl/RecentViewServiceImpl.java 第26-38行

第四步:追踪到Mapper层

Service调用recentViewMapper.insertOrUpdate(recentView),Mapper层实现:

java 复制代码
// [1] 插入或更新浏览记录到数据库(数据流向:Mapper → 数据库)
@Insert("INSERT INTO recent_view (user_id, dish_id, view_time) VALUES (#{userId}, #{dishId}, NOW()) " +
        "ON DUPLICATE KEY UPDATE view_time = NOW()")
@Options(useGeneratedKeys = true, keyProperty = "viewId", keyColumn = "view_id")
int insertOrUpdate(RecentView recentView);

位置src/main/java/com/scfs/mapper/RecentViewMapper.java 第21-24行

执行顺序说明

  • INSERT INTO recent_view:尝试插入新的浏览记录
  • ON DUPLICATE KEY UPDATE view_time = NOW():如果存在相同的user_iddish_id组合(通过唯一索引uk_user_dish保证),则更新浏览时间为当前时间
  • 这样确保同一个菜品只保留一条浏览记录,但浏览时间总是最新的
  • 数据库执行SQL后,返回受影响的行数(数据流向:数据库 → Mapper → Service)
第五步:数据返回流程

完整的数据返回路径

  1. 数据库 → Mapper:数据库执行INSERT或UPDATE语句,返回受影响的行数(通常为1)给Mapper
  2. Mapper → Service:Mapper返回受影响的行数给Service
  3. Service → Controller :Service判断行数大于0,返回true给Controller
  4. Controller处理:浏览记录保存成功,继续返回菜品详情给前端

这样,用户浏览菜品详情页时,系统就自动记录浏览记录,使用ON DUPLICATE KEY UPDATE确保同一菜品只保留最新的浏览时间,整个流程就完成了。

浏览记录自动记录时序图

数据库 RecentViewMapper RecentViewService DishController JavaScript dish.jsp 用户 数据库 RecentViewMapper RecentViewService DishController JavaScript dish.jsp 用户 alt [记录不存在] [记录已存在] alt [用户已登录] [1] 访问菜品详情页 [2] 页面加载 [3] AJAX GET /dish/{dishId} [4] 获取菜品详情 [5] 从Session获取用户信息 [6] 判断用户是否已登录 [7] 调用recentViewService.recordView() [8] 开始事务 [9] 调用recentViewMapper.insertOrUpdate() [10] 执行SQL: INSERT INTO recent_view (user_id, dish_id, view_time) VALUES (?, ?, NOW()) ON DUPLICATE KEY UPDATE view_time = NOW() [11] 返回受影响行数(1,插入) [12] 返回受影响行数(1,更新) [13] 返回受影响行数 [14] 提交事务 [15] 返回true [16] 返回Result对象 (success, message, data=菜品详情) [17] 显示菜品详情 [18] 页面显示完成 (浏览记录已自动记录)

为什么这样设计

为什么使用ON DUPLICATE KEY UPDATE而不是先查询再插入/更新?

  • 原因1:性能优化

    • 一次SQL操作完成插入或更新,减少数据库交互次数
    • 避免先查询再判断的两次数据库操作
    • 提升性能,减少数据库负载
  • 原因2:原子性

    • 在同一个SQL语句中完成,保证操作的原子性
    • 避免并发情况下的数据不一致问题
    • 使用唯一索引保证数据唯一性
  • 原因3:代码简洁

    • 不需要在Service层判断记录是否存在
    • 代码逻辑更简单,易于维护
    • 减少出错的可能性

功能四:查看最近浏览列表

功能说明

最近浏览列表功能是用户可以查看自己最近浏览过的菜品,方便快速回访。

抛出问题:用户进入用户中心查看最近浏览列表时,系统是怎么把浏览记录从数据库读取出来并展示到页面上的?

逐步追踪

第一步:找到用户操作的入口

用户在用户中心页面(user-center.jsp)点击"最近浏览"标签页,页面会自动调用loadRecentViews函数加载浏览列表。

第二步:追踪前端JavaScript处理

在user-center.jsp中,找到loadRecentViews函数:

javascript 复制代码
// [1] 加载最近浏览函数(数据流向:前端 → Controller)
function loadRecentViews() {
    // [2] 发送AJAX GET请求获取最近浏览列表
    $.ajax({
        url: '${pageContext.request.contextPath}/user/getRecentViews',
        type: 'GET',
        // [3] 请求成功回调:处理后端返回的结果(数据流向:Controller → 前端)
        success: function (response) {
            if (response.success) {
                // [4] 调用渲染函数,将最近浏览列表显示到页面
                renderRecentList(response.data);
            }
        }
    });
}

位置src/main/webapp/jsp/user-center.jsp 第366-377行

渲染函数renderRecentList(data)将后端返回的最近浏览列表渲染到页面(与收藏列表类似)。

第三步:追踪到Controller层

根据请求URL/user/getRecentViews,找到UserControllergetRecentViews方法:

java 复制代码
// [1] 处理GET请求 /user/getRecentViews(数据流向:前端 → Controller)
@RequestMapping("/getRecentViews")
@ResponseBody
public Result getRecentViews(HttpServletRequest request) {
    try {
        // [2] 从Session中获取当前登录用户(01-用户模块提供的USER_SESSION)
        HttpSession session = request.getSession();
        User user = (User) session.getAttribute("USER_SESSION");
        if (user == null) {
            return new Result(false, 401, "请先登录", null);
        }
        Long userId = user.getUserId();
        
        // [3] 获取RecentViewService服务
        RecentViewService recentViewService = org.springframework.web.context.support.WebApplicationContextUtils
                .getWebApplicationContext(request.getServletContext())
                .getBean(RecentViewService.class);
        
        // [4] 调用Service层获取最近浏览列表(数据流向:Controller → Service)
        List<RecentView> recentViews = recentViewService.getUserRecentViews(userId, 20);  // 限制20条
        // [5] 封装成Result对象返回(数据流向:Controller → 前端)
        return new Result(true, "获取最近浏览列表成功", recentViews);
    } catch (Exception e) {
        logger.error("获取最近浏览列表失败", e);
        return new Result(false, "获取最近浏览列表失败:" + e.getMessage());
    }
}

位置src/main/java/com/scfs/controller/UserController.java 第266-290行

第四步:追踪到Service层

Controller调用recentViewService.getUserRecentViews(userId, 20),Service层实现:

java 复制代码
// [1] 获取用户最近浏览列表(数据流向:Service → Mapper)
@Override
public List<RecentView> getUserRecentViews(Long userId, Integer limit) {
    // [2] 参数验证
    if (userId == null) {
        return null;
    }
    // [3] 设置默认限制数量
    if (limit == null || limit <= 0) {
        limit = 20; // 默认20条
    }
    try {
        // [4] 调用Mapper查询最近浏览列表(数据流向:Service → Mapper)
        return recentViewMapper.findByUserId(userId, limit);
    } catch (Exception e) {
        e.printStackTrace();
        return null;
    }
}

位置src/main/java/com/scfs/service/impl/RecentViewServiceImpl.java 第40-54行

第五步:追踪到Mapper层

Service调用recentViewMapper.findByUserId(userId, limit),Mapper层实现:

java 复制代码
// [1] 根据用户ID查询最近浏览列表(数据流向:Mapper → 数据库)
@Select("SELECT rv.view_id AS viewId, rv.user_id AS userId, rv.dish_id AS dishId, " +
        "rv.view_time AS viewTime, " +
        "d.dish_name AS dishName, d.price, d.image_url AS imageUrl, " +
        "s.store_name AS storeName, c.canteen_name AS canteenName " +
        "FROM recent_view rv " +
        "LEFT JOIN dish d ON rv.dish_id = d.dish_id " +
        "LEFT JOIN store s ON d.store_id = s.store_id " +
        "LEFT JOIN canteen c ON s.canteen_id = c.canteen_id " +
        "WHERE rv.user_id = #{userId} " +
        "ORDER BY rv.view_time DESC " +
        "LIMIT #{limit}")
List<RecentView> findByUserId(@Param("userId") Long userId, @Param("limit") Integer limit);

位置src/main/java/com/scfs/mapper/RecentViewMapper.java 第26-35行

执行顺序说明

  • FROM recent_view rv:从最近浏览表开始查询
  • LEFT JOIN dish d:关联菜品表,获取菜品信息
  • LEFT JOIN store s:关联店铺表,获取店铺信息
  • LEFT JOIN canteen c:关联食堂表,获取食堂信息
  • WHERE rv.user_id = #{userId}:只查询当前用户的浏览记录
  • ORDER BY rv.view_time DESC:按浏览时间倒序排列(最新的在前)
  • LIMIT #{limit}:限制返回的记录数(默认20条)
  • 数据库执行SQL查询后,返回List<RecentView>(数据流向:数据库 → Mapper → Service)
第六步:数据返回流程

完整的数据返回路径

  1. 数据库 → Mapper:数据库执行SELECT查询,通过JOIN关联查询获取浏览记录及关联的菜品、店铺、食堂信息,返回给Mapper
  2. Mapper → Service :Mapper将查询结果映射为List<RecentView>对象,返回给Service
  3. Service → Controller:Service直接返回最近浏览列表给Controller
  4. Controller处理
    • Controller封装成Result对象,successtruedata为最近浏览列表
    • 返回给前端(数据流向:Controller → 前端)
  5. 前端处理
    • JavaScript接收到Result对象
    • 判断response.success是否为true
    • 如果成功,调用renderRecentList(response.data)渲染最近浏览列表
    • renderRecentList()遍历浏览列表数组,为每个浏览项生成HTML,包含菜品名称、价格、店铺名称、食堂名称、浏览时间等信息
    • 将HTML插入到页面的最近浏览列表容器中

这样,用户点击"最近浏览"标签页,系统就从数据库查询浏览记录,通过JOIN查询获取完整信息,然后渲染到页面上,整个流程就完成了。

最近浏览列表加载时序图

数据库 RecentViewMapper RecentViewService UserController JavaScript user-center.jsp 用户 数据库 RecentViewMapper RecentViewService UserController JavaScript user-center.jsp 用户 [1] 点击"最近浏览"标签页 [2] 触发loadRecentViews() [3] AJAX GET /user/getRecentViews [4] 从Session获取用户信息 [5] 调用recentViewService.getUserRecentViews(userId, 20) [6] 调用recentViewMapper.findByUserId() [7] 执行SQL: SELECT rv.*, d.*, s.*, c.* FROM recent_view rv LEFT JOIN dish d ON rv.dish_id = d.dish_id LEFT JOIN store s ON d.store_id = s.store_id LEFT JOIN canteen c ON s.canteen_id = c.canteen_id WHERE rv.user_id = ? ORDER BY rv.view_time DESC LIMIT 20 [8] 返回浏览记录列表(包含关联信息) [9] 返回List<RecentView> [10] 返回List<RecentView> [11] 封装成Result对象 [12] 返回Result对象 (success, message, data=浏览列表) [13] 判断response.success [14] 调用renderRecentList(response.data) [15] 遍历浏览列表,生成HTML卡片 (包含菜品、店铺、食堂信息、浏览时间) [16] 将HTML插入到浏览列表容器 [17] 显示最近浏览列表

为什么这样设计

为什么限制最近浏览列表为20条?

  • 原因1:性能优化

    • 限制返回数量可以减少数据库查询和网络传输开销
    • 避免用户浏览记录过多导致查询缓慢
    • 提升页面加载速度
  • 原因2:用户体验

    • 20条记录足够用户查看最近的浏览历史
    • 如果记录太多,用户可能不会全部查看
    • 符合实际使用场景
  • 原因3:数据管理

    • 可以定期清理旧的浏览记录

    • 避免数据库数据无限增长

    • 便于后续的数据维护
      "LIMIT #{limit}")
      List findByUserId(@Param("userId") Long userId, @Param("limit") Integer limit);

      位置src/main/java/com/scfs/mapper/RecentViewMapper.java 第35-56行

      这个SQL查询通过JOIN关联查询,不仅获取浏览记录,还获取菜品名称、价格、图片、店铺名称、食堂名称等信息。查询结果按浏览时间倒序排列,只返回最近的N条记录(默认20条)。

      第六步:数据如何返回

      数据库查询成功后,后端返回Result对象,包含浏览列表数据。前端JavaScript接收到响应后,调用renderRecentList函数渲染列表:

      javascript 复制代码
      function renderRecentList(data) {
          const container = $('#recentList');
          container.empty();
      
          if (data.length === 0) {
              container.html('<div class="empty-state">暂无浏览记录</div>');
              return;
          }
      
          data.forEach(function (item) {
              // 点击整个菜品框进入菜品页面
              const html = '<div class="recent-item" style="cursor: pointer;" onclick="viewDish(' + item.dishId + ', event)">' +
                  '<div class="item-info">' +
                  '<h3 style="font-size: 14px; margin-bottom: 5px;">' + (item.dishName || '未知菜品') + ' - ¥' + (item.price || '0') + '</h3>' +
                  '<p style="font-size: 12px; color: #666; margin: 0;">' + (item.storeName || '') + '(' + (item.canteenName || '') + ')</p>' +
                  '</div>' +
              '</div>';
              container.append(html);
          });
      }

位置src/main/webapp/jsp/user-center.jsp 第443-463行

函数遍历浏览列表数据,为每个浏览项生成HTML,包括菜品名称、价格、店铺名称、食堂名称。点击整个菜品框可以跳转到菜品详情页。

这样,用户点击"最近浏览"标签页,系统就从数据库查询浏览记录,通过JOIN查询获取完整信息,然后渲染到页面上,整个流程就完成了。


总结

收藏与最近浏览模块的完整流程:

  1. 收藏功能

    • 用户点击收藏按钮,前端调用toggleDishFavorite函数
    • 函数判断当前状态,构造请求URL(/favorite/add/favorite/remove
    • 发送POST请求到后端,携带菜品ID
    • 后端Controller从Session获取用户ID,调用Service层
    • Service层检查是否已收藏,未收藏则插入记录,已收藏则删除记录
    • Mapper层执行SQL插入或删除favorite表中的记录
    • 后端返回结果,前端更新按钮状态
  2. 收藏列表

    • 用户点击"我的收藏"标签页,前端调用loadMyFavorites函数
    • 发送GET请求到/user/getMyFavorites接口
    • 后端Controller获取用户ID,调用Service层
    • Service层调用Mapper层查询数据库
    • Mapper层通过JOIN查询获取收藏记录和菜品、店铺、食堂的完整信息
    • 后端返回收藏列表,前端渲染到页面
  3. 最近浏览记录

    • 用户浏览菜品详情页时,Controller自动调用RecentViewService.recordView方法
    • Service层创建浏览记录,调用Mapper层
    • Mapper层使用ON DUPLICATE KEY UPDATE语法,如果已存在则更新浏览时间,如果不存在则插入新记录
    • 确保同一个菜品只保留一条浏览记录,但浏览时间总是最新的
  4. 最近浏览列表

    • 用户点击"最近浏览"标签页,前端调用loadRecentViews函数
    • 发送GET请求到/user/getRecentViews接口,默认返回20条
    • 后端Controller获取用户ID,调用Service层
    • Service层调用Mapper层查询数据库
    • Mapper层通过JOIN查询获取浏览记录和菜品、店铺、食堂的完整信息,按浏览时间倒序排列
    • 后端返回浏览列表,前端渲染到页面

学习建议:

  • 启动项目,登录后访问菜品详情页,观察收藏按钮的状态变化
  • 点击收藏按钮,查看数据库favorite表的变化
  • 在用户中心查看收藏列表和最近浏览列表
  • 理解favorite表和recent_view表的结构和关系
  • 注意ON DUPLICATE KEY UPDATE的使用场景和效果,理解为什么同一个菜品只保留一条浏览记录
相关推荐
小高不会迪斯科8 小时前
CMU 15445学习心得(二) 内存管理及数据移动--数据库系统如何玩转内存
数据库·oracle
e***8908 小时前
MySQL 8.0版本JDBC驱动Jar包
数据库·mysql·jar
l1t8 小时前
在wsl的python 3.14.3容器中使用databend包
开发语言·数据库·python·databend
冬奇Lab9 小时前
Android系统启动流程深度解析:从Bootloader到Zygote的完整旅程
android·源码阅读
失忆爆表症10 小时前
03_数据库配置指南:PostgreSQL 17 + pgvector 向量存储
数据库·postgresql
AI_567810 小时前
Excel数据透视表提速:Power Query预处理百万数据
数据库·excel
SQL必知必会11 小时前
SQL 窗口帧:ROWS vs RANGE 深度解析
数据库·sql·性能优化
泓博11 小时前
Android中仿照View selector自定义Compose Button
android·vue.js·elementui
Gauss松鼠会11 小时前
【GaussDB】GaussDB数据库开发设计之JDBC高可用性
数据库·数据库开发·gaussdb
+VX:Fegn089511 小时前
计算机毕业设计|基于springboot + vue鲜花商城系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计