Redis --- 使用Feed流实现社交平台的新闻流

news2025/4/24 22:51:50

 要实现一个 Feed 流(类似于社交媒体中的新闻流),通常涉及以下几个要素:

  1. 内容发布:用户发布内容(例如文章、状态更新、图片等)。
  2. 内容订阅:用户可以订阅其他用户的内容,获取实时更新。
  3. 内容展示:根据用户订阅的内容,将符合其兴趣或权限的内容按时间顺序展示出来。
  4. 实时更新:当有新的内容发布时,相关用户的 feed 流应该即时更新。

在社交平台、新闻流或类似应用中,Timeline(时间线)和智能排序是两个非常关键的功能。它们决定了用户在页面中看到的内容排序方式,直接影响用户体验。下面我将详细解释这两个概念,并且提供一些思路来实现它们。

在分布式系统和消息队列中,拉模式(Pull)和推模式(Push)是两种常见的数据传输方式。它们在不同的场景下有不同的应用。推拉结合模式(Push-Pull)结合了这两者的优点,能够更好地应对复杂的业务需求。 

  1. 拉模式是指消费者主动向服务端请求数据,服务端在接收到请求时返回数据。消费者控制请求的时机和频率。
  2. 推模式是指服务端主动将数据推送到消费者,消费者不需要发起请求,只需要接收数据。
  3. 推拉结合模式结合了推模式和拉模式的优点,消费者既可以主动拉取数据,也可以被服务器主动推送数据。通过这种模式,系统可以根据不同的场景灵活地选择推送或拉取方式,提升系统的性能和可靠性。        

另外,Feed流不能采用传统的分页模式:

 所以采用滚动分页:

在 Redis 中,ZSET(有序集合)是一个非常常用的数据结构,它可以用来存储带有分数的元素,并按分数进行排序。分页查询是获取ZSET部分元素的一种方式,通常通过 ZRANGEZREVRANGE 命令来实现。

在分页查询时,主要的目的是限制返回的数据量,并且支持通过“偏移量”(offset)和“数量”(limit)来控制分页的效果。Redis 的 ZSET 本身并不直接支持传统数据库那种基于页码的分页,但可以通过索引和 ZRANGE 命令来实现分页效果。

分页查询原理:

假设我们有一个存储博客点赞信息的 ZSET,其中每个博客的 ID 和点赞数是按分数score存储的。我们可以使用 ZRANGE(或 ZREVRANGE)命令来返回指定区间内的元素。

基本操作:

  • ZRANGE key start stop [WITHSCORES]:按分数升序返回 ZSET 中从 startstop 索引范围内的元素。WITHSCORES 可选,表示返回元素的分数。
  • ZREVRANGE key start stop [WITHSCORES]:按分数降序返回 ZSET 中从 startstop 索引范围内的元素。

分页查询示例:

假设有一个 ZSET,它存储了用户对某个博客的点赞数,键为 blog:likes:{id},其中 id 为博客的唯一标识。我们希望按照点赞数降序返回该博客的前10名用户。

ZRANGE blog:likes:1 0 9 WITHSCORES   # 获取从第1到第10个用户,WITHSCORES 返回每个用户的点赞数

上述命令会返回 ZSET 中按分数升序排的前10个用户和他们的点赞数。如果我们希望按点赞数降序排列,可以使用 ZREVRANGE

ZREVRANGE blog:likes:1 0 9 WITHSCORES  # 获取从第1到第10个用户,按分数降序

@Data
public class ScrollResult {
    private List<?> list;      // 存储查询结果的列表
    private Long minTime;      // 存储分页查询中最小的时间戳,用于下一页查询
    private Integer offset;    // 存储当前分页的偏移量(用于计算下一页的偏移)
}
@RestController
@RequestMapping("/blog")
public class BlogController {
    @Resource
    private IBlogService blogService;
    @Resource
    private IUserService userService;  
    @GetMapping
    public Result queryBlogOfFollow(
            @RequestParam("lastId") Long max, @RequestParam(value = "offset", defaultValue = "0") Integer offset) {
        return blogService.queryBlogOfFollow(max, offset);
    }
}

offset 是用来分页的:

offset 控制的是 跳过多少条数据,也就是说,它指定了从查询结果的第几条记录开始返回。例如:

  • offset = 0 表示从第一页的第一条记录开始查询。
  • offset = 2 表示从第二页的第一条记录开始查询,跳过前面两条。

