流量池添加及场景获客统计数据

This commit is contained in:
wong
2025-08-15 15:37:00 +08:00
parent e53f31487e
commit 25ae2458f0
6 changed files with 351 additions and 70 deletions

View File

@@ -67,6 +67,8 @@ Route::group('v1/', function () {
Route::get('getUserJourney', 'app\cunkebao\controller\traffic\GetPotentialListWithInCompanyV1Controller@getUserJourney');
Route::get('getUserTags', 'app\cunkebao\controller\traffic\GetPotentialListWithInCompanyV1Controller@getUserTags');
Route::get('getUserInfo', 'app\cunkebao\controller\traffic\GetPotentialListWithInCompanyV1Controller@getUser');
Route::get('getPackage', 'app\cunkebao\controller\traffic\GetPotentialListWithInCompanyV1Controller@getPackage');
Route::post('addPackage', 'app\cunkebao\controller\traffic\GetPotentialListWithInCompanyV1Controller@addPackage');
@@ -134,6 +136,7 @@ Route::group('v1/', function () {
Route::get('plan-stats', 'app\cunkebao\controller\StatsController@planStats');
Route::get('sevenDay-stats', 'app\cunkebao\controller\StatsController@customerAcquisitionStats7Days');
Route::get('today-stats', 'app\cunkebao\controller\StatsController@todayStats');
Route::get('friendRequestTaskStats', 'app\cunkebao\controller\StatsController@getFriendRequestTaskStats');
});

View File

@@ -51,7 +51,6 @@ class StatsController extends Controller
{
$num = $this->request->param('num', 4);
$planScene = Db::name('plan_scene')
->field('id,name,image')
->where(['status' => 1])
@@ -196,4 +195,157 @@ class StatsController extends Controller
return successJson($data, '获取成功');
}
public function getFriendRequestTaskStats()
{
$companyId = $this->request->userInfo['companyId'];
$taskId = $this->request->param('taskId', '');
if(empty($taskId)){
return errorJson('任务id不能为空');
}
$task = Db::name('customer_acquisition_task')->where(['id' => $taskId, 'companyId' => $companyId,'deleteTime' => 0])->find();
if(empty($task)){
return errorJson('任务不存在或已删除');
}
// 1. 获取startTime和endTime格式是日期
$startTime = $this->request->param('startTime', '');
$endTime = $this->request->param('endTime', '');
// 如果获取不到则默认为7天的跨度
if (empty($startTime)) {
$startTime = date('Y-m-d', time() - 86400 * 6);
}
if (empty($endTime)) {
$endTime = date('Y-m-d', time());
}
// 转换成时间戳格式
$startTimestamp = strtotime($startTime . ' 00:00:00');
$endTimestamp = strtotime($endTime . ' 23:59:59');
// 同时生成日期数组和时间戳二维数组
$dateArray = [];
$timestampArray = [];
$currentTimestamp = $startTimestamp;
while ($currentTimestamp <= $endTimestamp) {
// 生成日期格式数组
$dateArray[] = date('m-d', $currentTimestamp);
// 生成时间戳二维数组
$dayStart = $currentTimestamp;
$dayEnd = strtotime('+1 day', $currentTimestamp) - 1; // 23:59:59
$timestampArray[] = [$dayStart, $dayEnd];
$currentTimestamp = strtotime('+1 day', $currentTimestamp);
}
// 使用分组聚合统计,减少 SQL 次数
$allRows = Db::name('task_customer')
->field("FROM_UNIXTIME(createTime, '%m-%d') AS d, COUNT(*) AS c")
->where(['task_id' => $taskId])
->where('createTime', 'between', [$startTimestamp, $endTimestamp])
->group('d')
->select();
$successRows = Db::name('task_customer')
->field("FROM_UNIXTIME(addTime, '%m-%d') AS d, COUNT(*) AS c")
->where(['task_id' => $taskId])
->where('addTime', 'between', [$startTimestamp, $endTimestamp])
->whereIn('status', [1, 2, 4])
->group('d')
->select();
$passRows = Db::name('task_customer')
->field("FROM_UNIXTIME(passTime, '%m-%d') AS d, COUNT(*) AS c")
->where(['task_id' => $taskId])
->where('passTime', 'between', [$startTimestamp, $endTimestamp])
->group('d')
->select();
$errorRows = Db::name('task_customer')
->field("FROM_UNIXTIME(updateTime, '%m-%d') AS d, COUNT(*) AS c")
->where(['task_id' => $taskId, 'status' => 3])
->where('updateTime', 'between', [$startTimestamp, $endTimestamp])
->group('d')
->select();
// 将分组结果映射到连续日期数组
$mapToSeries = function(array $rows) use ($dateArray) {
$dict = [];
foreach ($rows as $row) {
// 兼容对象/数组两种返回
$d = is_array($row) ? ($row['d'] ?? '') : ($row->d ?? '');
$c = (int)(is_array($row) ? ($row['c'] ?? 0) : ($row->c ?? 0));
if ($d !== '') {
$dict[$d] = $c;
}
}
$series = [];
foreach ($dateArray as $d) {
$series[] = $dict[$d] ?? 0;
}
return $series;
};
$allNumArray = $mapToSeries($allRows);
$successNumArray = $mapToSeries($successRows);
$passNumArray = $mapToSeries($passRows);
$errorNumArray = $mapToSeries($errorRows);
// 计算通过率和成功率
$passRateArray = [];
$successRateArray = [];
for ($i = 0; $i < count($dateArray); $i++) {
// 通过率 = 通过数 / 总数
$passRate = ($allNumArray[$i] > 0) ? round(($passNumArray[$i] / $allNumArray[$i]) * 100, 2) : 0;
$passRateArray[] = $passRate;
// 成功率 = 成功数 / 总数
$successRate = ($allNumArray[$i] > 0) ? round(($successNumArray[$i] / $allNumArray[$i]) * 100, 2) : 0;
$successRateArray[] = $successRate;
}
// 计算总体统计
$totalAll = array_sum($allNumArray);
$totalSuccess = array_sum($successNumArray);
$totalPass = array_sum($passNumArray);
$totalError = array_sum($errorNumArray);
$totalPassRate = ($totalAll > 0) ? round(($totalPass / $totalAll) * 100, 2) : 0;
$totalSuccessRate = ($totalAll > 0) ? round(($totalSuccess / $totalAll) * 100, 2) : 0;
// 返回结果
$result = [
'startTime' => $startTime,
'endTime' => $endTime,
'dateArray' => $dateArray,
'allNumArray' => $allNumArray,
'successNumArray' => $successNumArray,
'passNumArray' => $passNumArray,
'errorNumArray' => $errorNumArray,
'passRateArray' => $passRateArray,
'successRateArray' => $successRateArray,
'totalStats' => [
'totalAll' => $totalAll,
'totalSuccess' => $totalSuccess,
'totalPass' => $totalPass,
'totalError' => $totalError,
'totalPassRate' => $totalPassRate,
'totalSuccessRate' => $totalSuccessRate
]
];
return successJson($result, '获取成功');
}
}

View File

@@ -4,6 +4,7 @@ namespace app\cunkebao\controller;
use app\common\model\Device as DeviceModel;
use app\common\model\DeviceWechatLogin as DeviceWechatLoginModel;
use app\common\model\WechatCustomer as WechatCustomerModel;
use app\cunkebao\model\Workbench;
use app\cunkebao\model\WorkbenchAutoLike;
use app\cunkebao\model\WorkbenchMomentsSync;
@@ -600,13 +601,20 @@ class WorkbenchController extends Controller
->field([
'd.id', 'd.imei', 'd.memo', 'd.alive',
'l.wechatId',
'a.nickname', 'a.alias', 'a.avatar', 'a.alias'
'a.nickname', 'a.alias', 'a.avatar', 'a.alias', '0 totalFriend'
])
->leftJoin('device_wechat_login l', 'd.id = l.deviceId and l.alive =' . DeviceWechatLoginModel::ALIVE_WECHAT_ACTIVE . ' and l.companyId = d.companyId')
->leftJoin('wechat_account a', 'l.wechatId = a.wechatId')
->whereIn('d.id', $workbench->config->deveiceGroups)
->order('d.id desc')
->select();
foreach ($deviceList as &$device) {
$curstomer = WechatCustomerModel::field('friendShip')->where(['wechatId' => $device['wechatId']])->find();
$device['totalFriend'] = $curstomer->friendShip->totalFriend ?? 0;
}
unset($device);
$workbench->config->deveiceGroupsOptions = $deviceList;
} else {
$workbench->config->deveiceGroupsOptions = [];

View File

@@ -4,6 +4,7 @@ namespace app\cunkebao\controller\plan;
use app\common\model\Device as DeviceModel;
use app\common\model\DeviceWechatLogin as DeviceWechatLoginModel;
use app\common\model\WechatCustomer as WechatCustomerModel;
use library\ResponseHelper;
use think\Controller;
use think\Db;
@@ -143,13 +144,18 @@ class GetAddFriendPlanDetailV1Controller extends Controller
->field([
'd.id', 'd.imei', 'd.memo', 'd.alive',
'l.wechatId',
'a.nickname', 'a.alias', '0 totalFriend'
'a.nickname', 'a.alias', '0 totalFriend', '0 totalFriend'
])
->leftJoin('device_wechat_login l', 'd.id = l.deviceId and l.alive =' . DeviceWechatLoginModel::ALIVE_WECHAT_ACTIVE . ' and l.companyId = d.companyId')
->leftJoin('wechat_account a', 'l.wechatId = a.wechatId')
->order('d.id desc')
->whereIn('d.id',$reqConf['deveiceGroups'])
->select();
foreach ($deveiceGroupsOptions as &$device) {
$curstomer = WechatCustomerModel::field('friendShip')->where(['wechatId' => $device['wechatId']])->find();
$device['totalFriend'] = $curstomer->friendShip->totalFriend ?? 0;
}
unset($device);
$reqConf['deveiceGroupsOptions'] = $deveiceGroupsOptions;
}else{
$reqConf['deveiceGroupsOptions'] = [];

View File

@@ -22,35 +22,49 @@ class GetPotentialListWithInCompanyV1Controller extends BaseController
*/
protected function makeWhere(array $params = []): array
{
if (!empty($keyword = $this->request->param('keyword'))) {
$keyword = $this->request->param('keyword','');
$device = $this->request->param('deviceId');
$status = $this->request->param('addStatus','');
$taskId = $this->request->param('taskId','');
$packageId = $this->request->param('packageId','');
$where = [];
if (!empty($keyword)) {
$where[] = ['p.identifier|wa.nickname|wa.phone|wa.wechatId|wa.alias', 'like', '%' . $keyword . '%'];
}
// 状态筛选
if ($status = $this->request->param('addStatus')) {
$where['s.status'] = $status;
} else {
$where['s.status'] = array('<>', TrafficSourceModel::STATUS_PASSED);
if (!empty($status)) {
if ($status == 1){
$where[] = ['s.status','=',4];
}elseif ($status == 2){
$where[] = ['s.status','=',0];
}elseif ($status == -1){
$where[] = ['s.status','=',2];
}elseif ($status == 3){
$where[] = ['s.status','=',2];
}
}
// 来源的筛选
if ($fromd = $this->request->param('packageId')) {
if ($fromd != -1) {
$where['tsp.id'] = $fromd;
if ($packageId) {
if ($packageId != -1) {
$where[] = ['tsp.id','=',$packageId];
} else {
$where[] = ['tsp.id', null];
$where[] = ['tsp.id','=', null];
}
}
if ($device = $this->request->param('deviceId')) {
$where['d.deviceId'] = $device;
if (!empty($device)) {
$where[] = ['d.deviceId','=',$device];
}
if (!empty($taskId)){
$where[] = ['t.sceneId','=',$taskId];
}
$where[] = ['s.companyId','=',$this->getUserInfo('companyId')];
$where['s.companyId'] = $this->getUserInfo('companyId');
return array_merge($where, $params);
return $where;
}
/**
@@ -59,55 +73,45 @@ class GetPotentialListWithInCompanyV1Controller extends BaseController
* @param array $where
* @return \think\Paginator
*/
protected function getPoolListByCompanyId(array $where)
protected function getPoolListByCompanyId(array $where,$isPage = true)
{
$query = TrafficPoolModel::alias('p')
->field(
[
'p.id', 'p.identifier', 'p.mobile', 'p.wechatId', 'p.identifier',
's.fromd', 's.status', 's.createTime', 's.companyId', 's.sourceId', 's.type',
'wa.nickname', 'wa.avatar', 'wa.gender', 'wa.phone',
'wa.nickname', 'wa.avatar', 'wa.gender', 'wa.phone','wa.alias'
]
)
->join('traffic_source s', 'p.identifier=s.identifier', 'left')
->join('traffic_source s', 'p.identifier=s.identifier')
->join('wechat_account wa', 'p.identifier=wa.wechatId', 'left')
->join('traffic_source_package_item tspi', 'p.identifier = tspi.identifier AND s.companyId = tspi.companyId', 'left')
->join('traffic_source_package tsp', 'tspi.packageId=tsp.id', 'left')
->join('device_wechat_login d', 's.sourceId=d.wechatId', 'left')
->order('p.id DESC,s.id DESC')
->group('p.identifier');
->where($where);
foreach ($where as $key => $value) {
if (is_numeric($key) && is_array($value) && isset($value[0]) && $value[0] === 'exp') {
$query->whereExp('', $value[1]);
continue;
}
if (is_array($value)) {
$query->where($key, ...$value);
continue;
}
$query->where($key, $value);
}
$result = $query->order('p.id DESC,s.id DESC')->group('p.identifier');
if ($isPage) {
$result = $query->paginate($this->request->param('limit/d', 10), false, ['page' => $this->request->param('page/d', 1)]);
$list = $result->items();
$total = $result->total();
}else{
$list = $result->select();
$total = '';
}
if ($isPage) {
foreach ($list as &$item) {
//流量池筛选
$package = Db::name('traffic_source_package_item')->alias('tspi')
->join('traffic_source_package p', 'tspi.packageId=p.id AND tspi.companyId=p.companyId')
->where(['tspi.companyId' => $item->companyId, 'tspi.identifier' => $item->identifier])
->where(['tspi.identifier' => $item->identifier])
->whereIn('tspi.companyId', [0, $item->companyId])
->column('p.name');
$package2 = Db::name('traffic_source_package_item')->alias('tspi')
->join('traffic_source_package p', 'tspi.packageId=p.id')
->where(['tspi.companyId' => $item->companyId, 'tspi.identifier' => $item->identifier, 'p.isSys' => 1])
->column('p.name');
$packages = array_merge($package, $package2);
$item['packages'] = $packages;
$item['packages'] = $package;
if ($item->type == 1) {
$tag = Db::name('wechat_friendship')->where(['wechatId' => $item->wechatId])->column('tags');
$tags = [];
@@ -121,6 +125,7 @@ class GetPotentialListWithInCompanyV1Controller extends BaseController
}
}
}
unset($item);
$data = ['list' => $list, 'total' => $total];
return json_encode($data, JSON_UNESCAPED_UNICODE);
@@ -356,6 +361,112 @@ class GetPotentialListWithInCompanyV1Controller extends BaseController
return ResponseHelper::success(['wechat' => $tags, 'siteLabels' => $siteLabels]);
}
public function getPackage()
{
$companyId = $this->getUserInfo('companyId');
$package = Db::name('traffic_source_package')
->whereIn('companyId', [$companyId,0])
->field('id,name')
->select();
return ResponseHelper::success($package);
}
public function addPackage()
{
try {
$addPackageId = $this->request->param('addPackageId','');
$packageName = $this->request->param('packageName','');
$userIds = $this->request->param('userIds',[]);
$companyId = $this->getUserInfo('companyId');
$userId = $this->getUserInfo('id');
if (empty($addPackageId) && empty($packageName)){
return ResponseHelper::error('存储的流量池不能为空');
}
if (!empty($addPackageId)){
$package = Db::name('traffic_source_package')
->where(['id' => $addPackageId,'isDel' => 0])
->whereIn('companyId', [$companyId,0])
->field('id,name')
->find();
if (empty($package)){
return ResponseHelper::error('该流量池不存在');
}
$packageId = $package['id'];
}else{
$package = Db::name('traffic_source_package')
->where(['isDel' => 0,'name' => $packageName])
->whereIn('companyId', [$companyId,0])
->field('id,name')
->find();
if (!empty($package)){
return ResponseHelper::error('该流量池名称已存在');
}
$packageId = Db::name('traffic_source_package')->insertGetId(['userId' => $userId,'companyId' => $companyId,'name' => $packageName,'isDel' => 0]);
}
if (!empty($userIds)){
if (!is_array($userIds)){
return ResponseHelper::error('选择的用户类型错误');
}
$result = Db::name('traffic_pool')->alias('tp')
->join('traffic_source tc','tp.identifier=tc.identifier')
->whereIn('tp.id',$userIds)
->where(['companyId' => $companyId])
->group('tp.identifier')
->column('tc.identifier');
}else{
$result = $this->getPoolListByCompanyId($this->makeWhere(),false);
$result = json_decode($result, true);
$result = array_column($result['list'],'identifier');
}
// 1000条为一组进行批量处理
$batchSize = 1000;
$totalRows = count($result);
for ($i = 0; $i < $totalRows; $i += $batchSize) {
$batchRows = array_slice($result, $i, $batchSize);
if (!empty($batchRows)) {
// 2. 批量查询已存在的手机
$existingPhones = [];
$existing = Db::name('traffic_source_package_item')
->where(['companyId' => $companyId,'packageId' => $packageId])
->whereIn('identifier', $batchRows)
->field('identifier')
->select();
$existingPhones = array_column($existing, 'identifier');
// 3. 过滤出新数据,批量插入
$newData = [];
foreach ($batchRows as $row) {
if (!in_array($row, $existingPhones)) {
$newData[] = [
'packageId' => $packageId,
'companyId' => $companyId,
'identifier' => $row,
'createTime' => time(),
];
}
}
// 4. 批量插入新数据
if (!empty($newData)) {
Db::name('traffic_source_package_item')->insertAll($newData);
}
}
}
return ResponseHelper::success('添加成功');
} catch (\Exception $e) {
return ResponseHelper::error($e->getMessage(), $e->getCode());
}
}
/* public function editUserTags()
{
$userId = $this->request->param('userId', '');

View File

@@ -240,8 +240,9 @@ class Adapter implements WeChatServiceInterface
->where('id', $task['id'])
->update([
'status' => $friendAddTaskCreated ? 1 : 3,
'fail_reason' => $friendAddTaskCreated ? '' : '已经是好友了',
'fail_reason' => '',
'processed_wechat_ids' => $task['processed_wechat_ids'],
'addTime' => time(),
'updateTime' => time()
]);
// ~~不用管,回头再添加再判断即可~~
@@ -298,7 +299,7 @@ class Adapter implements WeChatServiceInterface
Db::name('task_customer')
->where('id', $task['id'])
->update(['status' => 4, 'updateTime' => time()]);
->update(['status' => 4,'passTime' => time(), 'updateTime' => time()]);
$wechatFriendRecord = $this->getWeChatAccoutIdAndFriendIdByWeChatIdAndFriendPhone($passedWeChatId, $task['phone']);
$msgConf = is_string($task_info['msgConf']) ? json_decode($task_info['msgConf'], 1) : $task_info['msgConf'];