欢迎光临
我们一直在努力

基于鸿蒙os开发跑步管理系统(三)-数据模型设计

数据模型设计

模型总览

RunAdvisor 定义了 8 个数据模型类,分布在 5 个源文件中,构成应用的核心数据结构:

模型文件用途
UserProfile model/UserProfile.ets 用户个人档案
WeatherInfo model/WeatherInfo.ets 天气信息
FoodItem model/FoodItem.ets 食物条目
DietRecord model/FoodItem.ets 饮食摄入记录
RunAdvice model/RunAdvice.ets 跑步建议
RouteInfo model/RouteInfo.ets 路线信息
RoutePoint model/RouteInfo.ets 路线途经点
Landmark model/RouteInfo.ets 地标信息

模型关系图

UserProfile WeatherInfo
|– weight |– temperature
|– height |– feelsLike
|– age |– humidity
|– gender |– windSpeed
|– dailyStepGoal |– description
|– dailyCalorieGoal |– icon
|– weeklyRunGoal |– cityName
|– isProfileSet |
| |
+—-+ +—-+
| | |
v v v
RunAdvice <—- RunAdvisorService —-> WeatherInfo
|– runType
|– runTypeName FoodItem
|– targetDistance |– id
|– targetPaceMin/Sec |– name
|– targetHRMin/Max |– category
|– durationMin |– caloriesPer100g
|– warmupMin |– defaultGrams
|– cooldownMin |
|– tips v
|– intensity DietRecord
|– color |– foodId
|– description |– foodName
| |– grams
v |– calories
RouteInfo |– timestamp
|– id |– category
|– name
|– type
|– color
|– distanceKm
|– durationMin
|– difficulty
|– description
|– points[] —-> RoutePoint
|– lat
|– lng
|– name

Landmark
|– name
|– lat
|– lng

UserProfile – 用户档案模型

存储用户基本信息和运动目标,是整个应用的个性化基础。

字段定义

export class UserProfile {
weight: number = 70; // 体重 (kg)
height: number = 170; // 身高 (cm)
age: number = 30; // 年龄
gender: string = 'male'; // 性别 ('male' | 'female')
dailyStepGoal: number = 10000; // 每日步数目标
dailyCalorieGoal: number = 2200; // 每日热量目标 (kcal)
weeklyRunGoal: number = 5; // 每周跑步目标 (km)
isProfileSet: boolean = false; // 是否已完成资料设置
}

计算方法

// BMI 指数计算
getBMI(): number {
const heightM: number = this.height / 100;
return this.weight / (heightM * heightM);
}

// BMI 分类判断
getBMICategory(): string {
const bmi: number = this.getBMI();
if (bmi < 18.5) return '偏瘦';
if (bmi < 24) return '正常';
if (bmi < 28) return '偏胖';
return '肥胖';
}

// 最大心率 (220 – 年龄公式)
getMaxHR(): number {
return 220 this.age;
}

// 步长估算 (男性 0.78m, 女性 0.66m)
getStepLength(): number {
return this.gender === 'male' ? 0.78 : 0.66;
}

// 每步热量消耗 (男性 0.05kcal, 女性 0.04kcal)
getCaloriePerStep(): number {
return this.gender === 'male' ? 0.05 : 0.04;
}

持久化键

UserProfile 通过 PreferencesService 序列化为 JSON 存储,对应的常量键为:

  • Constants.PROFILE_KEY = ‘user_profile’
  • Constants.PROFILE_SET_KEY = ‘is_profile_set’

WeatherInfo – 天气信息模型

存储从 OpenWeatherMap API 获取的天气数据,并提供运动适宜性判断。

字段定义

export class WeatherInfo {
temperature: number = 0; // 实际温度 (Celsius)
feelsLike: number = 0; // 体感温度 (Celsius)
humidity: number = 0; // 湿度 (%)
windSpeed: number = 0; // 风速 (m/s)
description: string = ''; // 天气描述 (中文)
icon: string = ''; // 天气图标代码
cityName: string = ''; // 城市名称
}

