Browse Source

Merge branch 'mzh' into test_dev

mazhenhang 1 tháng trước cách đây
mục cha
commit
044b946434

+ 792 - 48
LiangZhiYUMao/pages/message/chat.vue

@@ -117,9 +117,23 @@
                 @click.stop="previewImage(msg.mediaUrl)" />
               
               <!-- 语音消息 -->
-              <view v-else-if="msg.messageType === 3" class="message-voice" @click.stop="playVoice(msg)">
-                <text>🔊</text>
-                <text>{{ msg.duration }}"</text>
+              <view 
+                v-else-if="msg.messageType === 3" 
+                class="message-voice" 
+                :style="{width: getVoiceWidth(msg.duration)}"
+                @click.stop="toggleVoicePlay(msg)">
+                <text class="voice-duration">{{ msg.duration }}''</text>
+                <view class="voice-icon-wrapper" :class="{playing: playingVoiceId === msg.messageId}">
+                  <image class="voice-icon" src="/static/voice-icon.png" mode="aspectFit"></image>
+                </view>
+                <!-- 暂停后的继续播放按钮 -->
+                <view 
+                  v-if="pausedVoiceId === msg.messageId" 
+                  class="voice-resume-btn" 
+                  :class="{'resume-btn-left': msg.fromUserId === userId, 'resume-btn-right': msg.fromUserId !== userId}"
+                  @click.stop="resumeVoice">
+                  <text class="resume-icon">▶</text>
+                </view>
               </view>
               
               <!-- 视频消息 -->
@@ -180,18 +194,20 @@
         v-else
         class="voice-button"
         @touchstart="startVoiceRecord"
-        @touchend="stopVoiceRecord">
+        @touchmove="onVoiceTouchMove"
+        @touchend="stopVoiceRecord"
+        @touchcancel="cancelVoiceRecord">
         按住说话
       </button>
       
       <!-- 表情按钮 -->
-      <view class="input-icon" @click="showEmojiPanel = !showEmojiPanel">
+      <view v-if="inputType === 'text'" class="input-icon" @click="showEmojiPanel = !showEmojiPanel">
         <text>😊</text>
       </view>
       
-      <!-- 更多按钮 -->
-      <view class="input-icon" @click="showMorePanel = !showMorePanel">
-        <text></text>
+      <!-- 发送按钮 -->
+      <view v-if="inputType === 'text'" class="send-button" :class="{disabled: !inputText.trim()}" @click="sendTextMessage">
+        <text>发送</text>
       </view>
     </view>
 
@@ -205,22 +221,6 @@
         {{ emoji }}
       </text>
     </view>
-
-    <!-- 更多功能面板 -->
-    <view v-if="showMorePanel" class="more-panel">
-      <view class="more-item" @click="chooseImage">
-        <view class="more-icon">🖼️</view>
-        <text class="more-text">图片</text>
-      </view>
-      <view class="more-item" @click="chooseVideo">
-        <view class="more-icon">📹</view>
-        <text class="more-text">视频</text>
-      </view>
-      <view class="more-item" @click="chooseFile">
-        <view class="more-icon">📁</view>
-        <text class="more-text">文件</text>
-      </view>
-    </view>
     
     <!-- 消息操作菜单 -->
     <view v-if="showMessageAction" class="message-action-mask" @click="hideMessageMenu">
@@ -242,6 +242,22 @@
         </view>
       </view>
     </view>
+    
+    <!-- 录音中提示 -->
+    <view v-if="showVoiceRecording" class="voice-recording-mask">
+      <view class="voice-recording-box" :class="{'canceling': voiceCanceling}">
+        <view class="voice-wave-container">
+          <view class="voice-wave-bar bar1" :style="{transform: `scaleY(${voiceVolume})`}"></view>
+          <view class="voice-wave-bar bar2" :style="{transform: `scaleY(${voiceVolume * 1.2})`}"></view>
+          <view class="voice-wave-bar bar3" :style="{transform: `scaleY(${voiceVolume * 1.5})`}"></view>
+          <view class="voice-wave-bar bar4" :style="{transform: `scaleY(${voiceVolume * 1.3})`}"></view>
+          <view class="voice-wave-bar bar5" :style="{transform: `scaleY(${voiceVolume})`}"></view>
+        </view>
+        <text class="voice-text">{{ voiceCanceling ? '松开取消发送' : '正在录音...' }}</text>
+        <text class="voice-time">{{ voiceRecordingTime }}''</text>
+        <text class="voice-tip" v-if="!voiceCanceling">松开发送,上滑取消</text>
+      </view>
+    </view>
   </view>
 </template>
 
@@ -266,7 +282,6 @@ export default {
       
       scrollToView: '',
       showEmojiPanel: false,
-      showMorePanel: false,
       
       isLogin: false,
       
@@ -287,7 +302,23 @@ export default {
 	    showBlockConfirmModal: false, // 控制拉黑确认弹窗显示
 		isVip: false, // 是否VIP用户
 	    remainingCount: 5, // 剩余可发送消息数(非VIP默认5)
-	    hasMessageLimit: true // 是否有发送限制(VIP为false)
+	    hasMessageLimit: true, // 是否有发送限制(VIP为false)
+	    
+	    // 语音录制相关
+	    recorderManager: null, // 录音管理器
+	    isRecording: false, // 是否正在录音
+	    voiceStartTime: 0, // 录音开始时间
+	    voiceDuration: 0, // 录音时长
+	    voiceTempPath: '', // 录音临时文件路径
+	    showVoiceRecording: false, // 显示录音中提示
+	    voiceRecordingTime: 0, // 录音计时(秒)
+	    voiceRecordingTimer: null, // 录音计时器
+	    voiceTouchStartY: 0, // 录音按下时的Y坐标
+	    voiceCanceling: false, // 是否正在取消录音
+	    voiceVolume: 0.3, // 录音音量(0-1),控制波形高度
+	    playingVoiceId: null, // 当前播放的语音消息ID
+	    pausedVoiceId: null, // 当前暂停的语音消息ID
+	    currentAudioContext: null // 当前音频上下文
 	};
   },
   
