WCFB 复用工作台企微打卡同步密钥
这个提交包含在:
@@ -40,6 +40,13 @@ function sanitizeCheckinApiResponse(payload) {
|
||||
return Object.keys(result).length ? result : null;
|
||||
}
|
||||
|
||||
function resolveCheckinSecret(config) {
|
||||
return (
|
||||
toSafeString(config?.wecomCheckinSecret, 500) ||
|
||||
toSafeString(config?.wecomAgentSecret, 500)
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeCheckinRecord(raw, { employeeByExternalUserId, timeZone }) {
|
||||
const externalUserId = toSafeString(raw?.userid ?? raw?.UserId, 128);
|
||||
const employee = employeeByExternalUserId.get(externalUserId);
|
||||
@@ -82,13 +89,14 @@ async function fetchWecomCheckinData({
|
||||
response: null,
|
||||
};
|
||||
}
|
||||
if (!toSafeString(config?.wecomCheckinSecret, 500)) {
|
||||
const checkinSecret = resolveCheckinSecret(config);
|
||||
if (!checkinSecret) {
|
||||
return {
|
||||
ok: false,
|
||||
skipped: true,
|
||||
retryable: false,
|
||||
code: "wecom_checkin_secret_missing",
|
||||
message: "Missing WECOM_CHECKIN_SECRET",
|
||||
message: "Missing WECOM_CHECKIN_SECRET or WECOM_AGENT_SECRET",
|
||||
records: [],
|
||||
response: null,
|
||||
};
|
||||
@@ -108,7 +116,7 @@ async function fetchWecomCheckinData({
|
||||
const tokenResult = await getWecomAppAccessToken({
|
||||
config: {
|
||||
...config,
|
||||
wecomAgentSecret: config.wecomCheckinSecret,
|
||||
wecomAgentSecret: checkinSecret,
|
||||
},
|
||||
timeoutMs: config.wecomRequestTimeoutMs ?? 5000,
|
||||
fetchImpl,
|
||||
@@ -177,5 +185,6 @@ module.exports = {
|
||||
fetchWecomCheckinData,
|
||||
formatDateInTimezone,
|
||||
normalizeCheckinRecord,
|
||||
resolveCheckinSecret,
|
||||
sanitizeCheckinApiResponse,
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ const { AppError } = require("../../../src/utils/AppError");
|
||||
const EMPLOYEE_ID = "11111111-1111-4111-8111-111111111111";
|
||||
const MANAGER_ID = "22222222-2222-4222-8222-222222222222";
|
||||
const DEPARTMENT_ID = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb";
|
||||
const originalFetch = global.fetch;
|
||||
|
||||
function buildEmployee(overrides = {}) {
|
||||
return {
|
||||
@@ -36,6 +37,14 @@ function buildFlag(flagKey, overrideMode = "force_on") {
|
||||
};
|
||||
}
|
||||
|
||||
function buildJsonResponse(payload, overrides = {}) {
|
||||
return {
|
||||
ok: overrides.ok ?? true,
|
||||
status: overrides.status ?? 200,
|
||||
json: async () => payload,
|
||||
};
|
||||
}
|
||||
|
||||
function buildApp(options = {}) {
|
||||
const app = Fastify();
|
||||
app.setErrorHandler((error, request, reply) => {
|
||||
@@ -91,6 +100,7 @@ function buildApp(options = {}) {
|
||||
wecomCheckinSyncEnabled: false,
|
||||
wecomCheckinSecret: "",
|
||||
leaveHolidayCalendarEnabled: false,
|
||||
...options.config,
|
||||
});
|
||||
app.decorate("prisma", {
|
||||
featureFlag: { findUnique: featureFlagFindUnique },
|
||||
@@ -179,6 +189,7 @@ describe("attendance unified routes", () => {
|
||||
let currentApp = null;
|
||||
|
||||
afterEach(async () => {
|
||||
global.fetch = originalFetch;
|
||||
if (currentApp) {
|
||||
await currentApp.close();
|
||||
currentApp = null;
|
||||
@@ -320,4 +331,115 @@ describe("attendance unified routes", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("records WeCom sync as skipped when no authorized app secret is configured", async () => {
|
||||
const { app, prisma } = buildApp({
|
||||
config: {
|
||||
wecomCheckinSyncEnabled: true,
|
||||
wecomCorpId: "corp-id",
|
||||
wecomAgentId: "1000001",
|
||||
wecomCheckinSecret: "",
|
||||
wecomAgentSecret: "",
|
||||
},
|
||||
});
|
||||
currentApp = app;
|
||||
|
||||
const response = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/attendance/wecom/sync",
|
||||
payload: {
|
||||
startDate: "2026-05-11",
|
||||
endDate: "2026-05-12",
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json().data).toMatchObject({
|
||||
skipped: true,
|
||||
code: "wecom_checkin_secret_missing",
|
||||
employeeCount: 1,
|
||||
boundUserCount: 1,
|
||||
recordCount: 0,
|
||||
});
|
||||
expect(prisma.wecomCheckinSyncRun.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
status: "skipped",
|
||||
error_code: "wecom_checkin_secret_missing",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the authorized workbench app secret fallback for WeCom checkin sync", async () => {
|
||||
global.fetch = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(buildJsonResponse({ errcode: 0, access_token: "token-from-workbench" }))
|
||||
.mockResolvedValueOnce(
|
||||
buildJsonResponse({
|
||||
errcode: 0,
|
||||
checkindata: [
|
||||
{
|
||||
userid: "wecom-user-1",
|
||||
checkin_time: 1778461200,
|
||||
checkin_type: "上班打卡",
|
||||
exception_type: "",
|
||||
source_type: "app",
|
||||
result: "正常",
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
const { app, prisma } = buildApp({
|
||||
config: {
|
||||
wecomCheckinSyncEnabled: true,
|
||||
wecomCorpId: "corp-id",
|
||||
wecomAgentId: "1000001",
|
||||
wecomCheckinSecret: "",
|
||||
wecomAgentSecret: "workbench-secret",
|
||||
},
|
||||
});
|
||||
currentApp = app;
|
||||
|
||||
const response = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/attendance/wecom/sync",
|
||||
payload: {
|
||||
startDate: "2026-05-11",
|
||||
endDate: "2026-05-12",
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json().data).toMatchObject({
|
||||
skipped: false,
|
||||
code: "wecom_checkin_fetched",
|
||||
employeeCount: 1,
|
||||
boundUserCount: 1,
|
||||
recordCount: 1,
|
||||
});
|
||||
expect(String(global.fetch.mock.calls[0][0])).toContain("corpsecret=workbench-secret");
|
||||
expect(String(global.fetch.mock.calls[1][0])).toContain("/cgi-bin/checkin/getcheckindata");
|
||||
expect(prisma.wecomCheckinRecord.createMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: [
|
||||
expect.objectContaining({
|
||||
employee_id: EMPLOYEE_ID,
|
||||
external_user_id: "wecom-user-1",
|
||||
checkin_type: "上班打卡",
|
||||
result: "正常",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
expect(prisma.wecomCheckinSyncRun.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
status: "success",
|
||||
record_count: 1,
|
||||
error_code: null,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
在新工单中引用
屏蔽一个用户