基于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的使用场景和效果,理解为什么同一个菜品只保留一条浏览记录
相关推荐
Kapaseker7 小时前
一杯美式搞懂 Any、Unit、Nothing
android·kotlin
黄林晴8 小时前
你的 Android App 还没接 AI?Gemini API 接入全攻略
android
恋猫de小郭18 小时前
2026 Flutter VS React Native ,同时在 AI 时代 VS Native 开发,你没见过的版本
android·前端·flutter
冬奇Lab19 小时前
PowerManagerService(上):电源状态与WakeLock管理
android·源码阅读
NineData1 天前
NineData智能数据管理平台新功能发布|2026年1-2月
数据库·sql·数据分析
IvorySQL1 天前
双星闪耀温哥华:IvorySQL 社区两项议题入选 PGConf.dev 2026
数据库·postgresql·开源
BoomHe1 天前
Now in Android 架构模式全面分析
android·android jetpack
ma_king1 天前
入门 java 和 数据库
java·数据库·后端
jiayou641 天前
KingbaseES 实战:审计追踪配置与运维实践
数据库
二流小码农1 天前
鸿蒙开发:上传一张参考图片便可实现页面功能
android·ios·harmonyos