Skip to content

Latest commit

 

History

History
662 lines (518 loc) · 19.8 KB

develop-log.md

File metadata and controls

662 lines (518 loc) · 19.8 KB

功能记录

1.Bean到controller

@Data
public class Employee implements Serializable {...}
@Mapper
public interface EmployeeMapper extends BaseMapper<Employee> {
}
public interface EmployeeService extends IService<Employee> {
}
@Service
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements EmployeeService {
} 
@Slf4j
@RestController
@RequestMapping("/employee")
public class EmployeeController {

    @Autowired
    private EmployeeService employeeService;
}

注意:在backend,request.js中更改前端接收响应的时间,方便debug

    // 超时,运行为10000,debug时为1000000
    timeout: 1000000

2.拦截未登录的请求

防止用户在未登录的情况下访问到其他界面

(1)框架

//设置过滤器名字和拦截pattern,在main类中设置@ServletComponentScan进行组件扫描
@WebFilter(filterName = "LoginFilter",urlPatterns = "/*")
@Slf4j
public class LoginFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        log.info("拦截到请求{}",request.getRequestURL());
        filterChain.doFilter(request,servletResponse);
    }
}

(2)逻辑

//路径匹配器,支持通配符
    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;
//        log.info("拦截到请求{}",request.getRequestURL());
        //1.获取本次请求的URI,这里URI(/employee/login)和URL(http://localhost:8080/backend/page/login/login.html)完全不同
        String requestURI = request.getRequestURI();
//        log.info("Url:{}",request.getRequestURL());
        log.info("URI{}",requestURI);
        //定义不需要处理的请求路径,登录登出不需要,静态页面也不需要(数据都是动态ajax向服务端的数据库请求的)
        String[] urls = new String[]{
            "/employee/login",
            "/employee/logout",
            "/backend/**",
            "/front/**"
        };
        //2.判断是否需要登录验证,不需要登录则返回
        boolean check = checkURLs(urls, requestURI);
        if(check){
            filterChain.doFilter(request, response);
            log.info("无需登录");
            return;//注意不要忘记返回
        }
        //3.若需要登录,就判断登录状态,如果登录了就放行
        if(request.getSession().getAttribute("employee") != null){
            log.info("已经登录");
            filterChain.doFilter(request, response);
            return;//注意不要忘记返回
        }
        //4.未登录,则根据前端的要求(request.js),设置响应
        //以输出流响应前端
        log.info("未登录");
        response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
        return;
    }

    /**
     * 判断requestURL是否是不需要验证登录的url
     * @param urls
     * @param requestURI
     * @return
     */
    private boolean checkURLs(String[] urls,String requestURI){
        for (String url : urls) {
            boolean match = PATH_MATCHER.match(url ,requestURI);
            if(match)
                return true;
        }
        return false;
    }

关于doFilter后要返回的问题:

调用doFilter后,Filter链的下一个filter执行,如此知道全部filter执行完成,按照逆序(这一点跟springboot中的拦截器一样)返回到每个filter,执行他们后面的代码,所以是需要return的

区分URI和URL:

URL:http://localhost:8080/backend/page/login/login.html (是浏览器上url栏的内容)

URI:/employee/login 向服务器发送的请求

3.增加用户

/**
 * 根据提交的表单,生成数据库记录,并初始化
 * @param request
 * @param employee
 * @return
 */
@PostMapping
public R<String> addEmployee(HttpServletRequest request,@RequestBody Employee employee){
    log.info(employee.toString());
    //设置初始密码123456,使用md5加密
    employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));
    //设置创建时间和修改时间
    employee.setCreateTime(LocalDateTime.now());
    employee.setUpdateTime(LocalDateTime.now());
    //获得当前登录用户的id以设置创建人
    long creatorId = (long) request.getSession().getAttribute("employee");
    employee.setCreateUser(creatorId);
    employee.setUpdateUser(creatorId);
    employeeService.save(employee);
    return R.success("新员工添加成功");
}

注意:这里在类上有注解@PostMapping("/employee"),不要在方法上重复这个路径

如果插入重复的username,则会从mysql抛出一个异常,我们用全局异常类来处理

4.全局异常类捕获增加用户的异常

/**
 * 全局异常处理类,方法返回响应体
 */
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@Slf4j
@ResponseBody
public class GlobalExceptionHandler {

    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
//      error message:Duplicate entry '123456' for key 'idx_username'
        String message = ex.getMessage();
        if (message.contains("Duplicate entry")){
            //按照" "将字符创分为小串
            String[] split = message.split(" ");
            String msg = "用户名" + split[2] + "已存在";
            return R.error(msg);
        }
        return R.error("未知错误");
    }
}

tips,使用ctrl+F5,清除缓存刷新

5.分页功能

首先配置mybatis-plus的分页插件,为此需要一个mybatis-plus的配置类

/**
 * 配置mybatisPlus中的分页插件
 */
@Configuration
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return mybatisPlusInterceptor;
    }
}

在EmployeeController类中,捕获分页的请求

