Administrator
发布于 2026-02-18 / 17 阅读
0
0

苍穹外卖Day3

公共字段自动填充

前置知识

Java反射,面向切面编程,枚举,注解

反射

对Java程序执行过程中产生的字节码进行操作,称为反射。允许程序运行时动态获得对象信息,属性并执行其方法。

面向切面编程

提取程序中的共性功能并对需要这种功能的程序进行该功能的统一配置管理的方式。常见于日志记录和字段注入。

要素有五个,简记为JAPAT:

枚举

类似于c/c++,枚举可以定义一系列固定的值,帮助我们使得程序规范并提升代码可读性。

注解

一种特殊的类。可以理解为一种特殊的“元数据”,“注释”。根据不同的类型,有不同的功能。注解可以通过反射使得编译器对方法/类执行特殊操作。在spring框架有广泛的应用,比如IOC/DI,以及为类和方法赋予各种特殊功能比如@RestController,@Service等

通过AOP编程实现自动填充公共字段

需求:项目中有大量的方法需要使用共性功能:在插入和更新数据时,要向数据库中插入的数据应该带有修改时间,修改人id等信息。如果在代码中调用其方法为其赋值那么就是浪费大量的时间了,并且不好修改维护。为了解耦,我们需要将这些共性功能提取出来,进行AOP,从而方便的为那些需要这种功能的对象和方法应用到该功能。

/**
 * 自动填充
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {

    /**
     * 数据库操作类型
     * @return
     */
    OperationType value();
}

/**
 * 自定义切面类,统一为公共字段赋值
 */
@Aspect
@Component
@Slf4j
public class AutoFillAspect {
    /**
     * 切入点
     */
    @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
    public void autoFillPointCut() {}

    /**
     * 通知 自动填充公共字段
     * @param joinPoint
     */
    @Before("autoFillPointCut()")//因为insert和update都是在操作之前,执行赋值。所以用Before。
    public void autoFill(JoinPoint joinPoint) {
        log.info("公共字段自动填充...");

        //获得方法签名对象
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        //获得方法上的注解
        AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);
        //获得注解中的操作类型
        OperationType operationType = autoFill.value();

        //获取当前目标方法的参数
        Object[] args = joinPoint.getArgs();

        if (args == null || args.length == 0) {
            return;
        }

        //实体对象
        Object entity = args[0];

        //准备赋值的数据
        LocalDateTime time = LocalDateTime.now();
        Long empId = BaseContext.getCurrentId();


        if (operationType == OperationType.INSERT) {
            //当前执行的是insert操作,为4个字段赋值
            try {
                //获得set方法对象----Method
                Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
                Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
                Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
                //通过反射调用目标对象的方法
                setCreateTime.invoke(entity, time);
                setUpdateTime.invoke(entity, time);
                setCreateUser.invoke(entity, empId);
                setUpdateUser.invoke(entity, empId);
            } catch (Exception ex) {
                log.error("公共字段自动填充失败:{}", ex.getMessage());
            }
        }

        else {
            //当前执行的是update操作,为2个字段赋值
            try {
                //获得set方法对象----Method
                Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
                //通过反射调用目标对象的方法
                setUpdateTime.invoke(entity, time);
                setUpdateUser.invoke(entity, empId);
            } catch (Exception ex) {
                log.error("公共字段自动填充失败:{}", ex.getMessage());
            }
        }
    }

}

这里,我们通过反射,获取到需要维护的字段并对它们进行赋值。通过自定义注解和枚举实现切入点表达式。只要在需要的方法上加上这个注解,就可以实现对切入点的控制,实现公共字段的自动注入。

新增菜品

要点:阿里云OSS配置,事务管理,主键返回。

菜品页面的功能中需要上传图片和修改图片的,我们可以通过配置阿里云oss解决。

我们使用了阿里云OSS对象存储服务之后,我们的项目当中如果涉及到文件上传这样的业务,在前端进行文件上传并请求到服务端时,在服务器本地磁盘当中就不需要再来存储文件了。我们直接将接收到的文件上传到oss。

文件上传的主要步骤:获得文件的扩展名,然后生成一个随机id确保它不会出现重复的问题。然后调用上传接口。

/**
 * 通用Controller
 */
@RestController
@RequestMapping("/admin/common")
@Slf4j
@Api(tags = "通用接口")
public class CommonController {
    /**
     * 文件上传
     * @param file
     * @return
     */
    @Autowired
    private AliOssUtil aliOssUtil;
    @PostMapping("/upload")
    @ApiOperation("文件上传")
    public Result<String> upload(MultipartFile file){
        log.info(file.getName());

        //原始文件名
        String originalFilename = file.getOriginalFilename();
        String extension = originalFilename.substring(originalFilename.lastIndexOf("."));

        //将文件上传的阿里云
        String fileName = UUID.randomUUID().toString() + extension;
        try {
            String filePath = aliOssUtil.upload(file.getBytes(), fileName);
            return Result.success(filePath);
        } catch (IOException e) {
            log.error("文件上传失败:{}", e.getMessage());
        }

        return Result.error(MessageConstant.UPLOAD_FAILED);
    }

}

oss云服务的配置

这里使用的是:application.yml文件+application-dev.yml文件配置的方法。为何如此配置?因为如果在application.yml中写死了,后续的维护和进行新的配置(比如切换到生产环境配置部署)时就会比较麻烦,所以需要使用引用的方式,用另外一个文件传参。

菜品分页查询

要点:PageHelper的使用

