prepare('SELECT COALESCE(SUM(size),0) AS s FROM soon_files WHERE user_id = :u AND deleted_at IS NULL'); $stmt->execute(['u' => $userId]); return (int)$stmt->fetchColumn(); } public static function fileCount(int $userId): int { $stmt = Db::pdo()->prepare('SELECT COUNT(*) FROM soon_files WHERE user_id = :u AND deleted_at IS NULL'); $stmt->execute(['u' => $userId]); return (int)$stmt->fetchColumn(); } public static function list(int $userId, int $limit, int $offset): array { $stmt = Db::pdo()->prepare( 'SELECT id, name, size, version, updated_at, created_at ' . 'FROM soon_files WHERE user_id = :u AND deleted_at IS NULL ' . 'ORDER BY updated_at DESC LIMIT :lim OFFSET :off' ); $stmt->bindValue('u', $userId, \PDO::PARAM_INT); $stmt->bindValue('lim', $limit, \PDO::PARAM_INT); $stmt->bindValue('off', $offset, \PDO::PARAM_INT); $stmt->execute(); return $stmt->fetchAll(); } public static function create(int $userId, string $name, string $json): array { $maxFiles = MembershipService::maxFilesLimit($userId); if (self::fileCount($userId) >= $maxFiles) { Json::fail('file_limit_exceeded', '已达文件数量上限(' . $maxFiles . ' 个),请清理文件或续订', 413); } $size = strlen($json); $quota = self::quotaBytes($userId); $used = self::usedBytes($userId); if ($quota > 0 && $used + $size > $quota) { Json::fail('quota_exceeded', '存储空间已满,请清理文件或续订', 413); } $now = date('Y-m-d H:i:s'); $stmt = Db::pdo()->prepare( 'INSERT INTO soon_files (user_id, name, json, size, version, created_at, updated_at) ' . 'VALUES (:u, :n, :j, :s, 1, :created_at, :updated_at)' ); $stmt->execute(['u' => $userId, 'n' => $name, 'j' => $json, 's' => $size, 'created_at' => $now, 'updated_at' => $now]); $id = (int)Db::pdo()->lastInsertId(); return ['id' => $id, 'name' => $name, 'size' => $size, 'version' => 1, 'updated_at' => $now]; } public static function update(int $userId, int $id, string $name, string $json, ?int $expectedVersion): array { $newSize = strlen($json); $quota = self::quotaBytes($userId); $used = self::usedBytes($userId); $pdo = Db::pdo(); $pdo->beginTransaction(); try { $stmt = $pdo->prepare('SELECT * FROM soon_files WHERE id = :id AND user_id = :u AND deleted_at IS NULL FOR UPDATE'); $stmt->execute(['id' => $id, 'u' => $userId]); $row = $stmt->fetch(); if (!$row) { throw new \RuntimeException('not_found'); } if ($expectedVersion !== null && (int)$row['version'] !== $expectedVersion) { throw new \RuntimeException('version_conflict'); } $oldSize = (int)$row['size']; if ($quota > 0 && ($used - $oldSize + $newSize) > $quota) { throw new \RuntimeException('quota_exceeded'); } $now = date('Y-m-d H:i:s'); $newVersion = (int)$row['version'] + 1; $upd = $pdo->prepare( 'UPDATE soon_files SET name = :n, json = :j, size = :s, version = :v, updated_at = :ts ' . 'WHERE id = :id AND version = :cv' ); $upd->execute([ 'n' => $name, 'j' => $json, 's' => $newSize, 'v' => $newVersion, 'ts' => $now, 'id' => $id, 'cv' => (int)$row['version'], ]); if ($upd->rowCount() === 0) { throw new \RuntimeException('version_conflict'); } $pdo->commit(); return ['id' => $id, 'name' => $name, 'size' => $newSize, 'version' => $newVersion, 'updated_at' => $now]; } catch (\RuntimeException $e) { if ($pdo->inTransaction()) $pdo->rollBack(); if ($e->getMessage() === 'not_found') Json::fail('not_found', '文件不存在', 404); if ($e->getMessage() === 'version_conflict') Json::fail('conflict', '版本冲突,请刷新后重试', 409); if ($e->getMessage() === 'quota_exceeded') Json::fail('quota_exceeded', '存储空间已满', 413); throw $e; } catch (\Throwable $e) { if ($pdo->inTransaction()) $pdo->rollBack(); throw $e; } } public static function softDelete(int $userId, int $id): void { $stmt = Db::pdo()->prepare('UPDATE soon_files SET deleted_at = :ts WHERE id = :id AND user_id = :u'); $stmt->execute(['ts' => date('Y-m-d H:i:s'), 'id' => $id, 'u' => $userId]); } public static function fetch(int $userId, int $id): array { $stmt = Db::pdo()->prepare('SELECT * FROM soon_files WHERE id = :id AND user_id = :u AND deleted_at IS NULL'); $stmt->execute(['id' => $id, 'u' => $userId]); $row = $stmt->fetch(); if (!$row) { Json::fail('not_found', '文件不存在', 404); } return $row; } }