SSM实战-外卖项目-06-用户地址簿功能、菜品展示、购物车、下单(一个业务涉及5张表)

news2024/11/24 23:10:22

文章目录

  • 外卖项目-第六天
    • 课程内容
    • 1. 用户地址簿功能
      • 1.1 需求分析
      • 1.2 数据模型
      • 1.3 导入功能代码
      • 1.4 功能测试 (其实需求分析里我就自己写了一份代码,而且测试过了,下面再测试了一遍)
    • 2. 菜品展示
      • 2.1 需求分析
      • 2.2 前端页面分析
      • 2.3 代码开发
        • 2.3.1 查询菜品方法修改
        • 2.3.2 根据分类ID查询套餐
      • 2.4 功能测试
    • 3. 购物车
      • 3.1 需求分析
      • 3.2 数据模型
      • 3.3 前端页面分析
      • 3.4 准备工作
      • 3.5 代码开发
        • 3.5.1 添加购物车
        • 3.5.2 查询购物车
        • 3.5.3 清空购物车
      • 3.6 功能测试
      • 3.7 更多功能
        • 3.7.1 减少/删除购物车商品
    • 4. 下单
      • 4.1 需求分析
      • 4.2 数据模型
      • 4.3 前端页面分析
      • 4.4 准备工作
      • 4.5 代码开发 (复杂,小心 涉及5张表(查3张写2张1删)信息 慢慢查 慢慢封装)
      • 4.6 功能测试
      • 4.7 更多功能
        • 4.7.1 查看订单

外卖项目-第六天

课程内容

  • 用户地址簿功能
  • 菜品展示
  • 购物车
  • 下单

本模块,没有什么难的技术,难点都在业务本身,理清业务逻辑是关键

1. 用户地址簿功能

1.1 需求分析

地址簿,指的是移动端消费者用户的地址信息,用户登录成功后可以维护自己的地址信息。同一个用户可以有多个地址信息,但是只能有一个默认地址

在这里插入图片描述
在这里插入图片描述

对于地址簿管理,我们需要实现以下几个功能:

  • 新增地址
  • 地址列表查询
  • 设置默认地址
  • 编辑地址
  • 删除地址

1.2 数据模型

用户的地址信息会存储在address_book表,即地址簿表中。具体表结构如下:
在这里插入图片描述

这里面有一个字段is_default,实际上我们在设置默认地址时,只需要更新这个字段就可以了。

1.3 导入功能代码

对于这一类的单表的增删改查,我们已经写过很多了,基本的开发思路都是一样的,那么本小节的用户地址簿管理的增删改查功能,我们就不再一一实现了,基本的代码我们都已经提供了,直接导入进来,做一个测试即可。

对于下面的地址管理的代码,我们可以直接从资料拷贝,也可以直接从下面的讲义中复制。

单表CRUD太简单了,自己快速写一下吧

先用CodeGenerator生成各个模块文件,然后修改好,最终版本如下

1). 实体类 AddressBook(自己快速写一下吧)

所属包: cn.whu.reggie.entity

表名修改驼峰命名:AddressBook
属性修改驼峰命名: userId、provinceCode、provinceName、cityCode、cityName、districtCode、districtName、isDefault、createTime、updateTime、createUser、updateUser、isDeleted
★:isDefault 改成 Integer 类型 (生成的是boolean类型 影响前端判断逻辑)
★:sex 改成 String 类型 (生成的是 Integer 类型 影响前端判断逻辑)

@Data
@EqualsAndHashCode(callSuper = false)
public class AddressBook implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 主键
     */
    private Long id;

    /**
     * 用户id
     */
    private Long userId;

    /**
     * 收货人
     */
    private String consignee;

    /**
     * 性别 0 女 1 男
     */
    private String sex;

    /**
     * 手机号
     */
    private String phone;

    /**
     * 省级区划编号
     */
    private String provinceCode;

    /**
     * 省级名称
     */
    private String provinceName;

    /**
     * 市级区划编号
     */
    private String cityCode;

    /**
     * 市级名称
     */
    private String cityName;

    /**
     * 区级区划编号
     */
    private String districtCode;

    /**
     * 区级名称
     */
    private String districtName;

    /**
     * 详细地址
     */
    private String detail;

    /**
     * 标签
     */
    private String label;

    /**
     * 默认 0 否 1是
     */
    private Integer isDefault;

    /**
     * 创建时间
     */
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    /**
     * 更新时间
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;

    /**
     * 创建人
     */
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;

    /**
     * 修改人
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;

    /**
     * 是否删除
     */
    private Integer isDeleted;

}

2). Mapper接口 AddressBookMapper

所属包: cn.whu.reggie.mapper

删除xml包,加上@Mapper注解
F2重命名为驼峰命名: AddressBookMapper

@Mapper
public interface AddressBookMapper extends BaseMapper<AddressBook> {

}

3). 业务层接口 AddressBookService

所属包: cn.whu.reggie.service

重命名为:AddressBookService

public interface AddressBookService extends IService<AddressBook> {

}

4). 业务层实现类 AddressBookServiceImpl

所属包: cn.whu.reggie.service.impl

重命名为: AddressBookServiceImpl

@Service
public class AddressBookServiceImpl extends ServiceImpl<AddressBookMapper, AddressBook> implements AddressBookService {

}

5). 控制层 AddressBookController

所属包: cn.whu.reggie.controller

重命名: AddressBookController
注入:AddressBookService

@RestController
@RequestMapping("/addressBook")
public class AddressBookController {
    @Autowired
    private AddressBookService addressBookService;
}

controller主要开发的功能:

A. 新增地址逻辑说明:

  • 需要记录当前是哪个用户的地址(关联当前登录用户)
  • 在这里插入图片描述
/**
 * 新增地址
 * @param addressBook
 * @return
 */
@PostMapping
public R<String> save(@RequestBody AddressBook addressBook){
    // userId需要设置一下
    //LoginCheckFilter也将id绑定到当前线程了 所以直接线程里拿也行 (不需要获取session了)
    Long userId = (Long) BaseContext.getCurrentId();
    addressBook.setUserId(userId);
    log.info("新增地址: addressBook = {}",addressBook);
    // 保存地址
    addressBookService.save(addressBook);
    return R.success("地址添加成功!");
}

LoginCheckFilter里将当前用户Id放到线程的局部变量区了,且提供了工具类,所以不需要获取session,直接BaseContext工具类拿线程里存的用户id即可

B. 查询指定用户的全部地址

  • 根据当前登录用户ID,查询所有的地址列表
  • address.html在这里插入图片描述
  • 在这里插入图片描述
/**
 * 查询指定用户全部地址
 * 其实是查询当前用户全部地址,写成了通用形式
 * @return
 */
@GetMapping("/list")
// 其实并没有传递参数 所以参数为空也可以实现逻辑
// 但是这么写比较有通用性,后期可以按照表的任意字段条件查询
// 条件查询Get方法,都是普通URL参数 非json不需要@RequestBody注解
public R<List<AddressBook>> list(AddressBook addressBook){
    Long id = BaseContext.getCurrentId();//直接线程里拿id,不需要麻烦地先获取session了
    addressBook.setUserId(id);
    log.info("获取当前用户所有地址,写成了通用的条件查询: addressBook = {}",addressBook);

    LambdaQueryWrapper<AddressBook> lqw = new LambdaQueryWrapper<>();
    lqw.eq(addressBook.getUserId()!=null,AddressBook::getUserId,addressBook.getUserId());
    lqw.orderByDesc(AddressBook::getUpdateTime);
    List<AddressBook> list = addressBookService.list(lqw);

    return R.success(list);
}

C. 设置默认地址

  • 每个用户可以有很多地址,但是默认地址只能有一个 ;

  • 先将该用户所有地址的is_default更新为0 , 然后将当前的设置的默认地址的is_default设置为1

  • 在这里插入图片描述

