chore: init saasshop repo + sql migrations runner + gitee go

This commit is contained in:
萝卜
2026-03-10 11:31:02 +00:00
commit 50f15cdea8
210 changed files with 29534 additions and 0 deletions

View File

@@ -0,0 +1,55 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminAccessTest extends TestCase
{
use RefreshDatabase;
public function test_admin_login_page_displays_new_console_naming(): void
{
$response = $this->get('/admin/login');
$response->assertOk();
$response->assertSee('总台管理登录');
$response->assertSee('登录总台管理');
}
public function test_platform_admin_can_login_and_open_dashboard_with_new_naming(): void
{
$this->seed();
$response = $this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
]);
$response->assertRedirect('/admin');
$dashboard = $this->get('/admin');
$dashboard->assertOk();
$dashboard->assertSee('总台管理');
$dashboard->assertSee('总台仪表盘');
$dashboard->assertSee('站点管理');
$dashboard->assertSee('订单监控');
$dashboard->assertSee('商品巡检');
$dashboard->assertSee('商品分类');
}
public function test_merchant_admin_cannot_login_from_admin_entry(): void
{
$this->seed();
$response = $this->from('/admin/login')->post('/admin/login', [
'email' => 'merchant.admin@demo.local',
'password' => 'Merchant@123456',
]);
$response->assertRedirect('/admin/login');
$this->get('/admin/login')->assertSee('当前账号是商家管理员,请从商家后台入口登录');
}
}

View File

