本笔记源自黑马程序员的视频课程——《瑞吉外卖》,总结了课程笔记、相关知识点以及可能遇到的问题解决方案,并且增加了课程中未实现的功能,供读者参考。笔记全面且条理清晰,希望帮助读者学习和理解这个外卖项目。
本项目全部笔记见:外卖项目笔记合集
1. 套餐管理业务开发
1.1 新增套餐
1.1.1 需求分析
套餐就是菜品的集合。
后台系统中可以管理套餐信息,通过”新增套餐”功能来添加一个新的套餐,在添加套餐时,需要选择当前套餐所属的套餐分类和包含的菜品,并且需要上传套餐对应的图片,在移动端会按照套餐分类来展示对应的套餐。

1.1.2 数据模型
新增套餐,其实就是将新增页面录入的套餐信息插入到setmeal表,还需要向setmeal_dish表插入套餐和菜品关联数据。所以在新增套餐时,涉及到两个表:
1、setmeal表(套餐表)

2、setmeal_dish表(套餐-菜品关系表)

1.1.3 准备工作
在开发业务功能前,先将需要用到的类和接口基本结构创建好:
1、实体类SetmealDish(从课程资料中导入,Setmeal实体类前面已经导入过了)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| @Data public class SetmealDish implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private Long setmealId;
private Long dishId;
private String name; @Data public class SetmealDto extends Setmeal {
private List<SetmealDish> setmealDishes;
private String categoryName; } private BigDecimal price;
private Integer copies;
private Integer sort;
@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、DTO SetmealDto(从课程资料中导入)
1 2 3 4 5 6 7
| @Data public class SetmealDto extends Setmeal {
private List<SetmealDish> setmealDishes;
private String categoryName; }
|
3、Mapper接口SetmealDishMapper
1 2 3
| @Mapper public interface SetmealDishMapper extends BaseMapper<SetmealDish>{ }
|
4、业务层接口SetmealDishService
1 2
| public interface SetmealDishService extends IService<SetmealDish> { }
|
5、业务层实现类SetmealDishServicelmpl
1 2 3 4 5 6 7 8
| @Service @Slf4j public class SetmealServiceImpl extends ServiceImpl<SetmealMapper, Setmeal> implements SetmealService {
@Autowired private SetmealDishService setmealDishService; }
|
6、控制层SetmealController
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Slf4j @RestController @RequestMapping("/setmeal") public class SetmealController { @Autowired private SetmealService setmealService;
@Autowired private SetmealDishService setmealDishService;
@Autowired private CategoryService categoryService; }
|
1.1.4 梳理交互过程及代码开发
在开发代码之前,需要梳理一下新增套餐时,前端页面和服务端的交互过程:
1、点击“新建套餐”,会请求页面backend/page/combo/add.html,并发送ajax请求/category/list?type=2&page=1&pageSize=1000
,请求服务端获取套餐分类数据并展示到下拉框中。此处已经在CategoryController中写过了,可以直接获取到套餐分类。

2、页面发送ajax请求/category/list?type=1
,请求服务端获取菜品分类数据并展示到添加菜品窗口中。此处已经在CategoryController中写过了,可以直接获取到套餐分类。
3、页面发送ajax请求/dish/list?categoryId=xxx(第一个菜品分类id)
,请求服务端,根据菜品分类,查询第一个菜品分类对应的所有菜品数据。这样一来,点击“套餐菜品:添加菜品”时,在弹出的页面内可以直接显示第一个分类的所有菜品。效果如下:

