generated from liutk/owl-admin-base
Merge branch 'main' of https://gitea.hmily.club/pdkj/store-manage into main
commit
c04dc12e8f
|
|
@ -6,11 +6,12 @@ use App\Admin\Filters\StoreFilter;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Employee;
|
use App\Models\Employee;
|
||||||
use App\Models\Keyword;
|
use App\Models\Keyword;
|
||||||
use App\Models\Ledger;
|
|
||||||
use App\Models\LedgerItem;
|
use App\Models\LedgerItem;
|
||||||
use App\Models\Store;
|
use App\Models\Store;
|
||||||
use App\Models\TaskPerformance;
|
use App\Models\TaskPerformance;
|
||||||
|
use App\Services\StatisticService;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Storage;
|
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(
|
$request->validate(
|
||||||
rules: [
|
rules: [
|
||||||
|
|
@ -47,15 +48,12 @@ class CockpitController extends Controller
|
||||||
|
|
||||||
$last = $request->input('last');
|
$last = $request->input('last');
|
||||||
|
|
||||||
$data = collect();
|
|
||||||
|
|
||||||
if (in_array($last, ['7days', '30days'])) {
|
if (in_array($last, ['7days', '30days'])) {
|
||||||
// 按天
|
// 按天
|
||||||
$days = match ($last) {
|
$days = match ($last) {
|
||||||
'7days' => 7,
|
'7days' => 7,
|
||||||
'30days' => 30,
|
'30days' => 30,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 今天
|
// 今天
|
||||||
$today = Carbon::today();
|
$today = Carbon::today();
|
||||||
// 开始时间
|
// 开始时间
|
||||||
|
|
@ -63,24 +61,7 @@ class CockpitController extends Controller
|
||||||
// 结束时间
|
// 结束时间
|
||||||
$endAt = $today->copy()->subDay();
|
$endAt = $today->copy()->subDay();
|
||||||
|
|
||||||
$ledgers = Ledger::select(['date', DB::raw('SUM(sales) as sales')])
|
return $statisticService->dailyLedgerTrend($startAt, $endAt);
|
||||||
->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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
elseif (in_array($last, ['180days', '365days'])) {
|
elseif (in_array($last, ['180days', '365days'])) {
|
||||||
// 按月
|
// 按月
|
||||||
|
|
@ -88,7 +69,6 @@ class CockpitController extends Controller
|
||||||
'180days' => 6, // 6个月
|
'180days' => 6, // 6个月
|
||||||
'365days' => 12, // 12个月
|
'365days' => 12, // 12个月
|
||||||
};
|
};
|
||||||
|
|
||||||
// 今天
|
// 今天
|
||||||
$today = Carbon::today();
|
$today = Carbon::today();
|
||||||
// 开始时间
|
// 开始时间
|
||||||
|
|
@ -96,27 +76,10 @@ class CockpitController extends Controller
|
||||||
// 结束时间
|
// 结束时间
|
||||||
$endAt = $today->copy()->startOfMonth()->subMonth()->endOfMonth();
|
$endAt = $today->copy()->startOfMonth()->subMonth()->endOfMonth();
|
||||||
|
|
||||||
$ledgers = Ledger::select([DB::raw("DATE_FORMAT(`date`, '%Y-%m') as month"), DB::raw('SUM(sales) as sales')])
|
return $statisticService->monthlyLedgerTrend($startAt, $endAt);
|
||||||
->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 $data->all();
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -205,7 +168,7 @@ class CockpitController extends Controller
|
||||||
->whereBetween('date', [$startAt->toDateString(), $endAt->toDateString()])
|
->whereBetween('date', [$startAt->toDateString(), $endAt->toDateString()])
|
||||||
->groupBy(['month', 'ledger_item_type_id'])
|
->groupBy(['month', 'ledger_item_type_id'])
|
||||||
->get()
|
->get()
|
||||||
->keyBy('month');
|
->groupBy('month');
|
||||||
|
|
||||||
for ($i=0; $i < $months; $i++) {
|
for ($i=0; $i < $months; $i++) {
|
||||||
$month = $startAt->format('Y-m');
|
$month = $startAt->format('Y-m');
|
||||||
|
|
@ -231,7 +194,7 @@ class CockpitController extends Controller
|
||||||
return [
|
return [
|
||||||
'lottery_types' => $lotteryTypes->map(function ($lotteryType) {
|
'lottery_types' => $lotteryTypes->map(function ($lotteryType) {
|
||||||
return [
|
return [
|
||||||
'id' => $lotteryType->id,
|
'id' => $lotteryType->key,
|
||||||
'name' => $lotteryType->name,
|
'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(
|
$request->validate(
|
||||||
rules: [
|
rules: [
|
||||||
|
|
@ -250,50 +213,25 @@ class CockpitController extends Controller
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$today = Carbon::today();
|
||||||
$last = $request->input('last');
|
$last = $request->input('last');
|
||||||
|
$input = ['sort' => '-sales'];
|
||||||
|
|
||||||
$storeSales = Ledger::select(['store_id', DB::raw('SUM(sales) as sales')])
|
if (in_array($last, ['7days', '30days'])) {
|
||||||
->when($last, function ($query, $last) {
|
$days = match ($last) {
|
||||||
$today = Carbon::today();
|
'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'])) {
|
return $statisticService->storeRanking($input, 30);
|
||||||
$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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -378,4 +316,65 @@ class CockpitController extends Controller
|
||||||
]);
|
]);
|
||||||
})->all();
|
})->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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,21 +4,22 @@ namespace App\Admin\Controllers\Finance;
|
||||||
|
|
||||||
use App\Admin\Controllers\AdminController;
|
use App\Admin\Controllers\AdminController;
|
||||||
use App\Services\StatisticService;
|
use App\Services\StatisticService;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
|
||||||
class StoreStatisticController extends AdminController
|
class StoreStatisticController extends AdminController
|
||||||
{
|
{
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
if ($this->actionOfGetData()) {
|
if ($this->actionOfGetData()) {
|
||||||
$input = request()->input();
|
$input = Arr::except(request()->input(), ['orderBy', 'orderDir']);
|
||||||
|
|
||||||
$sorts = [
|
$orderBy = request()->input('orderBy') ?: 'sales';
|
||||||
[request()->input('orderBy') ?: 'sales', request()->input('orderDir') ?: 'desc'],
|
$orderDir = request()->input('orderDir') ?: 'desc';
|
||||||
['id', 'asc'],
|
|
||||||
];
|
$input['sort'] = ($orderDir === 'desc' ? '-' : '').$orderBy;
|
||||||
|
|
||||||
return $this->response()->success([
|
return $this->response()->success([
|
||||||
'items' => (new StatisticService())->stores($input, $sorts),
|
'items' => (new StatisticService())->storeRanking($input),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,42 +30,43 @@ class StoreController extends AdminController
|
||||||
->bulkActions([])
|
->bulkActions([])
|
||||||
->filter($this->baseFilter()->body([
|
->filter($this->baseFilter()->body([
|
||||||
amis()->GroupControl()->mode('horizontal')->body([
|
amis()->GroupControl()->mode('horizontal')->body([
|
||||||
amisMake()->TextControl()->name('title')->label(__('store.title'))->columnRatio(3)->clearable(),
|
amis()->TextControl()->name('title')->label(__('store.title'))->columnRatio(3)->clearable(),
|
||||||
amisMake()->TreeSelectControl()->name('category_id')->label(__('store.category_id'))->columnRatio(3)
|
amis()->TreeSelectControl()->name('category_id')->label(__('store.category_id'))->columnRatio(3)
|
||||||
->source(admin_url('api/keywords/tree-list?parent_key=store_category'))
|
->source(admin_url('api/keywords/tree-list?parent_key=store_category'))
|
||||||
->labelField('name')
|
->labelField('name')
|
||||||
->valueField('key')
|
->valueField('key')
|
||||||
->onlyLeaf(true)
|
->onlyLeaf(true)
|
||||||
->clearable(),
|
->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'))
|
->source(admin_url('api/keywords/tree-list?parent_key=store_business'))
|
||||||
->labelField('name')
|
->labelField('name')
|
||||||
->valueField('key')
|
->valueField('key')
|
||||||
->clearable(),
|
->clearable(),
|
||||||
]),
|
]),
|
||||||
amisMake()->GroupControl()->mode('horizontal')->body([
|
amis()->GroupControl()->mode('horizontal')->body([
|
||||||
amisMake()->SelectControl()->name('level_id')->label(__('store.level_id'))->columnRatio(3)
|
amis()->SelectControl()->name('level_id')->label(__('store.level_id'))->columnRatio(3)
|
||||||
->source(admin_url('api/keywords/tree-list?parent_key=store_level'))
|
->source(admin_url('api/keywords/tree-list?parent_key=store_level'))
|
||||||
->labelField('name')
|
->labelField('name')
|
||||||
->valueField('key')
|
->valueField('key')
|
||||||
->clearable(),
|
->clearable(),
|
||||||
// amisMake()->InputCityControl()->name('region')->label(__('store.region'))->columnRatio(3)
|
// amis()->InputCityControl()->name('region')->label(__('store.region'))->columnRatio(3)
|
||||||
// ->allowDistrict(false)
|
// ->allowDistrict(false)
|
||||||
// ->extractValue(false)
|
// ->extractValue(false)
|
||||||
// ->clearable(),
|
// ->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([
|
->columns([
|
||||||
amisMake()->TableColumn()->name('id')->label(__('store.id')),
|
amis()->TableColumn()->name('id')->label(__('store.id')),
|
||||||
amisMake()->TableColumn()->name('title')->label(__('store.title')),
|
amis()->TableColumn()->name('title')->label(__('store.title')),
|
||||||
amisMake()->TableColumn()->name('master.name')->label(__('store.master_id')),
|
amis()->TableColumn()->name('master.name')->label(__('store.master_id')),
|
||||||
amisMake()->TableColumn()->name('category.name')->label(__('store.category_id')),
|
amis()->TableColumn()->name('category.name')->label(__('store.category_id')),
|
||||||
amisMake()->TableColumn()->name('business.name')->label(__('store.business_id')),
|
amis()->TableColumn()->name('business.name')->label(__('store.business_id')),
|
||||||
amisMake()->TableColumn()->name('level.name')->label(__('store.level_id')),
|
amis()->TableColumn()->name('level.name')->label(__('store.level_id')),
|
||||||
amisMake()->TableColumn()->name('region')->label(__('store.region'))->set('type', 'tpl')->set('tpl', '${region.province}-${region.city}'),
|
amis()->TableColumn()->name('profit_ratio')->label(__('store.profit_ratio'))->type('tpl')->set('tpl', '${profit_ratio}%'),
|
||||||
amisMake()->TableColumn()->name('business_status')->label(__('store.business_status'))->type('switch')->trueValue(BusinessStatus::Open)->falseValue(BusinessStatus::Close),
|
amis()->TableColumn()->name('region')->label(__('store.region'))->set('type', 'tpl')->set('tpl', '${region.province}-${region.city}'),
|
||||||
amisMake()->TableColumn()->name('created_at')->label(__('store.created_at')),
|
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->rowActions([
|
||||||
$this->rowShowButton()->visible($user->can('admin.store.stores.view')),
|
$this->rowShowButton()->visible($user->can('admin.store.stores.view')),
|
||||||
$this->rowEditTypeButton('drawer', 'lg')->visible($user->can('admin.store.stores.update')),
|
$this->rowEditTypeButton('drawer', 'lg')->visible($user->can('admin.store.stores.update')),
|
||||||
|
|
@ -79,40 +80,47 @@ class StoreController extends AdminController
|
||||||
public function form($edit): Form
|
public function form($edit): Form
|
||||||
{
|
{
|
||||||
return $this->baseForm()->title('')->body([
|
return $this->baseForm()->title('')->body([
|
||||||
amisMake()->TextControl()->name('title')->label(__('store.title'))->required(),
|
amis()->TextControl()->name('title')->label(__('store.title'))->required(),
|
||||||
amisMake()->SelectControl()->name('master_id')->label(__('store.master_id'))
|
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))
|
->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')
|
->labelField('name')
|
||||||
->valueField('id')
|
->valueField('id')
|
||||||
->searchable()
|
->searchable()
|
||||||
->required(),
|
->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'))
|
->source(admin_url('api/keywords/tree-list?parent_key=store_category'))
|
||||||
->labelField('name')
|
->labelField('name')
|
||||||
->valueField('key')
|
->valueField('key')
|
||||||
->onlyLeaf(true)
|
->onlyLeaf(true)
|
||||||
->required(),
|
->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'))
|
->source(admin_url('api/keywords/tree-list?parent_key=store_business'))
|
||||||
->labelField('name')
|
->labelField('name')
|
||||||
->valueField('key')
|
->valueField('key')
|
||||||
->required(),
|
->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'))
|
->source(admin_url('api/keywords/tree-list?parent_key=store_level'))
|
||||||
->labelField('name')
|
->labelField('name')
|
||||||
->valueField('key')
|
->valueField('key')
|
||||||
->required(),
|
->required(),
|
||||||
amisMake()->InputCityControl()->name('region')->label(__('store.region'))->allowDistrict(false)->extractValue(false)->required(),
|
amis()->NumberControl()
|
||||||
amisMake()->LocationControl()->name('location')->label(__('store.location'))->ak(config('baidu.js_secret'))->autoSelectCurrentLoc(),
|
->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
|
public function detail(): Form
|
||||||
{
|
{
|
||||||
$detail = amisMake()->Property()->items([
|
$detail = amis()->Property()->items([
|
||||||
['label' => __('store.title'), 'content' => '${title}'],
|
['label' => __('store.title'), 'content' => '${title}'],
|
||||||
['label' => __('store.master_id'), 'content' => '${master.name}'],
|
['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.category_id'), 'content' => '${category.name}'],
|
||||||
['label' => __('store.business_id'), 'content' => '${business.name}'],
|
['label' => __('store.business_id'), 'content' => '${business.name}'],
|
||||||
['label' => __('store.level_id'), 'content' => '${level.name}'],
|
['label' => __('store.level_id'), 'content' => '${level.name}'],
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@ class StoreService extends BaseService
|
||||||
'region' => ['required'],
|
'region' => ['required'],
|
||||||
'lon' => ['required'],
|
'lon' => ['required'],
|
||||||
'lat' => ['required'],
|
'lat' => ['required'],
|
||||||
|
'profit_ratio' => ['required', 'numeric', 'min:0', 'max:100'],
|
||||||
];
|
];
|
||||||
$updateRules = [
|
$updateRules = [
|
||||||
'master_id' => [Rule::unique('stores', 'master_id')->ignore($model, 'master_id')],
|
'master_id' => [Rule::unique('stores', 'master_id')->ignore($model, 'master_id')],
|
||||||
|
|
|
||||||
|
|
@ -254,6 +254,8 @@ Route::group([
|
||||||
$router->get('cockpit/store-number-distribution', [CockpitController::class, 'storeNumberDistribution']);
|
$router->get('cockpit/store-number-distribution', [CockpitController::class, 'storeNumberDistribution']);
|
||||||
// 门店分布(按城市)
|
// 门店分布(按城市)
|
||||||
$router->get('cockpit/store-distribution', [CockpitController::class, 'storeDistribution']);
|
$router->get('cockpit/store-distribution', [CockpitController::class, 'storeDistribution']);
|
||||||
|
// 门店分类
|
||||||
|
$router->get('cockpit/store-category', [CockpitController::class, 'storeCategory']);
|
||||||
// 年度目标
|
// 年度目标
|
||||||
$router->get('cockpit/yearly-goals', [CockpitController::class, 'yearlyGoals']);
|
$router->get('cockpit/yearly-goals', [CockpitController::class, 'yearlyGoals']);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@ class LedgerController extends Controller
|
||||||
throw new RuntimeException('上报数据已更新,不可重新上传');
|
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);
|
$validated['expected_commission'] = bcmul($validated['sales'], $ratio, 2);
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
use App\Services\StatisticService;
|
use App\Services\StatisticService;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
class StatisticsController extends Controller
|
class StatisticsController extends Controller
|
||||||
|
|
@ -18,7 +19,7 @@ class StatisticsController extends Controller
|
||||||
attributes: ['date' => '日期'],
|
attributes: ['date' => '日期'],
|
||||||
);
|
);
|
||||||
|
|
||||||
$input = $this->defaultFilterInput($request);
|
$input = Arr::except($this->filterInput($request), 'date');
|
||||||
|
|
||||||
$date = Carbon::yesterday();
|
$date = Carbon::yesterday();
|
||||||
if ($request->filled('date')) {
|
if ($request->filled('date')) {
|
||||||
|
|
@ -29,9 +30,10 @@ class StatisticsController extends Controller
|
||||||
$monthLedger = array_merge(
|
$monthLedger = array_merge(
|
||||||
['deadline' => $date->format('Y-m-d')],
|
['deadline' => $date->format('Y-m-d')],
|
||||||
$statisticService->ledger(
|
$statisticService->ledger(
|
||||||
$date->copy()->startOfMonth(),
|
array_merge($input, [
|
||||||
$date->copy(),
|
'start_at' => $date->copy()->startOfMonth()->toDateString(),
|
||||||
$input,
|
'end_at' => $date->copy()->toDateString(),
|
||||||
|
])
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -39,14 +41,15 @@ class StatisticsController extends Controller
|
||||||
$yesdayLedger = array_merge(
|
$yesdayLedger = array_merge(
|
||||||
['date' => $date->format('Y-m-d')],
|
['date' => $date->format('Y-m-d')],
|
||||||
$statisticService->ledger(
|
$statisticService->ledger(
|
||||||
$date->copy(),
|
array_merge($input, [
|
||||||
$date->copy(),
|
'start_at' => $date->copy()->toDateString(),
|
||||||
$input,
|
'end_at' => $date->copy()->toDateString(),
|
||||||
|
])
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 近30天趋势数据
|
// 近30天趋势数据
|
||||||
$trendsOf30days = $statisticService->ledgerTrends(
|
$trendsOf30days = $statisticService->dailyLedgerTrend(
|
||||||
$date->copy()->subDays(29),
|
$date->copy()->subDays(29),
|
||||||
$date->copy(),
|
$date->copy(),
|
||||||
$input,
|
$input,
|
||||||
|
|
@ -82,18 +85,20 @@ class StatisticsController extends Controller
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
$input = $this->defaultFilterInput($request);
|
$input = $this->filterInput($request);
|
||||||
|
|
||||||
$ledger = $statisticService->ledger(
|
$ledger = $statisticService->ledger(
|
||||||
Carbon::parse($request->input('start_at')),
|
array_merge($input, [
|
||||||
Carbon::parse($request->input('end_at')),
|
'start_at' => $request->input('start_at'),
|
||||||
$input,
|
'end_at' => $request->input('end_at'),
|
||||||
|
])
|
||||||
);
|
);
|
||||||
|
|
||||||
$beforeLedger = $statisticService->ledger(
|
$beforeLedger = $statisticService->ledger(
|
||||||
Carbon::parse($request->input('before_start_at')),
|
array_merge($input, [
|
||||||
Carbon::parse($request->input('before_end_at')),
|
'start_at' => $request->input('before_start_at'),
|
||||||
$input,
|
'end_at' => $request->input('before_end_at'),
|
||||||
|
])
|
||||||
);
|
);
|
||||||
|
|
||||||
// 销售涨幅
|
// 销售涨幅
|
||||||
|
|
@ -128,13 +133,11 @@ class StatisticsController extends Controller
|
||||||
);
|
);
|
||||||
|
|
||||||
$input = array_merge(
|
$input = array_merge(
|
||||||
$this->defaultFilterInput($request),
|
$this->filterInput($request),
|
||||||
$request->only(['start_at', 'end_at']),
|
$request->only(['start_at', 'end_at']),
|
||||||
);
|
);
|
||||||
|
|
||||||
$sorts = [['sales', 'desc'], ['id', 'asc']];
|
return $statisticService->storeRanking($input);
|
||||||
|
|
||||||
return $statisticService->stores($input, $sorts);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -156,28 +159,30 @@ class StatisticsController extends Controller
|
||||||
return $statisticService->sales(
|
return $statisticService->sales(
|
||||||
Carbon::parse($request->input('start_at')),
|
Carbon::parse($request->input('start_at')),
|
||||||
Carbon::parse($request->input('end_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')) {
|
if ($request->filled('store_id')) {
|
||||||
$input['store_id'] = $request->input('store_id');
|
$input['store_id'] = $request->input('store_id');
|
||||||
} else {
|
} elseif ($request->anyFilled(['province_code', 'city_code'])) {
|
||||||
$region = [];
|
$region = [];
|
||||||
|
|
||||||
if ($request->filled('province_code')) {
|
$provinceCode = (string) $request->input('province_code');
|
||||||
$region['provinceCode'] = $request->input('province_code');
|
if ($provinceCode !== '') {
|
||||||
|
$region['provinceCode'] = $provinceCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->filled('city_code')) {
|
$cityCode = (string) $request->input('city_code');
|
||||||
$region['cityCode'] = $request->input('city_code');
|
if ($cityCode !== '') {
|
||||||
|
$region['cityCode'] = $provinceCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
$input['region'] = $region;
|
$input['region'] = $region;
|
||||||
|
|
|
||||||
|
|
@ -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')])
|
->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();
|
->first();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'sales' => trim_zeros($ledger->sales ?? 0),
|
'sales' => trim_zeros($aggregate->sales ?? 0),
|
||||||
'expenditure' => trim_zeros($ledger->expenditure ?? 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)
|
$input = array_merge($input, [
|
||||||
->whereBetween('date', [$start->format('Y-m-d'), $end->format('Y-m-d')])
|
'date_range' => $start->toDateString().','.$end->toDateString(),
|
||||||
->get(['date', 'sales', 'expenditure'])
|
]);
|
||||||
|
|
||||||
|
$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');
|
->keyBy('date');
|
||||||
|
|
||||||
$data = collect();
|
$trend = collect();
|
||||||
|
|
||||||
while ($start->lte($end)) {
|
while ($start->lte($end)) {
|
||||||
$ledger = $ledgers->get(
|
$date = $start->toDateString();
|
||||||
$date = $start->format('Y-m-d')
|
|
||||||
);
|
|
||||||
|
|
||||||
$data->push([
|
$aggregate = $aggregates->get($date);
|
||||||
|
|
||||||
|
$trend->push([
|
||||||
'date' => $date,
|
'date' => $date,
|
||||||
'sales' => trim_zeros($ledger->sales ?? 0),
|
'sales' => trim_zeros($aggregate->sales ?? 0),
|
||||||
'expenditure' => trim_zeros($ledger->expenditure ?? 0),
|
'expenditure' => trim_zeros($aggregate->expenditure ?? 0),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$start->addDay();
|
$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')])
|
$input = array_merge($input, [
|
||||||
->filter(Arr::only($input, ['date_range', 'start_at', 'end_at']), LedgerFilter::class)
|
'date_range' => $start->toDateString().','.$end->toDateString(),
|
||||||
->groupBy('store_id');
|
]);
|
||||||
|
|
||||||
$stores = Store::filter(Arr::only($input, ['store_id', 'region']), StoreFilter::class)
|
$aggregates = Ledger::select([DB::raw("DATE_FORMAT(`date`, '%Y-%m') as `month`"), DB::raw('SUM(`sales`) as `sales`'), DB::raw('SUM(`expenditure`) as `expenditure`')])
|
||||||
->leftJoinSub($storeLedgerStats, 'store_ledger_stats', fn ($join) => $join->on('stores.id', '=', 'store_ledger_stats.store_id'))
|
->filter($input, LedgerFilter::class)
|
||||||
->when($sorts, function ($query, $sorts) {
|
->groupBy('month')
|
||||||
foreach ($sorts as $sort) {
|
->get()
|
||||||
$query->orderBy($sort[0], $sort[1]);
|
->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) {
|
$datetime->addMonth();
|
||||||
return [
|
}
|
||||||
'ranking' => $key + 1,
|
}
|
||||||
'store' => [
|
|
||||||
'id' => $store->id,
|
$trend = collect();
|
||||||
'title' => $store->title,
|
|
||||||
],
|
for ($i=0; $i < $diffMonths; $i++) {
|
||||||
'sales' => trim_zeros($store->sales ?: 0),
|
$month = $start->format('Y-m');
|
||||||
'expenditure' => trim_zeros($store->expenditure ?: 0),
|
|
||||||
];
|
$aggregate = $aggregates->get($month);
|
||||||
})->all();
|
|
||||||
|
$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
|
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 */
|
/** @var \Illuminate\Database\Eloquent\Collection */
|
||||||
$lotteryTypes = Keyword::where('parent_key', 'lottery_type')->get();
|
$lotteryTypes = Keyword::where('parent_key', 'lottery_type')->get();
|
||||||
|
|
||||||
|
|
@ -107,7 +133,6 @@ class StatisticService
|
||||||
DB::raw('SUM(expenditure) as expenditure')
|
DB::raw('SUM(expenditure) as expenditure')
|
||||||
])
|
])
|
||||||
->filter($input, LedgerFilter::class)
|
->filter($input, LedgerFilter::class)
|
||||||
->whereBetween('date', [$start->format('Y-m-d'), $end->format('Y-m-d')])
|
|
||||||
->groupBy(['date'])
|
->groupBy(['date'])
|
||||||
->get()
|
->get()
|
||||||
->keyBy('date');
|
->keyBy('date');
|
||||||
|
|
@ -121,7 +146,6 @@ class StatisticService
|
||||||
])
|
])
|
||||||
->filter($input, LedgerItemFilter::class)
|
->filter($input, LedgerItemFilter::class)
|
||||||
->whereIn('ledger_item_type_id', $lotteryTypes->pluck('key'))
|
->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'])
|
->groupBy(['date', 'ledger_item_type_id'])
|
||||||
->get()
|
->get()
|
||||||
->groupBy('date');
|
->groupBy('date');
|
||||||
|
|
@ -135,15 +159,6 @@ class StatisticService
|
||||||
/** @var \Illuminate\Support\Collection */
|
/** @var \Illuminate\Support\Collection */
|
||||||
$lotteryTypeStatistics = $ledgerItemStatistics->get($date, collect())->keyBy('ledger_item_type_id');
|
$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([
|
$data->push([
|
||||||
'date' => $date,
|
'date' => $date,
|
||||||
'ledger' => [
|
'ledger' => [
|
||||||
|
|
@ -166,4 +181,38 @@ class StatisticService
|
||||||
|
|
||||||
return $data->all();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue