diff --git a/application/ai/config/route.php b/application/ai/config/route.php
index 704b7d0..92dfc4e 100644
--- a/application/ai/config/route.php
+++ b/application/ai/config/route.php
@@ -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']);
\ No newline at end of file
diff --git a/application/ai/controller/DouBaoAI.php b/application/ai/controller/DouBaoAI.php
index a3c942c..4f29499 100644
--- a/application/ai/controller/DouBaoAI.php
+++ b/application/ai/controller/DouBaoAI.php
@@ -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';
+ }
+
}
\ No newline at end of file
diff --git a/application/api/controller/MessageController.php b/application/api/controller/MessageController.php
index daedae1..34b79b5 100644
--- a/application/api/controller/MessageController.php
+++ b/application/api/controller/MessageController.php
@@ -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,
+ '账号不在线,自动迁移到在线账号'
+ );
+ // 迁移结果已记录在服务中,这里不需要额外处理
}
}
diff --git a/application/chukebao/controller/DataProcessing.php b/application/chukebao/controller/DataProcessing.php
index af8070c..5f0121c 100644
--- a/application/chukebao/controller/DataProcessing.php
+++ b/application/chukebao/controller/DataProcessing.php
@@ -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);
diff --git a/application/chukebao/controller/MessageController.php b/application/chukebao/controller/MessageController.php
index 3ffeb9f..0f6b939 100644
--- a/application/chukebao/controller/MessageController.php
+++ b/application/chukebao/controller/MessageController.php
@@ -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]);
}
diff --git a/application/command.php b/application/command.php
index 2bf23db..8fc4a2e 100644
--- a/application/command.php
+++ b/application/command.php
@@ -47,4 +47,7 @@ return [
// 统一任务调度器
'scheduler:run' => 'app\command\TaskSchedulerCommand', // 统一任务调度器,支持多进程并发执行
+
+ // 检查未读/未回复消息并自动迁移好友
+ 'check:unread-message' => 'app\command\CheckUnreadMessageCommand', // 检查未读/未回复消息并自动迁移好友
];
diff --git a/application/command/CheckUnreadMessageCommand.php b/application/command/CheckUnreadMessageCommand.php
new file mode 100644
index 0000000..b5fa343
--- /dev/null
+++ b/application/command/CheckUnreadMessageCommand.php
@@ -0,0 +1,63 @@
+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("{$errorMsg}");
+ Log::error($errorMsg);
+ }
+ }
+}
+
diff --git a/application/command/TaskSchedulerCommand.php b/application/command/TaskSchedulerCommand.php
index 9cad5c1..b23dae2 100644
--- a/application/command/TaskSchedulerCommand.php
+++ b/application/command/TaskSchedulerCommand.php
@@ -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("找到配置文件:{$configFile}");
+ $config = include $configFile;
+ if (is_array($config) && !empty($config)) {
+ $this->tasks = $config;
+ break;
+ } else {
+ $output->writeln("配置文件返回的不是数组或为空:{$configFile}");
+ }
}
}
}
if (empty($this->tasks)) {
- $output->writeln('错误:未找到任务配置(task_scheduler),请检查 config/task_scheduler.php 是否存在且返回数组');
+ $output->writeln('错误:未找到任务配置(task_scheduler)');
+ $output->writeln('请检查以下位置:');
+ $output->writeln('1. config/task_scheduler.php 文件是否存在');
+ $output->writeln('2. 文件是否返回有效的数组');
+ $output->writeln('3. 文件权限是否正确');
+ if (defined('ROOT_PATH')) {
+ $output->writeln('项目根目录:' . ROOT_PATH . '');
+ $output->writeln('期望配置文件:' . ROOT_PATH . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'task_scheduler.php');
+ }
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("任务 {$taskId} 符合执行条件(schedule: {$task['schedule']})");
}
}
+ $output->writeln("已启用任务数: {$enabledCount},已禁用任务数: {$disabledCount}");
+
if (empty($tasksToRun)) {
$output->writeln('当前时间没有需要执行的任务');
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("任务 {$taskId} 正在运行中,跳过");
- 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("任务 {$taskId} 正在运行中(PID: {$lockPid}),跳过");
+ continue;
+ } else {
+ // 进程不存在,清除锁
+ Cache::rm($lockKey);
+ Cache::rm("scheduler_task_pid:{$taskId}");
+ }
+ } else {
+ // 如果没有 posix_kill,使用时间判断(2分钟内不重复执行)
+ if ((time() - $lockTime) < 120) {
+ $output->writeln("任务 {$taskId} 可能在运行中(2分钟内执行过),跳过");
+ continue;
+ }
+ }
+ } else {
+ // 如果没有PID记录,使用时间判断(2分钟内不重复执行)
+ if ((time() - $lockTime) < 120) {
+ $output->writeln("任务 {$taskId} 可能在运行中(2分钟内执行过),跳过");
+ continue;
+ }
+ }
}
// 创建子进程
@@ -291,8 +351,9 @@ class TaskSchedulerCommand extends Command
];
$output->writeln("启动任务:{$taskId} (PID: {$pid})");
- // 设置任务锁
+ // 设置任务锁和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,
]);
}
diff --git a/application/common/service/FriendTransferService.php b/application/common/service/FriendTransferService.php
new file mode 100644
index 0000000..314e17c
--- /dev/null
+++ b/application/common/service/FriendTransferService.php
@@ -0,0 +1,480 @@
+ 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
+ ];
+ }
+ }
+}
+
diff --git a/application/cunkebao/controller/device/GetAddResultedV1Controller.php b/application/cunkebao/controller/device/GetAddResultedV1Controller.php
index a2c78b0..4959870 100644
--- a/application/cunkebao/controller/device/GetAddResultedV1Controller.php
+++ b/application/cunkebao/controller/device/GetAddResultedV1Controller.php
@@ -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()
+ ]);
+ }
}
\ No newline at end of file
diff --git a/application/cunkebao/controller/plan/GetAddFriendPlanDetailV1Controller.php b/application/cunkebao/controller/plan/GetAddFriendPlanDetailV1Controller.php
index 7f5f609..3c31892 100644
--- a/application/cunkebao/controller/plan/GetAddFriendPlanDetailV1Controller.php
+++ b/application/cunkebao/controller/plan/GetAddFriendPlanDetailV1Controller.php
@@ -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'],
diff --git a/application/cunkebao/controller/plan/PlanSceneV1Controller.php b/application/cunkebao/controller/plan/PlanSceneV1Controller.php
index 3812e56..2e70152 100644
--- a/application/cunkebao/controller/plan/PlanSceneV1Controller.php
+++ b/application/cunkebao/controller/plan/PlanSceneV1Controller.php
@@ -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,
diff --git a/application/cunkebao/controller/plan/PostCreateAddFriendPlanV1Controller.php b/application/cunkebao/controller/plan/PostCreateAddFriendPlanV1Controller.php
index 4a727d5..67594a1 100644
--- a/application/cunkebao/controller/plan/PostCreateAddFriendPlanV1Controller.php
+++ b/application/cunkebao/controller/plan/PostCreateAddFriendPlanV1Controller.php
@@ -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'] ?? '',
diff --git a/application/cunkebao/controller/plan/PostUpdateAddFriendPlanV1Controller.php b/application/cunkebao/controller/plan/PostUpdateAddFriendPlanV1Controller.php
index d55662f..5511d52 100644
--- a/application/cunkebao/controller/plan/PostUpdateAddFriendPlanV1Controller.php
+++ b/application/cunkebao/controller/plan/PostUpdateAddFriendPlanV1Controller.php
@@ -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'] ?? '',
diff --git a/application/cunkebao/controller/workbench/WorkbenchController.php b/application/cunkebao/controller/workbench/WorkbenchController.php
index 3bd4169..03c6a82 100644
--- a/application/cunkebao/controller/workbench/WorkbenchController.php
+++ b/application/cunkebao/controller/workbench/WorkbenchController.php
@@ -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();
diff --git a/config/task_scheduler.php b/config/task_scheduler.php
index 7fa7ea3..e0b6438 100644
--- a/config/task_scheduler.php
+++ b/config/task_scheduler.php
@@ -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',
diff --git a/crontab_tasks.md b/crontab_tasks.md
index 3f1bdba..81def43 100644
--- a/crontab_tasks.md
+++ b/crontab_tasks.md
@@ -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
# 每分钟执行一次调度器(调度器内部会自动判断哪些任务需要执行)