| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172 |
- export function text(value) { return String(value ?? '').trim(); }
- function number(value) { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : 0; }
- function legacyId(row) { return number(row.legacyUserId || row.legacyUserData?.UserID); }
- function normalizePhone(value) {
- const digits = text(value).replace(/\D/g, '');
- if (/^86\d{11}$/.test(digits)) return digits.slice(2);
- return /^1\d{10}$/.test(digits) ? digits : '';
- }
- function phones(row) {
- return new Set([
- row.mobile, row.phone, row.mobilePhoneNumber, row.username,
- row.legacyUserData?.Mobile, row.legacyUserData?.UserName,
- ].map(normalizePhone).filter(Boolean));
- }
- function normalizeName(value) {
- return text(value).toLowerCase().replace(/[\s\p{P}\p{S}]+/gu, '');
- }
- function names(row) {
- return new Set([
- row.nickname, row.nickName, row.realName, row.name,
- row.legacyUserData?.HoneyName, row.legacyUserData?.TrueName,
- ].map(normalizeName).filter((value) => value.length >= 2));
- }
- function intersects(left, right) { return [...left].some((value) => right.has(value)); }
- export function isCanonical(row) { return text(row.sourceKey).startsWith('legacy-sync:user:') && !row.isDeleted; }
- export function isReadingImport(row) {
- return !text(row.sourceKey) && !legacyId(row) && row.isAdmin !== true && !row.isDeleted;
- }
- export function buildPlan(users) {
- const targets = users.filter(isCanonical);
- const sources = users.filter(isReadingImport);
- const targetsByPhone = new Map();
- for (const target of targets) {
- for (const phone of phones(target)) {
- const values = targetsByPhone.get(phone) || [];
- values.push(target);
- targetsByPhone.set(phone, values);
- }
- }
- const merge = [];
- const promote = []; // Never allocate old-system IDs for reading accounts.
- const held = [];
- let nameResolved = 0;
- let ambiguous = 0;
- let unmatched = 0;
- for (const source of sources) {
- if (source.legacyUserData?.ReadingSourceVerified !== 'xiaoshu') {
- held.push({ objectId: source.objectId, reason: 'unverified-source' });
- continue;
- }
- const candidates = new Map();
- for (const phone of phones(source)) {
- for (const candidate of targetsByPhone.get(phone) || []) candidates.set(candidate.objectId, candidate);
- }
- let possible = [...candidates.values()];
- if (possible.length > 1) {
- const sourceNames = names(source);
- const nameMatches = possible.filter((candidate) => intersects(sourceNames, names(candidate)));
- if (nameMatches.length === 1) { possible = nameMatches; nameResolved += 1; }
- }
- if (possible.length === 1) {
- merge.push({ source: source.objectId, target: possible[0].objectId });
- } else {
- held.push({ objectId: source.objectId, reason: possible.length > 1 ? 'ambiguous-identity' : 'unmatched-identity' });
- if (possible.length > 1) ambiguous += 1;
- else unmatched += 1;
- }
- }
- return { targets, sources, merge, promote, held, nameResolved, ambiguous, unmatched };
- }
|