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:
iven
2026-04-11 14:16:45 +08:00
parent 97d3c9026b
commit f29f6d76ee
16 changed files with 180 additions and 38 deletions

View File

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

View File

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

View File

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