Skip to content

Commit ae979f5

Browse files
committed
feat: 全局关键词搜索 + FULLTEXT索引优化
- 实时监控页面新增全局搜索开关(🌐 全局) - 后端全局搜索使用 MySQL FULLTEXT 索引(ngram),44秒→25ms - 全局搜索时停止 WebSocket 推送和轮询,避免结果被干扰 - 单会话搜索保持 ILIKE(数据量小,无需 FULLTEXT)
1 parent 38629c6 commit ae979f5

2 files changed

Lines changed: 64 additions & 11 deletions

File tree

backend/app/api/messages.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,10 @@ async def list_messages(
162162
db: AsyncSession = Depends(get_db),
163163
):
164164
"""获取消息列表 - 返回分页数据"""
165+
# 全局搜索时限制每页数量,避免大表全扫超时
166+
if not conversation_id and keyword and page_size > 50:
167+
page_size = 50
168+
165169
# 构建查询条件
166170
conditions = []
167171

@@ -181,10 +185,29 @@ async def list_messages(
181185
select(Sender.id).where(Sender.phone.isnot(None), Sender.phone != '')
182186
))
183187
if keyword:
184-
conditions.append(or_(
185-
Message.text.ilike(f"%{keyword}%"),
186-
Message.caption.ilike(f"%{keyword}%")
187-
))
188+
if conversation_id:
189+
# 单会话:用 ILIKE(数据量小)
190+
conditions.append(or_(
191+
Message.text.ilike(f"%{keyword}%"),
192+
Message.caption.ilike(f"%{keyword}%")
193+
))
194+
else:
195+
# 全局搜索:优先用 FULLTEXT 索引,降级用 ILIKE
196+
from sqlalchemy import text as sa_text
197+
try:
198+
# 测试 FULLTEXT 索引是否可用
199+
await db.execute(sa_text(
200+
"SELECT 1 FROM messages WHERE MATCH(text) AGAINST(:kw IN BOOLEAN MODE) LIMIT 1"
201+
).bindparams(kw=keyword))
202+
conditions.append(
203+
sa_text("MATCH(text) AGAINST(:kw IN BOOLEAN MODE)").bindparams(kw=keyword)
204+
)
205+
except Exception:
206+
# FULLTEXT 不可用,降级为 ILIKE
207+
conditions.append(or_(
208+
Message.text.ilike(f"%{keyword}%"),
209+
Message.caption.ilike(f"%{keyword}%")
210+
))
188211

189212
# 日期过滤
190213
if start_date:

