【无标题】

瑞吉外卖学习总结

主要业务:
基于MySQL实现员工登录、退出、添加、信息修改等功能,并通过md5加密,保证用户密码安全。

为什么要用transcation,保持事务的原子性:
1.把一条数据插入到俩张表的时候,就要用到事务了。譬如:如果 data在插入第一张表的时候成功了,在插入第二张表的时候失败了,怎么办?
要么保证全部成功(提交),要么就回滚(一条也别成功)。才能保证数据的准确性。
基于MySQL,Transaction实现菜品、菜品类别、套餐的新增、修改菜品(保持菜品表和菜品口味表之间的关联关系-用菜品id关联)、修改套餐(修改套餐的时候要保持套餐表和套餐内菜品详表之间的关联关系-用套餐id关联)
基于MySQL,Transaction分别实现菜品、菜品类别及套餐的新增、修改、删除、分页显示功能。

基于Redis实现用户登陆、验证码缓存、用户端菜品及套餐缓存,基于MySQL实现用户端菜品展示、购物车功能,下单提交等功能。

后台:

  1. 后台员工登陆、退出功能开发(用到了过滤器)
  2. 后台员工增加、分页查询、状态变更、信息修改(除了业务实现方法,还用到了异常捕获、对象转换器、公共字段填充等)
  3. 菜品分类的分页查询、删除、修改
  4. 新增菜品、图片上传、分页查询、菜品编辑

前台:

  1. 用户注册、验证码登录,通过md5加密保护用户密码安全
  2. 购物车开发、下单

后台员工登陆、退出功能开发、过滤器登录优化

后台员工登陆、退出功能开发(用到了过滤器)

员工登录

查看登录请求信息:
登录前端页面在请求体中打包登录名和密码

后端实现逻辑:

  1. 密码md5加密
  2. 根据用户名查询数据库中用户是否存在
  3. 密码比对
  4. 查看员工权限状态
  5. 将用户Id存入session并返回登陆成功结果(Employee对象)
 @RequestMapping("/login")
    public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee){
//        password通过md5加密
        String password=employee.getPassword();
        password=DigestUtils.md5DigestAsHex(password.getBytes());
//        根据页面提交的用户名查找数据库
        LambdaQueryWrapper<Employee> queryWrapper=new LambdaQueryWrapper<>();
        queryWrapper.eq(Employee::getUsername,employee.getUsername());//等值查询,这里的employee是输入参数
        Employee emp=emolyeeService.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());
        return R.success(emp);
    }

退出登录

  1. 跳转页面,并且在当前session中移除id
  2. HttpServletRequest
    HttpServletRequest对象代表客户端的请求,当客户端通过HTTP协议访问服务器时,HTTP请求头中的所有信息都封装在这个对象中,通过这个对象提供的方法,可以获得客户端请求的所有信息。
  3. HttpServletRequest中的 HttpSession getSession(booleancreate)方法
    用于返回和当前请求相关的HttpSession对象, 如果当前没有session且create是true, 返回一个新的session.
@RequestMapping("/logout")
    public R<String> logout(HttpServletRequest request){
//        清理缓存(然后前端进行页面跳转)
        request.getSession().removeAttribute("employee");
        return R.success("退出成功");
    }

登录优化(过滤器)

实现不同登录状态下,不同页面请求下的页面过滤与否
实现逻辑

  1. 获取本次请求的URL(ServletRequest servletRequest, ServletResponse servletResponse)
  2. 定义不需要处理的请求路径,判断本次请求是否需要处理,如果不需要处理,则直接放行。(dofilter)
  3. 判断登录状态,如果已登录,则直接放行
  4. 用户未登录,不过滤
package com.itheima.reggie.filter;

import com.alibaba.fastjson.JSON;
import com.itheima.reggie.common.BaseContext;
import com.itheima.reggie.common.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.AntPathMatcher;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebFilter(filterName = "LoginCheckFilter",urlPatterns = "/*")
@Slf4j
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;

//        1、获取本次请求的URI
        String requestURI = request.getRequestURI();

        log.info("拦截到请求:{}",requestURI);

