瑞吉外卖项目剩余功能补充

Posted 普通网友

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了瑞吉外卖项目剩余功能补充相关的知识,希望对你有一定的参考价值。

目录

菜品启售和停售

菜品批量启售和批量停售

菜品的批量删除

菜品删除逻辑优化

套餐管理的启售,停售

套餐管理的修改

后台按条件查看和展示客户订单

手机端减少购物车中的菜品或者套餐数量(前端展示有一点小问题)

用户查看自己订单

移动端的再来一单功能

移动端点击套餐图片查看套餐具体菜品

删除地址

修改地址

后台订单状态的修改

移动端登陆退出功能


这个是自己基于学习B站 黑马瑞吉外卖项目,补充一些视频里面没有定义的功能或者是需要自己实现的功能;仅供学习参考,本人可能代码不太规范,但是功能自己测试是没有问题的;

黑马程序员Java项目实战《瑞吉外卖》,轻松掌握springboot + mybatis plus开发核心技术的真java实战项目_哔哩哔哩_bilibili[这里是图片001]https://www.bilibili.com/video/BV13a411q753?spm_id_from=333.337.search-card.all.click

菜品启售和停售

前端发过来的请求(使用的是post方式):http://localhost:8080/dish/status/1?ids=1516568538387079169

后端接受的请求:

@PostMapping("/status/status")
public R<String> status(@PathVariable("status") Integer status,Long ids)
    log.info("status:",status);
    log.info("ids:",ids);
    return null;

先看看后端能不能接收到前端传过来的数据:

发现可以接收到前端参数后,开始补全controller层代码:在DishController中添加下面的接口代码;

/**
 * 对菜品进行停售或者是起售
 * @return
 */
@PostMapping("/status/status")
public R<String> status(@PathVariable("status") Integer status,Long ids)
    log.info("status:",status);
    log.info("ids:",ids);
    Dish dish = dishService.getById(ids);
    if (dish != null)
        dish.setStatus(status);
        dishService.updateById(dish);
        return R.success("开始启售");
    
    return R.error("售卖状态设置异常");

菜品批量启售和批量停售

把上面对单个菜品的售卖状态的方法进行修改;

/**
 * 对菜品批量或者是单个 进行停售或者是起售
 * @return
 */
@PostMapping("/status/status")
//这个参数这里一定记得加注解才能获取到参数,否则这里非常容易出问题
public R<String> status(@PathVariable("status") Integer status,@RequestParam List<Long> ids)
    //log.info("status:",status);
    //log.info("ids:",ids);
    LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper();
    queryWrapper.in(ids !=null,Dish::getId,ids);
    //根据数据进行批量查询
    List<Dish> list = dishService.list(queryWrapper);

    for (Dish dish : list) 
        if (dish != null)
            dish.setStatus(status);
            dishService.updateById(dish);
        
    
    return R.success("售卖状态修改成功");

**注意:controller层的代码是不可以直接写业务的,建议把它抽离到service层,controller调用一下service的方法就行;**下面的批量删除功能是抽离的,controller没有写业务代码;

菜品的批量删除

前端发来的请求:

在DishController中添加接口:

在DishFlavor实体类中,在private Integer isDeleted;字段上加上@TableLogic注解,表示删除是逻辑删除,由mybatis-plus提供的;

/**
 * 套餐批量删除和单个删除
 * @return
 */
@DeleteMapping
public R<String> delete(@RequestParam("ids") List<Long> ids)
        //删除菜品  这里的删除是逻辑删除
        dishService.deleteByIds(ids);
        //删除菜品对应的口味  也是逻辑删除
        LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.in(DishFlavor::getDishId,ids);
        dishFlavorService.remove(queryWrapper);
        return R.success("菜品删除成功");

DishServicez中添加相关的方法:

//根据传过来的id批量或者是单个的删除菜品
void deleteByIds(List<Long> ids);

在实现类实现该方法:

/**
 *套餐批量删除和单个删除
 * @param ids
 */
@Override
@Transactional
public void deleteByIds(List<Long> ids) 

    //构造条件查询器
    LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
    //先查询该菜品是否在售卖,如果是则抛出业务异常
    queryWrapper.in(ids!=null,Dish::getId,ids);
    List<Dish> list = this.list(queryWrapper);
    for (Dish dish : list) 
        Integer status = dish.getStatus();
        //如果不是在售卖,则可以删除
        if (status == 0)
            this.removeById(dish.getId());
        else 
            //此时应该回滚,因为可能前面的删除了,但是后面的是正在售卖
            throw new CustomException("删除菜品中有正在售卖菜品,无法全部删除");
        
    


功能测试:单个删除,批量删除,批量删除中有启售的…

测试成功!

菜品删除逻辑优化

上面写的菜品的删除功能有点小简单,下面完善一下相关的逻辑;

相关的service的注入,这里就不列举出来了,代码中使用了哪个service,你就autowire就行;

