Files
agent/mock/product.ts

349 lines
9.3 KiB
TypeScript
Raw Permalink Normal View History

2026-02-16 12:46:37 +08:00
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 });
},
};