/**
 * 根据id设置默认地址
 * @param addressBook
 * @return
 */
@PutMapping("/default")
public R<String> setDefault(@RequestBody AddressBook addressBook){
    log.info("设置默认地址: addressBook.id = {}, ||| {}",addressBook.getId(),addressBook);

    // 1. 先将该id用户下所有地址的setIsDefault设置为false
    // 注意使用的是UpdateWrapper
    LambdaUpdateWrapper<AddressBook> luw = new LambdaUpdateWrapper<>();
    luw.eq(AddressBook::getUserId,BaseContext.getCurrentId());
    luw.set(AddressBook::getIsDefault,0); // 两个条件 设置当前id下所有 is_default字段为0
    addressBookService.update(luw); //update方法,修改所有满足条件的 (updateById只会修改一条)

    // 2. 再设置当前记录的IsDefault为true
    addressBook.setIsDefault(1);
    addressBookService.updateById(addressBook);

    return R.success("默认地址设置成功!");//个人觉得更新操作不需要返回AddressBook实体
}

在这里插入图片描述

D. 根据ID查询地址 (修改时回显数据)
在这里插入图片描述
在这里插入图片描述
测试发现问题,标签不回显
标签:查看代码(如下)发现是通过变量 activeIndex 来维护选中项的

<span v-for="(item,index) in labelList" :key="index" @click="form.label = item;activeIndex = index" :class="{spanItem:true,spanActiveSchool:activeIndex == index}">{{item}}</span>

所以js刚得到回显数据就设置一下变量 activeIndex 的值即可

if(res.code === 1){
  this.form = res.data
  // 激活id是哪个 需要自己找一下 (加下面这一行代码)
  this.activeIndex = this.labelList.indexOf(res.data.label)
}

在这里插入图片描述
E. 修改地址信息

  • 提交修改后数据,更新记录
  • 在这里插入图片描述
    普通json格式数据,单表真的好简单啊
{
    "id": "1646754792561131522",
    "userId": "1646506632639148033",
    "consignee": "慕容紫英",
    "sex": 0,
    "phone": "13212345678",
    "provinceCode": null,
    "provinceName": null,
    "cityCode": null,
    "cityName": null,
    "districtCode": null,
    "districtName": null,
    "detail": "昆仑山青鸾峰",
    "label": "学校",
    "isDefault": 0,
    "createTime": "2023-04-14 13:58:32",
    "updateTime": "2023-04-14 15:24:47",
    "createUser": "1646506632639148033",
    "updateUser": "1646506632639148033",
    "isDeleted": 0
}
/**
 * 根据id修改地址信息
 * @param addressBook
 * @return
 */
@PutMapping
public R<String> updateById(@RequestBody AddressBook addressBook){
    log.info("修改用户信息: {}",addressBook);
    addressBookService.updateById(addressBook);
    return R.success("修改成功!");
}

测试没有问题,注意POJO-AddressBook 里sex属性得是String类型,否则和前端不匹配,麻烦

F. 查询默认地址

  • 根据当前登录用户ID 以及 is_default进行查询,查询当前登录用户is_default为1的地址信息
    后期下单要用到,先写着
    提示: @GetMapping("default")
 /**
  * 查询当前用户默认地址
  * @return
  */
 @GetMapping("/default")
 public R<AddressBook> getDefault(){
     log.info("查询当前用户默认地址,当前用户id={}",BaseContext.getCurrentId());

     LambdaQueryWrapper<AddressBook> lqw = new LambdaQueryWrapper<>();
     lqw.eq(AddressBook::getUserId,BaseContext.getCurrentId());
     lqw.eq(AddressBook::getIsDefault,1);
     //SQL:select * from address_book where user_id = ? and is_default = 1
     AddressBook addressBook = addressBookService.getOne(lqw);
     if(addressBook == null) return R.error("没有找到默认地址,请先设置一个默认地址");
     return R.success(addressBook);
 }

直接访问controller链接地址测试: http://localhost:8080/addressBook/default


完整代码实现如下:

  • 我的
/**
 * <p>
 * 地址管理 前端控制器
 * </p>
 *
 * @author whu
 * @since 2023-04-13
 */
@RestController
@RequestMapping("/addressBook")
@Slf4j
public class AddressBookController {

    @Autowired
    private AddressBookService addressBookService;

    /**
     * 新增地址
     * @param addressBook
     * @return
     */
    @PostMapping
    public R<String> save(@RequestBody AddressBook addressBook){
        // userId需要设置一下
        //LoginCheckFilter也将id绑定到当前线程了 所以直接线程里拿也行 (不需要获取session了)
        Long userId = BaseContext.getCurrentId();
        addressBook.setUserId(userId);
        log.info("新增地址: addressBook = {}",addressBook);
        // 保存地址
        addressBookService.save(addressBook);
        return R.success("地址添加成功!");//个人觉得保存操作不需要返回AddressBook实体
    }

    /**
     * 查询指定用户全部地址
     * 其实是查询当前用户全部地址,写成了通用形式
     * @return
     */
    @GetMapping("/list")
    // 其实并没有传递参数 所以参数为空也可以实现逻辑
    // 但是这么写比较有通用性,后期可以按照表的任意字段条件查询 (此方法 查询所有或者条件查询 通用)
    // 条件查询Get方法,都是普通URL参数 非json不需要@RequestBody注解
    public R<List<AddressBook>> list(AddressBook addressBook){
        Long id = BaseContext.getCurrentId();//直接线程里拿id,不需要麻烦地先获取session了
        addressBook.setUserId(id);
        log.info("获取当前用户所有地址,写成了通用的条件查询: addressBook = {}",addressBook);

        LambdaQueryWrapper<AddressBook> lqw = new LambdaQueryWrapper<>();
        // 根据当前用户id查询对应的地址
        lqw.eq(addressBook.getUserId()!=null,AddressBook::getUserId,addressBook.getUserId());
        // 排序条件 交互性更好
        lqw.orderByDesc(AddressBook::getUpdateTime);
        List<AddressBook> list = addressBookService.list(lqw);

        return R.success(list);
    }

    /**
     * 根据id设置默认地址
     * @param addressBook
     * @return
     */
    @PutMapping("/default")
    public R<String> setDefault(@RequestBody AddressBook addressBook){
        log.info("设置默认地址: addressBook.id = {}, ||| {}",addressBook.getId(),addressBook);

        // 1. 先将该id用户下所有地址的setIsDefault设置为false
        // 注意使用的是UpdateWrapper
        LambdaUpdateWrapper<AddressBook> luw = new LambdaUpdateWrapper<>();
        luw.eq(AddressBook::getUserId,BaseContext.getCurrentId());
        luw.set(AddressBook::getIsDefault,0); // 两个条件 设置当前id下所有 is_default字段为0
        addressBookService.update(luw); //update方法,修改所有满足条件的 (updateById只会修改一条)

        // 2. 再设置当前记录的IsDefault为true
        addressBook.setIsDefault(1);
        addressBookService.updateById(addressBook);

        return R.success("默认地址设置成功!");//个人觉得更新操作不需要返回AddressBook实体
    }

    /**
     * 根据id查询地址记录
     * 用于修改时地址回显
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    public R<AddressBook> getById(@PathVariable Long id){
        log.info("根据id查询地址记录: id = {}",id);
        AddressBook addressBook = addressBookService.getById(id);
        if(addressBook == null) return R.error("没有找到地址记录");
        return R.success(addressBook);
    }

    /**
     * 根据id修改地址信息
     * @param addressBook
     * @return
     */
    @PutMapping
    public R<String> updateById(@RequestBody AddressBook addressBook){
        log.info("修改用户信息: {}",addressBook);
        addressBookService.updateById(addressBook);
        return R.success("修改成功!");
    }

