parseTime($where['time'] ?? ''); $now = $this->basicInfo($time); $dayNum = (int)floor((strtotime($time[1]) - strtotime($time[0])) / 86400) + 1; $lastTime = [ date('Y/m/d 00:00:00', strtotime("-{$dayNum} days", strtotime($time[0]))), date('Y/m/d 23:59:59', strtotime('-1 day', strtotime($time[0]))), ]; $last = $this->basicInfo($lastTime); $data = []; foreach ($now as $key => $value) { $previous = (float)($last[$key] ?? 0); $base = $previous > 0 ? $previous : 1; $data[$key] = [ 'num' => $value, 'percent' => function_exists('bcdiv') ? bcmul(bcdiv((string)((float)$value - $previous), (string)$base, 4), '100', 2) : round((((float)$value - $previous) / $base) * 100, 2), ]; } return $data; } public function getTrend(array $where, bool $excel = false) { $time = $this->parseTime($where['time'] ?? ''); $dayCount = (int)floor((strtotime($time[1]) - strtotime($time[0])) / 86400) + 1; if ($dayCount <= 1) { return $this->trend($time, 0, $excel); } if ($dayCount <= 31) { return $this->trend($time, 1, $excel); } if ($dayCount <= 92) { return $this->trend($time, 3, $excel); } return $this->trend($time, 30, $excel); } public function getProductRanking(array $where): array { $time = $this->parseTime($where['time'] ?? ''); $productIds = $this->productIds(); if (!$productIds) { return []; } $sort = $where['sort'] ?? 'pay'; $sort = is_string($sort) && in_array($sort, self::RANK_SORT_FIELDS, true) ? $sort : 'pay'; $query = $this->productLogQuery($productIds)->whereBetweenTime('add_time', $time[0], $time[1]); $list = $query->with('storeName') ->field([ 'product_id', 'SUM(visit_num) as visit', 'COUNT(distinct(uid)) as user', 'SUM(cart_num) as cart', 'SUM(order_num) as orders', 'SUM(pay_num) as pay', 'SUM(pay_price * pay_num) as price', 'SUM(cost_price) as cost', 'ROUND((SUM(pay_price)-SUM(cost_price))/SUM(pay_price),2) as profit', 'SUM(collect_num) as collect', 'ROUND((COUNT(distinct(pay_uid))-1)/COUNT(distinct(uid)),2) as changes', 'COUNT(distinct(pay_uid))-1 as repeats', ]) ->group('product_id') ->order($sort, 'desc') ->order('product_id', 'asc') ->limit(20) ->select() ->toArray(); foreach ($list as &$item) { $item['profit'] = $item['profit'] ?? 0; $item['changes'] = $item['changes'] ?? 0; if ($item['repeats'] === null) { $item['repeats'] = 0; } else { $item['repeats'] = $this->repeats($productIds, $time, (int)$item['product_id']) / max((float)$item['repeats'], 1); } } return $list; } protected function basicInfo(array $time): array { $productIds = $this->productIds(); $data = [ 'browse' => 0, 'user' => 0, 'cart' => 0, 'order' => 0, 'pay' => 0, 'payPrice' => 0, 'cost' => 0, 'refundPrice' => 0, 'refund' => 0, 'payPercent' => 0, ]; if (!$productIds) { return $data; } $visit = $this->productLogQuery($productIds)->where('type', 'visit')->whereBetweenTime('add_time', $time[0], $time[1]); $data['browse'] = $visit->sum('visit_num'); $user = $this->productLogQuery($productIds)->where('type', 'visit')->whereBetweenTime('add_time', $time[0], $time[1]) ->field('COUNT(DISTINCT uid) AS count')->find(); $data['user'] = (int)($user['count'] ?? 0); $cart = $this->cartQuery($productIds)->whereBetweenTime('add_time', $time[0], $time[1]); $data['cart'] = $cart->sum('cart_num'); $order = $this->orderQuery()->where('is_del', 0)->whereBetweenTime('pay_time', $time[0], $time[1]); $data['order'] = $order->sum('total_num'); $paid = $this->orderQuery()->where('pay_status', OrderCenter::PAY_PAID)->where('is_del', 0) ->whereBetweenTime('pay_time', $time[0], $time[1]); $data['pay'] = $paid->sum('total_num'); $data['payPrice'] = $this->orderQuery()->where('pay_status', OrderCenter::PAY_PAID)->where('is_del', 0) ->whereBetweenTime('pay_time', $time[0], $time[1])->sum('pay_price'); $data['cost'] = array_sum($this->costTrend($time, '%Y-%m-%d')); $refunded = $this->orderQuery()->where('pay_status', OrderCenter::PAY_PAID) ->where('refund_status', OrderCenter::REFUND_SUCCESS)->where('is_del', 0) ->whereBetweenTime('pay_time', $time[0], $time[1]); $data['refundPrice'] = $refunded->sum('refund_price'); $data['refund'] = $this->orderQuery()->where('pay_status', OrderCenter::PAY_PAID) ->where('refund_status', OrderCenter::REFUND_SUCCESS)->where('is_del', 0) ->whereBetweenTime('pay_time', $time[0], $time[1])->sum('total_num'); $payPeople = $this->orderQuery()->where('pay_status', OrderCenter::PAY_PAID)->where('is_del', 0) ->whereBetweenTime('pay_time', $time[0], $time[1])->field('COUNT(DISTINCT uid) AS count')->find(); $payPeople = (int)($payPeople['count'] ?? 0); $data['payPercent'] = $data['user'] > 0 ? round($payPeople / $data['user'] * 100, 2) : 0; return $data; } protected function trend(array $time, int $step, bool $excel = false) { if ($step === 0) { $xAxis = array_map(static fn($hour) => str_pad((string)$hour, 2, '0', STR_PAD_LEFT), range(0, 23)); $timeType = '%H'; } else { $xAxis = []; $cursor = strtotime($time[0]); $end = strtotime($time[1]); while ($cursor <= $end) { if ($step === 30) { $xAxis[] = date('Y-m', $cursor); $cursor = strtotime('+1 month', $cursor); $timeType = '%Y-%m'; } else { $xAxis[] = date('m-d', $cursor); $cursor = strtotime("+{$step} day", $cursor); $timeType = '%m-%d'; } } } $productIds = $this->productIds(); $browse = $this->logTrend($productIds, $time, $timeType, 'SUM(visit_num)'); $user = $this->logTrend($productIds, $time, $timeType, 'COUNT(DISTINCT uid)'); $paid = ['pay_status' => OrderCenter::PAY_PAID, 'is_del' => 0]; $refunded = ['pay_status' => OrderCenter::PAY_PAID, 'refund_status' => OrderCenter::REFUND_SUCCESS, 'is_del' => 0]; $pay = $this->groupOrderTrend($paid, 'SUM(pay_price)', $time, $timeType); $refund = $this->groupOrderTrend($refunded, 'SUM(refund_price)', $time, $timeType); if (!$excel) { $data = [ '商品浏览量' => [], '商品访客量' => [], '支付金额' => [], '退款金额' => [], ]; foreach ($xAxis as $item) { $data['商品浏览量'][] = (float)($browse[$item] ?? 0); $data['商品访客量'][] = (float)($user[$item] ?? 0); $data['支付金额'][] = (float)($pay[$item] ?? 0); $data['退款金额'][] = (float)($refund[$item] ?? 0); } $series = []; foreach ($data as $name => $values) { $series[] = [ 'name' => $name, 'data' => $values, 'type' => in_array($name, ['商品浏览量', '商品访客量'], true) ? 'line' : 'bar', 'smooth' => 'true', 'yAxisIndex' => in_array($name, ['商品浏览量', '商品访客量'], true) ? 1 : 0, ]; } return compact('xAxis', 'series'); } $cart = $this->cartTrend($productIds, $time, $timeType, 'SUM(cart_num)'); $order = $this->groupOrderTrend(['is_del' => 0], 'SUM(total_num)', $time, $timeType); $payNum = $this->groupOrderTrend($paid, 'SUM(total_num)', $time, $timeType); $payCount = $this->groupOrderTrend($paid, 'COUNT(*)', $time, $timeType); $cost = $this->costTrend($time, $timeType); $refundNum = $this->groupOrderTrend($refunded, 'SUM(total_num)', $time, $timeType); $rows = []; foreach ($xAxis as $item) { $changes = !empty($user[$item]) ? round(((float)($payCount[$item] ?? 0) / (float)$user[$item]) * 100, 2) : 0; $rows[] = [ 'time' => $item, 'browse' => $browse[$item] ?? 0, 'user' => $user[$item] ?? 0, 'pay' => $pay[$item] ?? 0, 'refund' => $refund[$item] ?? 0, 'cart' => $cart[$item] ?? 0, 'order' => $order[$item] ?? 0, 'payNum' => $payNum[$item] ?? 0, 'cost' => $cost[$item] ?? 0, 'refundNum' => $refundNum[$item] ?? 0, 'changes' => min($changes, 100), ]; } /** @var ExportServices $exportService */ $exportService = app()->make(ExportServices::class); return ['url' => $exportService->productTrade($rows)]; } protected function productIds(): array { return array_map('intval', app()->make(CenterProductModel::class)->where('product_type', 'normal')->column('id')); } protected function productLogQuery(array $productIds) { $query = app()->make(StoreProductLog::class)->whereIn('product_id', $productIds ?: [0]); return $query; } protected function cartQuery(array $productIds) { return app()->make(StoreCart::class)->whereIn('product_id', $productIds ?: [0]); } protected function orderQuery() { return app()->make(OrderCenter::class)->where('product_type', 'normal'); } protected function logTrend(array $productIds, array $time, string $timeType, string $expression): array { if (!$productIds) { return []; } return array_column($this->productLogQuery($productIds)->whereBetweenTime('add_time', $time[0], $time[1]) ->field("FROM_UNIXTIME(add_time,'{$timeType}') AS days,{$expression} AS num") ->group('days')->select()->toArray(), 'num', 'days'); } protected function cartTrend(array $productIds, array $time, string $timeType, string $expression): array { if (!$productIds) { return []; } return array_column($this->cartQuery($productIds)->whereBetweenTime('add_time', $time[0], $time[1]) ->field("FROM_UNIXTIME(add_time,'{$timeType}') AS days,{$expression} AS num") ->group('days')->select()->toArray(), 'num', 'days'); } protected function groupOrderTrend(array $where, string $expression, array $time, string $timeType): array { $query = $this->orderQuery()->where($where)->whereBetweenTime('pay_time', $time[0], $time[1]); return array_column($query->field("FROM_UNIXTIME(pay_time,'{$timeType}') AS days,{$expression} AS num") ->group('days')->select()->toArray(), 'num', 'days'); } protected function costTrend(array $time, string $timeType): array { $rows = $this->orderQuery()->where('pay_status', OrderCenter::PAY_PAID)->where('is_del', 0) ->whereBetweenTime('pay_time', $time[0], $time[1]) ->field("FROM_UNIXTIME(pay_time,'{$timeType}') AS days,extra_info") ->select()->toArray(); $result = []; foreach ($rows as $row) { $extra = $row['extra_info'] ?? []; if (is_string($extra)) { $extra = json_decode($extra, true) ?: []; } $dayCost = 0.0; foreach (($extra['items'] ?? []) as $item) { $dayCost += (float)($item['attr_info']['cost'] ?? 0) * (float)($item['cart_num'] ?? 0); } $result[$row['days']] = ($result[$row['days']] ?? 0) + $dayCost; } return $result; } protected function repeats(array $productIds, array $time, int $productId): int { return count($this->productLogQuery($productIds)->where('type', 'pay')->where('product_id', $productId) ->whereBetweenTime('add_time', $time[0], $time[1])->field('COUNT(pay_uid) AS p')->group('pay_uid')->having('p > 1')->select()); } protected function parseTime(string $time): array { if (!str_contains($time, '-')) { throw new AdminException('请选择时间'); } [$start, $end] = explode('-', $time, 2); if (!$start || !$end) { throw new AdminException('请选择时间'); } return [$start, $end]; } }