关于SpringBoot一体化开发,可以将静态资源(html/css/js等)放在resource文件下,然后编写一个SpringMvc的WebMvcConfig(需要继承一个WebMvcConfigurationSupport,重写一个addResoucehandlers(增加来源处理器)方法)进行响应配置代码的编写;
demo:
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 WebMvcConfig extends WebMvcConfigurationSupport {
/**
* 设置静态资源映射
* @param registry
*/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
log.info("开始进行静态资源映射...");
registry.addResourceHandler("/backend/**(访问谁)").addResourceLocations("classpath:/backend/(映射到哪里去)");
registry.addResourceHandler("/front/** 访问谁)").addResourceLocations("classpath:/front/"(映射到哪里去));
}
}
前端发送一个post请求,请求体中携带本次请求应该带有的参数,在后台,经过javaweb提供的过滤器(使用它,需要在启动类中加入@ServletConponentScan),进行访问路径的过滤,然后放行到员工的cotroller层,在cotroller进行账号是否存在,密码是否正确,以及该账号的状态是否已被禁用,然后同一个返回一个结果给前端控制器,前端控制器将数据给前端页面;
注意:在登录成功后,要记录该登录用户到session域对象中,以备之后对用户信息的回显,判断用户登录状态,登出用户的操作;
前端发一个post请求到后端,后端清理session中保存的用户id,返回结果
A. 点击"保存"按钮, 页面发送ajax请求,将新增员工页面中输入的数据以json的形式提交到服务端, 请求方式POST, 请求路径 /employee
B. 服务端Controller接收页面提交的数据并调用Service将数据进行保存
C. Service调用Mapper操作数据库,保存数据
A. 在新增员工时, 按钮页面原型中的需求描述, 需要给员工设置初始默认密码 123456, 并对密码进行MD5加密。
B. 在组装员工信息时, 还需要封装创建时间、修改时间,创建人、修改人信息(从session中获取当前登录用户)。
因为在数据库中对用户名进行了唯一的约束,假设,前台提交的增加用户的用户名,在数据库中已经存在,便会抛出一个Duplicate entry异常;显然,在spring中如果让它抛出了,就不得劲了,此时我们便使用Spring提供的全局异常处理机制,也就是说,所有的异常处理都交给spring,spring对异常进行处理;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import java.sql.SQLIntegrityConstraintViolationException;
/**
* 全局异常处理
*/
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {
/**
* 异常处理方法
* @return
*/
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
log.error(ex.getMessage());
if(ex.getMessage().contains("Duplicate entry")){
String[] split = ex.getMessage().split(" ");
String msg = split[2] + "已存在";
return R.error(msg);
}
return R.error("未知错误");
}
}
注解说明:
@ControllerAdvice : 指定拦截那些类型的控制器;
@ResponseBody: 将方法的返回值 R 对象转换为json格式的数据, 响应给页面;
A. 点击菜单,打开员工管理页面时,执行查询:
1). 页面发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端
2). 服务端Controller接收页面提交的数据, 并组装条件调用Service查询数据
3). Service调用Mapper操作数据库,查询分页数据
4). Controller将查询到的分页数据, 响应给前端页面
5). 页面接收到分页数据, 并通过ElementUI的Table组件展示到页面上
B. 搜索栏输入员工姓名,回车,执行查询:
1). 页面发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端
2). 服务端Controller接收页面提交的数据, 并组装条件调用Service查询数据
3). Service调用Mapper操作数据库,查询分页数据
4). Controller将查询到的分页数据, 响应给前端页面
5). 页面接收到分页数据, 并通过ElementUI的Table组件展示到页面上
最终发送给服务端的请求为 : GET请求 , 请求链接 /employee/page?page=1&pageSize=10&name=xxx
1. 分页查询==》本项目dao层框架使用的是mybatis+mybatispuls==〉在dao层框架中提供了分页插件,如果要使用,只需要进行相应配置与导入即可;
2. 配置分页插件步骤:创建一个mybatisPlus的配置文件,要加上@Configration注解,然后编写一个返回返回值为mybatisPlusInterceptor(mybatisplus的拦截器)的方法;在方法中new一个mybatisPlusInterceptor,然后调用该对象中addInnerInterceptor(增加内部拦截器方法),该方法的概述为分页内部拦截器:
demo:
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 配置MP的分页插件
*/
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
}
3. cotroller中代码实现逻辑
1. 构造分页构造器;new 一个page<>对象
2. 构造条件构造器
3. 添加过滤条件
4. 添加排序条件
5. 执行查询
员工的启用\禁用操作中,在前台两者的操作只能是管理员进行,也就是说只有管理员(admin用户)可以对其他普通用户进行启用、禁用操作,所以普通用户登录系统后启用、禁用按钮不显示,这是在前台代码中对登录的用户进行了一个判断;
1). 页面发送ajax请求,将参数(id、status)提交到服务端
2). 服务端Controller接收页面提交的数据并调用Service更新数据
3). Service调用Mapper操作数据库
注意:启用、禁用员工账号,本质上就是一个更新操作,也就是对status状态字段进行操作。在Controller中创建update方法,此方法是一个通用的修改员工信息的方法。
关于后台查询出来的员工对象的id值为一个19位的Long类型,但是前端的Long类型只能处理16位,此时需要我们将后端发给前台的long类型的数据,提前转为String类型,而当前台提交过来也会将String再转为Long
具体实现:
该功能是由于在SpringMVC中, 将Controller方法返回值转换为json对象, 是通过jackson来实现的, 涉及到SpringMVC中的一个消息转换器MappingJackson2HttpMessageConverter, 解决方法是对这个消息转换器进行扩展
实现步骤:
1). 提供对象转换器JacksonObjectMapper,基于Jackson进行Java对象到json数据的转换
package com.itheima.reggie.common;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;
/**
* 对象映射器:基于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)
//long ==》String
.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). 在WebMvcConfig配置类中扩展Spring mvc的消息转换器,在此消息转换器中使用提供的对象转换器进行Java对象到json数据的转换
/**
* 扩展mvc框架的消息转换器
* @param converters
*/
@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);
}
1). 点击编辑按钮时,页面跳转到add.html,并在url中携带参数[员工id]
2). 在add.html页面获取url中的参数[员工id]
3). 发送ajax请求,请求服务端,同时提交员工id参数
4). 服务端接收请求,根据员工id查询员工信息,将员工信息以json形式响应给页面
5). 页面接收服务端响应的json数据,通过VUE的数据绑定进行员工信息回显
6). 点击保存按钮,发送ajax请求,将页面中的员工信息以json方式提交给服务端
7). 服务端接收员工信息,并进行处理,完成后给页面响应
8). 页面接收到服务端响应信息后进行相应处理
代码实现:
/**
* 根据id查询员工信息
* @param id
* @return
*/
@GetMapping("/{id}")
public R<Employee> getById(@PathVariable Long id){
log.info("根据id查询员工信息...");
Employee employee = employeeService.getById(id);
if(employee != null){
return R.success(employee);
}
return R.error("没有查询到对应员工信息");
}
代码实现:
/**
* 根据id修改员工信息
* @param employee
* @return
*/
@PutMapping
public R<String> update(HttpServletRequest request,@RequestBody Employee employee){
log.info(employee.toString());
Long empId = (Long)request.getSession().getAttribute("employee");
employee.setUpdateTime(LocalDateTime.now());
employee.setUpdateUser(empId);
employeeService.updateById(employee);
return R.success("员工信息修改成功");
}
分析:我们可以从数据库中各个表中观察得知,每一张数据表都含有createUser、createTi me、updateUser、updateTime这些公共字段,而在本项目的技术选型中,选用mybatis以及它的最好搭档mybatisplus进行dao层的框架的搭建,而在mybatisplus中提供了公共字段自动填充的功能,通过在实体类中需要填充的字段加上@TableField注解以及编写一个自定义元数据类去实现一个MetaObjectHandler接口,实现其中的方法,该类需要交给spring容器进行管理;在通过元数据对象.setValue去设置公共字段填充内容;
关于自动填充创建/更新用户ID,子啊自定义元数据类中在通过session与获取userID的自动填充,显然是不现实的,接下来想另外一个方法去进行登录用户的信息的保存与提取—》通过实现得知,当一个请求来到后端—〉首先经过了过滤器,然后经过cotroller ,然后经过元数据处理类,最后在去service,dao层,而这一条数据处理链,都是由一个线程来处理的,也就是说,客户端发送的每次http请求,对应的在服务端都会分配一个新的线程来处理,在处理过程中涉及到下面类中的方法都属于相同的一个线程:所以我们可以用这个线程先保存信息,通过thread中threadlocal局部表量,去设置/获取参数;
ThreadLocal常用方法:
A. public void set(T value) : 设置当前线程的线程局部变量的值
B. public T get() : 返回当前线程所对应的线程局部变量的值
C. public void remove() : 删除当前线程所对应的线程局部变量的值
作用:设置线程中数据与获取线程中数据
实现:
package cn.zkwf.takeout.common;
import cn.zkwf.takeout.entity.Employee;
/**
* @description:线程数据共享工具类
* @author: Sw_Ljb
* @PACKAGE_NAME:cn.zkwf.takeout.common
* @time: 2022/7/21 下午12:53
* @version: 1.0
*/
public class ThreadContext {
//1、直接new出来这个对象 范性T是参数类型
private static ThreadLocal<Long> threadLocal = new ThreadLocal<Long>();
/**
* 设置参数值
* @param empId
*/
public static void setParameter(Long empId ){
threadLocal.set(empId);
}
/**
* 得到参数
* @return
*/
public static Long getParameter(){
return threadLocal.get();
}
}
package cn.zkwf.takeout.common;
import cn.zkwf.takeout.entity.Employee;
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;
/**
* @description: 元数据处理类
* @author: Sw_Ljb
* @PACKAGE_NAME:cn.zkwf.takeout.common
* @time: 2022/7/21 下午12:46
* @version: 1.0
*/
@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
log.info("MP进入插入时自动注入元素信息");
//1、设置自动注入时间
metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime",LocalDateTime.now());
//2、设置自动注入操作人员 因为session不能在这使用 又由于这一条执行链是同一个线程 因此使用localthread进行参数共享
//2.1 得到线程中的值
Long empID = ThreadContext.getParameter();
metaObject.setValue("createUser",empID);
metaObject.setValue("updateUser",empID);
}
@Override
public void updateFill(MetaObject metaObject) {
log.info("MP进入更新数据时自动注入元素信息");
//1、设置更新自动注入时间
metaObject.setValue("updateTime",LocalDateTime.now());
//2、设置自动注入操作人员 因为session不能在这使用 又由于这一条执行链是同一个线程 因此使用localthread进行参数共享
//2.1 得到线程中的值
Long empID = ThreadContext.getParameter();
metaObject.setValue("updateUser",empID);
}
}
1). 在页面(backend/page/category/list.html)的新增分类表单中填写数据,点击 “确定” 发送ajax请求,将新增分类窗口输入的数据以json形式提交到服务端
2). 服务端Controller接收页面提交的数据并调用Service将数据进行保存
3). Service调用Mapper操作数据库,保存数据
1. 基础环境的搭建(实体类+mapper(继承basemapper<实体类>)+service(interface IService<实体类>)(serviceimpl(extends ServiceImpl<实体类mapper,实体类> interface service))+controller);
2. 代码实现流程:前端将数据转为json传递给后端,后端在controller中使用@RequestBody注解 使用实体类去接受,然后直接调用service直接操作;
1). 页面发送ajax请求,将分页查询参数(page、pageSize)提交到服务端
2). 服务端Controller接收页面提交的数据并调用Service查询数据
3). Service调用Mapper操作数据库,查询分页数据
4). Controller将查询到的分页数据响应给页面
5). 页面接收到分页数据并通过ElementUI的Table组件展示到页面上
/**
* 分页查询
* @param page
* @param pageSize
* @return
*/
@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). 点击删除,页面发送ajax请求,将参数(id)提交到服务端
2). 服务端Controller接收页面提交的数据并调用Service删除数据
3). Service调用Mapper操作数据库
分析:在执行删除分类操作时,删除逻辑不能时直接将分类,而是应该先检查当前分类下是否关联了菜品或者套餐,然后将这个分类的id拿着,去菜品或者套餐中查询是否含有该分类的数据,如果有抛一个自定义异常,如果没有,则可以删除该分类;
实现步骤:
package cn.zkwf.takeout.exception;
/**
* @description:
* @author: Sw_Ljb
* @PACKAGE_NAME:cn.zkwf.takeout.exception
* @time: 2022/7/21 下午2:05
* @version: 1.0
*/
public class CostomExcetion extends RuntimeException{
public CostomExcetion(String message) {
super(message);
}
}
package cn.zkwf.takeout.common;
import cn.zkwf.takeout.exception.CostomExcetion;
import cn.zkwf.takeout.resulttype.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import java.sql.SQLIntegrityConstraintViolationException;
/**
* @description:
* @author: Sw_Ljb
* @PACKAGE_NAME:cn.zkwf.takeout.common
* @time: 2022/7/20 下午1:53
* @version: 1.0
*/
@Slf4j
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
public class GlobalExceptionHandler {
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
//1、打印日志
log.info("自定义全局异常处理正在运行!!");
//Duplicate entry 'zhangsan' for key 'employee.idx_username'
if (ex.getMessage().contains("Duplicate entry ")){
//2、判断当前抛出的异常是否为Duplicate entry
String[] strArray = ex.getMessage().split(" ");
//3、截取异常信息中重点错误信息
String msg= strArray[2] +"已经存在了!!";
//4、设置返回信息
return R.error(msg);
}
//4、设置返回信息
return R.error("未知错误!");
}
@ExceptionHandler(CostomExcetion.class)
public R<String> costomerExceptionHandler(CostomExcetion ex){
//1、打印日志
log.info("自定义全局异常处理正在运行!!==>自定义异常");
return R.error(ex.getMessage());
}
}
@Autowired
private DishService dishService;
@Autowired
private SetmealService setmealService;
/**
* 根据id删除分类,删除之前需要进行判断
* @param id
*/
@Override
public void remove(Long id) {
//添加查询条件,根据分类id进行查询菜品数据
LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
dishLambdaQueryWrapper.eq(Dish::getCategoryId,id);
int count1 = dishService.count(dishLambdaQueryWrapper);
//如果已经关联,抛出一个业务异常
if(count1 > 0){
throw new CustomException("当前分类下关联了菜品,不能删除");//已经关联菜品,抛出一个业务异常
}
//查询当前分类是否关联了套餐,如果已经关联,抛出一个业务异常
LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId,id);
int count2 = setmealService.count(setmealLambdaQueryWrapper);
if(count2 > 0){
throw new CustomException("当前分类下关联了套餐,不能删除");//已经关联套餐,抛出一个业务异常
}
//正常删除分类
super.removeById(id);
}
当前端点"修改"按钮的时候,当信息被修改后,点击保存按钮,页面在发送一个请求,去修改分类信息
回显:回显这一步的操作前端已经实现
/**
* 根据id修改分类信息
* @param category
* @return
*/
@PutMapping
public R<String> update(@RequestBody Category category){
log.info("修改分类信息:{}",category);
categoryService.updateById(category);
return R.success("修改分类信息成功");
}
文件上传三要素:
表单属性 | 取值 | 说明 |
---|---|---|
method | post | 必须选择post方式提交 |
enctype | multipart/form-data | 采用multipart格式上传文件 |
type | file | 使用input的file控件上传 |
文件下载两种表现形式
1). 获取文件的原始文件名, 通过原始文件名获取文件后缀
2). 通过UUID重新声明文件名, 文件名称重复造成文件覆盖
3). 创建文件存放目录
4). 将上传的临时文件转存到指定位置
1). 定义输入流,通过输入流读取文件内容
2). 通过response对象,获取到输出流
3). 通过response对象设置响应数据格式(image/jpeg)
4). 通过输入流读取文件数据,然后通过上述的输出流写回浏览器
5). 关闭资源
package cn.zkwf.takeout.controller;
import cn.zkwf.takeout.resulttype.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.UUID;
/**
* @description:
* http://localhost:7070/common/upload
* Request Method: POST
* @author: Sw_Ljb
* @PACKAGE_NAME:cn.zkwf.takeout.controller
* @time: 2022/7/22 下午2:54
* @version: 1.0
*/
@Slf4j
@RestController
@RequestMapping("/common")
public class FileController {
@Value(value = "${serverpath.serverImpPath}")
private String serverImpPath;
//Content-Disposition: form-data; name="file"; filename="0a3b3288-3446-4420-bbff-f263d0c02d8e.jpg"
@RequestMapping("/upload")
public R<String> upload(MultipartFile file){
log.info("上传路径{}:",serverImpPath);
//更保险的做法 将上传过来的照片再次创建一个uuid
String suffdex = file.getOriginalFilename().substring(file.getOriginalFilename().indexOf('.'));
String imgName = UUID.randomUUID().toString()+suffdex;
try {
//上传路径/Users/mac/Desktop/fliedemo/takeoutServerImg保险期间对目录进行一次判断
File imgfile = new File(serverImpPath);
if (!imgfile.exists()){
imgfile.mkdirs();
}
file.transferTo(new File(serverImpPath+"/"+imgName));
} catch (IOException e) {
e.printStackTrace();
}
return R.success(imgName);
}
/**
* 下载数据
* @param response
* @param name
*/
@GetMapping("/download")
public void download(HttpServletResponse response, String name){
log.info("下载文件{}",name);
try {
//输入流
FileInputStream fis = new FileInputStream(new File(serverImpPath+"/"+name));
log.info(serverImpPath+"/"+name);
//输出流
ServletOutputStream outputStream = response.getOutputStream();
//先读出来 再响应给前台
//设置响应类型
response.setContentType("image/jpeg");
//1、定义一个读出来存的字节数组
byte[] bytes = new byte[1024];
//2、定一个读多少
int len= 0;
while ((len = fis.read(bytes))!=-1){
//3、写会前台
outputStream.write(bytes,0,len);
outputStream.flush();
}
//4、记得关流,释放资源
outputStream.close();
fis.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
分析:新增菜品,其实就是将新增页面录入的菜品信息插入到dish表,如果添加了口味做法,还需要向dish_flavor表插入数据。所以在新增菜品时,涉及到两个表:dish/dish_flavor
1). 点击新建菜品按钮, 访问页面(backend/page/food/add.html), 页面加载时发送ajax请求,请求服务端获取菜品分类数据并展示到下拉框中
2). 页面发送请求进行图片上传,请求服务端将图片保存到服务器(上传功能已实现)
3). 页面发送请求进行图片下载,将上传的图片进行回显(下载功能已实现)
4). 点击保存按钮,发送ajax请求,将菜品相关数据以json形式提交到服务端
对于前端传递到服务端的数据分析:
如果使用菜品类Dish来封装,只能封装菜品的基本属性,flavors属性是无法封装的。这个时候,我们需要自定义一个实体类,然后继承自 Dish,并对Dish的属性进行拓展,增加 flavors 集合属性(内部封装DishFlavor)。
页面传递的菜品口味信息,仅仅包含name 和 value属性,缺少一个非常重要的属性dishId, 所以在保存完菜品的基本信息后,我们需要获取到菜品ID,然后为菜品口味对象属性dishId赋值。
具体逻辑如下:
①. 保存菜品基本信息 ;
②. 获取保存的菜品ID ;
③. 获取菜品口味列表,遍历列表,为菜品口味对象属性dishId赋值;
④. 批量保存菜品口味列表;
注意:由于需要进行了两次数据库的保存操作,操作了两张表,那么为了保证数据的一致性,我们需要在方法上加上注解 @Transactional来控制事务。Service层方法上加的注解@Transactional要想生效,需要在引导类上加上注解 @EnableTransactionManagement, 开启对事务的支持。
代码实现:
@Autowired
private DishFlavorService dishFlavorService;
/**
* 新增菜品,同时保存对应的口味数据
* @param dishDto
*/
@Transactional
public void saveWithFlavor(DishDto dishDto) {
//保存菜品的基本信息到菜品表dish
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());
//保存菜品口味数据到菜品口味表dish_flavor
dishFlavorService.saveBatch(flavors);
}
系统中的菜品数据很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。
在菜品列表展示时,除了菜品的基本信息(名称、售价、售卖状态、更新时间)外,还有两个字段略微特殊,第一个是图片字段 ,我们从数据库查询出来的仅仅是图片的名字,图片要想在表格中回显展示出来,就需要下载这个图片。第二个是菜品分类,这里展示的是分类名称,而不是分类ID,此时我们就需要根据菜品的分类ID,去分类表中查询分类信息,然后在页面展示。
1). 访问页面(backend/page/food/list.html)时,发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端,获取分页数据
2). 页面发送请求,请求服务端进行图片下载,用于页面图片展示
分析:
实体类 Dish 中,仅仅包含 categoryId, 不包含 categoryName,这里我们可以返回DishDto对象,在该对象中我们可以拓展一个属性 categoryName,来封装菜品分类名称。
具体逻辑为:
1). 构造分页条件对象
2). 构建查询及排序条件
3). 执行分页条件查询
4). 遍历分页查询列表数据,根据分类ID查询分类信息,从而获取该菜品的分类名称
5). 封装数据并返回
实现:
/**
* 菜品信息分页查询
* @param page
* @param pageSize
* @param name
* @return
*/
@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> queryWrapper = new LambdaQueryWrapper<>();
//添加过滤条件
queryWrapper.like(name != null,Dish::getName,name);
//添加排序条件
queryWrapper.orderByDesc(Dish::getUpdateTime);
//执行分页查询
dishService.page(pageInfo,queryWrapper);
//对象拷贝
BeanUtils.copyProperties(pageInfo,dishDtoPage,"records");
List<Dish> records = pageInfo.getRecords();
List<DishDto> list = records.stream().map((item) -> {
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(item,dishDto);
Long categoryId = item.getCategoryId();//分类id
//根据id查询分类对象
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);
}
数据库查询菜品信息时,获取到的分页查询结果 Page 的泛型为 Dish,而我们最终需要给前端页面返回的类型为 DishDto,所以这个时候就要进行转换,基本属性我们可以直接通过属性拷贝的形式对Page中的属性进行复制,而对于结果列表 records属性,是需要进行特殊处理的(需要封装菜品分类名称);
在菜品管理列表页面点击修改按钮,跳转到修改菜品页面,在修改页面回显菜品相关信息并进行修改,最后点击确定按钮完成修改操作。
1). 点击菜品列表的中的修改按钮,携带菜品id跳转至add.html
2). 进入add.html,页面发送ajax请求,请求服务端获取分类数据,用于菜品分类下拉框中数据展示
3). add.html获取id, 发送ajax请求,请求服务端,根据id查询当前菜品信息,用于菜品信息回显
4). 页面发送请求,请求服务端进行图片下载,用于页图片回显
5). 点击保存按钮,页面发送ajax请求,将修改后的菜品相关数据以json形式提交到服务端
我们只需要在这里实现两个功能即可:
1). 根据ID查询菜品及菜品口味信息
2). 修改菜品及菜品口味信息
具体逻辑为:
A. 根据ID查询菜品的基本信息
B. 根据菜品的ID查询菜品口味列表数据
C. 组装数据并返回
/**
* 根据id查询菜品信息和对应的口味信息
* @param id
* @return
*/
public DishDto getByIdWithFlavor(Long id) {
//查询菜品基本信息,从dish表查询
Dish dish = this.getById(id);
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(dish,dishDto);
//查询当前菜品对应的口味信息,从dish_flavor表查询
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DishFlavor::getDishId,dish.getId());
List<DishFlavor> flavors = dishFlavorService.list(queryWrapper);
dishDto.setFlavors(flavors);
return dishDto;
}
实现步骤:
点击保存按钮,页面发送ajax请求,将修改后的菜品相关数据以json形式提交到服务端。在修改菜品信息时需要注意,除了要更新dish菜品表,还需要更新dish_flavor菜品口味表;在该方法中,我们既需要更新dish菜品基本信息表,还需要更新dish_flavor菜品口味表。而页面再操作时,关于菜品的口味,有修改,有新增,也有可能删除,我们应该如何更新菜品口味信息呢,其实,无论菜品口味信息如何变化,我们只需要保持一个原则: 先删除,后添加。
@Override
@Transactional
public void updateWithFlavor(DishDto dishDto) {
//更新dish表基本信息
this.updateById(dishDto);
//清理当前菜品对应口味数据---dish_flavor表的delete操作
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper();
queryWrapper.eq(DishFlavor::getDishId,dishDto.getId());
dishFlavorService.remove(queryWrapper);
//添加当前提交过来的口味数据---dish_flavor表的insert操作
List<DishFlavor> flavors = dishDto.getFlavors();
flavors = flavors.stream().map((item) -> {
item.setDishId(dishDto.getId());
return item;
}).collect(Collectors.toList());
dishFlavorService.saveBatch(flavors);
}
后台系统中可以管理套餐信息,通过新增套餐功能来添加一个新的套餐,在添加套餐时需要选择当前套餐所属的套餐分类和包含的菜品,并且需要上传套餐对应的图片,在移动端会按照套餐分类来展示对应的套餐。
新增套餐,其实就是将新增页面录入的套餐信息插入到setmeal表,还需要向setmeal_dish表插入套餐和菜品关联数据。所以在新增套餐时,涉及到两个表:
1). 点击新建套餐按钮,访问页面(backend/page/combo/add.html),页面加载发送ajax请求,请求服务端获取套餐分类数据并展示到下拉框中
2). 访问页面(backend/page/combo/add.html),页面加载时发送ajax请求,请求服务端获取菜品分类数据并展示到添加菜品窗口中
3). 当点击添加菜品窗口左侧菜单的某一个分类, 页面发送ajax请求,请求服务端,根据菜品分类查询对应的菜品数据并展示到添加菜品窗口中
4). 页面发送请求进行图片上传,请求服务端将图片保存到服务器
5). 页面发送请求进行图片下载,将上传的图片进行回显
6). 点击保存按钮,发送ajax请求,将套餐相关数据以json形式提交到服务端
分析发送的请求有5个,分别是:
A. 根据传递的参数,查询套餐分类列表
B. 根据传递的参数,查询菜品分类列表
C. 图片上传
D. 图片下载展示
E. 根据菜品分类ID,查询菜品列表
F. 保存套餐信息
/**
* 根据条件查询对应的菜品数据
* @param dish
* @return
*/
@GetMapping("/list")
public R<List<Dish>> list(Dish dish){
//构造查询条件
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(dish.getCategoryId() != null ,Dish::getCategoryId,dish.getCategoryId());
//添加条件,查询状态为1(起售状态)的菜品
queryWrapper.eq(Dish::getStatus,1);
//添加排序条件
queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
List<Dish> list = dishService.list(queryWrapper);
return R.success(list);
}
分析: 在进行套餐信息保存时,前端提交的数据,不仅包含套餐的基本信息,还包含套餐关联的菜品列表数据 setmealDishes。所以这个时候我们使用Setmeal就不能完成参数的封装了,我们需要在Setmeal的基本属性的基础上,再扩充一个属性 setmealDishes 来接收页面传递的套餐关联的菜品列表,而我们在准备工作中,导入进来的SetmealDto能够满足这个需求。
SetmealServiceImpl.saveWithDish():
具体逻辑:
A. 保存套餐基本信息
B. 获取套餐关联的菜品集合,并为集合中的每一个元素赋值套餐ID(setmealId)
C. 批量保存套餐关联的菜品集合
代码实现:
/**
* 新增套餐,同时需要保存套餐和菜品的关联关系
* @param setmealDto
*/
@Transactional
public void saveWithDish(SetmealDto setmealDto) {
//保存套餐的基本信息,操作setmeal,执行insert操作
this.save(setmealDto);
List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
setmealDishes.stream().map((item) -> {
item.setSetmealId(setmealDto.getId());
return item;
}).collect(Collectors.toList());
//保存套餐和菜品的关联信息,操作setmeal_dish,执行insert操作
setmealDishService.saveBatch(setmealDishes);
}
1). 访问页面(backend/page/combo/list.html),页面加载时,会自动发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端,获取分页数据
2). 在列表渲染展示时,页面发送请求,请求服务端进行图片下载,用于页面图片展示
基本逻辑:
1). 构建分页条件对象
2). 构建查询条件对象,如果传递了套餐名称,根据套餐名称模糊查询, 并对结果按修改时间降序排序
3). 执行分页查询
4). 由于查询套餐信息时, 只包含套餐的基本信息, 并不包含套餐的分类名称, 所以在这里查询到套餐的基本信息后, 还需要根据分类ID(categoryId), 查询套餐分类名称(categoryName),并最终将套餐的基本信息及分类名称信息封装到SetmealDto。组装数据并返回
代码实现:
/**
* 套餐分页查询
* @param page
* @param pageSize
* @param name
* @return
*/
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name){
//分页构造器对象
Page<Setmeal> pageInfo = new Page<>(page,pageSize);
Page<SetmealDto> dtoPage = new Page<>();
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
//添加查询条件,根据name进行like模糊查询
queryWrapper.like(name != null,Setmeal::getName,name);
//添加排序条件,根据更新时间降序排列
queryWrapper.orderByDesc(Setmeal::getUpdateTime);
setmealService.page(pageInfo,queryWrapper);
//对象拷贝
BeanUtils.copyProperties(pageInfo,dtoPage,"records");
List<Setmeal> records = pageInfo.getRecords();
List<SetmealDto> list = records.stream().map((item) -> {
SetmealDto setmealDto = new SetmealDto();
//对象拷贝
BeanUtils.copyProperties(item,setmealDto);
//分类id
Long categoryId = item.getCategoryId();
//根据分类id查询分类对象
Category category = categoryService.getById(categoryId);
if(category != null){
//分类名称
String categoryName = category.getName();
setmealDto.setCategoryName(categoryName);
}
return setmealDto;
}).collect(Collectors.toList());
dtoPage.setRecords(list);
return R.success(dtoPage);
}
在套餐管理列表页面,点击删除按钮,可以删除对应的套餐信息。也可以通过复选框选择多个套餐,点击批量删除按钮一次删除多个套餐。注意,对于状态为售卖中的套餐不能删除,需要先停售,然后才能删除
1). 点击删除, 删除单个套餐时,页面发送ajax请求,根据套餐id删除对应套餐
2). 删除多个套餐时,页面发送ajax请求,根据提交的多个套餐id删除对应套餐
分析:开发删除套餐功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可,一次请求为根据ID删除,一次请求为根据ID批量删除。观察删除单个套餐和批量删除套餐的请求信息可以发现,两种请求的地址和请求方式都是相同的,不同的则是传递的id个数,所以在服务端可以提供一个方法来统一处理。
分析:删除套餐时, 我们不仅要删除套餐, 还要删除套餐与菜品的关联关系。
实现:SetmealServiceImpl.removeWithDish():
具体的逻辑:
A. 查询该批次套餐中是否存在售卖中的套餐, 如果存在, 不允许删除
B. 删除套餐数据
C. 删除套餐关联的菜品数据
/**
* 删除套餐,同时需要删除套餐和菜品的关联数据
* @param ids
*/
@Transactional
public void removeWithDish(List<Long> ids) {
//select count(*) from setmeal where id in (1,2,3) and status = 1
//查询套餐状态,确定是否可用删除
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper();
queryWrapper.in(Setmeal::getId,ids);
queryWrapper.eq(Setmeal::getStatus,1);
int count = this.count(queryWrapper);
if(count > 0){
//如果不能删除,抛出一个业务异常
throw new CustomException("套餐正在售卖中,不能删除");
}
//如果可以删除,先删除套餐表中的数据---setmeal
this.removeByIds(ids);
//delete from setmeal_dish where setmeal_id in (1,2,3)
LambdaQueryWrapper<SetmealDish> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.in(SetmealDish::getSetmealId,ids);
//删除关系表中的数据----setmeal_dish
setmealDishService.remove(lambdaQueryWrapper);
}