@@ -0,0 +1,349 @@
<?php
namespace Tests\Feature;
use App\Models\Merchant;
use App\Models\Product;
use App\Models\ProductCategory;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
use function PHPUnit\Framework\assertStringContainsString;
class AdminBusinessPagesTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_admin_merchants_page_displays_site_management_content(): void
{
$this->loginAsPlatformAdmin();
$this->get('/admin/merchants')
->assertOk()
->assertSee('站点管理')
->assertSee('新增站点')
->assertSee('站点列表')
->assertSee('创建站点');
}
public function test_admin_orders_page_displays_filters_and_export_entry(): void
{
$this->loginAsPlatformAdmin();
$this->get('/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_admin_order_operations_focus_can_follow_current_filters(): void
{
$this->loginAsPlatformAdmin();
$this->get('/admin/orders?platform=wechat_mini&sort=latest')
->assertOk()
->assertSee('当前筛选已聚焦微信小程序订单,建议优先关注下单到支付转化是否顺畅,并同步排查小程序端支付回流体验。')
->assertSee('继续查看微信小程序订单');
$this->get('/admin/orders?payment_channel=wechat_pay&sort=latest')
->assertOk()
->assertSee('当前筛选已聚焦微信支付订单,建议优先核对支付成功率、回调稳定性与失败重试转化。')
->assertSee('继续查看微信支付订单');
$this->get('/admin/orders?device_type=mini-program&payment_status=failed&sort=latest')
->assertOk()
->assertSee('当前筛选已聚焦小程序环境订单,建议优先核对授权链路、支付唤起表现与下单回流是否顺畅。')
->assertSee('继续查看小程序环境订单');
$this->get('/admin/orders?device_type=mobile-webview&payment_status=failed&sort=latest')
->assertOk()
->assertSee('当前筛选已聚焦微信内网页订单,建议优先关注授权静默登录、页面跳转稳定性与支付拉起后的回流体验。')
->assertSee('继续查看微信内网页订单');
$this->get('/admin/orders?device_type=mobile&payment_status=failed&sort=latest')
->assertOk()
->assertSee('当前筛选已聚焦移动浏览器订单,建议优先关注 H5 下单链路、页面加载稳定性与支付转化流失点。')
->assertSee('继续查看移动浏览器订单');
$this->get('/admin/orders?device_type=desktop&payment_status=failed&sort=latest')
->assertOk()
->assertSee('当前筛选已聚焦桌面浏览器订单,建议优先关注 PC 端下单流程、页面首屏稳定性与高客单转化表现。')
->assertSee('继续查看桌面浏览器订单');
$this->get('/admin/orders?payment_status=failed&sort=latest')
->assertOk()
->assertSee('当前筛选已聚焦支付失败订单,建议优先排查支付渠道、回调结果与用户重试情况。')
->assertSee('继续查看支付失败订单');
$this->get('/admin/orders?status=completed&sort=latest')
->assertOk()
->assertSee('当前正在查看已完成订单,建议复盘高客单成交与复购来源,沉淀更稳定的转化路径。')
->assertSee('继续查看已完成订单');
}
public function test_admin_products_page_displays_import_and_history_entries(): void
{
$this->loginAsPlatformAdmin();
$this->get('/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_admin_product_operations_focus_can_follow_current_filters(): void
{
$this->loginAsPlatformAdmin();
$this->get('/admin/products?status=draft&sort=latest')
->assertOk()
->assertSee('当前正在查看草稿商品,建议优先补齐标题、分类、价格与库存后再安排上架。')
->assertSee('继续查看当前草稿');
$this->get('/admin/products?status=published&min_stock=0&max_stock=20&sort=stock_asc')
->assertOk()
->assertSee('当前筛选已聚焦已上架库存视角,建议优先确认低库存补货节奏,并同步观察高库存结构是否健康。')
->assertSee('继续查看当前库存视角');
$this->get('/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('/admin/products?status=published&category_id=1&keyword=%E6%BC%94%E7%A4%BA&sort=latest')
->assertOk()
->assertSee('当前筛选已聚焦已上架的“演示店铺 / 默认分类”分类下关键词“演示”命中的商品,建议优先核对在售商品命名、分类承接与站点承接是否一致,并同步观察价格带、库存结构与站点覆盖是否健康。')
->assertSee('继续查看当前已上架分类关键词商品');
$this->get('/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('/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('/admin/products?status=published&keyword=%E6%BC%94%E7%A4%BA&sort=latest')
->assertOk()
->assertSee('当前筛选已聚焦已上架商品中关键词“演示”命中的结果,建议优先核对在售商品命名、卖点表达与站点承接是否一致,并同步观察价格带、库存结构与站点覆盖是否健康。')
->assertSee('继续查看当前已上架关键词商品');
$this->get('/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('/admin/products?category_id=1&keyword=%E6%BC%94%E7%A4%BA&sort=latest')
->assertOk()
->assertSee('当前筛选已聚焦“演示店铺 / 默认分类”分类下关键词“演示”命中的商品,建议优先核对分类承接、命名卖点与站点承接是否一致,并同步观察相关商品的价格带、库存结构与站点覆盖。')
->assertSee('继续查看当前分类关键词商品');
$this->get('/admin/products?status=published&category_id=1&sort=latest')
->assertOk()
->assertSee('当前筛选已聚焦已上架的“演示店铺 / 默认分类”分类商品,建议优先核对该分类在售商品的价格带、库存结构与站点覆盖是否均衡。')
->assertSee('继续查看当前已上架分类商品');
$this->get('/admin/products?category_id=1&min_price=150&max_price=220&sort=price_desc')
->assertOk()
->assertSee('当前筛选已聚焦“演示店铺 / 默认分类”分类下价格带 ¥150.00 ~ ¥220.00 商品,建议优先核对该分类价格结构是否连贯,并同步观察库存分布、转化表现与站点覆盖是否健康。')
->assertSee('继续查看当前分类价格带商品');
$this->get('/admin/products?category_id=1&sort=latest')
->assertOk()
->assertSee('当前筛选已聚焦“演示店铺 / 默认分类”分类商品,建议优先核对分类承接是否准确,并同步观察价格带、库存结构与站点覆盖是否均衡。')
->assertSee('继续查看当前分类商品');
$this->get('/admin/products?keyword=%E6%BC%94%E7%A4%BA&sort=latest')
->assertOk()
->assertSee('当前筛选已聚焦关键词“演示”命中的商品,建议优先核对命名、卖点与站点承接是否一致,并同步观察相关商品的价格带与库存结构。')
->assertSee('继续查看当前关键词商品');
$this->get('/admin/products?min_price=150&max_price=220&sort=price_desc')
->assertOk()
->assertSee('当前筛选已聚焦价格带 ¥150.00 ~ ¥220.00 商品,建议优先核对定价梯度是否连贯,并同步观察库存结构、转化表现与站点覆盖是否匹配。')
->assertSee('继续查看当前价格带商品');
}
public function test_admin_import_histories_page_displays_filter_and_export_entries(): void
{
$this->loginAsPlatformAdmin();
$this->get('/admin/products/import-histories')
->assertOk()
->assertSee('平台商品导入历史')
->assertSee('筛选导入历史')
->assertSee('导出当前筛选 CSV')
->assertSee('返回商品页');
}
public function test_admin_product_summary_stats_match_export_summary_for_same_filters(): void
{
$this->loginAsPlatformAdmin();
$page = $this->get('/admin/products?merchant_id=1&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('/admin/products/export?merchant_id=1&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('站点,"1 / 演示店铺"', $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_admin_order_summary_stats_match_export_summary_for_same_filters(): void
{
$this->loginAsPlatformAdmin();
$page = $this->get('/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('/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_admin_products_batch_status_update_changes_selected_products(): void
{
$this->loginAsPlatformAdmin();
$products = Product::query()->orderBy('id')->take(2)->get();
$this->post('/admin/products/batch', [
'product_ids' => $products->pluck('id')->all(),
'action' => 'change_status',
'status' => 'offline',
])->assertRedirect('/admin/products')
->assertSessionHas('success', '批量操作已完成,本次更新 2 条商品。');
foreach ($products as $product) {
$this->assertSame('offline', $product->fresh()->status);
}
}
public function test_admin_products_batch_change_category_blocks_cross_merchant_operation(): void
{
$this->loginAsPlatformAdmin();
$secondMerchant = Merchant::query()->create([
'name' => '第二站点',
'slug' => 'merchant-2-demo',
'domain' => null,
'contact_name' => '测试人',
'contact_phone' => '13800000001',
'contact_email' => 'merchant2@example.com',
'plan' => 'basic',
'status' => 'active',
'activated_at' => now(),
'settings' => ['currency' => 'CNY'],
]);
$secondCategory = ProductCategory::query()->create([
'merchant_id' => $secondMerchant->id,
'name' => '第二站点分类',
'slug' => 'merchant-2-default',
'status' => 'active',
'sort' => 10,
'description' => '用于跨商家分类拦截测试',
]);
$secondProduct = Product::query()->create([
'merchant_id' => $secondMerchant->id,
'category_id' => $secondCategory->id,
'title' => '第二站点商品',
'slug' => 'merchant-2-product',
'sku' => 'SKU-MERCHANT-2-001',
'summary' => '用于跨商家批量改分类测试',
'content' => 'test',
'price' => 88,
'original_price' => 99,
'stock' => 10,
'status' => 'published',
'images' => [],
]);
$firstProduct = Product::query()->orderBy('id')->firstOrFail();
$firstMerchantCategory = ProductCategory::query()->where('merchant_id', $firstProduct->merchant_id)->orderBy('id')->firstOrFail();
$response = $this->from('/admin/products')->post('/admin/products/batch', [
'product_ids' => [$firstProduct->id, $secondProduct->id],
'action' => 'change_category',
'category_id' => $firstMerchantCategory->id,
]);
$response->assertRedirect('/admin/products');
$response->assertSessionHasErrors(['category_id']);
$this->assertStringContainsString('所选分类不属于商家 #', session('errors')->first('category_id'));
$this->assertSame($secondCategory->id, $secondProduct->fresh()->category_id);
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace Tests\Feature;
use App\Models\Plan;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlanExportTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_platform_admin_can_export_plans_csv(): void
{
$this->loginAsPlatformAdmin();
Plan::query()->create([
'code' => 'plan_export_01',
'name' => '套餐导出测试',
'billing_cycle' => 'monthly',
'price' => 9,
'list_price' => 19,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
'description' => '用于导出断言',
]);
$res = $this->get('/admin/plans/export');
$res->assertOk();
$res->assertHeader('content-type', 'text/csv; charset=UTF-8');
$content = $res->streamedContent();
$this->assertStringContainsString('套餐名称', $content);
$this->assertStringContainsString('套餐导出测试', $content);
$this->assertStringContainsString('plan_export_01', $content);
}
public function test_guest_cannot_export_plans_csv(): void
{
$this->seed();
$this->get('/admin/plans/export')
->assertRedirect('/admin/login');
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace Tests\Feature;
use App\Models\Plan;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlanSetStatusTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_platform_admin_can_set_plan_status(): void
{
$this->loginAsPlatformAdmin();
$plan = Plan::query()->create([
'code' => 'status_toggle_test',
'name' => '状态切换测试',
'billing_cycle' => 'monthly',
'price' => 9,
'list_price' => 9,
'status' => 'draft',
'sort' => 10,
'published_at' => null,
]);
$this->post('/admin/plans/' . $plan->id . '/set-status', [
'status' => 'active',
])->assertRedirect();
$plan->refresh();
$this->assertSame('active', $plan->status);
$this->assertNotNull($plan->published_at);
$this->post('/admin/plans/' . $plan->id . '/set-status', [
'status' => 'inactive',
])->assertRedirect();
$plan->refresh();
$this->assertSame('inactive', $plan->status);
}
public function test_guest_cannot_set_plan_status(): void
{
$this->seed();
$plan = Plan::query()->create([
'code' => 'status_toggle_guest_test',
'name' => '状态切换测试(guest)',
'billing_cycle' => 'monthly',
'price' => 9,
'list_price' => 9,
'status' => 'draft',
'sort' => 10,
]);
$this->post('/admin/plans/' . $plan->id . '/set-status', [
'status' => 'active',
])->assertRedirect('/admin/login');
}
}

View File

@@ -0,0 +1,144 @@
<?php
namespace Tests\Feature;
use App\Models\Plan;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlanTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_platform_admin_can_open_plans_page(): void
{
$this->loginAsPlatformAdmin();
$this->get('/admin/plans')
->assertOk()
->assertSee('套餐管理')
->assertSee('筛选条件')
->assertSee('套餐列表');
}
public function test_guest_cannot_open_plans_page(): void
{
$this->get('/admin/plans')->assertRedirect('/admin/login');
}
public function test_plans_page_can_filter_by_status_and_billing_cycle(): void
{
$this->loginAsPlatformAdmin();
Plan::query()->create([
'code' => 'basic_monthly',
'name' => '基础版(月付)',
'billing_cycle' => 'monthly',
'price' => 99,
'list_price' => 99,
'status' => 'active',
'sort' => 20,
'description' => '适合起步商家',
'published_at' => now(),
]);
Plan::query()->create([
'code' => 'enterprise_yearly_draft',
'name' => '企业版(年付草稿)',
'billing_cycle' => 'yearly',
'price' => 2999,
'list_price' => 3999,
'status' => 'draft',
'sort' => 30,
'description' => '仅供内部评审',
'published_at' => null,
]);
$this->get('/admin/plans?status=active&billing_cycle=monthly')
->assertOk()
->assertSee('基础版(月付)')
->assertDontSee('企业版(年付草稿)')
->assertSee('月付');
// 发布状态筛选(按 published_at基础版已发布企业版未发布
$this->get('/admin/plans?published=published')
->assertOk()
->assertSee('基础版(月付)')
->assertDontSee('企业版(年付草稿)');
$this->get('/admin/plans?published=unpublished')
->assertOk()
->assertSee('企业版(年付草稿)')
->assertDontSee('基础版(月付)');
}
public function test_platform_admin_can_create_plan(): void
{
$this->loginAsPlatformAdmin();
$this->get('/admin/plans/create')
->assertOk()
->assertSee('新建套餐')
->assertSee('套餐名称');
$this->post('/admin/plans', [
'code' => 'pro_monthly_form',
'name' => '专业版(月付)',
'billing_cycle' => 'monthly',
'price' => 199,
'list_price' => 299,
'status' => 'active',
'sort' => 5,
'description' => '主要面向成长型站点',
'published_at' => now()->format('Y-m-d H:i:s'),
])->assertRedirect('/admin/plans');
$this->get('/admin/plans')
->assertSee('专业版(月付)')
->assertSee('主要面向成长型站点');
}
public function test_platform_admin_can_update_plan(): void
{
$this->loginAsPlatformAdmin();
$plan = Plan::query()->create([
'code' => 'update_test',
'name' => '更新前套餐',
'billing_cycle' => 'monthly',
'price' => 50,
'list_price' => 80,
'status' => 'draft',
'sort' => 11,
]);
$this->get('/admin/plans/' . $plan->id . '/edit')
->assertOk()
->assertSee('编辑套餐');
$this->post('/admin/plans/' . $plan->id, [
'code' => 'update_test',
'name' => '更新后套餐',
'billing_cycle' => 'monthly',
'price' => 66,
'list_price' => 88,
'status' => 'active',
'sort' => 10,
'description' => '通过编辑表单更新',
])->assertRedirect('/admin/plans');
$this->get('/admin/plans')
->assertSee('更新后套餐')
->assertSee('¥66.00');
}
}

View File

@@ -0,0 +1,114 @@
<?php
namespace Tests\Feature;
use App\Models\Merchant;
use App\Models\Plan;
use App\Models\PlatformOrder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlatformOrderActivateSubscriptionErrorMetaTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_activation_failure_will_write_error_meta(): void
{
$this->loginAsPlatformAdmin();
$merchant = Merchant::query()->firstOrFail();
$plan = Plan::query()->create([
'code' => 'activate_error_meta_test',
'name' => '同步失败记录测试',
'billing_cycle' => 'monthly',
'price' => 9,
'list_price' => 9,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
// 构造一个“不满足 paid+activated”的订单触发失败
$order = PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_ACT_ERR_0001',
'order_type' => 'new_purchase',
'status' => 'pending',
'payment_status' => 'unpaid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 9,
'paid_amount' => 0,
'placed_at' => now(),
]);
$this->post('/admin/platform-orders/' . $order->id . '/activate-subscription')
->assertRedirect();
$order->refresh();
$this->assertNotEmpty(data_get($order->meta, 'subscription_activation_error.message'));
$this->assertNotEmpty(data_get($order->meta, 'subscription_activation_error.at'));
}
public function test_activation_success_will_clear_error_meta(): void
{
$this->loginAsPlatformAdmin();
$merchant = Merchant::query()->firstOrFail();
$plan = Plan::query()->create([
'code' => 'activate_error_meta_test2',
'name' => '同步成功清理失败记录测试',
'billing_cycle' => 'monthly',
'price' => 9,
'list_price' => 9,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
$order = PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_ACT_ERR_0002',
'order_type' => 'new_purchase',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 9,
'paid_amount' => 9,
'placed_at' => now(),
'paid_at' => now(),
'activated_at' => now(),
'meta' => [
'subscription_activation_error' => [
'message' => '历史错误',
'at' => now()->subDay()->toDateTimeString(),
'admin_id' => 1,
],
],
]);
$this->post('/admin/platform-orders/' . $order->id . '/activate-subscription')
->assertRedirect();
$order->refresh();
$this->assertEmpty(data_get($order->meta, 'subscription_activation_error'));
$this->assertNotNull($order->site_subscription_id);
}
}

View File

@@ -0,0 +1,104 @@
<?php
namespace Tests\Feature;
use App\Models\Merchant;
use App\Models\Plan;
use App\Models\PlatformOrder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlatformOrderActivateSubscriptionTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_platform_admin_can_activate_subscription_from_order(): void
{
$this->loginAsPlatformAdmin();
$merchant = Merchant::query()->firstOrFail();
$plan = Plan::query()->create([
'code' => 'activate_btn_test',
'name' => '按钮激活测试(月付)',
'billing_cycle' => 'monthly',
'price' => 66,
'list_price' => 66,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
$order = PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_BTN_ACT_0001',
'order_type' => 'new_purchase',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'list_amount' => 66,
'discount_amount' => 0,
'payable_amount' => 66,
'paid_amount' => 66,
'placed_at' => now()->subMinutes(10),
'paid_at' => now()->subMinutes(5),
'activated_at' => now()->subMinutes(1),
]);
$this->post('/admin/platform-orders/' . $order->id . '/activate-subscription')
->assertRedirect();
$order->refresh();
$this->assertNotNull($order->site_subscription_id);
}
public function test_guest_cannot_activate_subscription_from_order(): void
{
$this->seed();
$merchant = Merchant::query()->firstOrFail();
$plan = Plan::query()->create([
'code' => 'activate_btn_test_guest',
'name' => '按钮激活测试(游客)',
'billing_cycle' => 'monthly',
'price' => 66,
'list_price' => 66,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
$order = PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_BTN_ACT_0002',
'order_type' => 'new_purchase',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 66,
'paid_amount' => 66,
'placed_at' => now()->subMinutes(10),
]);
$this->post('/admin/platform-orders/' . $order->id . '/activate-subscription')
->assertStatus(302)
->assertRedirect('/admin/login');
}
}

View File

@@ -0,0 +1,374 @@
<?php
namespace Tests\Feature;
use App\Models\Merchant;
use App\Models\Plan;
use App\Models\PlatformOrder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use App\Models\SiteSubscription;
use App\Support\SubscriptionActivationService;
use Tests\TestCase;
class AdminPlatformOrderBatchActivateSubscriptionsTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_platform_admin_can_batch_activate_subscriptions_in_filtered_scope(): void
{
$this->loginAsPlatformAdmin();
$merchant = Merchant::query()->firstOrFail();
$plan = Plan::query()->create([
'code' => 'batch_activate_plan',
'name' => '批量同步测试套餐',
'billing_cycle' => 'monthly',
'price' => 10,
'list_price' => 10,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
// 可同步(命中):已支付 + 已生效 + 未同步
$syncable = PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_BATCH_SYNC_0001',
'order_type' => 'new_purchase',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 10,
'paid_amount' => 10,
'placed_at' => now()->subMinutes(10),
'paid_at' => now()->subMinutes(9),
'activated_at' => now()->subMinutes(8),
]);
// 不可同步(不应命中):未支付
PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_BATCH_SYNC_0002',
'order_type' => 'renewal',
'status' => 'activated',
'payment_status' => 'unpaid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 10,
'paid_amount' => 0,
'placed_at' => now()->subMinutes(5),
]);
$this->post('/admin/platform-orders/batch-activate-subscriptions', [
'scope' => 'filtered',
'syncable_only' => '1',
])->assertRedirect();
$syncable->refresh();
$this->assertNotNull($syncable->site_subscription_id);
$this->assertNotEmpty(data_get($syncable->meta, 'subscription_activation.subscription_id'));
$this->assertSame('batch_activate_subscription', data_get($syncable->meta, 'audit.0.action'));
}
public function test_platform_admin_batch_activate_records_failure_reason_and_summary(): void
{
$this->loginAsPlatformAdmin();
$merchant = Merchant::query()->firstOrFail();
$plan = Plan::query()->create([
'code' => 'batch_activate_fail_plan',
'name' => '批量同步失败治理测试套餐',
'billing_cycle' => 'monthly',
'price' => 10,
'list_price' => 10,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
$ok = PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_BATCH_FAIL_0001',
'order_type' => 'renewal',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 10,
'paid_amount' => 10,
'placed_at' => now()->subMinutes(10),
'paid_at' => now()->subMinutes(9),
'activated_at' => now()->subMinutes(8),
]);
// 强制失败:构造一条同样“可同步”的订单,然后在测试中用 DI 绑定一个假的 SubscriptionActivationService
// 让它在处理该订单时抛异常,从而验证失败原因落库与 Top 汇总。
$bad = PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_BATCH_FAIL_0002',
'order_type' => 'renewal',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 10,
'paid_amount' => 10,
'placed_at' => now()->subMinutes(7),
'paid_at' => now()->subMinutes(6),
'activated_at' => now()->subMinutes(5),
]);
$badId = $bad->id;
$this->app->bind(SubscriptionActivationService::class, function () use ($badId) {
return new class($badId) extends SubscriptionActivationService {
public function __construct(private int $badId) {}
public function activateOrder(int $orderId, ?int $adminId = null): SiteSubscription
{
if ($orderId === $this->badId) {
throw new \RuntimeException('模拟失败:订阅同步异常');
}
return parent::activateOrder($orderId, $adminId);
}
};
});
$res = $this->post('/admin/platform-orders/batch-activate-subscriptions', [
'scope' => 'filtered',
'syncable_only' => '1',
'limit' => 50,
]);
$res->assertRedirect()->assertSessionHas('success');
$ok->refresh();
$bad->refresh();
$this->assertNotNull($ok->site_subscription_id);
$this->assertNotEmpty(data_get($ok->meta, 'subscription_activation.subscription_id'));
$this->assertNotEmpty(data_get($bad->meta, 'subscription_activation_error.message'));
$this->assertNotEmpty(data_get($bad->meta, 'subscription_activation_error.at'));
// 批量结果摘要应包含失败原因Top
$msg = (string) $res->getSession()->get('success');
$this->assertStringContainsString('失败原因Top', $msg);
$this->assertStringContainsString('模拟失败:订阅同步异常', $msg);
}
public function test_platform_admin_batch_activate_respects_limit(): void
{
$this->loginAsPlatformAdmin();
$merchant = Merchant::query()->firstOrFail();
$plan = Plan::query()->create([
'code' => 'batch_activate_limit_plan',
'name' => '批量同步限额测试套餐',
'billing_cycle' => 'monthly',
'price' => 10,
'list_price' => 10,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
$o1 = PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_BATCH_LIMIT_0001',
'order_type' => 'renewal',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 10,
'paid_amount' => 10,
'placed_at' => now()->subMinutes(10),
'paid_at' => now()->subMinutes(9),
'activated_at' => now()->subMinutes(8),
]);
$o2 = PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_BATCH_LIMIT_0002',
'order_type' => 'renewal',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 10,
'paid_amount' => 10,
'placed_at' => now()->subMinutes(7),
'paid_at' => now()->subMinutes(6),
'activated_at' => now()->subMinutes(5),
]);
$o3 = PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_BATCH_LIMIT_0003',
'order_type' => 'renewal',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 10,
'paid_amount' => 10,
'placed_at' => now()->subMinutes(4),
'paid_at' => now()->subMinutes(3),
'activated_at' => now()->subMinutes(2),
]);
$this->post('/admin/platform-orders/batch-activate-subscriptions', [
'scope' => 'filtered',
'syncable_only' => '1',
'limit' => 1,
])->assertRedirect();
$o1->refresh();
$o2->refresh();
$o3->refresh();
// 当前实现按 id 倒序优先处理最新订单,因此 limit=1 时应只处理 o3
$this->assertNull($o1->site_subscription_id);
$this->assertNull($o2->site_subscription_id);
$this->assertNotNull($o3->site_subscription_id);
}
public function test_platform_admin_batch_activate_requires_syncable_only_filter(): void
{
$this->loginAsPlatformAdmin();
$merchant = Merchant::query()->firstOrFail();
$plan = Plan::query()->create([
'code' => 'batch_activate_guard_plan',
'name' => '批量同步防误操作测试套餐',
'billing_cycle' => 'monthly',
'price' => 10,
'list_price' => 10,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
$syncable = PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_BATCH_GUARD_0001',
'order_type' => 'new_purchase',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 10,
'paid_amount' => 10,
'placed_at' => now()->subMinutes(10),
'paid_at' => now()->subMinutes(9),
'activated_at' => now()->subMinutes(8),
]);
$this->post('/admin/platform-orders/batch-activate-subscriptions', [
'scope' => 'filtered',
// 故意不传 syncable_only
])->assertRedirect()->assertSessionHas('warning');
$syncable->refresh();
$this->assertNull($syncable->site_subscription_id);
}
public function test_platform_admin_batch_activate_scope_all_requires_confirm_yes(): void
{
$this->loginAsPlatformAdmin();
$merchant = Merchant::query()->firstOrFail();
$plan = Plan::query()->create([
'code' => 'batch_activate_all_guard_plan',
'name' => '批量同步全量二次确认测试套餐',
'billing_cycle' => 'monthly',
'price' => 10,
'list_price' => 10,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
$syncable = PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_BATCH_ALL_GUARD_0001',
'order_type' => 'new_purchase',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 10,
'paid_amount' => 10,
'placed_at' => now()->subMinutes(10),
'paid_at' => now()->subMinutes(9),
'activated_at' => now()->subMinutes(8),
]);
// 未确认:不应执行
$this->post('/admin/platform-orders/batch-activate-subscriptions', [
'scope' => 'all',
'limit' => 50,
])->assertRedirect()->assertSessionHas('warning');
$syncable->refresh();
$this->assertNull($syncable->site_subscription_id);
// 确认 YES应执行
$this->post('/admin/platform-orders/batch-activate-subscriptions', [
'scope' => 'all',
'confirm' => 'YES',
'limit' => 50,
])->assertRedirect()->assertSessionHas('success');
$syncable->refresh();
$this->assertNotNull($syncable->site_subscription_id);
}
public function test_guest_cannot_batch_activate_subscriptions(): void
{
$this->seed();
$this->post('/admin/platform-orders/batch-activate-subscriptions', [
'scope' => 'filtered',
])->assertRedirect('/admin/login');
}
}

View File

@@ -0,0 +1,102 @@
<?php
namespace Tests\Feature;
use App\Models\Merchant;
use App\Models\Plan;
use App\Models\PlatformOrder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlatformOrderClearSyncErrorsFilteredTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_platform_admin_can_clear_sync_errors_only_in_filtered_scope(): void
{
$this->loginAsPlatformAdmin();
$merchant = Merchant::query()->firstOrFail();
$plan = Plan::query()->create([
'code' => 'clear_sync_err_filtered_test',
'name' => '清理失败标记(范围)测试',
'billing_cycle' => 'monthly',
'price' => 1,
'list_price' => 1,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
$failedA = PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_CLEAR_ERR_F_0001',
'order_type' => 'new_purchase',
'status' => 'pending',
'payment_status' => 'unpaid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 1,
'paid_amount' => 0,
'placed_at' => now(),
'meta' => [
'subscription_activation_error' => [
'message' => '模拟失败A',
'at' => now()->toDateTimeString(),
'admin_id' => 1,
],
],
]);
$failedB = PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_CLEAR_ERR_F_0002',
'order_type' => 'new_purchase',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 1,
'paid_amount' => 1,
'placed_at' => now(),
'meta' => [
'subscription_activation_error' => [
'message' => '模拟失败B',
'at' => now()->toDateTimeString(),
'admin_id' => 1,
],
],
]);
// 仅清理“status=pending”范围内的失败标记应该只清掉 failedA
$this->post('/admin/platform-orders/clear-sync-errors', [
'scope' => 'filtered',
'status' => 'pending',
])->assertRedirect();
$failedA->refresh();
$failedB->refresh();
$this->assertEmpty(data_get($failedA->meta, 'subscription_activation_error'));
$this->assertNotEmpty(data_get($failedA->meta, 'audit.0.action'));
$this->assertSame('clear_sync_error', data_get($failedA->meta, 'audit.0.action'));
$this->assertNotEmpty(data_get($failedB->meta, 'subscription_activation_error.message'));
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace Tests\Feature;
use App\Models\Merchant;
use App\Models\Plan;
use App\Models\PlatformOrder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlatformOrderClearSyncErrorsTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_platform_admin_can_clear_sync_errors(): void
{
$this->loginAsPlatformAdmin();
$merchant = Merchant::query()->firstOrFail();
$plan = Plan::query()->create([
'code' => 'clear_sync_err_test',
'name' => '清理失败标记测试',
'billing_cycle' => 'monthly',
'price' => 1,
'list_price' => 1,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
$order = PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_CLEAR_ERR_0001',
'order_type' => 'new_purchase',
'status' => 'pending',
'payment_status' => 'unpaid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 1,
'paid_amount' => 0,
'placed_at' => now(),
'meta' => [
'subscription_activation_error' => [
'message' => '模拟失败',
'at' => now()->toDateTimeString(),
'admin_id' => 1,
],
],
]);
$this->post('/admin/platform-orders/clear-sync-errors', ['scope' => 'all'])
->assertRedirect();
$order->refresh();
$this->assertEmpty(data_get($order->meta, 'subscription_activation_error'));
}
public function test_guest_cannot_clear_sync_errors(): void
{
$this->seed();
$this->post('/admin/platform-orders/clear-sync-errors', ['scope' => 'all'])
->assertRedirect('/admin/login');
}
}

