Commit Graph
272 Commits
Author SHA1 Message Date
yml2213 7c8832078f feat(finance): 接单模板发单金额落规则级成本版本
此前接单平台成本版本只有 sys_work_order_reward 的全局 0 元占位,662 个模板的发单金额从未进入 cost_versions。\n\n现在为 662 个模板各生成一条规则级版本:634 条固定发单金额 + 28 条按件单价;全局 0 元版本仅作“按工单实际结算”占位。成本版本页新增“用户实付均价”和“平均实发/项”,支持按模板名称/规则键搜索,按商品筛选也能命中规则版本。\n\n历史工单快照关联到对应模板版本;模板保存时自动同步单条成本版本;删除模板会先把历史版本脱钩停用,保留快照可追溯。\n\n新增迁移 112 覆盖索引,用户实付聚合从约 1.1s 降到约 150ms。
2026-10-22 19:29:53 +08:00
yml2213 0889235870 feat(finance): 完成成本快照运行层、销售事实与人工支出登记
迁移 108-111:快照业务时间与预估转实际、销售事实表、系统成本项、公司人工支出登记。接单平台按工单实际 reward 入账;人工支出支持登记/审批/驳回/报销并写 actual 快照。新增销售毛利页、人工支出页与订单入口。
2026-10-22 16:41:03 +08:00
zhaoxing 72a758fd7c fix:修复问题接单平台使用游戏ID查询不到数据 2026-10-22 15:32:26 +08:00
zhaoxing 067d54fdd8 接单平台:新增店铺筛选条件 2026-10-22 14:45:40 +08:00
yml2213 ef36453694 feat(dcauth): 移除机器码,改用纯设备码认证
- DCAuth 协议升级到 v2,签名原文只含设备码;客户端 2.0.0,旧版本返回 426
- 删除客户端机器码采集、后端 machine_code_* 字段与日志、后台机器码展示
- 102 迁移在生产执行前直接改为无机器码结构;SQL 守卫仅放行该文件
- Mock 短信不启用每日限额;修复慢电脑首次启动的协议转发竞态
- 新增 macOS DCAuth.app 打包脚本,注册 dcauth:// 协议,浏览器可直接唤起
2026-10-21 20:46:25 +08:00
yml2213 040100f290 feat(dcauth): 新增后台手机号与设备认证登录
登录链路改为账号密码 + 短信验证码 + DCAuth 设备签名,并新增后台手机号绑定、
登录日志、设备绑定与重置接口。
2026-10-21 20:46:24 +08:00
yml2213 7542d547bf feat: 新增客服备用金台账与后台用户中文显示名
- 新增备用金账户、流水、支出、对账与权限
- 后台用户支持中文显示名并同步备用金姓名
- 审计、登录记录、任务及工单操作人优先展示中文名
- 补充备用金与人工支出方案文档
2026-10-18 13:55:24 +08:00
yml2213 584b6f52aa fix(work-order): 问题单处置存在活跃拼单份额时禁止取消/退回大厅/重开
生产实锤 WK26101503335487:客服对份额已提交的拼单母单走问题单处置-取消,
母单整单 cancelled 后份额验收/单人撤单全部被母单状态挡死,后台无补救入口。

- 抽出 getActiveWorkOrderShareCountWithClient 统一活跃份额统计口径
- resolveProblemWorkOrder 的 cancel/cancel_deduct/reopen 先过活跃份额防线
  (return_to_worker 不拦),返回结构改为 {order, failureReason}
- settleOverdueWorkOrder 复用同一统计函数,消除两份 COUNT 口径
- 服务层映射 409 work_order_problem_resolution_has_active_shares,
  文案引导客服先单独验收/单人撤单
2026-10-15 10:34:43 +08:00
yml2213 5a73fb74a9 feat(douyin-mashida): 新增抖音券核销后台界面(列表/核销/冲正/同步/对账/作废)
抖音订单并非纯自动发货:内部 SKU 命中 AFFILIATE_DASH_SKU_MAPPING 才走自动通道,
未命中显式回落人工,且核销依赖买家自己到 /redeem 输订单号。此前核销侧在后台完全
不可见:自动核销是 void .catch(warn) 只落日志,cancelVerify 出网方法已就绪但没有入口。

照快手电子凭证的骨架做一套抖音版(两张表两套状态机,不复用):
- 券码状态、平台侧状态、同步错误全部可视化,支持只看同步失败
- 核销/冲正/重新同步/平台对账/重发券码/作废,每个写操作都带审计
- 券码列显示「核销实际会用的券码」(platform_cert_no 优先),平台改过名时额外标注
- 冲正先打平台 cancelVerify 再回退本地,避免本地与平台状态漂移
2026-10-12 19:45:06 +08:00
yml2213 cb064d7cd5 fix(worker-platform): 工单号前缀改 WK 并在检索侧容错 O/0,彻底解决「按工单号搜不到」
## 线上定位到的真正根因

客服搜 `WO26101213031939` 搜不到,但用同单的平台号 `2628501763808879` 搜得到。
到生产库核对后确认:

- 库里真实值 `work_order_no = WO26101213031939`(hex 574f… = 字母 O),状态 pending_acceptance;
- 用字母 O 的 SQL 分支 → 1 行;用数字 0(`W026101213031939`)→ 0 行;
- 全库 127,906 单**全部**以 `WO` 开头、`W0` 开头 0 单、字母 `o` 只出现在第 2 位。

即:前缀里的字母 O 被输成数字 0(`WO` / `W0` 在界面字体里几乎无法区分),
落进通配符分支后必然为空 —— 与前面几轮的「筛选把单藏起来」是两个独立成因,
这也解释了为什么改筛选逻辑一直没解决。

## 修法

1. 生成端换前缀:generateWorkOrderNo 产出 `WK…`(K 与 0 不会看混),从源头消掉歧义;
2. 检索端同时兼容三种写法(历史 WO 号无需迁移):
   - isWorkOrderNoLookupKeyword 放宽为 `^W[oOkK0]…`;
   - 新增 buildWorkOrderNoKeywordVariants:W0→[W0, WO, WK]、WO→[WO, W0]、WK→[WK],
     SQL 分支对每个候选下发「等值 + 前缀」条件(仍是索引条件,不会退化成全表扫);
   - 前端 isExplicitOrderNoSearch 同步(否则面板不清理筛选、实时补丁护栏也不生效);
3. 顺手加一层归一化:NFKC(全角→半角)+ 去零宽字符,前后端同一口径
   (normalizeLookupKeyword / normalizeSearchText),覆盖「粘贴带不可见字符」这类同族问题;