并且,在“添加菜品”弹出页面内选择其他菜品分类时,也会发送请求到/dish/list?categoryId=xxx(对应的菜品分类id)
。
需要服务器处理该请求,获取该分类id的所有菜品对象,并响应回来,展示菜品在页面上。
代码开发:
在DishController中新增list方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
@GetMapping("/list") public R<List<Dish>> list(Dish dish){
LambdaQueryWrapper<Dish> lambdaQueryWrapper=new LambdaQueryWrapper<>(); lambdaQueryWrapper.eq(Dish::getStatus,1); lambdaQueryWrapper.eq(dish.getCategoryId()!=null, Dish::getCategoryId, dish.getCategoryId()); lambdaQueryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
List<Dish> list = dishService.list(lambdaQueryWrapper);
return R.success(list); }
|
说明:
该方法的返回值:由于查出的是多个菜品dish对象,所以用List集合
4、页面发送请求进行图片上传,请求服务端将图片保存到服务器。已经在前面写过。
5、页面发送请求进行图片下载,将上传的图片进行回显。已经在前面写过。
6、点击“保存”按钮,发送ajax请求,将套餐相关数据以json形式提交到服务端。发送请求给/setmeal
,method=post。需要服务器保存套餐相关数据,并返回响应。
“保存套餐”的实现思路:
json发来的数据里,有Setmeal类的属性categoryId, name, price, status, code, description, image,也有Setmeal类中没有的setmealDishes属性,所以控制器方法形参要用SetmealDto来接收,SetmealDto类中有setmealDishes属性。
在控制器方法中,需要做以下这些事:
(1)把json数据中,属于Setmeal类的属性category_id, name, price, status, code, description, image保存到setmeal表;
(2)保存(insert)后,由框架生成该setmeal的主键id(该套餐的id)
(3)把刚才自动生成的setmeal_id(setmeal表的主键id)值,逐个赋给setmealDishes对象的setmeal_id属性,就能够对应上同一套餐和多个菜品了。
(4)而setmeal_dish表还需要插入dish_id, name, price, copies等字段的数据,这些都由setmealDishes属性(List类型)封装,可以直接通过setmealDishService.saveBatch(setmealDishes)新增数据。

代码开发:
1、在SetmealController新增save方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @RestController @RequestMapping("/setmeal") @Slf4j public class SetmealController { @Autowired private SetmealService setmealService;
@Autowired private SetmealDishService setmealDishService;
@PostMapping public R<String> save(@RequestBody SetmealDto setmealDto){ log.info("套餐信息: {}",setmealDto); setmealService.saveWithDish(setmealDto); return R.success("新增套餐成功"); } }
|
2、在SetmealService中新增saveWithDish方法,并在SetmealServiceImpl中实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| @Service public class SetmealServiceImpl extends ServiceImpl<SetmealMapper, Setmeal> implements SetmealService{
@Autowired private SetmealDishService setmealDishService;
@Override @Transactional public void saveWithDish(SetmealDto setmealDto) { this.save(setmealDto);
List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes(); setmealDishes = setmealDishes.stream().map((item) ->{ item.setSetmealId(setmealDto.getId()); return item; }).collect(Collectors.toList());
setmealDishService.saveBatch(setmealDishes); } }
|
1.2 套餐分页查询
1.2.1 需求分析
系统中的套餐数据很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。
1.2.2 梳理交互过程
在开发代码之前,需要梳理一下套餐分页查询时前端页面和服务端的交互过程:
1、页面(backend/page/combo/list.html)发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端,获取分页数据。
这一步的具体过程:
list.html加载后执行钩子函数created(),其中执行init()方法,将page、pageSize、name赋值,并封装在params中,执行getSetmealPage(params)方法,发GET请求到/setmeal/page
,携带params参数,并需要服务器响应回来分页信息pageInfo。前端相关代码:

2、页面发送请求,请求服务端进行图片下载,用于页面图片展示。前面已经处理过。
1.2.3 代码开发
在SetmealController中新增page方法,处理1.2.2中的第1条请求即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
|
@GetMapping("/page") public R<Page> page(int page, int pageSize, String name){ Page<Setmeal> pageInfo = new Page<>(page, pageSize); Page<SetmealDto> setmealDtoPage = new Page<>();
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.like(StringUtils.isNotEmpty(name), Setmeal::getName, name); queryWrapper.orderByDesc(Setmeal::getUpdateTime); setmealService.page(pageInfo, queryWrapper);
BeanUtils.copyProperties(pageInfo, setmealDtoPage, "records");
List<Setmeal> records = pageInfo.getRecords(); List<SetmealDto> list = records.stream().map((item)->{ SetmealDto setmealDto = new SetmealDto();
BeanUtils.copyProperties(item, setmealDto); Long categoryId = item.getCategoryId(); Category category = categoryService.getById(categoryId); if(category != null){ String categoryName = category.getName(); setmealDto.setCategoryName(categoryName); } return setmealDto; }).collect(Collectors.toList());
setmealDtoPage.setRecords(list);
return R.success(setmealDtoPage); }
|
1.3 删除套餐(含批量操作)
1.3.1 需求分析
在套餐管理列表页面,点击“删除”按钮,可以删除对应的套餐信息。也可以通过复选框选择多个套餐,点击“批量删除”按钮,一次删除多个套餐。

