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:
2026-02-01 21:04:21 +08:00
parent aaa29ea9d3
commit 4c2c9b437b
9 changed files with 795 additions and 9 deletions

View File

@@ -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天前的数据
* 用于定时任务调用

View File

@@ -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

View File

@@ -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