4. 寄卖匹配弹窗的占位提示更新为 WK... / WO...。

## 验证

- 后端 npm run check 全绿:692 tests(689 pass / 3 skip),新增
  work-order-keyword-lookup.test.ts 直接断言 SQL 参数与「不得出现前置通配符」;
- 前端 npm run check 全绿:139 tests;
- 既有用例 worker-platform-repo.test.ts 的期望值同步为新形态(两条候选条件)。
2026-10-12 16:51:08 +08:00
yml2213 c5e8b78a33 fix(worker): 修复打手订单可见范围查询中参数类型推导不兼容错误
- 显式类型转换:在 worker_visible 的 4 个 UNION 分支中,为 workerSharePlaceholder 显式补充 ::bigint 与 ::text 类型转换;
- 避免类型推导冲突:消除由于 JSON 字符串比较导致的 bigint = text 操作符缺失错误(operator does not exist: text = bigint);
- 验证方式:在 PostgreSQL 容器中验证 PREPARE/EXECUTE 与在后端容器内实测 listWorkOrders 正常返回 20 条记录。
2026-10-11 15:25:48 +08:00
yml2213 6012add3c9 feat(worker): 打手接单界面支持查看超时历史订单并增加安全免责警示
- 数据库与查询层:通过索引 idx_work_order_events_timeout_worker_id 关联打手历史 timeout 事件与取消的拼单份额,打手在退回大厅或被他人抢走后仍能在「已超时」tab 查看历史记录;
- 状态与权限管控:针对超时工单将个人端状态稳定映射为 open,报酬与冻结押金置为 0,彻底禁用上传图片、提交验收、好友资料、申请取消等履约操作;
- 风险防护与免责说明:列表与抽屉详情顶部增加醒目的免责声明与警示横幅(告知订单已退回大厅或被他人接手,严禁在游戏内继续赠送,造成的损失平台概不负责);锁定买家游戏资料与快捷复制;
- 验证方式:通过后端全部 676 个测试、typecheck、check:sql 及前端 check(eslint、vitest/node:test、tsc、vite build)。
2026-10-11 15:20:46 +08:00
yml2213 b82f9c4063 fix(worker-platform): 补齐拼单份额超时自动处置机制与退回大厅流程
- 根因排查:此前超时扫描仅针对整单 in_progress,而拼单母单为 open 且 deadline_at 为空,导致打手超时的拼单份额无法被定时扫描命中与释放;
- 定时扫描:在 settleOverdueWorkOrders 中引入 listOverdueWorkOrderShares,自动扫描超时未提交资料的拼单份额(joined);
- 超时处置:新增 settleOverdueWorkOrderShare 事务处理,按母单 timeout_policy 释放或扣除押金,份额标记 cancelled,母单保持或恢复 open 状态重回抢单大厅,记录 timeout 事件并推送实时变更;
- 提交防呆:在打手提交拼单验收时检测个人份额是否已超时,超时则立即执行处置并拦截提交;
- 倒计时展示:在 mapWorkOrderForWorker 中根据 myShare.created_at + timeout_minutes 为拼单打手计算并展示个人履约截止时间;
- 质量验证:补齐拼单截止时间计算与处置链路测试,全量 676 个测试均通过,通过 npm run check 校验。
2026-10-11 14:53:56 +08:00
yml2213 748c18bc7d feat(order): 精准区分自动直发与接单平台打手履约,智能门控H5直充按钮
- 后端增加按订单批量关联接单工单查询,精准识别履约类型(自动直发 / 打手履约 / 电子凭证 / 拼多多发货)
- 订单列表与详情页中,仅当订单为自动直发(affiliate_dash / 码速达直充)时展示【H5】一键复制直充链接与兑换状态控制
- 接单平台工单单与纯核销单隐藏直充 H5 按钮与兑换暂停/关闭标签,防止客服向打手单买家误发直充链接
- 交付模式标签升级为「⚡ 自动直发」、「👥 打手履约」、「🎫 电子凭证」、「📦 拼多多发货」

验证方式:前端类型检查与打包构建通过,全量 132 项前端单测与 674 项后端单测全部通过
2026-10-10 19:44:20 +08:00
yml2213 03020e85c7 feat(order): 订单列表页UI重构与多平台(快手/91卡券、抖音、拼多多)适配
- 顶部新增平台快捷分流器(Segmented),支持「全部 / 快手·91卡券 / 抖音码速达 / 拼多多 / 本地模拟」一键筛选
- 重构订单表格列布局为平台与订单号、买家信息、商品规格、交付与履约状态、金额与支付时间、快捷操作等现代化字段
- 优化快手与 91卡券业务归集逻辑,统一数据查询与徽标显示口径
- 增强订单行快捷操作,支持一键复制 H5 领取链接与查看详情

验证方式:后端 674 项单测全通过,前端 132 项单测全通过,前端打包构建通过
2026-10-10 18:54:40 +08:00
yml2213 412d219cca feat(claim): 支持固定H5订单号领取与订单兑换有效性后台控制
- 数据库新增 099_order_claim_status.sql 迁移,扩展 claim_status、claim_status_reason 等字段
- 后端新增 /claims/h5/:orderNo、UID提交、分销提交接口,保留原有 token 接口双轨兼容
- 增加兑换有效性强拦截:暂停/关闭/退款/取消订单精准阻断并返回友好提示
- 后台新增 POST /admin/orders/:orderId/claim-status 接口与审计日志记录
- 前端管理后台(订单列表、详情页)支持 H5 兑换状态展示、一键复制链接及暂停/关闭/启用弹窗
- 前端领取页面适配 /h5/:orderNo 路由及暂停/关闭等状态的异常展示
- 验证方式:运行前后端类型检查、全量单元测试(后端 674、前端 132)及全量构建编译均通过
2026-10-10 18:05:46 +08:00
yml2213 85fc2eaa7c feat(worker-platform): 支持后台人工调级锁定等级与解除自动升级限制
- 做了什么:
  1. 新增数据库迁移 098_worker_level_lock.sql,在 worker_users 表增加 is_level_locked、level_locked_at、level_locked_reason、level_locked_by 字段;
  2. 后端自动升级函数 maybeUpgradeWorkerLevelWithClient 和判定函数 shouldApplyAutomaticWorkerLevel 增加 is_level_locked 守卫,被锁定的打手跳过自动升级;
  3. 后台打手审核调级服务 reviewAdminWorkerUser 支持等级锁定参数与变更记录,人工调级时自动写入 worker_level_logs 流水,并提供 updateAdminWorkerLevelLock 专用接口;
  4. 前端打手列表等级列展示「已锁定」状态及 Tooltip 原因,调级弹窗支持「锁定自动升级」开关与原因输入,操作列提供「解除限制」快捷操作;
  5. 打手详情抽屉补充展示等级锁定说明。
