新增转账消息的解析

This commit is contained in:
xuncha
2026-02-06 18:25:48 +08:00
committed by xuncha
parent 6e05e74d5e
commit bd995bc736
8 changed files with 134 additions and 21 deletions

1
.gitignore vendored
View File

@@ -59,3 +59,4 @@ wcdb/
*info
概述.md
chatlab-format.md
*.bak

View File

@@ -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()
})

View File

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

View File

@@ -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('<type>57</type>'))) {
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<string, string> = {}
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<string> => {
// 先查群昵称
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
*/

View File

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

View File

@@ -1972,6 +1972,10 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o
const [voiceWaveform, setVoiceWaveform] = useState<number[]>([])
const voiceAutoDecryptTriggered = useRef(false)
// 转账消息双方名称
const [transferPayerName, setTransferPayerName] = useState<string | undefined>(undefined)
const [transferReceiverName, setTransferReceiverName] = useState<string | undefined>(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('<msg>'))
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 (
<div className={`transfer-message ${isReceived ? 'received' : ''}`}>
<div className="transfer-icon">
@@ -3173,6 +3194,7 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o
</div>
<div className="transfer-info">
<div className="transfer-amount">{displayAmount}</div>
{transferDesc && <div className="transfer-desc">{transferDesc}</div>}
{payMemo && <div className="transfer-memo">{payMemo}</div>}
<div className="transfer-label">{isReceived ? '已收款' : '微信转账'}</div>
</div>

View File

@@ -85,6 +85,7 @@ export interface ElectronAPI {
}>
getContact: (username: string) => Promise<Contact | null>
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[]

View File

@@ -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 // 名片的昵称