//        定义不需要处理的请求路径
        String[] urls=new String[]{
                "/employee/login",
                "/employee/logout",
                "/backend/**",
                "/front/**",
                "/common/**",
                "/user/sendMsg",
                "/user/login"
        };
//        2、判断本次请求是否需要处理
        boolean check = check(urls, requestURI);
//        3、如果不需要处理,则直接放行
        if(check){
            log.info("本次请求{}不需要处理",requestURI);
            filterChain.doFilter(request,response);
            return;
        }
//        4、判断登录状态,如果已登录,则直接放行
        if(request.getSession().getAttribute("employee")!=null){
            log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("employee"));

//            保存同线程之间的empId,用作公共字段功能填充
            Long empId= (Long) request.getSession().getAttribute("employee");
            BaseContext.setCurrentId(empId);

            filterChain.doFilter(request,response);
            return;
        }

        if(request.getSession().getAttribute("user")!=null){
            log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("user"));

//            保存同线程之间的empId,用作公共字段功能填充
            Long empId= (Long) request.getSession().getAttribute("user");
            BaseContext.setCurrentId(empId);

            filterChain.doFilter(request,response);
            return;
        }

        log.info("用户未登录");
//        5、如果未登录则返回未登录结果,通过输出流向客户端页面响应数据
        response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
        return;
    }
    //路径匹配,检查本次请求是否需要放行
    public boolean check(String[] urls,String requestURI){
        for (String url : urls) {
            boolean match = PATH_MATCHER.match(url, requestURI);
            if(match==true){
                return true;
            }
        }
        return  false;
    }
}

员工管理:添加、删除、信息修改,分页显示,异常捕获

员工添加

员工表结构:
**在这里插入图片描述**
实现逻辑:

  1. md5加密员工密码
  2. 在数据库中保存新增Employee对象
    @PostMapping
    public R<String> save(HttpServletRequest request, @RequestBody Employee employee){
        log.info("新增员工,员工信息:{}",employee.toString());
        //设置初始密码,需要进行md5加密处理
        employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));

//        employee.setCreateTime(LocalDateTime.now());
//        employee.setUpdateTime(LocalDateTime.now());

//        Long empId = (Long) request.getSession().getAttribute("employee");

//        employee.setCreateUser(empId);
//        employee.setUpdateUser(empId);

        emolyeeService.save(employee);

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

异常捕获

解决:前面的程序还存在一个问题,就是当我们在新增员工时输入的账号已经存在,由于employee表中对该字段加入了唯一约束,此时程序会抛出异常:

故使用异常处理器进行全局异常捕获
实现逻辑:

  1. 添加全局异常类捕获异常
  2. 添加日志,返回R.errror(“…”)
    使用的是如下第一个方法,注意使用了
    @ControllerAdvice(annotations = {RestController.class})//决定RestController类要被拦截或者处理
@ControllerAdvice(annotations = {RestController.class})//决定RestController类要被拦截或者处理
@Slf4j
@ResponseBody
public class GlobalExceptionHandle {
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)//处理的异常类型
    public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
//        log.info(ex.getMessage());
//        return R.error("失败了");
        if(ex.getMessage().contains("Duplicate entry")){
            String[] split=ex.getMessage().split(" ");
            String message=split[2]+"已存在";
            return R.error(message);
        }
        return R.error("未知错误");
    }
    @ExceptionHandler(CustomException.class)
    public R<String> exceptionHandler(CustomException ex ){
//        log.info(ex.getMessage());
//        return R.error("失败了");
        log.error(ex.getMessage());

        return R.error(ex.getMessage());
    }
}

分页查询

程序执行过程:

  1. 前端页面发送请求,含有page pagesize name参数
  2. controller处理请求
  3. service调用mapper对象操作数据库
  4. controller将查询到的page对象返回到客户端
  5. 页面显示

实现逻辑

  1. List item

配置MP分页插件

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