注意,对于状态为“售卖中”的套餐不能删除,需要先停售,再删除。
1.3.2 梳理交互过程
在开发代码之前,需要梳理一下删除套餐时前端页面和服务端的交互过程:
1、删除单个套餐时,页面发送ajax请求,根据套餐id删除对应套餐

2、删除多个套餐时,页面发送ajax请求,根据提交的多个套餐id删除对应套餐

开发删除套餐功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。
观察删除单个套餐和批量删除套餐的请求信息可以发现,两种请求的地址和请求方式都是相同的,不同的则是传递的id个数,所以在服务端可以提供一个方法来统一处理。
1.3.3 代码开发
需要服务器对请求进行处理,从数据库中删除id对应的套餐,并响应回前端。
在SetmealController中新增delete方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
|
@Transactional @DeleteMapping public R<String> delete(String[] ids){ int count = 0; for(String id : ids){ Setmeal setmeal = setmealService.getById(id); if(setmeal.getStatus() == 0){ setmealService.removeById(id);
LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(SetmealDish::getSetmealId, id); setmealDishService.remove(queryWrapper); } else { count++; } } if(count > 0 && count == ids.length){ return R.error("选中的套餐均为'启售'状态,不可删除"); }else { return R.success("套餐删除成功") } }
|
1.4 启售/停售套餐(含批量操作)
这一节为自己补充实现的功能,经测试暂未发现问题,可供参考。如有问题欢迎讨论。
1.4.1 需求分析
点“启售/停售”,或者选中多个勾选框后,点击“批量启售/批量停售”,并点击“确认”后,发送/setmeal/status/0?ids=xxx到服务器,需要服务器处理。

服务器需要先根据id找到该套餐,再更改setmeal实体对象的status,然后用实体更新setmeal表中的数据。
1.4.2 代码开发
在SetmealController中新增switchStatus方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
@PostMapping("/status/{status}") public R<String> switchStatus(@PathVariable Integer status, String[] ids){ for (String id : ids){ Setmeal setmeal = setmealService.getById(id); setmeal.setStatus(status); setmealService.updateById(setmeal); } return R.success("状态修改成功"); }
|
1.5 修改套餐
这一节为自己补充实现的功能,经测试暂未发现问题,可供参考。如有问题欢迎讨论。
1.5.1 需求分析

在套餐管理列表页面的某套餐一行,点击”修改”按钮,请求/backend/page/combo/add.html?id=xxx,跳转到修改套餐页面。需要服务端在该页面回显该套餐信息。用户进行修改后点击“确定”按钮,完成修改操作。

1.5.2 梳理交互过程
在开发代码之前,需要梳理一下修改套餐时,前端页面和服务端的交互过程:
1、在list.html页面点击“修改”,执行addSetMeal(scope.row.id)方法,请求/backend/page/combo/add.html?id=xxxx
页面,其中id是要修改的这一行套餐的id。
2、在add.html页面加载完成后,执行钩子函数,并执行getDishTypeList()方法,请求/category/list?type=2&page=1&pageSize=1000
,method=get,请求获取所有的套餐分类(商务套餐/儿童套餐)。在前面已经写过,此处可以直接获取到。

3、在add.html页面加载完成后,执行钩子函数,并执行getDishType()方法,请求/category/list?type=1
,method=get,获取“+添加菜品”按钮处要显示的所有的菜品分类。在前面已经写过,此处可以直接获取到。

4、在add.html页面加载完成后,执行钩子函数(见第2步后的图)。由于url中有id,所以执行init()方法,其中执行querySetmealById(this.id)方法,请求/setmeal/套餐id
,请求方式GET,获取套餐信息以回显到页面。
需要服务器查询,并响应该套餐id的name, price, categoryName(第3步已经获取到所有的category对象), status, categoryId, setmealDishes, image, description等信息,封装到data中传回前端,以回显到修改页面上。
5、同时请求/dish/list?categoryId=xxxx
,获取“+添加菜品”按钮处要显示的某分类的所有菜品。已经写过。
6、同时请求服务端进行图片下载,用于页图片回显。已经写过。
7、填写表单后,点击“保存”,发送json请求给/setmeal
,请求方式PUT。需要服务器处理,将修改后的信息保存到数据表。
1.5.3 代码开发
我们需要处理1.5.2中的第4条和第7条的请求,其余的已经写过。
1、(对应1.5.2中第4条)修改页面的信息回显:
处理请求/setmeal/1633386599796461569
,路径参数是套餐的id。
服务端需要将该套餐id对应的name, price, categoryName(1.5.2的第3条中已经获取到), setmealDishes, image, description等信息响应给前端。

