流量池服务端
This commit is contained in:
@@ -379,10 +379,50 @@ class MomentsController extends BaseController
|
||||
|
||||
$total = KfMoments::where(['companyId' => $companyId, 'userId' => $userId, 'isDel' => 0])->count();
|
||||
|
||||
// 收集所有需要查询的微信账号ID
|
||||
$allWechatAccountIds = [];
|
||||
foreach ($list as $item) {
|
||||
$sendData = json_decode($item->sendData, true);
|
||||
$items = $sendData['jobPublishWechatMomentsItems'] ?? [];
|
||||
foreach ($items as $accountItem) {
|
||||
if (!empty($accountItem['wechatAccountId'])) {
|
||||
$allWechatAccountIds[] = $accountItem['wechatAccountId'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 批量查询微信账号信息
|
||||
$wechatAccountsMap = [];
|
||||
if (!empty($allWechatAccountIds)) {
|
||||
$wechatAccounts = Db::table('s2_wechat_account')
|
||||
->whereIn('id', array_unique($allWechatAccountIds))
|
||||
->field('id, wechatId, nickName, avatar')
|
||||
->select();
|
||||
foreach ($wechatAccounts as $account) {
|
||||
$wechatAccountsMap[$account['id']] = $account;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理数据
|
||||
$data = [];
|
||||
foreach ($list as $item) {
|
||||
$sendData = json_decode($item->sendData,true);
|
||||
$sendData = json_decode($item->sendData, true);
|
||||
$momentsItems = $sendData['jobPublishWechatMomentsItems'] ?? [];
|
||||
|
||||
// 构建账号详情列表
|
||||
$accounts = [];
|
||||
foreach ($momentsItems as $accountItem) {
|
||||
$wechatAccountId = $accountItem['wechatAccountId'] ?? 0;
|
||||
$accountInfo = $wechatAccountsMap[$wechatAccountId] ?? null;
|
||||
$accounts[] = [
|
||||
'wechatAccountId' => $wechatAccountId,
|
||||
'wechatId' => $accountInfo['wechatId'] ?? '',
|
||||
'nickName' => $accountInfo['nickName'] ?? '',
|
||||
'avatar' => $accountInfo['avatar'] ?? '',
|
||||
'labels' => $accountItem['labels'] ?? []
|
||||
];
|
||||
}
|
||||
|
||||
$data[] = [
|
||||
'id' => $item->id,
|
||||
'content' => $sendData['text'] ?? '',
|
||||
@@ -392,8 +432,9 @@ class MomentsController extends BaseController
|
||||
'link' => $sendData['link'] ?? [],
|
||||
'publicMode' => $sendData['publicMode'] ?? 2,
|
||||
'isSend' => $item->isSend,
|
||||
'sendTime' => date('Y-m-d H:i:s',$item->sendTime),
|
||||
'accountCount' => count($sendData['jobPublishWechatMomentsItems'] ?? [])
|
||||
'sendTime' => date('Y-m-d H:i:s', $item->sendTime),
|
||||
'accountCount' => count($accounts),
|
||||
'accounts' => $accounts
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -50,4 +50,7 @@ return [
|
||||
|
||||
// 检查未读/未回复消息并自动迁移好友
|
||||
'check:unread-message' => 'app\command\CheckUnreadMessageCommand', // 检查未读/未回复消息并自动迁移好友
|
||||
|
||||
// V2 流量池数据迁移
|
||||
'migrate:trafficPoolV2' => 'app\command\MigrateTrafficPoolV2Command', // 迁移数据到 V2 流量池系统
|
||||
];
|
||||
|
||||
235
Server/application/command/MigrateTrafficPoolV2Command.php
Normal file
235
Server/application/command/MigrateTrafficPoolV2Command.php
Normal file
@@ -0,0 +1,235 @@
|
||||
<?php
|
||||
|
||||
namespace app\command;
|
||||
|
||||
use think\facade\Log;
|
||||
use think\console\Input;
|
||||
use think\console\Output;
|
||||
use think\console\Command;
|
||||
use think\console\input\Option;
|
||||
use think\facade\App;
|
||||
use WeChatDeviceApi\Adapters\ChuKeBao\Adapter as ChuKeBaoAdapter;
|
||||
|
||||
/**
|
||||
* V2 流量池数据迁移命令
|
||||
*
|
||||
* 使用方法:
|
||||
* php think migrate:trafficPoolV2 # 执行完整迁移
|
||||
* php think migrate:trafficPoolV2 --step=1 # 只执行第1步:好友同步到流量池总表
|
||||
* php think migrate:trafficPoolV2 --step=2 # 只执行第2步:好友同步到公司流量详情表
|
||||
* php think migrate:trafficPoolV2 --step=3 # 只执行第3步:好友同步到流量来源表
|
||||
* php think migrate:trafficPoolV2 --step=4 # 只执行第4步:群成员同步到流量池总表
|
||||
* php think migrate:trafficPoolV2 --step=5 # 只执行第5步:群成员同步到公司流量详情表
|
||||
* php think migrate:trafficPoolV2 --step=6 # 只执行第6步:群成员同步到流量来源表
|
||||
* php think migrate:trafficPoolV2 --step=7 # 只执行第7步:同步微信标签
|
||||
*
|
||||
* 执行前请确保已运行 SQL 迁移脚本创建了 V2 版本的表
|
||||
*/
|
||||
class MigrateTrafficPoolV2Command extends Command
|
||||
{
|
||||
protected $lockFile;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->lockFile = App::getRuntimePath() . 'migrate_traffic_pool_v2.lock';
|
||||
}
|
||||
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('migrate:trafficPoolV2')
|
||||
->setDescription('迁移数据到 V2 流量池系统')
|
||||
->addOption('step', 's', Option::VALUE_OPTIONAL, '执行指定步骤(1-7),不指定则执行全部', null);
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
// 检查锁文件
|
||||
if (file_exists($this->lockFile)) {
|
||||
$lockTime = filectime($this->lockFile);
|
||||
if (time() - $lockTime < 7200) { // 2小时内
|
||||
$output->writeln('<error>迁移任务已在运行中,跳过本次执行</error>');
|
||||
return false;
|
||||
}
|
||||
unlink($this->lockFile);
|
||||
}
|
||||
|
||||
file_put_contents($this->lockFile, time());
|
||||
|
||||
try {
|
||||
$step = $input->getOption('step');
|
||||
$adapter = new ChuKeBaoAdapter();
|
||||
|
||||
$output->writeln('<info>====================================</info>');
|
||||
$output->writeln('<info> V2 流量池数据迁移开始</info>');
|
||||
$output->writeln('<info>====================================</info>');
|
||||
$output->writeln('');
|
||||
|
||||
$startTime = microtime(true);
|
||||
|
||||
if ($step === null) {
|
||||
// 执行完整迁移
|
||||
$results = $this->runFullMigration($adapter, $output);
|
||||
} else {
|
||||
// 执行指定步骤
|
||||
$results = $this->runStep((int)$step, $adapter, $output);
|
||||
}
|
||||
|
||||
$endTime = microtime(true);
|
||||
$duration = round($endTime - $startTime, 2);
|
||||
|
||||
$output->writeln('');
|
||||
$output->writeln('<info>====================================</info>');
|
||||
$output->writeln('<info> 迁移完成</info>');
|
||||
$output->writeln('<info>====================================</info>');
|
||||
$output->writeln("耗时: {$duration} 秒");
|
||||
$output->writeln('');
|
||||
$output->writeln('<comment>结果统计:</comment>');
|
||||
foreach ($results as $key => $value) {
|
||||
$output->writeln(" - {$key}: {$value} 条");
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$output->writeln('<error>迁移异常: ' . $e->getMessage() . '</error>');
|
||||
Log::error('V2流量池迁移异常:' . $e->getMessage() . "\n" . $e->getTraceAsString());
|
||||
return false;
|
||||
} finally {
|
||||
if (file_exists($this->lockFile)) {
|
||||
unlink($this->lockFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行完整迁移
|
||||
*/
|
||||
protected function runFullMigration(ChuKeBaoAdapter $adapter, Output $output)
|
||||
{
|
||||
$results = [
|
||||
'friend_pool' => 0,
|
||||
'friend_pool_company' => 0,
|
||||
'friend_pool_source' => 0,
|
||||
'chatroom_pool' => 0,
|
||||
'chatroom_pool_company' => 0,
|
||||
'chatroom_pool_source' => 0,
|
||||
'pool_tags' => 0,
|
||||
];
|
||||
|
||||
// === 好友数据迁移 ===
|
||||
$output->writeln('<comment>【好友数据迁移】</comment>');
|
||||
|
||||
// Step 1: 好友同步到流量池总表
|
||||
$output->writeln('<comment>[1/7] 同步好友到流量池总表 ck_traffic_pool ...</comment>');
|
||||
$results['friend_pool'] = $adapter->syncToTrafficPoolV2();
|
||||
$output->writeln("<info> 完成,影响行数: {$results['friend_pool']}</info>");
|
||||
|
||||
// Step 2: 好友同步到公司流量详情表
|
||||
$output->writeln('<comment>[2/7] 同步好友到公司流量详情表 ck_traffic_pool_company ...</comment>');
|
||||
$results['friend_pool_company'] = $adapter->syncToTrafficPoolCompanyV2();
|
||||
$output->writeln("<info> 完成,影响行数: {$results['friend_pool_company']}</info>");
|
||||
|
||||
// Step 3: 好友同步到流量来源表
|
||||
$output->writeln('<comment>[3/7] 同步好友到流量来源表 ck_traffic_pool_source ...</comment>');
|
||||
$results['friend_pool_source'] = $adapter->syncToTrafficPoolSourceV2();
|
||||
$output->writeln("<info> 完成,影响行数: {$results['friend_pool_source']}</info>");
|
||||
|
||||
// === 群成员数据迁移 ===
|
||||
$output->writeln('');
|
||||
$output->writeln('<comment>【群成员数据迁移】</comment>');
|
||||
|
||||
// Step 4: 群成员同步到流量池总表
|
||||
$output->writeln('<comment>[4/7] 同步群成员到流量池总表 ck_traffic_pool ...</comment>');
|
||||
$results['chatroom_pool'] = $adapter->syncChatroomMembersToTrafficPoolV2();
|
||||
$output->writeln("<info> 完成,影响行数: {$results['chatroom_pool']}</info>");
|
||||
|
||||
// Step 5: 群成员同步到公司流量详情表
|
||||
$output->writeln('<comment>[5/7] 同步群成员到公司流量详情表 ck_traffic_pool_company ...</comment>');
|
||||
$results['chatroom_pool_company'] = $adapter->syncChatroomMembersToTrafficPoolCompanyV2();
|
||||
$output->writeln("<info> 完成,影响行数: {$results['chatroom_pool_company']}</info>");
|
||||
|
||||
// Step 6: 群成员同步到流量来源表
|
||||
$output->writeln('<comment>[6/7] 同步群成员到流量来源表 ck_traffic_pool_source ...</comment>');
|
||||
$results['chatroom_pool_source'] = $adapter->syncChatroomMembersToTrafficPoolSourceV2();
|
||||
$output->writeln("<info> 完成,影响行数: {$results['chatroom_pool_source']}</info>");
|
||||
|
||||
// === 标签数据迁移 ===
|
||||
$output->writeln('');
|
||||
$output->writeln('<comment>【标签数据迁移】</comment>');
|
||||
|
||||
// Step 7: 同步微信标签
|
||||
$output->writeln('<comment>[7/7] 同步微信标签 ck_traffic_pool_tag ...</comment>');
|
||||
$results['pool_tags'] = $adapter->syncWechatTagsToV2();
|
||||
$output->writeln("<info> 完成,影响行数: {$results['pool_tags']}</info>");
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行指定步骤
|
||||
*/
|
||||
protected function runStep(int $step, ChuKeBaoAdapter $adapter, Output $output)
|
||||
{
|
||||
$results = [];
|
||||
|
||||
switch ($step) {
|
||||
case 1:
|
||||
$output->writeln('<comment>[Step 1] 同步好友到流量池总表 ck_traffic_pool ...</comment>');
|
||||
$results['friend_pool'] = $adapter->syncToTrafficPoolV2();
|
||||
$output->writeln("<info> 完成,影响行数: {$results['friend_pool']}</info>");
|
||||
break;
|
||||
|
||||
case 2:
|
||||
$output->writeln('<comment>[Step 2] 同步好友到公司流量详情表 ck_traffic_pool_company ...</comment>');
|
||||
$results['friend_pool_company'] = $adapter->syncToTrafficPoolCompanyV2();
|
||||
$output->writeln("<info> 完成,影响行数: {$results['friend_pool_company']}</info>");
|
||||
break;
|
||||
|
||||
case 3:
|
||||
$output->writeln('<comment>[Step 3] 同步好友到流量来源表 ck_traffic_pool_source ...</comment>');
|
||||
$results['friend_pool_source'] = $adapter->syncToTrafficPoolSourceV2();
|
||||
$output->writeln("<info> 完成,影响行数: {$results['friend_pool_source']}</info>");
|
||||
break;
|
||||
|
||||
case 4:
|
||||
$output->writeln('<comment>[Step 4] 同步群成员到流量池总表 ck_traffic_pool ...</comment>');
|
||||
$results['chatroom_pool'] = $adapter->syncChatroomMembersToTrafficPoolV2();
|
||||
$output->writeln("<info> 完成,影响行数: {$results['chatroom_pool']}</info>");
|
||||
break;
|
||||
|
||||
case 5:
|
||||
$output->writeln('<comment>[Step 5] 同步群成员到公司流量详情表 ck_traffic_pool_company ...</comment>');
|
||||
$results['chatroom_pool_company'] = $adapter->syncChatroomMembersToTrafficPoolCompanyV2();
|
||||
$output->writeln("<info> 完成,影响行数: {$results['chatroom_pool_company']}</info>");
|
||||
break;
|
||||
|
||||
case 6:
|
||||
$output->writeln('<comment>[Step 6] 同步群成员到流量来源表 ck_traffic_pool_source ...</comment>');
|
||||
$results['chatroom_pool_source'] = $adapter->syncChatroomMembersToTrafficPoolSourceV2();
|
||||
$output->writeln("<info> 完成,影响行数: {$results['chatroom_pool_source']}</info>");
|
||||
break;
|
||||
|
||||
case 7:
|
||||
$output->writeln('<comment>[Step 7] 同步微信标签 ck_traffic_pool_tag ...</comment>');
|
||||
$results['pool_tags'] = $adapter->syncWechatTagsToV2();
|
||||
$output->writeln("<info> 完成,影响行数: {$results['pool_tags']}</info>");
|
||||
break;
|
||||
|
||||
default:
|
||||
$output->writeln('<error>无效的步骤编号,请输入 1-7</error>');
|
||||
$output->writeln('');
|
||||
$output->writeln('步骤说明:');
|
||||
$output->writeln(' 1 - 同步好友到流量池总表');
|
||||
$output->writeln(' 2 - 同步好友到公司流量详情表');
|
||||
$output->writeln(' 3 - 同步好友到流量来源表');
|
||||
$output->writeln(' 4 - 同步群成员到流量池总表');
|
||||
$output->writeln(' 5 - 同步群成员到公司流量详情表');
|
||||
$output->writeln(' 6 - 同步群成员到流量来源表');
|
||||
$output->writeln(' 7 - 同步微信标签');
|
||||
break;
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
@@ -124,5 +124,43 @@ class SyncWechatDataToCkbTask extends Command
|
||||
return $ChuKeBaoAdapter->syncCallRecording();
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步数据到 V2 流量池总表
|
||||
*/
|
||||
protected function syncToTrafficPoolV2(ChuKeBaoAdapter $ChuKeBaoAdapter)
|
||||
{
|
||||
return $ChuKeBaoAdapter->syncToTrafficPoolV2();
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步数据到 V2 公司流量详情表
|
||||
*/
|
||||
protected function syncToTrafficPoolCompanyV2(ChuKeBaoAdapter $ChuKeBaoAdapter)
|
||||
{
|
||||
return $ChuKeBaoAdapter->syncToTrafficPoolCompanyV2();
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步数据到 V2 流量来源表
|
||||
*/
|
||||
protected function syncToTrafficPoolSourceV2(ChuKeBaoAdapter $ChuKeBaoAdapter)
|
||||
{
|
||||
return $ChuKeBaoAdapter->syncToTrafficPoolSourceV2();
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步微信标签到 V2 标签系统
|
||||
*/
|
||||
protected function syncWechatTagsToV2(ChuKeBaoAdapter $ChuKeBaoAdapter)
|
||||
{
|
||||
return $ChuKeBaoAdapter->syncWechatTagsToV2();
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行完整的 V2 流量池数据迁移
|
||||
*/
|
||||
protected function migrateToTrafficPoolV2(ChuKeBaoAdapter $ChuKeBaoAdapter)
|
||||
{
|
||||
return $ChuKeBaoAdapter->migrateToTrafficPoolV2();
|
||||
}
|
||||
}
|
||||
168
Server/application/common/model/TrafficPoolAllotRecord.php
Normal file
168
Server/application/common/model/TrafficPoolAllotRecord.php
Normal file
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
/**
|
||||
* 流量分配记录表模型类
|
||||
* 表名:ck_traffic_pool_allot_record
|
||||
* 用途:记录流量的分配历史
|
||||
*/
|
||||
class TrafficPoolAllotRecord extends Model
|
||||
{
|
||||
// 设置数据表名
|
||||
protected $name = 'traffic_pool_allot_record';
|
||||
|
||||
// 主键
|
||||
protected $pk = 'id';
|
||||
|
||||
// 自动写入时间戳
|
||||
protected $autoWriteTimestamp = true;
|
||||
protected $createTime = 'createTime';
|
||||
protected $updateTime = 'updateTime';
|
||||
|
||||
// 分配类型常量
|
||||
const ALLOT_TYPE_FIRST = 1; // 首次分配
|
||||
const ALLOT_TYPE_REASSIGN = 2; // 重新分配
|
||||
const ALLOT_TYPE_RECYCLE = 3; // 回收后分配
|
||||
|
||||
// 状态常量
|
||||
const STATUS_ACTIVE = 1; // 生效中
|
||||
const STATUS_EXPIRED = 2; // 已过期
|
||||
const STATUS_RECYCLED = 3; // 已回收
|
||||
|
||||
/**
|
||||
* 关联公司流量详情
|
||||
*/
|
||||
public function poolCompany()
|
||||
{
|
||||
return $this->belongsTo(TrafficPoolCompany::class, 'poolCompanyId', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建分配记录
|
||||
* @param int $poolCompanyId
|
||||
* @param string $identifier
|
||||
* @param int $companyId
|
||||
* @param string $toWechatId
|
||||
* @param int $toAccountId
|
||||
* @param int $toUserId
|
||||
* @param int $expireDays
|
||||
* @param int $operatorId
|
||||
* @param array $fromInfo [fromWechatId, fromAccountId, fromUserId]
|
||||
* @return static
|
||||
*/
|
||||
public static function createAllotRecord(
|
||||
int $poolCompanyId,
|
||||
string $identifier,
|
||||
int $companyId,
|
||||
string $toWechatId,
|
||||
int $toAccountId = null,
|
||||
int $toUserId = null,
|
||||
int $expireDays = 30,
|
||||
int $operatorId = null,
|
||||
array $fromInfo = []
|
||||
) {
|
||||
// 判断分配类型
|
||||
$existRecord = self::where('poolCompanyId', $poolCompanyId)
|
||||
->where('status', self::STATUS_ACTIVE)
|
||||
->find();
|
||||
|
||||
$allotType = self::ALLOT_TYPE_FIRST;
|
||||
if ($existRecord) {
|
||||
// 将原记录设为已回收
|
||||
$existRecord->save([
|
||||
'status' => self::STATUS_RECYCLED,
|
||||
'updateTime' => time()
|
||||
]);
|
||||
$allotType = self::ALLOT_TYPE_REASSIGN;
|
||||
}
|
||||
|
||||
// 检查之前是否有过分配记录(判断是否回收后分配)
|
||||
$hasHistoryRecord = self::where('poolCompanyId', $poolCompanyId)
|
||||
->where('status', 'in', [self::STATUS_EXPIRED, self::STATUS_RECYCLED])
|
||||
->count();
|
||||
if ($hasHistoryRecord && $allotType === self::ALLOT_TYPE_FIRST) {
|
||||
$allotType = self::ALLOT_TYPE_RECYCLE;
|
||||
}
|
||||
|
||||
$expireTime = $expireDays > 0 ? time() + ($expireDays * 86400) : null;
|
||||
|
||||
$record = self::create([
|
||||
'poolCompanyId' => $poolCompanyId,
|
||||
'identifier' => $identifier,
|
||||
'companyId' => $companyId,
|
||||
'allotType' => $allotType,
|
||||
'fromWechatId' => $fromInfo['fromWechatId'] ?? null,
|
||||
'fromAccountId' => $fromInfo['fromAccountId'] ?? null,
|
||||
'fromUserId' => $fromInfo['fromUserId'] ?? null,
|
||||
'toWechatId' => $toWechatId,
|
||||
'toAccountId' => $toAccountId,
|
||||
'toUserId' => $toUserId,
|
||||
'expireDays' => $expireDays,
|
||||
'expireTime' => $expireTime,
|
||||
'status' => self::STATUS_ACTIVE,
|
||||
'operatorId' => $operatorId,
|
||||
'createTime' => time(),
|
||||
]);
|
||||
|
||||
// 更新公司流量详情表的归属信息
|
||||
TrafficPoolCompany::where('id', $poolCompanyId)->update([
|
||||
'ownerWechatId' => $toWechatId,
|
||||
'ownerAccountId' => $toAccountId,
|
||||
'ownerUserId' => $toUserId,
|
||||
'allocateStatus' => TrafficPoolCompany::ALLOCATE_STATUS_ALLOCATED,
|
||||
'allocateTime' => time(),
|
||||
'expireTime' => $expireTime,
|
||||
'updateTime' => time()
|
||||
]);
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
/**
|
||||
* 回收分配
|
||||
* @param int $poolCompanyId
|
||||
* @param int $operatorId
|
||||
* @return bool
|
||||
*/
|
||||
public static function recycleAllot(int $poolCompanyId, int $operatorId = null)
|
||||
{
|
||||
// 更新当前生效的分配记录
|
||||
$activeRecord = self::where('poolCompanyId', $poolCompanyId)
|
||||
->where('status', self::STATUS_ACTIVE)
|
||||
->find();
|
||||
|
||||
if ($activeRecord) {
|
||||
$activeRecord->save([
|
||||
'status' => self::STATUS_RECYCLED,
|
||||
'updateTime' => time()
|
||||
]);
|
||||
}
|
||||
|
||||
// 更新公司流量详情表
|
||||
return TrafficPoolCompany::where('id', $poolCompanyId)->update([
|
||||
'ownerWechatId' => null,
|
||||
'ownerAccountId' => null,
|
||||
'ownerUserId' => null,
|
||||
'allocateStatus' => TrafficPoolCompany::ALLOCATE_STATUS_RECYCLED,
|
||||
'expireTime' => null,
|
||||
'updateTime' => time()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取流量的分配历史
|
||||
* @param int $poolCompanyId
|
||||
* @return \think\Collection
|
||||
*/
|
||||
public static function getAllotHistory(int $poolCompanyId)
|
||||
{
|
||||
return self::where('poolCompanyId', $poolCompanyId)
|
||||
->order('createTime DESC')
|
||||
->select();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
200
Server/application/common/model/TrafficPoolBehavior.php
Normal file
200
Server/application/common/model/TrafficPoolBehavior.php
Normal file
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
/**
|
||||
* 流量行为表模型类
|
||||
* 表名:ck_traffic_pool_behavior
|
||||
* 用途:记录流量的各种行为(包括所有消息互动)
|
||||
*/
|
||||
class TrafficPoolBehavior extends Model
|
||||
{
|
||||
// 设置数据表名
|
||||
protected $name = 'traffic_pool_behavior';
|
||||
|
||||
// 主键
|
||||
protected $pk = 'id';
|
||||
|
||||
// 自动写入时间戳
|
||||
protected $autoWriteTimestamp = 'createTime';
|
||||
protected $createTime = 'createTime';
|
||||
protected $updateTime = false;
|
||||
|
||||
// 行为类型常量
|
||||
const BEHAVIOR_TYPE_SEND_MSG = 1; // 发送消息
|
||||
const BEHAVIOR_TYPE_RECEIVE_MSG = 2; // 接收消息
|
||||
const BEHAVIOR_TYPE_VIEW = 3; // 浏览
|
||||
const BEHAVIOR_TYPE_CLICK = 4; // 点击
|
||||
const BEHAVIOR_TYPE_CONSULT = 5; // 咨询
|
||||
const BEHAVIOR_TYPE_ORDER = 6; // 下单
|
||||
const BEHAVIOR_TYPE_PAY = 7; // 支付
|
||||
const BEHAVIOR_TYPE_REFUND = 8; // 退款
|
||||
const BEHAVIOR_TYPE_LIKE_MOMENTS = 9; // 点赞朋友圈
|
||||
const BEHAVIOR_TYPE_COMMENT_MOMENTS = 10; // 评论朋友圈
|
||||
|
||||
// 行为类型名称映射
|
||||
const BEHAVIOR_TYPE_NAMES = [
|
||||
self::BEHAVIOR_TYPE_SEND_MSG => '发送消息',
|
||||
self::BEHAVIOR_TYPE_RECEIVE_MSG => '接收消息',
|
||||
self::BEHAVIOR_TYPE_VIEW => '浏览',
|
||||
self::BEHAVIOR_TYPE_CLICK => '点击',
|
||||
self::BEHAVIOR_TYPE_CONSULT => '咨询',
|
||||
self::BEHAVIOR_TYPE_ORDER => '下单',
|
||||
self::BEHAVIOR_TYPE_PAY => '支付',
|
||||
self::BEHAVIOR_TYPE_REFUND => '退款',
|
||||
self::BEHAVIOR_TYPE_LIKE_MOMENTS => '点赞朋友圈',
|
||||
self::BEHAVIOR_TYPE_COMMENT_MOMENTS => '评论朋友圈',
|
||||
];
|
||||
|
||||
/**
|
||||
* 关联公司流量详情
|
||||
*/
|
||||
public function poolCompany()
|
||||
{
|
||||
return $this->belongsTo(TrafficPoolCompany::class, 'poolCompanyId', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取额外信息
|
||||
* @param string $value
|
||||
* @return array
|
||||
*/
|
||||
public function getExtraAttr($value)
|
||||
{
|
||||
return $value ? json_decode($value, true) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置额外信息
|
||||
* @param array $value
|
||||
* @return string
|
||||
*/
|
||||
public function setExtraAttr($value)
|
||||
{
|
||||
return $value ? json_encode($value, JSON_UNESCAPED_UNICODE) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取行为类型名称
|
||||
* @return string
|
||||
*/
|
||||
public function getBehaviorTypeNameAttr()
|
||||
{
|
||||
return self::BEHAVIOR_TYPE_NAMES[$this->behaviorType] ?? '未知行为';
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录消息行为
|
||||
* @param int $poolCompanyId
|
||||
* @param string $identifier
|
||||
* @param int $companyId
|
||||
* @param int $behaviorType 发送/接收
|
||||
* @param int $messageId
|
||||
* @param int $wechatAccountId
|
||||
* @param array $extra
|
||||
* @return static
|
||||
*/
|
||||
public static function recordMessageBehavior(int $poolCompanyId, string $identifier, int $companyId, int $behaviorType, int $messageId = null, int $wechatAccountId = null, array $extra = [])
|
||||
{
|
||||
$behavior = self::create([
|
||||
'poolCompanyId' => $poolCompanyId,
|
||||
'identifier' => $identifier,
|
||||
'companyId' => $companyId,
|
||||
'behaviorType' => $behaviorType,
|
||||
'behaviorName' => self::BEHAVIOR_TYPE_NAMES[$behaviorType] ?? '消息',
|
||||
'messageId' => $messageId,
|
||||
'wechatAccountId' => $wechatAccountId,
|
||||
'extra' => $extra,
|
||||
'behaviorTime' => time(),
|
||||
'createTime' => time(),
|
||||
]);
|
||||
|
||||
// 更新流量统计
|
||||
$poolCompany = TrafficPoolCompany::find($poolCompanyId);
|
||||
if ($poolCompany) {
|
||||
$poolCompany->incrementMsgCount(1);
|
||||
}
|
||||
|
||||
return $behavior;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录订单行为
|
||||
* @param int $poolCompanyId
|
||||
* @param string $identifier
|
||||
* @param int $companyId
|
||||
* @param int $behaviorType
|
||||
* @param string $orderId
|
||||
* @param float $amount
|
||||
* @param array $extra
|
||||
* @return static
|
||||
*/
|
||||
public static function recordOrderBehavior(int $poolCompanyId, string $identifier, int $companyId, int $behaviorType, string $orderId, float $amount = 0, array $extra = [])
|
||||
{
|
||||
$behavior = self::create([
|
||||
'poolCompanyId' => $poolCompanyId,
|
||||
'identifier' => $identifier,
|
||||
'companyId' => $companyId,
|
||||
'behaviorType' => $behaviorType,
|
||||
'behaviorName' => self::BEHAVIOR_TYPE_NAMES[$behaviorType] ?? '订单',
|
||||
'orderId' => $orderId,
|
||||
'amount' => $amount,
|
||||
'extra' => $extra,
|
||||
'behaviorTime' => time(),
|
||||
'createTime' => time(),
|
||||
]);
|
||||
|
||||
// 如果是支付行为,更新订单统计
|
||||
if ($behaviorType === self::BEHAVIOR_TYPE_PAY) {
|
||||
$poolCompany = TrafficPoolCompany::find($poolCompanyId);
|
||||
if ($poolCompany) {
|
||||
$poolCompany->incrementOrderStats($amount);
|
||||
}
|
||||
}
|
||||
|
||||
return $behavior;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录朋友圈互动行为
|
||||
* @param int $poolCompanyId
|
||||
* @param string $identifier
|
||||
* @param int $companyId
|
||||
* @param int $behaviorType 点赞/评论
|
||||
* @param int $momentsId
|
||||
* @param array $extra
|
||||
* @return static
|
||||
*/
|
||||
public static function recordMomentsBehavior(int $poolCompanyId, string $identifier, int $companyId, int $behaviorType, int $momentsId, array $extra = [])
|
||||
{
|
||||
return self::create([
|
||||
'poolCompanyId' => $poolCompanyId,
|
||||
'identifier' => $identifier,
|
||||
'companyId' => $companyId,
|
||||
'behaviorType' => $behaviorType,
|
||||
'behaviorName' => self::BEHAVIOR_TYPE_NAMES[$behaviorType] ?? '朋友圈互动',
|
||||
'momentsId' => $momentsId,
|
||||
'extra' => $extra,
|
||||
'behaviorTime' => time(),
|
||||
'createTime' => time(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户行为轨迹
|
||||
* @param int $poolCompanyId
|
||||
* @param int $limit
|
||||
* @return \think\Collection
|
||||
*/
|
||||
public static function getUserJourney(int $poolCompanyId, int $limit = 50)
|
||||
{
|
||||
return self::where('poolCompanyId', $poolCompanyId)
|
||||
->order('behaviorTime DESC')
|
||||
->limit($limit)
|
||||
->select();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
200
Server/application/common/model/TrafficPoolCompany.php
Normal file
200
Server/application/common/model/TrafficPoolCompany.php
Normal file
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use think\Model;
|
||||
use think\Db;
|
||||
|
||||
/**
|
||||
* 公司流量详情表模型类
|
||||
* 表名:ck_traffic_pool_company
|
||||
* 用途:存储流量在各公司的详细信息,支持多租户
|
||||
*/
|
||||
class TrafficPoolCompany extends Model
|
||||
{
|
||||
// 设置数据表名
|
||||
protected $name = 'traffic_pool_company';
|
||||
|
||||
// 主键
|
||||
protected $pk = 'id';
|
||||
|
||||
// 自动写入时间戳
|
||||
protected $autoWriteTimestamp = true;
|
||||
protected $createTime = 'createTime';
|
||||
protected $updateTime = 'updateTime';
|
||||
|
||||
// 好友状态常量
|
||||
const FRIEND_STATUS_NOT_ADDED = 0; // 未加
|
||||
const FRIEND_STATUS_PENDING = 1; // 待通过
|
||||
const FRIEND_STATUS_PASSED = 2; // 已通过
|
||||
const FRIEND_STATUS_DELETED = 3; // 已删除(我删除对方)
|
||||
const FRIEND_STATUS_BE_DELETED = 4; // 被删除(对方删除我)
|
||||
|
||||
// 客户等级常量
|
||||
const LEVEL_NORMAL = 0; // 普通
|
||||
const LEVEL_IMPORTANT = 1; // 重要
|
||||
const LEVEL_VIP = 2; // VIP
|
||||
|
||||
// 意向度常量
|
||||
const INTENTION_UNKNOWN = 0; // 未知
|
||||
const INTENTION_LOW = 1; // 低
|
||||
const INTENTION_MEDIUM = 2; // 中
|
||||
const INTENTION_HIGH = 3; // 高
|
||||
|
||||
// 生命周期常量
|
||||
const LIFECYCLE_NEW = 1; // 新流量
|
||||
const LIFECYCLE_FOLLOWING = 2; // 跟进中
|
||||
const LIFECYCLE_CONVERTED = 3; // 已成交
|
||||
const LIFECYCLE_SILENT = 4; // 沉默
|
||||
const LIFECYCLE_LOST = 5; // 流失
|
||||
|
||||
// 状态常量
|
||||
const STATUS_DISABLED = 0; // 禁用
|
||||
const STATUS_NORMAL = 1; // 正常
|
||||
const STATUS_BLACKLIST = 2; // 黑名单
|
||||
|
||||
// 分配状态常量
|
||||
const ALLOCATE_STATUS_NOT = 0; // 未分配
|
||||
const ALLOCATE_STATUS_ALLOCATED = 1; // 已分配
|
||||
const ALLOCATE_STATUS_RECYCLED = 2; // 已回收
|
||||
|
||||
/**
|
||||
* 关联流量池总表
|
||||
*/
|
||||
public function pool()
|
||||
{
|
||||
return $this->belongsTo(TrafficPoolV2::class, 'poolId', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联来源记录
|
||||
*/
|
||||
public function sources()
|
||||
{
|
||||
return $this->hasMany(TrafficPoolSource::class, 'poolCompanyId', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联标签记录
|
||||
*/
|
||||
public function tags()
|
||||
{
|
||||
return $this->hasMany(TrafficPoolTag::class, 'poolCompanyId', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联行为记录
|
||||
*/
|
||||
public function behaviors()
|
||||
{
|
||||
return $this->hasMany(TrafficPoolBehavior::class, 'poolCompanyId', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联分配记录
|
||||
*/
|
||||
public function allotRecords()
|
||||
{
|
||||
return $this->hasMany(TrafficPoolAllotRecord::class, 'poolCompanyId', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据identifier和companyId查找或创建
|
||||
* @param string $identifier
|
||||
* @param int $companyId
|
||||
* @param int $poolId
|
||||
* @param array $data
|
||||
* @return static
|
||||
*/
|
||||
public static function findOrCreateByIdentifierAndCompany(string $identifier, int $companyId, int $poolId, array $data = [])
|
||||
{
|
||||
$record = self::where('identifier', $identifier)
|
||||
->where('companyId', $companyId)
|
||||
->where('isDel', 0)
|
||||
->find();
|
||||
|
||||
if (!$record) {
|
||||
$insertData = array_merge([
|
||||
'poolId' => $poolId,
|
||||
'identifier' => $identifier,
|
||||
'companyId' => $companyId,
|
||||
'createTime' => time(),
|
||||
], $data);
|
||||
|
||||
$record = self::create($insertData);
|
||||
}
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子更新消息统计
|
||||
* @param int $count 增加的消息数量
|
||||
* @return bool
|
||||
*/
|
||||
public function incrementMsgCount(int $count = 1)
|
||||
{
|
||||
return Db::table($this->getTable())
|
||||
->where('id', $this->id)
|
||||
->inc('totalMsgCount', $count)
|
||||
->inc('rfmF', $count)
|
||||
->update([
|
||||
'lastMsgTime' => time(),
|
||||
'lastInteractTime' => time(),
|
||||
'updateTime' => time()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子更新订单统计
|
||||
* @param float $amount 订单金额
|
||||
* @return bool
|
||||
*/
|
||||
public function incrementOrderStats(float $amount)
|
||||
{
|
||||
return Db::table($this->getTable())
|
||||
->where('id', $this->id)
|
||||
->inc('totalOrderCount', 1)
|
||||
->inc('totalOrderAmount', $amount)
|
||||
->inc('rfmM', $amount)
|
||||
->update([
|
||||
'lastOrderTime' => time(),
|
||||
'lastInteractTime' => time(),
|
||||
'updateTime' => time()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算RFM R值(最后互动距今天数)
|
||||
* @return int
|
||||
*/
|
||||
public function getRfmRAttr()
|
||||
{
|
||||
if (empty($this->lastInteractTime)) {
|
||||
return 9999; // 未互动过
|
||||
}
|
||||
return (int) floor((time() - $this->lastInteractTime) / 86400);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取自定义字段
|
||||
* @param string $value
|
||||
* @return array
|
||||
*/
|
||||
public function getCustomFieldsAttr($value)
|
||||
{
|
||||
return $value ? json_decode($value, true) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置自定义字段
|
||||
* @param array $value
|
||||
* @return string
|
||||
*/
|
||||
public function setCustomFieldsAttr($value)
|
||||
{
|
||||
return $value ? json_encode($value, JSON_UNESCAPED_UNICODE) : null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
171
Server/application/common/model/TrafficPoolGroup.php
Normal file
171
Server/application/common/model/TrafficPoolGroup.php
Normal file
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
/**
|
||||
* 流量池分组表模型类
|
||||
* 表名:ck_traffic_pool_group
|
||||
* 用途:管理流量池分组(如:高价值客户池、潜在客户池等)
|
||||
*/
|
||||
class TrafficPoolGroup extends Model
|
||||
{
|
||||
// 设置数据表名
|
||||
protected $name = 'traffic_pool_group';
|
||||
|
||||
// 主键
|
||||
protected $pk = 'id';
|
||||
|
||||
// 自动写入时间戳
|
||||
protected $autoWriteTimestamp = true;
|
||||
protected $createTime = 'createTime';
|
||||
protected $updateTime = 'updateTime';
|
||||
|
||||
// 规则类型常量
|
||||
const RULE_TYPE_DYNAMIC = 1; // 动态规则
|
||||
const RULE_TYPE_MANUAL = 2; // 手动添加
|
||||
|
||||
// 状态常量
|
||||
const STATUS_DISABLED = 0;
|
||||
const STATUS_ENABLED = 1;
|
||||
|
||||
// 系统默认分组编码
|
||||
const GROUP_CODE_ALL_FRIENDS = 'all_friends'; // 全部好友流量池
|
||||
const GROUP_CODE_HIGH_VALUE = 'high_value'; // 高价值客户池
|
||||
const GROUP_CODE_POTENTIAL = 'potential'; // 潜在客户池
|
||||
const GROUP_CODE_HIGH_INTERACT = 'high_interact'; // 高互动客户池
|
||||
|
||||
/**
|
||||
* 关联分组成员
|
||||
*/
|
||||
public function members()
|
||||
{
|
||||
return $this->hasMany(TrafficPoolGroupMember::class, 'groupId', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取规则配置
|
||||
* @param string $value
|
||||
* @return array|null
|
||||
*/
|
||||
public function getRuleConfigAttr($value)
|
||||
{
|
||||
return $value ? json_decode($value, true) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置规则配置
|
||||
* @param array $value
|
||||
* @return string
|
||||
*/
|
||||
public function setRuleConfigAttr($value)
|
||||
{
|
||||
return $value ? json_encode($value, JSON_UNESCAPED_UNICODE) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取公司可用的分组列表(包含系统分组和公司自定义分组)
|
||||
* @param int $companyId
|
||||
* @param bool $onlyEnabled
|
||||
* @return \think\Collection
|
||||
*/
|
||||
public static function getGroupsByCompany(int $companyId, bool $onlyEnabled = true)
|
||||
{
|
||||
$query = self::whereIn('companyId', [0, $companyId])
|
||||
->where('isDel', 0);
|
||||
|
||||
if ($onlyEnabled) {
|
||||
$query->where('status', self::STATUS_ENABLED);
|
||||
}
|
||||
|
||||
return $query->order('sort ASC, id ASC')->select();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据分组编码获取分组
|
||||
* @param string $groupCode
|
||||
* @param int $companyId
|
||||
* @return static|null
|
||||
*/
|
||||
public static function getByCode(string $groupCode, int $companyId = 0)
|
||||
{
|
||||
return self::where('groupCode', $groupCode)
|
||||
->whereIn('companyId', [0, $companyId])
|
||||
->where('isDel', 0)
|
||||
->find();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析规则配置生成SQL条件
|
||||
* @param array $ruleConfig
|
||||
* @return array [whereConditions, bindings]
|
||||
*/
|
||||
public static function parseRuleToConditions(array $ruleConfig)
|
||||
{
|
||||
$conditions = [];
|
||||
$bindings = [];
|
||||
|
||||
if (empty($ruleConfig['conditions'])) {
|
||||
return [$conditions, $bindings];
|
||||
}
|
||||
|
||||
$logic = strtoupper($ruleConfig['logic'] ?? 'AND');
|
||||
|
||||
foreach ($ruleConfig['conditions'] as $condition) {
|
||||
if ($condition['type'] === 'group') {
|
||||
// 嵌套分组,递归处理
|
||||
[$subConditions, $subBindings] = self::parseRuleToConditions($condition);
|
||||
if (!empty($subConditions)) {
|
||||
$conditions[] = '(' . implode(' ' . ($condition['logic'] ?? 'AND') . ' ', $subConditions) . ')';
|
||||
$bindings = array_merge($bindings, $subBindings);
|
||||
}
|
||||
} elseif ($condition['type'] === 'field') {
|
||||
// 字段条件
|
||||
$field = $condition['field'];
|
||||
$operator = $condition['operator'];
|
||||
$value = $condition['value'];
|
||||
|
||||
switch ($operator) {
|
||||
case '=':
|
||||
case '!=':
|
||||
case '>':
|
||||
case '<':
|
||||
case '>=':
|
||||
case '<=':
|
||||
$conditions[] = "`{$field}` {$operator} ?";
|
||||
$bindings[] = $value;
|
||||
break;
|
||||
case 'in':
|
||||
$placeholders = implode(',', array_fill(0, count($value), '?'));
|
||||
$conditions[] = "`{$field}` IN ({$placeholders})";
|
||||
$bindings = array_merge($bindings, $value);
|
||||
break;
|
||||
case 'not_in':
|
||||
$placeholders = implode(',', array_fill(0, count($value), '?'));
|
||||
$conditions[] = "`{$field}` NOT IN ({$placeholders})";
|
||||
$bindings = array_merge($bindings, $value);
|
||||
break;
|
||||
case 'between':
|
||||
$conditions[] = "`{$field}` BETWEEN ? AND ?";
|
||||
$bindings[] = $value[0];
|
||||
$bindings[] = $value[1];
|
||||
break;
|
||||
case 'like':
|
||||
$conditions[] = "`{$field}` LIKE ?";
|
||||
$bindings[] = '%' . $value . '%';
|
||||
break;
|
||||
}
|
||||
} elseif ($condition['type'] === 'tag') {
|
||||
// 标签条件需要特殊处理,通过子查询
|
||||
// 这里返回需要在Service层特殊处理
|
||||
$conditions[] = "EXISTS (SELECT 1 FROM ck_traffic_pool_tag tpt WHERE tpt.poolCompanyId = ck_traffic_pool_company.id AND tpt.tagName IN (?))";
|
||||
$bindings[] = implode("','", $condition['value']);
|
||||
}
|
||||
}
|
||||
|
||||
return [$conditions, $bindings, $logic];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
128
Server/application/common/model/TrafficPoolGroupMember.php
Normal file
128
Server/application/common/model/TrafficPoolGroupMember.php
Normal file
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
/**
|
||||
* 流量池分组成员表模型类
|
||||
* 表名:ck_traffic_pool_group_member
|
||||
* 用途:手动添加到分组的成员(ruleType=2时使用)
|
||||
*/
|
||||
class TrafficPoolGroupMember extends Model
|
||||
{
|
||||
// 设置数据表名
|
||||
protected $name = 'traffic_pool_group_member';
|
||||
|
||||
// 主键
|
||||
protected $pk = 'id';
|
||||
|
||||
// 自动写入时间戳
|
||||
protected $autoWriteTimestamp = 'createTime';
|
||||
protected $createTime = 'createTime';
|
||||
protected $updateTime = false;
|
||||
|
||||
// 添加方式常量
|
||||
const ADD_TYPE_MANUAL = 1; // 手动
|
||||
const ADD_TYPE_IMPORT = 2; // 批量导入
|
||||
|
||||
/**
|
||||
* 关联分组
|
||||
*/
|
||||
public function group()
|
||||
{
|
||||
return $this->belongsTo(TrafficPoolGroup::class, 'groupId', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联公司流量详情
|
||||
*/
|
||||
public function poolCompany()
|
||||
{
|
||||
return $this->belongsTo(TrafficPoolCompany::class, 'poolCompanyId', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量添加成员到分组
|
||||
* @param int $groupId
|
||||
* @param array $poolCompanyIds
|
||||
* @param int $companyId
|
||||
* @param int $operatorId
|
||||
* @param int $addType
|
||||
* @return int 成功添加的数量
|
||||
*/
|
||||
public static function batchAddMembers(int $groupId, array $poolCompanyIds, int $companyId, int $operatorId = null, int $addType = self::ADD_TYPE_MANUAL)
|
||||
{
|
||||
$count = 0;
|
||||
$time = time();
|
||||
|
||||
// 获取已存在的成员
|
||||
$existIds = self::where('groupId', $groupId)
|
||||
->whereIn('poolCompanyId', $poolCompanyIds)
|
||||
->where('isDel', 0)
|
||||
->column('poolCompanyId');
|
||||
|
||||
// 获取要添加的流量详情
|
||||
$poolCompanies = TrafficPoolCompany::whereIn('id', $poolCompanyIds)
|
||||
->where('companyId', $companyId)
|
||||
->where('isDel', 0)
|
||||
->column('identifier', 'id');
|
||||
|
||||
$insertData = [];
|
||||
foreach ($poolCompanyIds as $poolCompanyId) {
|
||||
if (in_array($poolCompanyId, $existIds)) {
|
||||
continue; // 跳过已存在的
|
||||
}
|
||||
if (!isset($poolCompanies[$poolCompanyId])) {
|
||||
continue; // 跳过不存在的
|
||||
}
|
||||
|
||||
$insertData[] = [
|
||||
'groupId' => $groupId,
|
||||
'poolCompanyId' => $poolCompanyId,
|
||||
'identifier' => $poolCompanies[$poolCompanyId],
|
||||
'companyId' => $companyId,
|
||||
'addType' => $addType,
|
||||
'operatorId' => $operatorId,
|
||||
'createTime' => $time,
|
||||
'isDel' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
if (!empty($insertData)) {
|
||||
(new self())->saveAll($insertData);
|
||||
$count = count($insertData);
|
||||
|
||||
// 更新分组成员数量缓存
|
||||
TrafficPoolGroup::where('id', $groupId)->setInc('memberCount', $count);
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量移除成员
|
||||
* @param int $groupId
|
||||
* @param array $poolCompanyIds
|
||||
* @return int
|
||||
*/
|
||||
public static function batchRemoveMembers(int $groupId, array $poolCompanyIds)
|
||||
{
|
||||
$count = self::where('groupId', $groupId)
|
||||
->whereIn('poolCompanyId', $poolCompanyIds)
|
||||
->where('isDel', 0)
|
||||
->update([
|
||||
'isDel' => 1,
|
||||
'deleteTime' => time()
|
||||
]);
|
||||
|
||||
if ($count > 0) {
|
||||
// 更新分组成员数量缓存
|
||||
TrafficPoolGroup::where('id', $groupId)->setDec('memberCount', $count);
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
135
Server/application/common/model/TrafficPoolSource.php
Normal file
135
Server/application/common/model/TrafficPoolSource.php
Normal file
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
/**
|
||||
* 流量来源表模型类
|
||||
* 表名:ck_traffic_pool_source
|
||||
* 用途:记录流量的获取渠道和来源路径
|
||||
*/
|
||||
class TrafficPoolSource extends Model
|
||||
{
|
||||
// 设置数据表名
|
||||
protected $name = 'traffic_pool_source';
|
||||
|
||||
// 主键
|
||||
protected $pk = 'id';
|
||||
|
||||
// 自动写入时间戳
|
||||
protected $autoWriteTimestamp = true;
|
||||
protected $createTime = 'createTime';
|
||||
protected $updateTime = 'updateTime';
|
||||
|
||||
// 来源类型常量
|
||||
const SOURCE_TYPE_FRIEND_ADD = 1; // 好友添加
|
||||
const SOURCE_TYPE_GROUP_MEMBER = 2; // 群成员
|
||||
const SOURCE_TYPE_POSTER = 3; // 海报获客
|
||||
const SOURCE_TYPE_PHONE = 4; // 电话获客
|
||||
const SOURCE_TYPE_ORDER = 5; // 订单获客
|
||||
const SOURCE_TYPE_API = 6; // API导入
|
||||
const SOURCE_TYPE_MANUAL = 7; // 手动导入
|
||||
const SOURCE_TYPE_FISSION = 8; // 裂变活动
|
||||
|
||||
// 来源类型名称映射
|
||||
const SOURCE_TYPE_NAMES = [
|
||||
self::SOURCE_TYPE_FRIEND_ADD => '好友添加',
|
||||
self::SOURCE_TYPE_GROUP_MEMBER => '群成员',
|
||||
self::SOURCE_TYPE_POSTER => '海报获客',
|
||||
self::SOURCE_TYPE_PHONE => '电话获客',
|
||||
self::SOURCE_TYPE_ORDER => '订单获客',
|
||||
self::SOURCE_TYPE_API => 'API导入',
|
||||
self::SOURCE_TYPE_MANUAL => '手动导入',
|
||||
self::SOURCE_TYPE_FISSION => '裂变活动',
|
||||
];
|
||||
|
||||
/**
|
||||
* 关联公司流量详情
|
||||
*/
|
||||
public function poolCompany()
|
||||
{
|
||||
return $this->belongsTo(TrafficPoolCompany::class, 'poolCompanyId', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取额外信息
|
||||
* @param string $value
|
||||
* @return array
|
||||
*/
|
||||
public function getExtraAttr($value)
|
||||
{
|
||||
return $value ? json_decode($value, true) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置额外信息
|
||||
* @param array $value
|
||||
* @return string
|
||||
*/
|
||||
public function setExtraAttr($value)
|
||||
{
|
||||
return $value ? json_encode($value, JSON_UNESCAPED_UNICODE) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取来源类型名称
|
||||
* @return string
|
||||
*/
|
||||
public function getSourceTypeNameAttr()
|
||||
{
|
||||
return self::SOURCE_TYPE_NAMES[$this->sourceType] ?? '未知来源';
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建来源记录
|
||||
* @param int $poolCompanyId
|
||||
* @param string $identifier
|
||||
* @param int $companyId
|
||||
* @param int $sourceType
|
||||
* @param array $data
|
||||
* @return static
|
||||
*/
|
||||
public static function createSource(int $poolCompanyId, string $identifier, int $companyId, int $sourceType, array $data = [])
|
||||
{
|
||||
// 检查是否为首次来源
|
||||
$existSource = self::where('poolCompanyId', $poolCompanyId)->find();
|
||||
$isFirstSource = $existSource ? 0 : 1;
|
||||
|
||||
$insertData = array_merge([
|
||||
'poolCompanyId' => $poolCompanyId,
|
||||
'identifier' => $identifier,
|
||||
'companyId' => $companyId,
|
||||
'sourceType' => $sourceType,
|
||||
'isFirstSource' => $isFirstSource,
|
||||
'createTime' => time(),
|
||||
], $data);
|
||||
|
||||
$source = self::create($insertData);
|
||||
|
||||
// 如果是首次来源,更新公司流量详情表
|
||||
if ($isFirstSource) {
|
||||
TrafficPoolCompany::where('id', $poolCompanyId)->update([
|
||||
'firstSourceType' => $sourceType,
|
||||
'firstSourceTime' => time(),
|
||||
'updateTime' => time()
|
||||
]);
|
||||
}
|
||||
|
||||
return $source;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取流量的所有来源
|
||||
* @param int $poolCompanyId
|
||||
* @return \think\Collection
|
||||
*/
|
||||
public static function getSourcesByPoolCompany(int $poolCompanyId)
|
||||
{
|
||||
return self::where('poolCompanyId', $poolCompanyId)
|
||||
->order('createTime DESC')
|
||||
->select();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
229
Server/application/common/model/TrafficPoolTag.php
Normal file
229
Server/application/common/model/TrafficPoolTag.php
Normal file
@@ -0,0 +1,229 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
/**
|
||||
* 流量标签关联表模型类
|
||||
* 表名:ck_traffic_pool_tag
|
||||
* 用途:记录流量与标签的关联关系
|
||||
*/
|
||||
class TrafficPoolTag extends Model
|
||||
{
|
||||
// 设置数据表名
|
||||
protected $name = 'traffic_pool_tag';
|
||||
|
||||
// 主键
|
||||
protected $pk = 'id';
|
||||
|
||||
// 自动写入时间戳
|
||||
protected $autoWriteTimestamp = true;
|
||||
protected $createTime = 'createTime';
|
||||
protected $updateTime = 'updateTime';
|
||||
|
||||
// 标签类型常量
|
||||
const TAG_TYPE_WECHAT = 1; // 微信标签
|
||||
const TAG_TYPE_SITE = 2; // 站内标签
|
||||
const TAG_TYPE_AI = 3; // AI标签
|
||||
|
||||
// 打标来源常量
|
||||
const SOURCE_MANUAL = 1; // 手动
|
||||
const SOURCE_RULE = 2; // 规则自动
|
||||
const SOURCE_AI = 3; // AI自动
|
||||
const SOURCE_WECHAT_SYNC = 4; // 微信同步
|
||||
|
||||
// 打标来源名称
|
||||
const SOURCE_NAMES = [
|
||||
self::SOURCE_MANUAL => '手动打标',
|
||||
self::SOURCE_RULE => '规则自动',
|
||||
self::SOURCE_AI => 'AI自动',
|
||||
self::SOURCE_WECHAT_SYNC => '微信同步',
|
||||
];
|
||||
|
||||
/**
|
||||
* 关联公司流量详情
|
||||
*/
|
||||
public function poolCompany()
|
||||
{
|
||||
return $this->belongsTo(TrafficPoolCompany::class, 'poolCompanyId', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联标签定义
|
||||
*/
|
||||
public function tagDefine()
|
||||
{
|
||||
return $this->belongsTo(TrafficPoolTagDefine::class, 'tagDefineId', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 为流量添加标签
|
||||
* @param int $poolCompanyId
|
||||
* @param string $identifier
|
||||
* @param int $companyId
|
||||
* @param int $tagDefineId
|
||||
* @param int $source
|
||||
* @param int $operatorId
|
||||
* @param string $tagValue
|
||||
* @param float $score AI置信度
|
||||
* @return static|null
|
||||
*/
|
||||
public static function addTag(
|
||||
int $poolCompanyId,
|
||||
string $identifier,
|
||||
int $companyId,
|
||||
int $tagDefineId,
|
||||
int $source = self::SOURCE_MANUAL,
|
||||
int $operatorId = null,
|
||||
string $tagValue = null,
|
||||
float $score = null
|
||||
) {
|
||||
// 检查标签定义是否存在
|
||||
$tagDefine = TrafficPoolTagDefine::find($tagDefineId);
|
||||
if (!$tagDefine) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查是否已存在
|
||||
$existTag = self::where('poolCompanyId', $poolCompanyId)
|
||||
->where('tagDefineId', $tagDefineId)
|
||||
->where('isDel', 0)
|
||||
->find();
|
||||
|
||||
if ($existTag) {
|
||||
// 更新现有标签
|
||||
$existTag->save([
|
||||
'tagValue' => $tagValue,
|
||||
'source' => $source,
|
||||
'operatorId' => $operatorId,
|
||||
'score' => $score,
|
||||
'updateTime' => time()
|
||||
]);
|
||||
return $existTag;
|
||||
}
|
||||
|
||||
// 如果是互斥标签,先删除同类目下的其他标签
|
||||
if ($tagDefine->isExclusive) {
|
||||
self::where('poolCompanyId', $poolCompanyId)
|
||||
->where('categoryId', $tagDefine->categoryId)
|
||||
->where('isDel', 0)
|
||||
->update([
|
||||
'isDel' => 1,
|
||||
'deleteTime' => time()
|
||||
]);
|
||||
}
|
||||
|
||||
// 创建新标签关联
|
||||
$tag = self::create([
|
||||
'poolCompanyId' => $poolCompanyId,
|
||||
'identifier' => $identifier,
|
||||
'companyId' => $companyId,
|
||||
'tagDefineId' => $tagDefineId,
|
||||
'tagType' => $tagDefine->tagType,
|
||||
'categoryId' => $tagDefine->categoryId,
|
||||
'tagName' => $tagDefine->tagName,
|
||||
'tagValue' => $tagValue,
|
||||
'source' => $source,
|
||||
'operatorId' => $operatorId,
|
||||
'score' => $score,
|
||||
'createTime' => time()
|
||||
]);
|
||||
|
||||
// 增加标签使用次数
|
||||
$tagDefine->incrementUseCount();
|
||||
|
||||
return $tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除流量标签
|
||||
* @param int $poolCompanyId
|
||||
* @param int $tagDefineId
|
||||
* @return bool
|
||||
*/
|
||||
public static function removeTag(int $poolCompanyId, int $tagDefineId)
|
||||
{
|
||||
$tag = self::where('poolCompanyId', $poolCompanyId)
|
||||
->where('tagDefineId', $tagDefineId)
|
||||
->where('isDel', 0)
|
||||
->find();
|
||||
|
||||
if ($tag) {
|
||||
$tag->save([
|
||||
'isDel' => 1,
|
||||
'deleteTime' => time()
|
||||
]);
|
||||
|
||||
// 减少标签使用次数
|
||||
$tagDefine = TrafficPoolTagDefine::find($tagDefineId);
|
||||
if ($tagDefine) {
|
||||
$tagDefine->decrementUseCount();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取流量的所有标签
|
||||
* @param int $poolCompanyId
|
||||
* @param int|null $tagType
|
||||
* @return \think\Collection
|
||||
*/
|
||||
public static function getTagsByPoolCompany(int $poolCompanyId, int $tagType = null)
|
||||
{
|
||||
$query = self::where('poolCompanyId', $poolCompanyId)
|
||||
->where('isDel', 0);
|
||||
|
||||
if ($tagType !== null) {
|
||||
$query->where('tagType', $tagType);
|
||||
}
|
||||
|
||||
return $query->order('createTime DESC')->select();
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步微信标签
|
||||
* @param int $poolCompanyId
|
||||
* @param string $identifier
|
||||
* @param int $companyId
|
||||
* @param array $wechatLabels 微信标签名称数组
|
||||
* @return int 同步的标签数量
|
||||
*/
|
||||
public static function syncWechatTags(int $poolCompanyId, string $identifier, int $companyId, array $wechatLabels)
|
||||
{
|
||||
$count = 0;
|
||||
|
||||
// 获取微信默认标签类目(假设ID为1)
|
||||
$wechatCategoryId = 1;
|
||||
|
||||
foreach ($wechatLabels as $labelName) {
|
||||
if (empty($labelName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取或创建标签定义
|
||||
$tagDefine = TrafficPoolTagDefine::getOrCreateByName($labelName, $companyId, $wechatCategoryId);
|
||||
|
||||
// 添加标签关联
|
||||
$tag = self::addTag(
|
||||
$poolCompanyId,
|
||||
$identifier,
|
||||
$companyId,
|
||||
$tagDefine->id,
|
||||
self::SOURCE_WECHAT_SYNC
|
||||
);
|
||||
|
||||
if ($tag) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
118
Server/application/common/model/TrafficPoolTagCategory.php
Normal file
118
Server/application/common/model/TrafficPoolTagCategory.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
/**
|
||||
* 标签类目表模型类
|
||||
* 表名:ck_traffic_pool_tag_category
|
||||
* 用途:管理标签的类目/分组,支持多级分类
|
||||
*/
|
||||
class TrafficPoolTagCategory extends Model
|
||||
{
|
||||
// 设置数据表名
|
||||
protected $name = 'traffic_pool_tag_category';
|
||||
|
||||
// 主键
|
||||
protected $pk = 'id';
|
||||
|
||||
// 自动写入时间戳
|
||||
protected $autoWriteTimestamp = true;
|
||||
protected $createTime = 'createTime';
|
||||
protected $updateTime = 'updateTime';
|
||||
|
||||
// 标签类型常量
|
||||
const TAG_TYPE_WECHAT = 1; // 微信标签
|
||||
const TAG_TYPE_SITE = 2; // 站内标签
|
||||
const TAG_TYPE_AI = 3; // AI标签
|
||||
|
||||
// 标签类型名称映射
|
||||
const TAG_TYPE_NAMES = [
|
||||
self::TAG_TYPE_WECHAT => '微信标签',
|
||||
self::TAG_TYPE_SITE => '站内标签',
|
||||
self::TAG_TYPE_AI => 'AI标签',
|
||||
];
|
||||
|
||||
// 状态常量
|
||||
const STATUS_DISABLED = 0;
|
||||
const STATUS_ENABLED = 1;
|
||||
|
||||
/**
|
||||
* 关联标签定义
|
||||
*/
|
||||
public function tagDefines()
|
||||
{
|
||||
return $this->hasMany(TrafficPoolTagDefine::class, 'categoryId', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联子类目
|
||||
*/
|
||||
public function children()
|
||||
{
|
||||
return $this->hasMany(self::class, 'parentId', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联父类目
|
||||
*/
|
||||
public function parent()
|
||||
{
|
||||
return $this->belongsTo(self::class, 'parentId', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取公司可用的标签类目
|
||||
* @param int $companyId
|
||||
* @param int|null $tagType
|
||||
* @return \think\Collection
|
||||
*/
|
||||
public static function getCategoriesByCompany(int $companyId, int $tagType = null)
|
||||
{
|
||||
$query = self::whereIn('companyId', [0, $companyId])
|
||||
->where('isDel', 0)
|
||||
->where('status', self::STATUS_ENABLED);
|
||||
|
||||
if ($tagType !== null) {
|
||||
$query->where('tagType', $tagType);
|
||||
}
|
||||
|
||||
return $query->order('sort ASC, id ASC')->select();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取类目树结构
|
||||
* @param int $companyId
|
||||
* @param int|null $tagType
|
||||
* @return array
|
||||
*/
|
||||
public static function getCategoryTree(int $companyId, int $tagType = null)
|
||||
{
|
||||
$categories = self::getCategoriesByCompany($companyId, $tagType)->toArray();
|
||||
return self::buildTree($categories);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建树结构
|
||||
* @param array $items
|
||||
* @param int $parentId
|
||||
* @return array
|
||||
*/
|
||||
private static function buildTree(array $items, int $parentId = 0)
|
||||
{
|
||||
$result = [];
|
||||
foreach ($items as $item) {
|
||||
if ($item['parentId'] == $parentId) {
|
||||
$children = self::buildTree($items, $item['id']);
|
||||
if (!empty($children)) {
|
||||
$item['children'] = $children;
|
||||
}
|
||||
$result[] = $item;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
143
Server/application/common/model/TrafficPoolTagDefine.php
Normal file
143
Server/application/common/model/TrafficPoolTagDefine.php
Normal file
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
/**
|
||||
* 标签定义表模型类
|
||||
* 表名:ck_traffic_pool_tag_define
|
||||
* 用途:定义具体的标签
|
||||
*/
|
||||
class TrafficPoolTagDefine extends Model
|
||||
{
|
||||
// 设置数据表名
|
||||
protected $name = 'traffic_pool_tag_define';
|
||||
|
||||
// 主键
|
||||
protected $pk = 'id';
|
||||
|
||||
// 自动写入时间戳
|
||||
protected $autoWriteTimestamp = true;
|
||||
protected $createTime = 'createTime';
|
||||
protected $updateTime = 'updateTime';
|
||||
|
||||
// 标签类型常量(与类目表一致)
|
||||
const TAG_TYPE_WECHAT = 1; // 微信标签
|
||||
const TAG_TYPE_SITE = 2; // 站内标签
|
||||
const TAG_TYPE_AI = 3; // AI标签
|
||||
|
||||
// 状态常量
|
||||
const STATUS_DISABLED = 0;
|
||||
const STATUS_ENABLED = 1;
|
||||
|
||||
/**
|
||||
* 关联类目
|
||||
*/
|
||||
public function category()
|
||||
{
|
||||
return $this->belongsTo(TrafficPoolTagCategory::class, 'categoryId', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联标签使用记录
|
||||
*/
|
||||
public function tags()
|
||||
{
|
||||
return $this->hasMany(TrafficPoolTag::class, 'tagDefineId', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取公司可用的标签定义
|
||||
* @param int $companyId
|
||||
* @param int|null $tagType
|
||||
* @param int|null $categoryId
|
||||
* @return \think\Collection
|
||||
*/
|
||||
public static function getTagDefinesByCompany(int $companyId, int $tagType = null, int $categoryId = null)
|
||||
{
|
||||
$query = self::whereIn('companyId', [0, $companyId])
|
||||
->where('isDel', 0)
|
||||
->where('status', self::STATUS_ENABLED);
|
||||
|
||||
if ($tagType !== null) {
|
||||
$query->where('tagType', $tagType);
|
||||
}
|
||||
|
||||
if ($categoryId !== null) {
|
||||
$query->where('categoryId', $categoryId);
|
||||
}
|
||||
|
||||
return $query->order('sort ASC, id ASC')->select();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据标签编码获取标签定义
|
||||
* @param string $tagCode
|
||||
* @param int $companyId
|
||||
* @return static|null
|
||||
*/
|
||||
public static function getByCode(string $tagCode, int $companyId = 0)
|
||||
{
|
||||
return self::where('tagCode', $tagCode)
|
||||
->whereIn('companyId', [0, $companyId])
|
||||
->where('isDel', 0)
|
||||
->find();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据标签名称获取或创建标签(用于微信标签同步)
|
||||
* @param string $tagName
|
||||
* @param int $companyId
|
||||
* @param int $categoryId
|
||||
* @return static
|
||||
*/
|
||||
public static function getOrCreateByName(string $tagName, int $companyId, int $categoryId = 1)
|
||||
{
|
||||
$tagCode = 'wechat_' . md5($tagName . '_' . $companyId);
|
||||
|
||||
$tag = self::where('tagCode', $tagCode)
|
||||
->where('companyId', $companyId)
|
||||
->where('isDel', 0)
|
||||
->find();
|
||||
|
||||
if (!$tag) {
|
||||
$tag = self::create([
|
||||
'companyId' => $companyId,
|
||||
'categoryId' => $categoryId,
|
||||
'tagType' => self::TAG_TYPE_WECHAT,
|
||||
'tagCode' => $tagCode,
|
||||
'tagName' => $tagName,
|
||||
'isSystem' => 0,
|
||||
'syncFromWechat' => 1,
|
||||
'status' => self::STATUS_ENABLED,
|
||||
'createTime' => time()
|
||||
]);
|
||||
}
|
||||
|
||||
return $tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加使用次数
|
||||
* @return bool
|
||||
*/
|
||||
public function incrementUseCount()
|
||||
{
|
||||
return $this->setInc('useCount');
|
||||
}
|
||||
|
||||
/**
|
||||
* 减少使用次数
|
||||
* @return bool
|
||||
*/
|
||||
public function decrementUseCount()
|
||||
{
|
||||
if ($this->useCount > 0) {
|
||||
return $this->setDec('useCount');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
85
Server/application/common/model/TrafficPoolV2.php
Normal file
85
Server/application/common/model/TrafficPoolV2.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
/**
|
||||
* 流量池总表模型类(V2版本)
|
||||
* 表名:ck_traffic_pool
|
||||
* 用途:存储全局唯一的流量标识,不区分公司
|
||||
*/
|
||||
class TrafficPoolV2 extends Model
|
||||
{
|
||||
// 设置数据表名(不带前缀)
|
||||
protected $name = 'traffic_pool';
|
||||
|
||||
// 主键
|
||||
protected $pk = 'id';
|
||||
|
||||
// 自动写入时间戳
|
||||
protected $autoWriteTimestamp = true;
|
||||
protected $createTime = 'createTime';
|
||||
protected $updateTime = 'updateTime';
|
||||
|
||||
// 标识类型常量
|
||||
const IDENTIFIER_TYPE_WECHAT_ID = 1; // 微信ID
|
||||
const IDENTIFIER_TYPE_WECHAT_ALIAS = 2; // 微信号
|
||||
const IDENTIFIER_TYPE_MOBILE = 3; // 手机号
|
||||
|
||||
// 性别常量
|
||||
const GENDER_UNKNOWN = 0;
|
||||
const GENDER_MALE = 1;
|
||||
const GENDER_FEMALE = 2;
|
||||
|
||||
/**
|
||||
* 关联公司流量详情
|
||||
*/
|
||||
public function companies()
|
||||
{
|
||||
return $this->hasMany(TrafficPoolCompany::class, 'poolId', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据identifier查找或创建流量记录
|
||||
* @param string $identifier 唯一标识
|
||||
* @param array $data 额外数据
|
||||
* @return static
|
||||
*/
|
||||
public static function findOrCreateByIdentifier(string $identifier, array $data = [])
|
||||
{
|
||||
$record = self::where('identifier', $identifier)->find();
|
||||
if (!$record) {
|
||||
$insertData = array_merge([
|
||||
'identifier' => $identifier,
|
||||
'identifierType' => self::IDENTIFIER_TYPE_WECHAT_ID,
|
||||
'createTime' => time(),
|
||||
], $data);
|
||||
|
||||
// 如果identifier是微信ID,同时设置wechatId
|
||||
if (empty($insertData['wechatId']) && $insertData['identifierType'] == self::IDENTIFIER_TYPE_WECHAT_ID) {
|
||||
$insertData['wechatId'] = $identifier;
|
||||
}
|
||||
|
||||
$record = self::create($insertData);
|
||||
}
|
||||
return $record;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新基础信息
|
||||
* @param array $data
|
||||
* @return bool
|
||||
*/
|
||||
public function updateBasicInfo(array $data)
|
||||
{
|
||||
$allowFields = ['nickname', 'avatar', 'gender', 'region', 'country', 'province', 'city', 'signature', 'wechatAlias', 'mobile'];
|
||||
$updateData = array_intersect_key($data, array_flip($allowFields));
|
||||
$updateData['updateTime'] = time();
|
||||
$updateData['lastSeenTime'] = time();
|
||||
|
||||
return $this->save($updateData);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
275
Server/application/common/service/TagEngineService.php
Normal file
275
Server/application/common/service/TagEngineService.php
Normal file
@@ -0,0 +1,275 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use think\facade\Log;
|
||||
|
||||
/**
|
||||
* 标签引擎服务类
|
||||
* 对接外部标签系统API
|
||||
*/
|
||||
class TagEngineService
|
||||
{
|
||||
/**
|
||||
* API基础URL
|
||||
* @var string
|
||||
*/
|
||||
private $baseUrl = 'http://192.168.1.134:3000';
|
||||
|
||||
/**
|
||||
* API Key
|
||||
* @var string
|
||||
*/
|
||||
private $apiKey = '69aebe46b03d334f1796ef88808d3042d5851d0fd91d728bcba6ad6be436acf6';
|
||||
|
||||
/**
|
||||
* 设置API基础URL
|
||||
* @param string $url
|
||||
* @return $this
|
||||
*/
|
||||
public function setBaseUrl($url)
|
||||
{
|
||||
$this->baseUrl = rtrim($url, '/');
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置API Key
|
||||
* @param string $key
|
||||
* @return $this
|
||||
*/
|
||||
public function setApiKey($key)
|
||||
{
|
||||
$this->apiKey = $key;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建请求头
|
||||
* @return array
|
||||
*/
|
||||
private function buildHeaders()
|
||||
{
|
||||
return [
|
||||
'Authorization: Bearer ' . $this->apiKey,
|
||||
'Content-Type: application/json'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过标识查询标签
|
||||
*
|
||||
* @param array $identifiers 用户标识列表,最多100个
|
||||
* 格式:[['type' => 'phone', 'value' => '13800138000'], ...]
|
||||
* @param array $options 查询选项
|
||||
* - include_tags: array 包含指定标签(标签代码列表)
|
||||
* - exclude_tags: array 排除指定标签(标签代码列表)
|
||||
* - tag_category: string 按分类筛选标签
|
||||
* - mask_identifier: bool 是否脱敏标识信息,默认 true
|
||||
* @return array|false
|
||||
*/
|
||||
public function queryByIdentifiers($identifiers, $options = [])
|
||||
{
|
||||
try {
|
||||
// 参数验证
|
||||
if (empty($identifiers) || !is_array($identifiers)) {
|
||||
Log::error('标签引擎:标识列表不能为空');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (count($identifiers) > 100) {
|
||||
Log::error('标签引擎:单次最多查询100个标识');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 构建请求数据
|
||||
$data = [
|
||||
'identifiers' => $identifiers,
|
||||
];
|
||||
|
||||
if (!empty($options)) {
|
||||
$data['options'] = $options;
|
||||
}
|
||||
|
||||
// 发起请求
|
||||
$url = $this->baseUrl . '/api/v1/tag/query-by-identifiers';
|
||||
$response = requestCurl($url, $data, 'POST', $this->buildHeaders(), 'json');
|
||||
|
||||
// 处理响应
|
||||
$result = handleApiResponse($response);
|
||||
|
||||
// 记录日志
|
||||
Log::info('标签引擎-通过标识查询标签', [
|
||||
'identifiers_count' => count($identifiers),
|
||||
'response' => $result
|
||||
]);
|
||||
|
||||
return $result;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('标签引擎-通过标识查询标签异常:' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过标签查询用户
|
||||
*
|
||||
* @param array $tagConditions 标签条件列表,最多10个条件
|
||||
* 格式:[
|
||||
* ['tag_code' => 'user.trade.total_amount', 'operator' => '>=', 'value' => '5000'],
|
||||
* ...
|
||||
* ]
|
||||
* 支持的操作符:=, !=, >, >=, <, <=, in, not_in
|
||||
* @param string $logic 逻辑关系:AND(默认)或 OR
|
||||
* @param bool $includeSensitive 是否返回敏感信息(QQ号、身份证),默认 false
|
||||
* @param int $page 页码,默认 1
|
||||
* @param int $pageSize 每页数量,默认 20,最大 100
|
||||
* @return array|false
|
||||
*/
|
||||
public function queryUsersByTags($tagConditions, $logic = 'AND', $includeSensitive = false, $page = 1, $pageSize = 20)
|
||||
{
|
||||
try {
|
||||
// 参数验证
|
||||
if (empty($tagConditions) || !is_array($tagConditions)) {
|
||||
Log::error('标签引擎:标签条件不能为空');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (count($tagConditions) > 10) {
|
||||
Log::error('标签引擎:单次最多10个标签条件');
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($pageSize > 100) {
|
||||
Log::error('标签引擎:单页最多返回100条记录');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 构建请求数据
|
||||
$data = [
|
||||
'tag_conditions' => $tagConditions,
|
||||
'logic' => strtoupper($logic),
|
||||
'include_sensitive' => $includeSensitive,
|
||||
'page' => max(1, intval($page)),
|
||||
'page_size' => min(100, max(1, intval($pageSize)))
|
||||
];
|
||||
|
||||
// 发起请求
|
||||
$url = $this->baseUrl . '/api/v1/tag/query-users-by-tags';
|
||||
$response = requestCurl($url, $data, 'POST', $this->buildHeaders(), 'json');
|
||||
|
||||
// 处理响应
|
||||
$result = handleApiResponse($response);
|
||||
|
||||
// 记录日志
|
||||
Log::info('标签引擎-通过标签查询用户', [
|
||||
'conditions_count' => count($tagConditions),
|
||||
'page' => $page,
|
||||
'page_size' => $pageSize,
|
||||
'response' => $result
|
||||
]);
|
||||
|
||||
return $result;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('标签引擎-通过标签查询用户异常:' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部调用 - 通过手机号查询标签
|
||||
*
|
||||
* @param string|array $phones 手机号或手机号数组
|
||||
* @param array $options 查询选项
|
||||
* @return array|false
|
||||
*/
|
||||
public function queryByPhone($phones, $options = [])
|
||||
{
|
||||
if (!is_array($phones)) {
|
||||
$phones = [$phones];
|
||||
}
|
||||
|
||||
$identifiers = [];
|
||||
foreach ($phones as $phone) {
|
||||
$identifiers[] = [
|
||||
'type' => 'phone',
|
||||
'value' => $phone
|
||||
];
|
||||
}
|
||||
|
||||
return $this->queryByIdentifiers($identifiers, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部调用 - 通过微信号查询标签
|
||||
*
|
||||
* @param string|array $wechats 微信号或微信号数组
|
||||
* @param array $options 查询选项
|
||||
* @return array|false
|
||||
*/
|
||||
public function queryByWechat($wechats, $options = [])
|
||||
{
|
||||
if (!is_array($wechats)) {
|
||||
$wechats = [$wechats];
|
||||
}
|
||||
|
||||
$identifiers = [];
|
||||
foreach ($wechats as $wechat) {
|
||||
$identifiers[] = [
|
||||
'type' => 'wechat',
|
||||
'value' => $wechat
|
||||
];
|
||||
}
|
||||
|
||||
return $this->queryByIdentifiers($identifiers, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部调用 - 通过身份证号查询标签
|
||||
*
|
||||
* @param string|array $idCards 身份证号或身份证号数组
|
||||
* @param array $options 查询选项
|
||||
* @return array|false
|
||||
*/
|
||||
public function queryByIdCard($idCards, $options = [])
|
||||
{
|
||||
if (!is_array($idCards)) {
|
||||
$idCards = [$idCards];
|
||||
}
|
||||
|
||||
$identifiers = [];
|
||||
foreach ($idCards as $idCard) {
|
||||
$identifiers[] = [
|
||||
'type' => 'id_card',
|
||||
'value' => $idCard
|
||||
];
|
||||
}
|
||||
|
||||
return $this->queryByIdentifiers($identifiers, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部调用 - 通过QQ号查询标签
|
||||
*
|
||||
* @param string|array $qqs QQ号或QQ号数组
|
||||
* @param array $options 查询选项
|
||||
* @return array|false
|
||||
*/
|
||||
public function queryByQQ($qqs, $options = [])
|
||||
{
|
||||
if (!is_array($qqs)) {
|
||||
$qqs = [$qqs];
|
||||
}
|
||||
|
||||
$identifiers = [];
|
||||
foreach ($qqs as $qq) {
|
||||
$identifiers[] = [
|
||||
'type' => 'qq',
|
||||
'value' => $qq
|
||||
];
|
||||
}
|
||||
|
||||
return $this->queryByIdentifiers($identifiers, $options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ Route::group('v1/', function () {
|
||||
Route::get('getUserList', 'app\cunkebao\controller\plan\PlanSceneV1Controller@getUserList');
|
||||
});
|
||||
|
||||
// 流量池相关
|
||||
// 流量池相关(V1 旧版接口,保持兼容)
|
||||
Route::group('traffic/pool', function () {
|
||||
Route::get('getPackage', 'app\cunkebao\controller\TrafficController@getPackage'); // 获取流量池包列表
|
||||
Route::get('getPackageDetail', 'app\cunkebao\controller\TrafficController@getPackageDetail'); // 获取流量池详情(元数据)
|
||||
@@ -71,25 +71,46 @@ Route::group('v1/', function () {
|
||||
|
||||
Route::get('user-list', 'app\cunkebao\controller\TrafficController@getTrafficPoolList'); // 获取流量池用户列表(数据列表)
|
||||
|
||||
|
||||
|
||||
|
||||
//Route::get('', 'app\cunkebao\controller\traffic\GetPotentialListWithInCompanyV1Controller@index');
|
||||
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::post('addPackage', 'app\cunkebao\controller\traffic\GetPotentialListWithInCompanyV1Controller@addPackage');
|
||||
|
||||
|
||||
|
||||
|
||||
Route::get('converted', 'app\cunkebao\controller\traffic\GetConvertedListWithInCompanyV1Controller@index');
|
||||
Route::get('types', 'app\cunkebao\controller\traffic\GetPotentialTypeSectionV1Controller@index');
|
||||
Route::get('sources', 'app\cunkebao\controller\traffic\GetTrafficSourceSectionV1Controller@index');
|
||||
Route::get('statistics', 'app\cunkebao\controller\traffic\GetPoolStatisticsV1Controller@index');
|
||||
});
|
||||
|
||||
// 流量池 V2 新版接口
|
||||
Route::group('traffic/pool/v2', function () {
|
||||
// 分组相关
|
||||
Route::get('groups', 'app\cunkebao\controller\TrafficPoolV2Controller@getGroups'); // 获取分组列表
|
||||
Route::get('group/detail', 'app\cunkebao\controller\TrafficPoolV2Controller@getGroupDetail'); // 获取分组详情
|
||||
Route::post('group/create', 'app\cunkebao\controller\TrafficPoolV2Controller@createGroup'); // 创建分组
|
||||
Route::put('group/update', 'app\cunkebao\controller\TrafficPoolV2Controller@updateGroup'); // 更新分组
|
||||
Route::delete('group/delete', 'app\cunkebao\controller\TrafficPoolV2Controller@deleteGroup'); // 删除分组
|
||||
Route::get('group/members', 'app\cunkebao\controller\TrafficPoolV2Controller@getGroupMembers'); // 获取分组成员
|
||||
Route::post('group/add-members', 'app\cunkebao\controller\TrafficPoolV2Controller@addMembersToGroup'); // 添加成员到分组
|
||||
Route::post('group/remove-members', 'app\cunkebao\controller\TrafficPoolV2Controller@removeMembersFromGroup'); // 移除分组成员
|
||||
|
||||
// 流量池成员相关
|
||||
Route::get('list', 'app\cunkebao\controller\TrafficPoolV2Controller@getPoolList'); // 获取流量池列表
|
||||
Route::get('detail', 'app\cunkebao\controller\TrafficPoolV2Controller@getPoolDetail'); // 获取流量详情
|
||||
Route::put('update', 'app\cunkebao\controller\TrafficPoolV2Controller@updatePool'); // 更新流量信息
|
||||
|
||||
// 标签相关
|
||||
Route::get('tag/categories', 'app\cunkebao\controller\TrafficPoolV2Controller@getTagCategories'); // 获取标签类目
|
||||
Route::get('tag/defines', 'app\cunkebao\controller\TrafficPoolV2Controller@getTagDefines'); // 获取标签定义
|
||||
Route::get('tag/pool-tags', 'app\cunkebao\controller\TrafficPoolV2Controller@getPoolTags'); // 获取流量的标签
|
||||
Route::post('tag/add', 'app\cunkebao\controller\TrafficPoolV2Controller@addTag'); // 添加标签
|
||||
Route::delete('tag/remove', 'app\cunkebao\controller\TrafficPoolV2Controller@removeTag'); // 移除标签
|
||||
|
||||
// 分配相关
|
||||
Route::post('allocate', 'app\cunkebao\controller\TrafficPoolV2Controller@allocatePool'); // 分配流量
|
||||
Route::post('recycle', 'app\cunkebao\controller\TrafficPoolV2Controller@recyclePool'); // 回收流量
|
||||
|
||||
// 统计相关
|
||||
Route::get('statistics', 'app\cunkebao\controller\TrafficPoolV2Controller@getStatistics'); // 获取统计数据
|
||||
});
|
||||
|
||||
// 工作台相关
|
||||
|
||||
@@ -0,0 +1,569 @@
|
||||
<?php
|
||||
|
||||
namespace app\cunkebao\controller;
|
||||
|
||||
use app\cunkebao\service\TrafficPoolService;
|
||||
use app\cunkebao\service\TrafficPoolGroupService;
|
||||
use app\common\model\TrafficPoolGroup;
|
||||
use app\common\model\TrafficPoolCompany;
|
||||
use app\common\model\TrafficPoolTag;
|
||||
use app\common\model\TrafficPoolTagCategory;
|
||||
use app\common\model\TrafficPoolTagDefine;
|
||||
use app\common\model\TrafficPoolAllotRecord;
|
||||
use app\common\service\ClassTableService;
|
||||
use library\ResponseHelper;
|
||||
|
||||
/**
|
||||
* 流量池控制器 V2
|
||||
* 基于新架构的流量池 API 接口
|
||||
*/
|
||||
class TrafficPoolV2Controller extends BaseController
|
||||
{
|
||||
/**
|
||||
* @var TrafficPoolService
|
||||
*/
|
||||
protected $poolService;
|
||||
|
||||
/**
|
||||
* @var TrafficPoolGroupService
|
||||
*/
|
||||
protected $groupService;
|
||||
|
||||
public function __construct(ClassTableService $classTable)
|
||||
{
|
||||
parent::__construct($classTable);
|
||||
$this->poolService = new TrafficPoolService();
|
||||
$this->groupService = new TrafficPoolGroupService();
|
||||
}
|
||||
|
||||
// ==================== 分组相关接口 ====================
|
||||
|
||||
/**
|
||||
* 获取流量池分组列表
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function getGroups()
|
||||
{
|
||||
$companyId = $this->getUserInfo('companyId');
|
||||
|
||||
try {
|
||||
$groups = $this->groupService->getGroupList($companyId, true);
|
||||
return ResponseHelper::success($groups);
|
||||
} catch (\Exception $e) {
|
||||
return ResponseHelper::error('获取分组列表失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分组详情
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function getGroupDetail()
|
||||
{
|
||||
$groupId = $this->request->param('groupId', 0, 'intval');
|
||||
$companyId = $this->getUserInfo('companyId');
|
||||
|
||||
if (empty($groupId)) {
|
||||
return ResponseHelper::error('分组ID不能为空');
|
||||
}
|
||||
|
||||
try {
|
||||
$detail = $this->groupService->getGroupDetail($groupId, $companyId);
|
||||
if (!$detail) {
|
||||
return ResponseHelper::error('分组不存在');
|
||||
}
|
||||
return ResponseHelper::success($detail);
|
||||
} catch (\Exception $e) {
|
||||
return ResponseHelper::error('获取分组详情失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建分组
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function createGroup()
|
||||
{
|
||||
$companyId = $this->getUserInfo('companyId');
|
||||
$userId = $this->getUserInfo('id');
|
||||
|
||||
$data = $this->request->param();
|
||||
|
||||
if (empty($data['groupName'])) {
|
||||
return ResponseHelper::error('分组名称不能为空');
|
||||
}
|
||||
|
||||
try {
|
||||
$group = $this->groupService->createGroup($companyId, $data, $userId);
|
||||
return ResponseHelper::success(['id' => $group->id], '创建成功');
|
||||
} catch (\Exception $e) {
|
||||
return ResponseHelper::error('创建分组失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新分组
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function updateGroup()
|
||||
{
|
||||
$groupId = $this->request->param('groupId', 0, 'intval');
|
||||
$companyId = $this->getUserInfo('companyId');
|
||||
|
||||
if (empty($groupId)) {
|
||||
return ResponseHelper::error('分组ID不能为空');
|
||||
}
|
||||
|
||||
$data = $this->request->param();
|
||||
|
||||
try {
|
||||
$this->groupService->updateGroup($groupId, $companyId, $data);
|
||||
return ResponseHelper::success(null, '更新成功');
|
||||
} catch (\Exception $e) {
|
||||
return ResponseHelper::error('更新分组失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除分组
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function deleteGroup()
|
||||
{
|
||||
$groupId = $this->request->param('groupId', 0, 'intval');
|
||||
$companyId = $this->getUserInfo('companyId');
|
||||
|
||||
if (empty($groupId)) {
|
||||
return ResponseHelper::error('分组ID不能为空');
|
||||
}
|
||||
|
||||
try {
|
||||
$this->groupService->deleteGroup($groupId, $companyId);
|
||||
return ResponseHelper::success(null, '删除成功');
|
||||
} catch (\Exception $e) {
|
||||
return ResponseHelper::error('删除分组失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 流量池成员相关接口 ====================
|
||||
|
||||
/**
|
||||
* 获取分组成员列表(用户列表)
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function getGroupMembers()
|
||||
{
|
||||
$groupId = $this->request->param('groupId', 0, 'intval');
|
||||
$companyId = $this->getUserInfo('companyId');
|
||||
$page = $this->request->param('page', 1, 'intval');
|
||||
$pageSize = $this->request->param('pageSize', 10, 'intval');
|
||||
$keyword = $this->request->param('keyword', '');
|
||||
|
||||
if (empty($groupId)) {
|
||||
return ResponseHelper::error('分组ID不能为空');
|
||||
}
|
||||
|
||||
$filters = [
|
||||
'keyword' => $keyword
|
||||
];
|
||||
|
||||
try {
|
||||
$result = $this->groupService->getGroupMembers($groupId, $companyId, $page, $pageSize, $filters);
|
||||
return ResponseHelper::success($result);
|
||||
} catch (\Exception $e) {
|
||||
return ResponseHelper::error('获取成员列表失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取流量池列表(全量,带筛选)
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function getPoolList()
|
||||
{
|
||||
$companyId = $this->getUserInfo('companyId');
|
||||
$page = $this->request->param('page', 1, 'intval');
|
||||
$pageSize = $this->request->param('pageSize', 10, 'intval');
|
||||
|
||||
$filters = [
|
||||
'keyword' => $this->request->param('keyword', ''),
|
||||
'friendStatus' => $this->request->param('friendStatus'),
|
||||
'level' => $this->request->param('level'),
|
||||
'lifecycle' => $this->request->param('lifecycle'),
|
||||
'allocateStatus' => $this->request->param('allocateStatus'),
|
||||
'ownerWechatId' => $this->request->param('ownerWechatId', ''),
|
||||
'rfmMMin' => $this->request->param('rfmMMin'),
|
||||
'rfmMMax' => $this->request->param('rfmMMax'),
|
||||
];
|
||||
|
||||
// 移除空值
|
||||
$filters = array_filter($filters, function($v) {
|
||||
return $v !== null && $v !== '';
|
||||
});
|
||||
|
||||
try {
|
||||
$result = $this->poolService->getPoolList($companyId, $page, $pageSize, $filters);
|
||||
return ResponseHelper::success($result);
|
||||
} catch (\Exception $e) {
|
||||
return ResponseHelper::error('获取流量池列表失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取流量详情
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function getPoolDetail()
|
||||
{
|
||||
$poolCompanyId = $this->request->param('id', 0, 'intval');
|
||||
$companyId = $this->getUserInfo('companyId');
|
||||
|
||||
if (empty($poolCompanyId)) {
|
||||
return ResponseHelper::error('流量ID不能为空');
|
||||
}
|
||||
|
||||
try {
|
||||
$detail = $this->poolService->getPoolDetail($poolCompanyId, $companyId);
|
||||
if (!$detail) {
|
||||
return ResponseHelper::error('流量不存在');
|
||||
}
|
||||
return ResponseHelper::success($detail);
|
||||
} catch (\Exception $e) {
|
||||
return ResponseHelper::error('获取流量详情失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新流量信息
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function updatePool()
|
||||
{
|
||||
$poolCompanyId = $this->request->param('id', 0, 'intval');
|
||||
$companyId = $this->getUserInfo('companyId');
|
||||
|
||||
if (empty($poolCompanyId)) {
|
||||
return ResponseHelper::error('流量ID不能为空');
|
||||
}
|
||||
|
||||
$data = $this->request->param();
|
||||
|
||||
try {
|
||||
$this->poolService->updatePool($poolCompanyId, $companyId, $data);
|
||||
return ResponseHelper::success(null, '更新成功');
|
||||
} catch (\Exception $e) {
|
||||
return ResponseHelper::error('更新失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加成员到分组(手动分组)
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function addMembersToGroup()
|
||||
{
|
||||
$groupId = $this->request->param('groupId', 0, 'intval');
|
||||
$poolCompanyIds = $this->request->param('poolCompanyIds/a', []);
|
||||
$companyId = $this->getUserInfo('companyId');
|
||||
$userId = $this->getUserInfo('id');
|
||||
|
||||
if (empty($groupId)) {
|
||||
return ResponseHelper::error('分组ID不能为空');
|
||||
}
|
||||
|
||||
if (empty($poolCompanyIds)) {
|
||||
return ResponseHelper::error('请选择要添加的成员');
|
||||
}
|
||||
|
||||
try {
|
||||
$count = $this->groupService->addMembers($groupId, $poolCompanyIds, $companyId, $userId);
|
||||
return ResponseHelper::success(['count' => $count], "成功添加 {$count} 个成员");
|
||||
} catch (\Exception $e) {
|
||||
return ResponseHelper::error('添加失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从分组移除成员
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function removeMembersFromGroup()
|
||||
{
|
||||
$groupId = $this->request->param('groupId', 0, 'intval');
|
||||
$poolCompanyIds = $this->request->param('poolCompanyIds/a', []);
|
||||
$companyId = $this->getUserInfo('companyId');
|
||||
|
||||
if (empty($groupId)) {
|
||||
return ResponseHelper::error('分组ID不能为空');
|
||||
}
|
||||
|
||||
if (empty($poolCompanyIds)) {
|
||||
return ResponseHelper::error('请选择要移除的成员');
|
||||
}
|
||||
|
||||
try {
|
||||
$count = $this->groupService->removeMembers($groupId, $poolCompanyIds, $companyId);
|
||||
return ResponseHelper::success(['count' => $count], "成功移除 {$count} 个成员");
|
||||
} catch (\Exception $e) {
|
||||
return ResponseHelper::error('移除失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 标签相关接口 ====================
|
||||
|
||||
/**
|
||||
* 获取标签类目列表
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function getTagCategories()
|
||||
{
|
||||
$companyId = $this->getUserInfo('companyId');
|
||||
$tagType = $this->request->param('tagType');
|
||||
|
||||
try {
|
||||
$categories = TrafficPoolTagCategory::getCategoryTree($companyId, $tagType);
|
||||
return ResponseHelper::success($categories);
|
||||
} catch (\Exception $e) {
|
||||
return ResponseHelper::error('获取标签类目失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取标签定义列表
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function getTagDefines()
|
||||
{
|
||||
$companyId = $this->getUserInfo('companyId');
|
||||
$tagType = $this->request->param('tagType');
|
||||
$categoryId = $this->request->param('categoryId');
|
||||
|
||||
try {
|
||||
$defines = TrafficPoolTagDefine::getTagDefinesByCompany($companyId, $tagType, $categoryId);
|
||||
return ResponseHelper::success($defines);
|
||||
} catch (\Exception $e) {
|
||||
return ResponseHelper::error('获取标签定义失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 为流量添加标签
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function addTag()
|
||||
{
|
||||
$poolCompanyId = $this->request->param('poolCompanyId', 0, 'intval');
|
||||
$tagDefineId = $this->request->param('tagDefineId', 0, 'intval');
|
||||
$tagValue = $this->request->param('tagValue', '');
|
||||
$companyId = $this->getUserInfo('companyId');
|
||||
$userId = $this->getUserInfo('id');
|
||||
|
||||
if (empty($poolCompanyId) || empty($tagDefineId)) {
|
||||
return ResponseHelper::error('参数不完整');
|
||||
}
|
||||
|
||||
// 验证流量归属
|
||||
$poolCompany = TrafficPoolCompany::where('id', $poolCompanyId)
|
||||
->where('companyId', $companyId)
|
||||
->where('isDel', 0)
|
||||
->find();
|
||||
|
||||
if (!$poolCompany) {
|
||||
return ResponseHelper::error('流量不存在');
|
||||
}
|
||||
|
||||
try {
|
||||
$tag = TrafficPoolTag::addTag(
|
||||
$poolCompanyId,
|
||||
$poolCompany->identifier,
|
||||
$companyId,
|
||||
$tagDefineId,
|
||||
TrafficPoolTag::SOURCE_MANUAL,
|
||||
$userId,
|
||||
$tagValue
|
||||
);
|
||||
|
||||
if ($tag) {
|
||||
return ResponseHelper::success(['id' => $tag->id], '添加成功');
|
||||
} else {
|
||||
return ResponseHelper::error('添加失败');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return ResponseHelper::error('添加标签失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除流量标签
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function removeTag()
|
||||
{
|
||||
$poolCompanyId = $this->request->param('poolCompanyId', 0, 'intval');
|
||||
$tagDefineId = $this->request->param('tagDefineId', 0, 'intval');
|
||||
$companyId = $this->getUserInfo('companyId');
|
||||
|
||||
if (empty($poolCompanyId) || empty($tagDefineId)) {
|
||||
return ResponseHelper::error('参数不完整');
|
||||
}
|
||||
|
||||
// 验证流量归属
|
||||
$poolCompany = TrafficPoolCompany::where('id', $poolCompanyId)
|
||||
->where('companyId', $companyId)
|
||||
->where('isDel', 0)
|
||||
->find();
|
||||
|
||||
if (!$poolCompany) {
|
||||
return ResponseHelper::error('流量不存在');
|
||||
}
|
||||
|
||||
try {
|
||||
$result = TrafficPoolTag::removeTag($poolCompanyId, $tagDefineId);
|
||||
if ($result) {
|
||||
return ResponseHelper::success(null, '移除成功');
|
||||
} else {
|
||||
return ResponseHelper::error('标签不存在');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return ResponseHelper::error('移除标签失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取流量的标签
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function getPoolTags()
|
||||
{
|
||||
$poolCompanyId = $this->request->param('poolCompanyId', 0, 'intval');
|
||||
$tagType = $this->request->param('tagType');
|
||||
$companyId = $this->getUserInfo('companyId');
|
||||
|
||||
if (empty($poolCompanyId)) {
|
||||
return ResponseHelper::error('流量ID不能为空');
|
||||
}
|
||||
|
||||
// 验证流量归属
|
||||
$poolCompany = TrafficPoolCompany::where('id', $poolCompanyId)
|
||||
->where('companyId', $companyId)
|
||||
->where('isDel', 0)
|
||||
->find();
|
||||
|
||||
if (!$poolCompany) {
|
||||
return ResponseHelper::error('流量不存在');
|
||||
}
|
||||
|
||||
try {
|
||||
$tags = TrafficPoolTag::getTagsByPoolCompany($poolCompanyId, $tagType);
|
||||
return ResponseHelper::success($tags);
|
||||
} catch (\Exception $e) {
|
||||
return ResponseHelper::error('获取标签失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 分配相关接口 ====================
|
||||
|
||||
/**
|
||||
* 分配流量给客服
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function allocatePool()
|
||||
{
|
||||
$poolCompanyId = $this->request->param('poolCompanyId', 0, 'intval');
|
||||
$toWechatId = $this->request->param('toWechatId', '');
|
||||
$toAccountId = $this->request->param('toAccountId', 0, 'intval');
|
||||
$toUserId = $this->request->param('toUserId', 0, 'intval');
|
||||
$expireDays = $this->request->param('expireDays', 30, 'intval');
|
||||
$companyId = $this->getUserInfo('companyId');
|
||||
$operatorId = $this->getUserInfo('id');
|
||||
|
||||
if (empty($poolCompanyId) || empty($toWechatId)) {
|
||||
return ResponseHelper::error('参数不完整');
|
||||
}
|
||||
|
||||
// 验证流量归属
|
||||
$poolCompany = TrafficPoolCompany::where('id', $poolCompanyId)
|
||||
->where('companyId', $companyId)
|
||||
->where('isDel', 0)
|
||||
->find();
|
||||
|
||||
if (!$poolCompany) {
|
||||
return ResponseHelper::error('流量不存在');
|
||||
}
|
||||
|
||||
try {
|
||||
$fromInfo = [
|
||||
'fromWechatId' => $poolCompany->ownerWechatId,
|
||||
'fromAccountId' => $poolCompany->ownerAccountId,
|
||||
'fromUserId' => $poolCompany->ownerUserId,
|
||||
];
|
||||
|
||||
$record = TrafficPoolAllotRecord::createAllotRecord(
|
||||
$poolCompanyId,
|
||||
$poolCompany->identifier,
|
||||
$companyId,
|
||||
$toWechatId,
|
||||
$toAccountId ?: null,
|
||||
$toUserId ?: null,
|
||||
$expireDays,
|
||||
$operatorId,
|
||||
$fromInfo
|
||||
);
|
||||
|
||||
return ResponseHelper::success(['id' => $record->id], '分配成功');
|
||||
} catch (\Exception $e) {
|
||||
return ResponseHelper::error('分配失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 回收流量分配
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function recyclePool()
|
||||
{
|
||||
$poolCompanyId = $this->request->param('poolCompanyId', 0, 'intval');
|
||||
$companyId = $this->getUserInfo('companyId');
|
||||
$operatorId = $this->getUserInfo('id');
|
||||
|
||||
if (empty($poolCompanyId)) {
|
||||
return ResponseHelper::error('流量ID不能为空');
|
||||
}
|
||||
|
||||
// 验证流量归属
|
||||
$poolCompany = TrafficPoolCompany::where('id', $poolCompanyId)
|
||||
->where('companyId', $companyId)
|
||||
->where('isDel', 0)
|
||||
->find();
|
||||
|
||||
if (!$poolCompany) {
|
||||
return ResponseHelper::error('流量不存在');
|
||||
}
|
||||
|
||||
try {
|
||||
TrafficPoolAllotRecord::recycleAllot($poolCompanyId, $operatorId);
|
||||
return ResponseHelper::success(null, '回收成功');
|
||||
} catch (\Exception $e) {
|
||||
return ResponseHelper::error('回收失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 统计相关接口 ====================
|
||||
|
||||
/**
|
||||
* 获取流量池统计数据
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function getStatistics()
|
||||
{
|
||||
$companyId = $this->getUserInfo('companyId');
|
||||
|
||||
try {
|
||||
$statistics = $this->poolService->getStatistics($companyId);
|
||||
return ResponseHelper::success($statistics);
|
||||
} catch (\Exception $e) {
|
||||
return ResponseHelper::error('获取统计数据失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
namespace app\cunkebao\controller\tag;
|
||||
|
||||
use app\common\service\TagEngineService;
|
||||
use library\ResponseHelper;
|
||||
use think\Controller;
|
||||
use think\Validate;
|
||||
|
||||
/**
|
||||
* 通过标识查询标签控制器
|
||||
*/
|
||||
class QueryTagsByIdentifiersController extends Controller
|
||||
{
|
||||
/**
|
||||
* 通过标识查询标签
|
||||
*
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
try {
|
||||
// 获取请求参数
|
||||
$identifiers = $this->request->param('identifiers', []);
|
||||
$options = $this->request->param('options', []);
|
||||
|
||||
// 参数验证
|
||||
$validate = Validate::make([
|
||||
'identifiers' => 'require|array',
|
||||
'identifiers.*' => 'array',
|
||||
]);
|
||||
|
||||
if (!$validate->check(['identifiers' => $identifiers])) {
|
||||
throw new \Exception($validate->getError(), 400);
|
||||
}
|
||||
|
||||
// 验证标识格式
|
||||
foreach ($identifiers as $key => $identifier) {
|
||||
if (!isset($identifier['type']) || !isset($identifier['value'])) {
|
||||
throw new \Exception("标识[{$key}]格式错误,必须包含type和value字段", 400);
|
||||
}
|
||||
|
||||
// 验证标识类型
|
||||
$allowedTypes = ['phone', 'id_card', 'wechat', 'qq'];
|
||||
if (!in_array($identifier['type'], $allowedTypes)) {
|
||||
throw new \Exception("标识[{$key}]类型不支持,仅支持:" . implode(', ', $allowedTypes), 400);
|
||||
}
|
||||
|
||||
// 验证值不为空
|
||||
if (empty($identifier['value'])) {
|
||||
throw new \Exception("标识[{$key}]的值不能为空", 400);
|
||||
}
|
||||
}
|
||||
|
||||
// 限制数量
|
||||
if (count($identifiers) > 100) {
|
||||
throw new \Exception('单次最多查询100个标识', 400);
|
||||
}
|
||||
|
||||
// 调用标签引擎服务
|
||||
$service = new TagEngineService();
|
||||
$result = $service->queryByIdentifiers($identifiers, $options);
|
||||
|
||||
if ($result === false) {
|
||||
throw new \Exception('查询标签失败', 500);
|
||||
}
|
||||
|
||||
// 检查返回结果
|
||||
if (isset($result['code']) && $result['code'] !== 0) {
|
||||
throw new \Exception($result['message'] ?? '查询失败', $result['code'] ?? 500);
|
||||
}
|
||||
|
||||
return ResponseHelper::success($result['data'] ?? $result, $result['message'] ?? '查询成功');
|
||||
} catch (\Exception $e) {
|
||||
return ResponseHelper::error($e->getMessage(), $e->getCode() ?: 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 快捷方法:通过手机号查询标签
|
||||
*
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function byPhone()
|
||||
{
|
||||
try {
|
||||
// 获取请求参数
|
||||
$phones = $this->request->param('phones', []);
|
||||
$options = $this->request->param('options', []);
|
||||
|
||||
// 参数验证
|
||||
if (empty($phones)) {
|
||||
throw new \Exception('手机号不能为空', 400);
|
||||
}
|
||||
|
||||
// 如果是字符串,转为数组
|
||||
if (is_string($phones)) {
|
||||
$phones = explode(',', $phones);
|
||||
}
|
||||
|
||||
if (!is_array($phones)) {
|
||||
throw new \Exception('手机号格式错误', 400);
|
||||
}
|
||||
|
||||
// 调用标签引擎服务
|
||||
$service = new TagEngineService();
|
||||
$result = $service->queryByPhone($phones, $options);
|
||||
|
||||
if ($result === false) {
|
||||
throw new \Exception('查询标签失败', 500);
|
||||
}
|
||||
|
||||
// 检查返回结果
|
||||
if (isset($result['code']) && $result['code'] !== 0) {
|
||||
throw new \Exception($result['message'] ?? '查询失败', $result['code'] ?? 500);
|
||||
}
|
||||
|
||||
return ResponseHelper::success($result['data'] ?? $result, $result['message'] ?? '查询成功');
|
||||
} catch (\Exception $e) {
|
||||
return ResponseHelper::error($e->getMessage(), $e->getCode() ?: 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 快捷方法:通过微信号查询标签
|
||||
*
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function byWechat()
|
||||
{
|
||||
try {
|
||||
// 获取请求参数
|
||||
$wechats = $this->request->param('wechats', []);
|
||||
$options = $this->request->param('options', []);
|
||||
|
||||
// 参数验证
|
||||
if (empty($wechats)) {
|
||||
throw new \Exception('微信号不能为空', 400);
|
||||
}
|
||||
|
||||
// 如果是字符串,转为数组
|
||||
if (is_string($wechats)) {
|
||||
$wechats = explode(',', $wechats);
|
||||
}
|
||||
|
||||
if (!is_array($wechats)) {
|
||||
throw new \Exception('微信号格式错误', 400);
|
||||
}
|
||||
|
||||
// 调用标签引擎服务
|
||||
$service = new TagEngineService();
|
||||
$result = $service->queryByWechat($wechats, $options);
|
||||
|
||||
if ($result === false) {
|
||||
throw new \Exception('查询标签失败', 500);
|
||||
}
|
||||
|
||||
// 检查返回结果
|
||||
if (isset($result['code']) && $result['code'] !== 0) {
|
||||
throw new \Exception($result['message'] ?? '查询失败', $result['code'] ?? 500);
|
||||
}
|
||||
|
||||
return ResponseHelper::success($result['data'] ?? $result, $result['message'] ?? '查询成功');
|
||||
} catch (\Exception $e) {
|
||||
return ResponseHelper::error($e->getMessage(), $e->getCode() ?: 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
namespace app\cunkebao\controller\tag;
|
||||
|
||||
use app\common\service\TagEngineService;
|
||||
use library\ResponseHelper;
|
||||
use think\Controller;
|
||||
use think\Validate;
|
||||
|
||||
/**
|
||||
* 通过标签查询用户控制器
|
||||
*/
|
||||
class QueryUsersByTagsController extends Controller
|
||||
{
|
||||
/**
|
||||
* 通过标签查询用户
|
||||
*
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
try {
|
||||
// 获取请求参数
|
||||
$tagConditions = $this->request->param('tag_conditions', []);
|
||||
$logic = $this->request->param('logic', 'AND');
|
||||
$includeSensitive = $this->request->param('include_sensitive', false);
|
||||
$page = $this->request->param('page', 1);
|
||||
$pageSize = $this->request->param('page_size', 20);
|
||||
|
||||
// 参数验证
|
||||
$validate = Validate::make([
|
||||
'tag_conditions' => 'require|array',
|
||||
'tag_conditions.*' => 'array',
|
||||
'logic' => 'in:AND,OR',
|
||||
'page' => 'number|>=:1',
|
||||
'page_size' => 'number|between:1,100',
|
||||
]);
|
||||
|
||||
$params = [
|
||||
'tag_conditions' => $tagConditions,
|
||||
'logic' => $logic,
|
||||
'page' => $page,
|
||||
'page_size' => $pageSize,
|
||||
];
|
||||
|
||||
if (!$validate->check($params)) {
|
||||
throw new \Exception($validate->getError(), 400);
|
||||
}
|
||||
|
||||
// 验证标签条件格式
|
||||
foreach ($tagConditions as $key => $condition) {
|
||||
if (!isset($condition['tag_code']) || !isset($condition['operator']) || !isset($condition['value'])) {
|
||||
throw new \Exception("标签条件[{$key}]格式错误,必须包含tag_code、operator和value字段", 400);
|
||||
}
|
||||
|
||||
// 验证操作符
|
||||
$allowedOperators = ['=', '!=', '>', '>=', '<', '<=', 'in', 'not_in'];
|
||||
if (!in_array($condition['operator'], $allowedOperators)) {
|
||||
throw new \Exception("标签条件[{$key}]操作符不支持,仅支持:" . implode(', ', $allowedOperators), 400);
|
||||
}
|
||||
|
||||
// 验证 in/not_in 的值必须是数组
|
||||
if (in_array($condition['operator'], ['in', 'not_in']) && !is_array($condition['value'])) {
|
||||
throw new \Exception("标签条件[{$key}]使用{$condition['operator']}操作符时,value必须是数组", 400);
|
||||
}
|
||||
}
|
||||
|
||||
// 限制条件数量
|
||||
if (count($tagConditions) > 10) {
|
||||
throw new \Exception('单次最多10个标签条件', 400);
|
||||
}
|
||||
|
||||
// 调用标签引擎服务
|
||||
$service = new TagEngineService();
|
||||
$result = $service->queryUsersByTags($tagConditions, $logic, $includeSensitive, $page, $pageSize);
|
||||
|
||||
if ($result === false) {
|
||||
throw new \Exception('查询用户失败', 500);
|
||||
}
|
||||
|
||||
// 检查返回结果
|
||||
if (isset($result['code']) && $result['code'] !== 0) {
|
||||
throw new \Exception($result['message'] ?? '查询失败', $result['code'] ?? 500);
|
||||
}
|
||||
|
||||
return ResponseHelper::success($result['data'] ?? $result, $result['message'] ?? '查询成功');
|
||||
} catch (\Exception $e) {
|
||||
return ResponseHelper::error($e->getMessage(), $e->getCode() ?: 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 快捷方法:查询高价值用户(示例)
|
||||
* 查询累计消费金额 >= 5000 的用户
|
||||
*
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function highValueUsers()
|
||||
{
|
||||
try {
|
||||
$page = $this->request->param('page', 1);
|
||||
$pageSize = $this->request->param('page_size', 20);
|
||||
$minAmount = $this->request->param('min_amount', 5000);
|
||||
|
||||
$tagConditions = [
|
||||
[
|
||||
'tag_code' => 'user.trade.total_amount',
|
||||
'operator' => '>=',
|
||||
'value' => strval($minAmount)
|
||||
]
|
||||
];
|
||||
|
||||
// 调用标签引擎服务
|
||||
$service = new TagEngineService();
|
||||
$result = $service->queryUsersByTags($tagConditions, 'AND', false, $page, $pageSize);
|
||||
|
||||
if ($result === false) {
|
||||
throw new \Exception('查询用户失败', 500);
|
||||
}
|
||||
|
||||
// 检查返回结果
|
||||
if (isset($result['code']) && $result['code'] !== 0) {
|
||||
throw new \Exception($result['message'] ?? '查询失败', $result['code'] ?? 500);
|
||||
}
|
||||
|
||||
return ResponseHelper::success($result['data'] ?? $result, $result['message'] ?? '查询成功');
|
||||
} catch (\Exception $e) {
|
||||
return ResponseHelper::error($e->getMessage(), $e->getCode() ?: 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 快捷方法:查询VIP用户(示例)
|
||||
* 查询用户等级为 VIP、SVIP 或金卡会员的用户
|
||||
*
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function vipUsers()
|
||||
{
|
||||
try {
|
||||
$page = $this->request->param('page', 1);
|
||||
$pageSize = $this->request->param('page_size', 20);
|
||||
$levels = $this->request->param('levels', ['VIP', 'SVIP', '金卡会员']);
|
||||
|
||||
// 如果是字符串,转为数组
|
||||
if (is_string($levels)) {
|
||||
$levels = explode(',', $levels);
|
||||
}
|
||||
|
||||
$tagConditions = [
|
||||
[
|
||||
'tag_code' => 'user.trade.level',
|
||||
'operator' => 'in',
|
||||
'value' => $levels
|
||||
]
|
||||
];
|
||||
|
||||
// 调用标签引擎服务
|
||||
$service = new TagEngineService();
|
||||
$result = $service->queryUsersByTags($tagConditions, 'AND', false, $page, $pageSize);
|
||||
|
||||
if ($result === false) {
|
||||
throw new \Exception('查询用户失败', 500);
|
||||
}
|
||||
|
||||
// 检查返回结果
|
||||
if (isset($result['code']) && $result['code'] !== 0) {
|
||||
throw new \Exception($result['message'] ?? '查询失败', $result['code'] ?? 500);
|
||||
}
|
||||
|
||||
return ResponseHelper::success($result['data'] ?? $result, $result['message'] ?? '查询成功');
|
||||
} catch (\Exception $e) {
|
||||
return ResponseHelper::error($e->getMessage(), $e->getCode() ?: 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
650
Server/application/cunkebao/service/TrafficPoolGroupService.php
Normal file
650
Server/application/cunkebao/service/TrafficPoolGroupService.php
Normal file
@@ -0,0 +1,650 @@
|
||||
<?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
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
507
Server/application/cunkebao/service/TrafficPoolService.php
Normal file
507
Server/application/cunkebao/service/TrafficPoolService.php
Normal file
@@ -0,0 +1,507 @@
|
||||
<?php
|
||||
|
||||
namespace app\cunkebao\service;
|
||||
|
||||
use app\common\model\TrafficPoolV2;
|
||||
use app\common\model\TrafficPoolCompany;
|
||||
use app\common\model\TrafficPoolSource;
|
||||
use app\common\model\TrafficPoolBehavior;
|
||||
use app\common\model\TrafficPoolTag;
|
||||
use app\common\model\TrafficPoolAllotRecord;
|
||||
use think\Db;
|
||||
|
||||
/**
|
||||
* 流量池核心服务类
|
||||
* 处理流量的入池、查询、更新等核心业务逻辑
|
||||
*/
|
||||
class TrafficPoolService
|
||||
{
|
||||
/**
|
||||
* 流量入池(核心方法)
|
||||
*
|
||||
* @param string $identifier 唯一标识(微信ID优先)
|
||||
* @param int $companyId 公司ID
|
||||
* @param int $sourceType 来源类型
|
||||
* @param array $poolData 流量总表数据
|
||||
* @param array $companyData 公司流量数据
|
||||
* @param array $sourceData 来源数据
|
||||
* @return array [poolId, poolCompanyId]
|
||||
*/
|
||||
public function enterPool(
|
||||
string $identifier,
|
||||
int $companyId,
|
||||
int $sourceType,
|
||||
array $poolData = [],
|
||||
array $companyData = [],
|
||||
array $sourceData = []
|
||||
) {
|
||||
Db::startTrans();
|
||||
try {
|
||||
// 1. 查找或创建总表记录
|
||||
$pool = TrafficPoolV2::findOrCreateByIdentifier($identifier, $poolData);
|
||||
|
||||
// 2. 查找或创建公司记录
|
||||
$poolCompany = TrafficPoolCompany::findOrCreateByIdentifierAndCompany(
|
||||
$identifier,
|
||||
$companyId,
|
||||
$pool->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();
|
||||
|
||||
// 获取来源历史
|
||||
$data['sources'] = TrafficPoolSource::getSourcesByPoolCompany($poolCompanyId)->toArray();
|
||||
|
||||
// 获取行为轨迹(最近50条)
|
||||
$data['behaviors'] = TrafficPoolBehavior::getUserJourney($poolCompanyId, 50)->toArray();
|
||||
|
||||
// 获取分配历史
|
||||
$data['allotRecords'] = TrafficPoolAllotRecord::getAllotHistory($poolCompanyId)->toArray();
|
||||
|
||||
// 计算 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
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user