View File

@@ -0,0 +1,148 @@
<?php
namespace Tests\Feature;
use App\Models\Merchant;
use App\Models\Plan;
use App\Models\PlatformOrder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlatformOrderExportTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_platform_admin_can_export_platform_orders_csv(): void
{
$this->loginAsPlatformAdmin();
$merchant = Merchant::query()->firstOrFail();
$plan = Plan::query()->create([
'code' => 'export_order_test',
'name' => '导出测试套餐',
'billing_cycle' => 'monthly',
'price' => 10,
'list_price' => 10,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_EXPORT_0001',
'order_type' => 'new_purchase',
'status' => 'pending',
'payment_status' => 'unpaid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 10,
'paid_amount' => 0,
'placed_at' => now(),
'meta' => [
'subscription_activation_error' => [
'message' => '模拟失败',
'at' => now()->toDateTimeString(),
'admin_id' => 1,
],
],
]);
// batch_synced_24h 筛选导出:构造一条 24h 内、一条超 24h 的订单
PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_EXPORT_BATCH_RECENT',
'order_type' => 'renewal',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 10,
'paid_amount' => 10,
'placed_at' => now(),
'paid_at' => now(),
'activated_at' => now(),
'meta' => [
'batch_activation' => [
'at' => now()->toDateTimeString(),
'admin_id' => 1,
'scope' => 'filtered',
],
],
]);
PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_EXPORT_BATCH_OLD',
'order_type' => 'renewal',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 10,
'paid_amount' => 10,
'placed_at' => now()->subHours(30),
'paid_at' => now()->subHours(30),
'activated_at' => now()->subHours(30),
'meta' => [
'batch_activation' => [
'at' => now()->subHours(30)->toDateTimeString(),
'admin_id' => 1,
'scope' => 'filtered',
],
],
]);
$res = $this->get('/admin/platform-orders/export');
$res->assertOk();
$res->assertHeader('content-type', 'text/csv; charset=UTF-8');
// StreamedResponse 在测试环境下需用 streamedContent() 获取内容
$content = $res->streamedContent();
$this->assertStringContainsString('订单号', $content);
$this->assertStringContainsString('PO_EXPORT_0001', $content);
$this->assertStringContainsString('同步失败原因', $content);
// include_meta=1 时应包含 meta(JSON) 列
$res2 = $this->get('/admin/platform-orders/export?include_meta=1');
$res2->assertOk();
$content2 = $res2->streamedContent();
$this->assertStringContainsString('原始meta(JSON)', $content2);
$this->assertStringContainsString('subscription_activation_error', $content2);
// batch_synced_24h=1 导出应只包含 24h 内批量同步过的订单
$res3 = $this->get('/admin/platform-orders/export?batch_synced_24h=1');
$res3->assertOk();
$content3 = $res3->streamedContent();
$this->assertStringContainsString('PO_EXPORT_BATCH_RECENT', $content3);
$this->assertStringNotContainsString('PO_EXPORT_BATCH_OLD', $content3);
}
public function test_guest_cannot_export_platform_orders_csv(): void
{
$this->seed();
$this->get('/admin/platform-orders/export')
->assertRedirect('/admin/login');
}
}

View File

@@ -0,0 +1,103 @@
<?php
namespace Tests\Feature;
use App\Models\Merchant;
use App\Models\Plan;
use App\Models\PlatformOrder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlatformOrderMarkPaidAndActivateTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_platform_admin_can_mark_order_paid_and_activated_and_sync_subscription(): void
{
$this->loginAsPlatformAdmin();
$merchant = Merchant::query()->firstOrFail();
$plan = Plan::query()->create([
'code' => 'mark_paid_test',
'name' => '标记支付测试(月付)',
'billing_cycle' => 'monthly',
'price' => 30,
'list_price' => 30,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
$order = PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_MARK_PAID_0001',
'order_type' => 'new_purchase',
'status' => 'pending',
'payment_status' => 'unpaid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 30,
'paid_amount' => 0,
'placed_at' => now(),
]);
$this->post('/admin/platform-orders/' . $order->id . '/mark-paid-and-activate')
->assertRedirect();
$order->refresh();
$this->assertSame('paid', $order->payment_status);
$this->assertSame('activated', $order->status);
$this->assertNotNull($order->paid_at);
$this->assertNotNull($order->activated_at);
$this->assertNotNull($order->site_subscription_id);
}
public function test_guest_cannot_mark_order_paid_and_activated(): void
{
$this->seed();
$merchant = Merchant::query()->firstOrFail();
$plan = Plan::query()->create([
'code' => 'mark_paid_test_guest',
'name' => '标记支付测试(游客)',
'billing_cycle' => 'monthly',
'price' => 30,
'list_price' => 30,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
$order = PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_MARK_PAID_0002',
'order_type' => 'new_purchase',
'status' => 'pending',
'payment_status' => 'unpaid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 30,
'paid_amount' => 0,
'placed_at' => now(),
]);
$this->post('/admin/platform-orders/' . $order->id . '/mark-paid-and-activate')
->assertRedirect('/admin/login');
}
}

View File

