Files
crawler-plugin/admin-frontend-vue/scripts/mock-admin-server.mjs
T
huangzd1997 ff1ffbbfa3 feat(站内通知): 铃铛面板支持时间/内容搜索、按天分组与分页
- 列表接口加 keyword(标题/内容模糊)与 startDate/endDate(年月日闭区间)参数,
  两端控制器透传,服务端补筛选日志
- 两端铃铛面板:搜索框(防抖 300ms)+ 日期区间 + 按年月日分组 + 翻页,
  面板改 Teleport 到 body(挂在顶栏时会被页面 el-select 压住,提 z-index 无效)
- 固化可见范围回归测试:超管全量/管理员只看本组/普通用户只看自己,
  含读写两侧的 user_id+audience 裁剪断言与两端控制器身份来源断言
2026-09-13 23:59:06 +08:00

253 lines
9.3 KiB
JavaScript
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.
// 本地浏览器验收用 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}`))