- 为什么:
  解决打手被人工调整等级(如 VIP5 调至 VIP4)后,因历史累计完成单数满足门槛导致后续结算再次自动升级回高等级的问题,并支持后续随时解除限制恢复自动升级。
- 验证方式:
  1. 后端新增 shouldApplyAutomaticWorkerLevel 与 mapWorkerUser 单元测试,运行 npm run check(670 测试全过,类型与 SQL guard 验证通过);
  2. 前端新增 worker-level-lock.test.ts 单元测试,运行 npm run check(130 测试全过,typecheck 与 build 验证通过)。
2026-10-10 13:44:57 +08:00
yml2213 4fce07a621 refactor(douyin-mashida)!: 纠偏——删掉自建的皮肤通道,改回复用 affiliate_dash
上一轮我把皮肤交付做成了「另建一套执行器 + 独立领取接口 + 只提交账号就结束」,
跟买家实际要走的过程不一致(真正的完成信号是「扫码绑定后的 UID == 买家填的 UID」),
而且核销还触发了老通知逻辑,给买家**又发了一条 /redeem** —— 买家收到两条链接。
本次按用户要求改回**完全复用仓库现成的 affiliate_dash 通道**(= 旧的快手皮肤自动发货)。

**删掉**
- 执行器 `douyin_mashida_skin`(含 registry/types/planner/bootstrap/路由白名单注册)
- `services/claim/claim-skin-service.ts`、`routes/claim-skin.ts`、`/api/v1/claim-skin` 挂载
- 迁移 096 的 `claim_submission_json` 全链路(task-repo / rows / inputs / task-detail /
  admin-read-service 的 claimSubmission),并新增 **097 兜底迁移**把该列 DROP 掉
- 顺带清掉上一轮专为已删接口加的死代码(utils/http.ts 的 buildMessageSuccessPayload 等)

**改回复用**
- `resolveOrderItemClaimLink`:通用解析(只看订单项、不看执行器)→
  `buildConfiguredClaimUrl(claimLinkBase, token)`;多履约单元取 task id 最小并告警;
  **绝不回退** hash 形态的 `buildClaimUrl`
- `cert_link` 与付款后那条**唯一**消息共用同一个解析结果
- **核销后不再给抖音买家发第二条消息**(`shouldNotifyBuyerAfterVerify` 只放行非抖音 provider)
- 买家侧走既有流程:输 UID → 二维码 → 扫码绑定 → 比对 UID → delivered/REDEEMED;
  在 delivered 分支 best-effort 核销平台券(幂等、失败只 warn,不拖慢买家响应)
- 后台回显链接改按 **order.provider** 判断(抖音用 `claimLinkBase`)

**SQL 守卫**:允许「删除历史迁移」,但强制要求「同时新增兜底迁移」配对
(修改历史迁移仍禁止),并把改动来源换成 `git status --porcelain`(否则未跟踪的新迁移看不见)

`npm run check`:tests 671 / pass 668 / fail 0 / skipped 3(含新增的物品图回退用例)
2026-10-09 16:13:39 +08:00
yml2213 56a8184576 feat(douyin-mashida): 皮肤自动发货(按内部 SKU 分流 + 领取链接 + 买家领取接口)
抖音码速达的皮肤类商品改为「自动发货 + 领取链接」:买家不用输订单号,点链接直接领。

**分流(按内部 SKU)**
- 平台闸门放行 `provider='douyin_mashida' && platform='doudian'`,走**独立分支**
  (原有 91/快手路径一行未动,避免外溢)
- 判据:内部 SKU(`sku_order_list[].code`,即 order_items.sku_code)命中权威表
  `AFFILIATE_DASH_SKU_MAPPING` → 新执行器 `douyin_mashida_skin`;未命中 → `manual_dispatch`
- 皮肤判定与商品图同源(都用那张权威表),保证「能路由 ⇒ 有图」
- 履约路由新增 `internal_sku` 匹配维度(`rules[].internalSku`,NFKC+去空白+小写,exact/contains);
  旧规则缺 `matchField` 时默认 `product_name`,**归一化/优先级/回退顺序完全不变**

**执行器与领取 token**
- 新增 `douyin-mashida-skin-executor`:建 claim token、置 `link_generated/pending_claim`,
  `resolveDeliveryLink` 返回本站路径式领取链接;注册进 registry、planner 分支、
  bootstrap 种子 profile(`抖音皮肤自动发货`);进 `ROUTABLE_EXECUTOR_KEYS`
- token 粒度 = 一个履约单元一个(`claim_tokens.task_id` 唯一);一单多项时
  cert_link/买家消息取 task id 最小的那个并告警

**cert_link 与发消息**
- 皮肤订单且该订单项已有 token 且配了 `claimLinkBase` → `cert_link = claimLinkBase/token`;
  其它情况一律用 `certLink`(老行为不变)
- 领取链接**只认** `claimLinkBase`,绝不回退 `buildClaimUrl`/`orders.claimBaseUrl`
  (那是 hash 形式的打手领取页,语义不同)
- 付款建单+发券成功后按 `messageTemplate`(`{link}/{sku}/{orderNo}/{productTitle}`)
  经 `order.message` 发给买家;best-effort,失败只记 warn

**买家领取接口**
- `GET/POST /api/v1/claim-skin/:token`(限流、公开);详情含内部SKU/引流标题/商品图/
  数量/金额/支付时间/动态表单字段;token 失效给明确 errorCode(404/410/409),不打 500
- 提交账号:先核销平台券再落记录(避免「记账了但券没核销」的资损),已提交幂等

**隐私**
- 买家游戏账号落 `fulfillment_tasks.claim_submission_json`(迁移 096),
  **不进** `work_orders.material_json`(那是发给打手的素材);详情接口不返回券码明文/手机号/地址/open_id
2026-10-09 15:06:43 +08:00
yml2213 de2fb3e008 fix(douyin-mashida): 平台会改写券码 —— 核销改用平台侧券码 + 失败自愈
真实联调踩到的坑(已用真单复现并定位):
`coupons.syncV2` 返回 `10000 成功` **并不代表平台沿用了我们提交的 cert_no**。
实测 3 笔订单里有 2 笔,平台把我们的 DYC 券码换成了它自己生成的券码。
我们却一直拿**自己的**券码去 `coupons.verifyV3`,平台回
`10017 卡券核销失败[isv.business-failed:010021|核销码与订单不匹配]`;
更坑的是这个错误平台要 **~10.5 秒**才返回,正好越过当时的 10 秒超时,
前端只看到 `douyin_mashida_gateway_timeout`,完全看不出真实原因。

