feat(risk-control): 新增 IP/账号手动封禁,并收口财务同步的全表更新

- 风控:worker_risk_bans 表 + 前置封禁中间件(登录一并拦截)+ 后台列表/新增/解封
- 财务:成本快照补商品维度收口到本批 order_item_id,不再整表扫 899MB 的 order_items
- 性能:order_items 覆盖索引(迁移 110)、大厅 pageSize 上限 20、已读/上传接口限流
This commit is contained in:
yml2213
2026-10-30 20:15:14 +08:00
parent 1b170d3f45
commit fb485f7e36
17 changed files with 1373 additions and 18 deletions
@@ -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']),
+5
View File
@@ -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 之前先行拦截超频/风控请求,避免外挂脚本打满数据库连接
+15
View File
@@ -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(
+12
View File
@@ -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) => {