feat(MemberService): enhance member ranking matching with improved name normalization and indexing
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 1m0s
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 1m0s
This commit is contained in:
@@ -742,22 +742,51 @@ class MemberService {
|
||||
const notFound = [];
|
||||
const matched = [];
|
||||
|
||||
// Indizes für schnelleres Matching (nach personId und Name)
|
||||
// Indizes für schnelleres Matching (nach personId und Name).
|
||||
// Namen aus der Mitgliederverwaltung können unsichtbare Unicode-Zeichen,
|
||||
// abweichende Bindestriche oder Akzente enthalten. Die Rangliste liefert
|
||||
// dagegen eine normalisierte Schreibweise. Deshalb zusätzlich einen
|
||||
// fehlertoleranten Namensschlüssel führen.
|
||||
const buildNameKey = (firstName, lastName, { loose = false } = {}) => {
|
||||
const name = `${firstName || ''} ${lastName || ''}`
|
||||
.normalize('NFKD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.replace(/[\u200B-\u200D\uFEFF]/g, '')
|
||||
.toLocaleLowerCase('de-DE')
|
||||
.trim();
|
||||
|
||||
return loose
|
||||
? name.replace(/[^\p{L}\p{N}]+/gu, '')
|
||||
: name.replace(/\s+/g, ' ');
|
||||
};
|
||||
|
||||
const createIndex = (entries) => {
|
||||
const byName = new Map();
|
||||
const byLooseName = new Map();
|
||||
const byPersonId = new Map();
|
||||
for (const e of (entries || [])) {
|
||||
const key = `${(e.firstname||'').toLowerCase().trim()}|${(e.lastname||'').toLowerCase().trim()}`;
|
||||
if (!byName.has(key)) byName.set(key, e);
|
||||
const key = buildNameKey(e.firstname, e.lastname);
|
||||
const looseKey = buildNameKey(e.firstname, e.lastname, { loose: true });
|
||||
if (key) {
|
||||
if (!byName.has(key)) byName.set(key, e);
|
||||
else byName.set(key, null);
|
||||
}
|
||||
// Ein lockerer Schlüssel ist nur sicher, wenn er eindeutig ist.
|
||||
if (looseKey) {
|
||||
if (!byLooseName.has(looseKey)) byLooseName.set(looseKey, e);
|
||||
else byLooseName.set(looseKey, null);
|
||||
}
|
||||
if (e.personId && !byPersonId.has(String(e.personId))) {
|
||||
byPersonId.set(String(e.personId), e);
|
||||
}
|
||||
}
|
||||
return { byName, byPersonId };
|
||||
return { byName, byLooseName, byPersonId };
|
||||
};
|
||||
|
||||
const currentIdx = createIndex(rankingsCurrent.entries);
|
||||
const quarterIdx = rankingsQuarter.success ? createIndex(rankingsQuarter.entries) : { byName: new Map(), byPersonId: new Map() };
|
||||
const quarterIdx = rankingsQuarter.success
|
||||
? createIndex(rankingsQuarter.entries)
|
||||
: { byName: new Map(), byLooseName: new Map(), byPersonId: new Map() };
|
||||
|
||||
// 4. Für jedes Mitglied TTR und QTTR aktualisieren
|
||||
let updatedTtr = 0;
|
||||
@@ -765,22 +794,30 @@ class MemberService {
|
||||
for (const member of members) {
|
||||
const firstName = member.firstName;
|
||||
const lastName = member.lastName;
|
||||
const key = `${(firstName||'').toLowerCase().trim()}|${(lastName||'').toLowerCase().trim()}`;
|
||||
const key = buildNameKey(firstName, lastName);
|
||||
const looseKey = buildNameKey(firstName, lastName, { loose: true });
|
||||
const personId = member.myTischtennisPlayerId ? String(member.myTischtennisPlayerId).trim() : null;
|
||||
const rankingEntry = personId ? (currentIdx.byPersonId.get(personId) || currentIdx.byName.get(key)) : currentIdx.byName.get(key);
|
||||
const rankingQuarterEntry = personId ? (quarterIdx.byPersonId.get(personId) || quarterIdx.byName.get(key)) : quarterIdx.byName.get(key);
|
||||
const findRankingEntry = (index) =>
|
||||
(personId && index.byPersonId.get(personId))
|
||||
|| index.byName.get(key)
|
||||
|| index.byLooseName.get(looseKey)
|
||||
|| null;
|
||||
const rankingEntry = findRankingEntry(currentIdx);
|
||||
const rankingQuarterEntry = findRankingEntry(quarterIdx);
|
||||
|
||||
if (rankingEntry || rankingQuarterEntry) {
|
||||
try {
|
||||
const oldTtr = member.ttr;
|
||||
const oldQttr = member.qttr;
|
||||
const historyPlayerId = this._extractTtrHistoryPlayerId(rankingEntry) || this._extractTtrHistoryPlayerId(rankingQuarterEntry);
|
||||
if (rankingEntry && typeof rankingEntry.fedRank === 'number') {
|
||||
member.ttr = rankingEntry.fedRank;
|
||||
const ttr = Number(rankingEntry?.fedRank);
|
||||
const qttr = Number(rankingQuarterEntry?.fedRank);
|
||||
if (Number.isFinite(ttr)) {
|
||||
member.ttr = ttr;
|
||||
if (member.ttr !== oldTtr) updatedTtr++;
|
||||
}
|
||||
if (rankingQuarterEntry && typeof rankingQuarterEntry.fedRank === 'number') {
|
||||
member.qttr = rankingQuarterEntry.fedRank;
|
||||
if (Number.isFinite(qttr)) {
|
||||
member.qttr = qttr;
|
||||
if (member.qttr !== oldQttr) updatedQttr++;
|
||||
} else if (!rankingsQuarter.success && (member.qttr == null)) {
|
||||
// Fallback: wenn QTTR-Abruf fehlgeschlagen ist und kein Wert vorhanden war,
|
||||
@@ -793,6 +830,13 @@ class MemberService {
|
||||
if (historyPlayerId && member.myTischtennisHistoryPlayerId !== historyPlayerId) {
|
||||
member.myTischtennisHistoryPlayerId = historyPlayerId;
|
||||
}
|
||||
// Nach einer eindeutigen Namenszuordnung die offizielle ID
|
||||
// übernehmen. Künftige Abrufe sind damit unabhängig von
|
||||
// Schreibweisen im Mitgliederbestand.
|
||||
const resolvedPlayerId = rankingEntry?.personId || rankingQuarterEntry?.personId;
|
||||
if (resolvedPlayerId && member.myTischtennisPlayerId !== String(resolvedPlayerId)) {
|
||||
member.myTischtennisPlayerId = String(resolvedPlayerId);
|
||||
}
|
||||
await member.save();
|
||||
const ratingChanged = member.ttr !== oldTtr || member.qttr !== oldQttr;
|
||||
const oldQttrNumber = Number(oldQttr);
|
||||
|
||||
Reference in New Issue
Block a user