Files
zyt/server/tests/WecomAcquisitionCustomerStatisticsSmokeTest.php
2026-08-24 16:56:37 +08:00

186 lines
7.2 KiB
PHP

<?php
declare(strict_types=1);
use app\adminapi\logic\firstvisit\WecomAcquisitionCustomerLogic;
use app\common\service\DataScope\DataScopeService;
use app\common\service\qywx\QywxCustomerAcquisitionApiService;
use app\common\service\qywx\QywxCustomerAcquisitionCustomerService;
use think\facade\Db;
require dirname(__DIR__) . '/vendor/autoload.php';
$app = new think\App();
$app->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();
$scopedAdmin = null;
foreach (Db::name('admin')->where('root', 0)->whereNull('delete_time')->select()->toArray() as $candidate) {
$candidateVisibleIds = DataScopeService::getVisibleAdminIds((int) $candidate['id'], $candidate);
if (is_array($candidateVisibleIds) && $candidateVisibleIds !== []) {
$scopedAdmin = $candidate;
break;
}
}
$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' => '',
]],
'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 {
$linkOwnerId = (int) ($scopedAdmin['id'] ?? $admin['id']);
$localLinkId = (int) Db::name('qywx_promotion_link')->insertGetId([
'pool_id' => 0,
'account_id' => 0,
'name' => '空来源统计冒烟链接',
'group_name' => '测试',
'wecom_url' => 'https://work.weixin.qq.com/ca/smoke',
'remote_link_id' => $linkId,
'remote_status' => 1,
'owner_admin_id' => $linkOwnerId,
'dept_id' => 0,
'create_time' => $eventTime,
'update_time' => $eventTime,
]);
$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, '精确消息次数标识未保存');
$assert((string) ($customer['state'] ?? 'missing') === '', '企业微信返回空来源时客户未按原样保存');
$assert((int) ($customer['promotion_link_id'] ?? 0) === $localLinkId, '客户未关联本地获客链接');
$assert((int) ($customer['owner_admin_id'] ?? -1) === 0, '冒烟客户应保持为未匹配成员');
$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'] ?? ''), '*'), '客户标识没有脱敏');
if ($scopedAdmin !== null) {
$scopedStats = WecomAcquisitionCustomerLogic::statistics(
['keyword' => $externalUserId, 'page_size' => 20],
(int) $scopedAdmin['id'],
$scopedAdmin
);
$assert(
(int) ($scopedStats['summary']['customer_count'] ?? 0) === 1,
'空来源且成员未匹配时,链接归属范围内仍应显示企微返回的客户'
);
$assert(count($scopedStats['lists'] ?? []) === 1, '空来源客户未进入有限权限账号的统计列表');
}
$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();
}