main
panliang 2024-04-16 17:02:37 +08:00
commit 06b694f6bf
7 changed files with 309 additions and 115 deletions

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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),
);
}
/**

View File

@ -13,7 +13,6 @@ class LedgerItem extends Model
use Filterable, HasDateTimeFormatter, HasFactory;
protected $casts = [
'date' => 'date',
'approved' => 'bool',
];

View File

@ -0,0 +1,169 @@
<?php
namespace App\Services;
use App\Admin\Filters\LedgerFilter;
use App\Admin\Filters\LedgerItemFilter;
use App\Admin\Filters\StoreFilter;
use App\Models\Keyword;
use App\Models\Ledger;
use App\Models\LedgerItem;
use App\Models\Store;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
class StatisticService
{
/**
* 总账统计
*/
public function ledger(Carbon $start, Carbon $end, array $input = []): array
{
$ledger = 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),
];
}
/**
* 总账数据趋势
*/
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();
}
}

View File

@ -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']);