HarmonyOS APP开发:健康数据采集与华为运动健康对接

举报
Jack20 发表于 2026/06/21 16:01:37 2026/06/21
【摘要】 HarmonyOS APP开发:健康数据采集与华为运动健康对接核心要点:本文系统讲解基于HarmonyOS的健康数据采集与华为运动健康生态对接技术,涵盖多源健康数据(步数/心率/睡眠/血氧)的采集与持久化、关系型数据库存储方案、数据可视化展示,以及通过华为Health Kit接入运动健康生态的完整实现。项目说明开发语言ArkTS核心API@ohos.data.relationalStore...

HarmonyOS APP开发:健康数据采集与华为运动健康对接

核心要点:本文系统讲解基于HarmonyOS的健康数据采集与华为运动健康生态对接技术,涵盖多源健康数据(步数/心率/睡眠/血氧)的采集与持久化、关系型数据库存储方案、数据可视化展示,以及通过华为Health Kit接入运动健康生态的完整实现。

项目 说明
开发语言 ArkTS
核心API @ohos.data.relationalStore、@ohos.health、@ohos.sensor

一、背景与动机

健康数据是运动传感应用的"最后一公里"——传感器采集的原始数据只有经过聚合、存储、分析后,才能转化为对用户有价值的健康洞察。然而,健康数据管理面临三大挑战:

  1. 数据孤岛:手机、手表、手环各自采集数据,互不相通
  2. 数据碎片化:步数、心率、睡眠、血氧分散在不同应用中
  3. 数据安全:健康数据属于最敏感的个人信息,存储和传输必须加密

华为运动健康生态(Health Kit)为解决这些问题提供了系统级方案:

  • 统一数据仓库:所有健康数据汇聚到华为运动健康平台
  • 标准数据类型:定义了步数、心率、睡眠等标准数据格式
  • 权限管控:用户完全控制哪些应用可以读写哪些健康数据
  • 跨设备同步:手机、手表、平板数据自动同步

健康数据类型全景

健康数据体系
├── 运动数据
│   ├── 步数(Step Count)
│   ├── 步频(Cadence)
│   ├── 距离(Distance)
│   ├── 卡路里(Calories)
│   └── 运动类型(Activity Type)
│
├── 生理数据
│   ├── 心率(Heart Rate)
│   ├── 血氧(Blood Oxygen)
│   ├── 体温(Body Temperature)
│   └── 血压(Blood Pressure)
│
├── 睡眠数据
│   ├── 睡眠时长(Sleep Duration)
│   ├── 深睡/浅睡(Sleep Stage)
│   └── 睡眠质量评分(Sleep Score)
│
└── 体测数据
    ├── 体重(Body Weight)
    ├── 体脂率(Body Fat)
    └── BMI指数

二、核心原理

2.1 健康数据采集架构

flowchart TD
    subgraph 数据源
        A1[加速度传感器<br/>步数/步频]
        A2[心率传感器<br/>PPG光学心率]
        A3[血氧传感器<br/>SpO2红外检测]
        A4[华为运动健康APP<br/>系统级数据]
    end
    
    subgraph 采集层
        B1[Sensor API<br/>实时数据订阅]
        B2[Health Kit<br/>历史数据读取]
    end
    
    subgraph 存储层
        C1[关系型数据库<br/>RDB本地存储]
        C2[偏好设置<br/>用户配置]
        C3[分布式数据<br/>跨设备同步]
    end
    
    subgraph 应用层
        D1[数据可视化<br/>图表/仪表盘]
        D2[健康报告<br/>周报/月报]
        D3[智能提醒<br/>久坐/目标]
    end
    
    A1 --> B1
    A2 --> B1
    A3 --> B1
    A4 --> B2
    
    B1 --> C1
    B2 --> C1
    B1 --> C2
    C1 --> C3
    
    C1 --> D1
    C1 --> D2
    C2 --> D3
    C3 --> D1
    
    classDef sourceStyle fill:#1a1a2e,stroke:#e94560,color:#fff,stroke-width:2px
    classDef collectStyle fill:#16213e,stroke:#0f3460,color:#e0e0e0,stroke-width:2px
    classDef storeStyle fill:#0f3460,stroke:#533483,color:#fff,stroke-width:2px
    classDef appStyle fill:#533483,stroke:#e94560,color:#fff,stroke-width:2px
    
    class A1,A2,A3,A4 sourceStyle
    class B1,B2 collectStyle
    class C1,C2,C3 storeStyle
    class D1,D2,D3 appStyle

2.2 数据库设计

健康数据采用关系型数据库(RDB)存储,核心表结构:

health_record表(健康记录主表):

字段 类型 说明
id INTEGER 主键自增
type TEXT 数据类型(STEP/HEART_RATE/SLEEP/…)
value REAL 数值
unit TEXT 单位
start_time INTEGER 开始时间戳
end_time INTEGER 结束时间戳
source TEXT 数据来源(SENSOR/HEALTH_KIT/MANUAL)
device_id TEXT 设备标识
metadata TEXT 附加元数据(JSON格式)
created_at INTEGER 记录创建时间

daily_summary表(每日汇总表):

字段 类型 说明
date TEXT 日期(YYYY-MM-DD)
total_steps INTEGER 总步数
total_distance REAL 总距离(米)
total_calories REAL 总卡路里
avg_heart_rate REAL 平均心率
max_heart_rate INTEGER 最大心率
sleep_duration INTEGER 睡眠时长(分钟)
deep_sleep INTEGER 深睡时长(分钟)
activity_minutes INTEGER 活动时长(分钟)

2.3 Health Kit数据读写流程

┌──────────┐     ┌──────────────┐     ┌──────────────┐
│  应用层   │────→│  Health Kit   │────→│  华为运动健康  │
  (APP)   │     │  Client API  │     │   Platform   │
└──────────┘     └──────────────┘     └──────────────┘
     │                  │                     │
     │  1.申请权限       │  2.鉴权+权限校验     │  3.数据读写
     │─────────────────→│────────────────────→│
     │                  │                     │
     │  4.返回数据       │  5.聚合/过滤        │  6.存储/同步
     │←─────────────────│←────────────────────│

三、代码实战

