Ver Fonte

推荐页面 匹配功能优化

yuxy há 11 horas atrás
pai
commit
9a7a95194f

+ 79 - 48
LiangZhiYUMao/pages/match/index.vue

@@ -38,17 +38,6 @@
 				<view v-if="selectedMode === 'smart'" class="mode-selected">✓ 已选择</view>
 			</view>
 
-			<view 
-				class="mode-item" 
-				:class="{ 'mode-item-active': selectedMode === 'online' }"
-				@tap="selectMode('online')"
-			>
-				<view class="mode-icon">⚡</view>
-				<view class="mode-title">实时在线</view>
-				<view class="mode-desc">只匹配当前在线的优质用户</view>
-				<view v-if="selectedMode === 'online'" class="mode-selected">✓ 已选择</view>
-			</view>
-
 			<view 
 				class="mode-item" 
 				:class="{ 'mode-item-active': selectedMode === 'precise' }"
@@ -94,7 +83,7 @@
 				</view>
 
 				<view class="popup-buttons">
-					<button class="btn-chat" @click="goToChat">开始聊天</button>
+					<button class="btn-like" @click="likeMatchedUser">喜欢</button>
 					<button class="btn-continue" @click="continueMatch">继续匹配</button>
 				</view>
 			</view>
@@ -114,7 +103,8 @@ export default {
 			matchScore: 0,
 			matchedUser: {},
 			userInfo: {},
-			pollingTimer: null
+			pollingTimer: null,
+			isLiked: false
 		}
 	},
 	
@@ -449,18 +439,51 @@ export default {
 			this.$refs.matchSuccessPopup.open();
 		},
 		
