康心伴Logo
康心伴WellAlly
技术

使用 Oura Ring API 构建健康仪表板:完整开发教程

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

核心要点

  • 完整数据集成: 睡眠、活动、心率、体温等 Oura Ring API 数据的完整集成
  • 实时数据更新: 使用 WebSocket 和轮询实现数据的实时更新
  • 智能数据洞察: 基于机器学习的健康建议和趋势分析
  • 响应式设计: 支持桌面、平板和移动设备的响应式 UI
  • 数据导出功能: 支持 PDF 和 Excel 格式的健康报告导出

Oura Ring 是目前市场上最先进的智能戒指设备之一,能够全天候追踪用户的睡眠、活动、心率等健康数据。本教程将指导您构建一个功能完整的健康仪表板,将 Oura Ring API 数据转化为直观、可操作的健康洞察。


项目架构

技术栈选择

前端框架: Next.js 14+ (App Router) 状态管理: Zustand 数据可视化: Recharts + D3.js UI 组件: shadcn/ui 样式: Tailwind CSS API 客户端: Axios 认证: NextAuth.js 数据库: PostgreSQL (Prisma) 缓存: Redis

系统架构图

正在渲染图表...
graph TB
    A[用户设备] --> B[Next.js 前端]
    B --> C[API 路由层]
    C --> D[认证中间件]
    D --> E[Oura Ring API]
    C --> F[数据缓存层]
    F --> G[Redis]
    C --> H[数据持久层]
    H --> I[PostgreSQL]
    B --> J[状态管理]
    J --> K[数据可视化]
    K --> L[睡眠图表]
    K --> M[活动图表]
    K --> N[心率图表]
    K --> O[健康评分]

    style B fill:#4f46e5,stroke:#333,stroke-width:2px
    style C fill:#06b6d4,stroke:#333,stroke-width:2px
    style K fill:#10b981,stroke:#333,stroke-width:2px

项目初始化

1. 创建 Next.js 项目

code
npx create-next-app@latest oura-dashboard --typescript --tailwind --app
cd oura-dashboard
Code collapsed

2. 安装依赖

code
# UI 和状态管理
npm install zustand @tanstack/react-query

# 数据可视化
npm install recharts d3 @types/d3

# UI 组件
npx shadcn-ui@latest init
npx shadcn-ui@latest add card
npx shadcn-ui@latest add button
npx shadcn-ui@latest add tabs

# API 和工具
npm install axios date-fns zod

# 认证
npm install next-auth @auth/prisma-adapter

# 数据库
npm install prisma @prisma/client
npx prisma init
Code collapsed

3. 环境配置

code
# .env.local

# Oura Ring API
OURA_CLIENT_ID=your_client_id
OURA_CLIENT_SECRET=your_client_secret
OURA_REDIRECT_URI=http://localhost:3000/api/auth/callback/oura

# 数据库
DATABASE_URL=postgresql://user:password@localhost:5432/oura_dashboard

# NextAuth
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=your_nextauth_secret

# Redis
REDIS_URL=redis://localhost:6379
Code collapsed

数据库设计

Prisma Schema

code
// prisma/schema.prisma

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id            String    @id @default(cuid())
  email         String    @unique
  name          String?
  ouraUserId    String?   @unique
  accessToken   String?   @db.Text
  refreshToken  String?   @db.Text
  tokenExpiresAt DateTime?
  createdAt     DateTime  @default(now())
  updatedAt     DateTime  @updatedAt

  sleepData     SleepData[]
  activityData  ActivityData[]
  heartRateData HeartRateData[]
}

model SleepData {
  id          String   @id @default(cuid())
  userId      String
  user        User     @relation(fields: [userId], references: [id], onDelete: Cascade)

  timestamp   DateTime
  score       Int
  duration    Int      // 分钟
  efficiency  Int
  latency     Int

  // 睡眠阶段(分钟)
  rem         Int
  deep        Int
  light       Int
  awake       Int

  // 心率数据
  hrLowest    Int?
  hrAverage   Int?
  rmssd       Int?     // HRV

  // 温度
  tempDeviation Float?

  bedTimeStart DateTime
  bedTimeEnd   DateTime

  createdAt   DateTime @default(now())

  @@unique([userId, timestamp])
  @@index([userId, timestamp])
}

