增加 mock json

master
old易 2025-07-28 13:23:43 +08:00
parent 8240a52a98
commit 2554bf8441
4 changed files with 199 additions and 88 deletions

View File

@ -13,6 +13,8 @@ RUN npm install
# 拷贝项目文件
COPY . .
# 启动服务(默认端口为 3000
# 暴露端口
EXPOSE 3000
# 启动服务
CMD ["node", "server.js"]

15
docker-compose.yml Normal file
View File

@ -0,0 +1,15 @@
version: '3.8'
services:
mock-server:
build:
context: .
dockerfile: Dockerfile
container_name: mock-server
ports:
- "3002:3000"
volumes:
- .:/app
environment:
- NODE_ENV=production
restart: unless-stopped

View File

@ -33,17 +33,153 @@
{
"method": "GET",
"path": "/api/mall/recommend",
"query": { "type": "1" },
"response": {
"products|4": [
"products": [
{
"id|+1": 1001,
"name": "@pick(['老年精纺防尿湿纸尿裤 医用护理','防摔防滑地垫 卫生间适老化改造专用','多功能床边扶手 起夜支撑辅助器','老人专用加厚棉拖鞋'])",
"price": "@float(39, 9999, 2, 2)",
"image": "@image('200x200', '#f4f4f4', '商品图')",
"soldCount": "@natural(50, 999)"
"id": 1,
"name": "老年精纺防尿湿纸尿裤 医用护理",
"price": 120.0,
"soldCount": 73,
"image": "https://res.klyj.jinzejk.com/mall/1.png"
},
{
"id": 2,
"name": "防摔防滑地垫 卫生间适老化改造专用",
"price": 999.0,
"soldCount": 999,
"image": "https://res.klyj.jinzejk.com/mall/2.png"
},
{
"id": 3,
"name": "多功能床边扶手 起夜支撑辅助器",
"price": 888.9,
"soldCount": 73,
"image": "https://res.klyj.jinzejk.com/mall/3.png"
},
{
"id": 4,
"name": "老人专用加厚棉拖鞋 老人专用加厚棉拖...",
"price": 129.0,
"soldCount": 73,
"image": "https://res.klyj.jinzejk.com/mall/4.png"
}
]
}
},
{
"method": "GET",
"path": "/api/mall/recommend",
"query": { "type": "2" },
"response": {
"products": [
{
"id": 1,
"name": "防脱育发洗发水增密何发首乌生姜洗发液",
"price": 120.0,
"soldCount": 73,
"image": "https://res.klyj.jinzejk.com/mall/safe/1.png"
},
{
"id": 2,
"name": "护理老人卧床病人用洗头神器",
"price": 9999.0,
"soldCount": 999,
"image": "https://res.klyj.jinzejk.com/mall/safe/2.png"
},
{
"id": 3,
"name": "硫磺液体香皂洗脸洗澡洗头祛痘止痒",
"price": 888.9,
"soldCount": 73,
"image": "https://res.klyj.jinzejk.com/mall/safe/3.png"
},
{
"id": 4,
"name": "防脱控油蓬松腺苷育发赋活洗发水",
"price": 120.0,
"soldCount": 73,
"image": "https://res.klyj.jinzejk.com/mall/safe/4.png"
}
]
}
},
{
"method": "GET",
"path": "/api/mall/recommend",
"query": { "type": "3" },
"response": {
"products": [
{
"id": 1,
"name": "轻奢碳纤维轻便登山拐杖老人防滑拐",
"price": 120.00,
"soldCount": 73,
"image": "https://res.klyj.jinzejk.com/mall/travel/1.png"
},
{
"id": 2,
"name": "雅德助行器坐板累了可坐 可洗澡防水",
"price": 9999.00,
"soldCount": 999,
"image": "https://res.klyj.jinzejk.com/mall/travel/2.png"
},
{
"id": 3,
"name": "买菜小拉车快递车带盖轻便可折叠",
"price": 888.90,
"soldCount": 73,
"image": "https://res.klyj.jinzejk.com/mall/travel/3.png"
},
{
"id": 4,
"name": "老人拐杖凳子折叠便携多功能",
"price": 120.00,
"soldCount": 73,
"image": "https://res.klyj.jinzejk.com/mall/travel/4.png"
}
]
}
},
{
"method": "GET",
"path": "/api/mall/recommend",
"query": { "type": "4" },
"response": {
"products": [
{
"id": 1,
"name": "防脱育发洗发水增密何发首乌生姜洗发液",
"price": 120.00,
"soldCount": 73,
"image": "https://res.klyj.jinzejk.com/mall/care/1.png"
},
{
"id": 2,
"name": "护理老人卧床病人用洗头神器",
"price": 9999.00,
"soldCount": 999,
"image": "https://res.klyj.jinzejk.com/mall/care/2.png"
},
{
"id": 3,
"name": "硫磺液体香皂洗脸洗澡洗头祛痘止痒",
"price": 888.90,
"soldCount": 73,
"image": "https://res.klyj.jinzejk.com/mall/care/3.png"
},
{
"id": 4,
"name": "防脱控油蓬松腺苷育发赋活洗发水",
"price": 120.00,
"soldCount": 73,
"image": "https://res.klyj.jinzejk.com/mall/care/4.png"
}
]
}
},
{
"method": "GET",
@ -81,7 +217,7 @@
"method": "GET",
"path": "/api/product/detail",
"response": {
"id": "1",
"id": 1,
"title": "防摔防滑地垫 卫生间适老化改造专用",
"subtitle": "600g专用 600g专用 600g专用简介",
"price": 120.0,

104
server.js
View File

@ -5,12 +5,11 @@ const fs = require('fs-extra');
const dayjs = require('dayjs');
const swaggerUi = require('swagger-ui-express');
const swaggerJSDoc = require('swagger-jsdoc');
const { Low, JSONFile } = require('lowdb');
const app = express();
const PORT = 3000;
const CONFIG_PATH = './mock-config.json';
const DB_PATH = './mock-db.json';
app.use(bodyParser.json());
@ -26,55 +25,31 @@ function formatResponse(data, code = 200, message = 'success') {
}
function registerMockApi(config) {
const { method, path, response } = config;
const { method, path, response, query, swagger } = config;
const methodName = method.toLowerCase();
app[methodName](path, async (req, res) => {
app[methodName](path, async (req, res, next) => {
try {
// 地址模块逻辑
if (path === '/api/user/address/list') {
await global.db.read();
return res.json(formatResponse({ list: global.db.data.addresses }));
console.log(`[MOCK] Incoming: ${req.method} ${req.path}`, req.query);
// 默认 fallback
if (path === '/api/mall/recommend' && Object.keys(req.query).length === 0) {
const defaultConfig = global.mockConfigs.find(c =>
c.path === path && c.query?.type === 'home'
);
if (defaultConfig) {
console.log(`[MOCK] Fallback to default (home)`);
return res.json(formatResponse(defaultConfig.response));
}
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);
res.json(formatResponse(data));
// 精确 query 匹配
if (query) {
const isMatch = Object.entries(query).every(([key, val]) => req.query[key] === val);
if (!isMatch) return next(); // 继续往下找
}
return res.json(formatResponse(response));
} catch (err) {
res.json(formatResponse(null, 500, err.message));
}
@ -82,40 +57,18 @@ function registerMockApi(config) {
}
async function startServer() {
// 初始化 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;
// 加载配置文件
// 读取配置
let mockConfigs = fs.existsSync(CONFIG_PATH) ? fs.readJsonSync(CONFIG_PATH) : [];
global.mockConfigs = mockConfigs;
// 注册接口
mockConfigs.forEach(registerMockApi);
// 添加 mock 接口
app.post('/mock-api/add', async (req, res) => {
const config = req.body;
if (!config.method || !config.path || !config.response) {
return res.json(formatResponse(null, 400, '参数不完整'));
}
registerMockApi(config);
global.mockConfigs.push(config);
await fs.writeJson(CONFIG_PATH, global.mockConfigs, { spaces: 2 });
res.json(formatResponse({ path: config.path }, 200, '接口已添加'));
});
app.get('/mock-api/list', (req, res) => {
res.json(formatResponse(global.mockConfigs));
});
app.get('/mock-api/routes', (req, res) => {
const list = global.mockConfigs.map(({ method, path }) => ({ method, path }));
res.json(list);
});
// swagger 文档支持
const swaggerDefinition = {
openapi: '3.0.0',
info: {
@ -144,7 +97,12 @@ async function startServer() {
}
};
paths[cleanPath] = paths[cleanPath] || {};
paths[cleanPath][methodName] = { summary, description, responses };
paths[cleanPath][methodName] = {
summary,
description,
parameters: swagger?.parameters || [],
responses
};
});
return { ...swaggerDefinition, paths };
}