一、项目概述
新手入门的SpringBoot+SSM企业级项目
1、软件开发整体介绍
1.1软件开发流程
我们一般在编码层
1.2角色分层
1.3软件环境
①开发环境(development):开发人员在开发阶段使用的环境,一般外部用户无法访问
②测试环境(testing):专门给测试人员使用的环境,用于测试项目,
一般外部用户无法访问
③生产环境(production):即线上环境,正式提供对外服务的环境
2、瑞吉外卖项目介绍
本项目共分为3期进行开发:
①第一期主要实现基本需求,其中移动端应用通过H5实现,用户可以通过手机浏览器访问。
②第二期主要针对移动端应用进行改进,使用微信小程序实现,用户使用起来更加方便。
③第三期主要针对系统进行优化升级,提高系统的访问性能。
2.1产品原型展示
①产品经理在需求分析后制作,一般都是网页。
②产品原型主要用于展示项目的功能,并不是最终的页面效果。
2.2技术选型
2.3功能架构
2.4角色
后台系统管理员:登录后台管理系统,拥有后台系统中的所有操作权限
后台系统普通员工:登录后台管理系统,对菜品、套餐、订单等进行管理
C端用户:登录移动端应用,可以浏览菜品、添加购物车、设置地址、在线下单等
二、项目实操
Ⅰ、员工管理
1、开发环境搭建
第一步
数据库环境搭建,导入sql文件
第二步
maven项目搭建:骨架创建maven。
pom.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!--parent : 继承的意思
此处继承父工程 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.itheima</groupId>
<artifactId>reggie_take_out</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.23</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.4.5</version>
</plugin>
</plugins>
</build>
</project>
第三步
导入SpringBoot配置文件applidation.yml
resources包下导入application.yml
server:
port: 8080 #tomcat端口号
spring:
application:
name: reggie_take_out #指定应用的名称 非必须
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/reggie?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: root
mybatis-plus:
configuration:
#address book---->AddressBook
#user_name---->userName
#在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: ASSIGN_ID
第四步
编写启动类,可尝试启动
@Slf4j
//可以使用log方法,打印info级别日志
@SpringBootApplication
//引导类or启动类
@ServletComponentScan//扫描webFilter注解 进一步创建过滤器
public class ReggieApplication {
public static void main(String[] args) {
SpringApplication.run(ReggieApplication.class,args);
log.info("项目启动成功。。。。");//打印info级别日志
}
}
注意:静态资源一般放在webapp/static或者webapp/resources等目录下。默认寻找这个目录下的静态资源。不在直接报错。
解决::配置映射文件
@Slf4j
//可以使用log方法,打印info级别日志
@Configuration//声明该类是配置类
public class WebMvcConfig extends WebMvcConfigurationSupport {
/*
静态资源文件应该都在webapp目录下
本项目没有webapp目录
需要设置静态资源映射
*/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
log.info("开始进行静态资源映射。。。");
registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
/*
tomcat服务器访问路径映射到相应的静态文件下
pathPatterns:服务器访问路径
classpath:(指定类文件的路径)此处指读取静态资源的路径resources目录
*/
}
2、功能开发
1、后台登录功能开发
第一步:
创建实体类Employee,和employee表进行映射
开发习惯:数据库一张表对应一个实体类
从资料直接导入
第二步:
创建Controller、Service、 Mappe
//mapper接口
@Mapper
public interface EmployeeMapper extends BaseMapper<Employee> {//泛型实体
}
//EmployeeService接口
@Mapper
public interface EmployeeService extends IService<Employee> {//继承IService泛型实体
}
//EmployeeServiceImpl继承serviceImpl(对应mapper类,对应实体类)继承 EmployeeService接口
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements EmployeeService {
}
//EmployeeController类
@Slf4j
@RestController
@RequestMapping("/employee")//路径
public class EmployeeController {
//注入
@Autowired
private EmployeeService employeeService;
第三步:
导入返回类R
此类是一个通用结果类,服务端响应的所有结果最终都会包装成此种类型返回给前端页面
/*
通用返回结果,限务端响应的数据最终部会封装成此对练
*/
@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;
}
}
第四步:
在Controller中创建登录方法
处理逻辑:
1、将页面提交的密码password进行md5加密处理
2、根据页面提交的用户名username查询数据库
3、如果没有查询到则返回登录失败结果
4、密码比对,如果不一致则返回登录失败结果
5、查看员工状态,如果为已禁用状态,则返回员工已禁用结果
6、登录成功,将员工id存入Session并返回登录成功结果
@Slf4j
@RestController
@RequestMapping("/employee")//路径
public class EmployeeController {
//注入
@Autowired
private EmployeeService employeeService;
/*
员工登录
*/
@PostMapping("/login")
//json在接收时,要有注解@RequestBody 分装成emplouee对象
public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee) {
//Employee的id存在Session中,表示成功。获取当前用户:request来get
/**
* 1、将页面提交的密码password进行md5加密处理,已经封装到Employee中
* 密码已经封装到employee中
*/
String password = employee.getPassword();//拿到密码
password = DigestUtils.md5DigestAsHex(password.getBytes());//md5加密处理
// 2、根据页面提交的用户名username查询数据库
//employee是泛型
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Employee::getUsername, employee.getUsername());//指定sql语句条件
Employee emp = employeeService.getOne(queryWrapper);
//getOne 这个用户名是唯一的用getOne调用,调用后分装成employee对象
//3、如果没有查询到则返回登录失败结果
if (emp == null) {
//返回结果封装成R对象
return R.error("登录失败");
}
//4、密码比对,如果不一致则返回登录失败结果
//数据库查的密码和处理后的密码对比
if (!emp.getPassword().equals(password)) {
return R.error("登录失败");
}
// 5、查看员工状态,如果为已禁用状态,则返回员工已禁用结果
if (emp.getStatus() == 0) {
//1账户可用。0账号不可用
return R.error("账户已禁用");
}
// 6、登录成功,将员工id存入Session并返回登录成功结果
request.getSession().setAttribute("employee", emp.getId());
return R.success(emp);
}
2、后台退出功能开发
直接点击右侧的退出按钮即可退出系统,退出系统后页面应跳转回登录页面
用户点击页面中退出按钮,发送请求,请求地址为/employee/logout,请求方式为POST。
我们只需要在Controller中创建对应的处理方法即可,具体的处理逻辑;
1、清理Session中的用户id
2、返回结果
/**
* 员工退出页面
*/
@PostMapping("/logout")
public R<String> logout(HttpServletRequest request){
// 清理Session中保存的当前登录员工的id
request.getSession().removeAttribute("employee");
//把employee属性移除
return R.success("退出成功");
}
3、员工管理业务开发
3.1完善登录功能
3.1.1问题
用户如果不登录,直接访问系统页面,可以直接访问
3.1.2方案
使用过滤器或者拦截器。没有登录跳转到登录界面
3.1.3代码开发
实现步骤
1、创建自定义过滤器LoginCheckFilter
2、在启动类上加入注解@Servletcomponentscan
3、完善过滤器的处理逻辑
创建filter包。给启动类添加扫描注解@ServletComponentScan
/*
检查用户是否已将登陆
*/
@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
@Slf4j
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();
log.info("拦截到请求:{}",requestURI);//{}requestURI占位符
//不需要拦截的直接放行的
String[] urls = new String[]{
"/employee/login",//登录页面
"/employee/logout",//退出页面
"/backend/**",//静态资源
"/front/**"
};
//2、判断本次请求是否需要处理--》请求的页面是否在要放行的页面中
boolean check = check(urls,requestURI);
//3、如果不需要处理,则直接放行
if (check){
log.info("本次请求{}不需要处理",requestURI);
filterChain.doFilter(request,response);
return;
}
//4、判断登录状态,如果已登录,则直接放行
if (request.getSession().getAttribute("employee") != null){
log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("employee"));
filterChain.doFilter(request,response);
return;
}
log.info("用户未登录");
// 5、如果未登录则返回未登录结果通,过输出流方式向客户端页面响应数据
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
return;
}
/**
* 路径匹配,检查本次请求是否需要放行
* @param urls
* 遍历要放行的数组。和请求的数组对比
* @param requestURL
* @return
*/
public boolean check(String[] urls,String requestURL) {
for (String url : urls){
boolean match = PATH_MATCHER.match(url,requestURL);
if (match){
return true;//放回true表示匹配上
}
}
return false;
}
}
@Slf4j
//可以使用log方法,打印info级别日志
@SpringBootApplication
//引导类or启动类
@ServletComponentScan//扫描webFilter注解 进一步创建过滤器
public class ReggieApplication {
public static void main(String[] args) {
SpringApplication.run(ReggieApplication.class,args);
log.info("项目启动成功。。。。");//打印info级别日志
}
}
4、新增员工
4.1数据模型
将新增页面录入的员工数据插入到employee表。
需要注意,employee表中对username字段加入了唯一约束,因为username是员工的登录账号,必须是唯一的
4.2代码开发
整个程序的执行过程:
1、页面发送ajax请求,将新增员工页面中输入的数据以json的形式提交到服务端
2、服务端Controller接收页面提交的数据并调用Service将数据进行保存
3、Service调用Mapper操作数据库,保存数据
前面的程序还存在一个问题,就是当我们在新增员工时输入的账号已经存在,由于employee表中对该字段加入了唯
一约束,此时程序会抛出异常:
java sql.SQLIntegrityConstraintviolationException: Duplicate entry
zhangsan for key idx_username
此时需要我们的程序进行异常捕获,通常有两种处理方式
1、在Controller方法中加入try、catch进行异常捕获
2、使用异常处理器进行全局异常捕获
全局异常处理器
*关键在@@ControllerAdvice和@ExceptionHandler两个注解
详解看注释
/**
* 全局异常处理器
* 关键在@@ControllerAdvice和@ExceptionHandler两个注解
*/
//
//拦截 类上加restController和controllee类
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody//封住成JSON数据
@Slf4j
public class GlobalExceptionHandler {
/**
* 异常处理方法
*/
//@ExceptionHandler声明要处理的类
//此处处理的是SQLIntegrityConstraintViolationException的异常
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R<String> exceptionHamdler(SQLIntegrityConstraintViolationException ex){
log.error(ex.getMessage());
//异常有多种
//判断异常信息里边是否有需要的关键字
//用户名唯一,重复报一下错误
//Duplicate entry 'zhangsan' for key'idx username
//关键字=Duplicate entry
if (ex.getMessage().contains("Duplicate entry")){
//冬天截取重复的用户名 用空格隔开
String[] split = ex.getMessage().split(" ");
//split[2] “zhangsan的下标”
String msg = split[2] + "已经存在";
//通用返回结果
return R.error(msg);
}
return R.error("未知错误");
}
}
总结:
1、根据产品原型明确业务需求
2、重点份析数据的流转过程和数据格式
3、通过debug断点调试跟踪程序执行过程
+++++++++++++++++++++++++++++++++++++++
其他功能流程和新增功能流程相同。数据格式不同+
+++++++++++++++++++++++++++++++++++++++
5、员工信息分页查询
5.1、程序的执行过程:
1、页面发送ajax请求,将分页查询参数(page、pagesize、name)提交到服务端
2、服务端Controller接收页面提交的数据并调用Service查询数据
3、Service调用Mapper操作数据库,查询分页数据
4、Controller将查询到的分页数据响应给页面 5、页面接收到分页数据并通过Elementul的Table组件展示到页面上
5.2、代码开发
第一步:
config包下MP分页插件配置类
/**
* 配置MP的分页插件
*/
@Configuration//声明这是配置类
public class MybatisPlusConfig {
@Bean//bean注解 spring来管理
//通过拦截器的方式把插件加载进来
public MybatisPlusInterceptor mybatisPlusConfigInterceptor(){
//创建拦截器对象
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
//加入插件
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
}
第二步:
controller层EmployeeController代码
/**
* 分页处理的数据 用page接受
* 员工信息查询
* @param page
* @param pageSize
* @param name
* @return
*/
@GetMapping("/page")//get请求 所以getmapping
//设置默认页数和单页数据条数 name:查询的用户
public R<Page> page(int page,int pageSize,String name){
log.info("page ={},pageSize = {},name = {}",page,pageSize,name);
//构造分页构造器
Page pageInfo = new Page(page,pageSize);
//构造条件构造器
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
//添加过滤添条件
queryWrapper.like(StringUtils.isNotEmpty(name),Employee::getName,name);
//添加排序条件
queryWrapper.orderByDesc(Employee::getUpdateTime);
//执行查询
employeeService.page(pageInfo,queryWrapper);
return R.success(pageInfo);
}
6、启用/禁用员工账号
只有管理员能进后台和设置账号的状态 。
某个员工账号状态为正常,则按钮显示为“禁用”,如果员工账号状态为已禁用,则按钮显示为“启用”
6.1、代码开发
页面中是怎么做到只有管理员admin能够看到启用、禁用按钮的?
6.2、执行过程
1、页面发送ajax请求,将参数(id、status)提交到服务端
2、服务端Controller接收页面提交的数据并调用Service更新数据
3、Service调用Mapper操作数据库
页面中的ajax请求是如果发送的呢?
第一步:
controller层EmployeeController代码
/**
* 根据id修改员工信息
* @return
*/
@PutMapping
public R<String> updata(HttpServletRequest request,@RequestBody Employee employee){
log.info(employee.toString());
//获取修改人信息
Long empId = (long)request.getSession().getAttribute("employee");
employee.setUpdateTime(LocalDateTime.now());//当前修改时间
employee.setUpdateUser(empId);//修改人信息。当前登录的人可以修改,所修改人=当前登陆人
employeeService.updateById(employee);
return R.success("员工信息修改成功");
}
数据库id和禁用启动操作id不同(id 19位)
原因:Long(1前6位)类型丢失精度
第二步:
解决上述问题:
在服务端给页面响应json数据时进行处理,将long型数据统一转为String字符串
具体实现步骤:
1)提供对象转换器jacksonobjectMapper,基于jackson进行Java对象到json数据的转换(资料中已经提供,直接复制到项目中使用)。放common包
2)在WebMvcConfig配置类中扩展Springmvc的消息转换器,在此消息转换器中使用提供的对象转换器进行Java对象到json数据的转换
/**
* 扩展mve框架的消息转换器
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
//创建消息转换器对象
// 我们自己new 的转换器放入 converters中。默认8个转换器,现在+1
MappingJackson2CborHttpMessageConverter messageConverter = new MappingJackson2CborHttpMessageConverter();
//设置对象转换器,底层使用Jackson将java对象转为json
messageConverter.setObjectMapper(new JacksonObjectMapper());
// 将上面的消息转换器对象追加到mvc框架的转换器集合中
//注意add导入index类。 0:优先使用自己的转换器
converters.add(0,messageConverter );
}
7、编辑员工信息
7.1、执行流程:
1、点击编辑按钮时,页面跳转到add.html,并在url中携带参数[员工id]
http://localhost:8000/backend/page/member/add.htmlid=1644891700080074753
2、在add.html页面获取url中的参数员工id]
3、发送ajax请求,请求服务端,同时提交员工id参数
4、服务端接收请求,根据员工id查询员工信息,将员工信息以json形式响应给页面
5、页面接收服务端响应的json数据,通过VUE的数据绑定进行员工信息回显
6、点击保存按钮,发送ajax请求,将页面中的员工信息以json方式提交给服务端
7、服务端接收员工信息,并进行处理,完成后给页面响应
8、页面接收到服务端响应信息后进行相应处理
注意:add.html页面为公共页面,新增员工和编辑员工都是在此页面操作
7.2、代码开发
controller层EmployeeController代码
/**
* 根据id查询员工信息
* 复用根据id修改员工信息
* @param id
* @return
*/
@GetMapping("/{id}")
public R<Employee> getById(@PathVariable Long id){
log.info("根据id查询员工信息。。。");
Employee employee = employeeService.getById(id);
if (employee != null){
return R.success(employee);
}
return R.error("没有查询到对应员工信息");
}
Ⅱ、分类管理
1、公共字段自动填充
1.1、问题分析
员工管理功能开发时,在新增员工时需要设置创建时间、创建人、修改时间、修改人等字段,在编辑员工时需要设置修改时间和修改人等字段。这些字段属于公共字段
> 多张表都有的字段
1.2、代码实现
MybatisPlus公共字段自动填充,也就是在插入或者更新的时候为指定字段赋予指定的值,使用它的好处就是可以统一对这些字段进行处理,避免了重复代码。
实现步骤:
1、在实体类的属性上加入***@TableField***注解,指定自动填充的策略
2、按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现MetaObjectHandler接口
/**
第一步:1、在实体类的属性上加入***@TableField***注解,指定自动填充的策略
* 那些是公共字段
* 就加上@TableField注解
* fill :填充 =号后边是填充策略
* 详细策略看FieldFill类
*/
@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;
1.3、功能完善
注意:①当前我们设置createUser和updateUser为固定值 后面我们需要进行改造,改为动态获得当前登录用户的id。
②可以使用ThreadLocal来解决此问题,它是DK中提供的一个类。
在学习ThreadLocal之前,先确认一个事情:客户端发送的每次http请求,对应的在服务端都会分配一个新的线程来处理,在处理过程中涉及到下面类中的方法都属于相同的一个线程:
1、LogincheckFilter的doFilter方法
2、EmployeeController的update方法
3、MyMetaObjectHandler的updateFi方法
/**
* 测试
* 可以在上面的三个方法中分别加入下面代码(获取当前线程id):
* long id = Thread。currentThread().getId();
* log.info("线程id:{}",id)
*/
什么是ThreadLocal?
ThreadLocal并不是一个Thread(线程),而是Thread的局部变量。当使用ThreadLocal维护变量时,Threadlocal为每个使用该
变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不 能访问。
/**
* ThreadLocal常用方法:
* public void set(T value) 设置当前线程的线程局部变量的值
* public T get() 返回当前线程所对应的线程局部变量的值
*
我们可以在LogincheckFilter的doFilter方法中获取当前登录用户id,并调用ThreadLocal的set方法来设置当前线程的线
程局部变量的值(用户id),然后在MyMetaObjectHandler的updateFill方法中调用ThreadLocal的get方法来获得当前
线程所对应的线程局部变量的值(用户id)。
实现步骤:
1、编写Basecontext工具类,基于ThreadLocal封装的工具类
2、在LoginCheckFilter的doFilter方法中调用BaseContext来设置当前登录用户的id
3、在MyMetaobjectHandler的方法中调用BaseContext获取登录用户的id
package com.itheima.reggie.common;
/**
* 1、编写Basecontext工具类,基于ThreadLocal封装的工具类
* 基于ThreadLocal封装工具类,用户保存和获取当前登录用户id
*/
public class BaseContext {
//id 是lang类型 所以泛型是long
private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
public static void setCurrentId(Long id) {
threadLocal.set(id);//保存值
}
public static Long getCurrentId(){
return threadLocal.get();//取值
}
}
// 第二步:在LoginCheckFilter的doFilter方法中调用BaseContext来设置当前登录用户的id
//4、判断登录状态,如果已登录,则直接放行
if (request.getSession().getAttribute("employee") != null){
log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("employee"));//获取id
//获取id
Long empId = (Long) request.getSession().getAttribute("employee");
BaseContext.setCurrentId(empId);
filterChain.doFilter(request,response);
return;
}
log.info("用户未登录");
3、在MyMetaobjectHandler的方法中调用BaseContext获取登录用户的id metaObject.setValue("updateUser",new Long(1));
metaObject.setValue("createUser",BaseContext.getCurrentId());
2、新增分类
菜品分类和套餐分类。在后台系统中添加菜品时选择一个菜品分类
在开发业务功能前,先将需要用到的类和接口基本结构创建好
实体类Category(直接从课程资料中导入即可)
Mapper接口CategoryMapper
业务层接口Categorysenvice
业务层实现类Categoryservicelmpl
控制层Categorycontroller
2.1代码开发
2.2程序的执行过程:
1、页面(backend/page/category/list.html)发送ajax请求,将新增分类窗口输入的数据以json形式提交到服务端
2、服务端Controller接收页面提交的数据并调用Service将数据进行保存
3、Service调用Mapper操作数据库,保存数据
package com.itheima.reggie.controller;
/**
* 分类管理
*/
@RestController
@RequestMapping("/categoty")
@Slf4j
public class CategoryController {
@Autowired
private CaregoryService caregoryService;
/**
* 新增分类成功
*/
@PutMapping
public R<String> save(@RequestBody Category category){
log.info("category:{}",category);
caregoryService.save(category);
return R.success("新增分类成功");
}
}
功能测试:可以新增菜品分类
3、分类信息分页查询
3.1执行过程:
1、页面发送ajax请求,将分页查询参数(page、pagesize)提交到服务端
2、服务端Controller接收页面提交的数据并调用Service查询数据
3、Service调用Mapper操作数据库,查询分页数据
4、Controller将查询到的分页数据响应给页面
5、页面接收到分页数据并通过Elementul的Table组件展示到页面上
package com.itheima.reggie.controller;
/**
* 分页查询
* @param page
* @param pageSize
* @return
*/
@GetMapping("/page")
public R<Page> page(int page, int pageSize){
//分页构造器
//页数和一页的条件
Page<Category> pageInfo = new Page<>(page,pageSize);
//条件构造器
LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
//添加排序条件,根据sort进行排序
queryWrapper.orderByAsc(Category::getSort);
//进行分页查询
categoryService.page(pageInfo,queryWrapper);
return R.success(pageInfo);
}
4、删除分类
需求分析
在分类管理列表页面,可以对某个分类进行删除操作。需要注意的是当分类关联了菜品或者套餐时,此分类不允许删除。
代码开发
1、页面发送ajax请求,将参数(id)提交到服务端
2、服务端Controller接收页面提交的数据并调用Service删除数据
3、Service调用Mapper操作数据库
CategoryController层
/**
* 根据id删除分类
* @param id
* @return
*/
@DeleteMapping
public R<String> delete(Long id){
log.info("删除分类,id为:{}",id);
categoryService.removeById(id);
return R.success("分类信息删除成功");
}
前面我们已经实现了根据id删除分类的功能,但是并没有检查删除的分类是否关联了菜品或者套餐,所以我们需要进行功
能完善。
要完善分类删除功能,需要先准备基础的类和接口:
1、实体类Dish(菜单实体类)和Setmeal(套餐实体类)(从课程资料中复制即可
2、Mapper接口DishMapper和SetmealMapper
3、Service接口DishService和Setmealservice
4、Service实现类DishServicelmpl和SetmealServicelmpl
service包下impl
@Service
public class CategoryServiceImpl extends ServiceImpl<CateGoryMapper,Category> implements CaregoryService {
//注入,
//菜品分类id private Long categoryId;
@Autowired
private DishService dishService;
@Autowired
private SetmealService setmealService;
/**
* 根据id删除分类,删除之前需要经进行判断
* @param id
*/
@Override
public void remove(Long id) {
//构造查询条件
LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
//添加查询条件,根据分类categoryId 数据库是:category_Id 进行查询
dishLambdaQueryWrapper.eq(Dish::getCategoryId,id);
//count 统计数据量
int count1 = dishService.count(dishLambdaQueryWrapper);
// 查询当前分类是否关联了菜品,如果已经关联、抛出一个业务异常
if (count1 > 0){
//已经关联菜品,抛出一个业务异常
throw new CustomException("当前分类下关联了菜品,不能删除");
}
// 查询当前分类是否关联了套餐,如果已经关联,抛出一个业务异常
LambdaQueryWrapper<Setmeal> setmealLambdQueryWrapper = new LambdaQueryWrapper<>();
//添加查询条件,根据分类id进行查询
setmealLambdQueryWrapper.eq(Setmeal::getCategoryId,id);
int count2= setmealService.count();
if (count2 > 0){
//已经关联套餐,抛出一个业务异常
throw new CustomException("当前分类下关联了套餐,不能删除");
}
//菜品和套餐都没有关联
//可以正常删除分类 调用删除方法
super.removeById(id);
}
}
common包下自定义异常类
/**
* 自定义业务异常
*/
public class CustomException extends RuntimeException{
public CustomException(String message){
super(message);
}
}
common包下全局异常类
/**
* 异常处理方法
*/
//@ExceptionHandler声明要处理的类
//此处处理的是SQLIntegrityConstraintViolationException的异常
@ExceptionHandler(CustomException.class)
public R<String> exceptionHamdler(CustomException ex){
log.error(ex.getMessage());
return R.error(ex.getMessage());
}
5、修改分类
service层impl下CategoryServiceImpl
/**
* 根据id修改分类信息
* @param category
* @return
*/
@PutMapping
public R<String> update(@RequestBody Category category){
log.info("修改分类信息:{}",category);
categoryService.updateById(category);
return R.success("修改分类信息成功");
}
Ⅲ、菜品管理业务开发
1、文件上传下载
文件上传介绍
- 服务端要接收客户端页面上传的文件,通常都会使用Apache的两个组件:
①commons-fileupload
②commons-io
/**
* 文件上传
* Spring框架在spring-web包中对文件上传进行了封装,简化服务端代码
* 我们只需要在Controller的方法中声明
* 一个MultipartFile类型的参数即可接收上传的文件,如下:
* 本质还是上边的两种方式
* @param file
* @return
*/
@PostMapping(value = "/upload")
public R<String> upload (MultipartFile file){
System.out.println(file);
return null;
}
}
文件下载介绍:
文件下载,download,指将文件从服务器传输到本地计算机的过程。
通过浏览器进行文件下载,通常有两种表现形式:
①以附件形式下载,弹出保存对话框,将文件保存到指定磁盘目录
②直接在浏览器中打开
- 通过浏览器进行文件下载,本质上就是服务端将文件以流的形式写回浏览器的过程。
1.1、文件上传代码实现
文件下载代码实现
第一步:
第二步:controller层创建CommonController菜品管理类
/**
* 文件上传和下载
*/
@RestController
@RequestMapping("/common")
@Slf4j
public class CommonController {
//读取配置文件中转存位置
//application.yml文件
//reggie:
// path: D:\
@Value("${reggie.path}")
private String basePath;
/**
* 文件上传
* @return
*/
@PostMapping("/upload")
public R<String> upload(MultipartFile file){
//file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会删除
log.info(file.toString());
/**
* 防止总是被拦截器拦截,在拦截器类设置不需要拦截直接放行
* String[] urls = new String[]{
* //菜品管理
* "/common"
* };
*/
//原始文件名
String originalFilename = file.getOriginalFilename();//abc.jpg
//动态截取文件后缀 suffix接受新的文件名 .之后的
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
//使用UUID重新生成文件名,防止文件名称重复造成文件覆盖 新文件 + 后缀(.jpg)
String fileName = UUID.randomUUID().toString() + suffix;//qwertt.jpg
//判断application.yml设置的指定位置目录是否存在
File dir = new File(basePath);
//判断当前目录是否存在
if (!dir.exists()){
//目录不存在 ,需要创建
dir.mkdirs();
}
try {
//将临时文件转存到指定位置.
//转存位置可在application.ym中设置
file.transferTo(new File(basePath + fileName));
} catch (IOException e) {
e.printStackTrace();
}
//文件名称需要放在数据库中 返回fileaName
return R.success(fileName);
}
/**
* 文件下载
* @param name
* @param response
*/
//通过流写回数据 不需要返回值
// 输出流需要respinse获得
@GetMapping("/download")
public void download(String name , HttpServletResponse response){
try {
//输入流,通过输入流读取文件内容
FileInputStream fileInputStream = new FileInputStream(new File(basePath + name));
//输出流,通过输出流将文件写回浏览器,在浏览器展示图片了 输出流需要respinse获得
ServletOutputStream outputStream = response.getOutputStream();
// 设置想要返回什么类型文件
response.setContentType("image/jpeg");
// 将读到的内容,放到数组中去
int len = 0;
byte[] bytes = new byte[1024];
//-1 说明没有读完
while ((fileInputStream.read(bytes)) != -1){
// 用过输出流向浏览器写。从第一个写 写len这么长
outputStream.write(bytes,0,len);
outputStream.flush();//刷新
}
// 关闭资源
outputStream.close();
fileInputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
2、新增菜品
数据模型:
新增菜品,其实就是将新增页面录入的菜品信息插入到dish表,如果添加了口味做法,还需要向dish_flavor表插入数据
所以在新增菜品时,涉及到两个表:
o dish
菜品表
dish flavor
菜品口味表
准备工作:
实体类DishFlavor(直接从课程资料中导入即可,Dish实体前面课程中已经导入过了)
Mapper接口DishFlavorMapper
业务层接口DishFlavorService
业务层实现类DishFlavorservicelmpl
控制层Dishcontroller
1、Mapper接口DishFlavorMapper
@Mapper
public interface DishFlavorMapper extends BaseMapper<DishFlavor> {
}
2、业务层接口DishFlavorService
public interface DishFlavorService extends IService<DishService> {
}
3、业务层实现类DishFlavorservicelmpl
public class DishFlavorServiceImpl extends ServiceImpl<DishFlavorMapper, DishFlavor> {
}
4、控制层Dishcontroller
/**
* 菜品管理
*/
@RestController
@RequestMapping("/dish")
public class DishController {
@Autowired
private DishService dishService;
@Autowired
private DishFlavorService dishFlavorService;
}
2.1梳理交互过程
1、页面(backend/page/food/add.html)发送ajax请求,请求服务端获取菜品分类数据并展示到下拉框中
2、页面发送请求进行图片上传,请求服务端将图片保存到服务器
3、页面发送请求进行图片下载,将上传的图片进行回显
4、点击保存按钮,发送ajax请求,将菜品相关数据以json形式提交到服务端
开发新增菜品功能,其实就是在服务端编写代码去处理前端页面发送的这4次请求即可
/**
控制层CategoryController.java
* 根据条件查询分类数据
* @param category
* @return
*/
@GetMapping("/list")
//list方法,根据条件查询方法
public R<List<Category>> list(Category category){
//条件构造器
LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
//动态添加条件
queryWrapper.eq(category.getType() != null,Category::getType,category.getType());
//添加排序条件
//第一个:根据sort排序
//第二个:根据创建时间排序
queryWrapper.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime);
List<Category> list = categoryService.list(queryWrapper);
return R.success(list);
}
DTO,全称为DataTransferObject,即数据传输对象,一般用于展示层与服务层之间的数据传输。
dish中的数据和实体类不是一一对应
导入DTO
//导入DTO需要单独建包
@Data
public class DishDto extends Dish {
//继承dish中的属性。扩展一些属性
// flavors接受页面传过来的数据
private List<DishFlavor> flavors = new ArrayList<>();
private String categoryName;
private Integer copies;
}
//controller层
/**
* 菜品管理
*/
@RestController
@RequestMapping("/dish")
@Slf4j
public class DishController {
//插入两张表
@Autowired
private DishService dishService;
@Autowired
private DishFlavorService dishFlavorService;
/**
* 新增菜品
* @param dishDto
* @return
*/
@PostMapping
//加@RequestBody接收JSON数据
public R<String> save(@RequestBody DishDto dishDto){
log.info(dishDto.toString());
dishService.saveWithFlavor(dishDto);
return R.success("新增菜品成功");
}
}
表操作
service层创建DishService接口
public interface DishService extends IService<Dish> {
//新增菜品,同时插入菜品对应的口味数据,需要操作两张表:dish、dish_flavol
public void saveWithFlavor(DishDto dishDto);
}
Service层impl包创建DishServiceImpl时间DishService接口
@Slf4j
@Service
public class DishServiceImpl extends ServiceImpl<DishMapper, Dish> implements DishService {
@Autowired
//控制口味表
private DishFlavorService dishFlavorService;
/**
* 新增菜品,同时保存对应的口味数据
* @param dishDto
*/
@Transactional//事务控制(数据库操作)的注解
public void saveWithFlavor(DishDto dishDto) {
//保存菜品的基本信息到莱品表dish
this.save(dishDto);
Long dishId = dishDto.getId();//菜品id
//菜品口味
List<DishFlavor> flavors = dishDto.getFlavors();
//遍历集合flavors 遍历DishFlavor实体类中的数据.此处使用流
flavors = flavors.stream().map((item) -> {
item.setDishId(dishId);
return item;
}).collect(Collectors.toList());
//保存菜品口味数据到菜品口味表dish_flavor
dishFlavorService.saveBatch(flavors);
}
}
Controller层创建DishController
/**
* 菜品管理
*/
@RestController
@RequestMapping("/dish")
@Slf4j
public class DishController {
//插入两张表
@Autowired
private DishService dishService;
@Autowired
private DishFlavorService dishFlavorService;
/**
* 新增菜品
* @param dishDto
* @return
*/
@PostMapping
//加@RequestBody接收JSON数据
public R<String> save(@RequestBody DishDto dishDto){
log.info(dishDto.toString());
dishService.saveWithFlavor(dishDto);
return R.success("新增菜品成功");
}
}
3、菜品信息分页查询
3.1交互过程
1、页面(backend/page/food/list.html)发送ajax请求,将分页查询参数(page、pagesize、name)
提交到服务端,获取分页数据
2、页面发送请求,请求服务端进行图片下载,用于页面图片展示
开发菜品信息分页查询功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求。
页面放回的是name(菜品名称),无菜品分类。
菜品分类无法展示
解决:①Dto中创建菜品分类属性categoryName
②Dto继承实体类category=给实体类加了一个属性(菜品分类)
categoryName
3.2代码开发
controller层dishcontroller分页查询代码
/**
* 菜品信息分页查询
* @param page
* @param pageSize
* @param name
* @return
*/
@GetMapping("/page")
public R<Page> page(int page,int pageSize, String name){
//构造器分页构造器对象
Page<Dish> pageInfo = new Page<>(page, pageSize);
//将dish中属性的值拷贝给DishDto
Page<DishDto> dishDtoPage = new Page<>();
//条件构造器
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
//根据name 通过like进行模糊查询。添加过滤条件
queryWrapper.like(name != null,Dish::getName,name);
//添加排序条件。根据更新时间updateTime 降序排序
queryWrapper.orderByDesc(Dish::getUpdateTime);
// 执行分页查询 调用page方法 ,传入pageInfo queryWrapper
dishService.page(pageInfo,queryWrapper);
// 拷贝属性 使用BeanUtils类 pageInfo拷贝到dishDtoPage,忽略records属性
BeanUtils.copyProperties(pageInfo,dishDtoPage,"records");
// 处理pageInfo中忽略的records属性为DishDto属性
// 遍历records 最后手机起来
List<Dish> records = pageInfo.getRecords();
List<DishDto> list = records.stream().map((item) -> {
DishDto dishDto = new DishDto();
//Dish实体类中其他属性都是空的,需要拷贝
BeanUtils.copyProperties(item,dishDto);
Long categoryId = item.getCategoryId();//分类id
//根据id查询分类对象
Category category = categoryService.getById(categoryId);
if (category != null) {
//获取分类名称 需要一个Dto并给Dto赋值
String categoryName = category.getName();
//需要一个Dto并给Dto赋值
dishDto.setCategoryName(categoryName);
// 此处categoryName包含菜品分类(categoryName)和其他普通属性
}
return dishDto;
}).collect(Collectors.toList());
dishDtoPage.setRecords(list);
return R.success(dishDtoPage);
}
4、修改菜品
4.1交互过程
1、页面发送ajax请求,请求服务端获取分类数据,用于菜品分类下拉框中数据展示
2、页面发送ajax请求,请求服务端,根据id查询当前菜品信息,用于菜品信息回显
3、页面发送请求,请求服务端进行图片下载,用于页图片回显
4、点击保存按钮,页面发送ajax请求,将修改后的菜品相关数据以json形式提交到服务端
开发修改菜品功能,其实就是在服务端编写代码去处理前端页面发送的这4次请求
4.2代码开发
4.2.1根据id获取当前菜品信息
service层DishServiceImpl
/**
* 根据:id查询菜品信息和对应的口味信息
* @param id
* @return
*/
public DishDto getByIdWithFlavor(Long id) {
//查询菜品基本信息,从dish表查询
Dish dish = this.getById(id);
//第一部分:拷贝普通用户值
//拷贝
DishDto dishDto = new DishDto();//创建dishDto对象
BeanUtils.copyProperties(dish,dishDto);
//查询当前菜品对应的口味信息,从dish_flavor表查询
// 第二部分:拷贝口味的值
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>(); //构造条件过滤器 <对应的实体>
//添加条件 调用DishFlavor中dishId,dish.getId:获取到id值
queryWrapper.eq(DishFlavor::getDishId,dish.getId());
//调用dishFlavorService查询
List<DishFlavor> flavors = dishFlavorService.list(queryWrapper);
// 单独给dishDto赋值
dishDto.setFlavors(flavors);
//查询完之后需要返回DishDto类型需要拷贝
// 把第一个和第二个的属性值放回给dishDto类型
return dishDto;
}
controller调用service中的修改菜品方法getByIdWithFlavor
/**
* 根据id查询菜品信息和对用的口味信息
* Service层写完 调用controller层
* @param id
* @return
*/
//口味不是普通属性,需要勇DishDto类型数据
@GetMapping("/{id}")
//@PathVariable 获取id注解
public R<DishDto> get(@PathVariable Long id){
//此处需要查两张表dish 和口味DISH_Flavor。在service层DishService接口中创建方法.并在dishserviceImpl实现接口方法
//调用service层方法
DishDto dishDto = dishService.getByIdWithFlavor(id);
//直接返回
return R.success(dishDto);
}
4.2.2修改菜品
修改代码开发
service业务层DishService接口中创建修改方法updateWithFlavor。并实现接口方法
public interface DishService extends IService<Dish> {
//新增菜品,同时插入菜品对应的口味数据,需要操作两张表:dish、dish_flavol
public void saveWithFlavor(DishDto dishDto);
//根据id查询菜品信息以及他所对应的口味信息
public DishDto getByIdWithFlavor(Long id);
//更新菜品信息,同时更新对应的口味信息
public void updateWithFlavor(DishDto dishDto);
}
DishServiceImpl实现接口中修改updateWithFlavor方法
/**
* 修改菜品信息和口味信息
* @param dishDto
*/
@Override
@Transactional//开启事务注解,保证事务一致性
public void updateWithFlavor(DishDto dishDto) {
//1、更新dish表基本信息
//dishDto继承dish ,此处继承调用dishDto == 更新dish中的方法,也就是普通属性
this.updateById(dishDto);
//2、清理当前菜品对应口味数据--dish__flavor表的delete操作
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();//创建对象
queryWrapper.eq(DishFlavor::getDishId,dishDto.getId());//添加条件
dishFlavorService.remove(queryWrapper);//执行sql
//3、添加当前提交过来的口味数据--dish__flavor表的insert操作
//插入来自页面的信息,在dishDto中已经封装
List<DishFlavor> flavors = dishDto.getFlavors();
//DishFlavor属性不全,
// 解决:遍历flavors,把每一项拿出来,从dishDto中重新setDishId
flavors = flavors.stream().map((item) -> {
item.setDishId(dishDto.getId());
return item;//处理之后重新付给flavors
}).collect(Collectors.toList());
dishFlavorService.saveBatch(flavors);
}
controller层DishController
/**
* 修改菜品
* @param dishDto
* @return
*/
@PutMapping
public R<String> update(@RequestBody DishDto dishDto){
log.info(dishDto.toString());
dishService.updateWithFlavor(dishDto);
return R.success("修改菜品成功");
}
Service业务层DishServiceimpl
/**
* 修改菜品信息
* @param dishDto
*/
@Override
@Transactional//开启事务注解,保证事务一 致性
public void updateWithFlavor(DishDto dishDto) {
//1、更新dish表基本信息
//dishDto继承dish ,此处继承调用dishDto == 更新dish中的方法,也就是普通属性
this.updateById(dishDto);
//2、清理当前菜品对应口味数据--dish__flavor表的delete操作
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();//创建对象
queryWrapper.eq(DishFlavor::getDishId,dishDto.getId());//添加条件
dishFlavorService.remove(queryWrapper);//执行sql
//3、添加当前提交过来的口味数据--dish__flavor表的insert操作
//插入来自页面的信息,在dishDto中已经封装
List<DishFlavor> flavors = dishDto.getFlavors();
//DishFlavor属性不全,
// 解决:遍历flavors,把每一项拿出来,从dishDto中重新setDishId
flavors = flavors.stream().map((item) -> {
item.setDishId(dishDto.getId());
return item;//处理之后重新付给flavors
}).collect(Collectors.toList());
dishFlavorService.saveBatch(flavors);
}
Ⅳ、套餐管理业务开发
1、新增套餐
需求分析
套餐就是菜品的集合。
后台系统中可以管理套餐信息,通过新增套餐功能来添加一个新的套餐,在添加套餐时需要选择当前套餐所属的套餐
分类和包含的菜品,并且需要上传套餐对应的图片,在移动端会按照套餐分类来展示对应的套餐。
数据模型
新增套餐,其实就是将新增页面录入的套餐信息插入到setmeal表,还需要向setmeal_dish表插入套餐和菜品关联数据。
————————————————————————————涉及的表:
setmeat 套餐表
setmeaLdish 套餐菜品关系表
准备工作
实体类SetmealDish(直接从课程资料中导入即可,Setmeal实体前面课程中已经导入过了
DTOSetmealDto(直接从课程资料中导入即可)
Mapper接口SetmealDishMapper
业务层接口SetmealDishservice
业务层实现类SetmealDishServicelmpl
控制层Setmealcontroller
SetmealDishMapper
@Mapper
//<>里的是实体
public interface SetmealDishMapper extends BaseMapper<SetmealDish> {
}
SetmealDishservice
//<>里是实体类
public interface SetmealDishservice extends IService<SetmealDish> {
}
SetmealDishServicelmpl
@Slf4j
@Service
//ServiceImpl两个泛型类型<对应的mapper,对应的实体类>
public class SetmealDishServicelmpl extends ServiceImpl<SetmealDishMapper, SetmealDish> implements SetmealDishservice {
}
Setmealcontroller
/**
* 套餐管理
*/
@RestController
@RequestMapping("/setmeal")
public class Setmealcontroller {
// 注入
@Autowired
private SetmealService setmealService;
@Autowired
private SetmealDishservice setmealDishservice;
}
1.1交互过程
1、页面(backend/page/combo/add.htmD)发送ajax请求,请求服务端获取套餐分类数据并展示到下拉框中
2、页面发送ajax请求,请求服务端获取菜品分类数据并展示到添加菜品窗口中
3、页面发送ajax请求,请求服务端,根据菜品分类查询对应的菜品数据并展示到添加菜品窗口中
4、页面发送请求进行图片上传,请求服务端将图片保存到服务器
5、页面发送请求进行图片下载,将上传的图片进行回显
6、点击保存按钮,发送ajax请求,将套餐相关数据以json形式提交到服务端
开发新增套餐功能,其实就是在服务端编写代码去处理前端页面发送的这6次请求即可。
controller层
3、页面发送ajax请求,请求服务端,根据菜品分类查询对应的菜品数据并展示到添加菜品窗口中
/**
* 根据条件查询对应的菜品数据
* @param dish
* @return
*/
@GetMapping("/lsit")
public R<List<Dish>> list(Dish dish){
//构造查询条件 泛型Dish
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
//添加条件
queryWrapper.eq(dish.getCategoryId() != null,Dish::getCategoryId,dish.getCategoryId());
//添加条件,查询状态为1(起售状态) 的菜品
queryWrapper.eq(Dish::getStatus,1);
// 添加排序条件, 根据实体类Dish类sort排序 desc降序排序
queryWrapper.orderByDesc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
//调用dishService 把queryWrapper分装成list类型
List<Dish> list = dishService.list(queryWrapper);
return R.success(list);
}
保存数据到对应表
service层SetmealService接口和实现接口方法SemealServiceImpl
SemealServiceImpl实现接口方法,controller层调用service方法
SetmealService
public interface SetmealService extends IService<Setmeal> {
/**
* 新增套餐,同时需要保存套餐和菜品的关联关系
* 将套餐基本信息和关联的菜品信息保存,所以泛型是SetmealDto
*/
public void saveWithDish(SetmealDto setmealDto);
}
SemealServiceImpl
public class SemealServiceImpl extends ServiceImpl<SetmealMapper, Setmeal> implements SetmealService {
@Autowired
private SetmealDishservice setmealDishservice;
/**
* 新增套餐,同时需要保存套餐和菜品的关联关系
*/
@Transactional//事务注解 要么全成功要么全失败
public void saveWithDish(SetmealDto setmealDto) {
// 保存套餐的基本信息,操作setmeal(套餐表),执行insert操作
this.save(setmealDto);
List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
setmealDishes.stream().map((item) ->{
item.setSetmealId(setmealDto.getId());
return item;
}).collect(Collectors.toList());
// 保存套餐和菜品的关联信息,操作setmeal_dish,执行insert操作
//saveBatch批量保存
setmealDishservice.saveBatch(setmealDishes);
}
}
controller层调用service层
/**
* 套餐管理
*/
@Slf4j
@RestController
@RequestMapping("/setmeal")
public class Setmealcontroller {
// 注入
@Autowired
private SetmealService setmealService;
@Autowired
private SetmealDishservice setmealDishservice;
/**
* 新增套餐
* 除了setmel属性还有其他属性(setmealDto)的属性
* 所以不能用setmel当泛型
* @RequestBody接受Json格式数据
* @param setmealDto
* @return
*/
@PostMapping
public R<String> save (@RequestBody SetmealDto setmealDto){
log.info("套餐信息:{}",setmealDto);
setmealService.saveWithDish(setmealDto);
return R.success("新增套餐成功");
}
}
2、套餐信息分页查询
1.1交互过程
1、页面(backend/page/combo/list.html)发送ajax请求,将分页查询参数(page、pagesizename)提交到服务端,获取分页数据
2、页面发送请求,请求服务端进行图片下载,用于页面图片展示
> 开发套餐信息分页查询功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求
/**
* 套餐分页查询
* @param page
* @param pageSize
* @param name
* @return
*/
@GetMapping("/page")
public R<Page> page(int page, int pageSize,String name){
// 分页构造器
Page<Setmeal> pageInfo = new Page<>(page,pageSize);//Setmeal实体类没有套餐分类
//SetmealDto继承Setmeal实体类,并添加套餐分类属性categoryName.
// 但SetmealDto中普通属性(Setmeal)没有值--》把分页查询的结果拷贝给dtoPage
Page<SetmealDto> dtoPage = new Page<>();
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
// 添加查询条件,根据(参数)name进行1ike模糊查询
queryWrapper.like(name != null,Setmeal::getName,name);
// Setmeal实体类中有 updateTime
//添加排序条件,根据更新时间降序排列
queryWrapper.orderByDesc(Setmeal::getUpdateTime);
//查询结果:调用setmealService page方法查询
setmealService.page(pageInfo,queryWrapper);
//拷贝对象(将pageInfo拷贝给dtoPage,recoreds是不用拷贝)
//recoreds是集合,把查询的数据封装进去,他的泛型是T,此处泛型是SetmealDto或Setmeal
BeanUtils.copyProperties(pageInfo,dtoPage,"recoreds");
//recoreds忽略,需要赋值
List<Setmeal> recoreds = pageInfo.getRecords();
//直接勇list集合收集
List <SetmealDto> list = recoreds.stream().map((item) -> {
//定义SetmealDto类型的List,此处定义一个SetmealDto
SetmealDto setmealDto = new SetmealDto();
//new的setmealDto此时只有categoryName,没有其他普通属性
//拷贝
BeanUtils.copyProperties(item,setmealDto);
//获得套餐分类ID
Long categoryId = item.getCategoryId();
// 通过分类id查询分类对象 .此处获得一个对象。是查询的分类的分类对象
Category category = categoryService.getById(categoryId);
if (category !=null){
//分类名称
String categoryName = category.getName();
//获得套餐分类categoryName
setmealDto.setCategoryName(categoryName);
//new的setmealDto此时只有categoryName,没有其他普通属性
}
//值返回给setmeaDto
return setmealDto;
//收集起来 并转成list集合
}).collect(Collectors.toList());
//recoreds计算之后 赋值给list 而List泛型为SetmealDto
//将数据勇list集合接受写在上方
// List<SetmealDto> list = null;
//页面套餐数据没有,页面数数据构和此处返回的数据结构不同。导致不匹配 套餐分类没有数据
// return R.success(dtoPage);
//此时list包含所有属性的值,将list给dtopage
dtoPage.setRecords(list);
return R.success(dtoPage);
}
}
3、删除套餐
单、多选删除。在售必须停售才能删除
service层
SetmealService接口
/**
* 删除套餐,同时需要删除套餐和莱品的关联数据
* @param ids
*/
public void removeWithDish(List<Long> ids);
SemealServiceImpl
/**
* 删除套餐,同时需要删除套餐和莱品的关联数据
* @param ids
*/
@Override
@Transactional
public void removeWithDish(List<Long> ids) {
//第一步:查询套餐状态,确定是否可用删除
//构造查询条件对象
//select count() from setmeal where id in (3) and status=1
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(Setmeal::getId,ids);//多查询多条件。第一个条件
queryWrapper.eq(Setmeal::getStatus,1);//第二个条件
int count =this.count(queryWrapper);
if (count > 0) {
//如果不能删除,抛出一个业务异常
throw new CustomException("正在售卖中,不能删除");
}
//第二步:如果可以删除,先删除套餐表中的数据--setmeal
this.removeByIds(ids);//批量删除
//delete from setmeal_dish where setmealid in (l,2,3)
LambdaQueryWrapper<SetmealDish> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.in(SetmealDish::getSetmealId,ids);
//第三步:删除关系表中的数据
setmealDishservice.remove(lambdaQueryWrapper);
}
controller层调用service层
Setmealcontroller
/**
* 删除套餐
* ids有多个,Long类型
* 使用liST集合接收
* @return
*/
@DeleteMapping
public R<String> delete(@RequestParam List<Long> ids){
log.info("id:{}",ids);
//controller层调用Service层
setmealService.removeWithDish(ids);
return R.success("套餐数据删除成功!");
}
Ⅴ、手机验证码登录
1、短信发送
第三方短信服务会和各个运营商(移动、联通、电信)对接
常用短信服务:
阿里云/华为云/腾讯云/京东/梦网/乐信
第一步: 阿里云短信服务-注册账号
https://www.aliyun.com/
第二步: 阿里云短信服务-设置短信签名
短信消息–国内消息设置
短信签名是短信发送者的署名,表示发送方的身份
第三步: 阿里云短信服务-设置短信模板
第四步:阿里云短信服务-设置Accesskey
参照官方提供的文档。
具体开发步骤:
1、导入maven坐标
2、调用API1
3、资料导入工具类
<!--短信发送 -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.5.16</version>
</dependency>
<dependency>
<groupId>com.liyun</groupId>
<artifactId>aliiyun-java-sdk-dysmsapi</artifactId>
<version>2.1.0</version>
</dependency>
2、手机验证码登录
2.1、交互过程
1、在登录页面(front/page/login.html)输入手机号,点击【获取验证码】按钮,页面发送ajax请求,在服务端调
用短信服务API给指定手机号发送验证码短信
2、在登录页面输入验证码,点击【登录】按钮,发送ajax请求,在服务端处理登录请求
开发手机验证码登录功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。
2.2准备
实体类User(直接从课程资料中导入即可)
Mapper接口UserMapper
业务层接口Userservice
业务层实现类Userservicelmpl
控制层Usercontroller
工具类SMSUtils、ValidatecodeUtils(直接从课程资料中导入即可)
2.3代码开发
代码开发-修改LoginCheckFilter
在LoginCheckFilter过滤器中扩展逻辑,判断移动端用户登录状态
//4-2、移动端登录 判断登录状态,如果已登录,则直接放行
// 第二步:在LoginCheckFilter的doFilter方法中调用BaseContext来设置当前登录用户的id
if (request.getSession().getAttribute("user") != null){
log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("user"));//获取id
//获取id
Long userId = (Long) request.getSession().getAttribute("user");
BaseContext.setCurrentId(userId);
filterChain.doFilter(request,response);
return;
}
log.info("用户未登录");
// 5、如果未登录则返回未登录结果通,过输出流方式向客户端页面响应数据
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
return;
}
登录校验
UserController
/**
* 移动端用户登录
* @param map
* @param session
* @return
*/
@PostMapping("/login")
//{phone:“13412345678“,code:“1234}
//①Dto继承实体类扩展一个属性
//②类型是kv 对 使用map
//登录成功给页面返回登录信息 泛型勇user
public R<User> sendMsg(@RequestBody Map map, HttpSession session){
log.info(map.toString());
//一、获取手机号
String phone = map.get("phone").toString();
//二、获取验证码
String code = map.get("code").toString();
//三、从Session中获取保存的验证码
Object codeInSession = session.getAttribute(phone);
//四、进行验证码的比对(页面提交的验证码和Session中保存的验证码比对)
if (codeInSession != null && codeInSession.equals(code)){
//如果能够比对成功,说明登录成功
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getPhone,phone);
User user = userService.getOne(queryWrapper);
if (user == null){
//判断当前(查询数据库有无)手机号对应的用户是否为新用户,如果是新用户就自动完成注册
user = new User();
user.setPhone(phone);
user.setStatus(1);//设置状态
userService.save(user);
}
//登录成功给页面返回登录信息 泛型勇user
return R.success(user);
}
return R.error("登录失败");
}
Ⅵ、菜品展示、购物车、下单
1、导入用户地址薄相关功能代码
实体类AddressBook(直接从课程资料中导入即可)
Mapper接口AddressBookMapper
业务层接口AddressBookservice
业务层实现类AddressBookservicelmp
控制层AddressBookcontroller(直接从课程资料中导入即可)
2、菜品展示
2.1、 交互过程
1、页面(front/index.html)发送ajax请求,获取分类数据(菜品分类和套餐分类)
2、页面发送ajax请求,获取第一个分类下的菜品或者套餐
开发菜品展示功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求
注意:首页加载完成后,还发送了一次ajax请求用于加载购物车数据,此处可以将这次请求的地址暂时修改一下,从静态json文件获取数据,等后续开发购物车功能时再修改回来,如下:
修改DisController的list方法并测试
移动端打开点击加1有口味选择
@GetMapping("/lsit")
public R<List<DishDto>> list(Dish dish){
//构造查询条件 泛型Dish
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
//添加条件
queryWrapper.eq(dish.getCategoryId() != null,Dish::getCategoryId,dish.getCategoryId());
//添加条件,查询状态为1(起售状态) 的菜品
queryWrapper.eq(Dish::getStatus,1);
// 添加排序条件, 根据实体类Dish类sort排序 desc降序排序
queryWrapper.orderByDesc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
//调用dishService 把queryWrapper分装成list类型
List<Dish> list = dishService.list(queryWrapper);
List<DishDto> dishDtoList = list.stream().map((item) -> {
DishDto dishDto = new DishDto();
//Dish实体类中其他属性都是空的,需要拷贝
BeanUtils.copyProperties(item,dishDto);
Long categoryId = item.getCategoryId();//分类id
//根据id查询分类对象
Category category = categoryService.getById(categoryId);
if (category != null) {
//获取分类名称 需要一个Dto并给Dto赋值
String categoryName = category.getName();
//需要一个Dto并给Dto赋值
dishDto.setCategoryName(categoryName);
// 此处categoryName包含菜品分类(categoryName)和其他普通属性
}
//查询口味
Long dishID = item.getId();//菜品iD
//根据菜品id查询口味
LambdaQueryWrapper<DishFlavor> lambdaQueryWrapper = new LambdaQueryWrapper<>();
//添加条件
lambdaQueryWrapper.eq(DishFlavor::getDishId,dishID);
//sQL:select * from dish_flavor where dish_id = ?
//查出口味的集合
List<DishFlavor> dishFlavorList = dishFlavorService.list(lambdaQueryWrapper);
dishDto.setFlavors(dishFlavorList);
return dishDto;
}).collect(Collectors.toList());
return R.success(dishDtoList);
}
创建SetmealController的list方法并测试
/**
* 根据条件查询套餐数据
* @param setmeal
* @return
*/
@GetMapping("/list")
public R<List<Setmeal>> list( Setmeal setmeal){
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(setmeal.getCategoryId() != null,Setmeal::getCategoryId,setmeal.getCategoryId());
queryWrapper.eq(setmeal.getStatus() != null,Setmeal::getStatus,setmeal.getStatus());
queryWrapper.orderByDesc(Setmeal::getUpdateTime);
List<Setmeal> list = setmealService.list(queryWrapper);
return R.success(list);
}
3、购物车
实体类Shoppingcart(直接从课程资料中导入即可)
Mapper接口ShoppingCartMapper
业务层接口Shoppingcartservice
业务层实现类ShoppingCartservicelmp
控制层Shoppingcartcontroller
添加购物车
ShoppingCartController
public class ShoppingCartController {
@Autowired
private ShoppingCartService shoppingCartService;
/**
* 添加购物车
* @param shoppingCart
* @return
*/
@PostMapping("/add")
public R<ShoppingCart> add(@RequestBody ShoppingCart shoppingCart){
log.info("购物车数据:{}",shoppingCart);
//设置用户id,指定当前是哪个用户的购物车数据
Long currentId = BaseContext.getCurrentId();
shoppingCart.setUserId(currentId);
Long dishId = shoppingCart.getDishId();
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId,currentId);
if(dishId != null){
//添加到购物车的是菜品
queryWrapper.eq(ShoppingCart::getDishId,dishId);
}else{
//添加到购物车的是套餐
queryWrapper.eq(ShoppingCart::getSetmealId,shoppingCart.getSetmealId());
}
//查询当前菜品或者套餐是否在购物车中
//SQL:select * from shopping_cart where user_id = ? and dish_id/setmeal_id = ?
ShoppingCart cartServiceOne = shoppingCartService.getOne(queryWrapper);
if(cartServiceOne != null){
//如果已经存在,就在原来数量基础上加一(更新操作)
Integer number = cartServiceOne.getNumber();
cartServiceOne.setNumber(number + 1);
shoppingCartService.updateById(cartServiceOne);
}else{
//如果不存在,则添加到购物车,数量默认就是一
shoppingCart.setNumber(1);
shoppingCart.setCreateTime(LocalDateTime.now());
shoppingCartService.save(shoppingCart);
cartServiceOne = shoppingCart;
}
return R.success(cartServiceOne);
}
查看、清空购物车
/**
* 查看购物车
* @return
*/
@GetMapping("/list")
public R<List<ShoppingCart>> list(){
log.info("查看购物车...");
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());
queryWrapper.orderByAsc(ShoppingCart::getCreateTime);
List<ShoppingCart> list = shoppingCartService.list(queryWrapper);
return R.success(list);
}
/**
* 清空购物车
* @return
*/
@DeleteMapping("/clean")
public R<String> clean(){
//SQL:delete from shopping_cart where user_id = ?
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());
shoppingCartService.remove(queryWrapper);
return R.success("清空购物车成功");
}
4、用户下单
去支付跳转页面
——————————
orders:订单表
order_detail:订单明细表
1、交互过程
2、准备
实体类Orders、OrderDetail(直接从课程资料中导入即可)
Mapper接口 OrderMapper、 OrderDetailMapper
业务层接口OrderService、OrderDetailservice
业务层实现类Orderservicelmpl、OrderDetaiSenicelmpl
控制层Ordercontroller、OrderDetailcontroller
OrderController
/**
* 订单
*/
@Slf4j
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private OrderService orderService;
/**
* 用户下单
* @param orders
* @return
*/
@PostMapping("/submit")
public R<String> submit(@RequestBody Orders orders){
log.info("订单数据:{}",orders);
orderService.submit(orders);
return R.success("下单成功");
}
OrderServiceImpl
@Service
@Slf4j
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Orders> implements OrderService {
@Autowired
private ShoppingCartService shoppingCartService;
@Autowired
private UserService userService;
@Autowired
private AddressBookservice addressBookService;
@Autowired
private OrderDetailService orderDetailService;
/**
* 用户下单
* @param orders
*/
@Transactional
public void submit(Orders orders) {
//获得当前用户id
Long userId = BaseContext.getCurrentId();
//查询当前用户的购物车数据
LambdaQueryWrapper<ShoppingCart> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(ShoppingCart::getUserId,userId);
List<ShoppingCart> shoppingCarts = shoppingCartService.list(wrapper);
if(shoppingCarts == null || shoppingCarts.size() == 0){
throw new CustomException("购物车为空,不能下单");
}
//查询用户数据
User user = userService.getById(userId);
//查询地址数据
Long addressBookId = orders.getAddressBookId();
AddressBook addressBook = addressBookService.getById(addressBookId);
if(addressBook == null){
throw new CustomException("用户地址信息有误,不能下单");
}
long orderId = IdWorker.getId();//订单号
AtomicInteger amount = new AtomicInteger(0);
List<OrderDetail> orderDetails = shoppingCarts.stream().map((item) -> {
OrderDetail orderDetail = new OrderDetail();
orderDetail.setOrderId(orderId);
orderDetail.setNumber(item.getNumber());
orderDetail.setDishFlavor(item.getDishFlavor());
orderDetail.setDishId(item.getDishId());
orderDetail.setSetmealId(item.getSetmealId());
orderDetail.setName(item.getName());
orderDetail.setImage(item.getImage());
orderDetail.setAmount(item.getAmount());
amount.addAndGet(item.getAmount().multiply(new BigDecimal(item.getNumber())).intValue());
return orderDetail;
}).collect(Collectors.toList());
orders.setId(orderId);
orders.setOrderTime(LocalDateTime.now());
orders.setCheckoutTime(LocalDateTime.now());
orders.setStatus(2);
orders.setAmount(new BigDecimal(amount.get()));//总金额
orders.setUserId(userId);
orders.setNumber(String.valueOf(orderId));
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()));
//向订单表插入数据,一条数据
this.save(orders);
//向订单明细表插入数据,多条数据
orderDetailService.saveBatch(orderDetails);
//清空购物车数据
shoppingCartService.remove(wrapper);
}
Ⅶ、缓存优化
用户数量多,系统访问量大
频繁访问数据库,系统性能下降,用户体验差
1、环境搭建
1、maven坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2、配置文件
在项目的application.yml中加入redis相关配置:
spring:
redis:
host:127.0.0.1
port: 6379
password: root
database: 0
3、配置类
在项自中加入配置类Redisconfig
便于观察redis中的key
默认是另一种序列化方式
1、缓存短信验证码
session有效期是30分钟
1、1实现思路
1、2、代码改造
Controller层UserController
①注入
> // 1、在服务端Usercontroller中注入RedisTemplate对象,用于操作Redis
@Autowired
private RedisTemplate redisTemplate;
②发送手机短信验证码方法
//第四步:需要将生成的验证码保存到Session
// session.setAttribute(phone,code);
//将生成的验证码缓存到Redis中,并且设置有效期为5分钟
//object key,Object value,Duration timeout)
redisTemplate.opsForValue().set(phone,code,5, TimeUnit.MINUTES);
③移动端用户登录获取验证码&成功删除验证码
//三、从Session中获取保存的验证码
// Object codeInSession = session.getAttribute(phone);
//从Redis中获取缓存的验证码
Object codeInSession=redisTemplate.opsForValue().get(phone);
(略)
//如果用户登录成功,删除Redis中缓存的验证码
redisTemplate.delete(phone);
2、缓存菜品数据
频繁查询数据,数据库性能下降
1、改造Dishcontroller的list方法,先从Redis中获取菜品数据,如果有则直接返回,无需查询数据库;如果没有则查询数据库,并将查询到的菜品数据放入Redis。
2、改造Dishcontroller的save和update方法,加入清理缓存的逻辑
注意:在使用缓存过程中,要注意保证数据库中的数据和缓存中的数据一致,如果数据库中的数据发生变化,需要及时清理缓存数据。
代码改造
DishController层
@Autowired
private RedisTemplate redisTemplate;
list方法
@GetMapping("/lsit")
public R<List<DishDto>> list(Dish dish){
List<DishDto> dishDtoList = null;
//动态构造key查询现根据菜品(分类 川 粤)和分类下的菜品,请求数据有分类 id 状态
//dish_135456454553_1
String key = "dish_" + dish.getCategoryId() + "_" + dish.getStatus();
//先从redis中获取缓存数据 强转
dishDtoList =(List<DishDto>) redisTemplate.opsForValue().get(key);
if (dishDtoList != null){
//如果存在,直接返回,无需查询数据库
return R.success(dishDtoList);
}
//构造查询条件 泛型Dish
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
//添加条件
queryWrapper.eq(dish.getCategoryId() != null,Dish::getCategoryId,dish.getCategoryId());
//添加条件,查询状态为1(起售状态) 的菜品
queryWrapper.eq(Dish::getStatus,1);
// 添加排序条件, 根据实体类Dish类sort排序 desc降序排序
queryWrapper.orderByDesc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
//调用dishService 把queryWrapper分装成list类型
List<Dish> list = dishService.list(queryWrapper);
dishDtoList = list.stream().map((item) -> {
DishDto dishDto = new DishDto();
//Dish实体类中其他属性都是空的,需要拷贝
BeanUtils.copyProperties(item,dishDto);
Long categoryId = item.getCategoryId();//分类id
//根据id查询分类对象
Category category = categoryService.getById(categoryId);
if (category != null) {
//获取分类名称 需要一个Dto并给Dto赋值
String categoryName = category.getName();
//需要一个Dto并给Dto赋值
dishDto.setCategoryName(categoryName);
// 此处categoryName包含菜品分类(categoryName)和其他普通属性
}
//查询口味
Long dishID = item.getId();//菜品iD
//根据菜品id查询口味
LambdaQueryWrapper<DishFlavor> lambdaQueryWrapper = new LambdaQueryWrapper<>();
//添加条件
lambdaQueryWrapper.eq(DishFlavor::getDishId,dishID);
//sQL:select * from dish_flavor where dish_id = ?
//查出口味的集合
List<DishFlavor> dishFlavorList = dishFlavorService.list(lambdaQueryWrapper);
dishDto.setFlavors(dishFlavorList);
return dishDto;
}).collect(Collectors.toList());
//如果不存在,需要查询数据库,将查询到的菜品数据缓存到Redis,1h过期
redisTemplate.opsForValue().set(key,dishDtoList,60, TimeUnit.MINUTES);
return R.success(dishDtoList);
}
Spring Cache
缓存套餐数据