StatsService.ts 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. import { Repository } from 'typeorm';
  2. import { AppDataSource } from '../config/database';
  3. import { WeightRecord } from '../entities/WeightRecord';
  4. import { WeightGoal, GoalStatus } from '../entities/WeightGoal';
  5. export class StatsService {
  6. private weightRecordRepository: Repository<WeightRecord>;
  7. private weightGoalRepository: Repository<WeightGoal>;
  8. constructor() {
  9. this.weightRecordRepository = AppDataSource.getRepository(WeightRecord);
  10. this.weightGoalRepository = AppDataSource.getRepository(WeightGoal);
  11. }
  12. /**
  13. * 获取用户统计数据
  14. */
  15. async getStats(userId: string, from?: string, to?: string): Promise<any> {
  16. // 获取记录
  17. const queryBuilder = this.weightRecordRepository
  18. .createQueryBuilder('wr')
  19. .where('wr.userId = :userId', { userId })
  20. .andWhere('wr.deletedAt IS NULL');
  21. if (from) {
  22. queryBuilder.andWhere('wr.recordDate >= :from', { from });
  23. }
  24. if (to) {
  25. queryBuilder.andWhere('wr.recordDate <= :to', { to });
  26. }
  27. const records = await queryBuilder
  28. .orderBy('wr.recordDate', 'ASC')
  29. .getMany();
  30. if (records.length === 0) {
  31. return {
  32. currentWeight: null,
  33. weightChange: null,
  34. daysTracked: 0,
  35. avgWeeklyChange: null,
  36. bodyFatChange: null,
  37. goalProgress: null,
  38. goalETA: null
  39. };
  40. }
  41. // 基础统计
  42. const firstRecord = records[0];
  43. const latestRecord = records[records.length - 1];
  44. const currentWeight = Number(latestRecord.weight);
  45. const initialWeight = Number(firstRecord.weight);
  46. const weightChange = Number((currentWeight - initialWeight).toFixed(2));
  47. const bodyFatChange = Number((Number(latestRecord.bodyFat) - Number(firstRecord.bodyFat)).toFixed(2));
  48. // 天数统计
  49. const firstDate = new Date(firstRecord.recordDate);
  50. const latestDate = new Date(latestRecord.recordDate);
  51. const daysTracked = Math.ceil((latestDate.getTime() - firstDate.getTime()) / (1000 * 60 * 60 * 24)) + 1;
  52. // 平均每周变化
  53. const weeksTracked = daysTracked / 7;
  54. const avgWeeklyChange = weeksTracked > 0 ? Number((weightChange / weeksTracked).toFixed(2)) : 0;
  55. // 获取活跃目标
  56. const goal = await this.weightGoalRepository.findOne({
  57. where: { userId, status: GoalStatus.ACTIVE }
  58. });
  59. let goalProgress: number | null = null;
  60. let goalETA: string | null = null;
  61. if (goal) {
  62. const totalWeightDiff = Math.abs(Number(goal.targetWeight) - Number(goal.startWeight));
  63. const currentWeightDiff = Math.abs(Number(goal.targetWeight) - currentWeight);
  64. goalProgress = totalWeightDiff > 0
  65. ? Number((((totalWeightDiff - currentWeightDiff) / totalWeightDiff) * 100).toFixed(1))
  66. : 100;
  67. // 计算ETA
  68. if (avgWeeklyChange !== 0 && currentWeightDiff > 0) {
  69. const weeksNeeded = currentWeightDiff / Math.abs(avgWeeklyChange);
  70. const etaDate = new Date();
  71. etaDate.setDate(etaDate.getDate() + Math.ceil(weeksNeeded * 7));
  72. goalETA = etaDate.toISOString().split('T')[0];
  73. }
  74. }
  75. return {
  76. currentWeight,
  77. weightChange,
  78. daysTracked,
  79. avgWeeklyChange,
  80. bodyFatChange,
  81. goalProgress,
  82. goalETA
  83. };
  84. }
  85. }