refactor: use spread metrics for content data fields

This commit is contained in:
wxs
2026-06-29 16:35:27 +08:00
parent 9eb1fe43cc
commit 0122e63872
10 changed files with 83 additions and 293 deletions
+33 -41
View File
@@ -54,29 +54,7 @@ describe("audience-profile-csv", () => {
hotRate: "缺失"
}
},
status: "success",
videos: {
personalVideo: {
averageComment: "4.5w",
averageDuration: "150s",
averageLike: "113.2w",
averageShare: "26.5w",
finishRate: "15.8%",
interactionRate: "3.9%",
medianPlay: "3738.4w",
publishedItems: "<5"
},
xingtuVideo: {
averageComment: "5.1w",
averageDuration: "170s",
averageLike: "150.3w",
averageShare: "68.4w",
finishRate: "19.9%",
interactionRate: "5.5%",
medianPlay: "4059.7w",
publishedItems: "<5"
}
}
status: "success"
},
record: {
authorId: "123",
@@ -85,6 +63,10 @@ describe("audience-profile-csv", () => {
: "达人 A",
: "300w"
},
spreadMetrics: {
"内容数据-个人视频-近30天-播放量中位数": "10913233",
"内容数据-只看指派-不排除营销流量-星图视频-近90天-作品平均评论数": "7502"
},
status: "success"
}
} satisfies AudienceProfileExportRow
@@ -95,8 +77,10 @@ describe("audience-profile-csv", () => {
expect(headerLine).toContain("达人信息,连接用户数");
expect(headerLine).not.toContain("抓取状态");
expect(headerLine).not.toContain("失败原因");
expect(headerLine).toContain("内容数据-个人视频-播放量中位数");
expect(headerLine).toContain("内容数据-星图视频-平均转发");
expect(headerLine).toContain("内容数据-个人视频-近30天-播放量中位数");
expect(headerLine).toContain(
"内容数据-只看指派-不排除营销流量-星图视频-近90天-作品平均评论数"
);
expect(headerLine).toContain("效果预估-1-20s视频-预期CPM");
expect(headerLine).toContain("效果预估-20-60s视频-爆文率");
expect(headerLine).toContain("效果预估-60s以上视频-预期播放量");
@@ -116,8 +100,15 @@ describe("audience-profile-csv", () => {
expect(headerLine).not.toContain("兴趣TOP");
expect(rowLine).toContain("71.7%");
expect(rowLine).toContain("60%");
expect(readCsvValue(csv, "内容数据-个人视频-播放量中位数")).toBe("3738.4w");
expect(readCsvValue(csv, "内容数据-星图视频-平均转发")).toBe("68.4w");
expect(readCsvValue(csv, "内容数据-个人视频-近30天-播放量中位数")).toBe(
"10913233"
);
expect(
readCsvValue(
csv,
"内容数据-只看指派-不排除营销流量-星图视频-近90天-作品平均评论数"
)
).toBe("7502");
expect(readCsvValue(csv, "效果预估-1-20s视频-预期CPM")).toBe("120.0");
expect(readCsvValue(csv, "效果预估-20-60s视频-爆文率")).toBe("缺失");
});
@@ -202,7 +193,7 @@ describe("audience-profile-csv", () => {
const row = buildSuccessRow();
const csv = buildAudienceProfileCsv([row], {
selectedHeaders: [
"内容数据-个人视频-播放量中位数",
"内容数据-个人视频-近30天-播放量中位数",
"观众画像-男性占比"
]
});
@@ -210,9 +201,9 @@ describe("audience-profile-csv", () => {
const [headerLine, rowLine] = csv.split("\n");
expect(headerLine).toBe(
"达人信息,连接用户数,内容数据-个人视频-播放量中位数,观众画像-男性占比"
"达人信息,连接用户数,内容数据-个人视频-近30天-播放量中位数,观众画像-男性占比"
);
expect(rowLine).toBe("达人 A,300w,3738.4w,71.7%");
expect(rowLine).toBe("达人 A,300w,10913233,71.7%");
expect(headerLine).not.toContain("秒思api-看后搜数");
expect(headerLine).not.toContain("粉丝画像-女性占比");
});
@@ -227,15 +218,15 @@ describe("audience-profile-csv", () => {
}
});
const csv = buildAudienceProfileCsv([row], {
selectedHeaders: ["内容数据-个人视频-播放量中位数"]
selectedHeaders: ["内容数据-个人视频-近30天-播放量中位数"]
});
const [headerLine, rowLine] = csv.split("\n");
expect(headerLine).toBe(
"达人ID,达人名称,导出状态,失败原因,内容数据-个人视频-播放量中位数"
"达人ID,达人名称,导出状态,失败原因,内容数据-个人视频-近30天-播放量中位数"
);
expect(rowLine).toBe("123,达人 A,成功,,3738.4w");
expect(rowLine).toBe("123,达人 A,成功,,10913233");
});
test("lists headers for field picker defaults", () => {
@@ -244,7 +235,7 @@ describe("audience-profile-csv", () => {
"达人信息",
"连接用户数",
"秒思api-看后搜数",
"内容数据-个人视频-播放量中位数",
"内容数据-个人视频-近30天-播放量中位数",
"效果预估-20-60s视频-预期CPM",
"观众画像-男性占比",
"铁粉画像-小镇青年占比"
@@ -260,7 +251,9 @@ describe("audience-profile-csv", () => {
label: "秒思api数据"
}),
expect.objectContaining({
headers: expect.arrayContaining(["内容数据-个人视频-播放量中位数"]),
headers: expect.arrayContaining([
"内容数据-只看指派-不排除营销流量-星图视频-近90天-作品平均评论数"
]),
label: "内容数据"
}),
expect.objectContaining({
@@ -310,12 +303,7 @@ function buildSuccessRow(
hotRate: "缺失"
}
},
status: "success",
videos: {
personalVideo: {
medianPlay: "3738.4w"
}
}
status: "success"
},
record: {
authorId: "123",
@@ -324,6 +312,10 @@ function buildSuccessRow(
: "达人 A",
: "300w"
},
spreadMetrics: {
"内容数据-个人视频-近30天-播放量中位数": "10913233",
"内容数据-只看指派-不排除营销流量-星图视频-近90天-作品平均评论数": "7502"
},
status: "success",
...overrides
}
+5 -53
View File
@@ -2,24 +2,12 @@ import { describe, expect, test, vi } from "vitest";
import {
buildBusinessAbilityEstimateUrl,
buildBusinessAbilityVideoUrl,
createBusinessAbilityClient,
mapBusinessAbilityEstimateResponse,
mapBusinessAbilityVideoResponse
mapBusinessAbilityEstimateResponse
} from "../src/content/market/business-ability-client";
describe("business-ability-client", () => {
test("builds commercial ability urls used by the Xingtu detail page", () => {
expect(
buildBusinessAbilityVideoUrl(
"6724241209444794382",
"https://www.xingtu.cn",
2
)
).toBe(
"https://www.xingtu.cn/gw/api/data_sp/get_author_spread_info?o_author_id=6724241209444794382&platform_source=1&platform_channel=1&type=2&flow_type=0&only_assign=true&range=2"
);
test("builds the commerce spread estimate url used by the Xingtu detail page", () => {
expect(
buildBusinessAbilityEstimateUrl(
"6724241209444794382",
@@ -30,19 +18,6 @@ describe("business-ability-client", () => {
);
});
test("maps video content metrics into page-style display values", () => {
expect(mapBusinessAbilityVideoResponse(buildVideoPayload())).toEqual({
averageComment: "5.1w",
averageDuration: "170s",
averageLike: "150.3w",
averageShare: "68.4w",
finishRate: "19.9%",
interactionRate: "5.5%",
medianPlay: "4059.7w",
publishedItems: "<5"
});
});
test("maps duration estimates into page-style display values", () => {
expect(mapBusinessAbilityEstimateResponse(buildEstimatePayload())).toEqual({
oneToTwenty: {
@@ -98,15 +73,12 @@ describe("business-ability-client", () => {
});
});
test("loads personal video, Xingtu video, and duration estimate metrics", async () => {
test("loads duration estimate metrics without requesting legacy video content metrics", async () => {
const requestedUrls: string[] = [];
const fetchImpl = vi.fn(async (input: string) => {
requestedUrls.push(input);
return {
json: async () =>
input.includes("get_author_commerce_spread_info")
? buildEstimatePayload()
: buildVideoPayload(),
json: async () => buildEstimatePayload(),
ok: true
};
});
@@ -126,35 +98,15 @@ describe("business-ability-client", () => {
estimates: expect.objectContaining({
twentyToSixty: expect.objectContaining({ expectedCpm: "212.0" })
}),
status: "success",
videos: {
personalVideo: expect.objectContaining({ medianPlay: "4059.7w" }),
xingtuVideo: expect.objectContaining({ medianPlay: "4059.7w" })
}
status: "success"
});
expect(requestedUrls).toEqual([
"https://www.xingtu.cn/gw/api/data_sp/get_author_spread_info?o_author_id=6724241209444794382&platform_source=1&platform_channel=1&type=1&flow_type=0&only_assign=true&range=2",
"https://www.xingtu.cn/gw/api/data_sp/get_author_spread_info?o_author_id=6724241209444794382&platform_source=1&platform_channel=1&type=2&flow_type=0&only_assign=true&range=2",
"https://www.xingtu.cn/gw/api/aggregator/get_author_commerce_spread_info?o_author_id=6724241209444794382"
]);
});
});
function buildVideoPayload() {
return {
avg_duration: 17002,
base_resp: { status_code: 0, status_message: "" },
comment_avg: 51404,
interact_rate: { value: 551 },
item_num: 2,
like_avg: 1503028,
play_mid: 40596960,
play_over_rate: { value: 1991 },
share_avg: 684318
};
}
function buildEstimatePayload() {
return {
base_resp: { status_code: 0, status_message: "" },
+5 -5
View File
@@ -196,8 +196,8 @@ describe("csv-exporter", () => {
authorId: "123",
authorName: "Alice",
spreadMetrics: {
"个人视频_近30天_完播率": "28.24%",
"只看指派_排除营销流量_星图视频_近30天_互动率": "4.02%"
"内容数据-个人视频-近30天-完播率": "28.24%",
"内容数据-只看指派-排除营销流量-星图视频-近30天-互动率": "4.02%"
},
status: "success"
} satisfies MarketRecord
@@ -207,12 +207,12 @@ describe("csv-exporter", () => {
expect(headerLine).toContain(
[
"秒思api-cp_search",
"个人视频_近30天_完播率",
"个人视频_近30天_播放量中位数"
"内容数据-个人视频-近30天-完播率",
"内容数据-个人视频-近30天-播放量中位数"
].join(",")
);
expect(headerLine).toContain(
"只看指派_排除营销流量_星图视频_近30天_互动率"
"内容数据-只看指派-排除营销流量-星图视频-近30天-互动率"
);
expect(rowLine).toContain("28.24%");
expect(rowLine).toContain("4.02%");
+9 -12
View File
@@ -1285,7 +1285,7 @@ describe("market-content-entry", () => {
]);
const buildCsv = vi.fn(() => "csv-output");
const loadSpreadMetrics = vi.fn(async (spreadAuthorId: string) => ({
"个人视频_近30天_完播率": spreadAuthorId === "spread-a" ? "28.24%" : "18.24%"
"内容数据-个人视频-近30天-完播率": spreadAuthorId === "spread-a" ? "28.24%" : "18.24%"
}));
const { createMarketController } = await import("../src/content/market/index");
@@ -1314,14 +1314,14 @@ describe("market-content-entry", () => {
authorId: "a",
spreadAuthorId: "spread-a",
spreadMetrics: {
"个人视频_近30天_完播率": "28.24%"
"内容数据-个人视频-近30天-完播率": "28.24%"
}
}),
expect.objectContaining({
authorId: "b",
spreadAuthorId: "spread-b",
spreadMetrics: {
"个人视频_近30天_完播率": "18.24%"
"内容数据-个人视频-近30天-完播率": "18.24%"
}
})
]);
@@ -1829,8 +1829,7 @@ describe("market-content-entry", () => {
const buildAudienceProfileCsv = vi.fn(() => "profile-csv");
const loadBusinessAbility = vi.fn(async () => ({
estimates: {},
status: "success" as const,
videos: {}
status: "success" as const
}));
const loadAudienceProfile = vi.fn(async (_record, target) => {
if (target.source === "fansDistribution" && target.authorType === 5) {
@@ -1918,8 +1917,7 @@ describe("market-content-entry", () => {
}));
const loadBusinessAbility = vi.fn(async () => ({
estimates: {},
status: "success" as const,
videos: {}
status: "success" as const
}));
const loadAudienceProfile = vi.fn(async () => ({
age: [{ label: "31-40", value: "60%" }],
@@ -2051,13 +2049,12 @@ describe("market-content-entry", () => {
]);
window.localStorage.setItem(
"sces:audience-profile:selectedHeaders",
JSON.stringify(["内容数据-个人视频-播放量中位数", "秒思api-看后搜数"])
JSON.stringify(["内容数据-个人视频-近30天-播放量中位数", "秒思api-看后搜数"])
);
const buildAudienceProfileCsv = vi.fn(() => "profile-csv");
const loadBusinessAbility = vi.fn(async () => ({
estimates: {},
status: "success" as const,
videos: {}
status: "success" as const
}));
const loadAudienceProfile = vi.fn(async () => ({
age: [],
@@ -2091,7 +2088,7 @@ describe("market-content-entry", () => {
await waitForMockCall(buildAudienceProfileCsv, 40, 50);
expect(buildAudienceProfileCsv.mock.calls[0][1]).toEqual({
selectedHeaders: ["内容数据-个人视频-播放量中位数", "秒思api-看后搜数"]
selectedHeaders: ["内容数据-个人视频-近30天-播放量中位数", "秒思api-看后搜数"]
});
});
@@ -2127,7 +2124,7 @@ describe("market-content-entry", () => {
window.localStorage.getItem("sces:audience-profile:selectedHeaders") ?? "[]"
) as string[];
expect(savedHeaders).not.toContain("秒思api-看后搜数");
expect(savedHeaders).toContain("内容数据-个人视频-播放量中位数");
expect(savedHeaders).toContain("内容数据-个人视频-近30天-播放量中位数");
expect(
document.querySelector('[data-plugin-export-status="text"]')?.textContent
).toContain("字段已保存");
+16 -16
View File
@@ -47,8 +47,8 @@ describe("spread-info", () => {
}
]);
expect(buildSpreadInfoColumns(personalConfigs).slice(0, 2)).toEqual([
"个人视频_近30天_完播率",
"个人视频_近30天_播放量中位数"
"内容数据-个人视频-近30天-完播率",
"内容数据-个人视频-近30天-播放量中位数"
]);
});
@@ -78,13 +78,13 @@ describe("spread-info", () => {
type: 2
}
])).toEqual([
"只看指派_排除营销流量_星图视频_近30天_完播率",
"只看指派_排除营销流量_星图视频_近30天_播放量中位数",
"只看指派_排除营销流量_星图视频_近30天_互动率",
"只看指派_排除营销流量_星图视频_近30天_作品平均时长",
"只看指派_排除营销流量_星图视频_近30天_作品平均评论数",
"只看指派_排除营销流量_星图视频_近30天_作品平均点赞数",
"只看指派_排除营销流量_星图视频_近30天_作品平均转发数"
"内容数据-只看指派-排除营销流量-星图视频-近30天-完播率",
"内容数据-只看指派-排除营销流量-星图视频-近30天-播放量中位数",
"内容数据-只看指派-排除营销流量-星图视频-近30天-互动率",
"内容数据-只看指派-排除营销流量-星图视频-近30天-作品平均时长",
"内容数据-只看指派-排除营销流量-星图视频-近30天-作品平均评论数",
"内容数据-只看指派-排除营销流量-星图视频-近30天-作品平均点赞数",
"内容数据-只看指派-排除营销流量-星图视频-近30天-作品平均转发数"
]);
});
@@ -150,13 +150,13 @@ describe("spread-info", () => {
await expect(
client.loadAuthorSpreadMetrics("7361012802036695050")
).resolves.toEqual({
"个人视频_近30天_互动率": "4.02%",
"个人视频_近30天_作品平均点赞数": "494458",
"个人视频_近30天_作品平均评论数": "7502",
"个人视频_近30天_作品平均时长": "56",
"个人视频_近30天_作品平均转发数": "188267",
"个人视频_近30天_完播率": "28.24%",
"个人视频_近30天_播放量中位数": "10913233"
"内容数据-个人视频-近30天-互动率": "4.02%",
"内容数据-个人视频-近30天-作品平均点赞数": "494458",
"内容数据-个人视频-近30天-作品平均评论数": "7502",
"内容数据-个人视频-近30天-作品平均时长": "56",
"内容数据-个人视频-近30天-作品平均转发数": "188267",
"内容数据-个人视频-近30天-完播率": "28.24%",
"内容数据-个人视频-近30天-播放量中位数": "10913233"
});
expect(fetchImpl).toHaveBeenCalledWith(
"https://www.xingtu.cn/gw/api/data_sp/get_author_spread_info?o_author_id=7361012802036695050&platform_source=1&platform_channel=1&type=1&flow_type=0&only_assign=false&range=2",