Parcourir la source

今日缘分 点击不感兴趣 屏蔽到redis 不再显示 点击打招呼可以发预设消息

YH_0525 il y a 1 mois
Parent
commit
d8d6c97134

+ 78 - 10
LiangZhiYUMao/pages/index/index.vue

@@ -125,7 +125,7 @@
 		</view>
 
 		<!-- 今日缘分推荐 -->
-		<!-- <view class="section fate-recommend-section">
+		<!-- <view class="section fate-recommend-section" v-if="todayRecommendUsers && todayRecommendUsers.length > 0">
 			<view class="section-header">
 				<text class="section-title">🌟 今日缘分</text>
 				<text class="section-more" @click="goToRecommend">查看更多 ></text>
@@ -134,21 +134,21 @@
 				<view class="fate-card" v-for="(user, index) in todayRecommendUsers" :key="index" 
 					@click="handleUserClick(user)">
 					<view class="user-avatar-wrapper">
-						<image :src="user.avatar" class="user-avatar" mode="aspectFill"
+						<image :src="user.avatarUrl || user.avatar" class="user-avatar" mode="aspectFill"
 							:data-section="'todayRecommend'" :data-index="index"
 							@error="handleImageError" @load="handleImageLoad"></image>
 						<view class="online-status" v-if="user.isOnline"></view>
 					</view>
 					<view class="user-basic-info">
 						<text class="user-nickname">{{ user.nickname }}</text>
-						<text class="user-age-location">{{ user.age }}岁 · {{ user.city }}</text>
+						<text class="user-age-location">{{ user.age }}岁 · {{ user.location || user.city }}</text>
 					</view>
 					<view class="match-score">
 						<text class="match-text">匹配度</text>
-						<text class="match-percentage">{{ user.matchScore }}%</text>
+						<text class="match-percentage">{{ formatMatchScore(user.compatibilityScore) }}%</text>
 					</view>
 					<view class="user-tags">
-						<text class="user-tag" v-for="(tag, tagIndex) in user.tags.slice(0, 2)" :key="tagIndex">
+						<text class="user-tag" v-for="(tag, tagIndex) in parseUserTags(user).slice(0, 2)" :key="tagIndex">
 							{{ tag }}
 						</text>
 					</view>
