康心伴Logo
康心伴WellAlly
开发

构建可穿戴设备聚合器:使用 Next.js BFF 模式整合健康数据

全面指南如何使用 Next.js 构建 Backend For Frontend (BFF) 架构,安全地整合来自 Apple HealthKit、Google Fit 和 Oura Ring 等多个可穿戴设备的健康数据。包含身份验证、数据标准化和错误处理。

W
WellAlly 开发团队
2026-03-08
15 分钟阅读

关键要点

  • BFF 模式简化前端数据获取:通过为前端应用量身定制的 API 层,减少客户端复杂度,统一处理多个第三方 API 的差异。
  • 身份验证与授权分离:可穿戴设备 API 需要不同的认证流程(OAuth2、API Key、JWT),BFF 层统一管理这些凭证,避免在前端暴露敏感信息。
  • 数据标准化至关重要:不同平台返回的健康数据格式各异(步数可能是 steps、count 或 value),建立统一的数据模型可提高代码可维护性。
  • 缓存策略降低 API 限流风险:大多数可穿戴设备 API 有严格的速率限制,使用 Redis 或内存缓存可减少对外部 API 的调用次数。
  • 错误处理需要细致设计:网络超时、API 限流、用户撤销授权等场景需要优雅处理,确保应用部分功能可用时不受影响。

在当今数字健康领域,用户经常使用多种可穿戴设备——Apple Watch 追踪步数、Oura Ring 监测睡眠、Fitbit 记录心率。对于开发者来说,这意味着需要整合来自不同厂商的 API,每个都有独特的认证方式、数据格式和速率限制。

本教程将指导你使用 Next.js 构建一个 Backend For Frontend (BFF) 服务,统一聚合这些数据源。我们将重点关注架构设计、安全性、数据标准化和错误处理。

前置条件:

  • Node.js (LTS 版本) 和 npm/yarn 已安装
  • 基本的 TypeScript 和 Next.js 知识
  • 了解 RESTful API 和 OAuth2 概念
  • 可选:拥有可穿戴设备开发者账号(Apple Developer、Google Cloud Console)

理解 BFF 架构模式

Backend For Frontend 是一种服务端模式,为特定的前端应用创建专用的 API 层。

为什么选择 BFF?

  1. 聚合多个数据源:可穿戴设备数据分散在不同平台,BFF 可以统一调用并组合结果
  2. 处理认证复杂性:将 OAuth 回调、令牌刷新等逻辑放在服务端,避免前端暴露敏感凭证
  3. 数据转换与标准化:将不同厂商的响应格式转换为统一的前端数据结构
  4. 性能优化:通过缓存、批量请求和并行处理减少延迟
  5. 安全性:API 密钥和刷新令牌安全存储在服务端环境变量中

架构概览

code
前端应用 (React/Next.js)
    ↓
BFF 层 (Next.js API Routes)
    ↓
┌─────────────┬──────────────┬──────────────┐
│Apple HealthKit│ Google Fit API│ Oura API    │
└─────────────┴──────────────┴──────────────┘
Code collapsed

项目初始化与设置

步骤 1:创建 Next.js 项目

code
npx create-next-app@latest wearable-aggregator --typescript --tailwind --app
cd wearable-aggregator
npm install axios redis ioredis zod
Code collapsed

依赖说明:

  • axios:HTTP 客户端,用于调用外部 API
  • redis / ioredis:Redis 客户端,用于缓存
  • zod:运行时类型验证,确保数据结构正确

步骤 2:配置环境变量

创建 .env.local 文件:

code
# Redis 配置
REDIS_URL=redis://localhost:6379

# Apple HealthKit 配置(需要后端服务,这里假设已有 HealthKit 数据同步服务)
APPLE_HEALTHKIT_BASE_URL=https://your-healthkit-service.com/api
APPLE_HEALTHKIT_API_KEY=your_api_key_here

# Google Fit API 配置
GOOGLE_FIT_CLIENT_ID=your_client_id
GOOGLE_FIT_CLIENT_SECRET=your_client_secret
GOOGLE_FIT_REDIRECT_URI=http://localhost:3000/api/auth/callback/google

# Oura Ring API 配置
OURA_CLIENT_ID=your_oura_client_id
OURA_CLIENT_SECRET=your_oura_client_secret
OURA_REDIRECT_URI=http://localhost:3000/api/auth/callback/oura

# JWT 密钥(用于生成前端访问令牌)
JWT_SECRET=your_super_secret_jwt_key
Code collapsed

定义统一数据模型

创建类型定义文件 types/health-data.ts

code
// types/health-data.ts

export interface UnifiedHealthData {
  userId: string;
  date: string; // ISO 8601 格式
  steps?: number;
  activeMinutes?: number;
  heartRate?: {
    resting?: number;
    average?: number;
    max?: number;
  };
  sleep?: {
    duration: number; // 分钟
    efficiency?: number;
    stages?: {
      deep?: number;
      light?: number;
      rem?: number;
      awake?: number;
    };
  };
  sources: string[]; // 数据来源:['apple-healthkit', 'google-fit', 'oura']
}

export interface WearableAuthConfig {
  accessToken: string;
  refreshToken?: string;
  expiresAt?: number;
}

export interface AggregationOptions {
  date: string;
  userId: string;
  sources?: ('apple-healthkit' | 'google-fit' | 'oura')[];
}
Code collapsed

实现 Redis 缓存层

创建 lib/cache.ts

code
// lib/cache.ts
import Redis from 'ioredis';

const redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379');

export const CACHE_TTL = {
  SHORT: 300, // 5 分钟
  MEDIUM: 1800, // 30 分钟
  LONG: 3600, // 1 小时
  VERY_LONG: 86400, // 24 小时
};

export async function getCache<T>(key: string): Promise<T | null> {
  try {
    const cached = await redis.get(key);
    return cached ? JSON.parse(cached) : null;
  } catch (error) {
    console.error('Cache get error:', error);
    return null;
  }
}

export async function setCache<T>(
  key: string,
  value: T,
  ttl: number = CACHE_TTL.MEDIUM
): Promise<void> {
  try {
    await redis.setex(key, ttl, JSON.stringify(value));
  } catch (error) {
    console.error('Cache set error:', error);
  }
}

export async function deleteCache(key: string): Promise<void> {
  try {
    await redis.del(key);
  } catch (error) {
    console.error('Cache delete error:', error);
  }
}

export function generateCacheKey(
  prefix: string,
  userId: string,
  date: string,
  source?: string
): string {
  return source
    ? `${prefix}:${userId}:${date}:${source}`
    : `${prefix}:${userId}:${date}`;
}
Code collapsed

实现数据源适配器

Apple HealthKit 适配器

创建 lib/adapters/apple-healthkit.ts

code
// lib/adapters/apple-healthkit.ts
import axios from 'axios';
import { UnifiedHealthData } from '@/types/health-data';

const APPLE_BASE_URL = process.env.APPLE_HEALTHKIT_BASE_URL;
const APPLE_API_KEY = process.env.APPLE_HEALTHKIT_API_KEY;

const api = axios.create({
  baseURL: APPLE_BASE_URL,
  headers: {
    'Authorization': `Bearer ${APPLE_API_KEY}`,
    'Content-Type': 'application/json',
  },
});

export async function fetchAppleHealthData(
  userId: string,
  date: string
): Promise<Partial<UnifiedHealthData>> {
  try {
    const [stepsRes, heartRateRes, sleepRes] = await Promise.all([
      api.get(`/users/${userId}/steps`, { params: { date } }),
      api.get(`/users/${userId}/heart-rate`, { params: { date } }),
      api.get(`/users/${userId}/sleep`, { params: { date } }),
    ]);

    return {
      steps: stepsRes.data.totalSteps || 0,
      activeMinutes: stepsRes.data.activeMinutes || 0,
      heartRate: {
        resting: heartRateRes.data.resting,
        average: heartRateRes.data.average,
        max: heartRateRes.data.max,
      },
      sleep: sleepRes.data ? {
        duration: sleepRes.data.durationMinutes || 0,
        efficiency: sleepRes.data.efficiency,
      } : undefined,
    };
  } catch (error) {
    if (axios.isAxiosError(error)) {
      if (error.response?.status === 401) {
        throw new Error('Apple HealthKit authentication failed');
      }
      if (error.response?.status === 429) {
        throw new Error('Apple HealthKit rate limit exceeded');
      }
    }
    console.error('Apple HealthKit fetch error:', error);
    return {}; // 失败时返回空对象,让聚合器继续
  }
}
Code collapsed

Google Fit 适配器

创建 lib/adapters/google-fit.ts

code
// lib/adapters/google-fit.ts
import axios from 'axios';
import { UnifiedHealthData } from '@/types/health-data';

const GOOGLE_FIT_BASE_URL = 'https://www.googleapis.com/fitness/v1/users/me';

// 从数据库或缓存获取用户的访问令牌
async function getGoogleAccessToken(userId: string): Promise<string> {
  // 实际实现中,这里应该从数据库查询
  // const user = await db.users.findById(userId);
  // return user.googleFitAccessToken;
  throw new Error('Token retrieval not implemented');
}

export async function fetchGoogleFitData(
  userId: string,
  date: string
): Promise<Partial<UnifiedHealthData>> {
  try {
    const accessToken = await getGoogleAccessToken(userId);
    const startTime = new Date(date).toISOString();
    const endTime = new Date(new Date(date).setHours(23, 59, 59)).toISOString();

    // 获取步数数据
    const stepsRes = await axios.post(
      `${GOOGLE_FIT_BASE_URL}/dataset:aggregate`,
      {
        aggregateBy: [{
          dataTypeName: 'com.google.step_count.delta',
          dataSourceId: 'derived:com.google.step_count.delta:com.google.android.gms:estimated_steps',
        }],
        bucketByTime: { durationMillis: 86400000 }, // 1天
        startTimeMillis: new Date(startTime).getTime(),
        endTimeMillis: new Date(endTime).getTime(),
      },
      {
        headers: { Authorization: `Bearer ${accessToken}` },
      }
    );

    const steps = stepsRes.data.bucket?.[0]?.dataset?.[0]?.point?.[0]?.value?.[0]?.intVal || 0;

    return {
      steps,
      activeMinutes: Math.round(steps / 100), // 估算
    };
  } catch (error) {
    if (axios.isAxiosError(error)) {
      if (error.response?.status === 401) {
        throw new Error('Google Fit token expired');
      }
    }
    console.error('Google Fit fetch error:', error);
    return {};
  }
}
Code collapsed

Oura Ring 适配器

创建 lib/adapters/oura.ts

code
// lib/adapters/oura.ts
import axios from 'axios';
import { UnifiedHealthData } from '@/types/health-data';

const OURA_BASE_URL = 'https://api.ouraring.com/v2';

async function getOuraAccessToken(userId: string): Promise<string> {
  // 从数据库获取用户令牌
  throw new Error('Token retrieval not implemented');
}

export async function fetchOuraData(
  userId: string,
  date: string
): Promise<Partial<UnifiedHealthData>> {
  try {
    const accessToken = await getOuraAccessToken(userId);

    // 获取每日活动数据
    const [activityRes, sleepRes, heartRateRes] = await Promise.all([
      axios.get(`${OURA_BASE_URL}/user/dailyactivity`, {
        headers: { Authorization: `Bearer ${accessToken}` },
        params: { start_date: date, end_date: date },
      }),
      axios.get(`${OURA_BASE_URL}/user/daily_sleep`, {
        headers: { Authorization: `Bearer ${accessToken}` },
        params: { start_date: date, end_date: date },
      }),
      axios.get(`${OURA_BASE_URL}/user/heartrate`, {
        headers: { Authorization: `Bearer ${accessToken}` },
        params: { start_date: date, end_date: date },
      }),
    ]);

    const activity = activityRes.data.data?.[0] || {};
    const sleep = sleepRes.data.data?.[0] || {};

    return {
      steps: activity.steps,
      activeMinutes: activity.active_calories ? Math.round(activity.active_calories / 7) : undefined,
      heartRate: {
        resting: activity.resting_hrv,
        average: heartRateRes.data.data?.average_heart_rate,
      },
      sleep: sleep.total_sleep_duration ? {
        duration: Math.round(sleep.total_sleep_duration / 60), // 转换为分钟
        efficiency: sleep.sleep_efficiency,
        stages: {
          deep: sleep.rem_sleep_duration ? Math.round(sleep.deep_sleep_duration / 60) : undefined,
          light: sleep.light_sleep_duration ? Math.round(sleep.light_sleep_duration / 60) : undefined,
          rem: sleep.rem_sleep_duration ? Math.round(sleep.rem_sleep_duration / 60) : undefined,
          awake: sleep.awake_time ? Math.round(sleep.awake_time / 60) : undefined,
        },
      } : undefined,
    };
  } catch (error) {
    console.error('Oura API fetch error:', error);
    return {};
  }
}
Code collapsed