3.1 健康数据本地存储管理器

实现基于RDB的健康数据持久化存储:

// HealthDataStore.ets — 健康数据本地存储管理器
import relationalStore from '@ohos.data.relationalStore';
import { BusinessError } from '@ohos.base';

// 健康数据记录
export interface HealthRecord {
  id?: number;
  type: HealthDataType;
  value: number;
  unit: string;
  startTime: number;
  endTime: number;
  source: string;
  deviceId: string;
  metadata?: Record<string, string>;
}

// 健康数据类型枚举
export enum HealthDataType {
  STEP_COUNT = 'STEP_COUNT',
  HEART_RATE = 'HEART_RATE',
  BLOOD_OXYGEN = 'BLOOD_OXYGEN',
  DISTANCE = 'DISTANCE',
  CALORIES = 'CALORIES',
  SLEEP_DURATION = 'SLEEP_DURATION',
  BODY_WEIGHT = 'BODY_WEIGHT',
  ACTIVITY_MINUTES = 'ACTIVITY_MINUTES',
}

// 每日汇总数据
export interface DailySummary {
  date: string;
  totalSteps: number;
  totalDistance: number;
  totalCalories: number;
  avgHeartRate: number;
  maxHeartRate: number;
  sleepDuration: number;
  deepSleep: number;
  activityMinutes: number;
}

// 数据库配置
const DB_CONFIG: relationalStore.StoreConfig = {
  name: 'HealthData.db',
  securityLevel: relationalStore.SecurityLevel.S3, // S3级安全(健康数据)
  encrypt: true, // 加密存储
};

// 建表SQL
const CREATE_HEALTH_RECORD_TABLE = `
  CREATE TABLE IF NOT EXISTS health_record (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    type TEXT NOT NULL,
    value REAL NOT NULL,
    unit TEXT NOT NULL,
    start_time INTEGER NOT NULL,
    end_time INTEGER NOT NULL,
    source TEXT NOT NULL DEFAULT 'SENSOR',
    device_id TEXT NOT NULL DEFAULT '',
    metadata TEXT,
    created_at INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)
  )
`;

const CREATE_DAILY_SUMMARY_TABLE = `
  CREATE TABLE IF NOT EXISTS daily_summary (
    date TEXT PRIMARY KEY,
    total_steps INTEGER DEFAULT 0,
    total_distance REAL DEFAULT 0,
    total_calories REAL DEFAULT 0,
    avg_heart_rate REAL DEFAULT 0,
    max_heart_rate INTEGER DEFAULT 0,
    sleep_duration INTEGER DEFAULT 0,
    deep_sleep INTEGER DEFAULT 0,
    activity_minutes INTEGER DEFAULT 0
  )
`;

// 索引SQL
const CREATE_INDEXES = [
  'CREATE INDEX IF NOT EXISTS idx_health_type ON health_record(type)',
  'CREATE INDEX IF NOT EXISTS idx_health_start_time ON health_record(start_time)',
  'CREATE INDEX IF NOT EXISTS idx_health_type_time ON health_record(type, start_time)',
];

@ObservedV2
export class HealthDataStore {
  // 数据库实例
  private store: relationalStore.RdbStore | null = null;

  // 数据库是否就绪
  @Trace isReady: boolean = false;

  /**
   * 初始化数据库
   */
  async init(context: Context): Promise<void> {
    try {
      this.store = await relationalStore.getRdbStore(context, DB_CONFIG);

      // 创建表
      await this.store.executeSql(CREATE_HEALTH_RECORD_TABLE);
      await this.store.executeSql(CREATE_DAILY_SUMMARY_TABLE);

      // 创建索引
      for (const sql of CREATE_INDEXES) {
        await this.store.executeSql(sql);
      }

      this.isReady = true;
      console.info('[HealthDataStore] 数据库初始化成功');
    } catch (error) {
      const e = error as BusinessError;
      console.error(`[HealthDataStore] 数据库初始化失败: ${e.message}`);
    }
  }

  /**
   * 插入健康数据记录
   */
  async insertRecord(record: HealthRecord): Promise<number> {
    if (!this.store) return -1;

    try {
      const valueBucket: relationalStore.ValuesBucket = {
        type: record.type,
        value: record.value,
        unit: record.unit,
        start_time: record.startTime,
        end_time: record.endTime,
        source: record.source,
        device_id: record.deviceId,
        metadata: record.metadata ? JSON.stringify(record.metadata) : null,
      };

      const rowId = await this.store.insert('health_record', valueBucket);
      console.info(`[HealthDataStore] 插入记录成功, rowId=${rowId}`);
      return rowId;
    } catch (error) {
      const e = error as BusinessError;
      console.error(`[HealthDataStore] 插入记录失败: ${e.message}`);
      return -1;
    }
  }

  /**
   * 批量插入健康数据记录
   */
  async batchInsertRecords(records: HealthRecord[]): Promise<number> {
    if (!this.store || records.length === 0) return 0;

    try {
      const valueBuckets: relationalStore.ValuesBucket[] = records.map(record => ({
        type: record.type,
        value: record.value,
        unit: record.unit,
        start_time: record.startTime,
        end_time: record.endTime,
        source: record.source,
        device_id: record.deviceId,
        metadata: record.metadata ? JSON.stringify(record.metadata) : null,
      }));

      const count = await this.store.batchInsert('health_record', valueBuckets);
      console.info(`[HealthDataStore] 批量插入${count}条记录`);
      return count;
    } catch (error) {
      const e = error as BusinessError;
      console.error(`[HealthDataStore] 批量插入失败: ${e.message}`);
      return 0;
    }
  }

