【苍穹外卖|Day3】公共字段自动填充、新增菜品功能、菜品分页查询功能、删除菜品功能、修改菜品功能、起售停售菜品

D3

本文记录「苍穹外卖」项目开发中的关键技术实践与踩坑思考,包含个人在实际开发中的具体过程遇到的问题 以及知识点总结
希望可以给一起学习的大家带来帮助

文章目录

1.公共字段自动填充

问题分析

实现思路

  • 自定义注解,用于标识需要进行公共字段自动填充的方法
  • 自定义切面类,统一拦截加入了该注解的方法,通过反射为公共字段赋值
  • 在Mapper的方法上加入该注解

技术点:枚举、注解、AOP、反射

1.自定义注解@AutoFill

java 复制代码
package com.sky.annotation;

import com.sky.enumeration.OperationType;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

//自定义注解,用于标识某个方法需要进行功能字段自动填充处理
@Target(ElementType.METHOD) //表示这个注解只能用在方法上(不能用在类、字段等地方)
@Retention(RetentionPolicy.RUNTIME) //表示这个注解在程序运行时仍然可用
public @interface AutoFill {
    //数据库操作类型:UPDATE INSERT
    OperationType value();
}

为什么用 value() 这种"方法"形式?

→ 这是 Java 为注解设计的特殊语法规则:用无参方法声明属性

2.自定义切面类

java 复制代码
package com.sky.aspect;


import com.sky.annotation.AutoFill;
import com.sky.context.BaseContext;
import com.sky.enumeration.OperationType;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.time.LocalDateTime;

//自定义切面:实现公共字段自动填充
@Aspect
@Component
@Slf4j
public class AutoFillAspect {
    //切入点
    @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
    public void autoFillPointCut(){}

    //前置通知
    @Before("autoFillPointCut()")
    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 now = LocalDateTime.now();
        Long currentId = BaseContext.getCurrentId();

        //为实体对象的公共字段赋值
        if(operationType == OperationType.INSERT){
            try {
                Method setCreateTime = entity.getClass().getDeclaredMethod("setCreateTime",LocalDateTime.class);
                Method setCreateUser = entity.getClass().getDeclaredMethod("setCreateUser", Long.class);
                Method setUpdateTime = entity.getClass().getDeclaredMethod("setUpdateTime", LocalDateTime.class);
                Method setUpdateUser = entity.getClass().getDeclaredMethod("setUpdateUser", Long.class);

                //通过反射为对象属性赋值
                setCreateTime.invoke(entity,now);
                setCreateUser.invoke(entity,currentId);
                setUpdateTime.invoke(entity,now);
                setUpdateUser.invoke(entity,currentId);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }else if(operationType == OperationType.UPDATE){
            try {
                Method setUpdateTime = entity.getClass().getDeclaredMethod("setUpdateTime", LocalDateTime.class);
                Method setUpdateUser = entity.getClass().getDeclaredMethod("setUpdateUser", Long.class);

                //通过反射为对象属性赋值
                setUpdateTime.invoke(entity,now);
                setUpdateUser.invoke(entity,currentId);

            } catch (Exception e) {
                e.printStackTrace();
            }
        }else {}
    }
}

3.剩余操作:

在Mapper层相关方法上加上注解

eg:@AutoFill(value = OperationType.UPDATE)

删除Service层多余代码

2.新增菜品

业务分析

  • 菜品名称必须唯一
  • 菜品必须属于某分类下,不能单独存在
  • 新增菜品时可以根据情况选择菜品的口味
  • 每个菜品必须对应一张图片

接口设计

  • 根据类型查询分类
  • 文件上传
  • 新增菜品
2.1文件上传

application.yml文件里添加阿里云配置

yml 复制代码
sky:
	//... ...
	alioss:
    	endpoint: ${sky.alioss.endpoint}
    	access-key-id: ${sky.alioss.access-key-id}
    	access-key-secret: ${sky.alioss.access-key-secret}
    	bucket-name: ${sky.alioss.bucket-name}

在-dev文件配置各项具体参数


AliOssProperties作用是 :自动把 application.ymlsky.alioss 开头的配置,读取并绑定到 Java 对象里

java 复制代码
@Component
@ConfigurationProperties(prefix = "sky.alioss")
@Data
public class AliOssProperties {

    private String endpoint;
    private String accessKeyId;
    private String accessKeySecret;
    private String bucketName;

}

AliOssUtil作用是:上传文件到阿里云