实现 BFF 聚合 API

创建 app/api/health-data/route.ts

code
// app/api/health-data/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { fetchAppleHealthData } from '@/lib/adapters/apple-healthkit';
import { fetchGoogleFitData } from '@/lib/adapters/google-fit';
import { fetchOuraData } from '@/lib/adapters/oura';
import { getCache, setCache, generateCacheKey, CACHE_TTL } from '@/lib/cache';
import { UnifiedHealthData } from '@/types/health-data';

// 验证请求参数
const querySchema = z.object({
  date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
  userId: z.string().min(1),
  sources: z.string().optional(), // 逗号分隔:'apple-healthkit,google-fit'
});

export async function GET(request: NextRequest) {
  try {
    // 1. 解析和验证请求参数
    const { searchParams } = new URL(request.url);
    const query = querySchema.parse({
      date: searchParams.get('date'),
      userId: searchParams.get('userId'),
      sources: searchParams.get('sources'),
    });

    const sources = query.sources
      ? (query.sources.split(',') as Array<'apple-healthkit' | 'google-fit' | 'oura'>)
      : ['apple-healthkit', 'google-fit', 'oura'];

    // 2. 检查缓存
    const cacheKey = generateCacheKey('health-data', query.userId, query.date);
    const cached = await getCache<UnifiedHealthData>(cacheKey);
    if (cached) {
      return NextResponse.json(cached);
    }

    // 3. 并行获取所有数据源
    const fetchPromises = sources.map(async (source) => {
      try {
        switch (source) {
          case 'apple-healthkit':
            return fetchAppleHealthData(query.userId, query.date);
          case 'google-fit':
            return fetchGoogleFitData(query.userId, query.date);
          case 'oura':
            return fetchOuraData(query.userId, query.date);
          default:
            return {};
        }
      } catch (error) {
        console.error(`Error fetching from ${source}:`, error);
        return {}; // 失败的数据源返回空对象
      }
    });

    const results = await Promise.all(fetchPromises);

    // 4. 聚合数据(取最完整的值)
    const aggregated: UnifiedHealthData = {
      userId: query.userId,
      date: query.date,
      steps: Math.max(...results.map(r => r.steps || 0)),
      activeMinutes: Math.max(...results.map(r => r.activeMinutes || 0)),
      heartRate: results.reduce((best, current) => ({
        resting: Math.max(best.resting || 0, current.heartRate?.resting || 0) || undefined,
        average: Math.max(best.average || 0, current.heartRate?.average || 0) || undefined,
        max: Math.max(best.max || 0, current.heartRate?.max || 0) || undefined,
      }), {}).resting ? results.reduce((best, current) => ({
        resting: Math.max(best.resting || 0, current.heartRate?.resting || 0) || undefined,
        average: Math.max(best.average || 0, current.heartRate?.average || 0) || undefined,
        max: Math.max(best.max || 0, current.heartRate?.max || 0) || undefined,
      }), {}) : undefined,
      sleep: results.reduce((best, current) => {
        if (!current.sleep) return best;
        if (!best) return current.sleep;
        // 选择数据更完整的睡眠记录
        return current.sleep.duration > best.duration ? current.sleep : best;
      }, undefined as UnifiedHealthData['sleep']),
      sources: results.filter(r => Object.keys(r).length > 0)
        .map((_, i) => sources[i]),
    };

    // 5. 缓存结果
    await setCache(cacheKey, aggregated, CACHE_TTL.MEDIUM);

    return NextResponse.json(aggregated);

  } catch (error) {
    if (error instanceof z.ZodError) {
      return NextResponse.json(
        { error: 'Invalid request parameters', details: error.errors },
        { status: 400 }
      );
    }

    console.error('Health data aggregation error:', error);
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    );
  }
}
Code collapsed

实现 OAuth 回调处理

Google Fit OAuth 回调

创建 app/api/auth/callback/google/route.ts

