Day67(4)-F:\硕士阶段\Java\课程资料\1、黑马程序员Java项目《苍穹外卖》企业级开发实战\sky-take-out-菜品管理

苍穹外卖

菜品管理

公共字段自动填充

image-20251222121459437

image-20251222122019387

技术栈

  1. 基于切面编程AOP,定义注解实现为公共字段赋值(定义在sky-server)
  2. 枚举类
  3. 定义AOP切面类(定义在sky-server,用aspect包)
  4. 基于反射获取和设置数据
1.基于切面编程AOP,定义注解实现为公共字段赋值(定义在sky-server)
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();
}
2.枚举类
package com.sky.enumeration;

/**
 * 数据库操作类型
 */
public enum OperationType {

    /**
     * 更新操作
     */
    UPDATE,

    /**
     * 插入操作
     */
    INSERT

}
3.定义AOP切面类(定义在sky-server,用aspect包)
4.基于反射获取和设置数据(62-65行,68-71行)
package com.sky.aspect;

import com.sky.annotation.AutoFill;
import com.sky.constant.AutoFillConstant;
import com.sky.context.BaseContext;
import com.sky.enumeration.OperationType;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
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()")
    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 now = LocalDateTime.now();
        Long currentId = BaseContext.getCurrentId();

        //根据当前不同操作类型,为对应的属性通过反射赋值
        if (operationType == OperationType.INSERT){
            //为四个公共字段赋值
            try {
                Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
                Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
                Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, 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(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, long.class);

                //通过反射为对象赋值
                setUpdateTime.invoke(entity,now);
                setUpdateUser.invoke(entity,currentId);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

通过AOP切面赋值后需要在mapper层加上对应注解

EmployeeMapper为例,第27和第43行

package com.sky.mapper;

import com.github.pagehelper.Page;
import com.sky.annotation.AutoFill;
import com.sky.dto.EmployeePageQueryDTO;
import com.sky.entity.Employee;
import com.sky.enumeration.OperationType;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface EmployeeMapper {

    /**
     * 根据用户名查询员工
     * @param username
     * @return
     */
    @Select("select * from employee where username = #{username}")
    Employee getByUsername(String username);

    /**
     * 插入员工数据
     * @param employee
     */
    @AutoFill(value = OperationType.INSERT)//OperationType是枚举类
    @Insert("insert into employee (name, username, password, phone, sex, id_number, create_time, update_time, create_user, update_user,status) " +
            "values (#{name},#{username},#{password},#{phone},#{sex},#{idNumber},#{createTime},#{updateTime},#{createUser},#{updateUser},#{status})")
    void insert(Employee employee);

    /**
     * 分页查询
     * @param employeePageQueryDTO
     * @return
     */
    Page<Employee> pageQuery(EmployeePageQueryDTO employeePageQueryDTO);

    /**
     * 根据主键来动态修改属性
     * @param employee
     */
    @AutoFill(value = OperationType.UPDATE)//OperationType是枚举类
    void update(Employee employee);

    /**
     * 根据id查询员工信息
     * @param id
     * @return
     */
    @Select("select * from employee where id = #{id}")
    Employee getById(Long id);
}

技术问题

问题1:@Pointcut 和 @Before 的作用及设计意图
  • @Pointcut (切入点)
    • 作用:定义“哪些方法”需要被拦截。它像是一个过滤器或搜索条件。你代码中的表达式指定了:只要是 mapper 包下的方法,且贴了 @AutoFill 标签,就进入我们的视野。
    • 设计意图复用与统一管理。如果你有多个通知(比如 @Before@After)都要拦截同样的逻辑,只需要引用这个切入点方法名即可,不用到处重复写长长的表达式。
  • @Before (前置通知)
    • 作用:定义“什么时候”干活。它指定在目标方法(Mapper 里的 SQL 操作)执行之前,先执行这段自动填充逻辑。
    • 设计意图非侵入式增强。数据库操作需要完整的数据。在执行 SQL 之前把缺失的 create_time 等补齐,这样 Mapper 层的代码就不用写这些琐碎的赋值,实现了业务逻辑和公共逻辑的分离。

问题2:JoinPoint 是什么?
  • 定义JoinPoint 代表“连接点”,通俗地说,它就是被拦截到的那个“犯罪现场”。它包含了被拦截方法的所有信息。
  • 常用属性/方法
    1. getSignature():获取被拦截方法的签名(包括方法名、参数类型、返回类型等)。
    2. getArgs():获取调用该方法时传入的实际参数值(比如你传入的 Employee 对象)。
    3. getTarget():获取被拦截的目标对象(即具体的 Mapper 实例)。

问题3:MethodSignature 与 AutoFill
  • MethodSignature
    • 它是 Signature 的子接口。因为 AOP 可以拦截方法、构造函数等,Signature 比较通用。
    • 作用:专门用于获取方法层面的详细信息。
    • 常用方法getMethod()(获取 Method 对象,从而拿到注解)、getReturnType()(返回类型)、getParameterNames()(参数名)。
  • AutoFill
    • 这就是你之前定义的那个自定义注解
    • 属性/方法:在你的代码里它有一个 value() 方法,返回的是 OperationType(INSERT 或 UPDATE)。
    • 之所以在 getAnnotation() 后面加上 AutoFill.class,主要有两个原因:唯一性指向和类型安全(泛型转换)。

问题4:getDeclaredMethod 的原理
  • getDeclaredMethod 是什么: 这是 Java 反射 API。它通过方法名在类中查找对应的 Method 对象。
  • 为什么要加 LocalDateTime.class: 在 Java 中,方法重载(方法名相同,参数不同)是允许的。
    • 比如类里可能有 setCreateTime(LocalDateTime t),也可能有 setCreateTime(String t)
    • 只给方法名 SET_CREATE_TIME 是不够的,必须告诉反射机制:“我要找的是那个参数类型为 LocalDateTime 的方法”。这就是唯一确定一个方法的方式。

问题5:反射赋值的其他方法

除了 method.invoke(entity, value) 这种调用 setter 方法的方式,确实还有其他手段:

  1. 直接修改成员变量(Field): 不找 set 方法,直接找 createTime 属性:

    Java

    Field field = entity.getClass().getDeclaredField("createTime");
    field.setAccessible(true); // 暴力反射,允许访问私有变量
    field.set(entity, now);
    
    • 对比invoke 调用 setter 更安全、更符合 Java 规范;直接操作 Field 则更暴力,不需要类里定义 setter 方法也能成功。
  2. 使用 Spring 提供的工具类: Spring 封装了 BeanUtilsReflectionUtils,可以让代码更简洁:

    Java

    ReflectionUtils.invokeMethod(method, entity, now);
    
  3. 使用第三方库(如 Hutool 或 BeanCopier): 这些库底层也是反射,但处理了缓存,性能更好。

新增菜品

image-20251222135409211

image-20251222135458044

image-20251222135535042

image-20251222135755884

image-20251222135817780

image-20251222135903475

image-20251222140352471

image-20251222140451113

文件上传(阿里云)

技术栈

  1. 阿里云配置,定义在配置属性类com.sky.properties里面(来加载),需要在yml配置文件中统一定义,并定义一个对应的工具类如AliOssUtil,配置类OssConfiguration用于配置AliOssUtil对象(来创建对象)
1.阿里云配置,定义在配置属性类文件com.sky.properties里面,需要在yml配置文见中统一定义
package com.sky.properties;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "sky.alioss")
@Data
public class AliOssProperties {

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

}
1.yml配置文件一般不会暴露具体信息,会封装到生产yml也就是dev.yml
2.application-dev.yml
sky:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    host: localhost
    port: 3306
    database: sky_take_out
    username: root
    password: root
  alioss:
    endpoint: oss-cn-beijing.aliyuncs.com
    access-key-id: LTAI5t5hEbLq6RPAc5ihjWEH
    access-key-secret: vY9VD0hh9q7TcFU2H9AP9x8FPEA8hr
    bucket-name: sky-itcast-david-ai
3.application.yml(40-44行)
server:
  port: 8080

spring:
  profiles:
    active: dev
  main:
    allow-circular-references: true
  datasource:
    url: jdbc:mysql://localhost:3306/sky_take_out
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 1234

mybatis:
  #mapper配置文件
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.sky.entity
  configuration:
    #开启驼峰命名
    map-underscore-to-camel-case: true

logging:
  level:
    com:
      sky:
        mapper: debug
        service: info
        controller: info

sky:
  jwt:
    # 设置jwt签名加密时使用的秘钥
    admin-secret-key: itcast
    # 设置jwt过期时间
    admin-ttl: 7200000
    # 设置前端传递过来的令牌名称
    admin-token-name: token
    #阿里云OSS
  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}
