initialize(); Config::set([ 'corp_id' => 'ww_test_corp', 'secret' => 'test_secret', 'base_uri' => 'https://qyapi.weixin.qq.com', 'timeout' => 5, ], 'qywx_customer_acquisition'); $json = static fn (array $data): Response => new Response(200, ['Content-Type' => 'application/json'], json_encode($data)); $mock = new MockHandler([ $json(['errcode' => 0, 'link_id_list' => ['link_1'], 'next_cursor' => 'cursor_2']), $json(['errcode' => 0, 'link' => ['link_id' => 'link_1', 'link_name' => '官网获客', 'url' => 'https://work.weixin.qq.com/ca/test']]), $json(['errcode' => 0, 'link_id' => 'link_2', 'url' => 'https://work.weixin.qq.com/ca/new']), $json(['errcode' => 0]), $json(['errcode' => 0]), $json(['errcode' => 0, 'customer_list' => [[ 'external_userid' => 'wm_customer_1', 'userid' => 'zhangsan', 'chat_status' => 1, 'state' => 'landing-page', ]], 'next_cursor' => 'customer_cursor_2']), $json(['errcode' => 0, 'external_userid' => 'wm_customer_1', 'userid' => 'zhangsan', 'chat_info' => [ 'link_id' => 'link_1', 'state' => 'landing-page', 'recv_msg_cnt' => 3, ]]), $json(['errcode' => 0, 'link_id_list' => []]), ]); $history = []; $stack = HandlerStack::create($mock); $stack->push(Middleware::history($history)); $service = new QywxCustomerAcquisitionApiService(new Client([ 'base_uri' => 'https://qyapi.weixin.qq.com/', 'handler' => $stack, 'http_errors' => false, ]), static fn (): string => 'mock_token'); $list = $service->listLinks('', 100); $detail = $service->getLink('link_1'); $created = $service->createLink(['link_name' => '官网获客', 'range' => ['user_list' => ['zhangsan', 'lisi']]]); $service->updateLink([ 'link_id' => 'link_1', 'link_name' => '官网获客-更新', 'range' => ['user_list' => ['lisi', 'zhangsan']], ]); $service->deleteLink('link_1'); $customers = $service->listCustomers('link_1', '', 1000); $chat = $service->getChatInfo('chat_key_1'); $permission = $service->checkPermission(); $assert = static function (bool $condition, string $message): void { if (!$condition) { throw new RuntimeException($message); } }; $assert($list['link_id_list'] === ['link_1'] && $list['next_cursor'] === 'cursor_2', 'list_link 返回解析失败'); $assert(($detail['link']['link_id'] ?? '') === 'link_1', 'get 返回解析失败'); $assert(($created['link_id'] ?? '') === 'link_2', 'create_link 返回解析失败'); $assert(($customers['customer_list'][0]['external_userid'] ?? '') === 'wm_customer_1', 'customer 客户列表解析失败'); $assert(($customers['next_cursor'] ?? '') === 'customer_cursor_2', 'customer 游标解析失败'); $assert(($chat['chat_info']['recv_msg_cnt'] ?? 0) === 3, 'get_chat_info 累计消息数解析失败'); $assert(($permission['ok'] ?? false) === true, '权限验证失败'); $assert(($permission['has_link'] ?? true) === false, '空链接列表应明确返回 has_link=false'); $assert(str_contains((string) ($permission['message'] ?? ''), '尚未'), '空链接列表应返回可操作的诊断提示'); $expectedPaths = [ '/cgi-bin/externalcontact/customer_acquisition/list_link', '/cgi-bin/externalcontact/customer_acquisition/get', '/cgi-bin/externalcontact/customer_acquisition/create_link', '/cgi-bin/externalcontact/customer_acquisition/update_link', '/cgi-bin/externalcontact/customer_acquisition/delete_link', '/cgi-bin/externalcontact/customer_acquisition/customer', '/cgi-bin/externalcontact/customer_acquisition/get_chat_info', '/cgi-bin/externalcontact/customer_acquisition/list_link', ]; $actualPaths = array_map(static fn (array $entry): string => $entry['request']->getUri()->getPath(), $history); $assert($actualPaths === $expectedPaths, '请求端点不正确:' . implode(', ', $actualPaths)); $createPayload = json_decode((string) $history[2]['request']->getBody(), true); $assert(($createPayload['range']['user_list'] ?? []) === ['zhangsan', 'lisi'], 'create_link 多人成员范围请求体不正确'); $updatePayload = json_decode((string) $history[3]['request']->getBody(), true); $assert(($updatePayload['range']['user_list'] ?? []) === ['lisi', 'zhangsan'], 'update_link 多人成员范围请求体不正确'); $deletePayload = json_decode((string) $history[4]['request']->getBody(), true); $assert(($deletePayload['link_id'] ?? '') === 'link_1', 'delete_link 请求体缺少官方 link_id'); $customerPayload = json_decode((string) $history[5]['request']->getBody(), true); $assert(($customerPayload['link_id'] ?? '') === 'link_1' && ($customerPayload['limit'] ?? 0) === 1000, 'customer 请求体不正确'); $chatPayload = json_decode((string) $history[6]['request']->getBody(), true); $assert(($chatPayload['chat_key'] ?? '') === 'chat_key_1', 'get_chat_info 请求体不正确'); $invalidUserService = new QywxCustomerAcquisitionApiService(new Client([ 'base_uri' => 'https://qyapi.weixin.qq.com/', 'handler' => HandlerStack::create(new MockHandler([ $json(['errcode' => 60111, 'errmsg' => "invalid string value `XiongCaiQian`: userid not found"]), ])), 'http_errors' => false, ]), static fn (): string => 'mock_token'); $invalidUserMessage = ''; try { $invalidUserService->createLink([ 'link_name' => '无效成员测试', 'range' => ['user_list' => ['XiongCaiQian']], ]); } catch (RuntimeException $e) { $invalidUserMessage = $e->getMessage(); } $assert(str_contains($invalidUserMessage, 'userid 不存在'), '60111 应返回明确的成员绑定诊断提示'); $assert(str_contains($invalidUserMessage, '应用可见范围'), '60111 应提示检查应用可见范围'); $invalidLinkService = new QywxCustomerAcquisitionApiService(new Client([ 'base_uri' => 'https://qyapi.weixin.qq.com/', 'handler' => HandlerStack::create(new MockHandler([ $json(['errcode' => 40058, 'errmsg' => 'invalid link_id']), ])), 'http_errors' => false, ]), static fn (): string => 'mock_token'); $invalidLinkCode = 0; try { $invalidLinkService->deleteLink('stale_link_id'); } catch (RuntimeException $e) { $invalidLinkCode = $e->getCode(); } $assert($invalidLinkCode === 40058, '企业微信错误码应保留在异常 code 中供业务层分类'); echo "QYWX_CUSTOMER_ACQUISITION_API_TEST_OK\n";