From b26fe1e117a1b598c013994816cd99f005ca170c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BE=9F=E7=94=B7=E6=97=A5=E8=AE=B0=5Cwww?= Date: Thu, 12 Feb 2026 02:50:18 +0800 Subject: [PATCH] =?UTF-8?q?=E6=97=A5=E5=BF=97=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(payload)/admin/importMap.js | 2 + src/app/api/delete-logs/route.ts | 91 +++++ src/collections/Announcements.ts | 3 + src/collections/Articles.ts | 3 + src/collections/Logs.ts | 104 +++++ src/collections/Media.ts | 5 + src/collections/Products.ts | 5 + src/components/views/LogsManagerView.tsx | 500 +++++++++++++++++++++++ src/globals/LogsManager.ts | 39 ++ src/hooks/logAction.ts | 107 +++++ src/payload-types.ts | 97 +++++ src/payload.config.ts | 6 +- 12 files changed, 960 insertions(+), 2 deletions(-) create mode 100644 src/app/api/delete-logs/route.ts create mode 100644 src/collections/Logs.ts create mode 100644 src/components/views/LogsManagerView.tsx create mode 100644 src/globals/LogsManager.ts create mode 100644 src/hooks/logAction.ts diff --git a/src/app/(payload)/admin/importMap.js b/src/app/(payload)/admin/importMap.js index 54344ae..ef9c8fe 100644 --- a/src/app/(payload)/admin/importMap.js +++ b/src/app/(payload)/admin/importMap.js @@ -29,6 +29,7 @@ import { SyncMedusaButton as SyncMedusaButton_31e6578e170fdd0bad7013c8202d6e08 } import { default as default_c2e3814fe427263135b1f5931c37f6f2 } from '../../../components/list/ProductGridStyler' import { ForceSyncButton as ForceSyncButton_28396efe36d6238add95cf44109e281c } from '../../../components/sync/ForceSyncButton' import { default as default_767734c8b7b095ea28d54c32abcf46e4 } from '../../../components/views/AdminPanel' +import { default as default_a766ef013722c08f9bb937940272cb5f } from '../../../components/views/LogsManagerView' import { S3ClientUploadHandler as S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24 } from '@payloadcms/storage-s3/client' import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc' @@ -64,6 +65,7 @@ export const importMap = { "/components/list/ProductGridStyler#default": default_c2e3814fe427263135b1f5931c37f6f2, "/components/sync/ForceSyncButton#ForceSyncButton": ForceSyncButton_28396efe36d6238add95cf44109e281c, "/components/views/AdminPanel#default": default_767734c8b7b095ea28d54c32abcf46e4, + "/components/views/LogsManagerView#default": default_a766ef013722c08f9bb937940272cb5f, "@payloadcms/storage-s3/client#S3ClientUploadHandler": S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24, "@payloadcms/next/rsc#CollectionCards": CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } diff --git a/src/app/api/delete-logs/route.ts b/src/app/api/delete-logs/route.ts new file mode 100644 index 0000000..68dc5e4 --- /dev/null +++ b/src/app/api/delete-logs/route.ts @@ -0,0 +1,91 @@ +import { getPayload } from 'payload' +import config from '@payload-config' +import { NextRequest } from 'next/server' + +/** + * 批量删除日志 API + * DELETE /api/delete-logs?startDate=YYYY-MM-DD&endDate=YYYY-MM-DD + */ +export async function DELETE(req: NextRequest) { + try { + const payload = await getPayload({ config }) + + // 检查用户权限 + const { user } = await payload.auth({ headers: req.headers }) + if (!user || !user.roles?.includes('admin')) { + return Response.json({ error: '需要管理员权限' }, { status: 403 }) + } + + // 获取日期参数 + const { searchParams } = new URL(req.url) + const startDate = searchParams.get('startDate') + const endDate = searchParams.get('endDate') + + if (!startDate || !endDate) { + return Response.json({ error: '请提供开始和结束日期' }, { status: 400 }) + } + + // 验证日期格式 + const start = new Date(startDate) + const end = new Date(endDate) + + if (isNaN(start.getTime()) || isNaN(end.getTime())) { + return Response.json({ error: '无效的日期格式' }, { status: 400 }) + } + + // 设置时间范围 + start.setHours(0, 0, 0, 0) + end.setHours(23, 59, 59, 999) + + // 查询要删除的日志 + const logsToDelete = await payload.find({ + collection: 'logs', + where: { + and: [ + { + createdAt: { + greater_than_equal: start.toISOString(), + }, + }, + { + createdAt: { + less_than_equal: end.toISOString(), + }, + }, + ], + }, + limit: 10000, // 限制一次删除数量 + }) + + // 批量删除 + let deletedCount = 0 + for (const log of logsToDelete.docs) { + try { + await payload.delete({ + collection: 'logs', + id: log.id, + context: { skipHooks: true }, // 跳过钩子 + }) + deletedCount++ + } catch (error) { + console.error(`Failed to delete log ${log.id}:`, error) + } + } + + return Response.json({ + success: true, + message: `成功删除 ${deletedCount} 条日志记录`, + deletedCount, + totalFound: logsToDelete.totalDocs, + }) + } catch (error) { + console.error('Delete logs error:', error) + return Response.json( + { + error: '删除失败', + message: error instanceof Error ? error.message : '未知错误', + }, + { status: 500 }, + ) + } +} diff --git a/src/collections/Announcements.ts b/src/collections/Announcements.ts index 776dbdd..8e20f46 100644 --- a/src/collections/Announcements.ts +++ b/src/collections/Announcements.ts @@ -1,4 +1,5 @@ import type { CollectionConfig } from 'payload' +import { logAfterChange, logAfterDelete } from '../hooks/logAction' import { BlocksFeature, BoldFeature, @@ -220,5 +221,7 @@ export const Announcements: CollectionConfig = { return data }, ], + afterChange: [logAfterChange], + afterDelete: [logAfterDelete], }, } diff --git a/src/collections/Articles.ts b/src/collections/Articles.ts index 3323b22..2e11987 100644 --- a/src/collections/Articles.ts +++ b/src/collections/Articles.ts @@ -1,4 +1,5 @@ import type { CollectionConfig } from 'payload' +import { logAfterChange, logAfterDelete } from '../hooks/logAction' import { BlocksFeature, BoldFeature, @@ -330,5 +331,7 @@ export const Articles: CollectionConfig = { return data }, ], + afterChange: [logAfterChange], + afterDelete: [logAfterDelete], }, } diff --git a/src/collections/Logs.ts b/src/collections/Logs.ts new file mode 100644 index 0000000..ad77350 --- /dev/null +++ b/src/collections/Logs.ts @@ -0,0 +1,104 @@ +import type { CollectionConfig } from 'payload' + +export const Logs: CollectionConfig = { + slug: 'logs', + admin: { + useAsTitle: 'action', + defaultColumns: ['action', 'collection', 'user', 'createdAt'], + description: '系统操作日志记录', + group: '系统', + pagination: { + defaultLimit: 50, + }, + listSearchableFields: ['action', 'collection', 'documentId'], + }, + access: { + // 只有 admin 和 editor 可以查看日志 + read: ({ req: { user } }) => { + if (!user) return false + return user.roles?.includes('admin') || user.roles?.includes('editor') || false + }, + // 禁止手动创建、更新日志(只能通过系统钩子自动创建) + create: () => false, + update: () => false, + // 只有 admin 可以删除 + delete: ({ req: { user } }) => { + if (!user) return false + return user.roles?.includes('admin') || false + }, + }, + fields: [ + { + name: 'action', + type: 'select', + required: true, + options: [ + { label: '创建', value: 'create' }, + { label: '更新', value: 'update' }, + { label: '删除', value: 'delete' }, + { label: '同步', value: 'sync' }, + { label: '登录', value: 'login' }, + { label: '登出', value: 'logout' }, + ], + admin: { + description: '操作类型', + }, + }, + { + name: 'collection', + type: 'text', + required: true, + index: true, + admin: { + description: '操作的集合名称', + }, + }, + { + name: 'documentId', + type: 'text', + index: true, + admin: { + description: '文档 ID', + }, + }, + { + name: 'documentTitle', + type: 'text', + admin: { + description: '文档标题', + }, + }, + { + name: 'user', + type: 'relationship', + relationTo: 'users', + required: true, + index: true, + admin: { + description: '操作用户', + }, + }, + { + name: 'changes', + type: 'json', + admin: { + description: '变更内容(JSON 格式)', + }, + }, + { + name: 'ip', + type: 'text', + admin: { + description: 'IP 地址', + }, + }, + { + name: 'userAgent', + type: 'text', + admin: { + description: '浏览器信息', + }, + }, + ], + timestamps: true, +} diff --git a/src/collections/Media.ts b/src/collections/Media.ts index 568cf42..f3f4bbb 100644 --- a/src/collections/Media.ts +++ b/src/collections/Media.ts @@ -1,4 +1,5 @@ import type { CollectionConfig } from 'payload' +import { logAfterChange, logAfterDelete } from '../hooks/logAction' export const Media: CollectionConfig = { slug: 'media', @@ -12,5 +13,9 @@ export const Media: CollectionConfig = { required: true, }, ], + hooks: { + afterChange: [logAfterChange], + afterDelete: [logAfterDelete], + }, upload: true, } diff --git a/src/collections/Products.ts b/src/collections/Products.ts index 11d07e1..41f04a7 100644 --- a/src/collections/Products.ts +++ b/src/collections/Products.ts @@ -1,4 +1,5 @@ import type { CollectionConfig } from 'payload' +import { logAfterChange, logAfterDelete } from '../hooks/logAction' import { AlignFeature, BlocksFeature, @@ -200,5 +201,9 @@ export const Products: CollectionConfig = { ], }, ], + hooks: { + afterChange: [logAfterChange], + afterDelete: [logAfterDelete], + }, timestamps: true, } diff --git a/src/components/views/LogsManagerView.tsx b/src/components/views/LogsManagerView.tsx new file mode 100644 index 0000000..21c8404 --- /dev/null +++ b/src/components/views/LogsManagerView.tsx @@ -0,0 +1,500 @@ +'use client' + +import React, { useState, useEffect } from 'react' +import { Button, useAuth } from '@payloadcms/ui' + +interface Log { + id: string + action: string + collection: string + documentId?: string + documentTitle?: string + user: { + id: string + email: string + } + createdAt: string + ip?: string +} + +export default function LogsManagerView() { + const { user } = useAuth() + const [logs, setLogs] = useState([]) + const [loading, setLoading] = useState(false) + const [totalDocs, setTotalDocs] = useState(0) + const [page, setPage] = useState(1) + const [limit] = useState(50) + + // 筛选条件 + const [days, setDays] = useState(7) // 默认显示最近7天 + const [selectedCollection, setSelectedCollection] = useState('all') + const [selectedAction, setSelectedAction] = useState('all') + + // 删除相关 + const [deleteStartDate, setDeleteStartDate] = useState('') + const [deleteEndDate, setDeleteEndDate] = useState('') + const [deleteLoading, setDeleteLoading] = useState(false) + const [deleteMessage, setDeleteMessage] = useState('') + + const isAdmin = user?.roles?.includes('admin') + + // 加载日志 + const loadLogs = async () => { + setLoading(true) + try { + // 计算日期范围 + const endDate = new Date() + const startDate = new Date() + startDate.setDate(startDate.getDate() - days) + + // 构建查询条件 + const whereConditions: any[] = [ + { + createdAt: { + greater_than_equal: startDate.toISOString(), + }, + }, + { + createdAt: { + less_than_equal: endDate.toISOString(), + }, + }, + ] + + if (selectedCollection !== 'all') { + whereConditions.push({ + collection: { + equals: selectedCollection, + }, + }) + } + + if (selectedAction !== 'all') { + whereConditions.push({ + action: { + equals: selectedAction, + }, + }) + } + + const query = new URLSearchParams({ + depth: '1', + limit: limit.toString(), + page: page.toString(), + sort: '-createdAt', + where: JSON.stringify({ + and: whereConditions, + }), + }) + + const response = await fetch(`/admin/api/logs?${query}`) + const data = await response.json() + + setLogs(data.docs || []) + setTotalDocs(data.totalDocs || 0) + } catch (error) { + console.error('Failed to load logs:', error) + } finally { + setLoading(false) + } + } + + // 删除指定日期范围的日志 + const handleDeleteLogs = async () => { + if (!deleteStartDate || !deleteEndDate) { + setDeleteMessage('请选择开始和结束日期') + return + } + + if (!confirm(`确定删除 ${deleteStartDate} 至 ${deleteEndDate} 的所有日志吗?此操作不可撤销!`)) { + return + } + + setDeleteLoading(true) + setDeleteMessage('') + + try { + const response = await fetch( + `/api/delete-logs?startDate=${deleteStartDate}&endDate=${deleteEndDate}`, + { + method: 'DELETE', + }, + ) + + const data = await response.json() + + if (data.success) { + setDeleteMessage(data.message) + // 重新加载日志 + loadLogs() + // 清空日期选择 + setDeleteStartDate('') + setDeleteEndDate('') + } else { + setDeleteMessage(`删除失败: ${data.error}`) + } + } catch (error) { + setDeleteMessage(`删除出错: ${error instanceof Error ? error.message : '未知错误'}`) + } finally { + setDeleteLoading(false) + } + } + + // 快速删除按钮 + const handleQuickDelete = (daysAgo: number) => { + const end = new Date() + const start = new Date() + start.setDate(start.getDate() - daysAgo) + + setDeleteStartDate(start.toISOString().split('T')[0]) + setDeleteEndDate(end.toISOString().split('T')[0]) + } + + useEffect(() => { + loadLogs() + }, [days, selectedCollection, selectedAction, page]) + + const actionLabels: Record = { + create: '创建', + update: '更新', + delete: '删除', + sync: '同步', + login: '登录', + logout: '登出', + } + + const actionColors: Record = { + create: '#10b981', + update: '#3b82f6', + delete: '#ef4444', + sync: '#8b5cf6', + login: '#14b8a6', + logout: '#6b7280', + } + + return ( +
+

