['type' => 'select', 'options' => ['xiaohongshu', 'douyin'], 'default' => 'xiaohongshu'], 'fontSize' => ['type' => 'number', 'default' => 14, 'min' => 10, 'max' => 24], 'watermark' => ['type' => 'text', 'default' => ''], 'pageAlignments' => ['type' => 'json', 'default' => '{}'], ]; } /** * 验证配置合法性 */ public function validateConfig(array $config): bool { $fields = $this->getConfigFields(); if (isset($config['size']) && !in_array($config['size'], $fields['size']['options'])) { return false; } if (isset($config['fontSize'])) { $size = intval($config['fontSize']); if ($size < $fields['fontSize']['min'] || $size > $fields['fontSize']['max']) { return false; } } // watermark 是文本类型,无需特殊验证 // pageAlignments 是 JSON 类型,存储时由框架自动序列化 return true; } /** * 执行输出处理 - 服务端接收base64数据,保存文件,创建记录 * 注意:HTML渲染在前端完成(html2canvas),服务端只做文件保存和记录创建 */ public function process(Post $post, array $config): array { return []; } /** * 返回预览HTML - 预览在前端完成,服务端返回空 */ public function getPreview(Post $post, array $config): string { return ''; } /** * 保存单页图片 * @param int $outputId 输出记录ID * @param int $page 页码(从1开始) * @param string $imageData base64编码的图片数据 * @return PostOutputFile|false */ public function savePageImage(int $outputId, int $page, string $imageData) { // 保留原始base64用于存储 $rawBase64 = str_replace('data:image/jpeg;base64,', '', $imageData); $rawBase64 = str_replace('data:image/png;base64,', '', $rawBase64); $rawBase64 = str_replace(' ', '+', $rawBase64); $decoded = base64_decode($rawBase64); if ($decoded === false) { return false; } $imageInfo = @getimagesizefromstring($decoded); $width = is_array($imageInfo) ? $imageInfo[0] : 0; $height = is_array($imageInfo) ? $imageInfo[1] : 0; $fileRecord = PostOutputFile::create([ 'output_id' => $outputId, 'page' => $page, 'file_path' => '', 'file_url' => '', 'file_size' => strlen($decoded), 'width' => $width, 'height' => $height, 'image_data' => $rawBase64, ]); return $fileRecord; } /** * 保存长图 * @param int $outputId 输出记录ID * @param string $imageData base64编码的长图数据 * @return PostOutputFile|false */ public function saveLongImage(int $outputId, string $imageData) { $rawBase64 = str_replace('data:image/jpeg;base64,', '', $imageData); $rawBase64 = str_replace('data:image/png;base64,', '', $rawBase64); $rawBase64 = str_replace(' ', '+', $rawBase64); $decoded = base64_decode($rawBase64); if ($decoded === false) { return false; } $imageInfo = @getimagesizefromstring($decoded); $width = is_array($imageInfo) ? $imageInfo[0] : 0; $height = is_array($imageInfo) ? $imageInfo[1] : 0; // 先删除该output_id下已有的长图(page=0) PostOutputFile::where('output_id', $outputId) ->where('page', 0) ->delete(); $fileRecord = PostOutputFile::create([ 'output_id' => $outputId, 'page' => 0, 'file_path' => '', 'file_url' => '', 'file_size' => strlen($decoded), 'width' => $width, 'height' => $height, 'image_data' => $rawBase64, ]); return $fileRecord; } /** * 打包某条输出记录的所有图片为ZIP * @param int $outputId * @return string ZIP文件路径 * @throws \Exception */ public function createZip(int $outputId): string { $output = PostOutput::find($outputId); if (!$output) { throw new \Exception('输出记录不存在'); } $files = PostOutputFile::where('output_id', $outputId) ->order('page', 'asc') ->select(); if ($files->isEmpty()) { throw new \Exception('没有可下载的文件'); } $tempDir = App::getRuntimePath() . 'temp'; if (!is_dir($tempDir)) { mkdir($tempDir, 0777, true); } $zipFileName = 'post_output_' . $outputId . '.zip'; $zipPath = $tempDir . '/' . $zipFileName; $tempFiles = []; try { $zip = new \ZipArchive(); if ($zip->open($zipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) { throw new \Exception('无法创建ZIP文件'); } foreach ($files as $file) { $pageName = str_pad((string) $file->page, 2, '0', STR_PAD_LEFT) . '.jpg'; if (!empty($file->image_data)) { $decoded = base64_decode($file->image_data); if ($decoded !== false) { $tempFilePath = $tempDir . '/page_' . $file->page . '_' . $outputId . '.jpg'; file_put_contents($tempFilePath, $decoded); $tempFiles[] = $tempFilePath; $zip->addFile($tempFilePath, $pageName); } } else { $filePath = App::getRootPath() . '/public' . $file->file_path; if (file_exists($filePath)) { $zip->addFile($filePath, $pageName); } } } $zip->close(); } finally { foreach ($tempFiles as $tempFile) { if (file_exists($tempFile)) { @unlink($tempFile); } } } return $zipPath; } /** * 删除输出记录及所有关联文件 * @param int $outputId * @return bool */ public function deleteOutput(int $outputId): bool { $files = PostOutputFile::where('output_id', $outputId)->select(); foreach ($files as $file) { $filePath = App::getRootPath() . '/public' . $file->file_path; if (file_exists($filePath)) { @unlink($filePath); } } PostOutputFile::where('output_id', $outputId)->delete(); PostOutput::destroy($outputId); return true; } /** * 创建输出记录 * @param int $postId 文章ID * @param array $config 配置 * @param int $adminId 管理员ID * @return PostOutput */ public function createOutput(int $postId, array $config, int $adminId): PostOutput { return PostOutput::create([ 'post_id' => $postId, 'output_type' => 'phone_image', 'config' => $config, 'status' => PostOutput::STATUS_PROCESSING, 'page_count' => 0, 'admin_id' => $adminId, ]); } /** * 完成输出记录 - 更新状态和页数 */ public function completeOutput(int $outputId, int $pageCount): bool { return PostOutput::where('id', $outputId)->update([ 'status' => PostOutput::STATUS_COMPLETED, 'page_count' => $pageCount, ]) > 0; } /** * 仅保存配置(不生成图片),创建新的历史记录 * @param int $postId 文章ID * @param array $config 配置(含 size/fontSize/watermark/pageAlignments/content_html) * @param int $adminId 管理员ID * @return PostOutput */ public static function saveConfigOnly(int $postId, array $config, int $adminId): PostOutput { return PostOutput::create([ 'post_id' => $postId, 'output_type' => 'phone_image', 'config' => $config, 'status' => PostOutput::STATUS_PROCESSING, 'page_count' => 0, 'admin_id' => $adminId, ]); } }