Files
xuetang/server/app/adminapi/lists/qywx/CustomerLists.php
T
2026-09-08 11:40:15 +08:00

340 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace app\adminapi\lists\qywx;
use app\adminapi\lists\BaseAdminDataLists;
use app\adminapi\logic\qywx\CustomerLogic;
use app\common\lists\ListsSearchInterface;
use app\common\model\auth\Admin;
use app\common\model\QywxExternalContact;
use think\facade\Db;
/**
* 企业微信客户列表
*/
class CustomerLists extends BaseAdminDataLists implements ListsSearchInterface
{
/**
* @notes 搜索条件
*/
public function setSearch(): array
{
// 表无 follow_user 列,跟进人在 follow_users(JSON);跟进人筛选在 baseQuery() 中处理
return [
'%like%' => ['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];
}
/**
* 添加时间筛选。
* firstexternal_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();
}
}