fixturePath = storage_path('framework/testing/reverse-logistics-' . uniqid('', true) . '.json'); file_put_contents($this->fixturePath, json_encode([ 'sourcePath' => 'C:/private/逆向物流报价.xlsx', 'generatedAt' => '2026-09-29 12:00:00', 'sheets' => [ [ 'index' => 0, 'name' => '申通退货', 'address' => 'A1:F8', 'values' => [ ['报价单', null, null, null, null, null], ['出发省份', '到达省份', '江苏', null, '安徽', null], [null, null, '面单价格', '续重价格', '面单价格', '续重价格'], [null, '江苏', 9, 1, 9, 1], [null, '广东', 9, 1.75, 9, 1.75], ['1、该价格不含税,生效日期:2026年5月1日', null, null, null, null, null], ['4、计泡标准:长*宽*高/6000', null, null, null, null, null], ['7、结算方式:总价=首重+续重*(货物结算重量-1)', null, null, null, null, null], ], 'formulas' => [[], [], [], [], []], ], [ 'index' => 1, 'name' => '常州德邦逆向', 'address' => 'A1:D5', 'values' => [ ['常州德邦逆向报价2026.4.7', null, null, null], ['出发省份', '到达省份江苏', null, '备注:'], [null, '面单价格', '续重价格', null], ['江苏', 9, 1, '1、此价格单件限重60KG以内 单边超1.6米或三边超2.5米加收7元每单'], ['新疆', 18, 5.5, '6.计算方式:面单+重量*费率'], ], 'formulas' => [[], [], [], [], []], ], [ 'index' => 2, 'name' => '中通快运逆向', 'address' => 'A1:F4', 'values' => [ ['中通快运逆向2026.4.21起', null, null, null, null, null], ['省份', '面单费', '省份', '0-30kg', '31-60kg', '备注'], ['安徽省', 9, '安徽省', 0.95, 0.95, '1、目前该报价仅支持常州仓收件地址。'], ['新疆', '/', '新疆', '/', '/', null], ], 'formulas' => [[], [], [], []], ], [ 'index' => 3, 'name' => '未识别报价表', 'address' => 'A1:B2', 'values' => [['随便', '什么'], ['没有报价结构', null]], 'formulas' => [[], []], ], ], ], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR)); } protected function tearDown(): void { if (isset($this->fixturePath) && is_file($this->fixturePath)) { unlink($this->fixturePath); } parent::tearDown(); } public function testItSplitsEverySupportedSheetLayoutIntoRates(): void { $snapshot = (new ReverseLogisticsQuoteSnapshot($this->fixturePath))->load(); $this->assertTrue($snapshot['available']); $this->assertSame(['未识别报价表'], $snapshot['unparsed_sheets']); $this->assertSame(['申通', '德邦', '中通快运'], array_column($snapshot['rate_groups'], 'carrier')); $this->assertSame(10, $snapshot['rate_summary']['rate_count']); $this->assertSame(8, $snapshot['rate_summary']['available_count']); $this->assertSame(2, $snapshot['rate_summary']['missing_count']); $this->assertSame(2, $snapshot['rate_summary']['weight_band_count']); $this->assertSame(['江苏', '广东', '新疆', '安徽'], $snapshot['provinces']['origins']); $this->assertSame(['江苏', '安徽'], $snapshot['provinces']['destinations']); $rules = collect($snapshot['rate_groups'])->keyBy('carrier'); $this->assertSame(4, $rules['申通']['rate_count']); $this->assertSame('2026-05-01', $rules['申通']['effective_date']); $this->assertSame('首重 + 续重 × (结算重量 - 1)', $rules['申通']['pricing_label']); $this->assertSame('行=出发省份,列=到达省份(2 个到达省)', $rules['申通']['route_label']); $this->assertContains('4、计泡标准:长*宽*高/6000', $rules['申通']['constraints']); $this->assertSame(2, $rules['德邦']['rate_count']); $this->assertSame('2026-04-07', $rules['德邦']['effective_date']); $this->assertSame('面单 + 重量 × 费率', $rules['德邦']['pricing_label']); $this->assertSame(4, $rules['中通快运']['rate_count']); $this->assertSame(2, $rules['中通快运']['available_count']); $this->assertSame('2026-04-21', $rules['中通快运']['effective_date']); $this->assertSame(['0-30kg', '31-60kg'], $rules['中通快运']['weight_bands']); $this->assertSame(1, $rules['中通快运']['destination_count']); $rates = collect($snapshot['rates']); $shentong = $rates->firstWhere(fn (array $rate) => $rate['carrier'] === '申通' && $rate['origin'] === '广东' && $rate['destination'] === '江苏'); $this->assertNotNull($shentong); $this->assertSame(9.0, $shentong['base_amount']); $this->assertSame(1.75, $shentong['increment_amount']); $this->assertSame('C5:D5', $shentong['source']); $this->assertSame('广东', $shentong['origin_key']); $this->assertSame('江苏', $shentong['destination_key']); $this->assertTrue($shentong['available']); $deppon = $rates->firstWhere(fn (array $rate) => $rate['carrier'] === '德邦' && $rate['origin'] === '新疆'); $this->assertNotNull($deppon); $this->assertSame('江苏', $deppon['destination']); $this->assertSame(18.0, $deppon['base_amount']); $this->assertSame(5.5, $deppon['increment_amount']); $bandRates = $rates->filter(fn (array $rate) => $rate['carrier'] === '中通快运' && $rate['origin'] === '安徽省'); $this->assertCount(2, $bandRates); $this->assertSame(['0-30kg', '31-60kg'], array_column($bandRates->values()->all(), 'weight_band')); $this->assertSame('安徽', $bandRates->first()['origin_key']); $this->assertSame('江苏', $bandRates->first()['destination_key']); $missing = $rates->filter(fn (array $rate) => ! $rate['available']); $this->assertCount(2, $missing); $this->assertSame(['新疆', '新疆'], $missing->pluck('origin')->values()->all()); } public function testItKeepsTheShellWhenTheSnapshotIsMissing(): void { $snapshot = (new ReverseLogisticsQuoteSnapshot(storage_path('framework/testing/missing-reverse-logistics.json')))->load(); $this->assertFalse($snapshot['available']); $this->assertSame('missing', $snapshot['state']); $this->assertSame('尚未导入本机逆向物流报价文件。', $snapshot['message']); $this->assertSame([], $snapshot['rates']); $this->assertSame([], $snapshot['rate_groups']); } }