/**
     * 处理请求http://localhost:8080/employee/page?page=1&pageSize=10
     * @return
     */
    @GetMapping("/page")
    public R<Page> page(int page,int pageSize,String name){
//        log.info("page:{},pageSize:{}",page,pageSize);
        //1.构造分页对象
        Page<Employee> employeePage = new Page<>(page,pageSize);
        //2.构造查询条件
        LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
        //3.增加可能的过滤条件
        //如果有指定name的查询,加上过滤条件
        //参数0表示在name不为空时才进行该查询
        queryWrapper.like(StringUtils.isNotEmpty(name),Employee::getName,name);
        //4.查询结果排序
        queryWrapper.orderByDesc(Employee::getUpdateTime);
        //5.执行查询,employeeService会将数据放入我们传入的employeePage中
        employeeService.page(employeePage,queryWrapper);
        return R.success(employeePage);
    }

6.员工管理中对员工执行update操作

/**
 * 对employee的修改请求(put,http://localhost:8080/employee)
 * @param request
 * @param employee 获取前端已经修改好的employee对象
 * @return
 */
@PutMapping()
public R<String> update(HttpServletRequest request,@RequestBody Employee employee){
    long empId  = (long) request.getSession().getAttribute("employee");
    employee.setUpdateTime(LocalDateTime.now());
    employee.setUpdateUser(empId);//设置是编号为empId的用户修改了本用户
    employeeService.updateById(employee);
    return R.success("修改成功");
}

这里会出现一个问题,我们在数据库中并没有找到要更新的记录,这是因为id过长,而在前端js对其进行了舍入,在Long长度大于17位时会出现精度丢失的问题.解决方法是从后端传入string给前端而不是long,为此需要消息转换器

7.使用消息转换器

如下消息转换器将转化java对象为json,这里将long转换为json

/**
 * 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
 * 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
 * 从Java对象生成JSON的过程称为 [序列化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);
    }
}

在mvc配置类的消息转换器扩展方法中,加入这个消息转换器

/**
     * 扩展mvc中的消息转换器
     * @param converters
     */
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
    log.info("扩展消息转换器");
    //消息转换器会将我们return给前端的R对象转换为对应的json
    //创建消息转换器对象
    MappingJackson2HttpMessageConverter messageConverter =
        new MappingJackson2HttpMessageConverter();
    //设置对象转换器,使用我们自己定义的消息转换器对象
    messageConverter.setObjectMapper(new JacksonObjectMapper());
    //将我们设置的消息转换器放入mvc框架的转化器集合中(放在首位才会优先使用)
    converters.add(0,messageConverter);
}

8. 处理路径参数为员工id的查询请求

    /**
     * 处理按照id查询员工信息的请求(url为 /employee/id的值)
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    public R<Employee> getById(@PathVariable long id){
//        log.info("id:{}",id);
        Employee employee = employeeService.getById(id);
        return R.success(employee);
    }

9.统一处理不同表的相同字段

问题:比如更新时间,创建时间等等字段,在employee表中有,在别的表也有,这会导致很多重复代码

mybatis-plus框架提供了解决方法,统一对他们进行处理:

1.在要处理的Bean类中加入注解

	@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.创建一个组件实现MetaObjectHandler接口

@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {

    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("insert时,进行公共字段填充");
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("update时,进行公共字段填充");

    }
}

重写方法

@Override
public void insertFill(MetaObject metaObject) {
    log.info("insert时,进行公共字段填充");
    metaObject.setValue("createTime", LocalDateTime.now());
    metaObject.setValue("updateTime", LocalDateTime.now());
    //这里暂时写死
    metaObject.setValue("updateUser", new Long(1));
    metaObject.setValue("createUser", new Long(1));
}

@Override
public void updateFill(MetaObject metaObject) {
    log.info("update时,进行公共字段填充");

    metaObject.setValue("updateTime", LocalDateTime.now());
    //这里暂时写死
    metaObject.setValue("updateUser", new Long(1));
}

引入新问题,如何获得当前登录的empId?

10.将登录id放入线程作用域

1.创建使用线程域变量的工具类

/**
 * 基于ThreadLocal封装工具类,用户保存和获取当前登录用户的id
 * 作用域在一个线程内
 */
public class BaseContext {
    private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    public static void setCurrentId(Long id){
        threadLocal.set(id);
    }

    public static Long getCurrentId(){
        return threadLocal.get();
    }
}

2.过滤器中,登录成功情况下获取empId

//在线程作用域中存入登录用户id
long empId = (long) request.getSession().getAttribute("employee");
BaseContext.setCurrentId(empId);

3.在统一处理界面将empId获取

@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {

    @Override
    public void insertFill(MetaObject metaObject) {
//        log.info("insert时,进行公共字段填充");
        metaObject.setValue("createTime", LocalDateTime.now());
        metaObject.setValue("updateTime", LocalDateTime.now());
        //通过线程域获取数据
        Long currentId = BaseContext.getCurrentId();
        metaObject.setValue("updateUser", currentId);
        metaObject.setValue("createUser", currentId);

    }

    @Override
    public void updateFill(MetaObject metaObject) {
//        log.info("update时,进行公共字段填充");
        metaObject.setValue("updateTime", LocalDateTime.now());
        Long currentId = BaseContext.getCurrentId();
        metaObject.setValue("updateUser", currentId);
    }
}

