2
0

CourseController.php 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. <?php
  2. namespace App\Http\Controllers\Library;
  3. use App\Http\Api\UserApi;
  4. use App\Http\Controllers\Controller;
  5. use App\Models\Attachment;
  6. use App\Models\Course;
  7. use Carbon\Carbon;
  8. use Illuminate\Contracts\View\View;
  9. use Illuminate\Http\Request;
  10. use Illuminate\Support\Collection;
  11. use Illuminate\Support\Facades\App;
  12. use Illuminate\Support\Facades\Storage;
  13. use Illuminate\Support\Str;
  14. class CourseController extends Controller
  15. {
  16. // 无封面时的渐变占位色池:按课程 id 取余,保证同一课程颜色稳定
  17. private array $coverGradients = [
  18. 'linear-gradient(160deg, #2d2010, rgb(150, 104, 40))',
  19. 'linear-gradient(160deg, #1a2d10, rgb(88, 122, 44))',
  20. 'linear-gradient(160deg, #0d1f3c, rgb(45, 92, 138))',
  21. 'linear-gradient(160deg, #2d1020, rgb(140, 60, 96))',
  22. 'linear-gradient(160deg, #1a1a2d, rgb(72, 70, 128))',
  23. 'linear-gradient(160deg, #1a2820, rgb(52, 110, 90))',
  24. ];
  25. // 讲师头像文字底色池
  26. private array $authorColors = [
  27. '#c8860a',
  28. '#2e7d32',
  29. '#1565c0',
  30. '#6a1b9a',
  31. '#c62828',
  32. '#00695c',
  33. '#4e342e',
  34. '#37474f',
  35. ];
  36. /**
  37. * 课程栏目首页:Hero 由布局渲染,正文含统计条、最新课程、开放报名、历史课程预览。
  38. */
  39. public function index(): View
  40. {
  41. $now = Carbon::now();
  42. $base = Course::where('publicity', 30);
  43. // 可报名:当前处于报名窗口内(报名开始 <= 现在 <= 报名结束)
  44. $signUpOpen = fn ($query) => $query
  45. ->where('sign_up_start_at', '<=', $now)
  46. ->where('sign_up_end_at', '>=', $now);
  47. // 进行中:当前处于课程时间窗口内(课程开始 <= 现在 <= 课程结束)
  48. $inProgress = fn ($query) => $query
  49. ->where('start_at', '<=', $now)
  50. ->where('end_at', '>=', $now);
  51. // 最新课程:可报名 且 进行中(已结束的课程无论多新都不出现在这里)
  52. $latest = (clone $base)
  53. ->where($signUpOpen)
  54. ->where($inProgress)
  55. ->orderByDesc('created_at')
  56. ->take(4)
  57. ->get();
  58. // 开放报名:可报名
  59. $open = (clone $base)
  60. ->where($signUpOpen)
  61. ->orderBy('start_at')
  62. ->get();
  63. // 历史课程:已结束(课程时间已过)
  64. $history = (clone $base)
  65. ->where('end_at', '<', $now)
  66. ->orderByDesc('end_at')
  67. ->take(5)
  68. ->get();
  69. $stats = [
  70. 'total' => (clone $base)->count(),
  71. 'open' => (clone $base)->where($signUpOpen)->count(),
  72. 'closed' => (clone $base)->where('end_at', '<', $now)->count(),
  73. ];
  74. return view('library.course.index', [
  75. 'latestCourses' => $this->present($latest),
  76. 'openCourses' => $this->present($open),
  77. 'historyCourses' => $this->present($history),
  78. 'stats' => $stats,
  79. ]);
  80. }
  81. /**
  82. * 历史课程列表页:分页展示已结束的公开课程。
  83. */
  84. public function history(Request $request): View
  85. {
  86. $perPage = 10;
  87. $paginator = Course::where('publicity', 30)
  88. ->where('end_at', '<', Carbon::now())
  89. ->orderByDesc('end_at')
  90. ->paginate($perPage);
  91. $paginator->setCollection($this->present($paginator->getCollection()));
  92. return view('library.course.history', [
  93. 'courses' => $paginator,
  94. 'total' => $paginator->total(),
  95. ]);
  96. }
  97. // -------------------------------------------------------------------------
  98. // 将 Course 集合加工为视图所需数组:批量解析讲师,避免 N+1
  99. // -------------------------------------------------------------------------
  100. private function present(Collection $courses): Collection
  101. {
  102. if ($courses->isEmpty()) {
  103. return collect();
  104. }
  105. // 讲师:按 teacher uuid 一次批量解析
  106. $teacherUids = $courses->pluck('teacher')->filter()->unique()->values();
  107. $teachers = $teacherUids->isEmpty()
  108. ? collect()
  109. : collect(UserApi::getListByUuid($teacherUids->all()))->keyBy('id');
  110. $baseUrl = rtrim(config('mint.server.workspace_base_path'), '/');
  111. return $courses->values()->map(function (Course $course, int $index) use ($teachers, $baseUrl) {
  112. $teacher = $teachers->get($course->teacher);
  113. return [
  114. 'id' => $course->id,
  115. 'title' => $course->title,
  116. 'subtitle' => $course->subtitle,
  117. 'summary' => $course->summary,
  118. 'number' => (int) $course->number,
  119. 'join' => $course->join,
  120. 'start_at' => $course->start_at,
  121. 'end_at' => $course->end_at,
  122. 'sign_up_end_at' => $course->sign_up_end_at,
  123. 'start_date' => $course->start_at ? Carbon::parse($course->start_at)->format('Y-m-d') : null,
  124. 'sign_up_end_date' => $course->sign_up_end_at ? Carbon::parse($course->sign_up_end_at)->format('Y-m-d') : null,
  125. 'cover_url' => $this->coverUrl($course),
  126. 'cover_gradient' => $this->coverGradients[$this->colorIndex($course->id) % count($this->coverGradients)],
  127. 'teacher' => $this->formatTeacher($teacher, $index),
  128. 'status' => $this->statusOf($course),
  129. 'detail_url' => $baseUrl.'/course/'.$course->id,
  130. ];
  131. });
  132. }
  133. private function statusOf(Course $course): string
  134. {
  135. if (! $course->sign_up_start_at || ! $course->sign_up_end_at) {
  136. return 'closed';
  137. }
  138. return Carbon::now()->between(
  139. Carbon::parse($course->sign_up_start_at),
  140. Carbon::parse($course->sign_up_end_at)
  141. ) ? 'open' : 'closed';
  142. }
  143. private function formatTeacher(?array $teacher, int $index): array
  144. {
  145. $name = $teacher['nickName'] ?? null;
  146. if (! $name || $name === 'unknown') {
  147. return ['name' => null, 'avatar' => null, 'initials' => null, 'color' => null];
  148. }
  149. return [
  150. 'name' => $name,
  151. 'avatar' => $teacher['avatar'] ?? null,
  152. 'initials' => mb_substr($name, 0, 2),
  153. 'color' => $this->authorColors[$index % count($this->authorColors)],
  154. ];
  155. }
  156. // -------------------------------------------------------------------------
  157. // 封面 URL:cover 可能是文件名(Storage),也可能是附件 uuid
  158. // -------------------------------------------------------------------------
  159. private function coverUrl(Course $course): ?string
  160. {
  161. $cover = $course->cover;
  162. if (! $cover) {
  163. return null;
  164. }
  165. // 附件 uuid:取 _m 中图
  166. if (Str::isUuid($cover)) {
  167. $attachment = Attachment::find($cover);
  168. if ($attachment) {
  169. return Storage::disk('public')->url($attachment->bucket.'/'.$attachment->id.'_m.jpg');
  170. }
  171. return null;
  172. }
  173. $thumb = str_replace('.jpg', '_m.jpg', $cover);
  174. return App::environment(['local', 'testing'])
  175. ? Storage::url($thumb)
  176. : Storage::temporaryUrl($thumb, now()->addDays(6));
  177. }
  178. private function colorIndex(string $id): int
  179. {
  180. return hexdec(substr(str_replace('-', '', $id), 0, 4)) % 255;
  181. }
  182. }