fix: match Xingtu profile endpoints
This commit is contained in:
parent
c8287e8d8e
commit
ca9ce02db5
@ -16,6 +16,16 @@ type FetchLike = (
|
|||||||
init?: RequestInit
|
init?: RequestInit
|
||||||
) => Promise<FetchResponseLike>;
|
) => Promise<FetchResponseLike>;
|
||||||
|
|
||||||
|
export type AudienceProfileRequestTarget =
|
||||||
|
| {
|
||||||
|
linkType: number;
|
||||||
|
source: "audienceDistribution";
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
authorType: number;
|
||||||
|
source: "fansDistribution";
|
||||||
|
};
|
||||||
|
|
||||||
interface AudienceProfileClientOptions {
|
interface AudienceProfileClientOptions {
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
fetchImpl?: FetchLike;
|
fetchImpl?: FetchLike;
|
||||||
@ -49,10 +59,13 @@ const GENDER_LABELS: Record<string, string> = {
|
|||||||
const AGE_ORDER = ["18-23", "24-30", "31-40", "41-50", "50+"];
|
const AGE_ORDER = ["18-23", "24-30", "31-40", "41-50", "50+"];
|
||||||
const CITY_TIER_ORDER = ["一线", "新一线", "二线", "三线", "四线", "五线"];
|
const CITY_TIER_ORDER = ["一线", "新一线", "二线", "三线", "四线", "五线"];
|
||||||
|
|
||||||
export const AUDIENCE_PROFILE_LINK_TYPES: Record<AudienceProfileKind, number> = {
|
export const AUDIENCE_PROFILE_TARGETS: Record<
|
||||||
audience: 3,
|
AudienceProfileKind,
|
||||||
fans: 1,
|
AudienceProfileRequestTarget
|
||||||
longtimeFans: 4
|
> = {
|
||||||
|
audience: { linkType: 5, source: "audienceDistribution" },
|
||||||
|
fans: { authorType: 1, source: "fansDistribution" },
|
||||||
|
longtimeFans: { authorType: 5, source: "fansDistribution" }
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createAudienceProfileClient(
|
export function createAudienceProfileClient(
|
||||||
@ -65,14 +78,14 @@ export function createAudienceProfileClient(
|
|||||||
return {
|
return {
|
||||||
async loadAudienceProfile(
|
async loadAudienceProfile(
|
||||||
record: MarketRecord,
|
record: MarketRecord,
|
||||||
linkType: number
|
target: AudienceProfileRequestTarget
|
||||||
): Promise<AudienceProfileResult> {
|
): Promise<AudienceProfileResult> {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetchImpl(
|
const response = await fetchImpl(
|
||||||
buildAudienceProfileUrl(record.authorId, baseUrl, linkType),
|
buildAudienceProfileUrl(record.authorId, baseUrl, target),
|
||||||
{
|
{
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@ -106,13 +119,22 @@ export function createAudienceProfileClient(
|
|||||||
export function buildAudienceProfileUrl(
|
export function buildAudienceProfileUrl(
|
||||||
authorId: string,
|
authorId: string,
|
||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
linkType = 3
|
target: AudienceProfileRequestTarget
|
||||||
): string {
|
): string {
|
||||||
const url = new URL("/gw/api/data_sp/author_audience_distribution", baseUrl);
|
const url = new URL(
|
||||||
|
target.source === "audienceDistribution"
|
||||||
|
? "/gw/api/data_sp/author_audience_distribution"
|
||||||
|
: "/gw/api/data_sp/get_author_fans_distribution",
|
||||||
|
baseUrl
|
||||||
|
);
|
||||||
url.searchParams.set("o_author_id", authorId);
|
url.searchParams.set("o_author_id", authorId);
|
||||||
url.searchParams.set("platform_source", "1");
|
url.searchParams.set("platform_source", "1");
|
||||||
url.searchParams.set("platform_channel", "1");
|
if (target.source === "audienceDistribution") {
|
||||||
url.searchParams.set("link_type", String(linkType));
|
url.searchParams.set("platform_channel", "1");
|
||||||
|
url.searchParams.set("link_type", String(target.linkType));
|
||||||
|
} else {
|
||||||
|
url.searchParams.set("author_type", String(target.authorType));
|
||||||
|
}
|
||||||
return url.toString();
|
return url.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import { buildMarketCsv } from "./csv-exporter";
|
import { buildMarketCsv } from "./csv-exporter";
|
||||||
import { buildAudienceProfileCsv } from "./audience-profile-csv";
|
import { buildAudienceProfileCsv } from "./audience-profile-csv";
|
||||||
import {
|
import {
|
||||||
AUDIENCE_PROFILE_LINK_TYPES,
|
AUDIENCE_PROFILE_TARGETS,
|
||||||
createAudienceProfileClient
|
createAudienceProfileClient,
|
||||||
|
type AudienceProfileRequestTarget
|
||||||
} from "./audience-profile-client";
|
} from "./audience-profile-client";
|
||||||
import { promptForBatchName } from "./batch-name-dialog";
|
import { promptForBatchName } from "./batch-name-dialog";
|
||||||
import { createBatchPayload, type BatchPayload } from "./batch-payload";
|
import { createBatchPayload, type BatchPayload } from "./batch-payload";
|
||||||
@ -58,7 +59,7 @@ export interface CreateMarketControllerOptions {
|
|||||||
getAuthState?: () => Promise<AuthStateValue>;
|
getAuthState?: () => Promise<AuthStateValue>;
|
||||||
loadAudienceProfile?: (
|
loadAudienceProfile?: (
|
||||||
record: MarketRecord,
|
record: MarketRecord,
|
||||||
linkType: number
|
target: AudienceProfileRequestTarget
|
||||||
) => Promise<AudienceProfileResult>;
|
) => Promise<AudienceProfileResult>;
|
||||||
loadAuthorMetrics?: (authorId: string) => Promise<MarketApiResult>;
|
loadAuthorMetrics?: (authorId: string) => Promise<MarketApiResult>;
|
||||||
searchBackendMetrics?: (starIds: string[]) => Promise<
|
searchBackendMetrics?: (starIds: string[]) => Promise<
|
||||||
@ -101,13 +102,13 @@ export function createMarketController(options: CreateMarketControllerOptions) {
|
|||||||
readBatchSubmitAck(sendRuntimeMessage, payload));
|
readBatchSubmitAck(sendRuntimeMessage, payload));
|
||||||
const audienceProfileTargets: Array<{
|
const audienceProfileTargets: Array<{
|
||||||
kind: AudienceProfileKind;
|
kind: AudienceProfileKind;
|
||||||
linkType: number;
|
target: AudienceProfileRequestTarget;
|
||||||
}> = [
|
}> = [
|
||||||
{ kind: "audience", linkType: AUDIENCE_PROFILE_LINK_TYPES.audience },
|
{ kind: "audience", target: AUDIENCE_PROFILE_TARGETS.audience },
|
||||||
{ kind: "fans", linkType: AUDIENCE_PROFILE_LINK_TYPES.fans },
|
{ kind: "fans", target: AUDIENCE_PROFILE_TARGETS.fans },
|
||||||
{
|
{
|
||||||
kind: "longtimeFans",
|
kind: "longtimeFans",
|
||||||
linkType: AUDIENCE_PROFILE_LINK_TYPES.longtimeFans
|
target: AUDIENCE_PROFILE_TARGETS.longtimeFans
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
let activeProgressLabel = "导出中";
|
let activeProgressLabel = "导出中";
|
||||||
@ -664,9 +665,9 @@ export function createMarketController(options: CreateMarketControllerOptions) {
|
|||||||
): Promise<AudienceProfileExportRow["profiles"]> {
|
): Promise<AudienceProfileExportRow["profiles"]> {
|
||||||
const profiles = {} as AudienceProfileExportRow["profiles"];
|
const profiles = {} as AudienceProfileExportRow["profiles"];
|
||||||
|
|
||||||
for (const { kind, linkType } of audienceProfileTargets) {
|
for (const { kind, target } of audienceProfileTargets) {
|
||||||
try {
|
try {
|
||||||
profiles[kind] = await loadAudienceProfile(record, linkType);
|
profiles[kind] = await loadAudienceProfile(record, target);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
profiles[kind] = {
|
profiles[kind] = {
|
||||||
failureReason:
|
failureReason:
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { describe, expect, test, vi } from "vitest";
|
import { describe, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
AUDIENCE_PROFILE_TARGETS,
|
||||||
createAudienceProfileClient,
|
createAudienceProfileClient,
|
||||||
mapAudienceProfileResponse
|
mapAudienceProfileResponse
|
||||||
} from "../src/content/market/audience-profile-client";
|
} from "../src/content/market/audience-profile-client";
|
||||||
@ -21,10 +22,10 @@ describe("audience-profile-client", () => {
|
|||||||
authorId: "7294473194298146854",
|
authorId: "7294473194298146854",
|
||||||
authorName: "奇奇de海洋",
|
authorName: "奇奇de海洋",
|
||||||
status: "success"
|
status: "success"
|
||||||
}, 3);
|
}, AUDIENCE_PROFILE_TARGETS.audience);
|
||||||
|
|
||||||
expect(fetchImpl).toHaveBeenCalledWith(
|
expect(fetchImpl).toHaveBeenCalledWith(
|
||||||
"https://www.xingtu.cn/gw/api/data_sp/author_audience_distribution?o_author_id=7294473194298146854&platform_source=1&platform_channel=1&link_type=3",
|
"https://www.xingtu.cn/gw/api/data_sp/author_audience_distribution?o_author_id=7294473194298146854&platform_source=1&platform_channel=1&link_type=5",
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
method: "GET"
|
method: "GET"
|
||||||
@ -42,6 +43,32 @@ describe("audience-profile-client", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("loads fans and iron fan profiles from Xingtu fan distribution endpoint", async () => {
|
||||||
|
const fetchImpl = vi.fn(async () => ({
|
||||||
|
json: async () => buildAudiencePayload(),
|
||||||
|
ok: true
|
||||||
|
}));
|
||||||
|
const client = createAudienceProfileClient({
|
||||||
|
baseUrl: "https://www.xingtu.cn",
|
||||||
|
fetchImpl,
|
||||||
|
timeoutMs: 1000
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.loadAudienceProfile({
|
||||||
|
authorId: "7294473194298146854",
|
||||||
|
authorName: "奇奇de海洋",
|
||||||
|
status: "success"
|
||||||
|
}, AUDIENCE_PROFILE_TARGETS.longtimeFans);
|
||||||
|
|
||||||
|
expect(fetchImpl).toHaveBeenCalledWith(
|
||||||
|
"https://www.xingtu.cn/gw/api/data_sp/get_author_fans_distribution?o_author_id=7294473194298146854&platform_source=1&author_type=5",
|
||||||
|
expect.objectContaining({
|
||||||
|
credentials: "include",
|
||||||
|
method: "GET"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("maps Xingtu audience distribution payload into named profile sections", () => {
|
test("maps Xingtu audience distribution payload into named profile sections", () => {
|
||||||
const result = mapAudienceProfileResponse(buildAudiencePayload());
|
const result = mapAudienceProfileResponse(buildAudiencePayload());
|
||||||
|
|
||||||
|
|||||||
@ -1624,8 +1624,8 @@ describe("market-content-entry", () => {
|
|||||||
{ authorId: "222", authorName: "达人 B", price21To60s: "¥22,000" }
|
{ authorId: "222", authorName: "达人 B", price21To60s: "¥22,000" }
|
||||||
]);
|
]);
|
||||||
const buildAudienceProfileCsv = vi.fn(() => "profile-csv");
|
const buildAudienceProfileCsv = vi.fn(() => "profile-csv");
|
||||||
const loadAudienceProfile = vi.fn(async (_record, linkType: number) => {
|
const loadAudienceProfile = vi.fn(async (_record, target) => {
|
||||||
if (linkType === 4) {
|
if (target.source === "fansDistribution" && target.authorType === 5) {
|
||||||
return {
|
return {
|
||||||
age: [{ label: "31-40", value: "30%" }],
|
age: [{ label: "31-40", value: "30%" }],
|
||||||
crowd: [{ label: "都市蓝领", value: "50%" }],
|
crowd: [{ label: "都市蓝领", value: "50%" }],
|
||||||
@ -1666,10 +1666,10 @@ describe("market-content-entry", () => {
|
|||||||
await waitForMockCall(buildAudienceProfileCsv, 40, 50);
|
await waitForMockCall(buildAudienceProfileCsv, 40, 50);
|
||||||
|
|
||||||
expect(loadAudienceProfile).toHaveBeenCalledTimes(3);
|
expect(loadAudienceProfile).toHaveBeenCalledTimes(3);
|
||||||
expect(loadAudienceProfile.mock.calls.map(([, linkType]) => linkType)).toEqual([
|
expect(loadAudienceProfile.mock.calls.map(([, target]) => target)).toEqual([
|
||||||
3,
|
{ linkType: 5, source: "audienceDistribution" },
|
||||||
1,
|
{ authorType: 1, source: "fansDistribution" },
|
||||||
4
|
{ authorType: 5, source: "fansDistribution" }
|
||||||
]);
|
]);
|
||||||
expect(buildAudienceProfileCsv).toHaveBeenCalledWith([
|
expect(buildAudienceProfileCsv).toHaveBeenCalledWith([
|
||||||
{
|
{
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user