-
Notifications
You must be signed in to change notification settings - Fork 28
Expand file tree
/
Copy pathserver.js
More file actions
195 lines (164 loc) · 5.54 KB
/
server.js
File metadata and controls
195 lines (164 loc) · 5.54 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
/**
* API中间件服务器
* 用于隐藏真实API路径,转发API请求到后端
*/
const express = require('express');
const cors = require('cors');
const morgan = require('morgan');
const path = require('path');
const { config } = require('./config');
const apiRoutes = require('./routes');
const fs = require('fs');
const crypto = require('crypto');
// 创建Express应用
const app = express();
// 处理多域名 CORS 允许列表(逗号分隔)
const corsOriginRaw = config.CORS_ORIGIN && config.CORS_ORIGIN !== '' ? config.CORS_ORIGIN : '*';
let corsOrigin;
if (corsOriginRaw === '*' ) {
corsOrigin = '*';
} else {
// 生成白名单数组
const corsWhitelist = corsOriginRaw.split(',').map(o => o.trim()).filter(Boolean);
// 按请求 Origin 动态返回单一值,避免浏览器报多值冲突
corsOrigin = function(origin, callback) {
// 无 Origin(如 curl/Postman)或白名单为空直接放行
if (!origin || corsWhitelist.length === 0) {
return callback(null, true);
}
// * 通配直接放行
if (corsWhitelist.includes('*')) {
return callback(null, true);
}
// 精确匹配放行
if (corsWhitelist.includes(origin)) {
return callback(null, true);
}
// 不在白名单,拒绝
return callback(new Error('Not allowed by CORS'));
};
}
// console.log(`CORS源配置: ${corsOrigin}`);
// 添加预检请求处理
app.options('*', cors()); // 启用对所有路由的OPTIONS请求处理
// 禁用ETag生成,防止304缓存问题
app.set('etag', false);
app.disable('etag');
// CORS配置
app.use(cors({
origin: corsOrigin,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
credentials: true
}));
// 启用请求日志
if (config.ENABLE_LOGGING) {
app.use(morgan('dev'));
}
// 解析JSON请求体
app.use(express.json({ limit: '10mb' }));
// 解析URL编码的请求体
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
// 提供静态文件
app.use(express.static(path.join(__dirname), {
index: false,
maxAge: '24h', // 服务端缓存时间为24小时
immutable: true,
etag: true
}));
// APP 配置接口 - 支持缓存 config.APP_CACHE_SECONDS 秒,可通过 ?nocache=1 强制刷新
// 组合最终路由路径,确保不会出现双斜杠
const appConfigRoute = `${config.API_PREFIX.replace(/\/$/, '')}${config.APP_ENDPOINT.startsWith('/') ? '' : '/'}${config.APP_ENDPOINT}`;
app.get(appConfigRoute, (req, res) => {
const { nocache } = req.query;
try {
const filePath = config.APP_CONFIG_FILE;
const fileContent = fs.readFileSync(filePath, 'utf8');
// Base64 编码
const encodedContent = Buffer.from(fileContent, 'utf8').toString('base64');
// 若请求带 nocache=1,则关闭缓存并返回最新数据
if (nocache === '1') {
res.set({
'Cache-Control': 'no-store, no-cache, must-revalidate, private, max-age=0',
'Pragma': 'no-cache',
'Expires': '0'
});
return res.json({ data: encodedContent, encoding: 'base64' });
}
// 计算 ETag
const etag = crypto.createHash('md5').update(encodedContent).digest('hex');
// 如果客户端已有最新版本,返回 304
if (req.headers['if-none-match'] === etag) {
return res.status(304).end();
}
// 设置缓存头,缓存 config.APP_CACHE_SECONDS 秒
res.set({
'ETag': etag,
'Cache-Control': `public, max-age=${config.APP_CACHE_SECONDS}`
});
return res.json({ data: encodedContent, encoding: 'base64' });
} catch (err) {
console.error('读取 APP 配置文件出错:', err);
return res.status(500).json({ error: true, message: '服务器读取配置错误' });
}
});
// 根路径显示伪站点
// 根路径显示伪站点
app.get('/', (_req, res) => {
res.sendFile(path.join(__dirname, 'index.html'));
});
// 设置API路由 - 放在静态文件和根路径之后
app.use(config.API_PREFIX, apiRoutes);
// 健康检查路由
app.get('/ping', (_req, res) => {
res.status(200).json({
status: 'UP',
message: 'pong',
timestamp: new Date().toISOString()
});
});
// 404处理
app.use((req, res) => {
// 检查请求路径是否以API前缀开头
if (req.path.startsWith(config.API_PREFIX)) {
// API请求返回JSON 404
res.status(404).json({
error: true,
message: '404 Not Found'
});
} else {
// 非API请求重定向到伪站点
res.redirect('/');
}
});
// 错误处理中间件
app.use((err, req, res, next) => {
console.error('服务器错误:', err);
// 记录更详细的错误信息
console.error(`URL: ${req.method} ${req.url}`);
console.error(`请求头: ${JSON.stringify(req.headers)}`);
// 返回统一的错误格式
res.status(500).json({
error: true,
message: 'Server Error',
detail: err.message
});
});
// 启动服务器
const PORT = config.PORT;
app.listen(PORT, () => {
console.log(`API中间件服务器已启动,监听端口: ${PORT}`);
console.log(`中间件API前缀: ${config.API_PREFIX}`);
console.log(`后端API地址: ${config.BACKEND_API_URL}`);
console.log(`CORS源: ${corsOrigin}`);
console.log(`允许的源地址: ${config.ALLOWED_ORIGINS}`);
console.log(`允许的支付回调路径: ${config.ALLOWED_PAYMENT_NOTIFY_PATHS} (默认为空)`);
});
// 处理未捕获的异常
process.on('uncaughtException', (err) => {
console.error('未捕获的异常:', err);
});
// 处理未处理的Promise拒绝
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的Promise拒绝:', reason);
});