star-cloud/app/Models/System/Company.php
sky121113 779c59138f [FEAT] 商品與機台多語系架構,並補上系統設定同步操作
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>
2026-06-15 09:00:10 +08:00

184 lines
5.5 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Models\System;
use App\Models\Machine\Machine;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Spatie\Permission\Models\Role;
class Company extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = [
'name',
'code',
'original_type',
'current_type',
'tax_id',
'contact_name',
'contact_phone',
'contact_email',
'status',
'start_date',
'end_date',
'warranty_start_date',
'warranty_end_date',
'software_start_date',
'software_end_date',
'note',
'settings',
];
protected $casts = [
'start_date' => 'date:Y-m-d',
'end_date' => 'date:Y-m-d',
'warranty_start_date' => 'date:Y-m-d',
'warranty_end_date' => 'date:Y-m-d',
'software_start_date' => 'date:Y-m-d',
'software_end_date' => 'date:Y-m-d',
'status' => 'integer',
'settings' => 'array',
];
/**
* Get the contract history for the company.
*/
public function contracts(): HasMany
{
return $this->hasMany(CompanyContract::class)->latest();
}
/**
* Get the users for the company.
*/
public function users(): HasMany
{
return $this->hasMany(User::class);
}
/**
* Get the machines for the company.
*/
public function machines(): HasMany
{
return $this->hasMany(Machine::class);
}
/**
* Get the products for the company.
*/
public function products(): HasMany
{
return $this->hasMany(\App\Models\Product\Product::class);
}
/**
* Scope僅篩選啟用的公司
*/
public function scopeActive($query)
{
return $query->where('status', 1);
}
/**
* 獲取公司自訂 Logo 的完整 URL (Accessor)
*/
public function getLogoUrlAttribute(): ?string
{
$settings = $this->settings ?? [];
$logoPath = $settings['logo_path'] ?? null;
// 只有在啟用品牌自訂功能且上傳了 Logo 時才使用自訂 Logo
if ($this->hasCustomBrandingEnabled() && $logoPath) {
return \Illuminate\Support\Facades\Storage::disk('public')->url($logoPath);
}
return asset('starcloud_icon.png');
}
/**
* 檢查此公司是否啟用且配置了品牌自訂功能
*/
public function hasCustomBrandingEnabled(): bool
{
$settings = $this->settings ?? [];
$enableCustomBranding = filter_var($settings['enable_custom_branding'] ?? false, FILTER_VALIDATE_BOOLEAN);
$logoPath = $settings['logo_path'] ?? null;
return $enableCustomBranding && !empty($logoPath);
}
/**
* 取得指定公司「所有機台已開語系的聯集」(商品翻譯實際要維護的語系)。
*
* - 來源:各機台 settings['languages'] 去重後聯集。
* - 一律保證含 fallback 語系 (zh_TW),且依系統白名單定義順序排列(穩定、不隨機台增減跳動)。
* - 僅回傳仍在白名單內的語系(機台殘留的失效語系自動濾除)。
* - 公司沒有任何機台、或機台皆未設語系時:退化為僅 [fallback]。
*
* 結果快取於 'company_active_locales:{companyId}',於機台語系異動時失效
* (見 forgetActiveLocales())。
*
* @param int|null $companyId null 代表系統層級(無租戶機台),直接回 [fallback]。
* @return array<int,string>
*/
public static function activeLocalesFor(?int $companyId): array
{
$fallback = config('locales.fallback', 'zh_TW');
$whitelist = array_keys(config('locales.supported', [$fallback => $fallback]));
if ($companyId === null) {
return [$fallback];
}
return \Illuminate\Support\Facades\Cache::rememberForever(
"company_active_locales:{$companyId}",
function () use ($companyId, $fallback, $whitelist) {
// 繞過全域範圍:本計算需涵蓋公司全部機台,與登入者可見範圍無關。
$langSets = Machine::withoutGlobalScopes()
->where('company_id', $companyId)
->pluck('settings');
$union = [];
foreach ($langSets as $settings) {
foreach ((array) ($settings['languages'] ?? []) as $locale) {
$union[$locale] = true;
}
}
// 保證含 fallback並只保留白名單內語系、依白名單順序輸出。
$union[$fallback] = true;
return array_values(array_filter(
$whitelist,
fn ($locale) => isset($union[$locale])
));
}
);
}
/**
* 失效指定公司的語系聯集快取(機台 settings.languages 異動後呼叫)。
*/
public static function forgetActiveLocales(?int $companyId): void
{
if ($companyId !== null) {
\Illuminate\Support\Facades\Cache::forget("company_active_locales:{$companyId}");
}
}
/**
* 實例代理:當前公司的語系聯集。
*
* @return array<int,string>
*/
public function activeLocales(): array
{
return static::activeLocalesFor($this->id);
}
}