validate([ 'preset' => 'nullable|string|in:week,last_week,month,last_month', 'start_date' => 'required_with:end_date|nullable|date_format:Y-m-d', 'end_date' => 'required_with:start_date|nullable|date_format:Y-m-d|after_or_equal:start_date', ], [ 'preset.in' => 'preset 参数只能是 week/last_week/month/last_month', 'start_date.required_with' => '自定义区间需同时提供开始与结束日期', 'end_date.required_with' => '自定义区间需同时提供开始与结束日期', 'start_date.date_format' => '开始日期格式为 Y-m-d', 'end_date.date_format' => '结束日期格式为 Y-m-d', 'end_date.after_or_equal' => '结束日期不能早于开始日期', ]); $store = $this->currentStore($request); [$startDate, $endDate, $preset] = $this->resolveDateRange($params); // 明细联订货单:按订单日期/状态过滤(已取消、已删除订单不计入) $baseQuery = StoreOrderItemModel::query() ->join('store_order', static function ($join): void { $join->on('store_order.id', '=', 'store_order_item.order_id') ->whereNull('store_order.deleted_at'); }) ->where('store_order_item.store_id', $store->id) ->where('store_order.status', '<>', StoreOrderModel::STATUS_CANCELLED) ->whereDate('store_order.order_date', '>=', $startDate) ->whereDate('store_order.order_date', '<=', $endDate); $totals = (clone $baseQuery) ->selectRaw('COALESCE(SUM(store_order_item.amount), 0) as total_amount') ->selectRaw('COALESCE(SUM(store_order_item.quantity), 0) as total_quantity') ->selectRaw('COUNT(DISTINCT store_order_item.order_id) as order_count') ->first(); $totalAmount = bcadd((string) $totals->total_amount, '0', 2); $rows = (clone $baseQuery) ->select('store_order_item.product_id') ->selectRaw('MAX(store_order_item.product_name) as product_name') ->selectRaw('MAX(store_order_item.product_spec) as product_spec') ->selectRaw('MAX(store_order_item.unit) as unit') ->selectRaw('COALESCE(SUM(store_order_item.quantity), 0) as quantity') ->selectRaw('COALESCE(SUM(store_order_item.weight), 0) as weight') ->selectRaw('COALESCE(SUM(store_order_item.amount), 0) as amount') ->groupBy('store_order_item.product_id') ->orderByDesc('amount') ->orderBy('store_order_item.product_id') ->get(); $items = $rows->map(static function (StoreOrderItemModel $row) use ($totalAmount): array { // 占比 %(1 位小数,四舍五入):tenths = round(amount × 1000 ÷ total),再 ÷10 $percent = 0.0; if (bccomp($totalAmount, '0', 2) > 0) { $tenths = bcdiv( bcadd(bcmul((string) $row->amount, '1000', 2), bcdiv($totalAmount, '2', 0), 2), $totalAmount, 0 ); $percent = (float) bcdiv($tenths, '10', 1); } return [ 'product_id' => (int) $row->product_id, 'product_name' => (string) $row->product_name, 'product_spec' => (string) $row->product_spec, 'unit' => (string) $row->unit, 'quantity' => (int) $row->quantity, 'weight' => bcadd((string) $row->weight, '0', 3), 'amount' => bcadd((string) $row->amount, '0', 2), 'percent' => $percent, ]; })->values()->all(); return $this->success([ 'preset' => $preset, 'start_date' => $startDate, 'end_date' => $endDate, 'total_amount' => $totalAmount, 'total_quantity' => (int) $totals->total_quantity, 'order_count' => (int) $totals->order_count, 'item_count' => count($items), 'items' => $items, ]); } /** * 解析统计区间:自定义日期优先;否则按 preset 推导(进行中的周期封顶到今天) * * @param array{preset?: ?string, start_date?: ?string, end_date?: ?string} $params * @return array{0: string, 1: string, 2: string} [开始日期, 结束日期, 实际 preset] */ private function resolveDateRange(array $params): array { if (! empty($params['start_date']) && ! empty($params['end_date'])) { return [$params['start_date'], $params['end_date'], 'custom']; } $preset = (string) ($params['preset'] ?? 'month'); $today = Carbon::today(); [$start, $end] = match ($preset) { 'week' => [$today->copy()->startOfWeek(), $today->copy()->endOfWeek()], 'last_week' => [$today->copy()->subWeek()->startOfWeek(), $today->copy()->subWeek()->endOfWeek()], 'last_month' => [$today->copy()->subMonthNoOverflow()->startOfMonth(), $today->copy()->subMonthNoOverflow()->endOfMonth()], default => [$today->copy()->startOfMonth(), $today->copy()->endOfMonth()], }; // 未来日期本无数据,封顶到今天避免误解 if ($end->greaterThan($today)) { $end = $today->copy(); } return [$start->toDateString(), $end->toDateString(), $preset]; } }