# nowcoder
**Repository Path**: windy_Boom/nowcoder
## Basic Information
- **Project Name**: nowcoder
- **Description**: 仿牛客论坛项目
- **Primary Language**: Java
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 4
- **Forks**: 1
- **Created**: 2022-02-27
- **Last Updated**: 2026-03-30
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
## 项目介绍
Nowcoder是一个基于SpringBoot,MyBatis,Redis,,MySQl实现的仿照牛客网站设计的论坛。主要实现了用户的注册登录、个人信息管理、发帖、评论、点赞和关注的基本功能,同时实现了实现网站UV和DAU统计。
### 首页图:
### 帖子详情
### 个人信息管理
### 私信列表
## 数据库表
#### 用户表 user
| 字段 | 类型 | 备注 |
| --------------- | --------- | ---------------------------------- |
| id | int | 主键、自增 |
| username | varchar | 用户名,创建索引 |
| password | varchar | 用户密码 |
| salt | varchar | 加密盐值 |
| email | varchar | 用户邮箱,创建索引 |
| type | int | 用户类型:0 普通、1 管理员、2 版主 |
| status | int | 用户状态:0 未激活、1 已激活 |
| activation_code | varchar | 激活码 |
| header_url | varchar | 用户头像地址 |
| create_time | timestamp | 注册时间 |
#### 评论表 comment
| 字段 | 类型 | 备注 |
| ----------- | --------- | ------------------------------------ |
| id | int | 主键、自增 |
| user_id | int | 评论的用户 id,创建索引 |
| entity_id | int | 评论实体 id,创建索引 |
| entity_type | int | 评论实体类型:1 帖子评论、2 评论回复 |
| target_id | int | 评论目标 id |
| content | text | 评论内容 |
| status | int | 评论状态:0 有效、1 无效 |
| create_time | timestamp | 评论发表时间 |
#### 帖子表 discuss_post
| 字段 | 类型 | 备注 |
| ------------- | --------- | -------------------------------- |
| id | int | 主键、自增 |
| user_id | int | 发帖的用户 id,创建索引 |
| title | varchar | 帖子表标题 |
| content | text | 帖子内容 |
| type | int | 帖子类型:0 普通、1 置顶 |
| comment_count | int | 评论数量 |
| status | int | 帖子状态:0 普通、1 精华、2 拉黑 |
| create_time | timestamp | 评论发表时间 |
#### 用户登录凭证表 login_ticket
| 字段 | 类型 | 备注 |
| ------- | --------- | ------------------------ |
| id | int | 主键、自增 |
| user_id | int | 登录用户 id |
| ticket | varchar | 登录凭证,随机字符串 |
| status | int | 登录状态:0 有效、1 无效 |
| expired | timestamp | 过期时间 |
#### 消息表 message
| 字段 | 类型 | 备注 |
| --------------- | --------- | ------------------------------------- |
| id | int | 主键、自增 |
| from_id | int | 发消息的 id,创建索引 |
| to_id | int | 收消息的 id,创建索引 |
| conversation_id | varchar | 会话 id,由通信双方 id 拼接,创建索引 |
| content | text | 消息内容 |
| status | int | 消息状态:0 未读、1 已读、2 删除 |
| create_time | timestamp | 消息发送时间 |
------
## 开发社区首页
### 搭建基本环境
构建 SpringBoot 的 maven 项目,引入 mysql 和 mybatis 依赖。
------
在 `application.properties` 配置文件中:
- 关闭 thymeleaf 缓存
- 配置数据库,设置基本连接信息、最大线程数,最小空闲线程数,最大空闲时间等
- mybatis,设置 mapper 文件的位置、实体类包名、使用主键等
------
创建 community 数据库和数据库表。
------
用户相关操作:
- 创建对应 user 表的 User 实体类
- 创建 UserMapper 接口,使用 `@Mapper` 注解
- 创建 user-mapper.xml,重复 sql 语句可以写在 `` 标签,通过 `` 引用。
------
### 开发社区首页(discuss_post 表)
功能拆分:开发社区首页,显示前 10 个帖子。开发分页组件,分页显示所有帖子。
用到的表是 discuss_post 数据库表,包括帖子 id、发帖人 id、标题、内容、类型、状态、发帖时间、评论数量(为了提高效率,避免关联查询,因此冗余存储)、分数(用于进行热度排名)。
#### 开发数据层
帖子相关操作:
- 创建对应 discuss_post 表的 DisscussPost 实体类。
- 创建 DisscussPostMapper 接口,使用 `@Mapper` 注解。
- 分页查询中用户 id 是可选参数,通过动态 SQL 选择,如果为 0 就不使用,在开发用户个人主页查询用户发帖记录时需要使用。
- 如果只有一个参数,并且在动态 SQL 的 `` 里使用,必须使用 `@Param` 加别名。
- 创建 `disscusspost-mapper.xml`。
- `where status != 2` 拉黑的帖子不展现。
- `` userID 为 0 时不使用,按照类型,发帖时间[排序]()。
------
#### 开发业务层
创建 DiscussPostService 类,可以分页查询帖子和帖子数量。
创建 UserService 类,实现根据 id 查询用户功能,因为显示帖子时不显示用户 id,而是显示用户名。
------
#### 开发视图层
把静态资源 css、html、img、js 放到 static 目录下。
把模板 mail、site、index.html 放到 template 目录下。
创建 HomeController,`getIndexPage` 方法,用 map 集合把帖子和用户封装到一起。
修改 `index.html`,使用 ` 0` 时显示分页信息。
如果 `page.current` 等于 1 或 `page.total`,代表是首页或末页,此时不能点击上一页和下一页,用 `disabled` 属性实现。
------
## 开发注册登录模块
------
### 发送邮件
在[新浪]()邮箱打开 SMTP 服务。
引入 `spring-boot-starter-mail` 依赖。
在配置文件配置主机、端口、发送邮箱、授权码等。
创建 MailClient 类,调用 JavaMailSender 发送邮件。
使用 thymeleaf 发送 HTML 邮件,调用 TemplateEngine 把信息封装到 HTML 模板。
【问题】发送邮件成功但没接收到,在垃圾箱中可找到。
------
### 注册功能
把 register.html 地址关联到首页的注册 href 属性。
设置域名、创建 CommunityUtil 工具类,在工具类创建生产随机字符串和 MD5 加密方法。
创建 LoginController,创建 `getRegisterPage` 方法,跳转注册页面。
在 UserService 中创建 `register` 方法,判断注册信息合规后插入数据库,发送激活邮件。
在 LoginController 创建 `register` 方法,调用 UserService 的 `register` 方法。
创建接口 CommunityConstant,定义激活码的三种状态,成功、重复、失败,让 UserService 和 LoginController 实现该接口。
点击激活邮件的 url 【本地服务器的url】后,服务器通过 LoginController 的 `activation` 方法查询数据库用户,如果 url 中的激活码和设置的一样,就把用户 status 改为 1。
------
### 生成验证码
在 `pom.xml` 导入 kaptcha 的 jar 包。
创建配置类 KaptchaConfig,设置验证码的大小、范围、长度等。
在 LoginController 类新增 `getKaptcha` 方法生成验证码图片。
在 `login.html` 中,将刷新验证码的链接绑定 `refresh_kaptcha` 方法,通过 id 选择器获取 img 组件,重新访问 `getKaptcha` 方法生成验证码图片。
【问题】由于访问同一个生成验证码路径,需要在 url 参数加上一个随机数字,保证会重新请求获取新图片。
------
### 登录退出功能(login_ticket 表)
登录成功时,需要生成一个登录凭证发送给客户端。凭证可以在多个业务中连续地验证用户的登陆状态,凭证信息存储在 login_ticket 数据库表中,status 的 0 和 1 表示有效和无序,expire 表示过期时间。
创建对应 login_ticket 表的 LoginTicket 实体类,对应 login_ticket 数据库表。
创建 LoginTicketMapper 接口,通过 `@Insert`、`@Select`、`@Update` 注解来插入、查询、更新凭证。
在 UserServce
- 创建 `login` 方法,验证账户合规后将凭证信息插入数据库,添加登录凭证到 map 中。
- 创建 `logout` 方法,将对应凭证设为无效。
在 LoginController
- 创建 `login` 方法,判断验证码正确后调用 UserServce 的 `login` 方法,如果 map 包含 ticket 代表登录成功,重定向跳转首页,否则添加错误信息并跳回登录页。
- 创建 `logout` 方法,判断验证码正确后调用 UserServce 的`logout` 方法,跳转至登录页。
在 `login.html` 绑定登录链接,`index.html` 绑定退出登录链接。
【问题】登录成功后,创建了凭证,但忘记将凭证信息插入数据库。
------
### 显示登录信息
创建 CookieUtil 工具类,通过 name 查询对应 cookie 的 value。
在 UserService 中新增 `findLoginTicket` 方法,根据 ticket 查询 LoginTicket。
创建 HostHolder 类用来模拟 session 的功能,利用 ThreadLocal 实现,存储用户信息。
创建 LoginTicketInterceptor 拦截器,实现 HandlerInterceptor 接口。
- 在 `preHandle` 方法中通过 CookieUtil 的 `getValue` 方法查询是否有凭证 cookie,如果有则通过 UserService 的 `findloginTicket` 方法查询用户 ID,再通过用户 ID 查询用户。最后将用户放入 hostHolder 中。
- 在 `postHandle` 方法中通过 hostHolder 的 `get` 方法获取用户,并将其存入视图中。
- 在 `afterCompletion` 方法中清除 hostHolder 中存放的用户信息。
创建 WebMvcConfig 配置类,实现 WebMvcConfigurer接口,配置 LoginTicketInterceptor,拦截除了静态资源之外的所有路径。
------
### 上传头像
在 UserService 新增 `updateHeader` 方法,更改指定用户的头像。
创建 UserController
- 新增 `getSettingPage` 方法访问账户设置 `setting.html` ,并在 `index.html` 的账号设置按钮关联该链接。
- 新增 `uploadHeader` 方法更新用户头像,如果上传出现错误将错误信息存在 Model 对象中。
如果没有错误,生成一个文件对象 dest,利用 MultipartFile 接口的 `transferTo` 方法将用户上传文件导入 dest,并从 hostHolder 中取出用户,更新用户的头像路径。
- 新增 `getHeader` 方法获取用户头像,利用文件输入流读取图片数据,利用 HttpServletResponse 的字节输出流再进行输出。
调整 `setting.html` 的 form 表单, method="post",enctype="multipart/form-data",并设置提交路径。
------
### 修改密码
在 UserService 中新增 `changePassword` 方法,判断原密码是否正确,正确则修改密码并返回 1,否则返回 0。
在 UserController 中新增 `changePassword` 方法,根据 UserService 的 `changePassword` 方法的返回值判断原密码是否成功修改,封装为 JSON 数据并返回。
在 `setting.html` 中
- 首先在前端判断两次输入的新密码是否一致,如果不一致不允许点击提交并显示错误信息。
- 利用 ajax 向 UserController 的 `changePassword` 方法发送 POST 请求,得到 JSON 数据并解析,如果状态码为 0 提示错误,如果状态码为 1 弹出修改成功提示。
【问题】js 的虚拟路径问题,需要加上 `../`。
【问题】使用 ajax 请求时,表单按钮类型必须是 button,不能是 submit,否则 405 报错。
【问题】使用 ajax 请求时,Controller 中方法的返回值必须是 JSON 数据,并且需要加上 `@ResponseBody`。
【问题】使用 ajax 请求时,回调函数需要先对返回的 JSON 数据进行解析再使用。
------
### 检查登录状态
利用拦截器,实现只处理带有自定义注解的方法,防止用户在未登录情况下通过 url 访问没有权限的页面。
创建 `@LoginRequired` 自定义注解,作用范围在方法上,有效期为运行时。
在 UserController 中需要在登录状态下调用的方法,访问设置页面、修改密码、上传头像等加上自定义注解。
创建 LoginRequiredInterceptor 拦截器,在 `preHandle` 方法中判断方法是否加了 `@LoginRequired` 注解,如果加了注解并且此时从 hostHolder 中获取不到用户则拒绝访问。
在 WebMvcConfig 配置类配置 LoginRequiredInterceptor,拦截除了静态资源之外的所有路径。
------
## 开发核心功能
### 敏感词过滤
利用字典树数据结构解决。
创建 SensitiveFilter 类
- 创建静态内部类 TrieNode ,通过 boolean 类型的结束符判断是否匹配到关键字尾部。
- 利用 `@PostConstruct` 注解,在构造方法执行后初始化字典树。
- 添加 `filter` 方法,利用双指针进行匹配,过滤敏感词。
【问题】判断子节点空时,直接添加了一个 new 的子节点,没有将对象赋值给子节点变量。
------
### 发布帖子
引入 fastjson 依赖,在 CommunityUtil 中新增 `getJSONString` 方法封装 JSON 信息。
在 DisscussPostMapper 接口新增 `insertDiscussPost` 方法,并在 `disscusspost-mapper.xml` 配置 insert 语句。
在 DiscussPostService 新增 `addDiscussPost` 方法调用 DisscussPostMapper 的 `insertDiscussPost` 方法,其中需要进行对标题内容和发帖内容进行 HTML 转义以及过滤敏感词。
创建 DiscussPostController 类,新增 `addDiscussPost` 方法,调用 DiscussPostService 的 `addDiscussPost` 方法发帖。
在 `index.html` 中为发帖按钮绑定函数,利用 Ajax 向 DiscussPostController 的 `addDiscussPost` 方法发送 POST 请求。
------
### 显示帖子内容
在 DisscussPostMapper 接口新增 `selectDiscussPostById` 方法,在 `disscusspost-mapper.xml` 配置 select 语句。
在 DiscussPostService 新增 `findDiscussPostById` 方法调用 DisscussPostMapper 的 `selectDiscussPostById` 方法。
在 DiscussPostController 新增 `getDiscussPost` 方法,调用 DiscussPostService 的 `findDiscussPostById` 方法查询帖子内容,将 DiscussPost 对象和 User 对象(通过 userId 查询,不在 DAO 层关联查询)数据存放到 Model 对象,返回模板 `discuss-detail`。
在 `discuss-detail.html` 取出 Model 对象存放的数据绑定到对应组件显示。
------
### 显示评论(comment 表)
创建 comment 表对应的实体类 Comment。
创建 CommentMapper 接口
- 新增 `selectCommentsByEntity` 方法,根据实体查询一页的评论数据。
- 新增 `selectCountByEntity` 方法,根据实体查询评论的数量。
- 在 `comment-mapper.xml` 配置 select 语句。
创建 CommentService 类
- 新增 `findCommentByEntity` 方法,调用 CommentMapper 的 `selectCommentByEntity` 方法。
- 新增 `findCommentCount` 方法,调用 CommentMapper 的 `selectCountByEntity` 方法。
在 DiscussPostController 的 `getDiscussPost` 方法中增加查询帖子评论和回复的逻辑,将结果存储在 Model 对象。
【问题】sql 的 xml 文件中绑定参数时,应传入实体类属性名,拼错成数据库字段名(entityId 写成 entity_id)。
------
### 添加评论
在 CommentMapper 接口新增 `insertComment` 方法,添加评论数据,在 `comment-mapper` 配置对应 sql。
在 DiscussPostMapper 接口新增 `updateCommentCount` 方法,增加评论数量,在 `discusspost-mapper` 配置对应 sql。
在 DiscussPostService 类新增 `updateCommentCount` 方法,调用 DiscussPostMapper 的 `updateCommentCount` 方法。
在 CommentService 类新增 `addComment` 方法,调用 CommentMapper 的 `insertComment` 新增评论,并调用 DiscussPostService 的 `updateCommentCount` 更新评论数量,使用 `@Transactional` 注解保证事务。
创建 CommentController 类,新增 `addComment` 方法,从 hostHolder 获取用户信息,然后调用 CommentService 的 `addComment` 方法添加评论。
【问题】sql 的 xml 文件中绑定参数时,应传入实体类属性名,拼错成数据库字段名(entityId 写成 entity_id)。
------
### 显示私信列表 (message 表)
创建对应 message 表的实体类 Message。
创建 MessageMapper 接口,增加查询会话列表、会话数量、私信列表、私信数量、未读私信数量等方法,在 `message-mapper.xml` 中配置对应的 sql。
创建 MessageService,调用 MessageMapper 中的方法。
创建 MessgaeController
- 新增 `getLetterList` 方法,将会话列表信息存储到 Model 对象,返回 `letter` 视图。
- 新增 `getLetterDetail` 方法,将每个会话具体的私信信息存储到 Model 对象,返回 `letter-datail` 视图。
------
### 发送私信
在 MessageMapper
- 新增 `insertMessage` 方法插入私信记录,在 `message-mapper.xml` 配置 insert 语句。
- 新增 `updateMessgae` 方法修改私信状态,在 `message-mapper.xml` 配置 update 语句,利用 foreach 动态 sql。
在 MessageService
- 新增 `addMessage` 发送私信方法,过滤敏感词后,调用 MessageMapper 的 `insertMessage` 。
- 新增 `readMessage` 方法读取信息,调用MessageMapper 的 `updateMessgae` 更新私信的状态为 1。
在 MessageController
- 新增 `getLetterIds` 方法,将私信集合中未读私信的 id 添加到 List 集合并返回,在 `getLetterDetail` 方法调用该方法设置已读。
- 新增 `sendLetter` 发送私信方法,设置私信信息后调用 MessageService 的 `addMessage` 发送。
------
### 统一异常处理
在 HomeController 中增加 `getErrorPage` 方法,返回错误页面。
创建 ExceptionAdvice 类
- 加上 `@ControllerAdvice` 注解,表示该类是 Controller 的全局配置类。
- 创建 `handleException` 方法,加上 `@ExceptionHandler` 注解,该方法在 Controller 出现异常后调用,处理捕获异常。如果是异步请求返回一个 JSON 数据,否则重定向至 HomeController 的 `getErrorPage` 方法。
------
### 统一日志处理
在 `pom.xml` 引入 aspectj 的依赖。
创建 ServiceLogAspect 类,添加 `@Aspect` 切面注解,配置切入点表达式,拦截所有 service 包下的方法,利用 `@Before` 记录日志。
------
## Redis
### 点赞
创建 RedisKeyUtil 工具类
- 定义分隔符 `:` 以及实体获得赞的 key 前缀常量 `like:entity`。
- 新增 `getEntityLikeKey(int entityType,int entityId)` 方法,通过实体类型和实体 id 生成对应实体获得赞的 key。
创建业务层的 LikeService 类
- 注入 RedisTemplate 实例。
- 新增 `like` 点赞方法,首先通过 RedisKeyUtil 工具类的 `getEntityLikeKey` 方法获得实体点赞的 key,然后通过 RedisTemplate 对象对 set 集合的 `isMember` 方法查询 userId 是否存在于对应 key 的 set 集合中,如果存在则移除出点赞的用户集合,如果不存在则添加到点赞的用户集合。
- 新增 `findEntityLikeCount` 方法查询实体的点赞数量,通过调用 set 集合的 `size` 方法查询元素个数。
- 新增 `findEntityLikeStatus` 方法查询某用户对某实体的点赞状态,逻辑如 `like` 方法,通过 set 集合的 `isMember` 方法实现。
创建表现层的 LikeController 类
- 注入 LikeService 和 HostHolder 实例。
- 新增 `like` 点赞方法,调用业务层的 `like` 方法进行点赞、调用 `findEntityLikeCount` 和 `findEntityLikeStatus` 查询点赞数量和点赞状态,封装到 map 集合,然后通过工具类封装成 JSON 数据返回。
(更新首页帖子点赞数量)在表现层的 HomeController 类
- 注入 LikeService 实例。
- 在 `getIndexPage` 方法在通过 LikeService 类的方法获得点赞数量,存储到 map 集合。
------
### 收到的赞
对点赞功能进行重构
在 RedisUnitl 工具类
- 新增用户获得赞 key 的前缀常量 `like:user`
- 新增 `getUserLikeKey(int userId)` 方法,通过用户 id 生成对应用户获得赞的 key。
在 LikeService 中
- 重构 `like` 方法,在参数列表中加入 entityUserId 表示被点赞用户的 id,用来更新用户的被点赞数量。
- 通过 RedisTemplate 对象的 `execute` 方法实现事务,保证被点赞用户点和点赞用户的数据更新一致。通过 `isMember` 方法查询用户的点赞状态,之后通过 `mutli` 方法开启事务。
- 当用户已点赞时,调用 `remove` 方法将当前用户从点赞用户的集合中移除,调用 `decrement` 方法将被点赞用户的被点赞数减 1;当用户未点赞时,调用 `add` 方法将当前用户添加到点赞用户的集合,调用 `increment` 方法将被点赞用户的被点赞数加 1。
- 增加 `findUserLikeCount` 方法,以用户 id 作为 key,调用 `get` 方法查询用户所获得的点赞数。
在 LikeController 中给 `like` 方法增加 entityUserId 参数即可。
------
### 关注
在 RedisUnitl 工具类
- 新增用户关注实体(帖子、评论、用户等)和粉丝(用户)的前缀常量 `followee` 和 `follower`
- 新增 `getFolloweeKey(int userId, int entityType)` 方法,通过用户 id 和实体类型生成用户关注实体的 key。
- 新增 `getFollowerKey(int entityType, int entityId)` 方法,通过实体类型和实体 id 生成实体用户粉丝的 key。
创建业务层的 FollowService 类
- 新增
```
follow
```
方法,当用户关注某实体时,
- 调用 `add` 方法将当前实体 id 和时间作为 value 和 score加入用户的关注集合。
- 调用 `add` 方法将当前用户 id 和时间作为 value 和 score 加入实体的粉丝集合。
- 新增
```
unfollow
```
方法,当用户取消关注某实体时,
- 调用 `remove` 方法将当前实体从用户的关注集合移除。
- 调用 `remove` 方法将用户从实体的粉丝集合移除。
------
### 个人主页
在业务层的 FollowService 类
- 新增 `findFolloweeCount` 方法,调用 zset 的 `zcard` 方法查询某用户关注的实体数量。
- 新增 `findFollowerCount` 方法,调用 zset 的 `zcard` 方法查询某实体的粉丝数量。
- 新增 `hasFollowed` 方法,根据 zset 的 `zscore` 方法返回值查询当前用户是否关注某实体。
在 UserController 中新增 `getProfilePage` 方法获取个人主页。
- 调用 LikeService 的 `findUserLikeCount` 查询用户获赞数,并添加到 Model 中。
- 调用 FollowService 的`findFolloweeCount`、`findFollowerCount` 、`hasFollowed` 方法分别查询关注数量、粉丝数量、用户是否关注三项信息并添加到 Model 对象中存储。
------
### 关注列表和粉丝列表
在业务层的 FollowService 类
- 新增 `findFollowees` 方法,查询用户关注列表,主要通过 zset 的 `reverseRange` 获取 value 即关注用户的 userId,再查询出其 user,之后通过 `score` 获取关注时间,存入 map 集合,将 map 添加到 list 列表返回。
- 新增 `findFollowers` 方法,查询用户粉丝列表,主要通过 zset 的 `reverseRange` 获取 value 即粉丝的 userId,再查询出其 user,之后通过 `score` 获取关注时间,存入 map 集合,将 map 添加到 list 列表返回。
在表现层的 FollowController 类
- 新增 `getFollowees` 方法,获取关注列表,存入 Model 对象。
- 新增 `getFollowers` 方法,获取粉丝列表,存入 Model 对象。
------
### 优化登录模块
**存储验证码**
在 RedisUntil 工具类
- 新增验证码前缀常量 `kaptcha`
- 新增 `getKaptchaKey` 方法,通过一个用户凭证(由于未登录,利用 cookie 实现)获得对应验证码的 key 值(利用 string 存储验证码)。
在表现层的 LoginController 类
- 重构 `getKaptcha` 方法,将验证码存入 [redis](),key 值是当前随机生成的一个字符串,同时将该字符串存入 cookie。
- 重构 `login` 方法,从 cookie 中获得随机字符串,生成验证码的 key 值,然后获取对应的 value 值即验证码。
------
**存储登录凭证**
在 RedisUntil 工具类
- 新增登录凭证前缀常量 `ticket`
- 新增 `getTicketKey` 方法,通过字符串获得登录凭证的对应 key 值(利用 string 存储)。
在业务层的 UserService 类
- 重构 `login` 方法,将登录凭证存入 [redis]() 中。
- 重构 `logout` 方法,先从 [redis]() 中获取登录凭证对象,将状态设为无效再重新存储进 [redis]()。
- 重构 `findLoginTicket` 方法,根据 ticket 字符串获得对应登录凭证的 key,然后从 [redis]() 查询登录凭证。
------
**缓存用户信息**
在 RedisUntil 工具类
- 新增用户前缀常量 `user`
- 新增 `getUserKey` 方法,通过用户 id 获得用户的对应 key 值(利用 string 存储)。
在业务层的 UserService 类
- 新增 `getCache`,从缓存获取用户信息。
- 新增 `initCache`,从 MySQL 查询用户信息并存入 [redis]()。
- 新增 `clearCache`,用户信息变更(更新头像,激活)时清除缓存。
- 重构 `findUserById` 方法,首先调用 `getCache`从缓存获取用户信息,如果获取为 null 则调用 `initCache`。