  /**
   * 查询指定时间范围的健康数据
   */
  async queryRecords(
    type: HealthDataType,
    startTime: number,
    endTime: number
  ): Promise<HealthRecord[]> {
    if (!this.store) return [];

    try {
      const predicates = new relationalStore.RdbPredicates('health_record');
      predicates.equalTo('type', type)
        .between('start_time', startTime, endTime)
        .orderByAsc('start_time');

      const resultSet = await this.store.query(predicates);
      const records: HealthRecord[] = [];

      while (resultSet.goToNextRow()) {
        records.push({
          id: resultSet.getLong(resultSet.getColumnIndex('id')),
          type: resultSet.getString(resultSet.getColumnIndex('type')) as HealthDataType,
          value: resultSet.getDouble(resultSet.getColumnIndex('value')),
          unit: resultSet.getString(resultSet.getColumnIndex('unit')),
          startTime: resultSet.getLong(resultSet.getColumnIndex('start_time')),
          endTime: resultSet.getLong(resultSet.getColumnIndex('end_time')),
          source: resultSet.getString(resultSet.getColumnIndex('source')),
          deviceId: resultSet.getString(resultSet.getColumnIndex('device_id')),
          metadata: resultSet.getString(resultSet.getColumnIndex('metadata'))
            ? JSON.parse(resultSet.getString(resultSet.getColumnIndex('metadata')))
            : undefined,
        });
      }

      resultSet.close();
      return records;
    } catch (error) {
      const e = error as BusinessError;
      console.error(`[HealthDataStore] 查询失败: ${e.message}`);
      return [];
    }
  }

  /**
   * 获取今日汇总数据
   */
  async getTodaySummary(): Promise<DailySummary> {
    const today = new Date();
    const dateStr = today.toISOString().split('T')[0]; // YYYY-MM-DD
    const dayStart = new Date(today.getFullYear(), today.getMonth(), today.getDate()).getTime();
    const dayEnd = dayStart + 86400000; // 24小时

    // 查询各类型数据并汇总
    const steps = await this.queryRecords(HealthDataType.STEP_COUNT, dayStart, dayEnd);
    const heartRates = await this.queryRecords(HealthDataType.HEART_RATE, dayStart, dayEnd);
    const distances = await this.queryRecords(HealthDataType.DISTANCE, dayStart, dayEnd);
    const calories = await this.queryRecords(HealthDataType.CALORIES, dayStart, dayEnd);
    const sleeps = await this.queryRecords(HealthDataType.SLEEP_DURATION, dayStart, dayEnd);

    const totalSteps = steps.reduce((sum, r) => sum + r.value, 0);
    const totalDistance = distances.reduce((sum, r) => sum + r.value, 0);
    const totalCalories = calories.reduce((sum, r) => sum + r.value, 0);
    const avgHR = heartRates.length > 0
      ? heartRates.reduce((sum, r) => sum + r.value, 0) / heartRates.length : 0;
    const maxHR = heartRates.length > 0
      ? Math.max(...heartRates.map(r => r.value)) : 0;
    const sleepDuration = sleeps.reduce((sum, r) => sum + r.value, 0);

    return {
      date: dateStr,
      totalSteps: Math.round(totalSteps),
      totalDistance: Math.round(totalDistance),
      totalCalories: Math.round(totalCalories * 10) / 10,
      avgHeartRate: Math.round(avgHR),
      maxHeartRate: Math.round(maxHR),
      sleepDuration: Math.round(sleepDuration),
      deepSleep: 0, // 需要睡眠分期数据
      activityMinutes: 0, // 需要活动检测数据
    };
  }

  /**
   * 获取最近7天汇总数据
   */
  async getWeeklySummary(): Promise<DailySummary[]> {
    const summaries: DailySummary[] = [];
    const now = new Date();

    for (let i = 6; i >= 0; i--) {
      const date = new Date(now);
      date.setDate(date.getDate() - i);
      const dateStr = date.toISOString().split('T')[0];
      const dayStart = new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
      const dayEnd = dayStart + 86400000;

      const steps = await this.queryRecords(HealthDataType.STEP_COUNT, dayStart, dayEnd);
      const distances = await this.queryRecords(HealthDataType.DISTANCE, dayStart, dayEnd);
      const calories = await this.queryRecords(HealthDataType.CALORIES, dayStart, dayEnd);

      summaries.push({
        date: dateStr,
        totalSteps: Math.round(steps.reduce((sum, r) => sum + r.value, 0)),
        totalDistance: Math.round(distances.reduce((sum, r) => sum + r.value, 0)),
        totalCalories: Math.round(calories.reduce((sum, r) => sum + r.value, 0) * 10) / 10,
        avgHeartRate: 0,
        maxHeartRate: 0,
        sleepDuration: 0,
        deepSleep: 0,
        activityMinutes: 0,
      });
    }

    return summaries;
  }

  /**
   * 删除指定时间范围的数据
   */
  async deleteRecords(type: HealthDataType, startTime: number, endTime: number): Promise<number> {
    if (!this.store) return 0;

    try {
      const predicates = new relationalStore.RdbPredicates('health_record');
      predicates.equalTo('type', type).between('start_time', startTime, endTime);
      const count = await this.store.delete(predicates);
      return count;
    } catch (error) {
      return 0;
    }
  }

  /**
   * 关闭数据库
   */
  async close(): Promise<void> {
    if (this.store) {
      try {
        await relationalStore.deleteRdbStore('HealthData.db');
      } catch (e) { /* 忽略 */ }
      this.store = null;
      this.isReady = false;
    }
  }
}

3.2 华为运动健康Kit对接

实现与华为Health Kit的数据读写对接:

// HealthKitManager.ets — 华为运动健康Kit对接管理器
import health from '@ohos.health';
import { BusinessError } from '@ohos.base';

// Health Kit权限类型
export enum HealthPermissionType {
  STEP_COUNT = 'ohos.permission.READ_HEALTH_DATA:STEP_COUNT',
  HEART_RATE = 'ohos.permission.READ_HEALTH_DATA:HEART_RATE',
  BLOOD_OXYGEN = 'ohos.permission.READ_HEALTH_DATA:BLOOD_OXYGEN',
  DISTANCE = 'ohos.permission.READ_HEALTH_DATA:DISTANCE',
  CALORIES = 'ohos.permission.READ_HEALTH_DATA:CALORIES',
  SLEEP = 'ohos.permission.READ_HEALTH_DATA:SLEEP',
}

// Health Kit数据记录
export interface HealthKitRecord {
  dataType: health.HealthDataType;
  startTime: number;
  endTime: number;
  value: number;
  unit: string;
}

@ObservedV2
export class HealthKitManager {
  // 权限状态
  @Trace permissionsGranted: boolean = false;
  @Trace isAvailable: boolean = false;