PageHelper的分页功能是针对PageHelper分页功能进行之后的第一条sql语句的。在调用mapper接口执行查询语句时在这个语句加limits并使用count(0)统计分页中条目数量。从而实现分页查询。

 @Override
    public PageResult page(EmployeePageQueryDTO employeePageQueryDTO) {
        //调用PageHelper 开始分页
        PageHelper.startPage(employeePageQueryDTO.getPage(), employeePageQueryDTO.getPageSize());

        //执行查询
        List<Employee> empList = employeeMapper.pageQuery(employeePageQueryDTO);

        //解析和封装结果
        Page<Employee> page = (Page<Employee>)empList;
        return new PageResult(page.getTotal(), page.getResult());

    }

删除菜品

要点:逻辑外键和约束条件的判断,动态SQL实现批量删除。

要求:起售中的菜品不能删除。有套餐关联的菜品不能删除。

思路:我们先判断传入的所有的id中,有无 不能删除 的菜品,然后再执行删除功能。

/**
     * 批量删除
     * @param ids
     * 业务规则:
     * 可以一次删除一个菜品,也可以批量删除菜品
     * 起售中的菜品不能删除
     * 被套餐关联的菜品不能删除
     * 删除菜品后,关联的口味数据也需要删除掉
     */
    @Override
    @Transactional
    public void deleteBatch(List<Long> ids) {
        //先判断菜品能否被删除--起售中状态
        //遍历id,如存在起售中菜品则不允许删除
        for (Long id : ids) {
            Dish dish = dishMapper.getById(id);
            if(dish.getStatus()==StatusConstant.ENABLE){
                throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
            }
        }

        //判断菜品是否被套餐关联
        List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(ids);//setmeal_dish表中,包含套餐-菜品关系。
        if(setmealIds != null && setmealIds.size() > 0){
            //起售中的菜品不能删除
            throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
        }

        //根据id批量删除菜品
        dishMapper.deleteByIds(ids);
        //根据id批量删除口味
        dishFlavorMapper.deleteByDishIds(ids);


    }

动态sql实现批量删除。遍历id,然后把它们拼接起来(开始符号,结束符号以及分隔符)

    <delete id="deleteByIds">
        delete from dish where id in
        <foreach item="id" collection="ids" open="(" separator="," close=")">
            #{id}
        </foreach>
    </delete>

修改菜品

要点:动态sql实现修改部分字段并保留不需要修改的字段。提升代码复用性。删除附加数据,比如菜品有大量的附加口味,那么可以先全部删除之后再统一全部添加回新的信息。

修改逻辑

    @Override
    @Transactional
    public void updateWithFlavor(DishDTO dishDTO) {
        //修改菜品表基本信息
        Dish dish = new Dish();
        BeanUtils.copyProperties(dishDTO, dish);

        dishMapper.update(dish);
        //包含口味信息。先删除原有的口味,再添加口味信息
        dishFlavorMapper.deleteByDishId(dishDTO.getId());

        List<DishFlavor> flavors = dishDTO.getFlavors();
        if(flavors != null && !flavors.isEmpty()){
            //遍历 flavors,设置 dishId 属性
            for (DishFlavor flavor : flavors) {
                flavor.setDishId(dishDTO.getId());
            }
            //添加新的口味数据
            dishFlavorMapper.insertBatch(flavors);
        }

    }

动态sql实现修改指定字段。先作条件判断传入的该字段是否为空,再决定进行修改。

     <update id="update">
        update dish
        <set>
        <if test="name != null">
            name = #{name},
        </if>
        <if test="categoryId != null">
            category_id = #{categoryId},
        </if>
        <if test="price != null">
            price = #{price},
        </if>
        <if test="image != null">
            image = #{image},
        </if>
        <if test="description != null">
            description = #{description},
        </if>
        <if test="status != null">
            status = #{status},
        </if>
        <if test="updateTime != null">
            update_time = #{updateTime},
        </if>
        <if test="updateUser != null">
            update_user = #{updateUser},
        </if>
        </set>
        where id = #{id}
    </update>

作业:菜品的停售和起售功能处理

要点:注意起售和停售的约束条件,逻辑外键维护表和表之间关系的理解。菜品表->菜品-套餐表->套餐表实现该维护。

停售菜品时,该菜品关联的套餐也应当被停售。

思路:停售菜品的执行不止要修改菜品状态,还需要通过菜品表,找出哪些套餐关联到了该停售菜品。然后把套餐状态修改。

@Override
    @Transactional
    public void startOrStop(Integer status, Long id) {
        //不需要创建新的mapper接口方法。直接使用已有的修改菜品。
        Dish dish = Dish.builder().id(id).status(status).build();
        dishMapper.update(dish);

        //同时需要再在停售菜品的同时,把相关的套餐的状态也改变。停售菜品同时需要停售关联的套餐
        //先条件判断,如果是禁用该菜品的话要查找该菜品关联到了哪些套餐
        if (status == StatusConstant.DISABLE) {
            // 如果是停售操作,还需要将包含当前菜品的套餐也停售
            List<Long> dishIds = new ArrayList<>();
            dishIds.add(id);
            // select setmeal_id from setmeal_dish where dish_id in (?,?,?)
            List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(dishIds);//通过菜品-套餐表的关联关系实现查询哪些套餐关联菜品。
            if (setmealIds != null && setmealIds.size() > 0) {
                for (Long setmealId : setmealIds) {
                    Setmeal setmeal = Setmeal.builder()
                            .id(setmealId)
                            .status(StatusConstant.DISABLE)
                            .build();
                    setmealMapper.update(setmeal);//通过类似的动态sql实现修改套餐的状态,提升代码复用性。
                }
            }
        }

    }


评论