From 6646a7a099e34540fed4ca0ffb8cee88d8bc6eaf Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 19 Dec 2025 00:12:12 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E9=92=89=E9=92=89=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E5=8A=9F=E8=83=BD=E5=92=8C=E6=9B=B4=E6=96=B0=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/admin/sync-dingtalk/route.ts | 123 +++++++++++++++++++-------- 图片标注功能-部署完成总结.md | 2 + 2 files changed, 91 insertions(+), 34 deletions(-) diff --git a/app/api/admin/sync-dingtalk/route.ts b/app/api/admin/sync-dingtalk/route.ts index d5af9e177..97e0f04ba 100644 --- a/app/api/admin/sync-dingtalk/route.ts +++ b/app/api/admin/sync-dingtalk/route.ts @@ -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 { +// 获取部门用户列表(使用新版 v2 API,带限流重试) +async function getDeptUsers(accessToken: string, deptId: number, cursor: number = 0, retryCount: number = 0): Promise { 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 { - 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 { + 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(); // email -> user info + const dingtalkUsersByEmail = new Map(); // email -> user info + const dingtalkUsersByName = new Map(); // 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, diff --git a/图片标注功能-部署完成总结.md b/图片标注功能-部署完成总结.md index 33f1dc517..881a8c811 100644 --- a/图片标注功能-部署完成总结.md +++ b/图片标注功能-部署完成总结.md @@ -522,3 +522,5 @@ curl http://localhost:8002/health + +