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

今日学习大纲
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下的测试文件都给删除掉!
需求分析和设计

接口设计


微信登录的整个流程,就像你去一家餐厅吃饭:
-
你(小程序用户)点击"微信登录"按钮 → 微信给你一个临时暗号(code)
-
你把暗号交给服务员(后端 Controller)
-
服务员拿着暗号去问微信官方:"这个暗号是谁的?" → 微信返回一个唯一身份标识(openid)
-
服务员查花名册(数据库):这个人来过吗?
-
来过 → 直接欢迎回来
-
没来过 → 先登记(自动注册),再欢迎
- 服务员给你发一张会员卡(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;
}
}
- code 是什么? --- 微信小程序调用
wx.login()后,微信会给一个临时码(code),有效期为 5 分钟,只能用一次 - 为什么需要后端去换? --- 因为需要
appid+secret(小程序秘钥),这两个东西绝对不能放在前端(会被盗用),必须由后端持有 - 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/login 把 code 传给后端 |
| 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(起售中) |
| 用途 | 管理后台的菜品列表 | 用户小程序浏览和点菜 |