diff --git a/.agents/skills/api-technical-specs/SKILL.md b/.agents/skills/api-technical-specs/SKILL.md index b265fb7..41bc9c7 100644 --- a/.agents/skills/api-technical-specs/SKILL.md +++ b/.agents/skills/api-technical-specs/SKILL.md @@ -204,6 +204,51 @@ description: 本技能規範定義了 Star Cloud 系統中所有機台 (IoT) 與 --- +### 3.8 B016: 機台系統設定回寫 (Settings Write-back) +機台主控台「系統設定」頁由**系統方**編輯後,將開關狀態回寫雲端。為 B014 下載的反向操作。 + +- **URL**: PATCH /api/v1/app/machine/setting/B016 +- **Authentication**: **User Token (Sanctum)** — 帶入 B000 核發之 `Authorization: Bearer `,且**僅系統管理員 (`identity=system`)** 可操作;非系統方一律 403。 +- **Headers**: + - `Content-Type: application/json` + - `Authorization: Bearer ` +- **Request Body:** + +| 參數 | 類型 | 必填 | 說明 | 範例 | +| :--- | :--- | :--- | :--- | :--- | +| machine | String | 是 | 機台序號 (serial_no) | SN202604130001 | +| settings | Object | 是 | 開關鍵值物件,鍵名採**後端原始 snake_case**(非 B014 的 PascalCase `*Set`)。只覆寫有送來的鍵,其餘保留 | {"tax_invoice_enabled": true} | + +> [!IMPORTANT] +> **命名方向不對稱**:B014 **下載**以 `DevSet`/`CashSet`/... PascalCase 包裝下發;B016 **回寫**直接使用後端鍵名(如 `shopping_cart_enabled`、`cash_bill_1000`、`is_spring_slot_1_10`)。 +> **白名單限定**:僅接受下列布林開關鍵,未列入者一律忽略;**不接受** `shopping_mode` 及 `OperationSet` 等非開關欄位(避免誤改)。 + +- **可回寫鍵(settings 之下,皆為布林):** + - 支付旗標:`shopping_cart_enabled`、`tax_invoice_enabled`、`card_terminal_enabled`、`credit_card_enabled`、`mobile_pay_enabled`、`card_pay_enabled`、`scan_pay_enabled`、`scan_pay_esun_enabled`、`scan_pay_tappay_enabled`、`cash_module_enabled`、`scan_pay_linepay_enabled`、`tappay_linepay`、`tappay_jkopay`、`tappay_easywallet`、`tappay_pipay`、`tappay_pluspay` + - 現金面額:`cash_bill_1000`、`cash_bill_500`、`cash_bill_100`、`cash_coin_50`、`cash_coin_10`、`cash_coin_5`、`cash_coin_1` + - 功能模組:`pickup_module_enabled`、`pickup_code_enabled`、`pass_code_enabled`、`welcome_gift_enabled`、`member_system_enabled`、`ambient_temp_monitoring_enabled` + - 貨道類型(machines 實體欄位):`is_spring_slot_1_10`、`is_spring_slot_11_20`、`is_spring_slot_21_30`、`is_spring_slot_31_40`、`is_spring_slot_41_50`、`is_spring_slot_51_60`(true=彈簧 / false=履帶) + +- **Response Body (Success 200):** + +| 參數 | 類型 | 說明 | 範例 | +| :--- | :--- | :--- | :--- | +| success | Boolean | 是否成功 | true | +| code | Integer | 業務狀態碼 | 200 | +| message | String | 回應訊息 | Settings updated successfully. | + +- **錯誤回應:** + +| HTTP | 情境 | message | +| :--- | :--- | :--- | +| 403 | 非系統管理員 / 未登入 | Forbidden | +| 404 | 查無該序號機台 | Machine not found | +| 422 | `settings` 非物件/陣列 | Invalid settings payload | + +> 註:寫回時同步更新 `machines.settings` JSON 與 `is_spring_slot_*` 實體欄位,並記錄 `updater_id`。為 system-admin 專用,套用 `withoutGlobalScopes` 跨租戶定位機台。 + +--- + ### 3.9 B660: 取貨碼驗證 (Pickup Code) 處理機台端的 8 位數代碼取貨流程。 diff --git a/app/Http/Controllers/Api/V1/App/MachineController.php b/app/Http/Controllers/Api/V1/App/MachineController.php index 301ab04..c4a1e88 100644 --- a/app/Http/Controllers/Api/V1/App/MachineController.php +++ b/app/Http/Controllers/Api/V1/App/MachineController.php @@ -534,6 +534,111 @@ class MachineController extends Controller ]); } + /** + * B016: Update Machine System Settings (Write-back from machine console) + * 機台主控台「系統設定」由系統方 (identity=system) 編輯後回寫雲端。 + * 認證:B000 核發的使用者 Token (auth:sanctum);僅系統管理員可操作。 + * Body: { machine: 序號, settings: { <後端鍵名>: bool, ... } } + * 接收的開關鍵名與 B014 getSettings 下發來源一致(machines.settings JSON 與 + * is_spring_slot_* 實體欄位)。不處理 shopping_mode 等非開關欄位,避免誤改。 + */ + public function updateSettings(Request $request) + { + // 1. 僅系統方可回寫(雙重把關:App 端隱藏控制 + 此處伺服器驗權) + $user = $request->user(); + if (!$user || !$user->isSystemAdmin()) { + return response()->json([ + 'success' => false, + 'code' => 403, + 'message' => __('Forbidden') + ], 403); + } + + // 2. 查找機台 + $serialNo = $request->input('machine'); + $machine = Machine::withoutGlobalScopes() + ->where('serial_no', $serialNo) + ->first(); + + if (!$machine) { + return response()->json([ + 'success' => false, + 'code' => 404, + 'message' => __('Machine not found') + ], 404); + } + + $settings = $request->input('settings', []); + if (!is_array($settings)) { + return response()->json([ + 'success' => false, + 'code' => 422, + 'message' => __('Invalid settings payload') + ], 422); + } + + // 3. 開關白名單:machines.settings JSON 內的布林鍵(對齊 B014 getSettings 來源) + $jsonBoolKeys = [ + 'shopping_cart_enabled', 'tax_invoice_enabled', 'card_terminal_enabled', + 'credit_card_enabled', 'mobile_pay_enabled', 'card_pay_enabled', 'scan_pay_enabled', + 'scan_pay_esun_enabled', 'scan_pay_tappay_enabled', 'cash_module_enabled', 'scan_pay_linepay_enabled', + 'tappay_linepay', 'tappay_jkopay', 'tappay_easywallet', 'tappay_pipay', 'tappay_pluspay', + 'cash_bill_1000', 'cash_bill_500', 'cash_bill_100', + 'cash_coin_50', 'cash_coin_10', 'cash_coin_5', 'cash_coin_1', + 'pickup_module_enabled', 'pickup_code_enabled', 'pass_code_enabled', + 'welcome_gift_enabled', 'member_system_enabled', 'ambient_temp_monitoring_enabled', + ]; + // 直接欄位白名單:machines 實體布林欄位(貨道類型) + $columnBoolKeys = [ + 'is_spring_slot_1_10', 'is_spring_slot_11_20', 'is_spring_slot_21_30', + 'is_spring_slot_31_40', 'is_spring_slot_41_50', 'is_spring_slot_51_60', + ]; + // 同時具備實體欄位的 settings 鍵(mutator 同步,需一併寫回避免不一致) + $mirrorColumns = [ + 'tax_invoice_enabled', 'card_terminal_enabled', 'scan_pay_esun_enabled', + 'scan_pay_linepay_enabled', 'shopping_cart_enabled', 'welcome_gift_enabled', + 'cash_module_enabled', 'member_system_enabled', 'ambient_temp_monitoring_enabled', + ]; + + // 4. 合併 JSON 設定(只覆寫有送來的鍵,其餘保留) + $jsonSettings = $machine->settings ?? []; + foreach ($jsonBoolKeys as $k) { + if (array_key_exists($k, $settings)) { + $jsonSettings[$k] = (bool) $settings[$k]; + } + } + + // 5. 整理實體欄位更新(貨道 + 鏡像欄位) + $columnData = []; + foreach ($columnBoolKeys as $k) { + if (array_key_exists($k, $settings)) { + $columnData[$k] = (bool) $settings[$k]; + } + } + foreach ($mirrorColumns as $k) { + if (array_key_exists($k, $jsonSettings)) { + $columnData[$k] = (bool) $jsonSettings[$k]; + } + } + + $machine->update(array_merge($columnData, [ + 'settings' => $jsonSettings, + 'updater_id' => $user->id, + ])); + + \Log::info('B016 Machine settings updated', [ + 'machine' => $machine->serial_no, + 'user_id' => $user->id, + 'keys' => array_keys($settings), + ]); + + return response()->json([ + 'success' => true, + 'code' => 200, + 'message' => __('Settings updated successfully.') + ]); + } + /** * B660: Verify/Consume Pickup Code * POST: 驗證 8 位數取貨碼 (與原邏輯一致,但加入日誌) diff --git a/config/api-docs.php b/config/api-docs.php index 8d44342..685d4bd 100644 --- a/config/api-docs.php +++ b/config/api-docs.php @@ -277,6 +277,59 @@ return [ ], 'notes' => '此 API 為機台初始化引導用,目前不強制驗證 User Token。金流密鑰與發票設定來源為機台關聯的金流配置 (payment_configs);DevSet / CashSet / FunctionSet / ShoppingMode 來源為 machines.settings、OperationSet / HardwareSet 來源為 machines 實體欄位,全部採機台端 *Set + PascalCase 風格(DevSet/CashSet 對齊機台既有結構,其餘為新定義,機台 App 端需另案實作)。' ], + [ + 'name' => 'B016: 機台系統設定回寫 (Settings Write-back)', + 'slug' => 'b016-settings-writeback', + 'method' => 'PATCH', + 'path' => '/api/v1/app/machine/setting/B016', + 'description' => '機台主控台「系統設定」頁由系統方編輯後回寫雲端,為 B014 下載的反向操作。需帶 B000 核發之 User Token,且僅系統管理員可操作。鍵名採後端原始 snake_case(非 B014 的 PascalCase *Set),僅白名單布林開關生效,不接受 shopping_mode 等非開關欄位。', + 'headers' => [ + 'Authorization' => 'Bearer ', + 'Content-Type' => 'application/json', + ], + 'parameters' => [ + 'machine' => [ + 'type' => 'string', + 'required' => true, + 'description' => '機台序號 (serial_no)', + 'example' => 'SN202604130001' + ], + 'settings' => [ + 'type' => 'object', + 'required' => true, + 'description' => "開關鍵值物件(皆為布林),鍵名採後端原始 snake_case,只覆寫有送來的鍵、其餘保留。白名單外鍵一律忽略。可回寫鍵:\n• 支付旗標:shopping_cart_enabled / tax_invoice_enabled / card_terminal_enabled / credit_card_enabled / mobile_pay_enabled / card_pay_enabled / scan_pay_enabled / scan_pay_esun_enabled / scan_pay_tappay_enabled / cash_module_enabled / scan_pay_linepay_enabled / tappay_linepay / tappay_jkopay / tappay_easywallet / tappay_pipay / tappay_pluspay\n• 現金面額:cash_bill_1000 / cash_bill_500 / cash_bill_100 / cash_coin_50 / cash_coin_10 / cash_coin_5 / cash_coin_1\n• 功能模組:pickup_module_enabled / pickup_code_enabled / pass_code_enabled / welcome_gift_enabled / member_system_enabled / ambient_temp_monitoring_enabled\n• 貨道類型(machines 實體欄位):is_spring_slot_1_10 / 11_20 / 21_30 / 31_40 / 41_50 / 51_60(true=彈簧 / false=履帶)", + 'example' => ['tax_invoice_enabled' => true, 'is_spring_slot_1_10' => false] + ], + ], + 'response_parameters' => [ + 'success' => [ + 'type' => 'boolean', + 'description' => '是否成功', + ], + 'code' => [ + 'type' => 'integer', + 'description' => '業務狀態碼', + ], + 'message' => [ + 'type' => 'string', + 'description' => '回應訊息', + ], + ], + 'request' => [ + 'machine' => 'SN202604130001', + 'settings' => [ + 'tax_invoice_enabled' => true, + 'cash_module_enabled' => false, + 'is_spring_slot_1_10' => true, + ], + ], + 'response' => [ + 'success' => true, + 'code' => 200, + 'message' => 'Settings updated successfully.', + ], + 'notes' => '僅系統管理員 (identity=system) 可操作,非系統方回 403 Forbidden;查無序號回 404 Machine not found;settings 非物件回 422 Invalid settings payload。寫回時同步更新 machines.settings JSON 與 is_spring_slot_* 實體欄位並記錄 updater_id,套用 withoutGlobalScopes 跨租戶定位機台。' + ], [ 'name' => 'B005: 廣告清單同步 (Ad Sync)', 'slug' => 'b005-ad-sync', diff --git a/routes/api.php b/routes/api.php index efa4b0e..bfa0852 100644 --- a/routes/api.php +++ b/routes/api.php @@ -53,6 +53,11 @@ Route::prefix('v1')->middleware(['throttle:api'])->group(function () { Route::get('machine/setting/B014', [App\Http\Controllers\Api\V1\App\MachineController::class, 'getSettings']); }); + // 機台端回寫系統設定 (B016):僅系統方 (identity=system) 可操作,認證使用 B000 核發的使用者 Token + Route::prefix('app')->middleware(['auth:sanctum', 'throttle:60,1'])->group(function () { + Route::patch('machine/setting/B016', [App\Http\Controllers\Api\V1\App\MachineController::class, 'updateSettings']); + }); + Route::prefix('app')->middleware(['iot.auth', 'throttle:100,1'])->group(function () { // 機台管理員 B000 登入驗證 (需帶機台 API Token) Route::post('admin/login/B000', [\App\Http\Controllers\Api\V1\App\MachineAuthController::class, 'loginB000'])->middleware('throttle:30,1');