修复:
- 新增 `coupons.list` 出网封装;同步成功后按 order_id 回查平台侧 `cert_no`/`cert_id`
  并落库(迁移 095 新增 platform_cert_no / platform_cert_id / platform_status / platform_synced_at)
- 核销时用 `resolveRedeemVerifyCertNo()`:**平台侧券码优先**,取不到才退回我方券码
- 核销失败后自愈 `tryRecoverVerify()`:
  ① 回查平台,若平台侧已核销 → 按对账结果标记本地已核销(修复"平台已核销、我们报失败"的漂移)
  ② 若是券码被平台改过 → 用平台券码**重试一次**
- 回查不到该订单的券 → 标记 sync 失败并告警(暴露"syncV2 成功但平台无券"的幽灵券)
- 默认出网超时 10s → 20s(平台业务错误要 ~10.5s 才返回,10s 会把业务错误伪装成超时)

实测验证:对一笔「平台侧已核销、本地仍是 synced」的订单调确认兑换,
10.7s 后返回 `status=verified`,platform_cert_no 成功回填为平台券码、
platform_status=3,并打出「对账发现平台侧已核销,按平台状态标记本地已核销」告警。
2026-10-09 14:08:55 +08:00
yml2213 72606796b3 feat(douyin-mashida): 抖音码速达最小接入后端(消息建单/券码同步/兑换核销/退款作废)
- 签名:出网 md5(secret+Σ(名+值)+secret) 与推送 md5(secret+data+pub_time+topic+secret)
  两套独立实现,含文档 golden 向量单测(与 91 大写 / affiliate_dash HMAC 都不通用)
- 进单:trade_TradePaid 推送 → 验签 → webhook_events 幂等 → order.orderDetail
  → upsertOrderFromSource(provider=douyin_mashida / platform=doudian)
- 精度:19 位订单号超出 JS 安全整数,JSON.parse 会把 p_id 抹成 ...000;
  故解析后从原始 JSON 文本用正则抓回精确数字(单测同时证明"不修会错")
- 券码:迁移 094 redeem_certificates((provider,order_id) 唯一做发券幂等)
  + DYC 券码生成 + coupons.syncV2 同步(子单号优先、父单兜底)
- 兑换:POST /api/v1/redeem/lookup|confirm,按 orderNo+IP 限流;
  核销成功后 order.message 通知买家(best-effort,失败只记错误日志)
- 退款/取消:作废未核销券;券已核销时打 WARN 资损告警(已消费不可回滚)
- 后台:GET/POST /api/v1/admin/platform-config/douyin-mashida,
  secret 出参置空 + 掩码,掩码回写不覆盖原值
- 消息路由挂在全局 body parser 之前(app.ts),验签必须用 body 原始字符串
2026-10-09 12:00:50 +08:00
yml2213andCursor 250c456a46 fix(consignment): 内部库存履约后不再把买家资料暴露给上一手打手
退回大厅留下的 last_assigned 在状态离开待接单后会重新可见。履约时摘掉上一手,并清掉已经漏出的内部现货工单。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-05 19:18:43 +08:00
yml2213andCursor eae78aae34 fix(consignment): 退库后不再带出上一单买家资料,撤单清掉上一手验收痕迹
现货退回仓库后只留卖家提报,不再挂买家角色、赠送凭证和店铺;工单撤单、退回、抢单时清掉验收与备注,买家下单资料仍保留。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-05 14:55:42 +08:00
yml2213 905236d83a perf(db): 干掉列表/计数热路径上的「整表拼单聚合」,并给数据库留出 1 核
生产实测(2026-10-03,order_site):postgres 在 22.6 小时里烧了 12.2 CPU·小时
(均 0.54 核),却因 2 核配额累计被 cgroup 节流 9,337 秒;top 12 条语句占全部
DB CPU 的 69.4%,其中三条最热的就是「工单列表 / 计数 / 大厅候选集」。

一、查询形状(根因,不是缺索引)
接单大厅那条 COUNT 被调 71,539 次、均 103ms,EXPLAIN(ANALYZE,BUFFERS) 显示
82.8ms 里 35ms 花在「把整张 work_order_shares 聚合一遍再 join」:
Index Only Scan rows=22,987 → GroupAggregate → 最终只用到 11 行;同一条 SQL 里
还有第二遍同样的全局聚合(filter_share_summary)。已改成按工单限定的关联子查询
(走 idx_work_order_shares_work_order):
- buildWorkOrderWhere 的 excludeFilledSharing 过滤条件;
- buildHallCandidateIdsSql 的 candidate_shares。
同参数实测:50.3ms → 2.1ms、35.9ms → 3.8ms、28.4ms → 1.8ms,buffer 10,145 → 187,
三种参数下结果逐行一致(11/11、5/5、11/11)。
有意保留 operational_share_summary 那一处全局聚合:全表统计(103,073 行)时
关联子查询要 103,073 次探测(330ms),一次聚合只要 55ms;已有用例钉住「只允许剩一处」。

二、容器与数据库参数
- POSTGRES_CPUS 2 → 3(4 核机器,同机其它容器实测只用 0.2-0.4 核且几乎不节流);
- max_parallel_workers_per_gather 2 → 1(>1 时一条查询要 leader+2 worker=3 核,必撞配额);
- checkpoint_timeout 300 → 900s、max_wal_size 1GB → 4GB(43 天累计 21,778 次检查点,
  sar 里就是每 20-40 分钟一轮写突发)。

三、迁移 092:热表 autovacuum 调参
可见性图陈旧导致 index-only scan 回表 5,580 次;work_orders/order_items/orders/
kuaishou_industry_vouchers/fulfillment_tasks 死元组 1.9 万-5.3 万、上次清理在前一天
甚至 9/29。把这些表的 scale_factor 0.2→0.05、analyze 0.1→0.02、cost_limit 200→800,
把「攒批猛扫」拆成「小步勤清」,并打开 5s 以上的 autovacuum 日志便于观测。

