feat(risk-control): 新增 IP/账号手动封禁,并收口财务同步的全表更新
- 风控:worker_risk_bans 表 + 前置封禁中间件(登录一并拦截)+ 后台列表/新增/解封 - 财务:成本快照补商品维度收口到本批 order_item_id,不再整表扫 899MB 的 order_items - 性能:order_items 覆盖索引(迁移 110)、大厅 pageSize 上限 20、已读/上传接口限流
This commit is contained in:
@@ -3,9 +3,12 @@ import type { Request } from 'express'
|
||||
|
||||
import {
|
||||
banAdminWorkerRisk,
|
||||
banAdminWorkerRiskTarget,
|
||||
getAdminWorkerRiskOverview,
|
||||
listAdminWorkerRiskBans,
|
||||
listAdminWorkerRiskEvents,
|
||||
releaseAdminWorkerRiskTracker,
|
||||
revokeAdminWorkerRiskBan,
|
||||
saveAdminWorkerRiskConfig,
|
||||
unbanAdminWorkerRisk,
|
||||
} from '../../../services/worker-platform/index.js'
|
||||
@@ -63,6 +66,70 @@ router.post(
|
||||
}),
|
||||
)
|
||||
|
||||
/**
|
||||
* 手动封禁(IP / 账号)的持久化列表。
|
||||
*
|
||||
* 与「实时风控」里那批内存 tracker 的区别:这里是落库的事实,进程重启不丢,
|
||||
* 中间件按它拦截 /api/v1/worker 下的所有请求(含登录)。
|
||||
*/
|
||||
router.get(
|
||||
'/worker-platform/risk-control/bans',
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
createJsonHandler((req) => listAdminWorkerRiskBans((req.query || {}) as JsonRecord), {
|
||||
successMessage: 'ok',
|
||||
errorMessage: '读取封禁列表失败',
|
||||
scope: '[admin/worker-platform/risk-control/bans]',
|
||||
}),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/worker-platform/risk-control/bans',
|
||||
requireAdminRoles(['admin']),
|
||||
createJsonHandler(
|
||||
(req) => banAdminWorkerRiskTarget(actorOf(req), (req.body || {}) as JsonRecord),
|
||||
{
|
||||
successMessage: '封禁已生效',
|
||||
errorMessage: '封禁失败',
|
||||
scope: '[admin/worker-platform/risk-control/bans]',
|
||||
audit: (req, data) => {
|
||||
const result = data as JsonRecord
|
||||
const ban = (result.ban || {}) as JsonRecord
|
||||
return {
|
||||
action: 'worker_risk_ban_created',
|
||||
targetType: String(
|
||||
ban.targetType || (req.body as JsonRecord | undefined)?.targetType || '',
|
||||
),
|
||||
targetId: String(ban.target || ''),
|
||||
data: { ban },
|
||||
}
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/worker-platform/risk-control/bans/:banId/revoke',
|
||||
requireAdminRoles(['admin']),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
revokeAdminWorkerRiskBan(
|
||||
paramOf(req.params.banId),
|
||||
actorOf(req),
|
||||
(req.body || {}) as JsonRecord,
|
||||
),
|
||||
{
|
||||
successMessage: '封禁已解除',
|
||||
errorMessage: '解除封禁失败',
|
||||
scope: '[admin/worker-platform/risk-control/bans/:banId/revoke]',
|
||||
audit: (req) => ({
|
||||
action: 'worker_risk_ban_revoked',
|
||||
targetType: 'worker_risk_ban',
|
||||
targetId: String(paramOf(req.params.banId)),
|
||||
}),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
router.get(
|
||||
'/worker-platform/risk-control/events',
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
|
||||
@@ -7,11 +7,16 @@ import workerHallRoutes from './worker/hall.js'
|
||||
import workerOrdersRoutes from './worker/orders.js'
|
||||
import workerConsignmentRoutes from './worker/consignment.js'
|
||||
import { requireWorkerSession } from './worker/session.js'
|
||||
import { createWorkerBanGuardMiddleware } from '../middleware/worker-ban-guard.js'
|
||||
import { createWorkerHallRiskMiddleware } from '../middleware/worker-risk-control.js'
|
||||
import { buildNotFoundPayload } from '../utils/http.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
// 手动封禁(IP / 账号)最先判:命中直接 403,连登录都不放行。
|
||||
// 整个过程只查进程内缓存,不碰数据库,被刷时也不会放大压力。
|
||||
router.use(createWorkerBanGuardMiddleware())
|
||||
|
||||
router.use(workerAuthPublicRoutes)
|
||||
|
||||
// 风控限流前置:在校验数据库 session 之前先行拦截超频/风控请求,避免外挂脚本打满数据库连接
|
||||
|
||||
@@ -22,9 +22,23 @@ import {
|
||||
} from '../../services/worker-platform/index.js'
|
||||
import { getRequiredWorkerSession, requireActiveWorker } from './session.js'
|
||||
import { createRouteHandler } from '../../utils/http.js'
|
||||
import { createRateLimitMiddleware } from '../../middleware/rate-limit.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
/**
|
||||
* 「标记已读」的限流。
|
||||
*
|
||||
* 打手每打开一张单就会 POST 一次,正常操作远低于 2 次/秒;
|
||||
* 脚本轮询同一个订单号时(生产实测 10 分钟 337 次、且集中在几个固定 id)会先被挡在这里,
|
||||
* 不必等它打到数据库。
|
||||
*/
|
||||
const markReadRateLimit = createRateLimitMiddleware({
|
||||
scope: 'worker:order-read',
|
||||
windowMs: 60_000,
|
||||
max: 120,
|
||||
})
|
||||
|
||||
router.get(
|
||||
'/orders',
|
||||
requireActiveWorker,
|
||||
@@ -167,6 +181,7 @@ router.get(
|
||||
router.post(
|
||||
'/orders/:workOrderId/read',
|
||||
requireActiveWorker,
|
||||
markReadRateLimit,
|
||||
createRouteHandler(
|
||||
(req) =>
|
||||
markWorkerOrderRead(
|
||||
|
||||
@@ -15,9 +15,20 @@ import {
|
||||
} from '../../services/worker-platform/index.js'
|
||||
import { getRequiredWorkerSession, requireActiveWorker } from './session.js'
|
||||
import { createRouteHandler } from '../../utils/http.js'
|
||||
import { createRateLimitMiddleware } from '../../middleware/rate-limit.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
/**
|
||||
* 附件上传限流:每次上传都要落 OSS + 写 file_assets,是最贵的写接口之一。
|
||||
* 正常交单不会超过 30 次/分钟(连拍多张图也只是十几次),脚本批量传图会先被挡。
|
||||
*/
|
||||
const uploadRateLimit = createRateLimitMiddleware({
|
||||
scope: 'worker:files-upload',
|
||||
windowMs: 60_000,
|
||||
max: 30,
|
||||
})
|
||||
|
||||
router.get(
|
||||
'/profile',
|
||||
createRouteHandler((req) => getWorkerProfile(getRequiredWorkerSession(req)), {
|
||||
@@ -125,6 +136,7 @@ router.post(
|
||||
router.post(
|
||||
'/files/upload',
|
||||
requireActiveWorker,
|
||||
uploadRateLimit,
|
||||
uploadSingleFile,
|
||||
createRouteHandler(
|
||||
(req) => {
|
||||
|
||||
Reference in New Issue
Block a user