[fix]:[FL-145][系统消息要支持删除]

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
chengkai3
2026-06-16 17:44:11 +08:00
parent 946312cb6e
commit 3899a2345e
5 changed files with 148 additions and 13 deletions
+15
View File
@@ -3,6 +3,7 @@ from sqlalchemy.orm import Session
from ...core.database import get_db
from ...core.dependencies import CurrentUser, get_current_user, require_permission
from ...schemas.auth import MessageResponse
from ...schemas.system_message import (
SystemMessageCreateRequest,
SystemMessageListResponse,
@@ -11,6 +12,7 @@ from ...schemas.system_message import (
)
from ...services.system_message_service import (
create_system_message,
delete_system_message,
get_unread_count,
list_user_messages,
mark_messages_as_read,
@@ -72,3 +74,16 @@ def mark_my_messages_read(
"""标记消息为已读"""
affected = mark_messages_as_read(db, current_user.user.id, payload.message_ids)
return {"affected": affected}
@router.delete("/{message_id}", response_model=MessageResponse)
def remove_system_message(
message_id: str,
_: CurrentUser = Depends(require_permission("admin.system_message")),
db: Session = Depends(get_db),
) -> MessageResponse:
"""删除系统消息(需要admin.system_message权限)"""
deleted = delete_system_message(db, message_id)
if not deleted:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="System message not found")
return MessageResponse(message="System message deleted")
+9 -1
View File
@@ -1,7 +1,7 @@
from datetime import datetime
from typing import TYPE_CHECKING
from sqlalchemy import func, select, update
from sqlalchemy import delete, func, select, update
from sqlalchemy.orm import Session
from ..models.system_message import SystemMessage
@@ -86,6 +86,14 @@ def mark_messages_as_read(
return result.rowcount or 0
def delete_system_message(db: Session, message_id: str) -> bool:
"""删除系统消息"""
stmt = delete(SystemMessage).where(SystemMessage.id == message_id)
result = db.execute(stmt)
db.commit()
return (result.rowcount or 0) > 0
def get_unread_count(db: Session, user_id: str) -> int:
"""获取用户未读消息数量"""
query = select(func.count()).select_from(SystemMessage).where(
+39
View File
@@ -0,0 +1,39 @@
from __future__ import annotations
from sqlalchemy import create_engine, select
from sqlalchemy.orm import Session
from app.core.database import Base
from app.models.system_message import SystemMessage
from app.schemas.system_message import SystemMessageCreateRequest
from app.services.system_message_service import create_system_message, delete_system_message
def test_delete_system_message_removes_existing_message() -> None:
engine = create_engine("sqlite+pysqlite:///:memory:")
Base.metadata.create_all(bind=engine, tables=[SystemMessage.__table__])
with Session(engine) as db:
message = create_system_message(
db,
SystemMessageCreateRequest(
title="系统通知",
content="测试内容",
message_type="info",
),
)
deleted = delete_system_message(db, message.id)
assert deleted is True
assert db.scalar(select(SystemMessage).where(SystemMessage.id == message.id)) is None
def test_delete_system_message_returns_false_when_missing() -> None:
engine = create_engine("sqlite+pysqlite:///:memory:")
Base.metadata.create_all(bind=engine, tables=[SystemMessage.__table__])
with Session(engine) as db:
deleted = delete_system_message(db, "missing-message-id")
assert deleted is False
+18
View File
@@ -14,3 +14,21 @@
- 风险与关注点:
- 改动仅影响系统消息公开响应 schema 的序列化方式,不改变接口字段、数据库结构或查询逻辑。
## Work Log - 系统消息支持删除(FL-145)
- 背景:
- 系统消息管理页已有创建、查看、标记已读能力,但缺少删除入口。
- 本次处理:
- 后端新增 `DELETE /api/v1/system-messages/{message_id}`,复用 `admin.system_message` 权限,删除系统消息记录。
- 前端系统消息列表新增删除操作,使用二次确认,并在删除后刷新列表与未读统计。
- 新增系统消息删除服务测试,覆盖存在记录删除与缺失记录返回 `False`
- 验证:
- `UV_CACHE_DIR=/tmp/fquiz-uv-cache uv run --with pytest --with sqlalchemy --with pydantic --with pydantic-settings --with email-validator python -m pytest api/tests/test_system_message_schema.py api/tests/test_system_message_service.py` 通过。
- `npm --workspace web exec eslint src/app/admin/system-messages/page.tsx` 通过。
- `npm --workspace web run build` 通过。
- 风险与关注点:
- 当前删除为管理端物理删除系统消息记录;广播消息删除后对所有用户不可见。
+59 -4
View File
@@ -8,6 +8,7 @@ import {
Empty,
Form,
Input,
Popconfirm,
Select,
Space,
Spin,
@@ -72,6 +73,7 @@ export default function AdminSystemMessagesPage() {
const [formApi] = Form.useForm<CreateMessageValues>();
const [messageTypeFilter, setMessageTypeFilter] = useState<SystemMessageType | "all">("all");
const [unreadOnly, setUnreadOnly] = useState(false);
const [deletingMessageId, setDeletingMessageId] = useState<string | null>(null);
const [error, setError] = useState("");
const [success, setSuccess] = useState("");
@@ -160,6 +162,39 @@ export default function AdminSystemMessagesPage() {
},
});
const deleteMutation = useMutation({
mutationFn: async (messageId: string) => {
if (!canManage) {
throw new Error("缺少 admin.system_message 权限");
}
const response = await fetchWithAuth(`/api/v1/system-messages/${messageId}`, {
method: "DELETE",
});
if (!response.ok) {
throw new Error(await readApiError(response));
}
return response.json() as Promise<{ message: string }>;
},
onMutate: (messageId) => {
setDeletingMessageId(messageId);
setSuccess("");
setError("");
},
onSuccess: async () => {
setError("");
setSuccess("系统消息已删除");
await refreshMessages();
},
onError: (candidate) => {
setSuccess("");
setError(candidate instanceof Error ? candidate.message : "删除失败");
},
onSettled: () => {
setDeletingMessageId(null);
},
});
useToastFeedback({
errorMessage: error,
successMessage: success,
@@ -233,9 +268,13 @@ export default function AdminSystemMessagesPage() {
key: "actions",
width: 120,
fixed: "right",
render: (_, item) => (
render: (_, item) => {
const isDeleting = deletingMessageId === item.id;
return (
<Space size="small">
<Button
disabled={item.is_read}
disabled={item.is_read || isDeleting}
loading={markReadMutation.isPending && markReadMutation.variables?.includes(item.id)}
size="small"
type="link"
@@ -243,10 +282,26 @@ export default function AdminSystemMessagesPage() {
>
</Button>
),
{canManage && (
<Popconfirm
title="删除系统消息"
description={`确认删除系统消息「${item.title}」吗?`}
okText="删除"
cancelText="取消"
okButtonProps={{ danger: true, loading: isDeleting }}
onConfirm={() => deleteMutation.mutate(item.id)}
>
<Button danger loading={isDeleting} size="small" type="link">
</Button>
</Popconfirm>
)}
</Space>
);
},
},
],
[markReadMutation],
[canManage, deleteMutation, deletingMessageId, markReadMutation],
);
if (initializing) {