page类是基于mabatisplus的内部类

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

        LambdaQueryWrapper<Employee> queryWrapper=new LambdaQueryWrapper<>();
        queryWrapper.like(!StringUtils.isEmpty(name),Employee::getUsername,name);//等值查询,这里的employee是输入参数
        queryWrapper.orderByDesc(Employee::getUpdateTime);

        emolyeeService.page(pageInfo,queryWrapper);

        return R.success(pageInfo);

    }

修改员工状态

程序执行流程:

  1. 页面发送ajax请求,将参数(id、 status)提交到服务端
  2. 服务端Controller接收页面提交的数据并调用Service更新数据
  3. Service调用Mapper操作数据库

实现逻辑:
注意:js对long型数据进行处理时会丢失精度,导致在根据id修改员工状态时提交的id和数据库中的id不一致。
如何解决这个问题?
可以在服务端给页面响应json数据时进行处理,将long型数据统一转为String字符串。

  1. 提供对象转换器JacksonobjectMapper,基于Jackson进行Java对象到json数据的转换
  2. 在WebMvcConfig配置类中扩展Spring mvc的消息转换器,在此消息转换器中使用提供的对象转换器进行Java对象到json数据的转换
  3. 根据id修改员工状态

1、

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

2、

@Slf4j
@Configuration//说明这是一个配置类
public class WebMvcConfig extends WebMvcConfigurationSupport {

    /**
     * 设置静态资源映射,映射到哪些静态资源文件
     * @param registry
     */
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        log.info("开始进行静态资源映射");
        registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
        registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
        registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
        registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
    }


//  在WebMvcConfig配置类中扩展Spring mvc的消息转换器,在此消息转换器中使用提供的对象转换器进行Java对象到json数据的转换
    @Override
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        //创建消息转换器
        MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
        //设置对象转换器,底层使用Jackson将Java转换为json
        messageConverter.setObjectMapper(new JacksonObjectMapper());
        //将上面的消息转换器对象追加到mvc框架的转换器集合中
        converters.add(0,messageConverter);
        super.extendMessageConverters(converters);
    }

}

3、

@PutMapping
    public R<String> update(HttpServletRequest request, @RequestBody Employee employee){
        log.info(employee.toString());

        Long name=(Long) request.getSession().getAttribute("employee");
//        employee.setUpdateTime(LocalDateTime.now());
//        employee.setUpdateUser(name);
        long id = Thread.currentThread().getId() ;
        log.info("线程id:{}" ,id);
        emolyeeService.updateById(employee);
        return R.success("员工信息修改成功");
    }

编辑员工信息

代码实现流程:

  1. 点击修改,进入修改页面,员工信息需回显
  2. 根据id查询数据库,服务端将查询结果提交给客户端
  3. 修改员工信息并提交,调用的是上文中已有的update方法

2、

@GetMapping("/{id}")
    public R<Employee> getById(@PathVariable String id){
        log.info("根据id查对象");
        Employee emp = emolyeeService.getById(id);
        if(emp!=null){
            return R.success(emp);
        }
        return R.error("没有查询到该用户信息");
    }

公共字段填充

需求:
对createUser、updateUser、createTime、updateTime等公共字段能够自动实现,不需要在业务类中多次冗余撰写(AOP的原理)
实现:
1.在实体类(这里是Category类)中加入TableField注解,指定自动填充的策略
2.编写元数据对象处理器,在此类中统一为公共字段赋值,需要实现MetaObjectHandler接口
3.通过TreadLocal对象获取每个独立线程的用户id

1、

@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、

package com.itheima.reggie.common;

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

/**
 * 按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现MetaObjectHandler接口
 */
@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());
        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());

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

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

    }
}

3、ThreadLocal
不是Thread,是Thread线程的局部变量,使用TreadLocal时,每一个独立线程都有自己的ThreadLocal对象。ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

ThreadLocal常用方法:
public void set(T value) 设置当前线程局部变量的值
public T get() 返回当前线程所对应的线程局部变量的值

实现逻辑:
1.编写BaseContext工具类,基于ThreadLocal封装的工具类
2.在LogincheckFilter的doFilter方法中调用BaseContext来设置当前登录用户的id
3.在MyMeta0bjectHandler的方法中调用BaseContext获取登录用户的id

