diff --git a/src/content/market/api-client.ts b/src/content/market/api-client.ts index 92cfa5a..3d38c0f 100644 --- a/src/content/market/api-client.ts +++ b/src/content/market/api-client.ts @@ -109,7 +109,7 @@ export function mapAuthorAseInfoResponse(payload: unknown): MarketApiResult { data.personal_avg_search_after_view_rate ); - if (!singleVideoAfterSearchRate || !personalVideoAfterSearchRate) { + if (!singleVideoAfterSearchRate && !personalVideoAfterSearchRate) { return { success: false, reason: "missing-rate" @@ -119,8 +119,8 @@ export function mapAuthorAseInfoResponse(payload: unknown): MarketApiResult { return { success: true, rates: { - singleVideoAfterSearchRate, - personalVideoAfterSearchRate + ...(singleVideoAfterSearchRate ? { singleVideoAfterSearchRate } : {}), + ...(personalVideoAfterSearchRate ? { personalVideoAfterSearchRate } : {}) } }; } diff --git a/src/content/market/csv-exporter.ts b/src/content/market/csv-exporter.ts index 7251e8d..b4cb64b 100644 --- a/src/content/market/csv-exporter.ts +++ b/src/content/market/csv-exporter.ts @@ -43,9 +43,40 @@ const RATE_COLUMNS: CsvColumn[] = [ } ]; +const BACKEND_METRIC_COLUMNS: CsvColumn[] = [ + { + header: "看后搜率", + readValue: (record: MarketRecord) => + record.backendMetrics?.afterViewSearchRate ?? "" + }, + { + header: "看后搜数", + readValue: (record: MarketRecord) => + record.backendMetrics?.afterViewSearchCount ?? "" + }, + { + header: "新增A3数", + readValue: (record: MarketRecord) => + record.backendMetrics?.a3IncreaseCount ?? "" + }, + { + header: "新增A3率", + readValue: (record: MarketRecord) => + record.backendMetrics?.newA3Rate ?? "" + }, + { + header: "CPA3", + readValue: (record: MarketRecord) => record.backendMetrics?.cpa3 ?? "" + }, + { + header: "cp_search", + readValue: (record: MarketRecord) => record.backendMetrics?.cpSearch ?? "" + } +]; + export function buildMarketCsv(records: MarketRecord[]): string { const baseColumns = buildBaseColumns(records); - const csvColumns = [...baseColumns, ...RATE_COLUMNS]; + const csvColumns = [...baseColumns, ...RATE_COLUMNS, ...BACKEND_METRIC_COLUMNS]; const headerLine = csvColumns.map((column) => column.header).join(","); const rowLines = records.map((record) => csvColumns.map((column) => escapeCsvCell(column.readValue(record))).join(",") @@ -57,10 +88,11 @@ export function buildMarketCsv(records: MarketRecord[]): string { function buildBaseColumns(records: MarketRecord[]): CsvColumn[] { const orderedHeaders: string[] = []; const seenHeaders = new Set(); + const excludedHeaders = new Set(["代表视频"]); records.forEach((record) => { Object.keys(record.exportFields ?? {}).forEach((header) => { - if (seenHeaders.has(header)) { + if (seenHeaders.has(header) || excludedHeaders.has(header)) { return; } diff --git a/src/content/market/types.ts b/src/content/market/types.ts index 0e8edf0..4f18fa4 100644 --- a/src/content/market/types.ts +++ b/src/content/market/types.ts @@ -60,7 +60,7 @@ export type MarketApiFailureReason = export type MarketApiSuccessResult = { success: true; - rates: Required; + rates: AfterSearchRates; }; export type MarketApiFailureResult = { diff --git a/tests/csv-exporter.test.ts b/tests/csv-exporter.test.ts index 1dd9991..47cde00 100644 --- a/tests/csv-exporter.test.ts +++ b/tests/csv-exporter.test.ts @@ -15,7 +15,13 @@ describe("csv-exporter", () => { "地区", "21-60s报价", "单视频看后搜率", - "个人视频看后搜率" + "个人视频看后搜率", + "看后搜率", + "看后搜数", + "新增A3数", + "新增A3率", + "CPA3", + "cp_search" ].join(",") ); }); @@ -27,10 +33,19 @@ describe("csv-exporter", () => { authorName: "Alice", exportFields: { 达人信息: "Alice", + 代表视频: "示例视频", 粉丝数: "100w", "21-60s报价": "¥450,000" }, status: "success", + backendMetrics: { + a3IncreaseCount: "78,366.22", + afterViewSearchCount: "9,689.96", + afterViewSearchRate: "0.36%", + cpSearch: "14.46", + cpa3: "1.79", + newA3Rate: "3.44%" + }, rates: { singleVideoAfterSearchRate: "0.5%-1%", personalVideoAfterSearchRate: "1% - 3%" @@ -40,11 +55,56 @@ describe("csv-exporter", () => { const [headerLine, rowLine] = csv.split("\n"); expect(headerLine).toBe( - ["达人信息", "粉丝数", "21-60s报价", "单视频看后搜率", "个人视频看后搜率"].join( - "," - ) + [ + "达人信息", + "粉丝数", + "21-60s报价", + "单视频看后搜率", + "个人视频看后搜率", + "看后搜率", + "看后搜数", + "新增A3数", + "新增A3率", + "CPA3", + "cp_search" + ].join(",") ); - expect(rowLine).toBe('Alice,100w,"¥450,000",0.5% - 1%,1% - 3%'); + expect(rowLine).toBe( + 'Alice,100w,"¥450,000",0.5% - 1%,1% - 3%,0.36%,"9,689.96","78,366.22",3.44%,1.79,14.46' + ); + }); + + test("omits the representative video column from exported page fields", () => { + const csv = buildMarketCsv([ + { + authorId: "123", + authorName: "Alice", + exportFields: { + 达人信息: "Alice", + 代表视频: "示例视频", + 粉丝数: "100w" + }, + status: "success" + } satisfies MarketRecord + ]); + + const [headerLine, rowLine] = csv.split("\n"); + + expect(headerLine).toBe( + [ + "达人信息", + "粉丝数", + "单视频看后搜率", + "个人视频看后搜率", + "看后搜率", + "看后搜数", + "新增A3数", + "新增A3率", + "CPA3", + "cp_search" + ].join(",") + ); + expect(rowLine).toBe("Alice,100w,,,,,,,,"); }); test("escapes commas and quotes", () => { @@ -77,7 +137,7 @@ describe("csv-exporter", () => { ]); const [, rowLine] = csv.split("\n"); - expect(rowLine).toBe("123,Alice,,,,"); + expect(rowLine).toBe("123,Alice,,,,,,,,,,"); }); test("uses normalized display values in export rows", () => { @@ -97,4 +157,21 @@ describe("csv-exporter", () => { expect(rowLine).toContain("0.5% - 1%"); expect(rowLine).toContain("0.02% - 0.1%"); }); + + test("emits empty backend metric cells when backend metrics are absent", () => { + const csv = buildMarketCsv([ + { + authorId: "123", + authorName: "Alice", + status: "success", + rates: { + singleVideoAfterSearchRate: "0.5%-1%", + personalVideoAfterSearchRate: "0.02 - 0.1%" + } + } satisfies MarketRecord + ]); + + const [, rowLine] = csv.split("\n"); + expect(rowLine).toBe("123,Alice,,,0.5% - 1%,0.02% - 0.1%,,,,,,"); + }); }); diff --git a/tests/market-api-client.test.ts b/tests/market-api-client.test.ts index e28f29d..2662cd0 100644 --- a/tests/market-api-client.test.ts +++ b/tests/market-api-client.test.ts @@ -33,12 +33,10 @@ describe("market-api-client", () => { }); }); - test("returns a missing-rate failure when the payload omits a required field", () => { + test("returns a missing-rate failure when the payload omits both rate fields", () => { expect( mapAuthorAseInfoResponse({ - data: { - avg_search_after_view_rate: "<0.02%" - } + data: {} }) ).toMatchObject({ success: false, @@ -46,6 +44,21 @@ describe("market-api-client", () => { }); }); + test("maps a partially populated payload into partial rates", () => { + expect( + mapAuthorAseInfoResponse({ + data: { + personal_avg_search_after_view_rate: "0.02 - 0.1%" + } + }) + ).toMatchObject({ + success: true, + rates: { + personalVideoAfterSearchRate: "0.02% - 0.1%" + } + }); + }); + test("returns a request-failed result for non-ok responses", async () => { const client = createMarketApiClient({ fetchImpl: async () => ({