Posted in

【最后召集】Go抢菜插件开发者闭门会报名开启:分享未公开的拼多多API长连接保活机制

第一章:抢菜插件Go语言版下载

抢菜插件Go语言版是一款轻量、高并发的自动化工具,专为应对生鲜平台(如京东到家、美团买菜、盒马等)限时上架商品设计。其核心优势在于原生协程支持、无依赖二进制分发、以及毫秒级HTTP请求调度能力,相比Python或Node.js版本显著降低内存占用与启动延迟。

获取源码与编译环境准备

确保系统已安装 Go 1.21+(推荐 1.22.x):

# 检查版本
go version
# 若未安装,前往 https://go.dev/dl/ 下载对应平台安装包

克隆官方仓库(注意:仅限学习与个人测试用途,禁止用于大规模刷单或违反平台《用户协议》的行为):

git clone https://github.com/gocook/vegetable-rush.git
cd vegetable-rush

编译生成可执行文件

项目采用模块化结构,主程序入口为 cmd/rusher/main.go

# 初始化模块并下载依赖(自动识别 go.mod)
go mod tidy
# 编译为当前系统可执行文件(Linux/macOS/Windows 通用)
go build -o rusher ./cmd/rusher
# 验证构建结果
./rusher --help

⚠️ 提示:若需交叉编译(如在 macOS 上生成 Windows 版),使用 GOOS=windows GOARCH=amd64 go build -o rusher.exe ./cmd/rusher

支持平台与配置说明

平台 是否内置支持 配置方式 备注
美团买菜 config/meituan.yaml 需手动填入 Cookie 和设备ID
京东到家 config/jdhome.yaml 支持扫码登录后自动提取Token
盒马鲜生 ⚠️ 实验性 config/hema.yaml 依赖 H5 接口,需配合抓包调试

首次运行前,请编辑对应 YAML 配置文件,填写 user_tokendevice_id 及目标商品 SKU 列表。所有敏感字段均不硬编码于源码中,保障基础安全性。

第二章:拼多多API长连接机制深度解析与Go实现

2.1 长连接心跳包协议逆向与TCP Keepalive参数调优实践

心跳协议逆向关键发现

通过 Wireshark 抓包与 tcpdump -s0 -w heartbeat.pcap port 8080 捕获客户端-服务端交互,识别出自定义二进制心跳帧:0x01 + uint32_t(seq) + uint64_t(timestamp_ms)

// 自定义心跳发送逻辑(服务端侧)
struct heartbeat_pkt {
    uint8_t  type;        // 0x01: heartbeat req, 0x02: ack
    uint32_t seq;         // 单调递增序列号,防重放
    uint64_t ts_ms;       // 精确到毫秒的时间戳,用于RTT计算
};

该结构体揭示协议依赖应用层序列号+时间戳实现双向活性检测,而非单纯依赖 TCP 层状态。

TCP Keepalive 内核参数对比调优

参数 默认值 推荐值 作用说明
net.ipv4.tcp_keepalive_time 7200s 600s 连接空闲后首次探测延迟
net.ipv4.tcp_keepalive_intvl 75s 30s 两次探测间隔
net.ipv4.tcp_keepalive_probes 9 3 失败后重试次数,避免误判断连

心跳协同机制流程

graph TD
    A[应用层心跳超时] -->|>30s无响应| B[标记连接异常]
    C[TCP Keepalive 触发] -->|内核探测失败| D[关闭 socket]
    B --> E[触发重连 + 上报监控]
    D --> E

2.2 WebSocket握手流程还原与Go net/http hijack实战封装

WebSocket 握手本质是 HTTP 协议的协议升级(Upgrade: websocket)过程,客户端发送含 Sec-WebSocket-Key 的请求,服务端需生成对应 Sec-WebSocket-Accept 响应头并返回 101 状态码。

握手关键字段对照表