这对于分页来说是非常重要的,可以确保你一次加载的数据不会过多,降低数据库和 Redis 的负担。


0max 控制查询的时间范围(避免查询过多数据):

0max 作为 分数范围 的参数,限制了 Redis 查询的 数据范围。具体来说:

  • 0:表示从时间戳最早的动态开始查找。这是为了确保不会遗漏从最早时间点开始的数据。
  • max:表示查询到的时间戳不会超过 max 的值。max 可能是一个具体的时间戳(如当前时间),用于限制查询的数据不超过这个时间点的数据。

因此,0max 控制的是 查询的时间范围,确保你只查询到特定时间段内的数据。

@Override
public Result queryBlogOfFollow(Long max, Integer offset) {
    // 获取当前登录用户的ID
    Long userId = UserHolder.getUser().getId();
    // 定义Redis的key,存储的是当前用户的动态信息(博客)
    String key = "feed:" + userId;
    // 从Redis ZSET中获取按时间戳降序排列的动态数据,返回指定的范围和分页
    Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet()
            .reverseRangeByScoreWithScores(key, 0, max, offset, 2);  // 获取分数(时间戳)小于max的前2条数据
  
    // 如果查询结果为空,直接返回空的响应
    if (typedTuples == null || typedTuples.isEmpty()) {
        return Result.ok();  // 如果没有数据,返回空的分页结果
    }

    // 用于存储动态的ID集合
    List<Long> ids = new ArrayList<>(typedTuples.size());
    // 用于记录分页查询中最小的时间戳
    long minTime = 0;
    // 用于记录当前页面的偏移量(即当前分页的位置)
    int os = 1;
    // 遍历查询结果
    for (ZSetOperations.TypedTuple<String> typedTuple : typedTuples) {
        // 获取动态ID(字符串形式)
        String idStr = typedTuple.getValue();
        // 将动态ID从String转换为Long,并添加到ids列表中
        Long id = Long.valueOf(idStr);
        ids.add(id);

        // 获取动态的时间戳(作为分数存储)
        long time = typedTuple.getScore().longValue();
        // 如果当前时间戳与上一条数据的时间戳相同,说明是同一时间段的动态,偏移量加1
        if (time == minTime) {
            os++;  // 同一时间戳的动态,增加偏移量
        } else {
            // 如果时间戳不同,更新最小时间戳,并重置偏移量
            minTime = time;  
            os = 1;  // 该时间戳下的动态的偏移量从1开始
        }
    }

    // 将动态ID列表转化为逗号分隔的字符串,用于查询数据库
    String idStr = StrUtil.join(",", ids);

    // 根据ID查询博客数据,返回的结果按照ID顺序排序
    List<Blog> blogs = query().in("id", ids).last("order by ids" + idStr + ")").list();

    // 创建一个ScrollResult对象,用于封装分页查询的结果
    ScrollResult r = new ScrollResult();
    r.setList(blogs);  // 设置当前页面的博客动态列表
    r.setOffset(os);   // 设置当前页的偏移量(分页位置)
    r.setMinTime(minTime);  // 设置当前页面的最小时间戳,用于下一次分页查询

    // 返回封装好的结果
    return Result.ok(r);  // 返回查询结果
}

方法签名与参数说明


