Files
crawler-plugin/admin-frontend-vue/scripts/mock-admin-server.mjs
T
huangzd1997 52b55df7b2 feat(任务判死): 心跳正常但 180 分钟无结果上报的二次判死线(13 模块)+ 同期待发改动
判死线(治 28131 型「主线程卡死、心跳线程照发」):
- 判据改看 biz_task_scope_state.last_chunk_at(HTTP 心跳不刷新它);从未上报跳过不判
- 中央线覆盖 DELETE_BRAND/PRODUCT_RISK_RESOLVE/PRICE_TRACK/SHOP_MATCH/PATROL_DELETE/QUERY_ASIN/WITHDRAW
- 自带线接入 COLLECT_DATA/SIMILAR_ASIN/APPEARANCE_PATENT/SHOP_DATA_CRAWL/PUBLISH/BRAND
- 客户端心跳带处理位置 progressText,判死文案含最后位置;no-result-upload-timeout-minutes 默认 180(0 关闭)

同期带上另一工作流的待发改动:跟价换 IP 重试、品牌检测重试上限与 LLM 并发下调、
教程包后台管理页与 V126 迁移、admin-vue 教程记录页。
2026-09-14 17:56:20 +08:00

267 lines
11 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 TUTORIAL_PACKAGES = [
{ id: 3, file_name: '数富AI-教学客户端-v3.zip', version: 'v3.2.0', object_key: 'tutorial/20260912090000-数富AI-教学客户端-v3.zip', file_size: 1048576, file_url: 'https://oss.aishufu.top/client/tutorial/t3.zip', created_at: '2026-09-12 09:00' },
{ id: 1, file_name: '数富AI-教学客户端.zip', version: '', object_key: 'tutorial/数富AI-教学客户端.zip', file_size: 0, file_url: 'https://oss.aishufu.top/client/tutorial/legacy.zip', created_at: '2026-09-01 08:00' },
{ id: 4, file_name: '数富AI-教学客户端-v4.zip', version: 'v3.10.0', object_key: 'tutorial/20260914103000-数富AI-教学客户端-v4.zip', file_size: 2097152, file_url: 'https://oss.aishufu.top/client/tutorial/t4.zip', created_at: '2026-09-14 10:30' },
{ id: 2, file_name: '数富AI-教学客户端-v2.zip', version: 'v3.1.0', object_key: 'tutorial/20260910120000-数富AI-教学客户端-v2.zip', file_size: 524288, file_url: 'https://oss.aishufu.top/client/tutorial/t2.zip', created_at: '2026-09-10 12:00' },
]
/* ===================== 站内通知夹具(铃铛面板:搜索/按天分组/分页验收用) ===================== */
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 === '/api/admin/tutorials') {
// 与 Java 侧同契约:created_at 降序返回(页面"当前生效"取第一条)。
const items = [...TUTORIAL_PACKAGES].sort((a, b) => (a.created_at < b.created_at ? 1 : -1))
return json(res, { success: true, data: { items } })
}
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}`))