diff --git a/app/Console/Commands/WarmProductsCacheCommand.php b/app/Console/Commands/WarmProductsCacheCommand.php new file mode 100644 index 0000000..faf3520 --- /dev/null +++ b/app/Console/Commands/WarmProductsCacheCommand.php @@ -0,0 +1,62 @@ +option('company'); + + if ($companyId) { + $companies = Company::where('id', $companyId)->pluck('id'); + } else { + // Only warm companies that actually have products to avoid empty cache entries + $companies = Company::whereHas('products')->pluck('id'); + } + + if ($companies->isEmpty()) { + $this->warn('No companies found with products to warm cache for.'); + return self::SUCCESS; + } + + $this->info("Starting to warm product catalog cache for {$companies->count()} companies..."); + $bar = $this->output->createProgressBar($companies->count()); + $bar->start(); + + foreach ($companies as $id) { + $catalogService->rebuildCache($id); + $bar->advance(); + } + + $bar->finish(); + $this->newLine(); + $this->info('✅ Product catalog cache warming completed!'); + + return self::SUCCESS; + } +} diff --git a/app/Http/Controllers/Admin/ProductController.php b/app/Http/Controllers/Admin/ProductController.php index 02ec54d..5586de8 100644 --- a/app/Http/Controllers/Admin/ProductController.php +++ b/app/Http/Controllers/Admin/ProductController.php @@ -12,10 +12,16 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; +use App\Services\Product\ProductCatalogService; class ProductController extends Controller { use \App\Traits\ImageHandler; + + public function __construct( + private readonly ProductCatalogService $catalogService + ) {} + public function index(Request $request) { $user = auth()->user(); @@ -219,6 +225,9 @@ class ProductController extends Controller DB::commit(); + // Rebuild catalog cache + $this->catalogService->rebuildCache($product->company_id); + if ($request->wantsJson()) { return response()->json([ 'success' => true, @@ -331,6 +340,9 @@ class ProductController extends Controller DB::commit(); + // Rebuild catalog cache + $this->catalogService->rebuildCache($product->company_id); + if ($request->wantsJson()) { return response()->json([ 'success' => true, @@ -357,6 +369,9 @@ class ProductController extends Controller $product->is_active = !$product->is_active; $product->save(); + // Rebuild catalog cache + $this->catalogService->rebuildCache($product->company_id); + $status = $product->is_active ? __('Enabled') : __('Disabled'); if (request()->ajax()) { @@ -395,8 +410,12 @@ class ProductController extends Controller Storage::disk('public')->delete($oldPath); } + $companyId = $product->company_id; $product->delete(); + // Rebuild catalog cache + $this->catalogService->rebuildCache($companyId); + if (request()->ajax()) { return response()->json([ 'success' => true, diff --git a/app/Http/Controllers/Api/V1/App/MachineController.php b/app/Http/Controllers/Api/V1/App/MachineController.php index 9107295..b8dfab8 100644 --- a/app/Http/Controllers/Api/V1/App/MachineController.php +++ b/app/Http/Controllers/Api/V1/App/MachineController.php @@ -22,6 +22,7 @@ use App\Models\StaffCardLog; use App\Models\Transaction\PassCodeLog; use App\Models\System\SystemOperationLog; use App\Services\Machine\MachineService; +use App\Services\Product\ProductCatalogService; class MachineController extends Controller { @@ -387,42 +388,14 @@ class MachineController extends Controller /** * B012_new: Download Product Catalog (Synchronous) */ - public function getProducts(Request $request) + public function getProducts(Request $request, ProductCatalogService $catalogService) { $machine = $request->get('machine'); - $products = \App\Models\Product\Product::where('company_id', $machine->company_id) - ->with(['translations']) - ->active() - ->get() - ->map(function ($product) { - // 提取多語系名稱 (Extract translations) - $nameEn = $product->translations->firstWhere('locale', 'en')?->value ?? ''; - $nameJp = $product->translations->firstWhere('locale', 'ja')?->value ?? ''; + // Cache First: Get from cache or rebuild if missing + $payload = $catalogService->getPayload($machine->company_id); - return [ - 't060v00' => (string) $product->id, // ID 仍建議維持字串,增加未來編號彈性 - 't060v01' => $product->name, - 't060v01_en' => $nameEn, - 't060v01_jp' => $nameJp, - 't060v03' => $product->spec ?? '', - '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 ?? '', // 物料編碼 (優先從 metadata 找,回退至條碼) - 'spring_limit' => (int) ($product->spring_limit ?? 10), - 'track_limit' => (int) ($product->track_limit ?? 10), - 't063v03' => (float) $product->price, - ]; - }); - - return response()->json([ - 'success' => true, - 'code' => 200, - 'data' => $products - ]); + return response()->json($payload); } diff --git a/app/Models/System/Company.php b/app/Models/System/Company.php index 5fba9d7..20f5a09 100644 --- a/app/Models/System/Company.php +++ b/app/Models/System/Company.php @@ -68,6 +68,14 @@ class Company extends Model return $this->hasMany(Machine::class); } + /** + * Get the products for the company. + */ + public function products(): HasMany + { + return $this->hasMany(\App\Models\Product\Product::class); + } + /** * Scope:僅篩選啟用的公司 */ diff --git a/app/Services/Product/ProductCatalogService.php b/app/Services/Product/ProductCatalogService.php new file mode 100644 index 0000000..51d8c5e --- /dev/null +++ b/app/Services/Product/ProductCatalogService.php @@ -0,0 +1,105 @@ +getCacheKey($companyId); + $cached = Cache::get($cacheKey); + + if ($cached) { + return $cached; + } + + return $this->rebuildCache($companyId); + } + + /** + * Rebuild the product catalog cache for a company. + * + * @param int $companyId + * @return array + */ + public function rebuildCache(int $companyId): array + { + $products = Product::where('company_id', $companyId) + ->with(['translations']) + ->active() + ->get() + ->map(fn($p) => $this->mapProduct($p)); + + $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 $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. + * + * @param Product $product + * @return array + */ + private function mapProduct($product): 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, + 't060v03' => $product->spec ?? '', + '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, + ]; + } +}