@Override
public Result queryBlogOfFollow(Long max, Integer offset) {
  • max:这是一个时间戳,表示查询的最大时间。Redis 中的 ZSET 是根据分数(score)排序的,通常我们用时间戳作为分数,因此 max 表示最大时间戳,通常是当前时间或某个固定的时间点。因此 max 表示查询的时间范围的上限。当我们查询 ZSET 时,可以通过 max 来限制查询返回的元素的时间戳范围。例如,查询 max 小于某个时间戳的所有动态,保证我们只获取当前时间之前的动态。
  • offset:表示分页的偏移量,用来指定从查询结果的哪个位置开始返回。它帮助我们在查询时跳过前 offset 条记录,从而实现分页。在分页的过程中,每次查询都需要传递不同的 offset,以便从正确的记录位置开始查询。通常 offset 是通过上次查询结果的偏移量计算出来的。

举个例子: 假设每页显示 2 条动态:

  • 第一页:offset = 0
  • 第二页:offset = 2(跳过前2条数据,查询从第3条开始的数据)
  • 第三页:offset = 4(跳过前4条数据,查询从第5条开始的数据)

该方法的目标是查询当前用户的关注者发布的博客(动态),并分页返回结果。


Redis 查询:获取关注者发布的博客动态


String key = "feed:" + userId;
Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet()
        .reverseRangeByScoreWithScores(key, 0, max, offset, 2);
  • key = "feed:" + userId:每个用户的动态(博客)存储在以 feed: 为前缀的 Redis ZSET 中,userId 是动态数据的唯一标识符。
  • reverseRangeByScoreWithScores(key, 0, max, offset, 2)
    • reverseRangeByScoreWithScores:该命令用于按分数降序返回指定范围的元素及其分数。这里的 score 对应的是时间戳,因此可以按时间顺序从最新的动态开始查询。
    • 0max:表示查询的时间戳范围。查询 score0max 的元素。
    • offset:表示分页查询的起始偏移量,通常是上一页的最后一条记录的索引
    • 2:每次查询时返回的结果数量。也就是说,这一命令会返回最多 2 条动态。根据业务需求,这里设置为每次返回 2 条数据。通过调整这个数字,可以控制每次查询返回的数据量。

通过 offset2 实现分页查询:

  • offset:用来控制查询的起始位置,避免一次查询返回所有数据。
  • 2:每次查询返回 2 条记录。这可以减少一次查询的结果集大小,提高查询效率。

灵活控制查询范围

0max 使得我们能够灵活地控制查询的时间范围。通常,0 是为了兼容性的写法,表示从最小时间开始查询。max 用于控制查询的上限,确保返回的动态时间不会超过指定时间。

返回值:

返回的是一个 Set<ZSetOperations.TypedTuple<String>>,每个 TypedTuple 包含两个部分:

  • 值(blogId):即每个动态的 ID,存储在 typedTuple.getValue() 中。
  • 分数(时间戳):即该动态的时间戳,存储在 typedTuple.getScore() 中。

解析数据:获取动态 ID 和时间戳


List<Long> ids = new ArrayList<>(typedTuples.size());
long minTime = 0;
int os = 1;
for (ZSetOperations.TypedTuple<String> typedTuple : typedTuples) {
    String idStr = typedTuple.getValue();
    Long id = Long.valueOf(idStr);
    ids.add(id);

    long time = typedTuple.getScore().longValue();
    if (time == minTime) {
        os++;
    } else {
        minTime = time;
        os = 1;
    }
}
  • ids:用于存储查询到的动态 ID。
  • minTime:用于记录当前页面最早的时间戳。在分页中,minTime 的作用是保证在下一次查询时,从上一次查询的时间戳之后开始获取数据,避免重复数据。
  • os:表示当前页的偏移量。每次分页查询时,os 递增,用于记录当前页面的偏移量。

在循环中:

  • typedTuple 中获取 动态 ID时间戳
  • 时间戳相等时,偏移量(os)加一,表示当前的时间戳下有多个动态,显示顺序为相同时间戳下的顺序。
  • 时间戳不相等时,更新 minTime 和偏移量 os

根据动态 ID 查询博客


String idStr = StrUtil.join(",", ids);
List<Blog> blogs = query().in("id", ids).last("order by ids" + idStr + ")").list();
  • StrUtil.join(",", ids):将动态 ID 列表 ids 转换为一个逗号分隔的字符串,生成用于 SQL 查询的 ID 列表。
  • 查询博客:使用 query().in("id", ids) 根据 ID 列表查询对应的博客。.last("order by ids" + idStr + ")") 用于保证查询的博客顺序与 Redis 中 ZSET 的顺序一致。这里的 order by 语句应该是通过动态拼接 idStr 来确保顺序正确。

注意:拼接 SQL 语句时要小心 SQL 注入问题,避免使用不安全的字符串拼接方法。


如何计算 offset(偏移量)


假设数据库中的动态(博客)是这样的:

Blog IDTimeStamp (ms)Content
11609459200000 (2021-01-01)Blog 1 (2021-01-01)
21609459200000 (2021-01-01)Blog 2 (2021-01-01)
31609462800000 (2021-01-01)Blog 3 (2021-01-01)
41609466400000 (2021-01-01)Blog 4 (2021-01-01)
51609470000000 (2021-01-01)Blog 5 (2021-01-01)

我们的目标是 分页查询 这些博客动态,使用 offsetminTime 来控制查询结果。

1. 第一次查询
  • 假设我们查询第一页的内容,查询条件
    • 每页返回 2 条数据。
    • offset = 0(第一页,从第1条数据开始查询)。
    • max = 1609470000000(查询的时间范围上限,保证不超出当前最大时间戳)。
reverseRangeByScoreWithScores("feed:123", 0, max, offset, 2);

返回结果

  • 从 Redis 返回的动态列表为:Blog 1Blog 2(时间戳 1609459200000)。

minTime 计算

  • minTime 是当前分页查询结果中 最小的时间戳。这里的 minTime 就是 1609459200000,这是 Blog 2 的时间戳。

os 计算

  • os1 开始,因为这是第一页。如果 os 在同一时间戳的动态中增加,它会递增。此时,os = 2,因为在相同时间戳(1609459200000)下,第一条动态 Blog 1os = 1,第二条动态 Blog 2os = 2

封装返回结果

ScrollResult r = new ScrollResult();
r.setList([Blog 1, Blog 2]);  // 返回这两条数据
r.setMinTime(1609459200000);  // 设置最小时间戳,供下一页查询
r.setOffset(2);  // 下一页的偏移量从2开始
2. 第二次查询
  • 下一页的查询,offset = 2(跳过前两条动态,从第三条数据开始查询),minTime = 1609459200000(上一页的最小时间戳)。
  • 查询参数:
reverseRangeByScoreWithScores("feed:123", minTime + 1, max, offset, 2);

查询条件

  • minTime + 1 表示从上一页返回的 最小时间戳之后 开始查询,因此查询的范围从时间戳大于 1609459200000 开始。
  • offset = 2,意味着查询从第三条数据开始。

返回结果

  • 从 Redis 返回的动态为:Blog 3Blog 4(时间戳 16094628000001609466400000)。

minTime 计算

  • minTime 是当前页面的最小时间戳,这里是 1609462800000,即 Blog 3 的时间戳。

os 计算

  • os 重置为 1,因为 Blog 3 的时间戳是新的时间段,os = 1 表示该时间段的第一条动态。

封装返回结果

ScrollResult r = new ScrollResult();
r.setList([Blog 3, Blog 4]);  // 返回这两条数据
r.setMinTime(1609462800000);  // 设置最小时间戳,供下一页查询
r.setOffset(2);  // 下一页的偏移量为2,表示如果继续分页,应该从第5条数据开始查询
3. 第三次查询
  • 下一页的查询,offset = 4(跳过前四条数据,从第五条数据开始查询),minTime = 1609462800000(上一页的最小时间戳)。
  • 查询条件:
reverseRangeByScoreWithScores("feed:123", minTime + 1, max, offset, 2);

查询条件

  • minTime + 1 表示从上一页返回的 最小时间戳之后 开始查询,查询的时间戳范围是 1609462800000 之后的数据。
  • offset = 4,意味着查询从第五条数据开始。

返回结果

  • 从 Redis 返回的动态为:Blog 5(时间戳 1609470000000)。

minTime 计算

  • minTime 是当前页面的最小时间戳,这里是 1609470000000,即 Blog 5 的时间戳。

os 计算

  • os 重置为 1,因为这条动态是当前时间段下的第一条动态。

封装返回结果

ScrollResult r = new ScrollResult();
r.setList([Blog 5]);  // 返回这条数据
r.setMinTime(1609470000000);  // 设置最小时间戳,供下一页查询(下一页无数据)
r.setOffset(2);  // 假设这已经是最后一页,下一次查询会返回空数据

分页查询总结:

  1. 第一次查询:使用 offset = 0,从第一页开始查询,返回 Blog 1Blog 2
  2. 第二次查询:使用 minTime = 1609459200000offset = 2,从第三条数据开始查询,返回 Blog 3Blog 4
  3. 第三次查询:使用 minTime = 1609462800000offset = 4,从第五条数据开始查询,返回 Blog 5

offsetminTime 的作用:

  • offset:控制查询从哪个位置开始,分页跳过之前的数据。每次查询后,offset 会更新以确保下一页的查询从正确的位置开始。
  • minTime:控制查询的时间范围。每一页返回的结果中,minTime 是当前页最小的时间戳,帮助下一页查询跳过已经返回的数据。

这个过程保证了 按时间戳分页查询 的效果,并避免了重复数据。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2293715.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

游戏引擎学习第88天

仓库:https://gitee.com/mrxiao_com/2d_game_2 调查碰撞检测器中的可能错误 在今天的目标是解决一个可能存在的碰撞检测器中的错误。之前有人提到在检测器中可能有一个拼写错误&#xff0c;具体来说是在测试某个变量时&#xff0c;由于引入了一个新的变量而没有正确地使用它&…

c++中priority_queue的应用及模拟实现

1.介绍 priority_queue 是一种数据结构&#xff0c;它允许你以特定的顺序存储和访问元素。在 C 标准模板库&#xff08;STL&#xff09;中&#xff0c;priority_queue 是一个基于容器适配器的类模板&#xff0c;它默认使用 std::vector 作为底层容器&#xff0c;并且默认使用最…

游戏引擎 Unity - Unity 设置为简体中文、Unity 创建项目

Unity Unity 首次发布于 2005 年&#xff0c;属于 Unity Technologies Unity 使用的开发技术有&#xff1a;C# Unity 的适用平台&#xff1a;PC、主机、移动设备、VR / AR、Web 等 Unity 的适用领域&#xff1a;开发中等画质中小型项目 Unity 适合初学者或需要快速上手的开…

【Elasticsearch】geohex grid聚合

在 Elasticsearch 中&#xff0c;地理边界过滤是一种用于筛选地理数据的技术&#xff0c;它可以根据指定的地理边界形状&#xff08;如矩形、多边形等&#xff09;来过滤符合条件的文档。这种方法在地理空间数据分析中非常有用&#xff0c;尤其是在需要将数据限制在特定地理区域…

crewai框架第三方API使用官方RAG工具(pdf,csv,json)

最近在研究调用官方的工具&#xff0c;但官方文档的说明是在是太少了&#xff0c;后来在一个视频里看到了如何配置&#xff0c;记录一下 以PDF RAG Search工具举例&#xff0c;官方文档对于自定义模型的说明如下&#xff1a; 默认情况下&#xff0c;该工具使用 OpenAI 进行嵌…

算法 哈夫曼树和哈夫曼编码

目录 前言 一&#xff0c;二进制转码 二&#xff0c;哈夫曼编码和哈夫曼树 三&#xff0c;蓝桥杯 16 哈夫曼树 总结 前言 这个文章需要有一定的树的基础&#xff0c;没学过树的伙伴可以去看我博客树的文章 当我们要编码一个字符串转成二进制的时候&#xff0c;我们要怎么…

Sumatra PDF:小巧免费,满足多样阅读需求

Sumatra PDF是一款完全免费的本地阅读器软件&#xff0c;以小巧的体积和全面的功能受到用户青睐。如今&#xff0c;它已经更新到3.3版本&#xff0c;带来了更多实用功能&#xff0c;尤其是新增的注释功能&#xff0c;值得我们再次关注。 软件特色 轻量级体积&#xff1a;压缩…

TiDB 分布式数据库多业务资源隔离应用实践

导读 随着 TiDB 在各行业客户中的广泛应用 &#xff0c;特别是在多个业务融合到一套 TiDB 集群中的场景&#xff0c;各企业对集群内多业务隔离的需求日益增加。与此同时&#xff0c;TiDB 在多业务融合场景下的资源隔离方案日趋完善&#xff0c;详情可参考文章 《你需要什么样的…

105,【5】buuctf web [BJDCTF2020]Easy MD5

进入靶场 先输入试试回显 输入的值成了password的内容 查看源码&#xff0c;尝试得到信息 什么也没得到 抓包&#xff0c;看看请求与响应里有什么信息 响应里得到信息 hint: select * from admin where passwordmd5($pass,true) 此时需要绕过MD5&#xff08;&#xff09;函…

BFS(广度优先搜索)——搜索算法

BFS&#xff0c;也就是广度&#xff08;宽度&#xff09;优先搜索&#xff0c;二叉树的层序遍历就是一个BFS的过程。而前、中、后序遍历则是DFS&#xff08;深度优先搜索&#xff09;。从字面意思也很好理解&#xff0c;DFS就是一条路走到黑&#xff0c;BFS则是一层一层地展开。…

33.Word:国家中长期人才发展规划纲要【33】

目录 NO1.2样式​ NO3​ 图表 ​ NO4.5.6​ 开始→段落标记视图→导航窗格→检查有无遗漏 NO1.2样式 F12/另存为&#xff1a;Word.docx&#xff1a;考生文件夹样式的复制样式的修改 样式的应用&#xff08;没有相似/超级多的情况下&#xff09;——替换 [ ]通配符&#x…

gym-anytrading

参考&#xff1a;https://github.com/upb-lea/gym-electric-motor AnyTrading 是一组基于 reinforcement learning (RL) 的 trading algorithms&#xff08;交易算法&#xff09;的 OpenAI Gym 环境集合。 该项目主要用于foreign exchange (FOREX) 和 stock markets (股票市场)…

如何自定义软件安装路径及Scoop包管理器使用全攻略

如何自定义软件安装路径及Scoop包管理器使用全攻略 一、为什么无法通过WingetUI自定义安装路径&#xff1f; 问题背景&#xff1a; WingetUI是Windows包管理器Winget的图形化工具&#xff0c;但无法直接修改软件的默认安装路径。原因如下&#xff1a; Winget设计限制&#xf…

私有化部署 DeepSeek + Dify,构建你的专属私人 AI 助手

私有化部署 DeepSeek Dify&#xff0c;构建你的专属私人 AI 助手 概述 DeepSeek 是一款开创性的开源大语言模型&#xff0c;凭借其先进的算法架构和反思链能力&#xff0c;为 AI 对话交互带来了革新性的体验。通过私有化部署&#xff0c;你可以充分掌控数据安全和使用安全。…

Java 进阶 01 —— 5 分钟回顾一下 Java 基础知识

Java 进阶 01 —— 5 分钟回顾一下 Java 基础知识 Java 生态圈Java 跨平台的语言 Java 虚拟机规范JVM 跨语言的平台多语言混合编程两种架构 举例 JVM 的生命周期 虚拟机的启动虚拟机的执行虚拟机的退出 JVM 发展历程 Sun Classic VMExact VMHotSpotBEA 的 JRockitIBM 的 J9 …

V103开发笔记1-20250113

2025-01-13 一、应用方向分析 应用项目&#xff1a; PCBFLY无人机项目&#xff08;包括飞控和手持遥控器&#xff09;&#xff1b; 分析移植项目&#xff0c;应用外设资源包括&#xff1a; GPIO, PWM,USART,GPIO模拟I2C/SPI, ADC,DMA,USB等&#xff1b; 二、移植项目的基本…

DeepSeek研究员在线爆料:R1训练仅用两到三周,春节期间观察到R1 zero强大进化

内容提要 刚刚我注意到DeepSeek研究员Daya Guo回复了网友有关DeepSeek R1的一些问题&#xff0c;以及接下来的公司的计划&#xff0c;只能说DeepSeek的R1仅仅只是开始&#xff0c;内部研究还在快速推进&#xff0c;DeepSeek 的研究员过年都没歇&#xff0c;一直在爆肝推进研究…

LLM推理--vLLM解读

主要参考&#xff1a; vLLM核心技术PagedAttention原理 总结一下 vLLM 的要点&#xff1a; Transformer decoder 结构推理时需要一个token一个token生成&#xff0c;且每个token需要跟前序所有内容做注意力计算&#xff08;包括输入的prompt和该token之前生成的token&#xf…

vscode软件操作界面UI布局@各个功能区域划分及其名称称呼

文章目录 abstract检查用户界面的主要区域官方文档关于UI的介绍 abstract 检查 Visual Studio Code 用户界面 - Training | Microsoft Learn 本质上&#xff0c;Visual Studio Code 是一个代码编辑器&#xff0c;其用户界面和布局与许多其他代码编辑器相似。 界面左侧是用于访…

一键开启/关闭deepseek

一键开启/关闭 Deepseek对应下载的模型一键开启 Deepseek&#xff0c;一键关闭Deepseek双击对应的bat&#xff0c;就可以启动https://mbd.pub/o/bread/Z56YmpZvbat 下载&#xff1a;https://mbd.pub/o/bread/Z56YmpZv 可以自己写下来&#xff0c;保存成bat文件&#xff0c;也可…