Browse Source

用户可设置地区,推荐列表根据用户性别推荐异性,筛选取消性别筛选

yuxy 1 month ago
parent
commit
0784d8cdda

+ 389 - 21
LiangZhiYUMao/pages/profile/index.vue

@@ -91,6 +91,14 @@
 					<text class="label">生肖</text>
 					<text class="value">{{ profile.animal || '未设置' }}</text>
 				</view>
+				
+				<view class="info-item" :class="{ editing: isEditing }" @click="handleAreaClick">
+					<text class="label">地区</text>
+					<picker v-if="isEditing" mode="multiSelector" :range="multiAreaData" range-key="name" :value="multiAreaIndex" @change="onAreaChange" @columnchange="onAreaColumnChange">
+						<view class="picker-value">{{ getAreaText() }}</view>
+					</picker>
+					<text v-else class="value">{{ getAreaText() }}</text>
+				</view>
 			</view>
 
 			<!-- 学业与工作 -->
@@ -289,6 +297,7 @@
 <script>
 	import userAuth from '@/utils/userAuth.js'
 	import { getZodiacByBirthday } from '@/utils/zodiac.js'
+	import api from '@/utils/api.js'
 	
 	export default {
 		data() {
@@ -327,7 +336,10 @@
 					isRealNameVerified: false,
 					isEducationVerified: false,
 					isWorkVerified: false,
-					isMaritalVerified: false
+					isMaritalVerified: false,
+					provinceId: null,
+					cityId: null,
+					areaId: null
 				},
 				
 				// 选项列表
@@ -374,7 +386,14 @@
 						tags: ['美食', '游戏', '宠物', '时尚', '购物', '化妆']
 					}
 				],
-				customHobbyInput: ''
+				customHobbyInput: '',
+				
+				// 省市区数据
+				provinceList: [],
+				cityList: [],
+				areaList: [],
+				multiAreaData: [[], [], []], // 三级联动数据
+				multiAreaIndex: [0, 0, 0] // 三级联动索引
 			}
 		},
 		computed: {
@@ -401,6 +420,8 @@
 			this.checkLoginStatus()
 		} else {
 			console.log('已登录,用户ID:', this.currentUserId)
+			// 加载省市区数据
+			this.loadAreaData()
 			// 加载用户资料
 			this.loadProfile()
 		}
