流量池服务端

This commit is contained in:
wong
2026-02-02 11:00:26 +08:00
parent f0c278a35f
commit 519a9b1d1b
22 changed files with 7239 additions and 2252 deletions

View 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();
}
}

View 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();
}
}

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

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

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

View 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();
}
}

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

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

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

View 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);
}
}

View 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);
}
}