公共字段自动填充
在我们的业务表中,有很多相同的字段,如创建人、创建时间、修改人、修改时间等等字段,大多数业务都需对这些字段进行赋值,这就会造成大量重复的代码。
create_time、create_user两字段需要在insert方法中使用,update_user、update_time两字段需要在insert、update方法中使用。
做好分类后我们就可以借助切面来统一进行处理。
一、自定义注解@AutoFill,用于标识需要进行公共字段填充的方法。
我们选择在server模块新建一个包annotation用于存放注解AutoFill,然后添加对应的注解@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME)。
再在该自定义注解中定义枚举类属性来判断当前操作的类型,可以定义方法value(),返回类型为事先存在的枚举类OperationType,意为使用@AutoFill注解时都需传入一个OperationType枚举类型的值(注解中的方法不需要写方法体)。
@Target(ElementType.METHOD)//指定该注解只能用于方法上
@Retention(RetentionPolicy.RUNTIME)//指定注解的生命周期
public @interface AutoFill {
// 数据库操作类型:UPDATE INSERT
// 定义方法,要求使用@AutoFill注解时必须提供一个OperationType枚举类型的值
// 这个值用于指示方法执行时的操作类型
OperationType value();
}
二、自定义切面类AutoFillAspect,统一拦截加入了@AutoFill注解的方法
在server模块新建一个包aspect用于存放切面类.AutoFillAspect,然后添加注解@Aspect @Component @Slf4j。
在该切面类中定义需要拦截的方法为mapper包下所有被@AutoFill注解标记的方法。并执行前置通知来填充字段。
来看前置通知,首先需获取拦截方法所需的操作类型并判断是update还是insert,可以通过调用连接点对象的getSignature()方法来获得签名,签名为接口,但拦截到的为方法,我们可向下转型为其子接口MethodSignature,然后利用反射,调用getMethod().getAnnotation(AutoFill.class)来获得方法上的注解对象,再通过获取注解的值来获取操作类型。
然后需要获取被拦截的方法的参数,即实体对象。我们仍通过调用连接点对象的getArgs()来获取参数,其会返回包含所有的参数的数组,我们通过Object[] args来接收,而为方便代码编写,我们约定要将实体类放在第一个位置,因此args[0]就是实体类,我们通过Object实例来接收(注意不要使用Employee实例接收,因此后期可能其他模块也使用自动填充,而Employee接收会报错)。同时为了防止空指针,我们对args数组添加非空判断。
再准备赋值的数据,修改时间直接获取当前时间即可,当前用户ID则调用BaseContext类中的方法getCurrentId()来获取。
最后根据当前不同的操作类型,为对应的属性通过反射来赋值。这里调用getClass().getMethod()方法时传入的第一个参数应该为方法名,但可能会出现打错字母导致找不到对应方法的情况,因此我们选择调用common模块constant包AutoFillConstant类中的常量字符串使代码更规范
@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) throws Throwable {
// 获取当前被拦截方法的信息,包括方法名、参数类型等
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 从方法上获取@AutoFill注解,这个注解定义了操作类型(如插入或更新)
AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);
// 获取注解中定义的操作类型
OperationType operationType = autoFill.value();
// 获取被拦截方法的参数列表,约定第一个参数是实体对象
Object[] args = joinPoint.getArgs();
// 如果没有参数或参数列表为空,则直接返回
if (args == null || args.length == 0) {
return;
}
// 约定第一个参数是实体对象
Object object = args[0];
// 准备自动填充的数据
LocalDateTime now = LocalDateTime.now();
Long currentId = BaseContext.getCurrentId(); // 获取当前用户ID,通常是从上下文中获取
// 根据不同的操作类型(插入或更新),使用反射为实体对象的属性赋值
if (operationType == OperationType.INSERT) {
// 如果是插入操作,需要为四个字段赋值:创建时间、创建人、更新时间、更新人
Method setCreateTime = object.getClass().getMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
Method setCreateUser = object.getClass().getMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
Method setUpdateTime = object.getClass().getMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setUpdateUser = object.getClass().getMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
//通过反射为对象属性赋值
setCreateTime.invoke(object, now);
setCreateUser.invoke(object, currentId);
setUpdateTime.invoke(object, now); // 插入时,更新时间和创建时间通常相同
setUpdateUser.invoke(object, currentId); // 插入时,更新人和创建人通常相同
} else if (operationType == OperationType.UPDATE) {
// 如果是更新操作,只需要为两个字段赋值:更新时间和更新人
Method setUpdateTime = object.getClass().getMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setUpdateUser = object.getClass().getMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
//通过反射为对象属性赋值
setUpdateTime.invoke(object, now);
setUpdateUser.invoke(object, currentId);
}
}
}
三、为Mapper层中所有需要拦截的方法加上自定义注解@AutoFill并指定操作类型(包括刚刚复制的文件中的包含更新和插入的方法)
@Mapper
public interface EmployeeMapper {
@Insert("insert into employee (name, username, password, phone, sex, id_number, create_time, update_time, create_user, update_user) " +
"values " +
"(#{name},#{username}, #{password},#{phone},#{sex}, #{idNumber}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})")
@AutoFill(value = OperationType.INSERT)
void insert(Employee employee);
@AutoFill(value = OperationType.UPDATE)
void update(Employee employee);
}
此时就可以删除原本接口实现类中用于赋值的代码,前置通知会完成这一部分。
——菜品模块————————
新增菜品
该业务需遵守的规则:
接口应分为三个:分类查询、文件上传、新增菜品。其中分类查询请求路径为/admin/category/list,请求方法为get,上文已实现在此不再赘述。
文件上传
文件上传功能需要先配置alioss,同样的参数名不再使用硬编码的方式编写,而是引用common模块properties包AliOssProperties类中定义的参数(该类上的注解@ConfigurationProperties(prefix = "sky.alioss")使该类成为配置属性类)。
注意在类中我们采用驼峰命名法:accessKeyId,但在配置文件中以"-"连接:access-key-id,springboot框架会自动对其进行转换,当然在配置文件中仍使用驼峰命名法也可行。
之前我们在application.yml中直接配置具体的值,但开发到上线使用需要修改相关信息,比较麻烦,我们采用引用的方式:在application-dev.yml文件中配置具体值(该文件适合在开发环境下使用),投入使用时prod文件会取代他,届时我们只需将主配置文件中spring.profiles.active的属性修改为为prod即可。
- application-dev.yml(开发环境配置)
- application-prod.yml(生产环境配置)
- application.yml(主配置文件)
引用方式为:空格${其他配置文件中属性名},原代码中数据库的相关配置信息也是这样实现的(之前自行编写的AliOssUtil和这次给出的AliOssUtil内容不一样,对返回的图片url的处理方式也不一致,因此endpoint: https://oss-cn-beijing.aliyuncs.com需要去掉https://):
//application.yml文件————————————————————————————
spring:
profiles:
active: dev #当前激活的配置文件是开发环境(dev)的配置文件
sky:
jwt:
admin-secret-key: itcast
admin-ttl: 604800000
admin-token-name: token
#阿里云配置
alioss:
access-key-id: ${sky.alioss.access-key-id} #注意":"后面需有一个空格
access-key-secret: ${sky.alioss.access-key-secret}
endpoint: ${sky.alioss.endpoint}
bucket-name: ${sky.alioss.bucket-name}
//application-dev.yml文件————————————————————————
sky:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
host: localhost
port: 3306
database: sky_take_out
username: root
password: 1234
alioss:
access-key-id: LTAI5t6nivgHXQ1rnBt3dudV # 阿里云OSS的Access Key ID
access-key-secret: EhrGz86soycvHNnK0V4PuZoDYgu4tm # 阿里云OSS的Access Key Secret
endpoint: oss-cn-beijing.aliyuncs.com # 阿里云OSS的端点
bucket-name: chn-webapp # 阿里云OSS的Bucket名称
在controller层创建CommonController,在该类中创建方法upload()用于响应路径为/admin/common/upload的post方法,同时注入AliOssUtil类的bean,在该方法中引用common模块utils包AliOssUtil类upload()方法,其会返回该图片的访问网址。
但AliOssUtil类中定义的四个属性endpoint、accessKeyId、accessKeySecret、bucketName尚未初始化,可以在server模块config包中新建OSSConfiguration类,同时添加@Configuration注解将其作为配置类,此时@Configuration修饰的自动配置类通常会在项目启动时就被创建。
//AliOssProperties——————————————————————————————————
@Component
@ConfigurationProperties(prefix = "sky.alioss")
@Data
public class AliOssProperties {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
}
//AliOssUtil——————————————————————————————————————————
@Data
@AllArgsConstructor
@Slf4j
public class AliOssUtil {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
// 文件上传方法,返回值为访问图片的url
public String upload(byte[] bytes, String objectName) {
//略
}
}
//OSSConfiguration——————————————————————————————————————————
//配置类,用于创建AliOssUtils对象
@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()
);
}
}
执行顺序为dev配置文件—yml配置文件—AliOssProperties文件获取相关信息并生成Bean—OSSConfiguration类在方法aliOssUtil的参数中获取bean实例—方法体内将AliOssProperties实例的值赋给AliOssUtil实例并返回—将OSSConfiguration转为bean。
再来回到CommonController的upload()方法中,其引用的AliOssUtil类upload()方法需要传入两个参数,一个为byte数组,一个为String。其中byte[]可以将接收的图片参数调用file.getBytes()转为数组,String为图片的名称,为了防止文件重名导致覆盖,我们可以使用uuid来重命名,获取UUID后后缀的过程不再赘述(需调用UUID.randomUUID().toString(),之前未调用toString()方法,不严谨)。
@RestController
@RequestMapping("/admin/common")
@Slf4j
@Api("通用接口")
public class CommonController {
@Autowired
private AliOssUtil aliOssUtil;
/**
* 文件上传
* @param file 1
* @return 1
*/
@PostMapping("/upload")
@ApiOperation("文件上传")
public Result<String> upload(MultipartFile file) throws IOException {//参数与前端保持一致,必须是file
log.info("文件上传:{}",file);
//获取原始文件名
String originalFilename = file.getOriginalFilename();
//截取原始文件名的后缀
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
//构建新文件名称
String objectName= UUID.randomUUID().toString()+suffix;
//获取文件的访问路径
String filePath=aliOssUtil.upload(file.getBytes(), objectName);
return Result.success(filePath);
}
}
接下来测试,因为需要上传图片,所以swagger接口文档并不适合,我们选择直接前后端联调,图片成功回显即为代码正确。
新增菜品
请求路径为/admin/dish,请求方法为post,参数为DishDTO类来接收前端提交的json数据,该类最后一个属性为private List<DishFlavor> flavors = new ArrayList<>();代表口味。因此在Impl中需要将DishDTO分为Dish dish和List<DishFlavor> flavors。
在SetmealServiceImpl中因为需要向套餐表和套餐菜品关系表两张表插入数据,因此需要注解@Transactional将当前方法交给spring进行事务管理。
在插入dish时,因为sql支持批量插入,因此不再每条数据依次插入,而是批量插入数据。同时因为flavors的dishId只有在插入dish数据后才能得到,因此我们修改插入dish的语句为
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
其中useGeneratedKeys="true"告诉MyBatis使用JDBC的getGeneratedKeys方法来检索数据库生成的主键值(例如,自增主键)。keyProperty="id"指定了MyBatis应该将检索到的主键值设置到传入对象的id属性中。然后在DishServiceImpl类中,给flavors赋值前获取dish的id,并遍历flavors集合插入对应的id。
在三层中逐层新建类和接口并编写代码:
//Controller———————————————————
@RestController
@RequestMapping("/admin/dish")
@Slf4j
@Api(tags = "菜品管理")
public class DishController {
@Autowired
private DishService dishService;
@PostMapping
@ApiOperation("新增菜品")
public Result save(@RequestBody DishDTO dishDTO) {
dishService.saveWithFlavor(dishDTO);
return Result.success();
}
}
//Service———————————————————————
public interface DishService {
public void saveWithFlavor(DishDTO dishDTO);
}
//ServiceImpl———————————————————
@Service
@Slf4j
public class SetmealServiceImpl implements SetmealService {
@Autowired
private SetmealMapper setmealMapper;
@Autowired
private SetmealDishMapper setmealDishMapper;
@Autowired
private DishMapper dishMapper;
/**
* 新增套餐,同时需要保存套餐和菜品的关联关系
* @param setmealDTO
*/
@Transactional
public void saveWithDish(SetmealDTO setmealDTO) {
Setmeal setmeal = new Setmeal();// 创建套餐对象
BeanUtils.copyProperties(setmealDTO, setmeal);// 将DTO中的属性复制到套餐对象中
//向套餐表插入数据
setmealMapper.insert(setmeal);// 调用Mapper插入套餐数据到数据库
//获取生成的套餐id
Long setmealId = setmeal.getId();
// 获取套餐关联的菜品列表
List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
setmealDishes.forEach(setmealDish -> {
setmealDish.setSetmealId(setmealId);// 为每个关联菜品设置套餐ID
});
//保存套餐和菜品的关联关系
setmealDishMapper.insertBatch(setmealDishes);// 批量插入套餐和菜品的关联数据到数据库
}
}
//Mapper———————————————————————
@Mapper
public interface DishFlavorMapper {
void insertBatch(List<DishFlavor> flavors);
}
@Mapper
public interface DishMapper {
@Select("select count(id) from dish where category_id = #{categoryId}")
Integer countByCategoryId(Long categoryId);
@AutoFill(value = OperationType.INSERT)//公共字段填充,操作类型为insert
void insert(Dish dish);
}
<?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="fla" separator=",">
(#{fla.dishId},#{fla.name},#{fla.value})
</foreach>
</insert>
</mapper>
<?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">
<!-- useGeneratedKeys="true"告诉MyBatis使用JDBC的getGeneratedKeys方法来检索数据库生成的主键值(例如,自增主键)。
keyProperty="id"指定了MyBatis应该将检索到的主键值设置到传入对象的id属性中。-->
insert into sky_take_out.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>
菜品分页查询
前面已经完成了员工的分页查询和菜品分类的分页查询,菜品的分页查询也类似,来看要求:根据页码展示菜品信息、每页展示10条数据、可根据需要传入菜品分类、菜品名称、菜品状态进行查询。
请求路径为/admin/dish/page,请求方法为get,传参里page和pagesize为必须,其他三者name、categoryId、status为可选参数。请求方式为Query,不需要@RequestBody。
使用DishPageQueryDTO类来接收,因为返回数据中有一项categoryName为分类名称,其在dish表中并不存在,需查询category表后返回对应的数据,因此在Impl中查询的返回类型为DishVO。查询的sql语句为:
select dish.*, category.name as categoryName
from dish
left outer join category on dish.category_id = category.id
其中limit 0,10之类的语句不需要写,已由PageHelper完成。因为dish表和category表中都含有name字段,且DishVO中与分类名称对应的字段为categoryName,因此需要给category.name 添加语句as categoryName来起别名。
//Controller———————————————————
@GetMapping("/page")
@ApiOperation("菜品分类查询")
public Result<PageResult> page( DishPageQueryDTO dishPageQueryDTO) {
log.info("菜品分页查询:{}", dishPageQueryDTO);
PageResult pageResult=dishService.pageQuery(dishPageQueryDTO);
return Result.success(pageResult);
}
//Service———————————————————————
PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO);
//ServiceImpl———————————————————
@Override
public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) {
//基于PageHelper执行分页查询
PageHelper.startPage(dishPageQueryDTO.getPage(),dishPageQueryDTO.getPageSize());
Page<DishVO> page=dishMapper.pageQuery(dishPageQueryDTO);
return new PageResult(page.getTotal(),page.getResult());
}
//Mapper———————————————————————
Page<DishVO> pageQuery(DishPageQueryDTO dishPageQueryDTO);
<select id="pageQuery" resultType="com.sky.vo.DishVO">
select dish.*, category.name as categoryName
from dish
left outer join category on dish.category_id = category.id
<where>
<if test="name!=null">and dish.name like concat('%',#{name},'%')</if>
<if test="categoryId != null">and dish.category_id = #{categoryId}</if>
<if test="status != null">and dish.status = #{status}</if>
</where>
order by dish.create_time desc
</select>
删除菜品
业务规则:可以删除单个菜品,也可以批量删除多个菜品。同时正在售卖(status为1)的菜品不可删除。被套餐关联的菜品不能删除。删除菜品后,相关联的口味数据也需要删除。
请求路径为/admin/dish,请求方法为delete,传参ids为各个菜品id,中间以","分隔,请求方式为Query,不需要@RequestBody。
执行删除对应id的菜品和口味时,最基础的方法是对每个id执行一次删除两者的操作,但因为传入的id可能为多个,一旦多起来会生成大量的sql语句,拖慢运行效率。因此我们选择使用动态SQL,传入一个列表,再xml映射文件中使用foreach进行删除,这样仅需两个SQL语句即可完成删除。
//Controller———————————————————
@DeleteMapping
@ApiOperation("批量删除套餐")
public Result delete(@RequestParam List<Long> ids){
setmealService.deleteByIds(ids);
return Result.success();
}
//Service———————————————————————
void deleteByIds(List<Long> ids);
//ServiceImpl———————————————————
@Transactional
@Override
public void deleteByIds(List<Long> ids) {
ids.forEach(id -> {
Setmeal setmeal = setmealMapper.getById(id);
if(setmeal.getStatus()== StatusConstant.ENABLE) {
throw new DeletionNotAllowedException(MessageConstant.SETMEAL_ON_SALE);
}});
SetmealMapper.deleteByIds(ids);
SetmealDishMapper.deleteByIds(ids);
}
//Mapper———————————————————————
public interface DishMapper {
@Select("select * from dish where id=#{id}")
Dish getById(Long id);
@Delete("delete from dish where id=#{id}")
void deleteById(Long id);//老版,这里仅仅做示范,不再使用
void deleteByIds(List<Long> ids);
}
public interface SetMealDishMapper {
List<Long> getSetMealIdsByDishIds(List<Long> dishIds);
}
public interface DishFlavorMapper {
void deleteByIds(List<Long> dishIds);
}
<mapper namespace="com.sky.mapper.DishMapper">
<delete id="deleteByIds">
delete from dish where id in
<foreach collection="ids" item="id" separator="," open="(" close=")">
#{id}
</foreach>
</delete>
</mapper>
<mapper namespace="com.sky.mapper.SetMealDishMapper">
<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>
</mapper>
<mapper namespace="com.sky.mapper.DishFlavorMapper">
<delete id="deleteByIds">
delete from dish_flavor where dish_id in
<foreach collection="dishIds" item="dishId" separator="," open="(" close=")">
#{dishId}
</foreach>
</delete>
</mapper>
修改菜品
其大致与新增菜品的需求类似,但多了一项根据ID查询回显的功能。涉及到的接口较多,例如根据id查询菜品信息用于回显、根据菜品查询分类用于回显、上传图片的接口、保存菜品信息的接口。因为中间两接口已实现,因此只需实现第一、第四个接口即可。
根据id查询菜品
请求路径为/admin/dish/{id},请求方法为get。返回类型为DishVO装在Result对象中。
先查询对应ID的菜品,因为上文在删除菜品判断能否删除—是否正在售卖部分已经编写了getById()方法,此处不需要编写,将查询到的dish值通过BeanUtils.copyProperties(dish,dishVO)拷贝到DishVO对象中,此时除了categoryName和flavors都已获取到,categoryName在删除页面的分页查询中就已获取,不需要补充。
根据菜品ID查询到的口味可能为多个,因此使用List接收泛型为DishFlavor,调用dishVO.setFlavors(dishFlavors)将数据赋给dishVO即可。然后将dishVO返回。
//Controller———————————————————
@GetMapping("/{id}")
@ApiOperation("根据ID查询菜品及口味")
public Result<DishVO> getById(@PathVariable Long id) {
log.info("根据id{}查询数据",id);
DishVO dishVO = dishService.getByIdWithFlavor(id);
return Result.success(dishVO);
}
//Service———————————————————————
DishVO getByIdWithFlavor(Long id);
//ServiceImpl———————————————————
@Override
public DishVO getByIdWithFlavor(Long id) {
//根据ID查询菜品数据
Dish dish = dishMapper.getById(id);//之前已在dishMapper中编写了getById()方法
//根据菜品ID查询口味数据
List<DishFlavor> dishFlavors = dishFlavorMapper.getByDishId(id);
//将查询到的数据封装进DishVO
log.info(dishFlavors.toString());
DishVO dishVO = new DishVO();
BeanUtils.copyProperties(dish,dishVO);//将获取到的Dish拷贝到dishVO中
dishVO.setFlavors(dishFlavors);
return dishVO;
}
//Mapper———————————————————————
@Select("select * from dish_flavor where dish_id=#{dishId}")
List<DishFlavor> getByDishId(Long DishId);//为便于理解这里id替换为DishId
修改菜品
请求路径为/admin/dish,请求方法为put。
获取ID后,分为修改菜品和口味两部分。
修改菜品部分,因为前端传入的为DishDTO对象,有数据不需要使用,因此选择新建Dish对象,并将数据拷入,然后作为参数传入update()方法。因为修改涉及公共字段自动填充,因此在Mapper层的方法需添加注解@AutoFill(value = OperationType.UPDATE)。同时因为各参数不一定有值,我们选择在xml文件中使用动态SQL。
改口味较复杂,分三种情况:增加口味、删除口味、不变,依次编写较为复杂,我们选择直接删除原有的数据,然后插入新的数据。删除口味所需的deleteByDishId()方法上文已实现,直接调用,新增口味同样也已存在,直接调用insertBatch()方法并添加相关逻辑。
//Controller———————————————————
@PutMapping
@ApiOperation("修改菜品")
public Result update(@RequestBody DishDTO dishDTO) {
dishService.upadateDishAndFlavor(dishDTO);
return Result.success();
}
//Service———————————————————————
void upadateDishAndFlavor(DishDTO dishDTO);
//ServiceImpl———————————————————
@Transactional
@Override
public void update(SetmealDTO setmealDTO) {
//更新套餐基本信息
Setmeal setmeal = new Setmeal();
BeanUtils.copyProperties(setmealDTO, setmeal);
setmealMapper.update(setmeal);
//删除原有套餐内菜品信息
Long setmealId = setmealDTO.getId();
setmealDishMapper.deleteBySetmealId(setmealId);
//重新向套餐内添加菜品信息
List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
setmealDishes.forEach(setmealDish -> {
setmealDish.setSetmealId(setmealId);
});
setmealDishMapper.insertBatch(setmealDishes);
}
//Mapper———————————————————————
@Delete("delete from setmeal_dish where setmeal_id=#{setmealId}")
void deleteBySetmealId(Long setmealId);
菜品起售/停售
请求路径为/admin/setmeal/status/{status},请求方法为post。
在Impl中,我们可以直接调用之前编写的dishMapper.update()方法,该方法要求传入dish对象,因此我们使用build()将传入的status和id赋值。
同时注意,如果是停售操作,包含当前菜品的套餐也需停售,因此我们还需判断菜品的状态是否为停售,如果是则将套餐也停售,这涉及到了多张数据表的操作,需要加入注解@Transactional保持其一致性。
上文已在SetmealDishMapper中编写了getSetMealIdsByDishIds()方法用于查询对应id的套餐,可以直接调用,但其参数要求是数组,我们需新建一数组并将id传入数组中。
该方法总计所需修改菜品、根据菜品ID查询套餐、修改套餐三步,其中前两步已有实现方法,直接调用即可,重点编写第三步:
//Controller———————————————————
@ApiOperation("菜品起售、停售")
@PostMapping("/status/{status}")
public Result<String> updateStatus(@PathVariable Integer status, Long id) {
dishService.updateStatus(status,id);
return Result.success();
}
//Service———————————————————————
void updateStatus(Integer status, Long id);
//ServiceImpl———————————————————
@Transactional//因为涉及多张表所以需开启事务管理
@Override
public void updateStatus(Integer status, Long id) {
Dish dish = Dish.builder()
.status(status)
.id(id)
.build();
dishMapper.update(dish);
// 如果是停售操作,还需要将包含当前菜品的套餐也停售
if (status == StatusConstant.DISABLE) {
//getSetMealIdsByDishIds()方法要求传参List,构建List并将id传入
List<Long> dishIds = new ArrayList<>();
dishIds.add(id);
// 查询包含当前菜品的多个套餐ID,并放入列表中
List<Long> setMealIds = setmealDishMapper.getSetMealIdsByDishIds(dishIds);
// 如果存在相关套餐,则进行停售处理
if (setMealIds != null && setMealIds.size() > 0) {
for (Long setmealId : setMealIds) {
// 构建Setmeal对象并设置状态和ID
Setmeal setmeal = Setmeal.builder()
.id(setmealId)
.status(StatusConstant.DISABLE)
.build();
// 更新套餐信息
setmealMapper.update(setmeal);
}
}
}
}
//Mapper———————————————————————
@Mapper
public interface SetmealMapper {
@AutoFill(OperationType.UPDATE)
void update(Setmeal setmeal);
}
<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>
——套餐模块————————
新增套餐
该业务需遵守的规则:
- 套餐名称唯一
- 套餐必须属于某个分类
- 套餐必须包含菜品
- 名称、分类、价格、图片为必填项
- 添加菜品窗口需要根据分类类型来展示菜品
- 新增的套餐默认为停售状态
接口应分为四个:根据类型查询分类(已完成)、根据分类id查询菜品、图片上传(已完成)、新增套餐。
根据分类ID查询菜品
请求路径为/admin/dish/list,请求方法为get,前端传入分类ID,后端使用Long categoryId接收。同时返回值为多个菜品,防在一个集合内,因此方法返回类型为:Result<List<Dish>>。
在三层中逐层新建类和接口并编写代码:
//Controller———————————————————
@GetMapping("/list")
@ApiOperation("根据分类id查询菜品")
public Result<List<Dish>> list(Long categoryId){
List<Dish> list = dishService.list(categoryId);
return Result.success(list);
}
//Service———————————————————————
List<Dish> list(Long categoryId);
//ServiceImpl———————————————————
public List<Dish> list(Long categoryId) {
// 使用Builder模式构建Dish对象,设置查询条件
Dish dish = Dish.builder()
.categoryId(categoryId)
.status(StatusConstant.ENABLE)
.build();
return dishMapper.list(dish);
}
//Mapper———————————————————————
List<Dish> list(Dish dish);
<select id="list" resultType="Dish" parameterType="Dish">
select * from dish
<where>
<if test="name != null">and name like concat('%',#{name},'%')</if>
<if test="categoryId != null">and category_id = #{categoryId}</if>
<if test="status != null">and status = #{status}</if>
</where>
order by create_time desc
</select>
新增套餐
请求路径为/admin/setmeal,请求方法为post,参数为SetmealDTO类来接收前端提交的json数据。与新增菜品类似,该类最后一个属性为private List<SetmealDish> setmealDishes包含多个关联菜品SetmealDishe。因此在Impl中需要将SetmealDTO分为Setmeal setmeal和List<SetmealDish> setmealDishes分别用来装套餐信息和套餐里的多个菜品。
先向套餐表插入数据,然后修改DishMapper.xml的insert方法resultType="Dish" parameterType="Dish"以便获取套餐的ID,再将套餐ID依次插入setmealDishe中为每个关联菜品设置套餐ID。最后使用setmealDishMapper.insertBatch()向套餐菜品关系表批量插入数据。同样,因为对两个表进行了操作,需要加入@Transactional注解。
在DishServiceImpl中因为需要向菜品表和口味表两张表插入数据,因此需要注解@Transactional将当前方法交给spring进行事务管理。因此是插入操作,还需在SetmealMapper的insert方法上 添加注解@AutoFill(OperationType.INSERT),菜品套餐关系表中无creat_time等信息,因此不需要添加。
在三层中逐层新建类和接口并编写代码:
//Controller———————————————————
@RestController
@RequestMapping("/admin/setmeal")
@Api(tags = "套餐相关接口")
@Slf4j
public class SetmealController {
@Autowired
private SetmealService setmealService;
@PostMapping
@ApiOperation("新增套餐")
public Result save(@RequestBody SetmealDTO setmealDTO) {
setmealService.saveWithDish(setmealDTO);
return Result.success();
}
}
//Service———————————————————————
public interface SetmealService {
void saveWithDish(SetmealDTO setmealDTO);
}
//ServiceImpl———————————————————
@Service
@Slf4j
public class SetmealServiceImpl implements SetmealService {
@Autowired
private SetmealMapper setmealMapper;
@Autowired
private SetmealDishMapper setmealDishMapper;
@Autowired
private DishMapper dishMapper;
@Transactional
public void saveWithDish(SetmealDTO setmealDTO) {
Setmeal setmeal = new Setmeal();
BeanUtils.copyProperties(setmealDTO, setmeal);
//向套餐表插入数据
setmealMapper.insert(setmeal);
//获取生成的套餐id
Long setmealId = setmeal.getId();
List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
setmealDishes.forEach(setmealDish -> {
setmealDish.setSetmealId(setmealId);
});
//保存套餐和菜品的关联关系
setmealDishMapper.insertBatch(setmealDishes);
}
}
//Mapper———————————————————————
@Mapper
public interface SetmealMapper {
@AutoFill(OperationType.INSERT)
void insert(Setmeal setmeal);
}
@Mapper
public interface SetmealDishMapper {
void insertBatch(List<SetmealDish> setmealDishes);
}
<mapper namespace="com.sky.mapper.SetmealMapper">
<insert id="insert" parameterType="Setmeal" useGeneratedKeys="true" keyProperty="id">
insert into setmeal
(category_id, name, price, status, description, image, create_time, update_time, create_user, update_user)
values (#{categoryId}, #{name}, #{price}, #{status}, #{description}, #{image}, #{createTime}, #{updateTime},
#{createUser}, #{updateUser})
</insert>
</mapper>
<mapper namespace="com.sky.mapper.SetmealDishMapper">
<insert id="insertBatch" parameterType="list">
insert into setmeal_dish
(setmeal_id,dish_id,name,price,copies)
values
<foreach collection="setmealDishes" item="sd" separator=",">
(#{sd.setmealId},#{sd.dishId},#{sd.name},#{sd.price},#{sd.copies})
</foreach>
</insert>
</mapper>
套餐分页查询
请求路径为/admin/setmeal/page,请求方法为get,传参里page和pagesize为必须,其他三者name、categoryId、status为可选参数。请求方式为Query,不需要@RequestBody。
使用SetmealPageQueryDTO类来接收,因为返回数据中有一项categoryName为分类名称,其在setmeal表中并不存在,需查询category表后返回对应的数据,因此因此在Impl中查询的返回类型为SetmealVO。
//Controller———————————————————
@GetMapping("/page")
@ApiOperation("分页查询")
public Result<PageResult> page(SetmealPageQueryDTO setmealPageQueryDTO) {
PageResult pageResult = setmealService.pageQuery(setmealPageQueryDTO);
return Result.success(pageResult);
}
//Service———————————————————————
public PageResult pageQuery(SetmealPageQueryDTO setmealPageQueryDTO) {
int pageNum = setmealPageQueryDTO.getPage();
int pageSize = setmealPageQueryDTO.getPageSize();
PageHelper.startPage(pageNum, pageSize);
Page<SetmealVO> page = setmealMapper.pageQuery(setmealPageQueryDTO);
return new PageResult(page.getTotal(), page.getResult());
}
//Mapper———————————————————————
Page<SetmealVO> pageQuery(SetmealPageQueryDTO setmealPageQueryDTO);
<select id="pageQuery" resultType="com.sky.vo.SetmealVO">
select setmeal.*, category.name
from setmeal left join category
on setmeal.category_id = category.id
<where>
<if test="name != null">and setmeal.name like concat('%', #{name}, '%')</if>
<if test="status != null">and setmeal.status = #{status}</if>
<if test="categoryId != null">and setmeal.category_id = #{categoryId}</if>
</where>
order by setmeal.create_time desc
</select>
删除套餐
业务规则:可以删除单个套餐,也可以批量删除多个套餐。同时正在售卖(status为1)的套餐不可删除。删除套餐后,还需根据套餐id删除套餐和菜品的关联关系。
请求路径为/admin/setmeal,请求方法为delete,传参ids为各个套餐id,中间以","分隔,请求方式为Query,不需要@RequestBody。
需要三步:查询该id的套餐能否被删除、删除套餐、删除套餐菜品关系表中的数据
//Controller———————————————————
@DeleteMapping
@ApiOperation("批量删除套餐")
public Result delete(@RequestParam List<Long> ids){
setmealService.deleteBatch(ids);
return Result.success();
}
//Service———————————————————————
void deleteBatch(List<Long> ids);
//ServiceImpl———————————————————
@Transactional
@Override
public void deleteByIds(List<Long> ids) {
ids.forEach(id -> {
Setmeal setmeal = setmealMapper.getById(id);
if(setmeal.getStatus()== StatusConstant.ENABLE) {
throw new DeletionNotAllowedException(MessageConstant.SETMEAL_ON_SALE);
}});
setmealMapper.deleteByIds(ids);
setmealDishMapper.deleteByIds(ids);
}
//Mapper———————————————————————
public interface SetmealMapper {
@Select("select * from setmeal where id=#{id}")
Setmeal getById(Long id);
void deleteByIds(List<Long> ids);
}
public interface SetMealDishMapper {
void deleteByIds(List<Long> ids) ;
}
<mapper namespace="com.sky.mapper.SetmealMapper">
<delete id="deleteByIds">
delete from setmeal where id in
<foreach collection="ids" item="id" separator="," open="(" close=")">
#{id}
</foreach>
</delete>
</mapper>
<mapper namespace="com.sky.mapper.SetmealDishMapper">
<delete id="deleteByIds">
delete from setmeal_dish where id in
<foreach collection="ids" item="id" separator="," open="(" close=")">
#{id}
</foreach>
</delete>
</mapper>
修改套餐
与修改菜品类似涉及到的接口较多,例如根据id查询套餐、根据类型查询分类(已完成)、根据分类id查询菜品(已完成)、图片上传(已完成)、修改套餐。同样只需实现因此只需实现第一、第五个接口即可。
根据id查询套餐
请求路径为/admin/setmeal/{id},请求方法为get。返回类型为SetmealVO装在Result对象中。
先查询对应ID的套餐,将查询到的dish值通过BeanUtils.copyProperties(dish,dishVO)拷贝到DishVO对象中,此时除了categoryName和flavors都已获取到,categoryName在删除页面的分页查询中就已获取,不需要补充。
再查询对应ID的套餐内的菜品,使用List接收泛型为SetmealDish,将数据赋给setmealVO并返回。
//Controller———————————————————
@GetMapping("/{id}")
@ApiOperation("根据id查询套餐")
public Result<SetmealVO> getById(@PathVariable Long id) {
SetmealVO setmealVO=setmealService.getById(id);
return Result.success(setmealVO);
}
//Service———————————————————————
SetmealVO getById(Long id);
//ServiceImpl———————————————————
@Override
public SetmealVO getById(Long id) {
Setmeal setmeal = setmealMapper.getById(id);
List<SetmealDish> setmealDishes = setmealDishMapper.getBySetmealId(id);
SetmealVO setmealVO = new SetmealVO();
BeanUtils.copyProperties(setmeal, setmealVO);
setmealVO.setSetmealDishes(setmealDishes);
return setmealVO;
}
//Mapper———————————————————————
@Select("select * from setmeal_dish where setmeal_id=#{setmealId}")
List<SetmealDish> getBySetmealId(Long setmealId);
修改菜品
请求路径为/admin/setmeal,请求方法为put。
获取ID后,分为修改套餐和套餐内菜品两部分。
修改菜品部分,因为前端传入的为SetmealDTO对象,有数据不需要使用,因此选择新建Setmeal对象并将数据拷入,然后作为参数传入update()方法。
改套餐内菜品我们依然先全部删除,然后重新添加。
//Controller———————————————————
@PutMapping
@ApiOperation("修改套餐")
public Result update(@RequestBody SetmealDTO setmealDTO) {
setmealService.update(setmealDTO);
return Result.success();
}
//Service———————————————————————
void update(SetmealDTO setmealDTO);
//ServiceImpl———————————————————
@Transactional
@Override
public void update(SetmealDTO setmealDTO) {
//更新套餐基本信息
Setmeal setmeal = new Setmeal();
BeanUtils.copyProperties(setmealDTO, setmeal);
setmealMapper.update(setmeal);
//删除原有套餐内菜品信息
Long setmealId = setmealDTO.getId();
setmealDishMapper.deleteBySetmealId(setmealId);
//重新向套餐内添加菜品信息
List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
setmealDishes.forEach(setmealDish -> {
setmealDish.setSetmealId(setmealId);
});
setmealDishMapper.insertBatch(setmealDishes);
}
//Mapper———————————————————————
@Delete("delete from setmeal_dish where setmeal_id=#{setmealId}")
void deleteBySetmealId(Long setmealId);
套餐起售/停售
请求路径为/admin/dish/status/{status},请求方法为post。
如果是启售操作,需先检查套餐内菜品是否都处于起售状态,这里三个if分别判断"是否是启售操作"、"是否查询到菜品"、"菜品状态是否为停售"。
//Controller———————————————————
@PostMapping("/status/{status}")
@ApiOperation("套餐起售、停售")
public Result<String> updateStatus(@PathVariable Integer status, Long id) {
setmealService.updateStatus(status, id);
return Result.success();
}
//Service———————————————————————
void updateStatus(Integer status, Long id);
//ServiceImpl———————————————————
@Override
public void updateStatus(Integer status, Long id) {
//起售套餐需先检查套餐内是否存在停售菜品
if(status == StatusConstant.ENABLE){
List<Dish> dishList = dishMapper.getBySetmealId(id);
if(dishList != null && dishList.size() > 0){
dishList.forEach(dish -> {
if (dish.getStatus() == StatusConstant.DISABLE) {
throw new SetmealEnableFailedException(MessageConstant.SETMEAL_ENABLE_FAILED);//有停售菜品提示"套餐内包含未启售菜品,无法启售"
}
});
}
}
Setmeal setmeal = Setmeal.builder()
.id(id)
.status(status)
.build();
setmealMapper.update(setmeal);
}
//Mapper———————————————————————
public interface DishMapper {
@Select("select a.* from dish a left join setmeal_dish b on a.id = b.dish_id where b.setmeal_id = #{setmealId}")
List<Dish> getBySetmealId(Long id);
}