2
0

CourseController.php 7.0 KB

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