用例:新增 share-summary-scope.test.ts(钉住热路径不再出现整表聚合、只保留一处且有注释说明),
env-documentation 白名单补 3 个新增部署变量。全量 599 通过,typecheck/lint/check:sql 通过。
2026-10-03 17:21:21 +08:00
yml2213 30ae35d3a5 fix(consignment): 撤销寄售履约回库分支必 500 —— SQL 参数比占位符多一个
线上客服 kf_13 撤销 WO26092609475746 时返回 500 internal_error,
真实库报:bind message supplies 5 parameters, but prepared statement "" requires 4。

根因:解绑现货的两条分支共用一个 5 元参数数组,但回库/取消分支的 SQL 只用到
$1..$4(第 5 个「备注前缀」只有作废分支才用)。于是运营只要选
「退单回库并恢复工单为未指派」或「退单回库并取消工单」就必定 500,
而假 client 的单测与类型检查都看不到这类错。

修复:
- 解绑 SQL 与参数抽到 consignment-revoke-inventory-sql.ts 一起构造,
  回库/取消分支只传 4 个参数,作废分支才多带备注前缀($5);
- 单测锁定「占位符个数 == 参数个数」,覆盖 reopen / archive_inventory /
  已确认加好友自动作废三条分支;
- db 层加参数个数自检(db/sql-arity.ts + db/client.ts):参数比占位符多时
  提前抛出带 SQL 片段的错误,不再让 pg 的 bind 报错裸奔,
  并跳过字符串/注释/dollar-quote 里的 $n,避免误杀。

验证:本地影子库端到端跑通三条分支(工单 100140 + 现货 INV26091917439822),
并复现修复前的 bind 报错;全量单测 588 通过,typecheck/lint/build 均通过。
2026-10-03 16:03:14 +08:00
yml2213 fbdef6ecab perf(worker): 「仅问题单」检索短路,从 pending 表反查而非枚举可见集
问题单页签原先走完整派生表达式:为该打手枚举全部可见单并逐行求值
嵌套 CASE + pending EXISTS(重打手 8,586 单 × 逐行探测 ≈ 233MB 缓冲读),
而问题单结果通常为空——生产实测 worker_order_cancel_requests 与
worker_order_feedbacks 的 pending 行数当前均为 0。

短路形状:
  wo.status = 'problem'
  OR (基础状态 = 'in_progress' AND wo.id IN (两张 pending 表反查))

- 左臂必须写成 sargable 的 wo.status = 'problem':基础状态等于 'problem'
  当且仅当 wo.status = 'problem'(派生分支没有 problem 映射);
- 反查走迁移 058 的两张 pending 专用索引,实测 8 个块;
- 仅当请求状态"只含 problem"时短路,混合状态仍走完整表达式(结果集完整性);
- 顺带把 workerOrderStatusExpression 拆出 workerOrderBaseStatusExpression 复用。

生产 EXPLAIN(worker=7):29,851 块 / 267ms → 13,387 块 / 118ms。
2026-10-03 15:22:57 +08:00
yml2213 c8a4b7e945 fix(worker): 删除依赖残留字段 task_id 的死 JOIN,店铺名兜底改读 material_json
核查结论:work_orders.task_id 是残留字段,不是"数据没同步上来"。
  1. 唯一写入点 createWorkOrder 写的是 input.taskId || null,而两个调用方
     一个硬编码 taskId: null、一个根本不传;updateWorkOrder 的白名单里也没有它;
     git 全历史检索 `taskId:` 只出现过 null 一种取值。
  2. 即便强写也无从关联:工单关联 100,231 个订单、履约任务关联 87,603 个订单,
     交集为 0;按 order_item_id 比对 100,229 条同样 0 命中。而两者时间范围
     (2026-07~10)与 orders.id 区间(1~280k)都重叠 —— 说明是按规则表分流的
     两条并行履约通道,一单只走一条。
  3. 依赖它的 idx_work_orders_task_unique 为空(8KB)。

改动:
  - 三处 `LEFT JOIN fulfillment_tasks ft ON ft.id = wo.task_id`
    (取消申请 / 售后案 / 工单反馈)恒为空,删除;
  - 店铺名兜底 `ft.shop_name/shop_id` 改为读工单自己的
    material_json->'source'->>'shopName'/'shopId';
  - 取消申请里的 `ft.platform_order_id` 一并删除(实测 needs_ft_pid_fallback = 0,纯 no-op);
  - 迁移 091 给列与索引打上注释,避免后续再被当成数据问题重复排查。

生产逐行回归(旧逻辑 vs 新逻辑取差集):
  取消申请 715/715 行、103 条店铺名从空变有值、回归 0;
  售后案 21/21 行、1 条、回归 0;工单反馈 348/348 行、15 条、回归 0;
  合计 119 条修复、0 条回归、行数与 platform_order_id 零变化。

验证:新增源码级守卫(禁止再 JOIN fulfillment_tasks、兜底必须读 material_json)。
2026-10-03 14:27:46 +08:00
yml2213 90de6b9419 perf(worker): 单号检索收敛为可走索引的等值/前缀,并支持关联单号反查
问题:后台「按单号检索」在平台单号/工单号分支里 OR 了一支
`work_order_no ILIKE '%'||$n||'%'`。前置通配符非 sargable,任何 B-Tree 都用不上,
整条查询退化成 work_orders 的 Seq Scan。

生产实测(10.3 万行 / 572MB,pg_stat_statements 46 小时窗口):
  - 列表查询 + 统计查询用同一份 filters 并行执行,一次检索打两遍全表;
    两者合计占全库 66% 的磁盘读(1.1TB/46h),单次读 26,442 个块(约 211MB)、398ms、返回 1 行。
  - 收敛后同库同参数:等值 0.19ms,前缀 30ms(约 2000 倍)。
  - 真实统计查询端到端 760ms → 61.6ms。

改动:
1. 平台单号/工单号分支收敛为「等值 + 前缀」,与 order-repo / task-repo 既有写法一致;
   工单号含随机小写段(10.2 万行里 7.4 万行非全大写),保留 LOWER() 比较。
2. 迁移 090 补 text_pattern_ops 前缀索引:当前库 collation 为 en_US.utf8,
   前缀 LIKE 在默认 B-Tree 上会退化成全索引扫(实测 438ms)。
3. 新增关联单号反查分支:SUB/INV/AS/DT 不在 work_orders 上,
   经关联表反查工单 id,四条链路均命中唯一索引。
4. 前端 isExplicitOrderNoSearch 同步补齐(否则搜到的单会被状态页签挡掉、
   实时补丁还会把行删掉)。