下面的代码可能会有点冗余,这里我就不进行抽离了;

    /**
     * 菜品批量删除和单个删除
     * 1.判断要删除的菜品在不在售卖的套餐中,如果在那不能删除
     * 2.要先判断要删除的菜品是否在售卖,如果在售卖也不能删除
     * @return
     */

    //遇到一个小问题,添加菜品后,然后再添加套餐,但是套餐可选择添加的菜品选项是没有刚刚添加的菜品的?
    //原因:redis存储的数据没有过期,不知道为什么redis没有重新刷新缓存
    // (与DishController中的@GetMapping("/list")中的缓存设置有关,目前不知道咋配置刷新缓存。。。。。
    // 解决方案,把redis中的数据手动的重新加载一遍,或者是等待缓存过期后再添加相关的套餐,或者改造成使用spring catch
    @DeleteMapping
    public R<String> delete(@RequestParam("ids") List<Long> ids)
        //根据菜品id在stemeal_dish表中查出哪些套餐包含该菜品
        LambdaQueryWrapper<SetmealDish> setmealDishLambdaQueryWrapper = new LambdaQueryWrapper<>();
        setmealDishLambdaQueryWrapper.in(SetmealDish::getDishId,ids);
        List<SetmealDish> SetmealDishList = setmealDishService.list(setmealDishLambdaQueryWrapper);
        //如果菜品没有关联套餐,直接删除就行  其实下面这个逻辑可以抽离出来,这里我就不抽离了
        if (SetmealDishList.size() == 0)
            //这个deleteByIds中已经做了菜品起售不能删除的判断力
            dishService.deleteByIds(ids);
            LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.in(DishFlavor::getDishId,ids);
            dishFlavorService.remove(queryWrapper);
            return R.success("菜品删除成功");
        

        //如果菜品有关联套餐,并且该套餐正在售卖,那么不能删除
        //得到与删除菜品关联的套餐id
        ArrayList<Long> Setmeal_idList = new ArrayList<>();
        for (SetmealDish setmealDish : SetmealDishList) 
            Long setmealId = setmealDish.getSetmealId();
            Setmeal_idList.add(setmealId);
        
        //查询出与删除菜品相关联的套餐
        LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
        setmealLambdaQueryWrapper.in(Setmeal::getId,Setmeal_idList);
        List<Setmeal> setmealList = setmealService.list(setmealLambdaQueryWrapper);
        //对拿到的所有套餐进行遍历,然后拿到套餐的售卖状态,如果有套餐正在售卖那么删除失败
        for (Setmeal setmeal : setmealList) 
            Integer status = setmeal.getStatus();
            if (status == 1)
                return R.error("删除的菜品中有关联在售套餐,删除失败!");
            
        
        
        //要删除的菜品关联的套餐没有在售,可以删除
        //这下面的代码并不一定会执行,因为如果前面的for循环中出现status == 1,那么下面的代码就不会再执行
        dishService.deleteByIds(ids);
        LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.in(DishFlavor::getDishId,ids);
        dishFlavorService.remove(queryWrapper);
        return R.success("菜品删除成功");
    

套餐管理的启售,停售

前端发来的请求:

根据前面菜品模块自己实现的功能,我们可以知道,我们只需要写一个批量处理的方法就可以完成单个或者是批量套餐的启售,停售;

SetmealController中的controller层代码:

/**
 * 对菜品批量或者是单个 进行停售或者是起售
 * @return
 */
@PostMapping("/status/status")
//这个参数这里一定记得加注解才能获取到参数,否则这里非常容易出问题
public R<String> status(@PathVariable("status") Integer status,@RequestParam List<Long> ids)
    setmealService.updateSetmealStatusById(status,ids);
    return R.success("售卖状态修改成功");

SetmealService中添加下面方法:

/**
 * 根据套餐id修改售卖状态
 * @param status
 * @param ids
 */
void updateSetmealStatusById(Integer status,List<Long> ids);

该方法的实现:

/**
 * 根据套餐id修改售卖状态
 * @param status
 * @param ids
 */
@Override
public void updateSetmealStatusById(Integer status,  List<Long> ids) 
    LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper();
    queryWrapper.in(ids !=null,Setmeal::getId,ids);
    List<Setmeal> list = this.list(queryWrapper);

    for (Setmeal setmeal : list) 
        if (setmeal != null)
            setmeal.setStatus(status);
            this.updateById(setmeal);
        
    

记得功能测试,我自己测试的时候是没有问题的;

套餐管理的修改

分为两步:数据回显示,和提交修改数据到数据库

前端点击套餐修改,前端发过来的请求:

请求方式是:get

携带的参数是:stemealId

然后我们发现在弹出编辑窗口是没有数据的:并且报了一个404,那就是说是**在数据回显的时候报错了**,没有找到具体的回显接口来处理这个请求;

SetmealController 中添加下面的代码:

/**
 * 回显套餐数据:根据套餐id查询套餐
 * @return
 */
@GetMapping("/id")
public R<SetmealDto> getData(@PathVariable Long id)
    SetmealDto setmealDto = setmealService.getDate(id);

    return R.success(setmealDto);

SetmealService添加下面的代码:

/**
 * 回显套餐数据:根据套餐id查询套餐
 * @return
 */
SetmealDto getDate(Long id);

该方法的实现:

/**
 * 回显套餐数据:根据套餐id查询套餐
 * @return
 */
@Override
public SetmealDto getDate(Long id) 
    Setmeal setmeal = this.getById(id);
    SetmealDto setmealDto = new SetmealDto();
    LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper();
    //在关联表中查询,setmealdish
    queryWrapper.eq(id!=null,SetmealDish::getSetmealId,id);

    if (setmeal != null)
        BeanUtils.copyProperties(setmeal,setmealDto);
        List<SetmealDish> list = setmealDishService.list(queryWrapper);
        setmealDto.setSetmealDishes(list);
        return setmealDto;
    
    return null;

测试:数据回显成功:

但是这样我们再点击添加菜品会发现,右边只展示菜品的价格并没有展示菜品对应的名称:

已选菜品中的菜品并没有展示对应的菜品名;

修改后的运行情况展示:(个人感觉这个菜品搜索框没啥用。。。。反正我是搜索不出来。。)

