fix: 修复日报生成时群成员名称显示错误

This commit is contained in:
wushili
2026-08-11 13:12:22 +08:00
parent 775b5aff18
commit 4d95fd7650
2 changed files with 292 additions and 148 deletions
+179 -148
View File
@@ -58,6 +58,17 @@ export interface Wcdb4GroupMember {
m_nsHeadImgUrl: string
}
/**
* 联系人表中独立保存的成员名称字段。
*
* 群成员接口的 nickname 在部分微信数据版本中会被通讯录备注覆盖,因此不能用它
* 推断微信昵称或备注。
*/
type Wcdb4ContactMemberNames = {
wechatNickname: string
remark: string
}
export interface Wcdb4ImageHardlink {
file_name?: string
full_path?: string
@@ -1684,76 +1695,16 @@ export class Wcdb4Client {
const rows = this.callJson<Record<string, unknown>[]>((handle, outJson) =>
this.wcdbGetGroupMembers!(handle, chatroomId, outJson)
)
const members = (Array.isArray(rows) ? rows : []).map((row) => {
const username = this.pickString(row, [
'username',
'userName',
'user_name',
'member_username',
'm_nsUsrName'
])
const wechatNickname = this.pickString(row, [
'nickname',
'nickName',
'wechatNickname',
'wechat_nickname',
'm_nsNickName'
])
const remark = this.pickString(row, [
'remark',
'remarkName',
'remark_name',
'contactRemark',
'contact_remark'
])
const rowGroupNickname = this.pickString(row, [
'groupNickname',
'group_nickname',
'displayName',
'display_name'
])
const memberNickname = this.pickString(row, ['name'])
const avatar = this.pickString(row, [
'avatarUrl',
'avatar_url',
'headImgUrl',
'm_nsHeadImgUrl'
])
if (username) {
if (avatar) this.avatarCache.set(username, avatar)
}
return {
m_nsUsrName: username,
nickname: wechatNickname || memberNickname || remark || rowGroupNickname,
groupNickname: groupNicknames.get(username) || rowGroupNickname,
wechatNickname: wechatNickname || memberNickname,
remark,
m_nsHeadImgUrl: avatar
}
})
const missingDisplayNames = members
.filter((member) => !member.nickname)
.map((member) => member.m_nsUsrName)
.filter(Boolean)
this.hydrateDisplayNames(missingDisplayNames)
const memberRows = Array.isArray(rows) ? rows : []
const contactNames = this.readContactMemberNames(this.groupMemberUsernames(memberRows))
const members = this.normalizeGroupMembers(memberRows, groupNicknames, contactNames)
this.hydrateAvatarUrls(
members
.filter((member) => !member.m_nsHeadImgUrl)
.map((member) => member.m_nsUsrName)
.filter(Boolean)
)
return members.map((member) => ({
...member,
nickname:
member.nickname || this.displayNameCache.get(member.m_nsUsrName) || member.m_nsUsrName,
wechatNickname:
member.wechatNickname || this.displayNameCache.get(member.m_nsUsrName) || '',
m_nsHeadImgUrl: member.m_nsHeadImgUrl || this.avatarCache.get(member.m_nsUsrName) || ''
}))
return this.withCachedGroupMemberAvatars(members)
} catch {
return []
}
@@ -1827,76 +1778,98 @@ export class Wcdb4Client {
this.wcdbGetGroupMembers as unknown as KoffiAsyncFunction,
chatroomId
)
const members = (Array.isArray(rows) ? rows : []).map((row) => {
const username = this.pickString(row, [
const memberRows = Array.isArray(rows) ? rows : []
const contactNames = await this.readContactMemberNamesAsync(
this.groupMemberUsernames(memberRows)
)
const members = this.normalizeGroupMembers(memberRows, groupNicknames, contactNames)
const missingAvatars = members
.filter((member) => !member.m_nsHeadImgUrl)
.map((member) => member.m_nsUsrName)
.filter(Boolean)
await this.hydrateAvatarUrlsAsync(missingAvatars)
return this.withCachedGroupMemberAvatars(members)
} catch (error) {
console.warn(`[WCDB4] async group members failed chatroom=${chatroomId}:`, error)
return []
}
}
/**
* 将群成员接口与联系人表组装为语义明确的成员快照。
*
* 群昵称只接受群聊数据;微信昵称与通讯录备注只接受 contact 表。即使群成员
* 接口返回了 nickname,也不能作为名称字段的回退,以免把通讯录备注误展示为微信昵称。
*/
private normalizeGroupMembers(
rows: Record<string, unknown>[],
groupNicknames: Map<string, string>,
contactNames: Map<string, Wcdb4ContactMemberNames>
): Wcdb4GroupMember[] {
return rows.map((row) => {
const username = this.pickString(row, [
'username',
'userName',
'user_name',
'member_username',
'm_nsUsrName'
])
const rowGroupNickname = this.pickString(row, [
'groupNickname',
'group_nickname',
'displayName',
'display_name'
])
const groupNickname = groupNicknames.get(username) || rowGroupNickname
const contact = contactNames.get(username)
const wechatNickname = contact?.wechatNickname || ''
const remark = contact?.remark || ''
const avatar = this.pickString(row, [
'avatarUrl',
'avatar_url',
'headImgUrl',
'm_nsHeadImgUrl'
])
if (username && avatar) this.avatarCache.set(username, avatar)
return {
m_nsUsrName: username,
// nickname 是旧调用方使用的通用显示字段,保持安全的名称优先级。
nickname: wechatNickname || groupNickname || username,
groupNickname,
wechatNickname,
remark,
m_nsHeadImgUrl: avatar
}
})
}
/**
* 从原始群成员行提取联系人查询键,避免把空值传进 SQL IN 条件。
*/
private groupMemberUsernames(rows: Record<string, unknown>[]): string[] {
return rows
.map((row) =>
this.pickString(row, [
'username',
'userName',
'user_name',
'member_username',
'm_nsUsrName'
])
const wechatNickname = this.pickString(row, [
'nickname',
'nickName',
'wechatNickname',
'wechat_nickname',
'm_nsNickName'
])
const remark = this.pickString(row, [
'remark',
'remarkName',
'remark_name',
'contactRemark',
'contact_remark'
])
const rowGroupNickname = this.pickString(row, [
'groupNickname',
'group_nickname',
'displayName',
'display_name'
])
const memberNickname = this.pickString(row, ['name'])
const avatar = this.pickString(row, [
'avatarUrl',
'avatar_url',
'headImgUrl',
'm_nsHeadImgUrl'
])
if (username && avatar) this.avatarCache.set(username, avatar)
return {
m_nsUsrName: username,
nickname: wechatNickname || memberNickname || remark || rowGroupNickname,
groupNickname: groupNicknames.get(username) || rowGroupNickname,
wechatNickname: wechatNickname || memberNickname,
remark,
m_nsHeadImgUrl: avatar
}
})
)
.filter(Boolean)
}
const missingNames = members
.filter((member) => !member.nickname)
.map((member) => member.m_nsUsrName)
.filter(Boolean)
const missingAvatars = members
.filter((member) => !member.m_nsHeadImgUrl)
.map((member) => member.m_nsUsrName)
.filter(Boolean)
await Promise.all([
this.hydrateDisplayNamesAsync(missingNames),
this.hydrateAvatarUrlsAsync(missingAvatars)
])
return members.map((member) => ({
...member,
nickname:
member.nickname || this.displayNameCache.get(member.m_nsUsrName) || member.m_nsUsrName,
wechatNickname:
member.wechatNickname || this.displayNameCache.get(member.m_nsUsrName) || '',
m_nsHeadImgUrl: member.m_nsHeadImgUrl || this.avatarCache.get(member.m_nsUsrName) || ''
}))
} catch (error) {
console.warn(`[WCDB4] async group members failed chatroom=${chatroomId}:`, error)
return []
}
/**
* 为已标准化的成员补充头像缓存,不参与名称字段的回退。
*/
private withCachedGroupMemberAvatars(members: Wcdb4GroupMember[]): Wcdb4GroupMember[] {
return members.map((member) => ({
...member,
m_nsHeadImgUrl: member.m_nsHeadImgUrl || this.avatarCache.get(member.m_nsUsrName) || ''
}))
}
getGroupNicknames(chatroomId: string): Map<string, string> {
@@ -2667,27 +2640,6 @@ export class Wcdb4Client {
return false
}
private hydrateDisplayNames(usernames: string[]): void {
if (!this.wcdbGetDisplayNames) return
const missing = this.uniq(usernames).filter((username) => !this.displayNameCache.has(username))
if (missing.length === 0) return
try {
const rows = this.callJson<Record<string, string> | Record<string, unknown>[]>(
(handle, outJson) => this.wcdbGetDisplayNames!(handle, JSON.stringify(missing), outJson)
)
this.readStringMap(rows, [
'nickname',
'displayName',
'display_name',
'remark',
'name'
]).forEach((name, username) => this.displayNameCache.set(username, name))
} catch {
// Names are optional; usernames are still enough to load chats.
}
}
private async hydrateDisplayNamesAsync(usernames: string[]): Promise<void> {
if (!this.wcdbGetDisplayNames) return
const missing = this.uniq(usernames).filter((username) => !this.displayNameCache.has(username))
@@ -2793,6 +2745,85 @@ export class Wcdb4Client {
}
}
/**
* 从 contact 表读取群成员的微信昵称与通讯录备注。
*
* 查询失败时返回空映射:群成员快照仍可用,并由调用方回退到群昵称或 wxid;
* 不得使用群成员接口的 nickname,因为该字段可能实际保存的是通讯录备注。
*/
private readContactMemberNames(usernames: string[]): Map<string, Wcdb4ContactMemberNames> {
const result = new Map<string, Wcdb4ContactMemberNames>()
if (!this.wcdbExecQuery || usernames.length === 0) return result
const inList = this.uniq(usernames)
.map((username) => `'${username.replace(/'/g, "''")}'`)
.join(',')
if (!inList) return result
try {
const rows = this.callJson<Record<string, unknown>[]>((handle, outJson) =>
this.wcdbExecQuery!(
handle,
'contact',
'',
`SELECT username, nick_name, remark FROM contact WHERE username IN (${inList})`,
outJson
)
)
return this.contactMemberNamesFromRows(rows)
} catch {
return result
}
}
/**
* 异步读取 contact 表的成员名称字段,语义与同步路径保持一致。
*/
private async readContactMemberNamesAsync(
usernames: string[]
): Promise<Map<string, Wcdb4ContactMemberNames>> {
const result = new Map<string, Wcdb4ContactMemberNames>()
if (!this.wcdbExecQuery || usernames.length === 0) return result
const inList = this.uniq(usernames)
.map((username) => `'${username.replace(/'/g, "''")}'`)
.join(',')
if (!inList) return result
try {
const rows = await this.callJsonAsync<Record<string, unknown>[]>(
this.wcdbExecQuery as unknown as KoffiAsyncFunction,
'contact',
'',
`SELECT username, nick_name, remark FROM contact WHERE username IN (${inList})`
)
return this.contactMemberNamesFromRows(rows)
} catch {
return result
}
}
/**
* 将 contact 查询行转换为按 wxid 索引的名称映射。
*/
private contactMemberNamesFromRows(
rows: Record<string, unknown>[]
): Map<string, Wcdb4ContactMemberNames> {
const result = new Map<string, Wcdb4ContactMemberNames>()
if (!Array.isArray(rows)) return result
for (const row of rows) {
const username = this.pickString(row, ['username', 'user_name', 'userName', 'm_nsUsrName'])
if (!username) continue
result.set(username, {
wechatNickname: this.pickString(row, ['nick_name', 'nickName', 'm_nsNickName']),
remark: this.pickString(row, ['remark', 'remark_name', 'remarkName'])
})
}
return result
}
private readContactAvatarUrls(usernames: string[]): Map<string, string> {
const result = new Map<string, string>()
if (!this.wcdbExecQuery || usernames.length === 0) return result