From 2869c9a27c509d76dc2ab7acfd2a480c0dfdce4e Mon Sep 17 00:00:00 2001 From: jingchuan Date: Fri, 24 Jul 2026 14:48:52 +0800 Subject: [PATCH] fix(oa-board): enforce brand planning field sources --- .../oa-brand-planning-board.service.ts | 156 ++++++++---------- 1 file changed, 72 insertions(+), 84 deletions(-) diff --git a/furniture-3d-system/src/server/services/oa-brand-planning-board.service.ts b/furniture-3d-system/src/server/services/oa-brand-planning-board.service.ts index 9a27dbcb..322bda37 100644 --- a/furniture-3d-system/src/server/services/oa-brand-planning-board.service.ts +++ b/furniture-3d-system/src/server/services/oa-brand-planning-board.service.ts @@ -124,17 +124,12 @@ type ProductSkuSource = { type ProductSource = { id: string spu: string - brand: string - purchaser: string payload: unknown - createdAt: Date - updatedAt: Date profile: { productCategory: string productPlanning: string spuOwner: string purchaseOwner: string - visualPlanning: string structureDesigner: string } | null skuItems: ProductSkuSource[] @@ -325,13 +320,6 @@ function findTaskDate( return kind === 'started' ? task.startedAtSource : task.finishedAtSource } -function findTaskOwner(flow: OaBoardProjectFlowInstanceInput | null, names: readonly string[]) { - const task = flow?.taskRecords.find((item) => - names.some((name) => (item.nodeName ?? '').includes(name)), - ) - return firstText(task?.ownerLabel) -} - function calculateDurationDays(start: Date | null, end: Date | null) { if (!start || !end) return null const difference = end.getTime() - start.getTime() @@ -340,7 +328,6 @@ function calculateDurationDays(start: Date | null, end: Date | null) { } function getGraphSpu(graph: OaBoardProjectGraphInput) { - if (graph.spu) return graph.spu.trim().toUpperCase() const reviewFlow = findFlow(graph, 'review') return readFlowField(reviewFlow, ['SPU'])?.toUpperCase() ?? null } @@ -355,12 +342,43 @@ function getFactoryLeadTime(payload: ProductPayload, sku: ProductSkuSource | nul return toText(asRecord(row).leadTime) } -function getProductImage(payload: ProductPayload, sku: ProductSkuSource | null) { - return firstText( - sku?.image, - readPathText(payload, 'openingReference', 'productImage'), - payload.previewImage, +function readFlowTableField( + flow: OaBoardProjectFlowInstanceInput | null, + tableFieldName: string, + columnFieldName: string, +) { + const tableField = flow?.fieldSnapshots.find((field) => field.fieldName === tableFieldName) + const rawValue = firstText( + tableField?.normalizedValue, + tableField?.fieldValue, + tableField?.fieldExtValue, ) + if (!rawValue) return null + + try { + const rows = JSON.parse(rawValue) + if (!Array.isArray(rows)) return null + + const values = rows + .flatMap((row) => { + const rowValue = asRecord(row) + const cells = rowValue.ROWVALUE ?? rowValue.rowValue + return Array.isArray(cells) ? cells : [] + }) + .filter((cell) => { + const cellValue = asRecord(cell) + return toText(cellValue.LABEL ?? cellValue.label) === columnFieldName + }) + .map((cell) => { + const cellValue = asRecord(cell) + return toDisplayFieldValue(cellValue.VALUE ?? cellValue.value) + }) + .filter((value): value is string => Boolean(value)) + + return values.length > 0 ? Array.from(new Set(values)).join(';') : null + } catch { + return null + } } function buildSnapshotRows( @@ -377,7 +395,7 @@ function buildSnapshotRows( return products.flatMap((product) => { const payload = asRecord(product.payload) const profile = product.profile - const productSkus = product.skuItems.length > 0 ? product.skuItems : [null] + const productSkus = product.skuItems const graph = graphBySpu.get(product.spu.trim().toUpperCase()) const reviewFlow = findFlow(graph, 'review') const artFlow = findFlow(graph, 'art') @@ -388,88 +406,63 @@ function buildSnapshotRows( const taskAssignedAt = dateFromUnknown( readFlowField(artFlow, ['品牌策划主管分配任务结束时间']), - ) ?? findTaskDate(artFlow, ['品牌策划主管分配任务'], 'finished') - const planCompletedAt = dateFromUnknown( - readFlowField(artFlow, ['出策划案结束时间']), - ) ?? findTaskDate(artFlow, ['出策划案'], 'finished') - const visualCompletedAt = dateFromUnknown( - readFlowField(artFlow, ['出图结束时间']), - ) ?? findTaskDate(artFlow, ['出图'], 'finished') - const detailCompletedAt = dateFromUnknown( - readFlowField(artFlow, ['平面结束时间']), - ) ?? findTaskDate(artFlow, ['平面'], 'finished') - const planningReviewAt = - findTaskDate(artFlow, ['品牌策划主管审核'], 'finished') ?? - findTaskDate(artFlow, ['品牌策划主管审核'], 'started') ?? - artFlow?.finishedAtSource ?? - null + ) + const planCompletedAt = dateFromUnknown(readFlowField(artFlow, ['出策划案结束时间'])) + const visualCompletedAt = dateFromUnknown(readFlowField(artFlow, ['出图结束时间'])) + const detailCompletedAt = dateFromUnknown(readFlowField(artFlow, ['平面结束时间'])) + const planningTaskAssignedAt = findTaskDate( + artFlow, + ['品牌策划主管分配任务'], + 'finished', + ) + const planningReviewAt = findTaskDate(artFlow, ['品牌策划主管审核'], 'finished') return productSkus.map((sku) => { const rowKey = `${product.id}:${sku?.skuItemId || sku?.id || 'PRODUCT'}` const adaptableBrands = toTextList(payload.adaptableBrands) - const fullPlanPath = firstText( - readFlowField(artFlow, ['全案路径']), - payload.flatOutputPath, - payload.output3dPath, - ) return { row_key: rowKey, product_id: product.id, sku_id: sku?.skuItemId || sku?.id || null, - image_url: getProductImage(payload, sku), - demand_type: firstText( - readPathText(payload, 'openingReference', 'demandType'), - payload.demandType, - readFlowField(reviewFlow, ['需求类型']), - ), + image_url: sku?.image ?? null, + demand_type: readPathText(payload, 'openingReference', 'demandType'), spu: product.spu || null, sku: sku?.skuCode || null, spec_name: sku?.specName || null, - size: sku?.size || firstText(payload.size, readPathText(payload, 'openingReference', 'size')), - color: sku?.color || toText(payload.color), - adaptable_brands: adaptableBrands.length > 0 ? adaptableBrands.join(';') : product.brand || null, + size: sku?.size || null, + color: sku?.color || null, + adaptable_brands: adaptableBrands.length > 0 ? adaptableBrands.join(';') : null, barcode: sku?.barcode || null, - category_name: firstText( - profile?.productCategory, - payload.productCategory, - readPathText(payload, 'openingReference', 'productCategory'), - ), - created_date: toMysqlDateTime(reviewFlow?.createdAtSource ?? product.createdAt), - structure_designer: firstText( - profile?.structureDesigner, - readPathText(payload, 'openingReference', 'structureDesigner'), - payload.structureDesigner, - ), - product_planner: firstText(profile?.productPlanning, payload.productPlanning), - spu_owner: firstText(profile?.spuOwner, payload.spuOwner), - purchase_owner: firstText(profile?.purchaseOwner, payload.purchaseOwner, product.purchaser), + category_name: toText(profile?.productCategory), + created_date: toMysqlDateTime(reviewFlow?.createdAtSource), + structure_designer: toText(profile?.structureDesigner), + product_planner: toText(profile?.productPlanning), + spu_owner: toText(profile?.spuOwner), + purchase_owner: toText(profile?.purchaseOwner), estimated_shipment_date: getFactoryLeadTime(payload, sku), product_detail_link: `/intake/non-integrated/${encodeURIComponent(product.id)}`, - approval_stage: graph?.currentApprovalStageName ?? graph?.currentStageName ?? null, - opening_code: firstText(graph?.openingCode, payload.demandFlowNo), + approval_stage: graph?.currentApprovalStageName ?? null, + opening_code: readFlowField(reviewFlow, ['开款编号']), market_insight_link: readFlowField(reviewFlow, ['市场洞察分析企划表链接']), - planning_owner: firstText( - readFlowField(artFlow, ['策划案负责人']), - findTaskOwner(artFlow, ['出策划案']), - profile?.visualPlanning, - payload.visualPlanning, - ), + planning_owner: readFlowField(artFlow, ['策划案负责人']), planning_received_at: toMysqlDateTime(taskAssignedAt), planning_completed_at: toMysqlDateTime(planCompletedAt), - visual_output_owner: firstText( - readFlowField(artFlow, ['出图任务统计-全案策划']), - findTaskOwner(artFlow, ['出图']), + visual_output_owner: readFlowTableField( + artFlow, + '出图任务统计', + '全案策划', ), visual_output_completed_at: toMysqlDateTime(visualCompletedAt), - detail_owner: firstText( - readFlowField(artFlow, ['平面任务统计-全案策划']), - findTaskOwner(artFlow, ['平面']), + detail_owner: readFlowTableField( + artFlow, + '平面任务统计', + '全案策划', ), detail_completed_at: toMysqlDateTime(detailCompletedAt), - full_plan_path: fullPlanPath, - total_duration_days: calculateDurationDays(taskAssignedAt, planningReviewAt), - last_source_updated_at: toMysqlDateTime(graph?.lastSourceUpdatedAt ?? product.updatedAt), + full_plan_path: readFlowField(artFlow, ['全案路径']), + total_duration_days: calculateDurationDays(planningTaskAssignedAt, planningReviewAt), + last_source_updated_at: toMysqlDateTime(graph?.lastSourceUpdatedAt), sync_version: syncVersion, } }) @@ -493,18 +486,13 @@ async function loadProducts(): Promise { select: { id: true, spu: true, - brand: true, payload: true, - createdAt: true, - updatedAt: true, - purchaser: true, profile: { select: { productCategory: true, productPlanning: true, spuOwner: true, purchaseOwner: true, - visualPlanning: true, structureDesigner: true, }, },