@@ -0,0 +1,123 @@
<?php
namespace Tests\Feature;
use App\Models\Merchant;
use App\Models\Plan;
use App\Models\PlatformOrder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlatformOrderShowTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_platform_admin_can_open_platform_order_show_page(): void
{
$this->loginAsPlatformAdmin();
$merchant = Merchant::query()->firstOrFail();
$plan = Plan::query()->create([
'code' => 'show_order_test',
'name' => '订单详情测试',
'billing_cycle' => 'monthly',
'price' => 10,
'list_price' => 10,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
$order = PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_SHOW_0001',
'order_type' => 'new_purchase',
'status' => 'pending',
'payment_status' => 'unpaid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 10,
'paid_amount' => 0,
'placed_at' => now(),
'meta' => [
'subscription_activation_error' => [
'message' => 'demo',
'at' => now()->toDateTimeString(),
],
'audit' => [
[
'action' => 'clear_sync_error',
'scope' => 'filtered',
'at' => now()->toDateTimeString(),
'admin_id' => 1,
],
[
'action' => 'batch_activate_subscription',
'scope' => 'filtered',
'at' => now()->toDateTimeString(),
'admin_id' => 1,
],
],
],
]);
$this->get('/admin/platform-orders/' . $order->id)
->assertOk()
->assertSee('平台订单详情')
->assertSee('PO_SHOW_0001')
->assertSee('标记支付并生效')
->assertSee('同步订阅')
->assertSee('订阅同步元数据')
->assertSee('审计记录')
->assertSee('清除同步失败标记');
}
public function test_guest_cannot_open_platform_order_show_page(): void
{
$this->seed();
$merchant = Merchant::query()->firstOrFail();
$plan = Plan::query()->create([
'code' => 'show_order_test_guest',
'name' => '订单详情测试(guest)',
'billing_cycle' => 'monthly',
'price' => 10,
'list_price' => 10,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
$order = PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_SHOW_0002',
'order_type' => 'new_purchase',
'status' => 'pending',
'payment_status' => 'unpaid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 10,
'paid_amount' => 0,
'placed_at' => now(),
]);
$this->get('/admin/platform-orders/' . $order->id)
->assertRedirect('/admin/login');
}
}

View File

@@ -0,0 +1,319 @@
<?php
namespace Tests\Feature;
use App\Models\Merchant;
use App\Models\Plan;
use App\Models\PlatformOrder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminPlatformOrderTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_platform_admin_can_open_platform_orders_page(): void
{
$this->loginAsPlatformAdmin();
$this->get('/admin/platform-orders')
->assertOk()
->assertSee('平台订单')
->assertSee('筛选条件')
->assertSee('平台订单列表')
->assertSee('最近批量同步')
->assertSee('最近24小时批量同步过');
}
public function test_guest_cannot_open_platform_orders_page(): void
{
$this->get('/admin/platform-orders')
->assertRedirect('/admin/login');
}
public function test_platform_orders_page_can_filter_by_status_and_payment_status(): void
{
$this->loginAsPlatformAdmin();
$merchant = Merchant::query()->firstOrFail();
$plan = Plan::query()->create([
'code' => 'pro_monthly_test',
'name' => '专业版(月付)',
'billing_cycle' => 'monthly',
'price' => 199,
'list_price' => 199,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO202603100101',
'order_type' => 'new_purchase',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => '专业版(月付)',
'billing_cycle' => 'monthly',
'period_months' => 1,
'quantity' => 1,
'list_amount' => 199,
'discount_amount' => 0,
'payable_amount' => 199,
'paid_amount' => 199,
'placed_at' => now()->subHour(),
'paid_at' => now()->subMinutes(30),
'activated_at' => now()->subMinutes(20),
]);
PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO202603100102',
'order_type' => 'renewal',
'status' => 'pending',
'payment_status' => 'unpaid',
'plan_name' => '专业版(月付)',
'billing_cycle' => 'monthly',
'period_months' => 1,
'quantity' => 1,
'list_amount' => 199,
'discount_amount' => 0,
'payable_amount' => 199,
'paid_amount' => 0,
'placed_at' => now()->subMinutes(10),
]);
$this->get('/admin/platform-orders?status=activated&payment_status=paid')
->assertOk()
->assertSee('PO202603100101')
->assertDontSee('PO202603100102')
->assertSee('专业版(月付)');
// 只看同步失败:构造一条带有 subscription_activation_error 的订单
PlatformOrder::query()->where('order_no', 'PO202603100102')->update([
'meta' => [
'subscription_activation_error' => [
'message' => '模拟失败',
'at' => now()->toDateTimeString(),
'admin_id' => 1,
],
],
]);
// 额外构造一条同原因失败,用于失败原因聚合统计
PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO202603100104',
'order_type' => 'renewal',
'status' => 'pending',
'payment_status' => 'unpaid',
'plan_name' => '专业版(月付)',
'billing_cycle' => 'monthly',
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 199,
'paid_amount' => 0,
'placed_at' => now()->subMinutes(9),
'meta' => [
'subscription_activation_error' => [
'message' => '模拟失败',
'at' => now()->toDateTimeString(),
'admin_id' => 1,
],
],
]);
// 再构造一条不同原因失败
PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO202603100105',
'order_type' => 'renewal',
'status' => 'pending',
'payment_status' => 'unpaid',
'plan_name' => '专业版(月付)',
'billing_cycle' => 'monthly',
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 199,
'paid_amount' => 0,
'placed_at' => now()->subMinutes(8),
'meta' => [
'subscription_activation_error' => [
'message' => '余额不足',
'at' => now()->toDateTimeString(),
'admin_id' => 1,
],
],
]);
$this->get('/admin/platform-orders?fail_only=1')
->assertOk()
->assertSee('PO202603100102')
->assertDontSee('PO202603100101');
// 失败原因聚合Top5应可见
$this->get('/admin/platform-orders')
->assertOk()
->assertSee('同步失败原因 TOP5')
->assertSee('模拟失败')
->assertSee('余额不足');
// 只看已同步:构造一条带有 subscription_activation 的订单 + 批量同步审计(用于列表展示)
PlatformOrder::query()->where('order_no', 'PO202603100101')->update([
'meta' => [
'subscription_activation' => [
'subscription_id' => 123,
'synced_at' => now()->toDateTimeString(),
'admin_id' => 1,
],
'audit' => [
[
'action' => 'batch_activate_subscription',
'scope' => 'filtered',
'at' => '2026-03-10 00:00:00',
'admin_id' => 1,
],
],
],
]);
$this->get('/admin/platform-orders')
->assertOk()
->assertSee('2026-03-10 00:00:00')
->assertSee('管理员1');
// 最近24小时批量同步筛选构造一条近24h一条超过24h
PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO202603100107',
'order_type' => 'renewal',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => '专业版(月付)',
'billing_cycle' => 'monthly',
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 199,
'paid_amount' => 199,
'placed_at' => now()->subMinutes(3),
'paid_at' => now()->subMinutes(2),
'activated_at' => now()->subMinutes(1),
'meta' => [
'batch_activation' => [
'at' => now()->subHours(1)->format('Y-m-d H:i:s'),
'admin_id' => 1,
'scope' => 'filtered',
],
],
]);
PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO202603100108',
'order_type' => 'renewal',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => '专业版(月付)',
'billing_cycle' => 'monthly',
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 199,
'paid_amount' => 199,
'placed_at' => now()->subMinutes(13),
'paid_at' => now()->subMinutes(12),
'activated_at' => now()->subMinutes(11),
'meta' => [
'batch_activation' => [
'at' => now()->subHours(30)->format('Y-m-d H:i:s'),
'admin_id' => 1,
'scope' => 'filtered',
],
],
]);
$this->get('/admin/platform-orders?batch_synced_24h=1')
->assertOk()
->assertSee('PO202603100107')
->assertDontSee('PO202603100108');
$this->get('/admin/platform-orders?synced_only=1')
->assertOk()
->assertSee('PO202603100101')
->assertDontSee('PO202603100102');
// sync_status 下拉筛选synced / failed / unsynced
$this->get('/admin/platform-orders?sync_status=synced')
->assertOk()
->assertSee('PO202603100101')
->assertDontSee('PO202603100102');
$this->get('/admin/platform-orders?sync_status=failed')
->assertOk()
->assertSee('PO202603100102')
->assertDontSee('PO202603100101');
// unsynced构造一条既无 subscription_activation 也无 error 的订单
PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO202603100103',
'order_type' => 'renewal',
'status' => 'pending',
'payment_status' => 'unpaid',
'plan_name' => '专业版(月付)',
'billing_cycle' => 'monthly',
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 199,
'paid_amount' => 0,
'placed_at' => now()->subMinutes(5),
]);
$this->get('/admin/platform-orders?sync_status=unsynced')
->assertOk()
->assertSee('PO202603100103')
->assertDontSee('PO202603100101')
->assertDontSee('PO202603100102');
// 只看可同步:已支付 + 已生效 + 未同步
PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO202603100106',
'order_type' => 'renewal',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => '专业版(月付)',
'billing_cycle' => 'monthly',
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 199,
'paid_amount' => 199,
'placed_at' => now()->subMinutes(4),
'paid_at' => now()->subMinutes(3),
'activated_at' => now()->subMinutes(2),
]);
$this->get('/admin/platform-orders?syncable_only=1')
->assertOk()
->assertSee('PO202603100106')
->assertDontSee('PO202603100101')
->assertDontSee('PO202603100102');
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminProtectedPagesTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_guest_is_redirected_to_admin_login_for_protected_pages(): void
{
foreach (['/admin', '/admin/merchants', '/admin/orders', '/admin/products', '/admin/product-categories'] as $path) {
$this->get($path)->assertRedirect('/admin/login');
}
}
public function test_platform_admin_can_access_key_admin_pages(): void
{
$this->loginAsPlatformAdmin();
$this->get('/admin')->assertOk()->assertSee('总台仪表盘');
$this->get('/admin/merchants')->assertOk()->assertSee('站点管理');
$this->get('/admin/orders')->assertOk()->assertSee('订单监控');
$this->get('/admin/products')->assertOk()->assertSee('商品巡检');
$this->get('/admin/product-categories')->assertOk()->assertSee('商品分类');
}
public function test_merchant_admin_is_forbidden_from_admin_pages(): void
{
$this->seed();
$this->post('/merchant-admin/login', [
'email' => 'merchant.admin@demo.local',
'password' => 'Merchant@123456',
])->assertRedirect('/merchant-admin');
$this->get('/admin')->assertForbidden()->assertSee('当前账号没有总台管理访问权限');
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace Tests\Feature;
use App\Models\Merchant;
use App\Models\Plan;
use App\Models\SiteSubscription;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminSiteSubscriptionExportTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_platform_admin_can_export_site_subscriptions_csv(): void
{
$this->loginAsPlatformAdmin();
$merchant = Merchant::query()->firstOrFail();
$plan = Plan::query()->create([
'code' => 'export_sub_test',
'name' => '导出订阅测试套餐',
'billing_cycle' => 'monthly',
'price' => 88,
'list_price' => 88,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
SiteSubscription::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'status' => 'activated',
'source' => 'manual',
'subscription_no' => 'SUB_EXPORT_0001',
'plan_name' => $plan->name,
'billing_cycle' => 'monthly',
'period_months' => 1,
'amount' => 88,
'starts_at' => now()->subDays(1),
'ends_at' => now()->addDays(29),
'activated_at' => now()->subDays(1),
]);
$res = $this->get('/admin/site-subscriptions/export');
$res->assertOk();
$res->assertHeader('content-type', 'text/csv; charset=UTF-8');
$content = $res->streamedContent();
$this->assertStringContainsString('订阅号', $content);
$this->assertStringContainsString('SUB_EXPORT_0001', $content);
$this->assertStringContainsString('到期时间', $content);
$this->assertStringContainsString('到期状态', $content);
$this->assertStringContainsString('未到期', $content);
// 状态导出应为“中文标签 + 原始值”
$this->assertStringContainsString('已生效 (activated)', $content);
}
public function test_guest_cannot_export_site_subscriptions_csv(): void
{
$this->seed();
$this->get('/admin/site-subscriptions/export')
->assertRedirect('/admin/login');
}
}

View File

