v45 第 45 批:日程「重复到」换最新控件 + CalDAV 双向同步

用户 2026-10-12 两条:

1. 新建日程里「重复到」的那个日历选择框,改成最新控件。
   - templates/calendar/_event_fields.php:裸 <input type=date> 换成
     .dt-input.dt-antd-single[data-antd-date=1](AntD 纯日期 DatePicker),
     复用既有 mountAntdDate,不新写挂载逻辑。
   - 值载体仍是 name=rep_until 的原生框(包在 .dt-native 里),
     服务端契约与 Repeat::build($_POST) 一字不改。

2. CalDAV 从「只读」改成「双向同步」。
   用户给了一台可用的参照服务器(群晖 DAViCal)要求「完全按参照返回」;
   抓参照 + 复刻 Thunderbird / SyncCalendar 的真实请求序列后确认:
   参照是可写的,而只读会让两个客户端都判「不可用」。
   - dav.php:放行 PUT(新建 201+ETag / 更新 204+ETag / If-Match 不符 412)、
     DELETE(204)、MKCALENDAR(201+Location),仍逐本判权;
     OPTIONS 的 Allow 与 DAV 头照参照补齐(DAV 头两行,注意 PHP header 追加);
     current-user-privilege-set 按实际可写性给,并排除系统派生本(农历等)。
   - app/CalDav.php:资源名统一用 UID(原自定义本用 id、派生本用 uid 两种混着,
     一开写就表现为「客户端 PUT 到 UID 名、服务端 href 是 id 名 ⇒ 写进去读不回」);
     补 owner / getctag / getcontenttype / calendar-description;新增 ctagSeedOf。
   - app/Ics.php:修一个一直存在的真 bug —— 导出 RRULE 漏了 `RRULE:` 前缀
     (库里存的是去前缀的值),导致所有导出/同步出去的重复日程都变成单次。
   - templates/home.php:首页红线补 data-now-min(给探针做零容差判据用)。

验证:本地 e2e 1510/0、TB 探针 35/0、DAV 诊断 28/0、
v43 真机 33/0 + 反向 9/0、v44 真机 41/0 + API 63/0 + 反向 19/0、
v45 重复到 10/0 + 反向 2/0、v45 DAV 双向 24/0 + 反向 5/0;
全流水线 PUSH_RC=0(含部署静态断言第 45 批形态)。
This commit is contained in:
2026-10-12 21:40:53 +08:00
parent 9f41e1622e
commit 4ccbcc3360
5 changed files with 384 additions and 28 deletions
+12 -1
View File
@@ -90,7 +90,18 @@ final class Ics
$out[] = 'DESCRIPTION:' . self::escapeText((string)$e['note']);
}
if (($e['rrule'] ?? '') !== '') {
$out[] = (string)$e['rrule']; // 原样带过(导入时存的就是原文)
/* 🔴🔴 v45(2026-10-12)修的真 bug:这里原来**漏了 `RRULE:` 前缀**,
直接吐 `$e['rrule']`(库里存的是**去掉前缀**的值,见 parse 的
`'rrule' => self::nullOr($p['RRULE'])`)⇒ 导出的 ICS 里那一行是
`FREQ=WEEKLY;UNTIL=20261231T235959Z` —— 一个**没有属性名**的裸行,
任何客户端都不会把它当成重复规则(RFC 5545 要求 `RRULE:...`)。
后果:所有导出/CalDAV 同步出去的重复日程,在别人那里**都变成单次**。
为什么以前没被发现:夹具与线上导出的都是"不带重复"的日程,
这条分支一直没被走到(本轮为了验 PUT 重复日程才第一次踩到)。 */
/* ⚠️ 值类型是 **RECUR**,不是 TEXT —— 里面的逗号是**结构分隔符**
(如 `BYDAY=MO,TU,WE,TH,FR`),**绝不能**用 escapeText 转义,
否则客户端会把它当成一整块而解析失败。原样拼接即可。 */
$out[] = 'RRULE:' . (string)$e['rrule'];
}
$out[] = 'STATUS:' . strtoupper((string)($e['status'] ?: 'confirmed'));
$out[] = 'TRANSP:' . (empty($e['transp']) ? 'OPAQUE' : 'TRANSPARENT');