1.前端静态资源导入
1.存放:前端静态资源存放的一个位置
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);
- 从ThreadLocal中获取当前登录用户的ID
BaseContext.getCurrentId();
10.文件上传下载(后端)
1.文件上传:指将本地图片、视频、音频等文件上传到服务器,供其他用户浏览或下载的过程。
要求后端接收参数是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);
}