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:
@@ -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
|
||||
|
||||
### D1:Admin Auth 沿用現有 AuthController,不新建 Controller
|
||||
|
||||
**決定**:`loginAdmin`、`logoutAdmin`、`adminProfile`、`updateAdminProfile` 直接沿用,不修改。
|
||||
|
||||
**理由**:方法已存在且邏輯完整,路由也已佔位。
|
||||
|
||||
---
|
||||
|
||||
### D2:業務邏輯拆到獨立 Controller
|
||||
|
||||
**決定**:新增 `AdminUserController`(用戶管理)、`AdminOfferController`(課程管理)、`AdminStatsController`(統計)。
|
||||
|
||||
**理由**:與 AuthController 職責分開,避免繼續膨脹。所有方法在開頭驗證 `auth()->user()->role === 'admin'`,非管理員回傳 403。
|
||||
|
||||
---
|
||||
|
||||
### D3:Toggle 語意(啟用/停用、驗證/取消驗證)
|
||||
|
||||
**決定**:`toggle-active` 和 `toggle-verified` 為 PUT 端點,後端直接反轉當前值(`is_active = !is_active`),不接受 body 傳入布林值。
|
||||
|
||||
**理由**:UI 是單一按鈕切換狀態,反轉語意最直覺,避免前端傳錯值。
|
||||
|
||||
---
|
||||
|
||||
### D4:adminAuth 獨立 Store,localStorage 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 token,role=admin)
|
||||
```
|
||||
Response 200:
|
||||
{
|
||||
"status": true,
|
||||
"data": {
|
||||
"total_members": 120,
|
||||
"total_providers": 18,
|
||||
"total_offers": 64
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `GET /api/admin/members`(需 Bearer token,role=admin)
|
||||
```
|
||||
Query: q(搜尋 name / email), page, per_page(default 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_page(default 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 層足夠 |
|
||||
Reference in New Issue
Block a user