From bd995bc736ad3991a32384ee58d0156001b06fe2 Mon Sep 17 00:00:00 2001 From: xuncha <1658671838@qq.com> Date: Fri, 6 Feb 2026 18:25:48 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E8=BD=AC=E8=B4=A6=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E7=9A=84=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + electron/main.ts | 4 ++ electron/preload.ts | 2 + electron/services/chatService.ts | 77 +++++++++++++++++++++++++++++++- src/pages/ChatPage.scss | 7 +++ src/pages/ChatPage.tsx | 60 +++++++++++++++++-------- src/types/electron.d.ts | 1 + src/types/models.ts | 3 ++ 8 files changed, 134 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index cccee17..8b7210e 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,4 @@ wcdb/ *info 概述.md chatlab-format.md +*.bak \ No newline at end of file diff --git a/electron/main.ts b/electron/main.ts index 98f9d92..308444c 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -864,6 +864,10 @@ function registerIpcHandlers() { return await chatService.getContactAvatar(username) }) + ipcMain.handle('chat:resolveTransferDisplayNames', async (_, chatroomId: string, payerUsername: string, receiverUsername: string) => { + return await chatService.resolveTransferDisplayNames(chatroomId, payerUsername, receiverUsername) + }) + ipcMain.handle('chat:getContacts', async () => { return await chatService.getContacts() }) diff --git a/electron/preload.ts b/electron/preload.ts index a3f3451..849e11d 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -131,6 +131,8 @@ contextBridge.exposeInMainWorld('electronAPI', { ipcRenderer.invoke('chat:getNewMessages', sessionId, minTime, limit), getContact: (username: string) => ipcRenderer.invoke('chat:getContact', username), getContactAvatar: (username: string) => ipcRenderer.invoke('chat:getContactAvatar', username), + resolveTransferDisplayNames: (chatroomId: string, payerUsername: string, receiverUsername: string) => + ipcRenderer.invoke('chat:resolveTransferDisplayNames', chatroomId, payerUsername, receiverUsername), getMyAvatarUrl: () => ipcRenderer.invoke('chat:getMyAvatarUrl'), downloadEmoji: (cdnUrl: string, md5?: string) => ipcRenderer.invoke('chat:downloadEmoji', cdnUrl, md5), getCachedMessages: (sessionId: string) => ipcRenderer.invoke('chat:getCachedMessages', sessionId), diff --git a/electron/services/chatService.ts b/electron/services/chatService.ts index dea984d..a4614c5 100644 --- a/electron/services/chatService.ts +++ b/electron/services/chatService.ts @@ -1120,6 +1120,9 @@ class ChatService { // 名片消息 let cardUsername: string | undefined let cardNickname: string | undefined + // 转账消息 + let transferPayerUsername: string | undefined + let transferReceiverUsername: string | undefined // 聊天记录 let chatRecordTitle: string | undefined let chatRecordList: Array<{ @@ -1151,8 +1154,8 @@ class ChatService { const cardInfo = this.parseCardInfo(content) cardUsername = cardInfo.username cardNickname = cardInfo.nickname - } else if (localType === 49 && content) { - // Type 49 消息(链接、文件、小程序、转账等) + } else if ((localType === 49 || localType === 8589934592049) && content) { + // Type 49 消息(链接、文件、小程序、转账等),8589934592049 也是转账类型 const type49Info = this.parseType49Message(content) xmlType = type49Info.xmlType linkTitle = type49Info.linkTitle @@ -1163,6 +1166,8 @@ class ChatService { fileExt = type49Info.fileExt chatRecordTitle = type49Info.chatRecordTitle chatRecordList = type49Info.chatRecordList + transferPayerUsername = type49Info.transferPayerUsername + transferReceiverUsername = type49Info.transferReceiverUsername } else if (localType === 244813135921 || (content && content.includes('57'))) { const quoteInfo = this.parseQuoteMessage(content) quotedContent = quoteInfo.content @@ -1199,6 +1204,8 @@ class ChatService { xmlType, cardUsername, cardNickname, + transferPayerUsername, + transferReceiverUsername, chatRecordTitle, chatRecordList }) @@ -1663,6 +1670,8 @@ class ChatService { fileName?: string fileSize?: number fileExt?: string + transferPayerUsername?: string + transferReceiverUsername?: string chatRecordTitle?: string chatRecordList?: Array<{ datatype: number @@ -1786,6 +1795,16 @@ class ChatService { } else if (feedesc) { result.linkTitle = feedesc } + + // 提取转账双方 wxid + const payerUsername = this.extractXmlValue(content, 'payer_username') + const receiverUsername = this.extractXmlValue(content, 'receiver_username') + if (payerUsername) { + result.transferPayerUsername = payerUsername + } + if (receiverUsername) { + result.transferReceiverUsername = receiverUsername + } break } @@ -2388,6 +2407,60 @@ class ChatService { } } + /** + * 解析转账消息中的付款方和收款方显示名称 + * 优先使用群昵称,群昵称为空时回退到微信昵称/备注 + */ + async resolveTransferDisplayNames( + chatroomId: string, + payerUsername: string, + receiverUsername: string + ): Promise<{ payerName: string; receiverName: string }> { + try { + const connectResult = await this.ensureConnected() + if (!connectResult.success) { + return { payerName: payerUsername, receiverName: receiverUsername } + } + + // 如果是群聊,尝试获取群昵称 + let groupNicknames: Record = {} + if (chatroomId.endsWith('@chatroom')) { + const nickResult = await wcdbService.getGroupNicknames(chatroomId) + if (nickResult.success && nickResult.nicknames) { + groupNicknames = nickResult.nicknames + } + } + + // 解析付款方名称:群昵称 > 备注 > 昵称 > alias > wxid + const resolveName = async (username: string): Promise => { + // 先查群昵称 + const groupNick = groupNicknames[username] + if (groupNick) return groupNick + + // 再查联系人信息 + const contact = await this.getContact(username) + if (contact) { + return contact.remark || contact.nickName || contact.alias || username + } + + // 兜底:查缓存 + const cached = this.avatarCache.get(username) + if (cached?.displayName) return cached.displayName + + return username + } + + const [payerName, receiverName] = await Promise.all([ + resolveName(payerUsername), + resolveName(receiverUsername) + ]) + + return { payerName, receiverName } + } catch { + return { payerName: payerUsername, receiverName: receiverUsername } + } + } + /** * 获取当前用户的头像 URL */ diff --git a/src/pages/ChatPage.scss b/src/pages/ChatPage.scss index 9d51e02..3887409 100644 --- a/src/pages/ChatPage.scss +++ b/src/pages/ChatPage.scss @@ -2489,6 +2489,13 @@ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); } + .transfer-desc { + font-size: 12px; + margin-bottom: 4px; + opacity: 0.9; + line-height: 1.4; + } + .transfer-memo { font-size: 13px; margin-bottom: 8px; diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index 94e96c5..a4e33f7 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -1972,6 +1972,10 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o const [voiceWaveform, setVoiceWaveform] = useState([]) const voiceAutoDecryptTriggered = useRef(false) + // 转账消息双方名称 + const [transferPayerName, setTransferPayerName] = useState(undefined) + const [transferReceiverName, setTransferReceiverName] = useState(undefined) + // 视频相关状态 const [videoLoading, setVideoLoading] = useState(false) const [videoInfo, setVideoInfo] = useState<{ videoUrl?: string; coverUrl?: string; thumbUrl?: string; exists: boolean } | null>(null) @@ -2136,6 +2140,26 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o } }, [isGroupChat, isSent, message.senderUsername, myAvatarUrl]) + // 解析转账消息的付款方和收款方显示名称 + useEffect(() => { + const payerWxid = (message as any).transferPayerUsername + const receiverWxid = (message as any).transferReceiverUsername + if (!payerWxid && !receiverWxid) return + // 仅对转账消息类型处理 + if (message.localType !== 49 && message.localType !== 8589934592049) return + + window.electronAPI.chat.resolveTransferDisplayNames( + session.username, + payerWxid || '', + receiverWxid || '' + ).then((result: { payerName: string; receiverName: string }) => { + if (result) { + setTransferPayerName(result.payerName) + setTransferReceiverName(result.receiverName) + } + }).catch(() => { }) + }, [(message as any).transferPayerUsername, (message as any).transferReceiverUsername, session.username]) + // 自动下载表情包 useEffect(() => { if (emojiLocalPath) return @@ -3002,6 +3026,7 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o let url = '' let appMsgType = '' let textAnnouncement = '' + let parsedDoc: Document | null = null try { const content = message.rawContent || message.parsedContent || '' @@ -3009,13 +3034,13 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o const xmlContent = content.substring(content.indexOf('')) const parser = new DOMParser() - const doc = parser.parseFromString(xmlContent, 'text/xml') + parsedDoc = parser.parseFromString(xmlContent, 'text/xml') - title = doc.querySelector('title')?.textContent || '链接' - desc = doc.querySelector('des')?.textContent || '' - url = doc.querySelector('url')?.textContent || '' - appMsgType = doc.querySelector('appmsg > type')?.textContent || doc.querySelector('type')?.textContent || '' - textAnnouncement = doc.querySelector('textannouncement')?.textContent || '' + title = parsedDoc.querySelector('title')?.textContent || '链接' + desc = parsedDoc.querySelector('des')?.textContent || '' + url = parsedDoc.querySelector('url')?.textContent || '' + appMsgType = parsedDoc.querySelector('appmsg > type')?.textContent || parsedDoc.querySelector('type')?.textContent || '' + textAnnouncement = parsedDoc.querySelector('textannouncement')?.textContent || '' } catch (e) { console.error('解析 AppMsg 失败:', e) } @@ -3143,19 +3168,10 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o // 转账消息 (type=2000) if (appMsgType === '2000') { try { - const content = message.rawContent || message.content || message.parsedContent || '' - - // 添加调试日志 - - - const parser = new DOMParser() - const doc = parser.parseFromString(content, 'text/xml') - - const feedesc = doc.querySelector('feedesc')?.textContent || '' - const payMemo = doc.querySelector('pay_memo')?.textContent || '' - const paysubtype = doc.querySelector('paysubtype')?.textContent || '1' - - + // 使用外层已解析好的 parsedDoc(已去除 wxid 前缀) + const feedesc = parsedDoc?.querySelector('feedesc')?.textContent || '' + const payMemo = parsedDoc?.querySelector('pay_memo')?.textContent || '' + const paysubtype = parsedDoc?.querySelector('paysubtype')?.textContent || '1' // paysubtype: 1=待收款, 3=已收款 const isReceived = paysubtype === '3' @@ -3163,6 +3179,11 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o // 如果 feedesc 为空,使用 title 作为降级 const displayAmount = feedesc || title || '微信转账' + // 构建转账描述:A 转账给 B + const transferDesc = transferPayerName && transferReceiverName + ? `${transferPayerName} 转账给 ${transferReceiverName}` + : undefined + return (
@@ -3173,6 +3194,7 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o
{displayAmount}
+ {transferDesc &&
{transferDesc}
} {payMemo &&
{payMemo}
}
{isReceived ? '已收款' : '微信转账'}
diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index 49aaf33..371c9b9 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -85,6 +85,7 @@ export interface ElectronAPI { }> getContact: (username: string) => Promise getContactAvatar: (username: string) => Promise<{ avatarUrl?: string; displayName?: string } | null> + resolveTransferDisplayNames: (chatroomId: string, payerUsername: string, receiverUsername: string) => Promise<{ payerName: string; receiverName: string }> getContacts: () => Promise<{ success: boolean contacts?: ContactInfo[] diff --git a/src/types/models.ts b/src/types/models.ts index a3b0963..986a694 100644 --- a/src/types/models.ts +++ b/src/types/models.ts @@ -64,6 +64,9 @@ export interface Message { fileSize?: number // 文件大小 fileExt?: string // 文件扩展名 xmlType?: string // XML 中的 type 字段 + // 转账消息 + transferPayerUsername?: string // 转账付款方 wxid + transferReceiverUsername?: string // 转账收款方 wxid // 名片消息 cardUsername?: string // 名片的微信ID cardNickname?: string // 名片的昵称