fix: 轮询按运营商账户限流
All checks were successful
构建并部署前端到测试环境 / deploy (push) Successful in 55s

This commit is contained in:
luo
2026-10-26 14:31:15 +08:00
parent 049f8257fb
commit 06684e1fc0
88 changed files with 7919 additions and 486 deletions

View File

@@ -0,0 +1,277 @@
import { mockApiPlugin as gatewayFixtures } from './gatewayBrowserFixtures.mjs'
const time = '2026-10-26T10:00:00+08:00'
const configs = [1, 2, 3].map((id) => ({
id,
config_name: `模拟配置 ${id}`,
priority: id,
description: '本地模拟,不代表真实配置',
status: id === 3 ? 0 : 1,
carrier_id: 8,
carrier_name: '模拟运营商账户',
card_category: 'normal',
card_condition: 'activated',
asset_status: 2,
package_state: 'active',
realname_check_interval: null,
carddata_check_interval: 300,
package_check_interval: 600,
card_status_check_interval: 0,
pre_reset_window: 3600,
pre_reset_carddata_interval: 60,
created_at: time,
updated_at: time
}))
const tasks = new Map()
const page = (items, url) => {
const current = Number(url.searchParams.get('page') || 1)
const size = Number(url.searchParams.get('page_size') || 20)
return {
items: items.slice((current - 1) * size, current * size),
page: current,
size,
page_size: size,
total: items.length,
total_pages: Math.ceil(items.length / size)
}
}
// 本地测试专用:所有 /api 都被当前或 gateway fixture 消费,无真实后端代理。
export const mockApiPlugin = {
name: 'polling-config-local-mock-api',
configureServer(server) {
server.middlewares.use(async (req, res, next) => {
const url = new URL(req.url, 'http://127.0.0.1')
const path = url.pathname
const auth = ['/api/auth/me', '/api/auth/login', '/api/auth/logout'].includes(path)
if (
!auth &&
!path.startsWith('/api/admin/polling-configs') &&
!['/api/admin/polling-stats/pre-reset', '/api/admin/polling-stats/tiers'].includes(path)
)
return next()
let body = ''
for await (const chunk of req) body += chunk
const input = body ? JSON.parse(body) : {}
const viewer = req.headers.authorization?.includes('local-polling-viewer')
const denied = req.headers.authorization?.includes('local-polling-denied')
const admin = !viewer && !denied
let data = {}
let code = 0
let msg = 'local mock only'
if (auth) {
const menus = [
{ url: '/polling-management/config', name: '轮询配置' },
{
url: '/polling-management/config-stats',
name: '轮询配置统计',
perm_code: 'polling_config_stats:page'
}
]
data = {
access_token: viewer
? 'local-polling-viewer'
: denied
? 'local-polling-denied'
: 'local-polling-admin',
refresh_token: '',
expires_in: 3600,
user: {
id: admin ? 1 : 2,
user_type: admin ? 1 : 2,
username: admin ? '本地模拟管理员' : '本地普通账号',
roles: [admin ? 'R_SUPER' : 'R_ADMIN']
},
permissions: denied ? [] : ['polling_config_stats:page'],
buttons: denied
? []
: [
'polling_config:match_preview',
'polling_config:recalculate_progress',
'polling_config_stats:pre_reset',
'polling_config_stats:tiers'
],
menus: [{ url: '/polling-management', name: '轮询管理', children: menus }]
}
} else if (path === '/api/admin/polling-configs/match-preview') {
const iccid = url.searchParams.get('iccid')
if (!iccid || (req.method === 'POST' && input.iccid !== iccid)) {
code = 1001
msg = 'Query 和 Body 的 ICCID 必须一致'
} else
data = {
card: {
iccid,
card_id: 42,
carrier_id: 8,
enable_polling: true,
asset_status: 2,
card_category: 'normal',
card_condition: 'activated',
package_state: 'active',
package_state_name: '有生效套餐'
},
matched:
req.method === 'POST' && input.overrides?.length
? input.overrides
.filter((item) => !item.disabled)
.map((item) => ({
id: item.id,
config_name: item.config_name,
priority: item.priority,
draft: true
}))
: [{ id: 1, config_name: configs[0].config_name, priority: 1, draft: false }],
intervals: [
{
config_id: 1,
config_name: configs[0].config_name,
inherited: false,
interval: 300,
mode: 'interval',
task_type: 'polling:carddata',
task_type_name: '流量'
},
{
config_id: 1,
config_name: configs[0].config_name,
inherited: true,
interval: 0,
mode: 'disabled',
task_type: 'polling:card_status',
task_type_name: '卡状态'
},
{
config_id: 0,
config_name: '',
inherited: false,
interval: null,
mode: 'unmatched',
task_type: 'polling:realname',
task_type_name: '实名'
}
]
}
} else if (path === '/api/admin/polling-configs/priorities') {
if (!admin) {
code = 403
msg = '仅管理员可操作'
} else {
input.ordered_ids.forEach((id, index) => {
const row = configs.find((item) => item.id === id)
if (row) row.priority = index + 1
})
data = null
}
} else if (path === '/api/admin/polling-configs/recalculate-due' && req.method === 'POST') {
if (!admin) {
code = 403
msg = '仅管理员可操作'
} else {
const id = `local-task-${tasks.size + 1}`
data = {
task_id: id,
status: 'pending',
carrier_id: input.carrier_id == null ? '' : String(input.carrier_id),
total: 100,
processed: 0,
advanced: 0,
error: '',
created_at: time,
started_at: '',
finished_at: ''
}
tasks.set(id, { data, polls: 0 })
}
} else if (path.startsWith('/api/admin/polling-configs/recalculate-due/')) {
const id = decodeURIComponent(
path.slice('/api/admin/polling-configs/recalculate-due/'.length)
)
if (!tasks.has(id))
tasks.set(id, {
data: {
task_id: id,
carrier_id: '8',
total: 100,
processed: 0,
advanced: 0,
error: '',
created_at: time,
started_at: '',
finished_at: ''
},
polls: 0
})
const entry = tasks.get(id)
entry.polls++
data = {
...entry.data,
status: entry.polls > 2 ? 'completed' : 'running',
processed: Math.min(100, entry.polls * 40),
advanced: entry.polls * 10,
started_at: time,
finished_at: entry.polls > 2 ? time : ''
}
} else if (path === '/api/admin/polling-stats/pre-reset') {
data = {
carriers: [
{
carrier_id: 8,
carrier_name: '模拟运营商账户',
previous: {
active_package_cards: 100,
checked_in_window: 80,
not_checked_in_window: 20,
reset_at: time,
last_offset_buckets: { le_5: 0, le_15: 20, le_30: 15, le_60: 25, gt_60: 10 }
},
upcoming: null
}
]
}
} else if (path === '/api/admin/polling-stats/tiers') {
data = {
by_config: [
{ config_id: 1, config_name: configs[0].config_name, cards: 95 },
{ config_id: 0, config_name: '', cards: 5 }
],
by_package_state: { active: 80, depleted: 10, pending: 5, none: 3, unknown: 2 },
computed_at: time,
total_cards: 100,
unmatched_in_stock: 2,
unmatched_sold_no_package: 3
}
} else if (path === '/api/admin/polling-configs' && req.method === 'GET') {
const query = url.searchParams.get('config_name') || ''
data = page(
configs.filter((item) => item.config_name.includes(query)),
url
)
} else if (path === '/api/admin/polling-configs' && req.method === 'POST') {
data = {
...input,
id: configs.length + 1,
status: 1,
created_at: time,
updated_at: time,
warnings: ['本地模拟:保存提示']
}
configs.push(data)
} else if (/^\/api\/admin\/polling-configs\/\d+$/.test(path) && req.method === 'PUT') {
const row = configs.find((item) => item.id === Number(path.split('/').at(-1)))
Object.assign(row, input)
for (const key of input.clear_fields || []) row[key] = null
delete row.clear_fields
data = { ...row, warnings: ['本地模拟:保存提示'] }
} else if (/^\/api\/admin\/polling-configs\/\d+$/.test(path) && req.method === 'GET')
data = configs.find((item) => item.id === Number(path.split('/').at(-1)))
else {
code = 400
msg = '此本地验收接口未定义,已拦截写请求'
}
res.setHeader('Content-Type', 'application/json; charset=utf-8')
res.end(JSON.stringify({ code, msg, data }))
})
gatewayFixtures.configureServer(server)
}
}