ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

小白勇闯《苍穹外卖》Day3

小白勇闯《苍穹外卖》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_timeupdate_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>
返回列表