@aochuang/client-sdk 1.0.366 → 1.0.368

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aochuang/client-sdk",
3
- "version": "1.0.366",
3
+ "version": "1.0.368",
4
4
  "description": "",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1"
@@ -30,7 +30,8 @@ export default class ClientChatAccount {
30
30
  onAccountRemoved: null,
31
31
  onUnreadMessageNumChange: null,
32
32
  onDeleteAccountSession: null,
33
- onAccountLoaded: null
33
+ onAccountLoaded: null,
34
+ onUnreadMessageNumLog: null
34
35
  }, options)
35
36
  if (!options.loadAccountDataMethod) {
36
37
  throw new Error('缺少loadAccountDataMethod配置')
@@ -51,6 +52,19 @@ export default class ClientChatAccount {
51
52
  this.onUnreadMessageNumChange = options.onUnreadMessageNumChange
52
53
  this.onDeleteAccountSession = options.onDeleteAccountSession
53
54
  this.onAccountLoaded = options.onAccountLoaded
55
+ this.onUnreadMessageNumLog = options.onUnreadMessageNumLog
56
+ }
57
+
58
+ _addUnreadMessageNumLog (title, data) {
59
+ if (this.onUnreadMessageNumLog) {
60
+ this.onUnreadMessageNumLog({
61
+ group: 'client-chat-account:unreadMessageNum',
62
+ content: {
63
+ title,
64
+ data
65
+ }
66
+ })
67
+ }
54
68
  }
55
69
 
56
70
  get accountData () {
@@ -198,6 +212,10 @@ export default class ClientChatAccount {
198
212
  })
199
213
  }
200
214
  this.data.accountUnreadMessageNumData = nextData
215
+ this._addUnreadMessageNumLog('服务端全量刷新账号未读数量', {
216
+ source: 'refreshAll',
217
+ accounts: nextData
218
+ })
201
219
  })
202
220
  }
203
221
 
@@ -216,23 +234,33 @@ export default class ClientChatAccount {
216
234
  return true
217
235
  }
218
236
  })
237
+ const before = index >= 0 ? this.data.accountUnreadMessageNumData[index] : null
238
+ const after = {
239
+ id: wechatAccountId,
240
+ num: rs.data[0].unread || 0,
241
+ muteNum: rs.data[0].muteUnread || 0
242
+ }
219
243
  if (index >= 0) {
220
- this.data.accountUnreadMessageNumData.splice(index, 1, {
221
- id: wechatAccountId,
222
- num: rs.data[0].unread || 0,
223
- muteNum: rs.data[0].muteUnread || 0
224
- })
244
+ this.data.accountUnreadMessageNumData.splice(index, 1, after)
225
245
  } else {
226
- this.data.accountUnreadMessageNumData.push({
227
- id: wechatAccountId,
228
- num: rs.data[0].unread || 0,
229
- muteNum: rs.data[0].muteUnread || 0
230
- })
246
+ this.data.accountUnreadMessageNumData.push(after)
231
247
  }
248
+ this._addUnreadMessageNumLog('服务端刷新账号未读数量', {
249
+ source: 'refresh',
250
+ wechatAccountId,
251
+ before: before ? { num: before.num, muteNum: before.muteNum } : null,
252
+ after: { num: after.num, muteNum: after.muteNum }
253
+ })
232
254
  })
233
255
  }
234
256
 
