Просмотр исходного кода

优质资源 我的资源列表

yuxy 1 месяц назад
Родитель
Сommit
4b826d0562
19 измененных файлов с 1856 добавлено и 155 удалено
  1. 129 4
      LiangZhiYUMao/pages/matchmaker-workbench/client-detail.vue
  2. 220 20
      LiangZhiYUMao/pages/matchmaker-workbench/my-resources.vue
  3. 108 9
      LiangZhiYUMao/pages/matchmaker-workbench/precise-match.vue
  4. 714 83
      LiangZhiYUMao/pages/matchmaker-workbench/quality-resources.vue
  5. 238 16
      LiangZhiYUMao/pages/matchmaker-workbench/resource-input.vue
  6. 96 7
      service/homePage/src/main/java/com/zhentao/controller/MyResourceController.java
  7. 28 2
      service/homePage/src/main/java/com/zhentao/controller/TagController.java
  8. 6 0
      service/homePage/src/main/java/com/zhentao/entity/Tag.java
  9. 63 0
      service/homePage/src/main/java/com/zhentao/entity/TagCategory.java
  10. 16 0
      service/homePage/src/main/java/com/zhentao/mapper/MyResourceMapper.java
  11. 13 0
      service/homePage/src/main/java/com/zhentao/mapper/TagCategoryMapper.java
  12. 18 0
      service/homePage/src/main/java/com/zhentao/service/MyResourceService.java
  13. 19 0
      service/homePage/src/main/java/com/zhentao/service/TagCategoryService.java
  14. 7 0
      service/homePage/src/main/java/com/zhentao/service/TagService.java
  15. 55 0
      service/homePage/src/main/java/com/zhentao/service/impl/MyResourceServiceImpl.java
  16. 27 0
      service/homePage/src/main/java/com/zhentao/service/impl/TagCategoryServiceImpl.java
  17. 11 0
      service/homePage/src/main/java/com/zhentao/service/impl/TagServiceImpl.java
  18. 5 0
      service/homePage/src/main/java/com/zhentao/vo/MyResourceVO.java
  19. 83 14
      service/homePage/src/main/resources/mapper/MyResourceMapper.xml

+ 129 - 4
LiangZhiYUMao/pages/matchmaker-workbench/client-detail.vue

@@ -11,7 +11,10 @@
 		<view class="client-basic-info">
 			<image :src="clientInfo.avatar" class="client-avatar" mode="aspectFill"></image>
 			<view class="basic-info">
-				<text class="client-name">{{ clientInfo.gender }} · {{ clientInfo.name }}</text>
+				<view class="client-name-wrapper">
+					<text class="client-name">{{ clientInfo.name }}</text>
+					<text class="gender-tag">{{ clientInfo.gender }}</text>
+				</view>
 				<view class="status-tag-wrapper">
 					<text class="status-tag register-tag registered">已注册</text>
 					<text class="status-tag match-tag" :class="{ 'matched': followForm.matchStatus === 1, 'unmatched': followForm.matchStatus === 0 || followForm.matchStatus === null }">
@@ -195,7 +198,7 @@
 			</view>
 
 			<!-- 提交按钮(预留) -->
-			<view class="follow-submit-btn" @click="handleSubmitFollow">保存跟进</view>
+			<view class="follow-submit-btn" :class="{ 'disabled': !canFollow }" @click="handleSubmitFollow">保存跟进</view>
 		</scroll-view>
 	</view>
 </template>
@@ -215,6 +218,11 @@ export default {
 				remark: ''
 			},
 			resourceId: null, // 资源ID
