seed(); $this->post('/merchant-admin/login', [ 'email' => 'merchant.admin@demo.local', 'password' => 'Merchant@123456', ])->assertRedirect('/merchant-admin'); } public function test_merchant_products_page_displays_import_and_history_entries(): void { $this->loginAsMerchantAdmin(); $this->get('/merchant-admin/products?status=published&min_price=150&max_price=220&sort=price_desc') ->assertOk() ->assertSee('商家商品管理') ->assertSee('批量导入商品') ->assertSee('下载导入模板') ->assertSee('导出当前筛选结果 CSV') ->assertSee('进入完整导入历史页') ->assertSee('导入历史摘要') ->assertSee('当前筛选摘要') ->assertSee('运营关注项') ->assertSee('当前信号') ->assertSee('工作台导航') ->assertSee('已上架') ->assertSee('价格从高到低'); } public function test_merchant_product_operations_focus_can_follow_current_filters(): void { $this->loginAsMerchantAdmin(); $this->get('/merchant-admin/products?status=draft&sort=latest') ->assertOk() ->assertSee('当前正在查看草稿商品,建议优先补齐标题、分类、价格与库存后再安排上架。') ->assertSee('继续查看当前草稿'); $this->get('/merchant-admin/products?status=published&min_stock=0&max_stock=20&sort=stock_asc') ->assertOk() ->assertSee('当前筛选已聚焦已上架库存视角,建议优先确认低库存补货节奏,并同步观察高库存结构是否健康。') ->assertSee('继续查看当前库存视角'); $this->get('/merchant-admin/products?status=published&category_id=1&keyword=%E6%BC%94%E7%A4%BA&min_price=150&max_price=220&sort=price_desc') ->assertOk() ->assertSee('当前筛选已聚焦已上架的“默认分类”分类下关键词“演示”命中的价格带 ¥150.00 ~ ¥220.00 商品,建议优先核对在售商品命名、分类承接、价格梯度与搜索结果是否一致,并同步观察库存结构与转化表现是否健康。') ->assertSee('继续查看当前已上架分类关键词价格带商品') ->assertSee('去看当前已上架分类关键词商品'); $this->get('/merchant-admin/products?status=published&category_id=1&keyword=%E6%BC%94%E7%A4%BA&sort=latest') ->assertOk() ->assertSee('当前筛选已聚焦已上架的“默认分类”分类下关键词“演示”命中的商品,建议优先核对在售商品命名、分类承接与搜索结果是否一致,并同步观察价格带与库存结构是否健康。') ->assertSee('继续查看当前已上架分类关键词商品'); $this->get('/merchant-admin/products?status=published&category_id=1&min_price=150&max_price=220&sort=price_desc') ->assertOk() ->assertSee('当前筛选已聚焦已上架的“默认分类”分类下价格带 ¥150.00 ~ ¥220.00 商品,建议优先核对该分类在售商品的价格结构、库存分布与转化表现是否协调。') ->assertSee('继续查看当前已上架分类价格带商品'); $this->get('/merchant-admin/products?status=published&keyword=%E6%BC%94%E7%A4%BA&min_price=150&max_price=220&sort=price_desc') ->assertOk() ->assertSee('当前筛选已聚焦已上架商品中关键词“演示”命中的价格带 ¥150.00 ~ ¥220.00 结果,建议优先核对在售商品命名、卖点表达与价格梯度是否一致,并同步观察库存结构、转化表现与搜索承接是否健康。') ->assertSee('继续查看当前已上架关键词价格带商品') ->assertSee('去看当前已上架关键词商品'); $this->get('/merchant-admin/products?status=published&keyword=%E6%BC%94%E7%A4%BA&sort=latest') ->assertOk() ->assertSee('当前筛选已聚焦已上架商品中关键词“演示”命中的结果,建议优先核对在售商品命名、卖点表达与搜索承接是否一致,并同步观察价格带与库存结构是否健康。') ->assertSee('继续查看当前已上架关键词商品'); $this->get('/merchant-admin/products?category_id=1&keyword=%E6%BC%94%E7%A4%BA&min_price=150&max_price=220&sort=price_desc') ->assertOk() ->assertSee('当前筛选已聚焦“默认分类”分类下关键词“演示”命中的价格带 ¥150.00 ~ ¥220.00 商品,建议优先核对分类承接、命名卖点与价格梯度是否一致,并同步观察库存结构、转化表现与搜索承接是否健康。') ->assertSee('继续查看当前分类关键词价格带商品') ->assertSee('去看当前分类关键词商品'); $this->get('/merchant-admin/products?category_id=1&keyword=%E6%BC%94%E7%A4%BA&sort=latest') ->assertOk() ->assertSee('当前筛选已聚焦“默认分类”分类下关键词“演示”命中的商品,建议优先核对分类承接、命名卖点与搜索结果是否一致,并同步观察相关商品的价格带与库存结构。') ->assertSee('继续查看当前分类关键词商品'); $this->get('/merchant-admin/products?status=published&category_id=1&sort=latest') ->assertOk() ->assertSee('当前筛选已聚焦已上架的“默认分类”分类商品,建议优先核对该分类在售商品的价格带、库存结构与转化表现是否均衡。') ->assertSee('继续查看当前已上架分类商品'); $this->get('/merchant-admin/products?category_id=1&min_price=150&max_price=220&sort=price_desc') ->assertOk() ->assertSee('当前筛选已聚焦“默认分类”分类下价格带 ¥150.00 ~ ¥220.00 商品,建议优先核对该分类价格结构是否连贯,并同步观察库存分布与转化表现是否健康。') ->assertSee('继续查看当前分类价格带商品'); $this->get('/merchant-admin/products?category_id=1&sort=latest') ->assertOk() ->assertSee('当前筛选已聚焦“默认分类”分类商品,建议优先核对分类承接是否准确,并同步观察价格带与库存结构是否均衡。') ->assertSee('继续查看当前分类商品'); $this->get('/merchant-admin/products?keyword=%E6%BC%94%E7%A4%BA&sort=latest') ->assertOk() ->assertSee('当前筛选已聚焦关键词“演示”命中的商品,建议优先核对命名、卖点与搜索承接是否一致,并同步观察相关商品的价格带与库存结构。') ->assertSee('继续查看当前关键词商品'); $this->get('/merchant-admin/products?min_price=150&max_price=220&sort=price_desc') ->assertOk() ->assertSee('当前筛选已聚焦价格带 ¥150.00 ~ ¥220.00 商品,建议优先核对定价梯度是否连贯,并同步观察库存结构与转化表现是否匹配。') ->assertSee('继续查看当前价格带商品'); } public function test_merchant_orders_page_displays_filters_and_export_entry(): void { $this->loginAsMerchantAdmin(); $this->get('/merchant-admin/orders?status=shipped&payment_status=paid&platform=wechat_mini&device_type=mini-program&payment_channel=wechat_pay&min_pay_amount=180&max_pay_amount=190&sort=pay_amount_desc') ->assertOk() ->assertSee('商家订单管理') ->assertSee('筛选条件') ->assertSee('导出当前筛选结果 CSV') ->assertSee('当前筛选摘要') ->assertSee('运营关注项') ->assertSee('当前信号') ->assertSee('工作台导航') ->assertSee('已发货') ->assertSee('已支付') ->assertSee('微信小程序') ->assertSee('小程序环境') ->assertSee('微信支付') ->assertSee('订单汇总'); } public function test_merchant_order_operations_focus_can_follow_current_filters(): void { $this->loginAsMerchantAdmin(); $this->get('/merchant-admin/orders?platform=wechat_mini&sort=latest') ->assertOk() ->assertSee('当前筛选已聚焦微信小程序订单,建议优先关注下单到支付转化是否顺畅,并同步排查小程序端支付回流体验。') ->assertSee('继续查看微信小程序订单'); $this->get('/merchant-admin/orders?payment_channel=wechat_pay&sort=latest') ->assertOk() ->assertSee('当前筛选已聚焦微信支付订单,建议优先核对支付成功率、回调稳定性与失败重试转化。') ->assertSee('继续查看微信支付订单'); $this->get('/merchant-admin/orders?device_type=mini-program&payment_status=failed&sort=latest') ->assertOk() ->assertSee('当前筛选已聚焦小程序环境订单,建议优先核对授权链路、支付唤起表现与下单回流是否顺畅。') ->assertSee('继续查看小程序环境订单'); $this->get('/merchant-admin/orders?device_type=mobile-webview&payment_status=failed&sort=latest') ->assertOk() ->assertSee('当前筛选已聚焦微信内网页订单,建议优先关注授权静默登录、页面跳转稳定性与支付拉起后的回流体验。') ->assertSee('继续查看微信内网页订单'); $this->get('/merchant-admin/orders?device_type=mobile&payment_status=failed&sort=latest') ->assertOk() ->assertSee('当前筛选已聚焦移动浏览器订单,建议优先关注 H5 下单链路、页面加载稳定性与支付转化流失点。') ->assertSee('继续查看移动浏览器订单'); $this->get('/merchant-admin/orders?device_type=desktop&payment_status=failed&sort=latest') ->assertOk() ->assertSee('当前筛选已聚焦桌面浏览器订单,建议优先关注 PC 端下单流程、页面首屏稳定性与高客单转化表现。') ->assertSee('继续查看桌面浏览器订单'); $this->get('/merchant-admin/orders?payment_status=failed&sort=latest') ->assertOk() ->assertSee('当前筛选已聚焦支付失败订单,建议优先排查支付渠道、回调结果与用户重试情况。') ->assertSee('继续查看支付失败订单'); $this->get('/merchant-admin/orders?status=completed&sort=latest') ->assertOk() ->assertSee('当前正在查看已完成订单,建议复盘高客单成交与复购来源,沉淀更稳定的转化路径。') ->assertSee('继续查看已完成订单'); } public function test_merchant_categories_and_users_pages_display_expected_content(): void { $this->loginAsMerchantAdmin(); $this->get('/merchant-admin/product-categories') ->assertOk() ->assertSee('商家商品分类') ->assertSee('新增分类') ->assertSee('分类列表'); $this->get('/merchant-admin/users') ->assertOk() ->assertSee('商家用户管理') ->assertSee('用户列表'); } public function test_merchant_import_histories_page_displays_filter_and_export_entries(): void { $this->loginAsMerchantAdmin(); $this->get('/merchant-admin/products/import-histories') ->assertOk() ->assertSee('商家商品导入历史') ->assertSee('筛选导入历史') ->assertSee('导出当前筛选 CSV') ->assertSee('返回商品页'); } public function test_merchant_product_summary_stats_match_export_summary_for_same_filters(): void { $this->loginAsMerchantAdmin(); $page = $this->get('/merchant-admin/products?keyword=%E6%BC%94%E7%A4%BA%E5%95%86%E5%93%81&status=published&min_price=150&max_price=220'); $page->assertOk()->assertViewHas('summaryStats', function (array $summaryStats) { return ($summaryStats['total_products'] ?? null) === 1 && ($summaryStats['total_stock'] ?? null) === 100 && (float) ($summaryStats['total_stock_value'] ?? 0) === 19900.0 && (float) ($summaryStats['average_price'] ?? 0) === 199.0; }); $export = $this->get('/merchant-admin/products/export?keyword=%E6%BC%94%E7%A4%BA%E5%95%86%E5%93%81&status=published&min_price=150&max_price=220'); $export->assertOk(); $content = $export->streamedContent(); assertStringContainsString('导出信息,商家商品导出', $content); assertStringContainsString('状态,已上架', $content); assertStringContainsString('最低价格,¥150.00', $content); assertStringContainsString('最高价格,¥220.00', $content); assertStringContainsString('导出商品数,1', $content); assertStringContainsString('导出总库存,100', $content); assertStringContainsString('导出总货值,19900.00', $content); assertStringContainsString('导出平均售价,199.00', $content); assertStringContainsString('演示商品', $content); } public function test_merchant_order_summary_stats_match_export_summary_for_same_filters(): void { $this->loginAsMerchantAdmin(); $page = $this->get('/merchant-admin/orders?status=shipped&payment_status=paid&platform=wechat_mini&device_type=mini-program&payment_channel=wechat_pay&min_pay_amount=180&max_pay_amount=190'); $page->assertOk()->assertViewHas('summaryStats', function (array $summaryStats) { return ($summaryStats['total_orders'] ?? null) === 1 && (float) ($summaryStats['total_pay_amount'] ?? 0) === 189.0 && (float) ($summaryStats['average_order_amount'] ?? 0) === 189.0 && ($summaryStats['paid_orders'] ?? null) === 1 && ($summaryStats['failed_payment_orders'] ?? null) === 0; }); $export = $this->get('/merchant-admin/orders/export?status=shipped&payment_status=paid&platform=wechat_mini&device_type=mini-program&payment_channel=wechat_pay&min_pay_amount=180&max_pay_amount=190'); $export->assertOk(); $content = $export->streamedContent(); assertStringContainsString('导出信息,商家订单导出', $content); assertStringContainsString('订单状态,已发货', $content); assertStringContainsString('支付状态,已支付', $content); assertStringContainsString('平台,微信小程序', $content); assertStringContainsString('设备类型,小程序环境', $content); assertStringContainsString('支付渠道,微信支付', $content); assertStringContainsString('最低实付金额,¥180.00', $content); assertStringContainsString('最高实付金额,¥190.00', $content); assertStringContainsString('导出订单数,1', $content); assertStringContainsString('导出实付总额,189.00', $content); assertStringContainsString('导出平均客单价,189.00', $content); assertStringContainsString('导出已支付订单数,1', $content); assertStringContainsString('导出支付失败订单,0', $content); assertStringContainsString('ORD202603080003', $content); } public function test_merchant_products_batch_update_blocks_out_of_scope_products(): void { $this->loginAsMerchantAdmin(); $merchant = Merchant::query()->firstOrFail(); $foreignMerchant = Merchant::query()->create([ 'name' => '外部站点', 'slug' => 'foreign-merchant-demo', 'domain' => null, 'contact_name' => '测试人', 'contact_phone' => '13800000002', 'contact_email' => 'foreign@example.com', 'plan' => 'basic', 'status' => 'active', 'activated_at' => now(), 'settings' => ['currency' => 'CNY'], ]); $foreignCategory = ProductCategory::query()->create([ 'merchant_id' => $foreignMerchant->id, 'name' => '外部分类', 'slug' => 'foreign-default', 'status' => 'active', 'sort' => 10, 'description' => '用于越权测试', ]); $foreignProduct = Product::query()->create([ 'merchant_id' => $foreignMerchant->id, 'category_id' => $foreignCategory->id, 'title' => '外部商品', 'slug' => 'foreign-product', 'sku' => 'SKU-FOREIGN-001', 'summary' => '用于越权测试', 'content' => 'test', 'price' => 66, 'original_price' => 88, 'stock' => 12, 'status' => 'published', 'images' => [], ]); $localProduct = Product::query()->forMerchant($merchant->id)->orderBy('id')->firstOrFail(); $response = $this->from('/merchant-admin/products')->post('/merchant-admin/products/batch', [ 'product_ids' => [$localProduct->id, $foreignProduct->id], 'action' => 'change_status', 'status' => 'offline', ]); $response->assertRedirect('/merchant-admin/products'); $response->assertSessionHasErrors(['product_ids']); $response->assertSessionHasErrors(['product_ids' => '勾选商品中存在越权或已删除的数据,请刷新后重试。']); $this->assertSame('published', $localProduct->fresh()->status); $this->assertSame('published', $foreignProduct->fresh()->status); } public function test_merchant_products_batch_change_category_blocks_invalid_category(): void { $this->loginAsMerchantAdmin(); $foreignMerchant = Merchant::query()->create([ 'name' => '外部分站', 'slug' => 'foreign-category-merchant-demo', 'domain' => null, 'contact_name' => '测试人', 'contact_phone' => '13800000003', 'contact_email' => 'foreign-category@example.com', 'plan' => 'basic', 'status' => 'active', 'activated_at' => now(), 'settings' => ['currency' => 'CNY'], ]); $foreignCategory = ProductCategory::query()->create([ 'merchant_id' => $foreignMerchant->id, 'name' => '外部分站分类', 'slug' => 'foreign-category', 'status' => 'active', 'sort' => 10, 'description' => '用于非法分类测试', ]); $product = Product::query()->where('merchant_id', Merchant::query()->firstOrFail()->id)->orderBy('id')->firstOrFail(); $originalCategoryId = $product->category_id; $response = $this->from('/merchant-admin/products')->post('/merchant-admin/products/batch', [ 'product_ids' => [$product->id], 'action' => 'change_category', 'category_id' => $foreignCategory->id, ]); $response->assertRedirect('/merchant-admin/products'); $response->assertSessionHasErrors(['category_id']); $response->assertSessionHasErrors(['category_id' => '所选分类不存在或不属于当前商家。']); $this->assertSame($originalCategoryId, $product->fresh()->category_id); } }