Compare commits
5 Commits
04b891e73d
...
362c32cbd6
| Author | SHA1 | Date | |
|---|---|---|---|
| 362c32cbd6 | |||
| 5a7564d504 | |||
| 3c8dc9e4af | |||
| 7942a46592 | |||
| 16ab8a13d2 |
@@ -22,6 +22,12 @@ spring:
|
||||
- Path=/note/**
|
||||
filters:
|
||||
- StripPrefix=1
|
||||
- id: user-relation
|
||||
uri: lb://han-note-user-relation
|
||||
predicates:
|
||||
- Path=/relation/**
|
||||
filters:
|
||||
- StripPrefix=1
|
||||
data:
|
||||
redis:
|
||||
database: 5 # Redis 数据库索引(默认为 0)
|
||||
|
||||
@@ -10,7 +10,7 @@ import org.springframework.stereotype.Component;
|
||||
@Component
|
||||
@Slf4j
|
||||
@RocketMQMessageListener(
|
||||
consumerGroup = "han_note_group",
|
||||
consumerGroup = "han_note_group_" + MQConstants.TOPIC_DELETE_NOTE_LOCAL_CACHE,
|
||||
topic = MQConstants.TOPIC_DELETE_NOTE_LOCAL_CACHE,
|
||||
messageModel = MessageModel.BROADCASTING
|
||||
)
|
||||
|
||||
@@ -91,6 +91,17 @@
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.hanserwei</groupId>
|
||||
<artifactId>han-note-user-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Rocket MQ -->
|
||||
<dependency>
|
||||
<groupId>org.apache.rocketmq</groupId>
|
||||
<artifactId>rocketmq-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
<build>
|
||||
<plugins>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package com.hanserwei.hannote.user.relation.biz.domain;
|
||||
package com.hanserwei.hannote.user.relation.biz;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
|
||||
@SpringBootApplication
|
||||
@MapperScan("com.hanserwei.hannote.user.relation.biz.domain.mapper")
|
||||
@EnableFeignClients(basePackages = "com.hanserwei.hannote")
|
||||
public class HannoteUserRelationBizApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(HannoteUserRelationBizApplication.class, args);
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.hanserwei.hannote.user.relation.biz.config;
|
||||
|
||||
import org.apache.rocketmq.spring.autoconfigure.RocketMQAutoConfiguration;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
@Configuration
|
||||
@Import(RocketMQAutoConfiguration.class)
|
||||
public class RocketMQConfig {
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.hanserwei.hannote.user.relation.biz.constant;
|
||||
|
||||
public interface MQConstants {
|
||||
|
||||
/**
|
||||
* Topic: 关注、取关共用一个
|
||||
*/
|
||||
String TOPIC_FOLLOW_OR_UNFOLLOW = "FollowUnfollowTopic";
|
||||
|
||||
/**
|
||||
* 关注标签
|
||||
*/
|
||||
String TAG_FOLLOW = "Follow";
|
||||
|
||||
/**
|
||||
* 取关标签
|
||||
*/
|
||||
String TAG_UNFOLLOW = "Unfollow";
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.hanserwei.hannote.user.relation.biz.constant;
|
||||
|
||||
public class RedisKeyConstants {
|
||||
|
||||
/**
|
||||
* 关注列表 KEY 前缀
|
||||
*/
|
||||
private static final String USER_FOLLOWING_KEY_PREFIX = "following:";
|
||||
|
||||
/**
|
||||
* 构建关注列表完整的 KEY
|
||||
* @param userId 用户 ID
|
||||
* @return 关注列表 KEY
|
||||
*/
|
||||
public static String buildUserFollowingKey(Long userId) {
|
||||
return USER_FOLLOWING_KEY_PREFIX + userId;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.hanserwei.hannote.user.relation.biz.consumer;
|
||||
|
||||
import com.hanserwei.framework.common.utils.JsonUtils;
|
||||
import com.hanserwei.hannote.user.relation.biz.constant.MQConstants;
|
||||
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.service.FansDOService;
|
||||
import com.hanserwei.hannote.user.relation.biz.service.FollowingDOService;
|
||||
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.stereotype.Component;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Objects;
|
||||
|
||||
@Component
|
||||
@RocketMQMessageListener(
|
||||
consumerGroup = "han_note_group_" + MQConstants.TOPIC_FOLLOW_OR_UNFOLLOW,
|
||||
topic = MQConstants.TOPIC_FOLLOW_OR_UNFOLLOW
|
||||
)
|
||||
@Slf4j
|
||||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(Message message) {
|
||||
// 消息体
|
||||
String bodyJsonStr = new String(message.getBody());
|
||||
// 标签
|
||||
String tags = message.getTags();
|
||||
|
||||
log.info("==> FollowUnfollowConsumer 消费了消息 {}, tags: {}", bodyJsonStr, tags);
|
||||
// 根据MQ标签判断操作类型
|
||||
if (Objects.equals(tags, MQConstants.TAG_FOLLOW)){
|
||||
// 关注
|
||||
handleFollowTagMessage(bodyJsonStr);
|
||||
} else if (Objects.equals(tags, MQConstants.TAG_UNFOLLOW)) {
|
||||
// 取关
|
||||
// TODO: 待实现
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关注
|
||||
* @param bodyJsonStr 消息体
|
||||
*/
|
||||
private void handleFollowTagMessage(String bodyJsonStr) {
|
||||
// 解析消息体转换为DTO对象
|
||||
FollowUserMqDTO followUserMqDTO = JsonUtils.parseObject(bodyJsonStr, FollowUserMqDTO.class);
|
||||
|
||||
// 判空
|
||||
if (Objects.isNull(followUserMqDTO)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 幂等性:通过联合唯一索引保证
|
||||
|
||||
Long userId = followUserMqDTO.getUserId();
|
||||
Long followUserId = followUserMqDTO.getFollowUserId();
|
||||
LocalDateTime createTime = followUserMqDTO.getCreateTime();
|
||||
|
||||
// 编程式事物
|
||||
boolean isSuccess = Boolean.TRUE.equals(transactionTemplate.execute(status -> {
|
||||
try {
|
||||
// 关注成功需往数据库添加两条记录
|
||||
// 关注表:一条记录
|
||||
boolean followRecordSaved = followingDOService.save(FollowingDO.builder()
|
||||
.userId(userId)
|
||||
.followingUserId(followUserId)
|
||||
.createTime(createTime)
|
||||
.build());
|
||||
// 粉丝表:一条记录
|
||||
if (followRecordSaved){
|
||||
return fansDOService.save(FansDO.builder()
|
||||
.userId(followUserId)
|
||||
.fansUserId(userId)
|
||||
.createTime(createTime)
|
||||
.build());
|
||||
}
|
||||
}catch (Exception e){
|
||||
status.setRollbackOnly();
|
||||
log.error("## 添加关注关系失败, userId: {}, followUserId: {}, createTime: {}", userId, followUserId, createTime);
|
||||
}
|
||||
return false;
|
||||
}));
|
||||
|
||||
log.info("## 数据库添加记录结果: {}", isSuccess);
|
||||
// TODO: 更新 Redis 中被关注用户的 ZSet 粉丝列表
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
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.service.RelationService;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/relation")
|
||||
@Slf4j
|
||||
public class RelationController {
|
||||
|
||||
@Resource
|
||||
private RelationService relationService;
|
||||
|
||||
@PostMapping("/follow")
|
||||
@ApiOperationLog(description = "关注用户")
|
||||
public Response<?> follow(@Validated @RequestBody FollowUserReqVO followUserReqVO) {
|
||||
return relationService.follow(followUserReqVO);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.hanserwei.hannote.user.relation.biz.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum LuaResultEnum {
|
||||
// ZSET 不存在
|
||||
ZSET_NOT_EXIST(-1L),
|
||||
// 关注已达到上限
|
||||
FOLLOW_LIMIT(-2L),
|
||||
// 已经关注了该用户
|
||||
ALREADY_FOLLOWED(-3L),
|
||||
// 关注成功
|
||||
FOLLOW_SUCCESS(0L),
|
||||
;
|
||||
|
||||
private final Long code;
|
||||
|
||||
/**
|
||||
* 根据类型 code 获取对应的枚举
|
||||
*
|
||||
* @param code 类型 code
|
||||
* @return 枚举
|
||||
*/
|
||||
public static LuaResultEnum valueOf(Long code) {
|
||||
for (LuaResultEnum luaResultEnum : LuaResultEnum.values()) {
|
||||
if (Objects.equals(code, luaResultEnum.getCode())) {
|
||||
return luaResultEnum;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,10 @@ public enum ResponseCodeEnum implements BaseExceptionInterface {
|
||||
PARAM_NOT_VALID("RELATION-10001", "参数错误"),
|
||||
|
||||
// ----------- 业务异常状态码 -----------
|
||||
CANT_FOLLOW_YOUR_SELF("RELATION-20001", "无法关注自己"),
|
||||
FOLLOW_USER_NOT_EXISTED("RELATION-20002", "关注的用户不存在"),
|
||||
FOLLOWING_COUNT_LIMIT("RELATION-20003", "您关注的用户已达上限,请先取关部分用户"),
|
||||
ALREADY_FOLLOWED("RELATION-20004", "您已经关注了该用户"),
|
||||
;
|
||||
|
||||
// 异常码
|
||||
|
||||
@@ -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 FollowUserMqDTO {
|
||||
|
||||
private Long userId;
|
||||
|
||||
private Long followUserId;
|
||||
|
||||
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 FollowUserReqVO {
|
||||
|
||||
@NotNull(message = "被关注用户 ID 不能为空")
|
||||
private Long followUserId;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.hanserwei.hannote.user.relation.biz.rpc;
|
||||
|
||||
import com.hanserwei.framework.common.response.Response;
|
||||
import com.hanserwei.hannote.user.api.UserFeignApi;
|
||||
import com.hanserwei.hannote.user.dto.req.FindUserByIdReqDTO;
|
||||
import com.hanserwei.hannote.user.dto.resp.FindUserByIdRspDTO;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@Component
|
||||
public class UserRpcService {
|
||||
|
||||
@Resource
|
||||
private UserFeignApi userFeignApi;
|
||||
|
||||
/**
|
||||
* 根据用户 ID 查询
|
||||
*
|
||||
* @param userId 用户 ID
|
||||
* @return 用户信息
|
||||
*/
|
||||
public FindUserByIdRspDTO findById(Long userId) {
|
||||
FindUserByIdReqDTO findUserByIdReqDTO = new FindUserByIdReqDTO();
|
||||
findUserByIdReqDTO.setId(userId);
|
||||
|
||||
Response<FindUserByIdRspDTO> response = userFeignApi.findById(findUserByIdReqDTO);
|
||||
|
||||
if (!response.isSuccess() || Objects.isNull(response.getData())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return response.getData();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
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;
|
||||
|
||||
public interface RelationService {
|
||||
|
||||
/**
|
||||
* 关注用户
|
||||
*
|
||||
* @param followUserReqVO 关注用户请求
|
||||
* @return 响应
|
||||
*/
|
||||
Response<?> follow(FollowUserReqVO followUserReqVO);
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
package com.hanserwei.hannote.user.relation.biz.service.impl;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.hanserwei.framework.biz.context.holder.LoginUserContextHolder;
|
||||
import com.hanserwei.framework.common.exception.ApiException;
|
||||
import com.hanserwei.framework.common.response.Response;
|
||||
import com.hanserwei.framework.common.utils.JsonUtils;
|
||||
import com.hanserwei.hannote.user.dto.resp.FindUserByIdRspDTO;
|
||||
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.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.vo.FollowUserReqVO;
|
||||
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;
|
||||
import com.hanserwei.hannote.user.relation.biz.util.DateUtils;
|
||||
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.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.script.DefaultRedisScript;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.scripting.support.ResourceScriptSource;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class RelationServiceImpl implements RelationService {
|
||||
|
||||
@Resource
|
||||
private UserRpcService userRpcService;
|
||||
@Resource
|
||||
private RedisTemplate<Object, Object> redisTemplate;
|
||||
@Resource
|
||||
private FollowingDOService followingDOService;
|
||||
@Resource
|
||||
private RocketMQTemplate rocketMQTemplate;
|
||||
|
||||
@Override
|
||||
public Response<?> follow(FollowUserReqVO followUserReqVO) {
|
||||
// 获取被关注用户 ID
|
||||
Long followUserId = followUserReqVO.getFollowUserId();
|
||||
|
||||
// 获取当前登录用户 ID
|
||||
Long userId = LoginUserContextHolder.getUserId();
|
||||
if (Objects.equals(userId, followUserId)) {
|
||||
throw new ApiException(ResponseCodeEnum.CANT_FOLLOW_YOUR_SELF);
|
||||
}
|
||||
// 校验关注的用户是否存在
|
||||
FindUserByIdRspDTO findUserByIdRspDTO = userRpcService.findById(followUserId);
|
||||
if (Objects.isNull(findUserByIdRspDTO)){
|
||||
throw new ApiException(ResponseCodeEnum.FOLLOW_USER_NOT_EXISTED);
|
||||
}
|
||||
|
||||
// 校验当前用户的Zset关注列表是否已经存在
|
||||
String followingRedisKey = RedisKeyConstants.buildUserFollowingKey(userId);
|
||||
|
||||
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
|
||||
|
||||
// Lua脚本路径
|
||||
script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/follow_check_and_add.lua")));
|
||||
// 返回值类型
|
||||
script.setResultType(Long.class);
|
||||
|
||||
// 当前时间
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
// 转为时间戳
|
||||
long timestamp = DateUtils.localDateTime2Timestamp(now);
|
||||
|
||||
// 执行Lua脚本拿到结果
|
||||
Long result = redisTemplate.execute(script, Collections.singletonList(followingRedisKey), followUserId, timestamp);
|
||||
|
||||
// 校验 Lua 脚本执行结果
|
||||
checkLuaScriptResult(result);
|
||||
|
||||
// ZSET不存在
|
||||
if (Objects.equals(result, LuaResultEnum.ZSET_NOT_EXIST.getCode())){
|
||||
// 从数据库查询当前用户的关注关系记录
|
||||
List<FollowingDO> followingDOS = followingDOService.list(new LambdaQueryWrapper<>(FollowingDO.class)
|
||||
.select(FollowingDO::getUserId)
|
||||
.select(FollowingDO::getFollowingUserId)
|
||||
.select(FollowingDO::getCreateTime)
|
||||
.eq(FollowingDO::getUserId, userId));
|
||||
|
||||
// 随机过期时间
|
||||
// 保底1天+随机秒数
|
||||
long expireSeconds = 60*60*24 + RandomUtil.randomInt(60*60*24);
|
||||
|
||||
// 如果记录为空,直接ZADD关系数据,并设置过期时间
|
||||
if (CollUtil.isEmpty(followingDOS)){
|
||||
DefaultRedisScript<Long> script2 = new DefaultRedisScript<>();
|
||||
script2.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/follow_add_and_expire.lua")));
|
||||
script2.setResultType(Long.class);
|
||||
|
||||
// TODO: 可以根据用户类型,设置不同的过期时间,若当前用户为大V, 则可以过期时间设置的长些或者不设置过期时间;如不是,则设置的短些
|
||||
// 如何判断呢?可以从计数服务获取用户的粉丝数,目前计数服务还没创建,则暂时采用统一的过期策略
|
||||
redisTemplate.execute(script2, Collections.singletonList(followingRedisKey), followUserId, timestamp, expireSeconds);
|
||||
}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 脚本:follow_check_and_add.lua , 将最新地关注关系添加进去
|
||||
result = redisTemplate.execute(script, Collections.singletonList(followingRedisKey), followUserId, timestamp);
|
||||
checkLuaScriptResult(result);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 发送 MQ
|
||||
// 构造消息体DTO
|
||||
FollowUserMqDTO followUserMqDTO = FollowUserMqDTO.builder()
|
||||
.userId(userId)
|
||||
.followUserId(followUserId)
|
||||
.createTime(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);
|
||||
}
|
||||
});
|
||||
|
||||
return Response.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 Lua 脚本结果,根据状态码抛出对应的业务异常
|
||||
* @param result Lua 脚本返回结果
|
||||
*/
|
||||
private static void checkLuaScriptResult(Long result) {
|
||||
LuaResultEnum luaResultEnum = LuaResultEnum.valueOf(result);
|
||||
|
||||
if (Objects.isNull(luaResultEnum)) throw new RuntimeException("Lua 返回结果错误");
|
||||
// 校验 Lua 脚本执行结果
|
||||
switch (luaResultEnum) {
|
||||
// 关注数已达到上限
|
||||
case FOLLOW_LIMIT -> throw new ApiException(ResponseCodeEnum.FOLLOWING_COUNT_LIMIT);
|
||||
// 已经关注了该用户
|
||||
case ALREADY_FOLLOWED -> throw new ApiException(ResponseCodeEnum.ALREADY_FOLLOWED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 Lua 脚本参数
|
||||
*
|
||||
* @param followingDOS 关注列表
|
||||
* @param expireSeconds 过期时间
|
||||
* @return Lua 脚本参数
|
||||
*/
|
||||
private static Object[] buildLuaArgs(List<FollowingDO> followingDOS, long expireSeconds) {
|
||||
int argsLength = followingDOS.size() * 2 + 1; // 每个关注关系有 2 个参数(score 和 value),再加一个过期时间
|
||||
Object[] luaArgs = new Object[argsLength];
|
||||
|
||||
int i = 0;
|
||||
for (FollowingDO following : followingDOS) {
|
||||
luaArgs[i] = DateUtils.localDateTime2Timestamp(following.getCreateTime()); // 关注时间作为 score
|
||||
luaArgs[i + 1] = following.getFollowingUserId(); // 关注的用户 ID 作为 ZSet value
|
||||
i += 2;
|
||||
}
|
||||
|
||||
luaArgs[argsLength - 1] = expireSeconds; // 最后一个参数是 ZSet 的过期时间
|
||||
return luaArgs;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.hanserwei.hannote.user.relation.biz.util;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
|
||||
public class DateUtils {
|
||||
|
||||
/**
|
||||
* LocalDateTime 转时间戳
|
||||
*
|
||||
* @param localDateTime LocalDateTime
|
||||
* @return 时间戳
|
||||
*/
|
||||
public static long localDateTime2Timestamp(LocalDateTime localDateTime) {
|
||||
return localDateTime.toInstant(ZoneOffset.UTC).toEpochMilli();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
-- 如果从数据库查询到的记录为空,则执行该脚本
|
||||
local key = KEYS[1] -- 操作的 Redis Key
|
||||
local followUserId = ARGV[1] -- 关注的用户ID
|
||||
local timestamp = ARGV[2] -- 时间戳
|
||||
local expireSeconds = ARGV[3] -- 过期时间(秒)
|
||||
|
||||
-- ZADD 添加关注关系
|
||||
redis.call('ZADD', key, timestamp, followUserId)
|
||||
-- 设置过期时间
|
||||
redis.call('EXPIRE', key, expireSeconds)
|
||||
return 0
|
||||
@@ -0,0 +1,20 @@
|
||||
-- 操作的 Key
|
||||
local key = KEYS[1]
|
||||
|
||||
-- 准备批量添加数据的参数
|
||||
local zaddArgs = {}
|
||||
|
||||
-- 遍历 ARGV 参数,将分数和值按顺序插入到 zaddArgs 变量中
|
||||
for i = 1, #ARGV - 1, 2 do
|
||||
table.insert(zaddArgs, ARGV[i]) -- 分数(关注时间)
|
||||
table.insert(zaddArgs, ARGV[i+1]) -- 值(关注的用户ID)
|
||||
end
|
||||
|
||||
-- 调用 ZADD 批量插入数据
|
||||
redis.call('ZADD', key, unpack(zaddArgs))
|
||||
|
||||
-- 设置 ZSet 的过期时间
|
||||
local expireTime = ARGV[#ARGV] -- 最后一个参数为过期时间
|
||||
redis.call('EXPIRE', key, expireTime)
|
||||
|
||||
return 0
|
||||
@@ -0,0 +1,26 @@
|
||||
-- LUA 脚本:校验并添加关注关系
|
||||
|
||||
local key = KEYS[1] -- 操作的 Redis Key
|
||||
local followUserId = ARGV[1] -- 关注的用户ID
|
||||
local timestamp = ARGV[2] -- 时间戳
|
||||
|
||||
-- 使用 EXISTS 命令检查 ZSET 是否存在
|
||||
local exists = redis.call('EXISTS', key)
|
||||
if exists == 0 then
|
||||
return -1
|
||||
end
|
||||
|
||||
-- 校验关注人数是否上限(是否达到 1000)
|
||||
local size = redis.call('ZCARD', key)
|
||||
if size >= 1000 then
|
||||
return -2
|
||||
end
|
||||
|
||||
-- 校验目标用户是否已经关注
|
||||
if redis.call('ZSCORE', key, followUserId) then
|
||||
return -3
|
||||
end
|
||||
|
||||
-- ZADD 添加关注关系
|
||||
redis.call('ZADD', key, timestamp, followUserId)
|
||||
return 0
|
||||
BIN
http-client/file/backgroundImg.jpg
Normal file
BIN
http-client/file/backgroundImg.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 744 KiB |
BIN
http-client/file/img.png
Normal file
BIN
http-client/file/img.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
@@ -3,7 +3,7 @@ POST http://localhost:8000/auth/verification/code/send
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"email": "ssw010723@gmail.com"
|
||||
"email": "2628273921@qq.com"
|
||||
}
|
||||
|
||||
### 登录/注册
|
||||
@@ -11,8 +11,8 @@ POST http://localhost:8000/auth/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"email": "ssw010723@gmail.com",
|
||||
"code": "135466",
|
||||
"email": "2628273921@qq.com",
|
||||
"code": "825004",
|
||||
"type": 1
|
||||
}
|
||||
|
||||
@@ -25,3 +25,124 @@ Authorization: Bearer {{token}}
|
||||
"newPassword": "wwgb1314"
|
||||
}
|
||||
|
||||
### 更新用户信息
|
||||
POST http://localhost:8000/user/user/update
|
||||
Content-Type: multipart/form-data; boundary=WebAppBoundary
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
--WebAppBoundary
|
||||
Content-Disposition: form-data; name="avatar"; filename="avatar.png"
|
||||
Content-Type: image/png
|
||||
|
||||
< ./file/img.png
|
||||
|
||||
--WebAppBoundary
|
||||
Content-Disposition: form-data; name="nickname"
|
||||
|
||||
Hanserwei
|
||||
|
||||
--WebAppBoundary
|
||||
Content-Disposition: form-data; name="hanNoteId"
|
||||
|
||||
hanserwei010723
|
||||
|
||||
--WebAppBoundary
|
||||
Content-Disposition: form-data; name="sex"
|
||||
|
||||
1
|
||||
|
||||
--WebAppBoundary
|
||||
Content-Disposition: form-data; name="birthday"
|
||||
|
||||
2001-07-23
|
||||
|
||||
--WebAppBoundary
|
||||
Content-Disposition: form-data; name="introduction"
|
||||
|
||||
我在HttpClient里测试的
|
||||
|
||||
--WebAppBoundary--
|
||||
Content-Disposition: form-data; name="backgroundImg"; filename="backgroundImg.png"
|
||||
Content-Type: image/jpeg
|
||||
|
||||
< ./file/backgroundImg.jpg
|
||||
|
||||
### 发布图文笔记
|
||||
POST http://localhost:8000/note/note/publish
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
{
|
||||
"type": 0,
|
||||
"imgUris": [
|
||||
"https://cdn.pixabay.com/photo/2025/10/05/15/06/autumn-9875155_1280.jpg"
|
||||
],
|
||||
"title": "图文笔记测试",
|
||||
"content": "这个是图文笔记的测试",
|
||||
"topicId": 1
|
||||
}
|
||||
|
||||
### 发布视频笔记
|
||||
POST http://localhost:8000/note/note/publish
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
{
|
||||
"type": 1,
|
||||
"videoUri": "https://cdn.pixabay.com/photo/2025/10/05/15/06/autumn-9875155_1280.jpg",
|
||||
"title": "视频笔记测试",
|
||||
"content": "这个是视频笔记的测试",
|
||||
"topicId": 2
|
||||
}
|
||||
|
||||
### 笔记详情
|
||||
POST http://localhost:8000/note/note/detail
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
{
|
||||
"id": {{noteId}}
|
||||
}
|
||||
|
||||
### 修改笔记
|
||||
POST http://localhost:8000/note/note/update
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
{
|
||||
"id": {{noteId}},
|
||||
"type": 0,
|
||||
"imgUris": [
|
||||
"https://cdn.pixabay.com/photo/2025/10/05/15/06/autumn-9875155_1280.jpg"
|
||||
],
|
||||
"title": "笔记修改测试",
|
||||
"content": "我把图文笔记的内容修改了",
|
||||
"topicId": 1
|
||||
}
|
||||
|
||||
### 关注自己
|
||||
POST http://localhost:8000/relation/relation/follow
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
{
|
||||
"followUserId": {{userId}}
|
||||
}
|
||||
|
||||
### 关注不存在的用户
|
||||
POST http://localhost:8000/relation/relation/follow
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
{
|
||||
"followUserId": -1
|
||||
}
|
||||
|
||||
### 正常关注用户
|
||||
POST http://localhost:8000/relation/relation/follow
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
{
|
||||
"followUserId": {{otherUserId}}
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
{
|
||||
"dev": {
|
||||
"token": "4bXpiBbjXEDFE4ZpqjCOHu1rP81qepl2ROOygrxRGb61K536ckLuyAwfyQHSMcyRdUzf8CxntLEMfbU2ynbYx9nJKlx4vpWZrHqv2mI4iMhnShQ4mPBi7OPPgZi22O2f"
|
||||
"token": "4bXpiBbjXEDFE4ZpqjCOHu1rP81qepl2ROOygrxRGb61K536ckLuyAwfyQHSMcyRdUzf8CxntLEMfbU2ynbYx9nJKlx4vpWZrHqv2mI4iMhnShQ4mPBi7OPPgZi22O2f",
|
||||
"otherToken": "mqFNHrWkPcipIAvw7Gn4cigOWYP54sn8HYlQX3CXTxHf90DhjFiROhWVgPqLBi35xKXOOfHlXeEdaQrkXf1JXd8hbXBOdZqnrycW96BJwTbUS40EqIZifVgPun3ai0Ek",
|
||||
"noteId": "1977249693272375330",
|
||||
"userId": "100",
|
||||
"otherUserId": "2100"
|
||||
}
|
||||
}
|
||||
@@ -175,4 +175,8 @@ CREATE TABLE `t_fans`
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_unicode_ci COMMENT ='用户粉丝表';
|
||||
|
||||
ALTER TABLE t_following ADD UNIQUE uk_user_id_following_user_id(user_id, following_user_id);
|
||||
|
||||
ALTER TABLE t_fans ADD UNIQUE uk_user_id_fans_user_id(user_id, fans_user_id);
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user