refactor: use spread metrics for content data fields
This commit is contained in:
parent
9eb1fe43cc
commit
0122e63872
@ -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,
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
@ -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[] {
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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: "" },
|
||||||
|
|||||||
@ -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%");
|
||||||
|
|||||||
@ -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("字段已保存");
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user