[ 'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '', ]]); DB::purge('business_mirror'); Schema::connection('business_mirror')->create('ops_jst_orders', function ($table): void { $table->integer('Id')->primary(); $table->bigInteger('o_id')->nullable(); $table->string('so_id')->nullable(); $table->string('shop_name')->nullable(); $table->dateTime('order_date')->nullable(); $table->text('labels')->nullable(); }); DB::connection('business_mirror')->table('ops_jst_orders')->insert([ ['Id' => 1, 'o_id' => 101, 'so_id' => '260901-391328633273048', 'shop_name' => '拼多多-优乐猫家具旗舰店', 'order_date' => '2026-09-01 00:00:13', 'labels' => '发货前售后,店铺-拼多多'], ['Id' => 2, 'o_id' => 102, 'so_id' => '3608400015164657', 'shop_name' => '京喜-久伴吾家具京喜自营工厂店', 'order_date' => '2026-09-01 00:00:44', 'labels' => '京东供销订单, 京喜'], ['Id' => 3, 'o_id' => 103, 'so_id' => '110211297173802010', 'shop_name' => '得物-全米', 'order_date' => '2026-08-31 23:59:00', 'labels' => '品牌直发'], ['Id' => 4, 'o_id' => 104, 'so_id' => '260915-000000000000001', 'shop_name' => '得物-全米', 'order_date' => '2026-09-15 10:00:00', 'labels' => ''], ]); // 商品明细(货号):o_id=101 是换商品后的 X00006 虚拟条码单,102 是普通单 Schema::connection('business_mirror')->create('ops_jst_order_item', function ($table): void { $table->integer('Id')->primary(); $table->bigInteger('o_id')->nullable(); $table->string('sku_id')->nullable(); $table->string('item_status')->nullable(); }); DB::connection('business_mirror')->table('ops_jst_order_item')->insert([ ['Id' => 1, 'o_id' => 101, 'sku_id' => 'OLD-SKU', 'item_status' => 'Replaced'], ['Id' => 2, 'o_id' => 101, 'sku_id' => 'X00006', 'item_status' => null], ['Id' => 3, 'o_id' => 102, 'sku_id' => 'JD-001', 'item_status' => null], ['Id' => 4, 'o_id' => 103, 'sku_id' => 'DW-9', 'item_status' => null], ]); // 全渠道镜像(天猫等):sync-item-codes 命令的回填来源之一 Schema::connection('business_mirror')->create('ops_jst_qmcust_orders', function ($table): void { $table->integer('Id')->primary(); $table->bigInteger('o_id')->nullable(); $table->string('so_id')->nullable(); $table->dateTime('order_date')->nullable(); }); Schema::connection('business_mirror')->create('ops_jst_qmcust_order_items', function ($table): void { $table->integer('Id')->primary(); $table->bigInteger('o_id')->nullable(); $table->string('sku_id')->nullable(); $table->string('item_status')->nullable(); }); DB::connection('business_mirror')->table('ops_jst_qmcust_orders')->insert([ ['Id' => 1, 'o_id' => 201, 'so_id' => '3308965970412095568', 'order_date' => '2026-09-02 10:00:00'], ]); DB::connection('business_mirror')->table('ops_jst_qmcust_order_items')->insert([ ['Id' => 1, 'o_id' => 201, 'sku_id' => 'azf', 'item_status' => 'Replaced'], ['Id' => 2, 'o_id' => 201, 'sku_id' => 'X00006', 'item_status' => 'Sent'], ]); } public function test_import_splits_labels_without_deduping_orders(): void { $this->allowPage(); $result = app(JstOrderLabelImporter::class)->import('2026-09-01', '2026-09-15', $this->company->id, true); $this->assertSame(3, $result['orders']); $this->assertSame(5, $result['rows']); $this->assertSame(1, $result['no_label_orders']); $rows = JstOrderLabel::query()->where('company_id', $this->company->id)->orderBy('id')->get(); $this->assertSame(['发货前售后', '店铺-拼多多', '京东供销订单', '京喜', '(无标签)'], $rows->pluck('label')->all()); // 同一订单拆成多行,订单不去重 $this->assertSame(2, $rows->where('order_no', '260901-391328633273048')->count()); $this->assertSame('2026-09-15 10:00:00', $rows->last()->ordered_at->format('Y-m-d H:i:s')); // 货号随导入写入:被替换掉的旧编码不计入 $this->assertSame('X00006', $rows->firstWhere('label', '发货前售后')->item_code); } public function test_import_window_excludes_out_of_range_orders(): void { $this->allowPage(); // 8-31 的订单不在 9 月窗口内 $result = app(JstOrderLabelImporter::class)->import('2026-09-01', '2026-09-15', $this->company->id, true); $orders = JstOrderLabel::query()->where('company_id', $this->company->id)->pluck('order_no')->unique()->all(); $this->assertNotContains('110211297173802010', $orders); $this->assertSame(3, $result['orders']); } public function test_page_filters_and_export(): void { $this->allowPage(); app(JstOrderLabelImporter::class)->import('2026-09-01', '2026-09-15', $this->company->id, true); $page = $this->loginAs()->get(route('business.pages.show', [ 'section' => 'planting-data', 'page' => 'jst-order-labels', ])); $page->assertOk() ->assertSee('聚水潭订单标签明细') ->assertSee('260901-391328633273048') ->assertSee('店铺-拼多多') ->assertSee('(无标签)') ->assertSee('货号') ->assertSee('X00006') ->assertSee('id="jst-label-date-from"', false) ->assertSee('data-jst-labels-clear="jst-label-date-to"', false); $filtered = $this->loginAs()->get(route('business.pages.show', [ 'section' => 'planting-data', 'page' => 'jst-order-labels', 'label' => '京喜', ])); $filtered->assertOk() ->assertSee('3608400015164657') ->assertDontSee('260901-391328633273048'); $export = $this->loginAs()->get(route('business.planting-data.jst-order-labels.export', [ 'label' => '京喜', ])); $export->assertOk()->assertDownload(); $csv = $export->streamedContent(); $this->assertStringContainsString('线上订单号,店铺名称,下单时间,是否种菜订单,标签,货号', explode("\n", $csv)[0]); $this->assertStringContainsString('3608400015164657', $csv); $this->assertStringContainsString('JD-001', $csv); $this->assertStringNotContainsString('260901-391328633273048', $csv); } public function test_item_code_marks_planting_orders_and_exports(): void { $this->allowPage(); app(JstOrderLabelImporter::class)->import('2026-09-01', '2026-09-15', $this->company->id, true); $rules = app(\App\Business\JstOrderLabelPlantingRules::class); // 货号 X00006(信誉订单虚拟条码)直接判为种菜订单,与标签映射无关 $this->assertSame('是', $rules->flag($this->company->id, '发货前售后', 'X00006')); $this->assertSame('否', $rules->flag($this->company->id, '发货前售后', 'JD-001')); $this->assertSame('是', $rules->flag($this->company->id, '发货前售后', 'JD-001,X00006')); // 导出里能看到货号列和「是否种菜订单=是」 $export = $this->loginAs()->get(route('business.planting-data.jst-order-labels.export', [])); $export->assertOk()->assertDownload(); $csv = $export->streamedContent(); $this->assertStringContainsString('是,发货前售后,X00006', $csv); $this->assertStringContainsString('否,京喜,JD-001', $csv); } public function test_sync_item_codes_command_backfills_existing_rows(): void { $this->allowPage(); app(JstOrderLabelImporter::class)->import('2026-09-01', '2026-09-15', $this->company->id, true); // 模拟历史数据:清空货号,并补一条全渠道镜像来源的标签行 JstOrderLabel::query()->where('company_id', $this->company->id)->update(['item_code' => null]); JstOrderLabel::create([ 'company_id' => $this->company->id, 'order_no' => '3308965970412095568', 'shop_name' => '天猫-亿家达旗舰店', 'ordered_at' => '2026-09-02 10:00:00', 'label' => '刷单待处理', 'created_from' => 'jst-order-label-qmcust-import', ]); $this->artisan('jst-order-labels:sync-item-codes', ['--company' => $this->company->id]) ->assertSuccessful(); $rows = JstOrderLabel::query()->where('company_id', $this->company->id)->get(); $this->assertSame('X00006', $rows->firstWhere('label', '发货前售后')->item_code); $this->assertSame('JD-001', $rows->firstWhere('label', '京喜')->item_code); $this->assertSame('X00006', $rows->firstWhere('order_no', '3308965970412095568')->item_code); } private function allowPage(): void { $this->user->attachPermission( Permission::where('name', 'read-admin-panel')->firstOrFail() ); } public function test_import_from_erp_export_file_dedupes_lines_and_labels(): void { $this->allowPage(); $path = $this->makeExportFile([ ['内部订单号', '线上订单号', '店铺名称', '下单时间', '标签'], ['6476896', '3424493003753167', '京东-柯诺苏家具旗舰店', '2026-03-01 01:30:52', '发货后售后,店铺-京东,晚发赔'], ['6476897', '3424493003753167', '京东-柯诺苏家具旗舰店', '2026-03-01 01:30:52', '发货后售后,店铺-京东,晚发赔'], ['6476898', '3424493003753167', '京东-柯诺苏家具旗舰店', '2026-03-01 01:30:52', '发货后售后'], // 同一单里只有部分明细行带标签:不能因此多写一行「(无标签)」 ['6476899', '3424493003753167', '京东-柯诺苏家具旗舰店', '2026-03-01 01:30:52', ''], ['6477350', '110197275064171099', '得物-米客米家家居旗舰店', '2026-03-01 08:05:36', ''], ['9535498', '3290154852795260451', '淘宝-亿家达家居鞋柜电脑桌', '2026-03-11 19:01:30', '历史订单'], ]); try { $result = app(JstOrderLabelImporter::class)->importFromFile($path, null, null, $this->company->id, false); } finally { @unlink($path); } // 同一订单的多条明细行只算一单,重复标签只写一行, // 且「部分明细行没标签」不会污染成「(无标签)」 $this->assertSame(3, $result['orders']); $this->assertSame(5, $result['rows']); $this->assertSame(1, $result['no_label_orders']); $this->assertSame(0, JstOrderLabel::query()->where('company_id', $this->company->id) ->where('order_no', '3424493003753167') ->where('label', JstOrderLabelImporter::NO_LABEL)->count()); $labels = JstOrderLabel::query()->where('company_id', $this->company->id) ->where('order_no', '3424493003753167')->pluck('label')->sort()->values()->all(); $this->assertSame(['发货后售后', '店铺-京东', '晚发赔'], $labels); $noLabel = JstOrderLabel::query()->where('company_id', $this->company->id) ->where('order_no', '110197275064171099')->firstOrFail(); $this->assertSame(JstOrderLabelImporter::NO_LABEL, $noLabel->label); $this->assertSame('得物-米客米家家居旗舰店', $noLabel->shop_name); } public function test_import_from_file_respects_window_and_replace(): void { $this->allowPage(); app(JstOrderLabelImporter::class)->import('2026-09-01', '2026-09-15', $this->company->id, true); $path = $this->makeExportFile([ ['线上订单号', '店铺名称', '下单时间', '标签'], ['A-1', '店铺甲', '2026-03-05 10:00:00', '刷单待处理'], ['A-2', '店铺甲', '2026-03-06 10:00:00', '刷单待处理'], ]); try { $result = app(JstOrderLabelImporter::class)->importFromFile( $path, '2026-03-06', '2026-03-31', $this->company->id, true, 'test' ); } finally { @unlink($path); } // 窗口外的 3-05 被跳过;9 月的老数据在窗口外因此不受 --replace 影响 $this->assertSame(1, $result['orders']); $this->assertSame(1, $result['skipped']); $this->assertSame(['A-2'], JstOrderLabel::query()->where('company_id', $this->company->id) ->where('order_no', 'like', 'A-%')->pluck('order_no')->all()); // 原有 9 月 5 行 + 新增 A-2 一行 $this->assertSame(6, JstOrderLabel::query()->where('company_id', $this->company->id)->count()); } /** * 按聚水潭「导出订单」的真实形态造一个最小 xlsx: * 字符串走 inlineStr、dimension 只写 A1(尺寸不可信)、一单多行。 * * @param list> $rows */ private function makeExportFile(array $rows): string { $path = tempnam(sys_get_temp_dir(), 'jst-export-') . '.xlsx'; $sheet = '' . '' . ''; foreach ($rows as $rowIndex => $values) { $sheet .= ''; foreach ($values as $columnIndex => $value) { $reference = Coordinate::stringFromColumnIndex($columnIndex + 1) . ($rowIndex + 1); $sheet .= '' . htmlspecialchars((string) $value, ENT_XML1) . ''; } $sheet .= ''; } $sheet .= ''; $zip = new \ZipArchive(); $this->assertTrue($zip->open($path, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) === true); $zip->addFromString('[Content_Types].xml', '' . '' . '' . '' . '' . '' . ''); $zip->addFromString('_rels/.rels', '' . '' . '' . ''); $zip->addFromString('xl/workbook.xml', '' . '' . ''); $zip->addFromString('xl/_rels/workbook.xml.rels', '' . '' . '' . ''); $zip->addFromString('xl/sharedStrings.xml', '' . ''); $zip->addFromString('xl/worksheets/sheet1.xml', "\xEF\xBB\xBF" . $sheet); $zip->close(); return $path; } }