table . '.json'; } private function read(): array { $f = $this->file(); if (!is_file($f)) return []; $d = json_decode(file_get_contents($f), true); return is_array($d) ? $d : []; } private function write(array $rows): void { $rows = $this->sanitizeUtf8($rows); // JSON_INVALID_UTF8_SUBSTITUTE (PHP 7.2+) 保证即使存在非法 UTF-8 也不会让 json_encode 返回 false, // 避免 file_put_contents(false) 把整个数据文件清空为 0 字节(灾难性数据丢失)。 $json = json_encode($rows, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_INVALID_UTF8_SUBSTITUTE); if ($json === false || $json === '') { error_log('[Model::write] json_encode failed for ' . $this->table . '; aborting write to avoid data loss'); return; } $f = $this->file(); $dir = dirname($f); if (!is_dir($dir)) { @mkdir($dir, 0755, true); } // 写入失败(最常见:服务器目录/文件权限不足,PHP 进程无写权限)必须记录日志, // 否则会“假成功”——页面跳转回去、用户以为保存了,数据却没变。 $bytes = @file_put_contents($f, $json); if ($bytes === false) { $err = error_get_last(); error_log('[Model::write] FAILED to write ' . $f . ' — ' . ($err['message'] ?? 'unknown error') . ' | 请检查目录/文件所有者是否为 PHP 运行用户(宝塔通常是 www)并赋予写权限'); } } /** 递归将数组中的字符串修复为合法 UTF-8,剔除非法字节序列 */ private function sanitizeUtf8($v) { if (is_array($v)) { return array_map([$this, 'sanitizeUtf8'], $v); } if (is_string($v) && !mb_check_encoding($v, 'UTF-8')) { return mb_convert_encoding($v, 'UTF-8', 'UTF-8'); } return $v; } private function sort(array &$rows): void { $col = $this->orderBy; usort($rows, function ($a, $b) use ($col) { $va = $a[$col] ?? 0; $vb = $b[$col] ?? 0; return $va <=> $vb; }); } /* ---------- 通用 CRUD ---------- */ public function all(): array { if (Db::driver() === 'mysql') { return Db::query("SELECT * FROM `{$this->table}` ORDER BY `{$this->orderBy}` ASC")->fetchAll(); } $rows = $this->read(); $this->sort($rows); return $rows; } public function find($id) { if (Db::driver() === 'mysql') { return Db::query("SELECT * FROM `{$this->table}` WHERE `{$this->pk}`=?", [$id])->fetch() ?: null; } foreach ($this->read() as $r) if (($r[$this->pk] ?? null) == $id) return $r; return null; } public function where(string $col, $val) { if (Db::driver() === 'mysql') { return Db::query("SELECT * FROM `{$this->table}` WHERE `{$col}`=?", [$val])->fetch() ?: null; } foreach ($this->read() as $r) if (($r[$col] ?? null) == $val) return $r; return null; } public function whereAll(string $col, $val): array { if (Db::driver() === 'mysql') { return Db::query("SELECT * FROM `{$this->table}` WHERE `{$col}`=? ORDER BY `{$this->orderBy}` ASC", [$val])->fetchAll(); } $out = []; foreach ($this->read() as $r) if (($r[$col] ?? null) == $val) $out[] = $r; $this->sort($out); return $out; } public function insert(array $data) { if (Db::driver() === 'mysql') { $cols = array_keys($data); $sql = "INSERT INTO `{$this->table}` (`" . implode('`,`', $cols) . "`) VALUES (" . implode(',', array_fill(0, count($cols), '?')) . ")"; Db::query($sql, array_values($data)); return Db::pdo()->lastInsertId(); } $rows = $this->read(); $id = $rows ? (max(array_column($rows, $this->pk)) + 1) : 1; $data[$this->pk] = $id; $rows[] = $data; $this->write($rows); return $id; } public function update($id, array $data): void { if (Db::driver() === 'mysql') { $cols = array_keys($data); $sql = "UPDATE `{$this->table}` SET `" . implode('`=?,`', $cols) . "`=? WHERE `{$this->pk}`=?"; Db::query($sql, array_merge(array_values($data), [$id])); return; } $rows = $this->read(); foreach ($rows as &$r) { if (($r[$this->pk] ?? null) == $id) { $r = array_merge($r, $data); break; } } $this->write($rows); } public function delete($id): void { if (Db::driver() === 'mysql') { Db::query("DELETE FROM `{$this->table}` WHERE `{$this->pk}`=?", [$id]); return; } $rows = array_filter($this->read(), fn($r) => ($r[$this->pk] ?? null) != $id); $this->write(array_values($rows)); } /** 按任意列批量删除(用于主从表级联删除从表) */ public function deleteRaw(string $col, $val): void { if (Db::driver() === 'mysql') { Db::query("DELETE FROM `{$this->table}` WHERE `{$col}`=?", [$val]); return; } $rows = array_filter($this->read(), fn($r) => ($r[$col] ?? null) != $val); $this->write(array_values($rows)); } public function count(): int { return count($this->all()); } }