瑞吉整理

189 阅读10分钟

1.前端静态资源导入

1.存放:前端静态资源存放的一个位置

image.png

2.静态资源映射

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

@Slf4j
@Configuration
public class aextends WebMvcConfigurationSupport {
    /**
     * 设置静态资源映射
     * @param registry
     */
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        // super.addResourceHandlers(registry);
        // 前面的地址是指请求内容
        // 后面的地址是指映射到具体的静态文件
        log.info("静态资源映射");
        registry.addResourceHandler("/backend/**").addResourceLocations("classpath:static/backend/");
        registry.addResourceHandler("/front/**").addResourceLocations("classpath:static/front/");
    }
}
   

2.mybtis-plus基本使用

1.实体类Employee
2.实体类对应的mapper

@Mapper
public interface EmployeeMapper extends BaseMapper<Employee>{
}

3.service接口

public interface EmployeeService extends IService<Employee> {
}

4.service实现类
service的实现绑的是mapper和实体类并实现实体类的service接口

@Service
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper,Employee> implements EmployeeService{
}

5.写controller(登录、登出)

@Slf4j
@RestController
@RequestMapping("/employee")
public class EmployeeController {

    @Autowired
    private EmployeeService employeeService;

    @PostMapping("/login")
    public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee){

        //1、将页面提交的密码password进行md5加密处理
        String password = employee.getPassword();
        password = DigestUtils.md5DigestAsHex(password.getBytes());

        //2、根据页面提交的用户名username查询数据库
        LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Employee::getUsername,employee.getUsername());
        Employee emp = employeeService.getOne(queryWrapper);

        //3、如果没有查询到则返回登录失败结果
        if(emp == null){
            return R.error("登录失败");
        }

        //4、密码比对,如果不一致则返回登录失败结果
        if(!emp.getPassword().equals(password)){
            return R.error("登录失败");
        }

        //5、查看员工状态,如果为已禁用状态,则返回员工已禁用结果
        if(emp.getStatus() == 0){
            return R.error("账号已禁用");
        }

        //6、登录成功,将员工id存入Session并返回登录成功结果
        request.getSession().setAttribute("employee",emp.getId());
        return R.success(emp);
    }
    @PostMapping("/logout")
    public R<String> logout(HttpServletRequest request){
        //清理Session中保存的当前登录员工的id
        request.getSession().removeAttribute("employee");
        return R.success("退出成功");
    }

	
}

3.通用结果类

服务端响应的所有结果最终都会包装成此种类型返回给前端页面。

@Data
public class R<T> {
    private  Integer code; //1成功0失败
    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;
    }
}

4.登录拦截(过滤器)

获取本次请求的URI,判断本次请求是否需要登录,才可以访问。如果不需要登录或者已登录,直接放行;未登陆,返回未登录结果。
"/backend/**"/front/**"这个其实指的是从前端静态资源直接访问这个是不需要拦截的。

@Slf4j
@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
public class LoginCheckFilter implements Filter {
    //路径匹配器
    public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        //获取本次请求的url
        String requestURI = request.getRequestURI();
        log.info("拦截到请求:{}",requestURI);
        //定义不需要处理的请求路径
        String[] urls = new String[]{
                "/employee/login",
                "employee/logout",
                "/backend/**",
                "/front/**"
        };

        //判断本次请求是否需要处理
        boolean check = check(urls,requestURI);
        if(check){
            log.info("本次请求{}不需要处理",requestURI);
            filterChain.doFilter(request,response);
            return;
        }
        //判断登陆状态,如果已登陆,则直接放行
        if(request.getSession().getAttribute("employee")!=null){
            log.info("用户已登陆,用户id为:{}",request.getSession().getAttribute("employee"));

            //将当前登陆用户id存到ThreadLocal
            Long empId = (Long) request.getSession().getAttribute("employee");
            BaseContext.setCurrentId(empId);

            //放行
            filterChain.doFilter(request,response);
            return;
        }
        log.info("用户未登陆");
        response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
        return;
    }
    /**
    * @Description: 路径匹配,检查本次请求是否需要放行
    * @Param: [urls, requestURI]
    * @return: boolean
    * @Author: liyunzhi
    * @Date: 2022/6/23
    */
    public boolean check(String[] urls,String requestURI){
        for(String url : urls){
            boolean match = PATH_MATCHER.match(url,requestURI);
            if(match){
                return true;
            }
        }
        return false;
    }
}

