fix(export,sns): share sns user count cache across pages

This commit is contained in:
aits2026
2026-03-05 19:21:37 +08:00
parent 4e0038c813
commit 7ead55d801
3 changed files with 136 additions and 23 deletions

View File

@@ -2000,6 +2000,17 @@ function ExportPage() {
const value = Number(rawCount) const value = Number(rawCount)
normalizedCounts[username] = Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0 normalizedCounts[username] = Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0
} }
void (async () => {
try {
const scopeKey = exportCacheScopeReadyRef.current
? exportCacheScopeRef.current
: await ensureExportCacheScope()
await configService.setExportSnsUserPostCountsCache(scopeKey, normalizedCounts)
} catch (cacheError) {
console.error('写入导出页朋友圈条数缓存失败:', cacheError)
}
})()
} catch (error) { } catch (error) {
console.error('加载朋友圈用户条数失败:', error) console.error('加载朋友圈用户条数失败:', error)
if (runToken !== snsUserPostCountsHydrationTokenRef.current) return if (runToken !== snsUserPostCountsHydrationTokenRef.current) return
@@ -2040,7 +2051,7 @@ function ExportPage() {
} }
applyBatch() applyBatch()
}, [patchSessionLoadTraceStage, snsUserPostCountsStatus]) }, [ensureExportCacheScope, patchSessionLoadTraceStage, snsUserPostCountsStatus])
const loadSessionSnsTimelinePosts = useCallback(async (target: SessionSnsTimelineTarget, options?: { reset?: boolean }) => { const loadSessionSnsTimelinePosts = useCallback(async (target: SessionSnsTimelineTarget, options?: { reset?: boolean }) => {
const reset = Boolean(options?.reset) const reset = Boolean(options?.reset)

View File

@@ -113,12 +113,14 @@ export default function SnsPage() {
const [hasNewer, setHasNewer] = useState(false) const [hasNewer, setHasNewer] = useState(false)
const [loadingNewer, setLoadingNewer] = useState(false) const [loadingNewer, setLoadingNewer] = useState(false)
const postsRef = useRef<SnsPost[]>([]) const postsRef = useRef<SnsPost[]>([])
const contactsRef = useRef<Contact[]>([])
const overviewStatsRef = useRef<SnsOverviewStats>(overviewStats) const overviewStatsRef = useRef<SnsOverviewStats>(overviewStats)
const overviewStatsStatusRef = useRef<OverviewStatsStatus>(overviewStatsStatus) const overviewStatsStatusRef = useRef<OverviewStatsStatus>(overviewStatsStatus)
const selectedUsernamesRef = useRef<string[]>(selectedUsernames) const selectedUsernamesRef = useRef<string[]>(selectedUsernames)
const searchKeywordRef = useRef(searchKeyword) const searchKeywordRef = useRef(searchKeyword)
const jumpTargetDateRef = useRef<Date | undefined>(jumpTargetDate) const jumpTargetDateRef = useRef<Date | undefined>(jumpTargetDate)
const cacheScopeKeyRef = useRef('') const cacheScopeKeyRef = useRef('')
const snsUserPostCountsCacheScopeKeyRef = useRef('')
const scrollAdjustmentRef = useRef<{ scrollHeight: number; scrollTop: number } | null>(null) const scrollAdjustmentRef = useRef<{ scrollHeight: number; scrollTop: number } | null>(null)
const contactsLoadTokenRef = useRef(0) const contactsLoadTokenRef = useRef(0)
const contactsCountHydrationTokenRef = useRef(0) const contactsCountHydrationTokenRef = useRef(0)
@@ -132,6 +134,9 @@ export default function SnsPage() {
useEffect(() => { useEffect(() => {
postsRef.current = posts postsRef.current = posts
}, [posts]) }, [posts])
useEffect(() => {
contactsRef.current = contacts
}, [contacts])
useEffect(() => { useEffect(() => {
overviewStatsRef.current = overviewStats overviewStatsRef.current = overviewStats
}, [overviewStats]) }, [overviewStats])
@@ -222,6 +227,21 @@ export default function SnsPage() {
return scopeKey return scopeKey
}, []) }, [])
const ensureSnsUserPostCountsCacheScopeKey = useCallback(async () => {
if (snsUserPostCountsCacheScopeKeyRef.current) return snsUserPostCountsCacheScopeKeyRef.current
const [wxidRaw, dbPathRaw] = await Promise.all([
configService.getMyWxid(),
configService.getDbPath()
])
const wxid = String(wxidRaw || '').trim()
const dbPath = String(dbPathRaw || '').trim()
const scopeKey = (dbPath || wxid)
? `${dbPath}::${wxid}`
: 'default'
snsUserPostCountsCacheScopeKeyRef.current = scopeKey
return scopeKey
}, [])
const persistSnsPageCache = useCallback(async (patch?: { posts?: SnsPost[]; overviewStats?: SnsOverviewStats }) => { const persistSnsPageCache = useCallback(async (patch?: { posts?: SnsPost[]; overviewStats?: SnsOverviewStats }) => {
if (!isDefaultViewNow()) return if (!isDefaultViewNow()) return
try { try {
@@ -484,17 +504,25 @@ export default function SnsPage() {
} }
}, []) }, [])
const hydrateContactPostCounts = useCallback(async (usernames: string[]) => { const hydrateContactPostCounts = useCallback(async (usernames: string[], options?: { force?: boolean }) => {
const force = options?.force === true
const targets = usernames const targets = usernames
.map((username) => String(username || '').trim()) .map((username) => String(username || '').trim())
.filter(Boolean) .filter(Boolean)
stopContactsCountHydration(true) stopContactsCountHydration(true)
if (targets.length === 0) return if (targets.length === 0) return
const readySet = new Set(
contactsRef.current
.filter((contact) => contact.postCountStatus === 'ready' && typeof contact.postCount === 'number')
.map((contact) => contact.username)
)
const pendingTargets = force ? targets : targets.filter((username) => !readySet.has(username))
const runToken = ++contactsCountHydrationTokenRef.current const runToken = ++contactsCountHydrationTokenRef.current
const totalTargets = targets.length const totalTargets = targets.length
const targetSet = new Set(targets) const targetSet = new Set(pendingTargets)
if (pendingTargets.length > 0) {
setContacts((prev) => { setContacts((prev) => {
let changed = false let changed = false
const next = prev.map((contact) => { const next = prev.map((contact) => {
@@ -503,17 +531,20 @@ export default function SnsPage() {
changed = true changed = true
return { return {
...contact, ...contact,
postCount: undefined, postCount: force ? undefined : contact.postCount,
postCountStatus: 'loading' as ContactPostCountStatus postCountStatus: 'loading' as ContactPostCountStatus
} }
}) })
return changed ? sortContactsForRanking(next) : prev return changed ? sortContactsForRanking(next) : prev
}) })
}
const preResolved = Math.max(0, totalTargets - pendingTargets.length)
setContactsCountProgress({ setContactsCountProgress({
resolved: 0, resolved: preResolved,
total: totalTargets, total: totalTargets,
running: true running: pendingTargets.length > 0
}) })
if (pendingTargets.length === 0) return
let normalizedCounts: Record<string, number> = {} let normalizedCounts: Record<string, number> = {}
try { try {
@@ -523,17 +554,25 @@ export default function SnsPage() {
normalizedCounts = Object.fromEntries( normalizedCounts = Object.fromEntries(
Object.entries(result.counts).map(([username, value]) => [username, normalizePostCount(value)]) Object.entries(result.counts).map(([username, value]) => [username, normalizePostCount(value)])
) )
void (async () => {
try {
const scopeKey = await ensureSnsUserPostCountsCacheScopeKey()
await configService.setExportSnsUserPostCountsCache(scopeKey, normalizedCounts)
} catch (cacheError) {
console.error('Failed to persist SNS user post counts cache:', cacheError)
}
})()
} }
} catch (error) { } catch (error) {
console.error('Failed to load contact post counts:', error) console.error('Failed to load contact post counts:', error)
} }
let resolved = 0 let resolved = preResolved
let cursor = 0 let cursor = 0
const applyBatch = () => { const applyBatch = () => {
if (runToken !== contactsCountHydrationTokenRef.current) return if (runToken !== contactsCountHydrationTokenRef.current) return
const batch = targets.slice(cursor, cursor + CONTACT_COUNT_BATCH_SIZE) const batch = pendingTargets.slice(cursor, cursor + CONTACT_COUNT_BATCH_SIZE)
if (batch.length === 0) { if (batch.length === 0) {
setContactsCountProgress({ setContactsCountProgress({
resolved: totalTargets, resolved: totalTargets,
@@ -585,6 +624,9 @@ export default function SnsPage() {
stopContactsCountHydration(true) stopContactsCountHydration(true)
setContactsLoading(true) setContactsLoading(true)
try { try {
const snsPostCountsScopeKey = await ensureSnsUserPostCountsCacheScopeKey()
const cachedPostCountsItem = await configService.getExportSnsUserPostCountsCache(snsPostCountsScopeKey)
const cachedPostCounts = cachedPostCountsItem?.counts || {}
const [contactsResult, sessionsResult] = await Promise.all([ const [contactsResult, sessionsResult] = await Promise.all([
window.electronAPI.chat.getContacts(), window.electronAPI.chat.getContacts(),
window.electronAPI.chat.getSessions() window.electronAPI.chat.getSessions()
@@ -610,14 +652,16 @@ export default function SnsPage() {
if (contactsResult.success && contactsResult.contacts) { if (contactsResult.success && contactsResult.contacts) {
for (const c of contactsResult.contacts) { for (const c of contactsResult.contacts) {
if (c.type === 'friend' || c.type === 'former_friend') { if (c.type === 'friend' || c.type === 'former_friend') {
const cachedCount = cachedPostCounts[c.username]
const hasCachedCount = typeof cachedCount === 'number' && Number.isFinite(cachedCount)
contactMap.set(c.username, { contactMap.set(c.username, {
username: c.username, username: c.username,
displayName: c.displayName, displayName: c.displayName,
avatarUrl: c.avatarUrl, avatarUrl: c.avatarUrl,
type: c.type === 'former_friend' ? 'former_friend' : 'friend', type: c.type === 'former_friend' ? 'former_friend' : 'friend',
lastSessionTimestamp: Number(sessionTimestampMap.get(c.username) || 0), lastSessionTimestamp: Number(sessionTimestampMap.get(c.username) || 0),
postCount: undefined, postCount: hasCachedCount ? Math.max(0, Math.floor(cachedCount)) : undefined,
postCountStatus: 'idle' postCountStatus: hasCachedCount ? 'ready' : 'idle'
}) })
} }
} }
@@ -668,7 +712,7 @@ export default function SnsPage() {
setContactsLoading(false) setContactsLoading(false)
} }
} }
}, [hydrateContactPostCounts, sortContactsForRanking, stopContactsCountHydration]) }, [ensureSnsUserPostCountsCacheScopeKey, hydrateContactPostCounts, sortContactsForRanking, stopContactsCountHydration])
const closeAuthorTimeline = useCallback(() => { const closeAuthorTimeline = useCallback(() => {
authorTimelineRequestTokenRef.current += 1 authorTimelineRequestTokenRef.current += 1

View File

@@ -41,6 +41,7 @@ export const CONFIG_KEYS = {
EXPORT_SESSION_MESSAGE_COUNT_CACHE_MAP: 'exportSessionMessageCountCacheMap', EXPORT_SESSION_MESSAGE_COUNT_CACHE_MAP: 'exportSessionMessageCountCacheMap',
EXPORT_SESSION_CONTENT_METRIC_CACHE_MAP: 'exportSessionContentMetricCacheMap', EXPORT_SESSION_CONTENT_METRIC_CACHE_MAP: 'exportSessionContentMetricCacheMap',
EXPORT_SNS_STATS_CACHE_MAP: 'exportSnsStatsCacheMap', EXPORT_SNS_STATS_CACHE_MAP: 'exportSnsStatsCacheMap',
EXPORT_SNS_USER_POST_COUNTS_CACHE_MAP: 'exportSnsUserPostCountsCacheMap',
SNS_PAGE_CACHE_MAP: 'snsPageCacheMap', SNS_PAGE_CACHE_MAP: 'snsPageCacheMap',
CONTACTS_LOAD_TIMEOUT_MS: 'contactsLoadTimeoutMs', CONTACTS_LOAD_TIMEOUT_MS: 'contactsLoadTimeoutMs',
CONTACTS_LIST_CACHE_MAP: 'contactsListCacheMap', CONTACTS_LIST_CACHE_MAP: 'contactsListCacheMap',
@@ -533,6 +534,11 @@ export interface ExportSnsStatsCacheItem {
totalFriends: number totalFriends: number
} }
export interface ExportSnsUserPostCountsCacheItem {
updatedAt: number
counts: Record<string, number>
}
export interface SnsPageOverviewCache { export interface SnsPageOverviewCache {
totalPosts: number totalPosts: number
totalFriends: number totalFriends: number
@@ -740,6 +746,58 @@ export async function setExportSnsStatsCache(
await config.set(CONFIG_KEYS.EXPORT_SNS_STATS_CACHE_MAP, map) await config.set(CONFIG_KEYS.EXPORT_SNS_STATS_CACHE_MAP, map)
} }
export async function getExportSnsUserPostCountsCache(scopeKey: string): Promise<ExportSnsUserPostCountsCacheItem | null> {
if (!scopeKey) return null
const value = await config.get(CONFIG_KEYS.EXPORT_SNS_USER_POST_COUNTS_CACHE_MAP)
if (!value || typeof value !== 'object') return null
const rawMap = value as Record<string, unknown>
const rawItem = rawMap[scopeKey]
if (!rawItem || typeof rawItem !== 'object') return null
const raw = rawItem as Record<string, unknown>
const rawCounts = raw.counts
if (!rawCounts || typeof rawCounts !== 'object') return null
const counts: Record<string, number> = {}
for (const [rawUsername, rawCount] of Object.entries(rawCounts as Record<string, unknown>)) {
const username = String(rawUsername || '').trim()
if (!username) continue
const valueNum = Number(rawCount)
counts[username] = Number.isFinite(valueNum) ? Math.max(0, Math.floor(valueNum)) : 0
}
const updatedAt = typeof raw.updatedAt === 'number' && Number.isFinite(raw.updatedAt)
? raw.updatedAt
: 0
return { updatedAt, counts }
}
export async function setExportSnsUserPostCountsCache(
scopeKey: string,
counts: Record<string, number>
): Promise<void> {
if (!scopeKey) return
const current = await config.get(CONFIG_KEYS.EXPORT_SNS_USER_POST_COUNTS_CACHE_MAP)
const map = current && typeof current === 'object'
? { ...(current as Record<string, unknown>) }
: {}
const normalized: Record<string, number> = {}
for (const [rawUsername, rawCount] of Object.entries(counts || {})) {
const username = String(rawUsername || '').trim()
if (!username) continue
const valueNum = Number(rawCount)
normalized[username] = Number.isFinite(valueNum) ? Math.max(0, Math.floor(valueNum)) : 0
}
map[scopeKey] = {
updatedAt: Date.now(),
counts: normalized
}
await config.set(CONFIG_KEYS.EXPORT_SNS_USER_POST_COUNTS_CACHE_MAP, map)
}
export async function getSnsPageCache(scopeKey: string): Promise<SnsPageCacheItem | null> { export async function getSnsPageCache(scopeKey: string): Promise<SnsPageCacheItem | null> {
if (!scopeKey) return null if (!scopeKey) return null
const value = await config.get(CONFIG_KEYS.SNS_PAGE_CACHE_MAP) const value = await config.get(CONFIG_KEYS.SNS_PAGE_CACHE_MAP)