mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-24 23:06:51 +00:00
feat(sns): show my post count in overview stats
This commit is contained in:
@@ -291,7 +291,7 @@ class SnsService {
|
||||
private configService: ConfigService
|
||||
private contactCache: ContactCacheService
|
||||
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 lastTimelineFallbackAt = 0
|
||||
private readonly timelineFallbackCooldownMs = 3 * 60 * 1000
|
||||
@@ -512,11 +512,13 @@ class SnsService {
|
||||
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 uniqueUsers = new Set<string>()
|
||||
let totalPosts = 0
|
||||
let myPosts = 0
|
||||
let offset = 0
|
||||
const normalizedMyWxid = this.toOptionalString(myWxid)
|
||||
|
||||
for (let round = 0; round < 2000; round++) {
|
||||
const result = await wcdbService.getSnsTimeline(pageSize, offset, undefined, undefined, 0, 0)
|
||||
@@ -531,6 +533,7 @@ class SnsService {
|
||||
for (const row of rows) {
|
||||
const username = this.pickTimelineUsername(row)
|
||||
if (username) uniqueUsers.add(username)
|
||||
if (normalizedMyWxid && username === normalizedMyWxid) myPosts += 1
|
||||
}
|
||||
|
||||
if (rows.length < pageSize) break
|
||||
@@ -539,7 +542,8 @@ class SnsService {
|
||||
|
||||
return {
|
||||
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 totalFriends = 0
|
||||
let myPosts: number | null = null
|
||||
|
||||
const postCountResult = await wcdbService.execQuery('sns', null, 'SELECT COUNT(1) AS total FROM SnsTimeLine')
|
||||
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?: {
|
||||
allowTimelineFallback?: 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 preferCache = options?.preferCache ?? false
|
||||
const now = Date.now()
|
||||
const myWxid = this.toOptionalString(this.configService.get('myWxid'))
|
||||
|
||||
try {
|
||||
if (preferCache && this.exportStatsCache && now - this.exportStatsCache.updatedAt <= this.exportStatsCacheTtlMs) {
|
||||
@@ -781,12 +810,13 @@ class SnsService {
|
||||
success: true,
|
||||
data: {
|
||||
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 fallbackError = ''
|
||||
|
||||
@@ -798,7 +828,7 @@ class SnsService {
|
||||
) {
|
||||
fallbackAttempted = true
|
||||
try {
|
||||
const timelineStats = await this.getExportStatsFromTimeline()
|
||||
const timelineStats = await this.getExportStatsFromTimeline(myWxid)
|
||||
this.lastTimelineFallbackAt = Date.now()
|
||||
if (timelineStats.totalPosts > 0) {
|
||||
totalPosts = timelineStats.totalPosts
|
||||
@@ -806,6 +836,9 @@ class SnsService {
|
||||
if (timelineStats.totalFriends > 0) {
|
||||
totalFriends = timelineStats.totalFriends
|
||||
}
|
||||
if (timelineStats.myPosts !== null) {
|
||||
myPosts = timelineStats.myPosts
|
||||
}
|
||||
} catch (error) {
|
||||
fallbackError = String(error)
|
||||
console.error('[SnsService] getExportStats timeline fallback failed:', error)
|
||||
@@ -814,7 +847,10 @@ class SnsService {
|
||||
|
||||
const normalizedStats = {
|
||||
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 cacheHasData = !!this.exportStatsCache && (this.exportStatsCache.totalPosts > 0 || this.exportStatsCache.totalFriends > 0)
|
||||
@@ -825,7 +861,8 @@ class SnsService {
|
||||
success: true,
|
||||
data: {
|
||||
totalPosts: this.exportStatsCache.totalPosts,
|
||||
totalFriends: this.exportStatsCache.totalFriends
|
||||
totalFriends: this.exportStatsCache.totalFriends,
|
||||
myPosts: this.exportStatsCache.myPosts
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -838,6 +875,7 @@ class SnsService {
|
||||
this.exportStatsCache = {
|
||||
totalPosts: normalizedStats.totalPosts,
|
||||
totalFriends: normalizedStats.totalFriends,
|
||||
myPosts: normalizedStats.myPosts,
|
||||
updatedAt: Date.now()
|
||||
}
|
||||
|
||||
@@ -848,7 +886,8 @@ class SnsService {
|
||||
success: true,
|
||||
data: {
|
||||
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({
|
||||
allowTimelineFallback: false,
|
||||
preferCache: true
|
||||
|
||||
@@ -25,6 +25,7 @@ interface Contact {
|
||||
interface SnsOverviewStats {
|
||||
totalPosts: number
|
||||
totalFriends: number
|
||||
myPosts: number | null
|
||||
earliestTime: number | null
|
||||
latestTime: number | null
|
||||
}
|
||||
@@ -39,6 +40,7 @@ export default function SnsPage() {
|
||||
const [overviewStats, setOverviewStats] = useState<SnsOverviewStats>({
|
||||
totalPosts: 0,
|
||||
totalFriends: 0,
|
||||
myPosts: null,
|
||||
earliestTime: null,
|
||||
latestTime: null
|
||||
})
|
||||
@@ -196,6 +198,9 @@ export default function SnsPage() {
|
||||
setOverviewStats({
|
||||
totalPosts: cachedTotalPosts,
|
||||
totalFriends: cachedTotalFriends,
|
||||
myPosts: typeof cachedOverview.myPosts === 'number' && Number.isFinite(cachedOverview.myPosts) && cachedOverview.myPosts >= 0
|
||||
? Math.floor(cachedOverview.myPosts)
|
||||
: null,
|
||||
earliestTime: cachedOverview.earliestTime ?? null,
|
||||
latestTime: cachedOverview.latestTime ?? null
|
||||
})
|
||||
@@ -234,6 +239,9 @@ export default function SnsPage() {
|
||||
|
||||
const totalPosts = Math.max(0, Number(statsResult.data.totalPosts || 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 latestTime: number | null = null
|
||||
|
||||
@@ -256,6 +264,7 @@ export default function SnsPage() {
|
||||
const nextOverviewStats = {
|
||||
totalPosts,
|
||||
totalFriends,
|
||||
myPosts,
|
||||
earliestTime,
|
||||
latestTime
|
||||
}
|
||||
@@ -279,7 +288,8 @@ export default function SnsPage() {
|
||||
if (overviewStatsStatus === 'loading') {
|
||||
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' } = {}) => {
|
||||
|
||||
@@ -469,6 +469,7 @@ export interface ExportSnsStatsCacheItem {
|
||||
export interface SnsPageOverviewCache {
|
||||
totalPosts: number
|
||||
totalFriends: number
|
||||
myPosts: number | null
|
||||
earliestTime: 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)
|
||||
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 {
|
||||
updatedAt: typeof raw.updatedAt === 'number' && Number.isFinite(raw.updatedAt) ? raw.updatedAt : 0,
|
||||
overviewStats: {
|
||||
totalPosts: Math.max(0, normalizeNumber(overviewObj.totalPosts)),
|
||||
totalFriends: Math.max(0, normalizeNumber(overviewObj.totalFriends)),
|
||||
myPosts: normalizeNullableCount(overviewObj.myPosts),
|
||||
earliestTime: normalizeNullableTimestamp(overviewObj.earliestTime),
|
||||
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)
|
||||
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] = {
|
||||
updatedAt: Date.now(),
|
||||
overviewStats: {
|
||||
totalPosts: normalizeNumber(payload?.overviewStats?.totalPosts),
|
||||
totalFriends: normalizeNumber(payload?.overviewStats?.totalFriends),
|
||||
myPosts: normalizeNullableCount(payload?.overviewStats?.myPosts),
|
||||
earliestTime: normalizeNullableTimestamp(payload?.overviewStats?.earliestTime),
|
||||
latestTime: normalizeNullableTimestamp(payload?.overviewStats?.latestTime)
|
||||
},
|
||||
|
||||
4
src/types/electron.d.ts
vendored
4
src/types/electron.d.ts
vendored
@@ -730,8 +730,8 @@ export interface ElectronAPI {
|
||||
selectExportDir: () => Promise<{ canceled: boolean; filePath?: string }>
|
||||
getSnsUsernames: () => Promise<{ success: boolean; usernames?: string[]; error?: string }>
|
||||
getUserPostCounts: () => Promise<{ success: boolean; data?: Record<string, number>; error?: string }>
|
||||
getExportStatsFast: () => Promise<{ success: boolean; data?: { totalPosts: number; totalFriends: number }; error?: string }>
|
||||
getExportStats: () => 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; myPosts: number | null }; error?: string }>
|
||||
installBlockDeleteTrigger: () => Promise<{ success: boolean; alreadyInstalled?: boolean; error?: string }>
|
||||
uninstallBlockDeleteTrigger: () => Promise<{ success: boolean; error?: string }>
|
||||
checkBlockDeleteTrigger: () => Promise<{ success: boolean; installed?: boolean; error?: string }>
|
||||
|
||||
Reference in New Issue
Block a user