Files
koc-loop/tests/resource-import.test.mjs
ABAPPLO 582c26e57e feat: 在账号资源库中展示导入的标签字段
KOC 资源导入模板新增「标签」列,导入后写入 accounts.tags 并在
账号资源库面板以 badge 形式展示,导出时也带回标签列以保证往返一致。

- lib/resource-import.ts: 新增 tags 字段与 HEADER 别名(标签/账号标签/人设标签),
  新增 normalizeTags/mergeTags 工具,按顿号规整与合并
- 修复 xlsx 解析器对自闭合空 cell (<c r="G6"/>) 的错位 bug,否则
  行内出现空 cell 会让后续 cell 的列引用读到错误 body(如老茄子行标签丢失)
- db/schema.ts + lib/mvp-db.ts: accounts 表新增 tags VARCHAR(500)
- app/api/resources-import/route.ts: SELECT/INSERT/UPDATE/预览全部带上 tags
- app/api/resources-export/route.ts: 导出表加「标签」列
- app/admin-app.tsx + app/globals.css: 资源卡片新增「账号标签」区,
  导入预览行展示标签
- public/KOC资源导入模板.xlsx: 模板补「标签(选填)」列与填写说明
- mysql/0005_account_tags.sql: 线上 MySQL 迁移
- tests/resource-import.test.mjs: 补 tags 解析/合并用例
2026-08-18 16:39:38 +08:00

133 lines
4.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import test from "node:test";
import { buildRecoveryWorkbook } from "../lib/recovery-workbook.ts";
import {
mergeCooperationSources,
mergeTags,
normalizeProfileUrl,
parseResourceFollowers,
parseResourceImportFile,
resourceImportMissingFields,
resourcePlatformUid,
} from "../lib/resource-import.ts";
test("parses CSV resources and normalizes public profile data", () => {
const csv = [
"账号主页,合作来源",
'"主页https://www.xiaohongshu.com/user/profile/abc123?xsec_token=secret",林林KOC社群',
].join("\n");
const rows = parseResourceImportFile("resources.csv", new TextEncoder().encode(csv));
assert.equal(rows.length, 1);
assert.deepEqual(rows[0], {
rowNumber: 2,
platform: "小红书",
nickname: "",
publicAccountId: "",
profileUrl: "https://www.xiaohongshu.com/user/profile/abc123",
ipLocation: "",
followers: 0,
followersResolved: false,
cooperationSource: "林林KOC社群",
tags: "",
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资源导入",
headers: ["账号主页", "合作来源"],
columnWidths: [48, 24],
rows: [
{
cells: ["https://www.xiaohongshu.com/user/profile/abc123", "存量资源包"],
images: [],
},
],
});
const rows = parseResourceImportFile("resources.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", () => {
const csv = "账号主页,合作来源\n,历史资源\nhttps://www.douyin.com/user/demo,社群";
const rows = parseResourceImportFile("resources.csv", new TextEncoder().encode(csv));
assert.match(rows[0].errors.join(""), /账号主页不能为空/);
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"),
"https://www.xiaohongshu.com/user/profile/abc",
);
assert.equal(
mergeCooperationSources("林林社群、木子", "木子;历史表格"),
"林林社群、木子、历史表格",
);
});
test("parses and normalizes the optional tags column", () => {
const csv = [
"账号链接,标签",
'"https://www.xiaohongshu.com/user/profile/abc123","美食探店, 旅游出行, 美食探店"',
].join("\n");
const [row] = parseResourceImportFile(
"resources.csv",
new TextEncoder().encode(csv),
);
assert.equal(row.tags, "美食探店、旅游出行");
assert.equal(mergeTags("美食探店", "旅游出行;数码汽车"), "美食探店、旅游出行、数码汽车");
});