我是兰瓶Coding,一枚刚踏入鸿蒙领域的转型小白,原是移动开发中级,如下是我学习笔记《零基础学鸿蒙》,若对你所有帮助,还请不吝啬的给个大大的赞~

前言🧐

先打个招呼:这不是“又一篇功能清单”,而是一份能上手、能落地、能被吐槽也能被点赞的鸿蒙(HarmonyOS/OpenHarmony)智慧健康管理应用全栈实践手记。写这篇的时候,我脑子里一直响着一句话——健康数据不是数字,是真人一天的起起伏伏。所以咱既要把架构“盘明白”,也要把体验“盘舒服”,顺手再把隐私和合规兜住。行了,废话不多说,**开整!**💪

一、背景:移动健康趋势 + 多设备协作需求

移动健康这几年像开了高铁:手表测心率、耳机测温、手机算步数、手环盯睡眠。真正的难点不在“能不能采”,而在怎么把多端数据揉成一个“懂你”的时间线

  • 设备碎片化:手表/手环型号五花八门,协议各有脾气。
  • 数据维度多:步数、心率、HRV、睡眠阶段、饮水、饮食……每个都自称“关键指标”
  • 协同场景多:手机看日报、平板看趋势、手表收提醒、家庭端做社交鼓励。

一句话目标一个账号、多设备协同、统一指标模型、可追溯可验证、够安全。否则“智慧”两个字摆那儿,多半要挨用户白眼。😅

二、需求分析:把“想要”掰成“能做”

  • 步数监测:日/周/月统计,目标达成动画,节假日特例(比如国庆爬山“爆表”)。

  • 心率/睡眠数据

    • 心率:静息、运动区间、异常提醒(过高/过低)。
    • 睡眠:分期(浅/深/REM)+ 入睡时长 + 夜醒次数。
  • 饮食记录:拍照/手录,估算热量与宏量营养素(碳水/蛋白/脂肪)。

  • 目标设定:步数目标、运动时长、体重变化;分阶段可调整

  • 社交分享:好友可见、家人鼓励、勋章系统(别小看“虚拟奖杯”😉)。

约束条件(写死在需求文档首页):
1)用户可控:采集范围、上传与否、分享对象——统统给开关。
2)离线可用:弱网或无网,核心功能不塌。
3)跨屏一致:手机、平板、穿戴设备体验一致性>花哨度。

三、架构设计:鸿蒙客户端 + 后端服务 + 可穿戴数据接口

3.1 总体拓扑

[ Wearable (Watch/Band) ] --BLE/GATT--> [ HarmonyOS App ]
            |                                  |
            |                                  |  Distributed Data (可选 P2P 同步)
            |                                  v
            |                             [ Local DB + Cache ]
            |                                  |
            |                               HTTPS/HTTP2
            v                                  v
     [ Vendor/Standard GATT ] -----> [ Backend API (Koa/NestJS) ] ----> [ DB + TSDB + Object Storage ]
                                                   |
                                                   +--> [ Analytics/Jobs ] --> 个性化提醒/趋势计算

3.2 模块拆分

  • 端侧(HarmonyOS ArkTS/Stage 模型)

    • 数据采集层:传感器、BLE、厂商 SDK。
    • 同步层:分布式数据管理(可选)+ 离线任务队列。
    • 展示层:趋势图、打点、目标进度。
    • 通知层:定时提醒 + 智能触发(高心率/久坐)。
  • 服务侧

    • API 层(REST + WebSocket/2 推送)。
    • 存储层(PostgreSQL + 时序数据库/列式:如 Timescale/ClickHouse)。
    • 任务层(队列处理、特征提取、聚合)。
  • 设备接口

    • BLE GATT:标准步数/心率服务 + 厂商自定义特征。
    • 厂商云桥(可选):以用户授权 Token 拉取历史数据。

四、功能模块:采集、同步、分析(趋势图)、提醒、互动

4.1 数据采集(端侧)

心率/步数(BLE GATT 订阅)——示例代码(ArkTS,简化示意):

