diff --git a/README.md b/README.md index 9b08114..7511fe3 100644 --- a/README.md +++ b/README.md @@ -1,388 +1,112 @@ # Star Cloud 智能販賣機管理平台 -> 基於 Docker 的全方位智能販賣機後台管理系統 +> 基於 Docker 的全方位智能販賣機後台管理系統 (Cloud 平台) -Star Cloud 是一個專為智能販賣機設計的後台管理系統,提供機台監控、庫存管理、銷售分析與會員管理等完整功能。本專案採用 Docker Compose 容器化架構,實現快速部署與環境一致性。 +Star Cloud 是一個專為智能販賣機設計的後台管理系統,負責管理機台、商品、銷售數據,並為硬體端點提供專用的 API。 --- -## 技術架構 +## 🚀 技術架構 -### 容器化架構 -本專案完全運行在 Docker 容器中,包含以下服務: +### 核心架構 +本專案採用 **傳統單體式架構 (Monolithic Architecture)**,結合 Laravel Blade 引擎進行伺服器端渲染 (SSR)。 -| 服務 | 容器名稱 | 技術 | 用途 | 連接埠 | +| 服務 | 容器名稱 | 技術 | 用途 | 本地 Port | |------|---------|------|------|--------| -| **應用程式** | star-cloud-laravel | Laravel 10 + PHP 8.5 | Web 應用與 API | 8090:80, 5175:5175 | -| **資料庫** | star-cloud-mysql | MySQL 8.0 | 關聯式資料庫 | 3306:3306 | -| **快取** | star-cloud-redis | Redis Alpine | 快取與 Session | 6380:6379 | +| **應用程式** | `star-cloud-laravel` | Laravel 12 + PHP 8.5 | 核心業務與渲染 | 8090 | +| **資料庫** | `star-cloud-mysql` | MySQL 8.0 | 數據持久化 | 3306 | +| **快取/隊列** | `star-cloud-redis` | Redis Alpine | IoT 高併發隊列 | 6380 | ### 後端技術棧 - -- **Framework**: Laravel 10.x -- **Language**: PHP 8.5+ +- **Framework**: Laravel 12.x +- **Language**: PHP 8.5 +- **Redis**: 用於 IoT 高併發隊列 (B010, B600 等) - **Database**: MySQL 8.0 -- **Cache/Session**: Redis -- **Authentication**: Laravel Sanctum (API Token) -- **Package Manager**: Composer 2.x ### 前端技術棧 - -- **Template Engine**: Blade Templates -- **UI Library**: Preline UI 3.x (Tailwind CSS 組件庫) -- **CSS Framework**: Tailwind CSS 3.x -- **JavaScript**: Alpine.js 3.x (輕量級互動框架) -- **Build Tool**: Vite 5.x -- **HTTP Client**: Axios +- **View**: Laravel Blade +- **CSS**: Tailwind CSS + **Preline UI** +- **JS**: Alpine.js (行為控制) +- **Build**: Vite --- -## 快速開始 +## 🛠️ 開發環境 (Laravel Sail) + +本專案建議使用 **Laravel Sail** 進行開發,避免直接在宿主機執行指令。 ### 前置需求 +- Docker Desktop (Windows/Mac) 或 Docker Engine (Linux) +- Git -確保您的系統已安裝以下軟體: +### 快速啟動 +1. `clone` 專案並進入目錄 +2. `cp .env.example .env` +3. `./vendor/bin/sail up -d` +4. `./vendor/bin/sail composer install` +5. `./vendor/bin/sail artisan key:generate` +6. `./vendor/bin/sail artisan migrate --seed` +7. `./vendor/bin/sail npm install` +8. `./vendor/bin/sail npm run dev` -- **Docker** 20.10+ -- **Docker Compose** 2.0+ -- **Git** - -> **提示**:Windows 使用者建議安裝 [Docker Desktop](https://www.docker.com/products/docker-desktop/),Linux 使用者可參考 [官方安裝文件](https://docs.docker.com/engine/install/) - -### 安裝步驟 - -#### 1. Clone 專案 - -```bash -git clone -cd star-cloud -``` - -#### 2. 環境設定 - -複製環境變數範例檔案: - -```bash -cp .env.example .env -``` - -**重要設定**(`.env` 檔案): - -```env -# 應用程式設定 -APP_NAME=Star Cloud -APP_ENV=local -APP_DEBUG=true -APP_URL=http://localhost:8090 - -# 資料庫設定(對應 Docker Compose 服務) -DB_CONNECTION=mysql -DB_HOST=mysql -DB_PORT=3306 -DB_DATABASE=star_cloud -DB_USERNAME=sail -DB_PASSWORD=password - -# Redis 設定(對應 Docker Compose 服務) -REDIS_HOST=redis -REDIS_PASSWORD=null -REDIS_PORT=6379 - -# Vite 開發伺服器 -VITE_PORT=5175 -``` - -#### 3. 啟動 Docker 容器 - -啟動所有服務(應用程式、資料庫、Redis): - -```bash -docker compose up -d -``` - -> **說明**:`-d` 參數表示背景執行 - -檢查容器狀態: - -```bash -docker compose ps -``` - -預期輸出: -``` -NAME STATUS PORTS -star-cloud-laravel Up X minutes 0.0.0.0:8090->80/tcp, 0.0.0.0:5175->5175/tcp -star-cloud-mysql Up X minutes 0.0.0.0:3306->3306/tcp -star-cloud-redis Up X minutes 0.0.0.0:6380->6379/tcp -``` - -#### 4. 初始化應用程式 - -**4.1 安裝後端依賴** - -```bash -docker compose exec laravel.test composer install -``` - -**4.2 產生應用程式金鑰** - -```bash -docker compose exec laravel.test php artisan key:generate -``` - -**4.3 執行資料庫遷移與種子** - -```bash -docker compose exec laravel.test php artisan migrate --seed -``` - -> **預設管理員帳號**: -> - Email: `admin` -> - Password: `password` - -**4.4 安裝前端依賴** - -```bash -docker compose exec laravel.test npm install -``` - -**4.5 編譯前端資源** - -```bash -# 開發模式(支援 Hot Module Replacement) -docker compose exec laravel.test npm run dev - -# 或生產模式 -docker compose exec laravel.test npm run build -``` - -#### 5. 訪問應用程式 - -- **應用程式**: http://localhost:8090 -- **Vite Dev Server**: http://localhost:5175 +### 常用開發指令 +| 功能 | 指令 | +|------|------| +| 啟動環境 | `./vendor/bin/sail up -d` | +| 停止環境 | `./vendor/bin/sail down` | +| Artisan 指令 | `./vendor/bin/sail artisan ` | +| Composer 指令 | `./vendor/bin/sail composer ` | +| NPM 指令 | `./vendor/bin/sail npm ` | +| 執行測試 | `./vendor/bin/sail test` | --- -## Docker 常用指令 +## 🔐 API 規範 -### 容器管理 +系統 API 分為兩大類,遵循不同的設計慣例: -```bash -# 啟動所有服務 -docker compose up -d +### 1. Admin/Web API (`/api/v1/...`) +- **對象**: 後台管理介面、APP UI。 +- **認證**: Laravel Sanctum (Session/Token)。 +- **格式**: 標準 RESTful JSON。 -# 停止所有服務 -docker compose down - -# 重啟服務 -docker compose restart - -# 查看容器日誌 -docker compose logs -f laravel.test - -# 進入應用程式容器 -docker compose exec laravel.test bash -``` - -### Laravel 指令 - -所有 Laravel Artisan 指令需在容器內執行: - -```bash -# 執行 Artisan 指令 -docker compose exec laravel.test php artisan - -# 範例:清除快取 -docker compose exec laravel.test php artisan cache:clear - -# 範例:執行 Migration -docker compose exec laravel.test php artisan migrate - -# 範例:建立新 Controller -docker compose exec laravel.test php artisan make:controller ExampleController -``` - -### 前端開發 - -```bash -# 安裝 npm 套件 -docker compose exec laravel.test npm install - -# 開發模式(即時編譯) -docker compose exec laravel.test npm run dev - -# 生產編譯 -docker compose exec laravel.test npm run build -``` - -### 資料庫操作 - -```bash -# 進入 MySQL 容器 -docker compose exec mysql bash - -# 直接執行 SQL -docker compose exec mysql mysql -u sail -ppassword star_cloud - -# 備份資料庫 -docker compose exec mysql mysqldump -u sail -ppassword star_cloud > backup.sql - -# 還原資料庫 -docker compose exec -T mysql mysql -u sail -ppassword star_cloud < backup.sql -``` +### 2. Machine IoT API (`/api/app/...`) +- **對象**: 智能販賣機、計時器等硬體。 +- **認證**: Header `Authorization: Bearer `。 +- **高併發處理**: 核心日誌 (B010 心跳、B600 交易) **嚴禁直寫 DB**,必須進入 **Redis Queue** 背景異步處理。 --- -## 主要功能模組 +## 🌐 多語系支援 (I18n) -### 核心功能 - -| 模組 | 功能描述 | -|------|---------| -| **儀錶板** | 銷售數據總覽、機台狀態即時監控、營收統計圖表 | -| **機台管理** | 機台列表、遠端控制、日誌查詢、維修管理、效期控制 | -| **倉庫管理** | 倉庫列表、庫存管理、調撥單、採購單、補貨單 | -| **商品管理** | 商品資料、分類管理、商品報表分析 | -| **銷售管理** | 交易紀錄、金流管理、促銷設定、營收報表 | -| **會員系統** | 會員管理、點數系統、來店禮、Line 整合 | -| **權限控制** | 角色管理、權限分配、功能權限設定 | -| **遠端管理** | 機台重啟、遠端出貨、遠端結帳、庫存調整 | +所有 UI 顯示文字必須支援多語系,禁止 Hard-coded。 +- **語系檔案**: `lang/zh_TW.json`, `lang/en.json`, `lang/ja.json`。 +- **呼叫方式**: 使用 `__('Phrases in English')` 或 `@lang('...')`。 +- **命名規範**: 優先使用「英文原始詞彙」作為 Key 名稱。 --- -## Preline UI 組件庫 +## 📂 目錄結構 -本專案已整合 **Preline UI 3.x**,這是一個基於 Tailwind CSS 的開源 UI 組件庫,提供 50+ 預構建組件。 - -### 可用組件類別 - -- **Navigation**: 導航列、側邊欄、分頁、麵包屑、頁籤 -- **Forms**: 輸入框、選擇器、開關、檔案上傳、日期選擇器 -- **Overlays**: 模態框、抽屜、下拉選單、提示框、彈出框 -- **Data Display**: 表格、卡片、時間軸、折疊面板、徽章 -- **Feedback**: 通知、警告、載入狀態、進度條 - -### 使用範例 - -```html - -
- - -
- - - - -``` - -**更多資源**: -- 官方文件: https://preline.co/docs/ -- 組件範例: https://preline.co/examples.html -- GitHub: https://github.com/htmlstreamofficial/preline +- `app/Http/Controllers/`: 控制器 +- `app/Models/{Domain}/`: 分領域的模型 (如 `Machine`, `Member`) +- `app/Services/{Domain}/`: 封裝商業邏輯與資料異動 +- `app/Jobs/{Domain}/`: IoT 異步隊列處理任務 (重要) +- `resources/views/`: Blade 模板 (按功能分資料夾) +- `resources/views/components/`: 可重用的 UI 組件 +- `routes/`: 路由定義 (`web.php` 與 `api.php`) --- -## 故障排除 +## 🚢 CI/CD 與部署 -### 容器無法啟動 - -```bash -# 檢查容器日誌 -docker compose logs - -# 重建容器 -docker compose down -docker compose up -d --build -``` - -### 連接資料庫失敗 - -確認 `.env` 中 `DB_HOST` 設定為 `mysql`(容器服務名稱),而非 `127.0.0.1`。 - -### 前端資源編譯失敗 - -```bash -# 清除 node_modules 重新安裝 -docker compose exec laravel.test rm -rf node_modules -docker compose exec laravel.test npm install -docker compose exec laravel.test npm run build -``` - -### 權限問題 - -```bash -# 修正儲存目錄權限 -docker compose exec laravel.test chmod -R 775 storage bootstrap/cache -``` - ---- - -## 部署至生產環境 - -### 1. 環境變數設定 - -將 `.env` 中的設定調整為生產環境: - -```env -APP_ENV=production -APP_DEBUG=false -APP_URL=https://your-domain.com -``` - -### 2. 編譯前端資源 - -```bash -docker compose exec laravel.test npm run build -``` - -### 3. 優化 Laravel - -```bash -docker compose exec laravel.test php artisan config:cache -docker compose exec laravel.test php artisan route:cache -docker compose exec laravel.test php artisan view:cache -``` - -### 4. 設定 HTTPS - -建議使用 Nginx Reverse Proxy + Let's Encrypt SSL 憑證。 - ---- - -## 開發團隊協作 - -### Git Workflow - -```bash -# 拉取最新程式碼 -git pull origin main - -# 重建容器(若 Docker 設定有變更) -docker compose down -docker compose up -d - -# 更新依賴 -docker compose exec laravel.test composer install -docker compose exec laravel.test npm install - -# 執行 Migration -docker compose exec laravel.test php artisan migrate -``` +- **自動化工具**: Gitea Actions (`.gitea/workflows/`)。 +- **Demo 環境**: 推送到 `demo` 分支會自動部署至 `demo-cloud.taiwan-star.com.tw`。 +- **伺服器路徑**: `/var/www/star-cloud-demo` (透過 `ssh gitea_work` 登入)。 --- ## 授權與版權 - © Star Cloud. All Rights Reserved. --- diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php index 3e0f699..71df662 100644 --- a/app/Http/Controllers/ProfileController.php +++ b/app/Http/Controllers/ProfileController.php @@ -7,6 +7,7 @@ use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Redirect; +use Illuminate\Support\Facades\Storage; use Illuminate\View\View; class ProfileController extends Controller @@ -17,17 +18,10 @@ class ProfileController extends Controller public function edit(Request $request): View { $user = $request->user(); - - // 如果沒有歷史紀錄,注入一些模擬資料供預覽 - if ($user->loginLogs()->count() === 0) { - $user->loginLogs()->createMany([ - ['ip_address' => '127.0.0.1', 'user_agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)', 'login_at' => now()->subHours(2)], - ['ip_address' => '192.168.1.100', 'user_agent' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X)', 'login_at' => now()->subDays(1)], - ]); - } return view('profile.edit', [ - 'user' => $user->load(['loginLogs' => fn($q) => $q->latest()]), + // 只取最新 10 筆登入紀錄 + 'user' => $user->load(['loginLogs' => fn($q) => $q->latest('login_at')->limit(10)]), ]); } @@ -36,13 +30,24 @@ class ProfileController extends Controller */ public function update(ProfileUpdateRequest $request): RedirectResponse { - $request->user()->fill($request->validated()); + $user = $request->user(); + $user->fill($request->validated()); - if ($request->user()->isDirty('email')) { - $request->user()->email_verified_at = null; + if ($user->isDirty('email')) { + $user->email_verified_at = null; } - $request->user()->save(); + if ($request->hasFile('avatar')) { + // Delete old avatar if exists + if ($user->avatar) { + Storage::disk('public')->delete($user->avatar); + } + + $path = $request->file('avatar')->store('avatars', 'public'); + $user->avatar = $path; + } + + $user->save(); return Redirect::route('profile.edit')->with('status', 'profile-updated'); } diff --git a/app/Http/Requests/ProfileUpdateRequest.php b/app/Http/Requests/ProfileUpdateRequest.php index 3dc5c7f..f4a10f8 100644 --- a/app/Http/Requests/ProfileUpdateRequest.php +++ b/app/Http/Requests/ProfileUpdateRequest.php @@ -19,7 +19,7 @@ class ProfileUpdateRequest extends FormRequest 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', Rule::unique(User::class)->ignore($this->user()->id)], 'phone' => ['nullable', 'string', 'max:20'], - 'avatar' => ['nullable', 'string', 'max:255'], + 'avatar' => ['nullable', 'image', 'mimes:jpeg,png,jpg,gif', 'max:2048'], ]; } } diff --git a/app/Listeners/LogSuccessfulLogin.php b/app/Listeners/LogSuccessfulLogin.php new file mode 100644 index 0000000..c10fc79 --- /dev/null +++ b/app/Listeners/LogSuccessfulLogin.php @@ -0,0 +1,44 @@ +request = $request; + } + + /** + * Handle the event. + * + * @param \Illuminate\Auth\Events\Login $event + * @return void + */ + public function handle(Login $event) + { + UserLoginLog::create([ + 'user_id' => $event->user->id, + 'ip_address' => $this->request->ip(), + 'user_agent' => $this->request->userAgent(), + 'login_at' => now(), + ]); + } +} diff --git a/app/Models/System/User.php b/app/Models/System/User.php index f6abf5c..5270eb0 100644 --- a/app/Models/System/User.php +++ b/app/Models/System/User.php @@ -54,4 +54,16 @@ class User extends Authenticatable { return $this->hasMany(\App\Models\UserLoginLog::class); } + + /** + * Get the user's avatar URL. + */ + public function getAvatarUrlAttribute(): string + { + if ($this->avatar) { + return asset('storage/' . $this->avatar); + } + + return "https://ui-avatars.com/api/?name=" . urlencode($this->name) . "&background=0D8ABC&color=fff"; + } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index b395363..7cf7f53 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,6 +2,9 @@ namespace App\Providers; +use App\Listeners\LogSuccessfulLogin; +use Illuminate\Auth\Events\Login; +use Illuminate\Support\Facades\Event; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider @@ -22,5 +25,9 @@ class AppServiceProvider extends ServiceProvider if (!$this->app->isLocal()) { \Illuminate\Support\Facades\URL::forceScheme('https'); } + + // 記錄使用者成功登入的歷史 + Event::listen(Login::class, LogSuccessfulLogin::class); } } + diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 2d65aac..29725d2 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -2,6 +2,8 @@ namespace App\Providers; +use App\Listeners\LogSuccessfulLogin; +use Illuminate\Auth\Events\Login; use Illuminate\Auth\Events\Registered; use Illuminate\Auth\Listeners\SendEmailVerificationNotification; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; @@ -18,6 +20,9 @@ class EventServiceProvider extends ServiceProvider Registered::class => [ SendEmailVerificationNotification::class, ], + Login::class => [ + LogSuccessfulLogin::class, + ], ]; /** diff --git a/lang/ja.json b/lang/ja.json index ace0f5b..0333540 100644 --- a/lang/ja.json +++ b/lang/ja.json @@ -146,5 +146,10 @@ "Sales": "販売", "Others": "その他", "AI Prediction": "AI予測", - "Roles": "ロール" + "Roles": "ロール", + "Yesterday": "昨日", + "Day Before": "一昨日", + "No login history yet": "ログイン履歴はまだありません", + "Signed in as": "ログイン中", + "Logout": "ログアウト" } diff --git a/lang/zh_TW.json b/lang/zh_TW.json index 9e9b0a8..70d1d6a 100644 --- a/lang/zh_TW.json +++ b/lang/zh_TW.json @@ -39,6 +39,8 @@ "Today's Transactions": "今日交易", "Yesterday's Transactions": "昨日交易", "Before Yesterday's Transactions": "前日交易", + "Yesterday": "昨日", + "Day Before": "前日", "vs Yesterday": "比昨日", "Machine Status List": "機台狀態列表", "Total items": "共 :count 筆", @@ -146,5 +148,8 @@ "Sales": "銷售管理", "Others": "其他功能", "AI Prediction": "AI智能預測", - "Roles": "角色設定" + "Roles": "角色設定", + "No login history yet": "尚無登入紀錄", + "Signed in as": "登入身份", + "Logout": "登出" } diff --git a/resources/css/app.css b/resources/css/app.css index f4ad827..cf477a8 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -29,7 +29,7 @@ /* Luxury Cards */ .luxury-card { - @apply bg-white dark:bg-[#1e293b] border-0; + @apply bg-white dark:bg-[#1e293b] border-0 rounded-2xl; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -2px rgba(0, 0, 0, 0.05); transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1); } diff --git a/resources/views/admin/dashboard.blade.php b/resources/views/admin/dashboard.blade.php index 280c8c1..26c27c3 100644 --- a/resources/views/admin/dashboard.blade.php +++ b/resources/views/admin/dashboard.blade.php @@ -8,8 +8,8 @@
-