具体操作步骤为:

  1. 创建 OSS 客户端对象
    → 用 endpointAccessKeyIdAccessKeySecret 构建一个 OSS 实例(相当于"登录"到 OSS)
  2. 发起上传请求
    → 调用 ossClient.putObject(bucketName, objectName, inputStream)
    → 相当于告诉 OSS:"把这段数据存到这个桶里的这个路径"
  3. 拼接公开访问 URL(可选)
    → 按照格式:https://<bucketName>.<endpoint>/<objectName>
    → 这个链接就能在浏览器直接访问文件(前提是 bucket 是 public-read)
java 复制代码
@Data
@AllArgsConstructor
@Slf4j
public class AliOssUtil {

    private String endpoint;
    private String accessKeyId;
    private String accessKeySecret;
    private String bucketName;

    /**
     * 文件上传
     *
     * @param bytes
     * @param objectName
     * @return
     */
    public String upload(byte[] bytes, String objectName) {

        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

        try {
            // 创建PutObject请求。
            ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(bytes));
        } catch (OSSException oe) {
            System.out.println("Caught an OSSException, which means your request made it to OSS, "
                    + "but was rejected with an error response for some reason.");
            System.out.println("Error Message:" + oe.getErrorMessage());
            System.out.println("Error Code:" + oe.getErrorCode());
            System.out.println("Request ID:" + oe.getRequestId());
            System.out.println("Host ID:" + oe.getHostId());
        } catch (ClientException ce) {
            System.out.println("Caught an ClientException, which means the client encountered "
                    + "a serious internal problem while trying to communicate with OSS, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message:" + ce.getMessage());
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }

        //文件访问路径规则 https://BucketName.Endpoint/ObjectName
        StringBuilder stringBuilder = new StringBuilder("https://");
        stringBuilder
                .append(bucketName)
                .append(".")
                .append(endpoint)
                .append("/")
                .append(objectName);

        log.info("文件上传到:{}", stringBuilder.toString());

        return stringBuilder.toString();
    }
}

OssConfiguration配置类作用是:把AliOssUtil的对象注入IOC容器,在此过程中把id和密钥啥的传给对象

java 复制代码
//配置类用于创建AliOssUtil
@Configuration
@Slf4j
public class OssConfiguration {

    @Bean
    @ConditionalOnMissingBean //只有当Spring容器中还没有这个Bean时,才创建
    public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties){
        log.info("开始创建阿里云文件上传工具类对象");
        return new AliOssUtil(aliOssProperties.getEndpoint(),
                aliOssProperties.getAccessKeyId(),
                aliOssProperties.getAccessKeySecret(),
                aliOssProperties.getBucketName());
    }
}
注解 角色 类比
@ConfigurationProperties 配置数据搬运工 把 YML 里的值"搬进"Java 对象
@Configuration Bean 工厂 用这些值"生产"出可用的组件

Controller层

新建一个CommonController,编写操作文件上传的方法

java 复制代码
RestController
@RequestMapping("/admin/common")
@Slf4j
public class CommonController{

    @Autowired
    AliOssUtil aliOssUtil;

    //文件上传
    @PostMapping("/upload")
    public Result<String> upload(MultipartFile file) {
        log.info("文件上传:{}",file);

        try {
            //原始文件名
            String originalFilename = file.getOriginalFilename();
            //截取原始文件名的后缀
            String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
            //构建原始文件名的后缀
            String objectName = UUID.randomUUID().toString() + "." + extension;
            String filePath = aliOssUtil.upload(file.getBytes(), objectName);
            return Result.success(filePath);
        } catch (IOException e) {
            log.error("文件删除失败:{}",e);
        }
        return null;
    }
}
问题 答案
Mapper 为什么必须是接口? 因为 MyBatis 依赖 JDK 动态代理,只能代理接口
写成 class 会怎样? MyBatis 无法识别,不能注入,XML 不生效,方法调用无效
还能链接 XML 吗? ❌ 不能!XML 的 namespace 只对接口生效
2.2新增菜品
插入菜品

1.创建一个DishController

java 复制代码
@RestController
@RequestMapping("/admin/dish")
@Slf4j
public class DishController {

    @Autowired
    private DishService dishService;

    //新增菜品
    @PostMapping
    public Result save(@RequestBody DishDTO dishDTO) {
        log.info("新增菜品:{}",dishDTO);
        dishService.saveWithFlavor(dishDTO);
        return Result.success();
    }
}

2.创建DishService接口和实现类DishServiceImpl

java 复制代码
@Service
@Slf4j
public class DishServiceImpl implements DishService {

    @Autowired
    private DishMapper dishMapper;
    @Autowired
    private DishFlavorMapper dishFlavorMapper;