注意: 需要在引导类上加上Servlet组件扫描(@ServletComponentScan)的注解,来扫描@WebFilter、@WebServlet、@WebListener等注解。

5.mybatis-plus公共字段标明及处理

1.用 @TableField标明(填充策略)

//字段默认填充策略
@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;

2.填充处理

@Component
@Slf4j
public class MyMetaObjecthandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("公共字段自动填充[insert]...");
        log.info(metaObject.toString());
        //createTime字段填充localdatetime.now
        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) {
        log.info("公共字段自动填充[update]...");
        log.info(metaObject.toString());

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

6.自定义异常与全局异常处理

1.自定义异常
RuntimeException属于非受控异常,系统可以处理也可不处理;受控异常则是必须处理的异常

public class CustomException extends RuntimeException{
    //有参构造
    public CustomException(String message) {
        super(message);
    }
}

2.全局异常处理
在异常处理器上加上注解@ControllerAdvice可通过annotations指定拦截哪一类的Controller方法。并在异常处理器的方法上加上注解@ExceptionHandller来指定那一类型的异常。
@ResponseBody将方法R的返回值转成json格式的数据响应给页面。
@RestControllerAdvice = @ControllerAdvice + @ResponseBody

@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j
public class GlobalExpectionHandler {
    /**
    * @Description: SQLIntegrityConstraintViolationException异常
    * @Description: 异常处理方法
    * @return: com.itheima.reggie.common.R<java.lang.String>
    * @Author: liyunzhi
    * @Date: 2022/6/24
    */
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
        log.error(ex.getMessage());
        //判断异常是否为Duplicate entry,然后拿出重复的字段
        if(ex.getMessage().contains("Duplicate entry")){
            String[] split = ex.getMessage().split(" ");
            String msg = split[2] + "已存在";
            return R.error(msg);
        }
        return R.error("未知错误");
    }

    /**
     * @Description: 处理自定义异常CustomException异常
     * @return: com.itheima.reggie.common.R<java.lang.String>
     * @Author: liyunzhi
     * @Date: 2022/6/24
     */
    @ExceptionHandler(CustomException.class)
    public R<String> exceptionHandler(CustomException ex){
        log.error(ex.getMessage());
        return R.error(ex.getMessage());
    }
}

7.分页配置和分页查询

1.MP分页拦截器

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

2.(基本)分页查询
(1)page对象
(2)条件构造器与过滤条件
(3)显示页面xxxService.page
(4)返回结果R.success(pageInfo)

@GetMapping("/page")
public R<Page> page(int page, int pageSize, String name){

    Page pageInfo = new Page(page,pageSize);
    LambdaQueryWrapper<Employee> qw = new LambdaQueryWrapper();
    //过滤条件
    qw.like(!StringUtils.isEmpty(name), Employee::getName, name);
    //添加排序条件
    qw.orderByDesc(Employee::getUpdateTime);
    employeeService.page(pageInfo,qw);
    return R.success(pageInfo);
}

8.消息转换器

用处举例:查数据库时,Long类型较长数据会出现精度问题,导致查询不到结果,此时就需要将该类型数据转成String类型去处理。
该自定义消息转换器主要指定了在进行json数据序列化及反序列化时,LocalDateTime、LocalData、LocalTime的处理方式,以及BigInteger及Lang类型数据,直接转换为字符串。

/**
 * 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
 * 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
 * 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
 */
// 继承自jackson,JSON<——>java对象,互转
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)
                //序列化器
                // 对Long数据进行处理,将Long型的数据转成String类型的数据后再反馈给前端
                .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);
    }
}

在WebConfig中重写方法extendMessageConverters

@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
    // 项目启动的时候就会调用
    log.info("扩展消息转换器");
    // 创建消息转换器对象
    MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
    // 设置对象转换器,底层使用Jackson将Java对象转为json
    messageConverter.setObjectMapper(new JacksonObjectMapper());
    // 将上面的消息转换器对象追加到mvc框架的转换器集合中,注意索引设置成0,优先使用
    converters.add(0,messageConverter);
}

9.自动填充时的登陆id获取

1.编写基于ThreadLocal封装的工具类BaseContext(用于保存和获取当前登录用户的ID)