model ActivityData {
  id          String   @id @default(cuid())
  userId      String
  user        User     @relation(fields: [userId], references: [id], onDelete: Cascade)

  timestamp   DateTime
  score       Int

  steps       Int
  calories    Int
  activeCalories Int

  // 活动时间(分钟)
  lowActivity    Int
  mediumActivity Int
  highActivity   Int

  nonWearTime Int
  restTime    Int
  sedentaryTime Int

  equivalentWalkingDistance Float?

  createdAt   DateTime @default(now())

  @@unique([userId, timestamp])
  @@index([userId, timestamp])
}

model HeartRateData {
  id        String   @id @default(cuid())
  userId    String
  user      User     @relation(fields: [userId], references: [id], onDelete: Cascade)

  timestamp DateTime
  heartRate Int
  source    String   // 'watch' or 'ring'

  createdAt DateTime @default(now())

  @@unique([userId, timestamp])
  @@index([userId, timestamp])
}
Code collapsed

API 集成层

Oura Ring API 客户端

code
// lib/oura-client.ts

import axios, { AxiosInstance } from 'axios';
import type { OuraDailySleep, OuraDailyActivity, OuraHeartRate } from '@/types/oura';

export class OuraClient {
  private client: AxiosInstance;
  private accessToken: string;

  constructor(accessToken: string) {
    this.accessToken = accessToken;
    this.client = axios.create({
      baseURL: 'https://api.ouraring.com/v2',
      headers: {
        'Authorization': `Bearer ${accessToken}`,
      },
    });
  }

  /**
   * 获取每日睡眠数据
   */
  async getDailySleep(startDate: Date, endDate: Date): Promise<OuraDailySleep[]> {
    const response = await this.client.get('/usercollection/daily_sleep', {
      params: {
        start_date: startDate.toISOString().split('T')[0],
        end_date: endDate.toISOString().split('T')[0],
      },
    });

    return response.data.data;
  }

  /**
   * 获取每日活动数据
   */
  async getDailyActivity(startDate: Date, endDate: Date): Promise<OuraDailyActivity[]> {
    const response = await this.client.get('/usercollection/daily_activity', {
      params: {
        start_date: startDate.toISOString().split('T')[0],
        end_date: endDate.toISOString().split('T')[0],
      },
    });

    return response.data.data;
  }

  /**
   * 获取心率数据
   */
  async getHeartRate(startDate: Date, endDate: Date): Promise<OuraHeartRate[]> {
    const response = await this.client.get('/usercollection/heartrate', {
      params: {
        start_datetime: startDate.toISOString(),
        end_datetime: endDate.toISOString(),
      },
    });

    return response.data.data;
  }

  /**
   * 获取健康摘要
   */
  async getDailySummary(date: Date) {
    const [sleep, activity] = await Promise.all([
      this.getDailySleep(date, date),
      this.getDailyActivity(date, date),
    ]);

    return {
      sleep: sleep[0] || null,
      activity: activity[0] || null,
      date: date.toISOString().split('T')[0],
    };
  }
}
Code collapsed

数据同步服务

code
// lib/data-sync.ts

import { OuraClient } from './oura-client';
import { prisma } from './db';

export class DataSyncService {
  /**
   * 同步用户的 Oura Ring 数据
   */
  static async syncUserData(userId: string, accessToken: string) {
    const client = new OuraClient(accessToken);

    // 获取最近 7 天的数据
    const endDate = new Date();
    const startDate = new Date();
    startDate.setDate(startDate.getDate() - 7);

    try {
      // 并行获取所有数据
      const [sleepData, activityData, heartRateData] = await Promise.all([
        client.getDailySleep(startDate, endDate),
        client.getDailyActivity(startDate, endDate),
        client.getHeartRate(startDate, endDate),
      ]);

      // 保存到数据库
      await this.saveSleepData(userId, sleepData);
      await this.saveActivityData(userId, activityData);
      await this.saveHeartRateData(userId, heartRateData);

      return {
        success: true,
        synced: {
          sleep: sleepData.length,
          activity: activityData.length,
          heartRate: heartRateData.length,
        },
      };
    } catch (error) {
      console.error('Data sync failed:', error);
      throw error;
    }
  }

