feat(sns): show my post count in overview stats

This commit is contained in:
tisonhuang
2026-03-04 13:32:18 +08:00
parent 3a878dd019
commit 38a023d0b6
4 changed files with 78 additions and 16 deletions

View File

@@ -291,7 +291,7 @@ class SnsService {
private configService: ConfigService private configService: ConfigService
private contactCache: ContactCacheService private contactCache: ContactCacheService
private imageCache = new Map<string, string>() private imageCache = new Map<string, string>()
private exportStatsCache: { totalPosts: number; totalFriends: number; updatedAt: number } | null = null private exportStatsCache: { totalPosts: number; totalFriends: number; myPosts: number | null; updatedAt: number } | null = null
private readonly exportStatsCacheTtlMs = 5 * 60 * 1000 private readonly exportStatsCacheTtlMs = 5 * 60 * 1000
private lastTimelineFallbackAt = 0 private lastTimelineFallbackAt = 0
private readonly timelineFallbackCooldownMs = 3 * 60 * 1000 private readonly timelineFallbackCooldownMs = 3 * 60 * 1000
@@ -512,11 +512,13 @@ class SnsService {
return raw.trim() return raw.trim()
} }
private async getExportStatsFromTimeline(): Promise<{ totalPosts: number; totalFriends: number }> { private async getExportStatsFromTimeline(myWxid?: string): Promise<{ totalPosts: number; totalFriends: number; myPosts: number | null }> {
const pageSize = 500 const pageSize = 500
const uniqueUsers = new Set<string>() const uniqueUsers = new Set<string>()
let totalPosts = 0 let totalPosts = 0
let myPosts = 0
let offset = 0 let offset = 0
const normalizedMyWxid = this.toOptionalString(myWxid)
for (let round = 0; round < 2000; round++) { for (let round = 0; round < 2000; round++) {
const result = await wcdbService.getSnsTimeline(pageSize, offset, undefined, undefined, 0, 0) const result = await wcdbService.getSnsTimeline(pageSize, offset, undefined, undefined, 0, 0)
@@ -531,6 +533,7 @@ class SnsService {
for (const row of rows) { for (const row of rows) {
const username = this.pickTimelineUsername(row) const username = this.pickTimelineUsername(row)
if (username) uniqueUsers.add(username) if (username) uniqueUsers.add(username)
if (normalizedMyWxid && username === normalizedMyWxid) myPosts += 1
} }
if (rows.length < pageSize) break if (rows.length < pageSize) break
@@ -539,7 +542,8 @@ class SnsService {
return { return {
totalPosts, totalPosts,
totalFriends: uniqueUsers.size totalFriends: uniqueUsers.size,
myPosts: normalizedMyWxid ? myPosts : null
} }
} }
@@ -735,9 +739,10 @@ class SnsService {
} }
} }
private async getExportStatsFromTableCount(): Promise<{ totalPosts: number; totalFriends: number }> { private async getExportStatsFromTableCount(myWxid?: string): Promise<{ totalPosts: number; totalFriends: number; myPosts: number | null }> {
let totalPosts = 0 let totalPosts = 0
let totalFriends = 0 let totalFriends = 0
let myPosts: number | null = null
const postCountResult = await wcdbService.execQuery('sns', null, 'SELECT COUNT(1) AS total FROM SnsTimeLine') const postCountResult = await wcdbService.execQuery('sns', null, 'SELECT COUNT(1) AS total FROM SnsTimeLine')
if (postCountResult.success && postCountResult.rows && postCountResult.rows.length > 0) { if (postCountResult.success && postCountResult.rows && postCountResult.rows.length > 0) {
@@ -764,16 +769,40 @@ class SnsService {
} }
} }
return { totalPosts, totalFriends } const normalizedMyWxid = this.toOptionalString(myWxid)
if (normalizedMyWxid) {
const myPostPrimary = await wcdbService.execQuery(
'sns',
null,
"SELECT COUNT(1) AS total FROM SnsTimeLine WHERE user_name = ?",
[normalizedMyWxid]
)
if (myPostPrimary.success && myPostPrimary.rows && myPostPrimary.rows.length > 0) {
myPosts = this.parseCountValue(myPostPrimary.rows[0])
} else {
const myPostFallback = await wcdbService.execQuery(
'sns',
null,
"SELECT COUNT(1) AS total FROM SnsTimeLine WHERE userName = ?",
[normalizedMyWxid]
)
if (myPostFallback.success && myPostFallback.rows && myPostFallback.rows.length > 0) {
myPosts = this.parseCountValue(myPostFallback.rows[0])
}
}
}
return { totalPosts, totalFriends, myPosts }
} }
async getExportStats(options?: { async getExportStats(options?: {
allowTimelineFallback?: boolean allowTimelineFallback?: boolean
preferCache?: boolean preferCache?: boolean
}): Promise<{ success: boolean; data?: { totalPosts: number; totalFriends: number }; error?: string }> { }): Promise<{ success: boolean; data?: { totalPosts: number; totalFriends: number; myPosts: number | null }; error?: string }> {
const allowTimelineFallback = options?.allowTimelineFallback ?? true const allowTimelineFallback = options?.allowTimelineFallback ?? true
const preferCache = options?.preferCache ?? false const preferCache = options?.preferCache ?? false
const now = Date.now() const now = Date.now()
const myWxid = this.toOptionalString(this.configService.get('myWxid'))
try { try {
if (preferCache && this.exportStatsCache && now - this.exportStatsCache.updatedAt <= this.exportStatsCacheTtlMs) { if (preferCache && this.exportStatsCache && now - this.exportStatsCache.updatedAt <= this.exportStatsCacheTtlMs) {
@@ -781,12 +810,13 @@ class SnsService {
success: true, success: true,
data: { data: {
totalPosts: this.exportStatsCache.totalPosts, totalPosts: this.exportStatsCache.totalPosts,
totalFriends: this.exportStatsCache.totalFriends totalFriends: this.exportStatsCache.totalFriends,
myPosts: this.exportStatsCache.myPosts
} }
} }
} }
let { totalPosts, totalFriends } = await this.getExportStatsFromTableCount() let { totalPosts, totalFriends, myPosts } = await this.getExportStatsFromTableCount(myWxid)
let fallbackAttempted = false let fallbackAttempted = false
let fallbackError = '' let fallbackError = ''
@@ -798,7 +828,7 @@ class SnsService {
) { ) {
fallbackAttempted = true fallbackAttempted = true
try { try {
const timelineStats = await this.getExportStatsFromTimeline() const timelineStats = await this.getExportStatsFromTimeline(myWxid)
this.lastTimelineFallbackAt = Date.now() this.lastTimelineFallbackAt = Date.now()
if (timelineStats.totalPosts > 0) { if (timelineStats.totalPosts > 0) {
totalPosts = timelineStats.totalPosts totalPosts = timelineStats.totalPosts
@@ -806,6 +836,9 @@ class SnsService {
if (timelineStats.totalFriends > 0) { if (timelineStats.totalFriends > 0) {
totalFriends = timelineStats.totalFriends totalFriends = timelineStats.totalFriends
} }
if (timelineStats.myPosts !== null) {
myPosts = timelineStats.myPosts
}
} catch (error) { } catch (error) {
fallbackError = String(error) fallbackError = String(error)
console.error('[SnsService] getExportStats timeline fallback failed:', error) console.error('[SnsService] getExportStats timeline fallback failed:', error)
@@ -814,7 +847,10 @@ class SnsService {
const normalizedStats = { const normalizedStats = {
totalPosts: Math.max(0, Number(totalPosts || 0)), totalPosts: Math.max(0, Number(totalPosts || 0)),
totalFriends: Math.max(0, Number(totalFriends || 0)) totalFriends: Math.max(0, Number(totalFriends || 0)),
myPosts: myWxid
? (myPosts === null ? null : Math.max(0, Number(myPosts || 0)))
: null
} }
const computedHasData = normalizedStats.totalPosts > 0 || normalizedStats.totalFriends > 0 const computedHasData = normalizedStats.totalPosts > 0 || normalizedStats.totalFriends > 0
const cacheHasData = !!this.exportStatsCache && (this.exportStatsCache.totalPosts > 0 || this.exportStatsCache.totalFriends > 0) const cacheHasData = !!this.exportStatsCache && (this.exportStatsCache.totalPosts > 0 || this.exportStatsCache.totalFriends > 0)
@@ -825,7 +861,8 @@ class SnsService {
success: true, success: true,
data: { data: {
totalPosts: this.exportStatsCache.totalPosts, totalPosts: this.exportStatsCache.totalPosts,
totalFriends: this.exportStatsCache.totalFriends totalFriends: this.exportStatsCache.totalFriends,
myPosts: this.exportStatsCache.myPosts
} }
} }
} }
@@ -838,6 +875,7 @@ class SnsService {
this.exportStatsCache = { this.exportStatsCache = {
totalPosts: normalizedStats.totalPosts, totalPosts: normalizedStats.totalPosts,
totalFriends: normalizedStats.totalFriends, totalFriends: normalizedStats.totalFriends,
myPosts: normalizedStats.myPosts,
updatedAt: Date.now() updatedAt: Date.now()
} }
@@ -848,7 +886,8 @@ class SnsService {
success: true, success: true,
data: { data: {
totalPosts: this.exportStatsCache.totalPosts, totalPosts: this.exportStatsCache.totalPosts,
totalFriends: this.exportStatsCache.totalFriends totalFriends: this.exportStatsCache.totalFriends,
myPosts: this.exportStatsCache.myPosts
} }
} }
} }
@@ -856,7 +895,7 @@ class SnsService {
} }
} }
async getExportStatsFast(): Promise<{ success: boolean; data?: { totalPosts: number; totalFriends: number }; error?: string }> { async getExportStatsFast(): Promise<{ success: boolean; data?: { totalPosts: number; totalFriends: number; myPosts: number | null }; error?: string }> {
return this.getExportStats({ return this.getExportStats({
allowTimelineFallback: false, allowTimelineFallback: false,
preferCache: true preferCache: true

View File

@@ -25,6 +25,7 @@ interface Contact {
interface SnsOverviewStats { interface SnsOverviewStats {
totalPosts: number totalPosts: number
totalFriends: number totalFriends: number
myPosts: number | null
earliestTime: number | null earliestTime: number | null
latestTime: number | null latestTime: number | null
} }
@@ -39,6 +40,7 @@ export default function SnsPage() {
const [overviewStats, setOverviewStats] = useState<SnsOverviewStats>({ const [overviewStats, setOverviewStats] = useState<SnsOverviewStats>({
totalPosts: 0, totalPosts: 0,
totalFriends: 0, totalFriends: 0,
myPosts: null,
earliestTime: null, earliestTime: null,
latestTime: null latestTime: null
}) })
@@ -196,6 +198,9 @@ export default function SnsPage() {
setOverviewStats({ setOverviewStats({
totalPosts: cachedTotalPosts, totalPosts: cachedTotalPosts,
totalFriends: cachedTotalFriends, totalFriends: cachedTotalFriends,
myPosts: typeof cachedOverview.myPosts === 'number' && Number.isFinite(cachedOverview.myPosts) && cachedOverview.myPosts >= 0
? Math.floor(cachedOverview.myPosts)
: null,
earliestTime: cachedOverview.earliestTime ?? null, earliestTime: cachedOverview.earliestTime ?? null,
latestTime: cachedOverview.latestTime ?? null latestTime: cachedOverview.latestTime ?? null
}) })
@@ -234,6 +239,9 @@ export default function SnsPage() {
const totalPosts = Math.max(0, Number(statsResult.data.totalPosts || 0)) const totalPosts = Math.max(0, Number(statsResult.data.totalPosts || 0))
const totalFriends = Math.max(0, Number(statsResult.data.totalFriends || 0)) const totalFriends = Math.max(0, Number(statsResult.data.totalFriends || 0))
const myPosts = (typeof statsResult.data.myPosts === 'number' && Number.isFinite(statsResult.data.myPosts) && statsResult.data.myPosts >= 0)
? Math.floor(statsResult.data.myPosts)
: null
let earliestTime: number | null = null let earliestTime: number | null = null
let latestTime: number | null = null let latestTime: number | null = null
@@ -256,6 +264,7 @@ export default function SnsPage() {
const nextOverviewStats = { const nextOverviewStats = {
totalPosts, totalPosts,
totalFriends, totalFriends,
myPosts,
earliestTime, earliestTime,
latestTime latestTime
} }
@@ -279,7 +288,8 @@ export default function SnsPage() {
if (overviewStatsStatus === 'loading') { if (overviewStatsStatus === 'loading') {
return '统计中...' return '统计中...'
} }
return `${overviewStats.totalPosts} ${formatDateOnly(overviewStats.earliestTime)} ~ ${formatDateOnly(overviewStats.latestTime)} ${overviewStats.totalFriends} 位好友` const myPostsLabel = overviewStats.myPosts === null ? '--' : String(overviewStats.myPosts)
return `${overviewStats.totalPosts} 我的朋友圈 ${myPostsLabel} ${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' } = {}) => {

View File

@@ -469,6 +469,7 @@ export interface ExportSnsStatsCacheItem {
export interface SnsPageOverviewCache { export interface SnsPageOverviewCache {
totalPosts: number totalPosts: number
totalFriends: number totalFriends: number
myPosts: number | null
earliestTime: number | null earliestTime: number | null
latestTime: number | null latestTime: number | null
} }
@@ -610,12 +611,18 @@ export async function getSnsPageCache(scopeKey: string): Promise<SnsPageCacheIte
if (typeof v === 'number' && Number.isFinite(v) && v > 0) return Math.floor(v) if (typeof v === 'number' && Number.isFinite(v) && v > 0) return Math.floor(v)
return null return null
} }
const normalizeNullableCount = (v: unknown) => {
if (v === null || v === undefined) return null
if (typeof v === 'number' && Number.isFinite(v) && v >= 0) return Math.floor(v)
return null
}
return { return {
updatedAt: typeof raw.updatedAt === 'number' && Number.isFinite(raw.updatedAt) ? raw.updatedAt : 0, updatedAt: typeof raw.updatedAt === 'number' && Number.isFinite(raw.updatedAt) ? raw.updatedAt : 0,
overviewStats: { overviewStats: {
totalPosts: Math.max(0, normalizeNumber(overviewObj.totalPosts)), totalPosts: Math.max(0, normalizeNumber(overviewObj.totalPosts)),
totalFriends: Math.max(0, normalizeNumber(overviewObj.totalFriends)), totalFriends: Math.max(0, normalizeNumber(overviewObj.totalFriends)),
myPosts: normalizeNullableCount(overviewObj.myPosts),
earliestTime: normalizeNullableTimestamp(overviewObj.earliestTime), earliestTime: normalizeNullableTimestamp(overviewObj.earliestTime),
latestTime: normalizeNullableTimestamp(overviewObj.latestTime) latestTime: normalizeNullableTimestamp(overviewObj.latestTime)
}, },
@@ -639,12 +646,18 @@ export async function setSnsPageCache(
if (typeof v === 'number' && Number.isFinite(v) && v > 0) return Math.floor(v) if (typeof v === 'number' && Number.isFinite(v) && v > 0) return Math.floor(v)
return null return null
} }
const normalizeNullableCount = (v: unknown) => {
if (v === null || v === undefined) return null
if (typeof v === 'number' && Number.isFinite(v) && v >= 0) return Math.floor(v)
return null
}
map[scopeKey] = { map[scopeKey] = {
updatedAt: Date.now(), updatedAt: Date.now(),
overviewStats: { overviewStats: {
totalPosts: normalizeNumber(payload?.overviewStats?.totalPosts), totalPosts: normalizeNumber(payload?.overviewStats?.totalPosts),
totalFriends: normalizeNumber(payload?.overviewStats?.totalFriends), totalFriends: normalizeNumber(payload?.overviewStats?.totalFriends),
myPosts: normalizeNullableCount(payload?.overviewStats?.myPosts),
earliestTime: normalizeNullableTimestamp(payload?.overviewStats?.earliestTime), earliestTime: normalizeNullableTimestamp(payload?.overviewStats?.earliestTime),
latestTime: normalizeNullableTimestamp(payload?.overviewStats?.latestTime) latestTime: normalizeNullableTimestamp(payload?.overviewStats?.latestTime)
}, },

View File

@@ -730,8 +730,8 @@ export interface ElectronAPI {
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; data?: Record<string, number>; error?: string }> getUserPostCounts: () => Promise<{ success: boolean; data?: Record<string, number>; error?: string }>
getExportStatsFast: () => Promise<{ success: boolean; data?: { totalPosts: number; totalFriends: number }; 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 }; error?: string }> getExportStats: () => Promise<{ success: boolean; data?: { totalPosts: number; totalFriends: number; myPosts: number | null }; error?: string }>
installBlockDeleteTrigger: () => Promise<{ success: boolean; alreadyInstalled?: boolean; error?: string }> installBlockDeleteTrigger: () => Promise<{ success: boolean; alreadyInstalled?: boolean; error?: string }>
uninstallBlockDeleteTrigger: () => Promise<{ success: boolean; error?: string }> uninstallBlockDeleteTrigger: () => Promise<{ success: boolean; error?: string }>
checkBlockDeleteTrigger: () => Promise<{ success: boolean; installed?: boolean; error?: string }> checkBlockDeleteTrigger: () => Promise<{ success: boolean; installed?: boolean; error?: string }>