[FEAT] 實作商品主檔 B012 Redis 快取機制 (Phase 1)
1. 新增 ProductCatalogService 負責商品清單轉換與 Redis 快取邏輯。 2. 新增 WarmProductsCacheCommand (artisan products:warm-cache) 用於預熱快取。 3. 修改 ProductController 在商品異動時自動重建快取。 4. 修改 MachineController@getProducts (B012) 改為 Cache-First 讀取。 5. 在 Company 模型補上 products 關聯以支援暖機指令。
This commit is contained in:
parent
f9d3217099
commit
2c8a2de81c
62
app/Console/Commands/WarmProductsCacheCommand.php
Normal file
62
app/Console/Commands/WarmProductsCacheCommand.php
Normal file
@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\System\Company;
|
||||
use App\Services\Product\ProductCatalogService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class WarmProductsCacheCommand extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'products:warm-cache {--company= : ID of the specific company to warm cache for}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Pre-warm the Redis cache for product catalogs of all or a specific company.';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @param ProductCatalogService $catalogService
|
||||
* @return int
|
||||
*/
|
||||
public function handle(ProductCatalogService $catalogService): int
|
||||
{
|
||||
$companyId = $this->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;
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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:僅篩選啟用的公司
|
||||
*/
|
||||
|
||||
105
app/Services/Product/ProductCatalogService.php
Normal file
105
app/Services/Product/ProductCatalogService.php
Normal file
@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Product;
|
||||
|
||||
use App\Models\Product\Product;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class ProductCatalogService
|
||||
{
|
||||
/**
|
||||
* Get the Redis cache key for a company's product catalog.
|
||||
*
|
||||
* @param int $companyId
|
||||
* @return string
|
||||
*/
|
||||
public function getCacheKey(int $companyId): string
|
||||
{
|
||||
return "products_catalog:{$companyId}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the product catalog payload, using cache if available.
|
||||
*
|
||||
* @param int $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 $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,
|
||||
];
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user