'sqlite', 'database.connections.sqlite.database' => ':memory:', 'audit.enabled' => false, ]); DB::purge('sqlite'); Schema::create('companies', function (Blueprint $table) { $table->id(); $table->string('company_name')->nullable(); $table->string('credit_code')->nullable(); $table->timestamps(); $table->softDeletes(); }); Schema::create('company_qcc_accounts', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('company_id')->unique(); $table->string('status', 20); $table->unsignedBigInteger('selected_by')->nullable(); $table->dateTime('selected_at'); $table->dateTime('first_success_at')->nullable(); $table->string('first_success_source')->nullable(); $table->string('first_success_ref')->nullable(); $table->text('unknown_note')->nullable(); $table->timestamps(); }); } protected function tearDown(): void { Schema::dropIfExists('company_qcc_accounts'); Schema::dropIfExists('companies'); parent::tearDown(); } public function test_first_success_marks_candidate_occupied_only_once(): void { $company = $this->company(); $account = $this->selectedAccount($company); $service = new QccCallService(); $service->recordSuccess($company->id, 'enterprise-profile', 'request-first'); $firstSuccessAt = $account->fresh()->first_success_at; $service->recordSuccess($company->id, 'risk-check', 'request-second'); $account = $account->fresh(); $this->assertSame(CompanyQccAccount::STATUS_OCCUPIED, $account->status); $this->assertSame('enterprise-profile', $account->first_success_source); $this->assertSame('request-first', $account->first_success_ref); $this->assertTrue($firstSuccessAt->equalTo($account->first_success_at)); } public function test_unknown_candidate_blocks_follow_up_calls(): void { $company = $this->company(); $account = $this->selectedAccount($company); $service = new QccCallService(); try { $service->call($company, 'enterprise-profile', 'request-timeout', function () { throw new RuntimeException('timeout'); }, fn($result) => $result === true); $this->fail('Expected timeout exception was not thrown.'); } catch (RuntimeException $exception) { $this->assertSame('timeout', $exception->getMessage()); } $this->assertSame(CompanyQccAccount::STATUS_UNKNOWN, $account->fresh()->status); $this->expectException(RuntimeException::class); $this->expectExceptionMessage('待人工确认'); $service->call($company, 'enterprise-profile', 'request-retry', fn() => true, fn($result) => $result === true); } public function test_later_timeout_does_not_downgrade_occupied_account(): void { $company = $this->company(); $account = $this->selectedAccount($company, CompanyQccAccount::STATUS_OCCUPIED); $service = new QccCallService(); try { $service->call($company, 'enterprise-profile', 'request-timeout', function () { throw new RuntimeException('timeout'); }, fn($result) => $result === true); $this->fail('Expected timeout exception was not thrown.'); } catch (RuntimeException $exception) { $this->assertSame('timeout', $exception->getMessage()); } $this->assertSame(CompanyQccAccount::STATUS_OCCUPIED, $account->fresh()->status); } public function test_indeterminate_response_marks_selected_account_unknown_but_explicit_failure_does_not(): void { $company = $this->company(); $account = $this->selectedAccount($company); $service = new QccCallService(); $service->call( $company, 'enterprise-profile', 'request-business-failed', fn() => ['code' => 400], fn($result) => $result['code'] === 200, fn($result) => !array_key_exists('code', $result) ); $this->assertSame(CompanyQccAccount::STATUS_SELECTED, $account->fresh()->status); $service->call( $company, 'enterprise-profile', 'request-unparseable', fn() => [], fn($result) => ($result['code'] ?? null) === 200, fn($result) => !array_key_exists('code', $result) ? '元禾响应缺少业务状态码' : false ); $account = $account->fresh(); $this->assertSame(CompanyQccAccount::STATUS_UNKNOWN, $account->status); $this->assertSame('元禾响应缺少业务状态码', $account->unknown_note); } public function test_result_classifier_exception_marks_selected_account_unknown(): void { $company = $this->company(); $account = $this->selectedAccount($company); $service = new QccCallService(); try { $service->call( $company, 'enterprise-profile', 'request-classifier-error', fn() => [], function () { throw new RuntimeException('响应字段不完整'); } ); $this->fail('Expected classifier exception was not thrown.'); } catch (RuntimeException $exception) { $this->assertSame('响应字段不完整', $exception->getMessage()); } $account = $account->fresh(); $this->assertSame(CompanyQccAccount::STATUS_UNKNOWN, $account->status); $this->assertStringContainsString('调用结果判定异常', $account->unknown_note); } protected function company(): Company { $id = DB::table('companies')->insertGetId([ 'company_name' => '测试企业', 'credit_code' => '91320100TEST00001', 'created_at' => now(), 'updated_at' => now(), ]); return Company::findOrFail($id); } protected function selectedAccount(Company $company, string $status = CompanyQccAccount::STATUS_SELECTED): CompanyQccAccount { return CompanyQccAccount::create([ 'company_id' => $company->id, 'status' => $status, 'selected_at' => now(), 'first_success_at' => $status === CompanyQccAccount::STATUS_OCCUPIED ? now()->subMinute() : null, 'first_success_source' => $status === CompanyQccAccount::STATUS_OCCUPIED ? 'seed' : null, 'first_success_ref' => $status === CompanyQccAccount::STATUS_OCCUPIED ? 'seed-ref' : null, ]); } }