235
- updateAccountUnreadMessageNum (accountId, callback) {
257
+ /**
258
+ * 本地增量更新账号未读数量
259
+ * @param { String } accountId
260
+ * @param { Function } callback 返回 { num, muteNum }
261
+ * @param { String } source 调用来源标记,仅用于日志排查
262
+ */
263
+ updateAccountUnreadMessageNum (accountId, callback, source) {
236
264
  return this.getAccount(accountId).then(rs => {
237
265
  if (!rs || !rs.data) {
238
266
  console.warn('未找到' + accountId + '工作微信号')
@@ -248,11 +276,26 @@ export default class ClientChatAccount {
248
276
  return true
249
277
  }
250
278
  })
279
+ const beforeNum = unreadMessageNumIndex >= 0 ? this.data.accountUnreadMessageNumData[unreadMessageNumIndex].num : 0
280
+ const beforeMuteNum = unreadMessageNumIndex >= 0 ? this.data.accountUnreadMessageNumData[unreadMessageNumIndex].muteNum : 0
251
281
  const nextReadMessageNum = callback({
252
282
  account: rs.data,
253
- unreadMesasgeNum: unreadMessageNumIndex >= 0 ? this.data.accountUnreadMessageNumData[unreadMessageNumIndex].num : 0,
254
- unreadMessageMuteNum: unreadMessageNumIndex >= 0 ? this.data.accountUnreadMessageNumData[unreadMessageNumIndex].muteNum : 0
283
+ unreadMesasgeNum: beforeNum,
284
+ unreadMessageMuteNum: beforeMuteNum
255
285
  })
286
+ if (this.onUnreadMessageNumLog && nextReadMessageNum !== undefined && nextReadMessageNum !== null) {
287
+ const afterNum = typeof nextReadMessageNum === 'object' ? nextReadMessageNum.num : nextReadMessageNum
288
+ const afterMuteNum = typeof nextReadMessageNum === 'object' ? nextReadMessageNum.muteNum : beforeMuteNum
289
+ if (afterNum !== beforeNum || afterMuteNum !== beforeMuteNum) {
290
+ this._addUnreadMessageNumLog('本地增量更新账号未读数量', {
291
+ source: source || 'unknown',
292
+ wechatAccountId: accountId,
293
+ before: { num: beforeNum, muteNum: beforeMuteNum },
294
+ after: { num: afterNum, muteNum: afterMuteNum },
295
+ delta: { num: (afterNum || 0) - (beforeNum || 0), muteNum: (afterMuteNum || 0) - (beforeMuteNum || 0) }
296
+ })
297
+ }
298
+ }
256
299
  if (nextReadMessageNum && typeof nextReadMessageNum === 'object') {
257
300
  /**
258
301
  * {
@@ -610,6 +610,10 @@ export default class ClientChatMessage {
610
610
  if (!groupField || !messageTime || (message && message.msgType === '_EMPTY_')) {
611
611
  return
612
612
  }
613
+ // 临时占位消息的wechatTime是浏览器本地时间,和手机端的消息时间不是同一个时钟,不能作为判断旧消息的基准,否则时钟偏差会导致对方紧接着的回复被当成旧消息过滤掉
614
+ if (message && message._temp) {
615
+ return
616
+ }
613
617
  const lastMessageTime = this.getLastMessageTime(groupField)
614
618
  if (!lastMessageTime || messageTime > lastMessageTime) {
615
619
  this.lastMessageTimeMap[groupField] = messageTime
@@ -311,11 +311,22 @@ export default class ClientChatSession {
311
311
  if (!rs.data) {
312
312
  return Promise.reject('未找到会话' + sessionId)
313
313
  }
314
+ /**
315
+ * 轮询合并上下文,会依次传给每个 serializeMethod(见 client-chat.js onCmdNewMessage 里 getSession 的回调)
316
+ * baseLastMsgTime 服务端返回的会话 lastMsgTime,用来判断某条消息服务端是否已经算进 unreadMsgCount
317
+ * processedMessageIds 本次轮询期间处理过的所有消息 id
318
+ * skippedMessageIds 服务端返回的会话已经包含(wechatTime <= baseLastMsgTime)、本地不再给会话未读 +1 的消息 id
319
+ * unreadMessageIds 本地给会话未读 +1 的消息 id
320
+ * accountUnreadDelta 本次轮询期间需要累加到工作微信号(账号)未读数量上的条数。
321
+ * 注意它和 unreadMessageIds.length 不同:账号未读初始值是登录时取的,不包含本次收到的任何消息,
322
+ * 所以被 skip 的消息也要算进来;只有"正在看当前会话被直接标记已读"的消息不算
323
+ */
314
324
  const context = {
315
325
  baseLastMsgTime: rs.data.lastMsgTime || 0,
316
326
  processedMessageIds: [],
317
327
  skippedMessageIds: [],
318
- unreadMessageIds: []
328
+ unreadMessageIds: [],
329
+ accountUnreadDelta: 0
319
330
  }
320
331
  const serializeTasks = pollRequest.serializeTasks.slice().sort((a, b) => {
321
332
  return a.wechatTime - b.wechatTime || a.index - b.index
@@ -326,7 +337,11 @@ export default class ClientChatSession {
326
337
  context.processedMessageIds.push(task.messageId)
327
338
  }
328
339
  })
329
- this.addSession(rs.data, null)
340
+ // wechatAccountId 传 null 表示非分页来源,onAddSession 会把未读同步到账号;
341
+ // 同时带上 unreadDelta,让账号只累加本次新增的条数,而不是服务端返回的整个 unreadMsgCount(那里面的存量部分账号初始值已经包含了)
342
+ this.addSession(rs.data, null, {
343
+ unreadDelta: context.accountUnreadDelta
344
+ })
330
345
  const wechatAccountId = rs.data.wechatAccountId
331
346
  this._addGetSessionLog('会话轮询合并完成', {
332
347
  sessionId,
@@ -341,6 +356,7 @@ export default class ClientChatSession {
341
356
  processedMessageCount: context.processedMessageIds.length,
342
357
  skippedMessageCount: context.skippedMessageIds.length,
343
358
  unreadMessageCount: context.unreadMessageIds.length,
359
+ accountUnreadDelta: context.accountUnreadDelta,
344
360
  hasSessionMap: !!this.sessionMap[sessionId],
345
361
  hasAllSessionGroup: !!this._getSessionBySessionGroup(sessionId, ALL_WECHAT_ACCOUNT_ID),
346
362
  hasAccountSessionGroup: !!this._getSessionBySessionGroup(sessionId, wechatAccountId)
@@ -486,8 +502,10 @@ export default class ClientChatSession {
486
502
  * "unreadMsgCount": 3,
487
503
  * "wechatAccountId": "2a39a725814acf07502299ab2285fff8d44377f5"
488
504
  * }
505
+ * @params { Object } - options
506
+ * @params { Number } - options.unreadDelta 本次新增的未读条数,新消息触发getSession拉回会话时传入,用于账号未读数量只累加新增部分而不是整个unreadMsgCount
489
507
  */
490
- addSession (data, wechatAccountId) {
508
+ addSession (data, wechatAccountId, options = {}) {
491
509
  if (!data || !data.conversationId) {
492
510
  throw new Error('缺少必须参数conversationId')
493
511
  }
@@ -591,7 +609,8 @@ export default class ClientChatSession {
591
609
  }
592
610
  this.onAddSession && this.onAddSession({
593
611
  wechatAccountId,
594
- session: nextData
612
+ session: nextData,
613
+ unreadDelta: options.unreadDelta
595
614
  })
596
615
  }
597
616
 
@@ -115,23 +115,42 @@ class ClientChat {
115
115
  const mergeSessionCommonOptions = (options, instance) => {
116
116
  let updateSessioning = false
117
117
  let removeSessioning = false
118
- options.onAddSession = ({ session, wechatAccountId }) => {
118
+ /**
119
+ * 会话新增回调,负责把新增会话的未读数量同步到工作微信号(账号)的未读数量上
120
+ * @param { Object } session - 新增的会话
121
+ * @param { String } wechatAccountId - 有值代表是分页加载(loadData/reload)来的会话,账号未读初始值已经包含,不累加
122
+ * @param { Number } unreadDelta - 本次真正新增的未读条数,只有新消息触发 getSession 拉回会话时才会传(见 client-chat-session.js getSession)
123
+ *
124
+ * 账号未读数量的初始值来自登录时的 getAccountUnreadMessageNumMethod,是服务端按该账号"所有会话"算的,
125
+ * 不管本地有没有分页加载到这些会话。所以非分页来源的会话要区分两种情况:
126
+ * 1. 分配过来的会话(loadAssignTaskSessionData):之前不属于这个账号,账号初始值里没有它,
127
+ * 需要把整个 session.unreadMsgCount 累加上去。此时 unreadDelta 为 undefined。
128
+ * 2. 新消息触发 getSession 拉回来的会话:一直属于这个账号,只是本地没翻页加载到,它的存量未读已经在账号初始值里了,
129
+ * 如果再把整个 unreadMsgCount 加上去就会重复计算存量部分(这就是"会话都已读了但账号还有未读数、刷新后归零"的主因)。
130
+ * 这种情况只能累加本次轮询期间新增的条数,即 unreadDelta。
131
+ */
132
+ options.onAddSession = ({ session, wechatAccountId, unreadDelta }) => {
119
133
  // 不是来源于分页的时候,没有wechatAccountId就是代表不是走的分页增加的会话,一般为获取新消息或则分配的会话
120
134
  if (!wechatAccountId && instance === this.session) {
121
- // 更新账号未读消息数量
122
- this.account.updateAccountUnreadMessageNum(session.wechatAccountId, ({ unreadMesasgeNum, unreadMessageMuteNum }) => {
123
- if (session.mute) {
124
- return {
125
- num: (unreadMesasgeNum || 0) + (session.unreadMsgCount || 0),
126
- muteNum: (unreadMessageMuteNum || 0) + (session.unreadMsgCount || 0)
127
- }
128
- } else {
129
- return {
130
- num: (unreadMesasgeNum || 0) + (session.unreadMsgCount || 0),
131
- muteNum: unreadMessageMuteNum
135
+ // unreadDelta 是数字 → getSession 来源,只加新增;否则 → 分配等来源,整个 unreadMsgCount 加上
136
+ const delta = typeof unreadDelta === 'number' ? unreadDelta : (session.unreadMsgCount || 0)
137
+ // delta 为 0 时不更新,避免产生无意义的账号未读更新和日志
138
+ if (delta > 0) {
139
+ // 更新账号未读消息数量
140
+ this.account.updateAccountUnreadMessageNum(session.wechatAccountId, ({ unreadMesasgeNum, unreadMessageMuteNum }) => {
141
+ if (session.mute) {
142
+ return {
143
+ num: (unreadMesasgeNum || 0) + delta,
144
+ muteNum: (unreadMessageMuteNum || 0) + delta
145
+ }
146
+ } else {
147
+ return {
148
+ num: (unreadMesasgeNum || 0) + delta,
149
+ muteNum: unreadMessageMuteNum
150
+ }
132
151
  }
133
- }
134
- })
152
+ }, typeof unreadDelta === 'number' ? 'onAddSession:getSession' : 'onAddSession')
153
+ }
135
154
  }
136
155
  // 同步未读会话
137
156
  if (instance !== this.sessionUnread && session.unreadMsgCount > 0) {
@@ -231,7 +250,7 @@ class ClientChat {
231
250
  muteNum: unreadMessageMuteNum
232
251
  }
233
252
  }
234
- })
253
+ }, 'onRemoveSession')
235
254
  // 删除会话会同步删除所有的实例,所以需要做这个检查
236
255
  if (this.sessionManage.currentSession && this.sessionManage.currentSession.conversationId === session.conversationId) {
237
256
  this.sessionManage.setCurrentSession(null)
@@ -287,7 +306,7 @@ class ClientChat {
287
306
  muteNum: unreadMessageMuteNum
288
307
  }
289
308
  }
290
- })
309
+ }, 'onSessionReadChange')
291
310
  }
292
311
  }
293
312
  this.session = new ClientChatSession({
@@ -445,8 +464,12 @@ class ClientChat {
445
464
  rs.data.lastMsgId = data.id
446
465
  rs.data.lastUpdateTime = data.updateDate
447
466
  }
467
+ // 正在看这个会话并且允许自动标记已读
468
+ const isCurrentSessionRead = this.sessionManage.currentSession && this.sessionManage.currentSession.conversationId === sessionId && this.shouldMarkCurrentSessionRead(sessionId, data)
469
+ // 会话未读数量:isAfterServerMessage 为 true 表示这条消息比服务端返回的会话 lastMsgTime 新,
470
+ // 服务端返回的 unreadMsgCount 还没算它,需要本地 +1;否则服务端已经算进去了,skip 掉避免重复
448
471
  if (!data.isSend && isAfterServerMessage) {
449
- if (this.sessionManage.currentSession && this.sessionManage.currentSession.conversationId === sessionId && this.shouldMarkCurrentSessionRead(sessionId, data)) {
472
+ if (isCurrentSessionRead) {
450
473
  rs.data.unreadMsgCount = 0
451
474
  } else {
452
475
  rs.data.unreadMsgCount = (rs.data.unreadMsgCount || 0) + 1
@@ -455,6 +478,13 @@ class ClientChat {
455
478
  } else {
456
479
  context.skippedMessageIds.push(data.id)
457
480
  }
481
+ // 账号未读数量:和会话未读数量的判断口径不一样。
482
+ // 账号未读初始值是登录时从服务端取的,不包含这条新消息,所以不管这条消息有没有被服务端算进会话的 unreadMsgCount(即不管是否被 skip),
483
+ // 账号都要 +1。唯一不加的情况是"正在看当前会话并被直接标记已读"(此时会话未读被置 0,不会再通过 onSessionReadChange 扣减账号)。
484
+ // 这个累计值会通过 addSession(rs.data, null, { unreadDelta }) 传给 onAddSession,账号只累加这个增量而不是整个 unreadMsgCount
485
+ if (!data.isSend && !(isAfterServerMessage && isCurrentSessionRead)) {
486
+ context.accountUnreadDelta += 1
487
+ }
458
488
  }
459
489
  return rs
460
490
  }, {
@@ -496,7 +526,7 @@ class ClientChat {
496
526
  muteNum: unreadMessageMuteNum
497
527
  }
498
528
  }
499
- })
529
+ }, 'onNewFriendMessage')
500
530
  }
501
531
  }
