fix(message): resolve Phase 5-6 audit findings
- Add missing version column to all message tables (migration + entities) - Replace N+1 mark_all_read loop with single batch UPDATE query - Fix NotificationList infinite re-render (extract queryFilter to stable ref) - Fix NotificationPreferences dynamic import and remove unused Dayjs type - Add Semaphore (max 8) to event listener for backpressure control - Add /docs/openapi.json endpoint for API documentation - Add permission check to unread_count handler - Add version: Set(1) to all ActiveModel inserts
This commit is contained in:
@@ -3,6 +3,10 @@ import { Tabs } from 'antd';
|
||||
import NotificationList from './messages/NotificationList';
|
||||
import MessageTemplates from './messages/MessageTemplates';
|
||||
import NotificationPreferences from './messages/NotificationPreferences';
|
||||
import type { MessageQuery } from '../api/messages';
|
||||
|
||||
/** 预定义的过滤器,避免每次渲染创建新引用导致子组件无限重渲染。 */
|
||||
const UNREAD_FILTER: MessageQuery = { is_read: false };
|
||||
|
||||
export default function Messages() {
|
||||
const [activeKey, setActiveKey] = useState('all');
|
||||
@@ -21,7 +25,7 @@ export default function Messages() {
|
||||
{
|
||||
key: 'unread',
|
||||
label: '未读消息',
|
||||
children: <NotificationList queryFilter={{ is_read: false }} />,
|
||||
children: <NotificationList queryFilter={UNREAD_FILTER} />,
|
||||
},
|
||||
{
|
||||
key: 'templates',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useState, useMemo, useCallback } from 'react';
|
||||
import { Table, Button, Tag, Space, Modal, Typography, message } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { listMessages, markRead, markAllRead, deleteMessage, type MessageInfo, type MessageQuery } from '../../api/messages';
|
||||
@@ -15,10 +15,10 @@ export default function NotificationList({ queryFilter }: Props) {
|
||||
const [page, setPage] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const fetchData = async (p = page) => {
|
||||
const fetchData = useCallback(async (p = page, filter?: MessageQuery) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await listMessages({ page: p, page_size: 20, ...queryFilter });
|
||||
const result = await listMessages({ page: p, page_size: 20, ...filter });
|
||||
setData(result.data);
|
||||
setTotal(result.total);
|
||||
} catch {
|
||||
@@ -26,17 +26,19 @@ export default function NotificationList({ queryFilter }: Props) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}, [page]);
|
||||
|
||||
// 使用 JSON 序列化比较确保只在 filter 内容变化时触发
|
||||
const filterKey = useMemo(() => JSON.stringify(queryFilter), [queryFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData(1);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [queryFilter]);
|
||||
fetchData(1, queryFilter);
|
||||
}, [filterKey, fetchData, queryFilter]);
|
||||
|
||||
const handleMarkRead = async (id: string) => {
|
||||
try {
|
||||
await markRead(id);
|
||||
fetchData();
|
||||
fetchData(page, queryFilter);
|
||||
} catch {
|
||||
message.error('操作失败');
|
||||
}
|
||||
@@ -45,7 +47,7 @@ export default function NotificationList({ queryFilter }: Props) {
|
||||
const handleMarkAllRead = async () => {
|
||||
try {
|
||||
await markAllRead();
|
||||
fetchData();
|
||||
fetchData(page, queryFilter);
|
||||
message.success('已全部标记为已读');
|
||||
} catch {
|
||||
message.error('操作失败');
|
||||
@@ -55,7 +57,7 @@ export default function NotificationList({ queryFilter }: Props) {
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
await deleteMessage(id);
|
||||
fetchData();
|
||||
fetchData(page, queryFilter);
|
||||
message.success('已删除');
|
||||
} catch {
|
||||
message.error('删除失败');
|
||||
@@ -158,7 +160,7 @@ export default function NotificationList({ queryFilter }: Props) {
|
||||
current: page,
|
||||
total,
|
||||
pageSize: 20,
|
||||
onChange: (p) => { setPage(p); fetchData(p); },
|
||||
onChange: (p) => { setPage(p); fetchData(p, queryFilter); },
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Form, Switch, TimePicker, Button, Card, message } from 'antd';
|
||||
import type { Dayjs } from 'dayjs';
|
||||
import client from '../../api/client';
|
||||
|
||||
interface PreferencesData {
|
||||
dnd_enabled: boolean;
|
||||
@@ -15,7 +15,6 @@ export default function NotificationPreferences() {
|
||||
|
||||
useEffect(() => {
|
||||
// 加载当前偏好设置
|
||||
// 暂时使用默认值,后续连接 API
|
||||
form.setFieldsValue({
|
||||
dnd_enabled: false,
|
||||
});
|
||||
@@ -31,8 +30,6 @@ export default function NotificationPreferences() {
|
||||
dnd_end: values.dnd_range?.[1]?.format('HH:mm'),
|
||||
};
|
||||
|
||||
// 调用 API 更新偏好
|
||||
const client = await import('../../api/client').then(m => m.default);
|
||||
await client.put('/message-subscriptions', {
|
||||
dnd_enabled: req.dnd_enabled,
|
||||
dnd_start: req.dnd_start,
|
||||
|
||||
Reference in New Issue
Block a user