// /features/ble/HeartRateClient.ets
import ble from '@ohos.bluetooth.le';
import { HeartRateSample } from '../models/metrics';

const HEART_RATE_SERVICE = '0000180d-0000-1000-8000-00805f9b34fb';
const HEART_RATE_MEAS   = '00002a37-0000-1000-8000-00805f9b34fb';

export class HeartRateClient {
  private device?: ble.BLEPeripheralDevice;

  async connect(deviceId: string) {
    this.device = await ble.connectDevice({ deviceId, transport: ble.BLETransport.TRANSPORT_LE });
    await this.device?.discoverServices();
    await this.subscribeHeartRate();
  }

  private async subscribeHeartRate() {
    const char = await this.device?.getCharacteristic(HEART_RATE_SERVICE, HEART_RATE_MEAS);
    await char?.setNotify(true, (value: Uint8Array) => {
      const bpm = value[1]; // 简化:真实场景需按协议位解析
      const sample: HeartRateSample = { bpm, ts: Date.now(), source: 'watch' };
      globalThis.eventBus.emit('metric:heartRate', sample);
    });
  }

  async disconnect() {
    await this.device?.disconnect();
  }
}

步数(系统传感器/厂商 SDK):若设备直连不到,可用系统步数传感器作为兜底;历史补齐走厂商云桥(用户授权后)。

4.2 本地存储与同步(RDB + 分布式数据 + 离线队列)

// /data/LocalStore.ts
import rdb from '@ohos.data.rdb';

export class LocalStore {
  private store!: rdb.RdbStore;

  async init() {
    const config: rdb.StoreConfig = { name: 'health.db', securityLevel: rdb.SecurityLevel.S1 };
    const store = await rdb.getRdbStore(globalThis.context, config);
    await store.executeSql(`
      CREATE TABLE IF NOT EXISTS heart_rate(
        ts INTEGER PRIMARY KEY,
        bpm INTEGER NOT NULL,
        source TEXT
      );
    `);
    await store.executeSql(`
      CREATE TABLE IF NOT EXISTS steps(
        ts INTEGER PRIMARY KEY,
        steps INTEGER NOT NULL,
        source TEXT
      );
    `);
    this.store = store;
  }

  async insertHeartRate(ts: number, bpm: number, source = 'watch') {
    await this.store.insert('heart_rate', { 'ts': ts, 'bpm': bpm, 'source': source });
  }

  async queryHeartRate(rangeStart: number, rangeEnd: number) {
    const result = await this.store.querySql(
      'SELECT ts,bpm FROM heart_rate WHERE ts BETWEEN ? AND ? ORDER BY ts ASC',
      [rangeStart, rangeEnd]
    );
    return result.rows;
  }
}

离线同步队列(重试、去重、背压):

// /sync/UploadQueue.ts
import http from '@ohos.net.http';

export class UploadQueue {
  private pending: Array<{ type: string; payload: any; id: string }> = [];
  private uploading = false;

  enqueue(job: { type: string; payload: any; id: string }) {
    if (this.pending.find(j => j.id === job.id)) return; // 幂等
    this.pending.push(job);
    this.flush();
  }

  async flush() {
    if (this.uploading || this.pending.length === 0) return;
    this.uploading = true;
    const job = this.pending[0];
    try {
      const client = http.createHttp();
      const res = await client.request('https://api.example.com/metrics', {
        method: http.RequestMethod.POST,
        header: { 'Content-Type': 'application/json' },
        extraData: JSON.stringify(job),
        connectTimeout: 5000,
        readTimeout: 5000
      });
      if (res.responseCode >= 200 && res.responseCode < 300) {
        this.pending.shift();
      } else {
        // 失败:指数退避
        await this.delay(2000);
      }
    } catch {
      await this.delay(3000);
    } finally {
      this.uploading = false;
      this.flush();
    }
  }

  private delay(ms: number) { return new Promise(r => setTimeout(r, ms)); }
}

