feat: Reimplement order and invoice management with new service and repository layers, introduce compression middleware, and update database schema.
This commit is contained in:
@@ -73,7 +73,7 @@ class InvoiceController extends Controller
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'customer_id' => 'required|numeric',
|
||||
'payment_id' => 'required|numeric',
|
||||
'payment_method' => 'required|numeric',
|
||||
'sub_total' => 'required|numeric',
|
||||
'total' => 'required|numeric',
|
||||
]);
|
||||
@@ -84,7 +84,7 @@ class InvoiceController extends Controller
|
||||
'sub_total' => $validated['sub_total'],
|
||||
'tax' => $validated['sub_total'] * 0.1,
|
||||
'total' => $validated['total'],
|
||||
'payment_id' => $validated['payment_id'],
|
||||
'payment_method' => $validated['payment_method'],
|
||||
'customer_id' => $validated['customer_id'],
|
||||
'status' => 0,
|
||||
];
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Http\Controllers\Api\Admin\Order;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Helpers\Helper;
|
||||
use App\Http\Requests\OrderRequest;
|
||||
use App\Http\Requests\UpdateOrderRequest;
|
||||
use App\Http\Resources\Api\Admin\OrderResource;
|
||||
@@ -12,7 +11,6 @@ use App\Jobs\VirtualMachine\Create\CreateVMJobs;
|
||||
use App\Jobs\VirtualMachine\Upgrade\UpgradeVirtualMachine;
|
||||
use App\Models\Order;
|
||||
use App\Repositories\Addon\AddonRepositoryInterface;
|
||||
use App\Repositories\Hosting\HostingRepositoryInterface;
|
||||
use App\Repositories\Invoice\InvoiceRepositoryInterface;
|
||||
use App\Repositories\InvoiceItem\InvoiceItemRepositoryInterface;
|
||||
use App\Repositories\Module\Hosting\HostingRepositoryInterface as ModuleHostingRepositoryInterface;
|
||||
@@ -21,23 +19,16 @@ use App\Repositories\Module\IpAddress\IpAddressRepositoryInterface;
|
||||
use App\Repositories\Module\Vmware\VmwareRepositoryInterface;
|
||||
use App\Repositories\Order\OrderRepositoryInterface;
|
||||
use App\Repositories\Os\OsRepositoryInterface;
|
||||
use App\Repositories\VPS\VPSRepositoryInterface;
|
||||
use App\Models\Customer;
|
||||
use Carbon\Carbon;
|
||||
use App\Services\OrderService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Yajra\DataTables\Facades\DataTables;
|
||||
|
||||
class OrderController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly OrderRepositoryInterface $orderRepository,
|
||||
private readonly HostingRepositoryInterface $hostingRepository,
|
||||
private readonly VPSRepositoryInterface $vPSRepository,
|
||||
private readonly InvoiceRepositoryInterface $invoiceRepository,
|
||||
private readonly InvoiceItemRepositoryInterface $invoiceItemRepository,
|
||||
private readonly ModuleHostingRepositoryInterface $moduleHostingRepository,
|
||||
@@ -46,287 +37,62 @@ class OrderController extends Controller
|
||||
private readonly OsRepositoryInterface $osRepository,
|
||||
private readonly IpAddressRepositoryInterface $ipAddressRepository,
|
||||
private readonly AddonRepositoryInterface $addonRepository,
|
||||
private readonly OrderService $orderService,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$perPage = min((int) $request->get('per_page', 15), 100);
|
||||
|
||||
$query = $this->orderRepository->filter(
|
||||
|
||||
$data = $this->orderRepository->filter(
|
||||
$request->all(),
|
||||
['payment', 'customer', 'invoice'],
|
||||
[],
|
||||
['*'],
|
||||
true
|
||||
);
|
||||
)->paginate($perPage);
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => [
|
||||
'items' => OrderResource::collection($query->get())->resolve(),
|
||||
'total' => $query->count(),
|
||||
'per_page' => $perPage,
|
||||
'items' => OrderResource::collection($data->items())->resolve(),
|
||||
'total' => $data->total(),
|
||||
'per_page' => $data->perPage(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function datatable(Request $request): JsonResponse
|
||||
{
|
||||
$query = $this->orderRepository->filter(
|
||||
$request->all(),
|
||||
['payment', 'customer', 'invoice'],
|
||||
[],
|
||||
['*'],
|
||||
true
|
||||
);
|
||||
|
||||
return DataTables::eloquent($query)
|
||||
->addColumn('order_num', fn($d) => $d->order_num)
|
||||
->addColumn('date', fn($d) => $d->date)
|
||||
->addColumn('customer', fn($d) => $d->customer?->fullname)
|
||||
->addColumn('payment', fn($d) => $d->payment?->name)
|
||||
->addColumn('amount', fn($d) => $d->amount)
|
||||
->addColumn('invoice_status', fn($d) => $d->invoice?->status)
|
||||
->addColumn('status', fn($d) => $d->status)
|
||||
->addColumn('action', fn($d) => $d->id)
|
||||
->rawColumns(['order_num', 'date', 'customer', 'payment', 'amount', 'invoice_status', 'status', 'action'])
|
||||
->make(true);
|
||||
}
|
||||
|
||||
public function store(OrderRequest $request): JsonResponse
|
||||
{
|
||||
$request->validated();
|
||||
|
||||
$customer = Customer::find($request->customer_id);
|
||||
if (!$customer || (int) $customer->status !== 1) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => 'Tài khoản khách hàng không hoạt động. Không thể tạo đơn hàng.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$total = 0;
|
||||
$product_item = [];
|
||||
$relid_data = [];
|
||||
|
||||
foreach ($request->product as $key => $item) {
|
||||
$product = \App\Models\Product::with('category')->find($item['product_id']);
|
||||
|
||||
if (!$product) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => "Sản phẩm không tồn tại (ID: {$item['product_id']})."
|
||||
], 422);
|
||||
}
|
||||
|
||||
// Derive legacy numeric type from product_type for module creation
|
||||
$moduleType = match ($product->product_type) {
|
||||
'hosting' => 3,
|
||||
'vps' => 4,
|
||||
default => 0,
|
||||
};
|
||||
$relid_data[$key]['type'] = $moduleType;
|
||||
|
||||
$message = $product->name;
|
||||
$relid_data[$key]['data'] = [
|
||||
'customer_id' => $request->customer_id,
|
||||
'status' => 0,
|
||||
'name' => $item['domain'] ?? $product->name,
|
||||
];
|
||||
|
||||
$billingDetails = $this->calculateBillingDetails($item['billing']);
|
||||
$relid_data[$key]['data'] = array_merge($relid_data[$key]['data'], $billingDetails);
|
||||
|
||||
$price_select = $item['price_custom'] ?? $product->price_sale ?? $product->price;
|
||||
$relid_data[$key]['data']['first_payment'] = $price_select * $billingDetails['billing'];
|
||||
$relid_data[$key]['data']['next_payment'] = ($item['price_custom'] ?? $product->price) * $billingDetails['billing'];
|
||||
|
||||
$generalCode = Helper::generateUniqueCode();
|
||||
|
||||
$billingCycle = match ((int) $item['billing']) {
|
||||
2 => 'monthly',
|
||||
3 => 'quarterly',
|
||||
4 => 'semi_annually',
|
||||
5 => 'annually',
|
||||
default => 'monthly',
|
||||
};
|
||||
|
||||
$product_item[$key] = [
|
||||
'product_id' => $product->id,
|
||||
'product_type' => $product->product_type,
|
||||
'months' => $billingDetails['billing'],
|
||||
'billing_cycle' => $billingCycle,
|
||||
'os_id' => $item['os_id'] ?? null,
|
||||
'quantity' => (int) ($item['quantity'] ?? 1),
|
||||
'server_name' => $item['domain'] ?? null,
|
||||
'price' => (int) $price_select,
|
||||
'invoice_type' => 1,
|
||||
'code' => $generalCode,
|
||||
];
|
||||
|
||||
$relid_data[$key]['code'] = $generalCode;
|
||||
|
||||
if ($item['quantity'] > 1) {
|
||||
for ($i = 1; $i < $item['quantity']; $i++) {
|
||||
$newCode = Helper::generateUniqueCode();
|
||||
$product_item[] = array_merge($product_item[$key], ['code' => $newCode]);
|
||||
$relid_data[] = array_merge($relid_data[$key], ['code' => $newCode]);
|
||||
}
|
||||
}
|
||||
|
||||
$total += $item['quantity'] * ($item['price_custom'] ?? ($product->price_sale ?? $product->price)) * $billingDetails['billing'];
|
||||
}
|
||||
|
||||
$paymentRecord = \App\Models\Payment::find((int) $request->payment_method);
|
||||
$isBankTransfer = $paymentRecord && (int) $paymentRecord->type === 1;
|
||||
|
||||
$order_item = [
|
||||
'order_num' => Helper::generateUniqueCode(),
|
||||
'customer_id' => $request->customer_id,
|
||||
'invoice_id' => 0,
|
||||
'discount_id' => $request->discount_id ?: null,
|
||||
'amount' => $total + ($total * (10 / 100)),
|
||||
'payment_id' => $paymentRecord?->id,
|
||||
'payment_proof' => $request->payment_proof ?? null,
|
||||
'user_id' => Auth::guard('admin')->id(),
|
||||
'status' => $request->status,
|
||||
'client_ip' => $request->getClientIp(),
|
||||
];
|
||||
|
||||
$invoice_data = [
|
||||
'date' => Carbon::today()->toDateString(),
|
||||
'due_date' => Carbon::today()->toDateString(),
|
||||
'date_paid' => (int)$request->status === 1 ? Carbon::today()->toDateString() : null,
|
||||
'sub_total' => $total,
|
||||
'tax' => $total * (10 / 100),
|
||||
'total' => $total + ($total * (10 / 100)),
|
||||
'payment_id' => $paymentRecord?->id,
|
||||
'customer_id' => $request->create_invoice ? $request->customer_id : 0,
|
||||
'status' => $request->status
|
||||
];
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
$invoice = $this->invoiceRepository->create($invoice_data);
|
||||
|
||||
$product_item = array_map(function ($array) use ($invoice) {
|
||||
$array['invoice_id'] = $invoice->id;
|
||||
return $array;
|
||||
}, $product_item);
|
||||
|
||||
if ($request->create_invoice) {
|
||||
$order_item['invoice_id'] = $invoice->id;
|
||||
}
|
||||
|
||||
$order = $this->orderRepository->create($order_item);
|
||||
|
||||
$relid_data = array_map(function ($array) use ($order) {
|
||||
$array['data']['order_id'] = $order->id;
|
||||
return $array;
|
||||
}, $relid_data);
|
||||
|
||||
$relid = [];
|
||||
|
||||
foreach ($relid_data as $item) {
|
||||
$code = $item['code'];
|
||||
|
||||
switch ($item['type']) {
|
||||
case 3:
|
||||
$relid_id = $this->moduleHostingRepository->create($item['data']);
|
||||
$relid[$code] = $relid_id->id;
|
||||
break;
|
||||
case 4:
|
||||
$item['data']['status'] = 2;
|
||||
$relid_id = $this->moduleVmwareRepository->create($item['data']);
|
||||
$relid[$code] = $relid_id->id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$product_item = array_map(function ($array) {
|
||||
unset($array['code']);
|
||||
return $array;
|
||||
}, $product_item);
|
||||
|
||||
$invoiceItemData = array_map(function ($item) {
|
||||
return \Illuminate\Support\Arr::except($item, ['billing_cycle']);
|
||||
}, $product_item);
|
||||
|
||||
$this->invoiceItemRepository->insert($invoiceItemData);
|
||||
|
||||
$now = now();
|
||||
\App\Models\OrderItem::insert(array_map(function ($item) use ($now) {
|
||||
$expiryDate = $now->copy()->addMonths($item['months']);
|
||||
return [
|
||||
'order_id' => $order->id,
|
||||
'product_id' => $item['product_id'],
|
||||
'billing_cycle' => $item['billing_cycle'],
|
||||
'months' => $item['months'],
|
||||
'expiry_date' => $expiryDate,
|
||||
'os_id' => $item['os_id'],
|
||||
'quantity' => $item['quantity'],
|
||||
'server_name' => $item['server_name'],
|
||||
'price' => $item['price'],
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
}, $product_item));
|
||||
|
||||
// Dispatch VM creation job for VPS/cloud products
|
||||
$vpsTypes = ['vps', 'cloud'];
|
||||
$createdOrderItems = \App\Models\OrderItem::where('order_id', $order->id)->get();
|
||||
foreach ($createdOrderItems as $orderItem) {
|
||||
if (in_array($orderItem->product?->product_type, $vpsTypes)) {
|
||||
\App\Jobs\VirtualMachine\Create\CreateVirtualMachineJob::dispatch($orderItem)
|
||||
->delay(now()->addSeconds(10));
|
||||
}
|
||||
}
|
||||
|
||||
// Deduct customer balance for non-bank-transfer payments
|
||||
if (!$isBankTransfer) {
|
||||
Customer::where('id', $request->customer_id)
|
||||
->decrement('total', (int) ($total + ($total * (10 / 100))));
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
$order = $this->orderService->createOrder($request->validated());
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Order created successfully',
|
||||
'data' => new OrderResource($order)
|
||||
'data' => new OrderResource($order),
|
||||
], 201);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return response()->json(['error' => true, 'message' => $e->getMessage()], 422);
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
return response()->json(['error' => true, 'message' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function show(int $id): JsonResponse
|
||||
public function show(string $order_num): JsonResponse
|
||||
{
|
||||
$order = Order::with([
|
||||
'orderItems.product',
|
||||
'orderItems.os',
|
||||
'user',
|
||||
'customer',
|
||||
'payment',
|
||||
'discount',
|
||||
'invoice'
|
||||
])->find($id);
|
||||
$order = $this->orderRepository->findByOrderNum($order_num);
|
||||
|
||||
if (!$order) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => "Can't find order by ID #" . $id
|
||||
'message' => "Can't find order by ID #" . $order_num,
|
||||
], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => new OrderResource($order)
|
||||
'data' => new OrderResource($order),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -337,7 +103,7 @@ class OrderController extends Controller
|
||||
if (!$order) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => "Can't find order by ID #" . $id
|
||||
'message' => "Can't find order by ID #" . $id,
|
||||
], 404);
|
||||
}
|
||||
|
||||
@@ -389,16 +155,14 @@ class OrderController extends Controller
|
||||
|
||||
$ip_address = $this->ipAddressRepository->filter([
|
||||
'module_host_id' => $os->osGroup->module_host_id,
|
||||
'status' => 0
|
||||
'status' => 0,
|
||||
]);
|
||||
|
||||
if (count($ip_address) > 0) {
|
||||
$this->moduleVmwareRepository->update($item->relid, [
|
||||
'module_ipaddress_id' => $ip_address->first()->id,
|
||||
]);
|
||||
$this->ipAddressRepository->update($ip_address->first()->id, [
|
||||
'status' => 1
|
||||
]);
|
||||
$this->ipAddressRepository->update($ip_address->first()->id, ['status' => 1]);
|
||||
$jobs[] = new CreateVMJobs($product->id);
|
||||
}
|
||||
}
|
||||
@@ -417,16 +181,10 @@ class OrderController extends Controller
|
||||
|
||||
DB::commit();
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Order updated successfully'
|
||||
]);
|
||||
return response()->json(['error' => false, 'message' => 'Order updated successfully']);
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
return response()->json(['error' => true, 'message' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -437,7 +195,7 @@ class OrderController extends Controller
|
||||
if (!$order) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => "Can't find order by ID #" . $id
|
||||
'message' => "Can't find order by ID #" . $id,
|
||||
], 404);
|
||||
}
|
||||
|
||||
@@ -458,16 +216,10 @@ class OrderController extends Controller
|
||||
|
||||
DB::commit();
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Order deleted successfully'
|
||||
]);
|
||||
return response()->json(['error' => false, 'message' => 'Order deleted successfully']);
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
return response()->json(['error' => true, 'message' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -478,7 +230,7 @@ class OrderController extends Controller
|
||||
if (!$order) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => "Can't find order by ID #" . $id
|
||||
'message' => "Can't find order by ID #" . $id,
|
||||
], 404);
|
||||
}
|
||||
|
||||
@@ -487,16 +239,10 @@ class OrderController extends Controller
|
||||
$this->orderRepository->update($id, ['status' => 1]);
|
||||
DB::commit();
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Order accepted successfully'
|
||||
]);
|
||||
return response()->json(['error' => false, 'message' => 'Order accepted successfully']);
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
return response()->json(['error' => true, 'message' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -507,7 +253,7 @@ class OrderController extends Controller
|
||||
if (!$order) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => "Can't find order by ID #" . $id
|
||||
'message' => "Can't find order by ID #" . $id,
|
||||
], 404);
|
||||
}
|
||||
|
||||
@@ -516,16 +262,10 @@ class OrderController extends Controller
|
||||
$this->orderRepository->update($id, ['status' => 2]);
|
||||
DB::commit();
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Order cancelled successfully'
|
||||
]);
|
||||
return response()->json(['error' => false, 'message' => 'Order cancelled successfully']);
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
return response()->json(['error' => true, 'message' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -541,21 +281,4 @@ class OrderController extends Controller
|
||||
'path' => $path,
|
||||
]);
|
||||
}
|
||||
|
||||
private function calculateBillingDetails($billing): array
|
||||
{
|
||||
$months = match ($billing) {
|
||||
2 => 1,
|
||||
3 => 3,
|
||||
4 => 6,
|
||||
5 => 12,
|
||||
default => 0
|
||||
};
|
||||
|
||||
return [
|
||||
'next_due_date' => $months ? Carbon::today()->addMonths($months)->timestamp : null,
|
||||
'next_invoice' => $months ? Carbon::today()->addMonths($months)->timestamp : null,
|
||||
'billing' => $months ?: 1
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\System;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\CompressionService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CompressionTestController extends Controller
|
||||
{
|
||||
/**
|
||||
* Test compression with large dataset
|
||||
*/
|
||||
public function test(Request $request): JsonResponse
|
||||
{
|
||||
// Generate large test data
|
||||
$testData = [
|
||||
'message' => 'Compression test response',
|
||||
'timestamp' => now()->toISOString(),
|
||||
'client_capabilities' => CompressionService::getClientCapabilities($request),
|
||||
'large_data' => $this->generateTestData(),
|
||||
'meta' => [
|
||||
'total_items' => 1000,
|
||||
'page' => 1,
|
||||
'per_page' => 50,
|
||||
'compression_enabled' => true,
|
||||
]
|
||||
];
|
||||
|
||||
$content = json_encode($testData);
|
||||
$compressionInfo = CompressionService::getCompressionInfo($content);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $testData,
|
||||
'compression_analysis' => $compressionInfo,
|
||||
'response_size' => CompressionService::formatBytes(strlen($content))
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get compression statistics
|
||||
*/
|
||||
public function stats(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'client_capabilities' => CompressionService::getClientCapabilities($request),
|
||||
'server_capabilities' => [
|
||||
'gzip' => function_exists('gzencode'),
|
||||
'deflate' => function_exists('gzdeflate'),
|
||||
'brotli' => function_exists('brotli_compress'),
|
||||
],
|
||||
'middleware_enabled' => true,
|
||||
'min_compression_size' => '1KB',
|
||||
'compression_level' => 6,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate large test data for compression testing
|
||||
*/
|
||||
private function generateTestData(): array
|
||||
{
|
||||
$data = [];
|
||||
|
||||
// Generate repeated patterns that compress well
|
||||
for ($i = 1; $i <= 100; $i++) {
|
||||
$data[] = [
|
||||
'id' => $i,
|
||||
'name' => "Test Item Number {$i}",
|
||||
'description' => 'This is a test description that contains repetitive text to demonstrate compression effectiveness. Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
|
||||
'status' => $i % 2 === 0 ? 'active' : 'inactive',
|
||||
'created_at' => now()->subDays($i)->toISOString(),
|
||||
'metadata' => [
|
||||
'category' => 'test-category-' . ($i % 5),
|
||||
'tags' => ['test', 'compression', 'api', 'laravel'],
|
||||
'properties' => [
|
||||
'property_1' => 'Repeated property value',
|
||||
'property_2' => 'Another repeated property value',
|
||||
'property_3' => 'Yet another repeated property value',
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ class InvoiceController extends Controller
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'customer_id' => 'required|numeric',
|
||||
'payment_id' => 'required|numeric',
|
||||
'payment_method' => 'required|numeric',
|
||||
'sub_total' => 'required|numeric',
|
||||
'total' => 'required|numeric',
|
||||
]);
|
||||
@@ -58,7 +58,7 @@ class InvoiceController extends Controller
|
||||
'sub_total' => $validated['sub_total'],
|
||||
'tax' => $validated['sub_total'] * 0.1,
|
||||
'total' => $validated['total'],
|
||||
'payment_id' => $validated['payment_id'],
|
||||
'payment_method' => $validated['payment_method'],
|
||||
'customer_id' => $validated['customer_id'],
|
||||
'status' => 0,
|
||||
];
|
||||
|
||||
@@ -171,7 +171,7 @@ class OrderController extends Controller
|
||||
'invoice_id' => 0,
|
||||
'discount_id' => $request->discount_id ?? 0,
|
||||
'amount' => $total + ($total * (10 / 100)),
|
||||
'payment_id' => $request->payment_id,
|
||||
'payment_method' => $request->payment_method,
|
||||
'user_id' => auth()->guard('admin')->user()->id,
|
||||
'status' => $request->status,
|
||||
'client_ip' => $request->getClientIp()
|
||||
@@ -184,7 +184,7 @@ class OrderController extends Controller
|
||||
'sub_total' => $total,
|
||||
'tax' => $total * (10 / 100),
|
||||
'total' => $total + ($total * (10 / 100)),
|
||||
'payment_id' => $request->payment_id,
|
||||
'payment_method' => $request->payment_method,
|
||||
'customer_id' => $request->create_invoice ? $request->customer_id : 0,
|
||||
'status' => $request->status
|
||||
];
|
||||
|
||||
@@ -437,11 +437,11 @@ class Helper
|
||||
return $html;
|
||||
}
|
||||
|
||||
public static function generateUniqueCode(): int
|
||||
public static function generateUniqueCode(): string
|
||||
{
|
||||
do {
|
||||
$code = random_int(1000000, 99999999);
|
||||
} while (Order::where("order_num", "=", $code)->first());
|
||||
$code = 'ORD-' . strtoupper(\Illuminate\Support\Str::random(8));
|
||||
} while (Order::where('order_num', $code)->exists());
|
||||
|
||||
return $code;
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ if (!function_exists('user')) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('generate_order_number')) {
|
||||
if (!function_exists('generate_order_num')) {
|
||||
/**
|
||||
* Generate a unique order number with given prefix
|
||||
*
|
||||
@@ -80,7 +80,7 @@ if (!function_exists('generate_order_number')) {
|
||||
* @param int $length Length of random number (default 6)
|
||||
* @return string Unique order number
|
||||
*/
|
||||
function generate_order_number(string $prefix = 'TGS', int $length = 6): string
|
||||
function generate_order_num(string $prefix = 'TGS', int $length = 6): string
|
||||
{
|
||||
do {
|
||||
$randomNumber = random_int(10 ** ($length - 1), (10 ** $length) - 1);
|
||||
|
||||
+3
-2
@@ -42,6 +42,7 @@ class Kernel extends HttpKernel
|
||||
'api' => [
|
||||
'throttle:api',
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
\App\Http\Middleware\CompressionMiddleware::class,
|
||||
],
|
||||
];
|
||||
|
||||
@@ -63,13 +64,13 @@ class Kernel extends HttpKernel
|
||||
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
|
||||
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
|
||||
'LoginCheck' => \App\Http\Middleware\LoginCheck::class,
|
||||
'abilities' => \Laravel\Sanctum\Http\Middleware\CheckAbilities::class,
|
||||
'ability' => \Laravel\Sanctum\Http\Middleware\CheckForAnyAbility::class,
|
||||
'CustomerLoginCheck' => \App\Http\Middleware\CustomerLoginCheck::class,
|
||||
'IsLoginCheck' => \App\Http\Middleware\IsLoginCheck::class,
|
||||
'Staff' => \App\Http\Middleware\StaffCheck::class,
|
||||
'json.response' => \App\Http\Middleware\ForceJsonResponse::class,
|
||||
'api.auth' => \App\Http\Middleware\ApiAuthCheck::class,
|
||||
'perm' => \App\Http\Middleware\CheckPermission::class,
|
||||
'compression' => \App\Http\Middleware\CompressionMiddleware::class,
|
||||
'compression.advanced' => \App\Http\Middleware\AdvancedCompressionMiddleware::class,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class AdvancedCompressionMiddleware
|
||||
{
|
||||
/**
|
||||
* Compression settings
|
||||
*/
|
||||
private const MIN_LENGTH = 1024; // Minimum content length to compress (1KB)
|
||||
private const COMPRESSION_LEVEL = 6; // Compression level (1-9)
|
||||
private const MAX_COMPRESSION_RATIO = 0.9; // Don't compress if compression ratio > 90%
|
||||
|
||||
/**
|
||||
* Content types that should be compressed
|
||||
*/
|
||||
private const COMPRESSIBLE_TYPES = [
|
||||
'application/json',
|
||||
'text/plain',
|
||||
'text/html',
|
||||
'text/xml',
|
||||
'application/xml',
|
||||
'text/css',
|
||||
'application/javascript',
|
||||
'text/javascript'
|
||||
];
|
||||
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$response = $next($request);
|
||||
|
||||
if (!$this->shouldCompress($request, $response)) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
return $this->compressResponse($request, $response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compress the response content
|
||||
*/
|
||||
private function compressResponse(Request $request, Response $response): Response
|
||||
{
|
||||
$content = $response->getContent();
|
||||
$originalLength = strlen($content);
|
||||
|
||||
if ($originalLength < self::MIN_LENGTH) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$acceptEncoding = $request->header('Accept-Encoding', '');
|
||||
$compressed = null;
|
||||
$encoding = null;
|
||||
|
||||
// Try Brotli compression first (best compression ratio)
|
||||
if (str_contains($acceptEncoding, 'br') && function_exists('brotli_compress')) {
|
||||
$compressed = brotli_compress($content, self::COMPRESSION_LEVEL);
|
||||
$encoding = 'br';
|
||||
}
|
||||
// Try Gzip compression
|
||||
elseif (str_contains($acceptEncoding, 'gzip') && function_exists('gzencode')) {
|
||||
$compressed = gzencode($content, self::COMPRESSION_LEVEL);
|
||||
$encoding = 'gzip';
|
||||
}
|
||||
// Fallback to Deflate
|
||||
elseif (str_contains($acceptEncoding, 'deflate') && function_exists('gzdeflate')) {
|
||||
$compressed = gzdeflate($content, self::COMPRESSION_LEVEL);
|
||||
$encoding = 'deflate';
|
||||
}
|
||||
|
||||
// Check compression efficiency
|
||||
if ($compressed !== false && $encoding) {
|
||||
$compressionRatio = strlen($compressed) / $originalLength;
|
||||
|
||||
// Only use compressed content if it's significantly smaller
|
||||
if ($compressionRatio < self::MAX_COMPRESSION_RATIO) {
|
||||
$response->setContent($compressed);
|
||||
$response->headers->set('Content-Encoding', $encoding);
|
||||
$response->headers->set('Content-Length', strlen($compressed));
|
||||
$response->headers->set('Vary', 'Accept-Encoding');
|
||||
|
||||
// Add compression info to headers for debugging
|
||||
if (config('app.debug')) {
|
||||
$response->headers->set('X-Compression-Ratio', round($compressionRatio * 100, 2) . '%');
|
||||
$response->headers->set('X-Original-Size', $originalLength);
|
||||
$response->headers->set('X-Compressed-Size', strlen($compressed));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the response should be compressed
|
||||
*/
|
||||
private function shouldCompress(Request $request, Response $response): bool
|
||||
{
|
||||
// Don't compress if already compressed
|
||||
if ($response->headers->has('Content-Encoding')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't compress file downloads
|
||||
if ($response->headers->has('Content-Disposition')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only compress successful responses
|
||||
if ($response->getStatusCode() >= 400) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if content type is compressible
|
||||
$contentType = $response->headers->get('Content-Type', '');
|
||||
foreach (self::COMPRESSIBLE_TYPES as $type) {
|
||||
if (str_contains($contentType, $type)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class CompressionMiddleware
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$response = $next($request);
|
||||
|
||||
// Chỉ nén response cho JSON API
|
||||
if (!$this->shouldCompress($request, $response)) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$content = $response->getContent();
|
||||
|
||||
// Chỉ nén nếu content size > 1KB
|
||||
if (strlen($content) < 1024) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$acceptEncoding = $request->header('Accept-Encoding', '');
|
||||
|
||||
// Kiểm tra client hỗ trợ gzip
|
||||
if (str_contains($acceptEncoding, 'gzip') && function_exists('gzencode')) {
|
||||
$compressed = gzencode($content, 6); // Level 6 compression
|
||||
|
||||
if ($compressed !== false) {
|
||||
$response->setContent($compressed);
|
||||
$response->headers->set('Content-Encoding', 'gzip');
|
||||
$response->headers->set('Content-Length', strlen($compressed));
|
||||
$response->headers->set('Vary', 'Accept-Encoding');
|
||||
}
|
||||
}
|
||||
// Fallback to deflate if gzip not supported
|
||||
elseif (str_contains($acceptEncoding, 'deflate') && function_exists('gzdeflate')) {
|
||||
$compressed = gzdeflate($content, 6);
|
||||
|
||||
if ($compressed !== false) {
|
||||
$response->setContent($compressed);
|
||||
$response->headers->set('Content-Encoding', 'deflate');
|
||||
$response->headers->set('Content-Length', strlen($compressed));
|
||||
$response->headers->set('Vary', 'Accept-Encoding');
|
||||
}
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the response should be compressed.
|
||||
*/
|
||||
private function shouldCompress(Request $request, Response $response): bool
|
||||
{
|
||||
// Chỉ nén JSON responses
|
||||
$contentType = $response->headers->get('Content-Type', '');
|
||||
if (!str_contains($contentType, 'application/json')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Không nén nếu đã có Content-Encoding
|
||||
if ($response->headers->has('Content-Encoding')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Không nén file downloads
|
||||
if ($response->headers->has('Content-Disposition')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Chỉ nén response thành công
|
||||
if ($response->getStatusCode() >= 400) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user