字段 客户端发送 服务端计算逻辑
Sec-WebSocket-Key 随机 Base64 字符串(如 dGhlIHNhbXBsZSBub25jZQ== 原样接收
Sec-WebSocket-Accept base64(sha1(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))

Go 中 hijack 实现核心步骤

func handleWS(w http.ResponseWriter, r *http.Request) {
    // 1. 验证 Upgrade 请求头
    if r.Header.Get("Upgrade") != "websocket" {
        http.Error(w, "Upgrade required", http.StatusUpgradeRequired)
        return
    }
    // 2. Hijack 连接,获取底层 TCP Conn 和 bufio.Writer
    h, ok := w.(http.Hijacker)
    if !ok {
        http.Error(w, "Hijacking not supported", http.StatusInternalServerError)
        return
    }
    conn, bufrw, err := h.Hijack()
    if err != nil {
        log.Println("Hijack failed:", err)
        return
    }
    defer conn.Close()

    // 3. 手动写入 101 Switching Protocols 响应
    bufrw.WriteString("HTTP/1.1 101 Switching Protocols\r\n")
    bufrw.WriteString("Upgrade: websocket\r\n")
    bufrw.WriteString("Connection: Upgrade\r\n")
    key := r.Header.Get("Sec-WebSocket-Key")
    accept := computeAcceptKey(key) // 见下方函数
    bufrw.WriteString("Sec-WebSocket-Accept: " + accept + "\r\n\r\n")
    bufrw.Flush()

    // 此时 conn 已升级为 WebSocket 原始连接,可读写帧
}

computeAcceptKey 函数对客户端 key 拼接固定 GUID 后做 SHA-1 哈希再 Base64 编码,是 RFC 6455 强制要求的验证机制,确保服务端确实理解 WebSocket 协议。

握手状态流转(mermaid)

graph TD
    A[Client: GET /ws] --> B{Server: Check Upgrade header}
    B -->|Valid| C[Compute Sec-WebSocket-Accept]
    C --> D[Hijack & Write 101 Response]
    D --> E[Raw TCP Conn Ready for WS Frames]
    B -->|Invalid| F[Return 426/400]

2.3 TLS会话复用与证书固定(Certificate Pinning)在Go客户端的落地

会话复用:减少握手开销

Go 的 http.Transport 默认启用 TLS 会话复用(RFC 5077),通过 tls.Config.SessionTicketsDisabled = false(默认)和 ClientSessionCache 实现。

tr := &http.Transport{
    TLSClientConfig: &tls.Config{
        ClientSessionCache: tls.NewLRUClientSessionCache(128),
    },
}

NewLRUClientSessionCache(128) 缓存最多 128 个会话票据,复用时跳过完整握手,仅需 1-RTT;SessionTicket 在服务端支持且客户端未禁用时自动生效。

证书固定:防御中间人攻击

需手动校验证书指纹,绕过系统信任链:

func pinCert(resp *http.Response) error {
    if len(resp.TLS.PeerCertificates) == 0 {
        return errors.New("no peer certificate")
    }
    cert := resp.TLS.PeerCertificates[0]
    sum := sha256.Sum256(cert.Raw)
    expected := "a1b2c3..." // 预置 SHA256 指纹
    if fmt.Sprintf("%x", sum) != expected {
        return errors.New("certificate pinning failed")
    }
    return nil
}

resp.TLS.PeerCertificates[0] 是叶证书原始 ASN.1 数据,sha256.Sum256(cert.Raw) 生成强绑定指纹;必须在 http.Client.CheckRedirect 或响应处理中调用,不可依赖 InsecureSkipVerify

机制 启用方式 安全收益 风险点
会话复用 ClientSessionCache 降低延迟、CPU消耗 会话票据泄露可被重放
证书固定 手动哈希校验 PeerCertificates 抵御 CA 误签/劫持 证书轮换需同步更新指纹
graph TD
    A[发起 HTTPS 请求] --> B{TLS 握手}
    B --> C[检查 SessionTicket 缓存]
    C -->|命中| D[快速恢复会话]
    C -->|未命中| E[完整握手 + 缓存票据]
    D & E --> F[校验服务器证书指纹]
    F -->|匹配| G[接受连接]
    F -->|不匹配| H[终止连接]

2.4 请求签名算法(HMAC-SHA256+时间戳+随机Nonce)的Go安全实现

核心安全要素

  • 时间戳:限制请求有效期(如±5分钟),防御重放攻击
  • Nonce:服务端需缓存并校验唯一性,避免重复使用
  • 密钥隔离:签名密钥绝不硬编码,应通过os.Getenv("API_SECRET")或Secret Manager注入

签名生成逻辑

func SignRequest(method, uri, body string, secret string) string {
    timestamp := strconv.FormatInt(time.Now().Unix(), 10)
    nonce := uuid.New().String()[:16]
    message := fmt.Sprintf("%s\n%s\n%s\n%s\n%s", method, uri, timestamp, nonce, body)
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write([]byte(message))
    return base64.StdEncoding.EncodeToString(mac.Sum(nil))
}

逻辑说明:按约定顺序拼接methoduritimestampnoncebody(空体传""),确保服务端完全一致重构。nonce截取16位兼顾熵值与存储效率;base64编码保证签名可安全嵌入HTTP头。

服务端校验关键点

步骤 验证项 安全意义
1 abs(now - timestamp) ≤ 300 防时效性重放
2 nonce NOT IN seen_nonces 防一次性重放
3 HMAC比对恒定时间 防侧信道时序攻击
graph TD
    A[客户端构造签名] --> B[附加Header: X-Timestamp/X-Nonce/X-Signature]
    B --> C[服务端解析并校验时效性]
    C --> D[查重Nonce并存入Redis Set]
    D --> E[恒定时间HMAC比对]

2.5 连接异常检测与自动重连状态机(Finite State Machine)设计与编码

核心状态定义

连接生命周期抽象为五个原子状态:DisconnectedConnectingConnectedDisconnectingFailed。状态迁移受网络事件(如 onSocketErroronTimeout)和业务指令(如 forceReconnect())双重驱动。

状态迁移约束(关键规则)

  • Connected 可因心跳超时进入 Disconnecting
  • Failed 状态必须经指数退避后才允许跳转至 Connecting
  • 任意状态下收到 close() 调用,均强制进入 Disconnecting

Mermaid 状态流转图

graph TD
    A[Disconnected] -->|connect()| B[Connecting]
    B -->|success| C[Connected]
    B -->|fail| E[Failed]
    C -->|heartbeat timeout| D[Disconnecting]
    D -->|cleanup ok| A
    E -->|retry after backoff| B

状态机核心实现(TypeScript)

enum ConnectionState {
  Disconnected, Connecting, Connected, Disconnecting, Failed
}

class ReconnectFSM {
  private state: ConnectionState = ConnectionState.Disconnected;
  private retryCount = 0;
  private readonly maxRetries = 5;

  connect(): void {
    if (this.state === ConnectionState.Connected) return;
    if (this.state === ConnectionState.Failed && this.retryCount >= this.maxRetries) {
      throw new Error('Max reconnection attempts exceeded');
    }
    this.state = ConnectionState.Connecting;
    // 启动带超时的连接尝试...
  }
}

逻辑分析:connect() 方法首先校验前置状态合法性,避免重复触发;retryCount 在失败回调中递增,配合 maxRetries 实现熔断保护;状态变更不依赖外部副作用,保障 FSM 的确定性与可测试性。

第三章:高并发抢菜核心逻辑工程化

3.1 基于Go Channel与Worker Pool的毫秒级任务调度架构

为支撑高并发、低延迟的定时/触发类任务(如实时风控检查、会话心跳续期),我们构建了轻量级内存调度器,摒弃外部依赖,纯用 Go 原生并发原语实现。

核心设计原则

  • 时间轮+优先队列双层调度:短周期(
  • Worker Pool 动态伸缩:基于 runtime.NumCPU() 初始化,按队列积压量 ±20% 自适应扩缩
  • Channel 零拷贝传递:任务结构体指针入队,避免 GC 压力

调度流程(mermaid)

graph TD
    A[新任务注册] --> B{周期 < 100ms?}
    B -->|是| C[插入时间轮槽位]
    B -->|否| D[推入最小堆]
    C & D --> E[主调度协程轮询]
    E --> F[到期任务 → workerChan]
    F --> G[空闲Worker消费执行]

关键代码片段

// 任务结构体(必须可比较,用于去重)
type Task struct {
    ID       uint64
    ExecAt   int64 // UnixMilli 时间戳
    Fn       func()
    Priority uint8
}

// 工作池启动示例
func NewWorkerPool(size int) *WorkerPool {
    pool := &WorkerPool{
        workers: make(chan *worker, size),
        tasks:   make(chan *Task, 1024), // 缓冲通道降低阻塞
    }
    for i := 0; i < size; i++ {
        go pool.startWorker() // 启动固定数量worker
    }
    return pool
}

tasks 通道容量设为 1024,兼顾突发流量缓冲与内存可控性;workers 使用带缓冲通道实现 worker 状态管理(空闲/忙碌),避免锁竞争。ExecAt 使用毫秒级时间戳,直接参与堆排序与时间轮索引计算,消除 time.Time 对象分配开销。

3.2 商品库存原子性校验与Redis Lua脚本协同方案

在高并发秒杀场景中,仅靠数据库乐观锁易引发大量回滚与连接竞争。Redis + Lua 提供服务端原子执行能力,规避网络往返与竞态。

核心Lua脚本实现

-- KEYS[1]: 库存key;ARGV[1]: 扣减数量;ARGV[2]: 当前业务ID(防超卖日志溯源)
if tonumber(redis.call('GET', KEYS[1])) >= tonumber(ARGV[1]) then
  redis.call('DECRBY', KEYS[1], ARGV[1])
  return 1  -- 成功
else
  return 0  -- 库存不足
end

逻辑分析:脚本在Redis单线程内完成“读-判-改”,无中间状态暴露;KEYS[1]确保操作聚焦单一商品键,ARGV[1]为安全整型参数,避免注入风险。

执行保障机制

  • ✅ 预热:商品上架时用SETNX初始化库存键
  • ✅ 回源:Lua返回0后触发DB最终一致性校验
  • ❌ 禁止:在脚本中调用redis.call('HGETALL')等非O(1)命令
组件 职责 原子性边界
Redis 库存快照与扣减 单key、单脚本内
Lua脚本 条件判断+原子操作 全局串行执行
应用层 日志记录与降级响应 无原子性保证

3.3 Go Context超时控制与Cancel传播在多层RPC调用中的精准应用

在微服务链路中,Context 的 DeadlineCancel 信号需穿透 HTTP/gRPC/DB 多层调用,避免 goroutine 泄漏与雪崩。

超时传递的链式封装

func callService(ctx context.Context, url string) error {
    // 派生带超时的子context,继承父级cancel信号
    childCtx, cancel := context.WithTimeout(ctx, 800*time.Millisecond)
    defer cancel() // 确保资源释放

    req, _ := http.NewRequestWithContext(childCtx, "GET", url, nil)
    resp, err := http.DefaultClient.Do(req)
    if err != nil && errors.Is(err, context.DeadlineExceeded) {
        log.Printf("upstream timeout at %s", url)
    }
    return err
}

WithTimeout 在父 Context 取消或超时时自动触发 cancel;defer cancel() 防止子 Context 泄漏;errors.Is(err, context.DeadlineExceeded) 是唯一可靠的超时判断方式。

Cancel 信号穿透关键路径

  • gRPC 客户端:ctx 直接传入 Invoke(),服务端通过 grpc.Peer(ctx) 捕获中断
  • 数据库查询:db.QueryContext(ctx, ...) 支持中断长事务
  • 中间件:所有中间件必须接收并透传 ctx,不可新建无关联 Context
组件 是否支持 Cancel 超时是否可继承 备注
net/http Do() 自动响应 Deadline
database/sql 需驱动支持(如 pgx/v5)
Redis (redis-go) WithContext() 显式调用

多跳 RPC 的 Cancel 传播图

graph TD
    A[Client: WithTimeout 1s] --> B[Service A: WithTimeout 800ms]
    B --> C[Service B: WithTimeout 500ms]
    C --> D[DB: QueryContext]
    A -.->|Cancel signal| B
    B -.->|Propagates| C
    C -.->|Propagates| D

第四章:插件部署、可观测性与反风控对抗

4.1 跨平台二进制打包(Linux/Windows/macOS)与UPX压缩优化

跨平台打包需统一构建流程,推荐使用 pyinstaller 配合平台专用 spec 配置:

# 通用打包命令(各平台分别执行)
pyinstaller --onefile --windowed \
  --add-data "assets;assets" \
  --upx-exclude=__init__.py \
  main.py

--upx-exclude 防止 UPX 错误压缩 Python 初始化模块;--add-data 保证资源路径跨平台兼容(分号在 Windows/macOS/Linux 中被 pyinstaller 自动适配)。

UPX 压缩效果对比(典型 Python CLI 工具):

平台 原始大小 UPX 后大小 压缩率
Linux 18.2 MB 6.4 MB 65%
Windows 19.1 MB 6.7 MB 65%
macOS 18.8 MB 6.5 MB 65%

构建自动化关键点

  • 使用 GitHub Actions 矩阵策略并行触发三平台构建
  • 通过 --upx-exclude 排除 .so/.dll 及含 .pyc 的目录,避免运行时解压失败
graph TD
  A[源码] --> B[PyInstaller 打包]
  B --> C{平台判断}
  C --> D[Linux: .AppImage/.bin]
  C --> E[Windows: .exe]
  C --> F[macOS: .app]
  D --> G[UPX 压缩]
  E --> G
  F --> G

4.2 Prometheus指标埋点与Gin中间件集成实现QPS/延迟/失败率实时监控

核心指标定义与Prometheus注册

Prometheus要求指标在进程启动时完成注册。需预先声明三类核心指标:

  • http_requests_total:Counter,按methodstatuspath标签计数请求总量
  • http_request_duration_seconds:Histogram,观测延迟分布(bucket默认0.01~10s)
  • http_request_errors_total:Counter,专用于5xx/4xx错误计数

Gin中间件实现

func PrometheusMiddleware() gin.HandlerFunc {
    reqCounter := promauto.NewCounterVec(
        prometheus.CounterOpts{
            Name: "http_requests_total",
            Help: "Total HTTP requests.",
        },
        []string{"method", "status", "path"},
    )
    reqDuration := promauto.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "http_request_duration_seconds",
            Help:    "HTTP request duration in seconds.",
            Buckets: prometheus.DefBuckets, // [0.005, 0.01, ..., 10]
        },
        []string{"method", "path"},
    )

    return func(c *gin.Context) {
        start := time.Now()
        c.Next()

        status := strconv.Itoa(c.Writer.Status())
        reqCounter.WithLabelValues(c.Request.Method, status, c.FullPath()).Inc()
        reqDuration.WithLabelValues(c.Request.Method, c.FullPath()).Observe(time.Since(start).Seconds())

        if c.Writer.Status() >= 400 {
            promauto.NewCounter(prometheus.CounterOpts{
                Name: "http_request_errors_total",
                Help: "Total HTTP errors.",
            }).Inc()
        }
    }
}

