diff --git a/app/Holiday.php b/app/Holiday.php index 6fed229..a28a56c 100644 --- a/app/Holiday.php +++ b/app/Holiday.php @@ -399,9 +399,24 @@ final class Holiday if ($t === false || $end === false || $end < $t) { return []; } - $years = [(int)substr($from, 0, 4)]; - if ((int)substr($to, 0, 4) !== $years[0]) { - $years[] = (int)substr($to, 0, 4); + // 🔴 2026-11-02:原来只取「首年 + 末年」两年。 + // 全量范围 1970~2100 下 ⇒ 只算了 1970 和 2100 的节气, + // 中间 128 年全丢(用户会觉得"日历上没节气")。 + // 改成逐年遍历,并夹紧到农历算法支持范围 + 年数上限。 + $yFrom = (int)substr($from, 0, 4); + $yTo = (int)substr($to, 0, 4); + if ($yFrom < Lunar::MIN_YEAR) { + $yFrom = Lunar::MIN_YEAR; + } + if ($yTo > Lunar::MAX_YEAR) { + $yTo = Lunar::MAX_YEAR; + } + if ($yTo - $yFrom + 1 > self::MAX_LUNAR_YEARS) { + $yTo = $yFrom + self::MAX_LUNAR_YEARS - 1; + } + $years = []; + for ($yy = $yFrom; $yy <= $yTo; $yy++) { + $years[] = $yy; } foreach ($years as $y) { foreach (self::SOLAR_TERMS as $idx => $name) { @@ -435,9 +450,25 @@ final class Holiday if ($t === false || $end === false || $end < $t) { return []; } - // 逐日扫区间,转农历后查表 —— 区间很短(月视图 ≤42 天),成本可忽略。 - // 这样天然处理"跨月/跨年"(如腊月二十三可能落在次年 1 月)。 - for ($i = 0; $t <= $end && $i < 400; $i++, $t = strtotime('+1 day', $t)) { + // 逐日扫区间,转农历后查表 —— 天然处理"跨月/跨年" + // (如腊月二十三可能落在次年 1 月)。 + // + // 🔴 2026-11-02:原来有 `$i < 400` 的硬上限。改成全量同步后请求范围是 + // 1970~2100(47846 天)⇒ 只算前 400 天,**今年的春节/中秋全丢**。 + // 改为与 lunarRange 一致的「夹范围 + 年数上限」。 + $minT = (int)strtotime(sprintf('%04d-01-31', Lunar::MIN_YEAR)); + $maxT = (int)strtotime(sprintf('%04d-12-31', Lunar::MAX_YEAR)); + if ($t < $minT) { + $t = $minT; + } + if ($end > $maxT) { + $end = $maxT; + } + $yearsLimitT = (int)strtotime('+' . self::MAX_LUNAR_YEARS . ' years', $t); + if ($end > $yearsLimitT) { + $end = $yearsLimitT; + } + for (; $t <= $end; $t = strtotime('+1 day', $t)) { $date = date('Y-m-d', $t); $y = (int)date('Y', $t); $m = (int)date('n', $t); $d = (int)date('j', $t); $l = Lunar::solarToLunar($y, $m, $d); @@ -695,6 +726,19 @@ final class Holiday * * ⚠️ 每天的农历都要出,不能只出初一十五 —— 用户要的是"看日历就知道今天农历几号"。 */ + /** + * 单次请求最多算多少年的农历。 + * + * 🔴 2026-11-02:原来是「最多 400 天」的硬上限,而客户端改成全量同步后 + * 请求范围是 1970~2100(47846 天)⇒ 只返回前 400 天的农历, + * 导致**今天的农历是空的**。 + * + * 改成按年计:客户端实际只需「今天 ±5 年」, + * 留 30 年余量足够,同时防止有人手拼 1900~2100 把服务器拖死 + * (算 7 万天农历约需数十秒)。 + */ + private const MAX_LUNAR_YEARS = 30; + public static function lunarRange(string $from, string $to): array { $out = []; @@ -703,8 +747,27 @@ final class Holiday if ($t === false || $end === false || $end < $t) { return []; } - // 上限保险:范围异常大时不至于把请求拖死(正常月视图最多 42 天) - for ($i = 0; $t <= $end && $i < 400; $i++, $t = strtotime('+1 day', $t)) { + + // ① 夹到农历算法支持的范围(1900-01-31 ~ 2100-12-31), + // 否则 solarToLunar 会一路返回 null,白跑空转。 + $minT = (int)strtotime(sprintf('%04d-01-31', Lunar::MIN_YEAR)); + $maxT = (int)strtotime(sprintf('%04d-12-31', Lunar::MAX_YEAR)); + if ($t < $minT) { + $t = $minT; + } + if ($end > $maxT) { + $end = $maxT; + } + + // ② 年数上限:超出就截断到 MAX_LUNAR_YEARS 年末 + $yearsLimitT = (int)strtotime('+' . self::MAX_LUNAR_YEARS . ' years', $t); + if ($end > $yearsLimitT) { + $end = $yearsLimitT; + } + + // ③ 逐日生成(农历必须逐日算,无法按年跳) + // 原来的 `$i < 400` 计数制已由上面的年数上限取代。 + for (; $t <= $end; $t = strtotime('+1 day', $t)) { $date = date('Y-m-d', $t); $y = (int)date('Y', $t); $m = (int)date('n', $t); $d = (int)date('j', $t); $l = Lunar::solarToLunar($y, $m, $d);