This commit is contained in:
Your Name
2026-08-25 10:45:24 +08:00
parent af1db59c07
commit b8ccbaf567
13 changed files with 420 additions and 452 deletions
@@ -7,7 +7,7 @@ namespace app\common\service\qywx;
use RuntimeException;
use think\facade\Db;
/** 把回调调度出的单个目标成员同步到同一条企业微信官方获客链接。 */
/** 把当前全部可用医助同步到同一条企业微信官方获客链接,由企微执行原生多人路由。 */
class QywxPromotionRangeSyncService
{
private QywxCustomerAcquisitionApiService $api;
@@ -27,7 +27,7 @@ class QywxPromotionRangeSyncService
}
$status = (int) ($row['status'] ?? 0);
$now = time();
if ($status === 0 || $status === 4 || ($status === 2 && (int) ($row['lock_until'] ?? 0) > $now)) {
if ($status === 0 || $status === 4 || $status === 5 || ($status === 2 && (int) ($row['lock_until'] ?? 0) > $now)) {
return null;
}
if ($status === 3 && (int) ($row['next_retry'] ?? 0) > $now) {
@@ -52,34 +52,55 @@ class QywxPromotionRangeSyncService
$token = (string) $claim['lock_token'];
$desiredVersion = (int) ($claim['desired_version'] ?? 0);
$memberId = (int) ($claim['desired_member_id'] ?? 0);
try {
$pool = Db::name('qywx_promotion_pool')->where('id', $poolId)->whereNull('delete_time')->find();
$link = Db::name('qywx_promotion_link')
->where('id', (int) ($claim['promotion_link_id'] ?? 0))
->whereNull('delete_time')->find();
$member = Db::name('qywx_promotion_pool_member')
->where('id', $memberId)->where('pool_id', $poolId)->whereNull('delete_time')->find();
if (!$pool || !$link || !$member || (int) ($member['enabled'] ?? 0) !== 1) {
throw new RuntimeException('分流方案、官方链接或目标成员已失效');
$members = Db::name('qywx_promotion_pool_member')
->where('pool_id', $poolId)->whereNull('delete_time')->order('id', 'asc')->select()->toArray();
if (!$pool || !$link || $members === []) {
throw new RuntimeException('分流方案、官方链接或方案成员已失效');
}
$remoteLinkId = trim((string) ($link['remote_link_id'] ?? ''));
$userId = trim((string) ($member['userid'] ?? ''));
if ($remoteLinkId === '' || $userId === '') {
throw new RuntimeException('官方链接 ID 或目标成员 userid 为空');
if ($remoteLinkId === '') {
throw new RuntimeException('官方链接 ID 为空');
}
$range = QywxPromotionMemberRange::evaluate($members, date('Y-m-d'), time());
$desiredUserIds = $range['userids'];
if ($desiredUserIds === []) {
$message = '所有成员均已禁用、未生效或达到今日上限;企业微信官方链接至少需要保留一名成员';
Db::name('qywx_promotion_range_sync')
->where('pool_id', $poolId)->where('lock_token', $token)->update([
'status' => 4,
'desired_member_id' => 0,
'applied_member_id' => 0,
'next_retry' => strtotime('tomorrow'),
'attempts' => 0,
'lock_token' => '',
'lock_until' => 0,
'last_error' => $message,
'update_time' => time(),
]);
Db::name('qywx_promotion_link')->where('id', (int) $link['id'])->update([
'sync_error' => $message,
'update_time' => time(),
]);
return ['status' => 'blocked', 'pool_id' => $poolId, 'member_id' => 0];
}
$this->api->updateLink([
'link_id' => $remoteLinkId,
'link_name' => mb_substr((string) ($pool['name'] ?? '获客分流方案'), 0, 30),
'range' => ['user_list' => [$userId]],
'range' => ['user_list' => $desiredUserIds],
'skip_verify' => (int) ($link['skip_verify'] ?? 0) === 1,
]);
$response = $this->api->getLink($remoteLinkId);
$remote = QywxCustomerAcquisitionLinkService::normaliseRemoteResponse($response, $remoteLinkId);
$actualUserIds = $remote['range_userids'];
if ($actualUserIds !== [$userId]) {
throw new RuntimeException('企业微信返回的成员范围与待同步成员不一致');
if (!QywxPromotionMemberRange::same($actualUserIds, $desiredUserIds)) {
throw new RuntimeException('企业微信返回的多人路由成员范围与方案可用医助不一致');
}
$url = $remote['url'];
$snapshot = json_encode($remote['snapshot'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
@@ -101,7 +122,8 @@ class QywxPromotionRangeSyncService
->where('lock_token', $token)
->update([
'status' => $freshVersion === $desiredVersion ? 0 : 1,
'applied_member_id' => $memberId,
'desired_member_id' => 0,
'applied_member_id' => 0,
'applied_version' => $desiredVersion,
'next_retry' => $freshVersion === $desiredVersion ? 0 : time(),
'attempts' => 0,
@@ -111,7 +133,7 @@ class QywxPromotionRangeSyncService
'update_time' => time(),
]);
return ['status' => 'synced', 'pool_id' => $poolId, 'member_id' => $memberId];
return ['status' => 'synced', 'pool_id' => $poolId, 'member_id' => 0];
} catch (\Throwable $e) {
$attempts = max(1, (int) ($claim['attempts'] ?? 0) + 1);
Db::name('qywx_promotion_range_sync')
@@ -136,16 +158,21 @@ class QywxPromotionRangeSyncService
/** @return array{selected:int,synced:int,failed:int} */
public function syncPending(int $limit = 100): array
{
// 每分钟分页重算全部方案,避免有效期、跨日上限变化漏同步,也避免方案数超过批次后饥饿。
$lastPoolId = 0;
do {
$reconcilePoolIds = Db::name('qywx_promotion_range_sync')
->where('pool_id', '>', $lastPoolId)
->order('pool_id', 'asc')
->limit(500)
->column('pool_id');
foreach ($reconcilePoolIds as $reconcilePoolId) {
$lastPoolId = (int) $reconcilePoolId;
QywxPromotionMemberSchedulerService::reconcilePool($lastPoolId);
}
} while (count($reconcilePoolIds) === 500);
$now = time();
$blockedPoolIds = Db::name('qywx_promotion_range_sync')
->where('status', 4)
->where('next_retry', '>', 0)
->where('next_retry', '<=', $now)
->limit(min(500, max(1, $limit)))
->column('pool_id');
foreach ($blockedPoolIds as $blockedPoolId) {
QywxPromotionMemberSchedulerService::reconcilePool((int) $blockedPoolId);
}
$poolIds = Db::name('qywx_promotion_range_sync')
->where(function ($query) use ($now): void {
$query->whereIn('status', [1, 3])->where('next_retry', '<=', $now)