    //新增菜品和对应的口味
    @Transactional
    @Override
    public void saveWithFlavor(DishDTO dishDTO) {

        Dish dish = new Dish();
        BeanUtils.copyProperties(dishDTO,dish);

        //向菜品表插入1条数据
        dishMapper.insert(dish);

        //获取刚才插入的菜品的id
        Long dishId = dish.getId();

        List<DishFlavor> flavors = dishDTO.getFlavors();
        if(flavors != null && flavors.size() > 0){
            //向口味表插入n条数据
            flavors.forEach(dishFlavor -> {
               dishFlavor.setDishId(dishId);
            });
            dishFlavorMapper.insertBatch(flavors);
        }

    }
}

3.实现Mapper接口

java 复制代码
@Mapper
public interface DishMapper {

    /**
     * 根据分类id查询菜品数量
     * @param categoryId
     * @return
     */
    @Select("select count(id) from dish where category_id = #{categoryId}")
    Integer countByCategoryId(Long categoryId);

    //插入菜品数据
    @AutoFill(value = OperationType.INSERT)
    void insert(Dish dish);
}

4.编写xml文件

java 复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.DishMapper">


    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
        insert into dish (name, category_id, price, image, description, status, create_time, update_time, create_user, update_user)
            values
        (#{name},#{categoryId}, #{price}, #{image}, #{description}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})
    </insert>
</mapper>
插入口味

1.新增DishFlavorMapper接口

java 复制代码
@Mapper
public interface DishFlavorMapper {

    //批量插入口味数据
    void insertBatch(List<DishFlavor> flavors);
}

2.编写DishFlavorMapper的xml文件

批量插入口味

xml 复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.DishFlavorMapper">

    <insert id="insertBatch">
        insert into dish_flavor (dish_id, name, value) values
        <foreach collection="flavors" item="df" separator=",">
            (#{df.dishId},#{df.name},#{df.value})
        </foreach>
    </insert>
</mapper>

3.菜品分页查询

1.在DishMapper增加分页查询的接口

java 复制代码
//菜品分页查询
    @GetMapping("/page")
    public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO){
        log.info("菜品分页查询:{}",dishPageQueryDTO);
        PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);
        return Result.success(pageResult);

2.在DishServiceImpl里增加

java 复制代码
//菜品分页查询
    @Override
    public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) {
        PageHelper.startPage(dishPageQueryDTO.getPage(),dishPageQueryDTO.getPageSize());
        Page<DishVO> page = dishMapper.pageQuery(dishPageQueryDTO);
        return new PageResult(page.getTotal(),page.getResult());
    }

3.在Mapper层增加

java 复制代码
//菜品分页查询
    Page<DishVO> pageQuery(DishPageQueryDTO dishPageQueryDTO);

4.在xml文件中增加

xml 复制代码
<select id="pageQuery" resultType="com.sky.vo.DishVO">
        select d.* , c.name as categoryName from dish d left outer join category c on d.category_id = c.id
        <where>
            <if test="name != null">
                and d.name like concat('%',#{name},'%')
            </if>
            <if test="categoryId != null">
                and d.category_id = #{categoryId}
            </if>
            <if test="status != null">
                and d.status = #{status}
            </if>
        </where>
        order by d.create_time desc
    </select>

4.删除菜品

业务规则

  • 可以一次删除一个菜品,也可以批量删除多个菜品
  • 起售的菜品不能删除
  • 被套餐关联的菜品不能删除
  • 删除菜品后,关联的口味数据也需要删除

1.Controller

java 复制代码
//菜品删除
    @DeleteMapping
    public Result delete(@RequestParam List<Long> ids){
        log.info("菜品批量删除:{}",ids);
        dishService.deleteBatch(ids);
        return Result.success();
    }

@RequestParam

功能 说明
参数绑定 将请求参数(query string / form data)映射到方法参数
类型转换 自动把字符串转成 intLongDate 等(失败会报 400)

2.Service

java 复制代码
//菜品批量删除
    @Transactional
    @Override
    public void deleteBatch(List<Long> ids) {
        //判断能否删除-是否存在起售中的菜品
        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.getSetmealIdsByDishId(ids);
        if(setmealIds != null && setmealIds.size() > 0){
            throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
        }

        //删除菜品表中的数据
        for (Long id : ids) {
            dishMapper.deleteById(id);
            //删除口味表的数据
            dishFlavorMapper.deleteByDishId(id);
        }


    }

3.DishMapper

java 复制代码
//根据主键查询菜品
    @Select("select * from dish where id = #{id}")
    Dish getById(Long id);

    //根据id删除数据
    @Delete("delete from dish where id = #{id}")
    void deleteById(Long id);

4.增加SetmealDishMapper

java 复制代码
@Mapper
public interface SetmealDishMapper {

    //根据菜品id查对应套餐id
    public List<Long> getSetmealIdsByDishId(List<Long> dishIds);
}

5.DishFlavorMapper

java 复制代码
//根据菜品id删除数据
    @Delete("delete from dish_flavor where dish_id = #{dishId}")
    void deleteByDishId(Long dishId);

5.修改菜品

  • 根据id查询菜品
  • 根据类型查询分类
  • 文件上传
  • 修改菜品
    • 修改菜品基本数据
    • 修改口味------先删除再插入
对象 全称 作用 使用方向
DishDTO Data Transfer Object(数据传输对象) 接收前端传来的请求数据 前端 → 后端(输入)
DishVO View Object(视图对象) 封装返回给前端的响应数据 后端 → 前端(输出)

1.Controller层

java 复制代码
//修改菜品
    @PutMapping
    public Result update(@RequestBody DishDTO dishDTO) {
        log.info("修改菜品:{}",dishDTO);
        dishService.updateWithFlavor(dishDTO);
        return Result.success();
    }

2.ServiceImpl

java 复制代码
//修改菜品基本信息和口味信息
    @Transactional
    @Override
    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.size() > 0){
            flavors.forEach(dishFlavor -> {
                dishFlavor.setDishId(dishDTO.getId());
            });
        }
        dishFlavorMapper.insertBatch(flavors);
    }

