Your Name 12a383cceb
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
优化钉钉同步功能
2025-12-18 15:19:37 +08:00

348 lines
11 KiB
TypeScript

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;
}
// 获取部门用户列表(分页)
async function getDeptUsers(accessToken: string, deptId: number, cursor: number = 0): Promise<any[]> {
const response = await fetch(
`https://oapi.dingtalk.com/topapi/user/listbypage?access_token=${accessToken}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
dept_id: deptId,
cursor: cursor,
size: 100,
order_field: "modify_desc"
}),
}
);
const data = await response.json();
if (data.errcode !== 0) {
console.error(`获取部门 ${deptId} 用户列表失败:`, data.errmsg);
return [];
}
const users = data.result?.list || [];
// 如果还有更多数据,递归获取
if (data.result?.has_more && data.result?.next_cursor) {
const moreUsers = await getDeptUsers(accessToken, deptId, data.result.next_cursor);
return [...users, ...moreUsers];
}
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 }),
}
);
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;
}
// 获取用户详细信息(包括部门)
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 dingtalkUsersMap = new Map<string, any>(); // email -> user info
for (const deptId of allDeptIds) {
const deptUsers = await getDeptUsers(accessToken, deptId);
for (const dtUser of deptUsers) {
// 使用邮箱作为匹配键
if (dtUser.email) {
dingtalkUsersMap.set(dtUser.email.toLowerCase(), dtUser);
}
// 也尝试使用 org_email
if (dtUser.org_email) {
dingtalkUsersMap.set(dtUser.org_email.toLowerCase(), dtUser);
}
}
// 添加延迟避免触发钉钉API限流
await new Promise(resolve => setTimeout(resolve, 50));
}
console.log(`[Sync DingTalk] Found ${dingtalkUsersMap.size} DingTalk users with email`);
// 获取我们数据库中的所有用户
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();
const dtUser = dingtalkUsersMap.get(emailLower);
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<string, string | null> = {};
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: dingtalkUsersMap.size,
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 });
}
}