优化钉钉同步功能
This commit is contained in:
parent
7a4fa8e1d8
commit
12a383cceb
@ -914,11 +914,15 @@ export function AdminSettingsClient() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try {
|
try {
|
||||||
toast.info("正在同步钉钉用户部门信息...");
|
toast.info("正在从钉钉同步用户信息,可能需要几分钟...");
|
||||||
const res = await fetch("/api/admin/sync-dingtalk", { method: "POST" });
|
const res = await fetch("/api/admin/sync-dingtalk", { method: "POST" });
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
toast.success(`同步完成:${data.stats.synced} 个用户已更新,${data.stats.skipped} 个跳过,${data.stats.errors} 个失败`);
|
toast.success(
|
||||||
|
`同步完成!匹配 ${data.stats.matched}/${data.stats.total} 个用户,` +
|
||||||
|
`更新 ${data.stats.synced} 个,跳过 ${data.stats.skipped} 个` +
|
||||||
|
(data.stats.errors > 0 ? `,失败 ${data.stats.errors} 个` : "")
|
||||||
|
);
|
||||||
mutateUsers(); // 刷新用户列表
|
mutateUsers(); // 刷新用户列表
|
||||||
} else {
|
} else {
|
||||||
toast.error(data.error || "同步失败");
|
toast.error(data.error || "同步失败");
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
import { user } from "@/lib/db/schema";
|
import { user } from "@/lib/db/schema";
|
||||||
import { eq, isNotNull, isNull, or } from "drizzle-orm";
|
import { eq, isNotNull, isNull, or, sql } from "drizzle-orm";
|
||||||
import { auth } from "@/app/(auth)/auth";
|
import { auth } from "@/app/(auth)/auth";
|
||||||
|
|
||||||
// 获取钉钉配置
|
// 获取钉钉配置
|
||||||
@ -30,6 +30,67 @@ async function getDingtalkAccessToken(appKey: string, appSecret: string) {
|
|||||||
return tokenData.access_token;
|
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) {
|
async function getUserDetailFromDingtalk(accessToken: string, userId: string) {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
@ -92,45 +153,69 @@ export async function POST() {
|
|||||||
const config = await getDingtalkConfig();
|
const config = await getDingtalkConfig();
|
||||||
const accessToken = await getDingtalkAccessToken(config.appKey, config.appSecret);
|
const accessToken = await getDingtalkAccessToken(config.appKey, config.appSecret);
|
||||||
|
|
||||||
// 获取需要同步的用户(有钉钉userId但没有部门信息的用户)
|
console.log("[Sync DingTalk] Starting full sync from DingTalk...");
|
||||||
const usersToSync = await db
|
|
||||||
|
// 获取所有部门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({
|
.select({
|
||||||
id: user.id,
|
id: user.id,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
dingtalkUserId: user.dingtalkUserId,
|
dingtalkUserId: user.dingtalkUserId,
|
||||||
department: user.department,
|
department: user.department,
|
||||||
})
|
})
|
||||||
.from(user)
|
.from(user);
|
||||||
.where(
|
|
||||||
// 有钉钉userId但没有部门信息
|
|
||||||
isNotNull(user.dingtalkUserId)
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`[Sync DingTalk] Found ${usersToSync.length} users to check`);
|
console.log(`[Sync DingTalk] Found ${dbUsers.length} users in database`);
|
||||||
|
|
||||||
let syncedCount = 0;
|
let syncedCount = 0;
|
||||||
let skippedCount = 0;
|
let skippedCount = 0;
|
||||||
let errorCount = 0;
|
let errorCount = 0;
|
||||||
const results: Array<{ email: string; status: string; department?: string }> = [];
|
let matchedCount = 0;
|
||||||
|
const results: Array<{ email: string; status: string; department?: string; dingtalkUserId?: string }> = [];
|
||||||
|
|
||||||
for (const u of usersToSync) {
|
// 遍历数据库用户,匹配钉钉用户
|
||||||
if (!u.dingtalkUserId) {
|
for (const dbUser of dbUsers) {
|
||||||
|
const emailLower = dbUser.email.toLowerCase();
|
||||||
|
const dtUser = dingtalkUsersMap.get(emailLower);
|
||||||
|
|
||||||
|
if (!dtUser) {
|
||||||
|
// 没有找到匹配的钉钉用户
|
||||||
skippedCount++;
|
skippedCount++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
matchedCount++;
|
||||||
// 获取用户详细信息
|
|
||||||
const userDetail = await getUserDetailFromDingtalk(accessToken, u.dingtalkUserId);
|
|
||||||
|
|
||||||
if (!userDetail) {
|
|
||||||
errorCount++;
|
|
||||||
results.push({ email: u.email, status: "failed", department: undefined });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
try {
|
||||||
// 获取部门名称列表
|
// 获取部门名称列表
|
||||||
const deptIdList = userDetail.dept_id_list || [];
|
const deptIdList = dtUser.dept_id_list || [];
|
||||||
const departmentNames: string[] = [];
|
const departmentNames: string[] = [];
|
||||||
|
|
||||||
for (const deptId of deptIdList) {
|
for (const deptId of deptIdList) {
|
||||||
@ -141,45 +226,74 @@ export async function POST() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const departmentStr = departmentNames.length > 0 ? departmentNames.join(" / ") : null;
|
const departmentStr = departmentNames.length > 0 ? departmentNames.join(" / ") : null;
|
||||||
|
const dingtalkUserId = dtUser.userid;
|
||||||
|
|
||||||
// 只有部门信息变化时才更新
|
// 检查是否需要更新
|
||||||
if (departmentStr && departmentStr !== u.department) {
|
const needUpdate =
|
||||||
await db
|
departmentStr !== dbUser.department ||
|
||||||
.update(user)
|
dingtalkUserId !== dbUser.dingtalkUserId ||
|
||||||
.set({
|
(!dbUser.name && dtUser.name);
|
||||||
department: departmentStr,
|
|
||||||
name: userDetail.name || undefined, // 同时更新姓名
|
if (needUpdate) {
|
||||||
})
|
const updateData: Record<string, string | null> = {};
|
||||||
.where(eq(user.id, u.id));
|
|
||||||
|
|
||||||
syncedCount++;
|
if (departmentStr && departmentStr !== dbUser.department) {
|
||||||
results.push({ email: u.email, status: "synced", department: departmentStr });
|
updateData.department = departmentStr;
|
||||||
console.log(`[Sync DingTalk] Updated user ${u.email}: ${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 {
|
} else {
|
||||||
skippedCount++;
|
skippedCount++;
|
||||||
results.push({ email: u.email, status: "skipped", department: u.department || undefined });
|
results.push({ email: dbUser.email, status: "already_synced" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加延迟避免触发钉钉API限流
|
// 添加延迟避免触发钉钉API限流
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
await new Promise(resolve => setTimeout(resolve, 30));
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[Sync DingTalk] Error syncing user ${u.email}:`, error);
|
console.error(`[Sync DingTalk] Error syncing user ${dbUser.email}:`, error);
|
||||||
errorCount++;
|
errorCount++;
|
||||||
results.push({ email: u.email, status: "error" });
|
results.push({ email: dbUser.email, status: "error" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`[Sync DingTalk] Sync completed. Synced: ${syncedCount}, Matched: ${matchedCount}, Skipped: ${skippedCount}, Errors: ${errorCount}`);
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: `同步完成`,
|
message: `同步完成`,
|
||||||
stats: {
|
stats: {
|
||||||
total: usersToSync.length,
|
total: dbUsers.length,
|
||||||
|
dingtalkUsers: dingtalkUsersMap.size,
|
||||||
|
matched: matchedCount,
|
||||||
synced: syncedCount,
|
synced: syncedCount,
|
||||||
skipped: skippedCount,
|
skipped: skippedCount,
|
||||||
errors: errorCount,
|
errors: errorCount,
|
||||||
},
|
},
|
||||||
results,
|
results: results.slice(0, 50), // 只返回前50条结果
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[Sync DingTalk API] Error:", error);
|
console.error("[Sync DingTalk API] Error:", error);
|
||||||
@ -221,7 +335,7 @@ export async function GET() {
|
|||||||
total: allUsers.length,
|
total: allUsers.length,
|
||||||
withDingtalkId: allUsers.filter(u => u.dingtalkUserId).length,
|
withDingtalkId: allUsers.filter(u => u.dingtalkUserId).length,
|
||||||
withDepartment: allUsers.filter(u => u.department).length,
|
withDepartment: allUsers.filter(u => u.department).length,
|
||||||
needSync: allUsers.filter(u => u.dingtalkUserId && !u.department).length,
|
needSync: allUsers.filter(u => !u.department).length, // 没有部门信息的用户都需要同步
|
||||||
};
|
};
|
||||||
|
|
||||||
return NextResponse.json({ stats });
|
return NextResponse.json({ stats });
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user