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
@@ -21,6 +21,13 @@ type UseRealtimeEventsOptions = {
token: string
onEvent: (event: RealtimeEvent) => void
onConnected?: () => void
/**
* 服务端告知「补发窗口溢出,历史事件已不连续」。
*
* 此时不能再依赖逐条快照补丁(会把列表写回旧值),调用方应整体失效对应查询,
* 让列表接口重新给出权威数据。
*/
onResync?: (info: { headEventId: number; reason: string }) => void
/** 鉴权失效(401/403)时触发:连接停止重连,调用方应清理会话并跳转登录页。 */
onAuthExpired?: () => void
}
@@ -33,13 +40,16 @@ export function useRealtimeEvents({
token,
onEvent,
onConnected,
onResync,
onAuthExpired,
}: UseRealtimeEventsOptions) {
const onEventRef = useRef(onEvent)
const onConnectedRef = useRef(onConnected)
const onResyncRef = useRef(onResync)
const onAuthExpiredRef = useRef(onAuthExpired)
onEventRef.current = onEvent
onConnectedRef.current = onConnected
onResyncRef.current = onResync
onAuthExpiredRef.current = onAuthExpired
useEffect(() => {
@@ -79,6 +89,15 @@ export function useRealtimeEvents({
}
onConnectedRef.current?.()
await readSseStream(response.body, (eventName, data) => {
// 补发窗口溢出:历史事件不连续,不能逐条复位快照,交给调用方整体重拉。
if (eventName === 'resync') {
const notice = normalizeResyncNotice(data)
if (notice) {
lastEventId = Math.max(lastEventId, notice.headEventId)
onResyncRef.current?.(notice)
}
return
}
if (eventName !== 'realtime') return
const event = normalizeRealtimeEvent(data)
if (event) {
@@ -197,3 +216,16 @@ function normalizeRealtimeEvent(value: string): RealtimeEvent | null {
return null
}
}
/** 服务端 resync 通知:补发窗口溢出,客户端应把游标推进到 headEventId 并整体重拉。 */
function normalizeResyncNotice(value: string): { headEventId: number; reason: string } | null {
try {
const parsed = JSON.parse(value) as { headEventId?: unknown; reason?: unknown }
return {
headEventId: Math.max(0, Number(parsed.headEventId) || 0),
reason: String(parsed.reason || ''),
}
} catch {
return null
}
}