feat: export business ability metrics
This commit is contained in:
@@ -35,6 +35,45 @@ describe("audience-profile-csv", () => {
|
||||
status: "success"
|
||||
}
|
||||
},
|
||||
businessAbility: {
|
||||
estimates: {
|
||||
oneToTwenty: {
|
||||
expectedCpe: "2.1",
|
||||
expectedCpm: "120",
|
||||
expectedPlay: "250w",
|
||||
hotRate: "100%"
|
||||
},
|
||||
twentyToSixty: {
|
||||
expectedCpe: "3.7",
|
||||
expectedCpm: "212",
|
||||
expectedPlay: "250w",
|
||||
hotRate: "100%"
|
||||
}
|
||||
},
|
||||
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"
|
||||
}
|
||||
}
|
||||
},
|
||||
record: {
|
||||
authorId: "123",
|
||||
authorName: "达人 A",
|
||||
@@ -52,6 +91,11 @@ describe("audience-profile-csv", () => {
|
||||
expect(headerLine).toContain("达人信息,连接用户数");
|
||||
expect(headerLine).not.toContain("抓取状态");
|
||||
expect(headerLine).not.toContain("失败原因");
|
||||
expect(headerLine).toContain("商业能力-个人视频-播放量中位数");
|
||||
expect(headerLine).toContain("商业能力-星图视频-平均转发");
|
||||
expect(headerLine).toContain("商业能力-1-20s视频-预期CPM");
|
||||
expect(headerLine).toContain("商业能力-20-60s视频-爆文率");
|
||||
expect(headerLine).toContain("商业能力-60s以上视频-预期播放量");
|
||||
expect(headerLine).toContain("观众画像-男性占比");
|
||||
expect(headerLine).toContain("粉丝画像-女性占比");
|
||||
expect(headerLine).not.toContain("铁粉画像-男性占比");
|
||||
@@ -63,6 +107,10 @@ 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, "商业能力-1-20s视频-预期CPM")).toBe("120");
|
||||
expect(readCsvValue(csv, "商业能力-20-60s视频-爆文率")).toBe("100%");
|
||||
});
|
||||
|
||||
test("leaves distribution cells empty when profile loading fails", () => {
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
import {
|
||||
buildBusinessAbilityEstimateUrl,
|
||||
buildBusinessAbilityVideoUrl,
|
||||
createBusinessAbilityClient,
|
||||
mapBusinessAbilityEstimateResponse,
|
||||
mapBusinessAbilityVideoResponse
|
||||
} 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"
|
||||
);
|
||||
|
||||
expect(
|
||||
buildBusinessAbilityEstimateUrl(
|
||||
"6724241209444794382",
|
||||
"https://www.xingtu.cn"
|
||||
)
|
||||
).toBe(
|
||||
"https://www.xingtu.cn/gw/api/aggregator/get_author_commerce_spread_info?o_author_id=6724241209444794382"
|
||||
);
|
||||
});
|
||||
|
||||
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: {
|
||||
expectedCpe: "2.1",
|
||||
expectedCpm: "120",
|
||||
expectedPlay: "250w",
|
||||
hotRate: "100%"
|
||||
},
|
||||
overSixty: {
|
||||
expectedCpe: "4.2",
|
||||
expectedCpm: "240",
|
||||
expectedPlay: "250w",
|
||||
hotRate: "100%"
|
||||
},
|
||||
twentyToSixty: {
|
||||
expectedCpe: "3.7",
|
||||
expectedCpm: "212",
|
||||
expectedPlay: "250w",
|
||||
hotRate: "100%"
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("loads personal video, Xingtu video, and duration estimate 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(),
|
||||
ok: true
|
||||
};
|
||||
});
|
||||
const client = createBusinessAbilityClient({
|
||||
baseUrl: "https://www.xingtu.cn",
|
||||
fetchImpl,
|
||||
timeoutMs: 1000
|
||||
});
|
||||
|
||||
await expect(
|
||||
client.loadBusinessAbility({
|
||||
authorId: "6724241209444794382",
|
||||
authorName: "李蠕蠕",
|
||||
status: "success"
|
||||
})
|
||||
).resolves.toMatchObject({
|
||||
estimates: expect.objectContaining({
|
||||
twentyToSixty: expect.objectContaining({ expectedCpm: "212" })
|
||||
}),
|
||||
status: "success",
|
||||
videos: {
|
||||
personalVideo: expect.objectContaining({ medianPlay: "4059.7w" }),
|
||||
xingtuVideo: expect.objectContaining({ medianPlay: "4059.7w" })
|
||||
}
|
||||
});
|
||||
|
||||
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: "" },
|
||||
cpe_1_20: "2.1035",
|
||||
cpe_20_60: "3.7161",
|
||||
cpe_60: "4.2069",
|
||||
cpm_1_20: "119.9976",
|
||||
cpm_20_60: "211.9958",
|
||||
cpm_60: "239.9953",
|
||||
platform_hot_rate: "1",
|
||||
vv: "2500049"
|
||||
};
|
||||
}
|
||||
@@ -1624,6 +1624,11 @@ describe("market-content-entry", () => {
|
||||
{ authorId: "222", authorName: "达人 B", price21To60s: "¥22,000" }
|
||||
]);
|
||||
const buildAudienceProfileCsv = vi.fn(() => "profile-csv");
|
||||
const loadBusinessAbility = vi.fn(async () => ({
|
||||
estimates: {},
|
||||
status: "success" as const,
|
||||
videos: {}
|
||||
}));
|
||||
const loadAudienceProfile = vi.fn(async (_record, target) => {
|
||||
if (target.source === "fansDistribution" && target.authorType === 5) {
|
||||
return {
|
||||
@@ -1648,6 +1653,7 @@ describe("market-content-entry", () => {
|
||||
const controller = trackController(createMarketController({
|
||||
buildAudienceProfileCsv,
|
||||
document,
|
||||
loadBusinessAbility,
|
||||
loadAudienceProfile,
|
||||
loadAuthorMetrics: async () => ({
|
||||
success: false,
|
||||
@@ -1666,6 +1672,10 @@ describe("market-content-entry", () => {
|
||||
await waitForMockCall(buildAudienceProfileCsv, 40, 50);
|
||||
|
||||
expect(loadAudienceProfile).toHaveBeenCalledTimes(3);
|
||||
expect(loadBusinessAbility).toHaveBeenCalledTimes(1);
|
||||
expect(loadBusinessAbility).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ authorId: "222" })
|
||||
);
|
||||
expect(loadAudienceProfile.mock.calls.map(([, target]) => target)).toEqual([
|
||||
{ linkType: 5, source: "audienceDistribution" },
|
||||
{ authorType: 1, source: "fansDistribution" },
|
||||
@@ -1678,6 +1688,7 @@ describe("market-content-entry", () => {
|
||||
fans: expect.objectContaining({ status: "success" }),
|
||||
longtimeFans: expect.objectContaining({ status: "success" })
|
||||
},
|
||||
businessAbility: expect.objectContaining({ status: "success" }),
|
||||
record: expect.objectContaining({ authorId: "222" })
|
||||
}
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user