运动适宜性判断逻辑

isGoodForRunning(): boolean {
if (this.temperature < 10 || this.temperature > 35) return false;
if (this.windSpeed > 15) return false;
if (this.humidity > 90) return false;
return true;
}

getWeatherAdvice(): string {
if (this.temperature < 0) return '气温过低,注意保暖防冻,建议室内运动';
if (this.temperature > 35) return '高温预警,避免户外跑步,注意防暑';
if (this.windSpeed > 10) return '风力较大,跑步时逆风会较吃力';
if (this.humidity > 80) return '湿度较高,注意补水和降温';
return '天气适宜跑步,享受运动吧!';
}

判断阈值

指标不适宜阈值建议调整阈值
温度 (低) < -10 C < 0 C
温度 (高) > 35 C > 35 C
风速 > 15 m/s > 10 m/s
湿度 > 90% > 80%

FoodItem – 食物条目模型

定义食物的基础热量信息,支持按重量计算热量。

字段定义

export class FoodItem {
id: string = ''; // 唯一标识 (如 'g1', 'm3', 'v5')
name: string = ''; // 食物名称
category: string = ''; // 类别 (谷物/肉类/蔬菜/水果/乳制品)
caloriesPer100g: number = 0; // 每100g热量 (kcal)
defaultGrams: number = 100; // 默认摄入量 (g)
}

热量计算方法

getCalories(grams: number): number {
return Math.round(this.caloriesPer100g * grams / 100);
}

ID 命名规则

前缀类别示例
g 谷物 g1=米饭, g2=馒头, g3=面条
m 肉类 m1=鸡胸肉, m3=牛肉, m10=虾仁
v 蔬菜 v1=西红柿, v6=土豆, v15=豆腐
f 水果 f1=苹果, f2=香蕉, f9=猕猴桃
d 乳制品 d1=牛奶, d2=酸奶, d4=鸡蛋

DietRecord – 饮食摄入记录模型

记录用户每次食物摄入的详细信息,用于每日热量统计。

字段定义

export class DietRecord {
foodId: string = ''; // 关联的 FoodItem.id
foodName: string = ''; // 食物名称 (冗余存储)
grams: number = 0; // 实际摄入量 (g)
calories: number = 0; // 本次摄入热量 (kcal)
timestamp: number = 0; // 记录时间 (Date.now())
category: string = ''; // 食物类别 (冗余存储)
}

创建方式

DietRecord 通过关联 FoodItem 创建,自动计算热量:

static create(food: FoodItem, grams: number): DietRecord {
const d = new DietRecord();
d.foodId = food.id;
d.foodName = food.name;
d.grams = grams;
d.calories = food.getCalories(grams);
d.timestamp = Date.now();
d.category = food.category;
return d;
}

每日热量计算

PreferencesService 中通过时间戳过滤当日记录:

getTodayCalories(): number {
const records: DietRecord[] = this.getDietRecords();
const todayStart: number = new Date(
new Date().getFullYear(),
new Date().getMonth(),
new Date().getDate()
).getTime();
let total: number = 0;
for (let i = 0; i < records.length; i++) {
if (records[i].timestamp >= todayStart) {
total += records[i].calories;
}
}
return total;
}

RunAdvice – 跑步建议模型

存储由 RunAdvisorService 算法生成的跑步方案数据。

字段定义

export class RunAdvice {
runType: string = ''; // 跑步类型 (easy/standard/endurance)
runTypeName: string = ''; // 类型中文名 (轻松跑/标准跑/耐力跑)
targetDistance: number = 0; // 目标距离 (km)
targetPaceMin: number = 0; // 配速分钟部分
targetPaceSec: number = 0; // 配速秒部分
targetHRMin: number = 0; // 目标心率下限 (bpm)
targetHRMax: number = 0; // 目标心率上限 (bpm)
durationMin: number = 0; // 预计时长 (分钟)
warmupMin: number = 0; // 热身时长 (分钟)
cooldownMin: number = 0; // 放松时长 (分钟)
tips: string = ''; // 专业建议文本
intensity: string = ''; // 强度等级 (低/中/高)
color: string = ''; // 主题色
description: string = ''; // 方案描述
}

