修复系统概览数据不准确的查询。修复交叉评查意见列表的数量查询。优化全局消息提示的层级。优化提交意见进行局部更新。

This commit is contained in:
2025-07-25 09:49:36 +08:00
parent 3dab54d551
commit ccd5cdf71e
29 changed files with 2444 additions and 1035 deletions
+213
View File
@@ -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` 并使用对应的配置!
-279
View File
@@ -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
+429
View File
@@ -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环境中测试多客户端功能提供了完整的基础设施支持。
+422
View File
@@ -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-5177Nginx监听端口
- 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
View File
@@ -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;
}
}
}
+12 -9
View File
@@ -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',
+2 -2
View File
@@ -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);
+2 -1
View File
@@ -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
View File
@@ -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}`
}
+2 -2
View File
@@ -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();
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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
View File
@@ -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 };
+2 -2
View File
@@ -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>
);
}
}
+15 -30
View File
@@ -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>
+5 -9
View File
@@ -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">
ziprar7ztar文件
zip7z文件
</div>
}
disabled={uploadType === 'single' || isUploading}
+14 -14
View File
@@ -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>
);
}
}
+2 -2
View File
@@ -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>
);
}
}
+249
View File
@@ -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>
);
}
+1 -1
View File
@@ -9,7 +9,7 @@
top: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 9999;
z-index: 99999;
display: flex;
flex-direction: column;
gap: 10px;
+54
View File
@@ -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')
};
};
-255
View File
@@ -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 "$@"
-169
View File
@@ -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";
-162
View File
@@ -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";
+377
View File
@@ -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
View File
@@ -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;
}
}
+342
View File
@@ -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
View File
@@ -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