{{ __('Connectivity Status') }}

-

{{ __('Real-time status monitoring') }}

+

{{ __('Connectivity Status') }}

+

{{ __('Real-time status monitoring') }}

@@ -26,21 +26,21 @@
- {{ __('Online Machines') }} + {{ __('Online Machines') }}
{{ $activeMachines }}
- {{ __('Offline Machines') }} + {{ __('Offline Machines') }}
{{ $alertsPending }}
- {{ __('Alerts Pending') }} + {{ __('Alerts Pending') }}
0
@@ -52,7 +52,7 @@

{{ $activeMachines }}

-

{{ __('Total Connected') }}

+

{{ __('Total Connected') }}

@@ -61,8 +61,8 @@
-

{{ __('Monthly Transactions') }}

-

{{ __('Monthly cumulative revenue overview') }}

+

{{ __('Monthly Transactions') }}

+

{{ __('Monthly cumulative revenue overview') }}

@@ -77,8 +77,8 @@
-

{{ __("Today's Transactions") }}

-

${{ number_format($totalRevenue / 30, 0) }}

+

{{ __("Today's Transactions") }}

+

${{ number_format($totalRevenue / 30, 0) }}

@@ -92,7 +92,7 @@
-

{{ __("Yesterday's Transactions") }}

+

{{ __("Yesterday") }}

@@ -103,7 +103,7 @@
-

{{ __("Before Yesterday's Transactions") }}

+

{{ __("Day Before") }}

@@ -120,12 +120,12 @@
-

{{ __('Machine Status List') }}

+

{{ __('Machine Status List') }}

{{ __('Total items', ['count' => count($latestActivities)]) }}
-

{{ __('Real-time monitoring across all machines') }}

+

{{ __('Real-time monitoring across all machines') }}

diff --git a/resources/views/layouts/admin.blade.php b/resources/views/layouts/admin.blade.php index 8578075..eebd2fa 100644 --- a/resources/views/layouts/admin.blade.php +++ b/resources/views/layouts/admin.blade.php @@ -129,7 +129,7 @@