    /**
     * 查询当前用户默认地址
     * @return
     */
    @GetMapping("/default")
    public R<AddressBook> getDefault(){
        log.info("查询当前用户默认地址,当前用户id={}",BaseContext.getCurrentId());

        LambdaQueryWrapper<AddressBook> lqw = new LambdaQueryWrapper<>();
        lqw.eq(AddressBook::getUserId,BaseContext.getCurrentId());
        lqw.eq(AddressBook::getIsDefault,1);
        //SQL:select * from address_book where user_id = ? and is_default = 1
        AddressBook addressBook = addressBookService.getOne(lqw);
        if(addressBook == null) return R.error("没有找到默认地址,请先设置一个默认地址");
        return R.success(addressBook);
    }

}


  • 教程提供的
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.itheima.reggie.common.BaseContext;
import com.itheima.reggie.common.R;
import com.itheima.reggie.entity.AddressBook;
import com.itheima.reggie.service.AddressBookService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;

/**
 * 地址簿管理
 */
@Slf4j
@RestController
@RequestMapping("/addressBook")
public class AddressBookController {
    @Autowired
    private AddressBookService addressBookService;

    /**
     * 新增
     */
    @PostMapping
    public R<AddressBook> save(@RequestBody AddressBook addressBook) {
        addressBook.setUserId(BaseContext.getCurrentId());
        log.info("addressBook:{}", addressBook);
        addressBookService.save(addressBook);
        return R.success(addressBook);
    }

    /**
     * 设置默认地址
     */
    @PutMapping("default")
    public R<AddressBook> setDefault(@RequestBody AddressBook addressBook) {
        log.info("addressBook:{}", addressBook);
        LambdaUpdateWrapper<AddressBook> wrapper = new LambdaUpdateWrapper<>();
        wrapper.eq(AddressBook::getUserId, BaseContext.getCurrentId());
        wrapper.set(AddressBook::getIsDefault, 0);
        //SQL:update address_book set is_default = 0 where user_id = ?
        addressBookService.update(wrapper);

        addressBook.setIsDefault(1);
        //SQL:update address_book set is_default = 1 where id = ?
        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);

        //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));
    }
}

1.4 功能测试 (其实需求分析里我就自己写了一份代码,而且测试过了,下面再测试了一遍)

代码导入进来,并且去阅读了一下地址管理各个功能的逻辑实现,接下来,我们就可以启动项目,进行一个测试。测试过程中,通过debug断点调试观察服务端程序的执行过程,在浏览器中使用调试工具查看页面和服务端的交互过程和请求响应数据。

1). 新增

填写表单数据,点击保存地址,查看网络请求。

在这里插入图片描述

测试完毕之后,检查数据库中的数据,是否正常插入。

在这里插入图片描述

2). 列表查询

当新增地址完成后,页面会再次发送一个请求,来查询该用户的所有地址列表,在界面进行展示。
在这里插入图片描述
在这里插入图片描述

3). 设置默认

在地址列表页面中,勾选 “设为默认地址” ,此时会发送PUT请求,来设置默认地址。

在这里插入图片描述

测试完毕后,我们再次查看数据库表中的数据:

在这里插入图片描述

2. 菜品展示

2.1 需求分析

在这里插入图片描述

2.2 前端页面分析

在开发代码之前,需要梳理一下前端页面和服务端的交互过程:

1). 页面(front/index.html)发送ajax请求,获取分类数据(菜品分类和套餐分类)
在这里插入图片描述

直接访问/category/list 不提供type参数,就是无条件查询,也就是查询category表下所有分类记录

该功能在之前的业务开发中,我们都已经实现了。通过请求响应的数据,我们也可以看到数据是可以正确获取到的。

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

注意:首页加载时,不仅发送请求获取分类列表,还发送了一次ajax请求用于加载购物车数据,而这两次请求必须全部成功,页面才可以正常渲染,而当前购物车列表查询功能还未实现(报404),所以列表目前并未渲染。此处可以先写一下该方法,暂时不写具体逻辑,不报错即可,等后续开发购物车功能时再写具体逻辑,如下:

CodeGenerator代码生成器快速生成一下shopping_cart模块: 修改好,然后写一个Controller方法,返回一个假的数据,之后就放那,等到开发购物车模块时再管
改js要清空不少缓存,很麻烦,所以不采用直接改js里的请求路径的方式了

@RestController
@RequestMapping("/shoppingCart")
@Slf4j
public class ShoppingCartController {
    @GetMapping("/list")
    public R<List<ShoppingCart>> list(){
        log.info("查询购物车");
        ShoppingCart cart = new ShoppingCart();
        cart.setUserId(BaseContext.getCurrentId());
        cart.setAmount(new BigDecimal(888.88));
        ArrayList<ShoppingCart> list = new ArrayList<>();
        list.add(cart);
        return R.success(list);
    }
}

提供方法之后,我们再次测试:

在这里插入图片描述
在这里插入图片描述

目前该部分的功能我们已经调通,左侧的分类菜单,和右侧的菜品信息我们都可以看到,后续我们只需要将购物车列表的数据改成调用服务端接口查询即可。

2). 页面发送ajax请求,获取第一个分类下的菜品或者套餐

在这里插入图片描述

A. 根据分类ID查询套餐列表

在这里插入图片描述

B. 根据分类ID查询菜品列表

在这里插入图片描述

异步请求,查询分类对应的菜品列表,功能我们已经实现了,但是我们之前查询的只是菜品的基本信息,不包含菜品的口味信息。所以在前端界面中,我们看不到选择菜品分类的信息。

在这里插入图片描述

经过上述的分析,我们可以看到,服务端我们主要提供两个方法, 分别用来:

A. 根据分类ID查询菜品列表(包含菜品口味列表), 具体请求信息如下:

请求说明
请求方式GET
请求路径/dish/list
请求参数?categoryId=1397844263642378242&status=1

该功能在服务端已经实现,我们需要修改此方法,在原有方法的基础上增加查询菜品的口味信息。

B. 根据分类ID查询套餐列表, 具体请求信息如下:

请求说明
请求方式GET
请求路径/setmeal/list
请求参数?categoryId=1397844263642378242&status=1

该功能在服务端并未实现。

2.3 代码开发

2.3.1 查询菜品方法修改

由于之前我们实现的根据分类查询菜品列表,仅仅查询了菜品的基本信息,未查询菜品口味信息,而移动端用户在点餐时,是需要选择口味信息的,所以我们需要对之前的代码实现进行完善,那么如何完善呢?

我们需要修改DishController的list方法,原来此方法的返回值类型为:R<List<Dish>>。为了满足移动端对数据的要求(菜品基本信息和菜品对应的口味信息),现在需要将方法的返回值类型改为:R<List<DishDto>> ,因为在DishDto中封装了菜品对应的口味信息:

在这里插入图片描述

代码逻辑:

A. 根据分类ID查询,查询目前正在启售的菜品列表 (已实现)

B. 遍历菜品列表,并查询菜品的分类信息及菜品的口味列表

C. 组装数据DishDto,并返回

代码实现:

// 教程版本二: 之前的版本不通用 没有口味信息 下面进行改造
/**
 * 根据条件查询对应的菜品数据 (多么通用的方法啊  自己写就只会写一个根据categoryId查询)
 * 加上口味信息,返回DishDto,更加通用了
 * 前台也可以复用这个方法了
 * 限定启售的理解: 1、后台新增套餐添加菜品时只能选择添加起售的菜品 2、前台给用户看到的更是只能是正在起售的商品了
 * @param categoryId
 * @return
 */
