项目全景:7 个阶段的渐进式演进
在开始之前,先用一张图看清整个项目的成长路径。我没有试图一次性把所有功能做出来——那样只会得到一个四不像的产物。而是像一个真实的产品一样,一个阶段解决一个问题,每一个阶段都在上一个阶段的基础上生长。
graph TD
V1["阶段一<br/>项目架构与设计<br/>2025.10 上旬"]
V2["阶段二<br/>用户登录 + 多租户 + 系统初始化<br/>2025.10 中旬"]
V3["阶段三<br/>应用中心<br/>2025.10 下旬"]
V4["阶段四<br/>统一网关<br/>2025.10 下旬 - 2025.11 中旬"]
V5["阶段五<br/>调用统计<br/>2025.11 中旬 - 2025.11 下旬"]
V6["阶段六<br/>智能告警<br/>2025.11 下旬 - 2025.12 上旬"]
V7["阶段七<br/>事件驱动 + SDK + 前端生态<br/>2025.12 上旬 - 2025.12 中旬"]
V1 --> V2 --> V3 --> V4 --> V5 --> V6 --> V7
为什么是这个顺序?因为我遵循了一个原则:先地基、再骨架、后血肉、最后皮肤。
- 阶段一先把架构想清楚,避免后面推倒重来
- 阶段二/三把”人”和”应用”的身份体系建好——这是所有安全机制的前提
- 阶段四的网关是整个平台的心脏——只有身份体系就位了,网关才知道”谁在请求”
- 阶段五/六让平台有了”眼睛”和”警报器”
- 阶段七是生态完善——SDK 降低接入门槛,事件驱动让架构更灵活
下面我逐一展开每个阶段做了什么、为什么这么做、踩了什么坑。
阶段一:项目架构与设计
1.1 从”画图”开始
动手写代码之前,我花了整整两周时间画架构图、写设计文档。
为什么要花这么多时间?因为我知道,API 开放平台是一个典型的”地基决定上层建筑”的项目。如果一开始对模块边界、通信方式、数据流向想不清楚,后面每加一个新功能都可能是灾难。
我给自己定了三个核心设计原则:
- 统一入口:所有流量——不管是管理后台的还是开放 API 的——全部经过网关,在入口层统一做认证、鉴权、限流
- 按领域拆分:每个微服务负责一个完整的业务领域,有自己的数据库,服务间用 RPC 通信
- 异步解耦:日志、统计、事件通知等非核心链路全部异步化,不拖慢主流程
1.2 技术选型的反复推敲
技术选型是这个阶段最头疼的事。每一个选择都有 trade-off,没有完美的方案,只有最合适的取舍。
| 层次 | 选择 | 备选 | 为什么选这个 |
|---|---|---|---|
| 网关 | Spring Cloud Gateway (WebFlux) | Zuul / Kong / APISIX | WebFlux 非阻塞模型,Spring 生态无缝集成 |
| RPC | Dubbo 3.x | OpenFeign / gRPC | Dubbo 泛化调用是网关”零依赖路由”的关键 |
| 消息队列 | Kafka | RabbitMQ / RocketMQ | 日志场景天然适合 Kafka 的高吞吐 + 持久化 |
| 缓存 | Redis | — | 限流、防重放、实时统计都需要原子操作 |
| 数据库 | MySQL 8.0 + MyBatis Plus | — | MyBatis Plus 的租户插件是实现多租户隔离的核心 |
| 注册中心 | Nacos 2.x | Eureka / Consul | 同时提供注册发现和配置中心,一个组件解决两个问题 |
| 搜索引擎 | Elasticsearch + IK 分词器 | — | 中文分词 + 跨索引聚合搜索 |
| 前端 | Vue 3 + TypeScript + Element Plus | React | Vue 3 Composition API 开发效率高,Element Plus 企业后台组件丰富 |
1.3 模块规划:9 大模块的职责边界
最终确定了 9 个模块,每个都有明确的职责边界:
graph TB
subgraph "基础设施"
COMMON["inner-intergration<br/>公共组件、工具类、<br/>Dubbo 接口、签名工具"]
end
subgraph "核心服务"
IAM["intelli-auth-iam-service<br/>IAM 认证服务<br/>:8081"]
API["intelli-api-platform-service<br/>API 平台服务<br/>:8082"]
APP["intelli-app-center-service<br/>应用中心服务<br/>:8085"]
GW["intelli-gateway-service<br/>统一网关<br/>:8080"]
GOV["intelli-governance-service<br/>治理中心<br/>:8083"]
end
subgraph "扩展服务"
SEARCH["intelli-search-service<br/>聚合搜索<br/>:8086"]
EVENT["intelli-event-service<br/>事件中心<br/>:8087"]
end
subgraph "生态"
SDK["intelli-sdk<br/>Java SDK"]
FE["intellihub-frontend<br/>Vue 3 前端控制台"]
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 : "订阅"
APP_API_SUBSCRIPTION }o--|| API_INFO : "被订阅的API"
APPLICATION {
string id PK
string tenant_id
string name "应用名称"
string app_key "32位安全随机"
string app_secret "64位安全随机"
string status "active/disabled"
int quota_limit "每日配额上限"
}
APP_API_SUBSCRIPTION {
string id PK
string app_id FK
string api_id FK
string status "active/cancelled"
}
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["JWT 认证链路<br/>────────<br/>① JWT 本地验签<br/>② 注入 X-User-Id<br/>③ 注入 X-Tenant-Id<br/>④ 静态路由转发"]
PATH -->|/open/**| APPKEY_FLOW["AppKey 认证链路<br/>────────<br/>① 动态路由匹配<br/>② Nonce 防重放<br/>③ HMAC-SHA256 验签<br/>④ 订阅关系校验<br/>⑤ IP 白名单检查<br/>⑥ 配额检查<br/>⑦ 动态路由转发"]
4.5 滑动窗口限流
限流算法我一开始用了最简单的固定窗口,但很快就发现了”边界突刺”问题。
举个例子:假设限制每分钟 100 次。用户在 12:00:59 发了 100 个请求,然后在 12:01:00 又发了 100 个——这两波实际间隔只有 1 秒,但因为落在不同的固定窗口里,都”合法”了。
换成滑动窗口后,情况完全不同:
graph LR
REQ[请求到达] --> REMOVE["① 移除过期记录<br/>ZREMRANGEBYSCORE key 0 now-window"]
REMOVE --> COUNT["② 统计窗口内请求数<br/>ZCARD key"]
COUNT --> JUDGE{"③ 超过阈值?"}
JUDGE -->|否| ALLOW["④ 放行<br/>ZADD key now now"]
JUDGE -->|是| REJECT["④ 拒绝<br/>HTTP 429"]
基于 Redis Sorted Set 实现,窗口定义是”当前时间往前推 N 秒”,而非”第 N 个自然分钟”。支持的限流维度:
| 维度 | Redis Key | 说明 |
|---|---|---|
| IP | rate_limit:ip:192.168.1.1 | 同 IP 所有接口 |
| Path | rate_limit:path:/api/login | 同接口所有 IP |
| IP+Path | rate_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 个线程,高峰期根本消费不过来,消息大量积压。
解决策略:
- 增加消费者线程数:2 → 8
- 补充 Redis 实时统计通道(就是前面说的”双通道”)
- 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 |
| 峰值 QPS | 2000+ | 单网关节点实测 |
| P99 响应延迟 | < 300ms | 含认证+限流+转发全链路 |
| 系统可用性 | 99.95% | 2 个月累计故障 < 1 小时 |
| 路由匹配耗时 | < 5ms | 三级缓存方案(从 50ms 优化) |
| 统计数据延迟 | < 10 秒 | 双通道方案(从 10 分钟优化) |
| 日志处理量 | 1000 万+/天 | Kafka + Redis 双通道,零丢失 |
| 搜索响应 | < 200ms | ES + 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 等平台。转载请联系作者。