frontend/src/pages/MonitoringPage.tsx

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,13 @@ interface MonitoringStatus {
6868

6969
export function MonitoringPage() {
7070
const [messages, setMessages] = useState<Message[]>([]);
71+
const [searchLoading, setSearchLoading] = useState(false);
7172
const [conversations, setConversations] = useState<Conversation[]>([]);
7273
const [monitoringStatus, setMonitoringStatus] = useState<MonitoringStatus[]>([]);
7374
const [selectedConversation, setSelectedConversation] = useState<number | null>(null);
7475
const [filter, setFilter] = useState("");
7576
const [phoneFilter, setPhoneFilter] = useState(false);
77+
const [globalSearch, setGlobalSearch] = useState(false);
7678
const [messageTypeFilter, setMessageTypeFilter] = useState<string>("all");
7779
const [alertFilter, setAlertFilter] = useState<string>("all");
7880
const [autoScroll, setAutoScroll] = useState(true);
@@ -198,7 +200,7 @@ export function MonitoringPage() {
198200
const newMessage = wsMessage.data as Message;
199201

200202
// 只有当新消息属于当前选中的会话时才添加到列表
201-
if (selectedConversation && newMessage.conversation_id === selectedConversation) {
203+
if (selectedConversation && !globalSearch && newMessage.conversation_id === selectedConversation) {
202204
setMessages((prev) => {
203205
// 检查消息是否已存在(防止重复)
204206
if (prev.some((m) => m.id === newMessage.id)) {
@@ -272,10 +274,10 @@ export function MonitoringPage() {
272274

273275
const interval = setInterval(async () => {
274276
// 轮询当前选中的会话消息
275-
if (selectedConversation) {
277+
if (selectedConversation && !globalSearch) {
276278
try {
277279
const params: any = {
278-
conversation_id: selectedConversation,
280+
...(globalSearch ? {} : { conversation_id: selectedConversation }),
279281
page: 1,
280282
page_size: 10, // 只获取最新的10条消息
281283
};
@@ -319,7 +321,7 @@ export function MonitoringPage() {
319321
setIsPolling(false);
320322
}
321323
};
322-
}, [isConnected, selectedConversation, pollingInterval]);
324+
}, [isConnected, selectedConversation, pollingInterval, globalSearch]);
323325

324326
const fetchGlobalStats = async () => {
325327
try {
@@ -508,10 +510,12 @@ export function MonitoringPage() {
508510
};
509511

510512
const fetchMessages = async () => {
511-
if (!selectedConversation) return;
513+
if (!globalSearch && !selectedConversation) return;
514+
if (globalSearch && !filter) return;
512515
try {
516+
if (globalSearch) setSearchLoading(true);
513517
const params: any = {
514-
conversation_id: selectedConversation,
518+
...(globalSearch ? {} : { conversation_id: selectedConversation }),
515519
page: currentPage,
516520
page_size: pageSize,
517521
};
@@ -540,6 +544,8 @@ export function MonitoringPage() {
540544
}, 50);
541545
} catch (error) {
542546
console.error("Failed to fetch messages:", error);
547+
} finally {
548+
setSearchLoading(false);
543549
}
544550
};
545551

@@ -875,6 +881,20 @@ export function MonitoringPage() {
875881
/>
876882
<span>📱 有手机号</span>
877883
</label>
884+
<label className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-cyber-blue/10 border border-cyber-blue/20 cursor-pointer min-h-[44px] text-xs sm:text-sm">
885+
<input
886+
type="checkbox"
887+
checked={globalSearch}
888+
onChange={(e) => {
889+
setGlobalSearch(e.target.checked);
890+
setMessages([]);
891+
setTotalCount(0);
892+
setCurrentPage(1);
893+
}}
894+
className="rounded w-4 h-4"
895+
/>
896+
<span>🌐 全局</span>
897+
</label>
878898
<select
879899
value={pageSize}
880900
onChange={(e) => {
@@ -897,6 +917,7 @@ export function MonitoringPage() {
897917
setAlertFilter("all");
898918
setPhoneFilter(false);
899919
setCurrentPage(1);
920+
setGlobalSearch(false);
900921
}}
901922
className="min-h-[44px]"
902923
>
@@ -910,10 +931,19 @@ export function MonitoringPage() {
910931
<div className="space-y-4">
911932
{/* 消息列表 */}
912933
<div ref={messageListRef} className="max-h-[400px] sm:max-h-[600px] overflow-y-auto tech-scrollbar pr-0 sm:pr-2">
913-
{!selectedConversation ? (
934+
{!selectedConversation && !globalSearch ? (
914935
<div className="flex items-center justify-center h-64 text-muted-foreground">
915936
请选择一个会话开始监控
916937
</div>
938+
) : globalSearch && !filter ? (
939+
<div className="flex items-center justify-center h-64 text-muted-foreground">
940+
请输入关键词进行全局搜索
941+
</div>
942+
) : searchLoading ? (
943+
<div className="flex items-center justify-center h-64 text-muted-foreground">
944+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-cyber-blue mr-3"></div>
945+
全局搜索中,请稍候...
946+
</div>
917947
) : filteredMessages.length === 0 ? (
918948
<div className="flex items-center justify-center h-64 text-muted-foreground">
919949
暂无消息

0 commit comments

Comments
 (0)