initialize(); final class PromotionDeleteApiFake extends QywxCustomerAcquisitionApiService { /** @var list */ public array $deleted = []; public function __construct( private string $failLinkId = '', private string $failMessage = 'mock remote deletion failed', private int $failCode = 0 ) { } public function deleteLink(string $linkId): void { $this->deleted[] = $linkId; if ($linkId === $this->failLinkId) { throw new RuntimeException($this->failMessage, $this->failCode); } } } $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 管理员,无法执行删除行为测试'); } $now = time(); $suffix = bin2hex(random_bytes(6)); $firstRemoteId = 'contract_first_' . $suffix; $secondRemoteId = 'contract_second_' . $suffix; Db::startTrans(); try { $poolId = (int) Db::name('qywx_promotion_pool')->insertGetId([ 'name' => '删除契约测试', 'public_key' => bin2hex(random_bytes(16)), 'status' => 1, 'owner_admin_id' => (int) $admin['id'], 'create_time' => $now, 'update_time' => $now, ]); $firstLinkId = (int) Db::name('qywx_promotion_link')->insertGetId([ 'pool_id' => $poolId, 'name' => '当前官方链接', 'wecom_url' => 'https://work.weixin.qq.com/ca/' . $firstRemoteId, 'remote_link_id' => $firstRemoteId, 'remote_status' => 1, 'status' => 1, 'owner_admin_id' => (int) $admin['id'], 'create_time' => $now, 'update_time' => $now, ]); $secondLinkId = (int) Db::name('qywx_promotion_link')->insertGetId([ 'pool_id' => $poolId, 'name' => '历史已软删官方链接', 'wecom_url' => 'https://work.weixin.qq.com/ca/' . $secondRemoteId, 'remote_link_id' => $secondRemoteId, 'remote_status' => 1, 'status' => 0, 'owner_admin_id' => (int) $admin['id'], 'create_time' => $now, 'update_time' => $now, 'delete_time' => $now - 10, ]); $memberId = (int) Db::name('qywx_promotion_pool_member')->insertGetId([ 'pool_id' => $poolId, 'admin_id' => (int) $admin['id'], 'userid' => 'contract_user_' . $suffix, 'enabled' => 1, 'create_time' => $now, 'update_time' => $now, ]); Db::name('qywx_promotion_range_sync')->insert([ 'pool_id' => $poolId, 'promotion_link_id' => $firstLinkId, 'desired_version' => 1, 'applied_version' => 1, 'status' => 0, 'create_time' => $now, 'update_time' => $now, ]); $firstAttempt = new PromotionDeleteApiFake( $secondRemoteId, '企业微信获客助手接口失败[40058]:invalid range', 40058 ); $message = ''; try { WecomPromotionLogic::deletePool($poolId, (int) $admin['id'], $admin, $firstAttempt); } catch (RuntimeException $e) { $message = $e->getMessage(); } $assert(str_contains($message, '本地方案已保留'), '远端部分删除失败没有明确保留本地方案'); $assert($firstAttempt->deleted === [$firstRemoteId, $secondRemoteId], '首次删除没有按顺序处理全部远端链接'); $assert(Db::name('qywx_promotion_pool')->where('id', $poolId)->whereNull('delete_time')->count() === 1, '远端失败时错误删除了本地方案'); $assert((int) Db::name('qywx_promotion_link')->where('id', $firstLinkId)->value('remote_status') === 2, '部分成功的远端删除进度没有保存'); $assert((int) Db::name('qywx_promotion_link')->where('id', $secondLinkId)->value('remote_status') === 1, '失败的远端链接被错误标记为已删除'); $assert((int) Db::name('qywx_promotion_pool_member')->where('id', $memberId)->value('enabled') === 1, '远端失败时错误禁用了方案成员'); $retry = new PromotionDeleteApiFake(); WecomPromotionLogic::deletePool($poolId, (int) $admin['id'], $admin, $retry); $assert($retry->deleted === [$secondRemoteId], '重试时重复删除了已经成功的远端链接'); $assert(Db::name('qywx_promotion_pool')->where('id', $poolId)->whereNotNull('delete_time')->count() === 1, '远端全部成功后没有软删除本地方案'); $assert((int) Db::name('qywx_promotion_link')->where('id', $secondLinkId)->value('remote_status') === 2, '历史软删链接没有同步删除企业微信端'); $assert((int) Db::name('qywx_promotion_pool_member')->where('id', $memberId)->value('enabled') === 0, '方案删除后成员仍处于启用状态'); $sync = Db::name('qywx_promotion_range_sync')->where('pool_id', $poolId)->find(); $assert((int) ($sync['status'] ?? 0) === 4 && (string) ($sync['lock_token'] ?? '') === '', '方案删除后同步租约没有终止'); // 企业微信明确返回 invalid link_id,说明远端目标已不存在或当前应用已无法操作; // 删除应保持幂等并继续清理本地方案,不能让陈旧 link_id 永久阻塞用户。 $staleRemoteId = 'contract_stale_' . $suffix; $stalePoolId = (int) Db::name('qywx_promotion_pool')->insertGetId([ 'name' => '失效链接删除测试', 'public_key' => bin2hex(random_bytes(16)), 'status' => 1, 'owner_admin_id' => (int) $admin['id'], 'create_time' => $now, 'update_time' => $now, ]); $staleLinkId = (int) Db::name('qywx_promotion_link')->insertGetId([ 'pool_id' => $stalePoolId, 'name' => '远端已失效链接', 'wecom_url' => 'https://work.weixin.qq.com/ca/' . $staleRemoteId, 'remote_link_id' => $staleRemoteId, 'remote_status' => 1, 'status' => 1, 'owner_admin_id' => (int) $admin['id'], 'create_time' => $now, 'update_time' => $now, ]); $staleApi = new PromotionDeleteApiFake( $staleRemoteId, '企业微信获客助手接口失败[40058]:invalid link_id', 40058 ); WecomPromotionLogic::deletePool($stalePoolId, (int) $admin['id'], $admin, $staleApi); $assert($staleApi->deleted === [$staleRemoteId], '失效链接没有请求企业微信删除接口'); $assert( Db::name('qywx_promotion_pool')->where('id', $stalePoolId)->whereNotNull('delete_time')->count() === 1, 'invalid link_id 阻塞了本地方案删除' ); $assert( (int) Db::name('qywx_promotion_link')->where('id', $staleLinkId)->value('remote_status') === 2, 'invalid link_id 没有按远端已删除状态收敛' ); } finally { Db::rollback(); } echo "WECOM_PROMOTION_DELETE_POOL_BEHAVIOR_OK\n";