iT邦幫忙

2026 iThome 鐵人賽

0
AI Engineering

Java × AI:30天打造智慧健康助理系列 第 20 篇

用 Spring Boot 建立每日健康摘要 API

  • 分享至 

  • xImage
  •  

Day 20|用 Spring Boot 建立每日健康摘要 API

目前我們已經完成三個主要功能:

  • BMI 計算與分類
  • 每日飲水量紀錄與統計
  • 睡眠時間紀錄與統計

不過我發現,目前這些功能還是各自獨立的。

例如,如果使用者想知道今天喝了多少水、睡了幾個小時,就需要分別呼叫兩個 API。

GET /health/water
GET /health/sleep?date=2026-10-17

如果未來再加入運動紀錄,可能又需要呼叫第三個 API。

這樣雖然沒有錯,但使用上好像不太方便。

所以今天決定讓智慧健康助理再升級一次!

把不同種類的健康紀錄整合成一份每日健康摘要。

今天的目標是:

使用 Spring Boot 整合不同 Service 的資料,建立可以一次查看每日飲水量與睡眠時間的健康摘要 API。


一、今天要完成什麼功能?

今天準備新增一個 API:

GET /health/summary?date=2026-10-17

使用者只需要指定日期,就能取得當天的健康紀錄。

例如:

{
  "date": "2026-10-17",
  "water": {
    "totalWater": 1500,
    "unit": "ml",
    "hasRecord": true
  },
  "sleep": {
    "totalHours": 7.5,
    "unit": "hours",
    "hasRecord": true
  }
}

這樣就不用分別查詢飲水和睡眠資料。

而且未來如果要加入運動紀錄,也可以繼續擴充這個 JSON。

例如:

{
  "date": "2026-10-17",
  "water": {
    "totalWater": 1500,
    "unit": "ml",
    "hasRecord": true
  },
  "sleep": {
    "totalHours": 7.5,
    "unit": "hours",
    "hasRecord": true
  },
  "exercise": {
    "minutes": 30,
    "unit": "minutes"
  }
}

不過今天先專注在已經完成的飲水和睡眠功能。

運動紀錄可以留到之後再加入。


二、先想清楚資料整合的方式

目前我們的系統架構大致如下:

WaterController
      ↓
WaterService
      ↓
WaterRepository
      ↓
water_records

以及:

SleepController
      ↓
SleepService
      ↓
SleepRepository
      ↓
sleep_records

如果要建立每日健康摘要,最直覺的做法可能是:

HealthSummaryController
      ↓
WaterRepository
      ↓
SleepRepository

但這樣會讓 Controller 直接負責操作資料庫。

和前幾天建立的分層概念不太一致。

所以今天決定建立一個新的 Service:

HealthSummaryService

讓它負責整合其他 Service 的資料。

整體流程就會變成:

HealthSummaryController
          ↓
HealthSummaryService
          │
          ├── WaterService
          │       ↓
          │   WaterRepository
          │       ↓
          │   water_records
          │
          └── SleepService
                  ↓
              SleepRepository
                  ↓
              sleep_records

這樣每個類別的工作就很清楚。

類別 負責工作
HealthSummaryController 接收健康摘要 API 請求
HealthSummaryService 整合不同健康紀錄
WaterService 處理飲水紀錄
SleepService 處理睡眠紀錄
Repository 查詢資料庫
DTO 定義回傳資料格式

這次不需要重新建立資料表,而是直接使用之前已經完成的功能。


三、為什麼不直接使用 Map?

之前我們曾經使用:

Map<String, Object>

建立 JSON 回傳資料。

例如:

Map<String, Object> result = new HashMap<>();

result.put("date", date);
result.put("totalWater", 1500);
result.put("totalHours", 7.5);

這樣確實可以完成今天的功能。

不過這次要整合的資料比較多,而且包含不同類型的健康紀錄。

如果全部放進同一個 Map,未來欄位越來越多時,可能比較難管理。

所以今天決定繼續使用 DTO。

我們要建立三個新的 DTO:

WaterSummaryResponse
SleepSummaryResponse
HealthSummaryResponse

其中:

WaterSummaryResponse

負責飲水摘要。

SleepSummaryResponse

負責睡眠摘要。

最後由:

HealthSummaryResponse

將兩者整合起來。


四、建立 WaterSummaryResponse DTO

首先建立:

WaterSummaryResponse.java

放在:

dto

Package 裡面。

完整程式如下:

package com.example.healthassistant.dto;

public class WaterSummaryResponse {

    private int totalWater;
    private String unit;
    private boolean hasRecord;

