881 lines
35 KiB
PHP
881 lines
35 KiB
PHP
<?php
|
||
|
||
namespace app\store\controller;
|
||
|
||
use app\common\model\TrafficPoolCompany;
|
||
use think\Db;
|
||
use think\facade\Log;
|
||
|
||
/**
|
||
* 客户管理控制器
|
||
*/
|
||
class CustomerController extends BaseController
|
||
{
|
||
/**
|
||
* 获取客户列表
|
||
* GET /v2/store/customers
|
||
*
|
||
* @return \think\response\Json
|
||
*/
|
||
public function getList()
|
||
{
|
||
try {
|
||
$userId = $this->userInfo['id'] ?? 0;
|
||
$companyId = $this->userInfo['companyId'] ?? 0;
|
||
|
||
if (empty($userId) || empty($companyId)) {
|
||
return json(['code' => 401, 'msg' => '请先登录']);
|
||
}
|
||
|
||
// 获取设备信息
|
||
$device = $this->device;
|
||
if (empty($device) || empty($device['wechatId'])) {
|
||
return json(['code' => 404, 'msg' => '设备未绑定微信']);
|
||
}
|
||
|
||
$wechatId = $device['wechatId'];
|
||
|
||
// 获取微信账号ID
|
||
$wechatAccount = Db::table('s2_wechat_account')
|
||
->where('wechatId', $wechatId)
|
||
->field('id')
|
||
->find();
|
||
|
||
if (empty($wechatAccount)) {
|
||
return json(['code' => 404, 'msg' => '微信账号不存在']);
|
||
}
|
||
|
||
$accountId = $wechatAccount['id'];
|
||
|
||
// 分页参数
|
||
$page = intval($this->request->param('page', 1));
|
||
$limit = intval($this->request->param('limit', 10));
|
||
$pageSize = intval($this->request->param('pageSize', 10));
|
||
|
||
if ($page <= 0) $page = 1;
|
||
if ($limit <= 0) $limit = $pageSize > 0 ? $pageSize : 10;
|
||
if ($limit > 100) $limit = 100;
|
||
|
||
// 搜索关键词
|
||
$keyword = $this->request->param('keyword', '');
|
||
|
||
// 筛选条件
|
||
$status = $this->request->param('status', ''); // 状态:潜在、活跃、沉默、流失
|
||
$value = $this->request->param('value', ''); // 价值:高、中、低
|
||
$lifecycle = $this->request->param('lifecycle', ''); // 生命周期
|
||
|
||
// 构建查询条件
|
||
// 从流量池公司表查询,关联流量池总表和微信好友表
|
||
// 注意:s2_wechat_friend 表没有 ck_ 前缀,使用数组形式 join 可以避免自动添加前缀
|
||
$query = Db::name('traffic_pool_company')
|
||
->alias('tpc')
|
||
->join('traffic_pool tp', 'tp.id = tpc.poolId', 'left')
|
||
->join(['s2_wechat_friend' => 'wf'], 'wf.wechatId = tp.wechatId AND wf.ownerWechatId = \'' . $wechatId . '\'', 'left')
|
||
->where([
|
||
['tpc.companyId', '=', $companyId],
|
||
['tpc.ownerAccountId', '=', $accountId], // 归属当前微信账号
|
||
['tpc.status', '=', TrafficPoolCompany::STATUS_NORMAL], // 正常状态
|
||
]);
|
||
|
||
// 关键词搜索(昵称、微信号、手机号)
|
||
if (!empty($keyword)) {
|
||
$query->where(function($query) use ($keyword) {
|
||
$query->where('tp.nickname', 'like', '%' . $keyword . '%')
|
||
->whereOr('tp.wechatAlias', 'like', '%' . $keyword . '%')
|
||
->whereOr('tp.mobile', 'like', '%' . $keyword . '%')
|
||
->whereOr('tpc.realName', 'like', '%' . $keyword . '%')
|
||
->whereOr('tpc.phone', 'like', '%' . $keyword . '%');
|
||
});
|
||
}
|
||
|
||
// 状态筛选(根据生命周期)
|
||
if (!empty($lifecycle)) {
|
||
$lifecycleMap = [
|
||
'潜在' => TrafficPoolCompany::LIFECYCLE_NEW,
|
||
'活跃' => TrafficPoolCompany::LIFECYCLE_FOLLOWING,
|
||
'沉默' => TrafficPoolCompany::LIFECYCLE_SILENT,
|
||
'流失' => TrafficPoolCompany::LIFECYCLE_LOST,
|
||
];
|
||
if (isset($lifecycleMap[$lifecycle])) {
|
||
$query->where('tpc.lifecycle', '=', $lifecycleMap[$lifecycle]);
|
||
}
|
||
}
|
||
|
||
// 价值筛选(根据意向度或等级)
|
||
if (!empty($value)) {
|
||
$valueMap = [
|
||
'高' => TrafficPoolCompany::INTENTION_HIGH,
|
||
'中' => TrafficPoolCompany::INTENTION_MEDIUM,
|
||
'低' => TrafficPoolCompany::INTENTION_LOW,
|
||
];
|
||
if (isset($valueMap[$value])) {
|
||
$query->where('tpc.intentionLevel', '=', $valueMap[$value]);
|
||
}
|
||
}
|
||
|
||
// 统计总数
|
||
$total = $query->count();
|
||
|
||
// 获取列表数据
|
||
$list = $query->field('tpc.id,tpc.poolId,tpc.companyId,tpc.ownerAccountId,tpc.realName,tpc.phone,tpc.email,tpc.lifecycle,tpc.intentionLevel,tpc.level,tpc.remark,tpc.createTime,tp.nickname,tp.avatar,tp.wechatId,tp.wechatAlias,tp.mobile,tp.gender,tp.region,tp.signature,wf.id as friendId,wf.alias as friendAlias,wf.nickname as friendNickname')
|
||
->order('tpc.id desc')
|
||
->page($page, $limit)
|
||
->select();
|
||
|
||
// 格式化数据
|
||
$result = [];
|
||
foreach ($list as $item) {
|
||
// 获取标签
|
||
$tags = Db::name('traffic_pool_tag')
|
||
->where([
|
||
['poolCompanyId', '=', $item['id']],
|
||
['isDel', '=', 0]
|
||
])
|
||
->column('tagName');
|
||
|
||
// 获取最后互动时间(从行为记录表)
|
||
$lastBehavior = Db::name('traffic_pool_behavior')
|
||
->where('poolCompanyId', $item['id'])
|
||
->order('behaviorTime desc')
|
||
->find();
|
||
|
||
$lastContact = '';
|
||
if (!empty($lastBehavior) && !empty($lastBehavior['behaviorTime'])) {
|
||
$lastContact = date('Y-m-d H:i:s', intval($lastBehavior['behaviorTime']));
|
||
}
|
||
|
||
// 获取价值评估(从RFM或估值相关表,这里先使用模拟数据)
|
||
$valuation = $this->calculateCustomerValuation($item['id']);
|
||
|
||
// 状态映射
|
||
$lifecycleMap = [
|
||
TrafficPoolCompany::LIFECYCLE_NEW => '潜在',
|
||
TrafficPoolCompany::LIFECYCLE_FOLLOWING => '活跃',
|
||
TrafficPoolCompany::LIFECYCLE_CONVERTED => '已成交',
|
||
TrafficPoolCompany::LIFECYCLE_SILENT => '沉默',
|
||
TrafficPoolCompany::LIFECYCLE_LOST => '流失',
|
||
];
|
||
|
||
// 价值映射
|
||
$intentionMap = [
|
||
TrafficPoolCompany::INTENTION_HIGH => '高',
|
||
TrafficPoolCompany::INTENTION_MEDIUM => '中',
|
||
TrafficPoolCompany::INTENTION_LOW => '低',
|
||
TrafficPoolCompany::INTENTION_UNKNOWN => '低',
|
||
];
|
||
|
||
$result[] = [
|
||
'id' => intval($item['id']),
|
||
'poolCompanyId' => intval($item['id']),
|
||
'name' => $item['realName'] ?? $item['nickname'] ?? '未知',
|
||
'nickname' => $item['nickname'] ?? '',
|
||
'wechatId' => $item['wechatAlias'] ?? $item['wechatId'] ?? '',
|
||
'avatar' => $item['avatar'] ?? '',
|
||
'phone' => $item['phone'] ?? $item['mobile'] ?? '',
|
||
'email' => $item['email'] ?? '',
|
||
'status' => $lifecycleMap[$item['lifecycle'] ?? TrafficPoolCompany::LIFECYCLE_NEW] ?? '潜在',
|
||
'value' => $intentionMap[$item['intentionLevel'] ?? TrafficPoolCompany::INTENTION_UNKNOWN] ?? '低',
|
||
'tags' => $tags ?: [],
|
||
'lastContact' => $lastContact,
|
||
'nextFollow' => !empty($item['nextFollowTime']) && is_numeric($item['nextFollowTime'])
|
||
? date('Y-m-d', intval($item['nextFollowTime']))
|
||
: '',
|
||
'notes' => $item['remark'] ?? '',
|
||
'addedDate' => !empty($item['createTime']) && is_numeric($item['createTime'])
|
||
? date('Y-m-d', intval($item['createTime']))
|
||
: '',
|
||
'valuation' => $valuation,
|
||
];
|
||
}
|
||
|
||
return json([
|
||
'code' => 200,
|
||
'msg' => '获取成功',
|
||
'data' => [
|
||
'list' => $result,
|
||
'total' => $total,
|
||
'page' => $page,
|
||
'limit' => $limit
|
||
]
|
||
]);
|
||
} catch (\Exception $e) {
|
||
Log::error('获取客户列表失败: ' . $e->getMessage());
|
||
return json(['code' => 500, 'msg' => '获取失败:' . $e->getMessage()]);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取客户详情
|
||
* GET /v2/store/customers/:id
|
||
*
|
||
* @return \think\response\Json
|
||
*/
|
||
public function detail()
|
||
{
|
||
try {
|
||
$userId = $this->userInfo['id'] ?? 0;
|
||
$companyId = $this->userInfo['companyId'] ?? 0;
|
||
|
||
if (empty($userId) || empty($companyId)) {
|
||
return json(['code' => 401, 'msg' => '请先登录']);
|
||
}
|
||
|
||
// 获取设备信息
|
||
$device = $this->device;
|
||
if (empty($device) || empty($device['wechatId'])) {
|
||
return json(['code' => 404, 'msg' => '设备未绑定微信']);
|
||
}
|
||
|
||
$wechatId = $device['wechatId'];
|
||
|
||
// 获取微信账号ID
|
||
$wechatAccount = Db::table('s2_wechat_account')
|
||
->where('wechatId', $wechatId)
|
||
->field('id')
|
||
->find();
|
||
|
||
if (empty($wechatAccount)) {
|
||
return json(['code' => 404, 'msg' => '微信账号不存在']);
|
||
}
|
||
|
||
$accountId = $wechatAccount['id'];
|
||
|
||
// 获取客户ID
|
||
$customerId = intval($this->request->param('id', 0));
|
||
if (empty($customerId)) {
|
||
return json(['code' => 400, 'msg' => '客户ID不能为空']);
|
||
}
|
||
|
||
// 查询客户详情
|
||
// 注意:s2_wechat_friend 表没有 ck_ 前缀,使用数组形式 join 可以避免自动添加前缀
|
||
$customer = Db::name('traffic_pool_company')
|
||
->alias('tpc')
|
||
->join('traffic_pool tp', 'tp.id = tpc.poolId', 'left')
|
||
->join(['s2_wechat_friend' => 'wf'], 'wf.wechatId = tp.wechatId AND wf.ownerWechatId = \'' . $wechatId . '\'', 'left')
|
||
->where([
|
||
['tpc.id', '=', $customerId],
|
||
['tpc.companyId', '=', $companyId],
|
||
['tpc.ownerAccountId', '=', $accountId],
|
||
])
|
||
->field('tpc.*,tp.*,wf.id as friendId,wf.alias as friendAlias,wf.nickname as friendNickname')
|
||
->find();
|
||
|
||
if (empty($customer)) {
|
||
return json(['code' => 404, 'msg' => '客户不存在']);
|
||
}
|
||
|
||
// 获取标签
|
||
$tags = Db::name('traffic_pool_tag')
|
||
->where([
|
||
['poolCompanyId', '=', $customerId],
|
||
['isDel', '=', 0]
|
||
])
|
||
->column('tagName');
|
||
|
||
// 获取流量池标签(系统标签或微信标签)
|
||
// 注意:从表结构看,isSystem字段在tagDefineId关联的标签定义表中
|
||
// 这里先获取所有标签,后续可以根据tagType区分
|
||
$allTags = Db::name('traffic_pool_tag')
|
||
->alias('tpt')
|
||
->join('traffic_pool_tag_define tptd', 'tpt.tagDefineId = tptd.id', 'left')
|
||
->where([
|
||
['tpt.poolCompanyId', '=', $customerId],
|
||
['tpt.isDel', '=', 0]
|
||
])
|
||
->field('tpt.tagName,tptd.isSystem')
|
||
->select();
|
||
|
||
$trafficPoolTags = [];
|
||
foreach ($allTags as $tag) {
|
||
// 系统标签或微信标签(tagType=1)作为流量池标签
|
||
if (!empty($tag['isSystem']) || (!empty($tag['tagType']) && $tag['tagType'] == 1)) {
|
||
$trafficPoolTags[] = $tag['tagName'];
|
||
}
|
||
}
|
||
|
||
// 获取来源信息
|
||
$sources = Db::name('traffic_pool_source')
|
||
->where('poolCompanyId', $customerId)
|
||
->order('createTime desc')
|
||
->select();
|
||
|
||
$sourceChannel = '未知';
|
||
$addTime = '';
|
||
if (!empty($sources)) {
|
||
$firstSource = $sources[0];
|
||
$sourceChannel = $firstSource['sourceName'] ?? '未知';
|
||
$addTime = !empty($firstSource['createTime']) && is_numeric($firstSource['createTime'])
|
||
? date('Y-m-d', intval($firstSource['createTime']))
|
||
: '';
|
||
}
|
||
|
||
// 获取互动统计
|
||
$interactionStats = $this->getInteractionStats($customerId);
|
||
|
||
// 获取价值评估
|
||
$valueEvaluation = $this->getValueEvaluation($customerId);
|
||
|
||
// 获取用户旅程(最近记录)
|
||
$journey = $this->getCustomerJourney($customerId, 10);
|
||
|
||
// 获取消费偏好(从行为记录分析)
|
||
$preferences = $this->getCustomerPreferences($customerId);
|
||
|
||
// 状态映射
|
||
$lifecycleMap = [
|
||
TrafficPoolCompany::LIFECYCLE_NEW => '潜在',
|
||
TrafficPoolCompany::LIFECYCLE_FOLLOWING => '活跃',
|
||
TrafficPoolCompany::LIFECYCLE_CONVERTED => '已成交',
|
||
TrafficPoolCompany::LIFECYCLE_SILENT => '沉默',
|
||
TrafficPoolCompany::LIFECYCLE_LOST => '流失',
|
||
];
|
||
|
||
$conversionStatus = $lifecycleMap[$customer['lifecycle'] ?? TrafficPoolCompany::LIFECYCLE_NEW] ?? '潜在';
|
||
|
||
// 生成首字母
|
||
$name = $customer['realName'] ?? $customer['nickname'] ?? '未知';
|
||
$initials = mb_substr($name, 0, 1, 'UTF-8');
|
||
|
||
return json([
|
||
'code' => 200,
|
||
'msg' => '获取成功',
|
||
'data' => [
|
||
// 基础信息
|
||
'id' => intval($customer['id']),
|
||
'poolCompanyId' => intval($customer['id']),
|
||
'initials' => $initials,
|
||
|
||
// 好友概览
|
||
'nickname' => $customer['nickname'] ?? '',
|
||
'remarkName' => $customer['realName'] ?? '',
|
||
'wechatId' => $customer['wechatAlias'] ?? $customer['wechatId'] ?? '',
|
||
'wechatPhone' => $customer['mobile'] ?? '',
|
||
'wechatLocation' => $customer['region'] ?? '',
|
||
'avatar' => $customer['avatar'] ?? '',
|
||
'conversionStatus' => $conversionStatus,
|
||
'sourceChannel' => $sourceChannel,
|
||
'addTime' => $addTime,
|
||
|
||
// 基础信息
|
||
'realName' => $customer['realName'] ?? '',
|
||
'sex' => $this->getGenderText($customer['gender'] ?? 0),
|
||
'age' => $this->calculateAge($customer['birthday'] ?? ''),
|
||
'personalPhone' => $customer['phone'] ?? '',
|
||
'email' => $customer['email'] ?? '',
|
||
'idNumber' => $this->maskIdNumber($customer['idCard'] ?? ''),
|
||
'address' => $customer['address'] ?? '',
|
||
|
||
// 标签
|
||
'tags' => $tags ?: [],
|
||
'trafficPoolTags' => $trafficPoolTags ?: [],
|
||
|
||
// 互动统计
|
||
'interactionStats' => $interactionStats,
|
||
|
||
// 价值评估
|
||
'valueEvaluation' => $valueEvaluation,
|
||
'valuationRank' => 'TOP 8%', // 需要计算
|
||
'valuationTrend' => '+12%', // 需要计算
|
||
|
||
// 用户旅程
|
||
'journey' => $journey,
|
||
|
||
// 消费偏好
|
||
'preferences' => $preferences,
|
||
|
||
// AI预测(需要实现)
|
||
'aiProfile' => [
|
||
'summary' => '该用户为典型的高净值客户,消费频率高且偏好高端产品。',
|
||
'predictions' => [
|
||
'预计未来7天内有85%概率下单',
|
||
'流失风险极低(5%),建议通过会员活动维持粘性',
|
||
'最佳触达时间:工作日12:00-14:00或周末下午'
|
||
]
|
||
],
|
||
]
|
||
]);
|
||
} catch (\Exception $e) {
|
||
Log::error('获取客户详情失败: ' . $e->getMessage());
|
||
return json(['code' => 500, 'msg' => '获取失败:' . $e->getMessage()]);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 更新客户信息
|
||
* PUT /v2/store/customers/:id
|
||
*
|
||
* @return \think\response\Json
|
||
*/
|
||
public function update()
|
||
{
|
||
try {
|
||
$userId = $this->userInfo['id'] ?? 0;
|
||
$companyId = $this->userInfo['companyId'] ?? 0;
|
||
|
||
if (empty($userId) || empty($companyId)) {
|
||
return json(['code' => 401, 'msg' => '请先登录']);
|
||
}
|
||
|
||
// 获取设备信息
|
||
$device = $this->device;
|
||
if (empty($device) || empty($device['wechatId'])) {
|
||
return json(['code' => 404, 'msg' => '设备未绑定微信']);
|
||
}
|
||
|
||
$wechatId = $device['wechatId'];
|
||
|
||
// 获取微信账号ID
|
||
$wechatAccount = Db::table('s2_wechat_account')
|
||
->where('wechatId', $wechatId)
|
||
->field('id')
|
||
->find();
|
||
|
||
if (empty($wechatAccount)) {
|
||
return json(['code' => 404, 'msg' => '微信账号不存在']);
|
||
}
|
||
|
||
$accountId = $wechatAccount['id'];
|
||
|
||
// 获取客户ID
|
||
$customerId = intval($this->request->param('id', 0));
|
||
if (empty($customerId)) {
|
||
return json(['code' => 400, 'msg' => '客户ID不能为空']);
|
||
}
|
||
|
||
// 验证客户是否存在且归属当前账号
|
||
$customer = Db::name('traffic_pool_company')
|
||
->where([
|
||
['id', '=', $customerId],
|
||
['companyId', '=', $companyId],
|
||
['ownerAccountId', '=', $accountId],
|
||
])
|
||
->find();
|
||
|
||
if (empty($customer)) {
|
||
return json(['code' => 404, 'msg' => '客户不存在']);
|
||
}
|
||
|
||
// 获取更新参数
|
||
$updateType = $this->request->param('updateType', ''); // wechat, personal, tags
|
||
|
||
$updateData = [];
|
||
$updateFields = [];
|
||
|
||
// 更新微信资料
|
||
if ($updateType === 'wechat' || $this->request->has('remarkName')) {
|
||
$remarkName = $this->request->param('remarkName', '');
|
||
if ($remarkName !== '') {
|
||
$updateData['realName'] = $remarkName; // 备注名存储在realName字段
|
||
$updateFields[] = '备注名';
|
||
}
|
||
}
|
||
|
||
// 更新基础信息
|
||
if ($updateType === 'personal') {
|
||
$realName = $this->request->param('realName', '');
|
||
$sex = $this->request->param('sex', '');
|
||
$age = $this->request->param('age', '');
|
||
$phone = $this->request->param('phone', '');
|
||
$email = $this->request->param('email', '');
|
||
$idNumber = $this->request->param('idNumber', '');
|
||
$address = $this->request->param('address', '');
|
||
|
||
if ($realName !== '') {
|
||
$updateData['realName'] = $realName;
|
||
$updateFields[] = '姓名';
|
||
}
|
||
if ($sex !== '') {
|
||
$updateData['gender'] = $sex === '男' ? 1 : ($sex === '女' ? 2 : 0);
|
||
$updateFields[] = '性别';
|
||
}
|
||
if ($age !== '') {
|
||
// 根据年龄计算生日(简化处理)
|
||
$birthYear = date('Y') - intval($age);
|
||
$updateData['birthday'] = $birthYear . '-01-01';
|
||
$updateFields[] = '年龄';
|
||
}
|
||
if ($phone !== '') {
|
||
$updateData['phone'] = $phone;
|
||
$updateFields[] = '手机号';
|
||
}
|
||
if ($email !== '') {
|
||
$updateData['email'] = $email;
|
||
$updateFields[] = '邮箱';
|
||
}
|
||
if ($idNumber !== '') {
|
||
$updateData['idCard'] = $idNumber;
|
||
$updateFields[] = '身份证号';
|
||
}
|
||
if ($address !== '') {
|
||
$updateData['address'] = $address;
|
||
$updateFields[] = '住址';
|
||
}
|
||
}
|
||
|
||
// 更新标签
|
||
if ($updateType === 'tags') {
|
||
$tags = $this->request->param('tags', []);
|
||
if (is_array($tags)) {
|
||
// 获取客户信息(用于获取identifier和companyId)
|
||
$customerInfo = Db::name('traffic_pool_company')
|
||
->where('id', $customerId)
|
||
->field('identifier,companyId')
|
||
->find();
|
||
|
||
if (!empty($customerInfo)) {
|
||
// 软删除旧标签(只删除站内标签,保留微信标签和系统标签)
|
||
// 通过关联标签定义表判断是否为站内标签
|
||
Db::name('traffic_pool_tag')
|
||
->alias('tpt')
|
||
->join('traffic_pool_tag_define tptd', 'tpt.tagDefineId = tptd.id', 'left')
|
||
->where([
|
||
['tpt.poolCompanyId', '=', $customerId],
|
||
['tptd.tagType', '=', 2], // 站内标签
|
||
['tpt.isDel', '=', 0]
|
||
])
|
||
->update([
|
||
'tpt.isDel' => 1,
|
||
'tpt.deleteTime' => time()
|
||
]);
|
||
|
||
// 添加新标签(站内标签)
|
||
foreach ($tags as $tag) {
|
||
if (!empty($tag)) {
|
||
// 查找或创建标签定义
|
||
$tagDefine = Db::name('traffic_pool_tag_define')
|
||
->where([
|
||
['companyId', 'in', [$companyId, 0]],
|
||
['tagName', '=', $tag],
|
||
['tagType', '=', 2], // 站内标签
|
||
['isDel', '=', 0]
|
||
])
|
||
->order('companyId desc') // 优先使用公司自定义标签
|
||
->find();
|
||
|
||
if (empty($tagDefine)) {
|
||
// 创建标签定义
|
||
$tagDefineId = Db::name('traffic_pool_tag_define')->insertGetId([
|
||
'companyId' => $companyId,
|
||
'tagType' => 2, // 站内标签
|
||
'tagCode' => 'custom_' . time() . '_' . rand(1000, 9999),
|
||
'tagName' => $tag,
|
||
'isSystem' => 0,
|
||
'status' => 1,
|
||
'createTime' => time(),
|
||
]);
|
||
} else {
|
||
$tagDefineId = $tagDefine['id'];
|
||
}
|
||
|
||
// 检查标签是否已存在
|
||
$existTag = Db::name('traffic_pool_tag')
|
||
->where([
|
||
['poolCompanyId', '=', $customerId],
|
||
['tagDefineId', '=', $tagDefineId],
|
||
['isDel', '=', 0]
|
||
])
|
||
->find();
|
||
|
||
if (empty($existTag)) {
|
||
Db::name('traffic_pool_tag')->insert([
|
||
'poolCompanyId' => $customerId,
|
||
'identifier' => $customerInfo['identifier'],
|
||
'companyId' => $customerInfo['companyId'],
|
||
'tagDefineId' => $tagDefineId,
|
||
'tagType' => 2, // 站内标签
|
||
'tagName' => $tag,
|
||
'source' => 1, // 手动
|
||
'operatorId' => $userId,
|
||
'createTime' => time(),
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
$updateFields[] = '标签';
|
||
}
|
||
}
|
||
|
||
// 更新客户信息
|
||
if (!empty($updateData)) {
|
||
$updateData['updateTime'] = time();
|
||
Db::name('traffic_pool_company')
|
||
->where('id', $customerId)
|
||
->update($updateData);
|
||
}
|
||
|
||
return json([
|
||
'code' => 200,
|
||
'msg' => '更新成功',
|
||
'data' => [
|
||
'updatedFields' => $updateFields
|
||
]
|
||
]);
|
||
} catch (\Exception $e) {
|
||
Log::error('更新客户信息失败: ' . $e->getMessage());
|
||
return json(['code' => 500, 'msg' => '更新失败:' . $e->getMessage()]);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 计算客户估值
|
||
*
|
||
* @param int $poolCompanyId 客户ID
|
||
* @return int
|
||
*/
|
||
private function calculateCustomerValuation($poolCompanyId)
|
||
{
|
||
// TODO: 实现真实的估值计算逻辑
|
||
// 可以从订单表、行为记录表等计算
|
||
return 50000; // 模拟数据
|
||
}
|
||
|
||
/**
|
||
* 获取互动统计
|
||
*
|
||
* @param int $poolCompanyId 客户ID
|
||
* @return array
|
||
*/
|
||
private function getInteractionStats($poolCompanyId)
|
||
{
|
||
// 统计聊天消息数
|
||
$chatCount = Db::name('traffic_pool_behavior')
|
||
->where([
|
||
['poolCompanyId', '=', $poolCompanyId],
|
||
['behaviorType', '=', 1] // 发送消息
|
||
])
|
||
->count();
|
||
|
||
// 统计朋友圈互动数
|
||
$momentsCount = Db::name('traffic_pool_behavior')
|
||
->where([
|
||
['poolCompanyId', '=', $poolCompanyId],
|
||
['behaviorType', 'in', [9, 10]] // 点赞朋友圈、评论朋友圈
|
||
])
|
||
->count();
|
||
|
||
// 统计红包转账总额(从行为记录中获取)
|
||
$redPacketTotal = Db::name('traffic_pool_behavior')
|
||
->where([
|
||
['poolCompanyId', '=', $poolCompanyId],
|
||
['behaviorType', '=', 7] // 支付
|
||
])
|
||
->sum('amount');
|
||
$redPacketTotal = round(floatval($redPacketTotal ?? 0), 2);
|
||
|
||
// 计算活跃度评分(简化计算)
|
||
$activeScore = min(100, ($chatCount * 2 + $momentsCount * 3 + $redPacketTotal / 10));
|
||
|
||
// 获取最后互动时间
|
||
$lastBehavior = Db::name('traffic_pool_behavior')
|
||
->where('poolCompanyId', $poolCompanyId)
|
||
->order('behaviorTime desc')
|
||
->find();
|
||
|
||
$lastInteraction = '从未互动';
|
||
if (!empty($lastBehavior) && !empty($lastBehavior['behaviorTime'])) {
|
||
$time = intval($lastBehavior['behaviorTime']);
|
||
$diff = time() - $time;
|
||
if ($diff < 3600) {
|
||
$lastInteraction = '刚刚';
|
||
} elseif ($diff < 86400) {
|
||
$lastInteraction = '今天 ' . date('H:i', $time);
|
||
} elseif ($diff < 172800) {
|
||
$lastInteraction = '昨天 ' . date('H:i', $time);
|
||
} else {
|
||
$lastInteraction = date('Y-m-d H:i', $time);
|
||
}
|
||
}
|
||
|
||
return [
|
||
'lastInteraction' => $lastInteraction,
|
||
'chatCount' => intval($chatCount),
|
||
'momentsCount' => intval($momentsCount),
|
||
'redPacketTotal' => number_format($redPacketTotal, 2),
|
||
'activeScore' => intval($activeScore)
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 获取价值评估
|
||
*
|
||
* @param int $poolCompanyId 客户ID
|
||
* @return array
|
||
*/
|
||
private function getValueEvaluation($poolCompanyId)
|
||
{
|
||
// TODO: 实现真实的价值评估计算
|
||
// 可以从RFM模型、CLV模型、社交裂变模型等计算
|
||
|
||
return [
|
||
'totalValuation' => 58600,
|
||
'models' => [
|
||
[
|
||
'name' => 'RFM 贡献模型',
|
||
'value' => 52000,
|
||
'weight' => 0.5,
|
||
'score' => 92
|
||
],
|
||
[
|
||
'name' => 'CLV 终身价值模型',
|
||
'value' => 78000,
|
||
'weight' => 0.3,
|
||
'score' => 88
|
||
],
|
||
[
|
||
'name' => '社交/裂变模型',
|
||
'value' => 15000,
|
||
'weight' => 0.2,
|
||
'score' => 75
|
||
]
|
||
]
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 获取用户旅程
|
||
*
|
||
* @param int $poolCompanyId 客户ID
|
||
* @param int $limit 限制数量
|
||
* @return array
|
||
*/
|
||
private function getCustomerJourney($poolCompanyId, $limit = 10)
|
||
{
|
||
// 从行为记录表获取
|
||
$behaviors = Db::name('traffic_pool_behavior')
|
||
->where('poolCompanyId', $poolCompanyId)
|
||
->order('behaviorTime desc')
|
||
->limit($limit)
|
||
->select();
|
||
|
||
$journey = [];
|
||
$typeMap = [
|
||
1 => '发送消息',
|
||
2 => '接收消息',
|
||
3 => '浏览',
|
||
4 => '点击',
|
||
5 => '咨询',
|
||
6 => '下单',
|
||
7 => '支付',
|
||
8 => '退款',
|
||
9 => '点赞朋友圈',
|
||
10 => '评论朋友圈',
|
||
];
|
||
|
||
foreach ($behaviors as $behavior) {
|
||
$type = $typeMap[$behavior['behaviorType']] ?? '未知行为';
|
||
$content = $behavior['behaviorName'] ?? $type;
|
||
if (!empty($behavior['targetName'])) {
|
||
$content .= ': ' . $behavior['targetName'];
|
||
}
|
||
|
||
$journey[] = [
|
||
'type' => $type,
|
||
'content' => $content,
|
||
'time' => !empty($behavior['behaviorTime']) && is_numeric($behavior['behaviorTime'])
|
||
? date('Y-m-d H:i:s', intval($behavior['behaviorTime']))
|
||
: '',
|
||
'source' => '存客宝',
|
||
'actionType' => $this->getActionType($behavior['behaviorType']),
|
||
'amount' => !empty($behavior['amount']) && floatval($behavior['amount']) > 0
|
||
? '¥' . number_format(floatval($behavior['amount']), 2)
|
||
: '',
|
||
];
|
||
}
|
||
|
||
return $journey;
|
||
}
|
||
|
||
/**
|
||
* 获取行为类型
|
||
*
|
||
* @param int $behaviorType 行为类型
|
||
* @return string
|
||
*/
|
||
private function getActionType($behaviorType)
|
||
{
|
||
if (in_array($behaviorType, [6, 7, 8])) {
|
||
return 'transaction'; // 交易
|
||
} elseif (in_array($behaviorType, [1, 2, 9, 10])) {
|
||
return 'social'; // 社交
|
||
} elseif (in_array($behaviorType, [3, 4, 5])) {
|
||
return 'footprint'; // 轨迹
|
||
} else {
|
||
return 'flow'; // 流量
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取消费偏好
|
||
*
|
||
* @param int $poolCompanyId 客户ID
|
||
* @return array
|
||
*/
|
||
private function getCustomerPreferences($poolCompanyId)
|
||
{
|
||
// TODO: 从行为记录和订单记录分析消费偏好
|
||
return [
|
||
'categories' => ['智能数码', '精品咖啡', '商务休闲'],
|
||
'recentItems' => ['iPhone 16 Pro', 'iPad Air'],
|
||
'coreInterest' => '数码发烧友 & 品质生活追求者'
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 获取性别文本
|
||
*
|
||
* @param int $gender 性别代码
|
||
* @return string
|
||
*/
|
||
private function getGenderText($gender)
|
||
{
|
||
$map = [
|
||
0 => '保密',
|
||
1 => '男',
|
||
2 => '女',
|
||
];
|
||
return $map[$gender] ?? '未知';
|
||
}
|
||
|
||
/**
|
||
* 计算年龄
|
||
*
|
||
* @param string $birthday 生日
|
||
* @return int
|
||
*/
|
||
private function calculateAge($birthday)
|
||
{
|
||
if (empty($birthday)) {
|
||
return 0;
|
||
}
|
||
|
||
$birthTimestamp = strtotime($birthday);
|
||
if ($birthTimestamp === false) {
|
||
return 0;
|
||
}
|
||
|
||
$age = date('Y') - date('Y', $birthTimestamp);
|
||
if (date('md', $birthTimestamp) > date('md')) {
|
||
$age--;
|
||
}
|
||
|
||
return $age;
|
||
}
|
||
|
||
/**
|
||
* 脱敏身份证号
|
||
*
|
||
* @param string $idNumber 身份证号
|
||
* @return string
|
||
*/
|
||
private function maskIdNumber($idNumber)
|
||
{
|
||
if (empty($idNumber) || strlen($idNumber) < 8) {
|
||
return $idNumber;
|
||
}
|
||
|
||
return substr($idNumber, 0, 4) . str_repeat('*', strlen($idNumber) - 8) . substr($idNumber, -4);
|
||
}
|
||
}
|
||
|