验证:生产 EXPLAIN 逐条比对 + 14 项仓储单测 + 6 项分类器单测。
2026-10-03 14:27:32 +08:00
yml2213 2982819548 fix(worker-risk): 伪造 uid 时风控事件因外键约束写不进去,改为查不到就存 NULL
worker_risk_events.worker_id 有指向 worker_users(id) 的外键。
外挂可以伪造 token 里的 uid(中间件只 base64 解 payload、不验签就以该 uid 建跟踪维度),
那种 uid 在 worker_users 里并不存在,直接写入会触发外键约束失败 ——
结果是**最该留痕的伪造流量反而记不下来**,而且因为留痕是 fire-and-forget,
失败只在日志里留一条,界面上完全看不出来。

生产实测(真实库事务内回滚验证):
  worker_id=41(真实账号)       -> 插入成功
  worker_id=99999999(伪造 uid) -> ERROR: violates foreign key constraint

改为用子查询解析 worker_id,查不到即写 NULL;target_key 仍保留原始 uid,
追溯能力不受影响。修复后实测两种情形均写入成功,伪造者 worker_id 为 NULL。

顺带确认:留痕链路本身是通的(后台 release 接口已实际写入过一条 manual_unbanned)。
2026-10-02 17:09:33 +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 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 21cd9135a2 perf(worker): 「我的订单」列表改成先取 id 再取详情,排序阶段不再物化宽列
打手端我的订单的排序是表达式:
  COALESCE(CASE WHEN wo.assigned_worker_id = $1 THEN wo.assigned_at END,
           CASE WHEN worker_share.status != 'cancelled' THEN worker_share.created_at END)
  DESC NULLS LAST, wo.id DESC
它依赖打手参数与拼单表,**没有任何索引能支撑**,规划器只能把该打手全部可见单
连同宽列(每行约 1.2KB material_json)物化后排序。生产实测(真实 service 路径):
名下 6609 单的打手首访 1112ms、5198 单的 808ms、3140 单的 772ms,每次读约 60MB
(列表缓存 2 秒一失效,所以这个代价每天要重复付很多次)。

改法:该排序走"延迟分页"——第一阶段只 SELECT wo.id(窄投影 + 同一 ORDER BY +
同一 LIMIT/OFFSET),第二阶段用 `WHERE wo.id = ANY($1::bigint[])` 取这一页的详情,
再按 id 顺序还原(ANY 不保证顺序)。行集、顺序、总数语义完全不变。

验证:后端 542 项单测全绿(新增 1 项守卫);生产上先用部署前的 id 顺序留底,
部署后用同一批打手比对首 20 条 id 与总数,并对比首访耗时。
2026-10-01 15:29:49 +08:00
yml2213andCursor 1239393fc2 perf: 筛选统计去掉 JSON 兜底,并补上 orders 覆盖索引
去掉 work_orders 堆访问后,JOIN 会把 orders 整表扫进 Hash Right Join。
领取页在快照新鲜时不再打上游;快手出网统一 10 秒超时。倒计时改为自适应间隔。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-01 15:05:38 +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 9573acc59d fix(consignment): 问题单「退回大厅」也要解绑寄卖现货
问题单处置的「退回大厅」只把订单置回 open,没有解绑被本工单占用的寄卖现货,也没摘 material 上的
寄售标记。后果是卖家端「我的寄卖」把这张单显示成「履约中 + 关联订单状态已变化,请联系客服核对」
(状态机只认 in_progress / problem / pending_acceptance,open 落到兜底分支),而现货永久停在
consumed 占库不还、既不再参与匹配也没有释放入口。

其余同义路径(后台退回大厅、超时重开、打手退单通过)本来都会解绑,只有问题单这条漏了,属口径不一致。
现在把解绑条件从「仅取消」扩到「取消或退回大厅」;「退回打手重做」保持占用(货还在这一单上履约)。
调用位置仍在状态 UPDATE 之前——该 UPDATE 在 open/cancelled 时会清空 friend_added_at,
清完再解绑会把已确认加好友的货误判成可回池(二次出货)。

验证:真实库跑通两条路径——退回打手重做(现货仍 consumed+绑定的、标记保留、打手端仍「已匹配-待加好友」)
与退回大厅(现货回 in_stock、解绑、标记摘除、打手端变「已通过(在库待匹配)」);后端 520 项单测全绿。
2026-10-01 12:22:56 +08:00
yml2213 40ee36d72a fix(consignment): 修复寄卖取消解绑、SUB 单号写空、拼单门禁与后台工单列表实时乱跳
1. 问题单处置「取消」后打手信息不消失:resolveProblemWorkOrder 的取消分支从未解绑寄卖现货,现货永久停在 consumed 占库不还,工单 material 上的寄售标记残留又让列表继续显示「供货打手」。取消时补上解绑;调用必须排在状态 UPDATE 之前——那条 UPDATE 会清空 friend_added_at,清完再解绑会把已确认加好友的货误判成可回池(二次出货);
2. 解绑函数在没有现货可解绑时直接 return,跳过了摘标记,于是「库存行早已不存在」的老帖永远摘不掉寄售标记(列表一直显示供货打手)。现在无论是否解绑到现货都摘标记,痕迹保留在 material.consignmentHistory 的 detachReason 里;
3. 内部现货履约写入的 sourceSubmissionNo 恒为空:取的是占库 `UPDATE ... RETURNING *` 的行,只带回本表列,没有 join 出来的 submission_no,导致工单列表把所有寄卖单都显示成「老单无提报单号」,SUB 单号这个唯一锚点丢失。改取带 join 的预读库存(内部现货资料、指派工单资料、指派事件留痕三处)。存量老单按业务口径不回填;
4. 后台接单工单列表实时乱跳:SSE 快照补丁按数组下标读 queryKey,面板 key 加了字段后下标全错(page 被读成 targetWorkOrderId → 任何页都被当成第 1 页),且完全没判「内部寄售匹配」等筛选,于是任意一条不在当前页的老单只要发生变更就被插到列表顶部。改为结构化 queryKey + 三态判定(match 才允许插顶、mismatch 才允许移除、其余只原地更新),插入还要求 page===1 且工单 id 大于当前首行;状态比对改用 rawStatus(快照的 status 是运营态,拼单母单会被显示成 in_progress),分类筛选因快照不带 parentCategoryId 归入不可证伪;
5. 新增「寄卖只做整单」硬门禁:已有人参与的拼单工单不参与现货匹配与库存提醒,履约入口一律返回 work_order_sharing_not_consignable,前端「手动匹配寄卖」入口按同一口径收敛;开了拼单但无人参与的整单不受影响,避免过度拦截。

