org-sync.service.ts 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. import Parse from '../../../../shared/db/parse-client.js';
  2. import {
  3. getInternalContactList,
  4. getContactDetailsBatch,
  5. } from './qiwe-api.service.js';
  6. import { listStores } from './organization.service.js';
  7. export interface OrgDepartmentDto {
  8. id: string;
  9. partyId: string;
  10. name: string;
  11. parentPartyId: string;
  12. regionCode: string;
  13. storeId: string;
  14. orderIndex: number;
  15. }
  16. export interface OrgMemberDto {
  17. id: string;
  18. userId: string;
  19. name: string;
  20. partyIds: string[];
  21. mobile: string;
  22. position: string;
  23. }
  24. export interface OrgTreeNode {
  25. partyId: string;
  26. name: string;
  27. regionCode: string;
  28. storeId: string;
  29. children: OrgTreeNode[];
  30. members: OrgMemberDto[];
  31. }
  32. const SEED_DEPARTMENTS = [
  33. { partyId: 'root', name: '拉迷总部', parentPartyId: '', regionCode: '', storeId: '', orderIndex: 0 },
  34. { partyId: 'region_east', name: '华东区域', parentPartyId: 'root', regionCode: '华东', storeId: '', orderIndex: 1 },
  35. { partyId: 'region_north', name: '华北区域', parentPartyId: 'root', regionCode: '华北', storeId: '', orderIndex: 2 },
  36. { partyId: 'region_south', name: '华南区域', parentPartyId: 'root', regionCode: '华南', storeId: '', orderIndex: 3 },
  37. ];
  38. function deptToDto(obj: Parse.Object): OrgDepartmentDto {
  39. return {
  40. id: obj.id!,
  41. partyId: obj.get('partyId') || '',
  42. name: obj.get('name') || '',
  43. parentPartyId: obj.get('parentPartyId') || '',
  44. regionCode: obj.get('regionCode') || '',
  45. storeId: obj.get('storeId') || '',
  46. orderIndex: obj.get('orderIndex') ?? 0,
  47. };
  48. }
  49. function memberToDto(obj: Parse.Object): OrgMemberDto {
  50. return {
  51. id: obj.id!,
  52. userId: obj.get('userId') || '',
  53. name: obj.get('name') || '',
  54. partyIds: (obj.get('partyIds') as string[]) || [],
  55. mobile: obj.get('mobile') || '',
  56. position: obj.get('position') || '',
  57. };
  58. }
  59. export async function listOrgDepartments(): Promise<OrgDepartmentDto[]> {
  60. const query = new Parse.Query('OrgDepartment');
  61. query.ascending('orderIndex');
  62. query.limit(500);
  63. const results = await query.find({ useMasterKey: true });
  64. return results.map(deptToDto);
  65. }
  66. export async function listOrgMembers(): Promise<OrgMemberDto[]> {
  67. const query = new Parse.Query('OrgMember');
  68. query.limit(1000);
  69. const results = await query.find({ useMasterKey: true });
  70. return results.map(memberToDto);
  71. }
  72. export async function getOrgMemberName(userId: string): Promise<string> {
  73. if (!userId) return '';
  74. const query = new Parse.Query('OrgMember');
  75. query.equalTo('userId', userId);
  76. const member = await query.first({ useMasterKey: true });
  77. return member?.get('name') || '';
  78. }
  79. export async function buildOrgTree(): Promise<OrgTreeNode[]> {
  80. const [departments, members] = await Promise.all([
  81. listOrgDepartments(),
  82. listOrgMembers(),
  83. ]);
  84. const nodeMap = new Map<string, OrgTreeNode>();
  85. for (const dept of departments) {
  86. nodeMap.set(dept.partyId, {
  87. partyId: dept.partyId,
  88. name: dept.name,
  89. regionCode: dept.regionCode,
  90. storeId: dept.storeId,
  91. children: [],
  92. members: [],
  93. });
  94. }
  95. for (const member of members) {
  96. const primaryParty = member.partyIds[0];
  97. const node = primaryParty ? nodeMap.get(primaryParty) : undefined;
  98. if (node) node.members.push(member);
  99. }
  100. const roots: OrgTreeNode[] = [];
  101. for (const dept of departments) {
  102. const node = nodeMap.get(dept.partyId)!;
  103. if (!dept.parentPartyId) {
  104. roots.push(node);
  105. continue;
  106. }
  107. const parent = nodeMap.get(dept.parentPartyId);
  108. if (parent) parent.children.push(node);
  109. else roots.push(node);
  110. }
  111. return roots;
  112. }
  113. async function upsertDepartment(data: Omit<OrgDepartmentDto, 'id'>): Promise<void> {
  114. const query = new Parse.Query('OrgDepartment');
  115. query.equalTo('partyId', data.partyId);
  116. const existing = await query.first({ useMasterKey: true });
  117. const obj = existing ?? new Parse.Object('OrgDepartment');
  118. obj.set('partyId', data.partyId);
  119. obj.set('name', data.name);
  120. obj.set('parentPartyId', data.parentPartyId);
  121. obj.set('regionCode', data.regionCode);
  122. obj.set('storeId', data.storeId);
  123. obj.set('orderIndex', data.orderIndex);
  124. obj.set('syncedAt', new Date());
  125. await obj.save(null, { useMasterKey: true });
  126. }
  127. async function upsertMember(data: Omit<OrgMemberDto, 'id'>): Promise<void> {
  128. const query = new Parse.Query('OrgMember');
  129. query.equalTo('userId', data.userId);
  130. const existing = await query.first({ useMasterKey: true });
  131. const obj = existing ?? new Parse.Object('OrgMember');
  132. obj.set('userId', data.userId);
  133. obj.set('name', data.name);
  134. obj.set('partyIds', data.partyIds);
  135. obj.set('mobile', data.mobile);
  136. obj.set('position', data.position);
  137. obj.set('syncedAt', new Date());
  138. await obj.save(null, { useMasterKey: true });
  139. }
  140. export async function bootstrapOrgTree(): Promise<void> {
  141. const stores = await listStores();
  142. let order = 10;
  143. for (const seed of SEED_DEPARTMENTS) {
  144. await upsertDepartment({ ...seed, orderIndex: seed.orderIndex });
  145. }
  146. for (const store of stores) {
  147. const partyId = `store_${store.code.toLowerCase()}`;
  148. const parentPartyId = SEED_DEPARTMENTS.find((d) => d.regionCode === store.region)?.partyId || 'root';
  149. await upsertDepartment({
  150. partyId,
  151. name: store.name,
  152. parentPartyId,
  153. regionCode: store.region,
  154. storeId: store.id,
  155. orderIndex: order++,
  156. });
  157. }
  158. console.log('[Org] 组织树种子已同步');
  159. }
  160. export async function syncOrgFromQiWe(): Promise<{
  161. departments: number;
  162. members: number;
  163. source: 'qiwe' | 'seed';
  164. errors: string[];
  165. }> {
  166. const errors: string[] = [];
  167. try {
  168. const contacts = await getInternalContactList();
  169. if (contacts.length === 0) {
  170. return { departments: 0, members: 0, source: 'seed', errors: ['企微未返回内部联系人,保留本地组织树'] };
  171. }
  172. const userIds = contacts.map((c) => c.userId).filter(Boolean);
  173. const details = await getContactDetailsBatch(userIds);
  174. const partyIds = new Set<string>();
  175. for (const contact of contacts) {
  176. if (contact.partyId) partyIds.add(contact.partyId);
  177. }
  178. let deptCount = 0;
  179. for (const partyId of partyIds) {
  180. await upsertDepartment({
  181. partyId,
  182. name: `部门 ${partyId.slice(-6)}`,
  183. parentPartyId: 'root',
  184. regionCode: '',
  185. storeId: '',
  186. orderIndex: deptCount,
  187. });
  188. deptCount++;
  189. }
  190. let memberCount = 0;
  191. for (const contact of contacts) {
  192. const detail = details.find((d) => d.userId === contact.userId);
  193. await upsertMember({
  194. userId: contact.userId,
  195. name: detail?.name || contact.userId,
  196. partyIds: contact.partyId ? [contact.partyId] : [],
  197. mobile: detail?.mobile || '',
  198. position: detail?.position || '',
  199. });
  200. memberCount++;
  201. }
  202. return { departments: deptCount, members: memberCount, source: 'qiwe', errors };
  203. } catch (err: unknown) {
  204. const msg = err instanceof Error ? err.message : String(err);
  205. errors.push(msg);
  206. return { departments: 0, members: 0, source: 'seed', errors };
  207. }
  208. }