gaoluyang
2026-06-29 27cd042df9aca0383a49f3514bc21958dd890912
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
import type { ImGroupRequestApi } from '#/api/im/group/request'
 
import { acceptHMRUpdate, defineStore } from 'pinia'
 
import { agreeGroupRequest as apiAgreeGroupRequest, getMyGroupRequest as apiGetMyGroupRequest, getUnhandledRequestList as apiGetUnhandledRequestList, pullMyGroupRequestList as apiPullMyGroupRequestList, refuseGroupRequest as apiRefuseGroupRequest } from '#/api/im/group/request'
import { getCurrentUserId } from '#/views/im/utils/auth'
import { ImGroupRequestHandleResult } from '#/views/im/utils/constants'
 
import { getDb, StorageKeys } from '../../utils/db'
import { runIncrementalPull } from '../../utils/pull'
 
type PendingRequest = { epoch: number; promise: Promise<void>; userId: number; }
 
/** clear() 时递增;旧账号 in-flight 的 pullGroupRequests 结果 resolve 后比对一致才写 store,防跨账号红点污染(与 friendStore 同口径) */
let storeEpoch = 0
let pendingUnhandledFetch: null | PendingRequest = null
 
/**
 * IM 加群申请 Store
 *
 * 仅维护「我管理的所有群」下未处理的申请列表(unhandledList);
 * 横幅 / Drawer 都从这里派生 count 和分组列表,避免给 ImGroupApi.GroupRespVO 挂 pendingRequestCount 字段
 *
 * 数据生命周期:
 * - 进 IM 后调一次 fetchUnhandledGroupRequestList 拉首页全量
 * - WebSocket 1503 → 调 addGroupRequestById(requestId) 拉单条 + push 到 unhandledList 头部
 * - WebSocket 1505 / 1506 → 按 requestId 从 unhandledList 移除
 * - WebSocket 1517 GROUP_ADMIN_ADD(自己被加为 admin)→ 重新调 fetchUnhandledGroupRequestList
 * - 本端 agree / refuse 处理后 → 本地按 requestId 移除
 */
