id, $companyData ); // 3. 创建来源记录 TrafficPoolSource::createSource( $poolCompany->id, $identifier, $companyId, $sourceType, $sourceData ); Db::commit(); return [ 'poolId' => $pool->id, 'poolCompanyId' => $poolCompany->id ]; } catch (\Exception $e) { Db::rollback(); throw $e; } } /** * 好友通过时同步流量信息 * * @param string $identifier 微信ID * @param int $companyId 公司ID * @param int $wechatFriendId 微信好友表ID * @param array $friendData 好友数据 * @return TrafficPoolCompany|null */ public function syncFriendPass(string $identifier, int $companyId, int $wechatFriendId, array $friendData = []) { // 查找流量池记录 $poolCompany = TrafficPoolCompany::where('identifier', $identifier) ->where('companyId', $companyId) ->where('isDel', 0) ->find(); if (!$poolCompany) { // 如果不存在,先入池 $result = $this->enterPool( $identifier, $companyId, TrafficPoolSource::SOURCE_TYPE_FRIEND_ADD, $friendData, array_merge($friendData, [ 'wechatFriendId' => $wechatFriendId, 'friendStatus' => TrafficPoolCompany::FRIEND_STATUS_PASSED, 'friendPassTime' => time() ]) ); $poolCompany = TrafficPoolCompany::find($result['poolCompanyId']); } else { // 更新现有记录 $poolCompany->save([ 'wechatFriendId' => $wechatFriendId, 'friendStatus' => TrafficPoolCompany::FRIEND_STATUS_PASSED, 'friendPassTime' => time(), 'updateTime' => time() ]); } // 更新总表基础信息 if (!empty($friendData)) { $pool = TrafficPoolV2::find($poolCompany->poolId); if ($pool) { $pool->updateBasicInfo($friendData); } } return $poolCompany; } /** * 同步微信标签 * * @param int $poolCompanyId 公司流量ID * @param array $labels 标签数组 * @return int 同步数量 */ public function syncWechatTags(int $poolCompanyId, array $labels) { $poolCompany = TrafficPoolCompany::find($poolCompanyId); if (!$poolCompany) { return 0; } return TrafficPoolTag::syncWechatTags( $poolCompanyId, $poolCompany->identifier, $poolCompany->companyId, $labels ); } /** * 获取流量池列表(分页) * * @param int $companyId 公司ID * @param int $page 页码 * @param int $pageSize 每页数量 * @param array $filters 筛选条件 * @return array */ public function getPoolList(int $companyId, int $page = 1, int $pageSize = 10, array $filters = []) { $query = TrafficPoolCompany::alias('tpc') ->join('ck_traffic_pool tp', 'tp.id = tpc.poolId', 'LEFT') ->where('tpc.companyId', $companyId) ->where('tpc.isDel', 0); // 应用筛选条件 if (!empty($filters['keyword'])) { $keyword = $filters['keyword']; $query->where(function($q) use ($keyword) { $q->where('tp.nickname', 'like', "%{$keyword}%") ->whereOr('tp.wechatId', 'like', "%{$keyword}%") ->whereOr('tp.wechatAlias', 'like', "%{$keyword}%") ->whereOr('tp.mobile', 'like', "%{$keyword}%") ->whereOr('tpc.realName', 'like', "%{$keyword}%") ->whereOr('tpc.phone', 'like', "%{$keyword}%"); }); } if (isset($filters['friendStatus'])) { $query->where('tpc.friendStatus', $filters['friendStatus']); } if (isset($filters['level'])) { $query->where('tpc.level', $filters['level']); } if (isset($filters['lifecycle'])) { $query->where('tpc.lifecycle', $filters['lifecycle']); } if (isset($filters['allocateStatus'])) { $query->where('tpc.allocateStatus', $filters['allocateStatus']); } if (!empty($filters['ownerWechatId'])) { $query->where('tpc.ownerWechatId', $filters['ownerWechatId']); } // RFM 筛选 if (isset($filters['rfmMMin'])) { $query->where('tpc.rfmM', '>=', $filters['rfmMMin']); } if (isset($filters['rfmMMax'])) { $query->where('tpc.rfmM', '<=', $filters['rfmMMax']); } // 统计总数 $total = $query->count(); // 查询列表 $list = $query->field([ 'tpc.id', 'tpc.poolId', 'tpc.identifier', 'tpc.companyId', 'tpc.friendStatus', 'tpc.level', 'tpc.intentionLevel', 'tpc.lifecycle', 'tpc.lastInteractTime', 'tpc.rfmF', 'tpc.rfmM', 'tpc.totalMsgCount', 'tpc.totalOrderCount', 'tpc.totalOrderAmount', 'tpc.ownerWechatId', 'tpc.allocateStatus', 'tpc.realName', 'tpc.phone', 'tpc.createTime', 'tp.nickname', 'tp.avatar', 'tp.gender', 'tp.wechatId', 'tp.wechatAlias', 'tp.mobile', 'tp.region' ]) ->order('tpc.id DESC') ->page($page, $pageSize) ->select(); // 获取标签 $poolCompanyIds = array_column($list->toArray(), 'id'); $tags = $this->getTagsForPoolCompanies($poolCompanyIds); // 组装数据 $result = []; foreach ($list as $item) { $data = $item->toArray(); // 计算 RFM R 值 $data['rfmR'] = $item->lastInteractTime ? (int)floor((time() - $item->lastInteractTime) / 86400) : 9999; // 计算 RFM 总分 $data['rfmScore'] = $this->calculateRfmScore($data['rfmR'], $data['rfmF'], $data['rfmM']); // 添加标签 $data['tags'] = $tags[$item->id] ?? []; $result[] = $data; } return [ 'list' => $result, 'total' => $total, 'page' => $page, 'pageSize' => $pageSize ]; } /** * 获取流量详情 * * @param int $poolCompanyId 公司流量ID * @param int $companyId 公司ID * @return array|null */ public function getPoolDetail(int $poolCompanyId, int $companyId) { $poolCompany = TrafficPoolCompany::alias('tpc') ->join('ck_traffic_pool tp', 'tp.id = tpc.poolId', 'LEFT') ->where('tpc.id', $poolCompanyId) ->where('tpc.companyId', $companyId) ->where('tpc.isDel', 0) ->field('tpc.*, tp.nickname, tp.avatar, tp.gender, tp.wechatId, tp.wechatAlias, tp.mobile, tp.region, tp.country, tp.province, tp.city, tp.signature') ->find(); if (!$poolCompany) { return null; } $data = $poolCompany->toArray(); // 获取标签 $data['tags'] = TrafficPoolTag::getTagsByPoolCompany($poolCompanyId)->toArray(); // 获取来源历史(带群归属信息,限制50条) $data['sources'] = TrafficPoolSource::getSourcesWithOwners($poolCompanyId, 50); // 获取行为轨迹(最近50条) $data['behaviors'] = TrafficPoolBehavior::getUserJourney($poolCompanyId, 50)->toArray(); // 获取分配历史 $data['allotRecords'] = TrafficPoolAllotRecord::getAllotHistory($poolCompanyId)->toArray(); // 如果消息数为0,从微信消息表中统计实际消息数 if (empty($data['totalMsgCount']) || $data['totalMsgCount'] == 0) { $msgCount = 0; // 优先通过wechatFriendId统计(最准确) if (!empty($poolCompany->wechatFriendId)) { $msgCount = Db::table('s2_wechat_message') ->where('wechatFriendId', $poolCompany->wechatFriendId) ->where('type', 1) // 好友消息(type=1) ->where('isDeleted', 0) ->count(); } // 如果wechatFriendId没有统计到,尝试通过identifier(微信ID)统计 if ($msgCount == 0 && !empty($poolCompany->identifier)) { // 统计发送者或接收者是该微信ID的消息 // 需要关联s2_wechat_friend表,通过wechatId匹配 $msgCount = Db::table('s2_wechat_message') ->alias('wm') ->join(['s2_wechat_friend' => 'wf'], 'wm.wechatFriendId = wf.id', 'LEFT') ->where(function($query) use ($poolCompany) { $query->where('wm.senderWechatId', $poolCompany->identifier) ->whereOr('wf.wechatId', $poolCompany->identifier); }) ->where('wm.type', 1) // 好友消息 ->where('wm.isDeleted', 0) ->where('wf.isDeleted', 0) ->count(); } // 如果从行为表也有记录,取较大值(兼容旧数据) $behaviorMsgCount = TrafficPoolBehavior::where('poolCompanyId', $poolCompanyId) ->whereIn('behaviorType', [ TrafficPoolBehavior::BEHAVIOR_TYPE_SEND_MSG, TrafficPoolBehavior::BEHAVIOR_TYPE_RECEIVE_MSG ]) ->count(); if ($behaviorMsgCount > $msgCount) { $msgCount = $behaviorMsgCount; } if ($msgCount > 0) { // 更新数据库中的消息数 $poolCompany->save([ 'totalMsgCount' => $msgCount, 'updateTime' => time() ]); $data['totalMsgCount'] = $msgCount; } } // 计算 RFM $data['rfmR'] = $poolCompany->lastInteractTime ? (int)floor((time() - $poolCompany->lastInteractTime) / 86400) : 9999; $data['rfmScore'] = $this->calculateRfmScore($data['rfmR'], $data['rfmF'], $data['rfmM']); return $data; } /** * 更新流量信息 * * @param int $poolCompanyId 公司流量ID * @param int $companyId 公司ID * @param array $data 更新数据 * @return bool */ public function updatePool(int $poolCompanyId, int $companyId, array $data) { $poolCompany = TrafficPoolCompany::where('id', $poolCompanyId) ->where('companyId', $companyId) ->where('isDel', 0) ->find(); if (!$poolCompany) { return false; } // 允许更新的字段 $allowFields = [ 'realName', 'phone', 'email', 'birthday', 'address', 'company', 'position', 'remark', 'customFields', 'level', 'intentionLevel', 'lifecycle', 'status' ]; $updateData = array_intersect_key($data, array_flip($allowFields)); $updateData['updateTime'] = time(); return $poolCompany->save($updateData); } /** * 批量获取流量的标签 * * @param array $poolCompanyIds * @return array */ protected function getTagsForPoolCompanies(array $poolCompanyIds) { if (empty($poolCompanyIds)) { return []; } $tags = TrafficPoolTag::whereIn('poolCompanyId', $poolCompanyIds) ->where('isDel', 0) ->select(); $result = []; foreach ($tags as $tag) { if (!isset($result[$tag->poolCompanyId])) { $result[$tag->poolCompanyId] = []; } $result[$tag->poolCompanyId][] = [ 'id' => $tag->id, 'tagDefineId' => $tag->tagDefineId, 'tagName' => $tag->tagName, 'tagType' => $tag->tagType, 'tagValue' => $tag->tagValue ]; } return $result; } /** * 计算 RFM 评分 * * @param int $r 最后互动距今天数 * @param int $f 互动频次 * @param float $m 消费金额 * @return array */ public function calculateRfmScore($r, $f, $m) { // R 评分(天数越少分数越高) if ($r <= 7) { $rScore = 5; } elseif ($r <= 30) { $rScore = 4; } elseif ($r <= 90) { $rScore = 3; } elseif ($r <= 180) { $rScore = 2; } else { $rScore = 1; } // F 评分 if ($f >= 100) { $fScore = 5; } elseif ($f >= 50) { $fScore = 4; } elseif ($f >= 20) { $fScore = 3; } elseif ($f >= 5) { $fScore = 2; } else { $fScore = 1; } // M 评分 if ($m >= 10000) { $mScore = 5; } elseif ($m >= 5000) { $mScore = 4; } elseif ($m >= 1000) { $mScore = 3; } elseif ($m >= 100) { $mScore = 2; } else { $mScore = 1; } return [ 'R' => $rScore, 'F' => $fScore, 'M' => $mScore, 'total' => $rScore + $fScore + $mScore ]; } /** * 获取流量统计数据 * * @param int $companyId 公司ID * @return array */ public function getStatistics(int $companyId) { $today = strtotime('today'); $yesterday = strtotime('yesterday'); $thisWeek = strtotime('monday this week'); $thisMonth = strtotime('first day of this month'); // 总流量数 $totalCount = TrafficPoolCompany::where('companyId', $companyId) ->where('isDel', 0) ->count(); // 好友数 $friendCount = TrafficPoolCompany::where('companyId', $companyId) ->where('friendStatus', TrafficPoolCompany::FRIEND_STATUS_PASSED) ->where('isDel', 0) ->count(); // 今日新增 $todayNewCount = TrafficPoolCompany::where('companyId', $companyId) ->where('createTime', '>=', $today) ->where('isDel', 0) ->count(); // 本周新增 $weekNewCount = TrafficPoolCompany::where('companyId', $companyId) ->where('createTime', '>=', $thisWeek) ->where('isDel', 0) ->count(); // 本月新增 $monthNewCount = TrafficPoolCompany::where('companyId', $companyId) ->where('createTime', '>=', $thisMonth) ->where('isDel', 0) ->count(); // 客户等级分布 $levelDistribution = TrafficPoolCompany::where('companyId', $companyId) ->where('isDel', 0) ->group('level') ->field('level, COUNT(*) as count') ->select() ->toArray(); // 生命周期分布 $lifecycleDistribution = TrafficPoolCompany::where('companyId', $companyId) ->where('isDel', 0) ->group('lifecycle') ->field('lifecycle, COUNT(*) as count') ->select() ->toArray(); // 来源分布 $sourceDistribution = TrafficPoolSource::where('companyId', $companyId) ->where('isFirstSource', 1) ->group('sourceType') ->field('sourceType, COUNT(*) as count') ->select() ->toArray(); return [ 'totalCount' => $totalCount, 'friendCount' => $friendCount, 'todayNewCount' => $todayNewCount, 'weekNewCount' => $weekNewCount, 'monthNewCount' => $monthNewCount, 'levelDistribution' => $levelDistribution, 'lifecycleDistribution' => $lifecycleDistribution, 'sourceDistribution' => $sourceDistribution ]; } /** * 从标签引擎同步用户标签 * * @param int $poolCompanyId 流量池公司ID * @param int $companyId 公司ID * @param int $operatorId 操作人ID * @return array 同步结果 */ public function syncTagsFromEngine(int $poolCompanyId, int $companyId, int $operatorId = null) { // 获取流量池记录 $poolCompany = TrafficPoolCompany::where('id', $poolCompanyId) ->where('companyId', $companyId) ->where('isDel', 0) ->find(); if (!$poolCompany) { throw new \Exception('流量不存在'); } // 获取标识信息用于查询标签引擎 $identifiers = []; // 微信ID if (!empty($poolCompany->identifier)) { $identifiers[] = [ 'type' => 'wechat', 'value' => $poolCompany->identifier ]; } // 手机号 if (!empty($poolCompany->phone)) { $identifiers[] = [ 'type' => 'phone', 'value' => $poolCompany->phone ]; } if (empty($identifiers)) { throw new \Exception('无有效标识可用于查询标签'); } // 调用标签引擎服务 $tagEngineService = new \app\common\service\TagEngineService(); $result = $tagEngineService->queryByIdentifiers($identifiers, [ 'mask_identifier' => false ]); //exit_data($result); if ($result === false) { throw new \Exception('标签引擎查询失败'); } // 检查返回结果 if (isset($result['code']) && $result['code'] !== 0) { throw new \Exception($result['message'] ?? '标签引擎返回错误'); } $data = $result['data'] ?? $result; if (!is_array($data)) { $data = []; } $syncedCount = 0; $skippedCount = 0; // 处理返回的标签数据 foreach ($data as $item) { if (empty($item['found']) || empty($item['tags'])) { continue; } foreach ($item['tags'] as $tagData) { try { // 查找或创建标签定义 $tagDefine = $this->findOrCreateTagDefine( $companyId, $tagData['tag_code'] ?? '', $tagData['tag_name'] ?? '', $tagData['category'] ?? '标签引擎', $tagData['tag_type'] ?? 'string' ); if (!$tagDefine) { $skippedCount++; continue; } // 添加标签到流量池 $tag = TrafficPoolTag::addTag( $poolCompanyId, $poolCompany->identifier, $companyId, $tagDefine->id, TrafficPoolTag::SOURCE_AI, // 来源为AI/外部同步 $operatorId, $tagData['tag_value'] ?? null, null // score ); if ($tag) { $syncedCount++; } else { $skippedCount++; } } catch (\Exception $e) { $skippedCount++; continue; } } } return [ 'syncedCount' => $syncedCount, 'skippedCount' => $skippedCount, 'total' => $syncedCount + $skippedCount ]; } /** * 查找或创建标签定义 * * @param int $companyId 公司ID * @param string $tagCode 标签代码 * @param string $tagName 标签名称 * @param string $categoryName 分类名称 * @param string $valueType 值类型 * @return \app\common\model\TrafficPoolTagDefine|null */ protected function findOrCreateTagDefine( int $companyId, string $tagCode, string $tagName, string $categoryName, string $valueType ) { if (empty($tagName)) { return null; } // 标签类型映射 $typeMap = [ 'numeric' => 'number', 'enum' => 'enum', 'string' => 'string', 'boolean' => 'boolean', 'datetime' => 'datetime', 'json' => 'json', ]; $mappedType = $typeMap[$valueType] ?? 'string'; // 先查找是否已存在该标签定义 $tagDefine = \app\common\model\TrafficPoolTagDefine::where('companyId', $companyId) ->where('tagName', $tagName) ->where('tagType', TrafficPoolTag::TAG_TYPE_AI) // AI标签类型 ->where('isDel', 0) ->find(); if ($tagDefine) { return $tagDefine; } // 查找或创建分类 $category = \app\common\model\TrafficPoolTagCategory::where('companyId', $companyId) ->where('categoryName', $categoryName) ->where('tagType', TrafficPoolTag::TAG_TYPE_AI) ->where('isDel', 0) ->find(); if (!$category) { $category = new \app\common\model\TrafficPoolTagCategory(); $category->save([ 'companyId' => $companyId, 'tagType' => TrafficPoolTag::TAG_TYPE_AI, 'categoryName' => $categoryName, 'description' => '从标签引擎同步的标签分类', 'sortOrder' => 0, 'isDel' => 0, 'createTime' => time(), 'updateTime' => time() ]); } // 创建标签定义 $tagDefine = new \app\common\model\TrafficPoolTagDefine(); $tagDefine->save([ 'companyId' => $companyId, 'categoryId' => $category->id, 'tagType' => TrafficPoolTag::TAG_TYPE_AI, 'tagCode' => $tagCode ?: 'engine_' . md5($tagName), 'tagName' => $tagName, 'valueType' => $mappedType, 'description' => '从标签引擎同步', 'isSystem' => 0, 'isDel' => 0, 'createTime' => time(), 'updateTime' => time() ]); return $tagDefine; } }