代理服务器-MOCK

This commit is contained in:
dongzp 2026-03-13 14:21:12 +08:00
commit 9b427b8fa0
11 changed files with 347608 additions and 0 deletions

317
index.api.ts Normal file
View File

@ -0,0 +1,317 @@
import * as http from 'http';
import * as https from 'https';
import * as fs from 'fs';
import * as path from 'path';
const proxyPort = 9443;
const targetHost = "devrmtapp.resmart.cn";
// 配置文件路径
const CONFIG_FILE = path.join(__dirname, 'mock-routes.json');
// 定义类型
interface RouteConfig {
[route: string]: string;
}
interface AppConfig {
cacheConfig: boolean;
reloadOnChange: boolean;
defaultContentType: string;
}
interface ConfigFile {
routes: RouteConfig;
config: AppConfig;
}
// 存储当前的路由配置
let MOCK_ROUTES: RouteConfig = {};
let CONFIG: AppConfig = {
cacheConfig: true,
reloadOnChange: true,
defaultContentType: "application/json"
};
// 过滤掉以 # 开头的路由(视为注释,不参与 mock
function filterActiveRoutes(routes: RouteConfig): RouteConfig {
const filtered: RouteConfig = {};
for (const [route, filePath] of Object.entries(routes)) {
if (route.startsWith('#')) continue;
filtered[route] = filePath;
}
return filtered;
}
// 加载配置文件
function loadConfig() {
try {
if (!fs.existsSync(CONFIG_FILE)) {
console.warn(`[CONFIG] 配置文件不存在: ${CONFIG_FILE}`);
console.warn(`[CONFIG] 正在创建默认配置文件...`);
// 创建默认配置
const defaultConfig: ConfigFile = {
routes: {
"/api2/test": "mock/test.txt"
},
config: {
cacheConfig: true,
reloadOnChange: true,
defaultContentType: "application/json"
}
};
// 确保mock目录存在
const mockDir = path.join(__dirname, 'mock');
if (!fs.existsSync(mockDir)) {
fs.mkdirSync(mockDir, { recursive: true });
}
// 保存配置文件
fs.writeFileSync(CONFIG_FILE, JSON.stringify(defaultConfig, null, 2), 'utf-8');
console.log(`[CONFIG] 已创建默认配置文件: ${CONFIG_FILE}`);
// 加载配置(# 开头的路由视为注释,不参与 mock
const configData: ConfigFile = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
MOCK_ROUTES = filterActiveRoutes(configData.routes || {});
CONFIG = configData.config || CONFIG;
} else {
const configData: ConfigFile = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
MOCK_ROUTES = filterActiveRoutes(configData.routes || {});
CONFIG = configData.config || CONFIG;
console.log(`[CONFIG] 配置文件已加载,共 ${Object.keys(MOCK_ROUTES).length} 个路由`);
}
// 验证mock文件是否存在
validateMockFiles();
} catch (error) {
console.error(`[CONFIG] 加载配置文件失败:`, error);
// 使用默认配置
MOCK_ROUTES = {
"/api2/user/list": "mock/user_list.txt"
};
CONFIG = {
cacheConfig: true,
reloadOnChange: true,
defaultContentType: "application/json"
};
}
}
// 验证mock文件是否存在
function validateMockFiles() {
console.log(`[CONFIG] 验证mock文件...`);
let missingFiles = [];
for (const [route, filePath] of Object.entries(MOCK_ROUTES)) {
const fullPath = path.join(__dirname, filePath);
if (!fs.existsSync(fullPath)) {
missingFiles.push({ route, filePath, fullPath });
console.warn(`[CONFIG] 警告: mock文件不存在 - ${filePath} (用于路由: ${route})`);
}
}
if (missingFiles.length > 0) {
console.log(`[CONFIG] 缺少 ${missingFiles.length} 个mock文件请创建这些文件`);
} else {
console.log(`[CONFIG] 所有mock文件验证通过`);
}
}
// 检查是否为mock路由的函数
function isMockRoute(requestPath: string): boolean {
return MOCK_ROUTES.hasOwnProperty(requestPath);
}
// 获取mock文件路径
function getMockFilePath(requestPath: string): string {
return MOCK_ROUTES[requestPath];
}
// 代理服务器
const proxyServer = http.createServer((clientReq, clientRes) => {
// 解析客户端请求的 URL
const parsedUrl = new URL(`http://localhost${clientReq.url!}`);
const requestPath = parsedUrl.pathname;
// 检查是否为需要mock的路由
if (isMockRoute(requestPath)) {
const mockFile = getMockFilePath(requestPath);
console.log(`[MOCK] 拦截路由: ${requestPath} -> 使用文件: ${mockFile}`);
try {
// 构建完整的文件路径
const mockFilePath = path.join(__dirname, mockFile);
// 检查文件是否存在
if (!fs.existsSync(mockFilePath)) {
console.error(`[MOCK] Mock文件不存在: ${mockFilePath}`);
clientRes.writeHead(404, { 'Content-Type': 'application/json' });
clientRes.end(JSON.stringify({
error: 'Mock file not found',
route: requestPath,
file: mockFile,
timestamp: new Date().toISOString()
}));
return;
}
// 读取本地mock文件
const mockData = fs.readFileSync(mockFilePath, 'utf-8');
// 设置响应头
const contentType = CONFIG.defaultContentType || 'application/json';
clientRes.writeHead(200, {
'Content-Type': contentType,
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
'X-Mock-Source': mockFile,
'X-Mock-Timestamp': new Date().toISOString()
});
// 返回mock数据
clientRes.end(mockData);
console.log(`[MOCK] 成功返回mock数据: ${mockFile}`);
} catch (error) {
console.error(`[MOCK] 读取mock文件失败: ${mockFile}`, error);
clientRes.writeHead(500, { 'Content-Type': 'application/json' });
clientRes.end(JSON.stringify({
error: 'Failed to read mock data',
route: requestPath,
file: mockFile,
message: (error as Error).message,
timestamp: new Date().toISOString()
}));
}
return; // 直接返回,不转发到目标服务器
}
// 如果不是mock路由正常代理转发
console.log(`[PROXY] 转发请求: ${requestPath}`);
// 目标服务器的选项
const options: http.RequestOptions = {
hostname: targetHost,
port: 443, // HTTPS 默认端口
method: clientReq.method,
path: parsedUrl.pathname + parsedUrl.search,
headers: {
...clientReq.headers,
host: targetHost
}
};
// 创建到目标服务器的 HTTPS 请求
const proxyReq = https.request(options, (proxyRes) => {
// 将目标服务器的响应头复制到客户端响应
clientRes.writeHead(proxyRes.statusCode!, proxyRes.headers);
// 将目标服务器的响应数据管道传输到客户端
proxyRes.pipe(clientRes);
});
// 错误处理
proxyReq.on('error', (err) => {
console.error('Proxy request error:', err);
clientRes.writeHead(500, { 'Content-Type': 'application/json' });
clientRes.end(JSON.stringify({
error: 'Proxy error',
message: err.message,
timestamp: new Date().toISOString()
}));
});
// 将客户端请求体管道传输到代理请求
clientReq.pipe(proxyReq);
});
// 添加管理接口用于重新加载配置
proxyServer.on('request', (req, res) => {
const parsedUrl = new URL(`http://localhost${req.url!}`);
const pathname = parsedUrl.pathname;
// 管理接口:重新加载配置
if (pathname === '/__reload-config' && req.method === 'POST') {
try {
const oldCount = Object.keys(MOCK_ROUTES).length;
loadConfig();
const newCount = Object.keys(MOCK_ROUTES).length;
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: true,
message: 'Configuration reloaded successfully',
routesCount: newCount,
routesChanged: newCount - oldCount
}));
console.log(`[ADMIN] 通过API重新加载配置 (路由数: ${oldCount} -> ${newCount})`);
} catch (error) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: false,
error: (error as Error).message
}));
}
}
// 管理接口:查看当前配置
if (pathname === '/__config' && req.method === 'GET') {
try {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
routes: MOCK_ROUTES,
config: CONFIG,
timestamp: new Date().toISOString(),
totalRoutes: Object.keys(MOCK_ROUTES).length
}, null, 2));
} catch (error) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: false,
error: (error as Error).message
}));
}
}
});
// 初始化:第一次加载配置
loadConfig();
// 如果配置了文件监视,则监听配置文件变化
if (CONFIG.reloadOnChange) {
fs.watchFile(CONFIG_FILE, (curr, prev) => {
console.log(`[CONFIG] 配置文件已修改,重新加载...`);
try {
const oldRoutesCount = Object.keys(MOCK_ROUTES).length;
loadConfig();
const newRoutesCount = Object.keys(MOCK_ROUTES).length;
console.log(`[CONFIG] 配置重载完成 (路由数: ${oldRoutesCount} -> ${newRoutesCount})`);
} catch (error) {
console.error(`[CONFIG] 重新加载配置文件失败:`, error);
}
});
console.log(`[CONFIG] 已启用配置文件监视: ${CONFIG_FILE}`);
}
proxyServer.listen(proxyPort, "0.0.0.0", () => {
console.log(`========================================`);
console.log(`代理服务器运行在: http://localhost:${proxyPort}`);
console.log(`目标服务器: https://${targetHost}`);
console.log(`配置文件: ${CONFIG_FILE}`);
console.log(`已配置Mock路由: ${Object.keys(MOCK_ROUTES).length}`);
console.log(`========================================`);
console.log(`管理接口:`);
console.log(` GET http://localhost:${proxyPort}/__config 查看当前配置`);
console.log(` POST http://localhost:${proxyPort}/__reload-config 重新加载配置`);
console.log(`========================================`);
console.log(`Mock路由列表:`);
for (const [route, file] of Object.entries(MOCK_ROUTES)) {
const filePath = path.join(__dirname, file);
const exists = fs.existsSync(filePath) ? '✓' : '✗';
console.log(` ${exists} ${route} -> ${file}`);
}
console.log(`========================================`);
});

