baseUrl = rtrim($baseUrl, '/'); $this->useDatabase = true; } /** * 通用HTTP请求方法 - 减少重复代码,无缓存 */ protected function request(string $method, string $endpoint, array $data = []): array { try { $httpMethod = strtolower($method); $response = match($httpMethod) { 'get' => Http::timeout(30)->get($this->baseUrl . $endpoint, $data), 'post' => Http::timeout(300)->post($this->baseUrl . $endpoint, $data), 'put' => Http::timeout(30)->put($this->baseUrl . $endpoint, $data), 'delete' => Http::timeout(30)->delete($this->baseUrl . $endpoint, $data), default => throw new \InvalidArgumentException("Unsupported HTTP method: {$method}") }; // 处理文件上传 if (isset($data['file']) && $data['file'] instanceof \Illuminate\Http\UploadedFile) { $response = Http::timeout(300) ->attach('file', file_get_contents($data['file']->getPathname()), $data['file']->getClientOriginalName()) ->{$httpMethod}($this->baseUrl . $endpoint, $data); } if ($response->successful()) { return $response->json(); } // 记录错误并抛出异常 $error = $this->handleErrorResponse($response, $endpoint); throw new \Exception($error); } catch (\Exception $e) { Log::error("API request failed: {$method} {$endpoint}", [ 'error' => $e->getMessage(), 'data' => $data ]); throw $e; } } /** * 处理错误响应 */ private function handleErrorResponse($response, string $endpoint): string { $status = $response->status(); $body = $response->body(); // 常见错误类型的友好提示 return match($status) { 404 => "未找到资源: {$endpoint}", 422 => "数据验证失败: {$body}", 500 => "服务器内部错误: {$body}", default => "HTTP {$status}: {$body}" }; } /** * 获取教材系列列表 */ public function getTextbookSeries(array $params = []): array { if ($this->useDatabase) { $series = TextbookSeries::query()->orderBy('sort_order')->get(); return ['data' => $series->toArray(), 'meta' => ['total' => $series->count()]]; } try { return $this->request('GET', '/textbooks/series', $params); } catch (\Exception $e) { // 失败时返回空数据而不是抛出异常,保持向后兼容 Log::warning('Failed to fetch textbook series, returning empty result', ['error' => $e->getMessage()]); return ['data' => [], 'meta' => []]; } } /** * 根据ID获取单个教材系列 */ public function getTextbookSeriesById(int $seriesId): ?array { if ($this->useDatabase) { return TextbookSeries::query()->find($seriesId)?->toArray(); } try { $result = $this->request('GET', "/textbooks/series/{$seriesId}"); return $result['data'] ?? null; } catch (\Exception $e) { Log::warning('Series not found or error occurred', [ 'series_id' => $seriesId, 'error' => $e->getMessage() ]); return null; } } /** * 创建教材系列 */ public function createTextbookSeries(array $data): array { if ($this->useDatabase) { $series = TextbookSeries::query()->create($data); return ['data' => $series->toArray()]; } try { return $this->request('POST', '/textbooks/series', $data); } catch (\Exception $e) { // 检查是否是series不存在的错误(虽然这里不太可能,但保持向后兼容) if (strpos($e->getMessage(), 'Series not found') !== false) { $seriesId = $data['id'] ?? 'unknown'; throw new \Exception("系列ID {$seriesId} 不存在"); } throw $e; } } /** * 更新教材系列 */ public function updateTextbookSeries(int $seriesId, array $data): array { if ($this->useDatabase) { $series = TextbookSeries::query()->findOrFail($seriesId); $series->fill($data); $series->save(); return ['data' => $series->toArray()]; } try { return $this->request('PUT', "/textbooks/series/{$seriesId}", $data); } catch (\Exception $e) { Log::error('Error updating textbook series', ['error' => $e->getMessage()]); throw $e; } } /** * 删除教材系列 */ public function deleteTextbookSeries(int $seriesId): bool { if ($this->useDatabase) { $series = TextbookSeries::query()->find($seriesId); return $series ? (bool) $series->delete() : false; } try { $this->request('DELETE', "/textbooks/series/{$seriesId}"); return true; } catch (\Exception $e) { Log::error('Error deleting textbook series', ['error' => $e->getMessage()]); return false; } } /** * 删除教材 */ public function deleteTextbook(int $textbookId): bool { if ($this->useDatabase) { $textbook = Textbook::query()->find($textbookId); return $textbook ? (bool) $textbook->delete() : false; } try { $this->request('DELETE', "/textbooks/by-id/{$textbookId}"); return true; } catch (\Exception $e) { Log::error('Error deleting textbook', ['error' => $e->getMessage(), 'textbook_id' => $textbookId]); return false; } } /** * 获取教材列表 */ public function getTextbooks(array $params = []): array { if ($this->useDatabase) { $query = Textbook::query(); if (isset($params['series_id'])) { $query->where('series_id', $params['series_id']); } if (isset($params['stage'])) { $query->where('stage', $params['stage']); } if (isset($params['grade'])) { $query->where('grade', $params['grade']); } if (array_key_exists('semester', $params) && $params['semester'] !== null) { $query->where('semester', $params['semester']); } if (isset($params['status'])) { $query->where('status', $params['status']); } $textbooks = $query->orderBy('id')->get(); return ['data' => $textbooks->toArray(), 'meta' => ['total' => $textbooks->count()]]; } try { return $this->request('GET', '/textbooks', $params); } catch (\Exception $e) { Log::warning('Failed to fetch textbooks, returning empty result', ['error' => $e->getMessage()]); return ['data' => [], 'meta' => []]; } } /** * 获取单个教材 */ public function getTextbook(int $textbookId): ?array { if ($this->useDatabase) { return Textbook::query()->find($textbookId)?->toArray(); } try { $result = $this->request('GET', "/textbooks/by-id/{$textbookId}"); return $result['data'] ?? null; } catch (\Exception $e) { Log::warning('Textbook not found or error occurred', [ 'textbook_id' => $textbookId, 'error' => $e->getMessage() ]); return null; } } /** * 创建教材 */ public function createTextbook(array $data): array { if ($this->useDatabase) { $textbook = Textbook::query()->create($data); return ['data' => $textbook->toArray()]; } try { return $this->request('POST', '/textbooks', $data); } catch (\Exception $e) { // 检查是否是series不存在的错误 if (strpos($e->getMessage(), 'Series not found') !== false) { $seriesId = $data['series_id'] ?? 'unknown'; throw new \Exception("系列ID {$seriesId} 不存在,请先创建教材系列或检查ID是否正确"); } throw $e; } } /** * 更新教材 */ public function updateTextbook(int $textbookId, array $data): array { if ($this->useDatabase) { $textbook = Textbook::query()->findOrFail($textbookId); $textbook->fill($data); $textbook->save(); return ['data' => $textbook->toArray()]; } try { return $this->request('PUT', "/textbooks/by-id/{$textbookId}", $data); } catch (\Exception $e) { Log::error('Error updating textbook', ['error' => $e->getMessage()]); throw $e; } } /** * 创建或更新教材(upsert模式) * 根据系列、学段、年级、学期、官方书名判断是否已存在 */ public function createOrUpdateTextbook(array $data): array { if ($this->useDatabase) { if (isset($data['id'])) { return $this->updateTextbook((int) $data['id'], $data); } $textbook = Textbook::query()->create($data); return ['data' => $textbook->toArray()]; } try { return $this->request('POST', '/textbooks/upsert', $data); } catch (\Exception $e) { // 检查是否是series不存在的错误 if (strpos($e->getMessage(), 'Series not found') !== false) { $seriesId = $data['series_id'] ?? 'unknown'; throw new \Exception("系列ID {$seriesId} 不存在,请先创建教材系列或检查ID是否正确"); } throw $e; } } /** * 删除教材目录节点 */ public function deleteTextbookCatalog(int $catalogId): bool { if ($this->useDatabase) { $catalog = TextbookCatalog::query()->find($catalogId); return $catalog ? (bool) $catalog->delete() : false; } try { $this->request('DELETE', "/textbooks/catalog/{$catalogId}"); return true; } catch (\Exception $e) { Log::error('Error deleting textbook catalog', ['error' => $e->getMessage(), 'catalog_id' => $catalogId]); return false; } } /** * 获取教材目录树 */ public function getTextbookCatalog(int $textbookId, string $format = 'tree'): array { if ($this->useDatabase) { $nodes = TextbookCatalog::query() ->where('textbook_id', $textbookId) ->orderBy('sort_order') ->get(); if ($format === 'tree') { return $this->buildCatalogTree($nodes->toArray()); } return $nodes->toArray(); } try { $result = $this->request('GET', "/textbooks/by-id/{$textbookId}/catalog", ['format' => $format]); return $result['data'] ?? []; } catch (\Exception $e) { Log::warning('Failed to fetch textbook catalog, returning empty result', ['error' => $e->getMessage()]); return []; } } /** * 预览教材命名 */ public function previewTextbookNaming(array $textbookData, array $seriesData): array { if ($this->useDatabase) { $title = $textbookData['official_title'] ?? $textbookData['display_title'] ?? ''; return ['data' => ['official_title' => $title]]; } try { $result = $this->request('POST', '/textbooks/naming-preview', [ 'textbook' => $textbookData, 'series' => $seriesData ]); return $result['data'] ?? []; } catch (\Exception $e) { Log::warning('Failed to preview textbook naming, returning empty result', ['error' => $e->getMessage()]); return []; } } /** * 导入教材元信息 */ public function importTextbookMetadata($file, string $commitMode = 'overwrite'): array { if ($this->useDatabase) { return [ 'data' => [], 'meta' => [ 'status' => 'pending', 'message' => 'Local import is pending implementation.', ], ]; } try { return $this->request('POST', '/textbooks/import/meta', [ 'file' => $file, 'commit_mode' => $commitMode ]); } catch (\Exception $e) { Log::error('Error importing textbook metadata', ['error' => $e->getMessage()]); throw $e; } } /** * 导入教材目录 */ public function importTextbookCatalog(int $textbookId, array $catalogData, string $commitMode = 'overwrite', ?int $seriesId = null): array { if ($this->useDatabase) { if (empty($catalogData)) { return [ 'success' => false, 'message' => '目录数据为空', 'success_count' => 0, 'error_count' => 0, 'errors' => [], ]; } $successCount = 0; $errorCount = 0; $errors = []; $parentStack = []; DB::transaction(function () use ($textbookId, $catalogData, $commitMode, $seriesId, &$successCount, &$errorCount, &$errors, &$parentStack) { if ($commitMode === 'overwrite') { $textbook = Textbook::query()->find($textbookId); if ($textbook && $seriesId !== null && (int) $textbook->series_id !== (int) $seriesId) { throw new \RuntimeException('教材系列不匹配,已阻止覆盖更新'); } TextbookCatalog::query()->where('textbook_id', $textbookId)->delete(); } foreach ($catalogData as $index => $node) { try { $depth = (int) ($node['depth'] ?? 1); $parentId = $node['parent_id'] ?? null; $title = trim((string) ($node['title'] ?? '')); if ($title === '') { throw new \Exception('目录标题不能为空'); } $tags = $node['tags'] ?? null; if (is_array($tags)) { $tags = empty($tags) ? null : json_encode($tags, JSON_UNESCAPED_UNICODE); } elseif (is_string($tags) && $tags !== '') { json_decode($tags, true); if (json_last_error() !== JSON_ERROR_NONE) { $tags = null; } } $meta = $node['meta'] ?? null; if (is_array($meta)) { $meta = empty($meta) ? null : json_encode($meta, JSON_UNESCAPED_UNICODE); } elseif (is_string($meta) && $meta !== '') { json_decode($meta, true); if (json_last_error() !== JSON_ERROR_NONE) { $meta = null; } } if (!$parentId && $depth > 1) { $parentId = $parentStack[$depth - 1] ?? null; } $payload = [ 'textbook_id' => $textbookId, 'parent_id' => $parentId, 'node_type' => $node['node_type'] ?? 'chapter', 'title' => $title, 'display_no' => $node['display_no'] ?? null, 'depth' => $depth, 'sort_order' => (int) ($node['sort_order'] ?? 0), 'path_key' => $node['path_key'] ?? null, 'is_required' => (bool) ($node['is_required'] ?? false), 'is_elective' => (bool) ($node['is_elective'] ?? false), 'tags' => $tags, 'page_start' => $node['page_start'] ?? null, 'page_end' => $node['page_end'] ?? null, 'meta' => $meta, ]; $record = TextbookCatalog::create($payload); $parentStack[$depth] = $record->id; foreach (array_keys($parentStack) as $stackDepth) { if ($stackDepth > $depth) { unset($parentStack[$stackDepth]); } } $successCount++; } catch (\Throwable $e) { $errorCount++; $errors[] = '第' . ($index + 2) . '行: ' . $e->getMessage(); } } }); return [ 'success' => $errorCount === 0, 'success_count' => $successCount, 'error_count' => $errorCount, 'errors' => $errors, 'message' => $errorCount === 0 ? '导入完成' : '部分导入失败', ]; } try { // 将数组转换为JSON字符串 $jsonData = json_encode($catalogData, JSON_UNESCAPED_UNICODE); // 直接发送 JSON 数据 return $this->request('POST', '/textbooks/import/catalog', [ 'textbook_id' => $textbookId, 'data' => $jsonData, 'commit_mode' => $commitMode ]); } catch (\Exception $e) { Log::error('Error importing textbook catalog', ['error' => $e->getMessage()]); throw $e; } } /** * 提交导入任务 */ public function commitImportJob(int $jobId): array { if ($this->useDatabase) { return [ 'data' => [], 'meta' => [ 'status' => 'noop', 'message' => 'Local import jobs are not tracked yet.', ], ]; } try { return $this->request('POST', "/api/textbooks/import/{$jobId}/commit"); } catch (\Exception $e) { Log::error('Error committing import job', ['error' => $e->getMessage()]); throw $e; } } /** * 获取导入任务列表 */ public function getImportJobs(array $params = []): array { if ($this->useDatabase) { return ['data' => [], 'meta' => ['total' => 0]]; } try { return $this->request('GET', '/textbooks/import/jobs', $params); } catch (\Exception $e) { Log::warning('Failed to fetch import jobs, returning empty result', ['error' => $e->getMessage()]); return ['data' => [], 'meta' => []]; } } /** * 获取单个导入任务 */ public function getImportJob(int $jobId): ?array { if ($this->useDatabase) { return null; } try { $result = $this->request('GET', "/api/textbooks/import/jobs/{$jobId}"); return $result['data'] ?? null; } catch (\Exception $e) { Log::warning('Import job not found or error occurred', ['error' => $e->getMessage()]); return null; } } /** * 完全同步教材系列到题库服务(清空+重新插入) */ public function syncTextbookSeriesToQuestionBank(): array { if ($this->useDatabase) { return ['data' => [], 'meta' => ['status' => 'noop']]; } try { // 获取MySQL中的所有系列 $mysqlSeries = DB::connection('mysql') ->table('textbook_series') ->orderBy('id') ->get(); if ($mysqlSeries->isEmpty()) { return [ 'success' => false, 'message' => 'MySQL中没有找到教材系列数据' ]; } // 准备数据 $seriesData = []; foreach ($mysqlSeries as $series) { $seriesData[] = [ 'id' => $series->id, 'name' => $series->name, 'slug' => $series->slug, 'publisher' => $series->publisher, 'region' => $series->region, 'stages' => json_decode($series->stages, true), 'is_active' => (bool)$series->is_active, 'sort_order' => (int)$series->sort_order, 'meta' => json_decode($series->meta, true), ]; } // 调用API进行完全同步 $result = $this->request('POST', '/textbooks/series/sync-all', [ 'series' => $seriesData ]); return [ 'success' => true, 'synced_count' => count($seriesData), 'data' => $result ]; } catch (\Exception $e) { Log::error('Error syncing textbook series', ['error' => $e->getMessage()]); return [ 'success' => false, 'message' => '同步失败: ' . $e->getMessage() ]; } } private function buildCatalogTree(array $nodes): array { $indexed = []; foreach ($nodes as $node) { $node['children'] = []; $indexed[$node['id']] = $node; } $tree = []; foreach ($indexed as $id => $node) { $parentId = $node['parent_id'] ?? null; if ($parentId && isset($indexed[$parentId])) { $indexed[$parentId]['children'][] = $node; } else { $tree[] = $node; } } return $tree; } }