Files
star-cloud/app/Http/Controllers/Api/V1/App/MachineController.php
sky121113 b60afc3abe
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 1m7s
[FIX] 修正與標準化 B005 廣告下載 API 以相容既有 Android App
1. 修改 MachineController@getAdvertisements 回傳欄位,將 t070v02 改為播放秒數,t070v03 改為位置代碼 (Flag)。
2. 根據 Android App 原始碼分析結果,調整位置對應:3 為待機廣告 (HomeActivity),1 為販賣頁廣告 (FontendActivity)。
3. 在 AdvertisementController@getMachineAds 增加 sort_order 排序,確保後台管理介面視圖與 API 輸出順序一致。
4. 更新廣告下載 API 的技術文件 (SKILL.md),明確標記欄位用途與位置代碼。
5. 在 routes/api.php 補上 B005 與 B009 的路由定義。
2026-04-07 11:41:24 +08:00

289 lines
9.5 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\Jobs\Machine\ProcessHeartbeat;
use App\Jobs\Machine\ProcessTimerStatus;
use App\Jobs\Machine\ProcessCoinInventory;
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();
// 映射舊版機台回傳格式 (Map legacy machine format)
// t060v00 -> product_id, num -> stock, tid -> slot_no
$legacyData = $payload['data'] ?? [];
$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'
]);
}
}