外卖点单后台管理系统
外卖后台管理系统
项目环境搭建
后端:Java 1.8.0_281,apache-maven-3.8.4,mysql-connector-java-8.0.29
pom.xml配置文件
<?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 https://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.7.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.weiwei.reggie</groupId>
<artifactId>reggie_take_out</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>reggie_take_out</name>
<description>reggie_take_out</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--阿里云短信服务-->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.5.16</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.23</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</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>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
配置类
-
springboot中的目录结构中的static和templates:
static:默认存放静态资源文件,可以直接通过访问地址进行访问,或者通过控制器方法
templates :默认存放动态资源文件,需要引入thymeleaf引擎,然后通过控制器方法进行访问
静态页面的return默认是跳转到/static/目录下,当在pom.xml中引入了thymeleaf组件,动态跳转会覆盖默认的静态跳转,默认就会跳转到/templates/下,注意看两者return代码也有区别,动态没有html后缀。
-
这里没有将资源文件放在static和templates中,而是将backend(网页端)和front(移动端)以包的方式将页面放在了resource下,然后通过配置信息类去进行资源绑定的映射。
@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
/**
*@author wei
*@Description 设置静态资源映射
*@Date 14:50 2022/5/21
*@Param * @param null
*@Return
**/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
log.info("开始进行静态资源映射....");
registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
}
}
-
配置R类(前后端交互协议的书写)
/** *@author wei *@Description 通过返回结果,服务端响应的数据最终会封装到此对象 *@Date 15:45 2022/5/21 *@Param * @param null *@Return **/ @Data public class R<T> implements Serializable { 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; } }
过滤器
主要作用:Filter对用户请求进行预处理,接着将请求交给Servlet进行处理并生成响应,最后Filter再对服务器响应进行后处理。Filter过滤器(超详细)
@Slf4j
@WebFilter(filterName = "LoginCheckFilter", urlPatterns = "/*")
public class LoginCheckFilter implements Filter {
//路径匹配器
private final static 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();
log.info("拦截到请求:{}",requestURI);
//不需要被过滤器处理的请求路径
String[] urls = {"/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"));
filterChain.doFilter(request,response);
return;
}
log.info("用户未登录");
//如果未登录则返回未登录结果,通过输出流方式向客户端页面响应数据,响应数据的内容与前端代码的拦截器内容相同:request.js的47行
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
return;
}
/**
* 获取请求路径进行与不需要被过滤器处理的请求路径,如果返回turn,则匹配成功直接放行
*
* @param requestURIs
* @param RequestURI
* @return
*/
public boolean check(String[] requestURIs, String RequestURI) {
for (String url : requestURIs) {
boolean match = PATH_MATCHER.match(url, RequestURI);
if (match)
return true;
}
return false;
}
}
这里通过设置过滤器来进行登录的校验和页面数据的保护。
员工管理模块
新增员工功能
需求分析:
前端通过表单提交的方式向服务器发送请求携带参数(json格式),请求的url为:
然后后端请求的url在controller层编写代码,提交的参数中没有的属性则需要在控制器方法中进行手动的设置,然后调用service层,service层调用dao层来保存员工数据,最后以json的格式响应给客户端。
@PostMapping
public R<String> save(HttpServletRequest request, @RequestBody Employee employee) {
log.info("employee:" + employee);
//设置初始密码
employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));
//设置修改时间和用户注册时间
employee.setCreateTime(LocalDateTime.now());
employee.setUpdateTime(LocalDateTime.now());
//获得当前登录用户的id(通过登录时将用户id保存到session域中)
Long empId = (Long) request.getSession().getAttribute("employee");
employee.setCreateUser(empId);
employee.setUpdateUser(empId);
employeeService.save(employee);
return R.success("保存成功");
}
由于在数据库中的username字段设置了unique,所以当前端的表单中出现了同一个个username时,后端由于dao层出现sql异常无法添加,所以我们应该设置一个全局异常处理器来处理出现的异常情况响应给浏览器。
@Slf4j
@ControllerAdvice
//作用:给Controller控制器添加统一的操作或处理
@ResponseBody
//作用:将controller的方法返回的对象通过适当的转换器转换为指定的格式之后,写入到response对象的body区,
// 通常用来返回JSON数据或者是XML数据。
public class GlobalExceptionHandler {
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R<String> exceptionHandler(SQLIntegrityConstraintViolationException sqlIntegrityConstraintViolationException){
log.error(sqlIntegrityConstraintViolationException.getMessage());
if(sqlIntegrityConstraintViolationException.getMessage().contains("Duplicate entry")){
String[] message=sqlIntegrityConstraintViolationException.getMessage().split(" ");
return R.error(message[2]+"该用户名已注册");//以json的格式响应给前端页面
}
return R.error("未知错误");
}
}
员工信息分页查询
登录到员工管理页面后,我们需要将数据库中的员工信息进行分页查询到前端进行展示,这里我们使用了mybatisplus提供的分页插件:
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
}
然后根据前端需要的请求路径和参数来编写我们的controller方法来处理请求:
/**
* 员工信息分页查询
* @param page 当前页数
* @param pageSize 每个页面展示的数据量
* @param name 根据姓名进行查询
* @return
*/
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name){
log.info("page={},pageSize={},name={}",page,pageSize,name);
//添加分页构造器
Page<Employee> employeePage = new Page<>(page,pageSize);
//添加条件构造器
LambdaQueryWrapper<Employee> employeeLambdaQueryWrapper = new LambdaQueryWrapper<>();
//添加过滤条件
employeeLambdaQueryWrapper.like(StringUtils.isNotEmpty(name),Employee::getName,name);
//添加排序功能
employeeLambdaQueryWrapper.orderByDesc(Employee::getCreateTime);
//不带查询功能执行查询
//SELECT id,name,username,password,phone,sex,id_number,status,create_time,update_time,create_user,update_user FROM employee ORDER BY create_time DESC LIMIT ?
//带条件查询的执行查询
//SELECT id,name,username,password,phone,sex,id_number,status,create_time,update_time,create_user,update_user FROM employee WHERE (name LIKE ?) ORDER BY create_time DESC LIMIT ?
employeeService.page(employeePage,employeeLambdaQueryWrapper);
return R.success(employeePage);
}
修改员工账号状态及信息功能
需求分析:
编辑功能就是修改员工的基本信息,当我们点击时会出现会出现员工的原来的基本信息:
将员工的基本信息回显到表单中,也就是前端通过一个get请求
去通过请求参数id来调用service层进而调用dao层获取员工的基本信息,最后再进行修改。
/**
* 根据员工id返回员工信息
* @param id
* @return
*/
@GetMapping("/{id}")
public R<Employee> getById(@PathVariable Long id){
Employee employee = employeeService.getById(id);
return R.success(employee);
}
后端@RequestBody注解对应的类在将HTTP的输入流(含请求体)装配到目标类(即:@RequestBody后面的类)时,会根据json字符串中的key来匹配对应实体类的属性,如果匹配一致且json中的该key对应的值符合(或可转换为)实体类的对应属性的类型要求时,会调用实体类的setter方法将值赋给该属性。
当我们修改完后点击'保存'按钮时,会出现更新失败的情况:
(前端请求到服务端的id和服务端响应给浏览器的响应体的id,都被js处理过后出现了精度的损失)
(后端数据库中的id)
原因在于我们是通过id修改员工的信息,而前端请求接收到的id是js处理过后的,与后端java中的在数据库保存的id是有精度损失的情况,所以我们无法通过比较前后端的Long型id来进行修改员工信息,即无法处理此次请求。(由上图可知响应给浏览器的id非String类型)
所以解决的办法就是在服务端给页面响应json数据时进行处理,将long型数据统一转换为String字符串。
解决方法:
-
提供对象转换器JacksonObjectMapper,基于jackson进行java对象到json的转换
-
在webMvcConfig配置类中扩展Spring MVC的消息转换器,在此消息转换器中使用对象转换器JacksonObjectMapper进行java对象到json数据的转换
/** * 对象映射器:基于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) .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框架的消息转换器:在项目启动时运行 * @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); }
公共字段填充
由于在每个类中都存在createTime,updateTime等每个表(类)都有的字段,但这些字段都是需要在插入表中为非null,这就需要我们在每个保存数据的方法中都需要手动set值进行插入,mybatisplus就为我们提供了一种为所有表中存在的公共字段进行填充。
步骤:
-
在需要填充的属性中加上
@TableField
,其中里面的属性fill对应的值对应三个值,FieldFill.INSERT(插入时填充该字段),Fill.UPDATE(更新时填充该字段),FieldFill.INSERT_UPDATE(插入或者更新时填充该字段)。 -
创建一个类去实现MetaObjectHandler接口,并且重写insertFill和updateFill两个方法
@Slf4j @Component /** *@author admin *@Description 填充公共字段的工具类 *@Date 21:41 2022/5/25 *@Param * @param null *@Return **/ public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { log.info("insertFill--"); log.info("线程id:"+Thread.currentThread().getId()); 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("updateFill--"); log.info("线程id:"+Thread.currentThread().getId()); metaObject.setValue("updateTime",LocalDateTime.now()); metaObject.setValue("updateUser",BaseContext.getCurrentId()); } }
这里由引出一个问题,就是updateUser和createUser获取的值是保存在了session中的,但是这个MyMetaObjectHandler类不能去获取session中的数据。
由下图可以看出,三个方法的调用都来自同一个线程。
解决步骤:
-
解决办法就是创建一个线程工具类,用来保存和获取当前登录用户的id。
/** * @ClassName BaseContext * @Description 线程工具类,每个线程来保存和获取当前登录用户的的id * @Author admin * @Date 2022/5/25 10:20 * @Version 1.0 */ public class BaseContext { private static ThreadLocal<Long> currentId=new ThreadLocal<>(); public static void setCurrentId(Long id){ currentId.set(id); } public static Long getCurrentId(){ return currentId.get(); } }
-
然后在登录过滤器中在确认登录成功后放行将存入session域中的id保存在线程中
//获取登录用户的id将它保存到ThreadLocal线程中,这样在该请求下就可以获取到用户id BaseContext.setCurrentId((Long)request.getSession().getAttribute("employee"));
-
最后就可以在MyMetaObjectHandler类中的方法进行属性的填充
metaObject.setValue("createUser",BaseContext.getCurrentId()); metaObject.setValue("updateUser",BaseContext.getCurrentId());
分类管理模块
删除菜品分类信息
删除菜品分类,由于dish表和setmeal表中有菜品分类的字段(category-id),所以我们在删除该菜品分类信息时需要去判断该菜品分类是否关联了某个菜品或者某个套餐,如果关联了则抛出异常,并向前端展示;如果没有关联则直接删除即可。
我们在CategoryService业务层进行创建一个remove方法(实现删除菜品分类信息功能),然后让CategoryServiceImpl去实现该业务逻辑。
//注入Dish和Setmeal的业务层,用来判断删除菜品分类时是否关联了菜品管理和套餐管理
@Autowired
private DishService dishService;
@Autowired
private SetmealService setmealService;
/**
* 根据id删除菜品分类,删除之前需要进行判断
* @param id
*/
@Override
public void remove(Long id) {
LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
dishLambdaQueryWrapper.eq(Dish::getCategoryId,id);
int count_dish = dishService.count(dishLambdaQueryWrapper);
//sql语句:SELECT COUNT( * ) FROM dish WHERE (category_id = ?)
//如果能根据categoryId查出dish,说明dish(菜品)中关联了category(菜品分类),不能进行删除
if(count_dish!=0){
throw new CustomException("该分类被某菜品关联,无法删除");
}
LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId,id);
int count_setmeal = setmealService.count(setmealLambdaQueryWrapper);
//同理,如果该菜品分类中存在着套餐分类则抛出业务异常
if(count_setmeal!=0){
throw new CustomException("该分类被某套餐关联,无法删除");
}
//如果都没有关联,则可以删除该菜品分类
super.removeById(id);
}
自定义业务异常类
这里我们自定义了一个业务异常类去管理业务层可能出现的业务逻辑的异常,然后在全局异常处理器去捕获该异常向前端响应。
public class CustomException extends RuntimeException{
public CustomException(String message){
super(message);
}
}
@ExceptionHandler(CustomException.class)
public R<String> exceptionHandler(CustomException customException){
log.error(customException.getMessage());
return R.error(customException.getMessage());
}
菜品管理模块
新增菜品功能!!!
菜品分类下拉框功能
在CategoryController层去实现该请求:
/**
* 根据条件查询进行新增菜品功能的菜品分类展示到下拉框选项
* @return
*/
@GetMapping("/list")
public R<List<Category>> list(Category category){
LambdaQueryWrapper<Category> categoryLambdaQueryWrapper = new LambdaQueryWrapper<>();
//添加条件查询:获取category的type字段,1代表菜品分类,2代表套餐分类
categoryLambdaQueryWrapper.eq(category.getType()!=null,Category::getType,category.getType());
//添加条件排序
categoryLambdaQueryWrapper.orderByDesc(Category::getSort).orderByDesc(Category::getUpdateTime);
//对应的sql语句:SELECT id,type,name,sort,create_time,update_time,create_user,update_user FROM category WHERE (type = ?) ORDER BY sort DESC,update_time DESC
List<Category> list = categoryService.list(categoryLambdaQueryWrapper);
return R.success(list);
}
文件的上传和下载
页面发送请求进行图片的上传,请求服务端将图片保存到服务器。
我们专门创建一个控制层来处理文件的上传和下载的请求处理。
具体的处理逻辑代码中有注释。
/**
* @ClassName CommonController
* @Description 文件上传和下载
* @Author admin
* @Date 2022/5/26 10:27
* @Version 1.0
*/
@RestController
@RequestMapping("common")
@Slf4j
public class CommonController {
/**
* 文件的上传
*
* @param file 参数名要和前端保持一致
*/
//上传路径:
/*
*在application.yml中配置
reggie:
path: C:\MR.Li\reggie_img\
*/
@Value("${reggie.path}")
private String path;
@PostMapping("/upload")
public R<String> upload(MultipartFile file) {
log.info("file:" + file);
//获取上传的原始文件名
String originalFilename = file.getOriginalFilename();
//获取上传的原始文件名的后缀名(.jpg,.pmg等)
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
//字符串拼接
String fileName = UUID.randomUUID() + suffix;
//判断path目录存不存在
File basepath = new File(path);
if (!basepath.exists()) {
//不存在则创建该目录
basepath.mkdirs();
}
File file_name = new File(path + fileName);
try {
//路径转移,File dest为目的路径
file.transferTo(file_name);
} catch (IOException e) {
e.printStackTrace();
}
//响应内容为上传文件的fileName
return R.success(fileName);
}
/**
* 文件的下载
*
* @param response
* @param name :需要下载的文件名
*/
@GetMapping("/download")
public void download(String name, HttpServletResponse response) {
FileInputStream fileInputStream = null;
ServletOutputStream outputStream = null;
File file = new File(path + name);
try {
//创建一个输入流来读取下载的文件内容
fileInputStream = new FileInputStream(file);
//通过输出流将文件内容显示到浏览器
outputStream = response.getOutputStream();
int len = 0;
byte[] bytes = new byte[1024];
// while ((fileInputStream.read(bytes)) != -1) {
while ((len = fileInputStream.read(bytes)) != -1) {
outputStream.write(bytes, 0, len);
outputStream.flush();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fileInputStream != null) {
try {
fileInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
将新增菜品保存到数据库
我们将数据填入表单后进行提交到后端请求保存到数据库中,但是表单提交的信息不仅只有cateory一张表的信息,还有dish_flavor表的信息,所以我们无法通过简单的一个实体类去保存数据。所以我们需要导入一个DishDto,用于封装页面提交的数据。
然后我们dishService业务层创建一个方法处理保存菜品同时保存对应的口味数据到数据库,然后dishServiceImpl去实现该业务逻辑。
@Override
@Transactional
public void saveWithFlavor(DishDto dishDto) {
//先将新增菜品的基本信息保存到菜品表dish
//对应的sql语句:INSERT INTO dish ( id, name, category_id, price, code, image, description, status, create_time, update_time, create_user, update_user ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
this.save(dishDto);
//由于dishDto继承了父类Dish,所以我们可以获取dish对应的id
Long dishId = dishDto.getId();
//获取保存菜品的口味集合并且给每个菜品对应的口味附上dishId
List<DishFlavor> flavors = dishDto.getFlavors();
for (DishFlavor dishFlavor : flavors) {
dishFlavor.setDishId(dishId);
}
//保存菜品口味数据到菜品口味表dish_flavor
//对应的sql语句:有两个,因为有两种口味存在
// INSERT INTO dish_flavor ( id, dish_id, name, value, create_time, update_time, create_user, update_user ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ? )
dishFlavorService.saveBatch(flavors);
}
在此方法上我们开始事务@Transactional
并且在主程序中开启事务管理@EnableTransactionManagement
菜品信息的分页查询
分页查询将菜品信息展示:难点在于分页查询不仅需要将属于菜品的字段进行展示,并且还需要展示该菜品的菜品分类的名(属于另一张表的字段),这时候dish就无法满足,就需要一个dto来满足响应的内容。
@GetMapping("/page")
public R<Page> page(int page, int pageSize, String name) {
//分页构造器
Page<Dish> dishPage = 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);
//调用service层实现条件分页效果
dishService.page(dishPage, dishLambdaQueryWrapper);
//分页对象的拷贝,其中records属性不进行拷贝,因为dishpage的erecords的泛型为dish,而dishDtoPage的泛型为dishDto
BeanUtils.copyProperties(dishPage, dishDtoPage, "records");
//创建一个dishDtoPage中的records
List<DishDto> dishDtoList = new ArrayList<>();
//遍历dishPage中的records集合
for (Dish dish : dishPage.getRecords()) {
DishDto dishDto = new DishDto();
//将父类对象拷贝给子类对象
BeanUtils.copyProperties(dish, dishDto);
//根据dishDTO中的CategoryId(继承父类dish的属性)的id查询关联的实体类category(菜品分类)
Category category = categoryService.getById(dishDto.getCategoryId());
//获取菜品分类的name并且赋值
if (category != null) {
String categoryName = category.getName();
dishDto.setCategoryName(categoryName);
}
//将拷贝过来的dishDto并且赋值了categoryName的对象放到dishDtoPage中的records中
dishDtoList.add(dishDto);
}
//将dishDtoPage中的records赋值
dishDtoPage.setRecords(dishDtoList);
return R.success(dishDtoPage);
}
修改菜品信息
1.菜品分类下拉框中数据展示并回显原菜品分类
这里我们将复用新增菜品时同一个请求路径@GetMapping("/list")
所映射的控制器方法R<List<Category>> list(Category category)
来达到菜品分类下拉框中数据展示,并且回显原菜品分类,这里我们要结合第2点一起才能实现。
2.将dish基本信息和dishFlavor回显到修改页面
这里的逻辑其实和前面的分页查询菜品差不多,都是基本的实体类dish和dish_flavor都无法满足响应给浏览器页面进行展示,所以才需要一个类既有dish的基本信息,又有每个dish对应的dish_flavor,所以会创建一个DishDto类封装并且响应给页面,也是因为后端数据库设计时一(dish)对多(dish_flavor)实体表的对应关系,基本业务逻辑如下:
/**
* 根据dish的id获取dish和dishFlavor
* @param id
*/
@Override
public DishDto getByIdWithFlavor(Long id) {
//根据id查询dish
Dish dish = this.getById(id);
DishDto dishDto = new DishDto();
//使用BeanUtils工具类实现对象的拷贝,子父类关系
BeanUtils.copyProperties(dish, dishDto);
//条件构造器
LambdaQueryWrapper<DishFlavor> dishFlavorLambdaQueryWrapper = new LambdaQueryWrapper<>();
//将dish的作为id查询条件在dishFlavor表中进行查询
//对应的sql语句:SELECT id,dish_id,name,value,create_time,update_time,create_user,update_user,is_deleted FROM dish_flavor WHERE (dish_id = ?)
dishFlavorLambdaQueryWrapper.eq(DishFlavor::getDishId, dishDto.getId());
//调用dishFlavorService业务层执行sql语句:list()方法返回多条数据
List<DishFlavor> dishFlavorList = dishFlavorService.list(dishFlavorLambdaQueryWrapper);
//赋值操作
dishDto.setFlavors(dishFlavorList);
//响应数据
return dishDto;
}
3.图片的回显到修改页面
这里复用已经写好的commonController类中的download
控制器方法即可。
4.将修改的信息更新到数据库
更新操作与新增菜品操作的业务逻辑有点不同,更新数据的请求是put请求,而插入数据的请求是post请求,并且如果把新增菜品的功能的业务逻辑复用到更新菜品的功能会出现异常,由于主键唯一无法进行插入数据,并且更新操作是根据唯一id进行更新,所以更新数据操作的业务逻辑的代码如下(注解上有该更新操作的业务逻辑):(写在dish的业务层)
@Override
public void updateWithFlavor(DishDto dishDto) {
//先更新dish的基本信息
this.updateById(dishDto);
//先将dishDto中的口味进行删除操作--->对dish_flavor表进行delete操作
LambdaQueryWrapper<DishFlavor> dishFlavorLambdaQueryWrapper = new LambdaQueryWrapper<>();
//对应的sql语句:delete * from dishFlavor where dish_id=?
dishFlavorLambdaQueryWrapper.eq(DishFlavor::getDishId, dishDto.getId());
dishFlavorService.remove(dishFlavorLambdaQueryWrapper);
//添加当前表单中提交过来的新数据--->对dish_flavor表进行insert操作
//对应的逻辑与saveWithFlavor的保存操作一样
Long dishId = dishDto.getId();
List<DishFlavor> flavors = dishDto.getFlavors();
for (DishFlavor dishFlavor : flavors) {
dishFlavor.setDishId(dishId);
}
dishFlavorService.saveBatch(flavors);
}
批量管理菜品操作
需求分析:可以单独对某一个菜品进行停售和删除操作,也可以对勾选后的菜品进行一个批量停售和删除的操作。
单独停售和删除的操作:
批量停售和删除的操作:
并且对正在用户端售卖的菜品无法进行批量删除操作。
我们将批量删除和修改售卖状态的业务逻辑代码写到到dish中service层中,然后通过DishController层直接进行调用即可。
DishService层中的批量删除和批量修改状态实现代码(具体逻辑代码注释中有写):
/**
* 批量删除菜品
* @param ids
*/
@Override
public void batchDeleteByIds(List<Long> ids) {
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(ids != null,Dish::getId,ids);
List<Dish> list = this.list(queryWrapper);
if (list != null){
for (Dish dish : list) {
//要先判断该菜品是否为停售状态,否则无法删除并且抛出异常处理
if (dish.getStatus() == 0){
this.removeByIds(ids);
}else {
throw new CustomException("有菜品正在售卖,无法全部删除!");
}
}
}
/**
* 批量修改菜品售卖状态
* @param status
* @param ids
*/
@Override
public boolean batchUpdateStatusByIds(Integer status, List<Long> ids) {
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
//对应的sql语句:SELECT id,name,category_id,price,code,image,description,status,sort,create_time,update_time,create_user,update_user,is_deleted FROM dish WHERE (id IN (?,?))
queryWrapper.in(ids != null,Dish::getId,ids);
List<Dish> list = this.list(queryWrapper);
if (list != null){
for (Dish dish : list) {
dish.setStatus(status);//修改菜品的售卖状态
this.updateById(dish);
}
return true;
}else{
return false;
}
}
批量操作用List类型作为形参接受多个前端请求过来的参数(id1,id2......),然后使用in语句进行批量查询,然后将符合查询条件进行修改操作即可。
DishController层进行调用:
// 批量或者单个改变菜品的销售状态
@PostMapping("/status/{status}")
public R<String> updateSaleStatus(@PathVariable("status") Integer status, @RequestParam List<Long> ids) {
//菜品的状态(1为售卖,0为停售)由前端修改完成后通过请求路径占位符的方式传到后端,然后请求参数的类型设置为list类型,这样就可以进行批量或者单个菜品进行修改售卖状态
if (dishService.batchUpdateStatusByIds(status, ids))
return R.success("菜品的售卖状态已更改!");
else
return R.error("售卖状态无法更改!");
}
//批量删除菜品
@DeleteMapping
public R<String> batchDelete(@RequestParam("ids") List<Long> ids) {
dishService.batchDeleteByIds(ids);
return R.success("成功删除菜品!");
}
订单明细模块
订单明细的分页查询
需求分析:
用户在用户端进行外卖下单后,可以从订单明细模块中分页实时显示所有用户们的订单信息,并且可以通过订单号,订单下单的开始时间和结束时间两种查询条件进行分页查询。
代码实现:
/**
* 客户端分页操作
* @param page 当前页数
* @param pageSize 每页显示数量
* @param number 订单号
* @param beginTime 订单开始时间
* @param endTime 订单结束时间
* @return
*/
@GetMapping("/page")
public R<Page> showPage(int page, int pageSize, Long number, String beginTime, String endTime) {
Page<Orders> ordersPage = new Page(page, pageSize);
LambdaQueryWrapper<Orders> queryWrapper = new LambdaQueryWrapper<>();
//增加查询条件
queryWrapper.like(number != null, Orders::getNumber, number)
.gt(StringUtils.isNotEmpty(beginTime),Orders::getOrderTime,beginTime)
.lt(StringUtils.isNotEmpty(endTime),Orders::getOrderTime,endTime);
ordersService.page(ordersPage, queryWrapper);
return R.success(ordersPage);
}
订单状态修改
需求分析:
将已经完成派送的订单修改状态为订单已完成,并且同时响应到用户端的历史订单中。
当我们点击①时,会修改②的订单状态为"已派送"并且也会更新到用户端的最新订单中。
代码实现:
代码逻辑比较简单就直接写到了Controller层即可。
/**
* 更改订单状态为已经派送==3
* @param orders
* @return
*/
@PutMapping
public R<Orders> updateStatus(@RequestBody Orders orders){
Integer status = orders.getStatus();
if (status != null){
orders.setStatus(3);
}
ordersService.updateById(orders);
return R.success(orders);
}
移动端
移动端短信验证登录功能
具体功能就是获取登录用户的手机号码,然后通过阿里云的短信服务给该用户发送验证码进行手机端的登录。
手机端登录界面:
跟电脑端登录界面一样,我们要将此页面的所发出的请求进行过滤,不能让过滤器所拦截,所以在拦截器中增加两个新的请求路径分别是发送短信和登录的请求。
//不需要被过滤器处理的请求路径
String[] urls = {"/employee/login", "/employee/logout", "/backend/**", "/front/**","/user/sendMsg","/user/login"};
并且与管理端相同,将用户端登录成功后也进行放行操作:
//用户端登录成功后进行放行
if(request.getSession().getAttribute("user")!=null){
log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("user"));
log.info("线程id:"+Thread.currentThread().getId());
//获取登录用户的id将它保存到ThreadLocal线程中,这样在该请求下就可以获取到用户id
BaseContext.setCurrentId((Long)request.getSession().getAttribute("user"));
filterChain.doFilter(request,response);
return;
}
然后就是去编写一个User的Controller的处理发送验证码和登录的验证码的请求方法即可。(具体逻辑代码中有注释)
/**
* 短信的发送
*
* @param user
* @param session
* @return
*/
@PostMapping("/sendMsg")
public R<String> SendMsg(@RequestBody User user, HttpSession session) {
//获取用户的手机号码
String phone = user.getPhone();
//先判断用户用户手机号码是否为空
if (StringUtils.isNotEmpty(phone)) {
//生成随机的4位验证码
String code = ValidateCodeUtils.generateValidateCode4String(4);
log.info(code);
//调用阿里云提供的短信服务API完成发送短信
//第一个参数为签名,第二个参数是模板编号,第三个参数为接受短信的手机号码,第4个是短信验证码
//SMSUtils.sendMessage("瑞吉外卖","",phone,code);
//将生成的验证码保存到session中
session.setAttribute(phone, code);
return R.success("手机验证码短信发送成功");
}
return R.error("短信发送失败");
}
/**
* 用户手机端登录操作
* @param map
* @param session
* @return
*/
@PostMapping("/login")
public R<User> login(@RequestBody Map map, HttpSession session) {
log.info("map:", map);
String phone = map.get("phone").toString();
String code = map.get("code").toString();
//从session中获取保存的验证码
Object codeInSession = session.getAttribute(phone);
//进行验证码的比对操作
if (codeInSession != null && codeInSession.equals(code)) {
//如果比对成功,说明登录成功
LambdaQueryWrapper<User> userLambdaQueryWrapper = new LambdaQueryWrapper<>();
userLambdaQueryWrapper.eq(User::getPhone,phone);
User one = userService.getOne(userLambdaQueryWrapper);
if(one==null){
//如果one等于null说明该用户还没有注册
one = new User();
one.setPhone(phone);
one.setStatus(1);
userService.save(one);
}
session.setAttribute("user",phone);
return R.success(one);
}
return R.error("登录失败");
}
用户下单功能
项目小总结
-
模块和功能的分类,就是将一些特定的功能做成一个类进行与实体业务层(就是实体类的dao,service和controller层)的解耦。例如文件的上传和下载,单独做一个CommonController来专门处理文件的上传和下载的请求。
-
总结 @requestBody加与不加的区别如下:
使用@requestBody.当请求content_type为:application/json类型的请求,数据类型为json时, json格式如下:{“aaa”:“111”,“bbb”:“222”}
不使用@requestBody.当请求content_type为:application/x-www-form-urlencoded类型的或multipart/form-data时,数据格式为aaa=111&bbb=222JQuery的$.ajax(url,[settings])
1.默认的ContentType的值为:application/x-www-form-urlencoded; charset=UTF-8
此格式为表单提交格式,数据为key1=value1&key2=value2的格式 。图二serrializeble,只用的是默认contentType类型。
2.虽然ajax的data属性值格式为:{key1:value1,key2:value2},但最后会转为key1=value1&key2=value2的格式提交到后台 。图二,虽然打印出的结果是不是&格式的,但是会转化。
3.如果ajax要和springmvc交互,要使用key1=value1&key2=value2的格式,后台springmvc只需要定义对象或者参数就行了,会自动映射。
4.如果springmvc的参数有@RequestBody注解(接收json字符串格式数据),ajax必须将date属性值转为json字符串,不能为json对象(js对象,会自动转为key=value形式)。并且,修改contentType的值为:application/json; charset=UTF-8,这样加了@RequestBody注解的属性才能自定映射到值。
5.使用在进行图片或者文件上传时使用 multipart/form-data 类型时、 数据会自动进行映射不要添加任何注解。
git管理
将瑞吉外卖项目提交到gitee中进行代码版本的管理。
步骤
-
点击idea工具栏中
VCS
中的Create repository
,然后选中瑞吉外卖项目设置为本地仓库交给git管理。 -
然后将写好的代码使用
git add
命令添加到缓存区。 -
然后先将缓存区的代码使用
git commit
命令提交到本地仓库中 -
然后再由本地仓库使用push到远程仓库中
然后我们就可以在gitee中看到我们从本地仓库提交过来的代码了
然后我们在本地仓库中创建一个新的分支v1.0来开发缓存技术的实现
并且也将该分支推送到远程仓库中去,步骤与前面的一样。
redis优化
redis环境配置
-
导入坐标
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
-
redis基本配置
spring:
redis:
port: 6379
host: 192.168.159.128
password: 123789
database: 0
-
配置RedisTemplate的序列化方式
/** * Redis配置类,配置Key的序列化器 */ @Configuration public class RedisConfig extends CachingConfigurerSupport { @Bean public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>(); //默认的Key序列化器为:JdkSerializationRedisSerializer redisTemplate.setKeySerializer(new StringRedisSerializer()); // redisTemplate.setValueSerializer(new StringRedisSerializer()); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); // redisTemplate.setHashValueSerializer(new StringRedisSerializer()); redisTemplate.setConnectionFactory(connectionFactory); return redisTemplate; } }
缓存短信验证码
代码实现:
UserController中的sendMsg方法:
//将生成的验证码保存到redis中,使用redisTemplate类
ValueOperations valueOperations = redisTemplate.opsForValue();
valueOperations.set(phone,code,5, TimeUnit.MINUTES);
UserController中的login方法:
//从redis中取出数据
Object codeInRedis = redisTemplate.opsForValue().get(phone);
//如果登录成功则删除存在redis中的验证码
redisTemplate.delete(phone);
缓存菜品数据
代码实现:
DishController中的list方法:
@GetMapping("/list")
public R<List<DishDto>> list(Dish dish){
List<DishDto> dishDtoList=null;
//动态拼接key
String key="dish_"+dish.getCategoryId()+"_"+dish.getStatus();
//获取redis中的菜品列表,存在则直接从缓存中取,如果不存在则从数据库查询并且保存到redis
dishDtoList=(List<DishDto>)redisTemplate.opsForValue().get(key);
//redis中存在
if(dishDtoList!=null)
return R.success(dishDtoList);
......从数据库中查询......
//redis中不存在则将查询后的结果存入redis即可
redisTemplate.opsForValue().set(key,dishDtoList,60, TimeUnit.MINUTES);
return R.success(dishDtoList);
}
DishController中的update方法(save方法同理):
@PutMapping
public R<String> update(@RequestBody DishDto dishDto){
dishService.updateWithFlavor(dishDto);
//全局清理:更新菜品操作时将缓存中的键中有"dish_"前缀的数据删除
//Set dish_ = redisTemplate.keys("dish_*");
//redisTemplate.delete(dish_);
//部分清理:更新菜品操作时将只删除修改该菜品分类的数据
String dish_id_status="dish_"+ dishDto.getCategoryId()+"_"+dishDto.getStatus();
redisTemplate.delete(dish_id_status);
return R.success("修改成功");
}
spring Cache
常用注解
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
使用spring cache优化套餐管理
在SetmealController中的list
控制器方法中使用@Cacheable
解。
@Cacheable(value = "SetmealCache",key = "'setmeal'+'_'+#setmeal.categoryId+'_'+#setmeal.status")
public R<List<Setmeal>> list(Setmeal setmeal){
....代码.....
}
在SetmealController中的save
(新增操作)和delete
(删除操作)控制器方法中使用@CacheEvict
注解。
@CacheEvict(value ="SetmealCache",allEntries = true )
public R<SetmealDto> save(@RequestBody SetmealDto setmealDto) {
log.info("setmealDto:" + setmealDto);
setmealService.saveWithDish(setmealDto);
return R.success(setmealDto);
}
@CacheEvict(value ="SetmealCache",allEntries = true )
public R<String> delete(Long ids) {
setmealService.removeById(ids);
LambdaQueryWrapper<SetmealDish> setmealDishLambdaQueryWrapper = new LambdaQueryWrapper<>();
setmealDishLambdaQueryWrapper.eq(ids != null, SetmealDish::getSetmealId, ids);
setmealDishService.remove(setmealDishLambdaQueryWrapper);
return R.success("删除成功");
}
本文作者:stepForward-
本文链接:https://www.cnblogs.com/sunshineTv/p/16417114.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步