    public WaterSummaryResponse(
            int totalWater,
            boolean hasRecord) {

        this.totalWater = totalWater;
        this.unit = "ml";
        this.hasRecord = hasRecord;
    }

    public int getTotalWater() {
        return totalWater;
    }

    public String getUnit() {
        return unit;
    }

    public boolean isHasRecord() {
        return hasRecord;
    }
}

這個 DTO 負責回傳:

{
  "totalWater": 1500,
  "unit": "ml",
  "hasRecord": true
}

這次多了一個新的欄位:

hasRecord

它用來表示當天是否有保存飲水紀錄。

為什麼要加入這個欄位?

因為:

{
  "totalWater": 0
}

不一定代表使用者當天完全沒有喝水。

也可能只是沒有輸入紀錄。

所以我們希望可以區分:

有紀錄

以及:

沒有紀錄

這樣未來在前端顯示資料時,就比較不容易產生誤解。


五、建立 SleepSummaryResponse DTO

接下來建立:

SleepSummaryResponse.java

完整程式如下:

package com.example.healthassistant.dto;

public class SleepSummaryResponse {

    private double totalHours;
    private String unit;
    private boolean hasRecord;

    public SleepSummaryResponse(
            double totalHours,
            boolean hasRecord) {

        this.totalHours = totalHours;
        this.unit = "hours";
        this.hasRecord = hasRecord;
    }

    public double getTotalHours() {
        return totalHours;
    }

    public String getUnit() {
        return unit;
    }

    public boolean isHasRecord() {
        return hasRecord;
    }
}

這個 DTO 負責回傳:

{
  "totalHours": 7.5,
  "unit": "hours",
  "hasRecord": true
}

和飲水紀錄一樣,我們也保留:

hasRecord

用來表示是否有睡眠紀錄。


六、建立 HealthSummaryResponse DTO

接下來終於要建立今天最重要的 DTO。

檔案名稱:

HealthSummaryResponse.java

完整程式如下:

package com.example.healthassistant.dto;

import java.time.LocalDate;

public class HealthSummaryResponse {

    private LocalDate date;
    private WaterSummaryResponse water;
    private SleepSummaryResponse sleep;

    public HealthSummaryResponse(
            LocalDate date,
            WaterSummaryResponse water,
            SleepSummaryResponse sleep) {

        this.date = date;
        this.water = water;
        this.sleep = sleep;
    }

    public LocalDate getDate() {
        return date;
    }

    public WaterSummaryResponse getWater() {
        return water;
    }

    public SleepSummaryResponse getSleep() {
        return sleep;
    }
}

這裡和之前的 DTO 有一點不同。

我們不再只使用:

String
int
double

這些基本資料型別。

而是直接將其他 DTO 放進來。

例如:

private WaterSummaryResponse water;

以及:

private SleepSummaryResponse sleep;

這樣就可以建立巢狀 JSON。

也就是 JSON 裡面還有其他 JSON 物件。

例如:

{
  "date": "2026-10-17",
  "water": {
    "totalWater": 1500,
    "unit": "ml",
    "hasRecord": true
  },
  "sleep": {
    "totalHours": 7.5,
    "unit": "hours",
    "hasRecord": true
  }
}

這種結構比把所有欄位都放在同一層更容易理解。


七、先讓 WaterRepository 可以判斷有沒有紀錄

接下來需要處理:

hasRecord

這個欄位。

我們之前的:

getTotalWater(LocalDate date)

只會回傳飲水總量。

如果沒有資料,就會回傳 0。

但我們還需要知道:

資料庫裡到底有沒有這一天的紀錄?

所以打開:

WaterRepository.java

新增:

public boolean existsByDate(LocalDate date) {

    String sql = """
        SELECT COUNT(*)
        FROM water_records
        WHERE record_date = ?
        """;

    Integer count = jdbcTemplate.queryForObject(
        sql,
        Integer.class,
        date
    );

    return count != null && count > 0;
}

這裡使用了 SQL:

COUNT(*)

它可以計算符合條件的資料筆數。

例如:

2026-10-17 有三筆飲水紀錄

就會得到:

3

如果沒有紀錄,就會得到:

0

因此可以透過:

count > 0

判斷是否有資料。


八、讓 SleepRepository 也可以判斷有沒有紀錄

接著打開:

SleepRepository.java

同樣新增:

public boolean existsByDate(LocalDate date) {

    String sql = """
        SELECT COUNT(*)
        FROM sleep_records
        WHERE sleep_date = ?
        """;

    Integer count = jdbcTemplate.queryForObject(
        sql,
        Integer.class,
        date
    );

    return count != null && count > 0;
}

