master
parent
e4d7532daa
commit
9cfa5cb149
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"addresses": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "刘先生",
|
||||||
|
"phone": "134****7788",
|
||||||
|
"fullAddress": "山东省济南市某某区幸福小区5号10楼1003室",
|
||||||
|
"isDefault": true,
|
||||||
|
"locationStatus": "正常",
|
||||||
|
"canEdit": true,
|
||||||
|
"canDelete": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"products": [],
|
||||||
|
"orders": []
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"fs-extra": "^11.3.0",
|
"fs-extra": "^11.3.0",
|
||||||
|
"lowdb": "^3.0.0",
|
||||||
"mockjs": "^1.1.0",
|
"mockjs": "^1.1.0",
|
||||||
"swagger-jsdoc": "^6.2.8",
|
"swagger-jsdoc": "^6.2.8",
|
||||||
"swagger-ui-express": "^5.0.1"
|
"swagger-ui-express": "^5.0.1"
|
||||||
|
|
@ -695,6 +696,21 @@
|
||||||
"integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==",
|
"integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lowdb": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/lowdb/-/lowdb-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-9KZRulmIcU8fZuWiaM0d5e2/nPnrFyXkeXVpqT+MJS+vgbgOf1EbtvgQmba8HwUFgDl1oeZR6XqEJnkJmQdKmg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"steno": "^2.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/typicode"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
|
@ -1082,6 +1098,18 @@
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/steno": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/steno/-/steno-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-mauOsiaqTNGFkWqIfwcm3y/fq+qKKaIWf1vf3ocOuTdco9XoHCO2AGF1gFYXuZFSWuP38Q8LBHBGJv2KnJSXyA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/typicode"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/swagger-jsdoc": {
|
"node_modules/swagger-jsdoc": {
|
||||||
"version": "6.2.8",
|
"version": "6.2.8",
|
||||||
"resolved": "https://registry.npmmirror.com/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz",
|
"resolved": "https://registry.npmmirror.com/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz",
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"fs-extra": "^11.3.0",
|
"fs-extra": "^11.3.0",
|
||||||
|
"lowdb": "^3.0.0",
|
||||||
"mockjs": "^1.1.0",
|
"mockjs": "^1.1.0",
|
||||||
"swagger-jsdoc": "^6.2.8",
|
"swagger-jsdoc": "^6.2.8",
|
||||||
"swagger-ui-express": "^5.0.1"
|
"swagger-ui-express": "^5.0.1"
|
||||||
|
|
|
||||||
240
server.js
240
server.js
|
|
@ -3,21 +3,17 @@ const bodyParser = require('body-parser');
|
||||||
const Mock = require('mockjs');
|
const Mock = require('mockjs');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const dayjs = require('dayjs');
|
const dayjs = require('dayjs');
|
||||||
|
|
||||||
// ✅ 引入 swagger 组件
|
|
||||||
const swaggerUi = require('swagger-ui-express');
|
const swaggerUi = require('swagger-ui-express');
|
||||||
const swaggerJSDoc = require('swagger-jsdoc');
|
const swaggerJSDoc = require('swagger-jsdoc');
|
||||||
|
const { Low, JSONFile } = require('lowdb');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = 3000;
|
const PORT = 3000;
|
||||||
const CONFIG_PATH = './mock-config.json';
|
const CONFIG_PATH = './mock-config.json';
|
||||||
|
const DB_PATH = './mock-db.json';
|
||||||
|
|
||||||
app.use(bodyParser.json());
|
app.use(bodyParser.json());
|
||||||
|
|
||||||
// 加载配置文件
|
|
||||||
let mockConfigs = fs.existsSync(CONFIG_PATH) ? fs.readJsonSync(CONFIG_PATH) : [];
|
|
||||||
|
|
||||||
// ✅ 统一响应结构
|
|
||||||
function formatResponse(data, code = 200, message = 'success') {
|
function formatResponse(data, code = 200, message = 'success') {
|
||||||
return {
|
return {
|
||||||
code,
|
code,
|
||||||
|
|
@ -29,13 +25,54 @@ function formatResponse(data, code = 200, message = 'success') {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ 注册 mock 接口
|
|
||||||
function registerMockApi(config) {
|
function registerMockApi(config) {
|
||||||
const { method, path, response } = config;
|
const { method, path, response } = config;
|
||||||
const methodName = method.toLowerCase();
|
const methodName = method.toLowerCase();
|
||||||
|
|
||||||
app[methodName](path, (req, res) => {
|
app[methodName](path, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
// 地址模块逻辑
|
||||||
|
if (path === '/api/user/address/list') {
|
||||||
|
await global.db.read();
|
||||||
|
return res.json(formatResponse({ list: global.db.data.addresses }));
|
||||||
|
}
|
||||||
|
if (path === '/api/user/address/add') {
|
||||||
|
const newItem = { id: Date.now(), ...req.body };
|
||||||
|
await global.db.read();
|
||||||
|
global.db.data.addresses.push(newItem);
|
||||||
|
await global.db.write();
|
||||||
|
return res.json(formatResponse({ id: newItem.id }, 200, '地址新增成功'));
|
||||||
|
}
|
||||||
|
if (path === '/api/user/address/update') {
|
||||||
|
const { id, ...rest } = req.body;
|
||||||
|
await global.db.read();
|
||||||
|
const item = global.db.data.addresses.find(i => i.id == id);
|
||||||
|
if (item) Object.assign(item, rest);
|
||||||
|
await global.db.write();
|
||||||
|
return res.json(formatResponse(null, 200, '地址已更新'));
|
||||||
|
}
|
||||||
|
if (path === '/api/user/address/delete') {
|
||||||
|
const { id } = req.body;
|
||||||
|
await global.db.read();
|
||||||
|
global.db.data.addresses = global.db.data.addresses.filter(i => i.id != id);
|
||||||
|
await global.db.write();
|
||||||
|
return res.json(formatResponse(null, 200, '地址已删除'));
|
||||||
|
}
|
||||||
|
if (path === '/api/user/address/set-default') {
|
||||||
|
const { id } = req.body;
|
||||||
|
await global.db.read();
|
||||||
|
global.db.data.addresses.forEach(i => i.isDefault = i.id == id);
|
||||||
|
await global.db.write();
|
||||||
|
return res.json(formatResponse(null, 200, '已设置为默认地址'));
|
||||||
|
}
|
||||||
|
if (path === '/api/user/address/detail') {
|
||||||
|
const { id } = req.query;
|
||||||
|
await global.db.read();
|
||||||
|
const item = global.db.data.addresses.find(i => i.id == id);
|
||||||
|
return res.json(formatResponse(item || null));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他静态 mock 响应
|
||||||
const data = Mock.mock(response);
|
const data = Mock.mock(response);
|
||||||
res.json(formatResponse(data));
|
res.json(formatResponse(data));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -44,110 +81,97 @@ function registerMockApi(config) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始注册全部
|
async function startServer() {
|
||||||
mockConfigs.forEach(registerMockApi);
|
// 初始化 lowdb
|
||||||
|
const adapter = new JSONFile(DB_PATH);
|
||||||
|
const db = new Low(adapter);
|
||||||
|
await db.read();
|
||||||
|
db.data ||= { addresses: [], products: [], orders: [] };
|
||||||
|
await db.write();
|
||||||
|
global.db = db;
|
||||||
|
|
||||||
// ✅ 添加新接口
|
// 加载配置文件
|
||||||
app.post('/mock-api/add', async (req, res) => {
|
let mockConfigs = fs.existsSync(CONFIG_PATH) ? fs.readJsonSync(CONFIG_PATH) : [];
|
||||||
const config = req.body;
|
global.mockConfigs = mockConfigs;
|
||||||
if (!config.method || !config.path || !config.response) {
|
mockConfigs.forEach(registerMockApi);
|
||||||
return res.json(formatResponse(null, 400, '参数不完整'));
|
|
||||||
}
|
|
||||||
|
|
||||||
registerMockApi(config);
|
// 添加 mock 接口
|
||||||
mockConfigs.push(config);
|
app.post('/mock-api/add', async (req, res) => {
|
||||||
await fs.writeJson(CONFIG_PATH, mockConfigs, { spaces: 2 });
|
const config = req.body;
|
||||||
|
if (!config.method || !config.path || !config.response) {
|
||||||
res.json(formatResponse({ path: config.path }, 200, '接口已添加'));
|
return res.json(formatResponse(null, 400, '参数不完整'));
|
||||||
});
|
}
|
||||||
|
registerMockApi(config);
|
||||||
// ✅ 获取全部 mock 定义
|
global.mockConfigs.push(config);
|
||||||
app.get('/mock-api/list', (req, res) => {
|
await fs.writeJson(CONFIG_PATH, global.mockConfigs, { spaces: 2 });
|
||||||
res.json(formatResponse(mockConfigs));
|
res.json(formatResponse({ path: config.path }, 200, '接口已添加'));
|
||||||
});
|
|
||||||
|
|
||||||
// ✅ 获取所有 URL 路由(简版)
|
|
||||||
app.get('/mock-api/routes', (req, res) => {
|
|
||||||
const list = mockConfigs.map(({ method, path }) => ({ method, path }));
|
|
||||||
res.json(list);
|
|
||||||
});
|
|
||||||
|
|
||||||
// ✅ Swagger 配置
|
|
||||||
const swaggerDefinition = {
|
|
||||||
openapi: '3.0.0',
|
|
||||||
info: {
|
|
||||||
title: 'Mock API Docs',
|
|
||||||
version: '1.0.0',
|
|
||||||
description: 'Dynamically registered mock APIs'
|
|
||||||
},
|
|
||||||
servers: [] // 将由请求动态生成
|
|
||||||
};
|
|
||||||
|
|
||||||
function buildSwaggerSpecFromMocks(configs) {
|
|
||||||
const paths = {};
|
|
||||||
|
|
||||||
configs.forEach(({ method, path, swagger }) => {
|
|
||||||
const cleanPath = path.replace(/:([^/]+)/g, '{$1}');
|
|
||||||
const methodName = method.toLowerCase();
|
|
||||||
|
|
||||||
const summary = swagger?.summary || `Mocked ${method} ${path}`;
|
|
||||||
const description = swagger?.description || '';
|
|
||||||
const responses = swagger?.responses || {
|
|
||||||
200: {
|
|
||||||
description: 'Mock response',
|
|
||||||
content: {
|
|
||||||
'application/json': {
|
|
||||||
schema: { type: 'object' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
paths[cleanPath] = paths[cleanPath] || {};
|
|
||||||
paths[cleanPath][methodName] = {
|
|
||||||
summary,
|
|
||||||
description,
|
|
||||||
responses
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
app.get('/mock-api/list', (req, res) => {
|
||||||
...swaggerDefinition,
|
res.json(formatResponse(global.mockConfigs));
|
||||||
paths
|
});
|
||||||
|
|
||||||
|
app.get('/mock-api/routes', (req, res) => {
|
||||||
|
const list = global.mockConfigs.map(({ method, path }) => ({ method, path }));
|
||||||
|
res.json(list);
|
||||||
|
});
|
||||||
|
|
||||||
|
const swaggerDefinition = {
|
||||||
|
openapi: '3.0.0',
|
||||||
|
info: {
|
||||||
|
title: 'Mock API Docs',
|
||||||
|
version: '1.0.0',
|
||||||
|
description: 'Dynamically registered mock APIs'
|
||||||
|
},
|
||||||
|
servers: []
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function buildSwaggerSpecFromMocks(configs) {
|
||||||
|
const paths = {};
|
||||||
|
configs.forEach(({ method, path, swagger }) => {
|
||||||
|
const cleanPath = path.replace(/:([^/]+)/g, '{$1}');
|
||||||
|
const methodName = method.toLowerCase();
|
||||||
|
const summary = swagger?.summary || `Mocked ${method} ${path}`;
|
||||||
|
const description = swagger?.description || '';
|
||||||
|
const responses = swagger?.responses || {
|
||||||
|
200: {
|
||||||
|
description: 'Mock response',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: { type: 'object' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
paths[cleanPath] = paths[cleanPath] || {};
|
||||||
|
paths[cleanPath][methodName] = { summary, description, responses };
|
||||||
|
});
|
||||||
|
return { ...swaggerDefinition, paths };
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use('/swagger', (req, res, next) => {
|
||||||
|
res.setHeader('Cache-Control', 'no-store');
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use('/swagger', swaggerUi.serve, (req, res, next) => {
|
||||||
|
const protocol = req.headers['x-forwarded-proto'] || req.protocol;
|
||||||
|
const host = req.get('host');
|
||||||
|
const dynamicSwaggerDefinition = {
|
||||||
|
...swaggerDefinition,
|
||||||
|
servers: [
|
||||||
|
{ url: `${protocol}://${host}`, description: 'Current host' }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
const spec = buildSwaggerSpecFromMocks(global.mockConfigs);
|
||||||
|
const dynamicSpec = { ...spec, servers: dynamicSwaggerDefinition.servers };
|
||||||
|
swaggerUi.setup(dynamicSpec)(req, res, next);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`✅ Mock Server is running at http://localhost:${PORT}`);
|
||||||
|
console.log(`📚 Swagger UI available at http://localhost:${PORT}/swagger`);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Swagger UI 路由
|
startServer();
|
||||||
app.use('/swagger', (req, res, next) => {
|
|
||||||
res.setHeader('Cache-Control', 'no-store');
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
app.use('/swagger', swaggerUi.serve, (req, res, next) => {
|
|
||||||
const protocol = req.headers['x-forwarded-proto'] || req.protocol;
|
|
||||||
const host = req.get('host');
|
|
||||||
|
|
||||||
const dynamicSwaggerDefinition = {
|
|
||||||
...swaggerDefinition,
|
|
||||||
servers: [
|
|
||||||
{
|
|
||||||
url: `${protocol}://${host}`,
|
|
||||||
description: 'Current host'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
const spec = buildSwaggerSpecFromMocks(mockConfigs);
|
|
||||||
const dynamicSpec = {
|
|
||||||
...spec,
|
|
||||||
servers: dynamicSwaggerDefinition.servers
|
|
||||||
};
|
|
||||||
|
|
||||||
swaggerUi.setup(dynamicSpec)(req, res, next);
|
|
||||||
});
|
|
||||||
|
|
||||||
// ✅ 启动服务
|
|
||||||
app.listen(PORT, () => {
|
|
||||||
console.log(`✅ Mock Server is running at http://localhost:${PORT}`);
|
|
||||||
console.log(`📚 Swagger UI available at http://localhost:${PORT}/swagger`);
|
|
||||||
});
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue