Files
ckb-SuperAdmin/Server/application/cunkebao/service/TrafficPoolGroupService.php
2026-02-02 11:00:26 +08:00

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
];
}
}