Compare commits
6 Commits
8ee14eaa29
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86b0cdb1e1 | ||
|
|
0a4872e6c9 | ||
|
|
c4978a389f | ||
|
|
c4807e23f2 | ||
|
|
fb7d0078c6 | ||
| 4bf4898e6a |
@@ -100,21 +100,34 @@ Request / Response 均採 JSON,個資欄位請遵守最小授權原則。
|
|||||||
|
|
||||||
### 3) Machine (機台)
|
### 3) Machine (機台)
|
||||||
|
|
||||||
* **GET /api/v1/machines**
|
* GET /api/v1/machines
|
||||||
* Params: page, per_page, status
|
|
||||||
* **GET /api/v1/machines/{id}**
|
* Params: page, per_page, status
|
||||||
* **POST /api/v1/machines/{id}/logs** (IoT)
|
|
||||||
* 用於機台回傳日誌,後端固定走 **Redis Queue 異步寫入**。
|
* GET /api/v1/machines/{id}
|
||||||
* 回傳 `202 Accepted` 表示任務已接收,由 `ProcessMachineLog` 背景處理。
|
|
||||||
* Request Example:
|
* POST /api/v1/machines
|
||||||
|
|
||||||
|
* PUT /api/v1/machines/{id}
|
||||||
|
|
||||||
|
* DELETE /api/v1/machines/{id}
|
||||||
|
|
||||||
|
* POST /api/v1/machines/{id}/status
|
||||||
|
|
||||||
|
* 用於下位機或 APP 回傳機台狀態
|
||||||
|
* request example:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"level": "info",
|
"temperature": 23.4,
|
||||||
"message": "Temperature stabilized at 23C",
|
"status_code": "OK",
|
||||||
"context": { "temp": 23.0 }
|
"firmware_version": "1.2.3",
|
||||||
|
"timestamp": "2025-11-20T15:00:00Z"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
* GET /api/v1/machines/{id}/logs
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 4) Orders / ShoppingCart
|
### 4) Orders / ShoppingCart
|
||||||
528
.agent/rules/backend-rules.md
Normal file
528
.agent/rules/backend-rules.md
Normal file
@@ -0,0 +1,528 @@
|
|||||||
|
---
|
||||||
|
trigger: always_on
|
||||||
|
---
|
||||||
|
|
||||||
|
STAR CLOUD 後台管理系統 - 智能販賣機管理平台
|
||||||
|
|
||||||
|
一、儀錶板模組 (Dashboard)
|
||||||
|
1.1 主頁面
|
||||||
|
|
||||||
|
功能描述: 系統總覽與關鍵數據展示
|
||||||
|
主要內容:
|
||||||
|
|
||||||
|
即時銷售數據
|
||||||
|
機台運行狀態
|
||||||
|
庫存警示
|
||||||
|
營收統計圖表
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
二、應用程式管理 (Application Management)
|
||||||
|
2.1 個人檔案
|
||||||
|
|
||||||
|
功能描述: 使用者個人資訊管理
|
||||||
|
包含功能:
|
||||||
|
|
||||||
|
基本資料編輯
|
||||||
|
密碼修改
|
||||||
|
通知設定
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
三、機台管理模組 (Machine Management)
|
||||||
|
3.1 機台日誌
|
||||||
|
|
||||||
|
功能描述: 機台操作歷史紀錄回溯
|
||||||
|
資料內容:
|
||||||
|
|
||||||
|
操作時間戳記
|
||||||
|
事件類型
|
||||||
|
操作人員
|
||||||
|
詳細描述
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
3.2 機台列表
|
||||||
|
|
||||||
|
功能描述: 所有機台資訊總覽
|
||||||
|
顯示資訊:
|
||||||
|
|
||||||
|
溫度監控
|
||||||
|
下位機狀態
|
||||||
|
刷卡機連線
|
||||||
|
掃碼機狀態
|
||||||
|
機台回傳訊息
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
3.3 機台權限
|
||||||
|
|
||||||
|
功能描述: 機台存取權限控管
|
||||||
|
設定項目:
|
||||||
|
|
||||||
|
人員權限分配
|
||||||
|
操作級別設定
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
3.4 機台稼動率
|
||||||
|
|
||||||
|
功能描述: 機台運行效率分析
|
||||||
|
統計數據:
|
||||||
|
|
||||||
|
運行時間
|
||||||
|
停機時間
|
||||||
|
稼動率百分比
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
3.5 效期管理
|
||||||
|
|
||||||
|
功能描述: 商品效期與貨道出貨控制
|
||||||
|
管理項目:
|
||||||
|
|
||||||
|
設定貨道是否可出貨
|
||||||
|
效期到期提醒
|
||||||
|
商品下架設定
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
3.6 維修管理單
|
||||||
|
|
||||||
|
功能描述: 機台維修工單系統
|
||||||
|
包含功能:
|
||||||
|
|
||||||
|
報修單建立
|
||||||
|
維修進度追蹤
|
||||||
|
維修歷史紀錄
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
3.7 機台管理擴充欄位
|
||||||
|
|
||||||
|
新增欄位:
|
||||||
|
|
||||||
|
保固區間
|
||||||
|
交機日期
|
||||||
|
租賃區間
|
||||||
|
保內/保外狀態顯示
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
3.8 機台設定參數
|
||||||
|
|
||||||
|
刷卡機秒數: 刷卡逾時設定
|
||||||
|
卡機結帳時間: 結帳流程時間限制
|
||||||
|
卡機結帳時間2: 備用結帳時間設定
|
||||||
|
金流緩衝時間: 金流處理緩衝時間
|
||||||
|
刷卡機編號: 刷卡機裝置識別
|
||||||
|
發票狀態碼: 發票開立狀態管理
|
||||||
|
|
||||||
|
3.9 APP到期提醒
|
||||||
|
|
||||||
|
功能描述: APP授權到期通知系統
|
||||||
|
|
||||||
|
|
||||||
|
四、APP管理模組 (APP Management)
|
||||||
|
4.1 UI元素設定
|
||||||
|
|
||||||
|
功能描述: APP版面配置設定
|
||||||
|
注意事項: 與新版差異較大,需特別處理
|
||||||
|
|
||||||
|
4.2 小幫手設定
|
||||||
|
|
||||||
|
功能描述: APP內建輔助功能設定
|
||||||
|
|
||||||
|
4.3 問卷設定
|
||||||
|
|
||||||
|
功能描述: 互動問卷建立與管理
|
||||||
|
|
||||||
|
4.4 互動遊戲設定
|
||||||
|
|
||||||
|
功能描述: APP互動遊戲配置
|
||||||
|
|
||||||
|
4.5 計時器
|
||||||
|
|
||||||
|
功能描述: 時間相關功能設定
|
||||||
|
|
||||||
|
|
||||||
|
五、倉庫管理模組 (Warehouse Management)
|
||||||
|
5.1 倉庫列表
|
||||||
|
|
||||||
|
5.1.1 倉庫列表(全部): 顯示所有倉庫
|
||||||
|
5.1.2 倉庫列表(個人): 顯示個人負責倉庫
|
||||||
|
|
||||||
|
5.2 庫存管理單
|
||||||
|
|
||||||
|
功能描述: 倉庫庫存異動管理
|
||||||
|
|
||||||
|
5.3 調撥單
|
||||||
|
|
||||||
|
功能描述: 倉庫間商品調撥作業
|
||||||
|
|
||||||
|
5.4 採購單
|
||||||
|
|
||||||
|
功能描述: 商品採購申請與管理
|
||||||
|
|
||||||
|
5.5 機台補貨管理
|
||||||
|
|
||||||
|
5.5.1 機台補貨單: 補貨工單建立
|
||||||
|
5.5.2 機台補貨紀錄: 個別補貨歷史
|
||||||
|
5.5.3 機台補貨紀錄(總): 所有補貨總覽
|
||||||
|
|
||||||
|
5.6 庫存查詢
|
||||||
|
|
||||||
|
5.6.1 機台庫存: 各機台即時庫存
|
||||||
|
5.6.2 人員庫存: 人員持有庫存
|
||||||
|
|
||||||
|
5.7 回庫單
|
||||||
|
|
||||||
|
功能描述: 商品退回倉庫管理
|
||||||
|
|
||||||
|
|
||||||
|
六、銷售管理模組 (Sales Management)
|
||||||
|
6.1 銷售&金流紀錄
|
||||||
|
|
||||||
|
功能描述: 銷售交易與金流明細
|
||||||
|
包含項目:
|
||||||
|
|
||||||
|
現金出貨 API
|
||||||
|
發票系統整合
|
||||||
|
各種出貨方式整理
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
6.2 取貨碼設定
|
||||||
|
|
||||||
|
功能描述: 取貨驗證碼管理
|
||||||
|
|
||||||
|
6.3 購買單
|
||||||
|
|
||||||
|
功能描述: 購買訂單管理
|
||||||
|
|
||||||
|
6.4 促銷時段設定
|
||||||
|
|
||||||
|
功能描述: 促銷活動時間設定
|
||||||
|
重要功能: (W) 重啟掃描商品 API
|
||||||
|
|
||||||
|
6.5 通行碼設定
|
||||||
|
|
||||||
|
功能描述: 特殊通行碼權限管理
|
||||||
|
|
||||||
|
6.6 來店禮設定
|
||||||
|
|
||||||
|
功能描述: 來店優惠活動設定
|
||||||
|
包含: 來店禮開關控制
|
||||||
|
|
||||||
|
|
||||||
|
七、分析管理模組 (Analysis Management)
|
||||||
|
7.1 零錢庫存分析
|
||||||
|
|
||||||
|
功能描述: 機台零錢數量監測與分析
|
||||||
|
|
||||||
|
7.2 機台報表分析
|
||||||
|
|
||||||
|
功能描述: 機台運營數據分析報表
|
||||||
|
|
||||||
|
7.3 商品報表分析
|
||||||
|
|
||||||
|
功能描述: 商品銷售數據分析
|
||||||
|
|
||||||
|
7.4 互動問卷分析
|
||||||
|
|
||||||
|
功能描述: 問卷結果統計與分析
|
||||||
|
|
||||||
|
|
||||||
|
八、稽核管理模組 (Audit Management)
|
||||||
|
8.1 採購單稽核
|
||||||
|
|
||||||
|
功能描述: 採購單審核流程
|
||||||
|
|
||||||
|
8.2 調撥單稽核
|
||||||
|
|
||||||
|
功能描述: 調撥單審核流程
|
||||||
|
|
||||||
|
8.3 補貨單稽核
|
||||||
|
|
||||||
|
功能描述: 補貨單審核流程
|
||||||
|
|
||||||
|
|
||||||
|
九、資料設定模組 (Data Configuration)
|
||||||
|
9.1 機台管理
|
||||||
|
|
||||||
|
功能描述: 機台基本資料設定
|
||||||
|
|
||||||
|
9.2 商品管理
|
||||||
|
|
||||||
|
功能描述: 商品資料維護
|
||||||
|
|
||||||
|
9.3 廣告管理
|
||||||
|
|
||||||
|
功能描述: 機台廣告影片管理
|
||||||
|
用途: 機台可讀取後台廣告影片
|
||||||
|
|
||||||
|
9.4 管理者可賣商品
|
||||||
|
|
||||||
|
功能描述: 管理者商品銷售權限
|
||||||
|
|
||||||
|
9.5 帳號管理
|
||||||
|
|
||||||
|
功能描述: 主帳號管理
|
||||||
|
|
||||||
|
9.6 子帳號管理
|
||||||
|
|
||||||
|
功能描述: 子帳號建立與管理
|
||||||
|
|
||||||
|
9.7 子帳號角色管理
|
||||||
|
|
||||||
|
功能描述: 子帳號權限角色設定
|
||||||
|
|
||||||
|
9.8 點數設定
|
||||||
|
|
||||||
|
功能描述: 客戶點數系統設定
|
||||||
|
特殊功能: 支援客戶自行新增點數
|
||||||
|
|
||||||
|
9.9 識別證管理
|
||||||
|
|
||||||
|
功能描述: 識別證資料管理
|
||||||
|
用途: 安霸系統使用
|
||||||
|
|
||||||
|
|
||||||
|
十、遠端管理模組 (Remote Management)
|
||||||
|
10.1 機台庫存
|
||||||
|
|
||||||
|
功能描述: 遠端修改機台庫存
|
||||||
|
|
||||||
|
10.2 機台重啟
|
||||||
|
|
||||||
|
功能描述: 遠端重啟機台系統
|
||||||
|
|
||||||
|
10.3 卡機重啟
|
||||||
|
|
||||||
|
功能描述: 遠端重啟刷卡機
|
||||||
|
|
||||||
|
10.4 遠端結帳
|
||||||
|
|
||||||
|
功能描述: 遠端執行結帳流程
|
||||||
|
|
||||||
|
10.5 遠端鎖定頁
|
||||||
|
|
||||||
|
功能描述: 遠端鎖定機台頁面
|
||||||
|
|
||||||
|
10.6 遠端找零
|
||||||
|
|
||||||
|
功能描述: 遠端執行找零功能
|
||||||
|
|
||||||
|
10.7 遠端出貨
|
||||||
|
|
||||||
|
功能描述: 遠端控制商品出貨
|
||||||
|
|
||||||
|
|
||||||
|
十一、Line管理模組 (Line Integration)
|
||||||
|
11.1 Line會員管理
|
||||||
|
|
||||||
|
功能描述: Line會員資料管理
|
||||||
|
|
||||||
|
11.2 Line機台管理
|
||||||
|
|
||||||
|
功能描述: Line綁定機台管理
|
||||||
|
|
||||||
|
11.3 Line商品管理
|
||||||
|
|
||||||
|
功能描述: Line商城商品設定
|
||||||
|
|
||||||
|
11.4 Line生活圈
|
||||||
|
|
||||||
|
功能描述: Line官方帳號整合
|
||||||
|
|
||||||
|
11.5 Line商城訂單
|
||||||
|
|
||||||
|
功能描述: Line商城訂單管理
|
||||||
|
|
||||||
|
11.6 Line優惠券
|
||||||
|
|
||||||
|
功能描述: Line優惠券發放與管理
|
||||||
|
|
||||||
|
|
||||||
|
十二、預約系統模組 (Reservation System)
|
||||||
|
12.1 Line會員管理
|
||||||
|
|
||||||
|
功能描述: 預約系統會員管理
|
||||||
|
|
||||||
|
12.2 Line店家管理
|
||||||
|
|
||||||
|
功能描述: 店家資訊設定
|
||||||
|
|
||||||
|
12.3 Line時段組合
|
||||||
|
|
||||||
|
功能描述: 預約時段設定
|
||||||
|
|
||||||
|
12.4 Line場地管理
|
||||||
|
|
||||||
|
功能描述: 場地資源管理
|
||||||
|
|
||||||
|
12.5 Line優惠券管理
|
||||||
|
|
||||||
|
功能描述: 預約優惠券管理
|
||||||
|
|
||||||
|
12.6 Line預約管理
|
||||||
|
|
||||||
|
功能描述: 預約單管理
|
||||||
|
|
||||||
|
12.7 Line訂單管理
|
||||||
|
|
||||||
|
功能描述: 預約訂單處理
|
||||||
|
|
||||||
|
|
||||||
|
十三、特殊權限管理模組 (Special Permissions)
|
||||||
|
13.1 庫存清空
|
||||||
|
|
||||||
|
功能描述: 特殊權限庫存清空功能
|
||||||
|
|
||||||
|
13.2 APK版本管理
|
||||||
|
|
||||||
|
功能描述: APP版本控制與更新
|
||||||
|
|
||||||
|
13.3 Discord通知設定
|
||||||
|
|
||||||
|
功能描述: Discord通知整合設定
|
||||||
|
|
||||||
|
|
||||||
|
十四、權限設定模組 (Permission Management)
|
||||||
|
14.1 功能權限設定
|
||||||
|
|
||||||
|
14.1.1 APP功能管理: APP功能權限
|
||||||
|
14.1.2 資料設定: 資料設定權限
|
||||||
|
14.1.3 銷售管理: 銷售管理權限
|
||||||
|
14.1.4 機台管理: 機台管理權限
|
||||||
|
14.1.5 倉庫管理: 倉庫管理權限
|
||||||
|
14.1.6 分析管理: 分析管理權限
|
||||||
|
14.1.7 稽核管理: 稽核管理權限
|
||||||
|
14.1.8 遠端管理: 遠端管理權限
|
||||||
|
14.1.9 Line管理: Line管理權限
|
||||||
|
|
||||||
|
14.2 權限角色設定
|
||||||
|
|
||||||
|
功能描述: 角色權限組合設定
|
||||||
|
|
||||||
|
14.3 其他功能管理
|
||||||
|
|
||||||
|
功能描述: 其他特殊功能權限
|
||||||
|
|
||||||
|
14.4 AI智能預測
|
||||||
|
|
||||||
|
功能描述: AI功能權限設定
|
||||||
|
|
||||||
|
|
||||||
|
資料庫設計建議
|
||||||
|
主要資料表規劃
|
||||||
|
machines # 機台資料表
|
||||||
|
├── warehouses # 倉庫資料表
|
||||||
|
├── products # 商品資料表
|
||||||
|
├── machine_stocks # 機台庫存表
|
||||||
|
├── warehouse_stocks # 倉庫庫存表
|
||||||
|
├── sales_records # 銷售紀錄表
|
||||||
|
├── purchase_orders # 採購單表
|
||||||
|
├── transfer_orders # 調撥單表
|
||||||
|
├── replenishment_orders # 補貨單表
|
||||||
|
├── maintenance_orders # 維修單表
|
||||||
|
├── machine_logs # 機台日誌表
|
||||||
|
├── users # 使用者表
|
||||||
|
├── roles # 角色表
|
||||||
|
├── permissions # 權限表
|
||||||
|
├── line_members # Line會員表
|
||||||
|
├── reservations # 預約表
|
||||||
|
└── advertisements # 廣告表
|
||||||
|
|
||||||
|
API 端點規劃
|
||||||
|
機台管理 API
|
||||||
|
|
||||||
|
GET /api/machines - 取得機台列表
|
||||||
|
POST /api/machines - 新增機台
|
||||||
|
PUT /api/machines/{id} - 更新機台
|
||||||
|
DELETE /api/machines/{id} - 刪除機台
|
||||||
|
GET /api/machines/{id}/logs - 取得機台日誌
|
||||||
|
POST /api/machines/{id}/restart - 遠端重啟
|
||||||
|
|
||||||
|
倉庫管理 API
|
||||||
|
|
||||||
|
GET /api/warehouses - 取得倉庫列表
|
||||||
|
GET /api/warehouses/{id}/stocks - 取得倉庫庫存
|
||||||
|
POST /api/transfer-orders - 建立調撥單
|
||||||
|
POST /api/purchase-orders - 建立採購單
|
||||||
|
POST /api/replenishment-orders - 建立補貨單
|
||||||
|
|
||||||
|
銷售管理 API
|
||||||
|
|
||||||
|
GET /api/sales - 取得銷售紀錄
|
||||||
|
POST /api/sales/cash - 現金出貨
|
||||||
|
GET /api/sales/invoice - 發票查詢
|
||||||
|
POST /api/pickup-codes - 建立取貨碼
|
||||||
|
|
||||||
|
遠端控制 API
|
||||||
|
|
||||||
|
POST /api/remote/checkout - 遠端結帳
|
||||||
|
POST /api/remote/dispense - 遠端出貨
|
||||||
|
POST /api/remote/change - 遠端找零
|
||||||
|
POST /api/remote/lock - 遠端鎖定
|
||||||
|
|
||||||
|
|
||||||
|
技術架構建議
|
||||||
|
後端技術
|
||||||
|
|
||||||
|
框架: Laravel 10+
|
||||||
|
資料庫: MySQL 8.0+
|
||||||
|
快取: Redis
|
||||||
|
佇列: Laravel Queue (Redis Driver)
|
||||||
|
API文件: Swagger/OpenAPI
|
||||||
|
|
||||||
|
前端技術
|
||||||
|
|
||||||
|
模板引擎: Blade
|
||||||
|
CSS框架: Tailwind CSS
|
||||||
|
JavaScript: Alpine.js / Vue.js
|
||||||
|
圖表庫: Chart.js / ApexCharts
|
||||||
|
|
||||||
|
第三方整合
|
||||||
|
|
||||||
|
Line API: Line Messaging API
|
||||||
|
Discord: Discord Webhook
|
||||||
|
金流: 藍新、綠界等
|
||||||
|
發票: 電子發票整合
|
||||||
|
|
||||||
|
|
||||||
|
開發優先順序建議
|
||||||
|
Phase 1 - 核心功能 (1-2個月)
|
||||||
|
|
||||||
|
使用者認證與權限系統
|
||||||
|
機台管理基本功能
|
||||||
|
倉庫管理基本功能
|
||||||
|
銷售紀錄查詢
|
||||||
|
|
||||||
|
Phase 2 - 進階功能 (2-3個月)
|
||||||
|
|
||||||
|
遠端控制功能
|
||||||
|
報表分析功能
|
||||||
|
稽核流程
|
||||||
|
APP管理功能
|
||||||
|
|
||||||
|
Phase 3 - 整合功能 (1-2個月)
|
||||||
|
|
||||||
|
Line整合
|
||||||
|
預約系統
|
||||||
|
AI智能預測
|
||||||
|
Discord通知
|
||||||
|
|
||||||
|
|
||||||
|
注意事項
|
||||||
|
|
||||||
|
安全性: 所有遠端控制功能需要雙重驗證
|
||||||
|
權限控管: 嚴格的角色權限分離
|
||||||
|
日誌記錄: 所有重要操作需記錄日誌
|
||||||
|
API限流: 防止API濫用
|
||||||
|
資料備份: 定期自動備份機制
|
||||||
|
錯誤處理: 完善的異常處理機制
|
||||||
|
測試: 重要功能需撰寫測試案例
|
||||||
|
RetryClaude can make mistakes. Please double-check responses.
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
---
|
|
||||||
trigger: always_on
|
|
||||||
---
|
|
||||||
|
|
||||||
# 開發框架規範說明書:Cloud 後台管理系統 (star-cloud)
|
|
||||||
|
|
||||||
## 1. 專案概述
|
|
||||||
* **目標**:打造一個強大且穩定的智能販賣機後台管理系統(Cloud 平台),負責管理機台、商品、銷售數據以及提供給端點機台串接的 API。
|
|
||||||
* **核心架構**:採用 **傳統單體式架構 (Monolithic Architecture)** 配 Laravel Blade 模板引擎進行伺服器端渲染 (SSR)。
|
|
||||||
* **工作流程**:後端處理業務邏輯與資料庫存取,並透過 Blade 引擎渲染包含 Tailwind CSS 類別的 HTML。前端互動行為由輕量級 Alpine.js 負責,UI 元件以 Preline UI 為主體。
|
|
||||||
|
|
||||||
## 2. 技術棧 (Tech Stack)
|
|
||||||
* **後端框架**:PHP 8.5 / Laravel 12
|
|
||||||
* **核心組件**:Redis (用於高併發 IoT 隊列與快取,為系統穩定之必要條件)
|
|
||||||
* **前端視圖 (View)**:Laravel Blade
|
|
||||||
* **前端互動 (JS)**:Alpine.js (專注於行為,不負責渲染)
|
|
||||||
* **介面與樣式 (CSS)**:Tailwind CSS + Preline UI (直接寫作於 Blade 模板中)
|
|
||||||
* **前端建置工具**:Vite
|
|
||||||
* **資料庫**:MySQL 8.0
|
|
||||||
* **開發環境**:Laravel Sail (Docker / WSL2)
|
|
||||||
|
|
||||||
## 3. 目錄結構與慣例
|
|
||||||
|
|
||||||
### 3.1 後端 (Laravel)
|
|
||||||
與標準 Laravel 結構保持一致,無過度拆分的模組化(與 ERP 的 Modular Monolith 區別):
|
|
||||||
* **Controllers**:`app/Http/Controllers/`,負責接收請求並回傳 `view()` 或 JSON。
|
|
||||||
* **Models**:`app/Models/{Domain}/`,按領域分群 (例如 `Machine`, `Member`, `System`)。
|
|
||||||
* **Routes**:`routes/web.php` 用於後台管理介面;`routes/api.php` 提供外部或機台調用介面 (需 V1 版本化)。
|
|
||||||
* **Services** (建議):`app/Services/{Domain}/`,將商業邏輯與資料異動封裝於 Service 中。
|
|
||||||
* **Traits**:`app/Traits/ApiResponse.php` 用於統一 API JSON 回傳格式。
|
|
||||||
* **Jobs**:`app/Jobs/{Domain}/`,**高併發 IoT 場景之必要實作**。所有日誌、心跳上報必須進入 Redis Queue 進行背景異步處理,嚴禁在 API 直連 DB 寫入日誌。
|
|
||||||
|
|
||||||
### 3.2 前端 (Blade / Tailwind / Alpine)
|
|
||||||
* **Views (頁面)**:位於 `resources/views/`。通常依功能建立資料夾(如 `resources/views/admin/machines/index.blade.php`)。
|
|
||||||
* **Layouts (版面)**:位於 `resources/views/layouts/`。定義全站的共用版面結構(如 header, sidebar, footer)。
|
|
||||||
* **Components (組件)**:位於 `resources/views/components/`。封裝可重用的 Blade 元件(如 Button, Modal, Table),支援透過 `<x-button>` 語法呼叫。
|
|
||||||
|
|
||||||
## 4. 開發標準 (Coding Standards)
|
|
||||||
* **命名規範**:
|
|
||||||
* Controllers: `PascalCaseController.php` (例如 `MachineController.php`)
|
|
||||||
* Models: `PascalCase.php` (例如 `Machine.php`)
|
|
||||||
* Blade Views: `kebab-case.blade.php` 或按資源名稱 (例如 `index.blade.php`, `create.blade.php`)
|
|
||||||
* Routes uri: `kebab-case` (例如 `/machine-logs`)
|
|
||||||
* **回傳格式**:
|
|
||||||
* Web 路由:回傳 `view()`,表單驗證失敗時直接使用 Laravel 內建的 redirect with errors。
|
|
||||||
* API 路由:回傳標準 JSON 格式的 `JsonResponse`。
|
|
||||||
|
|
||||||
## 5. UI 與前端開發指南
|
|
||||||
* **樣式撰寫**:全面使用 Tailwind CSS utility classes,**避免撰寫自訂 CSS**(除非少數特定動畫或覆寫)。
|
|
||||||
* **UI 元件庫**:遵循 **Preline UI** 的類別與 HTML 結構進行開發。
|
|
||||||
* **前端腳本**:
|
|
||||||
* 優先使用 **Alpine.js** (`x-data`, `x-show`, `@click` 等) 在 HTML 標籤內完成簡單的 DOM 狀態切換與互動邏輯。
|
|
||||||
* 避免在 Blade 內撰寫冗長的 `<script>` Vanilla JS;若邏輯過於複雜,可將 Alpine state 獨立成 js 檔案再於 Vite 引入,但原則上保持輕量。
|
|
||||||
|
|
||||||
## 6. AI 協作規則 (給 Antigravity AI)
|
|
||||||
* **角色設定**:你是一位專業的全端開發工程師助手。
|
|
||||||
* **代碼生成指令**:
|
|
||||||
* 所有的解釋說明請使用 **繁體中文**。
|
|
||||||
* **【警告】** 此專案前端禁用 React / Vue / Inertia.js。所有的前端頁面生成必須使用 **Blade 模板** 結合 **Tailwind CSS** 與 **Alpine.js**。
|
|
||||||
* 生成 UI 區塊時,必須優先參考與產生 **Preline UI** 風格與結構的標記語法。
|
|
||||||
* 開發新功能時,請建立標準的 Controller 搭配對應的 `resources/views/.../` 目錄。
|
|
||||||
|
|
||||||
## 7. 運行機制 (Docker / Sail)
|
|
||||||
由於專案運行在 Docker 容器環境中,請勿直接在宿主機 (Host) 執行 php 或 composer 指令。請使用專案內建的 `sail` 指令:
|
|
||||||
|
|
||||||
* **啟動環境**:`./vendor/bin/sail up -d`
|
|
||||||
* **執行 PHP 指令**:`./vendor/bin/sail php -v`
|
|
||||||
* **執行 Artisan 指令**:`./vendor/bin/sail artisan route:list`
|
|
||||||
* **執行 Composer**:`./vendor/bin/sail composer install`
|
|
||||||
* **執行 Node/NPM**:`./vendor/bin/sail npm run dev`
|
|
||||||
|
|
||||||
## 8. 部署與查修環境 (CI/CD)
|
|
||||||
* **自動化部署**:專案具備基於 Gitea Actions 的 CI/CD 自動化部署流程 (`.gitea/workflows/`)。
|
|
||||||
* **Demo 環境 (對應 `demo` 分支)**:
|
|
||||||
* 透過 `deploy-demo.yaml`,合併或推送到 `demo` 分支會自動部署至 `demo-cloud.taiwan-star.com.tw`。
|
|
||||||
* 登入伺服器查修:`ssh gitea_work`,路徑為 `/var/www/star-cloud-demo`。
|
|
||||||
|
|
||||||
## 9. 瀏覽器測試規範 (Browser Testing)
|
|
||||||
當需要進行瀏覽器自動化測試或手動驗證時,必須遵守以下連線資訊:
|
|
||||||
|
|
||||||
* **本地測試網址**:`http://localhost:8090/` (注意:非 8000 或 8080)
|
|
||||||
* **預設管理員帳號**:`admin`
|
|
||||||
* **預設管理員密碼**:`password`
|
|
||||||
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> 在執行 `open_browser_url` 或進行 E2E 測試時,請務必優先確認 Port 是否為 `8090`,以避免連線至錯誤的服務環境。
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
---
|
|
||||||
trigger: always_on
|
|
||||||
---
|
|
||||||
|
|
||||||
# 技能觸發規範 (Skill Trigger Rules)
|
|
||||||
|
|
||||||
本文件確保 AI 助手在對話中能**主動辨識**需要參照技能 (Skill) 的時機。
|
|
||||||
Skills 位於 `.agents/skills/`,採漸進式揭露以節省 Token。
|
|
||||||
**若對話內容命中以下任一觸發條件,必須先使用 `view_file` 讀取對應的 `SKILL.md` 後標記為 active 再進行作業。**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 觸發對照表
|
|
||||||
|
|
||||||
| 觸發詞 / 情境 | 對應 Skill | 路徑 |
|
|
||||||
|---|---|---|
|
|
||||||
| 機台通訊, IoT, 日誌上報, Log Ingestion, 異步隊列, Queue, Heartbeat, 心跳發報 | **IoT 通訊與高併發處理規範** | `.agents/skills/iot-communication/SKILL.md` |
|
|
||||||
| 介面, UI, 佈局, CSS, Tailwind, 奢華, 深色模式, Light Mode, Dark Mode, Blade, 樣式, 間距, 陰影, 動畫 | **極簡奢華風 UI 實作規範** | `.agents/skills/ui-minimal-luxury/SKILL.md` |
|
|
||||||
| 查詢、撈資料、Query、Controller、下拉選單、Eloquent、N+1、`->get()`、select、交易、Transaction、Bulk、分頁、索引 | **資料庫與 ORM 最佳實踐規範** | `/home/mama/.gemini/antigravity/global_skills/database-best-practices/SKILL.md` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 強制觸發場景
|
|
||||||
|
|
||||||
以下場景**無論對話中是否出現觸發詞**,都必須主動載入對應 Skill:
|
|
||||||
|
|
||||||
### 🔴 新增或修改頁面 (Views/Blade) 時
|
|
||||||
必須讀取:
|
|
||||||
1. **ui-minimal-luxury** — 確保符合極簡奢華風視覺與互動規範
|
|
||||||
|
|
||||||
### 🔴 新增機台通訊 API 端點時
|
|
||||||
必須讀取:
|
|
||||||
1. **iot-communication** — 決定是否使用異步隊列流程
|
|
||||||
|
|
||||||
### 🔴 修改 Job 或 Service 邏輯時
|
|
||||||
必須讀取:
|
|
||||||
1. **iot-communication** — 確保符合高併發處理架構
|
|
||||||
|
|
||||||
### 🔴 新增或修改 API 與 Controller 撈取資料庫邏輯時
|
|
||||||
必須讀取:
|
|
||||||
1. **database-best-practices** — 確認查詢優化、交易安全、批量寫入與索引規範
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
---
|
|
||||||
name: IoT 通訊與高併發處理規範
|
|
||||||
description: 規範智能販賣機與 Cloud 平台間的高頻通訊處理流程,包含 API 接收、異步隊列、業務邏輯拆分與日誌記錄。
|
|
||||||
---
|
|
||||||
|
|
||||||
# IoT 通訊與高併發處理規範 (IoT Communication Skill)
|
|
||||||
|
|
||||||
本規範確保 Star-Cloud 系統在處理成千上萬台機台的高頻發報時,能維持伺服器響應速度與資料一致性。
|
|
||||||
|
|
||||||
## 1. 處理管線 (Processing Pipeline)
|
|
||||||
|
|
||||||
所有來自機台的非即時性資料(日誌、心跳、狀態上報)必須遵循以下 pipeline:
|
|
||||||
|
|
||||||
1. **API Controller (接收層)**:驗證 Request 合法性,隨即分派 (Dispatch) 任務至 Queue,並回傳 `202 Accepted`。
|
|
||||||
2. **Job (異步層)**:由背景 Worker 讀取隊列任務,呼叫對應 Service 處理。
|
|
||||||
3. **Service (邏輯層)**:封裝商業邏輯,更新資料庫。
|
|
||||||
4. **Model (儲存層)**:執行資料存取。
|
|
||||||
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> **嚴禁**在 API Controller 直接進行資料庫寫入操作(針對機台發訊端點)。
|
|
||||||
|
|
||||||
## 2. 異步任務實作範例
|
|
||||||
|
|
||||||
### 2.1 API Endpoint
|
|
||||||
```php
|
|
||||||
public function storeLog(Request $request, int $id): JsonResponse
|
|
||||||
{
|
|
||||||
// 1. 驗證
|
|
||||||
$data = $request->validate([...]);
|
|
||||||
|
|
||||||
// 2. 異步分派
|
|
||||||
ProcessMachineLog::dispatch($id, $data);
|
|
||||||
|
|
||||||
// 3. 快速回應
|
|
||||||
return $this->successResponse([], 'Accepted', 202);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.2 Job 處理邏輯
|
|
||||||
Job 應保持單純,僅作為 Service 的觸發點:
|
|
||||||
```php
|
|
||||||
public function handle(MachineService $service): void
|
|
||||||
{
|
|
||||||
$service->recordLog($this->machineId, $this->logData);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 3. 隊列配置規範
|
|
||||||
|
|
||||||
- **連接驅動 (Driver)**:預設使用 `Redis` 以確保高併發吞吐量。
|
|
||||||
- **重試機制 (Retry)**:IoT 任務應設定合理的重試次數,避免因網路短暫波動遺失日誌。
|
|
||||||
- **失敗處理 (Failed Jobs)**:關鍵業務(如訂單同步)必須監控 `failed_jobs`。
|
|
||||||
|
|
||||||
## 4. 速率限制 (Rate Limiting)
|
|
||||||
|
|
||||||
- 所有的 IoT API 必須在 `routes/api.php` 中使用 `throttle:api` 或自定義 Middleware。
|
|
||||||
- 針對單一機台 ID 應限制其每一分鐘的最高連線數,防止遭受攻擊或機台 Bug 導致的連線暴衝。
|
|
||||||
|
|
||||||
## 5. 檢核項目 (Checklist)
|
|
||||||
- [ ] 是否使用了 `ApiResponse` Trait?
|
|
||||||
- [ ] 業務邏輯是否已封裝至 `App\Services`?
|
|
||||||
- [ ] 是否使用了 Redis Queue 進行非同步處理?
|
|
||||||
- [ ] 是否在 API 層級進行了基礎的參數驗證?
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
---
|
|
||||||
name: 極簡奢華風 UI 實作規範 (Minimal Luxury UI)
|
|
||||||
description: 定義 Star Cloud 管理後台的「極簡奢華風」設計規範,包含 CSS Tokens、常用組件樣式、動畫效果與互動模式,確保全站 15+ 模組的視覺一致性。
|
|
||||||
---
|
|
||||||
|
|
||||||
# 極簡奢華風 UI 實作規範 (Minimal Luxury UI)
|
|
||||||
|
|
||||||
本文件定義了 Star Cloud 專案的核心視覺語言。所有新頁面與組件開發必須嚴格遵守此規範。
|
|
||||||
|
|
||||||
## 1. 核心設計令牌 (Design Tokens)
|
|
||||||
|
|
||||||
### 色彩系統 (CSS Variables)
|
|
||||||
位於 `resources/css/app.css`:
|
|
||||||
- `--color-luxury-deep`: `#0f172a` (深色背景)
|
|
||||||
- `--color-luxury-card`: `#1e293b` (卡片背景)
|
|
||||||
- `--color-accent`: `#06b6d4` (青色點綴,適用於按鈕與標示)
|
|
||||||
|
|
||||||
### 字體 (Typography)
|
|
||||||
- **內文字體**: `Plus Jakarta Sans`
|
|
||||||
- **標題/顯示字體**: `Outfit`
|
|
||||||
- **特性**: 標題需帶有 `letter-spacing: -0.02em` 以增強精密感。
|
|
||||||
|
|
||||||
## 2. 核心組件樣式
|
|
||||||
|
|
||||||
### 豪華卡片 (Luxury Card)
|
|
||||||
```html
|
|
||||||
<div class="luxury-card p-6 rounded-2xl animate-luxury-in">
|
|
||||||
<!-- 內容 -->
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
- **特效**: 懸停時帶有 Y 軸平移與深度投影。
|
|
||||||
|
|
||||||
### 側邊導覽項 (Luxury Nav Item)
|
|
||||||
```html
|
|
||||||
<a href="#" class="luxury-nav-item active">
|
|
||||||
<i class="lucide-icon"></i>
|
|
||||||
<span>節點名稱</span>
|
|
||||||
</a>
|
|
||||||
```
|
|
||||||
- **啟用狀態**: 左側帶有圓角直條指示器,並輔以青色發光陰影。
|
|
||||||
|
|
||||||
### 按鈕組件 (Buttons)
|
|
||||||
- **Primary**: `.btn-luxury-primary` (青色漸層,適用於建立、儲存)
|
|
||||||
- **Secondary**: `.btn-luxury-secondary` (白色/深色背景,帶邊框,適用於編輯、篩選)
|
|
||||||
- **Ghost**: `.btn-luxury-ghost` (無背景,適用於取消、查看更多)
|
|
||||||
|
|
||||||
```html
|
|
||||||
<!-- Primary -->
|
|
||||||
<button class="btn-luxury-primary">
|
|
||||||
<i class="lucide-plus size-4"></i>
|
|
||||||
<span>建立新機台</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Ghost -->
|
|
||||||
<button class="btn-luxury-ghost">取消</button>
|
|
||||||
```
|
|
||||||
|
|
||||||
## 3. 動畫與互動
|
|
||||||
|
|
||||||
### 進場動畫
|
|
||||||
- **`.animate-luxury-in`**: 所有的主內容區域或卡片在頁面載入時,應具備由下而上的淡入效果。
|
|
||||||
|
|
||||||
### Alpine.js 互動模式 (以時間選擇器為例)
|
|
||||||
- **互動原則**: 點擊觸發下拉選單時,必須使用 `x-transition` 且帶有 `scale` 偏移。
|
|
||||||
- **樣式要求**: 選單背景需使用玻璃擬態 (Glassmorphism) 或帶透明度的深色背景。
|
|
||||||
|
|
||||||
## 4. UI 檢查清單 (AI 助手執行前必讀)
|
|
||||||
- [ ] 是否使用了正確的 `rounded-2xl` (或更圓) 的導角?
|
|
||||||
- [ ] 所有的圖示是否一致使用 `lucide-react` 風格?
|
|
||||||
- [ ] 卡片是否有適當的間距 (通常為 `p-6`)?
|
|
||||||
- [ ] 文字色階是否符合:標題 (slate-900/white)、副標 (slate-500/slate-400)?
|
|
||||||
|
|
||||||
## 5. 開發注意事項 (Important Notes)
|
|
||||||
|
|
||||||
### 技術限制備忘
|
|
||||||
- **CSS 編譯**: 複雜的 `box-shadow` 或漸層應直接寫原生 CSS 屬性,避免在 `@apply` 中使用帶空格的數值導致編譯失敗(詳見 KI: `tailwind-luxury-ui-patterns`)。
|
|
||||||
- **深色模式**: 互動式按鈕在深色模式下必須強化文字亮度(`dark:text-white`),並輔以青色發光效果。
|
|
||||||
|
|
||||||
### 即時動態呈現規範
|
|
||||||
- **格式**: `#機台編號 動作內容` (例如 `#V-001 執行出貨`)。
|
|
||||||
- **脈絡**: 必須呈現相對時間與機台位置。
|
|
||||||
|
|
||||||
---
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> **開發新功能前,必須確認 `app.css` 中的 `.btn-luxury-*` 系列組件是否滿足需求。**
|
|
||||||
> 嚴禁在 Blade 中寫入大量重複的 `bg-indigo-600` 等舊式類別。
|
|
||||||
|
|
||||||
---
|
|
||||||
> [!TIP]
|
|
||||||
> 當遇到未定義的 UI 區塊時,優先參考 `admin.dashboard.blade.php` 的卡片與即時動態實作方式進行衍生。
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
APP_NAME=starCloud
|
APP_NAME=startCloud
|
||||||
COMPOSE_PROJECT_NAME=star-cloud
|
COMPOSE_PROJECT_NAME=start-cloud
|
||||||
APP_ENV=local
|
APP_ENV=local
|
||||||
APP_KEY=
|
APP_KEY=
|
||||||
APP_DEBUG=true
|
APP_DEBUG=true
|
||||||
@@ -25,7 +25,7 @@ LOG_LEVEL=debug
|
|||||||
DB_CONNECTION=mysql
|
DB_CONNECTION=mysql
|
||||||
DB_HOST=mysql
|
DB_HOST=mysql
|
||||||
DB_PORT=3306
|
DB_PORT=3306
|
||||||
DB_DATABASE=star-cloud
|
DB_DATABASE=start-cloud
|
||||||
DB_USERNAME=sail
|
DB_USERNAME=sail
|
||||||
DB_PASSWORD=password
|
DB_PASSWORD=password
|
||||||
# FORWARD_DB_PORT=3308
|
# FORWARD_DB_PORT=3308
|
||||||
|
|||||||
@@ -22,12 +22,12 @@ jobs:
|
|||||||
echo "${{ secrets.DEMO_SSH_KEY }}" > ~/.ssh/id_rsa_demo
|
echo "${{ secrets.DEMO_SSH_KEY }}" > ~/.ssh/id_rsa_demo
|
||||||
chmod 600 ~/.ssh/id_rsa_demo
|
chmod 600 ~/.ssh/id_rsa_demo
|
||||||
rsync -avz --delete \
|
rsync -avz --delete \
|
||||||
--exclude='.git' \
|
--exclude='/.git' \
|
||||||
--exclude='node_modules' \
|
--exclude='/node_modules' \
|
||||||
--exclude='vendor' \
|
--exclude='/vendor' \
|
||||||
--exclude='storage' \
|
--exclude='/storage' \
|
||||||
--exclude='.env' \
|
--exclude='/.env' \
|
||||||
--exclude='public/build' \
|
--exclude='/public/build' \
|
||||||
-e "ssh -p 2227 -i ~/.ssh/id_rsa_demo -o StrictHostKeyChecking=no" \
|
-e "ssh -p 2227 -i ~/.ssh/id_rsa_demo -o StrictHostKeyChecking=no" \
|
||||||
./ root@220.132.7.82:/var/www/star-cloud-demo/
|
./ root@220.132.7.82:/var/www/star-cloud-demo/
|
||||||
rm ~/.ssh/id_rsa_demo
|
rm ~/.ssh/id_rsa_demo
|
||||||
@@ -92,9 +92,13 @@ jobs:
|
|||||||
|
|
||||||
# 3. Laravel 初始化與優化
|
# 3. Laravel 初始化與優化
|
||||||
php artisan migrate --force &&
|
php artisan migrate --force &&
|
||||||
|
php artisan storage:link &&
|
||||||
php artisan optimize:clear &&
|
php artisan optimize:clear &&
|
||||||
php artisan optimize &&
|
php artisan optimize &&
|
||||||
php artisan view:cache
|
php artisan view:cache &&
|
||||||
|
php artisan queue:restart &&
|
||||||
|
php artisan db:seed --class=RoleSeeder --force &&
|
||||||
|
php artisan db:seed --class=AdminUserSeeder --force
|
||||||
"
|
"
|
||||||
docker exec star-cloud-demo-laravel chmod -R 775 /var/www/html/storage /var/www/html/bootstrap/cache
|
docker exec star-cloud-demo-laravel chmod -R 775 /var/www/html/storage /var/www/html/bootstrap/cache
|
||||||
|
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -17,7 +17,3 @@ yarn-error.log
|
|||||||
/.fleet
|
/.fleet
|
||||||
/.idea
|
/.idea
|
||||||
/.vscode
|
/.vscode
|
||||||
/docs/API
|
|
||||||
/docs/*.xlsx
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
404
README.md
404
README.md
@@ -1,392 +1,112 @@
|
|||||||
# Star Cloud 智能販賣機管理平台
|
# Star Cloud 智能販賣機管理平台
|
||||||
|
|
||||||
> 基於 Docker 的全方位智能販賣機後台管理系統
|
## 專案簡介 (Project Description)
|
||||||
|
Star Cloud 是一個專為智能販賣機設計的後台管理系統,旨在提供全方位的機台監控、庫存管理、銷售分析與會員管理功能。透過此平台,管理者可以即時掌握機台運營狀態,優化補貨流程,並透過數據分析提升營運效益。
|
||||||
|
|
||||||
Star Cloud 是一個專為智能販賣機設計的後台管理系統,提供機台監控、庫存管理、銷售分析與會員管理等完整功能。本專案採用 Docker Compose 容器化架構,實現快速部署與環境一致性。
|
## 技術棧 (Technology Stack)
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 技術架構
|
|
||||||
|
|
||||||
### 容器化架構
|
|
||||||
本專案完全運行在 Docker 容器中,包含以下服務:
|
|
||||||
|
|
||||||
| 服務 | 容器名稱 | 技術 | 用途 | 連接埠 |
|
|
||||||
|------|---------|------|------|--------|
|
|
||||||
| **應用程式** | 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 |
|
|
||||||
|
|
||||||
### 後端技術棧
|
|
||||||
|
|
||||||
|
### 後端 (Backend)
|
||||||
- **Framework**: Laravel 10.x
|
- **Framework**: Laravel 10.x
|
||||||
- **Language**: PHP 8.5+
|
- **Language**: PHP 8.1+
|
||||||
- **Database**: MySQL 8.0
|
- **Database**: MySQL 8.0+
|
||||||
- **Cache/Session**: Redis
|
- **Authentication**: Laravel Sanctum (API Token Authentication)
|
||||||
- **Authentication**: Laravel Sanctum (API Token)
|
- **Tools**: Composer
|
||||||
- **Package Manager**: Composer 2.x
|
|
||||||
|
|
||||||
### 前端技術棧
|
### 前端 (Frontend)
|
||||||
|
- **Framework**: Blade Templates (Laravel 預設樣板引擎)
|
||||||
- **Template Engine**: Blade Templates
|
|
||||||
- **UI Library**: Preline UI 3.x (Tailwind CSS 組件庫)
|
|
||||||
- **CSS Framework**: Tailwind CSS 3.x
|
- **CSS Framework**: Tailwind CSS 3.x
|
||||||
- **JavaScript**: Alpine.js 3.x (輕量級互動框架)
|
- **JavaScript**: Alpine.js 3.x
|
||||||
- **Build Tool**: Vite 5.x
|
- **Build Tool**: Vite 5.x
|
||||||
- **HTTP Client**: Axios
|
- **HTTP Client**: Axios
|
||||||
|
|
||||||
---
|
## 安裝與使用說明 (Installation & Usage)
|
||||||
|
|
||||||
## 快速開始
|
請依照以下步驟將專案 Clone 至本地端並開始運行:
|
||||||
|
|
||||||
### 前置需求
|
|
||||||
|
|
||||||
|
### 0. 前置需求 (Prerequisites)
|
||||||
確保您的系統已安裝以下軟體:
|
確保您的系統已安裝以下軟體:
|
||||||
|
- PHP 8.1+
|
||||||
|
- Composer
|
||||||
|
- Node.js & npm
|
||||||
|
- MySQL 8.0+
|
||||||
|
|
||||||
- **Docker** 20.10+
|
若您尚未安裝 MySQL,Windows 使用者可至 [MySQL 官網](https://dev.mysql.com/downloads/installer/) 下載 Installer,或使用 XAMPP / Laragon 等整合環境。
|
||||||
- **Docker Compose** 2.0+
|
|
||||||
- **Git**
|
|
||||||
|
|
||||||
> **提示**:Windows 使用者建議安裝 [Docker Desktop](https://www.docker.com/products/docker-desktop/),Linux 使用者可參考 [官方安裝文件](https://docs.docker.com/engine/install/)
|
|
||||||
|
|
||||||
### 安裝步驟
|
|
||||||
|
|
||||||
#### 1. Clone 專案
|
|
||||||
|
|
||||||
|
### 1. 下載專案 (Clone Repository)
|
||||||
```bash
|
```bash
|
||||||
git clone <repository_url>
|
git clone <repository_url>
|
||||||
cd star-cloud
|
cd star-cloud
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 2. 環境設定
|
### 2. 安裝依賴套件 (Install Dependencies)
|
||||||
|
|
||||||
複製環境變數範例檔案:
|
安裝後端 PHP 套件:
|
||||||
|
```bash
|
||||||
|
composer install
|
||||||
|
```
|
||||||
|
|
||||||
|
安裝前端 Node.js 套件:
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 環境變數設定 (Environment Setup)
|
||||||
|
複製範例環境設定檔:
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
```
|
```
|
||||||
|
|
||||||
**重要設定**(`.env` 檔案):
|
請開啟 `.env` 檔案,並依照您的本地環境設定資料庫連線資訊:
|
||||||
|
```dotenv
|
||||||
```env
|
|
||||||
# 應用程式設定
|
|
||||||
APP_NAME=Star Cloud
|
|
||||||
APP_ENV=local
|
|
||||||
APP_DEBUG=true
|
|
||||||
APP_URL=http://localhost:8090
|
|
||||||
|
|
||||||
# 資料庫設定(對應 Docker Compose 服務)
|
|
||||||
DB_CONNECTION=mysql
|
DB_CONNECTION=mysql
|
||||||
DB_HOST=mysql
|
DB_HOST=127.0.0.1
|
||||||
DB_PORT=3306
|
DB_PORT=3306
|
||||||
DB_DATABASE=star_cloud
|
DB_DATABASE=star_cloud
|
||||||
DB_USERNAME=sail
|
DB_USERNAME=root
|
||||||
DB_PASSWORD=password
|
DB_PASSWORD=your_password
|
||||||
|
|
||||||
# Redis 設定(對應 Docker Compose 服務)
|
|
||||||
REDIS_HOST=redis
|
|
||||||
REDIS_PASSWORD=null
|
|
||||||
REDIS_PORT=6379
|
|
||||||
|
|
||||||
# Vite 開發伺服器
|
|
||||||
VITE_PORT=5175
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 3. 啟動 Docker 容器
|
產生應用程式金鑰 (Application Key):
|
||||||
|
|
||||||
啟動所有服務(應用程式、資料庫、Redis):
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose up -d
|
php artisan key:generate
|
||||||
```
|
```
|
||||||
|
|
||||||
> **說明**:`-d` 參數表示背景執行
|
### 4. 資料庫遷移 (Database Migration)
|
||||||
|
執行 Migration 以建立資料庫結構:
|
||||||
檢查容器狀態:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose ps
|
php artisan migrate
|
||||||
|
```
|
||||||
|
php artisan migrate --seed
|
||||||
```
|
```
|
||||||
|
|
||||||
預期輸出:
|
### 4.1 預設管理員帳號 (Default Admin Account)
|
||||||
```
|
執行上述指令後,系統會建立一組預設管理員帳號:
|
||||||
NAME STATUS PORTS
|
- **Email**: `admin@star-cloud.com`
|
||||||
star-cloud-laravel Up X minutes 0.0.0.0:8090->80/tcp, 0.0.0.0:5175->5175/tcp
|
- **Password**: `password`
|
||||||
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 安裝後端依賴**
|
|
||||||
|
|
||||||
|
### 5. 編譯前端資源 (Build Frontend Assets)
|
||||||
|
啟動開發模式 (Hot Module Replacement):
|
||||||
```bash
|
```bash
|
||||||
docker compose exec laravel.test composer install
|
npm run dev
|
||||||
```
|
```
|
||||||
|
或編譯生產環境檔案:
|
||||||
**4.2 產生應用程式金鑰**
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose exec laravel.test php artisan key:generate
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
**4.3 執行資料庫遷移與種子**
|
### 6. 啟動伺服器 (Start Server)
|
||||||
|
啟動 Laravel 開發伺服器:
|
||||||
```bash
|
```bash
|
||||||
docker compose exec laravel.test php artisan migrate --seed
|
php artisan serve --port=8001
|
||||||
```
|
```
|
||||||
|
預設網址為:http://localhost:8001
|
||||||
> **預設管理員帳號**:
|
|
||||||
> - 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
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Docker 常用指令
|
|
||||||
|
|
||||||
### 容器管理
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 啟動所有服務
|
|
||||||
docker compose up -d
|
|
||||||
|
|
||||||
# 停止所有服務
|
|
||||||
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 <command>
|
|
||||||
|
|
||||||
# 範例:清除快取
|
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 主要功能模組
|
## 主要功能模組
|
||||||
|
- **儀錶板 (Dashboard)**: 銷售數據概覽、機台狀態監控
|
||||||
### 核心功能
|
- **機台管理 (Machine Management)**: 機台列表、遠端控制、日誌查詢
|
||||||
|
- **商品與庫存 (Inventory)**: 商品管理、進銷存、補貨單
|
||||||
| 模組 | 功能描述 |
|
- **銷售管理 (Sales)**: 交易紀錄、營收報表
|
||||||
|------|---------|
|
- **權限設定 (Permissions)**: 角色與權限分配
|
||||||
| **儀錶板** | 銷售數據總覽、機台狀態即時監控、營收統計圖表 |
|
|
||||||
| **機台管理** | 機台列表、遠端控制、日誌查詢、維修管理、效期控制 |
|
|
||||||
| **倉庫管理** | 倉庫列表、庫存管理、調撥單、採購單、補貨單 |
|
|
||||||
| **商品管理** | 商品資料、分類管理、商品報表分析 |
|
|
||||||
| **銷售管理** | 交易紀錄、金流管理、促銷設定、營收報表 |
|
|
||||||
| **會員系統** | 會員管理、點數系統、來店禮、Line 整合 |
|
|
||||||
| **權限控制** | 角色管理、權限分配、功能權限設定 |
|
|
||||||
| **遠端管理** | 機台重啟、遠端出貨、遠端結帳、庫存調整 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Preline UI 組件庫
|
|
||||||
|
|
||||||
本專案已整合 **Preline UI 3.x**,這是一個基於 Tailwind CSS 的開源 UI 組件庫,提供 50+ 預構建組件。
|
|
||||||
|
|
||||||
### 可用組件類別
|
|
||||||
|
|
||||||
- **Navigation**: 導航列、側邊欄、分頁、麵包屑、頁籤
|
|
||||||
- **Forms**: 輸入框、選擇器、開關、檔案上傳、日期選擇器
|
|
||||||
- **Overlays**: 模態框、抽屜、下拉選單、提示框、彈出框
|
|
||||||
- **Data Display**: 表格、卡片、時間軸、折疊面板、徽章
|
|
||||||
- **Feedback**: 通知、警告、載入狀態、進度條
|
|
||||||
|
|
||||||
### 使用範例
|
|
||||||
|
|
||||||
```html
|
|
||||||
<!-- 下拉選單 -->
|
|
||||||
<div class="hs-dropdown relative inline-flex">
|
|
||||||
<button type="button" class="hs-dropdown-toggle px-4 py-2 bg-blue-600 text-white rounded-lg">
|
|
||||||
選單 <svg class="w-4 h-4 inline ml-2">...</svg>
|
|
||||||
</button>
|
|
||||||
<div class="hs-dropdown-menu hidden bg-white shadow-lg rounded-lg p-2 mt-2">
|
|
||||||
<a class="block px-3 py-2 rounded hover:bg-gray-100" href="#">選項 1</a>
|
|
||||||
<a class="block px-3 py-2 rounded hover:bg-gray-100" href="#">選項 2</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 模態框 -->
|
|
||||||
<button type="button" data-hs-overlay="#my-modal" class="px-4 py-2 bg-blue-600 text-white rounded-lg">
|
|
||||||
開啟模態框
|
|
||||||
</button>
|
|
||||||
<div id="my-modal" class="hs-overlay hidden">
|
|
||||||
<!-- 模態框內容 -->
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
**更多資源**:
|
|
||||||
- 官方文件: https://preline.co/docs/
|
|
||||||
- 組件範例: https://preline.co/examples.html
|
|
||||||
- GitHub: https://github.com/htmlstreamofficial/preline
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 故障排除
|
|
||||||
|
|
||||||
### 容器無法啟動
|
|
||||||
|
|
||||||
```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
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 授權與版權
|
|
||||||
|
|
||||||
© Star Cloud. All Rights Reserved.
|
© Star Cloud. All Rights Reserved.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 技術支援
|
|
||||||
|
|
||||||
如有問題或建議,請聯繫開發團隊或提交 Issue。
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Console\Commands\Machine;
|
|
||||||
|
|
||||||
use App\Models\Machine\Machine;
|
|
||||||
use Illuminate\Console\Command;
|
|
||||||
use Illuminate\Support\Facades\Http;
|
|
||||||
|
|
||||||
class SimulateMachineLogs extends Command
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $signature = 'simulate:machine-logs {--count=10 : 發送日誌的次數}';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $description = '模擬機台發送 API 日誌請求到後端 (用於壓測與驗證 Queue)';
|
|
||||||
|
|
||||||
public function handle(): void
|
|
||||||
{
|
|
||||||
$count = (int) $this->option('count');
|
|
||||||
$machines = Machine::all();
|
|
||||||
|
|
||||||
if ($machines->isEmpty()) {
|
|
||||||
$this->error('No machines found. Please run MachineSeeder first.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->info("Starting simulation of {$count} logs...");
|
|
||||||
|
|
||||||
$bar = $this->output->createProgressBar($count);
|
|
||||||
$bar->start();
|
|
||||||
|
|
||||||
// 由於是在同一個開發環境,且在 Sail 容器內部執行,
|
|
||||||
// 外部 8090 埠對應容器內部 8080 埠。
|
|
||||||
$baseUrl = 'http://localhost:8080/api/v1/machines/';
|
|
||||||
|
|
||||||
for ($i = 0; $i < $count; $i++) {
|
|
||||||
$machine = $machines->random();
|
|
||||||
$level = collect(['info', 'warning', 'error'])->random();
|
|
||||||
|
|
||||||
try {
|
|
||||||
Http::post($baseUrl . $machine->id . '/logs', [
|
|
||||||
'level' => $level,
|
|
||||||
'message' => "Simulated message #{$i} for machine {$machine->name}",
|
|
||||||
'context' => [
|
|
||||||
'simulated' => true,
|
|
||||||
'timestamp' => now()->toIso8601String(),
|
|
||||||
]
|
|
||||||
]);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$this->error("\nFailed to send log: " . $e->getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
$bar->advance();
|
|
||||||
}
|
|
||||||
|
|
||||||
$bar->finish();
|
|
||||||
$this->newLine();
|
|
||||||
$this->info('Simulation completed.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers\Admin;
|
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
|
||||||
|
|
||||||
abstract class AdminController extends Controller
|
|
||||||
{
|
|
||||||
// Admin 相關的共用邏輯可寫於此
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
namespace App\Http\Controllers\Admin;
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\System\AppConfig;
|
use App\Models\AppConfig;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class AppConfigController extends Controller
|
class AppConfigController extends Controller
|
||||||
|
|||||||
@@ -3,31 +3,25 @@
|
|||||||
namespace App\Http\Controllers\Admin;
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Machine\Machine;
|
use App\Models\Machine;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class DashboardController extends Controller
|
class DashboardController extends Controller
|
||||||
{
|
{
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
// 從資料庫獲取真實統計數據
|
// 模擬數據或從資料庫獲取
|
||||||
$totalRevenue = \App\Models\Member\MemberWallet::sum('balance');
|
// 由於目前沒有數據,我們先傳遞一些預設值或空集合
|
||||||
$activeMachines = Machine::where('status', 'online')->count();
|
$totalMachines = Machine::count();
|
||||||
$alertsPending = Machine::where('status', 'error')->count();
|
$onlineMachines = Machine::where('status', 'online')->count();
|
||||||
$memberCount = \App\Models\Member\Member::count();
|
$offlineMachines = Machine::where('status', 'offline')->count();
|
||||||
|
$errorMachines = Machine::where('status', 'error')->count();
|
||||||
// 獲取最新動態 (最近 3 筆機台日誌)
|
|
||||||
$latestActivities = \App\Models\Machine\MachineLog::with('machine')
|
|
||||||
->latest()
|
|
||||||
->limit(3)
|
|
||||||
->get();
|
|
||||||
|
|
||||||
return view('admin.dashboard', compact(
|
return view('admin.dashboard', compact(
|
||||||
'totalRevenue',
|
'totalMachines',
|
||||||
'activeMachines',
|
'onlineMachines',
|
||||||
'alertsPending',
|
'offlineMachines',
|
||||||
'memberCount',
|
'errorMachines'
|
||||||
'latestActivities'
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers\Admin;
|
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
|
||||||
use App\Models\Member\DepositBonusRule;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
|
|
||||||
class DepositBonusRuleController extends Controller
|
|
||||||
{
|
|
||||||
public function index()
|
|
||||||
{
|
|
||||||
$rules = DepositBonusRule::orderBy('min_amount')->get();
|
|
||||||
return view('admin.deposit-bonus-rules.index', compact('rules'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function store(Request $request)
|
|
||||||
{
|
|
||||||
$validated = $request->validate([
|
|
||||||
'name' => 'required|string|max:255',
|
|
||||||
'min_amount' => 'required|numeric|min:0',
|
|
||||||
'bonus_type' => 'required|in:fixed,percentage',
|
|
||||||
'bonus_value' => 'required|numeric|min:0',
|
|
||||||
'is_active' => 'boolean',
|
|
||||||
'start_at' => 'nullable|date',
|
|
||||||
'end_at' => 'nullable|date|after:start_at',
|
|
||||||
]);
|
|
||||||
|
|
||||||
DepositBonusRule::create($validated);
|
|
||||||
|
|
||||||
return redirect()->route('admin.deposit-bonus-rules.index')->with('success', '儲值回饋規則已建立');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function update(Request $request, DepositBonusRule $depositBonusRule)
|
|
||||||
{
|
|
||||||
$validated = $request->validate([
|
|
||||||
'name' => 'required|string|max:255',
|
|
||||||
'min_amount' => 'required|numeric|min:0',
|
|
||||||
'bonus_type' => 'required|in:fixed,percentage',
|
|
||||||
'bonus_value' => 'required|numeric|min:0',
|
|
||||||
'is_active' => 'boolean',
|
|
||||||
'start_at' => 'nullable|date',
|
|
||||||
'end_at' => 'nullable|date|after:start_at',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$depositBonusRule->update($validated);
|
|
||||||
|
|
||||||
return redirect()->route('admin.deposit-bonus-rules.index')->with('success', '儲值回饋規則已更新');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function destroy(DepositBonusRule $depositBonusRule)
|
|
||||||
{
|
|
||||||
$depositBonusRule->delete();
|
|
||||||
return redirect()->route('admin.deposit-bonus-rules.index')->with('success', '儲值回饋規則已刪除');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers\Admin;
|
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
|
||||||
use App\Models\Member\GiftDefinition;
|
|
||||||
use App\Models\Member\MembershipTier;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
|
|
||||||
class GiftDefinitionController extends Controller
|
|
||||||
{
|
|
||||||
public function index()
|
|
||||||
{
|
|
||||||
$gifts = GiftDefinition::with('tier')->get();
|
|
||||||
$tiers = MembershipTier::orderBy('sort_order')->get();
|
|
||||||
return view('admin.gift-definitions.index', compact('gifts', 'tiers'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function store(Request $request)
|
|
||||||
{
|
|
||||||
$validated = $request->validate([
|
|
||||||
'name' => 'required|string|max:255',
|
|
||||||
'type' => 'required|in:points,coupon,product,discount,cash',
|
|
||||||
'value' => 'required|numeric|min:0',
|
|
||||||
'tier_id' => 'nullable|exists:membership_tiers,id',
|
|
||||||
'trigger' => 'required|in:register,birthday,annual,upgrade,manual',
|
|
||||||
'validity_days' => 'required|integer|min:1',
|
|
||||||
'is_active' => 'boolean',
|
|
||||||
]);
|
|
||||||
|
|
||||||
GiftDefinition::create($validated);
|
|
||||||
|
|
||||||
return redirect()->route('admin.gift-definitions.index')->with('success', '禮品已建立');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function update(Request $request, GiftDefinition $giftDefinition)
|
|
||||||
{
|
|
||||||
$validated = $request->validate([
|
|
||||||
'name' => 'required|string|max:255',
|
|
||||||
'type' => 'required|in:points,coupon,product,discount,cash',
|
|
||||||
'value' => 'required|numeric|min:0',
|
|
||||||
'tier_id' => 'nullable|exists:membership_tiers,id',
|
|
||||||
'trigger' => 'required|in:register,birthday,annual,upgrade,manual',
|
|
||||||
'validity_days' => 'required|integer|min:1',
|
|
||||||
'is_active' => 'boolean',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$giftDefinition->update($validated);
|
|
||||||
|
|
||||||
return redirect()->route('admin.gift-definitions.index')->with('success', '禮品已更新');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function destroy(GiftDefinition $giftDefinition)
|
|
||||||
{
|
|
||||||
$giftDefinition->delete();
|
|
||||||
return redirect()->route('admin.gift-definitions.index')->with('success', '禮品已刪除');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,88 +2,142 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Admin;
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
use App\Models\Machine\Machine;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Machine;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\View\View;
|
|
||||||
|
|
||||||
class MachineController extends AdminController
|
class MachineController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* 顯示所有機台列表
|
* Display a listing of the resource.
|
||||||
*/
|
*/
|
||||||
public function index(Request $request): View
|
public function index()
|
||||||
{
|
{
|
||||||
$machines = Machine::query()
|
$machines = Machine::latest()->paginate(10);
|
||||||
->when($request->status, function ($query, $status) {
|
|
||||||
return $query->where('status', $status);
|
|
||||||
})
|
|
||||||
->latest()
|
|
||||||
->paginate(10);
|
|
||||||
|
|
||||||
return view('admin.machines.index', compact('machines'));
|
return view('admin.machines.index', compact('machines'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 顯示特定機台的日誌與詳細資訊
|
* Show the form for creating a new resource.
|
||||||
*/
|
*/
|
||||||
public function show(int $id): View
|
public function create()
|
||||||
{
|
{
|
||||||
$machine = Machine::with(['logs' => function ($query) {
|
return view('admin.machines.create');
|
||||||
$query->latest()->limit(50);
|
}
|
||||||
}])->findOrFail($id);
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a newly created resource in storage.
|
||||||
|
*/
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'name' => 'required|string|max:255',
|
||||||
|
'location' => 'nullable|string|max:255',
|
||||||
|
'status' => 'required|in:online,offline,error',
|
||||||
|
'temperature' => 'nullable|numeric',
|
||||||
|
'firmware_version' => 'nullable|string|max:50',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Machine::create($validated);
|
||||||
|
|
||||||
|
return redirect()->route('admin.machines.index')
|
||||||
|
->with('success', '機台建立成功');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the specified resource.
|
||||||
|
*/
|
||||||
|
public function show(Machine $machine)
|
||||||
|
{
|
||||||
return view('admin.machines.show', compact('machine'));
|
return view('admin.machines.show', compact('machine'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 顯示所有機台日誌列表
|
* Show the form for editing the specified resource.
|
||||||
*/
|
*/
|
||||||
public function logs(Request $request): View
|
public function edit(Machine $machine)
|
||||||
{
|
{
|
||||||
$logs = \App\Models\Machine\MachineLog::with('machine')
|
return view('admin.machines.edit', compact('machine'));
|
||||||
->when($request->level, function ($query, $level) {
|
|
||||||
return $query->where('level', $level);
|
|
||||||
})
|
|
||||||
->when($request->machine_id, function ($query, $machineId) {
|
|
||||||
return $query->where('machine_id', $machineId);
|
|
||||||
})
|
|
||||||
->latest()
|
|
||||||
->paginate(20);
|
|
||||||
|
|
||||||
$machines = Machine::select('id', 'name')->get();
|
|
||||||
|
|
||||||
return view('admin.machines.logs', compact('logs', 'machines'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 機台權限設定 (開發中)
|
* Update the specified resource in storage.
|
||||||
*/
|
*/
|
||||||
public function permissions(Request $request): View
|
public function update(Request $request, Machine $machine)
|
||||||
{
|
{
|
||||||
return view('admin.machines.index', ['machines' => Machine::paginate(1)]); // Placeholder
|
$validated = $request->validate([
|
||||||
|
'name' => 'required|string|max:255',
|
||||||
|
'location' => 'nullable|string|max:255',
|
||||||
|
'status' => 'required|in:online,offline,error',
|
||||||
|
'temperature' => 'nullable|numeric',
|
||||||
|
'firmware_version' => 'nullable|string|max:50',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$machine->update($validated);
|
||||||
|
|
||||||
|
return redirect()->route('admin.machines.index')
|
||||||
|
->with('success', '機台更新成功');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 機台使用率統計 (開發中)
|
* Remove the specified resource from storage.
|
||||||
*/
|
*/
|
||||||
public function utilization(Request $request): View
|
public function destroy(Machine $machine)
|
||||||
{
|
{
|
||||||
return view('admin.machines.index', ['machines' => Machine::paginate(1)]); // Placeholder
|
$machine->delete();
|
||||||
|
|
||||||
|
return redirect()->route('admin.machines.index')
|
||||||
|
->with('success', '機台已刪除');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 機台日誌
|
||||||
* 機台到期管理 (開發中)
|
public function logs()
|
||||||
*/
|
|
||||||
public function expiry(Request $request): View
|
|
||||||
{
|
{
|
||||||
return view('admin.machines.index', ['machines' => Machine::paginate(1)]); // Placeholder
|
return view('admin.placeholder', [
|
||||||
|
'title' => '機台日誌',
|
||||||
|
'description' => '機台操作歷史紀錄回溯',
|
||||||
|
'features' => [
|
||||||
|
'操作時間戳記',
|
||||||
|
'事件類型分類',
|
||||||
|
'操作人員記錄',
|
||||||
|
'詳細描述查詢',
|
||||||
|
]
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 機台權限
|
||||||
* 機台維護紀錄 (開發中)
|
public function permissions()
|
||||||
*/
|
|
||||||
public function maintenance(Request $request): View
|
|
||||||
{
|
{
|
||||||
return view('admin.machines.index', ['machines' => Machine::paginate(1)]); // Placeholder
|
return view('admin.placeholder', [
|
||||||
|
'title' => '機台權限',
|
||||||
|
'description' => '機台存取權限控管',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 機台稼動率
|
||||||
|
public function utilization()
|
||||||
|
{
|
||||||
|
return view('admin.placeholder', [
|
||||||
|
'title' => '機台稼動率',
|
||||||
|
'description' => '機台運行效率分析',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 效期管理
|
||||||
|
public function expiry()
|
||||||
|
{
|
||||||
|
return view('admin.placeholder', [
|
||||||
|
'title' => '效期管理',
|
||||||
|
'description' => '商品效期與貨道出貨控制',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 維修管理單
|
||||||
|
public function maintenance()
|
||||||
|
{
|
||||||
|
return view('admin.placeholder', [
|
||||||
|
'title' => '維修管理單',
|
||||||
|
'description' => '機台維修工單系統',
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers\Admin;
|
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
|
||||||
use App\Models\Member\MembershipTier;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
|
|
||||||
class MembershipTierController extends Controller
|
|
||||||
{
|
|
||||||
public function index()
|
|
||||||
{
|
|
||||||
$tiers = MembershipTier::orderBy('sort_order')->get();
|
|
||||||
return view('admin.membership-tiers.index', compact('tiers'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function store(Request $request)
|
|
||||||
{
|
|
||||||
$validated = $request->validate([
|
|
||||||
'name' => 'required|string|max:255',
|
|
||||||
'annual_fee' => 'required|numeric|min:0',
|
|
||||||
'discount_rate' => 'required|numeric|min:0|max:1',
|
|
||||||
'point_multiplier' => 'required|numeric|min:0',
|
|
||||||
'description' => 'nullable|string',
|
|
||||||
'is_default' => 'boolean',
|
|
||||||
]);
|
|
||||||
|
|
||||||
if ($request->is_default) {
|
|
||||||
MembershipTier::where('is_default', true)->update(['is_default' => false]);
|
|
||||||
}
|
|
||||||
|
|
||||||
MembershipTier::create($validated);
|
|
||||||
|
|
||||||
return redirect()->route('admin.membership-tiers.index')->with('success', '會員等級已建立');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function update(Request $request, MembershipTier $membershipTier)
|
|
||||||
{
|
|
||||||
$validated = $request->validate([
|
|
||||||
'name' => 'required|string|max:255',
|
|
||||||
'annual_fee' => 'required|numeric|min:0',
|
|
||||||
'discount_rate' => 'required|numeric|min:0|max:1',
|
|
||||||
'point_multiplier' => 'required|numeric|min:0',
|
|
||||||
'description' => 'nullable|string',
|
|
||||||
'is_default' => 'boolean',
|
|
||||||
]);
|
|
||||||
|
|
||||||
if ($request->is_default && !$membershipTier->is_default) {
|
|
||||||
MembershipTier::where('is_default', true)->update(['is_default' => false]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$membershipTier->update($validated);
|
|
||||||
|
|
||||||
return redirect()->route('admin.membership-tiers.index')->with('success', '會員等級已更新');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function destroy(MembershipTier $membershipTier)
|
|
||||||
{
|
|
||||||
$membershipTier->delete();
|
|
||||||
return redirect()->route('admin.membership-tiers.index')->with('success', '會員等級已刪除');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers\Admin;
|
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
|
||||||
use App\Models\Member\PointRule;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
|
|
||||||
class PointRuleController extends Controller
|
|
||||||
{
|
|
||||||
public function index()
|
|
||||||
{
|
|
||||||
$rules = PointRule::all();
|
|
||||||
return view('admin.point-rules.index', compact('rules'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function store(Request $request)
|
|
||||||
{
|
|
||||||
$validated = $request->validate([
|
|
||||||
'name' => 'required|string|max:255',
|
|
||||||
'trigger' => 'required|in:purchase,deposit,register,birthday,referral',
|
|
||||||
'points_per_unit' => 'required|integer|min:1',
|
|
||||||
'unit_amount' => 'required|numeric|min:0',
|
|
||||||
'validity_days' => 'required|integer|min:1',
|
|
||||||
'is_active' => 'boolean',
|
|
||||||
]);
|
|
||||||
|
|
||||||
PointRule::create($validated);
|
|
||||||
|
|
||||||
return redirect()->route('admin.point-rules.index')->with('success', '點數規則已建立');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function update(Request $request, PointRule $pointRule)
|
|
||||||
{
|
|
||||||
$validated = $request->validate([
|
|
||||||
'name' => 'required|string|max:255',
|
|
||||||
'trigger' => 'required|in:purchase,deposit,register,birthday,referral',
|
|
||||||
'points_per_unit' => 'required|integer|min:1',
|
|
||||||
'unit_amount' => 'required|numeric|min:0',
|
|
||||||
'validity_days' => 'required|integer|min:1',
|
|
||||||
'is_active' => 'boolean',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$pointRule->update($validated);
|
|
||||||
|
|
||||||
return redirect()->route('admin.point-rules.index')->with('success', '點數規則已更新');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function destroy(PointRule $pointRule)
|
|
||||||
{
|
|
||||||
$pointRule->delete();
|
|
||||||
return redirect()->route('admin.point-rules.index')->with('success', '點數規則已刪除');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,260 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers\Api;
|
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
|
||||||
use App\Models\Member\Member;
|
|
||||||
use App\Models\Member\SocialAccount;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
|
||||||
use Illuminate\Support\Facades\Hash;
|
|
||||||
use Illuminate\Support\Facades\Validator;
|
|
||||||
use Illuminate\Validation\Rules\Password;
|
|
||||||
|
|
||||||
class MemberController extends Controller
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* 會員註冊
|
|
||||||
*/
|
|
||||||
public function register(Request $request): JsonResponse
|
|
||||||
{
|
|
||||||
$validator = Validator::make($request->all(), [
|
|
||||||
'name' => ['required', 'string', 'max:255'],
|
|
||||||
'email' => ['nullable', 'email', 'unique:members,email'],
|
|
||||||
'phone' => ['nullable', 'string', 'unique:members,phone'],
|
|
||||||
'password' => ['required', Password::min(6)],
|
|
||||||
'birthday' => ['nullable', 'date'],
|
|
||||||
'gender' => ['nullable', 'in:male,female,other'],
|
|
||||||
], [
|
|
||||||
'name.required' => '請輸入姓名',
|
|
||||||
'email.unique' => '此 Email 已被註冊',
|
|
||||||
'phone.unique' => '此手機號碼已被註冊',
|
|
||||||
'password.required' => '請輸入密碼',
|
|
||||||
]);
|
|
||||||
|
|
||||||
if ($validator->fails()) {
|
|
||||||
return response()->json([
|
|
||||||
'success' => false,
|
|
||||||
'message' => '驗證失敗',
|
|
||||||
'errors' => $validator->errors(),
|
|
||||||
], 422);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 必須提供 email 或 phone 其中之一
|
|
||||||
if (empty($request->email) && empty($request->phone)) {
|
|
||||||
return response()->json([
|
|
||||||
'success' => false,
|
|
||||||
'message' => '請提供 Email 或手機號碼',
|
|
||||||
], 422);
|
|
||||||
}
|
|
||||||
|
|
||||||
$member = Member::create([
|
|
||||||
'name' => $request->name,
|
|
||||||
'email' => $request->email,
|
|
||||||
'phone' => $request->phone,
|
|
||||||
'password' => $request->password,
|
|
||||||
'birthday' => $request->birthday,
|
|
||||||
'gender' => $request->gender,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$token = $member->createToken('member-token')->plainTextToken;
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'success' => true,
|
|
||||||
'message' => '註冊成功',
|
|
||||||
'data' => [
|
|
||||||
'member' => $member,
|
|
||||||
'token' => $token,
|
|
||||||
],
|
|
||||||
], 201);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 會員登入(Email/Phone + Password)
|
|
||||||
*/
|
|
||||||
public function login(Request $request): JsonResponse
|
|
||||||
{
|
|
||||||
$validator = Validator::make($request->all(), [
|
|
||||||
'account' => ['required', 'string'],
|
|
||||||
'password' => ['required', 'string'],
|
|
||||||
], [
|
|
||||||
'account.required' => '請輸入帳號',
|
|
||||||
'password.required' => '請輸入密碼',
|
|
||||||
]);
|
|
||||||
|
|
||||||
if ($validator->fails()) {
|
|
||||||
return response()->json([
|
|
||||||
'success' => false,
|
|
||||||
'message' => '驗證失敗',
|
|
||||||
'errors' => $validator->errors(),
|
|
||||||
], 422);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 嘗試以 email 或 phone 查詢
|
|
||||||
$member = Member::where('email', $request->account)
|
|
||||||
->orWhere('phone', $request->account)
|
|
||||||
->first();
|
|
||||||
|
|
||||||
if (!$member || !Hash::check($request->password, $member->password)) {
|
|
||||||
return response()->json([
|
|
||||||
'success' => false,
|
|
||||||
'message' => '帳號或密碼錯誤',
|
|
||||||
], 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$member->is_active) {
|
|
||||||
return response()->json([
|
|
||||||
'success' => false,
|
|
||||||
'message' => '帳號已被停用',
|
|
||||||
], 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
$token = $member->createToken('member-token')->plainTextToken;
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'success' => true,
|
|
||||||
'message' => '登入成功',
|
|
||||||
'data' => [
|
|
||||||
'member' => $member,
|
|
||||||
'token' => $token,
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 社群登入
|
|
||||||
*/
|
|
||||||
public function socialLogin(Request $request): JsonResponse
|
|
||||||
{
|
|
||||||
$validator = Validator::make($request->all(), [
|
|
||||||
'provider' => ['required', 'in:line,google,facebook'],
|
|
||||||
'provider_id' => ['required', 'string'],
|
|
||||||
'access_token' => ['nullable', 'string'],
|
|
||||||
'name' => ['nullable', 'string'],
|
|
||||||
'email' => ['nullable', 'email'],
|
|
||||||
'avatar' => ['nullable', 'string'],
|
|
||||||
], [
|
|
||||||
'provider.required' => '請指定登入平台',
|
|
||||||
'provider.in' => '不支援的登入平台',
|
|
||||||
'provider_id.required' => '缺少社群用戶 ID',
|
|
||||||
]);
|
|
||||||
|
|
||||||
if ($validator->fails()) {
|
|
||||||
return response()->json([
|
|
||||||
'success' => false,
|
|
||||||
'message' => '驗證失敗',
|
|
||||||
'errors' => $validator->errors(),
|
|
||||||
], 422);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查詢是否已綁定
|
|
||||||
$socialAccount = SocialAccount::where('provider', $request->provider)
|
|
||||||
->where('provider_id', $request->provider_id)
|
|
||||||
->first();
|
|
||||||
|
|
||||||
if ($socialAccount) {
|
|
||||||
// 已綁定,直接登入
|
|
||||||
$member = $socialAccount->member;
|
|
||||||
|
|
||||||
// 更新 token
|
|
||||||
$socialAccount->update([
|
|
||||||
'access_token' => $request->access_token,
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
// 未綁定,建立新會員
|
|
||||||
$member = Member::create([
|
|
||||||
'name' => $request->name ?? '會員',
|
|
||||||
'email' => $request->email,
|
|
||||||
'avatar' => $request->avatar,
|
|
||||||
'email_verified_at' => $request->email ? now() : null, // 社群登入自動驗證
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 綁定社群帳號
|
|
||||||
$member->socialAccounts()->create([
|
|
||||||
'provider' => $request->provider,
|
|
||||||
'provider_id' => $request->provider_id,
|
|
||||||
'access_token' => $request->access_token,
|
|
||||||
'profile_data' => $request->only(['name', 'email', 'avatar']),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$member->is_active) {
|
|
||||||
return response()->json([
|
|
||||||
'success' => false,
|
|
||||||
'message' => '帳號已被停用',
|
|
||||||
], 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
$token = $member->createToken('member-token')->plainTextToken;
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'success' => true,
|
|
||||||
'message' => '登入成功',
|
|
||||||
'data' => [
|
|
||||||
'member' => $member,
|
|
||||||
'token' => $token,
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 取得個人資料
|
|
||||||
*/
|
|
||||||
public function profile(Request $request): JsonResponse
|
|
||||||
{
|
|
||||||
$member = $request->user();
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'success' => true,
|
|
||||||
'data' => [
|
|
||||||
'member' => $member->load('socialAccounts'),
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新個人資料
|
|
||||||
*/
|
|
||||||
public function updateProfile(Request $request): JsonResponse
|
|
||||||
{
|
|
||||||
$member = $request->user();
|
|
||||||
|
|
||||||
$validator = Validator::make($request->all(), [
|
|
||||||
'name' => ['nullable', 'string', 'max:255'],
|
|
||||||
'birthday' => ['nullable', 'date'],
|
|
||||||
'gender' => ['nullable', 'in:male,female,other'],
|
|
||||||
'avatar' => ['nullable', 'string'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
if ($validator->fails()) {
|
|
||||||
return response()->json([
|
|
||||||
'success' => false,
|
|
||||||
'message' => '驗證失敗',
|
|
||||||
'errors' => $validator->errors(),
|
|
||||||
], 422);
|
|
||||||
}
|
|
||||||
|
|
||||||
$member->update($request->only(['name', 'birthday', 'gender', 'avatar']));
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'success' => true,
|
|
||||||
'message' => '更新成功',
|
|
||||||
'data' => [
|
|
||||||
'member' => $member,
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 登出
|
|
||||||
*/
|
|
||||||
public function logout(Request $request): JsonResponse
|
|
||||||
{
|
|
||||||
$request->user()->currentAccessToken()->delete();
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'success' => true,
|
|
||||||
'message' => '登出成功',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers\Api\V1;
|
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
|
||||||
use App\Traits\ApiResponse;
|
|
||||||
|
|
||||||
abstract class ApiController extends Controller
|
|
||||||
{
|
|
||||||
use ApiResponse;
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers\Api\V1;
|
|
||||||
|
|
||||||
use App\Jobs\Machine\ProcessMachineLog;
|
|
||||||
use App\Models\Machine\Machine;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Support\Facades\Validator;
|
|
||||||
|
|
||||||
class MachineController extends ApiController
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* 接收機台回傳的日誌 (IoT Endpoint)
|
|
||||||
* 採用異步處理 (Queue)
|
|
||||||
*/
|
|
||||||
public function storeLog(Request $request, int $id): JsonResponse
|
|
||||||
{
|
|
||||||
$validator = Validator::make($request->all(), [
|
|
||||||
'level' => 'required|string|in:info,warning,error',
|
|
||||||
'message' => 'required|string',
|
|
||||||
'context' => 'nullable|array',
|
|
||||||
]);
|
|
||||||
|
|
||||||
if ($validator->fails()) {
|
|
||||||
return $this->errorResponse('Validation error', 422, $validator->errors());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 檢查機台是否存在
|
|
||||||
if (!Machine::where('id', $id)->exists()) {
|
|
||||||
return $this->errorResponse('Machine not found', 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 丟入隊列進行異步處理,回傳 202 Accepted
|
|
||||||
ProcessMachineLog::dispatch($id, $request->only(['level', 'message', 'context']));
|
|
||||||
|
|
||||||
return $this->successResponse([], 'Log accepted. Processing asynchronously.', 202);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers\Api\V1;
|
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
|
|
||||||
class MemberController extends Controller
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
namespace App\Http\Controllers\Auth;
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\System\User;
|
use App\Models\User;
|
||||||
use App\Providers\RouteServiceProvider;
|
use App\Providers\RouteServiceProvider;
|
||||||
use Illuminate\Auth\Events\Registered;
|
use Illuminate\Auth\Events\Registered;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use App\Models\Member\Member;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Inertia\Inertia;
|
|
||||||
use Inertia\Response;
|
|
||||||
|
|
||||||
class MemberController extends Controller
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Display a listing of the members.
|
|
||||||
*/
|
|
||||||
public function index()
|
|
||||||
{
|
|
||||||
$members = Member::query()
|
|
||||||
->latest()
|
|
||||||
->paginate(10);
|
|
||||||
|
|
||||||
return view('admin.members.index', [
|
|
||||||
'members' => $members,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Support\Facades\Http;
|
|
||||||
|
|
||||||
class SocialLoginTestController extends Controller
|
|
||||||
{
|
|
||||||
public function index()
|
|
||||||
{
|
|
||||||
return view('test.social-login');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function lineCallback(Request $request)
|
|
||||||
{
|
|
||||||
// 這裡可以實作後端換發 Token 的邏輯
|
|
||||||
// 為了測試方便,我們先直接顯示回傳的 code 與 state
|
|
||||||
// 或者嘗試交換 Token 並取得 User Profile
|
|
||||||
|
|
||||||
$code = $request->input('code');
|
|
||||||
$state = $request->input('state');
|
|
||||||
$error = $request->input('error');
|
|
||||||
|
|
||||||
return view('test.social-login', [
|
|
||||||
'line_data' => [
|
|
||||||
'code' => $code,
|
|
||||||
'state' => $state,
|
|
||||||
'error' => $error
|
|
||||||
]
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,7 @@ class TrustProxies extends Middleware
|
|||||||
*
|
*
|
||||||
* @var array<int, string>|string|null
|
* @var array<int, string>|string|null
|
||||||
*/
|
*/
|
||||||
protected $proxies = '*';
|
protected $proxies;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The headers that should be used to detect proxies.
|
* The headers that should be used to detect proxies.
|
||||||
|
|||||||
@@ -27,22 +27,11 @@ class LoginRequest extends FormRequest
|
|||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'username' => ['required', 'string'],
|
'email' => ['required', 'string', 'email'],
|
||||||
'password' => ['required', 'string'],
|
'password' => ['required', 'string'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 取得驗證規則的自訂錯誤訊息
|
|
||||||
*/
|
|
||||||
public function messages(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'username.required' => '請輸入帳號',
|
|
||||||
'password.required' => '請輸入密碼',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempt to authenticate the request's credentials.
|
* Attempt to authenticate the request's credentials.
|
||||||
*
|
*
|
||||||
@@ -52,11 +41,11 @@ class LoginRequest extends FormRequest
|
|||||||
{
|
{
|
||||||
$this->ensureIsNotRateLimited();
|
$this->ensureIsNotRateLimited();
|
||||||
|
|
||||||
if (! Auth::attempt($this->only('username', 'password'), $this->boolean('remember'))) {
|
if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
|
||||||
RateLimiter::hit($this->throttleKey());
|
RateLimiter::hit($this->throttleKey());
|
||||||
|
|
||||||
throw ValidationException::withMessages([
|
throw ValidationException::withMessages([
|
||||||
'username' => trans('auth.failed'),
|
'email' => trans('auth.failed'),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +68,7 @@ class LoginRequest extends FormRequest
|
|||||||
$seconds = RateLimiter::availableIn($this->throttleKey());
|
$seconds = RateLimiter::availableIn($this->throttleKey());
|
||||||
|
|
||||||
throw ValidationException::withMessages([
|
throw ValidationException::withMessages([
|
||||||
'username' => trans('auth.throttle', [
|
'email' => trans('auth.throttle', [
|
||||||
'seconds' => $seconds,
|
'seconds' => $seconds,
|
||||||
'minutes' => ceil($seconds / 60),
|
'minutes' => ceil($seconds / 60),
|
||||||
]),
|
]),
|
||||||
@@ -91,6 +80,6 @@ class LoginRequest extends FormRequest
|
|||||||
*/
|
*/
|
||||||
public function throttleKey(): string
|
public function throttleKey(): string
|
||||||
{
|
{
|
||||||
return Str::transliterate(Str::lower($this->string('username')).'|'.$this->ip());
|
return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Requests;
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
use App\Models\System\User;
|
use App\Models\User;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Jobs\Machine;
|
|
||||||
|
|
||||||
use App\Services\Machine\MachineService;
|
|
||||||
use Illuminate\Bus\Queueable;
|
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
|
|
||||||
class ProcessMachineLog implements ShouldQueue
|
|
||||||
{
|
|
||||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var int
|
|
||||||
*/
|
|
||||||
protected $machineId;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
protected $logData;
|
|
||||||
|
|
||||||
public function __construct(int $machineId, array $logData)
|
|
||||||
{
|
|
||||||
$this->machineId = $machineId;
|
|
||||||
$this->logData = $logData;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getMachineId(): int
|
|
||||||
{
|
|
||||||
return $this->machineId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function handle(MachineService $service): void
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$service->recordLog($this->machineId, $this->logData);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::error("Failed to process machine log for machine {$this->machineId}: " . $e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Models\System;
|
namespace App\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
@@ -1,14 +1,12 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Models\Machine;
|
namespace App\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
class Machine extends Model
|
class Machine extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'name',
|
'name',
|
||||||
'location',
|
'location',
|
||||||
@@ -1,16 +1,12 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Models\Machine;
|
namespace App\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
class MachineLog extends Model
|
class MachineLog extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
|
||||||
|
|
||||||
const UPDATED_AT = null;
|
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'machine_id',
|
'machine_id',
|
||||||
'level',
|
'level',
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models\Member;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
|
|
||||||
class DepositBonusRule extends Model
|
|
||||||
{
|
|
||||||
use HasFactory;
|
|
||||||
|
|
||||||
protected $fillable = [
|
|
||||||
'name',
|
|
||||||
'min_amount',
|
|
||||||
'bonus_type',
|
|
||||||
'bonus_value',
|
|
||||||
'is_active',
|
|
||||||
'start_at',
|
|
||||||
'end_at',
|
|
||||||
];
|
|
||||||
|
|
||||||
protected $casts = [
|
|
||||||
'min_amount' => 'decimal:2',
|
|
||||||
'bonus_value' => 'decimal:2',
|
|
||||||
'is_active' => 'boolean',
|
|
||||||
'start_at' => 'datetime',
|
|
||||||
'end_at' => 'datetime',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 取得目前有效的規則
|
|
||||||
*/
|
|
||||||
public function scopeActive($query)
|
|
||||||
{
|
|
||||||
return $query->where('is_active', true)
|
|
||||||
->where(function ($q) {
|
|
||||||
$q->whereNull('start_at')->orWhere('start_at', '<=', now());
|
|
||||||
})
|
|
||||||
->where(function ($q) {
|
|
||||||
$q->whereNull('end_at')->orWhere('end_at', '>=', now());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 計算回饋金額
|
|
||||||
*/
|
|
||||||
public function calculateBonus(float $depositAmount): float
|
|
||||||
{
|
|
||||||
if ($depositAmount < $this->min_amount) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->bonus_type === 'fixed') {
|
|
||||||
return $this->bonus_value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// percentage
|
|
||||||
return $depositAmount * ($this->bonus_value / 100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models\Member;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
||||||
|
|
||||||
class GiftDefinition extends Model
|
|
||||||
{
|
|
||||||
use HasFactory;
|
|
||||||
|
|
||||||
protected $fillable = [
|
|
||||||
'name',
|
|
||||||
'type',
|
|
||||||
'value',
|
|
||||||
'tier_id',
|
|
||||||
'trigger',
|
|
||||||
'validity_days',
|
|
||||||
'is_active',
|
|
||||||
];
|
|
||||||
|
|
||||||
protected $casts = [
|
|
||||||
'value' => 'decimal:2',
|
|
||||||
'validity_days' => 'integer',
|
|
||||||
'is_active' => 'boolean',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 適用等級
|
|
||||||
*/
|
|
||||||
public function tier(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(MembershipTier::class, 'tier_id');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 發放紀錄
|
|
||||||
*/
|
|
||||||
public function memberGifts(): HasMany
|
|
||||||
{
|
|
||||||
return $this->hasMany(MemberGift::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 有效禮品
|
|
||||||
*/
|
|
||||||
public function scopeActive($query)
|
|
||||||
{
|
|
||||||
return $query->where('is_active', true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models\Member;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
|
||||||
use Illuminate\Notifications\Notifiable;
|
|
||||||
use Laravel\Sanctum\HasApiTokens;
|
|
||||||
use Illuminate\Support\Str;
|
|
||||||
|
|
||||||
class Member extends Authenticatable
|
|
||||||
{
|
|
||||||
use HasApiTokens, HasFactory, Notifiable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 資料表名稱
|
|
||||||
*/
|
|
||||||
protected $table = 'members';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 可批量賦值的屬性
|
|
||||||
*/
|
|
||||||
protected $fillable = [
|
|
||||||
'uuid',
|
|
||||||
'name',
|
|
||||||
'email',
|
|
||||||
'phone',
|
|
||||||
'password',
|
|
||||||
'birthday',
|
|
||||||
'gender',
|
|
||||||
'avatar',
|
|
||||||
'is_active',
|
|
||||||
'email_verified_at',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 隱藏的屬性
|
|
||||||
*/
|
|
||||||
protected $hidden = [
|
|
||||||
'password',
|
|
||||||
'remember_token',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 屬性轉換
|
|
||||||
*/
|
|
||||||
protected $casts = [
|
|
||||||
'email_verified_at' => 'datetime',
|
|
||||||
'birthday' => 'date',
|
|
||||||
'is_active' => 'boolean',
|
|
||||||
'password' => 'hashed',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 建立時自動產生 UUID
|
|
||||||
*/
|
|
||||||
protected static function boot()
|
|
||||||
{
|
|
||||||
parent::boot();
|
|
||||||
|
|
||||||
static::creating(function ($model) {
|
|
||||||
if (empty($model->uuid)) {
|
|
||||||
$model->uuid = (string) Str::uuid();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 關聯:社群帳號
|
|
||||||
*/
|
|
||||||
public function socialAccounts()
|
|
||||||
{
|
|
||||||
return $this->hasMany(SocialAccount::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 關聯:錢包
|
|
||||||
*/
|
|
||||||
public function wallet()
|
|
||||||
{
|
|
||||||
return $this->hasOne(MemberWallet::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 關聯:點數帳戶
|
|
||||||
*/
|
|
||||||
public function points()
|
|
||||||
{
|
|
||||||
return $this->hasOne(MemberPoint::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 關聯:會員資格紀錄
|
|
||||||
*/
|
|
||||||
public function memberships()
|
|
||||||
{
|
|
||||||
return $this->hasMany(MemberMembership::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 關聯:禮品紀錄
|
|
||||||
*/
|
|
||||||
public function gifts()
|
|
||||||
{
|
|
||||||
return $this->hasMany(MemberGift::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 取得目前有效的會員資格
|
|
||||||
*/
|
|
||||||
public function activeMembership()
|
|
||||||
{
|
|
||||||
return $this->hasOne(MemberMembership::class)->active()->latest('starts_at');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 檢查是否已綁定指定社群
|
|
||||||
*/
|
|
||||||
public function hasSocialAccount(string $provider): bool
|
|
||||||
{
|
|
||||||
return $this->socialAccounts()->where('provider', $provider)->exists();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 取得或建立錢包
|
|
||||||
*/
|
|
||||||
public function getOrCreateWallet(): MemberWallet
|
|
||||||
{
|
|
||||||
return $this->wallet ?? $this->wallet()->create([
|
|
||||||
'balance' => 0,
|
|
||||||
'bonus_balance' => 0,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 取得或建立點數帳戶
|
|
||||||
*/
|
|
||||||
public function getOrCreatePoints(): MemberPoint
|
|
||||||
{
|
|
||||||
return $this->points ?? $this->points()->create([
|
|
||||||
'available_points' => 0,
|
|
||||||
'pending_points' => 0,
|
|
||||||
'expired_points' => 0,
|
|
||||||
'used_points' => 0,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models\Member;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
||||||
|
|
||||||
class MemberGift extends Model
|
|
||||||
{
|
|
||||||
use HasFactory;
|
|
||||||
|
|
||||||
public $timestamps = false;
|
|
||||||
|
|
||||||
protected $fillable = [
|
|
||||||
'member_id',
|
|
||||||
'gift_definition_id',
|
|
||||||
'status',
|
|
||||||
'claimed_at',
|
|
||||||
'expires_at',
|
|
||||||
];
|
|
||||||
|
|
||||||
protected $casts = [
|
|
||||||
'claimed_at' => 'datetime',
|
|
||||||
'expires_at' => 'datetime',
|
|
||||||
'created_at' => 'datetime',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 所屬會員
|
|
||||||
*/
|
|
||||||
public function member(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(Member::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 禮品定義
|
|
||||||
*/
|
|
||||||
public function giftDefinition(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(GiftDefinition::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 待領取的禮品
|
|
||||||
*/
|
|
||||||
public function scopePending($query)
|
|
||||||
{
|
|
||||||
return $query->where('status', 'pending')
|
|
||||||
->where(function ($q) {
|
|
||||||
$q->whereNull('expires_at')
|
|
||||||
->orWhere('expires_at', '>', now());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models\Member;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
||||||
|
|
||||||
class MemberMembership extends Model
|
|
||||||
{
|
|
||||||
use HasFactory;
|
|
||||||
|
|
||||||
protected $fillable = [
|
|
||||||
'member_id',
|
|
||||||
'tier_id',
|
|
||||||
'starts_at',
|
|
||||||
'expires_at',
|
|
||||||
'payment_id',
|
|
||||||
'auto_renew',
|
|
||||||
'status',
|
|
||||||
];
|
|
||||||
|
|
||||||
protected $casts = [
|
|
||||||
'starts_at' => 'datetime',
|
|
||||||
'expires_at' => 'datetime',
|
|
||||||
'auto_renew' => 'boolean',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 所屬會員
|
|
||||||
*/
|
|
||||||
public function member(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(Member::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 會員等級
|
|
||||||
*/
|
|
||||||
public function tier(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(MembershipTier::class, 'tier_id');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否有效
|
|
||||||
*/
|
|
||||||
public function getIsActiveAttribute(): bool
|
|
||||||
{
|
|
||||||
return $this->status === 'active'
|
|
||||||
&& (!$this->expires_at || $this->expires_at->isFuture());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 有效會員資格
|
|
||||||
*/
|
|
||||||
public function scopeActive($query)
|
|
||||||
{
|
|
||||||
return $query->where('status', 'active')
|
|
||||||
->where(function ($q) {
|
|
||||||
$q->whereNull('expires_at')
|
|
||||||
->orWhere('expires_at', '>', now());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models\Member;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
||||||
|
|
||||||
class MemberPoint extends Model
|
|
||||||
{
|
|
||||||
use HasFactory;
|
|
||||||
|
|
||||||
protected $fillable = [
|
|
||||||
'member_id',
|
|
||||||
'available_points',
|
|
||||||
'pending_points',
|
|
||||||
'expired_points',
|
|
||||||
'used_points',
|
|
||||||
];
|
|
||||||
|
|
||||||
protected $casts = [
|
|
||||||
'available_points' => 'integer',
|
|
||||||
'pending_points' => 'integer',
|
|
||||||
'expired_points' => 'integer',
|
|
||||||
'used_points' => 'integer',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 所屬會員
|
|
||||||
*/
|
|
||||||
public function member(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(Member::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 點數異動紀錄
|
|
||||||
*/
|
|
||||||
public function transactions(): HasMany
|
|
||||||
{
|
|
||||||
return $this->hasMany(PointTransaction::class, 'member_id', 'member_id');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models\Member;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
||||||
|
|
||||||
class MemberWallet extends Model
|
|
||||||
{
|
|
||||||
use HasFactory;
|
|
||||||
|
|
||||||
protected $fillable = [
|
|
||||||
'member_id',
|
|
||||||
'balance',
|
|
||||||
'bonus_balance',
|
|
||||||
];
|
|
||||||
|
|
||||||
protected $casts = [
|
|
||||||
'balance' => 'decimal:2',
|
|
||||||
'bonus_balance' => 'decimal:2',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 所屬會員
|
|
||||||
*/
|
|
||||||
public function member(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(Member::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 交易紀錄
|
|
||||||
*/
|
|
||||||
public function transactions(): HasMany
|
|
||||||
{
|
|
||||||
return $this->hasMany(WalletTransaction::class, 'member_id', 'member_id');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 總餘額 (儲值 + 回饋)
|
|
||||||
*/
|
|
||||||
public function getTotalBalanceAttribute(): float
|
|
||||||
{
|
|
||||||
return $this->balance + $this->bonus_balance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models\Member;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
||||||
|
|
||||||
class MembershipTier extends Model
|
|
||||||
{
|
|
||||||
use HasFactory;
|
|
||||||
|
|
||||||
protected $fillable = [
|
|
||||||
'name',
|
|
||||||
'annual_fee',
|
|
||||||
'discount_rate',
|
|
||||||
'point_multiplier',
|
|
||||||
'description',
|
|
||||||
'is_default',
|
|
||||||
'sort_order',
|
|
||||||
];
|
|
||||||
|
|
||||||
protected $casts = [
|
|
||||||
'annual_fee' => 'decimal:2',
|
|
||||||
'discount_rate' => 'decimal:2',
|
|
||||||
'point_multiplier' => 'decimal:2',
|
|
||||||
'is_default' => 'boolean',
|
|
||||||
'sort_order' => 'integer',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 此等級的會員紀錄
|
|
||||||
*/
|
|
||||||
public function memberships(): HasMany
|
|
||||||
{
|
|
||||||
return $this->hasMany(MemberMembership::class, 'tier_id');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 此等級的禮品定義
|
|
||||||
*/
|
|
||||||
public function giftDefinitions(): HasMany
|
|
||||||
{
|
|
||||||
return $this->hasMany(GiftDefinition::class, 'tier_id');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 取得預設等級
|
|
||||||
*/
|
|
||||||
public static function getDefault(): ?self
|
|
||||||
{
|
|
||||||
return static::where('is_default', true)->first();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否為免費等級
|
|
||||||
*/
|
|
||||||
public function getIsFreeAttribute(): bool
|
|
||||||
{
|
|
||||||
return $this->annual_fee <= 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models\Member;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
|
|
||||||
class PointRule extends Model
|
|
||||||
{
|
|
||||||
use HasFactory;
|
|
||||||
|
|
||||||
protected $fillable = [
|
|
||||||
'name',
|
|
||||||
'trigger',
|
|
||||||
'points_per_unit',
|
|
||||||
'unit_amount',
|
|
||||||
'validity_days',
|
|
||||||
'is_active',
|
|
||||||
];
|
|
||||||
|
|
||||||
protected $casts = [
|
|
||||||
'points_per_unit' => 'integer',
|
|
||||||
'unit_amount' => 'decimal:2',
|
|
||||||
'validity_days' => 'integer',
|
|
||||||
'is_active' => 'boolean',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 取得有效規則
|
|
||||||
*/
|
|
||||||
public function scopeActive($query)
|
|
||||||
{
|
|
||||||
return $query->where('is_active', true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根據金額計算可獲得點數
|
|
||||||
*/
|
|
||||||
public function calculatePoints(float $amount): int
|
|
||||||
{
|
|
||||||
if ($this->unit_amount <= 0) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (int) floor($amount / $this->unit_amount) * $this->points_per_unit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models\Member;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
||||||
|
|
||||||
class PointTransaction extends Model
|
|
||||||
{
|
|
||||||
use HasFactory;
|
|
||||||
|
|
||||||
public $timestamps = false;
|
|
||||||
|
|
||||||
protected $fillable = [
|
|
||||||
'member_id',
|
|
||||||
'type',
|
|
||||||
'points',
|
|
||||||
'balance_after',
|
|
||||||
'description',
|
|
||||||
'expires_at',
|
|
||||||
'reference_type',
|
|
||||||
'reference_id',
|
|
||||||
];
|
|
||||||
|
|
||||||
protected $casts = [
|
|
||||||
'points' => 'integer',
|
|
||||||
'balance_after' => 'integer',
|
|
||||||
'expires_at' => 'datetime',
|
|
||||||
'created_at' => 'datetime',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 所屬會員
|
|
||||||
*/
|
|
||||||
public function member(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(Member::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否已過期
|
|
||||||
*/
|
|
||||||
public function getIsExpiredAttribute(): bool
|
|
||||||
{
|
|
||||||
return $this->expires_at && $this->expires_at->isPast();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models\Member;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
|
|
||||||
class SocialAccount extends Model
|
|
||||||
{
|
|
||||||
use HasFactory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 資料表名稱
|
|
||||||
*/
|
|
||||||
protected $table = 'social_accounts';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 可批量賦值的屬性
|
|
||||||
*/
|
|
||||||
protected $fillable = [
|
|
||||||
'member_id',
|
|
||||||
'provider',
|
|
||||||
'provider_id',
|
|
||||||
'access_token',
|
|
||||||
'refresh_token',
|
|
||||||
'profile_data',
|
|
||||||
'token_expires_at',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 屬性轉換
|
|
||||||
*/
|
|
||||||
protected $casts = [
|
|
||||||
'profile_data' => 'array',
|
|
||||||
'token_expires_at' => 'datetime',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 隱藏的屬性
|
|
||||||
*/
|
|
||||||
protected $hidden = [
|
|
||||||
'access_token',
|
|
||||||
'refresh_token',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 關聯:會員
|
|
||||||
*/
|
|
||||||
public function member()
|
|
||||||
{
|
|
||||||
return $this->belongsTo(Member::class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models\Member;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
||||||
|
|
||||||
class WalletTransaction extends Model
|
|
||||||
{
|
|
||||||
use HasFactory;
|
|
||||||
|
|
||||||
public $timestamps = false;
|
|
||||||
|
|
||||||
protected $fillable = [
|
|
||||||
'member_id',
|
|
||||||
'type',
|
|
||||||
'amount',
|
|
||||||
'balance_after',
|
|
||||||
'description',
|
|
||||||
'reference_type',
|
|
||||||
'reference_id',
|
|
||||||
];
|
|
||||||
|
|
||||||
protected $casts = [
|
|
||||||
'amount' => 'decimal:2',
|
|
||||||
'balance_after' => 'decimal:2',
|
|
||||||
'created_at' => 'datetime',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 所屬會員
|
|
||||||
*/
|
|
||||||
public function member(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(Member::class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Models\System;
|
namespace App\Models;
|
||||||
|
|
||||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
@@ -18,7 +18,6 @@ class User extends Authenticatable
|
|||||||
* @var array<int, string>
|
* @var array<int, string>
|
||||||
*/
|
*/
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'username',
|
|
||||||
'name',
|
'name',
|
||||||
'email',
|
'email',
|
||||||
'password',
|
'password',
|
||||||
@@ -19,8 +19,6 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
*/
|
*/
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
if (!$this->app->isLocal()) {
|
//
|
||||||
\Illuminate\Support\Facades\URL::forceScheme('https');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Services\Machine;
|
|
||||||
|
|
||||||
use App\Models\Machine\Machine;
|
|
||||||
use App\Models\Machine\MachineLog;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
|
|
||||||
class MachineService
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* 處理機台日誌寫入與狀態更新
|
|
||||||
*/
|
|
||||||
public function recordLog(int $machineId, array $data): MachineLog
|
|
||||||
{
|
|
||||||
$machine = Machine::findOrFail($machineId);
|
|
||||||
|
|
||||||
// 建立日誌紀錄
|
|
||||||
$log = $machine->logs()->create([
|
|
||||||
'level' => $data['level'] ?? 'info',
|
|
||||||
'message' => $data['message'],
|
|
||||||
'context' => $data['context'] ?? null,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 同步更新機台最後活耀時間與狀態
|
|
||||||
$machine->update([
|
|
||||||
'last_heartbeat_at' => now(),
|
|
||||||
'status' => $this->resolveStatus($data),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return $log;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根據日誌內容判斷機台是否應標記成錯誤
|
|
||||||
*/
|
|
||||||
protected function resolveStatus(array $data): string
|
|
||||||
{
|
|
||||||
if (isset($data['level']) && $data['level'] === 'error') {
|
|
||||||
return 'error';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'online';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Traits;
|
|
||||||
|
|
||||||
use Illuminate\Http\JsonResponse;
|
|
||||||
|
|
||||||
trait ApiResponse
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* 回傳成功的回應
|
|
||||||
*
|
|
||||||
* @param mixed $data
|
|
||||||
* @param string $message
|
|
||||||
* @param int $code
|
|
||||||
* @return JsonResponse
|
|
||||||
*/
|
|
||||||
public function successResponse($data = [], string $message = 'OK', int $code = 200): JsonResponse
|
|
||||||
{
|
|
||||||
return response()->json([
|
|
||||||
'success' => true,
|
|
||||||
'code' => $code,
|
|
||||||
'message' => $message,
|
|
||||||
'data' => empty($data) ? new \stdClass() : $data, // 確保前端收到的是 Object 而非 Empty Array
|
|
||||||
], $code);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 回傳錯誤的回應
|
|
||||||
*
|
|
||||||
* @param string $message
|
|
||||||
* @param int $code
|
|
||||||
* @param mixed $errors
|
|
||||||
* @return JsonResponse
|
|
||||||
*/
|
|
||||||
public function errorResponse(string $message, int $code = 400, $errors = null): JsonResponse
|
|
||||||
{
|
|
||||||
$response = [
|
|
||||||
'success' => false,
|
|
||||||
'code' => $code,
|
|
||||||
'message' => $message,
|
|
||||||
];
|
|
||||||
|
|
||||||
if (!is_null($errors)) {
|
|
||||||
$response['errors'] = $errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
return response()->json($response, $code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
49
artisan
49
artisan
@@ -1,18 +1,53 @@
|
|||||||
#!/usr/bin/env php
|
#!/usr/bin/env php
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Foundation\Application;
|
|
||||||
use Symfony\Component\Console\Input\ArgvInput;
|
|
||||||
|
|
||||||
define('LARAVEL_START', microtime(true));
|
define('LARAVEL_START', microtime(true));
|
||||||
|
|
||||||
// Register the Composer autoloader...
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Register The Auto Loader
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Composer provides a convenient, automatically generated class loader
|
||||||
|
| for our application. We just need to utilize it! We'll require it
|
||||||
|
| into the script here so that we do not have to worry about the
|
||||||
|
| loading of any of our classes manually. It's great to relax.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
require __DIR__.'/vendor/autoload.php';
|
require __DIR__.'/vendor/autoload.php';
|
||||||
|
|
||||||
// Bootstrap Laravel and handle the command...
|
|
||||||
/** @var Application $app */
|
|
||||||
$app = require_once __DIR__.'/bootstrap/app.php';
|
$app = require_once __DIR__.'/bootstrap/app.php';
|
||||||
|
|
||||||
$status = $app->handleCommand(new ArgvInput);
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Run The Artisan Application
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When we run the console application, the current CLI command will be
|
||||||
|
| executed in this console and the response sent back to a terminal
|
||||||
|
| or another output device for the developers. Here goes nothing!
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
|
||||||
|
|
||||||
|
$status = $kernel->handle(
|
||||||
|
$input = new Symfony\Component\Console\Input\ArgvInput,
|
||||||
|
new Symfony\Component\Console\Output\ConsoleOutput
|
||||||
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Shutdown The Application
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Once Artisan has finished running, we will fire off the shutdown events
|
||||||
|
| so that any final work may be done by the application before we shut
|
||||||
|
| down the process. This is the last thing to happen to the request.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
$kernel->terminate($input, $status);
|
||||||
|
|
||||||
exit($status);
|
exit($status);
|
||||||
|
|||||||
14
compose.yaml
14
compose.yaml
@@ -6,12 +6,12 @@ services:
|
|||||||
args:
|
args:
|
||||||
WWWGROUP: '${WWWGROUP}'
|
WWWGROUP: '${WWWGROUP}'
|
||||||
image: 'sail-8.5/app'
|
image: 'sail-8.5/app'
|
||||||
container_name: star-cloud-laravel
|
container_name: start-cloud-laravel
|
||||||
hostname: star-cloud-laravel
|
hostname: start-cloud-laravel
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- 'host.docker.internal:host-gateway'
|
- 'host.docker.internal:host-gateway'
|
||||||
ports:
|
ports:
|
||||||
- '${APP_PORT:-80}:8080'
|
- '${APP_PORT:-80}:80'
|
||||||
- '${VITE_PORT:-5173}:${VITE_PORT:-5173}'
|
- '${VITE_PORT:-5173}:${VITE_PORT:-5173}'
|
||||||
environment:
|
environment:
|
||||||
WWWUSER: '${WWWUSER}'
|
WWWUSER: '${WWWUSER}'
|
||||||
@@ -29,8 +29,8 @@ services:
|
|||||||
|
|
||||||
mysql:
|
mysql:
|
||||||
image: 'mysql/mysql-server:8.0'
|
image: 'mysql/mysql-server:8.0'
|
||||||
container_name: star-cloud-mysql
|
container_name: start-cloud-mysql
|
||||||
hostname: star-cloud-mysql
|
hostname: start-cloud-mysql
|
||||||
ports:
|
ports:
|
||||||
- '${FORWARD_DB_PORT:-3306}:3306'
|
- '${FORWARD_DB_PORT:-3306}:3306'
|
||||||
environment:
|
environment:
|
||||||
@@ -56,8 +56,8 @@ services:
|
|||||||
timeout: 5s
|
timeout: 5s
|
||||||
redis:
|
redis:
|
||||||
image: 'redis:alpine'
|
image: 'redis:alpine'
|
||||||
container_name: star-cloud-redis
|
container_name: start-cloud-redis
|
||||||
hostname: star-cloud-redis
|
hostname: start-cloud-redis
|
||||||
ports:
|
ports:
|
||||||
- '${FORWARD_REDIS_PORT:-6379}:6379'
|
- '${FORWARD_REDIS_PORT:-6379}:6379'
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -2,27 +2,24 @@
|
|||||||
"name": "laravel/laravel",
|
"name": "laravel/laravel",
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"description": "The skeleton application for the Laravel framework.",
|
"description": "The skeleton application for the Laravel framework.",
|
||||||
"keywords": [
|
"keywords": ["laravel", "framework"],
|
||||||
"laravel",
|
|
||||||
"framework"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.2",
|
"php": "^8.1",
|
||||||
"guzzlehttp/guzzle": "^7.8",
|
"guzzlehttp/guzzle": "^7.2",
|
||||||
"laravel/framework": "^12.0",
|
"laravel/framework": "^10.10",
|
||||||
"laravel/sanctum": "^4.3",
|
"laravel/sanctum": "^3.3",
|
||||||
"laravel/tinker": "^2.10.1"
|
"laravel/tinker": "^2.8"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"fakerphp/faker": "^1.23",
|
"fakerphp/faker": "^1.9.1",
|
||||||
"laravel/breeze": "^2.0",
|
"laravel/breeze": "^1.29",
|
||||||
"laravel/pail": "^1.2.2",
|
"laravel/pint": "^1.0",
|
||||||
"laravel/pint": "^1.24",
|
"laravel/sail": "^1.18",
|
||||||
"laravel/sail": "^1.41",
|
"mockery/mockery": "^1.4.4",
|
||||||
"mockery/mockery": "^1.6",
|
"nunomaduro/collision": "^7.0",
|
||||||
"nunomaduro/collision": "^8.6",
|
"phpunit/phpunit": "^10.1",
|
||||||
"phpunit/phpunit": "^11.5.3"
|
"spatie/laravel-ignition": "^2.0"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
@@ -67,4 +64,4 @@
|
|||||||
},
|
},
|
||||||
"minimum-stability": "stable",
|
"minimum-stability": "stable",
|
||||||
"prefer-stable": true
|
"prefer-stable": true
|
||||||
}
|
}
|
||||||
|
|||||||
2623
composer.lock
generated
2623
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -62,7 +62,7 @@ return [
|
|||||||
'providers' => [
|
'providers' => [
|
||||||
'users' => [
|
'users' => [
|
||||||
'driver' => 'eloquent',
|
'driver' => 'eloquent',
|
||||||
'model' => App\Models\System\User::class,
|
'model' => App\Models\User::class,
|
||||||
],
|
],
|
||||||
|
|
||||||
// 'users' => [
|
// 'users' => [
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Database\Factories\Machine;
|
|
||||||
|
|
||||||
use App\Models\Machine\Machine;
|
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
|
||||||
|
|
||||||
class MachineFactory extends Factory
|
|
||||||
{
|
|
||||||
protected $model = Machine::class;
|
|
||||||
|
|
||||||
public function definition(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'name' => 'Machine-' . fake()->unique()->numberBetween(101, 999),
|
|
||||||
'location' => fake()->address(),
|
|
||||||
'status' => fake()->randomElement(['online', 'offline', 'error']),
|
|
||||||
'temperature' => fake()->randomFloat(2, 2, 10),
|
|
||||||
'firmware_version' => 'v' . fake()->randomElement(['1.0.0', '1.1.2', '2.0.1']),
|
|
||||||
'last_heartbeat_at' => fake()->dateTimeBetween('-1 day', 'now'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Database\Factories\Machine;
|
|
||||||
|
|
||||||
use App\Models\Machine\Machine;
|
|
||||||
use App\Models\Machine\MachineLog;
|
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
|
||||||
|
|
||||||
class MachineLogFactory extends Factory
|
|
||||||
{
|
|
||||||
protected $model = MachineLog::class;
|
|
||||||
|
|
||||||
public function definition(): array
|
|
||||||
{
|
|
||||||
$messages = [
|
|
||||||
'info' => [
|
|
||||||
'機台啟動完成',
|
|
||||||
'系統心跳上報',
|
|
||||||
'交易成功 (訂單 #'.fake()->numberBetween(1000, 9999).')',
|
|
||||||
'補貨作業完成',
|
|
||||||
'環境溫度穩定 (24C)',
|
|
||||||
],
|
|
||||||
'warning' => [
|
|
||||||
'貨道 A3 庫存偏低',
|
|
||||||
'通訊品質不穩定',
|
|
||||||
'感測器回報數值異常',
|
|
||||||
'機門開啟次數過多',
|
|
||||||
],
|
|
||||||
'error' => [
|
|
||||||
'馬達轉動失效 (貨道 B2)',
|
|
||||||
'硬幣器卡幣',
|
|
||||||
'散熱風扇停止運作',
|
|
||||||
'電源供應模組故障',
|
|
||||||
'網路連線中斷',
|
|
||||||
]
|
|
||||||
];
|
|
||||||
|
|
||||||
$level = fake()->randomElement(['info', 'warning', 'error']);
|
|
||||||
|
|
||||||
return [
|
|
||||||
'machine_id' => Machine::factory(),
|
|
||||||
'level' => $level,
|
|
||||||
'message' => fake()->randomElement($messages[$level]),
|
|
||||||
'context' => [
|
|
||||||
'ip' => fake()->ipv4(),
|
|
||||||
'uptime' => fake()->numberBetween(1000, 100000),
|
|
||||||
],
|
|
||||||
'created_at' => fake()->dateTimeBetween('-1 day', 'now'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Database\Factories\Member;
|
|
||||||
|
|
||||||
use App\Models\Member\Member;
|
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
|
||||||
use Illuminate\Support\Str;
|
|
||||||
|
|
||||||
class MemberFactory extends Factory
|
|
||||||
{
|
|
||||||
protected $model = Member::class;
|
|
||||||
|
|
||||||
public function definition(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'uuid' => (string) Str::uuid(),
|
|
||||||
'name' => fake()->name(),
|
|
||||||
'email' => fake()->unique()->safeEmail(),
|
|
||||||
'phone' => '09' . fake()->numberBetween(10000000, 99999999),
|
|
||||||
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
|
|
||||||
'birthday' => fake()->date(),
|
|
||||||
'gender' => fake()->randomElement(['male', 'female', 'other']),
|
|
||||||
'is_active' => true,
|
|
||||||
'email_verified_at' => now(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,7 @@ use Illuminate\Support\Facades\Hash;
|
|||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\System\User>
|
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
|
||||||
*/
|
*/
|
||||||
class UserFactory extends Factory
|
class UserFactory extends Factory
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('sessions', function (Blueprint $table) {
|
|
||||||
$table->string('id')->primary();
|
|
||||||
$table->foreignId('user_id')->nullable()->index();
|
|
||||||
$table->string('ip_address', 45)->nullable();
|
|
||||||
$table->text('user_agent')->nullable();
|
|
||||||
$table->longText('payload');
|
|
||||||
$table->integer('last_activity')->index();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('sessions');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
if (!Schema::hasColumn('users', 'username')) {
|
|
||||||
Schema::table('users', function (Blueprint $table) {
|
|
||||||
$table->string('username')->unique()->nullable()->after('id');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
if (Schema::hasColumn('users', 'username')) {
|
|
||||||
Schema::table('users', function (Blueprint $table) {
|
|
||||||
$table->dropColumn('username');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('members', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->uuid('uuid')->unique();
|
|
||||||
$table->string('name');
|
|
||||||
$table->string('email')->nullable()->unique();
|
|
||||||
$table->string('phone')->nullable()->unique();
|
|
||||||
$table->string('password')->nullable();
|
|
||||||
$table->date('birthday')->nullable();
|
|
||||||
$table->enum('gender', ['male', 'female', 'other'])->nullable();
|
|
||||||
$table->string('avatar')->nullable();
|
|
||||||
$table->boolean('is_active')->default(true);
|
|
||||||
$table->timestamp('email_verified_at')->nullable();
|
|
||||||
$table->rememberToken();
|
|
||||||
$table->timestamps();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('members');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('social_accounts', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->foreignId('member_id')->constrained('members')->onDelete('cascade');
|
|
||||||
$table->enum('provider', ['line', 'google', 'facebook']);
|
|
||||||
$table->string('provider_id');
|
|
||||||
$table->text('access_token')->nullable();
|
|
||||||
$table->text('refresh_token')->nullable();
|
|
||||||
$table->json('profile_data')->nullable();
|
|
||||||
$table->timestamp('token_expires_at')->nullable();
|
|
||||||
$table->timestamps();
|
|
||||||
|
|
||||||
// 同一平台同一用戶只能綁定一次
|
|
||||||
$table->unique(['provider', 'provider_id']);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('social_accounts');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* 會員錢包
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('member_wallets', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->foreignId('member_id')->constrained('members')->onDelete('cascade');
|
|
||||||
$table->decimal('balance', 12, 2)->default(0)->comment('錢包餘額');
|
|
||||||
$table->decimal('bonus_balance', 12, 2)->default(0)->comment('回饋金餘額');
|
|
||||||
$table->timestamps();
|
|
||||||
|
|
||||||
$table->unique('member_id');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('member_wallets');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* 錢包交易紀錄
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('wallet_transactions', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->foreignId('member_id')->constrained('members')->onDelete('cascade');
|
|
||||||
$table->enum('type', ['deposit', 'consume', 'refund', 'bonus', 'adjust'])->comment('交易類型');
|
|
||||||
$table->decimal('amount', 12, 2)->comment('異動金額');
|
|
||||||
$table->decimal('balance_after', 12, 2)->comment('異動後餘額');
|
|
||||||
$table->string('description')->nullable()->comment('說明');
|
|
||||||
$table->string('reference_type')->nullable()->comment('關聯類型');
|
|
||||||
$table->unsignedBigInteger('reference_id')->nullable()->comment('關聯ID');
|
|
||||||
$table->timestamp('created_at')->useCurrent();
|
|
||||||
|
|
||||||
$table->index(['member_id', 'created_at']);
|
|
||||||
$table->index(['reference_type', 'reference_id']);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('wallet_transactions');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* 儲值回饋規則
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('deposit_bonus_rules', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->string('name')->comment('規則名稱');
|
|
||||||
$table->decimal('min_amount', 12, 2)->comment('最低儲值金額');
|
|
||||||
$table->enum('bonus_type', ['fixed', 'percentage'])->comment('回饋類型');
|
|
||||||
$table->decimal('bonus_value', 12, 2)->comment('回饋值');
|
|
||||||
$table->boolean('is_active')->default(true);
|
|
||||||
$table->datetime('start_at')->nullable()->comment('開始時間');
|
|
||||||
$table->datetime('end_at')->nullable()->comment('結束時間');
|
|
||||||
$table->timestamps();
|
|
||||||
|
|
||||||
$table->index(['is_active', 'start_at', 'end_at']);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('deposit_bonus_rules');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* 會員點數帳戶
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('member_points', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->foreignId('member_id')->constrained('members')->onDelete('cascade');
|
|
||||||
$table->integer('available_points')->default(0)->comment('可用點數');
|
|
||||||
$table->integer('pending_points')->default(0)->comment('待生效點數');
|
|
||||||
$table->integer('expired_points')->default(0)->comment('已過期點數(統計)');
|
|
||||||
$table->integer('used_points')->default(0)->comment('已使用點數(統計)');
|
|
||||||
$table->timestamps();
|
|
||||||
|
|
||||||
$table->unique('member_id');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('member_points');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* 點數異動紀錄
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('point_transactions', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->foreignId('member_id')->constrained('members')->onDelete('cascade');
|
|
||||||
$table->enum('type', ['earn', 'use', 'expire', 'gift', 'adjust'])->comment('異動類型');
|
|
||||||
$table->integer('points')->comment('異動點數');
|
|
||||||
$table->integer('balance_after')->comment('異動後餘額');
|
|
||||||
$table->string('description')->nullable()->comment('說明');
|
|
||||||
$table->datetime('expires_at')->nullable()->comment('此筆點數到期日');
|
|
||||||
$table->string('reference_type')->nullable()->comment('關聯類型');
|
|
||||||
$table->unsignedBigInteger('reference_id')->nullable()->comment('關聯ID');
|
|
||||||
$table->timestamp('created_at')->useCurrent();
|
|
||||||
|
|
||||||
$table->index(['member_id', 'created_at']);
|
|
||||||
$table->index('expires_at');
|
|
||||||
$table->index(['reference_type', 'reference_id']);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('point_transactions');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* 點數規則
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('point_rules', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->string('name')->comment('規則名稱');
|
|
||||||
$table->enum('trigger', ['purchase', 'deposit', 'register', 'birthday', 'referral'])->comment('觸發條件');
|
|
||||||
$table->integer('points_per_unit')->default(1)->comment('每單位獲得點數');
|
|
||||||
$table->decimal('unit_amount', 12, 2)->default(100)->comment('單位金額');
|
|
||||||
$table->integer('validity_days')->default(365)->comment('點數有效天數');
|
|
||||||
$table->boolean('is_active')->default(true);
|
|
||||||
$table->timestamps();
|
|
||||||
|
|
||||||
$table->index('is_active');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('point_rules');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* 會員等級定義
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('membership_tiers', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->string('name')->comment('等級名稱');
|
|
||||||
$table->decimal('annual_fee', 12, 2)->default(0)->comment('年費金額');
|
|
||||||
$table->decimal('discount_rate', 4, 2)->default(1.00)->comment('折扣比例(0.95=95折)');
|
|
||||||
$table->decimal('point_multiplier', 4, 2)->default(1.00)->comment('點數倍率');
|
|
||||||
$table->text('description')->nullable()->comment('說明');
|
|
||||||
$table->boolean('is_default')->default(false)->comment('是否為預設等級');
|
|
||||||
$table->integer('sort_order')->default(0)->comment('排序');
|
|
||||||
$table->timestamps();
|
|
||||||
|
|
||||||
$table->index('is_default');
|
|
||||||
$table->index('sort_order');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('membership_tiers');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* 會員等級紀錄
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('member_memberships', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->foreignId('member_id')->constrained('members')->onDelete('cascade');
|
|
||||||
$table->foreignId('tier_id')->constrained('membership_tiers')->onDelete('cascade');
|
|
||||||
$table->datetime('starts_at')->comment('生效日');
|
|
||||||
$table->datetime('expires_at')->nullable()->comment('到期日');
|
|
||||||
$table->unsignedBigInteger('payment_id')->nullable()->comment('付款紀錄ID');
|
|
||||||
$table->boolean('auto_renew')->default(false)->comment('是否自動續約');
|
|
||||||
$table->enum('status', ['active', 'expired', 'cancelled'])->default('active');
|
|
||||||
$table->timestamps();
|
|
||||||
|
|
||||||
$table->index(['member_id', 'status']);
|
|
||||||
$table->index('expires_at');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('member_memberships');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* 禮品/福利定義
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('gift_definitions', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->string('name')->comment('禮品名稱');
|
|
||||||
$table->enum('type', ['points', 'coupon', 'product', 'discount', 'cash'])->comment('禮品類型');
|
|
||||||
$table->decimal('value', 12, 2)->default(0)->comment('數值');
|
|
||||||
$table->foreignId('tier_id')->nullable()->constrained('membership_tiers')->nullOnDelete()->comment('適用等級');
|
|
||||||
$table->enum('trigger', ['register', 'birthday', 'annual', 'upgrade', 'manual'])->comment('觸發條件');
|
|
||||||
$table->integer('validity_days')->default(30)->comment('有效天數');
|
|
||||||
$table->boolean('is_active')->default(true);
|
|
||||||
$table->timestamps();
|
|
||||||
|
|
||||||
$table->index(['is_active', 'trigger']);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('gift_definitions');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* 會員禮品發放紀錄
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('member_gifts', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->foreignId('member_id')->constrained('members')->onDelete('cascade');
|
|
||||||
$table->foreignId('gift_definition_id')->constrained('gift_definitions')->onDelete('cascade');
|
|
||||||
$table->enum('status', ['pending', 'claimed', 'expired'])->default('pending');
|
|
||||||
$table->datetime('claimed_at')->nullable()->comment('領取時間');
|
|
||||||
$table->datetime('expires_at')->nullable()->comment('有效期限');
|
|
||||||
$table->timestamp('created_at')->useCurrent();
|
|
||||||
|
|
||||||
$table->index(['member_id', 'status']);
|
|
||||||
$table->index('expires_at');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('member_gifts');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Database\Seeders;
|
|
||||||
|
|
||||||
use App\Models\System\User;
|
|
||||||
use Illuminate\Database\Seeder;
|
|
||||||
use Illuminate\Support\Facades\Hash;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 管理員帳號 Seeder
|
|
||||||
*
|
|
||||||
* 執行方式:php artisan db:seed --class=AdminUserSeeder
|
|
||||||
*/
|
|
||||||
class AdminUserSeeder extends Seeder
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the database seeds.
|
|
||||||
*/
|
|
||||||
public function run(): void
|
|
||||||
{
|
|
||||||
// 檢查是否已存在 admin 帳號,避免重複建立
|
|
||||||
$admin = User::where('username', 'admin')->first();
|
|
||||||
|
|
||||||
if ($admin) {
|
|
||||||
$this->command->info('Admin 帳號已存在,執行更新密碼與資料。');
|
|
||||||
$admin->update([
|
|
||||||
'name' => 'Admin',
|
|
||||||
'email' => 'admin@star-cloud.com',
|
|
||||||
'password' => Hash::make('password'),
|
|
||||||
'role' => 'admin',
|
|
||||||
]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
User::create([
|
|
||||||
'username' => 'admin',
|
|
||||||
'name' => 'Admin',
|
|
||||||
'email' => 'admin@star-cloud.com',
|
|
||||||
'password' => Hash::make('password'),
|
|
||||||
'role' => 'admin',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->command->info('Admin 帳號建立成功!');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,16 +9,15 @@ class DatabaseSeeder extends Seeder
|
|||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Seed the application's database.
|
* Seed the application's database.
|
||||||
*
|
|
||||||
* 執行全部 Seeder:php artisan db:seed
|
|
||||||
* 執行單一 Seeder:php artisan db:seed --class=AdminUserSeeder
|
|
||||||
*/
|
*/
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
$this->call([
|
// \App\Models\User::factory(10)->create();
|
||||||
AdminUserSeeder::class,
|
|
||||||
MachineSeeder::class,
|
\App\Models\User::factory()->create([
|
||||||
MemberSeeder::class,
|
'name' => 'Admin',
|
||||||
|
'email' => 'admin@star-cloud.com',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Database\Seeders;
|
|
||||||
|
|
||||||
use App\Models\Machine\Machine;
|
|
||||||
use App\Models\Machine\MachineLog;
|
|
||||||
use Illuminate\Database\Seeder;
|
|
||||||
|
|
||||||
class MachineSeeder extends Seeder
|
|
||||||
{
|
|
||||||
public function run(): void
|
|
||||||
{
|
|
||||||
// 建立 50 台機台
|
|
||||||
Machine::factory()->count(50)->create()->each(function ($machine) {
|
|
||||||
// 每台機台隨機建立 5-10 筆初始日誌
|
|
||||||
MachineLog::factory()->count(rand(5, 10))->create([
|
|
||||||
'machine_id' => $machine->id,
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Database\Seeders;
|
|
||||||
|
|
||||||
use App\Models\Member\Member;
|
|
||||||
use App\Models\Member\MemberWallet;
|
|
||||||
use Illuminate\Database\Seeder;
|
|
||||||
|
|
||||||
class MemberSeeder extends Seeder
|
|
||||||
{
|
|
||||||
public function run(): void
|
|
||||||
{
|
|
||||||
// 建立 20 位會員並分配錢包
|
|
||||||
Member::factory()->count(20)->create()->each(function ($member) {
|
|
||||||
MemberWallet::create([
|
|
||||||
'member_id' => $member->id,
|
|
||||||
'balance' => fake()->randomFloat(2, 100, 5000),
|
|
||||||
'bonus_balance' => fake()->randomFloat(2, 0, 500),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
FROM ubuntu:24.04
|
|
||||||
|
|
||||||
LABEL maintainer="Taylor Otwell"
|
|
||||||
|
|
||||||
ARG WWWGROUP
|
|
||||||
ARG NODE_VERSION=22
|
|
||||||
ARG MYSQL_CLIENT="mysql-client"
|
|
||||||
ARG POSTGRES_VERSION=18
|
|
||||||
|
|
||||||
WORKDIR /var/www/html
|
|
||||||
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
|
||||||
ENV TZ=UTC
|
|
||||||
ENV SUPERVISOR_PHP_COMMAND="/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80"
|
|
||||||
ENV SUPERVISOR_PHP_USER="sail"
|
|
||||||
ENV PLAYWRIGHT_BROWSERS_PATH=0
|
|
||||||
|
|
||||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
|
||||||
|
|
||||||
RUN echo "Acquire::http::Pipeline-Depth 0;" > /etc/apt/apt.conf.d/99custom && \
|
|
||||||
echo "Acquire::http::No-Cache true;" >> /etc/apt/apt.conf.d/99custom && \
|
|
||||||
echo "Acquire::BrokenProxy true;" >> /etc/apt/apt.conf.d/99custom
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get upgrade -y \
|
|
||||||
&& mkdir -p /etc/apt/keyrings \
|
|
||||||
&& apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python3 dnsutils librsvg2-bin fswatch ffmpeg nano \
|
|
||||||
&& curl -sS 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0xb8dc7e53946656efbce4c1dd71daeaab4ad4cab6' | gpg --dearmor | tee /etc/apt/keyrings/ppa_ondrej_php.gpg > /dev/null \
|
|
||||||
&& echo "deb [signed-by=/etc/apt/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu noble main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \
|
|
||||||
&& apt-get update \
|
|
||||||
&& apt-get install -y php8.4-cli php8.4-dev \
|
|
||||||
php8.4-pgsql php8.4-sqlite3 php8.4-gd \
|
|
||||||
php8.4-curl php8.4-mongodb \
|
|
||||||
php8.4-imap php8.4-mysql php8.4-mbstring \
|
|
||||||
php8.4-xml php8.4-zip php8.4-bcmath php8.4-soap \
|
|
||||||
php8.4-intl php8.4-readline \
|
|
||||||
php8.4-ldap \
|
|
||||||
php8.4-msgpack php8.4-igbinary php8.4-redis php8.4-swoole \
|
|
||||||
php8.4-memcached php8.4-pcov php8.4-imagick php8.4-xdebug \
|
|
||||||
&& curl -sLS https://getcomposer.org/installer | php -- --install-dir=/usr/bin/ --filename=composer \
|
|
||||||
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
|
|
||||||
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_VERSION.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \
|
|
||||||
&& apt-get update \
|
|
||||||
&& apt-get install -y nodejs \
|
|
||||||
&& npm install -g npm \
|
|
||||||
&& npm install -g pnpm \
|
|
||||||
&& npm install -g bun \
|
|
||||||
&& npx playwright install-deps \
|
|
||||||
&& curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /etc/apt/keyrings/yarn.gpg >/dev/null \
|
|
||||||
&& echo "deb [signed-by=/etc/apt/keyrings/yarn.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \
|
|
||||||
&& curl -sS https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /etc/apt/keyrings/pgdg.gpg >/dev/null \
|
|
||||||
&& echo "deb [signed-by=/etc/apt/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt noble-pgdg main" > /etc/apt/sources.list.d/pgdg.list \
|
|
||||||
&& apt-get update \
|
|
||||||
&& apt-get install -y yarn \
|
|
||||||
&& apt-get install -y $MYSQL_CLIENT \
|
|
||||||
&& apt-get install -y postgresql-client-$POSTGRES_VERSION \
|
|
||||||
&& apt-get -y autoremove \
|
|
||||||
&& apt-get clean \
|
|
||||||
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
|
||||||
|
|
||||||
RUN setcap "cap_net_bind_service=+ep" /usr/bin/php8.4
|
|
||||||
|
|
||||||
RUN userdel -r ubuntu
|
|
||||||
RUN groupadd --force -g $WWWGROUP sail
|
|
||||||
RUN useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail
|
|
||||||
RUN git config --global --add safe.directory /var/www/html
|
|
||||||
|
|
||||||
COPY start-container /usr/local/bin/start-container
|
|
||||||
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
|
||||||
COPY php.ini /etc/php/8.4/cli/conf.d/99-sail.ini
|
|
||||||
RUN chmod +x /usr/local/bin/start-container
|
|
||||||
|
|
||||||
EXPOSE 80/tcp
|
|
||||||
|
|
||||||
ENTRYPOINT ["start-container"]
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
[PHP]
|
|
||||||
post_max_size = 100M
|
|
||||||
upload_max_filesize = 100M
|
|
||||||
variables_order = EGPCS
|
|
||||||
pcov.directory = .
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
if [ "$SUPERVISOR_PHP_USER" != "root" ] && [ "$SUPERVISOR_PHP_USER" != "sail" ]; then
|
|
||||||
echo "You should set SUPERVISOR_PHP_USER to either 'sail' or 'root'."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -z "$WWWUSER" ]; then
|
|
||||||
usermod -u $WWWUSER sail
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -d /.composer ]; then
|
|
||||||
mkdir /.composer
|
|
||||||
fi
|
|
||||||
|
|
||||||
chmod -R ugo+rw /.composer
|
|
||||||
|
|
||||||
if [ $# -gt 0 ]; then
|
|
||||||
if [ "$SUPERVISOR_PHP_USER" = "root" ]; then
|
|
||||||
exec "$@"
|
|
||||||
else
|
|
||||||
exec gosu $WWWUSER "$@"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
|
|
||||||
fi
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
[supervisord]
|
|
||||||
nodaemon=true
|
|
||||||
user=root
|
|
||||||
logfile=/var/log/supervisor/supervisord.log
|
|
||||||
pidfile=/var/run/supervisord.pid
|
|
||||||
|
|
||||||
[program:php]
|
|
||||||
command=%(ENV_SUPERVISOR_PHP_COMMAND)s
|
|
||||||
user=%(ENV_SUPERVISOR_PHP_USER)s
|
|
||||||
environment=LARAVEL_SAIL="1"
|
|
||||||
stdout_logfile=/dev/stdout
|
|
||||||
stdout_logfile_maxbytes=0
|
|
||||||
stderr_logfile=/dev/stderr
|
|
||||||
stderr_logfile_maxbytes=0
|
|
||||||
@@ -1,474 +0,0 @@
|
|||||||
# Star Cloud API 分析與資料庫結構整理
|
|
||||||
|
|
||||||
> 基於 `docs/API/` 中 12 個 PDF 文件的完整分析,整理出 API 端點清單、資料流向及推導出的資料庫結構。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 一、API 端點總覽
|
|
||||||
|
|
||||||
| 代碼 | 名稱 | URL | Method | 說明 |
|
|
||||||
|------|------|-----|--------|------|
|
|
||||||
| B010 | 機台狀態上傳 & 指令撈回 | `/api/app/machine/status/{workid}` | POST | 機台定期心跳上報(狀態、溫度、門禁、版本),並撈回遠端指令。詳見 [b010_technical_spec.md](file:///home/mama/projects/star-cloud/docs/b010_technical_spec.md) |
|
|
||||||
| B017 | 遠端改庫存 | `/api/app/machine/reload_msg/{workid}` | POST | 機台撈取最新貨道庫存數量。詳見 [b017_technical_spec.md](file:///home/mama/projects/star-cloud/docs/b017_technical_spec.md) |
|
|
||||||
| B055 | 遠端出貨(取得指令) | `/api/app/machine/dispense/{workid}` | POST | 機台撈取遠端出貨指令列表。詳見 [b055_technical_spec.md](file:///home/mama/projects/star-cloud/docs/b055_technical_spec.md) |
|
|
||||||
| B055 | 遠端出貨(狀態回傳) | `/api/app/machine/dispense/{workid}` | PUT | 機台回報出貨結果(成功/失敗)及剩餘庫存。詳見 [b055_technical_spec.md](file:///home/mama/projects/star-cloud/docs/b055_technical_spec.md) |
|
|
||||||
| B220 | 零錢機庫存變動回傳 | `/api/app/coin/inventory/B220` | POST | 零錢機各面額庫存回傳(1/5/10/50/100/500/1000 元)。詳見 [b220_technical_spec.md](file:///home/mama/projects/star-cloud/docs/b220_technical_spec.md) |
|
|
||||||
| B600 | 消費金流回傳 | `/api/app/B600` | POST | 消費交易核心 API — 記錄金流類型、金額、商品、發票、會員等。詳見 [b600_technical_spec.md](file:///home/mama/projects/star-cloud/docs/b600_technical_spec.md) |
|
|
||||||
| B601 | 發票資訊回傳 | `/api/app/B601` | POST | 開立發票後回傳發票號碼、日期、隨機碼等。詳見 [b601_technical_spec.md](file:///home/mama/projects/star-cloud/docs/b601_technical_spec.md) |
|
|
||||||
| B602 | 出貨回傳 | `/api/app/B602` | POST | 出貨結果回傳(商品、貨道、金額、庫存、會員條碼等)。詳見 [b602_technical_spec.md](file:///home/mama/projects/star-cloud/docs/b602_technical_spec.md) |
|
|
||||||
| B650 | 驗證會員 | `/api/app/B650` | POST | 會員驗證(點數折抵、優惠券、取貨碼、線上訂單線下取貨等)。詳見 [b650_technical_spec.md](file:///home/mama/projects/star-cloud/docs/b650_technical_spec.md) |
|
|
||||||
| B710 | 計時器狀態回傳 | `/api/app/B710` | POST | 計時型機台的貨道使用狀態與倒數秒數回傳。詳見 [b710_technical_spec.md](file:///home/mama/projects/star-cloud/docs/b710_technical_spec.md) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 二、API 詳細分析
|
|
||||||
|
|
||||||
### 2.1 B010 — 機台狀態上傳 & 指令撈回
|
|
||||||
|
|
||||||
**用途**:機台定期向 Cloud 回報自身狀態,Cloud 透過 response 中的 `status` 碼下發遠端指令。
|
|
||||||
|
|
||||||
**Request 欄位**:
|
|
||||||
|
|
||||||
| 欄位 | 說明 |
|
|
||||||
|------|------|
|
|
||||||
| `key` | API 金鑰 (`KahnEwjhfDBHUYS7`) |
|
|
||||||
| `machine` | 機台序號 |
|
|
||||||
| `door` | 門禁狀態(非必填) |
|
|
||||||
| `temperature` | 溫度 |
|
|
||||||
| `M_Stus` | 機台型號(非必填) |
|
|
||||||
| `M_Stus2` | 當前頁面狀態碼 |
|
|
||||||
| `M_Ver` | 當前軟體版本 |
|
|
||||||
|
|
||||||
**頁面狀態碼 (M_Stus2)**:
|
|
||||||
- `0`: 離線, `1`: 主頁面, `2`: 販賣頁, `3`: 管理頁, `4`: 補貨頁, `5`: 教學頁
|
|
||||||
- `6`: 購買中, `7`: 鎖定頁, `60`: 出貨成功, `61`: 貨道測試, `62`: 付款選擇
|
|
||||||
- `63`: 等待付款, `64`: 出貨, `65`: 收據簽單, `66`: 通行碼, `67`: 取貨碼
|
|
||||||
- `68`: 訊息顯示, `69`: 取消購買, `610`: 購買結束, `611`: 來店禮, `612`: 出貨失敗
|
|
||||||
|
|
||||||
**遠端指令碼 (Response status)**:
|
|
||||||
- `49`: reload B017 (重新載入庫存), `50`: reload B005, `51`: reboot (重啟)
|
|
||||||
- `60`: reboot card machine, `61`: checkout, `70`: unlock, `71`: lock
|
|
||||||
- `72`: sellCode reload B023, `75`: exp reload B026, `79`: read B050
|
|
||||||
- `81`: sync timer status (B710), `85`: reload B055 (遠端出貨)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2.2 B600 — 消費金流回傳(核心交易 API)
|
|
||||||
|
|
||||||
**用途**:記錄每筆消費交易的完整資訊,為最核心的銷售數據 API。
|
|
||||||
|
|
||||||
**Request 欄位**:
|
|
||||||
|
|
||||||
| 欄位 | 參數名 | 說明 | 類型 |
|
|
||||||
|------|--------|------|------|
|
|
||||||
| req1 | key | API 金鑰 | String |
|
|
||||||
| req2 | machine | 機台編號 | String |
|
|
||||||
| req3 | payment_type | 金流類型代碼 | String |
|
|
||||||
| req4 | payment_request | 金流送出 data | String |
|
|
||||||
| req5 | payment_response | 金流回傳 data | String |
|
|
||||||
| req6 | product_id | 商品 ID | String |
|
|
||||||
| req7 | amount | 消費金額 | String |
|
|
||||||
| req8 | invoice_info | 發票歸戶資訊(統編-電話) | String |
|
|
||||||
| req9 | order_no | APP 定義訂單編號 | String |
|
|
||||||
| req10 | member_barcode | 掃會員得到的條碼 | String |
|
|
||||||
| req11 | machine_time | 機台時間 | String |
|
|
||||||
| req12 | status | 金流狀態 (0: 失敗, 1: 成功, 2: 50 結帳) | String |
|
|
||||||
| req13 | change_amount | 找零金額 | String |
|
|
||||||
| req14 | points_used | 使用的點數 | String |
|
|
||||||
| req15 | reserved | 系統保留 | String |
|
|
||||||
| req16 | items | 商品明細 `[{pid, amount, num}]` | JsonArray |
|
|
||||||
|
|
||||||
**金流類型 (req3) 完整對照**:
|
|
||||||
|
|
||||||
| 代碼 | 說明 |
|
|
||||||
|------|------|
|
|
||||||
| 1 | 信用卡 |
|
|
||||||
| 2 | 悠遊卡/一卡通 |
|
|
||||||
| 3 | 掃碼支付 |
|
|
||||||
| 4 | 手機支付 |
|
|
||||||
| 5 | 通行碼 |
|
|
||||||
| 6 | 取貨碼 |
|
|
||||||
| 7 | 來店禮 |
|
|
||||||
| 8 | 問卷 |
|
|
||||||
| 9 | 零錢 |
|
|
||||||
| 21-25 | 線下付款 + (1/2/3/4/9) |
|
|
||||||
| 30-34 | TapPay (Line/街口/悠遊付/Pi/全盈+) |
|
|
||||||
| 40 | 會員驗證取貨商品 |
|
|
||||||
| 41 | 線上訂單線下結帳 |
|
|
||||||
| 50-54 | 線下付款 + TapPay 系列 |
|
|
||||||
| 60 | 點數/優惠券全額折抵 |
|
|
||||||
| 61-64, 69 | 會員 + (1/2/3/4/9) |
|
|
||||||
| 70 | 官方 LinePay |
|
|
||||||
| 90-94 | 會員 + TapPay 系列 |
|
|
||||||
| 101-109 | 大豐環保點數 + 消費 |
|
|
||||||
| 110-114, 120 | 大豐環保 + TapPay/LinePay |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2.3 B650 — 驗證會員
|
|
||||||
|
|
||||||
**驗證類型 (req6)**:
|
|
||||||
|
|
||||||
| req6 | 說明 | Response 特殊欄位 |
|
|
||||||
|------|------|--------------------|
|
|
||||||
| 0 | 有點擊商品的驗證 | `userToken`, `amount`, `point`, `respondMSG` |
|
|
||||||
| 1 | 會員純驗證 | 基本驗證回應 |
|
|
||||||
| 2 | 會員兌換商品列表 | `productList[{pId, pName, token, validityPeriod, num}]` |
|
|
||||||
| 3 | 線上訂單線下結帳 | `orderUrl`, `orderNumber`, `orderAmount` |
|
|
||||||
| 4 | 會員贈品 | — |
|
|
||||||
|
|
||||||
**Response 狀態碼**:
|
|
||||||
- `100`: 成功 — 無可用點數
|
|
||||||
- `101`: 成功 — 全額折抵(點數或優惠券)
|
|
||||||
- `102`: 成功 — 部分折抵
|
|
||||||
- `103`: 取貨碼驗證成功
|
|
||||||
- `104`: 線上訂單驗證成功
|
|
||||||
- `21`: Key Error, `22`: Time Error, `23`: User Token Error
|
|
||||||
- `25`: 會員平台錯誤, `30`: 會員正在其他機台使用中
|
|
||||||
- `35`: 無效優惠券, `40`: 已兌換過, `41`: 無效訂單, `42`: 取貨碼錯誤
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 三、資料庫結構推導
|
|
||||||
|
|
||||||
根據以上 API 的欄位分析,以下是系統需要的**完整資料庫結構**:
|
|
||||||
|
|
||||||
### 🟢 已存在的資料表(比對現有 migration)
|
|
||||||
|
|
||||||
| 資料表 | 說明 | 對應 API |
|
|
||||||
|--------|------|----------|
|
|
||||||
| `users` | 後台管理員 | — |
|
|
||||||
| `machines` | 機台基本資料 | B010, B017, B055, B220, B600, B602 |
|
|
||||||
| `machine_logs` | 機台日誌 | B010 |
|
|
||||||
| `app_configs` | APP 設定 | — |
|
|
||||||
| `members` | 會員資料 | B650 |
|
|
||||||
| `social_accounts` | 社群帳號 | B650 |
|
|
||||||
| `member_wallets` | 會員錢包 | B650 |
|
|
||||||
| `wallet_transactions` | 錢包交易 | B600, B650 |
|
|
||||||
| `member_points` | 點數帳戶 | B650 |
|
|
||||||
| `point_transactions` | 點數異動 | B650, B600 |
|
|
||||||
| `point_rules` | 點數規則 | B650 |
|
|
||||||
| `membership_tiers` | 會員等級 | — |
|
|
||||||
| `member_memberships` | 會員等級紀錄 | — |
|
|
||||||
| `gift_definitions` | 禮品定義 | — |
|
|
||||||
| `member_gifts` | 禮品發放 | — |
|
|
||||||
| `deposit_bonus_rules` | 儲值回饋規則 | — |
|
|
||||||
|
|
||||||
### 🔴 需新增的資料表
|
|
||||||
|
|
||||||
以下是從 API 欄位推導出**目前資料庫中缺少**,但系統運作所必需的資料表:
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 1. `products` — 商品資料
|
|
||||||
|
|
||||||
> 來源:B600 (req6, req16), B602 (req5), B650 (productList), B710 (pid)
|
|
||||||
|
|
||||||
```
|
|
||||||
products
|
|
||||||
├── id BIGINT PK
|
|
||||||
├── name VARCHAR 商品名稱
|
|
||||||
├── price DECIMAL(10,2) 售價
|
|
||||||
├── cost DECIMAL(10,2) 成本(選填)
|
|
||||||
├── category_id BIGINT FK 分類(選填)
|
|
||||||
├── image VARCHAR 商品圖片 URL
|
|
||||||
├── barcode VARCHAR 商品條碼
|
|
||||||
├── description TEXT 商品描述
|
|
||||||
├── is_active BOOLEAN 是否上架
|
|
||||||
├── created_at TIMESTAMP
|
|
||||||
└── updated_at TIMESTAMP
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 2. `machine_slots` — 機台貨道
|
|
||||||
|
|
||||||
> 來源:B017 (tid, num), B055 (res2), B602 (req6, req9), B710 (cid)
|
|
||||||
|
|
||||||
```
|
|
||||||
machine_slots
|
|
||||||
├── id BIGINT PK
|
|
||||||
├── machine_id BIGINT FK → machines
|
|
||||||
├── slot_no VARCHAR 貨道編號 (tid/cid)
|
|
||||||
├── product_id BIGINT FK → products(可空)
|
|
||||||
├── stock INT 當前庫存數量
|
|
||||||
├── max_stock INT 最大容量
|
|
||||||
├── is_active BOOLEAN 是否啟用
|
|
||||||
├── created_at TIMESTAMP
|
|
||||||
└── updated_at TIMESTAMP
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 3. `orders` — 訂單(消費紀錄主表)
|
|
||||||
|
|
||||||
> 來源:B600 (核心), B601, B602
|
|
||||||
|
|
||||||
```
|
|
||||||
orders
|
|
||||||
├── id BIGINT PK
|
|
||||||
├── order_no VARCHAR UNIQUE APP 定義訂單編號 (req9)
|
|
||||||
├── machine_id BIGINT FK → machines (req2)
|
|
||||||
├── member_id BIGINT FK → members(可空)
|
|
||||||
├── payment_type TINYINT 金流類型代碼 (req3)
|
|
||||||
├── total_amount DECIMAL(10,2) 消費金額 (req7)
|
|
||||||
├── change_amount DECIMAL(10,2) 找零金額 (req13)
|
|
||||||
├── points_used INT 使用的點數 (req14)
|
|
||||||
├── discount_amount DECIMAL(10,2) 折扣前金額 (req15)
|
|
||||||
├── payment_status TINYINT 金流狀態 (req12: 0失敗/1成功/2:50結帳)
|
|
||||||
├── payment_request TEXT 金流送出 data (req4)
|
|
||||||
├── payment_response TEXT 金流回傳 data (req5)
|
|
||||||
├── member_barcode VARCHAR 會員條碼 (req10)
|
|
||||||
├── invoice_info VARCHAR 發票歸戶資訊 (req8)
|
|
||||||
├── machine_time DATETIME 機台時間 (req11)
|
|
||||||
├── flow_id VARCHAR Cloud 回傳的金流 ID (B600 response message)
|
|
||||||
├── created_at TIMESTAMP
|
|
||||||
└── updated_at TIMESTAMP
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 4. `order_items` — 訂單明細
|
|
||||||
|
|
||||||
> 來源:B600 (req16: `[{pid, amount, num}]`)
|
|
||||||
|
|
||||||
```
|
|
||||||
order_items
|
|
||||||
├── id BIGINT PK
|
|
||||||
├── order_id BIGINT FK → orders
|
|
||||||
├── product_id BIGINT FK → products (pid)
|
|
||||||
├── quantity INT 數量 (num)
|
|
||||||
├── unit_price DECIMAL(10,2) 單價 (amount)
|
|
||||||
├── subtotal DECIMAL(10,2) 小計
|
|
||||||
├── created_at TIMESTAMP
|
|
||||||
└── updated_at TIMESTAMP
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 5. `invoices` — 發票紀錄
|
|
||||||
|
|
||||||
> 來源:B601
|
|
||||||
|
|
||||||
```
|
|
||||||
invoices
|
|
||||||
├── id BIGINT PK
|
|
||||||
├── order_id BIGINT FK → orders(透過 flow_id 關聯)
|
|
||||||
├── flow_id VARCHAR B600 回傳的金流 ID (req3)
|
|
||||||
├── machine_id BIGINT FK → machines (req2)
|
|
||||||
├── rtn_code VARCHAR 發票回傳碼 (req4)
|
|
||||||
├── rtn_msg VARCHAR 發票回傳訊息 (req5)
|
|
||||||
├── invoice_no VARCHAR 發票號碼 (req6)
|
|
||||||
├── invoice_date DATE 發票日期 (req7)
|
|
||||||
├── random_number VARCHAR 隨機碼 (req8)
|
|
||||||
├── love_code VARCHAR 愛心碼 (req9)
|
|
||||||
├── business_tax_id VARCHAR 公司統編 (新增)
|
|
||||||
├── carrier_id VARCHAR 載具編號 (新增)
|
|
||||||
├── carrier_type VARCHAR 載具類型 (新增)
|
|
||||||
├── status TINYINT 發票狀態 (1:有效, 0:作廢)
|
|
||||||
├── created_at TIMESTAMP
|
|
||||||
└── updated_at TIMESTAMP
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 6. `dispense_records` — 出貨紀錄
|
|
||||||
|
|
||||||
> 來源:B602, B055
|
|
||||||
|
|
||||||
```
|
|
||||||
dispense_records
|
|
||||||
├── id BIGINT PK
|
|
||||||
├── order_id BIGINT FK → orders(透過 flow_id 關聯)
|
|
||||||
├── flow_id VARCHAR 金流 ID (req3)
|
|
||||||
├── machine_id BIGINT FK → machines (req2)
|
|
||||||
├── product_id BIGINT FK → products (req5)
|
|
||||||
├── slot_no VARCHAR 貨道編號 (req6)
|
|
||||||
├── amount DECIMAL(10,2) 消費金額 (req7)
|
|
||||||
├── remaining_stock INT 貨道剩餘庫存 (req9)
|
|
||||||
├── member_barcode VARCHAR 會員條碼 (req10)
|
|
||||||
├── machine_time DATETIME 機台時間 (req11)
|
|
||||||
├── coupon_order_no VARCHAR 商品券訂單編號 (req12)
|
|
||||||
├── dispense_status TINYINT 出貨狀態 (req13: 0成功/1失敗)
|
|
||||||
├── original_price DECIMAL(10,2) 折扣前金額 (req15)
|
|
||||||
├── points_used INT 使用的點數 (req16)
|
|
||||||
├── created_at TIMESTAMP
|
|
||||||
└── updated_at TIMESTAMP
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 7. `remote_dispense_commands` — 遠端出貨指令
|
|
||||||
|
|
||||||
> 來源:B055
|
|
||||||
|
|
||||||
```
|
|
||||||
remote_dispense_commands
|
|
||||||
├── id BIGINT PK
|
|
||||||
├── machine_id BIGINT FK → machines
|
|
||||||
├── command_id VARCHAR 執行命令 ID (res1)
|
|
||||||
├── slot_no VARCHAR 貨道 ID (res2)
|
|
||||||
├── status TINYINT 狀態 (0: 待執行, 1: 出貨成功, 2: 出貨失敗)
|
|
||||||
├── remaining_stock INT 出貨後剩餘庫存
|
|
||||||
├── created_at TIMESTAMP
|
|
||||||
└── updated_at TIMESTAMP
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 8. `coin_inventory_logs` — 零錢機庫存紀錄
|
|
||||||
|
|
||||||
> 來源:B220
|
|
||||||
|
|
||||||
```
|
|
||||||
coin_inventory_logs
|
|
||||||
├── id BIGINT PK
|
|
||||||
├── machine_id BIGINT FK → machines
|
|
||||||
├── value_1 INT 1 元庫存
|
|
||||||
├── value_5 INT 5 元庫存
|
|
||||||
├── value_10 INT 10 元庫存
|
|
||||||
├── value_50 INT 50 元庫存
|
|
||||||
├── value_100 INT 100 元庫存
|
|
||||||
├── value_500 INT 500 元庫存
|
|
||||||
├── value_1000 INT 1000 元庫存
|
|
||||||
├── account VARCHAR 操作人帳號 (0=消費者)
|
|
||||||
├── created_at TIMESTAMP
|
|
||||||
└── updated_at TIMESTAMP
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 9. `timer_statuses` — 計時器狀態
|
|
||||||
|
|
||||||
> 來源:B710
|
|
||||||
|
|
||||||
```
|
|
||||||
timer_statuses
|
|
||||||
├── id BIGINT PK
|
|
||||||
├── machine_id BIGINT FK → machines
|
|
||||||
├── slot_no VARCHAR 貨道 ID (cid)
|
|
||||||
├── product_id BIGINT FK → products (pid, 0=未設定)
|
|
||||||
├── status TINYINT 狀態 (0: 未啟用, 1: 使用中, 2: 異常)
|
|
||||||
├── remaining_seconds INT 剩餘秒數 (num)
|
|
||||||
├── created_at TIMESTAMP
|
|
||||||
└── updated_at TIMESTAMP
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 10. `payment_types` — 金流類型參照表(建議)
|
|
||||||
|
|
||||||
> 來源:B600 req3 的龐大對照表
|
|
||||||
|
|
||||||
```
|
|
||||||
payment_types
|
|
||||||
├── id BIGINT PK
|
|
||||||
├── code INT UNIQUE 金流類型代碼
|
|
||||||
├── name VARCHAR 名稱(中文)
|
|
||||||
├── category VARCHAR 分類(信用卡/電子票證/行動支付/TapPay/會員/環保點數...)
|
|
||||||
├── is_active BOOLEAN 是否啟用
|
|
||||||
├── created_at TIMESTAMP
|
|
||||||
└── updated_at TIMESTAMP
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 四、ER 關係圖
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
erDiagram
|
|
||||||
machines ||--o{ machine_slots : "擁有"
|
|
||||||
machines ||--o{ machine_logs : "產生"
|
|
||||||
machines ||--o{ orders : "交易於"
|
|
||||||
machines ||--o{ coin_inventory_logs : "零錢記錄"
|
|
||||||
machines ||--o{ timer_statuses : "計時器"
|
|
||||||
machines ||--o{ remote_dispense_commands : "遠端指令"
|
|
||||||
|
|
||||||
products ||--o{ machine_slots : "放置於"
|
|
||||||
products ||--o{ order_items : "被購買"
|
|
||||||
products ||--o{ timer_statuses : "計時商品"
|
|
||||||
|
|
||||||
orders ||--o{ order_items : "包含"
|
|
||||||
orders ||--o| invoices : "開立"
|
|
||||||
orders ||--o{ dispense_records : "出貨"
|
|
||||||
orders }o--o| members : "消費者"
|
|
||||||
orders }o--|| payment_types : "付款方式"
|
|
||||||
|
|
||||||
members ||--o{ social_accounts : "社群綁定"
|
|
||||||
members ||--o| member_wallets : "錢包"
|
|
||||||
members ||--o| member_points : "點數"
|
|
||||||
members ||--o{ member_memberships : "會員等級"
|
|
||||||
members ||--o{ member_gifts : "禮品"
|
|
||||||
|
|
||||||
member_wallets ||--o{ wallet_transactions : "異動"
|
|
||||||
member_points ||--o{ point_transactions : "異動"
|
|
||||||
membership_tiers ||--o{ member_memberships : "對應等級"
|
|
||||||
gift_definitions ||--o{ member_gifts : "對應禮品"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 五、API 資料流向圖
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────┐
|
|
||||||
│ 機台 (APP) │
|
|
||||||
└──────┬──────┘
|
|
||||||
│
|
|
||||||
├── B010 ──► 心跳上報 ──► machine_logs(狀態/溫度/版本)
|
|
||||||
│ ◄── 返回遠端指令碼
|
|
||||||
│
|
|
||||||
├── B017 ──► 撈取庫存 ──► machine_slots(最新貨道數量)
|
|
||||||
│
|
|
||||||
├── B055 ──► 撈取出貨指令 ──► remote_dispense_commands
|
|
||||||
│ PUT ──► 回報出貨結果 ──► 更新 command status + 庫存
|
|
||||||
│
|
|
||||||
├── B220 ──► 零錢機庫存 ──► coin_inventory_logs
|
|
||||||
│
|
|
||||||
├── B600 ──► 消費金流 ──► orders + order_items(核心交易)
|
|
||||||
│ ◄── 返回金流 ID (flow_id)
|
|
||||||
│
|
|
||||||
├── B601 ──► 發票回傳 ──► invoices(關聯 flow_id)
|
|
||||||
│
|
|
||||||
├── B602 ──► 出貨回傳 ──► dispense_records(關聯 flow_id)
|
|
||||||
│
|
|
||||||
├── B650 ──► 驗證會員 ──► members + member_points
|
|
||||||
│ ◄── 點數折抵/優惠券/取貨碼/線上訂單
|
|
||||||
│
|
|
||||||
└── B710 ──► 計時器狀態 ──► timer_statuses
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 六、重要觀察與建議
|
|
||||||
|
|
||||||
### 6.1 API 金鑰
|
|
||||||
- 所有 API 使用同一把硬編碼金鑰 `KahnEwjhfDBHUYS7`
|
|
||||||
- **建議**:遷移到 Star Cloud 後改為每台機台獨立的 API Token,使用 Laravel Sanctum 管理
|
|
||||||
|
|
||||||
### 6.2 資料一致性
|
|
||||||
- B600(金流)→ B601(發票)→ B602(出貨)三支 API 透過 `flow_id` 串聯
|
|
||||||
- 建議資料庫設計中以 `orders.flow_id` 作為三者的關聯基準
|
|
||||||
|
|
||||||
### 6.3 高頻 API
|
|
||||||
- **B010** 為最高頻率 API(每台機台數秒呼叫一次),必須走 **Redis Queue 異步寫入**
|
|
||||||
- B220(零錢機庫存)與 B710(計時器)也屬於高頻回報,建議異步處理
|
|
||||||
|
|
||||||
### 6.4 新舊版差異
|
|
||||||
- B220 和 B600/B602 各有兩個版本的 PDF(新舊版),新版增加了更多面額(500/1000 元)和 TapPay 金流類型
|
|
||||||
- 建議以**較新版本 (帶日期後綴的)** 為準進行開發
|
|
||||||
|
|
||||||
### 6.5 `machines` 表建議補充欄位
|
|
||||||
|
|
||||||
根據 B010 API,`machines` 表應包含:
|
|
||||||
|
|
||||||
| 欄位 | 說明 | 來源 |
|
|
||||||
|------|------|------|
|
|
||||||
| `serial_no` | 機台序號 | B010 machine |
|
|
||||||
| `model` | 機台型號 | B010 M_Stus |
|
|
||||||
| `current_status` | 當前頁面狀態碼 | B010 M_Stus2 |
|
|
||||||
| `current_version` | 當前軟體版本 | B010 M_Ver |
|
|
||||||
| `temperature` | 最新溫度 | B010 temperature |
|
|
||||||
| `door_status` | 門禁狀態 | B010 door |
|
|
||||||
| `last_heartbeat_at` | 最後心跳時間 | B010 呼叫時間 |
|
|
||||||
| `is_online` | 是否在線 | 由心跳超時判斷 |
|
|
||||||
@@ -1,627 +0,0 @@
|
|||||||
# Star Cloud — 智能販賣機管理平台技術架構規劃
|
|
||||||
|
|
||||||
> **目標**:為上千至上萬台智能販賣機建構集中式管理後台,承接所有機台透過 API 回寫的資料,並提供完整的營運管理介面。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 一、系統架構全景
|
|
||||||
|
|
||||||
```
|
|
||||||
┌──────────────────────────────────────────────┐
|
|
||||||
│ Star Cloud 管理後台 │
|
|
||||||
│ (Laravel 12 + Blade + Tailwind) │
|
|
||||||
│ ┌──────────┐ ┌──────────┐ ┌───────────┐ │
|
|
||||||
│ │ 儀表板 │ │ 機台管理 │ │ 銷售分析 │ │
|
|
||||||
│ │ Dashboard │ │ Machines │ │ Analytics │ │
|
|
||||||
│ └──────────┘ └──────────┘ └───────────┘ │
|
|
||||||
│ ┌──────────┐ ┌──────────┐ ┌───────────┐ │
|
|
||||||
│ │ 倉庫管理 │ │ 會員系統 │ │ 遠端操控 │ │
|
|
||||||
│ │Warehouses │ │ Members │ │ Remote │ │
|
|
||||||
│ └──────────┘ └──────────┘ └───────────┘ │
|
|
||||||
└──────────┬───────────────┬───────────────────┘
|
|
||||||
│ Web Routes │
|
|
||||||
│ (Blade SSR) │
|
|
||||||
┌──────────┴───────────────┴───────────────────┐
|
|
||||||
│ Laravel Application │
|
|
||||||
│ │
|
|
||||||
│ Controllers → Services → Models → MySQL │
|
|
||||||
│ │
|
|
||||||
└──────┬────────────────────┬──────────────────┘
|
|
||||||
│ │
|
|
||||||
┌──────────┴────┐ ┌───────┴───────┐
|
|
||||||
│ API Routes │ │ Redis Queue │
|
|
||||||
│ /api/v1/app │ │ (異步處理) │
|
|
||||||
└──────┬────────┘ └───────┬───────┘
|
|
||||||
│ │
|
|
||||||
┌──────┴────────┐ ┌───────┴───────┐
|
|
||||||
│ API Controller│──────►│ Jobs │
|
|
||||||
│ (驗證 + 分派) │ dispatch│ (Worker 執行) │
|
|
||||||
└───────────────┘ └───────┬───────┘
|
|
||||||
▲ │
|
|
||||||
┌────────────────┤ ▼
|
|
||||||
│ │ ┌──────────────┐
|
|
||||||
┌─────┴─────┐ ┌────┴─────┐ │ Services │
|
|
||||||
│ 販賣機 #1 │ │販賣機 #N │ │ (業務邏輯) │
|
|
||||||
│ B010 │ │ B600 │ └──────┬───────┘
|
|
||||||
│ B600 │ │ B602 │ │
|
|
||||||
│ B602 │ │ ... │ ▼
|
|
||||||
└────────────┘ └──────────┘ ┌──────────────┐
|
|
||||||
│ MySQL │
|
|
||||||
│ (持久化) │
|
|
||||||
└──────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 二、技術棧確認
|
|
||||||
|
|
||||||
| 層級 | 技術 | 說明 |
|
|
||||||
|------|------|------|
|
|
||||||
| **後端框架** | PHP 8.5 / Laravel 12 | 單體架構,Blade SSR |
|
|
||||||
| **資料庫** | MySQL 8.0 | 資料持久化 |
|
|
||||||
| **快取/隊列** | Redis | B010 心跳、B600 金流等高頻 API 必須異步 |
|
|
||||||
| **前端視圖** | Blade + Tailwind CSS + Preline UI | 極簡奢華風 |
|
|
||||||
| **前端互動** | Alpine.js | 輕量 DOM 互動 |
|
|
||||||
| **建置工具** | Vite | 前端資源編譯 |
|
|
||||||
| **開發環境** | Laravel Sail (Docker) | 本地開發 |
|
|
||||||
| **IoT 通訊** | Redis Queue + Jobs | 異步寫入,避免 API 直連 DB |
|
|
||||||
| **認證** | Laravel Sanctum | 後台用 Session,API 用 Token |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 三、多租戶與權限架構 (Multi-Tenant RBAC)
|
|
||||||
|
|
||||||
為支援系統管理員(內部營運)與租戶(客戶公司及子帳號)的雙層管理模式,平台採用以下三層級權限隔離架構:
|
|
||||||
|
|
||||||
### 3.1 租戶隔離層 (Tenant Isolation)
|
|
||||||
* **核心機制**:導入 `companies` 資料表作為租戶實體,所有歸屬於客戶的資源(`users`, `machines` 等)均需綁定 `company_id`。
|
|
||||||
* **身份判定**:
|
|
||||||
* **系統管理員 (內部)**:`company_id = null`,具備跨租戶檢視與管理機台、設定全局參數的最高權限。
|
|
||||||
* **客戶端帳號 (租戶)**:`company_id` 有值,所有查詢(透過 Eloquent Global Scopes 或 Middleware)自動注入 `where('company_id', $user->company_id)`,達成資料物理隔離。
|
|
||||||
|
|
||||||
### 3.2 角色與權限層 (Roles & Permissions)
|
|
||||||
* 採用 `spatie/laravel-permission` 套件並進行多租戶改造。
|
|
||||||
* **多租戶角色 (Tenant-Aware Roles)**:擴充 `roles` 表,加入 `company_id` 欄位。
|
|
||||||
* **系統預設角色** (`company_id = null`):如 Super Admin, System Support。
|
|
||||||
* **客戶自建角色** (`company_id = N`):如客戶建立的「分區維修員」、「會計」,確保客戶只能看見與分配屬於自己公司的角色給員工。
|
|
||||||
* **權限 (Permissions)**:定義系統最小操作單位(如 `machine.view`, `user.create`),供角色綁定。
|
|
||||||
|
|
||||||
### 3.3 流程控制與防禦 (Flow Control)
|
|
||||||
* **介面隔離**:依據身份動態隱藏/顯示系統級選單(例如「全站設定」、「合約管理」等多租戶不可見的功能)。
|
|
||||||
* **越權防禦**:客戶創建子帳號時,後端必定強制攔截並覆寫 `user->company_id = Auth::user()->company_id`,確保子帳號牢牢綁在自己的公司名下。同時限制其只能撈與分配自己公司名下的 Role。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 四、資料庫完整設計
|
|
||||||
|
|
||||||
### 4.1 資料表總覽
|
|
||||||
|
|
||||||
分為 **6 大領域**,共 **28 張表**:
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph LR
|
|
||||||
subgraph 系統管理
|
|
||||||
A1[users] --> A2[sessions]
|
|
||||||
A1 --> A3[personal_access_tokens]
|
|
||||||
A4[failed_jobs]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph 機台領域
|
|
||||||
B1[machines] --> B2[machine_slots]
|
|
||||||
B1 --> B3[machine_logs]
|
|
||||||
B1 --> B4[coin_inventories]
|
|
||||||
B1 --> B5[timer_statuses]
|
|
||||||
B1 --> B6[remote_commands]
|
|
||||||
B1 --> B7[machine_heartbeats]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph 商品領域
|
|
||||||
C1[products]
|
|
||||||
C2[product_categories]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph 交易領域
|
|
||||||
D1[orders] --> D2[order_items]
|
|
||||||
D1 --> D3[invoices]
|
|
||||||
D1 --> D4[dispense_records]
|
|
||||||
D5[payment_types]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph 會員領域
|
|
||||||
E1[members] --> E2[social_accounts]
|
|
||||||
E1 --> E3[member_wallets]
|
|
||||||
E3 --> E4[wallet_transactions]
|
|
||||||
E1 --> E5[member_points]
|
|
||||||
E5 --> E6[point_transactions]
|
|
||||||
E7[point_rules]
|
|
||||||
E8[deposit_bonus_rules]
|
|
||||||
E1 --> E9[membership_tiers]
|
|
||||||
E1 --> E10[member_memberships]
|
|
||||||
E1 --> E11[gift_definitions]
|
|
||||||
E1 --> E12[member_gifts]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph APP設定
|
|
||||||
F1[app_configs]
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.2 需新增的資料表(Migration 設計)
|
|
||||||
|
|
||||||
> 以下為 API 分析後推導出的缺失表,搭配各 API 欄位產生對應。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 👑 多租戶與權限基礎表 (RBAC & Tenant)
|
|
||||||
|
|
||||||
##### 🆕 `companies` — 租戶/客戶主表
|
|
||||||
所有客戶端的資料邊界,用來實現資料物理隔離。
|
|
||||||
|
|
||||||
| 欄位 | 類型 | 說明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `id` | BIGINT PK | — |
|
|
||||||
| `name` | VARCHAR(255) | 公司/組織名稱 |
|
|
||||||
| `tax_id` | VARCHAR(20) NULL | 統一編號 |
|
|
||||||
| `contact_name` | VARCHAR(100) NULL | 聯絡人姓名 |
|
|
||||||
| `contact_phone` | VARCHAR(50) NULL | 聯絡電話 |
|
|
||||||
| `status` | TINYINT DEFAULT 1 | 狀態 (1:啟用, 0:停用) |
|
|
||||||
| `valid_until` | DATE NULL | 合約使用期限 |
|
|
||||||
| `timestamps` | — | — |
|
|
||||||
| `deleted_at` | TIMESTAMP NULL | 軟刪除 (SoftDeletes) |
|
|
||||||
|
|
||||||
##### 🟡 `users` 表 — 需擴充欄位
|
|
||||||
將 Laravel 預設的使用者表與租戶綁定。
|
|
||||||
|
|
||||||
| 新增欄位 | 類型 | 說明 |
|
|
||||||
|----------|------|------|
|
|
||||||
| `company_id` | BIGINT FK NULL | → `companies` (NULL 代表系統管理員) |
|
|
||||||
| `status` | TINYINT DEFAULT 1 | 帳號狀態 (1:啟用, 0:停用) |
|
|
||||||
|
|
||||||
##### 🟡 `roles` 表 (Spatie 權限擴充)
|
|
||||||
Spatie 預設的 roles 表必須加上多租戶設計,確保留戶只能管理自己的角色。
|
|
||||||
|
|
||||||
| 新增/調整欄位 | 類型 | 說明 |
|
|
||||||
|----------|------|------|
|
|
||||||
| `company_id` | BIGINT FK NULL | → `companies` (NULL 代表系統全域角色) |
|
|
||||||
| **UNIQUE** | `(name, guard_name, company_id)` | 原套件無 `company_id`,需修改為複合唯一鍵 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 🟡 `machines` 表 — 需擴充欄位
|
|
||||||
|
|
||||||
現有 `machines` 表僅有基礎欄位,需為 B010 API 補充:
|
|
||||||
|
|
||||||
| 新增欄位 | 類型 | 說明 | 來源 API |
|
|
||||||
|----------|------|------|----------|
|
|
||||||
| `serial_no` | `VARCHAR UNIQUE` | 機台序號(API 用此識別) | B010 `machine` |
|
|
||||||
| `model` | `VARCHAR` | 機台型號 | B010 `M_Stus` |
|
|
||||||
| `current_page` | `TINYINT` | 當前頁面狀態碼 | B010 `M_Stus2` |
|
|
||||||
| `door_status` | `VARCHAR` | 門禁狀態 | B010 `door` |
|
|
||||||
| `is_online` | `BOOLEAN` | 是否在線(心跳超時判斷) | 計算欄位 |
|
|
||||||
| `api_token` | `VARCHAR` | 機台專屬 API Token | 取代硬編碼 key |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 🆕 `products` — 商品資料
|
|
||||||
|
|
||||||
| 欄位 | 類型 | 說明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `id` | BIGINT PK | — |
|
|
||||||
| `name` | VARCHAR(255) | 商品名稱 (預設語系) |
|
|
||||||
| `name_dictionary_key` | VARCHAR(100) NULL | 多語系字典鍵值 (對應 translations 表) |
|
|
||||||
| `sku` | VARCHAR(100) UNIQUE | 商品編號 |
|
|
||||||
| `price` | DECIMAL(10,2) | 售價 |
|
|
||||||
| `cost` | DECIMAL(10,2) NULL | 成本 |
|
|
||||||
| `category_id` | BIGINT FK NULL | → product_categories |
|
|
||||||
| `image` | VARCHAR NULL | 圖片 URL |
|
|
||||||
| `barcode` | VARCHAR NULL | 條碼 |
|
|
||||||
| `is_timer_product` | BOOLEAN | 是否為計時型商品 |
|
|
||||||
| `is_active` | BOOLEAN | 是否上架 |
|
|
||||||
| `timestamps` | — | — |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 🌐 多語系支援表 (i18n)
|
|
||||||
|
|
||||||
為了支援後台介面與 APP 端顯示的多語系內容(如商品名稱、分類名稱、系統提示),統一採用字典表架構。
|
|
||||||
|
|
||||||
##### 🆕 `translations` — 多語系字典表
|
|
||||||
|
|
||||||
| 欄位 | 類型 | 說明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `id` | BIGINT PK | — |
|
|
||||||
| `group` | VARCHAR(50) | 分組 (例如: `product`, `category`, `system`) |
|
|
||||||
| `key` | VARCHAR(100) | 字典鍵值 (例如: `prod_name_001`) |
|
|
||||||
| `locale` | VARCHAR(10) | 語系代碼 (例如: `zh_TW`, `en_US`, `ja_JP`) |
|
|
||||||
| `value` | TEXT | 翻譯內容 |
|
|
||||||
| `timestamps` | — | — |
|
|
||||||
| **UNIQUE** | `(group, key, locale)` | 確保同一組鍵值在同一語系下唯一 |
|
|
||||||
|
|
||||||
> **多語系實作策略**:
|
|
||||||
> 1. **靜態文案**:後台介面的靜態文字直接使用 Laravel 內建的 `lang/` 目錄 (JSON 或 PHP 陣列)。
|
|
||||||
> 2. **動態資料**:資料庫內容(如商品名稱 `products.name`)保留預設語系供後台快速搜尋。同時新增 `name_dictionary_key`。當對接外部 APP 或前台需要多語系時,透過 API 關聯 `translations` 表,回傳當前語系對應的 `value`。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 🆕 `machine_slots` — 機台貨道
|
|
||||||
|
|
||||||
| 欄位 | 類型 | 說明 | 來源 |
|
|
||||||
|------|------|------|------|
|
|
||||||
| `id` | BIGINT PK | — | — |
|
|
||||||
| `machine_id` | BIGINT FK | → machines | B017, B055, B710 |
|
|
||||||
| `slot_no` | VARCHAR(20) | 貨道編號 | B017 `tid`, B710 `cid` |
|
|
||||||
| `product_id` | BIGINT FK NULL | → products | B710 `pid` |
|
|
||||||
| `stock` | INT DEFAULT 0 | 當前庫存 | B017 `num` |
|
|
||||||
| `max_stock` | INT DEFAULT 0 | 最大容量 | — |
|
|
||||||
| `is_active` | BOOLEAN | 是否啟用 | — |
|
|
||||||
| `timestamps` | — | — | — |
|
|
||||||
| **UNIQUE** | `(machine_id, slot_no)` | 複合唯一鍵 | — |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 🆕 `orders` — 訂單主表
|
|
||||||
|
|
||||||
| 欄位 | 類型 | 說明 | 來源 |
|
|
||||||
|------|------|------|------|
|
|
||||||
| `id` | BIGINT PK | — | — |
|
|
||||||
| `flow_id` | VARCHAR UNIQUE | Cloud 金流 ID | B600 response |
|
|
||||||
| `order_no` | VARCHAR | APP 訂單號 | B600 `req9` |
|
|
||||||
| `machine_id` | BIGINT FK | → machines | B600 `req2` |
|
|
||||||
| `member_id` | BIGINT FK NULL | → members | B650 關聯 |
|
|
||||||
| `payment_type` | SMALLINT | 金流類型碼 | B600 `req3` |
|
|
||||||
| `total_amount` | DECIMAL(10,2) | 消費金額 | B600 `req7` |
|
|
||||||
| `change_amount` | DECIMAL(10,2) | 找零 | B600 `req13` |
|
|
||||||
| `points_used` | INT DEFAULT 0 | 使用點數 | B600 `req14` |
|
|
||||||
| `original_amount` | DECIMAL(10,2) NULL | 折扣前金額 | B602 `req15` |
|
|
||||||
| `payment_status` | TINYINT | 0:失敗/1:成功 | B600 `req12` |
|
|
||||||
| `payment_request` | TEXT NULL | 金流送出 data | B600 `req4` |
|
|
||||||
| `payment_response` | TEXT NULL | 金流回傳 data | B600 `req5` |
|
|
||||||
| `member_barcode` | VARCHAR NULL | 會員條碼 | B600 `req10` |
|
|
||||||
| `invoice_info` | VARCHAR NULL | 發票歸戶 | B600 `req8` |
|
|
||||||
| `machine_time` | DATETIME NULL | 機台時間 | B600 `req11` |
|
|
||||||
| `timestamps` | — | — | — |
|
|
||||||
| **INDEX** | `(machine_id, created_at)` | 查詢最佳化 | — |
|
|
||||||
| **INDEX** | `(payment_type)` | 金流類型篩選 | — |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 🆕 `order_items` — 訂單明細
|
|
||||||
|
|
||||||
| 欄位 | 類型 | 說明 | 來源 |
|
|
||||||
|------|------|------|------|
|
|
||||||
| `id` | BIGINT PK | — | — |
|
|
||||||
| `order_id` | BIGINT FK | → orders | — |
|
|
||||||
| `product_id` | BIGINT FK | → products | B600 `req16.pid` |
|
|
||||||
| `quantity` | INT | 數量 | B600 `req16.num` |
|
|
||||||
| `unit_price` | DECIMAL(10,2) | 單價 | B600 `req16.amount` |
|
|
||||||
| `subtotal` | DECIMAL(10,2) | 小計 | 計算 |
|
|
||||||
| `timestamps` | — | — | — |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 🆕 `invoices` — 發票紀錄
|
|
||||||
|
|
||||||
| 欄位 | 類型 | 說明 | 來源 |
|
|
||||||
|------|------|------|------|
|
|
||||||
| `id` | BIGINT PK | — | — |
|
|
||||||
| `order_id` | BIGINT FK | → orders(via flow_id) | — |
|
|
||||||
| `flow_id` | VARCHAR | 金流 ID | B601 `req3` |
|
|
||||||
| `machine_id` | BIGINT FK | → machines | B601 `req2` |
|
|
||||||
| `rtn_code` | VARCHAR NULL | 回傳碼 | B601 `req4` |
|
|
||||||
| `rtn_msg` | VARCHAR NULL | 回傳訊息 | B601 `req5` |
|
|
||||||
| `invoice_no` | VARCHAR NULL | 發票號碼 | B601 `req6` |
|
|
||||||
| `invoice_date` | DATE NULL | 發票日期 | B601 `req7` |
|
|
||||||
| `random_number` | VARCHAR NULL | 隨機碼 | B601 `req8` |
|
|
||||||
| `love_code` | VARCHAR NULL | 愛心碼 | B601 `req9` |
|
|
||||||
| `timestamps` | — | — | — |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 🆕 `dispense_records` — 出貨紀錄
|
|
||||||
|
|
||||||
| 欄位 | 類型 | 說明 | 來源 |
|
|
||||||
|------|------|------|------|
|
|
||||||
| `id` | BIGINT PK | — | — |
|
|
||||||
| `order_id` | BIGINT FK NULL | → orders | — |
|
|
||||||
| `flow_id` | VARCHAR NULL | 金流 ID | B602 `req3` |
|
|
||||||
| `machine_id` | BIGINT FK | → machines | B602 `req2` |
|
|
||||||
| `product_id` | VARCHAR | 商品 ID | B602 `req5` |
|
|
||||||
| `slot_no` | VARCHAR | 貨道編號 | B602 `req6` |
|
|
||||||
| `amount` | DECIMAL(10,2) | 消費金額 | B602 `req7` |
|
|
||||||
| `remaining_stock` | INT NULL | 剩餘庫存 | B602 `req9` |
|
|
||||||
| `dispense_status` | TINYINT | 0:成功/1:失敗 | B602 `req13` |
|
|
||||||
| `member_barcode` | VARCHAR NULL | 會員條碼 | B602 `req10` |
|
|
||||||
| `machine_time` | DATETIME NULL | 機台時間 | B602 `req11` |
|
|
||||||
| `points_used` | INT DEFAULT 0 | 使用點數 | B602 `req16` |
|
|
||||||
| `timestamps` | — | — | — |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 🆕 `remote_commands` — 遠端指令佇列
|
|
||||||
|
|
||||||
| 欄位 | 類型 | 說明 | 來源 |
|
|
||||||
|------|------|------|------|
|
|
||||||
| `id` | BIGINT PK | — | — |
|
|
||||||
| `machine_id` | BIGINT FK | → machines | — |
|
|
||||||
| `command_type` | VARCHAR(20) | 指令類型 | B010 response status |
|
|
||||||
| `status` | ENUM | pending/sent/success/failed | — |
|
|
||||||
| `payload` | JSON NULL | 指令附加資料 | — |
|
|
||||||
| `executed_at` | TIMESTAMP NULL | 執行時間 | — |
|
|
||||||
| `timestamps` | — | — | — |
|
|
||||||
|
|
||||||
> 對應 B010 遠端指令(reboot, lock, unlock, checkout, dispense 等)以及 B055 遠端出貨。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 🆕 `coin_inventories` — 零錢機庫存
|
|
||||||
|
|
||||||
| 欄位 | 類型 | 說明 | 來源 |
|
|
||||||
|------|------|------|------|
|
|
||||||
| `id` | BIGINT PK | — | — |
|
|
||||||
| `machine_id` | BIGINT FK | → machines | B220 `machine` |
|
|
||||||
| `value_1` | INT DEFAULT 0 | 1 元 | B220 |
|
|
||||||
| `value_5` | INT DEFAULT 0 | 5 元 | B220 |
|
|
||||||
| `value_10` | INT DEFAULT 0 | 10 元 | B220 |
|
|
||||||
| `value_50` | INT DEFAULT 0 | 50 元 | B220 |
|
|
||||||
| `value_100` | INT DEFAULT 0 | 100 元 | B220 |
|
|
||||||
| `value_500` | INT DEFAULT 0 | 500 元 | B220 |
|
|
||||||
| `value_1000` | INT DEFAULT 0 | 1000 元 | B220 |
|
|
||||||
| `operator` | VARCHAR NULL | 操作人 (0=消費者) | B220 `account` |
|
|
||||||
| `timestamps` | — | — | — |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 🆕 `timer_statuses` — 計時器狀態
|
|
||||||
|
|
||||||
| 欄位 | 類型 | 說明 | 來源 |
|
|
||||||
|------|------|------|------|
|
|
||||||
| `id` | BIGINT PK | — | — |
|
|
||||||
| `machine_id` | BIGINT FK | → machines | B710 `req2` |
|
|
||||||
| `slot_no` | VARCHAR | 貨道 ID | B710 `cid` |
|
|
||||||
| `product_id` | BIGINT FK NULL | → products (0=未設定) | B710 `pid` |
|
|
||||||
| `status` | TINYINT | 0:未啟用/1:使用中/2:異常 | B710 `status` |
|
|
||||||
| `remaining_seconds` | INT | 剩餘秒數 | B710 `num` |
|
|
||||||
| `timestamps` | — | — | — |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 🆕 `payment_types` — 金流類型參照
|
|
||||||
|
|
||||||
| 欄位 | 類型 | 說明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `id` | BIGINT PK | — |
|
|
||||||
| `code` | SMALLINT UNIQUE | 金流代碼 (1,2,3...120) |
|
|
||||||
| `name` | VARCHAR | 中文名稱 |
|
|
||||||
| `category` | VARCHAR | 大分類 |
|
|
||||||
| `is_active` | BOOLEAN | 是否啟用 |
|
|
||||||
| `timestamps` | — | — |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 五、IoT API 路由設計
|
|
||||||
|
|
||||||
### 5.1 路由結構
|
|
||||||
|
|
||||||
```php
|
|
||||||
// routes/api.php
|
|
||||||
Route::prefix('v1/app')->middleware(['throttle:iot'])->group(function () {
|
|
||||||
|
|
||||||
// B010 - 機台狀態上傳 & 指令撈回
|
|
||||||
Route::post('/machine/status', [MachineStatusController::class, 'heartbeat']);
|
|
||||||
|
|
||||||
// B017 - 遠端撈庫存
|
|
||||||
Route::post('/machine/reload', [MachineStockController::class, 'reload']);
|
|
||||||
|
|
||||||
// B055 - 遠端出貨
|
|
||||||
Route::post('/machine/dispense', [DispenseController::class, 'getCommands']);
|
|
||||||
Route::put('/machine/dispense', [DispenseController::class, 'updateStatus']);
|
|
||||||
|
|
||||||
// B220 - 零錢機庫存
|
|
||||||
Route::post('/coin/inventory', [CoinInventoryController::class, 'store']);
|
|
||||||
|
|
||||||
// B600 - 消費金流
|
|
||||||
Route::post('/transaction', [TransactionController::class, 'store']);
|
|
||||||
|
|
||||||
// B601 - 發票回傳
|
|
||||||
Route::post('/invoice', [InvoiceController::class, 'store']);
|
|
||||||
|
|
||||||
// B602 - 出貨回傳
|
|
||||||
Route::post('/dispense-record', [DispenseRecordController::class, 'store']);
|
|
||||||
|
|
||||||
// B650 - 驗證會員
|
|
||||||
Route::post('/member/verify', [MemberVerifyController::class, 'verify']);
|
|
||||||
|
|
||||||
// B710 - 計時器狀態
|
|
||||||
Route::post('/timer/status', [TimerStatusController::class, 'sync']);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.2 處理管線
|
|
||||||
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> 遵循 IoT 通訊技能規範:**嚴禁** API Controller 直接寫入 DB。
|
|
||||||
|
|
||||||
| API | 處理方式 | 原因 |
|
|
||||||
|-----|----------|------|
|
|
||||||
| B010 | **異步** (Redis Queue) | 最高頻,每台數秒一次 |
|
|
||||||
| B017 | **同步** (直接回應) | 低頻,需即時回傳庫存數據 |
|
|
||||||
| B055 GET | **同步** | 需即時回傳指令列表 |
|
|
||||||
| B055 PUT | **異步** | 出貨結果可背景處理 |
|
|
||||||
| B220 | **異步** | 庫存變動日誌可背景寫入 |
|
|
||||||
| B600 | **異步** | 高頻交易,核心數據 |
|
|
||||||
| B601 | **異步** | 發票資訊可背景寫入 |
|
|
||||||
| B602 | **異步** | 出貨紀錄可背景寫入 |
|
|
||||||
| B650 | **同步** | 需即時回傳驗證結果與折抵金額 |
|
|
||||||
| B710 | **異步** | 計時器狀態批量更新 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 六、後台模組 ↔ 資料表對應
|
|
||||||
|
|
||||||
| 後台模組(web.php) | 對應資料表 | 資料來源 API |
|
|
||||||
|---------------------|-----------|-------------|
|
|
||||||
| 1. 儀表板 Dashboard | orders, machines, dispense_records | 統計彙整 |
|
|
||||||
| 2. 會員管理 Members | members, social_accounts, member_wallets... | B650 |
|
|
||||||
| 3. 機台管理 Machines | machines, machine_logs, machine_slots | B010, B017 |
|
|
||||||
| 4. APP 管理 App | app_configs, timer_statuses | B710 |
|
|
||||||
| 5. 倉庫管理 Warehouses | products, machine_slots | B017, B602 |
|
|
||||||
| 6. 銷售管理 Sales | orders, order_items, dispense_records | B600, B602 |
|
|
||||||
| 7. 分析管理 Analysis | orders, dispense_records, coin_inventories | 全部 |
|
|
||||||
| 8. 稽核管理 Audit | orders, dispense_records | B600, B602 |
|
|
||||||
| 9. 資料設定 DataConfig | products, payment_types, users | — |
|
|
||||||
| 10. 遠端管理 Remote | remote_commands | B010, B055 |
|
|
||||||
| 11. Line 管理 | members (Line 綁定) | — |
|
|
||||||
| 12. 預約系統 Reservation | (未來擴充) | — |
|
|
||||||
| 13. 特殊權限 SpecialPerm | machines, app_configs | — |
|
|
||||||
| 14. 權限設定 Permission | users, roles | — |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 七、效能預估與應對
|
|
||||||
|
|
||||||
### 10,000 台機台的流量預估
|
|
||||||
|
|
||||||
| API | 頻率 | 每秒請求量 (QPS) | 每日資料量 |
|
|
||||||
|-----|------|-----------------|-----------|
|
|
||||||
| B010 心跳 | 每 5 秒/台 | **~2,000** | ~173M 筆 |
|
|
||||||
| B600 金流 | 每筆交易 | ~10-50 | ~50K-200K 筆 |
|
|
||||||
| B602 出貨 | 每筆交易 | ~10-50 | ~50K-200K 筆 |
|
|
||||||
| B710 計時器 | 每 10 秒/台 | ~1,000 | ~86M 筆 |
|
|
||||||
|
|
||||||
### 應對策略
|
|
||||||
|
|
||||||
| 策略 | 實作方式 |
|
|
||||||
|------|----------|
|
|
||||||
| **Redis Queue 異步寫入** | 所有高頻 API 進 Queue,Worker 批量寫入 |
|
|
||||||
| **心跳資料瘦身** | B010 不逐筆寫 DB,僅更新 `machines` 表的即時欄位 + 異常時寫 log |
|
|
||||||
| **資料分區** | `orders` 和 `dispense_records` 按月份分區 |
|
|
||||||
| **索引策略** | 所有查詢欄位建立適當 INDEX(machine_id + created_at 複合索引) |
|
|
||||||
| **速率限制** | 單台機台 60 req/min,全站 100,000 req/min |
|
|
||||||
| **快取** | 機台列表、商品列表等低變動資料用 Redis Cache |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 八、實作路線圖與預估時程
|
|
||||||
|
|
||||||
> **總結報告**:為確保開發品質與應對突發狀況,本專案預計總開發時程擴展為 **16 - 18 週**。以 1 人天 = 8 小時純開發時間計算,預估總投入約 **80 - 90 個工作天**。
|
|
||||||
|
|
||||||
### 🗓️ 專案開發甘特圖 (自動排除週末)
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
gantt
|
|
||||||
title Star Cloud 實作時程規劃
|
|
||||||
dateFormat YYYY-MM-DD
|
|
||||||
excludes weekends
|
|
||||||
|
|
||||||
section 第一階段 MVP
|
|
||||||
Phase 1 基礎設施與資料表 :active, p1, 2026-03-11, 14d
|
|
||||||
Phase 2 IoT API 與異步管線 :p2, after p1, 24d
|
|
||||||
Phase 3 後台 MVP 頁面整合 :p3, after p2, 18d
|
|
||||||
|
|
||||||
section 第二階段 進階
|
|
||||||
Phase 4 進階功能與報表分析 :p4, after p3, 30d
|
|
||||||
```
|
|
||||||
|
|
||||||
### 📅 詳細時程對照表
|
|
||||||
|
|
||||||
| 階段 (Phase) | 關鍵任務摘要 | 預估天數 | 預計工作日期 | 狀態 |
|
|
||||||
| :--- | :--- | :---: | :---: | :---: |
|
|
||||||
| **Phase 1** | 基礎架構、28張表設計、資料遷移基礎 | 14 工作天 | 03/11 ~ 03/30 | 準備啟動 |
|
|
||||||
| **Phase 2** | B010~B710 核心 API、Redis 異步 Job、Service | 24 工作天 | 03/31 ~ 05/01 | 規劃中 |
|
|
||||||
| **Phase 3** | 儀表板、機台/銷售/遠端管理 MVP 介面 | 18 工作天 | 05/04 ~ 05/27 | 規劃中 |
|
|
||||||
| **Phase 4** | 數據分析統計圖表、補貨演算法、自動對帳模組 | 30 工作天 | 05/28 ~ 07/08 | 規劃中 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🔵 第一階段:MVP 營運核心 (最優先上線)
|
|
||||||
**目標**:確保機台金流與指令能穩定連通,完成基礎營運管理功能。
|
|
||||||
|
|
||||||
#### Phase 1:基礎設施與核心資料表 (預計: 14 工作天)
|
|
||||||
- [ ] 擴充 `machines` 表(serial_no, model 等欄位)
|
|
||||||
- [ ] 建立 多租戶與權限基礎表 (`companies`, `spatie/laravel-permission` 相關表)
|
|
||||||
- [ ] 將 `company_id` 欄位加入 `users`, `machines`, `roles` 等核心表
|
|
||||||
- [ ] 建立 `translations` 多語系字典表,並擴充 `products` 語系關聯
|
|
||||||
- [ ] 新增 `products` + `product_categories` 表
|
|
||||||
- [ ] 新增 `machine_slots` 表
|
|
||||||
- [ ] 新增 `orders` + `order_items` 表
|
|
||||||
- [ ] 新增 `invoices` + `dispense_records` 表
|
|
||||||
- [ ] 新增 `remote_commands` 表 (遠端管理與對接用)
|
|
||||||
- [ ] 新增 `payment_types` 表 + Seeder
|
|
||||||
|
|
||||||
#### Phase 2:核心營運 IoT API 與異步管線 (預計: 24 工作天)
|
|
||||||
- [ ] 實作 **B010** API (心跳上報 + 狀態更新 + 遠端管理)
|
|
||||||
- [ ] 實作 **B017 / B055** API (遠端改庫存/出貨)
|
|
||||||
- [ ] 實作 **B600 / B601 / B602** API (金流/發票/出貨紀錄)
|
|
||||||
- [ ] 實作 **B650** API (會員驗證 + 點數折抵)
|
|
||||||
- [ ] 建立上述對應的 **Redis Queue Job + Service** 異步處理邏輯
|
|
||||||
|
|
||||||
#### Phase 3:後台 MVP 頁面整合 (預計: 18 工作天)
|
|
||||||
- [ ] **多租戶權限與身份切換機制 (Middleware & Views)**
|
|
||||||
- [ ] 儀表板 Dashboard:即時數據看板 (營收、機台數,依據租戶隔離資料)
|
|
||||||
- [ ] 機台管理 Machines:連線狀態、貨道庫存、指令操作 UI
|
|
||||||
- [ ] 銷售管理 Sales:訂單列表、出貨/發票追蹤
|
|
||||||
- [ ] 系統基礎設定:帳號與權限設定 (RBAC UI)、商品建檔、金流代碼設定、**多語系字典維護介面**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🟢 第二階段:進階營運與智慧分析 (持續優化)
|
|
||||||
**目標**:強化報表分析能力與自動化稽核。
|
|
||||||
|
|
||||||
#### Phase 4:進階功能與報表分析模組 (預計: 30 工作天)
|
|
||||||
- [ ] **分析管理 Analysis**:趨勢圖、商品熱銷分析、機台產能報表
|
|
||||||
- [ ] **倉庫管理 Warehouses**:補貨建議、入/出庫流程優化
|
|
||||||
- [ ] **擴充支持**:B220 (零錢機) + B710 (計時器) 支援
|
|
||||||
- [ ] **自動化稽核 Audit**:對帳機制、異常提醒
|
|
||||||
- [ ] **行銷模組**:Line 聯動、預約系統、UI 動態設定
|
|
||||||
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 九、目錄結構建議
|
|
||||||
|
|
||||||
```
|
|
||||||
app/
|
|
||||||
├── Http/Controllers/
|
|
||||||
│ ├── Admin/ ← 後台管理(已存在 14 模組)
|
|
||||||
│ └── Api/V1/
|
|
||||||
│ ├── App/ ← 機台 IoT API(新增)
|
|
||||||
│ │ ├── MachineStatusController.php (B010)
|
|
||||||
│ │ ├── MachineStockController.php (B017)
|
|
||||||
│ │ ├── DispenseController.php (B055)
|
|
||||||
│ │ ├── CoinInventoryController.php (B220)
|
|
||||||
│ │ ├── TransactionController.php (B600)
|
|
||||||
│ │ ├── InvoiceController.php (B601)
|
|
||||||
│ │ ├── DispenseRecordController.php (B602)
|
|
||||||
│ │ ├── MemberVerifyController.php (B650)
|
|
||||||
│ │ └── TimerStatusController.php (B710)
|
|
||||||
│ ├── MemberController.php ← 會員 API(已存在)
|
|
||||||
│ └── MachineController.php ← 機台日誌(已存在)
|
|
||||||
├── Jobs/
|
|
||||||
│ └── Machine/ ← IoT 異步任務(新增)
|
|
||||||
│ ├── ProcessHeartbeat.php
|
|
||||||
│ ├── ProcessTransaction.php
|
|
||||||
│ ├── ProcessDispenseRecord.php
|
|
||||||
│ ├── ProcessInvoice.php
|
|
||||||
│ ├── ProcessCoinInventory.php
|
|
||||||
│ └── ProcessTimerStatus.php
|
|
||||||
├── Services/
|
|
||||||
│ └── Machine/ ← 業務邏輯(新增)
|
|
||||||
│ ├── HeartbeatService.php
|
|
||||||
│ ├── TransactionService.php
|
|
||||||
│ ├── DispenseService.php
|
|
||||||
│ └── MemberVerifyService.php
|
|
||||||
└── Models/
|
|
||||||
├── Machine/ ← 已存在
|
|
||||||
│ ├── Machine.php
|
|
||||||
│ └── MachineLog.php
|
|
||||||
├── Transaction/ ← 新增
|
|
||||||
│ ├── Order.php
|
|
||||||
│ ├── OrderItem.php
|
|
||||||
│ ├── Invoice.php
|
|
||||||
│ ├── DispenseRecord.php
|
|
||||||
│ └── PaymentType.php
|
|
||||||
├── Product/ ← 新增
|
|
||||||
│ ├── Product.php
|
|
||||||
│ └── ProductCategory.php
|
|
||||||
└── Member/ ← 已存在
|
|
||||||
```
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
# B010 API (機台心跳與指令) 技術規範與執行指南
|
|
||||||
|
|
||||||
本文件整合了 B010 API 的業務邏輯討論與技術執行分析,作為 Star Cloud 實作機台通訊核心的指導準則。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. 業務邏輯與指令下發 (Command Flow)
|
|
||||||
|
|
||||||
### 1.1 指令下發機制
|
|
||||||
B010 是 Cloud 向機台下達遠端指令(如:開鎖、重啟、改庫存)的唯一通道。
|
|
||||||
* **指令隊列**:指令需先寫入 `remote_commands` 表,狀態標記為 `pending`。
|
|
||||||
* **撈取邏輯**:當機台定期呼叫 B010 時,後端 Service 會查詢該機台是否有等待執行的指令。
|
|
||||||
* **回應處理**:若有指令,則在 Response 的 `status` 欄位回傳對應的指令碼(如 51:重啟, 70:開鎖)。
|
|
||||||
* **優先順序**:當同時有多個指令待執行時,需按緊急程度(如:重啟 > 開鎖 > 載入配置)定義優先順序回傳。
|
|
||||||
|
|
||||||
### 1.2 執行回饋 (Feedback Loop)
|
|
||||||
* **確認機制**:機台收到指令後,必須確保執行結果能回報至雲端。
|
|
||||||
* **判定方案**:
|
|
||||||
* **被動判定**:若機台在其後的 B010 心跳中回報狀態正常(如:軟體版本更新成功、頁面碼正確),即視為成功。
|
|
||||||
* **主動判定**:對於關鍵指令(如遠端出貨),機台需另外呼叫狀態更新 API (如 B055 PUT) 回報結果。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 執行面挑戰:高併發與效能 (Performance)
|
|
||||||
|
|
||||||
### 2.1 寫入瓶頸分析
|
|
||||||
* **數據規模**:10,000 台機台,每 5 秒一個心跳,全站 QPS 約 2,000。
|
|
||||||
* **DB 壓力**:直接同步寫入 MySQL `machines` 與 `machine_logs` 將導致磁碟 I/O 鎖定。
|
|
||||||
|
|
||||||
### 2.2 解決策略 (Redis 緩衝)
|
|
||||||
* **即時狀態流動**:
|
|
||||||
* **最後在線時間**、**溫度**、**門禁狀態**、**頁面碼** 等動態數據優先存入 **Redis**。
|
|
||||||
* **批量更新 (Lazy Update)**:每 1~5 分鐘由後台任務將 Redis 數據批量寫回 MySQL `machines` 表。
|
|
||||||
* **日誌優化**:
|
|
||||||
* 正常的心跳資料不逐筆寫入 `machine_logs`。
|
|
||||||
* 僅在**狀態變更**(如:門被打開、溫度過高、軟體報錯)時,才觸發資料庫日誌記錄。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 資料庫結構擴充 (Schema Gap)
|
|
||||||
|
|
||||||
必須對原有的 `machines` 表進行欄位擴充,以承載 B010 的回傳值:
|
|
||||||
|
|
||||||
| 新增欄位 | 類型 | 說明 |
|
|
||||||
|----------|------|------|
|
|
||||||
| `serial_no` | VARCHAR(50) UNIQUE | 機台序號 (API `machine`) |
|
|
||||||
| `model` | VARCHAR(50) | 機台型號 (API `M_Stus`) |
|
|
||||||
| `current_page` | TINYINT | 當前頁面代碼 (API `M_Stus2`) |
|
|
||||||
| `door_status` | VARCHAR(10) | 門禁狀態 (`open`/`closed`) |
|
|
||||||
| `api_token` | VARCHAR(100) | 獨立安全憑證 (取代共用 Key) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 安全性與驗證策略 (Security)
|
|
||||||
|
|
||||||
* **API Key 遷移**:從目前全系統硬編碼的共用 Key,逐步遷移至每台機台專屬的 `api_token`。
|
|
||||||
* **實裝方式**:
|
|
||||||
* 後台管理介面新增「核發機台 Token」功能。
|
|
||||||
* 建立專屬 `IotAuthMiddleware`,驗證請求中的 Token 與 `machines` 表是否匹配。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. 離線與告警機制 (Heartbeat Logic)
|
|
||||||
|
|
||||||
* **在線判定**:心跳超時(建議超過 60 秒)即在 UI 將機台顯示為「離線」。
|
|
||||||
* **自動維護**:排程 Job 定期掃描 `machines.last_heartbeat_at`,對長期失聯的機台發出系統告警。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. 待確認事項 (To-be-confirmed)
|
|
||||||
|
|
||||||
### 6.1 指令下發與回應 (Command Flow)
|
|
||||||
1. **指令優先順序**:當後台同時有多個指令待執行 (如:改庫存 + 重啟) 時,應如何定義優先權?
|
|
||||||
2. **執行狀態回饋**:機台收到心跳指令後的執行結果確認,應採「被動判定」(機台下次心跳狀態正確則視為成功) 還是「主動回報」(機台另呼叫 API)?
|
|
||||||
|
|
||||||
### 6.2 效能與日誌策略 (Performance & Logs)
|
|
||||||
3. **Redis 更新頻率**:即時狀態從 Redis 批量同步至 MySQL 的建議時間間隔 (目前暫估 1~5 分鐘)。
|
|
||||||
4. **心跳日誌級別**:是否確認「正常心跳」不寫入資料庫日誌,僅記錄「異常/變更」事件?
|
|
||||||
|
|
||||||
### 6.3 在線/離線判定 (Presence)
|
|
||||||
5. **離線超時定義**:預計多久未收心跳判定為離線?(建議 30~60 秒)。
|
|
||||||
6. **斷線告警機制**:機台斷線時,是否需即時產出系統警示或推播給相關人員?
|
|
||||||
|
|
||||||
### 6.4 安全性驗證 (Auth)
|
|
||||||
7. **Token 傳遞方式**:將 API Key 遷移至專屬 Token 時,機台端是否能配合在 Header (`Authorization: Bearer <token>`) 帶入,或必須保留在 JSON Payload 中?
|
|
||||||
|
|
||||||
### 6.5 欄位細節 (Field Details)
|
|
||||||
8. **URL {workid} 定義**:URL 中的 `{workid}` 應對應為 `serial_no` (機台序號) 還是資料庫自增 `id`?
|
|
||||||
9. **M_Stus2 告警觸發**:20 多種頁面狀態碼中,哪些特定的錯誤代碼需要在管理後台觸發即時紅字警示?
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
# B017 API (遠端改庫存) 技術規範與執行指南
|
|
||||||
|
|
||||||
本文件記錄 B017 API 的業務邏輯與技術執行細節,用於指導後端管理系統遠端調整機台庫存的實作。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. 業務情境與觸發機制 (Trigger)
|
|
||||||
|
|
||||||
### 1.1 觸發流程
|
|
||||||
1. **管理行為**:管理員在後台管理介面手動修改特定機台的貨道庫存數量。
|
|
||||||
2. **指令預備**:後端系統記錄異動請求,並在該機台的 B010 (心跳) 回應中,將 `status` 設為 `49` (reload B017)。
|
|
||||||
3. **機台拉取**:機台收到心跳回應中的 `49` 代碼後,主動呼叫 B017 API (`/api/app/machine/reload_msg/{workid}`)。
|
|
||||||
4. **數據同步**:機台獲取最新庫存數據並更新本地狀態。
|
|
||||||
|
|
||||||
### 1.2 更新範圍
|
|
||||||
* **全量更新 (Full Sync)**:B017 API 回傳該機台**所有貨道**的最新庫存數量,而非僅回傳變動部分。這有助於機台軟體進行「最終狀態一致性」的自我校正。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 資料一致性與日誌 (Consistency & Audit)
|
|
||||||
|
|
||||||
### 2.1 更新確認機制
|
|
||||||
* **隱式成功 (Implicit Success)**:機台更新本地庫存後不需要回傳明確的 B017 確認 API。
|
|
||||||
* **驗證方式**:系統將以機台下次呼叫 B010 回報的狀態或機台後續的正常銷售紀錄,視為前次更新成功。
|
|
||||||
|
|
||||||
### 2.2 管理員異動日誌 (Audit Log)
|
|
||||||
* **強制要求**:每次後台發起遠端改庫存操作,必須記錄於 `activity_logs` 或特定的 `stock_adjustment_logs`。
|
|
||||||
* *紀錄內容*:管理者 ID、執行時間、機台 ID、調整前後的貨道庫存對照。
|
|
||||||
|
|
||||||
### 2.3 庫存衝突處理
|
|
||||||
* *情境*:若雲端下發庫存更新時,機台現場同時發生實體銷售。
|
|
||||||
* *處理原則*:**詳見下方待確認事項**。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 執行面優化 (Execution Strategy)
|
|
||||||
|
|
||||||
### 3.1 Redis 緩衝與效能
|
|
||||||
* **快取策略**:機台的貨道結構與庫存數據應快取於 Redis 中。當管理員調整庫存時,同步更新 Redis 與 MySQL。
|
|
||||||
* **讀取優化**:B017 API 應直接從 Redis 讀取數據回傳給機台,以降低 MySQL 的查詢壓力。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 欄位定義與對照
|
|
||||||
|
|
||||||
| 欄位名稱 | 資料庫對應 | 說明 |
|
|
||||||
|----------|------------|------|
|
|
||||||
| `workid` | `machines.serial_no` | 機台識別序號 |
|
|
||||||
| `channelid` | `machine_slots.slot_no` | 貨道編號 |
|
|
||||||
| `stock` | `machine_slots.stock` | 該貨道的當前可用庫存 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. 開發建議
|
|
||||||
* 建立一個專屬的 `StockService` 來處理 Redis 與 DB 的同步,以及異動日誌的寫入。
|
|
||||||
* 在 `B010` 的回應邏輯中,檢查 `remote_commands` 是否有 pending 的庫存異動指令,以動態切換 `status: 49`。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. 待確認事項 (To-be-confirmed)
|
|
||||||
|
|
||||||
1. **庫存衝突優先權**:當「雲端改庫存」與「現場實體銷售」同時發生時,最終庫存應以何者為準?(待主管確認)。
|
|
||||||
2. **API 欄位對照**:
|
|
||||||
* `workid` 是否嚴格對應 `serial_no`?
|
|
||||||
* `channelid` 是否對標 `slot_no`?
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
# B055 API (遠端出貨) 技術規範與執行指南
|
|
||||||
|
|
||||||
本文件定義 B055 API 的技術實作規範,用於處理從雲端發起的機台遠端出貨指令及其結果回饋。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. 業務流程與通訊機制
|
|
||||||
|
|
||||||
### 1.1 指令下發流程
|
|
||||||
1. **指令生成**:Cloud 後端(管理者或系統觸發)在 `remote_dispense_commands` 表中建立一筆狀態為 `0` (待執行) 的指令。
|
|
||||||
2. **心跳通知**:在該機台的 B010 回應中,將 `status` 設為 `85` (reload B055)。
|
|
||||||
3. **機台撈取 (POST)**:機台呼叫 B055 POST API (`/api/app/machine/dispense/{workid}`) 取得詳細出貨參數(貨道 ID、指令 ID)。
|
|
||||||
4. **執行與回報 (PUT)**:機台嘗試出貨後,呼叫 B055 PUT API 回報出貨結果及剩餘庫存。
|
|
||||||
|
|
||||||
### 1.2 數據關聯 (Data Linking)
|
|
||||||
* **出貨紀錄整合**:遠端出貨成功後,系統必須在 `dispense_records` 表中同步存入一筆紀錄,並將 `source` 標記為 `remote`(或對應識別),確保後台報表包含所有實體出貨數據。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 資料庫結構:`remote_dispense_commands`
|
|
||||||
|
|
||||||
| 欄位 | 說明 | 備註 |
|
|
||||||
|------|------|------|
|
|
||||||
| `command_id` | 執行命令 ID | 用於追蹤單次指令 |
|
|
||||||
| `slot_no` | 貨道 ID | 指派機台出貨的通道 |
|
|
||||||
| `status` | 執行狀態 | 0:待執行, 1:成功, 2:失敗 |
|
|
||||||
| `remaining_stock` | 剩餘庫存 | 由機台回報 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 執行面優化 (Execution Strategy)
|
|
||||||
|
|
||||||
* **Redis 隊列**:由於遠端出貨屬於高優先權操作,待執行指令應快取於 Redis 中,以確保機台呼叫 POST 時能即時回應。
|
|
||||||
* **異步處理**:機台 PUT 回報結果後,系統透過隊列更新 DB 狀態,並根據結果觸發後續邏輯(如扣除庫存、異動日誌)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 待確認事項 (To-be-confirmed)
|
|
||||||
|
|
||||||
### 4.1 業務情境 (Scenario)
|
|
||||||
1. **指令發起來源**:確認指令是由後台管理員手動測試發起,還是串接外部線上購買系統產出的取貨指令?
|
|
||||||
|
|
||||||
### 4.2 失敗處理機制 (Failure Handling)
|
|
||||||
2. **自動退款/告警**:若機台 PUT 回報出貨失敗,雲端是否需要自動發起退款(針對線上訂單)或發送維修告警通知?
|
|
||||||
|
|
||||||
### 4.3 指令效期與防重 (TTL & Idempotency)
|
|
||||||
3. **指令效期**:如果指令發出後機台失聯,該指令應在多久後失效 (TTL)?
|
|
||||||
4. **防重複機制**:如何嚴格確保同一個 `command_id` 不會被重複執行兩次?
|
|
||||||
|
|
||||||
### 4.4 安全性與權限 (Security)
|
|
||||||
5. **指令簽名**:除了 `api_token`,發送出貨指令是否需要額外的加密簽名驗證以防偽造?
|
|
||||||
6. **權限限制**:是否限制僅特定高級管理員可發起此類高風險指令?
|
|
||||||
|
|
||||||
### 4.5 欄位定義
|
|
||||||
7. **URL {workid}**:確認是否對標 `serial_no`。
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
# B220 API (零錢機庫存變動) 技術規範與執行指南
|
|
||||||
|
|
||||||
本文件定義 B220 API 的技術實作規範,用於監控機台內的硬幣與鈔票庫存數量,並提供補幣紀錄與找零水位告警功能。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. 業務情境與數據特性
|
|
||||||
|
|
||||||
### 1.1 數據採集
|
|
||||||
* **回報內容**:機台定期回報各面額(1, 5, 10, 50, 100, 500, 1000 元)的**當前累積總量 (Total Amount)**。
|
|
||||||
* **操作識別**:
|
|
||||||
* `account = 0`:消費者交易引起的庫存變動。
|
|
||||||
* `account != 0`:管理員或補貨員進行手動補幣或清空錢箱的操作。
|
|
||||||
|
|
||||||
### 1.2 報表與對帳
|
|
||||||
* **零錢庫存分析**:系統必須保留所有變動紀錄,以提供後台「分析報表 > 零錢庫存分析」選單顯示歷史庫存走勢與補幣紀錄。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 執行面優化與告警 (Execution & Alerts)
|
|
||||||
|
|
||||||
### 2.1 高頻數據處理 (待確認頻率)
|
|
||||||
* **快取策略**:機台各面額的最新總量應快取於 Redis 或更新至 `machines` 表的擴充欄位,供後台儀表板即時監看。
|
|
||||||
* **寫入過濾**:為節省儲存空間,建議僅在資料數值有實際變動時才寫入 `coin_inventory_logs`。
|
|
||||||
|
|
||||||
### 2.2 找零不足告警 (Low Inventory Warning)
|
|
||||||
* **機制**:當特定面額(如 5 元、10 元硬幣)低於預設的水位閾值時,系統應:
|
|
||||||
* 在管理後台儀表板顯示紅字告警。
|
|
||||||
* (選配)發送推播或系統通知給補貨人員。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 資料庫結構:`coin_inventory_logs`
|
|
||||||
|
|
||||||
| 欄位 | 說明 | 備註 |
|
|
||||||
|------|------|------|
|
|
||||||
| `machine_id` | 機台關聯 ID | |
|
|
||||||
| `value_x` | 各面額數量 | 包含 1, 5, 10, 50, 100, 500, 1000 |
|
|
||||||
| `account` | 操作人識別 | 0 為消費者,其餘為管理員 |
|
|
||||||
| `created_at` | 紀錄時間 | |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 待確認事項 (To-be-confirmed)
|
|
||||||
|
|
||||||
### 4.1 系統效能與頻率
|
|
||||||
1. **回報頻率**:確認機台呼叫 B220 的頻率(是每次交易後即時回報,還是定時回報?)。
|
|
||||||
|
|
||||||
### 4.2 告警配置
|
|
||||||
2. **水位閾值**:各面額的最低告警水位數值(如 5 元低於多少枚觸發告警)。
|
|
||||||
3. **通知方式**:觸發告警時的具體通知管道。
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
# B600 API (消費金流回傳) 技術規範與執行指南
|
|
||||||
|
|
||||||
本文件定義 B600 API 的技術實作規範,這是系統處理金流交易、訂單生成與跨 API 關聯的核心。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. 交易生命週期與 `flow_id` 串聯
|
|
||||||
|
|
||||||
為確保一筆交易數據的完整性,系統採用 `flow_id` 串聯三個連續上報的 API:
|
|
||||||
|
|
||||||
1. **B600 (金流上報)**:
|
|
||||||
* 機台於支付成功後即時呼叫。
|
|
||||||
* 雲端收到後產出專屬的 `flow_id` 並回傳給機台。
|
|
||||||
2. **B601 (發票上報)**:
|
|
||||||
* 機台開立發票後,攜帶 B600 取得的 `flow_id` 呼叫。
|
|
||||||
* 系統根據 `flow_id` 將發票資訊與 `orders` 表關聯。
|
|
||||||
3. **B602 (出貨上報)**:
|
|
||||||
* 機台實體出貨後,攜帶 `flow_id` 呼叫。
|
|
||||||
* 系統根據 `flow_id` 更新訂單的出貨狀態。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 金流類型與標準化
|
|
||||||
|
|
||||||
### 2.1 類型定義
|
|
||||||
根據 B600 PDF 文件規範 (Req3),系統優先採用以下預定義類型進行處理與對帳。
|
|
||||||
|
|
||||||
### 2.2 防重複上報 (Idempotency)
|
|
||||||
為防止網路不穩導致機台重複呼叫 B600,系統必須使用機台產生的 `order_no (tid)` 結合 `machine_id` 作為唯一鍵值校驗。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 執行面優化:異步隊列 (Async Queue)
|
|
||||||
|
|
||||||
在高併發環境下,為確保機台端不因等待資料庫寫入而時耗,B600 採以下流程:
|
|
||||||
|
|
||||||
1. **即時回應**:API 接收請求後,將原始 JSON 派發至 **Redis Queue**。
|
|
||||||
2. **背景處理**:由 `ProcessTransaction` 任務異步執行:
|
|
||||||
* 建立 `orders` 與 `order_items`。
|
|
||||||
* 計算並異動會員點數/優惠券。
|
|
||||||
* 同步更新儀表板統計數據。
|
|
||||||
3. **異步回傳**:機台呼叫 API 後會收到 `202 Accepted`,`flow_id` 則透過回應檔同步回傳。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 待確認事項 (To-be-confirmed)
|
|
||||||
|
|
||||||
### 4.1 數據完整性稽核
|
|
||||||
1. **自動對帳**:是否需要定期自動比對「B600 收取金額」與「B602 實際出貨金額」,並將差異標記為異常訂單?
|
|
||||||
|
|
||||||
### 4.2 失敗補償邏輯
|
|
||||||
2. **出貨失敗處理**:若 B602 回報「出貨失敗」,系統是否需觸發自動退款程序(限線上支付)或發送補發通知?
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
# B601 API (發票資訊回傳) 技術規範與執行指南
|
|
||||||
|
|
||||||
本文件定義 B601 API 的技術實作規範,用於紀錄每筆訂單對應的電子發票資訊。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. 業務流程與角色定義
|
|
||||||
|
|
||||||
### 1.1 系統角色
|
|
||||||
* **機台端**:負責與電子發票系統對接、開立發票,並於取得發票號碼後將資訊回傳至雲端。
|
|
||||||
* **雲端 (Star Cloud)**:僅負責**紀錄 (Logging)** 發票資訊,不涉及上傳傳財政部平台或寄送中獎通知。
|
|
||||||
|
|
||||||
### 1.2 串聯機制
|
|
||||||
* **關聯 ID**:必須攜帶 B600 取得的 `flow_id`,以便將發票資訊正確掛載至對應的 `orders`。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 資料庫結構:`invoices`
|
|
||||||
|
|
||||||
為符合台灣發票規範,除了原始 PDF 欄位外,本系統將擴充相關稅務欄位:
|
|
||||||
|
|
||||||
| 欄位 | 說明 | 備註 |
|
|
||||||
|------|------|------|
|
|
||||||
| `order_id` | 訂單關聯 ID | 透過 `flow_id` 對應 |
|
|
||||||
| `invoice_no` | 發票號碼 | 例:AB-12345678 |
|
|
||||||
| `invoice_date` | 發票日期 | |
|
|
||||||
| `random_number` | 隨機碼 | 4 位數 |
|
|
||||||
| `love_code` | 愛心碼 | 捐贈碼 |
|
|
||||||
| `business_tax_id` | **公司統編** | 8 位數 |
|
|
||||||
| `carrier_id` | **載具編號** | 手機條碼或自然人憑證 |
|
|
||||||
| `carrier_type` | **載具類型** | |
|
|
||||||
| `status` | **發票狀態** | 1:有效, 0:作廢 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 發票作廢機制 (Invoice Voiding)
|
|
||||||
|
|
||||||
* **需求確認**:當交易發生退款或特定失敗情境時,機台端需具備回報「發票作廢」的能力。
|
|
||||||
* **API 實作**:
|
|
||||||
* **方法一**:呼叫 B601 POST 時帶入 `status = 0`。
|
|
||||||
* **方法二**:新增 `/api/app/B601/void` 專屬端點(待後續細化)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 待確認事項 (To-be-confirmed)
|
|
||||||
|
|
||||||
### 4.1 邏輯關聯與超時
|
|
||||||
1. **發票缺失標記**:若收到 B600 (金流) 但在預設時間內(如 5 分鐘)未收到 B601,後台是否應將該訂單標記為「發票缺失」以供巡檢。
|
|
||||||
2. **重試機制**:機台端在 B601 呼叫失敗時的緩存與重傳策略。
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
# B602 API (出貨回傳) 技術規範與執行指南
|
|
||||||
|
|
||||||
本文件定義 B602 API 的技術實作規範,用於紀錄機台實體出貨結果,並作為訂單完結與庫存校準的依據。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. 業務邏輯與交易完結
|
|
||||||
|
|
||||||
### 1.1 串聯機制
|
|
||||||
* **關聯 ID**:必須攜帶 B600 取得的 `flow_id`,以確保出貨紀錄正確鏈結至對應的 `orders`。
|
|
||||||
|
|
||||||
### 1.2 多品項交易處理 (Multi-item Orders)
|
|
||||||
* **一對多關係**:系統支持一筆訂單(同一個 `flow_id`)對應多筆 `dispense_records`。
|
|
||||||
* **上報方式**:機台每完成一個貨道之出貨即呼叫一次 B602。雲端後端將自動彙整所有關聯之出貨紀錄以計算該訂單之總體狀態。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 庫存一致性機制 (Stock Consistency)
|
|
||||||
|
|
||||||
為確保雲端與機台端的物理庫存同步,系統採以下策略:
|
|
||||||
|
|
||||||
1. **強制信任機制**:系統將「強制信任」機台在 B602 中回報的 `remaining_stock` (絕對值),並直接更新資料庫中的貨道庫存。
|
|
||||||
2. **異常監控**:若機台回報的庫存與系統計算的預期值(原值 - 1)不符時,系統應於後台標記該次出貨為「庫存異動異常」,供管理員檢查是否發生卡貨或漏貨。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 資料庫結構:`dispense_records`
|
|
||||||
|
|
||||||
| 欄位 | 說明 | 備註 |
|
|
||||||
|------|------|------|
|
|
||||||
| `order_id` | 訂單 ID | 透過 `flow_id` 關聯 |
|
|
||||||
| `product_id` | 商品 ID | 出貨之商品 |
|
|
||||||
| `slot_no` | 貨道編號 | 出貨之貨道 |
|
|
||||||
| `dispense_status` | 出貨狀態 | 0: 成功, 1: 失敗 |
|
|
||||||
| `remaining_stock` | 剩餘庫存 | 機台回報之絕對值 |
|
|
||||||
| `source` | 來源標記 | `local` (B602), `remote` (B055) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 待確認事項 (To-be-confirmed)
|
|
||||||
|
|
||||||
### 4.1 訂單完結判斷
|
|
||||||
1. **超時標記**:若雲端收到 B600 但遲遲未收到 B602,預設在多久後應將訂單標記為「出貨超時」?
|
|
||||||
2. **狀態遷移**:出貨失敗 (`status=1`) 時,訂單的最終顯示文字與處理流程。
|
|
||||||
|
|
||||||
### 4.2 營收與點數處理
|
|
||||||
3. **出貨失敗補償**:若 B602 回報失敗:
|
|
||||||
* 行動支付是否自動觸發退款?
|
|
||||||
* 是否自動退回該次交易所扣除之會員點數與優惠券?
|
|
||||||
4. **現金交易失敗**:出貨失敗且為現金交易時,是否應產出待維護工單或通知補貨員。
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
# B650 API (會員/點數/取貨碼驗證) 技術規範與執行指南
|
|
||||||
|
|
||||||
本文件定義 B650 API 的技術實作規範,用於處理會員驗證、點數折抵、優惠券兌換、取貨碼驗證以及線上訂單線下結帳等多元功能。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. 業務功能與驗證類型 (req6)
|
|
||||||
|
|
||||||
根據 API 請求中的 `req6` 欄位,B650 提供以下驗證模式:
|
|
||||||
|
|
||||||
| req6 | 說明 | 核心流程 |
|
|
||||||
|------|------|----------|
|
|
||||||
| `0` | 點擊商品驗證 | 驗證會員資格與點數餘額 |
|
|
||||||
| `1` | 會員純驗證 | 僅確認會員身份是否有效 |
|
|
||||||
| `2` | 會員兌換商品清單 | 撈取該會員可使用的兌換券或贈品清單 |
|
|
||||||
| `3` | 線上訂單線下結帳 | 驗證取貨碼或訂單編號 |
|
|
||||||
| `4` | 會員贈品 | 處理行銷活動贈品領取 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 執行面機制 (Execution Strategy)
|
|
||||||
|
|
||||||
* **高可用性請求**:由於會員驗證通常發生在交易前夕,API 必須具備極速響應能力,建議與第三方系統(若有)採用高效能通訊協議。
|
|
||||||
* **Response 狀態碼校驗**:系統必須嚴格遵守 PDF 定義的狀態碼(如 `101` 全額折抵, `104` 線上訂單驗證成功, `30` 會員使用中等)進行前端反饋。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 待確認事項 (To-be-confirmed)
|
|
||||||
|
|
||||||
以下議題目前皆列為**待確認**,將於 Phase 2 或後續討論中釐清:
|
|
||||||
|
|
||||||
### 3.1 會員系統架構 (Architecture)
|
|
||||||
1. **資料主體**:Star Cloud 是直接管理會員資料庫,還是僅作為轉發請求至第三方 CRM 的代理伺服器?
|
|
||||||
|
|
||||||
### 3.2 安全與防雙重消費 (Security)
|
|
||||||
2. **鎖定機制**:是否需要在 Redis 實作「會員操作 Session」,防止同一帳號在多台機台同時扣點 (狀態碼 30)?
|
|
||||||
3. **Token 時效**:回傳之 `userToken` 的有效期與刷新機制。
|
|
||||||
|
|
||||||
### 3.3 線上/線下整合流程
|
|
||||||
4. **訂單轉換**:驗證取貨碼成功後,雲端是否應自動產出對應的銷售紀錄單?
|
|
||||||
|
|
||||||
### 3.4 扣點邏輯
|
|
||||||
5. **預扣 vs 實扣**:點數與優惠券應於 B650 驗證時預先鎖定,還是待 B602 出貨成功後才正式扣除?
|
|
||||||
|
|
||||||
### 3.5 界面展示需求
|
|
||||||
6. **商品資訊**:當 `req6 = 2` 時,回傳的產品清單是否需要包含圖片或詳細描述供機台屏幕顯示?
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
# B710 API (計時器狀態回傳) 技術規範與執行指南
|
|
||||||
|
|
||||||
本文件定義 B710 API 的技術實作規範,用於處理計時型設備(如洗衣服務、按摩服務)的剩餘時間監測與狀態回報。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. 業務流程與資料流
|
|
||||||
|
|
||||||
### 1.1 狀態回報
|
|
||||||
* **回報內容**:機台定期回報特定貨道 (product/timer) 的 `remaining_seconds` (剩餘秒數) 與目前狀態。
|
|
||||||
* **資料用途**:
|
|
||||||
* **即時監控**:供管理後台儀表板顯示設備的使用進度。
|
|
||||||
* **稼動分析**:紀錄設備的總租用時數與閒置率。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 執行面機制 (Execution Strategy)
|
|
||||||
|
|
||||||
* **異步處理**:由於計時狀態可能高頻回報,系統應優先將數據更新至 **Redis 快取**,以確保前端查詢的高效能與資料庫的負擔平衡。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 資料庫結構:`timer_statuses`
|
|
||||||
|
|
||||||
| 欄位 | 說明 | 備註 |
|
|
||||||
|------|------|------|
|
|
||||||
| `machine_id` | 機台 ID | |
|
|
||||||
| `product_id` | 產品/服務 ID | |
|
|
||||||
| `remaining_seconds` | 剩餘秒數 | |
|
|
||||||
| `status` | 運行狀態 | 1:運行中, 0:停止 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 待確認事項 (To-be-confirmed)
|
|
||||||
|
|
||||||
以下議題目前皆列為**待確認**,將於後續開發階段釐清:
|
|
||||||
|
|
||||||
### 4.1 回報頻率與快取
|
|
||||||
1. **更新頻率**:確認機台呼叫 B710 的精確頻率,以及資料庫寫入的節流策略(例如:僅在關鍵變動或每分鐘寫入一次)。
|
|
||||||
|
|
||||||
### 4.2 遠端通知與異常
|
|
||||||
2. **結束通知**:倒數結束時,雲端是否需主動對對消費者發送推播或系統訊息?
|
|
||||||
3. **斷線判定**:針對長期未回報的計時器,雲端應自動判定為結束還是異常?
|
|
||||||
|
|
||||||
### 4.3 復歸機制
|
|
||||||
4. **斷電補償**:機台意外斷電重啟後,是否需向雲端 B010 撈回最後的剩餘秒數以恢復服務?
|
|
||||||
|
|
||||||
### 4.4 報表需求
|
|
||||||
5. **稼動分析**:後台是否需要針對此數據計算「設備利用率」等績效指標。
|
|
||||||
198
docs/members.md
198
docs/members.md
@@ -1,198 +0,0 @@
|
|||||||
# 會員系統(Members)功能說明
|
|
||||||
|
|
||||||
> 此文件記錄會員系統的設計決策與功能說明,供開發與維護時參閱。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
|
|
||||||
會員系統用於智能販賣機商城,支援消費者透過多種社群管道(Line、Google、Facebook)加入會員。
|
|
||||||
|
|
||||||
**重要區分**:
|
|
||||||
- `users` 表:後台管理員登入帳號
|
|
||||||
- `members` 表:前台消費者會員帳號
|
|
||||||
|
|
||||||
兩者**完全獨立**,無關聯。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 資料表
|
|
||||||
|
|
||||||
### 1. `members` - 會員資料
|
|
||||||
|
|
||||||
| 欄位 | 類型 | 說明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `id` | bigint | 主鍵 |
|
|
||||||
| `uuid` | string | 唯一識別碼(對外使用) |
|
|
||||||
| `name` | string | 姓名 |
|
|
||||||
| `email` | string | 電子郵件(可空) |
|
|
||||||
| `phone` | string | 手機號碼(可空) |
|
|
||||||
| `password` | string | 密碼(社群登入可空) |
|
|
||||||
| `birthday` | date | 生日 |
|
|
||||||
| `gender` | enum | 性別 |
|
|
||||||
| `avatar` | string | 頭像 URL |
|
|
||||||
| `is_active` | boolean | 是否啟用 |
|
|
||||||
| `email_verified_at` | timestamp | Email 驗證時間 |
|
|
||||||
|
|
||||||
### 2. `social_accounts` - 社群帳號
|
|
||||||
|
|
||||||
| 欄位 | 類型 | 說明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `id` | bigint | 主鍵 |
|
|
||||||
| `member_id` | bigint | 關聯會員 |
|
|
||||||
| `provider` | enum | line / google / facebook |
|
|
||||||
| `provider_id` | string | 社群平台用戶 ID |
|
|
||||||
| `access_token` | text | 存取令牌 |
|
|
||||||
| `refresh_token` | text | 刷新令牌 |
|
|
||||||
| `profile_data` | json | 社群個人資料 |
|
|
||||||
| `token_expires_at` | timestamp | 令牌到期時間 |
|
|
||||||
|
|
||||||
### 3. `member_wallets` - 會員錢包
|
|
||||||
|
|
||||||
| 欄位 | 類型 | 說明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `member_id` | bigint | FK,唯一 |
|
|
||||||
| `balance` | decimal | 儲值餘額 |
|
|
||||||
| `bonus_balance` | decimal | 回饋金餘額 |
|
|
||||||
|
|
||||||
### 4. `wallet_transactions` - 錢包交易
|
|
||||||
|
|
||||||
| 欄位 | 類型 | 說明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `type` | enum | deposit/consume/refund/bonus/adjust |
|
|
||||||
| `amount` | decimal | 異動金額 |
|
|
||||||
| `balance_after` | decimal | 異動後餘額 |
|
|
||||||
| `reference_type/id` | | 關聯訂單或活動 |
|
|
||||||
|
|
||||||
### 5. `deposit_bonus_rules` - 儲值回饋規則
|
|
||||||
|
|
||||||
設定儲值達指定金額可獲得的回饋(固定金額或百分比)。
|
|
||||||
|
|
||||||
### 6. `member_points` - 點數帳戶
|
|
||||||
|
|
||||||
| 欄位 | 類型 | 說明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `available_points` | int | 可用點數 |
|
|
||||||
| `pending_points` | int | 待生效點數 |
|
|
||||||
| `expired_points` | int | 已過期(統計) |
|
|
||||||
| `used_points` | int | 已使用(統計) |
|
|
||||||
|
|
||||||
### 7. `point_transactions` - 點數異動
|
|
||||||
|
|
||||||
| 欄位 | 類型 | 說明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `type` | enum | earn/use/expire/gift/adjust |
|
|
||||||
| `points` | int | 異動點數 |
|
|
||||||
| `expires_at` | datetime | **此筆點數到期日** |
|
|
||||||
|
|
||||||
> 每筆獲得點數都記錄 `expires_at`,排程任務定期處理過期。
|
|
||||||
|
|
||||||
### 8. `point_rules` - 點數規則
|
|
||||||
|
|
||||||
設定消費/儲值/註冊等行為可獲得的點數及有效天數。
|
|
||||||
|
|
||||||
### 9. `membership_tiers` - 會員等級
|
|
||||||
|
|
||||||
| 欄位 | 類型 | 說明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `name` | string | 等級名稱 |
|
|
||||||
| `annual_fee` | decimal | 年費(0=免費) |
|
|
||||||
| `discount_rate` | decimal | 折扣比例 |
|
|
||||||
| `point_multiplier` | decimal | 點數倍率 |
|
|
||||||
|
|
||||||
### 10. `member_memberships` - 會員等級紀錄
|
|
||||||
|
|
||||||
記錄會員的等級歸屬及有效期間。
|
|
||||||
|
|
||||||
### 11. `gift_definitions` - 禮品定義
|
|
||||||
|
|
||||||
| 欄位 | 類型 | 說明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `type` | enum | points/coupon/product/discount/cash |
|
|
||||||
| `trigger` | enum | register/birthday/annual/upgrade/manual |
|
|
||||||
|
|
||||||
### 12. `member_gifts` - 禮品發放紀錄
|
|
||||||
|
|
||||||
記錄發放給會員的禮品及領取狀態。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ER 關係圖
|
|
||||||
|
|
||||||
```
|
|
||||||
members
|
|
||||||
├── social_accounts (1:N)
|
|
||||||
├── member_wallets (1:1)
|
|
||||||
│ └── wallet_transactions (1:N)
|
|
||||||
├── member_points (1:1)
|
|
||||||
│ └── point_transactions (1:N)
|
|
||||||
├── member_memberships (1:N)
|
|
||||||
│ └── membership_tiers
|
|
||||||
└── member_gifts (1:N)
|
|
||||||
└── gift_definitions
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 登入流程
|
|
||||||
|
|
||||||
```
|
|
||||||
使用者選擇社群登入
|
|
||||||
↓
|
|
||||||
取得 provider + provider_id
|
|
||||||
↓
|
|
||||||
查詢 social_accounts
|
|
||||||
↓
|
|
||||||
┌────┴────┐
|
|
||||||
已綁定 未綁定
|
|
||||||
↓ ↓
|
|
||||||
取得 member 建立新 member + social_account
|
|
||||||
↓ ↓
|
|
||||||
完成登入
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Email 驗證(可選功能)
|
|
||||||
|
|
||||||
若需要 Email 驗證,需設定 `.env` 的 SMTP 並讓 `Member` Model 實作 `MustVerifyEmail`。
|
|
||||||
|
|
||||||
社群登入時自動標記 `email_verified_at`,僅對手機/密碼註冊要求驗證。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## API 端點
|
|
||||||
|
|
||||||
| Method | Endpoint | 說明 | 認證 |
|
|
||||||
|--------|----------|------|------|
|
|
||||||
| POST | `/api/members/register` | 註冊會員 | 否 |
|
|
||||||
| POST | `/api/members/login` | 登入 | 否 |
|
|
||||||
| POST | `/api/members/social-login` | 社群登入 | 否 |
|
|
||||||
| GET | `/api/members/profile` | 取得個人資料 | 是 |
|
|
||||||
| PUT | `/api/members/profile` | 更新個人資料 | 是 |
|
|
||||||
| POST | `/api/members/logout` | 登出 | 是 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Postman 測試
|
|
||||||
|
|
||||||
匯入:`docs/postman/Star_Cloud_Members_API.postman_collection.json`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 社群登入實測
|
|
||||||
|
|
||||||
訪問 `/test/social-login` 測試 Google/Line 登入。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 開發進度
|
|
||||||
|
|
||||||
| 日期 | 項目 | 狀態 |
|
|
||||||
|------|------|------|
|
|
||||||
| 2026-01-12 | 會員核心 (members, social_accounts) | ✅ 完成 |
|
|
||||||
| 2026-01-12 | 錢包系統 (3 表 + 3 Model) | ✅ 完成 |
|
|
||||||
| 2026-01-12 | 點數系統 (3 表 + 3 Model) | ✅ 完成 |
|
|
||||||
| 2026-01-12 | 年度會員 (2 表 + 2 Model) | ✅ 完成 |
|
|
||||||
| 2026-01-12 | 贈送機制 (2 表 + 2 Model) | ✅ 完成 |
|
|
||||||
|
|
||||||
@@ -1,239 +0,0 @@
|
|||||||
{
|
|
||||||
"info": {
|
|
||||||
"name": "Star Cloud - 會員 API",
|
|
||||||
"description": "智能販賣機商城會員系統 API 測試集合",
|
|
||||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
|
||||||
},
|
|
||||||
"variable": [
|
|
||||||
{
|
|
||||||
"key": "base_url",
|
|
||||||
"value": "http://localhost/api",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "token",
|
|
||||||
"value": "",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"item": [
|
|
||||||
{
|
|
||||||
"name": "會員註冊",
|
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Accept",
|
|
||||||
"value": "application/json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"name\": \"測試會員\",\n \"email\": \"test@example.com\",\n \"phone\": \"0912345678\",\n \"password\": \"password123\",\n \"birthday\": \"1990-01-01\",\n \"gender\": \"male\"\n}"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{base_url}}/members/register",
|
|
||||||
"host": [
|
|
||||||
"{{base_url}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"members",
|
|
||||||
"register"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"event": [
|
|
||||||
{
|
|
||||||
"listen": "test",
|
|
||||||
"script": {
|
|
||||||
"exec": [
|
|
||||||
"if (pm.response.code === 201) {",
|
|
||||||
" var jsonData = pm.response.json();",
|
|
||||||
" pm.collectionVariables.set('token', jsonData.data.token);",
|
|
||||||
"}"
|
|
||||||
],
|
|
||||||
"type": "text/javascript"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "會員登入",
|
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Accept",
|
|
||||||
"value": "application/json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"account\": \"test@example.com\",\n \"password\": \"password123\"\n}"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{base_url}}/members/login",
|
|
||||||
"host": [
|
|
||||||
"{{base_url}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"members",
|
|
||||||
"login"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"event": [
|
|
||||||
{
|
|
||||||
"listen": "test",
|
|
||||||
"script": {
|
|
||||||
"exec": [
|
|
||||||
"if (pm.response.code === 200) {",
|
|
||||||
" var jsonData = pm.response.json();",
|
|
||||||
" pm.collectionVariables.set('token', jsonData.data.token);",
|
|
||||||
"}"
|
|
||||||
],
|
|
||||||
"type": "text/javascript"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "社群登入",
|
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Accept",
|
|
||||||
"value": "application/json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"provider\": \"line\",\n \"provider_id\": \"U1234567890abcdef\",\n \"access_token\": \"test_access_token\",\n \"name\": \"Line 用戶\",\n \"email\": \"line@example.com\",\n \"avatar\": \"https://example.com/avatar.jpg\"\n}"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{base_url}}/members/social-login",
|
|
||||||
"host": [
|
|
||||||
"{{base_url}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"members",
|
|
||||||
"social-login"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"event": [
|
|
||||||
{
|
|
||||||
"listen": "test",
|
|
||||||
"script": {
|
|
||||||
"exec": [
|
|
||||||
"if (pm.response.code === 200) {",
|
|
||||||
" var jsonData = pm.response.json();",
|
|
||||||
" pm.collectionVariables.set('token', jsonData.data.token);",
|
|
||||||
"}"
|
|
||||||
],
|
|
||||||
"type": "text/javascript"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "取得個人資料",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Accept",
|
|
||||||
"value": "application/json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "Authorization",
|
|
||||||
"value": "Bearer {{token}}"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{base_url}}/members/profile",
|
|
||||||
"host": [
|
|
||||||
"{{base_url}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"members",
|
|
||||||
"profile"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "更新個人資料",
|
|
||||||
"request": {
|
|
||||||
"method": "PUT",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Accept",
|
|
||||||
"value": "application/json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "Authorization",
|
|
||||||
"value": "Bearer {{token}}"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"name\": \"更新後的名字\",\n \"birthday\": \"1995-06-15\",\n \"gender\": \"female\"\n}"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{base_url}}/members/profile",
|
|
||||||
"host": [
|
|
||||||
"{{base_url}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"members",
|
|
||||||
"profile"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "登出",
|
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Accept",
|
|
||||||
"value": "application/json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "Authorization",
|
|
||||||
"value": "Bearer {{token}}"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{base_url}}/members/logout",
|
|
||||||
"host": [
|
|
||||||
"{{base_url}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"members",
|
|
||||||
"logout"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
# Star Cloud 多租戶與權限架構 (Multi-Tenant RBAC) 實作規劃
|
|
||||||
|
|
||||||
## 1. 架構目標
|
|
||||||
為了支援 Star Cloud 系統具備 B2B SaaS 的多租戶特性,讓「系統管理員(內部營運)」與「租戶(客戶公司)」能在同一套後台中共存。各租戶能自行建立子帳號並分配權限,且資料彼此絕對隔離。
|
|
||||||
|
|
||||||
將導入基於 `companies` 的租戶隔離,並結合 `spatie/laravel-permission` 實作多租戶的角色權限控制。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 核心實作架構
|
|
||||||
|
|
||||||
### 2.1 租戶隔離層 (Tenant Isolation)
|
|
||||||
建立實體的租戶邊界,確保每個客戶在物理與邏輯上只能存取自己的資料。
|
|
||||||
* **變更**:新增 `companies` 表(公司名稱、統編、狀態等)。
|
|
||||||
* **綁定**:在 `users`, `machines`, 等核心資源表中加上 `company_id`。
|
|
||||||
* **身份判定**:
|
|
||||||
* `company_id = null`:系統預設管理員(內部工程師 / 營運人員),具備跨公司存取能力。
|
|
||||||
* `company_id = N`:客戶端帳號(租戶)。
|
|
||||||
|
|
||||||
### 2.2 角色與權限層 (Roles & Permissions)
|
|
||||||
利用 `spatie/laravel-permission` 結合多租戶。
|
|
||||||
* **全域權限 (Permissions)**:定義系統所有的最小操作單位(如 `machine.view`, `user.create`),所有租戶共用權限定義。
|
|
||||||
* **多租戶角色 (Tenant-Aware Roles)**:原本套件的 `roles` 表必須擴充加上 `company_id` 欄位。
|
|
||||||
* 系統預設角色 (`company_id = null`):如 Super Admin。
|
|
||||||
* 客戶自建角色 (`company_id = N`):如客戶自定義的「台北區維修員」,只能分配給統一個 `company_id` 的使用者。
|
|
||||||
|
|
||||||
### 2.3 流程控制與防禦 (Flow Control)
|
|
||||||
* **自動過濾 (Global Scopes)**:寫一個 Trait `TenantScoped`,當非內部人員登入時,自動幫 Eloquent Model 的所有查詢加上 `where('company_id', auth()->user()->company_id)`。
|
|
||||||
* **越權防禦 (Create User)**:租戶在後台建立「新子帳號」時,API Controller 必須在背景強制 `NewUser->company_id = Auth::user()->company_id`。
|
|
||||||
* **角色分配防禦**:客戶分配角色給員工時,只能撈取並指定 `company_id` 與自己相同的 Role。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 實作步驟 (對應 Phase 1 與 Phase 3)
|
|
||||||
|
|
||||||
### 步驟 1:資料庫結構 (Migrations)
|
|
||||||
1. 建立 `create_companies_table` migration。
|
|
||||||
2. 建立 `add_company_id_to_users_table` migration。
|
|
||||||
3. 調整 `machines` 及其他資源表,加入 `company_id` 欄位與外鍵。
|
|
||||||
4. 安裝並發布 `spatie/laravel-permission` migrations,並接續寫一個 migration 為 `roles` 和 `permissions` 調整結構 (加入 `company_id` 至 `roles`)。
|
|
||||||
|
|
||||||
### 步驟 2:Eloquent Models 與 Scopes 修改
|
|
||||||
1. 建立 `Company` Model (`App\Models\System\Company`)。
|
|
||||||
2. 修改 `User` Model (`App\Models\Member\User`),加入 `company()` 關聯與 Helper methods (`isSystemAdmin()`, `isTenant()`)。
|
|
||||||
3. 建立 `TenantScoped` Trait,包含 Laravel Global Scope 邏輯。套用至 `Machine` 等需要隔離的 Model。
|
|
||||||
|
|
||||||
### 步驟 3:後端流程與介面防禦 (Middleware & Controllers)
|
|
||||||
1. 建立 `CheckTenantAccess` Middleware 進行基礎的路由存取權限卡控。
|
|
||||||
2. 在後台的「帳號與權限」模組中,實作過濾邏輯:
|
|
||||||
* 撈取使用者、角色列表時帶入 `company_id`。
|
|
||||||
* 創建或修改時強制綁定 `Auth::user()->company_id`。
|
|
||||||
|
|
||||||
### 步驟 4:前端 Blade 呈現切換
|
|
||||||
1. 使用 Blade 的 `@if(auth()->user()->isSystemAdmin())` 在 Sidebar 中隱藏或顯示「全站管理」、「合約設定」等專屬區塊。
|
|
||||||
2. 確保一般租戶登入時,左側選單僅顯示其有權限存取的「機台管理」、「我的專屬會員」、「所屬帳號設定」等區塊。
|
|
||||||
214
package-lock.json
generated
214
package-lock.json
generated
@@ -1,18 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "html",
|
"name": "star-cloud",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@alpinejs/collapse": "^3.15.3",
|
|
||||||
"@tailwindcss/forms": "^0.5.2",
|
"@tailwindcss/forms": "^0.5.2",
|
||||||
"alpinejs": "^3.4.2",
|
"alpinejs": "^3.4.2",
|
||||||
"autoprefixer": "^10.4.2",
|
"autoprefixer": "^10.4.2",
|
||||||
"axios": "^1.6.4",
|
"axios": "^1.6.4",
|
||||||
"laravel-vite-plugin": "^1.0.0",
|
"laravel-vite-plugin": "^1.0.0",
|
||||||
"postcss": "^8.4.31",
|
"postcss": "^8.4.31",
|
||||||
"preline": "^3.2.3",
|
|
||||||
"tailwindcss": "^3.1.0",
|
"tailwindcss": "^3.1.0",
|
||||||
"vite": "^5.0.0"
|
"vite": "^5.0.0"
|
||||||
}
|
}
|
||||||
@@ -30,13 +28,6 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@alpinejs/collapse": {
|
|
||||||
"version": "3.15.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@alpinejs/collapse/-/collapse-3.15.3.tgz",
|
|
||||||
"integrity": "sha512-nheS20BsFY1Eh1nyW0YNs7RMOiO/LipCTltEplbWunTcgdCeZtD7YPUim5xtbhc+0nJP4SkR7G0axRXaRf4m1g==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.21.5",
|
"version": "0.21.5",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
||||||
@@ -428,34 +419,6 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@floating-ui/core": {
|
|
||||||
"version": "1.7.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
|
|
||||||
"integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@floating-ui/utils": "^0.2.10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@floating-ui/dom": {
|
|
||||||
"version": "1.7.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
|
|
||||||
"integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@floating-ui/core": "^1.7.3",
|
|
||||||
"@floating-ui/utils": "^0.2.10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@floating-ui/utils": {
|
|
||||||
"version": "0.2.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
|
|
||||||
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@jridgewell/gen-mapping": {
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
"version": "0.3.13",
|
"version": "0.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||||
@@ -841,74 +804,6 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@svgdotjs/svg.draggable.js": {
|
|
||||||
"version": "3.0.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/@svgdotjs/svg.draggable.js/-/svg.draggable.js-3.0.6.tgz",
|
|
||||||
"integrity": "sha512-7iJFm9lL3C40HQcqzEfezK2l+dW2CpoVY3b77KQGqc8GXWa6LhhmX5Ckv7alQfUXBuZbjpICZ+Dvq1czlGx7gA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"peerDependencies": {
|
|
||||||
"@svgdotjs/svg.js": "^3.2.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@svgdotjs/svg.filter.js": {
|
|
||||||
"version": "3.0.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/@svgdotjs/svg.filter.js/-/svg.filter.js-3.0.9.tgz",
|
|
||||||
"integrity": "sha512-/69XMRCDoam2HgC4ldHIaDgeQf1ViHIsa0Ld4uWgiXtZ+E24DWHe/9Ib6kbNiZ7WRIdlVokUDR1Fg0kjIpkfbw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@svgdotjs/svg.js": "^3.2.4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@svgdotjs/svg.js": {
|
|
||||||
"version": "3.2.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@svgdotjs/svg.js/-/svg.js-3.2.5.tgz",
|
|
||||||
"integrity": "sha512-/VNHWYhNu+BS7ktbYoVGrCmsXDh+chFMaONMwGNdIBcFHrWqk2jY8fNyr3DLdtQUIalvkPfM554ZSFa3dm3nxQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/Fuzzyma"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@svgdotjs/svg.resize.js": {
|
|
||||||
"version": "2.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@svgdotjs/svg.resize.js/-/svg.resize.js-2.0.5.tgz",
|
|
||||||
"integrity": "sha512-4heRW4B1QrJeENfi7326lUPYBCevj78FJs8kfeDxn5st0IYPIRXoTtOSYvTzFWgaWWXd3YCDE6ao4fmv91RthA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 14.18"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@svgdotjs/svg.js": "^3.2.4",
|
|
||||||
"@svgdotjs/svg.select.js": "^4.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@svgdotjs/svg.select.js": {
|
|
||||||
"version": "4.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@svgdotjs/svg.select.js/-/svg.select.js-4.0.3.tgz",
|
|
||||||
"integrity": "sha512-qkMgso1sd2hXKd1FZ1weO7ANq12sNmQJeGDjs46QwDVsxSRcHmvWKL2NDF7Yimpwf3sl5esOLkPqtV2bQ3v/Jg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 14.18"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@svgdotjs/svg.js": "^3.2.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@swc/helpers": {
|
|
||||||
"version": "0.2.14",
|
|
||||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.2.14.tgz",
|
|
||||||
"integrity": "sha512-wpCQMhf5p5GhNg2MmGKXzUNwxe7zRiCsmqYsamez2beP7mKPCSiu+BjZcdN95yYSzO857kr0VfQewmGpS77nqA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@tailwindcss/forms": {
|
"node_modules/@tailwindcss/forms": {
|
||||||
"version": "0.5.10",
|
"version": "0.5.10",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.10.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.10.tgz",
|
||||||
@@ -946,13 +841,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@yr/monotone-cubic-spline": {
|
|
||||||
"version": "1.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz",
|
|
||||||
"integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/alpinejs": {
|
"node_modules/alpinejs": {
|
||||||
"version": "3.15.2",
|
"version": "3.15.2",
|
||||||
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.2.tgz",
|
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.2.tgz",
|
||||||
@@ -984,21 +872,6 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/apexcharts": {
|
|
||||||
"version": "4.7.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-4.7.0.tgz",
|
|
||||||
"integrity": "sha512-iZSrrBGvVlL+nt2B1NpqfDuBZ9jX61X9I2+XV0hlYXHtTwhwLTHDKGXjNXAgFBDLuvSYCB/rq2nPWVPRv2DrGA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@svgdotjs/svg.draggable.js": "^3.0.4",
|
|
||||||
"@svgdotjs/svg.filter.js": "^3.0.8",
|
|
||||||
"@svgdotjs/svg.js": "^3.2.4",
|
|
||||||
"@svgdotjs/svg.resize.js": "^2.0.2",
|
|
||||||
"@svgdotjs/svg.select.js": "^4.0.1",
|
|
||||||
"@yr/monotone-cubic-spline": "^1.0.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/arg": {
|
"node_modules/arg": {
|
||||||
"version": "5.0.2",
|
"version": "5.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
|
||||||
@@ -1119,6 +992,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.8.25",
|
"baseline-browser-mapping": "^2.8.25",
|
||||||
"caniuse-lite": "^1.0.30001754",
|
"caniuse-lite": "^1.0.30001754",
|
||||||
@@ -1252,27 +1126,6 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/datatables.net": {
|
|
||||||
"version": "2.3.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/datatables.net/-/datatables.net-2.3.6.tgz",
|
|
||||||
"integrity": "sha512-xQ/dCxrjfxM0XY70wSIzakkTZ6ghERwlLmAPyCnu8Sk5cyt9YvOVyOsFNOa/BZ/lM63Q3i2YSSvp/o7GXZGsbg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"jquery": ">=1.7"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/datatables.net-dt": {
|
|
||||||
"version": "2.3.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/datatables.net-dt/-/datatables.net-dt-2.3.6.tgz",
|
|
||||||
"integrity": "sha512-8OEUNCEfkeW+TuVUDlT1q6/XXOitgVzCdNqBivw8bK9DnaNk5F6JjT8lE2pQ4uAfoL/dTy2J+HKxTHeTh8HJlg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"datatables.net": "2.3.6",
|
|
||||||
"jquery": ">=1.7"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/delayed-stream": {
|
"node_modules/delayed-stream": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
@@ -1297,17 +1150,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/dropzone": {
|
|
||||||
"version": "6.0.0-beta.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/dropzone/-/dropzone-6.0.0-beta.2.tgz",
|
|
||||||
"integrity": "sha512-k44yLuFFhRk53M8zP71FaaNzJYIzr99SKmpbO/oZKNslDjNXQsBTdfLs+iONd0U0L94zzlFzRnFdqbLcs7h9fQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@swc/helpers": "^0.2.13",
|
|
||||||
"just-extend": "^5.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/dunder-proto": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
@@ -1733,24 +1575,11 @@
|
|||||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"jiti": "bin/jiti.js"
|
"jiti": "bin/jiti.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jquery": {
|
|
||||||
"version": "3.7.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
|
|
||||||
"integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/just-extend": {
|
|
||||||
"version": "5.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/just-extend/-/just-extend-5.1.1.tgz",
|
|
||||||
"integrity": "sha512-b+z6yF1d4EOyDgylzQo5IminlUmzSeqR1hs/bzjBNjuGras4FXq/6TrzjxfN0j+TmI0ltJzTNlqXUMCniciwKQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/laravel-vite-plugin": {
|
"node_modules/laravel-vite-plugin": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-1.3.0.tgz",
|
||||||
@@ -1916,13 +1745,6 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nouislider": {
|
|
||||||
"version": "15.8.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/nouislider/-/nouislider-15.8.1.tgz",
|
|
||||||
"integrity": "sha512-93TweAi8kqntHJSPiSWQ1o/uZ29VWOmal9YKb6KKGGlCkugaNfAupT7o1qTHqdJvNQ7S0su5rO6qRFCjP8fxtw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/object-assign": {
|
"node_modules/object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
@@ -2010,6 +1832,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@@ -2153,21 +1976,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/preline": {
|
|
||||||
"version": "3.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/preline/-/preline-3.2.3.tgz",
|
|
||||||
"integrity": "sha512-S13MFdC/1FWFz3S+oW1PlyZ6Alo0SZxJ9HwaZRg5IQZjcbKqCFIOXAbAhQeX0izauqWJXIQdKofhfCWBizwleQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Licensed under MIT and Preline UI Fair Use License",
|
|
||||||
"dependencies": {
|
|
||||||
"@floating-ui/dom": "^1.6.13",
|
|
||||||
"apexcharts": "^4.5.0",
|
|
||||||
"datatables.net-dt": "^2.2.2",
|
|
||||||
"dropzone": "^6.0.0-beta.2",
|
|
||||||
"nouislider": "^15.8.1",
|
|
||||||
"vanilla-calendar-pro": "^3.0.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/proxy-from-env": {
|
"node_modules/proxy-from-env": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
@@ -2369,6 +2177,7 @@
|
|||||||
"integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==",
|
"integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alloc/quick-lru": "^5.2.0",
|
"@alloc/quick-lru": "^5.2.0",
|
||||||
"arg": "^5.0.2",
|
"arg": "^5.0.2",
|
||||||
@@ -2465,6 +2274,7 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -2530,23 +2340,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/vanilla-calendar-pro": {
|
|
||||||
"version": "3.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/vanilla-calendar-pro/-/vanilla-calendar-pro-3.1.0.tgz",
|
|
||||||
"integrity": "sha512-yXDtCaedcKz6i5OOdWGwui0C8MAmjXjj7JzKZyjDlkczSRqnhI8BDGFygqT2K+qL1uY7R2fLYlTlxA6oyFs2yg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"type": "individual",
|
|
||||||
"url": "https://buymeacoffee.com/uvarov"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "5.4.21",
|
"version": "5.4.21",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
|
||||||
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.21.3",
|
"esbuild": "^0.21.3",
|
||||||
"postcss": "^8.4.43",
|
"postcss": "^8.4.43",
|
||||||
|
|||||||
@@ -6,14 +6,12 @@
|
|||||||
"build": "vite build"
|
"build": "vite build"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@alpinejs/collapse": "^3.15.3",
|
|
||||||
"@tailwindcss/forms": "^0.5.2",
|
"@tailwindcss/forms": "^0.5.2",
|
||||||
"alpinejs": "^3.4.2",
|
"alpinejs": "^3.4.2",
|
||||||
"autoprefixer": "^10.4.2",
|
"autoprefixer": "^10.4.2",
|
||||||
"axios": "^1.6.4",
|
"axios": "^1.6.4",
|
||||||
"laravel-vite-plugin": "^1.0.0",
|
"laravel-vite-plugin": "^1.0.0",
|
||||||
"postcss": "^8.4.31",
|
"postcss": "^8.4.31",
|
||||||
"preline": "^3.2.3",
|
|
||||||
"tailwindcss": "^3.1.0",
|
"tailwindcss": "^3.1.0",
|
||||||
"vite": "^5.0.0"
|
"vite": "^5.0.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,8 +21,8 @@
|
|||||||
<env name="APP_ENV" value="testing"/>
|
<env name="APP_ENV" value="testing"/>
|
||||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||||
<env name="CACHE_DRIVER" value="array"/>
|
<env name="CACHE_DRIVER" value="array"/>
|
||||||
<env name="DB_CONNECTION" value="sqlite"/>
|
<!-- <env name="DB_CONNECTION" value="sqlite"/> -->
|
||||||
<env name="DB_DATABASE" value=":memory:"/>
|
<!-- <env name="DB_DATABASE" value=":memory:"/> -->
|
||||||
<env name="MAIL_MAILER" value="array"/>
|
<env name="MAIL_MAILER" value="array"/>
|
||||||
<env name="PULSE_ENABLED" value="false"/>
|
<env name="PULSE_ENABLED" value="false"/>
|
||||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||||
|
|||||||
@@ -1,20 +1,55 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Foundation\Application;
|
use Illuminate\Contracts\Http\Kernel;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
define('LARAVEL_START', microtime(true));
|
define('LARAVEL_START', microtime(true));
|
||||||
|
|
||||||
// Determine if the application is in maintenance mode...
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Check If The Application Is Under Maintenance
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| If the application is in maintenance / demo mode via the "down" command
|
||||||
|
| we will load this file so that any pre-rendered content can be shown
|
||||||
|
| instead of starting the framework, which could cause an exception.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
|
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
|
||||||
require $maintenance;
|
require $maintenance;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register the Composer autoloader...
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Register The Auto Loader
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Composer provides a convenient, automatically generated class loader for
|
||||||
|
| this application. We just need to utilize it! We'll simply require it
|
||||||
|
| into the script here so we don't need to manually load our classes.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
require __DIR__.'/../vendor/autoload.php';
|
require __DIR__.'/../vendor/autoload.php';
|
||||||
|
|
||||||
// Bootstrap Laravel and handle the request...
|
/*
|
||||||
/** @var Application $app */
|
|--------------------------------------------------------------------------
|
||||||
|
| Run The Application
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Once we have the application, we can handle the incoming request using
|
||||||
|
| the application's HTTP kernel. Then, we will send the response back
|
||||||
|
| to this client's browser, allowing them to enjoy our application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
$app = require_once __DIR__.'/../bootstrap/app.php';
|
$app = require_once __DIR__.'/../bootstrap/app.php';
|
||||||
|
|
||||||
$app->handleRequest(Request::capture());
|
$kernel = $app->make(Kernel::class);
|
||||||
|
|
||||||
|
$response = $kernel->handle(
|
||||||
|
$request = Request::capture()
|
||||||
|
)->send();
|
||||||
|
|
||||||
|
$kernel->terminate($request, $response);
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
$dirs = ['tests', 'database', 'app'];
|
|
||||||
$files = [];
|
|
||||||
|
|
||||||
foreach ($dirs as $dirName) {
|
|
||||||
// Note: sail runs in /var/www/html
|
|
||||||
if (!is_dir(__DIR__ . '/' . $dirName)) continue;
|
|
||||||
$dir = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(__DIR__ . '/' . $dirName));
|
|
||||||
foreach ($dir as $file) {
|
|
||||||
if ($file->isFile() && $file->getExtension() === 'php') {
|
|
||||||
$files[] = $file->getPathname();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$replacements = [
|
|
||||||
'App\Models\User' => 'App\Models\System\User',
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($files as $file) {
|
|
||||||
$content = file_get_contents($file);
|
|
||||||
$original = $content;
|
|
||||||
|
|
||||||
foreach ($replacements as $old => $new) {
|
|
||||||
$content = str_replace($old, $new, $content);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($content !== $original) {
|
|
||||||
file_put_contents($file, $content);
|
|
||||||
echo "Updated: $file\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
echo "Done.\n";
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user