SetmealController中:
1 2 3 4 5 6 7 8 9 10
|
@GetMapping("/{id}") public R<SetmealDto> querySetmeal(@PathVariable Long id){ SetmealDto setmealDto = setmealService.getByIdWithDish(id); return R.success(setmealDto); }
|
SetmealService中添加getByIdWithDish方法,并在SetmealServiceImpl中实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
@Override @Transactional public SetmealDto getByIdWithDish(Long id) { Setmeal setmeal = this.getById(id);
SetmealDto setmealDto = new SetmealDto(); BeanUtils.copyProperties(setmeal, setmealDto);
LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(SetmealDish::getSetmealId, id); List<SetmealDish> list = setmealDishService.list(queryWrapper); setmealDto.setSetmealDishes(list);
return setmealDto; }
|
2、(对应前端第7条)将修改后的信息保存到数据表
填写表单后,点击“保存”,发送json请求给/setmeal
,请求方式PUT。需要服务器处理,将新的信息更新到数据表。更新仍然使用先删除再修改的方法,需要补充setmeal_id,才能和套餐对应上。
SetmealController中:
1 2 3 4 5 6 7 8 9 10 11
|
@PutMapping public R<String> update(@RequestBody SetmealDto setmealDto){ setmealService.updateWithDish(setmealDto); return R.success("修改成功"); }
|
SetmealService中添加getByIdWithDish方法,并在SetmealServiceImpl中实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
@Override @Transactional public void updateWithDish(SetmealDto setmealDto) { this.updateById(setmealDto);
LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(SetmealDish::getSetmealId, setmealDto.getId()); setmealDishService.remove(queryWrapper);
List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
setmealDishes = setmealDishes.stream().map((item) -> { item.setSetmealId(setmealDto.getId()); return item; }).collect(Collectors.toList());
setmealDishService.saveBatch(setmealDishes); }
|
2. 手机验证码登录
2.1 短信发送
2.1.1 短信服务介绍
目前市面上有很多第三方提供的短信服务,这些第三方短信服务会和各个运营商(移动、联通、电信)对接,我们只需要注册成为会员并且按照提供的开发文档进行调用就可以发送短信。需要说明的是,这些短信服务一般都是收费服务。
常用短信服务:
2.1.2 阿里云短信服务-介绍
阿里云短信服务(Short Message Service)是广大企业客户快速触达手机用户所优选使用的通信能力。调用API或用群发助手,即可发送验证码、通知类和营销类短信;国内验证短信秒级触达,到达率最高可达99%;国际/港澳台短信覆盖200多个国家和地区,安全稳定,广受出海企业选用。
应用场景:
2.1.3 阿里云短信服务-注册账号
阿里云官网: https://www.aliyun.com/
在官网首页进行注册。
2.1.4 阿里云短信服务-设置短信签名
注册成功后,点击登录按钮进行登录。登录后进入短信服务管理页面,选择国内消息菜单。


短信签名是短信发送者的署名,表示发送方的身份。
2.1.5 阿里云短信服务-设置短信模板
切换到【模板管理】标签页:

短信模板包含短信发送内容、场景、变量信息。
2.1.6 阿里云短信服务-设置AccessKey
1、光标移动到用户头像上,在弹出的窗口中点击【AccessKey管理】∶

2、点击“开始使用子用户AccessKey”(非管理员的权限,更安全)

3、创建用户:创建用户-填写登录名、显示名-勾选“编程访问”-会生成AccessKey ID & AccessKey Secret,复制存起来。

4、设置权限:点击刚创建的用户-权限管理-添加权限-搜索“SMS“,选择“AliyunDysmsFullAccess”和“AliyunDysmsReadOnlyAccess”两个权限

