SpringBoot实战——瑞吉外卖
数据库创建:
表功能:
address_book:地址表
category:菜品和套餐表
dish: 菜品表
dish_flavor: 菜品口味关系表
employee: 员工表
order_detail: 订单明细
orders:订单表
setmeal:套餐表
setmeal_dish:套餐菜品关系表
shopping_cart:购物车
user:用户表
项目创建 不必多言
pom配置: 这里其实版本号不一定是我的这个,各位可以自行在maven里导入适合自己的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 <?xml version="1.0" encoding="UTF-8" ?> <project xmlns ="http://maven.apache.org/POM/4.0.0" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" > <modelVersion > 4.0.0</modelVersion > <parent > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-parent</artifactId > <version > 2.4.5</version > <relativePath /> </parent > <groupId > com.Lf</groupId > <artifactId > reggie_take_out</artifactId > <version > 1.0-SNAPSHOT</version > <properties > <java.version > 1.8</java.version > </properties > <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > <scope > compile</scope > </dependency > <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <version > 3.4.2</version > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <version > 1.18.20</version > </dependency > <dependency > <groupId > com.alibaba</groupId > <artifactId > fastjson</artifactId > <version > 1.2.76</version > </dependency > <dependency > <groupId > commons-lang</groupId > <artifactId > commons-lang</artifactId > <version > 2.6</version > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <scope > runtime</scope > </dependency > <dependency > <groupId > com.alibaba</groupId > <artifactId > druid-spring-boot-starter</artifactId > <version > 1.1.23</version > </dependency > </dependencies > <build > <plugins > <plugin > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-maven-plugin</artifactId > <version > 2.4.5</version > </plugin > </plugins > </build > </project >
application.yml配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 server: port: 81 spring: application: name: reggie_take_out datasource: druid: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/reggie?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true username: root password: 123456 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: ASSIGN_ID
创建启动类: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.lf.reggie;import lombok.extern.slf4j.Slf4j;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@Slf4j @SpringBootApplication public class ReggieApplication { public static void main (String[] args) { SpringApplication.run(ReggieApplication.class,args); log.info("原神,启动!" ); } }
导入静态页面:
本次实战不要求前端页面编写,所以直接导入,资料来源与黑马程序员
功能开发 登录功能: 登录页面:
前端发送数据 -> controller接收 -> 调用service ->Mapper -> 数据库
流程还是老样子,不了解的可以看本网站springboot入门笔记
创建实体类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 package com.lf.reggie.domain;import com.baomidou.mybatisplus.annotation.FieldFill;import com.baomidou.mybatisplus.annotation.TableField;import lombok.Data;import java.io.Serializable;import java.time.LocalDateTime;@Data public class Employee implements Serializable { private static final long serialVersionUID = 1L ; private Long id; private String username; private String name; private String password; private String phone; private String sex; private String idNumber; private Integer status; private LocalDateTime createTime; private LocalDateTime updateTime; @TableField(fill = FieldFill.INSERT) private Long createUser; @TableField(fill = FieldFill.INSERT_UPDATE) private Long updateUser; }
mapper 1 2 3 4 5 6 7 8 9 10 11 12 import com.baomidou.mybatisplus.core.mapper.BaseMapper;import com.lf.reggie.domain.Employee;import org.apache.ibatis.annotations.Mapper;@Mapper public interface EmployeeMapper extends BaseMapper <Employee> {}
service 1 2 3 4 5 6 7 8 9 10 import com.baomidou.mybatisplus.extension.service.IService;import com.lf.reggie.domain.Employee;public interface EmployeeService extends IService <Employee> {}
service实现类 1 2 3 4 5 6 7 8 9 10 11 12 13 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;import com.lf.reggie.domain.Employee;import com.lf.reggie.mapper.EmployeeMapper;import org.springframework.stereotype.Service;@Service public class EmployeeServiceImpl extends ServiceImpl <EmployeeMapper, Employee> implements EmployeeService {}
通用返回类型R 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import lombok.Data;import java.util.HashMap;import java.util.Map;@Data public class R <T> { private Integer code; 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 ; } }
登录功能controller 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 package com.lf.reggie.controller;@Slf4j @RestController @RequestMapping("/employee") public class EmployeeController { @Autowired private EmployeeService employeeService; @PostMapping("/login") public R<Employee> login (HttpServletRequest request, @RequestBody Employee employee) { String password = employee.getPassword(); password = DigestUtils.md5DigestAsHex(password.getBytes()); LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper <>(); queryWrapper.eq(Employee::getUsername, employee.getUsername()); Employee emp = employeeService.getOne(queryWrapper); if (emp == null ) { return R.error("用户名不存在" ); } if (!emp.getPassword().equals(password)) { return R.error("密码错误" ); } if (emp.getStatus() == 0 ) { return R.error("该用户已被禁用" ); } HttpSession session = request.getSession(); session.setAttribute("employee" , emp.getId()); return R.success(emp); } }
退出功能 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @PostMapping("/logout") public R<String> logout (HttpServletRequest request) { request.removeAttribute("employee" ); return R.success("退出成功" ); }
员工操作 过滤器
过滤器是为了防止用户不登录而是通过网页url直接访问页面,有安全风险
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 package com.lf.reggie.filter;@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; String requestURI = request.getRequestURI(); System.out.println("访问路径" + requestURI); String[] urls = {"/employee/login" , "/employee/logout" , "/backend/**" , "/front/**" ,"/favicon.ico" }; boolean b = checkUrl(urls, requestURI); System.out.println(b); if (b) { filterChain.doFilter(request, response); return ; } HttpSession session = request.getSession(); Object employee = session.getAttribute("employee" ); if (employee != null ) { filterChain.doFilter(request, response); return ; } else { response.getWriter().write((JSON.toJSONString(R.error("NOTLOGIN" )))); return ; } } boolean checkUrl (String[] urls, String requestUrl) { for (String url : urls) { boolean match = PATH_MATCHER.match(url, requestUrl); if (match) return true ; } return false ; } }
添加员工操作 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @PostMapping public R<String> save (HttpServletRequest request,@RequestBody Employee employee) { log.info("新增员工信息{}" ,employee); employee.setPassword(DigestUtils.md5DigestAsHex("123456" .getBytes())); employee.setCreateTime(LocalDateTime.now()); employee.setUpdateTime(LocalDateTime.now()); HttpSession session = request.getSession(); Long id = (Long) session.getAttribute("employee" ); employee.setCreateUser(id); employee.setUpdateUser(id); employeeService.save(employee); return R.success("新增员工成功" ); }
关于controller的访问url你得看前端html页面访问的url,根据它写我们后端的@Post/Getmapping
全局异常捕获 当我们重复添加员工信息,数据库就会给我们报错,因为我们的username字段是唯一的
这种情况我们可以选择利用try - catch结构来捕获异常,但这样我们要在每一个可能抛出异常的代码上写一遍,未免太过麻烦,我们就可以选择全局异常捕获
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @ControllerAdvice(annotations = {RestController.class, Controller.class}) @ResponseBody @Slf4j public class GlobalExceptionHandler { @ExceptionHandler(SQLIntegrityConstraintViolationException.class) public R<String> exceptionHandler (SQLIntegrityConstraintViolationException ex) { log.error(ex.getMessage()); if (ex.getMessage().contains("Duplicate entry" )){ return R.error("员工已存在,添加失败" ); } return R.error("未知错误" ); } }
分页查询操作 要想实现分页查询,首先需要创建一个MP配置类,用来配置分页查询拦截器,具体实现如下,在我之前的学习笔记中也有相关介绍
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor () { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor (); interceptor.addInnerInterceptor(new PaginationInnerInterceptor ()); return interceptor; } }
controller:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @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.isNotEmpty(name), Employee::getName, name); queryWrapper.orderByDesc(Employee::getUpdateTime); employeeService.page(pageInfo, queryWrapper); return R.success(pageInfo); }
用户禁用启用 1 2 3 4 5 6 7 8 9 10 11 12 13 @PutMapping public R<String> updata (HttpServletRequest request,@RequestBody Employee employee) { HttpSession session = request.getSession(); Long empID = (Long)session.getAttribute("employee" ); employee.setUpdateTime(LocalDateTime.now()); employee.setUpdateUser(empID); employeeService.updateById(employee); return R.success("成功" ); }
这样写会有点小问题,因为我们修改是按照前端传来的id来定位对哪一行修改的,但是前端传来的id如果本身太长,会造成失真,也就是id会显示不正确,不正确的id自然也不能正确找到要修改的数据,所以如果只写上面的controller是不够的,还需要配置新的json转换器
转换器(JacksonObjectMapper)代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 package com.lf.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;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默认的数据转换器集合中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 package com.lf.reggie.config;import com.lf.reggie.common.JacksonObjectMapper;import lombok.extern.slf4j.Slf4j;import org.springframework.context.annotation.Configuration;import org.springframework.http.converter.HttpMessageConverter;import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;import java.util.List;@Slf4j @Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void extendMessageConverters (List<HttpMessageConverter<?>> converters) { MappingJackson2HttpMessageConverter mjhmc = new MappingJackson2HttpMessageConverter (); mjhmc.setObjectMapper(new JacksonObjectMapper ()); converters.add(0 ,mjhmc); } }
这样才算大功告成
注意,这里你如果跟着课程走,他的配置类可能是继承了 WebMvcConfigurationSupport 这个类,然后你可能出现404错误,因为WebMvcConfigurationSupport存在时会自动无效化mvc的默认静态资源路径(我用的是implements WebMvcConfigurer 不覆盖默认),但实际上只要你把静态路径放到/resources/static下,这里是默认静态访问路径,根本就不需要自己用WebMvcConfigurationSupport配置,我不太理解为什么课程要自己配置,多了很多麻烦
公共字段自动填充 1 2 3 4 5 6 7 8 9 10 11 @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;
在我们的employee实体类上,可以看到有这么几条属性,上面的注解@TableField(fill = FieldFill.INSERT)&
@TableField(fill = FieldFill.INSERT_UPDATE) 是MP包为我们提供的可以对公共字段插入或更新时,统一进行操作,而不用我们单独设置每个对象的公共属性,例如更新时间,我们可以利用这个注解在进行数据库更新时,自动为更新后的数据加入更新时间
写我们自定义的字段补全类,实现接口:implements MetaObjectHandler
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Component public class MyMetaObjecthandler implements MetaObjectHandler { @Override public void insertFill (MetaObject metaObject) { metaObject.setValue("createTime" , LocalDateTime.now()); metaObject.setValue("updateTime" , LocalDateTime.now()); System.out.println(BaseContext.getCurrentId()); metaObject.setValue("createUser" , BaseContext.getCurrentId()); metaObject.setValue("updateUser" , BaseContext.getCurrentId()); } @Override public void updateFill (MetaObject metaObject) { metaObject.setValue("updateTime" , LocalDateTime.now()); metaObject.setValue("updateUser" , BaseContext.getCurrentId()); } }
这里注意,我们在写controller时可以利用request获得session进而获得共享的数据,但在普通java类中没法这么操作,但我们可以利用线程共享,且数据只能在同一线程中使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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(); } }
最后再在filter中加入已登录用户的id
1 2 3 4 5 if (employee != null ) { BaseContext.setCurrentId(employee); filterChain.doFilter(request, response);
成功,现在我们不需要手动设置时间,操作人等,他会自动设置
菜品操作 实体类&mapper&服务接口&接口实现 实体类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 @Data public class Category implements Serializable { private static final long serialVersionUID = 1L ; private Long id; private Integer type; private String name; private Integer sort; @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; private Integer isDeleted; }
mapper:
1 2 3 4 5 6 7 8 @Mapper public interface CategoryMapper extends BaseMapper <Category> {}
服务接口:
1 2 3 4 5 6 7 public interface CategoryService extends IService <Category> {}
接口实现:
1 2 3 4 5 6 7 8 9 @Service public class CategoryServiceImpl extends ServiceImpl <CategoryMapper, Category> implements CategoryService {}
新增菜品 1 2 3 4 5 6 7 8 @PostMapping public R<String> save (@RequestBody Category category) { log.info("category:{}" ,category); categoryService.save(category); return R.success("新增成功" ); }
成功
分页操作 可以类比着之前的写
1 2 3 4 5 6 7 8 9 10 11 12 13 @GetMapping("/page") public R<Page> page (int page,int pageSize) { Page<Category> p = new Page <>(page, pageSize); LambdaQueryWrapper<Category> lambdaQueryWrapper = new LambdaQueryWrapper <>(); lambdaQueryWrapper.orderByDesc(Category::getSort); categoryService.page(p,lambdaQueryWrapper); return R.success(p); }
完成
删除分类
这个删除肯定不能直接remove这么简单了,按照业务需求,如果该分类下有东西,那我们就不能直接将其删除,所以在删除之前,我们应该查询该分类下有没有菜品(套餐)
service自定义删除方法
1 2 3 4 5 6 7 8 9 public interface CategoryService extends IService <Category> { public void remove (Long id) ; }
实现类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 @Service public class CategoryServiceImpl 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 <>(); 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); }
自定义删除异常:
1 2 3 4 5 6 7 8 9 10 11 public class CustomException extends RuntimeException { public CustomException (String message) { super (message); } }
还需要用到我们之前为了捕获用户重复加入写的全局异常捕捉(加一个新的捕捉方法):
1 2 3 4 5 6 7 8 9 10 11 @ExceptionHandler(CustomException.class) public R<String> exceptionHandler (CustomException ex) { log.error(ex.getMessage()); return R.error(ex.getMessage()); }
完成
修改分类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @PutMapping public R<String> update (@RequestBody Category category) { log.info("修改分类信息:{}" ,category); categoryService.updateById(category); return R.success("修改分类成功" ); }
这个比较简单,不做太多说明
OK
文件上传 文件上传的后端处理其实在springboot中得到了极大简化,只需要用到MultipartFile
具体代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @PostMapping("/upload") public R<String> upload (MultipartFile file) { log.info(file.toString()); String filename = UUID.randomUUID().toString(); try { file.transferTo(new File (basepath + filename)); } catch (IOException e) { R.error("上传失败" ); } return R.success(filename); }
配置文件中的基础路径:
1 2 reggie: path: D:\java\reggie_take_out\src\main\img\
文件下载(上传回显) 实现了文件上传功能后,一般还会追加一个上传回显功能,能够让用户直接看到自己上传的图片出现在上传框内,还能让其他用户也看到上传的图片。
代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @GetMapping("/download") public void download (String name, HttpServletResponse response) { try { FileInputStream fileInputStream = new FileInputStream (new File (basepath + name)); ServletOutputStream outputStream = response.getOutputStream(); response.setContentType("image/jpeg" ); byte [] bytes = new byte [1024 ]; int len = 0 ; while ((len = fileInputStream.read(bytes)) != -1 ){ outputStream.write(bytes,0 ,len); outputStream.flush(); } outputStream.close(); fileInputStream.close(); } catch (Exception e) { e.printStackTrace(); } }
这里传入的name其实是我们上传后返回给页面的文件名,然后利用fileInputStream将文件下载到本地并回显,最后关闭流。
新增菜品 由于新增菜品涉及到对两张表(菜品,口味)进行修改,因此我们常规的菜品和口味实体类无法同时封装新增的菜品信息,因此我们需要用的一个数据传输对象(dto)来支持前端到后端的数据传输,类似于这样:
1 2 3 4 5 6 7 8 9 @Data public class DishDto extends Dish { private List<DishFlavor> flavors = new ArrayList <>(); private String categoryName; private Integer copies; }
有了这个整合了dish和dishflavor两个类的数据传输对象,我们能够将前端传入的数据完全封装
1 2 3 4 5 6 7 8 9 10 11 12 13 @PostMapping public R<String> save (@RequestBody DishDto dishDto) { log.info(dishDto.toString()); dishService.saveWithFlavor(dishDto); return R.success("新增菜品成功" ); }
记得加@RequestBody
然后我们需要新增一条新的业务方法,能够支持同时修改dish和flavor两张表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Transactional public void saveWithFlavor (DishDto dishDto) { this .save(dishDto); Long id = dishDto.getId(); List<DishFlavor> flavors = dishDto.getFlavors(); for (DishFlavor item:flavors ) { item.setDishId(id); } dishFlavorService.saveBatch(flavors); }
补充PO、VO、DAO、BO、DTO、POJO解释 PO PO是“Persistent Object”的缩写,意为“持久化对象”。它通常用于表示数据库中的一条记录,即一组相关的数据。PO是由ORM(对象关系映射)框架生成或手动创建的Java对象,它们通常具有与数据库中的表相同的字段和数据类型。在Java开发中,PO常常被用作DAO(数据访问对象)层的数据模型,以及和数据库交互的对象。PO对象中的字段与数据库中的列相对应,每一行数据对应一个PO对象,PO对象中的字段值就是对应列的值。
VO VO是“Value Object”的缩写,意为“值对象”。VO通常用于表示程序中的某个值或者一组相关的值,例如用户的姓名、年龄、地址等等。VO通常是一个不可变对象,也就是说,它的值在创建之后就不能再修改。在Java开发中,VO对象通常用于在不同层之间传递数据,例如在Controller层和Service层之间传递数据。VO对象和PO对象类似,但是它们的作用不同。VO通常是从PO对象中提取出来的一部分数据,用于展示和传递给前端界面。
DAO DAO是“Data Access Object”的缩写,意为“数据访问对象”。DAO层是整个应用程序中与数据库交互的核心部分。DAO层负责将数据库中的数据转换成Java对象,并将Java对象的数据保存到数据库中。DAO层的主要作用是隔离上层业务逻辑和下层数据访问细节。在Java开发中,通常使用Hibernate等ORM框架来实现DAO层。DAO层的主要任务是实现数据的增删改查等基本操作,同时提供一些高级查询功能。
BO BO是“Business Object”的缩写,意为“业务对象”。BO通常用于表示某个业务逻辑的实体或者模型。BO通常包含一些业务逻辑和方法,例如计算某些值、验证数据、调用其他服务等等。在Java开发中,BO对象通常由Service层或者Facade层来创建,并且它们通常包含一些业务逻辑的实现,以及对数据的操作。BO通常是针对具体的业务场景而设计的,它们是具有业务含义的实体。
DTO DTO是“Data Transfer Object”的缩写,意为“数据传输对象”。DTO通常用于在不同层之间传输数据,例如在Controller层和Service层之间传输数据。DTO对象通常包含一些简单的数据结构,例如字符串、整数、布尔值等等。在Java开发中,DTO对象通常由Controller层或者Service层来创建,并且它们通常是不可变的。
POJO POJO是“Plain Old Java Object”的缩写,意为“简单的Java对象”。POJO通常指的是一个没有任何限制、继承或实现特定接口的普通Java对象。POJO对象通常是一种轻量级的Java对象,没有任何框架或者注解的依赖。在Java开发中,POJO对象通常用于表示简单的数据模型或者数据传输对象。
修改菜品 首先页面信息的修改一定离不开的就是回显数据
只有回显数据,才能看到我们即将修改的信息
回显的实现并不困难,从前端角度来说,只需要在页面加载完毕后自动向后端发送一次请求,调用后端的get方法获得商品数据,然后通过json发回前端进行显示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Transactional public DishDto getByIdWithFlavor (Long id) { DishDto dishDto = new DishDto (); Dish dish = dishService.getById(id); LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper <>(); queryWrapper.eq(DishFlavor::getDishId,id); List<DishFlavor> flavors = dishFlavorService.list(queryWrapper); BeanUtils.copyProperties(dish,dishDto); dishDto.setFlavors(flavors); return dishDto; }
代码实现也都是之前学过的内容
修改的代码也没什么好说的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Transactional public void updateWithFlavor (DishDto dishdto) { dishService.updateById(dishdto); LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper <>(); queryWrapper.eq(DishFlavor::getDishId,dishdto.getId()); dishFlavorService.remove(queryWrapper); dishFlavorService.saveBatch(dishdto.getFlavors()); }
以上方法均新增在service接口
1 2 3 4 5 6 7 8 9 10 11 public interface DishService extends IService <Dish> { public void saveWithFlavor (DishDto dishDto) ; public DishDto getByIdWithFlavor (Long id) ; public void updateWithFlavor (DishDto dishdto) ; }
套餐 新增套餐 由于新增套餐设计到多表操作,所以先在接口中定义新的方法:
1 2 public void saveWithMeal (SetmealDto setmealDto) ;
并在接口实现类中手动实现该方法:
1 2 3 4 5 6 7 8 9 10 11 12 @Override public void saveWithMeal (SetmealDto setmealDto) { this .save(setmealDto); List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes(); for (SetmealDish i:setmealDishes){ i.setSetmealId(setmealDto.getId()); } setMealDishService.saveBatch(setmealDishes); }
这样在controller中直接调用该方法,就能实现新增套餐并将套餐和餐品联系起来。
分页展示套餐 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 @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> lambdaQueryWrapper = new LambdaQueryWrapper <>(); lambdaQueryWrapper.like(name != null ,Setmeal::getName,name); lambdaQueryWrapper.orderByDesc(Setmeal::getUpdateTime); setMealService.page(pageInfo,lambdaQueryWrapper); BeanUtils.copyProperties(pageInfo,dtopage,"records" ); List<Setmeal> pageInfoRecords = pageInfo.getRecords(); ArrayList<SetmealDto> records = new ArrayList <>(); for (Setmeal item : pageInfoRecords) { SetmealDto sd = new SetmealDto (); BeanUtils.copyProperties(item,sd); sd.setCategoryName( categoryService.getById(item.getCategoryId()).getName()); records.add(sd); } dtopage.setRecords(records); return R.success(dtopage); }
分页操作我们之前已经进行过很多次了,具体操作不再赘述,但此处有一个细节,因为前端页面要求显示套餐分类,而普通的
Setmeal类中没有套餐分类属性,所以我们需要setmealdto。
效果展示:
用户手机端操作 验证码登录功能 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 @PostMapping("/sendMsg") public R<String> sendMsg (@RequestBody User user, HttpSession session) { String phone = user.getPhone(); String code = ValidateCodeUtils.generateValidateCode(4 ).toString(); log.info("验证码{}" ,code); session.setAttribute(phone,code); return R.success("短信发送成功!" ); } @PostMapping("/login") public R<User> login (@RequestBody Map map,HttpSession session) { log.info(map.toString()); String phone = map.get("phone" ).toString(); String code = map.get("code" ).toString(); Object session_code = session.getAttribute(phone); if (session_code != null && session_code.equals(code)){ LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper <>(); lambdaQueryWrapper.eq(User::getPhone,phone); User user = userService.getOne(lambdaQueryWrapper); if (user != null ){ }else { user = new User (); user.setPhone(phone); user.setStatus(1 ); userService.save(user); } session.setAttribute("user" ,user); return R.success(user); } return R.error("错误!" ); } }
生成随机码函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package com.lf.reggie.utils;import java.util.Random;public class ValidateCodeUtils { public static Integer generateValidateCode (int length) { Integer code = null ; if (length == 4 ){ code = new Random ().nextInt(9999 ); if (code < 1000 ){ code = code + 1000 ; } }else if (length == 6 ){ code = new Random ().nextInt(999999 ); if (code < 100000 ){ code = code + 100000 ; } }else { throw new RuntimeException ("只能生成4位或6位数字验证码" ); } return code; } public static String generateValidateCode4String (int length) { Random rdm = new Random (); String hash1 = Integer.toHexString(rdm.nextInt()); String capstr = hash1.substring(0 , length); return capstr; } }
简单说就是:前端收到发送验证码请求后,后端调用专门的api接通网络服务给前端传入的手机号发短信验证码(此处用log代替了),用户收到后,将验证码填入前端的验证码窗并点击登录,后端的登录方法接收到phone和code后先是根据phone从session(事先存入的)中取得对应code,然后进行对比,一致的话在进行之后的操作(判断是否新用户等)
说明:后面的功能基本上和之前的套路一致,笔记先不做了,之后看看有时间复习的时候做。 项目优化(redis) 环境搭建 pom:
1 2 3 4 5 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-redis</artifactId > </dependency >
application(redis连接信息):
1 2 3 4 5 redis: host: * port: 6379 password: * database: 0
缓存验证码 之前的验证码是保存在session中的
优化一下,将其保存在redis中
获取验证码后,通过set存入redis
1 2 stringRedisTemplate.opsForValue().set(phone,code,5 , TimeUnit.MINUTES);
登录时get获取验证码
1 2 String session_code = stringRedisTemplate.opsForValue().get(phone);
登陆成功删除
1 redisTemplate.delete(phone);
缓存菜品 当高并发模式下,每次调用菜品的list方法都要查询数据库,这样无疑增加了资源消耗,如果将
查出的菜品放到缓存中,在查找前先找缓存,如果缓存中存在需要的,则直接调用,另外缓存中的数据在修改或增加的时候需要删除,为的是在下次查找时放入新数据。
1 2 3 4 5 String key = "key" + dish.getCategoryId() + "_" + dish.getStatus(); dishDtos = (List<DishDto>) redisTemplate.opsForValue().get(key);
SpringCache 这是一个靠注解就能实现缓存的框架(不用自己手动存入redis)
常用注解:
@EnableCaching 放在启动类上,代表开启缓存注释功能
@Cacheable 有缓存直接返回,不调用方法(查找) 它还有个condition = “”,里面可以塞判断条件,比如在查找数据时在数据库找不到,这样缓存里面就会留下一个查询条件的key还有个为null的value,所以可以condition=’’#result != null’
@CachePut 放在方法上(一般是保存数据,把新增的数据加到缓存中)
使用方法:
1 @CachePut(value = "",key = "")
value: 缓存名称(相当于一个分类,每个分类下可以有多个key,比如用户缓存,下面可以有多个用户)
key:就是key值,用来查找value
这个key值可以通过一些特殊方法来取比如:#result 代表当前方法的返回值(比如返回一个user对象,你可以#result.id)#root通过这个你可以取到当前的方法名,比如(#root.methodName)还可以
#+参数名,然后用参数名里面的属性值 比如参数里面有个user(需要添加的新数据),你就可以这样写:key = #user.id,把用户id作为key
@CacheEvict 同理删除哪个,先指定name,再指定key(#user.id)(更新数据)
切换缓存产品 上一个说的springcache是spring自带的,他不需要引入除spring原始jar包之外的包,而我们如果想用redis作为我们的缓存服务产品(springCache如果项目重启,则缓存清空,redis可以保存本地一段时间),就需要引入专门的jar包
1 2 3 4 5 6 7 8 9 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-cache</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-redis</artifactId > </dependency >
然后可以在配置中设置过期时间
1 2 3 cache: redis: time-to-live: ***(单位ms)
注意: 用到cacheable将方法返回值缓存时,方法返回值需要能被序列化(一般返回的是自拟类的话就不可以,需要在类上继承 implements Serializable )