diff --git a/lib/__tests__/ding-workflow-delivery.test.ts b/lib/__tests__/ding-workflow-delivery.test.ts index 1ad83ef..b3a80f9 100644 --- a/lib/__tests__/ding-workflow-delivery.test.ts +++ b/lib/__tests__/ding-workflow-delivery.test.ts @@ -281,4 +281,41 @@ describe('Ding workflow delivery SLA', () => { expect(second.queueShiftDays).toBe(2) expect(second.startedAt).toBe('2026-08-03T01:00:00.000Z') }) + + it('uses businessId ordering and ignores the non-queue content-owner node', () => { + const makeInput = (businessId: string, workflowId: string, extraOwnerTask = false) => ({ + processCode: CONTENT_PROCESS_CODE, + sourceCreatedAt: '2026-08-01T00:00:00.000Z', + sourceFinishedAt: '2026-08-02T00:00:00.000Z', + formFields: formFields({ demandType: '抖音-千川素材', taskType: '布局拍摄' }), + tasks: [ + task('内容主管分配任务', '2026-08-01T01:00:00.000Z', '2026-08-01T01:00:00.000Z'), + ...(extraOwnerTask + ? [task('内容部负责人', '2026-08-02T01:00:00.000Z', '2026-08-02T01:00:00.000Z')] + : []), + task('编导审核', '2026-08-02T00:00:00.000Z', '2026-08-02T00:00:00.000Z'), + ], + businessId, + workflowId, + workflowStatus: 'COMPLETED', + }) + + const assessments = assessDingWorkflowDeliveryBatch([ + makeInput('B-002', 'a', true), + makeInput('B-001', 'z'), + makeInput('B-003', 'b'), + makeInput('B-004', 'c'), + ]) + + // The external queue uses businessId for equal assignment timestamps, + // and the unrelated 内容部负责人 task must not move the base date. + expect(assessments.slice(0, 3).map((assessment) => assessment.queueSequence)).toEqual([2, 1, 3]) + expect(assessments[0].queueShiftDays).toBe(0) + expect(assessments[3]).toMatchObject({ + queueDeferred: true, + queueSequence: 1, + queueShiftDays: 1, + startedAt: '2026-08-02T01:00:00.000Z', + }) + }) }) diff --git a/lib/ding-workflow-delivery.ts b/lib/ding-workflow-delivery.ts index b1fcf0a..4190d20 100644 --- a/lib/ding-workflow-delivery.ts +++ b/lib/ding-workflow-delivery.ts @@ -52,6 +52,8 @@ type DingWorkflowDeliveryInput = { sourceFinishedAt: string | null formFields: DingWorkflowDetailFormFields tasks: OngoingDingWorkflowActiveTask[] + /** Stable DingTalk business number used by the external queue as tie-breaker. */ + businessId?: string | null /** Used only by the batch queue calculation to produce a deterministic tie-breaker. */ workflowId?: string | null /** Raw workflow status; processing rows occupy queue slots, rejected rows do not. */ @@ -85,6 +87,7 @@ const CONTENT_QUEUE_GROUP_TAOXI_LAYOUT = new Set([ '淘系-逛逛+淘系主图视频', ]) const CONTENT_QUEUE_GROUP_TAOXI_SECOND_EDIT = new Set(['淘系-逛逛', '淘系主图']) +const CONTENT_QUEUE_ASSIGNMENT_NODE_NAMES = ['内容主管分配任务', '内容负责人分配任务'] const ART_RULES: ArtRule[] = [ { type: '视觉 打板', detail: '主图+sku+详情+视频', days: 15, byQuantity: true }, @@ -552,8 +555,7 @@ export function assessDingWorkflowDeliveryBatch(inputs: DingWorkflowDeliveryInpu if (!policy) return // The external dashboard schedules by the latest assignment timestamp, // while the unqueued assessment still starts at the first assignment. - const assignmentNames = ['内容主管分配任务', '内容负责人分配任务', '内容部负责人'] - const baseStart = latestTaskTimeByField(input.tasks, assignmentNames, 'createdAt')?.timestamp + const baseStart = latestTaskTimeByField(input.tasks, CONTENT_QUEUE_ASSIGNMENT_NODE_NAMES, 'createdAt')?.timestamp ?? parseIsoTime(assessments[index].startedAt) if (baseStart === null) return const entries = queueGroups.get(policy.key) || [] @@ -561,7 +563,9 @@ export function assessDingWorkflowDeliveryBatch(inputs: DingWorkflowDeliveryInpu index, policy, baseStart, - workflowId: normalized(input.workflowId) || String(index).padStart(8, '0'), + workflowId: normalized(input.businessId) + || normalized(input.workflowId) + || String(index).padStart(8, '0'), }) queueGroups.set(policy.key, entries) }) diff --git a/lib/ding-workflow-sync.ts b/lib/ding-workflow-sync.ts index cb5178e..1b5775c 100644 --- a/lib/ding-workflow-sync.ts +++ b/lib/ding-workflow-sync.ts @@ -1944,6 +1944,7 @@ async function readOnTimeDeliveryInsightsWithoutAccessCheck(now = new Date()): P sourceFinishedAt, formFields: normalizeDetailFormFields(row.formFields), tasks: allTasks, + businessId: clean(row.businessId), workflowId: clean(row.processInstanceId), workflowStatus: clean(row.status), }