diff --git a/electron/main.ts b/electron/main.ts index 6ca4580..21f63a4 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -1509,6 +1509,10 @@ function registerIpcHandlers() { return snsService.getSnsUsernames() }) + ipcMain.handle('sns:getUserPostCounts', async () => { + return snsService.getUserPostCounts() + }) + ipcMain.handle('sns:getExportStats', async () => { return snsService.getExportStats() }) diff --git a/electron/preload.ts b/electron/preload.ts index feb594b..41039e2 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -353,6 +353,7 @@ contextBridge.exposeInMainWorld('electronAPI', { getTimeline: (limit: number, offset: number, usernames?: string[], keyword?: string, startTime?: number, endTime?: number) => ipcRenderer.invoke('sns:getTimeline', limit, offset, usernames, keyword, startTime, endTime), getSnsUsernames: () => ipcRenderer.invoke('sns:getSnsUsernames'), + getUserPostCounts: () => ipcRenderer.invoke('sns:getUserPostCounts'), getExportStatsFast: () => ipcRenderer.invoke('sns:getExportStatsFast'), getExportStats: () => ipcRenderer.invoke('sns:getExportStats'), getUserPostStats: (username: string) => ipcRenderer.invoke('sns:getUserPostStats', username), diff --git a/electron/services/snsService.ts b/electron/services/snsService.ts index a05bb38..2bb2908 100644 --- a/electron/services/snsService.ts +++ b/electron/services/snsService.ts @@ -292,7 +292,9 @@ class SnsService { private contactCache: ContactCacheService private imageCache = new Map() private exportStatsCache: { totalPosts: number; totalFriends: number; myPosts: number | null; updatedAt: number } | null = null + private userPostCountsCache: { counts: Record; updatedAt: number } | null = null private readonly exportStatsCacheTtlMs = 5 * 60 * 1000 + private readonly userPostCountsCacheTtlMs = 5 * 60 * 1000 private lastTimelineFallbackAt = 0 private readonly timelineFallbackCooldownMs = 3 * 60 * 1000 @@ -506,10 +508,6 @@ class SnsService { return Number.isFinite(num) && num > 0 ? Math.floor(num) : 0 } - private escapeSqlString(value: string): string { - return value.replace(/'/g, "''") - } - private pickTimelineUsername(post: any): string { const raw = post?.username ?? post?.user_name ?? post?.userName ?? '' if (typeof raw !== 'string') return '' @@ -868,62 +866,82 @@ class SnsService { }) } + private async getUserPostCountsFromTimeline(): Promise> { + const pageSize = 500 + const counts: Record = {} + let offset = 0 + + for (let round = 0; round < 2000; round++) { + const result = await wcdbService.getSnsTimeline(pageSize, offset, undefined, undefined, 0, 0) + if (!result.success || !Array.isArray(result.timeline)) { + throw new Error(result.error || '获取朋友圈用户总条数失败') + } + + const rows = result.timeline + if (rows.length === 0) break + + for (const row of rows) { + const username = this.pickTimelineUsername(row) + if (!username) continue + counts[username] = (counts[username] || 0) + 1 + } + + if (rows.length < pageSize) break + offset += rows.length + } + + return counts + } + + async getUserPostCounts(options?: { + preferCache?: boolean + }): Promise<{ success: boolean; counts?: Record; error?: string }> { + const preferCache = options?.preferCache ?? true + const now = Date.now() + + try { + if ( + preferCache && + this.userPostCountsCache && + now - this.userPostCountsCache.updatedAt <= this.userPostCountsCacheTtlMs + ) { + return { success: true, counts: this.userPostCountsCache.counts } + } + + const counts = await this.getUserPostCountsFromTimeline() + this.userPostCountsCache = { + counts, + updatedAt: Date.now() + } + return { success: true, counts } + } catch (error) { + console.error('[SnsService] getUserPostCounts failed:', error) + if (this.userPostCountsCache) { + return { success: true, counts: this.userPostCountsCache.counts } + } + return { success: false, error: String(error) } + } + } + async getUserPostStats(username: string): Promise<{ success: boolean; data?: { username: string; totalPosts: number }; error?: string }> { const normalizedUsername = this.toOptionalString(username) if (!normalizedUsername) { return { success: false, error: '用户名不能为空' } } - const escapedUsername = this.escapeSqlString(normalizedUsername) - const primaryResult = await wcdbService.execQuery( - 'sns', - null, - `SELECT COUNT(1) AS total FROM SnsTimeLine WHERE user_name = '${escapedUsername}'` - ) - - const primaryTotal = (primaryResult.success && primaryResult.rows && primaryResult.rows.length > 0) - ? this.parseCountValue(primaryResult.rows[0]) - : 0 - if (primaryResult.success && primaryTotal > 0) { + const countsResult = await this.getUserPostCounts({ preferCache: true }) + if (countsResult.success) { + const totalPosts = countsResult.counts?.[normalizedUsername] ?? 0 return { success: true, data: { username: normalizedUsername, - totalPosts: primaryTotal + totalPosts: Math.max(0, Number(totalPosts || 0)) } } } - const fallbackResult = await wcdbService.execQuery( - 'sns', - null, - `SELECT COUNT(1) AS total FROM SnsTimeLine WHERE userName = '${escapedUsername}'` - ) - - if (fallbackResult.success) { - const fallbackTotal = fallbackResult.rows && fallbackResult.rows.length > 0 - ? this.parseCountValue(fallbackResult.rows[0]) - : 0 - return { - success: true, - data: { - username: normalizedUsername, - totalPosts: Math.max(primaryTotal, fallbackTotal) - } - } - } - - if (primaryResult.success) { - return { - success: true, - data: { - username: normalizedUsername, - totalPosts: primaryTotal - } - } - } - - return { success: false, error: primaryResult.error || fallbackResult.error || '统计单个好友朋友圈失败' } + return { success: false, error: countsResult.error || '统计单个好友朋友圈失败' } } // 安装朋友圈删除拦截 @@ -943,7 +961,12 @@ class SnsService { // 从数据库直接删除朋友圈记录 async deleteSnsPost(postId: string): Promise<{ success: boolean; error?: string }> { - return wcdbService.deleteSnsPost(postId) + const result = await wcdbService.deleteSnsPost(postId) + if (result.success) { + this.userPostCountsCache = null + this.exportStatsCache = null + } + return result } /** diff --git a/src/pages/SnsPage.tsx b/src/pages/SnsPage.tsx index b3459f3..5edb5b6 100644 --- a/src/pages/SnsPage.tsx +++ b/src/pages/SnsPage.tsx @@ -497,11 +497,12 @@ export default function SnsPage() { setAuthorTimelineTotalPosts(null) try { - const result = await window.electronAPI.sns.getUserPostStats(target.username) + const result = await window.electronAPI.sns.getUserPostCounts() if (requestToken !== authorTimelineStatsTokenRef.current) return - if (result.success && result.data) { - setAuthorTimelineTotalPosts(Math.max(0, Number(result.data.totalPosts || 0))) + if (result.success && result.counts) { + const totalPosts = result.counts[target.username] ?? 0 + setAuthorTimelineTotalPosts(Math.max(0, Number(totalPosts || 0))) } else { setAuthorTimelineTotalPosts(null) } diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index 0d05be4..72aaa57 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -789,6 +789,7 @@ export interface ElectronAPI { onExportProgress: (callback: (payload: { current: number; total: number; status: string }) => void) => () => void selectExportDir: () => Promise<{ canceled: boolean; filePath?: string }> getSnsUsernames: () => Promise<{ success: boolean; usernames?: string[]; error?: string }> + getUserPostCounts: () => Promise<{ success: boolean; counts?: Record; error?: string }> getExportStatsFast: () => Promise<{ success: boolean; data?: { totalPosts: number; totalFriends: number; myPosts: number | null }; error?: string }> getExportStats: () => Promise<{ success: boolean; data?: { totalPosts: number; totalFriends: number; myPosts: number | null }; error?: string }> getUserPostStats: (username: string) => Promise<{ success: boolean; data?: { username: string; totalPosts: number }; error?: string }>