WARCH3/WCHECK1/WEXT1 完成受控上线后续自动化
Issue: WARCH3, WCHECK1, WEXT1
这个提交包含在:
@@ -31,12 +31,37 @@ function hashPayload(value) {
|
||||
.digest("hex");
|
||||
}
|
||||
|
||||
function maskIdentifier(value) {
|
||||
const raw = toSafeString(value, 128);
|
||||
if (!raw) return "";
|
||||
const visibleLength = raw.length <= 4 ? 1 : raw.length <= 8 ? 2 : 4;
|
||||
return `${raw.slice(0, visibleLength)}***`;
|
||||
}
|
||||
|
||||
function maskDelimitedIdentifiers(value) {
|
||||
const raw = toSafeString(value, 1000);
|
||||
if (!raw) return raw;
|
||||
return raw
|
||||
.split(/[|,;\s]+/)
|
||||
.filter(Boolean)
|
||||
.map(maskIdentifier)
|
||||
.join("|");
|
||||
}
|
||||
|
||||
function sanitizeCheckinApiResponse(payload) {
|
||||
if (!payload || typeof payload !== "object") return null;
|
||||
const result = sanitizeWecomResponse(payload) ?? {};
|
||||
for (const key of ["invaliduser", "invalidparty", "invalidtag"]) {
|
||||
if (typeof result[key] === "string") {
|
||||
result[key] = maskDelimitedIdentifiers(result[key]);
|
||||
}
|
||||
}
|
||||
if (Array.isArray(payload.checkindata)) {
|
||||
result.checkindataCount = payload.checkindata.length;
|
||||
}
|
||||
if (Array.isArray(payload.info)) {
|
||||
result.ruleInfoCount = payload.info.length;
|
||||
}
|
||||
return Object.keys(result).length ? result : null;
|
||||
}
|
||||
|
||||
@@ -182,9 +207,121 @@ async function fetchWecomCheckinData({
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchWecomCheckinRules({
|
||||
config,
|
||||
date,
|
||||
externalUserIds,
|
||||
requireSyncEnabled = false,
|
||||
fetchImpl = fetch,
|
||||
}) {
|
||||
const userIds = Array.from(new Set((externalUserIds ?? []).map((id) => toSafeString(id, 128)).filter(Boolean)));
|
||||
if (requireSyncEnabled && !config?.wecomCheckinSyncEnabled) {
|
||||
return {
|
||||
ok: false,
|
||||
skipped: true,
|
||||
retryable: false,
|
||||
code: "wecom_checkin_sync_disabled",
|
||||
message: "WECOM_CHECKIN_SYNC_ENABLED is not true",
|
||||
rules: [],
|
||||
response: null,
|
||||
};
|
||||
}
|
||||
const checkinSecret = resolveCheckinSecret(config);
|
||||
if (!checkinSecret) {
|
||||
return {
|
||||
ok: false,
|
||||
skipped: true,
|
||||
retryable: false,
|
||||
code: "wecom_checkin_secret_missing",
|
||||
message: "Missing WECOM_CHECKIN_SECRET or WECOM_AGENT_SECRET",
|
||||
rules: [],
|
||||
response: null,
|
||||
};
|
||||
}
|
||||
if (!userIds.length) {
|
||||
return {
|
||||
ok: true,
|
||||
skipped: false,
|
||||
retryable: false,
|
||||
code: "wecom_checkin_no_bound_users",
|
||||
message: null,
|
||||
rules: [],
|
||||
response: { userCount: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
const tokenResult = await getWecomAppAccessToken({
|
||||
config: {
|
||||
...config,
|
||||
wecomAgentSecret: checkinSecret,
|
||||
},
|
||||
timeoutMs: config.wecomRequestTimeoutMs ?? 5000,
|
||||
fetchImpl,
|
||||
});
|
||||
if (!tokenResult.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
skipped: false,
|
||||
retryable: tokenResult.retryable,
|
||||
code: tokenResult.code,
|
||||
message: tokenResult.message,
|
||||
rules: [],
|
||||
response: tokenResult.response,
|
||||
};
|
||||
}
|
||||
|
||||
const dateValue = date instanceof Date ? date : buildShanghaiDateTime(date, false);
|
||||
const url = new URL("https://qyapi.weixin.qq.com/cgi-bin/checkin/getcheckinoption");
|
||||
url.searchParams.set("access_token", tokenResult.accessToken);
|
||||
const response = await fetchImpl(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
datetime: toUnixSeconds(dateValue),
|
||||
useridlist: userIds,
|
||||
}),
|
||||
});
|
||||
const payload = await response.json().catch(() => null);
|
||||
const sanitized = sanitizeCheckinApiResponse(payload);
|
||||
if (!response.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
skipped: false,
|
||||
retryable: response.status >= 500 || response.status === 429,
|
||||
code: response.status >= 500 ? "wecom_checkin_rule_http_5xx" : "wecom_checkin_rule_http_error",
|
||||
message: `WeCom checkin rule HTTP ${response.status}`,
|
||||
rules: [],
|
||||
response: { status: response.status, body: sanitized },
|
||||
};
|
||||
}
|
||||
if (!payload || Number(payload.errcode ?? 0) !== 0) {
|
||||
return {
|
||||
ok: false,
|
||||
skipped: false,
|
||||
retryable: false,
|
||||
code: "wecom_checkin_rule_api_error",
|
||||
message: `WeCom checkin rule errcode ${payload?.errcode ?? "missing_payload"}`,
|
||||
rules: [],
|
||||
response: sanitized,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
skipped: false,
|
||||
retryable: false,
|
||||
code: "wecom_checkin_rule_fetched",
|
||||
message: null,
|
||||
rules: Array.isArray(payload.info) ? payload.info : [],
|
||||
response: sanitized,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fetchWecomCheckinRules,
|
||||
fetchWecomCheckinData,
|
||||
formatDateInTimezone,
|
||||
maskDelimitedIdentifiers,
|
||||
normalizeCheckinRecord,
|
||||
resolveCheckinSecret,
|
||||
sanitizeCheckinApiResponse,
|
||||
|
||||
@@ -0,0 +1,435 @@
|
||||
const crypto = require("node:crypto");
|
||||
|
||||
function toSafeString(value, maxLength = 500) {
|
||||
if (value == null) return "";
|
||||
return String(value).trim().slice(0, maxLength);
|
||||
}
|
||||
|
||||
function sha256(value) {
|
||||
return crypto.createHash("sha256").update(String(value ?? "")).digest("hex");
|
||||
}
|
||||
|
||||
function stableJson(value) {
|
||||
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
||||
if (value && typeof value === "object") {
|
||||
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`;
|
||||
}
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
function digestObject(value) {
|
||||
return sha256(stableJson(value));
|
||||
}
|
||||
|
||||
function maskExternalUserId(value) {
|
||||
const raw = toSafeString(value, 128);
|
||||
if (!raw) return null;
|
||||
const visibleLength = raw.length <= 4 ? 1 : raw.length <= 8 ? 2 : 4;
|
||||
return `${raw.slice(0, visibleLength)}***`;
|
||||
}
|
||||
|
||||
function formatDateOnly(value) {
|
||||
const date = value instanceof Date ? value : new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return "";
|
||||
return date.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function monthOfDate(value) {
|
||||
return formatDateOnly(value).slice(0, 7);
|
||||
}
|
||||
|
||||
function toDateOnlyUtc(value) {
|
||||
const text = formatDateOnly(value);
|
||||
return text ? new Date(`${text}T00:00:00.000Z`) : null;
|
||||
}
|
||||
|
||||
function normalizeRecord(record) {
|
||||
const employeeId = toSafeString(record?.employee_id ?? record?.employeeId, 64);
|
||||
const externalUserId = toSafeString(record?.external_user_id ?? record?.externalUserId, 128);
|
||||
const checkinTime = record?.checkin_time ?? record?.checkinTime;
|
||||
const checkinDate = record?.checkin_date ?? record?.checkinDate ?? checkinTime;
|
||||
const checkinDateValue = toDateOnlyUtc(checkinDate);
|
||||
const checkinTimeValue = checkinTime instanceof Date ? checkinTime : new Date(checkinTime);
|
||||
if (!employeeId || !checkinDateValue || Number.isNaN(checkinTimeValue.getTime())) return null;
|
||||
return {
|
||||
employeeId,
|
||||
externalUserId,
|
||||
externalUserIdPrefix: maskExternalUserId(externalUserId),
|
||||
checkinDate: checkinDateValue,
|
||||
checkinDateString: formatDateOnly(checkinDateValue),
|
||||
checkinTime: checkinTimeValue,
|
||||
checkinType: toSafeString(record?.checkin_type ?? record?.checkinType ?? "unknown", 40) || "unknown",
|
||||
exceptionType: toSafeString(record?.exception_type ?? record?.exceptionType, 80),
|
||||
sourceType: toSafeString(record?.source_type ?? record?.sourceType ?? "unknown", 40) || "unknown",
|
||||
result: toSafeString(record?.result ?? "unknown", 80) || "unknown",
|
||||
rawHash: toSafeString(record?.raw_hash ?? record?.rawHash, 64),
|
||||
syncRunId: toSafeString(record?.sync_run_id ?? record?.syncRunId, 64),
|
||||
};
|
||||
}
|
||||
|
||||
function increment(map, key, amount = 1) {
|
||||
const normalized = toSafeString(key || "unknown", 80) || "unknown";
|
||||
map[normalized] = (map[normalized] || 0) + amount;
|
||||
}
|
||||
|
||||
function buildDailySummaries(records) {
|
||||
const groups = new Map();
|
||||
for (const raw of records || []) {
|
||||
const record = normalizeRecord(raw);
|
||||
if (!record) continue;
|
||||
const key = `${record.employeeId}|${record.checkinDateString}`;
|
||||
if (!groups.has(key)) {
|
||||
groups.set(key, {
|
||||
employee_id: record.employeeId,
|
||||
external_user_id_prefix: record.externalUserIdPrefix,
|
||||
checkin_date: record.checkinDate,
|
||||
record_count: 0,
|
||||
first_checkin_at: null,
|
||||
last_checkin_at: null,
|
||||
exception_count: 0,
|
||||
abnormal_count: 0,
|
||||
checkin_type_summary: {},
|
||||
result_summary: {},
|
||||
source_type_summary: {},
|
||||
rawHashes: [],
|
||||
source_record_count: 0,
|
||||
last_sync_run_id: record.syncRunId || null,
|
||||
});
|
||||
}
|
||||
const summary = groups.get(key);
|
||||
summary.record_count += 1;
|
||||
summary.source_record_count += 1;
|
||||
if (!summary.first_checkin_at || record.checkinTime < summary.first_checkin_at) {
|
||||
summary.first_checkin_at = record.checkinTime;
|
||||
}
|
||||
if (!summary.last_checkin_at || record.checkinTime > summary.last_checkin_at) {
|
||||
summary.last_checkin_at = record.checkinTime;
|
||||
}
|
||||
if (record.exceptionType) summary.exception_count += 1;
|
||||
if (record.exceptionType || !["normal", "regular", "success", "ok", "unknown", ""].includes(record.result.toLowerCase())) {
|
||||
summary.abnormal_count += 1;
|
||||
}
|
||||
increment(summary.checkin_type_summary, record.checkinType);
|
||||
increment(summary.result_summary, record.result);
|
||||
increment(summary.source_type_summary, record.sourceType);
|
||||
if (record.rawHash) summary.rawHashes.push(record.rawHash);
|
||||
if (record.syncRunId) summary.last_sync_run_id = record.syncRunId;
|
||||
}
|
||||
|
||||
return Array.from(groups.values()).map((summary) => ({
|
||||
...summary,
|
||||
raw_hash_digest: summary.rawHashes.length ? digestObject(summary.rawHashes.sort()) : null,
|
||||
rawHashes: undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
function buildMonthlySummaries(dailySummaries, { expectedWorkdaysByMonth = {} } = {}) {
|
||||
const groups = new Map();
|
||||
for (const day of dailySummaries || []) {
|
||||
const month = monthOfDate(day.checkin_date);
|
||||
if (!month) continue;
|
||||
const key = `${day.employee_id}|${month}`;
|
||||
if (!groups.has(key)) {
|
||||
groups.set(key, {
|
||||
employee_id: day.employee_id,
|
||||
month,
|
||||
start_date: toDateOnlyUtc(`${month}-01`),
|
||||
end_date: toDateOnlyUtc(day.checkin_date),
|
||||
summary_day_count: 0,
|
||||
checkin_day_count: 0,
|
||||
exception_day_count: 0,
|
||||
abnormal_record_count: 0,
|
||||
missing_day_candidate_count: 0,
|
||||
first_checkin_at: null,
|
||||
last_checkin_at: null,
|
||||
result_summary: {},
|
||||
rawHashes: [],
|
||||
});
|
||||
}
|
||||
const summary = groups.get(key);
|
||||
summary.summary_day_count += 1;
|
||||
if (day.record_count > 0) summary.checkin_day_count += 1;
|
||||
if (day.exception_count > 0 || day.abnormal_count > 0) summary.exception_day_count += 1;
|
||||
summary.abnormal_record_count += Number(day.abnormal_count || 0);
|
||||
if (!summary.first_checkin_at || (day.first_checkin_at && day.first_checkin_at < summary.first_checkin_at)) {
|
||||
summary.first_checkin_at = day.first_checkin_at;
|
||||
}
|
||||
if (!summary.last_checkin_at || (day.last_checkin_at && day.last_checkin_at > summary.last_checkin_at)) {
|
||||
summary.last_checkin_at = day.last_checkin_at;
|
||||
}
|
||||
if (day.checkin_date > summary.end_date) summary.end_date = day.checkin_date;
|
||||
for (const [result, count] of Object.entries(day.result_summary || {})) {
|
||||
increment(summary.result_summary, result, count);
|
||||
}
|
||||
if (day.raw_hash_digest) summary.rawHashes.push(day.raw_hash_digest);
|
||||
}
|
||||
|
||||
return Array.from(groups.values()).map((summary) => {
|
||||
const expectedWorkdays = Number(expectedWorkdaysByMonth[summary.month] || 0);
|
||||
const missing = expectedWorkdays > 0 ? Math.max(0, expectedWorkdays - summary.checkin_day_count) : 0;
|
||||
return {
|
||||
...summary,
|
||||
missing_day_candidate_count: missing,
|
||||
raw_hash_digest: summary.rawHashes.length ? digestObject(summary.rawHashes.sort()) : null,
|
||||
rawHashes: undefined,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function upsertDailySummaries(prisma, summaries) {
|
||||
let count = 0;
|
||||
for (const summary of summaries || []) {
|
||||
await prisma.wecomCheckinDailySummary.upsert({
|
||||
where: {
|
||||
employee_id_checkin_date: {
|
||||
employee_id: summary.employee_id,
|
||||
checkin_date: summary.checkin_date,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
employee_id: summary.employee_id,
|
||||
external_user_id_prefix: summary.external_user_id_prefix,
|
||||
checkin_date: summary.checkin_date,
|
||||
record_count: summary.record_count,
|
||||
first_checkin_at: summary.first_checkin_at,
|
||||
last_checkin_at: summary.last_checkin_at,
|
||||
exception_count: summary.exception_count,
|
||||
abnormal_count: summary.abnormal_count,
|
||||
checkin_type_summary: summary.checkin_type_summary,
|
||||
result_summary: summary.result_summary,
|
||||
source_type_summary: summary.source_type_summary,
|
||||
raw_hash_digest: summary.raw_hash_digest,
|
||||
source_record_count: summary.source_record_count,
|
||||
last_sync_run_id: summary.last_sync_run_id,
|
||||
},
|
||||
update: {
|
||||
external_user_id_prefix: summary.external_user_id_prefix,
|
||||
record_count: summary.record_count,
|
||||
first_checkin_at: summary.first_checkin_at,
|
||||
last_checkin_at: summary.last_checkin_at,
|
||||
exception_count: summary.exception_count,
|
||||
abnormal_count: summary.abnormal_count,
|
||||
checkin_type_summary: summary.checkin_type_summary,
|
||||
result_summary: summary.result_summary,
|
||||
source_type_summary: summary.source_type_summary,
|
||||
raw_hash_digest: summary.raw_hash_digest,
|
||||
source_record_count: summary.source_record_count,
|
||||
last_sync_run_id: summary.last_sync_run_id,
|
||||
archived_at: new Date(),
|
||||
},
|
||||
});
|
||||
count += 1;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
async function upsertMonthlySummaries(prisma, summaries) {
|
||||
let count = 0;
|
||||
for (const summary of summaries || []) {
|
||||
await prisma.wecomCheckinMonthlySummary.upsert({
|
||||
where: {
|
||||
employee_id_month: {
|
||||
employee_id: summary.employee_id,
|
||||
month: summary.month,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
employee_id: summary.employee_id,
|
||||
month: summary.month,
|
||||
start_date: summary.start_date,
|
||||
end_date: summary.end_date,
|
||||
summary_day_count: summary.summary_day_count,
|
||||
checkin_day_count: summary.checkin_day_count,
|
||||
exception_day_count: summary.exception_day_count,
|
||||
abnormal_record_count: summary.abnormal_record_count,
|
||||
missing_day_candidate_count: summary.missing_day_candidate_count,
|
||||
first_checkin_at: summary.first_checkin_at,
|
||||
last_checkin_at: summary.last_checkin_at,
|
||||
result_summary: summary.result_summary,
|
||||
raw_hash_digest: summary.raw_hash_digest,
|
||||
},
|
||||
update: {
|
||||
start_date: summary.start_date,
|
||||
end_date: summary.end_date,
|
||||
summary_day_count: summary.summary_day_count,
|
||||
checkin_day_count: summary.checkin_day_count,
|
||||
exception_day_count: summary.exception_day_count,
|
||||
abnormal_record_count: summary.abnormal_record_count,
|
||||
missing_day_candidate_count: summary.missing_day_candidate_count,
|
||||
first_checkin_at: summary.first_checkin_at,
|
||||
last_checkin_at: summary.last_checkin_at,
|
||||
result_summary: summary.result_summary,
|
||||
raw_hash_digest: summary.raw_hash_digest,
|
||||
archived_at: new Date(),
|
||||
},
|
||||
});
|
||||
count += 1;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
function summarizeRuleOption(rule) {
|
||||
const source = rule && typeof rule === "object" ? rule : {};
|
||||
const ruleKeys = Object.keys(source).filter((key) => !/loc|lat|lng|face|device|mac|wifi|payload/i.test(key)).sort();
|
||||
const group = source.group || source.groupname || source.group_name || {};
|
||||
const checkinDate = source.checkin_date || source.date || source.datetime || null;
|
||||
return {
|
||||
keyCount: ruleKeys.length,
|
||||
keySummary: ruleKeys.slice(0, 20),
|
||||
hasGroup: Boolean(group && Object.keys(group).length),
|
||||
hasScheduleList: Array.isArray(source.checkin_time) || Array.isArray(source.checkinTime) || Array.isArray(source.sch_checkin_time),
|
||||
hasSpecialDays: Boolean(source.spe_workdays || source.spe_offdays || source.speWorkdays || source.speOffdays),
|
||||
datePresent: Boolean(checkinDate),
|
||||
};
|
||||
}
|
||||
|
||||
function extractRuleUserId(rule) {
|
||||
return toSafeString(rule?.userid ?? rule?.UserId ?? rule?.user_id ?? rule?.open_userid, 128);
|
||||
}
|
||||
|
||||
function buildRuleSnapshots({ rules, employeeByExternalUserId, snapshotDate }) {
|
||||
const dateValue = toDateOnlyUtc(snapshotDate || new Date());
|
||||
return (rules || []).map((rule) => {
|
||||
const externalUserId = extractRuleUserId(rule);
|
||||
const employee = employeeByExternalUserId?.get(externalUserId);
|
||||
const ruleSummary = summarizeRuleOption(rule);
|
||||
return {
|
||||
employee_id: employee?.id || null,
|
||||
external_user_id_prefix: maskExternalUserId(externalUserId),
|
||||
snapshot_date: dateValue,
|
||||
rule_hash: digestObject(ruleSummary),
|
||||
rule_summary: ruleSummary,
|
||||
};
|
||||
}).filter((item) => item.employee_id && item.snapshot_date && item.rule_hash);
|
||||
}
|
||||
|
||||
async function upsertRuleSnapshots(prisma, snapshots) {
|
||||
let count = 0;
|
||||
for (const snapshot of snapshots || []) {
|
||||
await prisma.wecomCheckinRuleSnapshot.upsert({
|
||||
where: {
|
||||
employee_id_snapshot_date_rule_hash: {
|
||||
employee_id: snapshot.employee_id,
|
||||
snapshot_date: snapshot.snapshot_date,
|
||||
rule_hash: snapshot.rule_hash,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
employee_id: snapshot.employee_id,
|
||||
external_user_id_prefix: snapshot.external_user_id_prefix,
|
||||
snapshot_date: snapshot.snapshot_date,
|
||||
rule_hash: snapshot.rule_hash,
|
||||
rule_summary: snapshot.rule_summary,
|
||||
},
|
||||
update: {
|
||||
external_user_id_prefix: snapshot.external_user_id_prefix,
|
||||
rule_summary: snapshot.rule_summary,
|
||||
archived_at: new Date(),
|
||||
},
|
||||
});
|
||||
count += 1;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
async function archiveCheckinSummaries({ prisma, records, ruleSnapshots = [] }) {
|
||||
const dailySummaries = buildDailySummaries(records);
|
||||
const monthlySummaries = buildMonthlySummaries(dailySummaries);
|
||||
const dailyCount = await upsertDailySummaries(prisma, dailySummaries);
|
||||
const monthlyCount = await upsertMonthlySummaries(prisma, monthlySummaries);
|
||||
const ruleSnapshotCount = await upsertRuleSnapshots(prisma, ruleSnapshots);
|
||||
return {
|
||||
dailyCount,
|
||||
monthlyCount,
|
||||
ruleSnapshotCount,
|
||||
dailySummaries,
|
||||
monthlySummaries,
|
||||
};
|
||||
}
|
||||
|
||||
async function readCheckinArchiveSnapshot(prisma, { startDate, endDate, employeeIds = [], limit = 50 } = {}) {
|
||||
const where = {};
|
||||
if (startDate || endDate) {
|
||||
where.checkin_date = {
|
||||
...(startDate ? { gte: toDateOnlyUtc(startDate) } : {}),
|
||||
...(endDate ? { lte: toDateOnlyUtc(endDate) } : {}),
|
||||
};
|
||||
}
|
||||
if (employeeIds.length) {
|
||||
where.employee_id = { in: employeeIds };
|
||||
}
|
||||
const [daily, monthly, rules] = await Promise.all([
|
||||
prisma.wecomCheckinDailySummary.findMany({
|
||||
where,
|
||||
orderBy: [{ checkin_date: "desc" }, { updated_at: "desc" }],
|
||||
take: limit,
|
||||
}),
|
||||
prisma.wecomCheckinMonthlySummary.findMany({
|
||||
where: employeeIds.length ? { employee_id: { in: employeeIds } } : {},
|
||||
orderBy: [{ month: "desc" }, { updated_at: "desc" }],
|
||||
take: limit,
|
||||
}),
|
||||
prisma.wecomCheckinRuleSnapshot.findMany({
|
||||
where: employeeIds.length ? { employee_id: { in: employeeIds } } : {},
|
||||
orderBy: [{ snapshot_date: "desc" }, { updated_at: "desc" }],
|
||||
take: limit,
|
||||
}),
|
||||
]);
|
||||
return { daily, monthly, rules };
|
||||
}
|
||||
|
||||
function sanitizeDailySummary(row) {
|
||||
return {
|
||||
employeeId: row.employee_id,
|
||||
userIdPrefix: row.external_user_id_prefix,
|
||||
checkinDate: formatDateOnly(row.checkin_date),
|
||||
recordCount: row.record_count,
|
||||
firstCheckinAt: row.first_checkin_at ? new Date(row.first_checkin_at).toISOString() : null,
|
||||
lastCheckinAt: row.last_checkin_at ? new Date(row.last_checkin_at).toISOString() : null,
|
||||
exceptionCount: row.exception_count,
|
||||
abnormalCount: row.abnormal_count,
|
||||
resultSummary: row.result_summary || {},
|
||||
rawHashDigestPrefix: row.raw_hash_digest ? `${row.raw_hash_digest.slice(0, 12)}...` : null,
|
||||
};
|
||||
}
|
||||
|
||||
function sanitizeMonthlySummary(row) {
|
||||
return {
|
||||
employeeId: row.employee_id,
|
||||
month: row.month,
|
||||
summaryDayCount: row.summary_day_count,
|
||||
checkinDayCount: row.checkin_day_count,
|
||||
exceptionDayCount: row.exception_day_count,
|
||||
abnormalRecordCount: row.abnormal_record_count,
|
||||
missingDayCandidateCount: row.missing_day_candidate_count,
|
||||
firstCheckinAt: row.first_checkin_at ? new Date(row.first_checkin_at).toISOString() : null,
|
||||
lastCheckinAt: row.last_checkin_at ? new Date(row.last_checkin_at).toISOString() : null,
|
||||
};
|
||||
}
|
||||
|
||||
function sanitizeRuleSnapshot(row) {
|
||||
return {
|
||||
employeeId: row.employee_id,
|
||||
userIdPrefix: row.external_user_id_prefix,
|
||||
snapshotDate: formatDateOnly(row.snapshot_date),
|
||||
ruleHashPrefix: row.rule_hash ? `${row.rule_hash.slice(0, 12)}...` : null,
|
||||
ruleSummary: row.rule_summary || {},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
archiveCheckinSummaries,
|
||||
buildDailySummaries,
|
||||
buildMonthlySummaries,
|
||||
buildRuleSnapshots,
|
||||
digestObject,
|
||||
maskExternalUserId,
|
||||
readCheckinArchiveSnapshot,
|
||||
sanitizeDailySummary,
|
||||
sanitizeMonthlySummary,
|
||||
sanitizeRuleSnapshot,
|
||||
summarizeRuleOption,
|
||||
upsertDailySummaries,
|
||||
upsertMonthlySummaries,
|
||||
upsertRuleSnapshots,
|
||||
};
|
||||
在新工单中引用
屏蔽一个用户