  /**
   * 保存睡眠数据
   */
  private static async saveSleepData(userId: string, data: OuraDailySleep[]) {
    const operations = data.map((item) =>
      prisma.sleepData.upsert({
        where: {
          userId_timestamp: {
            userId,
            timestamp: new Date(item.timestamp),
          },
        },
        create: {
          userId,
          timestamp: new Date(item.timestamp),
          score: item.score,
          duration: item.duration,
          efficiency: item.efficiency,
          latency: item.latency,
          rem: item.rem,
          deep: item.deep,
          light: item.light,
          awake: item.awake,
          hrLowest: item.hr_lowest,
          hrAverage: item.hr_average,
          rmssd: item.rmssd,
          tempDeviation: item.temperature_deviation,
          bedTimeStart: new Date(item.bedtime_start),
          bedTimeEnd: new Date(item.bedtime_end),
        },
        update: {
          score: item.score,
          duration: item.duration,
          efficiency: item.efficiency,
          // ... 更新其他字段
        },
      })
    );

    await Promise.all(operations);
  }

  /**
   * 保存活动数据
   */
  private static async saveActivityData(userId: string, data: OuraDailyActivity[]) {
    // 类似睡眠数据的实现
  }

  /**
   * 保存心率数据
   */
  private static async saveHeartRateData(userId: string, data: OuraHeartRate[]) {
    // 类似实现
  }
}
Code collapsed

状态管理

Zustand Store

code
// store/use-dashboard-store.ts

import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface DashboardState {
  // 选中的日期范围
  dateRange: {
    from: Date;
    to: Date;
  };

  // 选中的数据类型
  selectedMetrics: ('sleep' | 'activity' | 'heartRate')[];

  // UI 状态
  sidebarOpen: boolean;
  isLoading: boolean;

  // Actions
  setDateRange: (from: Date, to: Date) => void;
  toggleMetric: (metric: 'sleep' | 'activity' | 'heartRate') => void;
  toggleSidebar: () => void;
  setLoading: (loading: boolean) => void;
}

export const useDashboardStore = create<DashboardState>()(
  persist(
    (set) => ({
      // 初始状态
      dateRange: {
        from: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
        to: new Date(),
      },
      selectedMetrics: ['sleep', 'activity', 'heartRate'],
      sidebarOpen: true,
      isLoading: false,

      // Actions
      setDateRange: (from, to) =>
        set({ dateRange: { from, to } }),

      toggleMetric: (metric) =>
        set((state) => ({
          selectedMetrics: state.selectedMetrics.includes(metric)
            ? state.selectedMetrics.filter((m) => m !== metric)
            : [...state.selectedMetrics, metric],
        })),

      toggleSidebar: () =>
        set((state) => ({ sidebarOpen: !state.sidebarOpen })),

      setLoading: (loading) =>
        set({ isLoading: loading }),
    }),
    {
      name: 'dashboard-storage',
    }
  )
);
Code collapsed

数据可视化组件

睡眠质量图表

code
// components/charts/SleepQualityChart.tsx

'use client';

