最新Springboot+mp核心技术外卖入门实战项目(二)

编码阶段二 —— 菜品管理

这是对应的视频链接【黑马程序员Java项目实战《瑞吉外卖》,轻松掌握springboot + mybatis plus开发核心技术的真java实战项目-哔哩哔哩】 https://b23.tv/3nr8oMw

大家如果做了可以一起探讨一下,我在这实现了那些视频中没有完成的功能

此阶段重要掌握的是菜品管理,包括增删改查等,需要特别要关注的是数据的回显,文件上传下载功能显示图片。当然其中使用DTO来传输数据的方式得比较熟练掌握,以后到公司对于我们初级程序员就做的增删改查这些东西。另外我也在这一阶段有一个bug,就是关于动态代理的问题,因为我通过@Autowired自动注入service的实现类而不是接口的方式来产生bean,这就会导致spring底层不知道你是用哪种方式的动态代理而报错,大家在编码的时候记得养成良好的编码习惯。我会在下一篇博客中好好整理jdk动态代理和cglib代理的区别以及spring底层使用动态代理的原理,大家可以点个赞收藏关注一波,一起进步。

菜品分类实体类

@Data
public class Category implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;
    //类型 1 菜品分类 2 套餐分类
    private Integer type;
    //分类名称
    private String name;
    //顺序
    private Integer sort;
    //创建时间
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    //更新时间
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;

    //创建人
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;

    //修改人
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;

}

公共字段自动填充,例如修改时候的updateTime,updateUser等等,配置之后就可以使得带有注解的公共字段在插入或修改时进行自动填充,减少代码的冗余。

/**
 * 自定义元数据对象处理器
 * @author William
 * @create 2022-04-22 13:26
 */
@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {
    /**
    * 插入操作,自动填充
    *@Param [metaObject]
    *@Return
    */
    @Override
    public void insertFill(MetaObject metaObject) {
      log.info("公共字段自动填充[insert]...");
      log.info(metaObject.toString());
      metaObject.setValue("createTime", LocalDateTime.now());
      metaObject.setValue("updateTime", LocalDateTime.now());
      metaObject.setValue("createUser", BaseContext.getCurrentId());
      metaObject.setValue("updateUser", BaseContext.getCurrentId());

    }

    /**
    * 更新操作,自动填充
    *@Param [metaObject]
    *@Return
    */
    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("公共字段自动填充[update]...");
        log.info(metaObject.toString());

        long id = Thread.currentThread().getId();
        log.info("线程id为:{}", id);

        metaObject.setValue("updateTime", LocalDateTime.now());
        metaObject.setValue("updateUser", BaseContext.getCurrentId());
    }
}

菜品分类实体类

@Data
public class Category implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;
    //类型 1 菜品分类 2 套餐分类
    private Integer type;
    //分类名称
    private String name;
    //顺序
    private Integer sort;
    
    //创建时间
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    //更新时间
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    //创建人
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;
    //修改人
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;
}

菜品分类Service层

public interface ICategoryService extends IService<Category> {

    Page page(int page, int pageSize);//分页展示


    void remove(Long ids);
}

/**
 * @author William
 * @create 2022-04-22 14:54
 */
@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryDAO, Category> implements ICategoryService {

    @Autowired
    private IDishService dishService;

    @Autowired
    private ISetmealService setmealService;

    /**
    * 分页展示
    *@Param [page, pageSize]
    *@Return
    */
    @Override
    public Page page(int page, int pageSize) {
        Page<Category> pageInfo = new Page<>(page, pageSize);
        LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.orderByAsc(Category::getSort);
        return this.page(pageInfo, queryWrapper);
    }

    /**
    * 根据id删除分类,删除之前需要进行判断
    *@Param [ids]
    *@Return
    */
    @Override
    public void remove(Long ids) {
        LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
        //查询当前分类是否关联了菜品,如果已经关联,抛出一个业务异常
        dishLambdaQueryWrapper.eq(Dish::getCategoryId, ids);//因为前端传来的是ids
        int count1 = dishService.count(dishLambdaQueryWrapper);
        if(count1 > 0){
            throw new CustomException("当前分类下关联了菜品,不能删除");
        }

        //查询当前分类是否关联了套餐,如果已经关联,抛出一个业务异常
        LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
        setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId, ids);
        int count2 = setmealService.count(setmealLambdaQueryWrapper);
        if(count2 > 0){
            throw new CustomException("当前分类下关联了套餐,不能删除");
        }
        //正常删除分类
        this.removeById(ids);
    }
}

