文件
hyb-oa/server/src/routes/me.js
T

146 行
5.0 KiB
JavaScript

const { z } = require("zod");
const { buildBusinessValidationError } = require("../utils/businessValidationError");
const { AppError } = require("../utils/AppError");
const { buildAuditLogData } = require("../utils/audit");
const { sendData } = require("../utils/reply");
const { decryptStringOrThrow, maskBankAccount } = require("../services/crypto");
const NOTIFICATION_PREFERENCES_SCHEMA = z
.object({
notificationPushEnabled: z.boolean(),
})
.strict();
function parseNotificationPreferences(body) {
const parsed = NOTIFICATION_PREFERENCES_SCHEMA.safeParse(body ?? {});
if (!parsed.success) {
throw buildBusinessValidationError("profile.notification_preferences_invalid", parsed.error.flatten());
}
return parsed.data;
}
function buildProfileCompleteness(employee) {
const hasEmergencyContact = Boolean(employee?.emergency_contact_name && employee?.emergency_contact_phone);
const hasBankAccount = Boolean(
employee?.bank_account_encrypted && (employee?.bank_institution || employee?.bank_name),
);
const hasAvatar = Boolean(employee?.avatar_path);
const hasIdNumber = Boolean(employee?.id_number_encrypted);
const completedCount = [hasEmergencyContact, hasBankAccount, hasAvatar, hasIdNumber].filter(Boolean).length;
return {
hasEmergencyContact,
hasBankAccount,
hasAvatar,
hasIdNumber,
score: completedCount * 25,
};
}
async function meRoutes(fastify) {
fastify.get(
"/me",
{
preHandler: fastify.authenticate,
},
async (request, reply) => {
const employee = await fastify.prisma.employee.findUnique({
where: { id: request.employee.id },
select: {
id: true,
employee_no: true,
name: true,
phone: true,
email: true,
department_id: true,
direct_manager_id: true,
status: true,
emergency_contact_name: true,
emergency_contact_phone: true,
bank_account_encrypted: true,
bank_name: true,
bank_institution: { select: { id: true, code: true, name: true } },
avatar_path: true,
id_number_encrypted: true,
notification_push_enabled: true,
roles: { select: { role_code: true } },
wecom_identity_binding: { select: { external_user_id: true, created_at: true } },
},
});
const roleCodes = (employee?.roles ?? []).map((r) => r.role_code);
const profileCompleteness = buildProfileCompleteness(employee);
let bankAccountMasked = null;
try {
const bankAccount = employee?.bank_account_encrypted
? decryptStringOrThrow(fastify.config, employee.bank_account_encrypted)
: null;
bankAccountMasked = bankAccount ? maskBankAccount(bankAccount) : null;
} catch (error) {
fastify.log.error({ err: error, requestId: request.id }, "FIELD_ENCRYPTION_KEY misconfigured");
throw new AppError({ code: "SYS_001", message: "服务器内部错误", statusCode: 500 });
}
return sendData(reply, request, {
id: employee?.id ?? request.employee.id,
employeeNo: employee?.employee_no ?? "",
name: employee?.name ?? "",
phone: employee?.phone ?? "",
email: employee?.email ?? "",
departmentId: employee?.department_id ?? "",
directManagerId: employee?.direct_manager_id ?? null,
status: employee?.status ?? "active",
notificationPushEnabled: employee?.notification_push_enabled ?? true,
roleCodes,
bankAccount: {
bankName: employee?.bank_institution?.name ?? employee?.bank_name ?? null,
bankInstitutionCode: employee?.bank_institution?.code ?? null,
accountNumberMasked: bankAccountMasked,
},
profileCompleteness,
wecomBinding: employee?.wecom_identity_binding ? { externalUserId: employee.wecom_identity_binding.external_user_id, createdAt: employee.wecom_identity_binding.created_at?.toISOString?.() ?? null } : null,
});
},
);
fastify.patch(
"/me/notification-preferences",
{
preHandler: fastify.authenticate,
},
async (request, reply) => {
const body = parseNotificationPreferences(request.body);
const employee = await fastify.prisma.employee.update({
where: { id: request.employee.id },
data: {
notification_push_enabled: body.notificationPushEnabled,
updated_by: request.employee.id,
},
select: {
id: true,
notification_push_enabled: true,
},
});
await fastify.prisma.auditLog.create({
data: buildAuditLogData(request, {
actorId: request.employee.id,
action: "EMPLOYEE_NOTIFICATION_PREFERENCE_UPDATED",
targetType: "employee",
targetId: employee.id,
extra: {
notificationPushEnabled: employee.notification_push_enabled,
},
}),
});
return sendData(reply, request, {
notificationPushEnabled: employee.notification_push_enabled,
});
},
);
}
module.exports = meRoutes;