connection = $mirror->connection(); } public function ready(): bool { return $this->datasetsReady(self::DATASETS); } public function promotionReady(): bool { return $this->datasetsReady(self::PROMOTION_DATASETS); } private function datasetsReady(array $datasets): bool { try { foreach ($datasets as $dataset) { if (! $this->mirror->ready($this->datasets->get($dataset))) { return false; } } return true; } catch (\Throwable) { return false; } } /** @return array{rows:array>} */ public function afterSales(array $filters, int $page, int $limit): array { $limit = min(max($limit, 1), 100); $query = $this->baseQuery(); $this->applyFilters($query, $filters); $rows = $query ->select($this->columns()) ->orderByDesc('o.order_date')->orderByDesc('o.o_id')->orderByDesc('i.oi_id') ->offset((max($page, 1) - 1) * $limit) ->limit($limit + 1) ->get()->map(static fn (object $row): array => (array) $row)->all(); $rows = array_map(fn (array $row): array => $this->baseRow($row), $rows); $this->hydrateRefunds($rows); return ['rows' => $rows]; } /** @return array{rows:array>,date:?string} */ public function promotionExpenses(array $filters, int $page, int $limit): array { $limit = min(max($limit, 1), 100); $date = trim((string) ($filters['date'] ?? '')); if ($date === '') { $date = (string) ($this->connection->table('ops_item_sales_cost_ratio')->max('stat_date') ?? ''); $date = substr($date, 0, 10); } if ($date === '') { return ['rows' => [], 'date' => null]; } $query = $this->connection->table('ops_item_sales_cost_ratio as c') ->where('c.stat_date', '>=', $date . ' 00:00:00') ->where('c.stat_date', '<', date('Y-m-d 00:00:00', strtotime($date . ' +1 day'))) ->where('c.push_fee', '<>', 0); if (($filters['shop_name'] ?? '') !== '') { $query->whereRaw("c.shop_name LIKE ? ESCAPE '!'", [$this->contains((string) $filters['shop_name'])]); } if (($filters['sku_id'] ?? '') !== '') { $needle = $this->contains((string) $filters['sku_id']); $query->where(function (Builder $items) use ($needle): void { $items->whereRaw("c.item_id LIKE ? ESCAPE '!'", [$needle]) ->orWhereExists(function (Builder $mapping) use ($needle): void { $mapping->selectRaw('1')->from('ops_jst_sku_map as filter_map') ->whereColumn('filter_map.shop_i_id', 'c.item_id') ->where(function (Builder $codes) use ($needle): void { $codes->whereRaw("filter_map.sku_id LIKE ? ESCAPE '!'", [$needle]) ->orWhereRaw("filter_map.i_id LIKE ? ESCAPE '!'", [$needle]) ->orWhereRaw("filter_map.shop_sku_id LIKE ? ESCAPE '!'", [$needle]); }); }); }); } $rows = $query->select(['c.shop_name', 'c.item_id as shop_style_code', 'c.push_fee as promotion_fee']) ->orderByDesc('c.push_fee')->orderBy('c.shop_name')->orderBy('c.item_id') ->offset((max($page, 1) - 1) * $limit)->limit($limit + 1) ->get()->map(static fn (object $row): array => (array) $row)->all(); $itemIds = array_values(array_unique(array_filter(array_column($rows, 'shop_style_code')))); $mappings = $itemIds === [] ? collect() : $this->connection->table('ops_jst_sku_map as m') ->leftJoin('ops_jst_shop_info as shop', 'shop.shop_id', '=', 'm.shop_id') ->whereIn('m.shop_i_id', $itemIds)->orderByDesc('m.modified') ->get(['m.shop_i_id', 'm.sku_id', 'm.i_id', 'm.shop_sku_id', 'm.properties_value', 'shop.shop_name']) ->groupBy('shop_i_id'); foreach ($rows as &$row) { $candidates = $mappings->get($row['shop_style_code'], collect()); $mapping = $candidates->firstWhere('shop_name', $row['shop_name']) ?? $candidates->first(); $row['product_code'] = $mapping->sku_id ?? null; $row['style_code'] = $mapping->i_id ?? null; $row['shop_product_code'] = $mapping->shop_sku_id ?? null; $row['online_color_spec'] = $mapping->properties_value ?? null; $row['promotion_fee'] = round((float) $row['promotion_fee'], 2); } unset($row); return ['rows' => $rows, 'date' => $date]; } private function baseQuery(): Builder { $latestSkus = $this->connection->table('ops_jst_sku') ->selectRaw('sku_id, MAX(autoid) AS latest_autoid') ->where('sku_type', 'normal')->groupBy('sku_id'); return $this->connection->table('ops_jst_orders as o') ->join('ops_jst_order_item as i', 'i.o_id', '=', 'o.o_id') ->leftJoinSub($latestSkus, 'sku_keys', 'sku_keys.sku_id', '=', 'i.sku_id') ->leftJoin('ops_jst_sku as s', 's.autoid', '=', 'sku_keys.latest_autoid') ->leftJoin('ops_jst_warehouse_info as wh', function ($join): void { $join->on('wh.wms_co_id', '=', $this->connection->raw('COALESCE(i.send_warehouse, o.wms_co_id)')); }); } private function applyFilters(Builder $query, array $filters): void { if (($filters['from'] ?? '') !== '') { $query->where('o.order_date', '>=', $filters['from'] . ' 00:00:00'); } if (($filters['to'] ?? '') !== '') { $query->where('o.order_date', '<', date('Y-m-d 00:00:00', strtotime($filters['to'] . ' +1 day'))); } if (($filters['order_no'] ?? '') !== '') { $needle = $this->contains((string) $filters['order_no']); $query->where(function (Builder $order) use ($needle): void { $order->whereRaw("CAST(o.o_id AS CHAR) LIKE ? ESCAPE '!'", [$needle]) ->orWhereRaw("o.so_id LIKE ? ESCAPE '!'", [$needle]) ->orWhereRaw("i.outer_oi_id LIKE ? ESCAPE '!'", [$needle]); }); } if (($filters['shop_name'] ?? '') !== '') { $query->whereRaw("o.shop_name LIKE ? ESCAPE '!'", [$this->contains((string) $filters['shop_name'])]); } if (($filters['sku_id'] ?? '') !== '') { $needle = $this->contains((string) $filters['sku_id']); $query->where(function (Builder $sku) use ($needle): void { $sku->whereRaw("i.sku_id LIKE ? ESCAPE '!'", [$needle]) ->orWhereRaw("i.shop_sku_id LIKE ? ESCAPE '!'", [$needle]) ->orWhereRaw("i.shop_i_id LIKE ? ESCAPE '!'", [$needle]); }); } } /** @return list */ private function columns(): array { return [ 'o.o_id as internal_order_no', 'o.so_id as online_order_no', 'i.outer_oi_id as sub_order_no', 'o.pay_date as pay_time', 'o.end_time as order_complete_time', 'o.send_date as send_time', 'o.order_date as order_time', 'o.shop_name', 'o.shop_id as shop_code', 'i.sku_id as product_code', 'i.i_id as style_code', 'i.shop_sku_id as shop_product_code', 'i.shop_i_id as shop_style_code', 'o.shop_site', 's.brand', 's.category', 's.vc_name as virtual_category', 's.cost_price as unit_cost', 'i.properties_value as color_spec', 'i.name as online_product_name', 's.name as sku_name', 'i.qty as product_quantity', 'i.amount as product_amount', 'i.buyer_paid_amount as payment_amount', 'i.seller_income_amount', 'i.is_gift', 'i.src_combine_sku_id', 'o.type as order_type', 'o.labels as order_tags', 'o.free_amount as merchant_discount', 'o.freight as freight_income', 'i.send_warehouse', 'o.wms_co_id', 'o.l_id as express_no', 'o.logistics_company as express_company', 'o.receiver_state as province', 'o.receiver_city as city', 'o.creator_name as salesperson', 'wh.name as shipping_warehouse', 'wh.remark1 as operation_cloud_warehouse', 's.supplier_id as supplier_code', 's.supplier_name', ]; } private function baseRow(array $row): array { $quantity = (float) ($row['product_quantity'] ?? 0); $amount = (float) ($row['product_amount'] ?? 0); $cost = (float) ($row['unit_cost'] ?? 0); $payment = (float) ($row['payment_amount'] ?? $amount); $sellerIncome = (float) ($row['seller_income_amount'] ?? 0); $row += [ 'after_sales_no' => null, 'refund_type' => null, 'question_type' => null, 'detail_return_type' => null, 'after_sales_tags' => null, ]; $row['product_name'] = ($row['sku_name'] ?? '') !== '' ? $row['sku_name'] : ($row['color_spec'] ?? null); $row['product_quantity'] = $quantity; $row['product_amount'] = $amount; $row['product_cost'] = $cost * $quantity; $row['reversal_order_cost'] = $amount < 0 ? $cost * abs($quantity) : 0.0; $row['merchant_discount'] = (float) ($row['merchant_discount'] ?? 0); $row['freight_income'] = (float) ($row['freight_income'] ?? 0); $row['payment_amount'] = $payment; $row['platform_discount'] = $sellerIncome > $payment ? $sellerIncome - $payment : 0.0; $row['reversal_order_amount'] = $amount < 0 ? abs($amount) : 0.0; $row['actual_shipped_amount'] = $amount; $row['actual_shipped_quantity'] = $quantity; $row['actual_shipped_cost'] = $cost * $quantity; foreach (['pre', 'post'] as $phase) { foreach (['application_quantity', 'application_amount', 'application_cost', 'return_quantity', 'return_amount', 'return_cost', 'refund_amount', 'refund_cost', 'platform_subsidy'] as $field) { $row[$phase . '_' . $field] = 0.0; } } $row['defective_quantity'] = 0.0; $row['is_gift'] = (int) ($row['is_gift'] ?? 0) === 1 ? '是' : '否'; $row['combine_type'] = empty($row['src_combine_sku_id']) ? '非组合' : '组合装'; $row['shipping_warehouse_code'] = $row['send_warehouse'] ?: ($row['wms_co_id'] ?? null); $row['product_link'] = $this->productLink( (string) ($row['shop_site'] ?? ''), (string) ($row['shop_name'] ?? ''), (string) ($row['shop_style_code'] ?? ''), ); foreach (['offline_customer_code', 'offline_customer_name', 'unique_code', 'registration_time', 'confirmation_time', 'warehouse_entry_time', 'classification_order', 'receiving_warehouse_code', 'receiving_warehouse'] as $field) { $row[$field] = null; } foreach ($this->expenseFields() as $field) { $row[$field] = null; } unset($row['shop_site'], $row['unit_cost'], $row['sku_name'], $row['seller_income_amount'], $row['src_combine_sku_id'], $row['send_warehouse'], $row['wms_co_id']); return $row; } /** @param array> $rows */ private function hydrateRefunds(array &$rows): void { $orderIds = array_values(array_unique(array_filter(array_column($rows, 'internal_order_no')))); if ($orderIds === []) { return; } $refunds = $this->connection->table('ops_jst_return_refund_info as a') ->join('ops_jst_return_refund_item as ri', 'ri.as_id', '=', 'a.as_id') ->whereIn('a.o_id', $orderIds) ->orderByDesc('a.as_date')->orderByDesc('a.as_id') ->get([ 'a.o_id as internal_order_no', 'a.as_id as after_sales_id', 'a.outer_as_id as outside_after_sales_id', 'a.as_date as after_sales_time', 'a.type as refund_type', 'a.question_type', 'a.labels as after_sales_tags', 'a.refund as refund_amount', 'a.free_amount as platform_subsidy', 'ri.outer_oi_id as sub_order_no', 'ri.sku_id as product_code', 'ri.type as detail_return_type', 'ri.qty as application_quantity', 'ri.amount as application_amount', 'ri.r_qty as return_quantity', 'ri.shop_amount as return_amount', 'ri.defective_qty as defective_quantity', ])->groupBy('internal_order_no'); foreach ($rows as &$row) { $matches = collect($refunds->get($row['internal_order_no'], collect()))->filter( static function (object $refund) use ($row): bool { return (string) ($refund->sub_order_no ?? '') !== '' ? (string) $refund->sub_order_no === (string) ($row['sub_order_no'] ?? '') : (string) ($refund->product_code ?? '') === (string) ($row['product_code'] ?? ''); }, )->values(); $first = $matches->first(); if ($first !== null) { $row['after_sales_no'] = $first->outside_after_sales_id ?: $first->after_sales_id; $row['refund_type'] = $first->refund_type; $row['question_type'] = $first->question_type; $row['detail_return_type'] = $first->detail_return_type; $row['after_sales_tags'] = $first->after_sales_tags; } $unitCost = $row['product_quantity'] == 0 ? 0 : $row['product_cost'] / $row['product_quantity']; $preReturned = 0.0; foreach ($matches as $refund) { $phase = empty($row['send_time']) || (string) $refund->after_sales_time < (string) $row['send_time'] ? 'pre' : 'post'; $application = (float) ($refund->application_quantity ?? 0); $returned = (float) ($refund->return_quantity ?? 0); $row[$phase . '_application_quantity'] += $application; $row[$phase . '_application_amount'] += (float) ($refund->application_amount ?? 0); $row[$phase . '_application_cost'] += $application * $unitCost; $row[$phase . '_return_quantity'] += $returned; $row[$phase . '_return_amount'] += (float) ($refund->return_amount ?? 0); $row[$phase . '_return_cost'] += $returned * $unitCost; $row[$phase . '_refund_amount'] += (float) ($refund->refund_amount ?? 0); $row[$phase . '_refund_cost'] += ($application - $returned) * $unitCost; $row[$phase . '_platform_subsidy'] += (float) ($refund->platform_subsidy ?? 0); $row['defective_quantity'] += (float) ($refund->defective_quantity ?? 0); if ($phase === 'pre') { $preReturned += $returned; } } $row['actual_shipped_quantity'] = $row['product_quantity'] - $preReturned; $row['actual_shipped_amount'] = $row['product_amount'] - $row['pre_return_amount']; $row['actual_shipped_cost'] = $row['actual_shipped_quantity'] * $unitCost; } unset($row); } private function productLink(string $site, string $shopName, string $itemId): ?string { if ($itemId === '') { return null; } $platform = mb_strtolower($site . ' ' . $shopName); if (str_contains($platform, '京东') || str_contains($platform, '京喜') || str_contains($platform, 'jd')) { return 'https://item.jd.com/' . rawurlencode($itemId) . '.html'; } if (str_contains($platform, '抖音')) { return 'https://haohuo.jinritemai.com/ecommerce/trade/detail/index.html?id=' . rawurlencode($itemId); } return null; } /** @return list */ private function expenseFields(): array { return [ 'account_500112', 'account_60031', 'account_800101', 'account_800103', 'account_800104', 'account_800105', 'account_800106', 'account_800107', 'account_800108', 'account_800109', 'account_800110', 'account_800114', 'account_8001201', 'account_8001202', 'account_800122', 'account_800125', 'account_800127', 'account_800129', 'account_800130', 'account_800132', 'account_800304', 'account_800501', 'account_800503', ]; } private function contains(string $value): string { return '%' . str_replace(['!', '%', '_'], ['!!', '!%', '!_'], trim($value)) . '%'; } }