feat:實作 Admin Panel — 平台管理後台

後端:
- AdminStatsController:總會員/教練/課程數統計 API
- AdminUserController:會員與教練列表、詳情、啟用/停用、教練驗證(toggle 反轉語意)
- AdminOfferController:全平台課程列表與刪除
- routes/api.php:新增 /api/admin/stats、members、providers、offers 等路由

前端(frontend/):
- adminAuth store、adminAxios(第三套獨立認證)
- /admin/* 路由群組 + requiresAdmin guard
- AdminNavBar、AdminLayout
- App.vue:isCoachPage → isBackofficePage(/coach/* 和 /admin/* 皆隱藏會員 NavBar)
- LoginView、DashboardView(統計卡片)
- MembersView、ProvidersView(含驗證操作)、OffersView(含刪除確認)

OpenSpec:
- 新增 specs:admin-auth / admin-user-management / admin-offer-management / admin-stats / admin-panel-ui
- 歸檔:openspec/changes/archive/2026-05-10-admin-panel

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-10 04:07:13 +08:00
parent da48a3652d
commit ad2c05779d
29 changed files with 1439 additions and 10 deletions
@@ -0,0 +1,125 @@
## Context
後端 Admin Auth 方法(loginAdmin / logoutAdmin / adminProfile / updateAdminProfile)已存在於 AuthController,路由也已佔位。User model 有 `is_active` 欄位;ProviderProfile 有 `is_verified` 欄位,目前無 API 可修改。前端已有 memberAuth / coachAuth 兩套認證模式,Admin 循同一模式新增第三套。
## Goals / Non-Goals
**Goals:**
- 管理員可登入後台、查看平台數據
- 管理員可列表/搜尋會員與教練,並啟用/停用帳號
- 管理員可驗證教練(設定 ProviderProfile.is_verified
- 管理員可查看全平台課程並刪除違規內容
- 前端 `/admin/*` 有獨立 Layout,不顯示會員 NavBar
**Non-Goals:**
- Admin 帳號自助註冊(透過後端 seeder 或直接 DB 建立)
- 細粒度角色權限(RBAC
- 操作日誌(Audit Log
- 批次操作(批量停用)
## Decisions
### D1Admin Auth 沿用現有 AuthController,不新建 Controller
**決定**`loginAdmin``logoutAdmin``adminProfile``updateAdminProfile` 直接沿用,不修改。
**理由**:方法已存在且邏輯完整,路由也已佔位。
---
### D2:業務邏輯拆到獨立 Controller
**決定**:新增 `AdminUserController`(用戶管理)、`AdminOfferController`(課程管理)、`AdminStatsController`(統計)。
**理由**:與 AuthController 職責分開,避免繼續膨脹。所有方法在開頭驗證 `auth()->user()->role === 'admin'`,非管理員回傳 403。
---
### D3Toggle 語意(啟用/停用、驗證/取消驗證)
**決定**`toggle-active``toggle-verified` 為 PUT 端點,後端直接反轉當前值(`is_active = !is_active`),不接受 body 傳入布林值。
**理由**:UI 是單一按鈕切換狀態,反轉語意最直覺,避免前端傳錯值。
---
### D4adminAuth 獨立 StorelocalStorage key `admin_token` / `admin_user`
**決定**:循 coachAuth 模式,新增第三套獨立 store。
**理由**:三種角色可能在不同 tab 同時使用,共用 store 會互相污染。
---
## Contracts
### API Schema
#### `POST /api/admin/login`(現有)
```
Body: { "email": "...", "password": "..." }
Response 200: { "status": true, "data": { "user": {...}, "token": "...", "token_type": "Bearer" } }
```
#### `GET /api/admin/stats`(需 Bearer tokenrole=admin
```
Response 200:
{
"status": true,
"data": {
"total_members": 120,
"total_providers": 18,
"total_offers": 64
}
}
```
#### `GET /api/admin/members`(需 Bearer tokenrole=admin
```
Query: q(搜尋 name / email, page, per_pagedefault 15
Response 200: { "status": true, "data": [...users with memberProfile], "meta": {...} }
```
#### `GET /api/admin/members/{id}`
```
Response 200: { "status": true, "data": { ...user, profile: {...} } }
Response 404: { "status": false, "message": "用戶不存在" }
```
#### `PUT /api/admin/members/{id}/toggle-active`
```
Response 200: { "status": true, "message": "帳號已停用" | "帳號已啟用", "data": { "is_active": false | true } }
Response 404: { "status": false, "message": "用戶不存在" }
```
#### `GET /api/admin/providers`(同 members 結構,含 providerProfile
#### `GET /api/admin/providers/{id}`
#### `PUT /api/admin/providers/{id}/toggle-active`
#### `PUT /api/admin/providers/{id}/toggle-verified`
```
Response 200: { "status": true, "message": "教練已驗證" | "已取消驗證", "data": { "is_verified": true | false } }
```
#### `GET /api/admin/offers`
```
Query: q(搜尋 title / location, page, per_pagedefault 15
Response 200: { "status": true, "data": [...offers with provider_id], "meta": {...} }
```
#### `DELETE /api/admin/offers/{id}`
```
Response 200: { "status": true, "message": "課程已刪除" }
Response 404: { "status": false, "message": "課程不存在" }
```
---
## Risks / Trade-offs
| 風險 | 緩解策略 |
|------|----------|
| toggle 反轉語意若網路重試,可能連按兩次回到原狀態 | MVP 接受,未來可改為明確 `{ is_active: true/false }` body |
| Admin 帳號只能透過 DB 或 seeder 建立,無自助註冊 | 開發期間用 tinker 建立,正式環境透過 seeder |
| `AdminUserController` 對 member / provider 各需重複驗證邏輯 | 用 private helper method 共用,避免複製貼上 |
| `/admin/*` 頁面無額外安全層(任何人知道路徑都可訪問登入頁) | MVP 接受,route guard 在 frontend 層足夠 |