| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744 |
- import Parse from '../../../../shared/db/parse-client.js';
- import { deriveHealthMetrics } from '../utils/group-metrics.util.js';
- import { type LifecyclePhase } from '../utils/lifecycle.util.js';
- import { getCommunityLifecycleMap, listCommunities } from './community.service.js';
- import { getDefaultStore, resolveOwnerName } from './organization.service.js';
- import { listGroupMembersByRoom, type GroupMemberDto } from './group-members.service.js';
- import { batchGetRoomDetails, getContactDetailsBatch } from './qiwe-api.service.js';
- export interface GroupChatDto {
- id: string;
- roomId: string;
- roomName: string;
- ownerId: string;
- ownerName: string;
- memberCount: number;
- avatarUrl: string;
- status: string;
- guid: string;
- storeId: string;
- storeName: string;
- communityId: string;
- communityName: string;
- activityLevel: string;
- healthScore: number;
- healthStatus: string;
- healthGrade: string;
- opsActivityScore: number;
- customerActivityScore: number;
- lifecyclePhase: string;
- hasDocument: boolean;
- documentPinned: boolean;
- documentInNotice: boolean;
- messageCountToday: number;
- messageCountTotal: number;
- memberChange24h: number;
- createdAt: string;
- ownerRole: string;
- updatedAt: string;
- }
- export interface GroupListFilters {
- storeId?: string;
- activityLevel?: string;
- healthStatus?: string;
- healthGrade?: string;
- lifecyclePhase?: string;
- hasDocument?: boolean;
- q?: string;
- }
- function startOfToday(): Date {
- const shanghaiDate = new Date().toLocaleDateString('en-CA', { timeZone: 'Asia/Shanghai' });
- return new Date(`${shanghaiDate}T00:00:00+08:00`);
- }
- async function countMessagesToday(roomId: string): Promise<number> {
- const query = new Parse.Query('Message');
- query.equalTo('roomId', roomId);
- query.containedIn('msgType', [0, 2, 6, 13, 14, 15, 16, 20, 22, 23, 29, 41, 78, 123, 141, 146, 213]);
- query.greaterThanOrEqualTo('timestamp', startOfToday());
- return query.count({ useMasterKey: true });
- }
- async function countMessagesTotal(roomId: string): Promise<number> {
- const query = new Parse.Query('Message');
- query.equalTo('roomId', roomId);
- query.containedIn('msgType', [0, 2, 6, 13, 14, 15, 16, 20, 22, 23, 29, 41, 78, 123, 141, 146, 213]);
- return query.count({ useMasterKey: true });
- }
- /** 消息入库时发现群不存在则创建占位群,便于列表与详情按 roomId 关联 */
- export async function ensureGroupChatForRoom(roomId: string, guid: string): Promise<void> {
- if (!roomId || !guid) return;
- const query = new Parse.Query('GroupChat');
- query.equalTo('roomId', roomId);
- query.equalTo('guid', guid);
- const existing = await query.first({ useMasterKey: true });
- if (existing) return;
- const obj = new Parse.Object('GroupChat');
- obj.set('roomId', roomId);
- obj.set('guid', guid);
- obj.set('roomName', `群 ${roomId.slice(-6)}`);
- obj.set('status', 'active');
- obj.set('memberCount', 0);
- initNewGroupDefaults(obj);
- await obj.save(null, { useMasterKey: true });
- console.log(`[GroupChat] 由消息自动创建群 roomId=${roomId}`);
- }
- /** 根据已同步的群消息补全 GroupChat(roomId 与 Message 对齐) */
- export async function backfillGroupChatsFromMessages(guid: string): Promise<number> {
- const query = new Parse.Query('Message');
- query.equalTo('guid', guid);
- query.equalTo('isGroupChat', 1);
- query.exists('roomId');
- query.limit(5000);
- query.select('roomId');
- const rows = await query.find({ useMasterKey: true });
- const roomIds = new Set<string>();
- for (const row of rows) {
- const rid = row.get('roomId');
- const key = rid ? String(rid) : '';
- if (key.length >= 14 && /^\d+$/.test(key)) roomIds.add(key);
- }
- let created = 0;
- for (const roomId of roomIds) {
- const before = new Parse.Query('GroupChat');
- before.equalTo('roomId', roomId);
- before.equalTo('guid', guid);
- const had = await before.first({ useMasterKey: true });
- if (had) continue;
- await ensureGroupChatForRoom(roomId, guid);
- created++;
- }
- if (created > 0) {
- console.log(`[GroupChat] 从消息补全 ${created} 个群记录`);
- }
- return created;
- }
- const METRICS_FRESH_MS = 10 * 60 * 1000;
- /** 详情页读取:优先用库内缓存指标,避免每次打开都扫 Message 表 */
- async function enrichGroupForRead(
- obj: Parse.Object,
- lifecycleMap: Map<string, LifecyclePhase>,
- ): Promise<GroupChatDto> {
- const roomId = obj.get('roomId') as string;
- const memberCount = obj.get('memberCount') ?? 0;
- const status = obj.get('status') || 'active';
- const hasDocument = obj.get('hasDocument') === true;
- const documentPinned = obj.get('documentPinned') === true;
- const documentInNotice = obj.get('documentInNotice') === true;
- const memberChange24h = obj.get('memberChange24h') ?? 0;
- const updatedAt = obj.get('updatedAt') as Date | undefined;
- const cachedTotal = obj.get('messageCountTotal') ?? 0;
- const freshMetrics =
- cachedTotal > 0
- && updatedAt
- && Date.now() - updatedAt.getTime() < METRICS_FRESH_MS;
- let messageCountToday = obj.get('messageCountToday') ?? 0;
- let messageCountTotal = cachedTotal;
- if (!freshMetrics) {
- [messageCountToday, messageCountTotal] = await Promise.all([
- countMessagesToday(roomId),
- messageCountTotal > 0 ? Promise.resolve(messageCountTotal) : countMessagesTotal(roomId),
- ]);
- }
- const metrics = deriveHealthMetrics({
- memberCount,
- messageCountToday,
- memberChange24h,
- hasDocument,
- documentPinned,
- documentInNotice,
- status,
- });
- const communityId = obj.get('communityId') as string;
- const lifecyclePhase = communityId && lifecycleMap.has(communityId)
- ? lifecycleMap.get(communityId)!
- : (obj.get('lifecyclePhase') as string) || 'pre_handover';
- obj.set('messageCountToday', messageCountToday);
- obj.set('messageCountTotal', messageCountTotal);
- obj.set('activityLevel', metrics.activityLevel);
- obj.set('opsActivityScore', metrics.opsActivityScore);
- obj.set('customerActivityScore', metrics.customerActivityScore);
- obj.set('healthGrade', metrics.healthGrade);
- obj.set('healthScore', metrics.healthScore);
- obj.set('healthStatus', metrics.healthStatus);
- obj.set('lifecyclePhase', lifecyclePhase);
- if (!freshMetrics) {
- void obj.save(null, { useMasterKey: true }).catch(() => {});
- }
- return toDto(obj);
- }
- async function enrichGroup(
- obj: Parse.Object,
- lifecycleMap: Map<string, LifecyclePhase>,
- persist = true,
- ): Promise<GroupChatDto> {
- const roomId = obj.get('roomId') as string;
- const guid = (obj.get('guid') as string) || '';
- const memberCount = obj.get('memberCount') ?? 0;
- const status = obj.get('status') || 'active';
- const hasDocument = obj.get('hasDocument') === true;
- const documentPinned = obj.get('documentPinned') === true;
- const documentInNotice = obj.get('documentInNotice') === true;
- const memberChange24h = obj.get('memberChange24h') ?? 0;
- const [messageCountToday, messageCountTotal] = await Promise.all([
- countMessagesToday(roomId),
- countMessagesTotal(roomId),
- ]);
- const metrics = deriveHealthMetrics({
- memberCount,
- messageCountToday,
- memberChange24h,
- hasDocument,
- documentPinned,
- documentInNotice,
- status,
- });
- const communityId = obj.get('communityId') as string;
- const lifecyclePhase = communityId && lifecycleMap.has(communityId)
- ? lifecycleMap.get(communityId)!
- : (obj.get('lifecyclePhase') as string) || 'pre_handover';
- let ownerName = (obj.get('ownerName') as string) || '';
- if (!ownerName && obj.get('ownerId')) {
- ownerName = await resolveOwnerName(obj.get('ownerId') || '');
- if (ownerName) {
- obj.set('ownerName', ownerName);
- }
- }
- obj.set('messageCountToday', messageCountToday);
- obj.set('messageCountTotal', messageCountTotal);
- obj.set('activityLevel', metrics.activityLevel);
- obj.set('opsActivityScore', metrics.opsActivityScore);
- obj.set('customerActivityScore', metrics.customerActivityScore);
- obj.set('healthGrade', metrics.healthGrade);
- obj.set('healthScore', metrics.healthScore);
- obj.set('healthStatus', metrics.healthStatus);
- obj.set('lifecyclePhase', lifecyclePhase);
- if (persist) {
- await obj.save(null, { useMasterKey: true });
- }
- return toDto(obj);
- }
- function toListDto(obj: Parse.Object, lifecycleMap: Map<string, LifecyclePhase>): GroupChatDto {
- const dto = toDto(obj);
- const communityId = obj.get('communityId') as string;
- if (communityId && lifecycleMap.has(communityId)) {
- dto.lifecyclePhase = lifecycleMap.get(communityId)!;
- }
- return dto;
- }
- function toDto(obj: Parse.Object): GroupChatDto {
- return {
- id: obj.id!,
- roomId: obj.get('roomId') || '',
- roomName: obj.get('roomName') || '未命名群',
- ownerId: obj.get('ownerId') || '',
- ownerName: obj.get('ownerName') || '',
- memberCount: obj.get('memberCount') ?? 0,
- avatarUrl: obj.get('avatarUrl') || '',
- status: obj.get('status') || 'active',
- guid: obj.get('guid') || '',
- storeId: obj.get('storeId') || '',
- storeName: obj.get('storeName') || '',
- communityId: obj.get('communityId') || '',
- communityName: obj.get('communityName') || '',
- activityLevel: obj.get('activityLevel') || 'inactive',
- healthScore: obj.get('healthScore') ?? 0,
- healthStatus: obj.get('healthStatus') || 'warning',
- healthGrade: obj.get('healthGrade') || 'C',
- opsActivityScore: obj.get('opsActivityScore') ?? 0,
- customerActivityScore: obj.get('customerActivityScore') ?? 0,
- lifecyclePhase: obj.get('lifecyclePhase') || 'pre_handover',
- hasDocument: obj.get('hasDocument') === true,
- documentPinned: obj.get('documentPinned') === true,
- documentInNotice: obj.get('documentInNotice') === true,
- messageCountToday: obj.get('messageCountToday') ?? 0,
- messageCountTotal: obj.get('messageCountTotal') ?? 0,
- memberChange24h: obj.get('memberChange24h') ?? 0,
- createdAt: (obj.get('createdAt') as Date)?.toISOString?.() || new Date().toISOString(),
- ownerRole: (obj.get('ownerRole') as string) || '',
- updatedAt: (obj.get('updatedAt') as Date)?.toISOString?.() || new Date().toISOString(),
- };
- }
- async function countActiveMembers(roomId: string): Promise<number> {
- const query = new Parse.Query('GroupMember');
- query.equalTo('roomId', roomId);
- query.notEqualTo('status', 'left');
- return query.count({ useMasterKey: true });
- }
- async function resolveOwnerProfile(ownerId: string): Promise<{ name: string; role: string }> {
- if (!ownerId) return { name: '', role: '' };
- const orgQuery = new Parse.Query('OrgMember');
- orgQuery.equalTo('userId', ownerId);
- const orgMember = await orgQuery.first({ useMasterKey: true });
- if (orgMember) {
- return {
- name: String(orgMember.get('name') || ownerId),
- role: String(orgMember.get('position') || '群主'),
- };
- }
- if (ACTIVE_QIWE_GUID) {
- try {
- const contacts = await getContactDetailsBatch([ownerId]);
- const contact = contacts[0];
- if (contact) {
- return {
- name: contact.name || ownerId,
- role: contact.position || '群主',
- };
- }
- } catch {
- /* 联系人接口失败时回退 userId */
- }
- }
- return { name: ownerId, role: '群主' };
- }
- async function ensureOrganizationFields(obj: Parse.Object): Promise<void> {
- if (!obj.get('storeId')) {
- const def = await getDefaultStore();
- if (def.storeId) {
- obj.set('storeId', def.storeId);
- obj.set('storeName', def.storeName);
- }
- }
- if (!obj.get('communityId')) {
- const roomName = String(obj.get('roomName') || '');
- const communities = await listCommunities();
- const match = communities
- .filter((c) => c.name && roomName.includes(c.name))
- .sort((a, b) => b.name.length - a.name.length)[0];
- if (match) {
- obj.set('communityId', match.id);
- obj.set('communityName', match.name);
- if (!obj.get('storeId') && match.storeId) {
- obj.set('storeId', match.storeId);
- obj.set('storeName', match.storeName);
- }
- }
- }
- }
- async function borrowOwnerFromSiblingRows(obj: Parse.Object, roomId: string): Promise<void> {
- if (obj.get('ownerId')) return;
- const query = new Parse.Query('GroupChat');
- query.equalTo('roomId', roomId);
- query.exists('ownerId');
- query.limit(20);
- const rows = await query.find({ useMasterKey: true });
- for (const row of rows) {
- const ownerId = String(row.get('ownerId') || '');
- if (!ownerId) continue;
- obj.set('ownerId', ownerId);
- if (row.get('ownerName')) obj.set('ownerName', row.get('ownerName'));
- return;
- }
- }
- async function inferOwnerFromMembers(
- roomId: string,
- members: GroupMemberDto[],
- ): Promise<{ ownerId: string; name: string; role: string } | null> {
- if (members.length === 0) return null;
- const userIds = members.map((m) => m.userId).filter(Boolean);
- const orgQuery = new Parse.Query('OrgMember');
- orgQuery.containedIn('userId', userIds.slice(0, 100));
- orgQuery.limit(100);
- const orgRows = await orgQuery.find({ useMasterKey: true });
- const orgMap = new Map(orgRows.map((row) => [String(row.get('userId')), row]));
- for (const member of members) {
- const org = orgMap.get(member.userId);
- if (org) {
- return {
- ownerId: member.userId,
- name: String(org.get('name') || member.nickname || member.userId),
- role: String(org.get('position') || '负责人'),
- };
- }
- }
- const msgQuery = new Parse.Query('Message');
- msgQuery.equalTo('roomId', roomId);
- msgQuery.limit(200);
- msgQuery.select('senderId', 'senderName');
- const messages = await msgQuery.find({ useMasterKey: true });
- const counts = new Map<string, { count: number; name: string }>();
- for (const message of messages) {
- const userId = String(message.get('senderId') || '');
- if (!userId) continue;
- const senderName = String(message.get('senderName') || userId);
- const current = counts.get(userId) || { count: 0, name: senderName };
- current.count += 1;
- if (senderName !== userId) current.name = senderName;
- counts.set(userId, current);
- }
- let best: { ownerId: string; name: string; count: number } | null = null;
- for (const [ownerId, value] of counts) {
- if (!best || value.count > best.count) {
- best = { ownerId, name: value.name, count: value.count };
- }
- }
- if (best) {
- return { ownerId: best.ownerId, name: best.name, role: '活跃成员' };
- }
- const first = members.find((m) => m.nickname && m.nickname !== m.userId);
- if (first) {
- return { ownerId: first.userId, name: first.nickname, role: '群成员' };
- }
- return null;
- }
- async function syncQiWeRoomFields(obj: Parse.Object, roomId: string): Promise<void> {
- const guid = String(obj.get('guid') || ACTIVE_QIWE_GUID);
- if (!guid) return;
- const needsSync =
- !obj.get('ownerId')
- || (obj.get('memberCount') ?? 0) === 0
- || isPlaceholderRoomName(String(obj.get('roomName') || ''));
- if (!needsSync) return;
- try {
- const details = await batchGetRoomDetails(guid, [roomId]);
- const detail = details[0];
- if (!detail) return;
- if (detail.roomName) obj.set('roomName', detail.roomName);
- if (detail.ownerId) obj.set('ownerId', detail.ownerId);
- if (detail.memberCount > 0) obj.set('memberCount', detail.memberCount);
- } catch (err: unknown) {
- const message = err instanceof Error ? err.message : String(err);
- console.warn(`[GroupChat] 详情补全企微字段失败 roomId=${roomId}: ${message}`);
- }
- }
- function pickBestGroupObject(rows: Parse.Object[], lifecycleMap: Map<string, LifecyclePhase>): Parse.Object {
- const dtos = rows.map((obj) => toListDto(obj, lifecycleMap));
- const [bestDto] = dedupeGroupsByRoomId(dtos);
- return rows.find((row) => row.id === bestDto?.id) || rows[0];
- }
- const ACTIVE_QIWE_GUID = process.env.QIWE_GUID || '';
- function scoreGroupForDedupe(g: GroupChatDto): number {
- let score = 0;
- if (g.guid === ACTIVE_QIWE_GUID) score += 100;
- if (g.messageCountTotal > 0) score += 50 + Math.min(g.messageCountTotal, 1000);
- if (g.roomName && !g.roomName.startsWith('群 ')) score += 20;
- if (g.memberCount > 0) score += 10;
- return score;
- }
- /** 同一 roomId 可能有多条记录(不同 guid / 来源),保留信息最完整的一条 */
- function dedupeGroupsByRoomId(groups: GroupChatDto[]): GroupChatDto[] {
- const map = new Map<string, GroupChatDto>();
- for (const g of groups) {
- if (!g.roomId) continue;
- const existing = map.get(g.roomId);
- if (!existing || scoreGroupForDedupe(g) > scoreGroupForDedupe(existing)) {
- map.set(g.roomId, g);
- }
- }
- return [...map.values()];
- }
- function isPlaceholderRoomName(name: string): boolean {
- return !name || name === '未命名群' || /^群 \d{6}$/.test(name);
- }
- /** 向企微拉取真实群名,替换「群 500656」类占位名 */
- export async function enrichPlaceholderGroupNames(guid: string, batchSize = 30): Promise<number> {
- if (!guid) return 0;
- const query = new Parse.Query('GroupChat');
- query.equalTo('guid', guid);
- query.limit(200);
- const groups = await query.find({ useMasterKey: true });
- const targets = groups.filter((g) => isPlaceholderRoomName(String(g.get('roomName') || '')));
- if (targets.length === 0) return 0;
- const roomIds = targets.slice(0, batchSize).map((g) => String(g.get('roomId')));
- let details: Awaited<ReturnType<typeof batchGetRoomDetails>>;
- try {
- details = await batchGetRoomDetails(guid, roomIds);
- } catch (err: unknown) {
- const message = err instanceof Error ? err.message : String(err);
- console.warn(`[GroupChat] 拉取群详情失败: ${message}`);
- return 0;
- }
- const detailMap = new Map(details.map((d) => [d.roomId, d]));
- let updated = 0;
- for (const obj of targets) {
- const roomId = String(obj.get('roomId'));
- const detail = detailMap.get(roomId);
- if (!detail?.roomName) continue;
- obj.set('roomName', detail.roomName);
- if (detail.memberCount > 0) obj.set('memberCount', detail.memberCount);
- if (detail.ownerId) obj.set('ownerId', detail.ownerId);
- await obj.save(null, { useMasterKey: true });
- updated++;
- }
- if (updated > 0) {
- console.log(`[GroupChat] 已补全 ${updated} 个群的真实名称`);
- }
- return updated;
- }
- let lastPrepareAt = 0;
- const PREPARE_TTL_MS = 10 * 60 * 1000;
- /**
- * 列表展示前对齐群目录:从 Message 表补全有消息但未入列表的群。
- * 带 TTL 缓存,避免每次打开列表都扫库/调企微。
- */
- export async function prepareGroupListForDisplay(force = false): Promise<number> {
- if (!ACTIVE_QIWE_GUID) return 0;
- if (!force && Date.now() - lastPrepareAt < PREPARE_TTL_MS) return 0;
- lastPrepareAt = Date.now();
- const created = await backfillGroupChatsFromMessages(ACTIVE_QIWE_GUID);
- await enrichPlaceholderGroupNames(ACTIVE_QIWE_GUID);
- return created;
- }
- /** 同步后批量刷新群指标(写库),不在列表读取路径调用 */
- export async function refreshAllGroupMetrics(limit = 200): Promise<number> {
- const query = new Parse.Query('GroupChat');
- query.limit(limit);
- const groups = await query.find({ useMasterKey: true });
- const lifecycleMap = await getCommunityLifecycleMap();
- await Promise.all(groups.map((obj) => enrichGroup(obj, lifecycleMap)));
- return groups.length;
- }
- export async function listGroupChats(
- filters: GroupListFilters = {},
- limit = 500,
- ): Promise<GroupChatDto[]> {
- const query = new Parse.Query('GroupChat');
- query.descending('messageCountTotal');
- query.addDescending('updatedAt');
- if (filters.storeId) query.equalTo('storeId', filters.storeId);
- if (filters.activityLevel) query.equalTo('activityLevel', filters.activityLevel);
- if (filters.healthStatus) query.equalTo('healthStatus', filters.healthStatus);
- if (filters.healthGrade) query.equalTo('healthGrade', filters.healthGrade);
- if (filters.lifecyclePhase) query.equalTo('lifecyclePhase', filters.lifecyclePhase);
- if (filters.hasDocument !== undefined) query.equalTo('hasDocument', filters.hasDocument);
- query.limit(limit);
- const [results, lifecycleMap] = await Promise.all([
- query.find({ useMasterKey: true }),
- getCommunityLifecycleMap(),
- ]);
- const enriched = results.map((obj) => toListDto(obj, lifecycleMap));
- let filtered = enriched;
- if (filters.lifecyclePhase) {
- filtered = filtered.filter((g) => g.lifecyclePhase === filters.lifecyclePhase);
- }
- if (filters.healthGrade) {
- filtered = filtered.filter((g) => g.healthGrade === filters.healthGrade);
- }
- if (!filters.q) {
- return dedupeGroupsByRoomId(filtered).sort((a, b) => b.messageCountTotal - a.messageCountTotal);
- }
- const q = filters.q.toLowerCase();
- return dedupeGroupsByRoomId(filtered.filter((g) =>
- g.roomName.toLowerCase().includes(q)
- || g.ownerName.toLowerCase().includes(q)
- || g.communityName.toLowerCase().includes(q)
- || g.storeName.toLowerCase().includes(q)
- || g.roomId.includes(q),
- ));
- }
- export async function getGroupChatByRoomId(roomId: string): Promise<GroupChatDto | null> {
- const query = new Parse.Query('GroupChat');
- query.equalTo('roomId', roomId);
- query.limit(20);
- const [rows, lifecycleMap] = await Promise.all([
- query.find({ useMasterKey: true }),
- getCommunityLifecycleMap(),
- ]);
- if (rows.length === 0) return null;
- const obj = pickBestGroupObject(rows, lifecycleMap);
- const needsQiWe =
- !obj.get('ownerId')
- || (obj.get('memberCount') ?? 0) === 0
- || isPlaceholderRoomName(String(obj.get('roomName') || ''));
- let membersLoaded: GroupMemberDto[] | null = null;
- await Promise.all([
- needsQiWe ? syncQiWeRoomFields(obj, roomId) : Promise.resolve(),
- !obj.get('ownerId') ? borrowOwnerFromSiblingRows(obj, roomId) : Promise.resolve(),
- !obj.get('storeId') ? ensureOrganizationFields(obj) : Promise.resolve(),
- ]);
- const needsMembers = (obj.get('memberCount') ?? 0) === 0 || !obj.get('ownerId');
- if (needsMembers) {
- if ((obj.get('memberCount') ?? 0) === 0) {
- const memberCount = await countActiveMembers(roomId);
- if (memberCount > 0) obj.set('memberCount', memberCount);
- }
- if ((obj.get('memberCount') ?? 0) === 0 || !obj.get('ownerId')) {
- const membersResult = await listGroupMembersByRoom(roomId, false, false);
- membersLoaded = membersResult.members;
- if (membersResult.total > 0 && (obj.get('memberCount') ?? 0) === 0) {
- obj.set('memberCount', membersResult.total);
- }
- }
- }
- if (!obj.get('ownerId')) {
- if (!membersLoaded) {
- const membersResult = await listGroupMembersByRoom(roomId, false, false);
- membersLoaded = membersResult.members;
- if (membersResult.total > 0 && (obj.get('memberCount') ?? 0) === 0) {
- obj.set('memberCount', membersResult.total);
- }
- }
- const inferred = membersLoaded ? await inferOwnerFromMembers(roomId, membersLoaded) : null;
- if (inferred) {
- obj.set('ownerId', inferred.ownerId);
- obj.set('ownerName', inferred.name);
- obj.set('ownerRole', inferred.role);
- }
- }
- const ownerId = String(obj.get('ownerId') || '');
- let owner = obj.get('ownerName')
- ? { name: String(obj.get('ownerName')), role: String(obj.get('ownerRole') || '群主') }
- : await resolveOwnerProfile(ownerId);
- if (ownerId && (!owner.name || owner.name === ownerId)) {
- if (!membersLoaded) {
- const membersResult = await listGroupMembersByRoom(roomId, false, false);
- membersLoaded = membersResult.members;
- }
- const member = membersLoaded?.find((m) => m.userId === ownerId);
- if (member?.nickname && member.nickname !== ownerId) {
- owner = { ...owner, name: member.nickname };
- }
- }
- if (owner.name) obj.set('ownerName', owner.name);
- if (owner.role) obj.set('ownerRole', owner.role);
- const dto = await enrichGroupForRead(obj, lifecycleMap);
- return {
- ...dto,
- createdAt: (obj.get('createdAt') as Date)?.toISOString?.() || dto.updatedAt,
- ownerRole: owner.role || dto.ownerRole || '群主',
- };
- }
- export async function migrateGroupBusinessFields(): Promise<void> {
- const query = new Parse.Query('GroupChat');
- query.limit(1000);
- const groups = await query.find({ useMasterKey: true });
- let migrated = 0;
- for (const group of groups) {
- let changed = false;
- if (group.get('hasDocument') === undefined) {
- group.set('hasDocument', false);
- changed = true;
- }
- if (group.get('documentPinned') === undefined) {
- group.set('documentPinned', false);
- changed = true;
- }
- if (group.get('documentInNotice') === undefined) {
- group.set('documentInNotice', false);
- changed = true;
- }
- if (group.get('memberChange24h') === undefined) {
- group.set('memberChange24h', 0);
- changed = true;
- }
- if (changed) {
- await group.save(null, { useMasterKey: true });
- migrated++;
- }
- }
- if (migrated > 0) {
- console.log(`[Organization] 已补全 ${migrated} 个群的基础字段`);
- }
- }
- /** 清除历史上自动填入的占位门店/小区数据(无真实业务归属时应为空) */
- export async function clearPlaceholderOrganizationFields(): Promise<void> {
- const query = new Parse.Query('GroupChat');
- query.limit(1000);
- const groups = await query.find({ useMasterKey: true });
- let cleared = 0;
- for (const group of groups) {
- const hasStore = group.get('storeId') || group.get('storeName');
- const hasCommunity = group.get('communityId') || group.get('communityName');
- if (!hasStore && !hasCommunity) continue;
- group.unset('storeId');
- group.unset('storeName');
- group.unset('communityId');
- group.unset('communityName');
- await group.save(null, { useMasterKey: true });
- cleared++;
- }
- if (cleared > 0) {
- console.log(`[Organization] 已清除 ${cleared} 个群的占位门店/小区数据`);
- }
- }
- export function initNewGroupDefaults(obj: Parse.Object): void {
- if (obj.get('hasDocument') === undefined) obj.set('hasDocument', false);
- if (obj.get('documentPinned') === undefined) obj.set('documentPinned', false);
- if (obj.get('documentInNotice') === undefined) obj.set('documentInNotice', false);
- if (obj.get('memberChange24h') === undefined) obj.set('memberChange24h', 0);
- }
|