TextbookApiController.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  1. <?php
  2. namespace App\Http\Controllers\Api;
  3. use App\Http\Controllers\Controller;
  4. use App\Services\TextbookApiService;
  5. use Illuminate\Http\Request;
  6. use Illuminate\Http\JsonResponse;
  7. use Illuminate\Support\Facades\Log;
  8. class TextbookApiController extends Controller
  9. {
  10. protected TextbookApiService $textbookService;
  11. public function __construct(TextbookApiService $textbookService)
  12. {
  13. $this->textbookService = $textbookService;
  14. }
  15. /**
  16. * 获取教材列表(按年级排序)
  17. *
  18. * @param Request $request
  19. * @return JsonResponse
  20. */
  21. public function index(Request $request): JsonResponse
  22. {
  23. try {
  24. $params = [
  25. 'page' => $request->get('page', 1),
  26. 'per_page' => $request->get('per_page', 50),
  27. ];
  28. // 可选过滤参数
  29. if ($request->has('grade')) {
  30. $params['grade'] = $request->get('grade');
  31. }
  32. if ($request->has('stage')) {
  33. $params['stage'] = $this->convertStageToCode($request->get('stage'));
  34. }
  35. if ($request->has('semester')) {
  36. $params['semester'] = $this->convertSemesterToCode($request->get('semester'));
  37. }
  38. if ($request->has('series_id')) {
  39. $params['series_id'] = $request->get('series_id');
  40. }
  41. if ($request->has('status')) {
  42. $params['status'] = $request->get('status');
  43. }
  44. $result = $this->textbookService->getTextbooks($params);
  45. // 格式化返回数据
  46. $textbooks = $this->formatTextbookList($result['data'] ?? []);
  47. // 按年级排序
  48. usort($textbooks, function ($a, $b) {
  49. // 先按学段排序:小学 < 初中 < 高中
  50. $stageOrder = ['primary' => 1, 'junior' => 2, 'senior' => 3];
  51. $stageA = $stageOrder[$a['stage_code']] ?? 99;
  52. $stageB = $stageOrder[$b['stage_code']] ?? 99;
  53. if ($stageA !== $stageB) {
  54. return $stageA - $stageB;
  55. }
  56. // 再按年级排序
  57. $gradeA = $a['grade'] ?? 0;
  58. $gradeB = $b['grade'] ?? 0;
  59. if ($gradeA !== $gradeB) {
  60. return $gradeA - $gradeB;
  61. }
  62. // 最后按学期排序
  63. $semesterA = $a['semester_code'] ?? 0;
  64. $semesterB = $b['semester_code'] ?? 0;
  65. return $semesterA - $semesterB;
  66. });
  67. return response()->json([
  68. 'success' => true,
  69. 'data' => $textbooks,
  70. 'meta' => $result['meta'] ?? [
  71. 'page' => $params['page'],
  72. 'per_page' => $params['per_page'],
  73. 'total' => count($textbooks),
  74. ]
  75. ]);
  76. } catch (\Exception $e) {
  77. Log::error('获取教材列表失败', ['error' => $e->getMessage()]);
  78. return response()->json([
  79. 'success' => false,
  80. 'message' => '获取教材列表失败: ' . $e->getMessage()
  81. ], 500);
  82. }
  83. }
  84. /**
  85. * 根据年级获取教材
  86. *
  87. * @param int $grade 年级(1-12)
  88. * @param Request $request
  89. * @return JsonResponse
  90. */
  91. public function getByGrade(int $grade, Request $request): JsonResponse
  92. {
  93. try {
  94. $schoolingSystem = (string) $request->get('schooling_system', '');
  95. $stageParam = $request->get('stage');
  96. $params = [
  97. 'grade' => $grade,
  98. 'per_page' => 100,
  99. ];
  100. if ($stageParam) {
  101. $params['stage'] = $this->convertStageToCode((string) $stageParam);
  102. } elseif ($schoolingSystem === '5-4') {
  103. $params['stage'] = $this->getStageByGrade($grade, $schoolingSystem);
  104. }
  105. if ($request->has('semester')) {
  106. $params['semester'] = $this->convertSemesterToCode($request->get('semester'));
  107. }
  108. if ($request->has('series_id')) {
  109. $params['series_id'] = $request->get('series_id');
  110. }
  111. $result = $this->textbookService->getTextbooks($params);
  112. $textbooks = $this->formatTextbookList($result['data'] ?? []);
  113. // 按学期排序
  114. usort($textbooks, function ($a, $b) {
  115. return ($a['semester_code'] ?? 0) - ($b['semester_code'] ?? 0);
  116. });
  117. return response()->json([
  118. 'success' => true,
  119. 'data' => $textbooks,
  120. 'meta' => [
  121. 'grade' => $grade,
  122. 'stage' => $params['stage'] ?? null,
  123. 'stage_label' => isset($params['stage']) ? $this->getStageLabel($params['stage']) : null,
  124. 'total' => count($textbooks),
  125. ]
  126. ]);
  127. } catch (\Exception $e) {
  128. Log::error('根据年级获取教材失败', [
  129. 'grade' => $grade,
  130. 'error' => $e->getMessage()
  131. ]);
  132. return response()->json([
  133. 'success' => false,
  134. 'message' => '获取教材失败: ' . $e->getMessage()
  135. ], 500);
  136. }
  137. }
  138. /**
  139. * 获取单个教材详情
  140. *
  141. * @param int $id 教材ID
  142. * @return JsonResponse
  143. */
  144. public function show(int $id): JsonResponse
  145. {
  146. try {
  147. $textbook = $this->textbookService->getTextbook($id);
  148. if (!$textbook) {
  149. return response()->json([
  150. 'success' => false,
  151. 'message' => '教材不存在'
  152. ], 404);
  153. }
  154. return response()->json([
  155. 'success' => true,
  156. 'data' => $this->formatTextbook($textbook)
  157. ]);
  158. } catch (\Exception $e) {
  159. Log::error('获取教材详情失败', [
  160. 'id' => $id,
  161. 'error' => $e->getMessage()
  162. ]);
  163. return response()->json([
  164. 'success' => false,
  165. 'message' => '获取教材详情失败: ' . $e->getMessage()
  166. ], 500);
  167. }
  168. }
  169. /**
  170. * 获取教材系列列表
  171. *
  172. * @param Request $request
  173. * @return JsonResponse
  174. */
  175. public function getSeries(Request $request): JsonResponse
  176. {
  177. try {
  178. $params = [];
  179. if ($request->has('stage')) {
  180. $params['stage'] = $this->convertStageToCode($request->get('stage'));
  181. }
  182. $result = $this->textbookService->getTextbookSeries($params);
  183. $series = array_map(function ($item) {
  184. return [
  185. 'id' => $item['id'],
  186. 'name' => $item['name'],
  187. 'slug' => $item['slug'] ?? '',
  188. 'publisher' => $item['publisher'] ?? '',
  189. 'region' => $item['region'] ?? '全国',
  190. 'stages' => $this->formatStages($item['stages'] ?? '[]'),
  191. 'is_active' => $item['is_active'] ?? true,
  192. 'sort_order' => $item['sort_order'] ?? 0,
  193. ];
  194. }, $result['data'] ?? []);
  195. // 按排序字段排序
  196. usort($series, fn($a, $b) => ($a['sort_order'] ?? 0) - ($b['sort_order'] ?? 0));
  197. return response()->json([
  198. 'success' => true,
  199. 'data' => $series,
  200. 'meta' => [
  201. 'total' => count($series),
  202. ]
  203. ]);
  204. } catch (\Exception $e) {
  205. Log::error('获取教材系列失败', ['error' => $e->getMessage()]);
  206. return response()->json([
  207. 'success' => false,
  208. 'message' => '获取教材系列失败: ' . $e->getMessage()
  209. ], 500);
  210. }
  211. }
  212. /**
  213. * 年级枚举
  214. */
  215. public function getGradeEnums(): JsonResponse
  216. {
  217. $schoolingSystem = (string) request()->get('schooling_system', '');
  218. $grades = [];
  219. $primaryMax = $schoolingSystem === '5-4' ? 5 : 6;
  220. foreach (range(1, $primaryMax) as $grade) {
  221. $grades[] = [
  222. 'grade' => $grade,
  223. 'label' => $this->getGradeLabel($grade, 'primary'),
  224. 'stage' => 'primary',
  225. 'stage_label' => $this->getStageLabel('primary'),
  226. ];
  227. }
  228. $juniorStart = $schoolingSystem === '5-4' ? 6 : 7;
  229. foreach (range($juniorStart, 9) as $grade) {
  230. $grades[] = [
  231. 'grade' => $grade,
  232. 'label' => $this->getGradeLabel($grade, 'junior'),
  233. 'stage' => 'junior',
  234. 'stage_label' => $this->getStageLabel('junior'),
  235. ];
  236. }
  237. foreach (range(10, 12) as $grade) {
  238. $grades[] = [
  239. 'grade' => $grade,
  240. 'label' => $this->getGradeLabel($grade, 'senior'),
  241. 'stage' => 'senior',
  242. 'stage_label' => $this->getStageLabel('senior'),
  243. ];
  244. }
  245. return response()->json([
  246. 'success' => true,
  247. 'data' => $grades,
  248. ]);
  249. }
  250. /**
  251. * 获取教材目录
  252. *
  253. * @param int $textbookId 教材ID
  254. * @param Request $request
  255. * @return JsonResponse
  256. */
  257. public function getCatalog(int $textbookId, Request $request): JsonResponse
  258. {
  259. try {
  260. $format = $request->get('format', 'tree'); // tree 或 flat
  261. $catalog = $this->textbookService->getTextbookCatalog($textbookId, $format);
  262. return response()->json([
  263. 'success' => true,
  264. 'data' => $catalog,
  265. 'meta' => [
  266. 'textbook_id' => $textbookId,
  267. 'format' => $format,
  268. ]
  269. ]);
  270. } catch (\Exception $e) {
  271. Log::error('获取教材目录失败', [
  272. 'textbook_id' => $textbookId,
  273. 'error' => $e->getMessage()
  274. ]);
  275. return response()->json([
  276. 'success' => false,
  277. 'message' => '获取教材目录失败: ' . $e->getMessage()
  278. ], 500);
  279. }
  280. }
  281. /**
  282. * 格式化教材列表
  283. */
  284. private function formatTextbookList(array $textbooks): array
  285. {
  286. return array_map(fn($item) => $this->formatTextbook($item), $textbooks);
  287. }
  288. /**
  289. * 格式化单个教材
  290. */
  291. private function formatTextbook(array $textbook): array
  292. {
  293. $stage = $textbook['stage'] ?? '';
  294. $semester = $textbook['semester'] ?? null;
  295. return [
  296. 'id' => $textbook['id'],
  297. 'name' => $textbook['official_title'] ?? '',
  298. 'display_name' => $textbook['official_title'] ?? '',
  299. 'cover' => $this->formatCoverUrl($textbook['cover_path'] ?? ''),
  300. 'series_id' => $textbook['series_id'] ?? null,
  301. 'series_name' => $textbook['series']['name'] ?? '',
  302. 'publisher' => $textbook['series']['publisher'] ?? '',
  303. 'stage' => $this->getStageLabel($stage),
  304. 'stage_code' => $stage,
  305. 'grade' => $textbook['grade'] ?? null,
  306. 'grade_label' => $this->getGradeLabel($textbook['grade'] ?? null, $stage),
  307. 'semester' => $this->getSemesterLabel($semester),
  308. 'semester_code' => $semester,
  309. 'module_type' => $textbook['module_type'] ?? null,
  310. 'volume_no' => $textbook['volume_no'] ?? null,
  311. 'isbn' => $textbook['isbn'] ?? '',
  312. 'approval_year' => $textbook['approval_year'] ?? null,
  313. 'curriculum_standard_year' => $textbook['curriculum_standard_year'] ?? null,
  314. 'status' => $textbook['status'] ?? 'draft',
  315. 'sort_order' => $textbook['sort_order'] ?? 0,
  316. ];
  317. }
  318. /**
  319. * 格式化封面URL
  320. */
  321. private function formatCoverUrl(?string $coverPath): string
  322. {
  323. if (empty($coverPath)) {
  324. return '';
  325. }
  326. // 如果已经是完整URL,直接返回
  327. if (str_starts_with($coverPath, 'http://') || str_starts_with($coverPath, 'https://')) {
  328. return $coverPath;
  329. }
  330. // 本地存储路径,添加域名
  331. return url('/storage/' . ltrim($coverPath, '/'));
  332. }
  333. /**
  334. * 学段代码转中文
  335. */
  336. private function getStageLabel(string $stage): string
  337. {
  338. return match ($stage) {
  339. 'primary' => '小学',
  340. 'junior' => '初中',
  341. 'senior' => '高中',
  342. default => $stage,
  343. };
  344. }
  345. /**
  346. * 中文学段转代码
  347. */
  348. private function convertStageToCode(string $stage): string
  349. {
  350. return match ($stage) {
  351. '小学' => 'primary',
  352. '初中' => 'junior',
  353. '高中' => 'senior',
  354. default => $stage,
  355. };
  356. }
  357. /**
  358. * 学期代码转中文
  359. */
  360. private function getSemesterLabel(?int $semester): string
  361. {
  362. return match ($semester) {
  363. 1 => '上册',
  364. 2 => '下册',
  365. default => '',
  366. };
  367. }
  368. /**
  369. * 中文学期转代码
  370. */
  371. private function convertSemesterToCode(string $semester): ?int
  372. {
  373. return match ($semester) {
  374. '上册', '1' => 1,
  375. '下册', '2' => 2,
  376. default => null,
  377. };
  378. }
  379. /**
  380. * 年级标签
  381. */
  382. private function getGradeLabel(?int $grade, string $stage): string
  383. {
  384. if ($grade === null) {
  385. return '';
  386. }
  387. return match ($stage) {
  388. 'primary' => $grade . '年级',
  389. 'junior' => match ($grade) {
  390. 7 => '七年级',
  391. 8 => '八年级',
  392. 9 => '九年级',
  393. default => $grade . '年级',
  394. },
  395. 'senior' => match ($grade) {
  396. 10 => '高一',
  397. 11 => '高二',
  398. 12 => '高三',
  399. default => '高' . ($grade - 9),
  400. },
  401. default => $grade . '年级',
  402. };
  403. }
  404. /**
  405. * 根据年级判断学段
  406. */
  407. private function getStageByGrade(int $grade, string $schoolingSystem = ''): string
  408. {
  409. if ($schoolingSystem === '5-4') {
  410. if ($grade >= 1 && $grade <= 5) {
  411. return 'primary';
  412. }
  413. if ($grade >= 6 && $grade <= 9) {
  414. return 'junior';
  415. }
  416. }
  417. if ($grade >= 1 && $grade <= 6) {
  418. return 'primary';
  419. } elseif ($grade >= 7 && $grade <= 9) {
  420. return 'junior';
  421. } else {
  422. return 'senior';
  423. }
  424. }
  425. /**
  426. * 格式化学段数组
  427. */
  428. private function formatStages($stages): array
  429. {
  430. if (is_string($stages)) {
  431. $stages = json_decode($stages, true) ?? [];
  432. }
  433. if (!is_array($stages)) {
  434. return [];
  435. }
  436. return array_map(fn($s) => [
  437. 'code' => $s,
  438. 'label' => $this->getStageLabel($s),
  439. ], $stages);
  440. }
  441. }