From 3992f6c33f77347fe159a12b14d49d840653acf9 Mon Sep 17 00:00:00 2001 From: wangziqi Date: Fri, 10 Jul 2026 10:04:00 +0800 Subject: [PATCH] feat: implement fetchOrgTree and fetchOrgTreeWithUsers via dingtalk API --- .../src/integration/dingtalk.service.ts | 107 +++++++++++++++--- 1 file changed, 94 insertions(+), 13 deletions(-) diff --git a/apps/server/src/integration/dingtalk.service.ts b/apps/server/src/integration/dingtalk.service.ts index 43249519..60d75478 100644 --- a/apps/server/src/integration/dingtalk.service.ts +++ b/apps/server/src/integration/dingtalk.service.ts @@ -47,6 +47,29 @@ export interface DingTalkAttendanceResult { checkType: string; } +// ── 组织架构 API 类型 ── + +interface DingTalkDeptListResponse { + errcode: number; + result?: Array<{ dept_id: number; name: string; parent_id: number }>; +} + +interface DingTalkDeptGetResponse { + errcode: number; + result?: { name: string; parent_id: number }; +} + +export interface OrgDeptNode { + id: number; + name: string; + parentId: number; + children: OrgDeptNode[]; +} + +export interface OrgDeptNodeWithUsers extends OrgDeptNode { + users: Array<{ userid: string; name: string; mobile: string; deptIds: number[] }>; +} + // ── 考勤排班 API 类型 ── /** 班次卡段打卡时间 */ @@ -237,7 +260,6 @@ export class DingTalkService { const t0 = Date.now(); const token = await this.getAccessToken(); - // ── Sync users from root department ── let userCount = 0; const seenUserIds = new Set(); @@ -256,20 +278,79 @@ export class DingTalkService { return { deptCount: 0, userCount }; } - /** - * 获取钉钉组织部门树(只含部门,不含用户)。 - * ponytail: Department entity removed; returns empty array. - */ - async fetchOrgTree(_rootDeptId = 1): Promise<[]> { - return []; + // ═══════════════════════════════════════════ + + /** 递归获取子部门列表(含名称) */ + private async getSubDepts(token: string, deptId: number): Promise> { + await this.rateLimit(); + const res = await fetch( + `https://oapi.dingtalk.com/topapi/v2/department/listsub?access_token=${token}`, + { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ dept_id: deptId }) }, + ); + const body = await res.json() as DingTalkDeptListResponse; + return body.errcode === 0 ? (body.result ?? []) : []; } - /** - * 获取钉钉组织部门树(含用户)。 - * ponytail: Department entity removed; returns empty array. - */ - async fetchOrgTreeWithUsers(_rootDeptId = 1): Promise<[]> { - return []; + /** 获取单个部门详情 */ + private async getDeptInfo(token: string, deptId: number): Promise<{ name: string; parent_id: number } | null> { + await this.rateLimit(); + const res = await fetch( + `https://oapi.dingtalk.com/topapi/v2/department/get?access_token=${token}`, + { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ dept_id: deptId }) }, + ); + const body = await res.json() as DingTalkDeptGetResponse; + return body.errcode === 0 && body.result ? body.result : null; + } + + /** 递归构建部门树节点 */ + private async buildDeptNode(token: string, deptId: number, name: string, parentId: number): Promise { + const subDepts = await this.getSubDepts(token, deptId); + const children = await Promise.all( + subDepts.map(sd => this.buildDeptNode(token, sd.dept_id, sd.name, sd.parent_id)), + ); + return { id: deptId, name, parentId, children }; + } + + /** 获取钉钉组织部门树(只含部门) */ + async fetchOrgTree(rootDeptId = 1): Promise { + if (!this.configured) return []; + const token = await this.getAccessToken(); + const rootInfo = await this.getDeptInfo(token, rootDeptId); + if (!rootInfo) return []; + const node = await this.buildDeptNode(token, rootDeptId, rootInfo.name, rootInfo.parent_id); + return [node]; + } + + /** 获取钉钉组织部门树(含用户) */ + async fetchOrgTreeWithUsers(rootDeptId = 1): Promise { + if (!this.configured) return []; + const token = await this.getAccessToken(); + const rootInfo = await this.getDeptInfo(token, rootDeptId); + if (!rootInfo) return []; + + // 从根部门拉全部用户(翻页),按 dept_id_list 分配到各部门 + const allUsers = await this.getDeptUsers(token, rootDeptId); + const usersByDept = new Map>(); + for (const u of allUsers) { + for (const did of u.dept_id_list) { + if (!usersByDept.has(did)) usersByDept.set(did, []); + usersByDept.get(did)!.push({ userid: u.userid, name: u.name, mobile: u.mobile, deptIds: u.dept_id_list }); + } + } + + const buildWithUsers = async (deptId: number, name: string, parentId: number): Promise => { + const subDepts = await this.getSubDepts(token, deptId); + const children = await Promise.all( + subDepts.map(sd => buildWithUsers(sd.dept_id, sd.name, sd.parent_id)), + ); + return { + id: deptId, name, parentId, children, + users: usersByDept.get(deptId) ?? [], + }; + }; + + const node = await buildWithUsers(rootDeptId, rootInfo.name, rootInfo.parent_id); + return [node]; } // ═══════════════════════════════════════════