502
532
  return nextSession
@@ -581,8 +611,12 @@ class ClientChat {
581
611
  rs.data.lastMsgId = data.id
582
612
  rs.data.lastUpdateTime = data.updateDate
583
613
  }
614
+ // 正在看这个会话并且允许自动标记已读
615
+ const isCurrentSessionRead = this.sessionManage.currentSession && this.sessionManage.currentSession.conversationId === sessionId && this.shouldMarkCurrentSessionRead(sessionId, data)
616
+ // 会话未读数量:isAfterServerMessage 为 true 表示这条消息比服务端返回的会话 lastMsgTime 新,
617
+ // 服务端返回的 unreadMsgCount 还没算它,需要本地 +1;否则服务端已经算进去了,skip 掉避免重复
584
618
  if (!data.isSend && isAfterServerMessage) {
585
- if (this.sessionManage.currentSession && this.sessionManage.currentSession.conversationId === sessionId && this.shouldMarkCurrentSessionRead(sessionId, data)) {
619
+ if (isCurrentSessionRead) {
586
620
  rs.data.unreadMsgCount = 0
587
621
  } else {
588
622
  rs.data.unreadMsgCount = (rs.data.unreadMsgCount || 0) + 1
@@ -594,6 +628,13 @@ class ClientChat {
594
628
  } else {
595
629
  context.skippedMessageIds.push(data.id)
596
630
  }
631
+ // 账号未读数量:和会话未读数量的判断口径不一样。
632
+ // 账号未读初始值是登录时从服务端取的,不包含这条新消息,所以不管这条消息有没有被服务端算进会话的 unreadMsgCount(即不管是否被 skip),
633
+ // 账号都要 +1。唯一不加的情况是"正在看当前会话并被直接标记已读"(此时会话未读被置 0,不会再通过 onSessionReadChange 扣减账号)。
634
+ // 这个累计值会通过 addSession(rs.data, null, { unreadDelta }) 传给 onAddSession,账号只累加这个增量而不是整个 unreadMsgCount
635
+ if (!data.isSend && !(isAfterServerMessage && isCurrentSessionRead)) {
636
+ context.accountUnreadDelta += 1
637
+ }
597
638
  }
598
639
  return rs
599
640
  }, {
@@ -638,7 +679,7 @@ class ClientChat {
638
679
  muteNum: unreadMessageMuteNum
639
680
  }
640
681
  }
641
- })
682
+ }, 'onNewRoomMessage')
642
683
  }