显示辅助方法

// 配速格式化: "7'30""
getPaceDisplay(): string {
return `${this.targetPaceMin}'${this.targetPaceSec < 10 ? '0' : ''}${this.targetPaceSec}"`;
}

// 心率区间格式化: "114-123"
getHRDisplay(): string {
return `${this.targetHRMin}${this.targetHRMax}`;
}

RouteInfo / RoutePoint / Landmark – 路线模型

RoutePoint – 途经点

export class RoutePoint {
lat: number = 0; // 纬度
lng: number = 0; // 经度
name: string = ''; // 途经点名称
}

RouteInfo – 路线信息

export class RouteInfo {
id: string = ''; // 路线ID
name: string = ''; // 路线名称
type: string = ''; // 路线类型 (easy/standard/endurance)
color: string = ''; // 主题色
distanceKm: number = 0; // 总距离 (km)
durationMin: number = 0; // 预计时长 (分钟)
difficulty: string = ''; // 难度等级
description: string = ''; // 路线描述
points: RoutePoint[] = []; // 途经点列表
}

Landmark – 地标

export class Landmark {
name: string = ''; // 地标名称
lat: number = 0; // 纬度
lng: number = 0; // 经度
}

三条路线数据

路线ID距离时长难度途经点数颜色
轻松跑 route_easy 3.5km 25min 入门 7 #4CAF50 (绿)
标准跑 route_standard 5.8km 40min 中等 12 #2196F3 (蓝)
耐力跑 route_endurance 10.2km 65min 挑战 14 #FF9800 (橙)

模型设计原则

静态工厂方法模式

所有模型类遵循统一的对象创建模式:无参构造器 + 静态工厂方法。这一设计直接源于 ArkTS 严格模式的约束。在 ArkTS 中,@Sendable 修饰的类不支持带参数的构造器,而所有需要跨上下文传递的模型类都应遵循此规范。

工厂方法的优势不仅在于合规性,还体现在默认值隔离。每个字段在类声明时初始化默认值(如 weight: number = 70),构造器创建的对象处于合法状态,工厂方法仅在需要时覆盖特定字段。这避免了 “半初始化对象” 问题——即使某些字段未被工厂方法赋值,对象仍持有合理默认值。

static create(weight: number, height: number, age: number, ): UserProfile {
const p = new UserProfile();
p.weight = weight;
p.height = height;
p.age = age;
return p;
}

冗余字段策略

DietRecord 中的 foodName 和 category 字段看似冗余(可通过 foodId 关联 FoodItem 获取),但这是有意的设计决策。

首先,饮食记录的时间跨度可能跨越食物数据库的更新周期。如果仅存储 foodId 引用,当 FoodItem 数据变更(如修正热量值或重命名)后,历史记录的显示将产生不一致。冗余存储确保历史记录始终反映添加时刻的准确信息。

其次,查询效率。今日热量统计需要遍历所有记录求和,如果每条记录都需通过 foodId 查询 FoodItem 再获取热量,时间复杂度从 O(n) 升至 O(n*m)(n 为记录数,m 为食物数)。冗余存储 calories 字段使统计查询保持 O(n)。

类型安全的 JSON 解析

ArkTS 严格模式禁止 any 类型,JSON.parse() 的返回值必须通过 Record<string, Object> 中转。所有模型的反序列化遵循以下模式:

const obj: Record<string, Object> = JSON.parse(json) as Record<string, Object>;
const p: UserProfile = new UserProfile();
p.weight = (obj['weight'] as number) ?? Constants.DEFAULT_WEIGHT;
p.height = (obj['height'] as number) ?? Constants.DEFAULT_HEIGHT;
p.age = (obj['age'] as number) ?? Constants.DEFAULT_AGE;