  // 需要申请的权限列表
  private readonly requiredPermissions: string[] = [
    HealthPermissionType.STEP_COUNT,
    HealthPermissionType.HEART_RATE,
    HealthPermissionType.BLOOD_OXYGEN,
    HealthPermissionType.DISTANCE,
    HealthPermissionType.CALORIES,
    HealthPermissionType.SLEEP,
  ];

  /**
   * 检查Health Kit可用性
   */
  async checkAvailability(): Promise<boolean> {
    try {
      // 检查设备是否支持Health Kit
      this.isAvailable = true; // 简化判断
      return this.isAvailable;
    } catch (e) {
      this.isAvailable = false;
      return false;
    }
  }

  /**
   * 申请Health Kit权限
   */
  async requestPermissions(): Promise<boolean> {
    try {
      // 在实际应用中,需要通过AbilityContext申请权限
      // 此处为简化示例
      console.info('[HealthKit] 正在申请健康数据权限...');

      // 检查已有权限
      for (const permission of this.requiredPermissions) {
        // 实际应使用 abilityAccessCtrl.checkAccessToken 检查
        console.info(`[HealthKit] 检查权限: ${permission}`);
      }

      this.permissionsGranted = true;
      console.info('[HealthKit] 权限申请成功');
      return true;
    } catch (error) {
      const e = error as BusinessError;
      console.error(`[HealthKit] 权限申请失败: ${e.message}`);
      this.permissionsGranted = false;
      return false;
    }
  }

  /**
   * 读取今日步数数据
   */
  async readTodayStepCount(): Promise<number> {
    if (!this.permissionsGranted) {
      console.warn('[HealthKit] 未获得步数读取权限');
      return 0;
    }

    try {
      const today = new Date();
      const startTime = new Date(today.getFullYear(), today.getMonth(), today.getDate()).getTime();
      const endTime = Date.now();

      // 使用Health Kit查询步数
      const result = await health.query({
        dataType: health.HealthDataType.STEP_COUNT,
        startTime: startTime,
        endTime: endTime,
      });

      let totalSteps = 0;
      if (result && Array.isArray(result)) {
        for (const record of result) {
          totalSteps += record.value ?? 0;
        }
      }

      console.info(`[HealthKit] 今日步数: ${totalSteps}`);
      return totalSteps;
    } catch (error) {
      const e = error as BusinessError;
      console.error(`[HealthKit] 读取步数失败: ${e.message}`);
      return 0;
    }
  }

  /**
   * 读取心率数据
   */
  async readHeartRate(startTime: number, endTime: number): Promise<HealthKitRecord[]> {
    if (!this.permissionsGranted) return [];

    try {
      const result = await health.query({
        dataType: health.HealthDataType.HEART_RATE,
        startTime: startTime,
        endTime: endTime,
      });

      const records: HealthKitRecord[] = [];
      if (result && Array.isArray(result)) {
        for (const item of result) {
          records.push({
            dataType: health.HealthDataType.HEART_RATE,
            startTime: item.startTime ?? 0,
            endTime: item.endTime ?? 0,
            value: item.value ?? 0,
            unit: 'bpm',
          });
        }
      }

      return records;
    } catch (error) {
      const e = error as BusinessError;
      console.error(`[HealthKit] 读取心率失败: ${e.message}`);
      return [];
    }
  }

  /**
   * 写入步数数据到Health Kit
   */
  async writeStepCount(steps: number, startTime: number, endTime: number): Promise<boolean> {
    if (!this.permissionsGranted) return false;

    try {
      await health.insert({
        dataType: health.HealthDataType.STEP_COUNT,
        startTime: startTime,
        endTime: endTime,
        value: steps,
      });

      console.info(`[HealthKit] 写入步数成功: ${steps}`);
      return true;
    } catch (error) {
      const e = error as BusinessError;
      console.error(`[HealthKit] 写入步数失败: ${e.message}`);
      return false;
    }
  }

  /**
   * 写入运动记录到Health Kit
   */
  async writeWorkoutRecord(
    activityType: string,
    startTime: number,
    endTime: number,
    steps: number,
    distance: number,
    calories: number
  ): Promise<boolean> {
    if (!this.permissionsGranted) return false;

    try {
      // 写入步数
      if (steps > 0) {
        await health.insert({
          dataType: health.HealthDataType.STEP_COUNT,
          startTime: startTime,
          endTime: endTime,
          value: steps,
        });
      }

      // 写入距离
      if (distance > 0) {
        await health.insert({
          dataType: health.HealthDataType.DISTANCE,
          startTime: startTime,
          endTime: endTime,
          value: distance,
        });
      }

      // 写入卡路里
      if (calories > 0) {
        await health.insert({
          dataType: health.HealthDataType.CALORIES,
          startTime: startTime,
          endTime: endTime,
          value: calories,
        });
      }

      console.info('[HealthKit] 运动记录写入成功');
      return true;
    } catch (error) {
      const e = error as BusinessError;
      console.error(`[HealthKit] 运动记录写入失败: ${e.message}`);
      return false;
    }
  }

  /**
   * 订阅健康数据变更
   */
  async subscribeHealthDataChange(
    dataType: health.HealthDataType,
    callback: (record: HealthKitRecord) => void
  ): Promise<void> {
    try {
      health.on('healthDataChange', {
        dataType: dataType,
      }, (data) => {
        callback({
          dataType: data.dataType,
          startTime: data.startTime ?? 0,
          endTime: data.endTime ?? 0,
          value: data.value ?? 0,
          unit: '',
        });
      });

      console.info(`[HealthKit] 已订阅数据变更: ${dataType}`);
    } catch (error) {
      const e = error as BusinessError;
      console.error(`[HealthKit] 订阅数据变更失败: ${e.message}`);
    }
  }

  /**
   * 取消订阅健康数据变更
   */
  async unsubscribeHealthDataChange(dataType: health.HealthDataType): Promise<void> {
    try {
      health.off('healthDataChange', {
        dataType: dataType,
      });
    } catch (e) { /* 忽略 */ }
  }
}

3.3 健康数据仪表盘界面

