Files
agent/mock/product.ts
2026-02-16 12:46:37 +08:00

349 lines
9.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 });
},
};