@GetMapping("/list")
public R<List<DishDto>> list(Dish dish){ // 千万注意是Long不是Integer 封装不上一直报404,找了半天的错误,要死
    log.info("根据id查询所有商品: (舍弃这种写法 根据xx查询 完全可以通用一点啊 直接用实体类接受条件)");
    log.info("根据条件所有商品: dish = {}",dish);
    // 1. 查询菜品基本信息 (不能直接用DishDto dishService.list(lqw);返回的时Dish,父类无法直接赋值给子类)
    LambdaQueryWrapper<Dish> lqw = new LambdaQueryWrapper<>();
    // 添加查询条件1:此处根据id查询
    lqw.eq(dish.getCategoryId()!=null,Dish::getCategoryId,dish.getCategoryId());
    // 添加查询条件2:启售商品
    lqw.eq(Dish::getStatus,1);
    // 排序条件:(交互性更好)  sort升序,更新时间降序
    lqw.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
    List<Dish> dishList = dishService.list(lqw);

    // 2. 查询对应口味信息 (并全部封装到Dto)
    // 流式编程确实逻辑看得清晰一点
    List<DishDto> dishDtos = dishList.stream().map(ds->{
        DishDto dto = new DishDto();
        // 2.1 普通属性直接复制
        BeanUtils.copyProperties(ds,dto);

        // 2.1.5 categoryName也查一下吧 怎么简单怎么来了,嘿嘿
        Category category = categoryService.getById(ds.getCategoryId());
        if(category!=null) dto.setCategoryName(category.getName()); //判断一下 更健壮

        // 2.2 口味信息查询后设置
        LambdaQueryWrapper<DishFlavor> dfLqw = new LambdaQueryWrapper<>();
        dfLqw.eq(DishFlavor::getDishId,ds.getId());
        //SQL:select * from dish_flavor where dish_id = ?
        List<DishFlavor> flavors = dishFlavorService.list(dfLqw);
        dto.setFlavors(flavors);
        return dto;
    }).collect(Collectors.toList());

    /*List<DishDto> dishDtos = new ArrayList<>();
    for (Dish ds : dishList) {
        DishDto dto = new DishDto();
        // 2.1 普通属性直接复制
        BeanUtils.copyProperties(ds,dto);
        // 2.2 口味信息查询后设置
        LambdaQueryWrapper<DishFlavor> dfLqw = new LambdaQueryWrapper<>();
        dfLqw.eq(DishFlavor::getDishId,ds.getId());
        List<DishFlavor> flavors = dishFlavorService.list(dfLqw);
        dto.setFlavors(flavors);
        dishDtos.add(dto);
    }*/

    return R.success(dishDtos);
}

2.3.2 根据分类ID查询套餐

在SetmealController中创建list方法,根据条件查询套餐数据。

写多了,没用到setmealDishes(中间表里套餐关联的菜品信息),先放着吧,多了只有好处没有坏处的

/**
 * 根据条件查询套餐数据
 * @param setmeal
 * @return
 */
@GetMapping("/list")
public R<List<SetmealDto>> list(Setmeal setmeal){
    log.info("根据categoryId查询该套餐分类下所有套餐数据 【可以拓展为通用的套餐条件查询】");
    log.info("setmeal = {}",setmeal);

    // 1. 查询套餐基本信息 (id和status都按照指定的条件来了)
    LambdaQueryWrapper<Setmeal> lqw = new LambdaQueryWrapper<>();
    // CategoryId 分类条件
    lqw.eq(setmeal.getCategoryId()!=null,Setmeal::getCategoryId,setmeal.getCategoryId());
    // Status 启售条件
    lqw.eq(setmeal.getStatus()!=null,Setmeal::getStatus,setmeal.getStatus());
    // 别忘了查询list一定加一个排序条件,提高用户体验
    lqw.orderByDesc(Setmeal::getUpdateTime);
    List<Setmeal> setmealList = setmealService.list(lqw);

    // 2. 查询套餐下的菜品信息 (中间表有冗余字段 查中间表记录即可)
    List<SetmealDto> setmealDtoList = setmealList.stream().map(sm -> {
        SetmealDto dto = new SetmealDto();
        // 2.1 父类属性直接复制
        BeanUtils.copyProperties(sm,dto);
        // 2.2 子类属性:setmealDishes 关联的中间表数据 查询后再添加
        LambdaQueryWrapper<SetmealDish> dfLqw = new LambdaQueryWrapper<>();
        dfLqw.eq(SetmealDish::getSetmealId,sm.getId());
        List<SetmealDish> setmealDishes = setmealDishService.list(dfLqw);
        dto.setSetmealDishes(setmealDishes);
        return dto;
    }).collect(Collectors.toList());

    // 写完发现 套餐并没有设置详情信息 那就先放这,迟早用得着

    return R.success(setmealDtoList);
}

2.4 功能测试

把菜品展示的功能代码完善完成之后,我们重新启动服务,来测试一个菜品展示的功能。测试过程中可以使用浏览器的监控工具查看页面和服务端的数据交互细节。

在这里插入图片描述

点击分类,根据分类查询菜品列表/套餐列表:

在这里插入图片描述

3. 购物车

3.1 需求分析

在这里插入图片描述

这里面我们需要实现的功能包括:

1). 添加购物车

2). 查询购物车

3). 清空购物车

3.2 数据模型

用户的购物车数据,也是需要保存在数据库中的,购物车对应的数据表为shopping_cart表,具体表结构如下:

在这里插入图片描述

说明:

  • 购物车数据是关联用户的,在表结构中,我们需要记录,每一个用户的购物车数据是哪些
  • 菜品列表展示出来的既有套餐,又有菜品,如果APP端选择的是套餐,就保存套餐ID(setmeal_id),如果APP端选择的是菜品,就保存菜品ID(dish_id)
  • 对同一个菜品/套餐,如果选择多份不需要添加多条记录,增加数量number即可

最终shopping_cart表中存储的数据示例:

在这里插入图片描述

3.3 前端页面分析

在开发代码之前,需要梳理一下购物车操作时前端页面和服务端的交互过程:

1). 点击 “加入购物车” 或者 “+” 按钮,页面发送ajax请求,请求服务端,将菜品或者套餐添加到购物车

在这里插入图片描述
在这里插入图片描述

2). 点击购物车图标,页面发送ajax请求,请求服务端查询购物车中的菜品和套餐

此时,我们就直接修改购物车模块的controller方法了 (之前为了不报错写了个假的嘛)

3). 点击清空购物车按钮,页面发送ajax请求,请求服务端来执行清空购物车操作

在这里插入图片描述

经过上述的分析,我们可以看到,对于购物车的功能,我们主要需要开发以下几个功能,具体的请求信息如下:

1). 加入购物车

请求说明
请求方式POST
请求路径/shoppingCart/add
请求参数json格式

注意菜品数据有flavor,用户直接选菜品肯定是有自己的口味嘛,就这一个用户这一次订单要用到,直接购物车表里一个varchar字段就够了

菜品数据: 
{"amount":118,"dishFlavor":"不要蒜,微辣","dishId":"1397851099502260226","name":"全家福","image":"a53a4e6a-3b83-4044-87f9-9d49b30a8fdc.jpg"}

套餐数据: 
{"amount":38,"setmealId":"1423329486060957698","name":"营养超值工作餐","image":"9cd7a80a-da54-4f46-bf33-af3576514cec.jpg"}

2). 查询购物车列表

请求说明
请求方式GET
请求路径/shoppingCart/list

3). 清空购物车功能

请求说明
请求方式DELETE
请求路径/shoppingCart/clean

3.4 准备工作

分析完毕购物车的业务需求和实现思路之后,在开发业务功能前,先将需要用到的类和接口基本结构创建好:

CodeGenerator(MP代码生器直接生成,然后修改为如下格式)

1). 实体类 ShoppingCart

所属包: cn.whu.reggie.entity

