更新
This commit is contained in:
@@ -422,9 +422,17 @@ SQL;
|
||||
* multiplies facts. Enterprise tag channels use the normalized relation
|
||||
* table, while legacy name-only channels keep a deduplicated JSON fallback.
|
||||
*
|
||||
* When the fact has an employee dimension, pass $followUserField so a tag
|
||||
* applied by employee A cannot make employee B's event match the channel.
|
||||
*
|
||||
* @param array<string, mixed>|null $channel
|
||||
*/
|
||||
public static function applyExternalUserChannelFilter(Query $query, string $field, ?array $channel): void
|
||||
public static function applyExternalUserChannelFilter(
|
||||
Query $query,
|
||||
string $field,
|
||||
?array $channel,
|
||||
?string $followUserField = null
|
||||
): void
|
||||
{
|
||||
if ($channel === null) {
|
||||
return;
|
||||
@@ -437,10 +445,14 @@ SQL;
|
||||
$tagPredicate = count($tagIds) === 1
|
||||
? 'channel_tag.tag_id = ?'
|
||||
: 'channel_tag.tag_id IN (' . implode(', ', array_fill(0, count($tagIds), '?')) . ')';
|
||||
$followUserPredicate = $followUserField === null
|
||||
? ''
|
||||
: "AND channel_tag.follow_user_id = {$followUserField} ";
|
||||
// 相关 EXISTS 走 (tag_id, external_userid) 索引,避免先物化整渠客户 ID 再 IN。
|
||||
$query->whereRaw(
|
||||
"EXISTS (SELECT 1 FROM {$tagTable} channel_tag "
|
||||
. "WHERE channel_tag.external_userid = {$field} "
|
||||
. $followUserPredicate
|
||||
. "AND {$tagPredicate} "
|
||||
. "AND EXISTS (SELECT 1 FROM {$contactTable} active_channel_contact "
|
||||
. 'WHERE active_channel_contact.external_userid = channel_tag.external_userid '
|
||||
@@ -472,6 +484,92 @@ SQL;
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter an add-event fact by its append-only channel snapshot. Events that
|
||||
* pre-date the snapshot migration explicitly fall back to the old projection,
|
||||
* but the fallback is constrained to the event's exact employee.
|
||||
*
|
||||
* @param array<string, mixed>|null $channel
|
||||
*/
|
||||
public static function applyExternalUserEventChannelFilter(
|
||||
Query $query,
|
||||
string $eventIdField,
|
||||
string $externalUserField,
|
||||
string $followUserField,
|
||||
?array $channel,
|
||||
bool $historicalContact = false
|
||||
): void
|
||||
{
|
||||
if ($channel === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$tagIds = self::channelTagIds($channel);
|
||||
if ($tagIds === [] || !QywxExternalContactEventTagSnapshotService::installed()) {
|
||||
if ($historicalContact) {
|
||||
self::applyHistoricalExternalUserChannelFilter(
|
||||
$query,
|
||||
$externalUserField,
|
||||
$channel,
|
||||
$followUserField
|
||||
);
|
||||
} else {
|
||||
self::applyExternalUserChannelFilter($query, $externalUserField, $channel, $followUserField);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$snapshotTable = self::tableWithPrefix('qywx_external_contact_event_tag');
|
||||
$snapshotPredicate = count($tagIds) === 1
|
||||
? 'event_channel_tag.tag_id = ?'
|
||||
: 'event_channel_tag.tag_id IN (' . implode(', ', array_fill(0, count($tagIds), '?')) . ')';
|
||||
$snapshotMatch = "EXISTS (SELECT 1 FROM {$snapshotTable} event_channel_tag"
|
||||
. " WHERE event_channel_tag.event_id = {$eventIdField}"
|
||||
. " AND event_channel_tag.follow_user_id = {$followUserField}"
|
||||
. " AND {$snapshotPredicate})";
|
||||
$snapshotMissing = "NOT EXISTS (SELECT 1 FROM {$snapshotTable} captured_event_channel"
|
||||
. " WHERE captured_event_channel.event_id = {$eventIdField}"
|
||||
. " AND captured_event_channel.follow_user_id = {$followUserField}"
|
||||
. " AND captured_event_channel.tag_id = '')";
|
||||
|
||||
$query->where(function ($channelQuery) use (
|
||||
$snapshotMatch,
|
||||
$snapshotMissing,
|
||||
$tagIds,
|
||||
$historicalContact,
|
||||
$externalUserField,
|
||||
$followUserField,
|
||||
$channel
|
||||
): void {
|
||||
$channelQuery->whereRaw($snapshotMatch, $tagIds)
|
||||
->whereOr(function ($legacyQuery) use (
|
||||
$snapshotMissing,
|
||||
$historicalContact,
|
||||
$externalUserField,
|
||||
$followUserField,
|
||||
$channel
|
||||
): void {
|
||||
$legacyQuery->whereRaw($snapshotMissing);
|
||||
if ($historicalContact) {
|
||||
self::applyHistoricalExternalUserChannelFilter(
|
||||
$legacyQuery,
|
||||
$externalUserField,
|
||||
$channel,
|
||||
$followUserField
|
||||
);
|
||||
} else {
|
||||
self::applyExternalUserChannelFilter(
|
||||
$legacyQuery,
|
||||
$externalUserField,
|
||||
$channel,
|
||||
$followUserField
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter an external_userid fact by the channel snapshot retained in
|
||||
* qywx_external_contact.follow_users, including soft-deleted contacts.
|
||||
@@ -481,14 +579,48 @@ SQL;
|
||||
* contact row itself retains follow_users and is the best available channel
|
||||
* snapshot for this specific historical statistic.
|
||||
*
|
||||
* When $followUserField is provided, JSON_SEARCH first locates that exact
|
||||
* employee object and JSON_CONTAINS checks only its tag array.
|
||||
*
|
||||
* @param array<string, mixed>|null $channel
|
||||
*/
|
||||
public static function applyHistoricalExternalUserChannelFilter(Query $query, string $field, ?array $channel): void
|
||||
public static function applyHistoricalExternalUserChannelFilter(
|
||||
Query $query,
|
||||
string $field,
|
||||
?array $channel,
|
||||
?string $followUserField = null
|
||||
): void
|
||||
{
|
||||
if ($channel === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$contactTable = self::tableWithPrefix('qywx_external_contact');
|
||||
$tagIds = self::channelTagIds($channel);
|
||||
if ($followUserField !== null && $tagIds !== []) {
|
||||
$safeFollowUsers = "IF(JSON_VALID(historical_channel_contact.follow_users),"
|
||||
. ' historical_channel_contact.follow_users, JSON_ARRAY())';
|
||||
$userPath = "JSON_UNQUOTE(JSON_SEARCH({$safeFollowUsers}, 'one', {$followUserField},"
|
||||
. " NULL, '$[*].userid'))";
|
||||
$tagsPath = "IFNULL(REPLACE({$userPath}, '.userid', '.tags'), '$.__missing__')";
|
||||
$tagsJson = "JSON_EXTRACT({$safeFollowUsers}, {$tagsPath})";
|
||||
$tagPredicates = [];
|
||||
$bindings = [];
|
||||
foreach ($tagIds as $tagId) {
|
||||
$tagPredicates[] = "JSON_CONTAINS({$tagsJson}, JSON_OBJECT('tag_id', ?))";
|
||||
$bindings[] = $tagId;
|
||||
}
|
||||
$query->whereRaw(
|
||||
"EXISTS (SELECT 1 FROM {$contactTable} historical_channel_contact"
|
||||
. " WHERE historical_channel_contact.external_userid = {$field}"
|
||||
. " AND {$userPath} IS NOT NULL"
|
||||
. ' AND (' . implode(' OR ', $tagPredicates) . '))',
|
||||
$bindings
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$patterns = self::buildLikePatterns($channel);
|
||||
if ($patterns === []) {
|
||||
$query->whereRaw('1 = 0');
|
||||
@@ -502,7 +634,6 @@ SQL;
|
||||
$segments[] = 'historical_channel_contact.follow_users LIKE ?';
|
||||
$bindings[] = $pattern;
|
||||
}
|
||||
$contactTable = self::tableWithPrefix('qywx_external_contact');
|
||||
$query->whereRaw(
|
||||
"EXISTS (SELECT 1 FROM {$contactTable} historical_channel_contact"
|
||||
. " WHERE historical_channel_contact.external_userid = {$field}"
|
||||
|
||||
@@ -139,6 +139,9 @@ class QywxPromotionAutomationStore
|
||||
Db::name('qywx_promotion_automation_action_log')->insert($log + ['task_id' => $row['id']]);
|
||||
}
|
||||
});
|
||||
|
||||
// 标签动作成功/终止后,以任务入队时的配置冻结事件渠道;重复调用由完成标记幂等保护。
|
||||
QywxExternalContactEventTagSnapshotService::captureFromPromotionTask($row);
|
||||
}
|
||||
|
||||
public function localNames(array $task): array
|
||||
@@ -167,7 +170,17 @@ class QywxPromotionAutomationStore
|
||||
public function syncCustomer(array $task): void
|
||||
{
|
||||
$started = time();
|
||||
CustomerLogic::upsertSingleExternalContactFromApi($task['external_userid']);
|
||||
$eventId = (int) Db::name('qywx_external_contact_event')
|
||||
->where('change_type', 'add_external_contact')
|
||||
->where('user_id', (string) $task['userid'])
|
||||
->where('external_userid', (string) $task['external_userid'])
|
||||
->where('event_time', (int) $task['event_time'])
|
||||
->value('id');
|
||||
CustomerLogic::upsertSingleExternalContactFromApi(
|
||||
$task['external_userid'],
|
||||
$eventId,
|
||||
(string) $task['userid']
|
||||
);
|
||||
// 旧方法在API空结果时只log并返回void;必须核验本地实际更新,避免把未同步记为成功。
|
||||
$updated = (int) Db::name('qywx_external_contact')->where('external_userid', $task['external_userid'])->value('update_time');
|
||||
if ($updated < $started) {
|
||||
|
||||
Reference in New Issue
Block a user