更新
This commit is contained in:
@@ -192,10 +192,45 @@ class CustomerLogic extends BaseLogic
|
||||
try {
|
||||
$useBatch = (bool) config('project.qywx_sync_use_batch_detail', true);
|
||||
$mergedMap = null;
|
||||
/** 已走「时间筛选 + 流式 batch」分支且未抛错时,不再 list+get 全量回退 */
|
||||
$createtimeFilterBatchPathDone = false;
|
||||
if ($useBatch) {
|
||||
try {
|
||||
$mergedMap = self::buildMergedExternalContactsMap($service, $userList);
|
||||
Log::info('qywx sync: 批量接口合并后客户数 ' . count($mergedMap));
|
||||
// 按跟进时间筛选:不构建全量合并表(10 万+ 会爆内存/慢);流式扫 batch,仅对候选 id get+写库
|
||||
if ($filterByFollowCreatetime) {
|
||||
$candidates = self::collectCandidateExternalUserIdsForCreatetimeWindow(
|
||||
$service,
|
||||
$userList,
|
||||
$ctimeFrom,
|
||||
$ctimeTo,
|
||||
$followCtimeMode
|
||||
);
|
||||
Log::info('qywx sync: 时间筛选模式 候选客户数(待 get 校验) ' . count($candidates));
|
||||
foreach (array_keys($candidates) as $extId) {
|
||||
$detail = $service->getExternalContactDetail($extId);
|
||||
if (empty($detail)) {
|
||||
continue;
|
||||
}
|
||||
$externalContact = $detail['external_contact'] ?? [];
|
||||
$followUsers = $detail['follow_user'] ?? [];
|
||||
self::upsertOneExternalContactBundle(
|
||||
$externalContact,
|
||||
is_array($followUsers) ? $followUsers : [],
|
||||
true,
|
||||
$followCtimeMode,
|
||||
$ctimeFrom,
|
||||
$ctimeTo,
|
||||
$syncCount,
|
||||
$newCount,
|
||||
$updateCount,
|
||||
$skippedCount
|
||||
);
|
||||
}
|
||||
$createtimeFilterBatchPathDone = true;
|
||||
} else {
|
||||
$mergedMap = self::buildMergedExternalContactsMap($service, $userList);
|
||||
Log::info('qywx sync: 批量接口合并后客户数 ' . count($mergedMap));
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('qywx sync: 批量拉取失败,回退 list+get — ' . $e->getMessage());
|
||||
$mergedMap = null;
|
||||
@@ -217,7 +252,7 @@ class CustomerLogic extends BaseLogic
|
||||
$skippedCount
|
||||
);
|
||||
}
|
||||
} else {
|
||||
} elseif (!$createtimeFilterBatchPathDone) {
|
||||
$processedCustomers = [];
|
||||
foreach ($userList as $user) {
|
||||
$userId = $user['userid'] ?? '';
|
||||
@@ -370,6 +405,82 @@ class CustomerLogic extends BaseLogic
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 流式扫 batch/get_by_user,按单条 follow_info 做时间窗口启发式,收集可能要落库的 external_userid(去重)。
|
||||
* 企微无「只拉今日」接口,仍需翻完所有分页;但不构建全量合并表,后续仅对候选调用 get 做精确过滤。
|
||||
*
|
||||
* @return array<string, true>
|
||||
*/
|
||||
private static function collectCandidateExternalUserIdsForCreatetimeWindow(
|
||||
WechatWorkService $service,
|
||||
array $userList,
|
||||
int $ctimeFrom,
|
||||
int $ctimeTo,
|
||||
string $followCtimeMode
|
||||
): array {
|
||||
$candidates = [];
|
||||
foreach ($userList as $user) {
|
||||
$userId = trim((string) ($user['userid'] ?? ''));
|
||||
if ($userId === '') {
|
||||
continue;
|
||||
}
|
||||
Log::info('qywx sync(batch 筛选扫描): 成员 ' . $userId . ' ' . ($user['name'] ?? ''));
|
||||
$cursor = '';
|
||||
do {
|
||||
$resp = $service->batchGetExternalContactByUser($userId, $cursor, 100);
|
||||
$err = (int) ($resp['errcode'] ?? -1);
|
||||
if ($err !== 0) {
|
||||
throw new \RuntimeException('errcode=' . $err . ' errmsg=' . (string) ($resp['errmsg'] ?? ''));
|
||||
}
|
||||
foreach ($resp['external_contact_list'] ?? [] as $item) {
|
||||
if (!is_array($item)) {
|
||||
continue;
|
||||
}
|
||||
$fi = $item['follow_info'] ?? [];
|
||||
if (!is_array($fi)) {
|
||||
$fi = [];
|
||||
}
|
||||
if (!self::batchFollowInfoHintMayMatchCreatetimeWindow($fi, $ctimeFrom, $ctimeTo, $followCtimeMode)) {
|
||||
continue;
|
||||
}
|
||||
$ec = $item['external_contact'] ?? [];
|
||||
$extId = trim((string) ($ec['external_userid'] ?? ''));
|
||||
if ($extId !== '') {
|
||||
$candidates[$extId] = true;
|
||||
}
|
||||
}
|
||||
$cursor = (string) ($resp['next_cursor'] ?? '');
|
||||
} while ($cursor !== '');
|
||||
}
|
||||
|
||||
return $candidates;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量接口单条 follow_info 是否「可能」命中时间策略(用于缩小候选;最终以 get 全量 follow_user 为准)
|
||||
*
|
||||
* @param array<string, mixed> $followInfo
|
||||
*/
|
||||
private static function batchFollowInfoHintMayMatchCreatetimeWindow(
|
||||
array $followInfo,
|
||||
int $ctimeFrom,
|
||||
int $ctimeTo,
|
||||
string $followCtimeMode
|
||||
): bool {
|
||||
if ($followCtimeMode === 'any') {
|
||||
return self::followCreatetimeInRange([$followInfo], $ctimeFrom, $ctimeTo);
|
||||
}
|
||||
$ct = self::normalizeFollowUserCreatetime($followInfo);
|
||||
if ($ct > 0 && $ct < $ctimeFrom) {
|
||||
return false;
|
||||
}
|
||||
if ($ct > 0 && $ct > $ctimeTo) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 batch/get_by_user 按成员分页拉取并合并跟进人(列表接口无按时间筛选)
|
||||
*
|
||||
@@ -379,8 +490,10 @@ class CustomerLogic extends BaseLogic
|
||||
private static function buildMergedExternalContactsMap(WechatWorkService $service, array $userList): array
|
||||
{
|
||||
$merged = [];
|
||||
|
||||
foreach ($userList as $user) {
|
||||
$userId = trim((string) ($user['userid'] ?? ''));
|
||||
|
||||
if ($userId === '') {
|
||||
continue;
|
||||
}
|
||||
@@ -388,6 +501,7 @@ class CustomerLogic extends BaseLogic
|
||||
$cursor = '';
|
||||
do {
|
||||
$resp = $service->batchGetExternalContactByUser($userId, $cursor, 100);
|
||||
|
||||
$err = (int) ($resp['errcode'] ?? -1);
|
||||
if ($err !== 0) {
|
||||
throw new \RuntimeException('errcode=' . $err . ' errmsg=' . (string) ($resp['errmsg'] ?? ''));
|
||||
@@ -468,6 +582,66 @@ class CustomerLogic extends BaseLogic
|
||||
* @param array<string, mixed> $externalContact
|
||||
* @param array<int, array<string, mixed>> $followUsers
|
||||
*/
|
||||
/**
|
||||
* 客户联系事件回调:按 external_userid 拉取详情并 UPSERT(不走全量同步文件锁)。
|
||||
*
|
||||
* @see https://developer.work.weixin.qq.com/document/path/92130
|
||||
*/
|
||||
public static function upsertSingleExternalContactFromApi(string $externalUserId): void
|
||||
{
|
||||
$externalUserId = trim($externalUserId);
|
||||
if ($externalUserId === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$service = new WechatWorkService();
|
||||
$detail = $service->getExternalContactDetail($externalUserId);
|
||||
if (empty($detail['external_contact']) || !is_array($detail['external_contact'])) {
|
||||
Log::warning('qywx external contact callback: 拉取详情为空', ['external_userid' => $externalUserId]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$followUsers = $detail['follow_user'] ?? [];
|
||||
if (!is_array($followUsers)) {
|
||||
$followUsers = [];
|
||||
}
|
||||
|
||||
$syncCount = 0;
|
||||
$newCount = 0;
|
||||
$updateCount = 0;
|
||||
$skippedCount = 0;
|
||||
self::upsertOneExternalContactBundle(
|
||||
$detail['external_contact'],
|
||||
$followUsers,
|
||||
false,
|
||||
'first',
|
||||
0,
|
||||
0,
|
||||
$syncCount,
|
||||
$newCount,
|
||||
$updateCount,
|
||||
$skippedCount
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户联系「删除企业客户」等事件:本地软删除一行。
|
||||
*/
|
||||
public static function softDeleteExternalContactRow(string $externalUserId): void
|
||||
{
|
||||
$externalUserId = trim($externalUserId);
|
||||
if ($externalUserId === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$now = time();
|
||||
Db::name('qywx_external_contact')->where('external_userid', $externalUserId)->update([
|
||||
'delete_time' => $now,
|
||||
'update_time' => $now,
|
||||
]);
|
||||
}
|
||||
|
||||
private static function upsertOneExternalContactBundle(
|
||||
array $externalContact,
|
||||
array $followUsers,
|
||||
|
||||
Reference in New Issue
Block a user