code
// app/api/auth/callback/google/route.ts
import { NextRequest, NextResponse } from 'next/server';
import axios from 'axios';

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const code = searchParams.get('code');
  const state = searchParams.get('state'); // 用户ID

  if (!code) {
    return NextResponse.redirect(new URL('/auth/error?no_code', request.url));
  }

  try {
    // 交换授权码获取访问令牌
    const tokenRes = await axios.post('https://oauth2.googleapis.com/token', {
      code,
      client_id: process.env.GOOGLE_FIT_CLIENT_ID,
      client_secret: process.env.GOOGLE_FIT_CLIENT_SECRET,
      redirect_uri: process.env.GOOGLE_FIT_REDIRECT_URI,
      grant_type: 'authorization_code',
    });

    const { access_token, refresh_token, expires_in } = tokenRes.data;

    // 保存令牌到数据库(这里简化处理)
    // await db.users.update(state, { googleFitTokens: { access_token, refresh_token, expiresAt: Date.now() + expires_in * 1000 } });

    return NextResponse.redirect(new URL('/dashboard?connected=google-fit', request.url));
  } catch (error) {
    console.error('Google OAuth error:', error);
    return NextResponse.redirect(new URL('/auth/error?google_failed', request.url));
  }
}
Code collapsed

前端集成示例

创建客户端组件 components/HealthDashboard.tsx

code
// components/HealthDashboard.tsx
'use client';

import { useQuery } from '@tanstack/react-query';

interface HealthDataProps {
  userId: string;
  date: string;
}

export function HealthDashboard({ userId, date }: HealthDataProps) {
  const { data, isLoading, error } = useQuery({
    queryKey: ['health-data', userId, date],
    queryFn: async () => {
      const res = await fetch(`/api/health-data?userId=${userId}&date=${date}`);
      if (!res.ok) throw new Error('Failed to fetch health data');
      return res.json();
    },
    staleTime: 30 * 60 * 1000, // 30 分钟
  });

  if (isLoading) return <div>加载中...</div>;
  if (error) return <div>加载失败: {error.message}</div>;

  return (
    <div className: "grid grid-cols-1 md:grid-cols-3 gap-6">
      <MetricCard
        title: "步数"
        value={data?.steps || 0}
        icon: "👣"
        sources={data?.sources}
      />
      <MetricCard
        title: "活动时间"
        value={`${data?.activeMinutes || 0} 分钟`}
        icon: "⏱️"
      />
      <MetricCard
        title: "静息心率"
        value={`${data?.heartRate?.resting || '-'} bpm`}
        icon: "❤️"
      />
      <div className: "md:col-span-3">
        <SleepCard sleep={data?.sleep} />
      </div>
    </div>
  );
}

function MetricCard({ title, value, icon, sources }: any) {
  return (
    <div className: "bg-white rounded-lg shadow p-6">
      <div className: "flex items-center justify-between">
        <div>
          <p className: "text-gray-500 text-sm">{title}</p>
          <p className: "text-3xl font-bold mt-2">{value}</p>
        </div>
        <span className: "text-4xl">{icon}</span>
      </div>
      {sources && (
        <p className: "text-xs text-gray-400 mt-4">
          来源: {sources.join(', ')}
        </p>
      )}
    </div>
  );
}

function SleepCard({ sleep }: any) {
  if (!sleep) return <div className: "text-gray-500">暂无睡眠数据</div>;

  return (
    <div className: "bg-white rounded-lg shadow p-6">
      <h3 className: "text-lg font-semibold mb-4">睡眠分析</h3>
      <div className: "grid grid-cols-2 md:grid-cols-4 gap-4">
        <div>
          <p className: "text-sm text-gray-500">总时长</p>
          <p className: "text-xl font-bold">{sleep.duration} 分钟</p>
        </div>
        <div>
          <p className: "text-sm text-gray-500">深睡</p>
          <p className: "text-xl font-bold">{sleep.stages?.deep || '-'} 分钟</p>
        </div>
        <div>
          <p className: "text-sm text-gray-500">浅睡</p>
          <p className: "text-xl font-bold">{sleep.stages?.light || '-'} 分钟</p>
        </div>
        <div>
          <p className: "text-sm text-gray-500">REM</p>
          <p className: "text-xl font-bold">{sleep.stages?.rem || '-'} 分钟</p>
        </div>
      </div>
    </div>
  );
}
Code collapsed

安全性最佳实践

1. 令牌管理

