diff --git a/furniture-3d-system/src/lib/oa-board-flow-selection.test.ts b/furniture-3d-system/src/lib/oa-board-flow-selection.test.ts new file mode 100644 index 00000000..920985bf --- /dev/null +++ b/furniture-3d-system/src/lib/oa-board-flow-selection.test.ts @@ -0,0 +1,92 @@ +import { selectLatestRestartableChildFlows } from './oa-board-flow-selection' + +type TestFlowRecord = { + flowType: string + processInstanceId: string + createdAtSource: Date | null + updatedAtSource: Date | null + status: string +} + +function buildFlow( + flowType: string, + processInstanceId: string, + createdAtSource: string | null, + status: string, + updatedAtSource = createdAtSource, +): TestFlowRecord { + return { + flowType, + processInstanceId, + createdAtSource: createdAtSource ? new Date(createdAtSource) : null, + updatedAtSource: updatedAtSource ? new Date(updatedAtSource) : null, + status, + } +} + +describe('OA 看板子流程版本选择', () => { + it('首单、美术和限价流程各自只保留最晚发起的一条', () => { + const records = [ + buildFlow('review', 'review-1', '2026-08-03T02:30:00Z', 'COMPLETED'), + buildFlow('order', 'order-refused', '2026-08-03T08:00:00Z', 'REFUSED'), + buildFlow('order', 'order-running', '2026-08-04T08:00:00Z', 'RUNNING'), + buildFlow('art', 'art-refused', '2026-08-05T08:00:00Z', 'REFUSED'), + buildFlow('art', 'art-completed', '2026-08-06T08:00:00Z', 'COMPLETED'), + buildFlow('pricing', 'pricing-terminated', '2026-08-17T01:49:00Z', 'TERMINATED'), + buildFlow('pricing', 'pricing-completed', '2026-08-17T01:51:00Z', 'COMPLETED'), + buildFlow('main', 'main-1', '2026-08-17T02:00:00Z', 'RUNNING'), + ] + + expect( + selectLatestRestartableChildFlows(records).map((record) => record.processInstanceId), + ).toEqual([ + 'review-1', + 'order-running', + 'art-completed', + 'pricing-completed', + 'main-1', + ]) + }) + + it('以发起时间为主,不让旧流程的较晚更新时间覆盖新流程', () => { + const records = [ + buildFlow( + 'art', + 'old-art-updated-later', + '2026-08-05T08:00:00Z', + 'REFUSED', + '2026-08-08T08:00:00Z', + ), + buildFlow( + 'art', + 'new-art', + '2026-08-06T08:00:00Z', + 'RUNNING', + '2026-08-06T09:00:00Z', + ), + ] + + expect(selectLatestRestartableChildFlows(records)).toEqual([records[1]]) + }) + + it('发起时间相同时用更新时间稳定决定最新记录', () => { + const records = [ + buildFlow( + 'pricing', + 'pricing-older-update', + '2026-08-17T01:51:00Z', + 'TERMINATED', + '2026-08-17T01:52:00Z', + ), + buildFlow( + 'pricing', + 'pricing-newer-update', + '2026-08-17T01:51:00Z', + 'COMPLETED', + '2026-08-17T01:58:00Z', + ), + ] + + expect(selectLatestRestartableChildFlows(records)).toEqual([records[1]]) + }) +}) diff --git a/furniture-3d-system/src/lib/oa-board-flow-selection.ts b/furniture-3d-system/src/lib/oa-board-flow-selection.ts new file mode 100644 index 00000000..b5847080 --- /dev/null +++ b/furniture-3d-system/src/lib/oa-board-flow-selection.ts @@ -0,0 +1,47 @@ +const RESTARTABLE_CHILD_FLOW_TYPES = new Set(['order', 'art', 'pricing']) + +export type OaBoardFlowSelectionRecord = { + flowType: string + processInstanceId: string + createdAtSource: Date | null + updatedAtSource: Date | null +} + +function toTimestamp(value: Date | null) { + const timestamp = value?.getTime() ?? 0 + return Number.isFinite(timestamp) ? timestamp : 0 +} + +function compareFlowRecency( + left: OaBoardFlowSelectionRecord, + right: OaBoardFlowSelectionRecord, +) { + const createdAtDiff = toTimestamp(left.createdAtSource) - toTimestamp(right.createdAtSource) + if (createdAtDiff !== 0) return createdAtDiff + + const updatedAtDiff = toTimestamp(left.updatedAtSource) - toTimestamp(right.updatedAtSource) + if (updatedAtDiff !== 0) return updatedAtDiff + + return left.processInstanceId.localeCompare(right.processInstanceId) +} + +export function selectLatestRestartableChildFlows( + records: T[], +) { + const latestByFlowType = new Map() + + records.forEach((record) => { + if (!RESTARTABLE_CHILD_FLOW_TYPES.has(record.flowType)) return + + const currentLatest = latestByFlowType.get(record.flowType) + if (!currentLatest || compareFlowRecency(record, currentLatest) > 0) { + latestByFlowType.set(record.flowType, record) + } + }) + + return records.filter( + (record) => + !RESTARTABLE_CHILD_FLOW_TYPES.has(record.flowType) || + latestByFlowType.get(record.flowType) === record, + ) +} diff --git a/furniture-3d-system/src/server/services/oa-board-sync.service.ts b/furniture-3d-system/src/server/services/oa-board-sync.service.ts index 056c91bf..ffa4b4ce 100644 --- a/furniture-3d-system/src/server/services/oa-board-sync.service.ts +++ b/furniture-3d-system/src/server/services/oa-board-sync.service.ts @@ -6,6 +6,7 @@ import { import type sql from 'mssql' import { Prisma } from '@prisma/client' import { prisma } from '@/lib/prisma' +import { selectLatestRestartableChildFlows } from '@/lib/oa-board-flow-selection' import { externalEmployeeDirectoryService } from './external-employee-directory.service' import type { OaBoardProjectGraphInput } from './oa-board.service' import { @@ -3106,7 +3107,10 @@ function buildProjectGroups(records: NormalizedFlowRecord[]): PreparedProjectGro }) } - return Array.from(groups.values()) + return Array.from(groups.values()).map((group) => ({ + ...group, + flowRecords: selectLatestRestartableChildFlows(group.flowRecords), + })) } function countNestedRecords(projectGraphs: OaBoardProjectGraphInput[]) {