[FEAT] 新增訂單收件人姓名遮罩與商品詞彙通用化調整

1. 於 Order Model 新增收件人姓名遮罩邏輯 (masked_pickup_recipient),將 member_barcode 進行去識別化處理以保護隱私,並保留括號/換行等後綴資訊。
2. 新增 OrderPickupRecipientMaskTest 單元測試,驗證中文字元及包含換行/括號的後綴遮罩正確性。
3. 更新後台銷售管理介面 (order-detail-panel.blade.php、tab-orders.blade.php),將原先顯示明文的 member_barcode 改用遮罩後的屬性展示。
4. 將機器通訊協定與狀態代碼中的「便當 (Bento)」字詞統一調整為「商品 (Product)」,以符合各機型通用語意。
5. 更新相關語系翻譯檔案 (zh_TW.json, en.json, ja.json) 及 MQTT 通訊規範文件中的詞彙對應。
6. 修改 MachineLog 中的翻譯取得邏輯,確保能透過錯誤代碼表對應到最新的商品語意。
This commit is contained in:
sky121113 2026-05-29 14:37:28 +08:00
parent 8d380e4902
commit e01e8077f9
10 changed files with 98 additions and 34 deletions

View File

@ -174,7 +174,7 @@ Mqtt3Client client = MqttClient.builder()
| :--- | :--- | :--- | :--- |
| **0416** | Microwave delivery door opening | info | 正在打開微波爐進貨口門 |
| **0417** | Microwave delivery door open error | error | 微波爐進貨口門打開錯誤 |
| **0418** | Pushing bento into microwave | info | 正在將便當推入微波爐 |
| **0418** | Pushing product into microwave | info | 正在將商品推入微波爐 |
| **0419** | Microwave delivery door closing | info | 正在關閉微波爐進貨口門 |
| **0420** | Microwave delivery door close error | error | 微波爐進貨口門關閉錯誤 |
@ -182,9 +182,9 @@ Mqtt3Client client = MqttClient.builder()
| 代碼 | 英文 Key (i18n) | 級別 | 範例繁中翻譯 |
| :--- | :--- | :--- | :--- |
| **0421** | Bento not detected in microwave | error | 微波爐內沒檢測到便當 |
| **0422** | Bento heating | info | 便當正在加熱 |
| **0423** | Bento heating remaining time | info | 便當加熱剩餘時間秒 |
| **0421** | Product not detected in microwave | error | 微波爐內未檢測到商品 |
| **0422** | Product heating | info | 商品正在加熱 |
| **0423** | Product heating remaining time | info | 商品加熱剩餘時間秒 |
| **0424** | Purchase successful | info | ✅ 購買成功 |
| **0431** | Microwave push rod push error | error | 微波爐內推桿推出錯誤 |
| **0432** | Microwave push rod retract error | error | 微波爐內推桿收回錯誤 |
@ -212,7 +212,7 @@ Mqtt3Client client = MqttClient.builder()
| **0434** | Pickup door closed (cart mode) | info | 取貨口門關閉成功 (購物車) |
| **0435** | Liquid dispense paused | warning | 暫停出液 (液體機) |
| **0441** | Pickup door unlocked | info | 取貨口門開鎖上報 |
| **04FF** | Bento purchase terminated | error | 便當購買終止 |
| **04FF** | Purchase terminated | error | 購買終止 |
### 3.3 交易生命週期上報 (Transaction) - `machine/{serial_no}/transaction`

View File

