diff --git a/src/Controllers/NodeController.php b/src/Controllers/NodeController.php index ccb06ab..78d3d02 100644 --- a/src/Controllers/NodeController.php +++ b/src/Controllers/NodeController.php @@ -62,18 +62,25 @@ class NodeController $data = $input['data']; // 交通信息只留 trip_number(车次/航班号)—— 航班号本身就隐含航空公司, // 座位号/登机口对「一趟车多人同行」没有意义,所以这些字段不再收集。 + // + // ⚠️ 历史列(line_name / carrier / seat_info / gate)仍然**留在 SQL 里**, + // 只是值固定为 null(collectInput 里不再从表单取)。原因不是「要写它们」, + // 而是 $data 里带着这几个键:Database 用的是原生预处理 + // (ATTR_EMULATE_PREPARES => false),SQL 里少了占位符就会抛 + // SQLSTATE[HY093](参数个数对不上)→ 新建/编辑节点整条挂掉。 + // 顺带的好处是:编辑一次旧节点就把它身上的历史脏数据清干净。 $sql = 'INSERT INTO trip_nodes (activity_id, node_type_id, node_category, original_plan_node_id, start_time, end_time, duration_minutes, start_location, end_location, title, description, travel_mode_id, vehicle_id, - trip_number, status, cancel_reason) + line_name, carrier, trip_number, seat_info, gate, status, cancel_reason) VALUES (:activity_id, :node_type_id, :node_category, :original_plan_node_id, :start_time, :end_time, :duration_minutes, :start_location, :end_location, :title, :description, :travel_mode_id, :vehicle_id, - :trip_number, :status, :cancel_reason)'; + :line_name, :carrier, :trip_number, :seat_info, :gate, :status, :cancel_reason)'; $stmt = $db->prepare($sql); $stmt->execute($data); @@ -103,13 +110,15 @@ class NodeController unset($data['activity_id'], $data['node_category'], $data['original_plan_node_id']); $data['id'] = (int)$node['id']; + // 同样必须保留历史列的占位符 —— 见 store() 里的说明(原生预处理不接受多余键) $sql = 'UPDATE trip_nodes SET node_type_id = :node_type_id, start_time = :start_time, end_time = :end_time, duration_minutes = :duration_minutes, start_location = :start_location, end_location = :end_location, title = :title, description = :description, travel_mode_id = :travel_mode_id, vehicle_id = :vehicle_id, - trip_number = :trip_number, status = :status, cancel_reason = :cancel_reason + line_name = :line_name, carrier = :carrier, trip_number = :trip_number, + seat_info = :seat_info, gate = :gate, status = :status, cancel_reason = :cancel_reason WHERE id = :id'; $stmt = $db->prepare($sql); $stmt->execute($data); diff --git a/src/Mime.php b/src/Mime.php index c896255..4d343b1 100644 --- a/src/Mime.php +++ b/src/Mime.php @@ -9,10 +9,13 @@ namespace App; * 为什么不用 fileinfo 扩展:线上同时装了 PHP 7.4 与 8.1,**7.4 没装这个扩展**, * 一旦站点切到 7.4,调用它的探测函数会直接致命错误(整页 500)。 * (部署脚本里有条静态断言要求全项目不出现该扩展的函数名,这条注释刻意绕开写法。) - * 这里改成三层兜底,纯标准库实现: + * 这里改成三层判定,纯标准库实现: * 1) getimagesize() —— 图片最可靠,还能顺带拿到宽高; - * 2) 魔数嗅探 —— PDF / ZIP 系(docx/xlsx)/ MP4 / MOV / HEIC 等; - * 3) 扩展名映射 —— 最后兜底。 + * 2) 魔数嗅探 —— PDF / ZIP 系(docx/xlsx)/ 老式 OLE(doc/xls)/ MP4 / MOV / HEIC 等; + * 3) 扩展名 —— **只用来「细化」内容已确认的类型,不能无中生有**。 + * 例:docx/xlsx 都是 zip 容器,魔数只能判到 application/zip, + * 这时才用扩展名细分成 OOXML;而一个内容是纯文本、只是名字叫 .jpg 的文件 + * 在第 2 层就得出「不是受支持的格式」→ 直接拒收(否则改个扩展名就能绕过类型白名单)。 */ final class Mime { @@ -76,7 +79,7 @@ final class Mime } $head = self::readHead($path, 32); - $mime = self::sniffMagic($head) ?? self::mimeFromName($originalName); + $mime = self::refineByMagic(self::sniffMagic($head), $originalName); return ['mime' => $mime, 'width' => null, 'height' => null, 'is_image' => $mime !== null && self::isImageMime($mime)]; } @@ -106,13 +109,47 @@ final class Mime return ['mime' => $mime, 'width' => null, 'height' => null, 'is_image' => $mime !== null && self::isImageMime($mime)]; } - /** 从扩展名推 MIME(第 3 层) */ + /** 从扩展名推 MIME(只用于「细化」,见 refineByMagic) */ public static function mimeFromName(string $name): ?string { $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION)); return self::EXT_TO_MIME[$ext] ?? null; } + /** + * 用扩展名「细化」魔数结果 —— 扩展名永远不能把 null 变成具体类型。 + * + * 只有这几种情况允许覆盖: + * · 魔数是 application/zip(docx/xlsx 都是 zip 容器)→ 按扩展名细分到 OOXML; + * · 魔数是老式 OLE(\xD0\xCF\x11\xE0)→ 按扩展名区分 doc / xls。 + * 其它一律以内容为准:认不出来就是认不出来,交给调用方拒绝。 + */ + private static function refineByMagic(?string $mime, string $originalName): ?string + { + if ($mime === null || $originalName === '') { + return $mime; + } + $byName = self::mimeFromName($originalName); + if ($byName === null) { + return $mime; + } + + if ($mime === 'application/zip') { + if (strpos($byName, 'application/vnd.openxmlformats') === 0) { + return $byName; // .docx / .xlsx + } + return $mime; + } + if ($mime === 'application/vnd.ms-excel') { + // 老式 doc / xls 都是 OLE2 容器,魔数分不出来,靠扩展名区分 + if ($byName === 'application/msword' || $byName === 'application/vnd.ms-excel') { + return $byName; + } + return $mime; + } + return $mime; + } + /** 魔数嗅探(第 2 层) */ private static function sniffMagic(string $head): ?string {