Almanac 資料服務
Package:
io.leandev.appfuse.almanac.*
AppFuse Server 提供一組連接 almanac 平台服務的客戶端,取得行政機關辦公日曆、國家 / ISO 地區代碼、台灣地址(縣市 / 鄉鎮市區 / 村里) 等公用資料。
過往這些資料由應用端直接連接各原始資料源(政府開放資料 CSV、內政部國土測繪中心 NLSC 等),各自處理格式、編碼、重試與快取。改由 almanac 統一前置(https://almanac.leandev.io)後,應用端只需面對一個穩定、認證一致的 JSON API,資料源的變動由 almanac 吸收。
核心特色
| 特色 | 說明 |
|---|---|
| 單一來源 | 三類公用資料統一經 almanac 取得,不再各自連原始資料源 |
| 認證一致 | 以 API Key(X-Almanac-Api-Key)認證;認證策略可插拔,預留 service account 擴充 |
| 檔案型快取 | 以 Ehcache 磁碟層為主、極小 heap 為輔——字典資料總量大、存取零散,避免佔用記憶體 |
| 與上一代等價的 API | CalendarService / LocationService / AddressService 方法簽名沿襲舊版,僅資料來源改為 almanac |
快速開始
純框架用法(builder)
import io.leandev.appfuse.almanac.Almanac;
import java.time.LocalDate;
import java.nio.file.Path;
try (Almanac almanac = Almanac.builder()
.apiKey("your-almanac-api-key")
.cacheDirectory(Path.of("/var/cache/almanac")) // 省略則用系統暫存目錄
.build()) {
boolean holiday = almanac.calendar().isHoliday(LocalDate.of(2026, 1, 1));
String iso3 = almanac.location().convertISO2CountryToISO3("TW"); // "TWN"
var cities = almanac.address().findAllCities();
}
Almanac 為 AutoCloseable,關閉時釋放 HTTP 連線池與快取資源。三個領域服務(calendar() / location() / address())為 lazy 建立的單例。
Spring Boot 用法
在參考實作 app-server 中,Almanac 以 Bean 形式提供,並由 app.almanac.* 外部設定驅動(見「設定」)。直接注入服務即可:
@Service
@RequiredArgsConstructor
public class HolidayService {
private final CalendarService calendarService; // 由 AlmanacConfig 提供
public boolean isBusinessDay(LocalDate date) {
return Boolean.TRUE.equals(calendarService.isWorkingDay(date));
}
}
框架本體只提供工具(
Almanac客戶端),不含 Spring 設定。Bean 的組裝與外部設定接線由消費端(app-server的AlmanacConfig/AlmanacProperties)承擔——這是 AppFuse「框架提供工具、應用負責配置」的一貫分工。
設定
消費端透過 app.almanac.* 設定(見 AlmanacProperties / AlmanacConfig):
app:
almanac:
# almanac 服務 base URL;預設正式環境(反代已 strip context-path,端點在 /api/v1 之下)
base-url: https://almanac.leandev.io
auth:
# 認證模式:api-key(目前支援)/ service-account(預留,尚未實作)
mode: api-key
# API Key(機密);建議以環境變數 ALMANAC_API_KEY 注入
api-key: ${ALMANAC_API_KEY:}
cache:
# 磁碟快取目錄(與 app.cache 的記憶體快取分開)
directory: ${app.home:${user.home}}/var/almanac-cache
# 快取存活時間(ISO-8601 duration,預設一天)
ttl: P1D
| 設定 | 預設 | 說明 |
|---|---|---|
app.almanac.base-url | https://almanac.leandev.io | almanac 服務位址(正式環境為 root context) |
app.almanac.auth.mode | api-key | 認證模式;service-account 為預留 |
app.almanac.auth.api-key | (空) | API Key;建議走環境變數 ALMANAC_API_KEY |
app.almanac.cache.directory | 系統暫存目錄 | 磁碟快取目錄 |
app.almanac.cache.ttl | P1D | 快取存活時間 |
dev 環境可留空
api-key:應用仍可啟動,僅在實際呼叫 almanac 時因缺少認證而失敗。test / prod 建議經/env-config收集後以環境變數注入。
認證
以 X-Almanac-Api-Key 標頭認證。認證策略由 AlmanacCredentials 介面抽象,內建實作為 ApiKeyCredentials:
Almanac.builder().apiKey("...").build(); // 固定 key
Almanac.builder().apiKey(() -> secretStore.almanac()).build(); // 動態 key(Supplier)
AlmanacCredentials 為函式介面,保留未來擴充 almanac service account(OAuth2 client-credentials → Bearer token)等其他認證方式的空間——屆時新增對應實作、以 credentials(...) 注入即可,現有 API Key 設定不受影響。
快取
almanac 視為可靠來源,客戶端採單層磁碟快取(不啟用 fallback):
- 以 Ehcache 磁碟層(檔案) 保存查得的資料,上面只掛極小 heap。
- 適合字典型資料——總量大、存取零散,全放記憶體成本過高、放太小又 hit rate 偏低。
- 與
app.cache.*的記憶體預算管制快取(CacheConfig)隔離,互不佔用記憶體預算。 - TTL 預設一天(
app.almanac.cache.ttl)。
服務 API
CalendarService — 行政機關辦公日曆
資料取自 almanac GET /api/v1/calendar/{year};時區固定 Asia/Taipei。
| 方法 | 說明 |
|---|---|
Boolean isWorkingDay(LocalDate | Date) | 是否為工作日 |
Boolean isHoliday(LocalDate | Date) | 是否為假日 |
LocalDate / Date plusWorkingDays(date, n) | 加 n 個工作日 |
LocalDate / Date minusWorkingDays(date, n) | 減 n 個工作日 |
LocalDate plusCalendarDays(date, n) | 加 n 個日曆日 |
LocalDate minusCalendarDays(date, n) | 減 n 個日曆日 |
List<CalendarDay> findCalendarDays(int year) | 取整年日曆 |
資料不可用(如該年份未涵蓋)時,
isHoliday/isWorkingDay與工作日加減方法回傳null(沿襲上一代行為)。
LocationService — 國家 / 行政區劃代碼
資料取自 almanac GET /api/v1/location/...。
| 方法 | 說明 |
|---|---|
String convertISO2CountryToISO3(String iso2) | alpha-2 → alpha-3(如 TW → TWN) |
String convertISO3CountryToISO2(String iso3) | alpha-3 → alpha-2 |
String findSubdivisionCodeByISO2AndName(iso2, name) | 以國家 + 區劃名稱(部分比對)查代碼,去國家前綴(如 US + California → CA) |
List<Country> findAllCountries() | 所有國家 |
List<Subdivision> findSubdivisions(String iso2) | 指定國家的所有行政區劃 |
AddressService — 台灣地址
資料取自 almanac GET /api/v1/address/...。
| 方法 | 說明 |
|---|---|
List<City> findAllCities() | 所有縣市 |
List<Town> findAllTownsByCity(String cityCode) | 指定縣市的鄉鎮市區 |
List<Village> findAllVillagesByTown(cityCode, townCode) | 指定鄉鎮市區的村里 |
Optional<City> findCity(String cityCode) / findCityByName(String name) | 依代碼 / 名稱查縣市 |
Optional<Town> findTown(cityCode, townCode) / findTownByName(cityName, townName) | 依代碼 / 名稱查鄉鎮市區 |
List<String> findAllRoadsByTown(cityCode, townCode) | 道路(almanac 未提供,固定回空清單) |
領域型別
皆為 record、實作 Serializable(供磁碟快取序列化):
| 型別 | 欄位 |
|---|---|
CalendarDay | date, holiday, description |
Country | alpha2, alpha3, numeric, name |
Subdivision | code, country, name, type |
City | id, code, name |
Town | id, code, name, cityCode, zipCode |
Village | id, code, name, cityCode, townCode |
almanac 端點對照
| 服務 | almanac 端點 |
|---|---|
| Calendar | GET /api/v1/calendar/{year} |
| Location | GET /api/v1/location/countries、/subdivisions、/countries/{alpha2}/subdivisions |
| Address | GET /api/v1/address/cities、/cities/{cityCode}/towns、/cities/{cityCode}/towns/{townCode}/villages |
錯誤處理
- 連線或解析失敗拋出
AlmanacException(runtime)。 - almanac 回 401(缺少 / 無效 API Key)、503(資料不可用)等狀態,會由底層
StandardHttpClient映射為對應例外向上拋出。 CalendarService的布林查詢與工作日加減在資料不可用時回null,呼叫端應做 null 檢查(或以Boolean.TRUE.equals(...)取值)。