From 761f30b611dcfdb52b9d799d120f63c95642524b Mon Sep 17 00:00:00 2001 From: renshen <1005457301@qq.com> Date: Fri, 30 Oct 2026 13:37:56 +0800 Subject: [PATCH 1/9] feat: add order profit directory --- app/Listeners/Menu/ShowInAdmin.php | 2 +- config/business_navigation.php | 7 +++++++ tests/Feature/Settings/NavigationTest.php | 10 ++++++++++ tests/Unit/View/BasicDataNavigationTest.php | 5 +++-- 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/app/Listeners/Menu/ShowInAdmin.php b/app/Listeners/Menu/ShowInAdmin.php index b1194183..051e3d04 100644 --- a/app/Listeners/Menu/ShowInAdmin.php +++ b/app/Listeners/Menu/ShowInAdmin.php @@ -35,7 +35,7 @@ class ShowInAdmin public static function groupBasicData(array $navigation): array { - $keys = ['order-data', 'refunds']; + $keys = ['order-data', 'order-profit', 'refunds']; $children = array_values(array_filter($navigation, static fn (array $section): bool => in_array($section['slug'], $keys, true))); if ($children === []) { return $navigation; diff --git a/config/business_navigation.php b/config/business_navigation.php index 5782f182..9084e3f0 100644 --- a/config/business_navigation.php +++ b/config/business_navigation.php @@ -654,6 +654,12 @@ $navigation = [ ['slug' => 'bill-offset', 'title' => '账单冲减'], ], ], + [ + 'slug' => 'order-profit', + 'title' => '订单利润', + 'icon' => 'trending_up', + 'pages' => [], + ], [ 'slug' => 'refunds', 'title' => '退款数据', @@ -1116,6 +1122,7 @@ $requestedSectionOrder = [ 'invoice-management', 'fund-management', 'order-data', + 'order-profit', 'refunds', 'billing-transactions', 'planting-data', diff --git a/tests/Feature/Settings/NavigationTest.php b/tests/Feature/Settings/NavigationTest.php index 1dcaae45..9be775cd 100644 --- a/tests/Feature/Settings/NavigationTest.php +++ b/tests/Feature/Settings/NavigationTest.php @@ -17,12 +17,22 @@ class NavigationTest extends FeatureTestCase ->assertSeeText('基础数据') ->assertSeeText('商品价格管理') ->assertSee('/workspace/order-data/raw-data', false) + ->assertSee('/workspace/order-profit', false) + ->assertSeeText('订单利润') ->assertSee('/workspace/refunds/non-jingxi-refund-data', false) ->assertSee('/workspace/product-inventory/normal-products', false) ->assertSee('/workspace/product-inventory/monthly-weighted-average-price', false) ->assertSee('/workspace/product-inventory/inventory-report', false); } + public function testOrderProfitDirectoryOpensBeforeDataIsConnected(): void + { + $this->loginAs() + ->get(route('business.pages.show', ['section' => 'order-profit'])) + ->assertOk() + ->assertSeeText('订单利润'); + } + public function testInventoryReportMenuPageIsAvailable(): void { $this->loginAs() diff --git a/tests/Unit/View/BasicDataNavigationTest.php b/tests/Unit/View/BasicDataNavigationTest.php index 162b25db..8bd2a8b9 100644 --- a/tests/Unit/View/BasicDataNavigationTest.php +++ b/tests/Unit/View/BasicDataNavigationTest.php @@ -9,14 +9,15 @@ require_once dirname(__DIR__, 3) . '/app/Listeners/Menu/ShowInAdmin.php'; class BasicDataNavigationTest extends TestCase { - public function test_group_moves_only_order_data_and_refunds_without_changing_pages(): void + public function test_group_moves_order_data_order_profit_and_refunds_without_changing_pages(): void { $navigation = require dirname(__DIR__, 3) . '/config/business_navigation.php'; $grouped = ShowInAdmin::groupBasicData($navigation); $slugs = array_column($grouped, 'slug'); $group = $grouped[array_search('basic-data', $slugs, true)]; - $this->assertSame(['order-data', 'refunds'], array_column($group['sections'], 'slug')); + $this->assertSame(['order-data', 'order-profit', 'refunds'], array_column($group['sections'], 'slug')); $this->assertNotContains('order-data', $slugs); + $this->assertNotContains('order-profit', $slugs); $this->assertNotContains('refunds', $slugs); foreach ($group['sections'] as $section) { $this->assertSame($navigation[array_search($section['slug'], array_column($navigation, 'slug'), true)], $section); -- 2.47.2 From 5676117b1410d062f7dc748db38b41e721bf2e49 Mon Sep 17 00:00:00 2001 From: renshen <1005457301@qq.com> Date: Fri, 30 Oct 2026 14:16:24 +0800 Subject: [PATCH 2/9] feat: add order profit data page --- app/Http/Controllers/Business/Pages.php | 1 + config/business_navigation.php | 8 +- .../Repositories/OrderProfitRepository.php | 413 ++++++++++++++++++ .../View/OrderProfitPageData.php | 146 +++++++ .../pages/order-data/order-profit.blade.php | 293 +++++++++++++ .../Feature/Business/OrderProfitPageTest.php | 81 ++++ tests/Feature/Settings/NavigationTest.php | 5 +- .../OrderProfitRepositoryTest.php | 168 +++++++ 8 files changed, 1113 insertions(+), 2 deletions(-) create mode 100644 modules/EcommerceFinance/Repositories/OrderProfitRepository.php create mode 100644 modules/EcommerceFinance/View/OrderProfitPageData.php create mode 100644 resources/views/business/pages/order-data/order-profit.blade.php create mode 100644 tests/Feature/Business/OrderProfitPageTest.php create mode 100644 tests/Unit/EcommerceFinance/OrderProfitRepositoryTest.php diff --git a/app/Http/Controllers/Business/Pages.php b/app/Http/Controllers/Business/Pages.php index bd0bc80f..604ec2a1 100644 --- a/app/Http/Controllers/Business/Pages.php +++ b/app/Http/Controllers/Business/Pages.php @@ -4706,6 +4706,7 @@ class Pages extends Controller 'return-pickup-management' => $this->contentForReturnPickupManagement(), 'withdrawal-management' => $this->contentForWithdrawalManagement(), 'systemization-todos' => app(\App\Http\Controllers\Business\SystemizationTodos::class)->page(), + 'order-profit-list' => app(\Modules\EcommerceFinance\View\OrderProfitPageData::class)->page(), 'todos' => $this->contentForTodos(), 'after-sales-return-refund' => $this->contentForAfterSalesReturnRefund(), 'bill-offset' => app(\Modules\EcommerceFinance\View\BillOffsetPageData::class)->page(), diff --git a/config/business_navigation.php b/config/business_navigation.php index 9084e3f0..6f42789d 100644 --- a/config/business_navigation.php +++ b/config/business_navigation.php @@ -658,7 +658,13 @@ $navigation = [ 'slug' => 'order-profit', 'title' => '订单利润', 'icon' => 'trending_up', - 'pages' => [], + 'pages' => [ + [ + 'slug' => 'order-profit-list', + 'title' => '订单利润', + 'show_in_section_menu' => false, + ], + ], ], [ 'slug' => 'refunds', diff --git a/modules/EcommerceFinance/Repositories/OrderProfitRepository.php b/modules/EcommerceFinance/Repositories/OrderProfitRepository.php new file mode 100644 index 00000000..403ac846 --- /dev/null +++ b/modules/EcommerceFinance/Repositories/OrderProfitRepository.php @@ -0,0 +1,413 @@ +readyCache ??= ($this->fields ?? app(GoodsProfitAfsRepository::class))->ready(); + } + + public function status(): array + { + try { + $state = $this->connection()->table('business_mirror_table_states') + ->where('dataset_key', GoodsProfitAfsRepository::MAIN_DATASET) + ->first(['initial_completed_at', 'last_succeeded_at', 'last_error_code']); + } catch (\Throwable) { + $state = null; + } + + return [ + 'ready' => $this->ready(), + 'initial_completed_at' => $state->initial_completed_at ?? null, + 'last_succeeded_at' => $state->last_succeeded_at ?? null, + 'last_error_code' => $state->last_error_code ?? null, + ]; + } + + /** + * @param array $filters + * @return array{rows:list>,has_more:bool} + */ + public function paginate(array $filters, int $page, int $limit): array + { + if (! $this->hasActiveFilters($filters)) { + return ['rows' => [], 'has_more' => false]; + } + + $columns = $this->columns(); + foreach ([ + 'shop_name', 'online_order_no', 'internal_order_no', 'order_time', + 'product_cost', 'refund_type', 'post_refund_amount', 'post_platform_subsidy', + 'pre_refund_amount', 'pre_platform_subsidy', 'classification_order', + ] as $field) { + if (! isset($columns[$field])) { + return ['rows' => [], 'has_more' => false]; + } + } + + $page = max($page, 1); + $limit = min(max($limit, 1), 100); + $orderValues = self::batchValues((string) ($filters['order_no'] ?? '')); + if ($orderValues !== []) { + $ids = $this->orderNumberIds($orderValues, $columns); + if ($ids === []) { + return ['rows' => [], 'has_more' => false]; + } + $query = $this->connection()->table(self::TABLE)->whereIn('_mirror_key', $ids); + $query = $this->filtered($query, array_replace($filters, ['order_no' => '']), $columns); + } else { + $query = $this->filtered($this->connection()->table(self::TABLE), $filters, $columns); + } + $shop = $this->quote($columns['shop_name']); + $online = $this->quote($columns['online_order_no']); + $internal = $this->quote($columns['internal_order_no']); + $orderTime = $this->quote($columns['order_time']); + $refundType = $this->quote($columns['refund_type']); + $categoryOrder = $this->quote($columns['classification_order']); + $cost = $this->sumExpression($columns, ['product_cost']); + $refund = $this->sumExpression($columns, ['pre_refund_amount', 'post_refund_amount']); + $platformSubsidy = $this->sumExpression($columns, ['pre_platform_subsidy', 'post_platform_subsidy']); + $quantity = $this->sumExpression($columns, ['product_quantity']); + $amount = $this->sumExpression($columns, ['product_amount']); + + $records = $query + ->selectRaw("{$shop} AS shop_name") + ->selectRaw("{$online} AS online_order_no") + ->selectRaw("{$internal} AS internal_order_no") + ->selectRaw("MAX({$orderTime}) AS order_time") + ->selectRaw("{$cost} AS cost_amount") + ->selectRaw("{$refund} AS refund_amount") + ->selectRaw("{$platformSubsidy} AS platform_subsidy") + ->selectRaw("{$quantity} AS product_quantity") + ->selectRaw("{$amount} AS product_amount") + ->selectRaw("MAX(NULLIF(TRIM({$refundType}), '')) AS refund_type") + ->selectRaw("MAX(NULLIF(TRIM({$categoryOrder}), '')) AS classification_order") + ->selectRaw('COUNT(*) AS source_row_count') + ->groupBy($columns['shop_name'], $columns['online_order_no'], $columns['internal_order_no']) + ->orderByRaw("MAX({$orderTime}) DESC") + ->orderByDesc($columns['internal_order_no']) + ->forPage($page, $limit + 1) + ->get(); + + $rows = $records->map(static fn (object $record): array => [ + 'shop_name' => trim((string) ($record->shop_name ?? '')), + 'online_order_no' => trim((string) ($record->online_order_no ?? '')), + 'internal_order_no' => trim((string) ($record->internal_order_no ?? '')), + 'order_time' => $record->order_time, + 'cost_amount' => round((float) ($record->cost_amount ?? 0), 2), + 'refund_amount' => round((float) ($record->refund_amount ?? 0), 2), + 'platform_subsidy' => round((float) ($record->platform_subsidy ?? 0), 2), + 'product_quantity' => round((float) ($record->product_quantity ?? 0), 2), + 'product_amount' => round((float) ($record->product_amount ?? 0), 2), + 'refund_type' => $record->refund_type, + 'classification_order' => $record->classification_order, + 'source_row_count' => (int) ($record->source_row_count ?? 0), + 'matched_package_fee' => null, + 'matched_freight_fee' => null, + 'matched_bill_fee' => null, + 'items' => [], + ])->all(); + + $hasMore = count($rows) > $limit; + $rows = array_slice($rows, 0, $limit); + $this->hydrateItems($rows, $columns); + + return ['rows' => $rows, 'has_more' => $hasMore]; + } + + /** @return list */ + public function shopNames(): array + { + $cacheKey = 'order-profit.shop-options:undefined8'; + $cached = Cache::get($cacheKey); + if (is_array($cached)) { + return $cached; + } + if (! $this->ready()) { + return []; + } + + $columns = $this->columns(); + $shopColumn = $columns['shop_name'] ?? null; + if ($shopColumn === null) { + return []; + } + + $names = $this->connection()->table(self::TABLE) + ->select($shopColumn) + ->whereNotNull($shopColumn) + ->where($shopColumn, '<>', '') + ->distinct() + ->orderBy($shopColumn) + ->pluck($shopColumn) + ->map(static fn (mixed $name): string => trim((string) $name)) + ->filter() + ->values() + ->all(); + Cache::put($cacheKey, $names, now()->addHours(24)); + + return $names; + } + + /** @return array */ + private function columns(): array + { + return ($this->fields ?? app(GoodsProfitAfsRepository::class))->fieldColumns(); + } + + /** + * @param list $orderValues + * @param array $columns + * @return list + */ + private function orderNumberIds(array $orderValues, array $columns): array + { + $ids = []; + $single = count($orderValues) === 1 ? $orderValues[0] : null; + foreach (['online_order_no', 'internal_order_no'] as $field) { + if (! isset($columns[$field])) { + continue; + } + if ($field === 'internal_order_no') { + $orderValues = array_values(array_filter( + $orderValues, + static fn (string $value): bool => mb_strlen($value) <= 11, + )); + if ($orderValues === []) { + continue; + } + } + + $query = $this->connection()->table(self::TABLE)->select('_mirror_key')->limit(10000); + if ($single !== null) { + $query->where($columns[$field], 'like', $single . '%'); + } else { + $query->whereIn($columns[$field], $orderValues); + } + foreach ($query->pluck('_mirror_key') as $id) { + $ids[(string) $id] = true; + } + } + + return array_keys($ids); + } + + /** + * @param array $filters + * @param array $columns + */ + private function filtered(Builder $query, array $filters, array $columns): Builder + { + $dateColumn = $this->queryDateColumn($columns); + $from = trim((string) ($filters['from'] ?? '')); + $to = trim((string) ($filters['to'] ?? '')); + + if ($dateColumn !== null && $from !== '') { + $query->where($dateColumn, '>=', $from . ' 00:00:00'); + } + if ($dateColumn !== null && $to !== '') { + $query->where($dateColumn, '<=', $to . ' 23:59:59'); + } + + $shops = self::batchValues((string) ($filters['shop_name'] ?? '')); + if ($shops !== [] && isset($columns['shop_name'])) { + $query->whereIn($columns['shop_name'], $shops); + } + + $orderValues = self::batchValues((string) ($filters['order_no'] ?? '')); + if ($orderValues !== []) { + $query->where(function (Builder $order) use ($orderValues, $columns): void { + foreach (['online_order_no', 'internal_order_no'] as $field) { + if (! isset($columns[$field])) { + continue; + } + $order->orWhereIn($columns[$field], $orderValues); + } + }); + } + + $skuValues = self::batchValues((string) ($filters['sku_id'] ?? '')); + if ($skuValues !== [] && isset($columns['product_code'])) { + $query->whereIn($columns['product_code'], $skuValues); + } + + return $query; + } + + /** + * @param list> $rows + * @param array $columns + */ + private function hydrateItems(array &$rows, array $columns): void + { + if ($rows === []) { + return; + } + + $itemFields = array_values(array_filter( + self::ITEM_FIELDS, + static fn (string $field): bool => isset($columns[$field]), + )); + if ($itemFields === []) { + return; + } + + $onlineValues = array_values(array_filter(array_column($rows, 'online_order_no'))); + $internalValues = array_values(array_filter(array_column($rows, 'internal_order_no'))); + if ($onlineValues === [] && $internalValues === []) { + return; + } + + $query = $this->connection()->table(self::TABLE); + $query->where(function (Builder $orders) use ($columns, $onlineValues, $internalValues): void { + if ($onlineValues !== [] && isset($columns['online_order_no'])) { + $orders->whereIn($columns['online_order_no'], $onlineValues); + } + if ($internalValues !== [] && isset($columns['internal_order_no'])) { + $method = $onlineValues === [] ? 'whereIn' : 'orWhereIn'; + $orders->{$method}($columns['internal_order_no'], $internalValues); + } + }); + + $select = []; + foreach ($itemFields as $field) { + $select[] = $columns[$field] . ' AS ' . $field; + } + $itemsByOrder = []; + foreach ($query->get($select) as $record) { + $item = []; + foreach ($itemFields as $field) { + $value = $record->{$field} ?? null; + $item[$field] = in_array($field, self::NUMERIC_FIELDS, true) + ? ($value === null || $value === '' ? null : (float) $value) + : $value; + } + $online = trim((string) ($item['online_order_no'] ?? '')); + $internal = trim((string) ($item['internal_order_no'] ?? '')); + $itemsByOrder[$online . "\0" . $internal][] = $item; + } + + foreach ($rows as &$row) { + $key = trim((string) $row['online_order_no']) . "\0" . trim((string) $row['internal_order_no']); + $row['items'] = $itemsByOrder[$key] ?? []; + } + unset($row); + } + + /** @param list $fields */ + private function sumExpression(array $columns, array $fields): string + { + $parts = []; + foreach ($fields as $field) { + if (isset($columns[$field])) { + $parts[] = 'COALESCE(CAST(NULLIF(TRIM(' . $this->quote($columns[$field]) + . "), '') AS DECIMAL(18,2)), 0)"; + } + } + + return $parts === [] ? '0' : 'SUM(' . implode(' + ', $parts) . ')'; + } + + private function queryDateColumn(array $columns): ?string + { + return Schema::connection($this->connectionName())->hasColumn( + self::TABLE, + GoodsProfitAfsLookupSchema::DATE_COLUMN, + ) + ? GoodsProfitAfsLookupSchema::DATE_COLUMN + : ($columns['order_time'] ?? null); + } + + private function connection(): ConnectionInterface + { + return DB::connection($this->connectionName()); + } + + private function connectionName(): string + { + return (string) config('upstream_mirrors.connection', 'business_mirror'); + } + + private function quote(string $column): string + { + if (preg_match('/^[A-Za-z0-9_]+$/', $column) !== 1) { + throw new \InvalidArgumentException('订单利润字段名不安全。'); + } + + return '`' . $column . '`'; + } + + /** @param array $filters */ + private function hasActiveFilters(array $filters): bool + { + foreach (['from', 'to', 'order_no', 'shop_name', 'sku_id'] as $key) { + if (trim((string) ($filters[$key] ?? '')) !== '') { + return true; + } + } + + return false; + } + + /** @return list */ + private static function batchValues(string $value): array + { + $parts = preg_split('/[\s,,;;]+/u', trim($value)) ?: []; + $values = []; + foreach ($parts as $part) { + $part = trim($part); + if ($part !== '') { + $values[$part] = true; + } + if (count($values) >= 100) { + break; + } + } + + return array_keys($values); + } +} diff --git a/modules/EcommerceFinance/View/OrderProfitPageData.php b/modules/EcommerceFinance/View/OrderProfitPageData.php new file mode 100644 index 00000000..01bc2d1c --- /dev/null +++ b/modules/EcommerceFinance/View/OrderProfitPageData.php @@ -0,0 +1,146 @@ + 'business.pages.order-data.order-profit'] + $this->data(); + } + + public function data(?array $input = null, ?int $page = null, ?int $limit = null): array + { + $filters = $this->filters($input); + $page = max($page ?? (int) request('page', 1), 1); + $limit = min(max($limit ?? (int) setting('default.list_limit', 25), 1), 100); + $rows = $this->paginator([], $filters, $page, $limit); + $error = null; + $status = $this->repository->status(); + $hasFilters = $this->hasFilters($filters); + + if ($hasFilters && ($status['ready'] ?? false)) { + try { + $result = $this->repository->paginate($filters, $page, $limit); + $rows = $this->paginator( + (array) ($result['rows'] ?? []), + $filters, + $page, + $limit, + (bool) ($result['has_more'] ?? false), + ); + } catch (Throwable $exception) { + report($exception); + $error = '订单利润数据暂时无法读取,请稍后重试。'; + } + } elseif ($hasFilters && ! ($status['ready'] ?? false)) { + $error = '订单利润共享镜像尚未完成首次同步。'; + } + + return [ + 'orderProfitRows' => $rows, + 'orderProfitFilters' => $filters, + 'orderProfitShopOptions' => $this->repository->shopNames(), + 'orderProfitStatus' => $status, + 'orderProfitError' => $error, + 'orderProfitHasFilters' => $hasFilters, + ]; + } + + /** @return array{from:string,to:string,order_no:string,shop_name:string,shop_names:array,sku_id:string} */ + private function filters(?array $input): array + { + $input ??= request()->query(); + $shops = []; + $rawShops = $input['shop_name'] ?? $input['shop_names'] ?? []; + foreach ((array) $rawShops as $shop) { + $shop = trim((string) $shop); + if ($shop !== '') { + $shops[$shop] = true; + } + if (count($shops) >= 50) { + break; + } + } + + return [ + 'from' => $this->date($input['from'] ?? null), + 'to' => $this->date($input['to'] ?? null), + 'order_no' => $this->batchText($input['order_no'] ?? null), + 'shop_name' => implode("\n", array_keys($shops)), + 'shop_names' => array_keys($shops), + 'sku_id' => $this->batchText($input['sku_id'] ?? null), + ]; + } + + private function date(mixed $value): string + { + $value = trim((string) $value); + if ($value === '') { + return ''; + } + + $timestamp = strtotime($value); + + return $timestamp === false ? '' : date('Y-m-d', $timestamp); + } + + private function batchText(mixed $value): string + { + $values = is_array($value) ? $value : preg_split('/[\r\n,,;;]+/u', (string) $value); + $items = []; + foreach ((array) $values as $item) { + $item = trim((string) $item); + if ($item !== '') { + $items[$item] = true; + } + if (count($items) >= 100) { + break; + } + } + + return implode("\n", array_keys($items)); + } + + private function hasFilters(array $filters): bool + { + return trim((string) $filters['order_no']) !== '' + || trim((string) $filters['shop_name']) !== '' + || trim((string) $filters['sku_id']) !== '' + || $filters['from'] !== '' + || $filters['to'] !== ''; + } + + private function paginator( + array $rows, + array $filters, + int $page, + int $limit, + ?bool $hasMore = null, + ): Paginator { + $paginator = new Paginator( + Collection::make($rows)->map(static fn (array $row): object => (object) $row), + $limit, + $page, + [ + 'path' => request()->url(), + 'query' => request()->except('page'), + ], + ); + + if ($hasMore !== null) { + $paginator->hasMorePagesWhen($hasMore); + } + + return $paginator; + } +} diff --git a/resources/views/business/pages/order-data/order-profit.blade.php b/resources/views/business/pages/order-data/order-profit.blade.php new file mode 100644 index 00000000..301c9b3a --- /dev/null +++ b/resources/views/business/pages/order-data/order-profit.blade.php @@ -0,0 +1,293 @@ +@php + $rows = $orderProfitRows ?? null; + $filters = $orderProfitFilters ?? []; + $status = $orderProfitStatus ?? []; + $hasFilters = (bool) ($orderProfitHasFilters ?? false); + $columns = [ + 'shop_name' => '店铺名称', + 'online_order_no' => '线上单号', + 'internal_order_no' => '内部单号', + 'product_quantity' => '数量', + 'product_amount' => '商品金额', + 'cost_amount' => '成本', + 'refund_amount' => '退款金额', + 'refund_type' => '退货类型', + 'platform_subsidy' => '退平台补贴', + 'matched_package_fee' => '包材费(自动匹配)', + 'matched_freight_fee' => '快递费(自动匹配)', + 'matched_bill_fee' => '账单费用(自动匹配)', + 'classification_order' => '分类单', + 'source_row_count' => '来源行数', + ]; + $numberFields = array_flip([ + 'product_quantity', 'product_amount', 'cost_amount', 'refund_amount', + 'platform_subsidy', 'matched_package_fee', 'matched_freight_fee', + 'matched_bill_fee', 'source_row_count', + ]); +@endphp + +
+ @include('business.pages.partials.upstream-data-styles') + + + +
+
+ + +
+
+ + +
+
+ + +
+ @include('business.pages.partials.order-data-shop-picker', [ + 'shopPickerOptions' => $orderProfitShopOptions ?? [], + 'shopPickerSelected' => $filters['shop_names'] ?? [], + 'shopPickerToggleId' => 'order-profit-shop', + ]) +
+ + +
+
+ + + + 重置 + + +
+
+ +
+ + 商品成本、退款、退货类型和退平台补贴来自聚水潭订单利润共享镜像;包材费、快递费、账单费用属于聚水潭页面自动匹配结果,当前源表未包含匹配结果,暂显示“—”。 +
+ + @if ($orderProfitError ?? null) + + @endif + +
+ @if (!empty($status)) +
+ 数据状态:{{ ($status['ready'] ?? false) ? '共享镜像已就绪' : '共享镜像同步中' }} + @if (!empty($status['last_succeeded_at'])) · 最近同步 {{ $status['last_succeeded_at'] }} @endif +
+ @endif +
+ + + + @foreach ($columns as $field => $header) + + @endforeach + + + + + @forelse ($rows ?? [] as $row) + @php + $detailId = 'order-profit-detail-' . $loop->index; + $items = collect(data_get($row, 'items', [])); + @endphp + + @foreach ($columns as $field => $header) + @php $value = data_get($row, $field); @endphp + + @endforeach + + + + + + @empty + + + + @endforelse + +
isset($numberFields[$field])])>{{ $header }}明细
isset($numberFields[$field])])> + @if ($value === null || $value === '') + - + @elseif (isset($numberFields[$field])) + {{ number_format((float) $value, 2) }} + @else + {{ $value }} + @endif + + +
+ {{ $hasFilters ? '暂无订单利润数据' : '请输入订单号、店铺或日期后查询' }} +
+
+
+ + 当前页 {{ $rows?->count() ?? 0 }} 条 + @if (!empty($status['ready'])) · 共享镜像 @endif + + +
+
+
+ + diff --git a/tests/Feature/Business/OrderProfitPageTest.php b/tests/Feature/Business/OrderProfitPageTest.php new file mode 100644 index 00000000..694384ae --- /dev/null +++ b/tests/Feature/Business/OrderProfitPageTest.php @@ -0,0 +1,81 @@ +shouldReceive('status')->once()->andReturn([ + 'ready' => true, + 'initial_completed_at' => '2026-10-01 00:00:00', + 'last_succeeded_at' => '2026-10-30 10:00:00', + 'last_error_code' => null, + ]); + $repository->shouldReceive('shopNames')->once()->andReturn(['唯品会MP-卓禾家具特卖旗舰店']); + $repository->shouldReceive('paginate')->once()->andReturn([ + 'has_more' => false, + 'rows' => [[ + 'shop_name' => '唯品会MP-卓禾家具特卖旗舰店', + 'online_order_no' => '26092142570188', + 'internal_order_no' => '9470588', + 'order_time' => '2026/9/21 9:40:36', + 'cost_amount' => 75.0, + 'refund_amount' => 20.0, + 'platform_subsidy' => 0.0, + 'product_quantity' => 1.0, + 'product_amount' => 259.0, + 'refund_type' => '仅退款', + 'classification_order' => '否', + 'source_row_count' => 2, + 'matched_package_fee' => null, + 'matched_freight_fee' => null, + 'matched_bill_fee' => null, + 'items' => [[ + 'after_sales_no' => '743208893', + 'product_code' => 'BJ018706N68V01', + 'style_code' => 'BJ0187', + 'shop_product_code' => 'SKU-1E73E04C8008B3AF', + 'product_name' => '款式一 60/40/53 胡桃色', + 'color_spec' => '胡桃色', + 'product_quantity' => 0, + 'product_amount' => 0, + 'product_cost' => 0, + 'refund_type' => '仅退款', + 'post_refund_amount' => 20, + 'post_refund_cost' => 0, + 'post_platform_subsidy' => 0, + 'combine_type' => '非组合', + 'shipping_warehouse' => '简尚-崔桥工厂仓', + 'express_no' => '76001654919378', + 'express_company' => '新北中通', + 'order_tags' => '发货后售后,店铺-唯品', + ]], + ]], + ]); + $this->app->instance(OrderProfitRepository::class, $repository); + + $this->loginAs()->get(route('business.pages.show', [ + 'section' => 'order-profit', + 'page' => 'order-profit-list', + 'order_no' => '26092142570188', + ])) + ->assertOk() + ->assertSeeText('订单利润') + ->assertSeeText('唯品会MP-卓禾家具特卖旗舰店') + ->assertSeeText('26092142570188') + ->assertSeeText('9470588') + ->assertSeeText('75.00') + ->assertSeeText('20.00') + ->assertSeeText('仅退款') + ->assertSeeText('包材费、快递费、账单费用属于聚水潭页面自动匹配结果') + ->assertSeeText('743208893') + ->assertSee('data-order-profit-detail', false) + ->assertSee('/workspace/order-profit', false); + } +} diff --git a/tests/Feature/Settings/NavigationTest.php b/tests/Feature/Settings/NavigationTest.php index 9be775cd..f9a79f00 100644 --- a/tests/Feature/Settings/NavigationTest.php +++ b/tests/Feature/Settings/NavigationTest.php @@ -28,7 +28,10 @@ class NavigationTest extends FeatureTestCase public function testOrderProfitDirectoryOpensBeforeDataIsConnected(): void { $this->loginAs() - ->get(route('business.pages.show', ['section' => 'order-profit'])) + ->get(route('business.pages.show', [ + 'section' => 'order-profit', + 'page' => 'order-profit-list', + ])) ->assertOk() ->assertSeeText('订单利润'); } diff --git a/tests/Unit/EcommerceFinance/OrderProfitRepositoryTest.php b/tests/Unit/EcommerceFinance/OrderProfitRepositoryTest.php new file mode 100644 index 00000000..f683f31f --- /dev/null +++ b/tests/Unit/EcommerceFinance/OrderProfitRepositoryTest.php @@ -0,0 +1,168 @@ + [ + 'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '', + ], + 'upstream_mirrors.connection' => 'business_mirror', + ]); + DB::purge('business_mirror'); + + $schema = Schema::connection('business_mirror'); + $schema->create(GoodsProfitAfsLookupSchema::TABLE, function (Blueprint $table): void { + $table->integer('id')->primary(); + $table->string('_mirror_key')->unique(); + foreach ([ + 'undefined1', 'undefined2', 'undefined3', 'undefined4', 'undefined7', + 'undefined8', 'undefined10', 'undefined11', 'undefined12', 'undefined13', + 'undefined18', 'undefined20', 'undefined21', 'undefined22', 'undefined43', + 'undefined45', 'undefined52', 'undefined53', 'undefined54', 'undefined56', + 'undefined59', 'undefined61', 'undefined66', 'undefined72', 'undefined73', + 'undefined151', + ] as $column) { + $table->string($column)->nullable(); + } + }); + $schema->create('ops_jst_goods_profit_kpi_items_afs_schemas', function (Blueprint $table): void { + $table->integer('id'); + $table->string('column'); + $table->string('name'); + }); + $schema->create('business_mirror_table_states', function (Blueprint $table): void { + $table->string('dataset_key'); + $table->dateTime('initial_completed_at')->nullable(); + $table->dateTime('last_succeeded_at')->nullable(); + $table->string('last_error_code')->nullable(); + }); + + $db = DB::connection('business_mirror'); + $db->table('business_mirror_table_states')->insert([ + 'dataset_key' => GoodsProfitAfsRepository::MAIN_DATASET, + 'initial_completed_at' => '2026-10-01 00:00:00', + 'last_succeeded_at' => '2026-10-30 10:00:00', + ]); + + $headers = [ + 'undefined1' => '内部订单号', + 'undefined2' => '线上订单号', + 'undefined3' => '子订单号', + 'undefined4' => '售后单号', + 'undefined7' => '订单时间', + 'undefined8' => '店铺名称', + 'undefined10' => '商品编码', + 'undefined11' => '款式编码', + 'undefined12' => '店铺商品编码', + 'undefined13' => '店铺款式编码', + 'undefined18' => '颜色及规格', + 'undefined20' => '商品数量', + 'undefined21' => '商品金额', + 'undefined22' => '商品成本', + 'undefined43' => '发货前退款金额', + 'undefined45' => '发货前退平台补贴', + 'undefined52' => '发货后退款金额', + 'undefined53' => '发货后退款成本', + 'undefined54' => '发货后退平台补贴', + 'undefined56' => '退款类型', + 'undefined59' => '分类单', + 'undefined61' => '组合装', + 'undefined66' => '发货仓库', + 'undefined72' => '快递单号', + 'undefined73' => '快递公司', + 'undefined151' => '商品名称', + ]; + foreach ($headers as $index => $name) { + $db->table('ops_jst_goods_profit_kpi_items_afs_schemas')->insert([ + 'id' => (int) substr($index, 9), + 'column' => $index, + 'name' => $name, + ]); + } + + $base = [ + 'undefined2' => '26092142570188', + 'undefined3' => '26092142570188_SKU-1E73E04C8008B3AF', + 'undefined7' => '2026/9/21 9:40:36', + 'undefined8' => '唯品会MP-卓禾家具特卖旗舰店', + 'undefined10' => 'BJ018706N68V01', + 'undefined11' => 'BJ0187', + 'undefined12' => 'SKU-1E73E04C8008B3AF', + 'undefined13' => 'SPU-1E73E04C800003AF', + 'undefined18' => '胡桃色', + 'undefined59' => '否', + 'undefined61' => '非组合', + 'undefined66' => '简尚-崔桥工厂仓', + 'undefined72' => '76001654919378', + 'undefined73' => '新北中通', + 'undefined151' => '款式一 60/40/53 胡桃色', + ]; + $db->table(GoodsProfitAfsLookupSchema::TABLE)->insert(array_replace($base, [ + 'id' => 1, + '_mirror_key' => 'row-1', + 'undefined1' => '9470588', + 'undefined4' => '', + 'undefined20' => '1', + 'undefined21' => '259', + 'undefined22' => '75', + 'undefined43' => '0', + 'undefined45' => '0', + 'undefined52' => '0', + 'undefined53' => '0', + 'undefined54' => '0', + 'undefined56' => '', + ])); + $db->table(GoodsProfitAfsLookupSchema::TABLE)->insert(array_replace($base, [ + 'id' => 2, + '_mirror_key' => 'row-2', + 'undefined1' => '9470588', + 'undefined4' => '743208893', + 'undefined20' => '0', + 'undefined21' => '0', + 'undefined22' => '0', + 'undefined43' => '0', + 'undefined45' => '0', + 'undefined52' => '20', + 'undefined53' => '0', + 'undefined54' => '0', + 'undefined56' => '仅退款', + ])); + } + + public function test_order_rows_are_aggregated_from_item_and_refund_lines(): void + { + $repository = app(OrderProfitRepository::class); + $result = $repository->paginate([ + 'from' => '', + 'to' => '', + 'order_no' => '26092142570188', + 'shop_name' => '', + 'sku_id' => '', + ], 1, 25); + + $this->assertCount(1, $result['rows']); + $row = $result['rows'][0]; + $this->assertSame('唯品会MP-卓禾家具特卖旗舰店', $row['shop_name']); + $this->assertSame('26092142570188', $row['online_order_no']); + $this->assertSame('9470588', $row['internal_order_no']); + $this->assertEquals(75.0, $row['cost_amount']); + $this->assertEquals(20.0, $row['refund_amount']); + $this->assertSame('仅退款', $row['refund_type']); + $this->assertCount(2, $row['items']); + $this->assertSame(['唯品会MP-卓禾家具特卖旗舰店'], $repository->shopNames()); + } +} -- 2.47.2 From 2448e4e4c7b8c4ea25d1b910f93e509870c0eafe Mon Sep 17 00:00:00 2001 From: renshen <1005457301@qq.com> Date: Fri, 30 Oct 2026 14:34:38 +0800 Subject: [PATCH 3/9] feat: load order profit page from shared mirror --- .../Repositories/OrderProfitRepository.php | 6 ++- .../View/OrderProfitPageData.php | 21 +++++++-- .../pages/order-data/order-profit.blade.php | 2 +- .../Feature/Business/OrderProfitPageTest.php | 43 +++++++++++++++++++ .../OrderProfitRepositoryTest.php | 4 +- 5 files changed, 68 insertions(+), 8 deletions(-) diff --git a/modules/EcommerceFinance/Repositories/OrderProfitRepository.php b/modules/EcommerceFinance/Repositories/OrderProfitRepository.php index 403ac846..e872b107 100644 --- a/modules/EcommerceFinance/Repositories/OrderProfitRepository.php +++ b/modules/EcommerceFinance/Repositories/OrderProfitRepository.php @@ -98,7 +98,11 @@ class OrderProfitRepository return ['rows' => [], 'has_more' => false]; } $query = $this->connection()->table(self::TABLE)->whereIn('_mirror_key', $ids); - $query = $this->filtered($query, array_replace($filters, ['order_no' => '']), $columns); + $query = $this->filtered( + $query, + array_replace($filters, ['order_no' => '', 'from' => '', 'to' => '']), + $columns, + ); } else { $query = $this->filtered($this->connection()->table(self::TABLE), $filters, $columns); } diff --git a/modules/EcommerceFinance/View/OrderProfitPageData.php b/modules/EcommerceFinance/View/OrderProfitPageData.php index 01bc2d1c..0e2e212b 100644 --- a/modules/EcommerceFinance/View/OrderProfitPageData.php +++ b/modules/EcommerceFinance/View/OrderProfitPageData.php @@ -72,13 +72,26 @@ class OrderProfitPageData } } + $orderNo = $this->batchText($input['order_no'] ?? null); + $sku = $this->batchText($input['sku_id'] ?? null); + $hasOtherFilters = $orderNo !== '' || $sku !== '' || $shops !== []; + $datesWereSubmitted = array_key_exists('from', $input) || array_key_exists('to', $input); + $reset = ! empty($input['reset']); + $from = $this->date($input['from'] ?? null); + $to = $this->date($input['to'] ?? null); + + if (! $datesWereSubmitted && ! $reset && ! $hasOtherFilters) { + $from = now()->subDays(6)->toDateString(); + $to = now()->toDateString(); + } + return [ - 'from' => $this->date($input['from'] ?? null), - 'to' => $this->date($input['to'] ?? null), - 'order_no' => $this->batchText($input['order_no'] ?? null), + 'from' => $from, + 'to' => $to, + 'order_no' => $orderNo, 'shop_name' => implode("\n", array_keys($shops)), 'shop_names' => array_keys($shops), - 'sku_id' => $this->batchText($input['sku_id'] ?? null), + 'sku_id' => $sku, ]; } diff --git a/resources/views/business/pages/order-data/order-profit.blade.php b/resources/views/business/pages/order-data/order-profit.blade.php index 301c9b3a..97daaac4 100644 --- a/resources/views/business/pages/order-data/order-profit.blade.php +++ b/resources/views/business/pages/order-data/order-profit.blade.php @@ -94,7 +94,7 @@ 查询 - + 重置 diff --git a/tests/Feature/Business/OrderProfitPageTest.php b/tests/Feature/Business/OrderProfitPageTest.php index 694384ae..7e0d85e0 100644 --- a/tests/Feature/Business/OrderProfitPageTest.php +++ b/tests/Feature/Business/OrderProfitPageTest.php @@ -8,6 +8,49 @@ use Tests\Feature\FeatureTestCase; class OrderProfitPageTest extends FeatureTestCase { + public function test_order_profit_page_loads_recent_rows_without_filters(): void + { + $repository = Mockery::mock(OrderProfitRepository::class); + $repository->shouldReceive('status')->once()->andReturn([ + 'ready' => true, + 'initial_completed_at' => '2026-10-01 00:00:00', + 'last_succeeded_at' => '2026-10-30 10:00:00', + 'last_error_code' => null, + ]); + $repository->shouldReceive('shopNames')->once()->andReturn([]); + $repository->shouldReceive('paginate')->once() + ->with( + Mockery::on(static fn (array $filters): bool => $filters['from'] === now()->subDays(6)->toDateString() + && $filters['to'] === now()->toDateString()), + 1, + 25, + ) + ->andReturn([ + 'has_more' => false, + 'rows' => [[ + 'shop_name' => '最近订单店铺', + 'online_order_no' => 'RECENT-ORDER-1', + 'internal_order_no' => '9001', + 'cost_amount' => 12.0, + 'refund_amount' => 0.0, + 'platform_subsidy' => 0.0, + 'product_quantity' => 1.0, + 'product_amount' => 20.0, + 'items' => [], + ]], + ]); + $this->app->instance(OrderProfitRepository::class, $repository); + + $this->loginAs()->get(route('business.pages.show', [ + 'section' => 'order-profit', + 'page' => 'order-profit-list', + ])) + ->assertOk() + ->assertSeeText('RECENT-ORDER-1') + ->assertSeeText('最近订单店铺') + ->assertSeeText('12.00'); + } + public function test_order_profit_page_renders_aggregated_order_and_source_details(): void { $repository = Mockery::mock(OrderProfitRepository::class); diff --git a/tests/Unit/EcommerceFinance/OrderProfitRepositoryTest.php b/tests/Unit/EcommerceFinance/OrderProfitRepositoryTest.php index f683f31f..c4508e07 100644 --- a/tests/Unit/EcommerceFinance/OrderProfitRepositoryTest.php +++ b/tests/Unit/EcommerceFinance/OrderProfitRepositoryTest.php @@ -147,8 +147,8 @@ class OrderProfitRepositoryTest extends TestCase { $repository = app(OrderProfitRepository::class); $result = $repository->paginate([ - 'from' => '', - 'to' => '', + 'from' => '2026-10-01', + 'to' => '2026-10-31', 'order_no' => '26092142570188', 'shop_name' => '', 'sku_id' => '', -- 2.47.2 From c28ccc510662a0532204ece8a04b6c06e5e34247 Mon Sep 17 00:00:00 2001 From: renshen <1005457301@qq.com> Date: Fri, 30 Oct 2026 14:51:20 +0800 Subject: [PATCH 4/9] fix: group order profit by online order and add column defaults --- .../Repositories/OrderProfitRepository.php | 12 ++++++++---- .../pages/order-data/order-profit.blade.php | 15 ++++++++++++--- tests/Feature/Business/OrderProfitPageTest.php | 2 ++ 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/modules/EcommerceFinance/Repositories/OrderProfitRepository.php b/modules/EcommerceFinance/Repositories/OrderProfitRepository.php index e872b107..0e71fa99 100644 --- a/modules/EcommerceFinance/Repositories/OrderProfitRepository.php +++ b/modules/EcommerceFinance/Repositories/OrderProfitRepository.php @@ -117,11 +117,13 @@ class OrderProfitRepository $platformSubsidy = $this->sumExpression($columns, ['pre_platform_subsidy', 'post_platform_subsidy']); $quantity = $this->sumExpression($columns, ['product_quantity']); $amount = $this->sumExpression($columns, ['product_amount']); + $orderKey = "COALESCE(NULLIF(TRIM({$online}), ''), TRIM({$internal}))"; $records = $query ->selectRaw("{$shop} AS shop_name") ->selectRaw("{$online} AS online_order_no") - ->selectRaw("{$internal} AS internal_order_no") + ->selectRaw("MAX({$internal}) AS internal_order_no") + ->selectRaw("{$orderKey} AS order_key") ->selectRaw("MAX({$orderTime}) AS order_time") ->selectRaw("{$cost} AS cost_amount") ->selectRaw("{$refund} AS refund_amount") @@ -131,7 +133,7 @@ class OrderProfitRepository ->selectRaw("MAX(NULLIF(TRIM({$refundType}), '')) AS refund_type") ->selectRaw("MAX(NULLIF(TRIM({$categoryOrder}), '')) AS classification_order") ->selectRaw('COUNT(*) AS source_row_count') - ->groupBy($columns['shop_name'], $columns['online_order_no'], $columns['internal_order_no']) + ->groupByRaw("{$shop}, {$orderKey}") ->orderByRaw("MAX({$orderTime}) DESC") ->orderByDesc($columns['internal_order_no']) ->forPage($page, $limit + 1) @@ -141,6 +143,7 @@ class OrderProfitRepository 'shop_name' => trim((string) ($record->shop_name ?? '')), 'online_order_no' => trim((string) ($record->online_order_no ?? '')), 'internal_order_no' => trim((string) ($record->internal_order_no ?? '')), + 'order_key' => trim((string) ($record->order_key ?? '')), 'order_time' => $record->order_time, 'cost_amount' => round((float) ($record->cost_amount ?? 0), 2), 'refund_amount' => round((float) ($record->refund_amount ?? 0), 2), @@ -332,11 +335,12 @@ class OrderProfitRepository } $online = trim((string) ($item['online_order_no'] ?? '')); $internal = trim((string) ($item['internal_order_no'] ?? '')); - $itemsByOrder[$online . "\0" . $internal][] = $item; + $orderKey = $online !== '' ? $online : $internal; + $itemsByOrder[$orderKey][] = $item; } foreach ($rows as &$row) { - $key = trim((string) $row['online_order_no']) . "\0" . trim((string) $row['internal_order_no']); + $key = trim((string) ($row['order_key'] ?? '')); $row['items'] = $itemsByOrder[$key] ?? []; } unset($row); diff --git a/resources/views/business/pages/order-data/order-profit.blade.php b/resources/views/business/pages/order-data/order-profit.blade.php index 97daaac4..6f97eaaa 100644 --- a/resources/views/business/pages/order-data/order-profit.blade.php +++ b/resources/views/business/pages/order-data/order-profit.blade.php @@ -129,11 +129,16 @@ @endif
- +
@foreach ($columns as $field => $header) - + @endforeach @@ -149,7 +154,11 @@ @php $value = data_get($row, $field); @endphp
isset($numberFields[$field])])>{{ $header }} isset($numberFields[$field]), + 'is-column-hidden' => in_array($field, ['product_quantity', 'product_amount', 'source_row_count'], true), + ])>{{ $header }}明细
isset($numberFields[$field])])> + data-export-field="{{ $field }}" + @class([ + 'is-number' => isset($numberFields[$field]), + 'is-column-hidden' => in_array($field, ['product_quantity', 'product_amount', 'source_row_count'], true), + ])> @if ($value === null || $value === '') - @elseif (isset($numberFields[$field])) diff --git a/tests/Feature/Business/OrderProfitPageTest.php b/tests/Feature/Business/OrderProfitPageTest.php index 7e0d85e0..1585597d 100644 --- a/tests/Feature/Business/OrderProfitPageTest.php +++ b/tests/Feature/Business/OrderProfitPageTest.php @@ -119,6 +119,8 @@ class OrderProfitPageTest extends FeatureTestCase ->assertSeeText('包材费、快递费、账单费用属于聚水潭页面自动匹配结果') ->assertSeeText('743208893') ->assertSee('data-order-profit-detail', false) + ->assertSee('data-finance-table-name="订单利润"', false) + ->assertSee('is-column-hidden', false) ->assertSee('/workspace/order-profit', false); } } -- 2.47.2 From 3e6a423c7e05f5c0c79b02933e0d86ac46653e12 Mon Sep 17 00:00:00 2001 From: renshen <1005457301@qq.com> Date: Fri, 30 Oct 2026 14:56:56 +0800 Subject: [PATCH 5/9] feat: expose complete order profit refund fields --- .../Repositories/OrderProfitRepository.php | 107 ++++++++++++------ .../pages/order-data/order-profit.blade.php | 44 ++++++- .../OrderProfitRepositoryTest.php | 27 ++++- 3 files changed, 137 insertions(+), 41 deletions(-) diff --git a/modules/EcommerceFinance/Repositories/OrderProfitRepository.php b/modules/EcommerceFinance/Repositories/OrderProfitRepository.php index 0e71fa99..943f8e02 100644 --- a/modules/EcommerceFinance/Repositories/OrderProfitRepository.php +++ b/modules/EcommerceFinance/Repositories/OrderProfitRepository.php @@ -23,8 +23,48 @@ class OrderProfitRepository private const NUMERIC_FIELDS = [ 'product_quantity', 'product_amount', 'product_cost', 'actual_shipped_quantity', 'actual_shipped_amount', 'actual_shipped_cost', + 'merchant_discount', 'freight_income', 'payment_amount', 'platform_discount', + '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', + ]; + + private const SUMMARY_NUMERIC_FIELDS = [ + 'cost_amount' => ['product_cost'], + 'product_quantity' => ['product_quantity'], + 'product_amount' => ['product_amount'], + 'actual_shipped_quantity' => ['actual_shipped_quantity'], + 'actual_shipped_amount' => ['actual_shipped_amount'], + 'actual_shipped_cost' => ['actual_shipped_cost'], + 'merchant_discount' => ['merchant_discount'], + 'freight_income' => ['freight_income'], + 'payment_amount' => ['payment_amount'], + 'platform_discount' => ['platform_discount'], + 'refund_amount' => ['pre_refund_amount', 'post_refund_amount'], + 'platform_subsidy' => ['pre_platform_subsidy', 'post_platform_subsidy'], + 'pre_platform_subsidy' => ['pre_platform_subsidy'], + 'pre_application_quantity' => ['pre_application_quantity'], + 'pre_application_amount' => ['pre_application_amount'], + 'pre_application_cost' => ['pre_application_cost'], + 'pre_return_quantity' => ['pre_return_quantity'], + 'pre_return_amount' => ['pre_return_amount'], + 'pre_return_cost' => ['pre_return_cost'], + 'pre_refund_amount' => ['pre_refund_amount'], + 'pre_refund_cost' => ['pre_refund_cost'], + 'post_platform_subsidy' => ['post_platform_subsidy'], + 'post_application_quantity' => ['post_application_quantity'], + 'post_application_amount' => ['post_application_amount'], + 'post_application_cost' => ['post_application_cost'], + 'post_return_quantity' => ['post_return_quantity'], + 'post_return_amount' => ['post_return_amount'], + 'post_return_cost' => ['post_return_cost'], + 'post_refund_amount' => ['post_refund_amount'], + 'post_refund_cost' => ['post_refund_cost'], + 'defective_quantity' => ['defective_quantity'], ]; private const ITEM_FIELDS = [ @@ -33,8 +73,13 @@ class OrderProfitRepository 'shop_product_code', 'shop_style_code', 'product_name', 'color_spec', 'product_quantity', 'product_amount', 'product_cost', 'actual_shipped_quantity', 'actual_shipped_amount', 'actual_shipped_cost', - 'refund_type', 'pre_refund_amount', 'pre_refund_cost', 'pre_platform_subsidy', - 'post_refund_amount', 'post_refund_cost', 'post_platform_subsidy', + 'merchant_discount', 'freight_income', 'payment_amount', 'platform_discount', + 'refund_type', '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', 'classification_order', 'combine_type', 'shipping_warehouse', 'express_no', 'express_company', 'order_tags', 'after_sales_tags', 'item_tags', ]; @@ -112,52 +157,48 @@ class OrderProfitRepository $orderTime = $this->quote($columns['order_time']); $refundType = $this->quote($columns['refund_type']); $categoryOrder = $this->quote($columns['classification_order']); - $cost = $this->sumExpression($columns, ['product_cost']); - $refund = $this->sumExpression($columns, ['pre_refund_amount', 'post_refund_amount']); - $platformSubsidy = $this->sumExpression($columns, ['pre_platform_subsidy', 'post_platform_subsidy']); - $quantity = $this->sumExpression($columns, ['product_quantity']); - $amount = $this->sumExpression($columns, ['product_amount']); $orderKey = "COALESCE(NULLIF(TRIM({$online}), ''), TRIM({$internal}))"; - $records = $query + $query ->selectRaw("{$shop} AS shop_name") ->selectRaw("{$online} AS online_order_no") ->selectRaw("MAX({$internal}) AS internal_order_no") ->selectRaw("{$orderKey} AS order_key") ->selectRaw("MAX({$orderTime}) AS order_time") - ->selectRaw("{$cost} AS cost_amount") - ->selectRaw("{$refund} AS refund_amount") - ->selectRaw("{$platformSubsidy} AS platform_subsidy") - ->selectRaw("{$quantity} AS product_quantity") - ->selectRaw("{$amount} AS product_amount") ->selectRaw("MAX(NULLIF(TRIM({$refundType}), '')) AS refund_type") ->selectRaw("MAX(NULLIF(TRIM({$categoryOrder}), '')) AS classification_order") - ->selectRaw('COUNT(*) AS source_row_count') + ->selectRaw('COUNT(*) AS source_row_count'); + foreach (self::SUMMARY_NUMERIC_FIELDS as $alias => $fields) { + $query->selectRaw($this->sumExpression($columns, $fields) . ' AS ' . $alias); + } + $records = $query ->groupByRaw("{$shop}, {$orderKey}") ->orderByRaw("MAX({$orderTime}) DESC") ->orderByDesc($columns['internal_order_no']) ->forPage($page, $limit + 1) ->get(); - $rows = $records->map(static fn (object $record): array => [ - 'shop_name' => trim((string) ($record->shop_name ?? '')), - 'online_order_no' => trim((string) ($record->online_order_no ?? '')), - 'internal_order_no' => trim((string) ($record->internal_order_no ?? '')), - 'order_key' => trim((string) ($record->order_key ?? '')), - 'order_time' => $record->order_time, - 'cost_amount' => round((float) ($record->cost_amount ?? 0), 2), - 'refund_amount' => round((float) ($record->refund_amount ?? 0), 2), - 'platform_subsidy' => round((float) ($record->platform_subsidy ?? 0), 2), - 'product_quantity' => round((float) ($record->product_quantity ?? 0), 2), - 'product_amount' => round((float) ($record->product_amount ?? 0), 2), - 'refund_type' => $record->refund_type, - 'classification_order' => $record->classification_order, - 'source_row_count' => (int) ($record->source_row_count ?? 0), - 'matched_package_fee' => null, - 'matched_freight_fee' => null, - 'matched_bill_fee' => null, - 'items' => [], - ])->all(); + $rows = $records->map(static function (object $record): array { + $row = [ + 'shop_name' => trim((string) ($record->shop_name ?? '')), + 'online_order_no' => trim((string) ($record->online_order_no ?? '')), + 'internal_order_no' => trim((string) ($record->internal_order_no ?? '')), + 'order_key' => trim((string) ($record->order_key ?? '')), + 'order_time' => $record->order_time, + 'refund_type' => $record->refund_type, + 'classification_order' => $record->classification_order, + 'source_row_count' => (int) ($record->source_row_count ?? 0), + 'matched_package_fee' => null, + 'matched_freight_fee' => null, + 'matched_bill_fee' => null, + 'items' => [], + ]; + foreach (array_keys(self::SUMMARY_NUMERIC_FIELDS) as $field) { + $row[$field] = round((float) ($record->{$field} ?? 0), 2); + } + + return $row; + })->all(); $hasMore = count($rows) > $limit; $rows = array_slice($rows, 0, $limit); diff --git a/resources/views/business/pages/order-data/order-profit.blade.php b/resources/views/business/pages/order-data/order-profit.blade.php index 6f97eaaa..21048024 100644 --- a/resources/views/business/pages/order-data/order-profit.blade.php +++ b/resources/views/business/pages/order-data/order-profit.blade.php @@ -7,12 +7,39 @@ 'shop_name' => '店铺名称', 'online_order_no' => '线上单号', 'internal_order_no' => '内部单号', + 'order_time' => '订单时间', 'product_quantity' => '数量', 'product_amount' => '商品金额', 'cost_amount' => '成本', + 'actual_shipped_quantity' => '实发数量', + 'actual_shipped_amount' => '实发金额', + 'actual_shipped_cost' => '实发成本', + 'merchant_discount' => '商家优惠', + 'freight_income' => '运费收入', + 'payment_amount' => '付款金额', + 'platform_discount' => '其中:平台优惠', 'refund_amount' => '退款金额', 'refund_type' => '退货类型', 'platform_subsidy' => '退平台补贴', + 'pre_platform_subsidy' => '发货前-退平台补贴', + 'pre_application_quantity' => '发货前-申请数量', + 'pre_application_amount' => '发货前-申请金额', + 'pre_application_cost' => '发货前-申请成本', + 'pre_return_quantity' => '发货前-退回数量', + 'pre_return_amount' => '发货前-退回金额', + 'pre_return_cost' => '发货前-退回成本', + 'pre_refund_amount' => '发货前-退款金额', + 'pre_refund_cost' => '发货前-退款成本', + 'post_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' => '发货后-退款成本', + 'defective_quantity' => '次品数量', 'matched_package_fee' => '包材费(自动匹配)', 'matched_freight_fee' => '快递费(自动匹配)', 'matched_bill_fee' => '账单费用(自动匹配)', @@ -20,8 +47,15 @@ 'source_row_count' => '来源行数', ]; $numberFields = array_flip([ - 'product_quantity', 'product_amount', 'cost_amount', 'refund_amount', - 'platform_subsidy', 'matched_package_fee', 'matched_freight_fee', + 'product_quantity', 'product_amount', 'cost_amount', + 'actual_shipped_quantity', 'actual_shipped_amount', 'actual_shipped_cost', + 'merchant_discount', 'freight_income', 'payment_amount', 'platform_discount', + 'refund_amount', 'platform_subsidy', + 'pre_platform_subsidy', 'pre_application_quantity', 'pre_application_amount', 'pre_application_cost', + 'pre_return_quantity', 'pre_return_amount', 'pre_return_cost', 'pre_refund_amount', 'pre_refund_cost', + 'post_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', + 'defective_quantity', 'matched_package_fee', 'matched_freight_fee', 'matched_bill_fee', 'source_row_count', ]); @endphp @@ -30,7 +64,7 @@ @include('business.pages.partials.upstream-data-styles')