18
mock-routes.json Normal file
View File

@ -0,0 +1,18 @@
{
"routes": {
"#/api2/test": "mock/test.txt",
"#/api2/test2": "mock/test2.txt",
"#/api2/oxygenerator/report/day": "mock/oxygenerator_day.txt",
"#/api2/oxygenerator/report/multi/day": "mock/oxygenerator_days.txt",
"#/api2/ventilator/report/day" :"mock/ventilator_day.txt",
"#/api2/ventilator/report/multi/day" :"mock/ventilator_days.txt",
"#/api2/device/history/list" :"mock/device_list.txt",
"#/api2/health/service/getById" :"mock/service_failed.txt",
"#/api2/device/one": "mock/device_detail.txt"
},
"config": {
"cacheConfig": true,
"reloadOnChange": true,
"defaultContentType": "application/json"
}
}

19
mock/device_detail.txt Normal file
View File

@ -0,0 +1,19 @@
{
"code": 0,
"msg": "success",
"success": true,
"data": {
"serial": "A3120991235",
"id": 1112,
"name": "家用无创呼吸机",
"buyTime": 1773331200000,
"model": "G3 A20",
"bindTime": 1773381129000,
"lastUseTime": null,
"imageFile": {
"objKey": "test/bmc-file/common/a119dce5-84af-4a19-bbca-72bf2e5419e8.jpg",
"bucket": "new-c",
"mimeType": "image/jpeg"
}
}
}