643
684
  }
644
685
  return nextSession
@@ -748,6 +789,26 @@ class ClientChat {
748
789
  getAccountGreetingUnreadMsgCountMethod: options.getAccountGreetingUnreadMsgCountMethod,
749
790
  batchGetAccountGreetingUnreadMsgCountMethod: options.batchGetAccountGreetingUnreadMsgCountMethod,
750
791
  clearAccountGreetingUnreadMsgCountMethod: options.clearAccountGreetingUnreadMsgCountMethod,
792
+ // 账号未读数量变化日志,附带该账号本地会话未读之和,用于排查账号未读数和会话未读数不一致的问题
793
+ onUnreadMessageNumLog: (log) => {
794
+ if (!this.socket) {
795
+ return
796
+ }
797
+ const wechatAccountId = log.content && log.content.data && log.content.data.wechatAccountId
798
+ if (wechatAccountId && this.session) {
799
+ log.content.data.localSessionUnread = Object.keys(this.session.sessionMap).reduce((rs, id) => {
800
+ const session = this.session.sessionMap[id]
801
+ if (session && session.wechatAccountId === wechatAccountId) {
802
+ rs.num += session.unreadMsgCount || 0
803
+ if (session.mute) {
804
+ rs.muteNum += session.unreadMsgCount || 0
805
+ }
806
+ }
807
+ return rs
808
+ }, { num: 0, muteNum: 0 })
809
+ }
810
+ this.socket.addLog(log)
811
+ },
751
812
  onAccountAdded: ({ account }) => {
752
813
  return this.account.refreshAccountUnreadMessageNum({
753
814
  wechatAccountId: account.id
@@ -919,25 +980,10 @@ class ClientChat {
919
980
  return this.contacts.getFriend(sessionFriendId, true)
920
981
  })
921
982
  }
922
- // 刷新工作微信号未读消息数量
923
- if (rs && rs.length) {
924
- rs.forEach(session => {
925
- this.account.updateAccountUnreadMessageNum(session.wechatAccountId, ({ unreadMesasgeNum, unreadMessageMuteNum }) => {
926
- if (session.mute) {
927
- return {
928
- num: (unreadMesasgeNum || 0) + (session.unreadMsgCount || 0),
929
- muteNum: (unreadMessageMuteNum || 0) + (session.unreadMsgCount || 0)
930
- }
931
- } else {
932
- return {
933
- num: (unreadMesasgeNum || 0) + (session.unreadMsgCount || 0),
934
- muteNum: unreadMessageMuteNum
935
- }
936
- }
937
- })
938
- })
939
- }
983
+ // 分配会话的未读数量已经在onAddSession里累加到工作微信号上了,这里不再重复累加
984
+ // 转移/删除的好友如果本地没有加载到对应会话,本地是无法扣减的,所以统一以服务端为准刷新一次
940
985
  return ps.finally(() => {
986
+ this.refreshAccountsUnreadMessageNum([].concat(wechatAccountIds, data.wechatAccountIds || []))
941
987
  // 这里应该标记那些工作微信号的通讯录修改了,后面要在界面打个提示,告诉用户通讯录变化了,让手动刷新一下
942
988
  // 同时也将缓存都清理一下,例如工作微信号分组统计数量等
943
989
  this.contacts.markModify() // 标记通讯录修改
@@ -963,6 +1009,8 @@ class ClientChat {
963
1009
  this.session.removeSession(session.conversationId)
964
1010
  }
965
1011
  }
1012
+ // 本地没有加载到该会话时无法扣减未读数量,以服务端为准刷新一次
1013
+ this.refreshAccountsUnreadMessageNum(data.wechatAccountIds)
966
1014
  // todo 刷新一下微信号,如果微信号一个好友都没了,应该要删除掉的
967
1015
  this.contacts.markModify() // 标记通讯录修改
968
1016
  }
@@ -1001,25 +1049,10 @@ class ClientChat {
1001
1049
  ps = this.account.reloadData()
1002
1050
  }
1003
1051
  }
1004
- // 刷新工作微信号未读消息数量
1005
- if (rs && rs.length) {
1006
- rs.forEach(session => {
1007
- this.account.updateAccountUnreadMessageNum(session.wechatAccountId, ({ unreadMesasgeNum, unreadMessageMuteNum }) => {
1008
- if (session.mute) {
1009
- return {
1010
- num: (unreadMesasgeNum || 0) + (session.unreadMsgCount || 0),
1011
- muteNum: (unreadMessageMuteNum || 0) + (session.unreadMsgCount || 0)
1012
- }
1013
- } else {
1014
- return {
1015
- num: (unreadMesasgeNum || 0) + (session.unreadMsgCount || 0),
1016
- muteNum: unreadMessageMuteNum
1017
- }
1018
- }
1019
- })
1020
- })
1021
- }
1052
+ // 分配会话的未读数量已经在onAddSession里累加到工作微信号上了,这里不再重复累加
1053
+ // 转移/删除的群聊如果本地没有加载到对应会话,本地是无法扣减的,所以统一以服务端为准刷新一次
1022
1054
  return ps.finally(() => {
1055
+ this.refreshAccountsUnreadMessageNum([].concat(wechatAccountIds, data.wechatAccountIds || []))
1023
1056
  // 这里应该标记那些工作微信号的通讯录修改了,后面要在界面打个提示,告诉用户通讯录变化了,让手动刷新一下
1024
1057
  // 同时也将缓存都清理一下,例如工作微信号分组统计数量等
1025
1058
  this.contacts.markModify() // 标记通讯录修改
@@ -1043,6 +1076,8 @@ class ClientChat {
1043
1076
  }
1044
1077
  // 删除群聊
1045
1078
  this.contacts.removeRoom(data.wechatChatroomId)
1079
+ // 本地没有加载到该会话时无法扣减未读数量,以服务端为准刷新一次
1080
+ this.refreshAccountsUnreadMessageNum(data.wechatAccountIds)
1046
1081
  }
1047
1082
  // 更新群聊
1048
1083
  else {
@@ -1080,6 +1115,8 @@ class ClientChat {
1080
1115
  wechatAccountId: data.wechatAccountId,
1081
1116
  unreadGreetingMsgCount: data.unreadGreetingMsgCount
1082
1117
  })
1118
+ // 刷新所有微信号请求数量
1119
+ this.account.getAccountGreetingUnreadMsgCount()
1083
1120
  },
1084
1121
  onCmdNewFriendRequest: ({ data }) => {
1085
1122
  // 刷新指定微信号请求数量
@@ -1151,6 +1188,10 @@ class ClientChat {
1151
1188
  Promise.all(ps).finally(() => {
1152
1189
  this.friendMessage.setLastReceiveMessageTime(Date.now())
1153
1190
  this.roomMessage.setLastReceiveMessageTime(Date.now())
1191
+ // 断线期间本地增量可能和服务端产生偏差(其他端已读、消息延迟等),重连后以服务端为准全量校准一次
1192
+ if (ps.length) {
1193
+ this.account.refreshAllAccountUnreadMesageNum()
1194
+ }
1154
1195
  })
1155
1196
  },
1156
1197
  onSocketClose: (evt) => {
@@ -1205,6 +1246,17 @@ class ClientChat {
1205
1246
  this.shouldMarkCurrentSessionReadMethod = method
1206
1247
  }
1207
1248
 
1249
+ /**
1250
+ * 以服务端为准刷新指定工作微信号的未读消息数量,用于本地增量无法处理的场景(例如本地未加载到的会话被删除/转移)
1251
+ * @param { Array<String> } wechatAccountIds
1252
+ */
1253
+ refreshAccountsUnreadMessageNum (wechatAccountIds) {
1254
+ const ids = Array.from(new Set((wechatAccountIds || []).filter(id => id && this.account.accountMap[id])))
1255
+ ids.forEach(wechatAccountId => {
1256
+ this.account.refreshAccountUnreadMessageNum({ wechatAccountId })
1257
+ })
1258
+ }
1259
+
1208
1260
  shouldMarkCurrentSessionRead (sessionId, data) {
1209
1261
  if (typeof this.shouldMarkCurrentSessionReadMethod === 'function') {
1210
1262
  return this.shouldMarkCurrentSessionReadMethod({
@@ -0,0 +1,296 @@
1
+ # 工作微信号未读数量与会话未读数量不一致问题修复
2
+
3
+ - 日期:2026-09-29
4
+ - 涉及文件:
5
+ - `services/client-chat/client-chat-wechat/client-chat.js`
6
+ - `services/client-chat/client-chat-wechat/client-chat-session.js`
7
+ - `services/client-chat/client-chat-wechat/client-chat-account.js`
8
+ - 相关提交:`2679dd9`(A2 / A4 / B1 / D),A1 为后续单独提交
9
+
10
+ ---
11
+
12
+ ## 1. 问题现象
13
+
14
+ 客户端左侧工作微信号徽标显示有未读(例如 8),但右侧"最近会话"里除免打扰会话外已经没有任何未读会话;刷新页面后,从服务端重新拉取的账号未读数量为 0。
15
+
16
+ 即:**本地账号未读数 > 服务端账号未读数,且本地会话未读之和为 0**。
17
+
18
+ ---
19
+
20
+ ## 2. 根因分析
21
+
22
+ SDK 里维护着两套彼此独立的本地"账本":
23
+
24
+ | 账本 | 初始值 | 增量维护 |
25
+ |---|---|---|
26
+ | 账号未读数(`account.accountUnreadMessageNumData`) | 登录时 `getAccountUnreadMessageNumMethod`,服务端按该账号**所有会话**(不管本地是否加载)计算 | 本地 ±N |
27
+ | 会话未读数(`session.sessionMap[id].unreadMsgCount`) | 分页拉会话列表时服务端给的 | 本地 ±1 |
28
+
29
+ 账号未读数只有两处减法,都是按**本地会话**的 `unreadMsgCount` 减:
30
+
31
+ - `onSessionReadChange`:会话被标记已读
32
+ - `onRemoveSession`:会话被移除
33
+
34
+ 所以,凡是"加到账号上的数,没有等量落到某个本地会话的 `unreadMsgCount` 上"的路径,都会在账号上留下永远减不掉的残数。再加上所有减法都用了 `Math.max(0, …)` 钳位,少算的被抹平、多算的一直留着,长期运行必然表现为"本地 ≥ 服务端,刷新归零"。
35
+
36
+ 本次排查出的具体路径:
37
+
38
+ ### 2.1 新消息触发 `getSession` 时,把服务端存量未读整个加到账号(主因)
39
+
40
+ 场景:某会话本地还没分页加载到(一个号几百个会话很常见),来了一条新消息 → `findSession` 找不到 → `getSession` 从服务端拉回 → `addSession(rs.data, null)` → `onAddSession` 把服务端返回的 `unreadMsgCount`(存量 k + 新增 1)**整个**加到账号。
41
+
42
+ 而账号初始值里已经含了这 k 条存量,于是多算了 k。用户点开该会话只减 k+1,账号永久多出 k。
43
+
44
+ 举例:会话 C 服务端存量 5、本地未加载,账号初始值 10。来 1 条新消息:
45
+
46
+ ```
47
+ 服务端:C 未读 6,账号 11
48
+ 本地: C 未读 6,账号 10 + 6 = 16 ← 多了 5
49
+ 点开 C:账号 16 - 6 = 10,实际应为 5,再点完其他会话后本地剩 5、服务端 0
50
+ ```
51
+
52
+ ### 2.2 分配会话(`CmdFriendInfoChanged` / `CmdChatroomInfoChanged` changeType=4)重复累加两次
53
+
54
+ `loadAssignTaskSessionData` 内部 `setSessionData(rs, null, false)` → `addSession(item, null)` → `onAddSession` 累加一次;外层 `.then` 回调里又对同一批 `rs` 手动 `updateAccountUnreadMessageNum` 累加一次。有未读的分配会话账号直接翻倍,点开只减一半。
55
+
56
+ ### 2.3 本地未加载到的会话被服务端删除 / 转移,本地无法扣减
57
+
58
+ 好友转移(changeType=4 且 `friends[].changeType === 3`)、删除好友(changeType=3)、群聊同理。服务端会把账号未读减 k;本地走 `removeSession` 时 `sessionMap` 里没有它,直接 return,账号不减。这个会话从未在本地列表出现过(所以看起来"全部已读"),账号却多 k。
59
+
60
+ ### 2.4 断线重连后没有校准
61
+
62
+ 重连本身不会单独制造偏差(补拉消息是成对累加的),但断线期间是偏差最容易堆积的时刻(手机端很可能处理了消息),且重连是天然的"该对账了"信号,此前没有利用。
63
+
64
+ ### 2.5 缺少排查手段
65
+
66
+ 账号未读数量的每次变化没有日志,拿到客户日志也无法定位是哪条路径漂的。
67
+
68
+ ---
69
+
70
+ ## 3. 修复内容
71
+
72
+ ### A1. `getSession` 路径账号只累加"本次新增"条数
73
+
74
+ **client-chat-session.js**
75
+
76
+ - `getSession` 的轮询合并 `context` 新增 `accountUnreadDelta: 0`(第 329 行附近),并在"会话轮询合并完成"日志里输出
77
+ - `addSession(data, wechatAccountId, options = {})` 新增第三个参数,`options.unreadDelta` 透传给 `onAddSession`(第 508 行)
78
+ - 合并完成后 `addSession(rs.data, null, { unreadDelta: context.accountUnreadDelta })`(第 343 行)
79
+
80
+ **client-chat.js**
81
+
82
+ - 好友 / 群聊两处 `getSession` 的 serialize 回调(第 486 / 636 行附近):
83
+
84
+ ```js
85
+ // 会话未读:只在服务端没算过这条消息时 +1(原逻辑不变)
86
+ if (!data.isSend && isAfterServerMessage) { ... }
87
+ // 账号未读:不管是否被 skip 都 +1,只有"正在看当前会话被直接标记已读"不加
88
+ if (!data.isSend && !(isAfterServerMessage && isCurrentSessionRead)) {
89
+ context.accountUnreadDelta += 1
90
+ }
91
+ ```
92
+
93
+ 注意 `accountUnreadDelta` **不等于** `unreadMessageIds.length`:被 skip 的消息表示服务端返回的会话已经把它算进 `unreadMsgCount`,但账号初始值是登录时取的,仍然不含这条,所以账号还是要 +1。
94
+
95
+ - `onAddSession`(第 132 行附近):
96
+
97
+ ```js
98
+ const delta = typeof unreadDelta === 'number' ? unreadDelta : (session.unreadMsgCount || 0)
99
+ if (delta > 0) { 账号 += delta }
100
+ ```
101
+
102
+ - `unreadDelta` 是数字 → `getSession` 来源 → 只加新增(日志 source:`onAddSession:getSession`)
103
+ - `unreadDelta` 为 `undefined` → 分配等来源 → 整个 `unreadMsgCount` 加上(原逻辑,日志 source:`onAddSession`)
104
+ - 分页来源(有 `wechatAccountId`)不加,原逻辑不变
105
+
106
+ 副带收益:`addSession` 的 `isRefreshAll` 路径(会话已存在时又被 `getSession` 拉回)以前也会整量重复累加,现在只加 delta。
107
+
108
+ ### A2. 去掉分配会话的重复累加
109
+
110
+ **client-chat.js** `onCmdFriendInfoChanged` / `onCmdChatroomInfoChanged` 的 `changeType === 4` 分支:删掉外层 `.then` 里对 `rs` 逐个 `updateAccountUnreadMessageNum(+unreadMsgCount)` 的两段代码,累加只保留 `onAddSession` 这一个入口。
111
+
112
+ ### A4. 删除 / 转移后以服务端为准刷新账号未读
113
+
114
+ **client-chat.js** 新增方法:
115
+
116
+ ```js
117
+ refreshAccountsUnreadMessageNum (wechatAccountIds) // 第 1251 行附近
118
+ ```
119
+
120
+ 对 id 去重、过滤掉不在 `accountMap` 中的账号后,逐个调用 `account.refreshAccountUnreadMessageNum({ wechatAccountId })`。
121
+
122
+ 调用位置:
123
+
124
+ | 位置 | 传入的账号 |
125
+ |---|---|
126
+ | 好友 changeType=4,`ps.finally` 内(`reloadData` 之后) | `rs` 中的账号 ∪ `data.wechatAccountIds` |
127
+ | 好友 changeType=3 | `data.wechatAccountIds` |
128
+ | 群聊 changeType=4,`ps.finally` 内 | `rs` 中的账号 ∪ `data.wechatAccountIds` |
129
+ | 群聊 changeType=3 | `data.wechatAccountIds` |
130
+
131
+ 本地没有会话数据时增量是做不了的,只能靠服务端对账。
132
+
133
+ ### B1. 重连后全量校准
134
+
135
+ **client-chat.js** `onSocketOpen`(第 1191 行附近):`loadLossMessageData` 补拉完成后调用 `account.refreshAllAccountUnreadMesageNum()`。加了 `if (ps.length)` 判断,首次连接(没有 `lastReceiveMessageTime`)不会多刷,因为登录时 `onAccountAdded` 已经拉过。
136
+
137
+ ### D. 排查日志
138
+
139
+ **client-chat-account.js**
140
+
141
+ - 新增配置项 `onUnreadMessageNumLog` 和 `_addUnreadMessageNumLog(title, data)`,日志 group:`client-chat-account:unreadMessageNum`
142
+ - `updateAccountUnreadMessageNum(accountId, callback, source)` 新增第三个参数 `source`(可选),数值有变化时记录:
143
+
144
+ ```json
145
+ {
146
+ "source": "onNewFriendMessage",
147
+ "wechatAccountId": "...",
148
+ "before": { "num": 10, "muteNum": 2 },
149
+ "after": { "num": 11, "muteNum": 2 },
150
+ "delta": { "num": 1, "muteNum": 0 },
151
+ "localSessionUnread": { "num": 11, "muteNum": 2 }
152
+ }
153
+ ```
154
+
155
+ - `refreshAccountUnreadMessageNum` / `refreshAllAccountUnreadMesageNum` 也各记一条(source:`refresh` / `refreshAll`,带 before / after),用于观察服务端值和本地值差多少
156
+
157
+ **client-chat.js**
158
+
159
+ - 五个本地增量调用点打上 source:`onAddSession`、`onAddSession:getSession`、`onRemoveSession`、`onSessionReadChange`、`onNewFriendMessage`、`onNewRoomMessage`
160
+ - 注入 `onUnreadMessageNumLog` 时(第 793 行附近),额外计算该账号在 `session.sessionMap` 中本地会话未读之和 `localSessionUnread`,一起写进日志后交给 `socket.addLog`
161
+
162
+ 日志受 `socket.logEnable` 控制,关闭时零开销;存储走现有 3MB 上限自动淘汰。
163
+
164
+ ---
165
+
166
+ ## 4. 如何用日志定位问题
167
+
168
+ 1. 导出客户端日志,过滤 group 为 `client-chat-account:unreadMessageNum`
169
+ 2. 逐条对比 `after.num` 与 `localSessionUnread.num`:
170
+ - 两者相等:账号和本地会话是配平的
171
+ - `after.num > localSessionUnread.num` 且此后一直不回落:从这条开始漂了,看 `source` 即可知道是哪条路径
172
+ 3. 对比 `source: refresh` 的 `before` / `after`:差值就是当时本地相对服务端的累计偏差
173
+ 4. 结合 `client-chat-session:getSession` 里"会话轮询合并完成"的 `accountUnreadDelta` / `unreadMessageCount` / `skippedMessageCount`,可以验证 A1 的增量是否正确
174
+
175
+ ---
176
+
177
+ ## 5. 测试场景
178
+
179
+ ### 5.1 测试前准备
180
+
181
+ - 打开 SDK 日志(`socket.logEnable = true`),测试过程中随时导出日志按 group 过滤
182
+ - 准备一个有较多会话的工作微信号(至少能分 3 页以上),下文称账号 X
183
+ - 准备"对照值"获取方式:刷新页面后左侧徽标显示的就是服务端值;或直接调 `getAccountUnreadMessageNumMethod`
184
+ - 每个场景通用验收标准:
185
+ 1. 操作过程中左侧账号徽标 = 右侧该账号本地会话未读之和(免打扰会话单独算 `muteNum`)
186
+ 2. 把所有会话点开读完后,账号徽标归 0
187
+ 3. 刷新页面后徽标值不变(本地 = 服务端)
188
+ 4. 日志中每条 `client-chat-account:unreadMessageNum` 的 `after.num` 与 `localSessionUnread.num` 相等
189
+
190
+ ### 5.2 A1:本地未加载的会话收到新消息
191
+
192
+ | 步骤 | 预期 |
193
+ |---|---|
194
+ | 1. 让账号 X 的某个会话 C 积累 k 条未读(例如 3 条),并确保 C 排在会话列表第 3 页之后(用其他会话把它往下压) | — |
195
+ | 2. 登录客户端,只加载第 1 页,记录账号 X 徽标值 N(其中已含 C 的 3 条) | 徽标 = N |
196
+ | 3. 用 C 对应的微信给账号 X 发 1 条消息 | 会话 C 出现在列表顶部,未读 4;账号徽标 = **N + 1**(修复前是 N + 4) |
197
+ | 4. 查日志 `client-chat-session:getSession` 的"会话轮询合并完成" | `accountUnreadDelta: 1`;如果服务端返回的会话已含这条消息则 `skippedMessageCount: 1, unreadMessageCount: 0`,否则反过来,但 `accountUnreadDelta` 都是 1 |
198
+ | 5. 查日志 `client-chat-account:unreadMessageNum` | 有一条 `source: "onAddSession:getSession"`,`delta.num: 1` |
199
+ | 6. 点开 C | 账号徽标 = N - 3;`source: "onSessionReadChange"`,`delta.num: -4` |
200
+ | 7. 把账号 X 其余会话全部点开 | 徽标归 0;刷新后仍为 0 |
201
+
202
+ 变体 A1-b:步骤 3 连续快速发 3 条消息(同一轮询周期内合并)→ 徽标 = N + 3,`accountUnreadDelta: 3`。
203
+
204
+ 变体 A1-c:步骤 3 前先点开另一个会话保持选中,再让 C 来消息 → 与主流程一致(C 不是当前会话,正常计数)。
205
+
206
+ 变体 A1-d:先点开 C(此时 C 已加载到本地,不走 getSession 路径)→ 走 `onNewFriendMessage` 分支,与本次改动无关,用作对照:徽标不变(自动已读)或 +1(`shouldMarkCurrentSessionRead` 返回 false 时)。
207
+
208
+ ### 5.3 A1 回归:分配来源不受影响
209
+
210
+ | 步骤 | 预期 |
211
+ |---|---|
212
+ | 1. 后台把一个有 k 条未读的好友从其他客服分配到当前客服的账号 X | 收到 `CmdFriendInfoChanged changeType=4` |
213
+ | 2. 观察账号 X 徽标 | 先 +k(`source: "onAddSession"`,注意不是 `onAddSession:getSession`),随后被 A4 的服务端刷新覆盖为服务端值(正常情况下两者相等,界面无跳动) |
214
+ | 3. 点开该会话 | 徽标 -k,归 0 |
215
+
216
+ ### 5.4 A2:分配会话不再重复累加
217
+
218
+ | 步骤 | 预期 |
219
+ |---|---|
220
+ | 1. 账号 X 徽标为 0 | — |
221
+ | 2. 后台分配 1 个有 3 条未读的好友到账号 X | 徽标 = **3**(修复前是 6) |
222
+ | 3. 查日志 | `source: "onAddSession"` 只有 1 条,`delta.num: 3`;紧接着一条 `source: "refresh"`,`before.num: 3, after.num: 3` |
223
+ | 4. 点开该会话 | 徽标 = 0 |
224
+ | 5. 同样步骤对群聊分配(`CmdChatroomInfoChanged changeType=4`)重复一遍 | 结果相同 |
225
+
226
+ ### 5.5 A4:本地未加载的会话被删除 / 转移
227
+
228
+ | 步骤 | 预期 |
229
+ |---|---|
230
+ | 1. 让账号 X 的会话 C 有 3 条未读,且排在本地未加载的页(同 5.2 步骤 1) | — |
231
+ | 2. 登录,只加载第 1 页,记录徽标 N | — |
232
+ | 3. 后台把 C 对应的好友**转移**给其他客服 | 收到 `CmdFriendInfoChanged changeType=4`,`friends[].changeType=3` |
233
+ | 4. 观察徽标 | = **N - 3**(修复前保持 N);日志有 `source: "refresh"`,`before.num: N, after.num: N-3` |
234
+ | 5. 把账号 X 其余会话点开 | 归 0 |
235
+ | 6. 换成后台**删除**好友(`changeType=3`)重复 1~5 | 结果相同 |
236
+ | 7. 换成群聊转移 / 删除(`CmdChatroomInfoChanged changeType=4 / 3`)重复 1~5 | 结果相同 |
237
+
238
+ 变体 A4-b:C 已加载到本地时执行同样操作 → 走 `onRemoveSession` 本地扣减,随后 `refresh` 覆盖,两者值应一致。
239
+
240
+ ### 5.6 B1:断线重连后校准
241
+
242
+ | 步骤 | 预期 |
243
+ |---|---|
244
+ | 1. 登录,账号 X 徽标为 N,本地会话和账号配平 | — |
245
+ | 2. 断开网络(或 devtools 里 Offline),等待 socket 触发 onclose | 日志 `socket:onclose` |
246
+ | 3. 断网期间:用手机端把账号 X 的某个有未读的会话读掉(服务端未读减少) | — |
247
+ | 4. 恢复网络,等待重连 | 日志 `socket:onopen` |
248
+ | 5. 观察徽标 | 补拉消息完成后,有一条 `source: "refreshAll"` 日志,徽标变为服务端当前值(比 N 少了步骤 3 读掉的条数) |
249
+ | 6. 首次登录(非重连)时查日志 | **没有** `source: "refreshAll"`(`ps.length` 为 0 时不刷) |
250
+
251
+ ### 5.7 D:日志字段正确性
252
+
253
+ | 检查项 | 预期 |
254
+ |---|---|
255
+ | 收到新消息(会话已在本地) | `source: "onNewFriendMessage"` 或 `"onNewRoomMessage"`,`delta.num: 1`;免打扰会话时 `delta.muteNum: 1` |
256
+ | 点开会话 | `source: "onSessionReadChange"`,`delta.num` 为负 |
257
+ | 删除会话 | `source: "onRemoveSession"`,`delta.num` 为负 |
258
+ | 切换免打扰 | `source: "refresh"` |
259
+ | 任意一条日志 | `localSessionUnread.num` = 右侧该账号所有本地会话未读之和(手动数一下核对);`after.num` = 左侧徽标当前值 |
260
+ | `logEnable = false` | 没有任何 `client-chat-account:unreadMessageNum` 日志,功能正常 |
261
+ | 长时间运行 | 日志总量受 3MB 上限约束,不会无限增长 |
262
+
263
+ ### 5.8 综合回归
264
+
265
+ | 场景 | 预期 |
266
+ |---|---|
267
+ | 正常收发消息、点开会话、免打扰、置顶、星标、删除会话 | 徽标行为与修复前一致 |
268
+ | "全部微号" 与具体微信号之间反复切换 | 徽标不变(此路径 A3 尚未处理,但不应比修复前更差) |
269
+ | 多个账号同时收消息 | 各账号徽标独立正确 |
270
+ | 群聊 @ 消息 | `lastAtMsgContent` 仍正常显示(A1 改动没有影响 `atMessage` 逻辑) |
271
+ | 企微(wework)入口 | 完全不受影响 |
272
+ | 跑一个工作日后刷新页面 | 刷新前后徽标一致,或差值明显小于修复前 |
273
+
274
+ ---
275
+
276
+ ## 6. 兼容性说明
277
+
278
+ - `updateAccountUnreadMessageNum` 第三参数、`addSession` 第三参数、`onAddSession` 的 `unreadDelta` 字段、`ClientChatAccount` 的 `onUnreadMessageNumLog` 均为可选,缺省行为与原来一致
279
+ - workphone-client `src/` 下没有直接调用上述方法的地方,无需改动业务代码
280
+ - `client-chat-wework` 是独立实现,未改动
281
+ - 分配路径下 `onAddSession` 先本地 +N、随后 A4 服务端刷新覆盖,界面可能有一次短暂跳动,最终以服务端为准
282
+
283
+ ---
284
+
285
+ ## 7. 未处理项(后续可选)
286
+
287
+ | 编号 | 内容 | 说明 |
288
+ |---|---|---|
289
+ | A3 | `addSession` 对已存在会话的静默覆盖(`client-chat-session.js` `isRefreshAll` 分支用 `Vue.set` 直接覆盖 `unreadMsgCount`)不同步账号 | 切号 / 分页拉回已存在会话时,本地多计或其他端已读的差额会在这里悄悄消失进账号。修法:覆盖前后比较 `unreadMsgCount`,差值通过 `onUpdateSession` 的 `changeProps` 流出并调整账号 |
290
+ | B2 | 新消息后按账号 debounce(1~2s)调 `refreshAccountUnreadMessageNum` | 兜底对账,能自动纠正所有本地增量无法覆盖的偏差(其他端已读、服务端不计某些消息类型、免打扰状态在其他端修改等) |
291
+ | C1 | 免打扰归属:增量按本地 `session.mute` 分桶,其他端改的 mute 本地感知不到 | 有 B2 后自愈 |
292
+ | C2 | `Math.max(0, …)` 钳位掩盖少算 | 可在开发环境结果 < 0 时打 warn |
293
+ | C3 | `updateAccountUnreadMessageNum` 是异步的(`getAccount().then`),账号不在 `accountMap` 时先 `loadData`,可能与 refresh 交叉 | `accountMap` 里已有时可改为同步执行 |
294
+ | C4 | 服务端计数规则核对:`msgType 10000`("你已添加了xx")、`49`(群邀请)等客户端计未读,服务端是否也计 | 需和后端确认,不一致在 `shouldIgnoreSessionByMessage` 补规则 |
295
+ | C5 | `getSession` 路径命中当前会话时只本地置 0,未调 `setSessionRead` 通知服务端 | 方向是"服务端 > 本地",不是本次现象,但也不一致 |
296
+ | — | 协议层面:`CmdNewMessage` 由服务端带上会话 `unreadMsgCount` 和账号 `unread`,客户端直接覆盖;增加"其他端已读"推送 | 长期方案,两边天然一致 |