苍穹外卖【day6|微信登录与商品浏览功能】

🌈个人主页 :一条泥憨鱼 (欢迎各位大佬莅临)

🎬精选专栏:数据结构与算法Java,AI与Agent

今日学习大纲

HttpClient

让我们可以在Java程序中通过编码的方式来发送http的请求

由于我们阿里云oss的包里面已经包含了相关坐标,所以我们可以不用再去自己导入依赖了

入门案例

在这里建一个测试类

复制代码
测试通过httpClient发送GET方式的请求
java 复制代码
//测试通过httpClient发送GET方式的请求
    @Test
    public void testGet() throws Exception{
        //创建httpClient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();
        //创建请求对象
        HttpGet httpGet = new HttpGet("http://localhost:8080//user/shop/status");
        //发送请求,接受响应结果
        CloseableHttpResponse response = httpClient.execute(httpGet);

        //获取服务端返回的代码
        int statusCode = response.getStatusLine().getStatusCode();
        System.out.println("服务端返回的状态码为:"+statusCode);

        HttpEntity entity = response.getEntity();
        String body = EntityUtils.toString(entity);
        System.out.println("服务端返回的数据为:"+body);

        //关闭资源
        response.close();
        httpClient.close();

    }

测试的时候一定要提前把项目跑起来,不然就执行不到了

复制代码
测试通过httpClient发送GET方式的请求
java 复制代码
@Test
    public void testPost() throws Exception{
        //创建httpClient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();
        //创建请求对象
        HttpPost httpPost = new HttpPost("http://localhost:8080//admin/employee/login");

        JSONObject jsonObject=new JSONObject();
        jsonObject.put("username","admin");
        jsonObject.put("password","123456");

        StringEntity entity=new StringEntity(jsonObject.toString());

        //指定请求编码方式
        entity.setContentEncoding("utf-8");
        //数据格式
        entity.setContentType("application/json");
        httpPost.setEntity(entity);
        //发送请求
        CloseableHttpResponse response = httpClient.execute(httpPost);
        //解析返回结果
        int code = response.getStatusLine().getStatusCode();
        System.out.println("响应码为:"+code);
        HttpEntity entity1 = response.getEntity();
        String body = EntityUtils.toString(entity1);
        System.out.println("响应数据为:"+body);
        //关闭资源
        response.close();
        httpClient.close();

    }

在utils工具类里面其实已经为我们准备好了工具包,我们可以快速实现httpClient对象的创建等操作

微信小程序开发

准备工作

先跟着视频注册一个自己的小程序,然后把信息和类目填好

然后再开发管理这个栏目中获取密钥(ID和密钥在后续代码中都会用上)

安装微信开发者工具,登录

创建小程序

界面展示

如果项目启动后直接就结束了,一定要去把之前test下的测试文件都给删除掉!

需求分析和设计

接口设计

微信登录的整个流程,就像你去一家餐厅吃饭:

  1. 你(小程序用户)点击"微信登录"按钮 → 微信给你一个临时暗号(code)

  2. 你把暗号交给服务员(后端 Controller)

  3. 服务员拿着暗号去问微信官方:"这个暗号是谁的?" → 微信返回一个唯一身份标识(openid)

  4. 服务员查花名册(数据库):这个人来过吗?

  • 来过 → 直接欢迎回来

  • 没来过 → 先登记(自动注册),再欢迎

  1. 服务员给你发一张会员卡(JWT令牌),以后每次来只要出示会员卡就行

整个微信登录涉及 13 个文件 ,按照数据流动的顺序,可以分为 5 层

java 复制代码
┌─────────────────────────────────────────────────┐
│          微信小程序(前端)                          │
│   用户点击登录 → wx.login() 拿到 code → 发给后端      │
└──────────────────┬──────────────────────────────┘
                   │  POST /user/user/login
                   │  请求体: { "code": "xxx" }
                   ▼
