import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Department } from '../entities/department.entity'; import { User } from '../entities/user.entity'; interface WeComTokenResponse { errcode: number; errmsg: string; access_token: string; expires_in: number; } interface WeComDeptListResponse { errcode: number; errmsg: string; department: Array<{ id: number; name: string; parentid: number }>; } interface WeComUserListResponse { errcode: number; errmsg: string; userlist: Array<{ userid: string; name: string; mobile: string; department: number[]; }>; } @Injectable() export class WeComService { private readonly logger = new Logger(WeComService.name); private accessToken: string | null = null; private tokenExpiresAt = 0; constructor( @InjectRepository(Department) private readonly deptRepo: Repository, @InjectRepository(User) private readonly userRepo: Repository, ) {} private get configured(): boolean { return !!(process.env.WECOM_CORP_ID && process.env.WECOM_CORP_SECRET); } private async getAccessToken(): Promise { if (this.accessToken && Date.now() < this.tokenExpiresAt - 60_000) { return this.accessToken; } const corpId = process.env.WECOM_CORP_ID!; const corpSecret = process.env.WECOM_CORP_SECRET!; const url = `https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=${corpId}&corpsecret=${corpSecret}`; const res = await fetch(url); const body: WeComTokenResponse = await res.json(); if (body.errcode !== 0) { throw new Error(`WeCom gettoken failed: ${body.errmsg} (${body.errcode})`); } this.accessToken = body.access_token; this.tokenExpiresAt = Date.now() + body.expires_in * 1000; return this.accessToken; } private async fetchDepartments( token: string, parentId = 1, ): Promise> { const all: WeComDeptListResponse['department'] = []; const url = `https://qyapi.weixin.qq.com/cgi-bin/department/list?access_token=${token}&id=${parentId}`; const res = await fetch(url); const body: WeComDeptListResponse = await res.json(); if (body.errcode !== 0) { if (body.errcode === 60003) return all; throw new Error(`WeCom department list failed: ${body.errmsg} (${body.errcode})`); } for (const dept of body.department) { all.push(dept); if (dept.id !== parentId) { const children = await this.fetchDepartments(token, dept.id); all.push(...children); } } return all; } private async fetchUsers( token: string, deptId: number, ): Promise> { const url = `https://qyapi.weixin.qq.com/cgi-bin/user/simplelist?access_token=${token}&department_id=${deptId}&fetch_child=1`; const res = await fetch(url); const body: WeComUserListResponse = await res.json(); if (body.errcode !== 0) { throw new Error(`WeCom user list failed: ${body.errmsg} (${body.errcode})`); } return body.userlist; } async syncAll(): Promise<{ deptCount: number; userCount: number }> { if (!this.configured) { this.logger.warn('WeCom not configured (WECOM_CORP_ID / WECOM_CORP_SECRET missing), skipping sync'); return { deptCount: 0, userCount: 0 }; } const token = await this.getAccessToken(); const wxDepts = await this.fetchDepartments(token); let deptCount = 0; for (const wd of wxDepts) { const sourceId = String(wd.id); let dept = await this.deptRepo.findOne({ where: { source: 'wecom', sourceId } }); if (dept) { dept.name = wd.name; dept.parentSourceId = (wd.parentid ? String(wd.parentid) : undefined) as any; } else { dept = this.deptRepo.create({ name: wd.name, source: 'wecom', sourceId, parentSourceId: (wd.parentid ? String(wd.parentid) : undefined), type: 'department', }); deptCount++; } await this.deptRepo.save(dept); } const syncedDepts = await this.deptRepo.find({ where: { source: 'wecom' } }); const idMap = new Map(syncedDepts.map((d) => [d.sourceId, d.id])); for (const dept of syncedDepts) { if (dept.parentSourceId && idMap.has(dept.parentSourceId)) { dept.parentId = idMap.get(dept.parentSourceId)!; } else if (dept.parentSourceId === '0' || dept.parentSourceId === '1') { dept.parentId = undefined as any; } } await this.deptRepo.save(syncedDepts); let userCount = 0; const seenUserIds = new Set(); for (const wd of wxDepts) { const wxUsers = await this.fetchUsers(token, wd.id); for (const wu of wxUsers) { if (seenUserIds.has(wu.userid)) continue; seenUserIds.add(wu.userid); let user = await this.userRepo.findOne({ where: { username: wu.userid } }); if (user) { user.name = wu.name; } else { user = this.userRepo.create({ username: wu.userid, name: wu.name, passwordHash: '', isActive: true, }); userCount++; } await this.userRepo.save(user); } } this.logger.log(`WeCom sync done: ${deptCount} new depts, ${userCount} new users`); return { deptCount, userCount }; } }