小白勇闯《苍穹外卖》Day3
碎碎念:开学马上大四了,但是前几年一直没有好好学习专业课,只是临近考试的时候突击复习一下,导致到现在还是小白T_T 7月放暑假在家陆陆续续看了黑马的Java基础和web相关课程,但是并没有完全看完,也没有深入理解...眼看时间已经来不及了,简历上还是一片空白,决定硬着头皮学项目了,干中学吧,加油!!!
叠个甲:只是发表一下学习记录,内容不一定正确,希望大家包容一下,欢迎大佬们多多指正!!!
菜品管理
前两天已经完成了环境搭建,员工管理,分类管理,今天开始菜品管理了
公共字段自动填充
技术点:枚举、注解、AOP、反射
——————————————这里恶补了一下知识点————————————————
枚举:适合做信息分类和标志。这里用来标识操作类型,insert/update
注解:自定义注解
public @i8nterface 注解名称{ public 属性类型 属性名() default 默认值 ; }特殊属性value:在使用时如果只有一个value, value名称可省略不写
原理:本质上是一个接口继承了annotation类,里面定义的属性其实上是一个一个的抽象方法
使用举例:
@注解名(aaa="李四",bbb=true,ccc={"java","python"}) public void test(){ }元注解:注解注解的注解
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) public @interface Test{ }@Retention约束存活范围
@Target约束标记范围
解析注释:使用解析注解的方法
AOP:面向切面编程。通用公共功能从业务代码中剥离。
@Aspect+@Component定义切面类;@Pointcut定义切点表达式;编写通知@Before前置,@AfterReturning返回,@AfterThrowing 异常通知,@After 最终通知,@Around 环绕通知
反射:加载类,并且以编程的方式解剖类中各种成分(成员变量,方法,构造器等)
①加载类,获取类的字节码文件:Calss对象(三种方法)
②获取类的构造器:Constructor对象
③获取类的成员变量:Field对象
set()复制,get()取值
④获取类的成员方法:Method对象
invoke()触发执行
反射作用:可以得到一个类的全部成分然后操作;可以破坏封装性;可以绕过泛型约束
需求
(1).在新增数据时, 将createTime、updateTime 设置为当前时间, createUser、updateUser设置为当前登录用户ID。
(2).在更新数据时, 将updateTime 设置为当前时间, updateUser设置为当前登录用户ID。
实现思路
代码开发
创建注解和切面的包和类
定义注解类
/** * 自动填充 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AutoFill { /** * 数据库操作类型 * @return */ OperationType value(); }已定义好的枚举类
/** * 数据库操作类型 */ public enum OperationType { /** * 更新操作 */ UPDATE, /** * 插入操作 */ INSERT }已定义好的AutoFillConstant常量类
/** * 公共字段自动填充相关常量 */ public class AutoFillConstant { /** * 实体类中的方法名称 */ public static final String SET_CREATE_TIME = "setCreateTime"; public static final String SET_UPDATE_TIME = "setUpdateTime"; public static final String SET_CREATE_USER = "setCreateUser"; public static final String SET_UPDATE_USER = "setUpdateUser"; }切面类
(@Slf4j 是 lombok 提供注解,编译期自动生成 Logger 日志实例,简化日志对象创建,直接使用log.info/log.error输出日志。)
/** * 自定义切面类,统一为公共字段赋值 */ @Aspect @Component @Slf4j public class AutoFillAspect { /** * 切入点 */ @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")//不仅要在mapper里还要满足注解 public void autoFillPointCut() {} /** * 通知 自动填充公共字段 * @param joinPoint */ @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 time = LocalDateTime.now(); Long empId = BaseContext.getCurrentId(); if (operationType == OperationType.INSERT) { //当前执行的是insert操作,为4个字段赋值 try { //获得set方法对象----Method Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class); Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class); Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class); Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class); //通过反射调用目标对象的方法 setCreateTime.invoke(entity, time); setUpdateTime.invoke(entity, time); setCreateUser.invoke(entity, empId); setUpdateUser.invoke(entity, empId); } catch (Exception ex) { log.error("公共字段自动填充失败:{}", ex.getMessage()); } }else if(operationType == OperationType.UPDATE){ //当前执行的是update操作,为2个字段赋值 try { //获得set方法对象----Method 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, time); setUpdateUser.invoke(entity, empId); } catch (Exception ex) { log.error("公共字段自动填充失败:{}", ex.getMessage()); } } } }这里我AOP掌握不熟练(下面是我问ai给我的解释)
public void autoFillPointCut () {}这个空方法 = 给切点表达式起别名,方便复用。
JoinPoint连接点,joinPoint相当于一个袋子装被拦截的 mapper 方法全部信息
这段代码的通俗理解:
先从袋子里拿出来[被拦截的那个方法];再拿到写在 mapper 方法上面的注解;把注解里面的内容取出来,得到INSERT或者UPDATE;从袋子里,拿到调用 mapper 时传进去的参数;判断是否为空;若不为空,取第一个参数,这个就是我们要存数据库的实体;准备要填的数据,时间和id;若为INSERT,用反射在运行的时候,找到这 4 个 set 方法,拿到方法对象,.invoke(对象,值)调用这个 set 方法,把值设置进去;若为UPDATE,找到这 2 个 set 方法,拿到方法对象,.invoke(对象,值)调用这个 set 方法,把值设置进去;反射很容易报错,捕获异常,打印错误
在Mapper接口的方法上加入 AutoFill 注解
@AutoFill(OperationType.INSERT) @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})") void insert(Category category); @AutoFill(OperationType.UPDATE) void update(Category category); @AutoFill(OperationType.UPDATE) void update(Employee employee);注释掉原代码
测试
(测试的时候突然登录不上前端页面了,发现是没打开nginx,大家不要学我犯这么笨蛋的错误)
发现打了断点但是修改员工数据时断点不停,前端显示修改成功
排查了以下三点:
Employee 实体的@Data注解存在,字段updateTime、updateUser定义正确。
看控制台没有输出公共字段自动填充失败,定位反射哪里报错。
数据库里这条员工记录,update_time、update_user发生变化。
拦截的是 Mapper 接口,MyBatis 运行时会生成代理对象来执行方法,IDEA 调试器对动态代理生成的类,断点经常无法触发。后面我也尝试修改断点的 Suspend 模式,从 All 改成 Thread,断点换到set赋值那里,但是代理场景下依旧不稳定。
所以我直接手动输出日志了
功能测试成功!
所以实际开发中日志打印也是很重要的调试手段,不能完全依赖 IDEA 断点。
原来是因为没有debug运行。。。
新增菜品
需求分析和设计
接口设计:
根据类型查询分类(已完成)
文件上传
新增菜品
数据库设计
代码开发
文件上传
—————————————————这里去补了一下知识点————————————————
需要先注册阿里云--->充值--->开通OSS--->创建bucket--->获取并配置AccessKey
application-dev.yml
alioss: endpoint: 你自己的 access-key-id: 你自己的 access-key-secret: 你自己的 bucket-name: 你自己的application.yml
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
@Configuration @Slf4j public class OssConfiguration { /** * 通过spring管理对象 * @param aliOssProperties * @return */ @Bean @ConditionalOnMissingBean public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties) { log.info("开始创建阿里云OSS工具类..."); return new AliOssUtil(aliOssProperties.getEndpoint(), aliOssProperties.getAccessKeyId(), aliOssProperties.getAccessKeySecret(), aliOssProperties.getBucketName()); } }新建类CommonController
@RestController @RequestMapping("/admin/common") @Slf4j @Api(tags = "通用接口") public class CommonController { @Autowired private AliOssUtil aliOssUtil; /** * 文件上传 * @param file * @return */ @PostMapping("/upload") @ApiOperation("文件上传") public Result<String> upload(MultipartFile file){ log.info(file.getName()); //原始文件名 String originalFilename = file.getOriginalFilename(); String extension = originalFilename.substring(originalFilename.lastIndexOf(".")); //将文件上传的阿里云 String fileName = UUID.randomUUID().toString() + extension; try { String filePath = aliOssUtil.upload(file.getBytes(), fileName); return Result.success(filePath); } catch (IOException e) { log.error("文件上传失败:{}", e.getMessage()); } return Result.error(MessageConstant.UPLOAD_FAILED); } }测试一下,可以正常上传
新增菜品
DishController
@RestController @RequestMapping("/admin/dish") @Api(tags = "菜品相关接口") @Slf4j public class DishController { @Autowired private DishService dishService; /** * 新增菜品 * @param dishDTO * @return */ @PostMapping @ApiOperation("新增菜品") public Result<String> save(@RequestBody DishDTO dishDTO){ log.info("新增菜品:{}", dishDTO); dishService.saveWithFlavor(dishDTO); return Result.success(); } }DishService
public interface DishService { /** * 新增菜品 * @param dishDTO */ void saveWithFlavor(DishDTO dishDTO); }DishServiceImpl
@Service public class DishServiceImpl implements DishService { @Autowired private DishMapper dishMapper; @Autowired private DishFlavorMapper dishFlavorMapper; /** * 新增菜品 * @param dishDTO */ @Transactional public void saveWithFlavor(DishDTO dishDTO) { Dish dish = new Dish(); BeanUtils.copyProperties(dishDTO, dish); //向菜品表dish插入1条数据 dishMapper.insert(dish); //获取菜品的主键值 Long dishId = dish.getId(); List<DishFlavor> flavors = dishDTO.getFlavors(); if(flavors != null && flavors.size() > 0){ //向口味表dish_flavor插入n条 flavors.forEach(dishFlavor -> { dishFlavor.setDishId(dishId); }); //批量插入 dishFlavorMapper.insertBatch(flavors); } } }DishMapper
/** * 插入菜品数据 * @param dish */ @AutoFill(OperationType.INSERT) void insert(Dish dish);DishMapper.xml
<!-- useGeneratedKeys:true 表示获取主键值 keyProperty="id" 表示将主键值赋给id属性--> <insert id="insert" useGeneratedKeys="true" keyProperty="id"> insert into dish (status, name, category_id, price, image, description, create_time, update_time, create_user,update_user) values (#{status}, #{name}, #{categoryId}, #{price}, #{image}, #{description}, #{createTime}, #{updateTime},#{createUser}, #{updateUser}) </insert>DishFlavorMapper
@Mapper public interface DishFlavorMapper { /** * 批量插入口味数据 * @param flavors */ void insertBatch(List<DishFlavor> flavors); }DishFlavorMapper.xml
<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>菜品分页查询
需求分析和设计
代码开发
DishController
/** * 菜品分页查询 * @param dishPageQueryDTO * @return */ @GetMapping("/page") @ApiOperation("菜品分页查询") public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO){ log.info("菜品分页查询:{}", dishPageQueryDTO); PageResult pageResult = dishService.pageQuery(dishPageQueryDTO); return Result.success(pageResult); }DishService
/** * 菜品分页查询 * @param dishPageQueryDTO * @return */ PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO);DishServiceImpl
/** * 菜品分页查询 * @param dishPageQueryDTO * @return */ 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
DTO:接收前端传给后端的数据(入参) VO:后端返回给前端的数据(出参)
/** * 菜品分页查询 * @param dishPageQueryDTO * @return */ Page<DishVO> pageQuery(DishPageQueryDTO dishPageQueryDTO);DishMapper.xml
涉及多表查询,左连接
<select id="pageQuery" resultType="com.sky.vo.DishVO"> select d.*,c.name 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"> and d.category_id = #{categoryId} </if> <if test="status != null"> and d.status = #{status} </if> </where> order by d.create_time desc </select>删除菜品
需求分析和设计
接口
数据库
代码开发
注意这里有批量删除,一旦选中的菜品绑定套餐,全部的都无法删除
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 public void deleteBatch(List<Long> ids) { ids.forEach(id->{ 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); } //删除菜品表中的数据 ids.forEach(id -> { dishMapper.deleteById(id); //删除口味表中的数据 dishFlavorMapper.deleteByDishId(id); });DishMapper
/** * 根据主键查询菜品数据 * @param id * @return */ @Select("select * from dish where id = #{id}") Dish getById(Long id); /** * 根据主键删除菜品数 * @param id */ @Delete("delete from dish where id = #{id}") void deleteById(Long id);SetmealDishMapper
@Mapper public interface SetmealDishMapper { /**根据菜品id查询关联的套餐id * @param ids * @return */ List<Long> getSetmealIdsByDishIds(List<Long> ids); }SetmealDishMapper.xml
<select id="getSetmealIdsByDishIds" resultType="java.lang.Long"> select setmeal_id from setmeal_dish where dish_id in <foreach collection="ids" separator="," open="(" close=")" item="dishId"> #{dishId} </foreach> </select>DishFlavorMapper
/** * 根据菜品id删除口味数据 * @param dishId */ @Delete("delete from dish_flavor where dish_id = #{dishId}") void deleteByDishId(Long dishId);修改菜品
需求分析和设计
根据id查询菜品
修改菜品
代码开发
根据id查询菜品
DishController
/** * 根据id查询菜品和关联的口味数据 * @param id * @return */ @GetMapping("/{id}") @ApiOperation("根据id查询菜品和关联的口味数据") public Result<DishVO> getById(@PathVariable Long id){ return Result.success(dishService.getByIdWithFlavor(id)); }DishService
/** * 根据id查询菜品和关联的口味数据 * @param id * @return */ DishVO getByIdWithFlavor(Long id);DishServiceImpl
/** * 根据id查询菜品和关联的口味数据 * * @param id * @return */ public DishVO getByIdWithFlavor(Long id) { //查询菜品表 Dish dish = dishMapper.getById(id); //查询关联的口味 List<DishFlavor> dishFlavorList = dishFlavorMapper.getByDishId(id); //封装成VO DishVO dishVO = new DishVO(); BeanUtils.copyProperties(dish, dishVO); dishVO.setFlavors(dishFlavorList); return dishVO; }DishFlavorMapper
/** * 根据菜品id查询对应的口味 * @param dishId * @return */ @Select("select * from dish_flavor where dish_id = #{dishId}") List<DishFlavor> getByDishId(Long dishId);修改菜品
DishController
/** * 修改菜品 * @param dishDTO * @return */ @PutMapping @ApiOperation("修改菜品") public Result update(@RequestBody DishDTO dishDTO){ log.info("修改菜品:{}", dishDTO); dishService.updateWithFlavor(dishDTO); return Result.success(); }DishService
/** * 根据id修改菜品和关联的口味 * @param dishDTO */ void updateWithFlavor(DishDTO dishDTO);DishServiceImpl
/** * 根据id修改菜品和关联的口味 * * @param dishDTO */ @Transactional public void updateWithFlavor(DishDTO dishDTO) { Dish dish = new Dish(); BeanUtils.copyProperties(dishDTO, dish); //修改菜品表dish,执行update操作 dishMapper.update(dish); //删除当前菜品关联的口味数据,操作dish_flavor,执行delete操作 dishFlavorMapper.deleteByDishId(dishDTO.getId()); //插入最新的口味数据,操作dish_flavor,执行insert操作 List<DishFlavor> flavors = dishDTO.getFlavors(); if (flavors != null && flavors.size() > 0) { flavors.forEach(dishFlavor -> { dishFlavor.setDishId(dishDTO.getId()); }); dishFlavorMapper.insertBatch(flavors); } }DishMapper
/** * 根据主键修改菜品信息 * @param dish */ @AutoFill(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>菜品起售停售
需求分析和设计
代码开发
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
<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>