// 本地浏览器验收用 mock(仅测试,不随前端产物发布):模拟 Admin 壳层所需的最小 API + 撞款控制台夹具。 import { createServer } from 'node:http' const PORT = Number(process.env.MOCK_PORT || 18100) const MENU_TREE = [ { key: 'account', name: '账号权限', children: [ { key: 'admin_users', name: '用户管理', route: '/account/users' }, { key: 'admin_columns', name: '菜单管理', route: '/account/menus' }, { key: 'admin_group_manage', name: '数据权限分组', route: '/account/groups' }, ], }, ] /* ===================== 撞款控制台夹具(对齐前端 parse 的 snake 契约) ===================== */ const STORE_GROUP = { 店A1: '陈新辉', 店A2: '陈新辉', 店B1: '魏振峰', 店B2: '魏振峰', 店B3: '魏振峰', 店C1: '郭亚芳' } const GROUP_DEFS = [ { name: '陈新辉', shops: ['店A1', '店A2'] }, { name: '魏振峰', shops: ['店B1', '店B2', '店B3'] }, { name: '郭亚芳', shops: ['店C1'] }, ] function occ(asin, brand, shop, country, day) { return { asin, date: `2026-08-${String(day).padStart(2, '0')} 09:00:00`, price: '9.90', brand, shop_name: shop, group_name: STORE_GROUP[shop], country_codes: [country], country } } // 撞款种子(≥2 店在售)与单店种子 const DUP_SEEDS = [ { asin: 'E0000001', brand: '品牌甲', occ: [occ('E0000001', '品牌甲', '店A1', 'UK', 20), occ('E0000001', '品牌甲', '店A1', 'UK', 24), occ('E0000001', '品牌甲', '店A2', 'DE', 22), occ('E0000001', '品牌甲', '店B1', 'FR', 21)] }, { asin: 'E0000002', brand: '品牌乙', occ: [occ('E0000002', '品牌乙', '店B1', 'UK', 10), occ('E0000002', '品牌乙', '店B2', 'DE', 12)] }, { asin: 'E0000003', brand: '品牌丙', occ: [occ('E0000003', '品牌丙', '店A2', 'DE', 15), occ('E0000003', '品牌丙', '店B3', 'IT', 16)] }, ] const SINGLE_SEEDS = [ { asin: 'E0000004', brand: '品牌甲', occ: [occ('E0000004', '品牌甲', '店A1', 'UK', 1)] }, { asin: 'E0000005', brand: '品牌丁', occ: [occ('E0000005', '品牌丁', '店B2', 'UK', 2), occ('E0000005', '品牌丁', '店B2', 'UK', 3)] }, { asin: 'E0000006', brand: '品牌戊', occ: [occ('E0000006', '品牌戊', '店C1', 'FR', 5)] }, { asin: 'E0000007', brand: '品牌己', occ: [occ('E0000007', '品牌己', '店B3', 'DE', 6)] }, { asin: 'E0000008', brand: '品牌庚', occ: [occ('E0000008', '品牌庚', '店A1', 'ES', 7)] }, ] function finalizeRow(seed) { const stores = [] const storeSet = new Set() const countries = new Set() let earliest = '' let latest = '' for (const o of seed.occ) { if (!storeSet.has(o.shop_name)) { storeSet.add(o.shop_name) stores.push(o.shop_name) } countries.add(o.country) if (!earliest || o.date < earliest) earliest = o.date if (o.date > latest) latest = o.date } const groups = [...new Set(stores.map((s) => STORE_GROUP[s] || ''))].filter(Boolean) return { asin: seed.asin, brand: seed.brand, store_count: stores.length, stores, groups, countries: [...countries], record_count: seed.occ.length, earliest, latest, occurrences: seed.occ, } } const ALL_ROWS = [...DUP_SEEDS, ...SINGLE_SEEDS].map(finalizeRow) const DUP_ROWS = ALL_ROWS.filter((row) => row.store_count >= 2) const SHOPS = Object.keys(STORE_GROUP).map((name) => { const codes = new Set() let recordCount = 0 let asinCount = 0 for (const row of ALL_ROWS) { if (row.stores.includes(name)) { asinCount += 1 for (const o of row.occurrences) { if (o.shop_name === name) { recordCount += 1 codes.add(o.country) } } } } return { shop_name: name, group_name: STORE_GROUP[name], country_codes: [...codes], asin_count: asinCount, record_count: recordCount } }) function computeGroups() { return GROUP_DEFS.map((group) => { const members = group.shops const hit = DUP_ROWS.filter((row) => members.filter((shop) => row.stores.includes(shop)).length >= 2) const asinUnique = ALL_ROWS.filter((row) => row.stores.some((shop) => members.includes(shop))).length const recordCount = ALL_ROWS.reduce((sum, row) => { if (row.stores.some((shop) => members.includes(shop))) sum += row.recordCount return sum }, 0) return { name: group.name, shop_count: members.length, asin_unique: asinUnique, record_count: recordCount, dup_count: hit.length } }) } const GROUPS = computeGroups() const RECORD_TOTAL = ALL_ROWS.reduce((sum, row) => sum + row.recordCount, 0) function consolePayload() { return { success: true, data: { pending: false, scanned_at: '2026-09-05 08:30:00', summary: { shop_count: SHOPS.length, asin_total: ALL_ROWS.length, record_total: RECORD_TOTAL, duplicate_asin_total: DUP_ROWS.length, duplicate_shop_count: DUP_ROWS.length ? 6 : 0, site_count: 5, asin_per_shop: 4.8, source: 'job', }, shops: SHOPS, dup: DUP_ROWS, groups: GROUPS, total_dup: DUP_ROWS.length, }, } } function json(res, payload, status = 200) { const body = JSON.stringify(payload) res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store', 'Content-Length': Buffer.byteLength(body), }) res.end(body) } const server = createServer((req, res) => { const url = (req.url || '').split('?')[0] const method = req.method || 'GET' console.log(`[mock] ${method} ${url}`) if (url === '/api/admin/current-user') { return json(res, { success: true, data: { item: { id: 1, username: 'admin', role: 'super_admin' } } }) } if (url === '/api/admin/current-user/menus') { return json(res, { success: true, data: { items: MENU_TREE } }) } if (url === '/api/admin/logout') { return json(res, { success: true }) } if (url === '/api/admin/users') { return json(res, { success: true, data: { items: [{ id: 1, username: 'admin', role: 'super_admin', creatorUsername: 'system', createdAt: '2026-01-01 00:00:00' }], total: 1, }, }) } if (url === '/api/admin/permission-menus') { return json(res, { success: true, data: { items: [] } }) } if (url === '/api/admin/shop-manage-groups') { return json(res, { success: true, data: { items: [] } }) } if (url === '/api/admin/shop-data-crawl/duplicate-check-console') { return json(res, consolePayload()) } if (url === '/api/admin/shop-data-crawl/duplicate-check-ledger') { return json(res, { success: true, data: { pending: false, scanned_at: '2026-09-05 08:30:00', items: ALL_ROWS, total: ALL_ROWS.length, page: 1, page_size: 20 }, }) } if (url.startsWith('/api/')) { return json(res, { success: true, data: { items: [], total: 0 } }) } return json(res, { success: true }, 200) }) server.listen(PORT, () => console.log(`mock admin api listening on ${PORT}`))