1000]); config(['database.connections.business_mirror' => [ 'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '', ]]); DB::purge('business_mirror'); Schema::connection('business_mirror')->create('store_violation_records', function ($table): void { $table->bigIncrements('id'); $table->string('platform', 32); $table->string('shop_name', 191)->default(''); $table->string('violation_type', 500)->nullable(); $table->string('violation_category', 64)->nullable(); $table->string('violation_no', 100); $table->date('violation_date')->nullable(); $table->decimal('points_deducted', 10, 2)->nullable(); $table->string('status', 64)->nullable(); $table->date('penalty_date')->nullable(); $table->date('appeal_deadline')->nullable(); $table->string('handling_status', 32)->default('未处理'); $table->string('source', 16)->default('crawl'); $table->string('window_name', 191)->nullable(); $table->date('crawl_date')->nullable(); $table->dateTime('synced_at')->nullable(); $table->timestamps(); $table->unique(['platform', 'violation_no']); }); Schema::connection('business_mirror')->create('business_mirror_table_states', function ($table): void { $table->string('dataset_key', 191)->primary(); $table->string('source_key', 64)->default(''); $table->string('source_object', 191)->default(''); $table->string('target_table', 64)->default(''); $table->string('status', 24)->default('pending'); $table->dateTime('last_attempted_at')->nullable(); $table->dateTime('last_succeeded_at')->nullable(); $table->unsignedBigInteger('mirror_row_count')->default(0); $table->string('last_error_code', 120)->nullable(); $table->dateTime('initial_completed_at')->nullable(); $table->timestamps(); }); DB::connection('business_mirror')->table('store_violation_records')->insert([ [ 'platform' => 'jd', 'shop_name' => '京东-亿家达全屋旗舰店', 'violation_type' => '延迟发货/缺货无货', 'violation_category' => '发货履约', 'violation_no' => 'JD202610040001', 'violation_date' => '2026-10-04', 'points_deducted' => 2, 'status' => '待处理', 'penalty_date' => null, 'appeal_deadline' => '2026-10-11', 'handling_status' => '未处理', 'source' => 'crawl', 'crawl_date' => '2026-10-04', 'created_at' => '2026-10-05 08:00:00', 'updated_at' => '2026-10-05 08:00:00', ], [ 'platform' => 'jd', 'shop_name' => '京东-卓禾卧室家具旗舰店', 'violation_type' => '订单存在揽收超时行为', 'violation_category' => '发货履约', 'violation_no' => 'JD202610040002', 'violation_date' => '2026-10-03', 'points_deducted' => null, 'status' => '超期未申诉', 'penalty_date' => '2026-10-05', 'appeal_deadline' => '2026-10-08', 'handling_status' => '处理中', 'source' => 'crawl', 'crawl_date' => '2026-10-04', 'created_at' => '2026-10-05 08:00:00', 'updated_at' => '2026-10-05 08:00:00', ], [ 'platform' => 'jingxi', 'shop_name' => '京喜-久伴吾家具京喜自营工厂店', 'violation_type' => '发票违规', 'violation_category' => '发票与考核', 'violation_no' => 'JX202610040001', 'violation_date' => '2026-10-04', 'points_deducted' => null, 'status' => '已处置', 'penalty_date' => null, 'appeal_deadline' => null, 'handling_status' => '已处理', 'source' => 'crawl', 'crawl_date' => '2026-10-04', 'created_at' => '2026-10-05 08:00:00', 'updated_at' => '2026-10-05 08:00:00', ], ]); DB::connection('business_mirror')->table('business_mirror_table_states')->insert([ 'dataset_key' => StoreViolationRecordRepository::DATASET_KEY, 'source_key' => 'operations_replica', 'source_object' => 'rpt.crawl_shop_violation', 'target_table' => StoreViolationRecordRepository::TABLE, 'status' => 'succeeded', 'last_succeeded_at' => '2026-10-05 05:30:00', 'mirror_row_count' => 3, 'created_at' => '2026-10-05 05:30:00', 'updated_at' => '2026-10-05 05:30:00', ]); } public function test_page_renders_unified_headers_and_platform_rows(): void { $this->allowPage(); $page = $this->loginAs()->get(route('business.pages.show', [ 'section' => 'financial-risk-control', 'page' => 'store-violation-alert', 'platform' => 'jd', ])); $page->assertOk() ->assertSee('店铺名') ->assertSee('违规类型') ->assertSee('违规分类') ->assertSee('违规单号') ->assertSee('违规日期') ->assertSee('扣分') ->assertSee('申诉截止日期') ->assertSee('处理状态') ->assertSee('JD202610040001') ->assertSee('京东-亿家达全屋旗舰店') ->assertSee('data-platform-panel="jingxi"', false); } public function test_page_filters_by_keyword(): void { $this->allowPage(); $page = $this->loginAs()->get(route('business.pages.show', [ 'section' => 'financial-risk-control', 'page' => 'store-violation-alert', 'platform' => 'jd', 'keyword' => '卓禾', ])); $page->assertOk() ->assertSee('JD202610040002') ->assertDontSee('JD202610040001') ->assertDontSee('JX202610040001'); } public function test_export_streams_unified_headers(): void { $this->allowPage(); $response = $this->loginAs()->get(route('business.store-violation-alert.export', [ 'platform' => 'jd', 'keyword' => '亿家达', ])); $response->assertOk(); $this->assertStringContainsString( 'spreadsheetml', (string) $response->headers->get('content-type'), ); $binary = $response->streamedContent(); $this->assertNotSame('', $binary); $temp = tempnam(sys_get_temp_dir(), 'store-violation-export'); file_put_contents($temp, $binary); $sheet = IOFactory::load($temp)->getActiveSheet(); $headers = $sheet->rangeToArray('A1:J1')[0]; $this->assertSame( ['店铺名', '违规类型', '违规分类', '违规单号', '违规日期', '扣分', '状态', '处罚日期', '申诉截止日期', '处理状态'], $headers, ); $this->assertSame('JD202610040001', $sheet->getCell('D2')->getValue()); unlink($temp); } public function test_import_preview_maps_headers_and_commit_upserts(): void { $this->allowPage(); $file = $this->makeWorkbook([ ['店铺', '违规编号', '违规内容', '违规日期', '扣分'], ['京东-纪辰家具旗舰店', 'JD202610040099', '订单延迟发货', '2026-10-02', 1], ['京东-亿家达全屋旗舰店', 'JD202610040001', '延迟发货/缺货无货', '2026-10-04', 2], ['缺少单号的店铺', '', '无效行', '2026-10-02', ''], ]); $preview = $this->loginAs()->post(route('business.store-violation-alert.import.preview'), [ 'platform' => 'jd', 'import' => $file, ]); $preview->assertRedirect(); $payload = $preview->getSession()->get('store_violation_preview'); $this->assertIsArray($payload); $this->assertSame(1, $payload['summary']['new']); $this->assertSame(1, $payload['summary']['same']); $this->assertSame(1, $payload['summary']['invalid']); $this->assertNotEmpty($payload['token']); $commit = $this->loginAs()->post(route('business.store-violation-alert.import.commit'), [ 'preview_token' => $payload['token'], ]); $commit->assertRedirect(); $record = DB::connection('business_mirror') ->table('store_violation_records') ->where('platform', 'jd') ->where('violation_no', 'JD202610040099') ->first(); $this->assertNotNull($record); $this->assertSame('发货履约', $record->violation_category); $this->assertSame('京东-纪辰家具旗舰店', $record->shop_name); $this->assertSame('import', $record->source); } public function test_import_preview_requires_unified_required_columns(): void { $this->allowPage(); $file = $this->makeWorkbook([ ['店铺名', '违规类型'], ['京东-纪辰家具旗舰店', '订单延迟发货'], ]); $response = $this->loginAs()->post(route('business.store-violation-alert.import.preview'), [ 'platform' => 'jd', 'import' => $file, ]); $response->assertRedirect(); $response->assertSessionHasErrors('import'); } /** @param list> $rows */ private function makeWorkbook(array $rows): UploadedFile { $spreadsheet = new Spreadsheet(); $spreadsheet->getActiveSheet()->fromArray($rows, null, 'A1'); $path = tempnam(sys_get_temp_dir(), 'store-violation-import') . '.xlsx'; (new Xlsx($spreadsheet))->save($path); $binary = (string) file_get_contents($path); unlink($path); return UploadedFile::fake()->createWithContent('店铺违规信息.xlsx', $binary); } private function allowPage(): void { $this->user->attachPermission( Permission::where('name', 'read-admin-panel')->firstOrFail() ); } }