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;
}
}