@@ -0,0 +1,143 @@
<?php
namespace Tests\Feature;
use App\Models\Merchant;
use App\Models\Plan;
use App\Models\SiteSubscription;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class AdminSiteSubscriptionTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
public function test_platform_admin_can_open_site_subscriptions_page(): void
{
$this->loginAsPlatformAdmin();
$this->get('/admin/site-subscriptions')
->assertOk()
->assertSee('订阅管理')
->assertSee('筛选条件')
->assertSee('订阅列表')
->assertSee('到期状态');
}
public function test_guest_cannot_open_site_subscriptions_page(): void
{
$this->get('/admin/site-subscriptions')
->assertRedirect('/admin/login');
}
public function test_site_subscriptions_page_can_filter_by_status_and_keyword(): void
{
$this->loginAsPlatformAdmin();
$merchant = Merchant::query()->firstOrFail();
$plan = Plan::query()->create([
'code' => 'sub_test_monthly',
'name' => '订阅测试套餐(月付)',
'billing_cycle' => 'monthly',
'price' => 88,
'list_price' => 88,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
SiteSubscription::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'status' => 'activated',
'source' => 'manual',
'subscription_no' => 'SUB202603100101',
'plan_name' => '订阅测试套餐(月付)',
'billing_cycle' => 'monthly',
'period_months' => 1,
'amount' => 88,
'starts_at' => now()->subDays(2),
'ends_at' => now()->addDays(28),
'activated_at' => now()->subDays(2),
]);
SiteSubscription::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'status' => 'pending',
'source' => 'manual',
'subscription_no' => 'SUB202603100102',
'plan_name' => '订阅测试套餐(月付)',
'billing_cycle' => 'monthly',
'period_months' => 1,
'amount' => 88,
'starts_at' => now(),
'ends_at' => now()->addMonth(),
]);
$this->get('/admin/site-subscriptions?status=activated&keyword=SUB202603100101')
->assertOk()
->assertSee('SUB202603100101')
->assertDontSee('SUB202603100102');
// 到期筛选构造“已过期”和“7天内到期”的订阅
SiteSubscription::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'status' => 'activated',
'source' => 'manual',
'subscription_no' => 'SUB202603100103',
'plan_name' => '订阅测试套餐(月付)',
'billing_cycle' => 'monthly',
'period_months' => 1,
'amount' => 88,
'starts_at' => now()->subDays(40),
'ends_at' => now()->subDays(1),
'activated_at' => now()->subDays(40),
]);
SiteSubscription::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'status' => 'activated',
'source' => 'manual',
'subscription_no' => 'SUB202603100104',
'plan_name' => '订阅测试套餐(月付)',
'billing_cycle' => 'monthly',
'period_months' => 1,
'amount' => 88,
'starts_at' => now()->subDays(25),
'ends_at' => now()->addDays(3),
'activated_at' => now()->subDays(25),
]);
$this->get('/admin/site-subscriptions?expiry=expired')
->assertOk()
->assertSeeInOrder(['SUB202603100103', '已过期'])
->assertDontSee('SUB202603100104');
$this->get('/admin/site-subscriptions?expiry=expiring_7d')
->assertOk()
->assertSeeInOrder(['SUB202603100104', '7天内到期'])
->assertDontSee('SUB202603100103');
// 新增筛选:按站点/套餐
$this->get('/admin/site-subscriptions?merchant_id=' . $merchant->id)
->assertOk()
->assertSee('订阅列表');
$this->get('/admin/site-subscriptions?plan_id=' . $plan->id)
->assertOk()
->assertSee('订阅列表');
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ExampleTest extends TestCase
{
use RefreshDatabase;
/**
* A basic test example.
*/
public function test_the_application_returns_a_successful_response(): void
{
$response = $this->get('/');
$response->assertStatus(200);
}
}

View File

