昨天的 Day 17,我們成功讓智慧健康助理加入七天飲水量統計功能!
現在系統不只可以新增飲水紀錄,還能查詢歷史資料、計算平均值,以及找出最高和最低的紀錄量。
不過仔細想想,既然我們的目標是打造「智慧健康助理」,好像不能一直只圍繞著飲水量打轉 XD
所以今天決定挑戰第二種日常健康紀錄:
睡眠時間紀錄!
這次要讓使用者可以輸入每天睡了幾個小時,並將資料保存到 H2 Database。
今天的目標是:
使用 Spring Boot、DTO、Repository 和 H2 Database,建立睡眠時間紀錄與查詢 API。
在一開始規劃智慧健康助理時,我希望系統可以逐漸整合不同種類的健康資訊。
目前已經完成:
接下來加入睡眠紀錄,就能讓系統開始管理第二種日常健康資料。
例如:
10/15 → 睡眠 7.5 小時
10/16 → 睡眠 6 小時
10/17 → 睡眠 8 小時
未來就可以進一步分析:
最近七天平均睡眠時間
甚至結合飲水紀錄,產生簡單的生活習慣摘要。
不過今天先不急著做統計,也不會讓 AI 分析睡眠品質。
先完成資料的接收、驗證、儲存與查詢。
今天準備新增兩個 API:
| HTTP Method | API | 功能 |
|---|---|---|
| POST | /health/sleep |
新增睡眠紀錄 |
| GET | /health/sleep |
查詢指定日期的睡眠紀錄 |
例如使用者傳送:
{
"sleepDate": "2026-10-17",
"hours": 7.5
}
系統就會將這筆資料保存到資料庫。
成功後預期回傳:
{
"message": "Sleep record saved successfully.",
"id": 1,
"sleepDate": "2026-10-17",
"hours": 7.5
}
之後如果想查詢某一天的睡眠紀錄,就可以使用:
GET /health/sleep?date=2026-10-17
這樣就不用每次都進入 H2 Console 手動查詢資料了。
在寫程式之前,我們先想一下睡眠紀錄需要哪些欄位。
目前先使用最簡單的設計:
| 欄位 | 資料型別 | 說明 |
|---|---|---|
| id | BIGINT | 每筆紀錄的編號 |
| sleep_date | DATE | 睡眠紀錄所屬日期 |
| hours | DOUBLE | 睡眠時數 |
例如:
| id | sleep_date | hours |
|---|---|---|
| 1 | 2026-10-15 | 7.5 |
| 2 | 2026-10-16 | 6.0 |
| 3 | 2026-10-17 | 8.0 |
這裡有一個需要先決定的問題:
如果睡眠時間跨越兩天,日期要算哪一天?
例如:
10/16 晚上 23:30 入睡
10/17 早上 07:00 起床
今天先約定:
sleepDate 代表起床當天的日期。
因此這筆睡眠紀錄會記在:
2026-10-17
另外,今天只記錄睡眠總時數,不記錄實際入睡和起床時間。
這樣可以先把資料庫和 API 功能完成,之後再逐步擴充。
首先建立新的 DTO:
SleepRequest.java
放在:
dto
Package 裡面。
完整程式如下:
package com.example.healthassistant.dto;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.DecimalMax;
import java.time.LocalDate;
public class SleepRequest {
@NotNull(message = "Sleep date is required.")
private LocalDate sleepDate;
@NotNull(message = "Sleep hours are required.")
@DecimalMin(value = "0.0", inclusive = false,
message = "Sleep hours must be greater than 0.")
@DecimalMax(value = "24.0",
message = "Sleep hours must not exceed 24.")
private Double hours;
public SleepRequest() {
}
public LocalDate getSleepDate() {
return sleepDate;
}
public void setSleepDate(LocalDate sleepDate) {
this.sleepDate = sleepDate;
}
public Double getHours() {
return hours;
}
public void setHours(Double hours) {
this.hours = hours;
}
}
這裡使用了之前 Day 11 學過的 Bean Validation。
@NotNull
代表欄位不能沒有值。
例如:
{
"hours": 7.5
}
因為沒有提供日期,就不符合我們的資料格式。
@DecimalMin(value = "0.0", inclusive = false)
代表睡眠時數必須大於 0。
例如:
{
"sleepDate": "2026-10-17",
"hours": -3
}
就不應該被接受。
@DecimalMax(value = "24.0")
代表我們暫時將單筆睡眠時數限制在 24 小時以內。
這只是今天練習使用的輸入範圍,並不是用來判斷睡眠是否健康。
接下來要讓資料庫可以保存睡眠資料。
打開之前建立的:
src/main/resources/schema.sql
保留原本的 water_records 資料表,並在下方新增:
CREATE TABLE IF NOT EXISTS sleep_records (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
sleep_date DATE NOT NULL,
hours DOUBLE PRECISION NOT NULL,
CONSTRAINT chk_sleep_hours
CHECK (hours > 0 AND hours <= 24)
);
這樣資料庫就會有兩張主要資料表:
water_records
sleep_records
分別負責儲存:
飲水紀錄
睡眠紀錄
因為昨天已經設定:
spring.sql.init.mode=always
所以 Spring Boot 啟動時會執行 schema.sql。
這裡使用:
CREATE TABLE IF NOT EXISTS
因此資料表已經存在時,就不會重複建立。
不過這不代表資料表結構會自動更新。未來如果修改欄位,還是需要另外處理資料庫結構變更。
前幾天我們已經建立:
WaterRepository.java
負責操作飲水紀錄。
今天則要新增:
SleepRepository.java
因為飲水量和睡眠時間是不同種類的資料。
如果全部寫在同一個 Repository 裡面,未來功能越來越多時,程式會變得很難管理。
所以今天繼續維持之前的分層架構:
SleepController
↓
SleepService
↓
SleepRepository
↓
H2 Database
每一層都有自己的工作。
接下來建立:
SleepRepository.java
放在:
repository
Package 裡面。
完整程式如下:
package com.example.healthassistant.repository;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import java.time.LocalDate;
import java.util.List;
import com.example.healthassistant.dto.SleepResponse;
@Repository
public class SleepRepository {
private final JdbcTemplate jdbcTemplate;
public SleepRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public void save(LocalDate date, double hours) {
String sql = """
INSERT INTO sleep_records (sleep_date, hours)
VALUES (?, ?)
""";
jdbcTemplate.update(sql, date, hours);
}
public List<SleepResponse> findByDate(LocalDate date) {
String sql = """
SELECT id, sleep_date, hours
FROM sleep_records
WHERE sleep_date = ?
ORDER BY id
""";
return jdbcTemplate.query(
sql,
(rs, rowNum) -> new SleepResponse(
rs.getLong("id"),
rs.getDate("sleep_date").toLocalDate(),
rs.getDouble("hours")
),
date
);
}
}
這裡使用了兩個之前學過的 JdbcTemplate Method。
jdbcTemplate.update(sql, date, hours);
負責新增睡眠紀錄。
jdbcTemplate.query(...)
負責查詢資料庫中的睡眠紀錄。
和之前的飲水量歷史查詢一樣,我們使用 query() 取得多筆資料。
因為今天暫時允許同一天新增多筆睡眠紀錄,所以查詢結果使用:
List<SleepResponse>
剛剛的 Repository 使用到:
SleepResponse
所以接下來建立:
SleepResponse.java
程式如下:
package com.example.healthassistant.dto;
import java.time.LocalDate;
public class SleepResponse {
private Long id;
private LocalDate sleepDate;
private double hours;
public SleepResponse(Long id, LocalDate sleepDate, double hours) {
this.id = id;
this.sleepDate = sleepDate;
this.hours = hours;
}
public Long getId() {
return id;
}
public LocalDate getSleepDate() {
return sleepDate;
}
public double getHours() {
return hours;
}
}
這個 DTO 用來表示資料庫查詢後的睡眠紀錄。
例如:
{
"id": 1,
"sleepDate": "2026-10-17",
"hours": 7.5
}
和 SleepRequest 不同的是:
SleepRequest 用來接收使用者輸入。
SleepResponse 則用來回傳已儲存的紀錄。
這樣可以讓資料輸入和輸出有比較清楚的區分。
Repository 完成後,接下來建立 Service。
檔案名稱:
SleepService.java
完整程式如下:
package com.example.healthassistant.service;
import com.example.healthassistant.dto.SleepRequest;
import com.example.healthassistant.dto.SleepResponse;
import com.example.healthassistant.repository.SleepRepository;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.util.List;
@Service
public class SleepService {
private final SleepRepository sleepRepository;
public SleepService(SleepRepository sleepRepository) {
this.sleepRepository = sleepRepository;
}
public SleepResponse addSleep(SleepRequest request) {
sleepRepository.save(
request.getSleepDate(),
request.getHours()
);
List<SleepResponse> records =
sleepRepository.findByDate(request.getSleepDate());
return records.get(records.size() - 1);
}
public List<SleepResponse> getSleepByDate(LocalDate date) {
return sleepRepository.findByDate(date);
}
}
這裡有兩個主要 Method。
負責新增睡眠紀錄。
sleepRepository.save(
request.getSleepDate(),
request.getHours()
);
會呼叫 Repository,將資料寫入 H2。
負責查詢指定日期的睡眠紀錄。
return sleepRepository.findByDate(date);
會回傳符合日期的資料。
這次先採用比較容易理解的方式:新增後再查詢該日期的紀錄,取得最後一筆作為回傳結果。
不過這個做法在多人同時新增資料時,可能取得別人剛剛新增的紀錄。正式系統應改用資料庫產生的 ID,精確取得本次新增的資料。
目前我們先以單人本機練習為主。
接下來終於要建立 API 了!
新增:
SleepController.java
完整程式如下:
package com.example.healthassistant.controller;
import com.example.healthassistant.dto.SleepRequest;
import com.example.healthassistant.dto.SleepResponse;
import com.example.healthassistant.service.SleepService;
import jakarta.validation.Valid;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/health/sleep")
public class SleepController {
private final SleepService sleepService;
public SleepController(SleepService sleepService) {
this.sleepService = sleepService;
}
@PostMapping
public ResponseEntity<Map<String, Object>> addSleep(
@Valid @RequestBody SleepRequest request) {
SleepResponse saved = sleepService.addSleep(request);
Map<String, Object> result = new HashMap<>();
result.put("message", "Sleep record saved successfully.");
result.put("id", saved.getId());
result.put("sleepDate", saved.getSleepDate());
result.put("hours", saved.getHours());
return ResponseEntity.ok(result);
}
@GetMapping
public ResponseEntity<List<SleepResponse>> getSleep(
@RequestParam
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
LocalDate date) {
List<SleepResponse> records =
sleepService.getSleepByDate(date);
return ResponseEntity.ok(records);
}
}
這次的 Controller 和之前的 WaterController 很相似。
因為我們已經熟悉:
@RestController
@RequestMapping
@PostMapping
@GetMapping
以及:
@Valid
所以建立新的 API 其實沒有想像中困難。
這也讓我發現,之前學過的 Spring Boot 架構可以一直重複使用。
只要資料類型不同,就建立對應的 Controller、Service、Repository 和 DTO。
接下來重新啟動 Spring Boot。
先測試新增資料。
HTTP Method:
POST
URL:
http://localhost:8080/health/sleep
Body 選擇:
raw → JSON
輸入:
{
"sleepDate": "2026-10-17",
"hours": 7.5
}
如果資料成功儲存,預期回傳類似:
{
"message": "Sleep record saved successfully.",
"id": 1,
"sleepDate": "2026-10-17",
"hours": 7.5
}
這裡的 id 由資料庫產生,所以實際數字可能不同。
接著再新增另一筆:
{
"sleepDate": "2026-10-16",
"hours": 6.0
}
預期就會新增另一筆睡眠紀錄。
接下來測試查詢功能。
HTTP Method:
GET
URL:
http://localhost:8080/health/sleep?date=2026-10-17
如果剛剛已經新增:
{
"sleepDate": "2026-10-17",
"hours": 7.5
}
預期回傳:
[
{
"id": 1,
"sleepDate": "2026-10-17",
"hours": 7.5
}
]
如果同一天有多筆紀錄,就會回傳多筆資料。
例如:
[
{
"id": 1,
"sleepDate": "2026-10-17",
"hours": 7.5
},
{
"id": 3,
"sleepDate": "2026-10-17",
"hours": 1.0
}
]
這樣就完成睡眠紀錄的新增與查詢!
除了測試正常資料,也要確認輸入驗證有沒有發揮作用。
{
"sleepDate": "2026-10-17",
"hours": -2
}
因為我們設定:
@DecimalMin(value = "0.0", inclusive = false)
所以這筆資料應該無法通過驗證。
{
"hours": 7.5
}
因為:
@NotNull
所以也不應該被接受。
{
"sleepDate": "2026-10-17",
"hours": 30
}
同樣會因為:
@DecimalMax(value = "24.0")
而驗證失敗。
如果前幾天建立的 GlobalExceptionHandler 已經正確處理 Bean Validation 錯誤,就可以回傳對應的錯誤資訊。
這樣能減少不合理資料進入資料庫的機會。
和之前的飲水紀錄一樣,我們可以進入 H2 Console 確認資料。
網址:
http://localhost:8080/h2-console
連接資料庫後,輸入:
SELECT * FROM sleep_records;
預期可以看到:
| ID | SLEEP_DATE | HOURS |
|---|---|---|
| 1 | 2026-10-17 | 7.5 |
| 2 | 2026-10-16 | 6.0 |
如果想依照日期排序,也可以使用:
SELECT *
FROM sleep_records
ORDER BY sleep_date;
這樣就能確認睡眠紀錄真的已經保存到資料庫。
今天新增了睡眠紀錄相關的 Controller、Service、Repository 和 DTO。
目前專案結構如下:
health-assistant
│
├── src/main/java/com/example/healthassistant
│ │
│ ├── HealthAssistantApplication.java
│ │
│ ├── controller
│ │ ├── HealthController.java
│ │ ├── WaterController.java
│ │ └── SleepController.java
│ │
│ ├── service
│ │ ├── BmiService.java
│ │ ├── WaterService.java
│ │ └── SleepService.java
│ │
│ ├── repository
│ │ ├── WaterRepository.java
│ │ └── SleepRepository.java
│ │
│ ├── dto
│ │ ├── HealthRequest.java
│ │ ├── WaterRequest.java
│ │ ├── WaterHistoryResponse.java
│ │ ├── WaterStatisticsResponse.java
│ │ ├── SleepRequest.java
│ │ └── SleepResponse.java
│ │
│ └── exception
│ └── GlobalExceptionHandler.java
│
├── src/main/resources
│ ├── application.properties
│ └── schema.sql
│
└── pom.xml
現在的智慧健康助理已經有兩組資料庫功能:
飲水紀錄
↓
WaterController
↓
WaterService
↓
WaterRepository
↓
water_records
以及:
睡眠紀錄
↓
SleepController
↓
SleepService
↓
SleepRepository
↓
sleep_records
這樣的架構也讓不同健康功能可以獨立管理。
雖然今天成功建立睡眠紀錄功能,但目前仍然是比較簡單的版本。
目前只輸入:
7.5 小時
但沒有記錄:
幾點入睡
幾點起床
所以還無法分析睡眠時間分布。
目前資料庫沒有設定同一天只能有一筆紀錄。
如果使用者重複新增,資料庫就會保存多筆。
未來可以再決定是否要支援午睡、分段睡眠,或限制每天只能有一筆主要睡眠紀錄。
目前只能新增與查詢。
還不能直接計算:
最近七天平均睡眠時間
這個功能可以留到下一篇繼續完成。
和飲水紀錄一樣,目前所有資料都放在同一個資料庫裡,沒有區分不同使用者。
因此仍然只適合單人本機練習。
今天雖然還沒有使用 AI,但我們已經開始建立第二種健康資料。
目前系統可以記錄:
飲水量
睡眠時間
未來如果再加入運動紀錄,就能整理出更完整的日常生活資訊。
例如:
{
"date": "2026-10-17",
"waterIntake": 1500,
"sleepHours": 7.5,
"exerciseMinutes": 30
}
之後可以先由 Java 計算各種統計數值,再讓 AI 協助產生文字摘要。
例如:
今天有記錄飲水量、睡眠時間及運動時間。
可以透過每週紀錄觀察生活習慣的變化。
不過要注意,睡眠時間不等於睡眠品質。
單純記錄睡了幾個小時,也無法直接判斷使用者是否有睡眠障礙。
所以目前的智慧健康助理仍然以資料紀錄和一般資訊整理為主,不會提供醫療診斷。
今天成功讓智慧健康助理加入第二種日常健康紀錄!
除了飲水量之外,現在也可以記錄睡眠時間。
今天完成的功能包括:
SleepRequest DTOsleep_records 資料表SleepRepository
JdbcTemplate 新增與查詢資料SleepService
SleepController
目前智慧健康助理已經具備:
BMI 計算與分類
│
├── 飲水量紀錄
│ ├── 新增紀錄
│ ├── 歷史查詢
│ └── 七天統計
│
└── 睡眠時間紀錄
├── 新增紀錄
└── 指定日期查詢
今天最大的收穫是:
學會將之前建立的 Spring Boot 分層架構,應用到不同種類的健康資料上。
原本以為每增加一個功能,都需要重新學習很多東西。
但實際整理後發現,Controller、Service、Repository 和 DTO 的概念都可以繼續沿用。
只要先把資料結構和功能需求想清楚,就能一步一步擴充系統。
今天已經成功建立睡眠時間紀錄功能。
但目前只能新增資料和查詢指定日期。
所以下一篇準備加入睡眠統計功能!
預計會學習:
讓智慧健康助理不只可以保存睡眠紀錄,也能開始整理睡眠時間的變化!