小白勇闯《苍穹外卖》Day3

📅 2026/8/7 9:26:25
小白勇闯《苍穹外卖》Day3
碎碎念开学马上大四了但是前几年一直没有好好学习专业课只是临近考试的时候突击复习一下导致到现在还是小白T_T 7月放暑假在家陆陆续续看了黑马的Java基础和web相关课程但是并没有完全看完也没有深入理解...眼看时间已经来不及了简历上还是一片空白决定硬着头皮学项目了干中学吧加油叠个甲只是发表一下学习记录内容不一定正确希望大家包容一下欢迎大佬们多多指正菜品管理前两天已经完成了环境搭建员工管理分类管理今天开始菜品管理了公共字段自动填充技术点枚举、注解、AOP、反射——————————————这里恶补了一下知识点————————————————枚举适合做信息分类和标志。这里用来标识操作类型insert/update注解自定义注解public i8nterface 注解名称{ public 属性类型 属性名() default 默认值 ; }特殊属性value在使用时如果只有一个value value名称可省略不写原理本质上是一个接口继承了annotation类里面定义的属性其实上是一个一个的抽象方法使用举例注解名(aaa李四,bbbtrue,ccc{java,python}) public void test(){ }元注解注解注解的注解RetentionRetentionPolicy.RUNTIME) Target({ElementType.METHOD}) public interface Test{ }Retention约束存活范围Target约束标记范围解析注释使用解析注解的方法AOP面向切面编程。通用公共功能从业务代码中剥离。AspectComponent定义切面类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---获取并配置AccessKeyapplication-dev.ymlalioss: endpoint: 你自己的 access-key-id: 你自己的 access-key-secret: 你自己的 bucket-name: 你自己的application.ymlalioss: 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}新建类OssConfigurationConfiguration 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()); } }新建类CommonControllerRestController RequestMapping(/admin/common) Slf4j Api(tags 通用接口) public class CommonController { Autowired private AliOssUtil aliOssUtil; /** * 文件上传 * param file * return */ PostMapping(/upload) ApiOperation(文件上传) public ResultString 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); } }测试一下可以正常上传新增菜品DishControllerRestController RequestMapping(/admin/dish) Api(tags 菜品相关接口) Slf4j public class DishController { Autowired private DishService dishService; /** * 新增菜品 * param dishDTO * return */ PostMapping ApiOperation(新增菜品) public ResultString save(RequestBody DishDTO dishDTO){ log.info(新增菜品{}, dishDTO); dishService.saveWithFlavor(dishDTO); return Result.success(); } }DishServicepublic interface DishService { /** * 新增菜品 * param dishDTO */ void saveWithFlavor(DishDTO dishDTO); }DishServiceImplService 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(); ListDishFlavor 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 表示获取主键值 keyPropertyid 表示将主键值赋给id属性-- insert idinsert useGeneratedKeystrue keyPropertyid 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}) /insertDishFlavorMapperMapper public interface DishFlavorMapper { /** * 批量插入口味数据 * param flavors */ void insertBatch(ListDishFlavor flavors); }DishFlavorMapper.xmlinsert idinsertBatch insert into dish_flavor(dish_id, name, value) values foreach collectionflavors itemdishFlavor separator, (#{dishFlavor.dishId},#{dishFlavor.name},#{dishFlavor.value}) /foreach /insert菜品分页查询需求分析和设计代码开发DishController/** * 菜品分页查询 * param dishPageQueryDTO * return */ GetMapping(/page) ApiOperation(菜品分页查询) public ResultPageResult 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()); PageDishVO page dishMapper.pageQuery(dishPageQueryDTO); return new PageResult(page.getTotal(), page.getResult()); }DishMapperDTO接收前端传给后端的数据入参 VO后端返回给前端的数据出参/** * 菜品分页查询 * param dishPageQueryDTO * return */ PageDishVO pageQuery(DishPageQueryDTO dishPageQueryDTO);DishMapper.xml涉及多表查询左连接select idpageQuery resultTypecom.sky.vo.DishVO select d.*,c.name categoryName from dish d left join category c on d.category_id c.id where if testname ! null and d.name like concat(%,#{name},%) /if if testcategoryId ! null and d.category_id #{categoryId} /if if teststatus ! null and d.status #{status} /if /where order by d.create_time desc /select删除菜品需求分析和设计接口数据库代码开发注意这里有批量删除一旦选中的菜品绑定套餐全部的都无法删除DishController/** * 品批量删除 * param ids * return */ DeleteMapping ApiOperation(菜品批量删除) public Result delete(RequestParam ListLong ids){ log.info(菜品批量删除{}, ids); dishService.deleteBatch(ids); return Result.success(); }DishServicevoid deleteBatch(ListLong ids);DishServiceImplTransactional public void deleteBatch(ListLong ids) { ids.forEach(id-{ Dish dish dishMapper.getById(id); //判断当前要删除的菜品状态是否为起售中 if(dish.getStatus() StatusConstant.ENABLE){ //如果是起售中抛出业务异常 throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE); } }); //判断当前要删除的菜品是否被套餐关联了 ListLong 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);SetmealDishMapperMapper public interface SetmealDishMapper { /**根据菜品id查询关联的套餐id * param ids * return */ ListLong getSetmealIdsByDishIds(ListLong ids); }SetmealDishMapper.xmlselect idgetSetmealIdsByDishIds resultTypejava.lang.Long select setmeal_id from setmeal_dish where dish_id in foreach collectionids separator, open( close) itemdishId #{dishId} /foreach /selectDishFlavorMapper/** * 根据菜品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 ResultDishVO 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); //查询关联的口味 ListDishFlavor 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}) ListDishFlavor 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操作 ListDishFlavor 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.xmlupdate idupdate update dish set if testname ! null name #{name}, /if if testcategoryId ! null category_id #{categoryId}, /if if testprice ! null price #{price}, /if if testimage ! null image #{image}, /if if testdescription ! null description #{description}, /if if teststatus ! null status #{status}, /if if testupdateTime ! null update_time #{updateTime}, /if if testupdateUser ! null update_user #{updateUser}, /if /set where id #{id} /update菜品起售停售需求分析和设计代码开发DishController/** * 菜品起售停售 * param status * param id * return */ PostMapping(/status/{status}) ApiOperation(菜品起售停售) public ResultString 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) { // 如果是停售操作还需要将包含当前菜品的套餐也停售 ListLong dishIds new ArrayList(); dishIds.add(id); // select setmeal_id from setmeal_dish where dish_id in (?,?,?) ListLong 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.xmlupdate idupdate parameterTypeSetmeal update setmeal set if testname ! null name #{name}, /if if testcategoryId ! null category_id #{categoryId}, /if if testprice ! null price #{price}, /if if teststatus ! null status #{status}, /if if testdescription ! null description #{description}, /if if testimage ! null image #{image}, /if if testupdateTime ! null update_time #{updateTime}, /if if testupdateUser ! null update_user #{updateUser} /if /set where id #{id} /update