目录
业务开发Day1
业务开发Day2
业务开发Day3
业务开发Day4
业务开发Day5
业务开发Day6
业务开发Day1
01-本章内容介绍
项目效果展示
登录界面
登录成功界面
管理界面展示
前端开发使用HTML5技术(自适应屏幕大小功能)
目录
02-软件开发整体介绍
软件开发流程
需求分析->设计->编码->测试->上线运维
角色分工
- 项目经理
- 产品经理
- UI设计师
- 架构师
- 开发工程师
- 测试工程师
- 运维工程师
软件环境
03-瑞吉外卖项目整体介绍
目录
- 项目介绍
- 产品原型展示
- 技术选型
- 功能架构
- 角色
项目介绍
本项目是专门为餐饮企业(餐厅、饭店)定制的一款软件产品,包括系统管理后台和移动端应用两部分。
- 系统管理后台主要提功给餐饮企业内部员工使用(功能:对餐厅的菜品、套餐、订单等进行管理维护等)
- 移动端应用主要提供给消费者使用(功能:在线浏览菜品、添加购物车、下单等)
产品原型展示
技术选型
- 用户层
- 网关层
- 应用层
- 数据层
- 以及使用到的工具
功能架构
- 移动端前台
- 系统管理后台
角色
- 后台系统管理员
- 后台系统普通员工
- C端用户
04-开发环境搭建-数据库环境搭建
- 使用navicat(数据库可视化界面)创建对应的数据库,数据库名:reggie,字符集:utf8mb4
- 操作步骤
- 第一步:右键点击数据库再点击运行sql文件
- 第二步:选择资料下载的位置,我的资料存储在D:\瑞吉外卖\资料\数据模型\db_reggie.sql,点击开始即可
- 第三步:打开表,即可查看数据库中执行完sql文件的所有信息
执行完sql文件对应的对象信息
05-开发环境搭建-maven项目环境搭建
创建maven项目
- 点击next
- 填写好项目相关信息后,点击finish
改pom文件
<?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> <groupId>com.itzq</groupId> <artifactId>reggie_take_out</artifactId> <version>1.0-SNAPSHOT</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.4.5</version> <relativePath/> </parent> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </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> 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
|
写YML
server: port: 8080 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: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: ASSIGN_ID 12345678910111213141516171819202122
|
主启动
在java下新建主启动类,带上包名
package com.itzq.reggie;
import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
@Slf4j @SpringBootApplication public class ReggieApplication { public static void main(String[] args) { SpringApplication.run(ReggieApplication.class,args); log.info("项目启动成功。。。"); } } 1234567891011121314
|
将前端资源放入resources下
在磁盘中找到前端资源下的目录
将目录放入resources下
启动工程
项目启动成功
访问路径 http://localhost:8080/backend/index.html ,出现无法访问,默认情况下我们只能访问static、template静态目录下的静态资源,此时我们可以通过配置类的方式来设置静态资源映射
配置类设置静态资源映射
通过继承WebMvcConfigurationSupport,重写addResourceHandlers方法来实现我们想要的功能
package com.itzq.reggie.config;
import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
@Slf4j @Configuration public class WebMvcConfig extends WebMvcConfigurationSupport {
@Override protected void addResourceHandlers(ResourceHandlerRegistry registry) { log.info("开始进行静态资源映射。。。"); registry.addResourceHandler("/backend/**") .addResourceLocations("classpath:/backend/"); registry.addResourceHandler("/front/**") .addResourceLocations("classpath:/front/"); } } 12345678910111213141516171819202122232425
|
重新启动工程!
浏览器访问地址:http://localhost:8080/backend/index.html,出现以下界面即配置成功,页面中请求出错是因为前端发送了一个ajax请求,而后端还没有写东西所以页面提示请求出错
06-后台系统登录功能-需求分析
需求分析
登录页面展示
访问路径:http://localhost:8080/backend/page/login/login.html
数据库的密码通过MD5加密了,它的明文密码为123456
输入正确的用户名和密码点击登录,按住f12,寻找到以下页面,可以看到请求路径跳转到地址为:localhost:8080/employee/login的页面,报404错误,因为后台系统还没有响应此请求的处理器,我们需要创建相关类来处理登录请求
07-后台系统登录功能-代码开发(创建controller,service,mapper,实体类)
代码开发
创建controller,service,mapper,实体类
- 在reggie包下分别创建controller,service(在此包下再创建一个impl包),mapper,entity包
- 在entity包下创建Employee类
package com.itzq.reggie.entity;
import com.baomidou.mybatisplus.annotation.FieldFill; import com.baomidou.mybatisplus.annotation.TableField; import lombok.Data; import java.io.Serializable; import java.time.LocalDateTime;
@Data public class Employee implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String username;
private String name;
private String password;
private String phone;
private String sex;
private String idNumber;
private Integer status;
private LocalDateTime createTime;
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT) private Long createUser;
@TableField(fill = FieldFill.INSERT_UPDATE) private Long updateUser;
} 12345678910111213141516171819202122232425262728293031323334353637383940
|
- 在mapper包下创建EmployeeMapper接口,并继承BaseMapper,添加@Mapper注解在该接口上
package com.itzq.reggie.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.itzq.reggie.entity.Employee; import org.apache.ibatis.annotations.Mapper;
@Mapper public interface EmployeeMapper extends BaseMapper<Employee> { }
12345678910
|
- 在service包下编写EmployeeService接口,并继承IService
package com.itzq.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.itzq.reggie.entity.Employee;
public interface EmployeeService extends IService<Employee> { }
12345678
|
- 在Impl包下编写EmployeeServiceImpl类,并继承ServiceImpl类,实现EmployeeService接口
package com.itzq.reggie.service.Impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.itzq.reggie.entity.Employee; import com.itzq.reggie.mapper.EmployeeMapper; import com.itzq.reggie.service.EmployeeService; import org.springframework.stereotype.Service;
@Service public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements EmployeeService { }
123456789101112
|
- 在controller包下编写EmployeeController类
package com.itzq.reggie.controller;
import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;
@Slf4j @RestController @RequestMapping("/employee") public class EmployeeController {
@Autowired private EmployeeService employeeService;
}
1234567891011121314151617
|
08-后台系统登录功能-代码开发(导入通用返回结果类)
导入通用结果类
在reggie包下创建一个子包为common,再在common包下创建一个R类
package com.itzq.reggie.common;
import lombok.Data; import java.util.HashMap; import java.util.Map;
@Data public class R<T> {
private Integer code;
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; }
}
123456789101112131415161718192021222324252627282930313233343536373839404142
|
09-后台系统登录功能-代码开发(梳理登录方法处理逻辑)
代码开发以及梳理
给EmployeeController类添加一个login方法
- @RequestBody 主要用于接收前端传递给后端的json字符串(请求体中的数据)
- HttpServletRequest request作用:如果登录成功,将员工对应的id存到session一份,这样想获取一份登录用户的信息就可以随时获取出来
package com.itzq.reggie.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.itzq.reggie.common.R; import com.itzq.reggie.entity.Employee; import com.itzq.reggie.service.EmployeeService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.util.DigestUtils; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
@Slf4j @RestController @RequestMapping("/employee") public class EmployeeController {
@Autowired private EmployeeService employeeService;
@PostMapping("/login") public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee){ return null; } }
123456789101112131415161718192021222324252627282930313233343536373839
|
在controller中创建登录方法
- 将页面提交的密码进行MD5加密处理
- 根据页面提交的用户名username查询数据库
- 如果没有查询到则返回登录失败结果
- 密码比对,如果不一致则返回登录结果
- 查看员工状态,如果已为禁用状态,则返回员工已禁用结果
- 登录成功,将员工id存入session并返回登录成功结果
10-后台系统登录功能-代码开发(实现登录处理逻辑)
编写代码实现逻辑
在EmployeeController类的login方法中添加代码实现登录处理逻辑
package com.itzq.reggie.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.itzq.reggie.common.R; import com.itzq.reggie.entity.Employee; import com.itzq.reggie.service.EmployeeService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.util.DigestUtils; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
@Slf4j @RestController @RequestMapping("/employee") public class EmployeeController {
@Autowired private EmployeeService employeeService;
@PostMapping("/login") public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee){ String password = employee.getPassword(); password= DigestUtils.md5DigestAsHex(password.getBytes());
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(Employee::getUsername,employee.getUsername()); Employee emp = employeeService.getOne(queryWrapper);
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);
}
@PostMapping("/logout") public R<String> logout(HttpServletRequest request){ request.getSession().removeAttribute("employee"); return R.success("退出成功"); } }
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
|
11-后台系统登录功能-代码开发(功能测试)
测试
通过debug方式启动项目,在箭头指向位置添加断点
在浏览器上访问地址:http://localhost:8080/backend/page/login/login.html
- 输入正确的用户名和密码点击登录
- 在debug调试期间用时较长,前端在10s内得不到响应则会抛出异常,
- 为了我们可以在后端进行长时间的调试,我们需要重新设置前端页面的响应超时时间找到request.js
将timeout后面的数据多添加两个0,并且清除浏览器带有的缓存
清除浏览器缓存
测试
- 输入错误的username,页面返回登录失败的信息
- 输入错误的密码,页面返回登录失败的信息
- 在数据库中将员工的status改为0,表示该员工处于被禁用状态,页面返回登录失败的信息
12-后台系统退出功能_需求分析&代码开发&功能测试
功能测试
在EmployeeController类中添加logout方法
@PostMapping("/logout") public R<String> logout(HttpServletRequest request){ request.getSession().removeAttribute("employee"); return R.success("退出成功"); } 1234567891011
|
登录成功时
退出登录后
13-分析后台系统首页构成和效果展示方式
展示效果以及分析
在index.html页面下的menuList作为数据的准备
遍历menuList的代码块,里面各个标注对应的重要含义
- 遍历menuList
- v-if,根据提供的menuList可知并不满足条件
- 标签名:通过name属性定义
- 如果v-if不满足条件,则通过v-else
- 点击菜单,会执行menuHandle方法
menuHandle方法最重要的是红框的语句
定义了一个iframe,用于展示另一个页面,这个页面从哪来?传给我什么数据,我就展示什么数据
那为什么登录成功后就是员工管理界面,因为在html中设置了初始值,当我们在点击菜单的时候,其实就是在切换url,展示一个新的界面
在html页面设置的初始界面
业务开发Day2
01-本章内容介绍
目录
针对员工这张表进行对数据的维护
点击添加员工后呈现的页面
在浏览器地址栏中输入地址:http://localhost:8080/backend/index.html ,在没有登录的情况下也可以进入管理界面,但最终我们想看到的效果是没有登录,就跳转到登录界面来,登录成功之后才能进入到管理界面
02-完善登录功能_问题分析并创建过滤器
问题分析
- 前面我们已经完成了后台系统的员工登录功能开发,但是还存在一个问题:用户如果不登录,直接访问系统首页面,照样可以正常访问。
- 这种设计并不合理,我们希望看到的效果应该是,只有登录成功后才可以访问系统中的页面,如果没有登录则跳转到登录页面。
- 那么,具体应该怎么实现呢? 答案就是使用过滤器或者拦截器,在过滤器或者拦截器中判断用户是否已经完成登录,如果没有登录则跳转到登录页面。
代码实现
- 创建自定义过滤器LoginCheckFilter
package com.itzq.reggie.filter;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException;
@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*") @Slf4j public class LoginCheckFilter implements Filter {
@Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; log.info("拦截到请求:{}",request.getRequestURI()); filterChain.doFilter(request,response); } }
123456789101112131415161718192021222324
|
- 在启动类上加入注解@ServletComponentScan
package com.itzq.reggie;
import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.web.servlet.ServletComponentScan;
@Slf4j @SpringBootApplication @ServletComponentScan public class ReggieApplication { public static void main(String[] args) { SpringApplication.run(ReggieApplication.class,args); log.info("项目启动成功。。。"); } } 12345678910111213141516
|
- 完善过滤器的处理逻辑
完善过滤器内容在下一章节
测试
在浏览器上访问:http://localhost:8080/backend/index.html ,控制台的呈现
03-完善登录功能_代码开发
处理逻辑
- 获取本次请求的URI
- .判断本次请求是否需要处理
- 如果不需要处理,则直接放行
- 判断登录状态,如果已登录,则直接放行
- 如果未登录则返回未登录结果
完善LoginCheckFilter过滤器代码
package com.itzq.reggie.filter;
import com.alibaba.fastjson.JSON; import com.itzq.reggie.common.R; import lombok.extern.slf4j.Slf4j; import org.springframework.util.AntPathMatcher;
import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException;
@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;
String requestURI = request.getRequestURI();
String[] urls = new String[]{ "/employee/login", "/employee/logout", "/backend/**", "/front/**" };
boolean check = check(urls, requestURI);
if (check) { filterChain.doFilter(request,response); return; }
if (request.getSession().getAttribute("employee") != null) { filterChain.doFilter(request,response); return; }
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN"))); return;
}
public boolean check(String[] urls, String requestURI){ for (String url : urls) { boolean match = PATH_MATCHER.match(url, requestURI); if (match) { return true; } } return false; } }
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
|
可以在index.html页面找到红方框下的信息
寻找到request.js代码中的响应拦截器部分
- 服务端若传递的code=0,msg=NOTLOGIN
- 则返回到登录页面
04-完善登录功能_功能测试
在LoginCheckFilter类中加入日志
作用:方便观察
package com.itzq.reggie.filter;
import com.alibaba.fastjson.JSON; import com.itzq.reggie.common.R; import lombok.extern.slf4j.Slf4j; import org.springframework.util.AntPathMatcher;
import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException;
@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;
String requestURI = request.getRequestURI(); log.info("拦截到的请求:{}",requestURI);
String[] urls = new String[]{ "/employee/login", "/employee/logout", "/backend/**", "/front/**" };
boolean check = check(urls, requestURI);
if (check) { log.info("本次请求:{},不需要处理",requestURI); filterChain.doFilter(request,response); return; }
if (request.getSession().getAttribute("employee") != null) { log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("employee")); filterChain.doFilter(request,response); return; }
log.info("用户未登录"); response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN"))); return;
}
public boolean check(String[] urls, String requestURI){ for (String url : urls) { boolean match = PATH_MATCHER.match(url, requestURI); if (match) { return true; } } return false; } }
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
|
测试
- 启动项目
- 在浏览器的地址栏中输入:http://localhost:8080/backend/index.html ,会发现跳转到了登录界面
- 控制台打印日志
- 在浏览器的地址栏中输入:http://localhost:8080/backend/page/login/login.html ,点击登录
- 控制台打印日志
05-新增员工_需求分析和数据模型
需求分析
- 新增员工,其实就是将我们新增页面录入的员工数据插入到employee表。
- 需要注意,employee表中对username字段加入了唯一约束,因为username是员工的登录账号,必须是唯一的
- employee表中的status字段已经设置了默认值1,表示状态正常。
06-新增员工_梳理程序执行流程
在开发代码之前,需要梳理一下整个程序的执行过程:
- 页面发送ajax请求,将新增员工页面中输入的数据以json的形式提交到服务端
- 服务端Controller接收页面提交的数据并调用Service将数据进行保存
- Service调用Mapper操作数据库,保存数据
前端js代码对页面进行校验(validate)
07-新增员工_代码开发和功能测试
代码开发和功能测试
在EmployeeController类中添加save方法用于将前端传来的json数据保存到数据库中
@PostMapping public R<String> save(@RequestBody Employee employee){ log.info("新增员工的信息:{}",employee.toString()); return R.success("添加员工成功"); } 12345
|
启动项目后,输入员工信息,点击保存
保存后,可以看到控制台上打印的数据,表示可以接收到前端传递到服务端的数据
在save方法中新增业务逻辑类代码,实现将数据存入数据库操作
@PostMapping public R<String> save(HttpServletRequest request, @RequestBody Employee employee){ log.info("新增员工的信息:{}",employee.toString()); employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));
employee.setCreateTime(LocalDateTime.now()); employee.setUpdateTime(LocalDateTime.now());
Long empID = (Long)request.getSession().getAttribute("employee");
employee.setCreateUser(empID); employee.setUpdateUser(empID);
employeeService.save(employee); return R.success("添加员工成功"); } 123456789101112131415161718
|
重启项目,查看employee表中的内容
输入信息,点击保存
再次查看employee表中数据库内容,发现新增加了我们添加的员工信息
再次添加相同的username(员工账号)
出现异常,因为在上面已经说过username(员工账号)的索引类型为unique,所以不能添加数据库中已存在的username
08-新增员工_编写全局异常处理器
全局异常处理器
前面的程序还存在一个问题,就是当我们在新增员工时输入的账号已经存在,由于employee表中对该字段加入了唯一约束,此时程序会抛出异常:
java.sql. SQLIntegrityConstraintViolationException: Duplicate entry ‘zhangsan’for key ‘idx_username
此时需要我们的程序进行异常捕获,通常有两种处理方式:
- 在Controller方法中加入try、catch进行异常捕获
- 使用异常处理器进行全局异常捕获(推荐使用第二种方式)
在common包下,建立GlobalExceptionHandler类,并添加exceptionHandler方法用来捕获异常,并返回结果
package com.itzq.reggie.common;
import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController;
import java.sql.SQLIntegrityConstraintViolationException;
@ControllerAdvice(annotations = {RestController.class, Controller.class}) @ResponseBody @Slf4j public class GlobalExceptionHandler {
@ExceptionHandler(SQLIntegrityConstraintViolationException.class) public R<String> exceptionHandler (SQLIntegrityConstraintViolationException exception){ log.error(exception.getMessage()); return R.error("failed"); } } 12345678910111213141516171819202122232425
|
启动项目,输入数据库已存在的username员工信息
页面上弹出方框中文字,表示方法成功执行
09-新增员工_完善全局异常处理器并测试
完善全局异常处理器并测试
完善GlobalExceptionHandler类中的exceptionHandler方法
package com.itzq.reggie.common;
import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController;
import java.sql.SQLIntegrityConstraintViolationException;
@ControllerAdvice(annotations = {RestController.class, Controller.class}) @ResponseBody @Slf4j public class GlobalExceptionHandler {
@ExceptionHandler(SQLIntegrityConstraintViolationException.class) public R<String> exceptionHandler (SQLIntegrityConstraintViolationException exception){ log.error(exception.getMessage()); if (exception.getMessage().contains("Duplicate entry")){ String[] split = exception.getMessage().split(" "); String msg = split[2] + "已存在"; return R.error(msg); } return R.error("unknown error"); } }
12345678910111213141516171819202122232425262728293031
|
重启项目,输入数据库已存在的username员工信息
页面上弹出方框中文字,表示方法成功执行
10-新增员工_小节
总结
- 根据产品原型明确业务需求
- 重点分析数据的流转过程和数据格式
- 通过debug断点调试跟踪程序执行过程
11-员工信息分页查询_需求分析
需求分析
系统中的员工很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。
12-员工信息分页查询_梳理程序执行流程
程序执行流程
在开发代码之前,需要梳理一下整个程序的执行过程:
- 页面发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端
- 服务端Controller接收页面提交的数据并调用Service查询数据
- Service调用Mapper操作数据库,查询分页数据
- Controller将查询到的分页数据响应给页面
- 页面接收到分页数据并通过ElementUI的Table组件展示到页面上
13-员工信息分页查询_代码开发1
配置MP分页插件
package com.itzq.reggie.config;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration;
@Configuration public class MybatisPlusConfig {
@Bean public MybatisPlusInterceptor mybatisPlusInterceptor(){ MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor()); return mybatisPlusInterceptor; } }
123456789101112131415161718192021
|
分析前端代码
返回值携带records和total
点击员工管理,前端页面发送的请求
在搜索框中输入员工姓名,前端页面发送的请求
相关代码
在EmployeeController类中,编写page方法
@GetMapping("/page") public R<Page> page(int page,int pageSize,int name){ log.info("page = {}, pageSize = {}, name = {}",page,pageSize,name); return null; }
123456
|
点击员工管理,控制台输出的数据
在搜索框中添加数据,点击搜索,控制台输出的数据
14-员工信息分页查询_代码开发2
代码中添加功能
在page方法中添加代码实现分页查询功能
@GetMapping("/page") public R<Page> page(int page,int pageSize,String name){ log.info("page = {}, pageSize = {}, name = {}",page,pageSize,name);
Page pageInfo = new Page(page, pageSize);
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.like(StringUtils.isNotEmpty(name),Employee::getName,name);
queryWrapper.orderByDesc(Employee::getUpdateTime);
employeeService.page(pageInfo,queryWrapper);
return R.success(pageInfo); } 123456789101112131415161718192021
|
15-员工信息分页查询_功能测试
测试
- 启动项目
- 登录管理界面 http://localhost:8080/backend/page/login/login.html
- 将会呈现以下界面
搜索框中输入:张
因为数据量有限,所以修改前端代码为每页两条数据,共两页
在list.html修改前端代码
刷新后的页面
16-员工信息分页查询_补充说明
说明
为什么后端传给页面的status数据为Integer类型,到页面展示效果的时候显示的是已禁用或者正常?
查看到前端list.html页面中的逻辑代码
由以上前端代码所表示的逻辑可知,当status为1时表示正常,为0时表示已禁用
17-启用、禁用员工账号_需求分析
需求分析
- 在员工管理列表页面,可以对某个员工账号进行启用或者禁用操作。账号禁用的员工不能登录系统,启用后的员工可以正常登录。
- 需要注意,只有管理员(admin用户)可以对其他普通用户进行启用、禁用操作,所以普通用户登录系统后启用、禁用按钮不显示。
- 管理员admin登录系统可以对所有员工账号进行启用、禁用操作。
- 如果某个员工账号状态为正常,则按钮显示为“禁用”,如果员工账号状态为已禁用,则按钮显示为“启用”
18-启用、禁用员工账号_分析页面按钮动态显示效果
分析
页面中是怎样做到只有管理员(admin)才能看到启用,禁用按钮?
- 非管理员看不见该按钮
从userInfo中取出username,并赋值给user模型数据
判断模型数据user的值是否等于admin,若等于则显示按钮
19-启用、禁用员工账号_分析页面ajax请求发送过程
分析
在开发代码之前,需要梳理一下整个程序的执行过程:
- 页面发送ajax请求,将参数(id、status)提交到服务端
- 服务端Controller接收页面提交的数据并调用Service更新数据
- Service调用Mapper操作数据库
点击禁用或启用按钮会调用以下方法,该方法会调用enable0rDisableEmployee方法,该方法封装到一个js文件当中,来发送ajax请求
enable0rDisableEmployee方法
20-启用、禁用员工账号_代码开发和功能测试
代码开发和功能测试
启用、禁用员工账号,本质上就是一个更新操作,也就是对status状态字段进行操作在Controller中创建update方法,此方法是一个通用的修改员工信息的方法
在EmployeeController类中添加update方法
@PutMapping public R<String> update(@RequestBody Employee employee){ log.info(employee.toString()); return null; }
123456
|
启动项目,控制台上输出日志信息
测试成功
完善update方法的代码逻辑
@PutMapping public R<String> update(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("员工修改信息成功"); } 12345678910
|
重启项目,点击禁用账号为zhangsi的信息员工信息
页面提示
查看数据表中的信息,发现status字段的值并没有发生改变
测试过程中没有报错,但是功能并没有实现,查看数据库中的数据也没有变化。观察控制台输出的SQL
SQL执行的结果是更新的数据行数为0,仔细观察id的值,和数据库中对应记录的id值并不相同
问题的原因:
- 即js对long型数据进行处理时丢失精度,导致提交的id和数据库中的id不一致。
如何解决这个问题?
- 我们可以在服务端给页面响应json数据时进行处理,将long型数据统一转为String字符串
21-启用、禁用员工账号_代码修复配置状态转换器
配置状态转换器
具体实现步骤:
- 提供对象转换器Jackson0bjectMapper,基于Jackson进行Java对象到json数据的转换(资料中已经提供,直接复制到项目中使用)
- 在WebMcConfig配置类中扩展Spring mvc的消息转换器,在此消息转换器中使用提供的对象转换器进行Java对象到json数据的转换
配置对象映射器JacksonObjectMapper ,继承ObjectMapper
package com.itzq.reggie.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;
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); } }
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
|
扩展mvc框架的消息转换器
@Override protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter(); messageConverter.setObjectMapper(new JacksonObjectMapper()); converters.add(0,messageConverter);
} 123456789101112131415
|
22-启用、禁用员工账号_再次测试
测试
启动项目,点击禁用张四
数据库中的status字段数据发生了改变,即成功
23-编辑员工信息_需求分析和梳理程序执行流程
需求分析
在员工管理列表页面点击编辑按钮,跳转到编辑页面,在编辑页面回显员工信息并进行修改,最后点击保存按钮完成编辑操作
流程
在开发代码之前需要梳理一下操作过程和对应的程序的执行流程:
- 点击编辑按钮时,页面跳转到add.html,并在url中携带参数[员工id]
- 在add.html页面获取url中的参数[员工id]
- 发送ajax请求,请求服务端,同时提交员工id参数
- 服务端接收请求,根据员工id查询员工信息,将员工信息以json形式响应给页面
- 页面接收服务端响应的json数据,通过VUE的数据绑定进行员工信息回显
- 点击保存按钮,发送ajax请求,将页面中的员工信息以json方式提交给服务端
- 服务端接收员工信息,并进行处理,完成后给页面响应
- 页面接收到服务端响应信息后进行相应处理
24-编辑员工信息_页面效果分析和代码开发
页面分析
调用requestUrlParam方法获取id的值
requestUrlParam方法的具体实现
取出浏览器中id值,前端调用queryEmployeeById方法,向服务器发送ajax请求,查询包含该id的所有信息,若code为1,则表示在数据库中查询到该id所包含的员工信息,反之则为null
代码开发
在EmployeeController类中添加方法getById
@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("没有查询到该员工信息"); } 12345678910
|
25-编辑员工信息_功能测试
测试
启动项目,点击任意一个员工的编辑按钮,按住f12,观察浏览器发送的请求
页面数据回显成功
对性别(radio)的简单处理
因为在页面上的显示效果为男、女,而服务端传递过来的数据为1、0所以我们要将数据进行简单的处理
业务开发Day3
01-本章内容介绍
内容
- 公共字段自动填充
- 新增分类
- 分类信息分页查询
- 删除分类
- 修改分类
02-公共字段自动填充-内容分析
问题分析
前面我们已经完成了后台系统的员工管理功能开发,在新增员工时需要设置创建时间、创建人、修改时间、修改人等字段,在编辑员工时需要设置修改时间和修改人等字段。这些字段属于公共字段,也就是很多表中都有这些字段,如下:
表中公共字段
能不能对于这些公共字段在某个地方统一处理,来简化开发呢?
答案就是使用**Mybatis Plus**提供的公共字段自动填充功能。 1
|
03-公共字段自动填充-代码实现并测试
代码实现
Mybatis Plus公共字段自动填充,也就是在插入或者更新的时候为指定字段赋予指定的值,使用它的好处就是可以统一对这些字段进行处理,避免了重复代码。
实现步骤:
- 在实体类的属性上加入@TableField注解,指定自动填充的策略
- 按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现MetaObjectHandler接口
在公共属性上添加@TableField注解
- @TableField(fill = FieldFill.INSERT),表示插入时填充字段
- @TableField(fill = FieldFill.INSERT_UPDATE),表示插入和更新时填充字段
在common包下,自定义元数据对象处理器(测试版)
package com.itzq.reggie.common;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.reflection.MetaObject; import org.springframework.stereotype.Component;
@Component @Slf4j public class MyMetaObjectHandler implements MetaObjectHandler {
@Override public void insertFill(MetaObject metaObject) { log.info("公共字段自动填充【insert】"); log.info(metaObject.toString()); }
@Override public void updateFill(MetaObject metaObject) { log.info("公共字段自动填充【update】"); log.info(metaObject.toString()); } } 1234567891011121314151617181920212223
|
在该行添加断点,debug方式启动项目
来到修改员工界面,不需要修改数据,点击保存
发现前端传到服务端的数据,updateUser与updateTime数据在controller中的对应方法中被修改
注释掉save方法的部分代码
修改MyMetaObjectHandler类中部分代码
- 先将createUser,updateUser属性的初始值设置为1,后面会讲解如何获取当前user
package com.itzq.reggie.common;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.reflection.MetaObject; import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component @Slf4j public class MyMetaObjectHandler implements MetaObjectHandler {
@Override public void insertFill(MetaObject metaObject) { log.info("公共字段自动填充【insert】"); log.info(metaObject.toString()); metaObject.setValue("createTime", LocalDateTime.now()); metaObject.setValue("updateTime", LocalDateTime.now()); metaObject.setValue("createUser", new Long(1)); metaObject.setValue("updateUser", new Long(1)); }
@Override public void updateFill(MetaObject metaObject) { log.info("公共字段自动填充【update】"); log.info(metaObject.toString()); } }
123456789101112131415161718192021222324252627282930
|
在insertFill方法上添加断点
debug方式重启项目
来到添加员工页面,输入数据,点击保存
断点走到insertFill方法末尾时,将初始为null的四个公共属性赋值成功
修改员工信息时也需要填充
对应代码
@Override public void updateFill(MetaObject metaObject) { log.info("公共字段自动填充【update】"); log.info(metaObject.toString()); metaObject.setValue("updateTime", LocalDateTime.now()); metaObject.setValue("updateUser", new Long(1)); } 1234567
|
注释掉处理公共属性的代码
在该行添加断点,并以debug方式重启项目
断点在updateFill方法执行之前
断点在updateFill方法执行之后,发现数据被修改,表示成功
04-公共字段自动填充-功能完善
功能完善
在学习ThreadLocal之前,我们需要先确认一个事情,就是客户端发送的每次http请求,对应的在服务端都会分配一个新的线程来处理,在处理过程中涉及到下面类中的方法都属于相同的一个线程:
- LoginCheckFilter的doFilter方法
- EmployeeController的update方法
- MyMetaObjectHandler的updateFill方法
可以在上面的三个方法中分别加入下面代码(获取当前线程id) :
\4. long id = Thread.currentThread().getId();
\5. log.info(“MyMetaObjectHandler线程id为:{}”,id);
LoginCheckFilter的doFilter方法
EmployeeController的update方法
MyMetaObjectHandler的updateFill方法
启动项目,在修改员工的页面中,点击保存
控制台上显示三条日志信息,表明类中的方法都属于相同的一个线程
什么是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)。
代码实现
在common包下添加BaseContext类
作用:基于ThreadLocal封装工具类,用于保护和获取当前用户id
package com.itzq.reggie.common;
public class BaseContext {
private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
public static void setCurrentId(Long id){ threadLocal.set(id); }
public static Long getCurrentId(){ return threadLocal.get(); } }
123456789101112131415
|
在LoginCheckFilter类中添加代码
在MyMetaObjectHandler类中,修改部分代码
重启项目,登录一个新的员工用户
来到新增员工界面,输入相应的信息,点击保存
发现以下字段添加成功,同理根据代码逻辑推断,更新用户操作会更新表中的updateTime,updateUser字段
05-新增分类-需求分析&数据模型&代码开发&功能测试
需求分析
- 后台系统中可以管理分类信息,分类包括两种类型,分别是菜品分类和套餐分类
- 当我们在后台系统中添加菜品时需要选择一个菜品分类
- 当我们在后台系统中添加一个套餐时需要选择一个套餐分类
- 在移动端也会按照菜品分类和套餐分类来展示对应的菜品和套餐
可以在后台系统的分类管理页面分别添加菜品分类和套餐分类,如下:
新增菜品分类
新增套餐分类
数据模型
新增分类,其实就是将我们新增窗口录入的分类数据插入到category表,表结构如下:
代码分析
在开发业务功能前,先将需要用到的类和接口基本结构创建好:
- 实体类Category(直接从课程资料中导入即可)
- Mapper接口CategoryMapper
- 业务层接口CategoryService
- 业务层实现类CategoryServicelmpl
- 控制层CategoryController
实体类Category
package com.itzq.reggie.entity;
import com.baomidou.mybatisplus.annotation.FieldFill; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import lombok.Data; import lombok.Getter; import lombok.Setter; import java.io.Serializable; import java.time.LocalDateTime;
@Data public class Category implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
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;
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
|
Mapper接口CategoryMapper
package com.itzq.reggie.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.itzq.reggie.entity.Category; import org.apache.ibatis.annotations.Mapper;
@Mapper public interface CategoryMapper extends BaseMapper<Category> { }
12345678910
|
业务层接口CategoryService
package com.itzq.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.itzq.reggie.entity.Category;
public interface CategoryService extends IService<Category> { }
12345678
|
业务层实现类CategoryServicelmpl
package com.itzq.reggie.service.Impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.itzq.reggie.entity.Category; import com.itzq.reggie.mapper.CategoryMapper; import com.itzq.reggie.service.CategoryService; import org.springframework.stereotype.Service;
@Service public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService { }
123456789101112
|
控制层CategoryController
package com.itzq.reggie.controller;
import com.itzq.reggie.common.R; import com.itzq.reggie.entity.Category; import com.itzq.reggie.service.CategoryService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;
@RestController @RequestMapping("/category") @Slf4j public class CategoryController {
@Autowired private CategoryService categoryService;
}
12345678910111213141516171819202122
|
在开发代码之前,需要梳理一下整个程序的执行过程:
- 页面(backend/page/category/list.html)发送ajax请求,将新增分类窗口输入的数据以json形式提交到服务端
- 服务端Controller接收页面提交的数据并调用Service将数据进行保存
- Service调用Mapper操作数据库,保存数据
可以看到新增菜品分类和新增套餐分类请求的服务端地址和提交的json数据结构相同,所以服务端只需要提供一个方法统一处理即可:
新增菜品分类
新增套餐分类
在控制层CategoryController中添加逻辑代码
package com.itzq.reggie.controller;
import com.itzq.reggie.common.R; import com.itzq.reggie.entity.Category; import com.itzq.reggie.service.CategoryService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;
@RestController @RequestMapping("/category") @Slf4j public class CategoryController {
@Autowired private CategoryService categoryService;
@PostMapping public R<String> save(@RequestBody Category category){ log.info("category:{}",category); categoryService.save(category); return R.success("新增分类成功"); } }
123456789101112131415161718192021222324252627282930313233
|
功能测试
测试前需注意,category表中的name字段索引类型为unique
初始阶段数据库表
启动项目,在新增菜品分类页面输入数据,点击保存,若提交相同分类名称(name字段),则会经过我们之前写的全局异常处理器类,报相关的错误
将页面提交的数据保存到了数据库
06-分类信息分页查询-需求分析&代码开发&功能测试
需求分析
系统中的分类很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。
代码开发
在开发代码之前,需要梳理一下整个程序的执行过程:
- 页面发送ajax请求,将分页查询参数(page、pageSize)提交到服务端
- 服务端Controller接收页面提交的数据并调用Service查询数据
- Service调用Mapper操作数据库,查询分页数据
- Controler将查询到的分页数据响应给页面
- 页面接收到分页数据并通过ElementUI的Table组件展示到页面上
在CategoryController类上添加page方法
@GetMapping("/page") public R<Page> page(int page, int pageSize){ Page<Category> pageInfo = new Page<>(page,pageSize); LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.orderByAsc(Category::getSort); categoryService.page(pageInfo,queryWrapper); return R.success(pageInfo); } 123456789101112131415161718
|
功能测试
点击分类管理,客户端发送请求给服务端,服务端接收数据,进行分页处理后,返回数据给页面,页面通过相应组件处理
页面返回的数据type数据为int类型,为什么页面展示出来的是文字?
因为返回的数据在前端做了相应的判断处理,再回显到页面
07-删除分类-需求分析&代码开发&功能测试
需求分析
- 在分类管理列表页面,可以对某个分类进行删除操作
- 需要注意的是当分类关联了菜品或者套餐时,此分类不允许删除
代码开发
在开发代码之前,需要梳理一下整个程序的执行过程:
1、页面发送ajax请求,将参数(id)提交到服务端
2、服务端Controller接收页面提交的数据并调用Service删除数据
3、Service调用Mapper操作数据库
在CategoryController类上添加delete方法
@DeleteMapping public R<String> delete(Long id){ log.info("将要删除的分类id:{}",id);
categoryService.removeById(id); return R.success("分类信息删除成功"); } 123456789101112
|
需要注意教程文档中给出的url的id参数为ids,需要在category.js中将参数改为id,以便于与后端接收数据做好相应的映射处理,若还是不能正常访问,清除浏览器缓存
功能测试
启动项目,点击删除按钮,弹出一个对话框,并点击确认,页面会重新发送分页请求,在页面将看不见该行数据,查看数据库也没有该行数据显示,删除数据成功
08-删除分类-功能完善
功能完善
前面我们已经实现了根据id删除分类的功能,但是并没有检查删除的分类是否关联了菜品或者套餐,所以我们需要进行功能完善。
要完善分类删除功能,需要先准备基础的类和接口︰
1、实体类Dish和Setmeal(从课程资料中复制即可)
2、Mapper接口DishMapper和setmealMapper
3、Service接口DishService和SetmealService
4、Service实现类DishServicelmpl和SetmealServicelmpl
实体类Dish和Setmeal
Dish
package com.itzq.reggie.entity;
import com.baomidou.mybatisplus.annotation.FieldFill; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import lombok.Data; import java.io.Serializable; import java.math.BigDecimal; import java.time.LocalDateTime;
@Data public class Dish implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String name;
private Long categoryId;
private BigDecimal price;
private String code;
private String image;
private String description;
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;
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
|
Setmeal
package com.itzq.reggie.entity;
import com.baomidou.mybatisplus.annotation.FieldFill; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import lombok.Data; import java.io.Serializable; import java.math.BigDecimal; import java.time.LocalDateTime;
@Data public class Setmeal implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private Long categoryId;
private String name;
private BigDecimal price;
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; }
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
|
Mapper接口DishMapper和SetmealMapper
DishMapper
package com.itzq.reggie.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.itzq.reggie.entity.Dish; import org.apache.ibatis.annotations.Mapper;
@Mapper public interface DishMapper extends BaseMapper<Dish> { }
12345678910
|
SetmealMapper
package com.itzq.reggie.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.itzq.reggie.entity.Setmeal; import org.apache.ibatis.annotations.Mapper;
@Mapper public interface SetmealMapper extends BaseMapper<Setmeal> { }
12345678910
|
Service接口DishService和SetmealService
DishService
package com.itzq.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.itzq.reggie.entity.Dish;
public interface DishService extends IService<Dish> { }
12345678
|
SetmealService
package com.itzq.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.itzq.reggie.entity.Setmeal;
public interface SetmealService extends IService<Setmeal> { }
12345678
|
Service实现类DishServicelmpl和SetmealServicelmpl
DishServicelmpl
package com.itzq.reggie.service.Impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.itzq.reggie.entity.Dish; import com.itzq.reggie.mapper.DishMapper; import com.itzq.reggie.service.DishService; import org.springframework.stereotype.Service;
@Service public class DishServicelmpl extends ServiceImpl<DishMapper, Dish> implements DishService { }
123456789101112
|
SetmealServicelmpl
package com.itzq.reggie.service.Impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.itzq.reggie.entity.Setmeal; import com.itzq.reggie.mapper.SetmealMapper; import com.itzq.reggie.service.SetmealService; import org.springframework.stereotype.Service;
@Service public class SetmealServicelmpl extends ServiceImpl<SetmealMapper, Setmeal> implements SetmealService { }
123456789101112
|
在common包下添加CustomException 类
package com.itzq.reggie.common;
public class CustomException extends RuntimeException{ public CustomException(String message){ super(message); } }
12345678
|
在GlobalExceptionHandler类,添加exceptionHandler方法用于处理CustomException异常
@ExceptionHandler(CustomException.class) public R<String> exceptionHandler (CustomException exception){ log.error(exception.getMessage());
return R.error(exception.getMessage()); } 123456
|
在CategoryService 接口中定义remove方法
package com.itzq.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.itzq.reggie.entity.Category;
public interface CategoryService extends IService<Category> { void remove(Long id); }
123456789
|
在CategoryServiceImpl 实现类中实现接口定义的remove方法,并为该方法添加所需要的逻辑代码
- 逻辑:查看当前要删除的分类id是否与菜品或套餐相关联,若与其中一个关联,则抛出异常
package com.itzq.reggie.service.Impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.itzq.reggie.common.CustomException; import com.itzq.reggie.entity.Category; import com.itzq.reggie.entity.Dish; import com.itzq.reggie.entity.Setmeal; import com.itzq.reggie.mapper.CategoryMapper; import com.itzq.reggie.service.CategoryService; import com.itzq.reggie.service.DishService; import com.itzq.reggie.service.SetmealService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service;
@Service public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService { @Autowired DishService dishService;
@Autowired SetmealService setmealService;
@Override public void remove(Long id) { LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>(); dishLambdaQueryWrapper.eq(Dish::getCategoryId, id); int count1 = dishService.count(dishLambdaQueryWrapper);
if (count1 > 0){ throw new CustomException("当前分类下关联了菜品,不能删除"); }
LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>(); int count2 = setmealService.count(setmealLambdaQueryWrapper);
if (count2 > 0){ throw new CustomException("当前分类下关联了套餐,不能删除"); }
super.removeById(id); } }
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
|
重启项目,点击删除按钮
内容提示
同理,若当前分类关联了套餐,也会有相应的提示,可以自己操作检验
09-修改分类-需求分析&分析页面回显效果&代码开发&功能测试
需求分析
在分类管理列表页面点击修改按钮,弹出修改窗口,在修改窗口回显分类信息并进行修改,最后点击确定按钮完成修改操作
分析页面回显效果
点击修改按钮,触发点击事件editHandle,并传递该行的数据
在editHandle函数中,将传来的参数赋值给classData下的name和sort数据
v-model实现数据的双向绑定
代码开发
在CategoryController类中添加update方法,请求方式为put请求
@PutMapping public R<String> update(@RequestBody Category category){ log.info("修改分类信息为:{}",category); categoryService.updateById(category); return R.success("修改分类信息成功"); } 1234567891011
|
功能测试
点击修改按钮
填入想要修改的信息后,点击确定
页面显示
数据库中内容
修改成功!
业务开发Day4
01-本章内容介绍
目录
- 文件上传下载
- 新增菜品
- 菜品信息分页查询
- 修改菜品
需要实现页面的功能
02-文件上传下载_文件上传下载介绍
内容
- 文件上传介绍
- 文件下载介绍
文件上传介绍
- 文件上传,也称为upload,是指将本地图片、视频、音频等文件上传到服务器上,可以供其他用户浏览或下载的过程
- 文件上传在项目中应用非常广泛,我们经常发微博、发微信朋友圈都用到了文件上传功能
文件上传时,对页面的form表单有如下要求:
- method=“post” 采用post方式提交数据
- enctype=“multipart/form-data” 采用multipart格式上传文件
- type=“file” 使用input的file控件上传
举例:
<form method="post" action="/common/upload" enctype="multipart/form-data"> <input name="myFile" type="file"/> <input type="submit" value="提交" /> <form> 1234
|
目前一些前端组件库也提供了相应的上传组件,但是底层原理还是基于form表单的文件上传
例如ElementUI中提供的upload上传组件:
服务端要接收客户端页面上传的文件,通常都会使用Apache的两个组件:
- commons-fileupload
- commons-io
Spring框架在spring-web包中对文件上传进行了封装,大大简化了服务端代码,我们只需要在Controller的方法中声明一个MultipartFile类型的参数即可接收上传的文件,例如:
文件下载介绍
- 文件下载,也称为download,是指将文件从服务器传输到本地计算机的过程
通过浏览器进行文件下载,通常有两种表现形式:
- 以附件形式下载,弹出保存对话框,将文件保存到指定磁盘目录
- 直接在浏览器中打开
通过浏览器进行文件下载,本质上就是服务端将文件以流的形式写回浏览器的过程。
03-文件上传下载_文件上传代码实现1
文件上传代码实现1
文件上传,页面端可以使用ElementUI提供的上传组件。
可以直接使用资料中提供的上传页面,位置:资料/文件上传下载页面/upload.html
upload.html-前端上传文件页面代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>文件上传</title> <link rel="stylesheet" href="../../plugins/element-ui/index.css" /> <link rel="stylesheet" href="../../styles/common.css" /> <link rel="stylesheet" href="../../styles/page.css" /> </head> <body> <div class="addBrand-container" id="food-add-app"> <div class="container"> <el-upload class="avatar-uploader" action="/common/upload" :show-file-list="false" :on-success="handleAvatarSuccess" :before-upload="beforeUpload" ref="upload"> <img v-if="imageUrl" :src="imageUrl" class="avatar"></img> <i v-else class="el-icon-plus avatar-uploader-icon"></i> </el-upload> </div> </div> <script src="../../plugins/vue/vue.js"></script> <script src="../../plugins/element-ui/index.js"></script> <script src="../../plugins/axios/axios.min.js"></script> <script src="../../js/index.js"></script> <script> new Vue({ el: '##food-add-app', data() { return { imageUrl: '' } }, methods: { handleAvatarSuccess (response, file, fileList) { this.imageUrl = `/common/download?name=${response.data}` }, beforeUpload (file) { if(file){ const suffix = file.name.split('.')[1] const size = file.size / 1024 / 1024 < 2 if(['png','jpeg','jpg'].indexOf(suffix) < 0){ this.$message.error('上传图片只支持 png、jpeg、jpg 格式!') this.$refs.upload.clearFiles() return false } if(!size){ this.$message.error('上传文件大小不能超过 2MB!') return false } return file } } } }) </script> </body> </html> 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
|
放置upload.html代码的位置(需创建一个demo文件夹)
在controller包下创建*CommonController类*,代码内容如下:
package com.itzq.reggie.controller;
import com.itzq.reggie.common.R; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile;
@RestController @RequestMapping("/common") @Slf4j public class CommonController {
@PostMapping("/upload") public R<String> upload(MultipartFile file){ log.info(file.toString()); return null; } } 123456789101112131415161718192021
|
注意:
- MultipartFile是spring类型,代表HTML中form data方式上传的文件,包含二进制数据+文件名称。
- MultipartFile后面的参数名必须为file,因为需要和前端页面的name保持一致,否则不会生效
启动项目,在浏览器地址栏输入:http://localhost:8080/backend/page/demo/upload.html
被filter拦截
登录后可正常上传文件,在此处添加断点,以debug方式启动项目
来到文件上传页面,点击上传文件,进入断点模式,查看文件的存储位置
在磁盘中寻找到文件,发现该文件为临时文件(TMP文件),所以需要转存到指定位置,否则本次请求完成后临时文件删除
放行后发现临时文件消失
04-文件上传下载_文件上传代码实现2
文件上传代码实现2
在开发上传文件代码前,先再LoginCheckFilter类的urls数组中添加 “/common/“**
作用:避免每次上传文件时都需要进行登录操作
将临时文件存储存储到指定位置
package com.itzq.reggie.controller;
import com.itzq.reggie.common.R; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile;
import java.io.File; import java.io.IOException;
@RestController @RequestMapping("/common") @Slf4j public class CommonController {
@PostMapping("/upload") public R<String> upload(MultipartFile file){ log.info(file.toString());
try { file.transferTo(new File("D:\\hello.jpg")); } catch (IOException e) { e.printStackTrace(); } return null; } } 12345678910111213141516171819202122232425262728293031
|
启动项目,浏览器地址栏输入地址:http://localhost:8080/backend/page/demo/upload.html
- 上传图片,查看指定存储文件的位置是否有上传的文件
文件转存的位置改为动态可配置的,通过配置文件的方式指定
- 使用 @Value(“${reggie.path}”)读取到配置文件中的动态转存位置
- 使用uuid方式重新生成文件名,避免文件名重复造成文件覆盖
- 通过获取原文件名来截取文件后缀
package com.itzq.reggie.controller;
import com.itzq.reggie.common.R; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile;
import java.io.File; import java.io.IOException; import java.util.UUID;
@RestController @RequestMapping("/common") @Slf4j public class CommonController {
@Value("${reggie.path}") private String basePath;
@PostMapping("/upload") public R<String> upload(MultipartFile file){ log.info(file.toString());
String originalFilename = file.getOriginalFilename(); String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
String fileName = UUID.randomUUID().toString() + suffix;
try { file.transferTo(new File(basePath + fileName)); } catch (IOException e) { e.printStackTrace(); } return null; } } 12345678910111213141516171819202122232425262728293031323334353637383940414243444546
|
重启项目,上传图片,在动态指定的位置上发现上传的文件
指定的目录或许不存在于磁盘中,所以我们要为程序添加逻辑代码
若目录不存在于磁盘中,则需要创建该目录
具体代码
File dir = new File(basePath); if (!dir.exists()){ dir.mkdir(); } 1234567
|
测试,
将配置文件中的目录信息修改为本地磁盘中不存在的目录
重启项目,上传图片,发现创建了一个新的目录,并将文件放入该目录下
服务端需返回文件名给前端,便于后续开发使用
05-文件上传下载_文件下载代码实现&测试
文件下载代码实现
前端处理
前端页面ElementUI的upload组件会在上传完图片后,触发img组件发送请求,服务端以流的形式(输出流)将文件写回浏览器,在浏览器中展示图片
定义前端发送回显图片请求的地址
在CommonController类中添加download方法
- 通过输入流读取文件内容
- 通过输出流将文件写回浏览器,在浏览器展示图片
- 关闭输入输出流,释放资源
@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(); }
fileInputStream.close(); outputStream.close();
} catch (IOException e) { e.printStackTrace(); } } 123456789101112131415161718192021222324252627282930
|
测试
启动项目,上传图片,图片回显到页面
06-新增菜品_需求分析&数据模型
需求分析
- 后台系统中可以管理菜品信息,通过新增功能来添加一个新的菜品
- 在添加菜品时需要选择当前菜品所属的菜品分类,并且需要上传菜品图片
- 在移动端会按照菜品分类来展示对应的菜品信息。
数据模型
dish表
dish_flavor表
新增菜品分类,会将前端传过来的数据保存在这两张表中
07-新增菜品_代码开发_查询分类数据
代码开发-准备工作
在开发业务功能前,先将需要用到的类和接口基本结构创建好:
- 实体类DishFlavor(直接从课程资料中导入即可,Dish实体前面课程中已经导入过了)
- Mapper接口DishFlavorMapper
- 业务层接口DishFlavorService
- 业务层实现类DishFlavorServicelmpl
- 控制层DishController
实体类DishFlavor
package com.itzq.reggie.entity;
import com.baomidou.mybatisplus.annotation.FieldFill; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import lombok.Data; import java.io.Serializable; import java.time.LocalDateTime;
@Data public class DishFlavor implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private Long dishId;
private String name;
private String value;
@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;
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
|
Mapper接口DishFlavorMapper
package com.itzq.reggie.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.itzq.reggie.entity.DishFlavor; import org.apache.ibatis.annotations.Mapper;
@Mapper public interface DishFlavorMapper extends BaseMapper<DishFlavor> { }
12345678910
|
业务层接口DishFlavorService
package com.itzq.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.itzq.reggie.entity.DishFlavor;
public interface DishFlavorService extends IService<DishFlavor> { }
12345678
|
业务层实现类DishFlavorServicelmpl
package com.itzq.reggie.service.Impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.itzq.reggie.entity.DishFlavor; import com.itzq.reggie.mapper.DishFlavorMapper; import com.itzq.reggie.service.DishFlavorService; import org.springframework.stereotype.Service;
@Service public class DishFlavorServicelmpl extends ServiceImpl<DishFlavorMapper, DishFlavor> implements DishFlavorService { }
123456789101112
|
控制层DishController
package com.itzq.reggie.controller;
import com.itzq.reggie.service.DishFlavorService; import com.itzq.reggie.service.DishService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;
@RestController @RequestMapping("/dish") @Slf4j public class DishController { @Autowired private DishService dishService;
@Autowired private DishFlavorService dishFlavorService;
}
123456789101112131415161718192021
|
代码开发-梳理交互过程
在开发代码之前,需要梳理一下新增菜品时前端页面和服务端的交互过程:
- 页面(backend/page/food/add.html)发送ajax请求,请求服务端获取菜品分类数据并展示到下拉框中
- 页面发送请求进行图片上传,请求服务端将图片保存到服务器
- 页面发送请求进行图片下载,将上传的图片进行回显
- 点击保存按钮,发送ajax请求,将菜品相关数据以json形式提交到服务端
开发新增菜品功能,其实就是在服务端编写代码去处理前端页面发送的这4次请求即可。
添加菜品页面展示
08-新增菜品_代码开发_查询分类数据
前端分析
一个vue实例被创建后会调用钩子函数,执行其中的方法
来到getDishList方法,执行其中getCategoryList方法
执行getCategoryList方法向服务端发送ajax请求,请求方式为get
在CategoryController类中,添加list方法,具体代码如下:
@GetMapping("/list") public R<List<Category>> list(Category category){ LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(category.getType() != null,Category::getType,category.getType()); queryWrapper.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime); List<Category> list = categoryService.list(queryWrapper); return R.success(list);
} 1234567891011121314
|
启动项目,进入菜品管理,点击菜品分类下拉框,成功获得数据
09-新增菜品_代码开发_接收页面提交的数据
接收图片文件
在本章节02-05,我们已经将图片的上传下载准备完毕
测试
添加一张图片,并回显图片
注意事项
- 价格在前端已被处理,在点击提交按钮后,先执行前端的submitForm方法,并将price做相应的处理(在页面中单位为元,在数据库中存储的单位为分),再通过ajax请求向后端提供相应的json数据
- 因为Dish实体类不满足接收flavor参数,即需要导入DishDto,用于封装页面提交的数据
DTO,全称为Data Transfer Object,即数据传输对象,一般用于展示层与服务层之间的数据传输。
- 在reggie包下,创建一个新包为dto
- 在该包下创建DishDto 数据传输类
package com.itzq.reggie.dto;
import com.itzq.reggie.entity.Dish; import com.itzq.reggie.entity.DishFlavor; import lombok.Data; import java.util.ArrayList; import java.util.List;
@Data public class DishDto extends Dish {
private List<DishFlavor> flavors = new ArrayList<>();
private String categoryName;
private Integer copies; }
12345678910111213141516171819
|
代码开发
在DishController类中添加save方法
- 代码逻辑:测试是否可以正常的接收前端传过来的json数据
- 注意:因为前端传来的是json数据,所以我们需要在参数前添加*@RequestBody*注解
- 具体代码如下:
@PostMapping public R<String> save(@RequestBody DishDto dishDto){ log.info("接收的dishDto数据:{}",dishDto.toString()); return null; } 12345
|
测试
debug方式启动项目,在标记的行处添加断点,用于查看数据
在添加菜品页面输入数据,点击保存
来到断点处,查看到数据准确无误的提交到服务端
10-新增菜品_代码开发_保存数据到菜品表和菜品口味表
分析
- 在保存数据到菜品表和菜品口味表的过程中,我们需要对保存到菜品口味表的数据做相应的处理
- 取出dishDto的dishId,通过stream流对每一组flavor的dishId赋值
- 保存菜品口味到菜品数据表
代码开发
在DishServicelmpl类中添加如下代码
package com.itzq.reggie.service.Impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.itzq.reggie.dto.DishDto; import com.itzq.reggie.entity.Dish; import com.itzq.reggie.entity.DishFlavor; import com.itzq.reggie.mapper.DishMapper; import com.itzq.reggie.service.DishFlavorService; import com.itzq.reggie.service.DishService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional;
import java.util.List; import java.util.stream.Collectors;
@Service @Transactional public class DishServicelmpl extends ServiceImpl<DishMapper, Dish> implements DishService {
@Autowired DishFlavorService dishFlavorService;
@Override public void saveWithFlavor(DishDto dishDto) {
super.save(dishDto); Long dishId = dishDto.getId(); List<DishFlavor> flavors = dishDto.getFlavors();
flavors = flavors.stream().map((item) -> { item.setDishId(dishId); return item; }).collect(Collectors.toList());
dishFlavorService.saveBatch(flavors);
} }
123456789101112131415161718192021222324252627282930313233343536373839404142434445
|
在ReggieApplication主启动类上,添加注解:@EnableTransactionManagement
11-新增菜品_代码开发_功能测试
功能测试
前提:在DishController类的save方法中添加代码
@PostMapping public R<String> save(@RequestBody DishDto dishDto){ log.info("接收的dishDto数据:{}",dishDto.toString());
dishService.saveWithFlavor(dishDto); return R.success("新增菜品成功"); } 12345678
|
重启项目,进入添加菜品页面,输入数据,点击保存
dish表中添加数据成功
dish_flavor表中添加数据成功,并成功为每组flavor数据附上dishId
12-菜品信息分页查询_需求分析
需求分析
- 系统中的菜品数据很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看
- 所以一般的系统中都会以分页的方式来展示列表数据。
图片和菜品分类比较特殊
- 图片列:会用到文件的下载功能
- 菜品分类列:只保存了菜品的category_id,需通过查找category_id所对应的菜品分类名称,从而回显数据
13-菜品信息分页查询_代码开发1
代码开发-梳理交互过程
在开发代码之前,需要梳理一下菜品分页查询时前端页面和服务端的交互过程:
- 页面(backend/page/food/list.html)发送ajax请求,将分页查询参数(page、pageSize、name),提交到服务端,获取分页数据
- 页面发送请求,请求服务端进行图片下载,用于页面图片展示
开发菜品信息分页查询功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。
代码
在DishController下,添加page方法,进行分页查询
@GetMapping("/page") public R<Page> page(int page, int pageSize, String name){
Page<Dish> pageInfo = new Page<>(); LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.like(name != null,Dish::getName,name); queryWrapper.orderByDesc(Dish::getUpdateTime); dishService.page(pageInfo,queryWrapper);
return R.success(pageInfo); } 123456789101112131415
|
测试
- 为什么只有宫保鸡丁有图片的展示效果,因为这是在刚刚添加菜品的时候添加的数据
- 保证了在服务端存在对应图片名的信息,而其他是菜品是直接从sql文件导入,服务端不一定有对应的图片名
- 为什么菜品分类中没有数据?
- 因为服务端传给前端的菜品分类数据不满足前端的要求,所以在页面中不能回显菜品分类数据
14-菜品信息分页查询_代码开发2
分析
在前端页面发现菜品分类对应的prop属性名为categoryName
但我们在响应的数据当中并没有发现categoryName字段
- 页面需要什么数据,服务端就应该返还什么样的数据,所以Dish对象不满足该页面要求
- 在之前我们创建了DishDto类,发现类中的属性名正好和前端的属性名对应
代码
修改DishController中的page方法
@GetMapping("/page") public R<Page> page(int page, int pageSize, String name){
Page<Dish> pageInfo = new Page<>(); Page<DishDto> dishDtoPage = new Page<>(); LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.like(name != null,Dish::getName,name); queryWrapper.orderByDesc(Dish::getUpdateTime); dishService.page(pageInfo,queryWrapper);
BeanUtils.copyProperties(pageInfo,dishDtoPage,"records");
List<Dish> records = pageInfo.getRecords();
List<DishDto> list = records.stream().map((item) -> { DishDto dishDto = new DishDto(); BeanUtils.copyProperties(item,dishDto); Long categoryId = item.getCategoryId(); Category category = categoryService.getById(categoryId); String categoryName = category.getName(); dishDto.setCategoryName(categoryName); return dishDto; }).collect(Collectors.toList());
dishDtoPage.setRecords(list);
return R.success(dishDtoPage); } 12345678910111213141516171819202122232425262728293031323334
|
15-菜品信息分页查询_功能测试
功能测试
前提
我们需要在分页方法中添加判空条件,若查询的数据为空,经过判断后跳过部分代码,就不会爆空指针异常
启动项目,点击菜品管理,可以看见页面展现的菜品分类信息
16-修改菜品_需求分析&梳理交互过程
需求分析
- 在菜品管理列表页面点击修改按钮,跳转到修改菜品页面
- 在修改页面回显菜品相关信息并进行修改
- 最后点击确定按钮完成修改操作
代码开发-梳理交互过程
在开发代码之前,需要梳理一下修改菜品时前端页面( add.html)和服务端的交互过程:
- 页面发送ajax请求,请求服务端获取分类数据,用于菜品分类下拉框中数据展示(已完成)
- 页面发送ajax请求,请求服务端,根据id查询当前菜品信息,用于菜品信息回显
- 页面发送请求,请求服务端进行图片下载,用于页图片回显(已完成)
- 点击保存按钮,页面发送ajax请求,将修改后的菜品相关数据以json形式提交到服务端
开发修改菜品功能,其实就是在服务端编写代码去处理前端页面发送的这4次请求即可。
17-修改菜品_代码开发_根据id查询对应的菜品和口味信息
代码
在DishService接口中添加方法*getByIdWithFlavor*
在DishServicelmpl中实现*getByIdWithFlavor*方法,并添加逻辑代码
- 根据服务端接收的id,查询菜品的基本信息-dish
- 创建dishDto对象,并将查询到的dish对象属性赋值给dishDto
- 根据查询到的dish对象,可以取出对应的菜品id,再通过等值条件查询,查询到DishFlavor数据信息
- 将查询到的flavor数据信息使用set方法赋值给dishDto对象
- 返回dishDto对象
@Override public DishDto getByIdWithFlavor(Long id) { Dish dish = super.getById(id);
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(dish,dishDto);
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(DishFlavor::getDishId,dish.getId()); List<DishFlavor> list = dishFlavorService.list(queryWrapper);
dishDto.setFlavors(list);
return dishDto; } 123456789101112131415161718192021
|
在DishController中添加get方法,实现添加在DishServicelmpl中的逻辑代码,返回查询到的数据信息
@GetMapping("/{id}") public R<DishDto> get(@PathVariable Long id){ DishDto dishDto = dishService.getByIdWithFlavor(id); return R.success(dishDto); } 1234567
|
18-修改菜品_代码开发_测试数据回显
测试数据回显
在DishController的get方法中加入断点
进入菜品管理,点击修改菜品按钮,程序跳转到断点处,查询回显的dishDto数据是否成功
页面回显成功
19-修改菜品_代码开发_修改菜品信息和口味信息
分析前端页面发送的请求
代码
在DishService接口中添加updateWithFlavor方法
DishServicelmpl类中实现DishService定义的方法,并添加代码逻辑
- 根据id修改菜品的基本信息
- 通过dish_id,删除菜品的flavor
- 获取前端提交的flavor数据
- 为条flavor的dishId属性赋值
- 将数据批量保存到dish_flavor数据库
@Override public void updateWithFlavor(DishDto dishDto) { super.updateById(dishDto);
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(DishFlavor::getDishId,dishDto.getId()); dishFlavorService.remove(queryWrapper);
List<DishFlavor> flavors = dishDto.getFlavors();
flavors = flavors.stream().map((item) -> { item.setDishId(dishDto.getId()); return item; }).collect(Collectors.toList());
dishFlavorService.saveBatch(flavors); } 1234567891011121314151617181920212223
|
在DishController类中添加方法update,并调用updateWithFlavor方法实现表中数据的修改
@PutMapping public R<String> update(@RequestBody DishDto dishDto){
log.info("接收的dishDto数据:{}",dishDto.toString());
dishService.updateWithFlavor(dishDto);
return R.success("新增菜品成功"); } 12345678910
|
20-修改菜品_功能测试
功能测试
启动项目,来到菜品管理界面
- 要修改菜品的初始值
- 点击修改,输入想要修改的信息,点击保存
- 跳转到菜品管理界面,修改菜品信息成功
业务开发Day5
01-本章内容介绍
效果展示
目录
- 新增套餐
- 套餐信息分页查询
- 删除套餐
02-新增套餐_需求分析&数据模型
需求分析
- 套餐就是菜品的集合
- 后台系统中可以管理套餐信息,通过新增套餐功能来添加一个新的套餐
- 在添加套餐时需要选择当前套餐所属的套餐分类和包含的菜品,并且需要上传套餐对应的图片
- 在移动端会按照套餐分类来展示对应的套餐。
数据模型
- 新增套餐,其实就是将新增页面录入的套餐信息插入到setmeal表,还需要向setmeal_dish表插入套餐和菜品关联数据
- 所以在新增套餐时,涉及到两个表:
- setmeal—-套餐表
- setmeal_dish—-套餐菜品关系表
setmeal
setmeal_dish
03-新增套餐_代码开发_准备工作&梳理交互过程
代码开发-准备工作
在开发业务功能前,先将需要用到的类和接口基本结构创建好:
- 实体类SetmealDish(直接从课程资料中导入即可,Setmeal实体前面课程中已经导入过了)
- DTO SetmealDto (直接从课程资料中导入即可)
- Mapper接口SetmealDishMapper
- 业务层接口SetmealDishService
- 业务层实现类SetmealDishservicelmpl
- 控制层SetmealController
SetmealDish—实体类
package com.itzq.reggie.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;
@Data public class SetmealDish implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private Long setmealId;
private Long dishId;
private String name;
private BigDecimal price;
private Integer copies;
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; }
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162
|
DTO SetmealDto—数据传输对象
package com.itzq.reggie.dto;
import com.itzq.reggie.entity.Setmeal; import com.itzq.reggie.entity.SetmealDish; import lombok.Data; import java.util.List;
@Data public class SetmealDto extends Setmeal {
private List<SetmealDish> setmealDishes;
private String categoryName; }
12345678910111213141516
|
SetmealDishMapper接口
package com.itzq.reggie.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.itzq.reggie.entity.SetmealDish; import org.apache.ibatis.annotations.Mapper;
@Mapper public interface SetmealDishMapper extends BaseMapper<SetmealDish> { }
1234567891011
|
SetmealDishService接口
package com.itzq.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.itzq.reggie.entity.SetmealDish;
public interface SetmealDishService extends IService<SetmealDish> { }
12345678
|
SetmealDishservicelmpl实现类
package com.itzq.reggie.service.Impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.itzq.reggie.entity.SetmealDish; import com.itzq.reggie.mapper.SetmealDishMapper; import com.itzq.reggie.service.SetmealDishService; import org.springframework.stereotype.Service;
@Service public class SetmealDishServiceImpl extends ServiceImpl<SetmealDishMapper,SetmealDish> implements SetmealDishService { }
123456789101112
|
SetmealController控制层
package com.itzq.reggie.controller;
import com.itzq.reggie.service.SetmealDishService; import com.itzq.reggie.service.SetmealService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;
@RestController @RequestMapping("/setmeal") @Slf4j public class SetmealController { @Autowired private SetmealService setmealService; @Autowired private SetmealDishService setmealDishService; } 1234567891011121314151617181920
|
代码开发-梳理交互过程
在开发代码之前,需要梳理一下新增套餐时前端页面和服务端的交互过程:
- 页面(backend/page/combo/add.html)发送ajax请求,请求服务端获取套餐分类数据并展示到下拉框中(已完成)
- 页面发送ajax请求,请求服务端,获取菜品分类数据并展示到添加菜品窗口中
- 页面发送ajax请求,请求服务端,根据菜品分类查询对应的菜品数据并展示到添加菜品窗口中
- 页面发送请求进行图片上传,请求服务端将图片保存到服务器(已完成)
- 页面发送请求进行图片下载,将上传的图片进行回显(已完成)
- 点击保存按钮,发送ajax请求,将套餐相关数据以json形式提交到服务端
开发新增套餐功能,其实就是在服务端编写代码去处理前端页面发送的这6次请求即可
04-新增套餐_代码开发_根据分类查询菜品
前端分析
启动项目,进入套餐管理,点击新建套餐,会发现页面发送的请求未被服务端接收
爆系统接口异常,服务端未定义查询菜品的方法
相关代码
在DishController类中,添加list方法
注意:需要添加额外的查询条件,只查询status为1的数据,表示该菜品为起售状态,才能被加入套餐中,供用户选择
@GetMapping("/list") public R<List<Dish>> list(Dish dish){
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(dish.getCategoryId() != null, Dish::getCategoryId, dish.getCategoryId()); queryWrapper.eq(Dish::getStatus,1);
List<Dish> list = dishService.list(queryWrapper); return R.success(list); } 12345678910111213
|
重启项目,发现查询数据成功,并回显到前端页面
05-新增套餐_代码开发_服务端接收页面提交的数据
前端分析
启动项目,来到添加套餐页面,输入数据,点击保存
查看前端页面发送的请求,请求方式
前端页面传输json数据给服务端
相关代码(测试版)
在SetmealController类中添加save方法
@PostMapping public R<String> save(@RequestBody SetmealDto setmealDto){ log.info("数据传输对象setmealDto:{}",setmealDto.toString()); return null; } 12345
|
添加断点
debug方式重启项目,来到添加套餐页面,输入数据,点击保存
跳转到服务端,查看是否接收到客服端提交的数据,发现数据成功接收
06-新增套餐_代码开发_保存数据到对应表
相关代码
在SetmealService接口,添加saveWithDish方法
实现类SetmealServicelmpl,实现接口添加的方法,并向方法中添加代码逻辑
@Override @Transactional public void saveWithDish(SetmealDto setmealDto) { save(setmealDto);
List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
setmealDishes = setmealDishes.stream().map((item) -> { item.setSetmealId(setmealDto.getId()); return item; }).collect(Collectors.toList());
setmealDishService.saveBatch(setmealDishes); } 12345678910111213141516
|
在SetmealController控制层的save方法中,调用saveWithDish方法,将数据保存至数据库
@PostMapping public R<String> save(@RequestBody SetmealDto setmealDto){ log.info("数据传输对象setmealDto:{}",setmealDto.toString()); setmealService.saveWithDish(setmealDto); return R.success("新增套餐成功"); } 123456
|
07-新增套餐_代码开发_功能测试
功能测试
来到新增套餐页面,输入数据,点击保存
setmeal_dish表—数据插入成功
setmeal—数据插入成功
08-套餐信息分页查询_需求分析&梳理交互过程
需求分析
- 系统中的套餐数据很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看
- 一般的系统中都会以分页的方式来展示列表数据
梳理交互过程
在开发代码之前,需要梳理一下套餐分页查询时前端页面和服务端的交互过程:
- 页面(backend/page/combo/list.html)发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端,获取分页数据
- 页面发送请求,请求服务端进行图片下载,用于页面图片展示
开发套餐信息分页查询功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可
09-套餐信息分页查询_代码开发&功能测试
前端分析
点击套餐管理,前端页面发送ajax请求,请求方式:get
代码开发
SetmealController类中,添加list方法
@GetMapping("/page") public R<Page> list(int page,int pageSize,String name){ Page<Setmeal> pageInfo = new Page<>(page, pageSize); Page<SetmealDto> dtoPage = new Page<>();
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(name != null, Setmeal::getName, name);
setmealService.page(pageInfo,queryWrapper);
BeanUtils.copyProperties(pageInfo,dtoPage,"records");
List<Setmeal> records = pageInfo.getRecords();
List<SetmealDto> list = records.stream().map((item) -> { SetmealDto setmealDto = new SetmealDto(); BeanUtils.copyProperties(item, setmealDto); Long categoryId = item.getCategoryId(); Category category = categoryService.getById(categoryId); if (category != null) { String categoryName = category.getName(); setmealDto.setCategoryName(categoryName); } return setmealDto; }).collect(Collectors.toList());
dtoPage.setRecords(list);
return R.success(dtoPage); } 1234567891011121314151617181920212223242526272829303132333435
|
注意
在套餐管理界面,套餐分类字段显示的是categoryId对应的中文,但在数据库里查询到的是categoryId,因此需要利用categoryId查询到categoryName,并赋值给数据传输对象SetmealDto
功能测试
启动项目,点击套餐管理,前端发送ajax请求,服务端接收前端发出的请求,并做相应的处理,向页面返回数据
数据成功回显到页面
10-删除套餐_需求分析&梳理交互过程
需求分析
- 在套餐管理列表页面点击删除按钮,可以删除对应的套餐信息
- 也可以通过复选框选择多个套餐,点击批量删除按钮一次删除多个套餐
- 注意,对于状态为售卖中的套餐不能删除,需要先停售,然后才能删除。
梳理交互过程
在开发代码之前,需要梳理一下删除套餐时前端页面和服务端的交互过程:
- 删除单个套餐时,页面发送ajax请求,根据套餐id删除对应套餐
- 删除多个套餐时,页面发送ajax请求,根据提交的多个套餐id删除对应套餐开发删除套餐功能
其实就是在服务端编写代码去处理前端页面发送的这2次请求即可
注意
- 观察删除单个套餐和批量删除套餐的请求信息可以发现,两种请求的地址和请求方式都是相同的
- 不同的则是传递的id个数,所以在服务端可以提供一个方法来统一处理。
11-删除套餐_代码开发&功能测试
代码开发(测试)
在SetmealController中添加delete方法
@DeleteMapping public R<String> delete(@RequestParam List<Long> ids){ log.info("ids为:",ids);
return null; } 123456
|
在delete方法上,添加断点
debug方式启动项目,来到套餐管理页面,点击删除按钮
跳转到服务端,查询ids可知服务端成功接收到前端传来的数据信息
代码开发(完善)
在SetmealService接口中添加removeWithDish方法
在SetmealServicelmpl实现类中实现对应接口中添加的方法
@Override @Transactional public void removeWithDish(List<Long> ids) { LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.in(Setmeal::getId,ids); queryWrapper.eq(Setmeal::getStatus,1);
int count = super.count(queryWrapper);
if (count > 0){ throw new CustomException("套餐正在售卖中,不能删除"); }
super.removeByIds(ids);
LambdaQueryWrapper<SetmealDish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>(); dishLambdaQueryWrapper.in(SetmealDish::getSetmealId,ids);
setmealDishService.remove(dishLambdaQueryWrapper);
} 12345678910111213141516171819202122232425262728
|
在SetmealController中完善代码—调用removeWithDish方法,实现套餐数据删除成功
@DeleteMapping public R<String> delete(@RequestParam List<Long> ids){ log.info("ids为:",ids); setmealService.removeWithDish(ids); return R.success("套餐数据删除成功");
}
12345678
|
注意:将setmeal表中status字段值改为0—为停售状态,方便测试
重启项目,来到套餐管理界面,点击删除按钮
页面显示删除成功
setmeal表中该行已被删除
12-本章内容介绍
手机验证码登录
- 点击获取验证码
- 收到短信,并输入验证码
- 点击登录,登录成功
客户端登录成功页面
本章内容介绍
- 短信发送
- 手机验证码登录(基于阿里云讲解)
13-短信发送_短信服务介绍和阿里云短信服务介绍
短信服务介绍
- 目前市面上有很多第三方提供的短信服务,这些第三方短信服务会和各个运营商(移动、联通、电信)对接
- 我们只需要注册成为会员并且按照提供的开发文档进行调用就可以发送短信
-* 需要说明的是*,这些短信服务一般都是收费服务
常用短信服务:
阿里云短信服务(Short Message Service)是广大企业客户快速触达手机用户所优选使用的通信能力。调用API或用群发助手,即可发送验证码、通知类和营销类短信;国内验证短信秒级触达,到达率最高可达99%;国际/港澳台短信覆盖200多个国家和地区,安全稳定,广受出海企业选用。
应用场景:
阿里云短信服务介绍
打开浏览器,登录阿里云—网址:https://cn.aliyun.com/
点击产品,在搜索框中输入短信服务,并点击搜索
来到短信息服务界面
选择符合自己业务需求的短信套餐包
14-短信发送_阿里云短信服务
设置短信签名
开通短信服务之后,进入短信服务管理页面,选择国内消息菜单,我们需要在这里添加短信签名
什么是短信签名?
- 短信签名是短信发送者的署名,表示发送方的身份
- 我们要调用阿里云短信服务发送短信,签名是必不可少的部分
添加短信签名方式
注意:个人申请签名是有一定的难度的,所以我们只需要了解一下使用短信签名的具体流程
设置短信模板
切换到【模板管理】标签页:
短信模板包含短信发送内容、场景、变量信息
每一个被设置好的模板有一个短信模板详情,模板详情包含了模板的6条信息
添加模板,并且提交后审核通过
设置AccessKey
AccessKey 是访问阿里云 API 的密钥,具有账户的完全权限,我们要想在后面通过API调用阿里云短信服务的接口发送短信,那么就必须要设置AccessKey。
光标移动到用户头像上,在弹出的窗口中点击【AccessKey管理】︰
进入到AccessKey的管理界面之后,提示两个选项:
- 继续使用AccessKey
- 开始使用子用户AccessKey
区别:
- 继续使用AccessKey
- 如果选择的是该选项,我们创建的是阿里云账号的AccessKey,是具有账户的完全权限
- 有了这个AccessKey之后,我们就可以通过API调用阿里云服务,不仅是短信服务,其他服务也可以调用
- 相对来说,并不安全,当前的AccessKey泄露,会影响到当前账户的其他云服务。
- 开始使用子用户AccessKey
- 可以创建一个子用户,这个子用户可以分配比较低的权限,比如仅分配短信发送的权限,不具备操作其他的服务的权限
- 即使这个AccessKey泄漏了,也不会影响其他的云服务, 相对安全。
创建子用户AccessKey。
- 点击创建用户
- 输入登录名称和显示名称(都是自定义),选择—Open API调用访问
注意:在java代码当中使用这个用户,所以我们选择—Open API调用访问
- 成功创建子用户AccessKey
- AccessKey ID:用户名
- AccessKey Secret:密码
- 需要将这对用户名和密码保存起来,后面在我们的程序当中会使用
权限管理
在新创建的子用户下点击添加权限
因为我们只需要使用短信服务,所以我们在搜索框输入sms,点击需要添加的权限
授权成功
表示当前我们只给该用户授予了两个权限,即使用户名和密码泄露,其他人也只能调用短信服务
授权成功之后就可以用代码的方式来调用短信服务
AccessKey泄露需要进行的处理
- AccessKey泄露出去,别人就可以使用我们的 AccessKey来发送短信,我们就需要收回我们的 AccessKey
- 我们可以禁用或者删除对应的 AccessKey
- 操作之后,相当于这个 AccessKey就作废了
15-短信发送_代码开发_参照官方文档封装发送短信工具类
参照官方文档
使用阿里云短信服务发送短信,可以参照官方提供的文档即可。
具体开发步骤:
- 导入maven坐标
<dependency> <groupId>com.aliyun</groupId> <artifactId>aliyun-java-sdk-core</artifactId> <version>4.5.16</version> </dependency> <dependency> <groupId>com.aliyun</groupId> <artifactId>aliyun-java-sdk-dysmsapi</artifactId> <version>2.1.0</version> </dependency> 12345678910
|
- 在reggie包下新建utils包,导入该工具类
package com.itzq.reggie.utils;
import com.aliyuncs.DefaultAcsClient; import com.aliyuncs.IAcsClient; import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest; import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse; import com.aliyuncs.exceptions.ClientException; import com.aliyuncs.profile.DefaultProfile;
public class SMSUtils {
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(); } }
}
1234567891011121314151617181920212223242526272829303132333435363738394041
|
查看短信服务产品文档的java SDK,了解短信服务java SDK的使用方法以及示例
16-手机验证码登录_需求分析_数据模型
需求分析
为了方便用户登录,移动端通常都会提供通过手机验证码登录的功能
手机验证码登录的优点:
- 方便快捷,无需注册,直接登录
- 使用短信验证码作为登录凭证,无需记忆密码
- 安全
登录流程:
- 输入手机号 > 获取验证码 > 输入验证码 > 点击登录 > 登录成功
注意:通过手机验证码登录,手机号是区分不同用户的标识
用户登录端界面
数据模型
通过手机验证码登录时,涉及的表为user表,即用户表。结构如下:
注意:
- 手机号是区分不同用户的标识,在用户登录的时候判断所输入的手机号是否存储在表中
- 如果不在表中,说明该用户为一个新的用户,将该用户自动保在user表中
17-手机验证码登录_代码开发_梳理交互过程&修改LoginCheckFilter
梳理交互过程
在开发代码之前,需要梳理一下登录时前端页面和服务端的交互过程:
- 在登录页面(front/page/login.html)输入手机号,点击【获取验证码】按钮,页面发送ajax请求,在服务端调用短信服务API给指定手机号发送验证码短信
- 在登录页面输入验证码,点击【登录】按钮,发送ajax请求,在服务端处理登录请求
开发手机验证码登录功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。
代码开发-准备工作
在开发业务功能前,先将需要用到的类和接口基本结构创建好:
- 实体类user (直接从课程资料中导入即可)
- Mapper接口UserMapper
- 业务层接口UserService
- 业务层实现类UserServicelmpl
- 控制层UserController
- 工具类SMSutils、ValidateCodeutils(直接从课程资料中导入即可)
实体类user
package com.itzq.reggie.entity;
import lombok.Data; import java.time.LocalDateTime; import java.util.Date; import java.util.List; import java.io.Serializable; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId;
@Data public class User implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String name;
private String phone;
private String sex;
private String idNumber;
private String avatar;
private Integer status; }
1234567891011121314151617181920212223242526272829303132333435363738394041424344
|
Mapper接口UserMapper
package com.itzq.reggie.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.itzq.reggie.entity.User; import org.apache.ibatis.annotations.Mapper;
@Mapper public interface UserMapper extends BaseMapper<User> { }
12345678910
|
业务层接口UserService
package com.itzq.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.itzq.reggie.entity.User;
public interface UserService extends IService<User> { }
12345678
|
业务层实现类UserServicelmpl
package com.itzq.reggie.service.Impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.itzq.reggie.entity.User; import com.itzq.reggie.mapper.UserMapper; import com.itzq.reggie.service.UserService; import org.springframework.stereotype.Service;
@Service public class UserServicelmpl extends ServiceImpl<UserMapper, User> implements UserService { }
123456789101112
|
控制层UserController
package com.itzq.reggie.controller;
import com.itzq.reggie.service.UserService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;
@RestController @RequestMapping("/user") @Slf4j public class UserController { @Autowired private UserService userService;
}
1234567891011121314151617
|
工具类SMSutils、ValidateCodeutils(直接从课程资料中导入即可)
- SMSutils类
package com.itzq.reggie.utils;
import com.aliyuncs.DefaultAcsClient; import com.aliyuncs.IAcsClient; import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest; import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse; import com.aliyuncs.exceptions.ClientException; import com.aliyuncs.profile.DefaultProfile;
public class SMSUtils {
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(); } }
}
1234567891011121314151617181920212223242526272829303132333435363738394041
|
- ValidateCodeutils类
package com.itzq.reggie.utils;
import java.util.Random;
public class ValidateCodeUtils {
public static Integer generateValidateCode(int length){ Integer code =null; if(length == 4){ code = new Random().nextInt(9999); if(code < 1000){ code = code + 1000; } }else if(length == 6){ code = new Random().nextInt(999999); if(code < 100000){ code = code + 100000; } }else{ throw new RuntimeException("只能生成4位或6位数字验证码"); } return code; }
public static String generateValidateCode4String(int length){ Random rdm = new Random(); String hash1 = Integer.toHexString(rdm.nextInt()); String capstr = hash1.substring(0, length); return capstr; } }
1234567891011121314151617181920212223242526272829303132333435363738394041424344
|
修改LoginCheckFilter
前面我们已经完成了LoginCheckFilter过滤器的开发,此过滤器用于检查用户的登录状态。我们在进行手机验证码登录时,发送的请求需要在此过滤器处理时直接放行。
在LoginCheckFilter类中的urls数组中,添加下面两条数据
- “/user/sendMsg”, //移动端发送短信
- “/user/login” //移动端登录
启动项目,在浏览器中输入访问地址:http://localhost:8080/front/page/login.html
注意:
使用h5开发的,自适应手机屏幕的大小,在浏览器中,需使用浏览器的手机模式打开,下面为具体步骤:
- 按住F12,弹出对应的页面
- 点击红色方框所标注的位置,将页面切换至手机浏览模式
成功显示用户登录页面
在LoginCheckFilter类下添加代码,判断用户是否登录
if(request.getSession().getAttribute("user") != null){ log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("user"));
Long userId = (Long)request.getSession().getAttribute("user"); BaseContext.setCurrentId(userId);
filterChain.doFilter(request,response); return; } 12345678910
|
添加代码的位置
18-手机验证码登录_代码开发_发送验证码短信
注意
在给出的前端资源资料中,login.html是被后面章节修改过的,因此我们需要重新导入front目录
在给出的代码目录中,找到day05下的front目录,复制该目录,将项目中的front目录覆盖
修改完成后,访问前端页面可能出现问题,因此我们需要重启项目,删除浏览器中的数据
若还是不能解决,关闭idea,重新打开idea代码编辑器
前端分析
在用户登录界面中,输入电话号码,点击获取验证码,页面会发送一个ajax请求
请求地址:http://localhost:8080/user/sendMsg
请求方式:POST
代码开发
注意
- 发送短信只需要调用封装的工具类中的方法即可
- 使用手机号登录功能流程跑通,在测试中我们不用真正的发送短信,只需要将验证码信息,通过日志输出
- 登录时,我们直接从控制台就可以看到生成的验证码(实际上也就是发送到我们手机上的验证码)
在UserController控制层中,添加sendMsg方法
@PostMapping("/sendMsg") public R<String> sendMsg(@RequestBody User user, HttpSession session){ String phone = user.getPhone();
if (StringUtils.isNotEmpty(phone)){ String code = ValidateCodeUtils.generateValidateCode4String(4); log.info("code={}",code);
session.setAttribute(phone,code); return R.success("短信发送成功"); } return R.error("短信发送失败"); } 12345678910111213141516171819
|
测试
19-手机验证码登录_代码开发_登录效验
前端分析
来到用户登录界面,按住F12,点击登录按钮,页面发送ajax请求,查看请求的地址以及方式
页面以json数据格式传输给服务端
代码开发
在UserController控制层类中,添加login方法,测试服务端是否可以接受前端提交的数据
@PostMapping("/login") public R<String> login(@RequestBody Map map, HttpSession session){
log.info(map.toString()); return R.error("短信发送失败"); }
1234567
|
重启项目,来到用户登录界面,输入正确手机号,点击获取验证码,查看服务端日志打印出的验证码信息,输入验证码,点击登录,前端发送ajax请求
前端发送ajax请求,服务端通过日志打印出手机和验证码信息,接收数据成功
注意
在login方法中,接收数据的参数类型为Map类型,也可以重新定义一个UserDto(用户类数据传输对象)用来接收数据
完善用户登录代码
@PostMapping("/login") public R<User> login(@RequestBody Map map, HttpSession session){ log.info(map.toString()); String phone = map.get("phone").toString(); String code = map.get("code").toString(); String codeInSession = session.getAttribute(phone).toString(); if (code != null && code.equals(codeInSession)){ LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(User::getPhone,phone); User user = userService.getOne(queryWrapper);
if (user == null){ user = new User(); user.setPhone(phone); userService.save(user); } return R.success(user); }
return R.error("登录失败"); } 123456789101112131415161718192021222324252627
|
20-手机验证码登录_功能测试
测试
重启项目,进入用户登录界面,输入手机号,点击获取验证码
服务器控制台打印出验证码日志信息
在用户界面输入获取到的验证码,点击登录
页面跳转到用户登录界面
- 经过分析得出:未将userId存储到session中
- 导致前端发送请求时,进入filter过滤器,判断用户登录状态为未登录状态
- 即跳转到登录界面
在login方法中,添加代码
目的:将userId保存到session当中,前端页面发送请求到服务端,经过filter过滤器,判断用户状态为已登录状态,页面将不会跳转到用户登录界面
重启项目,在用户登录页面输入手机号,和获取到的验证码,点击登录
页面成功跳转到服务用户界面
user表中成功添加上测试的手机号码(未注册的手机号码)
业务开发Day6
01-本章内容介绍
目录
- 导入用户地址簿相关功能代码
- 菜品展示
- 购物车
- 下单
效果展示
02-导入用户地址簿相关代码
需求分析
- 地址簿,指的是移动端消费者用户的地址信息
- 用户登录成功后可以维护自己的地址信息
- 同一个用户可以有多个地址信息,但是只能有一个默认地址。
页面展示
- 新增收货地址页面
- 地址管理页面
- 编辑收货地址页面
数据模型
用户的地址信息会存储在address_book表,即地址簿表中。具体表结构如下:
导入功能代码
功能代码清单:
- 实体类AddressBook(直接从课程资料中导入即可)
- Mapper接口AddressBookMapper
- 业务层接口AddressBookService
- 业务层实现类AddressBookServicelmpl
- 控制层AddressBookController(直接从课程资料中导入即可)
实体类AddressBook
package com.itzq.reggie.entity;
import com.baomidou.mybatisplus.annotation.FieldFill; import com.baomidou.mybatisplus.annotation.TableField; import lombok.Data; import java.io.Serializable; import java.time.LocalDateTime;
@Data public class AddressBook implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private Long userId;
private String consignee;
private String phone;
private String sex;
private String provinceCode;
private String provinceName;
private String cityCode;
private String cityName;
private String districtCode;
private String districtName;
private String detail;
private String label;
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;
private Integer isDeleted; }
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
|
Mapper接口AddressBookMapper
package com.itzq.reggie.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.itzq.reggie.entity.AddressBook; import org.apache.ibatis.annotations.Mapper;
@Mapper public interface AddressBookMapper extends BaseMapper<AddressBook> { }
12345678910
|
业务层接口AddressBookService
package com.itzq.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.itzq.reggie.entity.AddressBook;
public interface AddressBookService extends IService<AddressBook> { }
12345678
|
业务层实现类AddressBookServicelmpl
package com.itzq.reggie.service.Impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.itzq.reggie.entity.AddressBook; import com.itzq.reggie.mapper.AddressBookMapper; import com.itzq.reggie.service.AddressBookService; import org.springframework.stereotype.Service;
@Service public class AddressBookServicelmpl extends ServiceImpl<AddressBookMapper, AddressBook> implements AddressBookService { }
123456789101112
|
控制层AddressBookController
import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired;
@RestController @RequestMapping("/addressBook") @Slf4j public class AddressBookController { @Autowired private AddressBookService addressBookService; 123456789
|
完善地址管理页面
前端分析
点击方框里的图标,跳转到个人中心
在个人中心界面,点击地址管理
点击地址管理后,前端发送ajax请求,以下是该请求的地址和方式
新增代码
在AddressBookController控制层中,添加list方法
目的:查询指定用户的全部地址
@GetMapping("/list") public R<List<AddressBook>> list(AddressBook addressBook){ addressBook.setUserId(BaseContext.getCurrentId()); log.info("addressBook={}",addressBook);
LambdaQueryWrapper<AddressBook> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(addressBook.getUserId() != null, AddressBook::getUserId,addressBook.getUserId()); queryWrapper.orderByDesc(AddressBook::getUpdateTime);
List<AddressBook> list = addressBookService.list(queryWrapper); return R.success(list); } 12345678910111213
|
新增收货地址页面
前端分析
来到新增收货地址界面,输入相关信息,点击保存
前端发送ajax请求,以及请求的方式,服务端未响应,所以报404错误
新增代码
在AddressBookController控制层中,添加save方法
目的:将前端以json格式传输到后端的数据,保存到数据库中
@PostMapping public R<AddressBook> save(@RequestBody AddressBook addressBook){ addressBook.setUserId(BaseContext.getCurrentId()); log.info("addressBook={}",addressBook);
addressBookService.save(addressBook);
return R.success(addressBook); } 123456789
|
测试-1
重启项目,来到新增收货地址页面,输入地址信息后,点击保存地址
页面跳转到地址管理界面,前端发送ajax请求—显示出该用户所有地址
测试成功
设置默认地址
前端分析
在地址管理界面,点击圆圈—将该地址设置为默认地址
点击后,前端发送ajax请求,以下为请求地址和请求方式
新增代码
在AddressBookController控制层中,添加getDefault方法
目的:设置默认地址
@PutMapping("/default") public R<AddressBook> getDefault(@RequestBody AddressBook addressBook){ addressBook.setUserId(BaseContext.getCurrentId());
LambdaUpdateWrapper<AddressBook> updateWrapper = new LambdaUpdateWrapper<>(); updateWrapper.eq(addressBook.getUserId() != null,AddressBook::getUserId,addressBook.getUserId()); updateWrapper.set(AddressBook::getIsDefault,0);
addressBookService.update(updateWrapper);
addressBook.setIsDefault(1); addressBookService.updateById(addressBook);
return R.success(addressBook); } 123456789101112131415161718
|
测试-2
重启项目,来到地址管理界面,点击设置为默认地址
测试成功
成功将该地址设置为默认地址
该用户address_book表中,该地址is_default字段从0变为1,表示该地址为该用户的默认地址
03-菜品展示_需求分析
需求分析
- 用户登录成功后跳转到系统首页,在首页需要根据分类来展示菜品和套餐
- 如果菜品设置了口味信息,需要展示选择规格按钮,否则显示+按钮
页面效果展示
04-菜品展示_代码开发_梳理交互过程
梳理交互过程
在开发代码之前,需要梳理一下前端页面和服务端的交互过程:
- 页面(front/index.html)发送ajax请求,获取分类数据(菜品分类和套餐分类)
- 页面发送ajax请求,获取第一个分类下的菜品或者套餐
开发菜品展示功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可
前端分析
注意:首页加载完成后,页面还发送了一次ajax请求用于加载购物车数据
- 前端页面初始化数据发送ajax请求—获取所有菜品和套餐分类
该请求方法在此之前已存在—查询分类信息
- 前端页面初始化数据发送ajax请求— 获取购物车类商品集合
注此处可以将这次请求的地址暂时修改一下,从静态json文件中获取数据,等后续开发购物车功能时再修改回来,如下:
在front包下添加json文件
- json文件名:cartData.json
- json文件代码:
{"code":1,"msg":null,"data":[],"map":{}} 1
|
为什么只有以上两个ajax请求同时接收到页面返回的数据后才能正常显示?
因为Promse.all在处理多个异步请求时,需要等待绑定的每个ajax请求返回数据以后才能正常显示
页面在初始化时,发送ajax请求,获取第一个分类下的菜品或套餐
下面是请求地址和请求方式—(由请求地址可知,该请求为获取第一个分类下的菜品信息)
该请求也是在此之前已存在—通过条件查询获取该种类下的所有菜品信息,并且查询的菜品必须为起售状态(status=1)
测试
- 因修改过前端代码,所以需要清空浏览器的缓存数据(Ctrl+Shift+Delete),并重启项目
- 若还是不能正常访问,重启idea代码编辑器
显示成功
注意:
- 前端页面的需求:如果菜品设置了口味信息,需要展示选择规格按钮,否则显示+按钮
- 但返回的Dish类型中未包含菜品口味信息
- 所以需要修改部分代码让返回值既包含菜品的基本信息,也包含了口味的信息
05-菜品展示_代码开发_修改DishController的list方法并测试
修改DishController的list方法
- 添加的SQL语句为:select * from dish_flavors where dish_id = ?—转化为代码形式
- 查询出数据后,将查询的数据放入dishDto(dish的数据转换对象)对象中
- 返回list集合,list中对象类型为:DishDto
@GetMapping("/list") public R<List<DishDto>> list(Dish dish){
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(dish.getCategoryId() != null, Dish::getCategoryId, dish.getCategoryId()); queryWrapper.eq(Dish::getStatus,1);
List<Dish> list = dishService.list(queryWrapper);
List<DishDto> dishDtoList = list.stream().map((item) -> { DishDto dishDto = new DishDto(); BeanUtils.copyProperties(item,dishDto); Long categoryId = item.getCategoryId(); Category category = categoryService.getById(categoryId); if(category != null){ String categoryName = category.getName(); dishDto.setCategoryName(categoryName); }
Long dishId = item.getId();
LambdaQueryWrapper<DishFlavor> dishFlavorLambdaQueryWrapper= new LambdaQueryWrapper<>(); dishFlavorLambdaQueryWrapper.eq(dishId != null,DishFlavor::getDishId,dishId); List<DishFlavor> dishFlavors = dishFlavorService.list(dishFlavorLambdaQueryWrapper);
dishDto.setFlavors(dishFlavors);
return dishDto; }).collect(Collectors.toList());
return R.success(dishDtoList); } 1234567891011121314151617181920212223242526272829303132333435363738394041
|
测试
启动项目,点击登录,来到客户端首页,若该菜品有相应的口味选择,前端页面展示选择规格按钮;反之,前端页面展示+
按钮
06-菜品展示_代码开发_创建SetmealController的list方法并测试
前端分析
点击套餐分类时,报404异常,因为还未在服务端做相应的映射地址
- 点击套餐分类时的请求地址以及请求方式
- 通过url方式传参,参数类型为key-value键值对形式
创建SetmealController的list方法
在SetmealController控制层中,添加list方法
目的:通过套餐种类Id和套餐对应的状态查询出符合条件的套餐
@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); } 123456789101112131415
|
测试
前端页面成功显示出儿童套餐种类包含的套餐
07-购物车_需求分析&数据模型&梳理交互过程&准备工作
需求分析
- 移动端用户可以将菜品或者套餐添加到购物车
- 对于菜品来说,如果设置了口味信息,则需要选择规格后才能加入购物车
- 对于套餐来说,可以直接点击将当前套餐加入购物车
- 在购物车中可以修改菜品和套餐的数量,也可以清空购物车。
前端页面展示
数据模型
购物车对应的数据表为shopping_cart表,具体表结构如下:
梳理交互过程
在开发代码之前,需要梳理一下购物车操作时前端页面和服务端的交互过程:
- 点击加入购物车按钮或者*+*按钮,页面发送ajax请求,请求服务端,将菜品或者套餐添加到购物车
- 点击购物车图标,页面发送ajax请求,请求服务端查询购物车中的菜品和套餐
- 点击清空购物车按钮,页面发送ajax请求,请求服务端来执行清空购物车操作
开发购物车功能,其实就是在服务端编写代码去处理前端页面发送的这3次请求即可
准备工作
在开发业务功能前,先将需要用到的类和接口基本结构创建好
- 实体类ShoppingCart(直接从课程资料中导入即可)
- Mapper接口ShoppingCartMapper
- 业务层接口ShoppingCartService
- 业务层实现类ShoppingCartServicelmpl
- 控制层ShoppingCartController
实体类ShoppingCart
package com.itzq.reggie.entity;
import lombok.Data; import java.io.Serializable; import java.math.BigDecimal; import java.time.LocalDateTime;
@Data public class ShoppingCart implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String name;
private Long userId;
private Long dishId;
private Long setmealId;
private String dishFlavor;
private Integer number;
private BigDecimal amount;
private String image;
private LocalDateTime createTime; }
1234567891011121314151617181920212223242526272829303132333435363738394041424344
|
Mapper接口ShoppingCartMapper
package com.itzq.reggie.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.itzq.reggie.entity.ShoppingCart; import org.apache.ibatis.annotations.Mapper;
@Mapper public interface ShoppingCartMapper extends BaseMapper<ShoppingCart> { }
12345678910
|
业务层接口ShoppingCartService
package com.itzq.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.itzq.reggie.entity.ShoppingCart;
public interface ShoppingCartService extends IService<ShoppingCart> { }
12345678
|
业务层实现类ShoppingCartServicelmpl
package com.itzq.reggie.service.Impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.itzq.reggie.entity.ShoppingCart; import com.itzq.reggie.mapper.ShoppingCartMapper; import com.itzq.reggie.service.ShoppingCartService; import org.springframework.stereotype.Service;
@Service public class ShoppingCartServicelmpl extends ServiceImpl<ShoppingCartMapper, ShoppingCart> implements ShoppingCartService { }
123456789101112
|
控制层ShoppingCartController
package com.itzq.reggie.controller;
import com.itzq.reggie.service.ShoppingCartService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;
@RestController @RequestMapping("/shoppingCart") @Slf4j public class ShoppingCartController { @Autowired private ShoppingCartService shoppingCartService; }
12345678910111213141516
|
08-购物车_代码开发_添加购物车
前端分析
启动项目,用户登录外卖系统,选择喜欢的菜品或套餐,若用户选择菜品并且该菜品有相应的口味则需要选择口味,点击加入购物车
- 页面效果展示
- 前端页面发送的请求地址以及请求方式
- 前端页面以json格式将数据提交给服务端
代码
测试服务端是否能够接收前端页面提交的数据
在ShoppingCartController控制层,新增add方法(测试服务端是否可以成功接收前端页面提交的数据)
@RestController @RequestMapping("/shoppingCart") @Slf4j public class ShoppingCartController { @Autowired private ShoppingCartService shoppingCartService;
@PostMapping("/add") public R<ShoppingCart> add(@RequestBody ShoppingCart shoppingCart){ log.info("shoppingCart={}",shoppingCart); return null; } } 1234567891011121314
|
在指定位置添加断点,便于查看是否成功接收数据
debug方式启动项目,添加菜品或套餐到购物车
跳转到服务端,服务端成功接收到前端提交的数据
完善代码
在ShoppingCartController控制层中的add方法添加代码
目的:将用户添加到购物车的菜品或套餐信息保存到数据库中,若添加相同菜品或相同套餐,则只需要在shopping_cart表更新该菜品或者套餐的数量(number字段);反之,则将菜品或套餐直接保存数据库中
package com.itzq.reggie.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.itzq.reggie.common.BaseContext; import com.itzq.reggie.common.R; import com.itzq.reggie.entity.ShoppingCart; import com.itzq.reggie.service.ShoppingCartService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;
@RestController @RequestMapping("/shoppingCart") @Slf4j public class ShoppingCartController { @Autowired private ShoppingCartService shoppingCartService;
@PostMapping("/add") public R<ShoppingCart> add(@RequestBody ShoppingCart shoppingCart){ log.info("shoppingCart={}",shoppingCart);
Long currentId = BaseContext.getCurrentId(); shoppingCart.setUserId(currentId);
Long dishId = shoppingCart.getDishId(); LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
if (dishId != null){ queryWrapper.eq(ShoppingCart::getDishId,dishId); }else { queryWrapper.eq(ShoppingCart::getSetmealId,shoppingCart.getSetmealId()); }
ShoppingCart cartServiceOne = shoppingCartService.getOne(queryWrapper);
if (cartServiceOne != null){
Integer number = cartServiceOne.getNumber(); cartServiceOne.setNumber(number + 1); shoppingCartService.updateById(cartServiceOne); }else {
shoppingCartService.save(shoppingCart); cartServiceOne = shoppingCart; }
return R.success(cartServiceOne); } }
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
|
测试
重启项目,选择菜品或套餐,加入到购物车中
shopping_cart表中添加上用户选择的菜品信息
点击加号或减号可以修改该菜品的数量
目前还未开发减号相关的代码,所以只有加号可以正常操作
shopping_cart表中的number字段值得到更新
注意:amount字段的值为该菜品价格,而与数量无关
09-购物车_代码开发_查看购物车&清空购物车
查看购物车
前端分析
查看购物车,前端页面发送ajax请求,以下是请求的方式和请求的地址
代码开发
在ShoppingCartController类中,添加list方法
@GetMapping("list") public R<List<ShoppingCart>> list(){ LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(ShoppingCart::getUserId,BaseContext.getCurrentId()); queryWrapper.orderByDesc(ShoppingCart::getCreateTime);
List<ShoppingCart> list = shoppingCartService.list(queryWrapper);
return R.success(list); } 1234567891011121314
|
测试
重启项目,选择所需菜品或套餐,若需选择口味,则在选择口味后点击加入购物车
点击加入购物车后,页面发送三个ajax请求到服务端
- add— 》 将所选菜品或套餐添加到数据库表中
- list— 》 查询用户的购物车信息(需要测试的方法)
- download— 》 图片下载
查询用户购物车信息成功,并在页面展示
清空购物车
前端分析
点击清空用户购物车,页面发送ajax请求,以下是请求的方式和请求的地址
代码开发
在ShoppingCartController类中,添加clean方法
@DeleteMapping("/clean") public R<String> clean(){ LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());
shoppingCartService.remove(queryWrapper);
return R.success("成功清空购物车"); } 12345678910
|
测试
重启项目,来到购物车界面,点击清空按钮
执行成功
10-用户下单_需求分析&数据模型
需求分析
移动端用户将菜品或者套餐加入购物车后,可以点击购物车中的去结算按钮,页面跳转到订单确认页面,点击去支付按钮,完成下单操作
前端页面效果展示
数据模型
用户下单业务对应的数据表为orders表和order_detail表
- orders:订单表,具体表结构如下:
- order_detail:订单明细表,具体表结构如下:
11-用户下单_梳理交互过程&准备工作
梳理交互过程
在开发代码之前,需要梳理一下用户下单操作时前端页面和服务端的交互过程:
- 在购物车中点击去结算按钮,页面跳转到订单确认页面
- 在订单确认页面,发送ajax请求,请求服务端获取当前登录用户的默认地址
- 在订单确认页面,发送ajax请求,请求服务端获取当前登录用户的购物车数据
- 在订单确认页面点击去支付按钮,发送ajax请求,请求服务端完成下单操作
开发用户下单功能,其实就是在服务端编写代码去处理前端页面发送的请求即可
准备工作
在开发业务功能前,先将需要用到的类和接口基本结构创建好:
- 实体类Orders、OrderDetail(直接从课程资料中导入即可)
- Mapper接口OrderMapper、OrderDetailMapper
- 业务层接口OrderService、OrderDetailService
- 业务层实现类OrderServicelmpl、OrderDetailServicelmpl
- 控制层OrderController、OrderDetailController
实体类
Orders
package com.itzq.reggie.entity;
import lombok.Data; import java.io.Serializable; import java.math.BigDecimal; import java.time.LocalDateTime;
@Data public class Orders implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String number;
private Integer status;
private Long userId;
private Long addressBookId;
private LocalDateTime orderTime;
private LocalDateTime checkoutTime;
private Integer payMethod;
private BigDecimal amount;
private String remark;
private String userName;
private String phone;
private String address;
private String consignee; }
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162
|
OrderDetail
package com.itzq.reggie.entity;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import lombok.Data; import java.io.Serializable; import java.math.BigDecimal;
@Data public class OrderDetail implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String name;
private Long orderId;
private Long dishId;
private Long setmealId;
private String dishFlavor;
private Integer number;
private BigDecimal amount;
private String image; }
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
|
Mapper接口
OrderMapper
package com.itzq.reggie.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.itzq.reggie.entity.Orders; import org.apache.ibatis.annotations.Mapper;
@Mapper public interface OrderMapper extends BaseMapper<Orders> { }
12345678910
|
OrderDetailMapper
package com.itzq.reggie.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.itzq.reggie.entity.OrderDetail; import org.apache.ibatis.annotations.Mapper;
@Mapper public interface OrderDetailMapper extends BaseMapper<OrderDetail> { }
1234567891011
|
业务层接口
OrderService
package com.itzq.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.itzq.reggie.entity.Orders;
public interface OrderService extends IService<Orders> { }
12345678
|
OrderDetailService
package com.itzq.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.itzq.reggie.entity.OrderDetail;
public interface OrderDetailService extends IService<OrderDetail> { }
12345678
|
业务层实现类
OrderServicelmpl
package com.itzq.reggie.service.Impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.itzq.reggie.entity.Orders; import com.itzq.reggie.mapper.OrderMapper; import com.itzq.reggie.service.OrderService; import org.springframework.stereotype.Service;
@Service public class OrderServicelmpl extends ServiceImpl<OrderMapper, Orders> implements OrderService { }
123456789101112
|
OrderDetailServicelmpl
package com.itzq.reggie.service.Impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.itzq.reggie.entity.OrderDetail; import com.itzq.reggie.mapper.OrderDetailMapper; import com.itzq.reggie.service.OrderDetailService; import org.springframework.stereotype.Service;
@Service public class OrderDetailServicelmpl extends ServiceImpl<OrderDetailMapper, OrderDetail> implements OrderDetailService { }
123456789101112
|
控制层
OrderController
package com.itzq.reggie.controller;
import com.itzq.reggie.service.OrderService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;
@RestController @RequestMapping("/order") @Slf4j public class OrderController { @Autowired private OrderService orderService;
}
1234567891011121314151617
|
OrderDetailController
package com.itzq.reggie.controller;
import com.itzq.reggie.service.OrderDetailService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;
@RestController @RequestMapping("/orderDetail") @Slf4j public class OrderDetailController { @Autowired private OrderDetailService orderDetailService;
}
1234567891011121314151617
|
12-用户下单_代码开发1
前端分析
启动项目,来到外卖初始界面,点击去结算按钮
页面跳转到确认订单界面,前端界面发送ajax请求,用于获取该用户的默认地址,发现请求失败,服务端没有对应的映射
在AddressBookController控制层,添加getDefault,请求方式为get请求
@GetMapping("/default") public R<AddressBook> getDefault(){ Long currentId = BaseContext.getCurrentId();
LambdaQueryWrapper<AddressBook> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(AddressBook::getUserId,currentId); queryWrapper.eq(AddressBook::getIsDefault,1);
AddressBook addressBook = addressBookService.getOne(queryWrapper);
return R.success(addressBook); } 1234567891011121314
|
重启项目,点击去结算按钮,跳转到确认订单页面,成功接收到该用户的默认地址
处理好确认订单页面后,点击去支付,系统报404错误—服务端还未添加相应的映射
以下展示了前端页面发送ajax请求的方式以及请求的地址
提交给服务端的数据格式为json数据
代码开发1
在业务层接口OrderService中添加submit方法
package com.itzq.reggie.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.itzq.reggie.entity.Orders;
public interface OrderService extends IService<Orders> {
void submit(Orders orders); }
12345678910111213
|
在业务层实现类OrderServicelmpl中,实现业务层接口OrderService添加的submit方法
package com.itzq.reggie.service.Impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.itzq.reggie.entity.Orders; import com.itzq.reggie.mapper.OrderMapper; import com.itzq.reggie.service.OrderService; import org.springframework.stereotype.Service;
@Service public class OrderServicelmpl extends ServiceImpl<OrderMapper, Orders> implements OrderService {
@Override public void submit(Orders orders) {
} }
1234567891011121314151617181920212223242526272829
|
在OrderController类中,添加submit方法—通过调用orderService接口实现对数据库的操作
package com.itzq.reggie.controller;
import com.itzq.reggie.common.R; import com.itzq.reggie.entity.Orders; import com.itzq.reggie.service.OrderService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;
@RestController @RequestMapping("/order") @Slf4j public class OrderController { @Autowired private OrderService orderService;
@PostMapping("/submit") public R<String> submit(@RequestBody Orders orders){ log.info("orders={}",orders);
orderService.submit(orders); return R.success("用户下单成功"); } }
12345678910111213141516171819202122232425262728
|
13-用户下单_代码开发2
代码开发2
在业务层实现类OrderServicelmpl的submit方法中,添加逻辑代码
- 获取当前用户id
- 查询当前用户的购物车数据
- 查询用户数据
- 查询地址信息
@Override @Transactional public void submit(Orders orders) { Long currentId = BaseContext.getCurrentId();
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(ShoppingCart::getUserId,currentId); List<ShoppingCart> shoppingCarts = shoppingCartService.list();
if (shoppingCarts == null || shoppingCarts.size() == 0){ throw new CustomException("购物车为空,不能下单"); }
User user = userService.getById(currentId);
Long addressBookId = orders.getAddressBookId(); AddressBook addressBook = addressBookService.getById(addressBookId); if (addressBook == null){ throw new CustomException("地址信息有误,不能下单"); }
} 1234567891011121314151617181920212223242526272829303132333435
|
14-用户下单_代码开发3
代码开发3
提供完整的submit方法代码
- 设置订单id
- 向订单表设置属性
- 向订单表插入数据,一条数据
- 通过stream流,遍历购物车数据来获取的订单明细
- 向订单明细表插入数据,多条数据
- 清空购物车—通过用户id作为约束条件
package com.itzq.reggie.service.Impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.IdWorker; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.itzq.reggie.common.BaseContext; import com.itzq.reggie.common.CustomException; import com.itzq.reggie.entity.*; import com.itzq.reggie.mapper.OrderMapper; import com.itzq.reggie.service.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors;
@Service public class OrderServicelmpl extends ServiceImpl<OrderMapper, Orders> implements OrderService {
@Autowired private ShoppingCartService shoppingCartService;
@Autowired private UserService userService;
@Autowired private AddressBookService addressBookService;
@Autowired private OrderDetailService orderDetailService;
@Override @Transactional public void submit(Orders orders) { Long currentId = BaseContext.getCurrentId();
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(ShoppingCart::getUserId,currentId); List<ShoppingCart> shoppingCarts = shoppingCartService.list();
if (shoppingCarts == null || shoppingCarts.size() == 0){ throw new CustomException("购物车为空,不能下单"); }
User user = userService.getById(currentId);
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> orderDetailList= shoppingCarts.stream().map((item) -> { OrderDetail orderDetail = new OrderDetail(); orderDetail.setOrderId(orderId); orderDetail.setName(item.getName()); orderDetail.setImage(item.getImage()); orderDetail.setDishId(item.getDishId()); orderDetail.setSetmealId(item.getSetmealId()); orderDetail.setDishFlavor(item.getDishFlavor()); orderDetail.setNumber(item.getNumber()); orderDetail.setAmount(item.getAmount()); amount.addAndGet(item.getAmount().multiply(new BigDecimal(item.getNumber())).intValue());
return orderDetail; }).collect(Collectors.toList());
orders.setId(orderId); orders.setNumber(String.valueOf(orderId)); orders.setStatus(2); orders.setUserId(currentId); orders.setAddressBookId(addressBookId); orders.setOrderTime(LocalDateTime.now()); orders.setCheckoutTime(LocalDateTime.now()); orders.setAmount(new BigDecimal(amount.get())); orders.setPhone(addressBook.getPhone()); orders.setUserName(user.getName()); orders.setConsignee(addressBook.getConsignee()); orders.setAddress( (addressBook.getProvinceName() == null ? "":addressBook.getProvinceName())+ (addressBook.getCityName() == null ? "":addressBook.getCityName())+ (addressBook.getDistrictName() == null ? "":addressBook.getDistrictName())+ (addressBook.getDetail() == null ? "":addressBook.getDetail()) ); super.save(orders);
orderDetailService.saveBatch(orderDetailList);
shoppingCartService.remove(queryWrapper);
} }
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
|
15-用户下单_功能测试
功能测试
重启项目,登录用户,来到确定订单页面,点击去支付
支付成功
数据库中orders表的变化:
数据库中order_detail表的变化:
操作成功