>,date:?string} */ public function afterSales(array $filters, int $page, int $limit): array { $where = []; $bindings = []; if (($filters['from'] ?? '') !== '') { $where[] = 'o.[order_date] >= CAST(? AS date)'; $bindings[] = $filters['from']; } if (($filters['to'] ?? '') !== '') { $where[] = 'o.[order_date] < DATEADD(day, 1, CAST(? AS date))'; $bindings[] = $filters['to']; } if (($filters['order_no'] ?? '') !== '') { $needle = $this->contains($filters['order_no']); $where[] = "(CONVERT(varchar(30), o.[o_id]) LIKE ? ESCAPE '\\'" . " OR o.[so_id] LIKE ? ESCAPE '\\'" . " OR i.[outer_oi_id] LIKE ? ESCAPE '\\')"; array_push($bindings, $needle, $needle, $needle); } if (($filters['shop_name'] ?? '') !== '') { $where[] = "o.[shop_name] LIKE ? ESCAPE '\\'"; $bindings[] = $this->contains($filters['shop_name']); } if (($filters['sku_id'] ?? '') !== '') { $needle = $this->contains($filters['sku_id']); $where[] = "(i.[sku_id] LIKE ? ESCAPE '\\'" . " OR i.[shop_sku_id] LIKE ? ESCAPE '\\'" . " OR i.[shop_i_id] LIKE ? ESCAPE '\\')"; array_push($bindings, $needle, $needle, $needle); } $whereSql = $where === [] ? '1 = 1' : implode(' AND ', $where); [$offset, $fetch] = $this->pageBounds($page, $limit); $sql = "WITH paged_rows AS ( SELECT 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], o.[end_time], o.[send_date], o.[order_date], o.[shop_name], o.[shop_id], o.[shop_site], o.[type] AS order_type, o.[labels] AS order_tags, 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, o.[free_amount] AS merchant_discount, o.[freight] AS freight_income, i.[oi_id], 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, i.[properties_value] AS color_spec, i.[name] AS online_product_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], i.[sku_type], i.[send_warehouse] FROM [rpt].[jst_Orders] o INNER JOIN [rpt].[jst_OrderItem] i ON i.[o_id] = o.[o_id] WHERE {$whereSql} ORDER BY o.[order_date] DESC, o.[o_id] DESC, i.[oi_id] DESC OFFSET {$offset} ROWS FETCH NEXT {$fetch} ROWS ONLY ) SELECT p.[internal_order_no], p.[online_order_no], p.[sub_order_no], CAST(NULL AS nvarchar(100)) AS after_sales_no, CONVERT(varchar(19), p.[pay_date], 120) AS pay_time, CONVERT(varchar(19), p.[end_time], 120) AS order_complete_time, CONVERT(varchar(19), p.[send_date], 120) AS send_time, CONVERT(varchar(19), p.[order_date], 120) AS order_time, p.[shop_name], p.[shop_id] AS shop_code, p.[product_code], p.[style_code], p.[shop_product_code], p.[shop_style_code], p.[shop_site], s.[brand], s.[category], s.[vc_name] AS virtual_category, s.[cost_price] AS unit_cost, p.[color_spec], COALESCE(NULLIF(s.[name], ''), p.[color_spec]) AS product_name, CAST(p.[product_quantity] AS decimal(18,2)) AS product_quantity, CAST(ISNULL(p.[product_amount], 0) AS decimal(18,2)) AS product_amount, CAST(ISNULL(s.[cost_price], 0) * ISNULL(p.[product_quantity], 0) AS decimal(18,2)) AS product_cost, CAST(CASE WHEN ISNULL(p.[product_amount], 0) < 0 THEN ISNULL(s.[cost_price], 0) * ABS(ISNULL(p.[product_quantity], 0)) ELSE 0 END AS decimal(18,2)) AS reversal_order_cost, CAST(ISNULL(p.[merchant_discount], 0) AS decimal(18,2)) AS merchant_discount, CAST(ISNULL(p.[freight_income], 0) AS decimal(18,2)) AS freight_income, CAST(COALESCE(p.[payment_amount], p.[product_amount], 0) AS decimal(18,2)) AS payment_amount, CAST(CASE WHEN ISNULL(p.[seller_income_amount], 0) > ISNULL(p.[payment_amount], 0) THEN p.[seller_income_amount] - ISNULL(p.[payment_amount], 0) ELSE 0 END AS decimal(18,2)) AS platform_discount, CAST(CASE WHEN ISNULL(p.[product_amount], 0) < 0 THEN ABS(p.[product_amount]) ELSE 0 END AS decimal(18,2)) AS reversal_order_amount, CAST(ISNULL(p.[product_amount], 0) AS decimal(18,2)) AS actual_shipped_amount, CAST(ISNULL(p.[product_quantity], 0) AS decimal(18,2)) AS actual_shipped_quantity, CAST(ISNULL(s.[cost_price], 0) * ISNULL(p.[product_quantity], 0) AS decimal(18,2)) AS actual_shipped_cost, p.[order_type], p.[online_product_name], CAST(0 AS decimal(18,2)) AS pre_application_quantity, CAST(0 AS decimal(18,2)) AS pre_application_amount, CAST(0 AS decimal(18,2)) AS pre_application_cost, CAST(0 AS decimal(18,2)) AS pre_return_quantity, CAST(0 AS decimal(18,2)) AS pre_return_amount, CAST(0 AS decimal(18,2)) AS pre_return_cost, CAST(0 AS decimal(18,2)) AS pre_refund_amount, CAST(0 AS decimal(18,2)) AS pre_refund_cost, CAST(0 AS decimal(18,2)) AS pre_platform_subsidy, CAST(0 AS decimal(18,2)) AS post_application_quantity, CAST(0 AS decimal(18,2)) AS post_application_amount, CAST(0 AS decimal(18,2)) AS post_application_cost, CAST(0 AS decimal(18,2)) AS post_return_quantity, CAST(0 AS decimal(18,2)) AS post_return_amount, CAST(0 AS decimal(18,2)) AS post_return_cost, CAST(0 AS decimal(18,2)) AS post_refund_amount, CAST(0 AS decimal(18,2)) AS post_refund_cost, CAST(0 AS decimal(18,2)) AS post_platform_subsidy, CAST(0 AS decimal(18,2)) AS defective_quantity, CAST(NULL AS nvarchar(100)) AS refund_type, CAST(NULL AS nvarchar(100)) AS question_type, CAST(NULL AS nvarchar(100)) AS detail_return_type, CASE WHEN p.[is_gift] = 1 THEN N'是' ELSE N'否' END AS is_gift, CASE WHEN NULLIF(p.[src_combine_sku_id], '') IS NULL THEN N'非组合' ELSE N'组合装' END AS combine_type, p.[order_tags], CAST(NULL AS nvarchar(500)) AS after_sales_tags, s.[labels] AS item_tags, COALESCE(p.[send_warehouse], p.[wms_co_id]) AS shipping_warehouse_code, wh.[name] AS shipping_warehouse, wh.[remark1] AS operation_cloud_warehouse, s.[supplier_id] AS supplier_code, s.[supplier_name] AS supplier_name, p.[express_no], p.[express_company], p.[province], p.[city], p.[salesperson] FROM paged_rows p LEFT JOIN [rpt].[jst_sku] s ON s.[sku_id] = p.[product_code] LEFT JOIN [rpt].[jst_warehouseInfo] wh ON wh.[wms_co_id] = COALESCE(p.[send_warehouse], p.[wms_co_id]) ORDER BY p.[order_date] DESC, p.[internal_order_no] DESC, p.[oi_id] DESC"; $rows = $this->hydrateAfterSales($this->query()->select($sql, $bindings)); return $this->rowsResult($rows, fn (array $row): array => $this->normalizeAfterSalesRow($row)); } /** @return array{rows:array>} */ public function promotionExpenses(array $filters, int $page, int $limit): array { $date = $this->promotionDate($filters['date'] ?? null); if ($date === null) { return ['rows' => [], 'date' => null]; } $where = ['c.[stat_date] = CAST(? AS date)', 'ISNULL(c.[push_fee], 0) <> 0']; $bindings = [$date]; if (($filters['shop_name'] ?? '') !== '') { $where[] = "c.[shop_name] LIKE ? ESCAPE '\\'"; $bindings[] = $this->contains($filters['shop_name']); } if (($filters['sku_id'] ?? '') !== '') { $needle = $this->contains($filters['sku_id']); $where[] = "(c.[item_id] LIKE ? ESCAPE '\\' OR EXISTS (" . 'SELECT 1 FROM [rpt].[jst_skumap] filter_map ' . 'WHERE filter_map.[shop_i_id] = c.[item_id] ' . "AND (filter_map.[sku_id] LIKE ? ESCAPE '\\'" . " OR filter_map.[i_id] LIKE ? ESCAPE '\\'" . " OR filter_map.[shop_sku_id] LIKE ? ESCAPE '\\')))"; array_push($bindings, $needle, $needle, $needle, $needle); } $whereSql = implode(' AND ', $where); [$offset, $fetch] = $this->pageBounds($page, $limit); $sql = "WITH paged AS ( SELECT c.[shop_name], c.[item_id], CAST(c.[push_fee] AS decimal(18,2)) AS promotion_fee FROM [rpt].[ads_itemid_sales_cost_ratio_v2_di] c WHERE {$whereSql} ORDER BY c.[push_fee] DESC, c.[shop_name], c.[item_id] OFFSET {$offset} ROWS FETCH NEXT {$fetch} ROWS ONLY ) SELECT p.[shop_name], p.[item_id] AS shop_style_code, p.[promotion_fee] FROM paged p ORDER BY p.[promotion_fee] DESC, p.[shop_name], p.[item_id]"; $rows = $this->mapPromotionRows($this->query()->select($sql, $bindings)); $result = $this->rowsResult($rows, function (array $row): array { $row['promotion_fee'] = $this->decimal($row['promotion_fee'] ?? null) ?? 0.0; return $row; }); return array_merge($result, ['date' => $date]); } private function query(): UpstreamReadOnlyQuery { return $this->query ??= $this->queries->forSource(self::SOURCE); } private function normalizeAfterSalesRow(array $row): array { $link = $this->productLink( (string) ($row['shop_site'] ?? ''), (string) ($row['shop_name'] ?? ''), (string) ($row['shop_style_code'] ?? ''), ); unset($row['shop_site']); unset($row['unit_cost']); $row['product_link'] = $link; $row['offline_customer_code'] = null; $row['offline_customer_name'] = null; $row['unique_code'] = null; $row['registration_time'] = null; $row['confirmation_time'] = null; $row['warehouse_entry_time'] = null; $row['classification_order'] = null; $row['receiving_warehouse_code'] = null; $row['receiving_warehouse'] = null; foreach ($this->expenseFields() as $field) { $row[$field] = $this->decimal($row[$field] ?? null); } foreach ($this->amountFields() as $field) { if (array_key_exists($field, $row)) { $row[$field] = $this->decimal($row[$field]) ?? 0.0; } } return $row; } /** @param array> $rows */ private function hydrateAfterSales(array $rows): array { $orderIds = array_values(array_unique(array_filter(array_map( static fn (array $row): string => (string) ($row['internal_order_no'] ?? ''), $rows, )))); if ($orderIds === []) { return $rows; } $placeholders = implode(', ', array_fill(0, count($orderIds), '?')); $refunds = $this->query()->select( "SELECT a.[o_id] AS internal_order_no, CONVERT(nvarchar(100), a.[as_id]) AS after_sales_id, a.[outer_as_id] AS outside_after_sales_id, CONVERT(varchar(19), a.[as_date], 120) 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, TRY_CONVERT(decimal(18,2), ri.[defective_qty]) AS defective_quantity FROM [rpt].[jst_returnAndRefundInfo] a INNER JOIN [rpt].[jst_returnAndRefundItem] ri ON ri.[as_id] = a.[as_id] WHERE a.[o_id] IN ({$placeholders}) ORDER BY a.[as_date] DESC, a.[as_id] DESC", $orderIds, ); $byOrder = []; foreach ($refunds as $refund) { $byOrder[(string) ($refund['internal_order_no'] ?? '')][] = $refund; } foreach ($rows as &$row) { $matches = array_values(array_filter( $byOrder[(string) ($row['internal_order_no'] ?? '')] ?? [], static function (array $refund) use ($row): bool { $refundSubOrder = (string) ($refund['sub_order_no'] ?? ''); return $refundSubOrder !== '' ? $refundSubOrder === (string) ($row['sub_order_no'] ?? '') : (string) ($refund['product_code'] ?? '') === (string) ($row['product_code'] ?? ''); }, )); $first = $matches[0] ?? []; $row['after_sales_no'] = $first['outside_after_sales_id'] ?? $first['after_sales_id'] ?? null; $row['refund_type'] = $first['refund_type'] ?? null; $row['question_type'] = $first['question_type'] ?? null; $row['detail_return_type'] = $first['detail_return_type'] ?? null; $row['after_sales_tags'] = $first['after_sales_tags'] ?? null; $unitCost = (float) ($row['unit_cost'] ?? 0); $preReturnQuantity = 0.0; foreach ($matches as $refund) { $phase = empty($row['send_time']) || (string) ($refund['after_sales_time'] ?? '') < (string) $row['send_time'] ? 'pre' : 'post'; $applicationQuantity = (float) ($refund['application_quantity'] ?? 0); $returnQuantity = (float) ($refund['return_quantity'] ?? 0); $row[$phase . '_application_quantity'] += $applicationQuantity; $row[$phase . '_application_amount'] += (float) ($refund['application_amount'] ?? 0); $row[$phase . '_return_quantity'] += $returnQuantity; $row[$phase . '_return_amount'] += (float) ($refund['return_amount'] ?? 0); $row[$phase . '_refund_amount'] += (float) ($refund['refund_amount'] ?? 0); $row[$phase . '_platform_subsidy'] += (float) ($refund['platform_subsidy'] ?? 0); $row[$phase . '_application_cost'] += $applicationQuantity * $unitCost; $row[$phase . '_return_cost'] += $returnQuantity * $unitCost; $row[$phase . '_refund_cost'] += ($applicationQuantity - $returnQuantity) * $unitCost; $row['defective_quantity'] += (float) ($refund['defective_quantity'] ?? 0); if ($phase === 'pre') { $preReturnQuantity += $returnQuantity; } } $row['actual_shipped_quantity'] = (float) ($row['product_quantity'] ?? 0) - $preReturnQuantity; $row['actual_shipped_amount'] = (float) ($row['product_amount'] ?? 0) - (float) ($row['pre_return_amount'] ?? 0); $row['actual_shipped_cost'] = $row['actual_shipped_quantity'] * $unitCost; } unset($row); return $rows; } /** @param array> $rows */ private function mapPromotionRows(array $rows): array { $itemIds = array_values(array_unique(array_filter(array_map( static fn (array $row): string => (string) ($row['shop_style_code'] ?? ''), $rows, )))); if ($itemIds === []) { return $rows; } $placeholders = implode(', ', array_fill(0, count($itemIds), '?')); $mappings = $this->query()->select( "SELECT m.[shop_i_id] AS shop_style_code, m.[sku_id] AS product_code, m.[i_id] AS style_code, m.[shop_sku_id] AS shop_product_code, m.[properties_value] AS online_color_spec, shop.[shop_name], m.[modified] FROM [rpt].[jst_skumap] m LEFT JOIN [rpt].[jst_shopInfo] shop ON shop.[shop_id] = m.[shop_id] WHERE m.[shop_i_id] IN ({$placeholders}) ORDER BY m.[shop_i_id], m.[modified] DESC", $itemIds, ); $byItem = []; foreach ($mappings as $mapping) { $byItem[(string) ($mapping['shop_style_code'] ?? '')][] = $mapping; } foreach ($rows as &$row) { $candidates = $byItem[(string) ($row['shop_style_code'] ?? '')] ?? []; $mapping = collect($candidates)->firstWhere('shop_name', $row['shop_name'] ?? null) ?? ($candidates[0] ?? []); foreach (['product_code', 'style_code', 'shop_product_code', 'online_color_spec'] as $field) { $row[$field] = $mapping[$field] ?? null; } } unset($row); return $rows; } /** @return array */ private function amountFields(): array { return [ 'product_quantity', 'product_amount', 'product_cost', 'reversal_order_cost', 'merchant_discount', 'freight_income', 'payment_amount', 'platform_discount', 'reversal_order_amount', 'actual_shipped_amount', 'actual_shipped_quantity', 'actual_shipped_cost', 'pre_application_quantity', 'pre_application_amount', 'pre_application_cost', 'pre_return_quantity', 'pre_return_amount', 'pre_return_cost', 'pre_refund_amount', 'pre_refund_cost', 'pre_platform_subsidy', 'post_application_quantity', 'post_application_amount', 'post_application_cost', 'post_return_quantity', 'post_return_amount', 'post_return_cost', 'post_refund_amount', 'post_refund_cost', 'post_platform_subsidy', 'defective_quantity', ]; } /** @return array */ 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', ]; } /** @return array{0:int,1:int} */ private function pageBounds(int $page, int $limit): array { $page = max($page, 1); $limit = min(max($limit, 1), 100); $offset = ($page - 1) * $limit; return [$offset, $limit + 1]; } /** @return array{rows:array>} */ private function rowsResult(array $rows, callable $normalizer): array { foreach ($rows as &$row) { $row = $normalizer($row); } unset($row); return ['rows' => $rows]; } 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; } private function decimal(mixed $value): ?float { return $value === null || $value === '' ? null : round((float) $value, 2); } private function promotionDate(mixed $value): ?string { $date = trim((string) $value); if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $date) === 1) { return $date; } $row = $this->query()->first( 'SELECT TOP 1 CONVERT(varchar(10), [stat_date], 23) AS [latest_date] ' . 'FROM [rpt].[ads_itemid_sales_cost_ratio_v2_di] ORDER BY [stat_date] DESC' ); return isset($row['latest_date']) ? (string) $row['latest_date'] : null; } private function contains(mixed $value): string { return '%' . str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], (string) $value) . '%'; } }