day3-day5
在苍穹外卖项目中,公共字段自动填充、菜品全流程管理、Redis 缓存应用与店铺营业状态控制是提升开发效率、优化系统性能的关键模块。你是否好奇:如何避免重复编写创建时间、更新人等字段?菜品增删改查如何兼顾业务逻辑与数据一致性?Redis 如何优化店铺状态查询?这篇解析将逐一拆解这些核心知识点。
一、公共字段自动填充:如何告别重复赋值?
问题 1:苍穹外卖中哪些字段需要自动填充?为什么要做自动填充?
在实体类中,create_time(创建时间)、update_time(更新时间)、create_user(创建人 ID)、update_user(更新人 ID)是几乎所有表的公共字段。若每个接口都手动赋值,会存在代码冗余(重复写setCreateTime(LocalDateTime.now()))、赋值不一致(有的用new Date(),有的用LocalDateTime)问题,因此需要通过 MyBatis-Plus 的元对象处理器实现自动填充。
问题 2:苍穹外卖中如何实现公共字段自动填充?
1. 步骤 1:实体类字段添加注解
通过@TableField(fill = FieldFill.INSERT)指定填充时机(插入时填充)、FieldFill.INSERT_UPDATE(插入 / 更新时填充):
java
运行
public class BaseEntity {
@TableField(fill = FieldFill.INSERT) // 仅插入时填充
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE) // 插入/更新时填充
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT)
private Long createUser;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateUser;
}
(注:苍穹外卖中实体类如Dish、Category均继承BaseEntity)
2. 步骤 2:实现元对象处理器
自定义MyMetaObjectHandler,重写填充方法,通过SecurityUtils获取当前登录用户 ID:
java
运行
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
// 填充创建时间、更新时间
strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
// 填充创建人、更新人(从ThreadLocal获取当前登录用户ID)
strictInsertFill(metaObject, "createUser", Long.class, SecurityUtils.getCurrentId());
strictInsertFill(metaObject, "updateUser", Long.class, SecurityUtils.getCurrentId());
}
@Override
public void updateFill(MetaObject metaObject) {
// 更新时填充更新时间和更新人
strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
strictUpdateFill(metaObject, "updateUser", Long.class, SecurityUtils.getCurrentId());
}
}
(注:SecurityUtils.getCurrentId()通过 ThreadLocal 存储登录用户信息,避免每次从 Session 获取)
二、菜品管理全流程:新增、分页查询、删除、修改
1. 新增菜品:如何处理菜品与口味的关联?
核心痛点:
新增菜品需同时保存菜品基本信息(名称、分类、价格)和菜品口味(如辣度、甜度),二者是 “一对多” 关系,需保证事务一致性。
实现步骤:
-
步骤 1:设计 DTO 接收前端数据
DishDTO包含菜品基本属性 + 口味列表(List<DishFlavorDTO>):java
运行
public class DishDTO { private Long id; private String name; private Long categoryId; private BigDecimal price; private String image; private List<DishFlavorDTO> flavors; // 口味列表 // 其他字段... } -
步骤 2:Service 层事务处理用
@Transactional保证菜品和口味同时插入,失败则回滚:java
运行
@Override @Transactional public void saveWithFlavor(DishDTO dishDTO) { // 1. 保存菜品基本信息(自动填充公共字段) Dish dish = new Dish(); BeanUtils.copyProperties(dishDTO, dish); this.save(dish); // 调用MP的save方法,ID会自动回显 // 2. 保存菜品口味(关联菜品ID) List<DishFlavor> flavors = dishDTO.getFlavors().stream() .map(flavorDTO -> { DishFlavor flavor = new DishFlavor(); BeanUtils.copyProperties(flavorDTO, flavor); flavor.setDishId(dish.getId()); // 关联菜品ID return flavor; }).collect(Collectors.toList()); dishFlavorService.saveBatch(flavors); // 批量插入口味 }
2. 菜品分页查询:如何实现多条件筛选 + 分类名称回显?
核心需求:
支持按菜品名称模糊查询、按分类 ID 筛选、分页展示,且列表需显示分类名称(而非分类 ID)。
实现步骤:
-
步骤 1:分页插件配置启动类添加 MyBatis-Plus 分页插件,自动拦截分页查询:
java
运行
@Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } -
步骤 2:多条件分页查询用
Page对象封装分页参数,LambdaQueryWrapper构建条件,最后转换为DishVO回显分类名称:java
运行
@Override public PageResult<DishVO> pageQuery(DishPageQueryDTO dishPageQueryDTO) { // 1. 构建分页对象 Page<Dish> page = new Page<>(dishPageQueryDTO.getPage(), dishPageQueryDTO.getPageSize()); // 2. 多条件查询 Page<Dish> dishPage = lambdaQuery() .like(StringUtils.isNotBlank(dishPageQueryDTO.getName()), Dish::getName, dishPageQueryDTO.getName()) .eq(dishPageQueryDTO.getCategoryId() != null, Dish::getCategoryId, dishPageQueryDTO.getCategoryId()) .eq(Dish::getStatus, StatusConstant.ENABLE) // 只查启用的菜品 .orderByDesc(Dish::getUpdateTime) .page(page); // 3. 转换为VO,关联分类名称 List<DishVO> dishVOList = dishPage.getRecords().stream() .map(dish -> { DishVO dishVO = new DishVO(); BeanUtils.copyProperties(dish, dishVO); // 根据分类ID查询分类名称 Category category = categoryService.getById(dish.getCategoryId()); if (category != null) { dishVO.setCategoryName(category.getName()); } return dishVO; }).collect(Collectors.toList()); return new PageResult<>(dishPage.getTotal(), dishVOList); }
3. 删除菜品:如何处理关联套餐的情况?
核心规则:
若菜品被套餐关联,则不允许删除;否则可删除菜品及关联口味。
实现步骤:
java
运行
@Override
@Transactional
public void deleteBatch(List<Long> ids) {
// 1. 检查菜品是否被套餐关联
LambdaQueryWrapper<SetmealDish> wrapper = new LambdaQueryWrapper<>();
wrapper.in(SetmealDish::getDishId, ids);
int count = setmealDishService.count(wrapper);
if (count > 0) {
throw new BusinessException("部分菜品已被套餐关联,无法删除");
}
// 2. 删除菜品(物理删除)
this.removeByIds(ids);
// 3. 删除关联口味
LambdaQueryWrapper<DishFlavor> flavorWrapper = new LambdaQueryWrapper<>();
flavorWrapper.in(DishFlavor::getDishId, ids);
dishFlavorService.remove(flavorWrapper);
}
4. 修改菜品:如何回显口味并更新关联数据?
核心步骤:
-
步骤 1:查询菜品及关联口味根据菜品 ID 查询
Dish和DishFlavor,封装为DishVO返回前端:java
运行
@Override public DishVO getByIdWithFlavor(Long id) { // 1. 查询菜品基本信息 Dish dish = this.getById(id); DishVO dishVO = new DishVO(); BeanUtils.copyProperties(dish, dishVO); // 2. 查询关联口味 LambdaQueryWrapper<DishFlavor> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(DishFlavor::getDishId, id); List<DishFlavor> flavors = dishFlavorService.list(wrapper); dishVO.setFlavors(flavors); return dishVO; } -
步骤 2:更新菜品及口味先删除原有口味,再插入新口味,保证数据一致性:
java
运行
@Override @Transactional public void updateWithFlavor(DishDTO dishDTO) { // 1. 更新菜品基本信息 Dish dish = new Dish(); BeanUtils.copyProperties(dishDTO, dish); this.updateById(dish); // 2. 删除原有口味 LambdaQueryWrapper<DishFlavor> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(DishFlavor::getDishId, dishDTO.getId()); dishFlavorService.remove(wrapper); // 3. 插入新口味 List<DishFlavor> flavors = dishDTO.getFlavors().stream() .map(flavorDTO -> { DishFlavor flavor = new DishFlavor(); BeanUtils.copyProperties(flavorDTO, flavor); flavor.setDishId(dishDTO.getId()); return flavor; }).collect(Collectors.toList()); dishFlavorService.saveBatch(flavors); }
三、Redis 入门与苍穹外卖中的应用
1. Redis 核心概念:为什么用 Redis?
Redis 是内存数据库,读写速度极快(单机 QPS 可达 10 万 +),支持多种数据类型,苍穹外卖中主要用于缓存高频访问数据(如店铺营业状态)、减轻数据库压力。
2. Redis 常见数据类型:苍穹外卖用了哪些?
| 数据类型 | 特点 | 苍穹外卖应用场景 |
|---|---|---|
| String | 键值对(字符串 / 数字) | 存储店铺营业状态(shop_status:1 → 1表示营业,0休息) |
| Hash | 键值对集合(类似 Java Map) | 存储菜品信息(dish:1001 → {name: "麻辣小龙虾", price: 99}) |
| List | 有序列表(可重复) | 存储订单队列(如待处理订单 ID 列表) |
3. Redis 常用命令:基础操作
- String 类型:
SET shop_status 1(设置店铺状态为营业)、GET shop_status(获取状态)、EXPIRE shop_status 3600(设置过期时间 1 小时); - Hash 类型:
HSET dish:1001 name "麻辣小龙虾" price 99(设置 Hash 字段)、HGETALL dish:1001(获取所有字段)。
4. 苍穹外卖中 Java 操作 Redis:Spring Data Redis
步骤 1:引入依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
步骤 2:配置 Redis 连接
yaml
spring:
redis:
host: localhost
port: 6379
password: 123456
database: 0 # 选择0号数据库
步骤 3:Template 操作 Redis
用StringRedisTemplate操作 String 类型(店铺状态):
java
运行
@Autowired
private StringRedisTemplate stringRedisTemplate;
// 设置店铺状态
public void setShopStatus(Integer status) {
stringRedisTemplate.opsForValue().set("shop_status", status.toString());
}
// 获取店铺状态
public Integer getShopStatus() {
String status = stringRedisTemplate.opsForValue().get("shop_status");
return status == null ? null : Integer.parseInt(status);
}
四、店铺营业状态设置:Redis 缓存 + 全局拦截
问题 1:为什么用 Redis 存储店铺状态?
店铺状态是高频访问数据(用户下单前需判断店铺是否营业),若每次查询数据库会增加 IO 压力,用 Redis 缓存可将查询耗时从毫秒级降至微秒级。
问题 2:如何实现店铺状态的全局校验?
步骤 1:设置店铺状态接口
管理员通过接口修改 Redis 中的状态:
java
运行
@PostMapping("/status/{status}")
public Result<?> setShopStatus(@PathVariable Integer status) {
shopService.setStatus(status);
return Result.success();
}
步骤 2:全局拦截器校验状态
用户下单、浏览菜品前,拦截请求并校验 Redis 中的店铺状态:
java
运行
@Component
public class ShopStatusInterceptor implements HandlerInterceptor {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 获取Redis中的店铺状态
String status = stringRedisTemplate.opsForValue().get("shop_status");
// 2. 若店铺休息,返回错误
if ("0".equals(status)) {
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(JSON.toJSONString(Result.error("店铺休息中,暂不接单")));
return false;
}
// 3. 放行请求
return true;
}
}
步骤 3:注册拦截器
指定拦截路径(用户端接口):
java
运行
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private ShopStatusInterceptor shopStatusInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(shopStatusInterceptor)
.addPathPatterns("/user/**") // 拦截用户端所有接口
.excludePathPatterns("/user/shop/status"); // 放行店铺状态查询接口
}
}
总结:核心知识点关联逻辑
公共字段自动填充通过 MyBatis-Plus 简化重复编码,菜品管理通过事务保证数据一致性,Redis 优化高频数据访问,店铺状态控制通过拦截器实现全局校验 —— 这些知识点围绕 “高效开发”“性能优化”“业务安全” 展开,是苍穹外卖项目的核心技术亮点。掌握这些内容,不仅能理解项目架构,更能将其复用在其他 Java 后端项目中!
1940

被折叠的 条评论
为什么被折叠?



