lion 2 weeks ago
commit 7c14e3cb03

@ -0,0 +1,105 @@
<?php
namespace App\Console\Commands;
use App\Exceptions\YuanhePackInfoException;
use App\Models\Company;
use App\Models\CompanyQccAccount;
use App\Repositories\YuanheRepository;
use App\Services\QccCallService;
use Illuminate\Console\Command;
use Throwable;
class VerifyQccEnterprisePackInfo extends Command
{
protected $signature = 'qcc:verify-pack-info
{company_id : 已纳入候选池的本地企业 ID}
{--raw : 输出元禾完整响应 JSON}';
protected $description = '调用一次元禾企业户聚合接口 packInfo验证企业户占额状态流转';
public function handle(): int
{
$company = Company::find($this->argument('company_id'));
if (!$company) {
$this->error('企业不存在。');
return self::FAILURE;
}
$account = $company->qccAccount;
if (!$account || $account->status !== CompanyQccAccount::STATUS_SELECTED) {
$status = $account ? $account->status : 'none';
$this->error("企业必须处于候选状态selected当前状态{$status}。");
return self::FAILURE;
}
if (empty(trim((string) $company->credit_code))) {
$this->error('企业缺少统一社会信用代码,无法调用 packInfo。');
return self::FAILURE;
}
$requestRef = sprintf('pack-info-cli-%d-%s', $company->id, now()->format('YmdHisv'));
$yuanheRepository = new YuanheRepository();
$this->info("开始调用 packInfo{$company->company_name}{$company->credit_code}");
$this->line("请求引用:{$requestRef}");
try {
$response = (new QccCallService())->call(
$company,
'enterprise-pack-info-cli',
$requestRef,
fn () => $yuanheRepository->enterprisePackInfo([
'creditCode' => $company->credit_code,
'enterpriseName' => $company->company_name,
]),
fn (array $result) => array_key_exists('code', $result) && (int) $result['code'] === 0,
fn (array $result) => !array_key_exists('code', $result)
? '元禾 packInfo 响应缺少业务状态码 code'
: false
);
} catch (Throwable $exception) {
$this->error($exception->getMessage());
$this->line('签名 timestamp' . ($yuanheRepository->lastRequestTimestamp ?? '(未生成)'));
if ($exception instanceof YuanhePackInfoException) {
$this->table(
['HTTP 状态', 'Content-Type', '响应字节数'],
[[
$exception->httpStatus ?? '(无响应)',
$exception->contentType ?? '(未提供)',
$exception->rawResponse === null ? 0 : strlen($exception->rawResponse),
]]
);
if ($this->option('raw') && $exception->rawResponse !== null) {
$this->line('原始响应:');
$this->line($exception->rawResponse);
}
}
$this->line('调用后的企业户状态:' . $account->fresh()->status);
return self::FAILURE;
}
$freshAccount = $account->fresh();
$this->line('签名 timestamp' . ($yuanheRepository->lastRequestTimestamp ?? '(未生成)'));
$this->table(
['元禾 code', '元禾 msg', '调用后状态', '首次成功引用'],
[[
$response['code'] ?? '(缺失)',
$response['msg'] ?? '',
$freshAccount->status,
$freshAccount->first_success_ref ?? '',
]]
);
if ($this->option('raw')) {
$this->line(json_encode($response, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
}
if (($response['code'] ?? null) !== 0 && ($response['code'] ?? null) !== '0') {
$this->warn('元禾未返回明确成功code !== 0企业保持候选状态。');
return self::FAILURE;
}
$this->info('元禾返回明确成功,企业已标记为已占额。');
return self::SUCCESS;
}
}

@ -0,0 +1,18 @@
<?php
namespace App\Exceptions;
use RuntimeException;
class YuanhePackInfoException extends RuntimeException
{
public function __construct(
string $message,
public readonly ?int $httpStatus = null,
public readonly ?string $contentType = null,
public readonly ?string $rawResponse = null,
?\Throwable $previous = null
) {
parent::__construct($message, 0, $previous);
}
}

@ -0,0 +1,156 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Helpers\ResponseCode;
use App\Models\Company;
use App\Models\CompanyQccAccount;
use App\Models\OperateLog;
use Illuminate\Http\Request;
use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use RuntimeException;
class QccEnterpriseAccountController extends CommonController
{
public function candidates(Request $request)
{
$keyword = trim((string) $request->input('keyword', ''));
$status = $request->input('status');
$pageSize = min(max((int) $request->input('page_size', 20), 1), 100);
$query = Company::query()->select('id', 'company_name', 'credit_code', 'updated_at')->with('qccAccount');
if ($keyword !== '') {
$query->where(function ($subQuery) use ($keyword) {
$subQuery->where('company_name', 'like', '%' . $keyword . '%')
->orWhere('credit_code', 'like', '%' . $keyword . '%');
});
}
if ($status === 'none') {
$query->doesntHave('qccAccount');
} elseif (in_array($status, [CompanyQccAccount::STATUS_SELECTED, CompanyQccAccount::STATUS_OCCUPIED, CompanyQccAccount::STATUS_UNKNOWN], true)) {
$query->whereHas('qccAccount', fn($relation) => $relation->where('status', $status));
}
return $this->success($query->orderByDesc('id')->paginate($pageSize));
}
public function index(Request $request)
{
$status = $request->input('status');
$pageSize = min(max((int) $request->input('page_size', 20), 1), 100);
$query = CompanyQccAccount::query()->with('company:id,company_name,credit_code');
if (in_array($status, [CompanyQccAccount::STATUS_SELECTED, CompanyQccAccount::STATUS_OCCUPIED, CompanyQccAccount::STATUS_UNKNOWN], true)) {
$query->where('status', $status);
}
return $this->success($query->orderByDesc('id')->paginate($pageSize));
}
public function select(Request $request)
{
$validator = Validator::make($request->all(), ['company_ids' => 'required|array|min:1', 'company_ids.*' => 'integer|distinct']);
if ($validator->fails()) {
return $this->fail([ResponseCode::ERROR_PARAMETER, implode(',', $validator->errors()->all())]);
}
$companyIds = $request->input('company_ids');
$companies = Company::query()->whereIn('id', $companyIds)->get()->keyBy('id');
foreach ($companyIds as $companyId) {
$company = $companies->get($companyId);
if (!$company || empty(trim((string) $company->credit_code))) {
return $this->fail([ResponseCode::ERROR_BUSINESS, '存在不存在或缺少统一社会信用代码的企业']);
}
}
try {
DB::transaction(function () use ($companyIds) {
$existingCompanyIds = CompanyQccAccount::query()->whereIn('company_id', $companyIds)->lockForUpdate()->pluck('company_id');
if ($existingCompanyIds->isNotEmpty()) {
throw new RuntimeException('存在已纳入企业户标注的企业');
}
foreach ($companyIds as $companyId) {
CompanyQccAccount::query()->create([
'company_id' => $companyId,
'status' => CompanyQccAccount::STATUS_SELECTED,
'selected_by' => $this->getUserId(),
'selected_at' => now(),
]);
}
});
} catch (RuntimeException|QueryException $exception) {
return $this->fail([ResponseCode::ERROR_BUSINESS, $exception->getMessage()]);
}
$this->log('纳入企查查企业户候选池', implode(',', $companyIds));
return $this->success('已纳入候选池');
}
public function cancel(Request $request)
{
$validator = Validator::make($request->all(), ['id' => 'required|integer']);
if ($validator->fails()) {
return $this->fail([ResponseCode::ERROR_PARAMETER, implode(',', $validator->errors()->all())]);
}
try {
$companyId = DB::transaction(function () use ($request) {
$account = CompanyQccAccount::query()->whereKey($request->input('id'))->lockForUpdate()->first();
if (!$account) {
throw new RuntimeException('企业户关系不存在');
}
if ($account->status !== CompanyQccAccount::STATUS_SELECTED) {
throw new RuntimeException('仅候选状态可以取消');
}
$companyId = $account->company_id;
$account->delete();
return $companyId;
});
} catch (RuntimeException $exception) {
return $this->fail([ResponseCode::ERROR_BUSINESS, $exception->getMessage()]);
}
$this->log('取消企查查企业户候选关系', (string) $companyId);
return $this->success('已取消候选关系');
}
public function unknownConfirm(Request $request)
{
$validator = Validator::make($request->all(), [
'id' => 'required|integer',
'decision' => 'required|in:retry,occupied',
'evidence' => 'required|string|max:2000',
]);
if ($validator->fails()) {
return $this->fail([ResponseCode::ERROR_PARAMETER, implode(',', $validator->errors()->all())]);
}
$decision = $request->input('decision');
$evidence = trim($request->input('evidence'));
try {
$account = DB::transaction(function () use ($request, $decision, $evidence) {
$account = CompanyQccAccount::query()->whereKey($request->input('id'))->lockForUpdate()->first();
if (!$account || $account->status !== CompanyQccAccount::STATUS_UNKNOWN) {
throw new RuntimeException('仅未知状态可以人工确认');
}
if ($decision === 'occupied') {
$account->update([
'status' => CompanyQccAccount::STATUS_OCCUPIED,
'first_success_at' => now(),
'first_success_source' => 'manual-confirmation',
'first_success_ref' => $evidence,
'unknown_note' => $evidence,
]);
} else {
$account->update(['status' => CompanyQccAccount::STATUS_SELECTED, 'unknown_note' => $evidence]);
}
return $account->fresh();
});
} catch (RuntimeException $exception) {
return $this->fail([ResponseCode::ERROR_BUSINESS, $exception->getMessage()]);
}
$this->log('人工确认企查查企业户未知状态:' . $decision, $evidence);
return $this->success($account);
}
protected function log(string $name, string $remark = ''): void
{
$admin = $this->getUser();
if ($admin) {
OperateLog::addLogs($admin, $name, $remark);
}
}
}

@ -24,6 +24,11 @@ class Company extends SoftDeletesModel
return $this->hasMany(User::class, 'company_id', 'id'); return $this->hasMany(User::class, 'company_id', 'id');
} }
public function qccAccount()
{
return $this->hasOne(CompanyQccAccount::class, 'company_id', 'id');
}
/** /**
* 限制只返回有关联学员且至少有一条审核通过的报名记录的公司 * 限制只返回有关联学员且至少有一条审核通过的报名记录的公司
* 用于列表查询和统计查询 * 用于列表查询和统计查询

@ -0,0 +1,27 @@
<?php
namespace App\Models;
class CompanyQccAccount extends CommonModel
{
public const STATUS_SELECTED = 'selected';
public const STATUS_OCCUPIED = 'occupied';
public const STATUS_UNKNOWN = 'unknown';
protected $casts = [
'selected_at' => 'datetime:Y-m-d H:i:s',
'first_success_at' => 'datetime:Y-m-d H:i:s',
'created_at' => 'datetime:Y-m-d H:i:s',
'updated_at' => 'datetime:Y-m-d H:i:s',
];
public function company()
{
return $this->belongsTo(Company::class);
}
public function isCallable(): bool
{
return in_array($this->status, [self::STATUS_SELECTED, self::STATUS_OCCUPIED], true);
}
}

@ -2,6 +2,7 @@
namespace App\Repositories; namespace App\Repositories;
use App\Exceptions\YuanhePackInfoException;
use App\Models\AppointmentConfig; use App\Models\AppointmentConfig;
use App\Models\Course; use App\Models\Course;
use App\Models\ThirdAppointmentLog; use App\Models\ThirdAppointmentLog;
@ -15,6 +16,7 @@ class YuanheRepository
public $baseUrl; public $baseUrl;
public $customerId; public $customerId;
public $authKey; public $authKey;
public $lastRequestTimestamp;
public function __construct() public function __construct()
{ {
@ -26,7 +28,8 @@ class YuanheRepository
public function getHeader() public function getHeader()
{ {
$timestamp = time() * 1000; $timestamp = (string) floor(microtime(true) * 1000);
$this->lastRequestTimestamp = $timestamp;
$token = $this->customerId . $timestamp . $this->authKey; $token = $this->customerId . $timestamp . $this->authKey;
$token = md5($token); $token = md5($token);
$token = strtoupper($token); $token = strtoupper($token);
@ -80,6 +83,58 @@ class YuanheRepository
} }
} }
/**
* 查询企业户聚合信息。
*
* 此方法保留元禾响应外层的 code/data/msg/status供调用方判定一次
* 企业户调用是否明确成功;不要像 companyInfo 一样仅返回 data。
*/
public function enterprisePackInfo(array $params): array
{
$url = $this->baseUrl . '/master-service/openapi/businessCollege/enterprise/packInfo';
$header = $this->getHeader();
$ch = curl_init();
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3);
curl_setopt($ch, CURLOPT_TIMEOUT, 3);
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($params, JSON_UNESCAPED_UNICODE));
if (stripos($url, 'https://') !== false) {
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_SSLVERSION, 1);
}
$raw = curl_exec($ch);
$curlError = curl_error($ch);
$httpStatus = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE) ?: null;
curl_close($ch);
if ($raw === false) {
throw new YuanhePackInfoException(
'元禾 packInfo 网络调用失败:' . ($curlError ?: '未知 cURL 错误'),
$httpStatus ?: null,
$contentType
);
}
$result = json_decode($raw, true);
if (!is_array($result)) {
throw new YuanhePackInfoException(
'元禾 packInfo 响应不是有效 JSON 对象:' . json_last_error_msg(),
$httpStatus ?: null,
$contentType,
$raw
);
}
return $result;
}
/** /**
* 数据推送 * 数据推送
*/ */

