['name'], ]; } /** * @return int[] 入参 tag_ids 规范化后的非空字符串数组(实际为 string[]) */ private function normalizeTagIds(): array { $raw = $this->params['tag_ids'] ?? null; if ($raw === null || $raw === '') { return []; } if (is_string($raw)) { $raw = explode(',', $raw); } if (!is_array($raw)) { return []; } $ids = []; foreach ($raw as $v) { $s = trim((string) $v); if ($s !== '') { $ids[] = $s; } } return array_values(array_unique($ids)); } /** * 去重筛选: * - first(默认):按客户首次添加时间筛选(客户去重,系统原口径) * - any:按添加事件流水筛选(含老客被其他员工重加,对齐企微「当天有添加动作」) */ private function dedupeMode(): string { $mode = strtolower(trim((string) ($this->params['dedupe_mode'] ?? 'first'))); return $mode === 'any' ? 'any' : 'first'; } /** * @return string[] 跟进人关键词对应的企微 userid(含已软删 admin) */ private function resolveFollowStaffWxUserids(): array { $kw = trim((string) ($this->params['follow_user'] ?? '')); if ($kw === '') { return []; } $escaped = addcslashes($kw, '%_\\'); $rows = Db::name('admin') ->whereLike('name', '%' . $escaped . '%') ->where('work_wechat_userid', '<>', '') ->whereNotNull('work_wechat_userid') ->column('work_wechat_userid'); $wxUserids = []; foreach ($rows as $uid) { $uid = trim((string) $uid); if ($uid !== '') { $wxUserids[$uid] = true; } } // 关键词本身也可能是企微 userid(通常无中文);中文名不能当 userid 去筛事件流水 if ($kw !== '' && !preg_match('/\p{Han}/u', $kw)) { $wxUserids[$kw] = true; } return array_keys($wxUserids); } /** * 跟进人筛选:列表展示优先用 admin.name(见 lists() 补 admin_name), * 但库内 follow_users JSON 通常只有企微 userid,直接 LIKE 中文名会搜不到。 * 策略:admin.name 模糊匹配 → work_wechat_userid / admin.id,再按 userid / follow_admin_ids 过滤; * 同时保留对 follow_users 原文的 LIKE(兼容直接搜 userid、备注等)。 */ private function applyFollowUserFilter($query) { $kw = trim((string) ($this->params['follow_user'] ?? '')); if ($kw === '') { return; } $escaped = addcslashes($kw, '%_\\'); // 含已软删账号,避免离职后跟进人姓名映射丢失导致搜不到历史客户 $adminRows = Db::name('admin') ->whereLike('name', '%' . $escaped . '%') ->where('work_wechat_userid', '<>', '') ->whereNotNull('work_wechat_userid') ->field(['id', 'work_wechat_userid']) ->select() ->toArray(); $wxUserids = []; $adminIds = []; foreach ($adminRows as $row) { $uid = trim((string) ($row['work_wechat_userid'] ?? '')); if ($uid !== '') { $wxUserids[$uid] = true; } $aid = (int) ($row['id'] ?? 0); if ($aid > 0) { $adminIds[$aid] = true; } } $wxUserids = array_keys($wxUserids); $adminIds = array_keys($adminIds); $query->where(function ($q) use ($escaped, $wxUserids, $adminIds) { $q->whereLike('follow_users', '%' . $escaped . '%'); foreach ($wxUserids as $uid) { $uidEsc = addcslashes($uid, '%_\\'); $q->whereOr('follow_users', 'like', '%"userid":"' . $uidEsc . '"%'); } foreach ($adminIds as $aid) { // JSON 数组精确包含,避免 id=12 误命中 123 $q->whereOrRaw( 'JSON_CONTAINS(IFNULL(follow_admin_ids, \'[]\'), ?)', [json_encode($aid)] ); } }); } /** * @return array{0:int,1:int} [startTs, endTs],未设置则为 0 */ private function parseAddTimeBounds(): array { $addStart = trim((string) ($this->params['add_time_start'] ?? '')); $addEnd = trim((string) ($this->params['add_time_end'] ?? '')); $startTs = 0; $endTs = 0; if ($addStart !== '') { $t = strtotime($addStart . ' 00:00:00'); if ($t !== false) { $startTs = (int) $t; } } if ($addEnd !== '') { $t = strtotime($addEnd . ' 23:59:59'); if ($t !== false) { $endTs = (int) $t; } } return [$startTs, $endTs]; } /** * 添加时间筛选。 * first:external_first_add_time(客户去重) * any:事件流水 add_external_contact(含重加;可叠加跟进人 userid) * * @return bool true=已按事件+跟进人收窄,调用方勿再 applyFollowUserFilter */ private function applyAddTimeFilter($query): bool { [$startTs, $endTs] = $this->parseAddTimeBounds(); if ($startTs <= 0 && $endTs <= 0) { return false; } if ($this->dedupeMode() === 'any') { $eventQuery = Db::name('qywx_external_contact_event') ->where('change_type', 'add_external_contact') ->where('external_userid', '<>', ''); if ($startTs > 0) { $eventQuery->where('event_time', '>=', $startTs); } if ($endTs > 0) { $eventQuery->where('event_time', '<=', $endTs); } $followKw = trim((string) ($this->params['follow_user'] ?? '')); $staffScoped = false; if ($followKw !== '') { $staffIds = $this->resolveFollowStaffWxUserids(); // 去掉「关键词本身」这一项后若仍有 admin 映射,或关键词像 userid,则按事件员工收窄 $staffIds = array_values(array_filter($staffIds, static fn (string $id): bool => $id !== '')); if ($staffIds !== []) { $eventQuery->whereIn('user_id', $staffIds); $staffScoped = true; } } $extIds = $eventQuery->group('external_userid')->column('external_userid'); if ($extIds === []) { $query->whereRaw('1=0'); } else { $query->whereIn('external_userid', $extIds); } return $staffScoped; } $effExpr = 'COALESCE(NULLIF(external_first_add_time, 0), create_time)'; if ($startTs > 0) { $query->whereRaw($effExpr . ' > 0 AND ' . $effExpr . ' >= ?', [$startTs]); } if ($endTs > 0) { $query->whereRaw($effExpr . ' > 0 AND ' . $effExpr . ' <= ?', [$endTs]); } return false; } private function baseQuery() { $query = QywxExternalContact::where($this->searchWhere); // 添加时间可能已按「跟进人+事件流水」收窄;此时不必再 LIKE follow_users $followAlreadyScoped = $this->applyAddTimeFilter($query); if (!$followAlreadyScoped) { $this->applyFollowUserFilter($query); } // 标签筛选:JOIN 关系表按 tag_id 过滤;多个标签为 OR(命中任一即返回)。 // 走 zyt_qywx_external_contact_tag.idx_tag 索引,比 LIKE follow_users 快得多 $tagIds = $this->normalizeTagIds(); if ($tagIds !== []) { $matchedExtIds = Db::name('qywx_external_contact_tag') ->whereIn('tag_id', $tagIds) ->group('external_userid') ->column('external_userid'); if ($matchedExtIds === []) { // 没人命中:直接给一个不可能成立的条件,避免下面命中所有客户 $query->whereRaw('1=0'); } else { $query->whereIn('external_userid', $matchedExtIds); } } return $query; } /** * @notes 获取列表 */ public function lists(): array { // 按首次添加时间倒序:库字段 external_first_add_time;未回填(0)时回退 create_time(与列表「添加时间」展示一致) $lists = $this->baseQuery() ->orderRaw( '(COALESCE(NULLIF(external_first_add_time, 0), create_time) = 0) ASC, ' . 'COALESCE(NULLIF(external_first_add_time, 0), create_time) DESC, id DESC' ) ->limit($this->limitOffset, $this->limitLength) ->select() ->toArray(); $wxUserids = []; foreach ($lists as $item) { $raw = json_decode($item['follow_users'] ?? '[]', true); if (!is_array($raw)) { continue; } foreach ($raw as $fu) { if (!is_array($fu)) { continue; } $wx = trim((string) ($fu['userid'] ?? '')); if ($wx !== '') { $wxUserids[$wx] = true; } } } $wxUserids = array_keys($wxUserids); $adminNameByWx = []; if ($wxUserids !== []) { $adminNameByWx = Admin::whereIn('work_wechat_userid', $wxUserids)->column('name', 'work_wechat_userid'); } 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]; } } unset($fu); $item['follow_users'] = $followUsers; $followAdminIds = json_decode($item['follow_admin_ids'] ?? '[]', true); $item['follow_admin_ids'] = is_array($followAdminIds) ? $followAdminIds : []; // 解析标签 JSON 数组(值由 CustomerLogic::extractFollowUserTags 写入;按 tag_id 去重) $tags = json_decode((string) ($item['tags'] ?? '[]'), true); $item['tags'] = is_array($tags) ? $tags : []; $fromDb = (int) ($item['external_first_add_time'] ?? 0); $fromJson = CustomerLogic::minFollowCreatetime($followUsers); $item['external_first_add_time'] = $fromDb > 0 ? $fromDb : $fromJson; } unset($item); return $lists; } /** * @notes 获取数量 */ public function count(): int { return $this->baseQuery()->count(); } }