移动端部分的文章:/spost/72426…
项目建立
学生作者:吃饱饱坏蜀黍
springboot版本:2.7.12
jdk版本:20
开发修正器东西:IDEA 2023.1.2
数据库:Mysql(社区版)8.0.32
数据库办理东西:Navicat Premium 16
操作体系:window10
链接:pan.baidu.com/s/19YTUCBtg… 提取码:arqf
pom.xml
创立springboot项目,构建maven项目,导入所需的jar包,所运用到的jar包下面都有注解。
<?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.12</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.HuaiShuShu</groupId>
<artifactId>RuiJiWaiMai</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>20</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--Mysql较新版本的驱动-->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!--lombok东西包-->
<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>
<!--mybatispuls的jar包-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<!--操作json的jar包-->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.32</version>
</dependency>
<!--通用的语言包-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<!--druid数据源办理-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.18</version>
</dependency>
<!--热布置包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<!--mybatispuls的代码生成器-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.31</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>
sql文件
sql文件教长,我就不在这儿放置了,需求的小伙伴能够经过黑马的小程序获取,也能够经过我的网盘地址获取。
yml文件
在yml文件中装备对应的特点:
server:
port: 80
spring:
application:
#界说运用称号(默以为工程的称号,可选)
name: RuiJiWaiMai
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ruijiwaimai?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: root
devtools:
restart:
# 设置不参加热布置的文件或文件夹
exclude: static/**,public/**,config/application.yml
# 设置热布置的敞开状况
enabled: true
mybatis-plus:
#装备类型别名所对应的包
type-aliases-package: com.atguigu.pojo
configuration:
#在映射实体或许特点时,将数据库中表名和字段名中的下划线去掉,依照驼峰命名法映射
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: ASSIGN_ID
mybatis-plus代码生成器
在test测验文件夹下创立一个mybatis-plus代码生成器的类运转以下代码,快速生成整个项意图结构。
package com.huaishushu;
import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.OutputFile;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import java.util.Collections;
/**
* @Author ChiBaoBaoHuaiShuShu
* @Date 2023/4/19 13:25
* @PackageName:com.atguigu
* @Description: 代码生成器
* @Version 1.0
*/
public class FastAutoGeneratorTest {
public static void main(String[] args) {
FastAutoGenerator.create("jdbc:mysql://127.0.0.1:3306/ruijiwaimai? characterEncoding=utf-8&userSSL=false", "root", "root")
.globalConfig(builder -> {
builder.author("ChiBaoBaoHuaiShuShu") // 设置作者
//.enableSwagger()// 敞开 swagger 模式
.fileOverride() // 掩盖已生成文件
.outputDir("D://GongZuo//IDEA//XianMu//RuiJiWaiMai-springboot-MybatisPlus//RuiJiWaiMai//src//main/java"); // 指定输出目录
})
.packageConfig(builder -> {
builder.parent("com.huaishushu") // 设置父包名
//.moduleName("") // 设置父包模块名
.pathInfo(Collections.singletonMap(OutputFile.mapperXml, "D://GongZuo//IDEA//XianMu//RuiJiWaiMai-springboot-MybatisPlus//RuiJiWaiMai//src//main/resources//mapper"));// 设置mapperXml生成途径
})
.strategyConfig(builder -> {
builder.addInclude("address_book") // 设置需求生成的表名
.addInclude("category")
.addInclude("dish")
.addInclude("dish_flavor")
.addInclude("employee")
.addInclude("order_detail")
.addInclude("orders")
.addInclude("setmeal")
.addInclude("setmeal_dish")
.addInclude("shopping_cart")
.addInclude("user")
.addTablePrefix("t_", "c_"); // 设置过滤表前缀
})
.templateEngine(new FreemarkerTemplateEngine()) // 运用Freemarker 引擎模板,默许的是Velocity引擎模板
.execute();
}
}
项目结构
最终则能够取得对应的项目全体结构如下图:
其间的common文件和config文件时笔者第二天的时分才创立的,这个笔记也是第二天的时分才开始记录的所以就懒的去掉啦,而且我所用的jar包都是比较新版本的了和yml文件中的特点装备也有一些自己改的地方,咱们能够和黑马的材料对比一下。
还有便是咱们用MP的代码生成器生成的项目结构中,咱们需求把Controller层中的一切类中的@Controller注解替换成@RestController注解,这是由于咱们和前端约定好了回来一个R类型的实体类。
R实体类
package com.huaishushu.common;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
/**
* 通用回来成果,服务端呼应的数据最终都会封装成此目标
* @param <T>
*/
@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;
}
}
前端的代码文件下载咱们能够去b站黑马瑞吉外卖的视频下获取,也能够经过我的网盘地址下载,我的就没黑马那里的那么全了,我只下载了材料,没下载视频,所以需求视频的小伙伴能够去黑马那儿下载,下载好后的同学把前端的resources->static文件夹下即可。
温馨提示
后边的进程中,由于mybatispuls太长了,后期我或许会用MP来代指它,所以小伙伴们不要误会了。
事务开发
个人观点
在事务开发这块的时分,黑马的视频中教师许多都是直接在Controller层操控层中直接调用了MP供给的办法来完结的事务的开发,这是我以为不太合理的地方,由于咱们在学校学习MVC结构的时分咱们知道Controller层是指进行调用接口回来数据或参数和前端做交互,所今后边许多事务模块的开发中,我许多都是选用了MVC的结构来编写。
温馨提示
咱们运用黑马材猜中的sql文件生成数据库和表后,在构建项目结构的时分,不管小伙伴运用的是MP的代码生成器仍是黑马材料内的实体类,咱们都应该去查看一下数据库中的字段和实体类中的特点是否对应,以及有的模块开发中会涉及到一些逻辑删去,这时分咱们能够看一下数据库中是否有用来逻辑删去的字段,这个项目中运用的是is_delete来充当逻辑删去的字段。
因而咱们要留意数据库中是否有这个字段,我在开发进程中就发现有的表是短少这个字段的,可是事务开发中又需求逻辑删去,而且实体类中也有is_delete这个特点,所以能够判别是黑马材料的sql文件有缺失缝隙,所以咱们应该查看表字段和实体类特点,并及时弥补上去,而且由于需求运用到逻辑删去,所以应该在实体类的isDelete特点上增加@TableLogic注解。
假如是数据库短少了is_delete字段,则咱们为其短少的表增加上该字段即可。
职工模块
职工登录
代码开发
4)在Controller中创立登录办法 处理逻辑如下: 1、将页面提交的暗码password进行md5加密处理2、依据页面提交的用户名username查询数据库 3、假如没有查询到则回来登录失利成果 4、暗码比对,假如不共同则回来登录失利成果 5、查看职工状况,假如为已禁用状况,则回来职工已禁用成果6、登录成功,将职工id存入Session并回来登录成功成果
因而咱们需求先在EmployeeController类中界说对应的登录办法login,并依据对应的前台传递回来的employee的json数据进行封装赋值给login办法的参数,而且在参数中界说HttpServletRequest目标来获取Sesion目标并将用户信息同享到会话层中。
Controller层
/**
* <p>
* 职工信息 前端操控器
* </p>
*
* @author ChiBaoBaoHuaiShuShu
* @since 2023-05-28
*/
@Slf4j
@RestController
@RequestMapping("/employee")
public class EmployeeController {
@Autowired
IEmployeeService employeeService;
/**
* 职工登录
* @param request
* @param employee
* @return
*/
@PostMapping("/login")
public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee) {
String password = employee.getPassword();
//运用DigestUtils东西类进行md5加密处理
password = DigestUtils.md5DigestAsHex(password.getBytes());
//依据用户名进行查询
Employee emp = employeeService.login(employee.getUsername());
if (emp == null) {
return R.error("登录失利,账号或暗码过错!!!");
}
if (!emp.getPassword().equals(password)) {
return R.error("登录失利,账号或暗码过错!!!");
}
//查询职工状况,假如为已禁用状况,则放回职工已禁用成果
if (emp.getStatus() == 0) {
return R.error("账号已禁用");
}
request.getSession().setAttribute( "employee", emp.getId());
return R.success(emp);
}
}
在经过IEmployeeService目标调用的办法来获取Employee目标时,我的获取办法有些不同,由于我以为Controller层不因和数据库直接进行交互和事务操作,因而此处我在对应的service层中界说了对应的login办法,其间传递的参数为String类型的username,回来类型是Employee类型的目标。
service接口
/**
* <p>
* 职工信息 服务类
* </p>
*
* @author ChiBaoBaoHuaiShuShu
* @since 2023-05-28
*/
public interface IEmployeeService extends IService<Employee> {
/**
* 依据用户的username查询是否存在
* @param username
* @return
*/
Employee login(String username);
}
service完结类
/**
* <p>
* 职工信息 服务完结类
* </p>
*
* @author ChiBaoBaoHuaiShuShu
* @since 2023-05-28
*/
@Service
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements IEmployeeService {
@Autowired
EmployeeMapper employeeMapper;
/**
* 依据用户的username查询是否存在
* @param username
* @return
*/
@Override
public Employee login(String username) {
LambdaQueryWrapper<Employee> lqw = new LambdaQueryWrapper<>();
lqw.eq(Employee::getUsername, username);
Employee employee = employeeMapper.selectOne(lqw);
return employee;
}
}
这样Controller层调用办法则可获取到Employee或许null,之后则依照前面的逻辑进行判别并回来对应的信息即可。
职工退出
职工退出则相对简单许多,删去保存在Session目标中的Employee就能够了。
/**
* 职工退出
* @param request
* @return
*/
@PostMapping("/logout")
public R<String> logout(HttpServletRequest request) {
//整理Session中保存的当时登录职工的id
request.getSession().removeAttribute("employee");
return R.success("退出成功");
}
完善登录功用
前面咱们现已完结了后台体系的职工登录功用开发,可是还存在一个问题:用户假如不登录,直接拜访体系首页面,照样能够正常拜访。 这种规划并不合理,咱们希望看到的作用应该是,只要登录成功后才能够拜访体系中的页面,假如没有登录则跳转到登录页面 那么,详细应该怎么完结呢? 答案便是运用过滤器或许阻拦器,在过滤器或许阻拦器中判别用户是否现已完结登录,假如没有登录则跳转到登录页面
代码完结
完结进程 1、创立自界说过滤器LoginCheckFilter 2、在发动类上参加注解@ServletComponentScan 3、完善过滤器的处理逻辑
这块和视频中的相同,挑选的是过滤器来完结的登录功用,由于登录和退出的恳求以及拜访静态资源的恳求是不应该阻拦的,因而需求在装备不阻拦的恳求途径,而且界说一个check办法来把恳求逐个对比,而且界说了AntPathMatcher目标来辅助途径的比对,详细功用如下面所示:
/**
* @Author HuaiShuShu
* @Date 2023/5/28 21:56
* @PackageName:com.huaishushu.filter
* @Description: TODO
* @Version 1.0
*/
@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、获取本次恳求的URI
String requestURI = request.getRequestURI();
//界说不需求处理的恳求途径
String[] urls = new String[]{
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**"
};
//2、判别本次恳求是否需求处理
boolean check = check(urls, requestURI);
// 3、假如不需求处理,则直接放行
if (check) {
filterChain.doFilter(request,response);
return;
}
// 4、判别登录状况,假如已登录,则直接放行
if (request.getSession().getAttribute("employee") != null) {
filterChain.doFilter(request,response);
return;
}
// 5、假如未登录则回来未登录成果,经过输出流办法想客户端页面呼应数据
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
return;
}
/**
* 途径匹配,查看本次恳求是否需求放行
* @param urls
* @param requestURI
* @return
*/
public boolean check(String[] urls,String requestURI) {
for (String url : urls) {
//逐个进行匹配,假如匹配上了则直接回来true
boolean match = PATH_MATCHER.match(url, requestURI);
if (match) {
return true;
}
}
return false;
}
}
新增职工
在数据库中的employee表中咱们对username做了唯一束缚,这是为了保证账号的唯一性,而咱们的新增职工便是向username中增加信息。
代码开发
在开发代码之前,需求整理一下整个程序的履行进程: 1、页面发送ajax恳求,将新增职工页面中输入的数据以json的办法提交到服务端
2、服务端Controller接纳页面提交的数据并调用Service将数据进行保存
3、Service调用Mapper操作数据库,保存数据
事务以及逻辑剖析可知,当时台在增加职工的页面中输入并点保存后,后发送对应的恳求给服务器传达给后端,因而咱们需求接纳恳求中的数据,并将其存入数据库中。
因而需求界说在Controller层中界说增加职工的办法save,代码如下所示:
Controller层
/**
* 职工信息分页查询
* @param page
* @param pageSize
* @param name
* @return
*/
@GetMapping("/page")
public R<Page> page(Integer page, Integer pageSize, String name) {
Page<Employee> pageInfo = employeeService.page(page, pageSize, name);
return R.success(pageInfo);
}
service接口
/**
* 新增职工信息
* @param employee
* @return
*/
boolean save(HttpServletRequest request, Employee employee);
service完结类
/**
* 新增职工信息
* @param employee
* @return
*/
@Override
public boolean save(HttpServletRequest request, Employee employee) {
//设置初始暗码并进行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);
if (employeeMapper.insert(employee) > 0) {
return true;
}
return false;
}
其间的employeeService.save()的办法是mybatisplus中封装好的保存办法,咱们直招待用即可,这样咱们在增加职工页面输入好数据后点击保存即可增加数据到数据库了。
呈现的问题,由于数据库中employee表中的username增加了唯一束缚,因而当咱们在重复增加相同的username的职工信息时,则会在IDEA编译器的操控台中呈现报错。
因而咱们需求装备大局反常来捕获反常,并回来给前端。
咱们在common文件夹下新建一个GlobalExceptionHandler类来大局反常处理,其间代码如下:
/**
* @Author HuaiShuShu
* @Date 2023/5/29 0:06
* @PackageName:com.huaishushu.common
* @Description: 大局反常处理
* @Version 1.0
*/
//敞开反常处理,annotations特点能够装备需求阻拦的方位。
@ControllerAdvice(annotations = {RestController.class, Controller.class})
//增加@ResponseBody,由于之后捕获反常今后需求回来json办法的数据
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {
/**
* 反常处理办法
*
* @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("不知道过错!!!");
}
}
其间的SQLIntegrityConstraintViolationException.class为数据库反常类,将其装备给@ExceptionHandler注解能够只捕获这个类型的反常信息并回来,之后的这处String[] split = ex.getMessage().split(” “);String msg = split[2] + “已存在….”;是由于报错的提示信息为
运用split办法后,其处于索引2的方位,因而才运用这个的。
职工信息分页查询
代码开发
在开发代码之前,需求整理一下整个程序的履行进程:页面发送ajax恳求,将分页查询参数(page、pageSize、name)提交到服务端12、服务端Controller接纳页面提交的数据并调用Service查询数据。
- Service调用Mapper操作数据库,查询分页数据
- Controller将查询到的分页数据呼应给页面
- 页面接纳到分页数据并经过ElementUl的Table组件展现到页面上
经过这边的代码开发剖析,咱们知道需求在Controller层界说一个对应的办法来接纳阅读器发送url恳求并接纳里边的这三个参数page、pageSize、name,那么咱们能够在EmployeeController类中界说一个分页办法public R page(Integer page, Integer pageSize, String name)来完结这项功用。
一起咱们还需求在Config中界说MybatisPlus的装备类,来加载咱们的分页组件,这个是MybatisPlus内部就现已封装好的分页查询,详细的完结源码感兴趣的小伙伴能够去MybatisPlus的官网了解。
因而,咱们在config文件夹下创立一个叫MybatisPlusConfig的类来加载MybatisPlus的一些插件。
MybatisPlusConfig
package com.huaishushu.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;
/**
* @Author HuaiShuShu
* @Date 2023/5/29 19:18
* @PackageName:com.huaishushu.config
* @Description: 装备MP的分页插件
* @Version 1.0
*/
@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(Integer page, Integer pageSize, String name) {
Page<Employee> pageInfo = employeeService.page(page, pageSize, name);
return R.success(pageInfo);
}
service接口
/**
* 职工信息分页查询
* @param employee
* @return
*/
@PutMapping
public R<String> update(@RequestBody Employee employee) {
return null;
}
service完结类
/**
* 职工信息分页查询
* @param page
* @param pageSize
* @param name
* @return
*/
@Override
public Page<Employee> page(Integer page, Integer pageSize, String name) {
//构建分页结构器
Page pageInfo = new Page(page, pageSize);
LambdaQueryWrapper<Employee> lqw = new LambdaQueryWrapper<>();
lqw.like(StringUtils.isNoneEmpty(name), Employee::getName, name);
lqw.orderByDesc(Employee::getUpdateTime);
//此处不回来是由于查询好后的值是直接赋值给了上面的分页结构器了pageInfo
employeeMapper.selectPage(pageInfo, lqw);
return pageInfo;
}
查询出来的信息,咱们按时刻排序了一下。
问题:这处为什么不运用@RequestParam或@PathVariable来进行参数绑定呢,这样届时不是更好看到是什么和什么参数绑定了吗?
答:办法中的page、pageSize、name是经过url中参数的办法传递回来的,而且参数中不必定会有name,所以办法中参数绑定接纳这块不需求运用@RequestParam注解,不然增加上该注解后由于该注解特点中required的值默许是true,导致有必要传递回来对应的参数,而@PathVariable注解绑定参数的办法是经过url恳求中占位符的办法来绑定参数的,所以二者都不可行!!!
(笔者在写这块功用的时分就在想为什么不用@RequestParam或@PathVariable来绑定参数,然后测验之后阅读器一向报404而疑惑,后来查询材料后才解惑,仍是根底不够扎实)
启用/禁用职工账号
需求剖析
在职工办理列表页面,能够对某个职工账号进行启用或禁用操作。账号禁用的职工不能登录体系,启用后的职工能够正常登录。
需求留意,只要办理员(admin用户)能够对其他普通用户进行启用、禁用操作,所以普通用户登录体系后启用、禁用按钮不闪现。
在办理员权限的用户中,咱们能够看到职工信息修正栏中又禁用以及修正按钮,而普通用户没有,这是由于前端static/backend/page/member/list.html从阅读器中获取了咱们之前职工登录时保存在阅读器中的userInfo的信息,并做判别。
userInfo在阅读器中的方位。
做判别是否闪现启用或禁用按钮,然后是admin用户则闪现启用/禁用按钮。
代码开发
在开发代码之前,需求整理一下整个程序的履行进程
1、页面发送ajax恳求,将参数(id、status)提交到服务端
2、服务端Controller接纳页面提交的数据并调用Service更新数据
3、Service调用Mapper操作数据库
更改启用/禁用本质便是一个更新修正操作,所以只需求对数据库中employee表的响运用户的status进行更新修正罢了,那么是修正操作那么恳求办法一般都是PUT,而咱们这个项目中的前端也的确发送的恳求是PUT的恳求,所以咱们Controller层中的办法应该运用@PutMapping注解了,而且其传递的参数一个是id一个是status参数则咱们能够运用Employee目标来接纳参数。
Controller层
/**
* 启用/禁用职工账号
* @param employee
* @return
*/
@PutMapping
public R<String> update(@RequestBody Employee employee) {
if (employeeService.update(employee)) {
if (employee.getStatus() == 1) {
return R.success("启用成功");
} else {
return R.success("禁用成功");
}
}
return R.error("操作失利");
}
service接口
/**
* 启用/禁用职工账号
* @param employee
* @return
*/
boolean update(Employee employee);
service完结类
/**
* 启用/禁用职工账号
*
* @param employee
* @return
*/
@Override
public boolean update(HttpServletRequest resource, Employee employee) {
//经过session获取当时登录账号的id
Long empId = (Long) resource.getSession().getAttribute("employee");
employee.setUpdateTime(LocalDateTime.now());
employee.setUpdateUser(empId);
//依据id修正即可
if (employeeMapper.updateById(employee) > 0) {
return true;
}
return false;
}
service完结类这边的update办法这块调用的是mP的updateById办法依据id来修正就能够了。
呈现问题
当咱们以为写好的时分运转操作后,会发现IDEA操控台闪现一下sql语句,其成果闪现0,说明咱们没有成功对数据库进行更新修正。
呈现这种的原因是由于js保存long类型的数据呈现了精度保存的误差问题,表中数据和阅读器传递回来的id数据明显不同。
解决办法
将后端传递给前端的数据转化成string字符串类型的数据,这样传递的后js就能够正常保存。
详细完结进程:
- 供给目标转化器Jackson0bjectMapper,依据]ackson进行Java目标到json数据的转化 (材猜中现已供给,直接仿制到项目中运用)
- 在WebMvcConfig装备类中扩展Spring mvc的音讯转化器,在此音讯转化器中运用供给的目标转化器进行lava目标到ison数据的转化/放水
package com.huaishushu.common;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;
/**
* 目标映射器:依据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);
}
}
以上类的目标转化器,咱们将其仿制粘贴到common文件夹下即可,
修正职工信息
代码开发
在开发代码之前需求整理一下操作进程和对应的程序的履行流程:
- 点击修正按钮时,页面跳转到add.html,并在url中带着参数[职工id]
- 在add.htm[页面获取url中的参数[职工id]
- 发送ajax恳求,恳求服务端,一起提交职工id参数
- 服务端接纳恳求,依据职工id查询职工信息,将职工信息以json办法呼应给页面
- 页面接纳服务端呼应的ison数据,经过VUE的数据绑定进行职工信息回显
- 点击保存按钮,发送aiax恳求,将页面中的职工信息以ison办法提交给服务端
- 服务端接纳职工信息,并进行处理,完结后给页面呼应
- 页面接纳到服务端呼应信息后进行相应处理
留意:
add.html页面为公共页面,新增职工和修正职工都是在此页面操作
咱们在页面中点击修正按钮后会跳转到add这个页面,而且所发送的url恳求中是get类型还带着了id。
然后在页面也发送了下面的这个get恳求,来获取职工的信息
那么咱们只需求在Controller层增加一个getByid的办法即可来接纳id来查询职工信息。
/**
* 依据id查询职工信息
* 其时搭配修正职工信息运用的
* @param id
* @return
*/
@GetMapping("{id}")
public R<Employee> getById(@PathVariable("id") Long id) {
Employee employee = employeeService.getById(id);
log.info("依据id查询到的职工信息:"+employee);
if (employee != null) {
return R.success(employee);
}
//这种情况只要在页面刷新过慢时会呈现,或呈现线程安全时呈现
return R.error(null);
}
公共字段主动填充(元数据处理器)
代码完结
Mvbatis Plus公共字段主动填充,也便是在刺进或许更新的时分为指定字段赋予指定的值,运用它的优点便是能够一致对这些字段进行处理,避免了重复代码。
完结进程:
- 在实体类的特点上参加@TableField注解,指定主动填充的策略
- 依照结构要求编写元数据目标处理器,在此类中一致为公共字段赋值,此类需求完结MetaObjectHandler接口
在Employee这个实体类上对应的特点上增加@TableField注解,而该注解时MP为咱们供给的一个公共字段填充的字段,运用它需求编写元数据目标处理器并完结MetaObjectHandler接口。
@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;
/**
* 状况 0:禁用,1:正常
*/
private Integer status;
/**
* 创立时刻
*/
@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;
}
FieldFill枚举中有的特点
public enum FieldFill {
/**
* 默许不处理
*/
DEFAULT,
/**
* 刺进时填充字段
*/
INSERT,
/**
* 更新时填充字段
*/
UPDATE,
/**
* 刺进和更新时填充字段
*/
INSERT_UPDATE
}
元数据目标处理器MyMetaObjectHandler类创立在common文件夹下,里边关于id的获取先写死,由于咱们还没有办法直接获取session。
/**
* @Author HuaiShuShu
* @Date 2023/5/30 23:06
* @PackageName:com.huaishushu.common
* @Description: 自界说元数据目标处理器
* @Version 1.0
*/
@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {
/**
* 刺进操作主动填充
* @param metaObject 元目标
*/
@Override
public void insertFill(MetaObject metaObject) {
metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("createUser", new Long(1));
metaObject.setValue("updateUser", new Long(1));
}
/**
* 更新时主动填充
* @param metaObject 元目标
*/
@Override
public void updateFill(MetaObject metaObject) {
Long id = Thread.currentThread().threadId();
log.info("线程:{}",id);
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("updateUser", new Long(1));
}
}
功用完善
前面咱们现已完结了公共字段主动填充功用的代码开发,可是还有一个问题没有解决,便是咱们在主动填充createuser和updateUser时设置的用户id是固定值,现在咱们需求改造成动态获取当时登录用户的id。
有的同学或许想到,用户登录成功后咱们将用户id存入了Httpession中,现在我从Httpession中获取不就行了?留意,咱们在MvMetaObiectHandler类中是不能取得HtoSession目标的,所以咱们需求经过其他办法来获取登录用户id。
ThreadLocal
能够运用ThreadLocal来解决此问题,它是JDK中供给的一个类
在学习ThreadLocal之前,咱们需求先确认一个事情,便是客户端发送的每次http恳求,对应的在服务端都会分配一个新的线程来处理,在处理进程中涉及到下面类中的办法都归于相同的一个线程:
- LoginCheckFilter的doFilter办法
- EmployeeController的update办法
- MyMetaobjectHandler的updateFill办法
每个人的线程或许不共同,而且咱们要留意看喔,由于咱们没刷新一次就要发送一次http恳求,这儿边的线程就有或许会改变,所以咱们要留意看线程的id。
什么是ThreadLocal?
ThreadLocal并不是一个Thread,而是Thread的局部变量。当运用ThreadLocal维护变量时,ThreadLocal为每个运用该变量的线程供给独立的变量副本,所以每一个线程都能够独登时改变自己的副本,而不会影响其它线程所对应的副本。ThreadLocal为每个线程供给独自一份存储空间,具有线程隔离的作用,只要在线程内才能获取到对应的值,线程外则不能拜访。
ThreadLocal常用办法:
- public void set(T value) 设置当时线程的线程局部变量的值
- public T get() 回来当时线程所对应的线程局部变量的值
咱们能够在LoginCheckFilter的doFilter办法中获取当时登录用户id,并调用ThreadLocal的set办法来设置当时线程的线程局部变量的值 (用户id),然后在MyMetabiectHandler的updateFil办法中调用ThreadLocal的get办法来取得当时 线程所对应的线程局部变量的值 (用户id)。
完结进程:
- 编写BaseContext东西类,依据ThreadLocal封装的东西类
- 在LoginCheckFilter的doFilter办法中调用BaseContext来设置当时登录用户的id
- 在MyMetaobjectHandler的办法中调用BaseContext获取登录用户的id
在common文件夹下创立BaseContext东西类
public class BaseContext {
private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
/**
* 设置值
* @param id
*/
public static void setCurrentId(Long id) {
threadLocal.set(id);
}
/**
* 获取值
* @return
*/
public static Long getCurrentId() {
return threadLocal.get();
}
}
LoginCheckFilter过滤器类中的doFilter办法
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
//1、获取本次恳求的URI
String requestURI = request.getRequestURI();
log.info("阻拦到恳求:{}",requestURI);
//界说不需求处理的恳求途径
String[] urls = new String[]{
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**"
};
//2、判别本次恳求是否需求处理
boolean check = check(urls, requestURI);
// 3、假如不需求处理,则直接放行
if (check) {
filterChain.doFilter(request,response);
return;
}
// 4、判别登录状况,假如已登录,则直接放行
if (request.getSession().getAttribute("employee") != null) {
log.info("用户已登录,登录id:" + request.getSession().getAttribute("employee"));
//设置线程局部变量,供公共字段主动填充用
Long empId = (Long) request.getSession().getAttribute("employee");
BaseContext.setCurrentId(empId);
filterChain.doFilter(request, response);
return;
}
// 5、假如未登录则回来未登录成果,经过输出流办法想客户端页面呼应数据
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
return;
}
元数据目标处理器MyMetaObjectHandler中的id获取能够直接经过线程局部变量获取了。
/**
* @Author HuaiShuShu
* @Date 2023/5/30 23:06
* @PackageName:com.huaishushu.common
* @Description: 自界说元数据目标处理器
* @Version 1.0
*/
@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {
/**
* 刺进操作主动填充
* @param metaObject 元目标
*/
@Override
public void insertFill(MetaObject metaObject) {
metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("createUser", BaseContext.getCurrentId());
metaObject.setValue("updateUser", BaseContext.getCurrentId());
}
/**
* 更新时主动填充
* @param metaObject 元目标
*/
@Override
public void updateFill(MetaObject metaObject) {
Long id = Thread.currentThread().threadId();
log.info("线程:{}",id);
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("updateUser", BaseContext.getCurrentId());
}
}
温馨提示
其真实MyMetaObjectHandler类中直接主动安装一个session目标也是能够获取到id的,可是这样会有线程危险,不利于安全,在多用户环境下线程安全尤为重要。各位同僚也能够试一下其他的办法,可是也要必定要考虑线程安全的问题。
分类信息模块
温馨提示
假如有的小伙伴在做这个模块的事务开发的时分遇到了数据库表里的数据无法给实体类赋值的话,留意是看实体类中是不是多了一个isDeleted特点,这个特点是用来做逻辑删去的,可是黑马供给的sql文件中并没有在表中生成这个字段,所以才会无法正常赋值,咱们只需求在数据库中增加上is_deleted即可,而且要留意其默许赋值为0。
笔者由于在做的时分应该没遇到问题,所以当时没留意,后边写菜品信息删去的时分才留意到该问题,所今后续都有弥补。
新增分类
新增分类里的数据是存储在数据库中category表中,表的结构如下。
分类办理页面现在是没有数据的,由于咱们还没完结对应的操控层的办法。
有的小伙伴或许和博主是相同的,都是经过MP的代码生成器生成的项目结构,这时咱们就需求在实体类上的公共字段弥补上@TableField。
package com.huaishushu.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableLogic;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* <p>
* 菜品及套餐分类
* </p>
*
* @author ChiBaoBaoHuaiShuShu
* @since 2023-05-28
*/
@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;
}
代码开发
在开发代码之前,需求整理一下整个程序的履行进程:
- 页面(backend/page/category/list.htm)发送ajax恳求,将新增分类窗口输入的数据以ion办法提交到服务端
- 服务端Controller接纳页面提交的数据并调用Service将数据进行保存
- Service调用Mapper操作数据库,保存数据
能够看到新增菜品分类和新增套餐分类恳求的服务端地址和提交的ison数据结构相同,仅仅type字段的信息不同罢了,这个咱们数据库中就有解说,“1”为分类,“2为套餐”,所以服务端只需求供给一个办法一致处理,而且它的url恳求办法是post的,因而咱们在Controller界说一个post的办法来接纳即可。
咱们在分类的前端代码中能够看到,其只用到了一个code,不需求发送实体类之类的,那么咱们就只需求界说一个回来值是R类型的办法就能够了。
Controller层
package com.huaishushu.controller;
import com.huaishushu.common.R;
import com.huaishushu.entity.Category;
import com.huaishushu.service.ICategoryService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* <p>
* 菜品及套餐分类 前端操控器
* </p>
*
* @author ChiBaoBaoHuaiShuShu
* @since 2023-05-28
*/
@RestController
@RequestMapping("/category")
@Slf4j
public class CategoryController {
@Autowired
private ICategoryService categoryService;
/**
* 新增分类和套餐
* @param category
* @return
*/
@PostMapping
public R<String> save(@RequestBody Category category) {
boolean flag = categoryService.save(category);
log.info("新增是菜品仍是套餐:{},是否成功:{}",category.getType(),flag);
if (flag) {
return R.success("新增分类成功");
}
return R.error("新增分类失利");
}
}
Service接口
package com.huaishushu.service;
import com.huaishushu.entity.Category;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 菜品及套餐分类 服务类
* </p>
*
* @author ChiBaoBaoHuaiShuShu
* @since 2023-05-28
*/
public interface ICategoryService extends IService<Category> {
boolean save(Category category);
}
Service完结类
package com.huaishushu.service.impl;
import com.huaishushu.entity.Category;
import com.huaishushu.mapper.CategoryMapper;
import com.huaishushu.service.ICategoryService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* <p>
* 菜品及套餐分类 服务完结类
* </p>
*
* @author ChiBaoBaoHuaiShuShu
* @since 2023-05-28
*/
@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements ICategoryService {
@Autowired
private CategoryMapper categoryMapper;
@Override
public boolean save(Category category) {
return categoryMapper.insert(category) > 0;
}
}
之后咱们测验增加客家菜和晚年套餐,能够看到闪现分类增加成功,之后咱们能够到数据库中查看一下,是否增加成功,从下图能够看到咱们是增加成功了,而且type类型也是增加正确的,那么404报错是咱们还没有完结分类的分页查询,所以才会闪现404报错。
温馨提示
分类信息分页查询
代码开发
在开发代码之前,需求整理一下整个程序的履行进程
- 页面发送ajax恳求,将分页查询参数(page、pageSize)提交到服务端
- 服务端Controller接纳页面提交的数据并调用Service查询数据
- Service调用Mapper操作数据库,查询分页数据
- Controller将查询到的分页数据呼应给页面
- 页面接纳到分页数据并经过ElementUl的Table组件展现到页面上
因而咱们在Controller层页面中界说一个page办法并运用@GetMapping来接纳前端的url恳求。
Controller层
/**
* 分类信息的分页查询
* @param page
* @param pageSize
* @return
*/
@GetMapping("/page")
public R<Page> page(Integer page, Integer pageSize) {
Page<Category> pageInfo = categoryService.page(page, pageSize);
log.info("分类信息分页查询");
return R.success(pageInfo);
}
Service接口
/**
* 分类信息的分页查询
* @param page
* @param pageSize
* @return
*/
Page<Category> page(Integer page, Integer pageSize);
Service完结类
/**
* 分类信息的分页查询
* @param page
* @param pageSize
* @return
*/
@Override
public Page<Category> page(Integer page, Integer pageSize) {
//构建分页结构器
Page pageInfo = new Page(page, pageSize);
LambdaQueryWrapper<Category> lqw = new LambdaQueryWrapper<>();
lqw.orderByDesc(Category::getUpdateTime);
//此处不回来是由于查询好后的值是直接赋值给了上面的分页结构器了pageInfo
categoryMapper.selectPage(pageInfo, lqw);
return pageInfo;
}
查询出来的信息,咱们按时刻排序了一下。
删去分类
需求剖析
在分类办理列表页面,能够对某个分类进行删去操作。需求留意的是当分类相关了菜品或许套餐时,此分类不允许删去。
代码开发
在开发代码之前,需求整理一下整个程序的履行进程:
- 页面发送ajax恳求,将参数(id)提交到服务端
- 服务端Controller接纳页面提交的数据并调用Service删去数据
- Service调用Mapper操作数据库
那么咱们在Controller层创立对应的办法来接纳删去恳求即可,这儿咱们先直接删去信息,下一个小模块咱们的在完善这个删去功用,使其判别该分类是否有菜品绑定了这个分类。
Controller层
/**
* 依据id删去分类信息
* @param id
* @return
*/
@DeleteMapping
public R<String> delete(Long id) {
boolean flag = categoryService.delete(id);
log.info("删去分类信息的id:{},是否删去成功:{}",id,flag);
if (flag) {
return R.success("删去分类信息成功");
}
return R.error("删去分类信息失利!!!");
}
Service接口
/**
* 依据id来删去分类信息
* @param id
* @return
*/
boolean delete(Long id);
Service完结类
/**
* 依据id来删去分类信息
* @param id
* @return
*/
@Override
public boolean delete(Long id) {
return categoryMapper.deleteById(id) > 0;
}
这儿有的小伙伴或许会遇到一些问题,咱们能够从图中看到delete删去的url恳求中传递id的参数称号是ids,可是教师视频中的url恳求时id,所以这块咱们能够去到static/backend/api/category.js中修正。
删去功用完善
前面咱们现已完结了依据id删去分类的功用,可是并没有查看删去的分类是否相关了菜品或许套餐,所以咱们需求进行功用完善。
要完善分类删去功用,需求先预备根底的类和接口:
- 实体类Dish和Setmeal (从课程材猜中仿制即可)
- Mapper接口DishMapper和SetmealMapper
- Service接口DishService和SetmealService
- Service完结类DishServicelmpl和SetmealServicelmpl
假如是MP代码生成器生成的项目成果,此处也要记住在实体类Dish和Setmeal类中为公共字段增加@TableField,假如是仿制课程材料的则直接仿制就行了。
Dish实体类
package com.huaishushu.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* <p>
* 菜品办理
* </p>
*
* @author ChiBaoBaoHuaiShuShu
* @since 2023-05-28
*/
@Data
public class Dish implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
private Long id;
/**
* 菜品称号
*/
private String name;
/**
* 菜品分类id
*/
private Long categoryId;
/**
* 菜品价格
*/
private BigDecimal price;
/**
* 商品码
*/
private String code;
/**
* 图片
*/
private String image;
/**
* 描绘信息
*/
private String description;
/**
* 0 停售 1 起售
*/
private Integer status;
/**
* 次序
*/
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;
}
Setmeal实体类
package com.huaishushu.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* <p>
* 套餐
* </p>
*
* @author ChiBaoBaoHuaiShuShu
* @since 2023-05-28
*/
@Data
public class Setmeal implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
private Long id;
/**
* 菜品分类id
*/
private Long categoryId;
/**
* 套餐称号
*/
private String name;
/**
* 套餐价格
*/
private BigDecimal price;
/**
* 状况 0:停用 1:启用
*/
private Integer status;
/**
* 编码
*/
private String code;
/**
* 描绘信息
*/
private String description;
/**
* 图片
*/
private String image;
/**
* 创立时刻
*/
@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;
}
咱们在CategoryServiceImpl的delete办法中,追加事务逻辑的判别:
- 判别该分类是否相关了菜品,假如相关了则抛出事务反常
- 判别该分类是否相关了套餐,假如相关了则抛出事务反常
- 假如都没相关,则正常删去
CategoryServiceImpl类:
/**
* <p>
* 菜品及套餐分类 服务完结类
* </p>
*
* @author ChiBaoBaoHuaiShuShu
* @since 2023-05-28
*/
@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements ICategoryService {
@Autowired
private CategoryMapper categoryMapper;
@Autowired
private DishServiceImpl dishService;
@Autowired
private SetmealServiceImpl setmealService;
/**
* 新增分类和套餐
* @param category
* @return
*/
@Override
public boolean save(Category category) {
return categoryMapper.insert(category) > 0;
}
/**
* 分类信息的分页查询
* @param page
* @param pageSize
* @return
*/
@Override
public Page<Category> page(Integer page, Integer pageSize) {
//构建分页结构器
Page pageInfo = new Page(page, pageSize);
LambdaQueryWrapper<Category> lqw = new LambdaQueryWrapper<>();
lqw.orderByDesc(Category::getUpdateTime);
//此处不回来是由于查询好后的值是直接赋值给了上面的分页结构器了pageInfo
categoryMapper.selectPage(pageInfo, lqw);
return pageInfo;
}
/**
* 依据id来删去分类信息
* 删去之前判别菜品是否绑定了分类,没绑定分类信息则删,绑定了的话不能删去
* @param id
* @return
*/
@Override
public boolean delete(Long id) {
//查询当时分类是否相关了菜品,假如现已相关,抛出一个事务反常
LambdaQueryWrapper<Dish>dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
dishLambdaQueryWrapper.eq(Dish::getCategoryId,id);
long count = dishService.count(dishLambdaQueryWrapper);
if (count > 0) {
// 抛出一个事务反常
throw new CustomException("当时分类相关了菜品,不能删去!!!");
}
//查询当时分类是否相关了套餐,假如现已办理了抛出一个事务反常
LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId,id);
count = setmealService.count(setmealLambdaQueryWrapper);
if (count > 0) {
// 抛出一个事务反常
throw new CustomException("当时分类相关了套餐,不能删去!!!");
}
//正常删去分类
return categoryMapper.deleteById(id)>0;
}
}
其间咱们自界说了一个CustomException类来处理事务反常,其要承继RuntimeException接口,该接口是一个运转反常接口。
在接口开发的进程中,为了程序的健壮性,常常要考虑到代码履行的反常,并给前端一个友爱的展现,这儿就用到的自界说反常,承继RuntimeException类。那么这个RuntimeException和普通的Exception有什么区别呢。
Exception: 非运转时反常,在项目运转之前有必要处理掉。一般由程序员try catch 掉。
RuntimeException,运转时反常,在项目运转之后出错则直接中止运转,反常由JVM虚拟机处理。
在接口的逻辑判别呈现反常时,或许会影响后边代码。或许说绝对不忍受(允许)该代码块出错,那么咱们就用RuntimeException,可是咱们又不能由于体系挂掉,只在后台抛出反常而不给前端回来友爱的提示吧,至少给前端回来呈现反常的原因。因而接口的自界说反常作用就体现出来了。
CustomException类
/**
* @Author HuaiShuShu
* @Date 2023/5/31 17:10
* @PackageName:com.huaishushu.common
* @Description: 自界说事务反常
* @Version 1.0
*/
//RuntimeException运转时反常
public class CustomException extends RuntimeException{
public CustomException(String message) {
super(message);
}
}
之后再在咱们之前界说的大局反常处理器GlobalExceptionHandler中增加咱们自界说的事务反常,运用@ExceptionHandler注解来绑定咱们自界说的反常
GlobalExceptionHandler类
/**
* @Author HuaiShuShu
* @Date 2023/5/29 0:06
* @PackageName:com.huaishushu.common
* @Description: 大局反常处理器
* @Version 1.0
*/
//敞开反常处理,annotations特点能够装备需求阻拦的方位。
@ControllerAdvice(annotations = {RestController.class, Controller.class})
//增加@ResponseBody,由于之后捕获反常今后需求回来json办法的数据
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {
/**
* 反常处理办法
* 处理这个反常类型的报错(SQLIntegrityConstraintViolationException)
* @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("不知道过错!!!");
}
/**
* 反常处理办法
* 处理自界说的反常 CustomException.class
* @return
*/
@ExceptionHandler(CustomException.class)
public R<String> exceptionHandler(CustomException ex) {
log.error(ex.getMessage());
return R.error(ex.getMessage());
}
}
之后测验即可,咱们能够看到能够正常增加和删去刚增加的分类信息,而原本就有的分类信息无法删去,这是由于咱们自己新增加的还没有绑定菜品和套餐。
修正分类
这部分的修正功用和前面的职工信息的修正是相同的事务逻辑:
- 点击修正按钮后,回显当时行的分类信息。
- 在修正窗口完结修正分类信息后,点击确认按钮完结对分类信息的修正。
可是当咱们点击修正按钮后,弹出的修正窗口中现已有当时行的数据回显了,这是由于咱们的新增菜品分类、新增套餐分类,修正分类,这三个的窗口用的是同一套数据模版。
因而前段这块的代码便是把同一套数据模型修正了罢了,修正按钮那块把当时行的数据传曩昔了,然后下面的函数进行了数据替换,所以就能够闪现了(这块需求有必定前端根底的小伙伴看比较好)
当咱们点击确认后,前端会发送分类修正的url恳求,而且其带着了一个category实体类参数。
那么咱们接下来的做法和职工信息修正那块差不多了。
Controller层
/**
* 依据id修正分类信息
* @param category
* @return
*/
//@PutMapping
public R<String> update(@RequestBody Category category) {
log.info("修正分类信息:{}",category);
boolean flag = categoryService.update(category);
if (flag) {
return R.success("修正分类信息成功");
}
return R.error("修正分类信息失利!!!");
}
Service接口
/**
* 依据id修正分类信息
* @param category
* @return
*/
boolean update(Category category);
Service完结类
/**
* 依据id修正分类信息
* @param category
* @return
*/
@Override
public boolean update(Category category) {
return categoryMapper.updateById(category) > 0;
}
菜品模块
文件上传下载
文件上传介绍
文件上传,也称为upload,是指将本地图片、视频、音频等文件上传到服务器上,能够供其他用户阅读或下载的进程文件上传在项目中运用非常广泛,咱们常常发微博、发微信朋友圈都用到了文件上传功用。
文件上传时,对页面的form表单有如下要求
- method=”post” 选用post办法提交数据
- enctype=”multipart/form-data 选用multipart格局上传文件
- type=”file” 运用input的file控件上传
服务端要接纳客户端页面上传的文件,一般都会运用Apache的两个组件
- commons-fileupload
- commons-io
Spring结构在spring-web包中对文件上传进行了封装,大大简化了服务端代码,咱们只需求在Controller的办法中声明一个MultipartFile类型的参数即可接纳上传的文件,例如:
文件下载介绍
文件下载,也称为download,是指将文件从服务器传输到本地计算机的进程 经过阅读器进行文件下载,一般有两种表现办法:
- 以附件办法下载,弹出保存对话框,将文件保存到指定磁盘目录
- 直接在阅读器中打开
经过阅读器进行文件下载,本质上便是服务端将文件以流的办法写回阅读器的进程
文件上传代码完结
文件上传,页面端能够运用ElementUI供给的上传组件。
能够直接运用材猜中供给的上传页面,方位: 材料/文件上传下载页面/upload.html
将其仿制放在page文件夹下并新建一个demo的文件夹下。
经过查看它的代码,咱们能够看到,上传文件的url恳求途径是/common/upload,而且是post恳求。
所以咱们在Controller层下创立一个CommonController类,并经过它来创立接纳上传文件url完结上传功用。
CommonController类
/**
* @Author HuaiShuShu
* @Date 2023/5/31 20:46
* @PackageName:com.huaishushu.controller
* @Description: 文件上传和下载
* @Version 1.0
*/
@RestController
@RequestMapping("/common")
@Slf4j
public class CommonController {
@Value("${ruiJiWaiMai.path}")
private String basePath;
/**
* 文件上传
* @param file (该参数的称号应与前端的恳求表单中的name对应)
* @return
*/
@PostMapping("/upload")
//这儿MultipartFile类型的参数称号不能随意起,应该和前端的恳求表单中的name对应
public R<String> upload(MultipartFile file) {
//file是一个临时文件,需求转存到指定方位,不然本次恳求完结后临时文件会删去
//原始文件名
String originalFilename = file.getOriginalFilename();
//获取文件的后缀
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
//运用UUID从头生成文件名,防止文件名重复造成文件掩盖
String fileNmae = UUID.randomUUID().toString() + suffix;
//创立一个目录目标
File dir = new File(basePath);
//判别当时目录是否存在
if (!dir.exists()) {
//现在不存在,需求创立
dir.mkdir();
}
log.info(file.toString());
try {
//将临时文件转存到指定方位
file.transferTo(new File(basePath + fileNmae));
} catch (IOException e) {
throw new RuntimeException(e);
}
return R.success(fileNmae);
}
}
而且在yml文件中经过自界说特点来动态为上传途径赋值,并经过@Value注解为其basePath,笔者是在项目中的images文件夹下新建了一个path文件夹来保存上传的文件。
#自界说一个特点,用来做文件上传的地址
ruiJiWaiMai:
path: D:\GongZuo\IDEA\XianMu\RuiJiWaiMai-springboot-MybatisPlus\RuiJiWaiMai\src\main\resources\static\backend\images\path\
温馨提示
咱们上传好后能够去相应途径下查看是否上传成功,但发现上传后,可是在相应文件夹下却没有闪现的时分,咱们应该去先去登录账号先,由于咱们前面装备的过滤器中需求判别咱们是否现已登录了账号,没登录的话就会被过滤了。
解决办法:
- 能够先在登录页中登录账号后,在去测验上传功用。
- 能够在过滤器中增加免过滤的途径,/demo/upload
文件下载代码完结
文件下载,页面端能够运用标签展现下载的图片
文件下载咱们在CommonController类中增加download办法,并设置@GetMapping(“/download”)来获取前端下载的url恳求。
/**
* 文件下载
* @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) {
throw new RuntimeException(e);
}
}
这个便是上传成功后,前端组件主动调用咱们刚刚写好的下载办法,下载好后并回显给前端组件框了。
新增菜品
需求剖析
后台体系中能够办理菜品信息,经过新增功用来增加一个新的菜品,在增加菜品时需求挑选当时菜品所属的菜品分类而且需求上传菜品图片,在移动端会依照菜品分类来展现对应的菜品信息。
数据模型
新增菜品,其实便是将新增页面录入的菜品信息刺进到dish表,假如增加了口味做法,还需求向dish flavor表刺进数据。
所以在新增菜品时,涉及到两个表:
- dish 菜品表
- dish flavor 菜品口味表
代码开发-预备工作
在开发事务功用前,先将需求用到的类和接口根本结构创立好:
- 实体类 DishFlavor(直接从课程材猜中导入即可,Dish实体前面课程中现已导入过了)
- Mapper接口 DishFlavorMapper
- 事务层接口 DishFlavorService
- 事务层完结类 DishFlavorServicelmpl
- 操控层 DishController
假如运用的是MP生成的表结构,那么咱们需求在实体类 DishFlavor中为那几个公共字段增加@TableField注解,与前面职工实体类和分类实体类相同的。
关于Controller层的话,由于视频里黑马的教师打关于菜品口味和菜品办理的Controller层的操作都放在了DishController类里,所以这儿咱们也依照视频中相同这样操作。
关于MP代码生成器帮咱们生成的DishFlavorController类,咱们能够先留着,看看后期开发会不会用到,假如最终用不到在删掉也是能够的,现在是不影响开发的。
代码开发-整理交互进程
在开发代码之前,需求整理一下新增菜品时前端页面和服务端的交互进程:
-
页面(backend/page/food/add.html)发送ajax恳求,恳求服务端获取菜品分类数据并展现到下拉框中
-
页面发送恳求进行图片上传,恳求服务端将图片保存到服务器
-
页面发送恳求进行图片下载,将上传的图片进行回显
-
点击保存按钮,发送ajax恳求,将菜品相关数据以ison办法提交到服务端
开发新增菜品功用,其实便是在服务端编写代码去处理前端页面发送的这4次恳求即可,2和3这块的上传和回显,咱们在前面的测验中就现已在CommonController类中现已完结了。
分类下拉框完结
咱们来看分类下拉框这个事务功用的完结,它的前端代码逻辑,咱们刚进入到增加菜品这个页面的时分它的vue钩子函数就会调用getDishList()办法,并把type=1这个参数给传递曩昔给food.js,然后发送了下面的这个查询分类信息的url恳求给后端来获取分类信息中分类的数据。
由于它发送的恳求是/category的恳求,因而咱们在CategoryController类来编写它的操控办法来获取恳求并回来数据给前端。
Controller层
这儿由于前端传递的url恳求中有type参数,这儿咱们能够在操控办法界说参数来接纳它的时分,能够挑选直接界说一个list(Integer type)来接纳,也能够挑选界说实体类参数list(Category category)来接纳,这儿咱们挑选实体类来接纳,由于这样后期咱们也能够传递其他的参数来查询分类信息,复用性较好。
/**
* 依据条件来查询一切分类信息
* @param category
* @return
*/
@GetMapping("list")
public R<List<Category>> list(Category category) {
List<Category> categoryList = categoryService.list(category);
return R.success(categoryList);
}
Service接口
/**
* 依据条件来查询一切分类信息
* @param category
* @return
*/
List<Category> list(Category category);
Service完结类
/**
* 依据条件来查询一切分类信息
* @param category
* @return
*/
@Override
public List<Category> list(Category category) {
LambdaQueryWrapper<Category> lqw = new LambdaQueryWrapper<>();
lqw.eq(category.getType() != null, Category::getType, category.getType());
//增加个排序,一个是按次序升序排序,一个是按更新时刻降序排序
lqw.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime);
List<Category> categoryList = categoryMapper.selectList(lqw);
return categoryList;
}
之后测验,即可看到分类信息闪现成功了。
菜品增加保存完结
由于咱们点击保存后能够看到发送的恳求途径为http://localhost/dish,而且为post恳求,所以咱们在Controller层要界说的一个post的操控办法来接纳它的参数来完结存储。
代码开发-导入DTO
导入DishDto (方位: 材料/dto) ,用于封装页面提交的数据
由于咱们能够从图中看到前端页面传回来的参数类型较多,咱们无法只用Dish这个实体类来接纳,里边有些参数不是Dish类里的特点,因而咱们能够界说一个新的实体类来接纳这些参数,这种实体类的称号为DTO(数据传输服务目标)。
package com.huaishushu.dto;
import com.huaishushu.entity.Dish;
import com.huaishushu.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;
}
其间flavors是用来接纳页面传输回来的口味调集的,而别的两个现在暂时还用不上,放着不用理它就行了。
Controller层
/**
* <p>
* 菜品办理 前端操控器
* </p>
*
* @author ChiBaoBaoHuaiShuShu
* @since 2023-05-28
*/
@RestController
@RequestMapping("/dish")
@Slf4j
public class DishController {
@Autowired
private IDishService dishService;
@Autowired
private IDishFlavorService dishFlavorService;
/**
* 新增菜品信息,操作的是两张表,还需求刺进口味
* @return
*/
@PostMapping
public R<String> save(@RequestBody DishDto dishDto) {
dishService.saveWithFlavor(dishDto);
return R.success("新增菜品成功");
}
}
service接口
/**
* <p>
* 菜品办理 服务类
* </p>
*
* @author ChiBaoBaoHuaiShuShu
* @since 2023-05-28
*/
public interface IDishService extends IService<Dish> {
/**
* 新增菜品信息,操作的是两张表,还需求刺进口味
* @return
*/
void saveWithFlavor(DishDto dishDto);
}
service完结类
/**
* <p>
* 菜品办理 服务完结类
* </p>
*
* @author ChiBaoBaoHuaiShuShu
* @since 2023-05-28
*/
@Service
public class DishServiceImpl extends ServiceImpl<DishMapper, Dish> implements IDishService {
@Autowired
DishMapper dishMapper;
@Autowired
IDishFlavorService dishFlavorService;
/**
* 新增菜品信息,操作的是两张表,还需求刺进口味
* @return
*/
@Override
@Transactional
public void saveWithFlavor(DishDto dishDto) {
//保存菜品的根本信息到菜品表dish
this.save(dishDto);
//菜品id
Long dishId = dishDto.getId();
//为一切的菜品口味增加菜品的ID
for (DishFlavor dishFlavor : dishDto.getFlavors()) {
dishFlavor.setDishId(dishId);
}
//保存菜品口味数据到菜品口味表dish_flavor
dishFlavorService.saveBatch(dishDto.getFlavors());
}
}
在接口完结类中,由于前端传回来的菜品口味参数是没有菜品的参数的,因而咱们需求遍历为其每个口味赋值菜品的id。
菜品信息分页查询
需求剖析
体系中的菜品数据许多的时分,假如在一个页面中全部展现出来会显得比较乱,不便于查看,所以一般的体系中都会以分页的办法来展现列表数据。
从图中咱们能够看出,菜品里头闪现有菜品的分类的称号,这个分类称号在外面的dish表中是没有存储的,咱们是只保存有分类的id,所以菜品分页查询这块会有些复杂。
代码开发
代码开发-整理交互进程
在开发代码之前,需求整理一下菜品分页查询时前端页面和服务端的交互进程:
- 页面(backend/page/food/list.html)发送ajax恳求,将分页查询参数(page、pageSize、name)提交到服务端,获取分页数据,其间name是咱们在菜品办理中用来进行模糊查询的,这块和职工分页查询事务是相同的。
- 页面发送恳求,恳求服务端进行图片下载,用于页面图片展现
开发菜品信息分页查询功用,其实便是在服务端编写代码去处理前端页面发送的这2次恳求即可。
咱们这个模块的一切的MVC层的办法都是在Dish所对应的Controller、service、serviceImpl中中完结的,那么这儿有的小伙伴或许就会依照咱们之写职工分页查询那样,这样写没有问题,可是由于dish表中只保存分类信息的id,并没有保存称号,所以页面中是不会闪现分类称号的,所以咱们这样写的话是只完结了一半。
@Override
public Page<Dish> page(Integer page, Integer pageSize, String name) {
//构建分页结构器
Page pageInfo = new Page(page, pageSize);
LambdaQueryWrapper<Dish> lqw = new LambdaQueryWrapper<>();
lqw.like(StringUtils.isNoneEmpty(name), Dish::getName, name);
lqw.orderByDesc(Dish::getUpdateTime);
//此处不回来是由于查询好后的值是直接赋值给了上面的分页结构器了pageInfo
dishMapper.selectPage(pageInfo, lqw);
return pageInfo;
}
咱们能够到static/backend/page/food/list.html的文件下找到菜品分类的分类称号的变量,是categoryName,可是后端传给前端的数据是没有这个变量的。
Controller层
/**
* 菜品信息分页查询
* @param page
* @param pageSize
* @param name
* @return
*/
@GetMapping("/page")
public R<Page> page(Integer page, Integer pageSize, String name) {
Page<DishDto> dishDtoPage = dishService.page(page, pageSize, name);
log.info("菜品信息分页查询:{}",dishDtoPage);
return R.success(dishDtoPage);
}
Service接口
/**
* 菜品信息分页查询
* @param page
* @param pageSize
* @param name
* @return
*/
Page<DishDto> page(Integer page, Integer pageSize, String name);
Service完结类
/**
* 菜品信息分页查询
* @param page
* @param pageSize
* @param name
* @return
*/
@Override
public Page<DishDto> page(Integer page, Integer pageSize, String name) {
Page<Dish> pageInfo = new Page(page, pageSize);
LambdaQueryWrapper<Dish> lqw = new LambdaQueryWrapper<>();
lqw.like(StringUtils.isNoneEmpty(name), Dish::getName, name);
lqw.orderByDesc(Dish::getUpdateTime);
//此处不回来是由于查询好后的值是直接赋值给了上面的分页结构器了pageInfo
dishMapper.selectPage(pageInfo, lqw);
//构建DishDto的分页结构器
Page<DishDto> dishDtoPage = new Page<>();
//目标复制,但不复制“records”
BeanUtils.copyProperties(pageInfo, dishDtoPage, "records");
List<Dish> records = pageInfo.getRecords();
//这块用的是stream().map(字符流加lmabe)
List<DishDto> dishDtoList = records.stream().map((itme) -> {
DishDto dishDto = new DishDto();
//由于是新new出来的,里边什么数据都没有,因而需求复制赋值
BeanUtils.copyProperties(itme, dishDto);
Long categoryId = itme.getCategoryId();
Category category = categoryService.getById(categoryId);
//此处是由于当咱们自己增加或经过sql文件导入菜品时并没有设置分类,所以或许是会为空的
if (category != null) {
String categoryName = category.getName();
dishDto.setCategoryName(categoryName);
}
return dishDto;
}).collect(Collectors.toList());
dishDtoPage.setRecords(dishDtoList);
return dishDtoPage;
}
这块的Impl是完结类的事务逻辑完结关于不太熟练的小伙伴来说或许会有些懵,
咱们来整理一下全体咱们的意图,假如咱们仅仅单纯的时分 Page来进行分页查询,会短少categoryName分类称号,因而这儿咱们运用到了DishDto类这个数据传输服务目标,结构一个关于构建DishDto的分页结构器目标dishDtoPage,从之前咱们的代码整理的后端传递给前端的Page能够看到,分页的数据是封装在records调集里的,所以接下来外面的进程是:
- 将除records调集的其他一切数据复制给dishDtoPage目标。
- 从pageInfoz中获取records的list集兼并经过字节省加lambda表达式遍历它,它的每一个遍历目标变量为item,将每个dish目标的数据复制给dishdto(由于dishdto是咱们新new出来的所以其里边是空的。)
- 之后经过item获取categoryId,之后咱们在public Page page(Integer page, Integer pageSize, String name)办法外,新主动安装一个IcategoryService的目标,经过categoryService来经过id查询获取到Category目标。
- 由于咱们是经过自己增加或经过sql文件导入菜品时并没有设置分类,所以或许是会为空的,所以做一次判别,假如不为空则经过Category目标来获取categoryName分类称号,之后在赋值给dishDto里头的categoryName。
- 之后搜集其records的list调集遍历后的成果,并封装成一个list目标,并赋值给List dishDtoList。
- 最终在将dishDtoList这个list赋值给dishDtoPage的records,由于前面咱们经过BeanUtils东西类复制pageInfo和dishDtoPage的时分是排除了records,所以dishDtoPage的records此刻是为null的,当咱们把records赋值给dishDtoPage时,它才是完好的一个分页类。
功用测验
从第一步就能够看出传给前端page中是没有categoryName这个特点目标的。
当咱们创立出dishDtoPage目标时,它仍是空的,此刻内部是并没有数据的。
咱们能够看到当咱们运转完BeanUtils东西类为dishDtoPage赋值后,它的其他特点部分都是由数据了,只要records部分仍是空的。
之后当咱们运用steam字节省加lambda表达式来遍历records的list目标时,咱们新建的DishDto目标是空的。
当咱们将itme(其实便是dish目标)复制给dishDto后,它出了分类信息的称号没有以外,其他都是有数据了。
最终将搜集到的list目标赋值给dishDtoList,并将其在赋值给dishDtoPage目标的records,这样咱们就有一个完好的Page目标了
之后将这个目标放回给前端后,那么前端就能够正常闪现了。
问题-项目无法发动
在这处事务开发中,有的小伙伴或许会遇到以下的报错提示,这儿的报错信息闪现:
不鼓舞依靠循环引证,默许情况下制止运用循环引证。更新运用程序以删去 Bean 之间的依靠循环。作为最终的手法,能够经过将spring.main.allow-circular-references设置为true来主动打破循环。
这主要是由于咱们在前面的分类信息的service完结类中界说了一个主动安装dishService目标,这是一个bean,而咱们在刚刚的菜品分页查询中又界说了一个主动安装categoryService目标,这也是一个bean,所以导致了bean的嵌套了,在新版的spring中这是不鼓舞的。
解决办法
前面的报错信息也提示咱们了,所以咱们在yml文件中以下特点即可
spring:
main:
allow-circular-references: true
问题-菜品图片闪现
有的小伙伴会发现咱们的菜品展现中,只要咱们后边自己手动增加的菜品有图片,经过sql文件生成的那些菜品没有图片,这个问题是由于咱们的图片回显下载途径便是咱们的图片上传途径时指定的方位,所以咱们要将黑马供给给咱们的图片资源也保存到相同的途径下,这样在菜品展现时才会从指定的方位中下载回来图片进行回显。
修正菜品
需求剖析
在菜品办理列表页面点击修正按钮,跳转到修正菜品页面,在修正页面回显菜品相关信息并进行修正,最终点击确认按钮完结修正操作
代码开发-整理交互进程
在开发代码之前,需求整理一下修正菜品时前端页面 (add.html)和服务端的交互进程:
- 页面发送aiax恳求,恳求服务端获取分类数据,用于菜品分类下拉框中数据展现
- 页面发送ajax恳求,恳求服务端,依据id查询当时菜品信息,用于菜品信息回显
- 页面发送恳求,恳求服务端进行图片下载,用于页图片回显
- 点击保存按钮,页面发送ajax恳求,将修正后的菜品相关数据以ison办法提交到服务端
开发修正菜品功用,其实便是在服务端编写代码去处理前端页面发送的这4次恳求即可,由于咱们前面现已完结了关于菜品分类下拉框的事务,以及图片的上传和下载回显模块,其实就想担当于现已完结了1和3,那么咱们剩余只需求完结2和4即可。
恳求2
第2次恳求,关于依据id来查询当时菜品信息,并回显,咱们能够先看以下前端页面发送的url恳求链接,从中咱们能够看到其id是以恳求途径的信息回来的,并不是以参数的办法,因而咱们之后在Controller层中界说办法来接纳的时分就需求在@GetMapping中界说参数来接纳了。
Controller层
/**
* 依据id来查询菜品信息和关于的口味信息
* @param id
* @return
*/
@GetMapping("{id}")
public R<DishDto> getByIdWithFlavor(@PathVariable("id") Long id) {
DishDto dishDto = dishService.getByIdWithFlavor(id);
return R.success(dishDto);
}
Service接口
/**
* 依据id来查询菜品信息和关于的口味信息
* @param id
* @return
*/
public DishDto getByIdWithFlavor(Long id);
Service完结类
/**
* 依据id来查询菜品信息和关于的口味信息
* @param id
* @return
*/
@Override
public DishDto getByIdWithFlavor(Long id) {
//查询菜品根本信息,从dish表查询
Dish dish = this.getById(id);
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(dish,dishDto);
//查询当时菜品对应的口味信息,从dish_flavor表查询
LambdaQueryWrapper<DishFlavor> lqw = new LambdaQueryWrapper<>();
lqw.eq(DishFlavor::getDishId, dish.getId());
List<DishFlavor> flavors = dishFlavorService.list(lqw);
dishDto.setFlavors(flavors);
return dishDto;
}
事务剖析后咱们能够知道,咱们还需求回显菜品相关的口味信息,所以假如咱们光用dish目标来直接查询数据库的话,不无法回显完好的菜品信息中中的口味信息的,因而这儿咱们运用的是dishDto目标来完结的,它的完结进程为:
- 先查询菜品的根本信息,直接从dish表中查询
- 将查询到的菜品根本信息dish复制给dishDto目标
- 查询当时菜品对应的口味信息,从dish_flavor表查询
- 查询到后赋值给dishDto目标中的flavors特点即可
- 回来dishDto给前端
恳求4
当咱们点击保存按钮后,页面发送的恳求中,咱们能够看到它的恳求类型为PUT,而且参数的类型和咱们前面增加菜品信息的格局是相同的,因而咱们这块能够参阅之前新增菜品信息的办法。
Controller层
/**
* 修正菜品信息,操作的是两张表,还需求刺进口味
* @return
*/
@PutMapping
public R<String> update(@RequestBody DishDto dishDto) {
dishService.updateWithFlavor(dishDto);
return R.success("新增菜品成功");
}
Service接口
/**
* 修正菜品信息,操作的是两张表,还需求刺进口味
* dish、dish_flavor两张表
* @return
*/
void updateWithFlavor(DishDto dishDto);
Service完结类
/**
* 修正菜品信息,操作的是两张表,还需求刺进口味
* dish、dish_flavor两张表
* @return
*/
@Override
@Transactional
public void updateWithFlavor(DishDto dishDto) {
//更新菜品的根本信息到菜品表dish
this.updateById(dishDto);
//整理当时菜品对应口味数据--dish_flavor表的delete操作
LambdaQueryWrapper<DishFlavor> lqw = new LambdaQueryWrapper<>();
lqw.eq(DishFlavor::getDishId, dishDto.getId());
dishFlavorService.remove(lqw);
//菜品id
Long dishId = dishDto.getId();
//为一切的菜品口味增加菜品的ID
for (DishFlavor dishFlavor : dishDto.getFlavors()) {
dishFlavor.setDishId(dishId);
}
//更新菜品口味数据到菜品口味表dish_flavor
dishFlavorService.saveBatch(dishDto.getFlavors());
}
在完结类中,由于其的功用和前面新增菜品相似,所以咱们能够参阅前面的办法,以下是咱们的完结进程:
- 先修正更新菜品的根本信息。
- 整理当时菜品对应的口味数据。
- 当咱们从头保存口味信息时,遇到情况是和新增菜品时相同的,短少菜品的id,一切循环遍历为其赋值。
- 从头为其增加菜品的口味信息,以到达口味信息修正的事务意图。
后边更新那块有的小伙伴或许会想运用MP中的updateBatchById来批量修正,咱们不能运用updateBatchById批量修正的原因是,前端不必定会发回来相同的口味,或许多一个或许少一个口味,由于批量依据id修正是依据调集中的数量修正的,假如少了一个口味,那么就少改了一个,然后数据库中保留了一个口味导致多了一个口味,或许发回来多了一个口味,导致无法修正成功。
事务逻辑缝隙
咱们后边这边更新口味信息这块是由逻辑缝隙的,比方咱们删去过后,当咱们从头增加口味信息的时分,由于口味信息中保存由创立时刻和创立人的信息,这样咱们删去后在从头增加口味信息,那么创立时刻和创立人又变为当时的时刻和当时的用户信息了,这是不合理的。
这块需求看看黑马中的后续开发中是否会修补这部分的缝隙问题,或许笔者之后自己修正这部分的逻辑问题,由于笔者的这些笔记都是一边跟着做开发一边编写的,所今后续的内容还不清楚。
菜品状况的启用/禁用
需求剖析
在菜品办理列表页面点击禁用/启用按钮,或许批量挑选菜品后,来完结关于菜品状况的修正,这块的规划不考虑菜品是否已在套餐中,由于也普遍存在一些菜品只做套餐售卖,不做单点,因而这块的事务逻辑便是正常的直接启售/禁售,无需考虑其他的。
代码开发-整理交互进程
当咱们点击启用/禁用,或许批量挑选后运用批量启用/禁用时,前端发送ajax给后端,后端依据前端的发送回来的情况,在详细操作,关于状况的修正有如下4种类型的ajax恳求:
禁用:
启用:
-
-
它们所发送的恳求办法都是POST,传递的参数都是相同的,这是一个ids参数,这块有比较多种的接纳办法,笔者这儿运用的是字符串String去接纳的,由于笔者前面运用list去接纳的时分没有作用,还报错了,所以改了String,至于报错的原因是没有运用@RequestParam注解去标识list,所以导致没有正常接纳参数报错,关于其前置途径都是相同,唯一不相同的便是禁用恳求的后一个占位途径为0,而启用的则为1。
因而这块咱们就应该想到在Controller层的操控办法上的@POSTMapping中运用一个占位符来把它获取,之后做判别来进行是启用仍是禁用操作。
Controller层
/**
* 依据传递回来的status来判别是启用仍是修正操作
* 之后依据id来批量删去修正
*
* @param status
* @param ids
* @return
*/
@PostMapping("/status/{status}")
public R<String> updateStatusById(@PathVariable("status") Integer status, String ids) {
//应为参数ids为String类型的参数,而且里边的id用 , 离隔,因而咱们需求将其分割出来
String[] split = ids.split(",");
dishService.updateStatusById(status, split);
return R.success("修正状况成功");
}
Service接口
/**
* 依据传递回来的status来判别是启用仍是修正操作
* 之后依据id来批量删去修正
* @param status
* @param ids
* @return
*/
void updateStatusById(Integer status, String[] ids);
Service完结类
/**
* 依据传递回来的status来判别是启用仍是修正操作
* 之后依据id来批量删去修正
* @param status
* @param ids
* @return
*/
@Override
@Transactional
public void updateStatusById(Integer status, String[] ids) {
//禁售事务操作
if (status == 0) {
for (String id : ids) {
dishMapper.updateStatusById(0, Long.valueOf(id));
}
}
//起售事务操作
if (status == 1) {
for (String id : ids) {
dishMapper.updateStatusById(1, Long.valueOf(id));
}
}
}
mapper接口
/**
* 依据id修正菜品状况信息
* @param status
* @param id
* @return
*/
int updateStatusById(@Param("status") Integer status, @Param("id") Long id);
mapper.xml
<mapper namespace="com.huaishushu.mapper.DishMapper">
<update id="updateStatusById">
update dish
set status = #{status,jdbcType=NUMERIC}
where id = #{id,jdbcType=NUMERIC}
</update>
</mapper>
全体逻辑比较简单,我没有用MP我依据会比较费事,所以就运用了编写sql的办法来,这样反而很快。
删去菜品
需求剖析
在菜品办理列表页面点击删去按钮,或许批量挑选菜品后,来完结关于菜品信息的删去,可是由于咱们前面在新增菜品的时分也保存有对应的口味信息,因而当咱们删去菜品信息的时分,相应的口味信息也应该跟着一起删去掉,而且还要判别当时的菜品是否为起售,或许当时菜品是否在套餐中,假如在的话那么无法删去。
代码开发-整理交互进程
当咱们点击删去,或许批量挑选后运用批量删去的时分,前端发送ajax给后端,其恳求类型为DELETE类型,带着的参数类型与前面的启用和禁用是相同的string字符串类型的ids。
此处405是由于我前面现已界说并完结了这块功用,我仅仅把@DeleteMapping给注释掉了,阅读器还没反响过来,我开了热布置。
Controller层
/**
* 依据id删去菜品信息(也可批量删去)
* @param ids (id字符串)
* @return
*/
@DeleteMapping
public R<String> delete(String ids) {
String[] split = ids.split(",");
dishService.delete(split);
return R.success("删去成功");
}
Service接口
/**
* 依据id删去菜品信息(也可批量删去)
* @param ids (id数组)
*/
void delete(String[] ids);
Service完结类
/**
* 依据id删去菜品信息(也可批量删去)
* @param ids (id数组)
*/
@Override
@Transactional
public void delete(String[] ids) {
for (String id : ids) {
//进行事务判别,当时菜品是否在套餐中售卖,以及是否为起售状况,假如是则抛出事务反常
Dish dish = dishMapper.selectById(id);
LambdaQueryWrapper<SetmealDish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
dishLambdaQueryWrapper.eq(SetmealDish::getDishId, id);
long count = setmealDishService.count(dishLambdaQueryWrapper);
if (dish.getStatus() == 1) {
throw new CustomException("当时菜品正在起售,无法删去");
}
if (count > 0) {
throw new CustomException("当时菜品在套餐中,无法删去");
}
//删去菜品的根本信息
dishMapper.deleteById(id);
//删去菜品对应的口味信息
LambdaQueryWrapper<DishFlavor> lqw = new LambdaQueryWrapper<>();
lqw.eq(DishFlavor::getDishId, id);
dishFlavorService.remove(lqw);
}
}
全体思路便是遍历id,并进行事务逻辑判别当时菜品是否符合事务逻辑,是否在起售,是否在套餐中,假如都不在,那么逐个删去当时菜品的根本信息以及相关的口味信息。
套餐模块
新增套餐
需求剖析
套餐便是菜品的调集。
后台体系中能够办理套餐信息,经过新增套餐功用来增加一个新的套餐,在增加套餐时需求挑选当时套餐所属的套餐分类和包含的菜品,而且需求上传套餐对应的图片,在移动端会依照套餐分类来展现对应的套餐。
数据模型
新增套餐,其实便是将新增页面录入的套餐信息刺进到setmeal表,还需求向setmeal dish表刺进套餐和菜品相关数据所以在新增套餐时,涉及到两个表: setmeal 套餐表
setmeal dish 套餐菜品关系表
代码开发
在开发事务功用前,先将需求用到的类和接口根本结构创立好:
- 实体类SetmealDish(直接从课程材猜中导入即可,Setmeal实体前面课程中现已导入过了)
- DTO SetmealDto (直接从课程材猜中导入即可)
- Mapper接口 SetmealDishMapper
- 事务层接口 SetmealDishService
- 事务层完结类 SetmealDishServicelmpl
- 操控层 SetmealController
假如是运用了MP代码生成器的小伙伴则导入SetmealDto,而且在相应的实体类上弥补公共字段填充的注解@TableField,还有便是在实体类特点上isDeleted弥补@TableLogic注解,这个是用来做逻辑删去的时分运用的。
之后主要是在SetmealController来调用接口办法,而咱们运用MP生成的剩余的Controller等整个项目开发完后,假如真实没用,在删去即可。
代码开发-整理交互进程
在开发代码之前,需求整理一下新增套餐时前端页面和服务端的交互进程:
-
页面(backend/page/combo/add.html)发送ajax恳求,恳求服务端获取套餐分类数据并展现到下拉框中
-
页面发送aiax恳求,恳求服务端获取菜品分类数据并展现到增加菜品窗口中
-
页面发送aiax恳求,恳求服务端,依据菜品分类查询对应的菜品数据并展现到增加菜品窗口中
-
页面发送恳求进行图片上传,恳求服务端将图片保存到服务器
-
页面发送恳求进行图片下载,将上传的图片进行回显
-
点击保存按钮,发送ajax恳求,将套餐相关数据以ison办法提交到服务端
开发新增套餐功用,其实便是在服务端编写代码去处理前端页面发送的这6次恳求即可,不过1、2、4、5的这四次url的操控办法咱们都现已在前面的模块中完结了,1、2是在前面菜品的增加事务中完结的,图片也是,因而咱们只需求完结2、6即可。
2、菜品信息闪现
关于咱们的菜品信息闪现,咱们从页面发送的url中就能够看到,其是发送的分类信息的id来获取菜品的信息数据,因而咱们只需求在DishController层界说相应的操控办法来获取参数并回来数据即可。
由于它传递的参数是categoryId分类信息的id,而咱们的Dish实体类中也有这个特点,因而在操控器办法中界说实体类来接纳参数复用性会更好。
DishController类
/**
* 依据条件来查询菜品信息
* @param dish
*/
@GetMapping("/list")
public R<List<Dish>> list(Dish dish) {
List<Dish> dishList = dishService.listByCategoryId(dish);
return R.success(dishList);
}
IDishService接口
/**
* 依据条件来查询菜品信息
* @param dish
*/
List<Dish> listByCategoryId(Dish dish);
IDishService完结类
/**
* 依据条件来查询菜品信息
* @param dish
*/
@Override
public List<Dish> listByCategoryId(Dish dish) {
LambdaQueryWrapper<Dish> lqw = new LambdaQueryWrapper<>();
lqw.eq(dish.getCategoryId() != null, Dish::getCategoryId, dish.getCategoryId());
//查询状况为1的数据,便是起售的
lqw.eq(Dish::getStatus, 1);
//增加排序条件
lqw.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
List<Dish> dishList = dishMapper.selectList(lqw);
return dishList;
}
经过测验,咱们能够看到它现已能够依据不同的菜品分类来闪现菜品信息。
6、套餐信息的保存
这块的套餐保存和咱们前面的新增菜品信息时是相同的,都是对两张表进行操作,而且都是相关表中短少一个id,因而咱们模仿之前菜品信息新增的事务来写就能够了。
Controller层
/**
* <p>
* 套餐 前端操控器
* </p>
*
* @author ChiBaoBaoHuaiShuShu
* @since 2023-05-28
*/
@RestController
@RequestMapping("/setmeal")
@Slf4j
public class SetmealController {
@Autowired
private ISetmealService setmealService;
@Autowired
private ISetmealDishService setmealDishService;
/**
* 增加套餐信息
* 操作两张表,setmeal_dish和setmeal表
* @param setmealDto
* @return
*/
@PostMapping
public R<String> saver(@RequestBody SetmealDto setmealDto) {
log.info("新增套餐信息:{}",setmealDto);
setmealService.saverWithDish(setmealDto);
return R.success("增加套餐成功");
}
}
Service接口
/**
* <p>
* 套餐 服务类
* </p>
*
* @author ChiBaoBaoHuaiShuShu
* @since 2023-05-28
*/
public interface ISetmealService extends IService<Setmeal> {
/**
* 增加套餐信息
* 操作两张表,setmeal_dish和setmeal表
* @param setmealDto
* @return
*/
void saverWithDish(SetmealDto setmealDto);
}
Service完结类
/**
* <p>
* 套餐 服务完结类
* </p>
*
* @author ChiBaoBaoHuaiShuShu
* @since 2023-05-28
*/
@Service
public class SetmealServiceImpl extends ServiceImpl<SetmealMapper, Setmeal> implements ISetmealService {
@Autowired
private SetmealMapper setmealMapper;
@Autowired
private ISetmealDishService setmealDishService;
/**
* 增加套餐信息
* 操作两张表,setmeal_dish和setmeal表
* @param setmealDto
* @return
*/
@Override
@Transactional
public void saverWithDish(SetmealDto setmealDto) {
//保存根本的套餐信息setmeal
setmealMapper.insert(setmealDto);
//SetmealDishes中短少setmealId,需求为其赋值
Long setmealId = setmealDto.getId();
for (SetmealDish setmealDish : setmealDto.getSetmealDishes()) {
setmealDish.setSetmealId(String.valueOf(setmealId));
}
//保存套餐中所关于的菜品信息setmeal_dish
setmealDishService.saveBatch(setmealDto.getSetmealDishes());
}
}
功用测验
填写好测验信息后,点击保存,能够看到url恳求现已呼应成功了。
查看数据库也能够看到,套餐的信息的确保存了,以及套餐关于的菜品信息也是。
套餐信息分页查询
需求剖析
体系中的套餐数据许多的时分,假如在一个页面中全部展现出来会显得比较乱,不便于查看,所以一般的体系中都会以分页的办法来展现列表数据。
代码开发-整理交互进程
在开发代码之前,需求整理一下套餐分页查询时前端页面和服务端的交互进程:
- 页面(backend/page/combo/list.html)发送ajax恳求,将分页查询参数(page、pageSizename)提交到服务端,获取分页数据
- 页面发送恳求,恳求服务端进行图片下载,用于页面图片展现
开发套餐信息分页查询功用,其实便是在服务端编写代码去处理前端页面发送的这2次恳求即可。
这块的代码开发和前面的菜品信息的分页查询是相同的,所以全体的事务逻辑也是相同的,所以咱们模仿之前的菜品信息分页查询的模块,来自己测验的来完结这块代码会比较好。
Controller层
/**
* 套餐信息分页查询
* @param page
* @param pageSize
* @param name
* @return
*/
@GetMapping("/page")
public R<Page<SetmealDto>> page(Integer page,Integer pageSize,String name) {
Page<SetmealDto> setmealDtoPage = setmealService.page(page, pageSize, name);
log.info("菜品信息分页查询:{}",setmealDtoPage);
return R.success(setmealDtoPage);
}
Service接口
/**
* 菜品信息分页查询
* @param page
* @param pageSize
* @param name
* @return
*/
Page<SetmealDto> page(Integer page, Integer pageSize, String name);
Service完结类
/**
* 菜品信息分页查询
* @param page
* @param pageSize
* @param name
* @return
*/
@Override
public Page<SetmealDto> page(Integer page, Integer pageSize, String name) {
//结构Setmeal的分页结构器
Page<Setmeal> setmealPage = new Page<>();
LambdaQueryWrapper<Setmeal> lqw = new LambdaQueryWrapper<>();
lqw.like(StringUtils.isNoneEmpty(name), Setmeal::getName, name);
lqw.orderByDesc(Setmeal::getUpdateTime);
setmealMapper.selectPage(setmealPage, lqw);
//构建SetmealDto的分页结构器
Page<SetmealDto> setmealDtoPage = new Page<>();
//目标复制,但不复制“records”
BeanUtils.copyProperties(setmealPage,setmealDtoPage,"records");
List<Setmeal> records = setmealPage.getRecords();
List<SetmealDto> setmealDtoList = records.stream().map(item -> {
SetmealDto setmealDto = new SetmealDto();
//目标复制
BeanUtils.copyProperties(item, setmealDto);
Long categoryId = item.getCategoryId();
Category category = categoryService.getById(categoryId);
//此处是由于当咱们自己增加或经过sql文件导入菜品时并没有设置分类,一切或许是会为空的
if (category != null) {
String categoryName = category.getName();
setmealDto.setCategoryName(categoryName);
}
return setmealDto;
}).collect(Collectors.toList());
setmealDtoPage.setRecords(setmealDtoList);
return setmealDtoPage;
}
这块Service完结类的办法不理解的小伙伴,这时分应该停下来,去前面咱们编写的菜品信息分页查询的模块,复习一下,那儿我写有较为详细的逻辑思路。
功用测验
断点我就不跑了,这边的作用仍是和前面菜品信息分页查询时的相同,不理解的小伙伴能够去那个模块复习一下,感兴趣的小伙伴能够直接跑一下断点,作用应该是相同的。
修正套餐
需求剖析
在套餐办理列表页面点击修正按钮,跳转到修正套餐页面,在修正页面回显套餐相关信息并进行修正,最终点击确认按钮完结修正操作
代码开发-整理交互进程
在开发代码之前,需求整理一下修正套餐时前端页面 (add.html)和服务端的交互进程:
- 页面发送aiax恳求,恳求服务端获取分类数据,用于套餐分类下拉框中数据展现
- 页面发送ajax恳求,恳求服务端,依据id查询当时套餐信息,用于套餐信息回显
- 页面发送ajax恳求,恳求服务端取得菜品分类信息中所对应的菜品信息,用于套餐菜品中的增加菜品框的闪现。
- 页面发送恳求,恳求服务端进行图片下载,用于页图片回显
- 点击保存按钮,页面发送ajax恳求,将修正后的菜品相关数据以ison办法提交到服务端
开发修正套餐功用,其实便是在服务端编写代码去处理前端页面发送的这5次恳求即可,由于咱们前面现已完结了关于套餐分类信息下拉框的事务,以及图片的上传和下载回显模块,而且咱们也在套餐增加模块中完结了增加菜品框的信息回显功用。其实就想担当于现已完结了1、3、4,那么咱们剩余只需求完结2和5即可。
恳求2
第2次恳求,关于依据id来查询当时套餐信息,并回显,咱们能够先看以下前端页面发送的url恳求链接,从中咱们能够看到其id是以恳求途径的信息回来的,并不是以参数的办法,因而咱们之后在Controller层中界说办法来接纳的时分就需求在@GetMapping中界说参数来接纳了。
Controller层
/**
* 依据套餐id来套餐的根本信息以及套餐所对应的菜品信息
* @param setmealId
* @return
*/
@GetMapping("{setmealId}")
public R<SetmealDto> getByIdWithDish(@PathVariable("setmealId") Long setmealId) {
SetmealDto setmealDto = setmealService.getByIdWithDish(setmealId);
return R.success(setmealDto);
}
service接口
/**
* 依据套餐id来套餐的根本信息以及套餐所对应的菜品信息
* @param setmealId
* @return
*/
SetmealDto getByIdWithDish(Long setmealId);
servicea完结类
/**
* 依据套餐id来套餐的根本信息以及套餐所对应的菜品信息
* @param setmealId
* @return
*/
@Override
public SetmealDto getByIdWithDish(Long setmealId) {
//查询套餐的根本信息,从setmeala表中查询
Setmeal setmeal = setmealMapper.selectById(setmealId);
SetmealDto setmealDto = new SetmealDto();
BeanUtils.copyProperties(setmeal, setmealDto);
//查询当时套餐所对应的菜品信息,从setmeal_dish表查询
LambdaQueryWrapper<SetmealDish> lqw = new LambdaQueryWrapper<>();
lqw.eq(SetmealDish::getSetmealId, setmealId);
List<SetmealDish> dishes = setmealDishService.list(lqw);
setmealDto.setSetmealDishes(dishes);
return setmealDto;
}
这块的完结类回显思路和咱们前面修正菜品信息时的思路时相同的,都是先依据套餐id来查询套餐的根本信息先,然后在构建一个DTO目标,并把这些根本信息赋值给DTO目标,这样DTO目标就只差套餐所相关的菜品的调集了,那么咱们在运用条件结构器来完结对其的查询获取菜品的集兼并赋值给DTO目标中的调集特点即可。
恳求4
当咱们点击保存按钮后,页面发送的恳求中,咱们能够看到它的恳求类型为PUT,而且参数的类型和咱们前面增加套餐信息的格局是相同的,因而咱们这块能够参阅之前新增套餐信息的办法,而且全体的事务逻辑思路是和前面的菜品信息修正是相同的。
Controller层
/**
* 修正套餐信息,操作的是两张表,还需求刺进菜品信息
* @param setmealDto
* @return
*/
//@PutMapping
public R<String> update(@RequestBody SetmealDto setmealDto) {
setmealService.updateWithDish(setmealDto);
return R.success("修正套餐信息成功");
}
Service接口
/**
* 修正套餐信息,操作的是两张表,还需求刺进菜品信息
* @param setmealDto
* @return
*/
void updateWithDish(SetmealDto setmealDto);
Service完结类
/**
* 修正套餐信息,操作的是两张表,还需求刺进菜品信息
* @param setmealDto
* @return
*/
@Override
@Transactional
public void updateWithDish(SetmealDto setmealDto) {
//修正根本的套餐信息setmeal
setmealMapper.updateById(setmealDto);
//整理当时菜品对应口味数据--dish_flavor表的delete操作
LambdaQueryWrapper<SetmealDish> lqw = new LambdaQueryWrapper<>();
lqw.eq(SetmealDish::getSetmealId, setmealDto.getId());
setmealDishService.remove(lqw);
//SetmealDishes中短少setmealId,需求为其赋值
Long setmealId = setmealDto.getId();
for (SetmealDish setmealDish : setmealDto.getSetmealDishes()) {
setmealDish.setSetmealId(String.valueOf(setmealId));
}
//保存套餐中所关于的菜品信息setmeal_dish
setmealDishService.saveBatch(setmealDto.getSetmealDishes());
}
在完结类中,由于其的功用和前面修正菜品和新增套餐相似,所以咱们能够参阅前面的办法,以下是咱们的完结进程:
-
先修正更新套餐的根本信息。
-
整理当时套餐对应的菜品数据。
-
当咱们从头保存菜品信息时,遇到情况是和新增套餐时相同的,短少套餐的id,一切循环遍历为其赋值。
-
从头为其增加套餐的菜品信息,以到达菜品信息修正的事务意图。
停售/起售
需求剖析
在套餐办理列表页面点击禁用/启用按钮,或许批量挑选菜品后,来完结关于菜品状况的修正。
代码开发-整理交互进程
当咱们点击启用/禁用,或许批量挑选后运用批量启用/禁用时,前端发送ajax给后端,后端依据前端的发送回来的情况,在详细操作,关于状况的修正有如下4种类型的ajax恳求:
禁用:
启售:
它们所发送的恳求办法都是POST,传递的参数都是相同的,这是一个ids参数,这块有比较多种的接纳办法,唯一不相同的便是禁用恳求的后一个占位途径为0,而启用的则为1。
因而这块咱们就应该想到在Controller层的操控办法上的@POSTMapping中运用一个占位符来把它获取,之后做判别来进行是启用仍是禁用操作。
全体的事务逻辑思路是和菜品信息的启用/禁用是相同的。
Controller层
/**
* 依据传递回来的status来判别是启用仍是修正操作
* 之后依据id来批量删去修正
* @param status
* @param ids
* @return
*/
@PostMapping("/status/{status}")
public R<String> updateStatusById(@PathVariable("status") Integer status, String ids) {
//应为参数ids为String类型的参数,而且里边的id用 , 离隔,因而咱们需求将其分割出来
String[] split = ids.split(",");
setmealService.updateStatusById(status, split);
return R.success("修正状况成功");
}
Service接口
/**
* 依据传递回来的status来判别是启用仍是修正操作
* 之后依据id来批量修正
* @param status
* @param ids
* @return
*/
void updateStatusById(Integer status, String[] split);
Service完结类
/**
* 依据传递回来的status来判别是启用仍是修正操作
* 之后依据id来批量修正
* @param status
* @param ids
* @return
*/
@Override
@Transactional
public void updateStatusById(Integer status, String[] ids) {
//禁售事务操作
if (status == 0) {
for (String id : ids) {
setmealMapper.updateStatusById(0, Long.valueOf(id));
}
}
//起售事务操作
if (status == 1) {
for (String id : ids) {
setmealMapper.updateStatusById(1, Long.valueOf(id));
}
}
}
mapper接口
/**
* 依据id修正套餐状况信息
* @param status
* @param id
* @return
*/
int updateStatusById(@Param("status") Integer status, @Param("id") Long id);
mapper.xml
<mapper namespace="com.huaishushu.mapper.SetmealMapper">
<update id="updateStatusById">
update setmeal
set status = #{status,jdbcType=NUMERIC}
where id = #{id,jdbcType=NUMERIC}
</update>
</mapper>
全体逻辑比较简单,我没有用MP我依据会比较费事,所以就运用了编写sql的办法来,这样反而很块。
删去套餐
需求剖析
在套餐办理列表页面点击删去按钮,能够删去对应的套餐信息。也能够经过复选框挑选多个套餐,点击批量删去按钮一次删去多个套餐。留意,关于状况为售卖中的套餐不能删去,需求先停售,然后才能删去。
代码开发-整理交互进程
在开发代码之前,需求整理一下删去套餐时前端页面和服务端的交互进程
-
删去单个套餐时,页面发送aiax恳求,依据套餐id删去对应套餐
-
删去多个套餐时,页面发送ajax恳求,依据提交的多个套餐id删去对应套餐
开发删去套餐功用,其实便是在服务端编写代码去处理前端页面发送的这2次恳求即可。
调查删去单个套餐和批量删去套餐的恳求信息能够发现,两种恳求的地址和恳求办法都是相同的,不同的则是传递的id个数,所以在服务端能够供给一个办法来一致处理。
其实这个模块的事务功用和前面的菜品信息的删去功用是相同的,全体流程事务逻辑根本相同的,因而咱们能够模仿菜品信息删去的代码逻辑来完结这个模块的功用。
Controller层
/**
* 依据id字符串来批量删去套餐信息
* @param ids
* @return
*/
@DeleteMapping
public R<String> delete(String ids) {
log.info("删去的套餐id为:{}",ids);
String[] split = ids.split(",");
setmealService.delete(split);
return R.success("删去套餐成功");
}
Service接口
/**
* 依据id数组来批量删去套餐信息
* @param ids
*/
void delete(String[] ids);
Service完结类
/**
* 依据id数组来批量删去套餐信息
* @param ids
*/
@Override
@Transactional
public void delete(String[] ids) {
for (String id : ids) {
//判别当时套餐是否在售卖,假如在售卖则抛出一个事务反常
Setmeal setmeal = setmealMapper.selectById(id);
if (setmeal.getStatus() == 1) {
throw new CustomException("套餐正在售卖,无法删去,售卖的套餐称号为:" + setmeal.getName());
}
//删去套餐的根本信息
setmealMapper.deleteById(id);
//删去套餐相关的菜品信息
LambdaQueryWrapper<SetmealDish> lqw = new LambdaQueryWrapper<>();
lqw.eq(SetmealDish::getSetmealId, id);
setmealDishService.remove(lqw);
}
}
全体的思路便是获取ids参数,并遍历id然后进行事务判别,当时套餐是否正在售卖,假如在售卖则抛出反常,假如没有售卖则进行删去。
或许遇到的问题
乱码问题
有的小伙伴在项目开发进程中会遇到乱码问题,这个情况咱们只需求在yml文件中增加字符编码的设置即可(UTF-8)
server:
port: 80
servlet:
# 完结编码一致
encoding:
charset: utf-8
enabled: true
force: true
bean循环内嵌
在这处事务开发中,有的小伙伴或许会遇到以下的报错提示,这儿的报错信息闪现:
不鼓舞依靠循环引证,默许情况下制止运用循环引证。更新运用程序以删去 Bean 之间的依靠循环。作为最终的手法,能够经过将spring.main.allow-circular-references设置为true来主动打破循环。
解决办法:
前面的报错信息也提示咱们了,所以咱们在yml文件中以下特点即可
spring:
main:
allow-circular-references: true