HarmonyOS应用使用统计系统设计——数据埋点与成就系统实现

HarmonyOS应用使用统计系统设计——数据埋点与成就系统实现

技术栈:HarmonyOS 5.0 + ArkTS + Preferences

适用场景:用户行为分析、成就系统、数据可视化


前言

了解用户如何使用应用对产品迭代至关重要。本文将介绍如何在HarmonyOS应用中实现一个完整的使用统计系统,包括数据记录、统计分析和成就系统。

一、数据结构设计

1.1 使用记录

export interface UsageRecord {
  id: string;
  appId: string;      // 功能模块ID
  appName: string;    // 功能名称
  duration: number;   // 使用时长(秒)
  timestamp: number;  // 时间戳
  date: string;       // 日期 YYYY-MM-DD
}

1.2 统计数据

export interface StatisticsData {
  totalUseCount: number;    // 总使用次数
  totalDuration: number;    // 总使用时长
  todayUseCount: number;    // 今日使用次数
  todayDuration: number;    // 今日使用时长
  weeklyData: DailyData[];  // 周数据
  monthlyData: DailyData[]; // 月数据
  appUsage: AppUsageData[]; // 各功能使用统计
  achievements: Achievement[];
}

export interface DailyData {
  date: string;
  useCount: number;
  duration: number;
}

export interface AppUsageData {
  appId: string;
  appName: string;
  useCount: number;
  duration: number;
}

1.3 成就系统

export interface Achievement {
  id: string;
  name: string;
  description: string;
  icon: string;
  unlocked: boolean;
  unlockedAt?: number;
  requirement: number;  // 达成条件
  current: number;      // 当前进度
}

二、统计工具类实现

export class StatisticsUtil {
  private static readonly RECORDS_KEY = 'usage_records';
  private static readonly ACHIEVEMENTS_KEY = 'achievements';

  /**
   * 记录一次使用
   */
  static async recordUsage(appId: string, appName: string, duration: number): Promise<void> {
    const now = Date.now();
    const date = new Date(now).toISOString().split('T')[0];
    
    const record: UsageRecord = {
      id: `${now}_${Math.random().toString(36).substr(2, 9)}`,
      appId,
      appName,
      duration,
      timestamp: now,
      date
    };

    const records = await StatisticsUtil.getRecords();
    records.push(record);

    // 只保留最近30天的记录
    const thirtyDaysAgo = now - 30 * 24 * 60 * 60 * 1000;
    const filteredRecords = records.filter(r => r.timestamp > thirtyDaysAgo);

    await PreferencesUtil.putString(StatisticsUtil.RECORDS_KEY, JSON.stringify(filteredRecords));
    await StatisticsUtil.checkAchievements();
  }

  /**
   * 获取统计数据
   */
  static async getStatistics(): Promise<StatisticsData> {
    const records = await StatisticsUtil.getRecords();
    const now = Date.now();
    const today = new Date(now).toISOString().split('T')[0];
    const weekAgo = now - 7 * 24 * 60 * 60 * 1000;

    // 总计
    const totalUseCount = records.length;
    const totalDuration = records.reduce((sum, r) => sum + r.duration, 0);

    // 今日
    const todayRecords = records.filter(r => r.date === today);
    const todayUseCount = todayRecords.length;
    const todayDuration = todayRecords.reduce((sum, r) => sum + r.duration, 0);

    // 周数据
    const weeklyData = StatisticsUtil.aggregateByDate(
      records.filter(r => r.timestamp > weekAgo)
    );

    // 应用使用统计
    const appUsage = StatisticsUtil.aggregateByApp(records);

    // 成就
    const achievements = await StatisticsUtil.getAchievements(totalUseCount, totalDuration);

    return {
      totalUseCount,
      totalDuration,
      todayUseCount,
      todayDuration,
      weeklyData,
      monthlyData: [],
      appUsage,
      achievements
    };
  }

  /**
   * 按日期聚合
   */
  private static aggregateByDate(records: UsageRecord[]): DailyData[] {
    const map = new Map<string, DailyData>();
    
    for (const record of records) {
      const existing = map.get(record.date);
      if (existing) {
        existing.useCount++;
        existing.duration += record.duration;
      } else {
        map.set(record.date, {
          date: record.date,
          useCount: 1,
          duration: record.duration
        });
      }
    }
    
    return Array.from(map.values()).sort((a, b) => a.date.localeCompare(b.date));
  }