4.配置类OssConfiguration用于配置AliOssUtil对象(来创建对象)
package com.sky.config;

import com.sky.properties.AliOssProperties;
import com.sky.utils.AliOssUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 配置类:用于配置AliOssUtil对象
 */
@Configuration
@Slf4j
public class OssConfiguration {
    @Bean
    @ConditionalOnMissingBean//当没有这个bean再创建
    public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties){
        log.info("开始创建阿里云文件上传工具类对象:{}",aliOssProperties);
        return new AliOssUtil(aliOssProperties.getEndpoint(),
                                        aliOssProperties.getAccessKeyId(),
                                        aliOssProperties.getAccessKeySecret(),
                                        aliOssProperties.getBucketName());
    }
}
5.AliOssUtil对象(工具类)用于在controller直接执行上传操作(老师提供的)
package com.sky.utils;

import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.io.ByteArrayInputStream;

@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();
    }
}
6.controller层开发:涉及注入AliOssUtil,UUID使用
package com.sky.controller;

import com.sky.constant.MessageConstant;
import com.sky.result.Result;
import com.sky.utils.AliOssUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.UUID;

/**
 * 通用接口
 */
@Slf4j
@RestController
@RequestMapping("/admin/common")
@Api(tags = "通用接口")
public class CommonController {

