dashboard in better style
This commit is contained in:
@@ -358,6 +358,146 @@ class ProductController extends SuperController
|
||||
return response()->json(true);
|
||||
}
|
||||
|
||||
private const ACTIVATION_SNAPSHOT_KEY = 'products_activation_snapshot';
|
||||
|
||||
public function activationSnapshotStatus()
|
||||
{
|
||||
$row = Cnt::where('key', self::ACTIVATION_SNAPSHOT_KEY)->first();
|
||||
$items = $row?->value_json['items'] ?? [];
|
||||
|
||||
return response()->json([
|
||||
'has_snapshot' => ! empty($items),
|
||||
'saved_at' => $row?->value_json['saved_at'] ?? null,
|
||||
'count' => count($items),
|
||||
]);
|
||||
}
|
||||
|
||||
public function deactivateAllProducts()
|
||||
{
|
||||
$products = Product::query()
|
||||
->where(function ($query) {
|
||||
$query->where('active', 1)
|
||||
->orWhere('open_buy', 1)
|
||||
->orWhere('open_sell', 1);
|
||||
})
|
||||
->get(['id', 'active', 'show', 'open_buy', 'open_sell']);
|
||||
|
||||
$items = $products->map(static fn (Product $product) => [
|
||||
'id' => $product->id,
|
||||
'active' => (bool) $product->active,
|
||||
'show' => (bool) $product->show,
|
||||
'open_buy' => (bool) $product->open_buy,
|
||||
'open_sell' => (bool) $product->open_sell,
|
||||
])->values()->all();
|
||||
|
||||
Cnt::updateOrCreate(
|
||||
['key' => self::ACTIVATION_SNAPSHOT_KEY],
|
||||
[
|
||||
'title_fa' => 'ذخیره وضعیت محصولات',
|
||||
'value_json' => [
|
||||
'saved_at' => now()->timezone('Asia/Tehran')->format('Y-m-d H:i:s'),
|
||||
'items' => $items,
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$affected = Product::query()->update([
|
||||
'active' => 0,
|
||||
'show' => 0,
|
||||
'open_buy' => 0,
|
||||
'open_sell' => 0,
|
||||
]);
|
||||
|
||||
$this->broadcastPricesAfterResponse();
|
||||
|
||||
return response()->json([
|
||||
'message' => 'همه محصولات بسته شد.',
|
||||
'snapshot_count' => count($items),
|
||||
'affected' => $affected,
|
||||
'has_snapshot' => ! empty($items),
|
||||
]);
|
||||
}
|
||||
|
||||
public function restoreActivationSnapshot()
|
||||
{
|
||||
$row = Cnt::where('key', self::ACTIVATION_SNAPSHOT_KEY)->first();
|
||||
$items = $row?->value_json['items'] ?? [];
|
||||
|
||||
if (empty($items)) {
|
||||
return response()->json(['error' => 'اسنAPSHOTی برای بازگردانی وجود ندارد.'], 422);
|
||||
}
|
||||
|
||||
$restored = 0;
|
||||
foreach ($items as $item) {
|
||||
$updated = Product::where('id', $item['id'] ?? 0)->update([
|
||||
'active' => ! empty($item['active']) ? 1 : 0,
|
||||
'show' => ! empty($item['show']) ? 1 : 0,
|
||||
'open_buy' => ! empty($item['open_buy']) ? 1 : 0,
|
||||
'open_sell' => ! empty($item['open_sell']) ? 1 : 0,
|
||||
]);
|
||||
|
||||
if ($updated) {
|
||||
$restored++;
|
||||
}
|
||||
}
|
||||
|
||||
$this->auto_close_open();
|
||||
$this->broadcastPricesAfterResponse();
|
||||
|
||||
return response()->json([
|
||||
'message' => 'وضعیت قبلی محصولات بازگردانی شد.',
|
||||
'restored' => $restored,
|
||||
'has_snapshot' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function closeAllBuy()
|
||||
{
|
||||
Product::query()->update(['open_buy' => 0]);
|
||||
$inactive = Product::where('open_buy', 0)->where('open_sell', 0)->update([
|
||||
'active' => 0,
|
||||
'show' => 0,
|
||||
]);
|
||||
|
||||
$this->broadcastPricesAfterResponse();
|
||||
|
||||
return response()->json([
|
||||
'message' => 'خرید همه محصولات بسته شد.',
|
||||
'inactive' => $inactive,
|
||||
]);
|
||||
}
|
||||
|
||||
public function closeAllSell()
|
||||
{
|
||||
Product::query()->update(['open_sell' => 0]);
|
||||
$inactive = Product::where('open_buy', 0)->where('open_sell', 0)->update([
|
||||
'active' => 0,
|
||||
'show' => 0,
|
||||
]);
|
||||
|
||||
$this->broadcastPricesAfterResponse();
|
||||
|
||||
return response()->json([
|
||||
'message' => 'فروش همه محصولات بسته شد.',
|
||||
'inactive' => $inactive,
|
||||
]);
|
||||
}
|
||||
|
||||
public function syncProductShow()
|
||||
{
|
||||
$this->auto_close_open();
|
||||
$this->broadcastPricesAfterResponse();
|
||||
|
||||
return response()->json(['message' => 'نمایش محصولات طبق ساعات بهروز شد.']);
|
||||
}
|
||||
|
||||
private function broadcastPricesAfterResponse(): void
|
||||
{
|
||||
dispatch(function () {
|
||||
app(PriceBroadcastService::class)->broadcastAfterPriceUpdate();
|
||||
})->afterResponse();
|
||||
}
|
||||
|
||||
public function updatePrice(Request $request){
|
||||
|
||||
|
||||
|
||||
@@ -5,54 +5,106 @@ namespace App\Http\Controllers\Admin;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
class SettingsController extends Controller
|
||||
{
|
||||
private const DEPLOY_ADMIN_MOBILE_SUFFIX = '9128081392';
|
||||
|
||||
private const NEW_SETTING_NOTIFY_MOBILE = '09134469985';
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$query = Setting::query()
|
||||
->select(Setting::adminSelectColumns())
|
||||
->orderBy('key');
|
||||
|
||||
if (!$request->boolean('editable_only')) {
|
||||
return response()->json($query->get());
|
||||
if ($request->boolean('system_only')) {
|
||||
return response()->json(
|
||||
$query
|
||||
->where(function ($q) {
|
||||
$q->where('can_edit', 0)->orWhere('can_edit', false);
|
||||
})
|
||||
->where(function ($q) {
|
||||
$q->where('is_active', 1)
|
||||
->orWhere('is_active', true)
|
||||
->orWhere('is_active', '1');
|
||||
})
|
||||
->get()
|
||||
);
|
||||
}
|
||||
|
||||
return response()->json($query->where('can_edit', 1)->get());
|
||||
if ($request->boolean('editable_only')) {
|
||||
return response()->json($query->where('can_edit', 1)->get());
|
||||
}
|
||||
|
||||
return response()->json($query->get());
|
||||
}
|
||||
|
||||
public function edit(Request $request)
|
||||
{
|
||||
// Check if the setting exists
|
||||
$isDeployAdmin = $this->isDeployAdminUser();
|
||||
$isSystemDeploy = $request->boolean('system_deploy');
|
||||
$setting = Setting::where('key', $request->key)->first();
|
||||
$isNew = !$setting;
|
||||
|
||||
if ($setting) {
|
||||
$setting->value = $request->value;
|
||||
$setting->description = $request->description;
|
||||
if ($request->has('is_active')) {
|
||||
$setting->is_active = filter_var($request->is_active, FILTER_VALIDATE_BOOLEAN) ? 1 : 0;
|
||||
if ($isNew) {
|
||||
if ($isSystemDeploy && !$isDeployAdmin) {
|
||||
return response()->json(['message' => 'Unauthorized'], 403);
|
||||
}
|
||||
$setting->title_fa = $request->title_fa ?? '';
|
||||
if (Setting::hasTypeColumn()) {
|
||||
$setting->type = $request->type ?? '';
|
||||
}
|
||||
$setting->save();
|
||||
} else {
|
||||
|
||||
$setting = new Setting();
|
||||
$setting->key = $request->key;
|
||||
$setting->title_fa = $request->title_fa ?? '';
|
||||
$setting->value = $request->value;
|
||||
$setting->description = $request->description;
|
||||
$setting->is_active = filter_var($request->is_active ?? true, FILTER_VALIDATE_BOOLEAN) ? 1 : 0;
|
||||
$setting->can_edit = 1;
|
||||
if (Setting::hasTypeColumn()) {
|
||||
$setting->type = $request->type ?? '';
|
||||
}
|
||||
$setting->save();
|
||||
$setting->can_edit = $isSystemDeploy ? 0 : 1;
|
||||
} elseif (!$setting->can_edit && !$isDeployAdmin) {
|
||||
return response()->json(['message' => 'Forbidden'], 403);
|
||||
}
|
||||
|
||||
$setting->value = $request->value;
|
||||
$setting->description = $request->description;
|
||||
if ($request->has('is_active')) {
|
||||
$setting->is_active = filter_var($request->is_active, FILTER_VALIDATE_BOOLEAN) ? 1 : 0;
|
||||
} elseif ($isNew) {
|
||||
$setting->is_active = 1;
|
||||
}
|
||||
$setting->title_fa = $request->title_fa ?? '';
|
||||
if (Setting::hasTypeColumn()) {
|
||||
$setting->type = $request->type ?? '';
|
||||
}
|
||||
$setting->save();
|
||||
|
||||
if ($isNew && $isSystemDeploy) {
|
||||
$projectName = Setting::where('key', 'name_fa')->value('value') ?: 'پروژه';
|
||||
$settingTitle = trim($request->title_fa ?? '') ?: $request->key;
|
||||
$this->Send(
|
||||
self::NEW_SETTING_NOTIFY_MOBILE,
|
||||
"تنظیم جدید: {$settingTitle}\nپروژه: {$projectName}",
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
// Return the setting as a JSON response
|
||||
return response()->json($setting);
|
||||
}
|
||||
|
||||
private function normalizeAdminMobile(?string $mobile): string
|
||||
{
|
||||
$digits = preg_replace('/\D+/', '', (string) $mobile) ?? '';
|
||||
if (str_starts_with($digits, '98') && strlen($digits) >= 12) {
|
||||
$digits = substr($digits, 2);
|
||||
}
|
||||
if (strlen($digits) === 11 && str_starts_with($digits, '0')) {
|
||||
$digits = substr($digits, 1);
|
||||
}
|
||||
|
||||
return $digits;
|
||||
}
|
||||
|
||||
private function isDeployAdminUser(): bool
|
||||
{
|
||||
$user = auth('user')->user() ?? auth()->user();
|
||||
if (!$user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->normalizeAdminMobile($user->mobile ?? '') === self::DEPLOY_ADMIN_MOBILE_SUFFIX;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,11 @@ class ZaarboxPriceService
|
||||
|
||||
private const CACHE_TTL_MINUTES = 35;
|
||||
|
||||
/** بعد از خطای Zaarbox — TTL کوتاه تا با برگشت سرویس دوباره fetch شود */
|
||||
private const FAILED_CACHE_TTL_MINUTES = 2;
|
||||
|
||||
private const LAST_GOOD_CACHE_SUFFIX = ':last_good';
|
||||
|
||||
/** TTL کش فید — کران هر ۳۰ث refresh میکند؛ list فقط از همین میخواند */
|
||||
private const PRICE_LIST_CACHE_SECONDS = 15;
|
||||
|
||||
@@ -85,28 +90,71 @@ class ZaarboxPriceService
|
||||
return self::CACHE_KEY . ':' . $this->historyId();
|
||||
}
|
||||
|
||||
private function lastGoodCacheKey(): string
|
||||
{
|
||||
return $this->cacheKey() . self::LAST_GOOD_CACHE_SUFFIX;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{DAILY?: array, WEEKLY?: array, MONTHLY?: array} $prices
|
||||
*/
|
||||
private function hasChartData(array $prices): bool
|
||||
{
|
||||
foreach (['DAILY', 'WEEKLY', 'MONTHLY'] as $period) {
|
||||
if (! empty($prices[$period])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{DAILY: array, WEEKLY: array, MONTHLY: array}|null
|
||||
*/
|
||||
private function resolveLastGoodPrices(): ?array
|
||||
{
|
||||
$lastGood = Cache::get($this->lastGoodCacheKey());
|
||||
|
||||
if (is_array($lastGood['prices'] ?? null) && $this->hasChartData($lastGood['prices'])) {
|
||||
return $lastGood['prices'];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{DAILY: array<int, array{timestamp: int, value: int}>, WEEKLY: array, MONTHLY: array}
|
||||
*/
|
||||
public function getAllPrices(): array
|
||||
{
|
||||
$snapshot = app(PriceSnapshotService::class);
|
||||
$minRial = $snapshot->minValidChartPriceRialForExternalFeed();
|
||||
|
||||
if ($this->config->getBool('chart_use_local_data', false)) {
|
||||
$local = $snapshot->chartFromDatabase();
|
||||
if (! empty($local['DAILY'])) {
|
||||
return $snapshot->sanitizeChartPrices($local);
|
||||
$sanitizedLocal = $snapshot->sanitizeChartPrices($snapshot->chartFromDatabase());
|
||||
if ($this->hasChartData($sanitizedLocal)) {
|
||||
return $sanitizedLocal;
|
||||
}
|
||||
}
|
||||
|
||||
$minRial = $snapshot->minValidChartPriceRialForExternalFeed();
|
||||
$cached = Cache::get($this->cacheKey());
|
||||
|
||||
if (is_array($cached) && isset($cached['prices'])) {
|
||||
if (is_array($cached['prices'] ?? null) && $this->hasChartData($cached['prices'])) {
|
||||
return $snapshot->sanitizeChartPrices($cached['prices'], $minRial);
|
||||
}
|
||||
|
||||
return $snapshot->sanitizeChartPrices($this->refreshCache(), $minRial);
|
||||
$fresh = $this->refreshCache();
|
||||
$sanitized = $snapshot->sanitizeChartPrices($fresh, $minRial);
|
||||
if ($this->hasChartData($sanitized)) {
|
||||
return $sanitized;
|
||||
}
|
||||
|
||||
$lastGood = $this->resolveLastGoodPrices();
|
||||
if ($lastGood !== null) {
|
||||
return $snapshot->sanitizeChartPrices($lastGood, $minRial);
|
||||
}
|
||||
|
||||
return $sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,11 +163,29 @@ class ZaarboxPriceService
|
||||
public function refreshCache(): array
|
||||
{
|
||||
$prices = $this->fetchChartPrices();
|
||||
|
||||
Cache::put($this->cacheKey(), [
|
||||
$cacheKey = $this->cacheKey();
|
||||
$payload = [
|
||||
'prices' => $prices,
|
||||
'refreshed_at' => now()->toIso8601String(),
|
||||
], now()->addMinutes(self::CACHE_TTL_MINUTES));
|
||||
];
|
||||
|
||||
if ($this->hasChartData($prices)) {
|
||||
Cache::put($cacheKey, $payload, now()->addMinutes(self::CACHE_TTL_MINUTES));
|
||||
Cache::put($this->lastGoodCacheKey(), $payload, now()->addHours(24));
|
||||
|
||||
return $prices;
|
||||
}
|
||||
|
||||
Log::warning('Zaarbox chart refresh returned no data', [
|
||||
'history_id' => $this->historyId(),
|
||||
]);
|
||||
|
||||
$lastGood = $this->resolveLastGoodPrices();
|
||||
if ($lastGood !== null) {
|
||||
return $lastGood;
|
||||
}
|
||||
|
||||
Cache::put($cacheKey, array_merge($payload, ['failed' => true]), now()->addMinutes(self::FAILED_CACHE_TTL_MINUTES));
|
||||
|
||||
return $prices;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
-- حذف setting اشتباهی: has_page_earn_moneyB (تایپو؛ در کد استفاده نمیشود؛ جایگزین: has_page_earn_money)
|
||||
DELETE FROM `settings` WHERE `key` = 'has_page_earn_moneyB';
|
||||
@@ -52,6 +52,14 @@ Route::middleware('auth:user')->group(function (){
|
||||
Route::get('/product/auto_sell/{id}',[ProductController::class, 'auto_sell']);
|
||||
Route::get('/product/auto_buy/{id}',[ProductController::class, 'auto_buy']);
|
||||
Route::get('/product/auto/{method}/{action}',[ProductController::class, 'auto_group']);
|
||||
Route::prefix('products/quick')->group(function () {
|
||||
Route::get('activation-snapshot', [ProductController::class, 'activationSnapshotStatus']);
|
||||
Route::post('deactivate-all', [ProductController::class, 'deactivateAllProducts']);
|
||||
Route::post('restore-activation', [ProductController::class, 'restoreActivationSnapshot']);
|
||||
Route::post('close-buy', [ProductController::class, 'closeAllBuy']);
|
||||
Route::post('close-sell', [ProductController::class, 'closeAllSell']);
|
||||
Route::post('sync-show', [ProductController::class, 'syncProductShow']);
|
||||
});
|
||||
Route::get('/getProduct/{id}',[ProductController::class, 'getProduct']);
|
||||
Route::put('/products',[ProductController::class, 'editProducts']);
|
||||
Route::resource('/product',ProductController::class)->except(['destroy']);
|
||||
|
||||
Reference in New Issue
Block a user