[FEAT] 完善 IoT API 規范化、機台管理介面優化與 B005 改為 GET
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 1m4s

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)。
This commit is contained in:
2026-04-07 14:37:57 +08:00
parent b60afc3abe
commit f2147ae6c4
14 changed files with 548 additions and 85 deletions

View File

@@ -5,9 +5,11 @@ 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
@@ -104,7 +106,7 @@ class MachineController extends Controller
{
$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')
@@ -203,9 +205,11 @@ class MachineController extends Controller
$machine = $request->get('machine');
$advertisements = \App\Models\Machine\MachineAdvertisement::where('machine_id', $machine->id)
->with(['advertisement' => function ($query) {
$query->active();
}])
->with([
'advertisement' => function ($query) {
$query->active();
}
])
->get()
->filter(fn($ma) => $ma->advertisement !== null)
->map(function ($ma) {
@@ -232,7 +236,7 @@ class MachineController extends Controller
'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_pos_weight' => $posWeight[$ma->position] ?? 99,
'raw_sort' => (int) $ma->sort_order
];
})
@@ -241,7 +245,7 @@ class MachineController extends Controller
['raw_sort', 'asc']
])
->values()
->map(function($item) {
->map(function ($item) {
unset($item['raw_pos_weight'], $item['raw_sort']);
return $item;
});
@@ -260,10 +264,51 @@ class MachineController extends Controller
{
$machine = $request->get('machine');
$payload = $request->all();
$account = $payload['account'] ?? null;
// 映射舊版機台回傳格式 (Map legacy machine format)
// t060v00 -> product_id, num -> stock, tid -> slot_no
// 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,

View File

@@ -16,7 +16,6 @@ class Product extends Model
'category_id',
'name',
'name_dictionary_key',
'sku',
'barcode',
'spec',
'manufacturer',

View File

@@ -14,7 +14,7 @@ class OrderItem extends Model
'order_id',
'product_id',
'product_name',
'sku',
'barcode',
'price',
'quantity',
'subtotal',

View File

@@ -67,11 +67,10 @@ class MachineService
// 蒐集所有傳入的商品 ID (可能是 SKU 或 實際 ID)
$productCodes = collect($slotsData)->pluck('product_id')->filter()->unique()->toArray();
// 批次查詢商品 (支援以 SKU 查詢,確保對應至資料庫 ID)
$products = \App\Models\Product\Product::whereIn('sku', $productCodes)
->orWhereIn('id', $productCodes)
->get()
->keyBy(fn($p) => $p->sku ?: $p->id);
// 優先以 ID 查詢商品,若 ID 不存在則嘗試 Barcode (Prioritize ID lookup, fallback to Barcode)
$products = \App\Models\Product\Product::whereIn('id', $productCodes)
->orWhereIn('barcode', $productCodes)
->get();
foreach ($slotsData as $slotData) {
$slotNo = $slotData['slot_no'] ?? null;
@@ -79,11 +78,13 @@ class MachineService
$existingSlot = $machine->slots()->where('slot_no', $slotNo)->first();
// 查找對應的實體 ID
// 查找對應的實體 ID (支援 ID 與 Barcode 比對)
$productCode = $slotData['product_id'] ?? null;
$actualProductId = null;
if ($productCode) {
$actualProductId = $products->get($productCode)?->id;
$actualProductId = $products->first(function ($p) use ($productCode) {
return (string)$p->id === (string)$productCode || $p->barcode === (string)$productCode;
})?->id;
}
$updateData = [

View File

@@ -42,7 +42,7 @@ class TransactionService
$order->items()->create([
'product_id' => $item['product_id'],
'product_name' => $item['product_name'] ?? 'Unknown',
'sku' => $item['sku'] ?? null,
'barcode' => $item['barcode'] ?? null,
'price' => $item['price'],
'quantity' => $item['quantity'],
'subtotal' => $item['price'] * $item['quantity'],