实现健康数据可视化展示,包含今日汇总、7天趋势图和数据详情:

// HealthDashboardPage.ets — 健康数据仪表盘
import { HealthDataStore, HealthRecord, HealthDataType, DailySummary } from './HealthDataStore';
import { HealthKitManager, HealthKitRecord } from './HealthKitManager';
import sensor from '@ohos.sensor';

@Entry
@Component
struct HealthDashboardPage {
  // 管理器
  private dataStore: HealthDataStore = new HealthDataStore();
  private healthKit: HealthKitManager = new HealthKitManager();

  // 今日数据
  @State todaySteps: number = 0;
  @State todayDistance: number = 0;
  @State todayCalories: number = 0;
  @State avgHeartRate: number = 0;
  @State maxHeartRate: number = 0;
  @State sleepHours: number = 0;
  @State bloodOxygen: number = 0;
  @State stepGoal: number = 10000;
  @State stepProgress: number = 0;

  // 7天数据
  @State weeklySteps: number[] = [0, 0, 0, 0, 0, 0, 0];
  @State weeklyLabels: string[] = [];

  // 心率实时数据
  @State currentHeartRate: number = 0;
  @State isHeartRateMonitoring: boolean = false;

  // UI状态
  @State isLoading: boolean = true;
  @State activeTab: number = 0;

  // 心率订阅ID
  private heartRateSubscribeId: number = -1;

  async aboutToAppear(): Promise<void> {
    // 初始化数据库
    await this.dataStore.init(getContext(this));

    // 检查Health Kit
    await this.healthKit.checkAvailability();
    if (this.healthKit.isAvailable) {
      await this.healthKit.requestPermissions();
    }

    // 加载今日数据
    await this.loadTodayData();
    await this.loadWeeklyData();

    this.isLoading = false;
  }

  build() {
    Navigation() {
      Column({ space: 0 }) {
        // 标签栏
        this.TabBar()

        // 内容区
        if (this.isLoading) {
          this.LoadingView()
        } else {
          if (this.activeTab === 0) {
            this.TodayOverview()
          } else {
            this.WeeklyTrend()
          }
        }
      }
      .width('100%')
      .height('100%')
    }
    .title('健康数据')
    .titleMode(NavigationTitleMode.Mini)
  }

  // ===== 标签栏 =====
  @Builder
  TabBar() {
    Row({ space: 0 }) {
      ForEach(['今日概览', '7天趋势'], (label: string, index: number) => {
        Column() {
          Text(label)
            .fontSize(15)
            .fontColor(this.activeTab === index ? '#e94560' : '#78909C')
            .fontWeight(this.activeTab === index ? FontWeight.Bold : FontWeight.Normal)

          // 指示条
          Row()
            .width(24)
            .height(3)
            .borderRadius(2)
            .backgroundColor(this.activeTab === index ? '#e94560' : 'transparent')
            .margin({ top: 6 })
        }
        .layoutWeight(1)
        .padding({ top: 12, bottom: 12 })
        .onClick(() => { this.activeTab = index as number; })
      })
    }
    .width('100%')
    .backgroundColor('rgba(15, 52, 96, 0.6)')
  }

  // ===== 今日概览 =====
  @Builder
  TodayOverview() {
    Scroll() {
      Column({ space: 16 }) {
        // 步数环形进度
        this.StepProgressCard()

        // 健康数据卡片网格
        this.HealthDataGrid()

        // 心率监测卡片
        this.HeartRateCard()

        // 睡眠数据卡片
        this.SleepCard()

        // 数据来源信息
        this.DataSourceInfo()
      }
      .width('100%')
      .padding({ left: 20, right: 20, top: 16, bottom: 40 })
    }
    .scrollBar(BarState.Off)
  }

  // ===== 步数环形进度 =====
  @Builder
  StepProgressCard() {
    Column({ space: 16 }) {
      Stack() {
        // 背景圆环
        Circle()
          .width(200)
          .height(200)
          .fill('transparent')
          .stroke('#1a1a2e')
          .strokeWidth(14)

        // 进度圆环
        Circle()
          .width(200)
          .height(200)
          .fill('transparent')
          .stroke('#e94560')
          .strokeWidth(14)
          .strokeDasharray({
            lineDash: [
              Math.PI * 200 * Math.min(this.stepProgress, 1),
              Math.PI * 200
            ]
          })

        // 中心数据
        Column({ space: 4 }) {
          Text(this.todaySteps.toString())
            .fontSize(44)
            .fontColor('#FFFFFF')
            .fontWeight(FontWeight.Bold)
            .fontFamily('HarmonyOS Sans')

          Text(`/ ${this.stepGoal}`)
            .fontSize(13)
            .fontColor('#78909C')

          Text(`${Math.round(this.stepProgress * 100)}%`)
            .fontSize(12)
            .fontColor('#e94560')
            .margin({ top: 4 })
        }
      }

      // 副数据
      Row({ space: 24 }) {
        this.MiniStat('📏', `${(this.todayDistance / 1000).toFixed(1)}`, 'km')
        this.MiniStat('🔥', `${this.todayCalories.toFixed(0)}`, 'kcal')
      }
    }
    .width('100%')
    .padding(24)
    .borderRadius(24)
    .backgroundColor('rgba(15, 52, 96, 0.6)')
    .backdropBlur(20)
    .alignItems(HorizontalAlign.Center)
  }

  @Builder
  MiniStat(icon: string, value: string, unit: string) {
    Column({ space: 4 }) {
      Text(icon)
        .fontSize(20)
      Row({ space: 2 }) {
        Text(value)
          .fontSize(16)
          .fontColor('#FFFFFF')
          .fontWeight(FontWeight.Bold)
        Text(unit)
          .fontSize(10)
          .fontColor('#546E7A')
      }
    }
  }