@Data
@EqualsAndHashCode(callSuper = false)
public class ShoppingCart implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 主键
     */
    private Long id;

    /**
     * 名称
     */
    private String name;

    /**
     * 图片
     */
    private String image;

    /**
     * 主键
     */
    private Long userId;

    /**
     * 菜品id
     */
    private Long dishId;

    /**
     * 套餐id
     */
    private Long setmealId;

    /**
     * 口味
     */
    private String dishFlavor;

    /**
     * 数量
     */
    private Integer number;

    /**
     * 金额
     */
    private BigDecimal amount;

    /**
     * 创建时间
     */
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
}

2). Mapper接口 ShoppingCartMapper

所属包: cn.whu.reggie.mapper

@Mapper
public interface ShoppingCartMapper extends BaseMapper<ShoppingCart> {

}

3). 业务层接口 ShoppingCartService

所属包: cn.whu.reggie.service

public interface ShoppingCartService extends IService<ShoppingCart> {

}

4). 业务层实现类 ShoppingCartServiceImpl

所属包: cn.whu.reggie.service.impl

@Service
public class ShoppingCartServiceImpl extends ServiceImpl<ShoppingCartMapper, ShoppingCart> implements ShoppingCartService {

}

5). 控制层 ShoppingCartController

所属包: cn.whu.reggie.controller

@RestController
@RequestMapping("/shoppingCart")
@Slf4j
public class ShoppingCartController {
    @Autowired
    private ShoppingCartService shoppingCartService;
    
    @GetMapping("/list")
    public R<List<ShoppingCart>> list(){
        log.info("查询购物车");
        ShoppingCart cart = new ShoppingCart();
        cart.setUserId(BaseContext.getCurrentId());
        cart.setAmount(new BigDecimal(888.88));
        ArrayList<ShoppingCart> list = new ArrayList<>();
        list.add(cart);
        return R.success(list);
    }
}

之前假的数据,需要修改哦

3.5 代码开发

3.5.1 添加购物车

在ShoppingCartController中创建add方法,来完成添加购物车的逻辑实现,具体的逻辑如下:

A. 获取当前登录用户,为购物车对象赋值

B. 根据当前登录用户ID 及 本次添加的菜品ID/套餐ID,查询购物车数据是否存在

C. 如果已经存在,就在原来数量基础上加1 (user_id和dish_id 以及 user_id和setmeal_id 可以看成逻辑上的联合主键)

D. 如果不存在,则添加到购物车,数量默认就是1

代码实现如下:

注意LocalDateTime 直接注入不行的,自己设置,因为只有他一个。(一填充就填充4个,缺失3个导致报错)

/**
 * 加入一个菜品或一个套餐到购物车
 * @param shoppingCart
 * @return
 */
@PostMapping("/add")
public R<ShoppingCart> add(@RequestBody ShoppingCart shoppingCart){
    log.info("添加购物车: shoppingCart={}",shoppingCart);

    // 1. 设置当前用户id
    shoppingCart.setUserId(BaseContext.getCurrentId());

    LambdaQueryWrapper<ShoppingCart> lqw = new LambdaQueryWrapper<>();
    lqw.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());//还能用说明用户自登录开始,访问controller的都是一个线程啊

    // 2. 判断是菜品还是套餐
    // !!! 动态条件拼接 这都不会
    if(shoppingCart.getDishId()!=null){//菜品
        lqw.eq(ShoppingCart::getDishId,shoppingCart.getDishId());
    }else {//套餐
        lqw.eq(ShoppingCart::getSetmealId,shoppingCart.getSetmealId());
    }

    ShoppingCart cart = shoppingCartService.getOne(lqw); // 获取记录 主要为了获取原来的number
    if(cart==null){
        //  3. 购物车里没有该"userId-dishId"或者"userId-setmealId"组合  需要添加
        shoppingCart.setNumber(1); // 不设置默认值也是1
        shoppingCart.setCreateTime(LocalDateTime.now());
        shoppingCartService.save(shoppingCart);
        cart = shoppingCart;
    }else{
        //  4. 购物车里有该"userId-dishId"或者"userId-setmealId"组合  直接数量+1即可
        // 注意提交的数据没有Number值得先查 所以上面直接获取到了该条记录cart
        cart.setNumber(cart.getNumber()+1);
        shoppingCartService.updateById(cart);
    }

    return R.success(cart);//返回给前端,可能人家要用呢 (个人觉得不反回也没事儿)
}

看看自己最初写的,真的时不会编码,不会用API, 咋写成这鬼样子呢, 动态sql都不懂,save也可以加条件啊

反面代码教材

/**
 * 加入一个菜品或一个套餐到购物车
 * @param shoppingCart
 * @return
 */
@PostMapping("/add")
public R<String> add(@RequestBody ShoppingCart shoppingCart){
    log.info("添加购物车: shoppingCart={}",shoppingCart);

    // 1. 设置当前用户id
    shoppingCart.setUserId(BaseContext.getCurrentId());

    // 2. 判断是菜品还是套餐
    if(shoppingCart.getDishId()!=null){//菜品
        LambdaQueryWrapper<ShoppingCart> lqw = new LambdaQueryWrapper<>();
        lqw.eq(ShoppingCart::getUserId,shoppingCart.getUserId());
        lqw.eq(ShoppingCart::getDishId,shoppingCart.getDishId());
        ShoppingCart cart = shoppingCartService.getOne(lqw);
        if(cart==null){
            //  3. 购物车里没有该"userId-dishId"组合  需要添加
            shoppingCart.setNumber(1); // 不设置默认值也是1
            shoppingCartService.save(shoppingCart);
        }else{
            //  4. 购物车里有该"userId-dishId"组合  直接数量+1即可
            // 注意提交的数据没有Number值得先查 所以上面直接获取到了该条记录cart
            cart.setNumber(cart.getNumber()+1);
            shoppingCartService.updateById(cart);
        }
    }else {//套餐
        LambdaQueryWrapper<ShoppingCart> lqw = new LambdaQueryWrapper<>();
        lqw.eq(ShoppingCart::getUserId,shoppingCart.getUserId());
        lqw.eq(ShoppingCart::getSetmealId,shoppingCart.getSetmealId());
        ShoppingCart  cart = shoppingCartService.getOne(lqw);
        if(cart==null){
            //  3. 购物车里没有该"userId-setmealId"组合  需要添加
            shoppingCart.setNumber(1); // 不设置默认值也是1
            shoppingCartService.save(shoppingCart);
        }else{
            //  4. 购物车里有该"userId-dishId"组合  直接数量+1即可
            cart.setNumber(cart.getNumber()+1);
            shoppingCartService.updateById(cart);
        }
    }

    return R.success("加入购物车成功!");
}

测试没问题

3.5.2 查询购物车

在ShoppingCartController中创建list方法,根据当前登录用户ID查询购物车列表,并对查询的结果进行创建时间的倒序排序。

代码实现如下:

/**
 * 查询当前用户购物车列表数据
 * @return
 */
@GetMapping("/list")
public R<List<ShoppingCart>> list(){
    log.info("查询当前用户购物车");

    LambdaQueryWrapper<ShoppingCart> lqw = new LambdaQueryWrapper<>();
    lqw.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());
    // 江湖规矩 list查询得排序
    lqw.orderByDesc(ShoppingCart::getCreateTime); // 降序 后来者居上 (不过好像不好使)
    List<ShoppingCart> cartList = shoppingCartService.list();

    return R.success(cartList);
}

3.5.3 清空购物车

在ShoppingCartController中创建clean方法,在方法中获取当前登录用户,根据登录用户ID,删除购物车数据。

代码实现如下:

/**
 * 清空购物车
 * @return
 */
