UserDayStatisticsService.ts 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. import { AppDataSource, UserDayStatistics } from '../models/index.js';
  2. import { logger } from '../utils/logger.js';
  3. export interface UserDayStatisticsItem {
  4. accountId: number;
  5. fansCount?: number;
  6. worksCount?: number;
  7. playCount?: number;
  8. commentCount?: number;
  9. fansIncrease?: number;
  10. likeCount?: number;
  11. shareCount?: number;
  12. collectCount?: number;
  13. coverClickRate?: string;
  14. avgWatchDuration?: string;
  15. totalWatchDuration?: string;
  16. completionRate?: string;
  17. }
  18. export interface SaveResult {
  19. inserted: number;
  20. updated: number;
  21. }
  22. export class UserDayStatisticsService {
  23. private statisticsRepository = AppDataSource.getRepository(UserDayStatistics);
  24. /**
  25. * 获取「中国时区(Asia/Shanghai)当前日历日」的 Date,用于存库保证 record_date 与业务日期一致
  26. * 避免服务器非中国时区时出现“1月29日却生成 record_date=1月30”等问题
  27. */
  28. private getTodayInChina(): Date {
  29. const formatter = new Intl.DateTimeFormat('en-CA', {
  30. timeZone: 'Asia/Shanghai',
  31. year: 'numeric',
  32. month: '2-digit',
  33. day: '2-digit',
  34. });
  35. const parts = formatter.formatToParts(new Date());
  36. const get = (type: string) => parts.find((p) => p.type === type)?.value ?? '0';
  37. const y = parseInt(get('year'), 10);
  38. const m = parseInt(get('month'), 10) - 1;
  39. const d = parseInt(get('day'), 10);
  40. return new Date(Date.UTC(y, m, d, 0, 0, 0, 0));
  41. }
  42. /**
  43. * 保存用户每日统计数据
  44. * 同日更新,隔日新增
  45. * 「今天」以中国时区(Asia/Shanghai)为准,避免服务器时区导致 record_date 错日
  46. */
  47. async saveStatistics(item: UserDayStatisticsItem): Promise<SaveResult> {
  48. const today = this.getTodayInChina();
  49. // 检查今天是否已有记录
  50. const existing = await this.statisticsRepository.findOne({
  51. where: {
  52. accountId: item.accountId,
  53. recordDate: today,
  54. },
  55. });
  56. if (existing) {
  57. // 更新已有记录
  58. await this.statisticsRepository.update(existing.id, {
  59. fansCount: item.fansCount ?? existing.fansCount,
  60. worksCount: item.worksCount ?? existing.worksCount,
  61. playCount: item.playCount ?? existing.playCount,
  62. commentCount: item.commentCount ?? existing.commentCount,
  63. fansIncrease: item.fansIncrease ?? existing.fansIncrease,
  64. likeCount: item.likeCount ?? existing.likeCount,
  65. shareCount: item.shareCount ?? existing.shareCount,
  66. collectCount: item.collectCount ?? existing.collectCount,
  67. coverClickRate: item.coverClickRate ?? existing.coverClickRate ?? '0',
  68. avgWatchDuration: item.avgWatchDuration ?? existing.avgWatchDuration ?? '0',
  69. totalWatchDuration: item.totalWatchDuration ?? existing.totalWatchDuration ?? '0',
  70. completionRate: item.completionRate ?? existing.completionRate ?? '0',
  71. });
  72. logger.debug(`[UserDayStatistics] Updated record for account ${item.accountId}, date: ${today.toISOString().split('T')[0]}`);
  73. return { inserted: 0, updated: 1 };
  74. } else {
  75. // 插入新记录
  76. const newStat = this.statisticsRepository.create({
  77. accountId: item.accountId,
  78. recordDate: today,
  79. fansCount: item.fansCount ?? 0,
  80. worksCount: item.worksCount ?? 0,
  81. playCount: item.playCount ?? 0,
  82. commentCount: item.commentCount ?? 0,
  83. fansIncrease: item.fansIncrease ?? 0,
  84. likeCount: item.likeCount ?? 0,
  85. shareCount: item.shareCount ?? 0,
  86. collectCount: item.collectCount ?? 0,
  87. coverClickRate: item.coverClickRate ?? '0',
  88. avgWatchDuration: item.avgWatchDuration ?? '0',
  89. totalWatchDuration: item.totalWatchDuration ?? '0',
  90. completionRate: item.completionRate ?? '0',
  91. });
  92. await this.statisticsRepository.save(newStat);
  93. logger.debug(`[UserDayStatistics] Inserted new record for account ${item.accountId}, date: ${today.toISOString().split('T')[0]}`);
  94. return { inserted: 1, updated: 0 };
  95. }
  96. }
  97. /**
  98. * 保存指定日期的用户每日统计数据(按 accountId + recordDate 维度 upsert)
  99. * 说明:recordDate 会被归零到当天 00:00:00(本地时区),避免重复 key 不一致
  100. */
  101. async saveStatisticsForDate(
  102. accountId: number,
  103. recordDate: Date,
  104. patch: Omit<UserDayStatisticsItem, 'accountId'>
  105. ): Promise<SaveResult> {
  106. const d = new Date(recordDate);
  107. d.setHours(0, 0, 0, 0);
  108. const existing = await this.statisticsRepository.findOne({
  109. where: { accountId, recordDate: d },
  110. });
  111. if (existing) {
  112. await this.statisticsRepository.update(existing.id, {
  113. fansCount: patch.fansCount ?? existing.fansCount,
  114. worksCount: patch.worksCount ?? existing.worksCount,
  115. playCount: patch.playCount ?? existing.playCount,
  116. commentCount: patch.commentCount ?? existing.commentCount,
  117. fansIncrease: patch.fansIncrease ?? existing.fansIncrease,
  118. likeCount: patch.likeCount ?? existing.likeCount,
  119. shareCount: patch.shareCount ?? existing.shareCount,
  120. collectCount: patch.collectCount ?? existing.collectCount,
  121. coverClickRate: patch.coverClickRate ?? existing.coverClickRate ?? '0',
  122. avgWatchDuration: patch.avgWatchDuration ?? existing.avgWatchDuration ?? '0',
  123. totalWatchDuration: patch.totalWatchDuration ?? existing.totalWatchDuration ?? '0',
  124. completionRate: patch.completionRate ?? existing.completionRate ?? '0',
  125. });
  126. return { inserted: 0, updated: 1 };
  127. }
  128. const newStat = this.statisticsRepository.create({
  129. accountId,
  130. recordDate: d,
  131. fansCount: patch.fansCount ?? 0,
  132. worksCount: patch.worksCount ?? 0,
  133. playCount: patch.playCount ?? 0,
  134. commentCount: patch.commentCount ?? 0,
  135. fansIncrease: patch.fansIncrease ?? 0,
  136. likeCount: patch.likeCount ?? 0,
  137. shareCount: patch.shareCount ?? 0,
  138. collectCount: patch.collectCount ?? 0,
  139. coverClickRate: patch.coverClickRate ?? '0',
  140. avgWatchDuration: patch.avgWatchDuration ?? '0',
  141. totalWatchDuration: patch.totalWatchDuration ?? '0',
  142. completionRate: patch.completionRate ?? '0',
  143. });
  144. await this.statisticsRepository.save(newStat);
  145. return { inserted: 1, updated: 0 };
  146. }
  147. /**
  148. * 批量保存用户每日统计数据
  149. */
  150. async saveStatisticsBatch(items: UserDayStatisticsItem[]): Promise<SaveResult> {
  151. let insertedCount = 0;
  152. let updatedCount = 0;
  153. for (const item of items) {
  154. const result = await this.saveStatistics(item);
  155. insertedCount += result.inserted;
  156. updatedCount += result.updated;
  157. }
  158. logger.info(`[UserDayStatistics] Batch save completed: inserted=${insertedCount}, updated=${updatedCount}`);
  159. return { inserted: insertedCount, updated: updatedCount };
  160. }
  161. /**
  162. * 批量保存指定日期范围的统计数据(每条记录自带日期)
  163. */
  164. async saveStatisticsForDateBatch(
  165. items: Array<{ accountId: number; recordDate: Date } & Omit<UserDayStatisticsItem, 'accountId'>>
  166. ): Promise<SaveResult> {
  167. let insertedCount = 0;
  168. let updatedCount = 0;
  169. for (const it of items) {
  170. const { accountId, recordDate, ...patch } = it;
  171. const result = await this.saveStatisticsForDate(accountId, recordDate, patch);
  172. insertedCount += result.inserted;
  173. updatedCount += result.updated;
  174. }
  175. logger.info(`[UserDayStatistics] Date-batch save completed: inserted=${insertedCount}, updated=${updatedCount}`);
  176. return { inserted: insertedCount, updated: updatedCount };
  177. }
  178. /**
  179. * 获取账号指定日期的统计数据
  180. */
  181. async getStatisticsByDate(
  182. accountId: number,
  183. date: Date
  184. ): Promise<UserDayStatistics | null> {
  185. return await this.statisticsRepository.findOne({
  186. where: {
  187. accountId,
  188. recordDate: date,
  189. },
  190. });
  191. }
  192. /**
  193. * 获取账号指定日期范围的统计数据
  194. */
  195. async getStatisticsByDateRange(
  196. accountId: number,
  197. startDate: Date,
  198. endDate: Date
  199. ): Promise<UserDayStatistics[]> {
  200. return await this.statisticsRepository
  201. .createQueryBuilder('uds')
  202. .where('uds.account_id = :accountId', { accountId })
  203. .andWhere('uds.record_date >= :startDate', { startDate })
  204. .andWhere('uds.record_date <= :endDate', { endDate })
  205. .orderBy('uds.record_date', 'ASC')
  206. .getMany();
  207. }
  208. }