From 9d080bf9e891813b24254e47c72c7acea25453ab Mon Sep 17 00:00:00 2001 From: WangDL Date: Sun, 24 May 2026 16:17:00 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20M3=20audit=20=E2=80=94=20scheduleState?= =?UTF-8?q?=20persistence,=20AI=E2=86=92ReviewCard=20subscriber,=20ActiveR?= =?UTF-8?q?ecall=20queue,=20streak=20bug,=20domain=20events,=20admin=20pag?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - M3-02: Add scheduleState to ReviewCard Prisma model + persist in updateCard/insertCard - M3-02: Add ReviewCardSubscriber (OnEvent 'ai.analysis.completed' → generateCards) - M3-02: Add AdminReviewController (GET /admin-api/reviews) - M3-01: ActiveRecall now enqueues via AiAnalysisService instead of direct workflow call - M3-01: FocusItem model adds source field, worker uses status:'open' - M3-03: Fix streak calculation (break on gap), add StreakUpdatedEvent - M3-03: Add LearningGoal/StreakRecord/LearningStats to Prisma - M3-03: Fix FocusItem recommendation query (status:'pending' → 'open') - Admin pages: ReviewAdmin, NotificationAdmin, CacheAdmin Co-Authored-By: Claude Opus 4.7 --- src/App.tsx | 9 ++ src/config/menu.tsx | 2 + src/pages/CacheAdmin.tsx | 102 +++++++++++++++++++++ src/pages/NotificationAdmin.tsx | 155 ++++++++++++++++++++++++++++++++ src/pages/ReviewAdmin.tsx | 82 +++++++++++++++++ 5 files changed, 350 insertions(+) create mode 100644 src/pages/CacheAdmin.tsx create mode 100644 src/pages/NotificationAdmin.tsx create mode 100644 src/pages/ReviewAdmin.tsx diff --git a/src/App.tsx b/src/App.tsx index 27d9203..0426f02 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -38,6 +38,9 @@ const Placeholder = lazy(() => import('./pages/Placeholder')) const ForbiddenPage = lazy(() => import('./pages/403')) const NotFoundPage = lazy(() => import('./pages/404')) const ServerErrorPage = lazy(() => import('./pages/500')) +const ReviewAdmin = lazy(() => import('./pages/ReviewAdmin')) +const NotificationAdmin = lazy(() => import('./pages/NotificationAdmin')) +const CacheAdmin = lazy(() => import('./pages/CacheAdmin')) const queryClient = new QueryClient() @@ -152,6 +155,10 @@ function App() { path="vector" element={}>} /> + }>} + /> } /> + }>} /> + }>} /> } /> diff --git a/src/config/menu.tsx b/src/config/menu.tsx index 47eb745..8fcbaa4 100644 --- a/src/config/menu.tsx +++ b/src/config/menu.tsx @@ -42,8 +42,10 @@ export const adminMenuItems: AdminMenuItem[] = [ { path: '/vector', name: '向量检索' }, { path: '/config', name: '配置管理' }, { path: '/cache', name: '缓存管理' }, + { path: '/notification-admin', name: '通知管理' }, { path: '/safety', name: '内容安全' }, ]}, + { path: '/reviews', name: '复习数据', icon: , requiredRole: 'ADMIN' }, { path: '/settings', name: '系统配置', icon: , requiredRole: 'ADMIN' }, ] diff --git a/src/pages/CacheAdmin.tsx b/src/pages/CacheAdmin.tsx new file mode 100644 index 0000000..e1d26c5 --- /dev/null +++ b/src/pages/CacheAdmin.tsx @@ -0,0 +1,102 @@ +import React from 'react' +import { Card, Button, Statistic, Row, Col, Space, Input, Typography, message } from 'antd' +import { ClearOutlined, ReloadOutlined, DeleteOutlined } from '@ant-design/icons' +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import api from '@/lib/api' + +const { Title } = Typography + +export default function CacheAdmin() { + const qc = useQueryClient() + + const { data: stats, isLoading } = useQuery({ + queryKey: ['admin', 'cache-stats'], + queryFn: () => api.get('/admin-api/cache/stats').then(r => r.data?.data), + refetchInterval: 10_000, + }) + + const flushModule = useMutation({ + mutationFn: (module: string) => api.post(`/admin-api/cache/flush/${module}`), + onSuccess: (_, mod) => { + message.success(`已清除 ${mod} 模块缓存`) + qc.invalidateQueries({ queryKey: ['admin', 'cache-stats'] }) + }, + }) + + const flushAll = useMutation({ + mutationFn: () => api.post('/admin-api/cache/flush-all'), + onSuccess: () => { + message.success('已清除所有缓存') + qc.invalidateQueries({ queryKey: ['admin', 'cache-stats'] }) + }, + }) + + const resetStats = useMutation({ + mutationFn: () => api.post('/admin-api/cache/reset-stats'), + onSuccess: () => qc.invalidateQueries({ queryKey: ['admin', 'cache-stats'] }), + }) + + const modules = [ + { key: 'config', label: '配置缓存' }, + { key: 'workspace', label: '工作台缓存' }, + { key: 'admin', label: 'Admin 缓存' }, + { key: 'ai', label: 'AI 路由缓存' }, + { key: 'safety', label: '安全词库缓存' }, + { key: 'user', label: '用户信息缓存' }, + { key: 'kb', label: '知识库缓存' }, + ] + + return ( +
+ 缓存管理 + + + + + + + + + + + + + + + + + + + {modules.map(m => ( + + ))} + + + + + + + +
+ ) +} diff --git a/src/pages/NotificationAdmin.tsx b/src/pages/NotificationAdmin.tsx new file mode 100644 index 0000000..a1520b4 --- /dev/null +++ b/src/pages/NotificationAdmin.tsx @@ -0,0 +1,155 @@ +import React, { useState } from 'react' +import { Card, Table, Button, Modal, Form, Input, Select, Tag, Space, Typography, message } from 'antd' +import { PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons' +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import api from '@/lib/api' + +const { Title } = Typography + +const channelColors: Record = { + in_app: 'blue', push: 'green', email: 'orange', +} + +export default function NotificationAdmin() { + const [modalOpen, setModalOpen] = useState(false) + const [editing, setEditing] = useState(null) + const [form] = Form.useForm() + const qc = useQueryClient() + + const { data: templates, isLoading } = useQuery({ + queryKey: ['admin', 'notification-templates'], + queryFn: () => api.get('/admin-api/notifications/templates').then(r => r.data?.data ?? []), + }) + + const { data: sendLogs } = useQuery({ + queryKey: ['admin', 'notification-logs'], + queryFn: () => api.get('/admin-api/notifications/send-log?limit=50').then(r => r.data?.data ?? []), + }) + + const saveMutation = useMutation({ + mutationFn: (values: any) => + editing + ? api.patch(`/admin-api/notifications/templates/${editing.id}`, values) + : api.post('/admin-api/notifications/templates', values), + onSuccess: () => { + message.success(editing ? '模板已更新' : '模板已创建') + qc.invalidateQueries({ queryKey: ['admin', 'notification-templates'] }) + setModalOpen(false) + setEditing(null) + form.resetFields() + }, + }) + + const deleteMutation = useMutation({ + mutationFn: (id: string) => api.delete(`/admin-api/notifications/templates/${id}`), + onSuccess: () => { + message.success('模板已删除') + qc.invalidateQueries({ queryKey: ['admin', 'notification-templates'] }) + }, + }) + + const openCreate = () => { + setEditing(null) + form.resetFields() + setModalOpen(true) + } + + const openEdit = (record: any) => { + setEditing(record) + form.setFieldsValue(record) + setModalOpen(true) + } + + const templateColumns = [ + { title: '名称', dataIndex: 'name', width: 120 }, + { title: '类型', dataIndex: 'type', width: 120 }, + { title: '标题', dataIndex: 'title', width: 200, ellipsis: true }, + { + title: '渠道', dataIndex: 'channel', width: 80, + render: (c: string) => {c}, + }, + { + title: '状态', dataIndex: 'enabled', width: 60, + render: (e: boolean) => {e ? '启用' : '禁用'}, + }, + { + title: '操作', width: 100, + render: (_: any, record: any) => ( + + } style={{ marginBottom: 24 }}> + + + + +
+ + + { setModalOpen(false); setEditing(null) }} + onOk={() => form.submit()} + confirmLoading={saveMutation.isPending} + > +
+ + + + + + + + + + + + + + } + value={search} + onChange={e => setSearch(e.target.value)} + style={{ width: 240 }} + allowClear + /> +
+ + ) +}