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) { $query = TextbookSeries::query()->orderBy('sort_order'); // 默认只返回启用的系列(is_active = true) // 除非显式传入 include_inactive=true if (!isset($params['include_inactive']) || $params['include_inactive'] !== 'true') { $query->where('is_active', true); } // 支持按学段筛选 if (isset($params['stage'])) { $query->where('stages', 'like', '%' . $params['stage'] . '%'); } $series = $query->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() ->select('textbooks.*') ->join('textbook_series', 'textbooks.series_id', '=', 'textbook_series.id'); // 默认只返回已发布且系列启用的教材 // 除非显式传入 include_unpublished=true 或 include_inactive_series=true if (!isset($params['include_unpublished']) || $params['include_unpublished'] !== 'true') { $query->where('textbooks.status', 'published'); } if (!isset($params['include_inactive_series']) || $params['include_inactive_series'] !== 'true') { $query->where('textbook_series.is_active', true); } // 支持按系列ID筛选 if (isset($params['series_id'])) { $query->where('textbooks.series_id', $params['series_id']); } // 支持按学段筛选 if (isset($params['stage'])) { $query->where('textbooks.stage', $params['stage']); } // 支持按年级筛选 if (isset($params['grade'])) { $query->where('textbooks.grade', $params['grade']); } // 支持按学期筛选 if (array_key_exists('semester', $params) && $params['semester'] !== null) { $query->where('textbooks.semester', $params['semester']); } // 显式按状态筛选(会覆盖默认的已发布筛选) if (isset($params['status'])) { $query->where('textbooks.status', $params['status']); } $textbooks = $query->orderBy('textbooks.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) { // 先验证教材是否存在且已发布,且其系列处于启用状态 $textbook = Textbook::query() ->select('textbooks.*') ->join('textbook_series', 'textbooks.series_id', '=', 'textbook_series.id') ->where('textbooks.id', $textbookId) ->where('textbooks.status', 'published') ->where('textbook_series.is_active', true) ->first(); if (!$textbook) { Log::warning('Textbook not found or not published or series inactive', [ 'textbook_id' => $textbookId ]); return []; } $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() ]; } } /** * 根据教材系列、年级和学期获取教材及其目录结构 * * @param int $seriesId 教材系列ID * @param int $grade 年级(1-12) * @param int $semesterCode 学期代码(1=上册,2=下册) * @param string $catalogFormat 目录格式(tree 或 flat) * @return array 包含教材信息和目录结构的数组 */ public function getTextbookByFilter(int $seriesId, int $grade, int $semesterCode, string $catalogFormat = 'tree'): array { if ($this->useDatabase) { try { // 先查找符合条件且已发布的教材 $query = Textbook::query() ->select('textbooks.*') ->join('textbook_series', 'textbooks.series_id', '=', 'textbook_series.id') ->where('textbooks.series_id', $seriesId) ->where('textbooks.grade', $grade) ->where('textbooks.semester', $semesterCode) ->where('textbooks.status', 'published') ->where('textbook_series.is_active', true); $textbook = $query->first(); if (!$textbook) { return [ 'success' => false, 'message' => '未找到符合条件的教材', 'data' => null, 'meta' => [ 'series_id' => $seriesId, 'grade' => $grade, 'semester_code' => $semesterCode, 'semester_label' => $this->getSemesterLabel($semesterCode), ] ]; } // 获取教材目录 $catalog = $this->getTextbookCatalog($textbook->id, $catalogFormat); // 格式化教材信息 $textbookData = [ 'id' => $textbook->id, 'name' => $textbook->official_title ?? '', 'display_name' => $textbook->official_title ?? '', 'cover' => $this->formatCoverUrl($textbook->cover_path ?? ''), 'series_id' => $textbook->series_id, 'series_name' => $textbook->series->name ?? '', 'publisher' => $textbook->series->publisher ?? '', 'stage' => $this->getStageLabel($textbook->stage ?? ''), 'stage_code' => $textbook->stage ?? '', 'grade' => $textbook->grade, 'grade_label' => $this->getGradeLabel($textbook->grade, $textbook->stage ?? ''), 'semester' => $this->getSemesterLabel($textbook->semester), 'semester_code' => $textbook->semester, 'module_type' => $textbook->module_type ?? null, 'volume_no' => $textbook->volume_no ?? null, 'isbn' => $textbook->isbn ?? '', 'approval_year' => $textbook->approval_year ?? null, 'curriculum_standard_year' => $textbook->curriculum_standard_year ?? null, 'status' => $textbook->status ?? 'draft', 'sort_order' => $textbook->sort_order ?? 0, ]; return [ 'success' => true, 'data' => [ 'textbook' => $textbookData, 'catalog' => $catalog, ], 'meta' => [ 'series_id' => $seriesId, 'grade' => $grade, 'grade_label' => $textbookData['grade_label'], 'semester_code' => $semesterCode, 'semester_label' => $textbookData['semester'], 'catalog_format' => $catalogFormat, ] ]; } catch (\Exception $e) { Log::error('获取教材及目录失败', [ 'series_id' => $seriesId, 'grade' => $grade, 'semester_code' => $semesterCode, 'error' => $e->getMessage() ]); return [ 'success' => false, 'message' => '获取教材信息失败: ' . $e->getMessage(), 'data' => null, ]; } } // 外部API调用(如果需要) try { $result = $this->request('GET', '/textbooks/filter', [ 'series_id' => $seriesId, 'grade' => $grade, 'semester_code' => $semesterCode, 'catalog_format' => $catalogFormat, ]); return [ 'success' => true, 'data' => $result['data'] ?? null, 'meta' => $result['meta'] ?? [] ]; } catch (\Exception $e) { Log::warning('Failed to fetch textbook by filter, returning empty result', ['error' => $e->getMessage()]); return [ 'success' => false, 'message' => '获取教材信息失败: ' . $e->getMessage(), 'data' => null, ]; } } /** * 获取学期标签 */ private function getSemesterLabel(?int $semester): string { return match ($semester) { 1 => '上册', 2 => '下册', default => '', }; } /** * 获取学段标签 */ private function getStageLabel(string $stage): string { return match ($stage) { 'primary' => '小学', 'junior' => '初中', 'senior' => '高中', default => $stage, }; } /** * 格式化封面URL */ private function formatCoverUrl(?string $coverPath): string { if (empty($coverPath)) { return ''; } // 如果已经是完整URL,直接返回 if (str_starts_with($coverPath, 'http://') || str_starts_with($coverPath, 'https://')) { return $coverPath; } // 本地存储路径,添加域名 return url('/storage/' . ltrim($coverPath, '/')); } /** * 获取年级标签 */ private function getGradeLabel(?int $grade, string $stage): string { if ($grade === null) { return ''; } return match ($stage) { 'primary' => $grade . '年级', 'junior' => match ($grade) { 7 => '七年级', 8 => '八年级', 9 => '九年级', default => $grade . '年级', }, 'senior' => match ($grade) { 10 => '高一', 11 => '高二', 12 => '高三', default => '高' . ($grade - 9), }, default => $grade . '年级', }; } private function buildCatalogTree(array $nodes): array { // 建立ID索引,包含children数组 $indexed = []; foreach ($nodes as &$node) { // 使用引用 $node['children'] = []; $indexed[$node['id']] = &$node; } unset($node); // 释放引用 $tree = []; foreach ($nodes as &$node) { // 使用引用 $parentId = $node['parent_id'] ?? null; if ($parentId && isset($indexed[$parentId])) { // 父节点存在,将当前节点添加到父节点的children中 $indexed[$parentId]['children'][] = &$node; } else { // 根节点,直接添加到树中 $tree[] = &$node; } } unset($node); // 释放引用 return $tree; } }