mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 15:25:50 +00:00
fix(sns): derive per-user totals from timeline counts map
This commit is contained in:
@@ -1509,6 +1509,10 @@ function registerIpcHandlers() {
|
|||||||
return snsService.getSnsUsernames()
|
return snsService.getSnsUsernames()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('sns:getUserPostCounts', async () => {
|
||||||
|
return snsService.getUserPostCounts()
|
||||||
|
})
|
||||||
|
|
||||||
ipcMain.handle('sns:getExportStats', async () => {
|
ipcMain.handle('sns:getExportStats', async () => {
|
||||||
return snsService.getExportStats()
|
return snsService.getExportStats()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -353,6 +353,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
getTimeline: (limit: number, offset: number, usernames?: string[], keyword?: string, startTime?: number, endTime?: number) =>
|
getTimeline: (limit: number, offset: number, usernames?: string[], keyword?: string, startTime?: number, endTime?: number) =>
|
||||||
ipcRenderer.invoke('sns:getTimeline', limit, offset, usernames, keyword, startTime, endTime),
|
ipcRenderer.invoke('sns:getTimeline', limit, offset, usernames, keyword, startTime, endTime),
|
||||||
getSnsUsernames: () => ipcRenderer.invoke('sns:getSnsUsernames'),
|
getSnsUsernames: () => ipcRenderer.invoke('sns:getSnsUsernames'),
|
||||||
|
getUserPostCounts: () => ipcRenderer.invoke('sns:getUserPostCounts'),
|
||||||
getExportStatsFast: () => ipcRenderer.invoke('sns:getExportStatsFast'),
|
getExportStatsFast: () => ipcRenderer.invoke('sns:getExportStatsFast'),
|
||||||
getExportStats: () => ipcRenderer.invoke('sns:getExportStats'),
|
getExportStats: () => ipcRenderer.invoke('sns:getExportStats'),
|
||||||
getUserPostStats: (username: string) => ipcRenderer.invoke('sns:getUserPostStats', username),
|
getUserPostStats: (username: string) => ipcRenderer.invoke('sns:getUserPostStats', username),
|
||||||
|
|||||||
@@ -292,7 +292,9 @@ class SnsService {
|
|||||||
private contactCache: ContactCacheService
|
private contactCache: ContactCacheService
|
||||||
private imageCache = new Map<string, string>()
|
private imageCache = new Map<string, string>()
|
||||||
private exportStatsCache: { totalPosts: number; totalFriends: number; myPosts: number | null; updatedAt: number } | null = null
|
private exportStatsCache: { totalPosts: number; totalFriends: number; myPosts: number | null; updatedAt: number } | null = null
|
||||||
|
private userPostCountsCache: { counts: Record<string, number>; updatedAt: number } | null = null
|
||||||
private readonly exportStatsCacheTtlMs = 5 * 60 * 1000
|
private readonly exportStatsCacheTtlMs = 5 * 60 * 1000
|
||||||
|
private readonly userPostCountsCacheTtlMs = 5 * 60 * 1000
|
||||||
private lastTimelineFallbackAt = 0
|
private lastTimelineFallbackAt = 0
|
||||||
private readonly timelineFallbackCooldownMs = 3 * 60 * 1000
|
private readonly timelineFallbackCooldownMs = 3 * 60 * 1000
|
||||||
|
|
||||||
@@ -506,10 +508,6 @@ class SnsService {
|
|||||||
return Number.isFinite(num) && num > 0 ? Math.floor(num) : 0
|
return Number.isFinite(num) && num > 0 ? Math.floor(num) : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
private escapeSqlString(value: string): string {
|
|
||||||
return value.replace(/'/g, "''")
|
|
||||||
}
|
|
||||||
|
|
||||||
private pickTimelineUsername(post: any): string {
|
private pickTimelineUsername(post: any): string {
|
||||||
const raw = post?.username ?? post?.user_name ?? post?.userName ?? ''
|
const raw = post?.username ?? post?.user_name ?? post?.userName ?? ''
|
||||||
if (typeof raw !== 'string') return ''
|
if (typeof raw !== 'string') return ''
|
||||||
@@ -868,62 +866,82 @@ class SnsService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getUserPostCountsFromTimeline(): Promise<Record<string, number>> {
|
||||||
|
const pageSize = 500
|
||||||
|
const counts: Record<string, number> = {}
|
||||||
|
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<string, number>; 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 }> {
|
async getUserPostStats(username: string): Promise<{ success: boolean; data?: { username: string; totalPosts: number }; error?: string }> {
|
||||||
const normalizedUsername = this.toOptionalString(username)
|
const normalizedUsername = this.toOptionalString(username)
|
||||||
if (!normalizedUsername) {
|
if (!normalizedUsername) {
|
||||||
return { success: false, error: '用户名不能为空' }
|
return { success: false, error: '用户名不能为空' }
|
||||||
}
|
}
|
||||||
|
|
||||||
const escapedUsername = this.escapeSqlString(normalizedUsername)
|
const countsResult = await this.getUserPostCounts({ preferCache: true })
|
||||||
const primaryResult = await wcdbService.execQuery(
|
if (countsResult.success) {
|
||||||
'sns',
|
const totalPosts = countsResult.counts?.[normalizedUsername] ?? 0
|
||||||
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) {
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
username: normalizedUsername,
|
username: normalizedUsername,
|
||||||
totalPosts: primaryTotal
|
totalPosts: Math.max(0, Number(totalPosts || 0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fallbackResult = await wcdbService.execQuery(
|
return { success: false, error: countsResult.error || '统计单个好友朋友圈失败' }
|
||||||
'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 || '统计单个好友朋友圈失败' }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 安装朋友圈删除拦截
|
// 安装朋友圈删除拦截
|
||||||
@@ -943,7 +961,12 @@ class SnsService {
|
|||||||
|
|
||||||
// 从数据库直接删除朋友圈记录
|
// 从数据库直接删除朋友圈记录
|
||||||
async deleteSnsPost(postId: string): Promise<{ success: boolean; error?: string }> {
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -497,11 +497,12 @@ export default function SnsPage() {
|
|||||||
setAuthorTimelineTotalPosts(null)
|
setAuthorTimelineTotalPosts(null)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await window.electronAPI.sns.getUserPostStats(target.username)
|
const result = await window.electronAPI.sns.getUserPostCounts()
|
||||||
if (requestToken !== authorTimelineStatsTokenRef.current) return
|
if (requestToken !== authorTimelineStatsTokenRef.current) return
|
||||||
|
|
||||||
if (result.success && result.data) {
|
if (result.success && result.counts) {
|
||||||
setAuthorTimelineTotalPosts(Math.max(0, Number(result.data.totalPosts || 0)))
|
const totalPosts = result.counts[target.username] ?? 0
|
||||||
|
setAuthorTimelineTotalPosts(Math.max(0, Number(totalPosts || 0)))
|
||||||
} else {
|
} else {
|
||||||
setAuthorTimelineTotalPosts(null)
|
setAuthorTimelineTotalPosts(null)
|
||||||
}
|
}
|
||||||
|
|||||||
1
src/types/electron.d.ts
vendored
1
src/types/electron.d.ts
vendored
@@ -789,6 +789,7 @@ export interface ElectronAPI {
|
|||||||
onExportProgress: (callback: (payload: { current: number; total: number; status: string }) => void) => () => void
|
onExportProgress: (callback: (payload: { current: number; total: number; status: string }) => void) => () => void
|
||||||
selectExportDir: () => Promise<{ canceled: boolean; filePath?: string }>
|
selectExportDir: () => Promise<{ canceled: boolean; filePath?: string }>
|
||||||
getSnsUsernames: () => Promise<{ success: boolean; usernames?: string[]; error?: string }>
|
getSnsUsernames: () => Promise<{ success: boolean; usernames?: string[]; error?: string }>
|
||||||
|
getUserPostCounts: () => Promise<{ success: boolean; counts?: Record<string, number>; error?: string }>
|
||||||
getExportStatsFast: () => Promise<{ success: boolean; data?: { totalPosts: number; totalFriends: number; myPosts: number | null }; 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 }>
|
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 }>
|
getUserPostStats: (username: string) => Promise<{ success: boolean; data?: { username: string; totalPosts: number }; error?: string }>
|
||||||
|
|||||||
Reference in New Issue
Block a user