Commit Graph
16 Commits
Author SHA1 Message Date
yml2213 f0d745ea50 fix(worker-risk): 修掉自动封禁被对账撤销的竞态,以及账号不存在时写假留痕
## 一、刚下的封禁会被对账撤销(生产实测)

Tier 3 是「先把 tracker.banned 置 true,再 await 写库」。在写库提交前的窗口里,
并发的另一个请求会走对账分支、读到 status 仍是 active,于是把刚下的封禁撤销掉。

生产日志(worker 28):
  17:20:11.361 WARN  封禁标记与数据库不一致,已自动解除并放行   ← 对账把刚下的封禁撤了
  17:20:11.388 ERROR 打手账号已被系统自动永久封禁! count=30    ← 27ms 后才写出封禁

后果:刷子能漏进来一个请求;日志还会让运营以为"数据库里已经解封了"。
修法:新增 bannedAt,刚下的封禁在 BAN_GRACE_MS(60s) 内不与数据库对账。
人工解封走 releaseWorkerRiskTracker() 直接删 tracker,不受宽限期影响,处置不会变慢。

## 二、账号不存在时会写下"已永久封禁"的假记录

updateWorkerUser 在账号不存在时返回 null 而**不抛错**,原代码直接把返回值丢弃,
于是照样打出"已被系统自动永久封禁"的日志并写 tier3_autofrozen 留痕。
外挂伪造成随机 uid 时就会命中这条路径,留下一批查不到的假封禁记录。
修法:显式判断返回值,为空则记明确错误并跳过留痕。

## 测试

新增 2 个用例:
  - 刚下的封禁在宽限期内不得被对账撤销(锁住上面的竞态)
  - 账号不存在时不写"已封禁"的假日志
并更新 2 个原有用例:宽限期内本就不发起对账,原断言(bannedVerifiedAt > 0)已过时。

风控测试 11/11;后端全量 559 测试 556 通过 0 失败。
2026-10-02 17:59:06 +08:00
yml2213 78c676d2b9 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 全绿
2026-10-02 17:05:25 +08:00
yml2213 adaf4ed240 feat(worker-risk): 新增大厅阶梯式风控中间件与鉴权短缓存,惩罚外挂死循环狂刷并支持恶意封号
1. 新增 worker-risk-control 阶梯式风控:单打手/IP 限流 3次/秒(超限返 429);1 分钟累计违规 10 次升级为二级惩罚(锁定 5 分钟秒拒并带 Retry-After);惩罚期内持续暴力刷接口累计 30 次自动判定恶意不改,直接封禁打手账号(status='frozen')并作废所有 session;
2. 风控限流前置挂载在 requireWorkerSession 之前:命中限流或处于风控期时,微秒级在内存中基于 JWT payload 与 IP 拦截,彻底杜绝外挂流量穿透到 PostgreSQL 数据库;
3. verifyWorkerSessionToken 增加 3 秒内存校验缓存:并发请求直接复用内存鉴权对象,同一页面并发发出的多个接口请求仅查 1 次数据库,极大减轻连接池争抢压力;
4. 全套新增风控阶梯机制与自动封号自动化测试,后端 549 项测试全绿通过。
2026-10-02 16:30:53 +08:00
yml2213 142c4ee141 perf(realtime): 一次广播只序列化一次,不再按订阅者重复 stringify
抓包发现单个后台客户端 5 秒收到 981,776 字节 / 336 条事件(约 196KB/s、67 条/秒、
每条平均 2680 字节,最大 6418 字节)。事件构成:work_order.changed 34 条/秒、
order.changed 23 条/秒、admin_notification.changed 7.6 条/秒。

原广播实现在订阅者循环里逐个 `JSON.stringify(payload)` 并重建 SSE 模板串:
客户端越多,同一份载荷就被序列化越多次(后台/打手端都在线时更明显)。
改成先 `buildEventFrame()` 构造一次,循环里只写同一帧:

- broadcast() 只调用一次 buildEventFrame
- writeEvent()(ready 事件与断线补发)复用同一构造函数
- 行为不变,仅去掉重复序列化

