前言
作为第一个练手项目,巩固学习之前学习的知识
教学地址
https://www.bilibili.com/video/BV13a411q753?p=6&spm_id_from=pageDriver&vd_source=2497f00c3af066126b298d1871671eec
本次项目中使用到的技术
- spring
- springMVC
- springboot
- mybatis-plus
- mysql
- Linux
- lombok
- fastjson
- druid
- mysql
- vue2
- ElementUI
- ajax
- commons-email
- tencentcloud-sdk-java
项目前言(放在这儿!必看!)
- 教程中的业务逻辑都是写在controller层中的,本次代码按照规范把controller中的业务逻辑都放在了service层中方便其他controller复用
- 提供的pojo中没有写逻辑删除,但是提供的数据表中保留了逻辑删除字段,本次开发过程中增加逻辑删除功能
- 在开发前了解了代码整体结构,发现本项目使用的测试类较少,本次开发中尽量使用测试类测试代码,但本次以练习springboot+mybatis-plus练手为目的,这一步有部分省略。
- 教程中含有部分教学知识,部分模块使用教学新知识+代码编写的方式进行教学,根据目录看所教知识并不深如Redis,后续还需要通过其他教程学习。
- 开发按照模块根据业务需求先自己思考编写代码,然后再回顾教程,以加深印象
- 了解提供的前端源码、顺便替换了logo和部分样式、修改了前端登录成功后返回登录页面登录按钮还是“登陆中…”的bug
项目总结
- 项目其实主要还是CRUD的操作,基本上不用mapper.xml,这也是针对mybatis-plus的练习吧,项目还是以练习springboot+mybatis的CRUD为主,适合刚刚学完SpringMVC+springboot来练练手,刚开始做的几个模块还要跟着教程一个一个做,熟练了之后后面的模块大部分都是自己开发,昨完再看看教程看下有一些业务逻辑是不是遗漏的,这个项目做完CRUD肯定没问题了,但是sql还需要锻炼
- 这个项目主要是在后端,可以不用纠结与前端的一些代码,我就有时候看看前端修修改改其实挺费时的,因为一些语法或者是前端的代码对于我学后端的来说没那么熟悉
- 粗略看了下后面的redis等课程都是一些入门的操作,其实还是觉得应该系统学一下,就不看这个项目的来学习了
一点小惊喜
- 本文章被CSDN-AI翻牌子了
项目搭建
一、准备工作
- 创建一个springboot项目gigot_takeaway,勾选勾选Spring Web,MySQL和MyBatis,然后在pom.xml中导入druid,lombok和MyBatisPlus的坐标;导入前端文件和数据库表
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.23</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
- 将前端文件backend、front放行。注:可通过放在static目录下让资源自动放心,看弹幕说放static后续可能出现bug所以和教程保持一致
@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
/**
* 设置静态资源放行路径
*/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
log.info("静态资源放行成功...");
}
}
- 创建分类包,mapper、pojo、service、controller等完成项目框架搭建,并使用mp(mybatis-plus后续简称)的内置类,将service,serviceImpl继承相应方法,会自动生成selectAll等常用方法
- 创建通用的返回响应体类R,类包括code(返回状态编码)、msg(返回信息)、data(封装json属性)、map(封装动态对象)四个属性,并在其中增加两个三个静态方法用于设置上述四个属性
package com.gigottakeaway.common;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
@Data
public class R<T> {
private Integer code; //编码:1成功,0和其它数字为失败
private String msg; //错误信息
private T data; //数据
private Map map = new HashMap(); //动态数据
public static <T> R<T> success(T object) {
R<T> r = new R<T>();
r.data = object;
r.code = 1;
return r;
}
public static <T> R<T> error(String msg) {
R r = new R();
r.msg = msg;
r.code = 0;
return r;
}
public R<T> add(String key, Object value) {
this.map.put(key, value);
return this;
}
}
二、后台系统登录功能
1. 用户登录
功能分析
- 将用户输入的密码进行md5加密处理
- 根据username查询数据库
- 如果没有查询到则返回登录失败结果
- 判断密码
- 判断用户是否被禁用
- id存入session
- 通过封装类型R返回前台
- 编写测试类测试以上功能
- 完成前端到后端测试
ps:教程中是在controller中书写的,service层到controller层如何使用统一封装类并且传入session查阅了很多资料,没办法找了chatgpt问了下,是这样回复
业务逻辑代码
package com.gigottakeaway.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.gigottakeaway.common.R;
import com.gigottakeaway.mapper.EmployeeMapper;
import com.gigottakeaway.pojo.Employee;
import com.gigottakeaway.service.EmployeeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.nio.charset.StandardCharsets;
@Slf4j
@Service
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements EmployeeService {
@Autowired
private EmployeeMapper employeeMapper;
@Autowired
HttpServletRequest request;
public R login(Employee employee){
//将密码进行md5加密处理
String password = employee.getPassword();
password = DigestUtils.md5DigestAsHex(password.getBytes());
//根据username查询数据库
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Employee::getUsername,employee.getUsername());
//注:用户名在数据库中有唯一约束
employee = employeeMapper.selectOne(queryWrapper);
//如果没有查询到则返回登录失败结果
if(employee == null){
return R.error("用户名或密码错误,请重新输入!");
}
//判断密码
if(!employee.getPassword().equals(password)) {
return R.error("用户名或密码错误,请重新输入!");
}
//判断用户是否被禁用
if(employee.getStatus() == 0){
return R.error("该用户已被冻结,请联系系统管理员");
}
//id存入session
HttpSession session = request.getSession();
session.setAttribute("employee",employee.getId());
return R.success("登录成功");
}
}
登录功能前端页面分析
采用内嵌窗的方式,每一个标签栏对应一个地址,如果需要增加或修改可以直接修改这一块,具体可以看代码中的iframeUrl模型,了解实现方式
2. 退出登录功能
功能分析
- 删除用户session
- 向前端返回删除成功,由前端跳转页面
业务逻辑代码
/**
* 退出登录功能实现、清理Session中的用户id,返回结果
* @return
*/
@PostMapping("/logout")
public R logout(HttpServletRequest request){
//清理session保存的登录id
request.getSession().removeAttribute("employee");
return R.success("退出登录成功");
}
3. 登录拦截器
功能分析
- 创建一个拦截器,忘了看这儿https://www.bilibili.com/video/BV13a411q753?p=16&vd_source=2497f00c3af066126b298d1871671eec
- 拦截所有资源并排除登录及静态资源相关地址
- 使用AntPathMatcher工具类路径匹配器,支持通配符的方式匹配,如果有上述地址放行
- 判断用户是否已登录,已登录放行
- 封装对象给前端,因前端的需求,因此使用输出流的方式传给前端一个R封装对象
业务逻辑代码
package com.gigottakeaway.filter;
import com.alibaba.fastjson.JSON;
import com.gigottakeaway.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;
/**
* 检查用户是否完成登录,登录后放行资源
*/
@Slf4j
@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
public class LoginCheckFilter implements Filter {
//路径匹配器,支持通配符
public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
log.info("当前拦截到的请求:{}",request.getRequestURI());
//定义排除登录相关功能地址和静态资源相关地址
String[] urls = new String[]{
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**"
};
//判断本次请求是否在上面需要排除的地址中,如果在放行
if(check(urls,request.getRequestURI())){
log.info("用户访问登录或相关静态资源地址放行资源");
filterChain.doFilter(request,response);
return;
}
//判断用户是否已登录,已登录放行
if(request.getSession().getAttribute("employee") != null){
log.info("{}已登录,放行资源",request.getSession().getAttribute("employee"));
filterChain.doFilter(request,response);
return;
}
//前端需求代码中有拦截器,因此这这儿使用输出流的方式传给前端一个R封装对象
log.info("用户未登录返回前台,前端进行地址跳转");
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
return;
}
/**
* 路径匹配,用于排除登录相关资源的校验方法
*/
public boolean check(String[] urls,String requestUri){
for (String url : urls) {
//PATH_MATCHER.match(url,requestUri)方法,用于判断两个地址是否匹配,并且支持通配符的格式,如果匹配商返回true,否则返回false
boolean flag = PATH_MATCHER.match(url,requestUri);
if(flag){
return true;
}
}
return false;
}
}
4. 新增员工
功能分析
- 获取前端用户输入的数据
- 重写mp的save方法,增加默认密码(从yml中获取)、创建人(从session中获取)、创建时间、修改人、修改时间
- 封装R给前端
业务逻辑代码
@Override
public boolean save(Employee entity) {
log.info(""+entity);
Long getLoginId = (Long) request.getSession().getAttribute("employee");
entity.setPassword(DigestUtils.md5DigestAsHex(defalutPassword.getBytes()));
entity.setCreateUser(getLoginId);
entity.setCreateTime(LocalDateTime.now());
entity.setUpdateUser(getLoginId);
entity.setUpdateTime(LocalDateTime.now());
return super.save(entity);
}
5. 全局异常处理器
用户名重复报错处理
package com.gigottakeaway.common;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.sql.SQLIntegrityConstraintViolationException;
/**
* 全局异常处理
*/
@RestControllerAdvice(annotations = {RestController.class, Controller.class})
@Slf4j
public class GlobalExceptionHandler {
/**
* 异常处理方法
* @return
*/
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
log.error(ex.getMessage());
if(ex.getMessage().contains("Duplicate entry")){
//根据空格分割开异常
String[] split = ex.getMessage().split(" ");
//Duplicate entry 'pyw' for key 'employee.idx_username' 获取第三位的名称,并返回给前端,其他错误则抛给用户~
return R.error("该用户"+split[2]+"已存在,请换一个账号名称吧~");
}
return R.error("服务器处理异常,请再试一次~");
}
}
6. 员工信息分页查询
功能分析
- 接收前端传输的页码,分页大小,查询条件
- 接收前端信息并传入MP的Page类中交给MP存储
- 增加条件语句,并进行判断输入的条件是否为空,如果为空不增加此条件
- 获取分页数据
- 返回封装对象R把分页对象给前端
业务逻辑代码
public R<Page> selectPage(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();
//添加条件,如果name为空则不添加条件
queryWrapper.like(StringUtils.isNotEmpty(name),Employee::getName,name);
//获取分页数据,此方法会根据MP的IPage类自动封装数据给pageInfo,不需要重新取值
employeeMapper.selectPage(pageInfo,queryWrapper);
//返回数据给 前端名称:records 总数total
return R.success(pageInfo);
}
测试类
@Test
void setEmployeeServiceSelectPageTest(){
log.info(employeeService.selectPage(1,10,null)+"");
}
解决问题记录
- 混淆了IPage和Page的作用,Page对象通过selectPage()方法调用后会自动通过IPage类进行封装,不需要再赋值了
IPage和Page的区别链接:https://blog.csdn.net/m0_56231256/article/details/121503526
7. 员工账号禁用、启用
功能分析
1、后端接收前端修改的账号id和修改后的状态
2、重写MP的updateById方法,将修改用户、修改时间存入实体类中
3、封装对象给前端
业务逻辑代码
@Override
public boolean updateById(Employee entity) {
entity.setUpdateUser(GetSessionUtil.getEmployeeIdForSession(request));
entity.setUpdateTime(LocalDateTime.now());
return super.updateById(entity);
}
解决问题记录(非常非常重要!!!)
- 后端Long传输给前端JS如果位数过长会造成精度丢失
实例在var中存储1407898335821492226显示结果如下后两位精度丢失:
解决方案(了解即可,复制粘贴):
1、导入JacksonObjectMapper
2、在WebMvcConfig添加以下方法
/**
* 扩展mvc框架的消息转换器
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器...");
//创建消息转换器对象
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
//设置对象转换器,底层使用Jackson将Java对象转为json
messageConverter.setObjectMapper(new JacksonObjectMapper());
//将上面的消息转换器对象追加到mvc框架的转换器集合中.并优先级设置为最高
converters.add(0,messageConverter);
}
- 由于程序中多处使用了获取前端当前登录Session,在这儿写了一个工具类用于获取,降低耦合度(不过一个人的项目也没那么多需要修改的了,只是想到了就优化一下)
8. 修改员工信息
功能分析
- 获取前端传输过来的ID,查询出数据返回给前端
- 获取前端修改的信息,调用员工账号禁用、启用中的updateById方法修改数据到数据库中
业务逻辑代码
Controller层直接调用mp的getById
@GetMapping("/{id}")
public R getById(@PathVariable long id){
log.info("获取到的前台id:{}",id);
Employee employee = employeeService.getById(id);
return employee != null ? R.success(employee) : R.error("加载数据失败,请再试一次~");
}
9. 公共字段自动填充
功能分析
背景:因业务需求部分字段在插入,或更新时需要重复处理,造成代码高耦合度,后期运维困难,所以需要解决此问题
- 创建类MyMetaObjectHandler实现MP的MetaObjectHandler接口,复写insertFill(插入时设置字段值)和updateFill(更新时设置字段值)
- 在需要处理的实体类字段上增加@TableField(fill = FieldFill.INSERT_UPDATE)
- 注:获取前端Session,自动存储到创建人,更新人字段
但无法通过HttpSession获取Session对象,需要从线程中获取
- 编写BaseContext工具类,基于ThreadLocal封装工具类
- 在登录拦截器中调用BaseContext来设置当前登录用户的id
- 在MyMetaObjectHandler调用BaseContext获取用户的id
- 删除insert,update相关方法中的创建人、创建时间、更新人、更新时间
业务逻辑代码
package com.gigottakeaway.common;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.gigottakeaway.util.BaseContextUtil;
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(""+LocalDateTime.now());
metaObject.setValue("createTime",LocalDateTime.now());
metaObject.setValue("createUser", BaseContextUtil.getThreadLocal());
metaObject.setValue("updateTime",LocalDateTime.now());
metaObject.setValue("updateUser", BaseContextUtil.getThreadLocal());
}
@Override
public void updateFill(MetaObject metaObject) {
log.info("公共字段自动填充[update]...");
metaObject.setValue("updateTime",LocalDateTime.now());
metaObject.setValue("updateUser",BaseContextUtil.getThreadLocal());
}
}
解决问题记录
提供的实体类Employee里面只有创建人、更新人写了@TableField注解、创建时间、更新时间没写,记得加!找了好久的错误
@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;
10. 分类管理新增功能
需求分析
- 接收前端传输数据
- 使用MP封装的save方法存储数据
- 封装对象R返回给前端
业务逻辑代码
Controller
@PostMapping()
public R save(@RequestBody Category category){
log.info("新增分类{}",category);
return categoryService.save(category)? R.success("创建成功"): R.error("新增失败,请再试一次");
}
11. 分类管理分页查询功能
需求分析
- 接收前端传输的当前页码,显示最大数
- 使用MP分页类封装对象,并根据排序字段排序
- 封装对象R返回给前端
业务逻辑代码
public R<Page> selectPage(int page, int pageSize){
Page<Category> pageInfo = new Page(page,pageSize);
LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
//从小到大排序
queryWrapper.orderByAsc(Category::getSort);
pageInfo = categoryMapper.selectPage(pageInfo,queryWrapper);
return R.success(pageInfo);
}
12. 分类管理删除功能
需求分析
1、接收前端传回来的id
2、校验当前分类是否在菜品或套餐中有关联数据,如果抛出运行时异常,并返回前端,如果没有则封装对象R返回给前端
注:因为使用到了菜品和套餐两张数据表,需要导入实体类以及相应的mapper和service,嫌麻烦这次把所有实体类一起导入到了本项目中
业务逻辑代码
自定义异常类
package com.gigottakeaway.exception;
public class CustomException extends RuntimeException{
/**
* 自定义异常
* @param msg
*/
public CustomException(String msg){
super(msg);
}
}
全局异常处理器
/**
* 运行时异常处理方法
* @return
*/
@ExceptionHandler(CustomException.class)
public R<String> exceptionHandler(CustomException ex){
log.error(ex.getMessage());
return R.error(ex.getMessage());
}
}
Service
@Override
public boolean removeById(Serializable id) {
//查询当前分类是否关联了菜品,如果已经关联,抛出一个业务异常
LambdaQueryWrapper<Dish> dishQueryWrapper = new LambdaQueryWrapper();
dishQueryWrapper.eq(Dish::getCategoryId,id);
if(dishService.count(dishQueryWrapper) > 0){
log.info("删除分类与菜品关联");
throw new CustomException("当前分类下关联了菜品,不允许删除!");
}
//查询当前分类是否关联了套餐,如果已经关联,抛出一个业务异常
LambdaQueryWrapper<Setmeal> SetmealQueryWrapper = new LambdaQueryWrapper();
SetmealQueryWrapper.eq(Setmeal::getCategoryId,id);
if(setmealService.count(SetmealQueryWrapper) > 0){
log.info("删除分类与套餐关联");
throw new CustomException("当前分类下关联了套餐,不允许删除!");
}
//如果都没有关联,则正常删除分类
return super.removeById(id);
}
解决问题记录
1、教程中使用的是id,而实体提供的代码提供的是ids,注意接收参数
2、注意区分把参数作为请求体的一部分还是参数作为路径*的一部分
参数作为请求体接收参数
http://localhost/category/1659817228414709761
@DeleteMapping("/{ids}")
public R deleteById(@PathVariable Long ids){
log.info("删除分类数据{}",ids);
return categoryService.removeById(ids)? R.success("已成功删除一条数据~") : R.error("删除失败,请再试一次");
}
参数作为路径
http://localhost/category?ids=1659817228414709761
@DeleteMapping("/{ids}")
public R deleteById(@PathVariable Long ids){
log.info("删除分类数据{}",ids);
return categoryService.removeById(ids)? R.success("已成功删除一条数据~") : R.error("删除失败,请再试一次");
}
3.在service中多写了一个@Autowired,抛异常没有找到selectPage的int参数,找了半天错误,下次写的时候得注意一行一行写
13. 分类管理修改功能
功能分析
1、获取前端修改的信息,调用MP的updateById方法修改数据到数据库中
注:前端数据存储再模型中,不需要从数据库查询再到前端
业务逻辑代码
Controller
@PutMapping()
public R updateById(@RequestBody Category category){
log.info("更新分类数据{}",category);
categoryService.updateById(category);
return R.success("已成功更新一条数据~");
}
14. 文件上传
需求分析
- 获取前端传输的文件信息,使用spring框架的MultipartFile类进行接收(前端传输的也是这个对象),因文件传输可能在多个模块使用,因此单独定义一个Controller进行文件的上传和下载
- 用户传输的文件名可能重复,因此需要根据一个随机且不重复(UUid)的数据进行重命名
- 在项目启动时校验存储上传文件的文件夹upload是否存在,不存在则创建文件夹(注:此方法与教程有差异,考虑到每次上传图片都需要校验文件是否存在,会影响效率,所以在这个地方重写springboot的ApplicationRunner接口用于在项目启动时创建)
- 存储文件到服务器
- 封装对象R返回前端上传文件的路径(前端需要显示该图片)
业务逻辑代码
- CommonServiceImpl
@Override
public String uploadFile(MultipartFile file) throws IOException {
//获取原始文件名
String originalFileName = file.getOriginalFilename();
//获取文件后缀名
String suffix = originalFileName.substring(originalFileName.lastIndexOf("."));
//使用uuid重新生成文件名,防止文件名称重复造成文件覆盖
String fileName = UUID.randomUUID().toString() + suffix;
log.info("上传文件的原始图片文件名为:{} 根据UUid生成的文件名为:{}",originalFileName,fileName);
// 校验文件夹是否存在,放在项目启动时创建目录
// File dir = new File(filePath);
// if(!dir.exists()){
// dir.mkdir();
// }
//获取绝对路径
String absolutePath = GetAbsolutePathUtil.getAbsolutePathUtil(filePath);
//存储文件到服务器
/*File.separator 是一个与操作系统相关的文件分隔符。它是一个字符串常量,用于表示文件路径中的分隔符。
*在 Windows 系统上,File.separator 的值是反斜杠 \,例如 "C:\Users\Username\Documents\file.txt"。
*而在类 Unix 或 Linux 系统上,File.separator 的值是正斜杠 /,例如 "/home/username/documents/file.txt"。
*使用 File.separator 可以确保在不同操作系统上正确构建文件路径,以便代码在不同平台上都能正常运行。
* */
log.info("上传文件存储完整路径为:{}",absolutePath.toString()+File.separator+fileName);
file.transferTo(new File(absolutePath.toString()+File.separator+fileName));
return fileName;
}
- 在项目启动时校验上传文件的文件夹是否存在进行校验
package com.gigottakeaway.runner;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
@Slf4j
@Component
public class Initializer implements ApplicationRunner {
@Value("${takeout.filePath}")
private String filePath;
@Override
public void run(ApplicationArguments args) throws Exception {
log.info("开始初始化系统配置......");
//初始化文件上传保存路径校验
File dir = new File(filePath);
if (!dir.exists()) {
dir.mkdir();
}
Path basePath = Paths.get(""); // 默认为当前工作目录
Path absolutePath = basePath.resolve(filePath).toAbsolutePath();
log.info("初始化文件上传保存路径成功,地址为:{}",absolutePath.toString());
}
}
解决问题记录
- 实现ApplicationRunner接口时需要在实现类加上@Component,让他成为一个bean才能被spring所操控
15. 文件下载
需求分析
- 接收前端传输的文件名
- 根据前端提供的文件名通过输入流的方式从本地读取文件
- 定义响应头
- 将本地文件读取输入流,传入响应输出流返回前端
- 关闭输入流和输出流
注:因为通过流的方式返回给前端不需要返回值
业务逻辑代码
Controller(因本次操作都是对请求和响应操作,不需要放到service中)
/**
* 文件下载
* @param name
*/
@GetMapping("/download")
public void download(String name, HttpServletResponse resp) throws IOException {
log.info("下载文件,接收到的前端数据为{}",name);
String absolutePath = GetAbsolutePathUtil.getAbsolutePathUtil(filePath);
//输入流,通过输入流读取文件内容
FileInputStream fileInputStream = new FileInputStream(new File(absolutePath+File.separator+name));
//输出流,输出文件返回给前端
ServletOutputStream outputStream = resp.getOutputStream();
//设置响应时一个image文件
resp.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();
}
16. 菜品管理分页查询
需求分析
- 接收前端传输的分页信息和查询信息
- 根据排序字段、更新时间字段排序并增加按照菜品名称条件查询
- 因前端分类名称来源于分类数据表(Category),需建立Dto存储分类名称字段传输前端
- 把Page<Dish>的属性值拷贝到Page<DishDto>,并排除records
- 将dish的值存入dishDto并根据categoryId查询name分类名称
- 设置dishDtoPage的records属性(传输的实际数据属性)
- 封装对象R返回给前端
业务逻辑代码
@Autowired
DishMapper dishMapper;
@Autowired
DishFlavorService dishFlavorService;
@Autowired
@Lazy
CategoryService categoryService;
/**
* 分页查询
* @param page
* @param pageSize
* @param name
* @return
*/
@Override
public R<Page> selectPage(int page,int pageSize,String name) {
Page<Dish> pageInfo = new Page(page,pageSize);
LambdaQueryWrapper<Dish> qw = new LambdaQueryWrapper();
qw.orderByDesc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
qw.eq(StringUtils.isNotEmpty(name),Dish::getName,name);
dishMapper.selectPage(pageInfo,qw);
log.info("获取到的total为:{}",pageInfo);
//使用与前端交互的类DishDto ,把pageInfo拷贝到dishDtoPage
Page<DishDto> dishDtoPage = new Page<>(page,pageSize);
//复制Dish对象的属性到DishDto对象,排除records属性(传输的实际数据属性)
BeanUtils.copyProperties(pageInfo,dishDtoPage,"records");
//1、获取Page中的数据列表
List<Dish> dishList = pageInfo.getRecords();
//2、定义一个新的数组,用于存入Dto
List<DishDto> dishDtoList = new ArrayList<>();
//将dish的值存入dishDto并根据categoryId查询name分类名称
for (Dish dish : dishList) {
DishDto dishDto = new DishDto();
// 复制Dish对象的属性到DishDto对象
BeanUtils.copyProperties(dish,dishDto);
//根据菜品分类ID查询name
Category category = categoryService.getById(dish.getCategoryId());
//设置dishDto的CategoryName属性
dishDto.setCategoryName(category.getName());
//将dishDto放到dishDtoList集合中
dishDtoList.add(dishDto);
}
//设置dishDtoPage的records属性(传输的实际数据属性)
dishDtoPage.setRecords(dishDtoList);
return R.success(dishDtoPage);
}
解决问题记录
- 如果再ServiceImplA导入ServiceB,再ServiceImplB导入ServiceA会触发spring的依赖循环,spring不允许依赖循环,因此需要在根据类型自动注入@Autowired下增加@Lazy
17. 菜品管理保存数据
需求分析
- 接收前端传输数据,因数据中有口味(DishFlavor)接收请求体数据封装为DishDto
- 保存菜品分类(Dish)数据(DishDto继承至Dish,可通过MP的service的save方法直接保存)
- 根据DishDto得到id,dishFlavors为集合,设置数据的所有的DishId为Dish类的ID(一对多主子表关系)
- 调用dishFlavorService存储前端传输的dishFlavors集合
- 因同时操作两张表,需要给次方法加上事务保证数据同事插入
- 封装对象R返回给前端
业务逻辑代码
/**
* 保存数据
* @param dishDto
* @return
*/
@Override
@Transactional
public R saveWithFlavor(DishDto dishDto) {
//保存菜品的基本信息到菜品表dish
this.save(dishDto);
Long dishId = dishDto.getId();
//菜品口味
List<DishFlavor> dishFlavors = dishDto.getFlavors();
for (DishFlavor dishFlavor : dishFlavors) {
dishFlavor.setDishId(dishId);
}
dishFlavorService.saveBatch(dishFlavors);
return R.success("新增成功");
}
18. 菜品管理修改数据
需求分析
- 根据前端传输的id查询菜品、口味、分类信息
- 调用service的getbyid方法直接查询菜品信息
- 创建集合dishFlavors
- 查询口味表并将结果dishFlavors放入dishDto类中
- 分类表已实现,可以直接调用不重复说明
- 封装对象R<DishDto>给前端
- 接收前端修改信息使用DishDto接收数据,包含菜品和口味信息
- 调用service的updatebyid方法更改dish表数据
- 删除与本次菜品表相关的口味数据,再将前端传输的口味集合插入到数据库中
10.封装对象R返回给前端
业务逻辑代码
/**
* 通过id查询菜品信息和口味信息
* @param id
* @return
*/
@Override
public DishDto getByIdWithFlavor(Long id) {
//查询菜品信息
Dish dish = this.getById(id);
//对象拷贝
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(dish,dishDto);
//查询口味表
LambdaQueryWrapper<DishFlavor> qw = new LambdaQueryWrapper();
qw.eq(DishFlavor::getDishId,dish.getId());
List<DishFlavor> dishFlavors = dishFlavorService.list(qw);
dishDto.setFlavors(dishFlavors);
return dishDto;
}
/**
* 修改菜品
* @param dishDto
* @return
*/
@Override
@Transactional
public boolean updateWithFlavor(DishDto dishDto) {
this.updateById(dishDto);
//先删除口味数据,再重新添加口味数据
LambdaQueryWrapper<DishFlavor> qw = new LambdaQueryWrapper();
qw.eq(DishFlavor::getDishId,dishDto.getId());
dishFlavorService.remove(qw);
List<DishFlavor> dishDtoFlavors = dishDto.getFlavors();
for (DishFlavor dishDtoFlavor : dishDtoFlavors) {
dishDtoFlavor.setDishId(dishDto.getId());
}
dishFlavorService.saveBatch(dishDto.getFlavors());
return true;
}
19. 批量起售、批量停售
需求分析
- 获取前端数据
- 因返回的数据为id集合,新增DishMapper.xml使用xml的方式编写sql(如果使用MP可以使用循环的方式来update),也可以使用Lamda表达式,为了复习下Mapper.xml顺便就用这种方式写了
- 封装对象R返回前端
注:批量启用和启用传输的请求是同一个,不需要重复写一个单独启用功能
业务逻辑代码
service
/**
* 批量启售,批量停售
* @param status
* @param ids
* @return
*/
@Override
public int updateStatus(int status, List<Long> ids) {
int updateCount = dishMapper.updateStatusByIds(status,ids);
log.info("成功更新{}条数据",updateCount);
return updateCount;
}
DishMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gigottakeaway.mapper.DishMapper">
<update id="updateStatusByIds">
update dish
set status = #{status}
where id in
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
;
</update>
</mapper>
问题解决记录
- 前端传输了status和ids集合,传输的格式为/status/1?ids=123456789,并且数据在url上,不在请求体中,因此controller接收需要使用@PathVariable int status ,@RequestParam List ids
20. 批量删除
需求分析
- 获取前端数据
- 根据ids删除菜品表,使用lamda表达式写出需要删除的口味表的id,删除两张表需要增加事务
- 封装对象R返回前端
注:批量删除和删除传输的请求是同一个,不需要重复写一个单独删除功能
业务逻辑代码
service
/**
* 批量删除
*
* @param ids
* @return
*/
@Transactional
public int deleteByIds(List<Long> ids) {
int deleteCount = dishMapper.deleteByIds(ids);
log.info("成功删除{}条数据", deleteCount);
LambdaQueryWrapper<DishFlavor> qw = new LambdaQueryWrapper();
qw.in(DishFlavor::getDishId,ids);
dishFlavorService.remove(qw);
return deleteCount;
}
21. 套餐管理分页查询
需求分析
- 接收前端传输的分页信息和查询信息
- 根据排序字段、更新时间字段排序并增加按照菜品名称条件查询
- 因前端分类名称来源于分类数据表(Category),需建立Dto存储分类名称字段传输前端
- 把Page<Dish>的属性值拷贝到Page<DishDto>,并排除records
- 将dish的值存入dishDto并根据categoryId查询name分类名称
- 设置dishDtoPage的records属性(传输的实际数据属性)
- 封装对象R返回给前端
注:其他这儿和菜品管理一致的分页需求
业务逻辑代码
service
/**
* 分页查询
* @param page
* @param pageSize
* @param name
* @return
*/
public Page<SetmealDto> selectPage(int page, int pageSize, String name) {
//创建Page接收page数据
Page<Setmeal> pageInfo = new Page<>(page, pageSize);
Page<SetmealDto> pageinfoDto = new Page<>(page, page);
//条件语句建立,根据时间排序,模糊查询名称
LambdaQueryWrapper<Setmeal> qw = new LambdaQueryWrapper();
qw.orderByDesc(Setmeal::getUpdateTime);
qw.like(StringUtils.isNotEmpty(name), Setmeal::getName, name);
//调用分页查询
setmealMapper.selectPage(pageInfo, qw);
//拷贝对象到pageinfoDto,排除records
BeanUtils.copyProperties(pageInfo, pageinfoDto, "records");
//增加分类名称到pageinfoDto
List<Setmeal> setmeals = pageInfo.getRecords();
List<SetmealDto> setmealDtos = new ArrayList<>();
for (Setmeal setmeal : setmeals) {
SetmealDto setmealDto = new SetmealDto();
BeanUtils.copyProperties(setmeal, setmealDto);
String categoryName = categoryService.getById(setmeal.getCategoryId()).getName();
setmealDto.setCategoryName(categoryName);
setmealDtos.add(setmealDto);
}
pageinfoDto.setRecords(setmealDtos);
return pageinfoDto;
}
22. 套餐管理保存数据
需求分析
- 接收前端传输数据,因数据中有套餐菜品关联表接收请求体数据封装为SetmealDto
- 保存套餐(Setmeal)数据
- 通过setmealDto中的套餐id给套餐菜品关系表中的套餐id赋值
- 调用dishFlavorService存储前端传输的dishFlavors集合
- 因同时操作两张表,需要给次方法加上事务保证数据同事插入
- 封装对象R返回给前端
业务逻辑代码
/**
* 通过id查询套餐和套餐菜品关系表
* @param setmealDto
* @return true 修改成功 false 修改失败
*/
@Override
public boolean updateWithSetmealDish(SetmealDto setmealDto) {
//修改套餐
this.updateById(setmealDto);
//修改套餐菜品关系表
//删除与套餐关联的套餐菜品关系表
LambdaQueryWrapper<SetmealDish> qw = new LambdaQueryWrapper();
qw.eq(SetmealDish::getSetmealId,setmealDto.getId());
setmealDishService.remove(qw);
//根据前端传输的setmealDto重新插入套餐菜品关系表
List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
//setmealDto中的套餐id给套餐菜品关系表中的套餐id赋值
for (SetmealDish setmealDish : setmealDishes) {
setmealDish.setSetmealId(setmealDto.getId());
}
setmealDishService.saveBatch(setmealDishes);
return true;
}
23. 套餐管理修改数据
需求分析
- 根据前端传输的id查询数据返回给前端
- 修改套餐表中的数据
- 删除套餐菜品关系表
- 保存用户修改的套餐菜品关系表
- 封装对象R返回给前端
业务逻辑代码
/**
* 通过id查询套餐和套餐菜品关系表
* @param setmealDto
* @return true 修改成功 false 修改失败
*/
@Override
public boolean updateWithSetmealDish(SetmealDto setmealDto) {
//修改套餐
this.updateById(setmealDto);
//修改套餐菜品关系表
//删除与套餐关联的套餐菜品关系表
LambdaQueryWrapper<SetmealDish> qw = new LambdaQueryWrapper();
qw.eq(SetmealDish::getSetmealId,setmealDto.getId());
setmealDishService.remove(qw);
//根据前端传输的setmealDto重新插入套餐菜品关系表
List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
//setmealDto中的套餐id给套餐菜品关系表中的套餐id赋值
for (SetmealDish setmealDish : setmealDishes) {
setmealDish.setSetmealId(setmealDto.getId());
}
setmealDishService.saveBatch(setmealDishes);
return true;
}
24. 套餐管理批量停售,启售
需求分析
1、接收前端数据
2、根据前端传输的状态和id修改状态
3、封装数据R返回给前端
业务逻辑代码
/**
* 批量修改状态
* @param status
* @param ids
* @return
*/
@Override
public boolean updateStatus(int status, List<Long> ids) {
LambdaUpdateWrapper<Setmeal> uw = new LambdaUpdateWrapper<>();
uw.in(Setmeal::getId,ids).set(Setmeal::getStatus,status);
this.update(uw);
return true;
}
解决问题记录
- 前端传输过来的数据一直反复调用Controller,找了半天结果发现是把etmealService.updateStatus(status,ids);写成了updateStatus(status,ids);,没调service中的方法,一直在调controller里面的方法所以一直反复循环。
25. 套餐管理批量删除
需求分析
- 接收前端数据
- 根据前端传输的id集合删除id
- 封装对象R返回前端
业务逻辑代码
@Override
@Transactional
public boolean deleteWithSetmealDishById(List<Long> ids) {
//删除套餐表
this.removeByIds(ids);
//删除套餐菜品关系表
LambdaQueryWrapper<SetmealDish> qw = new LambdaQueryWrapper();
qw.in(SetmealDish::getSetmealId,ids);
setmealDishService.remove(qw);
return true;
}
26. 手机端短信验证码登录
需求分析
- 接收前端传输的手机号/邮箱信息
- 生成随机6位验证码
- 校验前端传输的数据是邮箱还是手机号
- 实现第三方SMSapi(腾讯云、qq邮箱)
- 调用第三方SMSapi成功后保存Session电话号码/邮箱作为键,验证码作为值
- 用户发送验证码成功后接收前端传输用户输入的手机号和验证码
- 获取Session中的手机号/邮箱并与前端传输的数据做对比,如果有数据表示用户登录成功
- 根据手机号/邮箱查询数据库是否存在用户
- 用户不存在自动注册一个用户
- 将用户id加入session中,表示用户已登录
业务逻辑代码
- 腾讯云短信Api工具类(本次项目使用,因有腾讯云的云服务器注册比较方便)
注:用户名密码需要自己的用户名和密码,注意安全性,不可暴露在互联网上否则会造成严重的财产损失!!!
package com.gigottakeaway.util;
import com.tencentcloudapi.common.Credential;
import com.tencentcloudapi.common.exception.TencentCloudSDKException;
import com.tencentcloudapi.common.profile.ClientProfile;
import com.tencentcloudapi.common.profile.HttpProfile;
import com.tencentcloudapi.sms.v20210111.SmsClient;
import com.tencentcloudapi.sms.v20210111.models.SendSmsRequest;
import com.tencentcloudapi.sms.v20210111.models.SendSmsResponse;
import com.tencentcloudapi.sms.v20210111.models.SendStatus;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class SMSTencentCloudUtils {
/**
* 发送短信
* @param signName 签名 签名信息可前往 [国内短信](https://console.cloud.tencent.com/smsv2/csms-sign)
* @param templateCode 模板 模板 ID 可前往 [国内短信](https://console.cloud.tencent.com/smsv2/csms-template)
* @param phoneNumbers 手机号 示例如:+8613711112222, 其中前面有一个+号 ,86为国家码,13711112222为手机号,最多不要超过200个手机号
* @param templateParamSet 参数 模板参数: 模板参数的个数需要与 TemplateId 对应模板的变量个数保持一致,若无模板参数,则设置为空
*/
public static SendStatus[] sendMessage(String signName, String templateCode,String[] phoneNumbers,String[] templateParamSet ) throws TencentCloudSDKException {
/* 必要步骤:
* 实例化一个认证对象,入参需要传入腾讯云账户密钥对secretId,secretKey。
* 这里采用的是从环境变量读取的方式,需要在环境变量中先设置这两个值。
* 你也可以直接在代码中写死密钥对,但是小心不要将代码复制、上传或者分享给他人,
* 以免泄露密钥对危及你的财产安全。
* SecretId、SecretKey 查询: https://console.cloud.tencent.com/cam/capi */
Credential cred = new Credential("用户名", "密码");
// 实例化一个http选项,可选,没有特殊需求可以跳过
HttpProfile httpProfile = new HttpProfile();
// 设置代理(无需要直接忽略)
// httpProfile.setProxyHost("真实代理ip");
// httpProfile.setProxyPort(真实代理端口);
/* SDK默认使用POST方法。
* 如果你一定要使用GET方法,可以在这里设置。GET方法无法处理一些较大的请求 */
httpProfile.setReqMethod("POST");
/* SDK有默认的超时时间,非必要请不要进行调整
* 如有需要请在代码中查阅以获取最新的默认值 */
httpProfile.setConnTimeout(60);
/* 指定接入地域域名,默认就近地域接入域名为 sms.tencentcloudapi.com ,也支持指定地域域名访问,例如广州地域的域名为 sms.ap-guangzhou.tencentcloudapi.com */
httpProfile.setEndpoint("sms.tencentcloudapi.com");
/* 非必要步骤:
* 实例化一个客户端配置对象,可以指定超时时间等配置 */
ClientProfile clientProfile = new ClientProfile();
/* SDK默认用TC3-HMAC-SHA256进行签名
* 非必要请不要修改这个字段 */
// clientProfile.setSignMethod("HmacSHA256");
clientProfile.setHttpProfile(httpProfile);
/* 实例化要请求产品(以sms为例)的client对象
* 第二个参数是地域信息,可以直接填写字符串ap-guangzhou,支持的地域列表参考 https://cloud.tencent.com/document/api/382/52071#.E5.9C.B0.E5.9F.9F.E5.88.97.E8.A1.A8 */
SmsClient client = new SmsClient(cred, "ap-guangzhou",clientProfile);
/* 实例化一个请求对象,根据调用的接口和实际情况,可以进一步设置请求参数
* 你可以直接查询SDK源码确定接口有哪些属性可以设置
* 属性可能是基本类型,也可能引用了另一个数据结构
* 推荐使用IDE进行开发,可以方便的跳转查阅各个接口和数据结构的文档说明 */
SendSmsRequest req = new SendSmsRequest();
/* 短信应用ID: 短信SdkAppId在 [短信控制台] 添加应用后生成的实际SdkAppId,示例如1400006666 */
// 应用 ID 可前往 [短信控制台](https://console.cloud.tencent.com/smsv2/app-manage) 查看
String sdkAppId = "短信应用ID";
req.setSmsSdkAppId(sdkAppId);
/* 短信签名内容: 使用 UTF-8 编码,必须填写已审核通过的签名 */
// 签名信息可前往 [国内短信](https://console.cloud.tencent.com/smsv2/csms-sign) 或 [国际/港澳台短信](https://console.cloud.tencent.com/smsv2/isms-sign) 的签名管理查看
req.setSignName(signName);
/* 模板 ID: 必须填写已审核通过的模板 ID */
// 模板 ID 可前往 [国内短信](https://console.cloud.tencent.com/smsv2/csms-template) 或 [国际/港澳台短信](https://console.cloud.tencent.com/smsv2/isms-template) 的正文模板管理查看
String templateId = "模板ID";
req.setTemplateId(templateId);
/* 模板参数: 模板参数的个数需要与 TemplateId 对应模板的变量个数保持一致,若无模板参数,则设置为空 */
String[] temp = templateParamSet;
req.setTemplateParamSet(temp);
/* 下发手机号码,采用 E.164 标准,+[国家或地区码][手机号]
* 示例如:+8613711112222, 其中前面有一个+号 ,86为国家码,13711112222为手机号,最多不要超过200个手机号 */
for (String phoneNumber : phoneNumbers) {
phoneNumber = "+86"+phoneNumber;
}
req.setPhoneNumberSet(phoneNumbers);
/* 通过 client 对象调用 SendSms 方法发起请求。注意请求方法名与请求对象是对应的
* 返回的 res 是一个 SendSmsResponse 类的实例,与请求对象对应 */
SendSmsResponse res = client.SendSms(req);
SendStatus[] sendStatusSet = res.getSendStatusSet();
log.info("腾讯云短信服务返回结果:");
log.info("PhoneNumber: " + sendStatusSet[0].getPhoneNumber());
log.info("Fee: " + sendStatusSet[0].getFee());
log.info("Code: " + sendStatusSet[0].getCode());
log.info("Message: " + sendStatusSet[0].getMessage());
return sendStatusSet;
}
}
- 阿里巴巴短信接口工具类(本次项目未使用)
package com.gigottakeaway.util;
//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 SMSAlibabaUtils {
/**
* 阿里巴巴发送短信
* @param signName 签名
* @param templateCode 模板
* @param phoneNumbers 手机号
* @param param 参数
*/
public static void sendMessage(String signName, String templateCode,String phoneNumbers,String param){
// DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", "", "");
// IAcsClient client = new DefaultAcsClient(profile);
//
// SendSmsRequest request = new SendSmsRequest();
// request.setSysRegionId("cn-hangzhou");
// request.setPhoneNumbers(phoneNumbers);
// request.setSignName(signName);
// request.setTemplateCode(templateCode);
// request.setTemplateParam("{\"code\":\""+param+"\"}");
// try {
// SendSmsResponse response = client.getAcsResponse(request);
// System.out.println("短信发送成功");
// }catch (ClientException e) {
// e.printStackTrace();
// }
}
}
- 手机号/邮箱账号校验工具类
package com.gigottakeaway.util;
import java.util.regex.Pattern;
/**
* 验证码,手机号校验工具类
*/
public class ValidatorUtils {
private static final String PHONE_NUMBER_PATTERN = "^(13[0-9]{9})|(15[0-9]{9})|(17[0-9]{9})|(18[0-9]{9})|(19[0-9]{9})$";
private static final String EMAIL_PATTERN = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$";
public static boolean validatePhoneNumber(String phoneNumber) {
return Pattern.matches(PHONE_NUMBER_PATTERN, phoneNumber);
}
public static boolean validateEmail(String email) {
return Pattern.matches(EMAIL_PATTERN, email);
}
}
- 随机数生成工具类
package com.gigottakeaway.util;
import java.util.Random;
/**
* 随机生成验证码工具类
*/
public class ValidateCodeUtils {
/**
* 随机生成验证码
* @param length 长度为4位或者6位
* @return
*/
public static Integer generateValidateCode(int length){
Integer code =null;
if(length == 4){
code = new Random().nextInt(9999);//生成随机数,最大为9999
if(code < 1000){
code = code + 1000;//保证随机数为4位数字
}
}else if(length == 6){
code = new Random().nextInt(999999);//生成随机数,最大为999999
if(code < 100000){
code = code + 100000;//保证随机数为6位数字
}
}else{
throw new RuntimeException("只能生成4位或6位数字验证码");
}
return code;
}
/**
* 随机生成指定长度字符串验证码
* @param length 长度
* @return
*/
public static String generateValidateCode4String(int length){
Random rdm = new Random();
String hash1 = Integer.toHexString(rdm.nextInt());
String capstr = hash1.substring(0, length);
return capstr;
}
}
- 短信验证码Service
@Override
public boolean sendMsg(User user, HttpSession session) {
//生成验证码
int ValidateCode = ValidateCodeUtils.generateValidateCode(6);
String LoginNum = user.getPhone();
log.info("生成的验证码为:{}",ValidateCode);
//校验前端传输的是电话还是邮箱
if(ValidatorUtils.validatePhoneNumber(LoginNum)){
//发送手机验证码
log.info("{}为手机号,准备发送短信",LoginNum);
//签名名称
String siginName = "签名名称";
//模板id
String templateCode = "模板id";
//电话号码 因为登录只有一个电话号只传输一个就好了
String[] phones = {LoginNum};
//模板参数 这儿其实就是验证码 模板里面定义的参数,模板中定义了几个参数这儿就要填对应的个数且顺序不能乱
String[] params = {ValidateCode+""};
try {
SendStatus[] sendStatuses = SMSTencentCloudUtils.sendMessage(siginName,templateCode, phones, params);
//打印腾讯云返回结果,这儿因为只传了一个手机号直接取数组第一个元素就可以了
String SMSCode =sendStatuses[0].getCode().toString();
//判断短信发送是否成功,成功getCode为OK,如果失败则抛出业务异常返回前端
if("Ok".equals(SMSCode)){
//发送成功,设置session存储手机号和对应的验证码
session.setAttribute("ValidateCode"+LoginNum,ValidateCode + "");
return true;
}else {
throw new CustomException("发送验证码失败,请再试一次");
}
} catch (TencentCloudSDKException e) {
throw new CustomException("短信平台问题,无法发送短信");
}
}else if(ValidatorUtils.validateEmail(user.getPhone())){
//TODO 发送邮箱
log.info("{}为邮箱,准备发送邮件",user.getPhone());
} else {
//如果不为手机号或邮箱则格式错误抛出业务异常返回前端
throw new CustomException("请输入手机号或邮箱");
}
return true;
}
用户登录Service
@Override
public boolean loginByCode(Map map, HttpSession session) {
//获取集合中的手机号和验证码
String LoginNum = map.get("phone").toString();
String validateCode = map.get("code").toString();
//获取存储验证码的session
String validateCodeSession = (String) session.getAttribute("ValidateCode" + LoginNum);
log.info("获取到的session数据:{}",validateCodeSession);
if(validateCodeSession != null && validateCodeSession.equals(validateCode)){
//登录成功 获取user
LambdaQueryWrapper<User> qw = new LambdaQueryWrapper();
qw.eq(User::getPhone,LoginNum);
User user = this.getOne(qw);
//如果查询到的用户为空说明用户是新用户,需要自动注册一个用户
if(user == null){
user = new User();
user.setPhone(LoginNum);
user.setStatus(1);
this.save(user);
}
session.setAttribute("employee", user.getId());
}else {
throw new CustomException("您输入的验证码有误,请检查!");
}
return true;
}
解决问题记录
- 在以下代码中,先获取user,如果user=null的话记得在if判断里面需要再new一次对象,因为此时对象为null
User user = this.getOne(qw);
//如果查询到的用户为空说明用户是新用户,需要自动注册一个用户
if(user == null){
user = new User();
user.setPhone(LoginNum);
user.setStatus(1);
this.save(user);
}
27. 地址管理查询列表
需求分析
- 获取前端数据
- 获取Session中的userId
- 通过userId获取当前地址列表
- 封装对象R返回前端
业务逻辑代码
Controller
/**
* 获取列表
* @return
*/
@GetMapping("list")
public R<List<AddressBook>> getList(HttpServletRequest req){
log.info("地址管理获取列表");
Long userId = (Long) req.getSession().getAttribute("user");
List<AddressBook> addressBooks = addressBookService.selectListByUser(userId);
return R.success(addressBooks);
}
service
@Override
public List<AddressBook> selectListByUser(Long userId) {
//查询数据库中的userId,查询当前用户下的地址
LambdaQueryWrapper<AddressBook> qw = new LambdaQueryWrapper<>();
qw.eq(AddressBook::getUserId,userId);
List<AddressBook> addressBooks = addressBookMapper.selectList(qw);
return addressBooks;
}
28. 地址管理修改数据
1、获取前端发送的地址Id
2、根据地址Id查询数据库
3、封装对象R返回地址对象
4、获取前端发送的修改数据
5、根据id修改数据
6、返回数据给前端
业务逻辑代码
Controller
/**
* 通过id查询
* @param id
* @return
*/
@GetMapping("/{id}")
public R<AddressBook> selectById(@PathVariable Long id){
log.info("地址管理通过id查询的前端参数为:{}",id);
AddressBook addressBook = addressBookService.getById(id);
return R.success(addressBook);
}
/**
* 通过id修改
* @param addressBook
* @return
*/
@PutMapping
public R updateById(@RequestBody AddressBook addressBook){
log.info("地址管理通过id修改接收到的前端参数为:{}",addressBook);
addressBookService.updateById(addressBook);
return R.success("修改成功");
}
29. 地址管理修改默认地址
需求分析
- 获取前端传输数据
- 获取当前登录用户存储再Session的userId
- 将登录用户的默认地址改为非默认状态
- 将登录用户勾选的默认地址改为默认状态
- 封装对象R返回给前端
业务逻辑代码
Controller
/**
* 修改默认地址
* @param addressBook
* @return
*/
@PutMapping("default")
public R updateDefault(@RequestBody AddressBook addressBook,HttpServletRequest req){
log.info("地址管理修改默认地址接收到的前端参数为:{}",addressBook);
Long userId = (Long) req.getSession().getAttribute("user");
addressBook.setUserId(userId);
addressBookService.updateDefault(addressBook);
return R.success("修改成功");
}
Service
@Override
@Transactional
public boolean updateDefault(AddressBook addressBook) {
//将本用户下的默认地址全部修改为0
//创建一个AddressBook对象用于更新
AddressBook updateAddressBookDefaultInit = new AddressBook();
Long userId = addressBook.getUserId();
updateAddressBookDefaultInit.setIsDefault(0);
updateAddressBookDefaultInit.setUserId(userId);
LambdaUpdateWrapper<AddressBook> uw = new LambdaUpdateWrapper<>();
uw.eq(AddressBook::getUserId,addressBook.getUserId());
uw.eq(AddressBook::getIsDefault,1);
addressBookMapper.update(updateAddressBookDefaultInit,uw);
//设置前端传输的地址为默认地址
addressBook.setIsDefault(1);
//因为拿到前端的地址id可以,清空userId提升update效率
addressBook.setUserId(null);
//更新前端传输的默认地址
addressBookMapper.updateById(addressBook);
return true;
}
30. 地址管理删除
需求分析
- 获取前端传输的id
- 根据id删除地址
- 封装对象R返回给前端
业务需求代码
@DeleteMapping()
public R deleteById(@RequestParam Long ids){
log.info("删除地址接收到的前端参数为:{}",ids);
addressBookService.removeById(ids);
return R.success("删除成功");
}
31.手机端主页显示
需求分析
- 获取分类管理(前面已做过,前端直接调用方法就可以了)
- 改造菜品(增加口味数据),套餐列表获取
业务逻辑代码
菜品管理service
/**
* 查询list
* @param dishDto
* @return
*/
@Override
public List<DishDto> list(DishDto dishDto) {
LambdaQueryWrapper<Dish> qw = new LambdaQueryWrapper();
Long categoryId = dishDto.getCategoryId();
if(StringUtils.isNotEmpty(categoryId.toString())){
qw.eq(Dish::getCategoryId,categoryId);
qw.eq(Dish::getStatus,dishDto.getStatus());
qw.orderByDesc(Dish::getUpdateTime);
}
List<Dish> dishes = this.list(qw);
List<DishDto> dishDtos = new ArrayList<>();
for (Dish dish : dishes) {
DishDto dishDtoTemp = new DishDto();
BeanUtils.copyProperties(dish,dishDtoTemp);
//查询菜品口味关联表集合
LambdaQueryWrapper<DishFlavor> dishFlavorQW = new LambdaQueryWrapper<>();
//查询菜品表的id
dishFlavorQW.eq(DishFlavor::getDishId,dish.getId());
//查询根据菜品表ID查询菜品口味关联表表集合
List<DishFlavor> dishFlavorList = dishFlavorService.list(dishFlavorQW);
dishDtoTemp.setFlavors(dishFlavorList);
//将拷贝的对象加入dishDtos集合
dishDtos.add(dishDtoTemp);
}
return dishDtos;
}
@Override
public List<Setmeal> selectList(Setmeal setmeal) {
LambdaQueryWrapper<Setmeal> qw = new LambdaQueryWrapper();
qw.eq(setmeal.getCategoryId()!=null,Setmeal::getCategoryId,setmeal.getCategoryId());
qw.eq(setmeal.getStatus()!=null,Setmeal::getStatus,setmeal.getStatus());
List<Setmeal> setmeals = setmealMapper.selectList(qw);
return setmeals;
}
32. 查询购物车列表
需求分析
- 获取前端数据,并获取session中的user添加到对象中
- 根据当前登录user查询
- 封装对象R返回给前端
业务逻辑代码
@Override
public List<ShoppingCart> selectList(ShoppingCart shoppingCart) {
//通过userId查询列表返回给前端
LambdaQueryWrapper<ShoppingCart> qw = new LambdaQueryWrapper<>();
qw.eq(ShoppingCart::getUserId,shoppingCart.getUserId());
return this.list(qw);
}
33. 添加购物车
需求分析
- 获取前端数据,并获取session中的user添加到查询对象中
- 判断接收到的数据是菜品或者套餐
- 判断套餐或菜品是否已存在,存在在原有基础上+1,不存在添加到购物车,数量默认为1
- 封装对象R返回给前端
业务逻辑代码
service
@Override
public ShoppingCart addShoppingCar(ShoppingCart shoppingCart) {
Long dishId = shoppingCart.getDishId();
//判断当前是套餐或者菜品,并查询数据
ShoppingCart shoppingCartTemp = new ShoppingCart();
LambdaQueryWrapper<ShoppingCart> qw = new LambdaQueryWrapper<>();
if(dishId != null){
//当前为菜品
log.info("当前数据为菜品dishId:{}",dishId);
qw.eq(ShoppingCart::getDishId,shoppingCart.getDishId());
}else {
//当前为套餐
log.info("当前数据为套餐dishId:{}",dishId);
qw.eq(ShoppingCart::getSetmealId,shoppingCart.getSetmealId());
}
qw.eq(ShoppingCart::getUserId,shoppingCart.getUserId());
shoppingCartTemp = this.getOne(qw);
//判断套餐或菜品是否已存在
if(shoppingCartTemp != null){
//如果已存在,再原来的基础上+1
log.info("当前数据已存在 shoppingCartTemp:{}",shoppingCartTemp);
LambdaUpdateWrapper<ShoppingCart> uw = new LambdaUpdateWrapper();
shoppingCartTemp.setNumber(shoppingCartTemp.getNumber()+1);
uw.set(ShoppingCart::getNumber,shoppingCartTemp.getNumber());
uw.eq(ShoppingCart::getId,shoppingCartTemp.getId());
this.update(uw);
}else {
//如果不存在,则添加到购物车,数量默认为1
log.info("当前数据不存在 shoppingCartTemp:{}",shoppingCartTemp);
shoppingCart.setNumber(1);
this.save(shoppingCart);
shoppingCartTemp = shoppingCart;
}
return shoppingCartTemp;
}
34. 减少购物车
需求分析
- 接收前端数据,并获取session中的user添加到对象中
- 判断当前是套餐或菜品,并查询数据
- 判断套餐或菜品是否小于1,大于1在原基础上数量-1,小于或者等于1删除数据
- 封装对象R返回前端
业务逻辑代码
service
@Override
public ShoppingCart subShoppingCar(ShoppingCart shoppingCart) {
Long dishId = shoppingCart.getDishId();
//判断当前是套餐或者菜品,并查询数据
ShoppingCart shoppingCartTemp = new ShoppingCart();
LambdaQueryWrapper<ShoppingCart> qw = new LambdaQueryWrapper<>();
if(dishId != null){
//当前为菜品
log.info("当前数据为菜品dishId:{}",dishId);
qw.eq(ShoppingCart::getDishId,shoppingCart.getDishId());
}else {
//当前为套餐
log.info("当前数据为套餐dishId:{}",dishId);
qw.eq(ShoppingCart::getSetmealId,shoppingCart.getSetmealId());
}
qw.eq(ShoppingCart::getUserId,shoppingCart.getUserId());
shoppingCartTemp = this.getOne(qw);
//判断套餐或菜品是否小于1
if(shoppingCartTemp.getNumber() > 1){
//大于1,再原来的基础上-1
log.info("大于1 shoppingCartTemp:{}",shoppingCartTemp);
LambdaUpdateWrapper<ShoppingCart> uw = new LambdaUpdateWrapper();
shoppingCartTemp.setNumber(shoppingCartTemp.getNumber()-1);
uw.set(ShoppingCart::getNumber,shoppingCartTemp.getNumber());
uw.eq(ShoppingCart::getId,shoppingCartTemp.getId());
this.update(uw);
}else {
//不大于1,删除数据
log.info("不大于1 shoppingCartTemp:{}",shoppingCartTemp);
shoppingCartTemp.setNumber(0);
this.removeById(shoppingCartTemp.getId());
}
return shoppingCartTemp;
}
35. 清空购物车
需求分析
- 接收前端数据,并获取session中的user添加到对象中
- 根据当前登录用户删除购物车
- 封装对象R返回前端
36. 获取默认订单地址
需求分析
- 接收前端数据,并获取session中的user添加到对象中
- 根据当前登录用户和默认地址获取地址
- 封装对象R返回前端
业务逻辑代码
@Override
public AddressBook getDefault(AddressBook addressBook) {
Long userId = addressBook.getUserId();
LambdaQueryWrapper<AddressBook> qw = new LambdaQueryWrapper<>();
qw.eq(AddressBook::getUserId,userId);
qw.eq(AddressBook::getIsDefault,1);
AddressBook addressBookReturn = this.getOne(qw);
return addressBookReturn;
}
37. 修改订单地址
需求分析
- 地址簿页面修改前端代码将用户选择的地址发送请求到后端,下订单页面获取后端用户选择的地址
- 将地址簿前端返回上一页改为跳转到下订单页面
- 接收前端请求并存储到session中
- 获取session中存储的地址再发送到前端
注:这个地方个人觉得黑马的设计不太合理,选择地址簿之后就修改默认地址,虽然数据库改了,但是返回到订单页面还是之前的地址,因此在这儿重写了前端
业务逻辑代码
address.js添加两个方法
- 用于设置当前登录用户选择地址
- 用于查询当前登录用户选择地址
//查询当前登录选择地址
function getNowAddressApi() {
return $axios({
'url': '/addressBook/getNowAddress',
'method': 'get'
})
}
//设置当前登录选择地址
function setNowAddressApi(id) {
return $axios({
url: `/addressBook/setNowAddress?id=${id}`,
method: 'post',
})
}
address.html
- 添加setNowAddress方法,用于发送和接收设置地址请求
- 重写itemClick方法,在跳转前调用setNowAddress方法,改为跳转’/front/page/add-order.html’
注:这个地方改为跳转是因为返回上一页不能调用create钩子函数
async setNowAddress(id){
if(id){
const res = await setNowAddressApi(id)
alert("获取到的后端数据为:"+res.data.id)
if(res.code == 1){
}else {
this.$message.error(res.msg)
}
}
},
itemClick(item){
const url = document.referrer
//表示是从订单页面跳转过来的
if(url.includes('order')){
// 修改默认地址
// this.setDefaultAddress(item)
// addressFindOneApi(item.id);
this.setNowAddress(item.id)
window.location.href = '/front/page/add-order.html'
}
}
add-order.html
- 添加getNowAddress方法,用于发送和接收获取地址请求
- 重写created中的initData方法,校验是否是从address.html地址簿跳转过来的,如果是则调用getNowAddress方法,如果不是则调用默认地址
//获取当前选择地址
async getNowAddress() {
const res = await getNowAddressApi()
if (res.code == 1) {
this.address = res.data
this.getFinishTime()
} else {
window.requestAnimationFrame(() => {
window.location.href = '/front/page/address-edit.html'
})
}
},
Controller
/**
* 获取默认地址
* @param addressBook
* @param session
* @return
*/
@GetMapping("/default")
public R<AddressBook> getDefalut(AddressBook addressBook , HttpSession session){
log.info("获取默认地址接收到的前端参数为:{}",addressBook);
Long userId = (Long) session.getAttribute("user");
addressBook.setUserId(userId);
AddressBook addressBookDefault = addressBookService.getDefault(addressBook);
if(addressBookDefault == null){
throw new CustomException("当前地址为空,请添加地址信息!");
}
session.setAttribute("nowAddress"+userId,addressBookDefault.getId());
session.setMaxInactiveInterval(365 *24 * 60 * 60 * 100); //30分*60秒
return R.success(addressBookDefault);
}
/**
* 设置用户现在选择的地址
*/
@PostMapping("/setNowAddress")
public R setNowAddress(@RequestParam("id") Long id, HttpServletRequest req) {
log.info("地址管理设置当前选择地址的前端参数为:id:{}", id);
Long userId = (Long) req.getSession().getAttribute("user");
HttpSession session = req.getSession();
session.setAttribute("nowAddress"+userId,id);
session.setMaxInactiveInterval(365 *24 * 60 * 60 * 100); //30分*60秒
return R.success("设置用户当前地址成功");
}
/**
* 获取用户现在选择的地址
*/
@GetMapping("/getNowAddress")
public R<AddressBook> getNowAddress(HttpServletRequest req){
log.info("地址管理获取当前选择地址");
Long userId = (Long) req.getSession().getAttribute("user");
HttpSession session = req.getSession();
Long dddressId = (Long) session.getAttribute("nowAddress" + userId);
log.info("开始通过地址id查询当前选择地址dddressId:{}",dddressId);
AddressBook addressBook = addressBookService.getById(dddressId);
return R.success(addressBook);
}
38. 手机端主页点击套餐图片显示套餐详情
需求分析
- 接收前端数据
- 查询套餐菜品匹配表数据
- 将数据拷贝到菜品Dto中,这个地方需要拷贝两个,一个是套餐菜品匹配表(份数),一个是菜品表(菜品详情信息)
- 封装对象R返回给前端
业务逻辑代码
- service
@Override
public List<DishDto> selectDishBySetmeal(Long setmealId) {
//查询套餐菜品匹配表数据
LambdaQueryWrapper<SetmealDish> qw = new LambdaQueryWrapper<>();
qw.eq(SetmealDish::getSetmealId,setmealId);
List<SetmealDish> setmealDishList = setmealDishService.list(qw);
List<DishDto> dishDtoList = setmealDishList.stream().map((item) -> {
DishDto dishDto = new DishDto();
//拷贝套餐菜品匹配表到dishDto,主要是为了前端显示份数字段
BeanUtils.copyProperties(item,dishDto);
//查询菜品id
Dish dish = dishService.getById(item.getDishId());
//拷贝对象到dishDto
BeanUtils.copyProperties(dish,dishDto);
return dishDto;
}).collect(Collectors.toList());
return dishDtoList;
}
39. 用户下单
需求分析
- 接收前端数据,并获取session中的user和当前选择地址添加到对象中
- 修改“获取默认地址”接口,增加把默认地址放入Session中(这个地方主要是获取地址要统一)并获取地址簿信息
- 根据当前登录用户获取购物车信息
- 获取用户信息,这一步主要是用于完善订单表数据库字段
- 生成订单号,注这个地方要先生成订单id,因为有明细表需要一起插入
- 插入数据到订单明细表中,并在插入时算出订单总额
- 插入数据到订单表
- 校验是否成功,如果成功清空购物车数据
- 封装对象R返回前端
注:这个地方需要加事务,因涉及两张表同时插入
业务逻辑代码
service
@Override
@Transactional
public boolean submit(Orders orders) {
//获取地址簿信息
LambdaQueryWrapper<AddressBook> addressBookLambdaQW = new LambdaQueryWrapper();
addressBookLambdaQW.eq(AddressBook::getId,orders.getAddressBookId());
AddressBook addressBook = addressBookService.getOne(addressBookLambdaQW);
if(addressBook == null){
throw new CustomException("当前地址错误,请添加后再试");
}
//获取购物车信息
LambdaQueryWrapper<ShoppingCart> shoppingCartQW = new LambdaQueryWrapper();
//根据当前登录用户获取购物车信息
shoppingCartQW.eq(ShoppingCart::getUserId,orders.getUserId());
List<ShoppingCart> shoppingCartList = shoppingCartService.list(shoppingCartQW);
if(shoppingCartList == null){
throw new CustomException("当前购物车信息有误,请检查后再试");
}
//获取用户信息,用于完善数据库字段
User user = userService.getById(orders.getUserId());
//生成订单号
Long orderNum = IdWorker.getId() ;
//订单合计金额 DoubleAdder类提供线程安全
DoubleAdder amountSum = new DoubleAdder();
//插入数据到订单明细表
List<OrderDetail> orderDetailList = shoppingCartList.stream().map((shoppingCartItem) -> {
OrderDetail orderDetail = new OrderDetail();
orderDetail.setName(shoppingCartItem.getName());
orderDetail.setImage(shoppingCartItem.getImage());
orderDetail.setOrderId(orderNum);
orderDetail.setDishId(shoppingCartItem.getDishId());
orderDetail.setSetmealId(shoppingCartItem.getSetmealId());
orderDetail.setDishFlavor(shoppingCartItem.getDishFlavor());
orderDetail.setNumber(shoppingCartItem.getNumber());
orderDetail.setAmount(shoppingCartItem.getAmount());
amountSum.add(shoppingCartItem.getAmount().doubleValue() * shoppingCartItem.getNumber());
return orderDetail;
}).collect(Collectors.toList());
boolean orderDetailSaveOKFlag = orderDetailService.saveBatch(orderDetailList);
//插入数据到订单表
orders.setId(orderNum);
orders.setNumber(orderNum+"");
orders.setStatus(2);
orders.setOrderTime(LocalDateTime.now());
orders.setCheckoutTime(LocalDateTime.now());
orders.setAmount(new BigDecimal(amountSum.doubleValue()));
orders.setPhone(user.getPhone());
orders.setAddress(addressBook.getDetail());
orders.setUserName(user.getName());
orders.setConsignee(addressBook.getConsignee());
boolean ordersSaveOkFlag = this.save(orders);
//校验是否保存成功
if(ordersSaveOkFlag&&orderDetailSaveOKFlag){
//保存成功清空购物车
shoppingCartService.remove(shoppingCartQW);
return true;
}else {
throw new CustomException("保存失败,请稍后再试");
}
}
40. 订单分页查询(用户)
需求分析
- 接收前端数据,并获取session中的user和当前选择地址添加到对象中
- 添加查询条件(这个地方写好是因为网页端的订单管理也会访问所以写了一个通用的service)
- 分页查询订单列表补全ordersDto信息
- 将订单明细集合添加到ordersDto集合中
- 将ordersDto集合放入pageInfoOrdersDto集合
- 封装对象R<List<Page>>返回前端
注:这个地方个人觉得其实使用mapper.xml更好,这样循环太多会查很多次数据库,为了练习mp才这样写的
业务逻辑代码
@Override
public Page<OrdersDto> selectPage(int page, int pagesize, OrdersDto ordersDto) {
Page<Orders> pageInfoOrders = new Page(page,pagesize);
Page<OrdersDto> pageInfoOrdersDto = new Page(page,pagesize);
//分页查询列表Orders
LambdaQueryWrapper<Orders> ordersDtoLambdaQW = new LambdaQueryWrapper<>();
ordersDtoLambdaQW.eq(ordersDto.getUserId() != null,Orders::getUserId,ordersDto.getUserId());
ordersDtoLambdaQW.like(StringUtils.isNotEmpty(ordersDto.getNumber()),Orders::getNumber,ordersDto.getNumber());
ordersDtoLambdaQW.ge(ordersDto.getBeginTime()!=null,Orders::getOrderTime,ordersDto.getBeginTime());
ordersDtoLambdaQW.le(ordersDto.getEndTime()!=null,Orders::getOrderTime,ordersDto.getEndTime());
ordersDtoLambdaQW.orderByDesc(Orders::getOrderTime);
ordersMapper.selectPage(pageInfoOrders, ordersDtoLambdaQW);
//pageInfoOrders赋值pageInfoOrdersDto
BeanUtils.copyProperties(pageInfoOrders,pageInfoOrdersDto);
List<Orders> ordersList = pageInfoOrders.getRecords();
//将ordersList赋值到ordersDtoList
List<OrdersDto> ordersDtoList = ordersList.stream().map((item) -> {
OrdersDto ordersDtoTemp = new OrdersDto();
BeanUtils.copyProperties(item,ordersDtoTemp);
//获取地址簿信息
LambdaQueryWrapper<AddressBook> addressBookQW = new LambdaQueryWrapper();
addressBookQW.eq(AddressBook::getId,item.getAddressBookId());
AddressBook addressBook = addressBookService.getOne(addressBookQW);
if(addressBook == null){
throw new CustomException("当前地址错误,请添加后再试");
}
//获取购物车信息
LambdaQueryWrapper<ShoppingCart> shoppingCartQW = new LambdaQueryWrapper();
//根据当前登录用户获取购物车信息
shoppingCartQW.eq(ShoppingCart::getUserId,item.getUserId());
List<ShoppingCart> shoppingCartList = shoppingCartService.list(shoppingCartQW);
if(shoppingCartList == null){
throw new CustomException("当前购物车信息有误,请检查后再试");
}
//获取用户信息
User user = userService.getById(item.getUserId());
//设置订单明细集合
LambdaQueryWrapper<OrderDetail> qw = new LambdaQueryWrapper();
qw.eq(OrderDetail::getOrderId,item.getId());
List<OrderDetail> orderDetailList = orderDetailService.list(qw);
ordersDtoTemp.setOrderDetails(orderDetailList);
//设置用户名
ordersDtoTemp.setUserName(user.getName());
//设置电话
ordersDtoTemp.setPhone(user.getPhone());
//设置地址
ordersDtoTemp.setAddress(addressBook.getDetail());
//设置收货人
ordersDtoTemp.setConsignee(addressBook.getConsignee());
return ordersDtoTemp;
}).collect(Collectors.toList());
pageInfoOrdersDto.setRecords(ordersDtoList);
return pageInfoOrdersDto;
}
41. 订单管理分页查询(后台管理端)
需求分析
- 接收前端数据
- 创建对象OrdersDto,并封装前端传输的查询数据(这个地方时间可以用String接收,但是要注意转换的时候需要先判断是否为空,如果不加判断用户未输入时间会报错)
- 调用订单管理分页查询service
- 封装对象R返回给前端
业务逻辑代码
controller
/**
* 获取订单分页信息
*
* @param page
* @param pageSize
* @return
*/
@GetMapping("/page")
public R<Page<OrdersDto>> selectPage(int page, int pageSize, String number, String beginTime, String endTime, HttpSession session) {
log.info("订单分页查询接收到的前端数据为:page:{},pageSize{},,OrdersDto{}", page, pageSize);
OrdersDto ordersDto = new OrdersDto();
ordersDto.setNumber(number);
if (StringUtils.isNotEmpty(beginTime) && StringUtils.isNotEmpty(endTime)) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
ordersDto.setBeginTime(LocalDateTime.parse(beginTime, formatter));
ordersDto.setEndTime(LocalDateTime.parse(endTime, formatter));
}
Page<OrdersDto> ordersDtoPage = ordersService.selectPage(page, pageSize, ordersDto);
return R.success(ordersDtoPage);
}
42. 修改订单状态(后台管理端)
需求分析
- 接收前端数据
- 根据id修改状态
- 返回数据给前端
业务逻辑代码
service
/**
* 修改订单状态
* @param orders
* @return
*/
@PutMapping()
public R updateOrderStatus(@RequestBody Orders orders){
log.info("更新订单状态接收到的前端数据为Orders:{}",orders);
boolean flag = ordersService.updateById(orders);
return flag?R.success("更新订单状态,修改成功!"):R.error("修改失败,请再试一次~");
}
43. 订单历史查询-再来一单
需求分析
- 接收前端数据,并获取session中的user和当前选择地址添加到对象中
- 根据订单id查询订单集合
- 将订单集合copy到购物车集合
- 保存购物车集合到数据库
- 封装对象R返回前端
业务逻辑代码
@Override
public boolean again(Orders orders) {
LambdaQueryWrapper<OrderDetail> qw = new LambdaQueryWrapper<>();
qw.eq(OrderDetail::getOrderId,orders.getId());
List<OrderDetail> orderDetailList = orderDetailService.list(qw);
//获取用户id,
Long userId = orders.getUserId();
List<ShoppingCart> shoppingCarts = orderDetailList.stream().map((item) ->{
ShoppingCart shoppingCart = new ShoppingCart();
//Copy对应属性值
BeanUtils.copyProperties(item,shoppingCart);
//设置一下userId
shoppingCart.setUserId(userId);
//设置一下创建时间为当前时间
shoppingCart.setCreateTime(LocalDateTime.now());
return shoppingCart;
}).collect(Collectors.toList());
//加入购物车
shoppingCartService.saveBatch(shoppingCarts);
return true;
}
45. 手机端退出登录
需求分析
- 获取前端数据
- 从Session中找到当前用户并删除
- 封装对象R返回前端
业务逻辑代码
@PostMapping("/loginout")
public R loginout(HttpSession session){
log.info("退出登录功能");
session.removeAttribute("user");
return R.success("退出登录成功");
}
项目部署到腾讯云服务器
代码修改
- 修改pom.xml文件如下(初始创建的pom文件打成jar包有问题,可以直接复制这一段)
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.gigottakeaway</groupId>
<artifactId>GigotTakeaway</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>GigotTakeaway</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.6.13</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.23</version>
</dependency>
<!--腾讯云SMS-->
<dependency>
<groupId>com.tencentcloudapi</groupId>
<artifactId>tencentcloud-sdk-java</artifactId>
<!-- go to https://search.maven.org/search?q=tencentcloud-sdk-java and get the latest version. -->
<!-- 请到https://search.maven.org/search?q=tencentcloud-sdk-java查询所有版本,最新版本如下 -->
<version>3.1.764</version>
</dependency>
<!--Eamil-->
<!-- https://mvnrepository.com/artifact/javax.activation/activation -->
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/javax.mail/mail -->
<dependency>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
<version>1.4.7</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-email -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-email</artifactId>
<version>1.4</version>
</dependency>
<!--随机用户名生成工具类-->
<!-- Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.1-jre</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<mainClass>com.gigottakeaway.GigotTakeawayApplication</mainClass>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
- 增加系统入口业务层,用于区分手机访问和pc访问,且可以直接通过地址:端口的形式访问(该项目端口为80所以直接通过路径就可以访问了)不需要静态页面路径,这个地方需要把"/"这个路径添加到拦截器放行里面,不然会被拦截器拦截
EntryController
package com.gigottakeaway.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.view.RedirectView;
import javax.servlet.http.HttpServletRequest;
/**
* 程序入口
*/
@RestController
@RequestMapping("/")
public class EntryController {
@GetMapping()
public RedirectView entry(HttpServletRequest request) {
String userAgent = request.getHeader("User-Agent");
if (userAgent != null && userAgent.contains("Mobile")) {
// 用户使用的是手机设备
return new RedirectView("/front/index.html");
} else {
// 用户使用的是电脑设备
return new RedirectView("/backend/index.html");
}
}
}
LoginCheckFilter
package com.gigottakeaway.filter;
import com.alibaba.fastjson.JSON;
import com.gigottakeaway.common.R;
import com.gigottakeaway.util.BaseContextUtils;
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;
/**
* 检查用户是否完成登录,登录后放行资源
*/
@Slf4j
@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
public class LoginCheckFilter implements Filter {
//路径匹配器,支持通配符
public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
log.info("当前拦截到的请求:{}",request.getRequestURI());
//定义排除登录相关功能地址和静态资源相关地址
String[] urls = new String[]{
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**",
"/user/sendMsg", //移动端发送短信
"/user/login",//移动端登录
"/" // 程序入口
};
//判断本次请求是否在上面需要排除的地址中,如果在放行
if(check(urls,request.getRequestURI())){
log.info("用户访问登录或相关静态资源地址放行资源");
filterChain.doFilter(request,response);
return;
}
//判断用户是否已登录,已登录放行
if(request.getSession().getAttribute("employee") != null){
log.info("{}已登录,放行资源",request.getSession().getAttribute("employee"));
Long idSession = (Long) request.getSession().getAttribute("employee");
BaseContextUtils.setThreadLocal(idSession);
filterChain.doFilter(request,response);
return;
}
//判断手机端用户是否登录
if(request.getSession().getAttribute("user") != null){
log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("user"));
Long userId = (Long)request.getSession().getAttribute("user");
BaseContextUtils.setThreadLocal(userId);
filterChain.doFilter(request,response);
return;
}
//前端需求代码中有拦截器,因此这这儿使用输出流的方式传给前端一个R封装对象
log.info("用户未登录返回前台,前端进行地址跳转");
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
return;
}
/**
* 路径匹配,用于排除登录相关资源的校验方法
*/
public boolean check(String[] urls,String requestUri){
for (String url : urls) {
//PATH_MATCHER.match(url,requestUri)方法,用于判断两个地址是否匹配,并且支持通配符的格式,如果匹配商返回true,否则返回false
boolean flag = PATH_MATCHER.match(url,requestUri);
if(flag){
return true;
}
}
return false;
}
}
- 增加短信验证码发送频率校验10分钟一次(因为挂了短信,防止恶意攻击把短信包给用完了)
package com.gigottakeaway.controller;
import com.gigottakeaway.common.R;
import com.gigottakeaway.exception.CustomException;
import com.gigottakeaway.pojo.User;
import com.gigottakeaway.service.UserService;
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;
import javax.servlet.http.HttpSession;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Map;
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
@Autowired
UserService userService;
@PostMapping("/sendMsg")
public R sendMsg(@RequestBody User user, HttpSession session){
log.info("短信验证码发送功能接收到的前端参数为:user:{},session:{}",user,session);
// 获取当前时间
LocalDateTime currentTime = LocalDateTime.now();
// 检查验证码发送时间
LocalDateTime lastSentTime = (LocalDateTime) session.getAttribute("lastSentTime");
if (lastSentTime != null) {
Duration duration = Duration.between(lastSentTime, currentTime);
long minutes = duration.toMinutes();
if (minutes < 10) {
throw new CustomException("验证码发送时间不足10分钟,请稍后再试");
}
}
userService.sendMsg(user, session);
// 更新验证码发送时间
session.setAttribute("lastSentTime", currentTime);
return R.success("成功发送短信");
}
@PostMapping("/login")
public R loginByCode(@RequestBody Map map,HttpSession session){
log.info("登录功能接收到的前端参数为:map:{},session:{}",map.toString(),session);
// 获取当前时间
LocalDateTime currentTime = LocalDateTime.now();
// 检查验证码发送时间
LocalDateTime sentTime = (LocalDateTime) session.getAttribute("lastSentTime");
if (sentTime != null) {
Duration duration = Duration.between(sentTime, currentTime);
long minutes = duration.toMinutes();
if (minutes >= 10) {
// 验证码已过期
throw new CustomException("验证码已过期,请重新获取");
}
} else {
// 验证码不存在或未发送
throw new CustomException("验证码不存在或未发送");
}
boolean flag = userService.loginByCode(map,session);
return flag?R.success("登录成功"):R.error("登录失败,请再试一次");
}
@PostMapping("/loginout")
public R loginout(HttpSession session){
log.info("退出登录功能");
session.removeAttribute("user");
return R.success("退出登录成功");
}
}
配置文件
- application.yml区分开发环境和正式环境
spring:
profiles:
active: dev
---
#开发环境
spring:
config:
activate:
on-profile: dev
application:
name: gigot_takeaway
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: 开发数据库地址
username: 用户名
password: 密码
server:
port: 80
servlet:
context-path: /
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
employee:
#默认密码
defalutPassword: "123456"
takeout:
#上传文件存储地址
filePath: "src/main/resources/upload/"
#后门 用于绕过短信登录
developerUsername: 用户名
developerPassword: 密码
startProfile: "开发"
---
#正式环境
spring:
config:
activate:
on-profile: prod
application:
name: gigot_takeaway
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: 正式数据库地址
username: 用户名
password: 密码
server:
port: 80
servlet:
context-path: /
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
employee:
#默认密码
defalutPassword: "123456"
takeout:
#上传文件存储地址
filePath: "upload/"
#后门 用于绕过短信登录
developerUsername: 用户名
developerPassword: 密码
startProfile: "正式"
Bug修改
- AddressBookController增加校验获取地址为空向前端抛出业务异常,需要让用户增加地址(文档代码已更新,此问题解决)
- 手机端登录输入正确的验证码确提示验证码错误,经排查发现是存储“检查验证码发送时间”的session名称错了,改一下名称就好(文档代码已更新,此问题解决)
- 打包后程序没有主入口(pom.xml文件里面有表现,先时注释了,但是注释没生效,后来删除掉就好了)
- 初次启动服务时因为Linux没有一些字体包,会自动下载,第一次访问时间较长(问题待以后前端解决)
部署到腾讯云服务器(使用宝塔运维工具)
-
打包程序为jar包
-
可以使用navicat的数据库同步到服务器,我这儿服务器上的版本比较低,就直接重新建表了
-
上传文件到服务器
-
配置网站
注:如果需要配置域名的话在【域名管理】中配置,并在【外网映射】中开启外网映射
-
测试
后台管理端
客户端
短信/邮箱验证码