feat: simplify KOC resource imports
This commit is contained in:
@@ -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<CanonicalHeader, number>, 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,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user