公共字段填充

  • 典型字段:创建时间(create_time)、创建人ID(create_user)、修改时间(update_time)、修改人ID(update_user)
  • 问题本质:多个业务表(员工表、分类表、菜品表等)存在相同字段导致代码冗余
  • 维护痛点:字段变更时需要修改多处代码,维护成本高且容易遗漏

参考利用注解反射实现公共字段自动填充功能

新增菜品

业务规则:

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

根据类型查询分类

参考导入分类模块功能代码

文件上传

  • 请求方式:POST请求,路径为/admin/common/upload
  • 请求头:Content-Type设置为multipart/form-data
  • 返回数据:包含图片绝对路径的data字段(阿里云地址)

Info

这个文件是Spring Boot项目中的一个配置属性类,主要用于读取和存储阿里云OSS(对象存储服务)的相关配置信息。

具体来说:

  1. @Component:将该类注册为Spring容器中的一个组件,使其可以被自动扫描和注入。
  2. @ConfigurationProperties(prefix = “sky.alioss”):这是关键注解,用于从application.yml或application.properties配置文件中读取以”sky.alioss”为前缀的配置属性。例如:
  3. @Data:Lombok注解,自动生成getter、setter、toString等方法,简化代码。
  4. 四个属性字段
    • endpoint:阿里云OSS的访问端点
    • accessKeyId:访问OSS的Access Key ID
    • accessKeySecret:访问OSS的Access Key Secret
    • bucketName:OSS存储空间名称

当Spring Boot启动时,它会自动读取配置文件中以sky.alioss开头的属性,并将它们的值注入到 AliOssProperties对象的相应字段中。这个类的作用是将配置文件中的阿里云OSS配置信息自动绑定到Java对象中,方便在项目中使用这些配置来连接和操作阿里云OSS服务。这样做的好处是将配置与代码分离,便于在不同环境(开发、测试、生产)中灵活切换配置。

@Component
@ConfigurationProperties(prefix = "sky.alioss")
@Data
public class AliOssProperties {
 
    private String endpoint;
    private String accessKeyId;
    private String accessKeySecret;
    private String bucketName;
}
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}

application-dev.yml中填入具体配置信息

引入依赖

<dependency>
	<groupId>com.aliyun.oss</groupId>
	<artifactId>aliyun-sdk-oss</artifactId>
</dependency>
 
@Data
@AllArgsConstructor
@Slf4j
public class AliOssUtil {
 
    private String endpoint;
    private String accessKeyId;
    private String accessKeySecret;
    private String bucketName;
 
    public String upload(byte[] bytes, String objectName) {
 
        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
	    // 创建PutObject请求。
		ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(bytes));
		 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();
    }
}

Tip

上述是精简后的代码,删除了异常控制,完整代码参考项目实际代码

@Configuration
@Slf4j
public class OssConfiguration {
 
    @Bean
    @ConditionalOnMissingBean
    public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties) {
        log.info("开始创建阿里云文件上传工具类:{}", aliOssProperties);
        return new AliOssUtil(aliOssProperties.getEndpoint(),
                aliOssProperties.getAccessKeyId(),
                aliOssProperties.getAccessKeySecret(),
                aliOssProperties.getBucketName());
    }
}

这个配置类的关键点包括:

  • @Configuration:标识这是一个配置类
  • @Bean:声明一个Bean,由Spring容器管理
  • @ConditionalOnMissingBean:条件注解,只有当容器中不存在 AliOssUtil 类型的Bean时,才会创建这个Bean

方法参数 AliOssProperties aliOssProperties 会由Spring自动注入,其中包含了从配置文件中读取的所有OSS相关配置。

