requireLogin(); $nodeId = (int)($_GET['id'] ?? 0); $node = Access::node($nodeId); // 含 family_id / activity_id / activity_title view('nodes/attachments', [ 'node' => $node, 'attachments' => Attachments::forTarget(Attachments::TARGET_NODE, $nodeId), 'maxMb' => (int)Settings::get('photo_max_mb', '12'), 'driverLabel' => Storage::label(), 'pageTitle' => '节点照片:' . ($node['title'] ?: '无标题'), ]); } /** * 上传附件。 * * 一个请求里同时收两类东西: * files[] —— 普通 multipart 文件(照片原图、PDF、视频……) * cropped[] —— 前端裁好的图片,JSON 字符串 {"name":..,"data":"data:image/...;base64,.."} * 分开传是为了省流量:没裁的照片原图直传,裁过的才走 base64。 * * 真正的处理逻辑在 App\Uploads —— 节点表单里的附件走同一份代码, * 免得两边的 MIME 白名单/大小上限/孤儿文件回滚各写一套、慢慢走偏。 */ public function upload(): void { $this->requireLogin(); // 请求体超过 post_max_size 时 PHP 会把 $_POST 和 $_FILES 整个丢掉, // 紧接着的 csrf_check() 必然失败、报「请求已过期」,用户完全看不懂。 // 提前认出来,给一句能行动的话。 if (Uploads::requestTooLarge()) { $nodeId = (int)($_POST['node_id'] ?? $_GET['node_id'] ?? 0); flash('error', '一次上传的内容超过服务器上限(' . ini_get('post_max_size') . '),请分批上传。'); redirect('attachments/node', ['id' => $nodeId]); } csrf_check(); $nodeId = (int)($_POST['node_id'] ?? 0); $node = Access::node($nodeId); $uploaderId = (int)Auth::id(); $target = Attachments::TARGET_NODE; $files = Uploads::handleFiles($_FILES['files'] ?? [], $target, $nodeId, $uploaderId, 0); $crop = Uploads::handleCropped((string)($_POST['cropped'] ?? ''), $target, $nodeId, $uploaderId, $files['count']); $uploaded = $files['uploaded'] + $crop['uploaded']; $errors = array_merge($files['errors'], $crop['errors']); $count = $crop['count']; if ($count === 0 && $uploaded === 0) { flash('error', '没有选择任何文件'); redirect('attachments/node', ['id' => $nodeId]); } $msg = "成功上传 {$uploaded} 个文件(存到" . Storage::label() . ')'; if (!empty($errors)) { $msg .= ';失败 ' . count($errors) . ' 个:' . implode(';', array_slice($errors, 0, 5)); } if (Storage::ossMisconfigured()) { $msg .= '|注意:已选择 OSS 但配置不完整,本次实际存到了本站服务器'; } flash('error', $msg); redirect('attachments/node', ['id' => $nodeId], 'photos'); } public function destroy(): void { $this->requireLogin(); csrf_check(); $id = (int)($_POST['id'] ?? 0); $att = Attachments::find($id); if (!$att || $att['target_type'] !== Attachments::TARGET_NODE) { flash('error', '附件不存在'); redirect('activities/index'); } $node = Access::node((int)$att['target_id']); // 顺带做归属校验 Attachments::delete($id); flash('error', '照片已删除'); redirect('attachments/node', ['id' => (int)$node['id']], 'photos'); } private function requireLogin(): void { if (!Auth::check()) { redirect('auth/login'); } } }