diff --git a/database/migrations/2026_10_22_150000_index_goods_profit_afs_order_time.php b/database/migrations/2026_10_22_150000_index_goods_profit_afs_order_time.php index 6aa8deed..10a2e1d8 100644 --- a/database/migrations/2026_10_22_150000_index_goods_profit_afs_order_time.php +++ b/database/migrations/2026_10_22_150000_index_goods_profit_afs_order_time.php @@ -25,7 +25,10 @@ return new class extends Migration if (! $schema->hasTable(GoodsProfitAfsLookupSchema::TABLE)) return; $changes = []; - foreach (array_keys(GoodsProfitAfsLookupSchema::INDEXES) as $index) { + foreach ([ + 'idx_afs_order_time_id', + 'idx_afs_shop_order_time_id', + ] as $index) { if ($schema->hasIndex(GoodsProfitAfsLookupSchema::TABLE, $index)) { $changes[] = 'DROP INDEX `' . $index . '`'; } diff --git a/database/migrations/2026_11_07_160000_index_goods_profit_afs_pay_send_time.php b/database/migrations/2026_11_07_160000_index_goods_profit_afs_pay_send_time.php new file mode 100644 index 00000000..89266e40 --- /dev/null +++ b/database/migrations/2026_11_07_160000_index_goods_profit_afs_pay_send_time.php @@ -0,0 +1,63 @@ +ensure( + DB::connection((string) config('upstream_mirrors.connection', 'business_mirror')), + ); + } + + public function down(): void + { + if (config('system_database.mode', 'bootstrap') === 'bootstrap') { + return; + } + + $db = DB::connection((string) config('upstream_mirrors.connection', 'business_mirror')); + if (! in_array($db->getDriverName(), ['mysql', 'mariadb'], true)) { + return; + } + + $schema = Schema::connection($db->getName()); + if (! $schema->hasTable(GoodsProfitAfsLookupSchema::TABLE)) { + return; + } + + $changes = []; + foreach ([ + 'idx_afs_pay_time_id', + 'idx_afs_shop_pay_time_id', + 'idx_afs_send_time_id', + 'idx_afs_shop_send_time_id', + ] as $index) { + if ($schema->hasIndex(GoodsProfitAfsLookupSchema::TABLE, $index)) { + $changes[] = 'DROP INDEX `' . $index . '`'; + } + } + if ($changes !== []) { + $db->statement('ALTER TABLE `' . GoodsProfitAfsLookupSchema::TABLE . '` ' + . implode(', ', $changes) . ', ALGORITHM=INPLACE, LOCK=NONE'); + } + + foreach ([ + GoodsProfitAfsLookupSchema::PAY_DATE_COLUMN, + GoodsProfitAfsLookupSchema::SEND_DATE_COLUMN, + ] as $column) { + if ($schema->hasColumn(GoodsProfitAfsLookupSchema::TABLE, $column)) { + $db->statement('ALTER TABLE `' . GoodsProfitAfsLookupSchema::TABLE . '` DROP COLUMN `' + . $column . '`, ALGORITHM=INPLACE, LOCK=NONE'); + } + } + } +}; diff --git a/modules/EcommerceFinance/Repositories/GoodsProfitAfsRepository.php b/modules/EcommerceFinance/Repositories/GoodsProfitAfsRepository.php index a9230a71..09d134ff 100644 --- a/modules/EcommerceFinance/Repositories/GoodsProfitAfsRepository.php +++ b/modules/EcommerceFinance/Repositories/GoodsProfitAfsRepository.php @@ -697,9 +697,9 @@ class GoodsProfitAfsRepository // until the matching additive lookup has been installed. if ($column === 'undefined7') { $this->hasDateLookup ??= Schema::connection($this->connectionName()) - ->hasColumn(self::MAIN_TABLE, GoodsProfitAfsLookupSchema::DATE_COLUMN); + ->hasColumn(self::MAIN_TABLE, GoodsProfitAfsLookupSchema::ORDER_DATE_COLUMN); if ($this->hasDateLookup) { - return '`' . GoodsProfitAfsLookupSchema::DATE_COLUMN . '`'; + return '`' . GoodsProfitAfsLookupSchema::ORDER_DATE_COLUMN . '`'; } } diff --git a/modules/EcommerceFinance/Repositories/OrderProfitRepository.php b/modules/EcommerceFinance/Repositories/OrderProfitRepository.php index 943f8e02..6bce53db 100644 --- a/modules/EcommerceFinance/Repositories/OrderProfitRepository.php +++ b/modules/EcommerceFinance/Repositories/OrderProfitRepository.php @@ -69,7 +69,7 @@ class OrderProfitRepository private const ITEM_FIELDS = [ 'internal_order_no', 'online_order_no', 'sub_order_no', 'after_sales_no', - 'order_time', 'shop_name', 'product_code', 'style_code', + 'pay_time', 'send_time', 'order_time', 'shop_name', 'product_code', 'style_code', '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', @@ -133,10 +133,17 @@ class OrderProfitRepository return ['rows' => [], 'has_more' => false]; } } + $dateField = $this->dateField($filters); + if ($this->hasDateRange($filters) + && $this->queryDateColumn($columns, $dateField) === null) { + return ['rows' => [], 'has_more' => false]; + } $page = max($page, 1); $limit = min(max($limit, 1), 100); $orderValues = self::batchValues((string) ($filters['order_no'] ?? '')); + $hasMore = null; + $selectedGroups = null; if ($orderValues !== []) { $ids = $this->orderNumberIds($orderValues, $columns); if ($ids === []) { @@ -149,7 +156,20 @@ class OrderProfitRepository $columns, ); } else { + $filtered = $this->filtered($this->connection()->table(self::TABLE), $filters, $columns); + $groups = $this->pageOrderGroups($filtered, $columns, $dateField, $page, $limit); + if ($groups === []) { + return ['rows' => [], 'has_more' => false]; + } + + $hasMore = count($groups) > $limit; + $groups = array_slice($groups, 0, $limit); $query = $this->filtered($this->connection()->table(self::TABLE), $filters, $columns); + $this->constrainToOrderGroups($query, $groups, $columns); + $selectedGroups = []; + foreach ($groups as $group) { + $selectedGroups[$group['shop_name'] . "\0" . $group['order_key']] = true; + } } $shop = $this->quote($columns['shop_name']); $online = $this->quote($columns['online_order_no']); @@ -158,6 +178,9 @@ class OrderProfitRepository $refundType = $this->quote($columns['refund_type']); $categoryOrder = $this->quote($columns['classification_order']); $orderKey = "COALESCE(NULLIF(TRIM({$online}), ''), TRIM({$internal}))"; + $sortDate = $this->quote( + $this->queryDateColumn($columns, $dateField) ?? $columns['order_time'], + ); $query ->selectRaw("{$shop} AS shop_name") @@ -173,10 +196,18 @@ class OrderProfitRepository } $records = $query ->groupByRaw("{$shop}, {$orderKey}") - ->orderByRaw("MAX({$orderTime}) DESC") - ->orderByDesc($columns['internal_order_no']) - ->forPage($page, $limit + 1) - ->get(); + ->orderByRaw("MAX({$sortDate}) DESC") + ->orderByRaw("MAX({$internal}) DESC"); + if ($selectedGroups === null) { + $records = $records->forPage($page, $limit + 1)->get(); + } else { + $records = $records->get()->filter(static function (object $record) use ($selectedGroups): bool { + return isset($selectedGroups[ + trim((string) ($record->shop_name ?? '')) . "\0" + . trim((string) ($record->order_key ?? '')) + ]); + })->values(); + } $rows = $records->map(static function (object $record): array { $row = [ @@ -200,7 +231,9 @@ class OrderProfitRepository return $row; })->all(); - $hasMore = count($rows) > $limit; + if ($hasMore === null) { + $hasMore = count($rows) > $limit; + } $rows = array_slice($rows, 0, $limit); $this->hydrateItems($rows, $columns); @@ -284,13 +317,85 @@ class OrderProfitRepository return array_keys($ids); } + /** + * 先按日期/筛选条件只取当前页的订单键,再对少量订单做宽表汇总。 + * 这样近 7 天未指定店铺时不会对整段宽表逐行汇总后再丢弃分页外数据。 + * + * @param array $columns + * @return list + */ + private function pageOrderGroups( + Builder $query, + array $columns, + string $dateField, + int $page, + int $pageSize, + ): array { + $shop = $this->quote($columns['shop_name']); + $online = $this->quote($columns['online_order_no']); + $internal = $this->quote($columns['internal_order_no']); + $orderKey = "COALESCE(NULLIF(TRIM({$online}), ''), TRIM({$internal}))"; + $sortDate = $this->quote( + $this->queryDateColumn($columns, $dateField) ?? $columns['order_time'], + ); + + return $query + ->selectRaw("{$shop} AS shop_name") + ->selectRaw("{$online} AS online_order_no") + ->selectRaw("MAX({$internal}) AS internal_order_no") + ->selectRaw("{$orderKey} AS order_key") + ->groupByRaw("{$shop}, {$orderKey}") + ->orderByRaw("MAX({$sortDate}) DESC") + ->orderByRaw("MAX({$internal}) DESC") + ->offset(($page - 1) * $pageSize) + ->limit($pageSize + 1) + ->get() + ->map(static fn (object $row): array => [ + 'shop_name' => trim((string) ($row->shop_name ?? '')), + 'online_order_no' => trim((string) ($row->online_order_no ?? '')), + 'internal_order_no' => trim((string) ($row->internal_order_no ?? '')), + 'order_key' => trim((string) ($row->order_key ?? '')), + ]) + ->all(); + } + + /** + * @param list $groups + * @param array $columns + */ + private function constrainToOrderGroups(Builder $query, array $groups, array $columns): void + { + $onlineOrders = []; + $internalOrders = []; + $shops = []; + foreach ($groups as $group) { + $shops[$group['shop_name']] = true; + if ($group['online_order_no'] !== '') { + $onlineOrders[$group['online_order_no']] = true; + } elseif ($group['internal_order_no'] !== '') { + $internalOrders[$group['internal_order_no']] = true; + } + } + + $query->where(function (Builder $orders) use ($columns, $onlineOrders, $internalOrders): void { + if ($onlineOrders !== []) { + $orders->whereIn($columns['online_order_no'], array_keys($onlineOrders)); + } + if ($internalOrders !== []) { + $method = $onlineOrders === [] ? 'whereIn' : 'orWhereIn'; + $orders->{$method}($columns['internal_order_no'], array_keys($internalOrders)); + } + }); + $query->whereIn($columns['shop_name'], array_keys($shops)); + } + /** * @param array $filters * @param array $columns */ private function filtered(Builder $query, array $filters, array $columns): Builder { - $dateColumn = $this->queryDateColumn($columns); + $dateColumn = $this->queryDateColumn($columns, $this->dateField($filters)); $from = trim((string) ($filters['from'] ?? '')); $to = trim((string) ($filters['to'] ?? '')); @@ -401,14 +506,19 @@ class OrderProfitRepository return $parts === [] ? '0' : 'SUM(' . implode(' + ', $parts) . ')'; } - private function queryDateColumn(array $columns): ?string + private function queryDateColumn(array $columns, string $dateField): ?string { - return Schema::connection($this->connectionName())->hasColumn( - self::TABLE, - GoodsProfitAfsLookupSchema::DATE_COLUMN, - ) - ? GoodsProfitAfsLookupSchema::DATE_COLUMN - : ($columns['order_time'] ?? null); + $definition = GoodsProfitAfsLookupSchema::DATE_COLUMNS[$dateField] ?? null; + if ($definition === null) { + return null; + } + + if (($columns[$dateField] ?? null) === $definition['source'] + && Schema::connection($this->connectionName())->hasColumn(self::TABLE, $definition['lookup'])) { + return $definition['lookup']; + } + + return $columns[$dateField] ?? null; } private function connection(): ConnectionInterface @@ -442,6 +552,23 @@ class OrderProfitRepository return false; } + /** @param array $filters */ + private function dateField(array $filters): string + { + $dateField = trim((string) ($filters['date_field'] ?? 'pay_time')); + + return isset(GoodsProfitAfsLookupSchema::DATE_COLUMNS[$dateField]) + ? $dateField + : 'pay_time'; + } + + /** @param array $filters */ + private function hasDateRange(array $filters): bool + { + return trim((string) ($filters['from'] ?? '')) !== '' + || trim((string) ($filters['to'] ?? '')) !== ''; + } + /** @return list */ private static function batchValues(string $value): array { diff --git a/modules/EcommerceFinance/Services/GoodsProfitAfsLookupSchema.php b/modules/EcommerceFinance/Services/GoodsProfitAfsLookupSchema.php index aa6a8366..12d126ee 100644 --- a/modules/EcommerceFinance/Services/GoodsProfitAfsLookupSchema.php +++ b/modules/EcommerceFinance/Services/GoodsProfitAfsLookupSchema.php @@ -10,11 +10,38 @@ class GoodsProfitAfsLookupSchema { public const TABLE = 'ops_jst_goods_profit_kpi_items_afs'; - public const DATE_COLUMN = '_mirror_afs_order_time'; + public const ORDER_DATE_COLUMN = '_mirror_afs_order_time'; + + public const PAY_DATE_COLUMN = '_mirror_afs_pay_time'; + + public const SEND_DATE_COLUMN = '_mirror_afs_send_time'; + + /** 页面时间字段 => 上游原列 / 只读查询用的派生日期列。 */ + public const DATE_COLUMNS = [ + 'order_time' => [ + 'source' => 'undefined7', + 'lookup' => self::ORDER_DATE_COLUMN, + ], + 'pay_time' => [ + 'source' => 'undefined5', + 'lookup' => self::PAY_DATE_COLUMN, + ], + 'send_time' => [ + 'source' => 'undefined6', + 'lookup' => self::SEND_DATE_COLUMN, + ], + ]; + + /** @deprecated Use ORDER_DATE_COLUMN. */ + public const DATE_COLUMN = self::ORDER_DATE_COLUMN; public const INDEXES = [ - 'idx_afs_order_time_id' => ['_mirror_afs_order_time', 'id'], - 'idx_afs_shop_order_time_id' => ['undefined8', '_mirror_afs_order_time', 'id'], + 'idx_afs_order_time_id' => [self::ORDER_DATE_COLUMN, 'id'], + 'idx_afs_shop_order_time_id' => ['undefined8', self::ORDER_DATE_COLUMN, 'id'], + 'idx_afs_pay_time_id' => [self::PAY_DATE_COLUMN, 'id'], + 'idx_afs_shop_pay_time_id' => ['undefined8', self::PAY_DATE_COLUMN, 'id'], + 'idx_afs_send_time_id' => [self::SEND_DATE_COLUMN, 'id'], + 'idx_afs_shop_send_time_id' => ['undefined8', self::SEND_DATE_COLUMN, 'id'], ]; public function ensure(ConnectionInterface $db): void @@ -25,21 +52,29 @@ class GoodsProfitAfsLookupSchema $schema = Schema::connection($db->getName()); if (! $schema->hasTable(self::TABLE) - || ! $schema->hasColumns(self::TABLE, ['id', 'undefined7', 'undefined8'])) { + || ! $schema->hasColumn(self::TABLE, 'id')) { return; } $statements = []; - if (! $schema->hasColumn(self::TABLE, self::DATE_COLUMN)) { - $expression = $this->dateExpression(); - // MariaDB requires the virtual column and its indexes in separate + foreach (self::DATE_COLUMNS as $definition) { + $source = $definition['source']; + $lookup = $definition['lookup']; + if (! $schema->hasColumn(self::TABLE, $source) + || $schema->hasColumn(self::TABLE, $lookup)) { + continue; + } + + // MariaDB requires virtual columns and their indexes in separate // ALTER statements to allow concurrent writes with LOCK=NONE. - $statements[] = 'ADD COLUMN `' . self::DATE_COLUMN - . '` VARCHAR(19) CHARACTER SET ascii COLLATE ascii_bin GENERATED ALWAYS AS (' . $expression . ') VIRTUAL'; + $statements[] = 'ADD COLUMN `' . $lookup + . '` VARCHAR(19) CHARACTER SET ascii COLLATE ascii_bin GENERATED ALWAYS AS (' + . $this->dateExpression($source) . ') VIRTUAL'; } $indexes = []; foreach (self::INDEXES as $name => $columns) { - if (! $schema->hasIndex(self::TABLE, $name)) { + if (! $schema->hasIndex(self::TABLE, $name) + && $schema->hasColumns(self::TABLE, $columns)) { $indexes[] = 'ADD INDEX `' . $name . '` (`' . implode('`, `', $columns) . '`)'; } } @@ -59,9 +94,20 @@ class GoodsProfitAfsLookupSchema } } - public function dateExpression(): string + public function dateExpression(string $column = 'undefined7'): string { - $raw = "REPLACE(TRIM(`undefined7`), '/', '-')"; + $source = null; + foreach (self::DATE_COLUMNS as $definition) { + if ($definition['source'] === $column) { + $source = $column; + break; + } + } + if ($source === null) { + throw new \InvalidArgumentException('订单利润日期字段不安全。'); + } + + $raw = "REPLACE(TRIM(`{$source}`), '/', '-')"; $pattern = '^[0-9]{1,4}-(0?[1-9]|1[0-2])-(0?[1-9]|[12][0-9]|3[01])' . '([[:space:]]+([01]?[0-9]|2[0-3])(:[0-5]?[0-9]){0,2}([.][0-9]+)?)?$'; diff --git a/modules/EcommerceFinance/View/OrderProfitPageData.php b/modules/EcommerceFinance/View/OrderProfitPageData.php index 0e2e212b..daf5a0f2 100644 --- a/modules/EcommerceFinance/View/OrderProfitPageData.php +++ b/modules/EcommerceFinance/View/OrderProfitPageData.php @@ -9,6 +9,12 @@ use Throwable; class OrderProfitPageData { + private const DATE_FIELDS = [ + 'pay_time' => '支付时间', + 'send_time' => '发货时间', + 'order_time' => '订单时间', + ]; + public function __construct(private readonly OrderProfitRepository $repository) { } @@ -56,7 +62,7 @@ class OrderProfitPageData ]; } - /** @return array{from:string,to:string,order_no:string,shop_name:string,shop_names:array,sku_id:string} */ + /** @return array{date_field:string,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(); @@ -79,6 +85,10 @@ class OrderProfitPageData $reset = ! empty($input['reset']); $from = $this->date($input['from'] ?? null); $to = $this->date($input['to'] ?? null); + $dateField = trim((string) ($input['date_field'] ?? 'pay_time')); + if (! isset(self::DATE_FIELDS[$dateField])) { + $dateField = 'pay_time'; + } if (! $datesWereSubmitted && ! $reset && ! $hasOtherFilters) { $from = now()->subDays(6)->toDateString(); @@ -86,6 +96,7 @@ class OrderProfitPageData } return [ + 'date_field' => $dateField, 'from' => $from, 'to' => $to, 'order_no' => $orderNo, 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 33978cb4..02c0e131 100644 --- a/resources/views/business/pages/order-data/order-profit.blade.php +++ b/resources/views/business/pages/order-data/order-profit.blade.php @@ -3,6 +3,12 @@ $filters = $orderProfitFilters ?? []; $status = $orderProfitStatus ?? []; $hasFilters = (bool) ($orderProfitHasFilters ?? false); + $dateField = $filters['date_field'] ?? 'pay_time'; + $dateFieldLabels = [ + 'pay_time' => '支付时间', + 'send_time' => '发货时间', + 'order_time' => '订单时间', + ]; $columns = [ 'shop_name' => '店铺名称', 'online_order_no' => '线上单号', @@ -115,11 +121,19 @@
- + + +
+
+
- +
@@ -159,7 +173,7 @@
- 商品成本、退款、退货类型和退平台补贴来自聚水潭订单利润共享镜像;包材费、快递费、账单费用属于聚水潭页面自动匹配结果,当前源表未包含匹配结果,暂显示“—”。 + 商品成本、退款、退货类型和退平台补贴来自聚水潭订单利润共享镜像;包材费、快递费、账单费用是聚胜算订单利润页面的自动匹配结果,当前商品利润明细镜像不含该结果,不能从现有字段推算,未取得页面结果时显示“—”。
@if ($orderProfitError ?? null) diff --git a/tests/Feature/Business/OrderProfitPageTest.php b/tests/Feature/Business/OrderProfitPageTest.php index 1f312016..3b4c2297 100644 --- a/tests/Feature/Business/OrderProfitPageTest.php +++ b/tests/Feature/Business/OrderProfitPageTest.php @@ -21,7 +21,8 @@ class OrderProfitPageTest extends FeatureTestCase $repository->shouldReceive('paginate')->once() ->with( Mockery::on(static fn (array $filters): bool => $filters['from'] === now()->subDays(6)->toDateString() - && $filters['to'] === now()->toDateString()), + && $filters['to'] === now()->toDateString() + && $filters['date_field'] === 'pay_time'), 1, 25, ) @@ -61,7 +62,15 @@ class OrderProfitPageTest extends FeatureTestCase 'last_error_code' => null, ]); $repository->shouldReceive('shopNames')->once()->andReturn(['唯品会MP-卓禾家具特卖旗舰店']); - $repository->shouldReceive('paginate')->once()->andReturn([ + $repository->shouldReceive('paginate')->once() + ->with( + Mockery::on(static fn (array $filters): bool => + $filters['date_field'] === 'send_time' + && $filters['order_no'] === '26092142570188'), + 1, + 25, + ) + ->andReturn([ 'has_more' => false, 'rows' => [[ 'shop_name' => '唯品会MP-卓禾家具特卖旗舰店', @@ -107,16 +116,18 @@ class OrderProfitPageTest extends FeatureTestCase 'section' => 'order-profit', 'page' => 'order-profit-list', 'order_no' => '26092142570188', + 'date_field' => 'send_time', ])) ->assertOk() ->assertSeeText('订单利润') + ->assertSeeTextInOrder(['时间类型', '支付时间', '发货时间', '订单时间', '开始日期', '结束日期']) ->assertSeeText('唯品会MP-卓禾家具特卖旗舰店') ->assertSeeText('26092142570188') ->assertSeeText('9470588') ->assertSeeText('75.00') ->assertSeeText('20.00') ->assertSeeText('仅退款') - ->assertSeeText('包材费、快递费、账单费用属于聚水潭页面自动匹配结果') + ->assertSeeText('包材费、快递费、账单费用是聚胜算订单利润页面的自动匹配结果') ->assertSeeText('743208893') ->assertSeeTextInOrder(['内部单号', '明细', '订单时间']) ->assertSee('data-finance-fixed="true"', false) diff --git a/tests/Integration/GoodsProfitAfsLookupTest.php b/tests/Integration/GoodsProfitAfsLookupTest.php index 5ae1dc80..463a9f69 100644 --- a/tests/Integration/GoodsProfitAfsLookupTest.php +++ b/tests/Integration/GoodsProfitAfsLookupTest.php @@ -29,7 +29,10 @@ class GoodsProfitAfsLookupTest extends TestCase $schema->dropIfExists(GoodsProfitAfsLookupSchema::TABLE); $schema->create(GoodsProfitAfsLookupSchema::TABLE, function (Blueprint $table): void { $table->char('_mirror_key', 64)->primary(); - $table->integer('id'); $table->string('undefined7', 50)->nullable(); + $table->integer('id'); + $table->string('undefined5', 50)->nullable(); + $table->string('undefined6', 50)->nullable(); + $table->string('undefined7', 50)->nullable(); $table->string('undefined8', 50)->nullable(); $table->string('amount', 50)->nullable(); }); try { @@ -40,14 +43,20 @@ class GoodsProfitAfsLookupTest extends TestCase foreach ($rawDates as $id => $raw) { $db->table(GoodsProfitAfsLookupSchema::TABLE)->insert([ '_mirror_key' => hash('sha256', (string) $id), 'id' => $id, 'undefined7' => $raw, + 'undefined5' => $raw, 'undefined6' => $raw, 'undefined8' => 'fixture shop', 'amount' => '-31.20', ]); } $before = $db->table(GoodsProfitAfsLookupSchema::TABLE)->orderBy('id')->get()->toJson(); $migration = require base_path('database/migrations/2026_10_22_150000_index_goods_profit_afs_order_time.php'); + $paySendMigration = require base_path('database/migrations/2026_11_07_160000_index_goods_profit_afs_pay_send_time.php'); $migration->up(); + $paySendMigration->up(); $migration->up(); + $paySendMigration->up(); $this->assertTrue($schema->hasIndex(GoodsProfitAfsLookupSchema::TABLE, 'idx_afs_order_time_id')); + $this->assertTrue($schema->hasIndex(GoodsProfitAfsLookupSchema::TABLE, 'idx_afs_pay_time_id')); + $this->assertTrue($schema->hasIndex(GoodsProfitAfsLookupSchema::TABLE, 'idx_afs_send_time_id')); $rows = $db->select('SELECT id, `_mirror_afs_order_time` AS indexed_date, ' . "CAST(STR_TO_DATE(REPLACE(`undefined7`, '/', '-'), '%Y-%m-%d %H:%i:%s') AS CHAR(19)) AS original_date " . 'FROM `' . GoodsProfitAfsLookupSchema::TABLE . '` ORDER BY id'); @@ -59,6 +68,7 @@ class GoodsProfitAfsLookupTest extends TestCase $db->table(GoodsProfitAfsLookupSchema::TABLE)->where('id', 0)->update(['undefined7' => 'bad timestamp']); $this->assertNull($db->table(GoodsProfitAfsLookupSchema::TABLE)->where('id', 0)->value('_mirror_afs_order_time')); $db->table(GoodsProfitAfsLookupSchema::TABLE)->where('id', 0)->update(['undefined7' => $rawDates[0]]); + $paySendMigration->down(); $migration->down(); $this->assertSame($before, $db->table(GoodsProfitAfsLookupSchema::TABLE)->orderBy('id')->get()->toJson()); @@ -69,7 +79,13 @@ class GoodsProfitAfsLookupTest extends TestCase 'source_key' => 'fixture', 'schema' => 'rpt', 'object' => 'afs', 'target' => GoodsProfitAfsLookupSchema::TABLE, 'keys' => ['id'], 'watermark' => null, ]]]); - foreach (['id' => 'int', 'undefined7' => 'nvarchar', 'undefined8' => 'nvarchar'] as $column => $type) { + foreach ([ + 'id' => 'int', + 'undefined5' => 'nvarchar', + 'undefined6' => 'nvarchar', + 'undefined7' => 'nvarchar', + 'undefined8' => 'nvarchar', + ] as $column => $type) { DB::table('upstream_schema_columns')->insert([ 'source_key' => 'fixture', 'schema_name' => 'rpt', 'object_name' => 'afs', 'object_type' => 'table', 'column_name' => $column, 'ordinal_position' => $column === 'id' ? 1 : (int) substr($column, 9), @@ -81,6 +97,8 @@ class GoodsProfitAfsLookupTest extends TestCase app(BusinessMirrorSchemaManager::class)->ensure($dataset); app(BusinessMirrorSchemaManager::class)->ensure($dataset); $this->assertTrue($schema->hasIndex(GoodsProfitAfsLookupSchema::TABLE, 'idx_afs_shop_order_time_id')); + $this->assertTrue($schema->hasIndex(GoodsProfitAfsLookupSchema::TABLE, 'idx_afs_shop_pay_time_id')); + $this->assertTrue($schema->hasIndex(GoodsProfitAfsLookupSchema::TABLE, 'idx_afs_shop_send_time_id')); } finally { $schema->dropIfExists(GoodsProfitAfsLookupSchema::TABLE); DB::purge('afs_lookup_fixture'); diff --git a/tests/Unit/EcommerceFinance/OrderProfitRepositoryTest.php b/tests/Unit/EcommerceFinance/OrderProfitRepositoryTest.php index 78b86e6b..37af6d8d 100644 --- a/tests/Unit/EcommerceFinance/OrderProfitRepositoryTest.php +++ b/tests/Unit/EcommerceFinance/OrderProfitRepositoryTest.php @@ -29,7 +29,8 @@ class OrderProfitRepositoryTest extends TestCase $table->integer('id')->primary(); $table->string('_mirror_key')->unique(); foreach ([ - 'undefined1', 'undefined2', 'undefined3', 'undefined4', 'undefined7', + 'undefined1', 'undefined2', 'undefined3', 'undefined4', 'undefined5', + 'undefined6', 'undefined7', 'undefined8', 'undefined10', 'undefined11', 'undefined12', 'undefined13', 'undefined18', 'undefined20', 'undefined21', 'undefined22', 'undefined43', 'undefined45', 'undefined46', 'undefined47', 'undefined48', 'undefined49', @@ -39,6 +40,13 @@ class OrderProfitRepositoryTest extends TestCase ] as $column) { $table->string($column)->nullable(); } + foreach ([ + GoodsProfitAfsLookupSchema::ORDER_DATE_COLUMN, + GoodsProfitAfsLookupSchema::PAY_DATE_COLUMN, + GoodsProfitAfsLookupSchema::SEND_DATE_COLUMN, + ] as $column) { + $table->string($column, 19)->nullable(); + } }); $schema->create('ops_jst_goods_profit_kpi_items_afs_schemas', function (Blueprint $table): void { $table->integer('id'); @@ -64,6 +72,8 @@ class OrderProfitRepositoryTest extends TestCase 'undefined2' => '线上订单号', 'undefined3' => '子订单号', 'undefined4' => '售后单号', + 'undefined5' => '支付时间', + 'undefined6' => '发货时间', 'undefined7' => '订单时间', 'undefined8' => '店铺名称', 'undefined10' => '商品编码', @@ -104,7 +114,12 @@ class OrderProfitRepositoryTest extends TestCase $base = [ 'undefined2' => '26092142570188', 'undefined3' => '26092142570188_SKU-1E73E04C8008B3AF', + 'undefined5' => '2026/9/21 9:41:00', + 'undefined6' => '2026/9/22 10:00:00', 'undefined7' => '2026/9/21 9:40:36', + '_mirror_afs_order_time' => '2026-09-21 09:40:36', + '_mirror_afs_pay_time' => '2026-09-21 09:41:00', + '_mirror_afs_send_time' => '2026-09-22 10:00:00', 'undefined8' => '唯品会MP-卓禾家具特卖旗舰店', 'undefined10' => 'BJ018706N68V01', 'undefined11' => 'BJ0187', @@ -166,8 +181,9 @@ class OrderProfitRepositoryTest extends TestCase { $repository = app(OrderProfitRepository::class); $result = $repository->paginate([ - 'from' => '2026-10-01', - 'to' => '2026-10-31', + 'date_field' => 'pay_time', + 'from' => '2026-09-01', + 'to' => '2026-09-30', 'order_no' => '26092142570188', 'shop_name' => '', 'sku_id' => '', @@ -184,6 +200,76 @@ class OrderProfitRepositoryTest extends TestCase $this->assertEquals(0.0, $row['post_return_amount']); $this->assertSame('仅退款', $row['refund_type']); $this->assertCount(2, $row['items']); + $this->assertSame('2026/9/21 9:41:00', $row['items'][0]['pay_time']); + $this->assertSame('2026/9/22 10:00:00', $row['items'][0]['send_time']); $this->assertSame(['唯品会MP-卓禾家具特卖旗舰店'], $repository->shopNames()); } + + public function test_send_time_filter_uses_its_own_indexed_date_column(): void + { + $repository = app(OrderProfitRepository::class); + + $inside = $repository->paginate([ + 'date_field' => 'send_time', + 'from' => '2026-09-22', + 'to' => '2026-09-22', + 'order_no' => '', + 'shop_name' => '', + 'sku_id' => '', + ], 1, 25); + $outside = $repository->paginate([ + 'date_field' => 'send_time', + 'from' => '2026-09-23', + 'to' => '2026-09-23', + 'order_no' => '', + 'shop_name' => '', + 'sku_id' => '', + ], 1, 25); + + $this->assertCount(1, $inside['rows']); + $this->assertSame('26092142570188', $inside['rows'][0]['online_order_no']); + $this->assertSame([], $outside['rows']); + } + + public function test_order_groups_are_paginated_before_wide_sums_are_calculated(): void + { + DB::connection('business_mirror')->table(GoodsProfitAfsLookupSchema::TABLE)->insert([ + 'id' => 3, + '_mirror_key' => 'row-3', + 'undefined1' => '9999', + 'undefined2' => 'ORDER-NEW', + 'undefined5' => '2026/9/23 8:00:00', + 'undefined6' => '2026/9/23 10:00:00', + 'undefined7' => '2026/9/23 7:59:00', + '_mirror_afs_order_time' => '2026-09-23 07:59:00', + '_mirror_afs_pay_time' => '2026-09-23 08:00:00', + '_mirror_afs_send_time' => '2026-09-23 10:00:00', + 'undefined8' => '唯品会MP-卓禾家具特卖旗舰店', + 'undefined10' => 'SKU-NEW', + 'undefined20' => '1', + 'undefined21' => '10', + 'undefined22' => '5', + 'undefined52' => '0', + 'undefined54' => '0', + 'undefined56' => '', + 'undefined59' => '否', + ]); + + $repository = app(OrderProfitRepository::class); + $filters = [ + 'date_field' => 'send_time', + 'from' => '2026-09-22', + 'to' => '2026-09-23', + 'order_no' => '', + 'shop_name' => '', + 'sku_id' => '', + ]; + $first = $repository->paginate($filters, 1, 1); + $second = $repository->paginate($filters, 2, 1); + + $this->assertSame('ORDER-NEW', $first['rows'][0]['online_order_no']); + $this->assertTrue($first['has_more']); + $this->assertSame('26092142570188', $second['rows'][0]['online_order_no']); + $this->assertFalse($second['has_more']); + } }