Compare commits
2 Commits
362c32cbd6
...
b70d9073d8
| Author | SHA1 | Date | |
|---|---|---|---|
| b70d9073d8 | |||
| f0afb23a73 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -50,3 +50,4 @@ build/
|
||||
/han-note-kv/han-note-kv-biz/logs/
|
||||
/han-note-note/han-note-note-biz/src/main/resources/application-dev.yml
|
||||
/han-note-user-relation/han-note-user-relation-biz/src/main/resources/application-dev.yml
|
||||
/han-note-user-relation/han-note-user-relation-biz/logs/
|
||||
|
||||
5
.idea/inspectionProfiles/Project_Default.xml
generated
5
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -1,6 +1,11 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="DuplicatedCode" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
<Languages>
|
||||
<language minSize="56" name="Java" />
|
||||
</Languages>
|
||||
</inspection_tool>
|
||||
<inspection_tool class="IncorrectHttpHeaderInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="customHeaders">
|
||||
<set>
|
||||
|
||||
@@ -38,6 +38,12 @@ public class NoteController {
|
||||
return noteService.updateNote(updateNoteReqVO);
|
||||
}
|
||||
|
||||
@PostMapping(value = "/delete")
|
||||
@ApiOperationLog(description = "笔记删除")
|
||||
public Response<?> deleteNote(@Validated @RequestBody DeleteNoteReqVO deleteNoteReqVO) {
|
||||
return noteService.deleteNote(deleteNoteReqVO);
|
||||
}
|
||||
|
||||
@PostMapping(value = "/visible/onlyme")
|
||||
@ApiOperationLog(description = "笔记仅对自己可见")
|
||||
public Response<?> visibleOnlyMe(@Validated @RequestBody UpdateNoteVisibleOnlyMeReqVO updateNoteVisibleOnlyMeReqVO) {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.hanserwei.hannote.note.biz.model.vo;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Builder
|
||||
public class DeleteNoteReqVO {
|
||||
|
||||
@NotNull(message = "笔记 ID 不能为空")
|
||||
private Long id;
|
||||
}
|
||||
@@ -28,6 +28,13 @@ public interface NoteService extends IService<NoteDO> {
|
||||
*/
|
||||
Response<?> updateNote(UpdateNoteReqVO updateNoteReqVO);
|
||||
|
||||
/**
|
||||
* 笔记删除
|
||||
* @param deleteNoteReqVO 笔记删除请求
|
||||
* @return 笔记删除结果
|
||||
*/
|
||||
Response<?> deleteNote(DeleteNoteReqVO deleteNoteReqVO);
|
||||
|
||||
/**
|
||||
* 笔记仅对自己可见
|
||||
* @param updateNoteVisibleOnlyMeReqVO 笔记仅对自己可见请求
|
||||
|
||||
@@ -334,6 +334,20 @@ public class NoteServiceImpl extends ServiceImpl<NoteDOMapper, NoteDO> implement
|
||||
}
|
||||
}
|
||||
|
||||
// 当前登录用户 ID
|
||||
Long currUserId = LoginUserContextHolder.getUserId();
|
||||
NoteDO selectNoteDO = this.getById(noteId);
|
||||
|
||||
// 笔记不存在
|
||||
if (Objects.isNull(selectNoteDO)) {
|
||||
throw new ApiException(ResponseCodeEnum.NOTE_NOT_FOUND);
|
||||
}
|
||||
|
||||
// 判断权限:非笔记发布者不允许更新笔记
|
||||
if (!Objects.equals(currUserId, selectNoteDO.getCreatorId())) {
|
||||
throw new ApiException(ResponseCodeEnum.NOTE_CANT_OPERATE);
|
||||
}
|
||||
|
||||
// 话题
|
||||
Long topicId = updateNoteReqVO.getTopicId();
|
||||
String topicName = null;
|
||||
@@ -428,11 +442,68 @@ public class NoteServiceImpl extends ServiceImpl<NoteDOMapper, NoteDO> implement
|
||||
return Response.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Response<?> deleteNote(DeleteNoteReqVO deleteNoteReqVO) {
|
||||
// 笔记 ID
|
||||
Long noteId = deleteNoteReqVO.getId();
|
||||
|
||||
NoteDO selectNoteDO = this.getById(noteId);
|
||||
|
||||
// 判断笔记是否存在
|
||||
if (Objects.isNull(selectNoteDO)) {
|
||||
throw new ApiException(ResponseCodeEnum.NOTE_NOT_FOUND);
|
||||
}
|
||||
|
||||
// 判断权限:非笔记发布者不允许删除笔记
|
||||
Long currUserId = LoginUserContextHolder.getUserId();
|
||||
if (!Objects.equals(currUserId, selectNoteDO.getCreatorId())) {
|
||||
throw new ApiException(ResponseCodeEnum.NOTE_CANT_OPERATE);
|
||||
}
|
||||
|
||||
// 逻辑删除
|
||||
NoteDO noteDO = NoteDO.builder()
|
||||
.id(noteId)
|
||||
.status(NoteStatusEnum.DELETED.getCode())
|
||||
.updateTime(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
boolean updateSuccess = this.updateById(noteDO);
|
||||
|
||||
// 若失败,则表示该笔记不存在
|
||||
if (!updateSuccess) {
|
||||
throw new ApiException(ResponseCodeEnum.NOTE_NOT_FOUND);
|
||||
}
|
||||
|
||||
// 删除缓存
|
||||
String noteDetailRedisKey = RedisKeyConstants.buildNoteDetailKey(noteId);
|
||||
redisTemplate.delete(noteDetailRedisKey);
|
||||
|
||||
// 同步发送广播模式 MQ,将所有实例中的本地缓存都删除掉
|
||||
rocketMQTemplate.syncSend(MQConstants.TOPIC_DELETE_NOTE_LOCAL_CACHE, noteId);
|
||||
log.info("====> MQ:删除笔记,删除本地缓存发送成功...");
|
||||
|
||||
return Response.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response<?> visibleOnlyMe(UpdateNoteVisibleOnlyMeReqVO updateNoteVisibleOnlyMeReqVO) {
|
||||
// 笔记 ID
|
||||
Long noteId = updateNoteVisibleOnlyMeReqVO.getId();
|
||||
|
||||
NoteDO selectNoteDO = this.getById(noteId);
|
||||
|
||||
// 判断笔记是否存在
|
||||
if (Objects.isNull(selectNoteDO)) {
|
||||
throw new ApiException(ResponseCodeEnum.NOTE_NOT_FOUND);
|
||||
}
|
||||
|
||||
// 判断权限:非笔记发布者不允许修改笔记权限
|
||||
Long currUserId = LoginUserContextHolder.getUserId();
|
||||
if (!Objects.equals(currUserId, selectNoteDO.getCreatorId())) {
|
||||
throw new ApiException(ResponseCodeEnum.NOTE_CANT_OPERATE);
|
||||
}
|
||||
|
||||
// 构建更新的实体类
|
||||
NoteDO noteDO = NoteDO.builder()
|
||||
.id(noteId)
|
||||
|
||||
@@ -102,6 +102,12 @@
|
||||
<artifactId>rocketmq-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- NaCos 配置中心 -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
<build>
|
||||
<plugins>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.hanserwei.hannote.user.relation.biz.config;
|
||||
|
||||
import com.google.common.util.concurrent.RateLimiter;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.cloud.context.config.annotation.RefreshScope;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@RefreshScope
|
||||
public class FollowUnfollowMqConsumerRateLimitConfig {
|
||||
|
||||
@Value("${mq-consumer.follow-unfollow.rate-limit}")
|
||||
private double rateLimit;
|
||||
|
||||
@Bean
|
||||
@RefreshScope
|
||||
public RateLimiter rateLimiter() {
|
||||
return RateLimiter.create(rateLimit);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,11 @@ public class RedisKeyConstants {
|
||||
*/
|
||||
private static final String USER_FOLLOWING_KEY_PREFIX = "following:";
|
||||
|
||||
/**
|
||||
* 粉丝列表 KEY 前缀
|
||||
*/
|
||||
private static final String USER_FANS_KEY_PREFIX = "fans:";
|
||||
|
||||
/**
|
||||
* 构建关注列表完整的 KEY
|
||||
* @param userId 用户 ID
|
||||
@@ -16,4 +21,12 @@ public class RedisKeyConstants {
|
||||
return USER_FOLLOWING_KEY_PREFIX + userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建粉丝列表完整的 KEY
|
||||
* @param userId 用户 ID
|
||||
* @return 粉丝列表 KEY
|
||||
*/
|
||||
public static String buildUserFansKey(Long userId) {
|
||||
return USER_FANS_KEY_PREFIX + userId;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,32 @@
|
||||
package com.hanserwei.hannote.user.relation.biz.consumer;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.google.common.util.concurrent.RateLimiter;
|
||||
import com.hanserwei.framework.common.utils.JsonUtils;
|
||||
import com.hanserwei.hannote.user.relation.biz.constant.MQConstants;
|
||||
import com.hanserwei.hannote.user.relation.biz.constant.RedisKeyConstants;
|
||||
import com.hanserwei.hannote.user.relation.biz.domain.dataobject.FansDO;
|
||||
import com.hanserwei.hannote.user.relation.biz.domain.dataobject.FollowingDO;
|
||||
import com.hanserwei.hannote.user.relation.biz.model.dto.FollowUserMqDTO;
|
||||
import com.hanserwei.hannote.user.relation.biz.model.dto.UnfollowUserMqDTO;
|
||||
import com.hanserwei.hannote.user.relation.biz.service.FansDOService;
|
||||
import com.hanserwei.hannote.user.relation.biz.service.FollowingDOService;
|
||||
import com.hanserwei.hannote.user.relation.biz.util.DateUtils;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.rocketmq.common.message.Message;
|
||||
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
|
||||
import org.apache.rocketmq.spring.core.RocketMQListener;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.script.DefaultRedisScript;
|
||||
import org.springframework.scripting.support.ResourceScriptSource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.Objects;
|
||||
|
||||
@Component
|
||||
@@ -23,19 +35,21 @@ import java.util.Objects;
|
||||
topic = MQConstants.TOPIC_FOLLOW_OR_UNFOLLOW
|
||||
)
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class FollowUnfollowConsumer implements RocketMQListener<Message> {
|
||||
private final TransactionTemplate transactionTemplate;
|
||||
private final FollowingDOService followingDOService;
|
||||
private final FansDOService fansDOService;
|
||||
|
||||
public FollowUnfollowConsumer(TransactionTemplate transactionTemplate, FollowingDOService followingDOService, FansDOService fansDOService) {
|
||||
this.transactionTemplate = transactionTemplate;
|
||||
this.followingDOService = followingDOService;
|
||||
this.fansDOService = fansDOService;
|
||||
}
|
||||
@Resource
|
||||
private RateLimiter rateLimiter;
|
||||
@Resource
|
||||
private RedisTemplate<Object, Object> redisTemplate;
|
||||
|
||||
@Override
|
||||
public void onMessage(Message message) {
|
||||
// 流量削峰:通过获取令牌,如果没有令牌可用,将阻塞,直到获得
|
||||
rateLimiter.acquire();
|
||||
// 消息体
|
||||
String bodyJsonStr = new String(message.getBody());
|
||||
// 标签
|
||||
@@ -43,17 +57,67 @@ public class FollowUnfollowConsumer implements RocketMQListener<Message> {
|
||||
|
||||
log.info("==> FollowUnfollowConsumer 消费了消息 {}, tags: {}", bodyJsonStr, tags);
|
||||
// 根据MQ标签判断操作类型
|
||||
if (Objects.equals(tags, MQConstants.TAG_FOLLOW)){
|
||||
if (Objects.equals(tags, MQConstants.TAG_FOLLOW)) {
|
||||
// 关注
|
||||
handleFollowTagMessage(bodyJsonStr);
|
||||
} else if (Objects.equals(tags, MQConstants.TAG_UNFOLLOW)) {
|
||||
// 取关
|
||||
// TODO: 待实现
|
||||
handleUnfollowTagMessage(bodyJsonStr);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取关
|
||||
*
|
||||
* @param bodyJsonStr 消息体
|
||||
*/
|
||||
private void handleUnfollowTagMessage(String bodyJsonStr) {
|
||||
// 消息体json串转换为DTO对象
|
||||
UnfollowUserMqDTO unfollowUserMqDTO = JsonUtils.parseObject(bodyJsonStr, UnfollowUserMqDTO.class);
|
||||
|
||||
// 判空
|
||||
if (Objects.isNull(unfollowUserMqDTO)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Long userId = unfollowUserMqDTO.getUserId();
|
||||
Long unfollowUserId = unfollowUserMqDTO.getUnfollowUserId();
|
||||
LocalDateTime createTime = unfollowUserMqDTO.getCreateTime();
|
||||
|
||||
// 编程式事务提交
|
||||
boolean isSuccess = Boolean.TRUE.equals(transactionTemplate.execute(status -> {
|
||||
try {
|
||||
// 数据库操作,两个数据库操作
|
||||
// 关注表:一条记录
|
||||
boolean isRemoved = followingDOService.remove(new LambdaQueryWrapper<>(FollowingDO.class)
|
||||
.eq(FollowingDO::getUserId, userId)
|
||||
.eq(FollowingDO::getFollowingUserId, unfollowUserId));
|
||||
if (isRemoved) {
|
||||
// 粉丝表:一条记录
|
||||
return fansDOService.remove(new LambdaQueryWrapper<>(FansDO.class)
|
||||
.eq(FansDO::getUserId, unfollowUserId)
|
||||
.eq(FansDO::getFansUserId, userId));
|
||||
}
|
||||
return true;
|
||||
}catch (Exception e){
|
||||
status.setRollbackOnly();
|
||||
log.error("## 取关失败, userId: {}, unfollowUserId: {}, createTime: {}", userId, unfollowUserId, createTime);
|
||||
}
|
||||
return false;
|
||||
}));
|
||||
|
||||
// 若数据库删除成功,更新 Redis,将自己从被取关用户的 ZSet 粉丝列表删除
|
||||
if (isSuccess) {
|
||||
// 被取关用户的粉丝列表 Redis Key
|
||||
String fansRedisKey = RedisKeyConstants.buildUserFansKey(unfollowUserId);
|
||||
// 删除指定粉丝
|
||||
redisTemplate.opsForZSet().remove(fansRedisKey, userId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关注
|
||||
*
|
||||
* @param bodyJsonStr 消息体
|
||||
*/
|
||||
private void handleFollowTagMessage(String bodyJsonStr) {
|
||||
@@ -82,14 +146,14 @@ public class FollowUnfollowConsumer implements RocketMQListener<Message> {
|
||||
.createTime(createTime)
|
||||
.build());
|
||||
// 粉丝表:一条记录
|
||||
if (followRecordSaved){
|
||||
if (followRecordSaved) {
|
||||
return fansDOService.save(FansDO.builder()
|
||||
.userId(followUserId)
|
||||
.fansUserId(userId)
|
||||
.createTime(createTime)
|
||||
.build());
|
||||
}
|
||||
}catch (Exception e){
|
||||
} catch (Exception e) {
|
||||
status.setRollbackOnly();
|
||||
log.error("## 添加关注关系失败, userId: {}, followUserId: {}, createTime: {}", userId, followUserId, createTime);
|
||||
}
|
||||
@@ -97,6 +161,19 @@ public class FollowUnfollowConsumer implements RocketMQListener<Message> {
|
||||
}));
|
||||
|
||||
log.info("## 数据库添加记录结果: {}", isSuccess);
|
||||
// TODO: 更新 Redis 中被关注用户的 ZSet 粉丝列表
|
||||
if (isSuccess) {
|
||||
// Lua脚本
|
||||
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
|
||||
script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/follow_check_and_update_fans_zset.lua")));
|
||||
|
||||
// 时间戳
|
||||
long timestamp = DateUtils.localDateTime2Timestamp(createTime);
|
||||
|
||||
// 构建关注有户的粉丝列表的KEY
|
||||
String fansZSetKey = RedisKeyConstants.buildUserFansKey(followUserId);
|
||||
|
||||
// 执行Lua脚本
|
||||
redisTemplate.execute(script, Collections.singletonList(fansZSetKey), userId, timestamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.hanserwei.hannote.user.relation.biz.controller;
|
||||
import com.hanserwei.framework.biz.operationlog.aspect.ApiOperationLog;
|
||||
import com.hanserwei.framework.common.response.Response;
|
||||
import com.hanserwei.hannote.user.relation.biz.model.vo.FollowUserReqVO;
|
||||
import com.hanserwei.hannote.user.relation.biz.model.vo.UnfollowUserReqVO;
|
||||
import com.hanserwei.hannote.user.relation.biz.service.RelationService;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -26,4 +27,9 @@ public class RelationController {
|
||||
return relationService.follow(followUserReqVO);
|
||||
}
|
||||
|
||||
@PostMapping("/unfollow")
|
||||
@ApiOperationLog(description = "取关用户")
|
||||
public Response<?> unfollow(@Validated @RequestBody UnfollowUserReqVO unfollowUserReqVO) {
|
||||
return relationService.unfollow(unfollowUserReqVO);
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,8 @@ public enum LuaResultEnum {
|
||||
ALREADY_FOLLOWED(-3L),
|
||||
// 关注成功
|
||||
FOLLOW_SUCCESS(0L),
|
||||
// 未关注该用户
|
||||
NOT_FOLLOWED(-4L),
|
||||
;
|
||||
|
||||
private final Long code;
|
||||
|
||||
@@ -17,6 +17,8 @@ public enum ResponseCodeEnum implements BaseExceptionInterface {
|
||||
FOLLOW_USER_NOT_EXISTED("RELATION-20002", "关注的用户不存在"),
|
||||
FOLLOWING_COUNT_LIMIT("RELATION-20003", "您关注的用户已达上限,请先取关部分用户"),
|
||||
ALREADY_FOLLOWED("RELATION-20004", "您已经关注了该用户"),
|
||||
CANT_UNFOLLOW_YOUR_SELF("RELATION-20005", "无法取关自己"),
|
||||
NOT_FOLLOWED("RELATION-20006", "你未关注对方,无法取关"),
|
||||
;
|
||||
|
||||
// 异常码
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.hanserwei.hannote.user.relation.biz.model.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Builder
|
||||
public class UnfollowUserMqDTO {
|
||||
|
||||
private Long userId;
|
||||
|
||||
private Long unfollowUserId;
|
||||
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.hanserwei.hannote.user.relation.biz.model.vo;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Builder
|
||||
public class UnfollowUserReqVO {
|
||||
|
||||
@NotNull(message = "被取关用户 ID 不能为空")
|
||||
private Long unfollowUserId;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.hanserwei.hannote.user.relation.biz.service;
|
||||
|
||||
import com.hanserwei.framework.common.response.Response;
|
||||
import com.hanserwei.hannote.user.relation.biz.model.vo.FollowUserReqVO;
|
||||
import com.hanserwei.hannote.user.relation.biz.model.vo.UnfollowUserReqVO;
|
||||
|
||||
public interface RelationService {
|
||||
|
||||
@@ -12,4 +13,11 @@ public interface RelationService {
|
||||
* @return 响应
|
||||
*/
|
||||
Response<?> follow(FollowUserReqVO followUserReqVO);
|
||||
|
||||
/**
|
||||
* 取关用户
|
||||
* @param unfollowUserReqVO 取关用户请求
|
||||
* @return 响应
|
||||
*/
|
||||
Response<?> unfollow(UnfollowUserReqVO unfollowUserReqVO);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,9 @@ import com.hanserwei.hannote.user.relation.biz.domain.dataobject.FollowingDO;
|
||||
import com.hanserwei.hannote.user.relation.biz.enums.LuaResultEnum;
|
||||
import com.hanserwei.hannote.user.relation.biz.enums.ResponseCodeEnum;
|
||||
import com.hanserwei.hannote.user.relation.biz.model.dto.FollowUserMqDTO;
|
||||
import com.hanserwei.hannote.user.relation.biz.model.dto.UnfollowUserMqDTO;
|
||||
import com.hanserwei.hannote.user.relation.biz.model.vo.FollowUserReqVO;
|
||||
import com.hanserwei.hannote.user.relation.biz.model.vo.UnfollowUserReqVO;
|
||||
import com.hanserwei.hannote.user.relation.biz.rpc.UserRpcService;
|
||||
import com.hanserwei.hannote.user.relation.biz.service.FollowingDOService;
|
||||
import com.hanserwei.hannote.user.relation.biz.service.RelationService;
|
||||
@@ -162,6 +164,106 @@ public class RelationServiceImpl implements RelationService {
|
||||
return Response.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response<?> unfollow(UnfollowUserReqVO unfollowUserReqVO) {
|
||||
// 被取关用户 ID
|
||||
Long unfollowUserId = unfollowUserReqVO.getUnfollowUserId();
|
||||
|
||||
// 当前登录用户id
|
||||
Long userId = LoginUserContextHolder.getUserId();
|
||||
|
||||
// 无法取关自己
|
||||
if (Objects.equals(userId, unfollowUserId)){
|
||||
throw new ApiException(ResponseCodeEnum.CANT_UNFOLLOW_YOUR_SELF);
|
||||
}
|
||||
|
||||
// 校验被取关用户是否存在
|
||||
FindUserByIdRspDTO findUserByIdRspDTO = userRpcService.findById(unfollowUserId);
|
||||
if (Objects.isNull(findUserByIdRspDTO)){
|
||||
throw new ApiException(ResponseCodeEnum.FOLLOW_USER_NOT_EXISTED);
|
||||
}
|
||||
|
||||
// 当前用户的关注列表 Redis Key
|
||||
String followingRedisKey = RedisKeyConstants.buildUserFollowingKey(userId);
|
||||
|
||||
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
|
||||
// Lua 脚本路径
|
||||
script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/unfollow_check_and_delete.lua")));
|
||||
// 返回值类型
|
||||
script.setResultType(Long.class);
|
||||
|
||||
// 执行 Lua 脚本,拿到返回结果
|
||||
Long result = redisTemplate.execute(script, Collections.singletonList(followingRedisKey), unfollowUserId);
|
||||
|
||||
// 校验 Lua 脚本执行结果
|
||||
// 取关的用户不在关注列表中
|
||||
if (Objects.equals(result, LuaResultEnum.NOT_FOLLOWED.getCode())) {
|
||||
throw new ApiException(ResponseCodeEnum.NOT_FOLLOWED);
|
||||
}
|
||||
|
||||
if (Objects.equals(result, LuaResultEnum.ZSET_NOT_EXIST.getCode())) { // ZSET 关注列表不存在
|
||||
// 从数据库查询当前用户的关注关系记录
|
||||
List<FollowingDO> followingDOS = followingDOService.list(new LambdaQueryWrapper<>(FollowingDO.class).eq(FollowingDO::getUserId, userId));
|
||||
|
||||
// 随机过期时间
|
||||
// 保底1天+随机秒数
|
||||
long expireSeconds = 60*60*24 + RandomUtil.randomInt(60*60*24);
|
||||
|
||||
// 若记录为空,则表示还未关注任何人,提示还未关注对方
|
||||
if (CollUtil.isEmpty(followingDOS)) {
|
||||
throw new ApiException(ResponseCodeEnum.NOT_FOLLOWED);
|
||||
} else { // 若记录不为空,则将关注关系数据全量同步到 Redis 中,并设置过期时间;
|
||||
// 构建 Lua 参数
|
||||
Object[] luaArgs = buildLuaArgs(followingDOS, expireSeconds);
|
||||
|
||||
// 执行 Lua 脚本,批量同步关注关系数据到 Redis 中
|
||||
DefaultRedisScript<Long> script3 = new DefaultRedisScript<>();
|
||||
script3.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/follow_batch_add_and_expire.lua")));
|
||||
script3.setResultType(Long.class);
|
||||
redisTemplate.execute(script3, Collections.singletonList(followingRedisKey), luaArgs);
|
||||
|
||||
// 再次调用上面的 Lua 脚本:unfollow_check_and_delete.lua , 将取关的用户删除
|
||||
result = redisTemplate.execute(script, Collections.singletonList(followingRedisKey), unfollowUserId);
|
||||
// 再次校验结果
|
||||
if (Objects.equals(result, LuaResultEnum.NOT_FOLLOWED.getCode())) {
|
||||
throw new ApiException(ResponseCodeEnum.NOT_FOLLOWED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 发送MQ
|
||||
// 构建消息体DTO
|
||||
UnfollowUserMqDTO unfollowUserMqDTO = UnfollowUserMqDTO.builder()
|
||||
.userId(userId)
|
||||
.unfollowUserId(unfollowUserId)
|
||||
.createTime(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
// 构造消息对象,并把DTO转换为JSON字符串设置到消息体中
|
||||
Message<String> message = MessageBuilder
|
||||
.withPayload(JsonUtils.toJsonString(unfollowUserMqDTO))
|
||||
.build();
|
||||
|
||||
// 通过冒号连接, 可让 MQ 发送给主题 Topic 时,携带上标签 Tag
|
||||
String destination = MQConstants.TOPIC_FOLLOW_OR_UNFOLLOW + ":" + MQConstants.TAG_UNFOLLOW;
|
||||
log.info("==> 开始发送取关操作 MQ, 消息体: {}", unfollowUserMqDTO);
|
||||
// 异步发送MQ消息,提升接口响应速度
|
||||
rocketMQTemplate.asyncSend(destination, message, new SendCallback() {
|
||||
|
||||
@Override
|
||||
public void onSuccess(SendResult sendResult) {
|
||||
log.info("==> 取关操作 MQ 发送成功,SendResult: {}", sendResult);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onException(Throwable throwable) {
|
||||
log.error("==> 取关操作 MQ 发送异常: ", throwable);
|
||||
}
|
||||
});
|
||||
return Response.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 Lua 脚本结果,根据状态码抛出对应的业务异常
|
||||
* @param result Lua 脚本返回结果
|
||||
|
||||
@@ -29,3 +29,6 @@ mybatis-plus:
|
||||
global-config:
|
||||
banner: false
|
||||
mapper-locations: classpath*:/mapperxml/*.xml
|
||||
mq-consumer: # MQ 消费者
|
||||
follow-unfollow: # 关注、取关
|
||||
rate-limit: 5000 # 每秒限流阈值
|
||||
@@ -10,3 +10,10 @@ spring:
|
||||
group: DEFAULT_GROUP # 所属组
|
||||
namespace: han-note # 命名空间
|
||||
server-addr: 127.0.0.1:8848 # 指定 Nacos 配置中心的服务器地址
|
||||
config:
|
||||
server-addr: http://127.0.0.1:8848 # 指定 Nacos 配置中心的服务器地址
|
||||
prefix: ${spring.application.name} # 配置 Data Id 前缀,这里使用应用名称作为前缀
|
||||
group: DEFAULT_GROUP # 所属组
|
||||
namespace: han-note # 命名空间
|
||||
file-extension: yaml # 配置文件格式
|
||||
refresh-enabled: true # 是否开启动态刷新
|
||||
@@ -0,0 +1,21 @@
|
||||
local key = KEYS[1] -- 操作的 Redis Key
|
||||
local fansUserId = ARGV[1] -- 粉丝ID
|
||||
local timestamp = ARGV[2] -- 时间戳
|
||||
|
||||
-- 使用 EXISTS 命令检查 ZSET 粉丝列表是否存在
|
||||
local exists = redis.call('EXISTS', key)
|
||||
if exists == 0 then
|
||||
return -1
|
||||
end
|
||||
|
||||
-- 获取粉丝列表大小
|
||||
local size = redis.call('ZCARD', key)
|
||||
|
||||
-- 若超过 5000 个粉丝,则移除最早关注的粉丝
|
||||
if size >= 5000 then
|
||||
redis.call('ZPOPMIN', key)
|
||||
end
|
||||
|
||||
-- 添加新的粉丝关系
|
||||
redis.call('ZADD', key, timestamp, fansUserId)
|
||||
return 0
|
||||
@@ -0,0 +1,20 @@
|
||||
-- LUA 脚本:校验并移除关注关系
|
||||
|
||||
local key = KEYS[1] -- 操作的 Redis Key
|
||||
local unfollowUserId = ARGV[1] -- 关注的用户ID
|
||||
|
||||
-- 使用 EXISTS 命令检查 ZSET 是否存在
|
||||
local exists = redis.call('EXISTS', key)
|
||||
if exists == 0 then
|
||||
return -1
|
||||
end
|
||||
|
||||
-- 校验目标用户是否被关注
|
||||
local score = redis.call('ZSCORE', key, unfollowUserId)
|
||||
if score == false or score == nil then
|
||||
return -4
|
||||
end
|
||||
|
||||
-- ZREM 删除关注关系
|
||||
redis.call('ZREM', key, unfollowUserId)
|
||||
return 0
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.hanserwei.hannote.user.relation.biz;
|
||||
|
||||
import com.hanserwei.framework.common.utils.JsonUtils;
|
||||
import com.hanserwei.hannote.user.relation.biz.constant.MQConstants;
|
||||
import com.hanserwei.hannote.user.relation.biz.model.dto.FollowUserMqDTO;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.rocketmq.client.producer.SendCallback;
|
||||
import org.apache.rocketmq.client.producer.SendResult;
|
||||
import org.apache.rocketmq.spring.core.RocketMQTemplate;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@SpringBootTest
|
||||
@Slf4j
|
||||
class MQTests {
|
||||
|
||||
@Resource
|
||||
private RocketMQTemplate rocketMQTemplate;
|
||||
|
||||
/**
|
||||
* 测试:发送一万条 MQ
|
||||
*/
|
||||
@Test
|
||||
void testBatchSendMQ() {
|
||||
for (long i = 0; i < 10000; i++) {
|
||||
// 构建消息体 DTO
|
||||
FollowUserMqDTO followUserMqDTO = FollowUserMqDTO.builder()
|
||||
.userId(i)
|
||||
.followUserId(i)
|
||||
.createTime(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
// 构建消息对象,并将 DTO 转成 Json 字符串设置到消息体中
|
||||
Message<String> message = MessageBuilder.withPayload(JsonUtils.toJsonString(followUserMqDTO))
|
||||
.build();
|
||||
|
||||
// 通过冒号连接, 可让 MQ 发送给主题 Topic 时,携带上标签 Tag
|
||||
String destination = MQConstants.TOPIC_FOLLOW_OR_UNFOLLOW + ":" + MQConstants.TAG_FOLLOW;
|
||||
|
||||
log.info("==> 开始发送关注操作 MQ, 消息体: {}", followUserMqDTO);
|
||||
|
||||
// 异步发送 MQ 消息,提升接口响应速度
|
||||
rocketMQTemplate.asyncSend(destination, message, new SendCallback() {
|
||||
@Override
|
||||
public void onSuccess(SendResult sendResult) {
|
||||
log.info("==> MQ 发送成功,SendResult: {}", sendResult);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onException(Throwable throwable) {
|
||||
log.error("==> MQ 发送异常: ", throwable);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -146,3 +146,12 @@ Authorization: Bearer {{token}}
|
||||
{
|
||||
"followUserId": {{otherUserId}}
|
||||
}
|
||||
|
||||
### 取消关注
|
||||
POST http://localhost:8000/relation/relation/unfollow
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
{
|
||||
"unfollowUserId": 2100
|
||||
}
|
||||
Reference in New Issue
Block a user