// 本地浏览器验收用 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 NOTIFICATIONS = [ ...[6, 5, 4, 3, 2, 1].map((seq) => notif(seq, '2026-09-13', 22, seq, seq <= 3)), ...[4, 3, 2, 1].map((seq) => notif(20 + seq, '2026-09-12', 20, seq, true)), ...[4, 3, 2, 1].map((seq) => notif(30 + seq, '2026-09-10', 18, seq, true)), ] function notif(id, day, hour, seq, read) { const minutes = String(seq * 7).padStart(2, '0') return { id, scene: id % 3 === 0 ? 'secret_balance' : 'task_failed', level: id % 3 === 0 ? 'error' : 'warning', title: id % 3 === 0 ? `用户密钥异常:测试用户${seq}` : `跟价任务失败`, content: id % 3 === 0 ? `用户 测试用户${seq}(uid=${1100 + seq})的代理设置对应服务商余额不足` : `用户 测试用户${seq} 有 ${seq} 个跟价任务失败`, read, readAt: read ? `${day} ${hour}:${minutes}:00` : null, createdAt: `${day} ${hour}:${minutes}:00`, } } /** 通知列表:关键字匹配标题/内容,日期按年月日区间(与 Java 侧同语义)。 */ function notificationPage(searchParams) { const keyword = (searchParams.get('keyword') || '').trim() const startDate = searchParams.get('startDate') || '' const endDate = searchParams.get('endDate') || '' const page = Math.max(1, Number(searchParams.get('page') || 1)) const pageSize = Math.min(100, Math.max(1, Number(searchParams.get('pageSize') || 20))) let rows = NOTIFICATIONS if (keyword) { rows = rows.filter((item) => `${item.title}${item.content}`.includes(keyword)) } if (startDate) { rows = rows.filter((item) => item.createdAt.slice(0, 10) >= startDate) } if (endDate) { rows = rows.filter((item) => item.createdAt.slice(0, 10) <= endDate) } const start = (page - 1) * pageSize return { success: true, data: { items: rows.slice(start, start + pageSize), total: rows.length, page, pageSize, unreadCount: rows.filter((item) => !item.read).length, }, } } 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 === '/api/admin/notifications/summary') { return json(res, { success: true, data: { unreadCount: NOTIFICATIONS.filter((item) => !item.read).length, latestId: NOTIFICATIONS.reduce((max, item) => Math.max(max, item.id), 0), }, }) } if (url === '/api/admin/notifications') { return json(res, notificationPage(new URL(req.url || '/', 'http://127.0.0.1').searchParams)) } 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}`))