修改具体的前端代码:把backend/combo/add.html中的335行修改为下面的代码;

因为这里的item是代表dish对象,dish实体类是使用name作为菜品名称的;

修改完成后,点击保存,我们发现前端发出一个put请求:

携带的参数为:

根据前端传过来的数据和需要的返回值,我们就可以知道controller层方法的返回值和用什么参数来接收前端传给我们的数据;注意这个套餐里面的菜品也要保存修改:需要把setealDish保存到seteal_dish表中;

点击修改后的保存,后端会接收到下面的数据:发现setmealId == null,所以这里需要自己单独填充;

controller层代码:

为了不把问题复杂化,我是先把相关的setmealDish内容移除然后再重新添加,这样就可以不用考虑dish重复的问题和哪些修改哪些没修改;

@PutMapping
public R<String> edit(@RequestBody SetmealDto setmealDto)

    if (setmealDto==null)
            return R.error("请求异常");
        

        if (setmealDto.getSetmealDishes()==null)
            return R.error("套餐没有菜品,请添加套餐");
        
        List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
        Long setmealId = setmealDto.getId();

        LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(SetmealDish::getSetmealId,setmealId);
        setmealDishService.remove(queryWrapper);

        //为setmeal_dish表填充相关的属性
        for (SetmealDish setmealDish : setmealDishes) 
            setmealDish.setSetmealId(setmealId);
        
        //批量把setmealDish保存到setmeal_dish表
        setmealDishService.saveBatch(setmealDishes);
        setmealService.updateById(setmealDto);

        return R.success("套餐修改成功");

后台按条件查看和展示客户订单

点击订单明细,前端会发下面的请求:携带的数据是分页使查询用的;

先写个controller看能不能接收到前端传过来的参数:发现只要参数和前端传过来的参数名对应就可以拿到参数的

主要使用到mybatis-plus动态sql语句的生成:

这里我就直接把功能直接写在controller层了,看自己需求分层;(本人这里偷个懒)

    /**
     * 后台查询订单明细
     * @param page
     * @param pageSize
     * @param number
     * @param beginTime
     * @param endTime
     * @return
     */
    @GetMapping("/page")
    public R<Page> page(int page, int pageSize, String number,String beginTime,String endTime)
        //分页构造器对象
        Page<Orders> pageInfo = new Page<>(page,pageSize);
        //构造条件查询对象
        LambdaQueryWrapper<Orders> queryWrapper = new LambdaQueryWrapper<>();

        //添加查询条件  动态sql  字符串使用StringUtils.isNotEmpty这个方法来判断
        //这里使用了范围查询的动态SQL,这里是重点!!!
        queryWrapper.like(number!=null,Orders::getNumber,number)
                .gt(StringUtils.isNotEmpty(beginTime),Orders::getOrderTime,beginTime)
                .lt(StringUtils.isNotEmpty(endTime),Orders::getOrderTime,endTime);

        orderService.page(pageInfo,queryWrapper);
        return R.success(pageInfo);
    

测试:

但是如果你想要这个username显示用户名的话,那么有两种办法:

方法1:就是在注册的user表中添加用户名;(实际上这个用户在注册的时候是没有填写username这个选项的,所以这里查询出来全是null,所以前端就展示不出来用户)

方法二:(推荐使用)

因为我们不可能老是自己去数据库修改具体的值,所以这里我们使用用户下单的****consignee来显示,数据库中也有,但是数据库中的consignee是可以为null的,所以在后台代码中帮订单添加该属性的时候要判断是否null!然后就是去修改前端代码就行:

把72行的userName改成consignee就行;

测试效果:

手机端减少购物车中的菜品或者套餐数量(前端展示有一点小问题)

前端请求: http://localhost:8080/shoppingCart/sub

请求方式:post

携带参数可能是dish_id 也可能是 setmealId,所以我们需要实体类shoppingCart来接收;

遇到的bug: 就是购物车里面的菜品和套餐的数量可能会减少至负数!!!所以这里要判断和需要前端处理;

而且不知道为什么。。。。上面的数量已经为0了,但是下面的加减还是可以变话的就导致了数据库中的数据可以为负数。。。前端的问题,,,暂时使用一个简单的做法解决。。。把数据库的该字段设置为无符号字段,所以当num数小于0的时候就会报错(500接口异常),但是左下角的小购物车还是会显示菜品为0

在ShoppingCartController中添加下面的接口方法来接收请求:

    /**
     * 客户端的套餐或者是菜品数量减少设置
     * 没必要设置返回值
     * @param shoppingCart
     */
    @PostMapping("/sub")
    @Transactional
    public R<ShoppingCart> sub(@RequestBody ShoppingCart shoppingCart)

        Long dishId = shoppingCart.getDishId();
        LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
        //代表数量减少的是菜品数量
        if (dishId != null)
            //通过dishId查出购物车对象
            queryWrapper.eq(ShoppingCart::getDishId,dishId);
            ShoppingCart cart1 = shoppingCartService.getOne(queryWrapper);
            cart1.setNumber(cart1.getNumber()-1);
            //对数据进行更新操作
            shoppingCartService.updateById(cart1);
            return R.success(cart1);
        
        Long setmealId = shoppingCart.getSetmealId();
        if (setmealId != null)
            //代表是套餐数量减少
            queryWrapper.eq(ShoppingCart::getSetmealId,setmealId);
            ShoppingCart cart2 = shoppingCartService.getOne(queryWrapper);
            cart2.setNumber(cart2.getNumber()-1);
            //对数据进行更新操作
            shoppingCartService.updateById(cart2);
            return R.success(cart2);
        

            return R.error("操作异常");
    

