import { NextResponse } from "next/server"; import { db } from "@/lib/db"; import { user } from "@/lib/db/schema"; import { eq, isNotNull, isNull, or, sql } from "drizzle-orm"; import { auth } from "@/app/(auth)/auth"; // 获取钉钉配置 async function getDingtalkConfig() { const settings = await db.query.authSettings.findFirst(); if (!settings?.isDingTalkEnabled || !settings.dingTalkAppKey || !settings.dingTalkAppSecret) { throw new Error("钉钉配置未启用或配置不完整"); } return { appKey: settings.dingTalkAppKey, appSecret: settings.dingTalkAppSecret, }; } // 获取钉钉 access_token async function getDingtalkAccessToken(appKey: string, appSecret: string) { const tokenResponse = await fetch( `https://oapi.dingtalk.com/gettoken?appkey=${appKey}&appsecret=${appSecret}` ); const tokenData = await tokenResponse.json(); if (tokenData.errcode !== 0) { throw new Error(`获取钉钉token失败: ${tokenData.errmsg}`); } return tokenData.access_token; } // 获取部门用户列表(使用新版 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/v2/user/list?access_token=${accessToken}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ dept_id: deptId, cursor: cursor, size: 100, }), } ); 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 []; } const users = data.result?.list || []; // 如果还有更多数据,递归获取 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]; } return users; } // 获取所有子部门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); } } return allDeptIds; } // 获取用户详细信息(包括部门) async function getUserDetailFromDingtalk(accessToken: string, userId: string) { const response = await fetch( `https://oapi.dingtalk.com/topapi/v2/user/get?access_token=${accessToken}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ userid: userId }), } ); const data = await response.json(); if (data.errcode !== 0) { console.error(`获取用户 ${userId} 信息失败:`, data.errmsg); return null; } return data.result; } // 获取部门名称 async function getDepartmentName(accessToken: string, deptId: number) { const response = await fetch( `https://oapi.dingtalk.com/topapi/v2/department/get?access_token=${accessToken}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ dept_id: deptId }), } ); const data = await response.json(); if (data.errcode !== 0) { console.error(`获取部门 ${deptId} 信息失败:`, data.errmsg); return null; } return data.result?.name; } // POST: 批量同步钉钉用户信息 export async function POST() { try { const session = await auth(); if (!session?.user?.id) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } // 检查是否为管理员 const currentUser = await db.query.user.findFirst({ where: eq(user.id, session.user.id), }); if (currentUser?.role !== "admin") { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); } // 获取钉钉配置 const config = await getDingtalkConfig(); const accessToken = await getDingtalkAccessToken(config.appKey, config.appSecret); console.log("[Sync DingTalk] Starting full sync from DingTalk..."); // 获取所有部门ID console.log("[Sync DingTalk] Fetching all department IDs..."); const allDeptIds = await getAllDeptIds(accessToken, 1); console.log(`[Sync DingTalk] Found ${allDeptIds.length} departments`); // 获取所有钉钉用户(按部门获取) 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) { usersWithEmail++; dingtalkUsersByEmail.set(dtUser.email.toLowerCase(), dtUser); } // 也尝试使用 org_email if (dtUser.org_email) { dingtalkUsersByEmail.set(dtUser.org_email.toLowerCase(), dtUser); } // 使用姓名作为备选匹配键 if (dtUser.name) { dingtalkUsersByName.set(dtUser.name, dtUser); } } // 添加延迟避免触发钉钉API限流(增加到300ms) await new Promise(resolve => setTimeout(resolve, 300)); } 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 .select({ id: user.id, email: user.email, name: user.name, dingtalkUserId: user.dingtalkUserId, department: user.department, }) .from(user); console.log(`[Sync DingTalk] Found ${dbUsers.length} users in database`); let syncedCount = 0; let skippedCount = 0; let errorCount = 0; let matchedCount = 0; const results: Array<{ email: string; status: string; department?: string; dingtalkUserId?: string }> = []; // 遍历数据库用户,匹配钉钉用户 for (const dbUser of dbUsers) { const emailLower = dbUser.email.toLowerCase(); // 优先通过邮箱匹配 let dtUser = dingtalkUsersByEmail.get(emailLower); // 如果邮箱匹配不到,尝试通过姓名匹配 if (!dtUser && dbUser.name) { dtUser = dingtalkUsersByName.get(dbUser.name); } if (!dtUser) { // 没有找到匹配的钉钉用户 skippedCount++; continue; } matchedCount++; try { // 获取部门名称列表 const deptIdList = dtUser.dept_id_list || []; const departmentNames: string[] = []; for (const deptId of deptIdList) { const deptName = await getDepartmentName(accessToken, deptId); if (deptName) { departmentNames.push(deptName); } } const departmentStr = departmentNames.length > 0 ? departmentNames.join(" / ") : null; const dingtalkUserId = dtUser.userid; // 检查是否需要更新 const needUpdate = departmentStr !== dbUser.department || dingtalkUserId !== dbUser.dingtalkUserId || (!dbUser.name && dtUser.name); if (needUpdate) { const updateData: Record = {}; if (departmentStr && departmentStr !== dbUser.department) { updateData.department = departmentStr; } if (dingtalkUserId && dingtalkUserId !== dbUser.dingtalkUserId) { updateData.dingtalkUserId = dingtalkUserId; } if (!dbUser.name && dtUser.name) { updateData.name = dtUser.name; } if (Object.keys(updateData).length > 0) { await db .update(user) .set(updateData) .where(eq(user.id, dbUser.id)); syncedCount++; results.push({ email: dbUser.email, status: "synced", department: departmentStr || undefined, dingtalkUserId: dingtalkUserId }); console.log(`[Sync DingTalk] Updated user ${dbUser.email}: dept=${departmentStr}, dtUserId=${dingtalkUserId}`); } else { skippedCount++; results.push({ email: dbUser.email, status: "no_change" }); } } else { skippedCount++; results.push({ email: dbUser.email, status: "already_synced" }); } // 添加延迟避免触发钉钉API限流 await new Promise(resolve => setTimeout(resolve, 30)); } catch (error) { console.error(`[Sync DingTalk] Error syncing user ${dbUser.email}:`, error); errorCount++; results.push({ email: dbUser.email, status: "error" }); } } console.log(`[Sync DingTalk] Sync completed. Synced: ${syncedCount}, Matched: ${matchedCount}, Skipped: ${skippedCount}, Errors: ${errorCount}`); return NextResponse.json({ success: true, message: `同步完成`, stats: { total: dbUsers.length, dingtalkUsers: totalDtUsers, dingtalkUsersWithEmail: usersWithEmail, matched: matchedCount, synced: syncedCount, skipped: skippedCount, errors: errorCount, }, results: results.slice(0, 50), // 只返回前50条结果 }); } catch (error) { console.error("[Sync DingTalk API] Error:", error); return NextResponse.json( { error: (error as Error).message || "同步失败" }, { status: 500 } ); } } // GET: 获取同步状态统计 export async function GET() { try { const session = await auth(); if (!session?.user?.id) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } // 检查是否为管理员 const currentUser = await db.query.user.findFirst({ where: eq(user.id, session.user.id), }); if (currentUser?.role !== "admin") { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); } // 统计用户数据 const allUsers = await db .select({ id: user.id, dingtalkUserId: user.dingtalkUserId, department: user.department, }) .from(user); const stats = { total: allUsers.length, withDingtalkId: allUsers.filter(u => u.dingtalkUserId).length, withDepartment: allUsers.filter(u => u.department).length, needSync: allUsers.filter(u => !u.department).length, // 没有部门信息的用户都需要同步 }; return NextResponse.json({ stats }); } catch (error) { console.error("[Sync DingTalk API] Error:", error); return NextResponse.json({ error: "获取统计失败" }, { status: 500 }); } }