table . '|' . $method . '|' . md5(serialize($args)); } /** 读取缓存;未命中返回 null(用 array_key_exists 区分「未缓存」与「缓存了空数组」) */ private function cacheGet(string $k) { return array_key_exists($k, self::$reqCache) ? self::$reqCache[$k] : null; } private function cachePut(string $k, $v): void { self::$reqCache[$k] = $v; } /** 任意写操作后清空请求内缓存,避免同请求内读到脏数据(CMS 读多写少,刷新成本可忽略) */ private function cacheFlush(): void { self::$reqCache = []; } /* ---------- 文件模式 ---------- */ private function file(): string { return Db::fileDir() . '/' . $this->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 { $k = $this->cacheKey('all'); $hit = $this->cacheGet($k); if ($hit !== null) return $hit; if (Db::driver() === 'mysql') { $rows = Db::query("SELECT * FROM `{$this->table}` ORDER BY `{$this->orderBy}` ASC")->fetchAll(); } else { $rows = $this->read(); $this->sort($rows); } $this->cachePut($k, $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) { $this->cacheFlush(); 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 { $this->cacheFlush(); 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 { $this->cacheFlush(); 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 { $this->cacheFlush(); 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)); } /** * 计数:MySQL 下走 COUNT(*) 聚合(避免 SELECT * 全表拉回再 count); * 可选按某列过滤。文件模式下回退为本地计数。 * @param string|null $col 过滤列(null 表示全表计数) * @param mixed $val 过滤值 */ public function count(string $col = null, $val = null): int { if (Db::driver() === 'mysql') { if ($col === null) { return (int) Db::query("SELECT COUNT(*) FROM `{$this->table}`")->fetchColumn(); } return (int) Db::query("SELECT COUNT(*) FROM `{$this->table}` WHERE `{$col}`=?", [$val])->fetchColumn(); } $rows = $this->all(); if ($col === null) return count($rows); $n = 0; foreach ($rows as $r) { if (($r[$col] ?? null) == $val) $n++; } return $n; } /** * 取某列等于 $val 的前 $limit 条。 * MySQL 下带 LIMIT(索引友好);文件模式用 whereAll + array_slice 兜底。 * 用于首页精选 / 列表分页等高频只读场景。 */ public function whereLimit(string $col, $val, int $limit): array { if (Db::driver() === 'mysql') { return Db::query( "SELECT * FROM `{$this->table}` WHERE `{$col}`=? ORDER BY `{$this->orderBy}` ASC LIMIT ?", [$val, $limit] )->fetchAll(); } return array_slice($this->whereAll($col, $val), 0, $limit); } }