解决前端展示的bug:对上面的代码进行修改(这是评论区一个老哥提供的思路)

   /**
     * 客户端的套餐或者是菜品数量减少设置
     * 没必要设置返回值
     * @param shoppingCart
     */
        @PostMapping("/sub")
    @Transactional
    public R<ShoppingCart> sub(@RequestBody ShoppingCart shoppingCart)

        Long dishId = shoppingCart.getDishId();
        LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
        
        //代表数量减少的是菜品数量
        if (dishId != null)
            //通过dishId查出购物车对象
            queryWrapper.eq(ShoppingCart::getDishId,dishId);
            //这里必须要加两个条件,否则会出现用户互相修改对方与自己购物车中相同套餐或者是菜品的数量
            queryWrapper.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());
            ShoppingCart cart1 = shoppingCartService.getOne(queryWrapper);
            cart1.setNumber(cart1.getNumber()-1);
            Integer LatestNumber = cart1.getNumber();
            if (LatestNumber > 0)
                //对数据进行更新操作
                shoppingCartService.updateById(cart1);
            else if(LatestNumber == 0)
                //如果购物车的菜品数量减为0,那么就把菜品从购物车删除
                shoppingCartService.removeById(cart1.getId());
            else if (LatestNumber < 0)
                return R.error("操作异常");
            

            return R.success(cart1);
        

        Long setmealId = shoppingCart.getSetmealId();
        if (setmealId != null)
            //代表是套餐数量减少
            queryWrapper.eq(ShoppingCart::getSetmealId,setmealId).eq(ShoppingCart::getUserId,BaseContext.getCurrentId());
            ShoppingCart cart2 = shoppingCartService.getOne(queryWrapper);
            cart2.setNumber(cart2.getNumber()-1);
            Integer LatestNumber = cart2.getNumber();
            if (LatestNumber > 0)
                //对数据进行更新操作
                shoppingCartService.updateById(cart2);
            else if(LatestNumber == 0)
                //如果购物车的套餐数量减为0,那么就把套餐从购物车删除
                shoppingCartService.removeById(cart2.getId());
            else if (LatestNumber < 0)
                return R.error("操作异常");
            
            return R.success(cart2);
        
            //如果两个大if判断都进不去
            return R.error("操作异常");
    

用户查看自己订单

在OrderController中添加下面的方法;

/**
 * 用户订单分页查询
 * @param page
 * @param pageSize
 * @return
 */
@GetMapping("/userPage")
public R<Page> page(int page, int pageSize)

    //分页构造器对象
    Page<Orders> pageInfo = new Page<>(page,pageSize);
    //构造条件查询对象
    LambdaQueryWrapper<Orders> queryWrapper = new LambdaQueryWrapper<>();

    //添加排序条件,根据更新时间降序排列
    queryWrapper.orderByDesc(Orders::getOrderTime);
    orderService.page(pageInfo,queryWrapper);

    return R.success(pageInfo);

其实这里还没有完善!!!下面继续完善代码;

通过order.html这个页面我们可以发现:前端还需要下面这些数据;所以我们后端要传给它。。。

分析前端代码: 这个item是从order.orderDetails里面 获取到的,但是orders实体类里面并没有orderDetails这个属性,而且数据库中这个order表里面也没有这个字段,所以这里我使用的是dto来封装数据给前端,这就需要使用到dto对象的分页查询了,,,,,而且离谱的是前端就是传了一个分页页面大小的数据,,,,所以我们只能从本地线程中获取用户id开始,,一路查询数据。。。。。

创建OrdersDto实体类:

package com.itheima.reggie.dto;

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.itheima.reggie.entity.OrderDetail;
import com.itheima.reggie.entity.Orders;
import lombok.Data;
import java.util.List;

/**
 * @author LJM
 * @create 2022/5/3
 */
@Data
public class OrderDto extends Orders  

    private List<OrderDetail> orderDetails;

代码:这里面的代码我踩了很多坑才写出来的,看到这里的小伙伴,希望给个赞,码字不易_谢谢

