java瑞吉外卖
环境搭建
一、数据库环境搭建
1.新建数据库reggie,这里字符集一般用utf8mb4,排序规则一般用utf8mb4_general_ci或utf8mb4_unicode_ci
2.然后导入表结构
二、创建springboot工程
然后检查maven仓库设置,jdk
这是我的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.4.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.itheima</groupId>
<artifactId>reggie_take_out</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>reggie_take_out</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.7.6</spring-boot.version>
</properties>
<dependencies>
<!-- Spring Boot Web Starter: 提供构建Web应用所需的所有依赖,包括Spring MVC和Tomcat -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>compile</scope>
</dependency>
<!-- MyBatis-Plus Boot Starter: MyBatis的增强工具,在MyBatis的基础上只做增强不做改变,简化开发、提高效率 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<!-- Fastjson: 一个Java语言编写的高性能功能完善的JSON库,用于将Java对象转换成JSON格式的字符串,以及将JSON字符串还原成Java对象 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
<!-- MySQL Connector/J: MySQL的官方JDBC驱动,用于Java应用与MySQL数据库的连接 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok: 一个Java库,可以通过简单的注解形式来帮助我们简化Java代码,特别是通过注解的方式消除样板代码,如getter和setter方法等 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
<!-- Spring Boot Test Starter: 提供对测试框架(如JUnit和Mockito)的支持,以及Spring TestContext Framework的集成 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Commons Lang: Apache Commons Lang库提供了一系列对java.lang包的扩展,包括字符串操作、数组操作、日期时间操作等实用工具类 -->
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<!-- Druid Spring Boot Starter: Druid是一个数据库连接池实现,支持多种数据库,性能高且监控功能强大 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.23</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.4.5</version>
</plugin>
</plugins>
</build>
</project>
再编写application.yml文件
server:
port: 8080
# 配置服务器端口为 8080
spring:
application:
#应用的名称,可选
name: reggie_take_out
# 设置应用名称为 reggie_take_out
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
# 设置数据库驱动类名为 MySQL 的 JDBC 驱动
url: jdbc:mysql://localhost:3306/reggie?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
# 配置数据库连接 URL,连接到本地 MySQL 数据库的 reggie 数据库,并设置时区、字符编码等参数
username: root
# 数据库用户名是 root
password: 123456
# 数据库密码是 123456
mybatis-plus:
configuration:
#在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射
map-underscore-to-camel-case: true
# 开启 MyBatis-Plus 的下划线转驼峰命名映射功能
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 设置 MyBatis-Plus 的日志实现类为标准输出
global-config:
db-config:
id-type: ASSIGN_ID
# 设置 MyBatis-Plus 的全局数据库配置中的 ID 生成策略为自动分配 ID
创建springboot程序入口
导入前端文件,注意前端文件的位置,在Boot项目中,前台默认就只能访问 resource目录下的static和templates文件夹下的文件;这里我在resources目录下创建了static和templates两个文件夹,并把前端资源导入
后台登录功能开发
创建实体类
先创建一个 包entity,把实体类employee放在这个包下
package com.itheima.reggie.entity;
import com.baomidou.mybatisplus.annotation.*;
import java.io.Serializable;
import java.time.LocalDateTime;
import lombok.Data;
@Data
public class Employee implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String name;
private String username;
private String password;
private String phone;
private String sex;
private String idNumber; //身份证号
private Integer status; //状态 0:禁用,1:正常
private LocalDateTime createTime;
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT)
private Long createUser;
@TableField(fill = FieldFill.UPDATE)
private Long updateUser;
}
创建Mapper接口EmployeeMapper
package com.itheima.reggie.mapper;
import com.itheima.reggie.entity.Employee;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface EmployeeMapper extends BaseMapper<Employee> {
}
创建Service接口EmployeeService和实现类EmployeeServiceImpl
package com.itheima.reggie.service;
import com.itheima.reggie.entity.Employee;
import com.baomidou.mybatisplus.extension.service.IService;
public interface EmployeeService extends IService<Employee> {
}
这里EmployeeService继承了Mybatis-Plus的IService,继承了很多crud的抽象方法
package com.itheima.reggie.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.reggie.entity.Employee;
import com.itheima.reggie.service.EmployeeService;
import com.itheima.reggie.mapper.EmployeeMapper;
import org.springframework.stereotype.Service;
@Service
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements EmployeeService{
}
这里EmployeeServiceImpl继承了Mybatis-Plus的ServiceImpl,ServiceImpl帮忙重写了IService里的抽象方法
创建EmployeeController
package com.itheima.reggie.controller;
import com.itheima.reggie.service.EmployeeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping("employee")
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
}
创建一个通用结果类R
package com.itheima.reggie.common;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
@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;
}
}
在EmpController中创建登录方法
业务逻辑:
1、将页面提交的密码password进行md5加密处理
2、根据页面提交的用户名username查询数据库
3、如果没有查询到则返回登录失败结果
4、密码比对,如果不一致则返回登录失败结果
5、查看员工状态,如果为已禁用状态,则返回员工已禁用结果
6、登录成功,将员工id存入Session并返回登录成功结果
package com.itheima.reggie.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.itheima.reggie.common.R;
import com.itheima.reggie.entity.Employee;
import com.itheima.reggie.service.EmployeeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.DigestUtils;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.nio.charset.StandardCharsets;
import java.security.DigestException;
@Slf4j
@RestController
@RequestMapping("employee")
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
/**
* 员工登录
* @param request
* @param employee
* @return
*/
@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);
}
}
注意这里字符串比较要用equals方法,不能用==
在EmpController中创建退出登录方法
业务逻辑:
①在controller中创建对应的处理方法来接受前端的请求,请求方式为post;
②清理session中的用户id
③返回结果(前端页面会进行跳转到登录页面)
/**
*员工退出
* @param request
* @return
*/
@PostMapping("/logout")
public R<String> logout(HttpServletRequest request){
//清理Session中保存的当前登录员工的id
request.getSession().removeAttribute("employee");
return R.success("退出登录成功");
}
完善登录功能
前面的登陆存在一个问题,如果用户不进行登陆,直接访问系统的首页,照样可以正常访问,这种设计是不合理的,我们希望看到的效果是只有完成了登陆后才可以访问系统中的页面,如果没有登陆则跳转到登陆页面;
创建自定义过滤器LoginCheckFilter
package com.itheima.reggie.filter;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
@WebFilter(filterName = "LoginCheckFilter",urlPatterns = "/*")
public class LoginCheckFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
log.info("拦截到了请求:{}",request.getRequestURI());
filterChain.doFilter(request,response);
}
}
并在启动类上加@ServletComponentScan注解
业务逻辑:
1.获取本次请求的URI
2.判断本次请求是否需要处理
3.如果不需要处理,则直接放行
4.判断登录状态,如果已登录,则直接放行
5.如果未登录则返回未登录状态
具体逻辑的代码实现:
package com.itheima.reggie.filter;
import com.alibaba.fastjson.JSON;
import com.itheima.reggie.common.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.AntPathMatcher;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@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;
//1.获取本次请求的url
String requestURI = request.getRequestURI();
log.info("拦截到请求:{}",requestURI);
//定义不需要拦截的请求路径
String[] urls = {
"/employee/login",
"/employee/logout",
"/backend/**",
"front/**"
};
//2.判断本次请求是否需要处理
boolean check = check(urls,requestURI);
//3.如果不需要处理,则直接方行
if (check == true){
log.info("本次请求不需要处理:",requestURI);
filterChain.doFilter(request,response);
return;
}
//4.判断登录状态,如果已经登录,则直接放行
if (request.getSession().getAttribute("employee") != null){
log.info("用户已登录,登录id为:{}",request.getSession().getAttribute("employee"));
filterChain.doFilter(request,response);
return;
}
log.info("用户未登录");
//5.如果未登录则返回未登录结果,通过输出流方式向客户端页面响应数据
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
return;
}
public boolean check(String[] urls,String requestURI){
for (String url : urls) {
boolean match = PATH_MATCHER.match(url,requestURI);
if(match == true){
return true;
}
}
return false;
}
}
新增员工
程序执行过程:
1.页面发生ajax请求,将新增员工页面输入的数据以json的形式提交到服务端
2.服务端Controller接收页面提交的数据并调用Service将数据进行保存
3.Service调用Mapper操作数据库,保存数据
/**
* 新增员工
* @param request
* @param employee
* @return
*/
@PostMapping
public R<String> save(HttpServletRequest request,@RequestBody Employee employee){
log.info("新增员工信息:{}",employee.toString());
//设置初始密码,需要进行md5加密处理
employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));
employee.setCreateTime(LocalDateTime.now());
employee.setUpdateTime(LocalDateTime.now());
//获得当前登录用户的id
Long empId = (Long) request.getSession().getAttribute("employee");
employee.setCreateUser(empId);
employee.setUpdateUser(empId);
employeeService.save(employee);
return R.success("新增用户成功");
}
注意:员工id用的是mybatis-plus的雪花算法,自动生成,不用设置
全局异常处理器
因为username我们设置的是唯一索引,所以下次再新增和上一个用户的username相同的时候就会出现异常,这时候我们需要写一个全局异常处理器来捕获异常
在common包下写一个GlobalExceptionHandler
package com.itheima.reggie.common;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.sql.SQLIntegrityConstraintViolationException;
/**
* 全局异常处理
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 异常处理方法
* @param ex
* @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("失败了");
}
}
员工信息分页查询
程序执行过程:
先配置Mybatis-Plus提供的分页插件
package com.itheima.reggie.config;
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;
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
}
然后编写业务逻辑代码
/**
* 员工页面分页查询
* @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 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.页面发送ajax请求,将参数(id,status)提交到服务端
2.服务端Controller接受页面提交的数据并调用Service更新数据
3.Service调用Mapper操作数据库
启用,禁用的员工账号,本质上就是一个更新操作,也就是对status状态字段进行修改操作;
在controller中创建update方法,此方法是一个通用的修改员工信息的方法
@PutMapping
public R<String> upDate(HttpServletRequest request,@RequestBody Employee employee){
log.info(employee.toString());
employee.setUpdateTime(LocalDateTime.now());
Long empId = (Long) request.getSession().getAttribute("employee");
employee.setUpdateUser(empId);
employeeService.updateById(employee);
return R.success("员工信息修改成功");
}
mybatis-plus对id使用了雪花算法,所以存入数据库中的id是19为长度,但是前端的js只能保证数据的前16位的数据的精度,对我们id后面三位数据进行了四舍五入,所以就出现了精度丢失;就会出现前度传过来的id和数据里面的id不匹配,就没办法正确的修改到我们想要的数据;
这时候我在employee类的id属性上加上了 @JsonFormat(shape = JsonFormat.Shape.STRING)
@JsonFormat(shape = JsonFormat.Shape.STRING)
private Long id;
编辑员工信息
业务逻辑主要在前端
程序执行流程:
1.点击编辑按钮时,页面跳转到add.html,并在url中携带参数(员工id)
2.在add.html页面中获取url中的参数(员工id)
3.发送ajax请求,请求服务端,同时提交员工id参数
4.服务端接收请求,根据员工id查询员工信息,将员工信息以json格式响应给页面
5.页面接收服务端响应的json数据,通过VUE的数据绑定进行员工信息回显
6.点击保存按钮,发送ajax请求,将页面中的员工信息以json格式提交给服务端
7.服务端接收员工信息,并进行处理,完成后给页面响应
8.页面接收到服务端响应信息后进行相应处理
后端代码:
@GetMapping("/{id}")
public R<Employee> getId(@PathVariable Long id){
log.info("根据id查询员工信息");
Employee employee = employeeService.getById(id);
if(employee != null){
return R.success(employee);
}
return R.error("没有查询到员工信息");
}
公共字段填充
这里我们要使用Mybatis-Plus提供的公共字段自动填充功能
实现步骤:
1.在实体类的属性上加上@TableField注解,指定自动填充的策略
2.按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现MetaObjectHandler接口
把相关的注解加在需要mybatis-plus自动帮我们填充的字段上面
@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;
package com.itheima.reggie.common;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Autowired
private HttpServletRequest request;
/**
* 插入操作自动填充
* @param metaObject
*/
@Override
public void insertFill(MetaObject metaObject) {
log.info("公共字段自动填充[insert]");
log.info(metaObject.toString());
metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("createUser",request.getSession().getAttribute("employee"));
metaObject.setValue("updateTime",LocalDateTime.now());
metaObject.setValue("updateUser",request.getSession().getAttribute("employee"));
}
/**
* 更新操作自动填充
* @param metaObject
*/
@Override
public void updateFill(MetaObject metaObject) {
log.info("公共字段自动填充[update]");
log.info(metaObject.toString());
metaObject.setValue("updateTime",LocalDateTime.now());
metaObject.setValue("updateUser",request.getSession().getAttribute("employee"));
}
}
新增分类
先创建实体类Category
package com.itheima.reggie.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 分类
*/
@Data
public class Category implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
//类型 1 菜品分类 2 套餐分类
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;
}
CategoryMapper接口
package com.itheima.reggie.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.reggie.entity.Category;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface CategoryMapper extends BaseMapper<Category> {
}
CategoryService接口
package com.itheima.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.reggie.entity.Category;
public interface CategoryService extends IService<Category> {
}
CategoryServiceImpl实现类
package com.itheima.reggie.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.reggie.entity.Category;
import com.itheima.reggie.mapper.CategoryMapper;
import com.itheima.reggie.service.CategoryService;
import org.springframework.stereotype.Service;
@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {
}
CategoryController类
package com.itheima.reggie.controller;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.reggie.common.R;
import com.itheima.reggie.entity.Category;
import com.itheima.reggie.service.CategoryService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@Slf4j
@RequestMapping("category")
public class CategoryController {
@Autowired
private CategoryService categoryService;
}
然后在Controller层进行业务逻辑编写
程序执行流程:
1.页面(backend/page/category/list.html)发送ajax请求,将新增分类窗口输入的数据以json格式提交到服务端
2.服务端Controller接收页面提交的数据并调用Service将数据进行保存
3.Service调用Mapper操作数据库,保存数据
@PostMapping
public R<String> save(@RequestBody Category category){
log.info("{category}",category);
categoryService.save(category);
return R.success("新增分类成功");
}
菜品类的分页
程序执行流程:
1.页面发送ajax请求,将分页查询参数(page,pageSize)提交到服务端
2.服务端Controller接收页面提交的数据并调用Service查询数据
3.Service调用Mapper操作数据库,查询分页数据
4.Controller将查询到的分页数据响应给页面
5.页面接收到分页数据并通过ElementUI的Table组件展示到页面上
@GetMapping("/page")
public R<Page> page(int page,int pageSize){
//分页构造器
Page<Category> pageInfo = new Page<>(page,pageSize);
//条件构造器
LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.orderByAsc(Category::getSort);
//进行分页查询
categoryService.page(pageInfo,queryWrapper);
return R.success(pageInfo);
}
删除分类
前端的js只能保证数据的前16位的数据的精度,对我们id后面三位数据进行了四舍五入,所以就出现了精度丢失,这里我们还是要在Category类的id上加入@JsonFormat(shape = JsonFormat.Shape.STRING)
@JsonFormat(shape = JsonFormat.Shape.STRING)
private Long id;
@DeleteMapping()
public R<String> delete(@RequestParam("ids") Long ids){ //注意这里前端传过来的数据是ids
log.info("删除分类,id为{}",ids);
categoryService.removeById(ids);
return R.success("分类信息删除成功");
}
因为可能需要删除的数据是与其他菜品表和套餐表关联的,所以删除之前要先判断该条数据是否与菜品表和套餐表中的数据关联;
创建Dish实体类,Setmeal实体类,DishMapper接口,SetmealMappper接口,DishService接口,SetmealService接口,DishServiceImpl实现类,SetmealServiceImpl实现类
添加自定义的service方法:(就是我们需要的业务mybatis没有提供,所以就需要自己另外在service创建新的方法,并且在相关的业务中实现)
package com.itheima.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.reggie.entity.Category;
public interface CategoryService extends IService<Category> {
public void remove(Long ids);
}
自定义一个业务异常类
package com.itheima.reggie.common;
/**
* 自定义业务异常类
*/
public class CustomException extends RuntimeException {
public CustomException(String message){
super(message);
}
}
在原先写好的全局异常处理中加入
@ExceptionHandler(CustomException.class)
public R<String> exceptionHandler(CustomException ex) {
log.error(ex.getMessage());
return R.error(ex.getMessage());
}
然后在CategoryServiceImpl实现类中重写这个方法
package com.itheima.reggie.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.reggie.common.CustomException;
import com.itheima.reggie.entity.Category;
import com.itheima.reggie.entity.Dish;
import com.itheima.reggie.entity.Setmeal;
import com.itheima.reggie.mapper.CategoryMapper;
import com.itheima.reggie.service.CategoryService;
import com.itheima.reggie.service.DishService;
import com.itheima.reggie.service.SetmealService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {
@Autowired
private DishService dishService;
@Autowired
private SetmealService setmealService;
/**
* 根据id删除分类,删除之前判断是否和菜品或套餐有关联数据
* @param ids
*/
@Override
public void remove(Long ids) {
LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
//添加查询条件
dishLambdaQueryWrapper.eq(Dish::getCategoryId,ids);
int count1 = dishService.count(dishLambdaQueryWrapper);
//查询当前分类是否关联了菜品,如果已经关联,则直接抛出异常
if (count1 > 0){
throw new CustomException("当前分类下关联了菜品,不能删除");
}
LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
//添加查询条件
setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId,ids);
int count2 = setmealService.count(setmealLambdaQueryWrapper);
//查询当前分类是否关联了套餐,如果已经关联,则直接抛出异常
if (count2 > 0){
throw new CustomException("当前分类下关联了套餐,不能删除");
}
super.removeById(ids);
}
}
最后在CategoryController中调用
@DeleteMapping()
public R<String> delete(@RequestParam("ids") Long ids){ //注意这里前端传过来的数据是ids
log.info("删除分类,id为{}",ids);
//categoryService.removeById(ids);
categoryService.remove(ids);
return R.success("分类信息删除成功");
}
修改分类
这里的编辑的数据回显,前端已经帮我们做好了,所以我们就不需要去数据库查询了,这样可以减少对数据库的操作
@PutMapping
public R<String> update(@RequestBody Category category){
log.info("修改分类信息:{}",category);
categoryService.updateById(category);
return R.success("修改分类信息成功");
}
记得在对应的实体类加上公共字段的值设置:前面我们配置了这个,所以这里只需要加注解就行
//创建时间
@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;
文件上传下载
yml配置文件:配置上传图片的存储位置
reggie:
path: D:\reggie\
新建一个CommonController类用于文件上传和下载
package com.itheima.reggie.controller;
import com.itheima.reggie.common.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
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.FileNotFoundException;
import java.io.IOException;
import java.util.UUID;
/**
* 文件上传和下载
*/
@RestController
@RequestMapping("/common")
@Slf4j
public class CommonController {
@Value("${reggie.path}")
private String basePath;
/**
* 文件上传
* @param file
* @return
*/
@PostMapping("/upload")
public R<String> upload(MultipartFile file){
//file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会删除
log.info(file.toString());
//原始文件名
String originalFilename = file.getOriginalFilename();//abc.jpg
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
//使用UUID重新生成文件名,防止文件名称重复造成文件覆盖
String fileName = UUID.randomUUID().toString() + suffix;//dfsdfdfd.jpg
//创建一个目录对象
File dir = new File(basePath);
//判断当前目录是否存在
if(!dir.exists()){
//目录不存在,需要创建
dir.mkdirs();
}
try {
//将临时文件转存到指定位置
file.transferTo(new File(basePath + fileName));
} catch (IOException e) {
e.printStackTrace();
}
return R.success(fileName);
}
/**
* 文件下载
* @param name
* @param response
*/
@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");
int len = 0;
byte[] bytes = new byte[1024];
while ((len = fileInputStream.read(bytes)) != -1){
outputStream.write(bytes,0,len);
outputStream.flush();
}
//关闭资源
outputStream.close();
fileInputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
新增菜品
创建DishFlavor实体类,DishFlavorMapper,DishFalvorService,DishFlavorServiceImpl
在CategoryController里写代码:
@GetMapping("/list")
//这个接口接收到参数其实就是一个前端传过来的type,这里之所以使用Category这个类来接受前端的数据,是为了以后方便
//因为这个Category类里面包含了type这个数据,返回的数据多了,你自己用啥取啥就行
private R<List<Category>> list(Category category){
//条件构造器
LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper();
//添加查询条件
queryWrapper.eq(category.getType() != null,Category::getType,category.getType());
//添加排序条件 使用两个排序条件,如果sort相同的情况下就使用更新时间进行排序
queryWrapper.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime);
List<Category> list = categoryService.list(queryWrapper);
return R.success(list);
}
点击保存按钮的时候,把前端的json数据提交到后台,后台接收数据,对数据进行处理;要与两张表打交道,一个是dish一个是dish_flavor表;
创建DishDto封装数据
package com.itheima.reggie.dto;
import com.itheima.reggie.entity.Dish;
import com.itheima.reggie.entity.DishFlavor;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class DishDto extends Dish {
private List<DishFlavor> flavors = new ArrayList<>();
private String categoryName;
private Integer copies;
}
在DishService中新增一个方法
//新增菜品,同时插入菜品对应的口味数据,需要操作两张表:dish、dish_flavor
public void saveWithFlavor(DishDto dishDto);
在DishServiceImpl重写这个方法
@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);
}
Controller层调用
@PostMapping
public R<String> save(@RequestBody DishDto dishDto){
log.info(dishDto.toString());
dishService.saveWithFlavor(dishDto);
return R.success("新增菜品成功");
}
菜品信息分页查询
/**
* 菜品信息分页查询
* @param page
* @param pageSize
* @param name
* @return
*/
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name){
//构造一个分页构造器对象
Page<Dish> dishPage = new Page<>(page,pageSize);
Page<DishDto> dishDtoPage = new Page<>(page,pageSize);
//上面对dish泛型的数据已经赋值了,这里对DishDto我们可以把之前的数据拷贝过来进行赋值
//构造一个条件构造器
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
//添加过滤条件 注意判断是否为空 使用对name的模糊查询
queryWrapper.like(name != null,Dish::getName,name);
//添加排序条件 根据更新时间降序排
queryWrapper.orderByDesc(Dish::getUpdateTime);
//去数据库处理分页 和 查询
dishService.page(dishPage,queryWrapper);
//获取到dish的所有数据 records属性是分页插件中表示分页中所有的数据的一个集合
List<Dish> records = dishPage.getRecords();
List<DishDto> list = records.stream().map((item) ->{
//对实体类DishDto进行categoryName的设值
DishDto dishDto = new DishDto();
//这里的item相当于Dish 对dishDto进行除categoryName属性的拷贝
BeanUtils.copyProperties(item,dishDto);
//获取分类的id
Long categoryId = item.getCategoryId();
//通过分类id获取分类对象
Category category = categoryService.getById(categoryId);
if ( category != null){
//设置实体类DishDto的categoryName属性值
String categoryName = category.getName();
dishDto.setCategoryName(categoryName);
}
return dishDto;
}).collect(Collectors.toList());
//对象拷贝 使用框架自带的工具类,第三个参数是不拷贝到属性
BeanUtils.copyProperties(dishPage,dishDtoPage,"records");
dishDtoPage.setRecords(list);
//因为上面处理的数据没有分类的id,这样直接返回R.success(dishPage)虽然不会报错,但是前端展示的时候这个菜品分类这一数据就为空
//所以进行了上面的一系列操作
return R.success(dishDtoPage);
}
修改菜品
菜品信息的回显
在DishService里添加自己要实现的方法
//根据id查询菜品信息和对应的口味信息
public DishDto getByIdWithFlavor(Long id);
方法的实现
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;
}
Controller层调用
/**
* 根据id来查询菜品信息和对应的口味信息
* @param id
* @return
*/
@GetMapping("/{id}")
public R<DishDto> get(@PathVariable Long id){ //这里返回什么数据是要看前端需要什么数据,不能直接想当然的就返回Dish对象
DishDto dishDto = dishService.getByIdWithFlavor(id);
return R.success(dishDto);
}
保存修改
在DishService添加自己想要的方法
//更新菜品信息,同时更新对应的口味信息
public void updateWithFlavor(DishDto dishDto);
方法的实现
@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套餐菜品分类表
创建SetmealDish实体类,SetmealService,SetmealServiceImpl,SetmealController,SetmealDto
新增套餐
新增套餐时前端页面和服务端的交互过程:
1.页面(backend/page/combo/add.html),请求服务端获取套餐分类数据并展示到下拉框中
2.页面发送ajax请求,请求服务端获取菜品分类数据并展示到添加菜品窗口中
3.页面发送ajax请求,请求服务端,根据菜品分类查询对应的菜品数据并展示到添加类窗口中
4.页面发送请求进行图片上传,请求服务端将图片保存到服务器
5.页面发送请求进行图片下载,将上传的图片进行回显
6.点击保存按钮,发送ajax请求,将套餐相关数据以json格式提交到服务端
在DishController中书写代码:
/**
* 根据条件查询对应的菜品数据
* @param dish
* @return
*/
@GetMapping("/list")
public R<List<DishDto>> list(Dish dish) {
log.info("dish:{}", dish);
//条件构造器
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.like(StringUtils.isNotEmpty(dish.getName()), Dish::getName, dish.getName());
queryWrapper.eq(null != dish.getCategoryId(), Dish::getCategoryId, dish.getCategoryId());
//添加条件,查询状态为1(起售状态)的菜品
queryWrapper.eq(Dish::getStatus,1);
queryWrapper.orderByDesc(Dish::getUpdateTime);
List<Dish> dishs = dishService.list(queryWrapper);
List<DishDto> dishDtos = dishs.stream().map(item -> {
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(item, dishDto);
Category category = categoryService.getById(item.getCategoryId());
if (category != null) {
dishDto.setCategoryName(category.getName());
}
LambdaQueryWrapper<DishFlavor> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DishFlavor::getDishId, item.getId());
dishDto.setFlavors(dishFlavorService.list(wrapper));
return dishDto;
}).collect(Collectors.toList());