feat: make audience export fields selective

This commit is contained in:
wxs
2026-07-27 15:37:35 +08:00
parent e062be6431
commit e15bc0657a
9 changed files with 858 additions and 1979 deletions
+129 -1
View File
@@ -6,9 +6,11 @@ import {
buildSpreadInfoUrl,
createSpreadInfoClient,
DEFAULT_SPREAD_INFO_CONFIGS,
filterUnloadedSpreadInfoConfigs,
matchesSpreadMetricRule,
normalizeSpreadInfoConfig,
mapSpreadInfoResponse
mapSpreadInfoResponse,
selectSpreadInfoConfigsForHeaders
} from "../src/content/market/spread-info";
describe("spread-info", () => {
@@ -90,6 +92,49 @@ describe("spread-info", () => {
]);
});
test("selects only unique request configs required by chosen columns", () => {
expect(
selectSpreadInfoConfigsForHeaders([
"内容数据-个人视频-近30天-完播率",
"内容数据-个人视频-近30天-作品平均评论数",
"内容数据-只看指派-排除营销流量-星图视频-近90天-互动率",
"效果预估-20-60s视频-预期CPM"
])
).toEqual([
{
flowType: 0,
onlyAssign: false,
range: 2,
type: 1
},
{
flowType: 1,
onlyAssign: true,
range: 3,
type: 2
}
]);
});
test("keeps newly selected configs when other content configs are cached", () => {
const personalConfig = DEFAULT_SPREAD_INFO_CONFIGS[0];
const xingtuConfig = DEFAULT_SPREAD_INFO_CONFIGS.find(
(config) =>
config.type === 2 &&
config.onlyAssign &&
config.flowType === 1 &&
config.range === 3
);
expect(xingtuConfig).toBeDefined();
expect(
filterUnloadedSpreadInfoConfigs(
[personalConfig, xingtuConfig!],
{ "内容数据-个人视频-近30天-完播率": "28%" }
)
).toEqual([xingtuConfig]);
});
test("maps spread info response values into display values", () => {
expect(
mapSpreadInfoResponse({
@@ -169,6 +214,89 @@ describe("spread-info", () => {
);
});
test("requests only the explicitly selected spread configs", async () => {
const fetchImpl = vi.fn(async () => ({
json: async () => ({ play_over_rate: { value: 2824 } }),
ok: true
}));
const client = createSpreadInfoClient({ fetchImpl });
const selectedConfig = {
flowType: 1 as const,
onlyAssign: true,
range: 3 as const,
type: 2 as const
};
await client.loadAuthorSpreadMetrics("7361012802036695050", [selectedConfig]);
expect(fetchImpl).toHaveBeenCalledTimes(1);
expect(fetchImpl.mock.calls[0][0]).toContain(
"type=2&flow_type=1&only_assign=true&range=3"
);
});
test("throws an HTTP error instead of returning empty metrics", async () => {
const client = createSpreadInfoClient({
fetchImpl: async () => ({
json: async () => ({}),
ok: false,
status: 429,
statusText: "Too Many Requests"
})
});
await expect(
client.loadAuthorSpreadMetricSnapshot("7361012802036695050", {
flowType: 0,
onlyAssign: true,
range: 2,
type: 2
})
).rejects.toThrow("HTTP 429: Too Many Requests");
});
test("throws an explicit error when Xingtu reports business rate limiting", async () => {
const client = createSpreadInfoClient({
fetchImpl: async () => ({
json: async () => ({
base_resp: {
status_code: 31157,
status_message: "您访问过于频繁,请24小时后重试"
}
}),
ok: true
})
});
await expect(
client.loadAuthorSpreadMetricSnapshot("7361012802036695050", {
flowType: 0,
onlyAssign: true,
range: 2,
type: 2
})
).rejects.toThrow(
"星图传播数据接口被限流 (status_code=31157): 您访问过于频繁,请24小时后重试"
);
});
test("propagates network errors instead of returning empty metrics", async () => {
const client = createSpreadInfoClient({
fetchImpl: async () => {
throw new Error("network unavailable");
}
});
await expect(
client.loadAuthorSpreadMetricSnapshot("7361012802036695050", {
flowType: 0,
onlyAssign: true,
range: 2,
type: 2
})
).rejects.toThrow("network unavailable");
});
test("normalizes fixed personal-video parameters before grouping", () => {
expect(
normalizeSpreadInfoConfig({