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
+4 -53
View File
@@ -11,10 +11,9 @@ import type {
AudienceProfileKind,
AudienceProfileResult,
BusinessAbilityDurationKind,
BusinessAbilityEstimateMetrics,
BusinessAbilityVideoKind,
BusinessAbilityVideoMetrics
BusinessAbilityEstimateMetrics
} from "./audience-profile-types";
import { buildSpreadInfoColumns } from "./spread-info";
type AudienceProfileCsvColumn = {
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_LAYOUTS: Array<{
@@ -146,7 +122,7 @@ export function listAudienceProfileSelectableFieldGroups(): AudienceProfileCsvFi
label: "秒思api数据"
},
{
headers: buildBusinessVideoColumns().map((column) => column.header),
headers: buildSpreadInfoColumns(),
label: "内容数据"
},
{
@@ -184,19 +160,7 @@ function listAudienceProfileSelectableHeaders(): string[] {
}
function buildBusinessAbilityColumns(): AudienceProfileCsvColumn[] {
return [...buildBusinessVideoColumns(), ...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)
}))
)
];
return buildBusinessEstimateColumns();
}
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(
row: AudienceProfileExportRow,
durationKey: BusinessAbilityDurationKind,
@@ -42,21 +42,6 @@ export interface AudienceProfileExportRow {
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"
@@ -74,7 +59,6 @@ export interface BusinessAbilitySuccess {
Record<BusinessAbilityDurationKind, BusinessAbilityEstimateMetrics>
>;
status: "success";
videos: Partial<Record<BusinessAbilityVideoKind, BusinessAbilityVideoMetrics>>;
}
export interface BusinessAbilityFailure {
+4 -95
View File
@@ -3,8 +3,7 @@ import type {
BusinessAbilityDurationKind,
BusinessAbilityEstimateMetrics,
BusinessAbilityResult,
BusinessAbilitySuccess,
BusinessAbilityVideoMetrics
BusinessAbilitySuccess
} from "./audience-profile-types";
interface FetchResponseLike {
@@ -23,11 +22,6 @@ interface BusinessAbilityClientOptions {
timeoutMs?: number;
}
const VIDEO_TYPES = {
personalVideo: 1,
xingtuVideo: 2
} as const;
export function createBusinessAbilityClient(
options: BusinessAbilityClientOptions = {}
) {
@@ -37,33 +31,20 @@ export function createBusinessAbilityClient(
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) {
if (!estimates.ok) {
return {
failureReason:
personalVideo.failureReason ??
xingtuVideo.failureReason ??
estimates.failureReason,
failureReason: estimates.failureReason,
status: "failed"
};
}
return {
estimates: mapBusinessAbilityEstimateResponse(estimates.payload),
status: "success",
videos: {
personalVideo: mapBusinessAbilityVideoResponse(personalVideo.payload),
xingtuVideo: mapBusinessAbilityVideoResponse(xingtuVideo.payload)
}
status: "success"
} 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(
authorId: string,
baseUrl: string
@@ -129,25 +94,6 @@ export function buildBusinessAbilityEstimateUrl(
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>> {
@@ -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 {
if (value === null) {
return "缺失";
@@ -238,19 +160,6 @@ function formatFixedDecimal(value: number | null, digits: number): string {
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 {
if (typeof value === "number" && Number.isFinite(value)) {
return value;
+6 -1
View File
@@ -265,6 +265,7 @@ export function createMarketController(options: CreateMarketControllerOptions) {
try {
const selectedRecords = filterRecordsBySelectionStrict(
await exportRecords(exportTarget.target, "画像导出中", {
includeSpreadMetrics: true,
showDetailedProgress: false
})
);
@@ -878,13 +879,17 @@ export function createMarketController(options: CreateMarketControllerOptions) {
loadAuthorBaseInfoSafe(authorId),
loadAuthorMetricsSafe(authorId)
]);
const spreadMetrics = baseRecord.spreadAuthorId
? await loadSpreadMetrics(baseRecord.spreadAuthorId)
: {};
const recordForRequests = {
...baseRecord,
authorName: baseRecord.authorName || authorId,
...(metricsResult.success ? { rates: metricsResult.rates } : {}),
...(backendMetrics
? { backendMetrics, backendMetricsStatus: "success" as const }
: {})
: {}),
...(Object.keys(spreadMetrics).length > 0 ? { spreadMetrics } : {})
};
const [profiles, businessAbility] = await Promise.all([
loadAudienceProfileSet(recordForRequests),
+1 -1
View File
@@ -226,7 +226,7 @@ function buildSpreadInfoColumnHeader(
config: SpreadInfoConfig,
metric: SpreadInfoMetricDefinition
): string {
return [...buildConfigPrefixParts(config), metric.label].join("_");
return ["内容数据", ...buildConfigPrefixParts(config), metric.label].join("-");
}
function buildConfigPrefixParts(config: SpreadInfoConfig): string[] {