[FEAT] 新增 B016 機台系統設定回寫端點與文件

1. MachineController 新增 updateSettings (B016):機台主控台系統設定由系統方編輯後回寫雲端,僅系統管理員可操作,白名單布林開關合併進 machines.settings JSON 與 is_spring_slot_* 實體欄位,記錄 updater_id
2. routes/api.php 新增 PATCH /api/app/machine/setting/B016 路由,採 auth:sanctum (B000 User Token) 與 throttle:60,1
3. api-technical-specs SKILL.md 新增 §3.8 B016 規格:認證、白名單鍵、403/404/422 錯誤碼,並標注與 B014 下載的 PascalCase 命名方向不對稱
4. config/api-docs.php 新增 B016 條目,對齊既有 entry 格式

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
sky121113 2026-06-11 11:21:17 +08:00
parent 339b33d09b
commit 231cff9574
4 changed files with 208 additions and 0 deletions

View File

@ -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 <user_token>`,且**僅系統管理員 (`identity=system`)** 可操作;非系統方一律 403。
- **Headers**:
- `Content-Type: application/json`
- `Authorization: Bearer <user_token>`
- **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) ### 3.9 B660: 取貨碼驗證 (Pickup Code)
處理機台端的 8 位數代碼取貨流程。 處理機台端的 8 位數代碼取貨流程。

View File

@ -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 * B660: Verify/Consume Pickup Code
* POST: 驗證 8 位數取貨碼 (與原邏輯一致,但加入日誌) * POST: 驗證 8 位數取貨碼 (與原邏輯一致,但加入日誌)

View File

@ -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 端需另案實作)。' '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 <user_token>',
'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_60true=彈簧 / 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 foundsettings 非物件回 422 Invalid settings payload。寫回時同步更新 machines.settings JSON 與 is_spring_slot_* 實體欄位並記錄 updater_id套用 withoutGlobalScopes 跨租戶定位機台。'
],
[ [
'name' => 'B005: 廣告清單同步 (Ad Sync)', 'name' => 'B005: 廣告清單同步 (Ad Sync)',
'slug' => 'b005-ad-sync', 'slug' => 'b005-ad-sync',

View File

@ -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']); 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 () { Route::prefix('app')->middleware(['iot.auth', 'throttle:100,1'])->group(function () {
// 機台管理員 B000 登入驗證 (需帶機台 API Token) // 機台管理員 B000 登入驗證 (需帶機台 API Token)
Route::post('admin/login/B000', [\App\Http\Controllers\Api\V1\App\MachineAuthController::class, 'loginB000'])->middleware('throttle:30,1'); Route::post('admin/login/B000', [\App\Http\Controllers\Api\V1\App\MachineAuthController::class, 'loginB000'])->middleware('throttle:30,1');