苍穹外卖项目Day03
1、菜品管理
1.1、公共字段自动填充
1.1.1、问题分析
业务表中的公共字段:
问题:代码冗余、不便于后期维护
1.1.2、实现思路
- 自定义注解AutoFill,用于标识需要进行公共字段自动填充的方法
- 自定义切面类AutoFillAspect,统一拦截加入了AutoFill注解的方法,通过反射为公共字段赋值
- 在Mapper的方法加上AutoFill注解
技术点:枚举、注解、AOP、反射
1.1.3、代码开发
AutoFill
/**
* 自定义注解,用于标识某个方法需要进行功能字段自动填充处理
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
//数据库操作类型:UPDATE 、INSERT
OperationType value();
}
AutoFillAspect
/**
* 自定义切点,实现公共字段自动填充处理逻辑
*/
@Aspect
@Component
@Slf4j
public class AutoFillAspect {
/**
* 切入点
*/
//直接Annotation会扫描全部,影响效率,加上Execution就只会扫描mp里面带AutoFill注解的方法
@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
public void autoFillPointCut(){
}
/**
* 前置通知,在通知中进行公共字段的赋值
* @param joinPoint
*/
@Before("autoFillPointCut()")
public void autoFill(JoinPoint joinPoint) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
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){
//为四个公共字段赋值
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);
}else {
//为两个公共字段赋值
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);
}
}
}
Mapper层
@AutoFill(value = OperationType.INSERT)
void insert(Category category);
@AutoFill(value = OperationType.UPDATE)
void update(Category category);
Impl层注释掉之前的公共字段
1.1.4、功能测试
测试时间的创建人更新人的数据是否正确
1.2、新增菜品
1.2.1、需求分析和设计
业务规则:
- 菜品名称必须是唯一的
- 菜品必须属于某个分类下,不能单独存在
- 新增菜品时可以根据情况选择菜品的口味
- 每个菜品必须对应一张图片
接口设计:
- 根据类型查询分类(已完成)
- 文件上传
- 新增菜品
数据库设计(dish菜品表和dish_favor口味表):
1.2.2、代码开发
开发文件上传接口:
配置阿里云:
application.yaml:
sky:
jwt:
# 设置jwt签名加密时使用的秘钥
admin-secret-key: itcast
# 设置jwt过期时间
admin-ttl: 7200000
# 设置前端传递过来的令牌名称
admin-token-name: token
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.yaml
alioss:
endpoint: oss-cn-beijing.aliyuncs.com
access-key-id: LTAI5tCbpEZCVVnr2z9sEEQ6
access-key-secret: nNZeAsoyR9eyTX2ReUoLpJj7jP9F1R
bucket-name: sky-jjq
CommonController
/**
* 通用接口
*/
@RestController
@RequestMapping("/admin/common")
@Api(tags = "通用接口")
@Slf4j
public class CommonController {
@Autowired
private AliOssUtil aliOssUtil;
/**
* 文件上传
* @param file
* @return
*/
@PostMapping("/upload")
@ApiOperation("文件上传")
public Result<String> upload(MultipartFile file){
log.info("文件上传:{}",file);
//文件请求路径
try {
//原始文件名
String originalFilename = file.getOriginalFilename();
//截图原始文件名的后缀 dsa.png
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.info("文件上传失败:{}",e);
}
return null;
}
}
OssConfiguration
/**
* 配置类,用于创建AliOssUtil对象
*/
@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());
}
}
DishController
@RestController
@Slf4j
@RequestMapping("/admin/dish")
@Api(tags = "菜品相关接口")
public class DishController {
@Autowired
private DishService dishService;
/**
* 新增菜品
* @param dishDTO
* @return
*/
@PostMapping
@ApiOperation("新增菜品")
public Result save(@RequestBody DishDTO dishDTO){
log.info("新增菜品:{}",dishDTO);
dishService.saveWithFalvor(dishDTO);
return Result.success();
}
DishService
public interface DishService {
/**
* 新增菜品和对应的口味数据
* @param dishDTO
*/
void saveWithFalvor(DishDTO dishDTO);
}
DishServiceImpl
@Service
@Slf4j
public class DishServiceImpl implements DishService {
@Autowired
private DishMapper dishMapper;
@Autowired
private DishFlavorMapper dishFlavorMapper;
/**
* 新增菜品和对应的口味数据
* @param dishDTO
*/
//涉及多个表的应用,保证数据的一致性
@Transactional
@Override
public void saveWithFalvor(DishDTO dishDTO) {
//向菜品表插入1条数据,因为DishDTO中包含了口味数据,我不需要,所以在这样用dish实体
Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO,dish);
//向菜品表插入1条数据
dishMapper.insert(dish);
//获取insert语句生成的主键值
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);
}
}
}
DishMapper
@Mapper
public interface DishMapper {
/**
* 根据分类id查询菜品数量
* @param categoryId
* @return
*/
@Select("select count(id) from sky_take_out.dish where category_id = #{categoryId}")
Integer countByCategoryId(Long categoryId);
/**
* 新增菜品
* @param dish
*/
@AutoFill(value = OperationType.INSERT)
void insert(Dish dish);
}
DishMapper.xml
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.sky.mapper.DishMapper">
<!--插入完成的属性值会赋给id-->
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into sky_take_out.dish (name, category_id, price, image, description, create_time, update_time, create_user, update_user)
VALUES (#{name},#{categoryId},#{price},#{image},#{description},#{createTime},#{updateTime},#{createUser},#{updateUser})
</insert>
</mapper>
DishFlavorMapper
@Mapper
public interface DishFlavorMapper {
/**
* 批量插入口味数据
* @param flavors
*/
void insertBatch(List<DishFlavor> flavors);
}
DishFlavorMapper.xml
<!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 sky_take_out.dish_flavor (dish_id, name, value) VALUES
<foreach collection="flavors" item="df" separator=",">
(#{df.dishId},#{df.name},#{df.value})
</foreach>
</insert>
</mapper>
1.2.3、功能测试
1.3、菜品分页查询
1.3.1、需求分析和设计
业务规则:
- 根据页码展示菜品信息
- 每页展示10条数据
- 分页查询时可以根据需要输入菜品名称、菜品分类、菜品状态进行查询
接口设计:
1.3.2、代码开发
根据菜品分页查询接口定义设计对应的DTO:
根据菜品分页查询接口定义设计对应的VO:
DishController
@GetMapping("/page")
@ApiOperation("菜品分页查询")
public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO){
log.info("菜品分页查询:{}",dishPageQueryDTO);
PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);
return Result.success(pageResult);
}
DishService
PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO);
Impl
@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());
}
DishMapper
Page<DishVO> pageQuery(DishPageQueryDTO dishPageQueryDTO);
DishMapper.xml
<select id="pageQuery" resultType="com.sky.vo.DishVO">
SELECT d.*,c.name as categoryName FROM dish d left 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">d.category_id = #{categoryId}</if>
<if test="status != null">d.status = #{status}</if>
</where>
order by d.create_time desc
</select>
1.3.3、功能测试
1.4、删除菜品
1.4.1、需求分析和设计
业务规则:
- 可以一次删除一个菜品,也可以批量删除菜品
- 起售中的菜品不能删除
- 被套餐关联的菜品不能删除
- 删除菜品后,关联的口味数据页需要删除掉
接口设计:
数据库设计:
1.4.2、代码开发
DishController
/**
*菜品批量删除
* @param ids
* @return
*/
@DeleteMapping
@ApiOperation("菜品批量删除")
public Result delete(@RequestParam List<Long> ids){
log.info("菜品批量删除:{}",ids);
dishService.deleteBatch(ids);
return Result.success();
}
DishService
void deleteBatch(List<Long> ids);
DishServiceImpl
@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.getSetmealIdsByDishIds(ids);
if (setmealIds != null && setmealIds.size() > 0){
//当前菜品被套餐关联了,不能删除
throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
}
//删除菜品关联的菜品数据
// for (Long id : ids) {
// dishMapper.deleteById(id);
// //删除菜品关联的口味数据
// dishFlavorMapper.deleteByDishId(id);
// }
//根据菜品id集合批量删除菜品数据
dishMapper.deleteByIds(ids);
//根据菜品id集合批量删除关联的口味数据
dishFlavorMapper.deleteByDishIds(ids);
}
DishMapper
void deleteByIds(List<Long> ids);
DishFlavorMapper
void deleteByDishIds(List<Long> dishIds);
DishMapper.xml
<delete id="deleteByIds">
delete from dish where id in
<foreach collection="ids" item="id" separator="," open="(" close=")">
#{id}
</foreach>
</delete>
DishFlavorMapper.xml
<delete id="deleteByDishIds">
delete from sky_take_out.dish_flavor where dish_id in
<foreach collection="dishIds" item="dishId" separator="," open="(" close=")">
#{dishId}
</foreach>
</delete>
1.4.3、功能测试
1.5、修改菜品
1.5.1、需求分析与设计
接口设计:
- 根据id查询菜品
- 根据类型查询分类(已实现)
- 文件上传(已实现)
- 修改菜品状态
- 修改菜品
1.5.2、代码开发
1、根据id查询菜品
DishController
@GetMapping("/{id}")
@ApiOperation("根据id查询菜品")
public Result<DishVO> getById(@PathVariable Long id){
log.info("根据id查询菜品:{}",id);
DishVO dishVO = dishService.getByIdWithFlavor(id);
return Result.success(dishVO);
}
DishService
DishVO getByIdWithFlavor(Long id);
DishServiceImpl
@Override
public DishVO getByIdWithFlavor(Long id) {
//根据id查询菜品数据
Dish dish = dishMapper.getById(id);
//根据菜品id查询口味数据
List<DishFlavor> dishFlavors = dishFlavorMapper.getByDishid(id);
//将查询到的数据封装到Vo
DishVO dishVO = new DishVO();
BeanUtils.copyProperties(dish,dishVO);
dishVO.setFlavors(dishFlavors);
return dishVO;
}
DishFlavorMapper
@Select("select * from sky_take_out.dish_flavor where dish_id = #{dishId}")
List<DishFlavor> getByDishid(Long dishId);
2、修改菜品和菜品状态
DishController
/**
* 修改菜品
* @param dishDTO
* @return
*/
@PutMapping
@ApiOperation("修改菜品")
public Result update(@RequestBody DishDTO dishDTO){
log.info("修改菜品:{}",dishDTO);
dishService.updateWithFlavor(dishDTO);
return Result.success();
}
/**
* 启用禁用菜品
* @param status
* @param id
* @return
*/
@PostMapping("/status/{status}")
@ApiOperation("启用禁用菜品")
public Result startOrStop(@PathVariable Integer status,Long id){
log.info("启用禁用菜品:{},{}",status,id);
dishService.startOrStop(status,id);
return Result.success();
}
DishService
/**
* 根据id修改菜品基本信息和对应的口味信息
* @param dishDTO
* @return
*/
void updateWithFlavor(DishDTO dishDTO);
/**
* 启用禁用菜品
* @param status
* @param id
*/
void startOrStop(Integer status, Long id);
DishServiceImpl
/**
* 根据id修改菜品基本信息和对应的口味信息
* @param dishDTO
* @return
*/
@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);
}
}
/**
* 启用禁用菜品
* @param status
* @param id
*/
@Override
public void startOrStop(Integer status, Long id) {
Dish dish = Dish.builder()
.status(status)
.id(id)
.build();
dishMapper.update(dish);
}
DishMapper
/**
* 根据菜品id集合批量删除菜品
* @param ids
*/
void deleteByIds(List<Long> ids);
/**
* 根据id动态修改菜品数据
* @param dish
*/
@AutoFill(value = OperationType.UPDATE)
void update(Dish dish);
DishMapper.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>
1.5.3、功能测试
@AutoFill(value = OperationType.UPDATE)
void update(Dish dish);
DishMapper.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>
1.5.3、功能测试