toDateString(); return $this->success([ 'overview' => $this->overview(), 'trend' => $this->salesTrend($today), 'order_status' => $this->orderStatusDistribution(), 'category_sales' => $this->categorySales(), 'top_products' => $this->topProducts(), 'top_stores' => $this->topStores(), 'recon' => $this->reconSummary(), 'latest_orders' => $this->latestOrders(), 'archives' => $this->archives(), ]); } /** * 核心指标卡:销售额 / 订单数 / 采购金额(累计、今日、近7天、环比、7日迷你趋势)+ 应收余额 * * @return array{ * sales: array{total: float, today: float, week: float, growth: float|null, trend7: float[]}, * orders: array{total: int, today: int, week: int, growth: float|null, trend7: int[]}, * purchase: array{total: float, today: float, week: float, growth: float|null, trend7: float[]}, * receivable: array{bill_total: float, received: float, unreceived: float} * } */ private function overview(): array { $today = now()->toDateString(); $weekStart = now()->subDays(6)->toDateString(); $lastWeekStart = now()->subDays(13)->toDateString(); $lastWeekEnd = now()->subDays(7)->toDateString(); // 近14天按日聚合(前7天用于环比,后7天用于迷你趋势图) $daily = $this->validOrderQuery() ->whereBetween('order_date', [$lastWeekStart, $today]) ->get(['order_date', 'total_amount']) ->groupBy(fn (StoreOrderModel $order) => $order->order_date->toDateString()); $salesByDay = []; $ordersByDay = []; foreach ($daily as $date => $orders) { $salesByDay[$date] = (float) $orders->sum('total_amount'); $ordersByDay[$date] = $orders->count(); } $salesWeek = $this->sumRange($salesByDay, $weekStart, $today); $salesLastWeek = $this->sumRange($salesByDay, $lastWeekStart, $lastWeekEnd); $ordersWeek = (int) $this->sumRange($ordersByDay, $weekStart, $today); $ordersLastWeek = (int) $this->sumRange($ordersByDay, $lastWeekStart, $lastWeekEnd); // 采购金额:实际价优先,无实际价用预估价 $purchaseDaily = PurchaseOrderModel::query() ->whereBetween('purchase_date', [$lastWeekStart, $today]) ->get(['purchase_date', 'estimate_amount', 'actual_amount']) ->groupBy(fn (PurchaseOrderModel $purchase) => $purchase->purchase_date->toDateString()) ->map(fn ($purchases) => (float) $purchases->sum( fn (PurchaseOrderModel $purchase) => (float) $purchase->actual_amount > 0 ? $purchase->actual_amount : $purchase->estimate_amount )); $purchaseWeek = $this->sumRange($purchaseDaily->all(), $weekStart, $today); $purchaseLastWeek = $this->sumRange($purchaseDaily->all(), $lastWeekStart, $lastWeekEnd); // 应收余额:账单总额 - 已支付账单金额 $billTotal = (float) BillModel::query()->sum('total_amount'); $received = (float) BillModel::query()->where('status', BillModel::STATUS_PAID)->sum('total_amount'); return [ 'sales' => [ 'total' => $this->money($this->validOrderQuery()->sum('total_amount')), 'today' => $this->money($salesByDay[$today] ?? 0), 'week' => $this->money($salesWeek), 'growth' => $this->growthRate($salesWeek, $salesLastWeek), 'trend7' => array_map( fn ($day) => $this->money($salesByDay[$day] ?? 0), $this->dateRange($weekStart, $today) ), ], 'orders' => [ 'total' => (int) $this->validOrderQuery()->count(), 'today' => (int) ($ordersByDay[$today] ?? 0), 'week' => $ordersWeek, 'growth' => $this->growthRate((float) $ordersWeek, (float) $ordersLastWeek), 'trend7' => array_map( static fn ($day) => (int) ($ordersByDay[$day] ?? 0), $this->dateRange($weekStart, $today) ), ], 'purchase' => [ 'total' => $this->money(PurchaseOrderModel::query()->sum(DB::raw( 'CASE WHEN actual_amount > 0 THEN actual_amount ELSE COALESCE(estimate_amount, 0) END' ))), 'today' => $this->money($purchaseDaily[$today] ?? 0), 'week' => $this->money($purchaseWeek), 'growth' => $this->growthRate($purchaseWeek, $purchaseLastWeek), 'trend7' => array_map( fn ($day) => $this->money($purchaseDaily[$day] ?? 0), $this->dateRange($weekStart, $today) ), ], 'receivable' => [ 'bill_total' => $this->money($billTotal), 'received' => $this->money($received), 'unreceived' => $this->money($billTotal - $received), ], ]; } /** * 近30天销售趋势(缺日补0) * * @return array */ private function salesTrend(string $today): array { $start = now()->subDays(self::TREND_DAYS - 1)->toDateString(); $daily = $this->validOrderQuery() ->whereBetween('order_date', [$start, $today]) ->get(['order_date', 'total_amount']) ->groupBy(fn (StoreOrderModel $order) => $order->order_date->toDateString()); return array_map(static function ($date) use ($daily) { $orders = $daily->get($date); return [ 'date' => $date, 'amount' => round((float) ($orders?->sum('total_amount') ?? 0), 2), 'orders' => $orders?->count() ?? 0, ]; }, $this->dateRange($start, $today)); } /** * 订单状态分布(含已取消,全部状态固定输出,缺省补0) * * @return array */ private function orderStatusDistribution(): array { $counts = StoreOrderModel::query() ->selectRaw('status, COUNT(*) as cnt') ->groupBy('status') ->pluck('cnt', 'status'); $result = []; foreach (StoreOrderModel::STATUS_NAMES as $status => $name) { $result[] = [ 'status' => $status, 'name' => $name, 'count' => (int) ($counts[$status] ?? 0), ]; } return $result; } /** * 品类销售占比(按明细金额汇总,金额降序) * * @return array */ private function categorySales(): array { $rows = $this->validItemQuery() ->groupBy('i.category_id') ->selectRaw('i.category_id, SUM(i.amount) as amount') ->orderByDesc('amount') ->get(); $categoryNames = ProductCategoryModel::query()->pluck('name', 'id'); return $rows->map(static fn ($row) => [ 'name' => $categoryNames[$row->category_id] ?? '未分类', 'amount' => round((float) $row->amount, 2), ])->all(); } /** * 热销商品 TOP10(按明细金额汇总;商品名/规格/单位取快照) * * @return array */ private function topProducts(): array { return $this->validItemQuery() ->groupBy('i.product_id') ->selectRaw(implode(', ', [ 'i.product_id', 'MAX(i.product_name) as product_name', 'MAX(i.product_spec) as product_spec', 'MAX(i.unit) as unit', 'SUM(i.quantity) as quantity', 'SUM(i.amount) as amount', ])) ->orderByDesc('amount') ->limit(self::TOP_LIMIT) ->get() ->map(static fn ($row) => [ 'product_id' => (int) $row->product_id, 'product_name' => (string) $row->product_name, 'product_spec' => (string) $row->product_spec, 'unit' => (string) $row->unit, 'quantity' => round((float) $row->quantity, 2), 'amount' => round((float) $row->amount, 2), ]) ->all(); } /** * 门店排行 TOP10(按订单金额汇总,含软删除门店名称回查) * * @return array */ private function topStores(): array { $rows = $this->validOrderQuery() ->groupBy('store_id') ->selectRaw('store_id, COUNT(*) as order_count, SUM(total_amount) as amount') ->orderByDesc('amount') ->limit(self::TOP_LIMIT) ->get(); $storeNames = StoreModel::withTrashed() ->whereIn('id', $rows->pluck('store_id')) ->pluck('name', 'id'); return $rows->map(static fn ($row) => [ 'store_id' => (int) $row->store_id, 'store_name' => $storeNames[$row->store_id] ?? '未知门店', 'order_count' => (int) $row->order_count, 'amount' => round((float) $row->amount, 2), ])->all(); } /** * 对账与回款概览:账单支付进度分布 + 待审核回款 * * @return array{ * bills: array, * pending_payment: array{count: int, amount: float} * } */ private function reconSummary(): array { $states = []; foreach (BillModel::PAY_STATE_NAMES as $state => $name) { $states[$state] = ['pay_state' => $state, 'name' => $name, 'count' => 0, 'amount' => 0.0]; } BillModel::query() ->get(['id', 'total_amount', 'status', 'payment_id']) ->each(static function (BillModel $bill) use (&$states) { $state = $bill->payState(); $states[$state]['count']++; $states[$state]['amount'] += (float) $bill->total_amount; }); $bills = array_values(array_map( fn ($state) => [ 'pay_state' => $state['pay_state'], 'name' => $state['name'], 'count' => $state['count'], 'amount' => $this->money($state['amount']), ], $states )); $pendingPayment = PaymentModel::query()->where('status', PaymentModel::STATUS_PENDING); return [ 'bills' => $bills, 'pending_payment' => [ 'count' => (int) $pendingPayment->count(), 'amount' => $this->money($pendingPayment->sum('amount')), ], ]; } /** * 最新订单(按下单日期倒序,含已取消,便于掌握最新动态) * * @return array */ private function latestOrders(): array { return StoreOrderModel::query() ->with('store:id,name') ->orderByDesc('order_date') ->orderByDesc('id') ->limit(self::LATEST_ORDER_LIMIT) ->get() ->map(static fn (StoreOrderModel $order) => [ 'id' => $order->id, 'order_no' => $order->order_no, 'store_name' => $order->store->name ?? '未知门店', 'order_date' => $order->order_date->toDateString(), 'total_amount' => round((float) $order->total_amount, 2), 'status' => $order->status, 'status_name' => StoreOrderModel::STATUS_NAMES[$order->status] ?? (string) $order->status, ]) ->all(); } /** * 基础档案计数:在营门店 / 在售商品 / 合作供应商 * * @return array{stores: int, products: int, suppliers: int} */ private function archives(): array { return [ 'stores' => StoreModel::query()->where('status', StoreModel::STATUS_NORMAL)->count(), 'products' => ProductModel::query()->where('status', ProductModel::STATUS_ON)->count(), 'suppliers' => SupplierModel::query()->where('status', SupplierModel::STATUS_NORMAL)->count(), ]; } /** 有效订单查询(排除已取消;软删除由模型全局作用域排除) */ private function validOrderQuery(): Builder { return StoreOrderModel::query()->where('status', '<>', StoreOrderModel::STATUS_CANCELLED); } /** * 有效订单明细查询(join 绕过模型全局作用域,需手动排除软删除订单与已取消订单) */ private function validItemQuery(): Builder { return StoreOrderItemModel::query() ->from('store_order_item as i') ->join('store_order as o', 'o.id', '=', 'i.order_id') ->whereNull('o.deleted_at') ->where('o.status', '<>', StoreOrderModel::STATUS_CANCELLED); } /** * 按日期区间(含端点)汇总每日数据,缺日按0计 * * @param array $daily 日期 => 当日值 */ private function sumRange(array $daily, string $start, string $end): float { $sum = 0.0; foreach ($this->dateRange($start, $end) as $date) { $sum += (float) ($daily[$date] ?? 0); } return $sum; } /** * 生成连续日期列表(含端点) * * @return string[] */ private function dateRange(string $start, string $end): array { $dates = []; $cursor = $start; while ($cursor <= $end) { $dates[] = $cursor; $cursor = date('Y-m-d', strtotime($cursor . ' +1 day')); } return $dates; } /** 金额统一保留两位 */ private function money(float|string|null $value): float { return round((float) $value, 2); } /** * 环比增长率(%):上期为0时无基准,返回 null */ private function growthRate(float $current, float $previous): ?float { if ($previous <= 0.0) { return null; } return round(($current - $previous) / $previous * 100, 1); } }