服务层设计

服务总览

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 的计步器传感器返回设备启动后的累计步数(非当日步数)。因此需要结合步数存储策略:

  1. 首次读取传感器获取累计步数 A
  2. 保存 A 作为当日基准
  3. 后续读取传感器获取累计步数 B
  4. 当日步数 = 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 渲染路线地图
Logo

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

更多推荐