feat: 實作機台日誌核心功能與 IoT 高併發處理架構
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 36s
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 36s
This commit is contained in:
64
app/Console/Commands/Machine/SimulateMachineLogs.php
Normal file
64
app/Console/Commands/Machine/SimulateMachineLogs.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?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.');
|
||||
}
|
||||
}
|
||||
10
app/Http/Controllers/Admin/AdminController.php
Normal file
10
app/Http/Controllers/Admin/AdminController.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
abstract class AdminController extends Controller
|
||||
{
|
||||
// Admin 相關的共用邏輯可寫於此
|
||||
}
|
||||
@@ -2,142 +2,36 @@
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Machine;
|
||||
use App\Models\Machine\Machine;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class MachineController extends Controller
|
||||
class MachineController extends AdminController
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
* 顯示所有機台列表
|
||||
*/
|
||||
public function index()
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$machines = Machine::latest()->paginate(10);
|
||||
$machines = Machine::query()
|
||||
->when($request->status, function ($query, $status) {
|
||||
return $query->where('status', $status);
|
||||
})
|
||||
->latest()
|
||||
->paginate(10);
|
||||
|
||||
return view('admin.machines.index', compact('machines'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new resource.
|
||||
* 顯示特定機台的日誌與詳細資訊
|
||||
*/
|
||||
public function create()
|
||||
public function show(int $id): View
|
||||
{
|
||||
return view('admin.machines.create');
|
||||
}
|
||||
$machine = Machine::with(['logs' => function ($query) {
|
||||
$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'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified resource.
|
||||
*/
|
||||
public function edit(Machine $machine)
|
||||
{
|
||||
return view('admin.machines.edit', compact('machine'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(Request $request, Machine $machine)
|
||||
{
|
||||
$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 destroy(Machine $machine)
|
||||
{
|
||||
$machine->delete();
|
||||
|
||||
return redirect()->route('admin.machines.index')
|
||||
->with('success', '機台已刪除');
|
||||
}
|
||||
|
||||
// 機台日誌
|
||||
public function logs()
|
||||
{
|
||||
return view('admin.placeholder', [
|
||||
'title' => '機台日誌',
|
||||
'description' => '機台操作歷史紀錄回溯',
|
||||
'features' => [
|
||||
'操作時間戳記',
|
||||
'事件類型分類',
|
||||
'操作人員記錄',
|
||||
'詳細描述查詢',
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
// 機台權限
|
||||
public function permissions()
|
||||
{
|
||||
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' => '機台維修工單系統',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
11
app/Http/Controllers/Api/V1/ApiController.php
Normal file
11
app/Http/Controllers/Api/V1/ApiController.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Traits\ApiResponse;
|
||||
|
||||
abstract class ApiController extends Controller
|
||||
{
|
||||
use ApiResponse;
|
||||
}
|
||||
39
app/Http/Controllers/Api/V1/MachineController.php
Normal file
39
app/Http/Controllers/Api/V1/MachineController.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use App\Models\System\User;
|
||||
use App\Providers\RouteServiceProvider;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\System\User;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
|
||||
46
app/Jobs/Machine/ProcessMachineLog.php
Normal file
46
app/Jobs/Machine/ProcessMachineLog.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?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,12 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Models\Machine;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Machine extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'location',
|
||||
@@ -1,12 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Models\Machine;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class MachineLog extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
const UPDATED_AT = null;
|
||||
|
||||
protected $fillable = [
|
||||
'machine_id',
|
||||
'level',
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Models\Member;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Models\Member;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Models\Member;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Models\Member;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Models\Member;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Models\Member;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Models\Member;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Models\Member;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Models\Member;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Models\Member;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Models\Member;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Models\Member;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Models\System;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace App\Models\System;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
45
app/Services/Machine/MachineService.php
Normal file
45
app/Services/Machine/MachineService.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?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';
|
||||
}
|
||||
}
|
||||
49
app/Traits/ApiResponse.php
Normal file
49
app/Traits/ApiResponse.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user