2.1.7 代码开发
使用阿里云短信服务发送短信,可以参照官方提供的文档即可。
具体开发步骤:
1、导入maven坐标
1 2 3 4 5 6 7 8 9 10 11
| <dependency> <groupId>com.aliyun</groupId> <artifactId>aliyun-java-sdk-core</artifactId> <version>4.5.16</version> </dependency> <dependency> <groupId>com.aliyun</groupId> <artifactId>aliyun-java-sdk-dysmsapi</artifactId> <version>2.1.0</version> </dependency>
|
2、调用API(此处暂时了解即可,老师暂时没有用短信验证,是使用控制台进行验证的)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
public class SMSUtils {
public static void sendMessage(String signName, String templateCode,String phoneNumbers,String param){ DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", "", ""); IAcsClient client = new DefaultAcsClient(profile);
SendSmsRequest request = new SendSmsRequest(); request.setSysRegionId("cn-hangzhou"); request.setPhoneNumbers(phoneNumbers); request.setSignName(signName); request.setTemplateCode(templateCode); request.setTemplateParam("{\\"code\\":\\""+param+"\\"}"); try { SendSmsResponse response = client.getAcsResponse(request); System.out.println("短信发送成功"); }catch (ClientException e) { e.printStackTrace(); } } }
|
2.2 手机验证码登录
2.2.1 需求分析
为了方便用户登录,移动端通常都会提供通过手机验证码登录的功能。
手机验证码登录的优点:
- 方便快捷,无需注册,直接登录
- 使用短信验证码作为登录凭证,无需记忆密码
- 安全
登录流程:
输入手机号>获取验证码>输入验证码>点击登录>登录成功
注意:通过手机验证码登录,手机号是区分不同用户的标识。
2.2.2 数据模型
通过手机验证码登录时,涉及的表为user表,即用户表。结构如下:

