WHMS 实现企微HR主数据镜像同步

这个提交包含在:
2026-05-18 19:19:07 +08:00
父节点 3a6bfa0eaa
当前提交 27968fba49
共修改 24 个文件,包含 2123 行新增和 39 行删除
+2
查看文件
@@ -65,6 +65,8 @@ function getConfig() {
wecomContactSyncEnabled: process.env.WECOM_CONTACT_SYNC_ENABLED === "true",
wecomContactSecret: process.env.WECOM_CONTACT_SECRET ?? "",
wecomContactDefaultDepartmentId: process.env.WECOM_CONTACT_DEFAULT_DEPARTMENT_ID ?? "",
wecomHrMasterSyncEnabled: process.env.WECOM_HR_MASTER_SYNC_ENABLED === "true",
wecomHrAssistantSecret: process.env.WECOM_HR_ASSISTANT_SECRET ?? "",
wecomOauthEnabled: process.env.WECOM_OAUTH_ENABLED === "true",
wecomRedirectUri: process.env.WECOM_REDIRECT_URI ?? "",
wecomAllowedReturnHosts: process.env.WECOM_ALLOWED_RETURN_HOSTS ?? "",
+102
查看文件
@@ -19,6 +19,11 @@ const {
normalizeOperation,
syncWecomDirectoryForEmployee,
} = require("../services/integrations/wecomDirectory");
const {
getWecomHrMasterSyncRun,
listWecomHrMasterSyncRuns,
runWecomHrMasterSync,
} = require("../services/integrations/wecomHrMaster");
const { listOrganizationDepartments } = require("../services/organization");
const { StorageNotFoundError, openReadStream, parseStoragePath, writeBuffer } = require("../services/storage");
@@ -82,6 +87,16 @@ const WECOM_DIRECTORY_RETRY_SCHEMA = z.object({
operation: z.enum(["create", "update", "disable"]).optional(),
}).strict();
const WECOM_HR_MASTER_SYNC_RUN_SCHEMA = z.object({
mode: z.enum(["dry_run", "apply"]).default("dry_run"),
userids: z.array(z.string().trim().min(1).max(128)).max(200).optional(),
limit: z.coerce.number().int().min(1).max(200).optional(),
}).strict();
const WECOM_HR_MASTER_SYNC_LIST_QUERY_SCHEMA = z.object({
pageSize: z.coerce.number().int().min(1).max(50).optional(),
}).strict();
const CONTRACT_CREATE_SCHEMA = z.object({
contractNo: z.string().trim().min(1).max(100),
contractType: CONTRACT_TYPE_SCHEMA,
@@ -174,6 +189,32 @@ function canUseWecomDirectorySync(fastify) {
);
}
function canUseWecomHrMasterSync(fastify) {
return Boolean(
fastify?.prisma?.wecomHrMasterSyncRun
&& fastify?.prisma?.wecomHrMasterSyncItem
&& fastify?.prisma?.wecomIdentityBinding
&& fastify?.prisma?.integrationBinding
&& fastify?.prisma?.featureFlag,
);
}
async function ensureWecomHrMasterSyncEnabled(fastify, request) {
const gate = await evaluateFeatureFlag({
prisma: fastify.prisma,
config: fastify.config,
flagKey: FEATURE_FLAG_KEYS.wecomHrMasterSync,
departmentId: request.employee.department_id ?? null,
strictDepartment: false,
});
if (!gate.enabled) {
throw buildValidationError({ featureFlag: "wecom_hr_master_sync_v1_disabled", reason: gate.reason }, 409);
}
if (!canUseWecomHrMasterSync(fastify)) {
throw buildValidationError({ featureFlag: "wecom_hr_master_sync_v1_unavailable" }, 409);
}
}
async function trySyncWecomDirectory(fastify, request, options) {
if (!canUseWecomDirectorySync(fastify)) {
return null;
@@ -808,6 +849,67 @@ async function employeesRoutes(fastify) {
return sendData(reply, request, stats);
});
fastify.post("/employees/wecom-hr-master-sync/runs", { preHandler: fastify.authenticate }, async (request, reply) => {
await ensureFeatureEnabled(fastify, request);
if (!canManageLifecycle(fastify, request)) {
throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403, details: { permission: "system.users" } });
}
await ensureWecomHrMasterSyncEnabled(fastify, request);
const body = parseBody(WECOM_HR_MASTER_SYNC_RUN_SCHEMA, request.body);
const run = await runWecomHrMasterSync({
prisma: fastify.prisma,
config: fastify.config,
actorId: request.employee.id,
mode: body.mode,
userids: body.userids ?? [],
limit: body.limit,
});
await fastify.prisma.auditLog.create({
data: buildAuditLogData(request, {
actorId: request.employee.id,
action: body.mode === "apply" ? "WECOM_HR_MASTER_SYNC_APPLY" : "WECOM_HR_MASTER_SYNC_DRY_RUN",
targetType: "wecom_hr_master_sync_run",
targetId: run.id,
extra: {
mode: run.mode,
status: run.status,
summary: run.summary,
requestedUserids: run.requestedUserids,
tokenSource: run.tokenSource,
},
}),
});
return sendData(reply, request, run);
});
fastify.get("/employees/wecom-hr-master-sync/runs", { preHandler: fastify.authenticate }, async (request, reply) => {
await ensureFeatureEnabled(fastify, request);
if (!canManageLifecycle(fastify, request)) {
throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403, details: { permission: "system.users" } });
}
await ensureWecomHrMasterSyncEnabled(fastify, request);
const query = parseBody(WECOM_HR_MASTER_SYNC_LIST_QUERY_SCHEMA, request.query);
const items = await listWecomHrMasterSyncRuns(fastify.prisma, { pageSize: query.pageSize });
return sendData(reply, request, { items });
});
fastify.get("/employees/wecom-hr-master-sync/runs/:id", { preHandler: fastify.authenticate }, async (request, reply) => {
await ensureFeatureEnabled(fastify, request);
if (!canManageLifecycle(fastify, request)) {
throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403, details: { permission: "system.users" } });
}
await ensureWecomHrMasterSyncEnabled(fastify, request);
const runId = parseId(request.params?.id);
const run = await getWecomHrMasterSyncRun(fastify.prisma, runId);
if (!run) {
throw new AppError({ code: "SYS_404", message: "Not Found", statusCode: 404 });
}
return sendData(reply, request, run);
});
fastify.get("/employees/:id/wecom-directory-sync", { preHandler: fastify.authenticate }, async (request, reply) => {
await ensureFeatureEnabled(fastify, request);
if (!canManageLifecycle(fastify, request)) {
+9
查看文件
@@ -277,6 +277,15 @@ const FEATURE_FLAG_BACKEND_IMPACT = Object.freeze({
],
},
],
wecom_hr_master_sync_v1: [
{
label: "企微 HR 主数据镜像",
routes: [
"/api/employees/wecom-hr-master-sync/runs",
"/api/employees/wecom-hr-master-sync/runs/:id",
],
},
],
todo_board_v1: [
{
label: "待办工作台",
+7
查看文件
@@ -324,6 +324,13 @@ const DEFAULT_FEATURE_FLAGS = [
department_ids: [],
note: "bootstrap default: force_off",
},
{
flag_key: "wecom_hr_master_sync_v1",
override_mode: "force_off",
rollout_type: "global",
department_ids: [],
note: "bootstrap default: force_off",
},
{
flag_key: "todo_board_v1",
override_mode: "force_off",
@@ -16,6 +16,7 @@ const FEATURE_FLAG_KEYS = {
weeklyReport: "weekly_report_v1",
employeeLifecycle: "employee_lifecycle_v1",
hrWecomDirectorySync: "hr_wecom_directory_sync_v1",
wecomHrMasterSync: "wecom_hr_master_sync_v1",
todoBoard: "todo_board_v1",
wecomEntry: "wecom_entry_v1",
projectDomain: "project_domain_v1",
@@ -145,6 +146,10 @@ function resolveEnvGate(config, flagKey) {
return Boolean(config.wecomContactSyncEnabled);
}
if (flagKey === FEATURE_FLAG_KEYS.wecomHrMasterSync) {
return Boolean(config.wecomHrMasterSyncEnabled);
}
if (flagKey === FEATURE_FLAG_KEYS.todoBoard) {
return Boolean(config.todoBoardEnabled);
}
文件差异内容过多而无法显示 加载差异