@DeleteMapping("/clean")
public R<String> clean(){
    log.info("清空购物车");
    LambdaQueryWrapper<ShoppingCart> lqw = new LambdaQueryWrapper<>();
    lqw.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());
    shoppingCartService.remove(lqw);
    return R.success("购物车已经清空~");
}

3.6 功能测试

按照前面分析的操作流程进行测试,测试功能以及数据库中的数据是否正常。

1). 添加购物车

当添加的是菜品信息,而这个用户的购物车中当前并没有这个菜品时,添加一条数据,数量为1。

在这里插入图片描述

检查数据库数据,由于是菜品保存的是dish_id。

在这里插入图片描述

这时在页面上,我们可以继续点击+号,在购物车中增加该菜品,此时,应该是对现有的购物车菜品数量加1,而不应该插入新的记录。
在这里插入图片描述

检查数据库数据:

在这里插入图片描述

如果添加的是套餐,该套餐在当前用户的购物车中并不存在,则添加一条数据,数量为1。

在这里插入图片描述

检查数据库数据:

在这里插入图片描述

2). 查看购物车

点击页面下面的购物车边栏,查看购物车数据列表是否正常展示。

在这里插入图片描述

3). 清空购物车

在购物车列表展示页中点击"清空", 查看购物车是否被清空。

在这里插入图片描述

并检查数据库中的数据,可以看到数据已经被删除。

3.7 更多功能

教程没做的功能这里补上

3.7.1 减少/删除购物车商品

在这里插入图片描述
ShoppingCartController 添加方法

@PostMapping("/sub")
public R<String> sub(@RequestBody ShoppingCart cart){
    log.info("购物车减少/删除商品:{}",cart);

    // 拼接查询条件
    LambdaQueryWrapper<ShoppingCart> lqw = new LambdaQueryWrapper<>();
    lqw.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());
    // 条件拼接 (java代码写动态sql 太爽了吧)
    if(cart.getDishId()!=null){
        lqw.eq(ShoppingCart::getDishId,cart.getDishId());
    }else {
        lqw.eq(ShoppingCart::getSetmealId,cart.getSetmealId());
    }
    // 先查
    cart = shoppingCartService.getOne(lqw);
    // 再判断是删除还是更新
    Integer number = cart.getNumber();
    if(number>1){
        cart.setNumber(number-1);
        shoppingCartService.updateById(cart);
    }else {
        shoppingCartService.remove(lqw);
    }

    return R.success("商品减少/删除成功!");
}

测试OK:
在这里插入图片描述

4. 下单

4.1 需求分析

移动端用户将菜品或者套餐加入购物车后,可以点击购物车中的 “去结算” 按钮,页面跳转到订单确认页面,点击 “去支付” 按钮则完成下单操作。

在这里插入图片描述

这里,我们需要说明一下,这里并不会去开发支付功能,因为不论是支付宝的支付,还是微信支付,都是需要企业资质的,而我们大家在测试的时候,是没有办法提供企业资质的,所以这一部分支付功能我们就不去实现了。

现在支付功能(微信支付宝都是)得上传营业执照才行,所以做不了喽

4.2 数据模型

用户下单业务对应的数据表为orders表和order_detail表(一对多关系,一个订单关联多个订单明细):

表名含义说明
orders订单表主要存储订单的基本信息(如: 订单号、状态、金额、支付方式、下单用户、收件地址等)
order_detail订单明细表主要存储订单详情信息(如: 该订单关联的套餐及菜品的信息)

具体的表结构如下:

A. orders 订单表

在这里插入图片描述

B. order_detail 订单明细表: 具体有哪些菜品、套餐,这些很复杂,单独一张表了

在这里插入图片描述

数据示例:

在这里插入图片描述

用户提交订单时,需要往订单表orders中插入一条记录,并且需要往order_detail中插入一条或多条记录。

4.3 前端页面分析

在开发代码之前,需要梳理一下用户下单操作时前端页面和服务端的交互过程:

在这里插入图片描述

页面跳转前端已经完成,我们无需操作。

之前地址单表管理模块写的获取默认地址,当时没用到,这里就自动生效啦~

2). 在订单确认页面,发送ajax请求,请求服务端获取当前登录用户的默认地址

在这里插入图片描述

该功能在用户地址簿管理功能开发时,已经实现,我们无需操作。

3). 在订单确认页面,发送ajax请求,请求服务端获取当前登录用户的购物车数据

在这里插入图片描述

该功能已经实现,我们无需操作。

在这里插入图片描述

经过上述的分析,我们看到前三步的功能我们都已经实现了,我们主要需要实现最后一步的下单功能,该功能具体的请求信息如下:

请求说明
请求方式POST
请求路径/order/submit
请求参数{“remark”:“老板,记得带一次性筷子”,“payMethod”:1,“addressBookId”:“1425792459560005634”}

4.4 准备工作

在开发业务功能前,先将需要用到的类和接口基本结构创建好:

MP代码生成器-CodeGenerator2生成然后修改即可

1). 实体类 Orders、OrderDetail

所属包: cn.whu.reggie.entity

修改驼峰命名: userId、addressBookId、orderTime、checkoutTime、payMethod、userName

@Data
@EqualsAndHashCode(callSuper = false)
public class Orders implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 主键
     */
    private Long id;

    /**
     * 订单号
     */
    private String number;

    /**
     * 订单状态 1待付款,2待派送,3已派送,4已完成,5已取消
     */
    private Integer status;

    /**
     * 下单用户
     */
    private Long userId;

    /**
     * 地址id
     */
    private Long addressBookId;

    /**
     * 下单时间
     */
    private LocalDateTime orderTime;

    /**
     * 结账时间
     */
    private LocalDateTime checkoutTime;

    /**
     * 支付方式 1微信,2支付宝
     */
    private Integer payMethod;

    /**
     * 实收金额
     */
    private BigDecimal amount;

    /**
     * 备注
     */
    private String remark;

    private String phone;

    private String address;

    private String userName;

    private String consignee;
}

修改驼峰命名: OrderDetail、orderId、dishId、setmealId、dishFlavor、

@Data
@EqualsAndHashCode(callSuper = false)
public class OrderDetail implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 主键
     */
    private Long id;

    /**
     * 名字
     */
    private String name;

    /**
     * 图片
     */
    private String image;

    /**
     * 订单id
     */
    private Long orderId;

    /**
     * 菜品id
     */
    private Long dishId;

    /**
     * 套餐id
     */
    private Long setmealId;

    /**
     * 口味
     */
    private String dishFlavor;

    /**
     * 数量
     */
    private Integer number;

    /**
     * 金额
     */
    private BigDecimal amount;

}

2). Mapper接口 OrderMapper、OrderDetailMapper

所属包: cn.whu.reggie.mapper

先删除xml目录,然后分别加上@Mapper注解

@Mapper
public interface OrdersMapper extends BaseMapper<Orders> {
}

还需要修改驼峰命名: OrderDetailMapper

@Mapper
public interface OrderDetailMapper extends BaseMapper<OrderDetail> {

}

3). 业务层接口 OrderService、OrderDetailService

所属包: cn.whu.reggie.service

修改命名: OrdersService (去掉前缀I)

import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.reggie.entity.Orders;

public interface OrderService extends IService<Orders> {
}

修改命名: OrderDetailService (去点前缀I以及改为驼峰命名)

public interface OrderDetailService extends IService<OrderDetail> {

}

4). 业务层实现类 OrderServiceImpl、OrderDetailServiceImpl

所属包: cn.whu.reggie.service.impl

不需要修改

@Service
public class OrdersServiceImpl extends ServiceImpl<OrdersMapper, Orders> implements OrdersService {
}

修改为驼峰命名: OrderDetailServiceImpl

@Service
public class OrderDetailServiceImpl extends ServiceImpl<OrderDetailMapper, OrderDetail> implements OrderDetailService {

}