不建议大家把业务写在controller,我是为了方便才写这里的,在企业的实际开发中千万不要这么干!!!请勿效仿!

    //抽离的一个方法,通过订单id查询订单明细,得到一个订单明细的集合
    //这里抽离出来是为了避免在stream中遍历的时候直接使用构造条件来查询导致eq叠加,从而导致后面查询的数据都是null
    public List<OrderDetail> getOrderDetailListByOrderId(Long orderId)
        LambdaQueryWrapper<OrderDetail> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(OrderDetail::getOrderId, orderId);
        List<OrderDetail> orderDetailList = orderDetailService.list(queryWrapper);
        return orderDetailList;
    

    /**
     * 用户端展示自己的订单分页查询
     * @param page
     * @param pageSize
     * @return
     * 遇到的坑:原来分页对象中的records集合存储的对象是分页泛型中的对象,里面有分页泛型对象的数据
     * 开始的时候我以为前端只传过来了分页数据,其他所有的数据都要从本地线程存储的用户id开始查询,
     * 结果就出现了一个用户id查询到 n个订单对象,然后又使用 n个订单对象又去查询 m 个订单明细对象,
     * 结果就出现了评论区老哥出现的bug(嵌套显示数据....)
     * 正确方法:直接从分页对象中获取订单id就行,问题大大简化了......
     */
    @GetMapping("/userPage")
    public R<Page> page(int page, int pageSize)
        //分页构造器对象
        Page<Orders> pageInfo = new Page<>(page,pageSize);
        Page<OrderDto> pageDto = new Page<>(page,pageSize);
        //构造条件查询对象
        LambdaQueryWrapper<Orders> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Orders::getUserId,BaseContext.getCurrentId());
        //这里是直接把当前用户分页的全部结果查询出来,要添加用户id作为查询条件,否则会出现用户可以查询到其他用户的订单情况
        //添加排序条件,根据更新时间降序排列
        queryWrapper.orderByDesc(Orders::getOrderTime);
        orderService.page(pageInfo,queryWrapper);

        //通过OrderId查询对应的OrderDetail
        LambdaQueryWrapper<OrderDetail> queryWrapper2 = new LambdaQueryWrapper<>();

        //对OrderDto进行需要的属性赋值
        List<Orders> records = pageInfo.getRecords();
        List<OrderDto> orderDtoList = records.stream().map((item) ->
            OrderDto orderDto = new OrderDto();
            //此时的orderDto对象里面orderDetails属性还是空 下面准备为它赋值
            Long orderId = item.getId();//获取订单id
            List<OrderDetail> orderDetailList = this.getOrderDetailListByOrderId(orderId);
            BeanUtils.copyProperties(item,orderDto);
            //对orderDto进行OrderDetails属性的赋值
            orderDto.setOrderDetails(orderDetailList);
            return orderDto;
        ).collect(Collectors.toList());

        //使用dto的分页有点难度.....需要重点掌握
        BeanUtils.copyProperties(pageInfo,pageDto,"records");
        pageDto.setRecords(orderDtoList);
        return R.success(pageDto);
    

代码测试:

点击去支付,然后点击去查看订单:

收工!

移动端的再来一单功能

由于这里没有写后台的确认订单功能,所以这里通过数据库修改订单状态来完成测试!

先把数据库中的订单表中的status改一些为4:这样在前端才能点击这个再来一单的按钮:

在order.html中可以看见这样一段前端代码:

<div class="btn" v-if="order.status === 4">  //状态是4才会让你点击下面这个再来一单
     <div class="btnAgain" @click="addOrderAgain(order)">再来一单
     </div>
</div>

然后找到addOrderAgain这个方法:前端使用post请求,请求地址order/again:

写后端接口:不建议把业务代码写在controller,不然以后想复用的时候就会很麻烦的!!!

    //客户端点击再来一单
    /**
     * 前端点击再来一单是直接跳转到购物车的,所以为了避免数据有问题,再跳转之前我们需要把购物车的数据给清除
     * ①通过orderId获取订单明细
     * ②把订单明细的数据的数据塞到购物车表中,不过在此之前要先把购物车表中的数据给清除(清除的是当前登录用户的购物车表中的数据),
     * 不然就会导致再来一单的数据有问题;
     * (这样可能会影响用户体验,但是对于外卖来说,用户体验的影响不是很大,电商项目就不能这么干了)
     */
    @PostMapping("/again")
    public R<String> againSubmit(@RequestBody Map<String,String> map)
        String ids = map.get("id");

        long id = Long.parseLong(ids);
       
        LambdaQueryWrapper<OrderDetail> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(OrderDetail::getOrderId,id);
        //获取该订单对应的所有的订单明细表
        List<OrderDetail> orderDetailList = orderDetailService.list(queryWrapper);

        //通过用户id把原来的购物车给清空,这里的clean方法是视频中讲过的,建议抽取到service中,那么这里就可以直接调用了
        shoppingCartService.clean();

        //获取用户id
        Long userId = BaseContext.getCurrentId();
        List<ShoppingCart> shoppingCartList = orderDetailList.stream().map((item) -> 
            //把从order表中和order_details表中获取到的数据赋值给这个购物车对象
            ShoppingCart shoppingCart = new ShoppingCart();
            shoppingCart.setUserId(userId);
            shoppingCart.setImage(item.getImage());
            Long dishId = item.getDishId();
            Long setmealId = item.getSetmealId();
            if (dishId != null) 
                //如果是菜品那就添加菜品的查询条件
                shoppingCart.setDishId(dishId);
             else 
                //添加到购物车的是套餐
                shoppingCart.setSetmealId(setmealId);
            
            shoppingCart.setName(item.getName());
            shoppingCart.setDishFlavor(item.getDishFlavor());
            shoppingCart.setNumber(item.getNumber());
            shoppingCart.setAmount(item.getAmount());
            shoppingCart.setCreateTime(LocalDateTime.now());
            return shoppingCart;
        ).collect(Collectors.toList());

        //把携带数据的购物车批量插入购物车表  这个批量保存的方法要使用熟练!!!
        shoppingCartService.saveBatch(shoppingCartList);

        return R.success("操作成功");
    

测试:

点击再来一单:

并且购物车表中也有数据:

移动端点击套餐图片查看套餐具体菜品

点击移动端套餐的图片,发现会向后端发送一个get请求,浏览器具体请求的图片我就不放了,我在前端页面找到了对应的axios请求:

至于前端展示需要的具体数据,我在前端页面没有找到。。。。。。然后就不想找了,但是找到了下面的代码;

然后我就试了三次,一次是返回R<List> ,一次是R<List>,还有一次是创建了一个SetmealDishDto,用R<List>,但是这个用dto怎么把dish的图片有序的设置进去我是真的处理不了>_<, 最后还是选择返回R<List>,,,,,不知道返回这个数据对不对哈,如果评论区有老哥有更好的写法,可以在评论区分享一下; 这个事情告诉我们和前端对接好是多么的重要!!!