@ -0,0 +1,96 @@
<?php
namespace App\Services;
use App\Models\Company;
use App\Models\CompanyQccAccount;
use Illuminate\Support\Facades\DB;
use RuntimeException;
use Throwable;
/**
* 所有企查查(或经元禾穿透)调用的统一状态入口。
* 实际调用由 $operation 传入;调用方必须区分明确成功、明确失败与结果不确定。
*/
class QccCallService
{
public function call(
Company $company,
string $source,
string $requestRef,
callable $operation,
callable $isSuccess,
?callable $isIndeterminate = null
)
{
$account = CompanyQccAccount::query()->where('company_id', $company->id)->first();
if (!$account) {
throw new RuntimeException('企业未纳入企查查企业户候选池');
}
if (!$account->isCallable()) {
throw new RuntimeException('企业户状态待人工确认,暂不能发起调用');
}
try {
$result = $operation();
} catch (Throwable $exception) {
$this->markUnknownWhenSelected($company->id, $exception->getMessage());
throw $exception;
}
try {
if ($isSuccess($result)) {
$this->recordSuccess($company->id, $source, $requestRef);
} elseif ($isIndeterminate) {
$indeterminate = $isIndeterminate($result);
if ($indeterminate) {
$reason = is_string($indeterminate) ? $indeterminate : '调用结果无法确认';
$this->markUnknownWhenSelected($company->id, $reason);
}
}
} catch (Throwable $exception) {
$this->markUnknownWhenSelected($company->id, '调用结果判定异常:' . $exception->getMessage());
throw $exception;
}
return $result;
}
public function recordSuccess(int $companyId, string $source, string $requestRef): CompanyQccAccount
{
return DB::transaction(function () use ($companyId, $source, $requestRef) {
$account = CompanyQccAccount::query()->where('company_id', $companyId)->lockForUpdate()->first();
if (!$account) {
throw new RuntimeException('企业未纳入企查查企业户候选池');
}
if ($account->status === CompanyQccAccount::STATUS_UNKNOWN) {
throw new RuntimeException('企业户状态待人工确认,不能确认调用成功');
}
if ($account->status === CompanyQccAccount::STATUS_SELECTED) {
$account->update([
'status' => CompanyQccAccount::STATUS_OCCUPIED,
'first_success_at' => now(),
'first_success_source' => $source,
'first_success_ref' => $requestRef,
'unknown_note' => null,
]);
}
return $account->fresh();
});
}
public function markUnknownWhenSelected(int $companyId, string $reason): ?CompanyQccAccount
{
return DB::transaction(function () use ($companyId, $reason) {
$account = CompanyQccAccount::query()->where('company_id', $companyId)->lockForUpdate()->first();
if (!$account || $account->status !== CompanyQccAccount::STATUS_SELECTED) {
return $account;
}
$account->update([
'status' => CompanyQccAccount::STATUS_UNKNOWN,
'unknown_note' => mb_substr($reason, 0, 2000),
]);
return $account->fresh();
});
}
}

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::create('company_qcc_accounts', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('company_id')->unique()->comment('企业ID');
$table->string('status', 20)->index()->comment('selected/occupied/unknown');
$table->unsignedBigInteger('selected_by')->nullable()->comment('选择操作人');
$table->dateTime('selected_at')->comment('纳入候选池时间');
$table->dateTime('first_success_at')->nullable()->comment('首次调用成功时间');
$table->string('first_success_source')->nullable()->comment('首次成功来源');
$table->string('first_success_ref')->nullable()->comment('首次成功请求引用');
$table->text('unknown_note')->nullable()->comment('未知原因或人工核验说明');
$table->timestamps();
$table->foreign('company_id')->references('id')->on('companies')->restrictOnDelete();
});
}
public function down()
{
Schema::dropIfExists('company_qcc_accounts');
}
};

