一、项目介绍
唐人外卖项目是专门为餐饮企业(餐厅、饭店)定制的一款软件产品,包括系统管理后台和移动端应用两部分。其中系统管理后台主要提供给餐饮企业内部员工使用,可以对餐厅的菜品、套餐、订单等进行管理维护。移动端应用主要提供给消费者使用,可以在线浏览菜品、添加购物车、下单等。
二、功能描述
1、管理端:餐饮企业内部员工使用。 主要功能有:
2、移动端:主要提供给消费者使用。主要功能有:
3、数据库文件
三、员工管理功能实现
1、登录功能
-
1.1 处理逻辑
1、将页面提交的密码password进行md5加密处理
2、根据页面提交的用户名username查询数据库
3、如果没有查询到则返回登录失败结果
4、密码比对,如果不一致则返回登录失败结果
5、查看员工状态,如果为已禁用状态,则返回员工已禁用结果
6、登录成功,将员工id存入Session并返回登录成功结果 -
1.2 处理流程
-
1.3 数据流向
前端发送请求,后端controller响应请求,同时调用service方法,service层又会通过mapper层操作数据库,完成数据的查询。
-
1.4 代码实现
/**
* 员工登录
* @param request 获取员工登录后的id存入session
* @param employee
* @return
*/
@PostMapping("/login")
public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee){ //requestBody接收json数据
//1.将页面提交的密码进行MD5加密处理
String password = employee.getPassword(); //获取密码
password = DigestUtils.md5DigestAsHex(password.getBytes());
//2.根据用户提交的用户名查询数据库
LambdaQueryWrapper<Employee> queryWrapper =new LambdaQueryWrapper<>();
queryWrapper.eq(Employee::getUsername,employee.getUsername());
//查询用户 ,数据库中的唯一索引 所以使用getOne
Employee emp = employeeService.getOne(queryWrapper);
//3.处理查询结果
if (emp ==null){
return R.error("登录失败!");
}
//4.密码比对,如果不一致则返回登录失败
if (!emp.getPassword().equals(password)){
return R.error("密码错误,登录失败!");
}
//5.查看员工状态,看是否被禁用,0表示禁用
if (emp.getStatus()==0){
return R.error("该账号已禁用!");
}
//6.登录成功,将员工id存入session并返回登录结果
request.getSession().setAttribute("employee",emp.getId());
return R.success(emp);
}
-
1.5 功能测试
发送请求:
数据封装:
2、登录功能完善-拦截器
- 2.1 创建过滤器LoginCheckFilter
过滤器具体的处理逻辑如下:
1、获取本次请求的URI
2、判断本次请求是否需要处理
3、如果不需要处理,则直接放行
4、判断登录状态,如果已登录,则直接放行
5、如果未登录则返回未登录结果 - 2.2 处理过程
- 2.3 代码实现
/**
* 检查用户是否登录
*/
@Slf4j
@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*") //拦截路径
public class LoginCheckFilter implements Filter {
//路径匹配器,支持通配符,Spring提供的工具包
public static final AntPathMatcher PATH_MATCHER =new AntPathMatcher();
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request =(HttpServletRequest)servletRequest;
HttpServletResponse response= (HttpServletResponse)servletResponse;
//1.获取本次请求的URI
String requestURI=request.getRequestURI(); //backend/index.html
log.info("拦截到请求:{}",requestURI);
//定义一些不需要处理的请求路径,直接放行
String[] urls = new String[]{
"/employee/login",
"employee/logout",
"/backend/**",
"/front/**",
"/common/**",
"/user/sendMsg", //移动端发送短信
"/user/login" //移动端登录
};
//2.判断本次请求是否需要处理
boolean check = check(urls, requestURI);
//3.如果不需要处理,直接放行
if (check){
log.info("本次请求:{}不需要处理",requestURI);
//放行
filterChain.doFilter(request,response);
return;
}
//4-1.判断登录状态,如果已经登录,则直接放行
if (request.getSession().getAttribute("employee") !=null){
log.info("用户已登陆,用户ID为{}",request.getSession().getAttribute("employee"));
Long empId =(Long) request.getSession().getAttribute("employee");
//将获取到的用户id存入当前线程
BaseContext.setCurrentId(empId);
filterChain.doFilter(request,response);
return;
}
//4-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);
filterChain.doFilter(request,response);
return;
}
log.info("用户未登录");
//5.未登录,则返回未登录结果,通过输出流的方式,向客户端页面响应数据
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
return;
}
/**
* 路径匹配,检查本次请求是否需要放行
* @param urls
* @param requestURI
* @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;
}
}
3、新增员工
- 3.1 处理逻辑:
1、页面发送ajax请求,将新增员工页面中输入的数据以json的形式提交到服务端
2、服务端Controller接收页面提交的数据并调用Service
3、Service调用Mapper操作数据库,保存数据
- 3.2 数据流向
- 3.3 逻辑代码
/**
* 新增员工
* @param employee
* @return
*/
@PostMapping
public R<String> save(HttpServletRequest request, @RequestBody Employee employee){
log.info("员工信息:{}",employee.toString());
//设置用户的初始密码 12345,需要进行md5加密处理
employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));
employee.setCreateTime(LocalDateTime.now());
employee.setUpdateTime(LocalDateTime.now());
//获取当前用户id
Long empId = (Long) request.getSession().getAttribute("employee");
employee.setCreateUser(empId);
employee.setUpdateUser(empId);
//mp提供的方法
employeeService.save(employee);
return R.success("新增员工成功");
}
4、员工信息分页查询
- 4.1 处理逻辑
1、页面发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端
2、服务端Controller接收页面提交的数据并调用Service查询数据
3、Service调用Mapper操作数据库,查询分页数据
4、Controller将查询到的分页数据响应给页面
- 4.2 逻辑代码
/**
* 员工信息的分页查询
* @param page
* @param pageSize
* @param name
* @return
*/
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name){ //mp提供的Page对象,包含相关的属性
log.info("page ={},pageSize={},name={}",page,pageSize,name);
//构造分页构造器
Page pageInfo =new Page(page,pageSize);
//条件构造器
LambdaQueryWrapper<Employee> queryWrapper =new LambdaQueryWrapper();
//添加过滤条件
queryWrapper.like(StringUtils.hasText(name),Employee::getName,name);
//添加排序条件,按更新时间排序
queryWrapper.orderByDesc(Employee::getUpdateTime);
//执行查询
employeeService.page(pageInfo,queryWrapper);
return R.success(pageInfo);
}
- 4.3 功能测试
新增员工信息时完成员工信息的封装
5、员工状态修改
- 5.1 处理逻辑
1、页面发送ajax请求,将参数(id、status)提交到服务端
2、服务端Controller接收页面提交的数据并调用Service更新数据
3、Service调用Mapper操作数据库
- 5.2 逻辑代码
/**
* 根据id修改员工信息
* @param employee
* @return
*/
@PutMapping
public R<String> update(HttpServletRequest request,@RequestBody Employee employee){
log.info(employee.toString());
//调用业务层方法(MP提供的)修改员工信息
employeeService.updateById(employee);
return R.success("员工信息修改成功!");
}
- 5.3 功能测试
测试过程中没有报错,但是功能并没有实现,查看数据库中的数据也没有变化。
观察控制台输出的SQL:
Bug根源:前端提供的用户id在执行SQL语句中的id和数据库保存的用户id不一致, 导致前后端获得的用户id不一致,所以数据更新失败
原因:js对long型数据进行处理时丢失精度,导致提交的id和数据库中的id不一致
解决办法:在服务端给页面响应json数据时,将long型数据统一转为String字符串 具体实现:
使用消息对象映射器JacksonObjectMapper将java对象转换为json数据:
在WebMvcConfig配置类中扩展SpringMvc的消息转换器,在此消息转换器中使用提供的对象转换器进行java对象到json数据的转换,将前端所有相关数据转换为指定类型:
/**
* 扩展SpringMvc的消息转换器
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器......");
//创建消息转换器对象,将返回结果转成相应的json,通过输出流的方式响应给页面
MappingJackson2HttpMessageConverter messageConverter =new MappingJackson2HttpMessageConverter();
//设置对象转换器,底层使用Jackson将java对象转为json
messageConverter.setObjectMapper(new JacksonObjectMapper());
//将上面的消息转换器对象追加到mvc框架的转换器集合中
converters.add(0,messageConverter);
}
6、修改员工信息
- 6.1 处理逻辑
1、点击编辑,发送ajax请求,请求服务端,同时提交员工id参数
2、服务端接收请求,根据员工id查询员工信息,将员工信息以json形式响应给页面
3、页面接收服务端响应的json数据,通过VUE的数据绑定进行员工信息回显
4、点击保存按钮,发送ajax请求,将页面中的员工信息以json方式提交给服务端
5、服务端接收员工信息,并进行处理,完成后给页面响应
- 6.2 逻辑代码
/**
* 根据id修改员工信息
* @param employee
* @return
*/
@PutMapping
public R<String> update(HttpServletRequest request,@RequestBody Employee employee){
log.info(employee.toString());
employeeService.updateById(employee);
return R.success("员工信息修改成功!");
}
四、分类管理功能实现
1、公共字段自动填充
数据库中多张表存在创建时间、更新时间、更新人等字段,这些字段在进行数据的插入、更新操作时需要频繁的手动添加,因此,使用Mybatis-plus提供的公共字段填充,来简化开发,开发步骤如下:
(1)添加注解,指定属性为公共字段,括号后面表示的是填充策略
(2)添加元数据对象处理器MyMetaObjectHandler实现MetaObjectHandler接口
/**
* 自定义元数据对象处理器
*/
@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());
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("updateUser", BaseContext.getCurrentId());
}
}
2、优化字段填充
客户端每次发送的请求,对应的在服务端都会分配一个新的线程来处理。这里使用Threadlocal进行优化:
Threadlocal并不是一个Thread,而是Thread的局部变量。当使用ThreadLocal维护变量时,
ThreadLocal会为每个使用该变量的线程提供独立的变量副本,每个线程都可以独立的改变自己的副本,而不会影响其他线程所对应的副本。
ThreadLocal为每个线程提供单独的一份存储空间,具有线程隔离的效果,只有在当前线程内才能获得对应的值,线程外不能访问。Threadlocal常用的方法:
Public void set(T value); //设置当前线程的局部变量的值
Public T get(); //返回当前线程局部变量的值
以下三个方法都属于同一线程:
1、LoginCheckFilter的doFilter方法
2、EmployeeController的update方法
3、MyMetaObjectHandler的updateFill方法
优化步骤:
1.编写BaseContext工具类,基于ThreadLocal封装的工具类
2.在LoginCheckFilter的doFilter方法中获取当前用户的id,调用Threadlocal的set方法设置当前线程局部变量的值;
3.在MyMetaObjectHandler的updateFill方法中调用Threadlocal的get方法获取当前线程局部变量的值;
/**
* 基于ThreadLocal的封装工具类,用于保存和获取当前登录用户的id
*/
public class BaseContext {
private static ThreadLocal<Long> threadLocal =new ThreadLocal<>();
//设置当前线程的id
public static void setCurrentId(Long id){
threadLocal.set(id);
}
//获取当前线程id
public static Long getCurrentId(){
return threadLocal.get();
}
}
3、新增分类
- 3.1 处理逻辑
1、页面(backend/page/category/list.html)发送ajax请求,将新增分类窗口输入的数据以json形式提交到服务端
2、服务端Controller接收页面提交的数据并调用Service将数据进行保存
3、Service调用Mapper操作数据库,保存数据
菜品和套餐除类型不一样,提交json数据结构都相同,可共用一个方法统一处理:
- 3.2 逻辑代码
/**
* 新增分类
* @param category
* @return
*/
@PostMapping
public R<String> save(@RequestBody Category category){
log.info("category:{}",category);
categoryService.save(category);
return R.success("新增分类成功!");
}
4、分类信息分页查询
- 4.1 处理逻辑
1、页面发送ajax请求,将分页查询参数(page、pageSize)提交到服务端
2、服务端Controller接收页面提交的数据并调用Service查询数据
2.Service调用Mapper操作数据库,查询分页数据
3.Controller将查询到的分页数据响应给页面
4.页面作数据展示
- 4.2 逻辑代码
/**
* 分页查询
* @param page
* @param pageSize
* @return
*/
@GetMapping("/page")
public R<Page> page(int page,int pageSize){
//分页构造器
Page<Category> pageInfo =new Page<>(page,pageSize);
//条件构造器
LambdaQueryWrapper<Category> queryWrapper =new LambdaQueryWrapper<>();
//添加排序条件,根据sort字段排序
queryWrapper.orderByAsc(Category::getSort);
//分页查询
categoryService.page(pageInfo,queryWrapper);
return R.success(pageInfo);
}
5、删除分类
- 5.1 处理逻辑
1、页面发送ajax请求,将参数(id)提交到服务端
2、服务端Controller接收页面提交的数据并调用Service删除数据
3、Service调用Mapper操作数据库
- 5.2 代码实现
/**
* 根据id删除分类
* @param ids
* @return
*/
@DeleteMapping
public R<String> delete(Long ids){
log.info("删除菜品,id为:{}",ids);
categoryService.remove(ids);
return R.success("分类信息删除成功!");
}
- 5.3 功能完善
检查删除的分类是否关联了菜品或者套餐,如果关联了则无法删除,要完善分类删除功能,需要先准备基础的类和接口:
1、实体类Dish和Setmeal
2、Mapper接口:DishMapper和SetmealMapper
3、Service接口:DishService和SetmealService
4、Service实现类:Dishservicelmpl和SetmealServicelmpl
自定义删除方法:
/**
* 根据id删除分类,删除之前先判断是否有关联
* @param id
*/
@Override
public void remove(Long id) {
LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
//根据分类id进行查询
dishLambdaQueryWrapper.eq(Dish::getCategoryId, id);
int count1 = dishService.count(dishLambdaQueryWrapper);
//查询当前分类是否关联了菜品,如果已关联了,抛出业务异常
if (count1 > 0) {
//已关联菜品,抛出业务异常
throw new CustomException("当前分类下关联了菜品,不能删除");
}
//查询当前分类是否关联了套餐,如果已关联,抛出业务异常
LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
//根据id进行查询
setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId, id);
int count2 = setmealService.count();
if (count2 > 0) {
//已关联了套餐
throw new CustomException("当前分类下关联了套餐,不能删除");
}
//正常情况下,正常删除
super.removeById(id);
}
若当前分类下关联了菜品或者套餐,则无法删除,添加一个业务异常类:
/**
* 自定义业务异常类
*/
public class CustomException extends RuntimeException {
public CustomException(String message){
super(message);
}
}
在全局异常处理器中添加异常捕获:
/**
* 处理自定义异常
* @param ex
* @return
*/
@ExceptionHandler(CustomException.class)
public R<String> exceptionHandler(CustomException ex) { //捕获到的异常
log.error(ex.getMessage());
return R.error(ex.getMessage());
}
五、菜品管理功能实现
1、文件的上传和下载
注意:文件上传时,对页面的form表单有如下要求:
method="post" 采用post方式提交数据
enctype="multipart/form-data" 采用multipart格式上传文件
type="file" 使用input的file控件上传
Spring框架在spring-web包中对文件上传进行了封装,大大简化了服务端代码,我们只需要在Controller的方法中声明一个MultipartFile类型的参数即可接收上传的文件,例如:
- 1.1 处理逻辑
1.获取上传文件的原始文件名;
2.使用UUID重新生成文件名,防止文件名重复造成文件覆盖;
3.创建一个目录对象,文件转存到指定位置
4.文件上传完毕后返回文件名称,将文件存入数据库 - 1.2 逻辑代码
/**
* 文件的上传
* @param file
* @return
*/
@PostMapping("/upload")
public R<String> update(MultipartFile file) { //file参数名已经和前端关联,此处文件名必须是file
log.info(file.toString());
//获取上传文件的原始文件名
String originalFilename = file.getOriginalFilename();
//suffix 为文件的后缀名
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
//使用UUID重新生成文件名,防止文件名重复造成文件覆盖
String fileName = UUID.randomUUID().toString()+suffix; //32位文件名
//创建一个目录对象
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);
}
- 1.3 文件下载
浏览器回显上传图片展示的过程,其实就是一次图片的下载,文件的上传下载本质上是对流的操作,通过IO操作从输入流中读取文件,再将图片信息展示到浏览器 - 1.4 逻辑代码
/**
* 文件下载
* @param name
*/
@GetMapping("/download")
public void download(String name, HttpServletResponse response){
try {
//1.通过输入流读取文件内容
FileInputStream fileInputStream =new FileInputStream(new File(basePath+name));
//2.通过输出流将文件写回浏览器,在浏览器展示图片
//在这里需要通过响应对象获取输出流
ServletOutputStream outputStream = response.getOutputStream();
//设置响应回去的文件类型
response.setContentType("img/jpeg");
byte[] bytes =new byte[1024];
int len =0;
//一直读取文件
while ((len =fileInputStream.read(bytes)) !=-1){
//将读取的文件写入
outputStream.write(bytes,0,len);
//刷新缓冲区
outputStream.flush();
}
//3.关闭资源
outputStream.close();
fileInputStream.close();
} catch (Exception exception) {
exception.printStackTrace();
}
}
2、新增菜品
新增的菜品会同步在移动端展示:
- 2.1 处理逻辑
1.页面发送ajax请求回显数据;
2.页面发送请求进行图片的上传;
3.页面发送请求进行图片的下载;
4.点击保存按钮,发送ajax请求,将菜品的数据已json形式提交到服务端。
Dish表中不存在口味信息,因此在数据操作时,新定义dishDto类,可以封装所有json数据提交到前端页面
- 2.2 逻辑代码
新增菜品设计到多张表的操作,操作过程较为复杂:
/**
* 新增菜品同时保存口味数据
* @param dishDto
*/
@Override
@Transactional //对多张表操作需要开启事务,同时需要再启动类加注解配置
public void saveWithFlavor(DishDto dishDto) {
//保存菜品的基本信息到dish表
this.save(dishDto);
Long dishId = dishDto.getId(); //菜品id
//获得菜品口味的集合
List<DishFlavor> flavors = dishDto.getFlavors();
//stream流的方式遍历集合,lambda表达式,为集合中的每个flavors赋值菜品id
flavors=flavors.stream().map((item) ->{
item.setDishId(dishId);
return item;
}).collect(Collectors.toList()); //相当于对集合做处理后又重新转为list集合
//保存菜品的口味表到菜品口味表dish_flavor
dishFlavorService.saveBatch(flavors); //saveBatch保存集合数据
}
3、菜品信息分页查询
- 3.1 处理逻辑
1.先查询菜品的基本信息 --dish表
2.创建一个Dto拷贝普通的属性
3.查询当期菜品所对应的口味信息 --dish_flavor表
4.最后为dto设置flavors属性 - 3.2 逻辑代码
/**
* 根据id查询菜品信息和口味信息
* @param id
* @return
*/
@Override
public DishDto getByIdWithFlavor(Long id) {
//1.先查询菜品的基本信息 --dish表
Dish dish = this.getById(id);
//创建一个Dto拷贝普通的属性
DishDto dishDto =new DishDto();
BeanUtils.copyProperties(dish,dishDto);
//2.查询当期菜品所对应的口味信息 --dish_flavor表
//条件构造器
LambdaQueryWrapper<DishFlavor> queryWrapper =new LambdaQueryWrapper<>();
//构造条件
queryWrapper.eq(DishFlavor::getDishId,dish.getId());
List<DishFlavor> flavors = dishFlavorService.list(queryWrapper);
//最后为dto设置flavors属性
dishDto.setFlavors(flavors);
return dishDto;
}
4、修改菜品
- 4.1 处理逻辑
1、页面发送ajax请求,请求服务端获取分类数据,用于菜品分类下拉框中数据展示
2、页面发送ajax请求,请求服务端,根据id查询当前菜品信息,用于菜品信息回显
3、页面发送请求,请求服务端进行图片下载,用于页图片回显
4、点击保存按钮,页面发送ajax请求,将修改后的菜品相关数据以json形式提交到服务端 - 4.2 逻辑代码
/**
* 更新菜品信息,同时更新对应的口味信息
* @param id
* @return
*/
@Override
@Transactional
public void updateWithFlavor(DishDto dishDto) {
//1.更新菜品dish信息
this.updateById(dishDto);
//2.更新dish_flavors口味信息
//先删除当前菜品对应的口味信息---delete
LambdaQueryWrapper<DishFlavor> queryWrapper =new LambdaQueryWrapper();
queryWrapper.eq(DishFlavor::getDishId,dishDto.getId());
dishFlavorService.remove(queryWrapper);
//再添加当前提交的口味信息---insert
List<DishFlavor> flavors = dishDto.getFlavors(); //获取当前表单中的口味信息
flavors =flavors.stream().map((item) ->{
item.setDishId(dishDto.getId());
return item;
}).collect(Collectors.toList());
//将集合数据存入数据库
dishFlavorService.saveBatch(flavors);
}
六、套餐管理功能实现
1、新增套餐
新增套餐,其实就是将新增页面录入的套餐信息插入到setmeal表,还需要向setmeal_dish表插入套餐和菜品关联数据。所以在新增套餐时,涉及到两个表:
-
setmeal 套餐表
-
setmeal_dish 套餐菜品关系表
-
处理逻辑
1、页面(backend/page/combo/add.html)发送ajax请求,请求服务端获取套餐分类数据并展示到下拉框中
2、页面发送ajax请求,请求服务端获取菜品分类数据并展示到添加菜品窗口中
3、页面发送ajax请求,请求服务端,根据菜品分类查询对应的菜品数据并展示到添加菜品窗口中
4、页面发送请求进行图片上传,请求服务端将图片保存到服务器
5、页面发送请求进行图片下载,将上传的图片进行回显
6、点击保存按钮,发送ajax请求,将套餐相关数据以json形式提交到服务端 -
代码实现
/**
* 新增套餐,同时保存套餐和菜品的关联关系
* @param setmealDto
*/
@Override
@Transactional
public void saveWithDish(SetmealDto setmealDto) {
//保存套餐的基本信息,操作Setmeal,执行insert操作
this.save(setmealDto);
final List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
//为集合上的每个对象赋予setmealId
setmealDishes.stream().map((item) ->{
item.setSetmealId(setmealDto.getId());
return item;
}).collect(Collectors.toList());
//保存套餐和菜品的关联关系,操作Setmeal_dish,执行insert操作
setmealDishService.saveBatch(setmealDishes);
}
2、删除套餐
- 2.1 处理逻辑
- 删除单个套餐时,页面发送ajax请求,根据套餐id删除对应套餐
- 删除多个套餐时,页面发送ajax请求,根据提交的多个套餐id删除对应套餐
- 2.2 代码实现
/**
* 删除套餐,同时删除套餐和菜品的关联数据
* @param ids
*/
@Override
@Transactional
public void removeWithDish(List<Long> ids) {
//查询套餐的状态,仅起售状态的可以删除
//查询条件 select count(*) from setmetal where id in(1,2,3) and status=1
LambdaQueryWrapper<Setmeal> queryWrapper =new LambdaQueryWrapper<>();
//构造查询条件
queryWrapper.in(Setmeal::getId,ids);
queryWrapper.eq(Setmeal::getStatus,1);
//查询
int count = this.count(queryWrapper);
//如果不能删除,抛出业务异常
if (count>0){
throw new CustomException("套餐正在售卖中,不能删除!");
}
//可以删除,先批量删除套餐表中的数据--- setmeal
this.removeByIds(ids);
//删除关系表中的数据--- setmeal_dish
// delete from setmetal_dish where setmeal_id in ();
//构造查询条件
LambdaQueryWrapper<SetmealDish> dishQueryWrapper =new LambdaQueryWrapper<>();
dishQueryWrapper.in(SetmealDish::getSetmealId,ids);
setmealDishService.remove(dishQueryWrapper);
}
3、修改套餐的售卖状态
-
3.1 处理逻辑
1.根据前端传入的id查询套餐集合;
2.查询条件按照套餐价格降序;
3.遍历套餐集合,更新集合中各个套餐的状态; -
3.2 代码实现
/**
* 修改套餐的售卖状态
*
* @return
*/
@PostMapping("/status/{status}")
public R<String> statusWithIds(@PathVariable("status") Integer status, @RequestParam List<Long> ids) {
//构造一个条件构造器
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(ids != null, Setmeal::getId, ids);
queryWrapper.orderByDesc(Setmeal::getPrice);
//根据条件进行批量查询
List<Setmeal> list = setmealService.list(queryWrapper);
for (Setmeal setmeal : list) {
if (list != null) {
//把浏览器传入的status参数复制给套餐
setmeal.setStatus(status);
setmealService.updateById(setmeal);
}
}
return R.success("售卖状态修改成功");
}
4、移动端套餐查询
-
处理逻辑
1.根据套餐id查询套餐;
2.添加查询条件:套餐未被禁用状态下,按照更新时间的降序排列;
3.返回查询到的套餐列表; -
代码实现
/**
* 移动端
* 套餐查询
* @param setmeal
* @return
*/
@GetMapping("/list")
public R<List<Setmeal>> list(Setmeal setmeal){
//构造查询条件
LambdaQueryWrapper<Setmeal> queryWrapper =new LambdaQueryWrapper<>();
//菜品id
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);
}
七、移动端功能实现
1、用户登录
- 1.1 处理逻辑
1、在登录页面(front/page/login.html)输入手机号,点击【获取验证码】按钮,页面发送aiax请求,在服务端调用短信服务API给指定手机号发送验证码短信
2、在登录页面输入验证码,点击【登录】按钮,发送ajax请求,在服务端处理登录请求
3、在过滤器中添加登录的处理请求
4、从session中获取验证码进行比对
5、根据手机号从数据库查询用户信息,如果用户不存在,就直接注册新用户
6、登录成功,将用户信息存入session - 1.2 代码实现
/**
* 移动端登录
* @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 codeInSession = session.getAttribute(phone);
//页面提交的验证码和session中保存的验证码比对
if (codeInSession !=null && codeInSession.equals(code)){
//比对成功,说明登录成功
LambdaQueryWrapper<User> queryWrapper =new LambdaQueryWrapper<>();
//查询数据库中的手机号
queryWrapper.eq(User::getPhone,phone);
User user = userService.getOne(queryWrapper);
if (user ==null){
//判断当前手机号是否为新用户,如果是新用户就自动完成注册
user =new User();
user.setPhone(phone);
user.setStatus(1);
//完成注册
userService.save(user);
}
//登录成功后,将userID存入session中
session.setAttribute("user",user.getId());
return R.success(user);
}
return R.error("登录失败");
}
2、菜品展示
- 逻辑功能
1、页面(front/index.html)发送ajax请求,获取分类数据(菜品分类和套餐分类)
2、页面发送ajax请求,获取第一个分类下的菜品或者套餐
开发菜品展示功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。 - 代码实现
/**
* 通过id查询套餐信息, 同时还要查询关联表setmeal_dish的菜品信息进行回显
* @param id 待查询的id
*/
@Override
public SetmealDto getByIdWithDish(Long id) {
// 根据id查询setmeal表中的基本信息
Setmeal setmeal = this.getById(id);
SetmealDto setmealDto = new SetmealDto();
// 对象拷贝。
BeanUtils.copyProperties(setmeal, setmealDto);
// 查询关联表setmeal_dish的菜品信息
LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(SetmealDish::getSetmealId, id);
List<SetmealDish> setmealDishList = setmealDishService.list(queryWrapper);
//设置套餐菜品属性
setmealDto.setSetmealDishes(setmealDishList);
return setmealDto;
}
3、添加购物车
- 处理逻辑
1、点击加入购物车或者按钮,页面发送ajax请求,请求服务端,将菜品或者套餐添加到购物车
2、点击购物车图标,页面发送ajax请求,请求服务端查询购物车中的菜品和套餐
3、点击清空购物车按钮,页面发送ajax请求,请求服务端来执行清空购物车操作
- 代码实现
/**
* 添加购物车
* @param shoppingCart
* @return
*/
@PostMapping("/add")
public R<ShoppingCart> add(@RequestBody ShoppingCart shoppingCart){
log.info("购物车数据:{}",shoppingCart);
//1.设置用户id,指定是哪个用户的购物车
Long currentId = BaseContext.getCurrentId();
shoppingCart.setUserId(currentId);
//2.查询当前菜品或套餐是否已经存在于购物车中
final Long dishId = shoppingCart.getDishId();
LambdaQueryWrapper<ShoppingCart> queryWrapper =new LambdaQueryWrapper<>();
//根据用户id查询
queryWrapper.eq(ShoppingCart::getUserId,currentId);
if (dishId != null){
//添加菜品信息到购物车
queryWrapper.eq(ShoppingCart::getDishId,dishId);
}else {
//添加套餐到购物车
queryWrapper.eq(ShoppingCart::getSetmealId,shoppingCart.getSetmealId());
}
//SQL:select * from shopping_cart where user_id =? and dish_id/setmeal_id =?
ShoppingCart cartOne = shoppingCartService.getOne(queryWrapper);
if (cartOne != null){
log.info("cartOne ==null!");
//已经存在,就在原来的基础上+1
Integer number = cartOne.getNumber(); //原来的数量
cartOne.setNumber(number+1);
shoppingCartService.updateById(cartOne); //更新操作
}else {
//不存在,添加到购物车,数量默认1
shoppingCart.setNumber(1);
shoppingCart.setCreateTime(LocalDateTime.now());
shoppingCartService.save(shoppingCart);
//重新把shoppingCart赋给cartOne
cartOne =shoppingCart;
}
return R.success(cartOne);
}
4、用户下单
- 处理逻辑
1、在购物车中点击去结算按钮,页面跳转到订单确认页面
2、在订单确认页面,发送ajax请求,请求服务端获取当前登录用户的默认地址
3、在订单确认页面,发送ajax请求,请求服务端获取当前登录用户的购物车数据
4、在订单确认页面点击支付按钮,发送ajax请求,请求服务端完成下单操作
- 代码实现
/**
* 用户下单
* @param orders
*/
@Override
@Transactional
public void submit(Orders orders) {
//先获得用户id
final Long currentId = BaseContext.getCurrentId();
//查询当前用户的购物车数据
LambdaQueryWrapper<ShoppingCart> queryWrapper =new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId,currentId);
//查询到购物车数据
final List<ShoppingCart> shoppingCarts = shoppingCartService.list(queryWrapper);
//对购物车数据进行判断
if (shoppingCarts ==null || shoppingCarts.size()==0){
throw new CustomException("购物车为空,不能下单");
}
//查询用户信息
final User user = userService.getById(currentId);
Long addressBookId = orders.getAddressBookId();
AddressBook addressBook = addressBookService.getById(addressBookId);
if (addressBook ==null){
throw new CustomException("地址信息为空,不能下单");
}
//向订单表中添加数据
long orderId = IdWorker.getId(); //mp提供的方法生成订单号
//原子操作
AtomicInteger amount =new AtomicInteger(0);
List<OrderDetail> orderDetails = shoppingCarts.stream().map((item)->{
OrderDetail orderDetail = new OrderDetail();
orderDetail.setOrderId(orderId);
orderDetail.setNumber(item.getNumber());
orderDetail.setDishFlavor(item.getDishFlavor());
orderDetail.setDishId(item.getDishId());
orderDetail.setSetmealId(item.getSetmealId());
orderDetail.setName(item.getName());
orderDetail.setImage(item.getImage());
orderDetail.setAmount(item.getAmount());
amount.addAndGet(item.getAmount().multiply(new BigDecimal(item.getNumber())).intValue());
return orderDetail;
}).collect(Collectors.toList());
//设置订单实体的其他属性
orders.setId(orderId);
orders.setOrderTime(LocalDateTime.now());
orders.setCheckoutTime(LocalDateTime.now());
orders.setStatus(2); //待派送
orders.setAmount(new BigDecimal(amount.get())); //总金额
orders.setUserId(currentId); //用户id
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);
}
6、后台查询订单明细
- 处理逻辑
1、根据分页信息查询订单;
2、添加查询条件,根据更新时间降序排列;
3、对于查询到的订单信息,进行信息处理用于前端页面展示; - 代码实现
/**
* 后台查询订单明细
* @param page
* @param pageSize
* @param number
* @param beginTime
* @param endTime
* @return
*/
@Override
public Page<OrdersDto> empPage(int page, int pageSize, String number, String beginTime, String endTime) {
Page<Orders> pageInfo = new Page<>(page, pageSize);
Page<OrdersDto> pageDto = new Page<>();
//创建条件构造器
LambdaQueryWrapper<Orders> queryWrapper = new LambdaQueryWrapper<>();
//添加条件,根据number进行like模糊查询
queryWrapper.like(number != null, Orders::getNumber, number);
queryWrapper.gt(StringUtils.isNotEmpty(beginTime), Orders::getOrderTime, beginTime);
queryWrapper.lt(StringUtils.isNotEmpty(endTime), Orders::getOrderTime, endTime);
//添加排序条件(根据更新时间降序排序)
queryWrapper.orderByDesc(Orders::getOrderTime);
this.page(pageInfo, queryWrapper);
//将其除了records中的内存复制到pageDto中
BeanUtils.copyProperties(pageInfo, pageDto, "records");
List<Orders> records = pageInfo.getRecords();
List<OrdersDto> collect = records.stream().map((item) -> {
OrdersDto ordersDto = new OrdersDto();
//对象拷贝
BeanUtils.copyProperties(item, ordersDto);
LambdaQueryWrapper<OrderDetail> lambdaQueryWrapper = new LambdaQueryWrapper<>();
//根据订单id查询订单详细信息
lambdaQueryWrapper.eq(OrderDetail::getOrderId, item.getId());
List<OrderDetail> orderDetails = orderDetailService.list(lambdaQueryWrapper);
ordersDto.setOrderDetails(orderDetails);
//根据userId查询用户姓名
Long userID = item.getUserId();
User user = userService.getById(userID);
ordersDto.setUserName(user.getName());
ordersDto.setPhone(user.getPhone());
//获取地址信息
Long addressBookId = item.getAddressBookId();
AddressBook addressBook = addressBookService.getById(addressBookId);
ordersDto.setAddress(addressBook.getDetail());
ordersDto.setConsignee(addressBook.getConsignee());
return ordersDto;
}).collect(Collectors.toList());
pageDto.setRecords(collect);
return pageDto;
}