逻辑分析:该中间件在请求进入时记录起始时间,c.Next()执行路由处理后,统一采集状态码、耗时并打标。FullPath()确保路径聚合一致性(如 /api/v1/users/:id/api/v1/users/:id),避免因动态参数导致指标爆炸。promauto自动注册指标至默认Registry,无需手动调用prometheus.MustRegister()

指标暴露与验证

指标名 类型 关键标签
http_requests_total Counter method, status, path
http_request_duration_seconds_bucket Histogram method, path, le

数据采集流程

graph TD
    A[HTTP Request] --> B[Gin Router]
    B --> C[Prometheus Middleware]
    C --> D[Record start time]
    D --> E[Execute handler]
    E --> F[Observe duration & inc counters]
    F --> G[Return response]
    G --> H[Scrape by Prometheus]

4.3 浏览器指纹模拟策略及Go驱动的Headless Chrome轻量级集成

浏览器指纹模拟需覆盖 User-Agentscreen.availHeightnavigator.hardwareConcurrency 等12+核心维度,避免被反爬系统识别为自动化环境。

指纹可控性关键参数

  • --user-agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
  • --window-size=1920,1080
  • --disable-blink-features=AutomationControlled

Go 启动 Headless Chrome 示例

cmd := exec.Command("chrome", 
    "--headless=new",
    "--no-sandbox",
    "--disable-gpu",
    "--remote-debugging-port=9222",
    "--user-agent="+ua)
