diff --git a/app/Admin/Controllers/CockpitController.php b/app/Admin/Controllers/CockpitController.php index ae6da9e..7fed793 100644 --- a/app/Admin/Controllers/CockpitController.php +++ b/app/Admin/Controllers/CockpitController.php @@ -6,11 +6,12 @@ use App\Admin\Filters\StoreFilter; use App\Http\Controllers\Controller; use App\Models\Employee; use App\Models\Keyword; -use App\Models\Ledger; use App\Models\LedgerItem; use App\Models\Store; use App\Models\TaskPerformance; +use App\Services\StatisticService; use Illuminate\Http\Request; +use Illuminate\Support\Arr; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Storage; @@ -37,7 +38,7 @@ class CockpitController extends Controller /** * 销售趋势 */ - public function salesTrend(Request $request): array + public function salesTrend(Request $request, StatisticService $statisticService): array { $request->validate( rules: [ @@ -47,15 +48,12 @@ class CockpitController extends Controller $last = $request->input('last'); - $data = collect(); - if (in_array($last, ['7days', '30days'])) { // 按天 $days = match ($last) { '7days' => 7, '30days' => 30, }; - // 今天 $today = Carbon::today(); // 开始时间 @@ -63,24 +61,7 @@ class CockpitController extends Controller // 结束时间 $endAt = $today->copy()->subDay(); - $ledgers = Ledger::select(['date', DB::raw('SUM(sales) as sales')]) - ->whereBetween('date', [$startAt->toDateString(), $endAt->toDateString()]) - ->groupBy('date') - ->get() - ->keyBy('date'); - - while ($startAt->lte($endAt)) { - $date = $startAt->toDateString(); - - $ledger = $ledgers->get($date); - - $data->push([ - 'date' => $date, - 'sales' => trim_zeros($ledger->sales ?? 0), - ]); - - $startAt->addDay(); - } + return $statisticService->dailyLedgerTrend($startAt, $endAt); } elseif (in_array($last, ['180days', '365days'])) { // 按月 @@ -88,7 +69,6 @@ class CockpitController extends Controller '180days' => 6, // 6个月 '365days' => 12, // 12个月 }; - // 今天 $today = Carbon::today(); // 开始时间 @@ -96,27 +76,10 @@ class CockpitController extends Controller // 结束时间 $endAt = $today->copy()->startOfMonth()->subMonth()->endOfMonth(); - $ledgers = Ledger::select([DB::raw("DATE_FORMAT(`date`, '%Y-%m') as month"), DB::raw('SUM(sales) as sales')]) - ->whereBetween('date', [$startAt->toDateString(), $endAt->toDateString()]) - ->groupBy('month') - ->get() - ->keyBy('month'); - - for ($i=0; $i < $months; $i++) { - $month = $startAt->format('Y-m'); - - $ledger = $ledgers->get($month); - - $data->push([ - 'month' => $month, - 'sales' => trim_zeros($ledger->sales ?? 0), - ]); - - $startAt->addMonth(); - } + return $statisticService->monthlyLedgerTrend($startAt, $endAt); } - return $data->all(); + return []; } /** @@ -205,7 +168,7 @@ class CockpitController extends Controller ->whereBetween('date', [$startAt->toDateString(), $endAt->toDateString()]) ->groupBy(['month', 'ledger_item_type_id']) ->get() - ->keyBy('month'); + ->groupBy('month'); for ($i=0; $i < $months; $i++) { $month = $startAt->format('Y-m'); @@ -231,7 +194,7 @@ class CockpitController extends Controller return [ 'lottery_types' => $lotteryTypes->map(function ($lotteryType) { return [ - 'id' => $lotteryType->id, + 'id' => $lotteryType->key, 'name' => $lotteryType->name, ]; }), @@ -242,7 +205,7 @@ class CockpitController extends Controller /** * 门店销量排名 */ - public function storeSalesRanking(Request $request): array + public function storeSalesRanking(Request $request, StatisticService $statisticService): array { $request->validate( rules: [ @@ -250,50 +213,25 @@ class CockpitController extends Controller ], ); + $today = Carbon::today(); $last = $request->input('last'); + $input = ['sort' => '-sales']; - $storeSales = Ledger::select(['store_id', DB::raw('SUM(sales) as sales')]) - ->when($last, function ($query, $last) { - $today = Carbon::today(); + if (in_array($last, ['7days', '30days'])) { + $days = match ($last) { + '7days' => 7, + '30days' => 30, + }; + $input['date_range'] = $today->copy()->subDays($days)->toDateString().','.$today->copy()->subDay()->toDateString(); + } elseif (in_array($last, ['180days', '365days'])) { + $months = match ($last) { + '180days' => 6, // 6个月 + '365days' => 12, // 12个月 + }; + $input['date_range'] = $today->copy()->startOfMonth()->subMonths($months)->toDateString().','.$today->copy()->startOfMonth()->subMonth()->endOfMonth()->toDateString(); + } - if (in_array($last, ['7days', '30days'])) { - $days = match ($last) { - '7days' => 7, - '30days' => 30, - }; - - $query->whereBetween('date', [ - $today->copy()->subDays($days)->toDateString(), - $today->copy()->subDay()->toDateString(), - ]); - } elseif (in_array($last, ['180days', '365days'])) { - $months = match ($last) { - '180days' => 6, // 6个月 - '365days' => 12, // 12个月 - }; - - $query->whereBetween('date', [ - $today->copy()->startOfMonth()->subMonths($months)->toDateString(), - $today->copy()->startOfMonth()->subMonth()->endOfMonth()->toDateString(), - ]); - } - }) - ->groupBy('store_id'); - - $stores = Store::leftJoinSub($storeSales, 'store_sales', fn ($join) => $join->on('stores.id', '=', 'store_sales.store_id')) - ->orderBy('store_sales.sales', 'DESC') - ->limit(30) - ->get(); - - return $stores->map(function (Store $store) { - return [ - 'store' => [ - 'id' => $store->id, - 'title' => $store->title, - ], - 'sales' => trim_zeros($store->sales ?: 0), - ]; - })->all(); + return $statisticService->storeRanking($input, 30); } /** @@ -378,4 +316,65 @@ class CockpitController extends Controller ]); })->all(); } + + /** + * 门店分类 + */ + public function storeCategory(Request $request): array + { + $request->validate( + rules: [ + 'category_id' => ['filled'], + ], + ); + + $categoryId = $request->input('category_id', 'store_category'); + + $categories = collect(); + + if ($parent = Keyword::where('key', $categoryId)->first()) { + /** @var \Illuminate\Database\Eloquent\Collection */ + $descendants = Keyword::where('path', 'like', "%-{$parent->id}-%")->get(); + + $categories = $descendants->where('parent_id', $parent->id) + ->map(function ($category) use ($descendants) { + return [ + 'id' => $category->key, + 'name' => $category->name, + 'descendants' => $descendants->filter(function ($descendant) use ($category) { + return preg_match("/-{$category->id}-/", $descendant->path); + })->map(function ($descendant) { + return [ + 'id' => $descendant->key, + 'name' => $descendant->name, + ]; + })->all(), + ]; + }); + } + + $aggregates = Store::select(['category_id', DB::raw('count(1) as stores_count')]) + ->onlyOpen() + ->groupBy('category_id') + ->get() + ->keyBy('category_id'); + + return $categories->map(function ($category) use ($aggregates) { + $storesCount = 0; + + $descendants = collect( + Arr::pull($category, 'descendants') + )->push($category); + + foreach ($descendants as $item) { + if ($aggregate = $aggregates->get($item['id'])) { + $storesCount += $aggregate->stores_count; + } + } + + return array_merge($category, [ + 'stores_count' => $storesCount, + ]); + })->values()->all(); + } } diff --git a/app/Admin/Controllers/Finance/StoreStatisticController.php b/app/Admin/Controllers/Finance/StoreStatisticController.php index 306245d..fd054ee 100644 --- a/app/Admin/Controllers/Finance/StoreStatisticController.php +++ b/app/Admin/Controllers/Finance/StoreStatisticController.php @@ -4,21 +4,22 @@ namespace App\Admin\Controllers\Finance; use App\Admin\Controllers\AdminController; use App\Services\StatisticService; +use Illuminate\Support\Arr; class StoreStatisticController extends AdminController { public function index() { if ($this->actionOfGetData()) { - $input = request()->input(); + $input = Arr::except(request()->input(), ['orderBy', 'orderDir']); - $sorts = [ - [request()->input('orderBy') ?: 'sales', request()->input('orderDir') ?: 'desc'], - ['id', 'asc'], - ]; + $orderBy = request()->input('orderBy') ?: 'sales'; + $orderDir = request()->input('orderDir') ?: 'desc'; + + $input['sort'] = ($orderDir === 'desc' ? '-' : '').$orderBy; return $this->response()->success([ - 'items' => (new StatisticService())->stores($input, $sorts), + 'items' => (new StatisticService())->storeRanking($input), ]); } diff --git a/app/Admin/Controllers/Store/StoreController.php b/app/Admin/Controllers/Store/StoreController.php index 917ebfa..0ef91bf 100644 --- a/app/Admin/Controllers/Store/StoreController.php +++ b/app/Admin/Controllers/Store/StoreController.php @@ -30,42 +30,43 @@ class StoreController extends AdminController ->bulkActions([]) ->filter($this->baseFilter()->body([ amis()->GroupControl()->mode('horizontal')->body([ - amisMake()->TextControl()->name('title')->label(__('store.title'))->columnRatio(3)->clearable(), - amisMake()->TreeSelectControl()->name('category_id')->label(__('store.category_id'))->columnRatio(3) + amis()->TextControl()->name('title')->label(__('store.title'))->columnRatio(3)->clearable(), + amis()->TreeSelectControl()->name('category_id')->label(__('store.category_id'))->columnRatio(3) ->source(admin_url('api/keywords/tree-list?parent_key=store_category')) ->labelField('name') ->valueField('key') ->onlyLeaf(true) ->clearable(), - amisMake()->SelectControl()->name('business_id')->label(__('store.business_id'))->columnRatio(3) + amis()->SelectControl()->name('business_id')->label(__('store.business_id'))->columnRatio(3) ->source(admin_url('api/keywords/tree-list?parent_key=store_business')) ->labelField('name') ->valueField('key') ->clearable(), ]), - amisMake()->GroupControl()->mode('horizontal')->body([ - amisMake()->SelectControl()->name('level_id')->label(__('store.level_id'))->columnRatio(3) + amis()->GroupControl()->mode('horizontal')->body([ + amis()->SelectControl()->name('level_id')->label(__('store.level_id'))->columnRatio(3) ->source(admin_url('api/keywords/tree-list?parent_key=store_level')) ->labelField('name') ->valueField('key') ->clearable(), - // amisMake()->InputCityControl()->name('region')->label(__('store.region'))->columnRatio(3) + // amis()->InputCityControl()->name('region')->label(__('store.region'))->columnRatio(3) // ->allowDistrict(false) // ->extractValue(false) // ->clearable(), - amisMake()->SelectControl()->name('business_status')->label(__('store.business_status'))->options(BusinessStatus::options())->columnRatio(3)->clearable(), + amis()->SelectControl()->name('business_status')->label(__('store.business_status'))->options(BusinessStatus::options())->columnRatio(3)->clearable(), ]), ])) ->columns([ - amisMake()->TableColumn()->name('id')->label(__('store.id')), - amisMake()->TableColumn()->name('title')->label(__('store.title')), - amisMake()->TableColumn()->name('master.name')->label(__('store.master_id')), - amisMake()->TableColumn()->name('category.name')->label(__('store.category_id')), - amisMake()->TableColumn()->name('business.name')->label(__('store.business_id')), - amisMake()->TableColumn()->name('level.name')->label(__('store.level_id')), - amisMake()->TableColumn()->name('region')->label(__('store.region'))->set('type', 'tpl')->set('tpl', '${region.province}-${region.city}'), - amisMake()->TableColumn()->name('business_status')->label(__('store.business_status'))->type('switch')->trueValue(BusinessStatus::Open)->falseValue(BusinessStatus::Close), - amisMake()->TableColumn()->name('created_at')->label(__('store.created_at')), + amis()->TableColumn()->name('id')->label(__('store.id')), + amis()->TableColumn()->name('title')->label(__('store.title')), + amis()->TableColumn()->name('master.name')->label(__('store.master_id')), + amis()->TableColumn()->name('category.name')->label(__('store.category_id')), + amis()->TableColumn()->name('business.name')->label(__('store.business_id')), + amis()->TableColumn()->name('level.name')->label(__('store.level_id')), + amis()->TableColumn()->name('profit_ratio')->label(__('store.profit_ratio'))->type('tpl')->set('tpl', '${profit_ratio}%'), + amis()->TableColumn()->name('region')->label(__('store.region'))->set('type', 'tpl')->set('tpl', '${region.province}-${region.city}'), + amis()->TableColumn()->name('business_status')->label(__('store.business_status'))->type('switch')->trueValue(BusinessStatus::Open)->falseValue(BusinessStatus::Close), + amis()->TableColumn()->name('created_at')->label(__('store.created_at')), $this->rowActions([ $this->rowShowButton()->visible($user->can('admin.store.stores.view')), $this->rowEditTypeButton('drawer', 'lg')->visible($user->can('admin.store.stores.update')), @@ -79,40 +80,47 @@ class StoreController extends AdminController public function form($edit): Form { return $this->baseForm()->title('')->body([ - amisMake()->TextControl()->name('title')->label(__('store.title'))->required(), - amisMake()->SelectControl()->name('master_id')->label(__('store.master_id')) + amis()->TextControl()->name('title')->label(__('store.title'))->required(), + amis()->SelectControl()->name('master_id')->label(__('store.master_id')) ->source($edit ? admin_url('api/employees?_all=1&employee_status='.EmployeeStatus::Online->value) : admin_url('api/employees?_all=1&store_id=0&employee_status='.EmployeeStatus::Online->value)) ->labelField('name') ->valueField('id') ->searchable() ->required(), - amisMake()->TreeSelectControl()->name('category_id')->label(__('store.category_id')) + amis()->TreeSelectControl()->name('category_id')->label(__('store.category_id')) ->source(admin_url('api/keywords/tree-list?parent_key=store_category')) ->labelField('name') ->valueField('key') ->onlyLeaf(true) ->required(), - amisMake()->SelectControl()->name('business_id')->label(__('store.business_id')) + amis()->SelectControl()->name('business_id')->label(__('store.business_id')) ->source(admin_url('api/keywords/tree-list?parent_key=store_business')) ->labelField('name') ->valueField('key') ->required(), - amisMake()->SelectControl()->name('level_id')->label(__('store.level_id')) + amis()->SelectControl()->name('level_id')->label(__('store.level_id')) ->source(admin_url('api/keywords/tree-list?parent_key=store_level')) ->labelField('name') ->valueField('key') ->required(), - amisMake()->InputCityControl()->name('region')->label(__('store.region'))->allowDistrict(false)->extractValue(false)->required(), - amisMake()->LocationControl()->name('location')->label(__('store.location'))->ak(config('baidu.js_secret'))->autoSelectCurrentLoc(), + amis()->NumberControl() + ->name('profit_ratio') + ->label(__('store.profit_ratio')) + ->placeholder(__('store.profit_ratio')) + ->precision(2) + ->showSteps(false) + ->required(), + amis()->InputCityControl()->name('region')->label(__('store.region'))->allowDistrict(false)->extractValue(false)->required(), + amis()->LocationControl()->name('location')->label(__('store.location'))->ak(config('baidu.js_secret'))->autoSelectCurrentLoc(), ]); } public function detail(): Form { - $detail = amisMake()->Property()->items([ + $detail = amis()->Property()->items([ ['label' => __('store.title'), 'content' => '${title}'], ['label' => __('store.master_id'), 'content' => '${master.name}'], - ['label' => __('store.business_status'), 'content' => amisMake()->Tag()->label('${business_status_text}')->color('${business_status_color}')], + ['label' => __('store.business_status'), 'content' => amis()->Tag()->label('${business_status_text}')->color('${business_status_color}')], ['label' => __('store.category_id'), 'content' => '${category.name}'], ['label' => __('store.business_id'), 'content' => '${business.name}'], ['label' => __('store.level_id'), 'content' => '${level.name}'], diff --git a/app/Admin/Services/StoreService.php b/app/Admin/Services/StoreService.php index 1ca1e82..f6f2ebb 100644 --- a/app/Admin/Services/StoreService.php +++ b/app/Admin/Services/StoreService.php @@ -76,6 +76,7 @@ class StoreService extends BaseService 'region' => ['required'], 'lon' => ['required'], 'lat' => ['required'], + 'profit_ratio' => ['required', 'numeric', 'min:0', 'max:100'], ]; $updateRules = [ 'master_id' => [Rule::unique('stores', 'master_id')->ignore($model, 'master_id')], diff --git a/app/Admin/routes.php b/app/Admin/routes.php index bafb6dc..eec4c0e 100644 --- a/app/Admin/routes.php +++ b/app/Admin/routes.php @@ -254,6 +254,8 @@ Route::group([ $router->get('cockpit/store-number-distribution', [CockpitController::class, 'storeNumberDistribution']); // 门店分布(按城市) $router->get('cockpit/store-distribution', [CockpitController::class, 'storeDistribution']); + // 门店分类 + $router->get('cockpit/store-category', [CockpitController::class, 'storeCategory']); // 年度目标 $router->get('cockpit/yearly-goals', [CockpitController::class, 'yearlyGoals']); }); diff --git a/app/Http/Controllers/Api/LedgerController.php b/app/Http/Controllers/Api/LedgerController.php index aba1220..fad939c 100644 --- a/app/Http/Controllers/Api/LedgerController.php +++ b/app/Http/Controllers/Api/LedgerController.php @@ -98,7 +98,7 @@ class LedgerController extends Controller throw new RuntimeException('上报数据已更新,不可重新上传'); } - $ratio = bcdiv($user->store->profit_ratio, 100, 2); + $ratio = bcdiv($user->store->profit_ratio, 100, 4); // 计算预期佣金 $validated['expected_commission'] = bcmul($validated['sales'], $ratio, 2); diff --git a/app/Http/Controllers/Api/StatisticsController.php b/app/Http/Controllers/Api/StatisticsController.php index 4539bbe..f6a8525 100644 --- a/app/Http/Controllers/Api/StatisticsController.php +++ b/app/Http/Controllers/Api/StatisticsController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers\Api; use App\Services\StatisticService; use Illuminate\Http\Request; +use Illuminate\Support\Arr; use Illuminate\Support\Carbon; class StatisticsController extends Controller @@ -18,7 +19,7 @@ class StatisticsController extends Controller attributes: ['date' => '日期'], ); - $input = $this->defaultFilterInput($request); + $input = Arr::except($this->filterInput($request), 'date'); $date = Carbon::yesterday(); if ($request->filled('date')) { @@ -29,9 +30,10 @@ class StatisticsController extends Controller $monthLedger = array_merge( ['deadline' => $date->format('Y-m-d')], $statisticService->ledger( - $date->copy()->startOfMonth(), - $date->copy(), - $input, + array_merge($input, [ + 'start_at' => $date->copy()->startOfMonth()->toDateString(), + 'end_at' => $date->copy()->toDateString(), + ]) ), ); @@ -39,14 +41,15 @@ class StatisticsController extends Controller $yesdayLedger = array_merge( ['date' => $date->format('Y-m-d')], $statisticService->ledger( - $date->copy(), - $date->copy(), - $input, + array_merge($input, [ + 'start_at' => $date->copy()->toDateString(), + 'end_at' => $date->copy()->toDateString(), + ]) ), ); // 近30天趋势数据 - $trendsOf30days = $statisticService->ledgerTrends( + $trendsOf30days = $statisticService->dailyLedgerTrend( $date->copy()->subDays(29), $date->copy(), $input, @@ -82,18 +85,20 @@ class StatisticsController extends Controller ], ); - $input = $this->defaultFilterInput($request); + $input = $this->filterInput($request); $ledger = $statisticService->ledger( - Carbon::parse($request->input('start_at')), - Carbon::parse($request->input('end_at')), - $input, + array_merge($input, [ + 'start_at' => $request->input('start_at'), + 'end_at' => $request->input('end_at'), + ]) ); $beforeLedger = $statisticService->ledger( - Carbon::parse($request->input('before_start_at')), - Carbon::parse($request->input('before_end_at')), - $input, + array_merge($input, [ + 'start_at' => $request->input('before_start_at'), + 'end_at' => $request->input('before_end_at'), + ]) ); // 销售涨幅 @@ -128,13 +133,11 @@ class StatisticsController extends Controller ); $input = array_merge( - $this->defaultFilterInput($request), + $this->filterInput($request), $request->only(['start_at', 'end_at']), ); - $sorts = [['sales', 'desc'], ['id', 'asc']]; - - return $statisticService->stores($input, $sorts); + return $statisticService->storeRanking($input); } /** @@ -156,28 +159,30 @@ class StatisticsController extends Controller return $statisticService->sales( Carbon::parse($request->input('start_at')), Carbon::parse($request->input('end_at')), - $this->defaultFilterInput($request), + $this->filterInput($request), ); } /** * 处理区域和门店过滤条件 */ - protected function defaultFilterInput(Request $request): array + protected function filterInput(Request $request): array { - $input = []; + $input = Arr::except($request->input(), ['store_id', 'province_code', 'city_code']); if ($request->filled('store_id')) { $input['store_id'] = $request->input('store_id'); - } else { + } elseif ($request->anyFilled(['province_code', 'city_code'])) { $region = []; - if ($request->filled('province_code')) { - $region['provinceCode'] = $request->input('province_code'); + $provinceCode = (string) $request->input('province_code'); + if ($provinceCode !== '') { + $region['provinceCode'] = $provinceCode; } - if ($request->filled('city_code')) { - $region['cityCode'] = $request->input('city_code'); + $cityCode = (string) $request->input('city_code'); + if ($cityCode !== '') { + $region['cityCode'] = $provinceCode; } $input['region'] = $region; diff --git a/app/Services/StatisticService.php b/app/Services/StatisticService.php index abb310c..9a149fb 100644 --- a/app/Services/StatisticService.php +++ b/app/Services/StatisticService.php @@ -18,77 +18,99 @@ class StatisticService /** * 总账统计 */ - public function ledger(Carbon $start, Carbon $end, array $input = []): array + public function ledger(array $input = []): array { - $ledger = Ledger::filter($input, LedgerFilter::class) + $aggregate = Ledger::filter($input, LedgerFilter::class) ->select([DB::raw('SUM(sales) as sales'), DB::raw('SUM(expenditure) as expenditure')]) - ->whereBetween('date', [$start->format('Y-m-d'), $end->format('Y-m-d')]) ->first(); return [ - 'sales' => trim_zeros($ledger->sales ?? 0), - 'expenditure' => trim_zeros($ledger->expenditure ?? 0), + 'sales' => trim_zeros($aggregate->sales ?? 0), + 'expenditure' => trim_zeros($aggregate->expenditure ?? 0), ]; } /** - * 总账数据趋势 + * 总账数据趋势(日) */ - public function ledgerTrends(Carbon $start, Carbon $end, array $input = []): array + public function dailyLedgerTrend(Carbon $start, Carbon $end, array $input = []): array { - $ledgers = Ledger::filter($input, LedgerFilter::class) - ->whereBetween('date', [$start->format('Y-m-d'), $end->format('Y-m-d')]) - ->get(['date', 'sales', 'expenditure']) + $input = array_merge($input, [ + 'date_range' => $start->toDateString().','.$end->toDateString(), + ]); + + $aggregates = Ledger::select(['date', DB::raw('SUM(`sales`) as `sales`'), DB::raw('SUM(`expenditure`) as `expenditure`')]) + ->filter($input, LedgerFilter::class) + ->groupBy('date') + ->get() ->keyBy('date'); - $data = collect(); + $trend = collect(); while ($start->lte($end)) { - $ledger = $ledgers->get( - $date = $start->format('Y-m-d') - ); + $date = $start->toDateString(); - $data->push([ + $aggregate = $aggregates->get($date); + + $trend->push([ 'date' => $date, - 'sales' => trim_zeros($ledger->sales ?? 0), - 'expenditure' => trim_zeros($ledger->expenditure ?? 0), + 'sales' => trim_zeros($aggregate->sales ?? 0), + 'expenditure' => trim_zeros($aggregate->expenditure ?? 0), ]); $start->addDay(); } - return $data->all(); + return $trend->all(); } /** - * 门店统计 + * 总账数据趋势(月) */ - public function stores(array $input = [], array $sorts = []): array + public function monthlyLedgerTrend(Carbon $start, Carbon $end, array $input = []): array { - $storeLedgerStats = Ledger::select(['store_id', DB::raw('SUM(sales) as sales'), DB::raw('SUM(expenditure) as expenditure')]) - ->filter(Arr::only($input, ['date_range', 'start_at', 'end_at']), LedgerFilter::class) - ->groupBy('store_id'); + $input = array_merge($input, [ + 'date_range' => $start->toDateString().','.$end->toDateString(), + ]); - $stores = Store::filter(Arr::only($input, ['store_id', 'region']), StoreFilter::class) - ->leftJoinSub($storeLedgerStats, 'store_ledger_stats', fn ($join) => $join->on('stores.id', '=', 'store_ledger_stats.store_id')) - ->when($sorts, function ($query, $sorts) { - foreach ($sorts as $sort) { - $query->orderBy($sort[0], $sort[1]); + $aggregates = Ledger::select([DB::raw("DATE_FORMAT(`date`, '%Y-%m') as `month`"), DB::raw('SUM(`sales`) as `sales`'), DB::raw('SUM(`expenditure`) as `expenditure`')]) + ->filter($input, LedgerFilter::class) + ->groupBy('month') + ->get() + ->keyBy('month'); + + $diffMonths = 0; + if ($start->lte($end)) { + $datetime = $start->copy(); + + while (true) { + $diffMonths += 1; + + if ($datetime->isSameMonth($end)) { + break; } - }) - ->get(); - return $stores->map(function (Store $store, $key) { - return [ - 'ranking' => $key + 1, - 'store' => [ - 'id' => $store->id, - 'title' => $store->title, - ], - 'sales' => trim_zeros($store->sales ?: 0), - 'expenditure' => trim_zeros($store->expenditure ?: 0), - ]; - })->all(); + $datetime->addMonth(); + } + } + + $trend = collect(); + + for ($i=0; $i < $diffMonths; $i++) { + $month = $start->format('Y-m'); + + $aggregate = $aggregates->get($month); + + $trend->push([ + 'month' => $month, + 'sales' => trim_zeros($aggregate->sales ?? 0), + 'expenditure' => trim_zeros($aggregate->expenditure ?? 0), + ]); + + $start->addMonth(); + } + + return $trend->all(); } /** @@ -96,6 +118,10 @@ class StatisticService */ public function sales(Carbon $start, Carbon $end, array $input = []): array { + $input = array_merge($input, [ + 'date_range' => $start->toDateString().','.$end->toDateString(), + ]); + /** @var \Illuminate\Database\Eloquent\Collection */ $lotteryTypes = Keyword::where('parent_key', 'lottery_type')->get(); @@ -107,7 +133,6 @@ class StatisticService DB::raw('SUM(expenditure) as expenditure') ]) ->filter($input, LedgerFilter::class) - ->whereBetween('date', [$start->format('Y-m-d'), $end->format('Y-m-d')]) ->groupBy(['date']) ->get() ->keyBy('date'); @@ -121,7 +146,6 @@ class StatisticService ]) ->filter($input, LedgerItemFilter::class) ->whereIn('ledger_item_type_id', $lotteryTypes->pluck('key')) - ->whereBetween('date', [$start->format('Y-m-d'), $end->format('Y-m-d')]) ->groupBy(['date', 'ledger_item_type_id']) ->get() ->groupBy('date'); @@ -135,15 +159,6 @@ class StatisticService /** @var \Illuminate\Support\Collection */ $lotteryTypeStatistics = $ledgerItemStatistics->get($date, collect())->keyBy('ledger_item_type_id'); - $lotteryTypes->map(function ($lotteryType) use ($lotteryTypeStatistics) { - $lotteryTypeStatistic = $lotteryTypeStatistics->get($lotteryType->key); - return [ - 'name' => $lotteryType->name, - 'sales' => trim_zeros($lotteryTypeStatistic->sales ?? 0), - 'expenditure' => trim_zeros($lotteryTypeStatistic->expenditure ?? 0), - ]; - }); - $data->push([ 'date' => $date, 'ledger' => [ @@ -166,4 +181,38 @@ class StatisticService return $data->all(); } + + /** + * 门店排名 + */ + public function storeRanking(array $input = [], int $top = 0): array + { + $storeLedgers = Ledger::select(['store_id', DB::raw('SUM(sales) as sales'), DB::raw('SUM(expenditure) as expenditure')]) + ->filter(Arr::except($input, ['region']), LedgerFilter::class) + ->groupBy('store_id'); + + $stores = Store::filter($input, StoreFilter::class) + ->leftJoinSub($storeLedgers, 'store_ledgers', fn ($join) => $join->on('stores.id', '=', 'store_ledgers.store_id')) + ->when($input['sort'] ?? '', function ($query, $sort) { + foreach (explode(',', $sort) as $sort) { + $column = ltrim($sort, '-'); + $direction = str_starts_with($sort, '-') ? 'desc' : 'asc'; + $query->orderBy($column, $direction); + } + }, fn ($query) => $query->orderBy('sales', 'desc')) + ->when($top > 0, fn ($query) => $query->limit($top)) + ->get(); + + return $stores->map(function (Store $store, $key) { + return [ + 'ranking' => $key + 1, + 'store' => [ + 'id' => $store->id, + 'title' => $store->title, + ], + 'sales' => trim_zeros($store->sales ?: 0), + 'expenditure' => trim_zeros($store->expenditure ?: 0), + ]; + })->all(); + } }