public class BaseContext {
    private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    /**
    * @Description: 设置值
    * @Param: [id]
    * @return: void
    * @Author: liyunzhi
    * @Date: 2022/6/24
    */
    public static void setCurrentId(Long id){
        threadLocal.set(id);
    }
    /**
    * @Description: 获取值
    * @Param: []
    * @return: java.lang.Long
    * @Author: liyunzhi
    * @Date: 2022/6/24
    */
    public static Long getCurrentId(){
        return threadLocal.get();
    }
}

2.在登录过滤器中存放当前登陆用户的ThreadLocal,在doFilter方法中,判定用户是否登录,如果登录,在放行之前获取HttpSession中的登录用户信息,调用BaseContext的setCurrentId方法将当前登录用户ID存入ThreadLocal。

//将当前登陆用户id存到ThreadLocal
Long empId = (Long) request.getSession().getAttribute("employee");
BaseContext.setCurrentId(empId);
  1. 从ThreadLocal中获取当前登录用户的ID BaseContext.getCurrentId();

10.文件上传下载(后端)

1.文件上传:指将本地图片、视频、音频等文件上传到服务器,供其他用户浏览或下载的过程。

image.png

image.png 要求后端接收参数是MultipartFile file,用来接收前端上传的文件,类型是MultipartFile,方法的形参名称需要与页面的file域的name一致。
上传逻辑:
(1)获取原始文件名,通过原始文件名获取文件后缀。
(2)通过UUID重新声明文件名,文件名称重复造成文件覆盖。
(3)创建文件存放目录
(4)将上传的临时文件转存到指定位置 2.文件下载,具体实现的是上传的文件在浏览器中那个框里显示
* 本质是服务器将文件以流的形式写回浏览器的过程(直接在浏览器打开)
* 形式一:以附件形式下载,弹出保存对话框,将文件保存到指定磁盘目录
* 形式二:直接在浏览器中打开
下载逻辑:
(1)定义输入流,通过输入流读取文件内容
(2)通过response对象,获取到输入流
(3)通过response对象设置响应数据格式(image/jpeg)
(4)通过输入流读取文件数据,然后通过上述的输出流写回浏览器
(5)关闭资源

@Slf4j
@RestController
@RequestMapping("/common")
public class CommonController {

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

    /**
    * @Description: 采用file.TransferTo来保存上传的文件
    * @Param: [file]
    * @return: com.itheima.reggie.common.R<java.lang.String>
    * @Author: liyunzhi
    * @Date: 2022/6/25
    */
    @PostMapping("/upload")
    public R<String> upload(MultipartFile 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);
        System.out.println(dir.getAbsolutePath());
        //判断这个目录对象是否存在
        if(!dir.exists()){
            dir.mkdirs();
        }
        //System.out.println("上传文件保存地址:"+dir);
        try {
            //将临时文件存到指定位置
            //file.transferTo(new File(basePath+fileName));
            file.transferTo(new File(dir.getAbsolutePath(),fileName));
        }catch (IOException e){
            e.printStackTrace();
        }
        //return R.success("上传成功");------------注意这里
        return R.success(fileName);
    }
    /**
    * @Description: 下载,具体实现的是上传的文件在浏览器中那个框里显示
     * 本质是服务器将文件以流的形式写回浏览器的过程(直接在浏览器打开)
     * 形式一:以附件形式下载,弹出保存对话框,将文件保存到指定磁盘目录
     * 形式二:直接在浏览器中打开
    * @Param: [name, response]
    * @return: void
    * @Author: liyunzhi
    * @Date: 2022/6/25
    */
    @GetMapping("/download")
    public void download(String name, HttpServletResponse response){
        try {
            //读取输入流文件内容
            FileInputStream fileInputStream = new FileInputStream(new File(basePath+name));
            //输出流,通过输出流将文件写回浏览器
            ServletOutputStream outputStream = response.getOutputStream();
            ////响应回去的类型:图片文件("image/jpeg")
            response.setContentType("image/png");
            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) {
            throw new RuntimeException(e);
        }
    }
}

11.操作两表之新增菜品