    @Autowired
    private AliOssUtil aliOssUtil;
    /**
     * 文件上传
     * @param file
     * @return
     */
    @ApiOperation("文件上传")
    @PostMapping("/upload")
    public Result<String> upload(MultipartFile file){//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 Result.error(MessageConstant.UPLOAD_FAILED);
    }
}

技术栈

  1. 事务管理@Transactional(新增菜品),需要在启动类通过@EnableTransactionManagement ,开启注解方式的事务管理
  2. 动态SQL,for循环插入
  3. 主键返回获得dishId,useGeneratedKeys="true" keyProperty="id"
1.事务管理@Transactional(新增菜品),需要在启动类通过@EnableTransactionManagement ,开启注解方式的事务管理
package com.sky;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@SpringBootApplication
@EnableTransactionManagement //开启注解方式的事务管理
@Slf4j
public class SkyApplication {
    public static void main(String[] args) {
        SpringApplication.run(SkyApplication.class, args);
        log.info("server started");
    }
}

serviceimpl层进行事务管理

/**
 * 新增菜品和对应的口味
 * @param dishDTO
 */
@Transactional
@Override
public void saveWithFlavor(DishDTO dishDTO) {
    Dish dish = new Dish();
    BeanUtils.copyProperties(dishDTO,dish);
    //向菜品表插入一条数据
    dishMapper.insert(dish);//主键返回获得dishId

    //获取insert语句里面生成的主键值
    Long dishId = dish.getId();

    List<DishFlavor> flavors = dishDTO.getFlavors();
    if (flavors != null && flavors.size() >0){
        flavors.forEach(dishFlavor -> {
            dishFlavor.setDishId(dishId);
        });
        //向口味表插入n条数据
        dishFlavorsMapper.insertBatch(flavors);
    }
}
2.动态SQL,for循环插入在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.DishFlavorsMapper">

    <insert id="insertBatch">
        insert into dish_flavor (dish_id, name, value)
            values
        <foreach collection="flavors" item="flavor" separator=",">
            (#{flavor.dishId},#{flavor.name},#{flavor.value})
        </foreach>
    </insert>
</mapper>
3.主键返回获得dishId,useGeneratedKeys="true" keyProperty="id"(第7行)
<?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, create_time, update_time, create_user, update_user, status)
            values
        (#{name},#{categoryId},#{price},#{image},#{description},#{createTime},#{updateTime},#{createUser},#{updateUser},#{status})
    </insert>
</mapper>

菜品分页查询

image-20251222163130807

image-20251222163200961

image-20251222163334750

image-20251222163418457

image-20251222163506399

技术栈

  1. 多表联查

菜品分页查询-controller层

/**
 * 菜品分页查询
 * @param dishPageQueryDTO
 * @return
 */
@GetMapping("/page")
@ApiOperation("菜品分页查询")
public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO){
    log.info("菜品分页查询");
    PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);
    return Result.success(pageResult);
}