/**
 * 基于ThreadLocal封装的工具类,用于保存和获取当前登录用户的id
 */
 package com.itheima.reggie.common;

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

/**
 * 按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现MetaObjectHandler接口
 */
@Component
@Slf4j
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、

if (request.getSession().getAttribute("employee") != null) {
    log.info("用户已登录,用户id为:{}", request.getSession().getAttribute("employee"));

    Long empId= (Long) request.getSession().getAttribute("employee");

    BaseContext.setCurrentId(empId);

    filterChain.doFilter(request, response);
    return;
}

3、(和上面重复了)

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

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

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

    }
}

菜品分类

分类信息分页查询

程序执行:
1、页面发送ajax请求,将分页查询参数(page.pageSize)提交到服务端
2、服务端Controller接收页面提交的数据并调用Service查询数据
3、Service调用Mapper操作数据库,查询分页数据
4、Controller将查询到的分页数据响应给页面
5、页面接收到分页数据并通过ElementUI的Table组件展示到页面上

2、

@GetMapping("/page")
public R<Page> page(int page, int pageSize) {
    //构造分页构造器
    Page<Category> pageInfo=new Page<>(page,pageSize);
    //构造条件构造器
    LambdaQueryWrapper<Category> queryWrapper=new LambdaQueryWrapper<>();
    //添加排序条件,根据sort进行排序
    queryWrapper.orderByAsc(Category::getSort);
    //进行分页查询
    categoryService.page(pageInfo,queryWrapper);

    return R.success(pageInfo);
}

类别删除(注意有关联菜品时不允许删除)

实现逻辑:

  1. 注入其他的service接口,查询关联的菜品和套餐
  2. 删除类别或者不删除类别
  3. 定义异常类,在全局类中添加对应方法

2、
在CatergoryService中添加remove方法
注意一部分数据库修改内容是基于对Service接口的重写,有的是直接在controller层中写。我认为原因有2:
①remove的重写是因为实际条件下才能删除这一条件的加持,在service实现类中重写remove方法可以减小冗余度
②让controller中代码的繁杂度减小

public interface CategoryService extends IService<Category> {
    public void remove(Long id);
}

在CategoryServicelmpl实现remove方法

@Service
public class CategoryServicelmpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {

    @Autowired
    private DishService dishService;

    @Autowired
    private SetmealService setmealService;

    @Override
    public void remove(Long id) {
        LambdaQueryWrapper<Dish> dishLambdaQueryWrapper=new LambdaQueryWrapper<>();
        //添加查询条件,根据分类id进行查询
        dishLambdaQueryWrapper.eq(Dish::getCategoryId,id);
        int count1 = dishService.count(dishLambdaQueryWrapper);

        //查询当前分类是否关联菜品,如果已经关联,抛出业务异常
        if(count1>0){
            //已经关联菜品,抛出业务异常
            throw new CustomException("已经关联菜品,不能删除");
        }

        //查询当前分类是否关联了套餐,如果已经关联,抛出业务异常
        LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper=new LambdaQueryWrapper<>();
        //添加查询条件,根据分类id进行查询
        setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId,id);
        int count2 = setmealService.count(setmealLambdaQueryWrapper);

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

3、定义异常类,在全局类中添加对应方法

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

在全局类中添加对应方法(第二个方法)

@ControllerAdvice(annotations = {RestController.class})//决定RestController类要被拦截或者处理
@Slf4j
@ResponseBody
public class GlobalExceptionHandle {
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)//处理的异常类型
    public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
//        log.info(ex.getMessage());
//        return R.error("失败了");
        if(ex.getMessage().contains("Duplicate entry")){
            String[] split=ex.getMessage().split(" ");
            String message=split[2]+"已存在";
            return R.error(message);
        }
        return R.error("未知错误");
    }

    @ExceptionHandler(CustomException.class)
    public R<String> exceptionHandler(CustomException ex ){
//        log.info(ex.getMessage());
//        return R.error("失败了");
        log.error(ex.getMessage());

        return R.error(ex.getMessage());
    }



}

修改分类

