831 lines
27 KiB
PHP
831 lines
27 KiB
PHP
<?php
|
||
|
||
namespace app\cunkebao\service;
|
||
|
||
use app\common\model\TrafficPoolGroup;
|
||
use app\common\model\TrafficPoolGroupMember;
|
||
use app\common\model\TrafficPoolCompany;
|
||
use app\common\model\TrafficPoolTag;
|
||
use think\Db;
|
||
|
||
/**
|
||
* 流量池分组服务类
|
||
* 处理流量池分组的查询、创建、成员管理等业务逻辑
|
||
*/
|
||
class TrafficPoolGroupService
|
||
{
|
||
/**
|
||
* 获取分组列表
|
||
*
|
||
* @param int $companyId 公司ID
|
||
* @param bool $withCount 是否包含成员数量
|
||
* @return array
|
||
*/
|
||
public function getGroupList(int $companyId, bool $withCount = true)
|
||
{
|
||
$groups = TrafficPoolGroup::getGroupsByCompany($companyId)->toArray();
|
||
|
||
if ($withCount) {
|
||
foreach ($groups as &$group) {
|
||
if ($group['ruleType'] == TrafficPoolGroup::RULE_TYPE_DYNAMIC) {
|
||
// 动态规则分组,实时计算成员数量
|
||
$group['memberCount'] = $this->countGroupMembers($group['id'], $companyId);
|
||
}
|
||
// 手动分组使用缓存的 memberCount
|
||
|
||
// 计算分组的 RFM 平均值
|
||
$rfmStats = $this->getGroupRfmStats($group['id'], $companyId);
|
||
$group['avgRfmR'] = $rfmStats['avgR'];
|
||
$group['avgRfmF'] = $rfmStats['avgF'];
|
||
$group['avgRfmM'] = $rfmStats['avgM'];
|
||
$group['avgRfmScore'] = $rfmStats['avgScore'];
|
||
}
|
||
}
|
||
|
||
return $groups;
|
||
}
|
||
|
||
/**
|
||
* 获取分组详情
|
||
*
|
||
* @param int $groupId 分组ID
|
||
* @param int $companyId 公司ID
|
||
* @return array|null
|
||
*/
|
||
public function getGroupDetail(int $groupId, int $companyId)
|
||
{
|
||
$group = TrafficPoolGroup::where('id', $groupId)
|
||
->whereIn('companyId', [0, $companyId])
|
||
->where('isDel', 0)
|
||
->find();
|
||
|
||
if (!$group) {
|
||
return null;
|
||
}
|
||
|
||
$data = $group->toArray();
|
||
|
||
// 计算成员数量
|
||
$data['memberCount'] = $this->countGroupMembers($groupId, $companyId);
|
||
|
||
// 计算 RFM 统计
|
||
$rfmStats = $this->getGroupRfmStats($groupId, $companyId);
|
||
$data['rfmStats'] = $rfmStats;
|
||
|
||
return $data;
|
||
}
|
||
|
||
/**
|
||
* 创建分组
|
||
*
|
||
* @param int $companyId 公司ID
|
||
* @param array $data 分组数据
|
||
* @param int $userId 创建用户ID
|
||
* @return TrafficPoolGroup
|
||
*/
|
||
public function createGroup(int $companyId, array $data, int $userId = null)
|
||
{
|
||
// 生成分组编码
|
||
$groupCode = $data['groupCode'] ?? 'custom_' . uniqid();
|
||
|
||
// 检查编码是否已存在
|
||
$existGroup = TrafficPoolGroup::where('companyId', $companyId)
|
||
->where('groupCode', $groupCode)
|
||
->where('isDel', 0)
|
||
->find();
|
||
|
||
if ($existGroup) {
|
||
throw new \Exception('分组编码已存在');
|
||
}
|
||
|
||
$group = TrafficPoolGroup::create([
|
||
'companyId' => $companyId,
|
||
'groupCode' => $groupCode,
|
||
'groupName' => $data['groupName'],
|
||
'groupIcon' => $data['groupIcon'] ?? null,
|
||
'groupColor' => $data['groupColor'] ?? null,
|
||
'description' => $data['description'] ?? null,
|
||
'isSystem' => 0,
|
||
'isDefault' => $data['isDefault'] ?? 0,
|
||
'ruleType' => $data['ruleType'] ?? TrafficPoolGroup::RULE_TYPE_DYNAMIC,
|
||
'ruleConfig' => $data['ruleConfig'] ?? null,
|
||
'sort' => $data['sort'] ?? 100,
|
||
'status' => TrafficPoolGroup::STATUS_ENABLED,
|
||
'userId' => $userId,
|
||
'createTime' => time()
|
||
]);
|
||
|
||
return $group;
|
||
}
|
||
|
||
/**
|
||
* 更新分组
|
||
*
|
||
* @param int $groupId 分组ID
|
||
* @param int $companyId 公司ID
|
||
* @param array $data 更新数据
|
||
* @return bool
|
||
*/
|
||
public function updateGroup(int $groupId, int $companyId, array $data)
|
||
{
|
||
$group = TrafficPoolGroup::where('id', $groupId)
|
||
->where('companyId', $companyId)
|
||
->where('isDel', 0)
|
||
->find();
|
||
|
||
if (!$group) {
|
||
throw new \Exception('分组不存在');
|
||
}
|
||
|
||
if ($group->isSystem) {
|
||
throw new \Exception('系统分组不允许修改');
|
||
}
|
||
|
||
$allowFields = [
|
||
'groupName', 'groupIcon', 'groupColor', 'description',
|
||
'isDefault', 'ruleType', 'ruleConfig', 'sort', 'status'
|
||
];
|
||
|
||
$updateData = array_intersect_key($data, array_flip($allowFields));
|
||
$updateData['updateTime'] = time();
|
||
|
||
return $group->save($updateData);
|
||
}
|
||
|
||
/**
|
||
* 删除分组
|
||
*
|
||
* @param int $groupId 分组ID
|
||
* @param int $companyId 公司ID
|
||
* @return bool
|
||
*/
|
||
public function deleteGroup(int $groupId, int $companyId)
|
||
{
|
||
$group = TrafficPoolGroup::where('id', $groupId)
|
||
->where('companyId', $companyId)
|
||
->where('isDel', 0)
|
||
->find();
|
||
|
||
if (!$group) {
|
||
throw new \Exception('分组不存在');
|
||
}
|
||
|
||
if ($group->isSystem) {
|
||
throw new \Exception('系统分组不允许删除');
|
||
}
|
||
|
||
Db::startTrans();
|
||
try {
|
||
// 删除分组
|
||
$group->save([
|
||
'isDel' => 1,
|
||
'deleteTime' => time()
|
||
]);
|
||
|
||
// 删除分组成员
|
||
TrafficPoolGroupMember::where('groupId', $groupId)
|
||
->where('isDel', 0)
|
||
->update([
|
||
'isDel' => 1,
|
||
'deleteTime' => time()
|
||
]);
|
||
|
||
Db::commit();
|
||
return true;
|
||
} catch (\Exception $e) {
|
||
Db::rollback();
|
||
throw $e;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取分组成员列表
|
||
*
|
||
* @param int $groupId 分组ID
|
||
* @param int $companyId 公司ID
|
||
* @param int $page 页码
|
||
* @param int $pageSize 每页数量
|
||
* @param array $filters 筛选条件
|
||
* @return array
|
||
*/
|
||
public function getGroupMembers(int $groupId, int $companyId, int $page = 1, int $pageSize = 10, array $filters = [])
|
||
{
|
||
$group = TrafficPoolGroup::where('id', $groupId)
|
||
->whereIn('companyId', [0, $companyId])
|
||
->where('isDel', 0)
|
||
->find();
|
||
|
||
if (!$group) {
|
||
throw new \Exception('分组不存在');
|
||
}
|
||
|
||
if ($group->ruleType == TrafficPoolGroup::RULE_TYPE_MANUAL) {
|
||
// 手动分组,从成员表查询
|
||
return $this->getManualGroupMembers($groupId, $companyId, $page, $pageSize, $filters);
|
||
} else {
|
||
// 动态规则分组,根据规则查询
|
||
return $this->getDynamicGroupMembers($group, $companyId, $page, $pageSize, $filters);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取手动分组成员
|
||
*/
|
||
protected function getManualGroupMembers(int $groupId, int $companyId, int $page, int $pageSize, array $filters)
|
||
{
|
||
$query = TrafficPoolGroupMember::alias('tpgm')
|
||
->join('ck_traffic_pool_company tpc', 'tpc.id = tpgm.poolCompanyId', 'LEFT')
|
||
->join('ck_traffic_pool tp', 'tp.id = tpc.poolId', 'LEFT')
|
||
->where('tpgm.groupId', $groupId)
|
||
->where('tpgm.companyId', $companyId)
|
||
->where('tpgm.isDel', 0)
|
||
->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('tpc.realName', 'like', "%{$keyword}%")
|
||
->whereOr('tpc.phone', 'like', "%{$keyword}%");
|
||
});
|
||
}
|
||
|
||
$total = $query->count();
|
||
|
||
$list = $query->field([
|
||
'tpc.id',
|
||
'tpc.poolId',
|
||
'tpc.identifier',
|
||
'tpc.companyId',
|
||
'tpc.friendStatus',
|
||
'tpc.level',
|
||
'tpc.intentionLevel',
|
||
'tpc.lastInteractTime',
|
||
'tpc.rfmF',
|
||
'tpc.rfmM',
|
||
'tpc.totalMsgCount',
|
||
'tpc.totalOrderAmount',
|
||
'tpc.lastMsgTime',
|
||
'tpc.firstSourceType',
|
||
'tpc.firstSourceTime',
|
||
'tpc.lifecycle',
|
||
'tpc.createTime',
|
||
'tpc.realName',
|
||
'tpc.phone',
|
||
'tp.nickname',
|
||
'tp.avatar',
|
||
'tp.wechatId',
|
||
'tp.wechatAlias',
|
||
'tp.gender',
|
||
'tp.region',
|
||
'tp.country',
|
||
'tp.province',
|
||
'tp.city',
|
||
'tpgm.createTime as addTime'
|
||
])
|
||
->order('tpgm.createTime DESC')
|
||
->page($page, $pageSize)
|
||
->select();
|
||
|
||
return $this->formatMemberList($list, $total, $page, $pageSize);
|
||
}
|
||
|
||
/**
|
||
* 获取动态规则分组成员
|
||
*/
|
||
protected function getDynamicGroupMembers(TrafficPoolGroup $group, int $companyId, int $page, int $pageSize, array $filters)
|
||
{
|
||
$query = TrafficPoolCompany::alias('tpc')
|
||
->join('ck_traffic_pool tp', 'tp.id = tpc.poolId', 'LEFT')
|
||
->where('tpc.companyId', $companyId)
|
||
->where('tpc.isDel', 0);
|
||
|
||
// 解析并应用规则
|
||
$ruleConfig = $group->ruleConfig;
|
||
if (!empty($ruleConfig)) {
|
||
$this->applyRuleConditions($query, $ruleConfig, $companyId);
|
||
}
|
||
|
||
// 应用额外筛选
|
||
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('tpc.realName', 'like', "%{$keyword}%")
|
||
->whereOr('tpc.phone', 'like', "%{$keyword}%");
|
||
});
|
||
}
|
||
|
||
$total = $query->count();
|
||
|
||
$list = $query->field([
|
||
'tpc.id',
|
||
'tpc.poolId',
|
||
'tpc.identifier',
|
||
'tpc.companyId',
|
||
'tpc.friendStatus',
|
||
'tpc.level',
|
||
'tpc.intentionLevel',
|
||
'tpc.lastInteractTime',
|
||
'tpc.rfmF',
|
||
'tpc.rfmM',
|
||
'tpc.totalMsgCount',
|
||
'tpc.totalOrderAmount',
|
||
'tpc.lastMsgTime',
|
||
'tpc.firstSourceType',
|
||
'tpc.firstSourceTime',
|
||
'tpc.lifecycle',
|
||
'tpc.createTime',
|
||
'tpc.realName',
|
||
'tpc.phone',
|
||
'tpc.createTime as addTime',
|
||
'tp.nickname',
|
||
'tp.avatar',
|
||
'tp.wechatId',
|
||
'tp.wechatAlias',
|
||
'tp.gender',
|
||
'tp.region',
|
||
'tp.country',
|
||
'tp.province',
|
||
'tp.city'
|
||
])
|
||
->order('tpc.id DESC')
|
||
->page($page, $pageSize)
|
||
->select();
|
||
|
||
return $this->formatMemberList($list, $total, $page, $pageSize);
|
||
}
|
||
|
||
/**
|
||
* 应用规则条件到查询
|
||
*/
|
||
protected function applyRuleConditions($query, array $ruleConfig, int $companyId)
|
||
{
|
||
if (empty($ruleConfig['conditions'])) {
|
||
return;
|
||
}
|
||
|
||
$logic = strtoupper($ruleConfig['logic'] ?? 'AND');
|
||
$conditions = $ruleConfig['conditions'];
|
||
|
||
if ($logic === 'AND') {
|
||
foreach ($conditions as $condition) {
|
||
$this->applyCondition($query, $condition, $companyId, 'AND');
|
||
}
|
||
} else {
|
||
$query->where(function($q) use ($conditions, $companyId) {
|
||
foreach ($conditions as $condition) {
|
||
$this->applyCondition($q, $condition, $companyId, 'OR');
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 应用单个条件
|
||
*/
|
||
protected function applyCondition($query, array $condition, int $companyId, string $logic = 'AND')
|
||
{
|
||
$method = $logic === 'OR' ? 'whereOr' : 'where';
|
||
|
||
if ($condition['type'] === 'group') {
|
||
// 嵌套分组
|
||
$subLogic = strtoupper($condition['logic'] ?? 'AND');
|
||
$subConditions = $condition['conditions'] ?? [];
|
||
|
||
$query->$method(function($q) use ($subConditions, $companyId, $subLogic) {
|
||
foreach ($subConditions as $subCond) {
|
||
$subMethod = $subLogic === 'OR' ? 'whereOr' : 'where';
|
||
$this->applyCondition($q, $subCond, $companyId, $subLogic);
|
||
}
|
||
});
|
||
} elseif ($condition['type'] === 'field') {
|
||
// 字段条件 - 根据字段所属表使用正确的别名
|
||
$fieldName = $condition['field'];
|
||
$operator = $condition['operator'];
|
||
$value = $condition['value'];
|
||
|
||
// 特殊处理:keyword 字段用于多字段搜索
|
||
if ($fieldName === 'keyword') {
|
||
$keyword = $value;
|
||
$query->$method(function($q) use ($keyword) {
|
||
$q->where('tp.nickname', 'like', "%{$keyword}%")
|
||
->whereOr('tp.wechatId', 'like', "%{$keyword}%")
|
||
->whereOr('tp.wechatAlias', 'like', "%{$keyword}%")
|
||
->whereOr('tpc.realName', 'like', "%{$keyword}%")
|
||
->whereOr('tpc.phone', 'like', "%{$keyword}%");
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 特殊处理:friendIds 字段用于指定好友ID列表
|
||
if ($fieldName === 'friendIds') {
|
||
if (is_array($value) && !empty($value)) {
|
||
$query->$method('tpc.id', 'in', $value);
|
||
}
|
||
return;
|
||
}
|
||
|
||
// ck_traffic_pool 表的字段(基础用户信息)
|
||
$tpFields = ['nickname', 'avatar', 'wechatId', 'wechatAlias', 'gender', 'region', 'country', 'province', 'city', 'signature'];
|
||
|
||
// 判断字段属于哪个表
|
||
if (in_array($fieldName, $tpFields)) {
|
||
$field = 'tp.' . $fieldName;
|
||
} else {
|
||
// ck_traffic_pool_company 表的字段(公司维度信息)
|
||
$field = 'tpc.' . $fieldName;
|
||
}
|
||
|
||
// 特殊处理:地区字段(province)
|
||
// 前端可能传递 "广东" 或 "广东 广州市"
|
||
if ($fieldName === 'province' && strpos($value, ' ') !== false) {
|
||
// 包含空格,说明是 "省份 城市" 格式
|
||
$parts = explode(' ', $value, 2);
|
||
$provinceName = trim($parts[0]);
|
||
$cityName = trim($parts[1]);
|
||
|
||
$query->$method(function($q) use ($provinceName, $cityName) {
|
||
$q->where('tp.province', '=', $provinceName)
|
||
->where('tp.city', 'like', "%{$cityName}%");
|
||
});
|
||
return;
|
||
}
|
||
|
||
switch ($operator) {
|
||
case '=':
|
||
case '!=':
|
||
case '>':
|
||
case '<':
|
||
case '>=':
|
||
case '<=':
|
||
$query->$method($field, $operator, $value);
|
||
break;
|
||
case 'in':
|
||
$query->$method($field, 'in', $value);
|
||
break;
|
||
case 'not_in':
|
||
$query->$method($field, 'not in', $value);
|
||
break;
|
||
case 'between':
|
||
$query->$method($field, 'between', $value);
|
||
break;
|
||
case 'like':
|
||
$query->$method($field, 'like', "%{$value}%");
|
||
break;
|
||
}
|
||
} elseif ($condition['type'] === 'tag') {
|
||
// 标签条件
|
||
$tagNames = $condition['value'];
|
||
$operator = $condition['operator'];
|
||
|
||
if ($operator === 'contains') {
|
||
$query->$method(function($q) use ($tagNames, $companyId) {
|
||
$q->whereExists(function($subQuery) use ($tagNames, $companyId) {
|
||
$subQuery->table('ck_traffic_pool_tag')
|
||
->where('ck_traffic_pool_tag.poolCompanyId = tpc.id')
|
||
->where('ck_traffic_pool_tag.companyId', $companyId)
|
||
->where('ck_traffic_pool_tag.tagName', 'in', $tagNames)
|
||
->where('ck_traffic_pool_tag.isDel', 0);
|
||
});
|
||
});
|
||
} elseif ($operator === 'not_contains') {
|
||
$query->$method(function($q) use ($tagNames, $companyId) {
|
||
$q->whereNotExists(function($subQuery) use ($tagNames, $companyId) {
|
||
$subQuery->table('ck_traffic_pool_tag')
|
||
->where('ck_traffic_pool_tag.poolCompanyId = tpc.id')
|
||
->where('ck_traffic_pool_tag.companyId', $companyId)
|
||
->where('ck_traffic_pool_tag.tagName', 'in', $tagNames)
|
||
->where('ck_traffic_pool_tag.isDel', 0);
|
||
});
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 格式化成员列表
|
||
*/
|
||
protected function formatMemberList($list, int $total, int $page, int $pageSize)
|
||
{
|
||
$result = [];
|
||
$poolCompanyIds = [];
|
||
|
||
// 收集所有的poolCompanyId
|
||
foreach ($list as $item) {
|
||
$poolCompanyIds[] = $item['id'];
|
||
}
|
||
|
||
// 批量查询标签
|
||
$tagsMap = [];
|
||
if (!empty($poolCompanyIds)) {
|
||
$tags = \think\Db::table('ck_traffic_pool_tag')
|
||
->alias('tpt')
|
||
->join('ck_traffic_pool_tag_define tptd', 'tpt.tagDefineId = tptd.id', 'LEFT')
|
||
->where('tpt.poolCompanyId', 'in', $poolCompanyIds)
|
||
->where('tpt.isDel', 0)
|
||
->where('tptd.isDel', 0)
|
||
->field('tpt.poolCompanyId, tptd.tagName, tptd.tagType')
|
||
->select();
|
||
|
||
foreach ($tags as $tag) {
|
||
$poolCompanyId = $tag['poolCompanyId'];
|
||
if (!isset($tagsMap[$poolCompanyId])) {
|
||
$tagsMap[$poolCompanyId] = [];
|
||
}
|
||
$tagsMap[$poolCompanyId][] = [
|
||
'tagName' => $tag['tagName'],
|
||
'tagType' => $tag['tagType']
|
||
];
|
||
}
|
||
}
|
||
|
||
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'] ?? 0, $data['rfmM'] ?? 0);
|
||
// 添加标签
|
||
$data['tags'] = $tagsMap[$item['id']] ?? [];
|
||
$result[] = $data;
|
||
}
|
||
|
||
return [
|
||
'list' => $result,
|
||
'total' => $total,
|
||
'page' => $page,
|
||
'pageSize' => $pageSize
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 计算分组成员数量
|
||
*/
|
||
public function countGroupMembers(int $groupId, int $companyId)
|
||
{
|
||
$group = TrafficPoolGroup::where('id', $groupId)
|
||
->whereIn('companyId', [0, $companyId])
|
||
->where('isDel', 0)
|
||
->find();
|
||
|
||
if (!$group) {
|
||
return 0;
|
||
}
|
||
|
||
if ($group->ruleType == TrafficPoolGroup::RULE_TYPE_MANUAL) {
|
||
return TrafficPoolGroupMember::where('groupId', $groupId)
|
||
->where('companyId', $companyId)
|
||
->where('isDel', 0)
|
||
->count();
|
||
}
|
||
|
||
// 动态规则分组
|
||
$query = TrafficPoolCompany::alias('tpc')
|
||
->join('ck_traffic_pool tp', 'tp.id = tpc.poolId', 'LEFT')
|
||
->where('tpc.companyId', $companyId)
|
||
->where('tpc.isDel', 0);
|
||
|
||
$ruleConfig = $group->ruleConfig;
|
||
if (!empty($ruleConfig)) {
|
||
$this->applyRuleConditions($query, $ruleConfig, $companyId);
|
||
}
|
||
|
||
return $query->count();
|
||
}
|
||
|
||
/**
|
||
* 获取分组 RFM 统计
|
||
*/
|
||
protected function getGroupRfmStats(int $groupId, int $companyId)
|
||
{
|
||
$group = TrafficPoolGroup::where('id', $groupId)
|
||
->whereIn('companyId', [0, $companyId])
|
||
->where('isDel', 0)
|
||
->find();
|
||
|
||
if (!$group) {
|
||
return ['avgR' => 0, 'avgF' => 0, 'avgM' => 0, 'avgScore' => 0];
|
||
}
|
||
|
||
if ($group->ruleType == TrafficPoolGroup::RULE_TYPE_MANUAL) {
|
||
$query = TrafficPoolCompany::alias('tpc')
|
||
->join('ck_traffic_pool_group_member tpgm', 'tpgm.poolCompanyId = tpc.id', 'INNER')
|
||
->where('tpgm.groupId', $groupId)
|
||
->where('tpgm.isDel', 0)
|
||
->where('tpc.isDel', 0);
|
||
} else {
|
||
$query = TrafficPoolCompany::alias('tpc')
|
||
->join('ck_traffic_pool tp', 'tp.id = tpc.poolId', 'LEFT')
|
||
->where('tpc.companyId', $companyId)
|
||
->where('tpc.isDel', 0);
|
||
|
||
$ruleConfig = $group->ruleConfig;
|
||
if (!empty($ruleConfig)) {
|
||
$this->applyRuleConditions($query, $ruleConfig, $companyId);
|
||
}
|
||
}
|
||
|
||
$stats = $query->field([
|
||
'AVG(DATEDIFF(NOW(), FROM_UNIXTIME(IFNULL(tpc.lastInteractTime, tpc.createTime)))) as avgR',
|
||
'AVG(tpc.rfmF) as avgF',
|
||
'AVG(tpc.rfmM) as avgM'
|
||
])->find();
|
||
|
||
$avgR = round($stats['avgR'] ?? 0, 1);
|
||
$avgF = round($stats['avgF'] ?? 0, 1);
|
||
$avgM = round($stats['avgM'] ?? 0, 2);
|
||
|
||
$rfmScore = $this->calculateRfmScore($avgR, $avgF, $avgM);
|
||
|
||
return [
|
||
'avgR' => $avgR,
|
||
'avgF' => $avgF,
|
||
'avgM' => $avgM,
|
||
'avgScore' => $rfmScore['total']
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 添加成员到分组
|
||
*
|
||
* @param int $groupId 分组ID
|
||
* @param array $poolCompanyIds 成员ID数组
|
||
* @param int $companyId 公司ID
|
||
* @param int $operatorId 操作人ID
|
||
* @return int 成功添加数量
|
||
*/
|
||
public function addMembers(int $groupId, array $poolCompanyIds, int $companyId, int $operatorId = null)
|
||
{
|
||
$group = TrafficPoolGroup::where('id', $groupId)
|
||
->where('companyId', $companyId)
|
||
->where('isDel', 0)
|
||
->find();
|
||
|
||
if (!$group) {
|
||
throw new \Exception('分组不存在');
|
||
}
|
||
|
||
if ($group->ruleType != TrafficPoolGroup::RULE_TYPE_MANUAL) {
|
||
throw new \Exception('动态规则分组不支持手动添加成员');
|
||
}
|
||
|
||
return TrafficPoolGroupMember::batchAddMembers($groupId, $poolCompanyIds, $companyId, $operatorId);
|
||
}
|
||
|
||
/**
|
||
* 从分组移除成员
|
||
*
|
||
* @param int $groupId 分组ID
|
||
* @param array $poolCompanyIds 成员ID数组
|
||
* @param int $companyId 公司ID
|
||
* @return int 成功移除数量
|
||
*/
|
||
public function removeMembers(int $groupId, array $poolCompanyIds, int $companyId)
|
||
{
|
||
$group = TrafficPoolGroup::where('id', $groupId)
|
||
->where('companyId', $companyId)
|
||
->where('isDel', 0)
|
||
->find();
|
||
|
||
if (!$group) {
|
||
throw new \Exception('分组不存在');
|
||
}
|
||
|
||
if ($group->ruleType != TrafficPoolGroup::RULE_TYPE_MANUAL) {
|
||
throw new \Exception('动态规则分组不支持手动移除成员');
|
||
}
|
||
|
||
return TrafficPoolGroupMember::batchRemoveMembers($groupId, $poolCompanyIds);
|
||
}
|
||
|
||
/**
|
||
* 计算 RFM 评分
|
||
*/
|
||
protected 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
|
||
* @param array $ruleConfig 规则配置
|
||
* @param int $page 页码
|
||
* @param int $pageSize 每页数量
|
||
* @param string $keyword 搜索关键词
|
||
* @return array
|
||
*/
|
||
public function previewGroupMembers(int $companyId, array $ruleConfig, int $page = 1, int $pageSize = 20, string $keyword = '')
|
||
{
|
||
$query = TrafficPoolCompany::alias('tpc')
|
||
->join('ck_traffic_pool tp', 'tp.id = tpc.poolId', 'LEFT')
|
||
->where('tpc.companyId', $companyId)
|
||
->where('tpc.isDel', 0);
|
||
|
||
// 应用规则条件
|
||
if (!empty($ruleConfig)) {
|
||
$this->applyRuleConditions($query, $ruleConfig, $companyId);
|
||
}
|
||
|
||
// 应用关键字搜索
|
||
if (!empty($keyword)) {
|
||
$query->where(function($q) use ($keyword) {
|
||
$q->where('tp.nickname', 'like', "%{$keyword}%")
|
||
->whereOr('tp.wechatId', 'like', "%{$keyword}%")
|
||
->whereOr('tp.wechatAlias', 'like', "%{$keyword}%")
|
||
->whereOr('tpc.realName', 'like', "%{$keyword}%")
|
||
->whereOr('tpc.phone', 'like', "%{$keyword}%");
|
||
});
|
||
}
|
||
|
||
$total = $query->count();
|
||
|
||
$list = $query->field([
|
||
'tpc.id',
|
||
'tpc.poolId',
|
||
'tpc.identifier',
|
||
'tpc.companyId',
|
||
'tpc.friendStatus',
|
||
'tpc.level',
|
||
'tpc.intentionLevel',
|
||
'tpc.lastInteractTime',
|
||
'tpc.rfmF',
|
||
'tpc.rfmM',
|
||
'tpc.totalMsgCount',
|
||
'tpc.totalOrderAmount',
|
||
'tpc.lastMsgTime',
|
||
'tpc.firstSourceType',
|
||
'tpc.firstSourceTime',
|
||
'tpc.lifecycle',
|
||
'tpc.createTime',
|
||
'tpc.realName',
|
||
'tpc.phone',
|
||
'tp.nickname',
|
||
'tp.avatar',
|
||
'tp.wechatId',
|
||
'tp.wechatAlias',
|
||
'tp.gender',
|
||
'tp.region',
|
||
'tp.country',
|
||
'tp.province',
|
||
'tp.city'
|
||
])
|
||
->order('tpc.id DESC')
|
||
->page($page, $pageSize)
|
||
->select();
|
||
|
||
return $this->formatMemberList($list, $total, $page, $pageSize);
|
||
}
|
||
}
|
||
|
||
|