(慎点/1w字+警告/刚入坑必看请自带水杯)后端入门玩家的第一个项目保姆级笔记包教包会

目前学习了项目的后端功能开发,针对前段时间的学习进行系统总结提升,根据项目开发流程总结

1. 资料中所给的前端界面是存放在/backend和/front之中,而springboot自带的是static,故需要做一层映射才可以访问到

public class webMvcConfig extends WebMvcConfigurationSupport {
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        log.info("开始进行静态资源映射");
//registry.addResourceHandler("通过前端访问的路径").addResourceLocations("需要映射的资源的位置");
        registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
        registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
    }

2. 为了保证前后端数据统一性,避免不同方法返回值之间的类型出现较大差异,创建了一个新的类R,思路就是,遇到困难就加一层,加一层解决不了就再加一层(后面的dto思路和此相似)

//使用lombok简化开发
@Data
public class R<T> {
    //编码:1成功,0和其它数字为失败
    private Integer code;
    //错误信息
    private String msg;
    //数据
    private T data;
    //动态数据
    private Map map = new HashMap(); 
    //返回成功信息
    public static <T> R<T> success(T object) {
        R<T> r = new R<T>();
        r.data = object;
        r.code = 1;
        return r;
    }
    //返回报错信息
    public static <T> R<T> error(String msg) {
        R r = new R();
        r.msg = msg;
        r.code = 0;
        return r;
    }
    //添加动态数据
    public R<T> add(String key, Object value) {
        this.map.put(key, value);
        return this;
    }

3. 登录界面开发

需求分析:前端传入数据之后,需要后端去数据库查询信息来判断该用户是否注册,并给与其一个唯一的sessionID用于标记用户已经登录,同时需要一个过滤器来判断用户是否登录,防止未登录用户进行敏感操作。

开发思路:常规三层,此处使用的是springboot + mybatis-plus开发

mapper和service层使用框架即可。

controller层:使用rest风格进行开发

@PostMapping("/login")
    //@RequestBody的作用是将前端的json对象传入函数
    public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee){
        String password = employee.getPassword();
        //md5加密
        password = DigestUtils.md5DigestAsHex(password.getBytes());
        //条件查询标准格式,使用的是LambdaQueryWrapper类
        LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
        //声明是以什么作为条件来查询
        queryWrapper.eq(Employee::getUsername,employee.getUsername());
        //从下一层取数据
        Employee emp = employeeService.getOne(queryWrapper);
        //进行一系列判断
        if(emp == null){
            return R.error("账号不存在");
        }

        if(!emp.getPassword().equals(password)){
            return R.error("密码错误");
        }
        //判断标志位
        if(emp.getStatus() == 0){
            return R.error("您的账号已被封号");
        }
        //把id放入session
        request.getSession().setAttribute("employee",emp.getId());
        //此处是因为后面测试时发现ThreadLocal获取到的id是null,故在此做出调整
        BaseContext.setCurrentId(emp.getId());
        return R.success(emp);
    }

    @PostMapping("/logout")
    public R<String> logout(HttpServletRequest request){
        //清理Session中保存的当前登录员工的id
        request.getSession().removeAttribute("employee");
        return R.success("退出成功");
    }

4.员工操作:

新增操作:-->调用自带的插入操作即可

@PostMapping
    public R<String> save(HttpServletRequest request,@RequestBody Employee employee){

        //设置初始密码123456,需要进行md5加密处理
        //DigestUtils.md5DigestAsHex使用该类进行md5加密
        employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));

        employeeService.save(employee);

        return R.success("新增员工成功");
    }

分页操作:登录进去过后发现前端发出了一个/page?page=xxx&pageSize=xxx,说白了就是select *操作加个limit限制查询条数,条件查询,可能有条件,故还要加个参数,有就调用没有就摆着

@GetMapping("/page")
    //返回对象是个page,是在com.baomidou.mybatisplus.extension.plugins.pagination.Page包下
    public R<Page> page(int page,int pageSize,String name){
        Page pageInfo = new Page<>(page,pageSize);
        LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
        //like模糊查询
        queryWrapper.like(!StringUtils.isEmpty(name),Employee::getName,name);
        //排序条件
        queryWrapper.orderByDesc(Employee::getUpdateTime);
        //去数据库查然后把结果放到pageInfo最后返回出来
        employeeService.page(pageInfo,queryWrapper);
        return R.success(pageInfo);
    }

