返回 AiToEarn
useFansRefresh.ts
根目录 / project / aitoearn-web / src / components / ChannelManager / hooks / useFansRefresh.ts
1 /**
2 * useFansRefresh - 频道粉丝数刷新逻辑
3 * 统一处理平台冷却、插件平台同步和账号 analytics 刷新。
4 */
5
6 'use client'
7
8 import type { SocialAccount } from '@/api/accounts/account.types'
9 import type { PlatType } from '@/app/config/platConfig'
10 import type { PluginPlatformType } from '@/store/plugin'
11 import { useCallback, useMemo, useState } from 'react'
12 import { useShallow } from 'zustand/react/shallow'
13 import { refreshAccountFansApi } from '@/api/accounts/account.api'
14 import { AccountStatus } from '@/app/config/accountConfig'
15 import { useTransClient } from '@/app/i18n/client'
16 import { useAccountStore } from '@/store/account'
17 import { usePlatformMetadataStore } from '@/store/platformMetadata'
18 import {
19 isPluginPlatformAccountReady,
20 PluginStatus,
21 usePluginStore,
22 } from '@/store/plugin'
23 import { notification } from '@/utils/ui/notification'
24 import { useFansRefreshCooldownStore } from '../fansRefreshCooldownStore'
25 import { isPlatformFansRefreshSupported, isPluginFansRefreshPlatform } from '../utils/fansRefreshSupport'
26
27 type FansRefreshTarget = 'all' | PlatType | null
28
29 type PlatformRefreshKind = 'sync' | 'async'
30
31 type PlatformRefreshStatus = 'success' | 'partial' | 'failed' | 'skipped'
32 type PlatformRefreshSkippedReason = 'cooldown' | 'empty' | 'unsupported'
33
34 interface FansRefreshProgress {
35 current: number
36 total: number
37 }
38
39 interface PlatformRefreshResult {
40 platform: PlatType
41 status: PlatformRefreshStatus
42 kind?: PlatformRefreshKind
43 skippedReason?: PlatformRefreshSkippedReason
44 remainingMs?: number
45 message?: string
46 }
47
48 function getUniquePlatforms(accounts: SocialAccount[]) {
49 const platforms: PlatType[] = []
50 const seen = new Set<PlatType>()
51
52 for (const account of accounts) {
53 if (!seen.has(account.type)) {
54 seen.add(account.type)
55 platforms.push(account.type)
56 }
57 }
58
59 return platforms
60 }
61
62 function isSuccessfulResult(result: PlatformRefreshResult) {
63 return result.status === 'success' || result.status === 'partial'
64 }
65
66 export function useFansRefresh() {
67 const { t } = useTransClient('account')
68 const [refreshingTarget, setRefreshingTarget] = useState<FansRefreshTarget>(null)
69 const [refreshProgress, setRefreshProgress] = useState<FansRefreshProgress | null>(null)
70 const platformInfoMap = usePlatformMetadataStore(state => state.map)
71
72 const { accountList } = useAccountStore(
73 useShallow(state => ({
74 accountList: state.accountList,
75 })),
76 )
77
78 const {
79 pluginStatus,
80 refreshAllPlatformAccounts,
81 syncAccountToDatabase,
82 openPluginModal,
83 } = usePluginStore(
84 useShallow(state => ({
85 pluginStatus: state.status,
86 refreshAllPlatformAccounts: state.refreshAllPlatformAccounts,
87 syncAccountToDatabase: state.syncAccountToDatabase,
88 openPluginModal: state.openPluginModal,
89 })),
90 )
91
92 const { markPlatformRefresh, getPlatformRefreshRemaining } = useFansRefreshCooldownStore(
93 useShallow(state => ({
94 markPlatformRefresh: state.markPlatformRefresh,
95 getPlatformRefreshRemaining: state.getPlatformRefreshRemaining,
96 })),
97 )
98
99 const usableAccounts = useMemo(
100 () => accountList.filter(account => account.status === AccountStatus.USABLE),
101 [accountList],
102 )
103
104 const refreshablePlatforms = useMemo(
105 () => getUniquePlatforms(usableAccounts)
106 .filter(platform => isPlatformFansRefreshSupported(platform, platformInfoMap.get(platform))),
107 [platformInfoMap, usableAccounts],
108 )
109
110 const getPlatformName = useCallback((platform: PlatType) => {
111 return platformInfoMap.get(platform)?.name || platform
112 }, [platformInfoMap])
113
114 const formatPlatformNames = useCallback(
115 (platforms: PlatType[]) => {
116 const names = platforms.map(platform => getPlatformName(platform))
117 const separator = t('channelManager.platformNameSeparator')
118 const lastSeparator = t('channelManager.platformNameLastSeparator')
119
120 if (names.length <= 1)
121 return names[0] || ''
122
123 if (names.length === 2)
124 return `${names[0]}${lastSeparator}${names[1]}`
125
126 return `${names.slice(0, -1).join(separator)}${lastSeparator}${names[names.length - 1]}`
127 },
128 [getPlatformName, t],
129 )
130
131 const getPlatformAccounts = useCallback(
132 (platform: PlatType) => usableAccounts.filter(account => account.type === platform),
133 [usableAccounts],
134 )
135
136 const refreshPluginPlatform = useCallback(
137 async (platform: PluginPlatformType, accounts: SocialAccount[]): Promise<PlatformRefreshResult> => {
138 const platformName = getPlatformName(platform)
139
140 if (pluginStatus !== PluginStatus.READY) {
141 openPluginModal()
142 return {
143 platform,
144 status: 'failed',
145 kind: 'sync',
146 message: t('channelManager.pluginNotReady', { platform: platformName }),
147 }
148 }
149
150 try {
151 await refreshAllPlatformAccounts()
152
153 const pluginAccount = usePluginStore.getState().platformAccounts[platform]
154 if (!isPluginPlatformAccountReady(pluginAccount)) {
155 openPluginModal()
156 return {
157 platform,
158 status: 'failed',
159 kind: 'sync',
160 message: t('channelManager.platformNotLoggedIn', { platform: platformName }),
161 }
162 }
163
164 const matchedAccount = accounts.find(account => account.uid === pluginAccount.uid)
165 if (!matchedAccount) {
166 return {
167 platform,
168 status: 'failed',
169 kind: 'sync',
170 message: t('channelManager.refreshFansNoMatchedPluginAccount', { platform: platformName }),
171 }
172 }
173
174 const result = await syncAccountToDatabase(platform, matchedAccount.groupId)
175 if (!result) {
176 return {
177 platform,
178 status: 'failed',
179 kind: 'sync',
180 message: t('channelManager.syncFailed'),
181 }
182 }
183
184 return { platform, status: 'success', kind: 'sync' }
185 }
186 catch (error) {
187 console.error('Refresh plugin fans failed:', error)
188 return {
189 platform,
190 status: 'failed',
191 kind: 'sync',
192 message: t('channelManager.refreshFansFailed'),
193 }
194 }
195 },
196 [
197 getPlatformName,
198 openPluginModal,
199 pluginStatus,
200 refreshAllPlatformAccounts,
201 syncAccountToDatabase,
202 t,
203 ],
204 )
205
206 const refreshAnalyticsPlatform = useCallback(
207 async (platform: PlatType, accounts: SocialAccount[]): Promise<PlatformRefreshResult> => {
208 try {
209 const responses = await Promise.all(
210 accounts.map(account => refreshAccountFansApi(account.id)),
211 )
212 const successCount = responses.filter(response => response?.code === 0).length
213
214 if (successCount === accounts.length)
215 return { platform, status: 'success', kind: 'sync' }
216
217 if (successCount > 0)
218 return { platform, status: 'partial', kind: 'sync' }
219
220 const failedResponse = responses.find(response => response?.message)
221 return {
222 platform,
223 status: 'failed',
224 kind: 'sync',
225 message: failedResponse?.message || t('channelManager.refreshFansFailed'),
226 }
227 }
228 catch (error) {
229 console.error('Refresh account fans failed:', error)
230 return {
231 platform,
232 status: 'failed',
233 kind: 'sync',
234 message: t('channelManager.refreshFansFailed'),
235 }
236 }
237 },
238 [t],
239 )
240
241 const refreshSinglePlatform = useCallback(
242 async (platform: PlatType): Promise<PlatformRefreshResult> => {
243 const accounts = getPlatformAccounts(platform)
244 if (accounts.length === 0) {
245 return { platform, status: 'skipped', skippedReason: 'empty' }
246 }
247
248 if (!isPlatformFansRefreshSupported(platform, platformInfoMap.get(platform))) {
249 return { platform, status: 'skipped', skippedReason: 'unsupported' }
250 }
251
252 const remainingMs = getPlatformRefreshRemaining(platform)
253 if (remainingMs > 0) {
254 return { platform, status: 'skipped', skippedReason: 'cooldown', remainingMs }
255 }
256
257 const result = isPluginFansRefreshPlatform(platform)
258 ? await refreshPluginPlatform(platform, accounts)
259 : await refreshAnalyticsPlatform(platform, accounts)
260
261 if (isSuccessfulResult(result)) {
262 markPlatformRefresh(platform)
263 }
264
265 return result
266 },
267 [
268 getPlatformAccounts,
269 getPlatformRefreshRemaining,
270 markPlatformRefresh,
271 platformInfoMap,
272 refreshAnalyticsPlatform,
273 refreshPluginPlatform,
274 ],
275 )
276
277 const notifyRefreshResults = useCallback(
278 (results: PlatformRefreshResult[], isAllRefresh: boolean) => {
279 const completedResults = results.filter(isSuccessfulResult)
280 const failedResults = results.filter(result => result.status === 'failed')
281 const cooldownResults = results.filter(result => result.skippedReason === 'cooldown')
282
283 if (completedResults.length === 0) {
284 if (cooldownResults.length > 0) {
285 const firstCooldownResult = cooldownResults[0]
286 const minutes = Math.ceil((firstCooldownResult.remainingMs || 0) / 60000)
287 notification.warning(
288 isAllRefresh
289 ? t('channelManager.refreshFansAllCooldown', { minutes })
290 : t('channelManager.refreshFansCooldown', {
291 platform: getPlatformName(firstCooldownResult.platform),
292 minutes,
293 }),
294 )
295 return
296 }
297
298 const firstFailedResult = failedResults[0]
299 notification.warning(firstFailedResult?.message || t('channelManager.refreshFansNoAvailableAccount'))
300 return
301 }
302
303 if (failedResults.length > 0) {
304 notification.warning(
305 t('channelManager.refreshFansPartialDone', {
306 platforms: formatPlatformNames(failedResults.map(result => result.platform)),
307 }),
308 )
309 return
310 }
311
312 const syncPlatforms = completedResults
313 .filter(result => result.kind === 'sync')
314 .map(result => result.platform)
315 const asyncPlatforms = completedResults
316 .filter(result => result.kind === 'async')
317 .map(result => result.platform)
318
319 if (syncPlatforms.length > 0 && asyncPlatforms.length > 0) {
320 notification.success(
321 t('channelManager.refreshFansMixedDone', {
322 syncPlatforms: formatPlatformNames(syncPlatforms),
323 }),
324 )
325 return
326 }
327
328 if (syncPlatforms.length > 0) {
329 notification.success(
330 t('channelManager.refreshFansSyncDone', {
331 platforms: formatPlatformNames(syncPlatforms),
332 }),
333 )
334 return
335 }
336
337 notification.success(
338 isAllRefresh
339 ? t('channelManager.refreshFansAllSubmitted')
340 : t('channelManager.refreshFansPlatformSubmitted', {
341 platform: formatPlatformNames(asyncPlatforms),
342 }),
343 )
344 },
345 [formatPlatformNames, getPlatformName, t],
346 )
347
348 const refreshPlatformFans = useCallback(
349 async (platform: PlatType) => {
350 if (refreshingTarget)
351 return
352
353 setRefreshProgress(null)
354 setRefreshingTarget(platform)
355 try {
356 const result = await refreshSinglePlatform(platform)
357 notifyRefreshResults([result], false)
358 }
359 finally {
360 setRefreshingTarget(null)
361 }
362 },
363 [notifyRefreshResults, refreshSinglePlatform, refreshingTarget],
364 )
365
366 const refreshAllFans = useCallback(async () => {
367 if (refreshingTarget)
368 return
369
370 if (refreshablePlatforms.length === 0) {
371 notification.warning(t('channelManager.refreshFansNoAvailableAccount'))
372 return
373 }
374
375 setRefreshingTarget('all')
376 setRefreshProgress({ current: 0, total: refreshablePlatforms.length })
377 try {
378 const results: PlatformRefreshResult[] = []
379 for (const [index, platform] of refreshablePlatforms.entries()) {
380 results.push(await refreshSinglePlatform(platform))
381 setRefreshProgress({ current: index + 1, total: refreshablePlatforms.length })
382 }
383 notifyRefreshResults(results, true)
384 }
385 finally {
386 setRefreshingTarget(null)
387 setRefreshProgress(null)
388 }
389 }, [notifyRefreshResults, refreshSinglePlatform, refreshablePlatforms, refreshingTarget, t])
390
391 return {
392 refreshingTarget,
393 refreshProgress,
394 refreshAllFans,
395 refreshPlatformFans,
396 }
397 }
398
398 lines TYPESCRIPT