4.3 分析与趋势图(端侧绘制)

轻量趋势图(Canvas2D 简例)

// /ui/TrendChart.ets
@Component
export struct TrendChart {
  @State data: Array<{ x: number, y: number }> = [];

  build() {
    Canvas({
      onReady: (ctx) => this.draw(ctx)
    })
    .width('100%').height(160);
  }

  private draw(ctx: RenderingContext2D) {
    if (this.data.length < 2) return;
    const W = ctx.width, H = ctx.height;
    ctx.clearRect(0, 0, W, H);
    // 边距
    const pad = 12;
    // 归一化
    const xs = this.data.map(p => p.x);
    const ys = this.data.map(p => p.y);
    const minY = Math.min(...ys), maxY = Math.max(...ys);
    ctx.beginPath();
    this.data.forEach((p, i) => {
      const x = pad + (i * (W - pad * 2)) / (this.data.length - 1);
      const y = pad + (H - pad * 2) * (1 - (p.y - minY) / Math.max(1, (maxY - minY)));
      if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
    });
    ctx.lineWidth = 2;
    ctx.stroke();
  }
}

端侧图表建议:小即美。复杂可视化(如睡眠阶段堆叠图)可在服务端聚合后只传“可绘数据”,降低端侧 CPU/电耗。

4.4 提醒机制(定时 + 智能触发)

  • 定时提醒:喝水、运动打卡、早睡。

  • 智能触发

    • 心率连续 5 分钟高于阈值且处于静息态 → 震动 + 提示。
    • 当日步数落后目标 30% 且现在是可行运动时段 → 轻推提醒。

ArkTS 通知/闹钟示例:

// /notify/Reminder.ts
import alarm from '@ohos.alarmManager';
import notification from '@ohos.notificationManager';

export async function scheduleDrinkWater(hour: number, minute: number) {
  const now = Date.now();
  const target = new Date(); target.setHours(hour, minute, 0, 0);
  if (target.getTime() <= now) target.setDate(target.getDate() + 1);

  await alarm.setAlarm({
    type: alarm.AlarmType.RTC_WAKEUP,
    triggerTime: target.getTime(),
    interval: 24 * 3600 * 1000, // 每日
    wantAgent: { /* 跳应用页面 */ }
  });

  await notification.publish({ id: 101, content: { contentType: notification.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
    normal: { title: 'Hydration Time', text: 'A glass of water makes your heart happier.' } } });
}

4.5 用户互动(社交/勋章/群挑战)

  • 周赛:步数 Top3 徽章;
  • 家人守护:异常心率代收提醒(需双向授权);
  • 分享卡片:将今日成绩渲染成长图,一键发圈。

五、UI/UX 设计:跨设备自适应 + 数据可视化

  • 布局:手机以单栏卡片为主,平板切成双栏信息+图表,手表只给关键数字 + 快捷动作
  • 色彩:健康域强烈建议高对比度 + 情绪色(绿色达标、橙色警示、红色异常)。
  • 动效:达到目标时进度环“啪”一声弹开(慎用,克制就好)。
  • 可访问性:字号、对比度、语音播报;个人感觉这块做细了,用户评价会蹭蹭涨。

ArkUI 断点自适应(简化示意):

// /ui/Responsive.ets
@Entry
@Component
struct Dashboard {
  @State stepsToday: number = 7534;

  build() {
    Column() {
      if (this.isTablet()) {
        Row() {
          this.leftPanel().width('40%');
          this.rightPanel().width('60%');
        }
      } else {
        this.leftPanel();
        this.rightPanel();
      }
    }.width('100%').height('100%').padding(16);
  }

  leftPanel() {
    Column() {
      Text('Today Steps').fontSize(18).fontWeight(FontWeight.Bold);
      Text(`${this.stepsToday}`).fontSize(40);
    }
  }

  rightPanel() {
    TrendChart({ data: /* map to chart points */ [] });
  }

  private isTablet(): boolean {
    return Display.getDefaultDisplaySync().width > 1000; // 粗略示意
  }
}