+ 📋 操作日志管理 +

+ + {/* 筛选区域 */} +
+

+ 🔍 筛选条件 +

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ +
+
+ + {/* 删除日志区域 (仅管理员) */} + {isAdmin && ( +
+

+ 🗑️ 删除日志(管理员) +

+ +
+

快速选择:

+
+ + + +
+
+ +
+
+ + setDeleteStartDate(e.target.value)} + style={{ + width: '100%', + padding: '0.5rem', + borderRadius: '4px', + border: '1px solid var(--theme-elevation-400)', + backgroundColor: 'var(--theme-elevation-0)', + }} + /> +
+ +
+ + setDeleteEndDate(e.target.value)} + style={{ + width: '100%', + padding: '0.5rem', + borderRadius: '4px', + border: '1px solid var(--theme-elevation-400)', + backgroundColor: 'var(--theme-elevation-0)', + }} + /> +
+ + +
+ + {deleteMessage && ( +
+ {deleteMessage} +
+ )} +
+ )} + + {/* 日志列表 */} +
+
+

+ 📊 日志记录(共 {totalDocs} 条) +

+
+ + {loading ? ( +
加载中...
+ ) : logs.length === 0 ? ( +
+ 没有找到日志记录 +
+ ) : ( + <> +
+ + + + + + + + + + + + + {logs.map((log) => ( + + + + + + + + + ))} + +
时间操作集合文档用户IP
+ {new Date(log.createdAt).toLocaleString('zh-CN')} + + + {actionLabels[log.action] || log.action} + + {log.collection} + {log.documentTitle || log.documentId || '-'} + + {typeof log.user === 'object' ? log.user.email : log.user} + {log.ip || '-'}
+
+ + {/* 分页 */} + {totalDocs > limit && ( +
+ + + 第 {page} / {Math.ceil(totalDocs / limit)} 页 + + +
+ )} + + )} +
+
+ ) +} diff --git a/src/globals/LogsManager.ts b/src/globals/LogsManager.ts new file mode 100644 index 0000000..faeea3c --- /dev/null +++ b/src/globals/LogsManager.ts @@ -0,0 +1,39 @@ +import type { GlobalConfig } from 'payload' + +export const LogsManager: GlobalConfig = { + slug: 'logs-manager', + access: { + read: ({ req: { user } }) => { + // admin 和 editor 可以访问 + if (!user) return false + return user.roles?.includes('admin') || user.roles?.includes('editor') || false + }, + update: ({ req: { user } }) => { + // 只有 admin 可以删除日志 + if (!user) return false + return user.roles?.includes('admin') || false + }, + }, + admin: { + group: '系统', + description: '日志查看和管理工具', + components: { + views: { + edit: { + default: { + Component: '/components/views/LogsManagerView', + }, + }, + }, + }, + }, + fields: [ + { + name: 'placeholder', + type: 'text', + admin: { + hidden: true, + }, + }, + ], +} diff --git a/src/hooks/logAction.ts b/src/hooks/logAction.ts new file mode 100644 index 0000000..2e68608 --- /dev/null +++ b/src/hooks/logAction.ts @@ -0,0 +1,107 @@ +import type { CollectionAfterChangeHook, CollectionAfterDeleteHook } from 'payload' + +/** + * 记录操作日志的通用函数 + */ +export async function logAction({ + req, + action, + collection, + documentId, + documentTitle, + changes, +}: { + req: any + action: 'create' | 'update' | 'delete' | 'sync' | 'login' | 'logout' + collection: string + documentId?: string + documentTitle?: string + changes?: any +}) { + try { + const { payload, user } = req + + if (!user) return // 无用户信息则不记录 + + // 获取 IP 地址 + const ip = + req.headers?.['x-forwarded-for'] || + req.headers?.['x-real-ip'] || + req.ip || + req.connection?.remoteAddress || + 'unknown' + + // 获取 User Agent + const userAgent = req.headers?.['user-agent'] || 'unknown' + + // 创建日志记录 + await payload.create({ + collection: 'logs', + data: { + action, + collection, + documentId: documentId?.toString(), + documentTitle, + user: user.id, + changes, + ip, + userAgent, + }, + // 不触发 hooks,避免递归 + context: { skipHooks: true }, + }) + } catch (error) { + // 静默失败,避免影响主要操作 + console.error('[Log Hook Error]:', error) + } +} + +/** + * afterChange 钩子:记录创建和更新操作 + */ +export const logAfterChange: CollectionAfterChangeHook = async ({ + doc, + req, + operation, + collection, +}) => { + // 跳过日志自身的操作,避免递归 + if (req.context?.skipHooks) return doc + + const collectionSlug = collection?.slug as string + + // 不记录 logs 和 users-sessions 自身 + if (collectionSlug === 'logs' || collectionSlug === 'users-sessions') return doc + + await logAction({ + req, + action: operation === 'create' ? 'create' : 'update', + collection: collectionSlug, + documentId: doc.id, + documentTitle: doc.title || doc.name || doc.email || doc.alt || `ID: ${doc.id}`, + changes: operation === 'update' ? { updatedFields: Object.keys(doc) } : undefined, + }) + + return doc +} + +/** + * afterDelete 钩子:记录删除操作 + */ +export const logAfterDelete: CollectionAfterDeleteHook = async ({ doc, req, collection }) => { + if (req.context?.skipHooks) return doc + + const collectionSlug = collection?.slug as string + + if (collectionSlug === 'logs' || collectionSlug === 'users-sessions') return doc + + await logAction({ + req, + action: 'delete', + collection: collectionSlug, + documentId: doc.id, + documentTitle: doc.title || doc.name || doc.email || doc.alt || `ID: ${doc.id}`, + }) + + return doc +} diff --git a/src/payload-types.ts b/src/payload-types.ts index c490171..34060ca 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -72,6 +72,7 @@ export interface Config { products: Product; announcements: Announcement; articles: Article; + logs: Log; 'payload-kv': PayloadKv; 'payload-jobs': PayloadJob; 'payload-locked-documents': PayloadLockedDocument; @@ -85,6 +86,7 @@ export interface Config { products: ProductsSelect | ProductsSelect; announcements: AnnouncementsSelect | AnnouncementsSelect; articles: ArticlesSelect | ArticlesSelect; + logs: LogsSelect | LogsSelect; 'payload-kv': PayloadKvSelect | PayloadKvSelect; 'payload-jobs': PayloadJobsSelect | PayloadJobsSelect; 'payload-locked-documents': PayloadLockedDocumentsSelect | PayloadLockedDocumentsSelect; @@ -97,9 +99,11 @@ export interface Config { fallbackLocale: null; globals: { 'admin-settings': AdminSetting; + 'logs-manager': LogsManager; }; globalsSelect: { 'admin-settings': AdminSettingsSelect | AdminSettingsSelect; + 'logs-manager': LogsManagerSelect | LogsManagerSelect; }; locale: null; user: User; @@ -386,6 +390,57 @@ export interface Article { createdAt: string; _status?: ('draft' | 'published') | null; } +/** + * 系统操作日志记录 + * + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "logs". + */ +export interface Log { + id: number; + /** + * 操作类型 + */ + action: 'create' | 'update' | 'delete' | 'sync' | 'login' | 'logout'; + /** + * 操作的集合名称 + */ + collection: string; + /** + * 文档 ID + */ + documentId?: string | null; + /** + * 文档标题 + */ + documentTitle?: string | null; + /** + * 操作用户 + */ + user: number | User; + /** + * 变更内容(JSON 格式) + */ + changes?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; + /** + * IP 地址 + */ + ip?: string | null; + /** + * 浏览器信息 + */ + userAgent?: string | null; + updatedAt: string; + createdAt: string; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "payload-kv". @@ -521,6 +576,10 @@ export interface PayloadLockedDocument { | ({ relationTo: 'articles'; value: number | Article; + } | null) + | ({ + relationTo: 'logs'; + value: number | Log; } | null); globalSlug?: string | null; user: { @@ -664,6 +723,22 @@ export interface ArticlesSelect { createdAt?: T; _status?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "logs_select". + */ +export interface LogsSelect { + action?: T; + collection?: T; + documentId?: T; + documentTitle?: T; + user?: T; + changes?: T; + ip?: T; + userAgent?: T; + updatedAt?: T; + createdAt?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "payload-kv_select". @@ -747,6 +822,18 @@ export interface AdminSetting { updatedAt?: string | null; createdAt?: string | null; } +/** + * 日志查看和管理工具 + * + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "logs-manager". + */ +export interface LogsManager { + id: number; + placeholder?: string | null; + updatedAt?: string | null; + createdAt?: string | null; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "admin-settings_select". @@ -757,6 +844,16 @@ export interface AdminSettingsSelect { createdAt?: T; globalType?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "logs-manager_select". + */ +export interface LogsManagerSelect { + placeholder?: T; + updatedAt?: T; + createdAt?: T; + globalType?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "TaskSchedulePublish". diff --git a/src/payload.config.ts b/src/payload.config.ts index c5577ff..2c57bab 100644 --- a/src/payload.config.ts +++ b/src/payload.config.ts @@ -10,7 +10,9 @@ import { Media } from './collections/Media' import { Products } from './collections/Products' import { Announcements } from './collections/Announcements' import { Articles } from './collections/Articles' +import { Logs } from './collections/Logs' import { AdminSettings } from './globals/AdminSettings' +import { LogsManager } from './globals/LogsManager' import { s3Storage } from '@payloadcms/storage-s3' import { en } from '@payloadcms/translations/languages/en' import { zh } from '@payloadcms/translations/languages/zh' @@ -42,8 +44,8 @@ export default buildConfig({ }, fallbackLanguage: 'zh', }, - collections: [Users, Media, Products, Announcements, Articles], - globals: [AdminSettings], + collections: [Users, Media, Products, Announcements, Articles, Logs], + globals: [AdminSettings, LogsManager], editor: lexicalEditor(), secret: process.env.PAYLOAD_SECRET || '', typescript: {