受众过滤经核对本来就是正确的(admin 事件只发后台、worker 事件按 workerIds 过滤),
realtime_events 也有自我清理(每 100 条保留最近 500 条,实测 555 行/2MB),
故本次只动序列化这一处。

验证:后端 548 项单测全绿(新增 1 项守卫,禁止 broadcast 内再出现 JSON.stringify)。
2026-10-01 15:46:43 +08:00
yml2213 5092e88618 perf(admin): 打手等级筛选用 UNION 形式、概览加短缓存、静态配置接口加缓存头
三处都出自审计清单,改动互不重叠。

1) 打手等级筛选(生产实测 1119ms)
   原写法是对 worker_users 的两个相关 EXISTS(接单人 / 拼单参与人),规划器无法用它
   驱动索引,只能顺序扫完整张 work_orders。改成先把等级解析成打手 id
   (worker_users(level_id) 有索引,等级只有 6 个、解析出几百个 id),再走索引。
   注意:**不能**写成 `assigned_worker_id = ANY(...) OR id IN (...)` —— 实测 1852ms,
   比原写法还慢(OR 右支要逐行回表取 id)。用 UNION 形式:
     wo.id IN (SELECT id FROM work_orders WHERE assigned_worker_id = ANY(...) UNION
               SELECT work_order_id FROM work_order_shares WHERE worker_id = ANY(...) AND status != 'cancelled')
   实测 61ms(count),语义与原来完全一致(含"空数组=过滤掉全部")。
   三个入口 listWorkOrders / countWorkOrders / getWorkOrderStatistics 都先解析;
   未解析时保留原相关 EXISTS 兜底。

2) 平台概览短缓存(5 秒)
   一次并发 5 条 count(4 条打 work_orders,一条是运营态 CASE),而池只有 8 条连接,
   后台刷新时与其他请求互相争抢(抓包显示整批 9 个请求同时发出、它自身 167ms)。
   看板计数滞后几秒无感,用 WorkerListCache 缓存并合并并发。

3) 三个静态配置接口加 HTTP 缓存头(private, max-age=300)
   接单分类、快手行业店铺、自定义店铺此前没有任何 Cache-Control,每次刷新都真发一次
   请求(抓包里 4 个这类请求)。带 private 是因为这些是带鉴权的后台接口。
   **dev-mock/status 故意不加**:它是可切换状态,缓存会让"切了开关界面没变"。

验证:后端 547 项单测全绿(新增 4 项守卫,分别锁住 UNION 形式、三个入口都解析、
概览缓存、以及 dev-mock 不得加缓存)。
2026-10-01 15:36:46 +08:00
yml2213 1ca15e9025 perf(db): 统计金额落到列上、会话刷新节流、补齐保留与内存治理
背景:生产 32 天内 >500ms 慢查询从 2.4k/天涨到 1.3 万/天(09-26 尖峰 14.3 万),
pg_stat_statements 显示后台工单列表族占 57%、打手端 visible_orders 占 27%;
同时 backend 容器十天从 194MB 涨到 754MB(768MB 限额的 98%),贴顶后整体变慢。

1. 新增迁移 083:work_orders 增加派生列 source_order_amount_fen,并从
   material_json.source.orderAmountFen 回填(只认 15 位以内整数,与 JS/SQL 口径一致)。
   统计查询原来每行要解析 1.2KB JSON(全表约 132MB)且同一表达式求值两次,
   生产实测 3.5~4.4 秒;改走派生列后 COALESCE 短路,未命中列的行才回退 JSON/orders.total_amount,
   口径与改造前完全一致。写入点:createWorkOrder / updateWorkOrder 各一处,并加静态守卫。
2. worker_sessions.last_seen_at 从"每个请求都写"改为"5 分钟才写一次":
   32 天被写了 3300 万次(占 DB 总时间 2.8%,并堆出 14% 死元组)。仍用一条语句返回鉴权有效性
   (EXISTS target AS valid),不能因为不写就让上层 401。
