WHMS 实现企微HR主数据镜像同步
这个提交包含在:
@@ -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)) {
|
||||
|
||||
@@ -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: "待办工作台",
|
||||
|
||||
在新工单中引用
屏蔽一个用户