下面的写法是修正后的写法,是评论区一位老哥指点后写出的;我开始返回的确实是<List>,不过下面的代码是纠正后的代码;

代码:

        /**
     * 移动端点击套餐图片查看套餐具体内容
     * 这里返回的是dto 对象,因为前端需要copies这个属性
     * 前端主要要展示的信息是:套餐中菜品的基本信息,图片,菜品描述,以及菜品的份数
     * @param SetmealId
     * @return
     */
    //这里前端是使用路径来传值的,要注意,不然你前端的请求都接收不到,就有点尴尬哈
    @GetMapping("/dish/id")
    public R<List<DishDto>> dish(@PathVariable("id") Long SetmealId)
        LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(SetmealDish::getSetmealId,SetmealId);
        //获取套餐里面的所有菜品  这个就是SetmealDish表里面的数据
        List<SetmealDish> list = setmealDishService.list(queryWrapper);

        List<DishDto> dishDtos = list.stream().map((setmealDish) -> 
            DishDto dishDto = new DishDto();
            //其实这个BeanUtils的拷贝是浅拷贝,这里要注意一下
            BeanUtils.copyProperties(setmealDish, dishDto);
            //这里是为了把套餐中的菜品的基本信息填充到dto中,比如菜品描述,菜品图片等菜品的基本信息
            Long dishId = setmealDish.getDishId();
            Dish dish = dishService.getById(dishId);
            BeanUtils.copyProperties(dish, dishDto);

            return dishDto;
        ).collect(Collectors.toList());

        return R.success(dishDtos);
    

测试展示: 之前我自己第一次写的这个代码的展示效果是没有这个菜品的具体份数的,因为当时我返回给前端的数据是:<List>;

删除地址

前端点击删除地址:然后发送删除请求到后端

在后端使用controller接收:

     /**
     * 根据地址id删除用户地址
     * @param id
     * @return
     */
    @DeleteMapping
    public R<String> delete(@RequestParam("ids") Long id)

        if (id == null)
            return R.error("请求异常");
        
        LambdaQueryWrapper<AddressBook> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(AddressBook::getId,id).eq(AddressBook::getUserId,BaseContext.getCurrentId());
        addressBookService.remove(queryWrapper);
        //addressBookService.removeById(id);  感觉直接使用这个removeById不太严谨.....
        return R.success("删除地址成功");
    

修改地址

点击修改符号,发现回显信息已经写好了;

回显信息的接口之前已经写好了:点击编辑,前端会发送下面的请求;

在该方法打一个debug,看回显信息是不是该接口:然后发现确实是该接口;

编写修改接口:修改完成后点击保存地址,前端会发下面的请求:

编写相关的接口:

    /**
     * 修改收货地址
     * @param addressBook
     * @return
     */
    @PutMapping
    public R<String> update(@RequestBody AddressBook addressBook)

        if (addressBook == null)
            return R.error("请求异常");
        
        addressBookService.updateById(addressBook);

        return R.success("修改成功");
    

后台订单状态的修改

在后台订单明细中点击派送按钮:前端会发送下面的请求来:是json格式的数据;

请求地址:http://localhost:8080/order

后台接收:

    @PutMapping
    public R<String> orderStatusChange(@RequestBody Map<String,String> map)

        String id = map.get("id");
        Long orderId = Long.parseLong(id);
        Integer status = Integer.parseInt(map.get("status"));

        if(orderId == null || status==null)
            return R.error("传入信息不合法");
        
        Orders orders = orderService.getById(orderId);
        orders.setStatus(status);
        orderService.updateById(orders);

        return R.success("订单状态修改成功");

    

测试:自己测试就行;

移动端登陆退出功能

    /**
     * 退出功能
     * ①在controller中创建对应的处理方法来接受前端的请求,请求方式为post;
     * ②清理session中的用户id
     * ③返回结果(前端页面会进行跳转到登录页面)
     * @return
     */
    @PostMapping("/loginout")
    public R<String> logout(HttpServletRequest request)
        //清理session中的用户id
        request.getSession().removeAttribute("user");
        return R.success("退出成功");
    

先自我介绍一下,小编13年上师交大毕业,曾经在小公司待过,去过华为OPPO等大厂,18年进入阿里,直到现在。深知大多数初中级java工程师,想要升技能,往往是需要自己摸索成长或是报班学习,但对于培训机构动则近万元的学费,着实压力不小。自己不成体系的自学效率很低又漫长,而且容易碰到天花板技术停止不前。因此我收集了一份《java开发全套学习资料》送给大家,初衷也很简单,就是希望帮助到想自学又不知道该从何学起的朋友,同时减轻大家的负担。添加下方名片,即可获取全套学习资料哦

Java项目瑞吉外卖保姆级学习笔记(改项目名称+改邮件验证码登录+功能补充)

前言

  • 本项目《听海餐厅》是基于黑马《瑞吉外卖》改名而来,感谢黑马贡献的高质量视频教程。
  • 本项目将短信登录改造成了邮箱登录。只想看邮箱验证码登录的小伙伴点此跳转【邮箱验证码】
  • 为了避免各位小伙伴面试时被面试官嘲讽【你们项目组还挺大啊】的尴尬场面,将原项目改名成了《听海餐厅》。

本期目录


一、 软件开发整体介绍

  • 本项目是一个单体架构,没有使用微服务。

1. 软件开发流程