菜品分页查询-serviceimpl层

/**
 * 菜品分页查询
 * @param dishPageQueryDTO
 * @return
 */
@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());
}

菜品分页查询-mapper层

/**
 * 菜品分页查询
 * @param dishPageQueryDTO
 * @return
 */
Page<DishVO> pageQuery(DishPageQueryDTO dishPageQueryDTO);
1.多表联查
<select id="pageQuery" resultType="com.sky.vo.DishVO">
    select d.*,c.name category_name 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>

删除菜品

image-20251222170247004

image-20251222170609593

image-20251222170733674

image-20251222170751617

技术栈

  1. 前端传来的String类型数字通过@RequestParam 转换为List
  2. foreach的SQL生成数组,通过open="(" close=")"
1.前端传来的String类型数字通过@RequestParam 转换为List
/**
 * 菜品的批量删除
 * @param ids
 * @return
 */
@DeleteMapping
@ApiOperation("菜品的批量删除")
public Result delete(@RequestParam List<Long> ids){
    //这里 @RequestParam 的作用就是将前端传来的字符串(如 1,2,3)或多个同名参数解析并填充到 List<Long> 集合中。
    log.info("菜品批量删除:{}",ids);
    dishService.deleteBatch(ids);
    return Result.success();
}
2.foreach的SQL生成数组,通过open="(" close=")"
<delete id="deleteByIds">
    delete from dish where id in
    <foreach collection="ids" item="id" separator="," open="(" close=")">
        #{id}
    </foreach>
</delete>

修改菜品

image-20251222190219965

image-20251222190237503

image-20251222190324439

image-20251222190407076

修改菜品-controller层

/**
 * 根据id修改菜品基本信息和对应的口味信息
 * @param dishDTO
 * @return
 */
@PutMapping
@ApiOperation("修改菜品")
public Result update(@RequestBody DishDTO  dishDTO){
    log.info("修改菜品:{}",dishDTO);
    dishService.updateWithFlavor(dishDTO);
    return Result.success();
}

修改菜品-serviceimpl层

/**
 * 根据id修改菜品基本信息和对应的口味信息
 * @param dishDTO
 */
@Transactional
@Override
public void updateWithFlavor(DishDTO dishDTO) {
    Dish dish = new Dish();
    BeanUtils.copyProperties(dishDTO,dish);

    //修改菜品表基本信息
    dishMapper.update(dish);

    //删除原有的口味数据
    dishFlavorsMapper.deleteByDishId(dishDTO.getId());

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

修改菜品-mapper层

老代码:flavor先删后添加,先delete然后insert

posted @ 2025-12-22 20:05  David大胃  阅读(7)  评论(0)    收藏  举报