┌─────────────────────────────────────────────────┐
│  ① Controller 层  --- 接待员(接收请求)               │
│     UserController.java                          │
└──────────────────┬──────────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────────┐
│  ② Service 层  --- 业务员(处理核心逻辑)               │
│     UserService.java(接口)                       │
│     UserServiceImpl.java(实现)                   │
│     ├─ 拿着code去微信服务器换 openid                 │
│     ├─ 查数据库有没有这个用户(没有就自动注册)          │
│     └─ 返回 User 对象                             │
└──────────────────┬──────────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────────┐
│  ③ Mapper 层  --- 仓库管理员(操作数据库)              │
│     UserMapper.java + UserMapper.xml              │
│     ├─ getByOpenid()  查用户                      │
│     └─ insert()       插入新用户                   │
└──────────────────┬──────────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────────┐
│  ④ 工具层  --- 工具箱(通用能力)                       │
│     HttpClientUtil.java  → 发HTTP请求              │
│     JwtUtil.java         → 生成/解析JWT令牌         │
│     BaseContext.java     → 线程级存储当前用户ID       │
└──────────────────┬──────────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────────┐
│  ⑤ 拦截器层  --- 门卫(验证身份)                       │
│     JwtTokenUserInterceptor.java                 │
│     WebMvcConfiguration.java                     │
│     ├─ 登录接口 /user/user/login → ⚡放行(免验证)    │
│     └─ 其他 /user/** 接口     → 🔒拦截(必须带Token) │
└─────────────────────────────────────────────────┘

二、逐文件详解

第 1 步:前端发请求 → Controller 接收

UserController.java

java 复制代码
@RestController
@RequestMapping("/user/user")
public class UserController {

    @Autowired
    private UserService userService;      // 业务层
    @Autowired
    private JwtProperties jwtProperties;  // JWT配置(密钥、过期时间)

    @PostMapping("/login")
    public Result<UserLoginVO> login(@RequestBody UserLoginDTO userLoginDTO) {
        // 1. 调用 Service,完成微信登录(code → openid → 查库/注册)
        User user = userService.wxLogin(userLoginDTO);

        // 2. 生成 JWT 令牌(相当于给用户发一张"通行证")
        Map<String, Object> claims = new HashMap<>();
        claims.put(JwtClaimsConstant.USER_ID, user.getId());
        String token = JwtUtil.createJWT(
            jwtProperties.getUserSecretKey(),  // 签名密钥
            jwtProperties.getUserTtl(),         // 有效期
            claims
        );

        // 3. 组装返回数据:用户ID + openid + Token
        UserLoginVO userLoginVO = UserLoginVO.builder()
                .id(user.getId())
                .openid(user.getOpenid())
                .token(token)
                .build();

        return Result.success(userLoginVO);
    }
}
  • @RestController = 这个类是一个 Web 接口,专门接收 HTTP 请求
  • @RequestMapping("/user/user") = 所有接口地址都以 /user/user 开头
  • @PostMapping("/login") = 这个方法的完整地址是 POST /user/user/login
  • @RequestBody = 把前端传来的 JSON 数据自动转成 Java 对象

UserLoginDTO.java --- 前端发来的请求数据

java 复制代码
@Data
public class UserLoginDTO implements Serializable {
    private String code;  // 微信小程序调用 wx.login() 获取的临时凭证
}

UserLoginVO.java --- 后端返回给前端的响应数据

java 复制代码
@Data @Builder
public class UserLoginVO implements Serializable {
    private Long id;       // 用户ID
    private String openid; // 微信唯一标识
    private String token;  // JWT令牌(通行证)
}
第 2 步:核心业务逻辑 → Service 处理

UserService.java --- 接口(定义契约):

java 复制代码
public interface UserService {
    User wxLogin(UserLoginDTO userLoginDTO);
}

UserServiceImpl.java --- 实现(真正干活的地方)

java 复制代码
@Service
public class UserServiceImpl implements UserService {

    // 微信官方提供的接口地址,用于用 code 换取 openid
    public static final String WX_LOGIN = "https://api.weixin.qq.com/sns/jscode2session";

    @Autowired
    private WeChatProperties weChatProperties;  // 微信配置(appid、secret)
    @Autowired
    private UserMapper userMapper;              // 数据库操作

    public User wxLogin(UserLoginDTO userLoginDTO) {
        // ① 拿着 code + appid + secret 去微信服务器换 openid
        String openid = getOpenid(userLoginDTO);

        // ② 如果换不到 openid,说明登录失败
        if (openid == null) {
            throw new LoginFailedException(MessageConstant.LOGIN_FAILED);
        }

        // ③ 去数据库查:这个 openid 的用户存在吗?
        User user = userMapper.getByOpenid(openid);

        // ④ 如果不存在 → 新用户,自动注册(插入一条记录)
        if (user == null) {
            user = User.builder()
                    .openid(openid)
                    .createTime(LocalDateTime.now())
                    .build();
            userMapper.insert(user);
            // 注意:insert 后 user 对象的 id 会被 MyBatis 自动回填
        }

        // ⑤ 返回用户对象
        return user;
    }

    // 私有方法:调用微信接口,用 code 换 openid
    private String getOpenid(UserLoginDTO userLoginDTO) {
        Map<String, String> map = new HashMap<>();
        map.put("appid", weChatProperties.getAppid());   // 小程序ID
        map.put("secret", weChatProperties.getSecret()); // 小程序密钥
        map.put("js_code", userLoginDTO.getCode());      // 前端传来的code
        map.put("grant_type", "authorization_code");     // 固定值

        // 发 GET 请求到微信服务器
        String json = HttpClientUtil.doGet(WX_LOGIN, map);

        // 解析返回的 JSON,提取 openid
        JSONObject jsonObject = JSON.parseObject(json);
        String openid = jsonObject.getString("openid");
        return openid;
    }
}
  1. code 是什么? --- 微信小程序调用 wx.login() 后,微信会给一个临时码(code),有效期为 5 分钟,只能用一次
  2. 为什么需要后端去换? --- 因为需要 appid + secret(小程序秘钥),这两个东西绝对不能放在前端(会被盗用),必须由后端持有
  3. openid 是什么? --- 每个用户在每个小程序里都有一个唯一不变的 ID 标识,就像身份证号
java 复制代码
前端小程序              后端服务器               微信服务器
   │                      │                      │
   │──①wx.login()────────▶│                      │
   │◀─② 返回 code ────────│                      │
   │                      │                      │
   │──③ POST /login ─────▶│                      │
   │   { code: "abc" }    │──④ GET + code ──────▶│
   │                      │◀─⑤ 返回 openid ──────│
   │                      │                      │
   │                      │──⑥ 查数据库           │
   │                      │  (有→直接返回)        │
   │                      │  (无→插入再返回)      │
   │                      │                      │
   │                      │──⑦ 生成JWT令牌        │
   │◀─⑧ 返回 id+openid ───│                      │
   │    + token            │                      │
第 3 步:操作数据库 → Mapper 层

UserMapper.java --- Java 接口:

java 复制代码
@Mapper  // MyBatis 的注解,告诉 Spring 这是一个数据库操作接口
public interface UserMapper {

    @Select("select * from user where openid = #{openid}")
    User getByOpenid(String openid);  // 根据 openid 查用户

    void insert(User user);           // 插入新用户(SQL写在XML中)
}

UserMapper.xml --- XML SQL:

XML 复制代码
<mapper namespace="com.sky.mapper.UserMapper">

    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
        insert into user (openid, name, phone, sex, id_number, avatar, create_time)
        values (#{openid}, #{name}, #{phone}, #{sex}, #{idNumber}, #{avatar}, #{createTime})
    </insert>

</mapper>

关键点:useGeneratedKeys="true" keyProperty="id" --- 插入成功后,数据库自动生成的主键 id 会回填到 User 对象的 id 字段上,所以后面 Controller 里才能用 user.getId()

第 4 步:工具类 → 支撑能力

① HttpClientUtil.java --- HTTP 请求工具

就是一个"信使",帮后端用代码发 HTTP 请求到外部服务器(比如微信服务器)。

java 复制代码
public static String doGet(String url, Map<String, String> paramMap) {
    // 1. 创建一个 HTTP 客户端
    // 2. 把参数拼接到 URL 上(如 ?appid=xxx&secret=yyy&js_code=zzz)
    // 3. 发送 GET 请求
    // 4. 如果返回 200,读取响应内容并返回
    // 5. 关闭连接
}

② JwtUtil.java --- JWT 令牌工具

JWT 是什么? --- 你可以把它理解成一张"电子通行证"。登录成功后,后端发给你这张通行证,之后每次请求都带着它,后端验证通过后才让你访问。

java 复制代码
// 生成令牌(登录时用)
public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {
    // 加密算法 HS256
    // claims:要存储的数据(本例中存了 userId)
    // secretKey:签名密钥(防止伪造)
    // ttlMillis:过期时间(本项目配置了 7200000 毫秒 = 2 小时)
}

// 解析令牌(后续请求验证时用)
public static Claims parseJWT(String secretKey, String token) {
    // 用同样的密钥解密 token,取出里面的数据
}

③ BaseContext.java --- 线程级上下文

java 复制代码
public class BaseContext {
    public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    public static void setCurrentId(Long id) { threadLocal.set(id); }
    public static Long getCurrentId() { return threadLocal.get(); }
    public static void removeCurrentId() { threadLocal.remove(); }
}

ThreadLocal 是什么? --- Tomcat 每处理一个请求就会分配一个线程。ThreadLocal 可以在这个线程的整个生命周期内存储数据,每个线程互不干扰 。这样在 Service 层任何地方都能用 BaseContext.getCurrentId() 拿到当前登录用户的 ID,而不需要层层传参。

第 5 步:拦截器 --- 身份验证

JwtTokenUserInterceptor.java --- 用户端拦截器:

java 复制代码
@Component
public class JwtTokenUserInterceptor implements HandlerInterceptor {

    @Autowired
    private JwtProperties jwtProperties;

    public boolean preHandle(HttpServletRequest request, ...) {
        // 1. 从请求头中取出 token
        String token = request.getHeader(jwtProperties.getUserTokenName());
        //    注意:用户端的 token 放在名为 "authentication" 的请求头中
        //    管理端则是放在 "token" 请求头中

        // 2. 用 JwtUtil 解析验证 token
        try {
            Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token);
            Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString());

            // 3. 把用户ID存入 BaseContext,后续代码可以随时获取
            BaseContext.setCurrentId(userId);

            return true;  // 验证通过,放行
        } catch (Exception ex) {
            response.setStatus(401);  // 验证失败,返回 401 未授权
            return false;
        }
    }
}

WebMvcConfiguration.java --- 拦截器注册配置:

java 复制代码
@Configuration
public class WebMvcConfiguration extends WebMvcConfigurationSupport {

    @Autowired
    private JwtTokenUserInterceptor jwtTokenUserInterceptor;

    protected void addInterceptors(InterceptorRegistry registry) {
        // 用户端拦截器
        registry.addInterceptor(jwtTokenUserInterceptor)
                .addPathPatterns("/user/**")           // 拦截所有 /user/ 开头的请求
                .excludePathPatterns("/user/user/login")   // 但登录接口不需要拦截!
                .excludePathPatterns("/user/shop/status"); // 获取店铺状态也不需要拦截
    }
}
java 复制代码
用户请求 /user/category/list
         │
         ▼
    ┌─────────────┐
    │  拦截器      │  ← 检查请求头里有没有 token
    │  (门卫)      │
    └──────┬───────┘
           │
    ┌──────┴──────┐
    │ 有token?    │
    └──────┬──────┘
      有 ✓ │      无 ✗
           │        │
           ▼        ▼
    token有效?    返回 401
           │       (未授权)
    ┌──────┴──────┐
    │ 有效?      │
    └──────┬──────┘
      是 ✓ │      否 ✗
           │        │
           ▼        ▼
      放行,进入   返回 401
      Controller
第 6 步:配置文件

application.yml 中 JWT 相关配置:

java 复制代码
sky:
  jwt:
    admin-secret-key: itcast      # 管理端JWT密钥
    admin-ttl: 7200000            # 管理端Token有效期(2小时)
    admin-token-name: token       # 管理端Token放在哪个请求头
    user-secret-key: itheima      # 用户端JWT密钥
    user-ttl: 7200000             # 用户端Token有效期(2小时)
    user-token-name: authentication  # 用户端Token放在哪个请求头

application-dev.yml 中微信配置:

java 复制代码
sky:
  wechat:
    appid: xxx   # 你的微信小程序AppID
    secret: xxx   # 你的微信小程序密钥

两个值通过 WeChatProperties.java 读取:

java 复制代码
@Component
@ConfigurationProperties(prefix = "sky.wechat")
@Data
public class WeChatProperties {
    private String appid;   // 对应 sky.wechat.appid
    private String secret;  // 对应 sky.wechat.secret
    // ...
}

小结

步骤 谁在干活 做了什么
1 微信小程序 调用 wx.login() 拿到临时 code
2 微信小程序 POST /user/user/logincode 传给后端
3 UserController 接收请求,调用 UserService.wxLogin()
4 UserServiceImpl code + appid + secret 调用微信接口换 openid
5 HttpClientUtil 实际发出 HTTP GET 请求到微信服务器
6 UserMapper openid 查数据库,有就返回,没有就插入新记录
7 JwtUtil userId 生成 JWT 令牌(有效期 2 小时)
8 UserController id + openid + token 组装好返回给小程序
9 微信小程序 保存 token,以后每次请求都带上
10 JwtTokenUserInterceptor 拦截后续请求,验证 token 是否有效
11 BaseContext 验证通过后,把 userId 存入线程上下文,方便后续代码使用

商品浏览功能

需求分析和设计

查询分类接口

根据分类id查询菜品接口

根据分类id查询套餐接口

根据套餐id查询包含的菜品

一、功能全景图

用户在小程序上浏览商品,整体流程是 "先看分类 → 再看菜品/套餐 → 再看套餐里的具体菜品"

java 复制代码
┌─────────────────────────────────────────────────┐
│                  微信小程序页面                     │
│                                                   │
│  ┌─────────┐  ┌─────────┐  ┌─────────────────┐  │
│  │ ① 分类页 │  │ ② 菜品页 │  │ ③ 套餐详情页     │  │
│  │ 点击分类  │→│ 显示菜品  │  │ 点击套餐          │  │
│  │ 获取菜品  │  │ (含口味)  │  │ 显示里面的菜品    │  │
│  └─────────┘  └─────────┘  └─────────────────┘  │
└──────────────┬──────────────┬────────────────────┘
               │              │
               ▼              ▼
┌──────────────────────────────────────────────────┐
│              C端 3 个 Controller                   │
│                                                   │
│  ① CategoryController  →  GET /user/category/list │
│  ② DishController      →  GET /user/dish/list     │
│  ③ SetmealController   →  GET /user/setmeal/list  │
│                        →  GET /user/setmeal/dish/{id}│
└──────────────────────────────────────────────────┘

二、接口总览(4 个接口)

序号 接口地址 作用 返回数据
1 GET /user/category/list?type=1 查询分类列表 List<Category>
2 GET /user/dish/list?categoryId=3 按分类查菜品(含口味) List<DishVO>
3 GET /user/setmeal/list?categoryId=3 按分类查套餐 List<Setmeal>
4 GET /user/setmeal/dish/{套餐id} 查套餐里有哪些菜品 List<DishItemVO>

三、逐接口详解

接口 1:查询分类 GET /user/category/list

作用:用户打开小程序,先看到分类列表(如"热销榜"、"主食"、"饮品"等),点击某个分类才能看到下面的菜品。

CategoryController.java

java 复制代码
@RestController("userCategoryController")
@RequestMapping("/user/category")
public class CategoryController {

    @Autowired
    private CategoryService categoryService;

    @GetMapping("/list")
    public Result<List<Category>> list(Integer type) {
        List<Category> list = categoryService.list(type);
        return Result.success(list);
    }
}

数据流向:

CategoryController.list() → 复用已有的 CategoryService.list() → CategoryMapper.xml 的 SQL:

XML 复制代码
<select id="list" resultType="Category">
    select * from category
    where status = 1          -- 只查启用中的分类
    <if test="type != null">
        and type = #{type}     -- 如果传了type,就按类型过滤(1=菜品分类 2=套餐分类)
    </if>
    order by sort asc, create_time desc
</select>

type 参数是可选的。不传 type 就查全部分类;传 type=1 就只查菜品分类。

接口 2:按分类查菜品 GET /user/dish/list?categoryId=3

作用:用户点击某个分类(如"主食")后,显示该分类下的所有菜品,同时带上每个菜品的口味选项(如"微辣"、"中辣"、"不要香菜")。

DishController.java

java 复制代码
@RestController("userDishController")
@RequestMapping("/user/dish")
public class DishController {
    @Autowired
    private DishService dishService;

    @GetMapping("/list")
    public Result<List<DishVO>> list(Long categoryId) {
        // 构造查询条件:指定分类 + 只查起售中的菜品
        Dish dish = new Dish();
        dish.setCategoryId(categoryId);
        dish.setStatus(StatusConstant.ENABLE);  // status = 1

        // 调用 Service 查询菜品 + 口味
        List<DishVO> list = dishService.listWithFlavor(dish);
        return Result.success(list);
    }
}

这里调用的不是 DishService.list(),而是 DishService.listWithFlavor(),区别在于:

  • DishService.list() → 返回 List<Dish>(只有菜品基本信息,没有口味)
  • DishService.listWithFlavor() → 返回 List<DishVO>(菜品 + 口味一起返回)

DishServiceImpl.listWithFlavor()

java 复制代码
public List<DishVO> listWithFlavor(Dish dish) {
    // 第一步:查菜品基本信息
    List<Dish> dishList = dishMapper.list(dish);

    List<DishVO> dishVOList = new ArrayList<>();

    // 第二步:给每个菜品,查它的口味列表
    for (Dish d : dishList) {
        DishVO dishVO = new DishVO();
        BeanUtils.copyProperties(d, dishVO);

        // 根据菜品id查口味数据
        List<DishFlavor> flavors = dishFlavorMapper.getByDishId(d.getId());
        dishVO.setFlavors(flavors);

        dishVOList.add(dishVO);
    }

    return dishVOList;
}

数据模型:

DishVO.java --- 返回给前端的数据结构:

java 复制代码
public class DishVO {
    private Long id;            // 菜品ID
    private String name;        // 菜品名称(如"宫保鸡丁")
    private Long categoryId;    // 分类ID
    private BigDecimal price;   // 价格
    private String image;       // 图片URL
    private String description; // 描述
    private Integer status;     // 状态
    private List<DishFlavor> flavors; // 🔑 口味列表(如:"微辣"、"中辣"、"重辣")
}

DishFlavor.java --- 口味实体:

java 复制代码
public class DishFlavor {
    private Long id;
    private Long dishId;    // 属于哪个菜品
    private String name;    // 口味名称(如"辣度")
    private String value;   // 口味选项(如"[\"微辣\",\"中辣\",\"重辣\"]",JSON数组格式)
}
java 复制代码
dish 表(菜品)              dish_flavor 表(口味)
┌────┬──────────┬──────┐    ┌────┬─────────┬──────┬───────────────┐
│ id │ name     │ price│    │ id │ dish_id │ name │ value         │
├────┼──────────┼──────┤    ├────┼─────────┼──────┼───────────────┤
│ 1  │ 宫保鸡丁  │ 28.0 │    │ 1  │ 1       │ 辣度  │ ["微辣","中辣"]│
│ 2  │ 鱼香肉丝  │ 32.0 │    │ 2  │ 1       │ 忌口  │ ["不要香菜"]   │
└────┴──────────┴──────┘    └────┴─────────┴──────┴───────────────┘
接口 3:按分类查套餐 GET /user/setmeal/list?categoryId=3

作用:用户点击某个套餐分类后,显示该分类下的所有套餐(如"双人套餐"、"家庭套餐")。

SetmealController.java

java 复制代码
@RestController("userSetmealController")
@RequestMapping("/user/setmeal")
public class SetmealController {
    @Autowired
    private SetmealService setmealService;

    @GetMapping("/list")
    public Result<List<Setmeal>> list(Long categoryId) {
        Setmeal setmeal = new Setmeal();
        setmeal.setCategoryId(categoryId);
        setmeal.setStatus(StatusConstant.ENABLE);  // 只查起售的套餐

        List<Setmeal> list = setmealService.list(setmeal);
        return Result.success(list);
    }
}

对应 SQL(SetmealMapper.xml):

XML 复制代码
<select id="list" parameterType="Setmeal" resultType="Setmeal">
    select * from setmeal
    <where>
        <if test="name != null"> and name like concat('%',#{name},'%') </if>
        <if test="categoryId != null"> and category_id = #{categoryId} </if>
        <if test="status != null"> and status = #{status} </if>
    </where>
</select>
接口 4:查套餐里的菜品 GET /user/setmeal/dish/{id}

作用:用户点击某个套餐后,弹出套餐详情,显示"这个套餐里包含哪些菜品,每个菜品几份"。

SetmealController.java

java 复制代码
@GetMapping("/dish/{id}")
public Result<List<DishItemVO>> dishList(@PathVariable("id") Long id) {
    List<DishItemVO> list = setmealService.getDishItemById(id);
    return Result.success(list);
}

底层 SQL(SetmealMapper.java):

java 复制代码
@Select("select sd.name, sd.copies, d.image, d.description " +
        "from setmeal_dish sd left join dish d on sd.dish_id = d.id " +
        "where sd.setmeal_id = #{setmealId}")
List<DishItemVO> getDishItemBySetmealId(Long setmealId);

返回的数据模型 DishItemVO.java:

java 复制代码
public class DishItemVO {
    private String name;        // 菜品名称
    private Integer copies;     // 份数
    private String image;       // 菜品图片
    private String description; // 菜品描述
}
java 复制代码
setmeal 表(套餐)           setmeal_dish 表(套餐-菜品关联)       dish 表(菜品)
┌────┬──────────┬──────┐    ┌────┬──────────┬─────────┬──────┐
│ id │ name     │ price│    │ id │setmeal_id│dish_id  │copies│
├────┼──────────┼──────┤    ├────┼──────────┼─────────┼──────┤
│ 1  │ 双人套餐  │ 58.0 │    │ 1  │ 1        │ 1       │ 1    │  ← 宫保鸡丁 1份
│ 2  │ 家庭套餐  │ 98.0 │    │ 2  │ 1        │ 2       │ 1    │  ← 鱼香肉丝 1份
└────┴──────────┴──────┘    │ 3  │ 1        │ 3       │ 2    │  ← 米饭 2份
                             └────┴──────────┴─────────┴──────┘

四、完整数据流图

java 复制代码
微信小程序
    │
    ├──① 用户进入首页 ──→ GET /user/category/list
    │                      返回:[{id:1, name:"热销榜"}, {id:2, name:"主食"}, ...]
    │
    ├──② 用户点击"主食" ──→ GET /user/dish/list?categoryId=2
    │                      返回:[{id:5, name:"米饭", price:2.00, flavors:[]},
    │                            {id:6, name:"炒面", price:15.00, flavors:[{name:"辣度",value:"微辣/中辣"}]}]
    │
    ├──③ 用户点击"套餐" ──→ GET /user/setmeal/list?categoryId=3
    │                      返回:[{id:1, name:"双人套餐", price:58.00},
    │                            {id:2, name:"家庭套餐", price:98.00}]
    │
    └──④ 用户点击"双人套餐"──→ GET /user/setmeal/dish/1
                              返回:[{name:"宫保鸡丁", copies:1, image:"xxx"},
                                    {name:"鱼香肉丝", copies:1, image:"xxx"},
                                    {name:"米饭", copies:2, image:"xxx"}]

五、涉及的所有文件清单

层级 文件 作用
Controller CategoryController.java 分类浏览接口
DishController.java 菜品浏览接口
SetmealController.java 套餐浏览接口
Service CategoryService.java / CategoryServiceImpl.java 分类业务逻辑
DishService.java / DishServiceImpl.java 菜品业务逻辑(含口味查询)
SetmealService.java / SetmealServiceImpl.java 套餐业务逻辑
Mapper CategoryMapper.java / CategoryMapper.xml 分类 SQL
DishMapper.java / DishMapper.xml 菜品 SQL
DishFlavorMapper.java 口味 SQL
SetmealMapper.java / SetmealMapper.xml 套餐 SQL
VO/DTO DishVO.java 菜品+口味返回对象
DishItemVO.java 套餐内菜品返回对象
Entity Dish.java 菜品表实体
DishFlavor.java 口味表实体

六、注意点:C端 vs 管理端的区别

对比项 管理端 (/admin/dish/list) 用户端 (/user/dish/list)
返回数据 List<Dish>(只有菜品,无口味) List<DishVO>(菜品 + 口味)
状态过滤 不过滤,管理端能看到停售的 只查 status=1(起售中)
用途 管理后台的菜品列表 用户小程序浏览和点菜
相关推荐
游码峰行1 小时前
游戏脚本挂攻防-在PoW中实现动态Hash策略及应用实践
后端
用户762352425911 小时前
Kafka客户端消息流转流程
后端
橘子星1 小时前
深入理解 AJAX 中的 JSON 序列化与 JS 异步处理
前端·javascript·后端
SimonKing1 小时前
Qoder 提供免费 Qwen3.7-Max,无需订阅
java·后端·程序员
IT_陈寒2 小时前
SpringBoot自动配置这么智能,为啥我写的Bean注入不了?
前端·人工智能·后端
vx-Biye_Design2 小时前
springboot安阳地区研学旅游服务小程序-计算机毕业设计源码12785
java·vue.js·windows·spring boot·tomcat·maven·mybatis
Csvn2 小时前
日志管理与排查 — journalctl & 系统日志实战
后端
摇滚侠2 小时前
MyBatis+Spring+SpringMVC SSM 整合 179-185
java·spring·mybatis
zhenlai20123 小时前
Vue3 + SpringBoot + AI:我做了一个股票分析工具(第1周复盘)
人工智能·spring boot·后端