# 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统计。 ### 首页图: image-20220227142713235 ### 帖子详情 image-20220227142815680 ### 个人信息管理 image-20220227142837087 ### 私信列表 image-20220227142851999 ## 数据库表 #### 用户表 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`。