文章目录
- 零、MyBatisPlus
- 一、管理端登录
- 1.0 统一的返回结果Result类
- 1.1 admin/login
- 1.2 admin/logout
- 1.3 Filter
- 1.4 自定义消息转换器
- 二、员工管理
- 2.1 新增员工-字段填充
- 2.2 全局异常捕获
- 2.3 员工信息分页查询
- 三、分类管理
- 3.1 分类的删除
- 四、菜品管理
- 4.1 文件的上传与下载
- 1 上传
- 2 下载
- 4.2 新增菜品
- 4.3 修改菜品
- 4.4 菜品信息分页查询
- 五、套餐管理
- 5.1 添加套餐
- 5.2 批量删除套餐
- 5.3 套餐信息分页查询
- 六、用户相关
- 6.1 发送验证码
- 6.2 登录
- 七、购物车
- 7.1 添加菜品和套餐
- 文件配置
目标很明确,快速掌握最最基础的SpringBoot + MyBatis-Plus怎么用,两天赶着把项目做了一大半,但过程里缺乏一些思考和总结,现在来复盘一下。仅列出觉得有价值的部分。
还是很适合作为上手项目,业务逻辑确实比较简单,主要是要掌握一整套流程,以及涉及到多个表的连接查询操作、一个表的分页查询应该如何处理,以及文件的上传下载、手机短信发送验证码知识。
但这样的项目,如果不主动思考,能得到的东西就很少了,因为它开发的流程已经给了一个答案,虽然未必是标准答案,但是直接照着抄、不考虑应该怎么实现,可能除了查表更熟练以外能收获的技能不多。不过查表更熟练也算小提升吧。
以及觉得如果有个ER图 / 接口说明的话,会清晰很多,不用这样对着前端分析传过来什么,应该传回去什么。
MyBatis Plus确实方便了很多,这个项目从头到尾没写过<if> <foreach> <set> <where>
,方便得让人不安,牛的。
自己一个人git还是缺少锻炼,体会不到那种pull
下来发现有冲突,需要merge的绝望。
下一步速成redis和微服务,主要还是学学各种中间件怎么使。然后找个能拿得出手的项目。
零、MyBatisPlus
极大简化CRUD代码。
- 基本上是傻瓜式操作,因为几乎不用记对应的SQL查询要怎么写,戳一个
.
就能得到一波hint和提示补全。 - 提供分页插件。
- 提供全局拦截规则,设置
@TableField
及对应的MetaObjectHandler
就可以对字段进行填充。
一、管理端登录
1.0 统一的返回结果Result类
还是有必要的,之前写前端的时候很需要这个code和msg让我知道这个接口我是调成功了还是失败了,调失败了的话问题在哪。
@Data
public class Result<T> {
/**
* code - 编码:1成功,0和其它数字为失败
* msg - 错误信息
* data - 数据
* map - 动态数据
*/
private Integer code;
private String msg;
private T data;
private Map map = new HashMap();
public static <T> Result<T> success(T object) {
Result<T> result = new Result<T>();
result.data = object;
result.code = 1;
return result;
}
public static <T> Result<T> error(String msg) {
Result result = new Result();
result.msg = msg;
result.code = 0;
return result;
}
public Result<T> add(String key, Object value) {
this.map.put(key, value);
return this;
}
}
1.1 admin/login
说明:这一部分是好久好久好久以前写的,改了改前端和接口,但逻辑是一样的。
客户端请求(TODO: 前端裸传密码还是有一点怪怪……有时间了解一下现实世界的实现):
POST
/admin/login
参数:
{
"name": "扣扣",
"password": "koukou123456"
}
管理员实体:
@Data
public class Admin {
private Long adminId;
private String password;
private String phoneNumber;
private String name;
}
逻辑:
- 将参数password进行MD5加密
import org.springframework.util.DigestUtils;
password = DigestUtils.md5DigestAsHex(password.getBytes());
-
判断数据库中是否存在该对象,与数据库中取到的密码是否一致
-
登录成功时,将管理员id存入当前session,作为本次会话的一个属性。
request.getSession().setAttribute("admin", adm.getAdminId());
AdminController
代码:
/**
* 密码md5加密 + 根据name查询数据库 + 比对密码
* @param request 该参数为了将该admin对象的id存入当前session中
* @param admin 封装好的Admin Bean参数
* @return
*/
@PostMapping("/login")
public Result<Admin> login(HttpServletRequest request, @RequestBody Admin admin) {
// 1 将页面提交的密码进行md5加密处理
String password = admin.getPassword();
password = DigestUtils.md5DigestAsHex(password.getBytes());
// 2 根据页面提交的用户名username查询数据库
LambdaQueryWrapper<Admin> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Admin::getName, admin.getName());
Admin adm = adminService.getOne(queryWrapper);
// 3、无结果返回登陆失败
if (adm == null) {
return Result.error("用户名错误,登录失败");
}
// 4、比对密码
if (!adm.getPassword().equals(password)) {
return Result.error("密码错误,登录失败");
}
// 5、登录成功,将管理员id存入Session并返回登录成功结果
request.getSession().setAttribute("admin", adm.getAdminId());
return Result.success(adm);
}
1.2 admin/logout
把当前管理员的id移出session
:
@PostMapping("/logout")
public Result<String> login(HttpServletRequest request) {
request.getSession().removeAttribute("admin");
return Result.success("退出成功");
}
1.3 Filter
Servelet中的Filter接口。需要加入@WebFilter
注解声明拦截路径,并在启动类加入@ServletComponentScan
注解,使得这个Filter可以被Scan到。
一些页面 / 接口需要在访问前判断当前是否为登录状态,所以设置这个Filter。
核心逻辑为判断当前访问的Url以及从Session中取出id。
/**
* 检查是否登录
*/
@WebFilter(urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter implements Filter {
/**
* 路径匹配器,用于检查该路径是否需要拦截
*/
private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void destroy() {
Filter.super.destroy();
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
}
/**
* 判断requestUrl是否在urls中
*/
public boolean canPass(String[] urls, String requestURI) {
for (String url: urls) {
if (PATH_MATCHER.match(url, requestURI)) {
return true;
}
}
return false;
}
}
核心为doFilter
方法,逻辑如下:
-
定义可放行请求路径集合,判断
request
的Url是否在集合中,如果在集合中,可以直接放行; -
尝试从session中得到login时存入的属性(可能是管理员login,也可能是用户login)
req.getSession().getAttribute("admin");
-
如果返回值不为空,说明已经登录,可以放行
-
否则需要
response
拒绝请求:Result<String> error = Result.error("对不起,您尚未登录,请先进行登录操作!"); resp.getWriter().write(JSONObject.toJSONString(error)); return;
完整代码如下:
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
HttpServletResponse resp = (HttpServletResponse) servletResponse;
// 可放行集合
String[] canPassUrls = {
"/admin/login",
"/admin/logout",
// 静态资源路径就不处理了
"/backend/**",
"/front/**",
// 一些其他请求,发送短信、移动端登录
"/common/**",
"/user/sendMsg",
"/user/login"
};
// 1、得到URI
String requestURI = req.getRequestURI();
log.info("拦截到请求: {}", requestURI);
// 2、得到登录状态
Object adminLoginId = req.getSession().getAttribute("admin");
Object userLoginId = req.getSession().getAttribute("user");
// 3、如果未登录且是不可访问页面,拒绝请求
if (!canPass(canPassUrls, requestURI) && adminLoginId == null && userLoginId == null) {
Result<String> error = Result.error("对不起,您尚未登录,请先进行登录操作!");
resp.getWriter().write(JSONObject.toJSONString(error));
return;
}
if (adminLoginId != null) {
BaseContext.setCurrentId((Long)adminLoginId);
}
if (userLoginId != null) {
BaseContext.setCurrentId((Long)userLoginId);
}
filterChain.doFilter(servletRequest, servletResponse);
}
1.4 自定义消息转换器
这部分只是意会了,让我自己写可能还是不会。
long转为js会精度丢失,那么我们就对数据进行转型,响应json时进行处理,将long转为字符串。
并且转换时间格式。
还是有点AOP的。
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;
/**
* 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
* 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
* 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
*/
public class JacksonObjectMapper extends ObjectMapper {
public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
public JacksonObjectMapper() {
super();
//收到未知属性时不报异常
this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
//反序列化时,属性不存在的兼容处理
this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
SimpleModule simpleModule = new SimpleModule()
.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
.addSerializer(BigInteger.class, ToStringSerializer.instance)
.addSerializer(Long.class, ToStringSerializer.instance)
.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
//注册功能模块 例如,可以添加自定义序列化器和反序列化器
this.registerModule(simpleModule);
}
}
WebMVCConfig
中需要进行相依ing的设置。
import com.beautysalon.common.JacksonObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import java.util.List;
@Slf4j
@Configuration
public class WebMVCConfig extends WebMvcConfigurationSupport {
/**
* 设置静态资源映射
* @param registry
*/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
}
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器");
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
messageConverter.setObjectMapper(new JacksonObjectMapper());
converters.add(0, messageConverter);
}
}
二、员工管理
2.1 新增员工-字段填充
可以统一处理的变量可以使用注解@TableField
,然后再定义一个Handler
实现填充方法。
@Slf4j
@Data
public class Employee {
private Long id;
private String name;
private String username;
private String password;
private String phone;
private String sex;
private String idNumberReal;
private Integer status;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT)
private Long createByAdmin;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateByAdmin;
}
实现MetaObjectHandler
接口和insertFill
、upDateFill
方法。
可以使用hasSetter
判断是否具有某个属性。
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
log.info("公共字段自动填充[insert]");
metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime", LocalDateTime.now());
if (metaObject.hasSetter("createByAdmin")) {
metaObject.setValue("createByAdmin", BaseContext.getCurrentId());
metaObject.setValue("updateByAdmin", BaseContext.getCurrentId());
}
if (metaObject.hasSetter("createUser")) {
metaObject.setValue("createUser", BaseContext.getCurrentId());
metaObject.setValue("updateUser", BaseContext.getCurrentId());
}
}
@Override
public void updateFill(MetaObject metaObject) {
log.info("公共字段自动填充[update]");
metaObject.setValue("updateTime", LocalDateTime.now());
if (metaObject.hasSetter("updateByAdmin")) {
metaObject.setValue("updateByAdmin", BaseContext.getCurrentId());
}
if (metaObject.hasSetter("updateUser")) {
metaObject.setValue("updateUser", BaseContext.getCurrentId());
}
}
}
BaseContext
如下,在login时设置了BaseContext相关属性,需要填充时再get,因为是静态方法,所以不需要注入:
/**
* 基于ThreadLocal封装工具类
*/
@Component
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();
}
}
2.2 全局异常捕获
使用@ControllerAdvice
和@ExceptionHandler
注解,@ExceptionHandler
指明了捕获什么样的异常。
/**
* 全局异常处理
*/
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {
/**
* 异常处理方法
* @return
*/
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public Result<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
log.error(ex.getMessage());
if(ex.getMessage().contains("Duplicate entry")){
String[] split = ex.getMessage().split(" ");
String msg = split[2] + "已存在";
return Result.error(msg);
}
return Result.error("未知错误");
}
/**
* 异常处理方法
* @return
*/
@ExceptionHandler(CustomException.class)
public Result<String> exceptionHandler(CustomException ex){
log.error(ex.getMessage());
return Result.error(ex.getMessage());
}
}
2.3 员工信息分页查询
需要配置MyBatis提供的分页插件拦截器:
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 {
/**
* 分页插件
* @return
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
}
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
使用MyBatis-Plus
的Page
进行分页:
@GetMapping("/page")
public Result<Page<Employee>> page(@RequestParam Integer page,
@RequestParam Integer pageSize,
@RequestParam(required = false) String name) {
log.info("员工分页信息查询:{}, {}", page, pageSize);
// 配置分页构造器
Page<Employee> pageInfo = new Page<>(page, pageSize);
// 条件构造器
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
// 添加过滤条件,如果name不为空,加入name=#{name}条件
queryWrapper.like(!StringUtils.isEmpty(name), Employee::getName, name);
// 添加排序条件
queryWrapper.orderByDesc(Employee::getUpdateTime);
// 执行查询
employeeService.page(pageInfo, queryWrapper);
return Result.success(pageInfo);
}
三、分类管理
3.1 分类的删除
删除前需要先去dish
表和setmeal
表查看有无菜品。操作涉及到3个表:
- dish表是否有元素categoryId为当前分类
- setmeal表是否有元素categoryId为当前分类
- category表删除该分类
四、菜品管理
4.1 文件的上传与下载
上传:保存到本地指定位置
下载:作为Response吐给浏览器显示
1 上传
在属性的yml文件中定义相关路径位置:
koukou:
path: E:\\leetcode\\project_pre\\BeautySalon\\src\\main\\resources\\front\\upload\\
使用${}
指定图片保存路径
@Value("${koukou.path}")
private String basePath;
@PostMapping("/upload")
public Result<String> upload(MultipartFile file) {
// 提取文件相关信息
String filename = file.getOriginalFilename();
int index = filename.lastIndexOf('.');
String ext = filename.substring(index);
// UUID赋予新名称
String newName = UUID.randomUUID().toString();
String path = basePath + newName + ext;
log.info(path);
// 保存文件
try {
file.transferTo(new File(path));
}
catch (IOException e) {
e.printStackTrace();
}
return Result.success(newName + ext);
}
2 下载
/**
* 让本地的图片在浏览器上显示,写入Response的输出流
* @param name
* @param response
*/
@GetMapping("/download")
public void download(String name, HttpServletResponse response) {
try {
// 输入流
FileInputStream fileInputStream = new FileInputStream(new File(basePath + name));
// 输出流
ServletOutputStream outputStream = response.getOutputStream();
// 设置response的content类型
response.setContentType("image/jpeg");
int len = 0;
byte[] bytes = new byte[1024];
while ((len = fileInputStream.read(bytes)) != -1) {
outputStream.write(bytes, 0, len);
outputStream.flush();
}
outputStream.close();
fileInputStream.close();
}
catch (Exception e) {
e.printStackTrace();
}
}
4.2 新增菜品
@Transactional(rollbackFor = Exception.class)
开启事务,并在启动类上加上@EnableTransactionManagement
.
设计到三个表:
- 菜品的分类:因为前端在新增菜品时,需要选择菜品分类,因此需要返回菜品的所有可能分类取值。
- dish表,表示菜品
- dish_flavor表,表示菜品的口味,由于是一对多关系,该表存储了dish的主码id
1、查询所有可能的菜品分类,使用一个category来接收参数,解释是这样以后需求增加时(比如按其它属性search)不必重构这个方法:
@GetMapping("/list")
public Result<List<Category>> list(Category category) {
log.info("根据条件查询分类数据");
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 Result.success(list);
}
2、这个add请求由于携带了额外的信息,用一个DishDTO
接住:
@PostMapping
public Result<String> save(@RequestBody DishDto dishDto) {
dishService.saveWithFlavor(dishDto);
return Result.success("成功保存菜品");
}
DishDto
继承了Dish
类,包含Dish的所有属性,但增加了flavors
的扩展。
categoryName
我觉得是想说明怎么实现两个表的连接,把categoryId
转为categoryName
。
/**
* DTO:Data Transfer Object,用于传输数据, 对dish的扩展
*/
@Data
public class DishDto extends Dish {
private List<DishFlavor> flavors = new ArrayList<>();
private String categoryName;
private Integer copies;
}
- 先将
dishDto
存入Dish表 - 然后设置每个
Flavor
的dishId,并存进Flavor表。
4.3 修改菜品
修改菜品的逻辑比较类似,但首先需要先把这个菜品的信息查询出来,放进DishDto
里传给前端,前端显示这个菜品。
使用到了BeanUtils.copyProperties
进行两个对象间的复制。
然后前端进行修改,然后再传回后端,后端进行修改。类似地,先update这个dish,然后再update这个菜品对应的口味。
4.4 菜品信息分页查询
类似地,需要查找菜品及其对应的口味,并将categoryId
转为name,同样用到了BeanUtils
进行Page之间的复制。
- 查找满足条件的分页数据
Page<Dish>
,赋值给Page<DishDto>
- 查找所有
dish
的口味和种类,赋值给DishDto
,加入列表。
@GetMapping("/page")
public Result<Page> page(int page, int pageSize, String name) {
// 先把分页数据查出来
Page<Dish> pageInfo = new Page<>(page, pageSize);
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(name != null, Dish::getName, name);
queryWrapper.orderByDesc(Dish::getUpdateTime);
dishService.page(pageInfo, queryWrapper);
// 和另一个flavor表综合
Page<DishDto> dishDtoPage = new Page<>();
// 把查询出来的数据拷贝到新对象
BeanUtils.copyProperties(pageInfo, dishDtoPage, "records");
// 处理records
List<Dish> dishes = pageInfo.getRecords();
List<DishDto> dishDtos = new ArrayList<>();
for (Dish dish: dishes) {
DishDto dishDto = new DishDto();
// 把dish拷贝到新对象
BeanUtils.copyProperties(dish, dishDto);
Long categoryId = dish.getCategoryId();
String categoryName = categoryService.getById(categoryId).getName();
dishDto.setCategoryName(categoryName);
dishDtos.add(dishDto);
}
// 赋值
dishDtoPage.setRecords(dishDtos);
return Result.success(dishDtoPage);
}
五、套餐管理
5.1 添加套餐
和新增菜品的逻辑很类似,涉及到setmeal和setmealdish两张表,setmeal保存套餐信息,setmealdish记录菜品与套餐间的关系。
- 先保存套餐信息
- 然后设置 套餐菜品关系 的套餐id,存入表
5.2 批量删除套餐
需要先批量删除setmeal套餐表,然后用.in
判断菜品套餐关系表,删除SetmealDish表中含该套餐id的项。
5.3 套餐信息分页查询
与菜品信息分页查询类似:
- Setmeal和SetmealDto之间的BeanUtils.copyProperties
- 以及两个Page之间的BeanUtils.copyProperties
@GetMapping("/page")
public Result<Page> page(int page, int pageSize, String name) {
Page<Setmeal> pageInfo = new Page<>(page, pageSize);
// 需要返回的数据类型
Page<SetmealDto> dtoPage = new Page<>();
// 先把这一页的信息查出来
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.like(name != null, Setmeal::getName, name);
setmealService.page(pageInfo, queryWrapper);
List<Setmeal> setmeals = pageInfo.getRecords();
List<SetmealDto> setmealDtos = new ArrayList<>();
BeanUtils.copyProperties(pageInfo, dtoPage, "records");
// 将id转换为name
for (Setmeal setmeal: setmeals) {
String categoryName = categoryService.getById(setmeal.getCategoryId()).getName();
SetmealDto setmealDto = new SetmealDto();
BeanUtils.copyProperties(setmeal, setmealDto);
setmealDto.setCategoryName(categoryName);
setmealDtos.add(setmealDto);
}
dtoPage.setRecords(setmealDtos);
return Result.success(dtoPage);
}
六、用户相关
6.1 发送验证码
生成4位验证码:
public class ValidateCodeUtils {
public static String generateValidateCode4String(int length){
Random rdm = new Random();
String hash1 = Integer.toHexString(rdm.nextInt());
String capstr = hash1.substring(0, length);
return capstr;
}
}
发送短信,即调用API发请求的过程:
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 {
private static final String SIGN_NAME = "小扣外卖";
private static final String TEMPLATE_CODE = "SM1";
/**
* 发送短信
* @param phoneNumbers 手机号
* @param param 参数
*/
public static void sendMessage(String phoneNumbers, String param){
DefaultProfile profile = DefaultProfile.getProfile(
"cn-hangzhou",
"key",
"private key"
);
IAcsClient client = new DefaultAcsClient(profile);
SendSmsRequest request = new SendSmsRequest();
request.setSysRegionId("cn-hangzhou");
request.setPhoneNumbers(phoneNumbers);
request.setSignName(SIGN_NAME);
request.setTemplateCode(TEMPLATE_CODE);
request.setTemplateParam("{\"code\":\"" + param + "\"}");
try {
SendSmsResponse response = client.getAcsResponse(request);
System.out.println("短信发送成功");
}catch (ClientException e) {
e.printStackTrace();
}
}
}
controller
需要调用工具类发送短信,并将验证码存入Session:
@PostMapping("/sendMsg")
public Result<String> sendMsg(@RequestBody User user, HttpSession session) {
String code = ValidateCodeUtils.generateValidateCode4String(4);
SMSUtils.sendMessage(user.getPhone(), code);
log.info("发送验证码:{}", code);
// 将验证码保存到Session
session.setAttribute(user.getPhone(), code);
return Result.success("短信发送成功,验证码为" + code);
}
6.2 登录
- 将用户发来的验证码,与session中存起来的验证码进行比较
- 不同,登录失败
- 相同,用户表中是否有该user,如果是新用户,加入user表里
- 将id存入session,以便
CheckLoginFilter
能够取到 - 如果仔细观察你会发现
userService.save(user)
以后用户自动拥有了一个id。
- 将id存入session,以便
@PostMapping("/login")
public Result<User> login(@RequestBody Map<String, String> map, HttpSession session) {
// 获取手机号、验证码进行比对
String phone = map.get("phone");
String code = map.get("code");
String sessionCode = (String) session.getAttribute(phone);
// 比对成功,登录成功
if (code != null && code.equals(sessionCode)) {
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getPhone, phone);
User user = userService.getOne(queryWrapper);
// 如果当前用户是新用户,加入user表中
if (user == null) {
user = new User();
user.setPhone(phone);
user.setStatus(1);
userService.save(user);
}
session.setAttribute("user", user.getId());
log.info("用户登录成功,{}", user.getId());
return Result.success(user);
}
// 比对失败
return Result.error("登录失败");
}
七、购物车
7.1 添加菜品和套餐
购物车表:
- 判断是菜品还是套餐
- 每个用户对应一个购物车id,查看该用户的购物车中是否存在该item
- 存在,count + 1,更新;不存在,count=1,写入。
@PostMapping("/add")
public Result<ShoppingCart> add(@RequestBody ShoppingCart shoppingCart){
log.info("购物车数据:{}",shoppingCart);
// 先设置相应属性,然后看看这道菜购物车里有没有,如果没有,加入表;如果有,number+1
shoppingCart.setUserId(BaseContext.getCurrentId());
// 查看当前菜品 或 套餐是否在购物车中
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper();
queryWrapper.eq(ShoppingCart::getUserId, shoppingCart.getUserId());
if (shoppingCart.getDishId() != null) {
queryWrapper.eq(ShoppingCart::getDishId, shoppingCart.getDishId());
}
if (shoppingCart.getSetmealId() != null) {
queryWrapper.eq(ShoppingCart::getSetmealId, shoppingCart.getSetmealId());
}
ShoppingCart target = shoppingCartService.getOne(queryWrapper);
if (target != null) {
// 在购物车里,数量加一
target.setNumber(target.getNumber() + 1);
shoppingCartService.updateById(target);
}
else {
shoppingCart.setNumber(1);
shoppingCartService.save(shoppingCart);
target = shoppingCart;
}
return Result.success(target);
}
文件配置
通过配置这里设置了端口,发送response的编码,mybatis plus的名字映射方式,全局id的生成方式,文件上传路径。
application.yml
server:
port: 629
servlet:
encoding:
force: true
charset: UTF-8
spring:
application:
#应用的名称,
name:
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/beautysalon?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
username: root
password:
mybatis-plus:
configuration:
#在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
# 全局id的生成方式
id-type: ASSIGN_ID
koukou:
path: E:\\leetcode\\project_pre\\BeautySalon\\src\\main\\resources\\front\\upload\\
pom.xml
<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>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</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>
<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>
</dependencies>