feat: export business ability metrics

This commit is contained in:
admin123 2026-05-18 18:27:47 +08:00
parent ca9ce02db5
commit 249e6a5971
7 changed files with 643 additions and 3 deletions

View File

@ -4,7 +4,11 @@ import type {
AudienceProfileDistributionItem, AudienceProfileDistributionItem,
AudienceProfileExportRow, AudienceProfileExportRow,
AudienceProfileKind, AudienceProfileKind,
AudienceProfileResult AudienceProfileResult,
BusinessAbilityDurationKind,
BusinessAbilityEstimateMetrics,
BusinessAbilityVideoKind,
BusinessAbilityVideoMetrics
} from "./audience-profile-types"; } from "./audience-profile-types";
type AudienceProfileCsvColumn = { type AudienceProfileCsvColumn = {
@ -43,12 +47,54 @@ const CROWD_LABELS = [
"小镇青年" "小镇青年"
]; ];
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( export function buildAudienceProfileCsv(
rows: AudienceProfileExportRow[] rows: AudienceProfileExportRow[]
): string { ): string {
const marketColumns = buildMarketCsvColumns(rows.map((row) => row.record)); const marketColumns = buildMarketCsvColumns(rows.map((row) => row.record));
const csvColumns = [ const csvColumns = [
...marketColumns.map(toMarketColumn), ...marketColumns.map(toMarketColumn),
...buildBusinessAbilityColumns(),
...PROFILE_LAYOUTS.flatMap((layout) => buildProfileColumns(layout)) ...PROFILE_LAYOUTS.flatMap((layout) => buildProfileColumns(layout))
]; ];
const headerLine = csvColumns.map((column) => column.header).join(","); const headerLine = csvColumns.map((column) => column.header).join(",");
@ -59,6 +105,51 @@ export function buildAudienceProfileCsv(
return [headerLine, ...rowLines].join("\n"); 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 { function toMarketColumn(column: CsvColumn): AudienceProfileCsvColumn {
return { return {
header: column.header, header: column.header,

View File

@ -37,6 +37,51 @@ export type AudienceProfileResult =
| AudienceProfileFailure; | AudienceProfileFailure;
export interface AudienceProfileExportRow { export interface AudienceProfileExportRow {
businessAbility?: BusinessAbilityResult;
profiles: AudienceProfileSet; profiles: AudienceProfileSet;
record: MarketRecord; record: MarketRecord;
} }
export type BusinessAbilityVideoKind =
| "personalVideo"
| "xingtuVideo";
export interface BusinessAbilityVideoMetrics {
averageComment: string;
averageDuration: string;
averageLike: string;
averageShare: string;
finishRate: string;
interactionRate: string;
medianPlay: string;
publishedItems: string;
}
export type BusinessAbilityDurationKind =
| "oneToTwenty"
| "twentyToSixty"
| "overSixty";
export interface BusinessAbilityEstimateMetrics {
expectedCpe: string;
expectedCpm: string;
expectedPlay: string;
hotRate: string;
}
export interface BusinessAbilitySuccess {
estimates: Partial<
Record<BusinessAbilityDurationKind, BusinessAbilityEstimateMetrics>
>;
status: "success";
videos: Partial<Record<BusinessAbilityVideoKind, BusinessAbilityVideoMetrics>>;
}
export interface BusinessAbilityFailure {
failureReason?: string;
status: "failed";
}
export type BusinessAbilityResult =
| BusinessAbilitySuccess
| BusinessAbilityFailure;

View File

@ -0,0 +1,281 @@
import type { MarketRecord } from "./types";
import type {
BusinessAbilityDurationKind,
BusinessAbilityEstimateMetrics,
BusinessAbilityResult,
BusinessAbilitySuccess,
BusinessAbilityVideoMetrics
} from "./audience-profile-types";
interface FetchResponseLike {
json(): Promise<unknown>;
ok: boolean;
}
type FetchLike = (
input: string,
init?: RequestInit
) => Promise<FetchResponseLike>;
interface BusinessAbilityClientOptions {
baseUrl?: string;
fetchImpl?: FetchLike;
timeoutMs?: number;
}
const VIDEO_TYPES = {
personalVideo: 1,
xingtuVideo: 2
} as const;
export function createBusinessAbilityClient(
options: BusinessAbilityClientOptions = {}
) {
const baseUrl = options.baseUrl ?? resolveBaseUrl();
const fetchImpl = options.fetchImpl ?? defaultFetch;
const timeoutMs = options.timeoutMs ?? 8000;
return {
async loadBusinessAbility(record: MarketRecord): Promise<BusinessAbilityResult> {
const personalVideo = await loadJson(
buildBusinessAbilityVideoUrl(record.authorId, baseUrl, VIDEO_TYPES.personalVideo)
);
const xingtuVideo = await loadJson(
buildBusinessAbilityVideoUrl(record.authorId, baseUrl, VIDEO_TYPES.xingtuVideo)
);
const estimates = await loadJson(
buildBusinessAbilityEstimateUrl(record.authorId, baseUrl)
);
if (!personalVideo.ok || !xingtuVideo.ok || !estimates.ok) {
return {
failureReason:
personalVideo.failureReason ??
xingtuVideo.failureReason ??
estimates.failureReason,
status: "failed"
};
}
return {
estimates: mapBusinessAbilityEstimateResponse(estimates.payload),
status: "success",
videos: {
personalVideo: mapBusinessAbilityVideoResponse(personalVideo.payload),
xingtuVideo: mapBusinessAbilityVideoResponse(xingtuVideo.payload)
}
} satisfies BusinessAbilitySuccess;
}
};
async function loadJson(url: string): Promise<
| { ok: true; payload: unknown }
| { failureReason: string; ok: false }
> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetchImpl(url, {
credentials: "include",
method: "GET",
signal: controller.signal
});
if (!response.ok) {
return { failureReason: "request-failed", ok: false };
}
return { ok: true, payload: await response.json() };
} catch (error) {
return {
failureReason:
error instanceof Error && error.name === "AbortError"
? "timeout"
: "request-failed",
ok: false
};
} finally {
clearTimeout(timeoutId);
}
}
}
export function buildBusinessAbilityVideoUrl(
authorId: string,
baseUrl: string,
videoType: number
): string {
const url = new URL("/gw/api/data_sp/get_author_spread_info", baseUrl);
url.searchParams.set("o_author_id", authorId);
url.searchParams.set("platform_source", "1");
url.searchParams.set("platform_channel", "1");
url.searchParams.set("type", String(videoType));
url.searchParams.set("flow_type", "0");
url.searchParams.set("only_assign", "true");
url.searchParams.set("range", "2");
return url.toString();
}
export function buildBusinessAbilityEstimateUrl(
authorId: string,
baseUrl: string
): string {
const url = new URL(
"/gw/api/aggregator/get_author_commerce_spread_info",
baseUrl
);
url.searchParams.set("o_author_id", authorId);
return url.toString();
}
export function mapBusinessAbilityVideoResponse(
payload: unknown
): BusinessAbilityVideoMetrics {
const data = getPayloadData(payload);
return {
averageComment: formatWan(readNumber(data?.comment_avg)),
averageDuration: formatDuration(readNumber(data?.avg_duration)),
averageLike: formatWan(readNumber(data?.like_avg)),
averageShare: formatWan(readNumber(data?.share_avg)),
finishRate: formatBasisPointRate(readNestedNumber(data, "play_over_rate", "value")),
interactionRate: formatBasisPointRate(
readNestedNumber(data, "interact_rate", "value")
),
medianPlay: formatWan(readNumber(data?.play_mid)),
publishedItems: formatPublishedItems(readNumber(data?.item_num))
};
}
export function mapBusinessAbilityEstimateResponse(
payload: unknown
): Partial<Record<BusinessAbilityDurationKind, BusinessAbilityEstimateMetrics>> {
const data = getPayloadData(payload);
const expectedPlay = formatWan(readNumber(data?.vv));
const hotRate = formatDecimalRate(readNumber(data?.platform_hot_rate));
return {
oneToTwenty: {
expectedCpe: formatDecimal(readNumber(data?.cpe_1_20), 1),
expectedCpm: formatDecimal(readNumber(data?.cpm_1_20), 0),
expectedPlay,
hotRate
},
overSixty: {
expectedCpe: formatDecimal(readNumber(data?.cpe_60), 1),
expectedCpm: formatDecimal(readNumber(data?.cpm_60), 0),
expectedPlay,
hotRate
},
twentyToSixty: {
expectedCpe: formatDecimal(readNumber(data?.cpe_20_60), 1),
expectedCpm: formatDecimal(readNumber(data?.cpm_20_60), 0),
expectedPlay,
hotRate
}
};
}
function formatPublishedItems(value: number | null): string {
if (value === null) {
return "";
}
return value > 0 && value < 5 ? "<5" : formatDecimal(value, 0);
}
function formatDuration(value: number | null): string {
if (value === null) {
return "";
}
return `${formatDecimal(value / 100, 0)}s`;
}
function formatBasisPointRate(value: number | null): string {
if (value === null) {
return "";
}
return `${formatDecimal(value / 100, 1)}%`;
}
function formatDecimalRate(value: number | null): string {
if (value === null) {
return "";
}
return `${formatDecimal(value * 100, 0)}%`;
}
function formatWan(value: number | null): string {
if (value === null) {
return "";
}
if (Math.abs(value) >= 10000) {
return `${formatDecimal(value / 10000, 1)}w`;
}
return formatDecimal(value, 0);
}
function formatDecimal(value: number | null, digits: number): string {
if (value === null || !Number.isFinite(value)) {
return "";
}
const fixed = value.toFixed(digits);
return fixed.replace(/\.0+$/, "").replace(/(\.\d*?)0+$/, "$1");
}
function readNestedNumber(
data: Record<string, unknown> | null,
objectKey: string,
valueKey: string
): number | null {
const objectValue = data?.[objectKey];
if (!isRecord(objectValue)) {
return null;
}
return readNumber(objectValue[valueKey]);
}
function readNumber(value: unknown): number | null {
if (typeof value === "number" && Number.isFinite(value)) {
return value;
}
if (typeof value === "string" && value.trim()) {
const numericValue = Number(value);
return Number.isFinite(numericValue) ? numericValue : null;
}
return null;
}
function getPayloadData(payload: unknown): Record<string, unknown> | null {
if (!isRecord(payload)) {
return null;
}
return isRecord(payload.data) ? payload.data : payload;
}
function resolveBaseUrl(): string {
if (typeof location !== "undefined" && location.origin) {
return location.origin;
}
return "https://xingtu.cn";
}
async function defaultFetch(input: string, init?: RequestInit) {
return fetch(input, init);
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}

View File

@ -5,6 +5,7 @@ import {
createAudienceProfileClient, createAudienceProfileClient,
type AudienceProfileRequestTarget type AudienceProfileRequestTarget
} from "./audience-profile-client"; } from "./audience-profile-client";
import { createBusinessAbilityClient } from "./business-ability-client";
import { promptForBatchName } from "./batch-name-dialog"; import { promptForBatchName } from "./batch-name-dialog";
import { createBatchPayload, type BatchPayload } from "./batch-payload"; import { createBatchPayload, type BatchPayload } from "./batch-payload";
import { import {
@ -36,7 +37,8 @@ import { isBackendMetricsResponseMessage } from "../../shared/backend-metrics-me
import type { import type {
AudienceProfileExportRow, AudienceProfileExportRow,
AudienceProfileKind, AudienceProfileKind,
AudienceProfileResult AudienceProfileResult,
BusinessAbilityResult
} from "./audience-profile-types"; } from "./audience-profile-types";
import type { import type {
BackendMetrics, BackendMetrics,
@ -57,6 +59,9 @@ export interface CreateMarketControllerOptions {
buildCsv?: (records: MarketRecord[]) => string; buildCsv?: (records: MarketRecord[]) => string;
document: Document; document: Document;
getAuthState?: () => Promise<AuthStateValue>; getAuthState?: () => Promise<AuthStateValue>;
loadBusinessAbility?: (
record: MarketRecord
) => Promise<BusinessAbilityResult>;
loadAudienceProfile?: ( loadAudienceProfile?: (
record: MarketRecord, record: MarketRecord,
target: AudienceProfileRequestTarget target: AudienceProfileRequestTarget
@ -78,6 +83,7 @@ export interface CreateMarketControllerOptions {
export function createMarketController(options: CreateMarketControllerOptions) { export function createMarketController(options: CreateMarketControllerOptions) {
const marketApiClient = createMarketApiClient(); const marketApiClient = createMarketApiClient();
const audienceProfileClient = createAudienceProfileClient(); const audienceProfileClient = createAudienceProfileClient();
const businessAbilityClient = createBusinessAbilityClient();
const sendRuntimeMessage = createRuntimeMessageSender(); const sendRuntimeMessage = createRuntimeMessageSender();
const resultStore = options.resultStore ?? createMarketResultStore(); const resultStore = options.resultStore ?? createMarketResultStore();
const loadAuthorMetrics = const loadAuthorMetrics =
@ -89,6 +95,8 @@ export function createMarketController(options: CreateMarketControllerOptions) {
const buildAudienceCsv = options.buildAudienceProfileCsv ?? buildAudienceProfileCsv; const buildAudienceCsv = options.buildAudienceProfileCsv ?? buildAudienceProfileCsv;
const loadAudienceProfile = const loadAudienceProfile =
options.loadAudienceProfile ?? audienceProfileClient.loadAudienceProfile; options.loadAudienceProfile ?? audienceProfileClient.loadAudienceProfile;
const loadBusinessAbility =
options.loadBusinessAbility ?? businessAbilityClient.loadBusinessAbility;
const getAuthState = options.getAuthState ?? (() => readAuthState(sendRuntimeMessage)); const getAuthState = options.getAuthState ?? (() => readAuthState(sendRuntimeMessage));
const mutationObserverFactory = const mutationObserverFactory =
options.mutationObserverFactory ?? options.mutationObserverFactory ??
@ -227,8 +235,12 @@ export function createMarketController(options: CreateMarketControllerOptions) {
toolbar, toolbar,
`画像导出中 ${index + 1}/${selectedRecords.length}...` `画像导出中 ${index + 1}/${selectedRecords.length}...`
); );
const profiles = await loadAudienceProfileSet(record); const [profiles, businessAbility] = await Promise.all([
loadAudienceProfileSet(record),
loadBusinessAbilitySafe(record)
]);
rows.push({ rows.push({
businessAbility,
profiles, profiles,
record record
}); });
@ -680,6 +692,20 @@ export function createMarketController(options: CreateMarketControllerOptions) {
return profiles; return profiles;
} }
async function loadBusinessAbilitySafe(
record: MarketRecord
): Promise<BusinessAbilityResult> {
try {
return await loadBusinessAbility(record);
} catch (error) {
return {
failureReason:
error instanceof Error ? error.message : "request-failed",
status: "failed"
};
}
}
async function prepareCurrentPageForExport(): Promise<void> { async function prepareCurrentPageForExport(): Promise<void> {
await runSyncCycle(); await runSyncCycle();
await harvestCurrentPageForExport(); await harvestCurrentPageForExport();

View File

@ -35,6 +35,45 @@ describe("audience-profile-csv", () => {
status: "success" 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: { record: {
authorId: "123", authorId: "123",
authorName: "达人 A", authorName: "达人 A",
@ -52,6 +91,11 @@ describe("audience-profile-csv", () => {
expect(headerLine).toContain("达人信息,连接用户数"); expect(headerLine).toContain("达人信息,连接用户数");
expect(headerLine).not.toContain("抓取状态"); expect(headerLine).not.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).toContain("粉丝画像-女性占比"); expect(headerLine).toContain("粉丝画像-女性占比");
expect(headerLine).not.toContain("铁粉画像-男性占比"); expect(headerLine).not.toContain("铁粉画像-男性占比");
@ -63,6 +107,10 @@ describe("audience-profile-csv", () => {
expect(headerLine).not.toContain("兴趣TOP"); expect(headerLine).not.toContain("兴趣TOP");
expect(rowLine).toContain("71.7%"); expect(rowLine).toContain("71.7%");
expect(rowLine).toContain("60%"); 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", () => { test("leaves distribution cells empty when profile loading fails", () => {

View File

@ -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"
};
}

View File

@ -1624,6 +1624,11 @@ describe("market-content-entry", () => {
{ authorId: "222", authorName: "达人 B", price21To60s: "¥22,000" } { authorId: "222", authorName: "达人 B", price21To60s: "¥22,000" }
]); ]);
const buildAudienceProfileCsv = vi.fn(() => "profile-csv"); const buildAudienceProfileCsv = vi.fn(() => "profile-csv");
const loadBusinessAbility = vi.fn(async () => ({
estimates: {},
status: "success" as const,
videos: {}
}));
const loadAudienceProfile = vi.fn(async (_record, target) => { const loadAudienceProfile = vi.fn(async (_record, target) => {
if (target.source === "fansDistribution" && target.authorType === 5) { if (target.source === "fansDistribution" && target.authorType === 5) {
return { return {
@ -1648,6 +1653,7 @@ describe("market-content-entry", () => {
const controller = trackController(createMarketController({ const controller = trackController(createMarketController({
buildAudienceProfileCsv, buildAudienceProfileCsv,
document, document,
loadBusinessAbility,
loadAudienceProfile, loadAudienceProfile,
loadAuthorMetrics: async () => ({ loadAuthorMetrics: async () => ({
success: false, success: false,
@ -1666,6 +1672,10 @@ describe("market-content-entry", () => {
await waitForMockCall(buildAudienceProfileCsv, 40, 50); await waitForMockCall(buildAudienceProfileCsv, 40, 50);
expect(loadAudienceProfile).toHaveBeenCalledTimes(3); expect(loadAudienceProfile).toHaveBeenCalledTimes(3);
expect(loadBusinessAbility).toHaveBeenCalledTimes(1);
expect(loadBusinessAbility).toHaveBeenCalledWith(
expect.objectContaining({ authorId: "222" })
);
expect(loadAudienceProfile.mock.calls.map(([, target]) => target)).toEqual([ expect(loadAudienceProfile.mock.calls.map(([, target]) => target)).toEqual([
{ linkType: 5, source: "audienceDistribution" }, { linkType: 5, source: "audienceDistribution" },
{ authorType: 1, source: "fansDistribution" }, { authorType: 1, source: "fansDistribution" },
@ -1678,6 +1688,7 @@ describe("market-content-entry", () => {
fans: expect.objectContaining({ status: "success" }), fans: expect.objectContaining({ status: "success" }),
longtimeFans: expect.objectContaining({ status: "success" }) longtimeFans: expect.objectContaining({ status: "success" })
}, },
businessAbility: expect.objectContaining({ status: "success" }),
record: expect.objectContaining({ authorId: "222" }) record: expect.objectContaining({ authorId: "222" })
} }
]); ]);