工作流程总结

  1. Spring Boot启动时读取配置文件(application.ymlapplication-dev.yml
  2. 通过 @ConfigurationProperties(prefix = "sky.alioss") 注解,将配置文件中以sky.alioss为前缀的属性值绑定到 AliOssProperties 对象
  3. OssConfiguration 配置类检测到容器中存在 AliOssProperties Bean,将其注入到 aliOssUtil 方法中
  4. 使用 AliOssProperties 中的配置值创建 AliOssUtil Bean,并注册到Spring容器中

这样就完成了基于配置文件的自动配置过程,实现了配置与代码的分离,便于在不同环境(开发、测试、生产)中灵活切换配置。

新增菜品

@Data
public class DishDTO implements Serializable {
 
    private Long id;
    //菜品名称
    private String name;
    //菜品分类id
    private Long categoryId;
    //菜品价格
    private BigDecimal price;
    //图片
    private String image;
    //描述信息
    private String description;
    //0 停售 1 起售
    private Integer status;
    //口味
    private List<DishFlavor> flavors = new ArrayList<>();
 
}
@Transactional
public void savewithFlavor(DishDTO dishDTO) {
	Dish dish = new Dish();
	BeanUtils.copyProperties(dishDTO, dish);
	//向菜品表插入一条数据
	dishMapper.insert(dish);
	// 获取插入后的id
	Long dishId = dish.getId();
	// 向口味表插入n条数据
	List<DishFlavor> flavors = dishDTO.getFlavors();
 
	if(flavors != null && flavors.size() > 0){
		flavors.forEach(dishFlavor -> {
			dishFlavor.setDishId(dishId);
		});
		dishFlavorMapper.insertBatch(flavors);
	}
}
<insert id="insertBatch">
	insert into dish_flavor (dish_id, name, value) values
	<foreach collection="flavors" item="dishFlavor" separator=",">
		(#{dishFlavor.dishId}, #{dishFlavor.name}, #{dishFlavor.value})
	</foreach>
</insert>

获取插入后的ID

在MyBatis中,可以通过配置让数据库生成的主键值自动回写到实体对象中。在你的代码中,这个过程是这样实现的:

  1. Mapper XML配置: 在DishMapper.xml中,insert语句配置了两个关键属性:

    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
    • useGeneratedKeys="true":告诉MyBatis使用数据库生成的主键
    • keyProperty="id":指定将生成的主键值设置到实体对象的id属性上
  2. 数据库表设计: 数据库中的dish表必须将主键字段(通常是id)设置为自增(AUTO_INCREMENT)。

  3. 执行流程

    • 当调用dishMapper.insert(dish)时,MyBatis会执行插入语句
    • 数据库生成新的主键值
    • MyBatis自动将生成的主键值回写到传入的dish对象的id字段中
    • 因为Java对象是引用传递,所以dish对象的id字段在插入后会被自动填充为新生成的ID值
  4. 获取ID

    //向菜品表插入一条数据
    dishMapper.insert(dish);
     
    // 获取插入后的id
    Long dishId = dish.getId();

这种机制依赖于MyBatis的useGeneratedKeys功能,它会在执行INSERT语句后,自动执行类似SELECT LAST_INSERT_ID()的语句(具体取决于数据库类型),然后将获取到的ID值设置到指定的属性中。

这是MyBatis的标准功能,适用于所有支持自增主键的数据库,如MySQL、PostgreSQL等。在你的项目中,这是通过在Mapper XML文件中配置useGeneratedKeys="true"keyProperty="id"来实现的。

菜品分页查询

业务规则:

  • 分页机制:按页码展示数据,每页固定显示10条记录
  • 条件查询:支持组合查询条件,包括菜品名称模糊匹配、分类精确筛选和状态过滤
  • 数据关联:菜品表中只存储分类ID,分类名称需关联分类表获取
@Data
public class DishPageQueryDTO implements Serializable {
 
    private int page;
 
    private int pageSize;
 
    private String name;
 
    //分类id
    private Integer categoryId;
 
    //状态 0表示禁用 1表示启用
    private Integer status;
 
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DishVO implements Serializable {
 
    private Long id;
    //菜品名称
    private String name;
    //菜品分类id
    private Long categoryId;
    //菜品价格
    private BigDecimal price;
    //图片
    private String image;
    //描述信息
    private String description;
    //0 停售 1 起售
    private Integer status;
    //更新时间
    private LocalDateTime updateTime;
    //分类名称
    private String categoryName;
    //菜品关联的口味
    private List<DishFlavor> flavors = new ArrayList<>();
 
    //private Integer copies;
}

public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) {
	PageHelper.startPage(dishPageQueryDTO.getPage(), dishPageQueryDTO.getPageSize());
	Page<DishVO> page = dishMapper.pageQuery(dishPageQueryDTO);
	return new PageResult(page.getTotal(),page.getResult());
}

这里分类名称需关联分类表获取,取别名categoryNameDishVO中名字对应

<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 name != ''">
			and d.name like '%${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>

删除菜品

业务规则:

  • 删除方式:支持单个删除和批量删除两种操作模式
  • 限制条件:
    • 起售中的菜品禁止删除(需先停售)
    • 被套餐关联的菜品禁止删除(避免影响套餐完整性)
  • 数据关联:删除菜品时需同步删除其关联的口味数据(dish_flavor表)

接口设计:

  • 请求方式:DELETE方法,路径为/admin/dish
  • 参数设计:
    • 通过Query参数ids传递菜品ID
    • 多个ID用逗号分隔(如”1,2,3”)
@Transactional
public void deleteBatch(List<Long> ids) {
	// 是否存在起售中的
	for (Long id : ids) {
		Dish dish = dishMapper.getById(id);
		if (dish.getStatus() == 1) {
			throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
		}
	}
	// 是否被套餐关联
	List<Long> setmealIdsByDishIds = setmealDishMapper.getSetmealIdsByDishIds(ids);
	if (setmealIdsByDishIds != null && setmealIdsByDishIds.size() > 0) {
		// 存在关联的套餐
		throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
	}
	// 删除菜品表数据
	for (Long id : ids) {
		dishMapper.deleteById(id);
		// 删除关联的口味数据
		dishFlavorMapper.deleteByDishId(id);
	}
}

查询setmeal_dish表判断当前菜品是否被套餐关联

<select id="getSetmealIdsByDishIds" resultType="java.lang.Long">
	SELECT setmeal_id
	FROM setmeal_dish
	WHERE dish_id IN
	<foreach collection="dishIds" item="dishId" separator="," open="(" close=")">
		#{dishId}
	</foreach>
</select>

修改菜品

  • 功能需求:通过输入框回显原有菜品信息,支持修改菜品名称、分类、价格、口味等字段
  • 技术难点:需要回显的数据较多,包括菜品基础信息和关联的口味数据
  • 接口分析:共需4个接口,其中2个已实现(查询分类接口和文件上传接口),需新增2个接口(根据id查询菜品、修改菜品

根据id查询菜品

还需要查询菜品关联的口味(两次查询)

public DishVO getByIdWithFlavor(Long id) {
	// 1. 查询菜品数据
	Dish dish = dishMapper.getById(id);
	// 2. 查询口味数据
	List<DishFlavor> flavors = dishFlavorMapper.getByDishId(id);
	// 3. 组装数据并返回
	DishVO dishVO = new DishVO();
	BeanUtils.copyProperties(dish, dishVO);
	dishVO.setFlavors(flavors);
	return dishVO;
}

参考DishVO

修改菜品

菜品关联口味修改思路

  • 先删除再插入
    • 处理逻辑:
      • 先删除原有口味数据
      • 再插入新传过来的口味数据
    • 技术实现:
      • 业务层面是修改操作
      • 技术层面是先删除再插入
    • 优势: 简化复杂情况的处理(口味可能被删除、追加或保持不变)
public void updateWithFlavor(DishDTO dishDTO) {
	// 1. 修改菜品表
	Dish dish = new Dish();
	BeanUtils.copyProperties(dishDTO, dish);
	dishMapper.update(dish);
	// 2. 删除旧口味数据
	dishFlavorMapper.deleteByDishId(dishDTO.getId());
	// 3. 插入新口味数据
	List<DishFlavor> flavors = dishDTO.getFlavors();
	if (flavors != null && flavors.size() > 0) {
		flavors.forEach(dishFlavor -> {
			dishFlavor.setDishId(dishDTO.getId());
		});
	}
	// 4. 口味表批量插入
	dishFlavorMapper.insertBatch(flavors);
}

这里插入新口味数据需要遍历flavors,设置对应的dishId,这是因为在前端新增口味数据,前端请求接口时不会设置该口味对应的dishId(尽管前端可以做到,但这里放到了后端处理)

菜品起售停售功能

业务规则

菜品起售表示该菜品可以对外售卖,在用户端可以点餐,菜品停售表示此菜品下架,用户端无法点餐。

业务规则为:如果执行停售操作,则包含此菜品的套餐也需要停售。

代码实现

DishController

/**
     * 菜品起售停售
     * @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();
}

DishService

/**
     * 菜品起售停售
     * @param status
     * @param id
*/
void startOrStop(Integer status, Long id);

DishServiceImpl

/**
     * 菜品起售停售
     *
     * @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);
            }
        }
    }
}

SetmealMapper

/**
     * 根据id修改套餐
     *
     * @param setmeal
 */
@AutoFill(OperationType.UPDATE)
void update(Setmeal setmeal);

SetmealMapper.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>