【苍穹外卖 | day31- 2】

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 后端项目中!

<think>好的,我现在需要解决用户提到的苍穹外卖系统中启用或禁用员工账号失败的问题。根据用户提供的引用内容,我需要先理解相关的代码和数据库结构,然后分析可能的问题原因。 首先,用户引用了EmployeeController中的startOrStop方法,这个方法处理启用和禁用账号的请求。代码路径是POST "/status/{status}",接收状态和员工ID。这里需要确认是否正确接收参数,是否有日志输出。根据代码中的日志记录,如果日志没有正确打印参数,可能参数绑定有问题,比如路径变量status是否正确映射,或者id是否通过请求参数传递。 接着,查看用户表设计,特别是status字段是int类型,允许1和0。如果数据库中的status字段类型不符,或者字段名有误,可能导致更新失败。例如,字段名是否确实是status,是否存在拼写错误。 然后,引用了updateEmp方法,这是一个PUT请求,用于更新员工信息。虽然startOrStop可能调用了不同的服务方法,但需要确认employeeService.startOrStop的实现是否正确,比如是否执行了正确的SQL更新语句,是否处理了异常情况,比如员工ID不存在。 可能的错误点包括:参数绑定不正确,如路径变量与方法参数名称不匹配;SQL语句错误,如字段名或表名错误;事务管理问题导致更新未提交;权限问题,如数据库用户没有更新权限;或者前端传递的参数格式不正确,比如id是否为Long类型。 需要逐步排查:首先检查日志输出,确认参数是否正确传递;然后检查SQL语句是否正确,是否有语法错误;接着验证数据库连接和权限;最后确认前端请求是否符合预期,比如是否传递了正确的id和status值。</think>### 逐步排查与解决方案 #### 1. **检查请求参数绑定** - **问题现象**:日志未输出正确参数值,或返回参数错误提示 - **解决方法**: - 确认路径变量`{status}`与`@PathVariable`参数名一致 - 检查前端是否通过`URL参数`或`请求体`传递`id`(根据代码应为URL参数) - 添加参数校验逻辑: ```java if(id == null || id <= 0){ return Result.error("非法ID"); } ``` #### 2. **验证SQL执行情况** - **问题现象**:日志显示执行SQL但数据库未更新 - **解决方法**: - 检查`employeeService.startOrStop`中SQL语句: ```sql UPDATE employee SET status = #{status} WHERE id = #{id} ``` - 确认数据库字段名是否为`status`(注意大小写敏感问题) - 添加SQL执行结果日志: ```java int affectedRows = employeeMapper.updateStatus(status, id); log.info("更新影响行数:{}", affectedRows); ``` #### 3. **事务与异常处理** - **问题现象**:控制台报错但无业务提示 - **解决方法**: - 添加事务注解保证原子性: ```java @Transactional public void startOrStop(Integer status, Long id) { //... } ``` - 捕获并处理异常: ```java try { employeeMapper.updateStatus(status, id); } catch (DataAccessException e) { log.error("数据库操作异常", e); throw new BusinessException("账号状态更新失败"); } ``` #### 4. **数据一致性验证** - **问题现象**:状态值不符合预期(如出现2、3等非法值) - **解决方法**: ```java if(status != 0 && status != 1){ throw new IllegalArgumentException("非法状态码"); } ``` #### 5. **前端联调验证** - **典型错误场景**: - 请求路径错误:未使用`/employee/status/1?id=1001`格式 - 请求方式错误:误用GET代替POST - 参数格式错误:传递字符串类型的id值 ### 补充验证步骤 1. **查看MyBatis日志**: 在`application.yml`中开启SQL日志: ```yaml logging.level.com.xxx.mapper: debug ``` 2. **数据库锁排查**: 检查是否出现行锁: ```sql SHOW OPEN TABLES WHERE In_use > 0; ``` ### 典型错误案例 **案例1**:前端传递`id=“1001”`(字符串类型) - **现象**:控制台报`NumberFormatException` - **修复**:前端改为传递数字类型 **案例2**:字段误写为`stats` - **现象**:SQL执行成功但数据未更新 - **修复**:修正字段名为`status` ### 最终代码优化建议 ```java @PostMapping("/status/{status}") @ApiOperation("启用禁用员工账号") public Result startOrStop( @PathVariable @Range(min = 0, max = 1, message = "状态值非法") Integer status, @RequestParam @Min(1) Long id) { if (employeeService.checkAdmin(id)) { return Result.error("管理员账号不可禁用"); } return Result.success(employeeService.updateStatus(status, id)); } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值