[ 'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '', ], 'upstream_mirrors.connection' => 'business_mirror', 'upstream_mirrors.datasets' => [ CourierFreightReconciliationMirrorRepository::DETAILS_DATASET => [ 'source_key' => 'jiansun', 'schema' => 'dbo', 'object' => 'jst_order_express_details', 'target' => 'js_jst_order_express_details', 'keys' => ['id'], 'watermark' => 'updatedAt', ], CourierFreightReconciliationMirrorRepository::EXTENSION_DATASET => [ 'source_key' => 'jiansun', 'schema' => 'dbo', 'object' => 'Jst_SendData_View_extension', 'target' => 'js_jst_send_data_view_extension', 'keys' => ['Id'], 'watermark' => null, ], CourierFreightReconciliationMirrorRepository::OUTBOUND_DATASET => [ 'source_key' => 'jiansun', 'schema' => 'dbo', 'object' => 'jst_salesOutboundInfo', 'target' => 'js_jst_sales_outbound_info', 'keys' => ['Id'], 'watermark' => 'updateTime', ], ], ]); DB::purge('business_mirror'); Schema::connection('business_mirror')->create('business_mirror_table_states', function (Blueprint $table): void { $table->string('dataset_key')->primary(); $table->dateTime('initial_completed_at')->nullable(); $table->string('status')->nullable(); $table->dateTime('last_succeeded_at')->nullable(); $table->string('run_mode')->nullable(); }); Schema::connection('business_mirror')->create('js_jst_order_express_details', function (Blueprint $table): void { $table->integer('id'); foreach (['updatedAt', 'so_id', 'logistic_id', 'logistic_company_name', 'platform_name', 'shop_name', 'warehouse_name', 'logistic_status_name'] as $column) { $table->string($column)->nullable(); } $table->dateTime('send_time')->nullable(); $table->decimal('weight', 20, 4)->nullable(); $table->decimal('f_weight', 20, 4)->nullable(); }); Schema::connection('business_mirror')->create('js_jst_send_data_view_extension', function (Blueprint $table): void { $table->integer('Id'); $table->integer('io_id')->nullable(); $table->integer('qty')->nullable(); foreach (['l', 'w', 'h', 'volume'] as $column) { $table->decimal($column, 20, 4)->nullable(); } foreach (['so_id', 'sku_ids', 'sku_name', 'shop_name', 'shop_i_id', 'supplier_name', 'receiver_state', 'receiver_city', 'receiver_district', 'expressName', 'wareHoursName'] as $column) { $table->string($column)->nullable(); } }); Schema::connection('business_mirror')->create('js_jst_sales_outbound_info', function (Blueprint $table): void { $table->integer('Id'); $table->integer('io_id')->nullable(); $table->decimal('freight', 20, 4)->nullable(); }); $this->repository = new CourierFreightReconciliationMirrorRepository( new BusinessMirrorDatasetRegistry(), new GenericBusinessMirrorRepository(app('db')), ); } public function test_it_rejects_an_unready_mirror(): void { $this->expectException(RuntimeException::class); $this->expectExceptionMessage('courier_freight_reconciliation_mirror_not_ready'); $this->repository->paginate([], 1, 25); } public function test_it_joins_workbook_fields_and_keeps_freight_aggregate_unique(): void { $this->markReady(); DB::connection('business_mirror')->table('js_jst_order_express_details')->insert([ 'id' => 1, 'updatedAt' => '2026-09-01 01:00:00', 'so_id' => 'SO-1', 'logistic_id' => 'YT-001', 'logistic_company_name' => '圆通速递', 'platform_name' => '淘宝天猫', 'shop_name' => '店铺A', 'warehouse_name' => '仓库A', 'send_time' => '2026-09-01 00:02:06', 'weight' => 0.8, 'f_weight' => 0.7, 'logistic_status_name' => '已发货', ]); DB::connection('business_mirror')->table('js_jst_send_data_view_extension')->insert([ ['Id' => 10, 'io_id' => 100, 'so_id' => 'SO-1', 'qty' => 1, 'l' => 10, 'w' => 20, 'h' => 30, 'sku_ids' => 'SKU-OLD', 'sku_name' => '旧商品', 'shop_name' => '旧店铺', 'shop_i_id' => 'OLD-I', 'supplier_name' => '供应商A', 'receiver_state' => '江苏省', 'receiver_city' => '常州市', 'receiver_district' => '武进区', 'expressName' => '圆通旧', 'wareHoursName' => '旧仓', 'volume' => 6000], ['Id' => 11, 'io_id' => 100, 'so_id' => 'SO-1', 'qty' => 2, 'l' => 11, 'w' => 21, 'h' => 31, 'sku_ids' => 'SKU-1', 'sku_name' => '演示商品', 'shop_name' => '店铺A', 'shop_i_id' => 'ITEM-1', 'supplier_name' => '供应商A', 'receiver_state' => '江苏省', 'receiver_city' => '常州市', 'receiver_district' => '武进区', 'expressName' => '圆通速递', 'wareHoursName' => '仓库A', 'volume' => 7161], ]); DB::connection('business_mirror')->table('js_jst_sales_outbound_info')->insert([ ['Id' => 100, 'io_id' => 100, 'freight' => 0.5], ['Id' => 101, 'io_id' => 100, 'freight' => 0.8], ]); $this->repository->summarize(['express_no' => 'YT', 'month' => '2026-09']); $result = $this->repository->paginate(['express_no' => 'YT', 'month' => '2026-09'], 1, 25); $this->assertSame(1, $result['total']); $this->assertSame('YT-001', $result['data'][0]['waybill']); $this->assertSame('ITEM-1', $result['data'][0]['product_id']); $this->assertSame('SKU-1', $result['data'][0]['barcode']); $this->assertSame('演示商品', $result['data'][0]['product_name']); $this->assertSame('江苏省', $result['data'][0]['province']); $this->assertSame(2, $result['data'][0]['quantity']); $this->assertSame(0.8, $result['data'][0]['freight']); $this->assertSame(0.8, $result['summary']['freight']); $this->assertSame('fresh', $result['sync']['code']); $this->assertArrayNotHasKey('receiver_address', $result['data'][0]); $this->assertArrayNotHasKey('password', $result['data'][0]); } public function test_it_keeps_a_shipment_visible_when_optional_detail_rows_are_missing(): void { $this->markReady(); DB::connection('business_mirror')->table('js_jst_order_express_details')->insert([ 'id' => 2, 'updatedAt' => '2026-09-02 01:00:00', 'so_id' => 'SO-2', 'logistic_id' => 'ZT-002', 'logistic_company_name' => '中通快递', 'platform_name' => '京东商城', 'shop_name' => '店铺B', 'warehouse_name' => '仓库B', 'send_time' => '2026-09-02 00:00:00', 'logistic_status_name' => '已发货', ]); $this->repository->summarize(['express_no' => 'ZT-002']); $result = $this->repository->paginate(['express_no' => 'ZT-002'], 1, 25); $this->assertSame(1, $result['total']); $this->assertSame('SO-2', $result['data'][0]['transaction_order_no']); $this->assertSame('店铺B', $result['data'][0]['shop_name']); $this->assertSame('仓库B', $result['data'][0]['warehouse']); $this->assertNull($result['data'][0]['product_id']); $this->assertNull($result['data'][0]['freight']); } public function test_pages_do_not_wait_for_full_aggregation_and_do_not_invent_totals(): void { $this->markReady(); $db = DB::connection('business_mirror'); $db->table('js_jst_order_express_details')->insert([ ['id' => 1, 'logistic_id' => 'A'], ['id' => 2, 'logistic_id' => 'B'], ['id' => 3, 'logistic_id' => 'C'], ]); $db->enableQueryLog(); $first = $this->repository->paginate([], 1, 2); $second = $this->repository->paginate([], 2, 2); $this->assertSame([3, 2], array_column($first['data'], 'detail_id')); $this->assertSame([1], array_column($second['data'], 'detail_id')); $this->assertTrue($first['has_more']); $this->assertFalse($second['has_more']); $this->assertNull($first['total']); $this->assertNull($first['summary']); $queries = strtolower(implode(' ', array_column($db->getQueryLog(), 'query'))); $db->disableQueryLog(); $this->assertStringNotContainsString('count(', $queries); $this->assertStringNotContainsString('sum(', $queries); } public function test_summary_reuses_cache_and_serializes_expensive_requests(): void { $this->markReady(); $db = DB::connection('business_mirror'); $db->table('js_jst_order_express_details')->insert(['id' => 1, 'logistic_id' => 'A']); $lock = Cache::lock('courier-freight-summary:calculation', 360); $this->assertTrue($lock->get()); try { $this->assertNull($this->repository->summarize([])); } finally { $lock->release(); } $summary = $this->repository->summarize([]); $this->assertSame(1, $summary['count']); $this->assertArrayHasKey('computed_at', $summary); $db->enableQueryLog(); $this->assertSame($summary, $this->repository->summarize(['month' => '', 'express_no' => ''])); $this->assertStringNotContainsString('sum(', strtolower(implode(' ', array_column($db->getQueryLog(), 'query')))); $db->disableQueryLog(); $this->assertSame(0, $this->repository->summarize(['express_no' => 'MISSING'])['count']); } public function test_summary_request_defers_database_work_until_after_the_response(): void { $this->markReady(); $db = DB::connection('business_mirror'); $db->table('js_jst_order_express_details')->insert(['id' => 1, 'logistic_id' => 'A']); $db->enableQueryLog(); $this->assertNull($this->repository->requestSummary([])); $this->assertSame([], $db->getQueryLog()); $this->app->terminate(); $result = $this->repository->requestSummary([]); $this->assertSame(1, $result['count']); $db->disableQueryLog(); } public function test_summary_lookup_keeps_full_long_order_identifiers_distinct(): void { $this->markReady(); $db = DB::connection('business_mirror'); $prefix = str_repeat('X', 64); $db->table('js_jst_order_express_details')->insert(['id' => 1, 'logistic_id' => 'A', 'so_id' => $prefix . 'A']); $db->table('js_jst_send_data_view_extension')->insert([ ['Id' => 1, 'so_id' => $prefix . 'A', 'io_id' => 1], ['Id' => 2, 'so_id' => $prefix . 'B', 'io_id' => 2], ]); $db->table('js_jst_sales_outbound_info')->insert([ ['Id' => 1, 'io_id' => 1, 'freight' => 1], ['Id' => 2, 'io_id' => 2, 'freight' => 2], ]); $summary = $this->repository->summarize([]); $this->assertSame(1, $summary['count']); $this->assertSame(1.0, $summary['freight']); } private function markReady(): void { $now = now()->toDateTimeString(); foreach ([ CourierFreightReconciliationMirrorRepository::DETAILS_DATASET, CourierFreightReconciliationMirrorRepository::EXTENSION_DATASET, CourierFreightReconciliationMirrorRepository::OUTBOUND_DATASET, ] as $dataset) { DB::connection('business_mirror')->table('business_mirror_table_states')->insert([ 'dataset_key' => $dataset, 'initial_completed_at' => '2026-09-01 09:00:00', 'status' => 'succeeded', 'last_succeeded_at' => $now, ]); } } }