@@ -0,0 +1,177 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
use function PHPUnit\Framework\assertStringContainsString;
use function PHPUnit\Framework\assertStringNotContainsString;
class ExportEndpointsTest extends TestCase
{
use RefreshDatabase;
protected function loginAsPlatformAdmin(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
}
protected function loginAsMerchantAdmin(): void
{
$this->seed();
$this->post('/merchant-admin/login', [
'email' => 'merchant.admin@demo.local',
'password' => 'Merchant@123456',
])->assertRedirect('/merchant-admin');
}
protected function loginAsSiteAdmin(): void
{
$this->seed();
$this->post('/site-admin/login', [
'email' => 'merchant.admin@demo.local',
'password' => 'Merchant@123456',
])->assertRedirect('/site-admin');
}
public function test_admin_order_and_product_exports_return_csv_response(): void
{
$this->loginAsPlatformAdmin();
$this->get('/admin/orders/export')
->assertOk()
->assertHeader('content-type', 'text/csv; charset=UTF-8');
$this->get('/admin/products/export')
->assertOk()
->assertHeader('content-type', 'text/csv; charset=UTF-8');
$this->get('/admin/products/import-histories/export')
->assertOk()
->assertHeader('content-type', 'text/csv; charset=UTF-8');
}
public function test_admin_product_export_uses_chinese_detail_headers(): void
{
$this->loginAsPlatformAdmin();
$response = $this->get('/admin/products/export');
$response->assertOk();
$content = $response->streamedContent();
assertStringContainsString('ID,站点ID,站点名称,分类ID,分类名称,分类标识,商品标题,商品Slug,SKU,售价,划线价,库存,状态,商品简介,创建时间,更新时间', $content);
}
public function test_merchant_order_and_product_exports_return_csv_response(): void
{
$this->loginAsMerchantAdmin();
$this->get('/merchant-admin/orders/export')
->assertOk()
->assertHeader('content-type', 'text/csv; charset=UTF-8');
$this->get('/merchant-admin/products/export')
->assertOk()
->assertHeader('content-type', 'text/csv; charset=UTF-8');
$this->get('/merchant-admin/products/import-histories/export')
->assertOk()
->assertHeader('content-type', 'text/csv; charset=UTF-8');
}
public function test_merchant_product_export_uses_chinese_detail_headers(): void
{
$this->loginAsMerchantAdmin();
$response = $this->get('/merchant-admin/products/export');
$response->assertOk();
$content = $response->streamedContent();
assertStringContainsString('ID,分类ID,分类名称,分类标识,商品标题,商品Slug,SKU,售价,划线价,库存,状态,商品简介,创建时间,更新时间', $content);
}
public function test_admin_order_export_uses_chinese_detail_headers(): void
{
$this->loginAsPlatformAdmin();
$response = $this->get('/admin/orders/export');
$response->assertOk();
$content = $response->streamedContent();
assertStringContainsString('ID,商家ID,商家名称,用户ID,订单号,订单状态,支付状态,平台,设备类型,支付渠道,买家姓名,买家手机,买家邮箱,商品金额,优惠金额,运费,实付金额,商品行数,商品件数,商品摘要,创建时间,支付时间,发货时间,完成时间,备注', $content);
assertStringContainsString('设备类型,全部', $content);
assertStringContainsString('支付渠道,全部', $content);
assertStringContainsString('桌面浏览器,微信支付', $content);
assertStringContainsString('移动浏览器,支付宝', $content);
assertStringContainsString('小程序环境,微信支付', $content);
assertStringContainsString('微信内网页,微信支付', $content);
assertStringContainsString('"APP 接口预留","APP 接口",微信支付', $content);
assertStringNotContainsString('ID,merchant_id,merchant_name', $content);
assertStringNotContainsString(',desktop,wechat_pay,', $content);
assertStringNotContainsString(',mobile,alipay,', $content);
assertStringNotContainsString(',mini-program,wechat_pay,', $content);
}
public function test_merchant_order_export_uses_chinese_detail_headers(): void
{
$this->loginAsMerchantAdmin();
$response = $this->get('/merchant-admin/orders/export');
$response->assertOk();
$content = $response->streamedContent();
assertStringContainsString('ID,用户ID,订单号,订单状态,支付状态,平台,设备类型,支付渠道,买家姓名,买家手机,买家邮箱,商品金额,优惠金额,运费,实付金额,商品行数,商品件数,商品摘要,创建时间,支付时间,发货时间,完成时间,备注', $content);
assertStringContainsString('设备类型,全部', $content);
assertStringContainsString('支付渠道,全部', $content);
assertStringContainsString('桌面浏览器,微信支付', $content);
assertStringContainsString('移动浏览器,支付宝', $content);
assertStringContainsString('小程序环境,微信支付', $content);
assertStringContainsString('微信内网页,微信支付', $content);
assertStringContainsString('"APP 接口预留","APP 接口",微信支付', $content);
assertStringNotContainsString('ID,user_id,order_no,status,payment_status,platform', $content);
assertStringNotContainsString(',desktop,wechat_pay,', $content);
assertStringNotContainsString(',mobile,alipay,', $content);
assertStringNotContainsString(',mini-program,wechat_pay,', $content);
}
public function test_site_product_export_uses_chinese_detail_headers(): void
{
$this->loginAsSiteAdmin();
$response = $this->get('/site-admin/products/export');
$response->assertOk();
$content = $response->streamedContent();
assertStringContainsString('ID,分类ID,分类名称,商品标题,商品Slug,SKU,售价,划线价,库存,状态,商品简介,创建时间,更新时间', $content);
assertStringNotContainsString('ID,category_id,category_name,title,slug,sku,price,original_price,stock,status,summary,created_at,updated_at', $content);
}
public function test_site_order_export_uses_chinese_detail_headers(): void
{
$this->loginAsSiteAdmin();
$response = $this->get('/site-admin/orders/export');
$response->assertOk();
$content = $response->streamedContent();
assertStringContainsString('ID,订单号,订单状态,支付状态,平台,设备类型,支付渠道,买家姓名,买家手机,买家邮箱,商品金额,优惠金额,运费,实付金额,创建时间,支付时间,完成时间,备注', $content);
assertStringContainsString('设备类型,全部', $content);
assertStringContainsString('支付渠道,全部', $content);
assertStringContainsString('桌面浏览器,微信支付', $content);
assertStringContainsString('移动浏览器,支付宝', $content);
assertStringContainsString('小程序环境,微信支付', $content);
assertStringContainsString('微信内网页,微信支付', $content);
assertStringContainsString('"APP 接口预留","APP 接口",微信支付', $content);
assertStringNotContainsString('ID,order_no,status,payment_status,platform,device_type,payment_channel', $content);
assertStringNotContainsString(',desktop,wechat_pay,', $content);
assertStringNotContainsString(',mobile,alipay,', $content);
assertStringNotContainsString(',mini-program,wechat_pay,', $content);
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class MerchantAdminAccessTest extends TestCase
{
use RefreshDatabase;
public function test_merchant_admin_login_page_is_accessible(): void
{
$response = $this->get('/merchant-admin/login');
$response->assertOk();
$response->assertSee('商家后台');
}
public function test_merchant_admin_can_login_and_open_dashboard(): void
{
$this->seed();
$response = $this->post('/merchant-admin/login', [
'email' => 'merchant.admin@demo.local',
'password' => 'Merchant@123456',
]);
$response->assertRedirect('/merchant-admin');
$dashboard = $this->get('/merchant-admin');
$dashboard->assertOk();
$dashboard->assertSee('商家仪表盘');
$dashboard->assertSee('商品管理');
$dashboard->assertSee('订单管理');
$dashboard->assertSee('总台管理');
}
public function test_platform_admin_cannot_login_from_merchant_admin_entry(): void
{
$this->seed();
$response = $this->from('/merchant-admin/login')->post('/merchant-admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
]);
$response->assertRedirect('/merchant-admin/login');
$this->get('/merchant-admin/login')->assertSee('当前账号不是商家管理员,不能登录商家后台');
}
}

View File

@@ -0,0 +1,377 @@
<?php
namespace Tests\Feature;
use App\Models\Merchant;
use App\Models\Product;
use App\Models\ProductCategory;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
use function PHPUnit\Framework\assertStringContainsString;
class MerchantBusinessPagesTest extends TestCase
{
use RefreshDatabase;
protected function loginAsMerchantAdmin(): void
{
$this->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);
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class MerchantProtectedPagesTest extends TestCase
{
use RefreshDatabase;
protected function loginAsMerchantAdmin(): void
{
$this->seed();
$this->post('/merchant-admin/login', [
'email' => 'merchant.admin@demo.local',
'password' => 'Merchant@123456',
])->assertRedirect('/merchant-admin');
}
public function test_guest_is_redirected_to_merchant_admin_login_for_protected_pages(): void
{
foreach (['/merchant-admin', '/merchant-admin/products', '/merchant-admin/orders', '/merchant-admin/product-categories', '/merchant-admin/users'] as $path) {
$this->get($path)->assertRedirect('/merchant-admin/login');
}
}
public function test_merchant_admin_can_access_key_merchant_pages(): void
{
$this->loginAsMerchantAdmin();
$this->get('/merchant-admin')->assertOk()->assertSee('商家后台仪表盘');
$this->get('/merchant-admin/products')->assertOk()->assertSee('商家商品管理');
$this->get('/merchant-admin/orders')->assertOk()->assertSee('商家订单管理');
$this->get('/merchant-admin/product-categories')->assertOk()->assertSee('商家商品分类');
$this->get('/merchant-admin/users')->assertOk()->assertSee('商家用户管理');
}
public function test_platform_admin_is_forbidden_from_merchant_admin_pages(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
$this->get('/merchant-admin')->assertForbidden()->assertSee('当前账号未绑定商家后台访问权限');
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class SiteAdminAccessTest extends TestCase
{
use RefreshDatabase;
public function test_site_admin_login_page_is_accessible(): void
{
$response = $this->get('/site-admin/login');
$response->assertOk();
$response->assertSee('站点后台登录');
$response->assertSee('登录站点后台');
}
public function test_site_admin_can_login_and_open_dashboard(): void
{
$this->seed();
$response = $this->post('/site-admin/login', [
'email' => 'merchant.admin@demo.local',
'password' => 'Merchant@123456',
]);
$response->assertRedirect('/site-admin');
$dashboard = $this->get('/site-admin');
$dashboard->assertOk();
$dashboard->assertSee('站点后台仪表盘');
$dashboard->assertSee('站点仪表盘');
$dashboard->assertSee('站点商品');
$dashboard->assertSee('站点订单');
$dashboard->assertSee('站点商家');
}
public function test_platform_admin_cannot_login_from_site_admin_entry(): void
{
$this->seed();
$response = $this->from('/site-admin/login')->post('/site-admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
]);
$response->assertRedirect('/site-admin/login');
$this->get('/site-admin/login')->assertSee('当前账号不是站点管理员,不能登录站点后台');
}
}

View File

@@ -0,0 +1,733 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class SiteAdminBusinessPagesTest extends TestCase
{
use RefreshDatabase;
protected function loginAsSiteAdmin(): void
{
$this->seed();
$this->post('/site-admin/login', [
'email' => 'merchant.admin@demo.local',
'password' => 'Merchant@123456',
])->assertRedirect('/site-admin');
}
public function test_site_dashboard_displays_site_scope_content(): void
{
$this->loginAsSiteAdmin();
$this->get('/site-admin')
->assertOk()
->assertSee('站点后台仪表盘')
->assertSee('站点管理员')
->assertSee('站点用户')
->assertSee('站点商品')
->assertSee('站点订单');
}
public function test_site_business_pages_display_expected_entries(): void
{
$this->loginAsSiteAdmin();
$this->get('/site-admin/merchants')
->assertOk()
->assertSee('站点商家')
->assertSee('筛选条件')
->assertSee('导出当前筛选结果 CSV')
->assertSee('当前筛选摘要')
->assertSee('站点运营摘要')
->assertSee('运营关注项')
->assertSee('工作台导航')
->assertSee('商品工作台')
->assertSee('订单工作台')
->assertSee('站点信息')
->assertSee('当前站点资料')
->assertSee('站点承接说明')
->assertSee('运营建议')
->assertSee('查看站点商品')
->assertSee('查看站点订单')
->assertSee('查看低库存商品')
->assertSee('查看待支付订单')
->assertSee('高库存已上架商品')
->assertSee('低库存补货视角')
->assertSee('最近新增商品')
->assertSee('高金额已支付订单')
->assertSee('待支付订单跟进')
->assertSee('支付失败订单排查')
->assertSee('查看当前站点资料')
->assertSee('查看承接记录')
->assertSee('返回当前筛选视图')
->assertSee('建议动作')
->assertSee('去看最近新增商品')
->assertSee('去看已上架商品')
->assertSee('去看待支付订单')
->assertSee('去看支付失败订单')
->assertSee('去看站点资料')
->assertSee('去看承接记录')
->assertSee('当前站点承接记录')
->assertSee('查看商品')
->assertSee('查看订单')
->assertSee('低库存')
->assertSee('支付失败')
->assertSee('快捷入口默认带入更适合运营查看的排序与状态条件')
->assertSee('/site-admin/products?sort=stock_desc&amp;status=published', false)
->assertSee('/site-admin/products?sort=stock_asc&amp;status=published', false)
->assertSee('/site-admin/products?sort=latest', false)
->assertSee('/site-admin/orders?sort=pay_amount_desc&amp;payment_status=paid', false)
->assertSee('/site-admin/orders?sort=latest&amp;payment_status=unpaid', false)
->assertSee('/site-admin/orders?sort=latest&amp;payment_status=failed', false)
->assertSee('#site-profile', false)
->assertSee('#site-records', false);
$this->get('/site-admin/products')
->assertOk()
->assertSee('站点商品')
->assertSee('筛选条件')
->assertSee('商品运营汇总')
->assertSee('当前筛选摘要')
->assertSee('运营关注项')
->assertSee('工作台导航')
->assertSee('当前信号')
->assertSee('建议动作')
->assertSee('商品状态统计')
->assertSee('最低价格')
->assertSee('最高库存')
->assertSee('导出当前筛选结果 CSV')
->assertSee('商品列表');
$this->get('/site-admin/orders')
->assertOk()
->assertSee('站点订单')
->assertSee('筛选条件')
->assertSee('订单汇总')
->assertSee('当前筛选摘要')
->assertSee('运营关注项')
->assertSee('工作台导航')
->assertSee('当前信号')
->assertSee('建议动作')
->assertSee('订单状态统计')
->assertSee('最低实付金额')
->assertSee('最高实付金额')
->assertSee('导出当前筛选结果 CSV')
->assertSee('订单列表');
}
public function test_site_merchant_page_can_reflect_current_filters_and_stats(): void
{
$this->loginAsSiteAdmin();
$this->get('/site-admin/merchants?keyword=%E6%BC%94%E7%A4%BA&status=active&plan=pro&sort=name_asc')
->assertOk()
->assertSee('演示店铺')
->assertSee('启用中')
->assertSee('专业版')
->assertSee('名称 A-Z')
->assertSee('站点管理员数')
->assertSee('站点商品数')
->assertSee('站点订单数')
->assertSee('商品分类数')
->assertSee('运营关注项')
->assertSee('工作台导航')
->assertSee('站点承接说明')
->assertSee('运营建议')
->assertSee('当前站点商品仍较少,建议优先查看最近新增与基础信息是否完整。')
->assertSee('当前站点订单已形成基础规模,建议优先关注待支付、支付失败与高金额订单。')
->assertSee('联系人信息已具备,可继续作为站点日常跟进入口。')
->assertSee('建议动作')
->assertSee('去看最近新增商品')
->assertSee('去看已上架商品')
->assertSee('去看待支付订单')
->assertSee('去看支付失败订单')
->assertSee('去看站点资料')
->assertSee('去看承接记录')
->assertSee('/site-admin/products?sort=stock_desc&amp;status=published', false)
->assertSee('/site-admin/products?sort=stock_asc&amp;status=published', false)
->assertSee('/site-admin/orders?sort=pay_amount_desc&amp;payment_status=paid', false)
->assertSee('/site-admin/orders?sort=latest&amp;payment_status=failed', false)
->assertSee('#site-profile', false)
->assertSee('#site-records', false);
}
public function test_site_merchant_export_contains_scope_summary_and_filtered_content(): void
{
$this->loginAsSiteAdmin();
$export = $this->get('/site-admin/merchants/export?keyword=%E6%BC%94%E7%A4%BA&status=active&plan=pro&sort=name_asc');
$export->assertOk();
$content = $export->streamedContent();
$this->assertStringContainsString('导出信息,站点商家导出', $content);
$this->assertStringContainsString('站点ID,1', $content);
$this->assertStringContainsString('站点名称,演示店铺', $content);
$this->assertStringContainsString('关键词,演示', $content);
$this->assertStringContainsString('状态,启用中', $content);
$this->assertStringContainsString('套餐,专业版', $content);
$this->assertStringContainsString('排序', $content);
$this->assertStringContainsString('名称 A-Z', $content);
$this->assertStringContainsString('站点管理员数,1', $content);
$this->assertStringContainsString('站点商品数,2', $content);
$this->assertStringContainsString('站点订单数,5', $content);
$this->assertStringContainsString('当前站点资料,', $content);
$this->assertStringContainsString('当前套餐,专业版', $content);
$this->assertStringContainsString('ID,名称,Slug,状态,套餐,联系人,联系电话,联系邮箱,管理员数,用户数,商品数,订单数,商品分类数,激活时间', $content);
$this->assertStringContainsString('演示店铺,demo-shop,启用中,专业版', $content);
}
public function test_site_product_filters_can_narrow_results(): void
{
$this->loginAsSiteAdmin();
$this->get('/site-admin/products?keyword=%E6%BC%94%E7%A4%BA%E5%95%86%E5%93%81&status=published')
->assertOk()
->assertSee('演示商品')
->assertDontSee('精选礼包');
}
public function test_site_product_range_filters_can_narrow_results(): void
{
$this->loginAsSiteAdmin();
$this->get('/site-admin/products?min_price=150&max_price=220')
->assertOk()
->assertSee('演示商品')
->assertDontSee('精选礼包');
$this->get('/site-admin/products?min_stock=60')
->assertOk()
->assertSee('演示商品')
->assertDontSee('精选礼包');
}
public function test_site_product_invalid_ranges_show_chinese_errors_and_empty_state(): void
{
$this->loginAsSiteAdmin();
$this->get('/site-admin/products?min_price=300&max_price=100&min_stock=abc')
->assertOk()
->assertSee('最低价格不能大于最高价格。')
->assertSee('最低库存必须为整数。')
->assertSee('暂无商品');
}
public function test_site_product_filter_summary_can_reflect_current_filters(): void
{
$this->loginAsSiteAdmin();
$this->get('/site-admin/products?keyword=%E6%BC%94%E7%A4%BA%E5%95%86%E5%93%81&status=published&category_id=1&min_price=150&max_price=220&min_stock=50&max_stock=120&sort=price_desc')
->assertOk()
->assertSee('当前筛选摘要')
->assertSee('演示商品')
->assertSee('已上架')
->assertSee('默认分类')
->assertSee('¥150.00 ~ ¥220.00')
->assertSee('50 ~ 120 件')
->assertSee('价格从高到低');
}
public function test_site_product_status_cards_can_drive_filtering(): void
{
$this->loginAsSiteAdmin();
$this->get('/site-admin/products?status=published')
->assertOk()
->assertSee('已上架')
->assertSee('演示商品')
->assertSee('精选礼包');
$this->get('/site-admin/products?status=draft')
->assertOk()
->assertSee('暂无商品');
}
public function test_site_order_filters_can_narrow_results(): void
{
$this->loginAsSiteAdmin();
$this->get('/site-admin/orders?status=shipped&platform=wechat_mini')
->assertOk()
->assertSee('ORD202603080003')
->assertDontSee('ORD202603080001');
}
public function test_site_order_pay_amount_range_filters_can_narrow_results(): void
{
$this->loginAsSiteAdmin();
$this->get('/site-admin/orders?min_pay_amount=180&max_pay_amount=190')
->assertOk()
->assertSee('ORD202603080003')
->assertDontSee('ORD202603080002')
->assertDontSee('ORD202603080004');
}
public function test_site_order_invalid_ranges_show_chinese_errors_and_empty_state(): void
{
$this->loginAsSiteAdmin();
$this->get('/site-admin/orders?min_pay_amount=300&max_pay_amount=100')
->assertOk()
->assertSee('最低实付金额不能大于最高实付金额。')
->assertSee('暂无订单');
}
public function test_site_order_filter_summary_can_reflect_current_filters(): void
{
$this->loginAsSiteAdmin();
$this->get('/site-admin/orders?keyword=ORD202603080003&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('ORD202603080003')
->assertSee('已发货')
->assertSee('已支付')
->assertSee('微信小程序')
->assertSee('小程序环境')
->assertSee('微信支付')
->assertSee('¥180.00 ~ ¥190.00')
->assertSee('实付金额从高到低');
}
public function test_site_product_operations_focus_and_workbench_are_displayed(): void
{
$this->loginAsSiteAdmin();
$this->get('/site-admin/products')
->assertOk()
->assertSee('当前站点商品仍较少,建议优先查看最近新增与已上架商品,确认基础信息是否完整。')
->assertSee('去看最近新增商品')
->assertSee('去看已上架商品')
->assertSee('已上架商品')
->assertSee('低库存商品')
->assertSee('分类覆盖数')
->assertSee('高库存已上架')
->assertSee('低库存补货')
->assertSee('最近新增')
->assertSee('草稿待整理')
->assertSee('返回当前筛选视图');
}
public function test_site_order_operations_focus_and_workbench_are_displayed(): void
{
$this->loginAsSiteAdmin();
$this->get('/site-admin/orders')
->assertOk()
->assertSee('当前站点订单已形成基础规模,建议优先关注待支付、支付失败与高金额已支付订单。')
->assertSee('去看待支付订单')
->assertSee('去看支付失败订单')
->assertSee('待支付订单')
->assertSee('支付失败订单')
->assertSee('已完成订单')
->assertSee('高金额已支付')
->assertSee('待支付跟进')
->assertSee('支付失败排查')
->assertSee('最近完成订单')
->assertSee('返回当前筛选视图');
}
public function test_site_product_operations_focus_can_follow_current_filters(): void
{
$this->loginAsSiteAdmin();
$this->get('/site-admin/products?status=published&min_stock=0&max_stock=20&sort=stock_asc')
->assertOk()
->assertSee('当前筛选已聚焦已上架库存视角,建议优先确认低库存补货节奏,并同步观察高库存结构是否健康。')
->assertSee('继续查看当前库存视角')
->assertSee('去看高库存商品')
->assertSee('/site-admin/products?status=published&amp;min_stock=0&amp;max_stock=20&amp;sort=stock_asc', false);
$this->get('/site-admin/products?status=draft')
->assertOk()
->assertSee('当前正在查看草稿商品,建议优先补齐标题、分类、价格与库存后再安排上架。')
->assertSee('继续查看当前草稿')
->assertSee('去看已上架商品')
->assertSee('/site-admin/products?status=draft&amp;sort=latest', false);
$response = $this->get('/site-admin/products?status=published&category_id=1&keyword=%E6%BC%94%E7%A4%BA&min_price=150&max_price=220&sort=price_desc');
$response
->assertOk()
->assertSee('当前筛选已聚焦已上架的“默认分类”分类下关键词“演示”命中的价格带 ¥150.00 ~ ¥220.00 商品,建议优先核对在售商品命名、分类承接、价格梯度与搜索结果是否一致,并同步观察库存结构是否健康。')
->assertSee('继续查看当前已上架分类关键词价格带商品')
->assertSee('去看当前已上架分类关键词商品')
->assertSee('status=published', false)
->assertSee('category_id=1', false)
->assertSee('keyword=%E6%BC%94%E7%A4%BA', false)
->assertSee('min_price=150', false)
->assertSee('max_price=220', false)
->assertSee('sort=price_desc', false);
$response = $this->get('/site-admin/products?status=published&category_id=1&keyword=%E6%BC%94%E7%A4%BA&sort=latest');
$response
->assertOk()
->assertSee('当前筛选已聚焦已上架的“默认分类”分类下关键词“演示”命中的商品,建议优先核对在售商品命名、分类承接与搜索结果是否一致,并同步观察价格带与库存结构是否健康。')
->assertSee('继续查看当前已上架分类关键词商品')
->assertSee('去看当前已上架分类商品')
->assertSee('status=published', false)
->assertSee('category_id=1', false)
->assertSee('keyword=%E6%BC%94%E7%A4%BA', false)
->assertSee('sort=latest', false);
$response = $this->get('/site-admin/products?status=published&category_id=1&min_price=150&max_price=220&sort=price_desc');
$response
->assertOk()
->assertSee('当前筛选已聚焦已上架的“默认分类”分类下价格带 ¥150.00 ~ ¥220.00 商品,建议优先核对该分类在售商品的价格结构、库存分布与承接效率是否协调。')
->assertSee('继续查看当前已上架分类价格带商品')
->assertSee('去看当前已上架分类商品')
->assertSee('status=published', false)
->assertSee('category_id=1', false)
->assertSee('min_price=150', false)
->assertSee('max_price=220', false)
->assertSee('sort=price_desc', false);
$response = $this->get('/site-admin/products?status=published&keyword=%E6%BC%94%E7%A4%BA&min_price=150&max_price=220&sort=price_desc');
$response
->assertOk()
->assertSee('当前筛选已聚焦已上架商品中关键词“演示”命中的价格带 ¥150.00 ~ ¥220.00 结果,建议优先核对在售商品命名、卖点表达与价格梯度是否一致,并同步观察库存结构与搜索承接是否健康。')
->assertSee('继续查看当前已上架关键词价格带商品')
->assertSee('去看当前已上架关键词商品')
->assertSee('status=published', false)
->assertSee('keyword=%E6%BC%94%E7%A4%BA', false)
->assertSee('min_price=150', false)
->assertSee('max_price=220', false)
->assertSee('sort=price_desc', false);
$response = $this->get('/site-admin/products?status=published&keyword=%E6%BC%94%E7%A4%BA&sort=latest');
$response
->assertOk()
->assertSee('当前筛选已聚焦已上架商品中关键词“演示”命中的结果,建议优先核对在售商品命名、卖点表达与搜索承接是否一致,并同步观察价格带与库存结构是否健康。')
->assertSee('继续查看当前已上架关键词商品')
->assertSee('去看全部已上架商品')
->assertSee('status=published', false)
->assertSee('keyword=%E6%BC%94%E7%A4%BA', false)
->assertSee('sort=latest', false);
$response = $this->get('/site-admin/products?category_id=1&keyword=%E6%BC%94%E7%A4%BA&min_price=150&max_price=220&sort=price_desc');
$response
->assertOk()
->assertSee('当前筛选已聚焦“默认分类”分类下关键词“演示”命中的价格带 ¥150.00 ~ ¥220.00 商品,建议优先核对分类承接、命名卖点与价格梯度是否一致,并同步观察库存结构与搜索承接是否健康。')
->assertSee('继续查看当前分类关键词价格带商品')
->assertSee('去看当前分类关键词商品')
->assertSee('category_id=1', false)
->assertSee('keyword=%E6%BC%94%E7%A4%BA', false)
->assertSee('min_price=150', false)
->assertSee('max_price=220', false)
->assertSee('sort=price_desc', false);
$response = $this->get('/site-admin/products?category_id=1&keyword=%E6%BC%94%E7%A4%BA&sort=latest');
$response
->assertOk()
->assertSee('当前筛选已聚焦“默认分类”分类下关键词“演示”命中的商品,建议优先核对分类承接、命名卖点与搜索结果是否一致,并同步观察相关商品的价格带与库存结构。')
->assertSee('继续查看当前分类关键词商品')
->assertSee('去看当前分类商品')
->assertSee('keyword=%E6%BC%94%E7%A4%BA', false)
->assertSee('category_id=1', false)
->assertSee('sort=latest', false);
$response = $this->get('/site-admin/products?status=published&category_id=1&sort=latest');
$response
->assertOk()
->assertSee('当前筛选已聚焦已上架的“默认分类”分类商品,建议优先核对该分类在售商品的价格带、库存结构与承接质量是否均衡。')
->assertSee('继续查看当前已上架分类商品')
->assertSee('去看当前分类商品')
->assertSee('status=published', false)
->assertSee('category_id=1', false)
->assertSee('sort=latest', false);
$response = $this->get('/site-admin/products?category_id=1&min_price=150&max_price=220&sort=price_desc');
$response
->assertOk()
->assertSee('当前筛选已聚焦“默认分类”分类下价格带 ¥150.00 ~ ¥220.00 商品,建议优先核对该分类价格结构是否连贯,并同步观察库存分布与承接效率是否健康。')
->assertSee('继续查看当前分类价格带商品')
->assertSee('去看当前分类商品')
->assertSee('category_id=1', false)
->assertSee('min_price=150', false)
->assertSee('max_price=220', false)
->assertSee('sort=price_desc', false);
$this->get('/site-admin/products?category_id=1&sort=latest')
->assertOk()
->assertSee('当前筛选已聚焦“默认分类”分类商品,建议优先核对分类承接是否准确,并同步观察价格带与库存结构是否均衡。')
->assertSee('继续查看当前分类商品')
->assertSee('去看低库存商品')
->assertSee('/site-admin/products?category_id=1&amp;sort=latest', false);
$this->get('/site-admin/products?keyword=%E6%BC%94%E7%A4%BA&sort=latest')
->assertOk()
->assertSee('当前筛选已聚焦关键词“演示”命中的商品,建议优先核对命名、卖点与搜索承接是否一致,并同步观察相关商品的价格带与库存结构。')
->assertSee('继续查看当前关键词商品')
->assertSee('去看最近新增商品')
->assertSee('/site-admin/products?keyword=%E6%BC%94%E7%A4%BA&amp;sort=latest', false);
$this->get('/site-admin/products?min_price=150&max_price=220&sort=price_desc')
->assertOk()
->assertSee('当前筛选已聚焦价格带 ¥150.00 ~ ¥220.00 商品,建议优先核对定价梯度是否连贯,并同步观察库存结构与转化表现是否匹配。')
->assertSee('继续查看当前价格带商品')
->assertSee('去看最近新增商品')
->assertSee('/site-admin/products?min_price=150&amp;max_price=220&amp;sort=price_desc', false);
}
public function test_site_order_operations_focus_can_follow_current_filters(): void
{
$this->loginAsSiteAdmin();
$this->get('/site-admin/orders?platform=wechat_mini&sort=latest')
->assertOk()
->assertSee('当前筛选已聚焦微信小程序订单,建议优先关注下单到支付转化是否顺畅,并同步排查小程序端支付回流体验。')
->assertSee('继续查看微信小程序订单')
->assertSee('去看待支付订单')
->assertSee('/site-admin/orders?platform=wechat_mini&amp;sort=latest', false);
$this->get('/site-admin/orders?payment_channel=wechat_pay&sort=latest')
->assertOk()
->assertSee('当前筛选已聚焦微信支付订单,建议优先核对支付成功率、回调稳定性与失败重试转化。')
->assertSee('继续查看微信支付订单')
->assertSee('去看支付失败订单')
->assertSee('/site-admin/orders?payment_channel=wechat_pay&amp;sort=latest', false);
$this->get('/site-admin/orders?device_type=mini-program&payment_status=failed&sort=latest')
->assertOk()
->assertSee('当前筛选已聚焦小程序环境订单,建议优先核对授权链路、支付唤起表现与下单回流是否顺畅。')
->assertSee('继续查看小程序环境订单')
->assertSee('去看微信支付订单')
->assertSee('/site-admin/orders?payment_status=failed&amp;device_type=mini-program&amp;sort=latest', false);
$this->get('/site-admin/orders?device_type=mobile-webview&payment_status=failed&sort=latest')
->assertOk()
->assertSee('当前筛选已聚焦微信内网页订单,建议优先关注授权静默登录、页面跳转稳定性与支付拉起后的回流体验。')
->assertSee('继续查看微信内网页订单')
->assertSee('去看待支付订单')
->assertSee('/site-admin/orders?payment_status=failed&amp;device_type=mobile-webview&amp;sort=latest', false);
$this->get('/site-admin/orders?device_type=mobile&payment_status=failed&sort=latest')
->assertOk()
->assertSee('当前筛选已聚焦移动浏览器订单,建议优先关注 H5 下单链路、页面加载稳定性与支付转化流失点。')
->assertSee('继续查看移动浏览器订单')
->assertSee('去看待支付订单')
->assertSee('/site-admin/orders?payment_status=failed&amp;device_type=mobile&amp;sort=latest', false);
$this->get('/site-admin/orders?device_type=desktop&payment_status=failed&sort=latest')
->assertOk()
->assertSee('当前筛选已聚焦桌面浏览器订单,建议优先关注 PC 端下单流程、页面首屏稳定性与高客单转化表现。')
->assertSee('继续查看桌面浏览器订单')
->assertSee('去看高金额已支付订单')
->assertSee('/site-admin/orders?payment_status=failed&amp;device_type=desktop&amp;sort=latest', false);
$this->get('/site-admin/orders?payment_status=failed&sort=latest')
->assertOk()
->assertSee('当前筛选已聚焦支付失败订单,建议优先排查支付渠道、回调结果与用户重试情况。')
->assertSee('继续查看支付失败订单')
->assertSee('去看高金额已支付订单')
->assertSee('/site-admin/orders?payment_status=failed&amp;sort=latest', false);
$this->get('/site-admin/orders?payment_status=unpaid&sort=latest')
->assertOk()
->assertSee('当前筛选已聚焦待支付订单,建议优先跟进下单未支付用户,并观察支付转化时效。')
->assertSee('继续查看待支付订单')
->assertSee('去看高金额已支付订单')
->assertSee('/site-admin/orders?payment_status=unpaid&amp;sort=latest', false);
$this->get('/site-admin/orders?status=completed&sort=latest')
->assertOk()
->assertSee('当前正在查看已完成订单,建议复盘高客单成交与复购来源,沉淀更稳定的转化路径。')
->assertSee('继续查看已完成订单')
->assertSee('去看高金额已支付订单')
->assertSee('/site-admin/orders?status=completed&amp;sort=latest', false);
}
public function test_site_order_status_cards_can_drive_filtering(): void
{
$this->loginAsSiteAdmin();
$this->get('/site-admin/orders?status=completed')
->assertOk()
->assertSee('已完成')
->assertSee('ORD202603080004')
->assertDontSee('ORD202603080001');
$this->get('/site-admin/orders?status=refunded')
->assertOk()
->assertSee('暂无订单');
}
public function test_site_business_pages_display_empty_state_when_filters_miss(): void
{
$this->loginAsSiteAdmin();
$this->get('/site-admin/products?keyword=no-match-keyword')
->assertOk()
->assertSee('暂无商品');
$this->get('/site-admin/orders?keyword=no-match-keyword')
->assertOk()
->assertSee('暂无订单');
}
public function test_site_export_endpoints_return_csv_response(): void
{
$this->loginAsSiteAdmin();
$this->get('/site-admin/products/export')
->assertOk()
->assertHeader('content-type', 'text/csv; charset=UTF-8');
$this->get('/site-admin/orders/export')
->assertOk()
->assertHeader('content-type', 'text/csv; charset=UTF-8');
}
public function test_site_invalid_product_export_is_blocked_with_redirect_and_error(): void
{
$this->loginAsSiteAdmin();
$response = $this->get('/site-admin/products/export?min_price=300&max_price=100');
$response->assertStatus(302);
$this->assertStringContainsString('/site-admin/products?', $response->headers->get('Location'));
$this->assertStringContainsString('min_price=300', $response->headers->get('Location'));
$this->assertStringContainsString('max_price=100', $response->headers->get('Location'));
$this->assertStringContainsString('sort=latest', $response->headers->get('Location'));
$this->followRedirects($this->get('/site-admin/products/export?min_price=300&max_price=100'))
->assertSee('最低价格不能大于最高价格。')
->assertSee('暂无商品');
}
public function test_site_invalid_order_export_is_blocked_with_redirect_and_error(): void
{
$this->loginAsSiteAdmin();
$response = $this->get('/site-admin/orders/export?min_pay_amount=300&max_pay_amount=100');
$response->assertStatus(302);
$this->assertStringContainsString('/site-admin/orders?', $response->headers->get('Location'));
$this->assertStringContainsString('min_pay_amount=300', $response->headers->get('Location'));
$this->assertStringContainsString('max_pay_amount=100', $response->headers->get('Location'));
$this->assertStringContainsString('sort=latest', $response->headers->get('Location'));
$this->followRedirects($this->get('/site-admin/orders/export?min_pay_amount=300&max_pay_amount=100'))
->assertSee('最低实付金额不能大于最高实付金额。')
->assertSee('暂无订单');
}
public function test_site_export_contains_scope_summary_and_filtered_content(): void
{
$this->loginAsSiteAdmin();
$productExport = $this->get('/site-admin/products/export?keyword=%E6%BC%94%E7%A4%BA%E5%95%86%E5%93%81&status=published&min_price=150&max_price=220');
$productExport->assertOk();
$productContent = $productExport->streamedContent();
$this->assertStringContainsString('导出信息,站点商品导出', $productContent);
$this->assertStringContainsString('站点ID,1', $productContent);
$this->assertStringContainsString('状态,已上架', $productContent);
$this->assertStringContainsString('分类,全部', $productContent);
$this->assertStringContainsString('最低价格,¥150.00', $productContent);
$this->assertStringContainsString('最高价格,¥220.00', $productContent);
$this->assertStringContainsString('排序,最新创建', $productContent);
$this->assertStringContainsString('导出商品数,1', $productContent);
$this->assertStringContainsString('导出总库存,100', $productContent);
$this->assertStringContainsString('演示商品', $productContent);
$this->assertStringNotContainsString('精选礼包', $productContent);
$orderExport = $this->get('/site-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');
$orderExport->assertOk();
$orderContent = $orderExport->streamedContent();
$this->assertStringContainsString('导出信息,站点订单导出', $orderContent);
$this->assertStringContainsString('站点ID,1', $orderContent);
$this->assertStringContainsString('订单状态,已发货', $orderContent);
$this->assertStringContainsString('支付状态,已支付', $orderContent);
$this->assertStringContainsString('平台,微信小程序', $orderContent);
$this->assertStringContainsString('设备类型,小程序环境', $orderContent);
$this->assertStringContainsString('支付渠道,微信支付', $orderContent);
$this->assertStringContainsString('最低实付金额,¥180.00', $orderContent);
$this->assertStringContainsString('最高实付金额,¥190.00', $orderContent);
$this->assertStringContainsString('排序,创建时间倒序', $orderContent);
$this->assertStringContainsString('导出订单数,1', $orderContent);
$this->assertStringContainsString('导出已支付订单数,1', $orderContent);
$this->assertStringContainsString('ORD202603080003', $orderContent);
$this->assertStringNotContainsString('ORD202603080001', $orderContent);
}
public function test_site_product_summary_stats_match_export_summary_for_same_filters(): void
{
$this->loginAsSiteAdmin();
$page = $this->get('/site-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;
});
$page->assertSee('商品总数')
->assertSee('总库存')
->assertSee('总货值')
->assertSee('平均售价')
->assertSee('19,900.00')
->assertSee('199.00');
$export = $this->get('/site-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();
$this->assertStringContainsString('导出商品数,1', $content);
$this->assertStringContainsString('导出总库存,100', $content);
$this->assertStringContainsString('导出总货值,19900.00', $content);
$this->assertStringContainsString('导出平均售价,199.00', $content);
}
public function test_site_order_summary_stats_match_export_summary_for_same_filters(): void
{
$this->loginAsSiteAdmin();
$page = $this->get('/site-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;
});
$page->assertSee('订单总数')
->assertSee('实付总额')
->assertSee('平均客单价')
->assertSee('已支付订单数')
->assertSee('支付失败订单')
->assertSee('小程序环境')
->assertSee('微信支付')
->assertSee('189.00');
$export = $this->get('/site-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();
$this->assertStringContainsString('设备类型,小程序环境', $content);
$this->assertStringContainsString('支付渠道,微信支付', $content);
$this->assertStringContainsString('导出订单数,1', $content);
$this->assertStringContainsString('导出实付总额,189.00', $content);
$this->assertStringContainsString('导出平均客单价,189.00', $content);
$this->assertStringContainsString('导出已支付订单数,1', $content);
$this->assertStringContainsString('导出支付失败订单,0', $content);
}
public function test_platform_site_list_displays_site_admin_entry_link(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
$this->get('/admin/merchants')
->assertOk()
->assertSee('进入站点后台')
->assertSee('当前阶段请使用该站点管理员账号登录');
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class SiteAdminProtectedPagesTest extends TestCase
{
use RefreshDatabase;
protected function loginAsSiteAdmin(): void
{
$this->seed();
$this->post('/site-admin/login', [
'email' => 'merchant.admin@demo.local',
'password' => 'Merchant@123456',
])->assertRedirect('/site-admin');
}
public function test_guest_is_redirected_to_site_admin_login_for_protected_pages(): void
{
foreach (['/site-admin', '/site-admin/merchants', '/site-admin/merchants/export', '/site-admin/products', '/site-admin/products/export', '/site-admin/orders', '/site-admin/orders/export'] as $path) {
$this->get($path)->assertRedirect('/site-admin/login');
}
}
public function test_site_admin_can_access_key_site_pages(): void
{
$this->loginAsSiteAdmin();
$this->get('/site-admin')->assertOk()->assertSee('站点后台仪表盘');
$this->get('/site-admin/merchants')->assertOk()->assertSee('站点商家');
$this->get('/site-admin/merchants/export')->assertOk()->assertHeader('content-type', 'text/csv; charset=UTF-8');
$this->get('/site-admin/products')->assertOk()->assertSee('站点商品');
$this->get('/site-admin/products/export')->assertOk()->assertHeader('content-type', 'text/csv; charset=UTF-8');
$this->get('/site-admin/orders')->assertOk()->assertSee('站点订单');
$this->get('/site-admin/orders/export')->assertOk()->assertHeader('content-type', 'text/csv; charset=UTF-8');
}
public function test_platform_admin_is_forbidden_from_site_admin_pages(): void
{
$this->seed();
$this->post('/admin/login', [
'email' => 'platform.admin@demo.local',
'password' => 'Platform@123456',
])->assertRedirect('/admin');
$this->get('/site-admin')->assertForbidden()->assertSee('当前账号未绑定站点后台访问权限');
}
}

View File

@@ -0,0 +1,177 @@
<?php
namespace Tests\Feature;
use App\Models\Merchant;
use App\Models\Plan;
use App\Models\PlatformOrder;
use App\Models\SiteSubscription;
use App\Support\SubscriptionActivationService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class SubscriptionActivationServiceTest extends TestCase
{
use RefreshDatabase;
public function test_activate_order_creates_subscription_and_links_order(): void
{
$this->seed();
$merchant = Merchant::query()->firstOrFail();
$plan = Plan::query()->create([
'code' => 'activate_test_monthly',
'name' => '激活测试(月付)',
'billing_cycle' => 'monthly',
'price' => 99,
'list_price' => 99,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
$order = PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_ACTIVATE_0001',
'order_type' => 'new_purchase',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'list_amount' => 99,
'discount_amount' => 0,
'payable_amount' => 99,
'paid_amount' => 99,
'placed_at' => now()->subMinutes(10),
'paid_at' => now()->subMinutes(5),
'activated_at' => now()->subMinutes(1),
]);
$service = new SubscriptionActivationService();
$subscription = $service->activateOrder($order->id, 1);
// 二次执行应为幂等:不重复续期/不重复创建,直接返回同一个订阅
$again = $service->activateOrder($order->id, 1);
$this->assertInstanceOf(SiteSubscription::class, $subscription);
$this->assertSame($subscription->id, $again->id);
$this->assertSame('activated', $subscription->status);
$this->assertSame($merchant->id, $subscription->merchant_id);
$this->assertSame($plan->id, $subscription->plan_id);
$this->assertNotNull($subscription->starts_at);
$this->assertNotNull($subscription->ends_at);
$this->assertTrue($subscription->ends_at->greaterThan($subscription->starts_at));
$order->refresh();
$this->assertSame($subscription->id, $order->site_subscription_id);
}
public function test_activate_order_extends_existing_subscription(): void
{
$this->seed();
$merchant = Merchant::query()->firstOrFail();
$plan = Plan::query()->create([
'code' => 'renew_test_monthly',
'name' => '续费测试(月付)',
'billing_cycle' => 'monthly',
'price' => 120,
'list_price' => 120,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
$subscription = SiteSubscription::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'status' => 'activated',
'source' => 'manual',
'subscription_no' => 'SUB_EXIST_0001',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'amount' => 120,
'starts_at' => now()->subDays(10),
'ends_at' => now()->addDays(20),
'activated_at' => now()->subDays(10),
]);
$oldEndsAt = $subscription->ends_at->copy();
$order = PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'site_subscription_id' => $subscription->id,
'order_no' => 'PO_RENEW_0001',
'order_type' => 'renewal',
'status' => 'activated',
'payment_status' => 'paid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'list_amount' => 120,
'discount_amount' => 0,
'payable_amount' => 120,
'paid_amount' => 120,
'placed_at' => now()->subMinutes(10),
'paid_at' => now()->subMinutes(5),
'activated_at' => now()->subMinutes(1),
]);
$service = new SubscriptionActivationService();
$updated = $service->activateOrder($order->id, 1);
$updated->refresh();
$this->assertTrue($updated->ends_at->greaterThan($oldEndsAt));
$this->assertSame('activated', $updated->status);
// 再次执行应为幂等ends_at 不应继续增长
$endsAtAfterFirst = $updated->ends_at->copy();
$again = $service->activateOrder($order->id, 1);
$again->refresh();
$this->assertTrue($again->ends_at->equalTo($endsAtAfterFirst));
}
public function test_activate_order_requires_paid_and_activated(): void
{
$this->seed();
$merchant = Merchant::query()->firstOrFail();
$plan = Plan::query()->create([
'code' => 'invalid_test',
'name' => '无效测试',
'billing_cycle' => 'monthly',
'price' => 10,
'list_price' => 10,
'status' => 'active',
'sort' => 10,
'published_at' => now(),
]);
$order = PlatformOrder::query()->create([
'merchant_id' => $merchant->id,
'plan_id' => $plan->id,
'order_no' => 'PO_INVALID_0001',
'order_type' => 'new_purchase',
'status' => 'pending',
'payment_status' => 'unpaid',
'plan_name' => $plan->name,
'billing_cycle' => $plan->billing_cycle,
'period_months' => 1,
'quantity' => 1,
'payable_amount' => 10,
'paid_amount' => 0,
'placed_at' => now(),
]);
$this->expectException(\InvalidArgumentException::class);
$service = new SubscriptionActivationService();
$service->activateOrder($order->id, 1);
}
}

10
tests/TestCase.php Normal file
View File

@@ -0,0 +1,10 @@
<?php
namespace Tests;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
//
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Tests\Unit;
use PHPUnit\Framework\TestCase;
class ExampleTest extends TestCase
{
/**
* A basic test example.
*/
public function test_that_true_is_true(): void
{
$this->assertTrue(true);
}
}