import { AppDataSource, UserDayStatistics } from '../models/index.js'; import { logger } from '../utils/logger.js'; export interface UserDayStatisticsItem { accountId: number; fansCount?: number; worksCount?: number; playCount?: number; commentCount?: number; fansIncrease?: number; likeCount?: number; shareCount?: number; collectCount?: number; coverClickRate?: string; avgWatchDuration?: string; totalWatchDuration?: string; completionRate?: string; } export interface SaveResult { inserted: number; updated: number; } export class UserDayStatisticsService { private statisticsRepository = AppDataSource.getRepository(UserDayStatistics); /** * 获取「中国时区(Asia/Shanghai)当前日历日」的 Date,用于存库保证 record_date 与业务日期一致 * 避免服务器非中国时区时出现“1月29日却生成 record_date=1月30”等问题 */ private getTodayInChina(): Date { const formatter = new Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit', }); const parts = formatter.formatToParts(new Date()); const get = (type: string) => parts.find((p) => p.type === type)?.value ?? '0'; const y = parseInt(get('year'), 10); const m = parseInt(get('month'), 10) - 1; const d = parseInt(get('day'), 10); return new Date(Date.UTC(y, m, d, 0, 0, 0, 0)); } /** * 保存用户每日统计数据 * 同日更新,隔日新增 * 「今天」以中国时区(Asia/Shanghai)为准,避免服务器时区导致 record_date 错日 */ async saveStatistics(item: UserDayStatisticsItem): Promise { const today = this.getTodayInChina(); // 检查今天是否已有记录 const existing = await this.statisticsRepository.findOne({ where: { accountId: item.accountId, recordDate: today, }, }); if (existing) { // 更新已有记录 await this.statisticsRepository.update(existing.id, { fansCount: item.fansCount ?? existing.fansCount, worksCount: item.worksCount ?? existing.worksCount, playCount: item.playCount ?? existing.playCount, commentCount: item.commentCount ?? existing.commentCount, fansIncrease: item.fansIncrease ?? existing.fansIncrease, likeCount: item.likeCount ?? existing.likeCount, shareCount: item.shareCount ?? existing.shareCount, collectCount: item.collectCount ?? existing.collectCount, coverClickRate: item.coverClickRate ?? existing.coverClickRate ?? '0', avgWatchDuration: item.avgWatchDuration ?? existing.avgWatchDuration ?? '0', totalWatchDuration: item.totalWatchDuration ?? existing.totalWatchDuration ?? '0', completionRate: item.completionRate ?? existing.completionRate ?? '0', }); logger.debug(`[UserDayStatistics] Updated record for account ${item.accountId}, date: ${today.toISOString().split('T')[0]}`); return { inserted: 0, updated: 1 }; } else { // 插入新记录 const newStat = this.statisticsRepository.create({ accountId: item.accountId, recordDate: today, fansCount: item.fansCount ?? 0, worksCount: item.worksCount ?? 0, playCount: item.playCount ?? 0, commentCount: item.commentCount ?? 0, fansIncrease: item.fansIncrease ?? 0, likeCount: item.likeCount ?? 0, shareCount: item.shareCount ?? 0, collectCount: item.collectCount ?? 0, coverClickRate: item.coverClickRate ?? '0', avgWatchDuration: item.avgWatchDuration ?? '0', totalWatchDuration: item.totalWatchDuration ?? '0', completionRate: item.completionRate ?? '0', }); await this.statisticsRepository.save(newStat); logger.debug(`[UserDayStatistics] Inserted new record for account ${item.accountId}, date: ${today.toISOString().split('T')[0]}`); return { inserted: 1, updated: 0 }; } } /** * 保存指定日期的用户每日统计数据(按 accountId + recordDate 维度 upsert) * 说明:recordDate 会被归零到当天 00:00:00(本地时区),避免重复 key 不一致 */ async saveStatisticsForDate( accountId: number, recordDate: Date, patch: Omit ): Promise { const d = new Date(recordDate); d.setHours(0, 0, 0, 0); const existing = await this.statisticsRepository.findOne({ where: { accountId, recordDate: d }, }); if (existing) { await this.statisticsRepository.update(existing.id, { fansCount: patch.fansCount ?? existing.fansCount, worksCount: patch.worksCount ?? existing.worksCount, playCount: patch.playCount ?? existing.playCount, commentCount: patch.commentCount ?? existing.commentCount, fansIncrease: patch.fansIncrease ?? existing.fansIncrease, likeCount: patch.likeCount ?? existing.likeCount, shareCount: patch.shareCount ?? existing.shareCount, collectCount: patch.collectCount ?? existing.collectCount, coverClickRate: patch.coverClickRate ?? existing.coverClickRate ?? '0', avgWatchDuration: patch.avgWatchDuration ?? existing.avgWatchDuration ?? '0', totalWatchDuration: patch.totalWatchDuration ?? existing.totalWatchDuration ?? '0', completionRate: patch.completionRate ?? existing.completionRate ?? '0', }); return { inserted: 0, updated: 1 }; } const newStat = this.statisticsRepository.create({ accountId, recordDate: d, fansCount: patch.fansCount ?? 0, worksCount: patch.worksCount ?? 0, playCount: patch.playCount ?? 0, commentCount: patch.commentCount ?? 0, fansIncrease: patch.fansIncrease ?? 0, likeCount: patch.likeCount ?? 0, shareCount: patch.shareCount ?? 0, collectCount: patch.collectCount ?? 0, coverClickRate: patch.coverClickRate ?? '0', avgWatchDuration: patch.avgWatchDuration ?? '0', totalWatchDuration: patch.totalWatchDuration ?? '0', completionRate: patch.completionRate ?? '0', }); await this.statisticsRepository.save(newStat); return { inserted: 1, updated: 0 }; } /** * 批量保存用户每日统计数据 */ async saveStatisticsBatch(items: UserDayStatisticsItem[]): Promise { let insertedCount = 0; let updatedCount = 0; for (const item of items) { const result = await this.saveStatistics(item); insertedCount += result.inserted; updatedCount += result.updated; } logger.info(`[UserDayStatistics] Batch save completed: inserted=${insertedCount}, updated=${updatedCount}`); return { inserted: insertedCount, updated: updatedCount }; } /** * 批量保存指定日期范围的统计数据(每条记录自带日期) */ async saveStatisticsForDateBatch( items: Array<{ accountId: number; recordDate: Date } & Omit> ): Promise { let insertedCount = 0; let updatedCount = 0; for (const it of items) { const { accountId, recordDate, ...patch } = it; const result = await this.saveStatisticsForDate(accountId, recordDate, patch); insertedCount += result.inserted; updatedCount += result.updated; } logger.info(`[UserDayStatistics] Date-batch save completed: inserted=${insertedCount}, updated=${updatedCount}`); return { inserted: insertedCount, updated: updatedCount }; } /** * 获取账号指定日期的统计数据 */ async getStatisticsByDate( accountId: number, date: Date ): Promise { return await this.statisticsRepository.findOne({ where: { accountId, recordDate: date, }, }); } /** * 获取账号指定日期范围的统计数据 */ async getStatisticsByDateRange( accountId: number, startDate: Date, endDate: Date ): Promise { return await this.statisticsRepository .createQueryBuilder('uds') .where('uds.account_id = :accountId', { accountId }) .andWhere('uds.record_date >= :startDate', { startDate }) .andWhere('uds.record_date <= :endDate', { endDate }) .orderBy('uds.record_date', 'ASC') .getMany(); } }