forked from wangziqi/gongxue-base
166 lines
5.4 KiB
TypeScript
166 lines
5.4 KiB
TypeScript
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<Department>,
|
|
@InjectRepository(User)
|
|
private readonly userRepo: Repository<User>,
|
|
) {}
|
|
|
|
private get configured(): boolean {
|
|
return !!(process.env.WECOM_CORP_ID && process.env.WECOM_CORP_SECRET);
|
|
}
|
|
|
|
private async getAccessToken(): Promise<string> {
|
|
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<Array<{ id: number; name: string; parentid: number }>> {
|
|
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<Array<{ userid: string; name: string; mobile: string; department: number[] }>> {
|
|
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<string>();
|
|
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 };
|
|
}
|
|
}
|