本文引用的文件
清鉴 AI 调查工具网关由独立模块 ai-gateway 提供,基于 Spring Cloud Gateway(WebFlux/Netty),默认监听端口 8080。它负责把前端请求按“用户粘性”转发到多台 ai-server 后端实例,并通过 PostgreSQL 持久化服务、节点和用户绑定关系。
AdminController 暴露 /gateway/admin/** REST 接口,用于维护 StickyRouteFilter 使用的 gateway_service 和 gateway_node 配置以及用户粘性路由。这些接口通常由运维人员或部署脚本调用,而不是普通业务客户端。
本 API 文档聚焦:
ai-gateway 中与管理接口直接相关的代码分层如下:
graph TB
Client["运维脚本或管理员"] --> Filter["AdminAuthWebFilter<br/>管理接口准入校验"]
Filter --> Controller["AdminController<br/>REST 路由"]
Controller --> Service["AdminService<br/>业务逻辑"]
Service --> Registry["GatewayRegistry<br/>服务/节点内存快照"]
Service --> ConfigRepo["GatewayConfigRepository<br/>PostgreSQL 写操作"]
Service --> RouteRepo["UserRouteRepository<br/>用户绑定读写"]
Service --> HealthChecker["HealthChecker<br/>手动探活"]
Service --> HealthReg["NodeHealthRegistry<br/>节点健康快照"]
图示来源
控制器只负责参数绑定、异常处理和阻塞操作调度;真实逻辑在 AdminService。所有 JDBC 调用都通过 Schedulers.boundedElastic() 执行,避免占用 Netty 事件循环线程。
章节来源
管理接口不是“绕过 x-token”,而是“额外要求管理员身份”。AdminAuthWebFilter 对 /gateway/admin/** 路径生效,顺序为:
gateway.admin.enabled=false,全部返回 404;gateway.admin.ip-whitelist;x-token 解析 userId;sys_user,要求 is_admin=1 且状态正常;因此,调用者仍需携带有效的 x-token,并且该 token 必须属于一个启用状态的管理员账号。
sequenceDiagram
participant Script as "运维脚本"
participant Filter as "AdminAuthWebFilter"
participant Token as "TokenResolver"
participant DB as "PostgreSQL"
participant Controller as "AdminController"
Script->>Filter: "/gateway/admin/** + x-token"
Filter->>Filter: 检查 gateway.admin.enabled
Filter->>Filter: 检查 IP 白名单
Filter->>Token: 解析 x-token → userId
Token->>DB: 查询 sys_user
DB-->>Filter: 管理员用户信息
Filter->>Controller: 放行
Controller->>Controller: 执行业务接口
Controller-->>Script: ApiResult
图示来源
| 场景 | HTTP 状态码 | 说明 |
|---|---|---|
| 管理接口未启用 | 404 | gateway.admin.enabled=false 时,管理接口整体消失 |
| IP 不在白名单 | 403 | 即使 token 有效也会被拒绝 |
| 缺少 x-token 或 token 无效 | 401 | 未登录或登录已失效 |
| 非管理员或停用用户 | 403 | 需要管理员权限 |
| 控制器业务异常 | 400/404/409 | 由 AdminApiException 决定 |
| 请求参数绑定失败 | 400 | 由 WebExchangeBindException 处理 |
章节来源
默认情况下,网关不充当跨域收口:
gateway.cors.enabled=false;ai-server 已有全局 CorsFilter;DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_UNIQUE 去除重复响应头。只有在确实需要网关统一处理跨域时才开启 gateway.cors.enabled=true,并配合 allowed-origin-patterns 限制来源。
章节来源
当前 AdminController 没有内置限流逻辑。生产环境建议:
gateway.admin.ip-whitelist 缩小来源 IP 范围;/gateway/admin 暴露在公网;x-token 对应的会话生命周期;管理接口维护三类核心数据:服务、节点、用户粘性路由。
erDiagram
GATEWAY_SERVICE ||--o{ GATEWAY_NODE : "包含"
GATEWAY_SERVICE ||--o{ GATEWAY_USER_ROUTE : "拥有"
GATEWAY_NODE ||--o{ GATEWAY_USER_ROUTE : "被绑定"
GATEWAY_SERVICE {
varchar id PK
varchar name
varchar path_prefix UK
varchar login_path
varchar health_path
varchar default_node_id
smallint enabled
timestamp create_time
timestamp update_time
}
GATEWAY_NODE {
varchar id PK
varchar service_id FK
varchar name
varchar base_url
smallint enabled
varchar remark
timestamp create_time
timestamp update_time
}
GATEWAY_USER_ROUTE {
bigint id PK
varchar service_id FK
bigint user_id
varchar username
varchar node_id FK
timestamp create_time
timestamp update_time
}
图示来源
关键字段含义:
gateway_service.path_prefix:对外路径前缀,例如 /js/a,全局唯一,不能以 / 结尾,也不能是 /gateway;gateway_service.login_path:相对 path_prefix 的登录路径,为空表示不做登录粘性;gateway_service.default_node_id:存量用户兜底节点;gateway_node.base_url:后端节点地址,不含上下文路径;gateway_user_route.user_id 与 username:至少一个非空,分别建立部分唯一索引,保证同一用户在同一个服务下只绑定一个节点。章节来源
所有接口基础路径为:
http://网关地址:8080/gateway/admin
| 接口 | HTTP 方法 | URL | 用途 | 主要调用方 |
|---|---|---|---|---|
| 查看状态 | GET | /gateway/admin/status |
获取服务、节点、健康状态汇总 | 运维监控脚本 |
| 查看服务列表 | GET | /gateway/admin/services |
获取服务及节点快照 | 运维面板 |
| 创建服务 | POST | /gateway/admin/services |
注册新的对外服务 | 部署脚本 |
| 更新服务 | PUT | /gateway/admin/services/{serviceId} |
修改服务配置 | 运维脚本 |
| 删除服务 | DELETE | /gateway/admin/services/{serviceId} |
删除无节点、无绑定的服务 | 运维脚本 |
| 创建节点 | POST | /gateway/admin/services/{serviceId}/nodes |
为服务添加后端节点 | 部署脚本 |
| 更新节点 | PUT | /gateway/admin/nodes/{nodeId} |
修改节点地址或备注 | 运维脚本 |
| 删除节点 | DELETE | /gateway/admin/nodes/{nodeId} |
移除不再使用的节点 | 运维脚本 |
| 检查节点 | POST | /gateway/admin/nodes/{nodeId}/check |
立即探测节点健康 | 运维脚本 |
| 分页查看用户路由 | GET | /gateway/admin/routes?serviceId=...&keyword=&page=&size= |
查看用户粘性绑定 | 运维面板 |
| 绑定用户路由 | POST | /gateway/admin/routes/bind |
为用户绑定节点 | 迁移脚本 |
| 解绑用户路由 | DELETE | /gateway/admin/routes/{id} |
解除用户绑定 | 运维脚本 |
| 导入已有绑定 | POST | /gateway/admin/routes/import-existing |
批量导入现有用户绑定 | 迁移脚本 |
| 重载配置 | POST | /gateway/admin/reload |
强制刷新服务/节点缓存 | 运维脚本 |
章节来源
除明确说明外,管理接口统一返回 ApiResult:
| 字段 | 类型 | 说明 |
|---|---|---|
code |
int | 成功为 200,失败为具体错误码 |
message |
string | 人类可读消息 |
data |
object/null | 业务数据;成功时可能为 null |
常见错误码:
| code | HTTP 状态码 | 含义 |
|---|---|---|
| 200 | 200 | 成功 |
| 400 | 400 | 参数错误或资源冲突 |
| 401 | 401 | 未登录或 token 无效 |
| 403 | 403 | 非管理员或 IP 不在白名单 |
| 404 | 404 | 服务或节点不存在 |
| 409 | 409 | 存在依赖:仍有节点、仍有用户绑定等 |
章节来源
查看网关当前加载的服务、节点及其健康状态。
/gateway/admin/statusx-token,且为管理员data 结构:| 字段 | 类型 | 说明 |
|---|---|---|
services |
array | 服务列表 |
services[].id |
string | 服务 ID |
services[].name |
string | 服务名称 |
services[].pathPrefix |
string | 对外路径前缀 |
services[].enabled |
int | 是否启用 |
services[].defaultNodeId |
string/null | 兜底节点 ID |
services[].routeCount |
int | 该服务的用户绑定数量 |
services[].nodes |
array | 节点列表 |
services[].nodes[].id |
string | 节点 ID |
services[].nodes[].name |
string | 节点名称 |
services[].nodes[].baseUrl |
string | 后端地址 |
services[].nodes[].enabled |
int | 是否启用 |
services[].nodes[].state |
string | 健康状态:UP/DOWN/UNKNOWN 等 |
services[].nodes[].lastError |
string/null | 最近一次探活错误 |
services[].nodes[].lastCheckedAt |
timestamp/null | 最近探活时间 |
services[].nodes[].routeCount |
int | 该节点的绑定用户数 |
| HTTP 状态码 | code | 触发条件 |
|---|---|---|
| 401 | 401 | 缺少 x-token 或 token 无效 |
| 403 | 403 | IP 不在白名单或非管理员 |
curl 示例:
curl -H "x-token: YOUR_ADMIN_TOKEN" \
"http://localhost:8080/gateway/admin/status"
章节来源
获取服务与节点快照,便于快速查看当前网关加载了哪些服务和节点。
/gateway/admin/servicesx-token,且为管理员data:与服务列表一致,即 services 数组。curl 示例:
curl -H "x-token: YOUR_ADMIN_TOKEN" \
"http://localhost:8080/gateway/admin/services"
章节来源
创建服务。
/gateway/admin/servicesx-token,且为管理员| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
id |
string | 是 | 服务 ID,全局唯一 |
name |
string | 是 | 服务名称 |
pathPrefix |
string | 是 | 对外路径前缀,例如 /js/a |
loginPath |
string/null | 否 | 相对 pathPrefix 的登录路径 |
healthPath |
string | 是 | 完整健康检查路径,例如 /js/a/sys/health |
defaultNodeId |
string/null | 否 | 存量用户兜底节点 ID |
enabled |
int | 否 | 是否启用,默认启用 |
code=200,data=null| HTTP 状态码 | code | 触发条件 |
|---|---|---|
| 400 | 400 | 服务已存在或 path-prefix 被其他服务占用 |
| 401 | 401 | 未登录或 token 无效 |
| 403 | 403 | IP 不在白名单或非管理员 |
curl 示例:
curl -X POST -H "x-token: YOUR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"id": "zsjz-ai",
"name": "智能分析后端",
"pathPrefix": "/js/a",
"loginPath": "/auth/login",
"healthPath": "/js/a/sys/health",
"defaultNodeId": "node-1",
"enabled": 1
}' \
"http://localhost:8080/gateway/admin/services"
章节来源
更新服务。
/gateway/admin/services/{serviceId}| 参数 | 类型 | 说明 |
|---|---|---|
serviceId |
string | 要更新的服务 ID |
id 可省略或由路径决定;实际实现仍会读取 DTO 中的 id 字段。code=200| HTTP 状态码 | code | 触发条件 |
|---|---|---|
| 400 | 400 | path-prefix 被其他服务占用 |
| 404 | 404 | 服务不存在 |
| 401/403 | 401/403 | 认证或权限失败 |
curl 示例:
curl -X PUT -H "x-token: YOUR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"id": "zsjz-ai",
"name": "智能分析后端",
"pathPrefix": "/js/a",
"loginPath": "/auth/login",
"healthPath": "/js/a/sys/health",
"defaultNodeId": "node-1",
"enabled": 1
}' \
"http://localhost:8080/gateway/admin/services/zsjz-ai"
章节来源
删除服务。只有满足以下条件才能删除:
服务下没有用户绑定。
方法:DELETE
URL:/gateway/admin/services/{serviceId}
成功响应:code=200
错误响应:
| HTTP 状态码 | code | 触发条件 |
|---|---|---|
| 404 | 404 | 服务不存在 |
| 409 | 409 | 服务下仍有节点或用户绑定 |
| 401/403 | 401/403 | 认证或权限失败 |
curl 示例:
curl -X DELETE -H "x-token: YOUR_ADMIN_TOKEN" \
"http://localhost:8080/gateway/admin/services/zsjz-ai"
章节来源
为服务新增节点。
/gateway/admin/services/{serviceId}/nodes| 参数 | 类型 | 说明 |
|---|---|---|
serviceId |
string | 目标服务 ID |
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
id |
string | 是 | 节点 ID,全局唯一 |
name |
string | 是 | 节点名称 |
baseUrl |
string | 是 | 后端地址,例如 http://host:port |
enabled |
int | 否 | 是否启用 |
remark |
string/null | 否 | 备注 |
code=200| HTTP 状态码 | code | 触发条件 |
|---|---|---|
| 400 | 400 | 节点 ID 已存在或服务下 base-url 已存在 |
| 404 | 404 | 服务不存在 |
| 401/403 | 401/403 | 认证或权限失败 |
curl 示例:
curl -X POST -H "x-token: YOUR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"id": "node-1",
"name": "节点1",
"baseUrl": "http://192.168.0.101:8980",
"enabled": 1,
"remark": "主节点"
}' \
"http://localhost:8080/gateway/admin/services/zsjz-ai/nodes"
章节来源
更新节点。
/gateway/admin/nodes/{nodeId}| 参数 | 类型 | 说明 |
|---|---|---|
nodeId |
string | 要更新的节点 ID |
code=200| HTTP 状态码 | code | 触发条件 |
|---|---|---|
| 400 | 400 | 同服务下 base-url 已存在 |
| 404 | 404 | 节点不存在 |
| 401/403 | 401/403 | 认证或权限失败 |
curl 示例:
curl -X PUT -H "x-token: YOUR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"id": "node-1",
"name": "节点1",
"baseUrl": "http://192.168.0.101:8980",
"enabled": 1,
"remark": "主节点"
}' \
"http://localhost:8080/gateway/admin/nodes/node-1"
章节来源
删除节点。前提是该节点没有被任何用户绑定。
/gateway/admin/nodes/{nodeId}code=200| HTTP 状态码 | code | 触发条件 |
|---|---|---|
| 404 | 404 | 节点不存在 |
| 409 | 409 | 节点上仍有用户绑定 |
| 401/403 | 401/403 | 认证或权限失败 |
curl 示例:
curl -X DELETE -H "x-token: YOUR_ADMIN_TOKEN" \
"http://localhost:8080/gateway/admin/nodes/node-1"
章节来源
立即探测指定节点的健康状态,不等待定时任务。
/gateway/admin/nodes/{nodeId}/checkdata:| 字段 | 类型 | 说明 |
|---|---|---|
nodeId |
string | 节点 ID |
state |
string | 探测后的健康状态 |
| HTTP 状态码 | code | 触发条件 |
|---|---|---|
| 404 | 404 | 节点不存在 |
| 401/403 | 401/403 | 认证或权限失败 |
curl 示例:
curl -X POST -H "x-token: YOUR_ADMIN_TOKEN" \
"http://localhost:8080/gateway/admin/nodes/node-1/check"
章节来源
分页查询用户粘性路由。
/gateway/admin/routes| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
serviceId |
string | 是 | — | 服务 ID |
keyword |
string | 否 | — | 模糊关键词 |
page |
int | 否 | 0 |
页码 |
size |
int | 否 | 20 |
每页大小 |
data:| 字段 | 类型 | 说明 |
|---|---|---|
total |
int | 总记录数 |
items |
array | 路由条目列表 |
| HTTP 状态码 | code | 触发条件 |
|---|---|---|
| 401/403 | 401/403 | 认证或权限失败 |
curl 示例:
curl -H "x-token: YOUR_ADMIN_TOKEN" \
"http://localhost:8080/gateway/admin/routes?serviceId=zsjz-ai&page=0&size=20"
章节来源
绑定用户到节点。
/gateway/admin/routes/bind| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
serviceId |
string | 是 | 服务 ID |
userId |
long/string | 二选一 | 用户 ID |
username |
string | 二选一 | 用户名 |
nodeId |
string | 是 | 目标节点 ID |
code=200| HTTP 状态码 | code | 触发条件 |
|---|---|---|
| 400 | 400 | 参数缺失或绑定失败 |
| 404 | 404 | 服务或节点不存在 |
| 401/403 | 401/403 | 认证或权限失败 |
curl 示例:
curl -X POST -H "x-token: YOUR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"serviceId": "zsjz-ai",
"userId": 1,
"nodeId": "node-1"
}' \
"http://localhost:8080/gateway/admin/routes/bind"
章节来源
解绑用户路由。
/gateway/admin/routes/{id}| 参数 | 类型 | 说明 |
|---|---|---|
id |
long | 用户路由记录 ID |
code=200| HTTP 状态码 | code | 触发条件 |
|---|---|---|
| 401/403 | 401/403 | 认证或权限失败 |
curl 示例:
curl -X DELETE -H "x-token: YOUR_ADMIN_TOKEN" \
"http://localhost:8080/gateway/admin/routes/1"
章节来源
批量导入已有用户绑定。
/gateway/admin/routes/import-existing| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
serviceId |
string | 是 | 目标服务 ID |
nodeId |
string | 是 | 目标节点 ID |
code=200| HTTP 状态码 | code | 触发条件 |
|---|---|---|
| 400 | 400 | 参数缺失或导入失败 |
| 404 | 404 | 服务或节点不存在 |
| 401/403 | 401/403 | 认证或权限失败 |
curl 示例:
curl -X POST -H "x-token: YOUR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"serviceId": "zsjz-ai",
"nodeId": "node-1"
}' \
"http://localhost:8080/gateway/admin/routes/import-existing"
章节来源
强制刷新服务与节点缓存。
/gateway/admin/reloadcode=200| HTTP 状态码 | code | 触发条件 |
|---|---|---|
| 401/403 | 401/403 | 认证或权限失败 |
curl 示例:
curl -X POST -H "x-token: YOUR_ADMIN_TOKEN" \
"http://localhost:8080/gateway/admin/reload"
章节来源
sequenceDiagram
participant Admin as "管理员或脚本"
participant Filter as "AdminAuthWebFilter"
participant Controller as "AdminController"
participant Service as "AdminService"
participant Registry as "GatewayRegistry"
participant DB as "PostgreSQL"
Admin->>Filter: 请求 /gateway/admin/**
Filter->>Filter: 检查 enabled、IP、x-token、管理员
Filter->>Controller: 放行
Controller->>Service: 执行业务方法
Service->>DB: 读写 gateway_service / gateway_node / gateway_user_route
Service->>Registry: reload()
Registry->>Registry: 发布 RefreshRoutesEvent
Registry-->>Service: 返回
Service-->>Controller: 成功
Controller-->>Admin: ApiResult
图示来源
StickyRouteFilter 是普通业务请求的核心过滤器。它根据 x-token 找到用户绑定的节点后,会严格校验:
若任一条件失败,抛出 StickyUnavailableException,最终转为 HTTP 503。
flowchart TD
Start["进入 StickyRouteFilter"] --> MatchService["匹配服务"]
MatchService --> IsLogin{"是否为登录请求"}
IsLogin -->|是| LoginDecide["按 username 查找绑定节点"]
IsLogin -->|否| NormalDecide["按 x-token 解析 userId 并查找绑定节点"]
LoginDecide --> FoundNode{"找到绑定节点?"}
NormalDecide --> FoundNode
FoundNode -->|是| ValidateNode["校验节点启用、健康"]
FoundNode -->|否| DefaultNode{"是否有默认节点?"}
DefaultNode -->|有| BindDefault["绑定到默认节点"]
DefaultNode -->|无| Error503["返回 503:用户未绑定节点"]
ValidateNode --> NodeUp{"节点 UP?"}
NodeUp -->|是| Forward["重写目标 URL 并转发"]
NodeUp -->|否| Error503
BindDefault --> ValidateNode
Forward --> End["返回响应,附加 X-Gateway-Node"]
Error503 --> End
图示来源
章节来源
当某个用户绑定节点的状态变为 DOWN 时,该用户的后续请求会返回 503,而不是自动切换到其他健康节点。这是设计决策:案件数据、附件、Agent 记忆等落在各节点本地磁盘,切到其他机器会导致“当前案件数据源未打开或已释放”等问题。
确认用户是否绑定了节点
调用:
curl -H "x-token: YOUR_ADMIN_TOKEN" \
"http://localhost:8080/gateway/admin/routes?serviceId=zsjz-ai&keyword=USERNAME_OR_USERID&page=0&size=100"
查看服务与节点健康状态
调用:
curl -H "x-token: YOUR_ADMIN_TOKEN" \
"http://localhost:8080/gateway/admin/status"
关注:
services[].nodes[].state;services[].nodes[].lastError;services[].nodes[].lastCheckedAt。手动探测节点
调用:
curl -X POST -H "x-token: YOUR_ADMIN_TOKEN" \
"http://localhost:8080/gateway/admin/nodes/NODE_ID/check"
如果返回的 state 仍为 DOWN,说明节点健康检查失败。
检查节点后端是否可达
使用 baseUrl 拼接 healthPath,从网关所在机器发起探测。例如:
curl -v "http://192.168.0.101:8980/js/a/sys/health"
判断是否需要迁移用户
如果节点已下线,应先:
注意:不要期望 503 后自动重试其他节点
当前实现不会因绑定节点 DOWN 而切换。运维必须通过管理接口调整绑定。
| 现象 | 原因 | 处理方式 |
|---|---|---|
| 单个用户请求 503 | 该用户绑定的节点 DOWN | 修复节点或迁移用户绑定 |
| 新用户登录 503 | 服务没有可用节点 | 先添加健康节点 |
| 匿名请求 503 | 服务暂无健康节点 | 检查节点配置与健康检查 |
| 用户未绑定节点 503 | 用户既无绑定也无默认节点 | 引导用户重新登录或预先绑定 |
章节来源
GET /gateway/admin/status 拉取当前服务与节点;POST /gateway/admin/services 创建;POST /gateway/admin/services/{serviceId}/nodes 添加节点;POST /gateway/admin/nodes/{nodeId}/check 验证节点健康;POST /gateway/admin/routes/import-existing 或 POST /gateway/admin/routes/bind 完成迁移;POST /gateway/admin/reload 确保缓存刷新;GET /gateway/admin/status 验证结果。GET /gateway/admin/services 判断是否已存在;PUT,并根据返回值判断是否真正变更;GatewayRegistry 支持多实例共享 PostgreSQL:
reload() 后立即刷新自身缓存;gateway.registry-poll-interval,默认 10 秒;RefreshRoutesEvent 刷新路由缓存。因此,单点写入后其他实例不会立即生效,最多延迟一个轮询周期。
章节来源
可能原因:
gateway.admin.enabled=false;/gateway/admin;8080。排查命令:
curl -v "http://localhost:8080/gateway/admin/status"
章节来源
可能原因:
gateway.admin.ip-whitelist;排查命令:
curl -H "x-token: YOUR_ADMIN_TOKEN" \
"http://localhost:8080/gateway/admin/status"
章节来源
说明另一个服务已经占用了相同的对外路径前缀。需要先修改冲突服务的路径前缀,或删除冲突服务。
章节来源
删除服务前必须:
删除节点前必须:
章节来源
这是粘性路由的预期行为。只要用户绑定的节点不可用,网关就不会自动切换。运维必须:
章节来源
AdminController 暴露的 /gateway/admin/** 接口是清鉴 AI 网关的配置面,用于维护 gateway_service、gateway_node 和 gateway_user_route 数据。它们由运维人员或部署脚本调用,负责:
与普通业务请求不同,这些接口需要管理员身份,并受 IP 白名单、x-token、管理员权限等多重保护。当用户绑定的节点 DOWN 时,网关返回 503 且不自动切换,运维必须通过管理接口调整绑定或恢复节点。