5). 控制层 OrderController、OrderDetailController

所属包: cn.whu.reggie.controller

加上日志注解,注入一下service
命名和路径,都把s去掉:、 /order

@Slf4j
@RestController
@RequestMapping("/order")
public class OrderController{    
    @Autowired
    private OrdersService ordersService;

}

加上日志注解: @Slf4j
命名改为小驼峰: orderDetail、OrderDetailController
注入service

@Slf4j
@RestController
@RequestMapping("/orderDetail")
public class OrderDetailController {
    
    @Autowired
    private OrderDetailService orderDetailService;

}

4.5 代码开发 (复杂,小心 涉及5张表(查3张写2张1删)信息 慢慢查 慢慢封装)

在OrderController中创建submit方法,处理用户下单的逻辑 :

/**
 * 用户下单
 * @param orders
 * @return
 */
@PostMapping("/submit")
public R<String> submit(@RequestBody Orders orders){
    log.info("订单数据:{}",orders);
    orderService.submit(orders);
    return R.success("下单成功");
}

由于下单的逻辑相对复杂,我们可以在OrderService中定义submit方法,来处理下单的具体逻辑:

/**
* 用户下单
* @param orders
*/
public void submit(Orders orders);

然后在OrderServiceImpl中完成下单功能的具体实现,下单功能的具体逻辑如下:

A. 获得当前用户id, 查询当前用户的购物车数据

B. 根据当前登录用户id, 查询用户数据

C. 根据地址ID, 查询地址数据

D. 组装订单明细数据, 批量保存订单明细

E. 组装订单数据, 批量保存订单数据

F. 删除当前用户的购物车列表数据

具体代码实现如下:

注意用户下单涉及3张表的DML(2insert+1delete),必然需要事务@Transactional

@Service
public class OrdersServiceImpl extends ServiceImpl<OrdersMapper, Orders> implements OrdersService {

    @Autowired
    private AddressBookService addressBookService;

    @Autowired
    private ShoppingCartService shoppingCartService;

    @Autowired
    private OrderDetailService orderDetailService;

    @Autowired
    private UserService userService;

    /**
     * 用户下单
     * 由于支付个人申请不了接口,于是就将 orders表 和 order_detail 表 填好即可
     * orders表一些数据需要计算,一些数据要到address_book表里查
     * order_detail要拿着当前用户id到购物车表shopping_cart里去查
     * @param orders
     */
    @Transactional
    public void submit(Orders order) {
        // 0. 准备信息
        // 0.1 当前用户
        Long userId = BaseContext.getCurrentId();
        User user = userService.getById(userId);

        // 0.2 地址信息
        Long addressBookId = order.getAddressBookId();
        AddressBook addressBook = addressBookService.getById(addressBookId);
        // 0.3 购物车里的订单详情
        LambdaQueryWrapper<ShoppingCart> lqw = new LambdaQueryWrapper<>();
        lqw.eq(ShoppingCart::getUserId, userId);
        List<ShoppingCart> carts = shoppingCartService.list(lqw);
        // 0.4 订单号 工具类先生成
        long orderId = IdWorker.getId();// mybatisPlus 提供的生产订单号等字段的工具类

        // 0.5 遍历一次购物车carts, 把需要得东西都一次计算/封装好
        // 用AtomicInteger来计算总价Amount 他做了原子维护,是原子操作 即使多线程地算也不会算错
        AtomicInteger amount = new AtomicInteger(0);
        List<OrderDetail> orderDetails = carts.stream().map(cart -> {
            // 总价amount也比较麻烦 // sql也行 java代码也行 反正要遍历carts 不如一次性全弄好
            BigDecimal number = new BigDecimal(cart.getNumber());
            amount.addAndGet(cart.getAmount().multiply(number).intValue());
            // 注意套餐肯定有优惠的,所以amount得自己填 商家自己可要填准了

            OrderDetail orderDetail = new OrderDetail();
            orderDetail.setOrderId(orderId);
            BeanUtils.copyProperties(cart, orderDetail,"id");//主键id是要自动生成的,不能有值
            return orderDetail;
        }).collect(Collectors.toList());

        // 更健壮
        if(carts==null||carts.size()==0) throw new CustomException("购物车为空,无法下单!");
        if(addressBook==null) throw new CustomException("用户地址信息有误,无法下单!");

        // 1. order表 (插入一条数据)

        order.setId(orderId); // 设置了id,insert时就不会再雪花算法生成了
        order.setNumber(String.valueOf(orderId)); // 订单号和id保持一致
        order.setStatus(2);//付款肯定也是这里做的 所以假设已经付款 待派送
        order.setUserId(userId);
        order.setOrderTime(LocalDateTime.now());
        order.setCheckoutTime(LocalDateTime.now());//还没结账啊 暂时就赋值这个吧
        order.setPhone(addressBook.getPhone());
        order.setConsignee(addressBook.getConsignee());
        order.setUserName(user.getName());
        order.setAmount(new BigDecimal(amount.get())); // 总价得根据购物车计算啊
        // 地址拼接有点麻烦
        order.setAddress((addressBook.getProvinceName() == null ? "" : addressBook.getProvinceName())
                + (addressBook.getCityName() == null ? "" : addressBook.getCityName())
                + (addressBook.getDistrictName() == null ? "" : addressBook.getDistrictName())
                + (addressBook.getDetail() == null ? "" : addressBook.getDetail()));
        // 最后订单表插入数据
        this.save(order);

        // 2. order_detail (插入多条数据   和order表多对一关系 当然由order_detail来维护了)
        // 订单明细表插入数据
        orderDetailService.saveBatch(orderDetails);

        // 3. 最后下单完毕,清空购物车
        shoppingCartService.remove(lqw);
    }
}

备注:

​ 上述逻辑处理中,计算购物车商品的总金额时,为保证我们每一次执行的累加计算是一个原子操作,我们这里用到了JDK中提供的一个原子类 AtomicInteger

4.6 功能测试

代码编写完成,我们重新启动服务,按照前面分析的操作流程进行测试,查看数据是否正常即可。在测试过程中,我们可以通过debug的形式来跟踪代码的正常执行。

在这里插入图片描述

检查数据库数据

订单表插入一条记录:

在这里插入图片描述

订单明细表插入6条记录():

在这里插入图片描述

同时,购物车的数据被删除: (就像是被移到了订单明细表被长期保存起来一样)
在这里插入图片描述

4.7 更多功能

教程没有实现的功能,这里给他补上

4.7.1 查看订单

在这里插入图片描述

在这里插入图片描述

OrderController

/**
 * 用户订单查询
 * @param page
 * @param pageSize
 * @return
 */
@GetMapping("/userPage")
public R<Page> userPage(Integer page,Integer pageSize){
    log.info("用户订单查询");
    Page<Orders> pageInfo = new Page<>(page,pageSize);
    ordersService.page(pageInfo);
    return R.success(pageInfo);
}

测试OK: (前端就这么设计的,没有做分页栏)

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/425186.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

关于二叉树访问顺序的选择题

第一题&#xff1a; 从题目中我们可以得出这是棵完全二叉树&#xff08;最后一列不满&#xff0c;且连续&#xff09;&#xff0c;层次顺序。 我们可以推出树的形状&#xff1a; 我们现根据层次遍历的性质画出出栈的图&#xff1a; ABCDEFGH 最开始是A所以第一个位置为A&…

STC单片机波特率初值计算以及通用串口配置程序

STC单片机波特率初值计算以及通用串口配置程序&#x1f516;这里以STC15手册上的数据展开。&#x1f530;串口模式一&#xff1a;可变波特率8位数据方式。以为最为常用的配置模式。&#x1f33f;定时器1模式0:16位定时器计数器&#x1f33f;定时器1模式2:8位定时器计数器 ✨当然…