code
// lib/token-manager.ts
import jwt from 'jsonwebtoken';

export function generateFrontendToken(userId: string): string {
  return jwt.sign(
    { userId },
    process.env.JWT_SECRET!,
    { expiresIn: '1h' }
  );
}

export function verifyFrontendToken(token: string): { userId: string } | null {
  try {
    return jwt.verify(token, process.env.JWT_SECRET!) as { userId: string };
  } catch {
    return null;
  }
}
Code collapsed

2. 速率限制

code
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { getCache, setCache } from './lib/cache';

const RATE_LIMIT = 100; // 每小时100次请求

export async function middleware(request: NextRequest) {
  const userId = request.headers.get('x-user-id');
  if (!userId) return NextResponse.next();

  const key = `ratelimit:${userId}:${Math.floor(Date.now() / 3600000)}`;
  const current = await getCache<number>(key) || 0;

  if (current >= RATE_LIMIT) {
    return NextResponse.json(
      { error: 'Rate limit exceeded' },
      { status: 429 }
    );
  }

  await setCache(key, current + 1, 3600);

  return NextResponse.next();
}
Code collapsed

测试策略

创建单元测试 __tests__/aggregation.test.ts

code
import { describe, it, expect, vi } from 'vitest';
import { GET } from '../app/api/health-data/route';
import { fetchAppleHealthData } from '../lib/adapters/apple-healthkit';

vi.mock('../lib/adapters/apple-healthkit');
vi.mock('../lib/adapters/google-fit');
vi.mock('../lib/adapters/oura');

describe('Health Data Aggregation', () => {
  it('should aggregate data from multiple sources', async () => {
    vi.mocked(fetchAppleHealthData).mockResolvedValue({
      steps: 8000,
      activeMinutes: 45,
    });

    const request = new Request(
      'http://localhost:3000/api/health-data?userId=test-user&date=2026-03-08'
    );

    const response = await GET(request as any);
    const data = await response.json();

    expect(data.steps).toBeGreaterThan(0);
    expect(data.sources).toContain('apple-healthkit');
  });

  it('should handle partial failures gracefully', async () => {
    vi.mocked(fetchAppleHealthData).mockRejectedValue(new Error('API error'));

    const request = new Request(
      'http://localhost:3000/api/health-data?userId=test-user&date=2026-03-08'
    );

    const response = await GET(request as any);

    // 应该返回200,即使某个数据源失败
    expect(response.status).toBe(200);
  });
});
Code collapsed

部署建议

Docker Compose 配置

code
# docker-compose.yml
version: '3.8'

services:
  nextjs:
    build: .
    ports:
      - "3000:3000"
    environment:
      - REDIS_URL=redis://redis:6379
    depends_on:
      - redis

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data

volumes:
  redis_data:
Code collapsed

总结

使用 Next.js 构建 BFF 层来聚合可穿戴设备数据,可以显著简化前端复杂度,同时提供更好的性能和安全性。通过本教程,你已经学会了:

  1. 设计统一的数据模型来抽象不同 API 的差异
  2. 使用缓存减少外部 API 调用
  3. 实现错误处理和部分失败容错
  4. 安全地管理 OAuth 令牌
  5. 使用 Zod 进行运行时验证

下一步

  • 添加实时数据更新功能(WebSocket 或 Server-Sent Events)
  • 实现数据导出功能(PDF、CSV)
  • 添加健康数据趋势分析和可视化
  • 集成更多可穿戴设备(Fitbit、Garmin 等)

参考资料

常见问题

Q: 如何处理用户撤销授权后的令牌刷新?

A: 实现一个令牌刷新服务,定期检查令牌过期时间并使用 refresh_token 获取新的 access_token。如果用户撤销授权,需要清除本地令牌并提示用户重新连接。

Q: 如何确保数据在传输过程中的安全性?

A: 始终使用 HTTPS 连接,对所有 API 端点实施身份验证,敏感数据(如健康记录)应考虑额外的加密层。

Q: 缓存策略如何选择?

A: 根据数据更新频率选择 TTL。历史数据可以使用较长的缓存时间(24小时),而今日数据应该使用较短的缓存(5-15分钟)。

相关文章

#

文章标签

nextjs
bff
healthtech
api
typescript

觉得这篇文章有帮助?

立即体验康心伴,开始您的健康管理之旅