export const useGroupRequestStore = defineStore('imGroupRequestStore', {
  state: () => ({
    /** 我管理的所有群下未处理申请列表(按 id 倒序) */
    unhandledList: [] as ImGroupRequestApi.GroupRequestRespVO[],
    /** fetchUnhandledGroupRequestList 是否成功执行过;避免横幅显示 0 然后跳数字的闪烁 */
    loaded: false
  }),
 
  getters: {
    /**
     * 各群下未处理申请数的 Map;O(N) 扫一次缓存供 ConversationItem 等 N 处复用,避免 N×M 重复 filter
     */
    getUnhandledGroupRequestCountMap(state): Map<number, number> {
      const map = new Map<number, number>()
      for (const request of state.unhandledList) {
        map.set(request.groupId, (map.get(request.groupId) ?? 0) + 1)
      }
      return map
    },
    /** 指定群下的未处理申请数 */
    getUnhandledGroupRequestCount(): (groupId: number) => number {
      return (groupId: number) => this.getUnhandledGroupRequestCountMap.get(groupId) ?? 0
    },
    /** 指定群下的未处理申请列表 */
    getUnhandledGroupRequestListByGroupId:
      (state) =>
      (groupId: number): ImGroupRequestApi.GroupRequestRespVO[] =>
        state.unhandledList.filter((r) => r.groupId === groupId)
  },
 
  actions: {
    /** 从 IndexedDB 恢复加群申请 */
    async loadGroupRequestList(): Promise<boolean> {
      try {
        const cached = await getDb().getAll<ImGroupRequestApi.GroupRequestRespVO>('groupRequests')
        if (!cached || cached.length === 0) {
          return false
        }
        this.unhandledList = cached
          .filter((request) => request.handleResult === ImGroupRequestHandleResult.UNHANDLED)
          .toSorted((requestA, requestB) => requestB.id - requestA.id)
        return true
      } catch (error) {
        console.warn('[IM groupRequestStore] 本地加群申请缓存读取失败', error)
        return false
      }
    },
 
    /** 保存加群申请列表 */
    saveGroupRequestList(): void {
      void getDb()
        .transaction(['groupRequests'], 'readwrite', async (tx) => {
          const db = getDb()
          await db.clearStore('groupRequests', tx)
          for (const request of this.unhandledList) {
            await db.put('groupRequests', request, tx)
          }
        })
        .catch((error) => console.warn('[IM groupRequestStore] 本地加群申请缓存写入失败', error))
    },
 
    /** 保存单条加群申请 */
    async saveGroupRequestRecord(request: ImGroupRequestApi.GroupRequestRespVO): Promise<void> {
      await getDb().put('groupRequests', request)
    },
 
    /** 保存单条加群申请 */
    saveGroupRequest(request: ImGroupRequestApi.GroupRequestRespVO): void {
      void this.saveGroupRequestRecord(request).catch((error) =>
        console.warn('[IM groupRequestStore] 本地加群申请写入失败', error)
      )
    },
 
    /** 拉取我管理的所有群下未处理申请;进 IM 后 / 升级 admin 后 / WS 推送有冲突时调用 */
    async fetchUnhandledGroupRequestList() {
      const requestEpoch = storeEpoch
      const requestUserId = getCurrentUserId()
      if (
        pendingUnhandledFetch?.epoch === requestEpoch &&
        pendingUnhandledFetch.userId === requestUserId
      ) {
        return pendingUnhandledFetch.promise
      }
      const promise = (async () => {
        const list = await apiGetUnhandledRequestList()
        if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) {
          return
        }
        this.unhandledList = list || []
        this.loaded = true
        this.saveGroupRequestList()
      })().finally(() => {
        if (
          pendingUnhandledFetch?.epoch === requestEpoch &&
          pendingUnhandledFetch.userId === requestUserId
        ) {
          pendingUnhandledFetch = null
        }
      })
      pendingUnhandledFetch = { epoch: requestEpoch, userId: requestUserId, promise }
      return promise
    },
 
    /**
     * WS 收到 1503:拉最新内容并置顶
     *
     * 同一对 group_id, user_id 复用记录时 requestId 不变但 applyContent / inviterUserId 会刷新,所以无条件 fetch + 排到头部
     */
    async addGroupRequestById(requestId: number) {
      const requestEpoch = storeEpoch
      const requestUserId = getCurrentUserId()
      const request = await apiGetMyGroupRequest(requestId)
      if (!request) {
        return
      }
      if (requestEpoch !== storeEpoch || getCurrentUserId() !== requestUserId) {
        return
      }
      this.upsertGroupRequest(request)
    },
 
    /**
     * 本地合并 / 新增单条加群申请(WS 推送 & 增量拉取共用)
     *
     * 未处理的按 id 去重后置顶;已处理的从未处理列表移除,避免补偿时把已同意 / 拒绝的记录塞回红点
     */
    upsertGroupRequest(request: ImGroupRequestApi.GroupRequestRespVO) {
      void this.upsertGroupRequestForPull(request).catch((error) =>
        console.warn('[IM groupRequestStore] 本地加群申请写入失败', error)
      )
    },
 
    /** 本地合并 / 新增单条加群申请 */
    async upsertGroupRequestForPull(request: ImGroupRequestApi.GroupRequestRespVO): Promise<void> {
      if (request.handleResult !== ImGroupRequestHandleResult.UNHANDLED) {
        await this.removeGroupRequestByIdForPull(request.id)
        return
      }
      this.unhandledList = [request, ...this.unhandledList.filter((r) => r.id !== request.id)]
      await this.saveGroupRequestRecord(request)
    },
 
    /**
     * 增量拉取加群申请变更并合并;含已处理的按 handleResult 走移除(已处理 → 从红点列表剔除)
     *
     * 只做重连 / 后续离线补偿,不负责首登:首登红点走 fetchUnhandledGroupRequestList(服务端直接过滤未处理,语义更精准、启动更轻)。
     * 故首次重连时游标为空 = 一次性全量走一遍(已处理记录命中 removeGroupRequestById 为 no-op,红点不受影响),之后增量。
     */
    async pullGroupRequests() {
      // 快照 epoch;账号在拉取途中切换(clear() → epoch++)时丢弃旧账号那几页结果,防跨账号红点污染
      const requestEpoch = storeEpoch
      const requestUserId = getCurrentUserId()
      const isActive = () => requestEpoch === storeEpoch && getCurrentUserId() === requestUserId
      await runIncrementalPull(
        StorageKeys.settings.groupRequestPullCursor,
        apiPullMyGroupRequestList,
        async (records) => {
          if (!isActive()) {
            return false
          }
          await Promise.all(records.map((vo) => this.upsertGroupRequestForPull(vo)))
          return true
        },
        isActive
      )
      if (isActive()) {
        this.loaded = true
      }
    },
 
    /** WS 收到 1505 / 1506 或本端处理完一条:按 requestId 从列表移除 */
    removeGroupRequestById(requestId: number) {
      this.unhandledList = this.unhandledList.filter((r) => r.id !== requestId)
      void getDb()
        .delete('groupRequests', requestId)
        .catch((error) => console.warn('[IM groupRequestStore] 本地加群申请删除失败', error))
    },
 
    /** 删除单条加群申请 */
    async removeGroupRequestByIdForPull(requestId: number): Promise<void> {
      this.unhandledList = this.unhandledList.filter((r) => r.id !== requestId)
      await getDb().delete('groupRequests', requestId)
    },
 
    /** 同意申请;本端处理后立即从列表移除,避免被反复点击 */
    async agreeGroupRequest(requestId: number) {
      await apiAgreeGroupRequest(requestId)
      this.removeGroupRequestById(requestId)
    },
 
    /** 拒绝申请 */
    async refuseGroupRequest(requestId: number, handleContent?: string) {
      await apiRefuseGroupRequest(requestId, handleContent)
      this.removeGroupRequestById(requestId)
    },
 
    /** 清空加群申请内存 */
    clear() {
      this.unhandledList = []
      this.loaded = false
      // 账号切换:递增 epoch 废弃旧账号 in-flight 的 pullGroupRequests 结果,避免写进新账号红点列表
      storeEpoch++
      pendingUnhandledFetch = null
    }
  }
})
 
export const useGroupRequestStoreWithOut = () => useGroupRequestStore()
 
if (import.meta.hot) {
  import.meta.hot.accept(acceptHMRUpdate(useGroupRequestStore, import.meta.hot))
}