workbook(); $this->artisan('business-mirror:import-kuka-posting-base', [ 'path' => $file, '--month' => '2026-10', '--company' => (string) $this->company->id, ])->assertExitCode(0); $connection = DB::connection('business_mirror'); $this->assertSame(12, $connection->table('kuka_posting_base_rows')->count()); $this->assertDatabaseHas('kuka_posting_base_imports', [ 'company_id' => $this->company->id, 'period' => '2026-10', 'row_count' => 12, 'store_count' => 12, ], 'business_mirror'); $this->loginAs() ->get(route('business.pages.show', [ 'section' => 'posting-data', 'page' => 'kuka-basic-data', 'store_name' => '天猫-HX旗舰店101', ])) ->assertOk() ->assertSeeText('内部订单号') ->assertSeeText('发货金额') ->assertSeeText('灵珊店铺业绩') ->assertSeeText('发货明细数据') ->assertSeeText('顾家报价单') ->assertSee('data-kuka-posting-subnav', false) ->assertSee('data-kuka-posting-panel="shipments"', false) ->assertDontSeeText('顾家过账 · 基础数据') ->assertDontSeeText('聚水潭共享镜像') ->assertDontSeeText('来源文件:') ->assertDontSeeText('基础数据 · 字段结构') ->assertDontSeeText('已从共享业务镜像读取 12 条数据') ->assertSeeText('KUKA-00') ->assertSeeText('SKU-00') ->assertDontSeeText('KUKA-01') ->assertDontSeeText('SKU-01'); @unlink($file); } public function test_kuka_posting_submenu_can_open_directly_without_a_second_sidebar_entry(): void { $file = $this->workbook(); $this->artisan('business-mirror:import-kuka-posting-base', [ 'path' => $file, '--month' => '2026-10', '--company' => (string) $this->company->id, ])->assertExitCode(0); $this->loginAs() ->get(route('business.pages.show', [ 'section' => 'posting-data', 'page' => 'kuka-basic-data', 'submenu' => 'performance', ])) ->assertOk() ->assertSee('id="kuka-posting-tab-performance"', false) ->assertSee('class="kuka-posting-subnav__tab is-active"', false) ->assertSee('data-kuka-posting-panel="performance"', false) ->assertSeeText('后续接入灵珊店铺业绩数据后'); $navigation = collect(\App\Utilities\SidebarPermissions::navigationFor($this->user)) ->firstWhere('slug', 'posting-data'); $kuka = collect($navigation['pages'])->firstWhere('slug', 'kuka-posting'); $this->assertSame(['kuka-basic-data'], array_column($kuka['children'], 'slug')); $this->assertSame(['基础数据'], array_column($kuka['children'], 'title')); @unlink($file); } public function test_incomplete_store_export_keeps_the_previous_period_data(): void { $complete = $this->workbook(); $this->artisan('business-mirror:import-kuka-posting-base', [ 'path' => $complete, '--month' => '2026-10', '--company' => (string) $this->company->id, ])->assertExitCode(0); $incomplete = $this->workbook(skipFirstStore: true); $this->artisan('business-mirror:import-kuka-posting-base', [ 'path' => $incomplete, '--month' => '2026-10', '--company' => (string) $this->company->id, ])->assertExitCode(1); $this->assertSame( 12, DB::connection('business_mirror')->table('kuka_posting_base_rows') ->where('period', '2026-10') ->count(), ); @unlink($complete); @unlink($incomplete); } private function workbook(bool $skipFirstStore = false): string { $fields = config('kuka_posting.fields'); $headers = array_column($fields, 'source'); $stores = array_values(config('kuka_posting.stores')); $rows = []; foreach ($stores as $index => $store) { if ($skipFirstStore && $index === 0) { continue; } $rows[] = [ '内部订单号' => sprintf('KUKA-%02d', $index), '标记多标签' => '测试标签', '售后单号' => '', '订单类型' => '普通订单', '线上订单号' => sprintf('ONLINE-%02d', $index), '订单状态' => '已发货', '发货仓' => '测试仓', '运营云仓' => '测试云仓', '分销商' => '', '货主分销' => '', '店铺' => $store, '买家账号' => 'buyer' . $index, '卖家备注' => '测试备注', '订单日期' => '2026-10-01 10:00:00', '发货日期' => '2026-10-02 11:00:00', '付款日期' => '2026-10-01 09:30:00', '确认收货日期' => '2026-10-05 12:00:00', '省' => '江苏省', '市' => '常州市', '区县' => '新北区', '订单快递公司' => '测试快递', '订单快递单号' => 'EXP-' . $index, '售后快递公司' => '', '售后快递单号' => '', '收货人' => '测试收货人', '售后登记日期' => '', '售后确认日期' => '', '收货仓' => '', '售后分类' => '', '问题类型' => '', '商品编码' => sprintf('SKU-%02d', $index), '原始线上订单号' => sprintf('ONLINE-%02d', $index), '款式编码' => sprintf('STYLE-%02d', $index), '货号' => sprintf('ITEM-%02d', $index), '物料编号' => sprintf('MAT-%02d', $index), '型号' => '测试型号', '成本' => '100.00', '产品材质' => '板材', '实发数量' => '1', '发货金额' => '199.00', ]; } $path = tempnam(sys_get_temp_dir(), 'kuka-posting-base-') . '.xlsx'; $zip = new ZipArchive(); $zip->open($path, ZipArchive::CREATE | ZipArchive::OVERWRITE); $zip->addFromString('xl/workbook.xml', <<<'XML' XML); $zip->addFromString('xl/_rels/workbook.xml.rels', <<<'XML' XML); $zip->addFromString('xl/worksheets/sheet1.xml', $this->worksheetXml([['汇总']])); $zip->addFromString('xl/worksheets/sheet2.xml', $this->worksheetXml(array_merge([$headers], $rows))); $zip->close(); return $path; } /** * @param list|array> $rows */ private function worksheetXml(array $rows): string { $xmlRows = []; foreach ($rows as $rowIndex => $row) { $cells = []; $values = array_is_list($row) ? $row : $this->rowValues($row); foreach ($values as $columnIndex => $value) { $reference = $this->columnName($columnIndex + 1) . ($rowIndex + 1); $cells[] = '' . htmlspecialchars((string) $value, ENT_XML1 | ENT_QUOTES, 'UTF-8') . ''; } $xmlRows[] = '' . implode('', $cells) . ''; } return '' . '' . '' . implode('', $xmlRows) . ''; } /** * @param array $row * @return list */ private function rowValues(array $row): array { return array_map( static fn (array $field): string => (string) ($row[$field['source']] ?? ''), config('kuka_posting.fields'), ); } private function columnName(int $index): string { $name = ''; while ($index > 0) { $index--; $name = chr(65 + ($index % 26)) . $name; $index = intdiv($index, 26); } return $name; } }