This commit is contained in:
Your Name
2026-04-30 09:27:42 +08:00
parent dc899e4af4
commit eb782305e6
460 changed files with 10785 additions and 928 deletions
@@ -734,6 +734,11 @@ class CustomerLogic extends BaseLogic
'delete_time' => $now,
'update_time' => $now,
]);
// 客户已删 → 关系表硬删(无 delete_time 列;统计场景不需要保留)
Db::name('qywx_external_contact_tag')
->where('external_userid', $externalUserId)
->delete();
}
/**
@@ -782,16 +787,23 @@ class CustomerLogic extends BaseLogic
->where('id', (int) $row['id'])
->update([
'follow_users' => json_encode([], JSON_UNESCAPED_UNICODE),
'tags' => '[]',
'delete_time' => $now,
'update_time' => $now,
]);
// 整客户已无人跟进 → 关系表清空
Db::name('qywx_external_contact_tag')
->where('external_userid', $externalUserId)
->delete();
return;
}
$minCreate = self::minFollowCreatetime($kept);
$update = [
'follow_users' => json_encode($kept, JSON_UNESCAPED_UNICODE),
'tags' => self::extractFollowUserTags($kept),
'update_time' => $now,
];
if ($minCreate > 0) {
@@ -802,6 +814,9 @@ class CustomerLogic extends BaseLogic
Db::name('qywx_external_contact')
->where('id', (int) $row['id'])
->update($update);
// 关系表按剩余 kept follow_users 同步(自动清掉离开员工那行 + 保留其他员工的标签)
self::syncContactTagsRelation($externalUserId, $kept);
}
private static function upsertOneExternalContactBundle(
@@ -843,6 +858,7 @@ class CustomerLogic extends BaseLogic
'corp_full_name' => $externalContact['corp_full_name'] ?? '',
'external_profile' => json_encode($externalContact['external_profile'] ?? [], JSON_UNESCAPED_UNICODE),
'follow_users' => json_encode($followUsers, JSON_UNESCAPED_UNICODE),
'tags' => self::extractFollowUserTags($followUsers),
'follow_admin_ids' => self::resolveFollowAdminIds($followUsers),
'external_first_add_time' => self::minFollowCreatetime($followUsers),
'create_time' => $now,
@@ -851,11 +867,168 @@ class CustomerLogic extends BaseLogic
];
self::upsertExternalContactRow($row, $syncCount, $newCount, $updateCount);
// 同步「客户↔员工↔标签」关系表(用于检索/统计;列表筛选/聚合不必再解析 follow_users JSON
self::syncContactTagsRelation((string) $row['external_userid'], $followUsers);
if (($syncCount % 25) === 0) {
usleep(8000);
}
}
/**
* 同步「客户↔员工↔标签」关系表(zyt_qywx_external_contact_tag)。
*
* 行粒度:(external_userid, follow_user_id, tag_id) 三元组——同一标签由不同员工打则各占一行。
* 用于检索/统计/聚合:按标签筛客户、统计某标签客户数、多标签 AND/OR、按分组漏斗等。
*
* 写入策略(保留 create_time,仅在改名/分组变化时刷 tag_name/group_name/type):
* 1. 计算目标三元组集合
* 2. 删除"该 external_userid 下"不在新集合里的旧关系(含跟进人离开 / 标签移除)
* 3. 对每个新三元组 INSERT ... ON DUPLICATE KEY UPDATE 刷新冗余字段
*
* @param array<int, mixed> $followUsers /externalcontact/get 返回的 follow_user[]
* @internal 仅供 UPSERT / 回填命令复用
*/
public static function syncContactTagsRelation(string $externalUserId, array $followUsers): void
{
$externalUserId = trim($externalUserId);
if ($externalUserId === '') {
return;
}
// 1. 拍平为 (follow_user_id, tag_id) → 行 的字典;保持 follow_user_id 维度区分
$rows = [];
foreach ($followUsers as $fu) {
if (!is_array($fu)) {
continue;
}
$followUserId = trim((string) ($fu['userid'] ?? ''));
$tags = $fu['tags'] ?? [];
if (!is_array($tags)) {
continue;
}
foreach ($tags as $t) {
if (!is_array($t)) {
continue;
}
$tagId = trim((string) ($t['tag_id'] ?? ''));
if ($tagId === '') {
continue;
}
$key = $followUserId . '|' . $tagId;
$rows[$key] = [
'follow_user_id' => mb_substr($followUserId, 0, 64),
'tag_id' => mb_substr($tagId, 0, 64),
'tag_name' => mb_substr((string) ($t['tag_name'] ?? ''), 0, 128),
'group_name' => mb_substr((string) ($t['group_name'] ?? ''), 0, 128),
'type' => isset($t['type']) ? (int) $t['type'] : 1,
];
}
}
$now = time();
// 2. 删除该客户下不在目标集合里的旧关系
if ($rows === []) {
Db::name('qywx_external_contact_tag')
->where('external_userid', $externalUserId)
->delete();
return;
}
// 用 (follow_user_id || tag_id) 拼字符串过滤;记录少时 PHP 拉出来比 SQL CONCAT 更高效
$existing = Db::name('qywx_external_contact_tag')
->where('external_userid', $externalUserId)
->field(['id', 'follow_user_id', 'tag_id'])
->select()
->toArray();
$deleteIds = [];
foreach ($existing as $ex) {
$key = (string) ($ex['follow_user_id'] ?? '') . '|' . (string) ($ex['tag_id'] ?? '');
if (!isset($rows[$key])) {
$deleteIds[] = (int) $ex['id'];
}
}
if ($deleteIds !== []) {
Db::name('qywx_external_contact_tag')
->whereIn('id', $deleteIds)
->delete();
}
// 3. UPSERT 新关系(命中唯一键时只刷冗余字段 + update_timecreate_time 保持原值)
foreach ($rows as $r) {
try {
Db::name('qywx_external_contact_tag')
->duplicate(['tag_name', 'group_name', 'type', 'update_time'])
->insert([
'external_userid' => $externalUserId,
'follow_user_id' => $r['follow_user_id'],
'tag_id' => $r['tag_id'],
'tag_name' => $r['tag_name'],
'group_name' => $r['group_name'],
'type' => $r['type'],
'create_time' => $now,
'update_time' => $now,
]);
} catch (\Throwable $e) {
Log::warning('qywx external contact tag upsert failed: ' . $e->getMessage(), [
'ext' => $externalUserId,
'follow_user_id' => $r['follow_user_id'],
'tag_id' => $r['tag_id'],
]);
}
}
}
/**
* 把所有 follow_user[].tags 合并去重(按 tag_id),返回 JSON 字符串。
*
* 一个客户可能同时被多个员工跟进,每个员工各自打的 tag 在各自的 follow_user.tags 里;
* 这里按 tag_id 唯一去重,保留 tag_name / group_name / type 完整结构,便于后台直接渲染/筛选,
* 不必再解析 follow_users 大 JSON。
*
* 来源结构示例:
* "tags":[{"group_name":"自媒体1","tag_name":"自媒体1","type":1,"tag_id":"etRr2..."}]
*
* @internal 仅供 UPSERT / 回填命令复用,不属于业务对外 API
* @param array<int, mixed> $followUsers
*/
public static function extractFollowUserTags(array $followUsers): string
{
$bag = [];
foreach ($followUsers as $fu) {
if (!is_array($fu)) {
continue;
}
$tags = $fu['tags'] ?? [];
if (!is_array($tags)) {
continue;
}
foreach ($tags as $t) {
if (!is_array($t)) {
continue;
}
$tagId = trim((string) ($t['tag_id'] ?? ''));
if ($tagId === '') {
continue;
}
if (isset($bag[$tagId])) {
continue;
}
$bag[$tagId] = [
'group_name' => (string) ($t['group_name'] ?? ''),
'tag_name' => (string) ($t['tag_name'] ?? ''),
'type' => isset($t['type']) ? (int) $t['type'] : 1,
'tag_id' => $tagId,
];
}
}
return json_encode(array_values($bag), JSON_UNESCAPED_UNICODE);
}
/**
* 将企微 follow_user[].userid 与后台 admin.work_wechat_userid 对齐,得到 zyt_admin.id 列表 JSON。
*
@@ -922,6 +1095,7 @@ class CustomerLogic extends BaseLogic
'corp_full_name',
'external_profile',
'follow_users',
'tags',
'follow_admin_ids',
'external_first_add_time',
'update_time',
@@ -948,6 +1122,90 @@ class CustomerLogic extends BaseLogic
}
}
/**
* 标签维度统计:用于「按标签筛客户」下拉选项 + 「按标签看分布」面板
*
* 数据源:zyt_qywx_external_contact_tag(关系表)+ zyt_qywx_external_contact(剔除已软删客户)
* 输出按 group_name 分组、组内按客户数倒序,每条带 tag_id / tag_name / customer_count
*
* @return array{
* total_tags: int,
* total_relations: int,
* total_tagged_customers: int,
* groups: array<int, array{group_name: string, customer_count: int, tags: array<int, array{tag_id: string, tag_name: string, customer_count: int}>}>
* }
*/
public static function getTagStats(): array
{
// 关系表里 external_userid 可能指向已被软删的客户;这里 INNER JOIN 主表过滤未删除的
$rows = Db::name('qywx_external_contact_tag')
->alias('ect')
->join('qywx_external_contact ec', 'ec.external_userid = ect.external_userid', 'INNER')
->whereNull('ec.delete_time')
->field([
'ect.tag_id',
'ect.tag_name',
'ect.group_name',
'COUNT(DISTINCT ect.external_userid) AS customer_count',
])
->group('ect.tag_id, ect.tag_name, ect.group_name')
->order('customer_count', 'desc')
->select()
->toArray();
$groupMap = [];
$customersUnion = [];
foreach ($rows as $r) {
$g = (string) ($r['group_name'] ?? '');
if (!isset($groupMap[$g])) {
$groupMap[$g] = [
'group_name' => $g,
'customer_count' => 0,
'tags' => [],
];
}
$cnt = (int) ($r['customer_count'] ?? 0);
$groupMap[$g]['tags'][] = [
'tag_id' => (string) ($r['tag_id'] ?? ''),
'tag_name' => (string) ($r['tag_name'] ?? ''),
'customer_count' => $cnt,
];
}
// 组内已经按 customer_count desc 排好序了(外面 SQL 排序结果);
// 组本身按"组内最大 customer_count"倒序排(最热门标签所在组优先)
foreach ($groupMap as &$g) {
$g['customer_count'] = $g['tags'][0]['customer_count'] ?? 0;
}
unset($g);
$groups = array_values($groupMap);
usort($groups, static function ($a, $b) {
return $b['customer_count'] <=> $a['customer_count'];
});
// 全表 distinct 客户数(带 tag 的客户)
$taggedCustomers = (int) Db::name('qywx_external_contact_tag')
->alias('ect')
->join('qywx_external_contact ec', 'ec.external_userid = ect.external_userid', 'INNER')
->whereNull('ec.delete_time')
->group('ect.external_userid')
->count();
$totalRelations = (int) Db::name('qywx_external_contact_tag')
->alias('ect')
->join('qywx_external_contact ec', 'ec.external_userid = ect.external_userid', 'INNER')
->whereNull('ec.delete_time')
->count();
return [
'total_tags' => count($rows),
'total_relations' => $totalRelations,
'total_tagged_customers' => $taggedCustomers,
'groups' => $groups,
];
}
/**
* @notes 获取统计信息
*/