外卖点单系统(瑞吉外卖)中的一些场景问题与解决方法

项目简介

本项目是一款针对餐饮用户商家设计的外卖点单系统,主要使用的技术栈用SpringBoot和MySQL。产品分为后台管理端和移动用户端。后台管理端主要用于维护菜品相关信息,例如填删菜品、查找菜品、填删套餐、查找套餐等等。移动用户端主要供用户浏览菜品、挑选菜品进购物车以及下单等。

场景问题与解决办法

静态资源映射

SpringBoot内置了Servlet容器,拥有默认的静态资源路径,对于不一样的路径,我们需要添加静态资源映射。

首先需要实现一个自定义配置类,继承WebMvcConfigurationSupport类,WebMvcConfigurationSupport是Spring MVC的核心配置。

WebMvcConfigurationSupport类的功能:主要用于执行WebMvc的各种配置,告诉Spring用于处理请求的有关工具,这些工具用于处理映射关系,解析参数,返回消息等等。一个完整的web请求和响应过程中需要执行的动作所使用的配置(程序、配置)基本都可以在WMS中进行管理

  1. 注册处理器映射,以便把请求导向具体的控制器、控制器方法、视图、资源。注意,这些映射器的目的是做一个关系匹配
  2. 注册处理器适配器,为具体的功能选择具体的处理程序。例如参数解析,消息转换等
  3. 注册异常处理器
  4. 注册路径映射解析有关类

WMS提供了通过Java代码配置WebMvc的方法,Spring拥有一个默认的子类WebMvcAutoConfiguration继承WMS来实现一些默认的MVC配置。如果想要自定义配置,可以通过以下解决方式解决:

  1. 对于一个使用@Configuration注解的配置类,添加@EnabledWebMvc注解,即可导入自定义的Mvc的Java配置。
  2. 继承WMS类,同时添加@Configuration注解,并重写对应的方法(对于重写有@Bean注解的方法同样需要添加@Bean注解)。
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
    /**
     * 设置静态资源映射
     */
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry){
        registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
        registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
        //addResourceHandler(String url)添加一个资源处理器ResourceHandlerRegistration,当遇到访问路径为url的请求时,映射到其他访问路径,
        //通过ResourceHandlerRegistration.addResourceLocations(url)为资源处理器中的访问路径添加映射路径url
    }
}

JS的id精度丢失

对于Long类型和Big Integer类型的数据(例如id),二进制中拥有64位的大小,JS内置有32位整数,而number类型的安全整数是53位,因此id等类型的数据传到前端时会发生精度丢失的问题。因此我们在把数据传给前端时,需要进行消息转换,把Long类型和BigInteger类型的数据转换成String类型的数据。可以通过配置Mvc来实现该消息转换。

首先是添加消息转换器,通过重写extendMessageConverters方法来向消息转换器列表converters中添加新的消息转换器对象:

@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
    @Override
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        log.info("扩展消息转换器...");
        //创建消息转换器对象
        MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
        //设置对象转换器,底层使用jackson将java对象转成json
        messageConverter.setObjectMapper(new JacksonObjectMapper());
        //将上面的消息转换器对象追加到MVC框架的转换器集合中
        converters.add(0,messageConverter);
    }
}

消息转换器对象的实现是通过继承Jackson提供的ObjectMapper类完成的:

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(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
                .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));

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

异常处理

项目中处理业务时会遇到各种异常,为了方便代码,减少耦合性,需要对异常进行统一的处理。
SpringBoot提供了全局异常处理来对业务中产生的各种异常进行统一的处理,此外也可以考虑使用Spring AOP来对业务异常进行处理。这里我们使用的是SpringBoot提供的全局异常处理。
@ControllerAdvice注解是Spring MVC提供的功能,在SpringBoot中可以直接使用,可以看作@Controller的加强版往往用于处理全局数据。例如全局异常处理、全局数据绑定、全局数据预处理等。相应的,需要和@ExceptionHandler、@ModelAttribute、@InitBinder搭配使用,完成各项功能。

  • 全局异常处理,处理业务中的各项异常,和@ExceptionHandler搭配使用
  • 全局数据绑定,可以用来做一些初始化的数据操作,用 @ModelAttribute 注解标记该方法的返回数据是一个全局数据,默认情况下,这个全局数据的 key 就是返回的变量名,value 就是方法返回值
  • 全局数据预处理,@ModelAttribute和@InitBinder搭配使用,实现对前端传来的数据的预处理,例如在Controller类的方法中使用@ModelAttribute(“a”)对方法的形参进行声明,会自动调用在 @ControllerAdvice 标记的类中使用@InitBinder(“a”)注解声明的方法来对该参数进行预处理。

