Allow partial market rates in CSV export
This commit is contained in:
parent
c7ae2fbfcb
commit
2f77199920
@ -109,7 +109,7 @@ export function mapAuthorAseInfoResponse(payload: unknown): MarketApiResult {
|
|||||||
data.personal_avg_search_after_view_rate
|
data.personal_avg_search_after_view_rate
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!singleVideoAfterSearchRate || !personalVideoAfterSearchRate) {
|
if (!singleVideoAfterSearchRate && !personalVideoAfterSearchRate) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
reason: "missing-rate"
|
reason: "missing-rate"
|
||||||
@ -119,8 +119,8 @@ export function mapAuthorAseInfoResponse(payload: unknown): MarketApiResult {
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
rates: {
|
rates: {
|
||||||
singleVideoAfterSearchRate,
|
...(singleVideoAfterSearchRate ? { singleVideoAfterSearchRate } : {}),
|
||||||
personalVideoAfterSearchRate
|
...(personalVideoAfterSearchRate ? { personalVideoAfterSearchRate } : {})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
export function buildMarketCsv(records: MarketRecord[]): string {
|
||||||
const baseColumns = buildBaseColumns(records);
|
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 headerLine = csvColumns.map((column) => column.header).join(",");
|
||||||
const rowLines = records.map((record) =>
|
const rowLines = records.map((record) =>
|
||||||
csvColumns.map((column) => escapeCsvCell(column.readValue(record))).join(",")
|
csvColumns.map((column) => escapeCsvCell(column.readValue(record))).join(",")
|
||||||
@ -57,10 +88,11 @@ export function buildMarketCsv(records: MarketRecord[]): string {
|
|||||||
function buildBaseColumns(records: MarketRecord[]): CsvColumn[] {
|
function buildBaseColumns(records: MarketRecord[]): CsvColumn[] {
|
||||||
const orderedHeaders: string[] = [];
|
const orderedHeaders: string[] = [];
|
||||||
const seenHeaders = new Set<string>();
|
const seenHeaders = new Set<string>();
|
||||||
|
const excludedHeaders = new Set(["代表视频"]);
|
||||||
|
|
||||||
records.forEach((record) => {
|
records.forEach((record) => {
|
||||||
Object.keys(record.exportFields ?? {}).forEach((header) => {
|
Object.keys(record.exportFields ?? {}).forEach((header) => {
|
||||||
if (seenHeaders.has(header)) {
|
if (seenHeaders.has(header) || excludedHeaders.has(header)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -60,7 +60,7 @@ export type MarketApiFailureReason =
|
|||||||
|
|
||||||
export type MarketApiSuccessResult = {
|
export type MarketApiSuccessResult = {
|
||||||
success: true;
|
success: true;
|
||||||
rates: Required<AfterSearchRates>;
|
rates: AfterSearchRates;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MarketApiFailureResult = {
|
export type MarketApiFailureResult = {
|
||||||
|
|||||||
@ -15,7 +15,13 @@ describe("csv-exporter", () => {
|
|||||||
"地区",
|
"地区",
|
||||||
"21-60s报价",
|
"21-60s报价",
|
||||||
"单视频看后搜率",
|
"单视频看后搜率",
|
||||||
"个人视频看后搜率"
|
"个人视频看后搜率",
|
||||||
|
"看后搜率",
|
||||||
|
"看后搜数",
|
||||||
|
"新增A3数",
|
||||||
|
"新增A3率",
|
||||||
|
"CPA3",
|
||||||
|
"cp_search"
|
||||||
].join(",")
|
].join(",")
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -27,10 +33,19 @@ describe("csv-exporter", () => {
|
|||||||
authorName: "Alice",
|
authorName: "Alice",
|
||||||
exportFields: {
|
exportFields: {
|
||||||
达人信息: "Alice",
|
达人信息: "Alice",
|
||||||
|
代表视频: "示例视频",
|
||||||
粉丝数: "100w",
|
粉丝数: "100w",
|
||||||
"21-60s报价": "¥450,000"
|
"21-60s报价": "¥450,000"
|
||||||
},
|
},
|
||||||
status: "success",
|
status: "success",
|
||||||
|
backendMetrics: {
|
||||||
|
a3IncreaseCount: "78,366.22",
|
||||||
|
afterViewSearchCount: "9,689.96",
|
||||||
|
afterViewSearchRate: "0.36%",
|
||||||
|
cpSearch: "14.46",
|
||||||
|
cpa3: "1.79",
|
||||||
|
newA3Rate: "3.44%"
|
||||||
|
},
|
||||||
rates: {
|
rates: {
|
||||||
singleVideoAfterSearchRate: "0.5%-1%",
|
singleVideoAfterSearchRate: "0.5%-1%",
|
||||||
personalVideoAfterSearchRate: "1% - 3%"
|
personalVideoAfterSearchRate: "1% - 3%"
|
||||||
@ -40,11 +55,56 @@ describe("csv-exporter", () => {
|
|||||||
|
|
||||||
const [headerLine, rowLine] = csv.split("\n");
|
const [headerLine, rowLine] = csv.split("\n");
|
||||||
expect(headerLine).toBe(
|
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", () => {
|
test("escapes commas and quotes", () => {
|
||||||
@ -77,7 +137,7 @@ describe("csv-exporter", () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const [, rowLine] = csv.split("\n");
|
const [, rowLine] = csv.split("\n");
|
||||||
expect(rowLine).toBe("123,Alice,,,,");
|
expect(rowLine).toBe("123,Alice,,,,,,,,,,");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("uses normalized display values in export rows", () => {
|
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.5% - 1%");
|
||||||
expect(rowLine).toContain("0.02% - 0.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%,,,,,,");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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(
|
expect(
|
||||||
mapAuthorAseInfoResponse({
|
mapAuthorAseInfoResponse({
|
||||||
data: {
|
data: {}
|
||||||
avg_search_after_view_rate: "<0.02%"
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
).toMatchObject({
|
).toMatchObject({
|
||||||
success: false,
|
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 () => {
|
test("returns a request-failed result for non-ok responses", async () => {
|
||||||
const client = createMarketApiClient({
|
const client = createMarketApiClient({
|
||||||
fetchImpl: async () => ({
|
fetchImpl: async () => ({
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user