@ -19,6 +19,7 @@
</coverage> </coverage>
<php> <php>
<env name="APP_ENV" value="testing"/> <env name="APP_ENV" value="testing"/>
<env name="APP_KEY" value="base64:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="/>
<env name="BCRYPT_ROUNDS" value="4"/> <env name="BCRYPT_ROUNDS" value="4"/>
<env name="CACHE_DRIVER" value="array"/> <env name="CACHE_DRIVER" value="array"/>
<!-- <env name="DB_CONNECTION" value="sqlite"/> --> <!-- <env name="DB_CONNECTION" value="sqlite"/> -->

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -50,6 +50,13 @@ Route::group(["namespace" => "Admin", "prefix" => "admin"], function () {
// 清除缓存 // 清除缓存
Route::post('other/clear-cache', [\App\Http\Controllers\Admin\OtherController::class, "clearCache"]); Route::post('other/clear-cache', [\App\Http\Controllers\Admin\OtherController::class, "clearCache"]);
// 企查查企业户标注
Route::get('qcc-enterprise-accounts/candidates', [\App\Http\Controllers\Admin\QccEnterpriseAccountController::class, 'candidates']);
Route::get('qcc-enterprise-accounts/index', [\App\Http\Controllers\Admin\QccEnterpriseAccountController::class, 'index']);
Route::post('qcc-enterprise-accounts/select', [\App\Http\Controllers\Admin\QccEnterpriseAccountController::class, 'select']);
Route::post('qcc-enterprise-accounts/cancel', [\App\Http\Controllers\Admin\QccEnterpriseAccountController::class, 'cancel']);
Route::post('qcc-enterprise-accounts/unknown-confirm', [\App\Http\Controllers\Admin\QccEnterpriseAccountController::class, 'unknownConfirm']);
// 课程管理 // 课程管理
Route::get('courses/index', [\App\Http\Controllers\Admin\CourseController::class, "index"]); Route::get('courses/index', [\App\Http\Controllers\Admin\CourseController::class, "index"]);
Route::get('courses/show', [\App\Http\Controllers\Admin\CourseController::class, "show"]); Route::get('courses/show', [\App\Http\Controllers\Admin\CourseController::class, "show"]);

@ -2,7 +2,6 @@
namespace Tests\Feature; namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase; use Tests\TestCase;
class ExampleTest extends TestCase class ExampleTest extends TestCase
@ -12,10 +11,10 @@ class ExampleTest extends TestCase
* *
* @return void * @return void
*/ */
public function test_the_application_returns_a_successful_response() public function test_root_redirects_to_admin_application()
{ {
$response = $this->get('/'); $response = $this->get('/');
$response->assertStatus(200); $response->assertRedirect('/admin/index.html');
} }
} }

@ -0,0 +1,189 @@
<?php
namespace Tests\Unit\Services;
use App\Models\Company;
use App\Models\CompanyQccAccount;
use App\Services\QccCallService;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use RuntimeException;
use Tests\TestCase;
class QccCallServiceTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
config([
'database.default' => '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,
]);
}
}
Loading…
Cancel
Save