首先自定义业务异常类,在业务中按需抛出业务异常

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

SpringBoot的全局异常处理

@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {
    //异常处理方法
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public R<String> exceptionHandler(SQLIntegrityConstraintViolationException exception){
        log.error(exception.getMessage());
        if(exception.getMessage().contains("Duplicate entry")){
            String[] exs = exception.getMessage().split(" ");
            String msg = exs[2] + "已存在!";
            return R.error(msg);
        }
        return R.error("失败!");
    }

    @ExceptionHandler(CustomException.class)
    public R<String> exceptionHandler(CustomException exception){
        log.error(exception.getMessage());
        return R.error(exception.getMessage());
    }
}

自动填充

在处理涉及到插入数据和更新数据的业务时,一个表的属性中往往有很多逻辑上相同的公共字段,对于它们的赋值与更新往往使用的是重复的代码,为了简化开发,可以利用MybatisPlus提供的自动填充功能。

MetaObjectHandler接口是MybatisPlus为我们提供的的一个扩展接口,我们可以利用这个接口在我们插入或者更新数据的时候,为一些字段指定默认值。(实现这个需求的方法不止一种,在sql层面也可以做到,在建表的时候也可以指定默认值。)

编写一个类实现该接口,同时重写insertFill()和updateFill()方法来实现公共字段的填充,使用@Component注解将该类交给Spring容器。

public class MyMetaObjectHandler implements MetaObjectHandler {
    @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());
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        long id = Thread.currentThread().getId();
//        log.info("线程id={}", id);
//        log.info("公共字段自动填充,Update");
        log.info(metaObject.toString());
        metaObject.setValue("updateTime", LocalDateTime.now());
        metaObject.setValue("updateUser", BaseContext.getCurrentId());
    }
}

在实体类中通过@TableField注解设置自动填充的公共字段

@Data
public class Employee implements Serializable {
    //... 其他属性
    @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;
}

分页查询

MybatisPlus提供了分页插件,可以把分页查询的结果封装到Page类里。通过设置一个分页插件的配置类(使用@Configuration注解),实现分页查询的功能。

@Configuration
public class MybatisPlusConfig {
   @Bean //告诉Spring容器下面这个方法可以得到一个Bean
   public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return mybatisPlusInterceptor;
    }
}

事务管理

当一个业务涉及到多个表的写操作时,需要将业务看作一整个事务来保证事务性原则。使用Spring提供的@Transactional注解可以实现对相应业务函数的事务的声明。同时在SpringBoot的启动类中需要使用@EnableTransactionManagement注解开启事务管理。

	@Transactional
    public void saveWithFlavor(DishDto dishDto) {
        //保存菜品到dish表中
        this.save(dishDto);
        Long dishId = dishDto.getId();
        List<DishFlavor> dishFlavors = dishDto.getFlavors();
        dishFlavors = dishFlavors.stream().map(item -> {
            item.setDishId(dishId);
            return item;
        }).collect(Collectors.toList());
        //保存菜品口味数据到口味表
        dishFlavorService.saveBatch(dishFlavors);
    }

过滤器

在一般的业务逻辑中,对于未登录的用户,只能访问登录页面,而不能访问主页。对此,可以使用过滤器或者拦截器实现。这里我们使用过滤器。

@ServletComponentScan是SpringBoot提供的注解之一,对启动类使用该注解后,Servlet、Filter、Listener可以直接通过@WebServlet、@WebFilter、@WebListener注解自动注册,无需其他代码。

使用@WebFilter注解实现了Filter接口的自定义过滤器类,重写doFilter方法,实现对请求路径的过滤:

@WebFilter(filterName = "loginCheckFilter", urlPatterns = "/*")
public class LoginCheckFilter implements Filter {
    //...
    @Override
    public void doFilter{
    	//...
    }
}

文件上传下载

在处理一些业务时,浏览器经常会传递给服务器一些图片,或者请求一些图片,涉及到文件的上传与下载。我们可以把文件的上传与下载封装到一个公共的Controller中,不涉及到具体的实体。上传文件时,通过MultipartFile类封装从前端传来的对象,之后通过Stream流将文件存储到服务器中。下载文件时,通过Stream流将文件写到response中

文件上传

    public R<String> upload(MultipartFile file){
        //file是一个临时文件,需要转存到指定位置
        //获得原始文件名
        String originalFilename = file.getOriginalFilename();
        String suffix = originalFilename.substring(originalFilename.lastIndexOf('.'));
        //使用UUID重新生成文件名,防止文件名重复造成文件覆盖
        String fileName = UUID.randomUUID() + 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);
    }

文件下载

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

    }
  • 0
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值