黑马点评开发日志Day01——项目介绍、环境搭建及短信登录功能
2025.08.16
黑马点评开发日志Day01——项目介绍、环境搭建及短信登录功能
一、项目介绍
1.1 项目功能
- 短信登录
- 使用redis共享session来实现
- 商户查询缓存
- 包含缓存击穿,缓存穿透,缓存雪崩等内容
- 优惠卷秒杀
- 使用Redis的计数器功能,结合Lua完成高性能的redis操作,同时学习Redis分布式锁的原理,包括Redis的三种消息队列
- 附近的商户
- 利用Redis的GEOHash来完成对于地理坐标的操作
- UV统计
- 使用Redis来完成统计功能
- 用户签到
- 使用Redis的BitMap数据统计功能
- 好友关注
- 基于Set集合的关注、取消关注,共同关注等等功能
- 达人探店
- 基于List来完成点赞列表的操作,同时基于SortedSet来完成点赞的排行榜功能

二、环境搭建
2.1 导入SQL
-
新建数据库
hmdp,导入hmdp.sql文件。 -
步骤:新建数据库
hmdp->右键sql脚本->选择hmdp.sql文件->点击执行


- 其中比较核心的表:
- tb_user:用户表
- tb_user_info:用户详情表
- tb_shop:商户信息表
- tb_shop_type:商户类型表
- tb_blog:用户日记表(达人探店日记)
- tb_follow:用户关注表
- tb_voucher:优惠券表
- tb_voucher_order:优惠券的订单表
2.2 导入项目
-
手机或者app端发起请求,请求nginx服务器,nginx基于七层模型的HTTP协议,可以实现基于Lua直接绕开tomcat访问redis,也可以作为静态资源服务器,轻松扛下上万并发, 负载均衡到下游tomcat服务器,打散流量。
-
在tomcat支撑起并发流量后,如果让tomcat直接去访问Mysql,根据经验Mysql企业级服务器只要上点并发,一般是16或32核心cpu,32或64G内存,像企业级mysql加上固态硬盘能够支撑的并发,大概就是4000起~7000左右,上万并发, 瞬间就会让Mysql服务器的cpu,硬盘全部打满,容易崩溃,所以在高并发场景下会选择使用mysql集群,同时为了进一步降低Mysql的压力,同时增加访问的性能,我们也会加入Redis,同时使用Redis集群使得Redis对外提供更好的服务。


- 修改
application.yaml文件中的数据库和redis连接信息:

2.3 启动项目
-
启动
HmDianPingApplication类进行测试发现报错:

-
原因:springboot版本过低,不支持Java17
-
解决方法:修改
pom.xml文件中的springboot版本为2.7.3 -
再次启动,启动成功:

-
访问
http://localhost:8081/shop-type/list进行测试

测试成功
2.4 测试前端代码
- 启动
nginx.exe,访问http://localhost:8080/,并打开手机模式,就可以看到前端页面

三、基于Session实现短信登录功能
3.1 基于Session实现登录
- 登录步骤:
- 发送短信验证码
- 短信验证码登录、注册
- 校验登录状态

3.2 发送短信验证码——需求分析
- 产品原型

