ab24f210a6
## 新增功能 - 會員與教練在 confirmed 預約期間可互傳文字與圖片訊息 - Presence Channel 顯示對方在線狀態(即時加入/離線) - 已讀回執:對方讀取訊息後即時更新 - 預約完成(completed)後訊息封存唯讀 - 圖片上傳使用 intervention/image 處理(移除 EXIF、限制尺寸、強制重新編碼) ## 通知系統 - 收到新訊息時 Bell Icon 即時更新(NotificationCreated via private channel) - 預約列表卡片顯示未讀角標(GET /api/bookings/messages/unread-counts 批次查詢) - 瀏覽器分頁在背景時推送 Web Notification ## 基礎建設 - 引入 Laravel Reverb 作為自架 WebSocket 伺服器 - docker-compose 新增 reverb service(連接 proxy_net,供 NPM 代理) - 前端安裝 laravel-echo + pusher-js,初始化 Echo plugin - Dockerfile 補 GD JPEG/WebP/FreeType 支援 ## 清理 - 移除 test_broadcast.php 與 resources/js/echo.js(Blade 殘留) - 移除 /ping、/testpost 測試路由 - channels.php 改用 class-based 授權語法,移除 debug log - BookingChat.vue otherType 提取為 computed 消除重複 - docker-entrypoint.sh 健康檢查改用 env var 取代硬編碼密碼 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
9.2 KiB
9.2 KiB
1. 基礎建設與套件安裝
- 1.1 [後端] 安裝
laravel/reverb:composer require laravel/reverb,執行php artisan reverb:install生成config/reverb.php - 1.2 [後端] 安裝
intervention/image:composer require intervention/image;用途:上傳圖片時移除 EXIF(含 GPS 座標)、強制重新編碼為 jpg/png 以防格式偽裝、限制最大尺寸(長邊 2048px) - 1.3 [前端] 安裝
laravel-echo與pusher-js:npm install laravel-echo pusher-js - 1.4 [後端] 更新
.env:設定BROADCAST_CONNECTION=reverb、REVERB_APP_ID=cfdive、REVERB_APP_KEY(32 字元隨機)、REVERB_APP_SECRET(32 字元隨機)、REVERB_HOST=reverb(Docker service DNS,非 bind address)、REVERB_PORT=8080;另補 Vite 前端用的VITE_REVERB_APP_KEY、VITE_REVERB_HOST=ws.hank-space.com、VITE_REVERB_PORT=443、VITE_REVERB_SCHEME=https - 1.5 [後端] 在
config/broadcasting.php確認reverbdriver 設定正確(host、port從 env 讀取) - 1.6 [後端] 在
bootstrap/app.php啟用 broadcasting:確認->withBroadcasting()已加入,或在routes/api.php明確呼叫Broadcast::routes(['middleware' => ['auth:sanctum']])以確保/broadcasting/authendpoint 存在並受 Sanctum 保護
2. Docker 與 Infrastructure 設定
- 2.1 [後端] 在
docker-compose.yml新增reverbservice:複用cfdive-platformimage,command: php artisan reverb:start --host=0.0.0.0 --port=8080 --debug,連接cfdive-network與proxy_net,restart: unless-stopped,depends_on: app - 2.2 [Infrastructure] 在 DNS 新增 A Record:
ws.hank-space.com→ VPS IP - 2.3 [Infrastructure] 在 Nginx Proxy Manager 新增 Proxy Host:Domain
ws.hank-space.com,Forward 至reverb:8080,啟用 WebSocket support,申請 SSL 憑證 - 2.4 [後端] 執行
docker-compose up --build reverb驗證 Reverb 容器啟動正常
3. 資料庫
- 3.1 [後端] 建立 migration
create_booking_messages_table:欄位id、booking_id(FK)、sender_id(FK users)、sender_type(enum: member/provider)、type(enum: text/image)、content(text)、read_at(timestamp nullable)、timestamps;加 index(booking_id, created_at) - 3.2 [後端] 執行
php artisan migrate並確認資料表建立
4. 後端 Model、Channel 與 Event
- 4.1 [後端] 建立
app/Models/BookingMessage.php:定義$fillable、booking()belongsTo、sender()morphTo 或一般 belongsTo(依 sender_type 切換) - 4.2 [後端] 建立
app/Broadcasting/BookingPresenceChannel.php:實作join()方法,eager loadschedule;Member 驗證booking->member_id === $user->id,Provider 驗證booking->schedule->provider_id === $user->id;confirmed以外狀態返回false;授權成功回傳['user_id', 'user_type', 'name'] - 4.3 [後端] 在
routes/channels.php註冊Broadcast::channel('presence-booking.{bookingId}', BookingPresenceChannel::class) - 4.4 [後端] 建立
app/Events/MessageSent.php:implementsShouldBroadcastNow(同步廣播,MVP 不走 queue),broadcastOn()返回new PresenceChannel("presence-booking.{$this->message->booking_id}"),broadcastWith()回傳id、sender_id、sender_type、type、content、created_at - 4.5 [後端] 建立
app/Events/MessageRead.php:implementsShouldBroadcastNow,廣播reader_type、last_read_message_id
5. 後端 API
- 5.1 [後端] 建立
app/Http/Controllers/BookingMessageController.php,方法:index(取得歷史)、store(發送文字/圖片)、markRead(標記已讀) - 5.2 [後端]
index方法:驗證使用者為參與方,回傳booking.messages()->orderBy('created_at')->get(),confirmed與completed均可讀取 - 5.3 [後端]
store方法:驗證 booking status 為confirmed(其他狀態回傳 403);text 訊息驗證content非空;image 訊息驗證file(mimes: jpg,png,gif,webp,max: 10240KB),存至Storage::disk('public'),路徑booking-images/{uuid}.{ext},content存完整 URL(Storage::url(...));建立BookingMessage;dispatchMessageSentevent(ShouldBroadcastNow) - 5.4 [後端]
markRead方法:更新「對方發送」且「id ≤ last_read_message_id」且「read_at IS NULL」的訊息之read_at;booking status 為confirmed時 dispatchMessageReadevent;completed時只更新 DB,不 broadcast(頻道已關閉) - 5.5 [後端] 在
routes/api.php新增路由(member 與 provider 各自的 auth middleware group):GET /api/bookings/{booking}/messages、POST /api/bookings/{booking}/messages、POST /api/bookings/{booking}/messages/read
6. 前端 Echo 初始化
- 6.1 [前端] 在
frontend/src/plugins/echo.js(新建)初始化Laravel Echo:broadcaster: 'reverb',wsHost: ws.hank-space.com,wsPort: 443,wssPort: 443,forceTLS: true,enabledTransports: ['ws', 'wss'] - 6.2 [前端] 在
frontend/src/plugins/echo.js支援雙 token:Member 用localStorage.getItem('token'),Coach 用localStorage.getItem('coach_token'),依當前角色動態設定 Authorization header;authEndpoint使用完整 URL:${import.meta.env.VITE_API_URL}/broadcasting/auth(跨 domain 不可使用相對路徑) - 6.3 [前端] 在
main.js掛載 Echo plugin
7. 前端訊息元件
- 7.1 [前端] 建立
frontend/src/components/BookingChat.vue:props 接收bookingId、bookingStatus、currentUserType - 7.2 [前端]
BookingChat.vue訂閱presence-booking.{bookingId},處理.here()(顯示在線狀態)、.joining()、.leaving() - 7.3 [前端]
BookingChat.vue監聽MessageSentevent,新訊息即時 append 到訊息列表 - 7.4 [前端]
BookingChat.vue監聽MessageReadevent,更新訊息「已讀」標記 - 7.5 [前端]
BookingChat.vue實作文字輸入框與送出按鈕,callPOST /api/bookings/{id}/messages - 7.6 [前端]
BookingChat.vue實作圖片上傳按鈕(<input type="file" accept="image/*">),以FormData送出 - 7.7 [前端]
BookingChat.vue在confirmed狀態顯示輸入區;completed狀態顯示「對話已封存」提示;其他狀態不渲染元件 - 7.8 [前端] 元件 mount 時呼叫
GET /api/bookings/{id}/messages載入歷史訊息,並送出markRead
8. 前端嵌入預約詳情頁
- 8.1 [前端] 在 Member 的預約詳情頁(
src/views/MyBookingsView.vue展開區塊)嵌入<BookingChat>元件 - 8.2 [前端] 在 Coach 的預約管理頁(
src/views/coach/BookingManagerView.vue)嵌入<BookingChat>元件,點擊「訊息」按鈕展開 - 8.3 [前端] 在預約列表卡片顯示未讀訊息角標:實作
GET /api/bookings/messages/unread-counts(一次回傳所有 booking 的未讀數,非逐筆呼叫);建立useBookingUnreadCountscomposable(60s 輪詢);BookingChat加emit('read')讓父層即時清零角標
8.5 訊息通知系統(實作中追加,超出原始 scope)
- 8.5.1 [後端] 建立
NewBookingMessageNotification:databasechannel only(不寄信);title含寄件方姓名(provider 加「教練」前綴);不走 Queue(同步寫入,確保廣播前 DB 已有資料) - 8.5.2 [後端] 建立
NotificationCreatedevent:ShouldBroadcastNow,廣播至private-App.Models.User.{id}(複用 channels.php 已有的授權);broadcastAs()='notification.created' - 8.5.3 [後端]
BookingMessageController::store()在 broadcastMessageSent後,同步 notify receiver,再 broadcastNotificationCreated - 8.5.4 [前端]
notifications.jsstore 新增startRealtime(userId)/stopRealtime():訂閱private-App.Models.User.{id},收到notification.created立刻呼叫fetchUnreadCount() - 8.5.5 [前端]
auth.js與coachAuth.js的init()/setAuth()/logout()均呼叫startRealtime/stopRealtime - 8.5.6 [前端]
BookingChat.vue新增瀏覽器通知(Web Notifications API):mount 時請求權限;收到對方MessageSent且document.hidden時推送;tag: booking-chat-{id}防止同一預約疊加通知
9. 整合測試與手動驗證
- 9.1 [整合測試] 驗證
/broadcasting/auth端點可存取(不是 404):分別用 member token 與 coach token 送出請求,確認路由已正確註冊且 Sanctum middleware 生效;合法參與方回傳 200,非參與方回傳 403,非 confirmed 狀態回傳 403 - 9.2 [整合測試] 驗證
POST /api/bookings/{id}/messages:text 訊息成功建立且廣播;image 上傳成功且 URL 可存取;invalid 檔案回傳 422 - 9.3 [整合測試] 驗證
markRead:己方訊息不更新;對方訊息read_at被設定;MessageRead廣播觸發 - 9.4 [整合測試] 驗證封存:
completed預約 POST 訊息回傳 403,GET 歷史正常回傳 - 9.5 [手動驗證] 開兩個瀏覽器分別登入 Member 與 Coach,確認訊息即時雙向傳達、在線狀態顯示正確、已讀回執正常觸發
- 9.6 [手動驗證] 測試圖片訊息:上傳圖片後對方即時看到圖片
- 9.7 [手動驗證] 關閉一個視窗,確認另一端顯示「對方已離線」