菜品分类Controller层

/**
 * @author William
 * @create 2022-04-22 14:56
 */
@RestController
@RequestMapping("/category")
@Slf4j
public class CategoryController {

    @Autowired
    private CategoryServiceImpl categoryService;

    @ApiOperation(value = "添加", notes = "新增分类")
    @PostMapping
    public R<String> save(@RequestBody Category category){
        log.info("category:{}", category);
        boolean save = categoryService.save(category);
        if(save){
            return R.success("新增分类成功");
        }
        return R.error("新增分类失败");
    }

    @ApiOperation(value = "分页", notes = "菜品分类分页")
    @GetMapping("/page")
    public R<Page> page(int page, int pageSize){
        Page pageInfo = categoryService.page(page, pageSize);
        return R.success(pageInfo);
    }

    @ApiOperation(value = "删除", notes = "根据id删除分类")
    @DeleteMapping
    public R<String> delete(Long ids){
        log.info("删除分类,ids为:{}", ids);
        categoryService.removeById(ids);
        return R.success("分类信息删除成功");
    }

    @ApiOperation(value = "修改", notes = "根据id修改分类信息")
    @PutMapping
    public R<String> update(@RequestBody Category category){
        log.info("修改分类信息:{}" , category);

        categoryService.updateById(category);
        return R.success("修改分类信息成功");
    }
}

文件上传下载

文件上传时,对页面的form表单有如下要求:

method="post" 采用post方式提交数据 ​ entype="multipart/form-data" 采用multipart格式上传文件 ​ type="file" 使用input的file控件上传

服务端要接收客户端上传的文件,通常会采用Apache的两个组件:

commons-fileupload commons-io

Spring框架在spring-web包中对文件上传进行了封装,大大简化了服务端代码,我们只需要在Controller的方法中声明一个MultipartFile类型的参数即可接收上传的文件。

文件下载,将文件从服务器传输到本地计算机的过程。通过浏览器进行文件下载,通常有两种表现形式:

以附件形式下载,弹出保存对话框,将文件保存到指定磁盘目录 直接在浏览器中打开

通过浏览器进行文件下载,本质上就是服务端将文件以流的形式写回浏览器的过程。

在yml文件中设置path

reggie:
  path: D:\img\

上传下载文件写common中的controller层

/**
 * @author William
 * @create 2022-04-22 17:32
 */
@RestController
@RequestMapping("/common")
@Slf4j
public class CommonController {

    @Value("${reggie.path}")
    private String basePath;

