This commit is contained in:
Your Name
2026-09-09 12:18:17 +08:00
parent 0cea43b027
commit 40319eea41
76 changed files with 4821 additions and 268 deletions
@@ -330,7 +330,8 @@ class WecomPromotionLogic
$createdRemote = true;
try {
$remote = self::normaliseRemoteLink($api->getLink($remoteLinkId), $remoteLinkId);
if (!QywxPromotionMemberRange::same($remote['range_userids'], $eligibleUserIds)) {
if (!QywxPromotionMemberRange::same($remote['range_userids'], $eligibleUserIds)
|| $remote['range_department_ids'] !== []) {
throw new RuntimeException('企业微信返回的多人路由成员范围与方案可用医助不一致');
}
} catch (\Throwable $e) {
@@ -488,9 +489,7 @@ class WecomPromotionLogic
'range_userids' => $eligibleUserIds,
// 前端据此确认标签、欢迎语等扩展配置已和方案一并提交并完成回读校验。
'automation_saved' => $automation !== null,
'sync_error' => $syncError,
'sync_queued' => $needsQueuedSync && !$syncImmediately,
];
] + self::memberSyncResult($id, $syncError);
}
/**
@@ -679,13 +678,13 @@ class WecomPromotionLogic
$memberMatched += $poolMemberResult['matched'];
$memberUpdated += $poolMemberResult['updated'];
}
$syncError = trim((string) ($saved['sync_error'] ?? ''));
$syncResult = self::memberSyncResult($poolId, (string) ($saved['sync_error'] ?? ''));
$syncError = $syncResult['sync_error'];
$updated++;
if ($syncError !== '') {
$syncErrorCount++;
}
$syncQueued = !empty($saved['sync_queued'])
|| !empty($poolMemberResult['dispatch']['queued']);
$syncQueued = $syncResult['sync_queued'];
if ($syncQueued) {
$syncQueuedCount++;
}
@@ -693,6 +692,7 @@ class WecomPromotionLogic
'id' => $poolId,
'name' => (string) ($pool['name'] ?? ''),
'success' => true,
'sync_status' => $syncResult['sync_status'],
'sync_error' => $syncError,
'sync_queued' => $syncQueued,
'member_matched' => $poolMemberResult['matched'],
@@ -754,7 +754,8 @@ class WecomPromotionLogic
]);
}
$dispatch = $updateIds !== []
// 状态相同也需要重算:上次保存可能只入队,远端仍保留已下线成员。
$dispatch = $matchedIds !== []
? QywxPromotionMemberSchedulerService::reconcilePool($poolId)
: null;
// 下线必须能够安全收缩远端范围;上线即使尚未到生效时段,也应先保存规则。
@@ -1235,7 +1236,94 @@ class WecomPromotionLogic
}
}
return ['id' => $id, 'pool_id' => $poolId, 'dispatch' => $planned, 'sync_error' => $syncError];
return ['id' => $id, 'pool_id' => $poolId, 'dispatch' => $planned]
+ self::memberSyncResult($poolId, $syncError);
}
/** 显式重新推送当前范围;一次请求仅处理一个方案,供批量前端逐方案调用。 */
public static function syncMemberRange(
int $poolId,
int $adminId,
array $adminInfo,
?QywxPromotionRangeSyncService $syncService = null
): array {
self::assertMemberDispatchSchema();
if (!QywxPromotionOperatorAccess::hasPagePermission($adminId, $adminInfo)) {
throw new RuntimeException('权限不足');
}
self::assertScopedRow('qywx_promotion_pool', $poolId, $adminId, $adminInfo);
$ready = Db::transaction(static function () use ($poolId): bool {
// 与成员保存、方案删除使用相同锁顺序,不重启删除中的同步任务。
$pool = Db::name('qywx_promotion_pool')->where('id', $poolId)->whereNull('delete_time')->lock(true)->find();
if (!$pool) {
throw new RuntimeException('分流方案不存在或已删除');
}
$sync = Db::name('qywx_promotion_range_sync')->where('pool_id', $poolId)->lock(true)->find();
if ((int) ($sync['status'] ?? 0) === 5
|| ((int) ($sync['status'] ?? 0) === 4
&& str_starts_with((string) ($sync['last_error'] ?? ''), '企业微信官方获客链接删除失败'))) {
return false;
}
$plan = QywxPromotionMemberSchedulerService::reconcilePool($poolId);
if ($plan['blocked']) {
return false;
}
$linkId = (int) (Db::name('qywx_promotion_range_sync')->where('pool_id', $poolId)->value('promotion_link_id') ?? 0);
// 即便快照看起来一致也重新 update/get,修复未被本地快照发现的企微端变化。
QywxPromotionMemberSchedulerService::requestPoolSync($poolId, $linkId);
return true;
});
$error = '';
if ($ready) {
try {
($syncService ?? new QywxPromotionRangeSyncService())->syncPool($poolId);
} catch (\Throwable $e) {
$error = $e->getMessage();
}
}
return self::memberSyncResult($poolId, $error);
}
/** 返回已确认的远端范围及任务状态,不能把无异常的 noop 当成同步成功。 */
private static function memberSyncResult(int $poolId, string $error = ''): array
{
$sync = Db::name('qywx_promotion_range_sync')->where('pool_id', $poolId)->find() ?: [];
$link = Db::name('qywx_promotion_link')->where('id', (int) ($sync['promotion_link_id'] ?? 0))
->whereNull('delete_time')->find() ?: [];
$members = Db::name('qywx_promotion_pool_member')->where('pool_id', $poolId)
->whereNull('delete_time')->select()->toArray();
$range = QywxPromotionMemberRange::evaluate($members, date('Y-m-d'), time(), QywxPromotionConfig::forPool($poolId));
$remoteUsers = self::decodeStringList($link['range_user_json'] ?? null);
$remoteDepartments = self::decodeStringList($link['range_department_json'] ?? null);
$status = (int) ($sync['status'] ?? -1);
$syncError = trim($error ?: (string) ($sync['last_error'] ?? $link['sync_error'] ?? ''));
if ($link === [] || trim((string) ($link['remote_link_id'] ?? '')) === ''
|| (int) ($link['remote_status'] ?? 0) === 2 || $range['userids'] === [] || in_array($status, [4, 5], true)) {
$resultStatus = 'blocked';
$syncError = $syncError ?: '当前没有可同步的官方链接或可用成员,请检查方案和成员规则';
} elseif ($error !== '' || $status === 3) {
$resultStatus = 'failed';
$syncError = $syncError ?: '企业微信成员范围同步失败,请重试';
} elseif ($status === 0 && (int) ($sync['desired_version'] ?? 0) > 0
&& (int) ($sync['desired_version'] ?? 0) === (int) ($sync['applied_version'] ?? -1)
&& (int) ($link['last_sync_time'] ?? 0) > 0 && $remoteDepartments === []
&& QywxPromotionMemberRange::same($range['userids'], $remoteUsers)) {
$resultStatus = 'synced';
$syncError = '';
} else {
$resultStatus = 'pending';
}
return [
'pool_id' => $poolId,
'sync_status' => $resultStatus,
'sync_error' => $syncError,
'sync_queued' => in_array($resultStatus, ['pending', 'failed'], true),
'range_userids' => $remoteUsers,
'range_department_ids' => $remoteDepartments,
];
}
public static function toggleMember(int $id, int $status, int $adminId, array $adminInfo): array