這樣就能確認指定日期是否有睡眠紀錄。

不過和飲水量不同的是:

同一天可能會有多筆睡眠資料。

因此今天的摘要會將同一天的睡眠時數加總。


九、在 SleepRepository 加入每日總時數查詢

目前的 SleepRepository 已經有:

findByDate()

可以查詢某一天的所有睡眠紀錄。

但今天我們希望直接取得當天的總睡眠時數。

所以再新增:

public double getTotalHoursByDate(LocalDate date) {

    String sql = """
        SELECT COALESCE(SUM(hours), 0)
        FROM sleep_records
        WHERE sleep_date = ?
        """;

    Double total = jdbcTemplate.queryForObject(
        sql,
        Double.class,
        date
    );

    return total != null ? total : 0.0;
}

這裡使用了:

SUM(hours)

將同一天的睡眠紀錄加總。

例如:

主要睡眠:7 小時
午睡:1 小時

就會得到:

8 小時

這樣就不用在 HealthSummaryService 裡面重新寫一個 for 迴圈。


十、修改 WaterService 與 SleepService

Repository 完成後,接下來要讓 Service 提供這些功能。

WaterService

打開:

WaterService.java

新增:

public boolean hasRecordByDate(LocalDate date) {

    return waterRepository.existsByDate(date);
}

之前已經有:

getTotalWaterByDate(LocalDate date)

所以不需要再重複建立。

SleepService

接著打開:

SleepService.java

新增:

public double getTotalHoursByDate(LocalDate date) {

    return sleepRepository.getTotalHoursByDate(date);
}

public boolean hasRecordByDate(LocalDate date) {

    return sleepRepository.existsByDate(date);
}

這樣我們就可以透過 Service 取得:

指定日期的飲水總量
指定日期是否有飲水紀錄
指定日期的睡眠總時數
指定日期是否有睡眠紀錄

接下來就可以開始整合資料了!


十一、建立 HealthSummaryService

今天最重要的部分來了!

新增:

HealthSummaryService.java

放在:

service

Package 裡面。

完整程式如下:

package com.example.healthassistant.service;

import com.example.healthassistant.dto.HealthSummaryResponse;
import com.example.healthassistant.dto.WaterSummaryResponse;
import com.example.healthassistant.dto.SleepSummaryResponse;

import org.springframework.stereotype.Service;

import java.time.LocalDate;

@Service
public class HealthSummaryService {

    private final WaterService waterService;
    private final SleepService sleepService;

    public HealthSummaryService(
            WaterService waterService,
            SleepService sleepService) {

        this.waterService = waterService;
        this.sleepService = sleepService;
    }

    public HealthSummaryResponse getDailySummary(LocalDate date) {

        int totalWater =
                waterService.getTotalWaterByDate(date);

        boolean hasWaterRecord =
                waterService.hasRecordByDate(date);

        double totalSleepHours =
                sleepService.getTotalHoursByDate(date);

        boolean hasSleepRecord =
                sleepService.hasRecordByDate(date);

        WaterSummaryResponse water =
                new WaterSummaryResponse(
                    totalWater,
                    hasWaterRecord
                );

        SleepSummaryResponse sleep =
                new SleepSummaryResponse(
                    totalSleepHours,
                    hasSleepRecord
                );

        return new HealthSummaryResponse(
            date,
            water,
            sleep
        );
    }
}

這裡使用了之前學過的建構子注入。

不過這次不是注入 Repository,而是注入兩個 Service:

private final WaterService waterService;

以及:

private final SleepService sleepService;

這樣 HealthSummaryService 就能整合不同功能的資料。

它不需要知道 SQL 怎麼寫,也不需要直接操作資料庫。

只需要呼叫其他 Service 已經提供的 Method。

這樣的設計也比較容易維護。


十二、建立 HealthSummaryController

接下來要讓使用者可以透過 API 取得每日健康摘要。

新增:

HealthSummaryController.java

完整程式如下:

package com.example.healthassistant.controller;

import com.example.healthassistant.dto.HealthSummaryResponse;
import com.example.healthassistant.service.HealthSummaryService;

import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDate;

@RestController
@RequestMapping("/health/summary")
public class HealthSummaryController {

    private final HealthSummaryService healthSummaryService;

    public HealthSummaryController(
            HealthSummaryService healthSummaryService) {

        this.healthSummaryService = healthSummaryService;
    }

