gongxiangp2/lib/ding-workflow-delivery.ts
2026-08-27 18:00:55 +08:00

416 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type {
DingWorkflowDetailFormFields,
OngoingDingWorkflowActiveTask,
} from '@/lib/ding-workflow-sync'
const ART_PROCESS_CODE = 'PROC-3571C915-0506-4490-A91A-8AA7EA33F838'
const CONTENT_PROCESS_CODE = 'PROC-AE9FF3E1-A442-4E6A-85CF-0F2B86409673'
const ART_NEW_STANDARD_FROM = Date.parse('2026-06-30T16:00:00.000Z')
type ArtRule = {
type: string
detail: string
days: number
byQuantity?: boolean
noQuantity?: boolean
capDays?: number | null
noCrossStack?: boolean
}
type ContentRule = {
demandType: string
taskType: string
installVideo: string
quantity: string
effectiveFrom?: string
effectiveTo?: string
totalHours: number
actualCalcTo: 'directorReview' | 'contentReview'
}
export type DingWorkflowDeliveryStatus = 'on_time' | 'overtime' | 'pending'
export type DingWorkflowDeliveryAssessment = {
status: DingWorkflowDeliveryStatus
standardDuration: number | null
durationUnit: 'day' | 'hour'
actualDuration: number | null
startedAt: string | null
finishedAt: string | null
startNodeLabel: string
endNodeLabel: string
}
const ART_RULES: ArtRule[] = [
{ type: '视觉 打板', detail: '主图+sku+详情+视频', days: 15, byQuantity: true },
{ type: '品牌/品类视觉制作规范', detail: '主图或详情视觉规范制作', days: 7, byQuantity: true },
{ type: '品牌/品类视觉制作规范', detail: '首图视角规范制定', days: 3, byQuantity: true },
{ type: '新品/优化策划全案', detail: '全套任务(主图sku详情/策划方案/买家秀)', days: 7, byQuantity: true, capDays: 7 },
{ type: '1688ai图+详情全套', detail: '主图+sku+详情', days: 1, byQuantity: true },
{ type: '新品策划全案/新品全套图文', detail: '全套新品的策划方案(主图视频脚本/策划方案/买家秀文案)', days: 2, byQuantity: true },
{ type: '优化策划全案/优化全套图文', detail: '全套优化的策划方案(主图视频脚本/策划方案/买家秀文案)', days: 2, byQuantity: true },
{ type: 'ai视频', detail: '买家秀视频5秒(8个/套)', days: 1, byQuantity: true },
{ type: 'ai视频', detail: '内容视频15秒(个)', days: 1, byQuantity: true },
{ type: 'ai视频', detail: '主图视频1分钟以内', days: 1, byQuantity: true },
{ type: '主图/sku图/细节图(出图)', detail: '创新出图(90%创新)3d', days: 2, noQuantity: true, capDays: 3 },
{ type: '主图/sku图/细节图(出图)', detail: '部分调整出图(修改占比50%)3d', days: 1, noQuantity: true, capDays: 3 },
{ type: '主图/sku图/细节图(出图)', detail: '套场景出图 3d', days: 1, noQuantity: true, capDays: 3 },
{ type: 'ai场景类(出图)', detail: '局部微调整', days: 1, noQuantity: true, capDays: 3 },
{ type: 'ai场景类(出图)', detail: '局部调整', days: 1, noQuantity: true, capDays: 3 },
{ type: 'ai场景类(出图)', detail: '多sku场景', days: 1, noQuantity: true, capDays: 3 },
{ type: 'ai场景类(出图)', detail: '单sku场景', days: 1, noQuantity: true, capDays: 3 },
{ type: '模型(出图)', detail: '软包或异型结构、材质超过3个以上以及特殊难度造型 3d', days: 2, noQuantity: true, capDays: 3 },
{ type: '模型(出图)', detail: '软包或异型结构、材质不超过3个 3d', days: 1, noQuantity: true, capDays: 3 },
{ type: '模型(出图)', detail: '纯板式或钢架结构 无特殊造型 3d', days: 1, noQuantity: true, capDays: 3 },
{ type: '模型调整(出图)', detail: '模型上传', days: 1, noQuantity: true, capDays: 2 },
{ type: '模型调整(出图)', detail: '模型调整', days: 1, noQuantity: true, capDays: 2 },
{ type: '批量调整(平面)', detail: '自动化表格创建', days: 1, noQuantity: true, capDays: 2 },
{ type: '批量调整(平面)', detail: '批量生成', days: 1, noQuantity: true, capDays: 2 },
{ type: '主图(平面)', detail: '制作(以优化主图的标准/全新设计)', days: 1, noQuantity: true, capDays: 2 },
{ type: '主图(平面)', detail: '活动打标(非批量线稿/尺寸 需手动)', days: 1, noQuantity: true, capDays: 2 },
{ type: '详情(平面)', detail: '详情制作(主图+SKU+详情)(超过所做页面的90%)', days: 2, noQuantity: true, capDays: 2 },
{ type: '详情(平面)', detail: '部分套版(主图+SKU+详情)(所做页面的50%)', days: 1, noQuantity: true, capDays: 2 },
{ type: '详情(平面)', detail: '直接套版(主图+SKU+详情)(修改文字,部分调整)', days: 1, noQuantity: true, capDays: 2 },
{ type: '详情(平面)', detail: '部分调整(修改文字或某一小部分)', days: 1, noQuantity: true, capDays: 2 },
{ type: '首页/二级页面/大促页面(平面)', detail: '制作', days: 3, noQuantity: true, capDays: 3 },
{ type: '首页/二级页面/大促页面(平面)', detail: '调整(视情况而定)', days: 1, noQuantity: true, capDays: 3 },
{ type: '首页/二级页面/大促页面(平面)', detail: '套版', days: 1, noQuantity: true, capDays: 3 },
{ type: '首页/二级页面/大促页面(平面)', detail: '上传装修', days: 1, noQuantity: true, capDays: 3 },
{ type: '首页/二级页面/大促页面(平面)', detail: '首页头图', days: 1, noQuantity: true, capDays: 3 },
{ type: '详情关联页(平面)', detail: '制作(根据产品数量而定)', days: 1, noQuantity: true },
{ type: '详情关联页(平面)', detail: '调整', days: 1, noQuantity: true },
{ type: '详情关联页(平面)', detail: '信任背书', days: 1, noQuantity: true },
{ type: '标签/包装/logo(平面)', detail: '制作', days: 2, byQuantity: true },
{ type: '首焦/轮播/banner/海报/详情头图(平面)', detail: '制作', days: 1, noQuantity: true },
{ type: '首焦/轮播/banner/海报/详情头图(平面)', detail: '调整', days: 1, noQuantity: true },
{ type: '首焦/轮播/banner/海报/详情头图(平面)', detail: '直播间海报', days: 1, noQuantity: true },
{ type: '资质/授权P图(平面)', detail: '调整', days: 1, noQuantity: true },
]
const CONTENT_RULES: ContentRule[] = [
{ demandType: '抖音-千川素材', taskType: '布局拍摄', installVideo: '/', quantity: '/', totalHours: 72, actualCalcTo: 'directorReview' },
{ demandType: '抖音-千川素材', taskType: '二次剪辑', installVideo: '/', quantity: '/', totalHours: 48, actualCalcTo: 'directorReview' },
{ demandType: '抖音-账号维护(内容部专用)', taskType: '布局拍摄', installVideo: '/', quantity: '/', effectiveTo: '2026-07-19 23:59:59', totalHours: 24, actualCalcTo: 'directorReview' },
{ demandType: '抖音-账号维护(内容部专用)', taskType: '布局拍摄', installVideo: '/', quantity: '/', effectiveFrom: '2026-07-20 00:00:00', totalHours: 72, actualCalcTo: 'directorReview' },
{ demandType: '抖音千川+抖音账户维护视频', taskType: '布局拍摄', installVideo: '/', quantity: '/', effectiveFrom: '2026-07-20 00:00:00', totalHours: 72, actualCalcTo: 'directorReview' },
{ demandType: '淘系-逛逛', taskType: '布局拍摄', installVideo: '/', quantity: '/', totalHours: 72, actualCalcTo: 'directorReview' },
{ demandType: '淘系-逛逛', taskType: '二次剪辑', installVideo: '/', quantity: '/', totalHours: 48, actualCalcTo: 'directorReview' },
{ demandType: '淘系主图', taskType: '布局拍摄', installVideo: '/', quantity: '/', effectiveTo: '2026-07-19 23:59:59', totalHours: 24, actualCalcTo: 'directorReview' },
{ demandType: '淘系主图', taskType: '布局拍摄', installVideo: '/', quantity: '/', effectiveFrom: '2026-07-20 00:00:00', totalHours: 72, actualCalcTo: 'directorReview' },
{ demandType: '淘系主图', taskType: '二次剪辑', installVideo: '/', quantity: '/', effectiveTo: '2026-05-23 23:59:59', totalHours: 16, actualCalcTo: 'directorReview' },
{ demandType: '淘系-逛逛+淘系主图视频', taskType: '布局拍摄', installVideo: '/', quantity: '/', effectiveFrom: '2026-07-20 00:00:00', totalHours: 72, actualCalcTo: 'directorReview' },
{ demandType: '淘系主图', taskType: '二次剪辑', installVideo: '/', quantity: '/', effectiveFrom: '2026-05-24 00:00:00', totalHours: 24, actualCalcTo: 'directorReview' },
{ demandType: 'AI视频', taskType: '布局拍摄', installVideo: '/', quantity: '/', totalHours: 24, actualCalcTo: 'directorReview' },
{ demandType: 'AI视频', taskType: '二次剪辑', installVideo: '/', quantity: '/', totalHours: 24, actualCalcTo: 'directorReview' },
{ demandType: '实拍-策划专用', taskType: '布局拍摄', installVideo: '/', quantity: '>0<=20', effectiveTo: '2026-07-19 23:59:59', totalHours: 60, actualCalcTo: 'contentReview' },
{ demandType: '实拍-策划专用', taskType: '布局拍摄', installVideo: '/', quantity: '>20', effectiveTo: '2026-07-19 23:59:59', totalHours: 72, actualCalcTo: 'contentReview' },
{ demandType: '实拍-策划专用', taskType: '布局拍摄', installVideo: '/', quantity: '/', effectiveFrom: '2026-07-20 00:00:00', totalHours: 48, actualCalcTo: 'contentReview' },
{ demandType: '白底图-策划专用', taskType: '布局拍摄', installVideo: '/', quantity: '/', effectiveTo: '2026-05-23 23:59:59', totalHours: 6, actualCalcTo: 'contentReview' },
{ demandType: '白底图-策划专用', taskType: '布局拍摄', installVideo: '/', quantity: '/', effectiveFrom: '2026-05-24 00:00:00', totalHours: 24, actualCalcTo: 'contentReview' },
{ demandType: '安装视频', taskType: '布局拍摄', installVideo: 'A档', quantity: '/', effectiveTo: '2026-07-19 23:59:59', totalHours: 24, actualCalcTo: 'contentReview' },
{ demandType: '安装视频', taskType: '布局拍摄', installVideo: 'B档', quantity: '/', effectiveTo: '2026-07-19 23:59:59', totalHours: 32, actualCalcTo: 'contentReview' },
{ demandType: '安装视频', taskType: '布局拍摄', installVideo: 'C档', quantity: '/', effectiveTo: '2026-07-19 23:59:59', totalHours: 48, actualCalcTo: 'contentReview' },
{ demandType: '安装视频', taskType: '布局拍摄', installVideo: '/', quantity: '/', effectiveFrom: '2026-07-20 00:00:00', totalHours: 48, actualCalcTo: 'contentReview' },
]
const LEGACY_ART_RULES = [
['新品', '仅出图', '', 2], ['新品', '仅平面', '', 3], ['新品', '仅视频', '', 2],
['新品', '出图 + 平面 + 视频', '', 5], ['新品', '出图+平面', '', 4],
['优化', '仅出图', '', 3], ['优化', '仅平面', '', 2], ['优化', '仅视频', '', 1],
['优化', '出图 + 平面 + 视频', '', 5], ['优化', '出图+平面', '', 4],
['产品改图', '仅出图', '', 2], ['产品改图', '仅平面', '', 3], ['产品改图', '仅视频', '', 2],
['产品改图', '出图 + 平面 + 视频', '', 5], ['产品改图', '出图+平面', '', 4],
['新品', '仅出图', '2026-04-19 00:00:00', 3], ['新品', '仅平面', '2026-04-19 00:00:00', 1],
['新品', '仅视频', '2026-04-19 00:00:00', 1], ['新品', '出图 + 平面 + 视频', '2026-04-19 00:00:00', 5],
['新品', '出图+平面', '2026-04-19 00:00:00', 4], ['优化', '仅出图', '2026-04-19 00:00:00', 3],
['优化', '仅平面', '2026-04-19 00:00:00', 1], ['优化', '仅视频', '2026-04-19 00:00:00', 1],
['优化', '出图 + 平面 + 视频', '2026-04-19 00:00:00', 5], ['优化', '出图+平面', '2026-04-19 00:00:00', 4],
['产品改图', '仅出图', '2026-04-19 00:00:00', 3], ['产品改图', '仅平面', '2026-04-19 00:00:00', 1],
['产品改图', '仅视频', '2026-04-19 00:00:00', 1], ['产品改图', '出图 + 平面 + 视频', '2026-04-19 00:00:00', 5],
['产品改图', '出图+平面', '2026-04-19 00:00:00', 4],
] as const
function normalized(value: unknown) {
return String(value ?? '').replace(/\u00a0/g, ' ').trim().replace(/\s+/g, ' ')
}
function canonical(value: unknown) {
return normalized(value).toLowerCase().replace(/\s+/g, '')
}
function parseIsoTime(value: string | null | undefined) {
if (!value) return null
const parsed = Date.parse(value)
return Number.isFinite(parsed) ? parsed : null
}
function roundTwo(value: number) {
return Math.round(value * 100) / 100
}
function parseFirstNumber(value: unknown) {
const match = normalized(value).replace(/,/g, '').match(/-?\d+(?:\.\d+)?/)
const numeric = match ? Number(match[0]) : Number.NaN
return Number.isFinite(numeric) ? numeric : null
}
function parseStatRows(rawValue: string | null) {
if (!rawValue) return [] as Record<string, string>[]
try {
const parsed = JSON.parse(rawValue)
if (!Array.isArray(parsed)) return []
return parsed.flatMap((item) => {
const rowValue = item && typeof item === 'object' ? (item as { rowValue?: unknown }).rowValue : null
if (!Array.isArray(rowValue)) return []
const row: Record<string, string> = {}
rowValue.forEach((cell) => {
if (!cell || typeof cell !== 'object') return
const record = cell as { label?: unknown; value?: unknown }
const label = normalized(record.label)
if (!label) return
const value = Array.isArray(record.value)
? record.value.map((entry) => normalized((entry as { name?: unknown })?.name ?? entry)).filter(Boolean).join(',')
: normalized(record.value)
row[label] = value
})
return Object.keys(row).length > 0 ? [row] : []
})
} catch {
return []
}
}
function isFullCase(type: string, detail: string) {
const text = canonical(`${type}${detail}`)
return text.includes(canonical('新品/优化策划全案'))
|| text.includes(canonical('策划方案+主图sku详情全套'))
|| text.includes(canonical('全套任务(主图sku详情/策划方案/买家秀)'))
}
function findArtRule(type: string, detail: string) {
const typeKey = canonical(type)
const detailKey = canonical(detail)
return ART_RULES.find((rule) => {
const ruleType = canonical(rule.type)
return (typeKey === ruleType || typeKey.includes(ruleType) || ruleType.includes(typeKey))
&& detailKey === canonical(rule.detail)
}) || null
}
function artCapGroup(rule: ArtRule) {
if (!rule.capDays) return null
const type = canonical(rule.type)
if (rule.capDays === 7) return '策划全案7天封顶组'
if (rule.capDays === 3) return '出图3天封顶组'
if (rule.capDays === 2 && type.includes(canonical('模型调整'))) return '模型调整2天封顶组'
if (rule.capDays === 2 && ['批量调整', '主图(平面)', '详情(平面)'].some((name) => type.includes(canonical(name)))) {
return '平面基础2天封顶组'
}
return `${rule.capDays}天封顶组`
}
function resolveNewArtStandardDays(fields: DingWorkflowDetailFormFields) {
const configs = [
{ rows: parseStatRows(fields.planningTaskStats), type: '策划类型', detail: '策划细分', quantity: '策划数量' },
{ rows: parseStatRows(fields.imageTaskStats), type: '出图类型', detail: '出图细分', quantity: '出图数量' },
{ rows: parseStatRows(fields.graphicTaskStats), type: '平面类型', detail: '平面细分', quantity: '平面数量' },
]
const entries: Array<{ type: string; detail: string; quantity: number; rule: ArtRule; capGroup: string | null }> = []
for (const config of configs) {
for (const row of config.rows) {
const type = normalized(row[config.type])
const detail = normalized(row[config.detail])
const parsedQuantity = Number(normalized(row[config.quantity]))
const quantity = Number.isFinite(parsedQuantity) ? parsedQuantity : 1
if (quantity <= 0) continue
if (isFullCase(type, detail)) return 7
const rule = findArtRule(type, detail)
if (rule) entries.push({ type, detail, quantity, rule, capGroup: artCapGroup(rule) })
}
}
const direct: number[] = []
const capGroups = new Map<string, { capDays: number; entries: typeof entries }>()
const noCrossGroups = new Map<string, typeof entries>()
const uniqueGroups = new Map<string, typeof entries[number]>()
entries.forEach((entry) => {
if (entry.capGroup && entry.rule.capDays) {
const bucket = capGroups.get(entry.capGroup) || { capDays: entry.rule.capDays, entries: [] }
bucket.entries.push(entry)
capGroups.set(entry.capGroup, bucket)
} else if (entry.rule.noCrossStack) {
noCrossGroups.set(entry.type, [...(noCrossGroups.get(entry.type) || []), entry])
} else if (entry.rule.noQuantity) {
const key = `${entry.type}||${entry.detail}`
const current = uniqueGroups.get(key)
if (!current || entry.rule.days > current.rule.days) uniqueGroups.set(key, entry)
} else {
direct.push(roundTwo(entry.rule.days * entry.quantity))
}
})
let total = roundTwo(direct.reduce((sum, value) => sum + value, 0))
capGroups.forEach((bucket) => {
const byKey = new Map<string, number>()
bucket.entries.forEach((entry) => {
const key = `${entry.type}|${entry.detail}`
const value = entry.rule.byQuantity ? roundTwo(entry.rule.days * entry.quantity) : entry.rule.days
byKey.set(key, roundTwo((byKey.get(key) || 0) + value))
})
total += roundTwo(Math.min(bucket.capDays, Array.from(byKey.values()).reduce((sum, value) => sum + value, 0)))
})
noCrossGroups.forEach((entriesInGroup) => {
total += Math.max(0, ...entriesInGroup.map((entry) => entry.rule.byQuantity ? roundTwo(entry.rule.days * entry.quantity) : entry.rule.days))
})
uniqueGroups.forEach((entry) => { total += entry.rule.days })
return total > 0 ? roundTwo(total) : null
}
function normalizeLegacySourceType(value: string | null) {
const text = normalized(value)
if (!text) return ''
if (text.includes('原图')) return '需要原图'
if (text.includes('非原图') || text.includes('无需')) return '不需要原图'
return text.replace(/\s*\+\s*/g, '+')
}
function resolveLegacyArtStandardDays(fields: DingWorkflowDetailFormFields, startedAt: number) {
const demandType = normalized(fields.demandType)
const sourceType = normalizeLegacySourceType(fields.sourceType)
const candidates = LEGACY_ART_RULES.filter(([ruleDemand, ruleSource, effectiveFrom]) => (
ruleDemand === demandType
&& normalizeLegacySourceType(ruleSource) === sourceType
&& (!effectiveFrom || startedAt >= Date.parse(`${String(effectiveFrom).replace(' ', 'T')}+08:00`))
))
return candidates.length ? Number(candidates.at(-1)?.[3]) : null
}
function taskTime(task: OngoingDingWorkflowActiveTask, field: 'createdAt' | 'finishedAt') {
const value = task[field]
const timestamp = parseIsoTime(value)
return timestamp === null ? null : { value: value as string, timestamp }
}
function earliestTaskTime(tasks: OngoingDingWorkflowActiveTask[], names: string[], field: 'createdAt' | 'finishedAt') {
return tasks.flatMap((task) => names.includes(task.nodeName) && taskTime(task, field) ? [taskTime(task, field)!] : [])
.sort((a, b) => a.timestamp - b.timestamp)[0] || null
}
function latestTaskTime(tasks: OngoingDingWorkflowActiveTask[], names: string[], after: number | null) {
return tasks.flatMap((task) => names.includes(task.nodeName) && taskTime(task, 'finishedAt') ? [taskTime(task, 'finishedAt')!] : [])
.filter((entry) => after === null || entry.timestamp >= after)
.sort((a, b) => b.timestamp - a.timestamp)[0] || null
}
function normalizeContentRuleValue(value: unknown) {
const text = normalized(value)
const aliases: Record<string, string> = {
'布景拍摄': '布局拍摄',
'抖音-千川素材视频': '抖音-千川素材',
'抖音-账号维护视频(内容部专用)': '抖音-账号维护(内容部专用)',
'淘系-逛逛视频': '淘系-逛逛',
'淘系主图视频': '淘系主图',
}
return aliases[text] || text
}
function matchContentQuantity(rule: string, quantity: number | null) {
if (!rule || rule === '/') return true
if (quantity === null) return false
if (rule === '>0<=20') return quantity > 0 && quantity <= 20
if (rule === '>20') return quantity > 20
return quantity === Number(rule)
}
function resolveContentRule(fields: DingWorkflowDetailFormFields, createdAt: number) {
const demandType = normalizeContentRuleValue(fields.demandType)
const taskType = normalizeContentRuleValue(fields.taskType) || '布局拍摄'
const installVideo = normalizeContentRuleValue(fields.installVideo)
let quantity = parseFirstNumber(fields.quantity)
if (quantity === null && demandType === '实拍-策划专用') quantity = parseFirstNumber(fields.cameraQuantity) ?? 1
const candidates = CONTENT_RULES.filter((rule) => {
if (rule.demandType !== demandType || rule.taskType !== taskType) return false
if (rule.installVideo !== '/' && rule.installVideo !== installVideo) return false
if (!matchContentQuantity(rule.quantity, quantity)) return false
if (rule.effectiveFrom && createdAt < Date.parse(`${rule.effectiveFrom.replace(' ', 'T')}+08:00`)) return false
if (rule.effectiveTo && createdAt > Date.parse(`${rule.effectiveTo.replace(' ', 'T')}+08:00`)) return false
return true
})
if (candidates.length > 0) {
return candidates.sort((a, b) => Number(b.installVideo !== '/') + Number(b.quantity !== '/') - Number(a.installVideo !== '/') - Number(a.quantity !== '/'))[0]
}
if (!['安装视频', '实拍-策划专用'].includes(demandType)) return null
return CONTENT_RULES.filter((rule) => (
rule.demandType === demandType
&& rule.taskType === taskType
&& (!rule.effectiveFrom || createdAt >= Date.parse(`${rule.effectiveFrom.replace(' ', 'T')}+08:00`))
&& (!rule.effectiveTo || createdAt <= Date.parse(`${rule.effectiveTo.replace(' ', 'T')}+08:00`))
)).sort((a, b) => a.totalHours - b.totalHours)[0] || null
}
function finishAssessment(
standardDuration: number | null,
durationUnit: 'day' | 'hour',
startedAt: { value: string; timestamp: number } | null,
finishedAt: { value: string; timestamp: number } | null,
startNodeLabel: string,
endNodeLabel: string,
): DingWorkflowDeliveryAssessment {
const divisor = durationUnit === 'day' ? 86_400_000 : 3_600_000
const actualDuration = startedAt && finishedAt
? roundTwo(Math.max(0, finishedAt.timestamp - startedAt.timestamp) / divisor)
: null
return {
status: standardDuration !== null && actualDuration !== null
? actualDuration <= standardDuration ? 'on_time' : 'overtime'
: 'pending',
standardDuration,
durationUnit,
actualDuration,
startedAt: startedAt?.value || null,
finishedAt: finishedAt?.value || null,
startNodeLabel,
endNodeLabel,
}
}
export function assessDingWorkflowDelivery(input: {
processCode: string
sourceCreatedAt: string | null
sourceFinishedAt: string | null
formFields: DingWorkflowDetailFormFields
tasks: OngoingDingWorkflowActiveTask[]
}): DingWorkflowDeliveryAssessment {
const createdAt = parseIsoTime(input.sourceCreatedAt)
if (input.processCode === ART_PROCESS_CODE) {
const start = earliestTaskTime(input.tasks, ['品牌策划主管分配任务'], 'createdAt')
const finish = latestTaskTime(input.tasks, ['品牌策划主管审核'], start?.timestamp ?? null)
const standard = start
? start.timestamp >= ART_NEW_STANDARD_FROM
? resolveNewArtStandardDays(input.formFields)
: resolveLegacyArtStandardDays(input.formFields, start.timestamp)
: null
return finishAssessment(standard, 'day', start, finish, '品牌策划主管分配任务', '品牌策划主管审核')
}
if (input.processCode === CONTENT_PROCESS_CODE) {
const assignmentNames = ['内容主管分配任务', '内容负责人分配任务', '内容部负责人']
const start = earliestTaskTime(input.tasks, assignmentNames, 'createdAt')
|| earliestTaskTime(input.tasks, ['编导'], 'createdAt')
|| (createdAt === null || !input.sourceCreatedAt ? null : { value: input.sourceCreatedAt, timestamp: createdAt })
const rule = createdAt === null ? null : resolveContentRule(input.formFields, createdAt)
const endNodeLabel = rule?.actualCalcTo === 'contentReview' ? '内容主管审核' : '编导审核'
const endNames = rule?.actualCalcTo === 'contentReview'
? ['内容主管审核', '内容负责人审核', '内容部负责人审核', '品牌中心负责人审核']
: ['编导审核']
const taskFinish = latestTaskTime(input.tasks, endNames, start?.timestamp ?? null)
const sourceFinishTimestamp = parseIsoTime(input.sourceFinishedAt)
const finish = taskFinish || (sourceFinishTimestamp === null || !input.sourceFinishedAt
? null
: { value: input.sourceFinishedAt, timestamp: sourceFinishTimestamp })
return finishAssessment(rule?.totalHours ?? null, 'hour', start, finish, '内容主管分配任务', endNodeLabel)
}
return finishAssessment(null, 'hour', null, null, '分配任务', '审核')
}