3. 保留策略补上 work_order_events(71.7 万行 / 269MB,此前完全没纳入清理,只增不减),
   新增配置 retention.workOrderEventDays(默认 180 天)与 WORK_ORDER_EVENT_RETENTION_DAYS 出口。
4. 内存治理:compose 后端限额 768m→1g 并显式设置 V8 老生代上限(--max-old-space-size=768),
   避免贴 cgroup 上限后 GC 抖动;新增每 10 分钟的进程内存快照日志(rss/heapUsed/heapLimit),
   用于区分"正常 GC 曲线"与"真泄漏"。
5. 限流桶加 5 万条硬上限:key 里含请求体字段(collect 的 orderNo)时可被构造成高基数,
   只按过期清理挡不住内存增长。

验证:后端 530 项单测全绿(新增 11 项,含派生列/会话/保留的静态守卫与解析函数单测)。
2026-10-01 12:55:09 +08:00
yml2213 9fb27b0ad2 feat(pdd): 拼多多订单支持 out_sku_sn 匹配与浏览器插件加密保活
- 拼多多进单 Cookie 失效即时拦截与告警修复(支持 43001 会话过期等错误识别)
- 拼多多订单映射优先使用 out_sku_sn(外部商家编码)作为商品规格与工单匹配核心字段
- 新增 Chrome/Edge 浏览器扩展 (Manifest V3),支持端到端 RSA-OAEP + AES-256-GCM 信封加密自动保活
- 后台店铺管理新增插件配对面板、专属密钥管理与一键导入配置 JSON
2026-09-14 12:24:48 +08:00
yml2213 25b89bb503 优化:降低访问日志噪声并增加日志文件轮转 2026-08-30 16:59:11 +08:00
yml2213 6ca54e468e 安全加固:增加后台登录 IP 限流并记录验证结果 2026-08-30 16:47:16 +08:00
yml2213 02c9de8b9c feat(ops): 数据保留清理、CORS 白名单与上传失败回滚
- 新增每日数据保留清理任务:按保留期清空原始报文、删除过期
  事件/审计记录,批量幂等可重跑;067 迁移补齐清理索引并清理
  冗余索引,check-sql-guard 增加防线
- 生产 CORS 改为显式来源白名单(CORS_ALLOWED_ORIGINS),禁用
  通配符,非通配响应补充 Vary: Origin
- 文件上传任一步失败时回滚已上传对象,清理失败记录 warn 日志
- 新增 HTTP/CORS 与 PostgreSQL 集成测试;若干文件仅 prettier 重排
2026-08-30 13:59:01 +08:00
yml2213 5c7c3e14e3 统一前后端代码格式化配置 2026-08-16 17:27:12 +08:00
yml2213 a2bce8b272 增强后端 TypeScript 严格类型检查 2026-05-26 10:07:19 +08:00
yml2213 49ac82698f 增强关键接口幂等与限流保护 2026-05-26 08:32:15 +08:00
yml ecab6ae9b7 feat: complete CORS config-driven middleware rewrite
- cors.ts: createCorsMiddleware factory with RuntimeConfig param
  - Wildcard ['*'] mode: Access-Control-Allow-Origin: * (backward compatible)
  - Specific origins mode: reflect matching origin + Allow-Credentials
  - Added Access-Control-Max-Age: 86400 for preflight caching
- app.ts: accept config param, use createCorsMiddleware(config)
- index.ts: pass runtimeConfig to createApp
- env-overrides.ts: add string[] to RuntimeConfigValue, corsOriginsEnv helper
- runtime-config.ts: cors.allowedOrigins: string[] type
- defaults.ts: cors.allowedOrigins: ['*'] default
- .env: CORS_ALLOWED_ORIGINS=* with production example
2026-05-21 23:22:39 +08:00
yml 0e6bf88beb [Snow Team] express-types: auto-commit work 2026-05-21 22:57:48 +08:00
yml efa41ba976 refactor backend route and fulfillment modules 2026-05-21 18:38:04 +08:00