Files
star-cloud/app/Http/Controllers/Api/V1/App/MachineController.php
sky121113 f2147ae6c4
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 1m4s
[FEAT] 完善 IoT API 規范化、機台管理介面優化與 B005 改為 GET
1. 將 B005 (廣告同步) 從 POST 改為 GET,符合 RESTful 規範。
2. 完善 B009 (庫存回報) 回應規格,加入業務代碼 (200 OK)。
3. API 文件 UI 優化:新增 Method Badge (方法標籤),並修正 JSON 中文/斜線轉義問題。
4. 機台管理介面優化:實作「唯讀庫存與效期」面板,並將日誌圖示改為「👁️」。
5. 標準化 ID 識別邏輯:資料表全面移除對 sku 的依賴,改以 id 為主、barcode 為輔。
6. 新增 Migration:正式移除 sku 欄位並同步 barcode 指向。
7. 更新多語系支援 (zh_TW, en, ja)。
2026-04-07 14:37:57 +08:00

334 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Http\Controllers\Api\V1\App;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Machine\Machine;
use App\Models\System\User;
use App\Jobs\Machine\ProcessHeartbeat;
use App\Jobs\Machine\ProcessTimerStatus;
use App\Jobs\Machine\ProcessCoinInventory;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Validator;
class MachineController extends Controller
{
/**
* B010: Machine Heartbeat & Status Update (Asynchronous)
*/
public function heartbeat(Request $request)
{
$machine = $request->get('machine');
$data = $request->except(['machine', 'key']); // 排除 Middleware 注入的 Model 物件與認證 key
// 異步處理狀態更新
ProcessHeartbeat::dispatch($machine->serial_no, $data);
// 取出待處理指令
$command = \App\Models\Machine\RemoteCommand::where('machine_id', $machine->id)
->pending()
->first();
$status = '49'; // 預設 49 (OK / No Command)
$message = 'OK';
if ($command) {
switch ($command->command_type) {
case 'reboot':
$status = '51';
$message = 'reboot';
break;
case 'reboot_card':
$status = '60';
$message = 'reboot card machine';
break;
case 'checkout':
$status = '61';
$message = 'checkout';
break;
case 'lock':
$status = '71';
$message = 'lock';
break;
case 'unlock':
$status = '70';
$message = 'unlock';
break;
case 'change':
$status = '82';
$message = $command->payload['amount'] ?? '0';
break;
case 'dispense':
$status = '85';
$message = $command->payload['slot_no'] ?? '';
break;
case 'reload_stock':
$status = '49';
$message = 'reload B017';
break;
}
// 標記為已發送 (sent)
$command->update(['status' => 'sent', 'executed_at' => now()]);
}
return response()->json([
'success' => true,
'code' => 200,
'message' => $message,
'status' => $status
], 202); // 202 Accepted
}
/**
* B018: Record Machine Restock/Setup Report (Asynchronous)
*/
public function recordRestock(Request $request)
{
$machine = $request->get('machine');
$data = $request->except(['machine', 'key']); // 排除 Middleware 注入物件
$data['serial_no'] = $machine->serial_no;
\App\Jobs\Machine\ProcessRestockReport::dispatch($data);
return response()->json([
'success' => true,
'code' => 200,
'message' => 'Restock report accepted',
'status' => '49'
], 202);
}
/**
* B017: Get Slot Info & Stock (Synchronous)
*/
public function getSlots(Request $request)
{
$machine = $request->get('machine');
$slots = $machine->slots()->with('product')->get();
// 自動轉 Success: 若機台來撈 B017代表之前的 reload_stock 指令已成功被機台響應
\App\Models\Machine\RemoteCommand::where('machine_id', $machine->id)
->where('command_type', 'reload_stock')
->where('status', 'sent')
->update(['status' => 'success', 'executed_at' => now()]);
return response()->json([
'success' => true,
'code' => 200,
'data' => $slots->map(function ($slot) {
return [
'slot_no' => $slot->slot_no,
'product_id' => $slot->product_id,
'stock' => $slot->stock,
'capacity' => $slot->capacity,
'price' => $slot->price,
'status' => $slot->status,
'expiry_date' => $slot->expiry_date,
'batch_no' => $slot->batch_no,
];
})
]);
}
/**
* B710: Sync Timer status (Asynchronous)
*/
public function syncTimer(Request $request)
{
$machine = $request->get('machine');
$data = $request->except(['machine', 'key']); // 排除 Middleware 注入物件
ProcessTimerStatus::dispatch($machine->serial_no, $data);
return response()->json(['success' => true], 202);
}
/**
* B220: Sync Coin Inventory (Asynchronous)
*/
public function syncCoinInventory(Request $request)
{
$machine = $request->get('machine');
$data = $request->except(['machine', 'key']); // 排除 Middleware 注入物件
ProcessCoinInventory::dispatch($machine->serial_no, $data);
return response()->json(['success' => true], 202);
}
/**
* B650: Verify Member Code/Barcode (Synchronous)
*/
public function verifyMember(Request $request)
{
$validator = Validator::make($request->all(), [
'code' => 'required|string',
]);
if ($validator->fails()) {
return response()->json(['success' => false, 'message' => 'Invalid code'], 400);
}
$code = $request->input('code');
// 搜尋會員 (barcode 或特定驗證碼)
$member = \App\Models\Member\Member::where('barcode', $code)
->orWhere('id', $code) // 暫時支援 ID
->first();
if (!$member) {
return response()->json([
'success' => false,
'code' => 404,
'message' => 'Member not found'
], 404);
}
return response()->json([
'success' => true,
'code' => 200,
'data' => [
'member_id' => $member->id,
'name' => $member->name,
'points' => $member->points,
'wallet_balance' => $member->wallet_balance ?? 0,
]
]);
}
/**
* B005: Download Machine Advertisements (Synchronous)
*/
public function getAdvertisements(Request $request)
{
$machine = $request->get('machine');
$advertisements = \App\Models\Machine\MachineAdvertisement::where('machine_id', $machine->id)
->with([
'advertisement' => function ($query) {
$query->active();
}
])
->get()
->filter(fn($ma) => $ma->advertisement !== null)
->map(function ($ma) {
// 定義顯示順序權重 (待機 > 購物 > 成功禮)
$posWeight = [
'standby' => 1,
'vending' => 2,
'visit_gift' => 3
];
// 為了相容現有機台 App 邏輯:
// App 讀取 t070v03 作為位置標籤 (flag)
// 1. HomeActivity (待機) 讀取 "3"
// 2. FontendActivity (販賣頁) 讀取 "1"
$posIdMap = [
'standby' => '3',
'vending' => '1',
'visit_gift' => '2'
];
return [
't070v01' => $ma->advertisement->name,
't070v02' => (string) ($ma->advertisement->duration ?? 15), // 秒數改放這裡
't070v03' => (string) ($posIdMap[$ma->position] ?? '1'), // 位置數字改放這裡 (App 會讀這欄當 Flag)
't070v04' => $ma->advertisement->url,
't070v05' => (string) $ma->sort_order,
'raw_pos_weight' => $posWeight[$ma->position] ?? 99,
'raw_sort' => (int) $ma->sort_order
];
})
->sortBy([
['raw_pos_weight', 'asc'],
['raw_sort', 'asc']
])
->values()
->map(function ($item) {
unset($item['raw_pos_weight'], $item['raw_sort']);
return $item;
});
return response()->json([
'success' => true,
'code' => 200,
'data' => $advertisements->values()
]);
}
/**
* B009: Report Machine Slot List / Supplementary (Synchronous)
*/
public function reportSlotList(Request $request, \App\Services\Machine\MachineService $machineService)
{
$machine = $request->get('machine');
$payload = $request->all();
$account = $payload['account'] ?? null;
// 1. 驗證帳號是否存在 (驗證執行補貨的人員身分)
$user = User::where('username', $account)
->orWhere('email', $account)
->first();
if (!$user) {
return response()->json([
'success' => false,
'code' => 403,
'message' => 'Unauthorized: Account not found',
'status' => ''
], 403);
}
// 2. 階層式權限驗證 (遵循 RBAC 多租戶規範)
$isAuthorized = false;
if ($user->isSystemAdmin()) {
// [系統層]:系統管理員可異動所有機台
$isAuthorized = true;
} elseif ($user->is_admin) {
// [公司層]:公司管理員需驗證此機台是否隸屬於該公司
$isAuthorized = ($machine->company_id === $user->company_id);
} else {
// [人員層]:一般人員需檢查 machine_user 授權表
$isAuthorized = $user->machines()->where('machine_id', $machine->id)->exists();
}
if (!$isAuthorized) {
return response()->json([
'success' => false,
'code' => 403,
'message' => 'Unauthorized: Account not authorized for this machine',
'status' => ''
], 403);
}
// 3. 映射舊版機台回傳格式 (Map legacy machine format)
// 支持單個物件 data: {} 或 陣列 data: [{}] (Handle both single object and array)
$legacyData = $payload['data'] ?? [];
if (Arr::isAssoc($legacyData)) {
$legacyData = [$legacyData];
}
$mappedSlots = array_map(function ($item) {
return [
'slot_no' => $item['tid'] ?? null,
'product_id' => $item['t060v00'] ?? null,
'stock' => $item['num'] ?? 0,
];
}, $legacyData);
// 過濾無效資料 (Filter invalid entries)
$mappedSlots = array_filter($mappedSlots, fn($s) => $s['slot_no'] !== null);
// 同步處理更新庫存 (直接更新不進隊列)
$machineService->syncSlots($machine, $mappedSlots);
return response()->json([
'success' => true,
'code' => 200,
'message' => 'Slot report synchronized success',
'status' => '49'
]);
}
}