initialize(); $assert = static function (bool $condition, string $message): void { if (!$condition) { throw new RuntimeException($message); } }; $admin = Db::name('admin')->where('root', 1)->whereNull('delete_time')->find(); if (!$admin) { throw new RuntimeException('未找到 root 管理员,无法执行获客客户统计冒烟测试'); } $assert( Db::name('dev_crontab') ->where('command', 'qywx:retry-customer-acquisition-events') ->whereNull('delete_time') ->count() === 1, '获客回调每分钟重试任务未写入数据库' ); $suffix = bin2hex(random_bytes(5)); $linkId = '__smoke_link_' . $suffix; $externalUserId = '__smoke_external_' . $suffix; $userId = '__smoke_user_' . $suffix; $eventTime = time(); $api = new class($linkId, $externalUserId, $userId) extends QywxCustomerAcquisitionApiService { public function __construct( private string $testLinkId, private string $testExternalUserId, private string $testUserId ) { } public function listCustomers(string $linkId, string $cursor = '', int $limit = 1000): array { return [ 'customer_list' => [[ 'external_userid' => $this->testExternalUserId, 'userid' => $this->testUserId, 'chat_status' => 2, 'state' => 'smoke-sync', ]], 'next_cursor' => '', ]; } public function getChatInfo(string $chatKey): array { return [ 'external_userid' => $this->testExternalUserId, 'userid' => $this->testUserId, 'chat_info' => [ 'link_id' => $this->testLinkId, 'state' => 'smoke-chat', 'recv_msg_cnt' => 5, ], ]; } }; $service = new QywxCustomerAcquisitionCustomerService($api); Db::startTrans(); try { $startMessage = [ 'MsgId' => 'smoke-start-' . $suffix, 'ChangeType' => 'customer_start_chat', 'CreateTime' => $eventTime, 'LinkID' => $linkId, 'ExternalUserID' => $externalUserId, 'UserID' => $userId, 'State' => 'smoke-start', ]; $service->handleCallback($startMessage); $chatMessage = [ 'MsgId' => 'smoke-chat-' . $suffix, 'ChangeType' => 'message_from_customer', 'CreateTime' => $eventTime + 1, 'ChatKey' => 'smoke-chat-key-' . $suffix, ]; $service->handleCallback($chatMessage); $duplicate = $service->handleCallback($chatMessage); $assert(($duplicate['duplicate'] ?? false) === true, '相同 message_from_customer 回调必须幂等'); // 远端客户列表中的 chat_status=2 是“未知”,不能把已由回调确认的状态 1 回退。 $service->syncLink($linkId); $customer = Db::name('qywx_customer_acquisition_customer') ->where('link_id', $linkId) ->where('external_userid', $externalUserId) ->where('userid', $userId) ->find(); $assert((int) ($customer['chat_status'] ?? -1) === 1, '列表同步错误回退了已确认的聊天状态'); $assert((int) ($customer['recv_msg_cnt'] ?? -1) === 5, '累计接收消息数不正确或被重复累加'); $assert((int) ($customer['message_count_known'] ?? 0) === 1, '精确消息次数标识未保存'); $stats = WecomAcquisitionCustomerLogic::statistics( ['keyword' => $externalUserId, 'page_size' => 20], (int) $admin['id'], $admin ); $summary = $stats['summary'] ?? []; $assert((int) ($summary['customer_count'] ?? 0) === 1, '获客客户汇总数量不正确'); $assert((int) ($summary['started_chat_count'] ?? 0) === 1, '已发消息客户数不正确'); $assert((int) ($summary['received_message_count'] ?? 0) === 5, '接收消息汇总不正确'); $assert((int) ($summary['message_count_known_count'] ?? 0) === 1, '精确消息统计覆盖数不正确'); $row = $stats['lists'][0] ?? []; $assert(!array_key_exists('external_userid', $row), '接口不应返回原始客户 ExternalUserID'); $assert(str_contains((string) ($row['external_userid_masked'] ?? ''), '*'), '客户标识没有脱敏'); $eventKeys = [ QywxCustomerAcquisitionCustomerService::eventKey($startMessage, 'customer_start_chat', '', $eventTime), QywxCustomerAcquisitionCustomerService::eventKey( $chatMessage, 'message_from_customer', (string) $chatMessage['ChatKey'], $eventTime + 1 ), ]; $events = Db::name('qywx_customer_acquisition_event') ->whereIn('event_key', $eventKeys) ->select()->toArray(); $assert(count($events) === 2, '回调事件审计数量不正确'); foreach ($events as $event) { $assert((int) ($event['status'] ?? 0) === 1, '成功回调未进入成功终态'); $assert((string) ($event['chat_key'] ?? '') === '', '成功后仍保存了敏感 ChatKey'); $assert(($event['raw_json'] ?? null) === null, '成功后仍保存了原始回调'); } echo "WECOM_ACQUISITION_CUSTOMER_STATISTICS_SMOKE_OK messages=5\n"; } finally { Db::rollback(); }