const { z } = require("zod"); const { AppError } = require("../utils/AppError"); const { sendData } = require("../utils/reply"); const QUERY_SCHEMA = z.object({ page: z.coerce.number().int().min(1).optional(), pageSize: z.coerce.number().int().min(1).max(100).optional(), actorId: z.string().uuid().optional(), action: z.string().min(1).max(100).optional(), targetType: z.string().min(1).max(100).optional(), dateFrom: z.string().datetime().optional(), dateTo: z.string().datetime().optional(), }); function parseQuery(query) { const parsed = QUERY_SCHEMA.safeParse(query ?? {}); if (!parsed.success) { throw new AppError({ code: "VALID_001", message: "参数校验失败", statusCode: 400, details: parsed.error.flatten(), }); } const page = parsed.data.page ?? 1; const pageSize = parsed.data.pageSize ?? 20; const dateFrom = parsed.data.dateFrom ? new Date(parsed.data.dateFrom) : null; const dateTo = parsed.data.dateTo ? new Date(parsed.data.dateTo) : null; if (dateFrom && dateTo && dateFrom > dateTo) { throw new AppError({ code: "VALID_001", message: "参数校验失败", statusCode: 400, details: { dateTo: "must_be_after_dateFrom" }, }); } return { page, pageSize, actorId: parsed.data.actorId, action: parsed.data.action, targetType: parsed.data.targetType, dateFrom, dateTo, }; } async function auditRoutes(fastify) { fastify.get( "/audit-logs", { preHandler: fastify.requirePermission("system.audit.view"), }, async (request, reply) => { const query = parseQuery(request.query); const skip = (query.page - 1) * query.pageSize; const where = { ...(query.actorId ? { actor_id: query.actorId } : {}), ...(query.action ? { action: query.action } : {}), ...(query.targetType ? { target_type: query.targetType } : {}), ...(query.dateFrom || query.dateTo ? { created_at: { ...(query.dateFrom ? { gte: query.dateFrom } : {}), ...(query.dateTo ? { lte: query.dateTo } : {}), }, } : {}), }; const [items, total] = await Promise.all([ fastify.prisma.auditLog.findMany({ where, orderBy: { created_at: "desc" }, skip, take: query.pageSize, select: { id: true, request_id: true, actor_id: true, action: true, target_type: true, target_id: true, ip_address: true, user_agent: true, extra: true, created_at: true, actor: { select: { id: true, name: true, email: true, }, }, }, }), fastify.prisma.auditLog.count({ where }), ]); return sendData(reply, request, { page: query.page, pageSize: query.pageSize, total, items: items.map((item) => ({ id: item.id, requestId: item.request_id, actorId: item.actor_id, actorName: item.actor?.name ?? "", actorEmail: item.actor?.email ?? "", action: item.action, targetType: item.target_type, targetId: item.target_id, ipAddress: item.ip_address, userAgent: item.user_agent, extra: item.extra, createdAt: item.created_at.toISOString(), })), }); }, ); } module.exports = auditRoutes;