后台系统
菜品管理(批量删除、起售停售)
套餐管理(修改、起售停售)
订单明细
员工管理
前台系统
个人中心(退出登录、最新订单查询、历史订单、地址管理-修改地址、地址管理-删除地址)
购物车(删除购物车中的商品)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dSWFcRuN-1654599626144)(E:\CodeStudy\学习笔记\笔记图库\博客项目图库\image-20220425122837673.png)]
数据库表说明
定义一个通用返回结果,服务端响应的数据最终都会分装成此对象,通过此结果类与前端进行交互。使用泛型,可以兼容不同的实体类。
@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;
}
}
package com.yawn.reggie.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.yawn.reggie.common.R;
import com.yawn.reggie.entity.Employee;
import com.yawn.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.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
@Slf4j
@RestController
@RequestMapping("/employee")
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
@PostMapping("/login")
public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee){
//1.将页面提交的密码进行MD5加密处理
String password = employee.getPassword();
password = DigestUtils.md5DigestAsHex(password.getBytes());
//2.根据页面提交的用户名查找数据库
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Employee::getUsername,employee.getUsername());
Employee emp = employeeService.getOne(queryWrapper);
//3.如果没有查询到则返回登录失败结果
if(emp == null){
return R.error("登陆失败");
}
//4.判断密码是否一致
if(!password.equals(emp.getPassword())){
return R.error("密码错误");
}
//判断员工状态是否正常
if(emp.getStatus()==0){
return R.error("账号已禁用");
}
//验证成功将员工id存入session
request.getSession().setAttribute("employee",emp.getId());
return R.success(emp);
}
}
使用过滤器或拦截器,判断用户是否已经登录。步骤如下:
1.创建自定义的 LoginCheckFilter
2.在启动类上加入 @ServletComponentScan注解
3.完善过滤器处理逻辑,逻辑如下:
package com.yawn.reggie.filter;
import com.alibaba.fastjson.JSON;
import com.yawn.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 {
private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;
//1.获取本次请求的URI
String requestURI = request.getRequestURI();
log.info("拦截到请求:{}",requestURI);
//定义不需要处理的请求
String[] urls = new String[]{
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**"
};
//2.判断本次请求是否需要处理
boolean check = check(urls, requestURI);
if(check){
log.info("本次请求{}不需要处理",requestURI);
filterChain.doFilter(request,response);
return;
}
//3.判断是否登录,如果已经登录则放行
if(request.getSession().getAttribute("employee")!=null){
log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("employee"));
filterChain.doFilter(request,response);
return;
}
log.info("用户未登录");
//4.如果未登录则拦截,不是直接返回登录页面,而是通过向客户端页面响应数据,再由前端代码控制跳转到登录页面
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;
}
}
1.清理 Session 中的用户 id
2.返回结果
@PostMapping("/logout")
public R<String> logout(HttpServletRequest request){
//清理 Session 中的用户 id
request.getSession().removeAttribute("employee");
//返回结果
return R.success("退出成功");
}
@PostMapping
public R<String> save(HttpServletRequest request, @RequestBody Employee employee){
log.info("新增员工,员工信息:{}",employee.toString());
//设置初始密码为123456,并加密。注意参数类型的转换
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("添加成功");
}
编写一个异常处理器,处理整个项目的异常情况,每个模块发生异常都可以进行捕捉。这样就不用重复得在每个模块中使用 try…catch 捕获异常,减轻代码量。基本原理是将异常处理功能通过 AOP 进行封装后,切入各个业务模块中。
//这个注解是关键
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {
//添加要处理的异常信息
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
log.error(ex.getMessage());
//这里的字符串处理很有用,学习一下
if(ex.getMessage().contains("Duplicate entry")){
String[] split = ex.getMessage().split(" ");
String msg = split[2] + "已存在";
return R.error(msg);
}
return R.error("未知错误");
}
}
总结
这是典型的请求响应式流程。
小心得:使用 Vue 这种框架开发,真的要做好交互一致,很多在前端代码中定义好的参数、变量名等,后端代码也和其保持一致,不可以自己随便定义,所以对于后端开发者来说,基本的前端阅读能力也是很重要的,最好能系统学习一下前端。
使用 MyBatisPlus 分页插件
@Configuration
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
}
编写 Controller
//分页查询
@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.isEmpty(name),Employee::getName,name);
//添加排序条件
queryWrapper.orderByDesc(Employee::getUpdateTime);
//执行查询
employeeService.page(pageInfo,queryWrapper);
return R.success(pageInfo);
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mgqMFAAO-1654599626145)(E:\CodeStudy\学习笔记\笔记图库\博客项目图库\image-20220501120322702.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HFOZT91v-1654599626145)(E:\CodeStudy\学习笔记\笔记图库\博客项目图库\image-20220501120322702.png)]
注意点:js 处理 long 型数据会出现精度丢失的情况(js 16位,SQL 19位),导致提交的 id 与数据库 id 不一致。解决办法是在服务端给页面响应 json 数据时,将 long 型数据统一转换为 String 字符串。
/**
* 扩展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); //下标为0表示把自己编写的消息转换器放在最前面
}
/**
* 根据id修改员工信息
* @param request
* @param employee
* @return
*/
@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("员工信息修改成功");
}
简单来说就是,修改和新增是同一个页面,点击“编辑”按钮,这时会先判断是否存在 id ,有 id 是进入编辑页面,无 id 是保存页面。然后前端页面会发送一个带有用户 id 的请求,并且要求服务端进行信息查询,服务端接收请求后根据用户 id 查询数据后,以 json 格式返回用户数据,前端页面拿到后进行数据回显,然后用户进行修改,点击“保存”按钮,由于进入的是编辑页面,所以保存按钮请求的是服务端的更新操作。
/**
* 根据id查询员工信息
* @param id
* @return
*/
@GetMapping("/{id}")
public R<Employee> getById(@PathVariable Long id){
log.info("根据id查询员工信息...");
Employee employee = employeeService.getById(id);
if(employee != null){
return R.success(employee);
}
return R.error("没有查询到对应员工信息");
}
获取当前用户 id 的解决方案
由于在 MyMetaObjecthandler 类中是不能获得 HttpSession 对象的,因此就无法通过 session 获得用户 id,可以用 ThreadLocal 解决这个问题,它是 JDK 中提供的一个类。
1.编写工具类
/**
* 基于ThreadLocal封装工具类,用户保存和获取当前登录用户id
* 因为是工具类,所以将方法和变量设置为静态方法
*/
public class BaseContext {
private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
/**
* 设置值
* @param id
*/
public static void setCurrentId(Long id){
threadLocal.set(id);
}
/**
* 获取值
* @return
*/
public static Long getCurrentId(){
return threadLocal.get();
}
}
2.在 LoginCheckFilter 过滤器中获取 id
Long empId = (Long) request.getSession().getAttribute("employee"); //获取用户id
BaseContext.setCurrentId(empId); //将用户id存入ThreadLocal中
3.编写自定义元数据对象处理器
/**
* 自定义元数据对象处理器
*/
@Component
@Slf4j
public class MyMetaObjecthandler implements MetaObjectHandler {
/**
* 插入操作,自动填充
* @param metaObject
*/
@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", BaseContext.getCurrentId());
metaObject.setValue("updateUser",BaseContext.getCurrentId());
}
/**
* 更新操作,自动填充
* @param metaObject
*/
@Override
public void updateFill(MetaObject metaObject) {
log.info("公共字段自动填充[update]...");
log.info(metaObject.toString());
long id = Thread.currentThread().getId();
log.info("线程id为:{}",id);
metaObject.setValue("updateTime",LocalDateTime.now());
metaObject.setValue("updateUser",BaseContext.getCurrentId());
}
}
首先搭建框架,步骤与员工管理相同,省略…
然后编写控制器:
@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("新增分类成功");
}
}
逻辑与员工信息分页查询一样,只是查询的表发生了变化。
/**
* 分页查询
* @param page
* @param pageSize
* @return
*/
@GetMapping("/page")
public R<Page> page(int page, int pageSize){
//分页构造器
Page<Category> pageInfo = new Page<>(page,pageSize);
//条件构造器,根据菜品分类sort属性排序
LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
//添加排序条件,根据sort进行排序
queryWrapper.orderByAsc(Category::getSort);
//分页查询
categoryService.page(pageInfo,queryWrapper);
return R.success(pageInfo);
}
删除分类时,需要先判断该类别中是否有菜品,如果有不能删除。通过在业务代码中实现自己编写的类完成这个功能。
1.自定义删除菜品分类方法,先在菜品业务接口中定义方法,再在实现类中实现重写方法。
public interface CategoryService extends IService<Category> {
public void remove(Long id);
}
@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {
@Autowired
private DishService dishService;
@Autowired
private SetmealService setmealService;
/**
* 自定义删除菜品分类方法
* @param id
*/
@Override
public void remove(Long id) {
//构建条件查询器
LambdaQueryWrapper<Dish> queryWrapper1 = new LambdaQueryWrapper<>();
//构建查询条件
queryWrapper1.eq(Dish::getCategoryId,id);
//执行查询
int count1 = dishService.count(queryWrapper1);
//判断是否存在菜品,存在则抛出异常
if(count1>0){
throw new CustomException("当前分类下关联了菜品,不能删除");
}
//构建条件查询器
LambdaQueryWrapper<Setmeal> queryWrapper2 = new LambdaQueryWrapper<>();
//构建查询条件
queryWrapper2.eq(Setmeal::getCategoryId,id);
//执行查询
int count2 = setmealService.count(queryWrapper2);
//判断是否存在菜品,存在则抛出异常
if(count2>0){
throw new CustomException("当前分类下关联了套餐,不能删除");
}
//既不关联菜品,也不关联套餐,这时可以正常删除分类,直接调用父类的方法
super.removeById(id);
}
}
2.自定义业务异常类,放在 common 包下。
public class CustomException extends RuntimeException {
public CustomException(String message){
super(message);
}
}
3.在全局异常处理器中添加该异常。
/**
* 异常处理方法
* @return
*/
@ExceptionHandler(CustomException.class)
public R<String> exceptionHandler(CustomException ex){
log.error(ex.getMessage());
return R.error(ex.getMessage());
}
4.编写控制器。
/**
* 根据id删除分类
* @param id
* @return
*/
@DeleteMapping
public R<String> delete(Long id){
log.info("删除分类,id为:{}",id);
categoryService.remove(id);
return R.success("分类信息删除成功");
}
/**
* 根据id修改分类信息
* @param category
* @return
*/
@PutMapping
public R<String> update(@RequestBody Category category){
log.info("修改分类信息:{}",category);
categoryService.updateById(category);
return R.success("修改分类信息成功");
}
Spring 框架在 spring-web 包中对文件上传进行了封装,大大简化了服务端代码,我们只需要在 Controller 的方法中声明一个 MultipartFile 类型的参数即可接收上传的文件。
1.在 application.yml 配置文件中配置默认路径
reggie:
path: E:\CodeStudy\Others\外卖项目\1 瑞吉外卖项目\代码\day04\
2.编写控制层代码
@RestController
@RequestMapping("/common")
@Slf4j
public class CommonController {
@Value("${reggie.path}")
private String basePath;
/**
* 文件上传
* @param file
* @return
*/
@PostMapping("/upload")
public R<String> upload(MultipartFile file){
//file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会删除
log.info(file.toString());
//原始文件名
String originalFilename = file.getOriginalFilename();//abc.jpg
String suffix = originalFilename.substring(originalFilename.lastIndexOf(".")); //后缀字符串
//使用UUID重新生成文件名,防止文件名称重复造成文件覆盖
String fileName = UUID.randomUUID().toString() + suffix;//dfsdfdfd.jpg
//创建一个目录对象
File dir = new File(basePath);
//判断当前目录是否存在
if(!dir.exists()){
//目录不存在,需要创建
dir.mkdirs();
}
try {
//将临时文件转存到指定位置
file.transferTo(new File(basePath + fileName));
} catch (IOException e) {
e.printStackTrace();
}
return R.success(fileName);
}
/**
* 文件下载,这里的下载指的是从后端输出图片到前端页面,也就是图片数据的回显
* @param name
* @param response
*/
@GetMapping("/download")
public void download(String name, HttpServletResponse response){
try {
//输入流,通过输入流读取文件内容
FileInputStream fileInputStream = new FileInputStream(new File(basePath + name));
//输出流,通过输出流将文件写回浏览器
ServletOutputStream outputStream = response.getOutputStream();
response.setContentType("image/jpeg");
int len = 0;
byte[] bytes = new byte[1024];
while ((len = fileInputStream.read(bytes)) != -1){
outputStream.write(bytes,0,len);
outputStream.flush();
}
//关闭资源
outputStream.close();
fileInputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
1.添加菜品时需要查询菜品分类信息,因为需要编写查询菜品分类方法,在 CategoryController 中添加方法。
@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).orderByAsc(Category::getUpdateTime);
List<Category> list = categoryService.list(queryWrapper);
return R.success(list);
}
2.编写菜品DTO
分析:
- Dish 表中没有专门的口味属性(DishFlavors),如果保存方法传入的参数直接为 Dish,是无法保存的
- 采用和做个人博客项目时类似的方法,个人博客是在显示博客详情页的时候重新封装了一个类,类里面有博客类属性、分类属性等;而这里是在新的类中放入菜品基本信息和口味信息,用于保存和显示。
@Data
public class DishDto extends Dish {
private List<DishFlavor> flavors = new ArrayList<>();
private String categoryName;
private Integer copies;
}
小心得:
- 善于使用 Debug 和日志排查问题,Debug 和日志可以看到具体的流程和数据生成过程
- @RequestBody 注解是为了保证控制器接受到前端页面的请求数据为 Json 格式
3.在菜品业务服务层编写实现方法(先写接口方法,再写实现类)
注意,用到了事务需要在启动类开启事务支持,即在启动类添加 @EnableTransactionManagement 注解
@Service
@Slf4j
public class DishServiceImpl extends ServiceImpl<DishMapper, Dish> implements DishService {
@Autowired
private DishFlavorService dishFlavorService;
@Override
@Transactional
public void saveWithFlavor(DishDto dishDto) {
//保存菜品的基本信息到菜品表dish
this.save(dishDto);
Long dishId = dishDto.getId();//菜品id
//菜品口味信息保存,因为直接保存时菜品的id无法保存进来
List<DishFlavor> dishFlavorList = dishDto.getFlavors();
dishFlavorList.stream().map((item) -> {
item.setDishId(dishId);
return item;
}).collect(Collectors.toList());
//保存菜品口味数据到菜品口味表dish_flavor
dishFlavorService.saveBatch(dishFlavorList);
}
}
4.控制层代码编写(选择哪种请求方式是根据前端代码决定的)
@PostMapping
public R<String> save(@RequestBody DishDto dishDto){
log.info(dishDto.toString());
dishService.saveWithFlavor(dishDto);
return R.success("保存成功");
}
分析:菜品分类列显示的是分类名称,但是数据其实是代码,无法直接显示,因此需要转换,方法就是使用 dishDto 类。过程稍微有些复杂,但是简单来说就是创建一个 dishDto 类型的 Page;然后将 dish 类型的 Page 除了 records 的属性赋值给 dishDto 类型的 Page,因为两个类型的 Page 中 records 代表的是两个类型的记录,类型不一样,不能直接进行赋值;再对 records 单独处理,将菜品名称取出赋给 dishDto 中的 categoryName 属性;最后返回的是dishDto 类型的 Page。
@GetMapping("/page")
public R<Page> page(int page, int pageSize, String name){
//构造分页构造器
Page<Dish> pageInfo = new Page<>(page,pageSize);
Page<DishDto> dishDtoPage = new Page<>();
//构造条件构造器
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
//添加过滤条件
queryWrapper.like(name != null,Dish::getName,name);
//添加排序条件
queryWrapper.orderByAsc(Dish::getUpdateTime);
//执行分页查询(不是用query)
dishService.page(pageInfo,queryWrapper);
//对象拷贝
BeanUtils.copyProperties(pageInfo,dishDtoPage,"records");
//处理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();//分类id
//根据id查询分类对象
Category category = categoryService.getById(categoryId);
if(category != null){
dishDto.setCategoryName(category.getName());
}
//返回处理好的dishDto对象
return dishDto;
}).collect(Collectors.toList());
dishDtoPage.setRecords(list);
return R.success(dishDtoPage);
}
分析:与新增菜品共用一个页面,但需要先进行数据回显,因此这里稍微复杂一些的是设计回显的方法和数据表结构。
1.在 DishService 接口中新增一个 getByIdWithFlavor 方法,然后在实现类中设计具体实现方法。
接口:public DishDto getByIdWithFlavor(Long id);
实现类:
@Override
public DishDto getByIdWithFlavor(Long id) {
//查询菜品基本信息,从dish表查询
Dish dish = this.getById(id);
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(dish,dishDto);
//查询当前菜品对应的口味信息,从dish_flavor表查询
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DishFlavor::getDishId,dish.getId());
List<DishFlavor> flavors = dishFlavorService.list(queryWrapper);
dishDto.setFlavors(flavors);
return dishDto;
}
2.在 DishService 接口中新增一个 updateWithFlavor方法,然后在实现类中设计具体实现方法。
接口:public void updateWithFlavor(DishDto dishDto);
方法:
@Override
@Transactional
public void updateWithFlavor(DishDto dishDto) {
//1.更新dish表基本信息
this.updateById(dishDto);
//2.清理当前菜品对应口味数据---dish_flavor表的delete操作
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper();
queryWrapper.eq(DishFlavor::getDishId,dishDto.getId());
dishFlavorService.remove(queryWrapper);
//3.添加当前提交过来的口味数据---dish_flavor表的insert操作
List<DishFlavor> flavors = dishDto.getFlavors();
flavors = flavors.stream().map((item) -> {
item.setDishId(dishDto.getId());
return item;
}).collect(Collectors.toList());
dishFlavorService.saveBatch(flavors);
}
3.控制器
/**
* 根据id查询菜品信息和对应的口味信息
* @param id
* @return
*/
@GetMapping("/{id}")
public R<DishDto> get(@PathVariable Long id){
DishDto dishDto = dishService.getByIdWithFlavor(id);
return R.success(dishDto);
}
/**
* 修改菜品
* @param dishDto
* @return
*/
@PutMapping
public R<String> update(@RequestBody DishDto dishDto){
log.info(dishDto.toString());
dishService.updateWithFlavor(dishDto);
return R.success("新增菜品成功");
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rOjRU3da-1654599626146)(E:\CodeStudy\学习笔记\笔记图库\博客项目图库\image-20220504132400496.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZyPtNr7G-1654599626146)(E:\CodeStudy\学习笔记\笔记图库\博客项目图库\image-20220504132400496.png)]
1.框架搭建,包括创建实体类,Dto类,Mapper,Service,Controller
2.业务代码和新增分类相似,编写业务代码和控制层代码
①在 DishController 中添加响应查询菜品信息的方法
@GetMapping("/list")
public R<List<Dish>> list(Dish dish){
log.info("dish:{}", dish);
//条件构造器
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(null != dish.getCategoryId(),Dish::getCategoryId,dish.getCategoryId());
queryWrapper.eq(Dish::getStatus, 1);
queryWrapper.orderByDesc(Dish::getUpdateTime);
List<Dish> list = dishService.list(queryWrapper);
return R.success(list);
}
②在 SetmealServiceImpl 中添加如下方法
@Service
@Slf4j
public class SetmealServiceImpl extends ServiceImpl<SetmealMapper, Setmeal> implements SetmealService {
@Autowired
private SetmealDishService setmealDishService;
@Override
@Transactional
public void saveWithDish(SetmealDto setmealDto) {
//保存套餐的基本信息,操作setmeal,执行insert操作
this.save(setmealDto);
List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
setmealDishes.stream().map((item)->{
item.setSetmealId(setmealDto.getId());
return item;
}).collect(Collectors.toList());
//保存套餐和菜品的关联信息,操作setmeal_dish,执行insert操作
setmealDishService.saveBatch(setmealDishes);
}
}
③在 SetmealController 中添加如下方法
@PostMapping
public R<String> save(@RequestBody SetmealDto setmealDto){
setmealService.saveWithDish(setmealDto);
return R.success("新增套餐成功");
}
@GetMapping("/page")
public R<Page> page(int page, int pageSize, String name){
//构造分页构造器
Page<Setmeal> pageInfo = new Page<>(page,pageSize);
Page<SetmealDto> setmealDtoPage = new Page<>();
//构造查询构造器
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
//添加查询条件,根据name进行like模糊查询
queryWrapper.like(name != null,Setmeal::getName,name);
//添加排序条件,根据更新时间降序排列
queryWrapper.orderByDesc(Setmeal::getUpdateTime);
setmealService.page(pageInfo,queryWrapper);
//属性拷贝
BeanUtils.copyProperties(pageInfo,setmealDtoPage,"records");
List<Setmeal> records = pageInfo.getRecords();
List<SetmealDto> list = records.stream().map((item)->{
SetmealDto setmealDto = new SetmealDto();
//对象拷贝
BeanUtils.copyProperties(item,setmealDto);
//分类id
Long categoryId = item.getCategoryId();
//根据分类id查询分类对象
Category category = categoryService.getById(categoryId);
if(category != null){
//分类名称
String categoryName = category.getName();
setmealDto.setCategoryName(categoryName);
}
return setmealDto;
}).collect(Collectors.toList());
setmealDtoPage.setRecords(list);
return R.success(setmealDtoPage);
}
需要注意套餐与菜品之间存在关联关系,因为删除套餐的同时要需要把响应的菜品信息删除,当然是setmeal_dish表中的菜品
/**
* 删除套餐,同时需要删除套餐和菜品的关联数据
* @param ids
*/
@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 = this.count(queryWrapper);
if(count > 0){
//如果不能删除,抛出一个业务异常
throw new CustomException("套餐正在售卖中,不能删除");
}
//删除setmeal
this.removeByIds(ids);
//删除setmeal_dish
LambdaQueryWrapper<SetmealDish> queryWrapper2 = new LambdaQueryWrapper<>();
queryWrapper2.in(SetmealDish::getSetmealId,ids);
setmealDishService.remove(queryWrapper2);
}
@DeleteMapping
public R<String> delete(@RequestParam List<Long> ids){
setmealService.removeWithDish(ids);
return R.success("套餐数据删除成功");
}
和分类修改类似,暂不写。
分析:传入的是 id 号,需要根据 id 号 更新整条记录。
/**
* 改变套餐状态
* @param ids
*/
@Override
@Transactional
public void changeStatus(List<Long> ids) {
for (Long id:ids) {
Setmeal setmeal = new Setmeal();
Setmeal setmeal1 = this.getById(id);
BeanUtils.copyProperties(setmeal1,setmeal,"status");
Integer status = setmeal1.getStatus();
if(status == 0){
setmeal.setStatus(1);
}else {
setmeal.setStatus(0);
}
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Setmeal::getId,id);
this.update(setmeal,queryWrapper);
}
}
@PostMapping("/status/{params.status}")
public R<String> change(@RequestParam List<Long> ids){
setmealService.changeStatus(ids);
return R.success("状态修改成功");
}
心得:
- 一定要结合前端代码编写方法,同时利用页面的响应工具抓请求状态,搞清楚自己这次请求响应传输的到底是什么数据
- 很多功能写不出来可能是对封装的方法不熟悉,因为要善于运用别人分享的资源,多查
使用阿里云的短信服务,具体申请步骤不难,但是审核麻烦,这里没有申请,了解如何调用 API 即可。
<dependency>
<groupId>com.aliyungroupId>
<artifactId>aliyun-java-sdk-coreartifactId>
<version>4.5.16version>
dependency>
<dependency>
<groupId>com.aliyungroupId>
<artifactId>aliyun-java-sdk-dysmsapiartifactId>
<version>2.1.0version>
dependency>
/**
* 短信发送工具类
*/
public class SMSUtils {
/**
* 发送短信
* @param signName 签名
* @param templateCode 模板
* @param phoneNumbers 手机号
* @param param 参数
*/
public static void sendMessage(String signName, String templateCode,String phoneNumbers,String param){
DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", "", "");
IAcsClient client = new DefaultAcsClient(profile);
SendSmsRequest request = new SendSmsRequest();
request.setSysRegionId("cn-hangzhou");
request.setPhoneNumbers(phoneNumbers);
request.setSignName(signName);
request.setTemplateCode(templateCode);
request.setTemplateParam("{\"code\":\""+param+"\"}");
try {
SendSmsResponse response = client.getAcsResponse(request);
System.out.println("短信发送成功");
}catch (ClientException e) {
e.printStackTrace();
}
}
}
1.搭建框架,包括编写用户实体类,Mapper 层、业务层、控制层,代码和前面类似,这里省略
2.完善登录过滤器,过滤未登录的用户,放行短信发送请求和登录请求;同时判断用户是否登录,登录则放行
//3.2判断是否登录,如果已经登录则放行
if(request.getSession().getAttribute("user")!=null){
log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("user"));
Long userId = (Long) request.getSession().getAttribute("user"); //获取用户id
BaseContext.setCurrentId(userId); //将用户id存入ThreadLocal中
filterChain.doFilter(request,response);
return;
}
3.客户端页面使用H5开发,调试页面时,将页面设置为移动端模式
4.编写控制器
分析:保存登录信息时,直接传递用户类是不行的,因为用户类中没有验证码信息。解决方法有两种,一个是像之前一样编写一个用户Dto类,另一个是使用 Map 保存,这样更方便一些。
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
@Autowired
private UserService userService;
/**
* 发送手机短信验证码
* @param user
* @return
*/
@PostMapping("/sendMsg")
public R<String> sendMsg(@RequestBody User user, HttpSession session) {
//获取手机号
String phone = user.getPhone();
if(phone.length()>0){
//生成4位验证码
String code = ValidateCodeUtils.generateValidateCode(4).toString();
log.info("code={}",code);
//调用阿里云提供的短信服务API完成发送短信
//SMSUtils.sendMessage("reggie","",phone,code);
//需要将生成的验证码保存到Session
session.setAttribute(phone,code);
R.success("手机验证码短信发送成功");
}
return R.error("短信发送失败");
}
/**
* 移动端用户登录
* @param map
* @param session
* @return
*/
@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();
//获取session中获取保存的验证码
Object code1 = session.getAttribute(phone);
//判断验证码是否匹配
if(code1 != null && code1.equals(code)){
//验证成功
//查询用户是否存在
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getPhone,phone);
User user = userService.getOne(queryWrapper);
if(user == null){
//user为空表示是新用户,需要进行注册
user = new User();
user.setPhone(phone);
user.setStatus(1);
userService.save(user);
}
session.setAttribute("user",user.getId());
return R.success(user);
}
return R.error("登录失败");
}
}
前端页面: getCode() 方法中获取code 的方法换成 sendMsgApi({phone:this.form.phone}),另外在 api 包下的 login.js 文件下添加 sendMsgApi 方法。
5.bug记录
1.首先搭建框架,省略
2.控制层代码编写
@RestController
@RequestMapping("/addressBook")
@Slf4j
public class AddressBookController {
@Autowired
private AddressBookService addressBookService;
/**
* 新增
*/
@PostMapping
public R<AddressBook> save(@RequestBody AddressBook addressBook) {
addressBook.setUserId(BaseContext.getCurrentId());
addressBookService.save(addressBook);
log.info("addressBook:{}", addressBook);
return R.success(addressBook);
}
/**
* 设置默认地址
*/
@PutMapping("default")
public R<AddressBook> setDefault(@RequestBody AddressBook addressBook){
log.info("addressBook:{}", addressBook);
Long currentId = BaseContext.getCurrentId();
LambdaUpdateWrapper<AddressBook> queryWrapper = new LambdaUpdateWrapper<>();
//将当前用户的所有默认地址信息都更新为0
queryWrapper.eq(AddressBook::getUserId,currentId);
queryWrapper.set(AddressBook::getIsDefault,0);
addressBookService.update(queryWrapper);
//把此条地址设为默认地址
addressBook.setIsDefault(1);
addressBookService.updateById(addressBook);
return R.success(addressBook);
}
/**
* 根据id查询地址
*/
@GetMapping("/{id}")
public R get(@PathVariable Long id) {
AddressBook addressBook = addressBookService.getById(id);
if (addressBook != null) {
return R.success(addressBook);
} else {
return R.error("没有找到该对象");
}
}
/**
* 查询默认地址
*/
@GetMapping("default")
public R<AddressBook> getDefault() {
LambdaQueryWrapper<AddressBook> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(AddressBook::getUserId, BaseContext.getCurrentId());
queryWrapper.eq(AddressBook::getIsDefault, 1);
//一个用户很可能同时拥有几个地址,所以应该同时查询当前用户id和默认地址
//SQL:select * from address_book where user_id = ? and is_default = 1
AddressBook addressBook = addressBookService.getOne(queryWrapper);
if (null == addressBook) {
return R.error("没有找到该对象");
} else {
return R.success(addressBook);
}
}
/**
* 查询指定用户的全部地址
*/
@GetMapping("/list")
public R<List<AddressBook>> list(AddressBook addressBook) {
addressBook.setUserId(BaseContext.getCurrentId());
log.info("addressBook:{}", addressBook);
//条件构造器
LambdaQueryWrapper<AddressBook> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(null != addressBook.getUserId(), AddressBook::getUserId, addressBook.getUserId());
queryWrapper.orderByDesc(AddressBook::getUpdateTime);
//SQL:select * from address_book where user_id = ? order by update_time desc
return R.success(addressBookService.list(queryWrapper));
}
/**
* 更新地址
* @param addressBook
* @return
*/
@PutMapping
private R<AddressBook> update(@RequestBody AddressBook addressBook){
log.info("addressBook:{}", addressBook);
Long currentId = BaseContext.getCurrentId();
LambdaUpdateWrapper<AddressBook> queryWrapper = new LambdaUpdateWrapper<>();
queryWrapper.eq(AddressBook::getUserId,currentId);
queryWrapper.eq(AddressBook::getId,addressBook.getId());
addressBookService.updateById(addressBook);
return R.success(addressBook);
}
}
1.修改菜品展示表,在 DishController 中修改 list 方法。
分析:后台菜品展示列表返回类型为 Dish,其中不包含口味信息,这样用户在前台添加菜品进入购物车时就无法添加口味信息,因此需要进行修改。
@GetMapping("/list")
public R<List<DishDto>> list(Dish dish){
//构造查询条件
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(dish.getCategoryId() != null ,Dish::getCategoryId,dish.getCategoryId());
//添加条件,查询状态为1(起售状态)的菜品
queryWrapper.eq(Dish::getStatus,1);
//添加排序条件
queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
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();//分类id
//根据id查询分类对象
Category category = categoryService.getById(categoryId);
if(category != null){
String categoryName = category.getName();
dishDto.setCategoryName(categoryName);
}
//当前菜品的id
Long dishId = item.getId();
LambdaQueryWrapper<DishFlavor> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(DishFlavor::getDishId,dishId);
//SQL:select * from dish_flavor where dish_id = ?
List<DishFlavor> dishFlavorList = dishFlavorService.list(lambdaQueryWrapper);
dishDto.setFlavors(dishFlavorList);
return dishDto;
}).collect(Collectors.toList());
return R.success(dishDtoList);
}
2.展示套餐信息,在 SetmealController 中添加 list 方法,用于信息套餐具体的菜品信息。
@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);
}
@RestController
@RequestMapping("/shoppingCart")
@Slf4j
public class ShoppingCartController {
@Autowired
private ShoppingCartService shoppingCartService;
/**
* 添加购物车
* @param shoppingCart
* @return
*/
@PostMapping("/add")
public R<ShoppingCart> add(@RequestBody ShoppingCart shoppingCart){
log.info("购物车数据:{}",shoppingCart);
//设置用户id
shoppingCart.setUserId(BaseContext.getCurrentId());
//构建查询构造器
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());
//判断添加的是菜品还是套餐
Long dishId = shoppingCart.getDishId();
if(dishId != null){
//添加的是菜品,添加相应查询条件
queryWrapper.eq(ShoppingCart::getDishId,dishId);
}else {
//添加的是套餐
queryWrapper.eq(ShoppingCart::getSetmealId,shoppingCart.getSetmealId());
}
//判断是否是第一次添加
ShoppingCart shoppingCart1 = shoppingCartService.getOne(queryWrapper);
if(shoppingCart1 != null){
//当前菜品或套餐已经存在,只需要把数量加1即可
Integer number = shoppingCart1.getNumber();
shoppingCart1.setNumber(number+1);
shoppingCartService.updateById(shoppingCart1);
}else {
//当前菜品或套餐不存在,需要把传入的数据存起来
shoppingCart.setNumber(1);
shoppingCart.setCreateTime(LocalDateTime.now());
shoppingCart1 = shoppingCart;
shoppingCartService.save(shoppingCart1);
}
return R.success(shoppingCart1);
}
/**
* 查看购物车
* @return
*/
@GetMapping("/list")
public R<List<ShoppingCart>> list(){
log.info("查看购物车...");
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());
queryWrapper.orderByAsc(ShoppingCart::getCreateTime);
List<ShoppingCart> list = shoppingCartService.list(queryWrapper);
return R.success(list);
}
/**
* 清空购物车
* @return
*/
@DeleteMapping("/clean")
public R<String> clean(){
//SQL:delete from shopping_cart where user_id = ?
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());
shoppingCartService.remove(queryWrapper);
return R.success("清空购物车成功");
}
}
注:具体的支付功能并没有开发,因为需要申请资质,这里只是将订单信息保存即可。
1.业务层实现
分析:这里主要是业务层代码比较复杂,需要考虑下单时的具体的实体类信息的设置,前端传递的信息不完整,因此需要手动设置订单表属性和订单明细表属性,两个表存放的数据属性不一样。而这些属性内容主要来自于用户表,购物车表还有地址表。一些细节的处理也需要学习,比如金额总数相关操作使用原子整型类,生成订单号的工具类,对购物车数据遍历求总金额的同时设置订单详细数据等。
@Service
@Slf4j
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Orders> implements OrderService {
@Autowired
private ShoppingCartService shoppingCartService;
@Autowired
private UserService userService;
@Autowired
private AddressBookService addressBookService;
@Autowired
private OrderDetailService orderDetailService;
@Override
@Transactional
public void submit(Orders orders) {
//获取当前用户id
Long currentId = BaseContext.getCurrentId();
//查询当前用户购物车数据
LambdaQueryWrapper<ShoppingCart> queryWrapper =new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId,currentId);
List<ShoppingCart> list = shoppingCartService.list(queryWrapper);
if(list == null || list.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> orderDetails = list.stream().map((item) -> {
OrderDetail orderDetail = new OrderDetail();
orderDetail.setOrderId(orderId);
orderDetail.setNumber(item.getNumber());
orderDetail.setDishFlavor(item.getDishFlavor());
orderDetail.setDishId(item.getDishId());
orderDetail.setSetmealId(item.getSetmealId());
orderDetail.setName(item.getName());
orderDetail.setImage(item.getImage());
orderDetail.setAmount(item.getAmount());
amount.addAndGet(item.getAmount().multiply(new BigDecimal(item.getNumber())).intValue());
return orderDetail;
}).collect(Collectors.toList());
//设置订单详细数据
orders.setId(orderId);
orders.setOrderTime(LocalDateTime.now());
orders.setCheckoutTime(LocalDateTime.now());
orders.setStatus(2);
orders.setAmount(new BigDecimal(amount.get()));//总金额
orders.setUserId(currentId);
orders.setNumber(String.valueOf(orderId));
orders.setUserName(user.getName());
orders.setConsignee(addressBook.getConsignee());
orders.setPhone(addressBook.getPhone());
orders.setAddress((addressBook.getProvinceName() == null ? "" : addressBook.getProvinceName())
+ (addressBook.getCityName() == null ? "" : addressBook.getCityName())
+ (addressBook.getDistrictName() == null ? "" : addressBook.getDistrictName())
+ (addressBook.getDetail() == null ? "" : addressBook.getDetail()));
//向订单表插入一条订单数据
this.save(orders);
//向订单明细表插入购物车中的多条具体菜品数据
orderDetailService.saveBatch(orderDetails);
//清空购物车
shoppingCartService.remove(queryWrapper);
}
}
rderId(orderId);
orderDetail.setNumber(item.getNumber());
orderDetail.setDishFlavor(item.getDishFlavor());
orderDetail.setDishId(item.getDishId());
orderDetail.setSetmealId(item.getSetmealId());
orderDetail.setName(item.getName());
orderDetail.setImage(item.getImage());
orderDetail.setAmount(item.getAmount());
amount.addAndGet(item.getAmount().multiply(new BigDecimal(item.getNumber())).intValue());
return orderDetail;
}).collect(Collectors.toList());
//设置订单详细数据
orders.setId(orderId);
orders.setOrderTime(LocalDateTime.now());
orders.setCheckoutTime(LocalDateTime.now());
orders.setStatus(2);
orders.setAmount(new BigDecimal(amount.get()));//总金额
orders.setUserId(currentId);
orders.setNumber(String.valueOf(orderId));
orders.setUserName(user.getName());
orders.setConsignee(addressBook.getConsignee());
orders.setPhone(addressBook.getPhone());
orders.setAddress((addressBook.getProvinceName() == null ? "" : addressBook.getProvinceName())
+ (addressBook.getCityName() == null ? "" : addressBook.getCityName())
+ (addressBook.getDistrictName() == null ? "" : addressBook.getDistrictName())
+ (addressBook.getDetail() == null ? "" : addressBook.getDetail()));
//向订单表插入一条订单数据
this.save(orders);
//向订单明细表插入购物车中的多条具体菜品数据
orderDetailService.saveBatch(orderDetails);
//清空购物车
shoppingCartService.remove(queryWrapper);
}
}
2.控制层,直接调用方法即可,内容略。