import Parse from '../../../../shared/db/parse-client.js'; import { getInternalContactList, getContactDetailsBatch, } from './qiwe-api.service.js'; import { listStores } from './organization.service.js'; export interface OrgDepartmentDto { id: string; partyId: string; name: string; parentPartyId: string; regionCode: string; storeId: string; orderIndex: number; } export interface OrgMemberDto { id: string; userId: string; name: string; partyIds: string[]; mobile: string; position: string; } export interface OrgTreeNode { partyId: string; name: string; regionCode: string; storeId: string; children: OrgTreeNode[]; members: OrgMemberDto[]; } const SEED_DEPARTMENTS = [ { partyId: 'root', name: '拉迷总部', parentPartyId: '', regionCode: '', storeId: '', orderIndex: 0 }, { partyId: 'region_east', name: '华东区域', parentPartyId: 'root', regionCode: '华东', storeId: '', orderIndex: 1 }, { partyId: 'region_north', name: '华北区域', parentPartyId: 'root', regionCode: '华北', storeId: '', orderIndex: 2 }, { partyId: 'region_south', name: '华南区域', parentPartyId: 'root', regionCode: '华南', storeId: '', orderIndex: 3 }, ]; function deptToDto(obj: Parse.Object): OrgDepartmentDto { return { id: obj.id!, partyId: obj.get('partyId') || '', name: obj.get('name') || '', parentPartyId: obj.get('parentPartyId') || '', regionCode: obj.get('regionCode') || '', storeId: obj.get('storeId') || '', orderIndex: obj.get('orderIndex') ?? 0, }; } function memberToDto(obj: Parse.Object): OrgMemberDto { return { id: obj.id!, userId: obj.get('userId') || '', name: obj.get('name') || '', partyIds: (obj.get('partyIds') as string[]) || [], mobile: obj.get('mobile') || '', position: obj.get('position') || '', }; } export async function listOrgDepartments(): Promise { const query = new Parse.Query('OrgDepartment'); query.ascending('orderIndex'); query.limit(500); const results = await query.find({ useMasterKey: true }); return results.map(deptToDto); } export async function listOrgMembers(): Promise { const query = new Parse.Query('OrgMember'); query.limit(1000); const results = await query.find({ useMasterKey: true }); return results.map(memberToDto); } export async function getOrgMemberName(userId: string): Promise { if (!userId) return ''; const query = new Parse.Query('OrgMember'); query.equalTo('userId', userId); const member = await query.first({ useMasterKey: true }); return member?.get('name') || ''; } export async function buildOrgTree(): Promise { const [departments, members] = await Promise.all([ listOrgDepartments(), listOrgMembers(), ]); const nodeMap = new Map(); for (const dept of departments) { nodeMap.set(dept.partyId, { partyId: dept.partyId, name: dept.name, regionCode: dept.regionCode, storeId: dept.storeId, children: [], members: [], }); } for (const member of members) { const primaryParty = member.partyIds[0]; const node = primaryParty ? nodeMap.get(primaryParty) : undefined; if (node) node.members.push(member); } const roots: OrgTreeNode[] = []; for (const dept of departments) { const node = nodeMap.get(dept.partyId)!; if (!dept.parentPartyId) { roots.push(node); continue; } const parent = nodeMap.get(dept.parentPartyId); if (parent) parent.children.push(node); else roots.push(node); } return roots; } async function upsertDepartment(data: Omit): Promise { const query = new Parse.Query('OrgDepartment'); query.equalTo('partyId', data.partyId); const existing = await query.first({ useMasterKey: true }); const obj = existing ?? new Parse.Object('OrgDepartment'); obj.set('partyId', data.partyId); obj.set('name', data.name); obj.set('parentPartyId', data.parentPartyId); obj.set('regionCode', data.regionCode); obj.set('storeId', data.storeId); obj.set('orderIndex', data.orderIndex); obj.set('syncedAt', new Date()); await obj.save(null, { useMasterKey: true }); } async function upsertMember(data: Omit): Promise { const query = new Parse.Query('OrgMember'); query.equalTo('userId', data.userId); const existing = await query.first({ useMasterKey: true }); const obj = existing ?? new Parse.Object('OrgMember'); obj.set('userId', data.userId); obj.set('name', data.name); obj.set('partyIds', data.partyIds); obj.set('mobile', data.mobile); obj.set('position', data.position); obj.set('syncedAt', new Date()); await obj.save(null, { useMasterKey: true }); } export async function bootstrapOrgTree(): Promise { const stores = await listStores(); let order = 10; for (const seed of SEED_DEPARTMENTS) { await upsertDepartment({ ...seed, orderIndex: seed.orderIndex }); } for (const store of stores) { const partyId = `store_${store.code.toLowerCase()}`; const parentPartyId = SEED_DEPARTMENTS.find((d) => d.regionCode === store.region)?.partyId || 'root'; await upsertDepartment({ partyId, name: store.name, parentPartyId, regionCode: store.region, storeId: store.id, orderIndex: order++, }); } console.log('[Org] 组织树种子已同步'); } export async function syncOrgFromQiWe(): Promise<{ departments: number; members: number; source: 'qiwe' | 'seed'; errors: string[]; }> { const errors: string[] = []; try { const contacts = await getInternalContactList(); if (contacts.length === 0) { return { departments: 0, members: 0, source: 'seed', errors: ['企微未返回内部联系人,保留本地组织树'] }; } const userIds = contacts.map((c) => c.userId).filter(Boolean); const details = await getContactDetailsBatch(userIds); const partyIds = new Set(); for (const contact of contacts) { if (contact.partyId) partyIds.add(contact.partyId); } let deptCount = 0; for (const partyId of partyIds) { await upsertDepartment({ partyId, name: `部门 ${partyId.slice(-6)}`, parentPartyId: 'root', regionCode: '', storeId: '', orderIndex: deptCount, }); deptCount++; } let memberCount = 0; for (const contact of contacts) { const detail = details.find((d) => d.userId === contact.userId); await upsertMember({ userId: contact.userId, name: detail?.name || contact.userId, partyIds: contact.partyId ? [contact.partyId] : [], mobile: detail?.mobile || '', position: detail?.position || '', }); memberCount++; } return { departments: deptCount, members: memberCount, source: 'qiwe', errors }; } catch (err: unknown) { const msg = err instanceof Error ? err.message : String(err); errors.push(msg); return { departments: 0, members: 0, source: 'seed', errors }; } }