'蔬菜', 'parent_id' => 0, 'sort' => 0, 'status' => 1]); $meatRoot = ProductCategoryModel::create(['name' => '肉禽蛋', 'parent_id' => 0, 'sort' => 1, 'status' => 1]); $level = CustomerLevelModel::factory()->create(); $store = StoreModel::factory()->create(['level_id' => $level->id]); $veg = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'category_id' => $vegRoot->id]); $meat = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'category_id' => $meatRoot->id]); ProductPriceModel::factory()->create(['product_id' => $veg->id, 'level_id' => $level->id, 'price' => '5.00']); ProductPriceModel::factory()->create(['product_id' => $meat->id, 'level_id' => $level->id, 'price' => '20.00']); $this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create()); $this->postJson('/mini/order', ['items' => [ ['product_id' => $veg->id, 'quantity' => 2], ['product_id' => $meat->id, 'quantity' => 1], ]])->assertJsonPath('success', true); // 接单后生成采购单 StoreOrderModel::query()->update(['status' => StoreOrderModel::STATUS_SUMMARIZED]); $this->actingAsSysUser(); $this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()]) ->assertJsonPath('success', true); return PurchaseOrderModel::first(); } /** xlsx 导出:正确 Content-Type + 中文文件名 RFC 5987 编码 */ public function test_export_xlsx_with_encoded_chinese_filename(): void { $purchase = $this->buildPurchaseWithItems(); $this->actingAsSysUser(); $response = $this->get("/purchase/order/{$purchase->id}/export"); $response->assertOk(); $this->assertStringContainsString( 'spreadsheetml', (string) $response->headers->get('Content-Type'), 'xlsx 应返回电子表格 Content-Type' ); $disposition = (string) $response->headers->get('Content-Disposition'); // Symfony 输出小写 utf-8''(RFC 5987),大小写不敏感断言 $this->assertMatchesRegularExpression("/filename\*=utf-8''/i", $disposition, '中文文件名应走 RFC 5987 编码'); $this->assertStringContainsString(rawurlencode('采购单'), $disposition); } /** 蔬果分类过滤:type=category 仅导出蔬菜/水果顶级分类商品 */ public function test_export_category_filters_to_vegetables(): void { $purchase = $this->buildPurchaseWithItems(); $this->actingAsSysUser(); Excel::fake(); $this->get("/purchase/order/{$purchase->id}/export?type=category")->assertOk(); Excel::assertDownloaded( $purchase->purchase_no . '_采购单.xlsx', static function (PurchaseOrderExport $export): bool { $items = $export->collection(); // 仅蔬菜商品一行,肉禽被过滤 return $items->count() === 1; } ); } /** type 参数非法 → 拒绝 */ public function test_export_invalid_type_rejected(): void { $purchase = $this->buildPurchaseWithItems(); $this->actingAsSysUser(); $this->get("/purchase/order/{$purchase->id}/export?type=doc") ->assertJsonPath('success', false); } /** 采购单不存在 → 拒绝 */ public function test_export_missing_purchase_rejected(): void { $this->actingAsSysUser(); $this->get('/purchase/order/99999/export') ->assertJsonPath('success', false); } /** 无 purchase.order.export 权限点 → 拦截 */ public function test_export_requires_export_permission(): void { $purchase = $this->buildPurchaseWithItems(); // 仅持有查询权限的用户 $this->actingAsSysUser(['purchase.order.query']); $response = $this->get("/purchase/order/{$purchase->id}/export"); $this->assertFalse($response->json('success'), '缺少权限点应被拦截'); } }