diff --git a/Server/application/cunkebao/config/route.php b/Server/application/cunkebao/config/route.php index 2a45e9f6..6e0ebd8f 100644 --- a/Server/application/cunkebao/config/route.php +++ b/Server/application/cunkebao/config/route.php @@ -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'); @@ -128,13 +130,14 @@ Route::group('v1/', function () { }); - //数据统计相关 - Route::group('dashboard',function (){ - Route::get('', 'app\cunkebao\controller\StatsController@baseInfoStats'); - 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::group('dashboard',function (){ + Route::get('', 'app\cunkebao\controller\StatsController@baseInfoStats'); + 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'); + }); })->middleware(['jwt']); diff --git a/Server/application/cunkebao/controller/StatsController.php b/Server/application/cunkebao/controller/StatsController.php index acea1e6a..38fa441c 100644 --- a/Server/application/cunkebao/controller/StatsController.php +++ b/Server/application/cunkebao/controller/StatsController.php @@ -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, '获取成功'); + } + + + } \ No newline at end of file diff --git a/Server/application/cunkebao/controller/WorkbenchController.php b/Server/application/cunkebao/controller/WorkbenchController.php index 69902f70..8f8c6d5f 100644 --- a/Server/application/cunkebao/controller/WorkbenchController.php +++ b/Server/application/cunkebao/controller/WorkbenchController.php @@ -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 = []; diff --git a/Server/application/cunkebao/controller/plan/GetAddFriendPlanDetailV1Controller.php b/Server/application/cunkebao/controller/plan/GetAddFriendPlanDetailV1Controller.php index 04ffac30..7ec88c07 100644 --- a/Server/application/cunkebao/controller/plan/GetAddFriendPlanDetailV1Controller.php +++ b/Server/application/cunkebao/controller/plan/GetAddFriendPlanDetailV1Controller.php @@ -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'] = []; diff --git a/Server/application/cunkebao/controller/traffic/GetPotentialListWithInCompanyV1Controller.php b/Server/application/cunkebao/controller/traffic/GetPotentialListWithInCompanyV1Controller.php index 4085727d..e8f2260f 100644 --- a/Server/application/cunkebao/controller/traffic/GetPotentialListWithInCompanyV1Controller.php +++ b/Server/application/cunkebao/controller/traffic/GetPotentialListWithInCompanyV1Controller.php @@ -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,67 +73,58 @@ 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'); - - 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->paginate($this->request->param('limit/d', 10), false, ['page' => $this->request->param('page/d', 1)]); - $list = $result->items(); - $total = $result->total(); - - 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]) - ->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; + ->where($where); - if ($item->type == 1) { - $tag = Db::name('wechat_friendship')->where(['wechatId' => $item->wechatId])->column('tags'); - $tags = []; - foreach ($tag as $k => $v) { - $v = json_decode($v, true); - if (!empty($v)) { - $tags = array_merge($tags, $v); + $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.identifier' => $item->identifier]) + ->whereIn('tspi.companyId', [0, $item->companyId]) + ->column('p.name'); + $item['packages'] = $package; + if ($item->type == 1) { + $tag = Db::name('wechat_friendship')->where(['wechatId' => $item->wechatId])->column('tags'); + $tags = []; + foreach ($tag as $k => $v) { + $v = json_decode($v, true); + if (!empty($v)) { + $tags = array_merge($tags, $v); + } } + $item['tags'] = $tags; } - $item['tags'] = $tags; - } + } } unset($item); $data = ['list' => $list, 'total' => $total]; @@ -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', ''); diff --git a/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php b/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php index 1998a070..7c347c42 100644 --- a/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php +++ b/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php @@ -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'];