Files
hzhub/docs/gateway-migration-plan.md
大壮 c2513849b4 feat: 添加ERP服务和系统服务,完善员工门户功能
## 新增服务模块

### 1. ERP服务 (hzhub-erp)
- 新增独立的ERP数据适配服务
- 支持SQL Server 2008 R2数据源
- 提供动态API配置管理系统
- 包含客户管理、销售数据等业务接口

### 2. 系统服务 (hzhub-system)
- 新增独立的系统管理服务
- 用户、角色、权限、部门、菜单管理
- 租户管理、操作日志、在线用户监控
- 工作流引擎(warm-flow)集成
- 企业微信审批同步功能

### 3. API网关 (hzhub-gateway)
- 新增Spring Cloud Gateway网关服务
- JWT认证、路由分发、限流熔断
- XSS防护、请求日志记录
- 统一入口端口8080

## 后台管理功能增强

### ERP动态API管理
- 新增动态API配置管理界面
- API测试、文档预览、统计监控
- 错误日志查看、缓存管理
- 从数据库表自动导入API配置

### 系统管理增强
- 企业微信配置管理
- 企业微信审批同步配置
- 部门和用户管理优化

## 员工门户功能完善

### 业务页面
- 审批中心:工作流审批、待办任务
- CRM管理:客户关系管理
- 经销商管理:经销商数据展示
- 供应链管理:采购、库存、销售
- BI报表:数据可视化分析
- ERP数据探索:SQL Server数据查询

### 个人中心
- 基本设置:个人信息管理
- 安全设置:密码修改、登录日志
- 锁屏功能:自动锁屏、手动锁屏

### 其他功能
- 标签页管理:多标签页导航
- 页面缓存:keepAlive缓存机制
- 会话超时:自动检测并提示

## 经销商门户

### 页面路由
- 新增经销商管理页面路由
- AI聊天界面完善

## 文档更新

- ERP API数据库初始化指南
- ERP API前端完整实现文档
- ERP API测试和验证指南
- Gateway路由迁移计划
- 项目配置文档更新

## 部署脚本

- 统一启动/停止/重启脚本
- Docker Compose配置优化
- Nginx配置文件更新

## 技术栈

- 后端: Spring Boot 3.5.8, Java 17
- 前端: Vue 3, TypeScript, Element Plus, Vben Admin
- 工作流: warm-flow 1.8.2
- 网关: Spring Cloud Gateway
- 数据库: MySQL 8.0, SQL Server 2008 R2
- 缓存: Redis 7
- 向量库: Weaviate 1.25.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 08:00:19 +00:00

