This commit is contained in:
Your Name
2026-09-03 16:03:41 +08:00
parent b4c11881b4
commit 928f72ec3d
25 changed files with 1926 additions and 394 deletions
+210 -18
View File
@@ -230,20 +230,49 @@ class CustomerLists extends BaseAdminDataLists implements ListsSearchInterface
$query->whereRaw($effExpr . ' > 0 AND ' . $effExpr . ' <= ?', [$endTs]);
}
return false;
}
private function baseQuery()
return false;
}
/**
* 按客户当前跟进关系中的添加方式筛选。
*
* follow_users 由同步逻辑使用 json_encode 写入,匹配数字值及历史字符串值;
* 数字后必须紧跟逗号或对象结束符,避免 add_way=1 误命中 16。
*/
private function applyAddWayFilter($query): void
{
if (!array_key_exists('add_way', $this->params) || $this->params['add_way'] === '') {
return;
}
$addWay = self::normalizeAddWay($this->params['add_way']);
if ($addWay === null) {
$query->whereRaw('1=0');
return;
}
$numberPrefix = '%"add_way":' . $addWay;
$stringPrefix = '%"add_way":"' . $addWay;
$query->where(function ($q) use ($numberPrefix, $stringPrefix) {
$q->where('follow_users', 'like', $numberPrefix . ',%')
->whereOr('follow_users', 'like', $numberPrefix . '}%')
->whereOr('follow_users', 'like', $stringPrefix . '",%')
->whereOr('follow_users', 'like', $stringPrefix . '"}%');
});
}
private function baseQuery()
{
$query = QywxExternalContact::where($this->searchWhere);
// 添加时间可能已按「跟进人+事件流水」收窄;此时不必再 LIKE follow_users
$followAlreadyScoped = $this->applyAddTimeFilter($query);
if (!$followAlreadyScoped) {
$this->applyFollowUserFilter($query);
}
// 标签筛选:JOIN 关系表按 tag_id 过滤;多个标签为 OR(命中任一即返回)。
if (!$followAlreadyScoped) {
$this->applyFollowUserFilter($query);
}
$this->applyAddWayFilter($query);
// 标签筛选:JOIN 关系表按 tag_id 过滤;多个标签为 OR(命中任一即返回)。
// 走 zyt_qywx_external_contact_tag.idx_tag 索引,比 LIKE follow_users 快得多
$tagIds = $this->normalizeTagIds();
if ($tagIds !== []) {
@@ -262,6 +291,154 @@ class CustomerLists extends BaseAdminDataLists implements ListsSearchInterface
return $query;
}
/**
* 批量读取当前页客户的加客渠道流水。
*
* 事件表是一对多关系,不能直接 JOIN 到分页主查询,否则会放大列表行数与 count。
* 同一客户可能被不同员工重复添加,因此保留所有不同的非空 state,并按最近事件排序。
*
* @param string[] $externalUserids
* @return array<string, array<int, array<string, mixed>>>
*/
private function loadAddChannelsByExternalUserid(array $externalUserids): array
{
$ids = [];
foreach ($externalUserids as $externalUserid) {
$externalUserid = trim((string) $externalUserid);
if ($externalUserid !== '') {
$ids[$externalUserid] = true;
}
}
$ids = array_keys($ids);
if ($ids === []) {
return [];
}
$events = Db::name('qywx_external_contact_event')
->where('change_type', 'add_external_contact')
->where('state', '<>', '')
->whereIn('external_userid', $ids)
->field(['id', 'external_userid', 'user_id', 'state', 'event_time'])
->order('event_time', 'desc')
->order('id', 'desc')
->select()
->toArray();
$poolIds = [];
foreach ($events as $event) {
$state = trim((string) ($event['state'] ?? ''));
if (preg_match('/^zyt_pool:([1-9]\d*)$/D', $state, $matches) === 1) {
$poolIds[(int) $matches[1]] = true;
}
}
$poolNamesById = [];
if ($poolIds !== []) {
// 历史渠道仍应显示已删除方案原来的名称,因此这里不限制 delete_time。
$poolNamesById = Db::name('qywx_promotion_pool')
->whereIn('id', array_keys($poolIds))
->column('name', 'id');
}
return self::projectAddChannelEvents($events, $poolNamesById);
}
/**
* @param array<int, array<string, mixed>> $events 已按 event_time DESC, id DESC 排序
* @param array<int|string, string> $poolNamesById
* @return array<string, array<int, array<string, mixed>>>
*/
private static function projectAddChannelEvents(array $events, array $poolNamesById): array
{
$channelsByExternalUserid = [];
$seen = [];
foreach ($events as $event) {
$externalUserid = trim((string) ($event['external_userid'] ?? ''));
$state = trim((string) ($event['state'] ?? ''));
if ($externalUserid === '' || $state === '' || isset($seen[$externalUserid][$state])) {
continue;
}
$seen[$externalUserid][$state] = true;
$poolId = 0;
if (preg_match('/^zyt_pool:([1-9]\d*)$/D', $state, $matches) === 1) {
$poolId = (int) $matches[1];
}
$poolName = $poolId > 0 ? trim((string) ($poolNamesById[$poolId] ?? '')) : '';
$channelsByExternalUserid[$externalUserid][] = [
'state' => $state,
'label' => $poolName !== ''
? $poolName
: ($poolId > 0 ? '获客助手方案 #' . $poolId : $state),
'source_type' => $poolId > 0 ? 'promotion_pool' : 'state',
'pool_id' => $poolId,
'user_id' => trim((string) ($event['user_id'] ?? '')),
'event_time' => (int) ($event['event_time'] ?? 0),
];
}
return $channelsByExternalUserid;
}
/**
* 企业微信客户详情 follow_user.add_way 的可读文案。
*
* add_way 是固定添加方式,state 是企业自定义渠道参数,两者不能混用。
* 未识别的新枚举保留原值,避免后续企微扩展时页面退化成“未记录”。
*/
private static function addWayLabel(int $addWay): string
{
$labels = [
0 => '未知添加方式',
1 => '通过扫描二维码添加',
2 => '通过搜索手机号添加',
3 => '通过名片分享添加',
4 => '通过群聊添加',
5 => '通过手机通讯录添加',
6 => '通过微信联系人添加',
8 => '安装第三方应用时自动添加',
9 => '通过搜索邮箱添加',
10 => '通过视频号添加',
11 => '通过日程参与人添加',
12 => '通过会议参与人添加',
13 => '通过微信好友添加',
14 => '通过智慧硬件专属客服添加',
15 => '通过上门服务客服添加',
16 => '通过获客链接添加',
17 => '通过定制开发添加',
18 => '通过需求回复添加',
21 => '通过第三方售前客服添加',
22 => '通过可能的商务伙伴添加',
24 => '通过接受微信好友申请添加',
201 => '通过内部成员共享添加',
202 => '通过管理员或负责人分配添加',
];
return $labels[$addWay] ?? '其他添加方式(' . $addWay . '';
}
/**
* @param mixed $value
*/
private static function normalizeAddWay($value): ?int
{
if (is_int($value)) {
return $value >= 0 ? $value : null;
}
if (!is_string($value)) {
return null;
}
$value = trim($value);
if ($value === '' || preg_match('/^\d+$/D', $value) !== 1) {
return null;
}
return (int) $value;
}
/**
* @notes 获取列表
*/
@@ -278,7 +455,12 @@ class CustomerLists extends BaseAdminDataLists implements ListsSearchInterface
->toArray();
$wxUserids = [];
$externalUserids = [];
foreach ($lists as $item) {
$externalUserid = trim((string) ($item['external_userid'] ?? ''));
if ($externalUserid !== '') {
$externalUserids[$externalUserid] = true;
}
$raw = json_decode($item['follow_users'] ?? '[]', true);
if (!is_array($raw)) {
continue;
@@ -298,19 +480,25 @@ class CustomerLists extends BaseAdminDataLists implements ListsSearchInterface
if ($wxUserids !== []) {
$adminNameByWx = Admin::whereIn('work_wechat_userid', $wxUserids)->column('name', 'work_wechat_userid');
}
$addChannelsByExternalUserid = $this->loadAddChannelsByExternalUserid(array_keys($externalUserids));
foreach ($lists as &$item) {
$followUsers = json_decode($item['follow_users'] ?? '[]', true);
$followUsers = is_array($followUsers) ? $followUsers : [];
foreach ($followUsers as &$fu) {
if (!is_array($fu)) {
continue;
}
$wx = trim((string) ($fu['userid'] ?? ''));
if ($wx !== '' && isset($adminNameByWx[$wx]) && $adminNameByWx[$wx] !== '') {
$fu['admin_name'] = $adminNameByWx[$wx];
}
}
foreach ($followUsers as &$fu) {
if (!is_array($fu)) {
continue;
}
$wx = trim((string) ($fu['userid'] ?? ''));
if ($wx !== '' && isset($adminNameByWx[$wx]) && $adminNameByWx[$wx] !== '') {
$fu['admin_name'] = $adminNameByWx[$wx];
}
$addWay = self::normalizeAddWay($fu['add_way'] ?? $fu['AddWay'] ?? null);
if ($addWay !== null) {
$fu['add_way'] = $addWay;
$fu['add_way_label'] = self::addWayLabel($addWay);
}
}
unset($fu);
$item['follow_users'] = $followUsers;
$followAdminIds = json_decode($item['follow_admin_ids'] ?? '[]', true);
@@ -320,6 +508,10 @@ class CustomerLists extends BaseAdminDataLists implements ListsSearchInterface
$tags = json_decode((string) ($item['tags'] ?? '[]'), true);
$item['tags'] = is_array($tags) ? $tags : [];
$externalUserid = trim((string) ($item['external_userid'] ?? ''));
$item['add_channels'] = $addChannelsByExternalUserid[$externalUserid] ?? [];
$item['add_channel_states'] = array_column($item['add_channels'], 'state');
$fromDb = (int) ($item['external_first_add_time'] ?? 0);
$fromJson = CustomerLogic::minFollowCreatetime($followUsers);
$item['external_first_add_time'] = $fromDb > 0 ? $fromDb : $fromJson;