3.DishMapper

java 复制代码
@AutoFill(value = OperationType.UPDATE)
    void update(Dish dish);

4.xml文件

xml 复制代码
<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>

5.DishFlavorMapper

java 复制代码
//根据菜品id删除数据
    @Delete("delete from dish_flavor where dish_id = #{dishId}")
    void deleteByDishId(Long dishId);

6.起售停售

  1. DishController
java 复制代码
/**
     * 菜品起售停售
     * @param status
     * @param id
     * @return
*/
@PostMapping("/status/{status}")
@ApiOperation("菜品起售停售")
public Result<String> startOrStop(@PathVariable Integer status, Long id){
    dishService.startOrStop(status,id);
    return Result.success();
}
  1. DishService
java 复制代码
/**
     * 菜品起售停售
     * @param status
     * @param id
*/
void startOrStop(Integer status, Long id);
  1. DishServiceImpl
java 复制代码
/**
     * 菜品起售停售
     *
     * @param status
     * @param id
*/
@Transactional
public void startOrStop(Integer status, Long id) {
    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);
            }
        }
    }
}
  1. SetmealMapper
java 复制代码
/**
     * 根据id修改套餐
     *
     * @param setmeal
 */
@AutoFill(OperationType.UPDATE)
void update(Setmeal setmeal);
  1. SetmealMapper.xml
xml 复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.SetmealMapper">

    <update id="update" parameterType="Setmeal">
        update setmeal
        <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="status != null">
                status = #{status},
            </if>
            <if test="description != null">
                description = #{description},
            </if>
            <if test="image != null">
                image = #{image},
            </if>
            <if test="updateTime != null">
                update_time = #{updateTime},
            </if>
            <if test="updateUser != null">
                update_user = #{updateUser}
            </if>
        </set>
        where id = #{id}
    </update>

</mapper>
相关推荐
wdfk_prog2 小时前
[Linux]学习笔记系列 -- [drivers][mmc]mmc_sd
linux·笔记·学习
整点薯条7782 小时前
2026 智能体技术解析:核心架构、能力边界与学习价值评估
学习·架构
无名-CODING2 小时前
SpringMVC处理流程完全指南:从请求到响应的完整旅程
java·后端·spring
瑶山2 小时前
Spring Cloud微服务搭建三、分布式任务调度XXL-JOB
java·spring cloud·微服务·xxljob
Re.不晚2 小时前
深入底层理解HashMap——妙哉妙哉的结构!!
java·哈希算法
Serene_Dream2 小时前
Java 内存区域
java·jvm
柒.梧.2 小时前
从零搭建SpringBoot+Vue+Netty+WebSocket+WebRTC视频聊天系统
vue.js·spring boot·websocket
BYSJMG2 小时前
计算机毕设推荐:基于大数据的共享单车数据可视化分析
大数据·后端·python·信息可视化·数据分析·课程设计
爱吃山竹的大肚肚2 小时前
文件上传大小超过服务器限制
java·数据库·spring boot·mysql·spring