From 984b98e4fc091fa422dbb51ae4027bf78a7428f9 Mon Sep 17 00:00:00 2001 From: kuaifan Date: Sat, 4 Apr 2026 07:43:26 +0800 Subject: [PATCH] =?UTF-8?q?feat(task):=20=E5=AE=9E=E7=8E=B0=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E5=90=88=E5=B9=B6=E8=BD=AC=E5=8F=91=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=8C=E6=94=AF=E6=8C=81=E6=89=B9=E9=87=8F=E9=80=89=E6=8B=A9?= =?UTF-8?q?=E5=92=8C=E8=BD=AC=E5=8F=91=E6=B6=88=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Http/Controllers/Api/DialogController.php | 66 ++++++++ app/Models/WebSocketDialogMsg.php | 153 +++++++++++++---- resources/assets/js/functions/web.js | 2 + .../js/pages/manage/components/DialogItem.vue | 25 +++ .../manage/components/DialogView/index.vue | 4 + .../components/DialogView/merge-forward.vue | 78 +++++++++ .../pages/manage/components/DialogWrapper.vue | 97 +++++++++-- .../manage/components/Forwarder/confirm.vue | 60 ++++++- .../manage/components/Forwarder/index.vue | 18 +- .../sass/pages/components/dialog-wrapper.scss | 155 ++++++++++++++++++ .../sass/pages/components/forwarder.scss | 49 ++++++ 11 files changed, 661 insertions(+), 46 deletions(-) create mode 100644 resources/assets/js/pages/manage/components/DialogView/merge-forward.vue diff --git a/app/Http/Controllers/Api/DialogController.php b/app/Http/Controllers/Api/DialogController.php index 9e73988e3..5121f0350 100755 --- a/app/Http/Controllers/Api/DialogController.php +++ b/app/Http/Controllers/Api/DialogController.php @@ -2311,6 +2311,7 @@ class DialogController extends AbstractController { $user = User::auth(); // + $msg_ids = Request::input('msg_ids'); $msg_id = intval(Request::input("msg_id")); $dialogids = Request::input('dialogids'); $userids = Request::input('userids'); @@ -2321,6 +2322,30 @@ class DialogController extends AbstractController return Base::retError("请选择对话或成员"); } // + // 支持批量逐条转发 + if (!empty($msg_ids) && is_array($msg_ids)) { + if (count($msg_ids) > 100) { + return Base::retError("最多转发100条消息"); + } + $allMsgs = []; + $msgs = WebSocketDialogMsg::whereIn('id', $msg_ids)->orderBy('created_at')->get(); + if ($msgs->isEmpty()) { + return Base::retError("消息不存在或已被删除"); + } + WebSocketDialog::checkDialog($msgs->first()->dialog_id); + foreach ($msgs as $msg) { + $res = $msg->forwardMsg($dialogids, $userids, $user, $show_source, $leave_message); + if (Base::isSuccess($res)) { + $allMsgs = array_merge($allMsgs, $res['data']['msgs']); + } + // 留言只在第一条时发送,后续不再重复 + $leave_message = ''; + } + return Base::retSuccess('转发成功', [ + 'msgs' => $allMsgs + ]); + } + // $msg = WebSocketDialogMsg::whereId($msg_id)->first(); if (empty($msg)) { return Base::retError("消息不存在或已被删除"); @@ -2330,6 +2355,47 @@ class DialogController extends AbstractController return $msg->forwardMsg($dialogids, $userids, $user, $show_source, $leave_message); } + /** + * @api {get} api/dialog/msg/merge-forward 合并转发消息 + * + * @apiDescription 需要token身份 + * @apiVersion 1.0.0 + * @apiGroup dialog + * @apiName msg__merge_forward + * + * @apiParam {Array} msg_ids 消息ID数组(最多100条) + * @apiParam {Array} dialogids 转发给的对话ID + * @apiParam {Array} userids 转发给的成员ID + * @apiParam {Number} show_source 是否显示原发送者信息 + * @apiParam {String} leave_message 转发留言 + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function msg__merge_forward() + { + $user = User::auth(); + // + $msg_ids = Request::input('msg_ids'); + $dialogids = Request::input('dialogids'); + $userids = Request::input('userids'); + $show_source = intval(Request::input("show_source")); + $leave_message = Request::input('leave_message'); + // + if (empty($dialogids) && empty($userids)) { + return Base::retError("请选择对话或成员"); + } + if (empty($msg_ids) || !is_array($msg_ids)) { + return Base::retError("请选择要转发的消息"); + } + if (count($msg_ids) > 100) { + return Base::retError("最多转发100条消息"); + } + // + return WebSocketDialogMsg::mergeForwardMsg($msg_ids, $dialogids, $userids, $user, $show_source, $leave_message); + } + /** * @api {get} api/dialog/msg/emoji emoji回复 * diff --git a/app/Models/WebSocketDialogMsg.php b/app/Models/WebSocketDialogMsg.php index 040cc4bfd..d2b61facd 100644 --- a/app/Models/WebSocketDialogMsg.php +++ b/app/Models/WebSocketDialogMsg.php @@ -492,6 +492,47 @@ class WebSocketDialogMsg extends AbstractModel * @param string $leaveMessage 转发留言 * @return mixed */ + /** + * 收集目标对话 + * @param array|int $userids 转发给的成员ID + * @param array|int $dialogids 转发给的对话ID + * @param User $user 当前用户 + * @return array + */ + private static function collectTargetDialogs($userids, $dialogids, $user) + { + $dialogs = []; + if ($userids) { + if (!is_array($userids)) { + $userids = [$userids]; + } + foreach ($userids as $userid) { + if (!User::whereUserid($userid)->exists()) { + continue; + } + $dialog = WebSocketDialog::checkUserDialog($user, $userid); + if ($dialog) { + $dialogs[$dialog->id] = $dialog; + } + } + } + if ($dialogids) { + if (!is_array($dialogids)) { + $dialogids = [$dialogids]; + } + foreach ($dialogids as $dialogid) { + if (isset($dialogs[$dialogid])) { + continue; + } + $dialog = WebSocketDialog::find($dialogid); + if ($dialog) { + $dialogs[$dialog->id] = $dialog; + } + } + } + return $dialogs; + } + public function forwardMsg($dialogids, $userids, $user, $showSource = 1, $leaveMessage = '') { return AbstractModel::transaction(function () use ($dialogids, $user, $userids, $showSource, $leaveMessage) { @@ -513,35 +554,7 @@ class WebSocketDialogMsg extends AbstractModel 'leave' => $leaveMessage ? 1 : 0, // 是否留言(用于判断是否发给AI) ]; $msgs = []; - $dialogs = []; - if ($userids) { - if (!is_array($userids)) { - $userids = [$userids]; - } - foreach ($userids as $userid) { - if (!User::whereUserid($userid)->exists()) { - continue; - } - $dialog = WebSocketDialog::checkUserDialog($user, $userid); - if ($dialog) { - $dialogs[$dialog->id] = $dialog; - } - } - } - if ($dialogids) { - if (!is_array($dialogids)) { - $dialogids = [$dialogids]; - } - foreach ($dialogids as $dialogid) { - if (isset($dialogs[$dialogid])) { - continue; - } - $dialog = WebSocketDialog::find($dialogid); - if ($dialog) { - $dialogs[$dialog->id] = $dialog; - } - } - } + $dialogs = self::collectTargetDialogs($userids, $dialogids, $user); foreach ($dialogs as $dialog) { $res = self::sendMsg('forward-' . $forwardId, $dialog->id, $this->type, $msgData, $user->userid); if (Base::isSuccess($res)) { @@ -564,6 +577,81 @@ class WebSocketDialogMsg extends AbstractModel }); } + /** + * 合并转发消息 + * @param array $msgIds 消息ID数组 + * @param array|int $dialogids 转发给的对话ID + * @param array|int $userids 转发给的成员ID + * @param User $user 当前用户 + * @param int $showSource 是否显示原发送者信息 + * @param string $leaveMessage 转发留言 + * @return array + */ + public static function mergeForwardMsg($msgIds, $dialogids, $userids, $user, $showSource = 1, $leaveMessage = '') + { + return AbstractModel::transaction(function () use ($msgIds, $dialogids, $userids, $user, $showSource, $leaveMessage) { + // 查询并验证所有消息 + $msgs = self::whereIn('id', $msgIds)->orderBy('created_at')->get(); + if ($msgs->isEmpty()) { + throw new ApiException('消息不存在或已被删除'); + } + // 验证所有消息属于同一对话 + $dialogId = $msgs->first()->dialog_id; + if ($msgs->pluck('dialog_id')->unique()->count() > 1) { + throw new ApiException('只能合并转发同一对话的消息'); + } + WebSocketDialog::checkDialog($dialogId); + // 收集发送者生成标题 + $senderIds = $msgs->pluck('userid')->unique()->values()->toArray(); + $senderNames = User::whereIn('userid', array_slice($senderIds, 0, 2)) + ->pluck('nickname') + ->toArray(); + $title = implode(Doo::translate('和'), $senderNames); + if (count($senderIds) > 2) { + $title .= Doo::translate('等人'); + } + $title .= Doo::translate('的聊天记录'); + // 组装消息列表 + $list = []; + foreach ($msgs as $msg) { + $list[] = [ + 'userid' => $msg->userid, + 'type' => $msg->type, + 'msg' => Base::json2array($msg->getRawOriginal('msg')), + 'created_at' => $msg->created_at->toDateTimeString(), + ]; + } + // 构建合并转发消息体 + $msgData = [ + 'title' => $title, + 'list' => $list, + 'count' => count($list), + 'forward_data' => [ + 'show' => $showSource, + 'leave' => $leaveMessage ? 1 : 0, + ], + ]; + $dialogs = self::collectTargetDialogs($userids, $dialogids, $user); + // 发送到每个目标对话 + $result = []; + foreach ($dialogs as $dialog) { + $res = self::sendMsg(null, $dialog->id, 'merge-forward', $msgData, $user->userid); + if (Base::isSuccess($res)) { + $result[] = $res['data']; + } + if ($leaveMessage) { + $res = self::sendMsg(null, $dialog->id, 'text', ['text' => $leaveMessage], $user->userid); + if (Base::isSuccess($res)) { + $result[] = $res['data']; + } + } + } + return Base::retSuccess('转发成功', [ + 'msgs' => $result + ]); + }); + } + /** * 删除消息 * @param array|int $ids @@ -695,6 +783,10 @@ class WebSocketDialogMsg extends AbstractModel case 'template': return self::previewTemplateMsg($data['msg']); + case 'merge-forward': + $action = Doo::translate("聊天记录"); + return "[{$action}] " . Base::cutStr($data['msg']['title'] ?? '', 50); + case 'preview': return $data['msg']['preview']; @@ -1262,6 +1354,9 @@ class WebSocketDialogMsg extends AbstractModel $msg['height'] = $imageSize[1]; } } + if ($type === 'merge-forward') { + $mtype = 'merge-forward'; + } if ($push_silence === null) { $push_silence = !in_array($type, ["text", "file", "record", "meeting"]); } diff --git a/resources/assets/js/functions/web.js b/resources/assets/js/functions/web.js index c63b3208b..7929a6673 100755 --- a/resources/assets/js/functions/web.js +++ b/resources/assets/js/functions/web.js @@ -461,6 +461,8 @@ import {convertLocalResourcePath} from "../components/Replace/utils"; case 'notice': const notice = data.msg.source === 'api' ? data.msg.notice : $A.L(data.msg.notice); return $A.cutString(notice, 50) + case 'merge-forward': + return `[${$A.L('聊天记录')}] ${$A.cutString(data.msg.title || '', 50)}` case 'template': return $A.templateMsgSimpleDesc(data.msg) case 'preview': diff --git a/resources/assets/js/pages/manage/components/DialogItem.vue b/resources/assets/js/pages/manage/components/DialogItem.vue index 54687c9fa..4b334302e 100644 --- a/resources/assets/js/pages/manage/components/DialogItem.vue +++ b/resources/assets/js/pages/manage/components/DialogItem.vue @@ -40,6 +40,9 @@ {{source.msg.source === 'api' ? source.msg.notice : $L(source.msg.notice)}}