    @GetMapping
    public ResponseEntity<HealthSummaryResponse> getSummary(
            @RequestParam
            @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
            LocalDate date) {

        HealthSummaryResponse summary =
                healthSummaryService.getDailySummary(date);

        return ResponseEntity.ok(summary);
    }
}

這次的 Controller 其實非常簡單。

因為整合資料的工作都交給:

HealthSummaryService

Controller 只需要負責:

接收日期
      ↓
呼叫 Service
      ↓
回傳 JSON

這也是前幾天學習分層架構的成果。


十三、使用 Postman 測試健康摘要 API

接下來重新啟動 Spring Boot。

在 Postman 選擇:

GET

URL 輸入:

http://localhost:8080/health/summary?date=2026-10-17

這次不需要傳送 Request Body。

假設資料庫裡面有以下資料。

飲水紀錄

日期 飲水量
2026-10-17 300 ml
2026-10-17 500 ml
2026-10-17 700 ml

當日總量:

300 + 500 + 700 = 1500 ml

睡眠紀錄

日期 睡眠時數
2026-10-17 7.0 小時
2026-10-17 0.5 小時

當日總時數:

7.0 + 0.5 = 7.5 小時

因此預期回傳:

{
  "date": "2026-10-17",
  "water": {
    "totalWater": 1500,
    "unit": "ml",
    "hasRecord": true
  },
  "sleep": {
    "totalHours": 7.5,
    "unit": "hours",
    "hasRecord": true
  }
}

這樣就成功透過一個 API,整合不同種類的健康紀錄!


十四、如果某一天沒有紀錄呢?

接下來測試另一種情況。

假設使用者查詢:

2026-10-10

但當天沒有任何飲水或睡眠紀錄。

預期回傳:

{
  "date": "2026-10-10",
  "water": {
    "totalWater": 0,
    "unit": "ml",
    "hasRecord": false
  },
  "sleep": {
    "totalHours": 0.0,
    "unit": "hours",
    "hasRecord": false
  }
}

這裡最重要的是:

"hasRecord": false

因為它代表資料庫裡沒有該日期的紀錄。

而不是直接判斷使用者當天完全沒有喝水或睡覺。

未來如果要設計前端介面,就可以根據這個欄位顯示:

當日尚無飲水紀錄

或是:

當日尚無睡眠紀錄

這樣會比單純顯示 0 更清楚。


十五、今天的 API 整理

經過今天的更新,我們已經完成不少 API。

整理如下:

HTTP Method API 功能
POST /health/bmi 計算 BMI
POST /health/water 新增飲水紀錄
GET /health/water 查詢今日飲水量
GET /health/water/date 查詢指定日期飲水量
GET /health/water/history 查詢七天飲水紀錄
GET /health/water/statistics 查詢七天飲水統計
POST /health/sleep 新增睡眠紀錄
GET /health/sleep 查詢指定日期睡眠紀錄
GET /health/sleep/statistics 查詢七天睡眠統計
GET /health/summary 查詢每日健康摘要

從最初只有:

GET /hello

到現在已經有多個健康相關 API。

這也讓我發現,Spring Boot 的分層架構真的很適合逐步擴充功能。


十六、目前的專案結構

今天新增了:

HealthSummaryController.java
HealthSummaryService.java
HealthSummaryResponse.java
WaterSummaryResponse.java
SleepSummaryResponse.java

目前的專案結構如下:

health-assistant
│
├── src/main/java/com/example/healthassistant
│   │
│   ├── HealthAssistantApplication.java
│   │
│   ├── controller
│   │   ├── HealthController.java
│   │   ├── WaterController.java
│   │   ├── SleepController.java
│   │   └── HealthSummaryController.java
│   │
│   ├── service
│   │   ├── BmiService.java
│   │   ├── WaterService.java
│   │   ├── SleepService.java
│   │   └── HealthSummaryService.java
│   │
│   ├── repository
│   │   ├── WaterRepository.java
│   │   └── SleepRepository.java
│   │
│   ├── dto
│   │   ├── HealthRequest.java
│   │   ├── WaterRequest.java
│   │   ├── WaterHistoryResponse.java
│   │   ├── WaterStatisticsResponse.java
│   │   ├── SleepRequest.java
│   │   ├── SleepResponse.java
│   │   ├── SleepHistoryResponse.java
│   │   ├── SleepStatisticsResponse.java
│   │   ├── WaterSummaryResponse.java
│   │   ├── SleepSummaryResponse.java
│   │   └── HealthSummaryResponse.java
│   │
│   └── exception
│       └── GlobalExceptionHandler.java
│
├── src/main/resources
│   ├── application.properties
│   └── schema.sql
│
└── pom.xml

