|
|
@@ -0,0 +1,245 @@
|
|
|
+import Parse from '../../../../shared/db/parse-client.js';
|
|
|
+import { batchGetRoomDetails, getContactDetailsBatch } from './qiwe-api.service.js';
|
|
|
+import { getOrgMemberName } from './org-sync.service.js';
|
|
|
+
|
|
|
+export interface GroupMemberDto {
|
|
|
+ id: string;
|
|
|
+ userId: string;
|
|
|
+ nickname: string;
|
|
|
+ status: string;
|
|
|
+ joinedAt: string | null;
|
|
|
+ leftAt: string | null;
|
|
|
+}
|
|
|
+
|
|
|
+const ACTIVE_QIWE_GUID = process.env.QIWE_GUID || '';
|
|
|
+
|
|
|
+function needsNickname(userId: string, nickname: string): boolean {
|
|
|
+ return !nickname || nickname === userId;
|
|
|
+}
|
|
|
+
|
|
|
+function toMemberDto(obj: Parse.Object): GroupMemberDto {
|
|
|
+ const userId = obj.get('userId') || '';
|
|
|
+ const nickname = obj.get('nickname') || '';
|
|
|
+ return {
|
|
|
+ id: obj.id!,
|
|
|
+ userId,
|
|
|
+ nickname: needsNickname(userId, nickname) ? userId : nickname,
|
|
|
+ status: obj.get('status') || 'active',
|
|
|
+ joinedAt: (obj.get('joinedAt') as Date)?.toISOString?.() ?? null,
|
|
|
+ leftAt: (obj.get('leftAt') as Date)?.toISOString?.() ?? null,
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+async function loadSenderNameMap(roomId: string): Promise<Map<string, string>> {
|
|
|
+ const map = new Map<string, string>();
|
|
|
+ const query = new Parse.Query('Message');
|
|
|
+ query.equalTo('roomId', roomId);
|
|
|
+ query.limit(1000);
|
|
|
+ query.select('senderId', 'senderName');
|
|
|
+ const rows = await query.find({ useMasterKey: true });
|
|
|
+ for (const row of rows) {
|
|
|
+ const userId = String(row.get('senderId') || '');
|
|
|
+ const name = String(row.get('senderName') || '');
|
|
|
+ if (!userId || !name || name === userId) continue;
|
|
|
+ map.set(userId, name);
|
|
|
+ }
|
|
|
+ return map;
|
|
|
+}
|
|
|
+
|
|
|
+async function persistMemberNickname(roomId: string, userId: string, nickname: string): Promise<void> {
|
|
|
+ if (!nickname || nickname === userId) return;
|
|
|
+ const query = new Parse.Query('GroupMember');
|
|
|
+ query.equalTo('roomId', roomId);
|
|
|
+ query.equalTo('userId', userId);
|
|
|
+ const existing = await query.first({ useMasterKey: true });
|
|
|
+ if (!existing) return;
|
|
|
+ const current = String(existing.get('nickname') || '');
|
|
|
+ if (current && current !== userId) return;
|
|
|
+ existing.set('nickname', nickname);
|
|
|
+ await existing.save(null, { useMasterKey: true });
|
|
|
+}
|
|
|
+
|
|
|
+async function enrichMemberNicknames(
|
|
|
+ members: GroupMemberDto[],
|
|
|
+ roomId: string,
|
|
|
+): Promise<GroupMemberDto[]> {
|
|
|
+ if (members.length === 0) return [];
|
|
|
+
|
|
|
+ const msgNames = await loadSenderNameMap(roomId);
|
|
|
+ const nameMap = new Map<string, string>();
|
|
|
+
|
|
|
+ for (const m of members) {
|
|
|
+ if (!needsNickname(m.userId, m.nickname)) {
|
|
|
+ nameMap.set(m.userId, m.nickname);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ const fromMsg = msgNames.get(m.userId);
|
|
|
+ if (fromMsg) nameMap.set(m.userId, fromMsg);
|
|
|
+ }
|
|
|
+
|
|
|
+ const needIds = members
|
|
|
+ .map((m) => m.userId)
|
|
|
+ .filter((id) => id && needsNickname(id, nameMap.get(id) || id));
|
|
|
+
|
|
|
+ for (let i = 0; i < needIds.length; i += 50) {
|
|
|
+ const batch = needIds.slice(i, i + 50);
|
|
|
+ const contacts = await getContactDetailsBatch(batch);
|
|
|
+ for (const c of contacts) {
|
|
|
+ if (c.name && c.name !== c.userId) nameMap.set(c.userId, c.name);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const stillNeedOrg = needIds.filter((id) => needsNickname(id, nameMap.get(id) || id));
|
|
|
+ if (stillNeedOrg.length > 0) {
|
|
|
+ const orgQuery = new Parse.Query('OrgMember');
|
|
|
+ orgQuery.containedIn('userId', stillNeedOrg.slice(0, 100));
|
|
|
+ orgQuery.limit(100);
|
|
|
+ const orgRows = await orgQuery.find({ useMasterKey: true });
|
|
|
+ for (const row of orgRows) {
|
|
|
+ const uid = String(row.get('userId') || '');
|
|
|
+ const name = String(row.get('name') || '');
|
|
|
+ if (uid && name && name !== uid) nameMap.set(uid, name);
|
|
|
+ }
|
|
|
+ for (const id of stillNeedOrg.slice(0, 10)) {
|
|
|
+ if (nameMap.has(id) && nameMap.get(id) !== id) continue;
|
|
|
+ const orgName = await getOrgMemberName(id);
|
|
|
+ if (orgName && orgName !== id) nameMap.set(id, orgName);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const enriched = members.map((m) => {
|
|
|
+ const resolved =
|
|
|
+ (!needsNickname(m.userId, m.nickname) ? m.nickname : '')
|
|
|
+ || nameMap.get(m.userId)
|
|
|
+ || m.userId;
|
|
|
+ return { ...m, nickname: resolved };
|
|
|
+ });
|
|
|
+
|
|
|
+ await Promise.all(
|
|
|
+ enriched
|
|
|
+ .filter((m) => !needsNickname(m.userId, m.nickname))
|
|
|
+ .map((m) => persistMemberNickname(roomId, m.userId, m.nickname)),
|
|
|
+ );
|
|
|
+
|
|
|
+ return enriched;
|
|
|
+}
|
|
|
+
|
|
|
+async function upsertMemberFromQiWe(
|
|
|
+ roomId: string,
|
|
|
+ guid: string,
|
|
|
+ userId: string,
|
|
|
+ nickname: string,
|
|
|
+): Promise<void> {
|
|
|
+ const query = new Parse.Query('GroupMember');
|
|
|
+ query.equalTo('roomId', roomId);
|
|
|
+ query.equalTo('userId', userId);
|
|
|
+ query.equalTo('guid', guid);
|
|
|
+ const existing = await query.first({ useMasterKey: true });
|
|
|
+
|
|
|
+ if (existing) {
|
|
|
+ if (nickname && needsNickname(userId, String(existing.get('nickname') || ''))) {
|
|
|
+ existing.set('nickname', nickname);
|
|
|
+ }
|
|
|
+ if (existing.get('status') !== 'active') {
|
|
|
+ existing.set('status', 'active');
|
|
|
+ existing.unset('leftAt');
|
|
|
+ }
|
|
|
+ await existing.save(null, { useMasterKey: true });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const obj = new Parse.Object('GroupMember');
|
|
|
+ obj.set('roomId', roomId);
|
|
|
+ obj.set('userId', userId);
|
|
|
+ obj.set('guid', guid);
|
|
|
+ obj.set('status', 'active');
|
|
|
+ if (nickname && nickname !== userId) obj.set('nickname', nickname);
|
|
|
+ await obj.save(null, { useMasterKey: true });
|
|
|
+}
|
|
|
+
|
|
|
+async function loadMembersFromMessages(roomId: string): Promise<GroupMemberDto[]> {
|
|
|
+ const query = new Parse.Query('Message');
|
|
|
+ query.equalTo('roomId', roomId);
|
|
|
+ query.limit(1000);
|
|
|
+ query.select('senderId', 'senderName');
|
|
|
+ const rows = await query.find({ useMasterKey: true });
|
|
|
+
|
|
|
+ const map = new Map<string, string>();
|
|
|
+ for (const row of rows) {
|
|
|
+ const userId = String(row.get('senderId') || '');
|
|
|
+ if (!userId) continue;
|
|
|
+ const name = String(row.get('senderName') || '');
|
|
|
+ if (!map.has(userId)) map.set(userId, userId);
|
|
|
+ if (name && name !== userId) map.set(userId, name);
|
|
|
+ }
|
|
|
+
|
|
|
+ return [...map.entries()].map(([userId, nickname]) => ({
|
|
|
+ id: `${roomId}:${userId}`,
|
|
|
+ userId,
|
|
|
+ nickname,
|
|
|
+ status: 'active',
|
|
|
+ joinedAt: null,
|
|
|
+ leftAt: null,
|
|
|
+ }));
|
|
|
+}
|
|
|
+
|
|
|
+async function loadMembersFromQiWe(roomId: string, guid: string): Promise<GroupMemberDto[]> {
|
|
|
+ const rooms = await batchGetRoomDetails(guid, [roomId]);
|
|
|
+ const room = rooms[0];
|
|
|
+ if (!room?.members?.length) return [];
|
|
|
+
|
|
|
+ const members: GroupMemberDto[] = [];
|
|
|
+ for (const mem of room.members) {
|
|
|
+ members.push({
|
|
|
+ id: `${roomId}:${mem.userId}`,
|
|
|
+ userId: mem.userId,
|
|
|
+ nickname: mem.nickname || mem.userId,
|
|
|
+ status: 'active',
|
|
|
+ joinedAt: null,
|
|
|
+ leftAt: null,
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ const enriched = await enrichMemberNicknames(members, roomId);
|
|
|
+ for (const m of enriched) {
|
|
|
+ await upsertMemberFromQiWe(roomId, guid, m.userId, m.nickname);
|
|
|
+ }
|
|
|
+ return enriched;
|
|
|
+}
|
|
|
+
|
|
|
+export async function listGroupMembersByRoom(
|
|
|
+ roomId: string,
|
|
|
+ includeLeft = false,
|
|
|
+): Promise<{ members: GroupMemberDto[]; total: number; source: 'db' | 'qiwe' }> {
|
|
|
+ const query = new Parse.Query('GroupMember');
|
|
|
+ query.equalTo('roomId', roomId);
|
|
|
+ if (!includeLeft) query.notEqualTo('status', 'left');
|
|
|
+ query.ascending('joinedAt');
|
|
|
+ query.limit(500);
|
|
|
+ const rows = await query.find({ useMasterKey: true });
|
|
|
+
|
|
|
+ let members: GroupMemberDto[] = [];
|
|
|
+ let source: 'db' | 'qiwe' = 'db';
|
|
|
+
|
|
|
+ if (rows.length > 0) {
|
|
|
+ members = rows.map(toMemberDto);
|
|
|
+ } else if (ACTIVE_QIWE_GUID) {
|
|
|
+ try {
|
|
|
+ const fromQiWe = await loadMembersFromQiWe(roomId, ACTIVE_QIWE_GUID);
|
|
|
+ if (fromQiWe.length > 0) {
|
|
|
+ members = fromQiWe;
|
|
|
+ source = 'qiwe';
|
|
|
+ }
|
|
|
+ } catch (err: unknown) {
|
|
|
+ const message = err instanceof Error ? err.message : String(err);
|
|
|
+ console.warn(`[GroupMember] 拉取群成员失败 roomId=${roomId}: ${message}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (members.length === 0) {
|
|
|
+ members = await loadMembersFromMessages(roomId);
|
|
|
+ }
|
|
|
+
|
|
|
+ members = await enrichMemberNicknames(members, roomId);
|
|
|
+ return { members, total: members.length, source };
|
|
|
+}
|