48
mock/device_list.txt Normal file
View File

@ -0,0 +1,48 @@
{
"code": 0,
"msg": "success",
"success": true,
"data": [{
"serial": "A3120991234",
"id": 1149,
"name": "家用无创呼吸机",
"buyTime": 1773244800000,
"provinceId": 1,
"cityId": 2,
"buyChannel": 1,
"storeName": "干撒干撒",
"model": "G3 A20",
"storeId": null,
"bindTime": 1773302358000,
"lastUseTime": null,
"thumbnailFile": {
"objKey": "dev/bmc-file/common/2f4b2f40-e15c-42a4-90e6-1521a64df8e7.jpg",
"bucket": "new-c",
"mimeType": "image/jpeg"
},
"devicePageFile": null,
"deviceType": 3,
"nonProtocolDevice": true,
"ventilatorType": null,
"bluetooth": null
}, {
"serial": "AS124C01084",
"id": 1148,
"name": "家用无创呼吸机",
"buyTime": 1773244800000,
"provinceId": 1,
"cityId": 2,
"buyChannel": 1,
"storeName": "哈哈",
"model": "恬梦 A20",
"storeId": null,
"bindTime": 1773302335000,
"lastUseTime": null,
"thumbnailFile": null,
"devicePageFile": null,
"deviceType": 3,
"nonProtocolDevice": true,
"ventilatorType": null,
"bluetooth": null
}]
}

63
mock/oxygenerator_day.txt Normal file
View File

@ -0,0 +1,63 @@
{
"code": 0,
"msg": "success",
"success": true,
"data": {
"timeList": [
{
"remark": "短时佩戴 (30秒)",
"startDate": 1772154000000,
"endDate": 1772154030000
},
{
"remark": "午后记录 (45分钟)",
"startDate": 1772172000000,
"endDate": 1772174700000
},
{
"remark": "晚上记录 (70分钟)",
"startDate": 1772193600000,
"endDate": 1772197800000
}
],
"baseInfo": {
"useDate": 1772121600000,
"useTime": 60,
"avgOxygenation": "97.5",
"respiratoryRate": 18,
"maxRespiratoryRate": 24,
"minRespiratoryRate": 10
}
}
}

View File

@ -0,0 +1,29 @@
{
"code": 0,
"msg": "success",
"success": true,
"data": {
"timeList": [{
"useDate": 1769270400000,
"useTime": 110
}, {
"useDate": 1769184000000,
"useTime": 1000
}],
"baseInfoList": [{
"useDate": 1769270400000,
"useTime": 100,
"avgOxygenation": "10.0",
"respiratoryRate": 50,
"maxRespiratoryRate": 20,
"minRespiratoryRate": 1
}, {
"useDate": 1769184000000,
"useTime": 100,
"avgOxygenation": "10.0",
"respiratoryRate": 50,
"maxRespiratoryRate": 20,
"minRespiratoryRate": 1
}]
}
}

6
mock/service_failed.txt Normal file
View File

@ -0,0 +1,6 @@
{
"code": -1,
"msg": "failed",
"success": false,
"data": null
}

1
mock/test.txt Normal file
View File

@ -0,0 +1 @@
测试

1
mock/test2.txt Normal file
View File

@ -0,0 +1 @@
23432423423423

345949
mock/ventilator_day.txt Normal file

File diff suppressed because it is too large Load Diff

1157
mock/ventilator_days.txt Normal file

File diff suppressed because it is too large Load Diff