mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-10-04 03:01:42 +08:00
feat: map transfer/hb status labels into export and chat cards
This commit is contained in:
@@ -1172,13 +1172,37 @@ const renderExportScript = (name: string): string => `
|
||||
}
|
||||
const renderPaymentContent = (data, kind) => {
|
||||
const isTransfer = kind === 'transfer'
|
||||
const pay = (isTransfer ? data.transfer : data.pay) || {}
|
||||
const amount = pay.amountText
|
||||
const memo = pay.payMemo
|
||||
const statusText = isTransfer ? pay.transferStatusText : pay.redPacketStatusText
|
||||
const meta = [amount, memo ? '备注:' + memo : '', statusText]
|
||||
.filter(Boolean)
|
||||
.join(' · ')
|
||||
const title = isTransfer
|
||||
? (data.title || '微信转账')
|
||||
: (pay.sendTitle || pay.receiveTitle || data.title || '微信红包')
|
||||
const blurb =
|
||||
data.description ||
|
||||
data.des ||
|
||||
(isTransfer ? '转账消息' : (pay.sceneText || '恭喜发财,大吉大利'))
|
||||
// 状态/金额/备注始终单独露出,避免被消息原文 des 盖住。
|
||||
const description = meta
|
||||
? (blurb && blurb !== meta ? blurb + ' · ' + meta : meta)
|
||||
: blurb
|
||||
const footerBits = isTransfer
|
||||
? [pay.paySubtype === '3' ? '收款' : pay.paySubtype === '1' || pay.paySubtype === '4' ? '转账' : '',
|
||||
pay.transferId ? '单号 ' + pay.transferId : '']
|
||||
: [pay.hbType ? '类型 ' + pay.hbType : '', pay.sendId ? 'sendid ' + pay.sendId : '']
|
||||
return '<div class="structured-content payment-content ' + (isTransfer ? 'transfer' : 'red-packet') +
|
||||
'" data-rich-kind="' + (isTransfer ? 'transfer' : 'redPacket') + '">' +
|
||||
'<div class="structured-kicker">' + (isTransfer ? '微信转账' : '微信红包') + '</div>' +
|
||||
'<div class="structured-title">' + displayText(data.title || (isTransfer ? '微信转账' : '微信红包')) + '</div>' +
|
||||
'<div class="structured-description">' +
|
||||
displayText(data.description || data.des || (isTransfer ? '转账消息' : '恭喜发财,大吉大利')) +
|
||||
'</div></div>'
|
||||
'<div class="structured-title">' + displayText(title) + '</div>' +
|
||||
'<div class="structured-description">' + displayText(description) + '</div>' +
|
||||
(footerBits.filter(Boolean).length
|
||||
? '<div class="structured-footer">' + displayText(footerBits.filter(Boolean).join(' · ')) + '</div>'
|
||||
: '') +
|
||||
'</div>'
|
||||
}
|
||||
const renderVoipContent = (data) => {
|
||||
const title = Number(data.roomType) === 1 ? '视频通话' : '语音通话'
|
||||
|
||||
+103
-5
@@ -1,3 +1,5 @@
|
||||
import { describeRedPacketStatus, describeTransferStatus } from '../shared/payment-status'
|
||||
|
||||
type TextContent = { type: 'text'; content: string }
|
||||
type VoiceContent = { type: 'voice'; duration?: number }
|
||||
type LocationContent = {
|
||||
@@ -22,6 +24,40 @@ type ShareContent = {
|
||||
appname?: string
|
||||
typeVal?: string
|
||||
articles?: ShareArticle[]
|
||||
transfer?: TransferPaymentInfo
|
||||
}
|
||||
|
||||
type TransferPaymentInfo = {
|
||||
paySubtype?: string
|
||||
amountText?: string
|
||||
transcationId?: string
|
||||
transferId?: string
|
||||
invalidTime?: string
|
||||
beginTransferTime?: string
|
||||
effectiveDate?: string
|
||||
payMemo?: string
|
||||
receiverUsername?: string
|
||||
payerUsername?: string
|
||||
transferStatus?: string
|
||||
/** `transfer_status` 展示文案(只读)。 */
|
||||
transferStatusText?: string
|
||||
}
|
||||
|
||||
type RedPacketPaymentInfo = {
|
||||
templateId?: string
|
||||
receiveTitle?: string
|
||||
sendTitle?: string
|
||||
sceneText?: string
|
||||
senderDes?: string
|
||||
receiverDes?: string
|
||||
iconUrl?: string
|
||||
nativeUrl?: string
|
||||
sendId?: string
|
||||
hbType?: string
|
||||
hbStatus?: string
|
||||
receiveStatus?: string
|
||||
/** `hb_status` / `receive_status` 展示文案(只读)。 */
|
||||
redPacketStatusText?: string
|
||||
}
|
||||
type ForwardedMessageItem = {
|
||||
messageType: number
|
||||
@@ -51,6 +87,7 @@ type RedPacketContent = {
|
||||
title: string
|
||||
description?: string
|
||||
url?: string
|
||||
pay?: RedPacketPaymentInfo
|
||||
}
|
||||
type VoipContent = { type: 'voip'; duration?: number; status: string; roomType?: number }
|
||||
type ImageContent = {
|
||||
@@ -522,12 +559,25 @@ function parseShareMessage(content: string): ParsedContent {
|
||||
}
|
||||
}
|
||||
|
||||
if (appMsgType === '2001') {
|
||||
if (appMsgType === '2001' || /mmpayhb|receivehongbao|wxpay:\/\/c2cbizmessagehandler\/hongbao/i.test(content)) {
|
||||
const pay = parseWcpayInfo(content)
|
||||
const url = decodeXmlUrl(extractXmlValue(content, 'url')) || undefined
|
||||
return {
|
||||
type: 'redPacket',
|
||||
title: extractXmlValue(content, 'title') || '微信红包',
|
||||
description: extractXmlValue(content, 'des') || '恭喜发财,大吉大利',
|
||||
url: decodeXmlUrl(extractXmlValue(content, 'url')) || undefined
|
||||
title:
|
||||
pay.sendTitle ||
|
||||
pay.receiveTitle ||
|
||||
extractXmlValue(content, 'title') ||
|
||||
'微信红包',
|
||||
description:
|
||||
extractXmlValue(content, 'des') ||
|
||||
pay.sceneText ||
|
||||
'恭喜发财,大吉大利',
|
||||
url,
|
||||
pay: {
|
||||
...pay,
|
||||
sendId: pay.sendId || extractSendIdFromUrl(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -557,7 +607,55 @@ function parseShareMessage(content: string): ParsedContent {
|
||||
url,
|
||||
appname,
|
||||
typeVal,
|
||||
articles: articles.length > 1 ? articles : undefined
|
||||
articles: articles.length > 1 ? articles : undefined,
|
||||
transfer: typeVal === '2000' ? parseWcpayInfo(content) : undefined
|
||||
}
|
||||
}
|
||||
|
||||
/** 只读解析 `<wcpayinfo>`(转账 / 红包展示字段;不做支付)。 */
|
||||
function parseWcpayInfo(content: string): TransferPaymentInfo & RedPacketPaymentInfo {
|
||||
const block = /<wcpayinfo>([\s\S]*?)<\/wcpayinfo>/i.exec(content)?.[1] || content
|
||||
const val = (tag: string): string | undefined => {
|
||||
const raw = extractXmlValue(block, tag)
|
||||
return raw ? decodeXmlEntities(raw) || undefined : undefined
|
||||
}
|
||||
const transferStatus = val('transfer_status')
|
||||
const hbStatus = val('hb_status')
|
||||
const receiveStatus = val('receive_status')
|
||||
return {
|
||||
paySubtype: val('paysubtype'),
|
||||
amountText: val('feedesc'),
|
||||
transcationId: val('transcationid'),
|
||||
transferId: val('transferid'),
|
||||
invalidTime: val('invalidtime'),
|
||||
beginTransferTime: val('begintransfertime'),
|
||||
effectiveDate: val('effectivedate'),
|
||||
payMemo: val('pay_memo') || undefined,
|
||||
receiverUsername: val('receiver_username'),
|
||||
payerUsername: val('payer_username'),
|
||||
transferStatus,
|
||||
transferStatusText: describeTransferStatus(transferStatus),
|
||||
templateId: val('templateid'),
|
||||
receiveTitle: val('receivertitle'),
|
||||
sendTitle: val('sendertitle'),
|
||||
sceneText: val('scenetext'),
|
||||
senderDes: val('senderdes'),
|
||||
receiverDes: val('receiverdes'),
|
||||
iconUrl: decodeXmlUrl(val('iconurl') || '') || undefined,
|
||||
nativeUrl: val('nativeurl'),
|
||||
hbType: val('hb_type'),
|
||||
hbStatus,
|
||||
receiveStatus,
|
||||
redPacketStatusText: describeRedPacketStatus(hbStatus, receiveStatus)
|
||||
}
|
||||
}
|
||||
|
||||
function extractSendIdFromUrl(url?: string): string | undefined {
|
||||
if (!url) return undefined
|
||||
try {
|
||||
return new URL(url).searchParams.get('sendid') || undefined
|
||||
} catch {
|
||||
return /sendid=(\d+)/i.exec(url)?.[1]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Vendored
+39
-1
@@ -155,6 +155,37 @@ import type {
|
||||
TextToSpeechSettingsResult
|
||||
} from '../shared/text-to-speech'
|
||||
|
||||
export type TransferPaymentInfo = {
|
||||
paySubtype?: string
|
||||
amountText?: string
|
||||
transcationId?: string
|
||||
transferId?: string
|
||||
invalidTime?: string
|
||||
beginTransferTime?: string
|
||||
effectiveDate?: string
|
||||
payMemo?: string
|
||||
receiverUsername?: string
|
||||
payerUsername?: string
|
||||
transferStatus?: string
|
||||
transferStatusText?: string
|
||||
}
|
||||
|
||||
export type RedPacketPaymentInfo = {
|
||||
templateId?: string
|
||||
receiveTitle?: string
|
||||
sendTitle?: string
|
||||
sceneText?: string
|
||||
senderDes?: string
|
||||
receiverDes?: string
|
||||
iconUrl?: string
|
||||
nativeUrl?: string
|
||||
sendId?: string
|
||||
hbType?: string
|
||||
hbStatus?: string
|
||||
receiveStatus?: string
|
||||
redPacketStatusText?: string
|
||||
}
|
||||
|
||||
export type ParsedContent =
|
||||
| { type: 'text'; content: string }
|
||||
| { type: 'voice'; duration?: number }
|
||||
@@ -167,6 +198,7 @@ export type ParsedContent =
|
||||
url: string
|
||||
appname?: string
|
||||
typeVal?: string
|
||||
transfer?: TransferPaymentInfo
|
||||
}
|
||||
| {
|
||||
type: 'miniProgram'
|
||||
@@ -178,7 +210,13 @@ export type ParsedContent =
|
||||
thumbDatName?: string
|
||||
thumbDataUrl?: string
|
||||
}
|
||||
| { type: 'redPacket'; title: string; description?: string; url?: string }
|
||||
| {
|
||||
type: 'redPacket'
|
||||
title: string
|
||||
description?: string
|
||||
url?: string
|
||||
pay?: RedPacketPaymentInfo
|
||||
}
|
||||
| { type: 'voip'; duration?: number; status: string; roomType?: number }
|
||||
| { type: 'image'; md5?: string; datName?: string; aeskey?: string; encrypVer?: number }
|
||||
| {
|
||||
|
||||
@@ -158,7 +158,22 @@ function CardBubble({ data }: { data: Extract<ParsedContent, { type: 'card' }> }
|
||||
}
|
||||
|
||||
function ShareBubble({ data }: { data: Extract<ParsedContent, { type: 'share' }> }): JSX.Element {
|
||||
const { title, des, url, appname, articles } = data
|
||||
const { title, des, url, appname, articles, transfer, typeVal } = data
|
||||
|
||||
if (String(typeVal) === '2000' || transfer) {
|
||||
const pay = transfer || {}
|
||||
const meta = [pay.amountText, pay.payMemo ? `备注:${pay.payMemo}` : '', pay.transferStatusText]
|
||||
.filter(Boolean)
|
||||
.join(' · ')
|
||||
return (
|
||||
<div className="transfer-message">
|
||||
<div className="transfer-kicker">微信转账</div>
|
||||
<div className="transfer-title">{title || '微信转账'}</div>
|
||||
<div className="transfer-meta">{meta || des || '转账消息'}</div>
|
||||
{pay.transferId ? <div className="transfer-footer">单号 {pay.transferId}</div> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (articles?.length) {
|
||||
return (
|
||||
@@ -254,6 +269,8 @@ function RedPacketBubble({
|
||||
const handleClick = (): void => {
|
||||
if (data.url) window.open(data.url, '_blank')
|
||||
}
|
||||
const statusText = data.pay?.redPacketStatusText
|
||||
const blurb = data.description || data.pay?.sceneText || '恭喜发财,大吉大利'
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -266,7 +283,7 @@ function RedPacketBubble({
|
||||
</span>
|
||||
<span className="red-packet-copy">
|
||||
<strong>{data.title}</strong>
|
||||
<small>{data.description || '恭喜发财,大吉大利'}</small>
|
||||
<small>{statusText ? `${blurb} · ${statusText}` : blurb}</small>
|
||||
</span>
|
||||
</div>
|
||||
<div className="red-packet-footer">微信红包</div>
|
||||
|
||||
@@ -364,6 +364,43 @@
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.transfer-message {
|
||||
width: min(280px, 48vw);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--wxex-border);
|
||||
border-radius: 6px;
|
||||
background: var(--wxex-bg-elevated);
|
||||
color: var(--wxex-text-primary);
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
.transfer-kicker {
|
||||
color: var(--wxex-text-muted);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.transfer-title {
|
||||
margin-top: 2px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.transfer-meta {
|
||||
margin-top: 6px;
|
||||
color: var(--wxex-text-primary);
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.transfer-footer {
|
||||
margin-top: 8px;
|
||||
color: var(--wxex-text-muted);
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.red-packet-message {
|
||||
width: min(280px, 48vw);
|
||||
overflow: hidden;
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* 转账 / 红包状态文案(只读展示)。
|
||||
* 文案对齐微信 UI 簇(待收款/已收款/…、待领取/已领取/…)。
|
||||
* 数字枚举为推断对齐;未知值原样回退。
|
||||
*/
|
||||
|
||||
const TRANSFER_STATUS_TEXT: Record<string, string> = {
|
||||
'0': '待收款',
|
||||
'1': '已收款,待入账',
|
||||
'2': '已收款',
|
||||
'3': '已退还',
|
||||
'4': '过期未收款,已退还',
|
||||
'5': '你已退还',
|
||||
'6': '已过期',
|
||||
'7': '此笔转账已被撤回',
|
||||
paid: '已收款',
|
||||
received: '已收款',
|
||||
pending: '待收款',
|
||||
unreceived: '待收款',
|
||||
refunded: '已退还',
|
||||
expired: '过期未收款,已退还',
|
||||
withdrawn: '此笔转账已被撤回',
|
||||
revoked: '此笔转账已被撤回',
|
||||
confirmed: '朋友已确认收款',
|
||||
accepted: '已被接收',
|
||||
accepting: '已被接收'
|
||||
}
|
||||
|
||||
const RED_PACKET_STATUS_TEXT: Record<string, string> = {
|
||||
'0': '待领取',
|
||||
'1': '已领取',
|
||||
'2': '已被领完',
|
||||
'3': '已过期',
|
||||
'4': '已被领取',
|
||||
received: '已领取',
|
||||
claimed: '已领取',
|
||||
expired: '已过期',
|
||||
exhausted: '已被领完',
|
||||
pending: '待领取',
|
||||
unclaimed: '待领取'
|
||||
}
|
||||
|
||||
const TRANSFER_STATUS_KEYWORDS: Array<[RegExp, string]> = [
|
||||
[/撤回|withdraw|revoke/i, '此笔转账已被撤回'],
|
||||
[/过期未收款|expired/i, '过期未收款,已退还'],
|
||||
[/已退还|退还|refund/i, '已退还'],
|
||||
[/待入账|pending.?credit/i, '已收款,待入账'],
|
||||
[/已收款|已收|paid|received/i, '已收款'],
|
||||
[/已被接收|accepted|accepting/i, '已被接收'],
|
||||
[/确认收款|confirmed/i, '朋友已确认收款'],
|
||||
[/待收款|请收款|pending|unreceived/i, '待收款'],
|
||||
[/已过期|expired/i, '已过期']
|
||||
]
|
||||
|
||||
const RED_PACKET_STATUS_KEYWORDS: Array<[RegExp, string]> = [
|
||||
[/领完|exhausted/i, '已被领完'],
|
||||
[/已被领取|已领取|received|claimed/i, '已领取'],
|
||||
[/过期|expired/i, '已过期'],
|
||||
[/待领取|未领取|pending|unclaimed/i, '待领取']
|
||||
]
|
||||
|
||||
function describeByMap(
|
||||
raw: string | number | undefined,
|
||||
table: Record<string, string>,
|
||||
keywords: Array<[RegExp, string]>
|
||||
): string | undefined {
|
||||
if (raw === undefined || raw === null || raw === '') return undefined
|
||||
const text = String(raw).trim()
|
||||
if (!text) return undefined
|
||||
const direct = table[text.toLowerCase()] || table[text]
|
||||
if (direct) return direct
|
||||
for (const [re, label] of keywords) {
|
||||
if (re.test(text)) return label
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
/** `transfer_status` → 展示文案(未知原样返回)。 */
|
||||
export function describeTransferStatus(raw?: string | number): string | undefined {
|
||||
return describeByMap(raw, TRANSFER_STATUS_TEXT, TRANSFER_STATUS_KEYWORDS)
|
||||
}
|
||||
|
||||
/** 红包 `hb_status` / `receive_status` → 展示文案。 */
|
||||
export function describeRedPacketStatus(
|
||||
hbStatus?: string | number,
|
||||
receiveStatus?: string | number
|
||||
): string | undefined {
|
||||
return (
|
||||
describeByMap(hbStatus, RED_PACKET_STATUS_TEXT, RED_PACKET_STATUS_KEYWORDS) ||
|
||||
describeByMap(receiveStatus, RED_PACKET_STATUS_TEXT, RED_PACKET_STATUS_KEYWORDS)
|
||||
)
|
||||
}
|
||||
|
||||
/** `paysubtype` 仅作辅助标签(真机见过 1/3/4;语义未在 dylib 钉死)。 */
|
||||
export function describePaySubtype(raw?: string | number): string | undefined {
|
||||
if (raw === undefined || raw === null || raw === '') return undefined
|
||||
const text = String(raw).trim()
|
||||
const table: Record<string, string> = {
|
||||
'1': '转账',
|
||||
'3': '收款',
|
||||
'4': '转账'
|
||||
}
|
||||
return table[text] || text
|
||||
}
|
||||
|
||||
export const TRANSFER_STATUS_LABELS = TRANSFER_STATUS_TEXT
|
||||
export const RED_PACKET_STATUS_LABELS = RED_PACKET_STATUS_TEXT
|
||||
@@ -97,6 +97,43 @@ type ShareContent = {
|
||||
appname?: string
|
||||
typeVal?: string
|
||||
articles?: ShareArticle[]
|
||||
/** `wcpayinfo` when typeVal === '2000' (微信转账). Display/export only. */
|
||||
transfer?: TransferPaymentInfo
|
||||
}
|
||||
|
||||
/** 转账 `wcpayinfo` 字段(只读展示;不含支付操作)。 */
|
||||
export type TransferPaymentInfo = {
|
||||
paySubtype?: string
|
||||
amountText?: string
|
||||
transcationId?: string
|
||||
transferId?: string
|
||||
invalidTime?: string
|
||||
beginTransferTime?: string
|
||||
effectiveDate?: string
|
||||
payMemo?: string
|
||||
receiverUsername?: string
|
||||
payerUsername?: string
|
||||
transferStatus?: string
|
||||
/** `transfer_status` 展示文案(只读)。 */
|
||||
transferStatusText?: string
|
||||
}
|
||||
|
||||
/** 红包 `wcpayinfo` 字段(只读展示)。 */
|
||||
export type RedPacketPaymentInfo = {
|
||||
templateId?: string
|
||||
receiveTitle?: string
|
||||
sendTitle?: string
|
||||
sceneText?: string
|
||||
senderDes?: string
|
||||
receiverDes?: string
|
||||
iconUrl?: string
|
||||
nativeUrl?: string
|
||||
sendId?: string
|
||||
hbType?: string
|
||||
hbStatus?: string
|
||||
receiveStatus?: string
|
||||
/** 展示文案(只读)。 */
|
||||
redPacketStatusText?: string
|
||||
}
|
||||
export type ForwardedMessageItem = {
|
||||
messageType: number
|
||||
@@ -126,6 +163,8 @@ type RedPacketContent = {
|
||||
title: string
|
||||
description?: string
|
||||
url?: string
|
||||
/** `wcpayinfo` 展示字段(只读)。 */
|
||||
pay?: RedPacketPaymentInfo
|
||||
}
|
||||
type VoipContent = { type: 'voip'; duration?: number; status: string; roomType?: number }
|
||||
type ImageContent = {
|
||||
|
||||
@@ -457,6 +457,74 @@ describe('export media', () => {
|
||||
expect(html).toContain("if (event.key === 'Escape')")
|
||||
expect(html).toContain('closeLightbox()')
|
||||
})
|
||||
|
||||
it('always surfaces transfer and red-packet amount, memo and status wording', () => {
|
||||
const html = renderExportPage('支付可读导出')
|
||||
const dom = new JSDOM(html, { runScripts: 'outside-only' })
|
||||
const messages: Message[] = [
|
||||
{
|
||||
...messageForArchive('transfer-paid', 'fixture', '支付可读导出', '收到转账2900.00元。', 1),
|
||||
type: '转账',
|
||||
contentData: {
|
||||
type: 'share',
|
||||
typeVal: '2000',
|
||||
title: '微信转账',
|
||||
des: '收到转账2900.00元。',
|
||||
url: '',
|
||||
transfer: {
|
||||
amountText: '¥2900.00',
|
||||
payMemo: '房租',
|
||||
transferStatus: '2',
|
||||
transferStatusText: '已收款',
|
||||
transferId: '1000050001202609130232821248942',
|
||||
paySubtype: '3'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
...messageForArchive('hb-claimed', 'fixture', '支付可读导出', '我给你发了一个红包,赶紧去拆!', 2),
|
||||
type: '微信红包',
|
||||
contentData: {
|
||||
type: 'redPacket',
|
||||
title: '中秋快乐',
|
||||
description: '我给你发了一个红包,赶紧去拆!',
|
||||
pay: {
|
||||
sendTitle: '中秋快乐',
|
||||
sceneText: '微信红包',
|
||||
hbStatus: '1',
|
||||
redPacketStatusText: '已领取',
|
||||
sendId: '1000039801202609247176036834007',
|
||||
hbType: '1'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
Object.assign(dom.window, {
|
||||
__WECHAT_EXPORT__: {
|
||||
version: 1,
|
||||
sourceId: 'fixture',
|
||||
name: '支付可读导出',
|
||||
exportedAt: '2026-09-24T12:00:00.000Z',
|
||||
messages
|
||||
}
|
||||
})
|
||||
|
||||
dom.window.eval(inlineScriptOf(html))
|
||||
|
||||
const cards = [...dom.window.document.querySelectorAll('.payment-content')]
|
||||
expect(cards).toHaveLength(2)
|
||||
const transfer = cards.find((node) => node.classList.contains('transfer'))!
|
||||
const redPacket = cards.find((node) => node.classList.contains('red-packet'))!
|
||||
// 即使消息带 des,金额/备注/状态也不能被盖住。
|
||||
expect(transfer.textContent).toContain('¥2900.00')
|
||||
expect(transfer.textContent).toContain('备注:房租')
|
||||
expect(transfer.textContent).toContain('已收款')
|
||||
expect(transfer.textContent).toContain('收到转账2900.00元。')
|
||||
expect(transfer.textContent).toContain('单号 1000050001202609130232821248942')
|
||||
expect(redPacket.textContent).toContain('已领取')
|
||||
expect(redPacket.textContent).toContain('我给你发了一个红包,赶紧去拆!')
|
||||
dom.window.close()
|
||||
})
|
||||
})
|
||||
|
||||
function messageForArchive(
|
||||
|
||||
@@ -206,4 +206,88 @@ describe('message parser', () => {
|
||||
messageType: 999
|
||||
})
|
||||
})
|
||||
|
||||
it('parses transfer wcpayinfo fields (type 2000)', () => {
|
||||
// 2026-09 真机转账采样字段名(含微信原文 transcationid 拼写)
|
||||
const xml = [
|
||||
'<msg><appmsg appid="" sdkver="">',
|
||||
'<title><![CDATA[微信转账]]></title>',
|
||||
'<des><![CDATA[收到转账2900.00元。]]></des>',
|
||||
'<type>2000</type>',
|
||||
'<url><![CDATA[https://support.weixin.qq.com/upgrade]]></url>',
|
||||
'<wcpayinfo>',
|
||||
'<paysubtype>3</paysubtype>',
|
||||
'<feedesc><![CDATA[¥2900.00]]></feedesc>',
|
||||
'<transcationid><![CDATA[53010003370089202609134087894875]]></transcationid>',
|
||||
'<transferid><![CDATA[1000050001202609130232821248942]]></transferid>',
|
||||
'<invalidtime><![CDATA[1789348128]]></invalidtime>',
|
||||
'<begintransfertime><![CDATA[1789261728]]></begintransfertime>',
|
||||
'<effectivedate><![CDATA[1]]></effectivedate>',
|
||||
'<pay_memo><![CDATA[房租]]></pay_memo>',
|
||||
'<receiver_username><![CDATA[wxid_receiver]]></receiver_username>',
|
||||
'<payer_username><![CDATA[]]></payer_username>',
|
||||
'<transfer_status>2</transfer_status>',
|
||||
'</wcpayinfo></appmsg></msg>'
|
||||
].join('')
|
||||
const parsed = parseMessageContent(xml, 49)
|
||||
expect(parsed).toMatchObject({
|
||||
type: 'share',
|
||||
typeVal: '2000',
|
||||
title: '微信转账',
|
||||
transfer: {
|
||||
paySubtype: '3',
|
||||
amountText: '¥2900.00',
|
||||
transcationId: '53010003370089202609134087894875',
|
||||
transferId: '1000050001202609130232821248942',
|
||||
invalidTime: '1789348128',
|
||||
beginTransferTime: '1789261728',
|
||||
effectiveDate: '1',
|
||||
payMemo: '房租',
|
||||
receiverUsername: 'wxid_receiver',
|
||||
transferStatus: '2',
|
||||
transferStatusText: '已收款'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('parses red packet wcpayinfo fields (type 2001 / mmpayhb)', () => {
|
||||
const xml = [
|
||||
'<msg><appmsg appid="" sdkver="">',
|
||||
'<title><![CDATA[中秋快乐]]></title>',
|
||||
'<des><![CDATA[我给你发了一个红包,赶紧去拆!]]></des>',
|
||||
'<type>2001</type>',
|
||||
'<wcpayinfo>',
|
||||
'<templateid><![CDATA[7a2a165d31da7fce6dd77e05c300028a]]></templateid>',
|
||||
'<url><![CDATA[https://wxapp.tenpay.com/mmpayhb/wxhb_personalreceive?msgtype=1&sendid=1000039801202609247176036834007&sign=abc]]></url>',
|
||||
'<iconurl><![CDATA[https://wx.gtimg.com/hongbao/1800/hb.png]]></iconurl>',
|
||||
'<receivertitle><![CDATA[恭喜发财,大吉大利]]></receivertitle>',
|
||||
'<sendertitle><![CDATA[中秋快乐,望重置]]></sendertitle>',
|
||||
'<scenetext><![CDATA[微信红包]]></scenetext>',
|
||||
'<senderdes><![CDATA[查看红包]]></senderdes>',
|
||||
'<receiverdes><![CDATA[领取红包]]></receiverdes>',
|
||||
'<nativeurl><![CDATA[wxpay://c2cbizmessagehandler/hongbao/receivehongbao?sendid=1]]></nativeurl>',
|
||||
'<hb_type>1</hb_type>',
|
||||
'<hb_status>1</hb_status>',
|
||||
'<receive_status>2</receive_status>',
|
||||
'</wcpayinfo></appmsg></msg>'
|
||||
].join('')
|
||||
const parsed = parseMessageContent(xml, 49)
|
||||
expect(parsed).toMatchObject({
|
||||
type: 'redPacket',
|
||||
title: '中秋快乐,望重置',
|
||||
pay: {
|
||||
templateId: '7a2a165d31da7fce6dd77e05c300028a',
|
||||
receiveTitle: '恭喜发财,大吉大利',
|
||||
sendTitle: '中秋快乐,望重置',
|
||||
sceneText: '微信红包',
|
||||
senderDes: '查看红包',
|
||||
receiverDes: '领取红包',
|
||||
sendId: '1000039801202609247176036834007',
|
||||
hbType: '1',
|
||||
hbStatus: '1',
|
||||
receiveStatus: '2',
|
||||
redPacketStatusText: '已领取'
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
describePaySubtype,
|
||||
describeRedPacketStatus,
|
||||
describeTransferStatus
|
||||
} from '../../src/shared/payment-status'
|
||||
|
||||
describe('payment status labels', () => {
|
||||
it('maps transfer_status numbers and keywords to UI wording', () => {
|
||||
expect(describeTransferStatus(2)).toBe('已收款')
|
||||
expect(describeTransferStatus('3')).toBe('已退还')
|
||||
expect(describeTransferStatus('paid')).toBe('已收款')
|
||||
expect(describeTransferStatus('收款方过期未收款,已退还')).toBe('过期未收款,已退还')
|
||||
expect(describeTransferStatus(undefined)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('maps red packet hb_status / receive_status', () => {
|
||||
expect(describeRedPacketStatus('0', undefined)).toBe('待领取')
|
||||
expect(describeRedPacketStatus('1', undefined)).toBe('已领取')
|
||||
expect(describeRedPacketStatus(undefined, '2')).toBe('已被领完')
|
||||
expect(describeRedPacketStatus('已被领取', undefined)).toBe('已领取')
|
||||
expect(describeRedPacketStatus(undefined, undefined)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('passes through unknown status values', () => {
|
||||
expect(describeTransferStatus('99-custom')).toBe('99-custom')
|
||||
expect(describeRedPacketStatus('99', '98')).toBe('99')
|
||||
expect(describePaySubtype('3')).toBe('收款')
|
||||
expect(describePaySubtype('9')).toBe('9')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user