@ -99,11 +99,12 @@ class MachineLog extends Model
{
$context = $this->context;
// 若 context 中已有翻譯標籤 (B013 封裝),則進行動態重組
// B013 封裝:優先用目前代碼表重組,讓舊日誌也能吃到最新文案。
if (isset($context['translated_label'])) {
$label = __($context['translated_label']);
$code = $context['raw_code'] ?? $context['error_code'] ?? '0000';
$mapping = \App\Services\Machine\MachineService::ERROR_CODE_MAP[$code] ?? null;
$label = __($mapping['label'] ?? $context['translated_label']);
$tid = $context['tid'] ?? null;
$code = $context['raw_code'] ?? '0000';
if ($tid) {
return __('Slot') . " {$tid}: {$label} (Code: {$code})";

View File

@ -121,6 +121,47 @@ class Order extends Model
return self::getPaymentTypeLabels()[$this->payment_type] ?? __('Unknown');
}
public function getMaskedPickupRecipientAttribute(): ?string
{
if (empty($this->member_barcode)) {
return null;
}
return self::maskPickupRecipientName($this->member_barcode);
}
public static function maskPickupRecipientName(string $value): string
{
$value = trim($value);
if ($value === '') {
return $value;
}
if (preg_match('/^([^\s(]+)(.*)$/us', $value, $matches) !== 1) {
return self::maskName($value);
}
return self::maskName($matches[1]) . ($matches[2] ?? '');
}
private static function maskName(string $name): string
{
$length = mb_strlen($name);
if ($length <= 1) {
return $name;
}
if ($length === 2) {
return mb_substr($name, 0, 1) . 'O';
}
return mb_substr($name, 0, 1)
. str_repeat('O', $length - 2)
. mb_substr($name, -1);
}
public function machine()
{
return $this->belongsTo(Machine::class);

View File

@ -18,7 +18,7 @@ class MachineService
* B013: 硬體狀態代碼對照表 (Hardware Status Code Mapping)
*
* 代碼格式Command 0x04 + Status Byte例如 0x04+0x03 = '0403'
* 出貨結果判定:僅 0402 (出貨成功) 0424 (便當取出成功) 視為成功,
* 出貨結果判定:僅 0402 (出貨成功) 0424 (商品取出成功) 視為成功,
* 其餘出貨結果碼均應觸發退款。
*
* 參考文件:廠商硬體通訊協議 Command(0x04) 狀態定義
@ -57,15 +57,15 @@ class MachineService
// --- 微波爐進貨口門 ---
'0416' => ['label' => 'Microwave delivery door opening', 'level' => 'info'], // 正在打開微波爐進貨口門
'0417' => ['label' => 'Microwave delivery door open error', 'level' => 'error'], // 微波爐進貨口門打開錯誤
'0418' => ['label' => 'Pushing bento into microwave', 'level' => 'info'], // 正在將便當推入微波爐
'0418' => ['label' => 'Pushing product into microwave', 'level' => 'info'], // 正在將商品推入微波爐
'0419' => ['label' => 'Microwave delivery door closing', 'level' => 'info'], // 正在關閉微波爐進貨口門
'0420' => ['label' => 'Microwave delivery door close error', 'level' => 'error'], // 微波爐進貨口門關閉錯誤
// --- 微波爐內部 ---
'0421' => ['label' => 'Bento not detected in microwave', 'level' => 'error'], // 微波爐內沒檢測到便當
'0422' => ['label' => 'Bento heating', 'level' => 'info'], // 便當正在加熱
'0423' => ['label' => 'Bento heating remaining time', 'level' => 'info'], // 便當加熱剩餘時間 (貨道號=秒數)
'0424' => ['label' => 'Purchase successful', 'level' => 'info'], // ✅ 購買成功 (原 Bento ready for pickup)
'0421' => ['label' => 'Product not detected in microwave', 'level' => 'error'], // 微波爐內未檢測到商品
'0422' => ['label' => 'Product heating', 'level' => 'info'], // 商品正在加熱
'0423' => ['label' => 'Product heating remaining time', 'level' => 'info'], // 商品加熱剩餘時間 (貨道號=秒數)
'0424' => ['label' => 'Purchase successful', 'level' => 'info'], // ✅ 購買成功
'0431' => ['label' => 'Microwave push rod push error', 'level' => 'error'], // 微波爐內推桿推出錯誤
'0432' => ['label' => 'Microwave push rod retract error', 'level' => 'error'], // 微波爐內推桿收回錯誤
@ -87,7 +87,7 @@ class MachineService
'0435' => ['label' => 'Liquid dispense paused', 'level' => 'warning'], // 暫停出液
// --- 終止 ---
'04FF' => ['label' => 'Bento purchase terminated', 'level' => 'error'], // 便當購買終止
'04FF' => ['label' => 'Purchase terminated', 'level' => 'error'], // 購買終止
// ═══════════════════════════════════════════════════════════════
// 貨道與馬達狀態類 (Prefix: 02 - SLOT_STATUS)
@ -116,7 +116,7 @@ class MachineService
*
* 僅以下代碼視為出貨成功,其餘出貨結果碼均應觸發退款。
* - 0402: 一般出貨成功
* - 0424: 便當取出成功 (微波爐機型)
* - 0424: 商品取出成功 (微波爐機型)
*/
public const DISPENSE_SUCCESS_CODES = ['0402', '0424'];

View File

@ -219,10 +219,10 @@
"Before Qty": "Before Qty",
"Belongs To": "Belongs To",
"Belongs To Company": "Belongs To Company",
"Bento heating": "Bento heating",
"Bento heating remaining time": "Bento heating remaining time",
"Bento not detected in microwave": "Bento not detected in microwave",
"Bento purchase terminated": "Bento purchase terminated",
"Product heating": "Product heating",
"Product heating remaining time": "Product heating remaining time",
"Product not detected in microwave": "Product not detected in microwave",
"Purchase terminated": "Purchase terminated",
"Bill Acceptor": "Bill Acceptor",
"Bracelet drop not detected": "Bracelet drop not detected",
"Bracelet scan failed": "Bracelet scan failed",
@ -1490,7 +1490,7 @@
"Purchasing": "Purchasing",
"Purpose": "Purpose",
"Push Status": "Push Status",
"Pushing bento into microwave": "Pushing bento into microwave",
"Pushing product into microwave": "Pushing product into microwave",
"QR": "QR",
"QR CODE": "QR CODE",
"QR Code": "QR Code",

View File

@ -219,10 +219,10 @@
"Before Qty": "変動前数量",
"Belongs To": "会社名",
"Belongs To Company": "所属会社",
"Bento heating": "お弁当加熱中",
"Bento heating remaining time": "お弁当加熱残り時間",
"Bento not detected in microwave": "電子レンジ内お弁当未検出",
"Bento purchase terminated": "お弁当購入中止",
"Product heating": "商品加熱中",
"Product heating remaining time": "商品加熱残り時間",
"Product not detected in microwave": "電子レンジ内商品未検出",
"Purchase terminated": "購入中止",
"Bill Acceptor": "紙幣識別機",
"Bracelet drop not detected": "ブレスレット落下未検出",
"Bracelet scan failed": "ブレスレットスキャン失敗",
@ -1490,7 +1490,7 @@
"Purchasing": "購入中",
"Purpose": "用途",
"Push Status": "アラート状態",
"Pushing bento into microwave": "お弁当を電子レンジに投入中",
"Pushing product into microwave": "商品を電子レンジに投入中",
"QR": "QR",
"QR CODE": "QRコード",
"QR Code": "QRコード",

View File

@ -219,10 +219,10 @@
"Before Qty": "異動前數量",
"Belongs To": "公司名稱",
"Belongs To Company": "公司名稱",
"Bento heating": "便當正在加熱",
"Bento heating remaining time": "便當加熱剩餘時間",
"Bento not detected in microwave": "微波爐內沒檢測到便當",
"Bento purchase terminated": "便當購買終止",
"Product heating": "商品正在加熱",
"Product heating remaining time": "商品加熱剩餘時間",
"Product not detected in microwave": "微波爐內未檢測到商品",
"Purchase terminated": "購買終止",
"Bill Acceptor": "紙鈔機",
"Bracelet drop not detected": "沒檢測到手環掉入取貨斗",
"Bracelet scan failed": "沒掃碼到手環",
@ -1490,7 +1490,7 @@
"Purchasing": "購買中",
"Purpose": "用途",
"Push Status": "告警狀態",
"Pushing bento into microwave": "正在將便當推入微波爐",
"Pushing product into microwave": "正在將商品推入微波爐",
"QR": "二維碼",
"QR CODE": "二維碼",
"QR Code": "QR Code",

View File

@ -137,7 +137,7 @@
<div class="grid grid-cols-2 gap-8 p-8 bg-slate-50/50 dark:bg-slate-800/30 rounded-[2rem] border border-slate-100 dark:border-slate-800">
<div class="space-y-1 col-span-2">
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{ __('Pickup Recipient (Prescription Slip)') }}</p>
<p class="text-sm font-extrabold text-slate-700 dark:text-slate-200">{{ $order->member_barcode }}</p>
<p class="text-sm font-extrabold text-slate-700 dark:text-slate-200">{{ $order->masked_pickup_recipient }}</p>
</div>
</div>
</section>

View File

@ -252,7 +252,7 @@
<svg class="w-3.5 h-3.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 9h3.75M15 12h3.75M15 15h3.75M4.5 19.5h15a2.25 2.25 0 002.25-2.25V6.75A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25v10.5A2.25 2.25 0 004.5 19.5zm6-10.125a1.875 1.875 0 11-3.75 0 1.875 1.875 0 013.75 0zm1.294 6.336a6.721 6.721 0 01-3.17.789 6.721 6.721 0 01-3.168-.789 3.376 3.376 0 016.338 0z" />
</svg>
{{ $order->member_barcode }}
{{ $order->masked_pickup_recipient }}
</span>
@endif
</div>
@ -409,7 +409,7 @@
<svg class="w-3.5 h-3.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 9h3.75M15 12h3.75M15 15h3.75M4.5 19.5h15a2.25 2.25 0 002.25-2.25V6.75A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25v10.5A2.25 2.25 0 004.5 19.5zm6-10.125a1.875 1.875 0 11-3.75 0 1.875 1.875 0 013.75 0zm1.294 6.336a6.721 6.721 0 01-3.17.789 6.721 6.721 0 01-3.168-.789 3.376 3.376 0 016.338 0z" />
</svg>
{{ $order->member_barcode }}
{{ $order->masked_pickup_recipient }}
</div>
@endif
</div>

View File

@ -0,0 +1,22 @@
<?php
namespace Tests\Unit;
use App\Models\Transaction\Order;
use PHPUnit\Framework\TestCase;
class OrderPickupRecipientMaskTest extends TestCase
{
public function test_it_masks_pickup_recipient_names(): void
{
$this->assertSame('鄭O', Order::maskPickupRecipientName('鄭英'));
$this->assertSame('鄭O英', Order::maskPickupRecipientName('鄭小英'));
$this->assertSame('鄭OO英', Order::maskPickupRecipientName('鄭曉小英'));
}
public function test_it_keeps_pickup_recipient_suffix(): void
{
$this->assertSame('黃O美 (2593)', Order::maskPickupRecipientName('黃麗美 (2593)'));
$this->assertSame("黃O美\n(2593)", Order::maskPickupRecipientName("黃麗美\n(2593)"));
}
}