From 51934b0638a592cde4ab80281a38b55aa0617837 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B7=AB=E5=87=A4=E8=90=8D?= Date: Wed, 12 Aug 2026 17:51:57 +0800 Subject: [PATCH] feat: simplify KOC resource imports --- app/admin-app.tsx | 6 +-- app/api/resources-import/route.ts | 67 +++++++++++++++++++----------- lib/resource-import.ts | 60 +++++++++++++++++++++++--- public/KOC资源导入模板.xlsx | Bin 6396 -> 7580 bytes tests/resource-import.test.mjs | 53 ++++++++++++++++++++++- 5 files changed, 151 insertions(+), 35 deletions(-) diff --git a/app/admin-app.tsx b/app/admin-app.tsx index b5f284a..5f63210 100644 --- a/app/admin-app.tsx +++ b/app/admin-app.tsx @@ -2261,9 +2261,9 @@ function ResourcesPage({
- 1

填写账号主页与合作来源

+ 1

账号链接必填,其余资料选填

- 2

系统解析公开账号资料

+ 2

系统只补全空缺的公开资料

3

确认后写入资源库

@@ -2282,7 +2282,7 @@ function ResourcesPage({ {!importPreview && (
-
还没有模板?只需填写小红书账号主页;合作来源可选填,账号名称、小红书号、IP属地和粉丝数会自动解析。
+
还没有模板?只有账号链接必填;已有昵称、账号ID、IP属地、粉丝数和合作来源可直接填写,系统只补全空缺字段。
下载模板
)} diff --git a/app/api/resources-import/route.ts b/app/api/resources-import/route.ts index 3c9fb64..86a4788 100644 --- a/app/api/resources-import/route.ts +++ b/app/api/resources-import/route.ts @@ -13,6 +13,7 @@ import { parseResourceImportFile, RESOURCE_IMPORT_MAX_BYTES, resourcePlatformUid, + resourceImportMissingFields, type ResourceImportRow, } from "../../../lib/resource-import"; @@ -86,20 +87,38 @@ async function enrichRows(rows: ResourceImportRow[], accounts: AccountRow[]) { return row; } const existing = existingByProfile.get(identityKey(row.platform, row.profileUrl)); - if (existing?.nickname && existing.public_account_id) { - return { - ...row, - nickname: existing.nickname, - publicAccountId: existing.public_account_id, - ipLocation: existing.ip_location || "待识别", - followers: Number(existing.followers || 0), - }; + const existingIpLocation = + existing?.ip_location && existing.ip_location !== "待识别" + ? existing.ip_location + : ""; + const baseline: ResourceImportRow = { + ...row, + nickname: row.nickname || existing?.nickname || "", + publicAccountId: row.publicAccountId || existing?.public_account_id || "", + ipLocation: row.ipLocation || existingIpLocation, + followers: row.followersResolved + ? row.followers + : Number(existing?.followers || 0), + followersResolved: + row.followersResolved || Number(existing?.followers || 0) > 0, + }; + if (resourceImportMissingFields(baseline).length === 0) { + return baseline; } - let details = await resolveXhsProfileDetailsFromMcp(row.profileUrl, mcpConfig) - .catch(() => resolveXhsPublicAccountDetails(row.profileUrl)); - if (!details.nickname || !details.redId || details.followers === null) { - const publicDetails = await resolveXhsPublicAccountDetails(row.profileUrl); + let details = await resolveXhsProfileDetailsFromMcp(row.profileUrl, mcpConfig).catch( + () => ({ nickname: null, redId: null, followers: null, ipLocation: null }), + ); + const mcpResult = { + nickname: baseline.nickname || details.nickname?.trim() || "", + publicAccountId: baseline.publicAccountId || details.redId?.trim() || "", + ipLocation: baseline.ipLocation || details.ipLocation?.trim() || "", + followersResolved: baseline.followersResolved || details.followers !== null, + }; + if (resourceImportMissingFields(mcpResult).length > 0) { + const publicDetails = await resolveXhsPublicAccountDetails(row.profileUrl).catch( + () => ({ nickname: null, redId: null, followers: null, ipLocation: null }), + ); details = { nickname: details.nickname || publicDetails.nickname, redId: details.redId || publicDetails.redId, @@ -107,18 +126,16 @@ async function enrichRows(rows: ResourceImportRow[], accounts: AccountRow[]) { ipLocation: details.ipLocation || publicDetails.ipLocation, }; } - const errors = [...row.errors]; - const nickname = details.nickname?.trim() || existing?.nickname || ""; - const publicAccountId = details.redId?.trim() || existing?.public_account_id || ""; - if (!nickname) errors.push("无法识别账号名称,请确认主页可公开访问"); - if (!publicAccountId) errors.push("无法识别小红书号,请确认主页可公开访问"); return { - ...row, - nickname, - publicAccountId, - ipLocation: details.ipLocation?.trim() || existing?.ip_location || "待识别", - followers: details.followers ?? Number(existing?.followers || 0), - errors, + ...baseline, + nickname: baseline.nickname || details.nickname?.trim() || "待识别账号", + publicAccountId: baseline.publicAccountId || details.redId?.trim() || "", + ipLocation: baseline.ipLocation || details.ipLocation?.trim() || "待识别", + followers: baseline.followersResolved + ? baseline.followers + : (details.followers ?? 0), + followersResolved: + baseline.followersResolved || details.followers !== null, }; }); } @@ -251,7 +268,7 @@ export async function POST(request: Request) { profile_url = CASE WHEN ? != '' THEN ? ELSE profile_url END, ip_location = CASE WHEN ? != '' AND ? != '待识别' THEN ? ELSE ip_location END, - followers = CASE WHEN ? > 0 OR followers = 0 THEN ? ELSE followers END, + followers = CASE WHEN ? = 1 THEN ? ELSE followers END, cooperation_source = ?, last_seen_at = CURRENT_TIMESTAMP WHERE id = ?`, @@ -265,7 +282,7 @@ export async function POST(request: Request) { row.ipLocation, row.ipLocation, row.ipLocation, - row.followers, + row.followersResolved ? 1 : 0, row.followers, row.cooperationSource, row.accountId, diff --git a/lib/resource-import.ts b/lib/resource-import.ts index 116d6af..cb14ef7 100644 --- a/lib/resource-import.ts +++ b/lib/resource-import.ts @@ -11,12 +11,17 @@ export type ResourceImportRow = { profileUrl: string; ipLocation: string; followers: number; + followersResolved: boolean; cooperationSource: string; errors: string[]; }; const HEADER_ALIASES = { - profileUrl: ["账号主页", "账号主页链接", "主页链接", "账号链接"], + profileUrl: ["账号链接", "账号主页", "账号主页链接", "主页链接"], + nickname: ["账号昵称", "账号名称", "昵称"], + publicAccountId: ["账号ID", "账号号", "小红书号", "抖音号"], + ipLocation: ["IP属地", "IP地址", "IP地区", "IP所在地"], + followers: ["粉丝数", "粉丝", "粉丝量"], cooperationSource: ["合作来源", "资源来源", "历史合作来源"], } as const; @@ -120,7 +125,10 @@ function parseCsv(text: string) { } function normalizeHeader(value: string) { - return value.replace(/[\s_\-()()]/g, "").toLocaleLowerCase("zh-CN"); + return value + .replace(/[\s_\-()()]/g, "") + .replace(/必填|选填/g, "") + .toLocaleLowerCase("zh-CN"); } function canonicalHeader(value: string): CanonicalHeader | null { @@ -186,6 +194,40 @@ function valueAt(row: string[], mapping: Map, key: Cano return index === undefined ? "" : String(row[index] ?? "").trim(); } +export function parseResourceFollowers(value: string) { + const normalized = value.trim().replace(/[,,\s]/g, "").replace(/\+$/, ""); + if (!normalized) return { value: 0, resolved: false, valid: true }; + const match = normalized.match(/^(\d+(?:\.\d+)?)(万|w|W|千|k|K)?$/); + if (!match) return { value: 0, resolved: false, valid: false }; + const multiplier = + match[2] === "万" || match[2]?.toLowerCase() === "w" + ? 10_000 + : match[2] === "千" || match[2]?.toLowerCase() === "k" + ? 1_000 + : 1; + return { + value: Math.round(Number(match[1]) * multiplier), + resolved: true, + valid: true, + }; +} + +export function resourceImportMissingFields( + row: Pick< + ResourceImportRow, + "nickname" | "publicAccountId" | "ipLocation" | "followersResolved" + >, +) { + const missing: string[] = []; + if (!row.nickname.trim()) missing.push("nickname"); + if (!row.publicAccountId.trim()) missing.push("publicAccountId"); + if (!row.ipLocation.trim() || row.ipLocation.trim() === "待识别") { + missing.push("ipLocation"); + } + if (!row.followersResolved) missing.push("followers"); + return missing; +} + function normalizeRows(rows: string[][]) { const header = findHeader(rows); if (!header) { @@ -198,18 +240,24 @@ function normalizeRows(rows: string[][]) { const rawProfileUrl = valueAt(source, header.mapping, "profileUrl"); const profileUrl = normalizeProfileUrl(rawProfileUrl); const platform = platformFromProfileUrl(profileUrl); + const rawFollowers = valueAt(source, header.mapping, "followers"); + const parsedFollowers = parseResourceFollowers(rawFollowers); const errors: string[] = []; if (!rawProfileUrl) errors.push("账号主页不能为空"); else if (!profileUrl) errors.push("账号主页链接格式不正确"); else if (!platform) errors.push("当前自动解析仅支持小红书账号主页"); + if (!parsedFollowers.valid) { + errors.push("粉丝数格式不正确,请填写数字或如 1.3万、10+"); + } result.push({ rowNumber: index + 1, platform, - nickname: "", - publicAccountId: "", + nickname: valueAt(source, header.mapping, "nickname"), + publicAccountId: valueAt(source, header.mapping, "publicAccountId"), profileUrl, - ipLocation: "待识别", - followers: 0, + ipLocation: valueAt(source, header.mapping, "ipLocation"), + followers: parsedFollowers.value, + followersResolved: parsedFollowers.resolved, cooperationSource: valueAt(source, header.mapping, "cooperationSource"), errors, }); diff --git a/public/KOC资源导入模板.xlsx b/public/KOC资源导入模板.xlsx index 68293bd146b689ea26bda71493ca3cb0a1ad7a26..15346f91b5315f4198e5b65ae258f8cb1eb09892 100644 GIT binary patch delta 5657 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 Ca+A0K delta 4453 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 E0L`B!_5c6? diff --git a/tests/resource-import.test.mjs b/tests/resource-import.test.mjs index 54562ca..456f2a8 100644 --- a/tests/resource-import.test.mjs +++ b/tests/resource-import.test.mjs @@ -4,7 +4,9 @@ import { buildRecoveryWorkbook } from "../lib/recovery-workbook.ts"; import { mergeCooperationSources, normalizeProfileUrl, + parseResourceFollowers, parseResourceImportFile, + resourceImportMissingFields, resourcePlatformUid, } from "../lib/resource-import.ts"; @@ -21,14 +23,50 @@ test("parses CSV resources and normalizes public profile data", () => { nickname: "", publicAccountId: "", profileUrl: "https://www.xiaohongshu.com/user/profile/abc123", - ipLocation: "待识别", + ipLocation: "", followers: 0, + followersResolved: false, cooperationSource: "林林KOC社群", errors: [], }); assert.equal(resourcePlatformUid(rows[0]), "abc123"); }); +test("uses optional account fields directly and only requires the profile URL", () => { + const csv = [ + "账号链接,账号昵称,账号ID,IP属地,粉丝数,合作来源", + "https://www.xiaohongshu.com/user/profile/abc123,番茄不炒蛋,4171542126,江西,10+,历史资源", + ].join("\n"); + const [row] = parseResourceImportFile( + "resources.csv", + new TextEncoder().encode(csv), + ); + assert.equal(row.nickname, "番茄不炒蛋"); + assert.equal(row.publicAccountId, "4171542126"); + assert.equal(row.ipLocation, "江西"); + assert.equal(row.followers, 10); + assert.equal(row.followersResolved, true); + assert.deepEqual(resourceImportMissingFields(row), []); +}); + +test("normalizes common follower formats and identifies missing enrichment fields", () => { + assert.deepEqual(parseResourceFollowers("1.3万"), { + value: 13_000, + resolved: true, + valid: true, + }); + assert.deepEqual(parseResourceFollowers("10+"), { + value: 10, + resolved: true, + valid: true, + }); + assert.deepEqual(parseResourceFollowers(""), { + value: 0, + resolved: false, + valid: true, + }); +}); + test("parses the first matching worksheet from an XLSX workbook", () => { const workbook = buildRecoveryWorkbook({ sheetName: "KOC资源导入", @@ -45,6 +83,7 @@ test("parses the first matching worksheet from an XLSX workbook", () => { assert.equal(rows[0].platform, "小红书"); assert.equal(rows[0].cooperationSource, "存量资源包"); assert.equal(rows[0].errors.length, 0); + assert.equal(rows[0].followersResolved, false); }); test("reports invalid required fields without hiding valid rows", () => { @@ -54,6 +93,18 @@ test("reports invalid required fields without hiding valid rows", () => { assert.match(rows[1].errors.join(";"), /仅支持小红书账号主页/); }); +test("rejects invalid optional follower values without requiring other optional fields", () => { + const csv = [ + "账号链接,粉丝数", + "https://www.xiaohongshu.com/user/profile/abc123,很多", + ].join("\n"); + const [row] = parseResourceImportFile( + "resources.csv", + new TextEncoder().encode(csv), + ); + assert.match(row.errors.join(";"), /粉丝数格式不正确/); +}); + test("normalizes profile URLs and merges cooperation sources", () => { assert.equal( normalizeProfileUrl("https://www.xiaohongshu.com/user/profile/abc/?foo=1#top"),