验证:后端 520 项单测(517 pass / 3 skip)、前端 78 项全绿;并对真实开发库跑通问题单取消三类场景(老帖无库存行 / 回库 / 已加好友作废)、退回打手与退回大厅的反向用例(不得解绑)、履约写入真实 SUB 单号、拼单门禁四入口 + 未过度拦截反向用例。
2026-10-01 12:17:00 +08:00
yml2213 1636616ec1 feat(worker-platform): 寄卖价格改由接单模板只读定价,我的寄卖支持服务端搜索与分页
1. 寄售价不再由卖家填写:打手端改为只读展示模板价(与履约押金同款只读框),服务端在发布时按同一套规则复算金额,保证展示口径与落库口径一致,直连接口也无法随意定价;
2. 模板缺档按「最接近的已配置档位」报价、距离相同取更低档:后台只配了 1/3/7 钻时,号主出 2 钻报 1 钻价、出 6 钻报 7 钻价,不再因为中间档没配价而拿不到价格;
3. 我的寄卖列表把分组、关键字、翻页全部下沉到 SQL:新增 listWorkerConsignmentPage 用 UNION ALL 合成「提报单行 + 无提报单库存兜底行」,CASE 推导 待审核/履约中/已完成 分组,返回当前分组 total 与三个分组的角标计数;
4. 关键字覆盖寄卖单号、规格名、系统/区服/系列/钻级、卖家角色名与数字 ID、联系方式、买家昵称与游戏 ID(读取 work_orders.material_json.collect.fields 的多种历史键名);
5. 前端搜索框 300ms 防抖后才请求服务端、换分组或换关键字自动回到第 1 页、列表底部增加分页器,并移除原来固定 pageSize 200 的隐形截断;
6. 新增仓库层与匹配层单测,其中包含 SQL 分组 CASE 与 domain/consignment-item-stage.ts 的 group 映射交叉断言,防止两处状态机漂移;后端 516 项、前端 66 项测试全部通过。
2026-10-01 11:07:28 +08:00
yml2213 cb8b249e57 feat(worker-platform): 实现打手寄卖白名单权限控制与存量发布打手平滑过渡
1. 新增数据库迁移 082_worker_consignment_whitelist.sql,worker_users 表增加 can_consign 与 consignment_activated_at 字段,存量在 consignment_submissions 提报过的打手默认自动开启;
2. 打手端提报接口 publishWorkerConsignment 增加白名单权限服务端强校验拦截(403 consignment_permission_required);
3. 管理后台打手管理表格增加「寄卖权限」实时 Switch 开关列与打手详情抽屉权限展示,支持客服即时开通或关闭打手寄卖权限;
4. 打手端我要寄卖页面在未开通权限时展示专属白名单引导卡片,一键呼起联系客服开通寄卖弹窗;
5. 补充寄卖权限拦截与路由单测,前端59项单测与后端507项单测全部通过。
2026-09-21 13:30:33 +08:00
yml2213 c115f5e944 fix(claim): 修复后台手动关闭任务后领取链接未失效的缺陷
1. 鉴权层激活 CLAIM_TERMINAL_STATUSES:在 getClaimContext 中增加 task 状态校验,对 closed 与 expired 终态任务实施 410 拦截并自愈更新关联 Token;
2. 仓储层新增 closeClaimTokensByTaskId():支持按 taskId 批量更新所有非 closed 状态的 Token 为 closed,彻底杜绝多 Token 或重发链接导致的历史活跃残留;
3. 后台关闭任务流程闭环:closeAdminTask 显式触发 closeClaimTokensByTaskId,确保任务关闭时关联领取链接立即失效;
4. 补充 CLAIM_TERMINAL_STATUSES 单元测试与完整排查分析方案文档。
2026-09-20 14:06:48 +08:00
yml2213 f07e4087ad feat(consignment): 优化寄售仓库与接单工单双向手动匹配,支持工单号与提报单号检索与强制匹配
1. 寄售仓库手动匹配订单扩充支持 open 状态工单,增强 WO 工单号与纯数字的模糊/前缀搜索,修复工单搜不到的问题并在卡片高亮展示内部工单号与状态;
2. 接单工单操作列全面支持【手动匹配寄售】,允许从接单工单主动调起寄售匹配弹窗;
3. 寄售现货筛选支持通过 SUB 提报单号、INV 仓库号、供货打手等快速搜索;
4. 完善规格冲突下的强制匹配确认与必填原因提交机制,确保履约全流程留痕合规。
2026-09-19 17:59:05 +08:00
yml2213 2c120bd3e5 fix(worker-platform): 接单平台工单详情抽屉完整展示寄售手动匹配与指派订单的卖家资料
1. 修复管理后台「接单平台 -> 接单工单」抽屉中,手动匹配或指派寄售库存履约的工单因 booster_material_json 为空导致卖家资料全部显示 '-' 的问题;
2. 后端在履约及指派时将卖家角色昵称、数字ID、联系方式、手机号、备注及上架凭证截图完整写入工单 material.consignment;
3. 后端工单查询及展示映射层增加自动回退与批量库存补充解析机制,确保历史与新增的寄售工单均能准确解析出卖家角色名、ID、区服、系统与截图;
4. 前端工单抽屉卡片优化寄售订单的资料展示与阶段标签,清晰展示卖家角色名、游戏ID、联系方式(微信/QQ/手机)、提报/仓库单号及上架截图。
2026-09-19 14:33:56 +08:00
yml2213 0b4ee964ed feat(pdd): 引入拼多多原始订单暂存仓 pdd_raw_orders 与两段式解耦流水线
1. 数据库新增 081_pdd_raw_orders.sql 迁移,创建 pdd_raw_orders 原始暂存表及索引;
2. 新增 pdd-raw-order-repo 仓储,支持原汁原味批量暂存、pending 单据消费、状态回填与重置;
3. 重构 pdd-sync-service 为采集暂存(Staging)与业务进单(Ingestion)两段式解耦流水线,实现断点续传;
4. 管理后台路由接入 raw-orders 原始流水查询及 replay 一键历史单重放 API。
2026-09-19 11:58:28 +08:00
yml2213 5e59a5fdb1 fix(order-service): 修复包含 Emoji 和孤立代理字符的订单在 PostgreSQL jsonb 下报错导致死循环重试进单的问题
1. 在 utils/json.ts 新增 sanitizeWellFormedUnicode、deepSanitizeUnicode 和 safeJsonStringify,自动清理空字节 (\0) 并将畸变孤立 UTF-16 代理字符 (Lone Surrogates) 安全转换为规范字符,杜绝 PostgreSQL jsonb 解析错误;
2. 拼多多订单映射器与通用进单服务 (order-service) 全面接入 Unicode 深度清洗与安全序列化,确保买家昵称/留言中的各类特殊 Emoji 和生僻字符平滑落库;
3. 数据库仓储层 (order-repo / order-item-repo) 针对 raw_payload_json、spec_json、item_snapshot_json 等 jsonb 字段增加兜底安全序列化保护;
4. 补充 utils/json.test.ts 与 pdd-order-mapper.test.ts 单元测试,覆盖畸变字符与空字节场景。
2026-09-19 11:40:02 +08:00
yml2213 439888631f fix(worker-platform): 内部寄售现货履约发单金额对齐寄卖成本价并补充留痕追溯
1. 内部现货履约时原子更新工单 reward_amount 为寄卖结算成本价,解决工单列表显示旧规则价与寄卖成本冲突问题;
2. 工单资料 material.consignment 与工单事件 flow 补充 previousRewardAmountFen/updatedRewardAmountFen/costPriceFen 等价格变更留痕;
3. 撤销寄售履约且退回未指派状态时支持自动还原原工单发单金额并在撤销事件中记录还原明细;
4. 新增 consignment-fulfillment-price-trace.test.ts 专项单测并通过全量测试回归。
2026-09-19 10:22:09 +08:00
yml2213 d6523635ea fix(worker-platform): 优化工单号精准检索与跨状态流转展示,修复状态更新后工单消失问题
1. 后端查询重构:新增 isWorkOrderNoLookupKeyword 工单号前缀与忽略大小写精准索引匹配,优化后端 work-order-query-repo 的工单号检索路径;
2. 前端单号智能穿透:管理员输入 WO... 等精准单号查询时,自动放宽细粒度排他状态/阶段过滤,避免加好友/验收等状态流转后工单因旧状态筛选脱靶消失;
3. 搜索栏交互升级:输入框清空即时重置关键词与页码,支持回车/按钮/清除全方位无缝触发;
4. 详情抽屉与多面板联动:增加 ordersQuery 数据更新对 detailOrder 的自动同步,并将 refreshAll 广播范围覆盖至寄售仓库与统计指标。
2026-09-18 19:36:17 +08:00
yml2213 c17b8a78d5 fix(worker-consignment): 修复卖家提交赠送截图后在接单平台无法回显的问题
1. 后端 listWorkerConsignments 接口从关联工单验收资料中提取 giftEvidenceImages、giftEvidenceNote、giftEvidenceSubmittedAt 并在打手端寄卖列表中下发;
2. 接单平台「我的寄卖」卡片新增已提交赠送凭证展示模块,支持展示缩略图、张数统计、大图预览及赠送备注;
3. 优化提报单与库存、工单的 SQL 关联兼容双向绑定,确保后台与前台均能稳定查到赠送凭证。
2026-09-18 18:35:15 +08:00
yml2213 c5d1e2c375 feat(consignment): 寄售匹配成功后向卖家端与客服后台透出买家加好友资料 2026-09-17 20:46:45 +08:00
yml2213 103299a7c9 feat(consignment): 发布寄卖自动从接单模板带出建议价
- 新纯函数 suggestConsignmentPriceFromRules:模板商品名(安卓暗潮1钻)用与
  订单同一套规格解析器拆解,系统+系列+钻级全对上才命中;系统按通配口径
  (模板写通用/双端两端都算,模板没写系统不命中——宁可不建议不给错价);
  多条命中取最高价