使用mp的分页查询还需配置一个mp分页拦截器

@Configuration
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return mybatisPlusInterceptor;
    }
}

更新操作:(禁用其实也是更新操作,只是修改了状态位)

@PutMapping
    public R<String> update(HttpServletRequest request,@RequestBody Employee employee){
       
        employeeService.updateById(employee);

        return R.success("员工信息修改成功");
    }

5. 对象映射器:java和json对象之间的转换(直接拿来用就可以了)

public class JacksonObjectMapper extends ObjectMapper {

    public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
    public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
    public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";

    public JacksonObjectMapper() {
        super();
        //收到未知属性时不报异常
        this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);

        //反序列化时,属性不存在的兼容处理
        this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);


        SimpleModule simpleModule = new SimpleModule()
                .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))

                .addSerializer(BigInteger.class, ToStringSerializer.instance)
                .addSerializer(Long.class, ToStringSerializer.instance)
                .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));

        //注册功能模块 例如,可以添加自定义序列化器和反序列化器
        this.registerModule(simpleModule);
    }
}

6.公共字段自动填充:

需求分析:不同实体都有4个公共字段,createTime,createUser,updateTime,updateUser,在实现其方法时如果每个方法都要手动去添加就太过于麻烦,于是使用mp提供的自动填充,去实现MetaObjectHandler接口,在实体类中需要自动填充的属性上加注解@TableField(在调用数据库相关什么方法的时候自动填充)

@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {
    //插入时自动填充
    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("公共字段自动填充【insert】。。。");
        log.info(metaObject.toString());
        metaObject.setValue("createTime", LocalDateTime.now());
       // log.info("createTime=" + LocalDateTime.now());
        metaObject.setValue("updateTime", LocalDateTime.now());
      //  log.info("updateTime=" + LocalDateTime.now());
        metaObject.setValue("createUser",BaseContext.getCurrentId());
       // log.info("createUser=" + BaseContext.getCurrentId());
        metaObject.setValue("updateUser",BaseContext.getCurrentId());
      //  log.info("updateUser=" + BaseContext.getCurrentId());
    }
    //更新时自动填充
    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("公共字段自动填充【update】。。。");
        log.info(metaObject.toString());

        metaObject.setValue("updateTime",LocalDateTime.now());
       // log.info("updateTime" + LocalDateTime.now());
        metaObject.setValue("updateUser",BaseContext.getCurrentId());
       // log.info("updateUser=" + BaseContext.getCurrentId());
    }
}

7.全局异常处理

需求分析:要返回一个客户看得懂的报错,就需要我们自己定制一个

public class GlobalExceptionHandler {
//    处理和数据库相关的报错
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
        log.error(ex.getMessage());
        if(ex.getMessage().contains("Duplicate entry")){
            String[] split = ex.getMessage().split(" ");
            String msg = split[2] + "已存在";
            return R.error(msg);
        }
        return R.error("未知错误");
    }
//处理用户层的异常
    @ExceptionHandler()
    public R<String> exceptionHandler(CustomException ex){
        log.error(ex.getMessage());
        return R.error(ex.getMessage());
    }
}

 自定义的用户异常类

public class CustomException extends RuntimeException {
    public CustomException(String message){
        super(message);
    }
}

8.分类管理:

需求分析:和员工类似,需要实现基础curd操作,菜品分类和和套餐分类其实也是状态码不同,这些均是在前端的请求中实现,后端只用开发更新操作即可。除删除操作外其他操作与上面思路一致。

删除操作需要注意到的点是,若需要删除的分类下绑定了菜品or套餐的话那应该不可删除(不然会出现某菜品绑定的是已经删除的分类),需要去查询菜品和套餐的表确认当前分类下的菜品和套餐为空

需要我们手动实现删除方法(是特化的方法无法使用mp自带的删除),其底层逻辑就是通过两次条件查询确认该分类没有关联菜品和套餐