@@ -362,17 +362,77 @@ onShow() {
 			// 加载今日推荐
 			async loadTodayRecommend() {
 				try {
-					// 调用 API 获取推荐用户
-					const data = await api.recommend.getTodayRecommend()
+					const userId = this.userInfo.userId || parseInt(uni.getStorageSync('userId'))
+					if (!userId) {
+						console.log('用户未登录,跳过今日推荐加载')
+						return
+					}
+					
+					// 调用推荐API获取推荐用户列表
+					const data = await api.recommend.getUsers({
+						userId: userId,
+						oppoOnly: 1,  // 只推荐异性
+						limit: 20     // 获取20个用户用于筛选
+					})
+					
 					if (data && data.length > 0) {
-					    this.todayRecommendUsers = data
+						// 按匹配度降序排序,取前3个匹配度最高的用户
+						const sortedUsers = data
+							.filter(user => user.compatibilityScore && user.compatibilityScore > 0)
+							.sort((a, b) => (b.compatibilityScore || 0) - (a.compatibilityScore || 0))
+							.slice(0, 3)
+						
+						// 处理用户数据
+						this.todayRecommendUsers = sortedUsers.map(user => ({
+							...user,
+							avatarUrl: this.validateImageUrl(user.avatarUrl || DEFAULT_IMAGES.avatar),
+							age: user.age || 0,
+							location: this.formatLocation(user),
+							isOnline: false  // 可以根据实际情况设置
+						}))
+						
+						console.log('今日缘分推荐加载成功,匹配度最高的用户:', this.todayRecommendUsers)
 					}
 				} catch (error) {
 					console.error('获取推荐用户失败:', error)
-					// 使用默认数据
+					this.todayRecommendUsers = []
 				}
 			},
 
+			// 格式化位置信息
+			formatLocation(user) {
+				const parts = []
+				if (user.provinceName) parts.push(user.provinceName)
+				if (user.cityName) parts.push(user.cityName)
+				if (parts.length === 0 && user.location) return user.location
+				if (parts.length === 0 && user.city) return user.city
+				return parts.join(' ')
+			},
+
+			// 格式化匹配度分数
+			formatMatchScore(score) {
+				if (!score) return '0.0'
+				return Number(score).toFixed(1)
+			},
+
+			// 解析用户标签
+			parseUserTags(user) {
+				const tags = []
+				if (user.star) tags.push(user.star)
+				if (user.jobTitle) tags.push(user.jobTitle)
+				if (user.hobby) {
+					try {
+						const hobbies = typeof user.hobby === 'string' ? JSON.parse(user.hobby) : user.hobby
+						if (Array.isArray(hobbies)) {
+							tags.push(...hobbies.slice(0, 2))
+						}
+					} catch (e) {
+						console.error('解析兴趣爱好失败:', e)
+					}
+				}
+				return tags
+			},
+
 			// 加载魅力指数
 			async loadCharmIndex() {
 				try {
@@ -683,8 +743,16 @@ onShow() {
 
 			// 处理用户点击
 			handleUserClick(user) {
+				const userId = user.userId || user.id
+				if (!userId) {
+					uni.showToast({
+						title: '用户信息错误',
+						icon: 'none'
+					})
+					return
+				}
 				uni.navigateTo({
-					url: `/pages/profile/index?userId=${user.id}`
+					url: `/pages/recommend/user-detail?userId=${userId}`
 				})
 			},
 

+ 26 - 233
LiangZhiYUMao/pages/message/chat.vue

@@ -124,10 +124,7 @@
                 @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="msg.fromUserId === userId ? 'http://115.190.125.125:9000/static-images/%E6%88%91%E6%96%B9%E8%AF%AD%E9%9F%B3%E6%B6%88%E6%81%AF' : 'http://115.190.125.125:9000/static-images/%E5%AF%B9%E6%96%B9%E8%AF%AD%E9%9F%B3%E6%B6%88%E6%81%AF'" 
-                    mode="aspectFit"></image>
+                  <image class="voice-icon" src="/static/voice-icon.png" mode="aspectFit"></image>
                 </view>
                 <!-- 暂停后的继续播放按钮 -->
                 <view 
@@ -267,7 +264,6 @@
 <script>
 import timManager from '@/utils/tim-manager.js';
 import TIM from 'tim-wx-sdk';
-import presenceManager from '@/utils/presence-manager.js';
 
 export default {
   data() {
@@ -322,9 +318,7 @@ export default {
 	    voiceVolume: 0.3, // 录音音量(0-1),控制波形高度
 	    playingVoiceId: null, // 当前播放的语音消息ID
 	    pausedVoiceId: null, // 当前暂停的语音消息ID
-	    currentAudioContext: null, // 当前音频上下文
-	    onlineStatusPollingTimer: null, // 在线状态轮询定时器
-	    voiceRecordCancelled: false // 录音是否已被取消(防止权限允许后自动恢复)
+	    currentAudioContext: null // 当前音频上下文
 	};
   },
   
@@ -421,11 +415,16 @@ export default {
     // 监听新消息
     this.listenMessages();
     
-    // 初始化在线状态监听
-    this.initPresence();
-    
-    // 发送已读回执(标记对方发来的消息为已读)
-    this.sendReadReceipt();
+    // 如果有预设消息,自动发送
+    if (options.message) {
+      const message = decodeURIComponent(options.message);
+      console.log('检测到预设消息,准备自动发送:', message);
+      // 等待一下再发送,确保TIM已经初始化完成
+      setTimeout(() => {
+        this.inputText = message;
+        this.sendTextMessage();
+      }, 1500);
+    }
   },
   
   onUnload() {
@@ -438,9 +437,6 @@ export default {
       timManager.tim.off(TIM.EVENT.MESSAGE_STATUS_CHANGED, this.handleStatusChange);
     }
     */
-    
-    // 清理在线状态监听
-    this.cleanupPresence();
   },
   
   methods: {
@@ -545,7 +541,7 @@ export default {
         
         // 导入当前用户(确保userId是字符串)
         const currentUserRes = await uni.request({
-          url: 'http://localhost:8083/api/im/importUser',
+          url: 'http://localhost:1004/api/im/importUser',
           method: 'POST',
           data: {
             userId: String(this.userId),
@@ -560,7 +556,7 @@ export default {
         
         // 导入目标用户(确保userId是字符串)
         const targetUserRes = await uni.request({
-          url: 'http://localhost:8083/api/im/importUser',
+          url: 'http://localhost:1004/api/im/importUser',
           method: 'POST',
           data: {
             userId: String(this.targetUserId),
@@ -585,7 +581,7 @@ export default {
     async getUserSig() {
       try {
         const [err, res] = await uni.request({
-          url: 'http://localhost:8083/api/im/getUserSig',
+          url: 'http://localhost:1004/api/im/getUserSig',
           method: 'GET',
           data: {
             userId: this.userId
@@ -762,65 +758,6 @@ export default {
       */
     },
     
-    /**
-     * 发送已读回执
-     */
-    sendReadReceipt() {
-      try {
-        console.log('📨 发送已读回执给用户:', this.targetUserId);
-        
-        // 通过WebSocket发送已读回执
-        const message = {
-          type: 'READ_RECEIPT',  // 已读回执类型
-          fromUserId: parseInt(this.userId),
-          toUserId: parseInt(this.targetUserId),
-          timestamp: Date.now()
-        };
-        
-        // 使用presenceManager的WebSocket发送
-        const presenceManager = require('@/utils/presence-manager.js').default;
-        if (presenceManager.isConnected) {
-          presenceManager.sendMessage(message);
-          console.log('✅ 已读回执发送成功');
-        } else {
-          console.warn('⚠️ WebSocket未连接,无法发送已读回执');
-        }
-      } catch (error) {
-        console.error('❌ 发送已读回执失败:', error);
-      }
-    },
-    
-    /**
-     * 监听已读回执
-     */
-    listenReadReceipt() {
-      // 在WebSocket消息处理中添加已读回执的监听
-      // 这个方法会在initPresence中被调用
-      console.log('👂 开始监听已读回执');
-    },
-    
-    /**
-     * 处理已读回执
-     */
-    handleReadReceipt(data) {
-      try {
-        console.log('📬 收到已读回执:', data);
-        
-        // 更新本地消息状态为已读
-        this.messages.forEach((msg, index) => {
-          // 只更新自己发送给对方的消息
-          if (msg.fromUserId === this.userId && 
-              msg.toUserId === this.targetUserId && 
-              msg.sendStatus === 2) {  // 只更新已送达的消息
-            this.$set(this.messages[index], 'sendStatus', 3);  // 更新为已读
-            console.log(`✅ 消息 ${msg.messageId} 状态更新为已读`);
-          }
-        });
-      } catch (error) {
-        console.error('❌ 处理已读回执失败:', error);
-      }
-    },
-    
     /**
      * 转换消息格式
      */
@@ -1139,7 +1076,7 @@ export default {
         
         // 调用后端同步接口
         const res = await uni.request({
-          url: 'http://localhost:8083/api/chat/syncTIMMessage',
+          url: 'http://localhost:1004/api/chat/syncTIMMessage',
           method: 'POST',
           data: syncData,
           header: {
@@ -1504,128 +1441,6 @@ export default {
       this.inputType = this.inputType === 'text' ? 'voice' : 'text';
     },
     
-    /**
-     * 初始化在线状态监听
-     */
-    async initPresence() {
-      console.log('🔌 初始化在线状态监听');
-      console.log('   - 当前用户ID:', this.userId);
-      console.log('   - 对方用户ID:', this.targetUserId);
-      
-      // 1. 首先通过HTTP API查询对方的在线状态(立即显示)
-      try {
-        const isOnline = await presenceManager.queryOnlineStatus(this.targetUserId);
-        this.isTargetOnline = isOnline;
-        console.log('   - HTTP查询初始在线状态:', this.isTargetOnline);
-      } catch (error) {
-        console.error('❌ HTTP查询在线状态失败:', error);
-        this.isTargetOnline = false;
-      }
-      
-      // 2. 连接WebSocket(如果未连接)
-      if (!presenceManager.getConnectionStatus()) {
-        presenceManager.connect(this.userId);
-        
-        // 等待WebSocket连接建立
-        await this.waitForWebSocketConnection();
-      }
-      
-      // 3. 监听对方用户的在线状态变化
-      this.handleStatusChange = (status) => {
-        console.log(`👤 对方用户 ${this.targetUserId} 状态变更: ${status}`);
-        this.isTargetOnline = (status === 'online');
-        console.log('   - 更新后的在线状态:', this.isTargetOnline);
-      };
-      
-      presenceManager.onStatusChange(this.targetUserId, this.handleStatusChange);
-      console.log('✅ 已订阅对方用户状态');
-      
-      // 4. 监听已读回执(通过WebSocket)
-      // 注册已读回执的回调
-      this.readReceiptCallback = (data) => {
-        if (data.type === 'READ_RECEIPT' && data.fromUserId == this.targetUserId) {
-          console.log('📬 收到已读回执:', data);
-          this.handleReadReceipt(data);
-        }
-      };
-      
-      // 将回调添加到presenceManager的消息处理中
-      if (!presenceManager.messageCallbacks) {
-        presenceManager.messageCallbacks = [];
-      }
-      presenceManager.messageCallbacks.push(this.readReceiptCallback);
-      console.log('✅ 已监听已读回执');
-      
-      // 4. 定期轮询在线状态(作为补充,每30秒查询一次)
-      this.onlineStatusPollingTimer = setInterval(async () => {
-        try {
-          const isOnline = await presenceManager.queryOnlineStatus(this.targetUserId);
-          if (this.isTargetOnline !== isOnline) {
-            console.log(`🔄 轮询检测到状态变化: ${this.isTargetOnline} -> ${isOnline}`);
-            this.isTargetOnline = isOnline;
-          }
-        } catch (error) {
-          console.error('❌ 轮询查询在线状态失败:', error);
-        }
-      }, 30000); // 30秒轮询一次
-    },
-    
-    /**
-     * 等待WebSocket连接建立
-     */
-    async waitForWebSocketConnection() {
-      return new Promise((resolve) => {
-        const maxWaitTime = 5000; // 最多等待5秒
-        const startTime = Date.now();
-        
-        const checkConnection = setInterval(() => {
-          if (presenceManager.getConnectionStatus()) {
-            console.log('✅ WebSocket已连接');
-            clearInterval(checkConnection);
-            resolve();
-          } else if (Date.now() - startTime > maxWaitTime) {
-            console.log('⚠️ WebSocket连接超时,继续使用HTTP轮询');
-            clearInterval(checkConnection);
-            resolve();
-          }
-        }, 100);
-      });
-    },
-    
-    /**
-     * 清理在线状态监听
-     */
-    cleanupPresence() {
-      console.log('🔌 清理在线状态监听');
-      
-      const presenceManager = require('@/utils/presence-manager.js').default;
-      
-      // 清除轮询定时器
-      if (this.onlineStatusPollingTimer) {
-        clearInterval(this.onlineStatusPollingTimer);
-        this.onlineStatusPollingTimer = null;
-      }
-      
-      // 移除在线状态监听
-      if (this.handleStatusChange) {
-        presenceManager.offStatusChange(this.targetUserId, this.handleStatusChange);
-        this.handleStatusChange = null;
-      }
-      
-      // 移除已读回执回调
-      if (this.readReceiptCallback && presenceManager.messageCallbacks) {
-        const index = presenceManager.messageCallbacks.indexOf(this.readReceiptCallback);
-        if (index > -1) {
-          presenceManager.messageCallbacks.splice(index, 1);
-          console.log('✅ 已移除已读回执监听');
-        }
-        this.readReceiptCallback = null;
-      }
-      
-      // 注意:不要断开WebSocket连接,因为其他页面可能还在使用
-      // presenceManager.disconnect();
-    },
-    
     /**
      * 开始录音
      */
@@ -1652,18 +1467,7 @@ export default {
         
         // 录音开始
         this.recorderManager.onStart(() => {
-          console.log('✅ 录音开始回调触发, voiceRecordCancelled:', this.voiceRecordCancelled);
-          
-          // 关键修复:如果录音已被取消,不再恢复录音状态
-          if (this.voiceRecordCancelled) {
-            console.log('⚠️ 录音已被取消,忽略onStart回调');
-            // 立即停止录音
-            if (this.recorderManager) {
-              this.recorderManager.stop();
-            }
-            return;
-          }
-          
+          console.log('✅ 录音开始回调触发');
           this.isRecording = true;
           this.showVoiceRecording = true;
           this.voiceStartTime = Date.now();
@@ -1726,7 +1530,6 @@ export default {
       // 立即设置录音状态(不等待onStart回调)
       this.isRecording = true;
       this.showVoiceRecording = true;
-      this.voiceRecordCancelled = false; // 重置取消标志
       this.voiceStartTime = Date.now();
       this.voiceRecordingTime = 0;
       this.voiceVolume = 0.3;
@@ -1838,9 +1641,6 @@ export default {
     cancelVoiceRecord() {
       console.log('❌ 取消录音, voiceCanceling:', this.voiceCanceling);
       
-      // 关键修复:标记录音已被取消,防止onStart回调恢复录音
-      this.voiceRecordCancelled = true;
-      
       // 清除计时器
       if (this.voiceRecordingTimer) {
         clearInterval(this.voiceRecordingTimer);
@@ -1853,22 +1653,15 @@ export default {
         this.voiceCanceling = true;
       }
       
-      // 立即隐藏录音UI
-      this.isRecording = false;
-      this.showVoiceRecording = false;
-      
-      if (this.recorderManager) {
-        // 尝试停止录音(如果已经开始)
-        try {
-          this.recorderManager.stop();
-        } catch (err) {
-          console.log('⚠️ 停止录音失败(可能还未开始):', err);
-        }
+      if (this.recorderManager && this.isRecording) {
+        this.recorderManager.stop();  // 这会触发onStop回调
+      } else {
+        // 如果没有在录音,直接重置状态
+        this.isRecording = false;
+        this.showVoiceRecording = false;
+        this.voiceCanceling = false;
       }
       
-      // 重置取消状态
-      this.voiceCanceling = false;
-      
       uni.showToast({
         title: '已取消录音',
         icon: 'none'
@@ -1945,7 +1738,7 @@ export default {
         
         // 使用uni.uploadFile上传到后端MinIO接口
         const [err, res] = await uni.uploadFile({
-          url: 'http://localhost:8083/api/voice/upload',
+          url: 'http://localhost:1004/api/voice/upload',
           filePath: this.voiceTempPath,
           name: 'file',
           header: {
@@ -2150,7 +1943,7 @@ export default {
 		    async getUserMessageLimit() {
 		      try {
 		        const [err, res] = await uni.request({
-		          url: 'http://localhost:8083/api/chat/getUserMessageLimit',
+		          url: 'http://localhost:1004/api/chat/getUserMessageLimit',
 		          method: 'GET',
 		          data: {
 		            userId: this.userId ,// 已在onLoad中初始化的当前用户ID

+ 192 - 53
LiangZhiYUMao/pages/today-recommend/index.vue

@@ -22,9 +22,9 @@
 								<view class="vip-badge" v-if="user.isVip">VIP</view>
 							</view>
 							<view class="user-details">
-								<text class="user-detail">{{ user.age }}岁</text>
-								<text class="user-detail">{{ user.height }}cm</text>
-								<text class="user-detail">{{ user.location }}</text>
+								<text class="user-detail" v-if="user.age && user.age !== '未知'">{{ user.age }}岁</text>
+								<text class="user-detail" v-if="user.height">{{ user.height }}cm</text>
+								<text class="user-detail" v-if="user.location && user.location !== '未填写'">{{ user.location }}</text>
 							</view>
 						</view>
 						<view class="online-status" :class="{ online: user.isOnline }">
@@ -35,19 +35,19 @@
 
 					<view class="card-content">
 						<view class="user-info-grid">
-							<view class="info-item">
+							<view class="info-item" v-if="user.job && user.job !== '未填写'">
 								<text class="info-label">职业</text>
 								<text class="info-value">{{ user.job }}</text>
 							</view>
-							<view class="info-item">
+							<view class="info-item" v-if="user.education && user.education !== '未填写'">
 								<text class="info-label">学历</text>
 								<text class="info-value">{{ user.education }}</text>
 							</view>
-							<view class="info-item">
+							<view class="info-item" v-if="user.constellation && user.constellation !== '未填写'">
 								<text class="info-label">星座</text>
 								<text class="info-value">{{ user.constellation }}</text>
 							</view>
-							<view class="info-item">
+							<view class="info-item" v-if="user.salary && user.salary !== '未填写'">
 								<text class="info-label">月收入</text>
 								<text class="info-value">{{ user.salary }}</text>
 							</view>
@@ -98,12 +98,15 @@
 
 <script>
 	import userAuth from '@/utils/userAuth.js'
+	import api from '@/utils/api.js'
+	import { DEFAULT_IMAGES, EDUCATION_TEXT, SALARY_RANGE_TEXT } from '@/config/index.js'
 
 	export default {
 		data() {
 			return {
 				currentUserId: null,
-				
+				currentUserGender: null,
+
 				// 推荐用户数据
 				recommendUsers: []
 			}
@@ -113,65 +116,209 @@
 			console.log('今日推荐页面加载')
 			this.currentUserId = userAuth.getUserId()
 			console.log('当前用户ID:', this.currentUserId)
-			
-			// 模拟加载推荐数据
+
+			// 加载今日推荐
 			this.loadTodayRecommend()
 		},
 
 		methods: {
 			// 加载今日推荐
-			loadTodayRecommend() {
-				uni.showLoading({
-					title: '加载推荐中...'
-				})
-				
-				// 模拟API调用延迟
-				setTimeout(() => {
-					uni.hideLoading()
-					
-					if (this.recommendUsers.length > 0) {
+			async loadTodayRecommend() {
+				try {
+					uni.showLoading({
+						title: '加载推荐中...'
+					})
+					const userId = this.currentUserId || parseInt(uni.getStorageSync('userId'))
+					if (!userId) {
+						uni.hideLoading()
 						uni.showToast({
-							title: `为您推荐了${this.recommendUsers.length}位会员`,
+							title: '请先登录',
+							icon: 'none'
+						})
+						return
+					}
+					await this.loadCurrentUserInfo()
+					const data = await api.recommend.getUsers({
+						userId,
+						oppoOnly: 1,
+						limit: 50
+					})
+					uni.hideLoading()
+					if (data && data.length > 0) {
+						const sortedUsers = data.filter(u => u.userId !== userId && u.compatibilityScore > 0 && (!this.currentUserGender || u.gender !== this.currentUserGender)).sort((a, b) => (b.compatibilityScore || 0) - (a.compatibilityScore || 0)).slice(0, 10)
+						this.recommendUsers = sortedUsers.map(u => this.formatUserData(u))
+						if (this.recommendUsers.length > 0) uni.showToast({
+							title: `为您推荐了${this.recommendUsers.length}位高匹配度会员`,
 							icon: 'success'
 						})
+						else uni.showToast({
+							title: '暂无推荐用户',
+							icon: 'none'
+						})
+					} else uni.showToast({
+						title: '暂无推荐用户',
+						icon: 'none'
+					})
+				} catch (e) {
+					uni.hideLoading()
+					console.error('获取推荐用户失败:', e)
+					uni.showToast({
+						title: '加载失败,请重试',
+						icon: 'none'
+					})
+				}
+			},
+
+			// 加载当前用户信息
+			async loadCurrentUserInfo() {
+				try {
+					const s = uni.getStorageSync('userInfo')
+					if (s && s.gender !== undefined) this.currentUserGender = parseInt(s.gender)
+					if (this.currentUserId) {
+						const u = await api.user.getDetailInfo(this.currentUserId)
+						if (u && u.gender !== undefined) this.currentUserGender = parseInt(u.gender)
 					}
-				}, 1000)
+				} catch (e) {
+					console.error('加载失败:', e)
+				}
+			},
+
+			// 格式化用户数据
+			formatUserData(u) {
+				return {
+					...u,
+					id: u.userId || u.id,
+					avatar: this.validateImageUrl(u.avatarUrl || u.avatar),
+					nickname: u.nickname || '用户',
+					age: this.calculateAge(u.birthDate || u.birth_date) || u.age || '未知',
+					height: u.height || null,
+					location: this.formatLocation(u),
+					job: u.jobTitle || u.job || '未填写',
+					education: this.formatEducation(u.education || u.education_lev || u.educationLevel),
+					constellation: u.star || u.constellation || '未填写',
+					salary: this.formatSalary(u.salaryRange),
+					hobbies: this.parseHobbies(u.hobby),
+					introduction: u.introduction || u.selfIntro || '',
+					isVip: u.isVip || false,
+					isOnline: u.isOnline || false,
+					compatibilityScore: u.compatibilityScore || 0
+				}
+			},
+
+			// 计算年龄
+			calculateAge(d) {
+				if (!d) return null
+				try {
+					const b = new Date(d),
+						t = new Date()
+					let a = t.getFullYear() - b.getFullYear()
+					const m = t.getMonth() - b.getMonth()
+					if (m < 0 || (m === 0 && t.getDate() < b.getDate())) a--
+					return a > 0 && a < 120 ? a : null
+				} catch (e) {
+					return null
+				}
+			},
+
+			// 格式化位置
+			formatLocation(u) {
+				const p = []
+				if (u.provinceName) p.push(u.provinceName)
+				if (u.cityName) p.push(u.cityName)
+				if (p.length === 0 && u.location) return u.location
+				if (p.length === 0 && u.city) return u.city
+				return p.join(' ') || '未填写'
+			},
+
+			// 格式化学历
+			formatEducation(e) {
+				if (!e) return '未填写'
+				return EDUCATION_TEXT[e] || e
+			},
+
+			// 格式化月收入
+			formatSalary(s) {
+				if (!s) return '未填写'
+				return SALARY_RANGE_TEXT[s] || s
+			},
+
+			// 解析兴趣爱好
+			parseHobbies(h) {
+				if (!h) return []
+				try {
+					const hs = typeof h === 'string' ? JSON.parse(h) : h
+					return Array.isArray(hs) ? hs.slice(0, 5) : []
+				} catch (e) {
+					return []
+				}
+			},
+
+			// 验证图片地址
+			validateImageUrl(url) {
+				if (!url || url === 'null' || url === 'undefined') return DEFAULT_IMAGES.avatar
+				let c = String(url).trim()
+				const i = c.indexOf('http')
+				if (i > 0) c = c.slice(i)
+				if (!c) return DEFAULT_IMAGES.avatar
+				if (c.startsWith('/')) return `http://115.190.125.125:9000${c}`
+				if (c.startsWith('http://') || c.startsWith('https://')) return c
+				return DEFAULT_IMAGES.avatar
+			},
+
+			// 格式化匹配度
+			formatMatchScore(s) {
+				if (!s) return '0.0'
+				return Number(s).toFixed(1)
 			},
 
 			// 处理不感兴趣
-			handlePass(user, index) {
+			async handlePass(user, index) {
 				uni.showModal({
 					title: '确认操作',
 					content: `确定对${user.nickname}不感兴趣吗?`,
-					success: (res) => {
+					success: async (res) => {
 						if (res.confirm) {
-							console.log('不感兴趣:', user.nickname)
-							uni.showToast({
-								title: '已记录您的偏好',
-								icon: 'success'
-							})
-							
-							// 从列表中移除
-							this.recommendUsers.splice(index, 1)
+							try {
+								await api.recommend.feedback({
+									userId: this.currentUserId,
+									targetUserId: user.userId || user.id,
+									type: 'dislike'
+								})
+								uni.showToast({
+									title: '已记录您的偏好',
+									icon: 'success'
+								})
+								this.recommendUsers.splice(index, 1)
+							} catch (e) {
+								console.error('失败:', e)
+								uni.showToast({
+									title: '操作失败,请重试',
+									icon: 'none'
+								})
+							}
 						}
 					}
 				})
 			},
 
 			// 处理喜欢
-			handleLike(user, index) {
-				console.log('喜欢:', user.nickname)
-				
-				// 动画效果
+			async handleLike(user, index) {
+				try {
+					await api.recommend.feedback({
+						userId: this.currentUserId,
+						targetUserId: user.userId || user.id,
+						type: 'like'
+					})
+				} catch (e) {
+					console.error('失败:', e)
+				}
 				uni.showToast({
 					title: `已喜欢${user.nickname} ❤️`,
 					icon: 'success'
 				})
-				
-				// 从列表中移除
 				setTimeout(() => {
 					this.recommendUsers.splice(index, 1)
-					
+
 					// 检查是否匹配成功(模拟)
 					const isMatch = Math.random() > 0.7 // 30%几率匹配成功
 					if (isMatch) {
@@ -199,16 +346,16 @@
 			// 处理打招呼
 			handleChat(user) {
 				console.log('打招呼:', user.nickname)
-				
+
 				const greetings = [
 					'你好,很高兴认识你!',
 					'Hi,看了你的资料很不错呢~',
 					'你好,我们聊聊天吧!',
 					'Hello,可以交个朋友吗?'
 				]
-				
+
 				const randomGreeting = greetings[Math.floor(Math.random() * greetings.length)]
-				
+
 				uni.showModal({
 					title: `向${user.nickname}打招呼`,
 					content: `发送消息:"${randomGreeting}"`,
@@ -217,20 +364,12 @@
 					confirmText: '发送',
 					success: (res) => {
 						if (res.confirm) {
-							uni.showToast({
-								title: '招呼已发送!',
-								icon: 'success'
+							uni.navigateTo({
+								url: `/pages/message/chat?targetUserId=${user.userId||user.id}&targetUserName=${encodeURIComponent(user.nickname)}&message=${encodeURIComponent(randomGreeting)}`
 							})
-							// TODO: 调用发送消息API
 						} else {
-							// 自定义消息
 							uni.navigateTo({
-								url: `/pages/message/chat?userId=${user.id}&nickname=${user.nickname}`
-							}).catch(() => {
-								uni.showToast({
-									title: '聊天功能开发中',
-									icon: 'none'
-								})
+								url: `/pages/message/chat?targetUserId=${user.userId||user.id}&targetUserName=${encodeURIComponent(user.nickname)}`
 							})
 						}
 					}