@@ -388,9 +419,31 @@ export default {
   onUnload() {
     // 页面卸载时移除监听
     timManager.offMessage(this.handleNewMessage);
+    
+    // 移除消息状态变更监听(已注释)
+    /*
+    if (timManager.tim && this.handleStatusChange) {
+      timManager.tim.off(TIM.EVENT.MESSAGE_STATUS_CHANGED, this.handleStatusChange);
+    }
+    */
   },
   
   methods: {
+    /**
+     * 计算语音消息宽度(根据时长动态变化)
+     * @param {Number} duration 语音时长(秒)
+     * @return {String} 宽度值
+     */
+    getVoiceWidth(duration) {
+      // 基础宽度 100rpx,每秒增加 10rpx,最大 400rpx
+      const baseWidth = 100;
+      const widthPerSecond = 10;
+      const maxWidth = 400;
+      
+      const width = Math.min(baseWidth + (duration * widthPerSecond), maxWidth);
+      return width + 'rpx';
+    },
+    
     /**
      * 初始化 TIM
      */
@@ -669,9 +722,29 @@ export default {
       timManager.onMessage(handleNewMessage);
       
       // 添加消息状态变更监听
+      // 注释原因:TIM.EVENT.MESSAGE_STATUS_CHANGED 在当前SDK版本中可能不存在,导致参数验证失败
+      // 消息状态更新已通过 handleNewMessage 处理,暂时不需要单独监听
+      /*
       if (timManager.tim) {
-        timManager.tim.on(TIM.EVENT.MESSAGE_STATUS_CHANGED, handleNewMessage);
+        const handleStatusChange = (event) => {
+          console.log('📝 消息状态变更:', event);
+          // MESSAGE_STATUS_CHANGED 事件的数据结构不同
+          if (event && event.data && Array.isArray(event.data)) {
+            event.data.forEach(msg => {
+              if (msg.conversationID === this.conversationID) {
+                const existingIndex = this.messages.findIndex(m => m.messageId === msg.ID);
+                if (existingIndex > -1) {
+                  this.$set(this.messages, existingIndex, this.convertMessage(msg));
+                }
+              }
+            });
+          }
+        };
+        
+        this.handleStatusChange = handleStatusChange;
+        timManager.tim.on(TIM.EVENT.MESSAGE_STATUS_CHANGED, handleStatusChange);
       }
+      */
     },
     
     /**
@@ -695,16 +768,53 @@ export default {
         sendStatus = 1; // 发送中
       }
     
+      // 处理不同类型消息的URL和时长
+      let mediaUrl = '';
+      let duration = 0;
+      let messageType = 1;
+      let content = '';
+      
+      if (timMsg.type === TIM.TYPES.MSG_TEXT) {
+        messageType = 1;
+        content = timMsg.payload.text;
+      } else if (timMsg.type === TIM.TYPES.MSG_IMAGE && timMsg.payload.imageInfoArray) {
+        messageType = 2;
+        content = '[图片]';
+        mediaUrl = timMsg.payload.imageInfoArray[0]?.url || '';
+      } else if (timMsg.type === TIM.TYPES.MSG_SOUND) {
+        // 原生语音消息
+        messageType = 3;
+        content = '[语音]';
+        mediaUrl = timMsg.payload.url || timMsg.payload.remoteAudioUrl || '';
+        duration = timMsg.payload.second || 0;
+      } else if (timMsg.type === TIM.TYPES.MSG_CUSTOM) {
+        // 自定义消息(我们的语音消息)
+        try {
+          const customData = JSON.parse(timMsg.payload.data);
+          if (customData.type === 'voice') {
+            messageType = 3;
+            content = '[语音]';
+            mediaUrl = customData.url;
+            // duration已经是秒数,直接使用
+            duration = parseInt(customData.duration) || 0;
+          }
+        } catch (e) {
+          console.error('解析自定义消息失败:', e);
+        }
+      } else if (timMsg.type === TIM.TYPES.MSG_VIDEO) {
+        messageType = 4;
+        content = '[视频]';
+      }
+      
       return {
         messageId: timMsg.ID,
         fromUserId: timMsg.from,
         toUserId: timMsg.to,
-        messageType: timMsg.type === TIM.TYPES.MSG_TEXT ? 1 : 
-                     timMsg.type === TIM.TYPES.MSG_IMAGE ? 2 :
-                     timMsg.type === TIM.TYPES.MSG_SOUND ? 3 :
-                     timMsg.type === TIM.TYPES.MSG_VIDEO ? 4 : 1,
-        content: timMsg.type === TIM.TYPES.MSG_TEXT ? timMsg.payload.text : '[图片]',
-        mediaUrl: timMsg.type === TIM.TYPES.MSG_IMAGE ? timMsg.payload.imageInfoArray?.[0]?.url : '',
+        messageType: messageType,
+        content: content || '[消息]',
+        mediaUrl: mediaUrl,
+        duration: duration,
+        payload: timMsg.payload, // 保留原始payload
         sendStatus: sendStatus,
         sendTime: new Date(timMsg.time * 1000),
         isRecalled: timMsg.isRevoked,
@@ -898,8 +1008,6 @@ export default {
           }
         }
       });
-      
-      this.showMorePanel = false;
     },
     
     /**
@@ -934,6 +1042,27 @@ export default {
           syncData.thumbnailUrl = imageInfo.imageUrl;
         }
         
+        // 如果是语音消息,添加语音信息
+        if (timMessage.type === 'TIMSoundElem' && timMessage.payload) {
+          syncData.mediaUrl = timMessage.payload.url || timMessage.payload.remoteAudioUrl;
+          syncData.duration = timMessage.payload.second || 0;
+          syncData.mediaSize = timMessage.payload.size || 0;
+        }
+        
+        // 如果是自定义消息(我们的语音消息)
+        if (timMessage.type === 'TIMCustomElem' && timMessage.payload) {
+          try {
+            const customData = JSON.parse(timMessage.payload.data);
+            if (customData.type === 'voice') {
+              syncData.mediaUrl = customData.url;
+              syncData.duration = customData.duration;
+              syncData.mediaSize = customData.size || 0;
+            }
+          } catch (e) {
+            console.error('解析自定义消息失败:', e);
+          }
+        }
+        
         // 调用后端同步接口
         const res = await uni.request({
           url: 'http://localhost:1004/api/chat/syncTIMMessage',
@@ -966,6 +1095,19 @@ export default {
         'TIMVideoFileElem': 4, // 视频
         'TIMFileElem': 5       // 文件
       };
+      
+      // 处理自定义消息(我们的语音消息)
+      if (timMessage.type === 'TIMCustomElem' && timMessage.payload) {
+        try {
+          const customData = JSON.parse(timMessage.payload.data);
+          if (customData.type === 'voice') {
+            return 3; // 语音消息
+          }
+        } catch (e) {
+          console.error('解析自定义消息类型失败:', e);
+        }
+      }
+      
       return typeMap[timMessage.type] || 1;
     },
     
@@ -1287,18 +1429,345 @@ export default {
     switchInputType() {
       this.inputType = this.inputType === 'text' ? 'voice' : 'text';
     },
-    startVoiceRecord() {
-      uni.showToast({
-        title: '按住说话',
-        icon: 'none'
-      });
+    
+    /**
+     * 开始录音
+     */
+    startVoiceRecord(e) {
+      console.log('🎤 [按住说话] startVoiceRecord 方法被调用');
+      
+      // 记录触摸起始位置
+      this.voiceTouchStartY = e.touches[0].clientY;
+      this.voiceCanceling = false;
+      
+      // 检查消息发送限制
+      if (this.hasMessageLimit && this.remainingCount <= 0) {
+        uni.showToast({
+          title: '今日消息发送次数已用完',
+          icon: 'none'
+        });
+        return;
+      }
+      
+      // 初始化录音管理器
+      if (!this.recorderManager) {
+        console.log('📱 初始化录音管理器');
+        this.recorderManager = uni.getRecorderManager();
+        
+        // 录音开始
+        this.recorderManager.onStart(() => {
+          console.log('✅ 录音开始回调触发');
+          this.isRecording = true;
+          this.showVoiceRecording = true;
+          this.voiceStartTime = Date.now();
+          console.log('   - isRecording:', this.isRecording);
+          console.log('   - showVoiceRecording:', this.showVoiceRecording);
+        });
+        
+        // 录音结束
+        this.recorderManager.onStop((res) => {
+          console.log('🎤 录音结束:', res, ', voiceCanceling:', this.voiceCanceling);
+          this.isRecording = false;
+          this.showVoiceRecording = false;
+          
+          // 如果是取消状态,不发送
+          if (this.voiceCanceling) {
+            console.log('❌ 录音已取消,不发送');
+            this.voiceCanceling = false;
+            return;
+          }
+          
+          this.voiceTempPath = res.tempFilePath;
+          this.voiceDuration = Math.floor(res.duration / 1000); // 转换为秒
+          
+          console.log('   - 文件路径:', this.voiceTempPath);
+          console.log('   - 时长:', this.voiceDuration, '秒');
+          
+          // 时长验证
+          if (this.voiceDuration < 1) {
+            uni.showToast({
+              title: '录音时间太短',
+              icon: 'none'
+            });
+            return;
+          }
+          
+          if (this.voiceDuration > 60) {
+            uni.showToast({
+              title: '录音时间不能超过60秒',
+              icon: 'none'
+            });
+            return;
+          }
+          
+          // 发送语音消息
+          this.sendVoiceMessage();
+        });
+        
+        // 录音错误
+        this.recorderManager.onError((err) => {
+          console.error('❌ 录音错误:', err);
+          this.isRecording = false;
+          this.showVoiceRecording = false;
+          uni.showToast({
+            title: '录音失败',
+            icon: 'none'
+          });
+        });
+      }
+      
+      // 立即设置录音状态(不等待onStart回调)
+      this.isRecording = true;
+      this.showVoiceRecording = true;
+      this.voiceStartTime = Date.now();
+      this.voiceRecordingTime = 0;
+      this.voiceVolume = 0.3;
+      console.log('🎬 立即设置录音状态');
+      console.log('   - isRecording:', this.isRecording);
+      console.log('   - showVoiceRecording:', this.showVoiceRecording);
+      
+      // 启动计时器
+      let volumeDirection = 1;
+      let currentVolume = 0.3;
+      this.voiceRecordingTimer = setInterval(() => {
+        this.voiceRecordingTime = Math.floor((Date.now() - this.voiceStartTime) / 1000);
+        
+        // 模拟更真实的音量波动
+        // 随机决定是否有声音(80%概率有声音)
+        if (Math.random() > 0.2) {
+          // 有声音:平滑波动
+          currentVolume += volumeDirection * (0.1 + Math.random() * 0.2);
+          if (currentVolume > 1.0) {
+            currentVolume = 1.0;
+            volumeDirection = -1;
+          } else if (currentVolume < 0.4) {
+            currentVolume = 0.4;
+            volumeDirection = 1;
+          }
+          this.voiceVolume = currentVolume;
+        } else {
+          // 无声音:降低到最小
+          this.voiceVolume = Math.max(0.2, this.voiceVolume * 0.8);
+        }
+      }, 100);
+      
+      // 开始录音
+      console.log('🎙️ 调用 recorderManager.start()');
+      try {
+        this.recorderManager.start({
+          format: 'mp3',
+          sampleRate: 16000,
+          numberOfChannels: 1,
+          encodeBitRate: 48000
+        });
+        console.log('✅ recorderManager.start() 调用成功');
+      } catch (err) {
+        console.error('❌ recorderManager.start() 调用失败:', err);
+        this.isRecording = false;
+        this.showVoiceRecording = false;
+      }
     },
+    
+    /**
+     * 停止录音
+     */
     stopVoiceRecord() {
+      console.log('🎤 [松开] stopVoiceRecord 方法被调用, isRecording:', this.isRecording, ', voiceCanceling:', this.voiceCanceling);
+      
+      // 清除计时器
+      if (this.voiceRecordingTimer) {
+        clearInterval(this.voiceRecordingTimer);
+        this.voiceRecordingTimer = null;
+      }
+      
+      // 如果正在取消,则取消录音
+      if (this.voiceCanceling) {
+        console.log('🚫 检测到取消状态,调用 cancelVoiceRecord');
+        this.cancelVoiceRecord();
+        return;
+      }
+      
+      if (this.isRecording && this.recorderManager) {
+        const duration = Date.now() - this.voiceStartTime;
+        
+        // 录音时间太短
+        if (duration < 1000) {
+          this.recorderManager.stop();
+          this.isRecording = false;
+          this.showVoiceRecording = false;
+          uni.showToast({
+            title: '录音时间太短',
+            icon: 'none'
+          });
+          return;
+        }
+        
+        this.recorderManager.stop();
+      }
+    },
+    
+    /**
+     * 触摸移动 - 检测上滑取消
+     */
+    onVoiceTouchMove(e) {
+      if (!this.isRecording) return;
+      
+      const currentY = e.touches[0].clientY;
+      const deltaY = this.voiceTouchStartY - currentY;
+      
+      // 上滑超过100px,显示取消提示
+      if (deltaY > 100) {
+        this.voiceCanceling = true;
+        console.log('⬆️ 上滑取消录音');
+      } else {
+        this.voiceCanceling = false;
+      }
+    },
+    
+    /**
+     * 取消录音
+     */
+    cancelVoiceRecord() {
+      console.log('❌ 取消录音, voiceCanceling:', this.voiceCanceling);
+      
+      // 清除计时器
+      if (this.voiceRecordingTimer) {
+        clearInterval(this.voiceRecordingTimer);
+        this.voiceRecordingTimer = null;
+      }
+      
+      // 先设置取消状态,再停止录音
+      // 这样onStop回调中能正确判断
+      if (!this.voiceCanceling) {
+        this.voiceCanceling = true;
+      }
+      
+      if (this.recorderManager && this.isRecording) {
+        this.recorderManager.stop();  // 这会触发onStop回调
+      } else {
+        // 如果没有在录音,直接重置状态
+        this.isRecording = false;
+        this.showVoiceRecording = false;
+        this.voiceCanceling = false;
+      }
+      
       uni.showToast({
-        title: '语音功能开发中',
+        title: '已取消录音',
         icon: 'none'
       });
     },
+    
+    /**
+     * 发送语音消息
+     */
+    async sendVoiceMessage() {
+      // 检查是否被拉黑
+      const isBlocked = await this.checkIsBlockedByTarget();
+      if (isBlocked) {
+        uni.showToast({
+          title: '你已被对方拉黑,无法发送消息',
+          icon: 'none'
+        });
+        return;
+      }
+      
+      try {
+        uni.showLoading({ title: '上传中...' });
+        
+        // 第一步:上传语音文件到MinIO
+        const uploadResult = await this.uploadVoiceToMinIO();
+        if (!uploadResult.success) {
+          throw new Error(uploadResult.message || '上传失败');
+        }
+        
+        console.log('✅ 语音文件上传成功:', uploadResult.fileUrl);
+        uni.showLoading({ title: '发送中...' });
+        
+        // 第二步:通过腾讯IM发送语音消息(携带MinIO URL)
+        const message = await timManager.sendVoiceMessage(
+          this.targetUserId,
+          uploadResult.fileUrl,
+          this.voiceDuration,
+          uploadResult.fileSize
+        );
+        
+        // 添加到消息列表
+        this.messages.push(this.convertMessage(message));
+        this.scrollToBottom();
+        
+        uni.hideLoading();
+        console.log('✅ 语音消息发送成功');
+        
+        // 同步到MySQL
+        this.syncMessageToMySQL(message);
+        
+        // 更新消息计数
+        if (!this.isVip) {
+          await this.updateMessageCount();
+        }
+        
+      } catch (error) {
+        uni.hideLoading();
+        console.error('❌ 语音消息发送失败:', error);
+        uni.showToast({
+          title: '发送失败: ' + error.message,
+          icon: 'none'
+        });
+      }
+    },
+    
+    /**
+     * 上传语音文件到MinIO
+     */
+    async uploadVoiceToMinIO() {
+      try {
+        console.log('📤 开始上传语音文件到MinIO...');
+        console.log('   - 文件路径:', this.voiceTempPath);
+        console.log('   - 时长:', this.voiceDuration, '秒');
+        
+        // 使用uni.uploadFile上传到后端MinIO接口
+        const [err, res] = await uni.uploadFile({
+          url: 'http://localhost:1004/api/voice/upload',
+          filePath: this.voiceTempPath,
+          name: 'file',
+          header: {
+            'Content-Type': 'multipart/form-data'
+          }
+        });
+        
+        if (err) {
+          console.error('❌ 上传请求失败:', err);
+          return {
+            success: false,
+            message: '网络请求失败'
+          };
+        }
+        
+        // 解析响应
+        const result = JSON.parse(res.data);
+        console.log('📥 MinIO上传响应:', result);
+        
+        if (result.success) {
+          return {
+            success: true,
+            fileUrl: result.fileUrl,
+            fileSize: result.fileSize
+          };
+        } else {
+          return {
+            success: false,
+            message: result.message || '上传失败'
+          };
+        }
+        
+      } catch (error) {
+        console.error('❌ 上传语音文件失败:', error);
+        return {
+          success: false,
+          message: error.message || '上传异常'
+        };
+      }
+    },
     insertEmoji(emoji) {
       this.inputText += emoji;
       this.showEmojiPanel = false;
@@ -1308,20 +1777,118 @@ export default {
         title: '视频功能开发中',
         icon: 'none'
       });
-      this.showMorePanel = false;
     },
     chooseFile() {
       uni.showToast({
         title: '文件功能开发中',
         icon: 'none'
       });
-      this.showMorePanel = false;
     },
-    playVoice() {
-      uni.showToast({
-        title: '语音播放功能开发中',
-        icon: 'none'
+    /**
+     * 切换语音播放/暂停
+     */
+    toggleVoicePlay(msg) {
+      // 如果正在播放这条语音,则暂停
+      if (this.playingVoiceId === msg.messageId) {
+        this.pauseVoice(msg);
+      } else {
+        // 否则开始播放
+        this.playVoice(msg);
+      }
+    },
+    
+    /**
+     * 播放语音
+     */
+    playVoice(msg) {
+      console.log('📢 [播放语音] 开始 - messageId:', msg.messageId);
+      
+      if (!msg.mediaUrl) {
+        // 如果是TIM消息,从payload中获取URL
+        const audioUrl = msg.payload?.url || msg.payload?.remoteAudioUrl;
+        if (!audioUrl) {
+          uni.showToast({
+            title: '语音文件不存在',
+            icon: 'none'
+          });
+          return;
+        }
+        msg.mediaUrl = audioUrl;
+      }
+      
+      // 如果有其他语音正在播放,先停止
+      if (this.currentAudioContext) {
+        console.log('⏹️ 停止之前的语音');
+        this.currentAudioContext.stop();
+        this.currentAudioContext.destroy();
+        this.currentAudioContext = null;
+      }
+      
+      // 清除暂停状态
+      this.pausedVoiceId = null;
+      
+      // 设置当前播放的语音ID,触发动画
+      this.playingVoiceId = msg.messageId;
+      console.log('🎬 [动画开始] playingVoiceId =', this.playingVoiceId);
+      
+      // 创建音频上下文
+      const innerAudioContext = uni.createInnerAudioContext();
+      innerAudioContext.src = msg.mediaUrl;
+      innerAudioContext.loop = false; // 不循环播放
+      this.currentAudioContext = innerAudioContext;
+      
+      innerAudioContext.onPlay(() => {
+        console.log('🔊 开始播放语音');
+      });
+      
+      innerAudioContext.onEnded(() => {
+        console.log('✅ 语音播放完成');
+        this.playingVoiceId = null; // 停止动画
+        this.pausedVoiceId = null;
+        this.currentAudioContext = null;
+        console.log('🛑 [动画停止] playingVoiceId =', this.playingVoiceId);
+        innerAudioContext.destroy();
       });
+      
+      innerAudioContext.onError((err) => {
+        console.error('❌ 语音播放失败:', err);
+        this.playingVoiceId = null; // 停止动画
+        this.pausedVoiceId = null;
+        this.currentAudioContext = null;
+        console.log('🛑 [动画停止-错误] playingVoiceId =', this.playingVoiceId);
+        uni.showToast({
+          title: '播放失败',
+          icon: 'none'
+        });
+        innerAudioContext.destroy();
+      });
+      
+      innerAudioContext.play();
+    },
+    
+    /**
+     * 暂停语音
+     */
+    pauseVoice(msg) {
+      if (this.currentAudioContext) {
+        this.currentAudioContext.pause();
+        this.playingVoiceId = null; // 停止动画
+        this.pausedVoiceId = msg.messageId; // 设置暂停状态
+        console.log('⏸️ [暂停] playingVoiceId =', this.playingVoiceId, ', pausedVoiceId =', this.pausedVoiceId);
+      }
+    },
+    
+    /**
+     * 继续播放语音
+     */
+    resumeVoice() {
+      if (this.currentAudioContext && this.pausedVoiceId) {
+        this.currentAudioContext.play();
+        this.playingVoiceId = this.pausedVoiceId; // 恢复动画
+        const resumedId = this.pausedVoiceId;
+        this.pausedVoiceId = null; // 清除暂停状态
+        console.log('▶️ [继续播放] playingVoiceId =', this.playingVoiceId, ', pausedVoiceId =', this.pausedVoiceId);
+      }
     },
 	showMoreOptions() {
 	    this.showMoreOptionsModal = true;
@@ -1722,7 +2289,79 @@ export default {
 .message-voice {
   display: flex;
   align-items: center;
-  gap: 10rpx;
+  gap: 12rpx;
+  padding: 0rpx 0;
+  min-width: 30rpx;
+  max-width: 400rpx;
+  cursor: pointer;
+  -webkit-tap-highlight-color: transparent;
+  outline: none;
+  border: none;
+  position: relative;
+}
+
+.voice-duration {
+  font-size: 27rpx;
+  color: #333;
+  flex-shrink: 0;
+  line-height: 1;
+}
+
+.voice-icon-wrapper {
+  display: flex;
+  flex-shrink: 0;
+  line-height: 0;
+  position: relative;
+  top: 1rpx;
+}
+
+.voice-icon {
+  width: 22rpx;
+  height: 22rpx;
+  display: block;
+  transform: none;
+  animation: none;
+}
+
+.voice-icon-wrapper.playing .voice-icon {
+  animation: voice-bounce 0.6s ease-in-out infinite;
+}
+
+.voice-resume-btn {
+  position: absolute;
+  top: 50%;
+  transform: translateY(-50%);
+  width: 32rpx;
+  height: 32rpx;
+  background: #07c160;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  box-shadow: 0 2rpx 8rpx rgba(7, 193, 96, 0.3);
+}
+
+.voice-resume-btn.resume-btn-left {
+  left: -40rpx;
+}
+
+.voice-resume-btn.resume-btn-right {
+  right: -40rpx;
+}
+
+.resume-icon {
+  color: #fff;
+  font-size: 16rpx;
+  margin-left: 2rpx;
+}
+
+@keyframes voice-bounce {
+  0%, 100% {
+    transform: scale(1);
+  }
+  50% {
+    transform: scale(1.2);
+  }
 }
 
 .message-video {
@@ -1777,6 +2416,24 @@ export default {
   font-size: 50rpx;
 }
 
+.send-button {
+  padding: 0 30rpx;
+  height: 70rpx;
+  line-height: 70rpx;
+  background-color: #07c160;
+  color: #fff;
+  border-radius: 8rpx;
+  text-align: center;
+  font-size: 28rpx;
+  margin-left: 10rpx;
+  transition: opacity 0.3s;
+}
+
+.send-button.disabled {
+  opacity: 0.5;
+  background-color: #95ec69;
+}
+
 .input-field {
   flex: 1;
   height: 70rpx;
@@ -1961,6 +2618,93 @@ export default {
 .vip-link:active {
   opacity: 0.7; /* 点击时透明度降低 */
 }
+
+/* 录音中提示遮罩 */
+.voice-recording-mask {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background-color: rgba(0, 0, 0, 0.6);
+  z-index: 10000;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+/* 录音提示框 */
+.voice-recording-box {
+  width: 300rpx;
+  height: 300rpx;
+  background-color: rgba(0, 0, 0, 0.8);
+  border-radius: 20rpx;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  color: #fff;
+  transition: background-color 0.3s;
+}
+
+.voice-recording-box.canceling {
+  background-color: rgba(220, 38, 38, 0.9);
+}
+
+
+.voice-wave-container {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 8rpx;
+  height: 100rpx;
+  margin-bottom: 20rpx;
+}
+
+.voice-wave-bar {
+  width: 6rpx;
+  background: #fff;
+  border-radius: 3rpx;
+  transition: transform 0.1s ease-out;
+  transform-origin: center;
+}
+
+.voice-wave-bar.bar1 {
+  height: 30rpx;
+}
+
+.voice-wave-bar.bar2 {
+  height: 45rpx;
+}
+
+.voice-wave-bar.bar3 {
+  height: 60rpx;
+}
+
+.voice-wave-bar.bar4 {
+  height: 50rpx;
+}
+
+.voice-wave-bar.bar5 {
+  height: 35rpx;
+}
+
+.voice-text {
+  font-size: 32rpx;
+  margin-bottom: 10rpx;
+}
+
+.voice-time {
+  font-size: 48rpx;
+  font-weight: bold;
+  color: #07c160;
+  margin: 10rpx 0;
+}
+
+.voice-tip {
+  font-size: 24rpx;
+  color: #ccc;
+}
 </style>
 
 

+ 9 - 0
LiangZhiYUMao/pages/message/index.vue

@@ -794,6 +794,15 @@ export default {
         case 'TIMFileElem':
           return '[文件]';
         case 'TIMCustomElem':
+          // 处理自定义消息(我们的语音消息)
+          try {
+            const customData = JSON.parse(message.payload.data);
+            if (customData.type === 'voice') {
+              return '[语音]';
+            }
+          } catch (e) {
+            console.error('解析自定义消息失败:', e);
+          }
           return '[自定义消息]';
         default:
           return '[未知消息]';

BIN
LiangZhiYUMao/static/voice-icon.png


+ 92 - 0
LiangZhiYUMao/utils/tim-manager.js

@@ -97,6 +97,19 @@ class TIMManager {
           'TIMVideoFileElem': 4,
           'TIMFileElem': 5
         };
+        
+        // 处理自定义消息(我们的语音消息)
+        if (msg.type === 'TIMCustomElem' && msg.payload) {
+          try {
+            const customData = JSON.parse(msg.payload.data);
+            if (customData.type === 'voice') {
+              return 3; // 语音消息
+            }
+          } catch (e) {
+            console.error('解析自定义消息类型失败:', e);
+          }
+        }
+        
         return typeMap[msg.type] || 1;
       };
       
@@ -113,6 +126,17 @@ class TIMManager {
             return '[视频]';
           case 'TIMFileElem':
             return '[文件]';
+          case 'TIMCustomElem':
+            // 处理自定义消息
+            try {
+              const customData = JSON.parse(msg.payload.data);
+              if (customData.type === 'voice') {
+                return '[语音]';
+              }
+            } catch (e) {
+              console.error('解析自定义消息内容失败:', e);
+            }
+            return '[自定义消息]';
           default:
             return '[未知消息]';
         }
@@ -127,6 +151,34 @@ class TIMManager {
         sendTime: timMessage.time
       };
       
+      // 如果是语音消息,添加语音信息
+      if (timMessage.type === 'TIMSoundElem' && timMessage.payload) {
+        syncData.mediaUrl = timMessage.payload.url || timMessage.payload.remoteAudioUrl;
+        syncData.duration = timMessage.payload.second || 0;
+        syncData.mediaSize = timMessage.payload.size || 0;
+      }
+      
+      // 如果是自定义消息(我们的语音消息)
+      if (timMessage.type === 'TIMCustomElem' && timMessage.payload) {
+        try {
+          const customData = JSON.parse(timMessage.payload.data);
+          if (customData.type === 'voice') {
+            syncData.mediaUrl = customData.url;
+            syncData.duration = customData.duration;
+            syncData.mediaSize = customData.size || 0;
+          }
+        } catch (e) {
+          console.error('解析自定义消息失败:', e);
+        }
+      }
+      
+      // 如果是图片消息,添加图片信息
+      if (timMessage.type === 'TIMImageElem' && timMessage.payload.imageInfoArray) {
+        const imageInfo = timMessage.payload.imageInfoArray[0];
+        syncData.mediaUrl = imageInfo.imageUrl;
+        syncData.thumbnailUrl = imageInfo.imageUrl;
+      }
+      
       // 调用后端同步接口
       const res = await uni.request({
         url: 'http://localhost:1004/api/chat/syncTIMMessage',
@@ -279,6 +331,46 @@ class TIMManager {
     }
   }
 
+  /**
+   * 发送语音消息(通过自定义消息携带MinIO URL)
+   * @param {String} toUserId 接收者ID
+   * @param {String} voiceUrl MinIO语音文件URL
+   * @param {Number} duration 语音时长(秒)
+   * @param {Number} fileSize 文件大小(字节)
+   */
+  async sendVoiceMessage(toUserId, voiceUrl, duration, fileSize) {
+    try {
+      console.log('🎤 准备发送语音消息:');
+      console.log('   - 接收者ID:', toUserId);
+      console.log('   - 语音URL:', voiceUrl);
+      console.log('   - 时长:', duration, '秒');
+      console.log('   - 文件大小:', fileSize, '字节');
+
+      // 使用自定义消息发送语音信息
+      const message = this.tim.createCustomMessage({
+        to: String(toUserId),
+        conversationType: TIM.TYPES.CONV_C2C,
+        payload: {
+          data: JSON.stringify({
+            type: 'voice',
+            url: voiceUrl,
+            duration: duration,
+            size: fileSize
+          }),
+          description: '[语音]',
+          extension: voiceUrl
+        }
+      });
+
+      const res = await this.tim.sendMessage(message);
+      console.log('✅ 语音发送成功');
+      return res.data.message;
+    } catch (error) {
+      console.error('❌ 语音发送失败:', error);
+      throw error;
+    }
+  }
+
   /**
    * 获取会话列表
    */

+ 7 - 0
service/websocket/pom.xml

@@ -111,6 +111,13 @@
             <version>2.0</version>
         </dependency>
 
+        <!-- MinIO 对象存储 -->
+        <dependency>
+            <groupId>io.minio</groupId>
+            <artifactId>minio</artifactId>
+            <version>8.5.2</version>
+        </dependency>
+
         <!-- Test -->
         <dependency>
             <groupId>org.springframework.boot</groupId>

+ 47 - 0
service/websocket/src/main/java/com/zhentao/config/MinioConfig.java

@@ -0,0 +1,47 @@
+package com.zhentao.config;
+
+import io.minio.MinioClient;
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * MinIO配置类
+ */
+@Configuration
+@ConfigurationProperties(prefix = "minio")
+@Data
+public class MinioConfig {
+
+    /**
+     * MinIO服务地址
+     */
+    private String endpoint;
+
+    /**
+     * 访问密钥
+     */
+    private String accessKey;
+
+    /**
+     * 秘密密钥
+     */
+    private String secretKey;
+
+    /**
+     * 存储桶名称
+     */
+    private String bucketName;
+
+    /**
+     * 创建MinioClient Bean
+     */
+    @Bean
+    public MinioClient minioClient() {
+        return MinioClient.builder()
+                .endpoint(endpoint)
+                .credentials(accessKey, secretKey)
+                .build();
+    }
+}

+ 123 - 0
service/websocket/src/main/java/com/zhentao/controller/VoiceUploadController.java

@@ -0,0 +1,123 @@
+package com.zhentao.controller;
+
+import com.zhentao.utils.MinioUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 语音文件上传控制器
+ */
+@RestController
+@RequestMapping("/api/voice")
+@Slf4j
+public class VoiceUploadController {
+
+    @Autowired
+    private MinioUtil minioUtil;
+
+    /**
+     * 上传语音文件
+     *
+     * @param file 语音文件
+     * @return 文件URL和相关信息
+     */
+    @PostMapping("/upload")
+    public ResponseEntity<Map<String, Object>> uploadVoice(@RequestParam("file") MultipartFile file) {
+        Map<String, Object> result = new HashMap<>();
+        
+        try {
+            // 验证文件
+            if (file.isEmpty()) {
+                result.put("success", false);
+                result.put("message", "文件不能为空");
+                return ResponseEntity.badRequest().body(result);
+            }
+
+            // 验证文件类型
+            String originalFilename = file.getOriginalFilename();
+            if (originalFilename == null || !isValidVoiceFile(originalFilename)) {
+                result.put("success", false);
+                result.put("message", "不支持的文件类型,仅支持 mp3, wav, ogg, m4a, amr, aac");
+                return ResponseEntity.badRequest().body(result);
+            }
+
+            // 验证文件大小(最大10MB)
+            long maxSize = 10 * 1024 * 1024;
+            if (file.getSize() > maxSize) {
+                result.put("success", false);
+                result.put("message", "文件大小不能超过10MB");
+                return ResponseEntity.badRequest().body(result);
+            }
+
+            // 上传到MinIO
+            byte[] fileBytes = file.getBytes();
+            String fileUrl = minioUtil.uploadVoiceFile(fileBytes, originalFilename);
+
+            log.info("✅ 语音文件上传成功: {}, 大小: {} bytes", originalFilename, file.getSize());
+
+            // 返回结果
+            result.put("success", true);
+            result.put("message", "上传成功");
+            result.put("fileUrl", fileUrl);
+            result.put("fileName", originalFilename);
+            result.put("fileSize", file.getSize());
+            
+            return ResponseEntity.ok(result);
+
+        } catch (Exception e) {
+            log.error("❌ 语音文件上传失败: {}", e.getMessage(), e);
+            result.put("success", false);
+            result.put("message", "上传失败: " + e.getMessage());
+            return ResponseEntity.status(500).body(result);
+        }
+    }
+
+    /**
+     * 验证是否为有效的语音文件
+     *
+     * @param filename 文件名
+     * @return 是否有效
+     */
+    private boolean isValidVoiceFile(String filename) {
+        String lowerFilename = filename.toLowerCase();
+        return lowerFilename.endsWith(".mp3") ||
+               lowerFilename.endsWith(".wav") ||
+               lowerFilename.endsWith(".ogg") ||
+               lowerFilename.endsWith(".m4a") ||
+               lowerFilename.endsWith(".amr") ||
+               lowerFilename.endsWith(".aac");
+    }
+
+    /**
+     * 删除语音文件
+     *
+     * @param fileUrl 文件URL
+     * @return 删除结果
+     */
+    @DeleteMapping("/delete")
+    public ResponseEntity<Map<String, Object>> deleteVoice(@RequestParam("fileUrl") String fileUrl) {
+        Map<String, Object> result = new HashMap<>();
+        
+        try {
+            minioUtil.deleteFileByUrl(fileUrl);
+            
+            log.info("✅ 语音文件删除成功: {}", fileUrl);
+            
+            result.put("success", true);
+            result.put("message", "删除成功");
+            return ResponseEntity.ok(result);
+
+        } catch (Exception e) {
+            log.error("❌ 语音文件删除失败: {}", e.getMessage(), e);
+            result.put("success", false);
+            result.put("message", "删除失败: " + e.getMessage());
+            return ResponseEntity.status(500).body(result);
+        }
+    }
+}

+ 0 - 1
service/websocket/src/main/java/com/zhentao/dto/BlockDto.java

@@ -1,6 +1,5 @@
 package com.zhentao.dto;
 
-import afu.org.checkerframework.checker.igj.qual.I;
 import lombok.Data;
 
 @Data

+ 260 - 0
service/websocket/src/main/java/com/zhentao/utils/MinioUtil.java

@@ -0,0 +1,260 @@
+package com.zhentao.utils;
+
+import io.minio.*;
+import io.minio.http.Method;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * MinIO工具类
+ * 用于上传、下载、删除文件等操作
+ */
+@Component
+@Slf4j
+public class MinioUtil {
+
+    @Autowired
+    private MinioClient minioClient;
+
+    @Value("${minio.bucket-name}")
+    private String bucketName;
+
+    @Value("${minio.endpoint}")
+    private String endpoint;
+
+    /**
+     * 初始化,检查并创建bucket
+     */
+    @PostConstruct
+    public void init() {
+        try {
+            createBucketIfNotExists();
+            log.info("✅ MinIO初始化成功,Bucket: {}", bucketName);
+        } catch (Exception e) {
+            log.error("❌ MinIO初始化失败: {}", e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 检查bucket是否存在,不存在则创建
+     */
+    private void createBucketIfNotExists() throws Exception {
+        boolean exists = minioClient.bucketExists(
+                BucketExistsArgs.builder()
+                        .bucket(bucketName)
+                        .build()
+        );
+
+        if (!exists) {
+            minioClient.makeBucket(
+                    MakeBucketArgs.builder()
+                            .bucket(bucketName)
+                            .build()
+            );
+            log.info("✅ 创建Bucket成功: {}", bucketName);
+        }
+    }
+
+    /**
+     * 上传文件(字节数组)
+     *
+     * @param fileBytes 文件字节数组
+     * @param fileName  文件名
+     * @param contentType 文件类型
+     * @return 文件访问URL
+     */
+    public String uploadFile(byte[] fileBytes, String fileName, String contentType) throws Exception {
+        try (InputStream inputStream = new ByteArrayInputStream(fileBytes)) {
+            return uploadFile(inputStream, fileName, contentType, fileBytes.length);
+        }
+    }
+
+    /**
+     * 上传文件(输入流)
+     *
+     * @param inputStream 文件输入流
+     * @param fileName    文件名
+     * @param contentType 文件类型
+     * @param fileSize    文件大小
+     * @return 文件访问URL
+     */
+    public String uploadFile(InputStream inputStream, String fileName, String contentType, long fileSize) throws Exception {
+        // 生成唯一文件名
+        String objectName = generateUniqueFileName(fileName);
+
+        // 上传文件
+        minioClient.putObject(
+                PutObjectArgs.builder()
+                        .bucket(bucketName)
+                        .object(objectName)
+                        .stream(inputStream, fileSize, -1)
+                        .contentType(contentType)
+                        .build()
+        );
+
+        log.info("✅ 文件上传成功: {}", objectName);
+
+        // 返回文件访问URL
+        return getFileUrl(objectName);
+    }
+
+    /**
+     * 上传语音文件
+     *
+     * @param fileBytes 语音文件字节数组
+     * @param originalFileName 原始文件名
+     * @return 文件访问URL
+     */
+    public String uploadVoiceFile(byte[] fileBytes, String originalFileName) throws Exception {
+        String contentType = getContentType(originalFileName);
+        return uploadFile(fileBytes, originalFileName, contentType);
+    }
+
+    /**
+     * 获取文件URL(永久访问)
+     *
+     * @param objectName 对象名称
+     * @return 文件访问URL
+     */
+    public String getFileUrl(String objectName) {
+        // 返回完整的访问路径
+        return String.format("%s/%s/%s", endpoint, bucketName, objectName);
+    }
+
+    /**
+     * 获取预签名URL(临时访问,7天有效)
+     *
+     * @param objectName 对象名称
+     * @return 预签名URL
+     */
+    public String getPresignedUrl(String objectName) throws Exception {
+        return minioClient.getPresignedObjectUrl(
+                GetPresignedObjectUrlArgs.builder()
+                        .method(Method.GET)
+                        .bucket(bucketName)
+                        .object(objectName)
+                        .expiry(7, TimeUnit.DAYS)
+                        .build()
+        );
+    }
+
+    /**
+     * 删除文件
+     *
+     * @param objectName 对象名称
+     */
+    public void deleteFile(String objectName) throws Exception {
+        minioClient.removeObject(
+                RemoveObjectArgs.builder()
+                        .bucket(bucketName)
+                        .object(objectName)
+                        .build()
+        );
+        log.info("✅ 文件删除成功: {}", objectName);
+    }
+
+    /**
+     * 根据URL删除文件
+     *
+     * @param fileUrl 文件URL
+     */
+    public void deleteFileByUrl(String fileUrl) throws Exception {
+        String objectName = extractObjectNameFromUrl(fileUrl);
+        deleteFile(objectName);
+    }
+
+    /**
+     * 从URL中提取对象名称
+     *
+     * @param fileUrl 文件URL
+     * @return 对象名称
+     */
+    private String extractObjectNameFromUrl(String fileUrl) {
+        // 格式: http://115.190.125.125:9000/minion-voice-files/voice/xxx.mp3
+        String prefix = String.format("%s/%s/", endpoint, bucketName);
+        if (fileUrl.startsWith(prefix)) {
+            return fileUrl.substring(prefix.length());
+        }
+        return fileUrl;
+    }
+
+    /**
+     * 生成唯一文件名
+     *
+     * @param originalFileName 原始文件名
+     * @return 唯一文件名
+     */
+    private String generateUniqueFileName(String originalFileName) {
+        String uuid = UUID.randomUUID().toString().replace("-", "");
+        String extension = getFileExtension(originalFileName);
+        String dateFolder = new java.text.SimpleDateFormat("yyyy/MM/dd").format(new java.util.Date());
+        return String.format("voice/%s/%s%s", dateFolder, uuid, extension);
+    }
+
+    /**
+     * 获取文件扩展名
+     *
+     * @param fileName 文件名
+     * @return 扩展名(包含点号)
+     */
+    private String getFileExtension(String fileName) {
+        if (fileName == null || !fileName.contains(".")) {
+            return "";
+        }
+        return fileName.substring(fileName.lastIndexOf("."));
+    }
+
+    /**
+     * 根据文件名获取Content-Type
+     *
+     * @param fileName 文件名
+     * @return Content-Type
+     */
+    private String getContentType(String fileName) {
+        String extension = getFileExtension(fileName).toLowerCase();
+        switch (extension) {
+            case ".mp3":
+                return "audio/mpeg";
+            case ".wav":
+                return "audio/wav";
+            case ".ogg":
+                return "audio/ogg";
+            case ".m4a":
+                return "audio/mp4";
+            case ".amr":
+                return "audio/amr";
+            case ".aac":
+                return "audio/aac";
+            default:
+                return "application/octet-stream";
+        }
+    }
+
+    /**
+     * 检查文件是否存在
+     *
+     * @param objectName 对象名称
+     * @return 是否存在
+     */
+    public boolean fileExists(String objectName) {
+        try {
+            minioClient.statObject(
+                    StatObjectArgs.builder()
+                            .bucket(bucketName)
+                            .object(objectName)
+                            .build()
+            );
+            return true;
+        } catch (Exception e) {
+            return false;
+        }
+    }
+}

+ 1 - 1
service/websocket/src/main/java/com/zhentao/vo/UserVipVo.java

@@ -1,6 +1,6 @@
 package com.zhentao.vo;
 
-import afu.org.checkerframework.checker.igj.qual.I;
+//import afu.org.checkerframework.checker.igj.qual.I;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;

+ 7 - 0
service/websocket/src/main/resources/application.yml

@@ -77,3 +77,10 @@ mybatis-plus:
 tim:
   sdkappid: 1600109674
   secret: cb2c6ade9b960ea635ff969f79054e8b582efe7aa75e4008c63042b0bbed1e7c
+
+# MinIO 对象存储配置
+minio:
+  endpoint: http://115.190.125.125:9000
+  access-key: minioadmin
+  secret-key: minioadmin
+  bucket-name: minion-voice-files