-		// 前往聊天
-		goToChat() {
-			this.$refs.matchSuccessPopup.close();
+		// 喜欢匹配的用户
+		async likeMatchedUser() {
+			if (!this.matchedUser || !this.matchedUser.userId) {
+				uni.showToast({
+					title: '用户信息错误',
+					icon: 'none'
+				});
+				return;
+			}
 			
-			// 跳转到聊天页面(修正参数名称)
+			const uid = parseInt(uni.getStorageSync('userId') || 1);
 			const targetUserId = this.matchedUser.userId;
-			const targetUserName = encodeURIComponent(this.matchedUser.nickname || '用户');
-			const targetUserAvatar = encodeURIComponent(this.matchedUser.avatarUrl || 'http://115.190.125.125:9001/static-images/default-avatar.svg');
 			
-			uni.navigateTo({
-				url: `/pages/message/chat?targetUserId=${targetUserId}&targetUserName=${targetUserName}&targetUserAvatar=${targetUserAvatar}`
-			});
+			try {
+				// 导入 api 模块
+				const api = require('@/utils/api.js').default;
+				
+				// 调用喜欢接口
+				await api.recommend.feedback({ 
+					userId: uid, 
+					targetUserId: targetUserId, 
+					type: 'like' 
+				});
+				
+				this.isLiked = true;
+				
+				uni.showToast({ 
+					title: '已喜欢', 
+					icon: 'success' 
+				});
+				
+				// 延迟关闭弹窗
+				setTimeout(() => {
+					this.$refs.matchSuccessPopup.close();
+					this.matchStatus = '点击下方按钮开始匹配';
+					this.matchedUser = {};
+					this.isLiked = false;
+				}, 1000);
+			} catch(e) {
+				console.error('喜欢失败', e);
+				uni.showToast({
+					title: '操作失败,请重试',
+					icon: 'none'
+				});
+			}
 		},
 		
 		// 继续匹配
@@ -552,13 +575,14 @@ export default {
 }
 
 .tip-box {
-	background: #E3F2FD;
+	background: linear-gradient(135deg, #E3F2FD 0%, #F3E5F5 100%);
 	border-left: 6rpx solid #2196F3;
-	padding: 24rpx;
-	margin: 20rpx;
-	border-radius: 12rpx;
+	padding: 28rpx 24rpx;
+	margin: 24rpx 20rpx;
+	border-radius: 16rpx;
 	display: flex;
 	align-items: center;
+	box-shadow: 0 2rpx 12rpx rgba(33, 150, 243, 0.1);
 }
 
 .tip-icon {
@@ -573,12 +597,12 @@ export default {
 }
 
 .match-animation {
-	height: 500rpx;
+	height: 320rpx;
 	display: flex;
 	flex-direction: column;
 	justify-content: center;
 	align-items: center;
-	margin: 40rpx 0;
+	margin: 20rpx 0 30rpx 0;
 }
 
 .heart-container {
@@ -606,26 +630,30 @@ export default {
 }
 
 .match-status {
-	margin-top: 60rpx;
-	font-size: 32rpx;
+	margin-top: 40rpx;
+	font-size: 30rpx;
 	color: #E91E63;
 	font-weight: bold;
+	text-align: center;
+	padding: 0 40rpx;
 }
 
 .match-modes {
-	margin: 40rpx 20rpx;
+	margin: 20rpx 20rpx 180rpx 20rpx;
+	padding-bottom: 20rpx;
 }
 
 .mode-item {
 	background: white;
-	border-radius: 16rpx;
-	padding: 32rpx;
-	margin-bottom: 24rpx;
-	box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
+	border-radius: 20rpx;
+	padding: 40rpx 32rpx;
+	margin-bottom: 20rpx;
+	box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
 	transition: all 0.3s;
 	position: relative;
 	cursor: pointer;
 	-webkit-tap-highlight-color: transparent;
+	border: 2rpx solid transparent;
 }
 
 .mode-item:active {
@@ -635,8 +663,9 @@ export default {
 
 .mode-item-active {
 	background: linear-gradient(135deg, #FFF5F7 0%, #FFFFFF 100%);
-	border: 2rpx solid #E91E63;
-	box-shadow: 0 6rpx 20rpx rgba(233, 30, 99, 0.2);
+	border: 3rpx solid #E91E63;
+	box-shadow: 0 8rpx 24rpx rgba(233, 30, 99, 0.25);
+	transform: translateY(-4rpx);
 }
 
 .mode-selected {
@@ -652,31 +681,33 @@ export default {
 }
 
 .mode-icon {
-	font-size: 60rpx;
+	font-size: 72rpx;
 	text-align: center;
-	margin-bottom: 16rpx;
+	margin-bottom: 20rpx;
 }
 
 .mode-title {
-	font-size: 32rpx;
+	font-size: 34rpx;
 	font-weight: bold;
 	color: #333;
 	text-align: center;
-	margin-bottom: 12rpx;
+	margin-bottom: 16rpx;
 }
 
 .mode-desc {
-	font-size: 24rpx;
+	font-size: 26rpx;
 	color: #999;
 	text-align: center;
-	line-height: 36rpx;
+	line-height: 40rpx;
+	padding: 0 20rpx;
 }
 
 .match-button-container {
 	position: fixed;
-	bottom: 60rpx;
-	left: 40rpx;
-	right: 40rpx;
+	bottom: 80rpx;
+	left: 30rpx;
+	right: 30rpx;
+	z-index: 100;
 }
 
 .start-match-btn {
@@ -786,7 +817,7 @@ export default {
 	gap: 20rpx;
 }
 
-.btn-chat,
+.btn-like,
 .btn-continue {
 	flex: 1;
 	height: 80rpx;
@@ -796,7 +827,7 @@ export default {
 	border: none;
 }
 
-.btn-chat {
+.btn-like {
 	background: #E91E63;
 	color: white;
 }

+ 7 - 4
LiangZhiYUMao/pages/recommend/index.vue

@@ -198,7 +198,6 @@
 					<view class="main" @click="showUserDetailByIndex(idx)">
 						<view class="row">
 							<text class="name">{{ u.nickname || '-' }}</text>
-							<text class="score-badge">{{ fmtScore(u.compatibilityScore) }}</text>
 						</view>
 						<view class="meta">
 							<text>{{ fmtGender(u.gender) }}</text>
@@ -1344,6 +1343,9 @@ export default {
 		border-top-right-radius: 16rpx; 
 		padding-bottom: env(safe-area-inset-bottom); 
 		border-top: 2rpx solid #E0E0E0;
+		height: 75vh !important;
+		display: flex;
+		flex-direction: column;
 	}
 	
 	.sheet-handle{ 
@@ -1367,8 +1369,8 @@ export default {
 	
 	.sheet-body{ 
 		padding: 20rpx 28rpx; 
-		max-height: 60vh; 
-		overflow: auto;
+		max-height: calc(75vh - 240rpx);
+		overflow-y: auto;
 	}
 	
 	.field{ 
@@ -1438,8 +1440,9 @@ export default {
 	.sheet-actions{ 
 		display: flex; 
 		gap: 16rpx; 
-		padding: 16rpx 28rpx 28rpx;
+		padding: 24rpx 28rpx 32rpx;
 		border-top: 2rpx solid #E0E0E0;
+		margin-top: 16rpx;
 	}
 	
 	.btn{ 

+ 34 - 0
service/randomMatch/src/main/java/com/zhentao/entity/User.java

@@ -0,0 +1,34 @@
+package com.zhentao.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@TableName(value = "users")
+@Data
+public class User implements Serializable {
+    @TableId(type = IdType.AUTO)
+    private Integer userId;
+    private String phone;
+    private String email;
+    private String nickname;
+    private String password;
+    private Integer gender;
+    private Date birthDate;
+    private String avatarUrl;
+    private Integer status;
+    private String sourceChannel;
+    private Integer isProfileComplete;
+    private Date createdAt;
+    private Date updatedAt;
+    private Integer consentToCollect;
+    private Date lastLoginAt;
+    private Date lastActiveAt;
+    private Integer hasWechatLogin;
+    
+    private static final long serialVersionUID = 1L;
+}

+ 45 - 0
service/randomMatch/src/main/java/com/zhentao/entity/UserProfile.java

@@ -0,0 +1,45 @@
+package com.zhentao.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@TableName(value = "user_profile")
+@Data
+public class UserProfile implements Serializable {
+    @TableId(type = IdType.AUTO)
+    private Integer profileId;
+    private Integer userId;
+    private Integer house;
+    private Integer car;
+    private Integer height;
+    private Integer weight;
+    private String star;
+    private String animal;
+    private String realName;
+    private String idCard;
+    private Integer educationLevel;
+    private Object hobby;
+    private String schoolName;
+    private String company;
+    private Integer salaryRange;
+    private String jobTitle;
+    private Integer maritalStatus;
+    private Integer isRealNameVerified;
+    private Integer isEducationVerified;
+    private Integer isWorkVerified;
+    private Integer isMaritalVerified;
+    private Date verifiedAt;
+    private Integer provinceId;
+    private Integer cityId;
+    private Integer areaId;
+    private Integer privacySalary;
+    private Integer privacyPhone;
+    private Integer authenticityScore;
+    
+    private static final long serialVersionUID = 1L;
+}

+ 24 - 0
service/randomMatch/src/main/java/com/zhentao/mapper/UserMapper.java

@@ -0,0 +1,24 @@
+package com.zhentao.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zhentao.entity.User;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+@Mapper
+public interface UserMapper extends BaseMapper<User> {
+    
+    /**
+     * 获取所有正常状态的用户(排除禁用和注销的用户)
+     */
+    @Select("SELECT * FROM users WHERE status = 1 AND is_profile_complete = 1")
+    List<User> getAllActiveUsers();
+    
+    /**
+     * 根据性别获取用户
+     */
+    @Select("SELECT * FROM users WHERE status = 1 AND is_profile_complete = 1 AND gender = #{gender}")
+    List<User> getUsersByGender(Integer gender);
+}

+ 16 - 0
service/randomMatch/src/main/java/com/zhentao/mapper/UserProfileMapper.java

@@ -0,0 +1,16 @@
+package com.zhentao.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zhentao.entity.UserProfile;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Select;
+
+@Mapper
+public interface UserProfileMapper extends BaseMapper<UserProfile> {
+    
+    /**
+     * 根据用户ID获取用户资料
+     */
+    @Select("SELECT * FROM user_profile WHERE user_id = #{userId}")
+    UserProfile getByUserId(Integer userId);
+}

+ 181 - 1
service/randomMatch/src/main/java/com/zhentao/service/MatchService.java

@@ -2,6 +2,10 @@ package com.zhentao.service;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.zhentao.entity.MatchData;
+import com.zhentao.entity.User;
+import com.zhentao.entity.UserProfile;
+import com.zhentao.mapper.UserMapper;
+import com.zhentao.mapper.UserProfileMapper;
 import com.zhentao.redis.RedisMatchPool;
 import com.zhentao.redis.RedisMatchQueue;
 import com.zhentao.util.MatchingAlgorithmUtils;
@@ -11,6 +15,9 @@ import org.springframework.data.redis.core.script.DefaultRedisScript;
 import org.springframework.stereotype.Service;
 
 import java.util.*;
+import java.time.LocalDate;
+import java.time.Period;
+import java.time.ZoneId;
 import java.util.concurrent.TimeUnit;
 
 @Slf4j
@@ -26,15 +33,21 @@ public class MatchService {
     private final ObjectMapper objectMapper;
     private final RedisMatchQueue matchQueue;
     private final RedisTemplate<String, Object> redisTemplate;
+    private final UserMapper userMapper;
+    private final UserProfileMapper userProfileMapper;
     private final java.util.concurrent.ConcurrentHashMap<String, Long> localLocks = new java.util.concurrent.ConcurrentHashMap<>();
 
     public MatchService(RedisMatchPool redisMatchPool,
                         ObjectMapper objectMapper, RedisMatchQueue matchQueue,
-                        RedisTemplate<String, Object> redisTemplate) {
+                        RedisTemplate<String, Object> redisTemplate,
+                        UserMapper userMapper,
+                        UserProfileMapper userProfileMapper) {
         this.redisMatchPool = redisMatchPool;
         this.objectMapper = objectMapper;
         this.matchQueue = matchQueue;
         this.redisTemplate = redisTemplate;
+        this.userMapper = userMapper;
+        this.userProfileMapper = userProfileMapper;
     }
 
     public Map<String, Object> handleMatchRequest(MatchData data) {
@@ -79,6 +92,20 @@ public class MatchService {
         }
         double threshold = getMatchThreshold(matchMode);
         
+        // 先尝试从匹配池中匹配
+        MatchResult poolResult = tryMatchFromPool(userId, userData, matchMode, threshold);
+        if (poolResult.isMatched()) {
+            return poolResult;
+        }
+        
+        // 如果匹配池中没有合适的,从数据库中的所有用户中匹配
+        return tryMatchFromDatabase(userId, userData, matchMode, threshold);
+    }
+    
+    /**
+     * 从匹配池中尝试匹配
+     */
+    private MatchResult tryMatchFromPool(String userId, MatchData userData, String matchMode, double threshold) {
         List<String> online = redisMatchPool.getAllUsersInPool();
         Set<String> unmatched = redisMatchPool.getUserUnmatchedSet(userId);
         Map<String, Double> scores = new HashMap<>();
@@ -124,6 +151,136 @@ public class MatchService {
         return new MatchResult(false, null, 0);
     }
     
+    /**
+     * 从数据库中的所有已注册用户中匹配
+     */
+    private MatchResult tryMatchFromDatabase(String userId, MatchData userData, String matchMode, double threshold) {
+        try {
+            // 获取异性用户(只匹配异性)
+            Integer targetGender = userData.getGender() == 1 ? 2 : 1;
+            List<User> allUsers = userMapper.getUsersByGender(targetGender);
+            
+            if (allUsers == null || allUsers.isEmpty()) {
+                log.info("数据库中没有找到异性用户");
+                return new MatchResult(false, null, 0);
+            }
+            
+            Set<String> unmatched = redisMatchPool.getUserUnmatchedSet(userId);
+            Map<String, Double> scores = new HashMap<>();
+            
+            for (User user : allUsers) {
+                String otherUserId = String.valueOf(user.getUserId());
+                
+                // 排除自己
+                if (userId.equals(otherUserId)) continue;
+                
+                // 排除已经不匹配的用户
+                if (unmatched.contains(otherUserId)) continue;
+                
+                // 构建MatchData
+                MatchData otherData = buildMatchDataFromUser(user);
+                if (otherData == null) continue;
+                
+                // 根据匹配模式进行额外筛选
+                if (!shouldMatchByMode(userData, otherData, matchMode)) {
+                    continue;
+                }
+                
+                // 计算匹配分数
+                double score = MatchingAlgorithmUtils.calculateMatchScore(userData, otherData);
+                scores.put(otherUserId, score);
+                
+                // 如果分数达到阈值,立即返回匹配成功
+                if (score >= threshold) {
+                    log.info("从数据库匹配成功: userId={}, matchedUserId={}, score={}", userId, otherUserId, score);
+                    return new MatchResult(true, otherUserId, score);
+                }
+            }
+            
+            // 如果没有达到阈值的,返回分数最高的
+            if (!scores.isEmpty()) {
+                Map.Entry<String, Double> best = scores.entrySet().stream()
+                    .max(Map.Entry.comparingByValue())
+                    .get();
+                
+                // 降低阈值要求,只要分数大于30就可以匹配
+                if (best.getValue() >= 30.0) {
+                    log.info("从数据库匹配成功(降低阈值): userId={}, matchedUserId={}, score={}", 
+                        userId, best.getKey(), best.getValue());
+                    return new MatchResult(true, best.getKey(), best.getValue());
+                }
+                
+                // 记录最佳但未匹配的用户
+                redisMatchPool.recordUnmatch(userId, best.getKey());
+            }
+            
+            log.info("数据库中未找到合适的匹配对象: userId={}", userId);
+            return new MatchResult(false, null, 0);
+            
+        } catch (Exception e) {
+            log.error("从数据库匹配时发生错误", e);
+            return new MatchResult(false, null, 0);
+        }
+    }
+    
+    /**
+     * 从User和UserProfile构建MatchData
+     */
+    private MatchData buildMatchDataFromUser(User user) {
+        try {
+            MatchData data = new MatchData();
+            data.setUserId(String.valueOf(user.getUserId()));
+            data.setNickname(user.getNickname());
+            data.setGender(user.getGender());
+            data.setAvatarUrl(user.getAvatarUrl());
+            
+            // 计算年龄
+            if (user.getBirthDate() != null) {
+                LocalDate birthDate = user.getBirthDate().toInstant()
+                    .atZone(ZoneId.systemDefault()).toLocalDate();
+                int age = Period.between(birthDate, LocalDate.now()).getYears();
+                data.setAge(age);
+            }
+            
+            // 获取用户资料
+            UserProfile profile = userProfileMapper.getByUserId(user.getUserId());
+            if (profile != null) {
+                // 设置兴趣爱好
+                if (profile.getHobby() != null) {
+                    try {
+                        if (profile.getHobby() instanceof String) {
+                            String hobbyStr = (String) profile.getHobby();
+                            List<String> interests = objectMapper.readValue(hobbyStr, List.class);
+                            data.setInterests(interests);
+                        } else if (profile.getHobby() instanceof List) {
+                            data.setInterests((List<String>) profile.getHobby());
+                        }
+                    } catch (Exception e) {
+                        log.warn("解析兴趣爱好失败: userId={}", user.getUserId());
+                        data.setInterests(new ArrayList<>());
+                    }
+                } else {
+                    data.setInterests(new ArrayList<>());
+                }
+                
+                // 设置城市信息(使用cityId)
+                if (profile.getCityId() != null) {
+                    data.setCity("城市" + profile.getCityId());
+                }
+            } else {
+                data.setInterests(new ArrayList<>());
+            }
+            
+            // 设置默认简介
+            data.setIntroduction("来自数据库的用户");
+            
+            return data;
+        } catch (Exception e) {
+            log.error("构建MatchData失败: userId={}", user.getUserId(), e);
+            return null;
+        }
+    }
+    
     /**
      * 根据匹配模式获取阈值
      */
@@ -259,8 +416,10 @@ public class MatchService {
     }
 
     private Map<String, Object> buildUserInfo(String userId) {
+        // 先尝试从匹配池获取
         MatchData data = redisMatchPool.getUserMatchData(userId);
         Map<String,Object> m = new HashMap<>();
+        
         if (data != null) {
             m.put("userId", data.getUserId());
             m.put("nickname", data.getNickname());
@@ -270,6 +429,27 @@ public class MatchService {
             m.put("interests", data.getInterests());
             m.put("city", data.getCity());
             m.put("introduction", data.getIntroduction());
+        } else {
+            // 如果匹配池中没有,从数据库获取
+            try {
+                Integer userIdInt = Integer.parseInt(userId);
+                User user = userMapper.selectById(userIdInt);
+                if (user != null) {
+                    MatchData dbData = buildMatchDataFromUser(user);
+                    if (dbData != null) {
+                        m.put("userId", dbData.getUserId());
+                        m.put("nickname", dbData.getNickname());
+                        m.put("gender", dbData.getGender());
+                        m.put("age", dbData.getAge());
+                        m.put("avatarUrl", dbData.getAvatarUrl());
+                        m.put("interests", dbData.getInterests());
+                        m.put("city", dbData.getCity());
+                        m.put("introduction", dbData.getIntroduction());
+                    }
+                }
+            } catch (Exception e) {
+                log.error("从数据库获取用户信息失败: userId={}", userId, e);
+            }
         }
         return m;
     }