mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 07:16:51 +00:00
fix(sns): add overview stats status and fallback resilience
This commit is contained in:
@@ -497,6 +497,8 @@ class SnsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let { totalPosts, totalFriends } = await this.getExportStatsFromTableCount()
|
let { totalPosts, totalFriends } = await this.getExportStatsFromTableCount()
|
||||||
|
let fallbackAttempted = false
|
||||||
|
let fallbackError = ''
|
||||||
|
|
||||||
// 某些环境下 SnsTimeLine 统计查询会返回 0,这里在允许时回退到与导出同源的 timeline 接口统计。
|
// 某些环境下 SnsTimeLine 统计查询会返回 0,这里在允许时回退到与导出同源的 timeline 接口统计。
|
||||||
if (
|
if (
|
||||||
@@ -504,23 +506,52 @@ class SnsService {
|
|||||||
(totalPosts <= 0 || totalFriends <= 0) &&
|
(totalPosts <= 0 || totalFriends <= 0) &&
|
||||||
now - this.lastTimelineFallbackAt >= this.timelineFallbackCooldownMs
|
now - this.lastTimelineFallbackAt >= this.timelineFallbackCooldownMs
|
||||||
) {
|
) {
|
||||||
this.lastTimelineFallbackAt = now
|
fallbackAttempted = true
|
||||||
|
try {
|
||||||
const timelineStats = await this.getExportStatsFromTimeline()
|
const timelineStats = await this.getExportStatsFromTimeline()
|
||||||
|
this.lastTimelineFallbackAt = Date.now()
|
||||||
if (timelineStats.totalPosts > 0) {
|
if (timelineStats.totalPosts > 0) {
|
||||||
totalPosts = timelineStats.totalPosts
|
totalPosts = timelineStats.totalPosts
|
||||||
}
|
}
|
||||||
if (timelineStats.totalFriends > 0) {
|
if (timelineStats.totalFriends > 0) {
|
||||||
totalFriends = timelineStats.totalFriends
|
totalFriends = timelineStats.totalFriends
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
fallbackError = String(error)
|
||||||
|
console.error('[SnsService] getExportStats timeline fallback failed:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedStats = {
|
||||||
|
totalPosts: Math.max(0, Number(totalPosts || 0)),
|
||||||
|
totalFriends: Math.max(0, Number(totalFriends || 0))
|
||||||
|
}
|
||||||
|
const computedHasData = normalizedStats.totalPosts > 0 || normalizedStats.totalFriends > 0
|
||||||
|
const cacheHasData = !!this.exportStatsCache && (this.exportStatsCache.totalPosts > 0 || this.exportStatsCache.totalFriends > 0)
|
||||||
|
|
||||||
|
// 计算结果全 0 时,优先使用已有非零缓存,避免瞬时异常覆盖有效统计。
|
||||||
|
if (!computedHasData && cacheHasData && this.exportStatsCache) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
totalPosts: this.exportStatsCache.totalPosts,
|
||||||
|
totalFriends: this.exportStatsCache.totalFriends
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当主查询结果全 0 且回退统计执行失败时,返回失败给前端显示明确状态(而非错误地展示 0)。
|
||||||
|
if (!computedHasData && fallbackAttempted && fallbackError) {
|
||||||
|
return { success: false, error: fallbackError }
|
||||||
}
|
}
|
||||||
|
|
||||||
this.exportStatsCache = {
|
this.exportStatsCache = {
|
||||||
totalPosts,
|
totalPosts: normalizedStats.totalPosts,
|
||||||
totalFriends,
|
totalFriends: normalizedStats.totalFriends,
|
||||||
updatedAt: Date.now()
|
updatedAt: Date.now()
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: true, data: { totalPosts, totalFriends } }
|
return { success: true, data: normalizedStats }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (this.exportStatsCache) {
|
if (this.exportStatsCache) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -74,6 +74,22 @@
|
|||||||
&.loading {
|
&.loading {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
color: #d94f45;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.feed-stats-retry {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-actions {
|
.header-actions {
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ interface SnsOverviewStats {
|
|||||||
latestTime: number | null
|
latestTime: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OverviewStatsStatus = 'loading' | 'ready' | 'error'
|
||||||
|
|
||||||
export default function SnsPage() {
|
export default function SnsPage() {
|
||||||
const [posts, setPosts] = useState<SnsPost[]>([])
|
const [posts, setPosts] = useState<SnsPost[]>([])
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
@@ -38,7 +40,7 @@ export default function SnsPage() {
|
|||||||
earliestTime: null,
|
earliestTime: null,
|
||||||
latestTime: null
|
latestTime: null
|
||||||
})
|
})
|
||||||
const [overviewStatsLoading, setOverviewStatsLoading] = useState(false)
|
const [overviewStatsStatus, setOverviewStatsStatus] = useState<OverviewStatsStatus>('loading')
|
||||||
|
|
||||||
// Filter states
|
// Filter states
|
||||||
const [searchKeyword, setSearchKeyword] = useState('')
|
const [searchKeyword, setSearchKeyword] = useState('')
|
||||||
@@ -78,6 +80,7 @@ export default function SnsPage() {
|
|||||||
const [loadingNewer, setLoadingNewer] = useState(false)
|
const [loadingNewer, setLoadingNewer] = useState(false)
|
||||||
const postsRef = useRef<SnsPost[]>([])
|
const postsRef = useRef<SnsPost[]>([])
|
||||||
const overviewStatsRef = useRef<SnsOverviewStats>(overviewStats)
|
const overviewStatsRef = useRef<SnsOverviewStats>(overviewStats)
|
||||||
|
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)
|
||||||
@@ -92,6 +95,9 @@ export default function SnsPage() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
overviewStatsRef.current = overviewStats
|
overviewStatsRef.current = overviewStats
|
||||||
}, [overviewStats])
|
}, [overviewStats])
|
||||||
|
useEffect(() => {
|
||||||
|
overviewStatsStatusRef.current = overviewStatsStatus
|
||||||
|
}, [overviewStatsStatus])
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
selectedUsernamesRef.current = selectedUsernames
|
selectedUsernamesRef.current = selectedUsernames
|
||||||
}, [selectedUsernames])
|
}, [selectedUsernames])
|
||||||
@@ -141,14 +147,17 @@ export default function SnsPage() {
|
|||||||
try {
|
try {
|
||||||
const scopeKey = await ensureSnsCacheScopeKey()
|
const scopeKey = await ensureSnsCacheScopeKey()
|
||||||
if (!scopeKey) return
|
if (!scopeKey) return
|
||||||
|
const existingCache = await configService.getSnsPageCache(scopeKey)
|
||||||
let postsToStore = patch?.posts ?? postsRef.current
|
let postsToStore = patch?.posts ?? postsRef.current
|
||||||
if (!patch?.posts && postsToStore.length === 0) {
|
if (!patch?.posts && postsToStore.length === 0) {
|
||||||
const existingCache = await configService.getSnsPageCache(scopeKey)
|
|
||||||
if (existingCache && Array.isArray(existingCache.posts) && existingCache.posts.length > 0) {
|
if (existingCache && Array.isArray(existingCache.posts) && existingCache.posts.length > 0) {
|
||||||
postsToStore = existingCache.posts as SnsPost[]
|
postsToStore = existingCache.posts as SnsPost[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const overviewToStore = patch?.overviewStats ?? overviewStatsRef.current
|
const overviewToStore = patch?.overviewStats
|
||||||
|
?? (overviewStatsStatusRef.current === 'ready'
|
||||||
|
? overviewStatsRef.current
|
||||||
|
: existingCache?.overviewStats ?? overviewStatsRef.current)
|
||||||
await configService.setSnsPageCache(scopeKey, {
|
await configService.setSnsPageCache(scopeKey, {
|
||||||
overviewStats: overviewToStore,
|
overviewStats: overviewToStore,
|
||||||
posts: postsToStore.slice(0, SNS_PAGE_CACHE_POST_LIMIT)
|
posts: postsToStore.slice(0, SNS_PAGE_CACHE_POST_LIMIT)
|
||||||
@@ -167,12 +176,18 @@ export default function SnsPage() {
|
|||||||
|
|
||||||
const cachedOverview = cached.overviewStats
|
const cachedOverview = cached.overviewStats
|
||||||
if (cachedOverview) {
|
if (cachedOverview) {
|
||||||
|
const cachedTotalPosts = Math.max(0, Number(cachedOverview.totalPosts || 0))
|
||||||
|
const cachedTotalFriends = Math.max(0, Number(cachedOverview.totalFriends || 0))
|
||||||
|
const hasCachedPosts = Array.isArray(cached.posts) && cached.posts.length > 0
|
||||||
|
const hasOverviewData = cachedTotalPosts > 0 || cachedTotalFriends > 0
|
||||||
setOverviewStats({
|
setOverviewStats({
|
||||||
totalPosts: Math.max(0, Number(cachedOverview.totalPosts || 0)),
|
totalPosts: cachedTotalPosts,
|
||||||
totalFriends: Math.max(0, Number(cachedOverview.totalFriends || 0)),
|
totalFriends: cachedTotalFriends,
|
||||||
earliestTime: cachedOverview.earliestTime ?? null,
|
earliestTime: cachedOverview.earliestTime ?? null,
|
||||||
latestTime: cachedOverview.latestTime ?? null
|
latestTime: cachedOverview.latestTime ?? null
|
||||||
})
|
})
|
||||||
|
// 只有明确有统计值(或确实无帖子)时才把缓存视为 ready,避免历史异常 0 卡住显示。
|
||||||
|
setOverviewStatsStatus(hasOverviewData || !hasCachedPosts ? 'ready' : 'loading')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(cached.posts) && cached.posts.length > 0) {
|
if (Array.isArray(cached.posts) && cached.posts.length > 0) {
|
||||||
@@ -197,7 +212,7 @@ export default function SnsPage() {
|
|||||||
}, [ensureSnsCacheScopeKey])
|
}, [ensureSnsCacheScopeKey])
|
||||||
|
|
||||||
const loadOverviewStats = useCallback(async () => {
|
const loadOverviewStats = useCallback(async () => {
|
||||||
setOverviewStatsLoading(true)
|
setOverviewStatsStatus('loading')
|
||||||
try {
|
try {
|
||||||
const statsResult = await window.electronAPI.sns.getExportStats()
|
const statsResult = await window.electronAPI.sns.getExportStats()
|
||||||
if (!statsResult.success || !statsResult.data) {
|
if (!statsResult.success || !statsResult.data) {
|
||||||
@@ -232,14 +247,28 @@ export default function SnsPage() {
|
|||||||
latestTime
|
latestTime
|
||||||
}
|
}
|
||||||
setOverviewStats(nextOverviewStats)
|
setOverviewStats(nextOverviewStats)
|
||||||
|
setOverviewStatsStatus('ready')
|
||||||
void persistSnsPageCache({ overviewStats: nextOverviewStats })
|
void persistSnsPageCache({ overviewStats: nextOverviewStats })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load SNS overview stats:', error)
|
console.error('Failed to load SNS overview stats:', error)
|
||||||
} finally {
|
setOverviewStatsStatus('error')
|
||||||
setOverviewStatsLoading(false)
|
|
||||||
}
|
}
|
||||||
}, [persistSnsPageCache])
|
}, [persistSnsPageCache])
|
||||||
|
|
||||||
|
const renderOverviewStats = () => {
|
||||||
|
if (overviewStatsStatus === 'error') {
|
||||||
|
return (
|
||||||
|
<button type="button" className="feed-stats-retry" onClick={() => { void loadOverviewStats() }}>
|
||||||
|
统计失败,点击重试
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (overviewStatsStatus === 'loading') {
|
||||||
|
return '统计中...'
|
||||||
|
}
|
||||||
|
return `共 ${overviewStats.totalPosts} 条 | ${formatDateOnly(overviewStats.earliestTime)} ~ ${formatDateOnly(overviewStats.latestTime)} | ${overviewStats.totalFriends} 位好友`
|
||||||
|
}
|
||||||
|
|
||||||
const loadPosts = useCallback(async (options: { reset?: boolean, direction?: 'older' | 'newer' } = {}) => {
|
const loadPosts = useCallback(async (options: { reset?: boolean, direction?: 'older' | 'newer' } = {}) => {
|
||||||
const { reset = false, direction = 'older' } = options
|
const { reset = false, direction = 'older' } = options
|
||||||
if (loadingRef.current) return
|
if (loadingRef.current) return
|
||||||
@@ -513,8 +542,8 @@ export default function SnsPage() {
|
|||||||
<div className="feed-header">
|
<div className="feed-header">
|
||||||
<div className="feed-header-main">
|
<div className="feed-header-main">
|
||||||
<h2>朋友圈</h2>
|
<h2>朋友圈</h2>
|
||||||
<div className={`feed-stats-line ${overviewStatsLoading ? 'loading' : ''}`}>
|
<div className={`feed-stats-line ${overviewStatsStatus}`}>
|
||||||
共 {overviewStats.totalPosts} 条 | {formatDateOnly(overviewStats.earliestTime)} ~ {formatDateOnly(overviewStats.latestTime)} | {overviewStats.totalFriends} 位好友
|
{renderOverviewStats()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="header-actions">
|
<div className="header-actions">
|
||||||
|
|||||||
Reference in New Issue
Block a user