11.自定义业务异常

自定义异常类:

/**
 * 自定义业务异常
 */
public class CustomException extends RuntimeException{
    public CustomException(String message){
        super(message);
    }
}

在全局异常处理类中:

/**
 * 捕获自定义异常,返回给客户端
 * @param ex
 * @return
 */
@ExceptionHandler(CustomException.class)
public R<String> customExceptionHandler(CustomException ex){
    return R.error(ex.getMessage());
}

12. 文件上传/下载

tips:项目引入新界面时,在maven中使用clear package,以更新目录

image-20220907182724267

@Slf4j
@RestController
@RequestMapping("/common")
public class CommonController {
    /**
     * 捕获上传文件的请求
     * @param file 跟请求中form-data中的name字段一致
     * @return
     */
    @RequestMapping("/upload")
    public R<String> upload(MultipartFile file){
        log.info(file.toString());
        return null;
    }
}

13.添加菜品

dish表中加入菜品,并为菜品设置口味选项等消息,这些信息在dish_favor表中;

1.提供Dish,DishFavor类到Service类,其中Dish需要提供Controller

2.需要一个类来接收请求,因为post请求中有口味信息,不能使用Dish简单地接收,这里需要提供一个新的类,继承了Dish,还包含了DishFavor

/**
 * 数据传输对象,包含了Dish对象中没有的字段
 */
@Data
public class DishDto extends Dish {

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

    private String categoryName;

    private Integer copies;
}

3.在DishController类中接收请求,用DishDto接收json数据

@PostMapping
public R<String> save(@RequestBody DishDto dishDto){
    log.info(dishDto.toString());
    dishService.saveWithFlavor(dishDto); //稍后定义
    return R.success("新增菜品成功");
}

4.自定义方法saveWithFlavor

因为涉及了两张表的操作,这里在方法上引入事务,ReggieApplication类上标注@EnableTransactionManagement开启事务功能

public interface DishService extends IService<Dish> {
    //在dish表存储dish,在dishFlavor表中存储flavor
    public void saveWithFlavor(DishDto dishDto);
}
@Service
public class DishServiceImpl extends ServiceImpl<DishMapper, Dish> implements DishService {

    @Autowired
    DishFlavorService dishFlavorService;

    /**
     * 新增菜品,并将该菜品的口味信息(可能有多个)存储到对应表中
     * @param dishDto 封装了菜品和口味的对象
     */
    @Override
    @Transactional //涉及了多张表的操作,需要开启事务
    public void saveWithFlavor(DishDto dishDto) {
        this.save(dishDto);//当我们将新菜品存入数据库,根据雪花算法生成了id
        //获取这个新生成的id
        Long dishId = dishDto.getId();
        //获取每个flavor,为其设置dishId
        List<DishFlavor> flavors = dishDto.getFlavors();
        for (DishFlavor flavor : flavors) {
            flavor.setDishId(dishId);
        }
        //在数据库中存储所有flavor
        dishFlavorService.saveBatch(flavors);
    }
}

tips:在查数据库可视化工具时如果发现没有数据,找下一页或者禁用限制行

image-20220908161920028

14.对菜品的分页查询

分页查询出菜品的各种信息,其中包含一个菜品种类的信息,在category表中

思路:先查出Dish,再根据Dish的分页查询结果,再查询Category,将最终结果封装在DishDto的分页对象中

@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name){
    Page<Dish> pageInfo = new Page<>(page,pageSize);
    //我们需要分类名称,而Dish没有,在DisDto中封装这个字段
    Page<DishDto> dishDtoPage = new Page<>();
    //1.Dish的分页查询
    LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.like(!StringUtils.isEmpty(name), Dish::getName,name);
    queryWrapper.orderByDesc(Dish::getUpdateTime);
    dishService.page(pageInfo, queryWrapper);
    //2.根据已有的分页查询结果,查询出分类名称
    //将分页数据中除了records的部分全部复制到dishDtoPage中
    BeanUtils.copyProperties(pageInfo, dishDtoPage,"records");
    //单独处理records
    List<Dish> records = pageInfo.getRecords();
    //通过流,将records每个item(Dish对象)和对categoryName的查询结果一起存入DishDto对象,并整理成list返回
    List<DishDto> dishDtos = records.stream().map((item)->{
        DishDto dishDto = new DishDto();
        BeanUtils.copyProperties(item, dishDto);
        Category category = categoryService.getById(dishDto.getCategoryId());
        String categoryName = category.getName();
        if (categoryName!=null) dishDto.setCategoryName(categoryName);
        return dishDto;
    }).collect(Collectors.toList());
    //将新records放入新分页对象
    dishDtoPage.setRecords(dishDtos);
    return R.success(dishDtoPage);
}