feat: export audience profiles by author ids

This commit is contained in:
2026-05-18 19:24:15 +08:00
parent 39c4191a95
commit 38da39589f
8 changed files with 667 additions and 4 deletions
+56
View File
@@ -0,0 +1,56 @@
import { describe, expect, test, vi } from "vitest";
import {
buildAuthorBaseInfoUrl,
createAuthorBaseClient,
mapAuthorBaseInfoResponse
} from "../src/content/market/author-base-client";
describe("author-base-client", () => {
test("builds Xingtu author base info url", () => {
expect(
buildAuthorBaseInfoUrl("6866044569306267651", "https://www.xingtu.cn")
).toBe(
"https://www.xingtu.cn/gw/api/author/get_author_base_info?o_author_id=6866044569306267651&platform_source=1&platform_channel=1&recommend=true&need_sec_uid=true&need_linkage_info=true"
);
});
test("maps author nickname into a market record", () => {
expect(mapAuthorBaseInfoResponse("6866044569306267651", {
base_resp: { status_code: 0, status_message: "Success" },
nick_name: "小九儿"
})).toEqual({
authorId: "6866044569306267651",
authorName: "小九儿",
status: "success"
});
});
test("loads author base info from Xingtu", async () => {
const fetchImpl = vi.fn(async () => ({
json: async () => ({
base_resp: { status_code: 0, status_message: "Success" },
nick_name: "小九儿"
}),
ok: true
}));
const client = createAuthorBaseClient({
baseUrl: "https://www.xingtu.cn",
fetchImpl,
timeoutMs: 1000
});
await expect(client.loadAuthorBaseInfo("6866044569306267651")).resolves.toEqual({
authorId: "6866044569306267651",
authorName: "小九儿",
status: "success"
});
expect(fetchImpl).toHaveBeenCalledWith(
"https://www.xingtu.cn/gw/api/author/get_author_base_info?o_author_id=6866044569306267651&platform_source=1&platform_channel=1&recommend=true&need_sec_uid=true&need_linkage_info=true",
expect.objectContaining({
credentials: "include",
method: "GET"
})
);
});
});
+35
View File
@@ -0,0 +1,35 @@
import { describe, expect, test } from "vitest";
import { parseAuthorIds } from "../src/content/market/author-id-input";
describe("author-id-input", () => {
test("parses newline comma and space separated Xingtu author ids", () => {
expect(parseAuthorIds(`
6866044569306267651
7040323176106033165,7088592143119286285
7222310247979810854
`)).toEqual({
duplicates: [],
ids: [
"6866044569306267651",
"7040323176106033165",
"7088592143119286285",
"7222310247979810854"
],
invalidTokens: []
});
});
test("deduplicates ids and reports invalid tokens", () => {
expect(parseAuthorIds(`
6866044569306267651
bad-id
123
6866044569306267651
`)).toEqual({
duplicates: ["6866044569306267651"],
ids: ["6866044569306267651"],
invalidTokens: ["bad-id", "123"]
});
});
});
+89
View File
@@ -328,6 +328,9 @@ describe("market-content-entry", () => {
expect(
document.querySelector('[data-plugin-export-audience-profile="button"]')
).not.toBeNull();
expect(
document.querySelector('[data-plugin-export-audience-profile-by-id="button"]')
).not.toBeNull();
expect(document.querySelector('[data-plugin-batch-submit="button"]')).not.toBeNull();
expect(document.querySelector('[data-plugin-export-status="text"]')).not.toBeNull();
@@ -340,12 +343,17 @@ describe("market-content-entry", () => {
const audienceProfileExportButton = document.querySelector(
'[data-plugin-export-audience-profile="button"]'
) as HTMLButtonElement | null;
const audienceProfileByIdExportButton = document.querySelector(
'[data-plugin-export-audience-profile-by-id="button"]'
) as HTMLButtonElement | null;
expect(exportButton?.style.backgroundColor).toBe("rgb(127, 29, 45)");
expect(batchSubmitButton?.style.backgroundColor).toBe("rgb(127, 29, 45)");
expect(audienceProfileExportButton?.style.backgroundColor).toBe("rgb(127, 29, 45)");
expect(audienceProfileByIdExportButton?.style.backgroundColor).toBe("rgb(127, 29, 45)");
expect(exportButton?.style.color).toBe("rgb(255, 255, 255)");
expect(batchSubmitButton?.style.color).toBe("rgb(255, 255, 255)");
expect(audienceProfileExportButton?.style.color).toBe("rgb(255, 255, 255)");
expect(audienceProfileByIdExportButton?.style.color).toBe("rgb(255, 255, 255)");
});
test("remounts the plugin action bar when the native market action row appears later", async () => {
@@ -1698,6 +1706,87 @@ describe("market-content-entry", () => {
);
});
test("audience profile export by id loads pasted creators without page selection", async () => {
document.body.innerHTML = buildRealMarketFixture([
{ authorId: "111", authorName: "达人 A", price21To60s: "¥11,000" }
]);
const buildAudienceProfileCsv = vi.fn(() => "profile-csv");
const loadAuthorBaseInfo = vi.fn(async (authorId: string) => ({
authorId,
authorName: authorId === "6866044569306267651" ? "小九儿" : "达人 B",
status: "success" as const
}));
const loadBusinessAbility = vi.fn(async () => ({
estimates: {},
status: "success" as const,
videos: {}
}));
const loadAudienceProfile = vi.fn(async () => ({
age: [{ label: "31-40", value: "60%" }],
crowd: [{ label: "都市蓝领", value: "80%" }],
cityTier: [{ label: "一线城市", value: "90%" }],
gender: [{ label: "男性", value: "60%" }],
status: "success" as const
}));
const onCsvReady = vi.fn();
const { createMarketController } = await import("../src/content/market/index");
const controller = trackController(createMarketController({
buildAudienceProfileCsv,
document,
loadAuthorBaseInfo,
loadBusinessAbility,
loadAudienceProfile,
loadAuthorMetrics: async () => ({
success: false,
reason: "request-failed"
}),
onCsvReady,
promptAuthorIds: () => `
6866044569306267651
7040323176106033165
6866044569306267651
bad-id
`,
window
}));
await controller.ready;
click('[data-plugin-export-audience-profile-by-id="button"]');
await waitForMockCall(buildAudienceProfileCsv, 40, 50);
expect(loadAuthorBaseInfo.mock.calls.map(([authorId]) => authorId)).toEqual([
"6866044569306267651",
"7040323176106033165"
]);
expect(loadAudienceProfile).toHaveBeenCalledTimes(6);
expect(loadBusinessAbility).toHaveBeenCalledTimes(2);
expect(buildAudienceProfileCsv).toHaveBeenCalledWith([
expect.objectContaining({
record: expect.objectContaining({
authorId: "6866044569306267651",
authorName: "小九儿",
exportFields: {
ID: "6866044569306267651",
: "小九儿",
: "成功",
: ""
}
})
}),
expect.objectContaining({
record: expect.objectContaining({
authorId: "7040323176106033165",
authorName: "达人 B"
})
})
]);
expect(onCsvReady).toHaveBeenCalledWith(
"profile-csv",
expect.stringMatching(/^达人连接用户画像_按ID导出_\d{8}_\d{4}\.csv$/)
);
});
test(
"selected export keeps a generic loading status while exporting the default paged range",
async () => {