import { escapeCsvCell } from "../../shared/csv"; import { buildMarketCsvColumns, type CsvColumn } from "./csv-exporter"; import type { AudienceProfileDistributionItem, AudienceProfileExportRow, AudienceProfileKind, AudienceProfileResult, BusinessAbilityDurationKind, BusinessAbilityEstimateMetrics, BusinessAbilityVideoKind, BusinessAbilityVideoMetrics } from "./audience-profile-types"; type AudienceProfileCsvColumn = { header: string; readValue: (row: AudienceProfileExportRow) => string; }; const PROFILE_LAYOUTS: Array<{ includeGender: boolean; kind: AudienceProfileKind; label: string; }> = [ { includeGender: true, kind: "audience", label: "观众画像" }, { includeGender: true, kind: "fans", label: "粉丝画像" }, { includeGender: false, kind: "longtimeFans", label: "铁粉画像" } ]; const GENDER_LABELS = ["男性", "女性"]; const AGE_LABELS = ["18-23", "24-30", "31-40", "41-50", "50+"]; const CITY_TIER_LABELS = [ "一线城市", "新一线城市", "二线城市", "三线城市", "四线城市", "五线城市" ]; const CROWD_LABELS = [ "精致妈妈", "都市银发", "新锐白领", "资深中产", "都市蓝领", "Z世代", "小镇中老年", "小镇青年" ]; const BUSINESS_VIDEO_LAYOUTS: Array<{ key: BusinessAbilityVideoKind; label: string; }> = [ { key: "personalVideo", label: "个人视频" }, { key: "xingtuVideo", label: "星图视频" } ]; const BUSINESS_VIDEO_METRIC_LAYOUTS: Array<{ key: keyof BusinessAbilityVideoMetrics; label: string; }> = [ { key: "medianPlay", label: "播放量中位数" }, { key: "finishRate", label: "完播率" }, { key: "interactionRate", label: "互动率" }, { key: "publishedItems", label: "发布作品" }, { key: "averageDuration", label: "平均时长" }, { key: "averageLike", label: "平均点赞" }, { key: "averageComment", label: "平均评论" }, { key: "averageShare", label: "平均转发" } ]; const BUSINESS_ESTIMATE_LAYOUTS: Array<{ key: BusinessAbilityDurationKind; label: string; }> = [ { key: "oneToTwenty", label: "1-20s视频" }, { key: "twentyToSixty", label: "20-60s视频" }, { key: "overSixty", label: "60s以上视频" } ]; const BUSINESS_ESTIMATE_METRIC_LAYOUTS: Array<{ key: keyof BusinessAbilityEstimateMetrics; label: string; }> = [ { key: "expectedCpm", label: "预期CPM" }, { key: "expectedCpe", label: "预期CPE" }, { key: "expectedPlay", label: "预期播放量" }, { key: "hotRate", label: "爆文率" } ]; export function buildAudienceProfileCsv( rows: AudienceProfileExportRow[] ): string { const marketColumns = buildMarketCsvColumns(rows.map((row) => row.record)); const csvColumns = [ ...marketColumns.map(toMarketColumn), ...buildBusinessAbilityColumns(), ...PROFILE_LAYOUTS.flatMap((layout) => buildProfileColumns(layout)) ]; const headerLine = csvColumns.map((column) => column.header).join(","); const rowLines = rows.map((row) => csvColumns.map((column) => escapeCsvCell(column.readValue(row))).join(",") ); return [headerLine, ...rowLines].join("\n"); } function buildBusinessAbilityColumns(): AudienceProfileCsvColumn[] { return [ ...BUSINESS_VIDEO_LAYOUTS.flatMap((videoLayout) => BUSINESS_VIDEO_METRIC_LAYOUTS.map((metricLayout) => ({ header: `商业能力-${videoLayout.label}-${metricLayout.label}`, readValue: (row: AudienceProfileExportRow) => readBusinessVideoValue(row, videoLayout.key, metricLayout.key) })) ), ...BUSINESS_ESTIMATE_LAYOUTS.flatMap((durationLayout) => BUSINESS_ESTIMATE_METRIC_LAYOUTS.map((metricLayout) => ({ header: `商业能力-${durationLayout.label}-${metricLayout.label}`, readValue: (row: AudienceProfileExportRow) => readBusinessEstimateValue(row, durationLayout.key, metricLayout.key) })) ) ]; } function readBusinessVideoValue( row: AudienceProfileExportRow, videoKey: BusinessAbilityVideoKind, metricKey: keyof BusinessAbilityVideoMetrics ): string { const businessAbility = row.businessAbility; if (!businessAbility || businessAbility.status !== "success") { return ""; } return businessAbility.videos[videoKey]?.[metricKey] ?? ""; } function readBusinessEstimateValue( row: AudienceProfileExportRow, durationKey: BusinessAbilityDurationKind, metricKey: keyof BusinessAbilityEstimateMetrics ): string { const businessAbility = row.businessAbility; if (!businessAbility || businessAbility.status !== "success") { return ""; } return businessAbility.estimates[durationKey]?.[metricKey] ?? ""; } function toMarketColumn(column: CsvColumn): AudienceProfileCsvColumn { return { header: column.header, readValue: (row) => column.readValue(row.record) }; } function buildProfileColumns(layout: { includeGender: boolean; kind: AudienceProfileKind; label: string; }): AudienceProfileCsvColumn[] { const columns: AudienceProfileCsvColumn[] = []; if (layout.includeGender) { columns.push( ...buildFixedDistributionColumns( layout.label, layout.kind, "gender", GENDER_LABELS ) ); } columns.push( ...buildFixedDistributionColumns(layout.label, layout.kind, "age", AGE_LABELS), ...buildFixedDistributionColumns( layout.label, layout.kind, "cityTier", CITY_TIER_LABELS ), ...buildFixedDistributionColumns(layout.label, layout.kind, "crowd", CROWD_LABELS) ); return columns; } function buildFixedDistributionColumns( prefix: string, kind: AudienceProfileKind, key: "age" | "cityTier" | "crowd" | "gender", labels: string[] ): AudienceProfileCsvColumn[] { return labels.map((label) => ({ header: `${prefix}-${label}占比`, readValue: (row) => readDistributionValue(row.profiles[kind], key, label) })); } function readDistributionValue( profile: AudienceProfileResult, key: "age" | "cityTier" | "crowd" | "gender", label: string ): string { if (profile.status !== "success") { return ""; } return ( readProfileDistributionItems(profile, key).find( (candidate) => candidate.label === label )?.value ?? "0%" ); } function readProfileDistributionItems( profile: AudienceProfileResult, key: "age" | "cityTier" | "crowd" | "gender" ): AudienceProfileDistributionItem[] { return profile.status === "success" ? profile[key] ?? [] : []; }