每个字段使用 as 进行类型断言,配合 ?? 运算符提供降级默认值。这确保了即使持久化数据损坏(如版本升级导致字段缺失),应用也不会崩溃,而是回退到默认值。

UserProfile 字段详解

体重 (weight)

单位:千克 (kg)。默认值 70,取值范围建议 30-200。体重是 BMI 计算和热量消耗估算的核心输入。BMI 公式为 weight / (height/100)^2,偏瘦/正常/偏胖/肥胖的判定阈值分别为 18.5、24、28。每步热量消耗系数因性别而异(男性 0.05 kcal/步,女性 0.04 kcal/步),体重也会影响实际消耗,当前采用简化估算。

身高 (height)

单位:厘米 (cm)。默认值 170。身高参与 BMI 计算和步长估算。步长估算公式为:男性 0.78m,女性 0.66m,这是基于统计平均值的简化模型。精确的步长应通过加速度传感器实时计算,但本应用采用固定值以简化实现。

年龄 (age)

默认值 30。年龄影响两个核心计算:最大心率(220 – age)和跑步建议的年龄调整。年龄超过 50 岁时,配速自动减慢 1 min/km,距离缩短 1-2km,确保运动安全。25 岁以下时配速加快 1 min/km,反映年轻群体的较高体能水平。

性别 (gender)

取值:‘male’ 或 ‘female’。性别影响步长估算和每步热量消耗系数。男性步长 0.78m(每 1000 步约 780m),女性步长 0.66m(每 1000 步约 660m)。热量消耗差异约为 20%,男性每步 0.05 kcal,女性 0.04 kcal。

每日步数目标 (dailyStepGoal)

默认值 10000 步。这一数值源自世界卫生组织的推荐运动量,约等于 7-8 公里的步行距离。Progress 组件以 dailyStepGoal 为 total 值,展示完成百分比。

每日热量目标 (dailyCalorieGoal)

