146 行
5.0 KiB
JavaScript
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;
|