This commit is contained in:
Your Name
2026-08-25 14:16:31 +08:00
parent f24afa116f
commit af603a4e9a
4 changed files with 95 additions and 20 deletions
@@ -434,21 +434,25 @@ class WecomPromotionLogic
try {
$api->deleteLink($remoteLinkId);
} catch (\Throwable $e) {
$message = '企业微信官方获客链接删除失败,本地方案已保留:' . $e->getMessage();
Db::name('qywx_promotion_link')->where('id', $linkId)->update([
'sync_error' => mb_substr($message, 0, 500),
'update_time' => time(),
]);
Db::name('qywx_promotion_range_sync')->where('pool_id', $id)->where('lock_token', $deleteToken)->update([
'status' => 4,
'next_retry' => 0,
'lock_token' => '',
'lock_until' => 0,
'last_error' => mb_substr($message, 0, 500),
'update_time' => time(),
]);
// 删除是幂等操作:企业微信明确返回 invalid link_id 时,远端目标已不存在
// 或已不再属于当前应用,继续完成本地清理;其他错误仍保留方案以便重试。
if (!self::isRemoteLinkAlreadyMissing($e)) {
$message = '企业微信官方获客链接删除失败,本地方案已保留:' . $e->getMessage();
Db::name('qywx_promotion_link')->where('id', $linkId)->update([
'sync_error' => mb_substr($message, 0, 500),
'update_time' => time(),
]);
Db::name('qywx_promotion_range_sync')->where('pool_id', $id)->where('lock_token', $deleteToken)->update([
'status' => 4,
'next_retry' => 0,
'lock_token' => '',
'lock_until' => 0,
'last_error' => mb_substr($message, 0, 500),
'update_time' => time(),
]);
throw new RuntimeException($message, 0, $e);
throw new RuntimeException($message, 0, $e);
}
}
// 多个历史官方链接部分成功时也保存进度,用户重试删除不会再次请求已删除链接。
Db::name('qywx_promotion_link')->where('id', $linkId)->update([
@@ -484,6 +488,16 @@ class WecomPromotionLogic
});
}
private static function isRemoteLinkAlreadyMissing(\Throwable $error): bool
{
$message = strtolower($error->getMessage());
$isInvalidLinkId = str_contains($message, 'invalid link_id');
$isInvalidParameter = (int) $error->getCode() === 40058
|| str_contains($message, '[40058]');
return $isInvalidParameter && $isInvalidLinkId;
}
public static function saveMember(array $params, int $adminId, array $adminInfo): array
{
self::assertMemberDispatchSchema();
@@ -187,7 +187,8 @@ class QywxCustomerAcquisitionApiService
if ($errcode === 60111) {
throw new RuntimeException(
'所选医助的企业微信 userid 不存在,或不在获客助手可调用应用的可见范围;'
. '请检查后台账号绑定和企业微信应用可见范围。企业微信返回:' . $errorMessage
. '请检查后台账号绑定和企业微信应用可见范围。企业微信返回:' . $errorMessage,
$errcode
);
}
@@ -195,7 +196,7 @@ class QywxCustomerAcquisitionApiService
'企业微信获客助手接口失败[%d]%s',
$errcode,
$errorMessage
));
), $errcode);
}
private function accessToken(): string
@@ -121,4 +121,19 @@ try {
$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";
@@ -16,15 +16,18 @@ final class PromotionDeleteApiFake extends QywxCustomerAcquisitionApiService
/** @var list<string> */
public array $deleted = [];
public function __construct(private string $failLinkId = '')
{
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('mock remote deletion failed');
throw new RuntimeException($this->failMessage, $this->failCode);
}
}
}
@@ -94,7 +97,11 @@ try {
'update_time' => $now,
]);
$firstAttempt = new PromotionDeleteApiFake($secondRemoteId);
$firstAttempt = new PromotionDeleteApiFake(
$secondRemoteId,
'企业微信获客助手接口失败[40058]invalid range',
40058
);
$message = '';
try {
WecomPromotionLogic::deletePool($poolId, (int) $admin['id'], $admin, $firstAttempt);
@@ -116,6 +123,44 @@ try {
$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();
}