3.3 发送短信验证码——代码实现
3.3.1 UserController
1 | /** |
3.3.2 IUserService
1 | /** |
3.3.3 UserServiceImpl
1 | /** |
3.4 发送短信验证码——功能测试
- 发送短信验证码

3.5 短信验证码登录——需求分析
- 产品原型

3.6 短信验证码登录——代码实现
3.6.1 UserController
1 | /** |
3.6.2 IUserService
1 | /** |
3.6.3 UserServiceImpl
1 | /** |
3.7 短信验证码登录——功能测试
- 短信验证码登录

3.8 校验登录状态——需求分析
- 产品原型

- 问题:当session请求变多时,每一个session都要单独进行校验,浪费资源
- 解决方案:使用拦截器进行校验

- 解决方案:使用拦截器进行校验
- 为了保护用户隐私,使用
UserDTO代替User作为返回结果,以隐藏用户关键信息
3.9 校验登录状态——代码实现
3.9.1 LoginInterceptor
1 | public class LoginInterceptor implements HandlerInterceptor { |
3.9.2 MvcConfig
1 |
|
3.9.3 UserController
1 |
|
四、基于Redis实现共享session登录
4.1 集群的session共享问题
- session共享问题:多台tomcat并不共享session存储空间,当请求切换到不同tomcat服务器时会导致数据丢失
- session的替代方案应满足:
- 数据共享
- 内存存储
- key-value结构
- 解决方案:使用Redis来存储session数据
4.2 基于Redis实现共享session登录——需求分析
- 变化:
- 使用session时由于每个请求都有独立的session空间,所以在保存验证码时key使用
code字段即可,但在使用Redis存储session时,key需要包含用户的唯一标识(如手机号)以区分不同用户的session - 在保存用户信息时key使用
token字段,value为用户信息


- 使用session时由于每个请求都有独立的session空间,所以在保存验证码时key使用
4.3 基于Redis实现共享session登录——代码实现
4.3.1 UserServiceImpl
1 | /** |
4.3.2 LoginInterceptor
1 | public class LoginInterceptor implements HandlerInterceptor { |
4.3.3 MvcConfig
1 |
|
4.4 基于Redis实现共享session登录——功能测试
- 登录时发生错误:服务器异常


- 原因:我们使用的StringRedisTemplate类要求的key和value都是
String类型,而UserDTO中的Id为Long类型,所以在类型转化时出现错误 - 解决方法:在
UserServiceImpl中将UserDTO转为Map时使用BeanUtil.beanToMap(userDTO, new HashMap<>(), CopyOptions.create().setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString())),将所有非String类型的值转为String类型 - UserServiceImpl
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/**
* 登录功能
* @param loginForm
* @param session
* @return
*/
public Result login(LoginFormDTO loginForm, HttpSession session) {
// 1. 校验手机号
String phone = loginForm.getPhone();
if(RegexUtils.isPhoneInvalid(phone)) {
// 2. 如果不符合,返回错误信息
return Result.fail("手机号格式错误");
}
// 3. 校验验证码
String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
String code = loginForm.getCode();
if (cacheCode == null || !cacheCode.toString().equals(code)) {
// 4. 不一致,报错
return Result.fail("验证码错误");
}
// 5. 一致,根据手机号查询用户
// mybatisplus
User user = query().eq("phone", phone).one();
// 6. 判断用户是否存在
if (user == null) {
// 不存在,创建新用户并保存
user = creatUserWithPhone(phone);
}
// 7. 存在,保存用户信息到redis
// 7.1 随机生成token作为登录令牌
String token = UUID.randomUUID().toString(true);
// 7.2 将User对象转为HashMap存储
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
CopyOptions.create().
setIgnoreNullValue(true).
setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
// 7.3 存储
String tokenKey = LOGIN_USER_KEY + token;
stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
// 7.4 设置token有效期
stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
// 8. 返回token
return Result.ok(token);
} - 再次登录,登录成功

- 查看redis中存储的验证码和用户信息

4.5 拦截器优化
- 目前存在的问题:只有访问拦截到的拦截器中需要拦截的接口时才会刷新token的有效期,如果访问其他接口则不会刷新token的有效期,导致用户登录失效
- 解决方案:再添加一个拦截器,该拦截器配置所有路径,但不进行拦截,只进行token有效期的刷新

4.5.1 RefreshTokenInterceptor
1 | public class RefreshTokenInterceptor implements HandlerInterceptor { |
4.5.2 LoginInterceptor
1 | public class LoginInterceptor implements HandlerInterceptor { |
4.5.3 MvcConfig
1 |
|
