Holiday:去掉三处 400 天上限,农历/节日/节气在全量范围下不再丢失

## 问题

lunarRange / traditionalRange 都有 `$i < 400` 硬上限。
2026-10-30 客户端改成全量同步(1970~2100,47846 天)后,
这个「防异常」的保险变成「削正常功能」:
- 只算前 400 天 ⇒ **今天的农历是空的**
- 传统节日同样只剩前 400 天 ⇒ 今年的春节/中秋全丢

solarTermRange 另有一个问题:只取首末两年,
130 年范围下只算 1970 和 2100 的节气,中间 128 年全丢。

## 修法

- 三处统一改为:夹到 Lunar::MIN_YEAR~MAX_YEAR,再按
  MAX_LUNAR_YEARS(30 年)截断,去掉按天计数上限
- solarTermRange 改为逐年遍历

## 实测(部署后)

今天 2026-11-02 → 廿四(修复前为空)
今年 11 个传统节日、24 个节气,齐全
2021~2031 农历 4017 条(修复前 400),节气 264 条(修复前 48)
1970~2100 农历 10958 条 / 0.47 秒(按 30 年上限截断,符合设计)

## 注意

本文件不在客户端仓库,需单独上传服务器。
验证前必须比对服务器 md5,否则会拿旧代码验出假结果(本次踩过)。
This commit is contained in:
2026-11-02 08:51:34 +08:00
parent ce752073db
commit 821cfd1542
+71 -8
View File
@@ -399,9 +399,24 @@ final class Holiday
if ($t === false || $end === false || $end < $t) { if ($t === false || $end === false || $end < $t) {
return []; return [];
} }
$years = [(int)substr($from, 0, 4)]; // 🔴 2026-11-02:原来只取「首年 + 末年」两年。
if ((int)substr($to, 0, 4) !== $years[0]) { // 全量范围 1970~2100 下 ⇒ 只算了 1970 和 2100 的节气,
$years[] = (int)substr($to, 0, 4); // 中间 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 ($years as $y) {
foreach (self::SOLAR_TERMS as $idx => $name) { foreach (self::SOLAR_TERMS as $idx => $name) {
@@ -435,9 +450,25 @@ final class Holiday
if ($t === false || $end === false || $end < $t) { if ($t === false || $end === false || $end < $t) {
return []; return [];
} }
// 逐日扫区间,转农历后查表 —— 区间很短(月视图 ≤42 天),成本可忽略。 // 逐日扫区间,转农历后查表 —— 天然处理"跨月/跨年"
// 这样天然处理"跨月/跨年"(如腊月二十三可能落在次年 1 月)。 // (如腊月二十三可能落在次年 1 月)。
for ($i = 0; $t <= $end && $i < 400; $i++, $t = strtotime('+1 day', $t)) { //
// 🔴 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); $date = date('Y-m-d', $t);
$y = (int)date('Y', $t); $m = (int)date('n', $t); $d = (int)date('j', $t); $y = (int)date('Y', $t); $m = (int)date('n', $t); $d = (int)date('j', $t);
$l = Lunar::solarToLunar($y, $m, $d); $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 public static function lunarRange(string $from, string $to): array
{ {
$out = []; $out = [];
@@ -703,8 +747,27 @@ final class Holiday
if ($t === false || $end === false || $end < $t) { if ($t === false || $end === false || $end < $t) {
return []; 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); $date = date('Y-m-d', $t);
$y = (int)date('Y', $t); $m = (int)date('n', $t); $d = (int)date('j', $t); $y = (int)date('Y', $t); $m = (int)date('n', $t); $d = (int)date('j', $t);
$l = Lunar::solarToLunar($y, $m, $d); $l = Lunar::solarToLunar($y, $m, $d);