HRWD-0..9 实现人事生命周期企微通讯录同步

这个提交包含在:
2026-05-17 21:01:28 +08:00
父节点 778132760f
当前提交 9ae42dadc1
共修改 24 个文件,包含 1553 行新增和 123 行删除
+158 -2
查看文件
@@ -12,6 +12,10 @@ const {
} = require("../services/employeeDirectoryFilters");
const { applyEmployeeTransfer } = require("../services/employeeLifecycle");
const { FEATURE_FLAG_KEYS, evaluateFeatureFlag } = require("../services/featureFlags");
const {
mapWecomDirectoryState,
syncWecomDirectoryForEmployee,
} = require("../services/integrations/wecomDirectory");
const { AppError } = require("../utils/AppError");
const { sendData } = require("../utils/reply");
const { buildAuditLogData } = require("../utils/audit");
@@ -53,6 +57,7 @@ const createBodySchema = z.object({
hireDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
roles: z.array(roleCodeSchema).min(1).optional(),
password: z.string().trim().min(1).optional(),
syncWecomDirectory: z.boolean().optional(),
});
const updateBodySchema = z.object({
@@ -218,6 +223,7 @@ function mapEmployeeSummary(row) {
hireDate: row.hire_date?.toISOString?.()?.slice(0, 10) ?? null,
avatarUrl: row.avatar_path ? `/api/avatars/${row.id}` : null,
roleCodes: row.roles.map((item) => item.role_code),
wecomDirectory: mapWecomDirectoryState(row),
createdAt: row.created_at?.toISOString?.() ?? null,
updatedAt: row.updated_at?.toISOString?.() ?? null,
};
@@ -445,6 +451,48 @@ async function setEmployeeAccountStatus(tx, request, options) {
return updated;
}
function canUseWecomDirectorySync(fastify) {
return Boolean(
fastify?.prisma?.wecomDirectorySyncRun
&& fastify?.prisma?.wecomIdentityBinding
&& fastify?.prisma?.featureFlag,
);
}
async function trySyncWecomDirectory(fastify, request, options) {
if (!canUseWecomDirectorySync(fastify)) {
return null;
}
const result = await syncWecomDirectoryForEmployee({
prisma: fastify.prisma,
config: fastify.config,
employeeId: options.employeeId,
actorId: request.employee.id,
operation: options.operation,
});
if (fastify.prisma.auditLog?.create) {
await fastify.prisma.auditLog.create({
data: buildAuditLogData(request, {
actorId: request.employee.id,
action: `WECOM_DIRECTORY_${String(options.operation ?? "update").toUpperCase()}`,
targetType: "employee",
targetId: options.employeeId,
extra: {
status: result.status,
code: result.code,
skipped: Boolean(result.skipped),
syncRunId: result.run?.id ?? null,
externalUserId: result.run?.externalUserId ?? null,
},
}),
});
}
return result;
}
function generateOneTimePassword() {
const password = `ResetA1${crypto.randomBytes(6).toString("hex")}`;
const errors = validatePassword(password);
@@ -978,6 +1026,13 @@ async function usersRoutes(fastify, opts = {}) {
department: { select: { id: true, name: true } },
direct_manager: { select: { id: true, employee_no: true, name: true } },
roles: { select: { role_code: true } },
wecom_identity_binding: {
select: { external_user_id: true, external_open_user_id: true },
},
wecom_directory_sync_runs: {
orderBy: { created_at: "desc" },
take: 1,
},
},
}),
fastify.prisma.employee.count({ where }),
@@ -1033,6 +1088,13 @@ async function usersRoutes(fastify, opts = {}) {
created_at: true,
updated_at: true,
roles: { select: { role_code: true } },
wecom_identity_binding: {
select: { external_user_id: true, external_open_user_id: true },
},
wecom_directory_sync_runs: {
orderBy: { created_at: "desc" },
take: 1,
},
},
});
@@ -1126,10 +1188,18 @@ async function usersRoutes(fastify, opts = {}) {
where: { employee_id: created.employee.id },
select: { role_code: true },
});
const directorySync =
body.syncWecomDirectory === false
? null
: await trySyncWecomDirectory(fastify, request, {
employeeId: created.employee.id,
operation: "create",
});
return sendData(reply, request, {
...mapEmployeeSummary({ ...created.employee, roles }),
oneTimePassword: created.oneTimePassword,
wecomDirectorySync: directorySync,
});
},
);
@@ -1173,6 +1243,10 @@ async function usersRoutes(fastify, opts = {}) {
});
});
for (const employeeId of body.employeeIds) {
await trySyncWecomDirectory(fastify, request, { employeeId, operation: "update" });
}
return sendData(reply, request, {
summary: { total: body.employeeIds.length, updated: body.employeeIds.length },
employeeIds: body.employeeIds,
@@ -1221,6 +1295,10 @@ async function usersRoutes(fastify, opts = {}) {
});
});
for (const employeeId of body.employeeIds) {
await trySyncWecomDirectory(fastify, request, { employeeId, operation: "update" });
}
return sendData(reply, request, {
summary: { total: body.employeeIds.length, updated: body.employeeIds.length },
employeeIds: body.employeeIds,
@@ -1268,6 +1346,10 @@ async function usersRoutes(fastify, opts = {}) {
});
});
for (const employeeId of body.employeeIds) {
await trySyncWecomDirectory(fastify, request, { employeeId, operation: "disable" });
}
return sendData(reply, request, {
summary: { total: body.employeeIds.length, updated: body.employeeIds.length },
employeeIds: body.employeeIds,
@@ -1312,6 +1394,10 @@ async function usersRoutes(fastify, opts = {}) {
});
});
for (const employeeId of body.employeeIds) {
await trySyncWecomDirectory(fastify, request, { employeeId, operation: "update" });
}
return sendData(reply, request, {
summary: { total: body.employeeIds.length, updated: body.employeeIds.length },
employeeIds: body.employeeIds,
@@ -1459,7 +1545,21 @@ async function usersRoutes(fastify, opts = {}) {
throw error;
}
return sendData(reply, request, result);
const wecomDirectorySync = [];
for (const item of result.imported) {
const syncResult = await trySyncWecomDirectory(fastify, request, {
employeeId: item.employeeId,
operation: "create",
});
if (syncResult) {
wecomDirectorySync.push(syncResult);
}
}
return sendData(reply, request, {
...result,
wecomDirectorySync,
});
},
);
}
@@ -1520,6 +1620,13 @@ async function usersRoutes(fastify, opts = {}) {
created_at: true,
updated_at: true,
roles: { select: { role_code: true } },
wecom_identity_binding: {
select: { external_user_id: true, external_open_user_id: true },
},
wecom_directory_sync_runs: {
orderBy: { created_at: "desc" },
take: 1,
},
},
});
if (!current) {
@@ -1589,6 +1696,13 @@ async function usersRoutes(fastify, opts = {}) {
created_at: true,
updated_at: true,
roles: { select: { role_code: true } },
wecom_identity_binding: {
select: { external_user_id: true, external_open_user_id: true },
},
wecom_directory_sync_runs: {
orderBy: { created_at: "desc" },
take: 1,
},
},
});
@@ -1618,10 +1732,23 @@ async function usersRoutes(fastify, opts = {}) {
throw error;
}
const shouldSyncDirectory =
Object.prototype.hasOwnProperty.call(body, "name")
|| Object.prototype.hasOwnProperty.call(body, "phone")
|| Object.prototype.hasOwnProperty.call(body, "email")
|| Object.prototype.hasOwnProperty.call(body, "departmentId")
|| Object.prototype.hasOwnProperty.call(body, "title");
const directorySync = shouldSyncDirectory
? await trySyncWecomDirectory(fastify, request, { employeeId: userId, operation: "update" })
: null;
return sendData(
reply,
request,
mapEmployeeDetail(updated, { config: fastify.config, logger: request.log ?? fastify.log }),
{
...mapEmployeeDetail(updated, { config: fastify.config, logger: request.log ?? fastify.log }),
wecomDirectorySync: directorySync,
},
);
},
);
@@ -1718,6 +1845,13 @@ async function usersRoutes(fastify, opts = {}) {
created_at: true,
updated_at: true,
roles: { select: { role_code: true } },
wecom_identity_binding: {
select: { external_user_id: true, external_open_user_id: true },
},
wecom_directory_sync_runs: {
orderBy: { created_at: "desc" },
take: 1,
},
},
});
if (!current) {
@@ -1783,6 +1917,13 @@ async function usersRoutes(fastify, opts = {}) {
created_at: true,
updated_at: true,
roles: { select: { role_code: true } },
wecom_identity_binding: {
select: { external_user_id: true, external_open_user_id: true },
},
wecom_directory_sync_runs: {
orderBy: { created_at: "desc" },
take: 1,
},
},
});
@@ -1870,10 +2011,15 @@ async function usersRoutes(fastify, opts = {}) {
return updated;
});
const directorySync = await trySyncWecomDirectory(fastify, request, {
employeeId: userId,
operation: "disable",
});
return sendData(reply, request, {
id: employee.id,
status: employee.status,
wecomDirectorySync: directorySync,
});
},
);
@@ -1903,10 +2049,15 @@ async function usersRoutes(fastify, opts = {}) {
now,
});
});
const directorySync = await trySyncWecomDirectory(fastify, request, {
employeeId: userId,
operation: "disable",
});
return sendData(reply, request, {
id: employee.id,
status: employee.status,
wecomDirectorySync: directorySync,
});
},
);
@@ -1928,10 +2079,15 @@ async function usersRoutes(fastify, opts = {}) {
now,
});
});
const directorySync = await trySyncWecomDirectory(fastify, request, {
employeeId: userId,
operation: "update",
});
return sendData(reply, request, {
id: employee.id,
status: employee.status,
wecomDirectorySync: directorySync,
});
},
);