err := cmd.Start() // 非阻塞启动,便于后续 WebSocket 连接

--headless=new 启用新版无头模式,兼容 Puppeteer/Cdp 协议;--remote-debugging-port 暴露 CDP 接口供 Go 的 chromedp 库控制;--no-sandbox 在容器中必需(生产环境应配 --userns 隔离)。

维度 常见值 可变性
devicePixelRatio 1.25 / 2.0
platform Win32
webdriver false(需 JS 注入覆盖)
graph TD
    A[Go 程序] --> B[启动 Chrome 实例]
    B --> C[注入指纹覆盖脚本]
    C --> D[执行目标页面加载]
    D --> E[提取 DOM/CDP 数据]

4.4 设备ID动态生成与Token轮换机制的Go标准库实现(crypto/rand + time.Now().UnixNano())

核心设计原则

设备ID需满足唯一性、不可预测性、时效性;Token须支持自动轮换,避免长期静态凭证暴露风险。

安全随机数生成

func generateDeviceID() string {
    b := make([]byte, 16)
    if _, err := rand.Read(b); err != nil {
        panic(err) // 生产环境应返回error而非panic
    }
    return fmt.Sprintf("%x-%d", b, time.Now().UnixNano())
}

crypto/rand.Read 提供密码学安全的随机字节(非 math/rand);UnixNano() 提供纳秒级时间戳,增强熵值。拼接后ID长度约48字符,冲突概率低于 2⁻¹²⁸。