  /**
   * 按应用聚合
   */
  private static aggregateByApp(records: UsageRecord[]): AppUsageData[] {
    const map = new Map<string, AppUsageData>();
    
    for (const record of records) {
      const existing = map.get(record.appId);
      if (existing) {
        existing.useCount++;
        existing.duration += record.duration;
      } else {
        map.set(record.appId, {
          appId: record.appId,
          appName: record.appName,
          useCount: 1,
          duration: record.duration
        });
      }
    }
    
    return Array.from(map.values()).sort((a, b) => b.useCount - a.useCount);
  }
}

三、成就系统实现

private static readonly ACHIEVEMENT_DEFINITIONS: Achievement[] = [
  { id: 'first_use', name: '初次体验', description: '完成第一次使用', icon: '🎉', requirement: 1, unlocked: false, current: 0 },
  { id: 'use_10', name: '常客', description: '累计使用10次', icon: '⭐', requirement: 10, unlocked: false, current: 0 },
  { id: 'use_50', name: '忠实用户', description: '累计使用50次', icon: '🏆', requirement: 50, unlocked: false, current: 0 },
  { id: 'duration_1h', name: '时间投入', description: '累计使用1小时', icon: '⏰', requirement: 3600, unlocked: false, current: 0 },
  { id: 'duration_10h', name: '深度用户', description: '累计使用10小时', icon: '💎', requirement: 36000, unlocked: false, current: 0 },
];

static async checkAchievements(): Promise<void> {
  const records = await StatisticsUtil.getRecords();
  const totalCount = records.length;
  const totalDuration = records.reduce((sum, r) => sum + r.duration, 0);
  
  const achievements = await StatisticsUtil.loadAchievements();
  
  for (const achievement of achievements) {
    if (achievement.unlocked) continue;
    
    let current = 0;
    if (achievement.id.startsWith('use_') || achievement.id === 'first_use') {
      current = totalCount;
    } else if (achievement.id.startsWith('duration_')) {
      current = totalDuration;
    }
    
    achievement.current = current;
    if (current >= achievement.requirement) {
      achievement.unlocked = true;
      achievement.unlockedAt = Date.now();
    }
  }
  
  await PreferencesUtil.putString(StatisticsUtil.ACHIEVEMENTS_KEY, JSON.stringify(achievements));
}

四、页面展示

@Entry
@Component
struct StatisticsPage {
  @State statistics: StatisticsData | null = null;

  aboutToAppear(): void {
    this.loadStatistics();
  }

  async loadStatistics(): Promise<void> {
    this.statistics = await StatisticsUtil.getStatistics();
  }

  build() {
    Column() {
      // 总览卡片
      Row() {
        Column() {
          Text(`${this.statistics?.totalUseCount || 0}`)
            .fontSize(32).fontWeight(FontWeight.Bold)
          Text('总使用次数').fontSize(12)
        }
        Column() {
          Text(this.formatDuration(this.statistics?.totalDuration || 0))
            .fontSize(32).fontWeight(FontWeight.Bold)
          Text('总使用时长').fontSize(12)
        }
      }

      // 成就列表
      Text('成就').fontSize(20).margin({ top: 20 })
      ForEach(this.statistics?.achievements || [], (item: Achievement) => {
        Row() {
          Text(item.icon).fontSize(24)
          Column() {
            Text(item.name).fontWeight(FontWeight.Bold)
            Text(item.description).fontSize(12)
          }
          Text(item.unlocked ? '✓' : `${item.current}/${item.requirement}`)
        }
      })
    }
  }

  formatDuration(seconds: number): string {
    if (seconds < 60) return `${seconds}秒`;
    if (seconds < 3600) return `${Math.floor(seconds / 60)}分`;
    return `${Math.floor(seconds / 3600)}时`;
  }
}

五、避坑指南

  1. 数据清理:定期清理过期数据,避免存储膨胀
  2. 异步处理:统计操作不要阻塞主线程
  3. 隐私保护:所有数据仅存储在本地
  4. 性能优化:大量数据时考虑分页加载

总结

本文实现了一个完整的应用使用统计系统,包括数据记录、统计分析和成就系统。这些数据可以帮助开发者了解用户行为,同时成就系统也能提升用户粘性。

posted @ 2025-12-18 11:06  柠果丶  阅读(1)  评论(0)    收藏  举报