diff --git a/app/Admin/Controllers/Finance/StoreStatisticController.php b/app/Admin/Controllers/Finance/StoreStatisticController.php index d6ed2ad..306245d 100644 --- a/app/Admin/Controllers/Finance/StoreStatisticController.php +++ b/app/Admin/Controllers/Finance/StoreStatisticController.php @@ -3,20 +3,22 @@ namespace App\Admin\Controllers\Finance; use App\Admin\Controllers\AdminController; -use App\Admin\Filters\LedgerFilter; -use App\Admin\Filters\StoreFilter; -use App\Models\Ledger; -use App\Models\Store; -use Illuminate\Http\Request; -use Illuminate\Support\Facades\DB; +use App\Services\StatisticService; class StoreStatisticController extends AdminController { public function index() { if ($this->actionOfGetData()) { + $input = request()->input(); + + $sorts = [ + [request()->input('orderBy') ?: 'sales', request()->input('orderDir') ?: 'desc'], + ['id', 'asc'], + ]; + return $this->response()->success([ - 'items' => $this->getStoreStatistics(request()), + 'items' => (new StatisticService())->stores($input, $sorts), ]); } @@ -40,36 +42,11 @@ class StoreStatisticController extends AdminController ])) ->columns([ amis()->TableColumn('ranking', '排序'), - amis()->TableColumn('title', '门店'), + amis()->TableColumn('store.title', '门店'), amis()->TableColumn('sales', '收入')->sortable(), amis()->TableColumn('expenditure', '支出')->sortable(), ]) ) ); } - - protected function getStoreStatistics(Request $request): array - { - /** @var \Illuminate\Database\Eloquent\Collection */ - $stats = Ledger::with(['store']) - ->select(['store_id', DB::raw('SUM(sales) as sales'), DB::raw('SUM(expenditure) as expenditure')]) - ->filter($request->input(), LedgerFilter::class) - ->groupBy('store_id') - ->get(); - - // 排序规则 - $sortBy = [ - [$request->input('orderBy') ?: 'sales', $request->input('orderDir') ?: 'desc'], - ]; - - return $stats->map(fn ($item) => [ - 'title' => $item->store->title, - 'sales' => trim_zeros($item->sales ?? '0'), - 'expenditure' => trim_zeros($item->expenditure ?? '0'), - ]) - ->sortBy($sortBy) - ->values() - ->map(fn ($item, $key) => array_merge($item, ['ranking' => $key + 1])) - ->all(); - } } diff --git a/app/Admin/Filters/LedgerFilter.php b/app/Admin/Filters/LedgerFilter.php index 4b51b74..8f34a74 100644 --- a/app/Admin/Filters/LedgerFilter.php +++ b/app/Admin/Filters/LedgerFilter.php @@ -44,4 +44,14 @@ class LedgerFilter extends ModelFilter $query->whereIn('check_status', explode(',', $checkStatus)); }); } + + public function startAt($startAt) + { + $this->where('date', '>=', $startAt); + } + + public function endAt($endAt) + { + $this->where('date', '<=', $endAt); + } } diff --git a/app/Admin/Filters/LedgerItemFilter.php b/app/Admin/Filters/LedgerItemFilter.php index 1afb388..2825a3b 100644 --- a/app/Admin/Filters/LedgerItemFilter.php +++ b/app/Admin/Filters/LedgerItemFilter.php @@ -18,9 +18,12 @@ class LedgerItemFilter extends ModelFilter return; } - $provinceCode = Arr::get($region, 'provinceCode'); - $cityCode = Arr::get($region, 'cityCode'); - if (empty($provinceCode) && empty($cityCode)) { + // 区划代码 - 省份 + $provinceCode = (string) Arr::get($region, 'provinceCode'); + // 区划代码 - 城市 + $cityCode = (string) Arr::get($region, 'cityCode'); + + if ($provinceCode === '' && $cityCode === '') { return; } diff --git a/app/Http/Controllers/Api/StatisticsController.php b/app/Http/Controllers/Api/StatisticsController.php index f68da6e..4539bbe 100644 --- a/app/Http/Controllers/Api/StatisticsController.php +++ b/app/Http/Controllers/Api/StatisticsController.php @@ -2,78 +2,119 @@ namespace App\Http\Controllers\Api; -use App\Admin\Filters\LedgerFilter; -use App\Admin\Filters\StoreFilter; -use App\Http\Resources\StoreResource; -use App\Models\Ledger; -use App\Models\Store; -use Illuminate\Database\Eloquent\Collection; +use App\Services\StatisticService; use Illuminate\Http\Request; -use Illuminate\Support\Arr; use Illuminate\Support\Carbon; -use Illuminate\Support\Facades\DB; class StatisticsController extends Controller { /** * 首页统计 */ - public function dashboard(Request $request): array + public function dashboard(Request $request, StatisticService $statisticService): array { - $query = Ledger::filter( - $this->defaultFilterInput($request), LedgerFilter::class + $request->validate( + rules: ['date' => ['filled', 'date_format:Y-m-d']], + attributes: ['date' => '日期'], ); - // 昨天 - $yesterday = Carbon::yesterday(); + $input = $this->defaultFilterInput($request); - //-------------------------------------------------------------------------- - // 本月总账录入 - //-------------------------------------------------------------------------- - $currentMonthLedger = (clone $query) - ->select([DB::raw('SUM(sales) as sales'), DB::raw('SUM(expenditure) as expenditure')]) - ->whereBetween('date', [$yesterday->copy()->startOfMonth()->format('Y-m-d'), $yesterday->format('Y-m-d')]) - ->first(); + $date = Carbon::yesterday(); + if ($request->filled('date')) { + $date = Carbon::parse($request->input('date')); + } + + // 本月上报数据统计 + $monthLedger = array_merge( + ['deadline' => $date->format('Y-m-d')], + $statisticService->ledger( + $date->copy()->startOfMonth(), + $date->copy(), + $input, + ), + ); - //-------------------------------------------------------------------------- // 昨日总账录入 - //-------------------------------------------------------------------------- - $yesterdayLedger = (clone $query)->where('date', $yesterday->format('Y-m-d'))->first(); + $yesdayLedger = array_merge( + ['date' => $date->format('Y-m-d')], + $statisticService->ledger( + $date->copy(), + $date->copy(), + $input, + ), + ); - //-------------------------------------------------------------------------- - // 近 30 天趋势数据 - //-------------------------------------------------------------------------- - $start = $yesterday->copy()->subDays(29); - $end = $yesterday->copy(); - /** @var \Illuminate\Database\Eloquent\Collection */ - $ledgers30days = (clone $query) - ->whereBetween('date', [$start, $end]) - ->get(['date', 'sales', 'expenditure']) - ->keyBy('date'); + // 近30天趋势数据 + $trendsOf30days = $statisticService->ledgerTrends( + $date->copy()->subDays(29), + $date->copy(), + $input, + ); return [ // 本月总账录入 - 'current_month_ledger' => [ - // 截止日期 - 'deadline' => $yesterday->format('Y-m-d'), - 'sales' => trim_zeros($currentMonthLedger->sales ?? 0), - 'expenditure' => trim_zeros($currentMonthLedger->expenditure ?? 0), - ], + 'current_month_ledger' => $monthLedger, // 昨日累计金额 - 'yesterday_ledger' => [ - 'date' => $yesterday->format('Y-m-d'), - 'sales' => trim_zeros($yesterdayLedger->sales ?? 0), - 'expenditure' => trim_zeros($yesterdayLedger->expenditure ?? 0), - ], + 'yesday_ledger' => $yesdayLedger, // 近30天趋势数据 - 'trend_data_of_30days' => $this->prepareTrendData($start->copy(), $end->copy(), $ledgers30days), + 'trends_of_30days' => $trendsOf30days, ]; } + /** + * 总账统计 + */ + public function ledger(Request $request, StatisticService $statisticService) + { + $request->validate( + rules: [ + 'start_at' => ['bail', 'required', 'date_format:Y-m-d'], + 'end_at' => ['bail', 'required', 'date_format:Y-m-d'], + 'before_start_at' => ['bail', 'required', 'date_format:Y-m-d'], + 'before_end_at' => ['bail', 'required', 'date_format:Y-m-d'], + ], + attributes: [ + 'start_at' => '开始日期', + 'end_at' => '结束日期', + 'before_start_at' => '对比开始日期', + 'before_end_at' => '对比结束日期', + ], + ); + + $input = $this->defaultFilterInput($request); + + $ledger = $statisticService->ledger( + Carbon::parse($request->input('start_at')), + Carbon::parse($request->input('end_at')), + $input, + ); + + $beforeLedger = $statisticService->ledger( + Carbon::parse($request->input('before_start_at')), + Carbon::parse($request->input('before_end_at')), + $input, + ); + + // 销售涨幅 + $salesGrowthRate = 0; + + if (bccomp($beforeLedger['sales'], '0', 2) === 0) { + $salesGrowthRate = '-'; + } else { + $diff = bcsub($ledger['sales'], $beforeLedger['sales'], 2); + $salesGrowthRate = bcdiv(bcmul($diff, '100'), $beforeLedger['sales'], 2); + } + + return array_merge($ledger, [ + 'sales_growth_rate' => $salesGrowthRate, + ]); + } + /** * 门店统计 */ - public function stores(Request $request) + public function stores(Request $request, StatisticService $statisticService) { $request->validate( rules: [ @@ -86,46 +127,37 @@ class StatisticsController extends Controller ], ); - $storeLedgerStats = Ledger::select(['store_id', DB::raw('SUM(sales) as sales')]) - ->whereBetween('date', [$request->input('start_at'), $request->input('end_at')]) - ->groupBy('store_id'); + $input = array_merge( + $this->defaultFilterInput($request), + $request->only(['start_at', 'end_at']), + ); - $stores = Store::filter($this->defaultFilterInput($request), StoreFilter::class) - ->leftJoinSub($storeLedgerStats, 'store_ledger_stats', fn ($join) => $join->on('stores.id', '=', 'store_ledger_stats.store_id')) - ->orderBy('sales', 'desc') - ->orderBy('id', 'asc') - ->get(); + $sorts = [['sales', 'desc'], ['id', 'asc']]; - return $stores->map(function (Store $store) { - return [ - 'store' => StoreResource::make($store), - 'sales' => trim_zeros($store->sales ?: 0), - ]; - }); + return $statisticService->stores($input, $sorts); } /** - * 准备趋势数据 + * 销售统计 */ - protected function prepareTrendData(Carbon $start, Carbon $end, Collection $ledgers): array + public function sales(Request $request, StatisticService $statisticService): array { - $data = collect(); + $request->validate( + rules: [ + 'start_at' => ['bail', 'required', 'date_format:Y-m-d'], + 'end_at' => ['bail', 'required', 'date_format:Y-m-d'], + ], + attributes: [ + 'start_at' => '开始日期', + 'end_at' => '结束日期', + ], + ); - do { - $ledger = $ledgers->get( - $date = $start->format('Y-m-d') - ); - - $data->push([ - 'date' => $date, - 'sales' => trim_zeros($ledger->sales ?? 0), - 'expenditure' => trim_zeros($ledger->expenditure ?? 0), - ]); - - $start->addDay(); - } while ($start->lte($end)); - - return $data->all(); + return $statisticService->sales( + Carbon::parse($request->input('start_at')), + Carbon::parse($request->input('end_at')), + $this->defaultFilterInput($request), + ); } /** diff --git a/app/Models/LedgerItem.php b/app/Models/LedgerItem.php index d00de50..fdb5dfd 100644 --- a/app/Models/LedgerItem.php +++ b/app/Models/LedgerItem.php @@ -13,7 +13,6 @@ class LedgerItem extends Model use Filterable, HasDateTimeFormatter, HasFactory; protected $casts = [ - 'date' => 'date', 'approved' => 'bool', ]; diff --git a/app/Services/StatisticService.php b/app/Services/StatisticService.php new file mode 100644 index 0000000..abb310c --- /dev/null +++ b/app/Services/StatisticService.php @@ -0,0 +1,169 @@ +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), + ]; + } + + /** + * 总账数据趋势 + */ + public function ledgerTrends(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']) + ->keyBy('date'); + + $data = collect(); + + while ($start->lte($end)) { + $ledger = $ledgers->get( + $date = $start->format('Y-m-d') + ); + + $data->push([ + 'date' => $date, + 'sales' => trim_zeros($ledger->sales ?? 0), + 'expenditure' => trim_zeros($ledger->expenditure ?? 0), + ]); + + $start->addDay(); + } + + return $data->all(); + } + + /** + * 门店统计 + */ + public function stores(array $input = [], array $sorts = []): 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'); + + $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]); + } + }) + ->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(); + } + + /** + * 销售统计 + */ + public function sales(Carbon $start, Carbon $end, array $input = []): array + { + /** @var \Illuminate\Database\Eloquent\Collection */ + $lotteryTypes = Keyword::where('parent_key', 'lottery_type')->get(); + + /** @var \Illuminate\Support\Collection */ + $ledgerStatistics = Ledger::select([ + 'date', + DB::raw('SUM(new_customers) as new_customers'), + DB::raw('SUM(sales) as sales'), + 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'); + + /** @var \Illuminate\Support\Collection */ + $ledgerItemStatistics = LedgerItem::select([ + 'date', + 'ledger_item_type_id', + DB::raw('SUM(sales) as sales'), + DB::raw('SUM(expenditure) as expenditure'), + ]) + ->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'); + + $data = collect(); + + while ($end->gte($start)) { + $date = $end->format('Y-m-d'); + + $ledgerStatistic = $ledgerStatistics->get($date); + /** @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' => [ + 'new_customers' => $ledgerStatistic->new_customers ?? 0, + 'sales' => trim_zeros($ledgerStatistic->sales ?? 0), + 'expenditure' => trim_zeros($ledgerStatistic->expenditure ?? 0), + ], + 'lottery_types' => $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), + ]; + }), + ]); + + $end->subDay(); + } + + return $data->all(); + } +} diff --git a/routes/api.php b/routes/api.php index 96ffa93..dc63764 100644 --- a/routes/api.php +++ b/routes/api.php @@ -39,6 +39,10 @@ Route::group([ Route::get('/statistics/dashboard', [StatisticsController::class, 'dashboard']); // 统计数据 - 门店统计 Route::get('/statistics/stores', [StatisticsController::class, 'stores']); + // 统计数据 - 销售统计 + Route::get('/statistics/sales', [StatisticsController::class, 'sales']); + // 统计数据 - 总账统计 + Route::get('/statistics/ledger', [StatisticsController::class, 'ledger']); // 数据上报 Route::apiResource('/ledgers', LedgerController::class)->only(['store', 'show']);