六、技术难点与“人肉避坑指南”

6.1 设备数据同步

  • 多源合并:手表与手机步数重复?以来源优先级 + 时间分片合并。
  • 时间漂移:设备时间不准 → 以服务器时间为准,上传时做 ts_server = now() 旁路列,分析用“服务器时间轴”。
  • 幂等与乱序:上传携带 deviceId + seq,服务端去重;乱序写入用时序 upsert

6.2 多设备适配

  • 分层:UI 组件(通用) + 渲染策略(断点/密度) + 硬件能力检测
  • 手表端:避免长列表,以 glance 卡片为主;交互尽量单击 + Crown 滚动。

6.3 隐私保护

  • 最小化采集:打开哪个能力,弹窗清晰告知用途。
  • 端侧加密:HUKS(密钥管理)+ RDB 加密列(需业务字段拆分)。
  • 传输与存储:TLS、后端分区存储(PII 与指标分离);定期可撤回/删除实现“被遗忘权”的近似效果。
  • 审计:谁、何时、为啥访问了我的数据?有记录、可追责

端侧密钥示意(伪代码):

// /sec/KeyVault.ts
import huks from '@ohos.security.huks';

export async function getOrCreateKey(alias: string) {
  const exist = await huks.isKeyExist({ properties: { alias } });
  if (!exist) {
    await huks.generateKey({ properties: { alias, purpose: huks.HuksKeyPurpose.ENCRYPT | huks.HuksKeyPurpose.DECRYPT, alg: huks.HuksAlg.AES } });
  }
  return alias;
}

6.4 算法推荐(轻量)

  • 步数目标自适应:最近 7 天 P70 作为次日目标的基线。
  • 睡眠建议:以入睡时长波动 + 夜醒次数给出“护肝建议”(文案友好、不过度医疗化)。
  • 异常阈值:心率阈值结合年龄与静息心率个体化,而不是“全员 120 次/分一刀切”。

七、后端服务:你得有个“稳”的腰

7.1 数据模型(PostgreSQL + Timescale)

-- 用户表(PII 与指标分离)
CREATE TABLE users (
  id           uuid PRIMARY KEY,
  phone_hash   text UNIQUE, -- 存哈希不存明文
  display_name text,
  created_at   timestamptz default now()
);

-- 心率时序
CREATE TABLE hr_samples (
  user_id   uuid not null,
  ts        timestamptz not null,
  bpm       smallint not null,
  source    text,
  PRIMARY KEY (user_id, ts)
);
SELECT create_hypertable('hr_samples', by_range('ts'));

-- 步数(聚合为分钟粒度)
CREATE TABLE steps_min (
  user_id   uuid not null,
  ts_min    timestamptz not null,
  steps     integer not null,
  source    text,
  PRIMARY KEY (user_id, ts_min)
);

7.2 API(Koa + JWT + 速率限制)

// server/app.ts
import Koa from 'koa';
import Router from 'koa-router';
import body from 'koa-body';
import jwt from 'koa-jwt';
import { rateLimiter } from './infra/ratelimit';
import { insertHeartRate, queryHeartRateRange } from './repo/metrics';

const app = new Koa();
const api = new Router({ prefix: '/api' });

api.post('/metrics/hr', async (ctx) => {
  const { samples } = ctx.request.body as { samples: Array<{ ts: number; bpm: number; source?: string }> };
  const userId = ctx.state.user.sub;
  await insertHeartRate(userId, samples);
  ctx.body = { ok: true, count: samples.length };
});

api.get('/metrics/hr', async (ctx) => {
  const userId = ctx.state.user.sub;
  const { start, end } = ctx.query as any;
  const rows = await queryHeartRateRange(userId, Number(start), Number(end));
  ctx.body = { list: rows };
});

app.use(rateLimiter({ limit: 120, windowSec: 60 }));
app.use(body());
app.use(jwt({ secret: process.env.JWT_SECRET! }).unless({ path: [/^\/healthz/] }));
app.use(api.routes());
app.listen(8080);

