| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228 |
- 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<SaveResult> {
- 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<UserDayStatisticsItem, 'accountId'>
- ): Promise<SaveResult> {
- 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<SaveResult> {
- 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<UserDayStatisticsItem, 'accountId'>>
- ): Promise<SaveResult> {
- 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<UserDayStatistics | null> {
- return await this.statisticsRepository.findOne({
- where: {
- accountId,
- recordDate: date,
- },
- });
- }
- /**
- * 获取账号指定日期范围的统计数据
- */
- async getStatisticsByDateRange(
- accountId: number,
- startDate: Date,
- endDate: Date
- ): Promise<UserDayStatistics[]> {
- 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();
- }
- }
|