From f927d7fb23965117acf9e8d0f6d66c5ba4c11487 Mon Sep 17 00:00:00 2001 From: laoyang Date: Thu, 22 Oct 2026 16:40:52 +0800 Subject: [PATCH] =?UTF-8?q?v79=20=E6=9C=8D=E5=8A=A1=E7=AB=AF=EF=BC=9A?= =?UTF-8?q?=E7=AB=AF=E4=B8=8A=E4=BB=BB=E5=8A=A1=E9=9D=A2=E6=9D=BF=E9=9C=80?= =?UTF-8?q?=E8=A6=81=E7=9A=84=E4=B8=89=E4=B8=AA=E5=8F=A3=E5=AD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 配合鸿蒙端 v79(任务面板 / 任务模块功能补齐),服务端只补三处, 其余全部复用既有路由。 ① GET /api/v1/tasks?view=boards —— 只回看板 + 分类元数据,**不带任务**。 端上原来是从已加载任务里反推看板,那样只能看到"有任务的看板": 用户刚建好、还没放任务的看板在端上永远选不到(表现为"网页能建, 端上挪不过去")。 ② POST /api/v1/tasks//attach(multipart 字段 file)与 DELETE /api/v1/tasks//attach?attachment_id= —— 都要先 requireWrite() 再 Task::canWrite(),两者都回整条任务(端上直接重画)。 ③ ApiHandler::taskOut() —— Task::find() 只 SELECT t.*,不含看板名/色 ⇒ 详情与各动作回包统一补 board_name / board_color_calc(7 个返回点)。 🔴 两条踩过的坑写进注释: · Api::dispatch() 只解析「资源/id/子动作」三段,附件 id 不能拼进路径; · DELETE 带 JSON body 时 Api::param 读不到(实测),id 必须走 query。 另新增 Attachment::removeFrom():删附件前先确认它确实挂在这条任务上, 拿别条任务的附件 id 来删自己的 → 404(已由 _fb_v79_api.py 钉死)。 验证:php -l 全过、e2e 1558/0、_fb_v79_api.py 26/0、已部署并冒烟。 --- app/ApiHandler.php | 105 ++++++++++++++++++++++++++++++++++++++++++--- app/Attachment.php | 23 ++++++++++ 2 files changed, 123 insertions(+), 5 deletions(-) diff --git a/app/ApiHandler.php b/app/ApiHandler.php index 3612ba3..89353d9 100644 --- a/app/ApiHandler.php +++ b/app/ApiHandler.php @@ -1712,6 +1712,28 @@ final class ApiHandler 任务 ═══════════════════════════════════════════════════════════════ */ + /** + * 一条任务 → 给 App 的形状。 + * + * 🔴 v79 为什么要包这一层:`Task::find()` 只 SELECT `t.*`,**不含看板名/看板色** + * (网页端是在模板里再查一次)。端上面板要显示"这条在哪个看板"、 + * 还要拿它的颜色去画那颗小圆点 —— 让端上为了两个字段再打一次 + * `/tasks?view=boards` 是多余的一跳,就在服务端补齐。 + * `board_color_calc` 与首页任务用的是**同一个**派生口径, + * 否则同一个看板在首页和详情里是两种颜色(用户能一眼看出来)。 + */ + private static function taskOut(int $fam, int $id, int $pid): array + { + $t = Task::find($fam, $id, $pid); + if (!$t) { + return []; + } + $bid = (int)($t['board_id'] ?? 0); + $t['board_name'] = Task::boardName($fam, $bid > 0 ? $bid : null); + $t['board_color_calc'] = Task::boardColorById($fam, $bid > 0 ? $bid : null); + return $t; + } + private function tasks(int $id, string $sub, string $method): void { [$fam, $pid] = Api::me(); @@ -1724,12 +1746,12 @@ final class ApiHandler if ($sub === 'status' && ($method === 'POST' || $method === 'PATCH')) { Api::requireWrite(); Task::setStatus($fam, $id, Api::str('status', 'todo'), Api::int('position')); - Api::ok(Task::find($fam, $id, $pid)); + Api::ok(self::taskOut($fam, $id, $pid)); } if ($sub === 'move' && $method === 'POST') { Api::requireWrite(); Task::moveToBoard($fam, $id, Api::int('board_id'), Api::int('before'), $pid); - Api::ok(Task::find($fam, $id, $pid)); + Api::ok(self::taskOut($fam, $id, $pid)); } if ($sub === 'comments') { if ($method === 'GET') { @@ -1743,7 +1765,53 @@ final class ApiHandler } } - if ($method === 'GET') { Api::ok($t); } + /* ═══════════════════════════════════════════════════════════ + v79(用户 2026-10-22):「鸿蒙端的功能,也需要同样适配…… + 任务模块的功能需要完善。」 + ⇒ 端上要对齐网页 v78 的「就地面板」,而面板里的**加附件**一直没有 API + (网页端走的是 `tasks/attach_add` 这条表单路由)。 + ──────────────────────────────────────────────────────────── + POST /api/v1/tasks//attach multipart,字段名 `file` + DELETE /api/v1/tasks//attach 参数 `attachment_id` + ⚠️ 路由只解析「资源/id/子动作」**三段**(`Api::dispatch`),没有第四段 + ⇒ 删附件**必须**把 id 放参数里。写成 `/attach/` 的话, + `Api::dispatch` 只会给到 `$sub='attach'`,aid 直接丢掉(静默 404)。 + ⚠️ 两个分支都回**整条任务**(`Task::find`,含 attachs):端上拿到就能 + 直接重画面板,不用再补一次 GET。 + ⚠️ 权限除了 `requireWrite()` 还要 `Task::canWrite()`:前者只说明 + "这个账号不是只读访客",后者才说明"这条任务归我管"。 + ═══════════════════════════════════════════════════════════ */ + if ($sub === 'attach') { + Api::requireWrite(); + if (!Task::canWrite($t, $pid)) { + Api::fail('这项任务不能改。', 'forbidden', 403); + } + if ($method === 'POST') { + $file = $_FILES['file'] ?? null; + if (!is_array($file)) { + Api::fail('没有收到文件。', 'unprocessable', 422); + } + $r = \App\Attachment::save($fam, Task::ATTACH, $id, $file, $pid); + if (empty($r['ok'])) { + $msg = (string)($r['error'] ?? ''); + Api::fail($msg !== '' ? $msg : '文件没存下来', 'unprocessable', 422); + } + Api::ok(self::taskOut($fam, $id, $pid), 201); + } + if ($method === 'DELETE') { + $aid = Api::int('attachment_id'); + if ($aid <= 0) { + Api::fail('要指定删哪个附件。', 'bad_request', 400); + } + if (!\App\Attachment::removeFrom(Task::ATTACH, $id, $aid)) { + Api::notFound('这个附件'); + } + Api::ok(self::taskOut($fam, $id, $pid)); + } + Api::fail('不支持的方法。', 'method_not_allowed', 405); + } + + if ($method === 'GET') { Api::ok(self::taskOut($fam, $id, $pid)); } if ($method === 'PATCH' || $method === 'PUT') { Api::requireWrite(); Task::update($fam, $id, [ @@ -1757,7 +1825,7 @@ final class ApiHandler 'board_id' => Api::int('board_id', (int)($t['board_id'] ?? 0)), 'status' => (string)$t['status'], // 状态只走 /status 子动作 ]); - Api::ok(Task::find($fam, $id, $pid)); + Api::ok(self::taskOut($fam, $id, $pid)); } if ($method === 'DELETE') { Api::requireWrite(); @@ -1787,7 +1855,7 @@ final class ApiHandler foreach (Api::people(Api::ints('shares'), $fam) as $sp) { Task::setShare($newId, $sp, 'write'); } - Api::ok(Task::find($fam, $newId, $pid), 201); + Api::ok(self::taskOut($fam, $newId, $pid), 201); } // ---- 列表:三种视图 ---- @@ -1801,6 +1869,33 @@ final class ApiHandler if ($view === 'done') { Api::ok(Task::doneList($fam, $pid, Api::int('category_id'), Api::str('keyword'))); } + /* v79:**只给元数据**(看板 + 分类),不带任务。 + 🔴 为什么需要它:端上「挪看板 / 选分类」要的是"**全部**看板", + 而看板视图(下面那段)每列都塞满了任务,为了拿 5 个名字去解析几百条任务 + 很浪费;更关键的是——**空看板在别的视图里根本不存在** + (端上原来只能从已加载任务里反推看板 ⇒ 空看板永远选不到)。 + 网页端同一份数据来自 `Task::boards()` / `Task::categories()`。 */ + if ($view === 'boards') { + $bs = []; + foreach (Task::boards($fam) as $b) { + $bs[] = [ + 'id' => (int)$b['id'], + 'name' => (string)$b['name'], + 'color' => Task::boardColor($b['color'] ?? null, (string)$b['name']), + 'open' => (int)($b['open_count'] ?? 0), + ]; + } + $cs = []; + foreach (Task::categories($fam) as $c) { + $cs[] = ['id' => (int)$c['id'], 'name' => (string)$c['name']]; + } + Api::ok([ + 'boards' => $bs, + 'categories' => $cs, + 'status' => Task::STATUS, + 'repeat' => Task::REPEAT, + ]); + } // 看板视图:把"列"与"任务"一起给出来(App 一次请求就够画整屏) $boards = Task::boards($fam); /* ⚠️ `Task::boardColumns()` 返回的是**包了一层的数组** diff --git a/app/Attachment.php b/app/Attachment.php index d7cfb37..3e82c66 100644 --- a/app/Attachment.php +++ b/app/Attachment.php @@ -128,6 +128,29 @@ final class Attachment } } + /** + * 删**某个宿主名下**的一颗附件(v79,App API 用)。 + * + * 🔴 为什么不能直接用 `remove($id)`:那个只认附件 id,谁传一个别的任务的 + * 附件 id 过来都能删掉(越权)。这里把 owner 一起放进 WHERE, + * 「不是这条任务的」一律返回 false,调用方据此回 404。 + */ + public static function removeFrom(string $type, int $targetId, int $id): bool + { + if ($id <= 0) { + return false; + } + $st = Database::get()->prepare( + 'SELECT COUNT(*) FROM attachments WHERE id = ? AND target_type = ? AND target_id = ?' + ); + $st->execute([$id, $type, $targetId]); + if ((int)$st->fetchColumn() <= 0) { + return false; + } + self::remove($id); + return true; + } + /** * 这个附件是「哪一类」—— 决定页面上画缩略图还是画图标、点开是预览还是下载。 *