544 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# HZHub-Gateway 迁移方案
## 一、现状分析
### 当前架构(去中心化)
```
前端门户 → Nginx/Vite代理 → 直连后端服务
admin → localhost:6039 (hzhub-ai)
employee → localhost:6039 (hzhub-ai) + localhost:8082 (hzhub-erp)
dealer → localhost:6039 (hzhub-ai) + localhost:8082 (hzhub-erp)
```
hzhub-gateway 已存在但未部署仅有基础路由配置3条路由 + CORS
### hzhub-ai 中可迁移的网关级功能
| 功能 | 位置 | 是否适合迁移 |
|------|------|:---:|
| Sa-Token JWT 验证 | `SaTokenConfig` + `SecurityConfig` | ✅ 是 — 全局统一鉴权 |
| XSS 过滤 | `XssFilter` (javax.servlet Filter) | ✅ 是 — WebFilter 可实现 |
| 请求/响应加密 | `CryptoFilter` (RSA 加解密) | ⚠️ 部分 — 需适配 WebFlux |
| 接口限流 | `@RateLimiter` + Redisson | ✅ 是 — 可用 Redis + RequestRateLimiter |
| 幂等性校验 | `@Idempotent` | ❌ 否 — 业务级,保留后端 |
| SSE 流式响应 | ChatController | ❌ 否 — 业务级,保留后端 |
| WebSocket 握手鉴权 | `PlusWebSocketInterceptor` | ❌ 否 — 协议不同,保留后端 |
| 数据权限拦截 | MyBatis 拦截器 | ❌ 否 — ORM 层,保留后端 |
### 核心挑战
1. **Servlet vs WebFlux**Gateway 基于 Spring WebFlux响应式hzhub-ai 的 Filter 是 Servlet 规范,不能直接复用,需改写为 `WebFilter`
2. **JWT 解析**Gateway 只需验证 JWT 合法性并透传用户信息,无需完整 Sa-Token 登录态
3. **加密适配**CryptoFilter 依赖 `ServletInputStream`,需改写为 `ServerHttpRequestDecorator`
---
## 二、目标架构
```
前端门户 → Gateway(:8080) → 后端服务
/ai/** → hzhub-ai(:8081)
/erp/** → hzhub-erp(:8082)
/system/** → hzhub-ai(:8081)
```
Gateway 承担:
- **统一鉴权**:验证 JWT Token解析用户信息注入请求头
- **XSS 防护**:对 POST/PUT 请求体进行 XSS 清洗
- **接口限流**:基于 Redis 的令牌桶限流
- **请求/响应加密**:按需解密请求、加密响应
- **路由转发**:按路径分发到对应服务
- **跨域处理**:全局 CORS
后端服务hzhub-ai / hzhub-erp承担
- **业务逻辑**CRUD、AI 对话、工作流等
- **数据权限**MyBatis 层的多租户数据隔离
- **幂等性**:防重复提交
- **SSE/WebSocket**:流式推送
### 服务间信任模型
```
外部请求 → Gateway → 后端服务
│ │
验证JWT 信任Gateway
透传的用户头
```
- Gateway 验证 JWT 通过后,将用户信息注入请求头:`X-User-Id``X-User-Name``X-Client-Id`
- 后端服务在 `SecurityConfig` 中配置:**来自 Gateway 的请求跳过 JWT 验证**
- 判断依据:检查请求头 `X-Gateway-Verified: true`(或共享内网 IP 白名单)
- 开发环境:保留直连后端的鉴权能力(双重模式)
---
## 三、实施步骤
### Phase 1: Gateway 基础增强(认证 + 路由)
**目标**:让 Gateway 能正常转发请求并验证 JWT
#### 3.1 修改 pom.xml
```xml
<!-- 已有spring-cloud-starter-gateway, spring-cloud-starter-loadbalancer -->
<!-- 新增 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-reactor-spring-boot3-starter</artifactId>
<version>1.39.0</version>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-jwt</artifactId>
<version>1.39.0</version>
</dependency>
```
#### 3.2 JWT 认证全局过滤器
```java
// src/main/java/org/hzhub/gateway/filter/AuthGlobalFilter.java
@Component
@RequiredArgsConstructor
public class AuthGlobalFilter implements GlobalFilter, Ordered {
private final SaTokenConfig saTokenConfig;
// 放行路径
private static final List<String> WHITE_LIST = List.of(
"/erp/test/", "/erp/customer/", "/actuator/"
);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getURI().getPath();
// 白名单放行
if (isWhiteList(path)) {
return chain.filter(exchange);
}
// 从 Header 获取 Token
String token = exchange.getRequest().getHeaders().getFirst("Authorization");
if (token == null || !token.startsWith("Bearer ")) {
return unauthorized(exchange, "未登录或登录已过期");
}
// 验证 JWT使用 sa-token-jwt
String jwtToken = token.substring(7);
try {
SaTokenInfo info = SaManager.getTokenInfo(jwtToken);
if (info == null || !info.isLogin()) {
return unauthorized(exchange, "登录已过期");
}
// 透传用户信息到后端
ServerHttpRequest mutated = exchange.getRequest().mutate()
.header("X-User-Id", info.getLoginId())
.header("X-Client-Id", exchange.getRequest().getHeaders().getFirst("ClientID"))
.header("X-Gateway-Verified", "true")
.build();
return chain.filter(exchange.mutate().request(mutated).build());
} catch (Exception e) {
return unauthorized(exchange, "Token 无效");
}
}
private boolean isWhiteList(String path) {
return WHITE_LIST.stream().anyMatch(path::startsWith);
}
private Mono<Void> unauthorized(ServerWebExchange exchange, String msg) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
byte[] bytes = ("{\"code\":401,\"msg\":\"" + msg + "\"}").getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
return exchange.getResponse().writeWith(Mono.just(buffer));
}
@Override
public int getOrder() { return -100; } // 最高优先级
}
```
#### 3.3 更新 application.yml
```yaml
server:
port: 8080
spring:
application:
name: hzhub-gateway
# Redis 配置(限流 + Token 验证共享)
data:
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:}
cloud:
gateway:
routes:
- id: hzhub-ai
uri: http://${AI_HOST:localhost}:${AI_PORT:8081}
predicates:
- Path=/ai/**,/system/**
filters:
- StripPrefix=1
- id: hzhub-erp
uri: http://${ERP_HOST:localhost}:${ERP_PORT:8082}
predicates:
- Path=/erp/**
filters:
- StripPrefix=1
globalcors:
cors-configurations:
'[/**]':
allowedOriginPatterns: "*"
allowedMethods: "*"
allowedHeaders: "*"
allowCredentials: true
maxAge: 3600
# Sa-TokenJWT 验证用,与后端服务共享 secret
sa-token:
jwt-secret-key: ${JWT_SECRET:abcdefghijklmnopqrstuvwxyz}
token-name: Authorization
# 日志
logging:
level:
org.hzhub.gateway: debug
```
---
### Phase 2: XSS 过滤 + 限流
#### 3.4 XSS 全局过滤器WebFlux 版)
```java
// src/main/java/org/hzhub/gateway/filter/XssGlobalFilter.java
@Component
public class XssGlobalFilter implements GlobalFilter, Ordered {
private static final List<String> EXCLUDE_PATHS = List.of("/ai/upload");
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getURI().getPath();
HttpMethod method = exchange.getRequest().getMethod();
// GET/DELETE 不处理;排除白名单
if (method == HttpMethod.GET || method == HttpMethod.DELETE
|| EXCLUDE_PATHS.stream().anyMatch(path::startsWith)) {
return chain.filter(exchange);
}
// 包装请求,对 Body 进行 XSS 清洗
ServerHttpRequest decorated = new ServerHttpRequestDecorator(exchange.getRequest()) {
@Override
public Flux<DataBuffer> getBody() {
return super.getBody().map(buffer -> {
byte[] bytes = new byte[buffer.readableByteCount()];
buffer.read(bytes);
String body = new String(bytes, StandardCharsets.UTF_8);
String cleaned = cleanXss(body);
byte[] cleanedBytes = cleaned.getBytes(StandardCharsets.UTF_8);
DataBufferFactory factory = buffer.factory();
DataBuffer newBuffer = factory.allocateBuffer(cleanedBytes.length);
newBuffer.write(cleanedBytes);
return newBuffer;
});
}
};
return chain.filter(exchange.mutate().request(decorated).build());
}
private String cleanXss(String value) {
if (value == null) return null;
return value
.replaceAll("<script>", "&lt;script&gt;")
.replaceAll("</script>", "&lt;/script&gt;")
.replaceAll("javascript:", "")
.replaceAll("on\\w+\\s*=", "");
}
@Override
public int getOrder() { return -50; }
}
```
#### 3.5 限流配置(基于 Redis
```yaml
# 在 application.yml 的 gateway 配置中添加
spring.cloud.gateway.default-filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10 # 每秒补充令牌数
redis-rate-limiter.burstCapacity: 20 # 令牌桶容量
key-resolver: "#{@ipKeyResolver}" # 按 IP 限流
```
```java
// src/main/java/org/hzhub/gateway/config/RateLimiterConfig.java
@Configuration
public class RateLimiterConfig {
/**
* 按 IP 地址限流
*/
@Bean
public KeyResolver ipKeyResolver() {
return exchange -> {
String ip = exchange.getRequest().getRemoteAddress() != null
? exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()
: "unknown";
return Mono.just("gateway:ratelimit:" + ip);
};
}
/**
* 按路径+IP 限流(更细粒度)
*/
@Bean
public KeyResolver pathIpKeyResolver() {
return exchange -> {
String path = exchange.getRequest().getURI().getPath();
String ip = exchange.getRequest().getRemoteAddress() != null
? exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()
: "unknown";
return Mono.just("gateway:ratelimit:" + path + ":" + ip);
};
}
}
```
---
### Phase 3: 后端服务适配
#### 3.6 hzhub-ai 的 SecurityConfig 改造
在 hzhub-ai 的 `SecurityConfig` 中新增:当请求来自 Gateway 时,跳过 JWT 验证。
```java
// hzhub-ai 的 SecurityConfig.java 中修改
private boolean isFromGateway(ServerHttpRequest request) {
String verified = request.getHeaders().getFirst("X-Gateway-Verified");
return "true".equals(verified);
}
```
或者更简单:在 Sa-Token 拦截器中添加检查:
```java
// 如果 Gateway 已验证,直接放行
if ("true".equals(request.getHeader("X-Gateway-Verified"))) {
return true;
}
```
#### 3.7 hzhub-erp 同理
在 hzhub-erp 的 `SecurityConfig.java` 中添加同样的 Gateway 信任逻辑。
---
### Phase 4: 前端路由切换
#### 3.8 hzhub-admin管理后台
修改 `.env.development`
```env
VITE_GLOB_API_URL=http://localhost:8080
```
修改 `vite.config.ts` 代理:
```typescript
'/api': {
target: 'http://localhost:8080', // 改为 Gateway
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, ''),
}
```
#### 3.9 hzhub-portal-employee员工门户
修改 `.env.development`
```env
VITE_API_URL=http://localhost:8080
```
`request.ts` 中已自动使用此 baseURL无需改动。
注意:原来直连 ERP 的请求(`/erp/customer/**`)也需经过 GatewayGateway 路由已覆盖 `/erp/**`
#### 3.10 hzhub-portal-dealer经销商门户
同 employee统一指向 Gateway。
---
### Phase 5: Docker Compose 集成
#### 3.11 新增 hzhub-gateway 服务
```yaml
# docker-compose.yml 新增
hzhub-gateway:
build:
context: ../hzhub-gateway
dockerfile: Dockerfile
container_name: hzhub-gateway
ports:
- "8080:8080"
environment:
SPRING_PROFILES_ACTIVE: prod
REDIS_HOST: hzhub-redis
REDIS_PORT: 6379
AI_HOST: hzhub-ai
AI_PORT: 6039
ERP_HOST: hzhub-erp
ERP_PORT: 8082
JWT_SECRET: ${JWT_SECRET:-abcdefghijklmnopqrstuvwxyz}
depends_on:
redis:
condition: service_healthy
hzhub-ai:
condition: service_started
hzhub-erp:
condition: service_started
networks:
- hzhub-network
```
#### 3.12 前端 Nginx 代理改为 Gateway
```nginx
# hzhub-portal-employee 的 Nginx 配置
# 原来: proxy_pass http://hzhub-ai:6039;
# 改为:
location /api/ {
proxy_pass http://hzhub-gateway:8080;
}
location /erp/ {
proxy_pass http://hzhub-gateway:8080;
}
```
#### 3.13 后端服务关闭外部端口暴露
```yaml
# docker-compose.yml 修改
hzhub-ai:
# ports:
# - "6039:6039" # 不再对外暴露,仅内网访问
hzhub-erp:
# ports:
# - "8082:8082" # 不再对外暴露,仅内网访问
```
---
## 四、文件变更清单
### 新增文件
| 文件 | 说明 |
|------|------|
| `hzhub-gateway/src/main/java/org/hzhub/gateway/filter/AuthGlobalFilter.java` | JWT 认证过滤器 |
| `hzhub-gateway/src/main/java/org/hzhub/gateway/filter/XssGlobalFilter.java` | XSS 过滤 |
| `hzhub-gateway/src/main/java/org/hzhub/gateway/config/RateLimiterConfig.java` | 限流 Key 解析器 |
| `hzhub-gateway/Dockerfile` | Gateway 容器构建 |
### 修改文件
| 文件 | 变更 |
|------|------|
| `hzhub-gateway/pom.xml` | 新增 sa-token-reactor、redis-reactive 依赖 |
| `hzhub-gateway/src/main/resources/application.yml` | 重写路由、Redis、Sa-Token 配置 |
| `hzhub-gateway/src/main/java/org/hzhub/HzhubGatewayApplication.java` | 无需改动 |
| `hzhub-ai/.../SecurityConfig.java` | 添加 Gateway 信任逻辑 |
| `hzhub-erp/.../SecurityConfig.java` | 添加 Gateway 信任逻辑 |
| `hzhub-admin/apps/web-antd/.env.development` | API 地址改为 Gateway |
| `hzhub-admin/apps/web-antd/vite.config.ts` | 代理目标改为 Gateway |
| `hzhub-portal-employee/.env.development` | VITE_API_URL 指向 Gateway |
| `hzhub-portal-dealer/.env.development` | 新增 API URL |
| `hzhub-deploy/docker-compose.yml` | 新增 Gateway 服务,调整网络 |
---
## 五、分阶段验证
### Phase 1 验证
```bash
# 1. 启动 Gateway
cd hzhub-gateway && mvn spring-boot:run
# 2. 测试路由转发
curl http://localhost:8080/erp/test/connection
# 应返回 SQL Server 连接信息
# 3. 测试鉴权(未登录)
curl http://localhost:8080/ai/chat/message
# 应返回 401
# 4. 测试鉴权(带 Token
curl -H "Authorization: Bearer <token>" http://localhost:8080/ai/chat/message
# 应正常返回结果
```
### Phase 2 验证
```bash
# 测试 XSS 过滤
curl -X POST http://localhost:8080/ai/xxx \
-H "Content-Type: application/json" \
-d '{"content": "<script>alert(1)</script>"}'
# 请求体应被清洗
# 测试限流(快速连续请求)
for i in {1..30}; do curl -s http://localhost:8080/ai/xxx; done
# 超过阈值应返回 429
```
### Phase 3 验证
```bash
# 前端登录后,所有请求走 Gateway
# 确认 AI 接口、ERP 接口均正常工作
# 确认 Token 过期后自动跳转登录页
```
---
## 六、风险与注意事项
| 风险 | 应对 |
|------|------|
| Sa-Token JWT 验证方式与后端不一致 | 统一使用 `StpLogicJwtForSimple`,共享 `jwt-secret-key` |
| Gateway 成为单点故障 | 后续可部署多实例 + Nginx 负载均衡 |
| SSE 流式响应经过 Gateway 可能超时 | 需在 Gateway 配置路由超时时间(默认无超时) |
| CORS 配置冲突 | Gateway 统一处理 CORS后端服务关闭 CORS |
| 开发环境直连 vs 生产走 Gateway | 后端服务保留双重模式:有 `X-Gateway-Verified` 跳过验证,否则自行验证 |
---
## 七、后续优化方向
1. **服务注册与发现**:引入 Nacos替代硬编码的服务地址
2. **熔断降级**:引入 Resilience4j对下游服务做熔断保护
3. **链路追踪**:引入 SkyWalking 或 Micrometer Trace
4. **灰度发布**:基于 Header 的蓝绿部署路由
5. **API 文档聚合**Gateway 聚合 Swagger/Knife4j 文档