public void remove(Long id) {
        log.info("id=" + id);
        LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
        //添加查询条件,根据分类id进行查询
        dishLambdaQueryWrapper.eq(Dish::getCategoryId, id);
        long count1 = dishService.count(dishLambdaQueryWrapper);
        //查询当前分类是否关联了套餐,如果已经关联,抛出一个业务异常
        LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
        //添加查询条件,根据分类id进行查询
        setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId, id);
        long count2 =  setmealService.count(setmealLambdaQueryWrapper);
        //查询当前分类是否关联了菜品,如果已经关联,抛出一个业务异常
        if (count1 > 0) {
            //已经关联菜品,抛出一个业务异常
            throw new CustomException("当前分类下关联了菜品,不能删除");
        }

        if (count2 > 0) {
            //已经关联套餐,抛出一个业务异常
            throw new CustomException("当前分类下关联了套餐,不能删除");
        }
            //正常删除分类
            super.removeById(id);

    }

 9.菜品管理

需求分析:通过页面可以看到我们需要实现的方法,针对单个菜品和多个菜品的编辑,停售和删除,新增单个菜品。

此处接触到第一个dto类,目的是补全新增操作中需要的分类,口味列表(遇到问题就加一层)

DTO:用于表现层和应用层之间的数据交互,简单来说Model面向业务,我们是通过业务来定义Model的。而DTO是面向界面UI,是通过UI的需求来定义的。 通过DTO我们实现了表现层与Model之间的解耦,表现层不引用Model

通过新增页面可以发现主要有两个点,菜品分类采用的是下拉框,说明我们需要在分类的controller内实现查询功能,得到分类的列表。

@GetMapping("/list")
    public R<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 = categoryService.list();
        return R.success(list);
    }

发现存在图片上传的功能,还有,页面上的图片显示是将图片下回浏览器实现(调用download方法),此处直接给出代码

public class CommonController {
    @Value("${waimai.path}")
    private String basePath;


    @PostMapping("/upload")
    public R<String> upload(MultipartFile file) throws IOException {
        //拿到文件后缀名,方便下一步的拼接
        String suffix = file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf("."));
        //利用生成的随机的UUID将图片重新命名,防止出现两张名字相同的图片而导致报错
        String fileName = UUID.randomUUID().toString() + suffix;
        //校验图片存放路径是否存在,不存在就重新创建一个
        File dir = new File(basePath);
        if(!dir.exists()){
            dir.mkdirs();
        }
//        将图片转存到指定路径
        file.transferTo(new File(basePath + fileName));
        return R.success(fileName);
    }

    @GetMapping("/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();
        }
    }
}

菜品口味的选择,查询后返回一个dto对象

public DishDto getByIdWithFlavor(Long id) {
        //查询菜品基本信息,从dish表查询
        Dish dish = this.getById(id);

        DishDto dishDto = new DishDto();
        BeanUtils.copyProperties(dish,dishDto);

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

        return dishDto;
    }

新增操作:

@Transactional
    public void saveWithFlavor(DishDto dishDto) {
        //保存菜品的基本信息到菜品表dish
        this.save(dishDto);

        Long dishId = dishDto.getId();//菜品id

        //菜品口味
        List<DishFlavor> flavors = dishDto.getFlavors();
        flavors = flavors.stream().map((item) -> {
            item.setDishId(dishId);
            return item;
        }).collect(Collectors.toList());

        //保存菜品口味数据到菜品口味表dish_flavor
        dishFlavorService.saveBatch(flavors);

    }

更新操作:

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);
    }

批量停售和删除操作:前端多选时相当于传入id数组

//停售起售菜品
    @PostMapping("/status/{status}")
    public R<String> sale(@PathVariable int status,
                          String[] ids){
        for(String id: ids){
            Dish dish = dishService.getById(id);
            dish.setStatus(status);
            dishService.updateById(dish);
        }
        return R.success("修改成功");
    }
    //删除菜品
    @DeleteMapping
    public R<String> delete(String[] ids){
        for (String id:ids) {
            dishService.removeById(id);
        }
        return R.success("删除成功");
    }

套餐管理:

需求分析:也是需要实现套餐的curd,和菜品管理相似。

整个后台界面开发就基本完成

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值