基于鸿蒙os开发跑步管理系统(四)-服务层设计
服务层设计
服务总览
RunAdvisor 包含 8 个 Service 类,全部采用单例模式设计,负责各自的业务逻辑处理:
| 服务 | 文件 | 职责 |
|---|---|---|
| PreferencesService | service/PreferencesService.ets | 本地数据持久化 |
| CalorieService | service/CalorieService.ets | 食物热量数据管理 |
| WeatherService | service/WeatherService.ets | 天气数据获取与缓存 |
| StepService | service/StepService.ets | 计步器数据读取 |
| LocationService | service/LocationService.ets | GPS 定位 |
| RunAdvisorService | service/RunAdvisorService.ets | 跑步建议算法 |
| RouteService | service/RouteService.ets | 路线数据管理 |
| LocationEnhanceService | service/LocationEnhanceService.ets | 卡尔曼滤波位置增强 |
单例模式实现
所有 Service 统一采用以下单例模式模板:
export class XxxService {
private static instance: XxxService | undefined = undefined;
private constructor() {}
static getInstance(): XxxService {
if (XxxService.instance === undefined) {
XxxService.instance = new XxxService();
}
return XxxService.instance;
}
}
关键设计要点:
- 构造器设为 private,禁止外部 new 创建
- instance 使用 undefined 初始值,符合 ArkTS 严格模式
- getInstance() 延迟初始化,首次调用时才创建实例
1. PreferencesService - 本地数据持久化
初始化
必须在 EntryAbility.onCreate() 中初始化,传入 UIAbilityContext:
init(context: common.UIAbilityContext): void {
this.store = preferences.getPreferencesSync(context, {
name: Constants.PREFERENCES_NAME // 'run_advisor_prefs'
});
}
核心方法
| 方法 | 功能 | 存储键 |
|---|---|---|
| saveProfile(profile) | 保存用户档案 JSON | PROFILE_KEY |
| getProfile(): UserProfile | 读取用户档案 | PROFILE_KEY |
| isProfileSet(): boolean | 判断是否已设置档案 | PROFILE_SET_KEY |
| saveDietRecords(records) | 保存饮食记录数组 | DIET_RECORD_KEY |
| getDietRecords(): DietRecord[] | 读取饮食记录 | DIET_RECORD_KEY |
| getTodayCalories(): number | 计算今日总热量 | - |
| saveStepCount(count) | 保存步数+日期 | STEP_COUNT_KEY + STEP_DATE_KEY |
| getStepCount(): number | 读取今日步数 | STEP_COUNT_KEY + STEP_DATE_KEY |
| clearProfile() | 清除用户档案 | PROFILE_KEY + PROFILE_SET_KEY |
序列化策略
所有复杂数据通过 JSON.stringify/JSON.parse 序列化,读取时逐字段重建对象实例:
getProfile(): UserProfile {
const json: string = store.getSync(Constants.PROFILE_KEY, '') as string;
if (json === '') return new UserProfile();
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;
// ... 逐字段赋值
return p;
}
步数日期过滤
步数数据按日期判断有效性,跨天自动归零:
getStepCount(): number {
const today: string = new Date().toISOString().split('T')[0];
const savedDate: string = store.getSync(Constants.STEP_DATE_KEY, '') as string;
if (savedDate !== today) return 0;
return store.getSync(Constants.STEP_COUNT_KEY, 0) as number;
}
2. CalorieService - 食物热量数据管理
数据规模
内置约 100 种中国常见食物,分为 5 大类别:
| 类别 | 数量 | ID 前缀 | 示例 |
|---|---|---|---|
| 谷物 | 20 种 | g1-g20 | 米饭(116)、馒头(221)、面条(110) |
| 肉类 | 20 种 | m1-m20 | 鸡胸肉(133)、牛肉(125)、虾仁(87) |
| 蔬菜 | 20 种 | v1-v20 | 西红柿(19)、黄瓜(15)、土豆(76) |
| 水果 | 20 种 | f1-f20 | 苹果(53)、香蕉(93)、西瓜(25) |
| 乳制品 | 10 种 | d1-d10 | 牛奶(54)、酸奶(72)、奶酪(328) |
核心方法
getAllFoods(): FoodItem[] // 获取全部食物
getFoodsByCategory(category: string): FoodItem[] // 按类别筛选
getCategories(): string[] // 获取类别列表 ['谷物','肉类','蔬菜','水果','乳制品']
searchFoods(keyword: string): FoodItem[] // 关键词搜索 (大小写不敏感)
getFoodById(id: string): FoodItem | undefined // 按ID查询
搜索实现
使用 indexOf 实现模糊匹配:
searchFoods(keyword: string): FoodItem[] {
if (keyword === '') return this.foods;
const result: FoodItem[] = [];
const kw: string = keyword.toLowerCase();
for (let i = 0; i < this.foods.length; i++) {
if (this.foods[i].name.toLowerCase().indexOf(kw) >= 0) {
result.push(this.foods[i]);
}
}
return result;
}
3. WeatherService - 天气数据获取
API 接口
使用 OpenWeatherMap REST API,请求格式:
GET https://api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lng}&appid={key}&units=metric&lang=zh_cn
响应解析
const result: Record<string, Object> = JSON.parse(response.result as string);
const main: Record<string, Object> = result['main'] as Record<string, Object>;
const weather0: Record<string, Object> = (result['weather'] as Object[])[0];
const wind: Record<string, Object> = result['wind'] as Record<string, Object>;
const info: WeatherInfo = WeatherInfo.create(
main['temp'] as number, // 温度
main['feels_like'] as number, // 体感温度
main['humidity'] as number, // 湿度
wind['speed'] as number, // 风速
weather0['description'] as string, // 描述
weather0['icon'] as string, // 图标
result['name'] as string // 城市名
);
降级策略
当网络请求失败或缓存不可用时,返回北京默认天气:
getDefaultWeather(): WeatherInfo {
return WeatherInfo.create(22, 20, 55, 3, '晴', '01d', '北京');
}
4. StepService - 计步器服务
数据获取流程
getStepCount()
|-- 优先读取 Preferences 缓存
| +-- 有缓存: 直接返回
|-- 读取计步器传感器
| +-- sensor.once(PEDOMETER)
|-- 保存到 Preferences
|-- 超时 3 秒返回 0
传感器调用
private querySensorSteps(): Promise<number> {
return new Promise((resolve: (value: number) => void) => {
try {
sensor.once(sensor.SensorId.PEDOMETER, (data: sensor.PedometerResponse) => {
resolve(data.steps);
});
} catch (e) {
resolve(0);
}
setTimeout(() => { resolve(0); }, 3000);
});
}
5. LocationService - 定位服务
定位请求
async getCurrentLocation(): Promise<number[]> {
const requestInfo: geoLocationManager.CurrentLocationRequest = {
priority: geoLocationManager.LocationRequestPriority.FIRST_FIX,
scenario: geoLocationManager.LocationRequestScenario.UNSET,
maxAccuracy: 0,
timeoutMs: 10000
};
const location = await geoLocationManager.getCurrentLocation(requestInfo);
return [location.latitude, location.longitude];
}
降级策略
定位失败时返回北京坐标 (39.9042, 116.4074),确保天气查询可用:
private currentLat: number = Constants.BEIJING_LAT; // 39.9042
private currentLng: number = Constants.BEIJING_LNG; // 116.4074
6. RunAdvisorService - 跑步建议算法
后文逐步更新。
7. RouteService - 路线数据管理
三条预置路线
由于模拟器无法直接收到相关位置信息,因此加入了三条预置的测试路线。
| 路线 | type | 距离 | 时长 | 难度 | 途经点 |
|---|---|---|---|---|---|
| 轻松跑 | easy | 3.5km | 25min | 入门 | 天安门→故宫→景山→什刹海→鼓楼 |
| 标准跑 | standard | 5.8km | 40min | 中等 | 天安门→故宫→景山→二环→雍和宫→王府井 (环线) |
| 耐力跑 | endurance | 10.2km | 65min | 挑战 | 天安门→前门→天坛→崇文门→建国门 (大环线) |
9 个预置地标
this.landmarks = [
Landmark.create('天安门', 39.9087, 116.3975),
Landmark.create('故宫', 39.9163, 116.3972),
Landmark.create('景山', 39.9210, 116.3935),
Landmark.create('北海', 39.9260, 116.3830),
Landmark.create('天坛', 39.8820, 116.3930),
Landmark.create('王府井', 39.9200, 116.4000),
Landmark.create('前门', 39.9050, 116.3970),
Landmark.create('鼓楼', 39.9280, 116.3860),
Landmark.create('什刹海', 39.9250, 116.3850),
];
核心方法
getRoutes(): RouteInfo[] // 获取全部路线
getRouteByType(type: string): RouteInfo | undefined // 按类型查询
getRouteById(id: string): RouteInfo | undefined // 按ID查询
getLandmarks(): Landmark[] // 获取地标列表
8. LocationEnhanceService - 位置增强服务
卡尔曼滤波器
实现 4 状态 (x, y, vx, vy) 卡尔曼滤波器,4x4 协方差矩阵手动展开:
export class KalmanFilter {
private stateX: number = 0; // 纬度状态
private stateY: number = 0; // 经度状态
private stateVx: number = 0; // 纬度方向速度
private stateVy: number = 0; // 经度方向速度
private p00...p33: number; // 4x4 协方差矩阵
private q: number = 0.001; // 过程噪声
private r: number = 0.01; // 观测噪声
}
传感器融合流程
1. EntryAbility.onCreate() 中启动陀螺仪监听
2. sensor.on(GYROSCOPE) 持续更新角速度数据
3. enhance(rawLat, rawLng) 被调用时:
a. 计算时间增量 dt
b. 将陀螺仪数据作为控制输入
c. KalmanFilter.update() 执行预测+更新
d. 返回平滑后的坐标
陀螺仪集成
startGyroscope(): void {
sensor.on(sensor.SensorId.GYROSCOPE, (data: sensor.GyroscopeResponse) => {
this.lastGyroX = data.x;
this.lastGyroY = data.y;
this.gyroAvailable = true;
}, { interval: 100000000 }); // 100ms 采样间隔
}
增强计算
enhance(rawLat: number, rawLng: number): number[] {
const dt: number = (now - this.lastUpdateTime) / 1000;
if (!this.gyroAvailable) {
return [rawLat, rawLng]; // 无陀螺仪时直接返回原始数据
}
const result: number[] = this.kalman.update(rawLat, rawLng, dt, this.lastGyroX, this.lastGyroY);
return result;
}
服务初始化时序
EntryAbility.onCreate()
|
+-- PreferencesService.getInstance().init(context) // 必须最先初始化
|
+-- LocationEnhanceService.getInstance().startGyroscope() // 启动传感器
|
v
Index.aboutToAppear()
|
+-- PreferencesService.getInstance().getProfile()
+-- PreferencesService.getInstance().isProfileSet()
+-- PreferencesService.getInstance().getStepCount()
+-- PreferencesService.getInstance().getTodayCalories()
+-- LocationService.getInstance().getCurrentLocation()
+-- WeatherService.getInstance().getWeather(lat, lng)
+-- StepService.getInstance().getStepCount()
服务间依赖关系
Index.ets
|-- PreferencesService (独立,被所有服务依赖)
|-- CalorieService (独立)
|-- WeatherService --> PreferencesService (缓存)
|-- StepService --> PreferencesService (步数存储)
|-- LocationService (独立)
|-- RunAdvisorService (依赖 UserProfile + WeatherInfo 参数)
|-- RouteService (独立)
|-- LocationEnhanceService (独立,需 EntryAbility 启动传感器)
PreferencesService 详细实现
存储架构
Preferences 采用 KV(键值对)存储模型,本应用使用的所有存储键如下:
| 键名 | 常量 | 值类型 | 用途 |
|---|---|---|---|
| user_profile | PROFILE_KEY | string (JSON) | 用户档案序列化数据 |
| is_profile_set | PROFILE_SET_KEY | string (‘true’/‘false’) | 是否完成首次设置 |
| diet_records | DIET_RECORD_KEY | string (JSON) | 饮食记录数组序列化 |
| step_count | STEP_COUNT_KEY | number | 今日步数 |
| step_date | STEP_DATE_KEY | string (ISO日期) | 步数对应日期 |
| weather_cache | WEATHER_CACHE_KEY | string (JSON) | 天气缓存数据 |
| weather_cache_time | WEATHER_CACHE_TIME_KEY | number | 天气缓存时间戳 |
序列化与反序列化细节
以 DietRecord 数组的序列化为例,完整流程:
saveDietRecords(records: DietRecord[]): void {
const json: string = JSON.stringify(records);
this.store.putSync(Constants.DIET_RECORD_KEY, json);
this.store.flushSync();
}
反序列化时需要逐条重建 DietRecord 实例,因为 JSON.parse 返回的是普通对象而非类实例:
getDietRecords(): DietRecord[] {
const json: string = this.store.getSync(Constants.DIET_RECORD_KEY, '') as string;
if (json === '') return [];
const arr: Object[] = JSON.parse(json) as Object[];
const result: DietRecord[] = [];
for (let i = 0; i < arr.length; i++) {
const obj: Record<string, Object> = arr[i] as Record<string, Object>;
const d: DietRecord = new DietRecord();
d.foodId = (obj['foodId'] as string) ?? '';
d.foodName = (obj['foodName'] as string) ?? '';
d.grams = (obj['grams'] as number) ?? 0;
d.calories = (obj['calories'] as number) ?? 0;
d.timestamp = (obj['timestamp'] as number) ?? 0;
d.category = (obj['category'] as string) ?? '';
result.push(d);
}
return result;
}
这种逐字段赋值的方式虽然代码量较大,但确保了类型安全。每一步都有 ?? 降级默认值,即使某字段缺失也不会导致运行时错误。
步数日期过滤详解
步数数据的特殊性在于它具有"每日归零"的语义。实现方式是同时存储步数值和对应日期:
saveStepCount(count: number): void {
this.store.putSync(Constants.STEP_COUNT_KEY, count);
this.store.putSync(Constants.STEP_DATE_KEY, new Date().toISOString().split('T')[0]);
this.store.flushSync();
}
读取时比较存储日期与当前日期,不匹配则返回 0:
getStepCount(): number {
const today: string = new Date().toISOString().split('T')[0];
const savedDate: string = this.store.getSync(Constants.STEP_DATE_KEY, '') as string;
if (savedDate !== today) return 0;
return this.store.getSync(Constants.STEP_COUNT_KEY, 0) as number;
}
toISOString().split(‘T’)[0] 生成 ‘YYYY-MM-DD’ 格式日期字符串,确保日期比较准确。注意:如果用户跨时区使用,日期判断可能偏差,但对日常使用影响极小。
CalorieService 食物数据构建
初始化策略
CalorieService 在构造器中构建全部食物数组,约 100 条 FoodItem 数据硬编码在代码中。这避免了外部数据文件的读取延迟,确保首次调用 getAllFoods() 时数据立即可用。
private constructor() {
this.foods = [];
this.foods.push(FoodItem.create('g1', '米饭', '谷物', 116, 200));
this.foods.push(FoodItem.create('g2', '馒头', '谷物', 221, 100));
// ... 约 100 条
}
每个 FoodItem 通过静态工厂方法创建,参数依次为 id、name、category、caloriesPer100g、defaultGrams。
搜索算法分析
searchFoods 使用 indexOf 实现子串匹配,搜索范围为食物名称:
searchFoods(keyword: string): FoodItem[] {
if (keyword === '') return this.foods;
const result: FoodItem[] = [];
const kw: string = keyword.toLowerCase();
for (let i = 0; i < this.foods.length; i++) {
if (this.foods[i].name.toLowerCase().indexOf(kw) >= 0) {
result.push(this.foods[i]);
}
}
return result;
}
时间复杂度为 O(n*m),n 为食物数量(约 100),m 为关键词长度。对于 100 条数据,性能完全满足实时搜索需求。toLowerCase() 确保大小写不敏感,但由于食物名称为中文,此处理论上无实际效果,保留是为了扩展兼容性(如搜索 “apple” 匹配英文名)。
分类筛选实现
getFoodsByCategory 遍历食物数组,返回匹配类别的子集:
getFoodsByCategory(category: string): FoodItem[] {
const result: FoodItem[] = [];
for (let i = 0; i < this.foods.length; i++) {
if (this.foods[i].category === category) {
result.push(this.foods[i]);
}
}
return result;
}
五种类别为:谷物、肉类、蔬菜、水果、乳制品。UI 层的分类标签选中时调用此方法,取消选中时调用 getAllFoods() 返回全部。
WeatherService 缓存机制
缓存有效性判断
WeatherService 实现了基于时间戳的缓存策略,缓存有效期为 30 分钟:
async getWeather(lat: number, lng: number): Promise<WeatherInfo> {
const prefs: PreferencesService = PreferencesService.getInstance();
const cachedTime: number = prefs.getWeatherCacheTime();
const now: number = Date.now();
if (now - cachedTime < 30 * 60 * 1000) {
const cached: WeatherInfo = prefs.getWeatherCache();
if (cached !== undefined) return cached;
}
return await this.fetchWeather(lat, lng);
}
缓存时间通过 Preferences 的 WEATHER_CACHE_TIME_KEY 存储和读取。首次启动时缓存时间为 0,条件判断必然失败,触发网络请求。成功请求后同时更新缓存数据和缓存时间。
HTTP 请求实现
private async fetchWeather(lat: number, lng: number): Promise<WeatherInfo> {
const httpObj: http.HttpClient = http.createHttp();
const url: string = `https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lng}&appid=${Constants.API_KEY}&units=metric&lang=zh_cn`;
try {
const response: http.HttpResponse = await httpObj.request(url, {
method: http.RequestMethod.GET,
connectTimeout: 10000,
readTimeout: 10000
});
if (response.responseCode === 200) {
const result: Record<string, Object> = JSON.parse(response.result as string);
const info: WeatherInfo = this.parseWeatherResponse(result);
this.cacheWeather(info);
return info;
}
} catch (e) {
hilog.error(0x0001, 'WeatherService', 'HTTP request failed');
}
return this.getDefaultWeather();
}
请求设置了 10 秒连接超时和 10 秒读取超时,防止网络异常时应用长时间无响应。任何错误(网络不可用、超时、响应码非 200、解析失败)都降级返回默认天气。
响应解析
OpenWeatherMap API 的 JSON 响应结构如下:
{
"main": {
"temp": 22,
"feels_like": 20,
"humidity": 55
},
"weather": [
{ "description": "晴", "icon": "01d" }
],
"wind": {
"speed": 3
},
"name": "Beijing"
}
解析代码通过 Record<string, Object> 逐层访问:
const main: Record<string, Object> = result['main'] as Record<string, Object>;
const weather0: Record<string, Object> = (result['weather'] as Object[])[0] as Record<string, Object>;
const wind: Record<string, Object> = result['wind'] as Record<string, Object>;
weather 是数组,取第一个元素 [0] 代表主要天气状况。icon 字段用于天气图标映射,当前应用使用 description 文字描述代替图标展示。
StepService 传感器交互
PEDOMETER 传感器
HarmonyOS 的计步器传感器返回设备启动后的累计步数(非当日步数)。因此需要结合步数存储策略:
- 首次读取传感器获取累计步数 A
- 保存 A 作为当日基准
- 后续读取传感器获取累计步数 B
- 当日步数 = B - A
但在本应用中,采用了更简单的方案:直接保存传感器读数作为当日步数,结合日期过滤实现每日归零。这是因为 Preferences 存储的步数带有日期标记,跨天自动返回 0。
传感器调用超时处理
private querySensorSteps(): Promise<number> {
return new Promise((resolve: (value: number) => void) => {
try {
sensor.once(sensor.SensorId.PEDOMETER, (data: sensor.PedometerResponse) => {
resolve(data.steps);
});
} catch (e) {
resolve(0);
}
setTimeout(() => { resolve(0); }, 3000);
});
}
sensor.once() 是一次性回调,但某些设备上可能不触发回调(传感器不可用或权限未授予)。3 秒超时确保 Promise 不会永远 pending。try-catch 捕获传感器 API 调用本身的异常(如传感器不存在)。
竞态条件处理
Promise 的 resolve 只会生效一次,即使 sensor.once 回调和 setTimeout 同时触发,第二次 resolve 调用不会覆盖结果。这是 JavaScript Promise 的标准行为,确保了传感器回调和超时回退之间的正确竞态。
LocationService 定位策略
优先级配置
const requestInfo: geoLocationManager.CurrentLocationRequest = {
priority: geoLocationManager.LocationRequestPriority.FIRST_FIX,
scenario: geoLocationManager.LocationRequestScenario.UNSET,
maxAccuracy: 0,
timeoutMs: 10000
};
FIRST_FIX 优先级表示优先获取首次定位结果,即使精度不高也尽快返回。这对天气查询场景是合理的——城市级精度即可满足需求,无需等待高精度定位。
maxAccuracy 设为 0 表示不限制精度,接受任何可用定位结果。timeoutMs 10 秒超时,与 HTTP 请求超时一致。
降级流程
getCurrentLocation()
|
+-- geoLocationManager.getCurrentLocation() 成功?
| |-- 是: 缓存坐标, 返回 [lat, lng]
| |-- 否: 记录错误日志
|
+-- 返回缓存的坐标
| |-- 有缓存: 返回上次成功定位的坐标
| |-- 无缓存: 返回北京坐标 (39.9042, 116.4074)
LocationService 维护 currentLat 和 currentLng 两个实例变量,每次成功定位后更新。这确保了即使后续定位失败,仍可使用最近一次的坐标发起天气请求。
与 LocationEnhanceService 的关系
LocationService 获取原始 GPS 坐标,LocationEnhanceService 提供增强后的坐标。当前版本中,首页的天气请求使用 LocationService 的原始坐标(天气查询对精度要求不高)。未来可扩展为:先通过 LocationService 获取粗略位置,再通过 LocationEnhanceService 精细化,用于路线导航场景。
LocationEnhanceService 数学模型
卡尔曼滤波器状态向量
4 状态向量 [x, y, vx, vy] 表示:
- x: 纬度方向位置
- y: 经度方向位置
- vx: 纬度方向速度(度/秒)
- vy: 经度方向速度(度/秒)
状态转移方程(预测步骤):
x_new = x + vx * dt
y_new = y + vy * dt
vx_new = vx
vy_new = vy
协方差矩阵手动展开
4x4 协方差矩阵 P 的 16 个元素存储为独立字段 p00 到 p33,避免使用二维数组(ArkTS 严格模式下的类型安全问题):
P = | p00 p01 p02 p03 |
| p10 p11 p12 p13 |
| p20 p21 p22 p23 |
| p30 p31 p32 p33 |
预测步骤中协方差更新:
p00_new = p00 + dt * (p02 + p20) + dt^2 * p22 + q
p01_new = p01 + dt * (p03 + p21) + dt^2 * p23
p02_new = p02 + dt * p22
...(共 16 个更新方程)
其中 q 为过程噪声参数,设为 0.001,表示对模型预测的不确定度。
更新步骤
卡尔曼增益 K 的计算:
K = P * H^T * (H * P * H^T + R)^-1
在位置观测场景中,观测矩阵 H = [1 0 0 0; 0 1 0 0](仅观测位置,不观测速度),简化后增益为:
kx = p00 / (p00 + r)
ky = p11 / (p11 + r)
状态更新:
x_new = x + kx * (z_x - x)
y_new = y + ky * (z_y - y)
其中 z_x、z_y 为 GPS 观测值,r 为观测噪声(0.01)。
陀螺仪融合
陀螺仪角速度数据作为控制输入融入预测步骤。角速度表示设备旋转方向,间接反映运动方向变化:
if (this.gyroAvailable) {
this.stateVx += this.lastGyroY * dt * 0.001;
this.stateVy -= this.lastGyroX * dt * 0.001;
}
角速度对速度的影响系数 0.001 为经验值,通过实际测试调优。x 轴角速度影响经度方向速度,y 轴角速度影响纬度方向速度,符号取反是因为陀螺仪坐标系与地理坐标系的旋转方向差异。
服务间协作场景
场景 1:应用冷启动
1. EntryAbility.onCreate()
-> PreferencesService.init(context)
-> LocationEnhanceService.startGyroscope()
2. Index.aboutToAppear()
-> PreferencesService.getProfile() // 读取用户档案
-> PreferencesService.isProfileSet() // 判断首次启动
-> PreferencesService.getStepCount() // 读取缓存步数
-> PreferencesService.getTodayCalories() // 读取今日热量
-> LocationService.getCurrentLocation() // GPS定位
-> WeatherService.getWeather(lat, lng) // 天气查询(含缓存检查)
-> StepService.getStepCount() // 传感器步数
3. 首次启动 -> 800ms延迟 -> navStack.pushPath('ProfileSetup')
非首次启动 -> 渲染首页四个卡片
场景 2:添加饮食记录
1. DietPage: 用户点击食物条目 "+"
-> selectedFood = food, showFoodDialog = true
2. FoodInputDialog: 用户输入重量,点击"添加"
-> DietRecord.create(food, grams)
-> PreferencesService.getDietRecords() // 读取所有记录
-> records.push(newRecord) // 追加新记录
-> PreferencesService.saveDietRecords(records) // 持久化
-> todayCalories = prefs.getTodayCalories() // 刷新热量统计
-> showFoodDialog = false // 关闭对话框
场景 3:查看跑步建议
1. AdvicePage: 渲染时调用 getAdvices()
-> RunAdvisorService.getAdvice(profile, weatherInfo)
-> 9步算法计算三种方案
-> 返回 RunAdvice[]
2. 用户点击"查看路线"
-> navStack.pushPath({ name: 'MapRoute', param: advice.runType })
-> MapRoutePageNav 根据 param 获取路线数据
-> RouteService.getRouteByType(type)
-> Canvas 渲染路线地图
更多推荐



所有评论(0)