1. 新增 config/locales.php:定義 11 種系統支援語系白名單(原文+中文註記)、每台機台上限 5 種與 fallback (zh_TW)。 2. Company 新增 activeLocalesFor()/activeLocales():計算公司所有機台已開語系的聯集(去重、依白名單排序、保底 zh_TW),加快取與 forgetActiveLocales() 失效機制。 3. products 新增 spec_dictionary_key 欄位(migration);Product 新增 localized_spec accessor 與 specTranslations 關聯,商品規格比照名稱支援多語系。 4. ProductController 商品建立/更新改收 names 與 specs 物件,寫入 translations(group=product/product_spec),既有商品首次編輯補建 spec key,刪除時一併清除規格翻譯。 5. ProductCatalogService(B012)保留既有 t060v01_en/_jp 欄位,新增 t060v01_i18n/t060v03_i18n locale map,依公司語系聯集輸出並回退 zh_TW,確保線上舊 App 相容。 6. 機台系統設定(updateSystemSettings)新增 languages 顯示語系驗證與寫入(最多 5 種、白名單、去重,僅系統管理員),異動時失效語系聯集快取並重建商品目錄。 7. B014(getSettings)新增 LangSet 下發機台顯示語系(Languages 有序清單+Default 預設)。 8. 商品建立/編輯頁名稱與規格改為多語系 Tab(讀公司機台語系聯集、完成度標記),新增共用元件 product-locale-tabs;機台系統設定彈窗新增顯示語系勾選 UI(限 5 選、預設標記)。 9. 三語系 JSON(zh_TW/en/ja)新增 5 組對齊鍵並維持字母排序。 10. 同步更新文件:docs/API/product_multilingual_spec.md(規格書)、api-technical-specs SKILL.md(B012 i18n/B014 LangSet)、config/api-docs.php(B012/B014 範例與欄位)。 11. 系統設定同步:新增 sync-settings 路由與 update_settings 指令類型,ProcessCommandAck 與遠端指令歷史(remote index/tab-history)支援 update_settings 標籤,機台系統設定頁新增「同步設定」操作按鈕,並補 B016 回寫測試。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
142 lines
4.9 KiB
PHP
142 lines
4.9 KiB
PHP
<?php
|
||
|
||
namespace App\Services\Product;
|
||
|
||
use App\Models\Product\Product;
|
||
use App\Models\System\Company;
|
||
use Illuminate\Support\Facades\Cache;
|
||
|
||
class ProductCatalogService
|
||
{
|
||
/**
|
||
* Get the Redis cache key for a company's product catalog.
|
||
*
|
||
* @param int|null $companyId
|
||
* @return string
|
||
*/
|
||
public function getCacheKey(?int $companyId): string
|
||
{
|
||
return "products_catalog:" . ($companyId ?? 'global');
|
||
}
|
||
|
||
/**
|
||
* Get the product catalog payload, using cache if available.
|
||
*
|
||
* @param int|null $companyId
|
||
* @return array
|
||
*/
|
||
public function getPayload(?int $companyId): array
|
||
{
|
||
$cacheKey = $this->getCacheKey($companyId);
|
||
$cached = Cache::get($cacheKey);
|
||
|
||
if ($cached) {
|
||
return $cached;
|
||
}
|
||
|
||
return $this->rebuildCache($companyId);
|
||
}
|
||
|
||
/**
|
||
* Rebuild the product catalog cache for a company.
|
||
*
|
||
* @param int|null $companyId
|
||
* @return array
|
||
*/
|
||
public function rebuildCache(?int $companyId): array
|
||
{
|
||
// 公司機台語系聯集:商品 i18n map 下發的語系集合(見 product_multilingual_spec §5.1)
|
||
$locales = Company::activeLocalesFor($companyId);
|
||
|
||
$products = Product::where('company_id', $companyId)
|
||
->with(['translations', 'specTranslations'])
|
||
->active()
|
||
->get()
|
||
->map(fn($p) => $this->mapProduct($p, $locales));
|
||
|
||
$payload = [
|
||
'success' => true,
|
||
'code' => 200,
|
||
'data' => $products->toArray(),
|
||
];
|
||
|
||
Cache::forever($this->getCacheKey($companyId), $payload);
|
||
|
||
return $payload;
|
||
}
|
||
|
||
/**
|
||
* Invalidate the product catalog cache for a company.
|
||
*
|
||
* @param int|null $companyId
|
||
* @return void
|
||
*/
|
||
public function invalidate(?int $companyId): void
|
||
{
|
||
Cache::forget($this->getCacheKey($companyId));
|
||
}
|
||
|
||
/**
|
||
* Map a Product model to the catalog format expected by machines.
|
||
*
|
||
* 相容鐵則:既有欄位(t060v01 / t060v01_en / t060v01_jp / t060v03 …)一律保留,
|
||
* 僅「新增」locale-keyed 的 t060v01_i18n(名稱)與 t060v03_i18n(規格)。
|
||
* 線上舊 App 忽略新欄位、照舊運作;新版 App 讀 *_i18n 以支援多語系切換。
|
||
*
|
||
* @param Product $product
|
||
* @param array<int,string> $locales 公司機台語系聯集(i18n map 下發的語系集合)
|
||
* @return array
|
||
*/
|
||
private function mapProduct($product, array $locales = ['zh_TW']): array
|
||
{
|
||
$nameEn = $product->translations->firstWhere('locale', 'en')?->value ?? '';
|
||
$nameJp = $product->translations->firstWhere('locale', 'ja')?->value ?? '';
|
||
|
||
return [
|
||
't060v00' => (string) $product->id,
|
||
't060v01' => $product->name,
|
||
't060v01_en' => $nameEn,
|
||
't060v01_jp' => $nameJp,
|
||
't060v01_i18n' => $this->buildI18nMap($product->translations, $product->name, $locales),
|
||
't060v03' => $product->spec ?? '',
|
||
't060v03_i18n' => $this->buildI18nMap($product->specTranslations, $product->spec ?? '', $locales),
|
||
't060v06' => $product->image_url
|
||
? (str_starts_with($product->image_url, 'http') ? $product->image_url : asset($product->image_url))
|
||
: '',
|
||
't060v09' => (float) $product->price,
|
||
't060v11' => (int) ($product->track_limit ?? 10),
|
||
't060v30' => (float) ($product->member_price ?? $product->price),
|
||
't060v40' => $product->metadata['marketing_plan'] ?? '',
|
||
't060v41' => $product->metadata['material_code'] ?? $product->barcode ?? '',
|
||
'spring_limit' => (int) ($product->spring_limit ?? 10),
|
||
'track_limit' => (int) ($product->track_limit ?? 10),
|
||
't063v03' => (float) $product->price,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 依公司機台語系聯集,建立 locale → 文字 的 i18n map。
|
||
*
|
||
* - 僅輸出 $locales 內的語系。
|
||
* - 缺翻譯時回退至 zh_TW,再回退至 $fallbackText(products.name / spec 主值),
|
||
* 確保不下發空字串,避免機台顯示空白。
|
||
*
|
||
* @param \Illuminate\Support\Collection $translations 該欄位的翻譯集合
|
||
* @param string|null $fallbackText 最終回退文字(主欄位值)
|
||
* @param array<int,string> $locales 公司機台語系聯集
|
||
* @return array<string,string>
|
||
*/
|
||
private function buildI18nMap($translations, ?string $fallbackText, array $locales): array
|
||
{
|
||
$zhFallback = $translations->firstWhere('locale', 'zh_TW')?->value
|
||
?? ($fallbackText ?? '');
|
||
|
||
$map = [];
|
||
foreach ($locales as $locale) {
|
||
$map[$locale] = $translations->firstWhere('locale', $locale)?->value ?: $zhFallback;
|
||
}
|
||
|
||
return $map;
|
||
}
|
||
}
|