速率限制(Redis 滑动窗口)与前文端侧队列相呼应;鉴权既要管住,也要不折腾用户。

八、实验与评估:别只看“能跑”,要看“跑得怎么样”

  • 功能实现覆盖率:需求清单对照测试用例;UI 可用性走 8 人小样本可用性测试(SUS 评分)。

  • 数据准确性

    • 心率:与医疗级脉搏仪对照抽样(误差 MAE/RE 分析)。
    • 步数:室内/室外/坐站转换场景对比(误检/漏检率)。
  • 响应速度

    • 端侧渲染:首页首屏 < 300ms,趋势图绘制 < 16ms/帧(60fps)。
    • 同步延迟:采集到可见 < 3s(弱网下 < 10s)。
  • 功耗:心率持续采集场景 1 小时耗电占比;夜间待机不打扰策略。

  • 稳健性:BLE 断连自动重连成功率 > 98%,上传重试最终达成率 > 99.5%。

九、总结与展望:下一站,预测与连接

把这套系统落下地,你已经从“凑功能”走到了“做产品”。往前再迈一步:

  • 个体化预测:基于近 30 天特征,预测次日目标可达性,提醒不打扰
  • 远程医疗接口:与医院/健康管理机构打通预约/问诊(基于用户主动授权与合规审查)。
  • 更多设备:耳夹式血氧、智能秤、跑步机数据流;指标统一模型持续进化。
  • 隐私计算:联邦学习/本地特征提取,上传脱敏统计量,而非原始数据。

结尾多一句“掏心窝”:健康不是 KPI,是生活方式。技术只是帮手,别让它喧宾夺主。

十、附:端到端“能跑”的最小闭环(步骤清单)

  1. 手表心率 GATT 订阅 → ArkTS 入库(RDB)。
  2. 本地上传队列(离线重试)→ 后端 Koa 写入 Timescale。
  3. 手机端趋势图拉取聚合数据 → 绘制折线图。
  4. 定时提醒与异常心率触发 → 通知到达手表端。
  5. 账号与隐私页可控开关 → 审计日志落库。

额外代码片段:目标设定与达成动画(ArkTS 简例)

// /features/goals/GoalRing.ets
@Component
export struct GoalRing {
  @Prop progress: number = 0.62; // 0~1
  build() {
    Canvas({ onReady: (ctx) => this.draw(ctx) }).width(160).height(160);
  }
  private draw(ctx: RenderingContext2D) {
    const W = ctx.width, H = ctx.height, R = Math.min(W, H)/2 - 8;
    ctx.clearRect(0,0,W,H);
    ctx.beginPath();
    ctx.arc(W/2, H/2, R, 0, 2*Math.PI);
    ctx.lineWidth = 10; ctx.globalAlpha = 0.2; ctx.stroke();
    ctx.globalAlpha = 1;
    ctx.beginPath();
    ctx.arc(W/2, H/2, R, -Math.PI/2, -Math.PI/2 + 2*Math.PI*this.progress);
    ctx.lineWidth = 10; ctx.stroke();
    ctx.font = '20px sans-serif'; ctx.textAlign = 'center';
    ctx.fillText(`${Math.round(this.progress*100)}%`, W/2, H/2+6);
  }
}

开发者自检清单(贴墙上)✅

  • 数据模型:指标与 PII 分离
  • BLE:订阅、重连、功耗控制
  • 本地:RDB 索引、异常兜底
  • 同步:队列 + 指数退避 + 幂等键
  • 后端:写入限流、时序 upsert、冷热分层
  • 隐私:最小化、可撤回、日志审计
  • UI:断点适配、关键指标首屏直达
  • 测试:准确性/性能/功耗/弱网

最后一句“碎碎念”🙃

做健康产品,别只盯“日活”和“留存”,也盯盯自己今天走了几步、几点睡。产品也许会“懂用户”,但别忘了先懂懂自己

(未完待续)

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