# 网关管理接口 **本文引用的文件** - [AdminController.java](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminController.java) - [AdminService.java](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminService.java) - [AdminAuthWebFilter.java](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminAuthWebFilter.java) - [AdminApiException.java](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminApiException.java) - [ApiResult.java](file://ai-gateway/src/main/java/com/zsjz/gateway/support/ApiResult.java) - [StickyUnavailableException.java](file://ai-gateway/src/main/java/com/zsjz/gateway/support/StickyUnavailableException.java) - [StickyRouteFilter.java](file://ai-gateway/src/main/java/com/zsjz/gateway/route/StickyRouteFilter.java) - [GatewayRegistry.java](file://ai-gateway/src/main/java/com/zsjz/gateway/registry/GatewayRegistry.java) - [application.yaml](file://ai-gateway/src/main/resources/application.yaml) - [gateway.sql](file://sql/gateway.sql) - [ai-gateway-user-sticky-routing.md](file://docs/design/ai-gateway-user-sticky-routing.md) ## 目录 1. [引言](#引言) 2. [项目结构与职责边界](#项目结构与职责边界) 3. [认证与访问控制](#认证与访问控制) 4. [跨域、限流与安全建议](#跨域限流与安全建议) 5. [数据模型说明](#数据模型说明) 6. [接口总览](#接口总览) 7. [详细接口定义](#详细接口定义) 8. [架构与调用关系](#架构与调用关系) 9. [节点 DOWN 时返回 503 的排查指南](#节点-down-时返回-503-的排查指南) 10. [运维脚本集成建议](#运维脚本集成建议) 11. [常见问题与故障定位](#常见问题与故障定位) 12. [结论](#结论) ## 引言 清鉴 AI 调查工具网关由独立模块 `ai-gateway` 提供,基于 Spring Cloud Gateway(WebFlux/Netty),默认监听端口 `8080`。它负责把前端请求按“用户粘性”转发到多台 `ai-server` 后端实例,并通过 PostgreSQL 持久化服务、节点和用户绑定关系。 `AdminController` 暴露 `/gateway/admin/**` REST 接口,用于维护 `StickyRouteFilter` 使用的 `gateway_service` 和 `gateway_node` 配置以及用户粘性路由。这些接口通常由运维人员或部署脚本调用,而不是普通业务客户端。 本 API 文档聚焦: - 服务注册、修改、删除; - 节点增删改查; - 节点健康检查; - 用户粘性路由分页查询、绑定、解绑、导入已有绑定; - 全局配置重载; - 认证方式、错误码、HTTP 状态码、响应体结构; - 当节点不可用时客户端收到 503 的排查方法。 ## 项目结构与职责边界 `ai-gateway` 中与管理接口直接相关的代码分层如下: ```mermaid graph TB Client["运维脚本或管理员"] --> Filter["AdminAuthWebFilter
管理接口准入校验"] Filter --> Controller["AdminController
REST 路由"] Controller --> Service["AdminService
业务逻辑"] Service --> Registry["GatewayRegistry
服务/节点内存快照"] Service --> ConfigRepo["GatewayConfigRepository
PostgreSQL 写操作"] Service --> RouteRepo["UserRouteRepository
用户绑定读写"] Service --> HealthChecker["HealthChecker
手动探活"] Service --> HealthReg["NodeHealthRegistry
节点健康快照"] ``` **图示来源** - [AdminController.java:1-153](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminController.java#L1-L153) - [AdminService.java:1-200](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminService.java#L1-L200) - [GatewayRegistry.java:1-116](file://ai-gateway/src/main/java/com/zsjz/gateway/registry/GatewayRegistry.java#L1-L116) - [AdminAuthWebFilter.java:1-90](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminAuthWebFilter.java#L1-L90) 控制器只负责参数绑定、异常处理和阻塞操作调度;真实逻辑在 `AdminService`。所有 JDBC 调用都通过 `Schedulers.boundedElastic()` 执行,避免占用 Netty 事件循环线程。 **章节来源** - [AdminController.java:1-153](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminController.java#L1-L153) - [AdminService.java:1-200](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminService.java#L1-L200) ## 认证与访问控制 ### 认证流程 管理接口不是“绕过 x-token”,而是“额外要求管理员身份”。`AdminAuthWebFilter` 对 `/gateway/admin/**` 路径生效,顺序为: 1. 如果 `gateway.admin.enabled=false`,全部返回 404; 2. 校验来源 IP 是否在 `gateway.admin.ip-whitelist`; 3. 从请求头 `x-token` 解析 userId; 4. 查询 `sys_user`,要求 `is_admin=1` 且状态正常; 5. 否则返回 401 或 403。 因此,调用者仍需携带有效的 `x-token`,并且该 token 必须属于一个启用状态的管理员账号。 ```mermaid 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 ``` **图示来源** - [AdminAuthWebFilter.java:1-90](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminAuthWebFilter.java#L1-L90) - [AdminController.java:1-153](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminController.java#L1-L153) ### 关键行为 | 场景 | HTTP 状态码 | 说明 | |---|---:|---| | 管理接口未启用 | 404 | `gateway.admin.enabled=false` 时,管理接口整体消失 | | IP 不在白名单 | 403 | 即使 token 有效也会被拒绝 | | 缺少 x-token 或 token 无效 | 401 | 未登录或登录已失效 | | 非管理员或停用用户 | 403 | 需要管理员权限 | | 控制器业务异常 | 400/404/409 | 由 `AdminApiException` 决定 | | 请求参数绑定失败 | 400 | 由 `WebExchangeBindException` 处理 | **章节来源** - [AdminAuthWebFilter.java:1-90](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminAuthWebFilter.java#L1-L90) - [AdminController.java:100-153](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminController.java#L100-L153) - [AdminApiException.java:1-30](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminApiException.java#L1-L30) ## 跨域、限流与安全建议 ### 跨域策略 默认情况下,网关不充当跨域收口: - `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` 限制来源。 **章节来源** - [application.yaml:1-81](file://ai-gateway/src/main/resources/application.yaml#L1-L81) ### 限流建议 当前 `AdminController` 没有内置限流逻辑。生产环境建议: - 在网关外层使用反向代理或云厂商网关做 IP 级限流; - 对写接口(创建、更新、删除、绑定、导入、重载)设置更严格的频率限制; - 将管理接口暴露范围限制在内网或运维网络; - 结合 `gateway.admin.ip-whitelist` 缩小来源 IP 范围; - 记录审计日志,至少保留操作人、IP、时间、接口、结果。 ### 安全建议 - 不要将 `/gateway/admin` 暴露在公网; - 白名单应精确到运维服务器地址或 CIDR; - 管理员账号应使用强密码并限制登录设备; - 定期轮换 `x-token` 对应的会话生命周期; - 不建议在生产环境关闭 IP 白名单; - 如需更强保护,可在网关前增加二次认证、跳板机或零信任入口。 ## 数据模型说明 管理接口维护三类核心数据:服务、节点、用户粘性路由。 ```mermaid 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.sql:1-107](file://sql/gateway.sql#L1-L107) 关键字段含义: - `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`:至少一个非空,分别建立部分唯一索引,保证同一用户在同一个服务下只绑定一个节点。 **章节来源** - [gateway.sql:1-107](file://sql/gateway.sql#L1-L107) ## 接口总览 所有接口基础路径为: ``` 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` | 强制刷新服务/节点缓存 | 运维脚本 | **章节来源** - [AdminController.java:25-99](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminController.java#L25-L99) ## 详细接口定义 ### 通用响应体 除明确说明外,管理接口统一返回 `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 | 存在依赖:仍有节点、仍有用户绑定等 | **章节来源** - [ApiResult.java:1-37](file://ai-gateway/src/main/java/com/zsjz/gateway/support/ApiResult.java#L1-L37) - [AdminApiException.java:1-30](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminApiException.java#L1-L30) ### GET /gateway/admin/status 查看网关当前加载的服务、节点及其健康状态。 - 方法:GET - URL:`/gateway/admin/status` - 请求头:需要 `x-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 示例: ```bash curl -H "x-token: YOUR_ADMIN_TOKEN" \ "http://localhost:8080/gateway/admin/status" ``` **章节来源** - [AdminController.java:31-38](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminController.java#L31-L38) - [AdminService.java:35-82](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminService.java#L35-L82) ### GET /gateway/admin/services 获取服务与节点快照,便于快速查看当前网关加载了哪些服务和节点。 - 方法:GET - URL:`/gateway/admin/services` - 请求头:需要 `x-token`,且为管理员 - 请求体:无 - 成功响应 `data`:与服务列表一致,即 `services` 数组。 curl 示例: ```bash curl -H "x-token: YOUR_ADMIN_TOKEN" \ "http://localhost:8080/gateway/admin/services" ``` **章节来源** - [AdminController.java:40-45](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminController.java#L40-L45) ### POST /gateway/admin/services 创建服务。 - 方法:POST - URL:`/gateway/admin/services` - 请求头:需要 `x-token`,且为管理员 - 请求体 JSON: | 字段 | 类型 | 必填 | 说明 | |---|---|---:|---| | `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 示例: ```bash 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" ``` **章节来源** - [AdminController.java:47-56](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminController.java#L47-L56) - [AdminService.java:84-103](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminService.java#L84-L103) ### PUT /gateway/admin/services/{serviceId} 更新服务。 - 方法:PUT - URL:`/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 示例: ```bash 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" ``` **章节来源** - [AdminController.java:58-67](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminController.java#L58-L67) - [AdminService.java:105-116](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminService.java#L105-L116) ### DELETE /gateway/admin/services/{serviceId} 删除服务。只有满足以下条件才能删除: - 服务存在; - 服务下没有节点; - 服务下没有用户绑定。 - 方法:DELETE - URL:`/gateway/admin/services/{serviceId}` - 成功响应:`code=200` - 错误响应: | HTTP 状态码 | code | 触发条件 | |---:|---:|---| | 404 | 404 | 服务不存在 | | 409 | 409 | 服务下仍有节点或用户绑定 | | 401/403 | 401/403 | 认证或权限失败 | curl 示例: ```bash curl -X DELETE -H "x-token: YOUR_ADMIN_TOKEN" \ "http://localhost:8080/gateway/admin/services/zsjz-ai" ``` **章节来源** - [AdminController.java:69-74](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminController.java#L69-L74) - [AdminService.java:118-131](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminService.java#L118-L131) ### POST /gateway/admin/services/{serviceId}/nodes 为服务新增节点。 - 方法:POST - URL:`/gateway/admin/services/{serviceId}/nodes` - 路径参数: | 参数 | 类型 | 说明 | |---|---|---| | `serviceId` | string | 目标服务 ID | - 请求体 JSON: | 字段 | 类型 | 必填 | 说明 | |---|---|---:|---| | `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 示例: ```bash 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" ``` **章节来源** - [AdminController.java:76-84](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminController.java#L76-L84) - [AdminService.java:133-153](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminService.java#L133-L153) ### PUT /gateway/admin/nodes/{nodeId} 更新节点。 - 方法:PUT - URL:`/gateway/admin/nodes/{nodeId}` - 路径参数: | 参数 | 类型 | 说明 | |---|---|---| | `nodeId` | string | 要更新的节点 ID | - 请求体:与创建节点相同。 - 成功响应:`code=200` - 错误响应: | HTTP 状态码 | code | 触发条件 | |---:|---:|---| | 400 | 400 | 同服务下 base-url 已存在 | | 404 | 404 | 节点不存在 | | 401/403 | 401/403 | 认证或权限失败 | curl 示例: ```bash 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" ``` **章节来源** - [AdminController.java:86-93](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminController.java#L86-L93) - [AdminService.java:155-168](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminService.java#L155-L168) ### DELETE /gateway/admin/nodes/{nodeId} 删除节点。前提是该节点没有被任何用户绑定。 - 方法:DELETE - URL:`/gateway/admin/nodes/{nodeId}` - 成功响应:`code=200` - 错误响应: | HTTP 状态码 | code | 触发条件 | |---:|---:|---| | 404 | 404 | 节点不存在 | | 409 | 409 | 节点上仍有用户绑定 | | 401/403 | 401/403 | 认证或权限失败 | curl 示例: ```bash curl -X DELETE -H "x-token: YOUR_ADMIN_TOKEN" \ "http://localhost:8080/gateway/admin/nodes/node-1" ``` **章节来源** - [AdminController.java:95-100](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminController.java#L95-L100) - [AdminService.java:170-179](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminService.java#L170-L179) ### POST /gateway/admin/nodes/{nodeId}/check 立即探测指定节点的健康状态,不等待定时任务。 - 方法:POST - URL:`/gateway/admin/nodes/{nodeId}/check` - 成功响应 `data`: | 字段 | 类型 | 说明 | |---|---|---| | `nodeId` | string | 节点 ID | | `state` | string | 探测后的健康状态 | - 错误响应: | HTTP 状态码 | code | 触发条件 | |---:|---:|---| | 404 | 404 | 节点不存在 | | 401/403 | 401/403 | 认证或权限失败 | curl 示例: ```bash curl -X POST -H "x-token: YOUR_ADMIN_TOKEN" \ "http://localhost:8080/gateway/admin/nodes/node-1/check" ``` **章节来源** - [AdminController.java:102-107](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminController.java#L102-L107) - [AdminService.java:181-198](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminService.java#L181-L198) ### GET /gateway/admin/routes 分页查询用户粘性路由。 - 方法:GET - URL:`/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 示例: ```bash curl -H "x-token: YOUR_ADMIN_TOKEN" \ "http://localhost:8080/gateway/admin/routes?serviceId=zsjz-ai&page=0&size=20" ``` **章节来源** - [AdminController.java:109-117](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminController.java#L109-L117) - [AdminService.java:200-200](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminService.java#L200-L200) ### POST /gateway/admin/routes/bind 绑定用户到节点。 - 方法:POST - URL:`/gateway/admin/routes/bind` - 请求体 JSON: | 字段 | 类型 | 必填 | 说明 | |---|---|---:|---| | `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 示例: ```bash 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" ``` **章节来源** - [AdminController.java:119-126](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminController.java#L119-L126) - [AdminService.java:200-200](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminService.java#L200-L200) ### DELETE /gateway/admin/routes/{id} 解绑用户路由。 - 方法:DELETE - URL:`/gateway/admin/routes/{id}` - 路径参数: | 参数 | 类型 | 说明 | |---|---|---| | `id` | long | 用户路由记录 ID | - 成功响应:`code=200` - 错误响应: | HTTP 状态码 | code | 触发条件 | |---:|---:|---| | 401/403 | 401/403 | 认证或权限失败 | curl 示例: ```bash curl -X DELETE -H "x-token: YOUR_ADMIN_TOKEN" \ "http://localhost:8080/gateway/admin/routes/1" ``` **章节来源** - [AdminController.java:128-133](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminController.java#L128-L133) - [AdminService.java:200-200](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminService.java#L200-L200) ### POST /gateway/admin/routes/import-existing 批量导入已有用户绑定。 - 方法:POST - URL:`/gateway/admin/routes/import-existing` - 请求体 JSON: | 字段 | 类型 | 必填 | 说明 | |---|---|---:|---| | `serviceId` | string | 是 | 目标服务 ID | | `nodeId` | string | 是 | 目标节点 ID | - 成功响应:`code=200` - 错误响应: | HTTP 状态码 | code | 触发条件 | |---:|---:|---| | 400 | 400 | 参数缺失或导入失败 | | 404 | 404 | 服务或节点不存在 | | 401/403 | 401/403 | 认证或权限失败 | curl 示例: ```bash 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" ``` **章节来源** - [AdminController.java:135-144](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminController.java#L135-L144) ### POST /gateway/admin/reload 强制刷新服务与节点缓存。 - 方法:POST - URL:`/gateway/admin/reload` - 成功响应:`code=200` - 错误响应: | HTTP 状态码 | code | 触发条件 | |---:|---:|---| | 401/403 | 401/403 | 认证或权限失败 | curl 示例: ```bash curl -X POST -H "x-token: YOUR_ADMIN_TOKEN" \ "http://localhost:8080/gateway/admin/reload" ``` **章节来源** - [AdminController.java:146-151](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminController.java#L146-L151) - [GatewayRegistry.java:72-96](file://ai-gateway/src/main/java/com/zsjz/gateway/registry/GatewayRegistry.java#L72-L96) ## 架构与调用关系 ### 管理接口调用链 ```mermaid 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 ``` **图示来源** - [AdminAuthWebFilter.java:1-90](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminAuthWebFilter.java#L1-L90) - [AdminController.java:25-151](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminController.java#L25-L151) - [AdminService.java:1-200](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminService.java#L1-L200) - [GatewayRegistry.java:1-116](file://ai-gateway/src/main/java/com/zsjz/gateway/registry/GatewayRegistry.java#L1-L116) ### 粘性路由与节点 DOWN 的关系 `StickyRouteFilter` 是普通业务请求的核心过滤器。它根据 `x-token` 找到用户绑定的节点后,会严格校验: 1. 节点是否存在; 2. 节点是否启用; 3. 节点是否健康; 4. 若任一条件失败,抛出 `StickyUnavailableException`,最终转为 HTTP 503。 ```mermaid 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 ``` **图示来源** - [StickyRouteFilter.java:1-180](file://ai-gateway/src/main/java/com/zsjz/gateway/route/StickyRouteFilter.java#L1-L180) - [StickyUnavailableException.java:1-25](file://ai-gateway/src/main/java/com/zsjz/gateway/support/StickyUnavailableException.java#L1-L25) **章节来源** - [StickyRouteFilter.java:1-180](file://ai-gateway/src/main/java/com/zsjz/gateway/route/StickyRouteFilter.java#L1-L180) - [StickyUnavailableException.java:1-25](file://ai-gateway/src/main/java/com/zsjz/gateway/support/StickyUnavailableException.java#L1-L25) ## 节点 DOWN 时返回 503 的排查指南 当某个用户绑定节点的状态变为 DOWN 时,该用户的后续请求会返回 503,而不是自动切换到其他健康节点。这是设计决策:案件数据、附件、Agent 记忆等落在各节点本地磁盘,切到其他机器会导致“当前案件数据源未打开或已释放”等问题。 ### 排查步骤 1. **确认用户是否绑定了节点** 调用: ```bash curl -H "x-token: YOUR_ADMIN_TOKEN" \ "http://localhost:8080/gateway/admin/routes?serviceId=zsjz-ai&keyword=USERNAME_OR_USERID&page=0&size=100" ``` 2. **查看服务与节点健康状态** 调用: ```bash curl -H "x-token: YOUR_ADMIN_TOKEN" \ "http://localhost:8080/gateway/admin/status" ``` 关注: - `services[].nodes[].state`; - `services[].nodes[].lastError`; - `services[].nodes[].lastCheckedAt`。 3. **手动探测节点** 调用: ```bash curl -X POST -H "x-token: YOUR_ADMIN_TOKEN" \ "http://localhost:8080/gateway/admin/nodes/NODE_ID/check" ``` 如果返回的 `state` 仍为 DOWN,说明节点健康检查失败。 4. **检查节点后端是否可达** 使用 `baseUrl` 拼接 `healthPath`,从网关所在机器发起探测。例如: ```bash curl -v "http://192.168.0.101:8980/js/a/sys/health" ``` 5. **判断是否需要迁移用户** 如果节点已下线,应先: - 将该节点上的用户解绑或重绑到新节点; - 确认新节点健康; - 再删除旧节点; - 最后让受影响用户重新登录,使粘性路由落到可用节点。 6. **注意:不要期望 503 后自动重试其他节点** 当前实现不会因绑定节点 DOWN 而切换。运维必须通过管理接口调整绑定。 ### 相关错误语义 | 现象 | 原因 | 处理方式 | |---|---|---| | 单个用户请求 503 | 该用户绑定的节点 DOWN | 修复节点或迁移用户绑定 | | 新用户登录 503 | 服务没有可用节点 | 先添加健康节点 | | 匿名请求 503 | 服务暂无健康节点 | 检查节点配置与健康检查 | | 用户未绑定节点 503 | 用户既无绑定也无默认节点 | 引导用户重新登录或预先绑定 | **章节来源** - [StickyRouteFilter.java:63-170](file://ai-gateway/src/main/java/com/zsjz/gateway/route/StickyRouteFilter.java#L63-L170) - [StickyUnavailableException.java:1-25](file://ai-gateway/src/main/java/com/zsjz/gateway/support/StickyUnavailableException.java#L1-L25) - [ai-gateway-user-sticky-routing.md:1-200](file://docs/design/ai-gateway-user-sticky-routing.md#L1-L200) ## 运维脚本集成建议 ### 推荐工作流 1. 启动时检查管理接口是否可用; 2. 使用 `GET /gateway/admin/status` 拉取当前服务与节点; 3. 若服务不存在,调用 `POST /gateway/admin/services` 创建; 4. 为每个后端实例调用 `POST /gateway/admin/services/{serviceId}/nodes` 添加节点; 5. 可选:调用 `POST /gateway/admin/nodes/{nodeId}/check` 验证节点健康; 6. 对于存量用户,调用 `POST /gateway/admin/routes/import-existing` 或 `POST /gateway/admin/routes/bind` 完成迁移; 7. 调用 `POST /gateway/admin/reload` 确保缓存刷新; 8. 再次调用 `GET /gateway/admin/status` 验证结果。 ### 幂等性建议 - 创建服务/节点前应先用 `GET /gateway/admin/services` 判断是否已存在; - 更新服务/节点时使用 `PUT`,并根据返回值判断是否真正变更; - 删除服务/节点前检查依赖; - 绑定用户路由前先查询是否已绑定; - 脚本应具备重试和失败告警机制。 ### 多网关实例说明 `GatewayRegistry` 支持多实例共享 PostgreSQL: - 写侧调用 `reload()` 后立即刷新自身缓存; - 其他实例通过定时任务轮询数据库,默认间隔 `gateway.registry-poll-interval`,默认 10 秒; - 配置变化时会发布 `RefreshRoutesEvent` 刷新路由缓存。 因此,单点写入后其他实例不会立即生效,最多延迟一个轮询周期。 **章节来源** - [GatewayRegistry.java:18-116](file://ai-gateway/src/main/java/com/zsjz/gateway/registry/GatewayRegistry.java#L18-L116) - [AdminService.java:1-200](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminService.java#L1-L200) ## 常见问题与故障定位 ### 问题一:管理接口返回 404 可能原因: - `gateway.admin.enabled=false`; - 外部反向代理没有暴露 `/gateway/admin`; - 请求目标不是网关端口 `8080`。 排查命令: ```bash curl -v "http://localhost:8080/gateway/admin/status" ``` **章节来源** - [AdminAuthWebFilter.java:40-55](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminAuthWebFilter.java#L40-L55) ### 问题二:管理接口返回 403 可能原因: - 来源 IP 不在 `gateway.admin.ip-whitelist`; - 当前用户不是管理员; - 当前用户状态被停用。 排查命令: ```bash curl -H "x-token: YOUR_ADMIN_TOKEN" \ "http://localhost:8080/gateway/admin/status" ``` **章节来源** - [AdminAuthWebFilter.java:56-85](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminAuthWebFilter.java#L56-L85) ### 问题三:创建服务报 path-prefix 冲突 说明另一个服务已经占用了相同的对外路径前缀。需要先修改冲突服务的路径前缀,或删除冲突服务。 **章节来源** - [AdminService.java:94-103](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminService.java#L94-L103) ### 问题四:删除服务或节点报 409 删除服务前必须: - 先删除其下所有节点; - 先解绑其下所有用户。 删除节点前必须: - 先解绑或重绑该节点上的用户。 **章节来源** - [AdminService.java:118-179](file://ai-gateway/src/main/java/com/zsjz/gateway/admin/AdminService.java#L118-L179) ### 问题五:用户请求 503 且无法自动恢复 这是粘性路由的预期行为。只要用户绑定的节点不可用,网关就不会自动切换。运维必须: - 恢复节点; - 或迁移用户绑定; - 或让用户重新登录。 **章节来源** - [StickyRouteFilter.java:120-170](file://ai-gateway/src/main/java/com/zsjz/gateway/route/StickyRouteFilter.java#L120-L170) - [ai-gateway-user-sticky-routing.md:1-200](file://docs/design/ai-gateway-user-sticky-routing.md#L1-L200) ## 结论 `AdminController` 暴露的 `/gateway/admin/**` 接口是清鉴 AI 网关的配置面,用于维护 `gateway_service`、`gateway_node` 和 `gateway_user_route` 数据。它们由运维人员或部署脚本调用,负责: - 注册和管理对外服务; - 管理后端节点; - 查看和维护用户粘性路由; - 手动探测节点健康; - 刷新网关路由缓存。 与普通业务请求不同,这些接口需要管理员身份,并受 IP 白名单、x-token、管理员权限等多重保护。当用户绑定的节点 DOWN 时,网关返回 503 且不自动切换,运维必须通过管理接口调整绑定或恢复节点。