import { useMemo } from 'react';
import { Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
import { format } from 'date-fns';
import { zhCN } from 'date-fns/locale';

interface SleepQualityChartProps {
  data: Array<{
    timestamp: Date;
    score: number;
    duration: number;
    rem: number;
    deep: number;
    light: number;
  }>;
}

export function SleepQualityChart({ data }: SleepQualityChartProps) {
  const chartData = useMemo(() => {
    return data.map((item) => ({
      date: format(item.timestamp, 'MM/dd', { locale: zhCN }),
      score: item.score,
      duration: Math.round(item.duration / 60), // 转换为小时
      rem: Math.round(item.rem),
      deep: Math.round(item.deep),
      light: Math.round(item.light),
    }));
  }, [data]);

  return (
    <ResponsiveContainer width: "100%" height={300}>
      <LineChart data={chartData}>
        <XAxis
          dataKey: "date"
          stroke: "#94a3b8"
          fontSize={12}
        />
        <YAxis
          stroke: "#94a3b8"
          fontSize={12}
          domain={[0, 100]}
        />
        <Tooltip
          contentStyle={{
            backgroundColor: 'rgba(15, 23, 42, 0.9)',
            border: '1px solid rgba(255, 255, 255, 0.1)',
            borderRadius: '8px',
          }}
        />
        <Line
          type: "monotone"
          dataKey: "score"
          stroke: "#8b5cf6"
          strokeWidth={2}
          dot={{ fill: '#8b5cf6', r: 4 }}
          activeDot={{ r: 6 }}
          name: "睡眠评分"
        />
      </LineChart>
    </ResponsiveContainer>
  );
}
Code collapsed

活动环形图

code
// components/charts/ActivityRingChart.tsx

'use client';

import { useMemo } from 'react';
import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from 'recharts';

interface ActivityRingChartProps {
  data: {
    lowActivity: number;
    mediumActivity: number;
    highActivity: number;
  };
}

const COLORS = {
  low: '#22c55e',
  medium: '#eab308',
  high: '#ef4444',
};

export function ActivityRingChart({ data }: ActivityRingChartProps) {
  const chartData = useMemo(() => {
    return [
      { name: '低强度活动', value: data.lowActivity, color: COLORS.low },
      { name: '中等强度活动', value: data.mediumActivity, color: COLORS.medium },
      { name: '高强度活动', value: data.highActivity, color: COLORS.high },
    ];
  }, [data]);

  return (
    <ResponsiveContainer width: "100%" height={250}>
      <PieChart>
        <Pie
          data={chartData}
          cx: "50%"
          cy: "50%"
          innerRadius={60}
          outerRadius={100}
          paddingAngle={5}
          dataKey: "value"
        >
          {chartData.map((entry, index) => (
            <Cell key={`cell-${index}`} fill={entry.color} />
          ))}
        </Pie>
        <Legend
          verticalAlign: "bottom"
          height={36}
          iconType: "circle"
        />
        <Tooltip
          formatter={(value: number) => `${Math.round(value)} 分钟`}
        />
      </PieChart>
    </ResponsiveContainer>
  );
}
Code collapsed

心率图表

code
// components/charts/HeartRateChart.tsx

'use client';

import { useMemo } from 'react';
import { Area, AreaChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
import { format } from 'date-fns';

interface HeartRateChartProps {
  data: Array<{
    timestamp: Date;
    heartRate: number;
  }>;
}

export function HeartRateChart({ data }: HeartRateChartProps) {
  const chartData = useMemo(() => {
    return data.map((item) => ({
      time: format(item.timestamp, 'HH:mm'),
      heartRate: item.heartRate,
    }));
  }, [data]);

  return (
    <ResponsiveContainer width: "100%" height={200}>
      <AreaChart data={chartData}>
        <XAxis
          dataKey: "time"
          stroke: "#94a3b8"
          fontSize={12}
        />
        <YAxis
          stroke: "#94a3b8"
          fontSize={12}
          domain={['dataMin - 10', 'dataMax + 10']}
        />
        <Tooltip
          contentStyle={{
            backgroundColor: 'rgba(15, 23, 42, 0.9)',
            border: '1px solid rgba(255, 255, 255, 0.1)',
            borderRadius: '8px',
          }}
          formatter={(value: number) => [`${value} bpm`, '心率']}
        />
        <Area
          type: "monotone"
          dataKey: "heartRate"
          stroke: "#f43f5e"
          fill: "#f43f5e"
          fillOpacity={0.3}
        />
      </AreaChart>
    </ResponsiveContainer>
  );
}
Code collapsed

主仪表板组件

code
// app/dashboard/page.tsx

import { auth } from '@/auth';
import { redirect } from 'next/navigation';
import { getDashboardData } from '@/lib/dashboard-data';
import { DashboardHeader } from '@/components/dashboard/DashboardHeader';
import { SleepCard } from '@/components/dashboard/SleepCard';
import { ActivityCard } from '@/components/dashboard/ActivityCard';
import { HeartRateCard } from '@/components/dashboard/HeartRateCard';
import { HealthScoreCard } from '@/components/dashboard/HealthScoreCard';

export default async function DashboardPage() {
  const session = await auth();

  if (!session?.user) {
    redirect('/login');
  }

  const dashboardData = await getDashboardData(session.user.id);

  return (
    <div className: "min-h-screen bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900">
      <DashboardHeader user={session.user} />

      <main className: "container mx-auto px-4 py-8">
        {/* 概览卡片 */}
        <div className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
          <HealthScoreCard
            title: "健康评分"
            score={dashboardData.healthScore}
            trend={dashboardData.healthScoreTrend}
          />
          <SleepCard
            score={dashboardData.sleepScore}
            duration={dashboardData.sleepDuration}
            efficiency={dashboardData.sleepEfficiency}
          />
          <ActivityCard
            score={dashboardData.activityScore}
            steps={dashboardData.steps}
            calories={dashboardData.calories}
          />
          <HeartRateCard
            resting={dashboardData.restingHeartRate}
            variability={dashboardData.hrv}
          />
        </div>

        {/* 详细图表 */}
        <div className: "grid grid-cols-1 lg:grid-cols-2 gap-6">
          <SleepChart data={dashboardData.sleepData} />
          <ActivityChart data={dashboardData.activityData} />
          <HeartRateChart data={dashboardData.heartRateData} />
          <TrendsChart data={dashboardData.trendsData} />
        </div>
      </main>
    </div>
  );
}
Code collapsed

数据导出功能

PDF 报告生成

code
// lib/export-pdf.ts

import { PDFDocument, rgb, StandardFonts } from 'pdf-lib';

export async function generateHealthReport(data: DashboardData) {
  const pdfDoc = await PDFDocument.create();
  const page = pdfDoc.addPage([600, 800]);
  const font = await pdfDoc.embedFont(StandardFonts.Helvetica);

  let yPosition = 750;

  // 标题
  page.drawText('健康报告', {
    x: 300,
    y: yPosition,
    size: 24,
    font,
    color: rgb(0, 0, 0),
    align: 'center',
  });
  yPosition -= 50;

  // 日期范围
  page.drawText(`报告期: ${data.dateRange.from} - ${data.dateRange.to}`, {
    x: 50,
    y: yPosition,
    size: 12,
    font,
  });
  yPosition -= 40;

  // 睡眠数据
  page.drawText('睡眠质量', {
    x: 50,
    y: yPosition,
    size: 16,
    font,
  });
  yPosition -= 25;
  page.drawText(`评分: ${data.sleepScore}/100`, {
    x: 70,
    y: yPosition,
    size: 12,
    font,
  });
  yPosition -= 20;
  page.drawText(`平均时长: ${data.sleepDuration} 小时`, {
    x: 70,
    y: yPosition,
    size: 12,
    font,
  });
  yPosition -= 40;

  // 活动数据
  page.drawText('活动数据', {
    x: 50,
    y: yPosition,
    size: 16,
    font,
  });
  yPosition -= 25;
  page.drawText(`评分: ${data.activityScore}/100`, {
    x: 70,
    y: yPosition,
    size: 12,
    font,
  });
  yPosition -= 20;
  page.drawText(`平均步数: ${data.steps}`, {
    x: 70,
    y: yPosition,
    size: 12,
    font,
  });

  const pdfBytes = await pdfDoc.save();
  return new Blob([pdfBytes], { type: 'application/pdf' });
}
Code collapsed

总结

本教程详细介绍了如何使用 Oura Ring API 构建功能完整的健康仪表板。通过正确的架构设计、数据可视化和用户体验优化,可以创建一个专业、可靠的健康数据平台。

关键要点:

  • 使用 Next.js App Router 实现高性能 SSR
  • Zustand 轻量级状态管理
  • Recharts 实现美观的数据可视化
  • 定期数据同步保证数据新鲜度
  • 数据导出功能提升用户体验

参考资料

#

文章标签

ouraring
仪表板
健康科技
api集成
数据可视化
typescript

觉得这篇文章有帮助?

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