雖然檔案數量越來越多,但每個 Class 都有比較明確的責任。

這樣未來新增功能時,就不用把所有程式都擠在同一個檔案裡。


十七、為什麼今天的功能對 AI 很重要?

今天雖然還沒有正式串接 AI,但我覺得這是很重要的一步。

因為我們已經開始把不同健康資料整理成統一的格式。

例如:

{
  "date": "2026-10-17",
  "water": {
    "totalWater": 1500,
    "unit": "ml",
    "hasRecord": true
  },
  "sleep": {
    "totalHours": 7.5,
    "unit": "hours",
    "hasRecord": true
  }
}

這樣未來如果要讓 AI 產生每日健康摘要,就可以將整理好的資料提供給 AI。

例如希望 AI 協助產生:

今天有記錄 1500 ml 的飲水量,
以及 7.5 小時的睡眠時間。

而不是讓 AI 自己去資料庫查詢,或重新計算所有數字。

這樣的分工比較清楚:

Spring Boot
    ↓
查詢資料庫
    ↓
Java 整理數據
    ↓
產生健康摘要 JSON
    ↓
AI 整理成自然語言

而且之後也可以控制哪些資料適合提供給 AI,避免不必要的個人資訊傳送。


十八、今天的功能還有哪些限制?

雖然今天已經成功整合健康資料,但目前仍然有一些限制。

1. 還沒有使用者帳號

目前所有健康紀錄都存在同一個資料庫裡。

因此健康摘要仍然只適合單人本機練習。

如果未來要讓多人使用,就需要加入使用者識別與權限控管。

2. BMI 尚未納入每日摘要

雖然我們之前已經完成 BMI 計算,但目前 BMI API 是即時計算,沒有將每次計算結果保存到資料庫。

所以今天的每日摘要只整合飲水和睡眠紀錄。

未來如果要加入 BMI 歷史資料,就需要先設計相關資料表。

3. 還沒有文字摘要功能

目前 API 回傳的是 JSON。

雖然程式可以整理數字,但還不能直接產生比較自然的文字說明。

這也是後續準備加入 AI 的原因之一。

4. 不會直接判斷健康狀況

目前的摘要只負責整理使用者輸入的紀錄。

不會直接根據飲水量或睡眠時間提供醫療診斷。

未來即使加入 AI,也需要清楚區分一般健康資訊和專業醫療建議。


Day 20 小結

今天成功讓智慧健康助理完成一個重要的功能:

每日健康摘要 API!

原本分散在不同 API 的飲水和睡眠資料,現在可以透過一個 API 一次取得。

今天學到的重點包括:

  1. 整合不同 Service 的資料
  2. 建立 HealthSummaryService
  3. 建立巢狀 DTO
  4. 使用 COUNT(*) 判斷資料是否存在
  5. 使用 SUM() 計算指定日期的睡眠總時數
  6. 建立 HealthSummaryController
  7. 使用 Postman 測試整合後的 JSON

目前智慧健康助理已經具備:

智慧健康助理
│
├── BMI 計算與分類
│
├── 飲水量紀錄
│   ├── 新增紀錄
│   ├── 歷史查詢
│   └── 七天統計
│
├── 睡眠時間紀錄
│   ├── 新增紀錄
│   ├── 指定日期查詢
│   └── 七天統計
│
└── 每日健康摘要
    ├── 整合飲水量
    └── 整合睡眠時間

今天最大的收穫是:

當系統開始擁有不同種類的資料時,如何整合資料,和如何儲存資料一樣重要。

而且今天也讓我更清楚地理解,為什麼前面要花時間建立 Controller、Service、Repository 和 DTO。

因為有了這些分層,今天才能直接使用已經完成的功能,而不需要把所有程式重新寫一次。

終於完成 Day 20!

距離 30 天挑戰結束,只剩下最後十天了!


明日預告

Day 21|終於要加入 AI!先認識生成式 AI API 與 Prompt

前面 20 天,我們已經完成智慧健康助理的 Java 後端基礎。

從 Spring Boot、REST API、JSON、資料驗證,到 H2 Database 和健康紀錄整合,都已經逐漸完成。

但我們的專案名稱是:

Java × AI:30天打造智慧健康助理

所以接下來終於要正式進入 AI 的部分了!


上一篇
用 Java 建立七天睡眠統計 API
下一篇
認識生成式 AI API 與 Prompt
系列文
Java × AI:30天打造智慧健康助理 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言