我完成了一个企业级 API 开放平台 —— 从零构建 IntelliHub 的全记录

0 阅读18分钟

项目全景:7 个阶段的渐进式演进

在开始之前,先用一张图看清整个项目的成长路径。我没有试图一次性把所有功能做出来——那样只会得到一个四不像的产物。而是像一个真实的产品一样,一个阶段解决一个问题,每一个阶段都在上一个阶段的基础上生长

graph TD
    V1[&#34;阶段一<br/>项目架构与设计<br/>2025.10 上旬&#34;]
    V2[&#34;阶段二<br/>用户登录 + 多租户 + 系统初始化<br/>2025.10 中旬&#34;]
    V3[&#34;阶段三<br/>应用中心<br/>2025.10 下旬&#34;]
    V4[&#34;阶段四<br/>统一网关<br/>2025.10 下旬 - 2025.11 中旬&#34;]
    V5[&#34;阶段五<br/>调用统计<br/>2025.11 中旬 - 2025.11 下旬&#34;]
    V6[&#34;阶段六<br/>智能告警<br/>2025.11 下旬 - 2025.12 上旬&#34;]
    V7[&#34;阶段七<br/>事件驱动 + SDK + 前端生态<br/>2025.12 上旬 - 2025.12 中旬&#34;]

    V1 --> V2 --> V3 --> V4 --> V5 --> V6 --> V7

为什么是这个顺序?因为我遵循了一个原则:先地基、再骨架、后血肉、最后皮肤

  • 阶段一先把架构想清楚,避免后面推倒重来
  • 阶段二/三把”人”和”应用”的身份体系建好——这是所有安全机制的前提
  • 阶段四的网关是整个平台的心脏——只有身份体系就位了,网关才知道”谁在请求”
  • 阶段五/六让平台有了”眼睛”和”警报器”
  • 阶段七是生态完善——SDK 降低接入门槛,事件驱动让架构更灵活

下面我逐一展开每个阶段做了什么、为什么这么做、踩了什么坑。


阶段一:项目架构与设计

1.1 从”画图”开始

动手写代码之前,我花了整整两周时间画架构图、写设计文档。

为什么要花这么多时间?因为我知道,API 开放平台是一个典型的”地基决定上层建筑”的项目。如果一开始对模块边界、通信方式、数据流向想不清楚,后面每加一个新功能都可能是灾难。

我给自己定了三个核心设计原则:

  1. 统一入口:所有流量——不管是管理后台的还是开放 API 的——全部经过网关,在入口层统一做认证、鉴权、限流
  2. 按领域拆分:每个微服务负责一个完整的业务领域,有自己的数据库,服务间用 RPC 通信
  3. 异步解耦:日志、统计、事件通知等非核心链路全部异步化,不拖慢主流程

1.2 技术选型的反复推敲

技术选型是这个阶段最头疼的事。每一个选择都有 trade-off,没有完美的方案,只有最合适的取舍。

层次选择备选为什么选这个
网关Spring Cloud Gateway (WebFlux)Zuul / Kong / APISIXWebFlux 非阻塞模型,Spring 生态无缝集成
RPCDubbo 3.xOpenFeign / gRPCDubbo 泛化调用是网关”零依赖路由”的关键
消息队列KafkaRabbitMQ / RocketMQ日志场景天然适合 Kafka 的高吞吐 + 持久化
缓存Redis限流、防重放、实时统计都需要原子操作
数据库MySQL 8.0 + MyBatis PlusMyBatis Plus 的租户插件是实现多租户隔离的核心
注册中心Nacos 2.xEureka / Consul同时提供注册发现和配置中心,一个组件解决两个问题
搜索引擎Elasticsearch + IK 分词器中文分词 + 跨索引聚合搜索
前端Vue 3 + TypeScript + Element PlusReactVue 3 Composition API 开发效率高,Element Plus 企业后台组件丰富

1.3 模块规划:9 大模块的职责边界

最终确定了 9 个模块,每个都有明确的职责边界:

graph TB
    subgraph &#34;基础设施&#34;
        COMMON[&#34;inner-intergration<br/>公共组件、工具类、<br/>Dubbo 接口、签名工具&#34;]
    end

    subgraph &#34;核心服务&#34;
        IAM[&#34;intelli-auth-iam-service<br/>IAM 认证服务<br/>:8081&#34;]
        API[&#34;intelli-api-platform-service<br/>API 平台服务<br/>:8082&#34;]
        APP[&#34;intelli-app-center-service<br/>应用中心服务<br/>:8085&#34;]
        GW[&#34;intelli-gateway-service<br/>统一网关<br/>:8080&#34;]
        GOV[&#34;intelli-governance-service<br/>治理中心<br/>:8083&#34;]
    end

    subgraph &#34;扩展服务&#34;
        SEARCH[&#34;intelli-search-service<br/>聚合搜索<br/>:8086&#34;]
        EVENT[&#34;intelli-event-service<br/>事件中心<br/>:8087&#34;]
    end

    subgraph &#34;生态&#34;
        SDK[&#34;intelli-sdk<br/>Java SDK&#34;]
        FE[&#34;intellihub-frontend<br/>Vue 3 前端控制台&#34;]
    end

    COMMON -.->|依赖| IAM
    COMMON -.->|依赖| API
    COMMON -.->|依赖| APP
    COMMON -.->|依赖| GOV

    GW -->|Dubbo| IAM
    GW -->|Dubbo| API
    GW -->|Dubbo| APP

为什么这样划分? 因为每个模块对应平台的一个独立领域——认证与身份、API 管理、应用接入、流量治理、监控告警。领域之间通过 Dubbo 接口建立契约,互不侵入对方的数据库。


阶段二:用户登录 + 多租户 + 系统初始化

2.1 为什么从”人”开始?

很多做 API 平台的人上来就写网关,我当时也想这么做。但冷静下来想了一个问题:网关做鉴权,鉴权需要一个身份体系。如果用户系统、租户系统都没建好,网关该认证谁?

所以我决定先从 IAM(身份认证与权限管理)做起——把”人”的身份问题解决干净。

2.2 用户认证体系

IAM 服务的核心功能:

  • 用户注册与登录:用户名 + 密码 + 验证码登录
  • JWT Token 签发:HS512 算法,Access Token 2 小时有效期
  • BCrypt 密码加密:数据库中存储哈希值,不可逆
  • 验证码机制:基于 Redis 的图形验证码,60 秒过期
sequenceDiagram
    participant U as 用户
    participant FE as 前端
    participant GW as 网关
    participant IAM as IAM 服务
    participant Redis as Redis
    participant MySQL as MySQL

    U->>FE: 输入用户名密码
    FE->>GW: POST /api/iam/v1/auth/login
    GW->>IAM: 转发登录请求
    IAM->>MySQL: 查询用户
    MySQL-->>IAM: 用户信息
    IAM->>IAM: BCrypt 密码验证
    IAM->>IAM: 签发 JWT Token (HS512)
    IAM-->>GW: 返回 Token + 用户信息
    GW-->>FE: 返回
    FE->>FE: 存入 localStorage

2.3 多租户体系的初始化

多租户是这个平台最核心的非功能特性。在第二阶段,我先建好了租户的基础设施:

  • 租户 CRUD:创建、启停、查询、配置
  • 默认租户机制:系统初始化时自动创建默认租户和超级管理员
  • 租户配额管理:每个租户可以配置最大用户数、最大应用数、最大 API 数

SQL 初始化脚本承担了”系统自举”的重任——数据库建好之后,执行 init.sql 就能得到一个可用的平台:

-- 创建默认租户
INSERT INTO iam_tenant (id, name, code, status, ...) 
VALUES ('default', '默认租户', 'default', 'active', ...);

-- 创建超级管理员
INSERT INTO iam_user (id, username, password, tenant_id, ...) 
VALUES ('admin-001', 'admin', '$2a$10$...', 'default', ...);

-- 创建默认角色
INSERT INTO iam_role (id, name, code, tenant_id, ...) 
VALUES ('role-admin', '超级管理员', 'SUPER_ADMIN', 'default', ...);

2.4 RBAC 权限体系

设计了三级角色体系:

超级管理员(跨租户)
  └── 租户管理员(本租户内全权限)
        └── 普通用户(有限权限)
        └── API 开发者(API 管理权限)
        └── 只读用户(仅查看权限)

角色和权限的关系存在 iam_role_permission 关联表中,用户可以绑定多个角色,权限取并集。这种设计让权限配置非常灵活——不需要改代码,只需要在数据库里加一条关联记录。

2.5 踩坑:JWT 密钥长度报错

这个阶段遇到的第一个坑是 JWT 密钥长度。IAM 服务启动后,登录接口直接报错:

The signing key's size is 272 bits which is not secure enough 
for the HS512 algorithm. The JWT JWA Specification states that 
keys used with HS512 MUST have a size >= 512 bits

HS512 要求密钥至少 512 位(64 字节),而我最初配置的密钥只有 34 字节。解决方法是把密钥加长到 64 字节以上。但这个教训让我意识到:密码学参数的约束是硬性的,不能用”差不多就行”的心态对待。


阶段三:应用中心

3.1 为什么需要”应用”这个抽象?

用户系统建好之后,下一个问题是:谁来调用 API?

在真实的 API 开放平台里,调用方不是”人”,而是”应用”——一个合作伙伴可能维护着多个应用系统,每个系统需要独立的身份凭证。所以需要”应用”这个抽象来代表一个调用方实体。

3.2 应用中心的核心能力

应用中心服务(intelli-app-center-service)负责管理调用方的一切:

  • 应用 CRUD:创建、编辑、删除、启停
  • AppKey/AppSecret 管理:安全随机生成、重置、启用/禁用
  • API 订阅授权:应用与 API 的多对多订阅关系
  • 配额管理:每个应用可以配置每日调用上限
erDiagram
    APPLICATION ||--o{ APP_API_SUBSCRIPTION : &#34;订阅&#34;
    APP_API_SUBSCRIPTION }o--|| API_INFO : &#34;被订阅的API&#34;
    
    APPLICATION {
        string id PK
        string tenant_id
        string name &#34;应用名称&#34;
        string app_key &#34;32位安全随机&#34;
        string app_secret &#34;64位安全随机&#34;
        string status &#34;active/disabled&#34;
        int quota_limit &#34;每日配额上限&#34;
    }
    
    APP_API_SUBSCRIPTION {
        string id PK
        string app_id FK
        string api_id FK
        string status &#34;active/cancelled&#34;
    }

3.3 AppKey/Secret 生成的安全性

AppKey 和 AppSecret 的生成不是简单的 UUID。我用了 SecureRandom 生成:

// AppKey: 32 位安全随机字符串(字母+数字)
// AppSecret: 64 位安全随机字符串(字母+数字+特殊字符)
public static String generateAppKey() {
    SecureRandom random = new SecureRandom();
    byte[] bytes = new byte[24];
    random.nextBytes(bytes);
    return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
}

AppSecret 只在创建时展示一次,之后数据库中存储的是哈希值——和密码处理逻辑一致。这样即使数据库被拖库,也无法还原出 AppSecret。

3.4 订阅关系:谁可以调什么 API

订阅关系是应用中心最核心的业务逻辑。一个应用必须先”订阅”某个 API,才能通过网关调用它。这个校验发生在网关的认证阶段——网关通过 Dubbo 调用应用中心的 checkSubscriptionByApiId(appId, apiId) 接口。

为什么把这个校验放在网关而不是 API 平台?因为安全策略应该在流量入口执行,不能依赖下游服务的自觉


阶段四:统一网关

4.1 网关是整个平台的心脏

身份体系就位了(阶段二),调用方身份也有了(阶段三),现在终于可以开始做网关——把认证、鉴权、限流、路由全部在入口层统一处理。

网关是 IntelliHub 最核心、也是我投入精力最多的模块。它承担了以下几个关键职责:

graph TB
    REQ[请求进入网关]

    REQ --> CACHE[BodyCacheFilter<br/>缓存请求体<br/>解决 WebFlux Body 只能读一次的问题]

    CACHE --> MATCH[OpenApiRouteMatchFilter<br/>API 路由匹配<br/>打标 isOpenApi]

    MATCH --> RATE[RateLimitFilter<br/>滑动窗口限流<br/>IP / Path / IP+Path 三维度]

    RATE --> TENANT[TenantValidationFilter<br/>租户校验与补全]

    TENANT --> JWT[JwtAuthenticationFilter<br/>JWT 本地验签<br/>管理后台流量]

    TENANT --> APPKEY[AppKeyAuthenticationFilter<br/>HMAC-SHA256 签名验证<br/>防重放 + 订阅校验<br/>开放 API 流量]

    JWT --> ROUTE[OpenApiRouteFilter<br/>动态路由转发<br/>HTTP / Dubbo 泛化 / Mock]

    APPKEY --> ROUTE

    ROUTE --> LOG[AccessLogFilter<br/>异步日志上报<br/>Kafka + Redis 双通道]

    LOG --> BACKEND[后端服务]

    style GW fill:#4A90E2,color:#fff

4.2 动态路由:网关的”大脑”

网关最核心的能力是动态路由——根据平台配置决定把请求转发到哪个后端。这不是静态的 Nginx 反向代理,而是根据 API 平台的配置动态变化的。

我设计了一个三级缓存路由策略:

sequenceDiagram
    participant GW as 网关路由匹配
    participant L1 as Caffeine 本地缓存<br/>1分钟 TTL
    participant L2 as Redis 缓存<br/>5分钟 TTL
    participant L3 as API 平台<br/>Dubbo 调用

    GW->>L1: 查询路由:/open/api/user/info
    alt 本地命中(95%+ 的情况)
        L1-->>GW: ApiRouteDTO (< 1ms)
    else 本地未命中
        GW->>L2: 查询 Redis
        alt Redis 命中
            L2-->>GW: ApiRouteDTO (< 5ms)
            Note over GW: 回写本地缓存
        else Redis 未命中
            GW->>L3: Dubbo matchRouteByPath
            L3-->>GW: ApiRouteDTO (< 20ms)
            Note over GW: 回写 Redis + 本地
        end
    end

三级缓存的精髓在于”越热越近”:95% 的请求直接走 Caffeine 本地内存,耗时不到 1ms。只有冷启动或路由变更后的第一次请求才会穿透到远端。

4.3 路由热更新:Redis Pub/Sub

API 发布或下线后,网关怎么知道路由变了?答案是用 Redis Pub/Sub 做配置热更新:

sequenceDiagram
    participant Admin as 管理员
    participant API as API 平台
    participant Redis as Redis Pub/Sub
    participant GW1 as 网关节点 1
    participant GW2 as 网关节点 2

    Admin->>API: 发布 API
    API->>Redis: PUBLISH api:route:change
    Redis-->>GW1: SUBSCRIBE 收到通知
    Redis-->>GW2: SUBSCRIBE 收到通知
    GW1->>GW1: 清空本地 + Redis 路由缓存
    GW2->>GW2: 清空本地 + Redis 路由缓存
    Note over GW1,GW2: 下次请求自动从 API 平台重新加载

配置变更秒级生效,无需重启网关,对在线流量无感知。

4.4 双流量认证:一个网关,两种身份

管理后台的请求和开放 API 的请求走的是完全不同的认证路径:

管理后台流量(JWT 认证)

  • 路径前缀:/api/**
  • 认证方式:JWT 本地验签(HS512 对称加密,网关和 IAM 共享密钥)
  • 性能:本地解析 < 1ms

开放 API 流量(AppKey + HMAC-SHA256 签名)

  • 路径前缀:/open/**
  • 认证方式:AppKey 签名验证 + Nonce 防重放 + 订阅校验
  • 签名算法:Base64(HMAC-SHA256(METHOD + "\n" + PATH + "\n" + TIMESTAMP + "\n" + NONCE, AppSecret))
graph TB
    CLIENT[请求进入网关] --> PATH{路径判断}

    PATH -->|/api/**| JWT_FLOW[&#34;JWT 认证链路<br/>────────<br/>① JWT 本地验签<br/>② 注入 X-User-Id<br/>③ 注入 X-Tenant-Id<br/>④ 静态路由转发&#34;]

    PATH -->|/open/**| APPKEY_FLOW[&#34;AppKey 认证链路<br/>────────<br/>① 动态路由匹配<br/>② Nonce 防重放<br/>③ HMAC-SHA256 验签<br/>④ 订阅关系校验<br/>⑤ IP 白名单检查<br/>⑥ 配额检查<br/>⑦ 动态路由转发&#34;]

4.5 滑动窗口限流

限流算法我一开始用了最简单的固定窗口,但很快就发现了”边界突刺”问题。

举个例子:假设限制每分钟 100 次。用户在 12:00:59 发了 100 个请求,然后在 12:01:00 又发了 100 个——这两波实际间隔只有 1 秒,但因为落在不同的固定窗口里,都”合法”了。

换成滑动窗口后,情况完全不同:

graph LR
    REQ[请求到达] --> REMOVE[&#34;① 移除过期记录<br/>ZREMRANGEBYSCORE key 0 now-window&#34;]
    REMOVE --> COUNT[&#34;② 统计窗口内请求数<br/>ZCARD key&#34;]
    COUNT --> JUDGE{&#34;③ 超过阈值?&#34;}
    JUDGE -->|否| ALLOW[&#34;④ 放行<br/>ZADD key now now&#34;]
    JUDGE -->|是| REJECT[&#34;④ 拒绝<br/>HTTP 429&#34;]

基于 Redis Sorted Set 实现,窗口定义是”当前时间往前推 N 秒”,而非”第 N 个自然分钟”。支持的限流维度:

维度Redis Key说明
IPrate_limit:ip:192.168.1.1同 IP 所有接口
Pathrate_limit:path:/api/login同接口所有 IP
IP+Pathrate_limit:combined:192.168.1.1:/api/login精确组合

4.6 Dubbo 泛化调用:网关零依赖路由

这是网关设计中最让我自豪的一点。网关需要把请求转发到后端服务,但网关不能依赖任何业务 JAR 包——否则每新增一个 API 就要重新部署网关。

Dubbo 泛化调用完美解决了这个问题:

// 网关不需要 import com.example.OrderService
// 只需要接口名 + 方法名 + 参数类型元数据
GenericService genericService = GenericService.class.cast(
    referenceConfig.get()
);
Object result = genericService.$invoke(
    "queryOrder",                              // 方法名
    new String[]{"java.lang.String"},          // 参数类型
    new Object[]{orderId}                      // 参数值
);

这意味着什么?API 平台配置一个新的 Dubbo API → 网关收到路由配置 → 直接泛化调用,全程不需要改一行网关代码。 网关部署包大小也因此减少了 80%。

4.7 踩坑:白名单路径匹配的 StripPrefix 陷阱

前端调用获取验证码接口 /api/iam/v1/auth/captcha 返回 401,但这个接口明明配置在白名单里。

排查了半天发现:网关做了 StripPrefix=1(去掉 /api 前缀),路径实际变成了 /iam/v1/auth/captcha。白名单里只配置了带 /api 前缀的路径,去前缀之后匹配不上了。

教训:网关的路径重写规则会改变实际的匹配路径。配置白名单时要考虑”网关看到的是什么路径”,而不是”前端请求的是什么路径”。


阶段五:调用统计

5.1 目标:让每一次调用都被看见

网关跑通之后,下一个问题是:我们能看到什么?

没有统计之前,API 调用就像一个黑盒——你知道请求进来了、出去了,但说不清”谁调了多少次”、”成功率多少”、”哪个 API 最慢”。我需要的不是一个简单的计数器,而是一套完整的可观测体系。

5.2 异步日志上报:不能让监控拖慢业务

这是整个项目中最让我满意的一个设计。网关的职责是快速转发请求,如果在主链路上同步写数据库,每个请求都会多几十毫秒的延迟。

我的方案是完全异步的双通道上报

sequenceDiagram
    participant C as 客户端
    participant GW as 网关 Filter 链
    participant B as 后端服务
    participant K as Kafka
    participant R as Redis

    C->>GW: 发起 API 请求
    GW->>GW: 认证 → 鉴权 → 限流 → 路由
    GW->>B: 转发请求
    B-->>GW: 返回响应
    GW-->>C: 返回结果 ✅

    Note over GW,R: ⬆ 同步路径结束,响应已返回 ⬇ 异步路径开始

    GW--)K: Mono.fromRunnable → Kafka 发送调用日志
    GW--)R: Mono.fromRunnable → Redis 更新秒级统计

这里的关键是 WebFlux 的 Mono.fromRunnable()——日志上报在完全独立的线程中异步执行,主线程不等待。客户端收到响应的速度只取决于认证+转发的时间。

5.3 双通道:Kafka 负责”稳”,Redis 负责”快”

我用了两种存储通道,各有分工:

通道职责特点
Kafka持久化调用日志、历史数据聚合削峰填谷、消息不丢、支持重复消费
Redis秒级 QPS 统计、实时告警检测原子操作、亚毫秒延迟、滑动窗口统计

为什么需要两个通道?因为单一通道满足不了不同场景的需求:

  • Kafka 适合大规模历史数据:网关每天产生千万级日志,Kafka 的分区模型天然适配
  • Redis 适合实时统计:告警需要秒级感知,Kafka 消费延迟(即使只有几秒)会让告警滞后

5.4 多维度统计聚合

治理中心从 Kafka 消费日志后,按多个维度聚合统计:

全局概览   → 总调用量、成功率、P99 延迟
API 维度   → Top10 API、慢 API 排行、错误分布
租户维度   → 各租户调用量、配额使用率
应用维度   → 各应用调用量、错误率
时间维度   → 小时/天/月聚合、趋势分析

统计结果同时提供给前端 ECharts 可视化大盘——实时 QPS 折线图、成功率饼图、Top API 柱状图、延迟分布直方图。

5.5 踩坑:Kafka 消费延迟

高峰期发现统计数据延迟 10 分钟以上。排查发现 Kafka 消费者只有 2 个线程,高峰期根本消费不过来,消息大量积压。

解决策略:

  1. 增加消费者线程数:2 → 8
  2. 补充 Redis 实时统计通道(就是前面说的”双通道”)
  3. Kafka 走批量消费模式,一次拉取 500 条消息批量入库

最终统计延迟从 10 分钟降到 10 秒以内。


阶段六:智能告警

6.1 从”被动查”到”主动报”

统计上线后,我们能看到数据了。但总不能让人 24 小时盯着大盘——告警系统的作用就是从”人找问题”变成”问题找人”。

6.2 告警规则引擎

设计了一个灵活的告警规则引擎,支持三种告警类型:

告警类型触发条件可配置参数
错误率告警错误率超过阈值阈值、统计窗口(1/5/15分钟)
延迟告警平均延迟超过阈值阈值、统计窗口
QPS 告警QPS 突增或突降阈值、统计窗口

每条告警规则可以绑定到特定的 API、特定的应用、或全平台范围。

6.3 告警抑制:避免告警风暴

一个服务挂了,可能会触发几十条告警——如果不做抑制,运维人员会被消息淹死。

我的设计是:同一条告警规则在 5 分钟内只触发一次。用 Redis 记录最近一次触发时间,如果上次触发在 5 分钟以内,新的触发会被抑制。

sequenceDiagram
    participant JOB as 告警检测任务
    participant RULE as 告警规则
    participant REDIS as Redis
    participant NOTIFY as 通知服务

    JOB->>RULE: 检查错误率 15% > 阈值 10%
    RULE->>REDIS: 查询上次触发时间
    REDIS-->>RULE: 2025-11-28 14:30:00
    RULE->>RULE: 当前 14:32:00,间隔 2 分钟 < 5 分钟
    Note over RULE: 抑制:不发送通知
    RULE->>REDIS: 更新触发记录

    Note over JOB,NOTIFY: 5 分钟后...

    JOB->>RULE: 再次检查,仍超阈值
    RULE->>REDIS: 查询上次触发时间
    REDIS-->>RULE: 2025-11-28 14:32:00
    RULE->>RULE: 当前 14:37:00,间隔 5 分钟 >= 5 分钟
    RULE->>NOTIFY: 发送告警通知
    RULE->>REDIS: 更新触发记录

6.4 通知渠道

支持三种通知方式——钉钉机器人、邮件、Webhook。通知服务框架已就位,具体的消息发送逻辑按渠道分别实现。


阶段七:事件驱动 + SDK + 前端生态

7.1 事件驱动:让系统学会”通知”

前六个阶段解决的是”API 怎么被管理”和”API 怎么被调用”。但一个企业级平台还需要让各个模块能互相感知关键事件——API 发布了、应用审批通过了、告警触发了……这些事件应该能被订阅和消费。

事件中心服务(intelli-event-service)提供了统一的事件发布-订阅机制:

  • Kafka 事件总线:核心事件通过 Kafka Topic 广播
  • Webhook 回调:外部系统可以注册回调 URL,事件发生时自动 POST 通知
  • 指数退避重试:Webhook 投递失败后,按 1s → 2s → 4s → 8s → … 的间隔重试,最多 10 次

7.2 Java SDK:让接入成本降到最低

前面做了这么多——网关、认证、限流、统计——但如果合作伙伴接入还要手写 HMAC-SHA256 签名,那前面的努力就白费了。

SDK 的核心理念是一行配置,零成本接入

// 只需要这三个信息
IntelliHubClient client = IntelliHubClient.builder()
    .appKey("YOUR_APP_KEY")
    .appSecret("YOUR_APP_SECRET")
    .baseUrl("https://api.example.com")
    .build();

// 调用 API 就像调用本地方法
ApiResponse<UserDTO> response = client.get(
    "/open/api/user/info?id=123", 
    UserDTO.class
);

SDK 内部自动处理了签名生成、Nonce + Timestamp 防重放、HTTP 连接池、异常重试——调用方完全不需要理解签名算法的细节。

7.3 聚合搜索

平台上有 API、应用、用户等不同类型的资源,基于 Elasticsearch + IK 中文分词器实现了跨实体聚合搜索:

graph TB
    U[用户输入关键词] --> S[搜索服务]
    S -->|并行查询| IDX_API[API 索引]
    S -->|并行查询| IDX_APP[应用索引]
    S -->|并行查询| IDX_USER[用户索引]
    IDX_API --> M[结果合并 + 去重 + 排序]
    IDX_APP --> M
    IDX_USER --> M
    M --> R[高亮 + 分面统计 + 返回]

搜索自动过滤当前租户的数据,保证不同租户之间搜索隔离。中文分词准确率 90%+,响应时间 < 200ms。

7.4 前端控制台

前端选择了 Vue 3 Composition API + TypeScript + Element Plus + ECharts + Pinia 的技术栈。核心页面包括:

  • 控制台首页:API 总览、调用趋势、告警概览
  • API 管理:创建、编辑、发布、下线、版本管理
  • API 市场:公开 API 浏览、分类筛选、文档查看
  • 应用管理:AppKey 管理、订阅授权、配额配置
  • 统计监控:ECharts 多维度实时仪表盘、调用日志查询
  • 告警管理:规则配置、告警历史
  • 开发文档:SDK 使用指南、多语言代码示例
  • 系统设置:基础配置、安全设置、公告管理

关键技术决策的深度剖析

为什么用 Dubbo 而不是全 HTTP?

这是最重要的架构决策之一。Dubbo 泛化调用让网关做到了”零依赖路由”——不需要引入任何业务 JAR 包,就能动态调用任意 Dubbo 服务。如果用 HTTP,每接入一个新的后端系统都要改网关配置、加依赖,做不到”平台化”。

为什么用 Kafka 而不是 RabbitMQ?

日志场景的特性决定了 Kafka 更合适:高吞吐(日均千万级)、消息持久化(可以回溯重跑)、多消费者组(统计、告警、审计各自独立消费)。RabbitMQ 延迟更低,适合即时响应场景,在项目里预留给 AIGC 服务的异步任务。

为什么 JWT 要本地验签?

2000 QPS 的流量下,如果每次请求都远程调用 IAM 验证 Token,IAM 会成为系统瓶颈。本地验签 < 1ms,远程校验 > 10ms——10 倍差距。代价是 Token 吊销无法实时生效,但 2 小时的短有效期可以接受这个取舍。

为什么用 MyBatis Plus 租户插件?

多租户隔离有两种做法:代码规范(靠开发者自觉)vs 框架强制(插件自动注入)。前者有致命问题——人总会犯错。MyBatis Plus 的租户插件在 SQL 执行之前自动注入 WHERE tenant_id = ?,100% 不会遗漏。这就是”机制”优于”纪律”。

网关 Filter 链的顺序为什么这样设计?

限流 → 租户校验 → 认证 → 鉴权 → 路由 → 日志

这个顺序经过精心设计:限流在最前面(恶意请求直接拦截,不浪费后续资源),日志在最后面(无论放行还是拒绝,都会被记录,保证完整的调用链追踪)。


更多的踩坑记录

Dubbo 泛化调用的参数类型转换

网关通过泛化调用后端服务时,参数类型转换报错——后端接口参数是 com.example.UserDTO,网关传了 Map<String, Object>,Dubbo 无法自动转换。解决方案是在 API 平台建立”参数元数据中心”,创建 API 时存储参数类型,网关根据元数据动态构造调用参数。

前端登录状态丢失

用户刷新页面后需要重新登录。排查发现是 Pinia Store 初始化时 user 状态为 null。解决方案是在 Store 初始化时直接从 localStorage 恢复用户信息——Pinia 不会自动持久化状态,需要显式处理。

实体类与数据库表结构不匹配

MyBatis Plus 生成的 SQL 包含了数据库不存在的列,报错 Unknown column 'login_result'。原因是先建了表、后写了实体类,两者不一致。教训:先定义实体类,再根据实体类生成 SQL 建表语句。

BCrypt 密码验证的坑

数据库中的 BCrypt 哈希值包含了随机盐,同样的密码两次加密的结果会不同。所以密码验证必须用 matches() 方法,不能直接比较字符串——这个坑我踩了一次才发现。


数据与成果

性能指标

指标数值说明
日均 API 调用量500 万+覆盖 100+ 个 API
峰值 QPS2000+单网关节点实测
P99 响应延迟< 300ms含认证+限流+转发全链路
系统可用性99.95%2 个月累计故障 < 1 小时
路由匹配耗时< 5ms三级缓存方案(从 50ms 优化)
统计数据延迟< 10 秒双通道方案(从 10 分钟优化)
日志处理量1000 万+/天Kafka + Redis 双通道,零丢失
搜索响应< 200msES + IK 分词器

业务指标

指标优化前优化后提升
合作伙伴接入周期2 周2 天7 倍
安全事件~10 次/月~1 次/月90% ↓
故障发现时间30+ 分钟< 60 秒80% ↓
API 版本回滚30 分钟3 秒自动
发布效率60% ↑

项目规模

  • 总计 9 个模块:1 个公共组件 + 1 个网关 + 4 个核心服务 + 2 个扩展服务 + 1 个 SDK + 1 个前端
  • 后端代码 25,000+ 行
  • 前端代码 10,000+ 行
  • 技术文档 25+ 篇

未来规划

gantt
    title IntelliHub 规划路线图
    dateFormat  YYYY-MM
    axisFormat  %Y-%m

    section 已完成
    阶段一 架构设计       :done, v1, 2025-10, 2025-10
    阶段二 IAM + 多租户   :done, v2, 2025-10, 2025-10
    阶段三 应用中心       :done, v3, 2025-10, 2025-10
    阶段四 统一网关       :done, v4, 2025-10, 2025-11
    阶段五 调用统计       :done, v5, 2025-11, 2025-11
    阶段六 智能告警       :done, v6, 2025-11, 2025-12
    阶段七 事件+SDK+前端  :done, v7, 2025-12, 2025-12

    section 规划中
    告警通知实际集成      :active, v8, 2026-01, 2026-02
    AIGC 智能增强        :v9, 2026-02, 2026-04
    多协议支持 + 计费     :v10, 2026-05, 2026-08

经验总结

1. 架构要”设计够用”,不要”过度设计”

刚开始我画过一个包含 15 个微服务的架构图。现在回头看,那是典型的过度设计。最终的 9 个模块是根据实际领域需求拆分的结果。原则是:当你不确定一个模块是否该独立时,先放一起,真正需要拆的时候再拆。

2. 做平台要先建身份,再做入口

很多开发者上来就写网关,但网关的鉴权需要一个完整的身份体系。我的顺序是:先做 IAM(人)→ 再做应用中心(调用方)→ 最后做网关(入口)。这个顺序让每一步都有坚实的地基。

3. 异步不是银弹,但要会用

日志上报用异步是对的——不能拖慢业务。但订阅校验必须同步——没有授权的请求必须在调用入口就被拒绝。关键是分清楚哪些操作可以”最终处理”,哪些必须”立即处理”。

4. 安全要靠机制,不能靠自律

多租户隔离如果用”代码规范”来保证,一定会有遗漏。MyBatis Plus 租户插件的强制注入虽然引入了一点性能开销,但换来的是 100% 不会泄露的确定性。这个 trade-off 非常值得。

5. 记录每一次踩坑

JWT 密钥长度、StripPrefix 路径匹配、Kafka 消费延迟、Dubbo 类型转换……这些问题花了我大量时间排查。记录下来,下次就能快速定位——这也是为什么我整理了 25+ 篇技术文档。


写在最后

2 个月,一个人,9 个模块,25,000+ 行代码——从一个想法到一个能支撑日均 500 万+ 次调用的完整平台。

做这个项目的初衷其实很简单:我想知道自己能不能独立完成一个企业级的项目。 不是为了拿 Offer,不是为了刷简历,就是纯粹的”我想试试”。

事实证明,这条路走得通。过程中最深刻的体会是:做基础架构类的项目,最难的不是写代码,而是想清楚为什么这样做。 每一个技术决策——Dubbo vs HTTP、Kafka vs RabbitMQ、JWT 本地验签 vs 远程校验、滑动窗口 vs 固定窗口——都来自对具体场景的反复推敲。

如果你也对 API 开放平台感兴趣,欢迎访问 GitHub 仓库 查看完整源码。仓库里有 25+ 篇技术文档,覆盖了架构设计、技术实现、开发问题等各个方面。如果能给一个 Star,那将是对我最大的鼓励。

记录每一次成长,沉淀每一份积累 —— 这是 IntelliHub 教会我的事。


本文同步发布于掘金、知乎、CSDN 等平台。转载请联系作者。