From 7743d2019c2be5d592804a6f64824101b7249de5 Mon Sep 17 00:00:00 2001 From: zhaoxing <2558691827@qq.com> Date: Mon, 26 Oct 2026 16:36:26 +0800 Subject: [PATCH] =?UTF-8?q?fix:=E4=BC=98=E5=8C=96=E5=86=8D=E6=8B=BC?= =?UTF-8?q?=E4=B8=80=E6=AC=A1bug,=E4=BC=98=E5=8C=96=E6=89=93=E6=89=8B?= =?UTF-8?q?=E7=AB=AF=E7=A7=BB=E5=8A=A8=E7=AB=AF=E6=8B=BC=E5=8D=95=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../realtime/realtime-event-service.ts | 25 ++- .../src/services/worker-platform/mappers.ts | 1 + .../work-order-display-mappers.test.ts | 16 ++ .../work-order-display-mappers.ts | 16 ++ apps/frontend/src/layouts/WorkerLayout.tsx | 19 +++ .../src/pages/worker/WorkerHallPage.tsx | 26 ++- .../pages/worker/WorkerOrderMobileList.tsx | 26 ++- .../pages/worker/WorkerOrderShareTable.tsx | 156 +++++++++++++----- .../src/pages/worker/WorkerOrdersPage.tsx | 1 + apps/frontend/src/styles/worker.css | 153 ++++++++++++++++- .../src/utils/worker-realtime-work-orders.ts | 41 ++++- .../tests/worker-realtime-work-orders.test.ts | 51 ++++++ 12 files changed, 465 insertions(+), 66 deletions(-) diff --git a/apps/backend/src/services/realtime/realtime-event-service.ts b/apps/backend/src/services/realtime/realtime-event-service.ts index b7ce85fc..afb5ed04 100644 --- a/apps/backend/src/services/realtime/realtime-event-service.ts +++ b/apps/backend/src/services/realtime/realtime-event-service.ts @@ -36,6 +36,7 @@ import { pickWorkerRepresentativeShare, resolveWorkerOrderStatusTabs, resolveWorkerPermissions, + stripWorkerPersonalShareFields, } from '../worker-platform/mappers.js' import { invalidateWorkerMyOrderCaches } from '../worker-platform/worker-my-order-cache.js' @@ -347,21 +348,27 @@ async function publishWorkOrderSnapshots(input: { Number(workOrder.sharing_total_quantity || 1), joinedQuantity, ) - publishRealtimeEvent({ - type: 'work_order.changed', - entityId: workOrderId, - scopes: ['worker_hall'], - operation: hallVisible ? 'upsert' : 'remove', - data: hallVisible - ? mapWorkOrderForHall( + // 大厅广播是「所有打手通用」的快照:myShare/myShares 是每个打手自己的份额, + // 必须整键剔除,不能带 null/[] 过去。前端补丁对广播行做浅合并,带空值的 + // 个人字段会把刚拼单成功写进缓存的份额冲掉,按钮从「再拼一次」变回「拼单」。 + const hallSnapshot = hallVisible + ? stripWorkerPersonalShareFields( + mapWorkOrderForHall( workOrder, { depositFreeAmount: 0 }, { // 广播无法按打手等级过滤可见延迟,携带入厅时间由客户端按各自延迟推迟展示。 sharingProgress: { joinedQuantity, pendingSubmissionCount }, }, - ) - : { workOrderId }, + ), + ) + : null + publishRealtimeEvent({ + type: 'work_order.changed', + entityId: workOrderId, + scopes: ['worker_hall'], + operation: hallVisible ? 'upsert' : 'remove', + data: hallSnapshot || { workOrderId }, broadcastWorkers: true, }) } diff --git a/apps/backend/src/services/worker-platform/mappers.ts b/apps/backend/src/services/worker-platform/mappers.ts index 697729fe..4cb76d59 100644 --- a/apps/backend/src/services/worker-platform/mappers.ts +++ b/apps/backend/src/services/worker-platform/mappers.ts @@ -98,6 +98,7 @@ export { resolveWorkOrderOperationalStatus, resolveWorkerShareProductName, resolveWorkerShareStatus, + stripWorkerPersonalShareFields, } from './work-order-display-mappers.js' export { diff --git a/apps/backend/src/services/worker-platform/work-order-display-mappers.test.ts b/apps/backend/src/services/worker-platform/work-order-display-mappers.test.ts index b592aa2c..9c7c5f7c 100644 --- a/apps/backend/src/services/worker-platform/work-order-display-mappers.test.ts +++ b/apps/backend/src/services/worker-platform/work-order-display-mappers.test.ts @@ -6,6 +6,7 @@ import { isWorkerOrderVipEvidencePending, pickWorkerRepresentativeShare, resolveWorkerOrderStatusTabs, + stripWorkerPersonalShareFields, summarizeWorkerShares, } from './work-order-display-mappers.js' @@ -248,3 +249,18 @@ test('份额级问题单:已验收份额被标记后只归问题单,母单 }) assert.deepEqual(tabs, ['problem']) }) + +test('大厅广播快照必须剔除打手私有份额字段', () => { + const snapshot = stripWorkerPersonalShareFields({ + workOrderId: 1, + myShare: null, + myShares: [], + sharingProgress: { joinedQuantity: 2, pendingSubmissionCount: 1 }, + }) + // 前端对广播行做浅合并:带 null/[] 过去会把缓存里的个人份额冲掉, + // 表现为拼单成功后按钮变回「拼单 ¥x/份」。 + assert.equal('myShare' in snapshot, false) + assert.equal('myShares' in snapshot, false) + assert.deepEqual(snapshot.sharingProgress, { joinedQuantity: 2, pendingSubmissionCount: 1 }) + assert.equal(snapshot.workOrderId, 1) +}) diff --git a/apps/backend/src/services/worker-platform/work-order-display-mappers.ts b/apps/backend/src/services/worker-platform/work-order-display-mappers.ts index 318f70df..c267a924 100644 --- a/apps/backend/src/services/worker-platform/work-order-display-mappers.ts +++ b/apps/backend/src/services/worker-platform/work-order-display-mappers.ts @@ -605,6 +605,22 @@ export function mapWorkOrderForWorker( } } +/** + * 大厅广播快照必须剔除打手私有字段。 + * + * 大厅广播是发给所有打手的通用快照,`myShare`/`myShares` 只有本人可见。 + * 若原样带 `null`/`[]` 过去,前端 `patchWorkerWorkOrderListEnvelope` 的浅合并 + * 会用空值覆盖行上已有的个人份额,按钮从「再拼一次」变回「拼单 ¥x/份」。 + */ +export function stripWorkerPersonalShareFields( + snapshot: T, +): Record { + const result: Record = { ...snapshot } + delete result.myShare + delete result.myShares + return result +} + /** * 抢单大厅专用轻量映射。 * diff --git a/apps/frontend/src/layouts/WorkerLayout.tsx b/apps/frontend/src/layouts/WorkerLayout.tsx index 40752692..61126c6d 100644 --- a/apps/frontend/src/layouts/WorkerLayout.tsx +++ b/apps/frontend/src/layouts/WorkerLayout.tsx @@ -18,6 +18,7 @@ import { isExplicitOrderNoSearch } from '@/utils/admin-realtime-work-orders' import { compareWorkerHallOrderRows, evaluateWorkerKeywordVerdict, + patchWorkerHallPersonalShares, patchWorkerWorkOrderInfiniteEnvelope, patchWorkerWorkOrderListEnvelope, resolveWorkerMyOrderRealtimeVerdict, @@ -324,6 +325,24 @@ function applyWorkerRealtimeEvent( const data = event.data if (event.scopes.includes('worker_my_orders')) { patchWorkerMyOrderCaches(queryClient, { ...event, data }) + // 大厅行上的个人份额只信任「我的订单」快照:大厅广播是通用快照,已剔除这些字段。 + // 否则后台撤掉我的份额后,大厅按钮会一直停在「再拼一次」。 + const hallWorkOrderId = Number(data.workOrderId) || Number(event.entityId) || 0 + if (hallWorkOrderId > 0 && 'myShare' in data && 'myShares' in data) { + for (const [queryKey, current] of queryClient.getQueriesData({ + queryKey: ['worker-hall-orders'], + })) { + queryClient.setQueryData( + queryKey, + patchWorkerHallPersonalShares( + current, + hallWorkOrderId, + data.myShare ?? null, + data.myShares ?? [], + ), + ) + } + } void queryClient.invalidateQueries({ queryKey: ['worker-after-sales-cases'] }) } if (event.scopes.includes('worker_hall')) { diff --git a/apps/frontend/src/pages/worker/WorkerHallPage.tsx b/apps/frontend/src/pages/worker/WorkerHallPage.tsx index 1fff52a8..ab4e9a7d 100644 --- a/apps/frontend/src/pages/worker/WorkerHallPage.tsx +++ b/apps/frontend/src/pages/worker/WorkerHallPage.tsx @@ -51,6 +51,7 @@ import { import type { WorkCategory, WorkOrder, + WorkOrderShare, WorkerLeaderboardPeriod, WorkerUser, } from '@/types/worker-platform' @@ -361,17 +362,27 @@ export default function WorkerHallPage() { requestId: createRequestId('share'), }) message.success('拼单成功,已进入我的订单') + const joinedShare = result.data?.share const currentProgress = sharingOrder.sharingProgress + // 本地即时反馈:把刚拿到的份额合并进行上的 myShares,按钮据此切成「再拼一次」。 + // 只写 myShare 虽然也能让按钮变一次,但连续拼单时「已参与 N 次」会一直停在 1, + // 且后端广播不带个人份额,必须在本地维护完整列表。 + const previousShares = resolveHallMyShares(sharingOrder) + const joinedQuantity = Number(joinedShare?.quantity || sharingQuantity) + const nextShares = joinedShare + ? [...previousShares.filter((share) => share.shareId !== joinedShare.shareId), joinedShare] + : previousShares queryClient.setQueriesData({ queryKey: ['worker-hall-orders'] }, (current: unknown) => patchWorkerWorkOrderInfiniteEnvelope(current, { operation: 'upsert', entityId: sharingOrder.workOrderId, data: { ...sharingOrder, - myShare: { quantity: Number(result.data.share?.quantity || sharingQuantity) }, + myShare: joinedShare || sharingOrder.myShare || null, + myShares: nextShares, sharingProgress: { - joinedQuantity: - Number(currentProgress?.joinedQuantity || 0) + Number(sharingQuantity || 0), + ...currentProgress, + joinedQuantity: Number(currentProgress?.joinedQuantity || 0) + joinedQuantity, pendingSubmissionCount: Number(currentProgress?.pendingSubmissionCount || 0) + 1, }, }, @@ -1163,13 +1174,18 @@ type HallCardProps = { } /** - * 我在该工单的份额摘要:参与次数 + 累计份数。 + * 我在该工单的全部有效份额。 * * 同一打手在同一工单可以多次拼单,`myShares` 是全部有效份额;旧数据只有 `myShare`。 * 大厅按钮据此固定显示「再拼一次」,不再随参与次数改文案。 */ +function resolveHallMyShares(order: WorkOrder): WorkOrderShare[] { + if (order.myShares?.length) return order.myShares + return order.myShare ? [order.myShare] : [] +} + function resolveHallMyShareSummary(order: WorkOrder): { count: number; quantity: number } { - const shares = order.myShares?.length ? order.myShares : order.myShare ? [order.myShare] : [] + const shares = resolveHallMyShares(order) return { count: shares.length, quantity: shares.reduce((total, share) => total + Number(share.quantity || 0), 0), diff --git a/apps/frontend/src/pages/worker/WorkerOrderMobileList.tsx b/apps/frontend/src/pages/worker/WorkerOrderMobileList.tsx index 1e090dee..139da7fd 100644 --- a/apps/frontend/src/pages/worker/WorkerOrderMobileList.tsx +++ b/apps/frontend/src/pages/worker/WorkerOrderMobileList.tsx @@ -1,5 +1,6 @@ import { CopyOutlined, + DownOutlined, EditOutlined, EyeOutlined, MessageOutlined, @@ -8,6 +9,7 @@ import { SearchOutlined, } from '@ant-design/icons' import { Alert, Button, Empty, Input, Pagination, Spin, Tag, Typography } from 'antd' +import { useState } from 'react' import type { ReactNode } from 'react' import { DeadlineCountdown } from '@/components/DeadlineCountdown' @@ -238,6 +240,9 @@ function WorkerOrderMobileCard({ | 'onRefresh' | 'onPageChange' > & { order: WorkOrder }) { + /** 拼单明细默认折叠,点标题行展开;单份订单不显示该区块。 */ + const [sharesExpanded, setSharesExpanded] = useState(false) + const shareCount = order.myShares?.length || 0 const items = getMaterialDetailItems(order).filter((item) => String(item.value || '').trim()) const screenshots = getMaterialScreenshots(order) const acceptanceFiles = getUniqueAcceptanceFiles(getWorkerAcceptance(order)) @@ -343,12 +348,23 @@ function WorkerOrderMobileCard({ {formatDateTime(order.assignedAt)} - {(order.myShares?.length || 0) > 1 && renderShareTable ? ( + {shareCount > 1 && renderShareTable ? (
- - 拼单明细({order.myShares?.length} 份) - - {renderShareTable(order)} + + {sharesExpanded ? ( +
{renderShareTable(order)}
+ ) : null}
) : null} diff --git a/apps/frontend/src/pages/worker/WorkerOrderShareTable.tsx b/apps/frontend/src/pages/worker/WorkerOrderShareTable.tsx index e3202bb6..b28301ad 100644 --- a/apps/frontend/src/pages/worker/WorkerOrderShareTable.tsx +++ b/apps/frontend/src/pages/worker/WorkerOrderShareTable.tsx @@ -22,11 +22,14 @@ const SHARE_STATUS_META: Record = { */ export function WorkerOrderShareTable({ order, + mobile = false, onOpenAcceptance, onOpenCancelRequest, onRemindAcceptance, }: { order: WorkOrder + /** 移动端改成卡片列表,避免 9 列表格在窄屏被压成竖排文字。 */ + mobile?: boolean onOpenAcceptance: (order: WorkOrder, shareId: number) => void onOpenCancelRequest: (order: WorkOrder, shareId: number) => void onRemindAcceptance: (order: WorkOrder) => void @@ -41,6 +44,48 @@ export function WorkerOrderShareTable({ if (shares.length === 0) return null + const renderShareActions = (share: WorkOrderShare) => { + const canAccept = !isTimedOut && share.status === 'joined' + const canEdit = !isTimedOut && ['joined', 'submitted'].includes(String(share.status)) + const canRemind = !isTimedOut && share.status === 'submitted' + const canCancel = !isTimedOut && ['joined', 'submitted'].includes(String(share.status)) + if (!canAccept && !canEdit && !canRemind && !canCancel) { + return ( + + 无 + + ) + } + return ( + + {canAccept ? ( + + ) : null} + {canEdit && share.status === 'submitted' ? ( + + ) : null} + {canRemind ? ( + + ) : null} + {canCancel ? ( + + ) : null} + + ) + } + const columns: TableColumnsType = [ { title: '份额', @@ -154,50 +199,79 @@ export function WorkerOrderShareTable({ { title: '操作', width: 190, - render: (_, share) => { - const canAccept = !isTimedOut && share.status === 'joined' - const canEdit = !isTimedOut && ['joined', 'submitted'].includes(String(share.status)) - const canRemind = !isTimedOut && share.status === 'submitted' - const canCancel = !isTimedOut && ['joined', 'submitted'].includes(String(share.status)) - if (!canAccept && !canEdit && !canRemind && !canCancel) { - return ( - - 无 - - ) - } - return ( - - {canAccept ? ( - - ) : null} - {canEdit && share.status === 'submitted' ? ( - - ) : null} - {canRemind ? ( - - ) : null} - {canCancel ? ( - - ) : null} - - ) - }, + render: (_, share) => renderShareActions(share), }, ] + if (mobile) { + return ( +
+ {shares.map((share, index) => { + const meta = SHARE_STATUS_META[String(share.status)] || { + label: String(share.status || '-'), + color: 'default', + } + const problemNote = String(share.problemNote || '').trim() + const canAct = !isTimedOut && ['joined', 'submitted'].includes(String(share.status)) + return ( +
+
+ + 第 {index + 1} 份 · #{share.shareId} + + + {meta.label} + +
+
+
+ 数量 + {share.quantity} 份 +
+
+ 奖励 + + {formatMoney(share.shareReward)} + +
+
+ 押金 + + {formatMoney(share.shareDeposit)} + +
+
+
+ 截止时间 + + {share.deadlineAt ? formatDateTime(share.deadlineAt) : '不限时'} + + {share.deadlineAt && share.status === 'joined' && !isTimedOut ? ( + + ) : null} +
+ {share.problemMarked ? ( +
+ + 问题单 + + {problemNote || '后台已标记该份额有问题'} +
+ ) : null} +
+ 验收信息 + +
+ {canAct ? ( +
{renderShareActions(share)}
+ ) : null} +
+ ) + })} +
+ ) + } + return ( rowKey="shareId" diff --git a/apps/frontend/src/pages/worker/WorkerOrdersPage.tsx b/apps/frontend/src/pages/worker/WorkerOrdersPage.tsx index 39ef4697..f3aac03a 100644 --- a/apps/frontend/src/pages/worker/WorkerOrdersPage.tsx +++ b/apps/frontend/src/pages/worker/WorkerOrdersPage.tsx @@ -592,6 +592,7 @@ export default function WorkerOrdersPage() { return ( { diff --git a/apps/frontend/src/styles/worker.css b/apps/frontend/src/styles/worker.css index 833729ed..0762e2d7 100644 --- a/apps/frontend/src/styles/worker.css +++ b/apps/frontend/src/styles/worker.css @@ -4894,8 +4894,155 @@ body { .worker-order-mobile-shares { display: flex; flex-direction: column; + gap: 8px; + margin: 8px 0 4px; +} + +/* 拼单明细默认折叠:整行可点,右侧显示「展开/收起」与箭头 */ +.worker-order-mobile-shares-toggle { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 8px 0 0; + background: transparent; + border: 0; + border-top: 1px dashed #f0f0f0; + color: #0f172a; + font-size: 12px; + font-weight: 600; + line-height: 1.4; + text-align: left; + cursor: pointer; +} + +.worker-order-mobile-shares-toggle:active { + opacity: 0.7; +} + +.worker-order-mobile-shares-hint { + display: inline-flex; + align-items: center; + gap: 4px; + flex-shrink: 0; + color: #94a3b8; + font-size: 12px; + font-weight: 400; +} + +.worker-order-mobile-shares-body { + display: flex; + flex-direction: column; + gap: 6px; +} + +/* 移动端拼单明细:窄屏改用卡片,避免多列表格被压成竖排文字 */ +.worker-order-share-cards { + display: flex; + flex-direction: column; + gap: 8px; +} + +.worker-order-share-card { + display: flex; + flex-direction: column; + gap: 8px; + padding: 10px; + background: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 10px; +} + +.worker-order-share-card-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.worker-order-share-card-title { + font-size: 13px; + color: #0f172a; +} + +.worker-order-share-card-meta { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 6px; +} + +.worker-order-share-card-meta-item { + display: flex; + flex-direction: column; + gap: 2px; + padding: 5px 8px; + background: #f8fafc; + border-radius: 8px; + min-width: 0; +} + +.worker-order-share-card-meta-label { + font-size: 11px; + color: #94a3b8; + line-height: 1.2; +} + +.worker-order-share-card-meta-value { + font-size: 13px; + font-weight: 700; + color: #0f172a; + line-height: 1.3; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.worker-order-share-card-line { + display: flex; + align-items: center; + flex-wrap: wrap; gap: 6px; - margin: 8px 12px 4px; - padding-top: 8px; - border-top: 1px dashed #f0f0f0; + font-size: 12px; + color: #475569; +} + +.worker-order-share-card-line-label { + color: #94a3b8; + font-size: 11px; + flex-shrink: 0; +} + +.worker-order-share-card-line-value { + color: #334155; +} + +.worker-order-share-card-problem { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 4px; + padding: 6px 8px; + background: #fef2f2; + border: 1px solid #fecaca; + border-radius: 8px; + color: #b91c1c; + font-size: 12px; + line-height: 1.4; +} + +.worker-order-share-card-acceptance { + display: flex; + flex-direction: column; + gap: 4px; + font-size: 12px; + color: #475569; +} + +.worker-order-share-card-actions { + display: flex; + justify-content: flex-end; + flex-wrap: wrap; + gap: 6px; + padding-top: 6px; + border-top: 1px dashed #eef2f7; } diff --git a/apps/frontend/src/utils/worker-realtime-work-orders.ts b/apps/frontend/src/utils/worker-realtime-work-orders.ts index 11630eae..f2eaaab1 100644 --- a/apps/frontend/src/utils/worker-realtime-work-orders.ts +++ b/apps/frontend/src/utils/worker-realtime-work-orders.ts @@ -155,10 +155,11 @@ export function patchWorkerWorkOrderListEnvelope( event: WorkerListEvent, insert = false, /** - * 就地更新时,快照里没有的「本打手私有字段」保留旧值。 + * 就地更新时,快照里没有(或为空)的「本打手私有字段」保留旧值。 * * 大厅广播是发给所有打手的通用快照,天然没有 `myShare`/`myShares`(那是每个打手自己的份额)。 - * 整行替换会把「再拼一次」冲回「拼单 ¥x/份」,所以覆盖行时必须保留。 + * 旧版本后端还会显式带 `null`/`[]` 过来;整行替换或直接合并空值都会把 + * 「再拼一次」冲回「拼单 ¥x/份」,所以覆盖行时两种情况都要保留。 */ preserveFields: readonly string[] = [], /** @@ -198,7 +199,11 @@ export function patchWorkerWorkOrderListEnvelope( ...(isRecord(event.data) ? event.data : {}), } for (const field of preserveFields) { - if (existing && !(field in merged) && field in existing) merged[field] = existing[field] + if (!existing || !(field in existing)) continue + // 快照没有该键,或显式带了 null/[](旧版大厅广播),都视为「没有个人份额信息」。 + const incoming = merged[field] + const incomingEmpty = incoming == null || (Array.isArray(incoming) && incoming.length === 0) + if (incomingEmpty) merged[field] = existing[field] } nextItems = items.map((entry, itemIndex) => (itemIndex === index ? merged : entry)) } else if (insert) { @@ -259,6 +264,36 @@ export function sortWorkerHallItems(items: unknown[]): unknown[] { }) } +/** + * 只把「我的订单」快照里的个人份额字段覆盖到大厅缓存行上。 + * + * 大厅广播是发给所有打手的通用快照,已经剔除 myShare/myShares(否则浅合并会 + * 冲掉「再拼一次」);但「后台撤销我的份额」「我的份额被验收」这类变化只出现在 + * 「我的订单」快照里,需要用后者回填大厅行。这里不增删、不重排,找不到工单就原样返回。 + */ +export function patchWorkerHallPersonalShares( + current: unknown, + workOrderId: number, + myShare: unknown, + myShares: unknown, +): unknown { + if (!isRecord(current) || !Array.isArray(current.pages) || workOrderId <= 0) return current + let changed = false + const pages = current.pages.map((page) => { + if (!isRecord(page) || !isRecord(page.data) || !Array.isArray(page.data.items)) return page + let pageChanged = false + const items = page.data.items.map((item) => { + if (!isRecord(item) || toNumber(item.workOrderId) !== workOrderId) return item + pageChanged = true + return { ...item, myShare, myShares } + }) + if (!pageChanged) return page + changed = true + return { ...page, data: { ...page.data, items } } + }) + return changed ? { ...current, pages } : current +} + /** 无限下拉(大厅):第 1 页允许插入,其余页只做就地更新;最终按置顶优先重排。 */ export function patchWorkerWorkOrderInfiniteEnvelope( current: unknown, diff --git a/apps/frontend/tests/worker-realtime-work-orders.test.ts b/apps/frontend/tests/worker-realtime-work-orders.test.ts index b38bcb89..62361010 100644 --- a/apps/frontend/tests/worker-realtime-work-orders.test.ts +++ b/apps/frontend/tests/worker-realtime-work-orders.test.ts @@ -5,6 +5,7 @@ import { compareWorkerHallOrderRows, evaluateWorkerKeywordVerdict, matchesWorkerWorkOrderKeyword, + patchWorkerHallPersonalShares, patchWorkerWorkOrderInfiniteEnvelope, patchWorkerWorkOrderListEnvelope, resolveWorkerMyOrderRealtimeVerdict, @@ -428,6 +429,56 @@ test('大厅广播覆盖行时必须保留本打手的 myShare/myShares,并更 assert.equal(insertedRow?.myShares ?? null, null) }) +test('旧版大厅广播显式带 null/[] 时也不能冲掉本打手份额', () => { + const cachedRow = buildRow({ + status: 'open', + myShare: { shareId: 88, quantity: 2, status: 'joined' }, + myShares: [{ shareId: 88, quantity: 2, status: 'joined' }], + sharingProgress: { joinedQuantity: 2, pendingSubmissionCount: 1 }, + }) + const envelope = { pages: [buildEnvelope([cachedRow])], pageParams: [1] } + const patched = patchWorkerWorkOrderInfiniteEnvelope( + envelope, + { + operation: 'upsert', + data: buildRow({ + status: 'open', + myShare: null, + myShares: [], + sharingProgress: { joinedQuantity: 3 }, + }), + }, + true, + ['myShare', 'myShares'], + ) as { pages: unknown[] } + const row = itemsOf(patched.pages[0])[0] as { + myShare?: { shareId: number } + myShares?: unknown[] + sharingProgress?: { joinedQuantity: number } + } + assert.equal(row.myShare?.shareId, 88, '旧事件不能把「再拼一次」冲掉') + assert.equal(row.myShares?.length, 1) + assert.equal(row.sharingProgress?.joinedQuantity, 3, '公开进度仍取广播最新值') +}) + +test('「我的订单」快照回填大厅行个人份额:撤销份额后清空,找不到工单不改缓存', () => { + const joined = buildRow({ + workOrderId: 5, + status: 'open', + myShare: { shareId: 66, quantity: 1, status: 'joined' }, + myShares: [{ shareId: 66, quantity: 1, status: 'joined' }], + }) + const envelope = { pages: [buildEnvelope([joined])], pageParams: [1] } + + const cleared = patchWorkerHallPersonalShares(envelope, 5, null, []) as { pages: unknown[] } + const row = itemsOf(cleared.pages[0])[0] as { myShare?: unknown; myShares?: unknown[] } + assert.equal(row.myShare ?? null, null, '后台撤掉份额后大厅按钮要能恢复成「拼单」') + assert.deepEqual(row.myShares, []) + + // 工单不在大厅缓存里时原样返回,不能凭空造行。 + assert.equal(patchWorkerHallPersonalShares(envelope, 999, null, []), envelope) +}) + /** * 线上坑:进大厅后过一会儿「排名靠后的订单跑到前面去」。 *