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

View File

@ -11,10 +11,9 @@ import type {
AudienceProfileKind, AudienceProfileKind,
AudienceProfileResult, AudienceProfileResult,
BusinessAbilityDurationKind, BusinessAbilityDurationKind,
BusinessAbilityEstimateMetrics, BusinessAbilityEstimateMetrics
BusinessAbilityVideoKind,
BusinessAbilityVideoMetrics
} from "./audience-profile-types"; } from "./audience-profile-types";
import { buildSpreadInfoColumns } from "./spread-info";
type AudienceProfileCsvColumn = { type AudienceProfileCsvColumn = {
header: string; header: string;
@ -60,29 +59,6 @@ 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_VIDEO_SECTION_LABEL = "内容数据";
const BUSINESS_ESTIMATE_SECTION_LABEL = "效果预估"; const BUSINESS_ESTIMATE_SECTION_LABEL = "效果预估";
const BUSINESS_ESTIMATE_LAYOUTS: Array<{ const BUSINESS_ESTIMATE_LAYOUTS: Array<{
@ -146,7 +122,7 @@ export function listAudienceProfileSelectableFieldGroups(): AudienceProfileCsvFi
label: "秒思api数据" label: "秒思api数据"
}, },
{ {
headers: buildBusinessVideoColumns().map((column) => column.header), headers: buildSpreadInfoColumns(),
label: "内容数据" label: "内容数据"
}, },
{ {
@ -184,19 +160,7 @@ function listAudienceProfileSelectableHeaders(): string[] {
} }
function buildBusinessAbilityColumns(): AudienceProfileCsvColumn[] { function buildBusinessAbilityColumns(): AudienceProfileCsvColumn[] {
return [...buildBusinessVideoColumns(), ...buildBusinessEstimateColumns()]; return buildBusinessEstimateColumns();
}
function buildBusinessVideoColumns(): AudienceProfileCsvColumn[] {
return [
...BUSINESS_VIDEO_LAYOUTS.flatMap((videoLayout) =>
BUSINESS_VIDEO_METRIC_LAYOUTS.map((metricLayout) => ({
header: `${BUSINESS_VIDEO_SECTION_LABEL}-${videoLayout.label}-${metricLayout.label}`,
readValue: (row: AudienceProfileExportRow) =>
readBusinessVideoValue(row, videoLayout.key, metricLayout.key)
}))
)
];
} }
function buildBusinessEstimateColumns(): AudienceProfileCsvColumn[] { function buildBusinessEstimateColumns(): AudienceProfileCsvColumn[] {
@ -211,19 +175,6 @@ function buildBusinessEstimateColumns(): AudienceProfileCsvColumn[] {
]; ];
} }
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( function readBusinessEstimateValue(
row: AudienceProfileExportRow, row: AudienceProfileExportRow,
durationKey: BusinessAbilityDurationKind, durationKey: BusinessAbilityDurationKind,

View File

@ -42,21 +42,6 @@ export interface AudienceProfileExportRow {
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 = export type BusinessAbilityDurationKind =
| "oneToTwenty" | "oneToTwenty"
| "twentyToSixty" | "twentyToSixty"
@ -74,7 +59,6 @@ export interface BusinessAbilitySuccess {
Record<BusinessAbilityDurationKind, BusinessAbilityEstimateMetrics> Record<BusinessAbilityDurationKind, BusinessAbilityEstimateMetrics>
>; >;
status: "success"; status: "success";
videos: Partial<Record<BusinessAbilityVideoKind, BusinessAbilityVideoMetrics>>;
} }
export interface BusinessAbilityFailure { export interface BusinessAbilityFailure {

View File

@ -3,8 +3,7 @@ import type {
BusinessAbilityDurationKind, BusinessAbilityDurationKind,
BusinessAbilityEstimateMetrics, BusinessAbilityEstimateMetrics,
BusinessAbilityResult, BusinessAbilityResult,
BusinessAbilitySuccess, BusinessAbilitySuccess
BusinessAbilityVideoMetrics
} from "./audience-profile-types"; } from "./audience-profile-types";
interface FetchResponseLike { interface FetchResponseLike {
@ -23,11 +22,6 @@ interface BusinessAbilityClientOptions {
timeoutMs?: number; timeoutMs?: number;
} }
const VIDEO_TYPES = {
personalVideo: 1,
xingtuVideo: 2
} as const;
export function createBusinessAbilityClient( export function createBusinessAbilityClient(
options: BusinessAbilityClientOptions = {} options: BusinessAbilityClientOptions = {}
) { ) {
@ -37,33 +31,20 @@ export function createBusinessAbilityClient(
return { return {
async loadBusinessAbility(record: MarketRecord): Promise<BusinessAbilityResult> { 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( const estimates = await loadJson(
buildBusinessAbilityEstimateUrl(record.authorId, baseUrl) buildBusinessAbilityEstimateUrl(record.authorId, baseUrl)
); );
if (!personalVideo.ok || !xingtuVideo.ok || !estimates.ok) { if (!estimates.ok) {
return { return {
failureReason: failureReason: estimates.failureReason,
personalVideo.failureReason ??
xingtuVideo.failureReason ??
estimates.failureReason,
status: "failed" status: "failed"
}; };
} }
return { return {
estimates: mapBusinessAbilityEstimateResponse(estimates.payload), estimates: mapBusinessAbilityEstimateResponse(estimates.payload),
status: "success", status: "success"
videos: {
personalVideo: mapBusinessAbilityVideoResponse(personalVideo.payload),
xingtuVideo: mapBusinessAbilityVideoResponse(xingtuVideo.payload)
}
} satisfies BusinessAbilitySuccess; } satisfies BusinessAbilitySuccess;
} }
}; };
@ -101,22 +82,6 @@ export function createBusinessAbilityClient(
} }
} }
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( export function buildBusinessAbilityEstimateUrl(
authorId: string, authorId: string,
baseUrl: string baseUrl: string
@ -129,25 +94,6 @@ export function buildBusinessAbilityEstimateUrl(
return url.toString(); 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( export function mapBusinessAbilityEstimateResponse(
payload: unknown payload: unknown
): Partial<Record<BusinessAbilityDurationKind, BusinessAbilityEstimateMetrics>> { ): Partial<Record<BusinessAbilityDurationKind, BusinessAbilityEstimateMetrics>> {
@ -177,30 +123,6 @@ export function mapBusinessAbilityEstimateResponse(
}; };
} }
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 { function formatDecimalRate(value: number | null): string {
if (value === null) { if (value === null) {
return "缺失"; return "缺失";
@ -238,19 +160,6 @@ function formatFixedDecimal(value: number | null, digits: number): string {
return value.toFixed(digits); return value.toFixed(digits);
} }
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 { function readNumber(value: unknown): number | null {
if (typeof value === "number" && Number.isFinite(value)) { if (typeof value === "number" && Number.isFinite(value)) {
return value; return value;

View File

@ -265,6 +265,7 @@ export function createMarketController(options: CreateMarketControllerOptions) {
try { try {
const selectedRecords = filterRecordsBySelectionStrict( const selectedRecords = filterRecordsBySelectionStrict(
await exportRecords(exportTarget.target, "画像导出中", { await exportRecords(exportTarget.target, "画像导出中", {
includeSpreadMetrics: true,
showDetailedProgress: false showDetailedProgress: false
}) })
); );
@ -878,13 +879,17 @@ export function createMarketController(options: CreateMarketControllerOptions) {
loadAuthorBaseInfoSafe(authorId), loadAuthorBaseInfoSafe(authorId),
loadAuthorMetricsSafe(authorId) loadAuthorMetricsSafe(authorId)
]); ]);
const spreadMetrics = baseRecord.spreadAuthorId
? await loadSpreadMetrics(baseRecord.spreadAuthorId)
: {};
const recordForRequests = { const recordForRequests = {
...baseRecord, ...baseRecord,
authorName: baseRecord.authorName || authorId, authorName: baseRecord.authorName || authorId,
...(metricsResult.success ? { rates: metricsResult.rates } : {}), ...(metricsResult.success ? { rates: metricsResult.rates } : {}),
...(backendMetrics ...(backendMetrics
? { backendMetrics, backendMetricsStatus: "success" as const } ? { backendMetrics, backendMetricsStatus: "success" as const }
: {}) : {}),
...(Object.keys(spreadMetrics).length > 0 ? { spreadMetrics } : {})
}; };
const [profiles, businessAbility] = await Promise.all([ const [profiles, businessAbility] = await Promise.all([
loadAudienceProfileSet(recordForRequests), loadAudienceProfileSet(recordForRequests),

View File

@ -226,7 +226,7 @@ function buildSpreadInfoColumnHeader(
config: SpreadInfoConfig, config: SpreadInfoConfig,
metric: SpreadInfoMetricDefinition metric: SpreadInfoMetricDefinition
): string { ): string {
return [...buildConfigPrefixParts(config), metric.label].join("_"); return ["内容数据", ...buildConfigPrefixParts(config), metric.label].join("-");
} }
function buildConfigPrefixParts(config: SpreadInfoConfig): string[] { function buildConfigPrefixParts(config: SpreadInfoConfig): string[] {

View File

@ -54,29 +54,7 @@ describe("audience-profile-csv", () => {
hotRate: "缺失" hotRate: "缺失"
} }
}, },
status: "success", 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",
@ -85,6 +63,10 @@ describe("audience-profile-csv", () => {
: "达人 A", : "达人 A",
: "300w" : "300w"
}, },
spreadMetrics: {
"内容数据-个人视频-近30天-播放量中位数": "10913233",
"内容数据-只看指派-不排除营销流量-星图视频-近90天-作品平均评论数": "7502"
},
status: "success" status: "success"
} }
} satisfies AudienceProfileExportRow } satisfies AudienceProfileExportRow
@ -95,8 +77,10 @@ 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("内容数据-个人视频-近30天-播放量中位数");
expect(headerLine).toContain("内容数据-星图视频-平均转发"); expect(headerLine).toContain(
"内容数据-只看指派-不排除营销流量-星图视频-近90天-作品平均评论数"
);
expect(headerLine).toContain("效果预估-1-20s视频-预期CPM"); expect(headerLine).toContain("效果预估-1-20s视频-预期CPM");
expect(headerLine).toContain("效果预估-20-60s视频-爆文率"); expect(headerLine).toContain("效果预估-20-60s视频-爆文率");
expect(headerLine).toContain("效果预估-60s以上视频-预期播放量"); expect(headerLine).toContain("效果预估-60s以上视频-预期播放量");
@ -116,8 +100,15 @@ 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, "内容数据-个人视频-近30天-播放量中位数")).toBe(
expect(readCsvValue(csv, "内容数据-星图视频-平均转发")).toBe("68.4w"); "10913233"
);
expect(
readCsvValue(
csv,
"内容数据-只看指派-不排除营销流量-星图视频-近90天-作品平均评论数"
)
).toBe("7502");
expect(readCsvValue(csv, "效果预估-1-20s视频-预期CPM")).toBe("120.0"); expect(readCsvValue(csv, "效果预估-1-20s视频-预期CPM")).toBe("120.0");
expect(readCsvValue(csv, "效果预估-20-60s视频-爆文率")).toBe("缺失"); expect(readCsvValue(csv, "效果预估-20-60s视频-爆文率")).toBe("缺失");
}); });
@ -202,7 +193,7 @@ describe("audience-profile-csv", () => {
const row = buildSuccessRow(); const row = buildSuccessRow();
const csv = buildAudienceProfileCsv([row], { const csv = buildAudienceProfileCsv([row], {
selectedHeaders: [ selectedHeaders: [
"内容数据-个人视频-播放量中位数", "内容数据-个人视频-近30天-播放量中位数",
"观众画像-男性占比" "观众画像-男性占比"
] ]
}); });
@ -210,9 +201,9 @@ describe("audience-profile-csv", () => {
const [headerLine, rowLine] = csv.split("\n"); const [headerLine, rowLine] = csv.split("\n");
expect(headerLine).toBe( 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("秒思api-看后搜数");
expect(headerLine).not.toContain("粉丝画像-女性占比"); expect(headerLine).not.toContain("粉丝画像-女性占比");
}); });
@ -227,15 +218,15 @@ describe("audience-profile-csv", () => {
} }
}); });
const csv = buildAudienceProfileCsv([row], { const csv = buildAudienceProfileCsv([row], {
selectedHeaders: ["内容数据-个人视频-播放量中位数"] selectedHeaders: ["内容数据-个人视频-近30天-播放量中位数"]
}); });
const [headerLine, rowLine] = csv.split("\n"); const [headerLine, rowLine] = csv.split("\n");
expect(headerLine).toBe( 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", () => { test("lists headers for field picker defaults", () => {
@ -244,7 +235,7 @@ describe("audience-profile-csv", () => {
"达人信息", "达人信息",
"连接用户数", "连接用户数",
"秒思api-看后搜数", "秒思api-看后搜数",
"内容数据-个人视频-播放量中位数", "内容数据-个人视频-近30天-播放量中位数",
"效果预估-20-60s视频-预期CPM", "效果预估-20-60s视频-预期CPM",
"观众画像-男性占比", "观众画像-男性占比",
"铁粉画像-小镇青年占比" "铁粉画像-小镇青年占比"
@ -260,7 +251,9 @@ describe("audience-profile-csv", () => {
label: "秒思api数据" label: "秒思api数据"
}), }),
expect.objectContaining({ expect.objectContaining({
headers: expect.arrayContaining(["内容数据-个人视频-播放量中位数"]), headers: expect.arrayContaining([
"内容数据-只看指派-不排除营销流量-星图视频-近90天-作品平均评论数"
]),
label: "内容数据" label: "内容数据"
}), }),
expect.objectContaining({ expect.objectContaining({
@ -310,12 +303,7 @@ function buildSuccessRow(
hotRate: "缺失" hotRate: "缺失"
} }
}, },
status: "success", status: "success"
videos: {
personalVideo: {
medianPlay: "3738.4w"
}
}
}, },
record: { record: {
authorId: "123", authorId: "123",
@ -324,6 +312,10 @@ function buildSuccessRow(
: "达人 A", : "达人 A",
: "300w" : "300w"
}, },
spreadMetrics: {
"内容数据-个人视频-近30天-播放量中位数": "10913233",
"内容数据-只看指派-不排除营销流量-星图视频-近90天-作品平均评论数": "7502"
},
status: "success", status: "success",
...overrides ...overrides
} }

View File

@ -2,24 +2,12 @@ import { describe, expect, test, vi } from "vitest";
import { import {
buildBusinessAbilityEstimateUrl, buildBusinessAbilityEstimateUrl,
buildBusinessAbilityVideoUrl,
createBusinessAbilityClient, createBusinessAbilityClient,
mapBusinessAbilityEstimateResponse, mapBusinessAbilityEstimateResponse
mapBusinessAbilityVideoResponse
} from "../src/content/market/business-ability-client"; } from "../src/content/market/business-ability-client";
describe("business-ability-client", () => { describe("business-ability-client", () => {
test("builds commercial ability urls used by the Xingtu detail page", () => { test("builds the commerce spread estimate url 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( expect(
buildBusinessAbilityEstimateUrl( buildBusinessAbilityEstimateUrl(
"6724241209444794382", "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", () => { test("maps duration estimates into page-style display values", () => {
expect(mapBusinessAbilityEstimateResponse(buildEstimatePayload())).toEqual({ expect(mapBusinessAbilityEstimateResponse(buildEstimatePayload())).toEqual({
oneToTwenty: { 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 requestedUrls: string[] = [];
const fetchImpl = vi.fn(async (input: string) => { const fetchImpl = vi.fn(async (input: string) => {
requestedUrls.push(input); requestedUrls.push(input);
return { return {
json: async () => json: async () => buildEstimatePayload(),
input.includes("get_author_commerce_spread_info")
? buildEstimatePayload()
: buildVideoPayload(),
ok: true ok: true
}; };
}); });
@ -126,35 +98,15 @@ describe("business-ability-client", () => {
estimates: expect.objectContaining({ estimates: expect.objectContaining({
twentyToSixty: expect.objectContaining({ expectedCpm: "212.0" }) twentyToSixty: expect.objectContaining({ expectedCpm: "212.0" })
}), }),
status: "success", status: "success"
videos: {
personalVideo: expect.objectContaining({ medianPlay: "4059.7w" }),
xingtuVideo: expect.objectContaining({ medianPlay: "4059.7w" })
}
}); });
expect(requestedUrls).toEqual([ 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" "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() { function buildEstimatePayload() {
return { return {
base_resp: { status_code: 0, status_message: "" }, base_resp: { status_code: 0, status_message: "" },

View File

@ -196,8 +196,8 @@ describe("csv-exporter", () => {
authorId: "123", authorId: "123",
authorName: "Alice", authorName: "Alice",
spreadMetrics: { spreadMetrics: {
"个人视频_近30天_完播率": "28.24%", "内容数据-个人视频-近30天-完播率": "28.24%",
"只看指派_排除营销流量_星图视频_近30天_互动率": "4.02%" "内容数据-只看指派-排除营销流量-星图视频-近30天-互动率": "4.02%"
}, },
status: "success" status: "success"
} satisfies MarketRecord } satisfies MarketRecord
@ -207,12 +207,12 @@ describe("csv-exporter", () => {
expect(headerLine).toContain( expect(headerLine).toContain(
[ [
"秒思api-cp_search", "秒思api-cp_search",
"个人视频_近30天_完播率", "内容数据-个人视频-近30天-完播率",
"个人视频_近30天_播放量中位数" "内容数据-个人视频-近30天-播放量中位数"
].join(",") ].join(",")
); );
expect(headerLine).toContain( expect(headerLine).toContain(
"只看指派_排除营销流量_星图视频_近30天_互动率" "内容数据-只看指派-排除营销流量-星图视频-近30天-互动率"
); );
expect(rowLine).toContain("28.24%"); expect(rowLine).toContain("28.24%");
expect(rowLine).toContain("4.02%"); expect(rowLine).toContain("4.02%");

View File

@ -1285,7 +1285,7 @@ describe("market-content-entry", () => {
]); ]);
const buildCsv = vi.fn(() => "csv-output"); const buildCsv = vi.fn(() => "csv-output");
const loadSpreadMetrics = vi.fn(async (spreadAuthorId: string) => ({ 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"); const { createMarketController } = await import("../src/content/market/index");
@ -1314,14 +1314,14 @@ describe("market-content-entry", () => {
authorId: "a", authorId: "a",
spreadAuthorId: "spread-a", spreadAuthorId: "spread-a",
spreadMetrics: { spreadMetrics: {
"个人视频_近30天_完播率": "28.24%" "内容数据-个人视频-近30天-完播率": "28.24%"
} }
}), }),
expect.objectContaining({ expect.objectContaining({
authorId: "b", authorId: "b",
spreadAuthorId: "spread-b", spreadAuthorId: "spread-b",
spreadMetrics: { spreadMetrics: {
"个人视频_近30天_完播率": "18.24%" "内容数据-个人视频-近30天-完播率": "18.24%"
} }
}) })
]); ]);
@ -1829,8 +1829,7 @@ describe("market-content-entry", () => {
const buildAudienceProfileCsv = vi.fn(() => "profile-csv"); const buildAudienceProfileCsv = vi.fn(() => "profile-csv");
const loadBusinessAbility = vi.fn(async () => ({ const loadBusinessAbility = vi.fn(async () => ({
estimates: {}, estimates: {},
status: "success" as const, 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) {
@ -1918,8 +1917,7 @@ describe("market-content-entry", () => {
})); }));
const loadBusinessAbility = vi.fn(async () => ({ const loadBusinessAbility = vi.fn(async () => ({
estimates: {}, estimates: {},
status: "success" as const, status: "success" as const
videos: {}
})); }));
const loadAudienceProfile = vi.fn(async () => ({ const loadAudienceProfile = vi.fn(async () => ({
age: [{ label: "31-40", value: "60%" }], age: [{ label: "31-40", value: "60%" }],
@ -2051,13 +2049,12 @@ describe("market-content-entry", () => {
]); ]);
window.localStorage.setItem( window.localStorage.setItem(
"sces:audience-profile:selectedHeaders", "sces:audience-profile:selectedHeaders",
JSON.stringify(["内容数据-个人视频-播放量中位数", "秒思api-看后搜数"]) JSON.stringify(["内容数据-个人视频-近30天-播放量中位数", "秒思api-看后搜数"])
); );
const buildAudienceProfileCsv = vi.fn(() => "profile-csv"); const buildAudienceProfileCsv = vi.fn(() => "profile-csv");
const loadBusinessAbility = vi.fn(async () => ({ const loadBusinessAbility = vi.fn(async () => ({
estimates: {}, estimates: {},
status: "success" as const, status: "success" as const
videos: {}
})); }));
const loadAudienceProfile = vi.fn(async () => ({ const loadAudienceProfile = vi.fn(async () => ({
age: [], age: [],
@ -2091,7 +2088,7 @@ describe("market-content-entry", () => {
await waitForMockCall(buildAudienceProfileCsv, 40, 50); await waitForMockCall(buildAudienceProfileCsv, 40, 50);
expect(buildAudienceProfileCsv.mock.calls[0][1]).toEqual({ 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") ?? "[]" window.localStorage.getItem("sces:audience-profile:selectedHeaders") ?? "[]"
) as string[]; ) as string[];
expect(savedHeaders).not.toContain("秒思api-看后搜数"); expect(savedHeaders).not.toContain("秒思api-看后搜数");
expect(savedHeaders).toContain("内容数据-个人视频-播放量中位数"); expect(savedHeaders).toContain("内容数据-个人视频-近30天-播放量中位数");
expect( expect(
document.querySelector('[data-plugin-export-status="text"]')?.textContent document.querySelector('[data-plugin-export-status="text"]')?.textContent
).toContain("字段已保存"); ).toContain("字段已保存");

View File

@ -47,8 +47,8 @@ describe("spread-info", () => {
} }
]); ]);
expect(buildSpreadInfoColumns(personalConfigs).slice(0, 2)).toEqual([ expect(buildSpreadInfoColumns(personalConfigs).slice(0, 2)).toEqual([
"个人视频_近30天_完播率", "内容数据-个人视频-近30天-完播率",
"个人视频_近30天_播放量中位数" "内容数据-个人视频-近30天-播放量中位数"
]); ]);
}); });
@ -78,13 +78,13 @@ describe("spread-info", () => {
type: 2 type: 2
} }
])).toEqual([ ])).toEqual([
"只看指派_排除营销流量_星图视频_近30天_完播率", "内容数据-只看指派-排除营销流量-星图视频-近30天-完播率",
"只看指派_排除营销流量_星图视频_近30天_播放量中位数", "内容数据-只看指派-排除营销流量-星图视频-近30天-播放量中位数",
"只看指派_排除营销流量_星图视频_近30天_互动率", "内容数据-只看指派-排除营销流量-星图视频-近30天-互动率",
"只看指派_排除营销流量_星图视频_近30天_作品平均时长", "内容数据-只看指派-排除营销流量-星图视频-近30天-作品平均时长",
"只看指派_排除营销流量_星图视频_近30天_作品平均评论数", "内容数据-只看指派-排除营销流量-星图视频-近30天-作品平均评论数",
"只看指派_排除营销流量_星图视频_近30天_作品平均点赞数", "内容数据-只看指派-排除营销流量-星图视频-近30天-作品平均点赞数",
"只看指派_排除营销流量_星图视频_近30天_作品平均转发数" "内容数据-只看指派-排除营销流量-星图视频-近30天-作品平均转发数"
]); ]);
}); });
@ -150,13 +150,13 @@ describe("spread-info", () => {
await expect( await expect(
client.loadAuthorSpreadMetrics("7361012802036695050") client.loadAuthorSpreadMetrics("7361012802036695050")
).resolves.toEqual({ ).resolves.toEqual({
"个人视频_近30天_互动率": "4.02%", "内容数据-个人视频-近30天-互动率": "4.02%",
"个人视频_近30天_作品平均点赞数": "494458", "内容数据-个人视频-近30天-作品平均点赞数": "494458",
"个人视频_近30天_作品平均评论数": "7502", "内容数据-个人视频-近30天-作品平均评论数": "7502",
"个人视频_近30天_作品平均时长": "56", "内容数据-个人视频-近30天-作品平均时长": "56",
"个人视频_近30天_作品平均转发数": "188267", "内容数据-个人视频-近30天-作品平均转发数": "188267",
"个人视频_近30天_完播率": "28.24%", "内容数据-个人视频-近30天-完播率": "28.24%",
"个人视频_近30天_播放量中位数": "10913233" "内容数据-个人视频-近30天-播放量中位数": "10913233"
}); });
expect(fetchImpl).toHaveBeenCalledWith( 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", "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",