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;
}

(注:苍穹外卖中实体类如DishCategory均继承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 查询DishDishFlavor,封装为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 后端项目中!

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