//修改分类
@PutMapping
public R<String> update(@RequestBody Category category){
    categoryService.updateById(category);
    return R.success("分类修改成功");

具体菜品

图片上传

执行流程:
页面发送ajax请求,请求服务端获取菜品回显在下拉框中
后台管理系统中添加菜品时可以上传菜品图片
页面发送请求下载图片,将图片回显在菜品显示页面

实现逻辑:

  1. 前端通过form表单等形式上传图片文件
  2. 服务端接受前端上传的文件(原理是基于apache的commons-fileupload以及commons-io包,但是基于MultipartFile类实现,是由springweb包提供的类)
  3. 以附件形式下载图片,保存到指定的磁盘目录(服务端将文件以流的形式写回浏览器)

具体实现:
1、文件上传,页面端可以使用ElementuI提供的上传组件。
2、服务端接受前端上传的文件(原理是基于apache的commons-fileupload以及commons-io包,但是基于MultipartFile类实现,是由springweb包提供的类)

@Slf4j
@RestController
@RequestMapping("/common")
public class CommonController {
    @Value("${reggie.path}")
    private String basePath;

    //文件上传
    @PostMapping("/upload")
    public R<String> upload(MultipartFile file){
        //file 是一个临时文件,需要转存到指定位置,否则请求完成后临时文件会删除
        //log.info("file:{}",file.toString());

        //原始文件名
        String originalFilename = file.getOriginalFilename();
        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);
    }
}


3、文件下载

//文件下载
@GetMapping("/download")
public void download(String name, HttpServletResponse response){
    try {
        //输入流,通过输入流读取文件内容
        FileInputStream fileInputStream=new FileInputStream(new File(basePath+name));
        //输出流,通过输出流将文件写回浏览器,在浏览器中展示图片
        ServletOutputStream outputStream = response.getOutputStream();

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

新增菜品

在开发代码之前,需要梳理一下新增菜品时前端页面和服务端的交互过程:

1、页面发送ajax请求,请求服务端获取菜品分类数据并展示到下拉框中
2、页面发送请求进行图片上传,请求服务端将图片保存到服务器
3、页面发送请求进行图片下载,将上传的图片进行回显
4、点击保存按钮,发送ajax请求,将菜品相关数据以json形式提交到服务端

开发新增菜品功能,其实就是在服务端编写代码去处理前端页面发送的这4次请求即可。

1、菜品分类下拉框:在CategoryController添加

//根据条件查询分类数据
@GetMapping("/list")
public R<List<Category>> list(Category category){
    //条件构造器
    LambdaQueryWrapper<Category> lambdaQueryWrapper=new LambdaQueryWrapper<>();
    //添加条件
    lambdaQueryWrapper.eq(category.getType()!=null,Category::getType,category.getType());
    //添加排序条件
    lambdaQueryWrapper.orderByAsc(Category::getSort).orderByAsc(Category::getUpdateTime);
    List<Category> list = categoryService.list(lambdaQueryWrapper);
    return R.success(list);
}

2、在DishService接口中添加方法saveWithFlavor,在DishServiceImpl实现

@Service
public class DishServiceImpl extends ServiceImpl<DishMapper, Dish> implements DishService {
    @Autowired
    private DishFlavorService dishFlavorService;

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

        Long dishid = dishDto.getId();
        //菜品口味
        List<DishFlavor> flavors = dishDto.getFlavors();
        flavors = flavors.stream().map((item) -> {
            item.setDishId(dishid);
            return item;
        }).collect(Collectors.toList());
        //dishFlavorService.saveBatch(dishDto.getFlavors());
        //保存菜品口味到菜品数据表dish_flavor
        dishFlavorService.saveBatch(flavors);
    }
}

由于以上代码涉及多表操作,在启动类上开启事务支持添加@EnableTransactionManagement注解,但是本人添加该注解会报错,项目启动会失败,并且springboot该注解应该是默认开启的,故没有添加
3、

@PostMapping
public R<String> save(@RequestBody DishDto dishDto){
    dishService.saveWithFlavor(dishDto);
    return R.success("新增菜品成功");
}

分页查询

菜品编辑

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值