  // ===== 健康数据卡片网格 =====
  @Builder
  HealthDataGrid() {
    Grid() {
      GridItem() {
        this.HealthCard('❤️', '心率', `${this.avgHeartRate}`, 'bpm',
          this.avgHeartRate >= 60 && this.avgHeartRate <= 100 ? '#00E676' : '#FF5252')
      }
      GridItem() {
        this.HealthCard('🫁', '血氧', `${this.bloodOxygen}`, '%',
          this.bloodOxygen >= 95 ? '#00E676' : '#FF5252')
      }
      GridItem() {
        this.HealthCard('📏', '距离', `${(this.todayDistance / 1000).toFixed(1)}`, 'km', '#4FC3F7')
      }
      GridItem() {
        this.HealthCard('🔥', '卡路里', `${this.todayCalories.toFixed(0)}`, 'kcal', '#FFB74D')
      }
    }
    .columnsTemplate('1fr 1fr')
    .rowsTemplate('1fr 1fr')
    .width('100%')
    .height(200)
    .columnsGap(10)
    .rowsGap(10)
  }

  @Builder
  HealthCard(icon: string, label: string, value: string, unit: string, color: string) {
    Column({ space: 8 }) {
      Text(icon)
        .fontSize(24)

      Text(label)
        .fontSize(11)
        .fontColor('#78909C')

      Row({ space: 2 }) {
        Text(value)
          .fontSize(20)
          .fontColor(color)
          .fontWeight(FontWeight.Bold)
        Text(unit)
          .fontSize(10)
          .fontColor('#546E7A')
      }
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .borderRadius(16)
    .backgroundColor('rgba(22, 33, 62, 0.8)')
  }

  // ===== 心率监测卡片 =====
  @Builder
  HeartRateCard() {
    Column({ space: 12 }) {
      Row({ space: 8 }) {
        Text('❤️')
          .fontSize(20)
        Text('心率监测')
          .fontSize(16)
          .fontColor('#FFFFFF')
          .fontWeight(FontWeight.Bold)
        Blank()

        Button(this.isHeartRateMonitoring ? '停止' : '开始')
          .height(28)
          .fontSize(12)
          .fontColor(this.isHeartRateMonitoring ? '#FF5252' : '#00E676')
          .backgroundColor('rgba(255,255,255,0.05)')
          .borderRadius(14)
          .onClick(() => this.toggleHeartRateMonitoring())
      }
      .width('100%')

      if (this.isHeartRateMonitoring) {
        // 实时心率显示
        Row({ space: 12 }) {
          Text('当前心率')
            .fontSize(13)
            .fontColor('#78909C')

          Text(`${this.currentHeartRate}`)
            .fontSize(36)
            .fontColor('#FF5252')
            .fontWeight(FontWeight.Bold)

          Text('BPM')
            .fontSize(12)
            .fontColor('#546E7A')
        }

        // 心率区间指示
        Row({ space: 4 }) {
          ForEach([
            { label: '静息', range: [60, 80], color: '#4FC3F7' },
            { label: '燃脂', range: [80, 120], color: '#00E676' },
            { label: '有氧', range: [120, 150], color: '#FFB74D' },
            { label: '无氧', range: [150, 180], color: '#FF5252' },
          ], (zone: { label: string; range: number[]; color: string }) => {
            Column() {
              Text(zone.label)
                .fontSize(9)
                .fontColor('#78909C')
              Row()
                .width(40)
                .height(4)
                .borderRadius(2)
                .backgroundColor(
                  this.currentHeartRate >= zone.range[0] && this.currentHeartRate < zone.range[1]
                    ? zone.color : `${zone.color}33`
                )
            }
          })
        }
      } else {
        Text('点击"开始"监测实时心率')
          .fontSize(13)
          .fontColor('#546E7A')
      }
    }
    .width('100%')
    .padding(16)
    .borderRadius(16)
    .backgroundColor('rgba(22, 33, 62, 0.8)')
  }

  // ===== 睡眠数据卡片 =====
  @Builder
  SleepCard() {
    Column({ space: 12 }) {
      Row({ space: 8 }) {
        Text('🌙')
          .fontSize(20)
        Text('睡眠数据')
          .fontSize(16)
          .fontColor('#FFFFFF')
          .fontWeight(FontWeight.Bold)
      }

      Row({ space: 24 }) {
        Column({ space: 4 }) {
          Text(`${this.sleepHours.toFixed(1)}`)
            .fontSize(28)
            .fontColor('#7C4DFF')
            .fontWeight(FontWeight.Bold)
          Text('小时')
            .fontSize(11)
            .fontColor('#78909C')
        }

        Column({ space: 4 }) {
          Text(this.sleepHours >= 7 ? '充足' : this.sleepHours >= 5 ? '不足' : '严重不足')
            .fontSize(14)
            .fontColor(this.sleepHours >= 7 ? '#00E676' : this.sleepHours >= 5 ? '#FFB74D' : '#FF5252')
            .fontWeight(FontWeight.Bold)
          Text('睡眠评价')
            .fontSize(11)
            .fontColor('#78909C')
        }
      }
    }
    .width('100%')
    .padding(16)
    .borderRadius(16)
    .backgroundColor('rgba(22, 33, 62, 0.8)')
  }

  // ===== 数据来源信息 =====
  @Builder
  DataSourceInfo() {
    Row({ space: 8 }) {
      Text('📡')
        .fontSize(14)
      Text(this.healthKit.isAvailable ? '数据来源:本地传感器 + 华为运动健康' : '数据来源:本地传感器')
        .fontSize(11)
        .fontColor('#546E7A')
    }
    .width('100%')
    .padding(12)
    .borderRadius(10)
    .backgroundColor('rgba(83, 52, 131, 0.2)')
  }

  // ===== 7天趋势 =====
  @Builder
  WeeklyTrend() {
    Scroll() {
      Column({ space: 16 }) {
        Text('📊 近7天步数趋势')
          .fontSize(16)
          .fontColor('#FFFFFF')
          .fontWeight(FontWeight.Bold)

        // 柱状图
        Row({ space: 8 }) {
          ForEach(this.weeklySteps, (steps: number, index: number) => {
            Column({ space: 4 }) {
              // 数值标签
              Text(`${Math.round(steps / 1000)}k`)
                .fontSize(9)
                .fontColor('#78909C')

              // 柱子
              Column()
                .width(32)
                .height(`${Math.max(steps / this.stepGoal * 100, 3)}%`)
                .backgroundColor(steps >= this.stepGoal ? '#00E676' : '#e94560')
                .borderRadius({ topLeft: 4, topRight: 4 })
            }
            .height(150)
            .justifyContent(FlexAlign.End)
            .layoutWeight(1)
          })
        }
        .width('100%')

        // 日期标签
        Row({ space: 8 }) {
          ForEach(this.weeklyLabels, (label: string) => {
            Text(label)
              .fontSize(10)
              .fontColor('#546E7A')
              .layoutWeight(1)
              .textAlign(TextAlign.Center)
          })
        }
        .width('100%')

        // 统计信息
        this.WeeklyStatsSection()
      }
      .width('100%')
      .padding({ left: 20, right: 20, top: 16, bottom: 40 })
    }
    .scrollBar(BarState.Off)
  }

  @Builder
  WeeklyStatsSection() {
    Column({ space: 12 }) {
      Text('📋 本周统计')
        .fontSize(16)
        .fontColor('#FFFFFF')
        .fontWeight(FontWeight.Bold)

      Row({ space: 16 }) {
        this.StatItem('日均步数', `${Math.round(this.weeklySteps.reduce((a, b) => a + b, 0) / 7)}`, '步')
        this.StatItem('达标天数', `${this.weeklySteps.filter(s => s >= this.stepGoal).length}`, '天')
        this.StatItem('最高步数', `${Math.max(...this.weeklySteps)}`, '步')
      }
    }
    .width('100%')
    .padding(16)
    .borderRadius(16)
    .backgroundColor('rgba(22, 33, 62, 0.8)')
  }

  @Builder
  StatItem(label: string, value: string, unit: string) {
    Column({ space: 4 }) {
      Text(label)
        .fontSize(11)
        .fontColor('#78909C')
      Row({ space: 2 }) {
        Text(value)
          .fontSize(16)
          .fontColor('#e94560')
          .fontWeight(FontWeight.Bold)
        Text(unit)
          .fontSize(9)
          .fontColor('#546E7A')
      }
    }
    .layoutWeight(1)
  }

  // ===== 加载视图 =====
  @Builder
  LoadingView() {
    Column() {
      LoadingProgress()
        .width(48)
        .height(48)
        .color('#e94560')
      Text('加载健康数据...')
        .fontSize(14)
        .fontColor('#78909C')
        .margin({ top: 16 })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }

  // ===== 业务逻辑 =====

  private async loadTodayData(): Promise<void> {
    // 优先从Health Kit读取
    if (this.healthKit.permissionsGranted) {
      this.todaySteps = await this.healthKit.readTodayStepCount();
    }

    // 从本地数据库读取补充数据
    if (this.dataStore.isReady) {
      const summary = await this.dataStore.getTodaySummary();
      if (this.todaySteps === 0) {
        this.todaySteps = summary.totalSteps;
      }
      this.todayDistance = summary.totalDistance;
      this.todayCalories = summary.totalCalories;
      this.avgHeartRate = summary.avgHeartRate;
      this.maxHeartRate = summary.maxHeartRate;
      this.sleepHours = summary.sleepDuration / 60;
    }

    // 计算步数进度
    this.stepProgress = Math.min(this.todaySteps / this.stepGoal, 1);

    // 模拟血氧数据(实际应从传感器读取)
    this.bloodOxygen = 98;
  }

  private async loadWeeklyData(): Promise<void> {
    if (!this.dataStore.isReady) return;

    const summaries = await this.dataStore.getWeeklySummary();
    this.weeklySteps = summaries.map(s => s.totalSteps);
    this.weeklyLabels = summaries.map(s => {
      const date = new Date(s.date);
      return ['日', '一', '二', '三', '四', '五', '六'][date.getDay()];
    });
  }

  /**
   * 切换心率监测
   */
  private toggleHeartRateMonitoring(): void {
    if (this.isHeartRateMonitoring) {
      this.stopHeartRateMonitoring();
    } else {
      this.startHeartRateMonitoring();
    }
  }

  private startHeartRateMonitoring(): void {
    this.isHeartRateMonitoring = true;

    try {
      this.heartRateSubscribeId = sensor.on(sensor.SensorId.HEART_RATE,
        (data: sensor.HeartRateResponse) => {
          this.currentHeartRate = data.heartRate;

          // 保存心率数据
          if (this.dataStore.isReady) {
            this.dataStore.insertRecord({
              type: HealthDataType.HEART_RATE,
              value: data.heartRate,
              unit: 'bpm',
              startTime: Date.now(),
              endTime: Date.now(),
              source: 'SENSOR',
              deviceId: '',
            });
          }
        },
        { interval: 1_000_000_000 } // 1秒采样
      );
    } catch (e) {
      console.error('[HealthDashboard] 心率传感器订阅失败');
      this.isHeartRateMonitoring = false;
    }
  }

  private stopHeartRateMonitoring(): void {
    this.isHeartRateMonitoring = false;
    if (this.heartRateSubscribeId !== -1) {
      try {
        sensor.off(sensor.SensorId.HEART_RATE, this.heartRateSubscribeId);
      } catch (e) { /* 忽略 */ }
    }
  }

  aboutToDisappear(): void {
    this.stopHeartRateMonitoring();
  }
}

四、踩坑与注意事项

4.1 Health Kit权限申请流程

Health Kit权限申请比普通权限更复杂,需要经过华为审核

申请流程:
1. 在AppGallery Connect中注册应用
2. 申请Health Kit使用权限(需说明用途)
3. 通过华为审核(通常1-3个工作日)
4. 在应用中动态申请用户授权
5. 用户同意后才能读写健康数据

⚠️ 重要:Health Kit权限不是标准的requestPermissionsFromUser流程,需要使用专门的Health Kit授权API。

4.2 数据库安全等级选择

安全等级 适用场景 加密方式
S1 公开数据 无加密
S2 一般个人数据 AES-128
S3 敏感个人数据(健康) AES-256
S4 极敏感数据(金融) AES-256+设备绑定

健康数据必须使用S3级安全,且encrypt: true开启加密。

4.3 数据同步冲突处理

当本地数据和Health Kit数据冲突时:

策略 说明 适用场景
本地优先 以本地数据库为准 自研传感器算法更精确
远端优先 以Health Kit为准 华为手表数据更可靠
合并去重 按时间戳去重后合并 多数据源互补
最新优先 以最近更新的为准 实时性要求高

推荐策略:传感器实时数据写本地,Health Kit历史数据做补充,合并时按时间戳去重。

4.4 心率传感器可用性

不是所有设备都支持PPG心率传感器:

// 检查心率传感器可用性
import sensor from '@ohos.sensor';

const isHeartRateAvailable = sensor.isSensorSupported(sensor.SensorId.HEART_RATE);
if (!isHeartRateAvailable) {
  console.warn('[Health] 心率传感器不可用,请使用手表设备');
  // 降级方案:从Health Kit读取历史心率数据
}

4.5 数据量与性能优化

长期使用后,健康数据量会快速增长:

数据类型 每日记录数 30天数据量
步数 ~100条 ~3000条
心率 ~86400条(1Hz) ~260万条
血氧 ~1440条(1次/分) ~4.3万条

优化建议

  1. 心率数据降采样:存储时1分钟聚合一次,只保存均值和极值
  2. 定期归档:超过30天的数据迁移到归档表
  3. 索引优化:为常用查询字段建立复合索引
  4. 批量写入:使用batchInsert替代循环insert

五、HarmonyOS 6适配

5.1 Health Kit增强API

HarmonyOS 6对Health Kit进行了多项增强:

// HarmonyOS 6:Health Kit聚合查询
import health from '@ohos.health';

// 按小时聚合步数数据
const hourlySteps = await health.queryAggregated({
  dataType: health.HealthDataType.STEP_COUNT,
  startTime: dayStart,
  endTime: dayEnd,
  aggregateBy: 'hour',  // 新增:按小时聚合
});

// 按天聚合心率数据
const dailyHeartRate = await health.queryAggregated({
  dataType: health.HealthDataType.HEART_RATE,
  startTime: weekStart,
  endTime: weekEnd,
  aggregateBy: 'day',   // 按天聚合
  statistics: ['avg', 'max', 'min', 'resting'],  // 新增:统计类型
});

5.2 健康数据实时同步

HarmonyOS 6支持健康数据的跨设备实时同步:

// HarmonyOS 6:跨设备健康数据同步
import healthSync from '@ohos.health.sync';

// 开启自动同步
await healthSync.enableAutoSync({
  dataTypes: [
    health.HealthDataType.STEP_COUNT,
    health.HealthDataType.HEART_RATE,
  ],
  syncInterval: 300000,  // 5分钟同步一次
  wifiOnly: false,        // 允许移动网络同步
});

// 监听同步状态
healthSync.on('syncComplete', (result) => {
  console.info(`同步完成: ${result.syncedRecords}条记录`);
  // 刷新UI数据
  this.loadTodayData();
});

healthSync.on('syncError', (error) => {
  console.error(`同步失败: ${error.message}`);
});

5.3 健康报告生成

HarmonyOS 6提供系统级健康报告生成能力:

// HarmonyOS 6:健康报告生成
import healthReport from '@ohos.health.report';

// 生成周报
const weeklyReport = await healthReport.generate({
  type: 'WEEKLY',
  startDate: weekStart,
  endDate: weekEnd,
  includeDataTypes: [
    health.HealthDataType.STEP_COUNT,
    health.HealthDataType.HEART_RATE,
    health.HealthDataType.SLEEP,
  ],
  language: 'zh-CN',
});

// 报告内容
console.info(`步数达标率: ${weeklyReport.stepGoalCompletion}%`);
console.info(`平均心率: ${weeklyReport.avgHeartRate} bpm`);
console.info(`睡眠评分: ${weeklyReport.sleepScore}/100`);
console.info(`健康建议: ${weeklyReport.recommendations}`);

5.4 隐私计算

HarmonyOS 6引入隐私计算框架,在不暴露原始数据的前提下进行健康分析:

// HarmonyOS 6:隐私计算
import privacyCompute from '@ohos.privacy.compute';

// 在加密数据上计算统计值(不暴露原始心率数据)
const result = await privacyCompute.aggregate({
  dataType: health.HealthDataType.HEART_RATE,
  operation: 'AVERAGE',  // 只返回均值,不返回原始数据
  timeRange: { start: weekStart, end: weekEnd },
});

console.info(`平均心率: ${result.value} bpm`); // 只有均值,无法反推原始数据

六、总结

本文完整实现了基于HarmonyOS的健康数据采集与华为运动健康对接系统,核心要点如下:

模块 关键技术 要点
本地存储 RDB关系型数据库 S3级安全+AES-256加密
Health Kit 华为健康生态对接 权限申请+数据读写+变更订阅
数据采集 多传感器+Health Kit 传感器实时+Health Kit历史
数据可视化 环形进度+柱状图+心率区间 今日概览+7天趋势双视图
心率监测 PPG心率传感器 1Hz采样+实时区间指示

最佳实践建议

  1. 数据库必须S3级加密,健康数据泄露后果严重
  2. Health Kit权限需提前申请,审核周期1-3天
  3. 心率数据需降采样存储,1Hz全量存储30天可达260万条
  4. 多数据源合并需去重,按时间戳+来源去重
  5. 传感器不可用时降级到Health Kit,确保功能可用性
  6. 隐私合规是底线,健康数据的使用必须获得用户明确授权

系列总结

本系列5篇文章从计步器、运动检测、跌倒检测、运动类型识别到健康数据采集,完整覆盖了HarmonyOS运动传感应用的开发全链路:

篇目 核心能力 关键API
316 计步器 步数统计+步态分析 ACCELEROMETER + PEDOMETER
317 运动检测 活动状态识别 ACCELEROMETER + GYROSCOPE + GRAVITY
318 跌倒检测 三阶段跌倒判定+紧急求助 ACCELEROMETER + NOTIFICATION + CALL
319 运动类型 步行/跑步/骑行分类 多传感器融合 + 决策树分类
320 健康数据 数据采集+存储+生态对接 RDB + Health Kit + 数据可视化

运动传感是HarmonyOS健康生态的基础能力,掌握这些技术将帮助你构建完整的运动健康应用。建议从系统级API(PEDOMETER、activityRecognition、Health Kit)入手,在系统能力不足时再自研算法补充。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。