feat(admin): Add activity logs page and fix AI chat markdown rendering
- Add paginated activity logs API with filters (date, module, action, keyword) - Add ActivityLogsPage with table, filters, and detail modal - Add markdown rendering support for AI chat messages - Remove prototype placeholder content from chat sidebar Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -290,6 +290,113 @@ export const activityService = {
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 分页查询运营日志(支持筛选)
|
||||
*
|
||||
* @param options 查询选项
|
||||
*/
|
||||
async getActivityLogs(options: {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
module?: string;
|
||||
action?: string;
|
||||
keyword?: string;
|
||||
}): Promise<{
|
||||
data: Array<{
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
tenantId: string;
|
||||
tenantName: string | null;
|
||||
userId: string;
|
||||
userName: string | null;
|
||||
module: string;
|
||||
feature: string;
|
||||
action: string;
|
||||
info: string | null;
|
||||
}>;
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}> {
|
||||
const { page, pageSize, startDate, endDate, module, action, keyword } = options;
|
||||
|
||||
// 构建 where 条件
|
||||
const where: any = {};
|
||||
|
||||
// 日期范围筛选
|
||||
if (startDate || endDate) {
|
||||
where.created_at = {};
|
||||
if (startDate) where.created_at.gte = startDate;
|
||||
if (endDate) {
|
||||
// endDate 设置到当天结束
|
||||
const endOfDay = new Date(endDate);
|
||||
endOfDay.setHours(23, 59, 59, 999);
|
||||
where.created_at.lte = endOfDay;
|
||||
}
|
||||
}
|
||||
|
||||
// 模块筛选
|
||||
if (module) {
|
||||
where.module = module;
|
||||
}
|
||||
|
||||
// 动作筛选
|
||||
if (action) {
|
||||
where.action = action;
|
||||
}
|
||||
|
||||
// 关键词搜索(用户名或租户名)
|
||||
if (keyword) {
|
||||
where.OR = [
|
||||
{ user_name: { contains: keyword, mode: 'insensitive' } },
|
||||
{ tenant_name: { contains: keyword, mode: 'insensitive' } },
|
||||
];
|
||||
}
|
||||
|
||||
// 并行查询数据和总数
|
||||
const [logs, total] = await Promise.all([
|
||||
prisma.simple_logs.findMany({
|
||||
where,
|
||||
orderBy: { created_at: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
select: {
|
||||
id: true,
|
||||
created_at: true,
|
||||
tenant_id: true,
|
||||
tenant_name: true,
|
||||
user_id: true,
|
||||
user_name: true,
|
||||
module: true,
|
||||
feature: true,
|
||||
action: true,
|
||||
info: true,
|
||||
}
|
||||
}),
|
||||
prisma.simple_logs.count({ where }),
|
||||
]);
|
||||
|
||||
return {
|
||||
data: logs.map(log => ({
|
||||
id: log.id,
|
||||
createdAt: log.created_at,
|
||||
tenantId: log.tenant_id,
|
||||
tenantName: log.tenant_name,
|
||||
userId: log.user_id,
|
||||
userName: log.user_name,
|
||||
module: log.module,
|
||||
feature: log.feature,
|
||||
action: log.action,
|
||||
info: log.info,
|
||||
})),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 清理过期日志(180天前的数据)
|
||||
* 用于定时任务调用
|
||||
|
||||
@@ -98,6 +98,81 @@ export async function getUserOverview(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询运营日志
|
||||
* GET /api/admin/stats/logs
|
||||
*
|
||||
* 查询参数:
|
||||
* - page: 页码 (默认1)
|
||||
* - pageSize: 每页条数 (默认20, 最大100)
|
||||
* - startDate: 开始日期 (YYYY-MM-DD)
|
||||
* - endDate: 结束日期 (YYYY-MM-DD)
|
||||
* - module: 模块筛选
|
||||
* - action: 动作筛选
|
||||
* - keyword: 关键词搜索 (用户名/租户名)
|
||||
*/
|
||||
export async function getActivityLogs(
|
||||
request: FastifyRequest<{
|
||||
Querystring: {
|
||||
page?: string;
|
||||
pageSize?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
module?: string;
|
||||
action?: string;
|
||||
keyword?: string;
|
||||
}
|
||||
}>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const {
|
||||
page: pageStr,
|
||||
pageSize: pageSizeStr,
|
||||
startDate: startDateStr,
|
||||
endDate: endDateStr,
|
||||
module,
|
||||
action,
|
||||
keyword,
|
||||
} = request.query;
|
||||
|
||||
// 解析分页参数
|
||||
const page = Math.max(1, Number(pageStr) || 1);
|
||||
const pageSize = Math.min(100, Math.max(1, Number(pageSizeStr) || 20));
|
||||
|
||||
// 解析日期参数
|
||||
const startDate = startDateStr ? new Date(startDateStr) : undefined;
|
||||
const endDate = endDateStr ? new Date(endDateStr) : undefined;
|
||||
|
||||
const result = await activityService.getActivityLogs({
|
||||
page,
|
||||
pageSize,
|
||||
startDate,
|
||||
endDate,
|
||||
module,
|
||||
action,
|
||||
keyword,
|
||||
});
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: {
|
||||
page: result.page,
|
||||
pageSize: result.pageSize,
|
||||
total: result.total,
|
||||
totalPages: Math.ceil(result.total / result.pageSize),
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error('[StatsController] 获取运营日志失败', { error: error.message });
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
message: error.message || '获取运营日志失败',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期日志(管理接口)
|
||||
* POST /api/admin/stats/cleanup
|
||||
|
||||
@@ -36,6 +36,17 @@ export async function statsRoutes(fastify: FastifyInstance) {
|
||||
handler: statsController.getLiveFeed,
|
||||
});
|
||||
|
||||
/**
|
||||
* 分页查询运营日志
|
||||
* GET /api/admin/stats/logs
|
||||
*
|
||||
* 权限: SUPER_ADMIN, PROMPT_ENGINEER
|
||||
*/
|
||||
fastify.get('/logs', {
|
||||
preHandler: [authenticate, requirePermission('tenant:view')],
|
||||
handler: statsController.getActivityLogs,
|
||||
});
|
||||
|
||||
/**
|
||||
* 清理过期日志
|
||||
* POST /api/admin/stats/cleanup
|
||||
|
||||
Reference in New Issue
Block a user