+			fromQualityResources: false, // 是否从优质资源列表进入
+			currentUserId: null, // 当前登录用户的用户ID
+			currentMatchmakerId: null, // 当前红娘的matchmaker_id
+			resourceMatchmakerId: null, // 资源的matchmaker_id
+			canFollow: true, // 是否可以跟进
 			clientInfo: {
 				id: 1,
 				name: '',
@@ -277,6 +285,24 @@ export default {
 			console.log('从id参数获取 (兼容):', resourceId)
 		}
 		
+		// 检查是否从优质资源列表进入
+		if (options.fromQualityResources === '1' || options.fromQualityResources === 1) {
+			this.fromQualityResources = true
+			console.log('从优质资源列表进入')
+		}
+		
+		// 获取当前登录用户的ID
+		const userId = uni.getStorageSync('userId')
+		if (userId) {
+			const rawUserId = parseInt(userId)
+			if (!isNaN(rawUserId) && rawUserId > 0) {
+				this.currentUserId = rawUserId
+				console.log('当前登录用户ID:', this.currentUserId)
+				// 获取当前红娘的matchmaker_id
+				this.getCurrentMatchmakerId()
+			}
+		}
+		
 		if (resourceId) {
 			// 确保resourceId是数字类型
 			resourceId = parseInt(resourceId)
@@ -509,6 +535,16 @@ export default {
 		},
 		// 提交跟进
 		async handleSubmitFollow() {
+			// 检查是否可以跟进
+			if (!this.canFollow) {
+				uni.showToast({
+					title: '该用户还没有添加到你的资源当中',
+					icon: 'none',
+					duration: 2000
+				})
+				return
+			}
+			
 			if (!this.resourceId) {
 				uni.showToast({
 					title: '资源ID不存在',
@@ -608,6 +644,51 @@ export default {
 				})
 			}
 		},
+		// 获取当前红娘的matchmaker_id
+		async getCurrentMatchmakerId() {
+			if (!this.currentUserId) {
+				console.warn('当前用户ID为空,无法获取红娘ID')
+				return
+			}
+			
+			try {
+				const baseUrl = process.env.NODE_ENV === 'development' 
+					? 'http://localhost:8083/api'  // 开发环境 - 通过网关
+					: 'https://your-domain.com/api'  // 生产环境
+				
+				// 根据user_id查询红娘信息
+				const [error, res] = await uni.request({
+					url: `${baseUrl}/matchmaker/by-user/${this.currentUserId}`,
+					method: 'GET'
+				})
+				
+				if (error) {
+					console.error('获取红娘信息失败:', error)
+					return
+				}
+				
+				if (res.statusCode === 200 && res.data && res.data.code === 200) {
+					const matchmaker = res.data.data
+					this.currentMatchmakerId = matchmaker.matchmakerId || matchmaker.matchmaker_id
+					console.log('获取到当前红娘的matchmaker_id:', this.currentMatchmakerId)
+					
+					// 如果已经加载了资源信息,重新判断是否可以跟进
+					if (this.resourceMatchmakerId !== null && this.resourceMatchmakerId !== undefined) {
+						if (this.fromQualityResources) {
+							if (!this.currentMatchmakerId || this.resourceMatchmakerId !== this.currentMatchmakerId) {
+								this.canFollow = false
+								console.log('从优质资源列表进入,且资源不属于当前红娘,禁用跟进功能')
+							} else {
+								this.canFollow = true
+								console.log('资源属于当前红娘,可以跟进')
+							}
+						}
+					}
+				}
+			} catch (e) {
+				console.error('获取红娘信息异常:', e)
+			}
+		},
 		// 从API加载客户信息
 		async loadClientInfo(resourceId) {
 			try {
@@ -670,6 +751,29 @@ export default {
 					const marrStatus = data.marrStatus !== undefined ? data.marrStatus : data.marr_status
 					// 择偶要求从my_resource表的mate_selection_criteria字段获取
 					const mateSelectionCriteria = data.mateSelectionCriteria || data.mate_selection_criteria
+					// 获取资源的matchmaker_id
+					const resourceMatchmakerId = data.matchmakerId || data.matchmaker_id
+					this.resourceMatchmakerId = resourceMatchmakerId
+					console.log('资源的matchmaker_id:', resourceMatchmakerId)
+					console.log('当前红娘的matchmaker_id:', this.currentMatchmakerId)
+					
+					// 判断是否可以跟进:如果从优质资源列表进入,且资源不属于当前红娘,则不能跟进
+					if (this.fromQualityResources) {
+						// 如果还没有获取到当前红娘的ID,先获取
+						if (!this.currentMatchmakerId) {
+							await this.getCurrentMatchmakerId()
+						}
+						if (!this.currentMatchmakerId || this.resourceMatchmakerId !== this.currentMatchmakerId) {
+							this.canFollow = false
+							console.log('从优质资源列表进入,且资源不属于当前红娘,禁用跟进功能')
+						} else {
+							this.canFollow = true
+							console.log('资源属于当前红娘,可以跟进')
+						}
+					} else {
+						// 从我的资源列表进入,默认可以跟进
+						this.canFollow = true
+					}
 					
 					// 择偶要求详情字段(从partner_requirement表,用于详情页)
 					const minAge = data.minAge !== undefined ? data.minAge : data.min_age
@@ -887,13 +991,28 @@ export default {
 	.basic-info {
 		flex: 1;
 
+		.client-name-wrapper {
+			display: flex;
+			align-items: center;
+			gap: 12rpx;
+			margin-bottom: 12rpx;
+		}
+
 		.client-name {
 			font-size: 38rpx;
 			font-weight: 600;
 			color: #2C2C2C;
-			margin-bottom: 12rpx;
 			letter-spacing: 0.5rpx;
-			display: block;
+		}
+
+		.gender-tag {
+			font-size: 24rpx;
+			font-weight: 500;
+			padding: 6rpx 14rpx;
+			border-radius: 12rpx;
+			background: #E3F2FD;
+			color: #2196F3;
+			border: 1rpx solid #BBDEFB;
 		}
 
 		.status-tag-wrapper {
@@ -1362,6 +1481,12 @@ export default {
 
 /* 保存按钮 */
 .follow-submit-btn {
+	&.disabled {
+		background: #CCCCCC !important;
+		color: #999999 !important;
+		opacity: 0.6;
+		cursor: not-allowed;
+	}
 	margin-top: 10rpx;
 	width: 100%;
 	padding: 22rpx 0;

+ 220 - 20
LiangZhiYUMao/pages/matchmaker-workbench/my-resources.vue

@@ -12,7 +12,7 @@
 		<view class="search-bar">
 			<view class="search-input-wrapper">
 				<view class="search-icon-small"></view>
-				<input type="text" class="search-input" placeholder="请输入搜索关键词" v-model="searchKeyword" @input="handleSearch" />
+				<input type="text" class="search-input" placeholder="请输入姓名、手机号或标签(如:温柔)" v-model="searchKeyword" @input="handleSearch" />
 			</view>
 		</view>
 
@@ -21,9 +21,10 @@
 		<view class="resource-section" v-if="registeredResources.length > 0">
 			<view class="section-header">
 				<text class="section-title">资源</text>
-				<text class="section-count">({{ registeredResources.length }})</text>
+				<text class="section-count">({{ registeredTotal }})</text>
 			</view>
-			<view class="resource-item" v-for="(item, index) in registeredResources" :key="item.id" @click="handleResourceClick(item)">
+			<view class="resource-list-container">
+				<view class="resource-item" v-for="(item, index) in getRegisteredPageData" :key="item.id" v-if="item" @click="handleResourceClick(item)">
 				<!-- 右上角选中图标 -->
 				<view class="select-icon">✓-</view>
 				
@@ -77,6 +78,19 @@
 					<text class="quality-star">★</text>
 					<text class="quality-text">优质资源</text>
 				</view>
+				</view>
+				<!-- 占位元素,确保数据不足3条时也保持固定高度 -->
+				<view class="resource-placeholder" v-for="(key, index) in getRegisteredPlaceholderKeys" :key="key"></view>
+			</view>
+			<!-- 资源分页控件 -->
+			<view class="pagination" v-if="registeredTotal > registeredPageSize">
+				<view class="pagination-btn" :class="{ disabled: registeredPageNum <= 1 }" @click="prevRegisteredPage">
+					<text>上一页</text>
+				</view>
+				<text class="pagination-info">{{ registeredPageNum }} / {{ getRegisteredTotalPages }}</text>
+				<view class="pagination-btn" :class="{ disabled: registeredPageNum >= getRegisteredTotalPages }" @click="nextRegisteredPage">
+					<text>下一页</text>
+				</view>
 			</view>
 		</view>
 
@@ -84,9 +98,10 @@
 		<view class="resource-section" v-if="unregisteredResources.length > 0">
 			<view class="section-header">
 				<text class="section-title">线索</text>
-				<text class="section-count">({{ unregisteredResources.length }})</text>
+				<text class="section-count">({{ unregisteredTotal }})</text>
 			</view>
-			<view class="resource-item" v-for="(item, index) in unregisteredResources" :key="item.id" @click="handleResourceClick(item)">
+			<view class="resource-list-container">
+				<view class="resource-item" v-for="(item, index) in getUnregisteredPageData" :key="item.id" v-if="item" @click="handleResourceClick(item)">
 				<!-- 右上角选中图标 -->
 				<view class="select-icon">✓-</view>
 				
@@ -137,11 +152,24 @@
 					<text class="quality-star">★</text>
 					<text class="quality-text">优质资源</text>
 				</view>
+				</view>
+				<!-- 占位元素,确保数据不足3条时也保持固定高度 -->
+				<view class="resource-placeholder" v-for="(key, index) in getUnregisteredPlaceholderKeys" :key="key"></view>
+			</view>
+			<!-- 线索分页控件 -->
+			<view class="pagination" v-if="unregisteredTotal > unregisteredPageSize">
+				<view class="pagination-btn" :class="{ disabled: unregisteredPageNum <= 1 }" @click="prevUnregisteredPage">
+					<text>上一页</text>
+				</view>
+				<text class="pagination-info">{{ unregisteredPageNum }} / {{ getUnregisteredTotalPages }}</text>
+				<view class="pagination-btn" :class="{ disabled: unregisteredPageNum >= getUnregisteredTotalPages }" @click="nextUnregisteredPage">
+					<text>下一页</text>
+				</view>
 			</view>
 		</view>
 
 		<!-- 空状态 -->
-		<view class="empty-state" v-if="registeredResources.length === 0 && unregisteredResources.length === 0">
+		<view class="empty-state" v-if="registeredTotal === 0 && unregisteredTotal === 0">
 			<text class="empty-text">暂无资源</text>
 		</view>
 	</scroll-view>
@@ -188,15 +216,65 @@ export default {
 				searchKeyword: '',
 				isFirstLoad: true, // 标记是否为首次加载
 				resources: [], // 保留用于兼容
-				registeredResources: [], // 已注册用户(资源部分)
-				unregisteredResources: [], // 未注册用户(线索部分)
-				refreshTimer: null // 定时刷新器
+				registeredResources: [], // 已注册用户(资源部分)- 全部数据
+				unregisteredResources: [], // 未注册用户(线索部分)- 全部数据
+				refreshTimer: null, // 定时刷新器
+				// 资源分页
+				registeredPageNum: 1, // 资源当前页码
+				registeredPageSize: 3, // 资源每页显示数量
+				registeredTotal: 0, // 资源总数
+				// 线索分页
+				unregisteredPageNum: 1, // 线索当前页码
+				unregisteredPageSize: 3, // 线索每页显示数量
+				unregisteredTotal: 0 // 线索总数
 			}
 		},
 		computed: {
 			// 全局未读消息数
 			unreadCount() {
 				return this.$store.getters.getTotalUnread || 0
+			},
+			// 获取资源当前页数据(确保始终返回3条,不足的用null占位)
+			getRegisteredPageData() {
+				const start = (this.registeredPageNum - 1) * this.registeredPageSize
+				const end = start + this.registeredPageSize
+				const pageData = this.registeredResources.slice(start, end)
+				// 如果数据不足3条,用null填充到3条
+				while (pageData.length < this.registeredPageSize) {
+					pageData.push(null)
+				}
+				return pageData
+			},
+			// 获取线索当前页数据(确保始终返回3条,不足的用null占位)
+			getUnregisteredPageData() {
+				const start = (this.unregisteredPageNum - 1) * this.unregisteredPageSize
+				const end = start + this.unregisteredPageSize
+				const pageData = this.unregisteredResources.slice(start, end)
+				// 如果数据不足3条,用null填充到3条
+				while (pageData.length < this.unregisteredPageSize) {
+					pageData.push(null)
+				}
+				return pageData
+			},
+			// 获取资源占位元素的key列表
+			getRegisteredPlaceholderKeys() {
+				return this.getRegisteredPageData.map((item, index) => {
+					return item ? null : 'registered-placeholder-' + index
+				}).filter(key => key !== null)
+			},
+			// 获取线索占位元素的key列表
+			getUnregisteredPlaceholderKeys() {
+				return this.getUnregisteredPageData.map((item, index) => {
+					return item ? null : 'unregistered-placeholder-' + index
+				}).filter(key => key !== null)
+			},
+			// 资源总页数
+			getRegisteredTotalPages() {
+				return Math.ceil(this.registeredTotal / this.registeredPageSize)
+			},
+			// 线索总页数
+			getUnregisteredTotalPages() {
+				return Math.ceil(this.unregisteredTotal / this.unregisteredPageSize)
 			}
 		},
 		onLoad() {
@@ -422,6 +500,18 @@ export default {
 							this.registeredResources = allResources.filter(item => item.isUser === 1)
 							this.unregisteredResources = allResources.filter(item => item.isUser === 0)
 							
+							// 设置总数
+							this.registeredTotal = this.registeredResources.length
+							this.unregisteredTotal = this.unregisteredResources.length
+							
+							// 如果当前页超出范围,重置到第一页
+							if (this.registeredPageNum > Math.ceil(this.registeredTotal / this.registeredPageSize) && this.registeredTotal > 0) {
+								this.registeredPageNum = 1
+							}
+							if (this.unregisteredPageNum > Math.ceil(this.unregisteredTotal / this.unregisteredPageSize) && this.unregisteredTotal > 0) {
+								this.unregisteredPageNum = 1
+							}
+							
 							// 保留resources用于兼容(包含所有资源)
 							this.resources = allResources
 							
@@ -434,9 +524,38 @@ export default {
 			},
 		// 搜索
 		async handleSearch() {
+				// 重置分页到第一页
+				this.registeredPageNum = 1
+				this.unregisteredPageNum = 1
 				// 重新加载资源数据,包含搜索关键词
 				await this.loadMyResources()
 		},
+		// 资源上一页
+		prevRegisteredPage() {
+			if (this.registeredPageNum > 1) {
+				this.registeredPageNum--
+			}
+		},
+		// 资源下一页
+		nextRegisteredPage() {
+			const totalPages = Math.ceil(this.registeredTotal / this.registeredPageSize)
+			if (this.registeredPageNum < totalPages) {
+				this.registeredPageNum++
+			}
+		},
+		// 线索上一页
+		prevUnregisteredPage() {
+			if (this.unregisteredPageNum > 1) {
+				this.unregisteredPageNum--
+			}
+		},
+		// 线索下一页
+		nextUnregisteredPage() {
+			const totalPages = Math.ceil(this.unregisteredTotal / this.unregisteredPageSize)
+			if (this.unregisteredPageNum < totalPages) {
+				this.unregisteredPageNum++
+			}
+		},
 		// 删除资源
 		async handleDelete(id) {
 				// 验证id是否有效
@@ -496,7 +615,9 @@ export default {
 										icon: 'success'
 									})
 									
-									// 删除成功后刷新资源列表
+									// 删除成功后刷新资源列表,并重置分页
+									this.registeredPageNum = 1
+									this.unregisteredPageNum = 1
 									await this.loadMyResources()
 								} else {
 									uni.showToast({
@@ -692,19 +813,21 @@ export default {
 		// 图片加载错误处理(内部方法)
 		handleImageError(index, type) {
 			try {
-				const resourceList = type === 'registered' ? this.registeredResources : this.unregisteredResources
-				const resource = resourceList && resourceList[index]
+				// 获取当前页的数据
+				const pageData = type === 'registered' ? this.getRegisteredPageData : this.getUnregisteredPageData
+				const resource = pageData && pageData[index]
 				if (!resource) {
 					console.warn('图片加载失败:资源不存在,index:', index, 'type:', type)
 					return
 				}
 				
 				const originalUrl = resource.avatar
+				const resourceId = resource.id
 				console.error('❌ 图片加载失败:', {
 					index: index,
 					type: type,
 					resourceName: resource.name,
-					resourceId: resource.id,
+					resourceId: resourceId,
 					originalAvatarUrl: originalUrl,
 					'URL类型': typeof originalUrl,
 					'URL长度': originalUrl ? originalUrl.length : 0,
@@ -719,12 +842,20 @@ export default {
 					!originalUrl.includes('placeholder') && 
 					!originalUrl.includes('default') &&
 					!originalUrl.includes('via.placeholder')) {
-					// 设置为空字符串,让CSS默认背景显示
+					// 根据resourceId在全部数据中找到对应的资源并更新
 					console.log('图片加载失败,将URL设置为空,显示CSS默认背景')
 					if (type === 'registered') {
-						this.$set(this.registeredResources[index], 'avatar', '')
+						const fullList = this.registeredResources
+						const foundIndex = fullList.findIndex(item => item.id === resourceId)
+						if (foundIndex !== -1) {
+							this.$set(this.registeredResources[foundIndex], 'avatar', '')
+						}
 					} else {
-						this.$set(this.unregisteredResources[index], 'avatar', '')
+						const fullList = this.unregisteredResources
+						const foundIndex = fullList.findIndex(item => item.id === resourceId)
+						if (foundIndex !== -1) {
+							this.$set(this.unregisteredResources[foundIndex], 'avatar', '')
+						}
 					}
 				} else {
 					console.log('图片加载失败,但URL已经是占位图或空,无需处理')
@@ -737,13 +868,14 @@ export default {
 		// 图片加载成功处理(内部方法)
 		handleImageLoad(index, type) {
 			try {
-				const resourceList = type === 'registered' ? this.registeredResources : this.unregisteredResources
-				if (resourceList && resourceList[index]) {
+				// 获取当前页的数据
+				const pageData = type === 'registered' ? this.getRegisteredPageData : this.getUnregisteredPageData
+				if (pageData && pageData[index]) {
 					console.log('图片加载成功:', {
 						index: index,
 						type: type,
-						resource: resourceList[index]?.name,
-						avatarUrl: resourceList[index]?.avatar
+						resource: pageData[index]?.name,
+						avatarUrl: pageData[index]?.avatar
 					})
 				}
 			} catch (e) {
@@ -833,12 +965,16 @@ export default {
 	/* 资源分组 */
 	.resource-section {
 		margin-bottom: 30rpx;
+		min-height: calc(50vh - 100rpx); /* 确保至少占半屏高度 */
+		display: flex;
+		flex-direction: column;
 		
 		.section-header {
 			display: flex;
 			align-items: center;
 			padding: 20rpx 0 15rpx;
 			margin-bottom: 10rpx;
+			flex-shrink: 0;
 			
 			.section-title {
 				font-size: 32rpx;
@@ -853,6 +989,70 @@ export default {
 				font-weight: normal;
 			}
 		}
+		
+		/* 资源列表容器,确保有固定高度 */
+		.resource-list-container {
+			flex: 1;
+			display: flex;
+			flex-direction: column;
+			min-height: calc(50vh - 200rpx);
+		}
+	}
+	
+	/* 分页控件 */
+	.pagination {
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		padding: 30rpx 0;
+		gap: 30rpx;
+		margin-top: 20rpx;
+		
+		.pagination-btn {
+			padding: 12rpx 30rpx;
+			background: linear-gradient(135deg, #9C27B0 0%, #BA68C8 100%);
+			color: #FFFFFF;
+			border-radius: 25rpx;
+			font-size: 26rpx;
+			font-weight: 500;
+			text-align: center;
+			box-shadow: 0 2rpx 8rpx rgba(156, 39, 176, 0.3);
+			transition: all 0.3s;
+			
+			&:active {
+				transform: scale(0.95);
+				box-shadow: 0 1rpx 4rpx rgba(156, 39, 176, 0.4);
+			}
+			
+			&.disabled {
+				background: #E0E0E0;
+				color: #999;
+				box-shadow: none;
+				opacity: 0.6;
+				
+				&:active {
+					transform: none;
+				}
+			}
+		}
+		
+		.pagination-info {
+			font-size: 26rpx;
+			color: #666;
+			font-weight: 500;
+		}
+	}
+	
+	/* 占位元素,用于保持固定高度 */
+	.resource-placeholder {
+		/* 使用与resource-item相同的高度和margin来占位 */
+		/* resource-item: padding 25rpx * 2 + margin-bottom 20rpx + 内容高度约250rpx = 约320rpx */
+		height: 320rpx;
+		margin-bottom: 20rpx;
+		/* 不显示内容,但保持空间 */
+		opacity: 0;
+		pointer-events: none;
+		flex-shrink: 0;
 	}
 
 	/* 空状态 */

+ 108 - 9
LiangZhiYUMao/pages/matchmaker-workbench/precise-match.vue

@@ -48,7 +48,7 @@
 						</view>
 					</view>
 				</view>
-				<view class="action-btn" @click="handleChat(client)">牵线聊聊</view>
+				<view class="action-btn" @click="handleChat(index)">牵线聊聊</view>
 			</view>
 		</scroll-view>
 
@@ -212,11 +212,97 @@ export default {
 			})
 		},
 		// 牵线聊聊
-		handleChat(client) {
-			// 跳转到聊天页面或打开聊天弹窗
-			uni.showToast({
-				title: '牵线成功',
-				icon: 'success'
+		handleChat(index) {
+			console.log('=== 牵线聊聊 ===')
+			console.log('传入的index:', index)
+			console.log('matchClients数组长度:', this.matchClients.length)
+			console.log('matchClients数组:', this.matchClients)
+			
+			// 验证 index 是否有效
+			if (index === null || index === undefined || index < 0 || index >= this.matchClients.length) {
+				console.error('❌ index无效:', index)
+				uni.showToast({
+					title: '数据索引错误',
+					icon: 'none'
+				})
+				return
+			}
+			
+			// 从数组中获取 client 对象
+			const client = this.matchClients[index]
+			console.log('从数组获取的client:', client)
+			console.log('client类型:', typeof client)
+			console.log('client是否为null:', client === null)
+			console.log('client是否为undefined:', client === undefined)
+			
+			// 验证 client 对象是否存在
+			if (!client) {
+				console.error('❌ client对象为空')
+				console.log('matchClients数组完整内容:', JSON.stringify(this.matchClients, null, 2))
+				uni.showToast({
+					title: '客户信息为空,请刷新重试',
+					icon: 'none',
+					duration: 3000
+				})
+				return
+			}
+			
+			console.log('client对象的所有键:', Object.keys(client))
+			
+			// 尝试多种方式获取用户ID
+			let targetUserId = null
+			
+			// 方式1: 使用 id 字段
+			if (client.id !== null && client.id !== undefined && client.id !== '') {
+				targetUserId = String(client.id)
+				console.log('✅ 从 client.id 获取用户ID:', targetUserId)
+			}
+			// 方式2: 使用 userId 字段(后端原始字段)
+			else if (client.userId !== null && client.userId !== undefined && client.userId !== '') {
+				targetUserId = String(client.userId)
+				console.log('✅ 从 client.userId 获取用户ID:', targetUserId)
+			}
+			// 方式3: 使用 user_id 字段(下划线格式)
+			else if (client.user_id !== null && client.user_id !== undefined && client.user_id !== '') {
+				targetUserId = String(client.user_id)
+				console.log('✅ 从 client.user_id 获取用户ID:', targetUserId)
+			}
+			
+			// 如果仍然没有获取到用户ID
+			if (!targetUserId || targetUserId === 'null' || targetUserId === 'undefined' || targetUserId === '') {
+				console.error('❌ 无法获取用户ID')
+				console.log('client对象完整内容:', JSON.stringify(client, null, 2))
+				uni.showToast({
+					title: '无法获取用户ID,请刷新重试',
+					icon: 'none',
+					duration: 3000
+				})
+				return
+			}
+			
+			// 获取其他必要信息
+			const targetUserName = client.name || client.nickname || '用户'
+			const targetUserAvatar = client.avatar || client.avatarUrl || '/static/default-avatar.svg'
+			
+			console.log('跳转参数:')
+			console.log('  - targetUserId:', targetUserId)
+			console.log('  - targetUserName:', targetUserName)
+			console.log('  - targetUserAvatar:', targetUserAvatar)
+			
+			// 跳转到聊天页面
+			// 注意:fromMatchmaker=1 表示来自红娘工作台,会跳过消息限制和审核
+			uni.navigateTo({
+				url: `/pages/message/chat?targetUserId=${targetUserId}&targetUserName=${encodeURIComponent(targetUserName)}&targetUserAvatar=${encodeURIComponent(targetUserAvatar)}&fromMatchmaker=1`,
+				success: () => {
+					console.log('✅ 跳转聊天页面成功')
+				},
+				fail: (err) => {
+					console.error('❌ 跳转聊天页面失败:', err)
+					uni.showToast({
+						title: '跳转失败,请重试',
+						icon: 'none'
+					})
+				}
 			})
 		},
 		// 从API加载匹配客户信息
@@ -273,13 +359,26 @@ export default {
 							originalPhoneValue = String(item.original_phone).trim()
 						}
 						
+						// 获取用户ID,支持多种字段名
+						let userId = null
+						if (item.userId !== null && item.userId !== undefined) {
+							userId = item.userId
+						} else if (item.user_id !== null && item.user_id !== undefined) {
+							userId = item.user_id
+						} else if (item.id !== null && item.id !== undefined) {
+							userId = item.id
+						}
+						
 						const mappedItem = {
-							id: item.userId,
-							name: item.nickname || '未知',
+							id: userId,  // 使用获取到的userId
+							userId: userId,  // 同时保留原始字段,方便容错
+							name: item.nickname || item.name || '未知',
+							nickname: item.nickname || item.name,  // 保留原始字段
 							gender: item.gender === 1 ? '男' : item.gender === 2 ? '女' : '未知',
 							status: '未匹配',
 							tags: item.tags || [],
-							avatar: item.avatarUrl || 'https://via.placeholder.com/150',
+							avatar: item.avatarUrl || item.avatar || 'https://via.placeholder.com/150',
+							avatarUrl: item.avatarUrl || item.avatar,  // 保留原始字段
 							requirement: item.requirement || '暂无要求',
 							contact: (item.phone && typeof item.phone === 'string') ? item.phone : (item.phone ? String(item.phone) : ''),
 							originalPhone: originalPhoneValue,

+ 714 - 83
LiangZhiYUMao/pages/matchmaker-workbench/quality-resources.vue

@@ -11,39 +11,64 @@
 		<view class="search-bar">
 			<view class="search-input-wrapper">
 				<view class="search-icon-small"></view>
-				<input type="text" class="search-input" placeholder="请输入搜索关键词" placeholder-style="color: #999;" />
+				<input type="text" class="search-input" placeholder="请输入搜索关键词" placeholder-style="color: #999;" v-model="searchKeyword" @input="handleSearch" />
 			</view>
 		</view>
 
 		<!-- 资源列表 -->
-		<scroll-view scroll-y class="content">
-			<view class="resource-item" v-for="(resource, index) in resources" :key="index">
+		<scroll-view scroll-y class="content" @scrolltolower="loadMore" :scroll-with-animation="true">
+			<view v-if="loading" class="loading-wrapper">
+				<text class="loading-text">加载中...</text>
+			</view>
+			<view v-else-if="resources.length === 0" class="empty-wrapper">
+				<text class="empty-text">暂无优质资源</text>
+			</view>
+			<view class="resource-item" v-for="(resource, index) in resources" :key="getResourceKey(resource, index)" @click="handleResourceClick(index)">
 				<view class="resource-header">
-					<view class="avatar"></view>
+					<image 
+						:src="getAvatarUrl(resource) || '/static/default-avatar.svg'" 
+						mode="aspectFill" 
+						class="avatar"
+						@error="handleImageError(index)"
+					></image>
 					<view class="resource-info">
 						<view class="name-gender">
 							<text class="name">{{ resource.name }}</text>
-							<text class="gender">{{ resource.gender }}</text>
-							<text class="status">{{ resource.status }}</text>
+							<text class="gender">{{ resource.gender === 1 ? '男' : '女' }}</text>
 						</view>
-						<view class="tags">
-							<view class="tag" v-for="(tag, tagIndex) in resource.tags" :key="tagIndex">{{ tag }}</view>
+						<view class="status-tag-wrapper">
+							<text class="status-tag register-tag registered">已注册</text>
+							<text class="status-tag match-tag" :class="{ 'matched': getIsMatch(resource) === 1, 'unmatched': getIsMatch(resource) === 0 || !getIsMatch(resource) }">
+								{{ getIsMatch(resource) === 1 ? '已匹配' : '未匹配' }}
+							</text>
 						</view>
 					</view>
 				</view>
 				<view class="resource-details">
-					<view class="requirement">
+					<view class="labels" v-if="resource.tags && resource.tags.length > 0">
+						<text class="label" v-for="(tag, tagIndex) in resource.tags" :key="tagIndex">{{ tag }}</text>
+					</view>
+					<view class="requirement-box">
 						<text class="requirement-label">择偶要求:</text>
-						<text class="requirement-content">{{ resource.requirement }}</text>
+						<text class="requirement-content">{{ getMateSelectionCriteria(resource) || '暂无' }}</text>
 					</view>
-					<view class="contact-info">
+					<view class="contact-box">
 						<text class="contact-label">联系方式:</text>
-						<text class="contact-number">{{ resource.contact }}</text>
-						<view class="copy-btn" @click="handleCopy(resource.contact)">复制</view>
+						<text class="contact-number">{{ getMaskedPhone(resource.phone) || '暂无' }}</text>
+						<text class="copy-btn" @click.stop="handleCopy(resource.phone)">复制</text>
+					</view>
+					<view class="action-buttons">
+						<view class="action-btn add-resource-btn" @click.stop="handleAddToMyResources(index)">添加到我的资源</view>
+						<view class="action-btn chat-btn" @click.stop="handleChat(resource)">牵线聊聊</view>
 					</view>
-					<view class="action-btn" @click="handleChat(resource)">牵线聊聊</view>
 				</view>
 			</view>
+			<view v-if="hasMore && !loading" class="load-more-wrapper">
+				<text class="load-more-text">上拉加载更多</text>
+			</view>
+			<view v-if="!hasMore && resources.length > 0" class="no-more-wrapper">
+				<text class="no-more-text">没有更多数据了</text>
+			</view>
 		</scroll-view>
 	</view>
 </template>
@@ -53,34 +78,27 @@ export default {
 	name: 'quality-resources',
 	data() {
 		return {
-			resources: [
-				{
-					name: '小高',
-					gender: '男',
-					status: '未匹配',
-					tags: ['气质男', '小清新'],
-					requirement: '165+ 本科',
-					contact: '123****8912'
-				},
-				{
-					name: '小子',
-					gender: '男',
-					status: '未匹配',
-					tags: ['周末有空'],
-					requirement: '165+ 本科',
-					contact: '123****8912'
-				},
-				{
-					name: '小博',
-					gender: '男',
-					status: '未匹配',
-					tags: ['电话联系', '优质男'],
-					requirement: '180+ 本科',
-					contact: '123****8912'
-				}
-			]
+			resources: [],
+			searchKeyword: '',
+			pageNum: 1,
+			pageSize: 10,
+			loading: false,
+			hasMore: true,
+			currentUserId: null // 当前登录用户的用户ID
 		}
 	},
+	onLoad() {
+		// 获取当前登录用户的ID
+		const userId = uni.getStorageSync('userId')
+		if (userId) {
+			const rawUserId = parseInt(userId)
+			if (!isNaN(rawUserId) && rawUserId > 0) {
+				this.currentUserId = rawUserId
+				console.log('当前登录用户ID:', this.currentUserId)
+			}
+		}
+		this.loadResources()
+	},
 	methods: {
 		// 返回上一页
 		handleBack() {
@@ -90,8 +108,125 @@ export default {
 		handleFilter() {
 			// 实现筛选功能
 		},
+		// 搜索
+		handleSearch() {
+			this.pageNum = 1
+			this.resources = []
+			this.hasMore = true
+			this.loadResources()
+		},
+		// 加载资源列表
+		async loadResources() {
+			if (this.loading) return
+			
+			try {
+				this.loading = true
+				const baseUrl = process.env.NODE_ENV === 'development' 
+					? 'http://localhost:8083/api'  // 开发环境 - 通过网关
+					: 'https://your-domain.com/api'  // 生产环境
+				
+				// 构建请求参数
+				const requestData = {
+					tagName: '优质资源',
+					keyword: this.searchKeyword || '',
+					pageNum: this.pageNum,
+					pageSize: this.pageSize
+				}
+				
+				// 如果已登录,传递currentUserId,用于排除该红娘已拥有的资源
+				if (this.currentUserId) {
+					requestData.currentUserId = this.currentUserId
+				}
+				
+				const [error, res] = await uni.request({
+					url: `${baseUrl}/my-resource/list-by-tag`,
+					method: 'GET',
+					data: requestData,
+					timeout: 10000
+				})
+				
+				if (error) {
+					console.error('加载优质资源失败:', error)
+					uni.showToast({
+						title: '加载失败',
+						icon: 'none'
+					})
+					return
+				}
+				
+				if (res.statusCode === 200 && res.data && res.data.code === 200) {
+					const pageData = res.data.data
+					if (pageData && pageData.records) {
+						// 调试:打印第一条数据的详细信息
+						if (pageData.records.length > 0) {
+							const firstRecord = pageData.records[0]
+							console.log('=== 第一条资源数据 ===')
+							console.log('完整数据:', JSON.stringify(firstRecord, null, 2))
+							console.log('isUser值:', firstRecord.isUser, '类型:', typeof firstRecord.isUser)
+							console.log('is_user值:', firstRecord.is_user, '类型:', typeof firstRecord.is_user)
+							console.log('userId值:', firstRecord.userId, '类型:', typeof firstRecord.userId)
+							console.log('user_id值:', firstRecord.user_id, '类型:', typeof firstRecord.user_id)
+							console.log('mateSelectionCriteria (驼峰):', firstRecord.mateSelectionCriteria)
+							console.log('mate_selection_criteria (下划线):', firstRecord.mate_selection_criteria)
+							console.log('phone:', firstRecord.phone)
+						}
+						
+						if (this.pageNum === 1) {
+							this.resources = pageData.records
+						} else {
+							this.resources = this.resources.concat(pageData.records)
+						}
+						
+						// 判断是否还有更多数据
+						this.hasMore = pageData.records && pageData.records.length >= this.pageSize
+						
+						console.log('加载优质资源成功,数量:', this.resources.length)
+					} else {
+						console.error('加载优质资源失败:', res.data.message)
+					}
+				} else {
+					console.error('加载优质资源失败:', res.data.message)
+					uni.showToast({
+						title: res.data.message || '加载失败',
+						icon: 'none'
+					})
+				}
+			} catch (e) {
+				console.error('加载优质资源异常:', e)
+				uni.showToast({
+					title: '加载异常',
+					icon: 'none'
+				})
+			} finally {
+				this.loading = false
+			}
+		},
+		// 加载更多
+		loadMore() {
+			if (this.hasMore && !this.loading) {
+				this.pageNum++
+				this.loadResources()
+			}
+		},
+		// 图片加载错误处理
+		handleImageError(index) {
+			if (this.resources[index]) {
+				// 设置头像为空,让CSS默认背景显示
+				// 支持下划线和驼峰两种格式
+				this.resources[index].avatarUrl = ''
+				this.resources[index].avatar_url = ''
+				this.resources[index].avatar = ''
+			}
+		},
 		// 复制联系方式
 		handleCopy(contact) {
+			if (!contact) {
+				uni.showToast({
+					title: '联系方式为空',
+					icon: 'none'
+				})
+				return
+			}
 			uni.setClipboardData({
 				data: contact,
 				success: () => {
@@ -104,11 +239,424 @@ export default {
 		},
 		// 牵线聊聊
 		handleChat(resource) {
-			// 跳转到聊天页面或打开聊天弹窗
-			uni.showToast({
-				title: '牵线成功',
-				icon: 'success'
+			console.log('=== 牵线聊聊 ===')
+			console.log('资源信息:', resource)
+			console.log('资源信息类型:', typeof resource)
+			console.log('资源信息的所有键:', resource ? Object.keys(resource) : 'resource为空')
+			
+			// 验证 resource 对象是否存在
+			if (!resource) {
+				console.error('❌ resource对象为空')
+				uni.showToast({
+					title: '资源信息为空',
+					icon: 'none'
+				})
+				return
+			}
+			
+			// 检查用户是否已注册(只有已注册用户才能聊天)
+			// 支持下划线和驼峰两种格式
+			const isUser = resource.isUser !== undefined ? resource.isUser : 
+			              (resource.is_user !== undefined ? resource.is_user : 0)
+			if (isUser !== 1 && isUser !== '1') {
+				console.warn('⚠️ 用户未注册,无法聊天')
+				uni.showToast({
+					title: '该用户尚未注册,无法聊天',
+					icon: 'none',
+					duration: 3000
+				})
+				return
+			}
+			
+			// 尝试多种方式获取用户ID
+			let targetUserId = null
+			
+			// 方式1: 使用 userId 字段(驼峰格式)
+			if (resource.userId !== null && resource.userId !== undefined && resource.userId !== '') {
+				targetUserId = String(resource.userId)
+				console.log('✅ 从 resource.userId 获取用户ID:', targetUserId)
+			}
+			// 方式2: 使用 user_id 字段(下划线格式)
+			else if (resource.user_id !== null && resource.user_id !== undefined && resource.user_id !== '') {
+				targetUserId = String(resource.user_id)
+				console.log('✅ 从 resource.user_id 获取用户ID:', targetUserId)
+			}
+			// 方式3: 使用 id 字段
+			else if (resource.id !== null && resource.id !== undefined && resource.id !== '') {
+				targetUserId = String(resource.id)
+				console.log('✅ 从 resource.id 获取用户ID:', targetUserId)
+			}
+			
+			// 如果仍然没有获取到用户ID
+			if (!targetUserId || targetUserId === 'null' || targetUserId === 'undefined' || targetUserId === '') {
+				console.error('❌ 无法获取用户ID')
+				console.log('resource对象完整内容:', JSON.stringify(resource, null, 2))
+				uni.showToast({
+					title: '无法获取用户ID,请刷新重试',
+					icon: 'none',
+					duration: 3000
+				})
+				return
+			}
+			
+			// 获取其他必要信息(支持下划线和驼峰格式)
+			const targetUserName = resource.name || '用户'
+			const targetUserAvatar = resource.avatarUrl || resource.avatar_url || resource.avatar || '/static/default-avatar.svg'
+			
+			console.log('跳转参数:')
+			console.log('  - targetUserId:', targetUserId)
+			console.log('  - targetUserName:', targetUserName)
+			console.log('  - targetUserAvatar:', targetUserAvatar)
+			
+			// 跳转到聊天页面
+			// 注意:fromMatchmaker=1 表示来自红娘工作台,会跳过消息限制和审核
+			uni.navigateTo({
+				url: `/pages/message/chat?targetUserId=${targetUserId}&targetUserName=${encodeURIComponent(targetUserName)}&targetUserAvatar=${encodeURIComponent(targetUserAvatar)}&fromMatchmaker=1`,
+				success: () => {
+					console.log('✅ 跳转聊天页面成功')
+				},
+				fail: (err) => {
+					console.error('❌ 跳转聊天页面失败:', err)
+					uni.showToast({
+						title: '跳转失败,请重试',
+						icon: 'none'
+					})
+				}
 			})
+		},
+		// 获取资源项的key(用于v-for)
+		getResourceKey(resource, index) {
+			if (!resource) return `resource-${index}`
+			// 支持下划线和驼峰两种格式
+			const resourceId = resource.resourceId || resource.resource_id
+			if (resourceId !== null && resourceId !== undefined && resourceId !== '') {
+				return `resource-${resourceId}`
+			}
+			return `resource-${index}`
+		},
+		// 点击资源项
+		handleResourceClick(index) {
+			console.log('=== 点击资源项 ===')
+			console.log('传入的index:', index)
+			console.log('resources数组长度:', this.resources.length)
+			
+			// 检查index是否有效
+			if (index === undefined || index === null || index < 0 || index >= this.resources.length) {
+				console.error('❌ index无效:', index)
+				uni.showToast({
+					title: '资源索引无效',
+					icon: 'none'
+				})
+				return
+			}
+			
+			// 从数组中获取resource对象
+			const resource = this.resources[index]
+			console.log('从数组获取的resource对象:', JSON.stringify(resource, null, 2))
+			
+			if (!resource) {
+				console.error('❌ resource对象为空')
+				uni.showToast({
+					title: '资源信息为空',
+					icon: 'none'
+				})
+				return
+			}
+			
+			// 支持下划线和驼峰两种格式
+			const resourceId = resource.resourceId || resource.resource_id
+			const isUser = resource.isUser !== undefined ? resource.isUser : 
+			              (resource.is_user !== undefined ? resource.is_user : 0)
+			
+			console.log('resource.resourceId:', resourceId)
+			console.log('resource.isUser:', isUser)
+			
+			// 检查resourceId是否有效
+			if (!resourceId || resourceId === null || resourceId === undefined || resourceId === '') {
+				console.error('❌ resourceId无效:', resourceId)
+				uni.showToast({
+					title: '资源ID无效,无法查看详情',
+					icon: 'none'
+				})
+				return
+			}
+			
+			// 判断是否为已注册用户
+			if (isUser === 1 || isUser === '1') {
+				// 已注册,跳转到客户详情页面,添加fromQualityResources参数标识来源
+				console.log('准备跳转,resourceId:', resourceId)
+				uni.navigateTo({
+					url: `/pages/matchmaker-workbench/client-detail?resourceId=${resourceId}&fromQualityResources=1`,
+					success: () => {
+						console.log('✅ 跳转成功,resourceId:', resourceId)
+					},
+					fail: (err) => {
+						console.error('❌ 跳转失败:', err)
+						uni.showToast({
+							title: '跳转失败,请重试',
+							icon: 'none'
+						})
+					}
+				})
+			} else {
+				// 未注册,提示用户(虽然后端已经过滤了,但为了容错仍然保留)
+				uni.showToast({
+					title: '该用户还未注册用户端',
+					icon: 'none',
+					duration: 2000
+				})
+			}
+		},
+		// 添加到我的资源
+		async handleAddToMyResources(index) {
+			console.log('=== 添加到我的资源 ===')
+			console.log('index:', index)
+			
+			// 检查index是否有效
+			if (index === undefined || index === null || index < 0 || index >= this.resources.length) {
+				console.error('❌ index无效:', index)
+				uni.showToast({
+					title: '资源索引无效',
+					icon: 'none'
+				})
+				return
+			}
+			
+			// 从数组中获取resource对象
+			const resource = this.resources[index]
+			console.log('从数组获取的resource对象:', JSON.stringify(resource, null, 2))
+			
+			if (!resource) {
+				console.error('❌ resource对象为空')
+				uni.showToast({
+					title: '资源信息为空',
+					icon: 'none'
+				})
+				return
+			}
+			
+			// 检查是否已登录
+			if (!this.currentUserId) {
+				uni.showToast({
+					title: '请先登录',
+					icon: 'none'
+				})
+				return
+			}
+			
+			try {
+				uni.showLoading({
+					title: '添加中...'
+				})
+				
+				const baseUrl = process.env.NODE_ENV === 'development' 
+					? 'http://localhost:8083/api'  // 开发环境 - 通过网关
+					: 'https://your-domain.com/api'  // 生产环境
+				
+				// 构建要添加的资源数据(复制原资源的所有信息,但使用当前红娘的matchmaker_id)
+				const resourceData = {
+					name: resource.name,
+					age: resource.age,
+					gender: resource.gender,
+					constellation: resource.constellation,
+					height: resource.height,
+					weight: resource.weight,
+					marrStatus: resource.marrStatus || resource.marr_status,
+					diploma: resource.diploma,
+					income: resource.income,
+					address: resource.address,
+					domicile: resource.domicile,
+					occupation: resource.occupation,
+					house: resource.house,
+					phone: resource.phone,
+					backupPhone: resource.backupPhone || resource.backup_phone,
+					car: resource.car,
+					mateSelectionCriteria: resource.mateSelectionCriteria || resource.mate_selection_criteria,
+					isUser: resource.isUser || resource.is_user || 1,
+					userId: resource.userId || resource.user_id
+				}
+				
+				// 获取标签ID列表
+				let tagIds = []
+				
+				// 方法1: 如果resource有resourceId,直接根据resourceId获取标签ID
+				const resourceId = resource.resourceId || resource.resource_id
+				if (resourceId) {
+					try {
+						const [tagError, tagRes] = await uni.request({
+							url: `${baseUrl}/my-resource/tag-ids/${resourceId}`,
+							method: 'GET'
+						})
+						
+						if (!tagError && tagRes.statusCode === 200 && tagRes.data && tagRes.data.code === 200) {
+							tagIds = tagRes.data.data || []
+							console.log('根据resourceId获取的标签ID:', tagIds)
+						}
+					} catch (e) {
+						console.warn('根据resourceId获取标签ID失败,将使用标签名称查询:', e)
+					}
+				}
+				
+				// 方法2: 如果方法1失败或没有resourceId,根据标签名称查询标签ID
+				if (tagIds.length === 0 && resource.tags && resource.tags.length > 0) {
+					try {
+						// 获取所有标签列表
+						const [tagListError, tagListRes] = await uni.request({
+							url: `${baseUrl}/tag/list`,
+							method: 'GET'
+						})
+						
+						if (!tagListError && tagListRes.statusCode === 200 && tagListRes.data && tagListRes.data.code === 200) {
+							const allTags = tagListRes.data.data || []
+							console.log('所有标签列表:', allTags)
+							
+							// 根据标签名称匹配标签ID
+							for (const tagName of resource.tags) {
+								const matchedTag = allTags.find(tag => {
+									const tagNameField = tag.name || tag.tag_name || tag.tagName
+									return tagNameField === tagName
+								})
+								
+								if (matchedTag) {
+									const tagId = matchedTag.id || matchedTag.tag_id
+									if (tagId && !tagIds.includes(tagId)) {
+										tagIds.push(tagId)
+										console.log(`找到标签 "${tagName}" 的ID:`, tagId)
+									}
+								} else {
+									console.warn(`未找到标签 "${tagName}" 的ID`)
+								}
+							}
+							
+							console.log('根据标签名称查询到的标签ID列表:', tagIds)
+						}
+					} catch (e) {
+						console.error('根据标签名称查询标签ID异常:', e)
+					}
+				}
+				
+				// 确保tagIds是整数数组
+				tagIds = tagIds.map(id => parseInt(id)).filter(id => !isNaN(id) && id > 0)
+				
+				if (tagIds.length === 0) {
+					console.warn('⚠️ 未获取到任何标签ID,资源将不包含标签')
+				} else {
+					console.log('✅ 最终获取到的标签ID列表:', tagIds)
+				}
+				
+				// 调用后端API添加资源
+				const url = `${baseUrl}/my-resource/add?currentUserId=${this.currentUserId}`
+				console.log('添加资源请求URL:', url)
+				console.log('添加资源数据:', JSON.stringify(resourceData, null, 2))
+				console.log('标签ID列表:', tagIds)
+				
+				const [error, res] = await uni.request({
+					url: url,
+					method: 'POST',
+					data: {
+						...resourceData,
+						tagIds: tagIds
+					},
+					header: {
+						'Content-Type': 'application/json'
+					}
+				})
+				
+				uni.hideLoading()
+				
+				if (error) {
+					console.error('添加到我的资源失败:', error)
+					uni.showToast({
+						title: '添加失败,请重试',
+						icon: 'none'
+					})
+					return
+				}
+				
+				if (res.statusCode === 200 && res.data && res.data.code === 200) {
+					uni.showToast({
+						title: '添加成功',
+						icon: 'success'
+					})
+					
+					// 发送刷新事件,通知我的资源页面刷新列表
+					uni.$emit('refreshResourceList')
+				} else {
+					console.error('添加到我的资源失败:', res.data.message)
+					uni.showToast({
+						title: res.data.message || '添加失败',
+						icon: 'none',
+						duration: 2000
+					})
+				}
+			} catch (e) {
+				uni.hideLoading()
+				console.error('添加到我的资源异常:', e)
+				uni.showToast({
+					title: '添加异常,请稍后重试',
+					icon: 'none'
+				})
+			}
+		},
+		// 获取头像URL(支持驼峰和下划线格式)
+		getAvatarUrl(resource) {
+			if (!resource) return ''
+			// 优先使用 avatarUrl(驼峰),如果没有则使用 avatar_url(下划线),最后使用 avatar
+			return resource.avatarUrl || resource.avatar_url || resource.avatar || ''
+		},
+		// 获取匹配状态(支持驼峰和下划线格式)
+		getIsMatch(resource) {
+			if (!resource) return 0
+			// 支持 isMatch(驼峰)和 is_match(下划线)两种格式
+			return resource.isMatch !== undefined ? resource.isMatch : 
+			       (resource.is_match !== undefined ? resource.is_match : 0)
+		},
+		// 获取择偶要求(支持驼峰和下划线格式)
+		getMateSelectionCriteria(resource) {
+			if (!resource) {
+				console.warn('getMateSelectionCriteria: resource为空')
+				return ''
+			}
+			
+			// 支持 mateSelectionCriteria(驼峰)和 mate_selection_criteria(下划线)两种格式
+			let criteria = resource.mateSelectionCriteria || resource.mate_selection_criteria || ''
+			
+			// 如果为空,直接返回
+			if (!criteria || criteria.trim() === '') {
+				return ''
+			}
+			
+			// 去除首尾空格
+			criteria = criteria.trim()
+			
+			// 如果获取到的是电话号码格式(11位数字或脱敏后的格式),则返回空字符串,让模板显示"暂无"
+			// 检查是否是纯数字(11位)或包含****的脱敏格式
+			const phonePattern = /^(\d{3}\*{4}\d{4}|\d{11})$/
+			if (phonePattern.test(criteria)) {
+				console.warn('⚠️ 择偶要求字段包含电话号码格式,返回空字符串', {
+					criteria: criteria,
+					phone: resource.phone,
+					'resource完整对象': resource
+				})
+				return ''
+			}
+			
+			// 如果择偶要求等于电话号码(未脱敏),也返回空
+			if (criteria === resource.phone || criteria === (resource.phone || '').replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')) {
+				console.warn('⚠️ 择偶要求等于电话号码,返回空字符串', {
+					criteria: criteria,
+					phone: resource.phone
+				})
+				return ''
+			}
+			
+			return criteria
+		},
+		// 获取脱敏手机号
+		getMaskedPhone(phone) {
+			if (!phone) return ''
+			// 手机号脱敏:显示前3位和后4位,中间用****代替
+			return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
 		}
 	}
 }
@@ -215,6 +763,7 @@ export default {
 		border-radius: 50%;
 		background: #F0F0F0;
 		margin-right: 20rpx;
+		flex-shrink: 0;
 	}
 
 	.resource-info {
@@ -239,87 +788,169 @@ export default {
 				background: #F5F5F5;
 				border-radius: 12rpx;
 			}
-
-			.status {
-				font-size: 24rpx;
-				color: #FF6B8A;
-				padding: 4rpx 12rpx;
-				background: #FFF3F5;
-				border-radius: 12rpx;
-			}
 		}
 
-		.tags {
-		display: flex;
-		flex-wrap: wrap;
-		gap: 10rpx;
+		.status-tag-wrapper {
+			display: flex;
+			align-items: center;
+			gap: 10rpx;
+			margin-bottom: 10rpx;
 
-			.tag {
+			.status-tag {
 				font-size: 22rpx;
-				color: #9C27B0;
 				padding: 6rpx 14rpx;
-				background: #F3E5F5;
-				border-radius: 14rpx;
+				border-radius: 12rpx;
+				font-weight: 500;
+
+				&.register-tag {
+					&.registered {
+						color: #4CAF50;
+						background: #E8F5E9;
+						border: 1rpx solid #C8E6C9;
+					}
+				}
+
+				&.match-tag {
+					&.matched {
+						color: #2196F3;
+						background: #E3F2FD;
+						border: 1rpx solid #BBDEFB;
+					}
+
+					&.unmatched {
+						color: #FF9800;
+						background: #FFF3E0;
+						border: 1rpx solid #FFE0B2;
+					}
+				}
 			}
 		}
 	}
 }
 
 .resource-details {
-	
-	.requirement {
+	.labels {
 		display: flex;
+		flex-wrap: wrap;
+		gap: 10rpx;
 		margin-bottom: 15rpx;
 
+		.label {
+			display: inline-block;
+			padding: 6rpx 14rpx;
+			background: #F3E5F5;
+			color: #9C27B0;
+			border-radius: 15rpx;
+			font-size: 22rpx;
+			font-weight: 500;
+		}
+	}
+
+	.requirement-box {
+		background: #F3E5F5;
+		border-radius: 12rpx;
+		padding: 16rpx 20rpx;
+		margin-bottom: 15rpx;
+		display: flex;
+		align-items: center;
+		flex-wrap: wrap;
+
 		.requirement-label {
-			font-size: 26rpx;
-			color: #666;
-			margin-right: 10rpx;
+			font-size: 28rpx;
+			color: #7B1FA2;
+			font-weight: 500;
+			margin-right: 8rpx;
+			white-space: nowrap;
 		}
 
 		.requirement-content {
-			font-size: 26rpx;
-			color: #333;
+			font-size: 28rpx;
+			color: #7B1FA2;
+			font-weight: 400;
 		}
 	}
 
-	.contact-info {
+	.contact-box {
+		margin-bottom: 20rpx;
 		display: flex;
 		align-items: center;
-		margin-bottom: 20rpx;
+		flex-wrap: wrap;
 
 		.contact-label {
 			font-size: 26rpx;
-			color: #666;
+			color: #333;
+			font-weight: 500;
 			margin-right: 10rpx;
+			white-space: nowrap;
 		}
 
 		.contact-number {
+			flex: 1;
 			font-size: 26rpx;
 			color: #333;
-			margin-right: 15rpx;
+			font-weight: 400;
+			min-width: 0;
 		}
 
 		.copy-btn {
 			font-size: 24rpx;
 			color: #9C27B0;
-			padding: 6rpx 16rpx;
-			background: #F3E5F5;
-			border-radius: 14rpx;
+			font-weight: 500;
+			margin-left: 15rpx;
+			white-space: nowrap;
+			cursor: pointer;
 		}
 	}
 
-	.action-btn {
-		width: 200rpx;
-		height: 60rpx;
+	.action-buttons {
 		display: flex;
+		justify-content: flex-end;
 		align-items: center;
-		justify-content: center;
-		background: #9C27B0;
-		color: #FFFFFF;
-		font-size: 28rpx;
-		border-radius: 30rpx;
-		margin-left: auto;
+		gap: 15rpx;
+
+		.action-btn {
+			padding: 14rpx 30rpx;
+			border-radius: 25rpx;
+			font-size: 26rpx;
+			font-weight: 500;
+			text-align: center;
+			
+			&.add-resource-btn {
+				background: #FF9800;
+				color: #FFFFFF;
+			}
+			
+			&.chat-btn {
+				background: linear-gradient(135deg, #9C27B0 0%, #BA68C8 100%);
+				color: #FFFFFF;
+			}
+		}
+	}
+}
+
+.loading-wrapper,
+.empty-wrapper,
+.load-more-wrapper,
+.no-more-wrapper {
+	display: flex;
+	justify-content: center;
+	align-items: center;
+	padding: 40rpx 0;
+	
+	.loading-text,
+	.empty-text,
+	.load-more-text,
+	.no-more-text {
+		font-size: 26rpx;
+		color: #999;
 	}
 }
+
+.empty-text {
+	color: #666;
+}
+
+.no-more-text {
+	color: #999;
+}
 </style>

+ 238 - 16
LiangZhiYUMao/pages/matchmaker-workbench/resource-input.vue

@@ -134,17 +134,33 @@
 					<view class="section-title">客户标签</view>
 					<view class="form-item">
 						<text class="form-label">选择标签</text>
+						<!-- 分类按钮 -->
+						<view class="category-buttons">
+							<view 
+								class="category-btn" 
+								:class="{ 'active': selectedCategoryId === category.id }"
+								v-for="category in categoryList" 
+								:key="category.id"
+								@click="selectCategory(category.id)"
+							>
+								<text class="category-btn-text">{{ category.name }}</text>
+							</view>
+						</view>
+						<!-- 标签列表 -->
 						<view class="tags-container">
 							<view 
 								class="tag-item" 
 								:class="{ 'selected': selectedTagIds.includes(tag.id) }"
-								v-for="tag in tagList" 
-								:key="tag.id"
-								@click="toggleTag(tag.id)"
+								v-for="tag in filteredTagList" 
+								:key="tag.id || tag.tag_id"
+								@click="toggleTag(tag.id || tag.tag_id)"
 							>
-								<text class="tag-text">{{ tag.name }}</text>
+								<text class="tag-text">{{ tag.name || tag.tag_name }}</text>
+							</view>
+							<view v-if="filteredTagList.length === 0 && !isLoadingTags" class="tag-empty">
+								<text>暂无标签</text>
 							</view>
-							<view v-if="tagList.length === 0" class="tag-loading">
+							<view v-if="isLoadingTags" class="tag-loading">
 								<text>加载中...</text>
 							</view>
 						</view>
@@ -212,8 +228,47 @@ export default {
 			houseIndex: 0,
 			carOptions: ['无', '有'],
 			carIndex: 0,
-			tagList: [], // 标签列表
-			selectedTagIds: [] // 选中的标签ID列表
+			tagList: [], // 所有标签列表
+			categoryList: [], // 标签分类列表
+			selectedCategoryId: 1, // 当前选中的分类ID,默认显示职业分类(category_id = 1)
+			selectedTagIds: [], // 选中的标签ID列表
+			isLoadingTags: false // 是否正在加载标签
+		}
+	},
+	computed: {
+		// 根据选中的分类筛选标签(只显示当前分类的标签,不显示全部)
+		filteredTagList() {
+			// 如果没有选中分类,返回空数组(不显示任何标签)
+			if (this.selectedCategoryId === null || this.selectedCategoryId === undefined) {
+				return []
+			}
+			
+			// 确保selectedCategoryId是数字类型
+			const selectedId = parseInt(this.selectedCategoryId)
+			if (isNaN(selectedId)) {
+				console.warn('selectedCategoryId不是有效数字:', this.selectedCategoryId)
+				return []
+			}
+			
+			// 筛选标签,只显示当前分类的标签
+			const filtered = this.tagList.filter(tag => {
+				// 获取标签的分类ID,优先使用 category_id(后端返回的字段名)
+				const tagCategoryId = tag.category_id !== null && tag.category_id !== undefined 
+					? parseInt(tag.category_id) 
+					: (tag.categoryId !== null && tag.categoryId !== undefined 
+						? parseInt(tag.categoryId) 
+						: null)
+				
+				// 如果tagCategoryId是NaN或null,说明数据有问题或未分类,不显示
+				if (isNaN(tagCategoryId) || tagCategoryId === null) {
+					return false
+				}
+				
+				// 只返回匹配当前分类的标签
+				return tagCategoryId === selectedId
+			})
+			
+			return filtered
 		}
 	},
 	onLoad() {
@@ -232,16 +287,69 @@ export default {
 		this.currentUserId = rawUserId
 		console.log('当前登录用户ID:', this.currentUserId)
 		
-		// 加载标签列表
+		// 加载标签分类和标签列表
+		this.loadCategories()
 		this.loadTags()
 	},
 	methods: {
 		goBack() {
 			uni.navigateBack()
 		},
+		// 加载标签分类列表
+		async loadCategories() {
+			try {
+				const baseUrl = process.env.NODE_ENV === 'development' 
+					? 'http://localhost:8083/api'  // 开发环境 - 通过网关
+					: 'https://your-domain.com/api'  // 生产环境
+				
+				const [error, res] = await uni.request({
+					url: `${baseUrl}/tag/categories`,
+					method: 'GET',
+					timeout: 10000
+				})
+				
+				if (error) {
+					console.error('加载标签分类失败:', error)
+					return
+				}
+				
+				if (res.statusCode === 200 && res.data && res.data.code === 200) {
+					// 将后端返回的下划线字段转换为驼峰命名
+					this.categoryList = (res.data.data || []).map(category => ({
+						id: category.id || category.category_id,
+						name: category.name || category.category_name,
+						code: category.code || category.category_code,
+						sortOrder: category.sortOrder || category.sort_order,
+						status: category.status
+					}))
+					console.log('标签分类列表加载成功:', this.categoryList)
+					
+					// 如果还没有选中分类,默认选中职业分类(category_id = 1)
+					if (this.selectedCategoryId === null || this.selectedCategoryId === undefined) {
+						// 查找职业分类(code为'occupation'或id为1)
+						const occupationCategory = this.categoryList.find(cat => 
+							(cat.code === 'occupation' || cat.id === 1)
+						)
+						if (occupationCategory) {
+							this.selectedCategoryId = occupationCategory.id
+							console.log('默认选中职业分类,ID:', this.selectedCategoryId)
+						} else if (this.categoryList.length > 0) {
+							// 如果找不到职业分类,选中第一个分类
+							this.selectedCategoryId = this.categoryList[0].id
+							console.log('默认选中第一个分类,ID:', this.selectedCategoryId)
+						}
+					}
+				} else {
+					console.error('加载标签分类失败:', res.data.message)
+				}
+			} catch (e) {
+				console.error('加载标签分类异常:', e)
+			}
+		},
 		// 加载标签列表
 		async loadTags() {
 			try {
+				this.isLoadingTags = true
 				const baseUrl = process.env.NODE_ENV === 'development' 
 					? 'http://localhost:8083/api'  // 开发环境 - 通过网关
 					: 'https://your-domain.com/api'  // 生产环境
@@ -258,15 +366,55 @@ export default {
 				}
 				
 				if (res.statusCode === 200 && res.data && res.data.code === 200) {
-					this.tagList = res.data.data || []
-					console.log('标签列表加载成功:', this.tagList)
+					// 直接使用后端返回的数据,不进行映射(因为Vue的响应式系统会保留原始属性)
+					// 只保存有分类的标签(category_id不为null)
+					this.tagList = (res.data.data || []).filter(tag => {
+						const categoryId = tag.category_id !== null && tag.category_id !== undefined 
+							? tag.category_id 
+							: (tag.categoryId !== null && tag.categoryId !== undefined ? tag.categoryId : null)
+						return categoryId !== null && categoryId !== undefined
+					})
+					console.log('标签列表加载成功,数量:', this.tagList.length)
+					if (this.tagList.length > 0) {
+						console.log('第一条标签数据:', this.tagList[0])
+						console.log('第一条标签的category_id:', this.tagList[0].category_id, '类型:', typeof this.tagList[0].category_id)
+					}
 				} else {
 					console.error('加载标签失败:', res.data.message)
 				}
 			} catch (e) {
 				console.error('加载标签异常:', e)
+			} finally {
+				this.isLoadingTags = false
 			}
 		},
+		// 选择分类
+		selectCategory(categoryId) {
+			this.selectedCategoryId = categoryId
+			console.log('=== 选择分类 ===')
+			console.log('分类ID:', categoryId, '类型:', typeof categoryId)
+			console.log('标签总数:', this.tagList.length)
+			
+			// 使用$nextTick确保DOM更新后再输出
+			this.$nextTick(() => {
+				console.log('筛选后的标签数量:', this.filteredTagList.length)
+				if (this.filteredTagList.length > 0) {
+					console.log('筛选后的标签名称:', this.filteredTagList.map(t => t.name || t.tag_name || t.tagName))
+				} else {
+					console.warn('没有找到匹配的标签!')
+					// 调试:查看前几条标签的category_id
+					if (this.tagList.length > 0) {
+						console.log('前3条标签的category_id:', 
+							this.tagList.slice(0, 3).map(t => ({
+								name: t.name || t.tag_name,
+								category_id: t.category_id,
+								categoryId: t.categoryId
+							}))
+						)
+					}
+				}
+			})
+		},
 		// 切换标签选择状态
 		toggleTag(tagId) {
 			const index = this.selectedTagIds.indexOf(tagId)
@@ -280,8 +428,9 @@ export default {
 			console.log('选中的标签ID:', this.selectedTagIds)
 		},
 		onGenderChange(e) {
-			this.genderIndex = e.detail.value
+			this.genderIndex = parseInt(e.detail.value) || 0
 			this.formData.gender = this.genderIndex + 1 // 1-男,2-女
+			console.log('性别选择 - genderIndex:', this.genderIndex, 'formData.gender:', this.formData.gender)
 		},
 		onConstellationChange(e) {
 			this.constellationIndex = e.detail.value
@@ -537,12 +686,34 @@ export default {
 						icon: 'success'
 					})
 					
-					// 发送刷新事件,通知我的资源页面刷新列表
-					uni.$emit('refreshResourceList')
-					
-					// 延迟返回上一页
+					// 延迟跳转到我的资源列表页面并刷新
 					setTimeout(() => {
-						uni.navigateBack()
+						// 获取页面栈,检查是否已经有我的资源列表页面
+						const pages = getCurrentPages()
+						const myResourcesPageIndex = pages.findIndex(page => {
+							return page.route && page.route.includes('matchmaker-workbench/my-resources')
+						})
+						
+						if (myResourcesPageIndex >= 0 && myResourcesPageIndex < pages.length - 1) {
+							// 如果我的资源列表页面已经在页面栈中(且不是当前页),返回到该页面
+							const delta = pages.length - myResourcesPageIndex - 1
+							uni.navigateBack({
+								delta: delta,
+								success: () => {
+									// 触发刷新事件
+									uni.$emit('refreshResourceList')
+								}
+							})
+						} else {
+							// 如果我的资源列表页面不在页面栈中,跳转到该页面
+							uni.redirectTo({
+								url: '/pages/matchmaker-workbench/my-resources',
+								success: () => {
+									// 触发刷新事件
+									uni.$emit('refreshResourceList')
+								}
+							})
+						}
 					}, 1500)
 				} else {
 					uni.showToast({
@@ -658,11 +829,43 @@ export default {
 			}
 		}
 		
+		.category-buttons {
+			display: flex;
+			flex-wrap: wrap;
+			gap: 12rpx;
+			margin-bottom: 20rpx;
+			
+			.category-btn {
+				padding: 10rpx 20rpx;
+				background: #F8F8F8;
+				border: 2rpx solid #E0E0E0;
+				border-radius: 20rpx;
+				transition: all 0.3s;
+				
+				&.active {
+					background: linear-gradient(135deg, #9C27B0 0%, #BA68C8 100%);
+					border-color: #9C27B0;
+					
+					.category-btn-text {
+						color: #FFFFFF;
+						font-weight: 600;
+					}
+				}
+				
+				.category-btn-text {
+					font-size: 24rpx;
+					color: #666;
+				}
+			}
+		}
+		
 		.tags-container {
 			display: flex;
 			flex-wrap: wrap;
 			gap: 15rpx;
 			margin-top: 10rpx;
+			min-height: 400rpx; /* 设置最小高度,确保空间不变 */
+			align-content: flex-start; /* 标签从顶部开始排列 */
 			
 			.tag-item {
 				padding: 12rpx 24rpx;
@@ -672,6 +875,7 @@ export default {
 				font-size: 26rpx;
 				color: #666;
 				transition: all 0.3s;
+				flex-shrink: 0; /* 防止标签被压缩 */
 				
 				&.selected {
 					background: linear-gradient(135deg, #F3E5F5 0%, #E1BEE7 100%);
@@ -690,6 +894,24 @@ export default {
 				padding: 20rpx;
 				color: #999;
 				font-size: 26rpx;
+				text-align: center;
+				width: 100%;
+				min-height: 400rpx; /* 加载时也保持固定高度 */
+				display: flex;
+				align-items: center;
+				justify-content: center;
+			}
+			
+			.tag-empty {
+				padding: 20rpx;
+				color: #999;
+				font-size: 26rpx;
+				text-align: center;
+				width: 100%;
+				min-height: 400rpx; /* 空状态时也保持固定高度 */
+				display: flex;
+				align-items: center;
+				justify-content: center;
 			}
 		}
 

+ 96 - 7
service/homePage/src/main/java/com/zhentao/controller/MyResourceController.java

@@ -7,6 +7,8 @@ import com.zhentao.entity.MyResource;
 import com.zhentao.entity.User;
 import com.zhentao.mapper.UserMapper;
 import com.zhentao.service.MyResourceService;
+import com.zhentao.service.MatchmakerService;
+import com.zhentao.entity.Matchmaker;
 import com.zhentao.vo.MyResourceVO;
 import com.zhentao.vo.ClientDetailVO;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -31,6 +33,9 @@ public class MyResourceController {
     @Autowired
     private UserMapper userMapper;
     
+    @Autowired
+    private MatchmakerService matchmakerService;
+    
     /**
      * 添加资源信息
      * 
@@ -140,10 +145,22 @@ public class MyResourceController {
                 System.out.println("信息:mateSelectionCriteria为null,字段会被保存为null");
             }
             
-            // 如果matchmakerId为空,使用当前登录用户的userId作为matchmakerId
+            // 如果matchmakerId为空,根据当前登录用户的userId从matchmakers表中查询对应的matchmaker_id
             if (myResource.getMatchmakerId() == null) {
                 if (currentUserId != null) {
-                    myResource.setMatchmakerId(currentUserId);
+                    // 根据user_id从matchmakers表中查询对应的红娘信息
+                    QueryWrapper<Matchmaker> matchmakerQueryWrapper = new QueryWrapper<>();
+                    matchmakerQueryWrapper.eq("user_id", currentUserId);
+                    Matchmaker matchmaker = matchmakerService.getOne(matchmakerQueryWrapper);
+                    
+                    if (matchmaker != null && matchmaker.getMatchmakerId() != null) {
+                        // 查询到红娘信息,设置matchmaker_id
+                        myResource.setMatchmakerId(matchmaker.getMatchmakerId());
+                        System.out.println("根据user_id=" + currentUserId + "查询到matchmaker_id=" + matchmaker.getMatchmakerId());
+                    } else {
+                        // 未查询到红娘信息,返回错误
+                        return Result.error("无法根据当前用户ID查询到对应的红娘信息,请确认该用户是否为红娘");
+                    }
                 } else {
                     return Result.error("无法获取当前登录用户ID");
                 }
@@ -165,8 +182,8 @@ public class MyResourceController {
     /**
      * 分页查询资源列表(包含用户头像)
      * 
-     * @param matchmakerId 红娘ID(可选,如果为空则使用currentUserId
-     * @param currentUserId 当前登录用户的用户ID(如果matchmakerId为空,则使用此值作为matchmakerId)
+     * @param matchmakerId 红娘ID(可选,如果为空则根据currentUserId从matchmakers表查询
+     * @param currentUserId 当前登录用户的用户ID(如果matchmakerId为空,则根据此值从matchmakers表查询对应的matchmaker_id)
      * @param keyword 搜索关键词(可选)
      * @param pageNum 页码(默认1)
      * @param pageSize 每页大小(默认10)
@@ -180,9 +197,25 @@ public class MyResourceController {
             @RequestParam(defaultValue = "1") Integer pageNum,
             @RequestParam(defaultValue = "10") Integer pageSize) {
         try {
-            // 如果matchmakerId为空,使用currentUserId作为matchmakerId
-            if (matchmakerId == null && currentUserId != null) {
-                matchmakerId = currentUserId;
+            // 如果matchmakerId为空,根据currentUserId从matchmakers表中查询对应的matchmaker_id
+            if (matchmakerId == null) {
+                if (currentUserId != null) {
+                    // 根据user_id从matchmakers表中查询对应的红娘信息
+                    QueryWrapper<Matchmaker> matchmakerQueryWrapper = new QueryWrapper<>();
+                    matchmakerQueryWrapper.eq("user_id", currentUserId);
+                    Matchmaker matchmaker = matchmakerService.getOne(matchmakerQueryWrapper);
+                    
+                    if (matchmaker != null && matchmaker.getMatchmakerId() != null) {
+                        // 查询到红娘信息,使用查询到的matchmaker_id
+                        matchmakerId = matchmaker.getMatchmakerId();
+                        System.out.println("根据user_id=" + currentUserId + "查询到matchmaker_id=" + matchmakerId);
+                    } else {
+                        // 未查询到红娘信息,返回错误
+                        return Result.error("无法根据当前用户ID查询到对应的红娘信息,请确认该用户是否为红娘");
+                    }
+                } else {
+                    return Result.error("无法获取当前登录用户ID或红娘ID");
+                }
             }
             
             Page<MyResourceVO> page = myResourceService.getResourceListWithAvatar(matchmakerId, keyword, pageNum, pageSize);
@@ -583,5 +616,61 @@ public class MyResourceController {
             return Result.error("获取最新跟进记录失败:" + e.getMessage());
         }
     }
+    
+    /**
+     * 根据标签名称分页查询资源列表(包含用户头像)
+     * 用于优质资源列表等场景
+     * 
+     * @param tagName 标签名称(如"优质资源")
+     * @param keyword 搜索关键词(可选)
+     * @param pageNum 页码(默认1)
+     * @param pageSize 每页大小(默认10)
+     * @return 分页数据(包含头像)
+     */
+    @GetMapping("/list-by-tag")
+    public Result<Page<MyResourceVO>> getResourceListByTagName(
+            @RequestParam String tagName,
+            @RequestParam(required = false) String keyword,
+            @RequestParam(required = false) Integer currentUserId,
+            @RequestParam(defaultValue = "1") Integer pageNum,
+            @RequestParam(defaultValue = "10") Integer pageSize) {
+        try {
+            // 如果提供了currentUserId,查询对应的matchmaker_id
+            Integer matchmakerId = null;
+            if (currentUserId != null) {
+                QueryWrapper<Matchmaker> matchmakerQueryWrapper = new QueryWrapper<>();
+                matchmakerQueryWrapper.eq("user_id", currentUserId);
+                Matchmaker matchmaker = matchmakerService.getOne(matchmakerQueryWrapper);
+                
+                if (matchmaker != null && matchmaker.getMatchmakerId() != null) {
+                    matchmakerId = matchmaker.getMatchmakerId();
+                    System.out.println("根据user_id=" + currentUserId + "查询到matchmaker_id=" + matchmakerId);
+                }
+            }
+            
+            Page<MyResourceVO> page = myResourceService.getResourceListByTagName(tagName, keyword, matchmakerId, pageNum, pageSize);
+            return Result.success(page);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("查询资源列表失败:" + e.getMessage());
+        }
+    }
+    
+    /**
+     * 根据资源ID获取标签ID列表
+     * 
+     * @param resourceId 资源ID
+     * @return 标签ID列表
+     */
+    @GetMapping("/tag-ids/{resourceId}")
+    public Result<java.util.List<Integer>> getTagIdsByResourceId(@PathVariable Integer resourceId) {
+        try {
+            java.util.List<Integer> tagIds = myResourceService.getTagIdsByResourceId(resourceId);
+            return Result.success(tagIds);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("查询标签ID列表失败:" + e.getMessage());
+        }
+    }
 }
 

+ 28 - 2
service/homePage/src/main/java/com/zhentao/controller/TagController.java

@@ -2,10 +2,13 @@ package com.zhentao.controller;
 
 import com.zhentao.common.Result;
 import com.zhentao.entity.Tag;
+import com.zhentao.entity.TagCategory;
 import com.zhentao.service.TagService;
+import com.zhentao.service.TagCategoryService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
 import java.util.List;
@@ -20,19 +23,42 @@ public class TagController {
     @Autowired
     private TagService tagService;
     
+    @Autowired
+    private TagCategoryService tagCategoryService;
+    
     /**
      * 获取所有启用的标签
      * @return 标签列表
      */
     @GetMapping("/list")
-    public Result<List<Tag>> getAllTags() {
+    public Result<List<Tag>> getAllTags(@RequestParam(required = false) Integer categoryId) {
         try {
-            List<Tag> tags = tagService.getAllActiveTags();
+            List<Tag> tags;
+            if (categoryId != null) {
+                tags = tagService.getActiveTagsByCategory(categoryId);
+            } else {
+                tags = tagService.getAllActiveTags();
+            }
             return Result.success(tags);
         } catch (Exception e) {
             e.printStackTrace();
             return Result.error("获取标签列表失败:" + e.getMessage());
         }
     }
+    
+    /**
+     * 获取所有启用的标签分类
+     * @return 分类列表
+     */
+    @GetMapping("/categories")
+    public Result<List<TagCategory>> getAllCategories() {
+        try {
+            List<TagCategory> categories = tagCategoryService.getAllActiveCategories();
+            return Result.success(categories);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("获取标签分类列表失败:" + e.getMessage());
+        }
+    }
 }
 

+ 6 - 0
service/homePage/src/main/java/com/zhentao/entity/Tag.java

@@ -34,5 +34,11 @@ public class Tag implements Serializable {
      */
     @TableField("status")
     private Integer status;
+    
+    /**
+     * 分类ID(关联tag_category表的category_id)
+     */
+    @TableField("category_id")
+    private Integer categoryId;
 }
 

+ 63 - 0
service/homePage/src/main/java/com/zhentao/entity/TagCategory.java

@@ -0,0 +1,63 @@
+package com.zhentao.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 标签分类实体类
+ */
+@Data
+@TableName("tag_category")
+public class TagCategory implements Serializable {
+    
+    private static final long serialVersionUID = 1L;
+    
+    /**
+     * 分类ID
+     */
+    @TableId(value = "category_id", type = IdType.AUTO)
+    private Integer id;
+    
+    /**
+     * 分类名称(如:职业、性格、外貌身材等)
+     */
+    @TableField("category_name")
+    private String name;
+    
+    /**
+     * 分类代码(如:occupation、personality、appearance等)
+     */
+    @TableField("category_code")
+    private String code;
+    
+    /**
+     * 排序顺序(数字越小越靠前)
+     */
+    @TableField("sort_order")
+    private Integer sortOrder;
+    
+    /**
+     * 状态(0-禁用,1-启用)
+     */
+    @TableField("status")
+    private Integer status;
+    
+    /**
+     * 创建时间
+     */
+    @TableField("create_time")
+    private Date createTime;
+    
+    /**
+     * 更新时间
+     */
+    @TableField("update_time")
+    private Date updateTime;
+}
+

+ 16 - 0
service/homePage/src/main/java/com/zhentao/mapper/MyResourceMapper.java

@@ -55,6 +55,22 @@ public interface MyResourceMapper extends BaseMapper<MyResource> {
             "LEFT JOIN users u ON up.user_id = u.user_id " +
             "WHERE up.user_id = #{userId}")
     Map<String, Object> selectUserProfileByUserId(@Param("userId") Integer userId);
+    
+    /**
+     * 根据标签名称分页查询资源列表(关联users表获取头像)
+     * 
+     * @param page 分页对象
+     * @param tagName 标签名称
+     * @param keyword 搜索关键词
+     * @param excludeMatchmakerId 要排除的红娘ID(如果提供,则排除该红娘已拥有的资源)
+     * @return 资源VO分页数据
+     */
+    Page<MyResourceVO> selectResourceListByTagName(
+            Page<MyResourceVO> page,
+            @Param("tagName") String tagName,
+            @Param("keyword") String keyword,
+            @Param("excludeMatchmakerId") Integer excludeMatchmakerId
+    );
 }
 
 

+ 13 - 0
service/homePage/src/main/java/com/zhentao/mapper/TagCategoryMapper.java

@@ -0,0 +1,13 @@
+package com.zhentao.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zhentao.entity.TagCategory;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 标签分类Mapper接口
+ */
+@Mapper
+public interface TagCategoryMapper extends BaseMapper<TagCategory> {
+}
+

+ 18 - 0
service/homePage/src/main/java/com/zhentao/service/MyResourceService.java

@@ -73,4 +73,22 @@ public interface MyResourceService extends IService<MyResource> {
      * @return 匹配结果列表(最多3个,按匹配度降序)
      */
     java.util.List<com.zhentao.vo.MatchResultVO> preciseMatch(Integer resourceId);
+    
+    /**
+     * 根据标签名称分页查询资源列表(包含用户头像)
+     * @param tagName 标签名称
+     * @param keyword 搜索关键词
+     * @param excludeMatchmakerId 要排除的红娘ID(如果提供,则排除该红娘已拥有的资源)
+     * @param pageNum 页码
+     * @param pageSize 每页大小
+     * @return 分页数据(包含头像)
+     */
+    Page<com.zhentao.vo.MyResourceVO> getResourceListByTagName(String tagName, String keyword, Integer excludeMatchmakerId, Integer pageNum, Integer pageSize);
+    
+    /**
+     * 根据资源ID获取标签ID列表
+     * @param resourceId 资源ID
+     * @return 标签ID列表
+     */
+    List<Integer> getTagIdsByResourceId(Integer resourceId);
 }

+ 19 - 0
service/homePage/src/main/java/com/zhentao/service/TagCategoryService.java

@@ -0,0 +1,19 @@
+package com.zhentao.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.zhentao.entity.TagCategory;
+
+import java.util.List;
+
+/**
+ * 标签分类Service接口
+ */
+public interface TagCategoryService extends IService<TagCategory> {
+    
+    /**
+     * 获取所有启用的分类
+     * @return 分类列表
+     */
+    List<TagCategory> getAllActiveCategories();
+}
+

+ 7 - 0
service/homePage/src/main/java/com/zhentao/service/TagService.java

@@ -15,5 +15,12 @@ public interface TagService extends IService<Tag> {
      * @return 标签列表
      */
     List<Tag> getAllActiveTags();
+    
+    /**
+     * 根据分类ID获取启用的标签
+     * @param categoryId 分类ID,如果为null则返回所有标签
+     * @return 标签列表
+     */
+    List<Tag> getActiveTagsByCategory(Integer categoryId);
 }
 

+ 55 - 0
service/homePage/src/main/java/com/zhentao/service/impl/MyResourceServiceImpl.java

@@ -203,6 +203,61 @@ public class MyResourceServiceImpl extends ServiceImpl<MyResourceMapper, MyResou
         return result;
     }
     
+    @Override
+    public Page<MyResourceVO> getResourceListByTagName(String tagName, String keyword, Integer excludeMatchmakerId, Integer pageNum, Integer pageSize) {
+        Page<MyResourceVO> page = new Page<>(pageNum, pageSize);
+        Page<MyResourceVO> result = myResourceMapper.selectResourceListByTagName(page, tagName, keyword, excludeMatchmakerId);
+        
+        // 为每个资源查询标签
+        if (result != null && result.getRecords() != null) {
+            System.out.println("=== 根据标签名称查询到的资源列表(包含头像) ===");
+            System.out.println("标签名称: " + tagName);
+            System.out.println("查询到的资源数量: " + result.getRecords().size());
+            for (MyResourceVO vo : result.getRecords()) {
+                System.out.println("资源ID: " + vo.getResourceId() + 
+                                 ", 姓名: " + vo.getName() + 
+                                 ", 手机号: " + vo.getPhone() + 
+                                 ", 头像URL: " + vo.getAvatarUrl() +
+                                 ", isUser: " + vo.getIsUser() + 
+                                 ", userId: " + vo.getUserId());
+                
+                // 确保 isUser 字段正确设置(由于SQL已经过滤了 is_user = 1,这里应该都是1)
+                if (vo.getIsUser() == null) {
+                    System.out.println("⚠️ 警告:资源 isUser 为 null,强制设置为 1");
+                    vo.setIsUser(1);
+                }
+                
+                // 查询该资源的标签
+                if (vo.getResourceId() != null) {
+                    List<Integer> tagIds = myResourceTagMapper.selectTagIdsByResourceId(vo.getResourceId());
+                    if (tagIds != null && !tagIds.isEmpty()) {
+                        // 根据标签ID查询标签名称
+                        List<String> tagNames = new java.util.ArrayList<>();
+                        for (Integer tagId : tagIds) {
+                            com.zhentao.entity.Tag tag = tagService.getById(tagId);
+                            if (tag != null && tag.getName() != null) {
+                                tagNames.add(tag.getName());
+                            }
+                        }
+                        vo.setTags(tagNames);
+                        System.out.println("  标签: " + tagNames);
+                    }
+                }
+            }
+        }
+        
+        return result;
+    }
+    
+    @Override
+    public List<Integer> getTagIdsByResourceId(Integer resourceId) {
+        if (resourceId == null) {
+            return new java.util.ArrayList<>();
+        }
+        List<Integer> tagIds = myResourceTagMapper.selectTagIdsByResourceId(resourceId);
+        return tagIds != null ? tagIds : new java.util.ArrayList<>();
+    }
+    
     @Override
     public ClientDetailVO getClientDetail(Integer resourceId) {
         System.out.println("=== MyResourceService.getClientDetail ===");

+ 27 - 0
service/homePage/src/main/java/com/zhentao/service/impl/TagCategoryServiceImpl.java

@@ -0,0 +1,27 @@
+package com.zhentao.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.zhentao.entity.TagCategory;
+import com.zhentao.mapper.TagCategoryMapper;
+import com.zhentao.service.TagCategoryService;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 标签分类Service实现类
+ */
+@Service
+public class TagCategoryServiceImpl extends ServiceImpl<TagCategoryMapper, TagCategory> implements TagCategoryService {
+    
+    @Override
+    public List<TagCategory> getAllActiveCategories() {
+        QueryWrapper<TagCategory> queryWrapper = new QueryWrapper<>();
+        queryWrapper.eq("status", 1);
+        queryWrapper.orderByAsc("sort_order");
+        queryWrapper.orderByAsc("category_id");
+        return this.list(queryWrapper);
+    }
+}
+

+ 11 - 0
service/homePage/src/main/java/com/zhentao/service/impl/TagServiceImpl.java

@@ -22,5 +22,16 @@ public class TagServiceImpl extends ServiceImpl<TagMapper, Tag> implements TagSe
         queryWrapper.orderByAsc("id");
         return this.list(queryWrapper);
     }
+    
+    @Override
+    public List<Tag> getActiveTagsByCategory(Integer categoryId) {
+        QueryWrapper<Tag> queryWrapper = new QueryWrapper<>();
+        queryWrapper.eq("status", 1);
+        if (categoryId != null) {
+            queryWrapper.eq("category_id", categoryId);
+        }
+        queryWrapper.orderByAsc("id");
+        return this.list(queryWrapper);
+    }
 }
 

+ 5 - 0
service/homePage/src/main/java/com/zhentao/vo/MyResourceVO.java

@@ -128,6 +128,11 @@ public class MyResourceVO implements Serializable {
      */
     private Integer isUser;
     
+    /**
+     * 用户ID(关联users表的user_id,如果isUser=1则此字段有值)
+     */
+    private Integer userId;
+    
     /**
      * 标签列表(从my_resource_tag关联表查询)
      */

+ 83 - 14
service/homePage/src/main/resources/mapper/MyResourceMapper.xml

@@ -84,6 +84,7 @@
         <result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/>
         <result property="avatarUrl" column="avatar_url" jdbcType="VARCHAR"/>
         <result property="isUser" column="is_user" jdbcType="INTEGER"/>
+        <result property="userId" column="user_id" jdbcType="INTEGER"/>
         <result property="isMatch" column="is_match" jdbcType="INTEGER"/>
         <result property="isInvitation" column="is_invitation" jdbcType="INTEGER"/>
         <result property="invitationTime" column="invitation_time" jdbcType="TIMESTAMP"/>
@@ -91,7 +92,7 @@
 
     <!-- 分页查询资源列表(关联users表获取头像) -->
     <select id="selectResourceListWithAvatar" resultMap="MyResourceVOMap">
-        SELECT 
+        SELECT DISTINCT
             r.resource_id,
             r.matchmaker_id,
             r.name,
@@ -112,29 +113,97 @@
             r.car,
             r.mate_selection_criteria,
             r.is_user,
+            r.user_id,
             r.create_time,
             r.update_time,
             u.avatar_url,
             COALESCE(f.is_match, 0) as is_match,
             COALESCE(f.is_invitation, 0) as is_invitation,
             f.invitation_time
-            FROM my_resource r
-            LEFT JOIN users u ON r.user_id = u.user_id
-            LEFT JOIN (
-                SELECT f1.resource_id, f1.matchmaker_id, f1.is_match, f1.is_invitation, f1.invitation_time
-                FROM user_follow_up f1
-                INNER JOIN (
-                    SELECT resource_id, matchmaker_id, MAX(update_time) as max_update_time
-                    FROM user_follow_up
-                    GROUP BY resource_id, matchmaker_id
-                ) f2 ON f1.resource_id = f2.resource_id 
-                    AND f1.matchmaker_id = f2.matchmaker_id 
-                    AND f1.update_time = f2.max_update_time
-            ) f ON r.resource_id = f.resource_id AND r.matchmaker_id = f.matchmaker_id
+        FROM my_resource r
+        LEFT JOIN users u ON r.user_id = u.user_id
+        LEFT JOIN (
+            SELECT f1.resource_id, f1.matchmaker_id, f1.is_match, f1.is_invitation, f1.invitation_time
+            FROM user_follow_up f1
+            INNER JOIN (
+                SELECT resource_id, matchmaker_id, MAX(update_time) as max_update_time
+                FROM user_follow_up
+                GROUP BY resource_id, matchmaker_id
+            ) f2 ON f1.resource_id = f2.resource_id 
+                AND f1.matchmaker_id = f2.matchmaker_id 
+                AND f1.update_time = f2.max_update_time
+        ) f ON r.resource_id = f.resource_id AND r.matchmaker_id = f.matchmaker_id
+        <if test="keyword != null and keyword != ''">
+            LEFT JOIN my_resource_tag mrt ON r.resource_id = mrt.resource_id
+            LEFT JOIN tag t ON mrt.tag_id = t.tag_id AND t.status = 1
+        </if>
         WHERE 1=1
         <if test="matchmakerId != null">
             AND r.matchmaker_id = #{matchmakerId}
         </if>
+        <if test="keyword != null and keyword != ''">
+            AND (r.name LIKE CONCAT('%', #{keyword}, '%') 
+                 OR r.phone LIKE CONCAT('%', #{keyword}, '%')
+                 OR t.tag_name LIKE CONCAT('%', #{keyword}, '%'))
+        </if>
+        ORDER BY r.create_time DESC
+    </select>
+    
+    <!-- 根据标签名称分页查询资源列表(关联users表获取头像) -->
+    <select id="selectResourceListByTagName" resultMap="MyResourceVOMap">
+        SELECT 
+            r.resource_id,
+            r.matchmaker_id,
+            r.name,
+            r.age,
+            r.gender,
+            r.constellation,
+            r.height,
+            r.weight,
+            r.marr_status,
+            r.diploma,
+            r.income,
+            r.address,
+            r.domicile,
+            r.occupation,
+            r.house,
+            r.phone,
+            r.backup_phone,
+            r.car,
+            r.mate_selection_criteria,
+            r.is_user,
+            r.user_id,
+            r.create_time,
+            r.update_time,
+            u.avatar_url,
+            COALESCE(f.is_match, 0) as is_match,
+            COALESCE(f.is_invitation, 0) as is_invitation,
+            f.invitation_time
+        FROM my_resource r
+        LEFT JOIN users u ON r.user_id = u.user_id
+        LEFT JOIN (
+            SELECT f1.resource_id, f1.matchmaker_id, f1.is_match, f1.is_invitation, f1.invitation_time
+            FROM user_follow_up f1
+            INNER JOIN (
+                SELECT resource_id, matchmaker_id, MAX(update_time) as max_update_time
+                FROM user_follow_up
+                GROUP BY resource_id, matchmaker_id
+            ) f2 ON f1.resource_id = f2.resource_id 
+                AND f1.matchmaker_id = f2.matchmaker_id 
+                AND f1.update_time = f2.max_update_time
+        ) f ON r.resource_id = f.resource_id AND r.matchmaker_id = f.matchmaker_id
+        INNER JOIN my_resource_tag mrt ON r.resource_id = mrt.resource_id
+        INNER JOIN tag t ON mrt.tag_id = t.tag_id
+        WHERE t.tag_name = #{tagName} AND t.status = 1
+        AND r.is_user = 1
+        <if test="excludeMatchmakerId != null">
+            AND NOT EXISTS (
+                SELECT 1 
+                FROM my_resource mr 
+                WHERE mr.phone = r.phone 
+                AND mr.matchmaker_id = #{excludeMatchmakerId}
+            )
+        </if>
         <if test="keyword != null and keyword != ''">
             AND (r.name LIKE CONCAT('%', #{keyword}, '%') 
                  OR r.phone LIKE CONCAT('%', #{keyword}, '%'))