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)}时`;
}
}
五、避坑指南
- 数据清理:定期清理过期数据,避免存储膨胀
- 异步处理:统计操作不要阻塞主线程
- 隐私保护:所有数据仅存储在本地
- 性能优化:大量数据时考虑分页加载
总结
本文实现了一个完整的应用使用统计系统,包括数据记录、统计分析和成就系统。这些数据可以帮助开发者了解用户行为,同时成就系统也能提升用户粘性。
浙公网安备 33010602011771号