流量池服务端
This commit is contained in:
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user