跳至主要内容

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 為輔——字典資料總量大、存取零散,避免佔用記憶體
與上一代等價的 APICalendarService / 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();
}

AlmanacAutoCloseable,關閉時釋放 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-serverAlmanacConfig / 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-urlhttps://almanac.leandev.ioalmanac 服務位址(正式環境為 root context)
app.almanac.auth.modeapi-key認證模式;service-account 為預留
app.almanac.auth.api-key(空)API Key;建議走環境變數 ALMANAC_API_KEY
app.almanac.cache.directory系統暫存目錄磁碟快取目錄
app.almanac.cache.ttlP1D快取存活時間

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(如 TWTWN
String convertISO3CountryToISO2(String iso3)alpha-3 → alpha-2
String findSubdivisionCodeByISO2AndName(iso2, name)以國家 + 區劃名稱(部分比對)查代碼,去國家前綴(如 US + CaliforniaCA
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(供磁碟快取序列化):

型別欄位
CalendarDaydate, holiday, description
Countryalpha2, alpha3, numeric, name
Subdivisioncode, country, name, type
Cityid, code, name
Townid, code, name, cityCode, zipCode
Villageid, code, name, cityCode, townCode

almanac 端點對照

服務almanac 端點
CalendarGET /api/v1/calendar/{year}
LocationGET /api/v1/location/countries/subdivisions/countries/{alpha2}/subdivisions
AddressGET /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(...) 取值)。