From d986639b52149fb3002803d6236b2561abc9977f Mon Sep 17 00:00:00 2001
From: Veal98 <1912420914@qq.com>
Date: Thu, 28 Jan 2021 22:09:02 +0800
Subject: [PATCH] Complete follow and notice function
---
README.md | 88 +-
docs/130-添加评论.md | 2 +-
docs/180-点赞.md | 2 +
docs/190-我收到的赞.md | 2 +-
docs/200-关注.md | 297 +
docs/210-关注列表和粉丝列表.md | 192 +
docs/220-优化登录模块.md | 226 +
docs/230-发送系统通知.md | 317 +
docs/240-显示系统通知.md | 212 +
.../error/log-error-2021-01-25.0.log | 630 ++
.../error/log-error-2021-01-27.0.log | 147 +
log/community/info/log-info-2021-01-25.0.log | 4264 ++++++++
log/community/info/log-info-2021-01-27.0.log | 3352 ++++++
log/community/log_error.log | 3112 ++++--
log/community/log_info.log | 9411 +++++++++--------
log/community/log_warn.log | 791 +-
log/community/warn/log-warn-2021-01-25.0.log | 550 +
log/community/warn/log-warn-2021-01-27.0.log | 323 +
.../community/aspect/ServiceLogAspect.java | 3 +
.../greate/community/config/WebMvcConfig.java | 7 +
.../controller/CommentController.java | 34 +-
.../controller/DiscussPostController.java | 3 +
.../controller/FollowController.java | 165 +
.../community/controller/HomeController.java | 2 +-
.../community/controller/LikeController.java | 25 +-
.../community/controller/LoginController.java | 37 +-
.../controller/MessageController.java | 143 +-
.../community/controller/UserController.java | 21 +-
.../interceptor/MessageInterceptor.java | 41 +
.../greate/community/dao/CommentMapper.java | 9 +
.../community/dao/LoginTicketMapper.java | 1 +
.../greate/community/dao/MessageMapper.java | 36 +
.../com/greate/community/entity/Event.java | 71 +
.../greate/community/event/EventConsumer.java | 65 +
.../greate/community/event/EventProducer.java | 28 +
.../community/service/CommentService.java | 13 +
.../community/service/DiscussPostSerivce.java | 3 +
.../community/service/FollowService.java | 172 +
.../community/service/MessageService.java | 45 +
.../greate/community/service/UserService.java | 73 +-
.../community/util/CommunityConstant.java | 19 +-
.../greate/community/util/RedisKeyUtil.java | 81 +-
src/main/resources/application.properties | 7 +
src/main/resources/mapper/comment-mapper.xml | 6 +
src/main/resources/mapper/message-mapper.xml | 47 +
src/main/resources/static/css/global.css | 4 +-
src/main/resources/static/css/letter.css | 4 +-
src/main/resources/static/js/discuss.js | 4 +-
src/main/resources/static/js/profile.js | 30 +-
src/main/resources/templates/index.html | 2 +-
.../templates/site/discuss-detail.html | 10 +-
.../resources/templates/site/followee.html | 162 +-
.../resources/templates/site/follower.html | 164 +-
src/main/resources/templates/site/letter.html | 6 +-
.../templates/site/notice-detail.html | 181 +-
src/main/resources/templates/site/notice.html | 130 +-
.../resources/templates/site/profile.html | 23 +-
.../java/com/greate/community/KafkaTests.java | 49 +
58 files changed, 19847 insertions(+), 5997 deletions(-)
create mode 100644 docs/200-关注.md
create mode 100644 docs/210-关注列表和粉丝列表.md
create mode 100644 docs/220-优化登录模块.md
create mode 100644 docs/230-发送系统通知.md
create mode 100644 docs/240-显示系统通知.md
create mode 100644 log/community/error/log-error-2021-01-25.0.log
create mode 100644 log/community/error/log-error-2021-01-27.0.log
create mode 100644 log/community/info/log-info-2021-01-25.0.log
create mode 100644 log/community/info/log-info-2021-01-27.0.log
create mode 100644 log/community/warn/log-warn-2021-01-25.0.log
create mode 100644 log/community/warn/log-warn-2021-01-27.0.log
create mode 100644 src/main/java/com/greate/community/controller/FollowController.java
create mode 100644 src/main/java/com/greate/community/controller/interceptor/MessageInterceptor.java
create mode 100644 src/main/java/com/greate/community/entity/Event.java
create mode 100644 src/main/java/com/greate/community/event/EventConsumer.java
create mode 100644 src/main/java/com/greate/community/event/EventProducer.java
create mode 100644 src/main/java/com/greate/community/service/FollowService.java
create mode 100644 src/test/java/com/greate/community/KafkaTests.java
diff --git a/README.md b/README.md
index 81e1159e..f7e3b586 100644
--- a/README.md
+++ b/README.md
@@ -41,54 +41,96 @@
## 🔔 功能列表
-- [x] 注册
- - 用户注册
- - 发送激活邮件
- - 激活用户
-- [x] 登录 | 登出
- - 用户登录(生成验证码)
- - 用户登出
-- [x] 账号设置
+- [x] 注册(MySQL)
+ - 用户注册成功,将用户信息存入 MySQL,但此时该用户状态为未激活
+ - 向用户发送激活邮件,用户点击链接则激活账号
+
+- [x] 登录 | 登出(MySQL、Redis)
+ - 进入登录界面,动态生成验证码,并将验证码短暂存入 Redis(60 秒)
+
+ - 用户登录成功(验证用户名、密码、验证码),生成登录凭证且设置状态为有效,并将登录凭证存入 Redis
+
+ 注意:登录凭证存在有效期,在所有的请求执行之前,都会检查凭证是否有效和是否过期,只要该用户的凭证有效并在有效期时间内,本次请求就会一直持有该用户信息(使用 ThreadLocal 持有用户信息)
+
+ - 勾选记住我,则延长登录凭证有效时间
+
+ - 用户登录成功,将用户信息短暂存入 Redis(1 小时)
+
+ - 用户登出,将凭证状态设为无效,并更新 Redis 中该用户的登录凭证信息
+
+- [x] 账号设置(MySQL)
- 修改头像
- 修改密码
-- [x] 检查登录状态(禁止未登录用户访问需要登录权限的界面)
-- [x] 帖子模块
+
+- [x] 检查登录状态(禁止未登录用户访问需要登录权限的界面,后续会使用 Spring Security 接管)
+
+- [x] 帖子模块(MySQL)
- 发布帖子(过滤敏感词)
- 分页显示帖子
- 查看帖子详情
-- [x] 评论模块(过滤敏感词)
+
+- [x] 评论模块(MySQL)
- 发布对帖子的评论(过滤敏感词)
- 分页显示评论
- 发布对评论的回复(过滤敏感词)
-- [x] 私信模块
+
+- [x] 私信模块(MySQL)
- 发送私信(过滤敏感词)
- - 发送列表(分页显示发出的私信)
- - 私信列表(分页显示收到的私信)
+ - 私信列表
+ - 查询当前用户的会话列表
+ - 每个会话只显示一条最新的私信
+ - 支持分页显示
- 私信详情
+ - 查询某个会话所包含的所有私信
+ - 访问私信详情时,将显示的私信设为已读状态
+ - 支持分页显示
+
- [x] 统一处理异常(404、500)
- 普通请求异常
- 异步请求异常
+
- [x] 统一记录日志
-- [x] 点赞模块
+
+- [x] 点赞模块(Redis)
- 点赞
- 获赞
-- [ ] 关注模块
- - 关注
- - 取消关注
- - 关注列表
- - 粉丝列表
-- [ ] 系统通知模块
- - 管理员发送系统通知
- - 用户接收系统通知
+
+- [x] 关注模块(Redis)
+
+ - 关注功能
+ - 取消关注功能
+ - 统计用户的关注数和粉丝数
+ - 关注列表(查询某个用户关注的人),支持分页
+ - 粉丝列表(查询某个用户的粉丝),支持分页
+
+- [x] 系统通知模块(Kafka)
+
+ - 通知列表
+ - 显示评论、点赞、关注三种类型的通知
+ - 通知详情
+ - 分页显示某一类主题所包含的通知
+ - 进入某种类型的系统通知详情,则将该页的所有未读的系统通知状态设置为已读
+ - 未读数量
+ - 分别显示每种类型的系统通知的未读数量
+ - 显示所有系统通知的未读数量
+
+ - 导航栏显示所有消息的未读数量(未读私信 + 未读系统通知)
+
- [ ] 搜索模块
+
- [ ] 权限控制
+
- [ ] 管理员模块
- 置顶帖子
- 加精帖子
- 删除帖子
+
- [ ] 网站数据统计
+
- [ ] 热帖排行
+
- [ ] 文件上传
+
- [ ] 优化网站性能
## 🎨 界面展示
diff --git a/docs/130-添加评论.md b/docs/130-添加评论.md
index 5b9259f4..2390d902 100644
--- a/docs/130-添加评论.md
+++ b/docs/130-添加评论.md
@@ -105,7 +105,7 @@ public int addComment(Comment comment) {
## Controller
-前端 的 name = "content" 和 `public String addComment(Comment comment) {` Comment 中的字段要对应
+**前端的 name = "xxx" 和 `public String addComment(Comment comment) {` Comment 实体类中的字段要一一对应**,这样即可直接从前端传值。
```java
@Controller
diff --git a/docs/180-点赞.md b/docs/180-点赞.md
index 767aef01..63305ac6 100644
--- a/docs/180-点赞.md
+++ b/docs/180-点赞.md
@@ -10,6 +10,8 @@
- 详情页统计帖子和评论/回复的点赞数量
- 详情页显示用户的点赞状态(赞过了则显示已赞)
+> Redis 一般不用 DAO 层,不像 MySQL
+
## Redis 配置
导包、配置端口等
diff --git a/docs/190-我收到的赞.md b/docs/190-我收到的赞.md
index 13d59ada..f23fc6bd 100644
--- a/docs/190-我收到的赞.md
+++ b/docs/190-我收到的赞.md
@@ -35,7 +35,7 @@ public class RedisKeyUtil {
// 某个用户的赞(被赞)
// like:user:userId -> int
public static String getUserLikeKey(int userId) {
- return PREFIX_ENTITY_LIKE + SPLIT + userId;
+ return PREFIX_USER_LIKE + SPLIT + userId;
}
}
diff --git a/docs/200-关注.md b/docs/200-关注.md
new file mode 100644
index 00000000..481c1ba6
--- /dev/null
+++ b/docs/200-关注.md
@@ -0,0 +1,297 @@
+# 关注
+
+---
+
+需求:
+
+- 开发关注、取消关注功能
+- 统计用户的关注数、粉丝数
+
+关键:
+
+- 若 A 关注了 B,则 A 是 B 的粉丝 Follower,B 是 A 的目标 Followee
+- 关注的目标可以是用户、帖子、题目等,在实现时将这些目标抽象为实体
+
+
+
+## 工具类:生成 Redis 的 Key
+
+```java
+/**
+ * 生成 Redis 的 key
+ */
+public class RedisKeyUtil {
+
+ private static final String SPLIT = ":";
+
+ private static final String PREFIX_FOLLOWER = "follower"; // 被关注(粉丝)
+ private static final String PREFIX_FOLLOWEE = "followee"; // 关注的目标
+
+ /**
+ * 某个用户关注的实体
+ * followee:userId:entityType -> zset(entityId, now) 以当前关注的时间进行排序
+ * @param userId 粉丝的 id
+ * @param entityType 关注的实体类型
+ * @return
+ */
+ public static String getFolloweeKey(int userId, int entityType) {
+ return PREFIX_FOLLOWEE + SPLIT + userId + SPLIT + entityType;
+ }
+
+ /**
+ * 某个实体拥有的粉丝
+ * follower:entityType:entityId -> zset(userId, now)
+ * @param entityType
+ * @param entityId
+ * @return
+ */
+ public static String getFollowerKey(int entityType, int entityId) {
+ return PREFIX_FOLLOWER + SPLIT + entityType + SPLIT + entityId;
+ }
+
+}
+```
+
+## Service
+
+```java
+/**
+ * 关注相关
+ */
+@Service
+public class FollowService {
+
+ @Autowired
+ private RedisTemplate redisTemplate;
+
+ /**
+ * 关注
+ * @param userId
+ * @param entityType
+ * @param entityId
+ */
+ public void follow(int userId, int entityType, int entityId) {
+ redisTemplate.execute(new SessionCallback() {
+ @Override
+ public Object execute(RedisOperations redisOperations) throws DataAccessException {
+ // 生成 Redis 的 key
+ String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
+ String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);
+
+ // 开启事务管理
+ redisOperations.multi();
+
+ // 插入数据
+ redisOperations.opsForZSet().add(followeeKey, entityId, System.currentTimeMillis());
+ redisOperations.opsForZSet().add(followerKey, userId, System.currentTimeMillis());
+
+ // 提交事务
+ return redisOperations.exec();
+ }
+ });
+ }
+
+ /**
+ * 取消关注
+ * @param userId
+ * @param entityType
+ * @param entityId
+ */
+ public void unfollow(int userId, int entityType, int entityId) {
+ redisTemplate.execute(new SessionCallback() {
+ @Override
+ public Object execute(RedisOperations redisOperations) throws DataAccessException {
+ // 生成 Redis 的 key
+ String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
+ String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);
+
+ // 开启事务管理
+ redisOperations.multi();
+
+ // 删除数据
+ redisOperations.opsForZSet().remove(followeeKey, entityId);
+ redisOperations.opsForZSet().remove(followerKey, userId);
+
+ // 提交事务
+ return redisOperations.exec();
+ }
+ });
+ }
+
+ /**
+ * 查询某个用户关注的实体的数量
+ * @param userId 用户 id
+ * @param entityType 实体类型
+ * @return
+ */
+ public long findFolloweeCount(int userId, int entityType) {
+ String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
+ return redisTemplate.opsForZSet().zCard(followeeKey);
+ }
+
+ /**
+ * 查询某个实体的粉丝数量
+ * @param entityType
+ * @param entityId
+ * @return
+ */
+ public long findFollowerCount(int entityType, int entityId) {
+ String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);
+ return redisTemplate.opsForZSet().zCard(followerKey);
+ }
+
+ /**
+ * 判断当前用户是否已关注该实体
+ * @param userId
+ * @param entityType
+ * @param entityId
+ * @return
+ */
+ public boolean hasFollowed(int userId, int entityType, int entityId) {
+ String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
+ return redisTemplate.opsForZSet().score(followeeKey, entityId) != null ;
+ }
+
+}
+```
+
+## Controller
+
+```java
+/**
+ * 关注
+ */
+@Controller
+public class FollowController {
+
+ @Autowired
+ private FollowService followService;
+
+ @Autowired
+ private HostHolder hostHolder;
+
+ /**
+ * 关注
+ * @param entityType
+ * @param entityId
+ * @return
+ */
+ @PostMapping("/follow")
+ @ResponseBody
+ public String follow(int entityType, int entityId) {
+ User user = hostHolder.getUser();
+
+ followService.follow(user.getId(), entityType, entityId);
+
+ return CommunityUtil.getJSONString(0, "已关注");
+ }
+
+ /**
+ * 取消关注
+ * @param entityType
+ * @param entityId
+ * @return
+ */
+ @PostMapping("/unfollow")
+ @ResponseBody
+ public String unfollow(int entityType, int entityId) {
+ User user = hostHolder.getUser();
+
+ followService.unfollow(user.getId(), entityType, entityId);
+
+ return CommunityUtil.getJSONString(0, "已取消关注");
+ }
+
+}
+```
+
+在 `UserController` 中添加进入个人主页查询关注/粉丝数量的逻辑:
+
+```java
+// 关注数量
+long followeeCount = followService.findFolloweeCount(userId, ENTITY_TYPE_USER);
+model.addAttribute("followeeCount", followeeCount);
+// 粉丝数量
+long followerCount = followService.findFollowerCount(ENTITY_TYPE_USER, userId);
+model.addAttribute("followerCount", followerCount);
+// 当前登录用户是否已关注该用户
+boolean hasFollowed = false;
+if (hostHolder.getUser() != null) {
+ hasFollowed = followService.hasFollowed(hostHolder.getUser().getId(), ENTITY_TYPE_USER, userId);
+}
+model.addAttribute("hasFollowed", hasFollowed);
+```
+
+## 前端
+
+```html
+
+```
+
+对应的 `profile.js`
+
+```js
+$(function(){
+ $(".follow-btn").click(follow);
+});
+
+function follow() {
+ var btn = this;
+ if($(btn).hasClass("btn-info")) {
+ // 关注TA
+ $.post(
+ CONTEXT_PATH + "/follow",
+ {"entityType":3, "entityId":$(btn).prev().val()},
+ function (data) {
+ data = $.parseJSON(data);
+ if (data.code == 0) {
+ // 偷个懒,直接刷新界面
+ window.location.reload();
+ }
+ else {
+ alert(data.msg);
+ }
+ }
+ )
+ } else {
+ // 取消关注
+ $.post(
+ CONTEXT_PATH + "/unfollow",
+ {"entityType":3, "entityId":$(btn).prev().val()},
+ function (data) {
+ data = $.parseJSON(data);
+ if (data.code == 0) {
+ // 偷个懒,直接刷新界面
+ window.location.reload();
+ }
+ else {
+ alert(data.msg);
+ }
+ }
+ )
+ }
+}
+```
+
+注意:
+
+```
+$.post(
+ CONTEXT_PATH + "/follow",
+ {"entityType":3, "entityId":$(btn).prev().val()},
+```
+
+中的 `"entityType"` 和 `“entityId”` 名称对应后端 `/follow` 方法的参数,其值需要从前端获取
\ No newline at end of file
diff --git a/docs/210-关注列表和粉丝列表.md b/docs/210-关注列表和粉丝列表.md
new file mode 100644
index 00000000..bbddc899
--- /dev/null
+++ b/docs/210-关注列表和粉丝列表.md
@@ -0,0 +1,192 @@
+# 关注列表、粉丝列表
+
+---
+
+业务层:
+
+- 查询某个用户关注的人,支持分页
+- 查询某个用户的粉丝,支持分页
+
+表现层:
+
+- 处理 “查询关注的人”、“查询粉丝” 的请求
+- 编写 “查询关注的人”、“查询粉丝” 模板
+
+## Service
+
+```java
+/**
+ * 分页查询某个用户关注的人(偷个懒,此处没有做对其他实体的关注)
+ * @param userId
+ * @param offset
+ * @param limit
+ * @return
+ */
+public List