Files
CFDivePlatform/routes/api.php
T
a620906209 4baa4cb52b feat:實作課程圖片上傳 — 封面 + 相簿管理
後端:
- Migration:diving_offers 新增 cover_image 欄位、新增 course_images 表(含索引)
- CourseImage Model(CREATED_AT、url accessor)
- DivingOffer:cover_image_url accessor、hasMany courseImages、static::deleting() 孤兒清理
- CourseImageController:封面上傳/刪除、相簿上傳(max 3)/刪除,統一 mimes+size 驗證
- DivingOfferController:index/show 回傳加入 cover_image_url 與 images 陣列
- 修正 APP_URL 加入 port(:8080),Storage::url() 才能產生正確圖片連結

前端:
- courseImageApi.js:uploadCover/deleteCover/uploadImage/deleteImage
- CourseCard:有封面顯示 <img>,無封面顯示漸層佔位
- CourseDetailView:封面大圖 + 相簿縮圖橫列(點擊開新分頁)
- OfferFormView(編輯模式):封面預覽/更換/刪除、相簿縮圖管理(達 3 張隱藏上傳按鈕)

基礎設施:
- docker-entrypoint.sh:加入 storage:link --force
- docker-compose.yml:移除 storage-data named volume(改用 bind mount,避免 Nginx 讀不到圖片)

測試:
- CourseImageTest.php:14 個 Feature Test 全部 PASS(Storage::fake)
  涵蓋:上傳成功/格式驗證/大小驗證/所有權、刪除/無封面不報錯、
        相簿上限/sort_order 遞增、孤兒清理

OpenSpec:
- course-images change 歸檔至 archive/2026-05-12-course-images
- 新增 specs/course-image-upload 主規格(含 bind mount 持久化說明)

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-12 03:54:45 +08:00

