## GitHub 同步说明 | 项 | 内容 | |:---|:---| | 仓库 | https://github.com/fnvtk/cunkebao_v3 | | 分支 | develop | | 方向 | 本地 → GitHub(单向 push) | | 是否拉取远程 | 否(未 fetch / pull / merge) | | 是否改本地源文件 | 否(仅 git add/commit,未编辑业务/文档正文) | | 远程基准 | origin/develop @b5b40e8b| | 本批工作区变更 | 999 files, +47622 / -4007 lines | ## 一并推送的本地已有 commit(此前未 push) 1.3aa7ecf8deploy: 官网 ckb.quwanzhi.com 宝塔上线 DNS+SSL+验收文档 2.16a70045fix(官网): 桌面导航仅保留一个留资 CTA 并重新上线 3.dbe7b7aafix(官网): Hero 改为主 CTA 按钮并去掉重复 1200+ 数据条 4.ac82726echore(部署): 五端上线验收46项并统一留资与缓存版本 ## 本 commit 目录统计 | 目录 | 文件数 | 说明 | |:---|---:|:---| | 开发文档/ | 639 | 10、项目管理、未完成项跨仓汇总、五端需求/部署/接口 | | Server/ | 129 | 后端 API、迁移、部署相关 | | Touchkebao/ | 80 | PC 工作台、获客、算力 | | 官网/ | 61 | ckb.quwanzhi.com 静态站与 CTA 迭代 | | Cunkebao/ | 49 | 移动端场景/设置/流量池 | | SuperAdmin/ | 26 | 超管后台 | | .cursor/ | 11 | 规则与 Skill | | 其他 | 14 | docker-compose、.obsidian、.codegraph 等 | ## 重点文件(本地为准) - 开发文档/1、需求/修改/未完成项与跨仓验收汇总.md - 开发文档/10、项目管理/账号密码与登录环境一览.md - 开发文档/10、项目管理/(新建项目管理目录与截图) - 官网/ 全站 + 宝塔上线文档 - 五端上线验收 46 项(见 ac82726e) ## 刻意未上传 - node_modules/、.DS_Store、.smart-env/ - .开发文档_nested_git_backup/ - **/__pycache__/ ## 验收 - 提交页(含本说明): https://github.com/fnvtk/cunkebao_v3/commit/HEAD - 分支树: https://github.com/fnvtk/cunkebao_v3/tree/develop Co-authored-by: Cursor <cursoragent@cursor.com>
237 lines
8.1 KiB
PHP
237 lines
8.1 KiB
PHP
<?php
|
||
|
||
namespace app\superadmin\controller\traffic;
|
||
|
||
use app\common\service\TrafficPoolSystemIdentifierService;
|
||
use app\superadmin\controller\BaseController;
|
||
use library\ResponseHelper;
|
||
use think\Db;
|
||
|
||
/**
|
||
* 超管客户池列表(V2 表,避免旧表 RIGHT JOIN 全表扫描)
|
||
*/
|
||
class GetPoolListController extends BaseController
|
||
{
|
||
protected function formatGender($gender): string
|
||
{
|
||
switch ((int) $gender) {
|
||
case 1:
|
||
return '男';
|
||
case 2:
|
||
return '女';
|
||
default:
|
||
return '保密';
|
||
}
|
||
}
|
||
|
||
protected function formatTimestamp($value): string
|
||
{
|
||
if ($value === null || $value === '') {
|
||
return '';
|
||
}
|
||
if (is_numeric($value)) {
|
||
return date('Y-m-d H:i:s', (int) $value);
|
||
}
|
||
return (string) $value;
|
||
}
|
||
|
||
protected function applyFilters($query)
|
||
{
|
||
$companyId = (int) $this->request->param('companyId/d', 0);
|
||
$ownerWechatId = trim((string) $this->request->param('ownerWechatId/s', ''));
|
||
$gender = trim((string) $this->request->param('gender/s', ''));
|
||
|
||
if ($companyId > 0) {
|
||
$query->where('tpc.companyId', $companyId);
|
||
}
|
||
|
||
if ($ownerWechatId !== '') {
|
||
$query->where('tpc.ownerWechatId', $ownerWechatId);
|
||
}
|
||
|
||
if ($gender === '男') {
|
||
$query->where('tp.gender', 1);
|
||
} elseif ($gender === '女') {
|
||
$query->where('tp.gender', 2);
|
||
}
|
||
|
||
return $query;
|
||
}
|
||
|
||
protected function buildQuery()
|
||
{
|
||
$keyword = trim((string) $this->request->param('keyword/s', ''));
|
||
|
||
$query = Db::name('traffic_pool_company')->alias('tpc')
|
||
->join('traffic_pool tp', 'tp.id = tpc.poolId')
|
||
->leftJoin('company c', 'c.companyId = tpc.companyId AND c.deleteTime = 0')
|
||
->where('tpc.isDel', 0);
|
||
|
||
TrafficPoolSystemIdentifierService::applyExcludeToPoolQuery($query);
|
||
|
||
$this->applyFilters($query);
|
||
|
||
if ($keyword !== '') {
|
||
$like = '%' . $keyword . '%';
|
||
$query->where(function ($q) use ($like, $keyword) {
|
||
$q->whereLike('tp.nickname', $like)
|
||
->whereOr('tp.wechatId', 'like', $like)
|
||
->whereOr('tp.wechatAlias', 'like', $like)
|
||
->whereOr('tp.mobile', 'like', $like)
|
||
->whereOr('tpc.identifier', 'like', $like)
|
||
->whereOr('tpc.realName', 'like', $like)
|
||
->whereOr('tpc.phone', 'like', $like);
|
||
if (ctype_digit($keyword)) {
|
||
$q->whereOr('tpc.id', (int) $keyword);
|
||
}
|
||
});
|
||
}
|
||
|
||
return $query;
|
||
}
|
||
|
||
protected function loadTagsMap(array $poolCompanyIds): array
|
||
{
|
||
if (empty($poolCompanyIds)) {
|
||
return [];
|
||
}
|
||
|
||
$rows = Db::name('traffic_pool_tag')
|
||
->whereIn('poolCompanyId', $poolCompanyIds)
|
||
->where('isDel', 0)
|
||
->field('poolCompanyId, tagName')
|
||
->select();
|
||
|
||
$map = [];
|
||
foreach ($rows as $row) {
|
||
$pid = (int) $row['poolCompanyId'];
|
||
$map[$pid][] = $row['tagName'];
|
||
}
|
||
|
||
return $map;
|
||
}
|
||
|
||
protected function loadSourceMap(array $poolCompanyIds): array
|
||
{
|
||
if (empty($poolCompanyIds)) {
|
||
return [];
|
||
}
|
||
|
||
$rows = Db::name('traffic_pool_source')
|
||
->whereIn('poolCompanyId', $poolCompanyIds)
|
||
->field('poolCompanyId, sourceName')
|
||
->order('id', 'desc')
|
||
->select();
|
||
|
||
$map = [];
|
||
foreach ($rows as $row) {
|
||
$pid = (int) $row['poolCompanyId'];
|
||
if (!isset($map[$pid])) {
|
||
$map[$pid] = $row['sourceName'] ?: '';
|
||
}
|
||
}
|
||
|
||
return $map;
|
||
}
|
||
|
||
protected function loadOwnerWechatMap(array $ownerWechatIds): array
|
||
{
|
||
// §6.2:优先 ck_wechat_account,回落 s2_wechat_account(减少匿名「未绑定」堆叠)
|
||
$identity = (new \app\common\service\WechatIdentityService())->identityMap($ownerWechatIds);
|
||
$map = [];
|
||
foreach ($identity as $wid => $info) {
|
||
$map[$wid] = ['wechatId' => $wid, 'nickname' => $info['nickname'] ?? '', 'alias' => $info['alias'] ?? ''];
|
||
}
|
||
return $map;
|
||
}
|
||
|
||
public function index()
|
||
{
|
||
$page = max(1, (int) $this->request->param('page/d', 1));
|
||
$limit = min(100, max(1, (int) $this->request->param('limit/d', 20)));
|
||
$keyword = trim((string) $this->request->param('keyword/s', ''));
|
||
$gender = trim((string) $this->request->param('gender/s', ''));
|
||
$companyId = (int) $this->request->param('companyId/d', 0);
|
||
$allowGlobal = (int) $this->request->param('allowGlobal/d', 0) === 1;
|
||
|
||
if ($companyId <= 0 && !$allowGlobal) {
|
||
return ResponseHelper::error('请先选择项目后再加载客户池列表', 400);
|
||
}
|
||
|
||
$query = $this->buildQuery();
|
||
|
||
// 无关键词且无性别筛选时,count 仅扫 traffic_pool_company(避免 JOIN 全表)
|
||
if ($keyword === '' && $gender !== '男' && $gender !== '女') {
|
||
$countQuery = Db::name('traffic_pool_company')->alias('tpc')
|
||
->where('tpc.isDel', 0);
|
||
TrafficPoolSystemIdentifierService::applyExcludeToCompanyQuery($countQuery);
|
||
$this->applyFilters($countQuery);
|
||
$total = $countQuery->count('tpc.id');
|
||
} else {
|
||
$total = (clone $query)->count('tpc.id');
|
||
}
|
||
|
||
$rows = $query
|
||
->field([
|
||
'tpc.id',
|
||
'tpc.companyId',
|
||
'tpc.identifier',
|
||
'tpc.ownerWechatId',
|
||
'tpc.createTime',
|
||
'tp.nickname',
|
||
'tp.wechatId',
|
||
'tp.avatar',
|
||
'tp.gender',
|
||
'tp.region',
|
||
'c.name as companyName',
|
||
])
|
||
->order('tpc.id', 'desc')
|
||
->page($page, $limit)
|
||
->select();
|
||
|
||
$poolCompanyIds = array_column($rows, 'id');
|
||
$ownerWechatMap = $this->loadOwnerWechatMap(array_column($rows, 'ownerWechatId'));
|
||
$lite = (int) $this->request->param('lite/d', 1) === 1;
|
||
$tagMap = $lite ? [] : $this->loadTagsMap($poolCompanyIds);
|
||
$sourceMap = $lite ? [] : $this->loadSourceMap($poolCompanyIds);
|
||
|
||
$list = [];
|
||
foreach ($rows as $row) {
|
||
$id = (int) $row['id'];
|
||
$wa = $ownerWechatMap[$row['ownerWechatId'] ?? ''] ?? [];
|
||
$ownerNickname = $wa['nickname'] ?? '';
|
||
$ownerAlias = $wa['alias'] ?? '';
|
||
$ownerDisplay = $ownerNickname ?: ($ownerAlias ?: ($row['ownerWechatId'] ?: '未绑定归属微信(待修复)'));
|
||
if ($ownerNickname && $ownerAlias) {
|
||
$ownerDisplay = $ownerNickname . ' · ' . $ownerAlias;
|
||
}
|
||
|
||
$list[] = [
|
||
'id' => $id,
|
||
'companyId' => (int) ($row['companyId'] ?? 0),
|
||
'nickname' => $row['nickname'] ?: ($row['identifier'] ?: '未知'),
|
||
'wechatId' => $row['wechatId'] ?: $row['identifier'],
|
||
'avatar' => $row['avatar'] ?: '',
|
||
'gender' => $this->formatGender($row['gender'] ?? 0),
|
||
'region' => $row['region'] ?: '',
|
||
'source' => $sourceMap[$id] ?? '',
|
||
'tags' => $tagMap[$id] ?? [],
|
||
'companyName' => $row['companyName'] ?: '',
|
||
'ownerWechatId' => $row['ownerWechatId'] ?: '',
|
||
'ownerWechatNickname' => $ownerNickname,
|
||
'ownerWechatAlias' => $ownerAlias,
|
||
'ownerWechatDisplay' => $ownerDisplay,
|
||
'createTime' => $this->formatTimestamp($row['createTime']),
|
||
'mobile' => 0,
|
||
];
|
||
}
|
||
|
||
return ResponseHelper::success([
|
||
'list' => $list,
|
||
'total' => $total,
|
||
'page' => $page,
|
||
'limit' => $limit,
|
||
]);
|
||
}
|
||
}
|