import Mock from 'mockjs'; /** 文件职责:费用设置页面 Mock 接口。 */ interface MockRequestOptions { body: null | string; type: string; url: string; } type PackagingFeeMode = 'item' | 'order'; type OrderPackagingFeeMode = 'fixed' | 'tiered'; interface PackagingFeeTierMock { fee: number; id: string; maxAmount: null | number; minAmount: number; sort: number; } interface AdditionalFeeItemMock { amount: number; enabled: boolean; } interface StoreFeesState { baseDeliveryFee: number; fixedPackagingFee: number; freeDeliveryThreshold: null | number; minimumOrderAmount: number; orderPackagingFeeMode: OrderPackagingFeeMode; otherFees: { cutlery: AdditionalFeeItemMock; rush: AdditionalFeeItemMock; }; packagingFeeMode: PackagingFeeMode; packagingFeeTiers: PackagingFeeTierMock[]; } const storeFeesMap = new Map(); /** 解析 URL 查询参数。 */ function parseUrlParams(url: string) { const parsed = new URL(url, 'http://localhost'); const params: Record = {}; parsed.searchParams.forEach((value, key) => { params[key] = value; }); return params; } /** 解析请求体 JSON。 */ function parseBody(options: MockRequestOptions) { if (!options.body) return {}; try { return JSON.parse(options.body); } catch (error) { console.error('[mock-store-fees] parseBody error:', error); return {}; } } /** 保留两位小数并裁剪为非负数。 */ function normalizeMoney(value: unknown, fallback = 0) { const parsed = Number(value); if (!Number.isFinite(parsed)) return fallback; return Math.round(Math.max(0, parsed) * 100) / 100; } /** 归一化包装费模式。 */ function normalizePackagingFeeMode(value: unknown, fallback: PackagingFeeMode) { return value === 'item' || value === 'order' ? value : fallback; } /** 归一化按订单包装费模式。 */ function normalizeOrderPackagingFeeMode( value: unknown, fallback: OrderPackagingFeeMode, ) { return value === 'fixed' || value === 'tiered' ? value : fallback; } /** 深拷贝阶梯列表。 */ function cloneTiers(source: PackagingFeeTierMock[]) { return source.map((item) => ({ ...item })); } /** 深拷贝状态对象。 */ function cloneStoreState(source: StoreFeesState): StoreFeesState { return { minimumOrderAmount: source.minimumOrderAmount, baseDeliveryFee: source.baseDeliveryFee, freeDeliveryThreshold: source.freeDeliveryThreshold, packagingFeeMode: source.packagingFeeMode, orderPackagingFeeMode: source.orderPackagingFeeMode, fixedPackagingFee: source.fixedPackagingFee, packagingFeeTiers: cloneTiers(source.packagingFeeTiers), otherFees: { cutlery: { ...source.otherFees.cutlery }, rush: { ...source.otherFees.rush }, }, }; } /** 排序并归一化阶梯列表。 */ function normalizeTiers( source: unknown, fallback: PackagingFeeTierMock[], ): PackagingFeeTierMock[] { if (!Array.isArray(source) || source.length === 0) { return cloneTiers(fallback); } const raw = source .map((item, index) => { const record = item as Partial; const minAmount = normalizeMoney(record.minAmount, 0); let maxAmount: null | number = null; if ( record.maxAmount !== null && record.maxAmount !== undefined && String(record.maxAmount) !== '' ) { maxAmount = normalizeMoney(record.maxAmount, minAmount); } return { id: typeof record.id === 'string' && record.id.trim() ? record.id : `fee-tier-${Date.now()}-${index}`, minAmount, maxAmount, fee: normalizeMoney(record.fee, 0), sort: Math.max(1, Number(record.sort) || index + 1), }; }) .toSorted((a, b) => { if (a.minAmount !== b.minAmount) return a.minAmount - b.minAmount; if (a.maxAmount === null) return 1; if (b.maxAmount === null) return -1; return a.maxAmount - b.maxAmount; }) .slice(0, 10); let hasUnbounded = false; return raw.map((item, index) => { let maxAmount = item.maxAmount; if (hasUnbounded) { maxAmount = item.minAmount + 0.01; } if (maxAmount !== null && maxAmount <= item.minAmount) { maxAmount = item.minAmount + 0.01; } if (maxAmount === null) hasUnbounded = true; return { ...item, maxAmount: index === raw.length - 1 ? maxAmount : (maxAmount ?? item.minAmount + 1), sort: index + 1, }; }); } /** 归一化其他费用。 */ function normalizeOtherFees( source: unknown, fallback: StoreFeesState['otherFees'], ) { const record = (source || {}) as Partial; return { cutlery: { enabled: Boolean(record.cutlery?.enabled), amount: normalizeMoney(record.cutlery?.amount, fallback.cutlery.amount), }, rush: { enabled: Boolean(record.rush?.enabled), amount: normalizeMoney(record.rush?.amount, fallback.rush.amount), }, }; } /** 归一化提交数据。 */ function normalizeStoreState(source: unknown, fallback: StoreFeesState) { const record = (source || {}) as Partial; const packagingFeeMode = normalizePackagingFeeMode( record.packagingFeeMode, fallback.packagingFeeMode, ); const orderPackagingFeeMode = packagingFeeMode === 'order' ? normalizeOrderPackagingFeeMode( record.orderPackagingFeeMode, fallback.orderPackagingFeeMode, ) : 'fixed'; return { minimumOrderAmount: normalizeMoney( record.minimumOrderAmount, fallback.minimumOrderAmount, ), baseDeliveryFee: normalizeMoney( record.baseDeliveryFee, fallback.baseDeliveryFee, ), freeDeliveryThreshold: record.freeDeliveryThreshold === null || record.freeDeliveryThreshold === undefined || String(record.freeDeliveryThreshold) === '' ? null : normalizeMoney( record.freeDeliveryThreshold, fallback.freeDeliveryThreshold ?? 0, ), packagingFeeMode, orderPackagingFeeMode, fixedPackagingFee: normalizeMoney( record.fixedPackagingFee, fallback.fixedPackagingFee, ), packagingFeeTiers: normalizeTiers( record.packagingFeeTiers, fallback.packagingFeeTiers, ), otherFees: normalizeOtherFees(record.otherFees, fallback.otherFees), } satisfies StoreFeesState; } /** 创建默认状态。 */ function createDefaultState(): StoreFeesState { return { minimumOrderAmount: 15, baseDeliveryFee: 3, freeDeliveryThreshold: 30, packagingFeeMode: 'order', orderPackagingFeeMode: 'tiered', fixedPackagingFee: 2, packagingFeeTiers: [ { id: `fee-tier-${Date.now()}-1`, minAmount: 0, maxAmount: 30, fee: 2, sort: 1, }, { id: `fee-tier-${Date.now()}-2`, minAmount: 30, maxAmount: 60, fee: 3, sort: 2, }, { id: `fee-tier-${Date.now()}-3`, minAmount: 60, maxAmount: null, fee: 5, sort: 3, }, ], otherFees: { cutlery: { enabled: false, amount: 1, }, rush: { enabled: false, amount: 3, }, }, }; } /** 确保门店状态存在。 */ function ensureStoreState(storeId = '') { const key = storeId || 'default'; let state = storeFeesMap.get(key); if (!state) { state = createDefaultState(); storeFeesMap.set(key, state); } return state; } Mock.mock(/\/store\/fees(?:\?|$)/, 'get', (options: MockRequestOptions) => { const params = parseUrlParams(options.url); const storeId = String(params.storeId || ''); const state = ensureStoreState(storeId); return { code: 200, data: { storeId, ...cloneStoreState(state), }, }; }); Mock.mock(/\/store\/fees\/save/, 'post', (options: MockRequestOptions) => { const body = parseBody(options); const storeId = String((body as { storeId?: unknown }).storeId || ''); const fallback = ensureStoreState(storeId); const next = normalizeStoreState(body, fallback); storeFeesMap.set(storeId || 'default', next); return { code: 200, data: { storeId, ...cloneStoreState(next), }, }; }); Mock.mock(/\/store\/fees\/copy/, 'post', (options: MockRequestOptions) => { const body = parseBody(options) as { sourceStoreId?: string; targetStoreIds?: string[]; }; const sourceStoreId = String(body.sourceStoreId || ''); const targetStoreIds = Array.isArray(body.targetStoreIds) ? body.targetStoreIds.map(String).filter(Boolean) : []; if (!sourceStoreId || targetStoreIds.length === 0) { return { code: 400, message: '参数错误', }; } const source = ensureStoreState(sourceStoreId); targetStoreIds.forEach((storeId) => { storeFeesMap.set(storeId, cloneStoreState(source)); }); return { code: 200, data: { copiedCount: targetStoreIds.length, }, }; });