150 lines
7.8 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\API\AuthController;
use App\Http\Controllers\API\DivingOfferController;
use App\Http\Controllers\API\ProviderOfferController;
use App\Http\Controllers\API\ScheduleController;
use App\Http\Controllers\API\ProviderBookingController;
use App\Http\Controllers\API\MemberBookingController;
use App\Http\Controllers\API\ReviewController;
use App\Http\Controllers\API\AdminReviewController;
use App\Http\Controllers\API\AdminBookingController;
use App\Http\Controllers\API\CourseImageController;
use App\Http\Controllers\API\AdminStatsController;
use App\Http\Controllers\API\AdminUserController;
use App\Http\Controllers\API\AdminOfferController;
// 這裡可以定義 API 路由,例如:
Route::get('/ping', function () {
return response()->json(['message' => 'pong']);
});
// 潛水課程(公開)
Route::get('/diving-offers', [DivingOfferController::class, 'index']);
Route::get('/diving-offers/{id}', [DivingOfferController::class, 'show']);
Route::get('/diving-offers/{id}/schedules', [ScheduleController::class, 'publicList']);
Route::get('/diving-offers/{id}/reviews', [ReviewController::class, 'publicList']);
// 你可以在這裡繼續新增 API 路由
Route::post('/testpost', function () {
$data = request()->all(); // 取得所有POST資料(array
return response()->json([
'data' => $data,
]);
});
// 會員註冊/登入
Route::post('/member/register', [AuthController::class, 'registerMember']);
Route::post('/member/login', [AuthController::class, 'loginMember']);
// Google 第三方登入(僅會員)
Route::get('/auth/google/redirect', [\App\Http\Controllers\API\SocialAuthController::class, 'redirectToGoogle']);
Route::get('/auth/google/callback', [\App\Http\Controllers\API\SocialAuthController::class, 'handleGoogleCallback']);
// 會員專屬 API(需登入)
Route::middleware(['auth:sanctum'])->prefix('member')->group(function () {
// 會員登出
Route::post('/logout', [AuthController::class, 'logoutMember']);
// 取得會員個人資料
Route::get('/profile', [AuthController::class, 'memberProfile']);
// 更新會員個人資料
Route::put('/profile', [AuthController::class, 'updateMemberProfile']);
// 修改密碼
Route::put('/change-password', [AuthController::class, 'changeMemberPassword']);
// 預約
Route::get('/bookings', [MemberBookingController::class, 'index']);
Route::post('/bookings', [MemberBookingController::class, 'store']);
Route::get('/bookings/{id}', [MemberBookingController::class, 'show']);
Route::delete('/bookings/{id}', [MemberBookingController::class, 'destroy']);
// 評價
Route::post('/reviews', [ReviewController::class, 'store']);
Route::put('/reviews/{id}', [ReviewController::class, 'update']);
Route::delete('/reviews/{id}',[ReviewController::class, 'destroy']);
});
// 有幫助投票(需登入,但不限 member prefix
Route::middleware('auth:sanctum')->post('/reviews/{id}/helpful', [ReviewController::class, 'toggleHelpful']);
// 服務提供者註冊/登入
Route::post('/provider/register', [AuthController::class, 'registerProvider']);
Route::post('/provider/login', [AuthController::class, 'loginProvider']);
// 服務提供者專屬 API(需登入)
Route::middleware(['auth:sanctum'])->prefix('provider')->group(function () {
// 服務提供者登出
Route::post('/logout', [AuthController::class, 'logoutProvider']);
// 取得服務提供者資料
Route::get('/profile', [AuthController::class, 'providerProfile']);
// 更新服務提供者資料
Route::put('/profile', [AuthController::class, 'updateProviderProfile']);
// 修改密碼
Route::put('/change-password', [AuthController::class, 'changeProviderPassword']);
// 教練課程管理
Route::get('/offers', [ProviderOfferController::class, 'index']);
Route::post('/offers', [ProviderOfferController::class, 'store']);
Route::get('/offers/{id}', [ProviderOfferController::class, 'show']);
Route::put('/offers/{id}', [ProviderOfferController::class, 'update']);
Route::delete('/offers/{id}', [ProviderOfferController::class, 'destroy']);
// 課程圖片
Route::post('/offers/{id}/cover', [CourseImageController::class, 'uploadCover']);
Route::delete('/offers/{id}/cover', [CourseImageController::class, 'deleteCover']);
Route::post('/offers/{id}/images', [CourseImageController::class, 'uploadImage']);
Route::delete('/images/{id}', [CourseImageController::class, 'deleteImage']);
// 時段管理
Route::get('/schedules', [ScheduleController::class, 'index']);
Route::post('/schedules', [ScheduleController::class, 'store']);
Route::put('/schedules/{id}', [ScheduleController::class, 'update']);
Route::delete('/schedules/{id}', [ScheduleController::class, 'destroy']);
// 預約管理
Route::get('/bookings', [ProviderBookingController::class, 'index']);
Route::put('/bookings/{id}/confirm', [ProviderBookingController::class, 'confirm']);
Route::put('/bookings/{id}/reject', [ProviderBookingController::class, 'reject']);
Route::put('/bookings/{id}/cancel', [ProviderBookingController::class, 'cancel']);
Route::put('/bookings/{id}/complete', [ProviderBookingController::class, 'complete']);
});
// 管理員註冊/登入
Route::post('/admin/register', [AuthController::class, 'registerAdmin']);
Route::post('/admin/login', [AuthController::class, 'loginAdmin']);
// 管理員專屬 API(需登入)
Route::middleware(['auth:sanctum', 'admin'])->prefix('admin')->group(function () {
// 管理員登出
Route::post('/logout', [AuthController::class, 'logoutAdmin']);
// 取得管理員個人資料
Route::get('/profile', [AuthController::class, 'adminProfile']);
// 更新管理員個人資料
Route::put('/profile', [AuthController::class, 'updateAdminProfile']);
// 修改密碼
Route::put('/change-password', [AuthController::class, 'changeAdminPassword']);
// 查詢會員資料
Route::get('/check-member/{id}', [AuthController::class, 'checkMember']);
// 查詢服務提供者資料
Route::get('/check-provider/{id}', [AuthController::class, 'checkProvider']);
// 統計數據
Route::get('/stats', [AdminStatsController::class, 'index']);
// 用戶管理
Route::get('/members', [AdminUserController::class, 'members']);
Route::get('/members/{id}', [AdminUserController::class, 'member']);
Route::put('/members/{id}/toggle-active', [AdminUserController::class, 'toggleMemberActive']);
Route::get('/providers', [AdminUserController::class, 'providers']);
Route::get('/providers/{id}', [AdminUserController::class, 'provider']);
Route::put('/providers/{id}/toggle-active', [AdminUserController::class, 'toggleProviderActive']);
Route::put('/providers/{id}/toggle-verified', [AdminUserController::class, 'toggleProviderVerified']);
// 課程管理
Route::get('/offers', [AdminOfferController::class, 'index']);
Route::delete('/offers/{id}', [AdminOfferController::class, 'destroy']);
// 預約管理
Route::get('/bookings', [AdminBookingController::class, 'index']);
Route::put('/bookings/{id}/complete', [AdminBookingController::class, 'complete']);
// 評價管理
Route::get('/reviews', [AdminReviewController::class, 'index']);
Route::delete('/reviews/{id}', [AdminReviewController::class, 'destroy']);
});
// 需要認證的通用路由
Route::middleware('auth:sanctum')->group(function () {
Route::post('/logout', [AuthController::class, 'logout']);
Route::get('/user', [AuthController::class, 'user']);
});