From b64a0deae5b5d33f9e20671a68936b27f0b9b00b Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期二, 21 七月 2026 18:03:03 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/dev_pro2.0' into dev_pro2.0
---
src/views/im/home/composables/useMessageSender.ts | 337 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 337 insertions(+), 0 deletions(-)
diff --git a/src/views/im/home/composables/useMessageSender.ts b/src/views/im/home/composables/useMessageSender.ts
new file mode 100644
index 0000000..4562128
--- /dev/null
+++ b/src/views/im/home/composables/useMessageSender.ts
@@ -0,0 +1,337 @@
+import type { Conversation, Message } from '../types'
+
+import { readChannelMessages as apiReadChannelMessages } from '#/api/im/message/channel'
+import {
+ readGroupMessages as apiReadGroupMessages,
+ recallGroupMessage as apiRecallGroupMessage,
+ sendGroupMessage as apiSendGroupMessage
+} from '#/api/im/message/group'
+import {
+ getPrivateMaxReadMessageId as apiGetPrivateMaxReadMessageId,
+ readPrivateMessages as apiReadPrivateMessages,
+ recallPrivateMessage as apiRecallPrivateMessage,
+ sendPrivateMessage as apiSendPrivateMessage
+} from '#/api/im/message/private'
+import { getCurrentUserId } from '#/views/im/utils/auth'
+
+import { MESSAGE_GROUP_READ_ENABLED, MESSAGE_PRIVATE_READ_ENABLED } from '../../utils/config'
+import { ImContentType, ImConversationType, ImMessageStatus } from '../../utils/constants'
+import { getClientConversationId } from '../../utils/db'
+import {
+ generateClientMessageId,
+ type QuoteMessage,
+ serializeMessage,
+ type TextMessage,
+ withQuotePayload
+} from '../../utils/message'
+import { useConversationStore } from '../store/conversationStore'
+import { useMessageStore } from '../store/messageStore'
+
+/** 闈炴枃鏈秷鎭殑鎵╁睍閫夐」锛堥�氱敤锛� */
+interface SendExtOptions {
+ atUserIds?: number[] // 缇よ亰 @ 鐨勭敤鎴风紪鍙峰垪琛�
+ receipt?: boolean // 鏄惁闇�瑕佺兢鍥炴墽锛堥粯璁� false锛�
+ targetId?: number // 瑕嗙洊榛樿鐨� targetId
+ /**
+ * 鏄惧紡鎸囧畾鐩爣浼氳瘽锛堣浆鍙� / 鍚嶇墖鎺ㄨ崘鍦烘櫙锛�
+ *
+ * 涓嶄紶鏃堕粯璁ゅ彇 conversationStore.activeConversation锛涗紶鍏ユ椂鎸夋湰鍊煎彂閫� + 涔愯鏇存柊鍒板搴斾細璇濓紝
+ * 涓嶈姹傝浼氳瘽褰撳墠鏄縺娲荤姸鎬侊紙閫傚悎鍙戠粰銆岄潪褰撳墠浼氳瘽銆嶇殑澶氫釜鐩爣锛�
+ */
+ conversation?: Conversation
+ /** 琚紩鐢ㄦ秷鎭紙鍙�夛級锛氬啓杩� content.quote 鐢ㄤ簬涔愯娓叉煋锛屾湇鍔$鎸� quote.messageId 鍙嶆煡閲嶇畻瑕嗙洊 */
+ quote?: QuoteMessage
+ /**
+ * 澶嶇敤宸插瓨鍦ㄧ殑鏈湴鍗犱綅娑堟伅 clientMessageId锛堝獟浣撲笂浼犲満鏅級
+ *
+ * 濯掍綋涓婁紶閾捐矾鍦ㄨ姹傛湇鍔$鍓嶅凡缁� insertMessage 浜嗗崰浣嶏紙甯� blob URL + 杩涘害鏉★級锛�
+ * 杩欓噷璺宠繃 buildLocalMessage / insertMessage锛岀洿鎺ユ嬁杩欎釜 id 璧� ackMessage 鏀跺熬锛岄伩鍏嶉噸澶嶆彃鍏ヤ袱鏉�
+ */
+ existingClientMessageId?: string
+}
+
+/**
+ * 娑堟伅鍙戦�� / 鎾ゅ洖 / 宸茶 缁勫悎寮忛�昏緫
+ *
+ * 璁捐瑕佺偣锛�
+ * 1. 绉佽亰 / 缇よ亰鎺ュ彛绛惧悕瀵圭О锛屾寜 conversation.type 鍒嗘敮璋冨害锛屽樊寮傚湪鍒嗘敮鍐呴儴娑堝寲
+ * 2. 鍙戦�佽蛋銆屼箰瑙傛洿鏂般�嶏細鍏� insertMessage 鍐欏叆 SENDING 鍗犱綅锛岃姹傛垚鍔� ackMessage 鏇存柊涓� NORMAL锛屽け璐ユ洿鏂颁负 FAILED
+ * 3. 鎾ゅ洖涓嶅仛涔愯鏇存柊锛氭湇鍔$閫氳繃 WebSocket RECALL 浜嬩欢鍥炰紶锛岀敱 websocketStore 缁熶竴鏇存柊鐘舵�侊紝閬垮厤缃戠粶澶辫触鍚庝笉鍙洖閫�
+ * 4. 宸茶涓婃姤锛氭湰绔珛鍒绘竻鏈鏁板苟璁板綍鏈湴璇讳綅缃紱鎺ュ彛澶辫触浠呰褰曟棩蹇�
+ */
+export const useMessageSender = () => {
+ const conversationStore = useConversationStore()
+ const messageStore = useMessageStore()
+
+ /** 鏋勯�犳湰鍦颁箰瑙傛秷鎭璞� */
+ const buildLocalMessage = (opts: {
+ atUserIds?: number[]
+ clientMessageId: string
+ content: string
+ targetId: number
+ type: number
+ }): Message => {
+ return {
+ clientMessageId: opts.clientMessageId,
+ type: opts.type,
+ content: opts.content,
+ status: ImMessageStatus.SENDING,
+ sendTime: Date.now(),
+ senderId: getCurrentUserId(),
+ targetId: opts.targetId,
+ selfSend: true,
+ atUserIds: opts.atUserIds
+ }
+ }
+
+ /**
+ * 鍙戦�佷换鎰忕被鍨嬬殑娑堟伅锛堝簳灞傚疄鐜帮級
+ * 1. 鏂囨湰銆佸浘鐗囥�佹枃浠躲�佽闊崇瓑閮借蛋杩欓噷
+ * 2. type / content 鐢辫皟鐢ㄦ柟鏋勯��
+ * 3. 杩斿洖鍊硷細鎴愬姛 true / 澶辫触 false锛堝け璐ユ椂鏈湴鍗犱綅宸叉爣 FAILED锛夛紱鍙傛暟缂哄け绛夋棤娉曞彂閫佺殑鍦烘櫙涔熻繑 false
+ * 杞彂 / 鍚嶇墖鎺ㄨ崘绛夊満鏅寜杩斿洖鍊煎喅瀹氭槸鍚︾户缁悗缁姩浣滐紙濡傛湁鐣欒█鏃朵粎鍦ㄥ悕鐗囨垚鍔熷悗鍐嶅彂鐣欒█锛�
+ */
+ const sendRaw = async (
+ type: number,
+ content: string,
+ options?: SendExtOptions
+ ): Promise<boolean> => {
+ // 1. 鍙傛暟鏍¢獙锛氫紭鍏堢敤鏄惧紡浼犲叆鐨� conversation锛堣浆鍙戝満鏅級锛屽惁鍒欏彇婵�娲讳細璇�
+ const conversation = options?.conversation ?? conversationStore.activeConversation
+ if (!conversation) {
+ return false
+ }
+ const realTarget = options?.targetId || conversation.targetId
+ if (!realTarget) {
+ return false
+ }
+
+ // 2. 鍑嗗 clientMessageId锛氬獟浣撲笂浼犻摼璺湪 step 1 宸茬粡 insertMessage 鍗犱綅锛岃繖閲岀洿鎺ュ鐢� id锛涘叾浣欏満鏅蛋榛樿涔愯鎻掑叆
+ let clientMessageId: string
+ if (options?.existingClientMessageId) {
+ clientMessageId = options.existingClientMessageId
+ // 鍗犱綅鑻ュ凡琚垹闄わ紙涓婁紶鏈熼棿鐢ㄦ埛鍙抽敭鍒犻櫎 / 鎾ゅ洖 / removeMessage 绛夛級鍒欐斁寮冨彂閫侊紝
+ // 鍚﹀垯 sendRaw 浠嶄細鎶婃秷鎭帹鍒版湇鍔$锛屽鑷�"鏈湴鏃犳皵娉� / 瀵规柟鍗存敹鍒颁竴鏉�"
+ const stillExists = messageStore
+ .getMessageList(conversation.type, realTarget)
+ .some((message) => message.clientMessageId === clientMessageId && !message._ackMerging)
+ if (!stillExists) {
+ return false
+ }
+ } else {
+ clientMessageId = generateClientMessageId()
+ const message = buildLocalMessage({
+ clientMessageId,
+ content,
+ targetId: realTarget,
+ type,
+ atUserIds: options?.atUserIds
+ })
+ const conversationInfo = {
+ type: conversation.type,
+ targetId: realTarget,
+ name: conversation.name || String(realTarget),
+ avatar: conversation.avatar || ''
+ }
+ void messageStore.insertMessage(conversationInfo, message).catch(() => undefined)
+ }
+
+ // 3. 鍙戦�佽姹傦細鎸変細璇濈被鍨嬪垎鍙戝埌涓嶅悓鎺ュ彛锛涙垚鍔熷悗 ackMessage 鏇存柊涓� NORMAL锛屽け璐ユ洿鏂颁负 FAILED
+ try {
+ if (conversation.type === ImConversationType.PRIVATE) {
+ const data = await apiSendPrivateMessage({
+ clientMessageId,
+ receiverId: realTarget,
+ type,
+ content
+ })
+ void messageStore
+ .ackMessage(conversation.type, realTarget, clientMessageId, {
+ id: data.id,
+ sendTime: new Date(data.sendTime).getTime(),
+ status: data.status,
+ receiptStatus: data.receiptStatus,
+ content: data.content
+ })
+ .catch(() => undefined)
+ } else if (conversation.type === ImConversationType.GROUP) {
+ const data = await apiSendGroupMessage({
+ clientMessageId,
+ groupId: realTarget,
+ type,
+ content,
+ atUserIds: options?.atUserIds,
+ receipt: options?.receipt
+ })
+ void messageStore
+ .ackMessage(conversation.type, realTarget, clientMessageId, {
+ id: data.id,
+ sendTime: new Date(data.sendTime).getTime(),
+ status: data.status,
+ receiptStatus: data.receiptStatus,
+ readCount: data.readCount,
+ content: data.content
+ })
+ .catch(() => undefined)
+ }
+ return true
+ } catch (error) {
+ console.error('[IM] 娑堟伅鍙戦�佸け璐�', { type, realTarget, clientMessageId }, error)
+ void messageStore
+ .ackMessage(conversation.type, realTarget, clientMessageId, {
+ status: ImMessageStatus.FAILED
+ })
+ .catch(() => undefined)
+ return false
+ }
+ }
+
+ /**
+ * 鍙戦�佹枃鏈秷鎭紙鏈�甯哥敤鐨勫揩鎹峰叆鍙o級锛歮essage-input.vue 鏂囨湰鍥炶溅璧拌繖閲�
+ * 杩斿洖鍊硷細鎴愬姛 true / 澶辫触 false / 绌烘枃鏈� false锛堜笌 sendRaw 瀵归綈锛岃浆鍙戝満鏅寜杩斿洖鍊煎垽鏂級
+ */
+ const send = async (text: string, options?: SendExtOptions): Promise<boolean> => {
+ if (!text.trim()) {
+ return false
+ }
+ const payload = withQuotePayload<TextMessage>({ content: text }, options?.quote)
+ return sendRaw(ImContentType.TEXT, serializeMessage(payload), options)
+ }
+
+ /**
+ * 鎾ゅ洖鏌愭潯娑堟伅
+ * 1. 鏈嶅姟绔細閫氳繃 WebSocket RECALL 浜嬩欢鍥炰紶锛屾湰绔� UI 鐢� websocketStore 缁熶竴鏇存柊
+ * 2. 姝ゅ涓嶅仛涔愯鎾ゅ洖锛岄伩鍏嶇綉缁滃け璐ュ悗鐘舵�佷笉鍙洖閫�
+ */
+ const recall = async (message: Message) => {
+ // 鍙傛暟鏍¢獙锛氭湰鍦板崰浣嶆秷鎭笉鑳芥挙鍥�
+ if (!message.id) {
+ return
+ }
+ const conversation = conversationStore.activeConversation
+ if (!conversation) {
+ return
+ }
+ // 绉佽亰 / 缇よ亰鎺ュ彛绛惧悕涓�鑷达紝鎸変細璇濈被鍨嬪垎鍙�
+ const isPrivate = conversation.type === ImConversationType.PRIVATE
+ try {
+ await (isPrivate ? apiRecallPrivateMessage(message.id) : apiRecallGroupMessage(message.id))
+ } catch (error) {
+ console.error('[IM] 鎾ゅ洖澶辫触', { messageId: message.id, type: conversation.type }, error)
+ }
+ }
+
+ /**
+ * 瑙﹀彂褰撳墠浼氳瘽鐨勫凡璇讳笂鎶ワ紙鍒囦細璇� / 杩涘叆椤甸潰鏃惰皟鐢級
+ * 1. 鏈绔嬪埢娓呮湭璇绘暟骞舵帹杩涜浣嶇疆
+ * 2. 宸茶浣嶇疆鍙栧凡鍔犺浇娑堟伅鍜屼細璇濇湯鏉℃秷鎭殑鏈�澶ф湇鍔$ id
+ */
+ const readActive = async () => {
+ const conversation = conversationStore.activeConversation
+ if (!conversation) {
+ return
+ }
+ let loadedMaxMessageId = 0
+ for (const message of messageStore.getMessages(
+ getClientConversationId(conversation.type, conversation.targetId)
+ )) {
+ if (message.id && message.id > loadedMaxMessageId) {
+ loadedMaxMessageId = message.id
+ }
+ }
+ const maxMessageId = Math.max(loadedMaxMessageId, conversation.lastMessageId || 0)
+ const readReported = conversationStore.isReportedReadPositionCovered(
+ conversation.type,
+ conversation.targetId,
+ maxMessageId
+ )
+ if (readReported) {
+ conversationStore.markConversationRead(conversation.type, conversation.targetId)
+ return
+ }
+ const isPrivate = conversation.type === ImConversationType.PRIVATE
+ const isGroup = conversation.type === ImConversationType.GROUP
+ const isChannel = conversation.type === ImConversationType.CHANNEL
+ // 鏈湴鏍囪宸茶锛氭湭璇绘暟娓呴浂锛圲I 绔嬪埢鍝嶅簲锛�
+ conversationStore.markConversationRead(conversation.type, conversation.targetId, maxMessageId)
+ if (!maxMessageId) {
+ return
+ }
+ // 鎺ュ彛璋冪敤锛氭寜浼氳瘽绫诲瀷鍒嗗彂锛屽苟鎸夊搴斿凡璇诲紑鍏虫帶鍒�
+ if (!isPrivate && !isGroup && !isChannel) {
+ return
+ }
+ if (isPrivate && !MESSAGE_PRIVATE_READ_ENABLED) {
+ return
+ }
+ if (isGroup && !MESSAGE_GROUP_READ_ENABLED) {
+ return
+ }
+ try {
+ if (isPrivate) {
+ await apiReadPrivateMessages(conversation.targetId, maxMessageId)
+ } else if (isGroup) {
+ await apiReadGroupMessages(conversation.targetId, maxMessageId)
+ } else {
+ await apiReadChannelMessages(conversation.targetId, maxMessageId)
+ }
+ conversationStore.markConversationReadReported(
+ conversation.type,
+ conversation.targetId,
+ maxMessageId
+ )
+ } catch (error) {
+ console.error(
+ '[IM] 鏍囪宸茶澶辫触',
+ { type: conversation.type, targetId: conversation.targetId, maxMessageId },
+ error
+ )
+ }
+ }
+
+ /**
+ * 鎷夊彇銆屽鏂瑰凡璇诲埌鎴戝摢鏉℃秷鎭�嶅苟琛ラ綈鏈湴鐘舵��
+ *
+ * 1. 寮ヨˉ绂荤嚎 / 澶氱鏈熼棿閿欒繃鐨� RECEIPT 鎺ㄩ�侊細杩涘叆绉佽亰浼氳瘽鎴栨柇绾块噸杩炲悗璋冧竴娆★紝
+ * 鎶婂鏂� maxReadId 鍚屾鍒版湰鍦版秷鎭� status锛岄伩鍏嶅鏂规槑鏄庤浜嗐�佹湰绔嵈浠嶆樉绀烘湭璇�
+ * 2. 浠呯鑱婁娇鐢細缇よ亰宸茶浣嶇疆鍦ㄦ瘡鏉℃秷鎭殑 readCount / receiptStatus 瀛楁锛岀绾挎媺鍙栬嚜甯﹀洖
+ */
+ const syncPrivateReadStatus = async (peerId: number) => {
+ if (!peerId) {
+ return
+ }
+ // 绉佽亰宸茶鍏抽棴锛氳烦杩囧鏂瑰凡璇讳綅缃悓姝ワ紝閬垮厤鏃犺皳鎺ュ彛璋冪敤
+ if (!MESSAGE_PRIVATE_READ_ENABLED) {
+ return
+ }
+ const cachedMaxReadId = messageStore.getPrivateReadMaxId(peerId)
+ if (cachedMaxReadId !== undefined) {
+ if (cachedMaxReadId > 0) {
+ messageStore.applyMessageReadReceipt({
+ conversationType: ImConversationType.PRIVATE,
+ targetId: peerId,
+ privateReadMaxId: cachedMaxReadId
+ })
+ }
+ return
+ }
+ try {
+ // 鎷夊彇瀵规柟宸茶鍒扮殑鏈�澶ф秷鎭� id
+ const maxReadId = await apiGetPrivateMaxReadMessageId(peerId)
+ messageStore.updatePrivateReadMaxId(peerId, maxReadId)
+ if (!maxReadId) {
+ return
+ }
+ // applyMessageReadReceipt 鍐呴儴鎶� 鈮� maxReadId 鐨勬湰绔秷鎭洖鎵ф洿鏂颁负 DONE
+ messageStore.applyMessageReadReceipt({
+ conversationType: ImConversationType.PRIVATE,
+ targetId: peerId,
+ privateReadMaxId: maxReadId
+ })
+ } catch (error) {
+ console.warn('[IM] 鎷夊彇瀵规柟宸茶浣嶇疆澶辫触', { peerId }, error)
+ }
+ }
+
+ return { send, sendRaw, recall, readActive, syncPrivateReadStatus }
+}
--
Gitblit v1.9.3