refactor(note):优化笔记点赞功能,使用 Roaring Bitmap 替代布隆过滤器
- 修改消费者组名称,统一命名规范 - 更新 HTTP 客户端测试用例中的授权令牌和笔记 ID - 引入 NoteLikeDOMapper 并替换原有的 service 查询方式 - 将布隆过滤器相关逻辑全部替换为 Roaring Bitmap 实现 - 新增多个 Lua 脚本支持 Roaring Bitmap 的操作与初始化 - 添加 Roaring Bitmap 相关的 Redis Key 构建方法 - 删除旧有的布隆过滤器校验逻辑及冗余代码 - 更新 Redis Key 常量类,增加 Roaring Bitmap 相关定义 - 日志字典文件中新增 rbitmap 关键词 - 优化点赞和取消点赞流程,提升性能与准确性
This commit is contained in:
1
.idea/dictionaries/project.xml
generated
1
.idea/dictionaries/project.xml
generated
@@ -9,6 +9,7 @@
|
|||||||
<w>mget</w>
|
<w>mget</w>
|
||||||
<w>nacos</w>
|
<w>nacos</w>
|
||||||
<w>operationlog</w>
|
<w>operationlog</w>
|
||||||
|
<w>rbitmap</w>
|
||||||
<w>rustfs</w>
|
<w>rustfs</w>
|
||||||
<w>zadd</w>
|
<w>zadd</w>
|
||||||
<w>zrevrangebyscore</w>
|
<w>zrevrangebyscore</w>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import java.util.stream.Collectors;
|
|||||||
@Component
|
@Component
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RocketMQMessageListener(
|
@RocketMQMessageListener(
|
||||||
consumerGroup = "han_note_group_" + MQConstants.TOPIC_LIKE_OR_UNLIKE,
|
consumerGroup = "han_note_count_group_" + MQConstants.TOPIC_LIKE_OR_UNLIKE,
|
||||||
topic = MQConstants.TOPIC_LIKE_OR_UNLIKE
|
topic = MQConstants.TOPIC_LIKE_OR_UNLIKE
|
||||||
)
|
)
|
||||||
public class CountNoteLikeConsumer implements RocketMQListener<String> {
|
public class CountNoteLikeConsumer implements RocketMQListener<String> {
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ public class RedisKeyConstants {
|
|||||||
*/
|
*/
|
||||||
public static final String NOTE_DETAIL_KEY = "note:detail:";
|
public static final String NOTE_DETAIL_KEY = "note:detail:";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Roaring Bitmap:用户笔记点赞 前缀
|
||||||
|
*/
|
||||||
|
public static final String R_BITMAP_USER_NOTE_LIKE_LIST_KEY = "rbitmap:note:likes:";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 布隆过滤器:用户笔记点赞
|
* 布隆过滤器:用户笔记点赞
|
||||||
*/
|
*/
|
||||||
@@ -76,4 +81,14 @@ public class RedisKeyConstants {
|
|||||||
public static String buildUserNoteCollectZSetKey(Long userId) {
|
public static String buildUserNoteCollectZSetKey(Long userId) {
|
||||||
return USER_NOTE_COLLECT_ZSET_KEY + userId;
|
return USER_NOTE_COLLECT_ZSET_KEY + userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建完整的 Roaring Bitmap:用户笔记点赞 KEY
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @return Roaring Bitmap:用户笔记点赞 KEY
|
||||||
|
*/
|
||||||
|
public static String buildRBitmapUserNoteLikeListKey(Long userId) {
|
||||||
|
return R_BITMAP_USER_NOTE_LIKE_LIST_KEY + userId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -21,6 +21,7 @@ import com.hanserwei.hannote.note.biz.domain.dataobject.NoteDO;
|
|||||||
import com.hanserwei.hannote.note.biz.domain.dataobject.NoteLikeDO;
|
import com.hanserwei.hannote.note.biz.domain.dataobject.NoteLikeDO;
|
||||||
import com.hanserwei.hannote.note.biz.domain.dataobject.TopicDO;
|
import com.hanserwei.hannote.note.biz.domain.dataobject.TopicDO;
|
||||||
import com.hanserwei.hannote.note.biz.domain.mapper.NoteDOMapper;
|
import com.hanserwei.hannote.note.biz.domain.mapper.NoteDOMapper;
|
||||||
|
import com.hanserwei.hannote.note.biz.domain.mapper.NoteLikeDOMapper;
|
||||||
import com.hanserwei.hannote.note.biz.enums.*;
|
import com.hanserwei.hannote.note.biz.enums.*;
|
||||||
import com.hanserwei.hannote.note.biz.model.dto.CollectUnCollectNoteMqDTO;
|
import com.hanserwei.hannote.note.biz.model.dto.CollectUnCollectNoteMqDTO;
|
||||||
import com.hanserwei.hannote.note.biz.model.dto.LikeUnlikeNoteMqDTO;
|
import com.hanserwei.hannote.note.biz.model.dto.LikeUnlikeNoteMqDTO;
|
||||||
@@ -76,6 +77,8 @@ public class NoteServiceImpl extends ServiceImpl<NoteDOMapper, NoteDO> implement
|
|||||||
private RedisTemplate<String, String> redisTemplate;
|
private RedisTemplate<String, String> redisTemplate;
|
||||||
@Resource
|
@Resource
|
||||||
private RocketMQTemplate rocketMQTemplate;
|
private RocketMQTemplate rocketMQTemplate;
|
||||||
|
@Resource
|
||||||
|
private NoteLikeDOMapper noteLikeDOMapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 笔记详情本地缓存
|
* 笔记详情本地缓存
|
||||||
@@ -630,14 +633,17 @@ public class NoteServiceImpl extends ServiceImpl<NoteDOMapper, NoteDO> implement
|
|||||||
// 2. 判断目标笔记,是否已经点赞过
|
// 2. 判断目标笔记,是否已经点赞过
|
||||||
Long userId = LoginUserContextHolder.getUserId();
|
Long userId = LoginUserContextHolder.getUserId();
|
||||||
|
|
||||||
// 布隆过滤器Key
|
// Roaring Bitmap Key
|
||||||
String bloomUserNoteLikeListKey = RedisKeyConstants.buildBloomUserNoteLikeListKey(userId);
|
String rbitmapUserNoteLikeListKey = RedisKeyConstants.buildRBitmapUserNoteLikeListKey(userId);
|
||||||
|
|
||||||
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
|
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
|
||||||
// Lua 脚本路径
|
// Lua 脚本路径
|
||||||
script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/bloom_note_like_check.lua")));
|
script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/rbitmap_note_like_check.lua")));
|
||||||
|
// 返回值类型
|
||||||
script.setResultType(Long.class);
|
script.setResultType(Long.class);
|
||||||
|
|
||||||
// 执行 Lua 脚本,拿到返回结果
|
// 执行 Lua 脚本,拿到返回结果
|
||||||
Long result = redisTemplate.execute(script, Collections.singletonList(bloomUserNoteLikeListKey), noteId);
|
Long result = redisTemplate.execute(script, Collections.singletonList(rbitmapUserNoteLikeListKey), noteId);
|
||||||
|
|
||||||
NoteLikeLuaResultEnum noteLikeLuaResultEnum = NoteLikeLuaResultEnum.valueOf(result);
|
NoteLikeLuaResultEnum noteLikeLuaResultEnum = NoteLikeLuaResultEnum.valueOf(result);
|
||||||
|
|
||||||
@@ -659,39 +665,26 @@ public class NoteServiceImpl extends ServiceImpl<NoteDOMapper, NoteDO> implement
|
|||||||
|
|
||||||
// 目标笔记已经被点赞
|
// 目标笔记已经被点赞
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
// 异步初始化布隆过滤器
|
// 异步初始化 Roaring Bitmap
|
||||||
threadPoolTaskExecutor.submit(() -> batchAddNoteLike2BloomAndExpire(userId, expireSeconds, bloomUserNoteLikeListKey));
|
threadPoolTaskExecutor.submit(() ->
|
||||||
|
batchAddNoteLike2RBitmapAndExpire(userId, expireSeconds, rbitmapUserNoteLikeListKey));
|
||||||
throw new ApiException(ResponseCodeEnum.NOTE_ALREADY_LIKED);
|
throw new ApiException(ResponseCodeEnum.NOTE_ALREADY_LIKED);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 若笔记未被点赞,查询当前用户是否点赞其他用户,有则同步初始化布隆过滤器
|
// 若目标笔记未被点赞,查询当前用户是否有点赞其他笔记,有则同步初始化 Roaring Bitmap
|
||||||
batchAddNoteLike2BloomAndExpire(userId, expireSeconds, bloomUserNoteLikeListKey);
|
batchAddNoteLike2RBitmapAndExpire(userId, expireSeconds, rbitmapUserNoteLikeListKey);
|
||||||
|
|
||||||
// 若数据库中也没有点赞记录,说明该用户还未点赞过任何笔记
|
// 添加当前点赞笔记 ID 到 Roaring Bitmap 中
|
||||||
// Lua 脚本路径
|
// Lua 脚本路径
|
||||||
script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/bloom_add_note_like_and_expire.lua")));
|
script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/rbitmap_add_note_like_and_expire.lua")));
|
||||||
// 返回值类型
|
// 返回值类型
|
||||||
script.setResultType(Long.class);
|
script.setResultType(Long.class);
|
||||||
redisTemplate.execute(script, Collections.singletonList(bloomUserNoteLikeListKey), noteId, expireSeconds);
|
redisTemplate.execute(script, Collections.singletonList(rbitmapUserNoteLikeListKey), noteId, expireSeconds);
|
||||||
}
|
}
|
||||||
// 目标笔记已经被点赞
|
// 目标笔记已经被点赞
|
||||||
case NOTE_LIKED -> {
|
case NOTE_LIKED -> {
|
||||||
// 校验 ZSet 列表中是否包含被点赞的笔记ID
|
|
||||||
Double score = redisTemplate.opsForZSet().score(userNoteLikeZSetKey, noteId);
|
|
||||||
|
|
||||||
if (Objects.nonNull(score)) {
|
|
||||||
throw new ApiException(ResponseCodeEnum.NOTE_ALREADY_LIKED);
|
throw new ApiException(ResponseCodeEnum.NOTE_ALREADY_LIKED);
|
||||||
}
|
}
|
||||||
// 若 Score 为空,则表示 ZSet 点赞列表中不存在,查询数据库校验
|
|
||||||
long count = noteLikeDOService.count(new LambdaQueryWrapper<>(NoteLikeDO.class)
|
|
||||||
.eq(NoteLikeDO::getNoteId, noteId)
|
|
||||||
.eq(NoteLikeDO::getStatus, LikeStatusEnum.LIKE.getCode()));
|
|
||||||
if (count > 0) {
|
|
||||||
// 数据库里面有点赞记录,而 Redis 中 ZSet 不存在,需要重新异步初始化 ZSet
|
|
||||||
asynInitUserNoteLikesZSet(userId, userNoteLikeZSetKey);
|
|
||||||
throw new ApiException(ResponseCodeEnum.NOTE_ALREADY_LIKED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// 3. 更新用户 ZSET 点赞列表
|
// 3. 更新用户 ZSET 点赞列表
|
||||||
LocalDateTime now = LocalDateTime.now();
|
LocalDateTime now = LocalDateTime.now();
|
||||||
@@ -768,6 +761,37 @@ public class NoteServiceImpl extends ServiceImpl<NoteDOMapper, NoteDO> implement
|
|||||||
return Response.success();
|
return Response.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化笔记点赞 Roaring Bitmap
|
||||||
|
*
|
||||||
|
* @param userId 用户 ID
|
||||||
|
* @param expireSeconds 过期时间
|
||||||
|
* @param rbitmapUserNoteLikeListKey RBitmap 列表 Key
|
||||||
|
*/
|
||||||
|
private void batchAddNoteLike2RBitmapAndExpire(Long userId, long expireSeconds, String rbitmapUserNoteLikeListKey) {
|
||||||
|
try {
|
||||||
|
// 异步全量同步一下,并设置过期时间
|
||||||
|
List<NoteLikeDO> noteLikeDOS = noteLikeDOMapper.selectList(new LambdaQueryWrapper<>(NoteLikeDO.class)
|
||||||
|
.eq(NoteLikeDO::getUserId, userId));
|
||||||
|
|
||||||
|
if (CollUtil.isNotEmpty(noteLikeDOS)) {
|
||||||
|
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
|
||||||
|
// Lua 脚本路径
|
||||||
|
script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/rbitmap_batch_add_note_like_and_expire.lua")));
|
||||||
|
// 返回值类型
|
||||||
|
script.setResultType(Long.class);
|
||||||
|
|
||||||
|
// 构建 Lua 参数
|
||||||
|
List<Object> luaArgs = Lists.newArrayList();
|
||||||
|
noteLikeDOS.forEach(noteLikeDO -> luaArgs.add(noteLikeDO.getNoteId())); // 将每个点赞的笔记 ID 传入
|
||||||
|
luaArgs.add(expireSeconds); // 最后一个参数是过期时间(秒)
|
||||||
|
redisTemplate.execute(script, Collections.singletonList(rbitmapUserNoteLikeListKey), luaArgs.toArray());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("## 异步初始化【笔记点赞】Roaring Bitmap 异常: ", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Response<?> unlikeNote(UnlikeNoteReqVO unlikeNoteReqVO) {
|
public Response<?> unlikeNote(UnlikeNoteReqVO unlikeNoteReqVO) {
|
||||||
// 笔记ID
|
// 笔记ID
|
||||||
@@ -780,37 +804,38 @@ public class NoteServiceImpl extends ServiceImpl<NoteDOMapper, NoteDO> implement
|
|||||||
// 当前登录用户ID
|
// 当前登录用户ID
|
||||||
Long userId = LoginUserContextHolder.getUserId();
|
Long userId = LoginUserContextHolder.getUserId();
|
||||||
|
|
||||||
// 布隆过滤器Key
|
// Roaring Bitmap Key
|
||||||
String bloomUserNoteLikeListKey = RedisKeyConstants.buildBloomUserNoteLikeListKey(userId);
|
String rbitmapUserNoteLikeListKey = RedisKeyConstants.buildRBitmapUserNoteLikeListKey(userId);
|
||||||
|
|
||||||
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
|
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
|
||||||
// Lua 脚本路径
|
// Lua 脚本路径
|
||||||
script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/bloom_note_unlike_check.lua")));
|
script.setScriptSource(new ResourceScriptSource(new ClassPathResource("/lua/rbitmap_note_unlike_check.lua")));
|
||||||
|
// 返回值类型
|
||||||
script.setResultType(Long.class);
|
script.setResultType(Long.class);
|
||||||
|
|
||||||
// 执行 Lua 脚本,拿到返回结果
|
// 执行 Lua 脚本,拿到返回结果
|
||||||
Long result = redisTemplate.execute(script, Collections.singletonList(bloomUserNoteLikeListKey), noteId);
|
Long result = redisTemplate.execute(script, Collections.singletonList(rbitmapUserNoteLikeListKey), noteId);
|
||||||
|
|
||||||
NoteUnlikeLuaResultEnum noteUnlikeLuaResultEnum = NoteUnlikeLuaResultEnum.valueOf(result);
|
NoteUnlikeLuaResultEnum noteUnlikeLuaResultEnum = NoteUnlikeLuaResultEnum.valueOf(result);
|
||||||
log.info("==> 【笔记取消点赞】Lua 脚本返回结果: {}", noteUnlikeLuaResultEnum);
|
log.info("==> 【笔记取消点赞】Lua 脚本返回结果: {}", noteUnlikeLuaResultEnum);
|
||||||
switch (Objects.requireNonNull(noteUnlikeLuaResultEnum)) {
|
switch (Objects.requireNonNull(noteUnlikeLuaResultEnum)) {
|
||||||
// 布隆过滤器不存在
|
// 布隆过滤器不存在
|
||||||
case NOT_EXIST -> {//笔记不存在
|
case NOT_EXIST -> {//笔记不存在
|
||||||
//异步初始化布隆过滤器
|
// 异步初始化 Roaring Bitmap
|
||||||
threadPoolTaskExecutor.submit(() -> {
|
threadPoolTaskExecutor.submit(() -> {
|
||||||
// 保底1天+随机秒数
|
// 保底1天+随机秒数
|
||||||
long expireSeconds = 60 * 60 * 24 + RandomUtil.randomInt(60 * 60 * 24);
|
long expireSeconds = 60 * 60 * 24 + RandomUtil.randomInt(60 * 60 * 24);
|
||||||
batchAddNoteLike2BloomAndExpire(userId, expireSeconds, bloomUserNoteLikeListKey);
|
batchAddNoteLike2RBitmapAndExpire(userId, expireSeconds, rbitmapUserNoteLikeListKey);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 从数据库中校验笔记是否被点赞
|
// 从数据库中校验笔记是否被点赞
|
||||||
long count = noteLikeDOService.count(new LambdaQueryWrapper<>(NoteLikeDO.class)
|
long count = noteLikeDOMapper.selectCount(new LambdaQueryWrapper<>(NoteLikeDO.class)
|
||||||
.eq(NoteLikeDO::getUserId, userId)
|
.eq(NoteLikeDO::getUserId, userId)
|
||||||
.eq(NoteLikeDO::getNoteId, noteId)
|
.eq(NoteLikeDO::getNoteId, noteId));
|
||||||
.eq(NoteLikeDO::getStatus, LikeStatusEnum.LIKE.getCode()));
|
|
||||||
if (count == 0) {
|
// 未点赞,无法取消点赞操作,抛出业务异常
|
||||||
log.info("==> 【笔记取消点赞】用户未点赞该笔记");
|
log.info("1111111");
|
||||||
throw new ApiException(ResponseCodeEnum.NOTE_NOT_LIKED);
|
if (count == 0) throw new ApiException(ResponseCodeEnum.NOTE_NOT_LIKED);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// 布隆过滤器校验目标笔记未被点赞(判断绝对正确)
|
// 布隆过滤器校验目标笔记未被点赞(判断绝对正确)
|
||||||
case NOTE_NOT_LIKED -> throw new ApiException(ResponseCodeEnum.NOTE_NOT_LIKED);
|
case NOTE_NOT_LIKED -> throw new ApiException(ResponseCodeEnum.NOTE_NOT_LIKED);
|
||||||
@@ -820,14 +845,9 @@ public class NoteServiceImpl extends ServiceImpl<NoteDOMapper, NoteDO> implement
|
|||||||
// 用户点赞列表ZsetKey
|
// 用户点赞列表ZsetKey
|
||||||
String userNoteLikeZSetKey = RedisKeyConstants.buildUserNoteLikeZSetKey(userId);
|
String userNoteLikeZSetKey = RedisKeyConstants.buildUserNoteLikeZSetKey(userId);
|
||||||
|
|
||||||
// TODO: 后续考虑换掉布隆过滤器。
|
|
||||||
|
|
||||||
Long removed = redisTemplate.opsForZSet().remove(userNoteLikeZSetKey, noteId);
|
Long removed = redisTemplate.opsForZSet().remove(userNoteLikeZSetKey, noteId);
|
||||||
|
|
||||||
if (Objects.nonNull(removed) && removed == 0) {
|
|
||||||
log.info("==> 【笔记取消点赞】用户未点赞该笔记");
|
|
||||||
throw new ApiException(ResponseCodeEnum.NOTE_NOT_LIKED);
|
|
||||||
}
|
|
||||||
|
|
||||||
//4. 发送 MQ, 数据更新落库
|
//4. 发送 MQ, 数据更新落库
|
||||||
// 构建MQ消息体
|
// 构建MQ消息体
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
-- 操作的 Key
|
||||||
|
local key = KEYS[1]
|
||||||
|
local noteId = ARGV[1] -- 笔记ID
|
||||||
|
local expireSeconds = ARGV[2] -- 过期时间(秒)
|
||||||
|
|
||||||
|
redis.call("R64.SETBIT", key, noteId, 1)
|
||||||
|
|
||||||
|
-- 设置过期时间
|
||||||
|
redis.call("EXPIRE", key, expireSeconds)
|
||||||
|
return 0
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
-- 操作的 Key
|
||||||
|
local key = KEYS[1]
|
||||||
|
|
||||||
|
for i = 1, #ARGV - 1 do
|
||||||
|
redis.call("R64.SETBIT", key, ARGV[i], 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 最后一个参数为过期时间
|
||||||
|
local expireTime = ARGV[#ARGV]
|
||||||
|
-- 设置过期时间
|
||||||
|
redis.call("EXPIRE", key, expireTime)
|
||||||
|
return 0
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
-- LUA 脚本:点赞 Roaring Bitmap
|
||||||
|
|
||||||
|
local key = KEYS[1] -- 操作的 Redis Key
|
||||||
|
local noteId = ARGV[1] -- 笔记ID
|
||||||
|
|
||||||
|
-- 使用 EXISTS 命令检查 Roaring Bitmap 是否存在
|
||||||
|
local exists = redis.call('EXISTS', key)
|
||||||
|
if exists == 0 then
|
||||||
|
return -1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 校验该篇笔记是否被点赞过(1 表示已经点赞,0 表示未点赞)
|
||||||
|
local isLiked = redis.call('R64.GETBIT', key, noteId)
|
||||||
|
if isLiked == 1 then
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 未被点赞,添加点赞数据
|
||||||
|
redis.call('R64.SETBIT', key, noteId, 1)
|
||||||
|
return 0
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
local key = KEYS[1] -- 操作的 Redis Key
|
||||||
|
local noteId = ARGV[1] -- 笔记ID
|
||||||
|
|
||||||
|
-- 使用 EXISTS 命令检查 Roaring Bitmap 是否存在
|
||||||
|
local exists = redis.call('EXISTS', key)
|
||||||
|
if exists == 0 then
|
||||||
|
return -1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 校验该篇笔记是否被点赞过(1 表示已经点赞,0 表示未点赞)
|
||||||
|
local isLiked = redis.call('R64.GETBIT', key, noteId)
|
||||||
|
if isLiked == 0 then
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 取消点赞,设置 Value 为 0
|
||||||
|
return redis.call('R64.SETBIT', key, noteId, 0)
|
||||||
@@ -202,16 +202,16 @@ Content-Type: application/json
|
|||||||
Authorization: Bearer {{thirdToken}}
|
Authorization: Bearer {{thirdToken}}
|
||||||
|
|
||||||
{
|
{
|
||||||
"id": 1981698494959714362
|
"id": 1985254482941837349
|
||||||
}
|
}
|
||||||
|
|
||||||
### 笔记取消点赞入口
|
### 笔记取消点赞入口
|
||||||
POST http://localhost:8000/note/note/unlike
|
POST http://localhost:8000/note/note/unlike
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
Authorization: Bearer {{otherToken}}
|
Authorization: Bearer {{thirdToken}}
|
||||||
|
|
||||||
{
|
{
|
||||||
"id": 1977249693272375330
|
"id": 1985254482941837349
|
||||||
}
|
}
|
||||||
|
|
||||||
### 笔记收藏入口
|
### 笔记收藏入口
|
||||||
|
|||||||
Reference in New Issue
Block a user