1.项目整体搭建
这里用到的是springboot3+mybatisplus
1.1数据库搭建
整体表搭建,这里我是直接用的老师给的数据库
1.2maven项目搭建
依赖
这两个jar包第一次用,记录一下
fastjson json处理,可将对象转化为json形式 可将对象中的属性以string的形式响应出去
因为这里用的是雪花算法,生成的id是64位,类型是long型,响应给客户端后客户端因为长度过大会精度损失使得客户端想服务器传递过来的id是错误的,所以这里不能以long类型传递,要以string传递
使用注解@JsonFormat(shape = JsonFormat.Shape.STRING)解决上面问题
commons-lang 工具类可用于MD5加密
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.5</version>
</parent>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.34</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
<version>1.2.20</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.7</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!--json,用户json转化,设置对象属性响应时的json形式 比如一个属性是long型,可以设置
string-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
<!--工具类,可以省略MD5等工具-->
<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>
</plugin>
</plugins>
</build>
1.3 application.yml配置
server:
port: 8080
servlet:
context-path: /
spring:
application:
#应用的名称,可选
name: reggie_take_out
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db_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 #主键策略,雪花算法
type-aliases-package: org.example.pojo
mapper-locations: classpath:/mapper/*.xml
#临时文件路径 项目启动后,有的功能要上传文件,这时文件会保存在临时文件中
reggie:
path: F:/Java图片/
1.4创建项目启动入口
@SpringBootApplication
@MapperScan("org.example.mapper")
@ServletComponentScan //将过滤器给扫描进来
public class Main {
public static void main(String[] args) {
SpringApplication.run(Main.class, args);
}
//mybatis plus插件
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); //分页插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); //乐观锁
return interceptor;
}
}
1.5前端资源引入
首先创建static目录,前端资源这里直接复制就行,切记要在static目录下
如果资源不在static目录,那么记得在application.yml中设置
1.6使用mybatisx生成
这里先连接数据库,选择所有表,然后生成对应的实体类,service,service实现类,mapper,mapper.xml
2.后台登录功能开发
登入的请求
请求体
这时候我们编写对应的controller
1、将页面提交的密码password进行md5加密处理
2、根据页面提交的用户名username查询数据库
3、如果没有查询到则返回登录失败结果
4、密码比对,如果不一致则返回登录失败结果
5、查看员工状态,如果为已禁用状态,则返回员工已禁用结果
6、登录成功,将员工id存入Session并返回登录成功结果后续客户端会根据session中的内容来判断它是否登入成功,我们也会根据session中的内容来判断是哪个用户来进行操作
/**
* @Description: 登入
* @param: request 如果登入成功,就将用户的id存入session中
* @param: employee
* @return: org.example.common.R
* @Author: 刘某人
* @Date 2024/8/8 18:38
*/
@PostMapping("login")
public R login(HttpServletRequest request, @RequestBody Employee employee) {
R r = employeeService.login(request, employee);
return r;
}
service实现类
@Override
public R login(HttpServletRequest request, Employee employee) {
employee.setPassword(DigestUtils.md5DigestAsHex(employee.getPassword().getBytes()));//加密
LambdaQueryWrapper<Employee> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Employee::getUsername,employee.getUsername());
Employee byId = employeeMapper.selectOne(wrapper); //根据客户端传来的用户名来
if(byId == null){
return R.error("没有这个用户");
}else{
if(employee.getPassword().equals(byId.getPassword())){
if(byId.getStatus() == 0){
return R.error("该用户已被禁用");
}else {
HttpSession session = request.getSession();
session.setAttribute("employee",byId.getId());
return R.success(byId);
}
}else {
return R.error("用户密码错误");
}
}
}
3.后台退出功能
退出的请求
编写对应的controller
客户端根据session中的内容来判断登入是否成功,这里我们只需把session清空就可以
/**
* @Description:员工退出 清理session中的id,返回结果
* @param: request
* @return: org.example.common.R
* @Author: 刘某人
* @Date 2024/8/8 21:57
*/
@PostMapping("logout")
public R logout(HttpServletRequest request) {
R r = employeeService.logout(request);
return r;
}
service实现类
@Override
public R logout(HttpServletRequest request) {
HttpSession session = request.getSession();
session.removeAttribute("employee"); //删除session中的数据
return R.success("退出成功");
}
4.员工管理模块
4.1完善员工登录功能(重点)
前面的登陆存在一个问题,如果用户不进行登陆,直接访问系统的首页,照样可以正常访问,这种设计是不合理的,我们希望看到的效果是只有完成了登陆后才可以访问系统中的页面,如果没有登陆则跳转到登陆页面
这里我们使用的是过滤器
自定义过滤器
package org.example.filter;
import com.alibaba.fastjson.JSON;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.example.common.BaseContext;
import org.example.common.R;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import java.io.IOException;
@WebFilter(filterName = "LoginCheckFilter", urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter implements Filter {
public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher(); //路径匹配器,支持通配符
/*
* @Description:
*检查是否登入
* 逻辑如下
* 1.获取本次请求的url
* 2.判断本次请求是否需要处理
* 3.如果不需要处理,那就直接放行
* 4.判断登入状态,如果已登录,则直接放行
* 5.如果为登入则返回未登录结果
* 注意:如果这里没有跳转登录页面,可以清理游览器缓存再次尝试
* @param: null
* @return:
* @Author: 刘某人
* @Date 2024/8/8 22:49
*/
@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 = new String[]{ //里面定义的是不需要处理的请求 "/backend/**", "/front/**" 这两个是静态资源,不需要过滤,只处理请求,//"/common/**"是文件的上传下载
"/employee/login", "/employee/logout", "/backend/**", "/front/**" ,"employee/page"
,"/common/**"
};
// 2.判断本次请求是否需要处理
boolean check = check(urls, requestURI);
// 3.如果不需要处理,那就直接放行
if (check) {
log.info("本次请求{}不需要处理", requestURI);
filterChain.doFilter(request, response);
return;
}
// 4.判断登入状态,如果已登录,则直接放行
Long employee = (Long)request.getSession().getAttribute("employee");//获取登入成功的session ,登入成功session有内容,未登入则没有也就是null
if (employee != null) { //非空则放行
// long id = Thread.currentThread().getId(); //获得当前线程id,为了后面的公共数据
// log.info("当前线程id为{}",id);
BaseContext.setCurrentId(employee); //将当前session的id存入当前线程
log.info("用户已登入,用户id为{}", employee);
filterChain.doFilter(request, response);
return;
}
//5.如果为登入则返回未登录结果,给客户端响应一个JSON数据
log.info("用户未登入");
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN"))); // JSON.toJSONString 将对象转换为JSON字符串,是fastjson这个依赖的
return;
}
/**
* @Description: 路径匹配,检查本次请求是否需要方行
* @param: urls
* @param: url
* @return: boolean
* @Author: 刘某人
* @Date 2024/8/8 23:08
*/
public boolean check(String[] urls, String url) {
for (String u : urls) {
boolean match = PATH_MATCHER.match(u, url); //匹配
if (match) {
return true;
}
}
return false;
}
}
这里需要给启动类上加@ServletComponentScan ,将我们的过滤器扫描进来,参考1.4我已经引入了
4.2新增员工
请求方式
请求体
编写controller代码
@Override
public R addEmployee(HttpServletRequest request,Employee employee) {
// employee.setCreateTime(new Date()); //设置初始时间
// employee.setUpdateTime(new Date()); //设置修改时间
employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes())); //设置默认密码
// HttpSession session = request.getSession();
// long emp = (long)session.getAttribute("employee"); //获取当前登录的人的id
// employee.setCreateUser(emp); //当前登入的人id创建的这个数据,所以这里设置他的id
// employee.setUpdateUser(emp); //修改同样道理
int insert = employeeMapper.insert(employee);
if (insert > 0){
return R.success("新增员工成功");
}else {
return R.error("新增员工失败");
}
}
4.3编写全局异常处理(重点)
在4.2新增员工中有可能会有异常,这里我们员工表中的员工账户设置的是唯一,如果我们在客户端输入的是一个重复的账户,那么就会报异常
创建全局异常处理类
@RestControllerAdvice(annotations = {RestController.class, Controller.class})
@Slf4j
public class GlobalExceptionHandler {
/**
* @Description: 主键插入重复异常处理
* @param: e
* @return: org.example.common.R
* @Author: 刘某人
* @Date 2024/8/9 16:42
*/
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R exceptionHandler(SQLIntegrityConstraintViolationException e) {
log.info("SQLIntegrityConstraintViolationException: {}", e.getMessage());
if (e.getMessage().contains("Duplicate entry")) {
String[] split = e.getMessage().split(" ");
String msg = split[2] + "已经存在";
return R.error(msg);
}
return R.error("未知错误");
}
这里为什么要e.getMessage().contains("Duplicate entry")
是因为报插入异常的时候有这个,一但有了这个就说明他是插入异常
4.4员工分页查询
请求方式和请求体
这里需要加入mybatisplus的分页插件,我已经加入了,参考1.4项目启动入口
这里的分页有两个功能1是分页,2是看用户会不会根据姓名来查询,也就是条件查询(这里用的模糊查询)
controller
/**
* @Description:
* 员工分页
* @param: page
* @param: pageSize
* @param: name 有可能会根据名字分页,看客户端输入不输入
* @return: org.example.common.R
* @Author: 刘某人
* @Date 2024/8/9 17:15
*/
@GetMapping("/page")
public R page(@Param("page") int page,@Param("pageSize") int pageSize,@Param("name") String name){
log.info("page={},pageSize={},name={}",page,pageSize,name);
R r= employeeService.page(page,pageSize,name);
return r;
}
service实现类
@Override
public R page(int page, int pageSize, String name) {
Page<Employee> page1 = new Page<>(page, pageSize);
LambdaQueryWrapper<Employee> wrapper = new LambdaQueryWrapper<>();
//姓名过滤
wrapper.like(name != null,Employee::getName,name);
//排序一下
wrapper.orderByDesc(Employee::getUpdateTime);
//查询 ,封装给page1
employeeMapper.selectPage(page1,wrapper);
return R.success(page1);
}
分页的三个时机,①用户登录成功时,分页查询一次 ②用户使用条件查询的时候分页一次 ③跳转页面的时候分页查询一次
4.5启用/禁用员工账户
这里需要我们在每个实体类的id上加入
@JsonFormat(shape = JsonFormat.Shape.STRING)原因前面已经说明了,客户端接收不了64位的long类型,会导致精度损失,这个注解是让我们把要响应给客户端的long类型设置为string类型,这样就不会精度损失了
要将每一个实体类的id上加入这个注解,切记
请求方式 (这个请求也是4.6的请求)
请求体
controller
/**
* @Description:
* 用户admin修改其他员工的状态,根据id修改员工信息,两个功能
* 注意:这里员工id是雪花算法生成的long类型,客户端会精度损失导致发送过来的id是错误的
* 解决方式:需要在对应的实体类id上加上 @JsonFormat(shape = JsonFormat.Shape.STRING) 注解
* @param: request
* @param: employee
* @return: org.example.common.R
* @Author: 刘某人
* @Date 2024/8/9 18:22
*/
@PutMapping()
public R updateEmployee(HttpServletRequest request,@RequestBody Employee employee){
R r=employeeService.updateEmployee(request,employee);
return r;
}
service实现类
/**
* @Description:
* 精度损失解决方式 在实体id上加@JsonFormat(shape = JsonFormat.Shape.STRING)注解
* 解决方式二:消息转化器
* @param: request
* @param: employee
* @return: org.example.common.R
* @Author: 刘某人
* @Date 2024/8/9 19:10
*/
@Override
public R updateEmployee(HttpServletRequest request, Employee employee) {
System.out.println(employee); //这里的员工id是不对的,因为雪花算法生成19位数字,而客户端接收的时候损失精度,在传到服务端的时候这个id就错了,所以修改失败
// Long employee1 =(Long) request.getSession().getAttribute("employee");
// employee.setUpdateUser(employee1);
// employee.setUpdateTime(new Date());
// long id = Thread.currentThread().getId(); //获得当前线程id,为了后面的公共数据
// log.info("当前线程id为{}",id);
int i = employeeMapper.updateById(employee);
System.out.println(i);
return R.success("员工信息修改成功");
}
4.6编辑员工信息
进入编辑员工的时候会出现一个请求,,用来展示编辑的内容
请求方式
controller
@GetMapping("/{id}")
public R getEmployeeById(@PathVariable Long id){
R r=employeeService.getEmployeeById(id);
return r;
}
service实现类
@Override
public R getEmployeeById(Long id) {
Employee byId = employeeMapper.selectById(id);
return R.success(byId);
}
修改的请求和4.5的请求一样这里无需展示
5.菜品分类管理
5.1公共字段填充(重点)
我们发现,数据库中,有好多张表,他们这几张都有这几个字段
那么这4个字段就是公共字段,而我们每次插入和修改的时候,这4个字段都比较固定
所有我们可以使用mybatisplus的公共字段填充功能,这样我们以后插入和修改的时候,这4个字段就不需要我们自己来编写了
第一步
所有实体类上这四个字段都加上这几个注解,切记是所有实体类
这是在指定自动填充的策略
比如说 createtimes 这个属性,只有在插入的时候才设置,其他时候不需要设置,所以这里只给他设置了插入填充
而updateTime字段, 插入和修改的时候都需要设置,所有这里是 INSERT_UPDATE
插入和修改填充
@TableField(fill = FieldFill.INSERT) private Date createTime; @TableField(fill=FieldFill.INSERT_UPDATE) private Date updateTime; @TableField(fill = FieldFill.INSERT) private Long createUser; @TableField(fill=FieldFill.INSERT_UPDATE) private Long updateUser;
第二步设置一个处理类:在此类中为公共字段赋值
这个类需要实现MetaObjectHandler 接口
package org.example.common;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.util.Date;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
/**
* @Description: 自定义元数据对象处理器
* 用来处理数据库中公共数据
* @param: null
* @return:
* @Author: 刘某人
* @Date 2024/8/9 21:27
*/
@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {
// @Autowired 方法一:不推荐,如果session过多,不知道是用的哪个session
// private HttpSession session;
/**
* @Description:
* 插入操作,自动填充
* @param: metaObject
* @return: void
* @Author: 刘某人
* @Date 2024/8/9 21:33
*/
@Override
public void insertFill(MetaObject metaObject) {
// long employee = (long)session.getAttribute("employee");
Long id = BaseContext.getCurrentId();//当前线程的id,也是当前登录用户的id,从session中获取的
log.info("公共字段自动填充[insert]");
metaObject.setValue("createTime",new Date());
metaObject.setValue("updateTime",new Date());
metaObject.setValue("createUser",BaseContext.getCurrentId());
metaObject.setValue("updateUser",BaseContext.getCurrentId());
log.info(metaObject.toString());
}
/**
* @Description:
*更新操作,自动填充
* @param: metaObject
* @return: void
* @Author: 刘某人
* @Date 2024/8/9 21:33
*/
@Override
public void updateFill(MetaObject metaObject) {
// long employee = (long)session.getAttribute("employee");
log.info("公共字段自动填充[update]");
long id = Thread.currentThread().getId(); //获得当前线程id,为了后面的公共数据
log.info("当前线程id为{}",id);
metaObject.setValue("updateTime",new Date());
metaObject.setValue("updateUser",BaseContext.getCurrentId());
log.info(metaObject.toString());
}
}
在创建一个BaseContext类 用来获取动态员工id
我们用线程来获取当前登入员工的id,只要是登入成功,这里就会把员工的id存入session中
用ThreadLocal 可以将session存储起来
方法:
发现:一个线程对应一次请求,不同线程对应不同请求
BaseContext 这里把ThreadLocal 设置为静态,是为了让它一直存在,可以一直获去当前当前请求的session,这个类还把ThreadLocal 的两个方法封装了
/**
* @Description:
* 基于于ThreadLocal封装的工具类,用户保存和获取当前登录用户id
* @param: null
* @return:
* @Author: 刘某人
* @Date 2024/8/9 22:16
*/
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();
}
}
因为每一次请求都需要经过过滤器,而我们的BaseContext正好可以过滤器中获取当前请求的id(也是session)
又因为只有登入成功才有session,所以我们的BaseContext在过滤器判断用户登入成功的那个地方获取session
过滤器判断登入成功代码(部分)完整部分上面有在4.1
// 4.判断登入状态,如果已登录,则直接放行 Long employee = (Long)request.getSession().getAttribute("employee");//获取登入成功的session ,登入成功session有内容,未登入则没有也就是null if (employee != null) { //非空则放行 // long id = Thread.currentThread().getId(); //获得当前线程id,为了后面的公共数据 // log.info("当前线程id为{}",id); BaseContext.setCurrentId(employee); //将当前session的id存入当前线程 log.info("用户已登入,用户id为{}", employee); filterChain.doFilter(request, response); return; }
5.2新增分类
请求方式
请求体
controller
@PostMapping
public R save(@RequestBody Category category){
log.info("category:{}",category);
categoryService.save(category);
return R.success("新增分类成功");
}
5.3菜品类的分页
请求方式
controller
/**
* @Description:
* 菜品分类的分页功能
* @param: page
* @param: pageSize
* @return: org.example.common.R
* @Author: 刘某人
* @Date 2024/8/11 20:52
*/
@GetMapping("page")
public R sapage(int page, int pageSize){
return categoryService.sapage(page,pageSize);
}
service实现类
@Override
public R sapage(int page, int pageSize) {
Page<Category> page1 = new Page<>(page, pageSize);
LambdaQueryWrapper<Category> wrapper = new LambdaQueryWrapper<>();
wrapper.orderByAsc(Category::getSort); //根据sort字段升序排序
wrapper.orderByDesc(Category::getUpdateTime);//如果sort有一样的那就根据最后修改时间排序
categoryMapper.selectPage(page1,wrapper);
return R.success(page1);
}
5.4删除分类(重点)
首先需要判断当前分类中有没有菜品 (套餐需要判断,分类也需要判断,看他们各自下面有没有菜品与套餐明细)
如果有那就无法删除
没有那就可以删除
请求方式
我们需要根据客户端传过来的分类id中查询当前分类有没有菜品
这个时候需要用到多表查询
contoller
/**
* @Description:
*根据id删除分类 ,如果要删除的分类他的里面有菜品那就不能删除,需要多表关联
* @param: id
* @return: org.example.common.R
* @Author: 刘某人
* @Date 2024/8/9 23:28
*/
@DeleteMapping
public R delete(Long ids){
log.info("删除ids为:{}",ids);
// categoryService.removeById( ids);
categoryService.remove(ids);
return R.success("当前分类删除成功");
}
service实现类
@Override
public void remove(long ids) {
//1.查询当前分类是否关联了菜品,如果关联了,则抛出业务异常
LambdaQueryWrapper<Dish> wrapper = new LambdaQueryWrapper<>();//dish是菜品实体类
wrapper.eq(Dish::getCategoryId,ids);
long count = dishservice.count(wrapper);
if(count>0){ //说明菜品表里有数据
//关联了菜品,抛出业务异常
throw new CustException("当前分类关联了菜品,不能删除");
}
//2.查询当前分类是否关联了套餐,如果关联了,则抛出业务异常
LambdaQueryWrapper<Setmeal> wrapper1 = new LambdaQueryWrapper<>(); //Setmeal是套餐实体类
wrapper1.eq(Setmeal::getCategoryId,ids);
long count1 = setmealService.count(wrapper1);
if(count1>0){
//关联了套餐,抛出业务异常
throw new CustException("当前分类关联了菜品,不能删除");
}
//3.正常删除
categoryMapper.deleteById(ids);
}
这里删除失败是抛出一个业务异常,所以我们需要自定义这么一个异常,再在全局异常处理上处理它
自定义异常
/**
* @Description:
*自定义业务异常,在service层抛出
* @param: null
* @return:
* @Author: 刘某人
* @Date 2024/8/9 23:50
*/
public class CustException extends RuntimeException{
public CustException(String message){
super(message);
}
}
添加异常到全程异常处理
@ExceptionHandler(CustException.class)
public R exceptionHandler(CustException e) {
log.info("CustException: ", e.getMessage());
return R.error(e.getMessage());
}
5.5修改分类
请求方式
请求体
controller
/**
* @Description:
*分类的修改功能
* @param: category
* @return: org.example.common.R
* @Author: 刘某人
* @Date 2024/8/10 0:06
*/
@PutMapping
public R update(@RequestBody Category category){
log.info("category:{}",category);
categoryService.updateById(category);
return R.success("修改分类成功");
}
6.菜品管理业务功能
6.1文件的上传与下载(重点)
这两个功能后续会多次用到
文件上传
请求方式
请求体
在application中设置服务端存储文件的位置
#临时文件路径 项目启动后,有的功能要上传文件,这时文件会保存在临时文件中
reggie:
path: F:/Java图片/
controller
MultipartFile file 这里的file必须和请求体中的保持一致
@Value("${reggie.path}")
private String besepath;
/**
* @Description:
* 通用的文件上传下载
* @param: null
* @return:
* @Author: 刘某人
* @Date 2024/8/10 17:14
*/
/**
* @Description: 文件上传
* @param: file 用来接收客户来传来的文件,这里的参数名字和客户端的保持一致不然接收不到
* @return: org.example.common.R
* @Author: 刘某人
* @Date 2024/8/10 17:17
*/
@RequestMapping("/upload")
public R upload(MultipartFile file) {
//创建一个目录,将临时文件都放进去
File file1 = new File(besepath);
boolean directory = file1.isDirectory();//判断目录是否存在
if (directory == false) { //不存在
file1.mkdirs(); //创建目录
}
//file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会删除
log.info(file.toString());
String filename = file.getOriginalFilename();//原始文件名称 ,,原始文件名称虽然好记,但是存在文件名重复的问题,所以使用UUID来重命名文件
//这里使用UUID来重命名文件,避免文件名称重复导致文件覆盖
String uuidstring = UUID.randomUUID().toString(); //生成的uuid
String substring = filename.substring(filename.lastIndexOf("."));//获取文件后缀名
uuidstring = uuidstring + substring; //拼接完成文件名称
try {
//将临时文件转存到指定位置
file.transferTo(new File(besepath + uuidstring));
} catch (IOException e) {
e.printStackTrace();
}
return R.success(uuidstring); //返回文件名称
}
文件下载(响应给客户端,让客户端可以显示图片)
因为要响应给客户端,所以这里形参设置的是Httpservleresponse
用它的输出流来响应给客户端
@Value("${reggie.path}")
private String besepath;
/**
* @Description: 文件下载,响应给客户端
* @param: name 文件名称
* @param: response 输入流需要response来获取
* @return: void
* @Author: 刘某人
* @Date 2024/8/10 18:09
*/
@GetMapping("/download")
public void doload(String name, HttpServletResponse response) {
try {
//输入流读取文件内容
FileInputStream inputStream = new FileInputStream(besepath + name);
//因为要响应给客户端,所以这里用response来获取输出流,在游览器上显示
ServletOutputStream outputStream = response.getOutputStream();
response.setContentType("image/jpeg");//设置响应文件类型
byte[] bytes = new byte[1024];
int len = 0;
while ((len = inputStream.read(bytes)) != -1) {
outputStream.write(bytes, 0, len);
outputStream.flush(); //刷新一下
}
inputStream.close();//关闭资源
outputStream.close();//关闭资源
} catch (Exception e) {
throw new RuntimeException(e);
}
}
6.2新增菜品
这是新增菜品的页面
这个新增菜品功能 需要给我们发4次请求
第一个请求是:菜品分类
第二个请求是:文件上传;
第三个请求是:文件下载
第四个请求是:提交添加
刚进入页面的时候客户端就给我发了一次请求,也就是上面这个图片的菜品分类
请求方式
controller
/**
* @Description:
* 菜品管理中添加菜品中的分类下拉框,根据type查询分类
* @param: type
* @return: org.example.common.R
* @Author: 刘某人
* @Date 2024/8/10 18:57
*/
@GetMapping("/list")
public R list(Category category){
LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(category!=null,Category::getType,category.getType());
queryWrapper.orderByAsc(Category::getSort); //根据sort升序排序
// queryWrapper.orderByDesc(Category::getUpdateTime); //根据更新时间降序排序
List<Category> list = categoryService.list(queryWrapper);
return R.success(list);
}
请求方式
请求体
这里我们可以发现,我们的菜品表(dish),它并没有这个flavors字段(口味)
口味字段在菜品口味表中(dish_flavor)中
所以我们这里需要创建一个实体类,实体类中需要有菜品表,还需要有多个口味表
用这个实体类来接收客户端发送过来的数据
实体类(dishdto)
这个实体类继承了菜品实体类,也就拥有了它的属性
而且还有一个 集合用来装口味 (毕竟一个菜,可以多有个口味(比如爱吃辣,不要香菜等)可以理解为1张菜品表对应多个菜品口味表 1对多的关系)
/**
* @Description:
* 数据传输对象
* 这里继承了菜品,所以可以传输菜品的数据
* 这里还用集合包裹了口味,所以可以传输口味的数据
* 所以这个类可以接受前端提交的菜品和口味的数据
* @param: null
* @return:
* @Author: 刘某人
* @Date 2024/8/10 21:38
*/
@Data
public class DishDto extends Dish {
private List<DishFlavor> flavors = new ArrayList<>(); //口味,多个,一个菜多个口味
private String categoryName; //菜品分类名称
private Integer copies;
}
controller
/**
* @Description:
* 菜品管理中的新建菜品
* 因为新建菜品中有一个口未选择,所以这里需要多表
* @param: dto
* @return: org.example.common.R
* @Author: 刘某人
* @Date 2024/8/10 21:40
*/
@PostMapping()
public R addDish(@RequestBody DishDto dto){
log.info("dto{}",dto);
dishService.addDish(dto);
return R.success("新增菜品成功");
}
service实现类
菜品表的信息都全,所以可以直接插入
根据这里,我们发现,客户端并没有给我们的菜品口味表传递它对应的菜品id过来,
所以我们这里要给我们每一个菜品口味表添加菜品id
@Transactional //事务防止一个表插入成功一个插入失败,这样子会导致数据不一致
@Override
public void addDish(DishDto dto) {
//保存菜品的基本信息到dish表
dishMapper.insert(dto);
//菜品口味保存DishFlavor
Long dishId = dto.getId(); //获取菜品id
List<DishFlavor> flavors = dto.getFlavors(); //获取菜品口味信息
for (DishFlavor flavor : flavors) {
flavor.setDishId(dishId); //设置菜品id
}
dishFlavorService.saveBatch(flavors);
}
6.3菜品信息分页(难点)
分析
这个功能 需要发送二个请求
请求一:分页
多个请求二:文件下载 (因为数据都展示出来了,图片资源也需要展示)
展示的是所有菜品,但菜品表中并没有菜品分类,所以这里需要多表查询
请求方式
图片左上角还有一个查询,那个请求方式和分页一样
因为这里还有一个菜品分类需要给客户端响应过去
所以这里我们响应过去的实体类还是dishdto这个实体类(实体类代码在6.2)
controller
@Autowired
private DishService dishService;
@Autowired
private CategoryService category;
@GetMapping("/page")
public R getDishByCategoryId(int page,int pageSize ,String name){
Page<Dish> page1 = new Page<>(page, pageSize);
Page<DishDto> disdtoPage = new Page<>();
LambdaQueryWrapper<Dish> wrapper = new LambdaQueryWrapper<>();
wrapper.like(name!=null,Dish::getName,name); //模糊
wrapper.orderByDesc(Dish::getUpdateTime); //排序
dishService.page(page1,wrapper);
//对象拷贝
BeanUtils.copyProperties(page1,disdtoPage,"records");//除了records的,其他都拷贝
List<Dish> records = page1.getRecords(); //dish表所有数据
List<DishDto> list =new ArrayList<>();
//多表查询
for (Dish dish:records) {
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(dish,dishDto); //将菜品保存到dishdto中
Long categoryId = dish.getCategoryId(); //获取菜品对应的菜品分类的id
Category category1 = category.getById(categoryId); //根据菜品分类id查询到菜品分类
if(category1!=null){
dishDto.setCategoryName(category1.getName()); //将菜品分类名称保存起来
}
list.add(dishDto); //添加到 list中
}
disdtoPage.setRecords(list);
return R.success(disdtoPage); //把dishdtopage分页响应回去
}
6.4修改菜品(难点)
分析:我们刚点进来修改菜品,这里就展示了未修改前的所有数据
也就是说:我们还没修改,客户端就已经给我们发送了3次请求
第一次请求:菜品分类
第二次请求:图片下载
第三次请求:显示要未修改前的信息
在加上最后的修改,这里一共用到了4次请求
第三次请求
请求方式
controller
/**
* @Description:
*菜品管理,修改菜单中的展示内容,需要用到多表查询
* @param: id
* @return: org.example.common.R
* @Author: 刘某人
* @Date 2024/8/10 23:39
*/
@GetMapping("/{id}")
public R getDishById(@PathVariable Long id){
DishDto dto = dishService.getDishByid(id);
return R.success(dto);
}
service实现类
/**
* @Description:根据id查询对应信息,以及他的口味信息,多表查询
* @param: id
* @return: org.example.dto.DishDto
* @Author: 刘某人
* @Date 2024/8/10 23:33
*/
@Override
public DishDto getDishByid(Long id) {
//查询菜品基本信息,从dish表查询
Dish dish = this.getById(id); //这里的this ,可以理解为DishService
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
@PutMapping()
public R updateDish(@RequestBody DishDto dto){
dishService.updateDish(dto);
return R.success("修改成功");
}
service实现类
梳理一下逻辑
首先,菜品表可以直接修改
菜品口味表:修改前
修改后
这里发现和之前的完全不一样,所有这里采用的策略是
先把菜品对应的菜品口味全部删除
再把从客户端获取的菜品口味插入
这样也是实现了修改的功能
@Override
public void updateDish(DishDto dto) {
//更新dish表
this.updateById(dto); //这里的this就是DishService
//清理当前菜品对应口味数据
LambdaQueryWrapper<DishFlavor> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DishFlavor::getDishId, dto.getId());//根据菜品id查询口味信息
dishFlavorMapper.delete(wrapper);//删除口味信息
//添加提交过来的口味数据
List<DishFlavor> flavors = dto.getFlavors();
for (DishFlavor flavor : flavors) {
flavor.setDishId(dto.getId()); //设置菜品id
}
dishFlavorService.saveBatch(flavors);//保存口味信息
}
6.5批量删除功能
批量删除和删除的请求方式是一样的,所以单个的就不需要写
请求方式
controller
/**
* @Description:
* 菜品管理的批量删除功能 ,删除菜品并且删除它在服务端保存的图片
* @param: ids
* @return: org.example.common.R
* @Author: 刘某人
* @Date 2024/8/10 19:24
*/
@DeleteMapping
public R delete( @RequestParam List<Long> ids){
dishService.delete(ids);
return R.success("删除成功");
}
这里我的批量删除是把数据库的信息删除,把对应的图片从磁盘中删除
service实现类
@Value("${reggie.path}")
private String sfile;
@Override
public void delete(List<Long> ids) {
System.out.println(ids + "-------------------------------------------------------------");
LambdaQueryWrapper<Dish> wrapper = new LambdaQueryWrapper<>();
wrapper.in(Dish::getId, ids);
List<Dish> listfile = dishMapper.selectList(wrapper); //根据id查询到所有菜品
File file = new File(sfile); //这里的sfile就是我们之前定义的服务端存放图片的文件夹
for (Dish dish : listfile
) {
File file1 = new File(file + "/" + dish.getImage()); //从菜品属性中获取的图片的名称
boolean delete = file1.delete(); //删除图片
if (true) {
System.out.println(file1.getPath() + "------------");
System.out.println(dish.getImage() + "删除成功");
} else {
System.out.println("删除失败");
}
}
this.removeByIds(ids); //this相当于DishService ,删除菜品信息
}
6.6批量停售启售
批量停售启售和停售启售的请求方式是一样的,所以单个的就不需要写
请求方式
controller
/**
* @Description:
* 批量修改菜品状态
* @param: status
* @param: ids
* @return: org.example.common.R
* @Author: 刘某人
* @Date 2024/8/10 21:34
*/
@PostMapping("status/{status}")
public R updateStatus(@PathVariable int status,@RequestParam List<Long> ids){
dishService.updateStatus(status,ids);
return R.success("修改成功");
}
service实现类
@Override
public void updateStatus(int status, List<Long> ids) {
LambdaQueryWrapper<Dish> wrapper = new LambdaQueryWrapper<>();
wrapper.in(Dish::getId, ids); //根据id查询
// List<Dish> list = dishService.list(wrapper);
List<Dish> list = dishMapper.selectList(wrapper); //查找到所有要修改的菜品
for (Dish dish : list
) {
dish.setStatus(status); //给每一个菜品设置它的状态
}
// dishService.updateBatchById(list);
this.updateBatchById(list); //提交修改
}
7.套餐管理业务功能
未完待续
标签:return,吉瑞,笔记,public,外卖,菜品,employee,new,id From: https://blog.csdn.net/weixin_61808639/article/details/141109118