- 新端点 GET /worker/consignment/price-suggestion(requireActiveWorker)
- 发布表单规格选齐后 300ms 防抖请求:同规格去重、过期响应丢弃;命中自动
  填入寄售价并提示来源与条数;卖家手改过价只提示不覆盖,换规格重新带出;
  提交成功状态复位;接口失败静默(本地没配模板保持手填,行为不变)
- 建议:本地模板没配不影响任何现有行为,生产库模板越多建议越准

后端 483(+建议价命中/不符排除/取最高/通用模板用例)/ 前端 54 测试全绿
docs/寄卖系统优化方案.md 记录批次 X
2026-09-17 13:14:10 +08:00
yml2213 b666a9c951 fix(consignment): 拼单份额验收收官补寄卖结算钩子 + 手动匹配弹窗三处复核遗留
- acceptWorkOrderShareAndSettle 最后一个份额把母单转 accepted 的分支,
  此前不释放发布押金也不结算供货货款(两钩子只在统一验收里):
  现在同口径补上;供货货款传 assignedWorkerId=0(拼单母单无单一接单人,
  份额打手已各自按 share_reward 结算,不会重复付款)
- 手动匹配弹窗:race_condition/unavailable/spec_mismatch/partial_requires_confirm
  四个错误码清空已选并自动重拉列表,数据过期不再死循环撞错
- 手动匹配弹窗库存侧:keyword+pageSize100 透传后端(原来 keyword 是死参数、
  只拉 50 条),300ms 防抖后端搜索;无供货打手时禁用指派选项并在提交前
  再拦一道(原来必 400 死路);选中订单回显 workerId/workerName
- 凭证核销反馈抽共享 utils/voucher-consume,WorkOrdersPanel 与弹窗共用,
  口径不再漂移(弹窗顺带获得"已提前核销 N 张"提示)
- docs/寄卖系统优化方案.md 记录批次 V

后端 477(+4)/ 前端 50(+1)测试全绿,typecheck / lint / format / check:sql 通过
2026-09-17 12:45:51 +08:00
yml2213 63837e473b fix(consignment): 复核修复——占库撞 080 CHECK 必 500、指派非卖家双付货款、手动匹配权限门控
- 占库默认 allocation_status 从自由文本改为「已安排」,不再撞 080 的
  CHECK ('待安排','已安排');加静态守卫测试(剥注释断言枚举内默认值)
- resolveConsignmentAssignRewardFen 加 assigneeIsSupplier:指派供货卖家本人
  → 报酬=结算价(货款走报酬渠道);指派别的打手 → 报酬保持原值(货款走
  供货货款渠道),两条渠道互斥,封住平台双付;补行为级测试 + 调用点钉住
- 手动匹配两处入口(库存面板行内按钮、拦截弹窗 footer)统一按
  consignment.fulfill 门控,编辑/作废仍为 manage,与后端权限一一对应
- docs/寄卖系统优化方案.md 记录批次 U

后端 473(+3)/ 前端 49(+1)测试全绿,typecheck / lint / format / check:sql / build 通过
2026-09-17 12:36:12 +08:00