runInCoroutine(static function (): array { $ids = []; // 插入 order $order = Order::query()->create([ 'company_id' => 1, 'platform_id' => 1, 'store_id' => 1, 'order_status_id' => 1, 'platform_order_id' => 'DASH_TEST_' . uniqid(), 'buyer_user_id' => 'test_buyer', 'payment_method_id' => 1, 'presale' => false, 'total_amount' => '100.00', 'total_paid' => '100.00', 'total_discount' => '0.00', 'total_received' => '100.00', 'freight_fee' => '0.00', 'tax_fee' => '0.00', 'discount_fee' => '0.00', 'commission_fee' => '0.00', 'coupon_amount' => '0.00', 'voucher_amount' => '0.00', 'order_type_id' => 1, 'created_date' => '2026-03-17 10:00:00', 'hash' => md5('dash_test_order'), 'raw' => '{}', 'ext' => '{}', ]); $ids['order'] = $order->id; // 插入 product $product = Product::query()->create([ 'company_id' => 1, 'platform_id' => 1, 'store_id' => 1, 'status_id' => 1, 'type_id' => 1, 'platform_item_id' => 'DASH_PROD_' . uniqid(), 'price' => '50.00', 'currency' => 'CNY', 'num' => 10, 'hash' => md5('dash_test_product'), 'raw' => '{}', ]); $ids['product'] = $product->id; // 插入 refund $refund = Refund::query()->create([ 'company_id' => 1, 'platform_id' => 1, 'store_id' => 1, 'platform_order_id' => 'DASH_TEST_ORD', 'platform_refund_id' => 'DASH_RF_' . uniqid(), 'refund_status_id' => 1, 'refund_type_id' => 1, 'refund_amount' => '50.00', 'freight_refund' => '0.00', 'refund_total' => '50.00', 'currency' => 'CNY', 'buyer_user_id' => 'test_buyer', 'order_created_date' => '2026-03-17 09:00:00', 'order_paid_date' => '2026-03-17 09:30:00', 'created_date' => '2026-03-17 10:00:00', 'hash' => md5('dash_test_refund'), 'raw' => '{}', ]); $ids['refund'] = $refund->id; // 插入 failed_message $failed = FailedMessage::query()->create([ 'error_id' => 'err_dash_' . uniqid(), 'data_type' => 'order', 'platform' => 'JD', 'platform_id' => 1, 'company_id' => 1, 'store_id' => 1, 'error_type' => 'RuntimeException', 'error_message' => 'Dashboard test error', 'error_code' => 0, 'error_trace' => '#0 test', 'original_message' => ['test' => true], 'retry_count' => 0, 'failed_at' => '2026-03-17 10:00:00+08', ]); $ids['failed'] = $failed->id; return $ids; }); } public static function tearDownAfterClass(): void { if (!empty(self::$testIds)) { $ids = self::$testIds; $cleanup = static function () use ($ids): void { if (isset($ids['order'])) { Order::query()->where('id', $ids['order'])->delete(); } if (isset($ids['product'])) { Product::query()->where('id', $ids['product'])->delete(); } if (isset($ids['refund'])) { Refund::query()->where('id', $ids['refund'])->delete(); } if (isset($ids['failed'])) { FailedMessage::query()->where('id', $ids['failed'])->delete(); } }; if (\Swoole\Coroutine::getCid() > 0) { $cleanup(); } else { \Swoole\Coroutine\run($cleanup); } self::$testIds = []; } parent::tearDownAfterClass(); } // ========== Overview ========== public function test_overview_returns_correct_structure(): void { $this->ensureTestData(); $response = $this->get('/api/v1/dashboard/overview', [], $this->authHeaders()); $response->assertStatus(200); $response->assertJsonPath('code', 0); $response->assertJsonStructure([ 'code', 'message', 'data' => [ 'today' => ['success', 'failed'], 'this_week' => ['success', 'failed'], 'this_month' => ['success', 'failed'], 'by_type', ], ]); } public function test_overview_by_type_contains_all_data_types(): void { $this->ensureTestData(); $response = $this->get('/api/v1/dashboard/overview', [], $this->authHeaders()); $response->assertStatus(200); $by_type = $response->json('data.by_type'); $types = array_column($by_type, 'data_type'); $this->assertContains('order', $types); $this->assertContains('product', $types); $this->assertContains('refund', $types); $this->assertContains('inventory', $types); } // ========== Trend ========== public function test_trend_returns_array(): void { $this->ensureTestData(); $response = $this->get('/api/v1/dashboard/trend', [ 'from' => '2026-03-01', 'to' => '2026-03-31', ], $this->authHeaders()); $response->assertStatus(200); $response->assertJsonPath('code', 0); $this->assertIsArray($response->json('data')); } public function test_trend_with_data_type_filter(): void { $this->ensureTestData(); $response = $this->get('/api/v1/dashboard/trend', [ 'from' => '2026-03-01', 'to' => '2026-03-31', 'data_type' => 'order', ], $this->authHeaders()); $response->assertStatus(200); $response->assertJsonPath('code', 0); } public function test_trend_invalid_group_by_returns_400(): void { $response = $this->get('/api/v1/dashboard/trend', [ 'group_by' => 'invalid', ], $this->authHeaders()); $response->assertStatus(400); $this->assertSame(400, $response->json('code')); } public function test_trend_invalid_data_type_returns_400(): void { $response = $this->get('/api/v1/dashboard/trend', [ 'data_type' => 'invalid', ], $this->authHeaders()); $response->assertStatus(400); $this->assertSame(400, $response->json('code')); } public function test_trend_invalid_date_format_returns_400(): void { $response = $this->get('/api/v1/dashboard/trend', [ 'from' => '2026/03/01', ], $this->authHeaders()); $response->assertStatus(400); $this->assertSame(400, $response->json('code')); } public function test_trend_from_after_to_returns_400(): void { $response = $this->get('/api/v1/dashboard/trend', [ 'from' => '2026-03-31', 'to' => '2026-03-01', ], $this->authHeaders()); $response->assertStatus(400); $this->assertSame(400, $response->json('code')); } // ========== Breakdown ========== public function test_breakdown_by_company_returns_correct_structure(): void { $this->ensureTestData(); $response = $this->get('/api/v1/dashboard/breakdown', [ 'dimension' => 'company', 'from' => '2026-03-01', 'to' => '2026-03-31', ], $this->authHeaders()); $response->assertStatus(200); $response->assertJsonPath('code', 0); $data = $response->json('data'); $this->assertIsArray($data); if (!empty($data)) { $first = $data[0]; $this->assertArrayHasKey('id', $first); $this->assertArrayHasKey('name', $first); $this->assertArrayHasKey('success', $first); $this->assertArrayHasKey('failed', $first); } } public function test_breakdown_by_platform(): void { $this->ensureTestData(); $response = $this->get('/api/v1/dashboard/breakdown', [ 'dimension' => 'platform', 'from' => '2026-03-01', 'to' => '2026-03-31', ], $this->authHeaders()); $response->assertStatus(200); $response->assertJsonPath('code', 0); } public function test_breakdown_by_store(): void { $this->ensureTestData(); $response = $this->get('/api/v1/dashboard/breakdown', [ 'dimension' => 'store', 'from' => '2026-03-01', 'to' => '2026-03-31', ], $this->authHeaders()); $response->assertStatus(200); $response->assertJsonPath('code', 0); } public function test_breakdown_missing_dimension_returns_400(): void { $response = $this->get('/api/v1/dashboard/breakdown', [], $this->authHeaders()); $response->assertStatus(400); $this->assertSame(400, $response->json('code')); $this->assertStringContainsString('dimension', $response->json('message')); } public function test_breakdown_invalid_dimension_returns_400(): void { $response = $this->get('/api/v1/dashboard/breakdown', [ 'dimension' => 'invalid', ], $this->authHeaders()); $response->assertStatus(400); $this->assertSame(400, $response->json('code')); } public function test_breakdown_with_data_type_filter(): void { $this->ensureTestData(); $response = $this->get('/api/v1/dashboard/breakdown', [ 'dimension' => 'company', 'data_type' => 'order', 'from' => '2026-03-01', 'to' => '2026-03-31', ], $this->authHeaders()); $response->assertStatus(200); $response->assertJsonPath('code', 0); } // ========== 认证检查 ========== public function test_overview_without_token_returns_401(): void { $response = $this->get('/api/v1/dashboard/overview'); $response->assertStatus(401); } public function test_trend_without_token_returns_401(): void { $response = $this->get('/api/v1/dashboard/trend'); $response->assertStatus(401); } public function test_breakdown_without_token_returns_401(): void { $response = $this->get('/api/v1/dashboard/breakdown'); $response->assertStatus(401); } }