feat(worker-risk): 风控后台页面(可调阈值/实时状态/事件留痕/人工封禁解封),并修复解封后仍被 403 的 bug
## 修复的 bug(生产已发生) 风控三级封号把 tracker.banned 置为 true 后,banned 检查发生在鉴权之前, 而它只存在于进程内存。后台把账号改回 active 时数据库变了、内存没变, 于是账号在库里已经正常、请求却永远 403,只能等容器重启才恢复。 修法两道: - **数据库是唯一事实来源**:banned 改为带 30 秒 TTL 的缓存,到点回查 worker_users.status;已解封(或账号已删)则清除标记并放行。 对被判 banned 的 key 最多每 30 秒查一次,即便对方仍以 65 QPS 刷也只是 每 30 秒一次查询。查询失败时**保守维持封禁**,不因数据库抖动放行刷子。 - **后台改状态时主动释放**:review 只允许设 active/rejected/disabled/pending_review (没有 frozen),所以走到那里就意味着不再由风控冻结,直接释放内存状态。 顺带修掉一个连带缺陷:解封时只清了 violationCount,没清滑动窗口计数, 账号刚恢复就会被上一轮残留的 windowCount 立刻限流、并在 60 秒内二次封禁。 现在整轮窗口状态一并清零。 ## 新增后台页面 接单平台 → 风控(/admin/worker-platform?tab=risk-control),三块: - 阈值配置:6 个阈值可视化编辑,落 app_config_entries,保存即热生效,无需重启 - 当前实时风控:进程内存状态(已封禁/惩罚中/仅记录),支持人工解封、封禁、仅释放内存状态 - 风控事件留痕:worker_risk_events 分页查询,可按类型/IP 筛选 ## 关键取舍 只记**显著事件**(二级惩罚、三级封号、人工处置),不记每一次被拒请求。 被刷时实测 45~65 QPS,按请求写库正是最初要解决的问题。实时计数仍走进程内存。 事件写入为 fire-and-forget 并吞异常,留痕失败绝不影响大厅请求。 ## 验证 - 新增 6 个用例覆盖解封立即生效、对账自愈、TTL 不重复查库、对账失败保守维持、阈值可配置 - 迁移 089 已在临时库实测:表结构/4 索引/CHECK 约束/外键均正确,非法 event_type 被拒 - 后端 557 测试 554 通过 0 失败;前后端 typecheck/lint/format/build 全绿
This commit is contained in:
@@ -15,6 +15,9 @@ import type {
|
||||
WorkerDepositUnfreeze,
|
||||
WorkerFinanceConfig,
|
||||
WorkerHallConfig,
|
||||
WorkerRiskConfig,
|
||||
WorkerRiskOverview,
|
||||
WorkerRiskEventList,
|
||||
WorkerFinanceRequest,
|
||||
WorkerCancelRequest,
|
||||
WorkerOrderFeedback,
|
||||
@@ -850,3 +853,49 @@ export function deductAdminWorkOrderPendingDeposit(
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 大厅风控(接单平台 → 风控)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function fetchAdminWorkerRiskOverview() {
|
||||
return apiGet<WorkerRiskOverview>('/api/v1/admin/worker-platform/risk-control')
|
||||
}
|
||||
|
||||
export function saveAdminWorkerRiskConfig(payload: WorkerRiskConfig) {
|
||||
return apiPost<{ config: WorkerRiskConfig }>(
|
||||
'/api/v1/admin/worker-platform/risk-control',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchAdminWorkerRiskEvents(params: {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
eventType?: string
|
||||
ip?: string
|
||||
workerId?: number
|
||||
sinceHours?: number
|
||||
}) {
|
||||
return apiGet<WorkerRiskEventList>('/api/v1/admin/worker-platform/risk-control/events', params)
|
||||
}
|
||||
|
||||
export function banAdminWorkerRisk(workerId: number, reason?: string) {
|
||||
return apiPost<{ workerId: number; status: string; reason: string }>(
|
||||
`/api/v1/admin/worker-platform/risk-control/workers/${workerId}/ban`,
|
||||
{ reason },
|
||||
)
|
||||
}
|
||||
|
||||
export function unbanAdminWorkerRisk(workerId: number, reason?: string) {
|
||||
return apiPost<{ workerId: number; status: string; reason: string }>(
|
||||
`/api/v1/admin/worker-platform/risk-control/workers/${workerId}/unban`,
|
||||
{ reason },
|
||||
)
|
||||
}
|
||||
|
||||
export function releaseAdminWorkerRiskTracker(workerId: number) {
|
||||
return apiPost<{ workerId: number }>(
|
||||
`/api/v1/admin/worker-platform/risk-control/workers/${workerId}/release`,
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user