修复系统概览数据不准确的查询。修复交叉评查意见列表的数量查询。优化全局消息提示的层级。优化提交意见进行局部更新。
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
# 动态客户端配置使用指南
|
||||
|
||||
## 🎯 概述
|
||||
|
||||
本项目支持通过Nginx代理自动识别客户端,无需手动设置环境变量。Nginx会在请求头中传递 `X-Client-ID`,前端应用会自动读取并使用对应的配置。
|
||||
|
||||
## 🔧 工作原理
|
||||
|
||||
```
|
||||
用户访问 → Nginx代理 → 添加X-Client-ID头部 → 前端应用 → 动态获取配置
|
||||
```
|
||||
|
||||
### 端口映射
|
||||
|
||||
| 端口 | 客户端ID | 说明 |
|
||||
|------|----------|------|
|
||||
| 5174 | client-a | 客户端A |
|
||||
| 5175 | client-b | 客户端B |
|
||||
| 5176 | client-c | 客户端C |
|
||||
| 5177 | client-d | 客户端D |
|
||||
|
||||
## 📁 相关文件
|
||||
|
||||
### 核心配置文件
|
||||
- `app/config/api-config.ts` - API配置管理(已更新支持动态配置)
|
||||
- `nginx-ubuntu-optimized.conf` - Nginx配置文件
|
||||
|
||||
### 新增工具文件
|
||||
- `app/utils/client-detection.ts` - 客户端检测工具函数
|
||||
- `app/examples/dynamic-config-usage.ts` - 使用示例
|
||||
- `app/routes/test.client-config.tsx` - 配置测试页面
|
||||
|
||||
## 🚀 使用方法
|
||||
|
||||
### 1. 在Remix Loader中使用
|
||||
|
||||
```typescript
|
||||
import type { LoaderFunctionArgs } from '@remix-run/node';
|
||||
import { getApiConfig, getApiBaseUrl } from '~/config/api-config';
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
// 方法1: 获取完整配置
|
||||
const config = getApiConfig(request);
|
||||
|
||||
// 方法2: 获取特定配置项
|
||||
const baseUrl = getApiBaseUrl(request);
|
||||
|
||||
// 使用动态配置进行API调用
|
||||
const response = await fetch(`${baseUrl}/api/data`);
|
||||
const data = await response.json();
|
||||
|
||||
return { data };
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 在Action中使用
|
||||
|
||||
```typescript
|
||||
import type { ActionFunctionArgs } from '@remix-run/node';
|
||||
import { getUploadUrl } from '~/config/api-config';
|
||||
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
const uploadUrl = getUploadUrl(request);
|
||||
const formData = await request.formData();
|
||||
|
||||
const response = await fetch(uploadUrl, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
```
|
||||
|
||||
### 3. OAuth重定向URL生成
|
||||
|
||||
```typescript
|
||||
import { getOAuthConfig } from '~/config/api-config';
|
||||
|
||||
export function generateOAuthUrl(request: Request): string {
|
||||
const oauthConfig = getOAuthConfig(request);
|
||||
|
||||
const params = new URLSearchParams({
|
||||
client_id: oauthConfig.clientId,
|
||||
redirect_uri: oauthConfig.redirectUri,
|
||||
response_type: 'code'
|
||||
});
|
||||
|
||||
return `${oauthConfig.serverUrl}/oauth/authorize?${params.toString()}`;
|
||||
}
|
||||
```
|
||||
|
||||
## 🧪 测试验证
|
||||
|
||||
### 1. 访问测试页面
|
||||
|
||||
通过不同端口访问测试页面来验证配置:
|
||||
|
||||
```bash
|
||||
# 客户端A
|
||||
http://localhost:5174/test/client-config
|
||||
|
||||
# 客户端B
|
||||
http://localhost:5175/test/client-config
|
||||
|
||||
# 客户端C
|
||||
http://localhost:5176/test/client-config
|
||||
|
||||
# 客户端D
|
||||
http://localhost:5177/test/client-config
|
||||
```
|
||||
|
||||
### 2. 检查配置状态
|
||||
|
||||
测试页面会显示:
|
||||
- ✅ Nginx代理是否正常工作
|
||||
- ✅ 客户端识别是否成功
|
||||
- ✅ 配置匹配是否正确
|
||||
|
||||
### 3. 验证请求头
|
||||
|
||||
在浏览器开发者工具中检查网络请求,确认包含:
|
||||
- `X-Client-ID`: 对应的客户端标识
|
||||
- `X-Original-Port`: 原始端口号
|
||||
- `X-Forwarded-Port`: 转发端口号
|
||||
|
||||
## 🔍 调试信息
|
||||
|
||||
### 查看Nginx日志
|
||||
|
||||
```bash
|
||||
# 查看客户端A的访问日志
|
||||
sudo tail -f /var/log/nginx/client-a-access.log
|
||||
|
||||
# 查看客户端D的访问日志
|
||||
sudo tail -f /var/log/nginx/client-d-access.log
|
||||
```
|
||||
|
||||
### 使用调试工具
|
||||
|
||||
```typescript
|
||||
import { detectClientFromRequest, getRequestDebugInfo } from '~/utils/client-detection';
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const clientId = detectClientFromRequest(request);
|
||||
const debugInfo = getRequestDebugInfo(request);
|
||||
|
||||
console.log('🔧 调试信息:', { clientId, debugInfo });
|
||||
|
||||
// ... 其他逻辑
|
||||
}
|
||||
```
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
### 1. 兼容性
|
||||
- 保持了原有的静态配置导出,确保现有代码不受影响
|
||||
- 新的动态配置函数是可选的,可以逐步迁移
|
||||
|
||||
### 2. 环境要求
|
||||
- 需要Nginx正确配置并运行
|
||||
- 确保 `nginx-ubuntu-optimized.conf` 配置文件已应用
|
||||
|
||||
### 3. 错误处理
|
||||
- 如果无法检测到客户端ID,会回退到默认值 'main'
|
||||
- 如果Nginx未正确配置,仍可通过环境变量设置客户端ID
|
||||
|
||||
## 🔄 迁移指南
|
||||
|
||||
### 从环境变量迁移到动态配置
|
||||
|
||||
**之前(需要设置环境变量):**
|
||||
```bash
|
||||
# .env文件
|
||||
NEXT_PUBLIC_CLIENT_ID=client-d
|
||||
```
|
||||
|
||||
**现在(自动检测):**
|
||||
```typescript
|
||||
// 在loader中
|
||||
const config = getApiConfig(request); // 自动从请求头获取客户端ID
|
||||
```
|
||||
|
||||
### 更新现有代码
|
||||
|
||||
1. **替换静态配置调用:**
|
||||
```typescript
|
||||
// 旧方式
|
||||
import { API_BASE_URL } from '~/config/api-config';
|
||||
|
||||
// 新方式
|
||||
import { getApiBaseUrl } from '~/config/api-config';
|
||||
const baseUrl = getApiBaseUrl(request);
|
||||
```
|
||||
|
||||
2. **在loader/action中传递request:**
|
||||
```typescript
|
||||
// 确保所有需要配置的地方都能访问到request对象
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const config = getApiConfig(request);
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## 🎉 优势
|
||||
|
||||
1. **无需手动配置** - 不再需要为每个客户端设置环境变量
|
||||
2. **自动识别** - 通过访问端口自动识别客户端
|
||||
3. **动态切换** - 同一个应用实例支持多个客户端
|
||||
4. **向后兼容** - 现有代码无需修改即可继续工作
|
||||
5. **易于调试** - 提供完整的调试信息和测试页面
|
||||
|
||||
现在你可以直接通过 `http://localhost:5177` 访问应用,系统会自动识别为 `client-d` 并使用对应的配置!
|
||||
@@ -1,279 +0,0 @@
|
||||
# 多客户端部署方案说明
|
||||
|
||||
## 概述
|
||||
|
||||
本方案实现了基于PM2和Nginx的多客户端部署架构,允许不同地区的客户通过不同端口访问同一个应用服务。
|
||||
|
||||
## 架构设计
|
||||
|
||||
```
|
||||
客户端A (51701) ──┐
|
||||
客户端B (51702) ──┼── Nginx反向代理 ──→ 主服务 (51703)
|
||||
客户端C (51704) ──┘
|
||||
```
|
||||
|
||||
### 端口分配
|
||||
|
||||
- **主服务**: `10.79.97.17:51703` - 核心应用服务
|
||||
- **客户端A**: `10.79.97.17:51701` - 地区A客户访问
|
||||
- **客户端B**: `10.79.97.17:51702` - 地区B客户访问
|
||||
- **客户端C**: `10.79.97.17:51704` - 地区C客户访问
|
||||
|
||||
## 文件说明
|
||||
|
||||
### 1. ecosystem.config.cjs
|
||||
PM2部署配置文件,定义了4个应用实例:
|
||||
- `docreview-main`: 主服务 (端口51703)
|
||||
- `docreview-client-a`: 客户端A代理 (端口51701)
|
||||
- `docreview-client-b`: 客户端B代理 (端口51702)
|
||||
- `docreview-client-c`: 客户端C代理 (端口51704)
|
||||
|
||||
### 2. api-config.ts
|
||||
应用配置文件,支持根据`CLIENT_ID`环境变量加载不同客户端配置:
|
||||
- 默认配置 (main)
|
||||
- 客户端A配置 (client-a)
|
||||
- 客户端B配置 (client-b)
|
||||
- 客户端C配置 (client-c)
|
||||
|
||||
### 3. nginx-multi-client.conf
|
||||
Nginx反向代理配置文件,为每个客户端端口配置独立的代理规则。
|
||||
|
||||
### 4. 部署脚本
|
||||
- `deploy-multi-client.sh`: Linux/macOS部署脚本
|
||||
- `deploy-multi-client.bat`: Windows部署脚本
|
||||
|
||||
## 部署步骤
|
||||
|
||||
### Windows环境部署
|
||||
|
||||
1. **检查环境依赖**
|
||||
```bash
|
||||
node --version
|
||||
npm --version
|
||||
pm2 --version
|
||||
```
|
||||
|
||||
2. **使用部署脚本**
|
||||
```bash
|
||||
# 完整部署
|
||||
deploy-multi-client.bat deploy
|
||||
|
||||
# 仅构建项目
|
||||
deploy-multi-client.bat build
|
||||
|
||||
# 仅部署PM2
|
||||
deploy-multi-client.bat pm2
|
||||
|
||||
# 检查状态
|
||||
deploy-multi-client.bat status
|
||||
```
|
||||
|
||||
3. **手动配置Nginx** (Windows)
|
||||
- 安装Nginx for Windows
|
||||
- 将`nginx-multi-client.conf`内容添加到nginx配置中
|
||||
- 重启Nginx服务
|
||||
|
||||
### Linux/macOS环境部署
|
||||
|
||||
1. **使用部署脚本**
|
||||
```bash
|
||||
chmod +x deploy-multi-client.sh
|
||||
|
||||
# 完整部署(包含Nginx配置)
|
||||
./deploy-multi-client.sh deploy
|
||||
|
||||
# 检查状态
|
||||
./deploy-multi-client.sh status
|
||||
```
|
||||
|
||||
### 手动部署步骤
|
||||
|
||||
1. **构建项目**
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
2. **启动PM2应用**
|
||||
```bash
|
||||
pm2 start ecosystem.config.cjs
|
||||
pm2 save
|
||||
pm2 startup
|
||||
```
|
||||
|
||||
3. **配置Nginx**
|
||||
```bash
|
||||
# 复制配置文件
|
||||
sudo cp nginx-multi-client.conf /etc/nginx/sites-available/docreview-multi-client
|
||||
sudo ln -s /etc/nginx/sites-available/docreview-multi-client /etc/nginx/sites-enabled/
|
||||
|
||||
# 测试配置
|
||||
sudo nginx -t
|
||||
|
||||
# 重载配置
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 客户端特定配置
|
||||
|
||||
每个客户端可以有独立的配置,在`api-config.ts`中定义:
|
||||
|
||||
```typescript
|
||||
const clientConfigs = {
|
||||
'client-a': {
|
||||
baseUrl: 'http://10.79.97.17:51701/api',
|
||||
uploadUrl: 'http://10.79.97.17:51701/api/upload',
|
||||
oauth: {
|
||||
serverUrl: 'http://10.79.97.17:51701/oauth',
|
||||
clientId: 'client-a-id',
|
||||
// ... 其他配置
|
||||
}
|
||||
},
|
||||
// ... 其他客户端配置
|
||||
};
|
||||
```
|
||||
|
||||
### 环境变量
|
||||
|
||||
每个PM2应用实例都有独立的环境变量:
|
||||
|
||||
- `CLIENT_ID`: 客户端标识 (main, client-a, client-b, client-c)
|
||||
- `PROXY_TARGET`: 代理目标地址 (仅客户端实例)
|
||||
- `PORT`: 监听端口
|
||||
|
||||
## 监控和管理
|
||||
|
||||
### PM2管理命令
|
||||
|
||||
```bash
|
||||
# 查看所有应用状态
|
||||
pm2 status
|
||||
|
||||
# 查看特定应用日志
|
||||
pm2 logs docreview-main
|
||||
pm2 logs docreview-client-a
|
||||
|
||||
# 重启应用
|
||||
pm2 restart docreview-main
|
||||
pm2 restart all
|
||||
|
||||
# 停止应用
|
||||
pm2 stop docreview-main
|
||||
pm2 stop all
|
||||
|
||||
# 删除应用
|
||||
pm2 delete docreview-main
|
||||
pm2 delete all
|
||||
```
|
||||
|
||||
### 日志文件位置
|
||||
|
||||
**PM2日志**:
|
||||
- 主服务: `logs/main-out.log`, `logs/main-error.log`
|
||||
- 客户端A: `logs/client-a-out.log`, `logs/client-a-error.log`
|
||||
- 客户端B: `logs/client-b-out.log`, `logs/client-b-error.log`
|
||||
- 客户端C: `logs/client-c-out.log`, `logs/client-c-error.log`
|
||||
|
||||
**Nginx日志**:
|
||||
- 客户端A: `/var/log/nginx/client-a-access.log`, `/var/log/nginx/client-a-error.log`
|
||||
- 客户端B: `/var/log/nginx/client-b-access.log`, `/var/log/nginx/client-b-error.log`
|
||||
- 客户端C: `/var/log/nginx/client-c-access.log`, `/var/log/nginx/client-c-error.log`
|
||||
|
||||
### 健康检查
|
||||
|
||||
每个客户端端口都提供健康检查接口:
|
||||
|
||||
```bash
|
||||
# 检查各端口状态
|
||||
curl http://10.79.97.17:51701/health # 客户端A
|
||||
curl http://10.79.97.17:51702/health # 客户端B
|
||||
curl http://10.79.97.17:51703/health # 主服务
|
||||
curl http://10.79.97.17:51704/health # 客户端C
|
||||
```
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **端口被占用**
|
||||
```bash
|
||||
# 查看端口占用
|
||||
netstat -tlnp | grep :51703
|
||||
|
||||
# 杀死占用进程
|
||||
kill -9 <PID>
|
||||
```
|
||||
|
||||
2. **PM2应用启动失败**
|
||||
```bash
|
||||
# 查看详细错误日志
|
||||
pm2 logs docreview-main --lines 50
|
||||
|
||||
# 重新加载配置
|
||||
pm2 reload ecosystem.config.cjs
|
||||
```
|
||||
|
||||
3. **Nginx代理失败**
|
||||
```bash
|
||||
# 检查nginx配置
|
||||
sudo nginx -t
|
||||
|
||||
# 查看nginx错误日志
|
||||
sudo tail -f /var/log/nginx/error.log
|
||||
```
|
||||
|
||||
4. **客户端配置不生效**
|
||||
- 检查`CLIENT_ID`环境变量是否正确设置
|
||||
- 确认`api-config.ts`中的客户端配置是否正确
|
||||
- 重启相关PM2应用
|
||||
|
||||
### 调试模式
|
||||
|
||||
启用调试模式查看详细日志:
|
||||
|
||||
```bash
|
||||
# 设置调试环境变量
|
||||
export DEBUG=*
|
||||
|
||||
# 重启应用
|
||||
pm2 restart all
|
||||
```
|
||||
|
||||
## 扩展和优化
|
||||
|
||||
### 添加新客户端
|
||||
|
||||
1. 在`ecosystem.config.cjs`中添加新的应用配置
|
||||
2. 在`api-config.ts`中添加客户端特定配置
|
||||
3. 在`nginx-multi-client.conf`中添加新的server块
|
||||
4. 重新部署应用
|
||||
|
||||
### 性能优化
|
||||
|
||||
1. **启用Nginx缓存**
|
||||
2. **配置负载均衡**
|
||||
3. **启用Gzip压缩**
|
||||
4. **配置SSL/TLS**
|
||||
|
||||
### 安全加固
|
||||
|
||||
1. **配置防火墙规则**
|
||||
2. **启用访问控制**
|
||||
3. **配置SSL证书**
|
||||
4. **设置访问频率限制**
|
||||
|
||||
## 联系支持
|
||||
|
||||
如果在部署过程中遇到问题,请检查:
|
||||
1. 系统依赖是否完整安装
|
||||
2. 端口是否被其他服务占用
|
||||
3. 配置文件语法是否正确
|
||||
4. 日志文件中的错误信息
|
||||
|
||||
部署完成后,可以通过以下地址访问不同客户端:
|
||||
- 客户端A: http://10.79.97.17:51701
|
||||
- 客户端B: http://10.79.97.17:51702
|
||||
- 客户端C: http://10.79.97.17:51704
|
||||
- 主服务: http://10.79.97.17:51703
|
||||
@@ -0,0 +1,429 @@
|
||||
# Nginx配置分析与优化建议
|
||||
|
||||
## 原始配置分析
|
||||
|
||||
### 当前配置文件:`nginx.conf`
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 5174;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
proxy_pass http://172.16.0.34:5173;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
# ... 其他三个相同的server块
|
||||
```
|
||||
|
||||
## 问题识别与分析
|
||||
|
||||
### 🔴 严重问题
|
||||
|
||||
#### 1. **缺少客户端标识机制**
|
||||
**问题**:所有端口代理到同一个后端,但没有传递客户端标识信息
|
||||
**影响**:应用无法区分来自不同端口的请求,无法实现多客户端配置切换
|
||||
**解决方案**:
|
||||
```nginx
|
||||
# 添加客户端标识变量和头部
|
||||
set $client_id "client-a"; # 每个server块设置不同的client_id
|
||||
proxy_set_header X-Client-ID $client_id;
|
||||
proxy_set_header X-Original-Port $server_port;
|
||||
```
|
||||
|
||||
#### 2. **缺少上游服务器配置**
|
||||
**问题**:直接在proxy_pass中硬编码后端地址
|
||||
**影响**:无法实现负载均衡、健康检查、连接池等高级功能
|
||||
**解决方案**:
|
||||
```nginx
|
||||
upstream vite_dev_server {
|
||||
server 172.16.0.34:5173;
|
||||
keepalive 32; # 连接池
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. **缺少开发环境特殊配置**
|
||||
**问题**:没有配置WebSocket支持,影响Vite热重载功能
|
||||
**影响**:开发时热重载可能不工作
|
||||
**解决方案**:
|
||||
```nginx
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_http_version 1.1;
|
||||
```
|
||||
|
||||
### 🟡 重要问题
|
||||
|
||||
#### 4. **缺少日志配置**
|
||||
**问题**:没有配置访问日志和错误日志
|
||||
**影响**:无法监控和调试请求
|
||||
**解决方案**:
|
||||
```nginx
|
||||
access_log /var/log/nginx/client-a-access.log;
|
||||
error_log /var/log/nginx/client-a-error.log warn;
|
||||
```
|
||||
|
||||
#### 5. **缺少超时配置**
|
||||
**问题**:使用默认超时设置,可能导致请求超时
|
||||
**影响**:长时间请求可能被意外中断
|
||||
**解决方案**:
|
||||
```nginx
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_send_timeout 30s;
|
||||
proxy_read_timeout 30s;
|
||||
```
|
||||
|
||||
#### 6. **缺少缓冲控制**
|
||||
**问题**:默认启用代理缓冲,影响实时性
|
||||
**影响**:开发环境下可能影响实时更新
|
||||
**解决方案**:
|
||||
```nginx
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_request_buffering off;
|
||||
```
|
||||
|
||||
### 🟢 改进建议
|
||||
|
||||
#### 7. **缺少健康检查端点**
|
||||
**建议**:添加健康检查端点便于监控
|
||||
**好处**:可以快速检测服务状态
|
||||
**实现**:
|
||||
```nginx
|
||||
location /health {
|
||||
access_log off;
|
||||
return 200 "Client A (Port 5174) - OK\n";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
```
|
||||
|
||||
#### 8. **缺少CORS配置**
|
||||
**建议**:添加CORS头部支持跨域请求
|
||||
**好处**:支持前端开发时的跨域请求
|
||||
**实现**:
|
||||
```nginx
|
||||
add_header Access-Control-Allow-Origin * always;
|
||||
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
|
||||
```
|
||||
|
||||
#### 9. **缺少文件上传大小限制**
|
||||
**建议**:设置合理的文件上传大小限制
|
||||
**好处**:防止大文件攻击,提高安全性
|
||||
**实现**:
|
||||
```nginx
|
||||
client_max_body_size 100M;
|
||||
```
|
||||
|
||||
#### 10. **缺少安全头部**
|
||||
**建议**:添加基本的安全头部
|
||||
**好处**:提高应用安全性
|
||||
**实现**:
|
||||
```nginx
|
||||
add_header X-Frame-Options SAMEORIGIN always;
|
||||
add_header X-Content-Type-Options nosniff always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
```
|
||||
|
||||
## 详细优化方案
|
||||
|
||||
### 1. 客户端标识传递机制
|
||||
|
||||
**原理**:通过Nginx变量和请求头传递客户端信息
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 5174;
|
||||
set $client_id "client-a"; # 设置客户端标识
|
||||
|
||||
location / {
|
||||
proxy_pass http://vite_dev_server;
|
||||
# 传递客户端标识
|
||||
proxy_set_header X-Client-ID $client_id;
|
||||
proxy_set_header X-Original-Port $server_port;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**应用端接收**:
|
||||
```typescript
|
||||
// 在Remix loader或action中
|
||||
export const loader: LoaderFunction = async ({ request }) => {
|
||||
const clientId = request.headers.get('X-Client-ID') || 'main';
|
||||
const config = getConfigForClient(clientId);
|
||||
return json({ config });
|
||||
};
|
||||
```
|
||||
|
||||
### 2. 上游服务器配置优化
|
||||
|
||||
**连接池配置**:
|
||||
```nginx
|
||||
upstream vite_dev_server {
|
||||
server 172.16.0.34:5173;
|
||||
keepalive 32; # 保持32个长连接
|
||||
keepalive_requests 100; # 每个连接最多处理100个请求
|
||||
keepalive_timeout 60s; # 连接空闲60秒后关闭
|
||||
}
|
||||
```
|
||||
|
||||
**健康检查**(需要nginx-plus或第三方模块):
|
||||
```nginx
|
||||
upstream vite_dev_server {
|
||||
server 172.16.0.34:5173 max_fails=3 fail_timeout=30s;
|
||||
# 3次失败后标记为不可用,30秒后重试
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 开发环境特殊配置
|
||||
|
||||
**WebSocket支持**:
|
||||
```nginx
|
||||
location / {
|
||||
proxy_pass http://vite_dev_server;
|
||||
|
||||
# WebSocket支持(Vite HMR必需)
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_http_version 1.1;
|
||||
|
||||
# 禁用缓冲以支持实时更新
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_request_buffering off;
|
||||
}
|
||||
```
|
||||
|
||||
**API请求特殊处理**:
|
||||
```nginx
|
||||
location /api/ {
|
||||
proxy_pass http://vite_dev_server;
|
||||
|
||||
# API请求不需要WebSocket支持
|
||||
proxy_set_header X-Client-ID $client_id;
|
||||
|
||||
# API请求超时配置
|
||||
proxy_connect_timeout 10s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 日志配置优化
|
||||
|
||||
**自定义日志格式**:
|
||||
```nginx
|
||||
log_format client_access '$remote_addr - $remote_user [$time_local] '
|
||||
'"$request" $status $body_bytes_sent '
|
||||
'"$http_referer" "$http_user_agent" '
|
||||
'client_id="$client_id" original_port="$server_port" '
|
||||
'response_time=$request_time';
|
||||
|
||||
access_log /var/log/nginx/client-a-access.log client_access;
|
||||
```
|
||||
|
||||
**日志轮转配置**:
|
||||
```bash
|
||||
# /etc/logrotate.d/nginx-clients
|
||||
/var/log/nginx/client-*-access.log {
|
||||
daily
|
||||
missingok
|
||||
rotate 52
|
||||
compress
|
||||
delaycompress
|
||||
notifempty
|
||||
create 644 www-data www-data
|
||||
postrotate
|
||||
systemctl reload nginx
|
||||
endscript
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 性能优化配置
|
||||
|
||||
**缓存配置**:
|
||||
```nginx
|
||||
# 静态资源缓存
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
|
||||
proxy_pass http://vite_dev_server;
|
||||
proxy_cache_valid 200 1h;
|
||||
add_header X-Cache-Status $upstream_cache_status;
|
||||
}
|
||||
```
|
||||
|
||||
**压缩配置**:
|
||||
```nginx
|
||||
# 在http块中
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_types
|
||||
text/plain
|
||||
text/css
|
||||
application/json
|
||||
application/javascript
|
||||
text/xml
|
||||
application/xml
|
||||
application/xml+rss
|
||||
text/javascript;
|
||||
```
|
||||
|
||||
### 6. 安全配置
|
||||
|
||||
**基础安全头部**:
|
||||
```nginx
|
||||
# 开发环境相对宽松的安全配置
|
||||
add_header X-Frame-Options SAMEORIGIN always;
|
||||
add_header X-Content-Type-Options nosniff always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
```
|
||||
|
||||
**请求限制**:
|
||||
```nginx
|
||||
# 限制请求频率(可选,开发环境通常不需要)
|
||||
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
|
||||
|
||||
location /api/ {
|
||||
limit_req zone=api burst=20 nodelay;
|
||||
proxy_pass http://vite_dev_server;
|
||||
}
|
||||
```
|
||||
|
||||
## 配置模板对比
|
||||
|
||||
### 原始配置(简化版)
|
||||
```nginx
|
||||
server {
|
||||
listen 5174;
|
||||
server_name localhost;
|
||||
location / {
|
||||
proxy_pass http://172.16.0.34:5173;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 优化后配置(完整版)
|
||||
```nginx
|
||||
upstream vite_dev_server {
|
||||
server 172.16.0.34:5173;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
log_format client_access '$remote_addr - $remote_user [$time_local] '
|
||||
'"$request" $status $body_bytes_sent '
|
||||
'client_id="$client_id" port="$server_port"';
|
||||
|
||||
server {
|
||||
listen 5174;
|
||||
server_name localhost 127.0.0.1;
|
||||
|
||||
set $client_id "client-a";
|
||||
|
||||
access_log /var/log/nginx/client-a-access.log client_access;
|
||||
error_log /var/log/nginx/client-a-error.log warn;
|
||||
|
||||
location / {
|
||||
proxy_pass http://vite_dev_server;
|
||||
|
||||
# 基础代理头部
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# 客户端标识头部
|
||||
proxy_set_header X-Client-ID $client_id;
|
||||
proxy_set_header X-Original-Port $server_port;
|
||||
|
||||
# WebSocket支持
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_http_version 1.1;
|
||||
|
||||
# 超时配置
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_send_timeout 30s;
|
||||
proxy_read_timeout 30s;
|
||||
|
||||
# 缓冲控制
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
|
||||
# 文件上传限制
|
||||
client_max_body_size 100M;
|
||||
}
|
||||
|
||||
location /health {
|
||||
access_log off;
|
||||
return 200 "Client A - OK";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 实施优先级
|
||||
|
||||
### 🔴 高优先级(必须实施)
|
||||
1. **客户端标识传递** - 核心功能需求
|
||||
2. **WebSocket支持** - 开发体验必需
|
||||
3. **上游服务器配置** - 性能和可维护性
|
||||
|
||||
### 🟡 中优先级(建议实施)
|
||||
4. **日志配置** - 监控和调试
|
||||
5. **超时配置** - 稳定性
|
||||
6. **健康检查** - 运维便利
|
||||
|
||||
### 🟢 低优先级(可选实施)
|
||||
7. **CORS配置** - 开发便利
|
||||
8. **安全头部** - 安全加固
|
||||
9. **缓存配置** - 性能优化
|
||||
|
||||
## 测试验证
|
||||
|
||||
### 功能测试
|
||||
```bash
|
||||
# 1. 测试客户端标识传递
|
||||
curl -H "Accept: application/json" http://localhost:5174/api/config
|
||||
|
||||
# 2. 测试健康检查
|
||||
curl http://localhost:5174/health
|
||||
|
||||
# 3. 测试WebSocket(需要浏览器)
|
||||
# 访问 http://localhost:5174 并检查开发者工具中的WebSocket连接
|
||||
```
|
||||
|
||||
### 性能测试
|
||||
```bash
|
||||
# 使用ab进行简单压力测试
|
||||
ab -n 1000 -c 10 http://localhost:5174/
|
||||
|
||||
# 使用wrk进行更详细的测试
|
||||
wrk -t12 -c400 -d30s http://localhost:5174/
|
||||
```
|
||||
|
||||
### 日志验证
|
||||
```bash
|
||||
# 检查日志是否正确记录客户端信息
|
||||
sudo tail -f /var/log/nginx/client-a-access.log | grep client_id
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
原始nginx配置虽然能够实现基本的反向代理功能,但缺少多客户端支持的关键特性。通过以上优化,可以实现:
|
||||
|
||||
1. **完整的多客户端支持** - 通过客户端标识传递
|
||||
2. **更好的开发体验** - WebSocket和热重载支持
|
||||
3. **增强的监控能力** - 详细的日志记录
|
||||
4. **提升的性能表现** - 连接池和缓存优化
|
||||
5. **基础的安全保障** - 安全头部和请求限制
|
||||
|
||||
这些改进为在Ubuntu环境中测试多客户端功能提供了完整的基础设施支持。
|
||||
@@ -0,0 +1,422 @@
|
||||
# Ubuntu环境下Nginx多客户端测试指南
|
||||
|
||||
## 概述
|
||||
|
||||
本指南详细说明如何在Ubuntu环境中配置和测试Nginx反向代理多客户端功能,实现根据不同端口动态切换API配置的能力。
|
||||
|
||||
## 架构说明
|
||||
|
||||
```
|
||||
客户端访问端口 Nginx代理 开发服务器
|
||||
5174 (client-a) ──→ 反向代理 ──→ 172.16.0.34:5173
|
||||
5175 (client-b) ──→ 反向代理 ──→ 172.16.0.34:5173
|
||||
5176 (client-c) ──→ 反向代理 ──→ 172.16.0.34:5173
|
||||
5177 (client-d) ──→ 反向代理 ──→ 172.16.0.34:5173
|
||||
```
|
||||
|
||||
每个端口通过 `X-Client-ID` 头部传递客户端标识,应用根据此标识动态选择对应的API配置。
|
||||
|
||||
## 环境要求
|
||||
|
||||
### 系统要求
|
||||
- Ubuntu 18.04+ 或其他Linux发行版
|
||||
- Nginx 1.18+
|
||||
- Node.js 18+
|
||||
- 网络访问权限到 172.16.0.34:5173
|
||||
|
||||
### 端口要求
|
||||
- 5174-5177:Nginx监听端口
|
||||
- 5173:开发服务器端口(需要在172.16.0.34上运行)
|
||||
|
||||
## 安装和配置步骤
|
||||
|
||||
### 1. 安装Nginx
|
||||
|
||||
```bash
|
||||
# 更新包管理器
|
||||
sudo apt update
|
||||
|
||||
# 安装Nginx
|
||||
sudo apt install nginx -y
|
||||
|
||||
# 检查Nginx版本
|
||||
nginx -v
|
||||
|
||||
# 启动Nginx服务
|
||||
sudo systemctl start nginx
|
||||
sudo systemctl enable nginx
|
||||
```
|
||||
|
||||
### 2. 配置Nginx
|
||||
|
||||
#### 2.1 备份原配置
|
||||
```bash
|
||||
# 备份默认配置
|
||||
sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.backup
|
||||
sudo cp /etc/nginx/sites-available/default /etc/nginx/sites-available/default.backup
|
||||
```
|
||||
|
||||
#### 2.2 创建多客户端配置
|
||||
```bash
|
||||
# 创建配置目录
|
||||
sudo mkdir -p /etc/nginx/conf.d
|
||||
|
||||
# 复制优化后的配置文件
|
||||
sudo cp nginx-ubuntu-optimized.conf /etc/nginx/conf.d/multi-client.conf
|
||||
|
||||
# 或者直接创建配置文件
|
||||
sudo nano /etc/nginx/conf.d/multi-client.conf
|
||||
# 然后粘贴 nginx-ubuntu-optimized.conf 的内容
|
||||
```
|
||||
|
||||
#### 2.3 创建日志目录
|
||||
```bash
|
||||
# 创建客户端专用日志目录
|
||||
sudo mkdir -p /var/log/nginx/clients
|
||||
|
||||
# 设置权限
|
||||
sudo chown -R www-data:www-data /var/log/nginx/clients
|
||||
sudo chmod -R 755 /var/log/nginx/clients
|
||||
```
|
||||
|
||||
#### 2.4 修改主配置文件
|
||||
```bash
|
||||
# 编辑主配置文件
|
||||
sudo nano /etc/nginx/nginx.conf
|
||||
```
|
||||
|
||||
确保包含以下配置:
|
||||
```nginx
|
||||
http {
|
||||
# 包含多客户端配置
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
|
||||
# 日志格式(如果主配置中没有)
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
# 其他配置...
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 验证配置
|
||||
|
||||
```bash
|
||||
# 测试Nginx配置语法
|
||||
sudo nginx -t
|
||||
|
||||
# 如果配置正确,重新加载Nginx
|
||||
sudo systemctl reload nginx
|
||||
|
||||
# 检查Nginx状态
|
||||
sudo systemctl status nginx
|
||||
```
|
||||
|
||||
### 4. 防火墙配置
|
||||
|
||||
```bash
|
||||
# 允许Nginx端口通过防火墙
|
||||
sudo ufw allow 5174
|
||||
sudo ufw allow 5175
|
||||
sudo ufw allow 5176
|
||||
sudo ufw allow 5177
|
||||
|
||||
# 或者允许端口范围
|
||||
sudo ufw allow 5174:5177/tcp
|
||||
|
||||
# 检查防火墙状态
|
||||
sudo ufw status
|
||||
```
|
||||
|
||||
## 应用配置修改
|
||||
|
||||
### 1. 修改api-config.ts
|
||||
|
||||
需要在开发环境配置中添加第四个客户端:
|
||||
|
||||
```typescript
|
||||
const getClientConfigs = (env: string): Record<string, Partial<ApiConfig>> => {
|
||||
if (env === 'development') {
|
||||
return {
|
||||
'client-a': {
|
||||
baseUrl: 'http://172.16.0.34:5174',
|
||||
uploadUrl: 'http://172.16.0.34:5174/admin/documents',
|
||||
// ... oauth配置
|
||||
},
|
||||
'client-b': {
|
||||
baseUrl: 'http://172.16.0.34:5175',
|
||||
uploadUrl: 'http://172.16.0.34:5175/admin/documents',
|
||||
// ... oauth配置
|
||||
},
|
||||
'client-c': {
|
||||
baseUrl: 'http://172.16.0.34:5176',
|
||||
uploadUrl: 'http://172.16.0.34:5176/admin/documents',
|
||||
// ... oauth配置
|
||||
},
|
||||
'client-d': {
|
||||
baseUrl: 'http://172.16.0.34:5177',
|
||||
uploadUrl: 'http://172.16.0.34:5177/admin/documents',
|
||||
// ... oauth配置
|
||||
}
|
||||
};
|
||||
}
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
### 2. 添加客户端检测逻辑
|
||||
|
||||
在应用中添加根据请求头自动检测客户端的逻辑:
|
||||
|
||||
```typescript
|
||||
// 在服务器端或中间件中
|
||||
const detectClientFromHeaders = (request: Request): string => {
|
||||
// 从Nginx传递的头部获取客户端ID
|
||||
const clientId = request.headers.get('X-Client-ID');
|
||||
const originalPort = request.headers.get('X-Original-Port');
|
||||
|
||||
if (clientId) {
|
||||
return clientId;
|
||||
}
|
||||
|
||||
// 根据端口映射客户端ID
|
||||
const portToClient: Record<string, string> = {
|
||||
'5174': 'client-a',
|
||||
'5175': 'client-b',
|
||||
'5176': 'client-c',
|
||||
'5177': 'client-d'
|
||||
};
|
||||
|
||||
return portToClient[originalPort || ''] || 'main';
|
||||
};
|
||||
```
|
||||
|
||||
## 测试步骤
|
||||
|
||||
### 1. 启动开发服务器
|
||||
|
||||
确保在172.16.0.34机器上启动开发服务器:
|
||||
```bash
|
||||
# 在项目目录中
|
||||
npm run dev
|
||||
# 或
|
||||
pnpm dev
|
||||
# 确保服务运行在5173端口
|
||||
```
|
||||
|
||||
### 2. 测试Nginx代理
|
||||
|
||||
```bash
|
||||
# 测试各个端口的健康检查
|
||||
curl http://localhost:5174/health
|
||||
curl http://localhost:5175/health
|
||||
curl http://localhost:5176/health
|
||||
curl http://localhost:5177/health
|
||||
|
||||
# 测试代理功能
|
||||
curl -H "Accept: text/html" http://localhost:5174/
|
||||
curl -H "Accept: text/html" http://localhost:5175/
|
||||
```
|
||||
|
||||
### 3. 验证客户端标识传递
|
||||
|
||||
```bash
|
||||
# 检查请求头传递
|
||||
curl -v http://localhost:5174/api/test 2>&1 | grep "X-Client-ID"
|
||||
|
||||
# 查看Nginx访问日志
|
||||
sudo tail -f /var/log/nginx/client-a-access.log
|
||||
sudo tail -f /var/log/nginx/client-b-access.log
|
||||
```
|
||||
|
||||
### 4. 浏览器测试
|
||||
|
||||
在浏览器中访问:
|
||||
- http://172.16.0.34:5174 (Client A)
|
||||
- http://172.16.0.34:5175 (Client B)
|
||||
- http://172.16.0.34:5176 (Client C)
|
||||
- http://172.16.0.34:5177 (Client D)
|
||||
|
||||
### 5. 环境变量测试
|
||||
|
||||
```bash
|
||||
# 设置客户端ID环境变量测试
|
||||
export CLIENT_ID=client-a
|
||||
npm run dev
|
||||
|
||||
# 或在启动时指定
|
||||
CLIENT_ID=client-b npm run dev
|
||||
```
|
||||
|
||||
## 监控和调试
|
||||
|
||||
### 1. 日志监控
|
||||
|
||||
```bash
|
||||
# 实时监控所有客户端日志
|
||||
sudo tail -f /var/log/nginx/client-*-access.log
|
||||
|
||||
# 监控错误日志
|
||||
sudo tail -f /var/log/nginx/client-*-error.log
|
||||
|
||||
# 监控Nginx主错误日志
|
||||
sudo tail -f /var/log/nginx/error.log
|
||||
```
|
||||
|
||||
### 2. 性能监控
|
||||
|
||||
```bash
|
||||
# 检查Nginx进程状态
|
||||
sudo systemctl status nginx
|
||||
|
||||
# 查看端口监听状态
|
||||
sudo netstat -tlnp | grep nginx
|
||||
|
||||
# 检查连接数
|
||||
sudo ss -tuln | grep :517
|
||||
```
|
||||
|
||||
### 3. 调试工具
|
||||
|
||||
```bash
|
||||
# 使用curl测试详细信息
|
||||
curl -v -H "X-Test: true" http://localhost:5174/api/config
|
||||
|
||||
# 使用httpie(需要安装)
|
||||
sudo apt install httpie
|
||||
http GET localhost:5174/health X-Test:debug
|
||||
```
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **端口被占用**
|
||||
```bash
|
||||
# 检查端口占用
|
||||
sudo lsof -i :5174
|
||||
# 杀死占用进程
|
||||
sudo kill -9 <PID>
|
||||
```
|
||||
|
||||
2. **权限问题**
|
||||
```bash
|
||||
# 检查Nginx用户权限
|
||||
sudo chown -R www-data:www-data /var/log/nginx/
|
||||
sudo chmod -R 755 /var/log/nginx/
|
||||
```
|
||||
|
||||
3. **配置语法错误**
|
||||
```bash
|
||||
# 详细检查配置
|
||||
sudo nginx -t -c /etc/nginx/nginx.conf
|
||||
```
|
||||
|
||||
4. **网络连接问题**
|
||||
```bash
|
||||
# 测试到开发服务器的连接
|
||||
telnet 172.16.0.34 5173
|
||||
# 或使用nc
|
||||
nc -zv 172.16.0.34 5173
|
||||
```
|
||||
|
||||
### 日志分析
|
||||
|
||||
```bash
|
||||
# 分析访问模式
|
||||
sudo awk '{print $1, $7, $9}' /var/log/nginx/client-a-access.log | sort | uniq -c
|
||||
|
||||
# 查找错误请求
|
||||
sudo grep "50[0-9]" /var/log/nginx/client-*-access.log
|
||||
|
||||
# 统计客户端访问量
|
||||
sudo grep -o 'client_id="[^"]*"' /var/log/nginx/client-*-access.log | sort | uniq -c
|
||||
```
|
||||
|
||||
## 性能优化建议
|
||||
|
||||
### 1. Nginx优化
|
||||
|
||||
```nginx
|
||||
# 在http块中添加
|
||||
worker_processes auto;
|
||||
worker_connections 1024;
|
||||
|
||||
# 启用gzip压缩
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_types text/plain text/css application/json application/javascript;
|
||||
|
||||
# 缓存配置
|
||||
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=10g
|
||||
inactive=60m use_temp_path=off;
|
||||
```
|
||||
|
||||
### 2. 系统优化
|
||||
|
||||
```bash
|
||||
# 增加文件描述符限制
|
||||
echo "* soft nofile 65535" | sudo tee -a /etc/security/limits.conf
|
||||
echo "* hard nofile 65535" | sudo tee -a /etc/security/limits.conf
|
||||
|
||||
# 优化内核参数
|
||||
echo "net.core.somaxconn = 65535" | sudo tee -a /etc/sysctl.conf
|
||||
sudo sysctl -p
|
||||
```
|
||||
|
||||
## 部署到生产环境
|
||||
|
||||
### 1. 安全加固
|
||||
|
||||
```nginx
|
||||
# 隐藏Nginx版本
|
||||
server_tokens off;
|
||||
|
||||
# 限制请求大小
|
||||
client_max_body_size 10M;
|
||||
|
||||
# 添加安全头部
|
||||
add_header X-Frame-Options DENY;
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
```
|
||||
|
||||
### 2. SSL配置
|
||||
|
||||
```nginx
|
||||
# HTTPS配置示例
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
ssl_certificate /path/to/cert.pem;
|
||||
ssl_certificate_key /path/to/key.pem;
|
||||
|
||||
# SSL优化配置
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 监控集成
|
||||
|
||||
```bash
|
||||
# 集成Prometheus监控
|
||||
sudo apt install nginx-module-prometheus
|
||||
|
||||
# 或使用日志分析工具
|
||||
sudo apt install goaccess
|
||||
goaccess /var/log/nginx/client-a-access.log -o report.html --log-format=COMBINED
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
通过以上配置,你可以在Ubuntu环境中成功测试Nginx多客户端反向代理功能。关键点包括:
|
||||
|
||||
1. **客户端标识传递**:通过 `X-Client-ID` 头部
|
||||
2. **端口映射**:5174-5177映射到不同客户端
|
||||
3. **配置动态切换**:应用根据客户端ID选择对应配置
|
||||
4. **日志分离**:每个客户端独立的访问和错误日志
|
||||
5. **健康检查**:每个端口提供独立的健康检查端点
|
||||
|
||||
这个方案为生产环境的多客户端部署提供了完整的测试基础。
|
||||
+18
-6
@@ -20,6 +20,9 @@ export type QueryParams = Record<string, string | number | boolean | undefined>;
|
||||
// const API_BASE_URL = 'http://172.18.0.100:3000';
|
||||
// const API_BASE_URL = 'http://172.16.0.119:9000/admin';
|
||||
|
||||
// 调试:打印当前API_BASE_URL的值
|
||||
console.log('🔍 axios-client.ts - API_BASE_URL.value:', API_BASE_URL.value);
|
||||
|
||||
// 文档URL前缀 (从配置文件导入)
|
||||
// export const DOCUMENT_URL = 'http://nas.7bm.co:9000/docauditai/';
|
||||
export { DOCUMENT_URL };
|
||||
@@ -32,7 +35,7 @@ const DEFAULT_TIMEOUT = 30000; // 增加到30秒
|
||||
|
||||
// 创建 axios 实例
|
||||
const axiosInstance = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
baseURL: API_BASE_URL.value === '/api' ? '' : API_BASE_URL.value, // 如果是相对路径,则不设置baseURL
|
||||
timeout: DEFAULT_TIMEOUT, // 增加超时时间
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -97,10 +100,17 @@ function buildUrl(endpoint: string, params?: QueryParams): string {
|
||||
if (endpoint.startsWith('http')) {
|
||||
fullUrl = endpoint;
|
||||
} else {
|
||||
// 确保API_BASE_URL格式正确
|
||||
const baseUrl = API_BASE_URL.endsWith('/') ? API_BASE_URL.slice(0, -1) : API_BASE_URL;
|
||||
const path = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
|
||||
fullUrl = `${baseUrl}${path}`;
|
||||
// 处理相对路径的情况
|
||||
if (API_BASE_URL.value === '/api') {
|
||||
// 如果是相对路径,直接使用endpoint
|
||||
const path = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
|
||||
fullUrl = path;
|
||||
} else {
|
||||
// 确保API_BASE_URL格式正确
|
||||
const baseUrl = API_BASE_URL.value.endsWith('/') ? API_BASE_URL.value.slice(0, -1) : API_BASE_URL.value;
|
||||
const path = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
|
||||
fullUrl = `${baseUrl}${path}`;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -189,6 +199,8 @@ export async function apiRequest<T>(
|
||||
if (USE_MOCK_DATA) {
|
||||
return getMockResponse<T>(endpoint);
|
||||
}
|
||||
|
||||
console.log('api-base-url-----------',API_BASE_URL.value)
|
||||
|
||||
try {
|
||||
// 构建 URL
|
||||
@@ -387,4 +399,4 @@ export async function downloadFile(path: string): Promise<Blob> {
|
||||
console.error('下载文件失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ function extractApiData<T>(responseData: unknown): T | null {
|
||||
export interface SubmitOpinionRequest {
|
||||
reviewPointResultId: string | number;
|
||||
documentId: string | number;
|
||||
evaluationPointId: number; // 必须是数字ID
|
||||
evaluationPointId: number | null; // 必须是数字ID
|
||||
auditOpinion: string;
|
||||
deductionScore: number;
|
||||
}
|
||||
@@ -60,6 +60,7 @@ export interface CrossCheckingOpinion {
|
||||
problem_message: string;
|
||||
proposer_id: number;
|
||||
created_at: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,7 +133,7 @@ export async function submitCrossCheckingOpinion(
|
||||
evaluation_result_id: opinionData.reviewPointResultId
|
||||
};
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/admin/cross_review/proposals`, {
|
||||
const response = await fetch(`${API_BASE_URL.value}/admin/cross_review/proposals`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -188,7 +189,7 @@ export async function getCrossCheckingOpinions(
|
||||
// 如果没传userId,默认用1
|
||||
const realUserId = userId ?? 1;
|
||||
// 实际后端API调用,拼接API_BASE_URL
|
||||
const response = await fetch(`${API_BASE_URL}/admin/cross_review/proposals/document`, {
|
||||
const response = await fetch(`${API_BASE_URL.value}/admin/cross_review/proposals/document`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -225,6 +226,7 @@ export async function getCrossCheckingOpinions(
|
||||
problem_message?: string;
|
||||
proposer_id: number;
|
||||
created_at: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
// 适配后端返回结构,使用新字段
|
||||
@@ -241,7 +243,8 @@ export async function getCrossCheckingOpinions(
|
||||
can_vote: item.can_vote ?? false,
|
||||
problem_message: item.problem_message || '',
|
||||
proposer_id: item.proposer_id,
|
||||
created_at: item.created_at
|
||||
created_at: item.created_at,
|
||||
status: item.status
|
||||
})) : [];
|
||||
|
||||
return {
|
||||
@@ -300,24 +303,24 @@ export async function performOpinionAction(
|
||||
switch (actionData.action) {
|
||||
case 'agree':
|
||||
message = '已赞同该意见';
|
||||
endpoint = `${API_BASE_URL}/admin/cross_review/proposals/${actionData.opinionId}/votes`;
|
||||
endpoint = `${API_BASE_URL.value}/admin/cross_review/proposals/${actionData.opinionId}/votes`;
|
||||
requestBody = { vote_type: 'agree', voter_id: userInfo?.user_id };
|
||||
break;
|
||||
case 'disagree':
|
||||
message = '已反对该意见';
|
||||
endpoint = `${API_BASE_URL}/admin/cross_review/proposals/${actionData.opinionId}/votes`;
|
||||
endpoint = `${API_BASE_URL.value}/admin/cross_review/proposals/${actionData.opinionId}/votes`;
|
||||
requestBody = { vote_type: 'disagree', voter_id: userInfo?.user_id };
|
||||
break;
|
||||
case 'withdraw_vote':
|
||||
message = '已撤销投票';
|
||||
// 撤销投票的接口,根据实际API调整
|
||||
endpoint = `${API_BASE_URL}/admin/cross_review/proposals/${actionData.opinionId}/votes`;
|
||||
endpoint = `${API_BASE_URL.value}/admin/cross_review/proposals/${actionData.opinionId}/votes`;
|
||||
requestBody = { vote_type: 'cancel', voter_id: userInfo?.user_id };
|
||||
break;
|
||||
case 'withdraw_opinion':
|
||||
message = '已撤销意见';
|
||||
// 撤销意见的接口,根据实际API调整
|
||||
endpoint = `${API_BASE_URL}/admin/cross_review/proposals/${actionData.opinionId}`;
|
||||
endpoint = `${API_BASE_URL.value}/admin/cross_review/proposals/${actionData.opinionId}`;
|
||||
requestBody = {};
|
||||
break;
|
||||
default:
|
||||
@@ -412,7 +415,7 @@ export async function checkProposalVotes(
|
||||
document_id: documentId
|
||||
};
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/admin/cross_review/proposals/document/check_pending_votes`, {
|
||||
const response = await fetch(`${API_BASE_URL.value}/admin/cross_review/proposals/document/check_pending_votes`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -389,7 +389,7 @@ export async function getCrossCheckingStats(userInfo?: { user_id?: number; [key:
|
||||
export async function getUserTaskDocuments(page: number = 1, pageSize: number = 10, jwtToken?: string): Promise<ApiResponse<UserTaskApiResponse>> {
|
||||
try {
|
||||
// 拼接绝对路径,去除多余斜杠
|
||||
const base = API_BASE_URL.endsWith('/') ? API_BASE_URL.slice(0, -1) : API_BASE_URL;
|
||||
const base = API_BASE_URL.value.endsWith('/') ? API_BASE_URL.value.slice(0, -1) : API_BASE_URL.value;
|
||||
const url = `${base}/admin/cross_review/tasks/user_tasks`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
@@ -436,7 +436,7 @@ export async function getUserTaskDocuments(page: number = 1, pageSize: number =
|
||||
export async function getTaskDocuments(taskId: number, page: number = 1, pageSize: number = 10, jwtToken?: string): Promise<ApiResponse<TaskDocumentApiResponse>> {
|
||||
try {
|
||||
// 拼接绝对路径,去除多余斜杠
|
||||
const base = API_BASE_URL.endsWith('/') ? API_BASE_URL.slice(0, -1) : API_BASE_URL;
|
||||
const base = API_BASE_URL.value.endsWith('/') ? API_BASE_URL.value.slice(0, -1) : API_BASE_URL.value;
|
||||
const url = `${base}/admin/cross_review/tasks/${taskId}/documents`;
|
||||
// console.log('最终请求URL:', url);
|
||||
|
||||
|
||||
@@ -324,7 +324,8 @@ export async function getReviewPoints(fileId: string, request: Request) {
|
||||
const scoringProposalsParams: PostgrestParams = {
|
||||
select: '*',
|
||||
filter: {
|
||||
'document_id': `eq.${fileId}`
|
||||
'document_id': `eq.${fileId}`,
|
||||
'deleted_at': `is.null`
|
||||
}
|
||||
};
|
||||
const scoringProposalsResponse = await postgrestGet('cross_scoring_proposals', scoringProposalsParams);
|
||||
|
||||
+12
-7
@@ -105,8 +105,8 @@ export async function getHomeData(reviewType?: string | null,userId?: string | n
|
||||
const startOfLastMonth = dayjs().subtract(1, 'month').startOf('month').format('YYYY-MM-DD HH:mm:ss');
|
||||
const endOfLastMonth = dayjs().subtract(1, 'month').endOf('month').format('YYYY-MM-DD HH:mm:ss');
|
||||
|
||||
console.log('传入的 reviewType', reviewType);
|
||||
console.log('传入的 userId', userId);
|
||||
// console.log('传入的 reviewType', reviewType);
|
||||
// console.log('传入的 userId', userId);
|
||||
|
||||
// 基于 reviewType 构建类型过滤条件
|
||||
const typeFilter = buildTypeFilter(reviewType || null);
|
||||
@@ -181,7 +181,7 @@ export async function getHomeData(reviewType?: string | null,userId?: string | n
|
||||
select: 'count',
|
||||
filter: {
|
||||
and: `(audit_status.neq.0,audit_status.neq.2)`,
|
||||
updated_at: `gte.${startOfThisMonth}`,
|
||||
upload_time: `gte.${startOfThisMonth}`,
|
||||
is_test_document: `eq.false`,
|
||||
user_id: `eq.${userId}`
|
||||
}
|
||||
@@ -212,8 +212,8 @@ export async function getHomeData(reviewType?: string | null,userId?: string | n
|
||||
const lastMonthReviewedParams: PostgrestParams = {
|
||||
select: 'count',
|
||||
filter: {
|
||||
or: `(audit_status.eq.1,audit_status.eq.-1)`,
|
||||
and: `(updated_at.gte.${startOfLastMonth},updated_at.lte.${endOfLastMonth})`,
|
||||
// or: `(audit_status.eq.1,audit_status.eq.-1)`,
|
||||
and: `(upload_time.gte.${startOfLastMonth},upload_time.lte.${endOfLastMonth},audit_status.neq.0,audit_status.neq.2)`,
|
||||
is_test_document: `eq.false`,
|
||||
user_id: `eq.${userId}`
|
||||
}
|
||||
@@ -226,7 +226,7 @@ export async function getHomeData(reviewType?: string | null,userId?: string | n
|
||||
if (!lastMonthReviewedParams.filter) {
|
||||
lastMonthReviewedParams.filter = {};
|
||||
}
|
||||
lastMonthReviewedParams.filter.or = lastMonthReviewedParams.filter.or + ',' + typeFilter;
|
||||
lastMonthReviewedParams.filter.or = typeFilter;
|
||||
} else {
|
||||
const [field, op, value] = typeFilter.split('.');
|
||||
if (!lastMonthReviewedParams.filter) {
|
||||
@@ -243,6 +243,8 @@ export async function getHomeData(reviewType?: string | null,userId?: string | n
|
||||
);
|
||||
// 上月已审核文件数量
|
||||
const lastMonthReviewed = lastMonthReviewedCount[0]?.count || 0;
|
||||
// console.log('上月已审核文件查询参数', lastMonthReviewedParams);
|
||||
// console.log('上月已审核文件数量', lastMonthReviewed);
|
||||
|
||||
// 计算同比增长
|
||||
let reviewGrowthValue = 0;
|
||||
@@ -285,8 +287,11 @@ export async function getHomeData(reviewType?: string | null,userId?: string | n
|
||||
'获取本月审核通过数量失败',
|
||||
[]
|
||||
);
|
||||
// console.log('本月审核通过数量查询参数', thisMonthTotalParams);
|
||||
// 本月审核通过数量
|
||||
const thisMonthPassTotal = thisMonthTotalCount[0]?.count || 0;
|
||||
// console.log('本月审核通过数量', thisMonthPassTotal);
|
||||
// console.log('本月已审核文件数量', monthlyReviewedFiles);
|
||||
|
||||
// 本月审核通过率
|
||||
const monthlyPassRate = (thisMonthPassTotal > 0 && monthlyReviewedFiles > 0)
|
||||
@@ -298,7 +303,7 @@ export async function getHomeData(reviewType?: string | null,userId?: string | n
|
||||
select: 'count',
|
||||
filter: {
|
||||
audit_status: `eq.1`,
|
||||
and: `(updated_at.gte.${startOfLastMonth},updated_at.lte.${endOfLastMonth})`,
|
||||
and: `(upload_time.gte.${startOfLastMonth},upload_time.lte.${endOfLastMonth})`,
|
||||
is_test_document: `eq.false`,
|
||||
user_id: `eq.${userId}`
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export class TokenManager {
|
||||
private oauthClient: OAuthClient;
|
||||
|
||||
constructor() {
|
||||
this.oauthClient = new OAuthClient(OAUTH_CONFIG);
|
||||
this.oauthClient = new OAuthClient(OAUTH_CONFIG.value);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -151,4 +151,4 @@ export class TokenManager {
|
||||
}
|
||||
|
||||
// 导出单例实例
|
||||
export const tokenManager = new TokenManager();
|
||||
export const tokenManager = new TokenManager();
|
||||
@@ -57,7 +57,7 @@ export async function getOrganizationTree(includeUsers: boolean = true, jwtToken
|
||||
|
||||
if (jwtToken) {
|
||||
// 如果提供了JWT Token,则使用fetch并携带Authorization头
|
||||
const url = `${API_BASE_URL}/admin/users/organizations?include_users=${includeUsers}`;
|
||||
const url = `${API_BASE_URL.value}/admin/users/organizations?include_users=${includeUsers}`;
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${jwtToken}`,
|
||||
|
||||
@@ -175,6 +175,7 @@ interface ReviewPointsListProps {
|
||||
scoringProposals?: ScoringProposal[];
|
||||
jwtToken?: string; // 添加JWT token参数
|
||||
userInfo?: UserInfo; // 添加用户信息参数
|
||||
onOpinionSubmitted?: (newProposal: ScoringProposal) => void; // 新增:意见提交成功后的回调
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -433,12 +434,14 @@ export function ReviewPointsList({
|
||||
onReviewPointSelect,
|
||||
scoringProposals = [],
|
||||
jwtToken,
|
||||
userInfo
|
||||
userInfo,
|
||||
onOpinionSubmitted
|
||||
}: ReviewPointsListProps) {
|
||||
// 状态管理
|
||||
const [searchText, setSearchText] = useState(''); // 搜索文本
|
||||
const [statusFilter, setStatusFilter] = useState<string | null>(null); // 状态过滤
|
||||
const [evaluationResultIds, setEvaluationResultIds] = useState<number[]>([]); // 评分提案的evaluation_result_id
|
||||
const [localScoringProposals, setLocalScoringProposals] = useState<ScoringProposal[]>(scoringProposals); // 本地状态管理scoringProposals
|
||||
const fetcher = useFetcher();
|
||||
|
||||
// 归一化 reviewPoints,确保每个点都有 id 字段
|
||||
@@ -452,17 +455,21 @@ export function ReviewPointsList({
|
||||
setNormalizedReviewPoints(norm);
|
||||
}, [reviewPoints]);
|
||||
|
||||
// 在组件中使用scoringProposals(这里只是简单使用以避免linter警告)
|
||||
// 将来可以用于显示相关的评分提案信息
|
||||
// 同步外部scoringProposals到本地状态
|
||||
useEffect(() => {
|
||||
if (scoringProposals && scoringProposals.length > 0) {
|
||||
// console.log('收到评分提案数据:', scoringProposals.length, '个提案');
|
||||
setLocalScoringProposals(scoringProposals);
|
||||
}, [scoringProposals]);
|
||||
|
||||
// 在组件中使用localScoringProposals
|
||||
useEffect(() => {
|
||||
if (localScoringProposals && localScoringProposals.length > 0) {
|
||||
// console.log('收到评分提案数据:', localScoringProposals.length, '个提案');
|
||||
// 获取提案的evaluation_result_id
|
||||
const evaluationResultIds = scoringProposals.map(proposal => Number(proposal.evaluation_result_id));
|
||||
const evaluationResultIds = localScoringProposals.map(proposal => Number(proposal.evaluation_result_id));
|
||||
setEvaluationResultIds(evaluationResultIds);
|
||||
// console.log('提案的evaluation_result_id:', evaluationResultIds);
|
||||
}
|
||||
}, [scoringProposals]);
|
||||
}, [localScoringProposals]);
|
||||
|
||||
// 提出意见模态框相关状态
|
||||
const [isOpinionModalOpen, setIsOpinionModalOpen] = useState(false);
|
||||
@@ -618,14 +625,14 @@ export function ReviewPointsList({
|
||||
* 打开意见列表模态框
|
||||
*/
|
||||
const handleOpenOpinionListModal = (reviewPoint: ReviewPoint) => {
|
||||
console.log('查看reviewPoint', reviewPoint);
|
||||
if (scoringProposals.length === 0) {
|
||||
// console.log('查看reviewPoint', reviewPoint);
|
||||
if (localScoringProposals.length === 0) {
|
||||
toastService.warning('当前文件尚未有人提出过意见');
|
||||
return;
|
||||
}
|
||||
setSelectedReviewPoint(reviewPoint);
|
||||
setIsOpinionListModalOpen(true);
|
||||
console.log('打开意见列表模态框');
|
||||
// console.log('打开意见列表模态框');
|
||||
// 直接传递reviewPoint的documentId,避免依赖状态更新
|
||||
loadOpinionListData(1, 10, reviewPoint.documentId);
|
||||
};
|
||||
@@ -714,14 +721,14 @@ export function ReviewPointsList({
|
||||
}
|
||||
|
||||
// 新增:详细打印每个校验条件
|
||||
console.log('校验前 selectedReviewPoint:', selectedReviewPoint);
|
||||
console.log('校验前 opinionForm:', opinionForm);
|
||||
console.log('校验前 userInfo:', userInfo);
|
||||
console.log('documentId:', selectedReviewPoint.documentId, 'isNaN:', isNaN(Number(selectedReviewPoint.documentId)), 'typeof:', typeof selectedReviewPoint.documentId);
|
||||
console.log('pointId:', selectedReviewPoint.pointId, 'isNaN:', isNaN(Number(selectedReviewPoint.pointId)), 'typeof:', typeof selectedReviewPoint.pointId);
|
||||
console.log('deductionScore:', opinionForm.deductionScore, 'typeof:', typeof opinionForm.deductionScore, 'isNaN:', isNaN(Number(opinionForm.deductionScore)));
|
||||
console.log('auditOpinion:', opinionForm.auditOpinion, 'trim:', String(opinionForm.auditOpinion).trim(), 'typeof:', typeof opinionForm.auditOpinion);
|
||||
console.log('user_id:', userInfo?.user_id, 'typeof:', typeof userInfo?.user_id);
|
||||
// console.log('校验前 selectedReviewPoint:', selectedReviewPoint);
|
||||
// console.log('校验前 opinionForm:', opinionForm);
|
||||
// console.log('校验前 userInfo:', userInfo);
|
||||
// console.log('documentId:', selectedReviewPoint.documentId, 'isNaN:', isNaN(Number(selectedReviewPoint.documentId)), 'typeof:', typeof selectedReviewPoint.documentId);
|
||||
// console.log('pointId:', selectedReviewPoint.pointId, 'isNaN:', isNaN(Number(selectedReviewPoint.pointId)), 'typeof:', typeof selectedReviewPoint.pointId);
|
||||
// console.log('deductionScore:', opinionForm.deductionScore, 'typeof:', typeof opinionForm.deductionScore, 'isNaN:', isNaN(Number(opinionForm.deductionScore)));
|
||||
// console.log('auditOpinion:', opinionForm.auditOpinion, 'trim:', String(opinionForm.auditOpinion).trim(), 'typeof:', typeof opinionForm.auditOpinion);
|
||||
// console.log('user_id:', userInfo?.user_id, 'typeof:', typeof userInfo?.user_id);
|
||||
|
||||
// 更严谨的校验逻辑
|
||||
if (
|
||||
@@ -741,12 +748,12 @@ export function ReviewPointsList({
|
||||
}
|
||||
|
||||
// 打印所有关键数据
|
||||
console.log('selectedReviewPoint:', selectedReviewPoint);
|
||||
console.log('opinionForm:', opinionForm);
|
||||
console.log('userInfo:', userInfo);
|
||||
// console.log('selectedReviewPoint:', selectedReviewPoint);
|
||||
// console.log('opinionForm:', opinionForm);
|
||||
// console.log('userInfo:', userInfo);
|
||||
|
||||
// 组装后端要求的字段名和内容
|
||||
const data: Record<string, any> = {
|
||||
const data = {
|
||||
document_id: Number(selectedReviewPoint.documentId),
|
||||
evaluation_point_id: Number(selectedReviewPoint.pointId),
|
||||
proposed_score: Number(opinionForm.deductionScore),
|
||||
@@ -759,10 +766,10 @@ export function ReviewPointsList({
|
||||
data.evaluation_result_id = Number(selectedReviewPoint.evaluationPointId);
|
||||
}
|
||||
// 打印最终请求体
|
||||
console.log('最终请求体:', data);
|
||||
// console.log('最终请求体:', data);
|
||||
// 用原生 fetch + application/json 提交
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL.replace(/\/$/, '')}/admin/cross_review/proposals`, {
|
||||
const response = await fetch(`${API_BASE_URL.value.replace(/\/$/, '')}/admin/cross_review/proposals`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -773,6 +780,28 @@ export function ReviewPointsList({
|
||||
const result = await response.json();
|
||||
if (response.ok) {
|
||||
toastService.success('意见提交成功');
|
||||
|
||||
// 创建新的提案对象
|
||||
const newProposal: ScoringProposal = {
|
||||
id: result.id || Date.now(), // 使用返回的ID或时间戳作为临时ID
|
||||
evaluation_result_id: data.evaluation_result_id,
|
||||
proposer_id: data.proposer_id as number,
|
||||
proposed_score: data.proposed_score,
|
||||
reason: data.reason,
|
||||
status: 'pending', // 默认状态
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
document_id: data.document_id
|
||||
};
|
||||
|
||||
// 更新本地状态
|
||||
setLocalScoringProposals(prev => [...prev, newProposal]);
|
||||
|
||||
// 调用父组件回调(如果提供)
|
||||
if (onOpinionSubmitted) {
|
||||
onOpinionSubmitted(newProposal);
|
||||
}
|
||||
|
||||
handleCloseOpinionModal();
|
||||
} else {
|
||||
toastService.error(result.detail || '提交意见失败');
|
||||
@@ -2487,7 +2516,7 @@ export function ReviewPointsList({
|
||||
</button>
|
||||
<button
|
||||
className="px-4 py-2 bg-green-700 border border-transparent rounded-md text-sm font-medium text-white hover:bg-green-600 disabled:opacity-50"
|
||||
onClick={handleSubmitOpinion}
|
||||
onClick={() => handleSubmitOpinion()}
|
||||
disabled={isSubmittingOpinion}
|
||||
>
|
||||
{isSubmittingOpinion ? '提交中...' : '发起投票'}
|
||||
@@ -2652,7 +2681,7 @@ export function ReviewPointsList({
|
||||
{
|
||||
title: "投票人",
|
||||
key: "votes",
|
||||
width: "25%",
|
||||
width: "22%",
|
||||
align: "center" as const,
|
||||
render: (_: unknown, record: CrossCheckingOpinion) => {
|
||||
// 投票类型配置
|
||||
@@ -2707,9 +2736,9 @@ export function ReviewPointsList({
|
||||
{
|
||||
title: "意见发起人",
|
||||
key: "proposer",
|
||||
width: "4%",
|
||||
width: "8%",
|
||||
render: (_: unknown, record: CrossCheckingOpinion) => (
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="flex items-center justify-center text-left">
|
||||
<span
|
||||
className="px-1.5 py-0.5 rounded text-xs font-medium text-yellow-700 bg-yellow-100 border border-yellow-200 whitespace-nowrap overflow-hidden text-ellipsis max-w-[80px] transition-all hover:scale-[1.03] hover:shadow-sm"
|
||||
>
|
||||
@@ -2721,7 +2750,7 @@ export function ReviewPointsList({
|
||||
{
|
||||
title: "发起时间",
|
||||
key: "created_at",
|
||||
width: "18%",
|
||||
width: "12%",
|
||||
render: (_: unknown, record: CrossCheckingOpinion) => (
|
||||
<div className="text-sm text-left">{record.created_at}</div>
|
||||
)
|
||||
@@ -2729,7 +2758,7 @@ export function ReviewPointsList({
|
||||
{
|
||||
title: "投票状态",
|
||||
key: "opinion_status",
|
||||
width: "10%",
|
||||
width: "12%",
|
||||
render: (_: unknown, record: CrossCheckingOpinion) => {
|
||||
let label = '';
|
||||
let color = '';
|
||||
@@ -2754,7 +2783,7 @@ export function ReviewPointsList({
|
||||
{
|
||||
title: "操作",
|
||||
key: "operation",
|
||||
width: "18%",
|
||||
width: "auto",
|
||||
align: "center" as const,
|
||||
render: (_: unknown, record: CrossCheckingOpinion) => {
|
||||
const isPerforming = (action: string) => performingAction === `${record.proposal_id}-${action}`;
|
||||
@@ -2869,7 +2898,7 @@ function OpinionActions({ record, isPerforming, handleOpinionAction, userInfo }:
|
||||
</>
|
||||
)}
|
||||
{/* 仅当can_vote为false时显示撤销投票按钮 */}
|
||||
{!record.can_vote && (
|
||||
{!record.can_vote && !isProposer && (
|
||||
<Button
|
||||
type="default"
|
||||
className="bg-yellow-600 hover:bg-yellow-700 text-white min-w-[80px] h-14 text-base font-medium rounded-lg flex items-center justify-center whitespace-nowrap shadow-md hover:shadow-lg transition-all duration-200 px-4 py-3"
|
||||
|
||||
@@ -202,7 +202,7 @@ export function Toast({
|
||||
aria-live="polite"
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
style={{ zIndex: 99999, position: 'relative' }}
|
||||
style={{ zIndex: 999999, position: 'relative' }}
|
||||
>
|
||||
<div className="toast-content">
|
||||
<div className="toast-icon-wrapper">
|
||||
|
||||
+155
-49
@@ -32,14 +32,16 @@ interface ApiConfig {
|
||||
const configs: Record<string, ApiConfig> = {
|
||||
// 开发环境
|
||||
development: {
|
||||
// baseUrl: '/api', // 改为相对路径,让nginx处理
|
||||
baseUrl: 'http://172.16.0.55:8008',
|
||||
// baseUrl: 'http://172.16.0.81:3000',
|
||||
// baseUrl: 'http://nas.7bm.co:3000',
|
||||
// documentUrl: 'http://172.16.0.81:9000/docauditai/',
|
||||
|
||||
documentUrl: 'http://172.16.0.55:8008/docauditai/',
|
||||
// documentUrl: '/api/docauditai/',
|
||||
|
||||
// uploadUrl: '/api/admin/documents', // 改为相对路径
|
||||
uploadUrl: 'http://172.16.0.55:8008/admin/documents',
|
||||
// uploadUrl: 'http://172.16.0.58:8008/admin/documents',
|
||||
// uploadUrl: 'http://172.16.0.58:8008/admin/documents',
|
||||
|
||||
oauth: {
|
||||
serverUrl: 'http://10.79.112.85', // IDaaS服务器地址
|
||||
clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO',
|
||||
@@ -103,35 +105,50 @@ const getClientConfigs = (env: string): Record<string, Partial<ApiConfig>> => {
|
||||
// 开发环境 - 本地nginx代理配置
|
||||
return {
|
||||
'client-a': {
|
||||
baseUrl: 'http://localhost:8001',
|
||||
uploadUrl: 'http://localhost:8001/admin/documents',
|
||||
baseUrl: '/api', // 改为相对路径,让nginx处理
|
||||
uploadUrl: '/api/admin/documents', // 改为相对路径
|
||||
documentUrl: '/api/docauditai/',
|
||||
oauth: {
|
||||
serverUrl: 'http://10.79.112.85',
|
||||
clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO',
|
||||
clientSecret: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb',
|
||||
redirectUri: 'http://localhost:8001/callback',
|
||||
redirectUri: 'http://172.16.0.34:5174/callback',
|
||||
appId: 'idaasoauth2'
|
||||
}
|
||||
},
|
||||
'client-b': {
|
||||
baseUrl: 'http://localhost:8002',
|
||||
uploadUrl: 'http://localhost:8002/admin/documents',
|
||||
baseUrl: '/api', // 改为相对路径,让nginx处理
|
||||
uploadUrl: '/api/admin/documents', // 改为相对路径
|
||||
documentUrl: '/api/docauditai/',
|
||||
oauth: {
|
||||
serverUrl: 'http://10.79.112.85',
|
||||
clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO',
|
||||
clientSecret: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb',
|
||||
redirectUri: 'http://localhost:8002/callback',
|
||||
redirectUri: 'http://172.16.0.34:5175/callback',
|
||||
appId: 'idaasoauth2'
|
||||
}
|
||||
},
|
||||
'client-c': {
|
||||
baseUrl: 'http://localhost:8003',
|
||||
uploadUrl: 'http://localhost:8003/admin/documents',
|
||||
baseUrl: '/api', // 改为相对路径,让nginx处理
|
||||
uploadUrl: '/api/admin/documents', // 改为相对路径
|
||||
documentUrl: '/api/docauditai/',
|
||||
oauth: {
|
||||
serverUrl: 'http://10.79.112.85',
|
||||
clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO',
|
||||
clientSecret: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb',
|
||||
redirectUri: 'http://localhost:8003/callback',
|
||||
redirectUri: 'http://172.16.0.34:5176/callback',
|
||||
appId: 'idaasoauth2'
|
||||
}
|
||||
},
|
||||
'client-d': {
|
||||
baseUrl: '/api', // 改为相对路径,让nginx处理
|
||||
uploadUrl: '/api/admin/documents', // 改为相对路径
|
||||
documentUrl: '/api/docauditai/',
|
||||
oauth: {
|
||||
serverUrl: 'http://10.79.112.85',
|
||||
clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO',
|
||||
clientSecret: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb',
|
||||
redirectUri: 'http://172.16.0.34:5177/callback',
|
||||
appId: 'idaasoauth2'
|
||||
}
|
||||
}
|
||||
@@ -139,36 +156,51 @@ const getClientConfigs = (env: string): Record<string, Partial<ApiConfig>> => {
|
||||
} else {
|
||||
// 生产环境 - 服务器配置
|
||||
return {
|
||||
'client-a': {
|
||||
baseUrl: 'http://10.79.97.17:51701',
|
||||
uploadUrl: 'http://10.79.97.17:51701/admin/documents',
|
||||
oauth: {
|
||||
serverUrl: 'http://10.79.112.85',
|
||||
clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO',
|
||||
clientSecret: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb',
|
||||
redirectUri: 'http://10.79.97.17:51701/callback',
|
||||
appId: 'idaasoauth2'
|
||||
}
|
||||
},
|
||||
'client-b': {
|
||||
baseUrl: 'http://10.79.97.17:51702',
|
||||
uploadUrl: 'http://10.79.97.17:51702/admin/documents',
|
||||
oauth: {
|
||||
serverUrl: 'http://10.79.112.85',
|
||||
clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO',
|
||||
clientSecret: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb',
|
||||
redirectUri: 'http://10.79.97.17:51702/callback',
|
||||
appId: 'idaasoauth2'
|
||||
}
|
||||
},
|
||||
'client-c': {
|
||||
'provincial': {
|
||||
baseUrl: 'http://10.79.97.17:51704',
|
||||
uploadUrl: 'http://10.79.97.17:51704/admin/documents',
|
||||
documentUrl: 'http://10.76.244.156:9000/docauditai/',
|
||||
oauth: {
|
||||
serverUrl: 'http://10.79.112.85',
|
||||
clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO',
|
||||
clientSecret: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb',
|
||||
redirectUri: 'http://10.79.97.17:51704/callback',
|
||||
redirectUri: 'http://10.79.97.17/callback',
|
||||
appId: 'idaasoauth2'
|
||||
}
|
||||
},
|
||||
'meizhou': {
|
||||
baseUrl: 'http://10.79.97.17:51705',
|
||||
uploadUrl: 'http://10.79.97.17:51705/admin/documents',
|
||||
documentUrl: 'http://10.76.244.156:9000/docauditai/',
|
||||
oauth: {
|
||||
serverUrl: 'http://10.79.112.85',
|
||||
clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO',
|
||||
clientSecret: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb',
|
||||
redirectUri: 'http://10.79.97.17/callback',
|
||||
appId: 'idaasoauth2'
|
||||
}
|
||||
},
|
||||
'jieyang': {
|
||||
baseUrl: 'http://10.79.97.17:51706',
|
||||
uploadUrl: 'http://10.79.97.17:51706/admin/documents',
|
||||
documentUrl: 'http://10.76.244.156:9000/docauditai/',
|
||||
oauth: {
|
||||
serverUrl: 'http://10.79.112.85',
|
||||
clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO',
|
||||
clientSecret: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb',
|
||||
redirectUri: 'http://10.79.97.17/callback',
|
||||
appId: 'idaasoauth2'
|
||||
}
|
||||
},
|
||||
'yunfu': {
|
||||
baseUrl: 'http://10.79.97.17:51707',
|
||||
uploadUrl: 'http://10.79.97.17:51707/admin/documents',
|
||||
documentUrl: 'http://10.76.244.156:9000/docauditai/',
|
||||
oauth: {
|
||||
serverUrl: 'http://10.79.112.85',
|
||||
clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO',
|
||||
clientSecret: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb',
|
||||
redirectUri: 'http://10.79.97.17/callback',
|
||||
appId: 'idaasoauth2'
|
||||
}
|
||||
}
|
||||
@@ -182,8 +214,46 @@ const getCurrentEnvironment = (): string => {
|
||||
return process.env.NEXT_PUBLIC_API_ENV || process.env.NODE_ENV || 'development';
|
||||
};
|
||||
|
||||
// 获取客户端ID
|
||||
const getClientId = (): string => {
|
||||
// 获取客户端ID - 支持从请求头动态获取
|
||||
const getClientId = (request?: Request): string => {
|
||||
// SSR: 通过请求头的 host 判断
|
||||
if (request && typeof window === 'undefined') {
|
||||
// 1. 优先 X-Client-ID
|
||||
const clientIdFromHeader = request.headers.get('X-Client-ID');
|
||||
if (clientIdFromHeader) return clientIdFromHeader;
|
||||
|
||||
// 2. 通过 host 端口判断
|
||||
const host = request.headers.get('host'); // 例如 172.24.238.60:5177
|
||||
if (host) {
|
||||
const port = host.split(':')[1];
|
||||
const portToClient: Record<string, string> = {
|
||||
'5174': 'client-a',
|
||||
'5175': 'client-b',
|
||||
'5176': 'client-c',
|
||||
'5177': 'client-d'
|
||||
};
|
||||
if (port && portToClient[port]) {
|
||||
return portToClient[port];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 浏览器端
|
||||
if (typeof window !== 'undefined') {
|
||||
const port = window.location.port;
|
||||
const portToClient: Record<string, string> = {
|
||||
'5174': 'client-a',
|
||||
'5175': 'client-b',
|
||||
'5176': 'client-c',
|
||||
'5177': 'client-d'
|
||||
};
|
||||
if (port && portToClient[port]) {
|
||||
console.log(`🎯 浏览器端检测到客户端ID: ${portToClient[port]} (端口: ${port})`);
|
||||
return portToClient[port];
|
||||
}
|
||||
}
|
||||
|
||||
// 回退到环境变量
|
||||
return process.env.CLIENT_ID || process.env.NEXT_PUBLIC_CLIENT_ID || 'main';
|
||||
};
|
||||
|
||||
@@ -204,9 +274,9 @@ const getConfigFromEnv = (defaultConfig: ApiConfig): ApiConfig => {
|
||||
};
|
||||
|
||||
// 获取当前配置 - 支持客户端特定配置
|
||||
const getCurrentConfig = (): ApiConfig => {
|
||||
const getCurrentConfig = (request?: Request): ApiConfig => {
|
||||
const env = getCurrentEnvironment();
|
||||
const clientId = getClientId();
|
||||
const clientId = getClientId(request);
|
||||
const defaultConfig = configs[env] || configs.development;
|
||||
|
||||
// 获取当前环境的客户端特定配置
|
||||
@@ -234,16 +304,52 @@ const getCurrentConfig = (): ApiConfig => {
|
||||
return finalConfig;
|
||||
};
|
||||
|
||||
// 导出当前环境的配置
|
||||
// 导出当前环境的配置(静态,用于兼容性)
|
||||
export const apiConfig = getCurrentConfig();
|
||||
|
||||
// 导出具体的配置项,方便使用
|
||||
export const {
|
||||
baseUrl: API_BASE_URL,
|
||||
documentUrl: DOCUMENT_URL,
|
||||
uploadUrl: UPLOAD_URL,
|
||||
oauth: OAUTH_CONFIG
|
||||
} = apiConfig;
|
||||
// 导出动态配置获取函数(支持从请求头获取客户端ID)
|
||||
export const getApiConfig = (request?: Request): ApiConfig => {
|
||||
return getCurrentConfig(request);
|
||||
};
|
||||
|
||||
// 导出具体的配置项,方便使用(现在是真正动态的)
|
||||
// 使用getter函数实现动态获取,避免ES模块中exports未定义的问题
|
||||
export const API_BASE_URL = {
|
||||
get value() {
|
||||
return getCurrentConfig().baseUrl;
|
||||
}
|
||||
};
|
||||
|
||||
export const DOCUMENT_URL = {
|
||||
get value() {
|
||||
return getCurrentConfig().documentUrl;
|
||||
}
|
||||
};
|
||||
|
||||
export const UPLOAD_URL = {
|
||||
get value() {
|
||||
return getCurrentConfig().uploadUrl;
|
||||
}
|
||||
};
|
||||
|
||||
export const OAUTH_CONFIG = {
|
||||
get value() {
|
||||
return getCurrentConfig().oauth;
|
||||
}
|
||||
};
|
||||
|
||||
// 动态获取配置项的函数
|
||||
export const getApiBaseUrl = (request?: Request): string => {
|
||||
return getApiConfig(request).baseUrl;
|
||||
};
|
||||
|
||||
export const getUploadUrl = (request?: Request): string => {
|
||||
return getApiConfig(request).uploadUrl;
|
||||
};
|
||||
|
||||
export const getOAuthConfig = (request?: Request) => {
|
||||
return getApiConfig(request).oauth;
|
||||
};
|
||||
|
||||
// 导出所有配置,供调试使用
|
||||
export { configs };
|
||||
|
||||
@@ -34,7 +34,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
|
||||
try {
|
||||
// 创建OAuth客户端
|
||||
const oauthClient = new OAuthClient(OAUTH_CONFIG);
|
||||
const oauthClient = new OAuthClient(OAUTH_CONFIG.value);
|
||||
|
||||
// 获取访问令牌
|
||||
const tokenResponse = await oauthClient.getAccessToken(code);
|
||||
@@ -130,4 +130,4 @@ export default function Callback() {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -262,35 +262,8 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
return Response.json({ success: true, data: response.data });
|
||||
}
|
||||
|
||||
if (intent === "submitCrossCheckingOpinion") {
|
||||
const { submitCrossCheckingOpinion } = await import("~/api/cross-checking/cross-file-result");
|
||||
|
||||
const reviewPointResultId = formData.get("reviewPointResultId") as string;
|
||||
const documentId = formData.get("documentId") as string;
|
||||
const auditPoint = formData.get("auditPoint") as string;
|
||||
const foundIssue = formData.get("foundIssue") as string;
|
||||
const auditOpinion = formData.get("auditOpinion") as string;
|
||||
const deductionScore = parseFloat(formData.get("deductionScore") as string);
|
||||
|
||||
const opinionData = {
|
||||
reviewPointResultId,
|
||||
documentId,
|
||||
auditPoint,
|
||||
foundIssue,
|
||||
auditOpinion,
|
||||
deductionScore
|
||||
};
|
||||
|
||||
const response = await submitCrossCheckingOpinion(opinionData, frontendJWT);
|
||||
|
||||
if (response.error) {
|
||||
return Response.json({ success: false, error: response.error }, { status: response.status || 500 });
|
||||
}
|
||||
|
||||
return Response.json({ success: true, data: response.data });
|
||||
}
|
||||
|
||||
if (intent === "getCrossCheckingOpinions") {
|
||||
if (intent === "getCrossCheckingOpinions") {
|
||||
const { getCrossCheckingOpinions } = await import("~/api/cross-checking/cross-file-result");
|
||||
|
||||
const documentId = formData.get("documentId") as string;
|
||||
@@ -328,7 +301,18 @@ export default function CrossCheckingResult() {
|
||||
const [reviewData, setReviewData] = useState<ReviewData | null>(null);
|
||||
const [activeReviewPointResultId, setActiveReviewPointResultId] = useState<string | null>(null);
|
||||
const [targetPage, setTargetPage] = useState<number | undefined>(undefined);
|
||||
const [localScoringProposals, setLocalScoringProposals] = useState<ScoringProposal[]>(scoring_proposals || []); // 本地状态管理scoringProposals
|
||||
|
||||
// 同步外部scoring_proposals到本地状态
|
||||
useEffect(() => {
|
||||
setLocalScoringProposals(scoring_proposals || []);
|
||||
}, [scoring_proposals]);
|
||||
|
||||
// 处理意见提交成功的回调
|
||||
const handleOpinionSubmitted = (newProposal: ScoringProposal) => {
|
||||
setLocalScoringProposals(prev => [...prev, newProposal]);
|
||||
};
|
||||
|
||||
// loader 数据加载出错
|
||||
useEffect(()=>{
|
||||
loadingBarService.hide();
|
||||
@@ -555,7 +539,7 @@ export default function CrossCheckingResult() {
|
||||
|
||||
const responseData = checkRes.data as CheckProposalResponse;
|
||||
const pendingProposals = responseData?.data?.pending_proposals || [];
|
||||
console.log("pendingProposals", pendingProposals);
|
||||
// console.log("pendingProposals", pendingProposals);
|
||||
|
||||
// 3. 构建模态框消息
|
||||
let modalMessage: string = '';
|
||||
@@ -698,9 +682,10 @@ export default function CrossCheckingResult() {
|
||||
activeReviewPointResultId={activeReviewPointResultId}
|
||||
onReviewPointSelect={handleReviewPointSelect}
|
||||
onStatusChange={handleReviewPointStatusChange}
|
||||
scoringProposals={scoring_proposals as ScoringProposal[]}
|
||||
scoringProposals={localScoringProposals}
|
||||
jwtToken={jwtToken}
|
||||
userInfo={userInfo}
|
||||
onOpinionSubmitted={handleOpinionSubmitted}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -311,14 +311,10 @@ export default function CrossCheckingUpload() {
|
||||
const isZip = file.type === 'application/zip' ||
|
||||
file.type === 'application/x-zip-compressed' ||
|
||||
file.name.toLowerCase().endsWith('.zip');
|
||||
const isRar = file.type === 'application/x-rar-compressed' ||
|
||||
file.name.toLowerCase().endsWith('.rar');
|
||||
const is7z = file.type === 'application/x-7z-compressed' ||
|
||||
file.name.toLowerCase().endsWith('.7z');
|
||||
const isTar = file.type === 'application/x-tar' ||
|
||||
file.name.toLowerCase().endsWith('.tar');
|
||||
|
||||
if (isZip || isRar || is7z || isTar) {
|
||||
|
||||
if (isZip || is7z) {
|
||||
validFiles.push({
|
||||
id: generateFileId(),
|
||||
file,
|
||||
@@ -333,7 +329,7 @@ export default function CrossCheckingUpload() {
|
||||
});
|
||||
|
||||
if (hasInvalidFiles) {
|
||||
messageService.error('只能上传ZIP或RAR格式的压缩文件', {
|
||||
messageService.error('只能上传ZIP或7Z格式的压缩文件', {
|
||||
title: '文件类型错误',
|
||||
confirmText: '确定',
|
||||
});
|
||||
@@ -879,14 +875,14 @@ export default function CrossCheckingUpload() {
|
||||
ref={multipleUploadRef}
|
||||
onFilesSelected={handleMultipleFilesSelected}
|
||||
className="custom-upload-area"
|
||||
accept=".zip,.rar,.7z,.tar"
|
||||
accept=".zip,.7z"
|
||||
multiple={false}
|
||||
icon="ri-folder-zip-line"
|
||||
buttonText="选择文件"
|
||||
mainText="点击或拖拽文件到此区域上传"
|
||||
tipText={
|
||||
<div className="upload-tip-error">
|
||||
请上传多个案件作为压缩包zip、rar、7z、tar文件
|
||||
请上传多个案件作为压缩包zip、7z文件
|
||||
</div>
|
||||
}
|
||||
disabled={uploadType === 'single' || isUploading}
|
||||
|
||||
+14
-14
@@ -122,16 +122,16 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
// 打印session信息
|
||||
console.log("=== 测试用户登录 - Session信息 ===");
|
||||
console.log("保存到session的userInfo:", enhancedUserInfo);
|
||||
console.log("session数据结构:", {
|
||||
isAuthenticated: true,
|
||||
userRole: userRole,
|
||||
accessToken: "mock_access_token_for_test",
|
||||
refreshToken: "mock_refresh_token_for_test",
|
||||
tokenIssuedAt: Date.now(),
|
||||
tokenExpiresIn: mockTokenExpiresIn,
|
||||
frontendJWT: frontendJWT,
|
||||
userInfo: enhancedUserInfo
|
||||
});
|
||||
// console.log("session数据结构:", {
|
||||
// isAuthenticated: true,
|
||||
// userRole: userRole,
|
||||
// accessToken: "mock_access_token_for_test",
|
||||
// refreshToken: "mock_refresh_token_for_test",
|
||||
// tokenIssuedAt: Date.now(),
|
||||
// tokenExpiresIn: mockTokenExpiresIn,
|
||||
// frontendJWT: frontendJWT,
|
||||
// userInfo: enhancedUserInfo
|
||||
// });
|
||||
|
||||
const cookie = await sessionStorage.commitSession(session);
|
||||
|
||||
@@ -184,7 +184,7 @@ export default function Login() {
|
||||
const handleOAuthLogin = () => {
|
||||
try {
|
||||
// 创建OAuth客户端
|
||||
const oauthClient = new OAuthClient(OAUTH_CONFIG);
|
||||
const oauthClient = new OAuthClient(OAUTH_CONFIG.value);
|
||||
|
||||
// 生成状态值
|
||||
const state = oauthClient.generateState();
|
||||
@@ -205,8 +205,8 @@ export default function Login() {
|
||||
|
||||
useEffect(() => {
|
||||
// 检查OAuth配置是否完整
|
||||
if (!OAUTH_CONFIG.serverUrl || !OAUTH_CONFIG.clientId || !OAUTH_CONFIG.clientSecret) {
|
||||
console.error("OAuth2.0配置不完整:", OAUTH_CONFIG);
|
||||
if (!OAUTH_CONFIG.value.serverUrl || !OAUTH_CONFIG.value.clientId || !OAUTH_CONFIG.value.clientSecret) {
|
||||
console.error("OAuth2.0配置不完整:", OAUTH_CONFIG.value);
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -280,4 +280,4 @@ export default function Login() {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
if (accessToken) {
|
||||
try {
|
||||
// 创建OAuth客户端
|
||||
const oauthClient = new OAuthClient(OAUTH_CONFIG);
|
||||
const oauthClient = new OAuthClient(OAUTH_CONFIG.value);
|
||||
|
||||
// 构建登出后重定向URL
|
||||
const url = new URL(request.url);
|
||||
@@ -48,4 +48,4 @@ export default function Logout() {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
/**
|
||||
* 客户端配置测试页面
|
||||
* 用于验证Nginx代理和客户端ID检测是否正常工作
|
||||
*/
|
||||
import { json, type LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { useLoaderData } from "@remix-run/react";
|
||||
import { getApiConfig } from "~/config/api-config";
|
||||
import { detectClientFromRequest, getRequestDebugInfo } from "~/utils/client-detection";
|
||||
|
||||
/**
|
||||
* 服务器端loader函数 - 获取配置和调试信息
|
||||
*/
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
// 获取客户端配置
|
||||
const config = getApiConfig(request);
|
||||
|
||||
// 获取客户端检测信息
|
||||
const detectedClientId = detectClientFromRequest(request);
|
||||
|
||||
// 获取调试信息
|
||||
const debugInfo = getRequestDebugInfo(request);
|
||||
|
||||
// 获取当前URL信息
|
||||
const url = new URL(request.url);
|
||||
|
||||
return json({
|
||||
config,
|
||||
detectedClientId,
|
||||
debugInfo,
|
||||
serverInfo: {
|
||||
url: url.href,
|
||||
host: url.host,
|
||||
port: url.port,
|
||||
pathname: url.pathname
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端配置测试页面组件
|
||||
*/
|
||||
export default function ClientConfigTest() {
|
||||
const data = useLoaderData<typeof loader>();
|
||||
|
||||
// 浏览器端检测客户端ID
|
||||
const browserClientId = typeof window !== 'undefined' ? (() => {
|
||||
const port = window.location.port;
|
||||
const portToClient: Record<string, string> = {
|
||||
'5174': 'client-a',
|
||||
'5175': 'client-b',
|
||||
'5176': 'client-c',
|
||||
'5177': 'client-d'
|
||||
};
|
||||
return port && portToClient[port] ? portToClient[port] : 'unknown';
|
||||
})() : 'server-side';
|
||||
|
||||
const browserPort = typeof window !== 'undefined' ? window.location.port : 'server-side';
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 py-8">
|
||||
<div className="max-w-4xl mx-auto px-4">
|
||||
<div className="bg-white rounded-lg shadow-lg p-6">
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-6">
|
||||
🧪 客户端配置测试页面
|
||||
</h1>
|
||||
|
||||
{/* 基本信息 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||
<div className="bg-blue-50 p-4 rounded-lg">
|
||||
<h2 className="text-lg font-semibold text-blue-900 mb-3">📍 访问信息</h2>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div><strong>当前URL:</strong> {data.serverInfo.url}</div>
|
||||
<div><strong>主机:</strong> {data.serverInfo.host}</div>
|
||||
<div><strong>端口:</strong> {data.serverInfo.port || '默认端口'}</div>
|
||||
<div><strong>路径:</strong> {data.serverInfo.pathname}</div>
|
||||
<div><strong>浏览器端口:</strong> {browserPort}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-50 p-4 rounded-lg">
|
||||
<h2 className="text-lg font-semibold text-green-900 mb-3">🎯 客户端检测</h2>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div><strong>服务器端检测:</strong>
|
||||
<span className={`ml-2 px-2 py-1 rounded text-xs ${
|
||||
data.detectedClientId !== 'main' ? 'bg-green-200 text-green-800' : 'bg-yellow-200 text-yellow-800'
|
||||
}`}>
|
||||
{data.detectedClientId}
|
||||
</span>
|
||||
</div>
|
||||
<div><strong>浏览器端检测:</strong>
|
||||
<span className={`ml-2 px-2 py-1 rounded text-xs ${
|
||||
browserClientId !== 'unknown' ? 'bg-green-200 text-green-800' : 'bg-yellow-200 text-yellow-800'
|
||||
}`}>
|
||||
{browserClientId}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Nginx请求头信息 */}
|
||||
<div className="mb-8">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-3">🔍 Nginx请求头信息</h2>
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<strong>X-Client-ID:</strong>
|
||||
<span className={`ml-2 px-2 py-1 rounded text-xs ${
|
||||
data.debugInfo.clientId ? 'bg-green-200 text-green-800' : 'bg-red-200 text-red-800'
|
||||
}`}>
|
||||
{data.debugInfo.clientId || '未检测到'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>X-Original-Port:</strong>
|
||||
<span className={`ml-2 px-2 py-1 rounded text-xs ${
|
||||
data.debugInfo.originalPort ? 'bg-green-200 text-green-800' : 'bg-red-200 text-red-800'
|
||||
}`}>
|
||||
{data.debugInfo.originalPort || '未检测到'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>X-Forwarded-Port:</strong>
|
||||
<span className="ml-2 px-2 py-1 rounded text-xs bg-gray-200 text-gray-800">
|
||||
{data.debugInfo.forwardedPort || '未设置'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>X-Real-IP:</strong>
|
||||
<span className="ml-2 px-2 py-1 rounded text-xs bg-gray-200 text-gray-800">
|
||||
{data.debugInfo.realIp || '未设置'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 当前配置信息 */}
|
||||
<div className="mb-8">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-3">⚙️ 当前API配置</h2>
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
||||
<div><strong>Base URL:</strong> {data.config.baseUrl}</div>
|
||||
<div><strong>Upload URL:</strong> {data.config.uploadUrl}</div>
|
||||
<div><strong>Document URL:</strong> {data.config.documentUrl}</div>
|
||||
<div><strong>OAuth Server:</strong> {data.config.oauth.serverUrl}</div>
|
||||
<div><strong>OAuth Redirect:</strong> {data.config.oauth.redirectUri}</div>
|
||||
<div><strong>OAuth Client ID:</strong> {data.config.oauth.clientId.substring(0, 20)}...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 状态检查 */}
|
||||
<div className="mb-8">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-3">✅ 状态检查</h2>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className={`w-3 h-3 rounded-full ${
|
||||
data.debugInfo.clientId ? 'bg-green-500' : 'bg-red-500'
|
||||
}`}></div>
|
||||
<span className={data.debugInfo.clientId ? 'text-green-700' : 'text-red-700'}>
|
||||
{data.debugInfo.clientId ? '✅ Nginx X-Client-ID 传递正常' : '❌ Nginx X-Client-ID 未传递'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className={`w-3 h-3 rounded-full ${
|
||||
data.detectedClientId !== 'main' ? 'bg-green-500' : 'bg-yellow-500'
|
||||
}`}></div>
|
||||
<span className={data.detectedClientId !== 'main' ? 'text-green-700' : 'text-yellow-700'}>
|
||||
{data.detectedClientId !== 'main' ? '✅ 服务器端客户端检测成功' : '⚠️ 服务器端使用默认配置'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className={`w-3 h-3 rounded-full ${
|
||||
browserClientId !== 'unknown' ? 'bg-green-500' : 'bg-yellow-500'
|
||||
}`}></div>
|
||||
<span className={browserClientId !== 'unknown' ? 'text-green-700' : 'text-yellow-700'}>
|
||||
{browserClientId !== 'unknown' ? '✅ 浏览器端客户端检测成功' : '⚠️ 浏览器端使用默认配置'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className={`w-3 h-3 rounded-full ${
|
||||
data.config.baseUrl.includes(data.detectedClientId.replace('client-', '517')) ? 'bg-green-500' : 'bg-yellow-500'
|
||||
}`}></div>
|
||||
<span className={data.config.baseUrl.includes(data.detectedClientId.replace('client-', '517')) ? 'text-green-700' : 'text-yellow-700'}>
|
||||
{data.config.baseUrl.includes(data.detectedClientId.replace('client-', '517')) ? '✅ 配置匹配正确' : '⚠️ 配置可能不匹配'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 调试信息 */}
|
||||
<div className="mb-8">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-3">🔧 完整调试信息</h2>
|
||||
<details className="bg-gray-50 p-4 rounded-lg">
|
||||
<summary className="cursor-pointer text-sm font-medium text-gray-700 mb-2">
|
||||
点击查看详细信息
|
||||
</summary>
|
||||
<pre className="text-xs bg-white p-3 rounded border overflow-auto">
|
||||
{JSON.stringify({
|
||||
serverData: data,
|
||||
browserInfo: {
|
||||
clientId: browserClientId,
|
||||
port: browserPort,
|
||||
userAgent: typeof window !== 'undefined' ? navigator.userAgent : 'server-side'
|
||||
}
|
||||
}, null, 2)}
|
||||
</pre>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
{/* 测试链接 */}
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-3">🔗 其他客户端测试链接</h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
{[
|
||||
{ port: '5174', client: 'client-a', name: '客户端A' },
|
||||
{ port: '5175', client: 'client-b', name: '客户端B' },
|
||||
{ port: '5176', client: 'client-c', name: '客户端C' },
|
||||
{ port: '5177', client: 'client-d', name: '客户端D' }
|
||||
].map(({ port, client, name }) => (
|
||||
<a
|
||||
key={port}
|
||||
href={`http://localhost:${port}/test/client-config`}
|
||||
className={`block p-3 rounded-lg text-center text-sm font-medium transition-colors ${
|
||||
browserPort === port
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-blue-100 text-blue-700 hover:bg-blue-200'
|
||||
}`}
|
||||
>
|
||||
{name}<br/>
|
||||
<span className="text-xs opacity-75">:{port}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 text-xs text-gray-500">
|
||||
最后更新: {data.timestamp}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 9999;
|
||||
z-index: 99999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 客户端检测工具函数
|
||||
* 用于在服务器端从请求头中获取客户端信息
|
||||
*/
|
||||
|
||||
/**
|
||||
* 从请求头中检测客户端ID
|
||||
* @param request - Remix Request对象
|
||||
* @returns 客户端ID字符串
|
||||
*/
|
||||
export const detectClientFromRequest = (request: Request): string => {
|
||||
// 从Nginx传递的头部获取客户端ID
|
||||
const clientId = request.headers.get('X-Client-ID');
|
||||
const originalPort = request.headers.get('X-Original-Port');
|
||||
|
||||
if (clientId) {
|
||||
console.log(`🎯 检测到客户端ID: ${clientId} (端口: ${originalPort})`);
|
||||
return clientId;
|
||||
}
|
||||
|
||||
// 根据端口映射客户端ID(备用方案)
|
||||
const portToClient: Record<string, string> = {
|
||||
'5174': 'client-a',
|
||||
'5175': 'client-b',
|
||||
'5176': 'client-c',
|
||||
'5177': 'client-d'
|
||||
};
|
||||
|
||||
if (originalPort && portToClient[originalPort]) {
|
||||
console.log(`🎯 通过端口映射检测到客户端: ${portToClient[originalPort]} (端口: ${originalPort})`);
|
||||
return portToClient[originalPort];
|
||||
}
|
||||
|
||||
console.log('⚠️ 未能检测到客户端ID,使用默认值: main');
|
||||
return 'main';
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取请求的调试信息
|
||||
* @param request - Remix Request对象
|
||||
* @returns 调试信息对象
|
||||
*/
|
||||
export const getRequestDebugInfo = (request: Request) => {
|
||||
return {
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
clientId: request.headers.get('X-Client-ID'),
|
||||
originalPort: request.headers.get('X-Original-Port'),
|
||||
forwardedPort: request.headers.get('X-Forwarded-Port'),
|
||||
realIp: request.headers.get('X-Real-IP'),
|
||||
forwardedFor: request.headers.get('X-Forwarded-For'),
|
||||
userAgent: request.headers.get('User-Agent')
|
||||
};
|
||||
};
|
||||
@@ -1,255 +0,0 @@
|
||||
#!/bin/bash
|
||||
# 多客户端部署脚本
|
||||
# 用于部署和管理3个不同地区客户端的反向代理服务
|
||||
|
||||
set -e
|
||||
|
||||
# 颜色输出
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 日志函数
|
||||
log_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
log_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# 检查依赖
|
||||
check_dependencies() {
|
||||
log_info "检查系统依赖..."
|
||||
|
||||
# 检查PM2
|
||||
if ! command -v pm2 &> /dev/null; then
|
||||
log_error "PM2 未安装,请先安装: npm install -g pm2"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查Nginx
|
||||
if ! command -v nginx &> /dev/null; then
|
||||
log_warning "Nginx 未安装,请手动安装并配置"
|
||||
fi
|
||||
|
||||
# 检查Node.js
|
||||
if ! command -v node &> /dev/null; then
|
||||
log_error "Node.js 未安装"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_success "依赖检查完成"
|
||||
}
|
||||
|
||||
# 构建项目
|
||||
build_project() {
|
||||
log_info "构建项目..."
|
||||
|
||||
# 安装依赖
|
||||
if [ -f "package-lock.json" ]; then
|
||||
npm ci
|
||||
else
|
||||
npm install
|
||||
fi
|
||||
|
||||
# 构建项目
|
||||
npm run build
|
||||
|
||||
log_success "项目构建完成"
|
||||
}
|
||||
|
||||
# 创建日志目录
|
||||
create_log_dirs() {
|
||||
log_info "创建日志目录..."
|
||||
|
||||
mkdir -p logs
|
||||
mkdir -p /var/log/nginx 2>/dev/null || log_warning "无法创建nginx日志目录,请手动创建"
|
||||
|
||||
log_success "日志目录创建完成"
|
||||
}
|
||||
|
||||
# 部署PM2应用
|
||||
deploy_pm2() {
|
||||
log_info "部署PM2应用..."
|
||||
|
||||
# 停止现有应用
|
||||
pm2 delete all 2>/dev/null || log_warning "没有运行中的PM2应用"
|
||||
|
||||
# 启动新应用
|
||||
pm2 start ecosystem.config.cjs
|
||||
|
||||
# 保存PM2配置
|
||||
pm2 save
|
||||
|
||||
# 设置开机自启
|
||||
pm2 startup
|
||||
|
||||
log_success "PM2应用部署完成"
|
||||
}
|
||||
|
||||
# 配置Nginx
|
||||
configure_nginx() {
|
||||
log_info "配置Nginx..."
|
||||
|
||||
# 检查配置文件
|
||||
if [ ! -f "nginx-multi-client.conf" ]; then
|
||||
log_error "nginx-multi-client.conf 文件不存在"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 复制配置文件到nginx目录
|
||||
if [ -d "/etc/nginx/sites-available" ]; then
|
||||
sudo cp nginx-multi-client.conf /etc/nginx/sites-available/docreview-multi-client
|
||||
sudo ln -sf /etc/nginx/sites-available/docreview-multi-client /etc/nginx/sites-enabled/
|
||||
elif [ -d "/etc/nginx/conf.d" ]; then
|
||||
sudo cp nginx-multi-client.conf /etc/nginx/conf.d/docreview-multi-client.conf
|
||||
else
|
||||
log_warning "请手动配置Nginx,配置文件: nginx-multi-client.conf"
|
||||
return
|
||||
fi
|
||||
|
||||
# 测试nginx配置
|
||||
sudo nginx -t
|
||||
|
||||
# 重载nginx
|
||||
sudo systemctl reload nginx
|
||||
|
||||
log_success "Nginx配置完成"
|
||||
}
|
||||
|
||||
# 检查服务状态
|
||||
check_status() {
|
||||
log_info "检查服务状态..."
|
||||
|
||||
echo "\n=== PM2 应用状态 ==="
|
||||
pm2 status
|
||||
|
||||
echo "\n=== 端口监听状态 ==="
|
||||
netstat -tlnp | grep -E ':(51701|51702|51703|51704)'
|
||||
|
||||
echo "\n=== 服务健康检查 ==="
|
||||
for port in 51701 51702 51703 51704; do
|
||||
if curl -s "http://10.79.97.17:$port/health" > /dev/null; then
|
||||
log_success "端口 $port: 正常"
|
||||
else
|
||||
log_error "端口 $port: 异常"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# 显示帮助信息
|
||||
show_help() {
|
||||
echo "多客户端部署脚本"
|
||||
echo ""
|
||||
echo "用法: $0 [选项]"
|
||||
echo ""
|
||||
echo "选项:"
|
||||
echo " deploy 完整部署(构建+PM2+Nginx)"
|
||||
echo " build 仅构建项目"
|
||||
echo " pm2 仅部署PM2应用"
|
||||
echo " nginx 仅配置Nginx"
|
||||
echo " status 检查服务状态"
|
||||
echo " stop 停止所有服务"
|
||||
echo " restart 重启所有服务"
|
||||
echo " logs 查看日志"
|
||||
echo " help 显示帮助信息"
|
||||
echo ""
|
||||
echo "客户端访问地址:"
|
||||
echo " 客户端A: http://10.79.97.17:51701"
|
||||
echo " 客户端B: http://10.79.97.17:51702"
|
||||
echo " 客户端C: http://10.79.97.17:51704"
|
||||
echo " 主服务: http://10.79.97.17:51703"
|
||||
}
|
||||
|
||||
# 停止服务
|
||||
stop_services() {
|
||||
log_info "停止服务..."
|
||||
pm2 stop all
|
||||
log_success "服务已停止"
|
||||
}
|
||||
|
||||
# 重启服务
|
||||
restart_services() {
|
||||
log_info "重启服务..."
|
||||
pm2 restart all
|
||||
log_success "服务已重启"
|
||||
}
|
||||
|
||||
# 查看日志
|
||||
show_logs() {
|
||||
echo "选择要查看的日志:"
|
||||
echo "1) 主服务日志"
|
||||
echo "2) 客户端A日志"
|
||||
echo "3) 客户端B日志"
|
||||
echo "4) 客户端C日志"
|
||||
echo "5) 所有日志"
|
||||
read -p "请选择 (1-5): " choice
|
||||
|
||||
case $choice in
|
||||
1) pm2 logs docreview-main ;;
|
||||
2) pm2 logs docreview-client-a ;;
|
||||
3) pm2 logs docreview-client-b ;;
|
||||
4) pm2 logs docreview-client-c ;;
|
||||
5) pm2 logs ;;
|
||||
*) log_error "无效选择" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# 主函数
|
||||
main() {
|
||||
case "${1:-help}" in
|
||||
"deploy")
|
||||
check_dependencies
|
||||
create_log_dirs
|
||||
build_project
|
||||
deploy_pm2
|
||||
configure_nginx
|
||||
check_status
|
||||
;;
|
||||
"build")
|
||||
build_project
|
||||
;;
|
||||
"pm2")
|
||||
check_dependencies
|
||||
create_log_dirs
|
||||
deploy_pm2
|
||||
;;
|
||||
"nginx")
|
||||
configure_nginx
|
||||
;;
|
||||
"status")
|
||||
check_status
|
||||
;;
|
||||
"stop")
|
||||
stop_services
|
||||
;;
|
||||
"restart")
|
||||
restart_services
|
||||
;;
|
||||
"logs")
|
||||
show_logs
|
||||
;;
|
||||
"help")
|
||||
show_help
|
||||
;;
|
||||
*)
|
||||
log_error "未知选项: $1"
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# 执行主函数
|
||||
main "$@"
|
||||
@@ -1,169 +0,0 @@
|
||||
# Nginx本地开发环境多客户端配置
|
||||
# 基于api-config.ts中的开发环境配置
|
||||
# 用于本地测试多客户端反向代理功能
|
||||
|
||||
# 上游服务器配置 - 指向本地开发服务器
|
||||
upstream docreview_local {
|
||||
server 127.0.0.1:5173; # Vite开发服务器
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
# 客户端A - 端口8001 (本地测试)
|
||||
server {
|
||||
listen 8001;
|
||||
server_name localhost 127.0.0.1;
|
||||
|
||||
# 访问日志
|
||||
access_log logs/local-client-a-access.log;
|
||||
error_log logs/local-client-a-error.log;
|
||||
|
||||
# 客户端标识
|
||||
set $client_id "client-a";
|
||||
|
||||
location / {
|
||||
# 反向代理到本地开发服务器
|
||||
proxy_pass http://docreview_local;
|
||||
|
||||
# 设置代理头部
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Client-ID $client_id;
|
||||
|
||||
# 开发环境特殊配置
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
# 连接设置
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_send_timeout 30s;
|
||||
proxy_read_timeout 30s;
|
||||
|
||||
# 禁用缓冲以支持热重载
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
|
||||
# 支持WebSocket (Vite HMR)
|
||||
proxy_http_version 1.1;
|
||||
}
|
||||
|
||||
# 健康检查
|
||||
location /health {
|
||||
access_log off;
|
||||
return 200 "Local Client A - OK";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
}
|
||||
|
||||
# 客户端B - 端口8002 (本地测试)
|
||||
server {
|
||||
listen 8002;
|
||||
server_name localhost 127.0.0.1;
|
||||
|
||||
# 访问日志
|
||||
access_log logs/local-client-b-access.log;
|
||||
error_log logs/local-client-b-error.log;
|
||||
|
||||
# 客户端标识
|
||||
set $client_id "client-b";
|
||||
|
||||
location / {
|
||||
# 反向代理到本地开发服务器
|
||||
proxy_pass http://docreview_local;
|
||||
|
||||
# 设置代理头部
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Client-ID $client_id;
|
||||
|
||||
# 开发环境特殊配置
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
# 连接设置
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_send_timeout 30s;
|
||||
proxy_read_timeout 30s;
|
||||
|
||||
# 禁用缓冲以支持热重载
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
|
||||
# 支持WebSocket (Vite HMR)
|
||||
proxy_http_version 1.1;
|
||||
}
|
||||
|
||||
# 健康检查
|
||||
location /health {
|
||||
access_log off;
|
||||
return 200 "Local Client B - OK";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
}
|
||||
|
||||
# 客户端C - 端口8003 (本地测试)
|
||||
server {
|
||||
listen 8003;
|
||||
server_name localhost 127.0.0.1;
|
||||
|
||||
# 访问日志
|
||||
access_log logs/local-client-c-access.log;
|
||||
error_log logs/local-client-c-error.log;
|
||||
|
||||
# 客户端标识
|
||||
set $client_id "client-c";
|
||||
|
||||
location / {
|
||||
# 反向代理到本地开发服务器
|
||||
proxy_pass http://docreview_local;
|
||||
|
||||
# 设置代理头部
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Client-ID $client_id;
|
||||
|
||||
# 开发环境特殊配置
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
# 连接设置
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_send_timeout 30s;
|
||||
proxy_read_timeout 30s;
|
||||
|
||||
# 禁用缓冲以支持热重载
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
|
||||
# 支持WebSocket (Vite HMR)
|
||||
proxy_http_version 1.1;
|
||||
}
|
||||
|
||||
# 健康检查
|
||||
location /health {
|
||||
access_log off;
|
||||
return 200 "Local Client C - OK";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
}
|
||||
|
||||
# 全局配置
|
||||
# 错误页面
|
||||
error_page 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root html;
|
||||
}
|
||||
|
||||
# 开发环境安全头部(相对宽松)
|
||||
add_header X-Frame-Options SAMEORIGIN;
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
|
||||
# 开发环境CORS支持
|
||||
add_header Access-Control-Allow-Origin *;
|
||||
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
|
||||
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,X-Client-ID";
|
||||
@@ -1,162 +0,0 @@
|
||||
# Nginx多客户端反向代理配置
|
||||
# 为3个不同地区客户端提供独立端口访问
|
||||
# 所有请求最终转发到主服务 10.79.97.17:51703
|
||||
|
||||
# 上游服务器配置
|
||||
upstream docreview_main {
|
||||
server 10.79.97.17:51703;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
# 客户端A - 端口51701
|
||||
server {
|
||||
listen 51701;
|
||||
server_name 10.79.97.17;
|
||||
|
||||
# 访问日志
|
||||
access_log /var/log/nginx/client-a-access.log;
|
||||
error_log /var/log/nginx/client-a-error.log;
|
||||
|
||||
# 客户端标识
|
||||
set $client_id "client-a";
|
||||
|
||||
location / {
|
||||
# 反向代理到主服务
|
||||
proxy_pass http://docreview_main;
|
||||
|
||||
# 设置代理头部
|
||||
proxy_set_header Host $host:51703;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Client-ID $client_id;
|
||||
|
||||
# 连接设置
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_send_timeout 30s;
|
||||
proxy_read_timeout 30s;
|
||||
|
||||
# 缓冲设置
|
||||
proxy_buffering on;
|
||||
proxy_buffer_size 4k;
|
||||
proxy_buffers 8 4k;
|
||||
|
||||
# WebSocket支持
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
|
||||
# 健康检查
|
||||
location /health {
|
||||
access_log off;
|
||||
return 200 "Client A - OK";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
}
|
||||
|
||||
# 客户端B - 端口51702
|
||||
server {
|
||||
listen 51702;
|
||||
server_name 10.79.97.17;
|
||||
|
||||
# 访问日志
|
||||
access_log /var/log/nginx/client-b-access.log;
|
||||
error_log /var/log/nginx/client-b-error.log;
|
||||
|
||||
# 客户端标识
|
||||
set $client_id "client-b";
|
||||
|
||||
location / {
|
||||
# 反向代理到主服务
|
||||
proxy_pass http://docreview_main;
|
||||
|
||||
# 设置代理头部
|
||||
proxy_set_header Host $host:51703;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Client-ID $client_id;
|
||||
|
||||
# 连接设置
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_send_timeout 30s;
|
||||
proxy_read_timeout 30s;
|
||||
|
||||
# 缓冲设置
|
||||
proxy_buffering on;
|
||||
proxy_buffer_size 4k;
|
||||
proxy_buffers 8 4k;
|
||||
|
||||
# WebSocket支持
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
|
||||
# 健康检查
|
||||
location /health {
|
||||
access_log off;
|
||||
return 200 "Client B - OK";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
}
|
||||
|
||||
# 客户端C - 端口51704
|
||||
server {
|
||||
listen 51704;
|
||||
server_name 10.79.97.17;
|
||||
|
||||
# 访问日志
|
||||
access_log /var/log/nginx/client-c-access.log;
|
||||
error_log /var/log/nginx/client-c-error.log;
|
||||
|
||||
# 客户端标识
|
||||
set $client_id "client-c";
|
||||
|
||||
location / {
|
||||
# 反向代理到主服务
|
||||
proxy_pass http://docreview_main;
|
||||
|
||||
# 设置代理头部
|
||||
proxy_set_header Host $host:51703;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Client-ID $client_id;
|
||||
|
||||
# 连接设置
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_send_timeout 30s;
|
||||
proxy_read_timeout 30s;
|
||||
|
||||
# 缓冲设置
|
||||
proxy_buffering on;
|
||||
proxy_buffer_size 4k;
|
||||
proxy_buffers 8 4k;
|
||||
|
||||
# WebSocket支持
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
|
||||
# 健康检查
|
||||
location /health {
|
||||
access_log off;
|
||||
return 200 "Client C - OK";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
}
|
||||
|
||||
# 全局配置
|
||||
# 错误页面
|
||||
error_page 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
|
||||
# 安全头部
|
||||
add_header X-Frame-Options DENY;
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
@@ -0,0 +1,377 @@
|
||||
# Ubuntu环境下的Nginx优化配置
|
||||
# 支持多客户端代理和动态请求头传递
|
||||
|
||||
# 上游服务器配置 - 指向开发服务器
|
||||
upstream vite_dev_server {
|
||||
server 172.16.0.34:5173;
|
||||
# 连接池配置,提高性能
|
||||
keepalive 32;
|
||||
# 失败重试配置
|
||||
# server 172.16.0.34:5173 backup; # 备用服务器(可选)
|
||||
}
|
||||
|
||||
# 后端 API 服务器配置
|
||||
upstream api_client_a {
|
||||
server 172.16.0.34:5174;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
upstream api_client_b {
|
||||
server 172.16.0.34:5175;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
upstream api_client_c {
|
||||
server 172.16.0.34:5176;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
upstream api_client_d {
|
||||
server 172.16.0.34:5177;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
# 日志格式定义 - 包含客户端标识
|
||||
log_format client_access '$remote_addr - $remote_user [$time_local] '
|
||||
'"$request" $status $body_bytes_sent '
|
||||
'"$http_referer" "$http_user_agent" '
|
||||
'client_id="$client_id" original_port="$server_port"';
|
||||
|
||||
# 客户端A配置 (端口5174)
|
||||
server {
|
||||
listen 5174;
|
||||
server_name localhost 127.0.0.1;
|
||||
|
||||
# 设置客户端标识变量
|
||||
set $client_id "client-a";
|
||||
|
||||
# 访问日志 - 包含客户端信息
|
||||
access_log /var/log/nginx/client-a-access.log client_access;
|
||||
error_log /var/log/nginx/client-a-error.log warn;
|
||||
|
||||
# 主要代理配置
|
||||
location / {
|
||||
# 反向代理到开发服务器
|
||||
proxy_pass http://vite_dev_server;
|
||||
|
||||
# 基础代理头部
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# 客户端特定头部 - 用于应用识别客户端
|
||||
proxy_set_header X-Client-ID $client_id;
|
||||
proxy_set_header X-Original-Port $server_port;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
|
||||
# 开发环境特殊配置 - 支持Vite热重载
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_http_version 1.1;
|
||||
|
||||
# 连接超时配置
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_send_timeout 30s;
|
||||
proxy_read_timeout 30s;
|
||||
|
||||
# 禁用缓冲以支持实时更新
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_request_buffering off;
|
||||
|
||||
# 处理大文件上传
|
||||
client_max_body_size 100M;
|
||||
|
||||
# 开发环境安全头部(相对宽松)
|
||||
add_header X-Frame-Options SAMEORIGIN always;
|
||||
add_header X-Content-Type-Options nosniff always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
|
||||
# 开发环境CORS支持
|
||||
add_header Access-Control-Allow-Origin * always;
|
||||
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS, PATCH" always;
|
||||
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,X-Client-ID,X-Original-Port,X-Forwarded-Port" always;
|
||||
add_header Access-Control-Allow-Credentials true always;
|
||||
}
|
||||
|
||||
# 健康检查端点
|
||||
location /health {
|
||||
access_log off;
|
||||
return 200 "Client A (Port 5174) - OK\n";
|
||||
add_header Content-Type text/plain;
|
||||
add_header X-Client-ID $client_id;
|
||||
}
|
||||
|
||||
# API代理特殊处理 - 修改为代理到对应的后端API服务器
|
||||
location /api/ {
|
||||
proxy_pass http://api_client_a;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Client-ID $client_id;
|
||||
proxy_set_header X-Original-Port $server_port;
|
||||
|
||||
# API请求超时配置
|
||||
proxy_connect_timeout 10s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
|
||||
# 全局错误页面配置
|
||||
error_page 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
|
||||
# 处理OPTIONS预检请求
|
||||
if ($request_method = 'OPTIONS') {
|
||||
return 204;
|
||||
}
|
||||
}
|
||||
|
||||
# 客户端B配置 (端口5175)
|
||||
server {
|
||||
listen 5175;
|
||||
server_name localhost 127.0.0.1;
|
||||
|
||||
set $client_id "client-b";
|
||||
|
||||
access_log /var/log/nginx/client-b-access.log client_access;
|
||||
error_log /var/log/nginx/client-b-error.log warn;
|
||||
|
||||
location / {
|
||||
proxy_pass http://vite_dev_server;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
proxy_set_header X-Client-ID $client_id;
|
||||
proxy_set_header X-Original-Port $server_port;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_http_version 1.1;
|
||||
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_send_timeout 30s;
|
||||
proxy_read_timeout 30s;
|
||||
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_request_buffering off;
|
||||
|
||||
client_max_body_size 100M;
|
||||
|
||||
# 开发环境安全头部(相对宽松)
|
||||
add_header X-Frame-Options SAMEORIGIN always;
|
||||
add_header X-Content-Type-Options nosniff always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
|
||||
# 开发环境CORS支持
|
||||
add_header Access-Control-Allow-Origin * always;
|
||||
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS, PATCH" always;
|
||||
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,X-Client-ID,X-Original-Port,X-Forwarded-Port" always;
|
||||
add_header Access-Control-Allow-Credentials true always;
|
||||
}
|
||||
|
||||
location /health {
|
||||
access_log off;
|
||||
return 200 "Client B (Port 5175) - OK\n";
|
||||
add_header Content-Type text/plain;
|
||||
add_header X-Client-ID $client_id;
|
||||
}
|
||||
|
||||
# API代理特殊处理 - 修改为代理到对应的后端API服务器
|
||||
location /api/ {
|
||||
proxy_pass http://api_client_b;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Client-ID $client_id;
|
||||
proxy_set_header X-Original-Port $server_port;
|
||||
|
||||
# API请求超时配置
|
||||
proxy_connect_timeout 10s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
|
||||
# 全局错误页面配置
|
||||
error_page 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
|
||||
# 处理OPTIONS预检请求
|
||||
if ($request_method = 'OPTIONS') {
|
||||
return 204;
|
||||
}
|
||||
}
|
||||
|
||||
# 客户端C配置 (端口5176)
|
||||
server {
|
||||
listen 5176;
|
||||
server_name localhost 127.0.0.1;
|
||||
|
||||
set $client_id "client-c";
|
||||
|
||||
access_log /var/log/nginx/client-c-access.log client_access;
|
||||
error_log /var/log/nginx/client-c-error.log warn;
|
||||
|
||||
location / {
|
||||
proxy_pass http://vite_dev_server;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
proxy_set_header X-Client-ID $client_id;
|
||||
proxy_set_header X-Original-Port $server_port;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_http_version 1.1;
|
||||
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_send_timeout 30s;
|
||||
proxy_read_timeout 30s;
|
||||
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_request_buffering off;
|
||||
|
||||
client_max_body_size 100M;
|
||||
|
||||
# 开发环境安全头部(相对宽松)
|
||||
add_header X-Frame-Options SAMEORIGIN always;
|
||||
add_header X-Content-Type-Options nosniff always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
|
||||
# 开发环境CORS支持
|
||||
add_header Access-Control-Allow-Origin * always;
|
||||
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS, PATCH" always;
|
||||
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,X-Client-ID,X-Original-Port,X-Forwarded-Port" always;
|
||||
add_header Access-Control-Allow-Credentials true always;
|
||||
}
|
||||
|
||||
location /health {
|
||||
access_log off;
|
||||
return 200 "Client C (Port 5176) - OK\n";
|
||||
add_header Content-Type text/plain;
|
||||
add_header X-Client-ID $client_id;
|
||||
}
|
||||
|
||||
# API代理特殊处理 - 修改为代理到对应的后端API服务器
|
||||
location /api/ {
|
||||
proxy_pass http://api_client_c;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Client-ID $client_id;
|
||||
proxy_set_header X-Original-Port $server_port;
|
||||
|
||||
# API请求超时配置
|
||||
proxy_connect_timeout 10s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
|
||||
# 全局错误页面配置
|
||||
error_page 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
|
||||
# 处理OPTIONS预检请求
|
||||
if ($request_method = 'OPTIONS') {
|
||||
return 204;
|
||||
}
|
||||
}
|
||||
|
||||
# 客户端D配置 (端口5177) - 预留扩展
|
||||
server {
|
||||
listen 5177;
|
||||
server_name localhost 127.0.0.1;
|
||||
|
||||
set $client_id "client-d";
|
||||
|
||||
access_log /var/log/nginx/client-d-access.log client_access;
|
||||
error_log /var/log/nginx/client-d-error.log warn;
|
||||
|
||||
location / {
|
||||
proxy_pass http://vite_dev_server;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
proxy_set_header X-Client-ID $client_id;
|
||||
proxy_set_header X-Original-Port $server_port;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_http_version 1.1;
|
||||
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_send_timeout 30s;
|
||||
proxy_read_timeout 30s;
|
||||
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_request_buffering off;
|
||||
|
||||
client_max_body_size 100M;
|
||||
|
||||
# 开发环境安全头部(相对宽松)
|
||||
add_header X-Frame-Options SAMEORIGIN always;
|
||||
add_header X-Content-Type-Options nosniff always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
|
||||
# 开发环境CORS支持
|
||||
add_header Access-Control-Allow-Origin * always;
|
||||
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS, PATCH" always;
|
||||
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,X-Client-ID,X-Original-Port,X-Forwarded-Port" always;
|
||||
add_header Access-Control-Allow-Credentials true always;
|
||||
}
|
||||
|
||||
location /health {
|
||||
access_log off;
|
||||
return 200 "Client D (Port 5177) - OK\n";
|
||||
add_header Content-Type text/plain;
|
||||
add_header X-Client-ID $client_id;
|
||||
}
|
||||
|
||||
# API代理特殊处理 - 修改为代理到对应的后端API服务器
|
||||
location /api/ {
|
||||
proxy_pass http://api_client_d;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Client-ID $client_id;
|
||||
proxy_set_header X-Original-Port $server_port;
|
||||
|
||||
# API请求超时配置
|
||||
proxy_connect_timeout 10s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
|
||||
# 全局错误页面配置
|
||||
error_page 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
|
||||
# 处理OPTIONS预检请求
|
||||
if ($request_method = 'OPTIONS') {
|
||||
return 204;
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
server {
|
||||
listen 5174;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
proxy_pass http://172.16.0.34:5173;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 5175;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
proxy_pass http://172.16.0.34:5173;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 5176;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
proxy_pass http://172.16.0.34:5173;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 5177;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
proxy_pass http://172.16.0.34:5173;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Ubuntu环境下Nginx多客户端配置快速部署脚本
|
||||
# 使用方法: chmod +x ubuntu-nginx-setup.sh && ./ubuntu-nginx-setup.sh
|
||||
|
||||
set -e # 遇到错误立即退出
|
||||
|
||||
# 颜色定义
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 日志函数
|
||||
log_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
log_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# 检查是否为root用户
|
||||
check_root() {
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
log_warning "检测到root用户,建议使用sudo执行此脚本"
|
||||
fi
|
||||
}
|
||||
|
||||
# 检查系统要求
|
||||
check_requirements() {
|
||||
log_info "检查系统要求..."
|
||||
|
||||
# 检查操作系统
|
||||
if [[ ! -f /etc/os-release ]]; then
|
||||
log_error "无法检测操作系统版本"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
. /etc/os-release
|
||||
log_info "操作系统: $PRETTY_NAME"
|
||||
|
||||
# 检查网络连接
|
||||
if ! ping -c 1 172.16.0.34 &> /dev/null; then
|
||||
log_warning "无法连接到开发服务器 172.16.0.34,请确保网络连接正常"
|
||||
fi
|
||||
|
||||
log_success "系统要求检查完成"
|
||||
}
|
||||
|
||||
# 安装Nginx
|
||||
install_nginx() {
|
||||
log_info "检查Nginx安装状态..."
|
||||
|
||||
if command -v nginx &> /dev/null; then
|
||||
NGINX_VERSION=$(nginx -v 2>&1 | cut -d' ' -f3 | cut -d'/' -f2)
|
||||
log_info "Nginx已安装,版本: $NGINX_VERSION"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "安装Nginx..."
|
||||
sudo apt update
|
||||
sudo apt install -y nginx
|
||||
|
||||
# 启动并启用Nginx服务
|
||||
sudo systemctl start nginx
|
||||
sudo systemctl enable nginx
|
||||
|
||||
log_success "Nginx安装完成"
|
||||
}
|
||||
|
||||
# 备份原始配置
|
||||
backup_config() {
|
||||
log_info "备份原始Nginx配置..."
|
||||
|
||||
BACKUP_DIR="/etc/nginx/backup-$(date +%Y%m%d-%H%M%S)"
|
||||
sudo mkdir -p "$BACKUP_DIR"
|
||||
|
||||
# 备份主要配置文件
|
||||
if [[ -f /etc/nginx/nginx.conf ]]; then
|
||||
sudo cp /etc/nginx/nginx.conf "$BACKUP_DIR/"
|
||||
fi
|
||||
|
||||
if [[ -f /etc/nginx/sites-available/default ]]; then
|
||||
sudo cp /etc/nginx/sites-available/default "$BACKUP_DIR/"
|
||||
fi
|
||||
|
||||
# 备份现有的conf.d配置
|
||||
if [[ -d /etc/nginx/conf.d ]]; then
|
||||
sudo cp -r /etc/nginx/conf.d "$BACKUP_DIR/"
|
||||
fi
|
||||
|
||||
log_success "配置已备份到: $BACKUP_DIR"
|
||||
}
|
||||
|
||||
# 创建多客户端配置
|
||||
create_multi_client_config() {
|
||||
log_info "创建多客户端Nginx配置..."
|
||||
|
||||
# 确保conf.d目录存在
|
||||
sudo mkdir -p /etc/nginx/conf.d
|
||||
|
||||
# 检查配置文件是否存在
|
||||
if [[ ! -f "nginx-ubuntu-optimized.conf" ]]; then
|
||||
log_error "找不到nginx-ubuntu-optimized.conf文件,请确保文件在当前目录"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 复制配置文件
|
||||
sudo cp nginx-ubuntu-optimized.conf /etc/nginx/conf.d/multi-client.conf
|
||||
|
||||
# 设置正确的权限
|
||||
sudo chown root:root /etc/nginx/conf.d/multi-client.conf
|
||||
sudo chmod 644 /etc/nginx/conf.d/multi-client.conf
|
||||
|
||||
log_success "多客户端配置已创建"
|
||||
}
|
||||
|
||||
# 创建日志目录
|
||||
setup_logging() {
|
||||
log_info "设置日志目录..."
|
||||
|
||||
# 创建客户端日志目录
|
||||
sudo mkdir -p /var/log/nginx/clients
|
||||
|
||||
# 设置权限
|
||||
sudo chown -R www-data:www-data /var/log/nginx
|
||||
sudo chmod -R 755 /var/log/nginx
|
||||
|
||||
# 创建日志轮转配置
|
||||
sudo tee /etc/logrotate.d/nginx-clients > /dev/null <<EOF
|
||||
/var/log/nginx/client-*-access.log {
|
||||
daily
|
||||
missingok
|
||||
rotate 52
|
||||
compress
|
||||
delaycompress
|
||||
notifempty
|
||||
create 644 www-data www-data
|
||||
postrotate
|
||||
systemctl reload nginx
|
||||
endscript
|
||||
}
|
||||
|
||||
/var/log/nginx/client-*-error.log {
|
||||
daily
|
||||
missingok
|
||||
rotate 52
|
||||
compress
|
||||
delaycompress
|
||||
notifempty
|
||||
create 644 www-data www-data
|
||||
postrotate
|
||||
systemctl reload nginx
|
||||
endscript
|
||||
}
|
||||
EOF
|
||||
|
||||
log_success "日志配置完成"
|
||||
}
|
||||
|
||||
# 配置防火墙
|
||||
setup_firewall() {
|
||||
log_info "配置防火墙规则..."
|
||||
|
||||
# 检查ufw是否安装
|
||||
if command -v ufw &> /dev/null; then
|
||||
# 允许Nginx端口
|
||||
sudo ufw allow 5174/tcp comment "Nginx Client A"
|
||||
sudo ufw allow 5175/tcp comment "Nginx Client B"
|
||||
sudo ufw allow 5176/tcp comment "Nginx Client C"
|
||||
sudo ufw allow 5177/tcp comment "Nginx Client D"
|
||||
|
||||
log_success "防火墙规则已配置"
|
||||
else
|
||||
log_warning "ufw未安装,跳过防火墙配置"
|
||||
fi
|
||||
}
|
||||
|
||||
# 验证配置
|
||||
validate_config() {
|
||||
log_info "验证Nginx配置..."
|
||||
|
||||
# 测试配置语法
|
||||
if sudo nginx -t; then
|
||||
log_success "Nginx配置语法正确"
|
||||
else
|
||||
log_error "Nginx配置语法错误,请检查配置文件"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 重启Nginx服务
|
||||
restart_nginx() {
|
||||
log_info "重启Nginx服务..."
|
||||
|
||||
sudo systemctl reload nginx
|
||||
|
||||
# 检查服务状态
|
||||
if sudo systemctl is-active --quiet nginx; then
|
||||
log_success "Nginx服务运行正常"
|
||||
else
|
||||
log_error "Nginx服务启动失败"
|
||||
sudo systemctl status nginx
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 测试端口监听
|
||||
test_ports() {
|
||||
log_info "测试端口监听状态..."
|
||||
|
||||
PORTS=(5174 5175 5176 5177)
|
||||
|
||||
for port in "${PORTS[@]}"; do
|
||||
if ss -tuln | grep -q ":$port "; then
|
||||
log_success "端口 $port 监听正常"
|
||||
else
|
||||
log_error "端口 $port 未监听"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# 测试健康检查
|
||||
test_health_checks() {
|
||||
log_info "测试健康检查端点..."
|
||||
|
||||
PORTS=(5174 5175 5176 5177)
|
||||
CLIENTS=("Client A" "Client B" "Client C" "Client D")
|
||||
|
||||
for i in "${!PORTS[@]}"; do
|
||||
port=${PORTS[$i]}
|
||||
client=${CLIENTS[$i]}
|
||||
|
||||
if curl -s "http://localhost:$port/health" | grep -q "OK"; then
|
||||
log_success "$client (端口 $port) 健康检查通过"
|
||||
else
|
||||
log_warning "$client (端口 $port) 健康检查失败"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# 显示测试命令
|
||||
show_test_commands() {
|
||||
log_info "测试命令示例:"
|
||||
|
||||
echo -e "\n${YELLOW}1. 健康检查:${NC}"
|
||||
echo " curl http://localhost:5174/health"
|
||||
echo " curl http://localhost:5175/health"
|
||||
echo " curl http://localhost:5176/health"
|
||||
echo " curl http://localhost:5177/health"
|
||||
|
||||
echo -e "\n${YELLOW}2. 测试代理功能:${NC}"
|
||||
echo " curl -v http://localhost:5174/ 2>&1 | grep 'X-Client-ID'"
|
||||
echo " curl -v http://localhost:5175/ 2>&1 | grep 'X-Client-ID'"
|
||||
|
||||
echo -e "\n${YELLOW}3. 监控日志:${NC}"
|
||||
echo " sudo tail -f /var/log/nginx/client-a-access.log"
|
||||
echo " sudo tail -f /var/log/nginx/client-*-error.log"
|
||||
|
||||
echo -e "\n${YELLOW}4. 浏览器测试:${NC}"
|
||||
echo " http://$(hostname -I | awk '{print $1}'):5174"
|
||||
echo " http://$(hostname -I | awk '{print $1}'):5175"
|
||||
echo " http://$(hostname -I | awk '{print $1}'):5176"
|
||||
echo " http://$(hostname -I | awk '{print $1}'):5177"
|
||||
|
||||
echo -e "\n${YELLOW}5. 环境变量测试:${NC}"
|
||||
echo " CLIENT_ID=client-a npm run dev"
|
||||
echo " CLIENT_ID=client-b npm run dev"
|
||||
}
|
||||
|
||||
# 显示管理命令
|
||||
show_management_commands() {
|
||||
log_info "管理命令:"
|
||||
|
||||
echo -e "\n${YELLOW}Nginx服务管理:${NC}"
|
||||
echo " sudo systemctl start nginx # 启动服务"
|
||||
echo " sudo systemctl stop nginx # 停止服务"
|
||||
echo " sudo systemctl restart nginx # 重启服务"
|
||||
echo " sudo systemctl reload nginx # 重新加载配置"
|
||||
echo " sudo systemctl status nginx # 查看状态"
|
||||
|
||||
echo -e "\n${YELLOW}配置管理:${NC}"
|
||||
echo " sudo nginx -t # 测试配置"
|
||||
echo " sudo nginx -s reload # 重新加载"
|
||||
|
||||
echo -e "\n${YELLOW}日志查看:${NC}"
|
||||
echo " sudo tail -f /var/log/nginx/error.log"
|
||||
echo " sudo tail -f /var/log/nginx/access.log"
|
||||
|
||||
echo -e "\n${YELLOW}端口检查:${NC}"
|
||||
echo " sudo ss -tuln | grep :517"
|
||||
echo " sudo lsof -i :5174"
|
||||
}
|
||||
|
||||
# 主函数
|
||||
main() {
|
||||
echo -e "${GREEN}======================================${NC}"
|
||||
echo -e "${GREEN} Ubuntu Nginx多客户端配置部署脚本 ${NC}"
|
||||
echo -e "${GREEN}======================================${NC}"
|
||||
echo
|
||||
|
||||
check_root
|
||||
check_requirements
|
||||
install_nginx
|
||||
backup_config
|
||||
create_multi_client_config
|
||||
setup_logging
|
||||
setup_firewall
|
||||
validate_config
|
||||
restart_nginx
|
||||
|
||||
echo
|
||||
log_success "多客户端Nginx配置部署完成!"
|
||||
echo
|
||||
|
||||
test_ports
|
||||
test_health_checks
|
||||
|
||||
echo
|
||||
show_test_commands
|
||||
show_management_commands
|
||||
|
||||
echo
|
||||
log_info "部署完成!现在可以开始测试多客户端功能。"
|
||||
log_info "请确保开发服务器在 172.16.0.34:5173 上运行。"
|
||||
echo
|
||||
}
|
||||
|
||||
# 脚本入口
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
main "$@"
|
||||
fi
|
||||
+2
-2
@@ -27,8 +27,8 @@ export default defineConfig({
|
||||
},
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 5173,
|
||||
// port: Number(process.env.PORT) || 5173,
|
||||
// port: 5173,
|
||||
port: Number(process.env.PORT) || 5173,
|
||||
open: true,
|
||||
// open: false,
|
||||
allowedHosts: ['nas.7bm.co', 'localhost', '127.0.0.1'], // 允许的主机名列表1
|
||||
|
||||
Reference in New Issue
Block a user