Merge branch 'dev1023' into dev

解决 9 个冲突文件,并修复合并后遗留问题:

- 大厅候选集:采用 FIFO 窗口 + 全部置顶单 UNION(列表与抢单/拼单复核同源)
- 拼单份额:采用多次参与模型(每次参与 INSERT 新份额、requestId 幂等),
  取消后可重新参与;移除 HEAD 已废弃的单行 rejoin 逻辑与对应测试
- 工单迁移与成本中心 103–108 编号撞车:合并重写为
  109_work_order_share_participation / 110_work_order_hall_and_shop_indexes;
  临时库全量执行与重跑幂等验证通过(10 列、唯一键已删、7 个索引)
- 实时补丁:保留个性化字段、置顶重排、分页总数同步;补回 orderTimelineVisible 类型
- 打手大厅:保留 memo 组件化,移植「再拼一次」多份额按钮逻辑
- 全项目表格统一 PercentTable:WorkerOrderShareTable 改造

验证:backend typecheck/lint/check:sql/test(833 pass)、
frontend typecheck/lint/test(172 pass)/build 全绿。
This commit is contained in:
yml2213
2026-10-26 12:10:53 +08:00
94 changed files with 8991 additions and 744 deletions
@@ -0,0 +1,101 @@
-- 109_work_order_share_participation.sql
-- 工单拼单份额模型:多次参与 + 份额级问题标记 + 撤单原因/份数。
-- (由原 110/111/113/114 四个增量迁移合并重写,纯新增列/索引,不改历史结构。)
--
-- 目标:
-- 1. 同一打手可对同一拼单工单多次参与,每次参与一条 work_order_shares;
-- 每份在参与时固化截止时间与超时策略,超时 / 验收 / 撤单按份额独立。
-- 2. 后台可对单个份额标记问题单:只影响该份额持有人,母单状态与其他参与者不变。
-- 3. 撤单 / 撤份额的原因与份数落库,列表可直接展示「撤 N 份 · 原因」。
--
-- 兼容性:
-- - 只加列 + 回填 + 删「一人一条」唯一键 + 建索引,不拆分历史行、不改历史资金;
-- - 历史合并份额(quantity > 1)保持原样,结算公式 quantity × unit_reward 完全兼容;
-- - DDL 全部幂等(IF NOT EXISTS / 受控 DROP),可安全重跑。
ALTER TABLE work_order_shares
-- 参与瞬间固化的时限(母单后续改配置不影响这一份)
ADD COLUMN IF NOT EXISTS deadline_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS timeout_minutes INTEGER NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS timeout_policy TEXT NOT NULL DEFAULT '',
-- 取消来源枚举:timeout / worker_request / admin / problem / settlement
ADD COLUMN IF NOT EXISTS cancelled_reason TEXT NOT NULL DEFAULT '',
-- 客户端幂等键(一次点击一个);唯一索引只约束非空值
ADD COLUMN IF NOT EXISTS request_id TEXT NOT NULL DEFAULT '',
-- 份额级问题标记(与份额状态正交;problem_marked_at 为 NULL = 正常)
ADD COLUMN IF NOT EXISTS problem_note TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS problem_marked_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS problem_marked_by TEXT NOT NULL DEFAULT '',
-- 后台撤单 / 撤份额:原因只做展示;份数 0 = 从未撤过
ADD COLUMN IF NOT EXISTS cancel_note TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS cancel_quantity INTEGER NOT NULL DEFAULT 0;
COMMENT ON COLUMN work_order_shares.deadline_at IS
'本份额截止时间:参与时 = created_at + 母单当时 timeout_minutes;0 分钟为 NULL';
COMMENT ON COLUMN work_order_shares.timeout_minutes IS
'参与时固化的时限(分钟),母单后续改配置不影响';
COMMENT ON COLUMN work_order_shares.timeout_policy IS
'参与时固化的超时策略 reopen/cancel_release/cancel_deduct';
COMMENT ON COLUMN work_order_shares.cancelled_reason IS
'取消来源:timeout/worker_request/admin/problem/settlement';
COMMENT ON COLUMN work_order_shares.request_id IS
'客户端幂等键(一次点击一个),重复请求返回原份额';
COMMENT ON COLUMN work_order_shares.problem_note IS
'份额级问题备注(后台标记问题时填写,选填)';
COMMENT ON COLUMN work_order_shares.problem_marked_at IS
'份额级问题标记时间;NULL = 正常。仅影响该份额持有人展示';
COMMENT ON COLUMN work_order_shares.problem_marked_by IS
'标记操作人(后台账号显示名)';
COMMENT ON COLUMN work_order_shares.cancel_note IS
'后台撤单/撤份额时填写的原因(自由文本,选填),仅用于展示';
COMMENT ON COLUMN work_order_shares.cancel_quantity IS
'被撤掉的份数(撤份额累计 / 单人撤单为整份份数;0 = 未撤过),仅用于展示';
-- 回填历史份额:按当时母单配置一次性固化(只碰尚未固化的历史行)。
UPDATE work_order_shares s
SET timeout_minutes = COALESCE(w.timeout_minutes, 0),
timeout_policy = COALESCE(NULLIF(w.timeout_policy, ''), 'reopen'),
deadline_at = CASE
WHEN COALESCE(w.timeout_minutes, 0) > 0
THEN s.created_at + (w.timeout_minutes || ' minutes')::interval
ELSE NULL
END
FROM work_orders w
WHERE w.id = s.work_order_id
AND s.timeout_minutes = 0;
-- 删除「同一工单同一打手只能一条」约束(否则第二次参与报 23505)。
-- 取消的历史份额保留用于追溯,重新参与直接新增一条。
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'work_order_shares_work_order_id_worker_id_key'
) THEN
ALTER TABLE work_order_shares
DROP CONSTRAINT work_order_shares_work_order_id_worker_id_key;
END IF;
END $$;
-- 到期扫描:只覆盖 joined 份额,走部分索引。
CREATE INDEX IF NOT EXISTS idx_work_order_shares_due
ON work_order_shares (deadline_at)
WHERE status = 'joined';
-- 打手在某工单的份额列表(我的订单展开、押金累计求和)。
CREATE INDEX IF NOT EXISTS idx_work_order_shares_worker_order
ON work_order_shares (worker_id, work_order_id, created_at DESC);
-- 母单份额集合推导(recompute 母单状态、剩余份数)。
CREATE INDEX IF NOT EXISTS idx_work_order_shares_order_status
ON work_order_shares (work_order_id, status, created_at ASC);
-- 幂等键:同一打手同一个 request_id 只能产生一条份额。
CREATE UNIQUE INDEX IF NOT EXISTS uq_work_order_shares_worker_request
ON work_order_shares (worker_id, request_id)
WHERE request_id <> '';
-- 打手「问题单」页签筛选与概览统计:按 (worker_id, work_order_id) 探测被标记的份额。
CREATE INDEX IF NOT EXISTS idx_work_order_shares_problem_worker
ON work_order_shares (worker_id, work_order_id)
WHERE problem_marked_at IS NOT NULL;
@@ -0,0 +1,25 @@
-- 110_work_order_hall_and_shop_indexes.sql
-- 工单相关热查询的部分索引(由原 103/106 两个索引迁移合并重写)。
--
-- 两处查询都已经有明确的 SQL 口径,这里只补与口径完全一致的索引:
-- 1. 后台接单工单「按店铺筛选」:BTRIM(o.shop_name) = ANY($n::text[]),
-- 普通 btree 无法命中表达式,必须建同表达式索引;
-- 2. 抢单大厅候选集:FIFO 窗口之外还要无条件并入全部置顶单,
-- 置顶单通常只有个位数,用「open 且 pinned_at 非空」的部分索引即可。
-- 全部 IF NOT EXISTS,可安全重跑。
-- 后台「接单工单 → 店铺」筛选(可多选):表达式索引 + 部分索引省掉空店铺条目。
CREATE INDEX IF NOT EXISTS idx_orders_shop_name_trimmed
ON orders (BTRIM(shop_name))
WHERE BTRIM(shop_name) <> '';
COMMENT ON INDEX idx_orders_shop_name_trimmed IS
'支持后台接单工单按订单店铺(orders.shop_name)精确多选筛选';
-- 抢单大厅置顶候选集:只索引处于 open 且已置顶的工单 id,避免扫全表。
CREATE INDEX IF NOT EXISTS idx_work_orders_hall_pinned
ON work_orders (id)
WHERE status = 'open' AND pinned_at IS NOT NULL;
COMMENT ON INDEX idx_work_orders_hall_pinned IS
'抢单大厅置顶候选集:open 且 pinned_at 非空的工单 id';