增加自动获取节假日
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
package com.pomelotea.hoperun.sign.holiday
|
||||
|
||||
import com.alibaba.fastjson.JSON
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.time.DayOfWeek
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.annotation.PostConstruct
|
||||
|
||||
@Service
|
||||
class HolidayService(
|
||||
private val config: HolidayConfig
|
||||
) {
|
||||
private val logger = LoggerFactory.getLogger(HolidayService::class.java)
|
||||
private val cache = ConcurrentHashMap<Int, HolidayCalendar>()
|
||||
private val dateFormatter = DateTimeFormatter.ISO_LOCAL_DATE
|
||||
private val httpClient = OkHttpClient.Builder()
|
||||
.connectTimeout(Duration.ofSeconds(10))
|
||||
.callTimeout(Duration.ofSeconds(10))
|
||||
.build()
|
||||
|
||||
@PostConstruct
|
||||
fun preload() {
|
||||
if (!config.enabled) {
|
||||
return
|
||||
}
|
||||
val now = LocalDate.now()
|
||||
val endYear = now.year + config.yearsAhead
|
||||
logger.info("节假日配置预加载开始: years={}..{}", now.year, endYear)
|
||||
for (year in now.year..endYear) {
|
||||
runCatching { getCalendar(year) }
|
||||
.onFailure { logger.warn("节假日配置预加载失败: year={}, err={}", year, it.message) }
|
||||
}
|
||||
}
|
||||
|
||||
fun isWorkingDay(dateStr: String): Boolean {
|
||||
val date = LocalDate.parse(dateStr, dateFormatter)
|
||||
return isWorkingDay(date)
|
||||
}
|
||||
|
||||
fun isWorkingDay(date: LocalDate): Boolean {
|
||||
if (!config.enabled) {
|
||||
return defaultIsWorkingDay(date)
|
||||
}
|
||||
val dateKey = date.format(dateFormatter)
|
||||
val calendar = getCalendar(date.year)
|
||||
if (calendar.workdays.contains(dateKey)) {
|
||||
return true
|
||||
}
|
||||
if (calendar.holidays.contains(dateKey)) {
|
||||
return false
|
||||
}
|
||||
return defaultIsWorkingDay(date)
|
||||
}
|
||||
|
||||
private fun defaultIsWorkingDay(date: LocalDate): Boolean {
|
||||
return date.dayOfWeek != DayOfWeek.SATURDAY && date.dayOfWeek != DayOfWeek.SUNDAY
|
||||
}
|
||||
|
||||
private fun getCalendar(year: Int): HolidayCalendar {
|
||||
val now = Instant.now()
|
||||
val cached = cache[year]
|
||||
if (cached != null && !isExpired(cached.updatedAt, now)) {
|
||||
return cached
|
||||
}
|
||||
|
||||
val remoteResult = fetchRemote(year)
|
||||
if (remoteResult != null) {
|
||||
val calendar = applyFallback(parse(remoteResult.body)).copy(updatedAt = now)
|
||||
cache[year] = calendar
|
||||
writeCache(year, remoteResult.body)
|
||||
logger.info(
|
||||
"节假日配置加载成功(远程): year={}, url={}, holidays={}, workdays={}",
|
||||
year,
|
||||
remoteResult.url,
|
||||
calendar.holidays.size,
|
||||
calendar.workdays.size
|
||||
)
|
||||
return calendar
|
||||
}
|
||||
|
||||
if (cached != null) {
|
||||
val refreshed = cached.copy(updatedAt = now)
|
||||
cache[year] = refreshed
|
||||
logger.warn(
|
||||
"节假日远程刷新失败,继续使用内存缓存: year={}, holidays={}, workdays={}",
|
||||
year,
|
||||
refreshed.holidays.size,
|
||||
refreshed.workdays.size
|
||||
)
|
||||
return refreshed
|
||||
}
|
||||
|
||||
val cachedRaw = readCache(year)
|
||||
if (cachedRaw != null) {
|
||||
val calendar = applyFallback(parse(cachedRaw)).copy(updatedAt = now)
|
||||
cache[year] = calendar
|
||||
logger.info(
|
||||
"节假日配置加载成功(本地缓存): year={}, path={}, holidays={}, workdays={}",
|
||||
year,
|
||||
cachePath(year).toString(),
|
||||
calendar.holidays.size,
|
||||
calendar.workdays.size
|
||||
)
|
||||
return calendar
|
||||
}
|
||||
|
||||
logger.warn("节假日配置缺失,仅使用fallback: year={}", year)
|
||||
val fallbackCalendar = applyFallback(HolidayCalendar(emptySet(), emptySet(), now))
|
||||
cache[year] = fallbackCalendar
|
||||
return fallbackCalendar
|
||||
}
|
||||
|
||||
private fun isExpired(updatedAt: Instant, now: Instant): Boolean {
|
||||
if (config.cacheTtlDays <= 0) {
|
||||
return false
|
||||
}
|
||||
return Duration.between(updatedAt, now).toDays() >= config.cacheTtlDays
|
||||
}
|
||||
|
||||
private fun fetchRemote(year: Int): FetchResult? {
|
||||
val urls = buildUrls(year)
|
||||
for (url in urls) {
|
||||
try {
|
||||
val request = Request.Builder().url(url).get().build()
|
||||
httpClient.newCall(request).execute().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
logger.warn("节假日配置拉取失败: url={}, code={}", url, response.code)
|
||||
return@use
|
||||
}
|
||||
val body = response.body?.string()
|
||||
if (!body.isNullOrBlank()) {
|
||||
return FetchResult(url = url, body = body)
|
||||
}
|
||||
logger.warn("节假日配置拉取为空: url={}", url)
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
logger.warn("节假日配置拉取异常: url={}, err={}", url, ex.message)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun buildUrls(year: Int): List<String> {
|
||||
if (config.sources.isEmpty() && config.proxySources.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
val urls = ArrayList<String>()
|
||||
config.sources.forEach { urls.add(it.replace("{year}", year.toString())) }
|
||||
config.proxySources.forEach { urls.add(it.replace("{year}", year.toString())) }
|
||||
return urls
|
||||
}
|
||||
|
||||
private fun parse(raw: String): HolidayCalendar {
|
||||
val entries = JSON.parseArray(raw, HolidayEntry::class.java) ?: emptyList()
|
||||
val holidays = LinkedHashSet<String>()
|
||||
val workdays = LinkedHashSet<String>()
|
||||
for (entry in entries) {
|
||||
val date = entry.date?.trim()
|
||||
if (date.isNullOrEmpty()) {
|
||||
continue
|
||||
}
|
||||
when (entry.isOffDay) {
|
||||
true -> holidays.add(date)
|
||||
false -> workdays.add(date)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
return HolidayCalendar(holidays, workdays, Instant.now())
|
||||
}
|
||||
|
||||
private fun applyFallback(calendar: HolidayCalendar): HolidayCalendar {
|
||||
if (config.fallback.holidays.isEmpty() && config.fallback.workdays.isEmpty()) {
|
||||
return calendar
|
||||
}
|
||||
val holidays = calendar.holidays.toMutableSet()
|
||||
val workdays = calendar.workdays.toMutableSet()
|
||||
for (date in config.fallback.holidays) {
|
||||
holidays.add(date)
|
||||
workdays.remove(date)
|
||||
}
|
||||
for (date in config.fallback.workdays) {
|
||||
workdays.add(date)
|
||||
holidays.remove(date)
|
||||
}
|
||||
return calendar.copy(holidays = holidays, workdays = workdays)
|
||||
}
|
||||
|
||||
private fun cachePath(year: Int): Path {
|
||||
return Paths.get(config.cacheDir).resolve("$year.json")
|
||||
}
|
||||
|
||||
private fun writeCache(year: Int, raw: String) {
|
||||
try {
|
||||
val path = cachePath(year)
|
||||
Files.createDirectories(path.parent)
|
||||
Files.write(path, raw.toByteArray(StandardCharsets.UTF_8))
|
||||
logger.info("节假日缓存写入成功: year={}, path={}", year, path.toString())
|
||||
} catch (ex: Exception) {
|
||||
logger.warn("节假日缓存写入失败: year={}, err={}", year, ex.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun readCache(year: Int): String? {
|
||||
return try {
|
||||
val path = cachePath(year)
|
||||
if (!Files.exists(path)) {
|
||||
return null
|
||||
}
|
||||
String(Files.readAllBytes(path), StandardCharsets.UTF_8)
|
||||
} catch (ex: Exception) {
|
||||
logger.warn("节假日缓存读取失败: year={}, err={}", year, ex.message)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private data class FetchResult(
|
||||
val url: String,
|
||||
val body: String
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user