Token轮换策略

轮换触发条件 频率 安全收益
启动时 1次 防止初始ID复用
每30分钟 定时 限制泄露凭证有效期
网络重连 事件驱动 应对设备迁移或会话劫持

轮换流程

graph TD
    A[生成新DeviceID] --> B[派生HMAC-SHA256 Token]
    B --> C[写入内存Token缓存]
    C --> D[更新HTTP Authorization Header]

第五章:开源协议声明与使用须知

常见开源协议核心差异对比

协议类型 允许商用 修改后必须开源 允许私有分发 传染性范围 典型项目示例
MIT React、Vue
Apache-2.0 ❌(但需保留NOTICE) Kubernetes、Kafka
GPL-3.0 ✅(衍生作品) ✅(但含源码) 强传染性(含动态链接) Linux内核、GIMP
AGPL-3.0 ✅(含SaaS部署) 最强传染性(网络服务即分发) Nextcloud、Mastodon

实际项目合规踩坑案例

某金融科技公司基于Apache-2.0许可的Log4j 2.17.1构建风控日志系统,但在生产环境误用未打补丁的2.15.0版本。虽协议未禁止旧版使用,但因未履行Apache-2.0第4条“明确标注修改内容”义务,且未在NOTICE文件中声明所用组件版本,在监管审计中被认定为开源治理缺失。后续整改要求:所有Java服务JAR包内嵌NOTICE文本文件,包含Log4j组件名称、版本、原始许可证URL及公司定制化说明。

