mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 15:25:50 +00:00
新增转账消息的解析
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -59,3 +59,4 @@ wcdb/
|
|||||||
*info
|
*info
|
||||||
概述.md
|
概述.md
|
||||||
chatlab-format.md
|
chatlab-format.md
|
||||||
|
*.bak
|
||||||
@@ -864,6 +864,10 @@ function registerIpcHandlers() {
|
|||||||
return await chatService.getContactAvatar(username)
|
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 () => {
|
ipcMain.handle('chat:getContacts', async () => {
|
||||||
return await chatService.getContacts()
|
return await chatService.getContacts()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -131,6 +131,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
ipcRenderer.invoke('chat:getNewMessages', sessionId, minTime, limit),
|
ipcRenderer.invoke('chat:getNewMessages', sessionId, minTime, limit),
|
||||||
getContact: (username: string) => ipcRenderer.invoke('chat:getContact', username),
|
getContact: (username: string) => ipcRenderer.invoke('chat:getContact', username),
|
||||||
getContactAvatar: (username: string) => ipcRenderer.invoke('chat:getContactAvatar', 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'),
|
getMyAvatarUrl: () => ipcRenderer.invoke('chat:getMyAvatarUrl'),
|
||||||
downloadEmoji: (cdnUrl: string, md5?: string) => ipcRenderer.invoke('chat:downloadEmoji', cdnUrl, md5),
|
downloadEmoji: (cdnUrl: string, md5?: string) => ipcRenderer.invoke('chat:downloadEmoji', cdnUrl, md5),
|
||||||
getCachedMessages: (sessionId: string) => ipcRenderer.invoke('chat:getCachedMessages', sessionId),
|
getCachedMessages: (sessionId: string) => ipcRenderer.invoke('chat:getCachedMessages', sessionId),
|
||||||
|
|||||||
@@ -1120,6 +1120,9 @@ class ChatService {
|
|||||||
// 名片消息
|
// 名片消息
|
||||||
let cardUsername: string | undefined
|
let cardUsername: string | undefined
|
||||||
let cardNickname: string | undefined
|
let cardNickname: string | undefined
|
||||||
|
// 转账消息
|
||||||
|
let transferPayerUsername: string | undefined
|
||||||
|
let transferReceiverUsername: string | undefined
|
||||||
// 聊天记录
|
// 聊天记录
|
||||||
let chatRecordTitle: string | undefined
|
let chatRecordTitle: string | undefined
|
||||||
let chatRecordList: Array<{
|
let chatRecordList: Array<{
|
||||||
@@ -1151,8 +1154,8 @@ class ChatService {
|
|||||||
const cardInfo = this.parseCardInfo(content)
|
const cardInfo = this.parseCardInfo(content)
|
||||||
cardUsername = cardInfo.username
|
cardUsername = cardInfo.username
|
||||||
cardNickname = cardInfo.nickname
|
cardNickname = cardInfo.nickname
|
||||||
} else if (localType === 49 && content) {
|
} else if ((localType === 49 || localType === 8589934592049) && content) {
|
||||||
// Type 49 消息(链接、文件、小程序、转账等)
|
// Type 49 消息(链接、文件、小程序、转账等),8589934592049 也是转账类型
|
||||||
const type49Info = this.parseType49Message(content)
|
const type49Info = this.parseType49Message(content)
|
||||||
xmlType = type49Info.xmlType
|
xmlType = type49Info.xmlType
|
||||||
linkTitle = type49Info.linkTitle
|
linkTitle = type49Info.linkTitle
|
||||||
@@ -1163,6 +1166,8 @@ class ChatService {
|
|||||||
fileExt = type49Info.fileExt
|
fileExt = type49Info.fileExt
|
||||||
chatRecordTitle = type49Info.chatRecordTitle
|
chatRecordTitle = type49Info.chatRecordTitle
|
||||||
chatRecordList = type49Info.chatRecordList
|
chatRecordList = type49Info.chatRecordList
|
||||||
|
transferPayerUsername = type49Info.transferPayerUsername
|
||||||
|
transferReceiverUsername = type49Info.transferReceiverUsername
|
||||||
} else if (localType === 244813135921 || (content && content.includes('<type>57</type>'))) {
|
} else if (localType === 244813135921 || (content && content.includes('<type>57</type>'))) {
|
||||||
const quoteInfo = this.parseQuoteMessage(content)
|
const quoteInfo = this.parseQuoteMessage(content)
|
||||||
quotedContent = quoteInfo.content
|
quotedContent = quoteInfo.content
|
||||||
@@ -1199,6 +1204,8 @@ class ChatService {
|
|||||||
xmlType,
|
xmlType,
|
||||||
cardUsername,
|
cardUsername,
|
||||||
cardNickname,
|
cardNickname,
|
||||||
|
transferPayerUsername,
|
||||||
|
transferReceiverUsername,
|
||||||
chatRecordTitle,
|
chatRecordTitle,
|
||||||
chatRecordList
|
chatRecordList
|
||||||
})
|
})
|
||||||
@@ -1663,6 +1670,8 @@ class ChatService {
|
|||||||
fileName?: string
|
fileName?: string
|
||||||
fileSize?: number
|
fileSize?: number
|
||||||
fileExt?: string
|
fileExt?: string
|
||||||
|
transferPayerUsername?: string
|
||||||
|
transferReceiverUsername?: string
|
||||||
chatRecordTitle?: string
|
chatRecordTitle?: string
|
||||||
chatRecordList?: Array<{
|
chatRecordList?: Array<{
|
||||||
datatype: number
|
datatype: number
|
||||||
@@ -1786,6 +1795,16 @@ class ChatService {
|
|||||||
} else if (feedesc) {
|
} else if (feedesc) {
|
||||||
result.linkTitle = 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
|
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
|
* 获取当前用户的头像 URL
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2489,6 +2489,13 @@
|
|||||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
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 {
|
.transfer-memo {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
|
|||||||
@@ -1972,6 +1972,10 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o
|
|||||||
const [voiceWaveform, setVoiceWaveform] = useState<number[]>([])
|
const [voiceWaveform, setVoiceWaveform] = useState<number[]>([])
|
||||||
const voiceAutoDecryptTriggered = useRef(false)
|
const voiceAutoDecryptTriggered = useRef(false)
|
||||||
|
|
||||||
|
// 转账消息双方名称
|
||||||
|
const [transferPayerName, setTransferPayerName] = useState<string | undefined>(undefined)
|
||||||
|
const [transferReceiverName, setTransferReceiverName] = useState<string | undefined>(undefined)
|
||||||
|
|
||||||
// 视频相关状态
|
// 视频相关状态
|
||||||
const [videoLoading, setVideoLoading] = useState(false)
|
const [videoLoading, setVideoLoading] = useState(false)
|
||||||
const [videoInfo, setVideoInfo] = useState<{ videoUrl?: string; coverUrl?: string; thumbUrl?: string; exists: boolean } | null>(null)
|
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])
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (emojiLocalPath) return
|
if (emojiLocalPath) return
|
||||||
@@ -3002,6 +3026,7 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o
|
|||||||
let url = ''
|
let url = ''
|
||||||
let appMsgType = ''
|
let appMsgType = ''
|
||||||
let textAnnouncement = ''
|
let textAnnouncement = ''
|
||||||
|
let parsedDoc: Document | null = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const content = message.rawContent || message.parsedContent || ''
|
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 xmlContent = content.substring(content.indexOf('<msg>'))
|
||||||
|
|
||||||
const parser = new DOMParser()
|
const parser = new DOMParser()
|
||||||
const doc = parser.parseFromString(xmlContent, 'text/xml')
|
parsedDoc = parser.parseFromString(xmlContent, 'text/xml')
|
||||||
|
|
||||||
title = doc.querySelector('title')?.textContent || '链接'
|
title = parsedDoc.querySelector('title')?.textContent || '链接'
|
||||||
desc = doc.querySelector('des')?.textContent || ''
|
desc = parsedDoc.querySelector('des')?.textContent || ''
|
||||||
url = doc.querySelector('url')?.textContent || ''
|
url = parsedDoc.querySelector('url')?.textContent || ''
|
||||||
appMsgType = doc.querySelector('appmsg > type')?.textContent || doc.querySelector('type')?.textContent || ''
|
appMsgType = parsedDoc.querySelector('appmsg > type')?.textContent || parsedDoc.querySelector('type')?.textContent || ''
|
||||||
textAnnouncement = doc.querySelector('textannouncement')?.textContent || ''
|
textAnnouncement = parsedDoc.querySelector('textannouncement')?.textContent || ''
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('解析 AppMsg 失败:', e)
|
console.error('解析 AppMsg 失败:', e)
|
||||||
}
|
}
|
||||||
@@ -3143,19 +3168,10 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o
|
|||||||
// 转账消息 (type=2000)
|
// 转账消息 (type=2000)
|
||||||
if (appMsgType === '2000') {
|
if (appMsgType === '2000') {
|
||||||
try {
|
try {
|
||||||
const content = message.rawContent || message.content || message.parsedContent || ''
|
// 使用外层已解析好的 parsedDoc(已去除 wxid 前缀)
|
||||||
|
const feedesc = parsedDoc?.querySelector('feedesc')?.textContent || ''
|
||||||
// 添加调试日志
|
const payMemo = parsedDoc?.querySelector('pay_memo')?.textContent || ''
|
||||||
|
const paysubtype = parsedDoc?.querySelector('paysubtype')?.textContent || '1'
|
||||||
|
|
||||||
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'
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// paysubtype: 1=待收款, 3=已收款
|
// paysubtype: 1=待收款, 3=已收款
|
||||||
const isReceived = paysubtype === '3'
|
const isReceived = paysubtype === '3'
|
||||||
@@ -3163,6 +3179,11 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o
|
|||||||
// 如果 feedesc 为空,使用 title 作为降级
|
// 如果 feedesc 为空,使用 title 作为降级
|
||||||
const displayAmount = feedesc || title || '微信转账'
|
const displayAmount = feedesc || title || '微信转账'
|
||||||
|
|
||||||
|
// 构建转账描述:A 转账给 B
|
||||||
|
const transferDesc = transferPayerName && transferReceiverName
|
||||||
|
? `${transferPayerName} 转账给 ${transferReceiverName}`
|
||||||
|
: undefined
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`transfer-message ${isReceived ? 'received' : ''}`}>
|
<div className={`transfer-message ${isReceived ? 'received' : ''}`}>
|
||||||
<div className="transfer-icon">
|
<div className="transfer-icon">
|
||||||
@@ -3173,6 +3194,7 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o
|
|||||||
</div>
|
</div>
|
||||||
<div className="transfer-info">
|
<div className="transfer-info">
|
||||||
<div className="transfer-amount">{displayAmount}</div>
|
<div className="transfer-amount">{displayAmount}</div>
|
||||||
|
{transferDesc && <div className="transfer-desc">{transferDesc}</div>}
|
||||||
{payMemo && <div className="transfer-memo">{payMemo}</div>}
|
{payMemo && <div className="transfer-memo">{payMemo}</div>}
|
||||||
<div className="transfer-label">{isReceived ? '已收款' : '微信转账'}</div>
|
<div className="transfer-label">{isReceived ? '已收款' : '微信转账'}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
1
src/types/electron.d.ts
vendored
1
src/types/electron.d.ts
vendored
@@ -85,6 +85,7 @@ export interface ElectronAPI {
|
|||||||
}>
|
}>
|
||||||
getContact: (username: string) => Promise<Contact | null>
|
getContact: (username: string) => Promise<Contact | null>
|
||||||
getContactAvatar: (username: string) => Promise<{ avatarUrl?: string; displayName?: string } | null>
|
getContactAvatar: (username: string) => Promise<{ avatarUrl?: string; displayName?: string } | null>
|
||||||
|
resolveTransferDisplayNames: (chatroomId: string, payerUsername: string, receiverUsername: string) => Promise<{ payerName: string; receiverName: string }>
|
||||||
getContacts: () => Promise<{
|
getContacts: () => Promise<{
|
||||||
success: boolean
|
success: boolean
|
||||||
contacts?: ContactInfo[]
|
contacts?: ContactInfo[]
|
||||||
|
|||||||
@@ -64,6 +64,9 @@ export interface Message {
|
|||||||
fileSize?: number // 文件大小
|
fileSize?: number // 文件大小
|
||||||
fileExt?: string // 文件扩展名
|
fileExt?: string // 文件扩展名
|
||||||
xmlType?: string // XML 中的 type 字段
|
xmlType?: string // XML 中的 type 字段
|
||||||
|
// 转账消息
|
||||||
|
transferPayerUsername?: string // 转账付款方 wxid
|
||||||
|
transferReceiverUsername?: string // 转账收款方 wxid
|
||||||
// 名片消息
|
// 名片消息
|
||||||
cardUsername?: string // 名片的微信ID
|
cardUsername?: string // 名片的微信ID
|
||||||
cardNickname?: string // 名片的昵称
|
cardNickname?: string // 名片的昵称
|
||||||
|
|||||||
Reference in New Issue
Block a user