@@ -609,17 +630,61 @@
 				uni.request({
 					url: this.gatewayURL + '/api/user/profile?userId=' + this.currentUserId,
 					method: 'GET',
-					success: (res) => {
+					success: async (res) => {
 						uni.hideLoading()
 						console.log('=== 用户资料加载 ===')
 						console.log('完整响应:', res.data)
 						
-						if (res.data && res.data.code === 200) {
-							console.log('原始salaryRange:', res.data.data.salaryRange)
-							this.profile = { ...this.profile, ...res.data.data }
-							console.log('合并后的profile.salaryRange:', this.profile.salaryRange)
-							console.log('显示的薪资文本:', this.getSalaryText())
-						} else {
+					if (res.data && res.data.code === 200) {
+						console.log('原始salaryRange:', res.data.data.salaryRange)
+						console.log('完整返回数据:', JSON.stringify(res.data.data, null, 2))
+						console.log('省市区ID:', {
+							provinceId: res.data.data.provinceId,
+							cityId: res.data.data.cityId,
+							areaId: res.data.data.areaId
+						})
+						
+						// 确保省市区ID正确赋值(处理可能的字段名不匹配)
+						const profileData = { ...res.data.data }
+						
+						// 如果字段名是下划线格式,转换为驼峰格式
+						if (profileData.province_id !== undefined && profileData.provinceId === undefined) {
+							profileData.provinceId = profileData.province_id
+						}
+						if (profileData.city_id !== undefined && profileData.cityId === undefined) {
+							profileData.cityId = profileData.city_id
+						}
+						if (profileData.area_id !== undefined && profileData.areaId === undefined) {
+							profileData.areaId = profileData.area_id
+						}
+						
+						this.profile = { ...this.profile, ...profileData }
+						console.log('合并后的profile:', {
+							provinceId: this.profile.provinceId,
+							cityId: this.profile.cityId,
+							areaId: this.profile.areaId
+						})
+						
+						// 先加载省市区数据,然后再初始化索引和显示
+						await this.loadAreaData()
+						
+						// 如果有省市区ID,加载对应的城市和区域数据,并初始化选择器索引
+						if (this.profile.provinceId || this.profile.cityId || this.profile.areaId) {
+							console.log('开始加载已保存的省市区数据...')
+							// 先加载省份对应的城市
+							if (this.profile.provinceId) {
+								await this.loadCitiesForProvince(this.profile.provinceId)
+							}
+							// 再加载城市对应的区域
+							if (this.profile.cityId) {
+								await this.loadAreasForCity(this.profile.cityId)
+							}
+							// 最后初始化选择器索引
+							setTimeout(() => {
+								this.initAreaIndex()
+							}, 300)
+						}
+					} else {
 							uni.showToast({
 								title: res.data?.message || '加载失败',
 								icon: 'none'
@@ -779,6 +844,282 @@
 				this.profile.privacyPhone = e.detail.value ? 1 : 0
 			},
 			
+			// 省市区相关方法
+			// 加载省市区数据
+			async loadAreaData() {
+				try {
+					console.log('开始加载省份数据...')
+					// 加载省份列表
+					const provinceRes = await api.area.getProvinces()
+					console.log('省份数据返回:', provinceRes)
+					
+					// api.js 已经处理了数据格式,直接使用返回的数组
+					if (Array.isArray(provinceRes)) {
+						this.provinceList = provinceRes
+					} else if (provinceRes && provinceRes.data) {
+						this.provinceList = provinceRes.data
+					} else if (provinceRes && provinceRes.code === 200 && provinceRes.data) {
+						this.provinceList = provinceRes.data
+					} else {
+						this.provinceList = []
+					}
+					
+					this.multiAreaData[0] = this.provinceList
+					console.log('省份列表:', this.provinceList)
+					
+					// 如果有已选择的省份,加载对应的城市
+					if (this.profile.provinceId) {
+						await this.loadCitiesForProvince(this.profile.provinceId)
+					} else if (this.provinceList.length > 0) {
+						// 默认加载第一个省份的城市
+						await this.loadCitiesForProvince(this.provinceList[0].id)
+					}
+				} catch (e) {
+					console.error('加载省份失败:', e)
+					uni.showToast({
+						title: '加载省份数据失败',
+						icon: 'none'
+					})
+				}
+			},
+			
+			// 根据省份ID加载城市
+			async loadCitiesForProvince(provinceId) {
+				if (!provinceId) return
+				
+				try {
+					console.log('加载城市数据,省份ID:', provinceId)
+					const cityRes = await api.area.getCities(provinceId)
+					console.log('城市数据返回:', cityRes)
+					
+					// api.js 已经处理了数据格式,直接使用返回的数组
+					if (Array.isArray(cityRes)) {
+						this.cityList = cityRes
+					} else if (cityRes && cityRes.data) {
+						this.cityList = cityRes.data
+					} else if (cityRes && cityRes.code === 200 && cityRes.data) {
+						this.cityList = cityRes.data
+					} else {
+						this.cityList = []
+					}
+					
+					this.multiAreaData[1] = this.cityList
+					console.log('城市列表:', this.cityList)
+					
+					// 如果有已选择的城市,加载对应的区域
+					if (this.profile.cityId) {
+						await this.loadAreasForCity(this.profile.cityId)
+					} else if (this.cityList.length > 0) {
+						// 默认加载第一个城市的区域
+						await this.loadAreasForCity(this.cityList[0].id)
+					} else {
+						this.multiAreaData[2] = []
+						this.areaList = []
+					}
+				} catch (e) {
+					console.error('加载城市失败:', e)
+					this.cityList = []
+					this.multiAreaData[1] = []
+					this.multiAreaData[2] = []
+					this.areaList = []
+				}
+			},
+			
+			// 根据城市ID加载区域
+			async loadAreasForCity(cityId) {
+				if (!cityId) return
+				
+				try {
+					console.log('加载区域数据,城市ID:', cityId)
+					const areaRes = await api.area.getAreas(cityId)
+					console.log('区域数据返回:', areaRes)
+					
+					// api.js 已经处理了数据格式,直接使用返回的数组
+					if (Array.isArray(areaRes)) {
+						this.areaList = areaRes
+					} else if (areaRes && areaRes.data) {
+						this.areaList = areaRes.data
+					} else if (areaRes && areaRes.code === 200 && areaRes.data) {
+						this.areaList = areaRes.data
+					} else {
+						this.areaList = []
+					}
+					
+					this.multiAreaData[2] = this.areaList
+					console.log('区域列表:', this.areaList)
+				} catch (e) {
+					console.error('加载区域失败:', e)
+					this.areaList = []
+					this.multiAreaData[2] = []
+				}
+			},
+			
+			// 省市区选择器列变化事件
+			async onAreaColumnChange(e) {
+				const column = e.detail.column // 列索引:0-省,1-市,2-区
+				const row = e.detail.value // 选中的行索引
+				
+				if (column === 0) {
+					// 选择了省份
+					const province = this.provinceList[row]
+					if (province && province.id) {
+						this.multiAreaIndex[0] = row
+						this.multiAreaIndex[1] = 0
+						this.multiAreaIndex[2] = 0
+						// 加载该省份的城市
+						await this.loadCitiesForProvince(province.id)
+					}
+				} else if (column === 1) {
+					// 选择了城市
+					const city = this.cityList[row]
+					if (city && city.id) {
+						this.multiAreaIndex[1] = row
+						this.multiAreaIndex[2] = 0
+						// 加载该城市的区域
+						await this.loadAreasForCity(city.id)
+					}
+				} else if (column === 2) {
+					// 选择了区域
+					this.multiAreaIndex[2] = row
+				}
+			},
+			
+			// 省市区选择器确认事件
+			onAreaChange(e) {
+				const values = e.detail.value
+				this.multiAreaIndex = values
+				
+				// 设置选中的省市区ID
+				if (this.provinceList[values[0]]) {
+					this.profile.provinceId = this.provinceList[values[0]].id
+				}
+				if (this.cityList[values[1]]) {
+					this.profile.cityId = this.cityList[values[1]].id
+				}
+				if (this.areaList[values[2]]) {
+					this.profile.areaId = this.areaList[values[2]].id
+				}
+			},
+			
+			// 获取地区显示文本
+			getAreaText() {
+				// 如果没有省市区ID,返回未设置
+				if (!this.profile.provinceId && !this.profile.cityId && !this.profile.areaId) {
+					return '未设置'
+				}
+				
+				let text = ''
+				let hasText = false
+				
+				// 查找省份名称
+				if (this.profile.provinceId) {
+					if (this.provinceList.length > 0) {
+						const province = this.provinceList.find(p => p.id === this.profile.provinceId)
+						if (province) {
+							text += province.name
+							hasText = true
+						}
+					} else {
+						// 如果省份列表还没加载,触发加载
+						if (this.provinceList.length === 0) {
+							this.loadAreaData()
+						}
+						return '加载中...'
+					}
+				}
+				
+				// 查找城市名称
+				if (this.profile.cityId) {
+					if (this.cityList.length > 0) {
+						const city = this.cityList.find(c => c.id === this.profile.cityId)
+						if (city) {
+							text += (hasText ? ' ' : '') + city.name
+							hasText = true
+						}
+					} else if (this.profile.provinceId) {
+						// 如果城市列表还没加载但有省份ID,加载城市数据
+						this.loadCitiesForProvince(this.profile.provinceId)
+						return hasText ? text + ' 加载中...' : '加载中...'
+					}
+				}
+				
+				// 查找区域名称
+				if (this.profile.areaId) {
+					if (this.areaList.length > 0) {
+						const area = this.areaList.find(a => a.id === this.profile.areaId)
+						if (area) {
+							text += (hasText ? ' ' : '') + area.name
+							hasText = true
+						}
+					} else if (this.profile.cityId) {
+						// 如果区域列表还没加载但有城市ID,加载区域数据
+						this.loadAreasForCity(this.profile.cityId)
+						return hasText ? text + ' 加载中...' : '加载中...'
+					}
+				}
+				
+				return text || '未设置'
+			},
+			
+			// 初始化省市区选择器索引(根据已保存的ID)
+			async initAreaIndex() {
+				if (!this.profile.provinceId) {
+					console.log('没有省份ID,跳过初始化')
+					return
+				}
+				
+				console.log('开始初始化省市区索引,当前省份ID:', this.profile.provinceId)
+				console.log('省份列表长度:', this.provinceList.length)
+				
+				// 如果省份列表还没加载,先加载
+				if (this.provinceList.length === 0) {
+					console.log('省份列表为空,先加载数据')
+					await this.loadAreaData()
+				}
+				
+				// 查找省份索引
+				const provinceIndex = this.provinceList.findIndex(p => p.id === this.profile.provinceId)
+				console.log('找到的省份索引:', provinceIndex, '省份ID:', this.profile.provinceId)
+				
+				if (provinceIndex >= 0) {
+					this.multiAreaIndex[0] = provinceIndex
+					
+					// 加载该省份的城市
+					await this.loadCitiesForProvince(this.profile.provinceId)
+					
+					// 等待城市加载完成后再查找城市索引
+					await new Promise(resolve => setTimeout(resolve, 300))
+					
+					if (this.profile.cityId) {
+						const cityIndex = this.cityList.findIndex(c => c.id === this.profile.cityId)
+						console.log('找到的城市索引:', cityIndex, '城市ID:', this.profile.cityId)
+						if (cityIndex >= 0) {
+							this.multiAreaIndex[1] = cityIndex
+							
+							// 加载该城市的区域
+							await this.loadAreasForCity(this.profile.cityId)
+							
+							// 等待区域加载完成后再查找区域索引
+							await new Promise(resolve => setTimeout(resolve, 300))
+							
+							if (this.profile.areaId) {
+								const areaIndex = this.areaList.findIndex(a => a.id === this.profile.areaId)
+								console.log('找到的区域索引:', areaIndex, '区域ID:', this.profile.areaId)
+								if (areaIndex >= 0) {
+									this.multiAreaIndex[2] = areaIndex
+								}
+							}
+						}
+					}
+					
+					// 强制更新视图
+					this.$forceUpdate()
+				} else {
+					console.warn('未找到省份索引,省份ID:', this.profile.provinceId)
+					console.log('当前省份列表:', this.provinceList.map(p => ({ id: p.id, name: p.name })))
+				}
+			},
+			
 			// 显示标签选择弹窗
 			addHobby() {
 				this.showHobbyModal = true
@@ -884,6 +1225,17 @@
 				}
 			},
 			
+			// 处理地区点击事件
+			handleAreaClick() {
+				if (!this.isEditing) {
+					this.startEdit()
+				}
+				// 如果数据还没加载,立即加载
+				if (this.provinceList.length === 0) {
+					this.loadAreaData()
+				}
+			},
+			
 			// 启用编辑/保存
 			enableEdit() {
 				if (this.isEditing) {
@@ -922,15 +1274,25 @@
 					car: this.profile.car,
 					hobby: this.profile.hobby,
 					privacySalary: this.profile.privacySalary,
-					privacyPhone: this.profile.privacyPhone
+					privacyPhone: this.profile.privacyPhone,
+					provinceId: this.profile.provinceId || null,
+					cityId: this.profile.cityId || null,
+					areaId: this.profile.areaId || null
 				}
 				
+				console.log('保存省市区数据:', {
+					provinceId: submitData.provinceId,
+					cityId: submitData.cityId,
+					areaId: submitData.areaId
+				})
+				
 				uni.request({
 					url: this.gatewayURL + '/api/user/profile',
 					method: 'PUT',
 					data: submitData,
 					success: (res) => {
 						uni.hideLoading()
+						console.log('保存响应:', res.data)
 						
 						if (res.data && res.data.code === 200) {
 							uni.showToast({
@@ -939,6 +1301,23 @@
 								duration: 1500
 							})
 							this.isEditing = false
+							
+							// 更新 storage 中的 userInfo,特别是性别信息
+							const storedUserInfo = uni.getStorageSync('userInfo');
+							if (storedUserInfo) {
+								// 更新性别信息
+								if (submitData.gender !== undefined && submitData.gender !== null) {
+									storedUserInfo.gender = submitData.gender;
+									console.log('更新storage中的性别信息:', submitData.gender);
+								}
+								// 更新其他可能变化的字段
+								if (submitData.birthDate) {
+									storedUserInfo.birthDate = submitData.birthDate;
+								}
+								uni.setStorageSync('userInfo', storedUserInfo);
+								console.log('已更新storage中的userInfo');
+							}
+							
 							// 重新加载资料
 							this.loadProfile()
 							
@@ -1262,17 +1641,6 @@
 				align-items: flex-start;
 			}
 			
-			// 添加右侧编辑提示(只在非编辑模式显示)
-			&:not(.editing)::after {
-				content: '点击编辑';
-				position: absolute;
-				right: 30rpx;
-				color: #CCCCCC;
-				font-size: 22rpx;
-				opacity: 0.5;
-				pointer-events: none;
-			}
-			
 			.label {
 				font-size: 30rpx;
 				color: #333333;

+ 210 - 28
LiangZhiYUMao/pages/recommend/index.vue

@@ -1,7 +1,7 @@
 <template>
 	<view class="recommend-page">
 		<view class="toolbar">
-			<button size="mini" class="tool-btn refresh-btn" @click="refresh"><text class="icon">⟳</text><text>换一批</text></button>
+			<button size="mini" class="tool-btn refresh-btn" @click="changeBatch"><text class="icon">⟳</text><text>换一批</text></button>
 			<button size="mini" class="tool-btn filter-btn" @click="openFilter"><text class="icon">🔍</text><text>筛选</text></button>
 		</view>
 		
@@ -21,14 +21,6 @@
 				<view class="sheet-handle"></view>
 				<view class="sheet-title">精确筛选</view>
 				<view class="sheet-body">
-					<view class="field">
-						<text class="field-label">性别</text>
-						<view class="seg">
-							<view :class="['seg-item', query.gender===null?'active':'']" @click="setGender(null)">不限</view>
-							<view :class="['seg-item', query.gender===1?'active':'']" @click="setGender(1)">男</view>
-							<view :class="['seg-item', query.gender===2?'active':'']" @click="setGender(2)">女</view>
-						</view>
-					</view>
 					<view class="field two">
 						<text class="field-label">年龄</text>
 						<input class="ipt" type="number" v-model.number="query.ageMin" placeholder="最小" />
@@ -201,7 +193,7 @@
 		<scroll-view scroll-y class="list">
 			<view v-if="loading" class="skeleton">加载中...</view>
 			<view v-else>
-				<view v-for="(u, idx) in list" :key="u.userId || idx" class="card">
+				<view v-for="(u, idx) in filteredList" :key="u.userId || idx" class="card">
 					<image :src="u.avatarUrl || '/static/close.png'" class="avatar" mode="aspectFill" @error="onImgErr(idx)"/>
 					<view class="main" @click="showUserDetailByIndex(idx)">
 						<view class="row">
@@ -218,7 +210,7 @@
 							<text v-if="u.star" class="tag">{{ u.star }}</text>
 							<text v-if="u.animal" class="tag">{{ u.animal }}</text>
 							<text v-if="u.jobTitle" class="tag">{{ u.jobTitle }}</text>
-							<text v-for="t in parseHobby(u.hobby)" :key="t" class="tag">{{ t }}</text>
+							<text v-for="(t, tIdx) in parseHobby(u.hobby)" :key="`hobby-${u.userId}-${tIdx}`" class="tag">{{ t }}</text>
 						</view>
 					</view>
 					<view class="actions">
@@ -266,10 +258,12 @@ export default {
 			list: [], 
 			loading: false, 
 			oppoOnly: 1,
+			currentUserId: null, // 当前登录用户ID
+			currentUserGender: null, // 当前登录用户性别
 			userDetail: null, // 用户详细信息
-		  query: { userId: null, gender: null, ageMin: null, ageMax: null, heightMin: null, heightMax: null, provinceId: null, cityId: null, areaId: null, educationMin: null, salaryMin: null, star: null, animal: null, hobbyTags: [], limit: 20, offset: 0 },
+			shownUserIds: [], // 已显示过的用户ID列表(用于换一批功能)
+		  query: { userId: null, ageMin: null, ageMax: null, heightMin: null, heightMax: null, provinceId: null, cityId: null, areaId: null, educationMin: null, salaryMin: null, star: null, animal: null, hobbyTags: [], limit: 10, offset: 0 },
       hobbyInput: '',
-      genderOptions: [{label:'不限', value:null},{label:'男', value:1},{label:'女', value:2}],
       educationOptions: [
         {label:'不限', value:null},{label:'高中及以下', value:1},{label:'大专', value:2},{label:'本科', value:3},{label:'硕士', value:4},{label:'博士', value:5}
       ],
@@ -292,18 +286,112 @@ export default {
 	computed: {
 	    unreadCount() {
 	      return this.$store.getters.getTotalUnread || 0;
+	    },
+	    // 过滤后的推荐列表:排除当前用户和未完善性别信息的用户
+	    filteredList() {
+	      return this.list.filter(user => {
+	        // 排除当前用户本身
+	        if (user.userId && this.currentUserId && user.userId === this.currentUserId) {
+	          return false;
+	        }
+	        // 排除未完善性别信息的用户(gender为null或0)
+	        if (!user.gender || user.gender === 0) {
+	          return false;
+	        }
+	        // 如果当前用户性别已设置,只显示异性
+	        if (this.currentUserGender && this.currentUserGender !== 0) {
+	          if (user.gender === this.currentUserGender) {
+	            return false; // 排除同性
+	          }
+	        }
+	        return true;
+	      });
 	    }
 	  },
 
-	onLoad() { this.refresh() },
+	async onLoad() { 
+		await this.loadCurrentUserInfo();
+		this.refresh();
+	},
+	
+	// 页面显示时重新加载用户信息和推荐列表
+	async onShow() {
+		console.log('推荐页面显示,重新加载用户信息和推荐列表');
+		// 重新加载当前用户信息(可能在其他页面修改了性别)
+		const genderChanged = await this.loadCurrentUserInfo();
+		
+		// 如果性别发生变化,或者列表为空,则刷新推荐列表
+		if (genderChanged || this.list.length === 0) {
+			console.log('性别发生变化或列表为空,刷新推荐列表');
+			console.log('当前用户性别:', this.currentUserGender);
+			// 性别变化时,清空已显示列表,重新开始
+			if (genderChanged) {
+				this.shownUserIds = [];
+			}
+			this.refresh(false);
+		}
+	},
 	methods: {
+		// 加载当前用户信息
+		async loadCurrentUserInfo() {
+			try {
+				const storedUserId = uni.getStorageSync('userId');
+				if (storedUserId) {
+					this.currentUserId = parseInt(storedUserId);
+					
+					// 优先从storage中获取用户信息(包括性别)
+					const storedUserInfo = uni.getStorageSync('userInfo');
+					const oldGender = this.currentUserGender;
+					
+					if (storedUserInfo && storedUserInfo.gender !== undefined && storedUserInfo.gender !== null) {
+						this.currentUserGender = parseInt(storedUserInfo.gender);
+						console.log('从storage获取当前用户性别:', this.currentUserGender);
+					}
+					
+					// 总是从API获取最新的用户信息,确保性别信息是最新的
+					try {
+						const userInfo = await api.user.getDetailInfo(this.currentUserId);
+						if (userInfo && userInfo.gender !== undefined && userInfo.gender !== null) {
+							const newGender = parseInt(userInfo.gender);
+							if (newGender !== this.currentUserGender) {
+								console.log('检测到性别变化:', '旧性别:', this.currentUserGender, '新性别:', newGender);
+								this.currentUserGender = newGender;
+								// 更新storage中的用户信息
+								if (storedUserInfo) {
+									storedUserInfo.gender = this.currentUserGender;
+									uni.setStorageSync('userInfo', storedUserInfo);
+								}
+							} else {
+								this.currentUserGender = newGender;
+								console.log('从API获取当前用户性别:', this.currentUserGender);
+							}
+						}
+					} catch (e) {
+						console.error('获取当前用户信息失败:', e);
+						// API失败时,如果storage中有性别信息,使用storage中的
+						if (!this.currentUserGender && storedUserInfo && storedUserInfo.gender !== undefined && storedUserInfo.gender !== null) {
+							this.currentUserGender = parseInt(storedUserInfo.gender);
+						}
+					}
+					
+					console.log('最终当前用户ID:', this.currentUserId, '性别:', this.currentUserGender);
+					
+					// 返回性别是否发生变化
+					return oldGender !== this.currentUserGender;
+				}
+				return false;
+			} catch (e) {
+				console.error('加载当前用户信息失败:', e);
+				return false;
+			}
+		},
 		// 切换选项卡
 		switchToTab(tab) {
 			this.currentTab = tab
 			if (tab === 'recommend') {
 				// 保持在当前推荐页面,刷新推荐列表
 				console.log('切换到推荐页面')
-				this.refresh()
+				this.refresh(false)
 			} else if (tab === 'match') {
 				// 跳转到匹配页面
 				console.log('切换到在线匹配')
@@ -331,22 +419,76 @@ export default {
 				uni.redirectTo({ url: tabPages[tab] })
 			}
 		},
-    async refresh() {
+    async refresh(isChangeBatch = false) {
 			this.loading = true
 			try {
-        const storedUserId = uni.getStorageSync('userId'); const userId = storedUserId ? parseInt(storedUserId) : 1
+				// 确保已加载当前用户信息
+				if (!this.currentUserId || this.currentUserGender === null || this.currentUserGender === undefined) {
+					await this.loadCurrentUserInfo();
+				}
+				
+        const userId = this.currentUserId || parseInt(uni.getStorageSync('userId') || '1');
+        
+        console.log('刷新推荐列表 - 当前用户ID:', userId, '性别:', this.currentUserGender, '换一批:', isChangeBatch);
+        
         // 如果没有筛选,走默认接口;有筛选,调用 search
         if (!this.hasFilter()) {
-          const data = await api.recommend.getUsers({ userId, oppoOnly: this.oppoOnly, limit: 20 })
-          const seen = new Set(); const merged = []
-          for (const it of (data || [])) { if (!seen.has(it.userId)) { seen.add(it.userId); merged.push(it) } }
-          this.list = merged.map(it=>({ ...it, avatarUrl: this.getSafeAvatar(it.avatarUrl) }))
+          // 如果是换一批,传递已显示的用户ID列表
+          let excludeIdsParam = '';
+          if (isChangeBatch && this.shownUserIds.length > 0) {
+            excludeIdsParam = this.shownUserIds.join(',');
+            console.log('换一批 - 排除已显示的用户ID:', excludeIdsParam);
+          }
+          
+          // 每页显示10个推荐用户
+          const params = { userId, oppoOnly: 1, limit: 10 };
+          if (excludeIdsParam) {
+            params.excludeIds = excludeIdsParam;
+          }
+          
+          const data = await api.recommend.getUsers(params);
+          console.log('推荐接口返回数据数量:', data ? data.length : 0, '数据:', data);
+          
+          // 如果换一批时返回的数据为空或很少,说明所有用户都显示过了,重新开始
+          if (isChangeBatch && (!data || data.length === 0)) {
+            console.log('所有用户都已显示过,重新开始');
+            this.shownUserIds = []; // 清空已显示列表
+            // 重新获取,不排除任何用户
+            const newData = await api.recommend.getUsers({ userId, oppoOnly: 1, limit: 10 });
+            if (newData && newData.length > 0) {
+              this.processRecommendData(newData, userId);
+              return;
+            }
+          }
+          
+          if (data && data.length > 0) {
+            this.processRecommendData(data, userId);
+          } else {
+            this.list = [];
+            if (isChangeBatch) {
+              uni.showToast({ title: '暂无更多推荐', icon: 'none' });
+            }
+          }
         } else {
-          this.query.userId = userId; this.query.limit = 20; this.query.offset = 0
+          this.query.userId = userId; this.query.limit = 10; this.query.offset = 0
+          // 不设置query.gender,让后端根据当前用户性别自动推荐异性
           const payload = this.buildQueryPayload()
           const data = await api.recommend.search(payload)
           const arr = Array.isArray(data) ? data : []
-          this.list = arr.map(it=>({ ...it, avatarUrl: this.getSafeAvatar(it.avatarUrl) }))
+          // 再次过滤:排除当前用户和未完善性别信息的用户
+          this.list = arr
+            .filter(it => {
+              // 排除当前用户本身
+              if (!it.userId || it.userId === userId) return false;
+              // 排除未完善性别信息的用户
+              if (!it.gender || it.gender === 0) return false;
+              // 如果当前用户性别已设置,只保留异性
+              if (this.currentUserGender && this.currentUserGender !== 0) {
+                if (it.gender === this.currentUserGender) return false; // 排除同性
+              }
+              return true;
+            })
+            .map(it=>({ ...it, avatarUrl: this.getSafeAvatar(it.avatarUrl) }))
         }
 			} catch (e) {
         const msg = (e && (e.message || e.msg)) ? String(e.message || e.msg) : '获取推荐失败'
@@ -354,15 +496,55 @@ export default {
         uni.showToast({ title: msg, icon: 'none' })
 			} finally { this.loading = false }
 		},
+		
+		// 处理推荐数据:过滤并记录已显示的用户ID
+		processRecommendData(data, userId) {
+		  const seen = new Set();
+		  const merged = [];
+		  
+		  for (const it of (data || [])) { 
+		    // 过滤:排除当前用户和未完善性别信息的用户
+		    if (!it.userId || it.userId === userId || !it.gender || it.gender === 0) {
+		      console.log('过滤用户:', it.userId, '原因: 当前用户或性别未完善');
+		      continue;
+		    }
+		    // 如果当前用户性别已设置,只保留异性
+		    if (this.currentUserGender && this.currentUserGender !== 0) {
+		      if (it.gender === this.currentUserGender) {
+		        console.log('过滤用户:', it.userId, '原因: 同性用户, 当前用户性别:', this.currentUserGender, '推荐用户性别:', it.gender);
+		        continue; // 排除同性
+		      }
+		    }
+		    if (!seen.has(it.userId)) { 
+		      seen.add(it.userId); 
+		      merged.push(it);
+		      // 记录已显示的用户ID
+		      if (!this.shownUserIds.includes(it.userId)) {
+		        this.shownUserIds.push(it.userId);
+		      }
+		    } 
+		  }
+		  
+		  console.log('过滤后的推荐列表数量:', merged.length, '已显示用户总数:', this.shownUserIds.length);
+		  this.list = merged.map(it=>({ ...it, avatarUrl: this.getSafeAvatar(it.avatarUrl) }));
+		},
+		// 换一批功能
+		changeBatch() {
+			console.log('点击换一批,当前已显示用户数:', this.shownUserIds.length);
+			this.refresh(true);
+		},
 		openFilter(){ this.ensureAreaData(); this.$refs.filterPopup.open('bottom') },
 		closeFilter(){ this.$refs.filterPopup.close() },
-		applyFilter(){ this.$refs.filterPopup.close(); this.refresh() },
+		applyFilter(){ 
+			this.$refs.filterPopup.close(); 
+			// 应用筛选时,清空已显示列表,重新开始
+			this.shownUserIds = [];
+			this.refresh(false); 
+		},
 		hasFilter(){
 		  const q=this.query
-		  return !!(q.gender||q.ageMin||q.ageMax||q.heightMin||q.heightMax||q.provinceId||q.cityId||q.areaId||q.educationMin||q.salaryMin||q.star||q.animal||this.hobbyInput)
+		  return !!(q.ageMin||q.ageMax||q.heightMin||q.heightMax||q.provinceId||q.cityId||q.areaId||q.educationMin||q.salaryMin||q.star||q.animal||this.hobbyInput)
 		},
-		genderText(v){ if(v===1)return '男'; if(v===2)return '女'; return '不限' },
-		setGender(v){ this.query.gender=v },
 		onEduChange(e){
 			const item = this.educationOptions[e.detail.value];
 			this.query.educationMin = item.value;
@@ -400,7 +582,7 @@ export default {
 			return q
 		},
 		resetFilter(){
-		  this.query={...this.query, gender:null, ageMin:null, ageMax:null, heightMin:null, heightMax:null, provinceId:null, cityId:null, areaId:null, educationMin:null, salaryMin:null, star:null, animal:null};
+		  this.query={...this.query, ageMin:null, ageMax:null, heightMin:null, heightMax:null, provinceId:null, cityId:null, areaId:null, educationMin:null, salaryMin:null, star:null, animal:null};
 		  this.hobbyInput=''; this.currentStarText=''; this.currentAnimalText='';
 		  this.areaDisplayText='';
 		  this.multiAreaIndex=[0, 0, 0];

+ 7 - 3
LiangZhiYUMao/utils/api.js

@@ -260,9 +260,13 @@ export default {
   // 推荐相关
   recommend: {
     // 获取推荐用户列表(网关转发到推荐服务)
-    getUsers: ({ userId, oppoOnly = 1, limit = 20 }) => request({
-      url: `/recommend/users?userId=${userId}&oppoOnly=${oppoOnly}&limit=${limit}`
-    }),
+    getUsers: ({ userId, oppoOnly = 1, limit = 20, excludeIds }) => {
+      let url = `/recommend/users?userId=${userId}&oppoOnly=${oppoOnly}&limit=${limit}`;
+      if (excludeIds) {
+        url += `&excludeIds=${excludeIds}`;
+      }
+      return request({ url });
+    },
     // 行为反馈:like/dislike
     feedback: ({ userId, targetUserId, type }) => request({
       url: `/recommend/feedback?userId=${userId}&targetUserId=${targetUserId}&type=${type}`

+ 1 - 1
package-lock.json

@@ -1,5 +1,5 @@
 {
-  "name": "xiangqinxiangmu",
+  "name": "qingluanzhilian",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {}

+ 42 - 10
service/Essential/src/main/java/com/zhentao/service/impl/UserProfileServiceImpl.java

@@ -1,5 +1,6 @@
 package com.zhentao.service.impl;
 
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
 import com.zhentao.entity.User;
 import com.zhentao.entity.UserProfile;
 import com.zhentao.entity.UserVip;
@@ -86,6 +87,17 @@ public class UserProfileServiceImpl implements UserProfileService {
             result.put("privacyPhone", profile.getPrivacyPhone());
             result.put("authenticityScore", profile.getAuthenticityScore());
             
+            // 省市区ID - 添加调试日志
+            System.out.println("=== 用户资料查询 - 省市区ID ===");
+            System.out.println("用户ID: " + userId);
+            System.out.println("provinceId: " + profile.getProvinceId());
+            System.out.println("cityId: " + profile.getCityId());
+            System.out.println("areaId: " + profile.getAreaId());
+            
+            result.put("provinceId", profile.getProvinceId());
+            result.put("cityId", profile.getCityId());
+            result.put("areaId", profile.getAreaId());
+            
             // 认证状态
             result.put("isRealNameVerified", profile.getIsRealNameVerified() == 1);
             result.put("isEducationVerified", profile.getIsEducationVerified() == 1);
@@ -115,6 +127,9 @@ public class UserProfileServiceImpl implements UserProfileService {
             result.put("privacySalary", 0);
             result.put("privacyPhone", 0);
             result.put("authenticityScore", 0);
+            result.put("provinceId", null);
+            result.put("cityId", null);
+            result.put("areaId", null);
             result.put("isRealNameVerified", false);
             result.put("isEducationVerified", false);
             result.put("isWorkVerified", false);
@@ -229,15 +244,19 @@ public class UserProfileServiceImpl implements UserProfileService {
             if (userProfile.getMaritalStatus() == null) {
                 userProfile.setMaritalStatus(existProfile.getMaritalStatus());
             }
-            if (userProfile.getProvinceId() == null) {
-                userProfile.setProvinceId(existProfile.getProvinceId());
-            }
-            if (userProfile.getCityId() == null) {
-                userProfile.setCityId(existProfile.getCityId());
-            }
-            if (userProfile.getAreaId() == null) {
-                userProfile.setAreaId(existProfile.getAreaId());
-            }
+            // 省市区字段:前端总是会传递这些字段,即使值为null
+            // 为了支持更新,我们直接使用传入的值,不进行null检查
+            // 添加调试日志
+            System.out.println("=== 更新省市区信息 ===");
+            System.out.println("传入的provinceId: " + userProfile.getProvinceId());
+            System.out.println("传入的cityId: " + userProfile.getCityId());
+            System.out.println("传入的areaId: " + userProfile.getAreaId());
+            System.out.println("原值provinceId: " + existProfile.getProvinceId());
+            System.out.println("原值cityId: " + existProfile.getCityId());
+            System.out.println("原值areaId: " + existProfile.getAreaId());
+            
+            // 注意:这里不再检查null,直接使用传入的值进行更新
+            // 如果前端传递了这些字段,就更新它们(即使值为null)
             if (userProfile.getPrivacySalary() == null) {
                 userProfile.setPrivacySalary(existProfile.getPrivacySalary());
             }
@@ -252,7 +271,20 @@ public class UserProfileServiceImpl implements UserProfileService {
             int score = calculateAuthenticityScore(userProfile);
             userProfile.setAuthenticityScore(score);
             
-            // 只调用一次updateById,包含评分更新
+            // 使用UpdateWrapper来确保省市区字段能够更新(包括null值)
+            UpdateWrapper<UserProfile> updateWrapper = new UpdateWrapper<>();
+            updateWrapper.eq("user_id", userProfile.getUserId());
+            
+            // 明确设置省市区字段,允许更新为null
+            updateWrapper.set("province_id", userProfile.getProvinceId());
+            updateWrapper.set("city_id", userProfile.getCityId());
+            updateWrapper.set("area_id", userProfile.getAreaId());
+            
+            // 先更新省市区字段
+            int areaResult = userProfileMapper.update(null, updateWrapper);
+            System.out.println("✅ 更新省市区字段 - 影响行数: " + areaResult);
+            
+            // 然后使用updateById更新其他字段(会自动忽略null值,但我们已经用UpdateWrapper更新了省市区)
             result = userProfileMapper.updateById(userProfile);
             System.out.println("✅ 更新user_profile表 - 用户ID: " + userProfile.getUserId() + ", 更新后评分: " + score + ", 影响行数: " + result);
         }

+ 41 - 2
service/Recommend/src/main/java/com/zhentao/controller/RecommendController.java

@@ -33,10 +33,49 @@ public class RecommendController {
             @RequestParam Integer userId,
             @RequestParam(required = false, defaultValue = "1") Integer oppoOnly,
             @RequestParam(required = false, defaultValue = "50") Integer limit,
-            @RequestParam(required = false, defaultValue = "0") Integer offset
+            @RequestParam(required = false, defaultValue = "0") Integer offset,
+            @RequestParam(required = false) String excludeIds
     ) {
         try {
-            List<RecommendUserVO> list = recommendService.getRecommendedUsers(userId, oppoOnly, limit);
+            // 强制只推荐异性,不允许推荐同性
+            oppoOnly = 1;
+            
+            // 解析excludeIds参数(逗号分隔的字符串)
+            List<Integer> excludeList = null;
+            if (excludeIds != null && !excludeIds.trim().isEmpty()) {
+                try {
+                    excludeList = java.util.Arrays.stream(excludeIds.split(","))
+                        .map(String::trim)
+                        .filter(s -> !s.isEmpty())
+                        .map(Integer::parseInt)
+                        .collect(java.util.stream.Collectors.toList());
+                } catch (Exception e) {
+                    System.err.println("解析excludeIds失败: " + e.getMessage());
+                    excludeList = null;
+                }
+            }
+            
+            List<RecommendUserVO> list;
+            if (excludeList != null && !excludeList.isEmpty()) {
+                // 使用带排除列表的方法
+                list = recommendService.getRecommendedUsers(userId, oppoOnly, limit, excludeList);
+            } else {
+                // 使用原方法
+                list = recommendService.getRecommendedUsers(userId, oppoOnly, limit);
+            }
+            
+            // Service层已经做了完整的过滤,这里只做最后的兜底检查
+            if (list != null) {
+                list.removeIf(user -> 
+                    user.getUserId() == null || 
+                    user.getUserId().equals(userId) || 
+                    user.getGender() == null || 
+                    user.getGender() == 0
+                );
+            } else {
+                list = new java.util.ArrayList<>();
+            }
+            System.out.println("推荐接口返回数据数量: " + (list != null ? list.size() : 0) + ", 排除用户数: " + (excludeList != null ? excludeList.size() : 0));
             return Result.success(list);
         } catch (Exception e) {
             e.printStackTrace();

+ 1 - 0
service/Recommend/src/main/java/com/zhentao/service/RecommendService.java

@@ -7,6 +7,7 @@ import java.util.List;
 
 public interface RecommendService {
     List<RecommendUserVO> getRecommendedUsers(Integer userId, Integer oppoOnly, Integer limit);
+    List<RecommendUserVO> getRecommendedUsers(Integer userId, Integer oppoOnly, Integer limit, List<Integer> excludeIds);
     List<RecommendUserVO> searchByRules(UserSearchQuery query);
     List<com.zhentao.pojo.Province> getAllProvinces();
     List<com.zhentao.pojo.City> getAllCities();

+ 156 - 18
service/Recommend/src/main/java/com/zhentao/service/impl/RecommendServiceImpl.java

@@ -2,12 +2,15 @@ package com.zhentao.service.impl;
 
 import com.zhentao.config.RecommendProps;
 import com.zhentao.mapper.RecommendMapper;
+import com.zhentao.mapper.UsersMapper;
+import com.zhentao.pojo.Users;
 import com.zhentao.service.RecommendService;
 import com.zhentao.vo.RecommendUserVO;
 import com.zhentao.dto.UserSearchQuery;
 import org.springframework.stereotype.Service;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 
 import javax.annotation.Resource;
 import java.util.List;
@@ -29,6 +32,9 @@ public class RecommendServiceImpl implements RecommendService {
     @Resource
     private RecommendMapper recommendMapper;
 
+    @Resource
+    private UsersMapper usersMapper;
+
     @Resource
     private RecommendProps recommendProps;
 
@@ -41,37 +47,86 @@ public class RecommendServiceImpl implements RecommendService {
         if (userId == null) {
             throw new IllegalArgumentException("userId cannot be null");
         }
-        if (oppoOnly == null) {
-            oppoOnly = 1; // default opposite gender
-        }
+        // 强制只推荐异性,不允许推荐同性
+        oppoOnly = 1; // 始终强制设置为1,只推荐异性
         if (limit == null || limit <= 0) {
             limit = 50;
         }
+        // 先查询当前用户的性别信息,用于清除缓存和调试
+        Users currentUser = null;
+        try {
+            currentUser = usersMapper.selectById(userId);
+            if (currentUser != null) {
+                log.info("当前用户ID: {}, 性别: {}", userId, currentUser.getGender());
+            } else {
+                log.warn("当前用户不存在,userId: {}", userId);
+            }
+        } catch (Exception ex) {
+            log.warn("获取当前用户信息失败", ex);
+        }
+        
+        // 清除可能存在的旧缓存(当用户性别改变时,缓存可能包含错误的数据)
         // 读取或生成候选池缓存
         int poolSize = Math.min(Math.max(limit * 5, 100), 500);
         String poolKey = "rec:pool:" + userId + ":" + oppoOnly + ":" + poolSize;
         List<RecommendUserVO> pool = null;
+        
+        // 如果启用了缓存,先尝试清除可能存在的旧缓存(基于用户ID的所有缓存)
         if (recommendProps.isCacheEnabled()) {
             try {
-                String cached = stringRedisTemplate.opsForValue().get(poolKey);
-                if (cached != null && !cached.isEmpty()) {
-                    pool = objectMapper.readValue(cached, new TypeReference<List<RecommendUserVO>>(){});
+                // 清除该用户的所有推荐池缓存,确保获取最新数据
+                String cachePattern = "rec:pool:" + userId + ":*";
+                java.util.Set<String> keys = stringRedisTemplate.keys(cachePattern);
+                if (keys != null && !keys.isEmpty()) {
+                    stringRedisTemplate.delete(keys);
+                    log.info("已清除用户 {} 的推荐缓存,共 {} 个键", userId, keys.size());
                 }
-            } catch (Exception ignore) {}
-        }
-        if (pool == null) {
-            try {
-                pool = recommendMapper.selectRecommendedUsers(userId, oppoOnly, poolSize);
-            } catch (Exception ex) {
-                log.error("selectRecommendedUsers failed, fallback to empty list (possibly DB not available)", ex);
-                return java.util.Collections.emptyList();
+            } catch (Exception ignore) {
+                log.warn("清除缓存失败", ignore);
             }
-            if (recommendProps.isCacheEnabled() && pool != null && !pool.isEmpty()) {
+        }
+        // 始终从数据库查询最新数据,不依赖缓存(确保性别变更后能立即生效)
+        try {
+            pool = recommendMapper.selectRecommendedUsers(userId, oppoOnly, poolSize);
+            log.info("推荐查询返回数据量: {}, oppoOnly: {}, userId: {}", pool != null ? pool.size() : 0, oppoOnly, userId);
+            if (pool == null || pool.isEmpty()) {
+                log.warn("推荐查询返回空结果,可能原因:1.当前用户性别未设置 2.数据库中没有异性用户 3.SQL查询条件过严");
+                // 如果查询返回空,尝试直接查询数据库中的异性用户数量
                 try {
-                    String json = objectMapper.writeValueAsString(pool);
-                    stringRedisTemplate.opsForValue().set(poolKey, json, recommendProps.getCacheTtlSeconds(), TimeUnit.SECONDS);
-                } catch (Exception ignore) {}
+                    QueryWrapper<Users> queryWrapper = new QueryWrapper<>();
+                    queryWrapper.eq("status", 1)
+                            .isNotNull("gender")
+                            .ne("gender", 0)
+                            .ne("user_id", userId);
+                    if (currentUser != null && currentUser.getGender() != null && currentUser.getGender() != 0) {
+                        queryWrapper.ne("gender", currentUser.getGender());
+                    }
+                    long oppositeGenderCount = usersMapper.selectList(queryWrapper).size();
+                    log.info("数据库中符合条件的异性用户数量: {}", oppositeGenderCount);
+                    if (oppositeGenderCount > 0) {
+                        log.warn("数据库中有 {} 个异性用户,但SQL查询返回空,可能是SQL查询逻辑有问题", oppositeGenderCount);
+                    }
+                } catch (Exception e) {
+                    log.error("查询异性用户数量失败", e);
+                }
+            } else {
+                log.info("返回数据中的性别分布: 男性={}, 女性={}, 未知={}", 
+                    pool.stream().filter(u -> u.getGender() != null && u.getGender() == 1).count(),
+                    pool.stream().filter(u -> u.getGender() != null && u.getGender() == 2).count(),
+                    pool.stream().filter(u -> u.getGender() == null || u.getGender() == 0).count());
             }
+        } catch (Exception ex) {
+            log.error("selectRecommendedUsers failed, fallback to empty list (possibly DB not available)", ex);
+            ex.printStackTrace();
+            return java.util.Collections.emptyList();
+        }
+        
+        // 查询成功后,更新缓存(可选,用于性能优化)
+        if (recommendProps.isCacheEnabled() && pool != null && !pool.isEmpty()) {
+            try {
+                String json = objectMapper.writeValueAsString(pool);
+                stringRedisTemplate.opsForValue().set(poolKey, json, recommendProps.getCacheTtlSeconds(), TimeUnit.SECONDS);
+            } catch (Exception ignore) {}
         }
 
         // 过滤用户的"不喜欢"集合,避免再次出现
@@ -81,6 +136,33 @@ public class RecommendServiceImpl implements RecommendService {
                 pool.removeIf(u -> u.getUserId() != null && disliked.contains(String.valueOf(u.getUserId())));
             }
         } catch (Exception ignore) {}
+        
+        // 再次过滤:确保不包含当前用户本身和未完善性别信息的用户,以及同性用户
+        if (pool == null) {
+            pool = new ArrayList<>();
+        }
+        final Integer finalCurrentUserGender = currentUser != null ? currentUser.getGender() : null;
+        pool.removeIf(u -> {
+            if (u.getUserId() == null || u.getUserId().equals(userId)) {
+                log.debug("过滤当前用户自己: userId={}", u.getUserId());
+                return true;
+            }
+            if (u.getGender() == null || u.getGender() == 0) {
+                log.debug("过滤未设置性别的用户: userId={}", u.getUserId());
+                return true;
+            }
+            // 如果当前用户性别已设置,排除同性
+            if (finalCurrentUserGender != null && finalCurrentUserGender != 0) {
+                if (u.getGender().equals(finalCurrentUserGender)) {
+                    log.debug("过滤同性用户: userId={}, gender={}, 当前用户gender={}", u.getUserId(), u.getGender(), finalCurrentUserGender);
+                    return true;
+                }
+            }
+            return false;
+        });
+        log.info("过滤后的推荐列表数量: {}", pool.size());
+        
+        log.info("过滤后的推荐列表数量: {}", pool != null ? pool.size() : 0);
 
         // 迭代式 MMR:每次从候选中选择 reRank 分最高者加入结果集
         double lambda = recommendProps.getMmrLambda();
@@ -148,6 +230,33 @@ public class RecommendServiceImpl implements RecommendService {
         return result;
     }
 
+    @Override
+    public List<RecommendUserVO> getRecommendedUsers(Integer userId, Integer oppoOnly, Integer limit, List<Integer> excludeIds) {
+        // 调用原方法获取推荐列表
+        List<RecommendUserVO> allUsers = getRecommendedUsers(userId, oppoOnly, limit * 10); // 获取更多数据以便过滤
+        
+        if (excludeIds == null || excludeIds.isEmpty()) {
+            // 如果没有排除列表,直接返回前limit个
+            return allUsers.stream().limit(limit).collect(java.util.stream.Collectors.toList());
+        }
+        
+        // 过滤掉已显示的用户
+        java.util.Set<Integer> excludeSet = new java.util.HashSet<>(excludeIds);
+        List<RecommendUserVO> filtered = allUsers.stream()
+            .filter(u -> u.getUserId() != null && !excludeSet.contains(u.getUserId()))
+            .limit(limit)
+            .collect(java.util.stream.Collectors.toList());
+        
+        log.info("排除已显示用户后,返回 {} 个用户(排除 {} 个用户ID)", filtered.size(), excludeIds.size());
+        
+        // 如果过滤后数量不足,说明所有用户都显示过了,返回空列表让前端重新开始
+        if (filtered.size() < limit && allUsers.size() > 0) {
+            log.info("所有用户都已显示过,建议前端重新开始");
+        }
+        
+        return filtered;
+    }
+
     @Override
     public List<RecommendUserVO> searchByRules(UserSearchQuery q) {
         if (q == null || q.getUserId() == null) {
@@ -181,6 +290,23 @@ public class RecommendServiceImpl implements RecommendService {
             }
         } catch (Exception ignore) {}
 
+        // 获取当前用户的性别信息
+        Integer currentUserGender = null;
+        try {
+            Users currentUser = usersMapper.selectById(q.getUserId());
+            if (currentUser != null) {
+                currentUserGender = currentUser.getGender();
+            }
+        } catch (Exception ex) {
+            log.warn("Failed to get current user gender, userId: " + q.getUserId(), ex);
+        }
+        
+        // 如果当前用户未完善性别信息,返回空列表
+        if (currentUserGender == null || currentUserGender == 0) {
+            log.warn("Current user has no gender information, userId: " + q.getUserId());
+            return java.util.Collections.emptyList();
+        }
+        
         // 召回
         List<RecommendUserVO> pool;
         try {
@@ -189,6 +315,18 @@ public class RecommendServiceImpl implements RecommendService {
             log.error("selectByRules failed, fallback to empty list (possibly DB not available)", ex);
             return java.util.Collections.emptyList();
         }
+        
+        // 过滤:确保不包含当前用户本身和未完善性别信息的用户,只推荐异性
+        if (pool != null && !pool.isEmpty()) {
+            final Integer finalCurrentUserGender = currentUserGender;
+            pool.removeIf(user -> 
+                user.getUserId() == null || 
+                user.getUserId().equals(q.getUserId()) || 
+                user.getGender() == null || 
+                user.getGender() == 0 ||
+                user.getGender().equals(finalCurrentUserGender)  // 排除同性
+            );
+        }
 
         // 重排(沿用 MMR)
         double lambda = recommendProps.getMmrLambda();

+ 28 - 16
service/Recommend/src/main/resources/mapper/RecommendMapper.xml

@@ -274,30 +274,40 @@
           ) AS compatibility_score,
           u.last_active_at
         FROM users u
-        JOIN user_profile p ON p.user_id = u.user_id
+        LEFT JOIN user_profile p ON p.user_id = u.user_id
         CROSS JOIN (
           SELECT
             u.user_id AS cu_id,
             u.gender AS cu_gender,
             u.birth_date AS cu_birth,
-            up.city_id AS cu_city,
-            up.province_id AS cu_province,
-            up.height AS cu_height,
-            up.weight AS cu_weight,
-            up.education_level AS cu_edu,
-            up.salary_range AS cu_salary,
-            up.hobby AS cu_hobby,
-            up.star AS cu_star
-            ,up.animal AS cu_animal
-            ,up.school_name AS cu_school
-            ,up.job_title AS cu_job_title
+            COALESCE(up.city_id, NULL) AS cu_city,
+            COALESCE(up.province_id, NULL) AS cu_province,
+            COALESCE(up.height, NULL) AS cu_height,
+            COALESCE(up.weight, NULL) AS cu_weight,
+            COALESCE(up.education_level, NULL) AS cu_edu,
+            COALESCE(up.salary_range, NULL) AS cu_salary,
+            COALESCE(up.hobby, NULL) AS cu_hobby,
+            COALESCE(up.star, NULL) AS cu_star,
+            COALESCE(up.animal, NULL) AS cu_animal,
+            COALESCE(up.school_name, NULL) AS cu_school,
+            COALESCE(up.job_title, NULL) AS cu_job_title
           FROM users u
-          JOIN user_profile up ON up.user_id = u.user_id
+          LEFT JOIN user_profile up ON up.user_id = u.user_id
           WHERE u.user_id = #{userId}
+            AND u.gender IS NOT NULL
+            AND u.gender <> 0
         ) cur
-        WHERE u.user_id <> cur.cu_id
+        WHERE cur.cu_id IS NOT NULL
+          AND cur.cu_gender IS NOT NULL
+          AND cur.cu_gender <> 0
+          AND u.user_id <> cur.cu_id
           AND u.status = 1
-          AND (#{oppoOnly} = 0 OR u.gender <> cur.cu_gender)
+          AND u.gender IS NOT NULL 
+          AND u.gender <> 0
+          AND (
+            #{oppoOnly} = 0 
+            OR u.gender <> cur.cu_gender
+          )
         ORDER BY compatibility_score DESC, u.last_active_at DESC
         LIMIT #{limit}
         ]]>
@@ -476,10 +486,12 @@
         ) cur
         <where>
           u.status = 1
+          AND u.gender IS NOT NULL 
+          AND u.gender &lt;&gt; 0
           <if test="q.userId != null">AND u.user_id &lt;&gt; #{q.userId}</if>
           AND (
             (#{q.gender} IS NOT NULL AND u.gender = #{q.gender})
-            OR (#{q.gender} IS NULL AND (cur.cu_gender IS NULL OR u.gender &lt;&gt; cur.cu_gender))
+            OR (#{q.gender} IS NULL AND (cur.cu_gender IS NOT NULL AND cur.cu_gender &lt;&gt; 0 AND u.gender &lt;&gt; cur.cu_gender))
           )
           <if test="q.provinceId != null">AND p.province_id = #{q.provinceId}</if>
           <if test="q.cityId != null">AND p.city_id = #{q.cityId}</if>