diff --git a/app/ApiHandler.php b/app/ApiHandler.php index 7ad6478..ab3ebb7 100644 --- a/app/ApiHandler.php +++ b/app/ApiHandler.php @@ -1821,9 +1821,39 @@ final class ApiHandler } if ($method === 'POST') { Api::requireWrite(); + /* 🔴 #77g(用户 2026-11-05):「留言是可以需要添加附件的。」 + + ⇒ 支持 **multipart/form-data**(带 `file`)与原来的 + **JSON / 表单**(只有 content)两种提交方式。 + 前端有附件就走 multipart,没附件走原来的路径 —— 不破坏旧调用方。 + + ⚠️ **允许"只发附件不写字"**:拍张照片直接发是常见用法。 + 网页端 `tasks/comment_add` 早有同样的处理(占位文案「(附件)」), + 这里与它保持同一口径。 + ⚠️ 附件走 `Attachment::save()`,`kind` 用 `'task_comment'` + —— 与网页端 `attach_save_files(..., 'task_comment', ...)` + 是同一个归属类型,否则同一条留言的附件在两端互相看不见。 + ⚠️ 一个评论先建(拿到 cid),再存附件 —— 顺序不能反: + `Attachment::save()` 要 `owner_id`,而 cid 是插入后才有的。 */ $txt = Api::str('content', '', 4000); - if ($txt === '') { Api::fail('评论不能是空的。', 'unprocessable', 422); } - Api::ok(['id' => Task::addComment($id, $pid, $txt)], 201); + $file = $_FILES['file'] ?? null; + $hasFile = is_array($file) + && trim((string)($file['name'] ?? '')) !== '' + && (int)($file['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_OK; + if ($txt === '' && !$hasFile) { + Api::fail('评论不能是空的。', 'unprocessable', 422); + } + if ($txt === '' && $hasFile) { + $txt = '(附件)'; + } + $cid = Task::addComment($id, $pid, $txt); + if ($hasFile) { + $r = \App\Attachment::save($fam, 'task_comment', $cid, $file, $pid); + if (empty($r['ok'])) { + Api::fail('评论发出去了,但附件没存下来。', 'unprocessable', 422); + } + } + Api::ok(['id' => $cid], 201); } } @@ -1913,7 +1943,15 @@ final class ApiHandler 'repeat_rule' => Api::str('repeat_rule', (string)($t['repeat_rule'] ?? '')), 'parent_id' => Api::int('parent_id', (int)($t['parent_id'] ?? 0)), 'assignee_id' => Api::int('assignee_id', (int)($t['assignee_id'] ?? 0)), - 'category_id' => Api::int('category_id', (int)($t['category_id'] ?? 0)), + /* 🔴 #84:原来写的是 `Api::int('category_id', (int)($t['category_id'] ?? 0))` + —— 但 `Api::int()` 是**读请求参数**的,它先看 $_REQUEST 里有没有 + `category_id`,没有才用第二个参数当默认值。 + PATCH 时用户会传这个字段 ⇒ 走到默认值时值是对的; + 但 **GET 时没有这个请求参数** ⇒ 拿到的仍然是默认值 —— + 恰好默认值本身是从 $t 算的,所以"看起来对"。 + 真正的问题是读起来极容易误判成"已经取到表里的值了"。 + ⇒ 直接写表里的值,不绕 Api::int。 */ + 'category_id' => (int)($t['category_id'] ?? 0), 'board_id' => Api::int('board_id', (int)($t['board_id'] ?? 0)), 'status' => (string)$t['status'], // 状态只走 /status 子动作 ]); diff --git a/app/Task.php b/app/Task.php index d3af753..194e369 100644 --- a/app/Task.php +++ b/app/Task.php @@ -557,12 +557,22 @@ final class Task // 抛 PDOException: Unknown column 'p.name',而它又被 PHP 以 // "200 + Fatal error 正文"吐出来,看着像页面正常返回。 // 改 SQL 之后一定要用「真实库表结构」校验列名(见 sql-schema-column-check)。 + /* 🔴 #84(用户 2026-11-05):「鸿蒙端的分类那个地方没带进来。 + 明明是有分类的,但是它却显示的是未分类。」 + + ⇒ 补 `LEFT JOIN task_categories`。 + 原来这条 SQL **没有 JOIN 分类表**,虽然 `t.*` 里有 `category_id`, + 但没有 `category_name` —— 端上 `category: s(d.category_name)` + 永远拿到空串,于是显示成「未分类」。 + (`doneList` / 详情页那几条 SQL 都有这个 JOIN,只有 find() 漏了。) */ 'SELECT t.*, p.title AS parent_title, + c.name AS category_name, a.name AS assignee_name, a.avatar AS assignee_avatar, a.avatar_color AS assignee_color, na.nickname AS assignee_nick, cr.name AS creator_name, cr.avatar AS creator_avatar, cr.avatar_color AS creator_color, nc.nickname AS creator_nick FROM tasks t + LEFT JOIN task_categories c ON c.id = t.category_id LEFT JOIN tasks p ON p.id = t.parent_id LEFT JOIN persons a ON a.id = t.assignee_id LEFT JOIN nicknames na ON na.target_id = t.assignee_id AND na.viewer_id = ?