子任务跟随父分类,修复父子跨本导致客户端无法显示层级
用户问:「任务是带有子任务功能的,能否通过 caldav,把任务的子任务也同步给客户端?」
## 答案:能,但实测发现父子被分到了不同本
改前数据:
父 #12「轻舟的幼儿园发票」 category_id = 1(家庭事务)→ tasks-1.ics
子 #29「送财务报销」 category_id = NULL(收件箱) → tasks-inbox.ics
客户端渲染「家庭事务」时看不到子任务,渲染「收件箱」时又找不到父任务,
⇒ 不可能显示成层级,只能两条并列。
## 改法
### 1. 子任务跟随父分类(TaskDav::effectiveCat)
有效分类 = 自己的 category_id
↓ 为空且是子任务时
父任务的 category_id
⚠️ **不改数据库**,只在视图层算。
改库等于篡改用户数据,而且单向不可逆。
边界:
| 情况 | 有效分类 |
|---|---|
| 自己无分类 + 父有分类 | 跟父(父子同本) |
| 自己无分类 + 父也无分类 | 都进收件箱(父子同本) |
| 自己有分类 | 用自己的(尊重用户明确设定) |
`tasksIn()` 改为 SQL 联查 `p.category_id`(一次到位,避免 N+1),
再在内存里按有效分类过滤。
### 2. SUMMARY 去掉硬加的「└ 」前缀
原来给子任务 SUMMARY 加「└ 」。这会**污染用户数据** ——
客户端把标题写回来(PUT)时前缀被存进库,用户的标题就多了一坨。
层级改由标准属性表达:
- `RELATED-TO:task-12` —— RFC 5545 §3.8.4.5 标准父子
- `X-FAMILYBOOK-PARENT:轻舟的幼儿园发票` —— 展示用补充,不支持的客户端无视
### 3. 跨本时的降级提示
用户明确给子任务设了别的分类时(尊重其选择),父子仍会跨本。
那种情况在 DESCRIPTION 尾部补一句人话说明,避免客户端把
「有 RELATED-TO 但找不到父」当成数据损坏而不显示这条。
## 验证
改后分布:
| 本 | 总数 | 父 | 子 |
|---|---|---|---|
| 家庭事务 | 7 | 6 | 1(#29) |
| 工作相关 | 19 | 19 | 0 |
| 个人事务 | 2 | 2 | 0 |
| 党支部工作 | 1 | 1 | 0 |
| 轻舟相关 | 1 | 1 | 0 |
| 收件箱 | 2 | 2 | 0 |
- 总数仍 32 条,没丢没重
- **所有父子都在同一本里**
- HTTP 端到端 39 项全过(含跨本隔离、鉴权、日历本回归)
## 客户端支持度说明(服务端无法改变)
| 客户端 | 层级显示 |
|---|---|
| DAVx⁵ + Tasks.org | 认 RELATED-TO,显示缩进子项 |
| 苹果提醒事项 | 有原生子任务 UI,对 RELATED-TO 支持有限 |
| Thunderbird | 不显示层级,两条并列 |
数据是正确的,显示成什么样取决于客户端能力。
想在所有客户端都看到层级,只能靠前端自己渲染 —— 那是网页端的事。
This commit is contained in:
+34
-8
@@ -131,15 +131,27 @@ final class Ics
|
||||
$out[] = 'LAST-MODIFIED:' . self::dateTimeValue((string)$t['updated_at']);
|
||||
}
|
||||
|
||||
// ---- 标题(子任务加视觉前缀,纯展示)----
|
||||
/* ---- 标题 ----
|
||||
🔴 2026-11-02 改:原来给子任务硬加「└ 」前缀。那会**污染用户数据** ——
|
||||
客户端把标题写回来(PUT)时,前缀会被存进数据库,
|
||||
用户的任务标题就凭空多了一坨。
|
||||
层级改由 RELATED-TO 表达(标准属性),
|
||||
另给 X-FAMILYBOOK-PARENT 做展示用的补充,不碰 SUMMARY。 */
|
||||
$title = (string)($t['title'] ?? '');
|
||||
if ((int)($t['parent_id'] ?? 0) > 0) {
|
||||
$title = '└ ' . $title;
|
||||
}
|
||||
$out[] = 'SUMMARY:' . self::escapeText($title);
|
||||
|
||||
if (($t['note'] ?? '') !== '') {
|
||||
$out[] = 'DESCRIPTION:' . self::escapeText((string)$t['note']);
|
||||
$parentId = (int)($t['parent_id'] ?? 0);
|
||||
$note = (string)($t['note'] ?? '');
|
||||
/* 跨本父子:客户端会看到 RELATED-TO 指向一个本里不存在的 UID,
|
||||
部分客户端会**直接不显示这条**(当成孤立子项)。
|
||||
补一句人话说明,让用户知道这是"父在别的分类里",不是数据损坏。 */
|
||||
if (!empty($t['parent_in_other_book']) && ($t['parent_title'] ?? '') !== '') {
|
||||
$hint = '(此任务的上级「' . (string)$t['parent_title']
|
||||
. '」在另一个分类里,网页端可以看到两级的完整关系)';
|
||||
$note = $note === '' ? $hint : $note . "\n\n" . $hint;
|
||||
}
|
||||
if ($note !== '') {
|
||||
$out[] = 'DESCRIPTION:' . self::escapeText($note);
|
||||
}
|
||||
|
||||
/* ---- 截止时间 ----
|
||||
@@ -168,10 +180,24 @@ final class Ics
|
||||
$out[] = 'CATEGORIES:' . self::escapeText($board);
|
||||
}
|
||||
|
||||
// ---- 父子关系(标准表达,但多数客户端不显示;不影响语义)----
|
||||
$parentId = (int)($t['parent_id'] ?? 0);
|
||||
/* ---- 父子关系 ----
|
||||
`RELATED-TO` 是 RFC 5545 §3.8.4.5 的标准属性,值是**父资源的 UID**。
|
||||
本模块的 UID 规范是 `task-<id>`,所以这里拼 `task-<parentId>`
|
||||
正好对上同一本里那个资源的 UID。
|
||||
|
||||
⚠️ 各客户端的支持度:
|
||||
· DAVx⁵ + Tasks.org:认,显示成缩进的子项
|
||||
· 苹果提醒事项:认自己的 parent 语义,对 RELATED-TO 支持有限
|
||||
· Thunderbird:完全不显示层级,两条并列
|
||||
这是客户端能力差异,服务端无法改变 —— 数据是正确的。 */
|
||||
if ($parentId > 0) {
|
||||
$out[] = 'RELATED-TO:task-' . $parentId;
|
||||
/* 展示用的补充属性:值 = 父标题。
|
||||
支持的客户端能直接显示"上级是谁",不支持的无视。
|
||||
用 X- 前缀是因为这是本服务特有的,不进标准语义。 */
|
||||
if (($t['parent_title'] ?? '') !== '') {
|
||||
$out[] = 'X-FAMILYBOOK-PARENT:' . self::escapeText((string)$t['parent_title']);
|
||||
}
|
||||
}
|
||||
|
||||
$out[] = 'END:VTODO';
|
||||
|
||||
Reference in New Issue
Block a user