[fix]:[FL-145][系统消息要支持删除]
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -3,6 +3,7 @@ from sqlalchemy.orm import Session
|
|||||||
|
|
||||||
from ...core.database import get_db
|
from ...core.database import get_db
|
||||||
from ...core.dependencies import CurrentUser, get_current_user, require_permission
|
from ...core.dependencies import CurrentUser, get_current_user, require_permission
|
||||||
|
from ...schemas.auth import MessageResponse
|
||||||
from ...schemas.system_message import (
|
from ...schemas.system_message import (
|
||||||
SystemMessageCreateRequest,
|
SystemMessageCreateRequest,
|
||||||
SystemMessageListResponse,
|
SystemMessageListResponse,
|
||||||
@@ -11,6 +12,7 @@ from ...schemas.system_message import (
|
|||||||
)
|
)
|
||||||
from ...services.system_message_service import (
|
from ...services.system_message_service import (
|
||||||
create_system_message,
|
create_system_message,
|
||||||
|
delete_system_message,
|
||||||
get_unread_count,
|
get_unread_count,
|
||||||
list_user_messages,
|
list_user_messages,
|
||||||
mark_messages_as_read,
|
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)
|
affected = mark_messages_as_read(db, current_user.user.id, payload.message_ids)
|
||||||
return {"affected": affected}
|
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")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from sqlalchemy import func, select, update
|
from sqlalchemy import delete, func, select, update
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from ..models.system_message import SystemMessage
|
from ..models.system_message import SystemMessage
|
||||||
@@ -86,6 +86,14 @@ def mark_messages_as_read(
|
|||||||
return result.rowcount or 0
|
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:
|
def get_unread_count(db: Session, user_id: str) -> int:
|
||||||
"""获取用户未读消息数量"""
|
"""获取用户未读消息数量"""
|
||||||
query = select(func.count()).select_from(SystemMessage).where(
|
query = select(func.count()).select_from(SystemMessage).where(
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -14,3 +14,21 @@
|
|||||||
|
|
||||||
- 风险与关注点:
|
- 风险与关注点:
|
||||||
- 改动仅影响系统消息公开响应 schema 的序列化方式,不改变接口字段、数据库结构或查询逻辑。
|
- 改动仅影响系统消息公开响应 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` 通过。
|
||||||
|
|
||||||
|
- 风险与关注点:
|
||||||
|
- 当前删除为管理端物理删除系统消息记录;广播消息删除后对所有用户不可见。
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
Empty,
|
Empty,
|
||||||
Form,
|
Form,
|
||||||
Input,
|
Input,
|
||||||
|
Popconfirm,
|
||||||
Select,
|
Select,
|
||||||
Space,
|
Space,
|
||||||
Spin,
|
Spin,
|
||||||
@@ -72,6 +73,7 @@ export default function AdminSystemMessagesPage() {
|
|||||||
const [formApi] = Form.useForm<CreateMessageValues>();
|
const [formApi] = Form.useForm<CreateMessageValues>();
|
||||||
const [messageTypeFilter, setMessageTypeFilter] = useState<SystemMessageType | "all">("all");
|
const [messageTypeFilter, setMessageTypeFilter] = useState<SystemMessageType | "all">("all");
|
||||||
const [unreadOnly, setUnreadOnly] = useState(false);
|
const [unreadOnly, setUnreadOnly] = useState(false);
|
||||||
|
const [deletingMessageId, setDeletingMessageId] = useState<string | null>(null);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [success, setSuccess] = 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({
|
useToastFeedback({
|
||||||
errorMessage: error,
|
errorMessage: error,
|
||||||
successMessage: success,
|
successMessage: success,
|
||||||
@@ -233,20 +268,40 @@ export default function AdminSystemMessagesPage() {
|
|||||||
key: "actions",
|
key: "actions",
|
||||||
width: 120,
|
width: 120,
|
||||||
fixed: "right",
|
fixed: "right",
|
||||||
render: (_, item) => (
|
render: (_, item) => {
|
||||||
<Button
|
const isDeleting = deletingMessageId === item.id;
|
||||||
disabled={item.is_read}
|
|
||||||
loading={markReadMutation.isPending && markReadMutation.variables?.includes(item.id)}
|
return (
|
||||||
size="small"
|
<Space size="small">
|
||||||
type="link"
|
<Button
|
||||||
onClick={() => markReadMutation.mutate([item.id])}
|
disabled={item.is_read || isDeleting}
|
||||||
>
|
loading={markReadMutation.isPending && markReadMutation.variables?.includes(item.id)}
|
||||||
标记已读
|
size="small"
|
||||||
</Button>
|
type="link"
|
||||||
),
|
onClick={() => markReadMutation.mutate([item.id])}
|
||||||
|
>
|
||||||
|
标记已读
|
||||||
|
</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) {
|
if (initializing) {
|
||||||
|
|||||||
Reference in New Issue
Block a user