学生作者:吃饱饱坏蜀黍
springboot版本:2.7.12
jdk版本:20
开发编辑器工具:IDEA 2023.1.2
数据库:Mysql(社区版)8.0.32
数据库管理工具:Navicat Premium 16
操作系统:window10
网盘链接:链接:https://pan.baidu.com/s/19YTUCBtgdDPrmnQenn_VBQ
提取码:arqf
创建springboot项目,构建maven项目,导入所需的jar包,所使用到的jar包下面都有注解。
<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.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.7.12version>
<relativePath/>
parent>
<groupId>com.HuaiShuShugroupId>
<artifactId>RuiJiWaiMaiartifactId>
<version>0.0.1-SNAPSHOTversion>
<properties>
<java.version>20java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>com.mysqlgroupId>
<artifactId>mysql-connector-jartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.5.3.1version>
dependency>
<dependency>
<groupId>com.alibaba.fastjson2groupId>
<artifactId>fastjson2artifactId>
<version>2.0.32version>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-lang3artifactId>
<version>3.12.0version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
<version>1.2.18version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-generatorartifactId>
<version>3.5.1version>
dependency>
<dependency>
<groupId>org.freemarkergroupId>
<artifactId>freemarkerartifactId>
<version>2.3.31version>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
exclude>
excludes>
configuration>
plugin>
plugins>
build>
project>
sql文件教长,我就不在这里放置了,需要的小伙伴可以通过黑马的小程序获取,也可以通过我的网盘地址获取。
在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
在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类型的实体类。
package com.huaishushu.common;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
/**
* 通用返回结果,服务端响应的数据最终都会封装成此对象
* @param
*/
@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层
/**
*
* 员工信息 前端控制器
*
*
* @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接口
/**
*
* 员工信息 服务类
*
*
* @author ChiBaoBaoHuaiShuShu
* @since 2023-05-28
*/
public interface IEmployeeService extends IService<Employee> {
/**
* 根据用户的username查询是否存在
* @param username
* @return
*/
Employee login(String username);
}
service实现类
/**
*
* 员工信息 服务实现类
*
*
* @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查询数据。
通过这边的代码开发分析,我们知道需要在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就能够正常保存。
具体实现步骤:
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页面为公共页面,新增员工和编辑员工都是在此页面操作
我们在页面中点击编辑按钮后会跳转到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公共字段自动填充,也就是在插入或者更新的时候为指定字段赋予指定的值,使用它的好处就是可以统一对这些字段进行处理,避免了重复代码。
实现步骤:
在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来解决此问题,它是JDK中提供的一个类
在学习ThreadLocal之前,我们需要先确认一个事情,就是客户端发送的每次http请求,对应的在服务端都会分配一个新的线程来处理,在处理过程中涉及到下面类中的方法都属于相同的一个线程:
每个人的线程可能不一致,并且大家要注意看喔,因为我们没刷新一次就要发送一次http请求,这里面的线程就有可能会改变,所以大家要注意看线程的id。
什么是ThreadLocal?
ThreadLocal并不是一个Thread,而是Thread的局部变量。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。
ThreadLocal常用方法:
我们可以在LoginCheckFilter的doFilter方法中获取当前登录用户id,并调用ThreadLocal的set方法来设置当前线程的线程局部变量的值 (用户id),然后在MyMetabiectHandler的updateFil方法中调用ThreadLocal的get方法来获得当前
线程所对应的线程局部变量的值 (用户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;
/**
*
* 菜品及套餐分类
*
*
* @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;
}
在开发代码之前,需要梳理一下整个程序的执行过程:
可以看到新增菜品分类和新增套餐分类请求的服务端地址和提交的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.*;
/**
*
* 菜品及套餐分类 前端控制器
*
*
* @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;
/**
*
* 菜品及套餐分类 服务类
*
*
* @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;
/**
*
* 菜品及套餐分类 服务实现类
*
*
* @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报错。
温馨提示
在开发代码之前,需要梳理一下整个程序的执行过程
因此我们在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;
}
查询出来的信息,咱们按时间排序了一下。
在分类管理列表页面,可以对某个分类进行删除操作。需要注意的是当分类关联了菜品或者套餐时,此分类不允许删除。
在开发代码之前,需要梳理一下整个程序的执行过程:
那么我们在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删除分类的功能,但是并没有检查删除的分类是否关联了菜品或者套餐,所以我们需要进行功能完善。
要完善分类删除功能,需要先准备基础的类和接口:
如果是MP代码生成器生成的项目结果,此处也要记得在实体类Dish和Setmeal类中为公共字段添加@TableField,如果是复制课程资料的则直接复制就行了。
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;
/**
*
* 菜品管理
*
*
* @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;
}
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;
/**
*
* 套餐
*
*
* @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类:
/**
*
* 菜品及套餐分类 服务实现类
*
*
* @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表单有如下要求
服务端要接收客户端页面上传的文件,通常都会使用Apache的两个组件
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\
温馨提示
我们上传好后可以去相应路径下查看是否上传成功,但发现上传后,但是在相应文件夹下却没有显示的时候,我们应该去先去登录账号先,因为我们前面配置的过滤器中需要判断我们是否已经登录了账号,没登录的话就会被过滤了。
解决方法:
文件下载,页面端可以使用标签展示下载的图片
文件下载我们在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表插入数据。
所以在新增菜品时,涉及到两个表:
在开发业务功能前,先将需要用到的类和接口基本结构创建好:
如果使用的是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的控制方法来接收它的参数来实现存储。
导入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层
/**
*
* 菜品管理 前端控制器
*
*
* @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接口
/**
*
* 菜品管理 服务类
*
*
* @author ChiBaoBaoHuaiShuShu
* @since 2023-05-28
*/
public interface IDishService extends IService<Dish> {
/**
* 新增菜品信息,操作的是两张表,还需要插入口味
* @return
*/
void saveWithFlavor(DishDto dishDto);
}
service实现类
/**
*
* 菜品管理 服务实现类
*
*
* @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,所以菜品分页查询这块会有些复杂。
代码开发-梳理交互过程
在开发代码之前,需要梳理一下菜品分页查询时前端页面和服务端的交互过程:
开发菜品信息分页查询功能,其实就是在服务端编写代码去处理前端页面发送的这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集合里的,所以接下来外面的步骤是:
从第一步就可以看出传给前端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)和服务端的交互过程:
开发修改菜品功能,其实就是在服务端编写代码去处理前端页面发送的这4次请求即可,因为我们前面已经完成了对于菜品分类下拉框的业务,以及图片的上传和下载回显模块,其实就想担当于已经完成了1和3,那么我们剩下只需要完成2和4即可。
第二次请求,对于根据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对象来完成的,它的实现步骤为:
当我们点击保存按钮后,页面发送的请求中,我们可以看到它的请求类型为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());
}
在实现类中,因为其的功能和前面新增菜品类似,所以我们可以参考前面的方法,以下是我们的实现步骤:
后面更新那块有的小伙伴可能会想使用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 套餐菜品关系表
在开发业务功能前,先将需要用到的类和接口基本结构创建好:
如果是使用了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即可。
对于我们的菜品信息显示,我们从页面发送的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;
}
经过测试,我们可以看到它已经可以根据不同的菜品分类来显示菜品信息。
这块的套餐保存和我们前面的新增菜品信息时是一样的,都是对两张表进行操作,并且都是关联表中缺少一个id,因此我们模仿之前菜品信息新增的业务来写就可以了。
Controller层
/**
*
* 套餐 前端控制器
*
*
* @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接口
/**
*
* 套餐 服务类
*
*
* @author ChiBaoBaoHuaiShuShu
* @since 2023-05-28
*/
public interface ISetmealService extends IService<Setmeal> {
/**
* 添加套餐信息
* 操作两张表,setmeal_dish和setmeal表
* @param setmealDto
* @return
*/
void saverWithDish(SetmealDto setmealDto);
}
Service实现类
/**
*
* 套餐 服务实现类
*
*
* @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请求已经响应成功了。
查看数据库也可以看到,套餐的信息确实保存了,以及套餐对于的菜品信息也是。
系统中的套餐数据很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。
在开发代码之前,需要梳理一下套餐分页查询时前端页面和服务端的交互过程:
开发套餐信息分页查询功能,其实就是在服务端编写代码去处理前端页面发送的这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)和服务端的交互过程:
开发修改套餐功能,其实就是在服务端编写代码去处理前端页面发送的这5次请求即可,因为我们前面已经完成了对于套餐分类信息下拉框的业务,以及图片的上传和下载回显模块,并且我们也在套餐添加模块中完成了添加菜品框的信息回显功能。其实就想担当于已经完成了1、3、4,那么我们剩下只需要完成2和5即可。
第二次请求,对于根据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对象中的集合属性即可。
当我们点击保存按钮后,页面发送的请求中,我们可以看到它的请求类型为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的方式来,这样反而很块。
在套餐管理列表页面点击删除按钮,可以删除对应的套餐信息。也可以通过复选框选择多个套餐,点击批量删除按钮一次删除多个套餐。注意,对于状态为售卖中的套餐不能删除,需要先停售,然后才能删除。
在开发代码之前,需要梳理一下删除套餐时前端页面和服务端的交互过程
开发删除套餐功能,其实就是在服务端编写代码去处理前端页面发送的这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然后进行业务判断,当前套餐是否正在售卖,如果在售卖则抛出异常,如果没有售卖则进行删除。
目前市面上有很多第三方提供的短信服务,这些第三方短信服务会和各个运营商(移动、联通、电信)对接,我们只需要注册成为会员并且按照提供的开发文档进行调用就可以发送短信。需要说明的是,这些短信服务一般都是收费服务。
常用短信服务:
我们项目使用的是阿里云的短信服务。
阿里云短信服务(Short Message Service) 是广大企业客户快速触达手机用户所优选使用的通信能力。调用API或用群发助手,即可发送验证码、通知类和营销类短信;国内验证短信秒级触达,到达率最高可达99%,国际/港澳台短信覆盖200多个国家和地区,安全稳定,广受出海企业选用。
应用场景:
感兴趣的小伙伴可以去阿里云官网的产品中搜索短信服务,我们项目中使用的是第一类的短信验证码。
阿里云官网: https://www.aliyun.com/
点击官网首页注册按钮,跳转到如下注册页面
注册登记号后,点击控制台后并搜索短信服务进去里面,选择国内消息进入到文本短信。
点击添加签名后我们进入填写基本信息就可以了。
这里我测回并修改了签名,改成了吃饱饱坏蜀黍了。
光标移动到用户头像上,在弹出的窗口中点击[AccessKey 管理]
选择子用户AccessKey更安全一些,不然直接使用AccessKey权限太高了,泄露后危险太大。
不需要控制台,因为我们可以直接使用当前的账户在页码操作,这里我们需要的是使用它提供的API接口来操作。
如果咱们的key和密码泄露后,我们可以在认证管理往下拉,然后对对应的key进行操作,禁用和删除都是可以的。
笔者本人目前为在校大三的学生,所以在注册阿里云的时候使用了学生认证。
使用阿里云短信服务发送短信,可以参照官方提供的文档即可
具体开发步骤:
导入maven坐标
<dependency>
<groupId>com.aliyungroupId>
<artifactId>aliyun-java-sdk-coreartifactId>
<version>4.5.16version>
dependency>
<dependency>
<groupId>com.aliyungroupId>
<artifactId>aliyun-java-sdk-dysmsapiartifactId>
<version>2.1.0version>
dependency>
调用API
/**
* 短信发送工具类
*/
public class SMSUtils {
/**
* 发送短信
* @param signName 签名
* @param templateCode 模板
* @param phoneNumbers 手机号
* @param param 参数
*/
public static void sendMessage(String signName, String templateCode,String phoneNumbers,String param){
DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", "", "");
IAcsClient client = new DefaultAcsClient(profile);
SendSmsRequest request = new SendSmsRequest();
request.setSysRegionId("cn-hangzhou");
request.setPhoneNumbers(phoneNumbers);
request.setSignName(signName);
request.setTemplateCode(templateCode);
request.setTemplateParam("{\"code\":\""+param+"\"}");
try {
SendSmsResponse response = client.getAcsResponse(request);
System.out.println("短信发送成功");
}catch (ClientException e) {
e.printStackTrace();
}
}
}
这个工具类直接在资料里的工具类中就能看到,咱们直接复制进项目中就可以,单独开一个Utils包来存放工具类就可以了。
箭头所指的地方填上我们前面在阿里云短信服务那边申请的AccessKey和密钥。
为了方便用户登录,移动端通常都会提供通过手机验证码登录的功能。
手机验证码登录的优点:
登录流程:
输入手机号>获取验证码>输入验证码>点击登录>登录成功
注意:通过手机验证码登录,手机号是区分不同用户的标识。
通过手机验证码登录时,涉及的表为user表,即用户表。结构如下:
在开发代码之前,需要梳理一下登录时前端页面和服务端的交互过程:
开发手机验证码登录功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。
在开发业务功能前,先将需要用到的类和接口基本结构创建好:实体类 User (直接从课程资料中导入即可)
使用MP代码生成器的小伙伴,直接复制导入工具类即可。
前面我们已经完成了LoginCheckFilter过滤器的开发,此过滤器用于检查用户的登录状态。我们在进行手机验证码登录时,发送的请求需要在此过滤器处理时直接放行。
并且第四步那边添加一个4-2,用来判断移动端用户是否登录了,这里的userId就是我们之后登录后会保存用户的id到session中,这样过滤器就能够检验我们是否登录了。
// 4-2、判断登录状态,如果已登录,则直接放行
if (request.getSession().getAttribute("userId") != null) {
log.info("移动端用户已登录,用户id为:" + request.getSession().getAttribute("userId"));
//设置线程局部变量,供公共字段自动填充用
Long userId = (Long) request.getSession().getAttribute("userId");
BaseContext.setCurrentId(userId);
filterChain.doFilter(request, response);
return;
}
前端当我们点击获取验证码的时候会去调用获取验证码的时候,它会去调用getCode()点击函数,但是我们这块的点击函数黑马给的资料中是缺少67行的sendMsgApi这个api请求的,因此我们需要补充这行代码,并且在移动端front文件下api包的login.js中添加上对应的sendMsgApi请求函数。
我们生成验证码使用的是工具类ValidateCodeUtils,发送短信验证码使用的是SMSUtils类,在ValidateCodeUtils中有一个generateValidateCode是用来生成4或6位的验证码的,之后我们直接调用这个方法即可
/**
* 随机生成验证码
* @param length 长度为4位或者6位
* @return
*/
public static String generateValidateCode(int length,String phone){
Integer code =null;
if(length == 4){
code = new Random().nextInt(9999);//生成随机数,最大为9999
if(code < 1000){
code = code + 1000;//保证随机数为4位数字
}
}else if(length == 6){
code = new Random().nextInt(999999);//生成随机数,最大为999999
if(code < 100000){
code = code + 100000;//保证随机数为6位数字
}
}else{
throw new RuntimeException("只能生成4位或6位数字验证码");
}
return code.toString();
}
当我们在移动端登录页面输入手机号后点击获取验证码,可以看到前端页面发送的url,以及其携带的手机号参数,因此我们需要在Controller层中定义控制方法来接收传递过来的手机号参数,并生成验证码
Controller层
/**
* 移动端通过手机号获取验证码(session实现)
* @param user
* @return
*/
@PostMapping("/sendMsg")
public R<String> sendMsg(@RequestBody User user,HttpSession session) {
//获取手机号
String phone = user.getPhone();
if (StringUtils.isNoneEmpty(phone)) {
//生成随机的4位验证码
String code = ValidateCodeUtils.generateValidateCode(4);
//将验证码code保存到session中
session.setAttribute("code",code);
log.info("验证码,code为:{}",code);
//调用阿里云提供的短信API完成发送短信
//SMSUtils.sendMessage("?","?",phone,code);
return R.success("手机验证码短信发送成功");
}
return R.error("短信发送失败!!!");
}
因为这块不涉及到与数据库持久层的操作,也没有复杂的业务逻辑,因此直接在控制层进行方法调用并返回即可,SMSUtils.sendMessage这个方法就看各位小伙伴自己的条件了,如果有条件的在申请好签名以及模板后,将签名和模板分别填入即可。
如图当我们输入手机和验证码后,前端页面发送的url请求为:http://localhost/user/login,并且请求的参数为json形式,里头有phone和code两个参数。
因为黑马给的前端资料有些问题,这块可能有的小伙伴只有电话号码phone这个参数,并没有code这个参数,我们需要去static/front/page/login.html文件中78行左右添加或修改成这两段代码。
// 将手机号保存到浏览器的session
sessionStorage.setItem("userPhone",this.form.phone)
// 将移动端用户信息保存到浏览器的本地存储空间
localStorage.setItem('user',JSON.stringify(res.data))
Controller层
因为前端发送的url请求中包含了phone和code两个参数,如果光用User类型的参数来接收是无法接收的,因为User实体类中没有定义code,因此这里我们可以有两个方法:
这里笔者选择的是和黑马的一样选择了使用Map对象来接收。
/**
* 移动端登录
* @param map
* @return
*/
@PostMapping("/login")
public R<User> login(@RequestBody Map<String,String> map, HttpSession session) {
log.info("用户登录提交的手机号和验证码为:{}",map);
//获取手机号
String phone = map.get("phone");
//获取验证码
String code = map.get("code");
//从redis中获取保存的验证码
String cacheCode = validateCodeUtils.get(phone);
log.info("手机号:{},在redis中的验证码为:{}",phone,cacheCode);
if (cacheCode == null) {
return R.error("验证码已过期!!!");
}
if (code.equals(cacheCode)) {
//如果能够比对成功,说明登录成功
//判断当前手机号对应的用户是否为新用户,如果是新用户就自动完成注册
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
lqw.eq(User::getPhone, phone);
User user = userService.getOne(lqw);
if (user == null) {
user = new User();
user.setPhone(phone);
user.setName("新用户:"+phone);
userService.save(user);
}
//将用户id保存到session中,供过滤器检测用户是否登录
session.setAttribute("userId", user.getId());
return R.success(user);
}
return R.error("登录失败,验证码错误!!!");
}
在里面进行登录的时候我们添加了一些业务判断,如果当前手机号不在数据库中user表中有存储,那么说明我们这个用户没有注册过,那么我们就帮其自动注册一个账户。
使用session对象来存储固然简便,但是相对来说有些固定简略,因为往往在显示当中验证码是有时间限制的,不会长时间有效,并且保存在了session当中,也容易被人通过浏览器的开发者模式查看当前会话中存储的数据来获取验证码code。
因此通常我们都是把验证码保存到了缓存中,并且通过设置缓存的保存时间来删除存储的数据来实现验证码的时效性。
需要大家先自己去下载redis,windows版安装包下载地址:https://github.com/tporadowski/redis/releases,安装好后才能使用。
使用redis来缓存,那么首先我们需要导入maven坐标,因为springboot已经整合有redis缓存,所以我们不需要导入版本。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
在App启动类上添加@EnableCaching注解开启缓存。
在yml文件中添加相应的配置,里面一些配置,这里验证码的过期时间我设置的是600秒过期,也就是10分钟,小伙伴可以根据自己的需求来更改。
spring:
#配置redis
cache:
type: redis
redis:
#是否用前缀
use-key-prefix: false
#指定key的前缀
# key-prefix: sms_
#是否缓存空值
cache-null-values: false
#设置生命周期
time-to-live: 600s
redis:
host: localhost
port: 6379
因为我们使用redis缓存来存储验证码,因此我们需要对验证码工具类做一些调整,在ValidateCodeUtils类上添加@Component将其装入spring容器中管理,因为后面我们使用到的@CachePut和@Cacheable注解本质都是bean,既然是bean我们就需要将其转入容器中进行管理,以便后期的使用。
package com.huaishushu.Utils;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component;
import java.util.Random;
/**
* 随机生成验证码工具类
*/
@Component
public class ValidateCodeUtils {
/**
* 随机生成验证码
* @param length 长度为4位或者6位
* @return
*/
@CachePut(value = "phoneCode",key = "#phone")
public String generateValidateCode(int length,String phone){
Integer code =null;
if(length == 4){
code = new Random().nextInt(9999);//生成随机数,最大为9999
if(code < 1000){
code = code + 1000;//保证随机数为4位数字
}
}else if(length == 6){
code = new Random().nextInt(999999);//生成随机数,最大为999999
if(code < 100000){
code = code + 100000;//保证随机数为6位数字
}
}else{
throw new RuntimeException("只能生成4位或6位数字验证码");
}
return code.toString();
}
/**
* 随机生成指定长度字符串验证码
* @param length 长度
* @return
*/
@CachePut(value = "phoneCode",key = "#phone")
public String generateValidateCode4String(int length,String phone){
Random rdm = new Random();
String hash1 = Integer.toHexString(rdm.nextInt());
String capstr = hash1.substring(0, length);
return capstr;
}
/**
* 用来获取redis缓存中电话号码phone所对应的code验证码
* @param phone
* @return
*/
@Cacheable(value = "phoneCode",key = "#phone")
public String get(String phone) {
return null;
}
}
我在验证码生成工具类中在生成验证码的方法上添加@CachePut,并设置其在redis中存储的空间为"phoneCode",其key为phone,vlue为该的方法放回置,并且在后面新定义了一个get方法来获取redis中存储验证码数据。
当我们在移动端登录页面输入手机号后点击获取验证码,可以看到前端页面发送的url,以及其携带的手机号参数,因此我们需要在Controller层中定义控制方法来接收传递过来的手机号参数,并生成验证码。
Controller层
/**
* 移动端通过手机号获取验证码
* @param user
* @return
*/
//@PostMapping("/sendMsg")
public R<String> sendMsg(@RequestBody User user) {
//获取手机号
String phone = user.getPhone();
if (StringUtils.isNoneEmpty(phone)) {
//生成随机的4位验证码,并且在生成验证码的同时以phone为key,存储了验证码code
String code = validateCodeUtils.generateValidateCode(4,phone);
log.info("验证码,code为:{}",code);
//调用阿里云提供的短信API完成发送短信
//SMSUtils.sendMessage("?","?",phone,code);
return R.success("手机验证码短信发送成功");
}
return R.error("短信发送失败!!!");
}
因为这块不涉及到与数据库持久层的操作,也没有复杂的业务逻辑,因此直接在控制层进行方法调用并返回即可,SMSUtils.sendMessage这个方法就看各位小伙伴自己的条件了,如果有条件的在申请好签名以及模板后,将签名和模板分别填入即可。
我们前面修改了ValidateCodeUtils类中的generateValidateCode方法,添加了一个String类型的phone参数就是为了就是为了以phone为key,验证码code为vlue来存储到redis中的。
从下图我们可以看到里面存在了已手机号19716415316为key存储的数据了,并且其验证码code的值能正常保存,之后我们将其取出并对比即可。
如图当我们输入手机和验证码后,前端页面发送的url请求为:http://localhost/user/login,并且请求的参数为json形式,里头有phone和code两个参数。
因为黑马给的前端资料有些问题,这块可能有的小伙伴只有电话号码phone这个参数,并没有code这个参数,我们需要去static/front/page/login.html文件中78行左右添加或修改成这两段代码。
// 将手机号保存到浏览器的session
sessionStorage.setItem("userPhone",this.form.phone)
// 将移动端用户信息保存到浏览器的本地存储空间
localStorage.setItem('user',JSON.stringify(res.data))
Controller层
因为前端发送的url请求中包含了phone和code两个参数,如果光用User类型的参数来接收是无法接收的,因为User实体类中没有定义code,因此这里我们可以有两个方法:
这里笔者选择的是和黑马的一样选择了使用Map对象来接收。
/**
* 移动端登录
* @param map
* @return
*/
@PostMapping("/login")
public R<User> login(@RequestBody Map<String,String> map, HttpSession session) {
log.info("用户登录提交的手机号和验证码为:{}",map);
//获取手机号
String phone = map.get("phone");
//获取验证码
String code = map.get("code");
//从redis中获取保存的验证码
String cacheCode = validateCodeUtils.get(phone);
log.info("手机号:{},在redis中的验证码为:{}",phone,cacheCode);
if (cacheCode == null) {
return R.error("验证码已过期!!!");
}
if (code.equals(cacheCode)) {
//如果能够比对成功,说明登录成功
//判断当前手机号对应的用户是否为新用户,如果是新用户就自动完成注册
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
lqw.eq(User::getPhone, phone);
User user = userService.getOne(lqw);
if (user == null) {
user = new User();
user.setPhone(phone);
userService.save(user);
}
//将用户id保存到session中,供过滤器检测用户是否登录
session.setAttribute("userId", user.getId());
return R.success(user);
}
return R.error("登录失败,验证码错误!!!");
}
在里面进行登录的时候我们添加了一些业务判断,如果当前手机号不在数据库中user表中有存储,那么说明我们这个用户没有注册过,那么我们就帮其自动注册一个账户。
前面我们使用的是使用阿里云,短信服务的方式来发送验证码短信来获取验证码进而进行登录的,这个服务是需要花钱的,因此我们可以换一个方式,如果学习过springboot的小伙伴应该知道springboot整合了邮件的发送功能。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-mailartifactId>
dependency>
在yml文件中设置必要的配置信息,这个是必须配置的
spring:
#设置邮箱
mail:
# 邮件传输的协议以及供应商的名称
host: smtp.163.com
# 邮箱号(用来发送验证码的邮箱号),填写自己的邮箱
username:
# 密钥,填写自己的密钥
password:
这里的密钥字不是咱们邮箱的密码,而是在邮箱中设置开启POP3/SMTP/IMAP服务后获取的密钥,将获取的密钥自己保存一份,这个密钥的作用就是可以从第三方平台访问使用邮箱的钥匙。
选择开启容易一项即可,然后发送短信给它,之后页面就会反馈一个密钥给你了,这个密钥只显示一次,所以要记得保存。
再再yml文件中最下方自定义一些我们自己到时要发送邮箱时的一些属性。
#自定义的邮箱数据
myMail:
#发送人(我们自己的邮箱),此处替换成自己的就可以
from: ************@163.com
#接收人,此处替换成自己的就可以
to: **********@qq.com
#标题
subject: 验证码
在Utils文件夹下创建我们的发送邮箱的工具类SendMailUtils
/**
* @Author HuaiShuShu
* @Date 2023/5/23 15:49
* @PackageName:com.itheima.service.impl
* @Description: 发送邮件的工具类
* @Version 1.0
*/
@Component
public class SendMailUtils {
@Autowired
private JavaMailSender javaMailSender;
//发送人(我们自己的邮箱)
@Value("${myMail.from}")
private String from;
//接收人
@Value("${myMail.to}")
private String to;
//标题
@Value("${myMail.subject}")
private String subject;
public void sendMail(String code) {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(from+"(吃饱饱坏蜀黍)");
message.setTo(to);
message.setSubject(subject);
message.setText(code);
javaMailSender.send(message);
}
}
在UserController.sendMsg()方法中使用该方法,将生成的验证码传入到该方法中调用,之后邮箱就会调用该方法发送验证码。
/**
* 移动端通过手机号获取验证码
* @param user
* @return
*/
@PostMapping("/sendMsg")
public R<String> sendMsg(@RequestBody User user) {
//获取手机号
String phone = user.getPhone();
if (StringUtils.isNoneEmpty(phone)) {
//生成随机的4位验证码,并且在生成验证码的同时以phone为key,存储了验证码code
String code = validateCodeUtils.generateValidateCode(4,phone);
log.info("验证码,code为:{}",code);
//使用邮箱发送验证码登录
sendMailUtils.sendMail(code);
return R.success("手机验证码短信发送成功");
}
return R.error("短信发送失败!!!");
}
页面用户退出所发送的url请求。
因为用户退出的业务逻辑比较简单,首先因为我们在过滤器中判断用户是否已经登录,依据的是用户的id是否有保存在session当中,而userId是我们在用户登录的时候保存进session当中的,因此我们要实现用户退出,删除掉session当中的userId即可。
Controller层
/**
* 用户退出
* @param session
* @return
*/
@PostMapping("loginout")
public R<String> loginout(HttpSession session) {
//删除session中保存的用户id
session.removeAttribute("userId");
return R.success("成功退出登录");
}
输入手机号,获得验证码
输入验证码
点击登录
地址,指的是移动端消费者用户的地址信息,用户登录成功后可以维护自己的地址信息。同一个用户可以有多个地址信息,但是只能有一个默认地址。
用户的地址信息会存储在address book表,即地址簿表中。具体表结构如下:
功能代码清单:
和笔者一样通过MP的代码生成器生成的项目结构的小伙伴,可以直接有这些类和接口了,但是要在实体类中的公共字段部分添加@TableField,以及在is_delete属性上添加@TableLogic注解来实现逻辑删除,还需要把isDefault属性的类型改为Integer不然会影响后面地址在前端页面的显示,并且在之后的AddressBookController 中修改成@RequestMapping(“/addressBook”)。
黑马提供的资料当中是缺少这些类的,但是可以在源码的day6中解压压缩包获取里面的类,也可以通过我我分享的网盘来获取,我的资料都是经过补全的,当然也可以直接复制下面的代码来生成,其他的控制层、业务层、持久层都是和前面的结构一样,大家自行创建就可以了。
/**
*
* 地址管理
*
*
* @author ChiBaoBaoHuaiShuShu
* @since 2023-05-28
*/
@TableName("address_book")
@Data
public class AddressBook implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
private Long id;
/**
* 用户id
*/
private Long userId;
/**
* 收货人
*/
private String consignee;
/**
* 性别 0 女 1 男
*/
private Integer sex;
/**
* 手机号
*/
private String phone;
/**
* 省级区划编号
*/
private String provinceCode;
/**
* 省级名称
*/
private String provinceName;
/**
* 市级区划编号
*/
private String cityCode;
/**
* 市级名称
*/
private String cityName;
/**
* 区级区划编号
*/
private String districtCode;
/**
* 区级名称
*/
private String districtName;
/**
* 详细地址
*/
private String detail;
/**
* 标签
*/
private String label;
/**
* 默认 0 否 1是
*/
private Integer isDefault;
/**
* 创建时间
*/
@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;
/**
* 是否删除
*/
@TableLogic
private Integer isDeleted;
}
进入地址簿
当我们进入地址簿管理页面后,点击添加收货地址并后填写数据点击保存后,前端页面会发送url以及其携带的参数。
又需求分析后,我们可以知道当我们填写好数据后并点击保存地址时,会发送一个json类型的参数,并且我们可以发现参数中的所有属性均是AddressBook实体类中的属性,所以我们可以在Controller层定义一个控制方法其参数为AddressBook类型来接收前端传递的url。
Controller层
/**
*
* 地址管理 前端控制器
*
*
* @author ChiBaoBaoHuaiShuShu
* @since 2023-05-28
*/
@RestController
@RequestMapping("/addressBook")
public class AddressBookController {
@Autowired
private IAddressBookService addressBookService;
/**
* 添加收货地址
* @param addressBook
* @return
*/
@PostMapping
public R<String> addressBook(@RequestBody AddressBook addressBook) {
addressBookService.addressBook(addressBook);
return R.success("新增地址成功");
}
}
Service接口
/**
*
* 地址管理 服务类
*
*
* @author ChiBaoBaoHuaiShuShu
* @since 2023-05-28
*/
public interface IAddressBookService extends IService<AddressBook> {
/**
* 添加收货地址
* @param addressBook
* @return
*/
void addressBook(AddressBook addressBook);
}
Service实现类
/**
*
* 地址管理 服务实现类
*
*
* @author ChiBaoBaoHuaiShuShu
* @since 2023-05-28
*/
@Service
@Slf4j
public class AddressBookServiceImpl extends ServiceImpl<AddressBookMapper, AddressBook> implements IAddressBookService {
@Autowired
private AddressBookMapper addressBookMapper;
/**
* 添加收货地址
* @param addressBook
* @return
*/
@Override
public void addressBook(AddressBook addressBook) {
addressBook.setUserId(BaseContext.getCurrentId());
int insert = addressBookMapper.insert(addressBook);
if (insert <= 0) {
throw new CustomException("添加新地址失败");
}
log.info("用户新增收货地址:{}",addressBook);
}
}
实现类这里要注意一下,因为从前端页面传递回来的参数中是没有userId的,所以我们要是使用之前定义的前面线程工程类,来给AddressBook变量赋值userId的值,实现逻辑和我们前面获取存储员工模块的功能一样。
填好数据并点击保存地址时,数据库中该表能够正常添加数据并保存。
当我们点击地址管理后,地址管理页面首先会发送url请求,来向服务器获取当前用户的所有地址并显示到当前页面中。
虽然url中没有携带参数,但是我们在控制方法中还是定义一个AddressBook对象的参数,为我们以后做扩展可以用,比如模糊查询地址或者手机号什么的来查询地址。
Controller层
/**
* 查询当前用户的所有地址
* @param addressBook
* @return
*/
@GetMapping("/list")
public R<List<AddressBook>> list(AddressBook addressBook) {
log.info("查询当前用户的所有地址,其id为:{}",BaseContext.getCurrentId());
List<AddressBook> addressBooks = addressBookService.list(addressBook);
return R.success(addressBooks);
}
Service接口
/**
* 查询当前用户的所有地址
* @param addressBook
* @return
*/
List<AddressBook> list(AddressBook addressBook);
Service实现类
/**
* 查询当前用户的所有地址
* @param addressBook
* @return
*/
@Override
public List<AddressBook> list(AddressBook addressBook) {
LambdaQueryWrapper<AddressBook> lqw = new LambdaQueryWrapper<>();
//根据当前的用户id来查询
lqw.eq(AddressBook::getUserId, BaseContext.getCurrentId());
lqw.orderByDesc(AddressBook::getUpdateTime);
List<AddressBook> addressBooks = addressBookMapper.selectList(lqw);
return addressBooks;
}
这里的实现类是根据当前登录用户的id来进行查询的,并且将查询的结果进行了降序排序。
当我们点击修改按钮进去到修改地址页面后,会有如下两次请求:
页面(front/page/address-edit.html)发送ajax请求,将当前地址的id传递给服务器,服务端接的控制方法接收到请求并处理后将当前地址的数据返回给前端页面。
当我们修改好数据后,前端页面发送ajax请求,将填写好的数据已json的形式发送给服务器,后端的控制方法接收到后,将其更新到数据库中。
根据第一次页面的url请求,我们可以发现其是将地址的id保存到了请求路径当中,这个需要我们在控制方法中使用占位符和
Controller层
/**
* 根据id来查询地址
* 在修改地址页面中起到数据回显的作用
* @param id
* @return
*/
@GetMapping("{id}")
private R<AddressBook> get(@PathVariable("id") Long id) {
AddressBook addressBook = addressBookService.getById(id);
if (addressBook != null) {
return R.success(addressBook);
}else {
return R.error("没有这个地址id");
}
}
因为数据回显较为简单,因此我们就直接写在控制层就可以了。
通过需求分析我们可以知道其参数类型和添加地址时是一样的,因此我们还是定义一个AddressBook对象的参数来接收这些url当中的json参数,之后我们在将其更新到数据库即可,因为其用户id是不变,所以其控制方法也简单,直接根据实体类的id来更新修改即可。
Controller层
/**
* 修改地址
* @param addressBook
* @return
*/
@PutMapping
public R<String> update(@RequestBody AddressBook addressBook) {
boolean flag = addressBookService.updateById(addressBook);
if (flag) {
return R.success("修改地址成");
} else {
return R.error("修改地址失败!!!");
}
}
地址修改页面的数据回显成功
修改地址成功
当我们在地址管理页面中点击设置成默认地址后,前端页面会发送ajax请求,其url请求如下图所示。
由需求分析可知,我们需要在控制层Controller中定义控制方法来接收该请求所携带的地址id,并根据该地址id来修改默认地址。
Controller层
/**
* 修改默认地址
* @param addressBook
* @return
*/
@PutMapping("/default")
public R<String> setDefault(@RequestBody AddressBook addressBook) {
addressBookService.setDefault(addressBook);
return R.success("设置默认地址成功");
}
Service接口
/**
* 修改默认地址
* @param addressBook
* @return
*/
void setDefault(AddressBook addressBook);
Service实现类
/**
* 修改默认地址
* @param addressBook
* @return
*/
@Transactional
@Override
public void setDefault(AddressBook addressBook) {
//获取当前用户id
addressBook.setUserId(BaseContext.getCurrentId());
//先将当前用户的其他的地址都设置成不是默认的地址
LambdaUpdateWrapper<AddressBook> luw = new LambdaUpdateWrapper<>();
luw.eq(AddressBook::getUserId, addressBook.getUserId());
luw.set(AddressBook::getIsDefault, 0);
addressBookMapper.update(null, luw);
//在将用户选择的当前地址id的地址设置成默认地址
addressBook.setIsDefault(1);
addressBookMapper.updateById(addressBook);
}
实现类中我们需要先获取到当前登录用户的id,在这之后实现该业功能的思路是可以先将当前用户的所有地址都设置为不是默认的地址先,之后在单独根据地址的id来设置地址的默认状态将其修改为默认地址。
用户登录成功后跳转到系统首页,在首页需要根据分类来展示菜品和套餐。如果菜品设置了口味信息,需要展示 选择规格 按钮,否则显示 + 按钮,当我们点击不同的分类信息,那么就是展示不同分类中的菜品信息,并且当我们点击选择规格后,显示选择口味窗口,并且点击在套餐的页面中点击套餐的图,可以查看套餐中的菜品信息。
在开发代码之前,需要梳理一下前端页面和服务端的交互过程:
因为获取分类数据的控制方法我们在前面的菜品添加业务的时候就已经完成了,这块我们就不用再去写了,而之所以我们登录进去后空白什么都不显示,是因为前端代码index.html的298行代码处,它同时调用了两个js的方法,并且要两个方法都成功相应它才会渲染页面。
获取分类的url请求:
获取购物车内商品信息的url:
因此我们可以先暂时的修改前端的代码,稍微修改一下main.js中的获取购物车内商品的集合的url请求,修改成31行,并启用,把30行注释掉,让cartListApi(data)这个方法去访问我们front下的一个cartData.json文件,这个文件黑马给的资料中没有,但是可以在其源码中获取,也可以在我提供给各位的资料中获取,或者在复制下面的代码自己创建一个cartData.json文件也是可以的。
cartData.json文件
{"code":1,"msg":null,"data":[],"map":{}}
此处的json文件就只是返回一个假数据,我们写死的,让我们在写菜品展示模块时方便调试和开发而已,之后还是会把main.js中的31行的代码注释掉恢复30行代码,恢复原样来开发购物车模块的。
有的小伙伴修改了main.js和添加了cartData.json文件文件移动端页面首页还是不显示,这时候我们可以看一下移动端发送的请求是不是还是http://localhost/shoppingCart/list,如果还是那么我们需要清理一下浏览器的记录,因为main.js加载在了浏览器中,我们在项目中修改了并没有加载到浏览器,所以清理一下浏览器记录就可以了。
因为我们我们的菜品大多是设置有口味信息的,所以菜品信息那不应该只显示一个 + 号,而是应该显示选择规格这个按钮的,这是因为我们的前面带菜品菜品控制器中实现的时候,返回的类型是List,因为Dish实体类中并没有定义口味的属性,所以它没法显示,所以这块是我们需要修改的,我们可以将其改为DishDto,因为Dto中继承了dish的基本属性,并且还有DishFlavor口味的属性信息。
因此我们需要先注释掉我们前面在Dish控制器、接口、实现类中写的list方法,并重新来编辑代码来实现即可,我们知道要使用DishDto后我们就可以大致来完成了。
Controller层
/**
* 根据条件来查询菜品信息
* 适用于前台和后台进行菜品展示
* @param dish
* @return
*/
@GetMapping("/list")
public R<List<DishDto>> list(Dish dish) {
List<DishDto> dishDtoList = dishService.listByCategoryId(dish);
return R.success(dishDtoList);
}
Service接口
/**
* 根据条件来查询菜品信息
* 适用于前台和后台进行菜品展示
* @param dish
* @return
*/
List<DishDto> listByCategoryId(Dish dish);
Service实现类
/**
* 根据条件来查询菜品信息
* 适用于前台和后台进行菜品展示
* @param dish
* @return
*/
@Override
public List<DishDto> 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);
//这块用的是stream().map(字节流加lmabe)
List<DishDto> dishDtoList = dishList.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);
}
//先查询到当前菜品的口味信息
Long dishId = itme.getId();
LambdaQueryWrapper<DishFlavor> dishDtoLambdaQueryWrapper = new LambdaQueryWrapper<>();
dishDtoLambdaQueryWrapper.eq(DishFlavor::getDishId, dishId);
List<DishFlavor> dishFlavorList = dishFlavorService.list(dishDtoLambdaQueryWrapper);
//将当前菜品的口味信息赋值给Dto中的口味集合。
dishDto.setFlavors(dishFlavorList);
return dishDto;
}).collect(Collectors.toList());
return dishDtoList;
}
此处实现类中前面查询到list部分都是和之前一样的,但是之后我们要想办法将dishList赋值给给dishDtoList,因为我们最后要返回的时List,而这块的实现方式我们前面其实已经写过很多了,比如我们在之前的菜品分页查询中,我们要想办法在后台的菜品管理页面中展示菜品的同时显示菜品的分类名称,所以我们可以参考这部分的代码:
我们也不用担心前台页面实现了菜品展示,后台的菜品管理页面是否会收到影响,这个是不用担心的,因为dishDto继承了dish,所以在菜品页面中需要展示的属性信息,dishDto中都有,所以不会有影响。
前台菜品展示
后台菜品管理页面
下图是前台中套餐显示菜品的的url其中携带了两个参数一个是分类信息的id(categoryId)一个是套餐的启用状态status,这个list中我们的SetmealController控制器类中并没有定义实现,因此我们需要定义一个控制方法去响应该url请求。
注意:url请求中携带的两个参数并不是json形式的,因此不可用@RequestBody注解。
Controller层
/**
* 展示前台的套餐中的所有套餐信息以及套餐中对应的菜品信息。
* 此处返回值类型是List也是可以的,但是我写成这样的目的是因为方便以后扩展做准备,套餐管理页面显示所有套餐信息以及套餐所对应的菜品信息。
* @param setmeal
* @return
*/
@GetMapping("/list")
public R<List<SetmealDto>> list(Setmeal setmeal) {
log.info("根据条件来展示前台的套餐中的所有套餐信息以及套餐中对应的菜品信息,条件为:{}",setmeal);
List<SetmealDto> setmealDtoList = setmealService.listSetmealDto(setmeal);
return R.success(setmealDtoList);
}
Service接口
/**
* 展示前台的套餐中的所有套餐信息以及套餐中对应的菜品信息。
* 此处返回值类型是List也是可以的,但是我写成这样的目的是因为方便以后扩展做准备,套餐管理页面显示所有套餐信息以及套餐所对应的菜品信息。
* @param setmeal
* @return
*/
List<SetmealDto> listSetmealDto(Setmeal setmeal);
Service实现类
/**
* 展示前台的套餐中的所有套餐信息以及套餐中对应的菜品信息。
* 此处返回值类型是List也是可以的,但是我写成这样的目的是因为方便以后扩展做准备,套餐管理页面显示所有套餐信息以及套餐所对应的菜品信息。
* @param setmeal
* @return
*/
@Override
public List<SetmealDto> listSetmealDto(Setmeal setmeal) {
//先根据分类id来查询套餐的基本信息
LambdaQueryWrapper<Setmeal> lqw = new LambdaQueryWrapper<>();
lqw.eq(Setmeal::getCategoryId, setmeal.getCategoryId());
lqw.eq(Setmeal::getStatus, setmeal.getStatus());
List<Setmeal> setmealList = setmealMapper.selectList(lqw);
//开始为SetmealDto赋值
List<SetmealDto> setmealDtoList = setmealList.stream().map(item -> {
SetmealDto setmealDto = new SetmealDto();
//将Setmeal的基本信息拷贝给setmealDto
BeanUtils.copyProperties(item, setmealDto);
//根据套餐的id来查询套餐中菜品的信息,并将其赋值给SetmealDto中的List setmealDishes
Long setmealId = item.getId();
LambdaQueryWrapper<SetmealDish> setmealDishLambdaQueryWrapper = new LambdaQueryWrapper<>();
setmealDishLambdaQueryWrapper.eq(SetmealDish::getSetmealId, setmealId);
List<SetmealDish> setmealDishList = setmealDishService.list(setmealDishLambdaQueryWrapper);
setmealDto.setSetmealDishes(setmealDishList);
return setmealDto;
}).collect(Collectors.toList());
return setmealDtoList;
}
我们实现类返回的数据类型是List的类型,这块笔者一开始思考的套餐信息的展示中可能会涉及到套餐中菜品信息的展示,因此我在实现类中为SetmealDto对象赋值并返回对应的数据,虽然这块的套餐的展示没有涉及到菜品的信息展示,但是可以保留这块实现类,为后期的扩展做准备,比如在套餐管理页面中显示每个套餐关联的菜品信息。
当我们在前台点击套餐图片时,前端页面会发送以下的请求,我们可以看到其传输的路径中有一个套餐的id,因此我们在Controller层中应该定义使用占位符来接收这个id来响应其的需求。
这里我们还需要分析一下我们定义方法后,返回给前端的是什么类型的数据,我们可以通过以下步骤:
因此我们确定了返回DishDto的数据先,但是又可以看到185行有一个list,说明需要遍历集合的,进而可以确定Controller层返回的是一个List类型的数据了。
Controller层
/**
* 前台查看套餐信息中的菜品信息
* 根据套餐id来查询菜品的信息
* @param setmealId
* @return
*/
@GetMapping("/dish/{SetmealId}")
public R<List<DishDto>> getListDishDtoById(@PathVariable("SetmealId") Long setmealId) {
log.info("根据套餐id来查询菜品的信息,ID为:{}",setmealId);
List<DishDto> dishDtoList = setmealService.getListDishDtoById(setmealId);
return R.success(dishDtoList);
}
Service接口
/**
* 前台查看套餐信息中的菜品信息
* 根据套餐id来查询菜品的信息
* @param setmealId
* @return
*/
List<DishDto> getListDishDtoById(Long setmealId);
Service实现类
/**
* 前台查看套餐信息中的菜品信息
* 根据套餐id来查询菜品的信息
* @param setmealId
* @return
*/
@Override
public List<DishDto> getListDishDtoById(Long setmealId) {
//查询当前套餐中所对应的菜品信息(setmeal_dish表)
LambdaQueryWrapper<SetmealDish> lqw = new LambdaQueryWrapper<>();
lqw.eq(SetmealDish::getSetmealId, setmealId);
List<SetmealDish> setmealDishList = setmealDishService.list(lqw);
List<DishDto> dishDtoList = setmealDishList.stream().map(item -> {
DishDto dishDto = new DishDto();
//通过菜品id查询基本的菜品信息
String dishId = item.getDishId();
Dish dish = dishService.getById(dishId);
BeanUtils.copyProperties(dish, dishDto);
//将套餐中菜品所对应的份数赋值给Dto
dishDto.setCopies(item.getCopies());
//查询菜品所对应的口味信息并赋值给Dto
LambdaQueryWrapper<DishFlavor> dishFlavorLambdaQueryWrapper = new LambdaQueryWrapper<>();
dishFlavorLambdaQueryWrapper.eq(DishFlavor::getDishId, dishId);
List<DishFlavor> dishFlavorList = dishFlavorService.list(dishFlavorLambdaQueryWrapper);
dishDto.setFlavors(dishFlavorList);
return dishDto;
}).collect(Collectors.toList());
return dishDtoList;
}
实现类的业务逻辑是我们通过传递的套餐id去setmeal_dish表中查找菜品的list集合信息,之后通过遍历该集合,并在集合中通过菜品id去Dish表中获取Dish对象,并将Dish对象赋值给DishDto,之后在通过菜品id来查询菜品所对应的口味信息,之后将口味信息也赋值给Dto。
移动端用户可以将菜品或者套餐添加到购物车。对于菜品来说,如果设置了口味信息,则需要选择规格后才能加入购物车;对于套餐来说,可以直接点击 + 将当前套餐加入购物车。在购物车中可以修改菜品和套餐的数量也可以清空购物车。
购物车对应的数据表为shopping_cart表,具体表结构如下:
在开发代码之前,需要梳理一下购物车操作时前端页面和服务端的交互过程:
开发购物车功能,其实就是在服务端编写代码去处理前端页面发送的这3次请求即可
在开发业务功能前,先将需要用到的类和接口基本结构创建好:
使用MP代码生成器的小伙伴将shoppingCartController的@RequestMapping修改成@RequestMapping(/shoppingCart)即可。
如果是自己手敲项目结构的,那么其他层的类按照之前的形式创建即可。
如需求分析中分析,当我们点击加入购物车或者点击 + 号将菜品、套餐加入购物车时,页面发送的url请求的结构都相同,唯一不同的就是里面的参数而已。
菜品:
套餐:
可以看到二者请求响应的控制方法时一样的,只是菜品的请求参数中又口味和菜品id,套餐中有套餐id,只是参数不同罢了,因此我们都可以使用ShoppingCart的形参来接收。
Controller层
/**
* 添加购物车
* @param shoppingCart
* @return
*/
@PostMapping("/add")
public R<ShoppingCart> add(@RequestBody ShoppingCart shoppingCart) {
ShoppingCart cart = shoppingCartService.add(shoppingCart);
return R.success(cart);
}
Service接口
/**
* 添加购物车
* @param shoppingCart
* @return
*/
ShoppingCart add(ShoppingCart shoppingCart);
Service实现类
/**
*
* 购物车 服务实现类
*
*
* @author ChiBaoBaoHuaiShuShu
* @since 2023-05-28
*/
@Slf4j
@Service
public class ShoppingCartServiceImpl extends ServiceImpl<ShoppingCartMapper, ShoppingCart> implements IShoppingCartService {
@Autowired
private ShoppingCartMapper shoppingCartMapper;
/**
* 存储上一个菜品的口味,
* 用来辅助菜品添加购物车,
* 实现连续添加同一口味的菜品
*/
private String dishFlavor;
/**
* 添加购物车
* @param shoppingCart
* @return
*/
@Override
public ShoppingCart add(ShoppingCart shoppingCart) {
//获取当前用户的id,并为其赋值
Long userId = BaseContext.getCurrentId();
shoppingCart.setUserId(userId);
//设置创建时间为当前时间
shoppingCart.setCreateTime(LocalDateTime.now());
//当时选择规格按钮添加菜品的时候,则必然会有口味,则这时更新口味
if (shoppingCart.getDishFlavor() != null) {
dishFlavor = shoppingCart.getDishFlavor();
}
//如果当前加入购物车的是个菜品,并且没有口味,说明目前调用的是连续添加同一口味的菜品
if (shoppingCart.getDishId() != null && shoppingCart.getDishFlavor() == null) {
//将选择规格时选择的口味赋值给它
shoppingCart.setDishFlavor(dishFlavor);
}
//添加之前先查询当前口味的菜品或套餐是否存在于购物车
LambdaQueryWrapper<ShoppingCart> lqw = new LambdaQueryWrapper<>();
lqw.eq(shoppingCart.getSetmealId() != null, ShoppingCart::getSetmealId, shoppingCart.getSetmealId());
lqw.eq(shoppingCart.getDishId() != null, ShoppingCart::getDishId, shoppingCart.getDishId());
lqw.eq(ShoppingCart::getUserId, userId);
lqw.eq(StringUtils.isNoneEmpty(shoppingCart.getDishFlavor()), ShoppingCart::getDishFlavor, shoppingCart.getDishFlavor());
ShoppingCart cart = shoppingCartMapper.selectOne(lqw);
//判断是否存在购物车
if (cart != null) {
//存在购物车则数量+1
Integer number = cart.getNumber();
cart.setNumber(number + 1);
shoppingCartMapper.updateById(cart);
} else {
//不存在购物车,则将商品的数量设置为1
shoppingCart.setNumber(1);
shoppingCartMapper.insert(shoppingCart);
cart = shoppingCart;
}
return cart;
}
}
这处实现类,我们的核心业务逻辑是判断添加的商品它是否存在,而判断是否存在的依据是:
因此整体的业务逻辑:
要实现我们的查看购物车商品之前要去main.js文件中将31行注释掉,恢复30行的url,这是我们前面在实现前台菜品展示时修改了这部分的url请求,现在我们要实现购物车的商品展示了所以要恢复这块内容了,修改了js文件之后要记得清除以下浏览器的缓存,不然刚修改的js不易生效。
当我们进去到移动端页面时或者点击购物车图标时,其会发送以下的url请求给服务器来获取购物车内的商品信息,因此我们只需要在控制层实现控制方法来响应其即可。
Controller层
/**
* 查询当前用户购物车内的商品信息
* @return
*/
@GetMapping("list")
public R<List<ShoppingCart>> list() {
log.info("查询id为:{} 的用户购物车内的商品信息",BaseContext.getCurrentId());
List<ShoppingCart> shoppingCartList = shoppingCartService.listShoppingCart();
return R.success(shoppingCartList);
}
Service接口
/**
* 查询当前用户购物车内的商品信息
* @return
*/
List<ShoppingCart> listShoppingCart();
Service实现类
/**
* 查询当前用户购物车内的商品信息
* @return
*/
@Override
public List<ShoppingCart> listShoppingCart() {
//获取当前用户的id
Long userId = BaseContext.getCurrentId();
LambdaQueryWrapper<ShoppingCart> lqw = new LambdaQueryWrapper<>();
lqw.eq(ShoppingCart::getUserId, userId);
//按添加时间来个降序
lqw.orderByDesc(ShoppingCart::getCreateTime);
List<ShoppingCart> shoppingCartList = shoppingCartMapper.selectList(lqw);
return shoppingCartList;
}
整体的实现逻辑比较简单,通过线程工具类来获取当前登录用户的id,之后通过MP的条件构造器来添加条件查询并返回值即可。
温馨提示
<div class="dishFlavor">{{item.dishFlavor}}div>
<div class="divAdd" v-else>
<div v-if="item.flavors == null">
<img src="./images/add.png" @click.prevent.stop="addCart(item)"/>
div>
<div v-else>
<img src="./images/add.png" @click.prevent.stop="chooseFlavorClick(item)"/>
div>
div>
这块功能的实现笔者这边遇到了困难,在点击 - 按钮时候前端发送url请求只有一个菜品id或者商品id,并没有携带口味这个属性,因为笔者前面实现的过程中考虑了口味这块的属性不同口味的菜品算作了一个新的商品加入购物车。
所以在减少购物车内菜品数量的时候需要传递口味和id一起后端才能知道要对哪个菜品进行减少更新操作,因此缺少了口味属性的参数,笔者是后端的,所以实在没想到什么好的解决方式。
用户下单业务对应的数据表为orders表和order detail表
orders: 订单表
order_detail: 订单明细表
移动端用户将菜品或者套餐加入购物车后,可以点击购物车中的 “去结算” 按钮,页面跳转到订单确认页面,点去 “去支付” 按钮则完成下单操作。
我们暂时只做到订单详情表这块了,因为支付功能的开发,需要我们去支付宝获取微信申请相关的资质的。
在开发代码之前,需要梳理一下用户下单操作时前端页面和服务端的交互过程
1、在购物车中点击 “去结算” 按钮,页面跳转到订单确认页面去结算。
2、在订单确认页面,发送ajax请求,请求服务端获取当前登录用户的默认地址。
3、在订单确认页面,发送aiax请求,请求服务端获取当前登录用户的购物车数据。
4、在订单确认页面点击 “去支付”按钮,发送ajax请求,请求服务端完成下单操作。
开发用户下单功能,其实就是在服务端编写代码去处理前端页面发送的请求即可
当我们点击去结算进入订单确认页面后,前端页面发送两个url请求:
因此我们在地址对应的控制层和业务层当中实现具体的控制方法,响应其url请求。
Controller层
/**
* 查询默认地址
*/
@GetMapping("default")
public R<AddressBook> getDefault() {
//获取登录用户的id
Long userId = BaseContext.getCurrentId();
LambdaQueryWrapper<AddressBook> lqw = new LambdaQueryWrapper<>();
lqw.eq(AddressBook::getUserId, userId);
lqw.eq(AddressBook::getIsDefault, 1);
AddressBook addressBook = addressBookService.getOne(lqw);
if (addressBook == null) {
return R.error("没有找到该对象");
} else {
return R.success(addressBook);
}
}
因为实现比较简单而且也没有什么参数,就是获取当前用户id,然后使用MP的条件构造器来设置用户id以及默认地址的条件,进行查询,之后进行判断该地址对象是否为空即可。
因此较为简单,而且不复杂,就直接写在了Controller层了。
在实现业务功能前,先将需要用到的类和接口基本结构创建好
使用MP的小伙伴代码生成器的小伙伴需要修改OrderDetailController的为@RequestMapping(“/orderDetail”)
当我们点击 “去支付” 按钮后,前端页面会发送一个url请求给服务器,并且里面携带有addressBookId(地址id)、payMethod(支付方式)、remark(备注),这些都是Orders实体类中的属性,因此我们在控制方法中可以使用Orders对象来接收这些参数。
Controller层
/**
*
* 订单表 前端控制器
*
*
* @author ChiBaoBaoHuaiShuShu
* @since 2023-05-28
*/
@Slf4j
@RestController
@RequestMapping("/order")
public class OrdersController {
@Autowired
private IOrdersService ordersService;
/**
* 用户下单
* 操作两张表,一个订单表orders,一张订单详情表order_detail
* @param orders
* @return
*/
@PostMapping("/submit")
public R<String> submit(@RequestBody Orders orders) {
log.info("添加订单:{}",orders);
ordersService.submit(orders);
return R.success("用户下单成功");
}
}
Service接口
/**
*
* 订单表 服务类
*
*
* @author ChiBaoBaoHuaiShuShu
* @since 2023-05-28
*/
public interface IOrdersService extends IService<Orders> {
/**
* 添加订单
* 操作两张表,一个订单表orders,一张订单详情表order_detail
* @param orders
* @return
*/
void submit(Orders orders);
}
Service实现类
/**
*
* 订单表 服务实现类
*
*
* @author ChiBaoBaoHuaiShuShu
* @since 2023-05-28
*/
@Service
public class OrdersServiceImpl extends ServiceImpl<OrdersMapper, Orders> implements IOrdersService {
@Autowired
private OrdersMapper ordersMapper;
@Autowired
private IOrderDetailService orderDetailService;
@Autowired
private IUserService userService;
@Autowired
private IAddressBookService addressBookService;
@Autowired
private IShoppingCartService shoppingCartService;
@Override
@Transactional
public void submit(Orders orders) {
//获取当前的用户信息
Long userId = BaseContext.getCurrentId();
User user = userService.getById(userId);
//获取当前的地址信息
Long addressBookId = orders.getAddressBookId();
AddressBook addressBook = addressBookService.getById(addressBookId);
if (addressBook == null) {
throw new CustomException("用户地址有误,无法下单,请检查地址是否正确!!!");
}
//获取登录用户购物车内的商品信息
LambdaQueryWrapper<ShoppingCart> lqw = new LambdaQueryWrapper<>();
lqw.eq(ShoppingCart::getUserId, userId);
List<ShoppingCart> shoppingCartList = shoppingCartService.list(lqw);
if (shoppingCartList == null && shoppingCartList.size() == 0) {
throw new CustomException("购物车为空,无法下单!!!");
}
//总金额,AtomicInteger类型不用担心线程的问题。
AtomicInteger amount = new AtomicInteger(0);
//订单号
long orderId = IdWorker.getId();
//为订单详情表的对象赋值,进而获得List,用来给订单详情表插入数据
List<OrderDetail> orderDetailList = shoppingCartList.stream().map(item -> {
OrderDetail orderDetail = new OrderDetail();
//订单号
orderDetail.setOrderId(orderId);
//商品数
orderDetail.setNumber(item.getNumber());
//商品口味
orderDetail.setDishFlavor(item.getDishFlavor());
//菜品id
orderDetail.setDishId(item.getDishId());
//套餐id
orderDetail.setSetmealId(item.getSetmealId());
//菜品或商品名称
orderDetail.setName(item.getName());
//商品图片
orderDetail.setImage(item.getImage());
//商品单价
orderDetail.setAmount(item.getAmount());
//计算总金额,multiply(作为为相乘)
amount.addAndGet(item.getAmount().multiply(new BigDecimal(item.getNumber())).intValue());
return orderDetail;
}).collect(Collectors.toList());
//为订单对象Orders orders赋值,为插入订单表做准备
orders.setId(orderId);
orders.setNumber(String.valueOf(orderId));
//创建时间
orders.setOrderTime(LocalDateTime.now());
//付款时间
orders.setCheckoutTime(LocalDateTime.now());
//支付状态
orders.setStatus(2);
//总金额
orders.setAmount(new BigDecimal(amount.get()));
//用户id(用户)
orders.setUserId(userId);
//用户名称
orders.setUserName(user.getName());
//收货人(地址)
orders.setConsignee(addressBook.getConsignee());
//收货人电话
orders.setPhone(addressBook.getPhone ());
//收货地址(省名称+市名称+区名称+详细地址)
orders.setAddress((addressBook.getProvinceName() == null ? "" : addressBook.getProvinceName ())
+ (addressBook.getCityName() == null ? "" : addressBook.getCityName())
+ (addressBook.getDistrictName () == null ? "" : addressBook.getDistrictName())
+(addressBook.getDetail() == null ? "" : addressBook.getDetail()));
//向订单表内插入数据,一条
ordersMapper.insert(orders);
//向订单详情表插入数据,多条
orderDetailService.saveBatch(orderDetailList);
//清空登录用户的购物车
LambdaQueryWrapper<ShoppingCart> shoppingCartLambdaQueryWrapper = new LambdaQueryWrapper<>();
shoppingCartLambdaQueryWrapper.eq(ShoppingCart::getUserId, BaseContext.getCurrentId());
shoppingCartService.remove(shoppingCartLambdaQueryWrapper);
}
}
这块实现类的代码较长,但是整体的业务逻辑并不复杂,总的可以分为以下几步:
总体逻辑并不复杂,就是比较繁琐,细心就基本可以完成了。
点击去支付后,跳转到下单成功界面。
当我们完成支付的时候可以点击查看订单、或者在用户首页中点击查看历史订单进入订单页面,又或者当我们进入用户首页的时候其会显示时间最新的一条订单信息,这时候可能就有小伙伴们说不知道咱们这个订单需要显示一些什么消息,后端该返回什么信息给前端,我们可以去到产品原型中找到对应的产品需求概述。
我们可以看到移动端中我们显示我的订单时,每条对应的订单中要显示订单中的详情信息,这些时OrderDetail实体类中的属性,而上面的订单时间、支付状态、总金额等是Order实体类的数据。
因此我们应该思考到我们需要使用到关于订单的Dto了。
在Dto文件夹中新建一个Dto的类,其命名为OrdersDto,其代码如下:
/**
* @Author HuaiShuShu
* @Date 2023/6/7 15:49
* @PackageName:com.huaishushu.dto
* @Description: 订单和订单详情中的菜品信息的显示
* @Version 1.0
*/
@Data
public class OrdersDto extends Orders {
private List<OrderDetail> orderDetails;
}
从中我们分析发现其调用的方法都是一样的,只是参数不同,一个是分页显示显示5条信息,一个是只显示一条。
因此我们在Controller层实现相关控制方法来响应其即可。
Controller层
/**
* 查询当前用户的订单,并进行分页
* @param page
* @param pageSize
* @return
*/
@GetMapping("/userPage")
public R<Page> userPage(Integer page, Integer pageSize) {
Page<OrdersDto> ordersDtoPage = ordersService.userPage(page, pageSize);
log.info("移动端拥有查看订单的分页信息,{}",ordersDtoPage.getRecords());
return R.success(ordersDtoPage);
}
Service接口
/**
* 查询当前用户的订单,并进行分页
* @param page
* @param pageSize
* @return
*/
Page<OrdersDto> userPage(Integer page, Integer pageSize);
Service实现类
/**
* 查询当前用户的订单,并进行分页
* @param page
* @param pageSize
* @return
*/
@Override
public Page<OrdersDto> userPage(Integer page, Integer pageSize) {
//获取当前用户的id
Long userId = BaseContext.getCurrentId();
//创建Orders的分页构造器
Page<Orders> ordersPage = new Page<>(page, pageSize);
LambdaQueryWrapper<Orders> lqw = new LambdaQueryWrapper<>();
lqw.eq(Orders::getUserId, userId);
//添加排序,使支付状态进度不高的排在前面,之后根据下单时间排序
lqw.orderByAsc(Orders::getStatus).orderByDesc(Orders::getOrderTime);
ordersMapper.selectPage(ordersPage, lqw);
//构建OrdersDto的分页构造器
Page<OrdersDto> ordersDtoPage = new Page<>();
//对象拷贝,但不拷贝“records”
BeanUtils.copyProperties(ordersPage, ordersDtoPage, "records");
List<Orders> ordersList = ordersPage.getRecords();
List<OrdersDto> ordersDtoList = ordersList.stream().map(item -> {
OrdersDto ordersDto = new OrdersDto();
//因为ordersDto是新new出来的,其属性为空值,因此需要为其赋值
BeanUtils.copyProperties(item, ordersDto);
//查询订单的详细信息并为其Dto赋值
LambdaQueryWrapper<OrderDetail> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(OrderDetail::getOrderId, item.getNumber());
List<OrderDetail> orderDetailList = orderDetailService.list(lambdaQueryWrapper);
ordersDto.setOrderDetails(orderDetailList);
return ordersDto;
}).collect(Collectors.toList());
ordersDtoPage.setRecords(ordersDtoList);
return ordersDtoPage;
}
实现类中的业务处理逻辑和之前后台菜品分页查询,分类信息分页查询那些逻辑差不多,整体的思路如下:
当我们在订单页面点击再来一单时,系统自动帮我们点击订单中的信息,加入购物车,并实现跳转到菜单页面。
当我们点击再来一单的时候,前端页面会发送以下的url,其中携带了订单的id。
因此我们需要根据这个订单id,来完成 “再来一单” 的业务逻辑开发。
Controller层
/**
* 再来一单
* @param orders
* @return
*/
@PostMapping("/again")
public R<String> again(@RequestBody Orders orders) {
log.info("再来一单的订单id:{}",orders.getId());
ordersService.again(orders);
return R.success("再来一单成功");
}
Service接口
/**
* 再来一单
* @param orders
* @return
*/
void again(Orders orders);
Service实现类
/**
* 再来一单
* @param orders
* @return
*/
@Override
@Transactional
public void again(Orders orders) {
//获取用户id
Long userId = BaseContext.getCurrentId();
//查询订单详情中的商品信息
LambdaQueryWrapper<OrderDetail> lqw = new LambdaQueryWrapper<>();
lqw.eq(OrderDetail::getOrderId, orders.getId());
List<OrderDetail> orderDetailList = orderDetailService.list(lqw);
if (orderDetailList == null || orderDetailList.size() == 0) {
throw new RuntimeException("当前订单有误,请刷新页面!!!");
}
List<ShoppingCart> shoppingCartList = orderDetailList.stream().map(item -> {
//构建购物车对象
ShoppingCart shoppingCart = new ShoppingCart();
//给购物车对象赋值
//菜品、套餐名称
shoppingCart.setName(item.getName());
//图片
shoppingCart.setImage(item.getImage());
//用户id
shoppingCart.setUserId(userId);
//菜品id
shoppingCart.setDishId(item.getDishId());
//套餐id
shoppingCart.setSetmealId(item.getSetmealId());
//口味
shoppingCart.setDishFlavor(item.getDishFlavor());
//数量
shoppingCart.setNumber(item.getNumber());
//金额
shoppingCart.setAmount(item.getAmount());
//创建时间
shoppingCart.setCreateTime(LocalDateTime.now());
return shoppingCart;
}).collect(Collectors.toList());
//将购物车对象插入到数据库中
shoppingCartService.saveBatch(shoppingCartList);
}
整体的业务逻辑比较简单,就是先根据订单id去查询订单详细表,之后得到List orderDetailList对象,之后遍历并给购物车ShoppingCart对象赋值就可以了,最后将steam得到的数据收集并赋值给List orderDetailList,最后将其批量插入数据库shopping_cart表即可。
我们可以从产品原型中查看到后台的订单明细的功能要求,以及数据显示,并且还要去能够根据订单号、日期等进行订单的查询。
我们进入订单明细管理页面后会发送url请求给服务器进行分页查询的显示,并且当我们输入订单号以及选择日期范围后会将订单号以及开始时间和终止时间作为参数一起传递给服务器。
因此我们只需要在Orders订单所对应的控制层和业务层中实现相关的方法来响应,因为其传递的传递的参数中有开始时间和终止时间,这两个参数在其他实体类中都是不存在的。
所以这块的控制方法我们可以直接在控制方法的形参部分定义同名的参数即可,这块的两个世界参数的类型定义为字符串的类型,订单号也是。
Controller层
/**
* 后台订单的分页查询
* @param page
* @param pageSize
* @param number
* @param beginTime 开始时间(下单)
* @param endTime 终止时间(下单)
* @return
*/
@GetMapping("/page")
public R<Page> page(Integer page, Integer pageSize,String number, String beginTime, String endTime) {
log.info("订单查询:开始时间和结束时间:{}·······{}", beginTime, endTime);
Page<Orders> ordersPage = ordersService.page(page, pageSize, number, beginTime, endTime);
return R.success(ordersPage);
}
Service接口
/**
* 后台订单的分页查询
* @param page
* @param pageSize
* @param number
* @param beginTime 开始时间(下单)
* @param endTime 终止时间(下单)
* @return
*/
Page<Orders> page(Integer page, Integer pageSize,String number, String beginTime, String endTime);
Service实现类
/**
* 后台订单的分页查询
* @param page
* @param pageSize
* @param number
* @param beginTime 开始时间(下单)
* @param endTime 终止时间(下单)
* @return
*/
@Override
public Page<Orders> page(Integer page, Integer pageSize,String number, String beginTime, String endTime) {
//条件查询构造器
LambdaQueryWrapper<Orders> lqw = new LambdaQueryWrapper<>();
lqw.between(StringUtils.isNotEmpty(beginTime) && StringUtils.isNotEmpty(endTime), Orders::getOrderTime, beginTime, beginTime);
lqw.like(StringUtils.isNotEmpty(number), Orders::getNumber, number);
//分页查询构造器
Page<Orders> ordersPage = new Page<>(page, pageSize);
ordersMapper.selectPage(ordersPage, lqw);
return ordersPage;
}
当我们点击派送、完成后,并点击确认后,前端页面发送url请求给服务器,其中携带了两个参数一个是订单id和状态status的参数,这应为修改后的状态了。
因为url中携带的两个参数均在order实体类中有定义,因此我们可以使用Order对象来作为参数来接收响应。
因为整体的业务实现过程比较简单,就是单纯的根据订单id来修改订单的状态,因此我就写在控制层了。
Controller层
/**
* 修改订单状态
* @param orders
* @return
*/
@PutMapping
public R<String> update(@RequestBody Orders orders) {
//log.info("修改订单状态:{}", orders);
LambdaUpdateWrapper<Orders> luw = new LambdaUpdateWrapper<>();
luw.set(Orders::getStatus, orders.getStatus());
luw.eq(Orders::getNumber, orders.getId());
boolean flag = ordersService.update(luw);
if (flag) {
return R.success("修改成功");
}
return R.error("修改订单状态失败!!!");
}
有的小伙伴在项目开发过程中会遇到乱码问题,这个情况我们只需要在yml文件中添加字符编码的设置即可(UTF-8)
server:
port: 80
servlet:
# 实现编码统一
encoding:
charset: utf-8
enabled: true
force: true
在这处业务开发中,有的小伙伴可能会遇到以下的报错提示,这里的报错信息显示:
不鼓励依赖循环引用,默认情况下禁止使用循环引用。更新应用程序以删除 Bean 之间的依赖循环。作为最后的手段,可以通过将spring.main.allow-circular-references设置为true来自动打破循环。
前面的报错信息也提示我们了,所以我们在yml文件中以下属性即可
spring:
main:
allow-circular-references: true