目前我們已經完成三個主要功能:
不過我發現,目前這些功能還是各自獨立的。
例如,如果使用者想知道今天喝了多少水、睡了幾個小時,就需要分別呼叫兩個 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<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.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.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
用來表示是否有睡眠紀錄。
接下來終於要建立今天最重要的 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
}
}
這種結構比把所有欄位都放在同一層更容易理解。
接下來需要處理:
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.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 已經有:
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 迴圈。
Repository 完成後,接下來要讓 Service 提供這些功能。
打開:
WaterService.java
新增:
public boolean hasRecordByDate(LocalDate date) {
return waterRepository.existsByDate(date);
}
之前已經有:
getTotalWaterByDate(LocalDate date)
所以不需要再重複建立。
接著打開:
SleepService.java
新增:
public double getTotalHoursByDate(LocalDate date) {
return sleepRepository.getTotalHoursByDate(date);
}
public boolean hasRecordByDate(LocalDate date) {
return sleepRepository.existsByDate(date);
}
這樣我們就可以透過 Service 取得:
指定日期的飲水總量
指定日期是否有飲水紀錄
指定日期的睡眠總時數
指定日期是否有睡眠紀錄
接下來就可以開始整合資料了!
今天最重要的部分來了!
新增:
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。
這樣的設計也比較容易維護。
接下來要讓使用者可以透過 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
這也是前幾天學習分層架構的成果。
接下來重新啟動 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。
整理如下:
| 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,但我覺得這是很重要的一步。
因為我們已經開始把不同健康資料整理成統一的格式。
例如:
{
"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,避免不必要的個人資訊傳送。
雖然今天已經成功整合健康資料,但目前仍然有一些限制。
目前所有健康紀錄都存在同一個資料庫裡。
因此健康摘要仍然只適合單人本機練習。
如果未來要讓多人使用,就需要加入使用者識別與權限控管。
雖然我們之前已經完成 BMI 計算,但目前 BMI API 是即時計算,沒有將每次計算結果保存到資料庫。
所以今天的每日摘要只整合飲水和睡眠紀錄。
未來如果要加入 BMI 歷史資料,就需要先設計相關資料表。
目前 API 回傳的是 JSON。
雖然程式可以整理數字,但還不能直接產生比較自然的文字說明。
這也是後續準備加入 AI 的原因之一。
目前的摘要只負責整理使用者輸入的紀錄。
不會直接根據飲水量或睡眠時間提供醫療診斷。
未來即使加入 AI,也需要清楚區分一般健康資訊和專業醫療建議。
今天成功讓智慧健康助理完成一個重要的功能:
每日健康摘要 API!
原本分散在不同 API 的飲水和睡眠資料,現在可以透過一個 API 一次取得。
今天學到的重點包括:
HealthSummaryService
COUNT(*) 判斷資料是否存在SUM() 計算指定日期的睡眠總時數HealthSummaryController
目前智慧健康助理已經具備:
智慧健康助理
│
├── BMI 計算與分類
│
├── 飲水量紀錄
│ ├── 新增紀錄
│ ├── 歷史查詢
│ └── 七天統計
│
├── 睡眠時間紀錄
│ ├── 新增紀錄
│ ├── 指定日期查詢
│ └── 七天統計
│
└── 每日健康摘要
├── 整合飲水量
└── 整合睡眠時間
今天最大的收穫是:
當系統開始擁有不同種類的資料時,如何整合資料,和如何儲存資料一樣重要。
而且今天也讓我更清楚地理解,為什麼前面要花時間建立 Controller、Service、Repository 和 DTO。
因為有了這些分層,今天才能直接使用已經完成的功能,而不需要把所有程式重新寫一次。
終於完成 Day 20!
距離 30 天挑戰結束,只剩下最後十天了!
前面 20 天,我們已經完成智慧健康助理的 Java 後端基礎。
從 Spring Boot、REST API、JSON、資料驗證,到 H2 Database 和健康紀錄整合,都已經逐漸完成。
但我們的專案名稱是:
Java × AI:30天打造智慧健康助理
所以接下來終於要正式進入 AI 的部分了!