From 94729e517018e5563b4d70b44d3d1fd4766c0def Mon Sep 17 00:00:00 2001 From: Hanserwei <2628273921@qq.com> Date: Sun, 9 Nov 2025 22:09:23 +0800 Subject: [PATCH] =?UTF-8?q?refactor(note):=E4=BC=98=E5=8C=96=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E7=82=B9=E8=B5=9E=E5=8A=9F=E8=83=BD=EF=BC=8C=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=20Roaring=20Bitmap=20=E6=9B=BF=E4=BB=A3=E5=B8=83?= =?UTF-8?q?=E9=9A=86=E8=BF=87=E6=BB=A4=E5=99=A8=20-=20=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E6=B6=88=E8=B4=B9=E8=80=85=E7=BB=84=E5=90=8D=E7=A7=B0=EF=BC=8C?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E5=91=BD=E5=90=8D=E8=A7=84=E8=8C=83=20-=20?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=20HTTP=20=E5=AE=A2=E6=88=B7=E7=AB=AF?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E7=94=A8=E4=BE=8B=E4=B8=AD=E7=9A=84=E6=8E=88?= =?UTF-8?q?=E6=9D=83=E4=BB=A4=E7=89=8C=E5=92=8C=E7=AC=94=E8=AE=B0=20ID=20-?= =?UTF-8?q?=20=E5=BC=95=E5=85=A5=20NoteLikeDOMapper=20=E5=B9=B6=E6=9B=BF?= =?UTF-8?q?=E6=8D=A2=E5=8E=9F=E6=9C=89=E7=9A=84=20service=20=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E6=96=B9=E5=BC=8F=20-=20=E5=B0=86=E5=B8=83=E9=9A=86?= =?UTF-8?q?=E8=BF=87=E6=BB=A4=E5=99=A8=E7=9B=B8=E5=85=B3=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E5=85=A8=E9=83=A8=E6=9B=BF=E6=8D=A2=E4=B8=BA=20Roaring=20Bitma?= =?UTF-8?q?p=20=E5=AE=9E=E7=8E=B0=20-=20=E6=96=B0=E5=A2=9E=E5=A4=9A?= =?UTF-8?q?=E4=B8=AA=20Lua=20=E8=84=9A=E6=9C=AC=E6=94=AF=E6=8C=81=20Roarin?= =?UTF-8?q?g=20Bitmap=20=E7=9A=84=E6=93=8D=E4=BD=9C=E4=B8=8E=E5=88=9D?= =?UTF-8?q?=E5=A7=8B=E5=8C=96=20-=20=E6=B7=BB=E5=8A=A0=20Roaring=20Bitmap?= =?UTF-8?q?=20=E7=9B=B8=E5=85=B3=E7=9A=84=20Redis=20Key=20=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=E6=96=B9=E6=B3=95=20-=20=E5=88=A0=E9=99=A4=E6=97=A7?= =?UTF-8?q?=E6=9C=89=E7=9A=84=E5=B8=83=E9=9A=86=E8=BF=87=E6=BB=A4=E5=99=A8?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C=E9=80=BB=E8=BE=91=E5=8F=8A=E5=86=97=E4=BD=99?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=20-=20=E6=9B=B4=E6=96=B0=20Redis=20Key=20?= =?UTF-8?q?=E5=B8=B8=E9=87=8F=E7=B1=BB=EF=BC=8C=E5=A2=9E=E5=8A=A0=20Roarin?= =?UTF-8?q?g=20Bitmap=20=E7=9B=B8=E5=85=B3=E5=AE=9A=E4=B9=89=20-=20?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E5=AD=97=E5=85=B8=E6=96=87=E4=BB=B6=E4=B8=AD?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=20rbitmap=20=E5=85=B3=E9=94=AE=E8=AF=8D=20-?= =?UTF-8?q?=20=E4=BC=98=E5=8C=96=E7=82=B9=E8=B5=9E=E5=92=8C=E5=8F=96?= =?UTF-8?q?=E6=B6=88=E7=82=B9=E8=B5=9E=E6=B5=81=E7=A8=8B=EF=BC=8C=E6=8F=90?= =?UTF-8?q?=E5=8D=87=E6=80=A7=E8=83=BD=E4=B8=8E=E5=87=86=E7=A1=AE=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/dictionaries/project.xml | 1 + .../biz/consumer/CountNoteLikeConsumer.java | 2 +- .../note/biz/constant/RedisKeyConstants.java | 15 +++ .../biz/service/impl/NoteServiceImpl.java | 108 +++++++++++------- .../lua/rbitmap_add_note_like_and_expire.lua | 10 ++ ...rbitmap_batch_add_note_like_and_expire.lua | 12 ++ .../resources/lua/rbitmap_note_like_check.lua | 20 ++++ .../lua/rbitmap_note_unlike_check.lua | 17 +++ http-client/gateApi.http | 6 +- 9 files changed, 143 insertions(+), 48 deletions(-) create mode 100644 han-note-note/han-note-note-biz/src/main/resources/lua/rbitmap_add_note_like_and_expire.lua create mode 100644 han-note-note/han-note-note-biz/src/main/resources/lua/rbitmap_batch_add_note_like_and_expire.lua create mode 100644 han-note-note/han-note-note-biz/src/main/resources/lua/rbitmap_note_like_check.lua create mode 100644 han-note-note/han-note-note-biz/src/main/resources/lua/rbitmap_note_unlike_check.lua diff --git a/.idea/dictionaries/project.xml b/.idea/dictionaries/project.xml index 5bc28bd..ab5fea2 100644 --- a/.idea/dictionaries/project.xml +++ b/.idea/dictionaries/project.xml @@ -9,6 +9,7 @@ mget nacos operationlog + rbitmap rustfs zadd zrevrangebyscore diff --git a/han-note-count/han-note-count-biz/src/main/java/com/hanserwei/hannote/count/biz/consumer/CountNoteLikeConsumer.java b/han-note-count/han-note-count-biz/src/main/java/com/hanserwei/hannote/count/biz/consumer/CountNoteLikeConsumer.java index 12c7a29..e66fe13 100644 --- a/han-note-count/han-note-count-biz/src/main/java/com/hanserwei/hannote/count/biz/consumer/CountNoteLikeConsumer.java +++ b/han-note-count/han-note-count-biz/src/main/java/com/hanserwei/hannote/count/biz/consumer/CountNoteLikeConsumer.java @@ -28,7 +28,7 @@ import java.util.stream.Collectors; @Component @Slf4j @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 ) public class CountNoteLikeConsumer implements RocketMQListener { diff --git a/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/constant/RedisKeyConstants.java b/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/constant/RedisKeyConstants.java index c4eadb5..0927ede 100644 --- a/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/constant/RedisKeyConstants.java +++ b/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/constant/RedisKeyConstants.java @@ -7,6 +7,11 @@ public class RedisKeyConstants { */ 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) { 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; + } } \ No newline at end of file diff --git a/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/service/impl/NoteServiceImpl.java b/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/service/impl/NoteServiceImpl.java index d2d0f1f..f7ad016 100644 --- a/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/service/impl/NoteServiceImpl.java +++ b/han-note-note/han-note-note-biz/src/main/java/com/hanserwei/hannote/note/biz/service/impl/NoteServiceImpl.java @@ -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.TopicDO; 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.model.dto.CollectUnCollectNoteMqDTO; import com.hanserwei.hannote.note.biz.model.dto.LikeUnlikeNoteMqDTO; @@ -76,6 +77,8 @@ public class NoteServiceImpl extends ServiceImpl implement private RedisTemplate redisTemplate; @Resource private RocketMQTemplate rocketMQTemplate; + @Resource + private NoteLikeDOMapper noteLikeDOMapper; /** * 笔记详情本地缓存 @@ -630,14 +633,17 @@ public class NoteServiceImpl extends ServiceImpl implement // 2. 判断目标笔记,是否已经点赞过 Long userId = LoginUserContextHolder.getUserId(); - // 布隆过滤器Key - String bloomUserNoteLikeListKey = RedisKeyConstants.buildBloomUserNoteLikeListKey(userId); + // Roaring Bitmap Key + String rbitmapUserNoteLikeListKey = RedisKeyConstants.buildRBitmapUserNoteLikeListKey(userId); + DefaultRedisScript script = new DefaultRedisScript<>(); // 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); + // 执行 Lua 脚本,拿到返回结果 - Long result = redisTemplate.execute(script, Collections.singletonList(bloomUserNoteLikeListKey), noteId); + Long result = redisTemplate.execute(script, Collections.singletonList(rbitmapUserNoteLikeListKey), noteId); NoteLikeLuaResultEnum noteLikeLuaResultEnum = NoteLikeLuaResultEnum.valueOf(result); @@ -659,38 +665,25 @@ public class NoteServiceImpl extends ServiceImpl implement // 目标笔记已经被点赞 if (count > 0) { - // 异步初始化布隆过滤器 - threadPoolTaskExecutor.submit(() -> batchAddNoteLike2BloomAndExpire(userId, expireSeconds, bloomUserNoteLikeListKey)); + // 异步初始化 Roaring Bitmap + threadPoolTaskExecutor.submit(() -> + batchAddNoteLike2RBitmapAndExpire(userId, expireSeconds, rbitmapUserNoteLikeListKey)); throw new ApiException(ResponseCodeEnum.NOTE_ALREADY_LIKED); } - // 若笔记未被点赞,查询当前用户是否点赞其他用户,有则同步初始化布隆过滤器 - batchAddNoteLike2BloomAndExpire(userId, expireSeconds, bloomUserNoteLikeListKey); + // 若目标笔记未被点赞,查询当前用户是否有点赞其他笔记,有则同步初始化 Roaring Bitmap + batchAddNoteLike2RBitmapAndExpire(userId, expireSeconds, rbitmapUserNoteLikeListKey); - // 若数据库中也没有点赞记录,说明该用户还未点赞过任何笔记 + // 添加当前点赞笔记 ID 到 Roaring Bitmap 中 // 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); - redisTemplate.execute(script, Collections.singletonList(bloomUserNoteLikeListKey), noteId, expireSeconds); + redisTemplate.execute(script, Collections.singletonList(rbitmapUserNoteLikeListKey), noteId, expireSeconds); } // 目标笔记已经被点赞 case NOTE_LIKED -> { - // 校验 ZSet 列表中是否包含被点赞的笔记ID - Double score = redisTemplate.opsForZSet().score(userNoteLikeZSetKey, noteId); - - if (Objects.nonNull(score)) { - 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); - } + throw new ApiException(ResponseCodeEnum.NOTE_ALREADY_LIKED); } } // 3. 更新用户 ZSET 点赞列表 @@ -768,6 +761,37 @@ public class NoteServiceImpl extends ServiceImpl implement 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 noteLikeDOS = noteLikeDOMapper.selectList(new LambdaQueryWrapper<>(NoteLikeDO.class) + .eq(NoteLikeDO::getUserId, userId)); + + if (CollUtil.isNotEmpty(noteLikeDOS)) { + DefaultRedisScript 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 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 public Response unlikeNote(UnlikeNoteReqVO unlikeNoteReqVO) { // 笔记ID @@ -780,37 +804,38 @@ public class NoteServiceImpl extends ServiceImpl implement // 当前登录用户ID Long userId = LoginUserContextHolder.getUserId(); - // 布隆过滤器Key - String bloomUserNoteLikeListKey = RedisKeyConstants.buildBloomUserNoteLikeListKey(userId); + // Roaring Bitmap Key + String rbitmapUserNoteLikeListKey = RedisKeyConstants.buildRBitmapUserNoteLikeListKey(userId); DefaultRedisScript script = new DefaultRedisScript<>(); // 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); // 执行 Lua 脚本,拿到返回结果 - Long result = redisTemplate.execute(script, Collections.singletonList(bloomUserNoteLikeListKey), noteId); + Long result = redisTemplate.execute(script, Collections.singletonList(rbitmapUserNoteLikeListKey), noteId); NoteUnlikeLuaResultEnum noteUnlikeLuaResultEnum = NoteUnlikeLuaResultEnum.valueOf(result); log.info("==> 【笔记取消点赞】Lua 脚本返回结果: {}", noteUnlikeLuaResultEnum); switch (Objects.requireNonNull(noteUnlikeLuaResultEnum)) { // 布隆过滤器不存在 case NOT_EXIST -> {//笔记不存在 - //异步初始化布隆过滤器 + // 异步初始化 Roaring Bitmap threadPoolTaskExecutor.submit(() -> { // 保底1天+随机秒数 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::getNoteId, noteId) - .eq(NoteLikeDO::getStatus, LikeStatusEnum.LIKE.getCode())); - if (count == 0) { - log.info("==> 【笔记取消点赞】用户未点赞该笔记"); - throw new ApiException(ResponseCodeEnum.NOTE_NOT_LIKED); - } + .eq(NoteLikeDO::getNoteId, noteId)); + + // 未点赞,无法取消点赞操作,抛出业务异常 + log.info("1111111"); + if (count == 0) 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 implement // 用户点赞列表ZsetKey String userNoteLikeZSetKey = RedisKeyConstants.buildUserNoteLikeZSetKey(userId); - // TODO: 后续考虑换掉布隆过滤器。 Long removed = redisTemplate.opsForZSet().remove(userNoteLikeZSetKey, noteId); - if (Objects.nonNull(removed) && removed == 0) { - log.info("==> 【笔记取消点赞】用户未点赞该笔记"); - throw new ApiException(ResponseCodeEnum.NOTE_NOT_LIKED); - } //4. 发送 MQ, 数据更新落库 // 构建MQ消息体 diff --git a/han-note-note/han-note-note-biz/src/main/resources/lua/rbitmap_add_note_like_and_expire.lua b/han-note-note/han-note-note-biz/src/main/resources/lua/rbitmap_add_note_like_and_expire.lua new file mode 100644 index 0000000..eb8cc93 --- /dev/null +++ b/han-note-note/han-note-note-biz/src/main/resources/lua/rbitmap_add_note_like_and_expire.lua @@ -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 diff --git a/han-note-note/han-note-note-biz/src/main/resources/lua/rbitmap_batch_add_note_like_and_expire.lua b/han-note-note/han-note-note-biz/src/main/resources/lua/rbitmap_batch_add_note_like_and_expire.lua new file mode 100644 index 0000000..c342205 --- /dev/null +++ b/han-note-note/han-note-note-biz/src/main/resources/lua/rbitmap_batch_add_note_like_and_expire.lua @@ -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 diff --git a/han-note-note/han-note-note-biz/src/main/resources/lua/rbitmap_note_like_check.lua b/han-note-note/han-note-note-biz/src/main/resources/lua/rbitmap_note_like_check.lua new file mode 100644 index 0000000..ce64463 --- /dev/null +++ b/han-note-note/han-note-note-biz/src/main/resources/lua/rbitmap_note_like_check.lua @@ -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 diff --git a/han-note-note/han-note-note-biz/src/main/resources/lua/rbitmap_note_unlike_check.lua b/han-note-note/han-note-note-biz/src/main/resources/lua/rbitmap_note_unlike_check.lua new file mode 100644 index 0000000..c95dd23 --- /dev/null +++ b/han-note-note/han-note-note-biz/src/main/resources/lua/rbitmap_note_unlike_check.lua @@ -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) diff --git a/http-client/gateApi.http b/http-client/gateApi.http index e517dd0..ba641f5 100644 --- a/http-client/gateApi.http +++ b/http-client/gateApi.http @@ -202,16 +202,16 @@ Content-Type: application/json Authorization: Bearer {{thirdToken}} { - "id": 1981698494959714362 + "id": 1985254482941837349 } ### 笔记取消点赞入口 POST http://localhost:8000/note/note/unlike Content-Type: application/json -Authorization: Bearer {{otherToken}} +Authorization: Bearer {{thirdToken}} { - "id": 1977249693272375330 + "id": 1985254482941837349 } ### 笔记收藏入口