关注功能(Redis+异步ajax)
关注、取消关注
1.编写工具类RedisKeyUtil统一关注Redis的key
关注:k:v = followee:userId:entityType --> zset(entityId, date)
粉丝:k:v = follower:entityType:entityId -->zset(userId, date)
public class RedisKeyUtil {
// 关注
private static final String PREFIX_FOLLOWEE = "followee";
// 粉丝
private static final String PREFIX_FOLLOWER = "follower";
/**
* 某个用户关注的实体(用户,帖子)
* followee:userId:entityType --> zset(entityId, date)
*/
public static String getFolloweeKey(int userId, int entityType) {
return PREFIX_FOLLOWEE + SPLIT + userId + SPLIT + entityType;
}
/**
* 某个实体拥有的粉丝
* follower:entityType:entityId -->zset(userId, date)
*/
public static String getFollowerKey(int entityType, int entityId) {
return PREFIX_FOLLOWER + SPLIT +entityType + SPLIT +entityId;
}
}
2.编写Service层业务
@Autowired
private RedisTemplate redisTemplate;
/**关注**/
public void follow(int userId, int entityType, int entityId) {
redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations redisOperations) throws DataAccessException {
String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);
// 开启事务
redisOperations.multi();
/**
* System.currentTimeMillis()->用于获取当前系统时间,以毫秒为单位
* 关注时,首先将实体(用户或帖子)id添加用户关注的集合中,再将用户id添加进实体粉丝的集合中
*/
redisOperations.opsForZSet().add(followeeKey, entityId, System.currentTimeMillis());
redisOperations.opsForZSet().add(followerKey, userId, System.currentTimeMillis());
return redisOperations.exec();
}
});
}
/**取消关注**/
public void unfollow(int userId, int entityType, int entityId) {
redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations redisOperations) throws DataAccessException {
String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);
// 开启事务
redisOperations.multi();
/**关注时,首先将实体(用户或帖子)id移除用户关注的集合中,再将用户id移除进实体粉丝的集合中**/
redisOperations.opsForZSet().remove(followeeKey, entityId);
redisOperations.opsForZSet().remove(followerKey, userId);
return redisOperations.exec();
}
});
}
/**查询关注的实体(用户)数量**/
public long findFolloweeCount(int userId, int entityType) {
String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
// opsForZSet().zCard获取有序集合中的数量
return redisTemplate.opsForZSet().zCard(followeeKey);
}
/**查询粉丝的实体数量**/
public long findFollowerCount(int entityType, int entityId) {
String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);
return redisTemplate.opsForZSet().zCard(followerKey);
}
/**查询当前用户是否已关注该实体**/
// userId->当前登录用户 entityType->用户类型 entityId->关注的用户id
public boolean hasFollowed(int userId, int entityType, int entityId) {
String followeeKey =RedisKeyUtil.getFolloweeKey(userId, entityType);
/**
* opsForZSet().score 获取有序集合中指定元素权重分数 followee:userId:entityType = entityId的分数(这里是时间)
* 若有时间,则表明已关注;
*/
return redisTemplate.opsForZSet().score(followeeKey, entityId) != null;
}
3.编写Controller层
3.1关注与取消关注按钮的实现(FollowController)
/**关注**/
@RequestMapping(value = "/follow", method = RequestMethod.POST)
@ResponseBody // 关注是异步请求
public String follow(int entityType, int entityId) {
followService.follow(hostHolder.getUser().getId(), entityType, entityId);
return CommunityUtil.getJSONString(0,"已关注");
}
/**取消关注**/
@RequestMapping(value = "/unfollow", method = RequestMethod.POST)
@ResponseBody // 关注是异步请求
public String unfollow(int entityType, int entityId) {
followService.unfollow(hostHolder.getUser().getId(), entityType, entityId);
return CommunityUtil.getJSONString(0,"已取消关注");
}
3.2主页中显示关注数量,粉丝数量(UserController)
/**
* 个人主页
*/
@RequestMapping(value = "/profile/{userId}", method = RequestMethod.GET)
public String getProfilePage(@PathVariable("userId") int userId, Model model) {
User user = userService.findUserById(userId);
if (user == null) {
throw new RuntimeException("该用户不存在!");
}
model.addAttribute("user", user);
// 点赞数量
....
// 关注数量(这里只考虑关注用户类型的情况)
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);
return "/site/profile";
}
4.编写JS异步请求和前端页面(核心部分)
$(function(){
$(".follow-btn").click(follow);
});
function follow() {
var btn = this;
if($(btn).hasClass("btn-info")) {
// 关注TA
$.post(
CONTEXT_PATH + "/follow",
// "entityId":$(btn).prev().val() 获取btn按钮上一个的值
{"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);
}});
}}
<input type="hidden" id="entityId" th:value="${user.id}">
<!-- hasFollowed为true:已关注 ->按钮变灰 ,false:未关注 ->按钮变蓝 -->
<button type="button" th:class="|btn ${hasFollowed?'btn-secondary':'btn-info'} btn-sm float-right mr-5 follow-btn|"
<!-- 只有登录过且当前登录用户不是看的自己的主页就显示已关注/关注TA -->
th:text="${hasFollowed?'已关注':'关注TA'}" th:if="${loginUser!=null && loginUser.id!=user.id}">关注TA</button>
<span>关注了 <a th:text="${followeeCount}">5</a> 人</span>
<span>关注者 <a th:text="${followerCount}">123</a> 人</span>
关注列表(同粉丝列表)
1.编写Service层(查询某用户关注的人)
/**查询某用户关注的人**/
public List<Map<String, Object>> findFollowees(int userId, int offset, int limit){
String followeeKey = RedisKeyUtil.getFolloweeKey(userId, ENTITY_TYPE_USER);
// 按最新时间倒序查询目标用户id封装在set<Integet>中
Set<Integer> targetIds = redisTemplate.opsForZSet().reverseRange(followeeKey, offset, offset + limit - 1);
if (targetIds == null) {
return null;
}
// 将user信息Map和redis用户关注时间Map一起封装到list
ArrayList<Map<String, Object>> list = new ArrayList<>();
for (Integer targetId: targetIds) {
HashMap<String, Object> map = new HashMap<>();
// 用户信息map
User user = userService.findUserById(targetId);
map.put("user", user);
// 目标用户关注时间map(将long型拆箱成基本数据类型)
Double score = redisTemplate.opsForZSet().score(followeeKey, targetId);
map.put("followeeTime", new Date(score.longValue()));
list.add(map);
}
return list;
}
2.编写Controller层
/** 查询某用户关注列表**/
@RequestMapping(value = "/followees/{userId}", method = RequestMethod.GET)
public String getFollowees(@PathVariable("userId")int userId, Page page, Model model) {
// 当前访问的用户信息
User user = userService.findUserById(userId);
// Controller层统一处理异常
if (user == null) {
throw new RuntimeException("该用户不存在!");
}
model.addAttribute("user", user);
// 设置分页信息
page.setLimit(3);
page.setPath("/followees/" + userId);
page.setRows((int) followService.findFolloweeCount(userId, ENTITY_TYPE_USER));
List<Map<String, Object>> userList = followService.findFollowees(userId, page.getOffset(), page.getLimit());
if (userList != null) {
for (Map<String, Object> map : userList) {
User u = (User) map.get("user");
map.put("hasFollowed", hasFollowed(u.getId()));
}
}
model.addAttribute("users", userList);
return "/site/followee";
}
/**判端当前登录用户与关注、粉丝列表的关注关系**/
private Boolean hasFollowed(int userId) {
if (hostHolder.getUser() == null) {
return false;
}
// 调用当前用户是否已关注user实体Service
return followService.hasFollowed(hostHolder.getUser().getId(), ENTITY_TYPE_USER, userId);
}
3.编写前端页面
3.1 带参数路径跳转
<span>关注了 <a th:href="@{|/followees/${user.id}|}" th:text="${followeeCount}">5</a> 人</span>
<span>关注者 <a th:href="@{|/followers/${user.id}|}" th:text="${followerCount}">123</a> 人</span>
3.2 列表页面
<li th:each="map:${users}">
<a th:href="@{|/user/profile/{map.user.id}|}">
<img th:src="${map.user.headerUrl}"alt="用户头像" >
</a>
<div>
<h6>
<span th:utext="${map.user.username}">落基山脉下的闲人</span>
<span>
关注于 <i th:text="${#dates.format(map.followerTime,'yyyy-MM-dd HH:mm:ss')}">2019-04-28 14:13:25</i>
</span>
</h6>
<div>
<input type="hidden" id="entityId" th:value="${map.user.id}">
<button type="button" th:class="|${map.hasFollowed?'btn-secondary':'btn-info'}|"
th:text="${map.hasFollowed?'已关注':'关注TA'}" th:if="${loginUser!=null && loginUser.id!=map.user.id}">关注TA</button>
</div>
</div>
</li>
系统通知功能(Kafka消息队列)
发送系统通知功能(点赞、关注、评论时通知)
1.编写Kafka消息队列事件Event实体类
/**
* Kafka消息队列事件(评论、点赞、关注事件
*/
public class Event {
// Kafka必要的主题变量
private String topic;
// 发起事件的用户id
private int userId;
// 用户发起事件的实体类型(评论、点赞、关注类型)
private int entityType;
// 用户发起事件的实体(帖子、评论、用户)id
private int entityId;
// 被发起事件的用户id(被评论、被点赞、被关注用户)
private int entityUserId;
// 其他可扩充内容对应Comment中的content->显示用户xxx评论、点赞、关注了xxx
private Map<String, Object> data = new HashMap<>();
public String getTopic() {
return topic;
}
// 注意这里所有set方法返回Event类型,变成链式编程
public Event setTopic(String topic) {
this.topic = topic;
return this;
}
public int getUserId() {
return userId;
}
public Event setUserId(int userId) {
this.userId = userId;
return this;
}
public int getEntityType() {
return entityType;
}
public Event setEntityType(int entityType) {
this.entityType = entityType;
return this;
}
public int getEntityId() {
return entityId;
}
public Event setEntityId(int entityId) {
this.entityId = entityId;
return this;
}
public int getEntityUserId() {
return entityUserId;
}
public Event setEntityUserId(int entityUserId) {
this.entityUserId = entityUserId;
return this;
}
public Map<String, Object> getData() {
return data;
}
// 方便外界直接调用key-value,而不用再封装一下传整个Map集合
public Event setData(String key, Object value) {
this.data.put(key, value);
return this;
}
}
2.编写Kafka生产者
/**
* Kafka事件生产者(主动调用)相当于一个开关
*/
@Component
public class EventProducer {
@Autowired
private KafkaTemplate kafkaTemplate;
// 处理事件
public void fireMessage(Event event) {
// 将事件发布到指定的主题,内容为event对象转化的json格式字符串
kafkaTemplate.send(event.getTopic(), JSONObject.toJSONString(event));
}
}
3.编写Kafka消费者
/**
* QQ:260602448--xumingyu
* Kafka事件消费者(被动调用)
* 对Message表扩充:1:系统通知,当生产者调用时,存入消息队列,消费者自动调用将event事件相关信息存入Message表
*/
@Component
public class EventConsumer implements CommunityConstant {
private static final Logger logger = LoggerFactory.getLogger(EventConsumer.class);
@Autowired
private MessageService messageService;
@KafkaListener(topics = {TOPIC_COMMENT, TOPIC_LIKE, TOPIC_FOLLOW})
public void handleCommentMessage(ConsumerRecord record) {
if (record == null || record.value() == null) {
logger.error("消息的内容为空!");
return;
}
// 将record.value字符串格式转化为Event对象
Event event = JSONObject.parseObject(record.value().toString(), Event.class);
// 注意:event中若data = null,是fastjson依赖版本的问题(不能太高1.0.xx)
if (event == null) {
logger.error("消息格式错误!");
return;
}
Message message = new Message();
message.setFromId(SYSTEM_USER_ID);
// Message表中ToId设置为被发起事件的用户id
message.setToId(event.getEntityUserId());
// ConversationId设置为事件的主题(点赞、评论、关注)
message.setConversationId(event.getTopic());
message.setCreateTime(new Date());
// 设置content为可扩展内容,封装在Map集合中,用于显示xxx评论..了你的帖子
HashMap<String, Object> content = new HashMap<>();
content.put("userId", event.getUserId());
content.put("entityId", event.getEntityId());
content.put("entityType", event.getEntityType());
// 将event.getData里的k-v存到context这个Map中,再封装进message
// Map.Entry是为了更方便的输出map键值对,Entry可以一次性获得key和value者两个值
if (!event.getData().isEmpty()) {
for (Map.Entry<String, Object> entry : event.getData().entrySet()) {
content.put(entry.getKey(), entry.getValue());
}
}
// 将content(map类型)转化成字符串类型封装进message
message.setContent(JSONObject.toJSONString(content));
messageService.addMessage(message);
}
}
4.在CommunityConstant添加Kafka主题静态常量
public interface CommunityConstant {
/**
* Kafka主题: 评论
*/
String TOPIC_COMMENT = "comment";
/**
* Kafka主题: 点赞
*/
String TOPIC_LIKE = "like";
/**
* Kafka主题: 关注
*/
String TOPIC_FOLLOW = "follow";
/**
* 系统用户ID
*/
int SYSTEM_USER_ID = 1;
}
5.处理触发评论事件CommentController
@RequestMapping(value = "/add/{discussPostId}", method = RequestMethod.POST)
public String addComment(@PathVariable("discussPostId") int discussPostId, Comment comment) {
comment.setUserId(hostHolder.getUser().getId());
comment.setStatus(0);
comment.setCreateTime(new Date());
commentService.addComment(comment);
/**
* 触发评论事件
* 评论完后,调用Kafka生产者,发送系统通知
*/
Event event = new Event()
.setTopic(TOPIC_COMMENT)
.setEntityId(comment.getEntityId())
.setEntityType(comment.getEntityType())
.setUserId(hostHolder.getUser().getId())
.setData("postId", discussPostId);
/**
* event.setEntityUserId要分情况设置被发起事件的用户id
* 1.评论的是帖子,被发起事件(评论)的用户->该帖子发布人id
* 2.评论的是用户的评论,被发起事件(评论)的用户->该评论发布人id
*/
if (comment.getEntityType() == ENTITY_TYPE_POST) {
// 先找评论表对应的帖子id,在根据帖子表id找到发帖人id
DiscussPost target = discussPostService.findDiscussPostById(comment.getEntityId());
event.setEntityUserId(target.getUserId());
} else if (comment.getEntityType() == ENTITY_TYPE_COMMENT) {
Comment target = commentService.findCommentById(comment.getEntityId());
event.setEntityUserId(target.getUserId());
}
eventProducer.fireMessage(event);
return "redirect:/discuss/detail/" + discussPostId;
}
6.处理触发关注事件FollowController
@RequestMapping(value = "/follow", method = RequestMethod.POST)
@ResponseBody // 关注是异步请求
public String follow(int entityType, int entityId) {
followService.follow(hostHolder.getUser().getId(), entityType, entityId);
/**
* 触发关注事件
* 关注完后,调用Kafka生产者,发送系统通知
*/
Event event = new Event()
.setTopic(TOPIC_FOLLOW)
.setUserId(hostHolder.getUser().getId())
.setEntityType(entityType)
.setEntityId(entityId)
.setEntityUserId(entityId);
// 用户关注实体的id就是被关注的用户id->EntityId=EntityUserId
eventProducer.fireMessage(event);
return CommunityUtil.getJSONString(0, "已关注");
}
7.处理触发点赞事件LikeController
@RequestMapping(value = "/like", method = RequestMethod.POST)
@ResponseBody
// 加了一个postId变量,对应的前端和js需要修改
public String like(int entityType, int entityId, int entityUserId, int postId) {
User user = hostHolder.getUser();
// 点赞
likeService.like(user.getId(), entityType, entityId, entityUserId);
// 获取对应帖子、留言的点赞数量
long likeCount = likeService.findEntityLikeCount(entityType, entityId);
// 获取当前登录用户点赞状态(1:已点赞 0:赞)
int likeStatus = likeService.findEntityLikeStatus(user.getId(), entityType, entityId);
// 封装结果到Map
Map<String, Object> map = new HashMap<>();
map.put("likeCount", likeCount);
map.put("likeStatus", likeStatus);
/**
* 触发点赞事件
* 只有点赞完后,才会调用Kafka生产者,发送系统通知,取消点赞不会调用事件
*/
if (likeStatus == 1) {
Event event = new Event()
.setTopic(TOPIC_LIKE)
.setEntityId(entityId)
.setEntityType(entityType)
.setUserId(user.getId())
.setEntityUserId(entityUserId)
.setData("postId", postId);
// 注意:data里面存postId是因为点击查看后链接到具体帖子的页面
eventProducer.fireMessage(event);
}
return CommunityUtil.getJSONString(0, null, map);
}
<!--对应的前端postId变量以及js的修改-->
<a
th:onclick="|like(this,1,${post.id},${post.userId},${post.id});|">
</a>
function like(btn, entityType, entityId, entityUserId, postId) {
$.post(
CONTEXT_PATH + "/like",
{"entityType": entityType, "entityId": entityId, "entityUserId": entityUserId, "postId":postId},
function(data) {
.....}
);}
查询系统通知
1.编写Dao层接口(及Mapper.xml)
/**
* 查询某个主题最新通知
*/
Message selectLatestNotice(@Param("userId")int userId, @Param("topic")String topic);
/**
* 查询某个主题通知个数
*/
int selectNoticeCount(@Param("userId")int userId, @Param("topic")String topic);
/**
* 查询某个主题未读个数(topic可为null,若为null:查询所有类系统未读通知个数)
*/
int selectNoticeUnreadCount(@Param("userId")int userId, @Param("topic")String topic);
/**
* 分页查询某个主题的详情
*/
List<Message> selectNotices(@Param("userId")int userId, @Param("topic")String topic, @Param("offset")int offset, @Param("limit")int limit);
<!--系统通知-->
<select id="selectLatestNotice" resultType="Message">
select <include refid="selectFields"></include>
from message
where id in (
select max(id) from message
where status != 2
and from_id = 1
and to_id = #{userId}
and conversation_id = #{topic}
)
</select>
<select id="selectNoticeCount" resultType="int">
select count(id) from message
where status != 2
and from_id = 1
and to_id = #{userId}
and conversation_id = #{topic}
</select>
<!--topic为null时查询所有类系统未读通知--->
<select id="selectNoticeUnreadCount" resultType="int">
select count(id) from message
where status = 0
and from_id = 1
and to_id = #{userId}
<if test="topic!=null">
and conversation_id = #{topic}
</if>
</select>
<select id="selectNotices" resultType="Message">
select <include refid="selectFields"></include>
from message
where status != 2
and from_id = 1
and to_id = #{userId}
and conversation_id = #{topic}
order by create_time desc
limit #{offset}, #{limit}
</select>
2.编写Service业务层
public Message findLatestNotice(int userId, String topic) {
return messageMapper.selectLatestNotice(userId, topic);
}
public int findNoticeCount(int userId, String topic) {
return messageMapper.selectNoticeCount(userId, topic);
}
public int findNoticeUnreadCount(int userId, String topic) {
return messageMapper.selectNoticeUnreadCount(userId, topic);
}
public List<Message> findNotices(int userId, String topic, int offset, int limit) {
return messageMapper.selectNotices(userId, topic, offset, limit);
}
3.编写MessageController层
3.1查询系统通知接口(评论类通知、点赞类通知、关注类通知三种类似)
/**
* 查询系统通知
*/
@RequestMapping(value = "/notice/list", method = RequestMethod.GET)
public String getNoticeList(Model model) {
User user = hostHolder.getUser();
/**查询评论类通知**/
Message message = messageService.findLatestNotice(user.getId(), TOPIC_COMMENT);
if (message != null) {
HashMap<String, Object> messageVO = new HashMap<>();
messageVO.put("message", message);
// 转化message表中content为HashMap<k,v>类型
String content = HtmlUtils.htmlUnescape(message.getContent());
Map<String, Object> data = JSONObject.parseObject(content, HashMap.class);
// 将content数据中的每一个字段都存入map
// 用于显示->用户[user] (评论、点赞、关注[entityType])...了你的(帖子、回复、用户[entityId]) 查看详情连接[postId]
messageVO.put("user", userService.findUserById((Integer) data.get("userId")));
messageVO.put("entityType", data.get("entityType"));
messageVO.put("entityId", data.get("entityId"));
messageVO.put("postId", data.get("postId"));
// 共几条会话
int count = messageService.findNoticeCount(user.getId(), TOPIC_COMMENT);
messageVO.put("count", count);
// 评论类未读数
int unreadCount = messageService.findNoticeUnreadCount(user.getId(), TOPIC_COMMENT);
messageVO.put("unreadCount", unreadCount);
model.addAttribute("commentNotice", messageVO);
}
/**查询点赞类通知**/
message = messageService.findLatestNotice(user.getId(), TOPIC_LIKE);
if (message != null) {
HashMap<String, Object> messageVO = new HashMap<>();
messageVO.put("message", message);
// 转化message表中content为HashMap<k,v>类型
String content = HtmlUtils.htmlUnescape(message.getContent());
Map<String, Object> data = JSONObject.parseObject(content, HashMap.class);
// 将content数据中的每一个字段都存入map
// 用于显示->用户[user] (评论、点赞、关注[entityType])...了你的(帖子、回复、用户[entityId]) 查看详情连接[postId]
messageVO.put("user", userService.findUserById((Integer) data.get("userId")));
messageVO.put("entityType", data.get("entityType"));
messageVO.put("entityId", data.get("entityId"));
messageVO.put("postId", data.get("postId"));
// 共几条会话
int count = messageService.findNoticeCount(user.getId(), TOPIC_LIKE);
messageVO.put("count", count);
// 点赞类未读数
int unreadCount = messageService.findNoticeUnreadCount(user.getId(), TOPIC_LIKE);
messageVO.put("unreadCount", unreadCount);
model.addAttribute("likeNotice", messageVO);
}
/**查询关注类通知**/
message = messageService.findLatestNotice(user.getId(), TOPIC_FOLLOW);
if (message != null) {
HashMap<String, Object> messageVO = new HashMap<>();
messageVO.put("message", message);
// 转化message表中content为HashMap<k,v>类型
String content = HtmlUtils.htmlUnescape(message.getContent());
Map<String, Object> data = JSONObject.parseObject(content, HashMap.class);
// 将content数据中的每一个字段都存入map
// 用于显示->用户[user] (评论、点赞、关注)...了你的(帖子、回复、用户[entityType]) 查看详情连接[postId]
messageVO.put("user", userService.findUserById((Integer) data.get("userId")));
messageVO.put("entityType", data.get("entityType"));
messageVO.put("entityId", data.get("entityId"));
messageVO.put("postId", data.get("postId"));
// 共几条会话
int count = messageService.findNoticeCount(user.getId(), TOPIC_FOLLOW);
messageVO.put("count", count);
// 关注类未读数
int unreadCount = messageService.findNoticeUnreadCount(user.getId(), TOPIC_FOLLOW);
messageVO.put("unreadCount", unreadCount);
model.addAttribute("followNotice", messageVO);
}
// 查询未读私信数量
int letterUnreadCount = messageService.findLetterUnreadCount(user.getId(), null);
model.addAttribute("letterUnreadCount", letterUnreadCount);
// 查询所有未读系统通知数量
int noticeUnreadCount = messageService.findNoticeUnreadCount(user.getId(), null);
model.addAttribute("noticeUnreadCount", noticeUnreadCount);
return "/site/notice";
}
3.2查询系统通知详情页接口
/**
* 查询系统通知详情页(分页)
*/
@RequestMapping(value = "/notice/detail/{topic}", method = RequestMethod.GET)
public String getNoticeDetail(@PathVariable("topic")String topic, Page page, Model model) {
User user = hostHolder.getUser();
page.setLimit(5);
page.setPath("/notice/detail/" + topic);
page.setRows(messageService.findNoticeCount(user.getId(), topic));
List<Message> noticeList = messageService.findNotices(user.getId(), topic, page.getOffset(), page.getLimit());
// 聚合拼接User
List<Map<String, Object>> noticeVoList = new ArrayList<>();
if (noticeList != null) {
for (Message notice : noticeList) {
HashMap<String, Object> map = new HashMap<>();
// 将查询出来的每一个通知封装Map
map.put("notice", notice);
// 发起事件的user
map.put("user", userService.findUserById(user.getId()));
// 把message中的content内容转化Object
String content = HtmlUtils.htmlUnescape(notice.getContent());
Map<String, Object> data = JSONObject.parseObject(content, HashMap.class);
map.put("entityType", data.get("entityType"));
map.put("entityId", data.get("entityId"));
map.put("postId", data.get("postId"));
// 系统通知->id=1的系统用户
map.put("fromUser", userService.findUserById(notice.getFromId()));
noticeVoList.add(map);
}
}
model.addAttribute("notices", noticeVoList);
//设置已读(当打开这个页面是就更改status =1)
List<Integer> ids = getLetterIds(noticeList);
if (!ids.isEmpty()) {
messageService.readMessage(ids);
}
return "/site/notice-detail";
}
4.通过AOP编程实现查询未读消息总数(私信消息+系统消息)
4.1编写MessageInterceptor拦截器
@Component
public class MessageInterceptor implements HandlerInterceptor {
@Autowired
private HostHolder hostHolder;
@Autowired
private MessageService messageService;
// 查询未读消息总数(AOP),controller之后,渲染模板之前
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
User user = hostHolder.getUser();
if (user != null && modelAndView != null) {
int letterUnreadCount = messageService.findLetterUnreadCount(user.getId(), null);
int noticeUnreadCount = messageService.findNoticeUnreadCount(user.getId(), null);
modelAndView.addObject("allUnreadCount", letterUnreadCount + noticeUnreadCount);
}
}}
// index页前端对应代码
<li th:if="${loginUser!=null}">
<a th:href="@{/letter/list}">消息
<span th:text="${allUnreadCount!=0?allUnreadCount:''}">消息未读总数</span>
</a>
</li>
4.2注册拦截器
@Autowired
private MessageInterceptor messageInterceptor;
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(messageInterceptor)
.excludePathPatterns("/* */*.css", "/**/ *.js", "/* */*.png", "/ **/ *.jpg", "/* */*.jpeg");
}
5.编写前端页面(核心部分)
5.1系统通知页
<li>
<a class="active" th:href="@{/notice/list}">
系统通知<span th:text="${noticeUnreadCount}" th:if="${noticeUnreadCount!=0}">系统通知未读数</span>
</a>
</li>
<!-- 通知列表 -->
<ul class="list-unstyled">
<!--评论类通知-->
<li th:if="${commentNotice!=null}">
<span th:text="${commentNotice.unreadCount!=0?commentNotice.unreadCount:''}">评论通知未读数</span>
<img src="http://xxx.png" alt="通知图标">
<h6>
<span>评论</span>
<span th:text="${#dates.format(commentNotice.message.createTime,'yyyy-MM-dd HH:mm:ss')}">2019-04-28 14:13:25</span>
</h6>
<div>
<a th:href="@{/notice/detail/comment}">
用户 <i th:utext="${commentNotice.user.username}">评论发起人用户名</i>
评论了你的<b th:text="${commentNotice.entityType==1?'帖子':'回复'}">帖子</b> ...</a>
<ul>
<span>共 <i th:text="${commentNotice.count}">3</i> 条会话</span>
</ul>
</div>
</li>
<!--点赞类通知-->
<li th:if="${likeNotice!=null}">
<span th:text="${likeNotice.unreadCount!=0?likeNotice.unreadCount:''}">3</span>
<img src="http://like.png" alt="通知图标">
<div>
<h6>
<span>赞</span>
<span th:text="${#dates.format(likeNotice.message.createTime,'yyyy-MM-dd HH:mm:ss')}">2019-04-28 14:13:25</span>
</h6>
<div>
<a th:href="@{/notice/detail/like}">
用户
<i th:utext="${likeNotice.user.username}">nowcoder</i>
点赞了你的<b th:text="${likeNotice.entityType==1?'帖子':'回复'}">帖子</b> ...
</a>
<ul>
<span class="text-primary">共 <i th:text="${likeNotice.count}">3</i> 条会话</span>
</ul>
</div>
</div>
</li>
<!--关注类通知-->
<li th:if="${followNotice!=null}">
<span th:text="${followNotice.unreadCount!=0?followNotice.unreadCount:''}">3</span>
<img src="http://follow.png" class="mr-4 user-header" alt="通知图标">
<div>
<h6>
<span>关注</span>
<span th:text="${#dates.format(followNotice.message.createTime,'yyyy-MM-dd HH:mm:ss')}">2019-04-28 14:13:25</span>
</h6>
<div>
<a th:href="@{/notice/detail/follow}">
用户
<i th:utext="${followNotice.user.username}">nowcoder</i>
关注了你 ...
</a>
<ul>
<span class="text-primary">共 <i th:text="${followNotice.count}">3</i> 条会话</span>
</ul>
</div>
</div>
</li>
</ul>
5.2系统通知详情页
<div class="col-4 text-right">
<button type="button" class="btn btn-secondary btn-sm" onclick="back();">返回</button>
</div>
<!-- 通知列表 -->
<ul>
<li th:each="map:${notices}">
<img th:src="${map.fromUser.headerUrl}" alt="系统图标">
<div>
<div>
<strong th:utext="${map.fromUser.username}">系统名</strong>
<small th:text="${#dates.format(map.notice.createTime,'yyyy-MM-dd HH:mm:ss')}">2019-04-25 15:49:32</small>
</div>
<div>
<!--显示评论信息-->
<span th:if="${topic.equals('comment')}">
用户
<i th:utext="${map.user.username}">发起事件人</i>
评论了你的<b th:text="${map.entityType==1?'帖子':'回复'}">帖子</b>,
<a th:href="@{|/discuss/detail/${map.postId}|}">点击查看</a> !
</span>
<!--显示点赞信息-->
<span th:if="${topic.equals('like')}">
用户
<i th:utext="${map.user.username}">发起事件人</i>
点赞了你的<b th:text="${map.entityType==1?'帖子':'回复'}">帖子</b>,
<a th:href="@{|/discuss/detail/${map.postId}|}">点击查看</a> !
</span>
<!--显示关注信息-->
<span th:if="${topic.equals('follow')}">
用户
<i th:utext="${map.user.username}">发起事件人</i>
关注了你,
<a th:href="@{|/user/profile/${map.user.id}|}">点击查看</a> !
</span>
</div>
</div>
</li>
</ul>
<script>
function back() {
location.href = CONTEXT_PATH + "/notice/list";
}
</script>
搜索功能(Elasticsearch+Kafka)
1.编写实体类映射到Elasticsearch服务器
// Elasticsearch表名
@Document(indexName = "discusspost", type = "_doc", shards = 6, replicas = 3)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class DiscussPost {
@Id
private int id;
// Elaticsearch与数据库表映射
@Field(type = FieldType.Integer)
private int userId;
// analyzer:最大中文分词解析器, searchAnalyzer:智能中文分词解析器
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
private String title;
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
private String content;
@Field(type = FieldType.Integer)
private int type;
@Field(type = FieldType.Integer)
private int status;
@Field(type = FieldType.Date)
private Date createTime;
@Field(type = FieldType.Integer)
private int commentCount;
@Field(type = FieldType.Double)
private double score;
2.编写xxxRepository接口继承ElasticsearchRepository<Class, Integer>
/**
* ElasticsearchRepository<DiscussPost, Integer>
* DiscussPost:接口要处理的实体类
* Integer:实体类中的主键是什么类型
* ElasticsearchRepository:父接口,其中已经事先定义好了对es服务器访问的增删改查各种方法。Spring会给它自动做一个实现,我们直接去调就可以了。
*/
@Repository
public interface DiscussPostRepository extends ElasticsearchRepository<DiscussPost, Integer> {
}
3.编写ElasticsearchService业务层
/**
* 用Elasticsearch服务器搜索帖子service
*/
@Service
public class ElasticsearchService {
@Autowired
private DiscussPostRepository discussRepository;
@Autowired
private ElasticsearchTemplate elasticTemplate;
public void saveDiscussPost(DiscussPost post) {
discussRepository.save(post);
}
public void deleteDiscussPost(int id) {
discussRepository.deleteById(id);
}
/**
* Elasticsearch高亮搜索
* current:当前页(不是offset起始页)
*/
public Page<DiscussPost> searchDiscussPost(String keyword, int current, int limit) {
SearchQuery searchQuery = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.multiMatchQuery(keyword, "title", "content"))
.withSort(SortBuilders.fieldSort("type").order(SortOrder.DESC))
.withSort(SortBuilders.fieldSort("score").order(SortOrder.DESC))
.withSort(SortBuilders.fieldSort("createTime").order(SortOrder.DESC))
.withPageable(PageRequest.of(current, limit))
.withHighlightFields(
new HighlightBuilder.Field("title").preTags("<em>").postTags("</em>"),
new HighlightBuilder.Field("content").preTags("<em>").postTags("</em>")
).build();
// new SearchResultMapper()匿名类,处理高亮
return elasticTemplate.queryForPage(searchQuery, DiscussPost.class, new SearchResultMapper() {
@Override
public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> aClass, Pageable pageable) {
SearchHits hits = response.getHits();
if (hits.getTotalHits() <= 0) {
return null;
}
List<DiscussPost> list = new ArrayList<>();
for (SearchHit hit : hits) {
DiscussPost post = new DiscussPost();
// elasticsearch中将json格式数据封装为了map,在将map字段存进post中
String id = hit.getSourceAsMap().get("id").toString();
post.setId(Integer.valueOf(id));
String userId = hit.getSourceAsMap().get("userId").toString();
post.setUserId(Integer.valueOf(userId));
String title = hit.getSourceAsMap().get("title").toString();
post.setTitle(title);
String content = hit.getSourceAsMap().get("content").toString();
post.setContent(content);
String status = hit.getSourceAsMap().get("status").toString();
post.setStatus(Integer.valueOf(status));
// createTime字符串是Long类型
String createTime = hit.getSourceAsMap().get("createTime").toString();
post.setCreateTime(new Date(Long.valueOf(createTime)));
String commentCount = hit.getSourceAsMap().get("commentCount").toString();
post.setCommentCount(Integer.valueOf(commentCount));
// 处理高亮显示的结果
HighlightField titleField = hit.getHighlightFields().get("title");
if (titleField != null) {
// [0]->搜寻结果为多段时,取第一段
post.setTitle(titleField.getFragments()[0].toString());
}
HighlightField contentField = hit.getHighlightFields().get("content");
if (contentField != null) {
post.setContent(contentField.getFragments()[0].toString());
}
list.add(post);
}
return new AggregatedPageImpl(list, pageable,
hits.getTotalHits(), response.getAggregations(), response.getScrollId(), hits.getMaxScore());
}
});
}
}
4.修改发布帖子和增加评论Controller
发布帖子时,将帖子异步提交到Elasticsearch服务器
增加评论时,将帖子异步提交到Elasticsearch服务器
/**
* Kafka主题: 发布帖子(常量接口)
*/
String TOPIC_PUBILISH = "publish";
/**--------------------------------------------------------**/
@RequestMapping(value = "/add/{discussPostId}", method = RequestMethod.POST)
public String addComment(@PathVariable("discussPostId") int discussPostId, Comment comment) {
// ............
/**
* 增加评论时,将帖子异步提交到Elasticsearch服务器
* 通过Kafka消息队列去提交,修改Elasticsearch中帖子的评论数
*/
//若评论为帖子类型时,才需要加入消息队列处理
if (comment.getEntityType() == ENTITY_TYPE_POST) {
event = new Event()
.setTopic(TOPIC_PUBILISH)
.setUserId(comment.getUserId())
.setEntityType(ENTITY_TYPE_POST)
.setEntityId(discussPostId);
eventProducer.fireMessage(event);
}
return "redirect:/discuss/detail/" + discussPostId;
}
@RequestMapping(value = "/add", method = RequestMethod.POST)
@ResponseBody
// 异步请求要加@ResponseBody,且不要在Controller层用Model
public String addDiscussPost(String title, String content) {
//.................
/**
* 发布帖子时,将帖子异步提交到Elasticsearch服务器
* 通过Kafka消息队列去提交,将新发布的帖子存入Elasticsearch
*/
Event event = new Event()
.setTopic(TOPIC_PUBILISH)
.setUserId(user.getId())
.setEntityType(ENTITY_TYPE_POST)
.setEntityId(post.getId());
eventProducer.fireMessage(event);
// 返回Json格式字符串,报错的情况将来统一处理
return CommunityUtil.getJSONString(0, "发布成功!");
}
5.在消费组件中增加方法(消费帖子发布事件)
/**
* 消费帖子发布事件,将新增的帖子和添加评论后帖子评论数通过消息队列的方式save进Elastisearch服务器中
*/
@KafkaListener(topics = {TOPIC_PUBILISH})
public void handleDiscussPostMessage(ConsumerRecord record) {
if (record == null || record.value() == null) {
logger.error("消息的内容为空!");
return;
}
// 将record.value字符串格式转化为Event对象
Event event = JSONObject.parseObject(record.value().toString(), Event.class);
// 注意:event若data=null,是fastjson依赖版本的问题
if (event == null) {
logger.error("消息格式错误!");
return;
}
DiscussPost post = discussPostService.findDiscussPostById(event.getEntityId());
elasticsearchService.saveDiscussPost(post);
}
6.编写SearchController类
@Controller
public class SearchController implements CommunityConstant {
@Autowired
private UserService userService;
@Autowired
private LikeService likeService;
@Autowired
private ElasticsearchService elasticsearchService;
// search?keyword=xxx
@RequestMapping(value = "/search", method = RequestMethod.GET)
public String search(String keyword, Page page, Model model) {
// 搜索帖子
// 在调用elasticsearchService完成搜索的时候,查询条件设置的是从第几页开始,所以要填getCurrent,填getOffset会导致翻页的时候查询错误
org.springframework.data.domain.Page<DiscussPost> searchResult =
elasticsearchService.searchDiscussPost(keyword, page.getCurrent() - 1, page.getLimit());
// 聚合数据
List<Map<String, Object>> discussPosts = new ArrayList<>();
if (searchResult != null) {
for (DiscussPost post : searchResult) {
Map<String, Object> map = new HashMap<>();
// 帖子
map.put("post", post);
// 作者
map.put("user", userService.findUserById(post.getUserId()));
// 点赞数量
map.put("likeCount", likeService.findEntityLikeCount(ENTITY_TYPE_POST, post.getId()));
discussPosts.add(map);
}
}
model.addAttribute("discussPosts", discussPosts);
// 为了页面上取的默认值方便
model.addAttribute("keyword", keyword);
page.setPath("/search?keyword=" + keyword);
page.setRows(searchResult == null ? 0 :(int) searchResult.getTotalElements());
return "/site/search";
}
}
7.编写前端页面(核心部分)
<!-- 搜索表单 -->
<form method="get" th:action="@{/search}">
<!--th:value->设置默认值 model.addAttribute("keyword", keyword) -->
<input type="search" aria-label="Search" name="keyword" th:value="${keyword}"/>
<button type="submit">搜索</button>
</form>
<li th:each="map:${discussPosts}">
<img th:src="${map.user.headerUrl}" alt="用户头像" style="width:50px;height:50px">
<div>
<a th:href="@{|/discuss/detail/${map.post.id}|}" th:utext="${map.post.title}"><em>搜索词高亮显示</em>可链接的帖子标题</a>
<div th:utext="${map.post.content}">
帖子内容<em>搜索词高亮显示</em>
</div>
<div>
<u th:utext="${map.user.username}">寒江雪</u>
发布于 <b th:text="${#dates.format(map.post.createTime,'yyyy-MM-dd HH:mm:ss')}">帖子发布时间</b>
<ul>
<li>赞 <i th:text="${map.likeCount}">11</i></li>
<li>|</li>
<li>回复 <i th:text="${map.post.commentCount}">7</i></li>
</ul>
</div>
</div>
</li>
权限控制
部署SpringSecurity权限控制
1.配置SecurityConfig类
登录检查:废弃之前的拦截器配置,采用SpringSecurity
权限配置:对所有请求分配访问权限
/**
* springsecurity配置
* 之所以没有configure(AuthenticationManagerBuilder auth),是因为要绕过security自带的方案
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter implements CommunityConstant {
@Override
public void configure(WebSecurity web) throws Exception {
// 忽略静态资源
web.ignoring().antMatchers("/resources/* *");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 授权
http.authorizeRequests()
// 需要授权的请求
.antMatchers(
"/user/setting",
"/user/upload",
"/discuss/add",
"/comment/add/* *",
"/letter/* *",
"/notice/* *",
"/like",
"/follow",
"/unfollow"
)
// 这3中权限可以访问以上请求
.hasAnyAuthority(
AUTHORITY_USER,
AUTHORITY_ADMIN,
AUTHORITY_MODERATOR
)
// 其他请求方行
.anyRequest().permitAll()
// 禁用 防止csrf攻击功能
.and().csrf().disable();
// 权限不够时的处理
http.exceptionHandling()
.authenticationEntryPoint(new AuthenticationEntryPoint() {
// 没有登录
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
// 同步请求重定向返回HTML,异步请求返回json
String xRequestedWith = request.getHeader("x-requested-with");
if ("XMLHttpRequest".equals(xRequestedWith)) {
// 处理异步请求
response.setContentType("application/plain;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write(CommunityUtil.getJSONString(403, "你还没有登录哦!"));
} else {
response.sendRedirect(request.getContextPath() + "/login");
}
}
})
// 权限不足
.accessDeniedHandler(new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
String xRequestedWith = request.getHeader("x-requested-with");
if ("XMLHttpRequest".equals(xRequestedWith)) {
response.setContentType("application/plain;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write(CommunityUtil.getJSONString(403, "你没有访问此功能的权限!"));
} else {
response.sendRedirect(request.getContextPath() + "/denied");
}
}
});
// Security底层默认会拦截/logout请求,进行退出处理.
// 覆盖它默认的逻辑,才能执行我们自己的退出代码.
//底层:private String logoutUrl = "/logout";
http.logout().logoutUrl("/securitylogout");
}
}
2.编写UserService增加自定义登录认证方法绕过security自带认证流程
/**绕过Security认证流程,采用原来的认证方案,封装认证结果**/
public Collection<? extends GrantedAuthority> getAuthorities(int userId) {
User user = this.findUserById(userId);
List<GrantedAuthority> list = new ArrayList<>();
list.add(new GrantedAuthority() {
@Override
public String getAuthority() {
switch (user.getType()) {
case 1:
return AUTHORITY_ADMIN;
case 2:
return AUTHORITY_MODERATOR;
default:
return AUTHORITY_USER;
}
}
});
return list;
}
3.编写登录凭证拦截器LoginTicketInterceptor
构建用户认证结果,并存入SecurityContext,以便于Security进行授权
@Override
/**在Controller访问所有路径之前获取凭证**/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//...................................
if (loginTicket != null && loginTicket.getStatus() == 0 && loginTicket.getExpired().after(new Date())) {
// ...............................
/**
* QQ:260602448
* 构建用户认证结果,并存入SecurityContext,以便于Security进行授权
*/
Authentication authentication = new UsernamePasswordAuthenticationToken(
user, user.getPassword(), userService.getAuthorities(user.getId()));
SecurityContextHolder.setContext(new SecurityContextImpl(authentication));
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 释放线程资源
hostHolder.clear();
// 释放SecurityContext资源
SecurityContextHolder.clearContext();
}
4.退出登录时释放SecurityContext资源
/**
* 退出登录功能
*/
@RequestMapping(value = "/logout", method = RequestMethod.GET)
public String logout(@CookieValue("ticket") String ticket) {
userService.logout(ticket);
// 释放SecurityContext资源
SecurityContextHolder.clearContext();
return "redirect:/login";
}
5.注意:防止CSRF攻击
CSRF攻击原理
由于服务端SpringSecurity自带防止CSRF攻击,因此只要编写前端页面防止CSRF攻击即可 \ (常发生在提交表单时)
<!--访问该页面时,在此处生成CSRF令牌.-->
<meta name="_csrf" th:content="${_csrf.token}">
<meta name="_csrf_header" th:content="${_csrf.headerName}">
Ajax异步请求时携带该参数
function publish() {
$("#publishModal").modal("hide");
// 发送AJAX请求之前,将CSRF令牌设置到请求的消息头中.
var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");
$(document).ajaxSend(function(e, xhr, options){
xhr.setRequestHeader(header, token);
});
// ...............................
}
置顶、加精、删除
1.编写Mapper、Service层
思路:改变帖子状态
置顶:type = (0-正常,1-置顶) 加精:status = (0-正常,1-加精,2-删除)
int updateType(@Param("id")int id,@Param("type") int type);
int updateStatus(@Param("id")int id,@Param("status") int status);
<!--------------------Mapper.xml------------------------->
<update id="updateType">
update discuss_post set type = #{type} where id = #{id}
</update>
<update id="updateStatus">
update discuss_post set status = #{status} where id = #{id}
</update>
<!--------------------Service层------------------------->
public int updateType(int id, int type) {
return discussPostMapper.updateType(id, type);
}
public int updateStatus(int id, int status) {
return discussPostMapper.updateStatus(id, status);
}
2.编写DiscussPostController层
// 置顶、取消置顶(与以下类似)
@RequestMapping(value = "/top", method = RequestMethod.POST)
@ResponseBody
public String setTop(int id) {
DiscussPost post = discussPostService.findDiscussPostById(id);
// 获取置顶状态,1为置顶,0为正常状态,1^1=0 0^1=1
int type = post.getType() ^ 1;
discussPostService.updateType(id, type);
// 返回结果给JS异步请求
HashMap<String, Object> map = new HashMap<>();
map.put("type", type);
// 触发事件,修改Elasticsearch中的帖子type
Event event = new Event()
.setTopic(TOPIC_PUBILISH)
.setUserId(hostHolder.getUser().getId())
.setEntityType(ENTITY_TYPE_POST)
.setEntityId(id);
eventProducer.fireMessage(event);
return CommunityUtil.getJSONString(0, null, map);
}
// 加精、取消加精
@RequestMapping(value = "/wonderful", method = RequestMethod.POST)
@ResponseBody
public String setWonderful(int id) {
DiscussPost post = discussPostService.findDiscussPostById(id);
int status = post.getStatus() ^ 1;
discussPostService.updateStatus(id, status);
// 返回结果给JS异步请求
HashMap<String, Object> map = new HashMap<>();
map.put("status", status);
// 触发事件,修改Elasticsearch中的帖子status
Event event = new Event()
.setTopic(TOPIC_PUBILISH)
.setUserId(hostHolder.getUser().getId())
.setEntityType(ENTITY_TYPE_POST)
.setEntityId(id);
eventProducer.fireMessage(event);
return CommunityUtil.getJSONString(0, null, map);
}
// 删除
@RequestMapping(value = "/delete", method = RequestMethod.POST)
@ResponseBody
public String setDelete(int id) {
discussPostService.updateStatus(id, 2);
// 触发删帖事件,将帖子从Elasticsearch中删除
Event event = new Event()
.setTopic(TOPIC_DELETE)
.setUserId(hostHolder.getUser().getId())
.setEntityType(ENTITY_TYPE_POST)
.setEntityId(id);
eventProducer.fireMessage(event);
return CommunityUtil.getJSONString(0);
}
3.编写Kafka消费者中删除(TOPIC_DELETE)的主题事件
/**帖子删除事件**/
@KafkaListener(topics = {TOPIC_DELETE})
public void handleDeleteMessage(ConsumerRecord record) {
if (record == null || record.value() == null) {
logger.error("消息的内容为空!");
return;
}
// 将record.value字符串格式转化为Event对象
Event event = JSONObject.parseObject(record.value().toString(), Event.class);
// 注意:event若data=null,是fastjson依赖版本的问题
if (event == null) {
logger.error("消息格式错误!");
return;
}
elasticsearchService.deleteDiscussPost(event.getEntityId());
}
4.在SecurityConfig中给予(置顶、加精、删除)权限
// 授权
http.authorizeRequests()
// 需要授权的请求
// ...............
)
.antMatchers(
"/discuss/top",
"/discuss/wonderful"
)
.hasAnyAuthority(
AUTHORITY_MODERATOR // 版主授予加精、置顶权限
)
.antMatchers(
"/discuss/delete"
)
.hasAnyAuthority(
AUTHORITY_ADMIN // 管理员授予删除帖子权限
)
// 其他请求方行
.anyRequest().permitAll()
// 禁用 防止csrf攻击功能
.and().csrf().disable();
5.编写前端代码(核心部分)
5.1引用pom.xml,使用sec:xxx
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
5.2 引入thymeleaf支持security的头文件
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<div>
<input type="hidden" id="postId" th:value="${post.id}">
<button type="button" class="btn" id="topBtn"
th:text="${post.type==1?'取消置顶':'置顶'}" sec:authorize="hasAnyAuthority('moderator')">置顶</button>
<button type="button" class="btn" id="wonderfulBtn"
th:text="${post.status==1?'取消加精':'加精'}" sec:authorize="hasAnyAuthority('moderator')">加精</button>
<button type="button" class="btn" id="deleteBtn"
th:disabled="${post.status==2}" sec:authorize="hasAnyAuthority('admin')">删除</button>
</div>
5.3 编写JS中的异步Ajax请求
// 页面加载完以后调用
$(function(){
$("#topBtn").click(setTop);
$("#wonderfulBtn").click(setWonderful);
$("#deleteBtn").click(setDelete);
});
// 置顶、取消置顶
function setTop() {
$.post(
CONTEXT_PATH + "/discuss/top",
{"id":$("#postId").val()},
function(data) {
data = $.parseJSON(data);
if(data.code == 0) {
$("#topBtn").text(data.type == 1 ? '取消置顶':'置顶');
} else {
alert(data.msg);
}
}
);
}
// 加精、取消加精
function setWonderful() {
$.post(
CONTEXT_PATH + "/discuss/wonderful",
{"id":$("#postId").val()},
function(data) {
data = $.parseJSON(data);
if(data.code == 0) {
$("#wonderfulBtn").text(data.status == 1 ? '取消加精':'加精');
} else {
alert(data.msg);
}
}
);
}
// 删除
function setDelete() {
$.post(
CONTEXT_PATH + "/discuss/delete",
{"id":$("#postId").val()},
function(data) {
data = $.parseJSON(data);
if(data.code == 0) {
location.href = CONTEXT_PATH + "/index";
} else {
alert(data.msg);
}
}
);
}