文章目录
- 外卖项目-第六天
- 课程内容
- 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 功能测试
- 4. 下单
- 4.1 需求分析
- 4.2 数据模型
- 4.3 前端页面分析
- 4.4 准备工作
- TODO
- 4.5 代码开发
- 4.6 功能测试
外卖项目-第六天
课程内容
- 用户地址簿功能
- 菜品展示
- 购物车
- 下单
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). 清空购物车
在购物车列表展示页中点击"清空", 查看购物车是否被清空。
并检查数据库中的数据,可以看到数据已经被删除。
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;
}
TODO
4.5 代码开发
在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. 删除当前用户的购物车列表数据
具体代码实现如下:
@Autowired
private ShoppingCartService shoppingCartService;
@Autowired
private UserService userService;
@Autowired
private AddressBookService addressBookService;
@Autowired
private OrderDetailService orderDetailService;
/**
* 用户下单
* @param orders
*/
@Transactional
public void submit(Orders orders) {
//获得当前用户id
Long userId = BaseContext.getCurrentId();
//查询当前用户的购物车数据
LambdaQueryWrapper<ShoppingCart> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(ShoppingCart::getUserId,userId);
List<ShoppingCart> shoppingCarts = shoppingCartService.list(wrapper);
if(shoppingCarts == null || shoppingCarts.size() == 0){
throw new CustomException("购物车为空,不能下单");
}
//查询用户数据
User user = userService.getById(userId);
//查询地址数据
Long addressBookId = orders.getAddressBookId();
AddressBook addressBook = addressBookService.getById(addressBookId);
if(addressBook == null){
throw new CustomException("用户地址信息有误,不能下单");
}
long orderId = IdWorker.getId();//订单号
AtomicInteger amount = new AtomicInteger(0);
//组装订单明细信息
List<OrderDetail> orderDetails = 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(userId);
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(wrapper);
}
备注:
上述逻辑处理中,计算购物车商品的总金额时,为保证我们每一次执行的累加计算是一个原子操作,我们这里用到了JDK中提供的一个原子类 AtomicInteger
4.6 功能测试
代码编写完成,我们重新启动服务,按照前面分析的操作流程进行测试,查看数据是否正常即可。在测试过程中,我们可以通过debug的形式来跟踪代码的正常执行。
检查数据库数据
订单表插入一条记录:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dedBvIN1-1681565780759)(assets/image-20210814084925524.png)]
订单明细表插入四条记录():
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W76dqHL8-1681565780759)(assets/image-20210814085019401.png)]
同时,购物车的数据被删除:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EFkeLL7T-1681565780759)(assets/image-20210814085058814.png)]