添加nginx相关配置,首页系统概览添加用户id查询

This commit is contained in:
2025-07-24 09:42:39 +08:00
parent 8800e982ab
commit 913702ea10
8 changed files with 1204 additions and 135 deletions
+279
View File
@@ -0,0 +1,279 @@
# 多客户端部署方案说明
## 概述
本方案实现了基于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
+112 -113
View File
@@ -1,4 +1,4 @@
import { postgrestGet, type PostgrestParams } from "../postgrest-client";
import { postgrestGet, postgrestPost, type PostgrestParams } from "../postgrest-client";
import dayjs from 'dayjs';
/**
@@ -96,7 +96,7 @@ function buildTypeFilter(reviewType: string | null): string {
* @param reviewType 从客户端传入的 reviewType 值
* @returns 主页数据
*/
export async function getHomeData(reviewType?: string | null): Promise<HomeStatistics> {
export async function getHomeData(reviewType?: string | null,userId?: string | number): Promise<HomeStatistics> {
try {
// 获取当前日期和时间相关值
const startOfToday = dayjs().startOf('day').format('YYYY-MM-DD HH:mm:ss');
@@ -105,7 +105,8 @@ export async function getHomeData(reviewType?: string | null): Promise<HomeStati
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('传入的 reviewType', reviewType);
console.log('传入的 userId', userId);
// 基于 reviewType 构建类型过滤条件
const typeFilter = buildTypeFilter(reviewType || null);
@@ -146,7 +147,8 @@ export async function getHomeData(reviewType?: string | null): Promise<HomeStati
filter: {
or: `(audit_status.eq.0,audit_status.eq.2,audit_status.is.null)`,
created_at: `gte.${startOfToday}`,
is_test_document: `eq.false`
is_test_document: `eq.false`,
user_id: `eq.${userId}`
}
};
@@ -180,7 +182,8 @@ export async function getHomeData(reviewType?: string | null): Promise<HomeStati
filter: {
and: `(audit_status.neq.0,audit_status.neq.2)`,
updated_at: `gte.${startOfThisMonth}`,
is_test_document: `eq.false`
is_test_document: `eq.false`,
user_id: `eq.${userId}`
}
};
@@ -211,7 +214,8 @@ export async function getHomeData(reviewType?: string | null): Promise<HomeStati
filter: {
or: `(audit_status.eq.1,audit_status.eq.-1)`,
and: `(updated_at.gte.${startOfLastMonth},updated_at.lte.${endOfLastMonth})`,
is_test_document: `eq.false`
is_test_document: `eq.false`,
user_id: `eq.${userId}`
}
};
@@ -258,7 +262,8 @@ export async function getHomeData(reviewType?: string | null): Promise<HomeStati
filter: {
audit_status: `eq.1`,
created_at: `gte.${startOfThisMonth}`,
is_test_document: `eq.false`
is_test_document: `eq.false`,
user_id: `eq.${userId}`
}
};
@@ -294,7 +299,8 @@ export async function getHomeData(reviewType?: string | null): Promise<HomeStati
filter: {
audit_status: `eq.1`,
and: `(updated_at.gte.${startOfLastMonth},updated_at.lte.${endOfLastMonth})`,
is_test_document: `eq.false`
is_test_document: `eq.false`,
user_id: `eq.${userId}`
}
};
@@ -352,15 +358,18 @@ export async function getHomeData(reviewType?: string | null): Promise<HomeStati
// 根据 reviewType 设置要查询的文档类型
if (reviewType === 'contract') {
// 合同类型 - 直接查询类型 1
const typeToQuery = 1;
const typeToQuery = [1];
// 调用数据库函数获取本月指定类型的问题数量
const thisMonthIssuesResponse = await handleApiResponse<{ count: number }[]>(
postgrestGet(`rpc/count_evaluation_results_by_type?type_val=${typeToQuery}&start_time=${startOfThisMonth}&end_time=${endOfThisMonth}`, {
select: '*',
filter: {}
postgrestPost('rpc/count_evaluation_results_by_type', {
start_time: startOfThisMonth,
end_time: endOfThisMonth,
type_val: typeToQuery,
userid: parseInt(userId as string)
}),
'获取本月问题数据失败',
'获取合同本月问题数据失败',
[]
);
@@ -369,9 +378,11 @@ export async function getHomeData(reviewType?: string | null): Promise<HomeStati
// 调用数据库函数获取上月指定类型的问题数量
const lastMonthIssuesResponse = await handleApiResponse<{ count: number }[]>(
postgrestGet(`rpc/count_evaluation_results_by_type?type_val=${typeToQuery}&start_time=${startOfLastMonth}&end_time=${endOfLastMonth}`, {
select: '*',
filter: {}
postgrestPost('rpc/count_evaluation_results_by_type', {
start_time: startOfLastMonth,
end_time: endOfLastMonth,
type_val: typeToQuery,
userid: parseInt(userId as string)
}),
'获取上月问题数据失败',
[]
@@ -382,120 +393,108 @@ export async function getHomeData(reviewType?: string | null): Promise<HomeStati
} else if (reviewType === 'record') {
// 记录类型 - 需要查询类型 2 和类型 3,并合并结果
const typeToQuery = [2,3];
// 查询类型 2 的本月问题数量
const thisMonthType2Response = await handleApiResponse<{ count: number }[]>(
postgrestGet(`rpc/count_evaluation_results_by_type?type_val=2&start_time=${startOfThisMonth}&end_time=${endOfThisMonth}`, {
select: '*',
filter: {}
postgrestPost('rpc/count_evaluation_results_by_type', {
start_time: startOfThisMonth,
end_time: endOfThisMonth,
type_val: typeToQuery,
userid: parseInt(userId as string)
}),
'获取本月类型2问题数据失败',
'获取本月许可卷宗类型2问题数据失败',
[]
);
// 查询类型 3 的本月问题数量
const thisMonthType3Response = await handleApiResponse<{ count: number }[]>(
postgrestGet(`rpc/count_evaluation_results_by_type?type_val=3&start_time=${startOfThisMonth}&end_time=${endOfThisMonth}`, {
select: '*',
filter: {}
}),
'获取本月类型3问题数据失败',
[]
);
// 合并本月两种类型的问题数量
// 本月两种类型的问题数量
const thisMonthType2Count = thisMonthType2Response[0]?.count || 0;
const thisMonthType3Count = thisMonthType3Response[0]?.count || 0;
thisMonthIssuesCount = thisMonthType2Count + thisMonthType3Count;
thisMonthIssuesCount = thisMonthType2Count
// 查询类型 2 的上月问题数量
// 上月两种类型的问题数量
const lastMonthType2Response = await handleApiResponse<{ count: number }[]>(
postgrestGet(`rpc/count_evaluation_results_by_type?type_val=2&start_time=${startOfLastMonth}&end_time=${endOfLastMonth}`, {
select: '*',
filter: {}
postgrestPost('rpc/count_evaluation_results_by_type', {
start_time: startOfLastMonth,
end_time: endOfLastMonth,
type_val: typeToQuery,
userid: parseInt(userId as string)
}),
'获取上月类型2问题数据失败',
'获取上月许可卷宗类型2问题数据失败',
[]
);
// 查询类型 3 的上月问题数量
const lastMonthType3Response = await handleApiResponse<{ count: number }[]>(
postgrestGet(`rpc/count_evaluation_results_by_type?type_val=3&start_time=${startOfLastMonth}&end_time=${endOfLastMonth}`, {
select: '*',
filter: {}
}),
'获取上月类型3问题数据失败',
[]
);
// 合并上月两种类型的问题数量
// 上月两种类型的问题数量
const lastMonthType2Count = lastMonthType2Response[0]?.count || 0;
const lastMonthType3Count = lastMonthType3Response[0]?.count || 0;
lastMonthIssuesCount = lastMonthType2Count + lastMonthType3Count;
lastMonthIssuesCount = lastMonthType2Count
} else {
// 如果没有指定类型,则使用原来的查询方式获取所有类型的问题数量
const thisMonthIssuesParams: PostgrestParams = {
select: 'count',
filter: {
and: `(created_at.gte.${startOfThisMonth},created_at.lte.${endOfThisMonth})`,
'evaluated_results->result': 'eq.false' // 使用->操作符访问JSONB字段
}
};
// 添加类型过滤条件
if (typeFilter) {
if (typeFilter.startsWith('(')) {
thisMonthIssuesParams.or = typeFilter;
} else {
const [field, op, value] = typeFilter.split('.');
if (!thisMonthIssuesParams.filter) {
thisMonthIssuesParams.filter = {};
}
thisMonthIssuesParams.filter[field] = `${op}.${value}`;
}
}
const thisMonthIssuesResponse = await handleApiResponse<{ count: number }[]>(
postgrestGet('evaluation_results', thisMonthIssuesParams),
'获取本月问题数据失败',
[]
);
// 本月问题数量
thisMonthIssuesCount = thisMonthIssuesResponse[0]?.count || 0;
// 上月问题数量
const lastMonthIssuesParams: PostgrestParams = {
select: 'count',
filter: {
and: `(created_at.gte.${startOfLastMonth},created_at.lte.${endOfLastMonth})`,
'evaluated_results->result': 'eq.false' // 使用->操作符访问JSONB字段
}
};
// 添加类型过滤条件
if (typeFilter) {
if (typeFilter.startsWith('(')) {
lastMonthIssuesParams.or = typeFilter;
} else {
const [field, op, value] = typeFilter.split('.');
if (!lastMonthIssuesParams.filter) {
lastMonthIssuesParams.filter = {};
}
lastMonthIssuesParams.filter[field] = `${op}.${value}`;
}
}
const lastMonthIssuesResponse = await handleApiResponse<{ count: number }[]>(
postgrestGet('evaluation_results', lastMonthIssuesParams),
'获取上月问题数据失败',
[]
);
// 上月问题数量
lastMonthIssuesCount = lastMonthIssuesResponse[0]?.count || 0;
}
// 暂时不会存在没有指定类型得情况,暂不实现。
// else {
// // 如果没有指定类型,则使用原来的查询方式获取所有类型的问题数量
// const thisMonthIssuesParams: PostgrestParams = {
// select: 'count',
// filter: {
// and: `(created_at.gte.${startOfThisMonth},created_at.lte.${endOfThisMonth})`,
// 'evaluated_results->result': 'eq.false',
// user_id: `eq.${userId}`
// }
// };
// // 添加类型过滤条件
// if (typeFilter) {
// if (typeFilter.startsWith('(')) {
// thisMonthIssuesParams.or = typeFilter;
// } else {
// const [field, op, value] = typeFilter.split('.');
// if (!thisMonthIssuesParams.filter) {
// thisMonthIssuesParams.filter = {};
// }
// thisMonthIssuesParams.filter[field] = `${op}.${value}`;
// }
// }
// const thisMonthIssuesResponse = await handleApiResponse<{ count: number }[]>(
// postgrestGet('evaluation_results', thisMonthIssuesParams),
// '获取本月问题数据失败',
// []
// );
// // 本月问题数量
// thisMonthIssuesCount = thisMonthIssuesResponse[0]?.count || 0;
// // 上月问题数量
// const lastMonthIssuesParams: PostgrestParams = {
// select: 'count',
// filter: {
// and: `(created_at.gte.${startOfLastMonth},created_at.lte.${endOfLastMonth})`,
// 'evaluated_results->result': 'eq.false',
// user_id: `eq.${userId}`
// }
// };
// // 添加类型过滤条件
// if (typeFilter) {
// if (typeFilter.startsWith('(')) {
// lastMonthIssuesParams.or = typeFilter;
// } else {
// const [field, op, value] = typeFilter.split('.');
// if (!lastMonthIssuesParams.filter) {
// lastMonthIssuesParams.filter = {};
// }
// lastMonthIssuesParams.filter[field] = `${op}.${value}`;
// }
// }
// const lastMonthIssuesResponse = await handleApiResponse<{ count: number }[]>(
// postgrestGet('evaluation_results', lastMonthIssuesParams),
// '获取上月问题数据失败',
// []
// );
// // 上月问题数量
// lastMonthIssuesCount = lastMonthIssuesResponse[0]?.count || 0;
// }
// 计算问题数量同比增长
let issuesGrowthValue = 0;
+110 -6
View File
@@ -44,7 +44,7 @@ const configs: Record<string, ApiConfig> = {
serverUrl: 'http://10.79.112.85', // IDaaS服务器地址
clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO',
clientSecret: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb', // 需要替换为实际的Client Secret
redirectUri: 'http://10.79.97.17/', // 回调地址
redirectUri: 'http://10.79.97.17/callback', // 回调地址
appId: 'idaasoauth2' // 应用ID,用于登出
}
},
@@ -75,7 +75,8 @@ const configs: Record<string, ApiConfig> = {
serverUrl: 'http://10.79.112.85', // IDaaS服务器地址
clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO',
clientSecret: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb', // 需要替换为实际的Client Secret
redirectUri: 'http://10.79.97.17/', // 回调地址
redirectUri: 'http://10.79.97.17/callback', // 回调地址
appId: 'idaasoauth2' // 应用ID,用于登出
}
},
@@ -95,12 +96,97 @@ const configs: Record<string, ApiConfig> = {
}
};
// 客户端特定配置 - 支持多客户端部署
// 根据环境自动选择配置
const getClientConfigs = (env: string): Record<string, Partial<ApiConfig>> => {
if (env === 'development') {
// 开发环境 - 本地nginx代理配置
return {
'client-a': {
baseUrl: 'http://localhost:8001',
uploadUrl: 'http://localhost:8001/admin/documents',
oauth: {
serverUrl: 'http://10.79.112.85',
clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO',
clientSecret: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb',
redirectUri: 'http://localhost:8001/callback',
appId: 'idaasoauth2'
}
},
'client-b': {
baseUrl: 'http://localhost:8002',
uploadUrl: 'http://localhost:8002/admin/documents',
oauth: {
serverUrl: 'http://10.79.112.85',
clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO',
clientSecret: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb',
redirectUri: 'http://localhost:8002/callback',
appId: 'idaasoauth2'
}
},
'client-c': {
baseUrl: 'http://localhost:8003',
uploadUrl: 'http://localhost:8003/admin/documents',
oauth: {
serverUrl: 'http://10.79.112.85',
clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO',
clientSecret: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb',
redirectUri: 'http://localhost:8003/callback',
appId: 'idaasoauth2'
}
}
};
} 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': {
baseUrl: 'http://10.79.97.17:51704',
uploadUrl: 'http://10.79.97.17:51704/admin/documents',
oauth: {
serverUrl: 'http://10.79.112.85',
clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO',
clientSecret: 'VYk1AC5XIJEfnEXwyq0u9JEY3fi3byCfSD58zANGeb',
redirectUri: 'http://10.79.97.17:51704/callback',
appId: 'idaasoauth2'
}
}
};
}
};
// 获取当前环境,默认为development
const getCurrentEnvironment = (): string => {
// 优先使用环境变量,然后使用 NODE_ENV
return process.env.NEXT_PUBLIC_API_ENV || process.env.NODE_ENV || 'development';
};
// 获取客户端ID
const getClientId = (): string => {
return process.env.CLIENT_ID || process.env.NEXT_PUBLIC_CLIENT_ID || 'main';
};
// 从环境变量获取配置,如果环境变量不存在则使用默认配置
const getConfigFromEnv = (defaultConfig: ApiConfig): ApiConfig => {
return {
@@ -117,17 +203,35 @@ const getConfigFromEnv = (defaultConfig: ApiConfig): ApiConfig => {
};
};
// 获取当前配置
// 获取当前配置 - 支持客户端特定配置
const getCurrentConfig = (): ApiConfig => {
const env = getCurrentEnvironment();
const clientId = getClientId();
const defaultConfig = configs[env] || configs.development;
// 获取当前环境的客户端特定配置
const clientConfigs = getClientConfigs(env);
const clientConfig = clientConfigs[clientId];
// 合并默认配置和客户端特定配置
let finalConfig = defaultConfig;
if (clientConfig) {
finalConfig = {
...defaultConfig,
...clientConfig,
oauth: {
...defaultConfig.oauth,
...clientConfig.oauth
}
};
}
// 如果是浏览器环境,尝试从环境变量覆盖配置
if (typeof window !== 'undefined' || process.env.NEXT_PUBLIC_API_BASE_URL) {
return getConfigFromEnv(defaultConfig);
return getConfigFromEnv(finalConfig);
}
return defaultConfig;
return finalConfig;
};
// 导出当前环境的配置
@@ -155,4 +259,4 @@ export const setEnvironment = (env: string): ApiConfig => {
// environment: getCurrentEnvironment(),
// config: apiConfig
// });
// }
// }
+18 -11
View File
@@ -11,7 +11,7 @@ import { getDocuments, type DocumentUI, type DocumentSearchParams } from "~/api/
import { useState, useEffect } from "react";
import { getHomeData } from "~/api/home/home";
import dayjs from 'dayjs';
import type { UserRole } from '~/api/login/auth.server';
// import type { UserRole } from '~/api/login/auth.server';
import { type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node";
import { logout, getUserSession } from "~/api/login/auth.server";
@@ -48,7 +48,8 @@ export const meta: MetaFunction = () => {
export async function loader({ request }: LoaderFunctionArgs) {
try {
// 从根loader获取用户角色
const { userRole } = await getUserSession(request);
const { userRole, userInfo, frontendJWT } = await getUserSession(request);
// 返回默认值,实际数据将在客户端根据 sessionStorage 加载
return Response.json({
@@ -63,7 +64,9 @@ export async function loader({ request }: LoaderFunctionArgs) {
},
recentFiles: [],
reviewType: null,
userRole: userRole
userRole: userRole,
userInfo,
frontendJWT
});
} catch (error) {
// 错误处理
@@ -89,7 +92,7 @@ export async function action({ request }: ActionFunctionArgs) {
export default function Home() {
const navigate = useNavigate();
const { homeData: initialHomeData, recentFiles: initialRecentFiles, userRole: serverUserRole } = useLoaderData<typeof loader>();
const { homeData: initialHomeData, recentFiles: initialRecentFiles, userRole: serverUserRole, userInfo } = useLoaderData<typeof loader>();
const [recentFiles, setRecentFiles] = useState<DocumentUI[]>(initialRecentFiles || []);
const [homeData, setHomeData] = useState(initialHomeData);
const [currentDateTime, setCurrentDateTime] = useState({
@@ -97,7 +100,7 @@ export default function Home() {
time: ''
});
const [isLoading, setIsLoading] = useState(true);
const userRole = serverUserRole as UserRole;
// const userRole = serverUserRole as UserRole;
// 打印服务器端传递的用户角色
useEffect(() => {
@@ -155,7 +158,7 @@ export default function Home() {
const reviewType = sessionStorage.getItem('reviewType');
// 加载主页数据
const newHomeData = await getHomeData(reviewType || undefined);
const newHomeData = await getHomeData(reviewType || undefined,userInfo.user_id);
setHomeData(newHomeData);
// 加载文档数据
@@ -177,7 +180,8 @@ export default function Home() {
try {
const documentSearchParams: DocumentSearchParams = {
page: 1,
pageSize: 10
pageSize: 10,
userId: userInfo.user_id
};
// 根据 reviewType 添加过滤条件
@@ -186,6 +190,7 @@ export default function Home() {
const response = await getDocuments(documentSearchParams);
if (!response.error && response.data) {
// console.log('合同文档数据',response.data.documents);
return response.data.documents;
}
} else if (reviewType === 'record') {
@@ -209,6 +214,7 @@ export default function Home() {
);
// 限制数量
// console.log('卷宗文档数据',mergedDocs);
return mergedDocs.slice(0, documentSearchParams.pageSize);
}
} else {
@@ -236,7 +242,7 @@ export default function Home() {
setIsLoading(true);
// 更新主页数据
const newHomeData = await getHomeData(currentReviewType || undefined);
const newHomeData = await getHomeData(currentReviewType || undefined,userInfo.user_id);
setHomeData(newHomeData);
// 更新文档数据
@@ -301,14 +307,15 @@ export default function Home() {
</div>
<div className="user-profile p-4 border-b border-gray-100 flex items-center">
<div className="avatar w-10 h-10 rounded-full bg-primary text-white flex items-center justify-center">
<span>{userRole === 'developer' ? '管' : '用'}</span>
<span>{userInfo.nick_name.charAt(userInfo.nick_name.length-1)}</span>
</div>
<div className="ml-1">
<p className="text-sm font-medium mb-0">{userRole === 'developer' ? '系统管理员' : '普通用户'}</p>
{/* <p className="text-sm font-medium mb-0">{userRole === 'developer' ? '系统管理员' : '普通用户'}</p> */}
<p className="text-sm font-medium mb-0">{userInfo.nick_name}</p>
{/* <p className="text-xs text-gray-500 mb-0">{userRole === 'developer' ? '超级管理员' : '标准权限'}</p> */}
</div>
</div>
{/* 登出操作 */}
{/* 登出操作 */}
<Button
type="default"
size="small"
+255
View File
@@ -0,0 +1,255 @@
#!/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 "$@"
+99 -5
View File
@@ -1,9 +1,11 @@
// ecosystem.config.cjs - CommonJS 版本
// 多客户端部署配置:支持3个不同地区客户端通过不同端口访问
module.exports = {
apps: [
// 主服务 - 生产环境 (端口: 51703)
{
name: 'docreview-frontend',
name: 'docreview-main',
script: 'node',
args: [
'-r', 'dotenv/config',
@@ -17,15 +19,107 @@ module.exports = {
env: {
NODE_ENV: 'production',
PORT: 51703,
CLIENT_ID: 'main'
},
env_testing: {
NODE_ENV: 'testing',
PORT: 51703,
CLIENT_ID: 'main'
},
error_file: './logs/err.log',
out_file: './logs/out.log',
log_file: './logs/combined.log',
error_file: './logs/main-err.log',
out_file: './logs/main-out.log',
log_file: './logs/main-combined.log',
time: true
},
// 客户端A - 反向代理服务 (端口: 51701)
{
name: 'docreview-client-a',
script: 'node',
args: [
'-r', 'dotenv/config',
'./node_modules/.bin/remix-serve',
'./build/server/index.js'
],
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: '1G',
env: {
NODE_ENV: 'production',
PORT: 51701,
CLIENT_ID: 'client-a',
PROXY_TARGET: 'http://10.79.97.17:51703'
},
env_testing: {
NODE_ENV: 'testing',
PORT: 51701,
CLIENT_ID: 'client-a',
PROXY_TARGET: 'http://10.79.97.17:51703'
},
error_file: './logs/client-a-err.log',
out_file: './logs/client-a-out.log',
log_file: './logs/client-a-combined.log',
time: true
},
// 客户端B - 反向代理服务 (端口: 51702)
{
name: 'docreview-client-b',
script: 'node',
args: [
'-r', 'dotenv/config',
'./node_modules/.bin/remix-serve',
'./build/server/index.js'
],
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: '1G',
env: {
NODE_ENV: 'production',
PORT: 51702,
CLIENT_ID: 'client-b',
PROXY_TARGET: 'http://10.79.97.17:51703'
},
env_testing: {
NODE_ENV: 'testing',
PORT: 51702,
CLIENT_ID: 'client-b',
PROXY_TARGET: 'http://10.79.97.17:51703'
},
error_file: './logs/client-b-err.log',
out_file: './logs/client-b-out.log',
log_file: './logs/client-b-combined.log',
time: true
},
// 客户端C - 反向代理服务 (端口: 51704)
{
name: 'docreview-client-c',
script: 'node',
args: [
'-r', 'dotenv/config',
'./node_modules/.bin/remix-serve',
'./build/server/index.js'
],
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: '1G',
env: {
NODE_ENV: 'production',
PORT: 51704,
CLIENT_ID: 'client-c',
PROXY_TARGET: 'http://10.79.97.17:51703'
},
env_testing: {
NODE_ENV: 'testing',
PORT: 51704,
CLIENT_ID: 'client-c',
PROXY_TARGET: 'http://10.79.97.17:51703'
},
error_file: './logs/client-c-err.log',
out_file: './logs/client-c-out.log',
log_file: './logs/client-c-combined.log',
time: true
}
],
};
};
+169
View File
@@ -0,0 +1,169 @@
# 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
@@ -0,0 +1,162 @@
# 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";