瑞吉外卖学习总结
主要业务:
基于MySQL实现员工登录、退出、添加、信息修改等功能,并通过md5加密,保证用户密码安全。
为什么要用transcation,保持事务的原子性:
1.把一条数据插入到俩张表的时候,就要用到事务了。譬如:如果 data在插入第一张表的时候成功了,在插入第二张表的时候失败了,怎么办?
要么保证全部成功(提交),要么就回滚(一条也别成功)。才能保证数据的准确性。
基于MySQL,Transaction实现菜品、菜品类别、套餐的新增、修改菜品(保持菜品表和菜品口味表之间的关联关系-用菜品id关联)、修改套餐(修改套餐的时候要保持套餐表和套餐内菜品详表之间的关联关系-用套餐id关联)
基于MySQL,Transaction分别实现菜品、菜品类别及套餐的新增、修改、删除、分页显示功能。
基于Redis实现用户登陆、验证码缓存、用户端菜品及套餐缓存,基于MySQL实现用户端菜品展示、购物车功能,下单提交等功能。
后台:
- 后台员工登陆、退出功能开发(用到了过滤器)
- 后台员工增加、分页查询、状态变更、信息修改(除了业务实现方法,还用到了异常捕获、对象转换器、公共字段填充等)
- 菜品分类的分页查询、删除、修改
- 新增菜品、图片上传、分页查询、菜品编辑
前台:
- 用户注册、验证码登录,通过md5加密保护用户密码安全
- 购物车开发、下单
后台员工登陆、退出功能开发、过滤器登录优化
后台员工登陆、退出功能开发(用到了过滤器)
员工登录
查看登录请求信息:
登录前端页面在请求体中打包登录名和密码
后端实现逻辑:
- 密码md5加密
- 根据用户名查询数据库中用户是否存在
- 密码比对
- 查看员工权限状态
- 将用户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);
}
退出登录
- 跳转页面,并且在当前session中移除id
- HttpServletRequest
HttpServletRequest对象代表客户端的请求,当客户端通过HTTP协议访问服务器时,HTTP请求头中的所有信息都封装在这个对象中,通过这个对象提供的方法,可以获得客户端请求的所有信息。 - HttpServletRequest中的 HttpSession getSession(booleancreate)方法
用于返回和当前请求相关的HttpSession对象, 如果当前没有session且create是true, 返回一个新的session.
@RequestMapping("/logout")
public R<String> logout(HttpServletRequest request){
// 清理缓存(然后前端进行页面跳转)
request.getSession().removeAttribute("employee");
return R.success("退出成功");
}
登录优化(过滤器)
实现不同登录状态下,不同页面请求下的页面过滤与否
实现逻辑
- 获取本次请求的URL(ServletRequest servletRequest, ServletResponse servletResponse)
- 定义不需要处理的请求路径,判断本次请求是否需要处理,如果不需要处理,则直接放行。(dofilter)
- 判断登录状态,如果已登录,则直接放行
- 用户未登录,不过滤
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;
}
}
员工管理:添加、删除、信息修改,分页显示,异常捕获
员工添加
员工表结构:
实现逻辑:
- md5加密员工密码
- 在数据库中保存新增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表中对该字段加入了唯一约束,此时程序会抛出异常:
故使用异常处理器进行全局异常捕获
实现逻辑:
- 添加全局异常类捕获异常
- 添加日志,返回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());
}
}
分页查询
程序执行过程:
- 前端页面发送请求,含有page pagesize name参数
- controller处理请求
- service调用mapper对象操作数据库
- controller将查询到的page对象返回到客户端
- 页面显示
实现逻辑
- 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);
}
修改员工状态
程序执行流程:
- 页面发送ajax请求,将参数(id、 status)提交到服务端
- 服务端Controller接收页面提交的数据并调用Service更新数据
- Service调用Mapper操作数据库
实现逻辑:
注意:js对long型数据进行处理时会丢失精度,导致在根据id修改员工状态时提交的id和数据库中的id不一致。
如何解决这个问题?
可以在服务端给页面响应json数据时进行处理,将long型数据统一转为String字符串。
- 提供对象转换器JacksonobjectMapper,基于Jackson进行Java对象到json数据的转换
- 在WebMvcConfig配置类中扩展Spring mvc的消息转换器,在此消息转换器中使用提供的对象转换器进行Java对象到json数据的转换
- 根据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("员工信息修改成功");
}
编辑员工信息
代码实现流程:
- 点击修改,进入修改页面,员工信息需回显
- 根据id查询数据库,服务端将查询结果提交给客户端
- 修改员工信息并提交,调用的是上文中已有的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);
}
类别删除(注意有关联菜品时不允许删除)
实现逻辑:
- 注入其他的service接口,查询关联的菜品和套餐
- 删除类别或者不删除类别
- 定义异常类,在全局类中添加对应方法
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请求,请求服务端获取菜品回显在下拉框中
后台管理系统中添加菜品时可以上传菜品图片
页面发送请求下载图片,将图片回显在菜品显示页面
实现逻辑:
- 前端通过form表单等形式上传图片文件
- 服务端接受前端上传的文件(原理是基于apache的commons-fileupload以及commons-io包,但是基于MultipartFile类实现,是由springweb包提供的类)
- 以附件形式下载图片,保存到指定的磁盘目录(服务端将文件以流的形式写回浏览器)
具体实现:
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("新增菜品成功");
}