2. 角色分工

  • 项目经理:对整个项目负责,任务分配、把控进度。
  • 产品经理:进行需求调研,输出需求调研文档、产品原型等。
  • UI设计师:根据产品原型输出界面效果图。
  • 架构师:项目整体架构设计、技术选型等。
  • 开发工程师:代码实现。
  • 测试工程师:编写测试用例,输出测试报告。
  • 运维工程师:软件环境搭建、项目上线。

3. 软件环境

  • 开发环境 (development):开发人员在开发阶段使用的环境,一般外部用户无法访问。
  • 测试环境 (testing):专门给测试人员使用的环境,用于测试项目,一般外部用户无法访问。
  • 生产环境 (production):即线上环境,正式提供对外服务的环境。

二、 项目介绍


1. 项目介绍

1.1 B端和C端

  • 本项目 (瑞吉外卖) 是专门为餐饮企业 (餐厅、饭店) 定制的一款软件产品,包括系统管理后台和移动端应用两部分。
    • 其中系统管理后台主要提供给餐饮企业内部员工使用,可以对餐厅的菜品、套餐、订单等进行管理维护。
    • 移动端应用主要提供给消费者使用,可以在线浏览菜品、添加购物车、下单等。

1.2 开发周期

  • 本项目共分为3期进行开发:
  • 第一期主要实现基本需求,其中移动端应用通过 H5 实现,用户可以通过手机浏览器访问。
  • 第二期主要针对移动端应用进行改进,使用微信小程序实现,用户使用起来更加方便。
  • 第三期主要针对系统进行优化升级,提高系统的访问性能。

2. 产品原型展示


2.1 后台管理登录界面



2.2 后台管理界面展示


2.3 用户端登录界面展示



2.4 邮件验证码展示



2.5 用户端界面展示


3. 技术选型


4. 功能架构


5. 角色

  • 后台系统管理员:登录后台管理系统,拥有后台系统中的所有操作权限。
  • 后台系统普通员工:登录后台管理系统,对菜品、套餐、订单等进行管理。
  • C端用户:登录移动端应用,可以浏览菜品、添加购物车、设置地址、在线下单等。

三、 开发环境搭建

1. 数据库环境搭建

1.1 数据库设计

  • 本项目不涉及到微服务架构,所有模块的数据表都统一放在一个数据库中。

  • 因此只需创建一个数据库,命名为 reggie

    CREATE DATABASE IF NOT EXISTS reggie CHARACTER SET 'utf8';
    

1.2 数据表设计

  • 根据功能划分为不同模块,我根据模块的不同创建不同的数据表。

    数据表描述难点
    后台系统用户表该数据库的root用户就是管理员
    C 端用户表
    订单表
    菜品表
    地址表
    套餐表
  • 很不幸,我想破脑袋也只能想到划分为 6 张表。但老师对该项目却划分出 11 张表。

  • 导入老师设计好的数据表:

  • 老师设计数据表的思路:

    数据表描述
    address_book地址簿表
    category菜品和套餐的分类表 (荤菜、素菜、周一套餐 )
    dish菜品表
    dish_flavor菜品口味关系表
    employee后台系统普通员工表
    order_detail订单明细表
    orders订单表
    setmeal套餐表
    setmeal_dish套餐菜品关系表
    shopping_cart购物车表 (感觉增删会很频繁)
    userC 端用户表

2. Spring Boot项目搭建

  • 本项目使用的 IDEA 版本为 2022.2.2 Ultimate 。

2.1 检查Maven目录与本地仓库

  • 创建前首先确保自己的 Maven 软件和 Maven 仓库已经与 IDEA 关联好:

2.2 创建Spring Boot项目

  • 按下图设置:

  • 导入依赖,有些依赖如 MyBatis-Plus 和 Druid 依赖没有被官方收录,我们先导入下图的三个:

2.3 整合第三方依赖

  • 打开项目的 pom.xml 文件,加入以下依赖:

    <!-- MyBatis-Plus -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.4.3</version>
    </dependency>
    
    <!-- Lombok官方收录无需写版本号 -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
    
    <!-- Fastjson:将对象转成json -->
    <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>
    
    <!-- Druid数据库连接池 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.2.12</version>
    </dependency>
    
  • 牢记每次修改完 pom.xml 配置文件都必须按 Shift + Ctrl + O 来刷新 Maven,Maven 才能帮你下载并导入依赖。

2.4 编写Spring Boot配置文件

  • 把 Spring Boot 默认的配置文件 application.properties 修改为 application.yml

  • application.yml 中添加如下配置:

    # 配置服务器端口
    server:
      port: 8080
    
    # 配置Druid数据库连接池
    spring:
      application:
        # 应用名称 (可选)
        name: reggie_take_out
      datasource:
        druid:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://localhost:3306/reggie?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
          username: root
          password: 你的数据库密码
    
    # 配置Mybatis-Plus
    mybatis-plus:
      configuration:
        # 在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射
        # 例如:属性名映射 user_name --> userName
        # 例如:类名映射 address_book --> AddressBook
        map-underscore-to-camel-case: true
         # 开启MP运行日志
        log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
      global-config:
        db-config:
          id-type: ASSIGN_ID
    

2.5 运行Spring Boot启动类

  • 打开 src/main/java/edu/ouc/ReggieTakeOutApplication.java ,添加 Lombok 提供的 @Slf4j 注解,方便输出日志来调试:

    @Slf4j  // 日志
    @SpringBootApplication  // Spring Boot启动类
    public class ReggieTakeOutApplication 
    
        public static void main(String[] args) 
            SpringApplication.run(ReggieTakeOutApplication.class, args);
            // 打印Slf4j日志
            log.info("项目启动成功");
        
    
    
  • 运行 ReggieTakeOutApplication.java

  • 看到如图所示的输出,就说明你的 Spring Boot 开发环境已经搭建好了。

