651 lines
21 KiB
PHP
651 lines
21 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.friendStatus',
|
|
'tpc.level',
|
|
'tpc.lastInteractTime',
|
|
'tpc.rfmF',
|
|
'tpc.rfmM',
|
|
'tpc.totalMsgCount',
|
|
'tpc.totalOrderAmount',
|
|
'tpc.realName',
|
|
'tpc.phone',
|
|
'tp.nickname',
|
|
'tp.avatar',
|
|
'tp.wechatId',
|
|
'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.friendStatus',
|
|
'tpc.level',
|
|
'tpc.lastInteractTime',
|
|
'tpc.rfmF',
|
|
'tpc.rfmM',
|
|
'tpc.totalMsgCount',
|
|
'tpc.totalOrderAmount',
|
|
'tpc.realName',
|
|
'tpc.phone',
|
|
'tpc.createTime as addTime',
|
|
'tp.nickname',
|
|
'tp.avatar',
|
|
'tp.wechatId'
|
|
])
|
|
->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') {
|
|
// 字段条件
|
|
$field = 'tpc.' . $condition['field'];
|
|
$operator = $condition['operator'];
|
|
$value = $condition['value'];
|
|
|
|
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 = [];
|
|
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);
|
|
$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')
|
|
->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')
|
|
->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
|
|
];
|
|
}
|
|
}
|
|
|
|
|