    @ApiOperation(value = "文件上传", notes = "文件上传")
    @PostMapping("/upload")
    public R<String> upload(MultipartFile file){
        //file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会删除
        log.info(file.toString());
        //原始文件名
        String originalFilename = file.getOriginalFilename();//abc.jpg
        String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
        //使用UUID重新生成文件名,防止文件名称重复造成文件覆盖
        String fileName = UUID.randomUUID().toString() + suffix;
        //创建一个目录对象
        File dir = new File(basePath);
        //判断当前目录是否存在
        if(!dir.exists()){
            dir.mkdirs();
        }
        try {
            file.transferTo(new File(basePath + fileName));
        } catch (IOException e) {
            e.printStackTrace();
        }
        return R.success(fileName);
    }
    @ApiOperation(value = "下载", notes = "文件下载")
    @GetMapping(value = "/download")
    public void download(String name, HttpServletResponse response){

        try {//通过输入流读取文件
            FileInputStream fileInputStream = new FileInputStream(new File(basePath + name));
            //通过输出流将文件写回浏览器,在浏览器展示图片
            ServletOutputStream outputStream = response.getOutputStream();
            response.setContentType("image/jpeg");

            int len = 0;
            byte[] bytes = new byte[1024];
            while((len = fileInputStream.read(bytes)) != -1){
                outputStream.write(bytes, 0, len);
                outputStream.flush();
            }
            //关闭资源
            outputStream.close();
            fileInputStream.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

新增菜品

新增菜品,其实就是将新增页面录入的菜品信息插入到dish表,如果添加了口味做法,还需要向dish_flavor表插入数据。涉及到dish和dish_flavor表

先在category中加入一个列表查询方法

List<Category> list(Category category);//根据条件查询分类数据

/**
    * 根据条件查询分类数据
    *@Param [category]
    *@Return
    */
    @Override
    public List<Category> list(Category category) {
        LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(category.getType() != null, Category::getType, category.getType());
        queryWrapper.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime);
        List<Category> list = this.list(queryWrapper);
        return list;
    }

@ApiOperation(value = "查询", notes = "根据条件查询分类数据")
    @GetMapping("/list")
    public R<List<Category>> list(Category category){
        List<Category> list = categoryService.list(category);
        return R.success(list);
    }

因为新增菜品时数据传输还需要有dish_flavor中没有的数据,所以我们需要使用DTO来进行数据传输。

DTO(Data Transfer Object):即数据传输对象,一般用于展示层与服务层之间的数据传输。

因为到这里业务就可能比较复杂,有时候需要操作两张表及以上,这时候我们实现类中就需要使用到事务的注解,另外想要开启事务得在启动类加

 @EnableTransactionManagement注解

@Data
public class DishDto extends Dish {

    private List<DishFlavor> flavors = new ArrayList<>();

    private String categoryName;

    private Integer copies;
}

菜品的接口

/**
 * @author William
 * @create 2022-04-22 16:04
 */
public interface IDishService extends IService<Dish> {

    //新增菜品,同时插入菜品对应的口味数据,需要操作dish和dish_flavor两张表
    void saveWithFlavor(DishDto dishDto);

    Page<DishDto> page(int page, int pageSize, String name);

    //根据id查询菜品信息和对应的口味信息
    DishDto getByIdWithFlavor(Long id);

    void updateWithFlavor(DishDto dishDto);
}
@RestController
@RequestMapping("/dish")
@Slf4j
public class DishController {

    @Autowired
    private IDishService dishService;

//    @Autowired
//    private DishFlavorServiceImpl dishFlavorService;

    @ApiOperation(value = "新增", notes = "新增菜品")
    @PostMapping
    public R<String> save(@RequestBody DishDto dishDto){
        log.info(dishDto.toString());
        dishService.saveWithFlavor(dishDto);
        return R.success("新增菜品成功");
    }
}
/**
    * 菜品信息分页查询
    *@Param [page, pageSize, name]
    *@Return
    */
    @Override
    public Page page(int page, int pageSize, String name) {
        Page<Dish> pageInfo = new Page<>(page, pageSize);
        Page<DishDto> dishDtoPage = new Page<>();
        LambdaQueryWrapper<Dish>  queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.like(name != null, Dish::getName, name);
        queryWrapper.orderByDesc(Dish::getUpdateTime);
        this.page(pageInfo, queryWrapper);
        //对象拷贝
        BeanUtils.copyProperties(pageInfo, dishDtoPage, "records");

        List<Dish> records = pageInfo.getRecords();

        List<DishDto> list = records.stream().map((item) -> {
            DishDto dishDto = new DishDto();
            BeanUtils.copyProperties(item, dishDto);

            Long categoryId = item.getCategoryId();//分类id
            //根据id查询分类对象
            Category category = categoryService.getById(categoryId);

            if(category != null){//防止空指针
                String categoryName = category.getName();
                dishDto.setCategoryName(categoryName);
            }
            return dishDto;
        }).collect(Collectors.toList());

        dishDtoPage.setRecords(list);

        return dishDtoPage;
    }

    /**
    * 根据id查询菜品信息和对应的口味信息
    *@Param [id]
    *@Return
    */
    @Override
    public DishDto getByIdWithFlavor(Long id) {
        Dish dish = this.getById(id);

        DishDto dishDto = new DishDto();
        BeanUtils.copyProperties(dish, dishDto);
        //查询当前菜品对应的口味信息,从fish_flavor表查询
        LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(DishFlavor::getDishId, dish.getId());
        List<DishFlavor> flavors = dishFlavorService.list(queryWrapper);
        dishDto.setFlavors(flavors);

        return dishDto;
    }

    /**
    *
    *@Param [dishDto]
    *@Return
    */
    @Override
    @Transactional
    public void updateWithFlavor(DishDto dishDto) {
        //更新dish表基本信息
        this.updateById(dishDto);
        
        //清理当前菜品对应口味数据——dish_flavor表的delete操作
        LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(DishFlavor::getDishId, dishDto.getId());
        dishFlavorService.remove(queryWrapper);
        //插入当前提交过来的口味数据——dish_flavor表的insert操作
        List<DishFlavor> flavors = dishDto.getFlavors();
        flavors =  flavors.stream().map((item) -> {
            item.setDishId(dishDto.getId());
            return item;
        }).collect(Collectors.toList());

        dishFlavorService.saveBatch(flavors);
    }

菜品controller层部分代码

@ApiOperation(value = "分页", notes = "菜品信息分页查询")
    @GetMapping(value = "/page")
    public R<Page> page(int page, int pageSize, String name){
        Page<DishDto> dishDtoPage = dishService.page(page, pageSize, name);
        return R.success(dishDtoPage);
    }

    @ApiOperation(value = "查询", notes = "根据id查询菜品信息和对应的口味信息")
    @GetMapping(value = "/{id}")
    public R<DishDto> get(@PathVariable Long id){
        DishDto dishDto = dishService.getByIdWithFlavor(id);
        return R.success(dishDto);
    }

    @ApiOperation(value = "修改", notes = "修改菜品")
    @PutMapping
    public R<String> update(@RequestBody DishDto dishDto){
        log.info(dishDto.toString());
        dishService.updateWithFlavor(dishDto);
        return R.success("更新菜品成功");
    }

在我这阶段编写的时候报了一个动态代理的bug,因为我在服务层和controller层都是用自动注入service的实现类而不是service的接口,这里我们就可以多去了解一下Spring中的动态代理。注意前面不要用实现类,这样会导致spring报动态代理的异常

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
主从分离是一种常见的数据库优化策略,在高并发场景下可以提升系统的性能和可用性。下面是使用 Spring Boot、Mybatis Plus 和 Druid 实现主从分离的步骤: 1. 引入依赖 在 pom.xml 文件中引入以下依赖: ```xml <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.0</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.6</version> </dependency> ``` 2. 配置数据源 在 application.yml 文件中配置主从数据源的信息: ```yaml spring: datasource: master: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/master_db?useUnicode=true&amp;characterEncoding=UTF-8&amp;autoReconnect=true&amp;useSSL=false&amp;zeroDateTimeBehavior=convertToNull&amp;allowMultiQueries=true username: root password: root slave: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3307/slave_db?useUnicode=true&amp;characterEncoding=UTF-8&amp;autoReconnect=true&amp;useSSL=false&amp;zeroDateTimeBehavior=convertToNull&amp;allowMultiQueries=true username: root password: root ``` 其中,master 和 slave 分别代表主库和从库的配置信息。 3. 配置 Druid 数据源 在 application.yml 文件中添加 Druid 数据源的配置: ```yaml spring: datasource: type: com.alibaba.druid.pool.DruidDataSource druid: initial-size: 5 min-idle: 5 max-active: 20 max-wait: 60000 time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 300000 validation-query: SELECT 1 FROM DUAL test-while-idle: true test-on-borrow: false test-on-return: false pool-prepared-statements: true max-pool-prepared-statement-per-connection-size: 20 filters: stat,wall,log4j connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 ``` 4. 配置 Mybatis Plus 在 application.yml 文件中添加 Mybatis Plus 的配置: ```yaml mybatis-plus: mapper-locations: classpath:mapper/**/*.xml global-config: db-config: id-type: auto field-strategy: not_null table-prefix: mp_ configuration: map-underscore-to-camel-case: true cache-enabled: false ``` 其中,mapper-locations 配置了 Mybatis Plus 的 Mapper 文件路径,global-config 配置了一些全局的配置,configuration 配置了 Mybatis 的一些参数。 5. 实现动态数据源 创建 DynamicDataSource 类,继承 AbstractRoutingDataSource,并实现 determineCurrentLookupKey() 方法: ```java public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DataSourceContextHolder.get(); } } ``` 其中,DataSourceContextHolder 是一个线程安全的类,用于存储当前线程使用的数据源类型。 6. 配置数据源路由 创建 DataSourceConfiguration 类,配置数据源路由: ```java @Configuration public class DataSourceConfiguration { @Bean public DataSource masterDataSource() { DruidDataSource dataSource = new DruidDataSource(); return dataSource; } @Bean public DataSource slaveDataSource() { DruidDataSource dataSource = new DruidDataSource(); return dataSource; } @Bean public DynamicDataSource dynamicDataSource() { DynamicDataSource dataSource = new DynamicDataSource(); Map<Object, Object> dataSourceMap = new HashMap<>(2); dataSourceMap.put("master", masterDataSource()); dataSourceMap.put("slave", slaveDataSource()); dataSource.setTargetDataSources(dataSourceMap); dataSource.setDefaultTargetDataSource(masterDataSource()); return dataSource; } @Bean public SqlSessionFactory sqlSessionFactory() throws Exception { MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dynamicDataSource()); ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); try { sqlSessionFactoryBean.setMapperLocations(resolver.getResources("classpath:mapper/**/*.xml")); } catch (IOException e) { throw new RuntimeException(e); } return sqlSessionFactoryBean.getObject(); } } ``` 其中,masterDataSource() 和 slaveDataSource() 分别返回主库和从库的 Druid 数据源,dynamicDataSource() 返回动态数据源,sqlSessionFactory() 配置了 Mybatis 的 SqlSessionFactory。 7. 实现数据源切换 创建 DataSourceContextHolder 类,用于在当前线程上存储数据源的类型: ```java public class DataSourceContextHolder { private static final ThreadLocal<String> contextHolder = new ThreadLocal<>(); public static void set(String dataSourceType) { contextHolder.set(dataSourceType); } public static String get() { return contextHolder.get(); } public static void clear() { contextHolder.remove(); } } ``` 创建 DataSourceAspect 类,实现切面拦截: ```java @Aspect @Component public class DataSourceAspect { @Before("@annotation(com.example.demo.annotation.Master) ") public void setMasterDataSource() { DataSourceContextHolder.set("master"); } @Before("@annotation(com.example.demo.annotation.Slave) ") public void setSlaveDataSource() { DataSourceContextHolder.set("slave"); } @After("@annotation(com.example.demo.annotation.Master) || @annotation(com.example.demo.annotation.Slave)") public void clearDataSource() { DataSourceContextHolder.clear(); } } ``` 其中,@Master 和 @Slave 是自定义的注解,用于标记使用主库和从库。 8. 实现主从分离 在 Mybatis Plus 的 Mapper 接口中,使用 @Master 和 @Slave 注解标记方法,实现主从分离: ```java public interface UserMapper extends BaseMapper<User> { @Master User selectById(Integer id); @Slave List<User> selectAll(); } ``` 其中,selectById() 方法使用主库,selectAll() 方法使用从库。 9. 测试主从分离 使用 JUnit 进行测试: ```java @SpringBootTest class DemoApplicationTests { @Autowired private UserMapper userMapper; @Test void contextLoads() { User user = userMapper.selectById(1); System.out.println(user.getName()); } @Test void testSelectAll() { List<User> userList = userMapper.selectAll(); userList.forEach(user -> System.out.println(user.getName())); } } ``` 其中,contextLoads() 方法测试使用主库,testSelectAll() 方法测试使用从库。 以上就是使用 Spring Boot、Mybatis Plus 和 Druid 实现主从分离的步骤。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值