2.2.3 梳理交互过程
在开发代码之前,需要梳理一下登录时前端页面和服务端的交互过程:
1、在登录页面(front/page/login.html)输入手机号,点击【获取验证码】按钮,执行getCode()方法,页面发送ajax请求/user/sendMsg
,method=post,在服务端调用短信服务API给指定手机号发送验证码短信。
2、在登录页面输入验证码,点击【登录】按钮,执行btnLogin({phone:this.form.phone})方法,发送ajax请求/user/login
,method=post,服务端需要处理登录请求。
如果服务端响应回来登录成功,则跳转到/front/index.html
。
2.2.4 准备工作
在开发业务功能前,先将需要用到的类和接口基本结构创建好:
1、实体类User(直接从课程资料中导入即可)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| @Data public class User implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String name;
private String phone;
private String sex;
private String idNumber;
private String avatar;
private Integer status; }
|
2、Mapper接口UserMapper
1 2 3
| @Mapper public interface UserMapper extends BaseMapper<User> { }
|
3、Service接口UserService
1 2
| public interface UserService extends IService<User> { }
|
4、Service实现类UserServicelmpl
1 2 3
| @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { }
|
5、控制层UserController
1 2 3 4 5 6 7 8 9
| @Slf4j @RestController @RequestMapping("/user") public class UserController {
@Autowired private UserService userService;
}
|
6、工具类SMSutils、 ValidateCodeutils(直接从课程资料中导入即可)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
public class SMSUtils {
public static void sendMessage(String signName, String templateCode,String phoneNumbers,String param){ DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", "", ""); IAcsClient client = new DefaultAcsClient(profile);
SendSmsRequest request = new SendSmsRequest(); request.setSysRegionId("cn-hangzhou"); request.setPhoneNumbers(phoneNumbers); request.setSignName(signName); request.setTemplateCode(templateCode); request.setTemplateParam("{\"code\":\""+param+"\"}"); try { SendSmsResponse response = client.getAcsResponse(request); System.out.println("短信发送成功"); }catch (ClientException e) { e.printStackTrace(); } } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
|
public class ValidateCodeUtils {
public static Integer generateValidateCode(int length){ Integer code = null; if(length == 4){ code = new Random().nextInt(9999); if(code < 1000){ code = code + 1000; } }else if(length == 6){ code = new Random().nextInt(999999); if(code < 100000){ code = code + 100000; } }else{ throw new RuntimeException("只能生成4位或6位数字验证码"); } return code; }
public static String generateValidateCode4String(int length){ Random rdm = new Random(); String hash1 = Integer.toHexString(rdm.nextInt()); String capstr = hash1.substring(0, length); return capstr; } }
|
2.2.5 代码开发
1、修改过滤器
前面我们已经完成了LoginCheckFilter过滤器的开发,此过滤器用于检查用户的登录状态。我们在进行手机验证码登录时,发送的请求需要在此过滤器处理时直接放行。

LoginCheckFilter过滤器添加4-2部分,完整的LoginCheckFilter过滤器如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
|
@Slf4j @WebFilter(filterName = "loginCheckFilter", urlPatterns = "/*")
public class LoginCheckFilter implements Filter {
public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
@Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse;
String requestURI = request.getRequestURI(); log.info("拦截到请求:{}", requestURI);
String[] urls = new String[]{ "/employee/login", "/employee/logout", "/backend/**", "/front/**", "/user/sendMsg", "/user/login" }; boolean check = check(urls, requestURI);
if(check){ log.info("本次请求{}无需处理", requestURI); chain.doFilter(request, response); return; }
HttpSession session = request.getSession(); if(session.getAttribute("employee") != null){ log.info("用户已登录,用户id为:{}", session.getAttribute("employee"));
Long empId = (Long) session.getAttribute("employee"); BaseContext.setCurrentId(empId); chain.doFilter(request, response); return; } if(session.getAttribute("user") != null){ log.info("用户已登录,用户id为:{}", session.getAttribute("employee")); Long userId = (Long) session.getAttribute("user"); BaseContext.setCurrentId(userId); chain.doFilter(request, response); return; } else { log.info("用户未登录"); response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN"))); return; } }
public boolean check(String[] urls, String requestURI){ for(String url : urls){ boolean match = PATH_MATCHER.match(url, requestURI); if(match){ return true; } } return false; } }
|
2、前端资源
资料/前端资源/front部分代码不全,建议直接拷贝资料中的day05的front资源。
或者也可以按如下步骤手动修改:
(1)修改/front/api/login.js中内容,新增:
1 2 3 4 5 6 7
| function sendMsgApi(data) { return $axios({ 'url': '/user/sendMsg', 'method': 'post', data }) }
|
(2)修改/front/page/login.html中getCode()方法:
1 2
| sendMsgApi({phone:this.form.phone})
|
3、UserController中新增方法,处理发送验证码的请求
处理请求/user/sendMsg,生成验证码code,在IDEA的console显示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
@PostMapping("/sendMsg") public R<String> sendMsg(@RequestBody User user, HttpSession session){ String phone = user.getPhone(); if(StringUtils.isNotEmpty(phone)){ String code = ValidateCodeUtils.generateValidateCode(4).toString(); log.info("code = {}", code);
session.setAttribute(phone, code); return R.success("手机验证码短信发送成功"); } return R.error("手机短信发送失败"); }
|
4、在UserController新增login方法,处理登录请求
目标:处理请求/user/login。
思路:验证用户填写的phone对应的code,是否是服务器(sendMsg方法)刚才生成的code。如果是,则登录成功,向session中保存该user的id。如果不一致,则登录失败。如果数据库中不存在该手机号,则会重新注册。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
|
@PostMapping("/login") public R<User> login(@RequestBody Map<String, String> map, HttpSession session){ log.info("map: {}", map.toString()); String phone = map.get("phone"); String code = map.get("code"); Object codeInSession = session.getAttribute(phone); if(codeInSession != null && codeInSession.equals(code)){
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(User::getPhone, phone); User user = userService.getOne(queryWrapper); if(user == null){ user = new User(); user.setPhone(phone); user.setStatus(1); userService.save(user); } session.setAttribute("user", user.getId()); return R.success(user); } return R.error("登录失败"); }
|
补充:老师示范的代码没有保存用户名,所以用户名在user表中为null。后续下单后,在后台“订单明细”页面会看不到用户名。此处可以完善一下,修改login()方法的代码,将新用户注册部分改为:
1 2 3 4 5 6 7 8 9 10
| if(user == null){ user = new User(); user.setPhone(phone); user.setStatus(1); log.info("user_id为:{}", user.getId()); user.setId(IdWorker.getId()); user.setName("用户" + (user.getId() + 1000000L) % 1000000L); userService.save(user); }
|