linux高级存储功能

高级存储功能 1、Stratis管理分层存储 通过Stratis&#xff0c;便捷的使用精简配置&#xff08;thin provisioning&#xff09;&#xff0c;快照&#xff08;snapshots&#xff09;和基于池&#xff08;pool-based&#xff09;的管理和监控的等高级存储功能。 &#xff08;1&a…

加拿大亚马逊FBA海运收费标准有哪些

众所周知&#xff0c;加拿大亚马逊的海运物流费用一般有头程运费、尾端派送费和仓储费等其他费用。那么&#xff0c;加拿大亚马逊FBA海运收费标准有哪些呢?接下来一起来了解下加拿大亚马逊FBA海运收费标准和费用。加拿大亚马逊FBA海运收费标准有哪些? 加拿大亚马逊FBA海运收费…

《Java8实战》第6章 用流收集数据

collect 是一个归约操作&#xff0c;就像 reduce 一样可以接受各种做法作为参数&#xff0c;将流中的元素累积成一个汇总结果。具体的做法是通过定义新的Collector 接口来定义的&#xff0c;因此区分 Collection、Collector 和 collect 是很重要的。用 collect 和收集器能够做什…

PADS-微处理器、USB转UART芯片、MINI-USB PCB封装设计

目录 1 微处理器PCB封装设计 2 USB转UART芯片PCB封装设计 3 MINI-USB PCB封装设计 4 添加验证 详细步骤不再文字说明&#xff0c;直接截取关键步骤截图 1 微处理器PCB封装设计 查看芯片手册 器件高度 器件高度1.6 管脚中心点间距0.5&#xff0c;管脚焊盘长1.2、宽0.3&…

[ 云计算 | Azure ] Chapter 04 | 核心体系结构之数据中心、区域与区域对、可用区和地理区域

本章节主要内容进行讲解&#xff1a;Azure云计算的核心体系结构组件中的&#xff1a;Azure物理基础设施&#xff08;Physical infrastructure&#xff09;&#xff0c;区域&#xff08;Regions&#xff09;和区域对&#xff08;Region Pairs&#xff09;、地理数据中心&#xf…

一文解决ethtool 原理介绍和解决网卡丢包排查思路

前言 之前记录过处理因为 LVS 网卡流量负载过高导致软中断发生丢包的问题&#xff0c;RPS 和 RFS 网卡多队列性能调优实践[1]&#xff0c;对一般人来说压力不大的情况下其实碰见的概率并不高。这次想分享的话题是比较常见服务器网卡丢包现象排查思路&#xff0c;如果你是想了解…

UE 简单插件制作

本文主要是提供几个写UE插件的实例&#xff0c;借此来了解在UE里使用C创建自定义插件的做法&#xff1a; 写一个使场景变暗的简单插件写一个自定义窗口&#xff0c;展示项目里所有的动画资产 写一个使场景变暗的简单插件 参考&#xff1a;Unreal Engine 5 - Writing Plugins …

统信UOS专业版系统安装教程 - 手动分区安装UOS系统

全文导读&#xff1a;本文主要介绍了安装UOS系统过程中使用手动分区安装方法&#xff0c;一般没有特殊要求建议使用全盘安装UOS系统。 准备环境 制作好统信UOS专业版启动U盘 一台CPU频率≥2GHz、内存≥4GB、硬盘≥64GB的电脑 安装步骤 一、制作UOS 系统启动盘 制作UOS 系…

自动驾驶TPM技术杂谈 ———— CCRT验收标准(评分标准)

文章目录介绍评价方法指标体系算分方法一级指标二级指标三级指标四级指标五级指标行车辅助能力得分说明跟车能力得分说明前车静止识别与响应得分说明前车低速识别与响应得分说明前车减速识别与响应得分说明前车切入识别与响应得分说明前车切出识别与响应得分说明跟随前车起停得…

数据结构小知识------时间与空间复杂度

本章思维导图&#xff1a; 一&#xff0c;时间复杂度 1.1时间复杂度的概念 &#x1f310;&#xff1a;什么是时间复杂度呢&#xff1f;时间复杂度其实就是一个程序运行时它的指令运行的次数。 在这里&#xff0c;程序默认每条指令的运行时间是一样的。所以时间复杂度就可以理解…

【云原生进阶之容器】第六章容器网络6.4.2--Flannel的安装与部署

1 flannel的安装与部署 见链接一篇文章带你了解Flannel - Flannel - 操作系统 - 深度开源 1.1 部署环境规划 1.2 安装部署 #tar -xf flannel-v0.13.0.tar.gz #mv /apps/svr/flannel-v0.13.0 #ln –svfn /apps/svr/flannel-v0.13.0 /apps/svr/flannel 1.2.1 调整Flannel配置…

设计模式(超详细)

设计模式 原则 什么是SOLID原则&#xff1f; S单一职责SRP Single-Responsibility Principle 一个类,最好只做一件事,只有一个引起它的变化。单一职责原则可以看做是低耦合,高内聚在面向对象原则的引申,将职责定义为引起变化的原因,以提高内聚性减少引起变化的原因。 比如…

项目资源管理流程:五步专业指南

项目资源管理是描述大多数项目经理的一项关键职能的方式——收集完成工作所需的团队成员、设备和其他材料&#xff08;也称为资源&#xff09;。 以下是项目资源管理的步骤清单&#xff1a; 步骤1&#xff1a;资源规划 为了确定完成项目的资源需求&#xff0c;你首先需要了…

SpringCloud-Gateway实现网关

网关作为流量的入口&#xff0c;常用的功能包括路由转发、权限校验、限流等Spring Cloud 是Spring官方推出的第二代网关框架&#xff0c;由WebFluxNettyReactor实现的响应式的API网关&#xff0c;它不能在传统的servlet容器工作&#xff0c;也不能构建war包。基于Filter的方式提…

个人开发者如何选择阿里云服务器配置CPU内存带宽?

阿里云服务器个人用怎么选择&#xff1f;云服务器吧建议选择ECS共享型s6&#xff0c;不限制CPU性能&#xff0c;选择1核2G或2核4G都可以&#xff0c;云服务器s6处理器采用2.5 GHz主频的Intel Xeon Platinum 8269CY&#xff08;Cascade Lake&#xff09;&#xff0c;睿频3.2 GHz…

【论文阅读--WSOL】Spatial-Aware Token for Weakly Supervised Object Localization

文章目录方法实验Limitation论文&#xff1a;https://arxiv.org/abs/2303.10438代码&#xff1a;https://github.com/wpy1999/SAT/blob/main/Model/SAT.py方法 这篇文章的方法应该属于FAM这一类。 额外添加的一个spatial token&#xff0c;从第10-12层开始&#xff0c;利用其得…

Vue3技术1之Vue3简介、创建Vue3工程、分析工程结构、安装开发者工具与初识setup

Vue3技术1Vue3简介发展提升创建Vue3工程使用vue-cli创建使用vite创建分析工程结构&#xff08;由vue-cli创建的&#xff09;main.jsvue.config.jsApp.vue安装开发者工具初识setupsetup的两种返回值返回一个对象App.vue返回一个函数App.vueVue2与Vue3混合使用App.vue总结Vue3简介…

【致敬未来的攻城狮计划】— 连续打卡第一天:提前对CPK_RA2E1是瑞萨RA系列开发板的初体验,了解一下(文字上的初理解)

系列文章目录 系列文章目录 前言 一、瑞萨MCU&#xff08;CPK_RA2E1是瑞萨RA系列开发板&#xff09;是什么&#xff1f; 首先引入是什么&#xff1f; 他的优势在哪&#xff1f; 瑞萨CPK_RA2E1 对标stm32 相似之处和不同之处&#xff1f; 瑞萨CPK_RA2E1如何开发&#xff…