diff --git a/Server/application/ai/config/route.php b/Server/application/ai/config/route.php index 286b7c24..ada60ae6 100644 --- a/Server/application/ai/config/route.php +++ b/Server/application/ai/config/route.php @@ -14,6 +14,7 @@ Route::group('v1/ai', function () { //豆包ai Route::group('doubao', function () { Route::post('text', 'app\ai\controller\DouBaoAI@text'); + Route::post('aiChat', 'app\ai\controller\DouBaoAI@aiChat'); }); diff --git a/Server/application/ai/controller/DouBaoAI.php b/Server/application/ai/controller/DouBaoAI.php index 18012dbb..d61a6c89 100644 --- a/Server/application/ai/controller/DouBaoAI.php +++ b/Server/application/ai/controller/DouBaoAI.php @@ -56,4 +56,40 @@ class DouBaoAI $result = json_decode($result, true); return successJson($result); } + + + public function aiChat() + { + $this->__init(); + + $content = input('content',''); + if (empty($content)){ + return json_encode(['code' => 500, 'msg' => '提示词缺失']); + } + + $content = $content. ' + + 请结合上面的聊天记录给我最佳的客服回复'; + + // 发送请求 + $params = [ + 'model' => 'doubao-1-5-pro-32k-250115', + 'messages' => [ + ['role' => 'system', 'content' => '以下是客服跟用户的对话.'], + ['role' => 'user', 'content' => $content], + ], + /*'extra_headers' => [ + 'x-is-encrypted' => true + ], + 'temperature' => 1, + 'top_p' => 0.7, + 'max_tokens' => 4096, + 'frequency_penalty' => 0,*/ + ]; + $result = requestCurl($this->apiUrl, $params, 'POST', $this->headers, 'json'); + $result = json_decode($result, true); + return successJson($result); + } + + } \ No newline at end of file diff --git a/Server/application/chukebao/config/route.php b/Server/application/chukebao/config/route.php index 1384ac63..9d2a93a6 100644 --- a/Server/application/chukebao/config/route.php +++ b/Server/application/chukebao/config/route.php @@ -26,8 +26,35 @@ Route::group('v1/', function () { //客服相关 Route::group('message/', function () { Route::get('list', 'app\chukebao\controller\MessageController@getList'); // 获取好友列表 + Route::get('readMessage', 'app\chukebao\controller\MessageController@readMessage'); // 读取消息 }); + //AI相关 + Route::group('ai/', function () { + //问答 + Route::group('questions/', function () { + Route::get('list', 'app\chukebao\controller\QuestionsController@getList'); // 问答列表 + Route::post('add', 'app\chukebao\controller\QuestionsController@create'); // 问答添加 + Route::post('update', 'app\chukebao\controller\QuestionsController@update'); // 问答更新 + Route::get('delete', 'app\chukebao\controller\QuestionsController@delete'); // 问答删除 + Route::get('detail', 'app\chukebao\controller\QuestionsController@detail'); // 问答详情 + }); + + Route::group('settings/', function () { + Route::get('get', 'app\chukebao\controller\AiSettingsController@getSetting'); + Route::post('set', 'app\chukebao\controller\AiSettingsController@setSetting'); + }); + + + Route::group('friend/', function () { + Route::post('set', 'app\chukebao\controller\AiSettingsController@setFriend'); + }); + + + }); + + + }); diff --git a/Server/application/chukebao/controller/AiSettingsController.php b/Server/application/chukebao/controller/AiSettingsController.php new file mode 100644 index 00000000..64161ddc --- /dev/null +++ b/Server/application/chukebao/controller/AiSettingsController.php @@ -0,0 +1,153 @@ + false, + 'round' => 10, + 'aiStopSetting' => [ + 'status' => true, + 'key' => ['好', '不错', '好的', '下次', '可以'] + ], + 'fileSetting' => [ + 'type' => 1, + 'content' => '' + ], + 'modelSetting' => [ + 'model' => 'GPT-4', + 'role' => '你是一名销售的AI助理,同时也是一个工智能技术专家,你的名字叫小灵,你是单身女性,出生于2003年10月10日,喜欢听音乐和看电影有着丰富的人生阅历,前成熟大方,分享用幽默风趣的语言和客户交流,顾客问起你的感情,回复内容中不要使用号,特别注意不要跟客户问题,不要更多选择发送的信息。', + 'businessBackground' => '灵销智能公司开发了多款AI营销智能技术产品,以提升销售GPT AI大模型为核心,接入打造的销售/营销/客服等AI智能应用,为企业AI办公,AI助理,AI销售,AI营销,AI直播等大AI应用产品。', + 'dialogueStyle' => '客户:你们的AI解决方案具体是怎么收费的?销售:嗯,朋友,我们的AI解决方案是根据项目需求来定的,这样吧,你能跟我说说你们的具体情况吗,不过这样一分钱,您看怎么样?我们可以给您做个详细的方案对比。', + ] + ]; + + const TYPE_DATA = ['audioSetting', 'round', 'aiStopSetting', 'fileSetting', 'modelSetting']; + + /** + * 获取配置信息 + * @return \think\response\Json + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function getSetting() + { + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + $data = Db::name('ai_settings')->where(['userId' => $userId, 'companyId' => $companyId])->find(); + if (empty($data)) { + $setting = self::SETTING_DEFAULT; + $data = [ + 'companyId' => $companyId, + 'userId' => $userId, + 'config' => json_encode($setting, 256), + 'createTime' => time(), + 'updateTime' => time() + ]; + Db::name('ai_settings')->insert($data); + + } else { + $setting = json_decode($data['config'], true); + } + + return ResponseHelper::success($setting, '获取成功'); + } + + + /** + * 配置 + * @return \think\response\Json + * @throws \Exception + */ + public function setSetting() + { + $key = $this->request->param('key', ''); + $value = $this->request->param('value', ''); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($key) || empty($value)) { + return ResponseHelper::error('参数缺失'); + } + + + if (!in_array($key, self::TYPE_DATA)) { + return ResponseHelper::error('该类型不在配置项'); + } + + Db::startTrans(); + try { + $data = Db::name('ai_settings')->where(['userId' => $userId, 'companyId' => $companyId])->find(); + if (empty($data)) { + $setting = self::SETTING_DEFAULT; + } else { + $setting = json_decode($data['config'], true); + } + $setting[$key] = $value; + $setting = json_encode($setting, 256); + Db::name('ai_settings')->where(['id' => $data['id']])->update(['config' => $setting, 'updateTime' => time()]); + Db::commit(); + return ResponseHelper::success(' ', '配置成功'); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error('配置失败:' . $e->getMessage()); + } + } + + + + public function setFriend() + { + $friendId = $this->request->param('friendId', ''); + $wechatAccountId = $this->request->param('wechatAccountId', ''); + $type = $this->request->param('type', 0); + + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($friendId) || empty($wechatAccountId)) { + return ResponseHelper::error('参数缺失'); + } + $friend = Db::table('s2_wechat_friend')->where(['id' => $friendId,'wechatAccountId' => $wechatAccountId])->find(); + + if (empty($friend)) { + return ResponseHelper::error('该好友不存在'); + } + + $aiFriendSettings = AiFriendSettings::where(['userId' => $userId, 'companyId' => $companyId,'friendId' => $friendId,'wechatAccountId' => $wechatAccountId])->find(); + Db::startTrans(); + try { + if (empty($aiFriendSettings)) { + $aiFriendSettings = new AiFriendSettings(); + $aiFriendSettings->companyId = $companyId; + $aiFriendSettings->userId = $userId; + $aiFriendSettings->type = $type; + $aiFriendSettings->wechatAccountId = $wechatAccountId; + $aiFriendSettings->friendId = $friendId; + $aiFriendSettings->createTime = time(); + $aiFriendSettings->updateTime = time(); + }else{ + $aiFriendSettings->type = $type; + $aiFriendSettings->updateTime = time(); + } + $aiFriendSettings->save(); + Db::commit(); + return ResponseHelper::success(' ', '配置成功'); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error('配置失败:' . $e->getMessage()); + } + + } + +} \ No newline at end of file diff --git a/Server/application/chukebao/controller/CustomerServiceController.php b/Server/application/chukebao/controller/CustomerServiceController.php index 93892fe5..3197e91a 100644 --- a/Server/application/chukebao/controller/CustomerServiceController.php +++ b/Server/application/chukebao/controller/CustomerServiceController.php @@ -25,8 +25,8 @@ class CustomerServiceController extends BaseController ->group('id') ->select(); foreach ($list as $k=>&$v){ - $v['createTime'] = !empty($v['createTime']) ? date('Y-m-d H:i:s',$v['createTime']) : ''; - $v['updateTime'] = !empty($v['updateTime']) ? date('Y-m-d H:i:s',$v['updateTime']) : ''; + $v['createTime'] = !empty($v['createTime']) && is_numeric($v['createTime']) ? date('Y-m-d H:i:s',$v['createTime']) : ''; + $v['updateTime'] = !empty($v['updateTime']) && is_numeric($v['updateTime']) ? date('Y-m-d H:i:s',$v['updateTime']) : ''; $v['labels'] = json_decode($v['labels'],true); unset( $v['accountUserName'], diff --git a/Server/application/chukebao/controller/MessageController.php b/Server/application/chukebao/controller/MessageController.php index c213b7e8..531714f9 100644 --- a/Server/application/chukebao/controller/MessageController.php +++ b/Server/application/chukebao/controller/MessageController.php @@ -53,6 +53,9 @@ class MessageController extends BaseController ->field('id,nickname,avatar') ->find(); $v['msgInfo'] = $friend; + $v['unreadCount'] = Db::table('s2_wechat_message') + ->where(['wechatFriendId' => $v['wechatFriendId'],'isRead' => 0]) + ->count(); } if (!empty($v['wechatChatroomId'])){ @@ -61,12 +64,45 @@ class MessageController extends BaseController ->field('id,nickname,chatroomAvatar as avatar') ->find(); $v['msgInfo'] = $chatroom; + $v['unreadCount'] = Db::table('s2_wechat_message') + ->where(['wechatChatroomId' => $v['wechatChatroomId'],'isRead' => 0]) + ->count(); } - } unset($v); return ResponseHelper::success($list); } + + + public function readMessage(){ + $wechatFriendId = $this->request->param('wechatFriendId', ''); + $wechatChatroomId = $this->request->param('wechatChatroomId', ''); + $accountId = $this->getUserInfo('s2_accountId'); + if (empty($accountId)){ + return ResponseHelper::error('请先登录'); + } + if (empty($wechatChatroomId) && empty($wechatFriendId)){ + return ResponseHelper::error('参数缺失'); + } + + $where = []; + if (!empty($wechatChatroomId)){ + $where[] = ['wechatChatroomId','=',$wechatChatroomId]; + } + + if (!empty($wechatFriendId)){ + $where[] = ['wechatFriendId','=',$wechatFriendId]; + } + + Db::table('s2_wechat_message')->where($where)->update(['isRead' => 1]); + + + return ResponseHelper::success([]); + + + } + + } \ No newline at end of file diff --git a/Server/application/chukebao/controller/QuestionsController.php b/Server/application/chukebao/controller/QuestionsController.php new file mode 100644 index 00000000..9ee8799e --- /dev/null +++ b/Server/application/chukebao/controller/QuestionsController.php @@ -0,0 +1,232 @@ +request->param('page', 1); + $limit = $this->request->param('limit', 10); + $keyword = $this->request->param('keyword', ''); + $accountId = $this->getUserInfo('s2_accountId'); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($accountId)){ + return ResponseHelper::error('请先登录'); + } + $query = Questions::where(['userId' => $userId,'companyId' => $companyId,'isDel' => 0]) + ->order('id desc'); + if (!empty($keyword)){ + $query->where('questions|answers', 'like', '%'.$keyword.'%'); + } + $list = $query->page($page, $limit)->select()->toArray(); + $total = $query->count(); + + foreach ($list as $k => &$v){ + $user = Db::name('users')->where(['id' => $v['userId']])->field('username,account')->find(); + if (!empty($user)){ + $v['userName'] = !empty($user['username']) ? $user['username'] : $user['account']; + }else{ + $v['userName'] = ''; + } + $v['answers'] = json_decode($v['answers'],true); + } + unset($v); + return ResponseHelper::success(['list'=>$list,'total'=>$total]); + } + + + /** + * 新增 + * @return \think\response\Json + * @throws \Exception + */ + public function create(){ + + $type = $this->request->param('type', 0); + $questions = $this->request->param('questions', ''); + $answers = $this->request->param('answers', []); + $status = $this->request->param('status', 0); + $accountId = $this->getUserInfo('s2_accountId'); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($accountId)){ + return ResponseHelper::error('请先登录'); + } + + if (empty($questions) || empty($answers)){ + return ResponseHelper::error('问题和答案不能为空'); + } + + Db::startTrans(); + try { + $questionsModel = new Questions(); + $questionsModel->type = $type; + $questionsModel->questions = $questions; + $questionsModel->answers = !empty($answers) ? json_encode($answers,256) : json_encode([],256); + $questionsModel->status = $status; + $questionsModel->accountId = $accountId; + $questionsModel->userId = $userId; + $questionsModel->companyId = $companyId; + $questionsModel->createTime = time(); + $questionsModel->updateTime = time(); + $questionsModel->save(); + Db::commit(); + return ResponseHelper::success(' ','创建成功'); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error('创建失败:'.$e->getMessage()); + } + } + + + /** + * 更新 + * @return \think\response\Json + * @throws \Exception + */ + public function update(){ + + $id = $this->request->param('id', 0); + $accountId = $this->getUserInfo('s2_accountId'); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + $type = $this->request->param('type', 0); + $questions = $this->request->param('questions', ''); + $answers = $this->request->param('answers', []); + $status = $this->request->param('status', 0); + $accountId = $this->getUserInfo('s2_accountId'); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($accountId)){ + return ResponseHelper::error('请先登录'); + } + + if (empty($id)){ + return ResponseHelper::error('参数缺失'); + } + + if (empty($questions) || empty($answers)){ + return ResponseHelper::error('问题和答案不能为空'); + } + Db::startTrans(); + try { + $questionsData = Questions::where(['id' => $id,'userId' => $userId,'companyId' => $companyId,'isDel' => 0])->find(); + $questionsData->type = $type; + $questionsData->questions = $questions; + $questionsData->answers = !empty($answers) ? json_encode($answers,256) : json_encode([],256); + $questionsData->status = $status; + $questionsData->accountId = $accountId; + $questionsData->userId = $userId; + $questionsData->companyId = $companyId; + $questionsData->updateTime = time(); + $questionsData->save(); + Db::commit(); + return ResponseHelper::success(' ','更新成功'); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error('更新失败:'.$e->getMessage()); + } + } + + + + + + /** + * 删除 + * @return \think\response\Json + * @throws \Exception + */ + public function delete(){ + + $id = $this->request->param('id', 0); + $accountId = $this->getUserInfo('s2_accountId'); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($accountId)){ + return ResponseHelper::error('请先登录'); + } + + if (empty($id)){ + return ResponseHelper::error('参数缺失'); + } + $questions = Questions::where(['id' => $id,'userId' => $userId,'companyId' => $companyId,'isDel' => 0])->find(); + + if (empty($questions)){ + return ResponseHelper::error('该问题不存在或者已删除'); + } + $res = Questions::where(['id' => $id])->update(['isDel' => 1,'deleteTime' => time()]); + + if (!empty($res)){ + return ResponseHelper::success('','已删除'); + }else{ + return ResponseHelper::error('删除失败'); + } + + } + + + /** + * 详情 + * @return \think\response\Json + * @throws \Exception + */ + public function detail(){ + + $id = $this->request->param('id', 0); + $accountId = $this->getUserInfo('s2_accountId'); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($accountId)){ + return ResponseHelper::error('请先登录'); + } + + if (empty($id)){ + return ResponseHelper::error('参数缺失'); + } + $questions = Questions::where(['id' => $id,'userId' => $userId,'companyId' => $companyId,'isDel' => 0])->find(); + + if (empty($questions)){ + return ResponseHelper::error('该问题不存在或者已删除'); + } + + $questions['answers'] = json_decode($questions['answers'],true); + $user = Db::name('users')->where(['id' => $questions['userId']])->field('username,account')->find(); + if (!empty($user)){ + $questions['userName'] = !empty($user['username']) ? $user['username'] : $user['account']; + }else{ + $questions['userName'] = ''; + } + + unset( + $questions['isDel'], + $questions['deleteTime'], + $questions['createTime'], + $questions['updateTime'] + ); + + return ResponseHelper::success($questions,'获取成功'); + + + } + + + + +} \ No newline at end of file diff --git a/Server/application/chukebao/controller/WechatChatroomController.php b/Server/application/chukebao/controller/WechatChatroomController.php index e6c64f5c..49fb802d 100644 --- a/Server/application/chukebao/controller/WechatChatroomController.php +++ b/Server/application/chukebao/controller/WechatChatroomController.php @@ -22,9 +22,57 @@ class WechatChatroomController extends BaseController $total = $query->count(); - foreach ($list as $k=>&$v){ - $v['createTime'] = !empty($v['createTime']) ? date('Y-m-d H:i:s',$v['createTime']) : ''; - $v['updateTime'] = !empty($v['updateTime']) ? date('Y-m-d H:i:s',$v['updateTime']) : ''; + // 提取所有聊天室ID,用于批量查询 + $chatroomIds = array_column($list, 'id'); + + + // 一次性查询所有聊天室的未读消息数量 + $unreadCounts = []; + if (!empty($chatroomIds)) { + $unreadResults = Db::table('s2_wechat_message') + ->field('wechatChatroomId, COUNT(*) as count') + ->where('wechatChatroomId', 'in', $chatroomIds) + ->where('isRead', 0) + ->group('wechatChatroomId') + ->select(); + + foreach ($unreadResults as $result) { + $unreadCounts[$result['wechatChatroomId']] = $result['count']; + } + } + // 一次性查询所有聊天室的最新消息 + $latestMessages = []; + if (!empty($chatroomIds)) { + // 使用子查询获取每个聊天室的最新消息ID + $subQuery = Db::table('s2_wechat_message') + ->field('MAX(id) as max_id, wechatChatroomId') + ->where('wechatChatroomId', 'in', $chatroomIds) + ->group('wechatChatroomId') + ->buildSql(); + + // 查询最新消息的详细信息 + $messageResults = Db::table('s2_wechat_message') + ->alias('m') + ->join([$subQuery => 'sub'], 'm.id = sub.max_id') + ->field('m.*, sub.wechatChatroomId') + ->select(); + + foreach ($messageResults as $message) { + $latestMessages[$message['wechatChatroomId']] = $message; + } + } + + // 处理每个聊天室的数据 + foreach ($list as $k => &$v) { + $v['createTime'] = !empty($v['createTime']) ? date('Y-m-d H:i:s', $v['createTime']) : ''; + $v['updateTime'] = !empty($v['updateTime']) ? date('Y-m-d H:i:s', $v['updateTime']) : ''; + + $config = [ + 'unreadCount' => isset($unreadCounts[$v['id']]) ? $unreadCounts[$v['id']] : 0, + 'chat' => isset($latestMessages[$v['id']]), + 'msgTime' => isset($latestMessages[$v['id']]) ? $latestMessages[$v['id']]['wechatTime'] : 0 + ]; + $v['config'] = $config; } unset($v); diff --git a/Server/application/chukebao/controller/WechatFriendController.php b/Server/application/chukebao/controller/WechatFriendController.php index bd8f0095..82a144ed 100644 --- a/Server/application/chukebao/controller/WechatFriendController.php +++ b/Server/application/chukebao/controller/WechatFriendController.php @@ -22,10 +22,60 @@ class WechatFriendController extends BaseController $total = $query->count(); - foreach ($list as $k=>&$v){ - $v['createTime'] = !empty($v['createTime']) ? date('Y-m-d H:i:s',$v['createTime']) : ''; - $v['updateTime'] = !empty($v['updateTime']) ? date('Y-m-d H:i:s',$v['updateTime']) : ''; - $v['passTime'] = !empty($v['passTime']) ? date('Y-m-d H:i:s',$v['passTime']) : ''; + // 提取所有好友ID + $friendIds = array_column($list, 'id'); + + // 一次性查询所有好友的未读消息数量 + $unreadCounts = []; + if (!empty($friendIds)) { + $unreadResults = Db::table('s2_wechat_message') + ->field('wechatFriendId, COUNT(*) as count') + ->where('wechatFriendId', 'in', $friendIds) + ->where('isRead', 0) + ->group('wechatFriendId') + ->select(); + + foreach ($unreadResults as $result) { + $unreadCounts[$result['wechatFriendId']] = $result['count']; + } + } + + // 一次性查询所有好友的最新消息 + $latestMessages = []; + if (!empty($friendIds)) { + // 使用子查询获取每个好友的最新消息ID + $subQuery = Db::table('s2_wechat_message') + ->field('MAX(id) as max_id, wechatFriendId') + ->where('wechatFriendId', 'in', $friendIds) + ->group('wechatFriendId') + ->buildSql(); + + // 查询最新消息的详细信息 + $messageResults = Db::table('s2_wechat_message') + ->alias('m') + ->join([$subQuery => 'sub'], 'm.id = sub.max_id') + ->field('m.*, sub.wechatFriendId') + ->select(); + + foreach ($messageResults as $message) { + $latestMessages[$message['wechatFriendId']] = $message; + } + } + + // 处理每个好友的数据 + foreach ($list as $k => &$v) { + $v['createTime'] = !empty($v['createTime']) ? date('Y-m-d H:i:s', $v['createTime']) : ''; + $v['updateTime'] = !empty($v['updateTime']) ? date('Y-m-d H:i:s', $v['updateTime']) : ''; + $v['passTime'] = !empty($v['passTime']) ? date('Y-m-d H:i:s', $v['passTime']) : ''; + + $config = [ + 'unreadCount' => isset($unreadCounts[$v['id']]) ? $unreadCounts[$v['id']] : 0, + 'chat' => isset($latestMessages[$v['id']]), + 'msgTime' => isset($latestMessages[$v['id']]) ? $latestMessages[$v['id']]['wechatTime'] : 0 + ]; + + // 将消息配置添加到好友数据中 + $v['config'] = $config; } unset($v); diff --git a/Server/application/chukebao/model/AiFriendSettings.php b/Server/application/chukebao/model/AiFriendSettings.php new file mode 100644 index 00000000..de99b4a9 --- /dev/null +++ b/Server/application/chukebao/model/AiFriendSettings.php @@ -0,0 +1,17 @@ +setName('clean:expired_group_messages') + ->setDescription('Clean expired group messages from the database') + ->addOption('days', 'd', Option::VALUE_OPTIONAL, 'Number of days to keep messages (default: 90)', 90) + ->addOption('dry-run', null, Option::VALUE_NONE, 'Perform a dry run without deleting any data') + ->addOption('batch-size', 'b', Option::VALUE_OPTIONAL, 'Batch size for deletion (default: 1000)', 1000); + } + + protected function execute(Input $input, Output $output) + { + $days = (int)$input->getOption('days'); + $dryRun = $input->getOption('dry-run'); + $batchSize = (int)$input->getOption('batch-size'); + + if ($dryRun) { + $output->writeln("Running in dry-run mode. No data will be deleted."); + } + + $cutoffDate = date('Y-m-d H:i:s', strtotime("-{$days} days")); + $output->writeln("Cleaning group messages older than {$cutoffDate} (keeping last {$days} days)"); + + // 清理微信群组消息 + $this->cleanWechatGroupMessages($cutoffDate, $dryRun, $batchSize, $output); + + $output->writeln("Group message cleanup completed successfully."); + } + + protected function cleanWechatGroupMessages($cutoffDate, $dryRun, $batchSize, Output $output) + { + $output->writeln("\nCleaning s2_wechat_group_message table..."); + + // 获取符合条件的消息总数 + $totalCount = Db::table('s2_wechat_group_message') + ->where('createTime', '<', $cutoffDate) + ->count(); + + if ($totalCount === 0) { + $output->writeln(" No expired group messages found."); + return; + } + + $output->writeln(" Found {$totalCount} group messages to clean up."); + + if ($dryRun) { + $output->writeln(" Dry run mode: would delete {$totalCount} group messages."); + return; + } + + // 计算需要执行的批次数 + $batches = ceil($totalCount / $batchSize); + $deletedCount = 0; + + $output->writeln(" Deleting in {$batches} batches of {$batchSize} records..."); + + // 分批删除数据 + for ($i = 0; $i < $batches; $i++) { + // 获取一批要删除的ID + $ids = Db::table('s2_wechat_group_message') + ->where('createTime', '<', $cutoffDate) + ->limit($batchSize) + ->column('id'); + + if (empty($ids)) { + break; + } + + // 删除这批数据 + $count = Db::table('s2_wechat_group_message') + ->whereIn('id', $ids) + ->delete(); + + $deletedCount += $count; + $progress = round(($deletedCount / $totalCount) * 100, 2); + $output->write(" Progress: {$progress}% ({$deletedCount}/{$totalCount})\r"); + + // 短暂暂停,减轻数据库负担 + usleep(500000); // 暂停0.5秒 + } + + $output->writeln(""); + $output->writeln(" Successfully deleted {$deletedCount} expired group messages."); + + // 优化表 + $output->writeln(" Optimizing table..."); + Db::execute("OPTIMIZE TABLE s2_wechat_group_message"); + $output->writeln(" Table optimization completed."); + } +} \ No newline at end of file diff --git a/Server/application/command/CleanExpiredMessages.php b/Server/application/command/CleanExpiredMessages.php new file mode 100644 index 00000000..d499f653 --- /dev/null +++ b/Server/application/command/CleanExpiredMessages.php @@ -0,0 +1,100 @@ +setName('clean:expired_messages') + ->setDescription('Clean expired messages from the database') + ->addOption('days', 'd', Option::VALUE_OPTIONAL, 'Number of days to keep messages (default: 90)', 90) + ->addOption('dry-run', null, Option::VALUE_NONE, 'Perform a dry run without deleting any data') + ->addOption('batch-size', 'b', Option::VALUE_OPTIONAL, 'Batch size for deletion (default: 1000)', 1000); + } + + protected function execute(Input $input, Output $output) + { + $days = (int)$input->getOption('days'); + $dryRun = $input->getOption('dry-run'); + $batchSize = (int)$input->getOption('batch-size'); + + if ($dryRun) { + $output->writeln("Running in dry-run mode. No data will be deleted."); + } + + $cutoffDate = date('Y-m-d H:i:s', strtotime("-{$days} days")); + $output->writeln("Cleaning messages older than {$cutoffDate} (keeping last {$days} days)"); + + // 清理微信消息 + $this->cleanWechatMessages($cutoffDate, $dryRun, $batchSize, $output); + + $output->writeln("Message cleanup completed successfully."); + } + + protected function cleanWechatMessages($cutoffDate, $dryRun, $batchSize, Output $output) + { + $output->writeln("\nCleaning s2_wechat_message table..."); + + // 获取符合条件的消息总数 + $totalCount = Db::table('s2_wechat_message') + ->where('createTime', '<', $cutoffDate) + ->count(); + + if ($totalCount === 0) { + $output->writeln(" No expired messages found."); + return; + } + + $output->writeln(" Found {$totalCount} messages to clean up."); + + if ($dryRun) { + $output->writeln(" Dry run mode: would delete {$totalCount} messages."); + return; + } + + // 计算需要执行的批次数 + $batches = ceil($totalCount / $batchSize); + $deletedCount = 0; + + $output->writeln(" Deleting in {$batches} batches of {$batchSize} records..."); + + // 分批删除数据 + for ($i = 0; $i < $batches; $i++) { + // 获取一批要删除的ID + $ids = Db::table('s2_wechat_message') + ->where('createTime', '<', $cutoffDate) + ->limit($batchSize) + ->column('id'); + + if (empty($ids)) { + break; + } + + // 删除这批数据 + $count = Db::table('s2_wechat_message') + ->whereIn('id', $ids) + ->delete(); + + $deletedCount += $count; + $progress = round(($deletedCount / $totalCount) * 100, 2); + $output->write(" Progress: {$progress}% ({$deletedCount}/{$totalCount})\r"); + + // 短暂暂停,减轻数据库负担 + usleep(500000); // 暂停0.5秒 + } + + $output->writeln(""); + $output->writeln(" Successfully deleted {$deletedCount} expired messages."); + + // 优化表 + $output->writeln(" Optimizing table..."); + Db::execute("OPTIMIZE TABLE s2_wechat_message"); + $output->writeln(" Table optimization completed."); + } +} \ No newline at end of file diff --git a/Server/application/command/OptimizeMessageIndexes.php b/Server/application/command/OptimizeMessageIndexes.php new file mode 100644 index 00000000..a604191e --- /dev/null +++ b/Server/application/command/OptimizeMessageIndexes.php @@ -0,0 +1,112 @@ +setName('optimize:message_indexes') + ->setDescription('Optimize database indexes for message-related tables'); + } + + protected function execute(Input $input, Output $output) + { + $output->writeln("Starting index optimization for message-related tables..."); + + // 优化 s2_wechat_message 表索引 + $this->optimizeWechatMessageIndexes($output); + + // 优化 s2_wechat_chatroom 表索引 + $this->optimizeWechatChatroomIndexes($output); + + // 优化 s2_wechat_friend 表索引 + $this->optimizeWechatFriendIndexes($output); + + $output->writeln("Index optimization completed successfully."); + } + + protected function optimizeWechatMessageIndexes(Output $output) + { + $output->writeln("Optimizing s2_wechat_message table indexes..."); + + // 检查并添加 wechatChatroomId 索引 + $this->addIndexIfNotExists('s2_wechat_message', 'idx_chatroom_id', 'wechatChatroomId', $output); + + // 检查并添加 wechatFriendId 索引 + $this->addIndexIfNotExists('s2_wechat_message', 'idx_friend_id', 'wechatFriendId', $output); + + // 检查并添加 isRead 索引 + $this->addIndexIfNotExists('s2_wechat_message', 'idx_is_read', 'isRead', $output); + + // 检查并添加 type 索引 + $this->addIndexIfNotExists('s2_wechat_message', 'idx_type', 'type', $output); + + // 检查并添加 createTime 索引 + $this->addIndexIfNotExists('s2_wechat_message', 'idx_create_time', 'createTime', $output); + + // 检查并添加组合索引 (wechatChatroomId, isRead) + $this->addIndexIfNotExists('s2_wechat_message', 'idx_chatroom_read', 'wechatChatroomId,isRead', $output); + + // 检查并添加组合索引 (wechatFriendId, isRead) + $this->addIndexIfNotExists('s2_wechat_message', 'idx_friend_read', 'wechatFriendId,isRead', $output); + } + + protected function optimizeWechatChatroomIndexes(Output $output) + { + $output->writeln("Optimizing s2_wechat_chatroom table indexes..."); + + // 检查并添加 accountId 索引 + $this->addIndexIfNotExists('s2_wechat_chatroom', 'idx_account_id', 'accountId', $output); + + // 检查并添加 isDeleted 索引 + $this->addIndexIfNotExists('s2_wechat_chatroom', 'idx_is_deleted', 'isDeleted', $output); + + // 检查并添加组合索引 (accountId, isDeleted) + $this->addIndexIfNotExists('s2_wechat_chatroom', 'idx_account_deleted', 'accountId,isDeleted', $output); + } + + protected function optimizeWechatFriendIndexes(Output $output) + { + $output->writeln("Optimizing s2_wechat_friend table indexes..."); + + // 检查并添加 accountId 索引 + $this->addIndexIfNotExists('s2_wechat_friend', 'idx_account_id', 'accountId', $output); + + // 检查并添加 isDeleted 索引 + $this->addIndexIfNotExists('s2_wechat_friend', 'idx_is_deleted', 'isDeleted', $output); + + // 检查并添加组合索引 (accountId, isDeleted) + $this->addIndexIfNotExists('s2_wechat_friend', 'idx_account_deleted', 'accountId,isDeleted', $output); + } + + protected function addIndexIfNotExists($table, $indexName, $columns, Output $output) + { + try { + // 检查索引是否已存在 + $indexExists = false; + $indexes = Db::query("SHOW INDEX FROM {$table}"); + + foreach ($indexes as $index) { + if ($index['Key_name'] === $indexName) { + $indexExists = true; + break; + } + } + + if (!$indexExists) { + // 添加索引 + Db::execute("ALTER TABLE {$table} ADD INDEX {$indexName} ({$columns})"); + $output->writeln(" - Added index {$indexName} on {$table}({$columns})"); + } else { + $output->writeln(" - Index {$indexName} already exists on {$table}"); + } + } catch (\Exception $e) { + $output->writeln(" - Error adding index {$indexName} to {$table}: " . $e->getMessage()); + } + } +} \ No newline at end of file diff --git a/Server/application/command/ScheduleMessageMaintenance.php b/Server/application/command/ScheduleMessageMaintenance.php new file mode 100644 index 00000000..359faf93 --- /dev/null +++ b/Server/application/command/ScheduleMessageMaintenance.php @@ -0,0 +1,121 @@ +setName('schedule:message_maintenance') + ->setDescription('Schedule and run message maintenance tasks') + ->addOption('optimize-indexes', null, Option::VALUE_NONE, 'Run index optimization') + ->addOption('clean-messages', null, Option::VALUE_NONE, 'Clean expired messages') + ->addOption('days', 'd', Option::VALUE_OPTIONAL, 'Number of days to keep messages (default: 90)', 90) + ->addOption('batch-size', 'b', Option::VALUE_OPTIONAL, 'Batch size for deletion (default: 1000)', 1000) + ->addOption('dry-run', null, Option::VALUE_NONE, 'Perform a dry run without deleting any data'); + } + + protected function execute(Input $input, Output $output) + { + $optimizeIndexes = $input->getOption('optimize-indexes'); + $cleanMessages = $input->getOption('clean-messages'); + $days = (int)$input->getOption('days'); + $batchSize = (int)$input->getOption('batch-size'); + $dryRun = $input->getOption('dry-run'); + + // 如果没有指定任何选项,则运行所有维护任务 + if (!$optimizeIndexes && !$cleanMessages) { + $optimizeIndexes = true; + $cleanMessages = true; + } + + $output->writeln("Starting scheduled message maintenance tasks..."); + $startTime = microtime(true); + + // 运行索引优化 + if ($optimizeIndexes) { + $this->runCommand($output, 'optimize:message_indexes'); + } + + // 清理过期消息 + if ($cleanMessages) { + $options = []; + + if ($days !== 90) { + $options[] = "--days={$days}"; + } + + if ($batchSize !== 1000) { + $options[] = "--batch-size={$batchSize}"; + } + + if ($dryRun) { + $options[] = "--dry-run"; + } + + $this->runCommand($output, 'clean:expired_messages', $options); + $this->runCommand($output, 'clean:expired_group_messages', $options); + } + + $endTime = microtime(true); + $executionTime = round($endTime - $startTime, 2); + $output->writeln("All maintenance tasks completed in {$executionTime} seconds."); + } + + protected function runCommand(Output $output, $command, array $options = []) + { + $output->writeln("\nRunning command: {$command}"); + + $optionsStr = implode(' ', $options); + $fullCommand = "php think {$command} {$optionsStr}"; + + $output->writeln("Executing: {$fullCommand}"); + $output->writeln("\nCommand output:"); + + // 执行命令并实时输出结果 + $descriptorSpec = [ + 0 => ["pipe", "r"], // stdin + 1 => ["pipe", "w"], // stdout + 2 => ["pipe", "w"] // stderr + ]; + + $process = proc_open($fullCommand, $descriptorSpec, $pipes); + + if (is_resource($process)) { + // 关闭标准输入 + fclose($pipes[0]); + + // 读取标准输出 + while (!feof($pipes[1])) { + $line = fgets($pipes[1]); + if ($line !== false) { + $output->write($line); + } + } + fclose($pipes[1]); + + // 读取标准错误 + $errorOutput = stream_get_contents($pipes[2]); + fclose($pipes[2]); + + // 获取命令执行结果 + $exitCode = proc_close($process); + + if ($exitCode !== 0) { + $output->writeln("\nCommand failed with exit code {$exitCode}"); + if (!empty($errorOutput)) { + $output->writeln("Error output:"); + $output->writeln($errorOutput); + } + } else { + $output->writeln("\nCommand completed successfully."); + } + } else { + $output->writeln("Failed to execute command."); + } + } +} \ No newline at end of file