默认值 2200 kcal。这是中国成年女性日均推荐摄入量的近似值(男性约 2400 kcal)。用户可根据自身需求调整。热量进度条使用橙色 (#FF9800) 区分于步数的蓝色 (#4A90D9)。

每周跑步目标 (weeklyRunGoal)

单位:千米 (km)。默认值 5km。这一字段当前仅作为展示用途,未来可扩展为周训练计划的基础参数。

是否已设置档案 (isProfileSet)

布尔值,默认 false。这是首次启动引导流程的关键判断依据。当 Preferences 中 PROFILE_SET_KEY 对应值为 false 或不存在时,应用判定为首次启动,自动跳转至 ProfileSetupPage。设置完成后写入 true,此后不再触发引导。

WeatherInfo 运动适宜性判断详解

温度阈值分析

温度范围判断生理依据
< -10 C 不适宜 低温冻伤风险,肌肉僵硬易拉伤
-10 ~ 0 C 适宜但需注意 保暖防冻,建议室内热身后外出
0 ~ 35 C 适宜 正常跑步温度区间
> 35 C 不适宜 热射病风险,核心体温过高

体感温度 (feelsLike) 综合了风速和湿度的影响,比实际温度更贴近人体感受。当前 isGoodForRunning() 使用实际温度判断,未来可改用 feelsLike 提高判断准确性。

风速阈值分析

风速范围判断影响
< 5 m/s 适宜 微风有助散热
5 ~ 10 m/s 适宜但吃力 逆风增加跑步负荷约 5-8%
10 ~ 15 m/s 需注意 建议顺风出发逆风返回
> 15 m/s 不适宜 跑步姿态受影响,安全风险

湿度阈值分析

湿度范围判断影响
< 60% 适宜 汗液蒸发正常,散热有效
60 ~ 80% 稍高 建议增加补水频率
80 ~ 90% 较高 体感闷热,散热效率下降
> 90% 不适宜 汗液难以蒸发,核心体温快速上升

FoodItem 热量数据来源

食物热量数据参考中国食物成分表(中国疾病预防控制中心营养与健康所编),单位为千卡每 100 克 (kcal/100g)。需要注意:

  • 热量值为食物原料的均值,实际值受品种、产地、烹饪方式影响
  • 默认摄入量 (defaultGrams) 根据常见食用份量设定(如米饭 200g、馒头 100g、鸡蛋 60g)
  • 搜索功能支持中文模糊匹配,用户输入 “鸡” 可匹配 “鸡胸肉”、“鸡翅”、“鸡腿” 等
  • DietRecord 每日统计实现

    每日热量统计通过时间戳过滤实现。获取今日零点的时间戳,遍历所有记录,仅统计 timestamp >= 今日零点的记录:

    getTodayCalories(): number {
    const records: DietRecord[] = this.getDietRecords();
    const todayStart: number = new Date(
    new Date().getFullYear(),
    new Date().getMonth(),
    new Date().getDate()
    ).getTime();
    let total: number = 0;
    for (let i = 0; i < records.length; i++) {
    if (records[i].timestamp >= todayStart) {
    total += records[i].calories;
    }
    }
    return total;
    }

    这种实现简单可靠,但有一个潜在问题:当记录数量累积过多时,遍历效率会下降。当前设计中没有记录清理机制,长期使用后 DietRecord 数组可能增长到数百甚至数千条。未来优化方向包括:按日期分桶存储、定期归档历史记录、或引入 RDB 数据库替代。

    RunAdvice 配速与心率关系

    配速和心率是跑步训练的两个核心指标,它们之间存在正相关关系。配速越快(数值越小),心率越高。RunAdvice 中设定的三个心率区间对应三种训练效果:

    方案心率区间训练效果
    轻松跑 60-65% HRmax 有氧基础、脂肪燃烧、恢复
    标准跑 65-70% HRmax 有氧耐力、心肺功能提升
    耐力跑 70-80% HRmax 乳酸阈值训练、速度耐力

    心率区间基于最大心率百分比划分,这是运动科学中广泛使用的分区方法。实际训练中还需考虑静息心率和心率储备(Karvonen 公式),但本应用采用简化公式以降低用户输入负担。

    RoutePoint 与 Canvas 映射

    RoutePoint 中的 GPS 坐标 (lat, lng) 在 Canvas 渲染时需要映射为屏幕像素坐标。映射过程:

  • 遍历所有途经点,找出经纬度的最小值和最大值(边界框)
  • 计算缩放比例:scaleX = canvasWidth / (maxLng – minLng),scaleY = canvasHeight / (maxLat – minLat),取较小值保持等比例
  • 计算偏移量:使路线居中显示
  • 每个途经点:screenX = (lng – minLng) * scale + offsetX,screenY = (maxLat – lat) * scale + offsetY(纬度方向取反,因为屏幕 Y 轴向下)
  • Landmark 使用相同的映射逻辑渲染为圆形标记,确保地标与路线的相对位置正确。

    模型序列化与反序列化全流程

    以 UserProfile 为例,完整的数据生命周期:

    1. 创建: UserProfile.create(65, 175, 25, 'male', …)
    2. 使用: profile.getBMI(), profile.getStepLength()
    3. 序列化: JSON.stringify(profile) -> '{"weight":65,"height":175,…}'
    4. 存储: preferences.putSync(PROFILE_KEY, jsonString)
    5. 持久化: preferences.flushSync()
    6. 读取: preferences.getSync(PROFILE_KEY, '') as string
    7. 反序列化: JSON.parse(json) as Record<string, Object>
    8. 重建: new UserProfile() + 逐字段赋值

    flushSync() 确保数据立即写入磁盘,虽然性能开销略高于异步 flush,但保证了应用异常退出时数据不丢失。在用户保存档案和添加饮食记录等关键操作中,同步持久化是必要的。

    赞(0)
    未经允许不得转载:171主机测评 » 基于鸿蒙os开发跑步管理系统(三)-数据模型设计
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址