initial commit
This commit is contained in:
348
mock/product.ts
Normal file
348
mock/product.ts
Normal file
@@ -0,0 +1,348 @@
|
||||
import type { Request, Response } from 'express';
|
||||
|
||||
type ProductStatus =
|
||||
| 'draft'
|
||||
| 'pending_review'
|
||||
| 'online'
|
||||
| 'offline'
|
||||
| 'rejected';
|
||||
|
||||
type ProductItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
category: string;
|
||||
sku: string;
|
||||
originalPrice: number;
|
||||
salePrice: number;
|
||||
costPrice?: number;
|
||||
stock: number;
|
||||
safetyStock: number;
|
||||
status: ProductStatus;
|
||||
description?: string;
|
||||
imageUrl?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
type ProductBody = Partial<ProductItem>;
|
||||
|
||||
const PRODUCT_STATUS_LIST: ProductStatus[] = [
|
||||
'draft',
|
||||
'pending_review',
|
||||
'online',
|
||||
'offline',
|
||||
'rejected',
|
||||
];
|
||||
|
||||
const now = () => new Date().toISOString();
|
||||
|
||||
let productList: ProductItem[] = [
|
||||
{
|
||||
id: 'prod-1001',
|
||||
name: '智能降噪耳机 Pro',
|
||||
category: 'audio',
|
||||
sku: 'AUDIO-001',
|
||||
originalPrice: 1599,
|
||||
salePrice: 1299,
|
||||
costPrice: 760,
|
||||
stock: 45,
|
||||
safetyStock: 20,
|
||||
status: 'online',
|
||||
description: '旗舰主动降噪,支持空间音频。',
|
||||
imageUrl:
|
||||
'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?q=80&w=200&auto=format&fit=crop',
|
||||
createdAt: '2024-02-14T09:00:00.000Z',
|
||||
updatedAt: '2024-02-14T09:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'prod-1002',
|
||||
name: '机械键盘无线版',
|
||||
category: 'peripheral',
|
||||
sku: 'KEYB-002',
|
||||
originalPrice: 799,
|
||||
salePrice: 599,
|
||||
costPrice: 320,
|
||||
stock: 0,
|
||||
safetyStock: 10,
|
||||
status: 'offline',
|
||||
description: '三模连接,热插拔轴体。',
|
||||
imageUrl:
|
||||
'https://images.unsplash.com/photo-1511467687858-23d96c32e4ae?q=80&w=200&auto=format&fit=crop',
|
||||
createdAt: '2024-02-14T10:30:00.000Z',
|
||||
updatedAt: '2024-02-14T10:30:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'prod-1003',
|
||||
name: '4K 电竞显示器',
|
||||
category: 'digital',
|
||||
sku: 'DISP-003',
|
||||
originalPrice: 3999,
|
||||
salePrice: 3599,
|
||||
costPrice: 2410,
|
||||
stock: 8,
|
||||
safetyStock: 12,
|
||||
status: 'pending_review',
|
||||
description: '144Hz 刷新率,HDR 1000。',
|
||||
imageUrl:
|
||||
'https://images.unsplash.com/photo-1527443224154-c4a3942d3acf?q=80&w=200&auto=format&fit=crop',
|
||||
createdAt: '2024-02-14T11:15:00.000Z',
|
||||
updatedAt: '2024-02-14T11:15:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'prod-1004',
|
||||
name: '便携投影仪 Lite',
|
||||
category: 'digital',
|
||||
sku: 'PROJ-004',
|
||||
originalPrice: 2999,
|
||||
salePrice: 2599,
|
||||
stock: 22,
|
||||
safetyStock: 8,
|
||||
status: 'draft',
|
||||
description: '1080P 分辨率,自动梯形校正。',
|
||||
createdAt: '2024-02-15T08:20:00.000Z',
|
||||
updatedAt: '2024-02-15T08:20:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'prod-1005',
|
||||
name: '智能手环 X2',
|
||||
category: 'wearable',
|
||||
sku: 'WEAR-005',
|
||||
originalPrice: 399,
|
||||
salePrice: 299,
|
||||
costPrice: 120,
|
||||
stock: 15,
|
||||
safetyStock: 15,
|
||||
status: 'rejected',
|
||||
description: '支持心率、血氧与睡眠监测。',
|
||||
createdAt: '2024-02-15T10:00:00.000Z',
|
||||
updatedAt: '2024-02-15T10:00:00.000Z',
|
||||
},
|
||||
];
|
||||
|
||||
const coreFieldChanged = (
|
||||
product: ProductItem,
|
||||
patch: ProductBody,
|
||||
): boolean => {
|
||||
return (
|
||||
(typeof patch.name === 'string' && patch.name !== product.name) ||
|
||||
(typeof patch.sku === 'string' && patch.sku !== product.sku) ||
|
||||
(typeof patch.originalPrice === 'number' &&
|
||||
patch.originalPrice !== product.originalPrice) ||
|
||||
(typeof patch.salePrice === 'number' &&
|
||||
patch.salePrice !== product.salePrice)
|
||||
);
|
||||
};
|
||||
|
||||
const isProductStatus = (value: unknown): value is ProductStatus => {
|
||||
return (
|
||||
typeof value === 'string' &&
|
||||
PRODUCT_STATUS_LIST.includes(value as ProductStatus)
|
||||
);
|
||||
};
|
||||
|
||||
const isPositiveNumber = (value: unknown): value is number => {
|
||||
return typeof value === 'number' && value > 0;
|
||||
};
|
||||
|
||||
const hasCreatePriceError = (payload: ProductBody): boolean => {
|
||||
if (!isPositiveNumber(payload.originalPrice)) {
|
||||
return true;
|
||||
}
|
||||
if (!isPositiveNumber(payload.salePrice)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return payload.salePrice > payload.originalPrice;
|
||||
};
|
||||
|
||||
const hasPriceError = (payload: ProductBody): boolean => {
|
||||
if (typeof payload.originalPrice === 'number' && payload.originalPrice <= 0) {
|
||||
return true;
|
||||
}
|
||||
if (typeof payload.salePrice === 'number' && payload.salePrice <= 0) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
typeof payload.originalPrice === 'number' &&
|
||||
typeof payload.salePrice === 'number' &&
|
||||
payload.salePrice > payload.originalPrice
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const parseString = (value: unknown): string | undefined => {
|
||||
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
||||
};
|
||||
|
||||
export default {
|
||||
'GET /api/products': (req: Request, res: Response) => {
|
||||
const current = Number(req.query.current) || 1;
|
||||
const pageSize = Number(req.query.pageSize) || 10;
|
||||
const name = parseString(req.query.name);
|
||||
const category = parseString(req.query.category);
|
||||
const sku = parseString(req.query.sku);
|
||||
const status = parseString(req.query.status) as ProductStatus | undefined;
|
||||
const stockWarning = parseString(req.query.stockWarning);
|
||||
|
||||
let filtered = [...productList];
|
||||
|
||||
if (name) {
|
||||
filtered = filtered.filter((item) => item.name.includes(name));
|
||||
}
|
||||
if (category) {
|
||||
filtered = filtered.filter((item) => item.category === category);
|
||||
}
|
||||
if (sku) {
|
||||
filtered = filtered.filter((item) => item.sku.includes(sku));
|
||||
}
|
||||
if (status) {
|
||||
filtered = filtered.filter((item) => item.status === status);
|
||||
}
|
||||
if (stockWarning === 'warning') {
|
||||
filtered = filtered.filter(
|
||||
(item) => item.stock > 0 && item.stock < item.safetyStock,
|
||||
);
|
||||
}
|
||||
if (stockWarning === 'empty') {
|
||||
filtered = filtered.filter((item) => item.stock === 0);
|
||||
}
|
||||
|
||||
const start = (current - 1) * pageSize;
|
||||
const data = filtered.slice(start, start + pageSize);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data,
|
||||
total: filtered.length,
|
||||
});
|
||||
},
|
||||
|
||||
'GET /api/products/:id': (req: Request, res: Response) => {
|
||||
const { id } = req.params;
|
||||
const item = productList.find((product) => product.id === id);
|
||||
|
||||
if (!item) {
|
||||
res.status(404).json({ success: false, message: '商品不存在' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json(item);
|
||||
},
|
||||
|
||||
'POST /api/products': (req: Request, res: Response) => {
|
||||
const payload = req.body as ProductBody;
|
||||
|
||||
if (hasCreatePriceError(payload)) {
|
||||
res.status(400).json({ success: false, message: '价格校验失败' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.status !== undefined && !isProductStatus(payload.status)) {
|
||||
res.status(400).json({ success: false, message: '状态非法' });
|
||||
return;
|
||||
}
|
||||
|
||||
const created: ProductItem = {
|
||||
id: `prod-${Date.now()}`,
|
||||
name: String(payload.name || ''),
|
||||
category: String(payload.category || 'digital'),
|
||||
sku: String(payload.sku || ''),
|
||||
originalPrice: Number(payload.originalPrice || 0),
|
||||
salePrice: Number(payload.salePrice || 0),
|
||||
costPrice:
|
||||
typeof payload.costPrice === 'number'
|
||||
? Number(payload.costPrice)
|
||||
: undefined,
|
||||
stock: Number(payload.stock || 0),
|
||||
safetyStock: Number(payload.safetyStock || 0),
|
||||
status: payload.status ?? 'draft',
|
||||
description: payload.description,
|
||||
imageUrl: payload.imageUrl,
|
||||
createdAt: now(),
|
||||
updatedAt: now(),
|
||||
};
|
||||
|
||||
productList = [created, ...productList];
|
||||
res.json(created);
|
||||
},
|
||||
|
||||
'PUT /api/products/status': (req: Request, res: Response) => {
|
||||
const payload = req.body as { ids?: string[]; status?: ProductStatus };
|
||||
const ids = Array.isArray(payload.ids) ? payload.ids : [];
|
||||
const status = payload.status;
|
||||
|
||||
if (!status || !['online', 'offline'].includes(status)) {
|
||||
res.status(400).json({ success: false, message: '状态非法' });
|
||||
return;
|
||||
}
|
||||
|
||||
productList = productList.map((item) =>
|
||||
ids.includes(item.id) ? { ...item, status, updatedAt: now() } : item,
|
||||
);
|
||||
|
||||
res.json({ success: true });
|
||||
},
|
||||
|
||||
'PUT /api/products/:id': (req: Request, res: Response) => {
|
||||
const { id } = req.params;
|
||||
const payload = req.body as ProductBody;
|
||||
const index = productList.findIndex((product) => product.id === id);
|
||||
|
||||
if (index === -1) {
|
||||
res.status(404).json({ success: false, message: '商品不存在' });
|
||||
return;
|
||||
}
|
||||
|
||||
const current = productList[index];
|
||||
|
||||
if (payload.status !== undefined && !isProductStatus(payload.status)) {
|
||||
res.status(400).json({ success: false, message: '状态非法' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (current.status === 'online' && coreFieldChanged(current, payload)) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: '已上架商品修改核心字段前需先下架',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const merged = {
|
||||
...current,
|
||||
...payload,
|
||||
updatedAt: now(),
|
||||
} as ProductItem;
|
||||
|
||||
if (hasPriceError(merged)) {
|
||||
res.status(400).json({ success: false, message: '价格校验失败' });
|
||||
return;
|
||||
}
|
||||
|
||||
productList[index] = merged;
|
||||
res.json(productList[index]);
|
||||
},
|
||||
|
||||
'DELETE /api/products/:id': (req: Request, res: Response) => {
|
||||
const { id } = req.params;
|
||||
const target = productList.find((item) => item.id === id);
|
||||
|
||||
if (!target) {
|
||||
res.status(404).json({ success: false, message: '商品不存在' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!['draft', 'offline'].includes(target.status)) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: '仅草稿和已下架商品可删除',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
productList = productList.filter((item) => item.id !== id);
|
||||
res.json({ success: true });
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user