菜品管理
公共字段自动填充
问题分析
字段名
含义
数据类型
create_time
创建时间
datetime
create_user
创建人id
bigint
update_time
修改时间
datetime
update_user
修改人id
bigint
这些公共字段会在多处被执行相同的操作,导致代码冗余、不便于后期维护。
实现思路
自定义注解 AutoFill,用于标识需要进行公共字段自动填充的方法。
自定义切面类 AutoFillAspect,统一拦截加入了 AutoFill 注解的方法,通过反射为公共字段赋值。
在 Mapper 的方法上加入 AutoFill 注解。
代码开发
自定义注解AutoFill
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
OperationType value () ;
}
自定义切面AutoFillAspect
@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) {
}
}
完善自定义切面AutoFillAspect的autoFill方法
@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();
}
}
}
在Mapper接口的方法上加入AutoFill注解
@Insert("insert into employee (name, username, password, phone, sex, id_number, status, create_time, " +
"update_time, create_user, update_user) VALUES (#{name}, #{username}, #{password}, #{phone}, " +
"#{sex}, #{idNumber}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})")
@AutoFill(OperationType.INSERT)
void insert (Employee employee) ;
@AutoFill(OperationType.UPDATE)
void update (Employee employee) ;
@Insert("insert into category(type, name, sort, status, create_time, update_time, create_user, " +
"update_user) VALUES (#{type}, #{name}, #{sort}, #{status}, #{createTime}, #{updateTime}, " +
"#{createUser}, #{updateUser})")
@AutoFill(OperationType.INSERT)
void insert (Category category) ;
@AutoFill(OperationType.UPDATE)
void update (Category category) ;
将业务层为公共字段赋值的代码注释掉
功能测试
通过观察控制台输出的SQL来确定公共字段填充是否完成。
新增菜品
需求分析和设计
产品原型
业务规则
菜品名称必须是唯一的。
菜品必须属于某个分类下,不能单独存在。
新增菜品时可以根据情况选择菜品的口味
每个菜品必须对应一张图片。
接口设计
根据类型查询分类(已完成)
文件上传
新增菜品
数据库设计
dish菜品表
字段名
数据类型
说明
备注
id
bigint
主键
自增
name
varchar(32)
菜品名称
唯一
category_id
bigint
分类id
逻辑外键
price
decimal(10,2)
菜品价格
image
varchar(255)
图片路径
description
varchar(255)
菜品描述
status
int
售卖状态
1起售 0停售
create_time
datetime
创建时间
update_time
datetime
最后修改时间
create_user
bigint
创建人id
update_user
bigint
最后修改人id
dish_flavor口味表
字段名
数据类型
说明
备注
id
bigint
主键
自增
dish_id
bigint
菜品id
逻辑外键
name
varchar(32)
口味名称
value
varchar(255)
口味值
代码开发
文件上传接口
在application-dev.yml配置阿里云相关信息
sky:
alioss:
endpoint: oss-cn-beijing.aliyuncs.com
access-key-id: LTAI5tPeFLzsPPT8gG3LPW64
access-key-secret: U6k1brOZ8gaOIXv3nXbulGTUzy6Pd7
bucket-name: sky-itcast
在application.yml引用application-dev.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}
自定义配置类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());
}
}
自定义CommonController类并实现upload方法
@RestController
@RequestMapping("/admin/commom")
@Slf4j
@Api(tags = "通用接口")
public class CommonController {
@Autowired
private AliOssUtil aliOssUtil;
@PostMapping("/upload")
@ApiOperation("文件上传")
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 (Exception e) {
log.error("文件上传失败:{}" , e);
}
return Result.error(MessageConstant.UPLOAD_FAILED);
}
}
新增菜品接口
根据新增菜品接口设计对应的DTO
@Data
public class DishDTO implements Serializable {
private Long id;
private String name;
private Long categoryId;
private BigDecimal price;
private String image;
private String description;
private Integer status;
private List<DishFlavor> flavors = new ArrayList <>();
}
自定义DishController类并创建save方法
@RestController
@RequestMapping("/admin/dish")
@Api(tags = "菜品相关接口")
@Slf4j
public class DishController {
@Autowired
private DishService dishService;
@PostMapping
public Result save (@RequestBody DishDTO dishDTO) {
log.info("新增菜品:{}" , dishDTO);
dishService.saveWithFlavor(dishDTO);
return Result.success();
}
}
自定义DishService接口并声明saveWithFlavor方法
public interface DishService {
void saveWithFlavor (DishDTO dishDTO) ;
}
自定义DishServiceImpl实现类并实现saveWithFlavor方法
@Service
public class DishServiceImpl implements DishService {
@Autowired
private DishMapper dishMapper;
@Autowired
private DishFlavorMapper dishFlavorMapper;
@Override
@Transactional
public void saveWithFlavor (DishDTO dishDTO) {
Dish dish = new Dish ();
BeanUtils.copyProperties(dishDTO, dish);
dishMapper.insert(dish);
Long dishId = dish.getId();
List<DishFlavor> flavors = dishDTO.getFlavors();
if (flavors != null && !flavors.isEmpty()) {
flavors = flavors.stream().filter(dishFlavor -> !Objects.equals(dishFlavor.getName(), "" )).collect(Collectors.toList());
flavors.forEach(dishFlavor -> dishFlavor.setDishId(dishId));
dishFlavorMapper.insertBatch(flavors);
}
}
}
在DishMapper中声明insert方法
@AutoFill(value = OperationType.INSERT)
void insert (Dish dish) ;
自定义DishMapper.xml并编写SQL
<?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 >
自定义DishFlavorMapper接口并声明insertBatch方法
@Mapper
public interface DishFlavorMapper {
void insertBatch (List<DishFlavor> flavors) ;
}
自定义DishFlavorMapper.xml并编写SQL
<?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 >
功能测试
可以通过接口文档进行测试,最后完成前后端联调测试即可。
菜品分页查询
需求分析和设计
产品原型
业务规则
根据页码展示菜品信息。
每页展示10条数据。
分页查询时可以根据需要输入菜品名称、菜品分类、菜品状态进行查询。
接口设计
代码开发
根据菜品分页查询接口定义设计对应的DTO
@Data
public class DishPageQueryDTO implements Serializable {
private int page;
private int pageSize;
private String name;
private Integer categoryId;
private Integer status;
}
根据菜品分页查询接口定义设计对应的VO
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DishVO implements Serializable {
private Long id;
private String name;
private Long categoryId;
private BigDecimal price;
private String image;
private String description;
private Integer status;
private LocalDateTime updateTime;
private String categoryName;
private List<DishFlavor> flavors = new ArrayList <>();
}
根据接口定义创建DishController的page分页查询方法
@GetMapping("/page")
public Result<PageResult> page (DishPageQueryDTO dishPageQueryDTO) {
log.info("菜品分页查询:{}" , dishPageQueryDTO);
PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);
return Result.success(pageResult);
}
在 DishService 中扩展分页查询方法
PageResult pageQuery (DishPageQueryDTO dishPageQueryDTO) ;
在 DishServiceImpl 中实现分页查询方法
@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 接口中声明 pageQuery 方法
Page<DishVO> pageQuery (DishPageQueryDTO dishPageQueryDTO) ;
在 DishMapper.xml 中编写SQL
<select id ="pageQuery" resultType ="com.sky.vo.DishVO" >
select d.*, c.name category_name from dish d left join category c on d.category_id = c.id
<where >
<if test ="name != null" > and d.name = #{name}</if >
<if test ="name != null" > and d.category_id = #{categoryId}</if >
<if test ="name != null" > and d.status = #{status}</if >
</where >
order by d.create_time desc
</select >
功能测试
可以通过接口文档进行测试,最后完成前后端联调测试即可。
删除菜品
需求分析和设计
产品原型
业务规则
可以一次删除一个菜品,也可以批量删除菜品。
启售中的菜品不能删除。
被套餐关联的菜品不能删除。
删除菜品后,关联的口味数据也需要删除掉。
接口设计
数据库设计
代码开发
根据删除菜品的接口定义在DishController中创建方法
@DeleteMapping
@ApiOperation("批量删除菜品")
public Result delete (@RequestParam List<Long> ids) {
log.info("批量删除菜品:{}" , ids);
dishService.deleteBatch(ids);
return Result.success();
}
在DishService接口中声明deleteBatch方法
void deleteBatch (List<Long> ids) ;
在DishServiceImpl中实现deleteBatch方法
@Override
@Transactional
public void deleteBatch (List<Long> ids) {
for (Long id : ids) {
Dish dish = dishMapper.getById(id);
if (dish.getStatus().equals(StatusConstant.ENABLE)) {
throw new DeletionNotAllowedException (MessageConstant.DISH_ON_SALE);
}
}
List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(ids);
if (setmealIds != null && !setmealIds.isEmpty()) {
throw new DeletionNotAllowedException (MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
}
for (Long id : ids) {
dishMapper.deleteById(id);
dishFlavorMapper.deleteByDishId(id);
}
}
在DishMapper中声明getById方法,并配置SQL
@Select("select * from dish where id = #{id}")
Dish getById (Long id) ;
创建SetmealDishMapper,声明getSetmealIdsByDishIds方法,并在xml文件中编写SQL
package com.sky.mapper;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface SetmealDishMapper {
List<Long> getSetmealIdsByDishIds (List<Long> dishIds) ;
}
<?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.SetmealDishMapper" >
<select id ="getSetmealIdsByDishIds" resultType ="java.lang.Long" >
select setmeal_id from setmeal_dish where dish_id in
<foreach collection ="dishIds" item ="dishId" open ="(" separator ="," close =")" >
#{dishId}
</foreach >
</select >
</mapper >
在DishMapper中声明deleteById方法并配置SQL
@Delete("delete from dish where id = #{id}")
void deleteById (Long id) ;
在DishFlavorMapper中声明deleteByDishId方法并配置SQL
@Delete("delete from dish_flavor where dish_id = #{dishId}")
void deleteByDishId (Long dishId) ;
代码优化:根据菜品id集合批量删除菜品数据及其关联的口味数据
@Override
@Transactional
public void deleteBatch (List<Long> ids) {
for (Long id : ids) {
Dish dish = dishMapper.getById(id);
if (dish.getStatus().equals(StatusConstant.ENABLE)) {
throw new DeletionNotAllowedException (MessageConstant.DISH_ON_SALE);
}
}
List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(ids);
if (setmealIds != null && !setmealIds.isEmpty()) {
throw new DeletionNotAllowedException (MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
}
dishMapper.deleteByIds(ids);
dishFlavorMapper.deleteByDishIds(ids);
}
void deleteByIds (List<Long> ids) ;
void deleteByDishIds (List<Long> dishIds) ;
<delete id ="deleteByIds" >
delete from dish where id in
<foreach collection ="ids" item ="id" open ="(" separator ="," close =")" >
#{id}
</foreach >
</delete >
<delete id ="deleteByDishIds" >
delete from dish_flavor where dish_id in
<foreach collection ="dishIds" item ="dishId" open ="(" separator ="," close =")" >
#{dishId}
</foreach >
</delete >
功能测试
可以通过接口文档进行测试,最后完成前后端联调测试即可。
修改菜品
需求分析和设计
产品原型
接口设计
根据id查询菜品
根据类型查询分类(已实现)
文件上传(已实现)
修改菜品
代码开发
根据id查询菜品接口开发
根据根据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接口中声明getByIdWithFlavor方法
DishVO getByIdWithFlavor (Long id) ;
在DishServiceImpl中实现getByIdWithFlavor方法
@Override
@Transactional
public DishVO getByIdWithFlavor (Long id) {
Dish dish = dishMapper.getById(id);
List<DishFlavor> flavors = dishFlavorMapper.getByDishId(id);
DishVO dishVO = new DishVO ();
BeanUtils.copyProperties(dish, dishVO);
dishVO.setFlavors(flavors);
return dishVO;
}
在DishFlavorMapper中声明getByDishId方法,并配置SQL
@Select("select * from dish_flavor where dish_id = #{dishId}")
List<DishFlavor> getByDishId (Long dishId) ;
修改菜品接口开发
根据修改菜品的接口定义在DishController中创建方法
@PutMapping
@ApiOperation("修改菜品")
public Result update (@RequestBody DishDTO dishDTO) {
log.info("修改菜品:{}" , dishDTO);
dishService.updateWithFlavor(dishDTO);
return Result.success();
}
在DishService接口中声明updateWithFlavor方法
void updateWithFlavor (DishDTO dishDTO) ;
在DishServiceImpl中实现updateWithFlavor方法
@Override
@Transactional
public void updateWithFlavor (DishDTO dishDTO) {
Dish dish = new Dish ();
BeanUtils.copyProperties(dishDTO, dish);
dishMapper.update(dish);
dishFlavorMapper.deleteByDishId(dishDTO.getId());
Long dishId = dish.getId();
List<DishFlavor> flavors = dishDTO.getFlavors();
if (flavors != null && !flavors.isEmpty()) {
flavors = flavors.stream().filter(dishFlavor -> !dishFlavor.getName().isEmpty()).collect(Collectors.toList());
flavors.forEach(dishFlavor -> dishFlavor.setDishId(dishId));
dishFlavorMapper.insertBatch(flavors);
}
}
在DishMapper中声明update方法
@AutoFill(OperationType.UPDATE)
void update (Dish dish) ;
在DishMapper.xml中声明update方法并配置SQL
<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 >
功能测试
可以通过接口文档进行测试,最后完成前后端联调测试即可。
菜品启售停售
需求分析和设计
产品原型
业务规则
接口设计
代码开发
根据菜品启售停售的接口定义在DishController中创建方法
@PostMapping("/status/{status}")
@ApiOperation("菜品启售停售")
public Result startOrStop (@PathVariable Integer status, Long id) {
log.info("菜品启售停售:{},{}" , status, id);
dishService.startOrStop(status, id);
return Result.success();
}
在DishService接口中声明startOrStop方法
void startOrStop (Integer status, Long id) ;
在DishServiceImpl中实现startOrStop方法
@Override
@Transactional
public void startOrStop (Integer status, Long id) {
Dish dish = Dish.builder()
.status(status)
.id(id)
.build();
dishMapper.update(dish);
if (status.equals(StatusConstant.DISABLE)) {
List<Long> dishIds = new ArrayList <>();
dishIds.add(id);
List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(dishIds);
if (setmealIds != null && !setmealIds.isEmpty()) {
for (Long setmealId : setmealIds) {
Setmeal setmeal = Setmeal.builder()
.status(StatusConstant.DISABLE)
.id(setmealId)
.build();
setmealMapper.update(setmeal);
}
}
}
}
在SetmealMapper中声明update方法
@AutoFill(OperationType.UPDATE)
void update (Setmeal setmeal) ;
创建SetmealMapper.xml并编写SQL
<?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" >
update setmeal
<set >
<if test ="categoryId != null" > category_id = #{categoryId},</if >
<if test ="name != null" > name = #{name},</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集合批量修改套餐数据
@Override
@Transactional
public void startOrStop (Integer status, Long id) {
Dish dish = Dish.builder()
.status(status)
.id(id)
.build();
dishMapper.update(dish);
if (status.equals(StatusConstant.DISABLE)) {
List<Long> dishIds = new ArrayList <>();
dishIds.add(id);
List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(dishIds);
if (setmealIds != null && !setmealIds.isEmpty()) {
setmealMapper.startOrStopBatch(setmealIds, StatusConstant.DISABLE);
}
}
}
@AutoFill(OperationType.UPDATE)
void startOrStopBatch (List<Long> setmealIds, Integer startOrStop) ;
//SetmealMapper.xml
<update id ="startOrStopBatch" >
update setmeal set status = #{startOrStop} where id in
<foreach collection ="setmealIds" item ="setmealId" open ="(" separator ="," close =")" >
#{setmealId}
</foreach >
</update >
功能测试
可以通过接口文档进行测试,最后完成前后端联调测试即可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了