1、豆包新增生成图片功能
2、消息优化 3、场景获客新增全局配置 4、工作台新增全局配置
This commit is contained in:
@@ -13,6 +13,7 @@ Route::group('v1/ai', function () {
|
||||
|
||||
//豆包ai
|
||||
Route::group('doubao', function () {
|
||||
Route::post('text', 'app\ai\controller\DouBaoAI@text');
|
||||
Route::post('text', 'app\ai\controller\DouBaoAI@text'); // 文本生成
|
||||
Route::post('image', 'app\ai\controller\DouBaoAI@image'); // 图片生成
|
||||
});
|
||||
})->middleware(['jwt']);
|
||||
@@ -49,7 +49,7 @@ class DouBaoAI extends Controller
|
||||
],
|
||||
];
|
||||
}
|
||||
$result = requestCurl($this->apiUrl, $params, 'POST', $this->headers, 'json');
|
||||
$result = requestCurl($this->apiUrl.'/api/v3/chat/completions', $params, 'POST', $this->headers, 'json');
|
||||
$result = json_decode($result, true);
|
||||
if(isset($result['error'])){
|
||||
$error = $result['error'];
|
||||
@@ -64,5 +64,211 @@ class DouBaoAI extends Controller
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片生成功能(基于火山方舟 Seedream 4.0-4.5 API)
|
||||
* 参考文档:https://www.volcengine.com/docs/82379/1541523?lang=zh
|
||||
*
|
||||
* @param array $params 请求参数,如果为空则从请求中获取
|
||||
* @return string JSON格式的响应
|
||||
*/
|
||||
public function image($params = [])
|
||||
{
|
||||
try {
|
||||
// 如果参数为空,从请求中获取
|
||||
if (empty($params)){
|
||||
$content = $this->request->param('content', '');
|
||||
$model = $this->request->param('model', 'doubao-seedream-4-5-251128');
|
||||
$size = $this->request->param('size', '16:9'); // 支持档位(1K/2K/4K)、比例(16:9/9:16等)、像素(1280x720等)
|
||||
$responseFormat = $this->request->param('response_format', 'url'); // url 或 b64_json
|
||||
$sequentialImageGeneration = $this->request->param('sequential_image_generation', 'disabled'); // enabled 或 disabled
|
||||
$watermark = $this->request->param('watermark', true); // true 或 false
|
||||
|
||||
// 参数验证
|
||||
if(empty($content)){
|
||||
return json_encode(['code' => 500, 'msg' => '提示词(prompt)不能为空']);
|
||||
}
|
||||
|
||||
// 验证和规范化尺寸参数
|
||||
$size = $this->validateAndNormalizeSize($size);
|
||||
if(!in_array($responseFormat, ['url', 'b64_json'])){
|
||||
$responseFormat = 'url';
|
||||
}
|
||||
|
||||
if(!in_array($sequentialImageGeneration, ['enabled', 'disabled'])){
|
||||
$sequentialImageGeneration = 'disabled';
|
||||
}
|
||||
|
||||
// 构建请求参数(根据火山方舟文档)
|
||||
$params = [
|
||||
'model' => $model,
|
||||
'prompt' => $content,
|
||||
'sequential_image_generation' => $sequentialImageGeneration,
|
||||
'response_format' => $responseFormat,
|
||||
'size' => $size,
|
||||
'stream' => false,
|
||||
'watermark' => true
|
||||
];
|
||||
}
|
||||
|
||||
// 确保API URL正确(图片生成API的endpoint)
|
||||
$imageApiUrl = $this->apiUrl. '/api/v3/images/generations';
|
||||
// 发送请求
|
||||
$result = requestCurl($imageApiUrl, $params, 'POST', $this->headers, 'json');
|
||||
$result = json_decode($result, true);
|
||||
// 错误处理
|
||||
if(isset($result['error'])){
|
||||
$error = $result['error'];
|
||||
$errorMsg = isset($error['message']) ? $error['message'] : '图片生成失败';
|
||||
$errorCode = isset($error['code']) ? $error['code'] : 'unknown';
|
||||
|
||||
\think\facade\Log::error('火山方舟图片生成失败', [
|
||||
'error' => $error,
|
||||
'params' => $params
|
||||
]);
|
||||
|
||||
return json_encode([
|
||||
'code' => 500,
|
||||
'msg' => $errorMsg,
|
||||
'error_code' => $errorCode
|
||||
]);
|
||||
}
|
||||
|
||||
// 成功响应处理(根据火山方舟文档的响应格式)
|
||||
if(isset($result['data']) && is_array($result['data']) && !empty($result['data'])){
|
||||
$imageData = $result['data'][0];
|
||||
|
||||
// 根据 response_format 获取图片数据
|
||||
$imageUrl = null;
|
||||
$imageB64 = null;
|
||||
|
||||
if(isset($imageData['url'])){
|
||||
$imageUrl = $imageData['url'];
|
||||
}
|
||||
|
||||
if(isset($imageData['b64_json'])){
|
||||
$imageB64 = $imageData['b64_json'];
|
||||
}
|
||||
|
||||
// 计算token(如果有usage信息)
|
||||
$token = 0;
|
||||
if(isset($result['usage']['total_tokens'])){
|
||||
$token = intval($result['usage']['total_tokens']) * 20;
|
||||
}
|
||||
|
||||
// 构建返回数据
|
||||
$responseData = [
|
||||
'token' => $token,
|
||||
'image_url' => $imageUrl,
|
||||
'image_b64' => $imageB64,
|
||||
'model' => $params['model'] ?? '',
|
||||
'size' => $params['size'] ?? '2K',
|
||||
'created' => isset($result['created']) ? $result['created'] : time()
|
||||
];
|
||||
|
||||
// 根据请求的response_format返回对应的数据
|
||||
if($params['response_format'] == 'url' && $imageUrl){
|
||||
$responseData['content'] = $imageUrl;
|
||||
} elseif($params['response_format'] == 'b64_json' && $imageB64){
|
||||
$responseData['content'] = $imageB64;
|
||||
}
|
||||
|
||||
return json_encode([
|
||||
'code' => 200,
|
||||
'msg' => '图片生成成功',
|
||||
'data' => $responseData
|
||||
]);
|
||||
} else {
|
||||
// 响应格式不符合预期
|
||||
\think\facade\Log::warning('火山方舟图片生成响应格式异常', [
|
||||
'result' => $result,
|
||||
'params' => $params
|
||||
]);
|
||||
|
||||
return json_encode([
|
||||
'code' => 500,
|
||||
'msg' => '图片生成响应格式异常',
|
||||
'raw_response' => $result
|
||||
]);
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
\think\facade\Log::error('火山方舟图片生成异常', [
|
||||
'message' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
return json_encode([
|
||||
'code' => 500,
|
||||
'msg' => '图片生成异常:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证和规范化尺寸参数
|
||||
* 支持三种格式:
|
||||
* 1. 档位形式:1K, 2K, 4K(不区分大小写)
|
||||
* 2. 比例形式:16:9, 9:16, 1:1, 4:3, 3:4 等
|
||||
* 3. 像素形式:1280x720, 2048x2048 等(宽度1280-4096,高度720-4096,宽高比0.0625-16)
|
||||
*
|
||||
* @param string $size 尺寸参数
|
||||
* @return string 规范化后的尺寸值
|
||||
*/
|
||||
private function validateAndNormalizeSize($size)
|
||||
{
|
||||
if (empty($size)) {
|
||||
return '2K';
|
||||
}
|
||||
|
||||
$size = trim($size);
|
||||
|
||||
// 1. 检查是否为档位形式(1K, 2K, 4K)
|
||||
$sizeUpper = strtoupper($size);
|
||||
if (in_array($sizeUpper, ['1K', '2K', '4K'])) {
|
||||
return $sizeUpper;
|
||||
}
|
||||
|
||||
// 2. 检查是否为比例形式(如 16:9, 9:16, 1:1)
|
||||
if (preg_match('/^(\d+):(\d+)$/', $size, $matches)) {
|
||||
$width = intval($matches[1]);
|
||||
$height = intval($matches[2]);
|
||||
|
||||
if ($width > 0 && $height > 0) {
|
||||
$ratio = $width / $height;
|
||||
// 验证宽高比范围:0.0625 ~ 16
|
||||
if ($ratio >= 0.0625 && $ratio <= 16) {
|
||||
return $size; // 返回比例形式,如 "16:9"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 检查是否为像素形式(如 1280x720, 2048x2048)
|
||||
if (preg_match('/^(\d+)x(\d+)$/i', $size, $matches)) {
|
||||
$width = intval($matches[1]);
|
||||
$height = intval($matches[2]);
|
||||
|
||||
// 验证宽度范围:1280 ~ 4096
|
||||
if ($width < 1280 || $width > 4096) {
|
||||
return '2K'; // 默认返回 2K
|
||||
}
|
||||
|
||||
// 验证高度范围:720 ~ 4096
|
||||
if ($height < 720 || $height > 4096) {
|
||||
return '2K'; // 默认返回 2K
|
||||
}
|
||||
|
||||
// 验证宽高比范围:0.0625 ~ 16
|
||||
$ratio = $width / $height;
|
||||
if ($ratio < 0.0625 || $ratio > 16) {
|
||||
return '2K'; // 默认返回 2K
|
||||
}
|
||||
|
||||
return $size; // 返回像素形式,如 "1280x720"
|
||||
}
|
||||
|
||||
// 如果都不匹配,返回默认值
|
||||
return '2K';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace app\api\controller;
|
||||
|
||||
use app\api\model\WechatMessageModel;
|
||||
use app\common\service\FriendTransferService;
|
||||
use think\Db;
|
||||
use think\facade\Request;
|
||||
|
||||
@@ -389,38 +390,17 @@ class MessageController extends BaseController
|
||||
if ($item['msgType'] == 10000 && strpos($item['content'],'开启了朋友验证') !== false) {
|
||||
Db::table('s2_wechat_friend')->where('id',$item['wechatFriendId'])->update(['isDeleted'=> 1,'deleteTime' => $wechatTime]);
|
||||
}else{
|
||||
//优先分配在线客服
|
||||
//优先分配在线客服 - 使用新的好友迁移服务
|
||||
$friend = Db::table('s2_wechat_friend')->where('id',$item['wechatFriendId'])->find();
|
||||
if (!empty($friend)){
|
||||
$accountId = $item['accountId'];
|
||||
$accountData = Db::table('s2_company_account')->where('id',$accountId)->find();
|
||||
if (!empty($accountData)){
|
||||
$account = new AccountController();
|
||||
$account->getlist(['pageIndex' => 0,'pageSize' => 100,'departmentId' => $accountData['departmentId']]);
|
||||
$accountIds = Db::table('s2_company_account')->where(['departmentId' => $accountData['departmentId'],'alive' => 1])->column('id');
|
||||
if (!empty($accountIds)){
|
||||
if (!in_array($friend['accountId'],$accountIds)){
|
||||
// 执行切换好友命令
|
||||
$randomKey = array_rand($accountIds, 1);
|
||||
$toAccountId = $accountIds[$randomKey];
|
||||
$toAccountData = Db::table('s2_company_account')->where('id',$toAccountId)->find();
|
||||
$automaticAssign = new AutomaticAssign();
|
||||
$automaticAssign->allotWechatFriend([
|
||||
'wechatFriendId' => $friend['id'],
|
||||
'toAccountId' => $toAccountId
|
||||
], true);
|
||||
Db::table('s2_wechat_friend')
|
||||
->where('id',$friend['id'])
|
||||
->update([
|
||||
'accountId' => $toAccountId,
|
||||
'accountUserName' => $toAccountData['userName'],
|
||||
'accountRealName' => $toAccountData['realName'],
|
||||
'accountNickname' => $toAccountData['nickname'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
$friendTransferService = new FriendTransferService();
|
||||
$result = $friendTransferService->transferFriend(
|
||||
$item['wechatFriendId'],
|
||||
$accountId,
|
||||
'账号不在线,自动迁移到在线账号'
|
||||
);
|
||||
// 迁移结果已记录在服务中,这里不需要额外处理
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ use library\ResponseHelper;
|
||||
use app\api\model\WechatFriendModel;
|
||||
use app\api\model\WechatMessageModel;
|
||||
use app\api\controller\MessageController;
|
||||
use think\Db;
|
||||
|
||||
|
||||
class DataProcessing extends BaseController
|
||||
@@ -60,6 +61,7 @@ class DataProcessing extends BaseController
|
||||
$friend->conRemark = $newRemark;
|
||||
$friend->updateTime = time();
|
||||
$friend->save();
|
||||
|
||||
$msg = '修改备成功';
|
||||
break;
|
||||
case 'CmdModifyFriendLabel': //修改好友标签
|
||||
@@ -73,6 +75,7 @@ class DataProcessing extends BaseController
|
||||
$friend->labels = json_encode($labels,256);
|
||||
$friend->updateTime = time();
|
||||
$friend->save();
|
||||
|
||||
$msg = '修标签成功';
|
||||
break;
|
||||
case 'CmdAllotFriend': //迁移好友
|
||||
@@ -199,6 +202,7 @@ class DataProcessing extends BaseController
|
||||
$data->updateTime = time();
|
||||
$data->isTop = $isTop;
|
||||
$data->save();
|
||||
|
||||
break;
|
||||
}
|
||||
return ResponseHelper::success('',$msg,$codee);
|
||||
|
||||
@@ -30,143 +30,191 @@ class MessageController extends BaseController
|
||||
return ResponseHelper::error('请先登录');
|
||||
}
|
||||
|
||||
// 直接查询好友ID列表
|
||||
$ids = Db::table('s2_wechat_friend')
|
||||
->where(['accountId' => $accountId, 'isDeleted' => 0])
|
||||
->column('id');
|
||||
$friendIds = empty($ids) ? [0] : $ids; // 避免 IN 查询为空
|
||||
|
||||
// 直接查询好友信息
|
||||
$friends = Db::table('s2_wechat_friend')
|
||||
->where(['accountId' => $accountId, 'isDeleted' => 0])
|
||||
->column('id,nickname,avatar,conRemark,labels,groupId,wechatAccountId,wechatId,extendFields,phone,region,isTop');
|
||||
|
||||
|
||||
// 构建好友子查询
|
||||
$friendSubQuery = Db::table('s2_wechat_friend')
|
||||
// 直接查询群聊信息
|
||||
$chatrooms = Db::table('s2_wechat_chatroom')
|
||||
->where(['accountId' => $accountId, 'isDeleted' => 0])
|
||||
->field('id')
|
||||
->buildSql();
|
||||
->column('id,nickname,chatroomAvatar,chatroomId,isTop');
|
||||
|
||||
// 优化后的查询:使用MySQL兼容的查询方式
|
||||
$unionQuery = "
|
||||
(SELECT m.id, m.content, m.wechatFriendId, m.wechatChatroomId, m.createTime, m.wechatTime,m.wechatAccountId, 2 as msgType, wc.nickname, wc.chatroomAvatar as avatar, wc.chatroomId, wc.isTop
|
||||
FROM s2_wechat_chatroom wc
|
||||
INNER JOIN s2_wechat_message m ON wc.id = m.wechatChatroomId AND m.type = 2
|
||||
INNER JOIN (
|
||||
SELECT wechatChatroomId, MAX(wechatTime) as maxTime, MAX(id) as maxId
|
||||
FROM s2_wechat_message
|
||||
WHERE type = 2
|
||||
GROUP BY wechatChatroomId
|
||||
) latest ON m.wechatChatroomId = latest.wechatChatroomId AND m.wechatTime = latest.maxTime AND m.id = latest.maxId
|
||||
WHERE wc.accountId = {$accountId} AND wc.isDeleted = 0
|
||||
)
|
||||
UNION ALL
|
||||
(SELECT m.id, m.content, m.wechatFriendId, m.wechatChatroomId, m.createTime, m.wechatTime, 1 as msgType, 1 as nickname, 1 as avatar, 1 as chatroomId, 1 as wechatAccountId, 0 as isTop
|
||||
FROM s2_wechat_message m
|
||||
INNER JOIN (
|
||||
SELECT wechatFriendId, MAX(wechatTime) as maxTime, MAX(id) as maxId
|
||||
FROM s2_wechat_message
|
||||
WHERE type = 1 AND wechatFriendId IN {$friendSubQuery}
|
||||
GROUP BY wechatFriendId
|
||||
) latest ON m.wechatFriendId = latest.wechatFriendId AND m.wechatTime = latest.maxTime AND m.id = latest.maxId
|
||||
WHERE m.type = 1 AND m.wechatFriendId IN {$friendSubQuery}
|
||||
)
|
||||
ORDER BY wechatTime DESC
|
||||
LIMIT " . (($page - 1) * $limit) . ", {$limit}
|
||||
";
|
||||
// 获取群聊ID列表
|
||||
$chatroomIds = array_keys($chatrooms);
|
||||
if (empty($chatroomIds)) {
|
||||
$chatroomIds = [0];
|
||||
}
|
||||
|
||||
// 1. 查询群聊最新消息
|
||||
$chatroomMessages = [];
|
||||
if (!empty($chatroomIds) && $chatroomIds[0] != 0) {
|
||||
$chatroomIdsStr = implode(',', array_map('intval', $chatroomIds));
|
||||
$chatroomLatestQuery = "
|
||||
SELECT wc.id as chatroomId, m.id, m.content, m.wechatChatroomId, m.createTime, m.wechatTime, m.wechatAccountId,
|
||||
wc.nickname, wc.chatroomAvatar as avatar, wc.chatroomId, wc.isTop, 2 as msgType
|
||||
FROM s2_wechat_chatroom wc
|
||||
INNER JOIN (
|
||||
SELECT wechatChatroomId, MAX(wechatTime) as maxTime, MAX(id) as maxId
|
||||
FROM s2_wechat_message
|
||||
WHERE type = 2 AND wechatChatroomId IN ({$chatroomIdsStr})
|
||||
GROUP BY wechatChatroomId
|
||||
) latest ON wc.id = latest.wechatChatroomId
|
||||
INNER JOIN s2_wechat_message m ON m.wechatChatroomId = latest.wechatChatroomId
|
||||
AND m.wechatTime = latest.maxTime AND m.id = latest.maxId
|
||||
WHERE wc.accountId = {$accountId} AND wc.isDeleted = 0
|
||||
";
|
||||
$chatroomMessages = Db::query($chatroomLatestQuery);
|
||||
}
|
||||
|
||||
$list = Db::query($unionQuery);
|
||||
// 2. 查询好友最新消息
|
||||
$friendMessages = [];
|
||||
if (!empty($friendIds) && $friendIds[0] != 0) {
|
||||
$friendIdsStr = implode(',', array_map('intval', $friendIds));
|
||||
$friendLatestQuery = "
|
||||
SELECT m.wechatFriendId, m.id, m.content, m.createTime, m.wechatTime,
|
||||
f.wechatAccountId, 1 as msgType, 0 as isTop
|
||||
FROM s2_wechat_message m
|
||||
INNER JOIN (
|
||||
SELECT wechatFriendId, MAX(wechatTime) as maxTime, MAX(id) as maxId
|
||||
FROM s2_wechat_message
|
||||
WHERE type = 1 AND wechatFriendId IN ({$friendIdsStr})
|
||||
GROUP BY wechatFriendId
|
||||
) latest ON m.wechatFriendId = latest.wechatFriendId
|
||||
AND m.wechatTime = latest.maxTime AND m.id = latest.maxId
|
||||
INNER JOIN s2_wechat_friend f ON f.id = m.wechatFriendId
|
||||
WHERE m.type = 1 AND m.wechatFriendId IN ({$friendIdsStr})
|
||||
";
|
||||
$friendMessages = Db::query($friendLatestQuery);
|
||||
}
|
||||
|
||||
// 对分页后的结果进行排序(按wechatTime降序)
|
||||
usort($list, function ($a, $b) {
|
||||
// 合并结果并排序
|
||||
$allMessages = array_merge($chatroomMessages, $friendMessages);
|
||||
usort($allMessages, function ($a, $b) {
|
||||
return $b['wechatTime'] <=> $a['wechatTime'];
|
||||
});
|
||||
|
||||
// 批量统计未读数量(isRead=0),按好友/群聊分别聚合
|
||||
$friendIds = [];
|
||||
$chatroomIds = [];
|
||||
// 计算总数
|
||||
$totalCount = count($allMessages);
|
||||
|
||||
// 分页处理
|
||||
$list = array_slice($allMessages, ($page - 1) * $limit, $limit);
|
||||
|
||||
// 收集需要查询的ID
|
||||
$queryFriendIds = [];
|
||||
$queryChatroomIds = [];
|
||||
foreach ($list as $row) {
|
||||
if (!empty($row['wechatFriendId'])) {
|
||||
$friendIds[] = $row['wechatFriendId'];
|
||||
$queryFriendIds[] = $row['wechatFriendId'];
|
||||
}
|
||||
if (!empty($row['wechatChatroomId'])) {
|
||||
$chatroomIds[] = $row['wechatChatroomId'];
|
||||
$queryChatroomIds[] = $row['wechatChatroomId'];
|
||||
}
|
||||
}
|
||||
$friendIds = array_values(array_unique(array_filter($friendIds)));
|
||||
$chatroomIds = array_values(array_unique(array_filter($chatroomIds)));
|
||||
$queryFriendIds = array_unique($queryFriendIds);
|
||||
$queryChatroomIds = array_unique($queryChatroomIds);
|
||||
|
||||
$friendUnreadMap = [];
|
||||
if (!empty($friendIds)) {
|
||||
// 获取未读消息数量
|
||||
$friendUnreadMap = Db::table('s2_wechat_message')
|
||||
->where(['isRead' => 0])
|
||||
->whereIn('wechatFriendId', $friendIds)
|
||||
// 批量查询未读数量(优化:合并查询)
|
||||
$unreadMap = [];
|
||||
if (!empty($queryFriendIds)) {
|
||||
$friendUnreads = Db::table('s2_wechat_message')
|
||||
->where(['isRead' => 0, 'type' => 1])
|
||||
->whereIn('wechatFriendId', $queryFriendIds)
|
||||
->field('wechatFriendId, COUNT(*) as cnt')
|
||||
->group('wechatFriendId')
|
||||
->column('COUNT(*) AS cnt', 'wechatFriendId');
|
||||
->select();
|
||||
foreach ($friendUnreads as $item) {
|
||||
$unreadMap['friend_' . $item['wechatFriendId']] = (int)$item['cnt'];
|
||||
}
|
||||
}
|
||||
|
||||
$chatroomUnreadMap = [];
|
||||
if (!empty($chatroomIds)) {
|
||||
// 获取未读消息数量
|
||||
$chatroomUnreadMap = Db::table('s2_wechat_message')
|
||||
->where(['isRead' => 0])
|
||||
->whereIn('wechatChatroomId', $chatroomIds)
|
||||
if (!empty($queryChatroomIds)) {
|
||||
$chatroomUnreads = Db::table('s2_wechat_message')
|
||||
->where(['isRead' => 0, 'type' => 2])
|
||||
->whereIn('wechatChatroomId', $queryChatroomIds)
|
||||
->field('wechatChatroomId, COUNT(*) as cnt')
|
||||
->group('wechatChatroomId')
|
||||
->column('COUNT(*) AS cnt', 'wechatChatroomId');
|
||||
->select();
|
||||
foreach ($chatroomUnreads as $item) {
|
||||
$unreadMap['chatroom_' . $item['wechatChatroomId']] = (int)$item['cnt'];
|
||||
}
|
||||
}
|
||||
|
||||
// 批量查询AI类型
|
||||
$aiTypeData = [];
|
||||
if (!empty($friendIds)) {
|
||||
$aiTypeData = FriendSettings::where('friendId', 'in', $friendIds)->column('friendId,type');
|
||||
if (!empty($queryFriendIds)) {
|
||||
$aiTypeData = FriendSettings::where('friendId', 'in', $queryFriendIds)->column('friendId,type');
|
||||
}
|
||||
|
||||
// 格式化数据
|
||||
foreach ($list as $k => &$v) {
|
||||
|
||||
$createTime = !empty($v['createTime']) ? date('Y-m-d H:i:s', $v['createTime']) : '';
|
||||
$wechatTime = !empty($v['wechatTime']) ? date('Y-m-d H:i:s', $v['wechatTime']) : '';
|
||||
|
||||
|
||||
$unreadCount = 0;
|
||||
$v['aiType'] = 0;
|
||||
|
||||
if (!empty($v['wechatFriendId'])) {
|
||||
$v['nickname'] = !empty($friends[$v['wechatFriendId']]) ? $friends[$v['wechatFriendId']]['nickname'] : '';
|
||||
$v['avatar'] = !empty($friends[$v['wechatFriendId']]) ? $friends[$v['wechatFriendId']]['avatar'] : '';
|
||||
$v['conRemark'] = !empty($friends[$v['wechatFriendId']]) ? $friends[$v['wechatFriendId']]['conRemark'] : '';
|
||||
$v['groupId'] = !empty($friends[$v['wechatFriendId']]) ? $friends[$v['wechatFriendId']]['groupId'] : '';
|
||||
$v['wechatAccountId'] = !empty($friends[$v['wechatFriendId']]) ? $friends[$v['wechatFriendId']]['wechatAccountId'] : '';
|
||||
$v['wechatId'] = !empty($friends[$v['wechatFriendId']]) ? $friends[$v['wechatFriendId']]['wechatId'] : '';
|
||||
$v['extendFields'] = !empty($friends[$v['wechatFriendId']]) ? $friends[$v['wechatFriendId']]['extendFields'] : [];
|
||||
$v['region'] = !empty($friends[$v['wechatFriendId']]) ? $friends[$v['wechatFriendId']]['region'] : '';
|
||||
$v['phone'] = !empty($friends[$v['wechatFriendId']]) ? $friends[$v['wechatFriendId']]['phone'] : '';
|
||||
$v['isTop'] = !empty($friends[$v['wechatFriendId']]) ? $friends[$v['wechatFriendId']]['isTop'] : 0;
|
||||
$v['labels'] = !empty($friends[$v['wechatFriendId']]) ? json_decode($friends[$v['wechatFriendId']]['labels'], true) : [];
|
||||
// 好友消息
|
||||
$friendId = $v['wechatFriendId'];
|
||||
$friend = $friends[$friendId] ?? null;
|
||||
|
||||
$v['nickname'] = $friend['nickname'] ?? '';
|
||||
$v['avatar'] = $friend['avatar'] ?? '';
|
||||
$v['conRemark'] = $friend['conRemark'] ?? '';
|
||||
$v['groupId'] = $friend['groupId'] ?? '';
|
||||
$v['wechatAccountId'] = $friend['wechatAccountId'] ?? '';
|
||||
$v['wechatId'] = $friend['wechatId'] ?? '';
|
||||
$v['extendFields'] = $friend['extendFields'] ?? [];
|
||||
$v['region'] = $friend['region'] ?? '';
|
||||
$v['phone'] = $friend['phone'] ?? '';
|
||||
$v['isTop'] = $friend['isTop'] ?? 0;
|
||||
$v['labels'] = !empty($friend['labels']) ? json_decode($friend['labels'], true) : [];
|
||||
|
||||
$unreadCount = isset($friendUnreadMap[$v['wechatFriendId']]) ? (int)$friendUnreadMap[$v['wechatFriendId']] : 0;
|
||||
$v['aiType'] = isset($aiTypeData[$v['wechatFriendId']]) ? $aiTypeData[$v['wechatFriendId']] : 0;
|
||||
$unreadCount = $unreadMap['friend_' . $friendId] ?? 0;
|
||||
$v['aiType'] = $aiTypeData[$friendId] ?? 0;
|
||||
$v['id'] = $friendId;
|
||||
unset($v['chatroomId']);
|
||||
}
|
||||
|
||||
if (!empty($v['wechatChatroomId'])) {
|
||||
} elseif (!empty($v['wechatChatroomId'])) {
|
||||
// 群聊消息
|
||||
$chatroomId = $v['wechatChatroomId'];
|
||||
$chatroom = $chatrooms[$chatroomId] ?? null;
|
||||
|
||||
$v['nickname'] = $chatroom['nickname'] ?? '';
|
||||
$v['avatar'] = $chatroom['chatroomAvatar'] ?? '';
|
||||
$v['conRemark'] = '';
|
||||
$unreadCount = isset($chatroomUnreadMap[$v['wechatChatroomId']]) ? (int)$chatroomUnreadMap[$v['wechatChatroomId']] : 0;
|
||||
$v['isTop'] = $chatroom['isTop'] ?? 0;
|
||||
$v['chatroomId'] = $chatroom['chatroomId'] ?? '';
|
||||
|
||||
$unreadCount = $unreadMap['chatroom_' . $chatroomId] ?? 0;
|
||||
$v['id'] = $chatroomId;
|
||||
unset($v['wechatFriendId']);
|
||||
}
|
||||
|
||||
$v['id'] = !empty($v['wechatFriendId']) ? $v['wechatFriendId'] : $v['wechatChatroomId'];
|
||||
$v['config'] = [
|
||||
'top' => !empty($v['isTop']) ? true : false,
|
||||
'unreadCount' => $unreadCount,
|
||||
'chat' => true,
|
||||
'msgTime' => $v['wechatTime'],
|
||||
'msgTime' => $wechatTime,
|
||||
];
|
||||
$v['createTime'] = $createTime;
|
||||
$v['lastUpdateTime'] = $wechatTime;
|
||||
|
||||
// 最新消息内容已经在UNION查询中获取,直接使用
|
||||
$v['latestMessage'] = [
|
||||
'content' => $v['content'],
|
||||
'content' => $v['content'] ?? '',
|
||||
'wechatTime' => $wechatTime
|
||||
];
|
||||
|
||||
unset($v['wechatFriendId'], $v['wechatChatroomId'],$v['isTop']);
|
||||
|
||||
unset($v['wechatChatroomId'], $v['isTop'], $v['msgType']);
|
||||
}
|
||||
unset($v);
|
||||
return ResponseHelper::success($list);
|
||||
|
||||
return ResponseHelper::success(['list' => $list, 'total' => $totalCount]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -47,4 +47,7 @@ return [
|
||||
|
||||
// 统一任务调度器
|
||||
'scheduler:run' => 'app\command\TaskSchedulerCommand', // 统一任务调度器,支持多进程并发执行
|
||||
|
||||
// 检查未读/未回复消息并自动迁移好友
|
||||
'check:unread-message' => 'app\command\CheckUnreadMessageCommand', // 检查未读/未回复消息并自动迁移好友
|
||||
];
|
||||
|
||||
63
application/command/CheckUnreadMessageCommand.php
Normal file
63
application/command/CheckUnreadMessageCommand.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace app\command;
|
||||
|
||||
use app\common\service\FriendTransferService;
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\Output;
|
||||
use think\facade\Log;
|
||||
|
||||
/**
|
||||
* 检查未读/未回复消息并自动迁移好友命令
|
||||
*
|
||||
* 功能:
|
||||
* 1. 检查消息未读超过30分钟的好友
|
||||
* 2. 检查消息未回复超过30分钟的好友
|
||||
* 3. 自动迁移这些好友到其他在线账号
|
||||
*/
|
||||
class CheckUnreadMessageCommand extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('check:unread-message')
|
||||
->setDescription('检查未读/未回复消息并自动迁移好友')
|
||||
->addOption('minutes', 'm', \think\console\input\Option::VALUE_OPTIONAL, '未读/未回复分钟数,默认30分钟', 30)
|
||||
->addOption('page-size', 'p', \think\console\input\Option::VALUE_OPTIONAL, '每页处理数量,默认100条', 100);
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
$minutes = intval($input->getOption('minutes'));
|
||||
if ($minutes <= 0) {
|
||||
$minutes = 30;
|
||||
}
|
||||
|
||||
$pageSize = intval($input->getOption('page-size'));
|
||||
if ($pageSize <= 0) {
|
||||
$pageSize = 100;
|
||||
}
|
||||
|
||||
$output->writeln("开始检查未读/未回复消息(超过{$minutes}分钟,每页处理{$pageSize}条)...");
|
||||
|
||||
try {
|
||||
$friendTransferService = new FriendTransferService();
|
||||
$result = $friendTransferService->checkAndTransferUnreadOrUnrepliedFriends($minutes, $pageSize);
|
||||
|
||||
$output->writeln("检查完成:");
|
||||
$output->writeln(" 总计需要迁移的好友数:{$result['total']}");
|
||||
$output->writeln(" 成功迁移的好友数:{$result['transferred']}");
|
||||
$output->writeln(" 迁移失败的好友数:{$result['failed']}");
|
||||
|
||||
if ($result['total'] > 0) {
|
||||
Log::info("未读/未回复消息检查完成:总计{$result['total']},成功{$result['transferred']},失败{$result['failed']}");
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$errorMsg = "检查未读/未回复消息异常:" . $e->getMessage();
|
||||
$output->writeln("<error>{$errorMsg}</error>");
|
||||
Log::error($errorMsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ class TaskSchedulerCommand extends Command
|
||||
/**
|
||||
* 最大并发进程数
|
||||
*/
|
||||
protected $maxConcurrent = 10;
|
||||
protected $maxConcurrent = 20;
|
||||
|
||||
/**
|
||||
* 当前运行的进程数
|
||||
@@ -61,23 +61,48 @@ class TaskSchedulerCommand extends Command
|
||||
$this->maxConcurrent = 1;
|
||||
}
|
||||
|
||||
// 加载任务配置(优先使用框架配置,其次直接引入配置文件,避免加载失败)
|
||||
// 加载任务配置
|
||||
// 方法1:尝试通过框架配置加载
|
||||
$this->tasks = Config::get('task_scheduler', []);
|
||||
|
||||
// 如果通过 Config 没有读到,再尝试直接 include 配置文件
|
||||
|
||||
// 方法2:如果框架配置没有,直接加载配置文件
|
||||
if (empty($this->tasks)) {
|
||||
// 以项目根目录为基准查找 config/task_scheduler.php
|
||||
$configFile = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'task_scheduler.php';
|
||||
if (is_file($configFile)) {
|
||||
$config = include $configFile;
|
||||
if (is_array($config) && !empty($config)) {
|
||||
$this->tasks = $config;
|
||||
// 获取项目根目录
|
||||
if (!defined('ROOT_PATH')) {
|
||||
define('ROOT_PATH', dirname(__DIR__, 2));
|
||||
}
|
||||
|
||||
// 尝试多个可能的路径
|
||||
$possiblePaths = [
|
||||
ROOT_PATH . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'task_scheduler.php',
|
||||
__DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'task_scheduler.php',
|
||||
dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'task_scheduler.php',
|
||||
];
|
||||
|
||||
foreach ($possiblePaths as $configFile) {
|
||||
if (is_file($configFile)) {
|
||||
$output->writeln("<info>找到配置文件:{$configFile}</info>");
|
||||
$config = include $configFile;
|
||||
if (is_array($config) && !empty($config)) {
|
||||
$this->tasks = $config;
|
||||
break;
|
||||
} else {
|
||||
$output->writeln("<error>配置文件返回的不是数组或为空:{$configFile}</error>");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($this->tasks)) {
|
||||
$output->writeln('<error>错误:未找到任务配置(task_scheduler),请检查 config/task_scheduler.php 是否存在且返回数组</error>');
|
||||
$output->writeln('<error>错误:未找到任务配置(task_scheduler)</error>');
|
||||
$output->writeln('<error>请检查以下位置:</error>');
|
||||
$output->writeln('<error>1. config/task_scheduler.php 文件是否存在</error>');
|
||||
$output->writeln('<error>2. 文件是否返回有效的数组</error>');
|
||||
$output->writeln('<error>3. 文件权限是否正确</error>');
|
||||
if (defined('ROOT_PATH')) {
|
||||
$output->writeln('<error>项目根目录:' . ROOT_PATH . '</error>');
|
||||
$output->writeln('<error>期望配置文件:' . ROOT_PATH . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'task_scheduler.php</error>');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -104,16 +129,24 @@ class TaskSchedulerCommand extends Command
|
||||
|
||||
// 筛选需要执行的任务
|
||||
$tasksToRun = [];
|
||||
$enabledCount = 0;
|
||||
$disabledCount = 0;
|
||||
|
||||
foreach ($this->tasks as $taskId => $task) {
|
||||
if (!isset($task['enabled']) || !$task['enabled']) {
|
||||
$disabledCount++;
|
||||
continue;
|
||||
}
|
||||
$enabledCount++;
|
||||
|
||||
if ($this->shouldRun($task['schedule'], $currentMinute, $currentHour, $currentDay, $currentMonth, $currentWeekday)) {
|
||||
$tasksToRun[$taskId] = $task;
|
||||
$output->writeln("<info>任务 {$taskId} 符合执行条件(schedule: {$task['schedule']})</info>");
|
||||
}
|
||||
}
|
||||
|
||||
$output->writeln("已启用任务数: {$enabledCount},已禁用任务数: {$disabledCount}");
|
||||
|
||||
if (empty($tasksToRun)) {
|
||||
$output->writeln('<info>当前时间没有需要执行的任务</info>');
|
||||
return true;
|
||||
@@ -266,9 +299,36 @@ class TaskSchedulerCommand extends Command
|
||||
// 检查任务是否已经在运行(防止重复执行)
|
||||
$lockKey = "scheduler_task_lock:{$taskId}";
|
||||
$lockTime = Cache::get($lockKey);
|
||||
if ($lockTime && (time() - $lockTime) < 300) { // 5分钟内不重复执行
|
||||
$output->writeln("<comment>任务 {$taskId} 正在运行中,跳过</comment>");
|
||||
continue;
|
||||
|
||||
// 如果锁存在,检查进程是否真的在运行
|
||||
if ($lockTime) {
|
||||
$lockPid = Cache::get("scheduler_task_pid:{$taskId}");
|
||||
if ($lockPid) {
|
||||
// 检查进程是否真的在运行
|
||||
if (function_exists('posix_kill')) {
|
||||
// 使用 posix_kill(pid, 0) 检查进程是否存在(0信号不杀死进程,只检查)
|
||||
if (@posix_kill($lockPid, 0)) {
|
||||
$output->writeln("<comment>任务 {$taskId} 正在运行中(PID: {$lockPid}),跳过</comment>");
|
||||
continue;
|
||||
} else {
|
||||
// 进程不存在,清除锁
|
||||
Cache::rm($lockKey);
|
||||
Cache::rm("scheduler_task_pid:{$taskId}");
|
||||
}
|
||||
} else {
|
||||
// 如果没有 posix_kill,使用时间判断(2分钟内不重复执行)
|
||||
if ((time() - $lockTime) < 120) {
|
||||
$output->writeln("<comment>任务 {$taskId} 可能在运行中(2分钟内执行过),跳过</comment>");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 如果没有PID记录,使用时间判断(2分钟内不重复执行)
|
||||
if ((time() - $lockTime) < 120) {
|
||||
$output->writeln("<comment>任务 {$taskId} 可能在运行中(2分钟内执行过),跳过</comment>");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建子进程
|
||||
@@ -291,8 +351,9 @@ class TaskSchedulerCommand extends Command
|
||||
];
|
||||
$output->writeln("<info>启动任务:{$taskId} (PID: {$pid})</info>");
|
||||
|
||||
// 设置任务锁
|
||||
// 设置任务锁和PID
|
||||
Cache::set($lockKey, time(), 600); // 10分钟过期
|
||||
Cache::set("scheduler_task_pid:{$taskId}", $pid, 600); // 保存PID,10分钟过期
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,37 +398,51 @@ class TaskSchedulerCommand extends Command
|
||||
}
|
||||
|
||||
// 构建命令
|
||||
// 使用项目根目录下的 think 脚本(同命令行 php think)
|
||||
if (!defined('ROOT_PATH')) {
|
||||
define('ROOT_PATH', dirname(__DIR__, 2));
|
||||
// 使用指定的网站目录作为执行目录
|
||||
$executionPath = '/www/wwwroot/mckb_quwanzhi_com/Server';
|
||||
|
||||
// 获取 PHP 可执行文件路径
|
||||
$phpPath = PHP_BINARY ?: 'php';
|
||||
|
||||
// 获取 think 脚本路径(使用执行目录)
|
||||
$thinkPath = $executionPath . DIRECTORY_SEPARATOR . 'think';
|
||||
|
||||
// 检查 think 文件是否存在
|
||||
if (!is_file($thinkPath)) {
|
||||
$errorMsg = "错误:think 文件不存在:{$thinkPath}";
|
||||
Log::error($errorMsg);
|
||||
file_put_contents($logFile, $errorMsg . "\n", FILE_APPEND);
|
||||
return;
|
||||
}
|
||||
$thinkPath = ROOT_PATH . DIRECTORY_SEPARATOR . 'think';
|
||||
$command = "php {$thinkPath} {$task['command']}";
|
||||
|
||||
// 构建命令(使用绝对路径,确保在 Linux 上能正确执行)
|
||||
$command = escapeshellarg($phpPath) . ' ' . escapeshellarg($thinkPath) . ' ' . escapeshellarg($task['command']);
|
||||
if (!empty($task['options'])) {
|
||||
foreach ($task['options'] as $option) {
|
||||
$command .= ' ' . escapeshellarg($option);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加日志重定向
|
||||
// 添加日志重定向(在后台执行)
|
||||
$command .= " >> " . escapeshellarg($logFile) . " 2>&1";
|
||||
|
||||
// 记录任务开始
|
||||
$logMessage = "\n" . str_repeat('=', 60) . "\n";
|
||||
$logMessage .= "任务开始执行: {$taskId}\n";
|
||||
$logMessage .= "执行时间: " . date('Y-m-d H:i:s') . "\n";
|
||||
$logMessage .= "执行目录: {$executionPath}\n";
|
||||
$logMessage .= "命令: {$command}\n";
|
||||
$logMessage .= str_repeat('=', 60) . "\n";
|
||||
file_put_contents($logFile, $logMessage, FILE_APPEND);
|
||||
|
||||
// 执行命令
|
||||
// 执行命令(使用指定的执行目录,Linux 环境)
|
||||
$descriptorspec = [
|
||||
0 => ['file', (PHP_OS_FAMILY === 'Windows' ? 'NUL' : '/dev/null'), 'r'], // stdin
|
||||
0 => ['file', '/dev/null', 'r'], // stdin
|
||||
1 => ['file', $logFile, 'a'], // stdout
|
||||
2 => ['file', $logFile, 'a'], // stderr
|
||||
];
|
||||
|
||||
$process = @proc_open($command, $descriptorspec, $pipes, ROOT_PATH);
|
||||
$process = @proc_open($command, $descriptorspec, $pipes, $executionPath);
|
||||
|
||||
if (is_resource($process)) {
|
||||
// 关闭管道
|
||||
@@ -412,12 +487,8 @@ class TaskSchedulerCommand extends Command
|
||||
// 关闭进程
|
||||
proc_close($process);
|
||||
} else {
|
||||
// 如果 proc_open 失败,尝试直接执行(后台执行)
|
||||
if (PHP_OS_FAMILY === 'Windows') {
|
||||
pclose(popen("start /B " . $command, "r"));
|
||||
} else {
|
||||
exec($command . ' > /dev/null 2>&1 &');
|
||||
}
|
||||
// 如果 proc_open 失败,使用 exec 在后台执行(Linux 环境)
|
||||
exec("cd " . escapeshellarg($executionPath) . " && " . $command . ' > /dev/null 2>&1 &');
|
||||
}
|
||||
|
||||
$endTime = microtime(true);
|
||||
@@ -448,12 +519,17 @@ class TaskSchedulerCommand extends Command
|
||||
|
||||
if ($result == $pid || $result == -1) {
|
||||
// 进程已结束
|
||||
$taskId = $info['task_id'];
|
||||
unset($this->runningProcesses[$pid]);
|
||||
|
||||
// 清除任务锁和PID
|
||||
Cache::rm("scheduler_task_lock:{$taskId}");
|
||||
Cache::rm("scheduler_task_pid:{$taskId}");
|
||||
|
||||
$duration = time() - $info['start_time'];
|
||||
Log::info("子进程执行完成", [
|
||||
'pid' => $pid,
|
||||
'task' => $info['task_id'],
|
||||
'task' => $taskId,
|
||||
'duration' => $duration,
|
||||
]);
|
||||
}
|
||||
|
||||
480
application/common/service/FriendTransferService.php
Normal file
480
application/common/service/FriendTransferService.php
Normal file
@@ -0,0 +1,480 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use app\api\controller\AutomaticAssign;
|
||||
use think\Db;
|
||||
use think\facade\Log;
|
||||
|
||||
/**
|
||||
* 好友迁移服务类
|
||||
* 负责处理好友在不同账号之间的迁移逻辑
|
||||
*/
|
||||
class FriendTransferService
|
||||
{
|
||||
/**
|
||||
* 迁移好友到其他账号
|
||||
* @param int $wechatFriendId 微信好友ID
|
||||
* @param int $currentAccountId 当前账号ID
|
||||
* @param string $reason 迁移原因
|
||||
* @return array ['success' => bool, 'message' => string, 'toAccountId' => int|null]
|
||||
*/
|
||||
public function transferFriend($wechatFriendId, $currentAccountId, $reason = '')
|
||||
{
|
||||
try {
|
||||
// 获取好友信息
|
||||
$friend = Db::table('s2_wechat_friend')->where('id', $wechatFriendId)->find();
|
||||
if (empty($friend)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => '好友不存在',
|
||||
'toAccountId' => null
|
||||
];
|
||||
}
|
||||
|
||||
// 获取当前账号的部门信息
|
||||
$accountData = Db::table('s2_company_account')->where('id', $currentAccountId)->find();
|
||||
if (empty($accountData)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => '当前账号不存在',
|
||||
'toAccountId' => null
|
||||
];
|
||||
}
|
||||
|
||||
// 获取同部门的在线账号列表
|
||||
$accountIds = Db::table('s2_company_account')
|
||||
->where([
|
||||
'departmentId' => $accountData['departmentId'],
|
||||
'alive' => 1
|
||||
])
|
||||
->column('id');
|
||||
if (empty($accountIds)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => '没有可用的在线账号',
|
||||
'toAccountId' => null
|
||||
];
|
||||
}
|
||||
|
||||
// 如果好友当前账号不在可用账号列表中,或者需要迁移到其他账号
|
||||
$needTransfer = !in_array($friend['accountId'], $accountIds);
|
||||
|
||||
// 如果需要迁移,选择目标账号
|
||||
if ($needTransfer || $currentAccountId != $friend['accountId']) {
|
||||
// 排除当前账号,选择其他账号
|
||||
$availableAccountIds = array_filter($accountIds, function($id) use ($currentAccountId) {
|
||||
return $id != $currentAccountId;
|
||||
});
|
||||
|
||||
if (empty($availableAccountIds)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => '没有其他可用的在线账号',
|
||||
'toAccountId' => null
|
||||
];
|
||||
}
|
||||
|
||||
// 随机选择一个账号
|
||||
$availableAccountIds = array_values($availableAccountIds);
|
||||
$randomKey = array_rand($availableAccountIds, 1);
|
||||
$toAccountId = $availableAccountIds[$randomKey];
|
||||
|
||||
// 获取目标账号信息
|
||||
$toAccountData = Db::table('s2_company_account')->where('id', $toAccountId)->find();
|
||||
if (empty($toAccountData)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => '目标账号不存在',
|
||||
'toAccountId' => null
|
||||
];
|
||||
}
|
||||
|
||||
// 执行迁移
|
||||
$automaticAssign = new AutomaticAssign();
|
||||
$result = $automaticAssign->allotWechatFriend([
|
||||
'wechatFriendId' => $wechatFriendId,
|
||||
'toAccountId' => $toAccountId
|
||||
], true);
|
||||
|
||||
$resultData = json_decode($result, true);
|
||||
|
||||
if (isset($resultData['code']) && $resultData['code'] == 200) {
|
||||
// 更新好友的账号信息
|
||||
Db::table('s2_wechat_friend')
|
||||
->where('id', $wechatFriendId)
|
||||
->update([
|
||||
'accountId' => $toAccountId,
|
||||
'accountUserName' => $toAccountData['userName'],
|
||||
'accountRealName' => $toAccountData['realName'],
|
||||
'accountNickname' => $toAccountData['nickname'],
|
||||
]);
|
||||
|
||||
$logMessage = "好友迁移成功:好友ID={$wechatFriendId},从账号{$currentAccountId}迁移到账号{$toAccountId}";
|
||||
if (!empty($reason)) {
|
||||
$logMessage .= ",原因:{$reason}";
|
||||
}
|
||||
Log::info($logMessage);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => '好友迁移成功',
|
||||
'toAccountId' => $toAccountId
|
||||
];
|
||||
} else {
|
||||
$errorMsg = isset($resultData['msg']) ? $resultData['msg'] : '迁移失败';
|
||||
Log::error("好友迁移失败:好友ID={$wechatFriendId},错误:{$errorMsg}");
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => $errorMsg,
|
||||
'toAccountId' => null
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => '好友已在正确的账号上,无需迁移',
|
||||
'toAccountId' => $friend['accountId']
|
||||
];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error("好友迁移异常:好友ID={$wechatFriendId},错误:" . $e->getMessage());
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => '迁移异常:' . $e->getMessage(),
|
||||
'toAccountId' => null
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量迁移好友到其他账号(按账号分组处理)
|
||||
* @param array $friends 好友列表,格式:[['friendId' => int, 'accountId' => int], ...]
|
||||
* @param int $currentAccountId 当前账号ID
|
||||
* @param string $reason 迁移原因
|
||||
* @return array ['transferred' => int, 'failed' => int]
|
||||
*/
|
||||
public function transferFriendsBatch($friends, $currentAccountId, $reason = '')
|
||||
{
|
||||
$transferred = 0;
|
||||
$failed = 0;
|
||||
|
||||
if (empty($friends)) {
|
||||
return ['transferred' => 0, 'failed' => 0];
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取当前账号的部门信息
|
||||
$accountData = Db::table('s2_company_account')->where('id', $currentAccountId)->find();
|
||||
if (empty($accountData)) {
|
||||
Log::error("批量迁移失败:当前账号不存在,账号ID={$currentAccountId}");
|
||||
return ['transferred' => 0, 'failed' => count($friends)];
|
||||
}
|
||||
|
||||
// 获取同部门的在线账号列表
|
||||
$accountIds = Db::table('s2_company_account')
|
||||
->where([
|
||||
'departmentId' => $accountData['departmentId'],
|
||||
'alive' => 1
|
||||
])
|
||||
->column('id');
|
||||
if (empty($accountIds)) {
|
||||
Log::warning("批量迁移失败:没有可用的在线账号,账号ID={$currentAccountId}");
|
||||
return ['transferred' => 0, 'failed' => count($friends)];
|
||||
}
|
||||
// 排除当前账号,选择其他账号
|
||||
$availableAccountIds = array_filter($accountIds, function($id) use ($currentAccountId) {
|
||||
return $id != $currentAccountId;
|
||||
});
|
||||
if (empty($availableAccountIds)) {
|
||||
Log::warning("批量迁移失败:没有其他可用的在线账号,账号ID={$currentAccountId}");
|
||||
return ['transferred' => 0, 'failed' => count($friends)];
|
||||
}
|
||||
|
||||
// 随机选择一个目标账号(同一批次使用同一个目标账号)
|
||||
$availableAccountIds = array_values($availableAccountIds);
|
||||
$randomKey = array_rand($availableAccountIds, 1);
|
||||
$toAccountId = $availableAccountIds[$randomKey];
|
||||
|
||||
// 获取目标账号信息
|
||||
$toAccountData = Db::table('s2_company_account')->where('id', $toAccountId)->find();
|
||||
if (empty($toAccountData)) {
|
||||
Log::error("批量迁移失败:目标账号不存在,账号ID={$toAccountId}");
|
||||
return ['transferred' => 0, 'failed' => count($friends)];
|
||||
}
|
||||
|
||||
// 批量获取好友信息
|
||||
$friendIds = array_column($friends, 'friendId');
|
||||
$friendList = Db::table('s2_wechat_friend')
|
||||
->where('id', 'in', $friendIds)
|
||||
->select();
|
||||
|
||||
$friendMap = [];
|
||||
foreach ($friendList as $friend) {
|
||||
$friendMap[$friend['id']] = $friend;
|
||||
}
|
||||
|
||||
// 批量执行迁移
|
||||
$automaticAssign = new AutomaticAssign();
|
||||
$updateData = [];
|
||||
|
||||
foreach ($friends as $friendItem) {
|
||||
$wechatFriendId = $friendItem['friendId'];
|
||||
|
||||
if (!isset($friendMap[$wechatFriendId])) {
|
||||
$failed++;
|
||||
Log::warning("批量迁移失败:好友不存在,好友ID={$wechatFriendId}");
|
||||
continue;
|
||||
}
|
||||
|
||||
$friend = $friendMap[$wechatFriendId];
|
||||
|
||||
// 如果好友当前账号不在可用账号列表中,或者需要迁移到其他账号
|
||||
$needTransfer = !in_array($friend['accountId'], $accountIds) || $currentAccountId != $friend['accountId'];
|
||||
|
||||
if ($needTransfer) {
|
||||
// 执行迁移
|
||||
$result = $automaticAssign->allotWechatFriend([
|
||||
'wechatFriendId' => $wechatFriendId,
|
||||
'toAccountId' => $toAccountId
|
||||
], true);
|
||||
|
||||
$resultData = json_decode($result, true);
|
||||
|
||||
if (isset($resultData['code']) && $resultData['code'] == 200) {
|
||||
// 收集需要更新的数据
|
||||
$updateData[] = [
|
||||
'id' => $wechatFriendId,
|
||||
'accountId' => $toAccountId,
|
||||
'accountUserName' => $toAccountData['userName'],
|
||||
'accountRealName' => $toAccountData['realName'],
|
||||
'accountNickname' => $toAccountData['nickname'],
|
||||
];
|
||||
$transferred++;
|
||||
} else {
|
||||
$errorMsg = isset($resultData['msg']) ? $resultData['msg'] : '迁移失败';
|
||||
$failed++;
|
||||
Log::warning("批量迁移失败:好友ID={$wechatFriendId},错误:{$errorMsg}");
|
||||
}
|
||||
} else {
|
||||
// 无需迁移
|
||||
$transferred++;
|
||||
}
|
||||
}
|
||||
|
||||
// 批量更新好友的账号信息
|
||||
if (!empty($updateData)) {
|
||||
foreach ($updateData as $data) {
|
||||
Db::table('s2_wechat_friend')
|
||||
->where('id', $data['id'])
|
||||
->update([
|
||||
'accountId' => $data['accountId'],
|
||||
'accountUserName' => $data['accountUserName'],
|
||||
'accountRealName' => $data['accountRealName'],
|
||||
'accountNickname' => $data['accountNickname'],
|
||||
]);
|
||||
}
|
||||
|
||||
$logMessage = "批量迁移成功:账号ID={$currentAccountId},共" . count($updateData) . "个好友迁移到账号{$toAccountId}";
|
||||
if (!empty($reason)) {
|
||||
$logMessage .= ",原因:{$reason}";
|
||||
}
|
||||
Log::info($logMessage);
|
||||
}
|
||||
|
||||
return [
|
||||
'transferred' => $transferred,
|
||||
'failed' => $failed
|
||||
];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error("批量迁移异常:账号ID={$currentAccountId},错误:" . $e->getMessage());
|
||||
return [
|
||||
'transferred' => $transferred,
|
||||
'failed' => count($friends) - $transferred
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查并迁移未读或未回复的好友
|
||||
* @param int $unreadMinutes 未读分钟数,默认30分钟
|
||||
* @param int $pageSize 每页处理数量,默认100
|
||||
* @return array ['total' => int, 'transferred' => int, 'failed' => int]
|
||||
*/
|
||||
public function checkAndTransferUnreadOrUnrepliedFriends($unreadMinutes = 30, $pageSize = 100)
|
||||
{
|
||||
$total = 0;
|
||||
$transferred = 0;
|
||||
$failed = 0;
|
||||
|
||||
try {
|
||||
$currentTime = time();
|
||||
$timeThreshold = $currentTime - ($unreadMinutes * 60); // 超过指定分钟数的时间点
|
||||
$last24Hours = $currentTime - (24 * 60 * 60); // 近24小时的时间点
|
||||
|
||||
// 确保每页数量合理
|
||||
$pageSize = max(1, min(1000, intval($pageSize)));
|
||||
|
||||
// 查询需要迁移的好友
|
||||
// 条件:以消息表为主表,查询近24小时内的消息
|
||||
// 1. 最后一条消息是用户发送的消息(isSend=0)
|
||||
// 2. 消息时间在近24小时内
|
||||
// 3. 消息时间超过指定分钟数(默认30分钟)
|
||||
// 4. 在这条用户消息之后,客服没有发送任何回复
|
||||
// 即:用户发送了消息,但客服超过30分钟没有回复,需要迁移给其他客服处理
|
||||
|
||||
// SQL逻辑说明(以消息表为主表):
|
||||
// 1. 从消息表开始,筛选近24小时内的用户消息(isSend=0)
|
||||
// 2. 找到每个好友的最后一条用户消息(通过MAX(id))
|
||||
// 3. 这条消息的时间超过指定分钟数(wm.wechatTime <= timeThreshold)
|
||||
// 4. 在这条用户消息之后,客服没有发送任何回复(NOT EXISTS isSend=1的消息)
|
||||
// 5. 关联好友表,确保好友未删除且已分配账号
|
||||
|
||||
// 先统计总数
|
||||
$countSql = "
|
||||
SELECT COUNT(DISTINCT wm.wechatFriendId) as total
|
||||
FROM s2_wechat_message wm
|
||||
INNER JOIN (
|
||||
SELECT wechatFriendId, MAX(id) as maxId
|
||||
FROM s2_wechat_message
|
||||
WHERE type = 1
|
||||
AND isSend = 0 -- 用户发送的消息
|
||||
AND wechatTime >= ? -- 近24小时内的消息
|
||||
GROUP BY wechatFriendId
|
||||
) last_msg ON wm.id = last_msg.maxId
|
||||
INNER JOIN s2_wechat_friend wf ON wf.id = wm.wechatFriendId
|
||||
WHERE wm.type = 1
|
||||
AND wm.isSend = 0 -- 最后一条消息是用户发送的(客服接收的)
|
||||
AND wm.wechatTime >= ? -- 近24小时内的消息
|
||||
AND wm.wechatTime <= ? -- 超过指定时间(默认30分钟)
|
||||
AND wf.isDeleted = 0
|
||||
AND wf.accountId IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
-- 检查在这条用户消息之后,是否有客服的回复
|
||||
SELECT 1
|
||||
FROM s2_wechat_message
|
||||
WHERE wechatFriendId = wm.wechatFriendId
|
||||
AND type = 1
|
||||
AND isSend = 1 -- 客服发送的消息
|
||||
AND wechatTime > wm.wechatTime -- 在用户消息之后
|
||||
)
|
||||
";
|
||||
|
||||
$countResult = Db::query($countSql, [$last24Hours, $last24Hours, $timeThreshold]);
|
||||
$total = isset($countResult[0]['total']) ? intval($countResult[0]['total']) : 0;
|
||||
|
||||
if ($total == 0) {
|
||||
Log::info("未找到需要迁移的未读/未回复好友(近24小时内)");
|
||||
return [
|
||||
'total' => 0,
|
||||
'transferred' => 0,
|
||||
'failed' => 0
|
||||
];
|
||||
}
|
||||
|
||||
Log::info("开始检查未读/未回复好友(近24小时内),共找到 {$total} 个需要迁移的好友,将分页处理(每页{$pageSize}条)");
|
||||
|
||||
// 分页处理
|
||||
$page = 1;
|
||||
$processed = 0;
|
||||
|
||||
do {
|
||||
$offset = ($page - 1) * $pageSize;
|
||||
|
||||
$sql = "
|
||||
SELECT DISTINCT
|
||||
wf.id as friendId,
|
||||
wf.accountId,
|
||||
wm.wechatAccountId,
|
||||
wm.wechatTime,
|
||||
wm.id as lastMessageId
|
||||
FROM s2_wechat_message wm
|
||||
INNER JOIN (
|
||||
SELECT wechatFriendId, MAX(id) as maxId
|
||||
FROM s2_wechat_message
|
||||
WHERE type = 1
|
||||
AND isSend = 0 -- 用户发送的消息
|
||||
AND wechatTime >= ? -- 近24小时内的消息
|
||||
GROUP BY wechatFriendId
|
||||
) last_msg ON wm.id = last_msg.maxId
|
||||
INNER JOIN s2_wechat_friend wf ON wf.id = wm.wechatFriendId
|
||||
WHERE wm.type = 1
|
||||
AND wm.isSend = 0 -- 最后一条消息是用户发送的(客服接收的)
|
||||
AND wm.wechatTime >= ? -- 近24小时内的消息
|
||||
AND wm.wechatTime <= ? -- 超过指定时间(默认30分钟)
|
||||
AND wf.isDeleted = 0
|
||||
AND wf.accountId IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
-- 检查在这条用户消息之后,是否有客服的回复
|
||||
SELECT 1
|
||||
FROM s2_wechat_message
|
||||
WHERE wechatFriendId = wm.wechatFriendId
|
||||
AND type = 1
|
||||
AND isSend = 1 -- 客服发送的消息
|
||||
AND wechatTime > wm.wechatTime -- 在用户消息之后
|
||||
)
|
||||
ORDER BY wf.accountId ASC, wm.id ASC
|
||||
LIMIT ? OFFSET ?
|
||||
";
|
||||
|
||||
$friends = Db::query($sql, [$last24Hours, $last24Hours, $timeThreshold, $pageSize, $offset]);
|
||||
$currentPageCount = count($friends);
|
||||
|
||||
if ($currentPageCount == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
Log::info("处理第 {$page} 页,本页 {$currentPageCount} 条记录");
|
||||
|
||||
// 按 accountId 分组
|
||||
$friendsByAccount = [];
|
||||
foreach ($friends as $friend) {
|
||||
$accountId = $friend['accountId'];
|
||||
if (!isset($friendsByAccount[$accountId])) {
|
||||
$friendsByAccount[$accountId] = [];
|
||||
}
|
||||
$friendsByAccount[$accountId][] = $friend;
|
||||
}
|
||||
// 按账号分组批量处理
|
||||
foreach ($friendsByAccount as $accountId => $accountFriends) {
|
||||
$batchResult = $this->transferFriendsBatch(
|
||||
$accountFriends,
|
||||
$accountId,
|
||||
"消息未读或未回复超过{$unreadMinutes}分钟"
|
||||
);
|
||||
|
||||
$transferred += $batchResult['transferred'];
|
||||
$failed += $batchResult['failed'];
|
||||
$processed += count($accountFriends);
|
||||
|
||||
Log::info("账号 {$accountId} 批量迁移完成:成功{$batchResult['transferred']},失败{$batchResult['failed']},共" . count($accountFriends) . "个好友");
|
||||
}
|
||||
|
||||
$page++;
|
||||
|
||||
// 每处理一页后记录进度
|
||||
Log::info("已处理 {$processed}/{$total} 条记录,成功:{$transferred},失败:{$failed}");
|
||||
|
||||
} while ($currentPageCount == $pageSize && $processed < $total);
|
||||
|
||||
Log::info("未读/未回复好友迁移完成:总计{$total},成功{$transferred},失败{$failed}");
|
||||
|
||||
return [
|
||||
'total' => $total,
|
||||
'transferred' => $transferred,
|
||||
'failed' => $failed
|
||||
];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error("检查未读/未回复好友异常:" . $e->getMessage());
|
||||
return [
|
||||
'total' => $total,
|
||||
'transferred' => $transferred,
|
||||
'failed' => $failed
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,12 @@ class GetAddResultedV1Controller extends BaseController
|
||||
$deviceIds = $this->getAllDevicesIdWithInCompany($companyId) ?: [0];
|
||||
|
||||
// 从 s2_device 导入数据。
|
||||
$this->getNewDeviceFromS2_device($deviceIds, $companyId);
|
||||
$newDeviceIds = $this->getNewDeviceFromS2_device($deviceIds, $companyId);
|
||||
|
||||
// 如果有新设备,自动加入到全局配置中
|
||||
if (!empty($newDeviceIds)) {
|
||||
$this->addDevicesToGlobalConfigs($newDeviceIds, $companyId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,12 +62,23 @@ class GetAddResultedV1Controller extends BaseController
|
||||
*
|
||||
* @param array $ids
|
||||
* @param int $companyId
|
||||
* @return void
|
||||
* @return array 返回新添加的设备ID数组
|
||||
*/
|
||||
protected function getNewDeviceFromS2_device(array $ids, int $companyId): void
|
||||
protected function getNewDeviceFromS2_device(array $ids, int $companyId): array
|
||||
{
|
||||
$ids = implode(',', $ids);
|
||||
|
||||
// 先查询要插入的新设备ID
|
||||
$newDeviceIds = Db::query("SELECT d.id
|
||||
FROM s2_device d
|
||||
JOIN s2_company_account a ON d.currentAccountId = a.id
|
||||
WHERE isDeleted = 0 AND deletedAndStop = 0 AND d.id NOT IN ({$ids}) AND a.departmentId = {$companyId}");
|
||||
$newDeviceIds = array_column($newDeviceIds, 'id');
|
||||
|
||||
if (empty($newDeviceIds)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$sql = "INSERT INTO ck_device(`id`, `imei`, `model`, phone, operatingSystem, memo, alive, brand, rooted, xPosed, softwareVersion, extra, createTime, updateTime, deleteTime, companyId)
|
||||
SELECT
|
||||
d.id, d.imei, d.model, d.phone, d.operatingSystem, d.memo, d.alive, d.brand, d.rooted, d.xPosed, d.softwareVersion, d.extra, d.createTime, d.lastUpdateTime, d.deleteTime, a.departmentId AS companyId
|
||||
@@ -86,6 +102,8 @@ class GetAddResultedV1Controller extends BaseController
|
||||
companyId = VALUES(companyId)";
|
||||
|
||||
Db::query($sql);
|
||||
|
||||
return $newDeviceIds;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -162,4 +180,144 @@ class GetAddResultedV1Controller extends BaseController
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将新设备自动加入到全局配置中(planType=0的计划和工作台)
|
||||
*
|
||||
* @param array $newDeviceIds 新添加的设备ID数组
|
||||
* @param int $companyId 公司ID
|
||||
* @return void
|
||||
*/
|
||||
protected function addDevicesToGlobalConfigs(array $newDeviceIds, int $companyId): void
|
||||
{
|
||||
try {
|
||||
// 1. 更新全局计划(场景获客)的设备组
|
||||
$this->addDevicesToGlobalPlans($newDeviceIds, $companyId);
|
||||
|
||||
// 2. 更新全局工作台的设备组
|
||||
$this->addDevicesToGlobalWorkbenches($newDeviceIds, $companyId);
|
||||
} catch (\Exception $e) {
|
||||
// 记录错误但不影响设备添加流程
|
||||
\think\facade\Log::error('自动添加设备到全局配置失败:' . $e->getMessage(), [
|
||||
'newDeviceIds' => $newDeviceIds,
|
||||
'companyId' => $companyId
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将新设备加入到全局计划(planType=0)的设备组
|
||||
*
|
||||
* @param array $newDeviceIds 新添加的设备ID数组
|
||||
* @param int $companyId 公司ID
|
||||
* @return void
|
||||
*/
|
||||
protected function addDevicesToGlobalPlans(array $newDeviceIds, int $companyId): void
|
||||
{
|
||||
// 查询所有全局计划(planType=0)
|
||||
$plans = Db::name('customer_acquisition_task')
|
||||
->where('companyId', $companyId)
|
||||
->where('planType', 0) // 全局计划
|
||||
->where('deleteTime', 0)
|
||||
->field('id,reqConf')
|
||||
->select();
|
||||
|
||||
foreach ($plans as $plan) {
|
||||
$reqConf = json_decode($plan['reqConf'], true) ?: [];
|
||||
$deviceGroups = isset($reqConf['device']) ? $reqConf['device'] : [];
|
||||
|
||||
if (!is_array($deviceGroups)) {
|
||||
$deviceGroups = [];
|
||||
}
|
||||
|
||||
// 合并新设备ID(去重)
|
||||
$deviceGroups = array_unique(array_merge($deviceGroups, $newDeviceIds));
|
||||
$reqConf['device'] = array_values($deviceGroups); // 重新索引数组
|
||||
|
||||
// 更新数据库
|
||||
Db::name('customer_acquisition_task')
|
||||
->where('id', $plan['id'])
|
||||
->update([
|
||||
'reqConf' => json_encode($reqConf, JSON_UNESCAPED_UNICODE),
|
||||
'updateTime' => time()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将新设备加入到全局工作台(planType=0)的设备组
|
||||
*
|
||||
* @param array $newDeviceIds 新添加的设备ID数组
|
||||
* @param int $companyId 公司ID
|
||||
* @return void
|
||||
*/
|
||||
protected function addDevicesToGlobalWorkbenches(array $newDeviceIds, int $companyId): void
|
||||
{
|
||||
// 查询所有全局工作台(planType=0)
|
||||
$workbenches = Db::name('workbench')
|
||||
->where('companyId', $companyId)
|
||||
->where('planType', 0) // 全局工作台
|
||||
->where('isDel', 0)
|
||||
->field('id,type')
|
||||
->select();
|
||||
|
||||
foreach ($workbenches as $workbench) {
|
||||
// 根据工作台类型更新对应的配置表
|
||||
$this->updateWorkbenchDevices($workbench['id'], $workbench['type'], $newDeviceIds);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新工作台的设备组
|
||||
*
|
||||
* @param int $workbenchId 工作台ID
|
||||
* @param int $type 工作台类型
|
||||
* @param array $newDeviceIds 新设备ID数组
|
||||
* @return void
|
||||
*/
|
||||
protected function updateWorkbenchDevices(int $workbenchId, int $type, array $newDeviceIds): void
|
||||
{
|
||||
$configTableMap = [
|
||||
1 => 'workbench_auto_like', // 自动点赞
|
||||
2 => 'workbench_moments_sync', // 朋友圈同步
|
||||
3 => 'workbench_group_push', // 群消息推送
|
||||
4 => 'workbench_group_create', // 自动建群
|
||||
5 => 'workbench_traffic_config', // 流量分发
|
||||
6 => 'workbench_import_contact', // 通讯录导入
|
||||
7 => 'workbench_group_welcome', // 入群欢迎语
|
||||
];
|
||||
|
||||
$tableName = $configTableMap[$type] ?? null;
|
||||
if (empty($tableName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 查询配置
|
||||
$config = Db::name($tableName)
|
||||
->where('workbenchId', $workbenchId)
|
||||
->field('id,devices')
|
||||
->find();
|
||||
|
||||
if (empty($config)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 解析设备组
|
||||
$deviceGroups = json_decode($config['devices'], true) ?: [];
|
||||
if (!is_array($deviceGroups)) {
|
||||
$deviceGroups = [];
|
||||
}
|
||||
|
||||
// 合并新设备ID(去重)
|
||||
$deviceGroups = array_unique(array_merge($deviceGroups, $newDeviceIds));
|
||||
$deviceGroups = array_values($deviceGroups); // 重新索引数组
|
||||
|
||||
// 更新数据库
|
||||
Db::name($tableName)
|
||||
->where('id', $config['id'])
|
||||
->update([
|
||||
'devices' => json_encode($deviceGroups, JSON_UNESCAPED_UNICODE),
|
||||
'updateTime' => time()
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -287,6 +287,13 @@ class GetAddFriendPlanDetailV1Controller extends Controller
|
||||
$newData['messagePlans'] = $msgConf;
|
||||
$newData = array_merge($newData, $sceneConf, $reqConf, $tagConf, $plan);
|
||||
|
||||
// 确保 planType 有默认值(0=全局,1=独立,默认1)
|
||||
if (!isset($newData['planType'])) {
|
||||
$newData['planType'] = 1;
|
||||
} else {
|
||||
$newData['planType'] = intval($newData['planType']);
|
||||
}
|
||||
|
||||
// 移除不需要的字段
|
||||
unset(
|
||||
$newData['sceneConf'],
|
||||
|
||||
@@ -61,6 +61,13 @@ class PlanSceneV1Controller extends BaseController
|
||||
$val['reqConf'] = json_decode($val['reqConf'],true) ?: [];
|
||||
$val['msgConf'] = json_decode($val['msgConf'],true) ?: [];
|
||||
$val['tagConf'] = json_decode($val['tagConf'],true) ?: [];
|
||||
|
||||
// 确保 planType 有默认值(0=全局,1=独立,默认1)
|
||||
if (!isset($val['planType'])) {
|
||||
$val['planType'] = 1;
|
||||
} else {
|
||||
$val['planType'] = intval($val['planType']);
|
||||
}
|
||||
|
||||
$stats = $statsMap[$val['id']] ?? [
|
||||
'acquiredCount' => 0,
|
||||
|
||||
@@ -150,6 +150,8 @@ class PostCreateAddFriendPlanV1Controller extends BaseController
|
||||
'userId' => $this->getUserInfo('id'),
|
||||
'companyId' => $this->getUserInfo('companyId'),
|
||||
'status' => !empty($params['status']) ? 1 : 0,
|
||||
// 计划类型:0=全局,1=独立(默认)
|
||||
'planType' => isset($params['planType']) ? intval($params['planType']) : 1,
|
||||
// 拉群配置
|
||||
'groupInviteEnabled' => $groupInviteEnabled,
|
||||
'groupName' => $params['groupName'] ?? '',
|
||||
|
||||
@@ -125,6 +125,8 @@ class PostUpdateAddFriendPlanV1Controller extends BaseController
|
||||
'msgConf' => json_encode($msgConf, JSON_UNESCAPED_UNICODE),
|
||||
'tagConf' => json_encode($tagConf, JSON_UNESCAPED_UNICODE),
|
||||
'status' => !empty($params['status']) ? 1 : 0,
|
||||
// 计划类型:0=全局,1=独立(默认)
|
||||
'planType' => isset($params['planType']) ? intval($params['planType']) : 1,
|
||||
// 拉群配置
|
||||
'groupInviteEnabled' => $groupInviteEnabled,
|
||||
'groupName' => $params['groupName'] ?? '',
|
||||
|
||||
@@ -79,6 +79,8 @@ class WorkbenchController extends Controller
|
||||
$workbench->type = $param['type'];
|
||||
$workbench->status = !empty($param['status']) ? 1 : 0;
|
||||
$workbench->autoStart = !empty($param['autoStart']) ? 1 : 0;
|
||||
// 计划类型:0=全局,1=独立(默认1)
|
||||
$workbench->planType = isset($param['planType']) ? intval($param['planType']) : 1;
|
||||
$workbench->userId = $userInfo['id'];
|
||||
$workbench->companyId = $userInfo['companyId'];
|
||||
$workbench->createTime = time();
|
||||
@@ -133,7 +135,6 @@ class WorkbenchController extends Controller
|
||||
case self::TYPE_GROUP_CREATE: // 自动建群
|
||||
$config = new WorkbenchGroupCreate;
|
||||
$config->workbenchId = $workbench->id;
|
||||
$config->planType = !empty($param['planType']) ? $param['planType'] : 0;
|
||||
$config->executorId = !empty($param['executorId']) ? $param['executorId'] : 0;
|
||||
|
||||
$config->devices = json_encode($param['deviceGroups'] ?? [], JSON_UNESCAPED_UNICODE);
|
||||
@@ -295,7 +296,7 @@ class WorkbenchController extends Controller
|
||||
|
||||
$list = Workbench::where($where)
|
||||
->with($with)
|
||||
->field('id,companyId,name,type,status,autoStart,userId,createTime,updateTime')
|
||||
->field('id,companyId,name,type,status,autoStart,planType,userId,createTime,updateTime')
|
||||
->order('id', 'desc')
|
||||
->page($page, $limit)
|
||||
->select()
|
||||
@@ -474,7 +475,7 @@ class WorkbenchController extends Controller
|
||||
break;
|
||||
|
||||
case self::TYPE_IMPORT_CONTACT:
|
||||
if (!empty($item->importContact)) {
|
||||
if (!empty($item->importContact)) {
|
||||
$item->config = $item->importContact;
|
||||
$item->config->devices = json_decode($item->config->devices, true);
|
||||
$item->config->poolGroups = json_decode($item->config->pools, true);
|
||||
@@ -568,7 +569,7 @@ class WorkbenchController extends Controller
|
||||
|
||||
|
||||
$workbench = Workbench::where($where)
|
||||
->field('id,name,type,status,autoStart,createTime,updateTime,companyId')
|
||||
->field('id,name,type,status,autoStart,planType,createTime,updateTime,companyId')
|
||||
->with($with)
|
||||
->find();
|
||||
|
||||
@@ -1187,6 +1188,12 @@ class WorkbenchController extends Controller
|
||||
$workbench->name = $param['name'];
|
||||
$workbench->status = !empty($param['status']) ? 1 : 0;
|
||||
$workbench->autoStart = !empty($param['autoStart']) ? 1 : 0;
|
||||
// 更新计划类型:0=全局,1=独立(默认保留原值或1)
|
||||
if (isset($param['planType'])) {
|
||||
$workbench->planType = intval($param['planType']);
|
||||
} elseif (!isset($workbench->planType) || $workbench->planType === null) {
|
||||
$workbench->planType = 1;
|
||||
}
|
||||
$workbench->updateTime = time();
|
||||
$workbench->save();
|
||||
|
||||
@@ -1457,6 +1464,12 @@ class WorkbenchController extends Controller
|
||||
$newWorkbench->type = $workbench->type;
|
||||
$newWorkbench->status = 1; // 新拷贝的默认启用
|
||||
$newWorkbench->autoStart = $workbench->autoStart;
|
||||
// 复制计划类型(0=全局,1=独立,默认1)
|
||||
if (isset($workbench->planType)) {
|
||||
$newWorkbench->planType = intval($workbench->planType);
|
||||
} else {
|
||||
$newWorkbench->planType = 1;
|
||||
}
|
||||
$newWorkbench->userId = $this->request->userInfo['id'];
|
||||
$newWorkbench->companyId = $this->request->userInfo['companyId'];
|
||||
$newWorkbench->save();
|
||||
|
||||
@@ -289,6 +289,86 @@ return [
|
||||
'log_file' => 'all_friends.log',
|
||||
],
|
||||
|
||||
// 检查未读/未回复消息并自动迁移好友(每5分钟执行一次)
|
||||
'check_unread_message' => [
|
||||
'command' => 'check:unread-message',
|
||||
'schedule' => '*/5 * * * *', // 每5分钟
|
||||
'options' => ['--minutes=30'], // 30分钟未读/未回复
|
||||
'enabled' => true,
|
||||
'max_concurrent' => 1,
|
||||
'log_file' => 'check_unread_message.log',
|
||||
],
|
||||
|
||||
// 同步部门列表,用于部门管理与权限控制
|
||||
'department_list' => [
|
||||
'command' => 'department:list',
|
||||
'schedule' => '*/30 * * * *', // 每30分钟
|
||||
'options' => [],
|
||||
'enabled' => true,
|
||||
'max_concurrent' => 1,
|
||||
'log_file' => 'crontab_department.log',
|
||||
],
|
||||
|
||||
// 同步内容库,将外部内容同步到系统内容库
|
||||
'content_sync' => [
|
||||
'command' => 'content:sync',
|
||||
'schedule' => '0 2 * * *', // 每天2点
|
||||
'options' => [],
|
||||
'enabled' => true,
|
||||
'max_concurrent' => 1,
|
||||
'log_file' => 'crontab_content_sync.log',
|
||||
],
|
||||
|
||||
// 朋友圈采集任务,采集好友朋友圈内容
|
||||
'moments_collect' => [
|
||||
'command' => 'moments:collect',
|
||||
'schedule' => '0 6 * * *', // 每天6点
|
||||
'options' => [],
|
||||
'enabled' => true,
|
||||
'max_concurrent' => 1,
|
||||
'log_file' => 'crontab_moments_collect.log',
|
||||
],
|
||||
|
||||
// 分配规则列表,同步分配规则数据
|
||||
'allotrule_list' => [
|
||||
'command' => 'allotrule:list',
|
||||
'schedule' => '0 3 * * *', // 每天3点
|
||||
'options' => [],
|
||||
'enabled' => true,
|
||||
'max_concurrent' => 1,
|
||||
'log_file' => 'crontab_allotrule_list.log',
|
||||
],
|
||||
|
||||
// 自动创建分配规则,根据规则自动创建分配任务
|
||||
'allotrule_autocreate' => [
|
||||
'command' => 'allotrule:autocreate',
|
||||
'schedule' => '0 4 * * *', // 每天4点
|
||||
'options' => [],
|
||||
'enabled' => true,
|
||||
'max_concurrent' => 1,
|
||||
'log_file' => 'crontab_allotrule_autocreate.log',
|
||||
],
|
||||
|
||||
// 工作台:入群欢迎语任务,自动发送入群欢迎消息
|
||||
'workbench_group_welcome' => [
|
||||
'command' => 'workbench:groupWelcome',
|
||||
'schedule' => '*/1 * * * *', // 每1分钟
|
||||
'options' => [],
|
||||
'enabled' => true,
|
||||
'max_concurrent' => 1,
|
||||
'log_file' => 'workbench_groupWelcome.log',
|
||||
],
|
||||
|
||||
// 采集客服自己的朋友圈,同步客服账号的朋友圈内容
|
||||
'own_moments_collect' => [
|
||||
'command' => 'own:moments:collect',
|
||||
'schedule' => '*/30 * * * *', // 每30分钟
|
||||
'options' => [],
|
||||
'enabled' => true,
|
||||
'max_concurrent' => 1,
|
||||
'log_file' => 'own_moments_collect.log',
|
||||
],
|
||||
|
||||
// 已禁用的任务(注释掉的任务)
|
||||
// 'workbench_group_push' => [
|
||||
// 'command' => 'workbench:groupPush',
|
||||
|
||||
@@ -169,15 +169,14 @@ crontab -l
|
||||
*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:import-contact >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/import_contact.log 2>&1
|
||||
# 工作台入群欢迎语
|
||||
*/1 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:groupWelcome >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/workbench_groupWelcome.log 2>&1
|
||||
|
||||
# 消息提醒
|
||||
*/1 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think kf:notice >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/kf_notice.log 2>&1
|
||||
# 客服评分
|
||||
0 2 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechat:calculate-score >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/calculate_score.log 2>&1
|
||||
|
||||
# 采集客服自己的朋友圈
|
||||
*/30 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think own:moments:collect >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/own_moments_collect.log 2>&1
|
||||
|
||||
*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think own:moments:collect >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/own_moments_collect.log 2>&1
|
||||
# 检查未读/未回复消息并自动迁移好友
|
||||
*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think check:unread-message --minutes=30 >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/check_unread_message.log 2>&1
|
||||
|
||||
|
||||
# 每分钟执行一次调度器(调度器内部会自动判断哪些任务需要执行)
|
||||
|
||||
Reference in New Issue
Block a user