2.6 导入前端代码

  • 本项目以后端开发为主要学习目标,前端代码已经提供好,直接加载即可。

  • 粘贴到 src/main/resources/static 目录下:

  • 重启项目,在浏览器中输入 http://localhost:8080/backend/index.html :

  • 看到此页面说明前端代码已成功导入。

四、 后台登录退出功能开发

1. 后台登录功能开发

  • 本章从后台管理页面的登录功能开始。

1.1 需求分析

① 请求分析

  • 先来查看后台登录的前端页面,浏览器地址栏输入 http://localhost:8080/backend/page/login/login.html

  • 按 F12 打开浏览器的控制台,点击 “登录” 按钮,查看浏览器是以何种方式向服务器发送请求的:

  • 可以看到请求方式是 POST ,请求 URL 是 http://localhost:8080/employee/login 。由于我们还没写对应的 Controller ,报 404 是很正常的。

  • 按照 Spring Boot 的开发思路,我们应该按照:数据层 (Mapper) –> 服务层 (Service) –> 表现层 (Controller) 三步走来开发。

② 数据库分析

  • 后台登录功能对应的数据表为 employee ,其表结构如下:

    DESC employee;
    

  • 对数据表中的字段进行逐一分析:

    字段作用
    id员工编号,主键,为什么不用自增?可能使用了MyBatis-Plus的雪花算法
    name员工姓名
    username登录账号,加了唯一约束,登录账号不允许重复
    password登录密码
    phone手机号码
    sex性别
    id_number身份证号码
    status员工状态 (禁用/可用)
    create_time创建时间
    update_time修改时间
    create_user创建人是谁,以员工ID记录
    update_user修改人是谁,以员工ID记录
  • 目前数据表 employee 仅有一条记录,就是后台管理员的记录:

③ 前端代码分析

  • 打开 src/main/resources/static/backend/page/login/login.html ,这是后台管理系统的登录页面。

  • 其中,我们后端工程师需要关心的最核心的东西就是:前后端数据交换的统一格式 (也称前后端协议) 。具体来说,是后端响应给前端的数据格式。就是下面这段代码:

    methods: 
        async handleLogin() 
            this.$refs.loginForm.validate(async (valid) => 
                if (valid) 
                    this.loading = true
                    let res = await loginApi(this.loginForm)
                    if (String(res.code) === '1')  // code:状态,1表示登录成功
                        localStorage.setItem('userInfo', JSON.stringify(res.data))	// data:数据,这里指的是账号和密码
                        window.location.href = '/backend/index.html'
                     else 	//登录失败
                        this.$message.error(res.msg)	// msg:登录失败提示信息
                        this.loading = false
                    
                
            )
        
    
    
  • 从上面这段登录代码中可以看出来,这里和前端工程师约定好的前后端数据交换的统一格式应当包含 3 部分:

    协议描述
    code状态,1 表示成功
    data传递的数据
    msg操作失败/成功的提示信息

    json 格式如下所示:

    res
        "code":1,
        "data":
        	"username":"admin",
            "password":"123456"
    	,
        "msg":"登录成功/登录失败"
    
    

2. 代码编写

2.1 创建实体类

  • 创建数据表 employee 的实体类 Employee.java 。并用 Lombok 快速生成 Getter 、Setter 、toString() 、equals() 等。

  • 其中的属性与数据表 employee 的字段一一对应。

  • 创建 src/main/java/edu/ouc/entity/Employee.java

    @Data
    public class Employee 
        private static final Long serialVersionUID = 1L;// 序列化ID
        private Long id;
        private String name;
        private String username;
        private String password;
        private String phone;
        private String sex;
        private String idNumber;
        private Integer status;
        private LocalDateTime createTime;
        private LocalDateTime updateTime;
        
        @TableField(fill = FieldFill.INSERT)
        private Long createUser;
        
        @TableField(fill = FieldFill.INSERT)
        private Long updateUser;
    
    

2.2 数据层开发

  • 登录只涉及到对数据库的查询操作。即,根据账户名 username 查询密码 password

  • 创建数据层、服务层和表现层对应的文件夹。

① 创建数据层接口
  • 创建 src/main/java/edu/ouc/mapper/EmployeeMapper.java

  • 继承 MyBatis-Plus 的 BaseMapper<T> 泛型接口,添加 @Mapper 注解。就能获取父类 BaseMapper<T> 中已经写好的增删改查方法。

    @Mapper
    public interface EmployeeMapper extends BaseMapper<Employee> 
    
    

2.3 服务层开发

① 创建服务层接口
  • 创建 src/main/java/edu/ouc/service/IEmployeeService.java

  • 我们可以让服务层接口继承 MyBatis-Plus 的 IService<T> 泛型接口来进行快速开发。

    public interface IEmployeeService extends IService<Employee> 
    
    
  • 继承 IService<T> 后能获取很多通用的增删改查方法:

②创建服务层接口实现类
  • 创建 src/main/java/edu/ouc/service/impl/IEmployeeServiceImpl.java
  • 我们可以让其先继承 MyBatis-Plus 的 ServiceImpl<M,T> ,再实现 IEmployeeService.java 。其中,<M,T> 中的 M 指的是对应的 DAO 接口,T 指的是实体类。这样,我们就无需实现 IEmployeeService.java 中全部的方法,而是根据需要,既可以使用提供的基础 CRUD 方法,也可以自定义新的方法。

2.4 表现层开发

① 创建前后端统一格式协议类R