商业产品集成GPL组件的风险路径

flowchart LR
    A[商业SaaS平台] --> B{集成GPL-3.0库libpq.so<br>(PostgreSQL客户端)}
    B --> C[静态链接]
    B --> D[动态链接]
    C --> E[触发传染性:<br>整个二进制必须开源]
    D --> F[法院判例倾向不传染<br>但需提供libpq.so源码]
    F --> G[实际操作:<br>将libpq.so单独打包为Docker volume<br>在启动脚本中LD_LIBRARY_PATH指定]

企业级合规检查清单

  • 每个微服务Docker镜像构建时执行syft <image>生成SBOM(软件物料清单)
  • CI/CD流水线强制调用license-checker --failOnLicense GPL-2.0拦截高风险协议依赖
  • 前端项目package.json"license"字段必须与LICENSE文件内容完全一致,禁止使用"SEE LICENSE IN LICENSE"模糊声明
  • 使用reuse lint工具验证每个源码文件头部是否含SPDX标识:SPDX-License-Identifier: MIT
  • 对于AGPL-3.0许可的Redis模块,必须在用户界面显著位置提供源码下载入口(如/source-code路由),且下载包需包含完整构建脚本

开源协议升级引发的连锁反应

2023年Elasticsearch将默认协议从Apache-2.0变更为SSPL(Server Side Public License),导致某云厂商自研ES兼容层被迫重构:原基于Apache-2.0的Query DSL解析器可直接复用,但新版本必须剥离所有SSPL代码路径,改用OpenSearch SDK对接。技术团队耗时8人月完成协议适配,包括重写索引映射转换器、重构慢查询日志采集模块,并通过diff -r es-v7.10.2 opensearch-2.9.0逐行比对API兼容性。所有变更均提交至内部GitLab仓库,commit message严格遵循[LICENSE] Replace ES client with OpenSearch SDK规范。

专注后端开发日常,从 API 设计到性能调优,样样精通。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注