要操作的是菜品和菜品口味两张表。我们不仅需要保存菜品的基本信息,还要保存菜品的口味信息,需要操作两张表,所以就需要在接口中自定义接口方法,来保存上述两部分数据。
页面传递的菜品口味信息仅仅包含name和value属性,缺少一个非常重要的属性dishId,所以在保存玩菜品的基本信息后,我们需要获取到菜品ID,然后为菜品口味对象属性dishId赋值。具体逻辑:
(1)保存菜品基本信息
(2)获取保存的菜品ID
(3)获取菜品口味列表,遍历列表,为菜品口味对象属性dishId赋值
(4)批量保存菜品口味列表

@Override
@Transactional
public void saveWithFlavor(DishDto dishDto) {
    //保存菜品的基本信息到菜品表
    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());
    //将口味添加到菜品表
    dishFlavorService.saveBatch(flavors);
}

Controller

dishService.saveWithFlavor(dishDto);

12.涉及第二张表的分页查询(对象拷贝)

菜品表(基本信息)+菜品分类表(类别名称),需要根据菜品的分类ID去分类表中查询分类信息,然后再页面显示。
具体逻辑:
(1)构造分页对象
(2)构建查询及排序条件
(3)执行分页条件查询
(4)遍历分页查询列表数据,根据ID查询分类信息,从而获取该菜品的分类名称
(5)封装数据并返回

@GetMapping("/page")
public R<Page>page(int page, int pageSize, String name){
    //构造分页对象
    Page<Dish> pageInfo = new Page<>(page,pageSize);
    Page<DishDto> dishDtoPage = new Page<>();
    //条件构造器
    LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
    //过滤条件
    dishLambdaQueryWrapper.like(name != null,Dish::getName,name);
    dishLambdaQueryWrapper.orderByDesc(Dish::getUpdateTime);
    dishService.page(pageInfo,dishLambdaQueryWrapper);
    //return R.success(pageInfo);

    //对象拷贝:将pageInfo的属性对应拷贝到dishDtoPage,但不拷贝records属性
    BeanUtils.copyProperties(pageInfo,dishDtoPage,"records");
    List<Dish> records = pageInfo.getRecords();//获取当前页数据
    List<DishDto> list = records.stream().map((item) -> {
        //item是每一个Dish对象
        DishDto dishDto = new DishDto();
        //拷贝dish普通属性到dishDto
        BeanUtils.copyProperties(item,dishDto);

        //拿到分类id去查分类表,得到分类名称
        Long categoryId = item.getCategoryId();//分类id
        //根据id查询分类对象,给到dishDto
        Category category = categoryService.getById(categoryId);
        if (category != null) {
            String categoryName = category.getName();
            dishDto.setCategoryName(categoryName);
        }
        return dishDto;
    }).collect(Collectors.toList());
    dishDtoPage.setRecords(list);

    return R.success(dishDtoPage);
}

13.批量启售停售(LambdaUpdateWrapper的使用)

@PostMapping("/status/{status}")
public R<String> updateMulStatus(@PathVariable Integer status, Long[] ids){
    List<Long> list = Arrays.asList(ids);
    //构造条件构造器
    LambdaUpdateWrapper<Setmeal> updateWrapper = new LambdaUpdateWrapper<>();
    //添加过滤条件
    updateWrapper.set(Setmeal::getStatus,status).in(Setmeal::getId,list);
    setmealService.update(updateWrapper);

    return R.success("套餐信息修改成功");
}

14.批量(单个)删除

@DeleteMapping
public R<String> delete(@RequestParam List<Long> ids){
    log.info("菜品id{}",ids);
    dishService.removeDish(ids);
    return R.success("菜品删除成功");
}
public void removeDish(List<Long> ids) {
    //构造条件构造器
    LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
    //添加过滤条件
    dishLambdaQueryWrapper.in(Dish::getId,ids);
    dishLambdaQueryWrapper.eq(Dish::getStatus,1);
    int count = this.count(dishLambdaQueryWrapper);
    if(count > 0){
        throw new CustomException("菜品正在售卖");
    }
    //正常删除
    this.removeByIds(ids);

    LambdaQueryWrapper<SetmealDish> setmealDishLambdaQueryWrapper = new LambdaQueryWrapper<>();
    setmealDishLambdaQueryWrapper.in(SetmealDish::getDishId,ids);
    //删除关系表中的数据setmeal_dish
    setmealDishService.remove(setmealDishLambdaQueryWrapper);
}