完善钉钉同步功能和更新文档

This commit is contained in:
Your Name 2025-12-19 00:12:12 +08:00
parent 9285fc1822
commit 6646a7a099
2 changed files with 91 additions and 34 deletions

View File

@ -30,10 +30,10 @@ async function getDingtalkAccessToken(appKey: string, appSecret: string) {
return tokenData.access_token;
}
// 获取部门用户列表(分页)
async function getDeptUsers(accessToken: string, deptId: number, cursor: number = 0): Promise<any[]> {
// 获取部门用户列表(使用新版 v2 API,带限流重试)
async function getDeptUsers(accessToken: string, deptId: number, cursor: number = 0, retryCount: number = 0): Promise<any[]> {
const response = await fetch(
`https://oapi.dingtalk.com/topapi/user/listbypage?access_token=${accessToken}`,
`https://oapi.dingtalk.com/topapi/v2/user/list?access_token=${accessToken}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
@ -41,12 +41,18 @@ async function getDeptUsers(accessToken: string, deptId: number, cursor: number
dept_id: deptId,
cursor: cursor,
size: 100,
order_field: "modify_desc"
}),
}
);
const data = await response.json();
// 如果是限流错误,等待后重试(最多重试3次)
if (data.errcode === 90002 && retryCount < 3) {
console.log(`[Sync DingTalk] 部门 ${deptId} API限流,等待2秒后重试 (${retryCount + 1}/3)...`);
await new Promise(resolve => setTimeout(resolve, 2000));
return getDeptUsers(accessToken, deptId, cursor, retryCount + 1);
}
if (data.errcode !== 0) {
console.error(`获取部门 ${deptId} 用户列表失败:`, data.errmsg);
return [];
@ -56,6 +62,7 @@ async function getDeptUsers(accessToken: string, deptId: number, cursor: number
// 如果还有更多数据,递归获取
if (data.result?.has_more && data.result?.next_cursor) {
await new Promise(resolve => setTimeout(resolve, 300)); // 获取下一页前延迟
const moreUsers = await getDeptUsers(accessToken, deptId, data.result.next_cursor);
return [...users, ...moreUsers];
}
@ -63,29 +70,51 @@ async function getDeptUsers(accessToken: string, deptId: number, cursor: number
return users;
}
// 获取所有子部门ID
async function getAllDeptIds(accessToken: string, parentId: number = 1): Promise<number[]> {
const response = await fetch(
`https://oapi.dingtalk.com/topapi/v2/department/listsub?access_token=${accessToken}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ dept_id: parentId }),
// 获取所有子部门ID(使用队列方式避免递归导致的大量并发请求)
async function getAllDeptIds(accessToken: string, rootId: number = 1): Promise<number[]> {
const allDeptIds: number[] = [rootId];
const queue: number[] = [rootId];
while (queue.length > 0) {
const parentId = queue.shift()!;
// 添加延迟避免限流(增加到150ms)
await new Promise(resolve => setTimeout(resolve, 150));
let retryCount = 0;
let data: any = null;
while (retryCount < 3) {
const response = await fetch(
`https://oapi.dingtalk.com/topapi/v2/department/listsub?access_token=${accessToken}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ dept_id: parentId }),
}
);
data = await response.json();
// 如果是限流错误,等待后重试
if (data.errcode === 90002) {
console.log(`[Sync DingTalk] 获取子部门API限流,等待2秒后重试...`);
await new Promise(resolve => setTimeout(resolve, 2000));
retryCount++;
continue;
}
break;
}
if (!data || data.errcode !== 0) {
console.error(`获取子部门列表失败:`, data?.errmsg);
continue;
}
const subDepts = data.result || [];
for (const dept of subDepts) {
allDeptIds.push(dept.dept_id);
queue.push(dept.dept_id);
}
);
const data = await response.json();
if (data.errcode !== 0) {
console.error(`获取子部门列表失败:`, data.errmsg);
return [parentId];
}
const subDepts = data.result || [];
let allDeptIds = [parentId];
for (const dept of subDepts) {
const childDeptIds = await getAllDeptIds(accessToken, dept.dept_id);
allDeptIds = [...allDeptIds, ...childDeptIds];
}
return allDeptIds;
@ -161,25 +190,43 @@ export async function POST() {
console.log(`[Sync DingTalk] Found ${allDeptIds.length} departments`);
// 获取所有钉钉用户(按部门获取)
const dingtalkUsersMap = new Map<string, any>(); // email -> user info
const dingtalkUsersByEmail = new Map<string, any>(); // email -> user info
const dingtalkUsersByName = new Map<string, any>(); // name -> user info
let totalDtUsers = 0;
let usersWithEmail = 0;
for (const deptId of allDeptIds) {
const deptUsers = await getDeptUsers(accessToken, deptId);
// 调试:打印第一个部门的第一个用户结构
if (totalDtUsers === 0 && deptUsers.length > 0) {
console.log(`[Sync DingTalk] Sample user data structure:`, JSON.stringify(deptUsers[0], null, 2));
}
for (const dtUser of deptUsers) {
totalDtUsers++;
// 使用邮箱作为匹配键
if (dtUser.email) {
dingtalkUsersMap.set(dtUser.email.toLowerCase(), dtUser);
usersWithEmail++;
dingtalkUsersByEmail.set(dtUser.email.toLowerCase(), dtUser);
}
// 也尝试使用 org_email
if (dtUser.org_email) {
dingtalkUsersMap.set(dtUser.org_email.toLowerCase(), dtUser);
dingtalkUsersByEmail.set(dtUser.org_email.toLowerCase(), dtUser);
}
// 使用姓名作为备选匹配键
if (dtUser.name) {
dingtalkUsersByName.set(dtUser.name, dtUser);
}
}
// 添加延迟避免触发钉钉API限流
await new Promise(resolve => setTimeout(resolve, 50));
// 添加延迟避免触发钉钉API限流(增加到300ms)
await new Promise(resolve => setTimeout(resolve, 300));
}
console.log(`[Sync DingTalk] Found ${dingtalkUsersMap.size} DingTalk users with email`);
console.log(`[Sync DingTalk] Found ${totalDtUsers} total DingTalk users`);
console.log(`[Sync DingTalk] Found ${usersWithEmail} users with email`);
console.log(`[Sync DingTalk] Found ${dingtalkUsersByName.size} unique names`);
// 获取我们数据库中的所有用户
const dbUsers = await db
@ -203,7 +250,14 @@ export async function POST() {
// 遍历数据库用户,匹配钉钉用户
for (const dbUser of dbUsers) {
const emailLower = dbUser.email.toLowerCase();
const dtUser = dingtalkUsersMap.get(emailLower);
// 优先通过邮箱匹配
let dtUser = dingtalkUsersByEmail.get(emailLower);
// 如果邮箱匹配不到,尝试通过姓名匹配
if (!dtUser && dbUser.name) {
dtUser = dingtalkUsersByName.get(dbUser.name);
}
if (!dtUser) {
// 没有找到匹配的钉钉用户
@ -287,7 +341,8 @@ export async function POST() {
message: `同步完成`,
stats: {
total: dbUsers.length,
dingtalkUsers: dingtalkUsersMap.size,
dingtalkUsers: totalDtUsers,
dingtalkUsersWithEmail: usersWithEmail,
matched: matchedCount,
synced: syncedCount,
skipped: skippedCount,

View File

@ -522,3 +522,5 @@ curl http://localhost:8002/health