Prechádzať zdrojové kódy

Merge remote-tracking branch 'origin/test_dev' into test

YH_0525 1 mesiac pred
rodič
commit
c16a212ad0
27 zmenil súbory, kde vykonal 3005 pridanie a 228 odobranie
  1. 13 0
      LiangZhiYUMao/App.vue
  2. 7 0
      LiangZhiYUMao/pages.json
  3. 85 26
      LiangZhiYUMao/pages/index/index.vue
  4. 353 17
      LiangZhiYUMao/pages/matchmaker-workbench/client-detail.vue
  5. 320 32
      LiangZhiYUMao/pages/matchmaker-workbench/message.vue
  6. 238 60
      LiangZhiYUMao/pages/matchmaker-workbench/my-resources.vue
  7. 253 49
      LiangZhiYUMao/pages/matchmaker-workbench/precise-match.vue
  8. 289 0
      LiangZhiYUMao/pages/matchmaker-workbench/system-messages.vue
  9. 33 1
      LiangZhiYUMao/pages/message/chat.vue
  10. 33 22
      LiangZhiYUMao/utils/tim-presence-manager.js
  11. 17 18
      LiangZhiYUMao/utils/websocket.js
  12. 283 0
      service/homePage/src/main/java/com/zhentao/controller/MyResourceController.java
  13. 82 0
      service/homePage/src/main/java/com/zhentao/entity/FollowUpStatistics.java
  14. 94 0
      service/homePage/src/main/java/com/zhentao/entity/UserFollowUp.java
  15. 26 0
      service/homePage/src/main/java/com/zhentao/mapper/FollowUpStatisticsMapper.java
  16. 17 0
      service/homePage/src/main/java/com/zhentao/mapper/MyResourceMapper.java
  17. 26 0
      service/homePage/src/main/java/com/zhentao/mapper/UserFollowUpMapper.java
  18. 39 0
      service/homePage/src/main/java/com/zhentao/service/FollowUpStatisticsService.java
  19. 17 0
      service/homePage/src/main/java/com/zhentao/service/MyResourceService.java
  20. 28 0
      service/homePage/src/main/java/com/zhentao/service/UserFollowUpService.java
  21. 145 0
      service/homePage/src/main/java/com/zhentao/service/impl/FollowUpStatisticsServiceImpl.java
  22. 412 0
      service/homePage/src/main/java/com/zhentao/service/impl/MyResourceServiceImpl.java
  23. 53 0
      service/homePage/src/main/java/com/zhentao/service/impl/UserFollowUpServiceImpl.java
  24. 5 0
      service/homePage/src/main/java/com/zhentao/vo/ClientDetailVO.java
  25. 102 0
      service/homePage/src/main/java/com/zhentao/vo/MatchResultVO.java
  26. 15 0
      service/homePage/src/main/java/com/zhentao/vo/MyResourceVO.java
  27. 20 3
      service/homePage/src/main/resources/mapper/MyResourceMapper.xml

+ 13 - 0
LiangZhiYUMao/App.vue

@@ -3,6 +3,12 @@ import timPresenceManager from '@/utils/tim-presence-manager.js';
 import timManager from '@/utils/tim-manager.js';
 
 export default {
+	data() {
+		return {
+			_timInitializing: false
+		}
+	},
+
 	onLaunch: function () {
 		console.log('=== App启动 ===');
 		
@@ -42,6 +48,11 @@ export default {
 		 * 初始化全局TIM
 		 */
 		async initGlobalTIM() {
+			// 避免在登录过程中被重复触发
+			if (this._timInitializing) {
+				return
+			}
+			this._timInitializing = true
 			try {
 				// 获取当前登录用户ID
 				const userInfo = uni.getStorageSync('userInfo');
@@ -98,6 +109,8 @@ export default {
 			}
 			} catch (error) {
 				console.error('❌ 初始化全局TIM失败:', error);
+			} finally {
+				this._timInitializing = false
 			}
 		},
 		

+ 7 - 0
LiangZhiYUMao/pages.json

@@ -70,6 +70,13 @@
 				"navigationStyle": "custom"
 			}
 		},
+		{
+			"path": "pages/matchmaker-workbench/system-messages",
+			"style": {
+				"navigationBarTitleText": "系统通知",
+				"navigationStyle": "custom"
+			}
+		},
 		{
 			"path": "pages/matchmaker-workbench/mine",
 			"style": {

+ 85 - 26
LiangZhiYUMao/pages/index/index.vue

@@ -222,7 +222,7 @@
 				<text class="tabbar-text">我的</text>
 			</view>
 		</view>
-		
+
 		<!-- 红娘提示弹框 -->
 		<uni-popup ref="matchmakerPopup" type="dialog">
 			<uni-popup-dialog
@@ -240,42 +240,39 @@
 
 <script>
 	import api, { request } from '@/utils/api.js'
-    import { formatTime, formatCountdown, isLoggedIn, goToLogin } from '@/utils/util.js'
+	import { formatTime, formatCountdown, isLoggedIn, goToLogin } from '@/utils/util.js'
 	import { DEFAULT_IMAGES, ACTIVITY_TYPES } from '@/config/index.js'
 	
+	import timManager from '@/utils/tim-manager.js'
+
 	export default {
-		components: {},
 		data() {
 			return {
 				// 用户信息
-			userInfo: {
-				nickname: '小张',
-				userId: null,
-				isMatchmaker: '1' // 测试用,实际从后端获取
-			},
+				userInfo: {
+					nickname: '小张',
+					userId: null,
+					isMatchmaker: '1' // 测试用,实际从后端获取
+				},
 				matchCount: 3,
-			
 
 				// 轮播图数据
-				bannerList: [
-
-				],
+				bannerList: [],
 
 				// 公告数据
-				noticeList: [
+				noticeList: [],
+
+				// 功能入口
+				functionList: [
+					{ id: 1, name: '星命测算', icon: '💖', path: '/pages/astrology/index', bgColor: '#FF6B9D', iconColor: '#FFFFFF', gradient: 'linear-gradient(135deg, #FF6B9D 0%, #FF8EAB 100%)' },
+					{ id: 2, name: '红娘列表', icon: '👤', path: '/pages/matchmakers/list', bgColor: '#6BC5F8', iconColor: '#FFFFFF', gradient: 'linear-gradient(135deg, #6BC5F8 0%, #87CEEB 100%)' },
+					// { id: 3, name: '加入红娘', icon: '💕', path: '/pages/part-time-matchmaker/index', bgColor: '#9B7EDE', iconColor: '#FFFFFF', gradient: 'linear-gradient(135deg, #9B7EDE 0%, #B19CD9 100%)' },
+					// { id: 3, name: '加入红娘', icon: '💕', path: '/pages/part-time-matchmaker/index', bgColor: '#9B7EDE', iconColor: '#FFFFFF', gradient: 'linear-gradient(135deg, #9B7EDE 0%, #B19CD9 100%)' },
+					{ id: 4, name: '精品课程', icon: '📚', path: '/pages/courses/list', bgColor: '#FF8C42', iconColor: '#FFFFFF', gradient: 'linear-gradient(135deg, #FF8C42 0%, #FFA366 100%)' },
+					{ id: 5, name: '今日缘分', icon: '💝', path: '/pages/recommend/index', bgColor: '#FF69B4', iconColor: '#FFFFFF', gradient: 'linear-gradient(135deg, #FF69B4 0%, #FF8CC8 100%)', needLogin: true },
+					// { id: 6, name: '专属定制', icon: '🎁', path: '/pages/customize/index', bgColor: '#FFA500', iconColor: '#FFFFFF', gradient: 'linear-gradient(135deg, #FFA500 0%, #FFB84D 100%)' }
 				],
 
-			// 功能入口
-			functionList: [
-				{ id: 1, name: '星命测算', icon: '💖', path: '/pages/astrology/index', bgColor: '#FF6B9D', iconColor: '#FFFFFF', gradient: 'linear-gradient(135deg, #FF6B9D 0%, #FF8EAB 100%)' },
-				{ id: 2, name: '红娘列表', icon: '👤', path: '/pages/matchmakers/list', bgColor: '#6BC5F8', iconColor: '#FFFFFF', gradient: 'linear-gradient(135deg, #6BC5F8 0%, #87CEEB 100%)' },
-				// { id: 3, name: '加入红娘', icon: '💕', path: '/pages/part-time-matchmaker/index', bgColor: '#9B7EDE', iconColor: '#FFFFFF', gradient: 'linear-gradient(135deg, #9B7EDE 0%, #B19CD9 100%)' },
-				// { id: 3, name: '加入红娘', icon: '💕', path: '/pages/part-time-matchmaker/index', bgColor: '#9B7EDE', iconColor: '#FFFFFF', gradient: 'linear-gradient(135deg, #9B7EDE 0%, #B19CD9 100%)' },
-				{ id: 4, name: '精品课程', icon: '📚', path: '/pages/courses/list', bgColor: '#FF8C42', iconColor: '#FFFFFF', gradient: 'linear-gradient(135deg, #FF8C42 0%, #FFA366 100%)' },
-				{ id: 5, name: '今日缘分', icon: '💝', path: '/pages/recommend/index', bgColor: '#FF69B4', iconColor: '#FFFFFF', gradient: 'linear-gradient(135deg, #FF69B4 0%, #FF8CC8 100%)', needLogin: true },
-				// { id: 6, name: '专属定制', icon: '🎁', path: '/pages/customize/index', bgColor: '#FFA500', iconColor: '#FFFFFF', gradient: 'linear-gradient(135deg, #FFA500 0%, #FFB84D 100%)' }
-			],
-
 				// 热门活动
 				hotActivities: [],
 
@@ -356,11 +353,73 @@
 			},
 			
 			// 前往红娘工作台
-			goToMatchmakerWorkspace() {
+			async goToMatchmakerWorkspace() {
 				console.log('🚀 前往红娘工作台')
 				this.$refs.matchmakerPopup.close()
 				
-				// 跳转到红娘工作台
+				try {
+					const userId = uni.getStorageSync('userId')
+					if (!userId) {
+						uni.showToast({ title: '请先登录', icon: 'none' })
+						return
+					}
+					
+					if (!timManager.tim) {
+						try {
+							timManager.init(1600109674)
+							console.log('✅ TIM SDK 初始化完成(首页)')
+						} catch (e) {
+							console.error('❌ TIM 初始化失败(首页):', e)
+						}
+					}
+					
+					const res = await uni.request({
+						url: 'http://localhost:8081/api/matchmaker/current',
+						method: 'GET',
+						data: { userId }
+					})
+					
+					if (!res[1] || res[1].data.code !== 200) {
+						uni.showToast({ title: '获取红娘信息失败', icon: 'none' })
+						return
+					}
+					
+					const matchmakerInfo = res[1].data.data || {}
+					const imUserId = matchmakerInfo.imUserId
+					if (!imUserId) {
+						uni.showToast({ title: '红娘IM账号缺失', icon: 'none' })
+						return
+					}
+					
+					const sigRes = await uni.request({
+						url: `http://localhost:8083/api/im/getUserSig?userId=${imUserId}`,
+						method: 'GET'
+					})
+					
+					if (!sigRes[1] || sigRes[1].data.code !== 200) {
+						uni.showToast({ title: '获取UserSig失败', icon: 'none' })
+						return
+					}
+					
+					const userSig = sigRes[1].data.data.userSig
+					console.log('✅ 首页获取 UserSig:', userSig ? '成功' : '失败')
+					
+					try {
+						await timManager.logout()
+						console.log('✅ 已登出之前的IM账号(首页)')
+					} catch (e) {
+						console.log('⚠️ 登出失败或未登录(首页):', e.message)
+					}
+					
+					await timManager.login(imUserId, userSig)
+					console.log('✅ 红娘已在首页登录 IM:', imUserId)
+					
+					uni.$emit('matchmakerIMReady', { imUserId })
+				} catch (error) {
+					console.error('❌ 首页初始化红娘IM失败:', error)
+					uni.showToast({ title: '红娘IM初始化失败', icon: 'none' })
+				}
+				
 				uni.navigateTo({
 					url: '/pages/matchmaker-workbench/index'
 				})

+ 353 - 17
LiangZhiYUMao/pages/matchmaker-workbench/client-detail.vue

@@ -12,7 +12,12 @@
 			<image :src="clientInfo.avatar" class="client-avatar" mode="aspectFill"></image>
 			<view class="basic-info">
 				<text class="client-name">{{ clientInfo.gender }} · {{ clientInfo.name }}</text>
-				<text class="client-status">{{ clientInfo.status }}</text>
+				<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 }">
+						{{ followForm.matchStatus === 1 ? '已匹配' : '未匹配' }}
+					</text>
+				</view>
 				<view class="client-tags">
 					<text class="tag" v-for="(tag, index) in clientInfo.tags" :key="index">{{ tag }}</text>
 				</view>
@@ -92,7 +97,10 @@
 			</view>
 			<view class="detail-item">
 				<text class="detail-label">收入:</text>
-				<text class="detail-value">{{ clientInfo.details.income }}</text>
+				<text class="detail-value">
+					{{ clientInfo.details.income }}
+					<text v-if="clientInfo.details.income && clientInfo.details.income !== '未知'">万元/年</text>
+				</text>
 			</view>
 			<view class="detail-item">
 				<text class="detail-label">购房情况:</text>
@@ -150,22 +158,26 @@
 				</view>
 			</view>
 
+			<!-- 匹配状态 -->
+			<view class="follow-group">
+				<text class="group-label">匹配状态</text>
+				<view class="status-tags-row">
+					<view class="status-tag" :class="{ active: followForm.matchStatus === 0 }" @click="handleMatchStatusChange(0)">未匹配</view>
+					<view class="status-tag" :class="{ active: followForm.matchStatus === 1 }" @click="handleMatchStatusChange(1)">已匹配</view>
+				</view>
+			</view>
+
 			<!-- 跟进进度 -->
 			<view class="follow-group">
 				<text class="group-label">跟进进度</text>
 				<view class="status-tags-row">
-					<view class="status-tag" :class="{ active: followForm.progress === 'matched' }" @click="followForm.progress = 'matched'">已匹配</view>
 					<view class="status-tag" :class="{ active: followForm.progress === 'notMet' }" @click="followForm.progress = 'notMet'">未见面</view>
 					<view class="status-tag" :class="{ active: followForm.progress === 'communicating' }" @click="followForm.progress = 'communicating'">沟通中</view>
+					<view class="status-tag" :class="{ active: followForm.invitationStatus === 0 }" @click="handleInvitationStatusChange(0)">未邀约</view>
+					<view class="status-tag" :class="{ active: followForm.invitationStatus === 1 }" @click="handleInvitationStatusChange(1)">已邀约</view>
 				</view>
 			</view>
 
-			<!-- 邀约状态 -->
-			<view class="follow-group">
-				<text class="group-label">邀约状态</text>
-				<view class="input-like">{{ followForm.inviteStatus || '未邀约' }}</view>
-			</view>
-
 			<!-- 邀约时间 -->
 			<view class="follow-group">
 				<text class="group-label">邀约时间</text>
@@ -197,10 +209,12 @@ export default {
 			followForm: {
 				type: 'phone',
 				progress: 'matched',
-				inviteStatus: '未邀约',
+				matchStatus: 0, // 0-未匹配,1-已匹配
+				invitationStatus: 0, // 0-未邀约,1-已邀约
 				inviteDate: '',
 				remark: ''
 			},
+			resourceId: null, // 资源ID
 			clientInfo: {
 				id: 1,
 				name: '',
@@ -242,6 +256,11 @@ export default {
 		if (stored === true || stored === false) {
 			this.isDarkTheme = stored
 		}
+		
+		// 如果已经有resourceId,刷新统计数据
+		if (this.resourceId) {
+			this.loadFollowStatistics(this.resourceId)
+		}
 	},
 	onLoad(options) {
 		console.log('=== 客户详情页面 onLoad ===')
@@ -270,6 +289,7 @@ export default {
 				return
 			}
 			console.log('最终使用的resourceId:', resourceId, '类型:', typeof resourceId)
+			this.resourceId = resourceId
 			this.loadClientInfo(resourceId)
 		} else {
 			console.error('未获取到资源ID')
@@ -321,12 +341,272 @@ export default {
 		onInviteDateChange(e) {
 			this.followForm.inviteDate = e.detail.value
 		},
-		// 提交跟进(暂时仅提示)
-		handleSubmitFollow() {
-			uni.showToast({
-				title: '跟进已保存',
-				icon: 'success'
-			})
+		// 匹配状态变更
+		handleMatchStatusChange(status) {
+			this.followForm.matchStatus = status
+		},
+		// 邀约状态变更
+		handleInvitationStatusChange(status) {
+			this.followForm.invitationStatus = status
+			// 如果选择已邀约且没有设置邀约时间,提示用户选择
+			if (status === 1 && !this.followForm.inviteDate) {
+				uni.showToast({
+					title: '请选择邀约时间',
+					icon: 'none'
+				})
+			}
+		},
+		// 加载最新的跟进信息,更新表单字段
+		async loadLatestFollowInfo(resourceId) {
+			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}/my-resource/latest-follow/${resourceId}`,
+					method: 'GET',
+					header: {
+						'Content-Type': 'application/json'
+					}
+				})
+				
+				if (error) {
+					console.error('加载最新跟进信息失败:', error)
+					return
+				}
+				
+				if (res.statusCode === 200 && res.data && res.data.code === 200) {
+					const followUp = res.data.data
+					
+					if (followUp) {
+						// 更新跟进方式(1-电话,2-面谈,3-其他)
+						if (followUp.followType !== null && followUp.followType !== undefined) {
+							const followType = followUp.followType
+							if (followType === 1) {
+								this.followForm.type = 'phone'
+							} else if (followType === 2) {
+								this.followForm.type = 'meet'
+							} else if (followType === 3) {
+								this.followForm.type = 'other'
+							}
+						}
+						
+						// 更新跟进进度(1-已匹配,2-未见面,3-沟通中)
+						if (followUp.followProgress !== null && followUp.followProgress !== undefined) {
+							const progress = followUp.followProgress
+							if (progress === 1) {
+								this.followForm.progress = 'matched'
+							} else if (progress === 2) {
+								this.followForm.progress = 'notMet'
+							} else if (progress === 3) {
+								this.followForm.progress = 'communicating'
+							}
+						}
+						
+						// 更新匹配状态
+						if (followUp.isMatch !== null && followUp.isMatch !== undefined) {
+							this.followForm.matchStatus = parseInt(followUp.isMatch) || 0
+						}
+						
+						// 更新邀约状态
+						if (followUp.isInvitation !== null && followUp.isInvitation !== undefined) {
+							this.followForm.invitationStatus = parseInt(followUp.isInvitation) || 0
+						}
+						
+						// 更新邀约时间
+						if (followUp.invitationTime) {
+							const date = new Date(followUp.invitationTime)
+							const year = date.getFullYear()
+							const month = String(date.getMonth() + 1).padStart(2, '0')
+							const day = String(date.getDate()).padStart(2, '0')
+							this.followForm.inviteDate = `${year}-${month}-${day}`
+						} else {
+							this.followForm.inviteDate = ''
+						}
+						
+						// 更新备注
+						if (followUp.remark !== null && followUp.remark !== undefined) {
+							this.followForm.remark = followUp.remark || ''
+						} else {
+							this.followForm.remark = ''
+						}
+						
+						console.log('表单字段已更新:', {
+							type: this.followForm.type,
+							progress: this.followForm.progress,
+							matchStatus: this.followForm.matchStatus,
+							invitationStatus: this.followForm.invitationStatus,
+							inviteDate: this.followForm.inviteDate,
+							remark: this.followForm.remark
+						})
+					} else {
+						console.log('暂无跟进记录,保持表单当前值')
+					}
+				}
+			} catch (e) {
+				console.error('加载最新跟进信息异常:', e)
+			}
+		},
+		// 加载跟进统计数据
+		async loadFollowStatistics(resourceId) {
+			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}/my-resource/follow-statistics/${resourceId}`,
+					method: 'GET',
+					header: {
+						'Content-Type': 'application/json'
+					}
+				})
+				
+				if (error) {
+					console.error('加载统计数据失败:', error)
+					return
+				}
+				
+				if (res.statusCode === 200 && res.data && res.data.code === 200) {
+					const statistics = res.data.data
+					console.log('=== 统计数据响应 ===')
+					console.log('完整响应:', JSON.stringify(res.data, null, 2))
+					console.log('统计数据对象:', statistics)
+					console.log('followCount:', statistics?.followCount, statistics?.follow_count)
+					console.log('matchCount:', statistics?.matchCount, statistics?.match_count)
+					console.log('phoneCount:', statistics?.phoneCount, statistics?.phone_count)
+					console.log('interviewCount:', statistics?.interviewCount, statistics?.interview_count)
+					
+					// 更新统计数据(支持驼峰和下划线两种字段名格式)
+					if (this.clientInfo && this.clientInfo.stats) {
+						const followCount = statistics?.followCount ?? statistics?.follow_count ?? 0
+						const matchCount = statistics?.matchCount ?? statistics?.match_count ?? 0
+						const phoneCount = statistics?.phoneCount ?? statistics?.phone_count ?? 0
+						const interviewCount = statistics?.interviewCount ?? statistics?.interview_count ?? 0
+						
+						this.clientInfo.stats.followTimes = followCount
+						this.clientInfo.stats.matchCount = matchCount
+						this.clientInfo.stats.phoneCalls = phoneCount
+						this.clientInfo.stats.interviews = interviewCount
+						
+						console.log('更新后的统计数据:', {
+							followTimes: this.clientInfo.stats.followTimes,
+							matchCount: this.clientInfo.stats.matchCount,
+							phoneCalls: this.clientInfo.stats.phoneCalls,
+							interviews: this.clientInfo.stats.interviews
+						})
+					} else {
+						console.warn('clientInfo或stats不存在')
+					}
+				} else {
+					console.error('统计数据接口返回错误:', res.data)
+				}
+			} catch (e) {
+				console.error('加载统计数据异常:', e)
+			}
+		},
+		// 提交跟进
+		async handleSubmitFollow() {
+			if (!this.resourceId) {
+				uni.showToast({
+					title: '资源ID不存在',
+					icon: 'none'
+				})
+				return
+			}
+			
+			try {
+				uni.showLoading({
+					title: '保存中...'
+				})
+				
+				const baseUrl = process.env.NODE_ENV === 'development' 
+					? 'http://localhost:8083/api'  // 开发环境 - 通过网关
+					: 'https://your-domain.com/api'  // 生产环境
+				
+				// 准备请求数据
+				// 跟进方式转换:phone->1, meet->2, other->3
+				let followType = 1 // 默认电话
+				if (this.followForm.type === 'meet') {
+					followType = 2
+				} else if (this.followForm.type === 'other') {
+					followType = 3
+				}
+				
+				// 跟进进度转换:matched->1, notMet->2, communicating->3
+				let progress = 1 // 默认已匹配
+				if (this.followForm.progress === 'notMet') {
+					progress = 2
+				} else if (this.followForm.progress === 'communicating') {
+					progress = 3
+				}
+				
+				const requestData = {
+					followType: followType,
+					progress: this.followForm.progress, // 保持字符串格式,后端会转换
+					matchStatus: this.followForm.matchStatus,
+					invitationStatus: this.followForm.invitationStatus,
+					invitationTime: this.followForm.inviteDate || null,
+					remark: this.followForm.remark || '' // 添加备注字段
+				}
+				
+				console.log('保存跟进数据:', requestData)
+				
+				const [error, res] = await uni.request({
+					url: `${baseUrl}/my-resource/update-follow/${this.resourceId}`,
+					method: 'PUT',
+					data: requestData,
+					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'
+					})
+					
+					// 刷新统计数据
+					if (this.resourceId) {
+						this.loadFollowStatistics(this.resourceId)
+					}
+					
+					// 重新加载最新的跟进信息,更新表单字段
+					if (this.resourceId) {
+						this.loadLatestFollowInfo(this.resourceId)
+					}
+					
+					// 触发资源列表刷新事件
+					uni.$emit('refreshResourceList')
+				} else {
+					const errorMsg = res.data?.message || '保存失败'
+					uni.showToast({
+						title: errorMsg,
+						icon: 'none'
+					})
+				}
+			} catch (e) {
+				uni.hideLoading()
+				console.error('保存跟进异常:', e)
+				uni.showToast({
+					title: '保存失败,请稍后重试',
+					icon: 'none'
+				})
+			}
 		},
 		// 从API加载客户信息
 		async loadClientInfo(resourceId) {
@@ -431,6 +711,24 @@ export default {
 						}
 					}
 					
+					// 加载匹配状态和邀约状态
+					const isMatch = data.isMatch !== null && data.isMatch !== undefined ? data.isMatch : 
+					               (data.is_match !== null && data.is_match !== undefined ? data.is_match : 0)
+					const isInvitation = data.isInvitation !== null && data.isInvitation !== undefined ? data.isInvitation : 
+					                    (data.is_Invitation !== null && data.is_Invitation !== undefined ? data.is_Invitation : 0)
+					const invitationTime = data.invitationTime || data.Invitation_time
+					
+					this.followForm.matchStatus = parseInt(isMatch) || 0
+					this.followForm.invitationStatus = parseInt(isInvitation) || 0
+					if (invitationTime) {
+						// 将日期转换为YYYY-MM-DD格式
+						const date = new Date(invitationTime)
+						const year = date.getFullYear()
+						const month = String(date.getMonth() + 1).padStart(2, '0')
+						const day = String(date.getDate()).padStart(2, '0')
+						this.followForm.inviteDate = `${year}-${month}-${day}`
+					}
+					
 					// 映射数据到clientInfo
 					this.clientInfo = {
 						id: resourceId,
@@ -445,7 +743,7 @@ export default {
 						originalPhone: data.phone || '', // 保存原始手机号
 						originalBackupPhone: backupPhone || '', // 保存原始备用手机号
 						stats: {
-							followTimes: 0, // 可以从其他接口获取
+							followTimes: 0, // 将从统计数据接口获取
 							matchCount: 0,
 							phoneCalls: 0,
 							interviews: 0
@@ -470,6 +768,9 @@ export default {
 					
 					console.log('=== 映射后的clientInfo ===')
 					console.log('clientInfo:', JSON.stringify(this.clientInfo, null, 2))
+					
+					// 加载跟进统计数据
+					this.loadFollowStatistics(resourceId)
 				} else {
 					uni.showToast({
 						title: res.data.message || '加载失败',
@@ -592,6 +893,41 @@ export default {
 			color: #2C2C2C;
 			margin-bottom: 12rpx;
 			letter-spacing: 0.5rpx;
+			display: block;
+		}
+
+		.status-tag-wrapper {
+			display: flex;
+			align-items: center;
+			gap: 10rpx;
+			margin-bottom: 18rpx;
+		}
+
+		.status-tag {
+			display: inline-block;
+			padding: 4rpx 12rpx;
+			border-radius: 12rpx;
+			font-size: 22rpx;
+			font-weight: 500;
+
+			&.register-tag {
+				&.registered {
+					background: #E8F5E9;
+					color: #4CAF50;
+				}
+			}
+
+			&.match-tag {
+				&.matched {
+					background: #E3F2FD;
+					color: #2196F3;
+				}
+
+				&.unmatched {
+					background: #FFF3E0;
+					color: #FF9800;
+				}
+			}
 		}
 
 		.client-status {

+ 320 - 32
LiangZhiYUMao/pages/matchmaker-workbench/message.vue

@@ -13,30 +13,118 @@
 				<text>加载中...</text>
 			</view>
 			
-			<!-- 会话列表 -->
+			<!-- 消息内容 -->
 			<view v-else-if="conversationList.length > 0">
-				<view 
-					v-for="conversation in conversationList" 
-					:key="conversation.conversationID"
-					class="message-item user-message"
-					@click="openChat(conversation)"
-				>
-					<image 
-						v-if="conversation.userProfile.avatar" 
-						:src="conversation.userProfile.avatar" 
-						class="message-avatar-img"
-					/>
-					<view v-else class="message-avatar">
-						{{ conversation.userProfile.nick || conversation.userProfile.userID.charAt(0) }}
-					</view>
+				<!-- 系统通知卡片 -->
+				<view class="message-item system-notification" v-if="systemNotification" @click="openSystemMessages">
+					<text class="message-type">{{ systemNotification.title }}</text>
+					<text class="message-time">{{ systemNotification.timeText }}</text>
+					<text class="message-content">{{ systemNotification.content }}</text>
+					<text class="message-footer">{{ systemNotification.footer }}</text>
+				</view>
+				
+				<!-- 撮合成功通知卡片 -->
+				<view class="message-item match-success" v-if="matchSuccessNotification">
+					<view class="message-icon heart"></view>
 					<view class="message-body">
-						<text class="message-type">{{ conversation.userProfile.nick || conversation.userProfile.userID }}</text>
-						<text class="message-content">{{ getLastMessageText(conversation.lastMessage) }}</text>
+						<text class="message-type">{{ matchSuccessNotification.title }}</text>
+						<text class="message-content">{{ matchSuccessNotification.content }}</text>
+					</view>
+					<text class="message-time">{{ matchSuccessNotification.timeText }}</text>
+				</view>
+				
+				<!-- 今天 -->
+				<view v-if="todayConversations.length > 0">
+					<view class="time-group">
+						<text class="time-label">今天</text>
 					</view>
-					<view class="message-right">
-						<text class="message-time">{{ formatTime(conversation.lastMessage.lastTime) }}</text>
-						<view v-if="conversation.unreadCount > 0" class="unread-badge">
-							{{ conversation.unreadCount > 99 ? '99+' : conversation.unreadCount }}
+					<view
+						v-for="conversation in todayConversations"
+						:key="conversation.conversationID"
+						class="message-item user-message"
+						@click="openChat(conversation)"
+					>
+						<image
+							v-if="conversation.userProfile.avatar"
+							:src="conversation.userProfile.avatar"
+							class="message-avatar-img"
+						/>
+						<view v-else class="message-avatar">
+							{{ conversation.userProfile.nick || conversation.userProfile.userID.charAt(0) }}
+						</view>
+						<view class="message-body">
+							<text class="message-type">{{ conversation.userProfile.nick || conversation.userProfile.userID }}</text>
+							<text class="message-content">{{ getLastMessageText(conversation.lastMessage) }}</text>
+						</view>
+						<view class="message-right">
+							<text class="message-time">{{ formatTime(conversation.lastMessage.lastTime) }}</text>
+							<view v-if="conversation.unreadCount > 0" class="unread-badge">
+								{{ conversation.unreadCount > 99 ? '99+' : conversation.unreadCount }}
+							</view>
+						</view>
+					</view>
+				</view>
+				
+				<!-- 昨日 -->
+				<view v-if="yesterdayConversations.length > 0">
+					<view class="time-group">
+						<text class="time-label">昨日</text>
+					</view>
+					<view
+						v-for="conversation in yesterdayConversations"
+						:key="conversation.conversationID"
+						class="message-item user-message"
+						@click="openChat(conversation)"
+					>
+						<image
+							v-if="conversation.userProfile.avatar"
+							:src="conversation.userProfile.avatar"
+							class="message-avatar-img"
+						/>
+						<view v-else class="message-avatar">
+							{{ conversation.userProfile.nick || conversation.userProfile.userID.charAt(0) }}
+						</view>
+						<view class="message-body">
+							<text class="message-type">{{ conversation.userProfile.nick || conversation.userProfile.userID }}</text>
+							<text class="message-content">{{ getLastMessageText(conversation.lastMessage) }}</text>
+						</view>
+						<view class="message-right">
+							<text class="message-time">{{ formatTime(conversation.lastMessage.lastTime) }}</text>
+							<view v-if="conversation.unreadCount > 0" class="unread-badge">
+								{{ conversation.unreadCount > 99 ? '99+' : conversation.unreadCount }}
+							</view>
+						</view>
+					</view>
+				</view>
+				
+				<!-- 更早 -->
+				<view v-if="earlierConversations.length > 0">
+					<view class="time-group">
+						<text class="time-label">更早</text>
+					</view>
+					<view
+						v-for="conversation in earlierConversations"
+						:key="conversation.conversationID"
+						class="message-item user-message"
+						@click="openChat(conversation)"
+					>
+						<image
+							v-if="conversation.userProfile.avatar"
+							:src="conversation.userProfile.avatar"
+							class="message-avatar-img"
+						/>
+						<view v-else class="message-avatar">
+							{{ conversation.userProfile.nick || conversation.userProfile.userID.charAt(0) }}
+						</view>
+						<view class="message-body">
+							<text class="message-type">{{ conversation.userProfile.nick || conversation.userProfile.userID }}</text>
+							<text class="message-content">{{ getLastMessageText(conversation.lastMessage) }}</text>
+						</view>
+						<view class="message-right">
+							<text class="message-time">{{ formatTime(conversation.lastMessage.lastTime) }}</text>
+							<view v-if="conversation.unreadCount > 0" class="unread-badge">
+								{{ conversation.unreadCount > 99 ? '99+' : conversation.unreadCount }}
+							</view>
 						</view>
 					</view>
 				</view>
@@ -64,7 +152,9 @@
 			</view>
 			<view class="tabbar-item message active" @click="navigateToMessage">
 				<view class="tabbar-icon">
-					<view class="badge">3</view>
+					<view v-if="totalUnreadCount > 0" class="badge">
+						{{ totalUnreadCount > 99 ? '99+' : totalUnreadCount }}
+					</view>
 				</view>
 				<text class="tabbar-text">消息</text>
 			</view>
@@ -79,6 +169,7 @@
 <script>
 	import timManager from '@/utils/tim-manager.js'
 	import TIM from 'tim-wx-sdk'
+	import api from '@/utils/api.js'
 	
 	export default {
 		data() {
@@ -87,12 +178,56 @@
 				conversationList: [],
 				matchmakerInfo: null,
 				imUserId: '',
-				totalUnreadCount: 0
+				totalUnreadCount: 0,
+				// 系统消息未读数
+				systemUnread: 0,
+				
+				// 顶部系统通知占位(后续可从后端加载)
+				systemNotification: {
+					title: '系统通知',
+					content: '您的线索审核已通过,获得20积分奖励,当期积分可兑换【资源查看权限】x1',
+					footer: '完成线索录采集,积分+20(距离黄金级还差72分)',
+					timeText: '刚刚'
+				},
+				// 撮合成功通知占位
+				matchSuccessNotification: {
+					title: '撮合成功通知',
+					content: '您推荐的李先生和王女士已成功匹配,获得50积分+100元现金奖励',
+					timeText: '10分钟前'
+				}
+			}
+		},
+		computed: {
+			// 今天的会话
+			todayConversations() {
+				const today = new Date()
+				return this.conversationList.filter(conv => this.isSameDay(conv.lastMessage, today))
+			},
+			// 昨日会话
+			yesterdayConversations() {
+				const yesterday = new Date()
+				yesterday.setDate(yesterday.getDate() - 1)
+				return this.conversationList.filter(conv => this.isSameDay(conv.lastMessage, yesterday))
+			},
+			// 更早会话
+			earlierConversations() {
+				const today = new Date()
+				const yesterday = new Date()
+				yesterday.setDate(yesterday.getDate() - 1)
+				return this.conversationList.filter(conv => {
+					const msg = conv.lastMessage
+					if (!msg || !msg.lastTime) return false
+					const isToday = this.isSameDay(msg, today)
+					const isYesterday = this.isSameDay(msg, yesterday)
+					return !isToday && !isYesterday
+				})
 			}
 		},
 		
-		async onLoad() {
-			await this.initIM()
+		onLoad() {
+			// 为避免误用用户端 IM 账号,这里不再复用当前 TIM 登录状态,进入页面即按红娘流程重新初始化
+			console.log('🔍 红娘消息页强制按红娘流程初始化 IM')
+			this.initIM()
 		},
 		
 		onShow() {
@@ -100,6 +235,8 @@
 			if (this.imUserId) {
 				this.loadConversationList()
 			}
+			// 同步系统消息未读
+			this.loadSystemUnread()
 		},
 		
 		onUnload() {
@@ -108,6 +245,77 @@
 		},
 		
 		methods: {
+			// 判断消息是否与给定日期同一天
+			isSameDay(lastMessage, baseDate) {
+				if (!lastMessage || !lastMessage.lastTime) return false
+				const msgDate = new Date(lastMessage.lastTime * 1000)
+				const d = baseDate instanceof Date ? baseDate : new Date(baseDate)
+				return (
+					msgDate.getFullYear() === d.getFullYear() &&
+					msgDate.getMonth() === d.getMonth() &&
+					msgDate.getDate() === d.getDate()
+				)
+			},
+
+			// 加载系统消息未读数,并同步最新一条系统通知到顶部卡片
+			async loadSystemUnread() {
+				try {
+					const userId = uni.getStorageSync('userId')
+					if (!userId) return
+					const count = await api.message.getSystemUnreadCount(userId)
+					// getSystemUnreadCount 可能返回 {code,data} 或直接 number,这里统一兼容
+					if (typeof count === 'number') {
+						this.systemUnread = count
+					} else if (count && typeof count.data === 'number') {
+						this.systemUnread = count.data
+					}
+					
+					// 同时拉取最新一条系统通知,用于顶部卡片展示
+					const res = await api.message.getSystemList(userId, 1, 1)
+					const list = (res && (res.list || res.data?.list)) || []
+					if (list.length > 0) {
+						const item = list[0]
+						// 计算时间文案
+						let timeText = ''
+						const t = item.createdAt || item.created_at
+						if (t) {
+							const d2 = new Date(t)
+							const now = new Date()
+							const diff = now - d2
+							if (diff < 60000) {
+								timeText = '刚刚'
+							} else if (diff < 3600000) {
+								timeText = Math.floor(diff / 60000) + '分钟前'
+							} else {
+								const mm = String(d2.getMonth() + 1).padStart(2, '0')
+								const dd = String(d2.getDate()).padStart(2, '0')
+								const hh = String(d2.getHours()).padStart(2, '0')
+								const mi = String(d2.getMinutes()).padStart(2, '0')
+								timeText = `${mm}-${dd} ${hh}:${mi}`
+							}
+						}
+						
+						// 用最新系统通知更新卡片文案(小字显示最新内容)
+						this.systemNotification = {
+							...this.systemNotification,
+							title: '系统通知',
+							content: item.title || this.systemNotification.content,
+							footer: item.content || this.systemNotification.footer,
+							timeText: timeText || this.systemNotification.timeText
+						}
+					}
+				} catch (e) {
+					console.log('红娘消息页加载系统未读失败', e)
+				}
+			},
+
+			// 跳转系统通知列表
+			openSystemMessages() {
+				uni.navigateTo({
+					url: '/pages/matchmaker-workbench/system-messages'
+				})
+			},
+
 			// 初始化 IM
 			async initIM() {
 				try {
@@ -181,13 +389,21 @@
 			// 加载会话列表
 			async loadConversationList() {
 				try {
+					// SDK 未登录 / 未 ready 时不调用会话列表接口,避免报“接口调用时机不合理”
+					if (!timManager.isLogin || !timManager.tim) {
+						console.log('⚠️ TIM 未 ready,暂不加载会话列表')
+						return
+					}
 					const tim = timManager.getTim()
 					const { data } = await tim.getConversationList()
-					this.conversationList = data.conversationList
+					this.conversationList = data.conversationList || []
+					
+					// 补全用户头像和昵称
+					await this.loadUserAvatars()
 					
 					// 计算总未读数
 					this.totalUnreadCount = this.conversationList.reduce((total, conv) => {
-						return total + conv.unreadCount
+						return total + (conv.unreadCount || 0)
 					}, 0)
 					
 					console.log('✅ 会话列表:', this.conversationList)
@@ -224,12 +440,84 @@
 			// 会话列表更新
 			onConversationListUpdated(event) {
 				console.log('🔄 会话列表更新:', event.data)
-				this.conversationList = event.data
+				this.conversationList = event.data || []
 				
-				// 更新总未读数
-				this.totalUnreadCount = this.conversationList.reduce((total, conv) => {
-					return total + conv.unreadCount
-				}, 0)
+				// 补全头像昵称后再计算未读
+				this.loadUserAvatars().then(() => {
+					this.totalUnreadCount = this.conversationList.reduce((total, conv) => {
+						return total + (conv.unreadCount || 0)
+					}, 0)
+				})
+			},
+			
+			// 批量补全用户头像和昵称
+			async loadUserAvatars() {
+				try {
+					if (!this.conversationList || this.conversationList.length === 0) {
+						return
+					}
+					
+					// 收集对端用户ID(排除红娘 m_ 开头的ID)
+					const userIds = []
+					this.conversationList.forEach(conv => {
+						let targetUserId = ''
+						if (conv.userProfile && conv.userProfile.userID) {
+							targetUserId = String(conv.userProfile.userID)
+						} else if (conv.conversationID && conv.conversationID.indexOf('C2C') === 0) {
+							targetUserId = conv.conversationID.replace('C2C', '')
+						}
+						if (!targetUserId) return
+						if (targetUserId.startsWith('m_')) return
+						userIds.push(targetUserId)
+					})
+					
+					const uniqueIds = Array.from(new Set(userIds))
+					if (uniqueIds.length === 0) return
+					
+					console.log('🔄 红娘消息页批量获取用户信息,数量:', uniqueIds.length)
+					
+					const res = await uni.request({
+						url: 'http://localhost:8083/api/user/batch',
+						method: 'GET',
+						data: {
+							userIds: uniqueIds.join(',')
+						}
+					})
+					
+					if (!res[1] || res[1].statusCode !== 200 || res[1].data.code !== 200) {
+						console.warn('⚠️ 红娘消息页批量获取用户信息失败')
+						return
+					}
+					
+					const list = res[1].data.data || []
+					const userMap = {}
+					list.forEach(user => {
+						if (!user || user.userId == null) return
+						userMap[String(user.userId)] = {
+							nickname: user.nickname,
+							avatarUrl: user.avatarUrl
+						}
+					})
+					
+					// 回填到会话列表的 userProfile 中
+					this.conversationList = this.conversationList.map(conv => {
+						let targetUserId = ''
+						if (conv.userProfile && conv.userProfile.userID) {
+							targetUserId = String(conv.userProfile.userID)
+						} else if (conv.conversationID && conv.conversationID.indexOf('C2C') === 0) {
+							targetUserId = conv.conversationID.replace('C2C', '')
+						}
+						const info = targetUserId ? userMap[targetUserId] : null
+						if (info) {
+							conv.userProfile = conv.userProfile || {}
+							conv.userProfile.nick = info.nickname || conv.userProfile.nick || `用户${targetUserId}`
+							conv.userProfile.avatar = info.avatarUrl || conv.userProfile.avatar || ''
+						}
+						return conv
+					})
+				} catch (error) {
+					console.error('❌ 红娘消息页加载用户头像失败:', error)
+				}
 			},
 			
 			// 打开聊天页面

+ 238 - 60
LiangZhiYUMao/pages/matchmaker-workbench/my-resources.vue

@@ -16,9 +16,77 @@
 			</view>
 		</view>
 
-		<scroll-view scroll-y class="content">
-			<!-- 资源列表 -->
-			<view class="resource-item" v-for="(item, index) in resources" :key="index" @click="handleResourceClick(item)">
+	<scroll-view scroll-y class="content">
+		<!-- 资源部分(已注册用户) -->
+		<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>
+			</view>
+			<view class="resource-item" v-for="(item, index) in registeredResources" :key="item.id" @click="handleResourceClick(item)">
+				<!-- 右上角选中图标 -->
+				<view class="select-icon">✓-</view>
+				
+				<view class="resource-header">
+					<image 
+						:src="item.avatar" 
+						mode="aspectFill" 
+						class="resource-avatar"
+						@error="handleImageErrorRegistered(index)"
+						@load="handleImageLoadRegistered(index)"
+						:lazy-load="true"
+					></image>
+					<view class="resource-basic-info">
+						<view class="name-gender">
+							<text class="resource-name">{{ item.name }}</text>
+							<text class="resource-gender gender-tag">{{ item.gender }}</text>
+						</view>
+						<view class="status-tag-wrapper">
+							<text class="status-tag register-tag registered">已注册</text>
+							<text class="status-tag match-tag" :class="{ 'matched': item.isMatch === 1, 'unmatched': item.isMatch === 0 || !item.isMatch }">
+								{{ item.isMatch === 1 ? '已匹配' : '未匹配' }}
+							</text>
+						</view>
+					</view>
+				</view>
+
+				<view class="resource-details">
+					<view class="labels">
+						<text class="label" v-for="(label, idx) in item.labels" :key="idx">{{ label }}</text>
+					</view>
+
+					<view class="requirement-box">
+						<text class="requirement-label">择偶要求:</text>
+						<text class="requirement-content">{{ item.requirement }}</text>
+					</view>
+
+					<view class="contact-box">
+						<text class="contact-label">联系方式:</text>
+						<text class="contact-number">{{ item.contact }}</text>
+						<text class="copy-btn" @click.stop="handleCopyContact(item.originalPhone || item.contact)">复制</text>
+					</view>
+
+					<view class="action-buttons">
+						<view class="delete-btn" @click.stop="handleDelete(item.id)">删除</view>
+						<view class="match-btn" @click.stop="handleMatch(item.id)">精准匹配</view>
+					</view>
+				</view>
+
+				<!-- 优质资源标签 -->
+				<view class="quality-tag" v-if="item.isQuality">
+					<text class="quality-star">★</text>
+					<text class="quality-text">优质资源</text>
+				</view>
+			</view>
+		</view>
+
+		<!-- 线索部分(未注册用户) -->
+		<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>
+			</view>
+			<view class="resource-item" v-for="(item, index) in unregisteredResources" :key="item.id" @click="handleResourceClick(item)">
 				<!-- 右上角选中图标 -->
 				<view class="select-icon">✓-</view>
 				
@@ -27,8 +95,8 @@
 						:src="item.avatar" 
 						mode="aspectFill" 
 						class="resource-avatar"
-						@error="handleImageError(index)"
-						@load="handleImageLoad(index)"
+						@error="handleImageErrorUnregistered(index)"
+						@load="handleImageLoadUnregistered(index)"
 						:lazy-load="true"
 					></image>
 					<view class="resource-basic-info">
@@ -37,7 +105,7 @@
 							<text class="resource-gender gender-tag">{{ item.gender }}</text>
 						</view>
 						<view class="status-tag-wrapper">
-							<text class="status-tag register-tag" :class="{ 'registered': item.isUser === 1, 'unregistered': item.isUser === 0 }">{{ item.isUser === 1 ? '已注册' : '未注册' }}</text>
+							<text class="status-tag register-tag unregistered">未注册</text>
 						</view>
 					</view>
 				</view>
@@ -70,7 +138,13 @@
 					<text class="quality-text">优质资源</text>
 				</view>
 			</view>
-		</scroll-view>
+		</view>
+
+		<!-- 空状态 -->
+		<view class="empty-state" v-if="registeredResources.length === 0 && unregisteredResources.length === 0">
+			<text class="empty-text">暂无资源</text>
+		</view>
+	</scroll-view>
 
 		<!-- 底部添加按钮 -->
 		<view class="add-button" @click="handleAdd">
@@ -113,44 +187,10 @@ export default {
 			return {
 				searchKeyword: '',
 				isFirstLoad: true, // 标记是否为首次加载
-				resources: [
-					{
-						id: 1,
-						avatar: 'https://example.com/avatar1.jpg',
-						name: '小高',
-						gender: '男',
-						status: '已审核',
-						isPlus: true,
-						labels: ['气质男', '小清新'],
-						requirement: '165+ 本科',
-						contact: '123****8912',
-						isQuality: false
-					},
-					{
-						id: 2,
-						avatar: 'https://example.com/avatar2.jpg',
-						name: '小美',
-						gender: '女',
-						status: '已审核',
-						isPlus: true,
-						labels: ['温柔', '知性', '爱读书'],
-						requirement: '175+ 硕士 有房',
-						contact: '138****6543',
-						isQuality: false
-					},
-					{
-						id: 3,
-						avatar: 'https://example.com/avatar3.jpg',
-						name: '阿强',
-						gender: '男',
-						status: '待审核',
-						isPlus: false,
-						labels: ['阳光', '运动型'],
-						requirement: '160+ 大专以上',
-						contact: '159****2234',
-						isQuality: true
-					}
-				]
+				resources: [], // 保留用于兼容
+				registeredResources: [], // 已注册用户(资源部分)
+				unregisteredResources: [], // 未注册用户(线索部分)
+				refreshTimer: null // 定时刷新器
 			}
 		},
 		onLoad() {
@@ -163,16 +203,26 @@ export default {
 				console.log('收到刷新资源列表事件')
 				this.loadMyResources()
 			})
+			
+			// 启动定时刷新,每30秒检查一次用户注册状态变化
+			this.startAutoRefresh()
 		},
 		onShow() {
 			// 页面显示时刷新列表(从其他页面返回时,非首次加载)
 			if (!this.isFirstLoad) {
 				this.loadMyResources()
 			}
+			// 重新启动定时刷新
+			this.startAutoRefresh()
+		},
+		onHide() {
+			// 页面隐藏时停止定时刷新
+			this.stopAutoRefresh()
 		},
 		onUnload() {
-			// 页面卸载时移除事件监听
+			// 页面卸载时移除事件监听和停止定时刷新
 			uni.$off('refreshResourceList')
+			this.stopAutoRefresh()
 		},
 		methods: {
 			// 加载我的资源数据
@@ -232,7 +282,7 @@ export default {
 								console.log('第一条记录的完整数据:', JSON.stringify(pageData.records[0], null, 2))
 							}
 							// 将后端数据转换为前端需要的格式
-							this.resources = pageData.records.map(item => {
+							const allResources = pageData.records.map(item => {
 								// 直接使用mate_selection_criteria字段作为择偶要求
 								// 支持多种可能的字段名(mateSelectionCriteria或mate_selection_criteria)
 								let requirement = item.mateSelectionCriteria || item.mate_selection_criteria || ''
@@ -319,6 +369,11 @@ export default {
 								// 确保isUser是数字类型
 								isUser = parseInt(isUser) || 0
 								
+								// 处理isMatch字段
+								let isMatch = item.isMatch !== null && item.isMatch !== undefined ? item.isMatch : 
+								              (item.is_match !== null && item.is_match !== undefined ? item.is_match : 0)
+								isMatch = parseInt(isMatch) || 0
+								
 								// 调试日志:检查isUser字段
 								console.log('=== isUser字段处理 ===')
 								console.log('资源ID:', resourceId, '姓名:', item.name)
@@ -347,6 +402,7 @@ export default {
 									gender: item.gender === 1 ? '男' : item.gender === 2 ? '女' : '未知',
 									status: '已审核', // 可以根据实际字段判断
 									isUser: isUser, // 添加isUser字段,默认为0
+									isMatch: isMatch, // 添加isMatch字段,默认为0
 									isPlus: false,
 									labels: resourceTags, // 使用处理后的标签列表
 									requirement: requirement || '暂无要求',
@@ -356,7 +412,14 @@ export default {
 								}
 							}).filter(item => item !== null) // 过滤掉无效的资源(resourceId无效的)
 							
-							console.log('处理后的资源列表:', this.resources)
+							// 将资源分为两部分:已注册和未注册
+							this.registeredResources = allResources.filter(item => item.isUser === 1)
+							this.unregisteredResources = allResources.filter(item => item.isUser === 0)
+							
+							// 保留resources用于兼容(包含所有资源)
+							this.resources = allResources
+							
+							console.log('处理后的资源列表 - 已注册:', this.registeredResources.length, '未注册:', this.unregisteredResources.length)
 						}
 					}
 				} catch (e) {
@@ -449,10 +512,32 @@ export default {
 		},
 		// 精准匹配
 		handleMatch(id) {
-			// 实现精准匹配功能
+			// 查找资源项
+			const allResources = [...this.registeredResources, ...this.unregisteredResources]
+			const resource = allResources.find(item => item.id === id)
+			
+			if (!resource) {
+				uni.showToast({
+					title: '资源不存在',
+					icon: 'none'
+				})
+				return
+			}
+			
+			// 检查用户是否已注册
+			if (resource.isUser !== 1) {
+				uni.showToast({
+					title: '该用户还未注册用户端,注册之后才能进行匹配',
+					icon: 'none',
+					duration: 3000
+				})
+				return
+			}
+			
+			// 已注册用户,跳转到精准匹配页面
 			console.log('精准匹配:', id)
 			uni.navigateTo({
-				url: `/pages/matchmaker-workbench/precise-match?id=${id}`
+				url: `/pages/matchmaker-workbench/precise-match?resourceId=${id}`
 			})
 		},
 		// 复制联系方式
@@ -565,18 +650,53 @@ export default {
 				url: '/pages/matchmaker-workbench/mine'
 			})
 		},
-		// 图片加载错误处理
-		handleImageError(index) {
+		// 启动自动刷新
+		startAutoRefresh() {
+			// 清除之前的定时器
+			this.stopAutoRefresh()
+			// 每30秒刷新一次,检查用户注册状态变化
+			this.refreshTimer = setInterval(() => {
+				console.log('定时刷新:检查用户注册状态变化')
+				this.loadMyResources()
+			}, 30000) // 30秒
+		},
+		// 停止自动刷新
+		stopAutoRefresh() {
+			if (this.refreshTimer) {
+				clearInterval(this.refreshTimer)
+				this.refreshTimer = null
+			}
+		},
+		// 已注册资源图片加载错误处理
+		handleImageErrorRegistered(index) {
+			this.handleImageError(index, 'registered')
+		},
+		// 未注册资源图片加载错误处理
+		handleImageErrorUnregistered(index) {
+			this.handleImageError(index, 'unregistered')
+		},
+		// 已注册资源图片加载成功处理
+		handleImageLoadRegistered(index) {
+			this.handleImageLoad(index, 'registered')
+		},
+		// 未注册资源图片加载成功处理
+		handleImageLoadUnregistered(index) {
+			this.handleImageLoad(index, 'unregistered')
+		},
+		// 图片加载错误处理(内部方法)
+		handleImageError(index, type) {
 			try {
-				const resource = this.resources && this.resources[index]
+				const resourceList = type === 'registered' ? this.registeredResources : this.unregisteredResources
+				const resource = resourceList && resourceList[index]
 				if (!resource) {
-					console.warn('图片加载失败:资源不存在,index:', index)
+					console.warn('图片加载失败:资源不存在,index:', index, 'type:', type)
 					return
 				}
 				
 				const originalUrl = resource.avatar
 				console.error('❌ 图片加载失败:', {
 					index: index,
+					type: type,
 					resourceName: resource.name,
 					resourceId: resource.id,
 					originalAvatarUrl: originalUrl,
@@ -595,7 +715,11 @@ export default {
 					!originalUrl.includes('via.placeholder')) {
 					// 设置为空字符串,让CSS默认背景显示
 					console.log('图片加载失败,将URL设置为空,显示CSS默认背景')
-					this.$set(this.resources[index], 'avatar', '')
+					if (type === 'registered') {
+						this.$set(this.registeredResources[index], 'avatar', '')
+					} else {
+						this.$set(this.unregisteredResources[index], 'avatar', '')
+					}
 				} else {
 					console.log('图片加载失败,但URL已经是占位图或空,无需处理')
 					console.log('当前URL:', originalUrl)
@@ -604,14 +728,16 @@ export default {
 				console.error('处理图片错误时发生异常:', e)
 			}
 		},
-		// 图片加载成功处理
-		handleImageLoad(index) {
+		// 图片加载成功处理(内部方法)
+		handleImageLoad(index, type) {
 			try {
-				if (this.resources && this.resources[index]) {
+				const resourceList = type === 'registered' ? this.registeredResources : this.unregisteredResources
+				if (resourceList && resourceList[index]) {
 					console.log('图片加载成功:', {
 						index: index,
-						resource: this.resources[index]?.name,
-						avatarUrl: this.resources[index]?.avatar
+						type: type,
+						resource: resourceList[index]?.name,
+						avatarUrl: resourceList[index]?.avatar
 					})
 				}
 			} catch (e) {
@@ -698,6 +824,44 @@ export default {
 		padding: 0 20rpx 140rpx;
 	}
 
+	/* 资源分组 */
+	.resource-section {
+		margin-bottom: 30rpx;
+		
+		.section-header {
+			display: flex;
+			align-items: center;
+			padding: 20rpx 0 15rpx;
+			margin-bottom: 10rpx;
+			
+			.section-title {
+				font-size: 32rpx;
+				font-weight: bold;
+				color: #9C27B0;
+				margin-right: 10rpx;
+			}
+			
+			.section-count {
+				font-size: 26rpx;
+				color: #999;
+				font-weight: normal;
+			}
+		}
+	}
+
+	/* 空状态 */
+	.empty-state {
+		display: flex;
+		justify-content: center;
+		align-items: center;
+		padding: 100rpx 0;
+		
+		.empty-text {
+			font-size: 28rpx;
+			color: #999;
+		}
+	}
+
 	/* 资源列表 */
 	.resource-item {
 		background: #FFFFFF;
@@ -809,6 +973,20 @@ export default {
 								color: #E91E63;
 							}
 						}
+						
+						&.match-tag {
+							margin-left: 10rpx;
+							
+							&.matched {
+								background: #E3F2FD;
+								color: #2196F3;
+							}
+							
+							&.unmatched {
+								background: #FFF3E0;
+								color: #FF9800;
+							}
+						}
 					}
 				}
 			}

+ 253 - 49
LiangZhiYUMao/pages/matchmaker-workbench/precise-match.vue

@@ -7,9 +7,40 @@
 			<view class="header-right"></view>
 		</view>
 
+		<!-- 加载中 -->
+		<view class="loading-container" v-if="loading">
+			<text class="loading-text">正在匹配中...</text>
+		</view>
+
+		<!-- 资源跟进状态 -->
+		<view class="follow-status-section" v-if="!loading && resourceFollowInfo">
+			<!-- 匹配状态 -->
+			<view class="follow-status-group">
+				<text class="status-label">匹配状态</text>
+				<view class="status-tags">
+					<text class="status-tag" :class="{ 'active': resourceFollowInfo.isMatch === 1 }">
+						{{ resourceFollowInfo.isMatch === 1 ? '已匹配' : '未匹配' }}
+					</text>
+				</view>
+			</view>
+			<!-- 跟进进度 -->
+			<view class="follow-status-group">
+				<text class="status-label">跟进进度</text>
+				<view class="status-tags">
+					<text class="status-tag" :class="{ 'active': resourceFollowInfo.isInvitation === 1 }">
+						{{ resourceFollowInfo.isInvitation === 1 ? '已邀约' : '未邀约' }}
+					</text>
+				</view>
+			</view>
+		</view>
+
 		<!-- 匹配客户列表 -->
-		<scroll-view scroll-y class="match-list">
-			<view class="match-client" v-for="(client, index) in matchClients" :key="index">
+		<scroll-view scroll-y class="match-list" v-else>
+			<view class="match-client" v-for="(client, index) in matchClients" :key="client.id || index">
+				<!-- 匹配度标签 -->
+				<view class="match-score-badge" v-if="client.matchScore">
+					<text class="score-text">匹配度: {{ Math.round(client.matchScore) }}%</text>
+				</view>
 				<view class="client-info">
 					<image :src="client.avatar" class="client-avatar" mode="aspectFill"></image>
 					<view class="info-content">
@@ -21,6 +52,13 @@
 						<view class="client-tags">
 							<text class="tag" v-for="(tag, tagIndex) in client.tags" :key="tagIndex">{{ tag }}</text>
 						</view>
+						<view class="user-details" v-if="client.age || client.height || client.diploma">
+							<text class="detail-item" v-if="client.age">{{ client.age }}岁</text>
+							<text class="detail-item" v-if="client.height">{{ client.height }}cm</text>
+							<text class="detail-item" v-if="client.diploma">{{ client.diploma }}</text>
+							<text class="detail-item" v-if="client.income">{{ client.income }}</text>
+							<text class="detail-item" v-if="client.occupation">{{ client.occupation }}</text>
+						</view>
 						<view class="requirement">
 							<text class="requirement-label">择偶要求:</text>
 							<text class="requirement-content">{{ client.requirement }}</text>
@@ -67,48 +105,31 @@
 <script>
 export default {
 	name: 'precise-match',
-	data() {
-		return {
-			matchClients: [
-				{
-					id: 1,
-					name: '小高',
-					gender: '女',
-					status: '未匹配',
-					tags: ['气质男', '小清新'],
-					avatar: 'https://images.unsplash.com/photo-1506744038136-46273834b3fb?ixlib=rb-1.2.1&auto=format&fit=crop&w=200&q=60',
-					requirement: '165+ 本科',
-					contact: '123****8912'
-				},
-				{
-					id: 2,
-					name: '小子',
-					gender: '女',
-					status: '未匹配',
-					tags: ['周末有空'],
-					avatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?ixlib=rb-1.2.1&auto=format&fit=crop&w=200&q=60',
-					requirement: '165+ 本科',
-					contact: '123****8912'
-				},
-				{
-					id: 3,
-					name: '小博',
-					gender: '女',
-					status: '未匹配',
-					tags: ['电话联系', '运动'],
-					avatar: 'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?ixlib=rb-1.2.1&auto=format&fit=crop&w=200&q=60',
-					requirement: '180+ 本科',
-					contact: '123****8912'
-				}
-			]
-		}
-	},
+		data() {
+			return {
+				resourceId: null,
+				matchClients: [],
+				loading: false,
+				resourceFollowInfo: null // 资源跟进信息
+			}
+		},
 	onLoad(options) {
-		// 从URL参数获取客户ID
-		if (options.id) {
-			// 这里可以根据ID从API获取匹配客户信息
-			console.log('当前客户ID:', options.id)
-			// this.loadMatchClients(options.id)
+		// 从URL参数获取资源ID
+		if (options.resourceId) {
+			console.log('当前资源ID:', options.resourceId)
+			this.loadMatchClients(options.resourceId)
+		} else if (options.id) {
+			// 兼容旧的参数名
+			console.log('当前资源ID (兼容):', options.id)
+			this.loadMatchClients(options.id)
+		} else {
+			uni.showToast({
+				title: '缺少资源ID参数',
+				icon: 'none'
+			})
+			setTimeout(() => {
+				uni.navigateBack()
+			}, 1500)
 		}
 	},
 	methods: {
@@ -137,15 +158,99 @@ export default {
 			})
 		},
 		// 从API加载匹配客户信息
-		async loadMatchClients(id) {
+		async loadMatchClients(resourceId) {
+			if (!resourceId) {
+				console.error('资源ID为空')
+				return
+			}
+			
+			this.resourceId = resourceId
+			this.loading = true
+			
 			try {
-				// 这里调用API获取匹配客户信息
-				// const res = await api.matchmaker.getPreciseMatch(id)
-				// this.matchClients = res.data
+				const baseUrl = process.env.NODE_ENV === 'development' 
+					? 'http://localhost:8083/api'  // 开发环境 - 通过网关
+					: 'https://your-domain.com/api'  // 生产环境
+				
+				// 同时加载匹配结果和资源跟进信息
+				const [matchError, matchRes] = await uni.request({
+					url: `${baseUrl}/my-resource/precise-match/${resourceId}`,
+					method: 'GET'
+				})
+				
+				// 加载资源跟进信息
+				const [followError, followRes] = await uni.request({
+					url: `${baseUrl}/my-resource/client-detail/${resourceId}`,
+					method: 'GET'
+				})
+				
+				this.loading = false
+				
+				// 处理跟进信息
+				if (!followError && followRes.statusCode === 200 && followRes.data && followRes.data.code === 200) {
+					const followData = followRes.data.data
+					this.resourceFollowInfo = {
+						isMatch: followData.isMatch !== null && followData.isMatch !== undefined ? followData.isMatch : 
+						        (followData.is_match !== null && followData.is_match !== undefined ? followData.is_match : 0),
+						isInvitation: followData.isInvitation !== null && followData.isInvitation !== undefined ? followData.isInvitation : 
+						             (followData.is_Invitation !== null && followData.is_Invitation !== undefined ? followData.is_Invitation : 0)
+					}
+				}
+				
+				if (matchError) {
+					console.error('加载匹配客户失败:', matchError)
+					uni.showToast({
+						title: '加载失败',
+						icon: 'none'
+					})
+					return
+				}
+				
+				if (matchRes.statusCode === 200 && matchRes.data && matchRes.data.code === 200) {
+					const matchList = matchRes.data.data || []
+					
+					// 转换数据格式
+					this.matchClients = matchList.map(item => {
+						return {
+							id: item.userId,
+							name: item.nickname || '未知',
+							gender: item.gender === 1 ? '男' : item.gender === 2 ? '女' : '未知',
+							status: '未匹配',
+							tags: item.tags || [],
+							avatar: item.avatarUrl || 'https://via.placeholder.com/150',
+							requirement: item.requirement || '暂无要求',
+							contact: item.phone || '',
+							matchScore: item.matchScore || 0,
+							age: item.age,
+							height: item.height,
+							diploma: item.diploma,
+							income: item.income,
+							occupation: item.occupation,
+							address: item.address
+						}
+					})
+					
+					console.log('匹配结果:', this.matchClients)
+					
+					if (this.matchClients.length === 0) {
+						uni.showToast({
+							title: '未找到匹配的用户',
+							icon: 'none'
+						})
+					}
+				} else {
+					const errorMsg = matchRes.data?.message || '加载失败'
+					console.error('加载匹配客户失败:', errorMsg)
+					uni.showToast({
+						title: errorMsg,
+						icon: 'none'
+					})
+				}
 			} catch (e) {
-				console.error('加载匹配客户失败:', e)
+				this.loading = false
+				console.error('加载匹配客户异常:', e)
 				uni.showToast({
-					title: '加载失败',
+					title: '加载失败,请稍后重试',
 					icon: 'none'
 				})
 			}
@@ -309,6 +414,30 @@ export default {
 							color: #333;
 						}
 					}
+					
+					.user-details {
+						display: flex;
+						flex-wrap: wrap;
+						gap: 10rpx;
+						margin-bottom: 15rpx;
+						font-size: 24rpx;
+						color: #999;
+						
+						.detail-item {
+							display: flex;
+							align-items: center;
+							
+							&::after {
+								content: '|';
+								margin: 0 10rpx;
+								color: #ddd;
+							}
+							
+							&:last-child::after {
+								display: none;
+							}
+						}
+					}
 
 					.contact-info {
 						display: flex;
@@ -350,6 +479,81 @@ export default {
 				font-weight: bold;
 				margin-left: auto;
 			}
+			
+			.match-score-badge {
+				position: absolute;
+				top: 20rpx;
+				right: 20rpx;
+				background: linear-gradient(135deg, #FF6B8A 0%, #FF8E9B 100%);
+				color: #FFFFFF;
+				padding: 8rpx 16rpx;
+				border-radius: 20rpx;
+				font-size: 22rpx;
+				font-weight: bold;
+				z-index: 10;
+				
+				.score-text {
+					color: #FFFFFF;
+				}
+			}
+		}
+	}
+	
+	/* 加载中 */
+	.loading-container {
+		display: flex;
+		justify-content: center;
+		align-items: center;
+		padding: 100rpx 0;
+		
+		.loading-text {
+			font-size: 28rpx;
+			color: #999;
+		}
+	}
+
+	/* 资源跟进状态区域 */
+	.follow-status-section {
+		background-color: #FFFFFF;
+		margin: 20rpx;
+		padding: 30rpx;
+		border-radius: 20rpx;
+		box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
+		display: flex;
+		justify-content: space-around;
+		align-items: center;
+	}
+
+	.follow-status-group {
+		display: flex;
+		flex-direction: column;
+		align-items: center;
+		gap: 12rpx;
+	}
+
+	.status-label {
+		font-size: 26rpx;
+		color: #666;
+		font-weight: 500;
+	}
+
+	.status-tags {
+		display: flex;
+		gap: 10rpx;
+	}
+
+	.status-tag {
+		padding: 8rpx 20rpx;
+		border-radius: 20rpx;
+		font-size: 24rpx;
+		background-color: #FAF5FF;
+		color: #9C27B0;
+		border: 2rpx solid #E1BEE7;
+
+		&.active {
+			background-color: #F8BBD0;
+			border-color: #F06292;
+			color: #FFFFFF;
 		}
 	}
 

+ 289 - 0
LiangZhiYUMao/pages/matchmaker-workbench/system-messages.vue

@@ -0,0 +1,289 @@
+<template>
+  <view class="matchmaker-system-page">
+    <!-- 顶部导航栏 -->
+    <view class="header">
+      <view class="back-btn" @click="goBack"></view>
+      <text class="header-title">系统通知</text>
+      <view class="placeholder">
+        <button class="read-all-btn" @click.stop="markAllRead" size="mini">一键已读</button>
+      </view>
+    </view>
+
+    <!-- 列表区域 -->
+    <scroll-view scroll-y class="content" @scrolltolower="loadMore">
+      <view v-if="loading && list.length === 0" class="loading-container">
+        <text>加载中...</text>
+      </view>
+
+      <view v-else-if="list.length === 0" class="empty-container">
+        <text>暂无系统通知</text>
+      </view>
+
+      <view v-else>
+        <view
+          v-for="item in list"
+          :key="item.id"
+          class="system-item"
+          @click="openDetail(item)"
+        >
+          <view class="item-left">
+            <view class="icon-wrapper">
+              <text class="icon">📢</text>
+              <view v-if="item.isRead === 0" class="dot"></view>
+            </view>
+          </view>
+          <view class="item-body">
+            <view class="item-title-row">
+              <text class="item-title">{{ item.title || '系统通知' }}</text>
+              <text class="item-time">{{ formatTime(item.createdAt || item.created_at) }}</text>
+            </view>
+            <text class="item-content">{{ item.content }}</text>
+          </view>
+        </view>
+
+        <view class="load-more" v-if="hasMore">
+          {{ loading ? '加载中...' : '上拉加载更多' }}
+        </view>
+        <view class="load-more" v-else>没有更多了</view>
+      </view>
+    </scroll-view>
+  </view>
+</template>
+
+<script>
+import api from '@/utils/api.js'
+
+export default {
+  data() {
+    return {
+      userId: null,
+      list: [],
+      pageNum: 1,
+      pageSize: 20,
+      hasMore: true,
+      loading: false
+    }
+  },
+  onLoad() {
+    this.userId = uni.getStorageSync('userId')
+    this.loadList()
+  },
+  methods: {
+    async loadList() {
+      if (this.loading || !this.hasMore) return
+      this.loading = true
+      try {
+        const res = await api.message.getSystemList(this.userId, this.pageNum, this.pageSize)
+        const data = (res && (res.list || res.data?.list)) || []
+        this.list = this.list.concat(data)
+        const total = (typeof res.total === 'number') ? res.total : (res.data?.total || this.list.length)
+        this.hasMore = this.list.length < total
+        this.pageNum++
+      } catch (e) {
+        console.log('红娘系统通知加载失败', e)
+      } finally {
+        this.loading = false
+      }
+    },
+    loadMore() {
+      this.loadList()
+    },
+    async markAllRead() {
+      if (!this.userId) return
+      try {
+        await api.message.markAllSystemRead(this.userId)
+        // 本地列表全部置为已读
+        this.list.forEach(item => {
+          item.isRead = 1
+        })
+        uni.showToast({ title: '已全部标记为已读', icon: 'success' })
+      } catch (e) {
+        console.log('一键已读失败', e)
+        uni.showToast({ title: '操作失败,请稍后重试', icon: 'none' })
+      }
+    },
+    async openDetail(item) {
+      try {
+        const data = await api.message.getSystemDetail(item.id)
+        if (item.isRead === 0) {
+          try {
+            await api.message.markSystemRead(item.id)
+            item.isRead = 1
+          } catch (e) {}
+        }
+        uni.showModal({
+          title: data.title || '系统通知',
+          content: (data.content || '').replace(/\n/g, '\n'),
+          showCancel: false
+        })
+      } catch (e) {
+        uni.showToast({ title: '获取详情失败', icon: 'none' })
+      }
+    },
+    formatTime(t) {
+      if (!t) return ''
+      const d = new Date(t)
+      const month = String(d.getMonth() + 1).padStart(2, '0')
+      const day = String(d.getDate()).padStart(2, '0')
+      const hour = String(d.getHours()).padStart(2, '0')
+      const minute = String(d.getMinutes()).padStart(2, '0')
+      return `${month}-${day} ${hour}:${minute}`
+    },
+    goBack() {
+      uni.navigateBack()
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.matchmaker-system-page {
+  min-height: 100vh;
+  background: #FFF9F9;
+  display: flex;
+  flex-direction: column;
+}
+
+.header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 25rpx 30rpx;
+  padding-top: calc(25rpx + env(safe-area-inset-top));
+  background: #FFF9F9;
+  border-bottom: 1rpx solid #F0F0F0;
+
+  .back-btn {
+    width: 70rpx;
+    height: 70rpx;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    background: rgba(240, 240, 240, 0.5);
+    border-radius: 50%;
+    background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23333"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>');
+    background-size: 40rpx 40rpx;
+    background-repeat: no-repeat;
+    background-position: center;
+  }
+
+  .header-title {
+    font-size: 36rpx;
+    font-weight: bold;
+    color: #333;
+  }
+
+  .placeholder {
+    min-width: 120rpx;
+    display: flex;
+    justify-content: flex-end;
+    align-items: center;
+
+    .read-all-btn {
+      min-width: 96rpx;
+      height: 48rpx;
+      line-height: 48rpx;
+      padding: 0 12rpx;
+      font-size: 22rpx;
+      color: #666;
+      background: #ffffff;
+      border-radius: 999rpx;
+      border: 1rpx solid #e5e5e5;
+      box-shadow: 0 4rpx 10rpx rgba(0,0,0,0.04);
+      text-align: center;
+    }
+  }
+}
+
+.content {
+  flex: 1;
+  padding: 20rpx 20rpx 40rpx;
+}
+
+.system-item {
+  display: flex;
+  padding: 24rpx 20rpx;
+  margin-bottom: 20rpx;
+  border-radius: 20rpx;
+  background: #FFFFFF;
+  box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.05);
+}
+
+.item-left {
+  margin-right: 20rpx;
+
+  .icon-wrapper {
+    width: 64rpx;
+    height: 64rpx;
+    border-radius: 50%;
+    background: linear-gradient(135deg, #FCE4EC 0%, #F8BBD0 100%);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    position: relative;
+
+    .icon {
+      font-size: 32rpx;
+    }
+
+    .dot {
+      position: absolute;
+      top: -6rpx;
+      right: -6rpx;
+      width: 18rpx;
+      height: 18rpx;
+      border-radius: 50%;
+      background: #FF4444;
+      border: 2rpx solid #FFFFFF;
+    }
+  }
+}
+
+.item-body {
+  flex: 1;
+  min-width: 0;
+
+  .item-title-row {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 8rpx;
+
+    .item-title {
+      font-size: 30rpx;
+      font-weight: bold;
+      color: #333;
+    }
+
+    .item-time {
+      font-size: 24rpx;
+      color: #999;
+      margin-left: 20rpx;
+      flex-shrink: 0;
+    }
+  }
+
+  .item-content {
+    font-size: 26rpx;
+    color: #666;
+    line-height: 1.5;
+  }
+}
+
+.loading-container,
+.empty-container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  padding: 80rpx 0;
+  color: #999;
+  font-size: 28rpx;
+}
+
+.load-more {
+  text-align: center;
+  padding: 20rpx 0 40rpx;
+  color: #999;
+  font-size: 24rpx;
+}
+</style>

+ 33 - 1
LiangZhiYUMao/pages/message/chat.vue

@@ -398,8 +398,12 @@ export default {
       this.userId = String(rawUserId);
     }
     
-    // 头像:默认先用用户信息中的头像(红娘入口会被 loadMatchmakerAvatarByImId 覆盖)
+    // 头像:默认先用用户信息中的头像(红娘入口会被 loadMatchmakerAvatar 覆盖)
     this.userAvatar = userInfo.avatar || userInfo.avatarUrl || '/static/default-avatar.svg';
+    // 如果是红娘入口,再从红娘后台加载一次头像覆盖默认值
+    if (this.fromMatchmaker) {
+      await this.loadMatchmakerAvatar();
+    }
     
     // 获取对方用户信息(确保是字符串格式)
     this.targetUserId = String(options.targetUserId);
@@ -460,6 +464,34 @@ export default {
   },
 
   methods: {
+    /**
+     * 红娘入口:根据当前登录用户ID获取红娘资料并设置头像
+     */
+    async loadMatchmakerAvatar() {
+      try {
+        const userId = uni.getStorageSync('userId');
+        if (!userId) {
+          console.warn('⚠️ 红娘聊天页:本地无 userId,无法加载红娘头像');
+          return;
+        }
+        const res = await uni.request({
+          url: 'http://localhost:8081/api/matchmaker/current',
+          method: 'GET',
+          data: { userId }
+        });
+        if (!res[1] || res[1].statusCode !== 200 || res[1].data.code !== 200) {
+          console.warn('⚠️ 红娘聊天页:获取红娘信息失败');
+          return;
+        }
+        const info = res[1].data.data || {};
+        if (info.avatarUrl) {
+          this.userAvatar = info.avatarUrl;
+        }
+      } catch (e) {
+        console.error('❌ 红娘聊天页加载头像失败:', e);
+      }
+    },
+
     /**
      * 初始化 TIM
      */

+ 33 - 22
LiangZhiYUMao/utils/tim-presence-manager.js

@@ -6,6 +6,9 @@
 import timManager from './tim-manager.js';
 import TIM from 'tim-wx-sdk';
 
+// WebSocket/TIM 调试日志开关
+const DEBUG_WS = false;
+
 class TIMPresenceManager {
   constructor() {
     this.ws = null;
@@ -40,12 +43,14 @@ class TIMPresenceManager {
   async init(userId) {
     this.userId = userId;
     
-    console.log('🚀 ========== 初始化 tim-presence-manager ==========');
-    console.log('🚀 用户ID:', userId);
-    console.log('🚀 WebSocket URL:', this.wsUrl);
+    if (DEBUG_WS) {
+      console.log('🚀 ========== 初始化 tim-presence-manager ==========');
+      console.log('🚀 用户ID:', userId);
+      console.log('🚀 WebSocket URL:', this.wsUrl);
+    }
     
     // 连接 WebSocket(接收服务端推送的状态变更)
-    console.log('🚀 开始连接 WebSocket...');
+    if (DEBUG_WS) console.log('🚀 开始连接 WebSocket...');
     this.connectWebSocket();
     
     console.log('✅ tim-presence-manager 初始化完成(WebSocket连接中...)');
@@ -58,9 +63,11 @@ class TIMPresenceManager {
   connectWebSocket() {
     try {
       const wsUrl = `${this.wsUrl}?userId=${this.userId}`;
-      console.log('🔌 准备连接 WebSocket:', wsUrl);
-      console.log('   当前用户ID:', this.userId);
-      console.log('   WebSocket URL:', wsUrl);
+      if (DEBUG_WS) {
+        console.log('🔌 准备连接 WebSocket:', wsUrl);
+        console.log('   当前用户ID:', this.userId);
+        console.log('   WebSocket URL:', wsUrl);
+      }
       
       // 先关闭旧连接
       if (this.ws) {
@@ -74,7 +81,7 @@ class TIMPresenceManager {
       this.ws = uni.connectSocket({
         url: wsUrl,
         success: () => {
-          console.log('✅ WebSocket连接请求已发送成功');
+          if (DEBUG_WS) console.log('✅ WebSocket连接请求已发送成功');
         },
         fail: (err) => {
           console.error('❌ WebSocket连接请求发送失败:', err);
@@ -85,11 +92,13 @@ class TIMPresenceManager {
       
       // 使用 SocketTask 对象的监听器(推荐方式)
       this.ws.onOpen((res) => {
-        console.log('🎉 ========== WebSocket 连接成功 ==========');
-        console.log('   响应数据:', res);
-        console.log('   用户ID:', this.userId);
-        console.log('   连接URL:', wsUrl);
-        console.log('==========================================');
+        if (DEBUG_WS) {
+          console.log('🎉 ========== WebSocket 连接成功 ==========');
+          console.log('   响应数据:', res);
+          console.log('   用户ID:', this.userId);
+          console.log('   连接URL:', wsUrl);
+          console.log('==========================================');
+        }
         
         this.isConnected = true;
         this.reconnectCount = 0; // 重置重连计数
@@ -111,7 +120,7 @@ class TIMPresenceManager {
       
       // 监听消息
       this.ws.onMessage((res) => {
-        console.log('📨 收到WebSocket消息:', res.data);
+        if (DEBUG_WS) console.log('📨 收到WebSocket消息:', res.data);
         this.handleMessage(res.data);
       });
       
@@ -136,12 +145,14 @@ class TIMPresenceManager {
       
       // 监听关闭
       this.ws.onClose((res) => {
-        console.log('🔌 ========== WebSocket 连接关闭 ==========');
-        console.log('   关闭信息:', res);
-        console.log('   关闭码:', res?.code);
-        console.log('   关闭原因:', res?.reason);
-        console.log('   用户ID:', this.userId);
-        console.log('=========================================');
+        if (DEBUG_WS) {
+          console.log('🔌 ========== WebSocket 连接关闭 ==========');
+          console.log('   关闭信息:', res);
+          console.log('   关闭码:', res?.code);
+          console.log('   关闭原因:', res?.reason);
+          console.log('   用户ID:', this.userId);
+          console.log('=========================================');
+        }
         
         this.isConnected = false;
         this.stopHeartbeat();
@@ -173,7 +184,7 @@ class TIMPresenceManager {
     
     // 监听 IM 连接状态变化
     this.timConnectListener = (event) => {
-      console.log('📡 TIM 连接状态变更:', event.data.state);
+      if (DEBUG_WS) console.log('📡 TIM 连接状态变更:', event.data.state);
       
       let imStatus = 'offline';
       
@@ -193,7 +204,7 @@ class TIMPresenceManager {
     };
     
     timManager.tim.on(TIM.EVENT.NET_STATE_CHANGE, this.timConnectListener);
-    console.log('✅ 已监听 TIM 连接状态变更');
+    if (DEBUG_WS) console.log('✅ 已监听 TIM 连接状态变更');
   }
   
   /**

+ 17 - 18
LiangZhiYUMao/utils/websocket.js

@@ -3,13 +3,10 @@
  * 实现了心跳机制、断线重连、消息队列等功能
  */
 
+const DEBUG_WS = false;
+
 class WebSocketManager {
   constructor() {
-    this.socket = null;
-    this.isConnected = false;
-    this.userId = null;
-    this.url = '';
-    
     // 心跳相关
     this.heartbeatTimer = null;
     this.heartbeatInterval = 30000; // 30秒
@@ -38,19 +35,19 @@ class WebSocketManager {
    */
   connect(userId, baseUrl = 'ws://localhost:8083') {
     if (this.isConnected) {
-      console.log('WebSocket已连接');
+      if (DEBUG_WS) console.log('WebSocket已连接');
       return;
     }
 
     this.userId = userId;
     this.url = `${baseUrl}/ws/chat?userId=${userId}`;
     
-    console.log('正在连接WebSocket...', this.url);
+    if (DEBUG_WS) console.log('正在连接WebSocket...', this.url);
 
     this.socket = uni.connectSocket({
       url: this.url,
       success: () => {
-        console.log('WebSocket连接请求已发送');
+        if (DEBUG_WS) console.log('WebSocket连接请求已发送');
       },
       fail: (err) => {
         console.error('WebSocket连接失败', err);
@@ -60,7 +57,7 @@ class WebSocketManager {
 
     // 监听连接打开
     uni.onSocketOpen((res) => {
-      console.log('WebSocket连接成功');
+      if (DEBUG_WS) console.log('WebSocket连接成功');
       this.isConnected = true;
       this.reconnectCount = 0;
       
@@ -79,9 +76,9 @@ class WebSocketManager {
     // 监听消息接收
     uni.onSocketMessage((res) => {
       try {
-        console.log('🔵 收到原始WebSocket消息:', res.data);
+        if (DEBUG_WS) console.log('🔵 收到原始WebSocket消息:', res.data);
         const message = JSON.parse(res.data);
-        console.log('🔵 解析后的消息对象:', JSON.stringify(message, null, 2));
+        if (DEBUG_WS) console.log('🔵 解析后的消息对象:', JSON.stringify(message, null, 2));
         this.handleMessage(message);
       } catch (e) {
         console.error('❌ 解析消息失败', e);
@@ -91,7 +88,7 @@ class WebSocketManager {
 
     // 监听连接关闭
     uni.onSocketClose((res) => {
-      console.log('WebSocket连接关闭', res);
+      if (DEBUG_WS) console.log('WebSocket连接关闭', res);
       this.isConnected = false;
       this.stopHeartbeat();
       
@@ -116,7 +113,7 @@ class WebSocketManager {
    * 处理接收到的消息
    */
   handleMessage(message) {
-    console.log('收到消息', message);
+    if (DEBUG_WS) console.log('收到消息', message);
 
     switch (message.type) {
       case 'pong':
@@ -135,9 +132,11 @@ class WebSocketManager {
         
       case 'ack':
         // 消息确认
-        console.log('✅ 收到ACK消息!');
-        console.log('   messageId:', message.messageId);
-        console.log('   完整ACK消息:', JSON.stringify(message));
+        if (DEBUG_WS) {
+          console.log('✅ 收到ACK消息!');
+          console.log('   messageId:', message.messageId);
+          console.log('   完整ACK消息:', JSON.stringify(message));
+        }
         if (this.onMessageCallback) {
           console.log('   正在调用onMessageCallback...');
           this.onMessageCallback(message);
@@ -169,7 +168,7 @@ class WebSocketManager {
         
       case 'online':
         // 上线通知
-        console.log('用户上线', message.fromUserId);
+        if (DEBUG_WS) console.log('用户上线', message.fromUserId);
         break;
         
       case 'error':
@@ -182,7 +181,7 @@ class WebSocketManager {
         break;
         
       default:
-        console.log('未知消息类型', message.type);
+        if (DEBUG_WS) console.log('未知消息类型', message.type);
     }
   }
 

+ 283 - 0
service/homePage/src/main/java/com/zhentao/controller/MyResourceController.java

@@ -22,6 +22,12 @@ public class MyResourceController {
     @Autowired
     private MyResourceService myResourceService;
     
+    @Autowired
+    private com.zhentao.service.UserFollowUpService userFollowUpService;
+    
+    @Autowired
+    private com.zhentao.service.FollowUpStatisticsService followUpStatisticsService;
+    
     @Autowired
     private UserMapper userMapper;
     
@@ -287,5 +293,282 @@ public class MyResourceController {
             return Result.error("资源删除失败:" + e.getMessage());
         }
     }
+    
+    /**
+     * 精准匹配
+     * 根据资源用户的择偶要求,匹配推荐异性用户
+     * 
+     * @param resourceId 资源ID
+     * @return 匹配结果列表(最多3个)
+     */
+    @GetMapping("/precise-match/{resourceId}")
+    public Result<java.util.List<com.zhentao.vo.MatchResultVO>> preciseMatch(@PathVariable Integer resourceId) {
+        try {
+            System.out.println("=== 精准匹配请求 ===");
+            System.out.println("资源ID: " + resourceId);
+            
+            java.util.List<com.zhentao.vo.MatchResultVO> matchResults = myResourceService.preciseMatch(resourceId);
+            
+            if (matchResults == null || matchResults.isEmpty()) {
+                return Result.success("未找到匹配的用户", new java.util.ArrayList<>());
+            }
+            
+            System.out.println("匹配成功,返回 " + matchResults.size() + " 个结果");
+            return Result.success(matchResults);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("精准匹配失败:" + e.getMessage());
+        }
+    }
+    
+    /**
+     * 更新跟进信息(保存到跟进表)
+     * 
+     * @param resourceId 资源ID
+     * @param requestBody 请求体,包含followType、followProgress、isMatch、isInvitation、invitationTime、remark
+     * @return 更新结果
+     */
+    @PutMapping("/update-follow/{resourceId}")
+    public Result<Void> updateFollow(
+            @PathVariable Integer resourceId,
+            @RequestBody java.util.Map<String, Object> requestBody) {
+        try {
+            System.out.println("=== 保存跟进信息到跟进表 ===");
+            System.out.println("资源ID: " + resourceId);
+            System.out.println("请求数据: " + requestBody);
+            
+            // 获取资源信息
+            MyResource resource = myResourceService.getById(resourceId);
+            if (resource == null) {
+                return Result.error("资源不存在");
+            }
+            
+            // 获取当前登录的红娘ID(这里需要从session或token中获取,暂时使用resource的matchmakerId)
+            Integer matchmakerId = resource.getMatchmakerId();
+            if (matchmakerId == null) {
+                // 如果资源没有红娘ID,尝试从请求参数获取
+                if (requestBody.containsKey("matchmakerId")) {
+                    matchmakerId = Integer.valueOf(requestBody.get("matchmakerId").toString());
+                } else {
+                    return Result.error("无法获取红娘ID");
+                }
+            }
+            
+            // 创建跟进记录
+            com.zhentao.entity.UserFollowUp followUp = new com.zhentao.entity.UserFollowUp();
+            followUp.setResourceId(resourceId);
+            followUp.setUserId(resource.getUserId());
+            followUp.setMatchmakerId(matchmakerId);
+            
+            // 跟进方式(1-电话,2-面谈,3-其他)
+            if (requestBody.containsKey("followType")) {
+                Object followTypeObj = requestBody.get("followType");
+                if (followTypeObj != null) {
+                    String followTypeStr = followTypeObj.toString();
+                    // 将字符串转换为数字:phone->1, face->2, other->3
+                    int followType = 1; // 默认电话
+                    if ("face".equals(followTypeStr)) {
+                        followType = 2;
+                    } else if ("other".equals(followTypeStr)) {
+                        followType = 3;
+                    } else if (followTypeStr.matches("\\d+")) {
+                        followType = Integer.parseInt(followTypeStr);
+                    }
+                    followUp.setFollowType(followType);
+                }
+            } else {
+                followUp.setFollowType(1); // 默认电话
+            }
+            
+            // 跟进进度(1-已匹配,2-未见面,3-沟通中)
+            if (requestBody.containsKey("progress")) {
+                Object progressObj = requestBody.get("progress");
+                if (progressObj != null) {
+                    String progressStr = progressObj.toString();
+                    int followProgress = 1; // 默认已匹配
+                    if ("notMet".equals(progressStr)) {
+                        followProgress = 2;
+                    } else if ("communicating".equals(progressStr)) {
+                        followProgress = 3;
+                    } else if ("matched".equals(progressStr)) {
+                        followProgress = 1;
+                    } else if (progressStr.matches("\\d+")) {
+                        followProgress = Integer.parseInt(progressStr);
+                    }
+                    followUp.setFollowProgress(followProgress);
+                }
+            }
+            
+            // 匹配状态(0-未匹配,1-已匹配)
+            if (requestBody.containsKey("matchStatus")) {
+                Object matchStatusObj = requestBody.get("matchStatus");
+                Integer isMatch = matchStatusObj != null ? Integer.valueOf(matchStatusObj.toString()) : 0;
+                followUp.setIsMatch(isMatch);
+                System.out.println("设置isMatch: " + isMatch);
+            } else {
+                followUp.setIsMatch(0); // 默认未匹配
+            }
+            
+            // 邀约状态(0-未邀约,1-已邀约)
+            if (requestBody.containsKey("invitationStatus")) {
+                Object invitationStatusObj = requestBody.get("invitationStatus");
+                Integer isInvitation = invitationStatusObj != null ? Integer.valueOf(invitationStatusObj.toString()) : 0;
+                followUp.setIsInvitation(isInvitation);
+                System.out.println("设置isInvitation: " + isInvitation);
+            } else {
+                followUp.setIsInvitation(0); // 默认未邀约
+            }
+            
+            // 邀约时间
+            if (requestBody.containsKey("invitationTime") || requestBody.containsKey("inviteDate")) {
+                Object invitationTimeObj = requestBody.get("invitationTime");
+                if (invitationTimeObj == null) {
+                    invitationTimeObj = requestBody.get("inviteDate");
+                }
+                if (invitationTimeObj != null && !invitationTimeObj.toString().trim().isEmpty()) {
+                    try {
+                        // 解析日期字符串 (格式: YYYY-MM-DD)
+                        String dateStr = invitationTimeObj.toString().trim();
+                        java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd");
+                        java.util.Date invitationTime = sdf.parse(dateStr);
+                        followUp.setInvitationTime(invitationTime);
+                        System.out.println("设置invitationTime: " + invitationTime);
+                    } catch (Exception e) {
+                        System.err.println("解析邀约时间失败: " + e.getMessage());
+                        followUp.setInvitationTime(null);
+                    }
+                } else {
+                    followUp.setInvitationTime(null);
+                }
+            }
+            
+            // 跟进备注
+            if (requestBody.containsKey("remark")) {
+                Object remarkObj = requestBody.get("remark");
+                if (remarkObj != null) {
+                    String remark = remarkObj.toString().trim();
+                    // 如果备注不为空,则设置;如果为空字符串,设置为null
+                    followUp.setRemark(remark.isEmpty() ? null : remark);
+                    System.out.println("设置remark: " + (remark.isEmpty() ? "null" : remark));
+                } else {
+                    followUp.setRemark(null);
+                }
+            } else {
+                // 如果没有remark字段,设置为null(不更新现有备注)
+                // 注意:这里不设置,让Service层处理
+            }
+            
+            // 保存或更新跟进记录
+            boolean success = userFollowUpService.saveOrUpdateFollowUp(followUp);
+            if (success) {
+                System.out.println("✅ 跟进信息保存成功");
+                
+                // 更新统计数据
+                try {
+                    followUpStatisticsService.updateStatistics(resourceId, matchmakerId);
+                    System.out.println("✅ 统计数据更新成功");
+                } catch (Exception e) {
+                    System.err.println("⚠️ 统计数据更新失败: " + e.getMessage());
+                    e.printStackTrace();
+                    // 统计数据更新失败不影响跟进信息保存
+                }
+                
+                return Result.success("跟进信息保存成功", null);
+            } else {
+                System.out.println("❌ 跟进信息保存失败");
+                return Result.error("跟进信息保存失败");
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("保存跟进信息失败:" + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取跟进统计数据
+     * 
+     * @param resourceId 资源ID
+     * @return 统计数据
+     */
+    @GetMapping("/follow-statistics/{resourceId}")
+    public Result<com.zhentao.entity.FollowUpStatistics> getFollowStatistics(@PathVariable Integer resourceId) {
+        try {
+            System.out.println("=== 获取跟进统计数据 ===");
+            System.out.println("资源ID: " + resourceId);
+            
+            // 获取资源信息
+            MyResource resource = myResourceService.getById(resourceId);
+            if (resource == null) {
+                return Result.error("资源不存在");
+            }
+            
+            Integer matchmakerId = resource.getMatchmakerId();
+            if (matchmakerId == null) {
+                return Result.error("无法获取红娘ID");
+            }
+            
+            // 获取或创建统计数据
+            com.zhentao.entity.FollowUpStatistics statistics = 
+                followUpStatisticsService.getOrCreateStatistics(resourceId, matchmakerId);
+            
+            if (statistics == null) {
+                return Result.error("统计数据不存在");
+            }
+            
+            System.out.println("=== 返回统计数据 ===");
+            System.out.println("statistics_id: " + statistics.getStatisticsId());
+            System.out.println("resource_id: " + statistics.getResourceId());
+            System.out.println("matchmaker_id: " + statistics.getMatchmakerId());
+            System.out.println("follow_count: " + statistics.getFollowCount());
+            System.out.println("match_count: " + statistics.getMatchCount());
+            System.out.println("phone_count: " + statistics.getPhoneCount());
+            System.out.println("interview_count: " + statistics.getInterviewCount());
+            System.out.println("完整对象: " + statistics);
+            
+            return Result.success(statistics);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("获取统计数据失败:" + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取最新的跟进记录
+     * 
+     * @param resourceId 资源ID
+     * @return 最新的跟进记录
+     */
+    @GetMapping("/latest-follow/{resourceId}")
+    public Result<com.zhentao.entity.UserFollowUp> getLatestFollow(@PathVariable Integer resourceId) {
+        try {
+            System.out.println("=== 获取最新跟进记录 ===");
+            System.out.println("资源ID: " + resourceId);
+            
+            // 获取资源信息
+            MyResource resource = myResourceService.getById(resourceId);
+            if (resource == null) {
+                return Result.error("资源不存在");
+            }
+            
+            Integer matchmakerId = resource.getMatchmakerId();
+            if (matchmakerId == null) {
+                return Result.error("无法获取红娘ID");
+            }
+            
+            // 获取最新的跟进记录
+            com.zhentao.entity.UserFollowUp followUp = 
+                userFollowUpService.getLatestByResourceIdAndMatchmakerId(resourceId, matchmakerId);
+            
+            if (followUp == null) {
+                return Result.success("暂无跟进记录", null);
+            }
+            
+            System.out.println("最新跟进记录: " + followUp);
+            return Result.success(followUp);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("获取最新跟进记录失败:" + e.getMessage());
+        }
+    }
 }
 

+ 82 - 0
service/homePage/src/main/java/com/zhentao/entity/FollowUpStatistics.java

@@ -0,0 +1,82 @@
+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("follow_up_statistics")
+public class FollowUpStatistics implements Serializable {
+    
+    private static final long serialVersionUID = 1L;
+    
+    /**
+     * 统计ID(主键)
+     */
+    @TableId(value = "statistics_id", type = IdType.AUTO)
+    private Integer statisticsId;
+    
+    /**
+     * 资源ID(关联my_resource表的resource_id)
+     */
+    @TableField("resource_id")
+    private Integer resourceId;
+    
+    /**
+     * 用户ID(关联users表的user_id,资源用户)
+     */
+    @TableField("user_id")
+    private Integer userId;
+    
+    /**
+     * 红娘ID(关联matchmakers表的matchmaker_id)
+     */
+    @TableField("matchmaker_id")
+    private Integer matchmakerId;
+    
+    /**
+     * 跟进次数(统计user_follow_up表中的记录数)
+     */
+    @TableField("follow_count")
+    private Integer followCount;
+    
+    /**
+     * 匹配对象数(统计is_match=1的记录数)
+     */
+    @TableField("match_count")
+    private Integer matchCount;
+    
+    /**
+     * 电话沟通次数(统计follow_type=1的记录数)
+     */
+    @TableField("phone_count")
+    private Integer phoneCount;
+    
+    /**
+     * 面谈次数(统计follow_type=2的记录数)
+     */
+    @TableField("interview_count")
+    private Integer interviewCount;
+    
+    /**
+     * 创建时间
+     */
+    @TableField("create_time")
+    private Date createTime;
+    
+    /**
+     * 更新时间
+     */
+    @TableField("update_time")
+    private Date updateTime;
+}
+

+ 94 - 0
service/homePage/src/main/java/com/zhentao/entity/UserFollowUp.java

@@ -0,0 +1,94 @@
+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("user_follow_up")
+public class UserFollowUp implements Serializable {
+    
+    private static final long serialVersionUID = 1L;
+    
+    /**
+     * 跟进ID(主键)
+     */
+    @TableId(value = "follow_up_id", type = IdType.AUTO)
+    private Integer followUpId;
+    
+    /**
+     * 资源ID(关联my_resource表的resource_id)
+     */
+    @TableField("resource_id")
+    private Integer resourceId;
+    
+    /**
+     * 用户ID(关联users表的user_id,资源用户)
+     */
+    @TableField("user_id")
+    private Integer userId;
+    
+    /**
+     * 红娘ID(关联matchmakers表的matchmaker_id)
+     */
+    @TableField("matchmaker_id")
+    private Integer matchmakerId;
+    
+    /**
+     * 跟进方式(1-电话,2-面谈,3-其他)
+     */
+    @TableField("follow_type")
+    private Integer followType;
+    
+    /**
+     * 跟进进度(1-已匹配,2-未见面,3-沟通中)
+     */
+    @TableField("follow_progress")
+    private Integer followProgress;
+    
+    /**
+     * 匹配状态(0-未匹配,1-已匹配)
+     */
+    @TableField("is_match")
+    private Integer isMatch;
+    
+    /**
+     * 邀约状态(0-未邀约,1-已邀约)
+     */
+    @TableField("is_invitation")
+    private Integer isInvitation;
+    
+    /**
+     * 邀约时间
+     */
+    @TableField("invitation_time")
+    private Date invitationTime;
+    
+    /**
+     * 跟进备注
+     */
+    @TableField("remark")
+    private String remark;
+    
+    /**
+     * 创建时间
+     */
+    @TableField("create_time")
+    private Date createTime;
+    
+    /**
+     * 更新时间
+     */
+    @TableField("update_time")
+    private Date updateTime;
+}
+

+ 26 - 0
service/homePage/src/main/java/com/zhentao/mapper/FollowUpStatisticsMapper.java

@@ -0,0 +1,26 @@
+package com.zhentao.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zhentao.entity.FollowUpStatistics;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * 跟进统计表Mapper
+ */
+@Mapper
+public interface FollowUpStatisticsMapper extends BaseMapper<FollowUpStatistics> {
+    
+    /**
+     * 根据资源ID和红娘ID查询统计数据
+     * 
+     * @param resourceId 资源ID
+     * @param matchmakerId 红娘ID
+     * @return 统计数据
+     */
+    FollowUpStatistics selectByResourceIdAndMatchmakerId(
+            @Param("resourceId") Integer resourceId,
+            @Param("matchmakerId") Integer matchmakerId
+    );
+}
+

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

@@ -6,6 +6,9 @@ import com.zhentao.entity.MyResource;
 import com.zhentao.vo.MyResourceVO;
 import org.apache.ibatis.annotations.Mapper;
 import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.Map;
 
 /**
 * @author Administrator
@@ -38,6 +41,20 @@ public interface MyResourceMapper extends BaseMapper<MyResource> {
             @Param("matchmakerId") Integer matchmakerId,
             @Param("keyword") String keyword
     );
+    
+    /**
+     * 根据用户ID查询用户资料信息
+     * 
+     * @param userId 用户ID
+     * @return 用户资料Map
+     */
+    @Select("SELECT up.height, up.weight, up.marital_status, up.house, up.car, " +
+            "up.education_level, up.salary_range, up.job_title, up.company, " +
+            "u.birth_date " +
+            "FROM user_profile up " +
+            "LEFT JOIN users u ON up.user_id = u.user_id " +
+            "WHERE up.user_id = #{userId}")
+    Map<String, Object> selectUserProfileByUserId(@Param("userId") Integer userId);
 }
 
 

+ 26 - 0
service/homePage/src/main/java/com/zhentao/mapper/UserFollowUpMapper.java

@@ -0,0 +1,26 @@
+package com.zhentao.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zhentao.entity.UserFollowUp;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * 用户跟进表Mapper
+ */
+@Mapper
+public interface UserFollowUpMapper extends BaseMapper<UserFollowUp> {
+    
+    /**
+     * 根据资源ID和红娘ID查询最新的跟进记录
+     * 
+     * @param resourceId 资源ID
+     * @param matchmakerId 红娘ID
+     * @return 跟进记录
+     */
+    UserFollowUp selectLatestByResourceIdAndMatchmakerId(
+            @Param("resourceId") Integer resourceId,
+            @Param("matchmakerId") Integer matchmakerId
+    );
+}
+

+ 39 - 0
service/homePage/src/main/java/com/zhentao/service/FollowUpStatisticsService.java

@@ -0,0 +1,39 @@
+package com.zhentao.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.zhentao.entity.FollowUpStatistics;
+
+/**
+ * 跟进统计服务接口
+ */
+public interface FollowUpStatisticsService extends IService<FollowUpStatistics> {
+    
+    /**
+     * 根据资源ID和红娘ID获取或创建统计数据
+     * 如果不存在则创建,如果存在则更新统计数据
+     * 
+     * @param resourceId 资源ID
+     * @param matchmakerId 红娘ID
+     * @return 统计数据
+     */
+    FollowUpStatistics getOrCreateStatistics(Integer resourceId, Integer matchmakerId);
+    
+    /**
+     * 更新统计数据(根据user_follow_up表重新统计)
+     * 
+     * @param resourceId 资源ID
+     * @param matchmakerId 红娘ID
+     * @return 是否成功
+     */
+    boolean updateStatistics(Integer resourceId, Integer matchmakerId);
+    
+    /**
+     * 根据资源ID和红娘ID获取统计数据
+     * 
+     * @param resourceId 资源ID
+     * @param matchmakerId 红娘ID
+     * @return 统计数据,如果不存在返回null
+     */
+    FollowUpStatistics getStatistics(Integer resourceId, Integer matchmakerId);
+}
+

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

@@ -56,4 +56,21 @@ public interface MyResourceService extends IService<MyResource> {
      * @return 客户详情
      */
     com.zhentao.vo.ClientDetailVO getClientDetail(Integer resourceId);
+    
+    /**
+     * 根据手机号更新资源的注册状态
+     * 当用户注册时,如果my_resource表中存在相同手机号的资源记录,自动更新is_user和user_id
+     * @param phone 手机号
+     * @param userId 用户ID
+     * @return 是否成功更新
+     */
+    boolean updateResourceStatusByPhone(String phone, Integer userId);
+    
+    /**
+     * 精准匹配
+     * 根据资源用户的择偶要求,匹配推荐异性用户
+     * @param resourceId 资源ID
+     * @return 匹配结果列表(最多3个,按匹配度降序)
+     */
+    java.util.List<com.zhentao.vo.MatchResultVO> preciseMatch(Integer resourceId);
 }

+ 28 - 0
service/homePage/src/main/java/com/zhentao/service/UserFollowUpService.java

@@ -0,0 +1,28 @@
+package com.zhentao.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.zhentao.entity.UserFollowUp;
+
+/**
+ * 用户跟进服务接口
+ */
+public interface UserFollowUpService extends IService<UserFollowUp> {
+    
+    /**
+     * 保存或更新跟进信息
+     * 
+     * @param followUp 跟进信息
+     * @return 是否成功
+     */
+    boolean saveOrUpdateFollowUp(UserFollowUp followUp);
+    
+    /**
+     * 根据资源ID和红娘ID获取最新的跟进记录
+     * 
+     * @param resourceId 资源ID
+     * @param matchmakerId 红娘ID
+     * @return 跟进记录,如果不存在返回null
+     */
+    UserFollowUp getLatestByResourceIdAndMatchmakerId(Integer resourceId, Integer matchmakerId);
+}
+

+ 145 - 0
service/homePage/src/main/java/com/zhentao/service/impl/FollowUpStatisticsServiceImpl.java

@@ -0,0 +1,145 @@
+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.FollowUpStatistics;
+import com.zhentao.entity.UserFollowUp;
+import com.zhentao.mapper.FollowUpStatisticsMapper;
+import com.zhentao.mapper.UserFollowUpMapper;
+import com.zhentao.service.FollowUpStatisticsService;
+import com.zhentao.service.MyResourceService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 跟进统计服务实现类
+ */
+@Service
+public class FollowUpStatisticsServiceImpl extends ServiceImpl<FollowUpStatisticsMapper, FollowUpStatistics> 
+        implements FollowUpStatisticsService {
+    
+    @Autowired
+    private UserFollowUpMapper userFollowUpMapper;
+    
+    @Autowired
+    private MyResourceService myResourceService;
+    
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public FollowUpStatistics getOrCreateStatistics(Integer resourceId, Integer matchmakerId) {
+        // 查询是否已存在统计数据
+        QueryWrapper<FollowUpStatistics> queryWrapper = new QueryWrapper<>();
+        queryWrapper.eq("resource_id", resourceId);
+        queryWrapper.eq("matchmaker_id", matchmakerId);
+        FollowUpStatistics statistics = this.getOne(queryWrapper);
+        
+        if (statistics == null) {
+            // 创建新的统计数据
+            statistics = new FollowUpStatistics();
+            statistics.setResourceId(resourceId);
+            statistics.setMatchmakerId(matchmakerId);
+            
+            // 查询资源信息获取user_id
+            com.zhentao.entity.MyResource resource = myResourceService.getById(resourceId);
+            if (resource != null && resource.getUserId() != null) {
+                statistics.setUserId(resource.getUserId());
+            }
+            
+            Date now = new Date();
+            statistics.setCreateTime(now);
+            statistics.setUpdateTime(now);
+            
+            // 初始化统计数据
+            statistics.setFollowCount(0);
+            statistics.setMatchCount(0);
+            statistics.setPhoneCount(0);
+            statistics.setInterviewCount(0);
+            
+            this.save(statistics);
+        }
+        
+        // 更新统计数据
+        updateStatistics(resourceId, matchmakerId);
+        
+        // 重新查询返回最新数据
+        return this.getOne(queryWrapper);
+    }
+    
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean updateStatistics(Integer resourceId, Integer matchmakerId) {
+        // 查询该资源的所有跟进记录
+        QueryWrapper<UserFollowUp> followUpQuery = new QueryWrapper<>();
+        followUpQuery.eq("resource_id", resourceId);
+        followUpQuery.eq("matchmaker_id", matchmakerId);
+        List<UserFollowUp> followUpList = userFollowUpMapper.selectList(followUpQuery);
+        
+        // 统计各项数据
+        int followCount = followUpList.size();
+        int matchCount = 0;
+        int phoneCount = 0;
+        int interviewCount = 0;
+        
+        for (UserFollowUp followUp : followUpList) {
+            // 统计匹配对象数(is_match=1)
+            if (followUp.getIsMatch() != null && followUp.getIsMatch() == 1) {
+                matchCount++;
+            }
+            
+            // 统计电话沟通次数(follow_type=1)
+            if (followUp.getFollowType() != null && followUp.getFollowType() == 1) {
+                phoneCount++;
+            }
+            
+            // 统计面谈次数(follow_type=2)
+            if (followUp.getFollowType() != null && followUp.getFollowType() == 2) {
+                interviewCount++;
+            }
+        }
+        
+        // 查询或创建统计数据
+        QueryWrapper<FollowUpStatistics> queryWrapper = new QueryWrapper<>();
+        queryWrapper.eq("resource_id", resourceId);
+        queryWrapper.eq("matchmaker_id", matchmakerId);
+        FollowUpStatistics statistics = this.getOne(queryWrapper);
+        
+        if (statistics == null) {
+            // 创建新的统计数据
+            statistics = new FollowUpStatistics();
+            statistics.setResourceId(resourceId);
+            statistics.setMatchmakerId(matchmakerId);
+            
+            // 查询资源信息获取user_id
+            com.zhentao.entity.MyResource resource = myResourceService.getById(resourceId);
+            if (resource != null && resource.getUserId() != null) {
+                statistics.setUserId(resource.getUserId());
+            }
+            
+            Date now = new Date();
+            statistics.setCreateTime(now);
+            statistics.setUpdateTime(now);
+        }
+        
+        // 更新统计数据
+        statistics.setFollowCount(followCount);
+        statistics.setMatchCount(matchCount);
+        statistics.setPhoneCount(phoneCount);
+        statistics.setInterviewCount(interviewCount);
+        statistics.setUpdateTime(new Date());
+        
+        return this.saveOrUpdate(statistics);
+    }
+    
+    @Override
+    public FollowUpStatistics getStatistics(Integer resourceId, Integer matchmakerId) {
+        QueryWrapper<FollowUpStatistics> queryWrapper = new QueryWrapper<>();
+        queryWrapper.eq("resource_id", resourceId);
+        queryWrapper.eq("matchmaker_id", matchmakerId);
+        return this.getOne(queryWrapper);
+    }
+}
+

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

@@ -49,6 +49,9 @@ public class MyResourceServiceImpl extends ServiceImpl<MyResourceMapper, MyResou
     
     @Autowired(required = false)
     private PartnerRequirementService partnerRequirementService;
+    
+    @Autowired(required = false)
+    private com.zhentao.service.UserFollowUpService userFollowUpService;
 
     @Override
     @Transactional(rollbackFor = Exception.class)
@@ -241,6 +244,27 @@ public class MyResourceServiceImpl extends ServiceImpl<MyResourceMapper, MyResou
         // 择偶要求从my_resource表的mate_selection_criteria字段获取
         clientDetail.setMateSelectionCriteria(myResource.getMateSelectionCriteria());
         
+        // 从跟进表获取匹配和邀约状态
+        if (userFollowUpService != null) {
+            com.zhentao.entity.UserFollowUp followUp = userFollowUpService.getLatestByResourceIdAndMatchmakerId(
+                    myResource.getResourceId(), myResource.getMatchmakerId());
+            if (followUp != null) {
+                clientDetail.setIsMatch(followUp.getIsMatch());
+                clientDetail.setIsInvitation(followUp.getIsInvitation());
+                clientDetail.setInvitationTime(followUp.getInvitationTime());
+            } else {
+                // 如果没有跟进记录,设置默认值
+                clientDetail.setIsMatch(0);
+                clientDetail.setIsInvitation(0);
+                clientDetail.setInvitationTime(null);
+            }
+        } else {
+            // 如果服务不可用,设置默认值
+            clientDetail.setIsMatch(0);
+            clientDetail.setIsInvitation(0);
+            clientDetail.setInvitationTime(null);
+        }
+        
         // 查询该资源的标签
         if (myResource.getResourceId() != null) {
             List<Integer> tagIds = myResourceTagMapper.selectTagIdsByResourceId(myResource.getResourceId());
@@ -317,6 +341,394 @@ public class MyResourceServiceImpl extends ServiceImpl<MyResourceMapper, MyResou
         
         return clientDetail;
     }
+    
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean updateResourceStatusByPhone(String phone, Integer userId) {
+        if (phone == null || phone.trim().isEmpty() || userId == null) {
+            System.out.println("⚠️ 更新资源状态失败:手机号或用户ID为空");
+            return false;
+        }
+        
+        try {
+            // 根据手机号查询资源列表
+            QueryWrapper<MyResource> queryWrapper = new QueryWrapper<>();
+            queryWrapper.eq("phone", phone.trim());
+            List<MyResource> resources = this.list(queryWrapper);
+            
+            if (resources == null || resources.isEmpty()) {
+                System.out.println("未找到匹配的资源记录,手机号: " + phone);
+                return false;
+            }
+            
+            // 更新所有匹配的资源记录
+            boolean hasUpdate = false;
+            for (MyResource resource : resources) {
+                // 只更新未注册的资源(is_user = 0)
+                if (resource.getIsUser() == null || resource.getIsUser() == 0) {
+                    resource.setIsUser(1);
+                    resource.setUserId(userId);
+                    resource.setUpdateTime(new Date());
+                    boolean updated = this.updateById(resource);
+                    if (updated) {
+                        hasUpdate = true;
+                        System.out.println("✅ 更新资源注册状态成功 - 资源ID: " + resource.getResourceId() + 
+                                         ", 手机号: " + phone + ", 用户ID: " + userId);
+                    } else {
+                        System.out.println("⚠️ 更新资源注册状态失败 - 资源ID: " + resource.getResourceId());
+                    }
+                } else {
+                    System.out.println("资源已注册,跳过更新 - 资源ID: " + resource.getResourceId());
+                }
+            }
+            
+            return hasUpdate;
+        } catch (Exception e) {
+            System.err.println("❌ 更新资源注册状态时发生异常: " + e.getMessage());
+            e.printStackTrace();
+            return false;
+        }
+    }
+    
+    @Override
+    public java.util.List<com.zhentao.vo.MatchResultVO> preciseMatch(Integer resourceId) {
+        System.out.println("=== 开始精准匹配 ===");
+        System.out.println("资源ID: " + resourceId);
+        
+        // 1. 获取资源用户信息
+        MyResource resource = this.getById(resourceId);
+        if (resource == null) {
+            System.out.println("资源不存在,resourceId: " + resourceId);
+            return new java.util.ArrayList<>();
+        }
+        
+        // 检查用户是否已注册
+        if (resource.getIsUser() == null || resource.getIsUser() != 1 || resource.getUserId() == null) {
+            System.out.println("资源用户未注册,无法进行匹配");
+            throw new RuntimeException("该用户还未注册用户端,注册之后才能进行匹配");
+        }
+        
+        Integer resourceUserId = resource.getUserId();
+        Integer resourceGender = resource.getGender(); // 1-男, 2-女
+        
+        System.out.println("资源用户ID: " + resourceUserId + ", 性别: " + resourceGender);
+        
+        // 2. 获取择偶要求
+        PartnerRequirement requirement = null;
+        if (partnerRequirementService != null) {
+            requirement = partnerRequirementService.getByUserId(resourceUserId.longValue());
+        }
+        
+        if (requirement == null) {
+            System.out.println("未找到择偶要求,使用默认匹配逻辑");
+        } else {
+            System.out.println("择偶要求: 年龄" + requirement.getMinAge() + "-" + requirement.getMaxAge() + 
+                             ", 身高" + requirement.getMinHeight() + "-" + requirement.getMaxHeight() + 
+                             ", 学历" + requirement.getEducationLevel());
+        }
+        
+        // 3. 确定目标性别(异性)
+        Integer targetGender = (resourceGender == 1) ? 2 : 1; // 1-男找女, 2-女找男
+        
+        // 4. 查询所有异性用户
+        QueryWrapper<User> userQuery = new QueryWrapper<>();
+        userQuery.eq("gender", targetGender);
+        userQuery.eq("status", 1); // 只查询正常状态的用户
+        List<User> allUsers = userMapper.selectList(userQuery);
+        
+        System.out.println("找到 " + allUsers.size() + " 个异性用户");
+        
+        if (allUsers.isEmpty()) {
+            return new java.util.ArrayList<>();
+        }
+        
+        // 5. 计算每个用户的匹配度
+        List<com.zhentao.vo.MatchResultVO> matchResults = new java.util.ArrayList<>();
+        
+        for (User user : allUsers) {
+            // 跳过自己
+            if (user.getUserId().equals(resourceUserId)) {
+                continue;
+            }
+            
+            com.zhentao.vo.MatchResultVO matchResult = new com.zhentao.vo.MatchResultVO();
+            matchResult.setUserId(user.getUserId());
+            matchResult.setNickname(user.getNickname());
+            matchResult.setGender(user.getGender());
+            matchResult.setAvatarUrl(user.getAvatarUrl());
+            
+            // 查询用户资料(通过SQL)
+            // 由于homePage服务可能没有UserProfileMapper,我们通过MyResourceMapper执行SQL查询
+            java.util.Map<String, Object> profileMap = myResourceMapper.selectUserProfileByUserId(user.getUserId());
+            
+            if (profileMap != null) {
+                matchResult.setHeight((Integer) profileMap.get("height"));
+                Object birthDateObj = profileMap.get("birth_date");
+                if (birthDateObj != null) {
+                    java.util.Date birthDate = null;
+                    if (birthDateObj instanceof java.util.Date) {
+                        birthDate = (java.util.Date) birthDateObj;
+                    } else if (birthDateObj instanceof java.sql.Date) {
+                        birthDate = new java.util.Date(((java.sql.Date) birthDateObj).getTime());
+                    } else if (birthDateObj instanceof java.time.LocalDate) {
+                        java.time.LocalDate localDate = (java.time.LocalDate) birthDateObj;
+                        birthDate = java.sql.Date.valueOf(localDate);
+                    }
+                    matchResult.setAge(calculateAge(birthDate));
+                }
+                matchResult.setMaritalStatus((Integer) profileMap.get("marital_status"));
+                matchResult.setHouse((Integer) profileMap.get("house"));
+                matchResult.setCar((Integer) profileMap.get("car"));
+                
+                // 学历转换
+                Integer educationLevel = (Integer) profileMap.get("education_level");
+                matchResult.setDiploma(convertEducationLevel(educationLevel));
+                
+                // 收入转换
+                Integer salaryRange = (Integer) profileMap.get("salary_range");
+                matchResult.setIncome(convertSalaryRange(salaryRange));
+                
+                // 职业
+                matchResult.setOccupation((String) profileMap.get("job_title"));
+                
+                // 住址(从company获取,这里简化处理)
+                matchResult.setAddress((String) profileMap.get("company"));
+            }
+            
+            // 查询用户手机号(脱敏)
+            matchResult.setPhone(user.getPhone() != null ? maskPhone(user.getPhone()) : "");
+            
+            // 查询标签
+            List<String> tags = new java.util.ArrayList<>();
+            // 这里可以查询用户的标签,暂时使用空列表
+            matchResult.setTags(tags);
+            
+            // 计算匹配度
+            double matchScore = calculateMatchScore(resource, requirement, matchResult);
+            matchResult.setMatchScore(matchScore);
+            
+            // 查询匹配到的用户的择偶要求(而不是资源用户的择偶要求)
+            PartnerRequirement candidateRequirement = null;
+            if (partnerRequirementService != null) {
+                candidateRequirement = partnerRequirementService.getByUserId(user.getUserId().longValue());
+            }
+            
+            // 择偶要求文本(显示匹配到的用户的要求)
+            matchResult.setRequirement(formatRequirement(candidateRequirement));
+            
+            matchResults.add(matchResult);
+        }
+        
+        // 6. 按匹配度降序排序,取前3个
+        matchResults.sort((a, b) -> Double.compare(b.getMatchScore(), a.getMatchScore()));
+        
+        int limit = Math.min(3, matchResults.size());
+        List<com.zhentao.vo.MatchResultVO> topMatches = matchResults.subList(0, limit);
+        
+        System.out.println("匹配完成,返回 " + topMatches.size() + " 个结果");
+        for (com.zhentao.vo.MatchResultVO result : topMatches) {
+            System.out.println("用户: " + result.getNickname() + ", 匹配度: " + result.getMatchScore());
+        }
+        
+        return topMatches;
+    }
+    
+    /**
+     * 计算匹配度分数
+     */
+    private double calculateMatchScore(MyResource resource, PartnerRequirement requirement, com.zhentao.vo.MatchResultVO candidate) {
+        double totalScore = 0.0;
+        int factorCount = 0;
+        
+        if (requirement == null) {
+            // 如果没有择偶要求,返回基础分数
+            return 60.0;
+        }
+        
+        // 1. 年龄匹配 (权重: 20%)
+        if (requirement.getMinAge() != null && requirement.getMaxAge() != null && candidate.getAge() != null) {
+            int age = candidate.getAge();
+            if (age >= requirement.getMinAge() && age <= requirement.getMaxAge()) {
+                totalScore += 20.0;
+            } else {
+                // 年龄差距越大,分数越低
+                int gap = Math.min(Math.abs(age - requirement.getMinAge()), Math.abs(age - requirement.getMaxAge()));
+                totalScore += Math.max(0, 20.0 - gap * 2);
+            }
+            factorCount++;
+        }
+        
+        // 2. 身高匹配 (权重: 20%)
+        if (requirement.getMinHeight() != null && requirement.getMaxHeight() != null && candidate.getHeight() != null) {
+            int height = candidate.getHeight();
+            if (height >= requirement.getMinHeight() && height <= requirement.getMaxHeight()) {
+                totalScore += 20.0;
+            } else {
+                int gap = Math.min(Math.abs(height - requirement.getMinHeight()), Math.abs(height - requirement.getMaxHeight()));
+                totalScore += Math.max(0, 20.0 - gap * 0.5);
+            }
+            factorCount++;
+        }
+        
+        // 3. 学历匹配 (权重: 15%)
+        if (requirement.getEducationLevel() != null && candidate.getDiploma() != null) {
+            int requiredLevel = requirement.getEducationLevel();
+            int candidateLevel = parseEducationLevel(candidate.getDiploma());
+            if (candidateLevel >= requiredLevel) {
+                totalScore += 15.0;
+            } else {
+                totalScore += Math.max(0, 15.0 - (requiredLevel - candidateLevel) * 3);
+            }
+            factorCount++;
+        }
+        
+        // 4. 收入匹配 (权重: 15%)
+        if (requirement.getSalaryRange() != null && candidate.getIncome() != null) {
+            int requiredRange = requirement.getSalaryRange();
+            int candidateRange = parseSalaryRange(candidate.getIncome());
+            if (candidateRange >= requiredRange) {
+                totalScore += 15.0;
+            } else {
+                totalScore += Math.max(0, 15.0 - (requiredRange - candidateRange) * 3);
+            }
+            factorCount++;
+        }
+        
+        // 5. 房产匹配 (权重: 10%)
+        if (requirement.getHouse() != null && requirement.getHouse() != 0 && candidate.getHouse() != null) {
+            if (candidate.getHouse() == 1) {
+                totalScore += 10.0;
+            }
+            factorCount++;
+        }
+        
+        // 6. 车辆匹配 (权重: 10%)
+        if (requirement.getCar() != null && requirement.getCar() != 0 && candidate.getCar() != null) {
+            if (candidate.getCar() == 1) {
+                totalScore += 10.0;
+            }
+            factorCount++;
+        }
+        
+        // 7. 婚姻状况匹配 (权重: 10%)
+        if (requirement.getMaritalStatus() != null && requirement.getMaritalStatus() != 0 && candidate.getMaritalStatus() != null) {
+            if (candidate.getMaritalStatus().equals(requirement.getMaritalStatus())) {
+                totalScore += 10.0;
+            }
+            factorCount++;
+        }
+        
+        // 如果没有任何匹配因子,返回基础分数
+        if (factorCount == 0) {
+            return 60.0;
+        }
+        
+        // 归一化到0-100
+        double maxScore = 100.0;
+        return Math.min(100.0, totalScore);
+    }
+    
+    /**
+     * 计算年龄(从出生日期)
+     */
+    private Integer calculateAge(java.util.Date birthDate) {
+        if (birthDate == null) {
+            return null;
+        }
+        java.util.Calendar birth = java.util.Calendar.getInstance();
+        birth.setTime(birthDate);
+        java.util.Calendar now = java.util.Calendar.getInstance();
+        int age = now.get(java.util.Calendar.YEAR) - birth.get(java.util.Calendar.YEAR);
+        if (now.get(java.util.Calendar.DAY_OF_YEAR) < birth.get(java.util.Calendar.DAY_OF_YEAR)) {
+            age--;
+        }
+        return age;
+    }
+    
+    /**
+     * 转换学历等级为文本
+     */
+    private String convertEducationLevel(Integer level) {
+        if (level == null) return null;
+        switch (level) {
+            case 1: return "高中";
+            case 2: return "专科";
+            case 3: return "本科";
+            case 4: return "硕士";
+            case 5: return "博士";
+            default: return "未知";
+        }
+    }
+    
+    /**
+     * 解析学历文本为等级
+     */
+    private int parseEducationLevel(String diploma) {
+        if (diploma == null) return 0;
+        if (diploma.contains("博士")) return 5;
+        if (diploma.contains("硕士")) return 4;
+        if (diploma.contains("本科")) return 3;
+        if (diploma.contains("专科")) return 2;
+        if (diploma.contains("高中")) return 1;
+        return 0;
+    }
+    
+    /**
+     * 转换收入范围为文本
+     */
+    private String convertSalaryRange(Integer range) {
+        if (range == null) return null;
+        switch (range) {
+            case 1: return "5k以下";
+            case 2: return "5-10k";
+            case 3: return "10-20k";
+            case 4: return "20-50k";
+            case 5: return "50k+";
+            default: return "未知";
+        }
+    }
+    
+    /**
+     * 解析收入文本为范围
+     */
+    private int parseSalaryRange(String income) {
+        if (income == null) return 0;
+        if (income.contains("50k+") || income.contains("50k以上")) return 5;
+        if (income.contains("20-50k") || income.contains("20k")) return 4;
+        if (income.contains("10-20k") || income.contains("10k")) return 3;
+        if (income.contains("5-10k") || income.contains("5k")) return 2;
+        return 1;
+    }
+    
+    /**
+     * 手机号脱敏
+     */
+    private String maskPhone(String phone) {
+        if (phone == null || phone.length() < 11) {
+            return phone;
+        }
+        return phone.substring(0, 3) + "****" + phone.substring(7);
+    }
+    
+    /**
+     * 格式化择偶要求文本
+     */
+    private String formatRequirement(PartnerRequirement requirement) {
+        if (requirement == null) {
+            return "暂无要求";
+        }
+        StringBuilder sb = new StringBuilder();
+        if (requirement.getMinAge() != null && requirement.getMaxAge() != null) {
+            sb.append("年龄").append(requirement.getMinAge()).append("-").append(requirement.getMaxAge()).append("岁 ");
+        }
+        if (requirement.getMinHeight() != null && requirement.getMaxHeight() != null) {
+            sb.append("身高").append(requirement.getMinHeight()).append("-").append(requirement.getMaxHeight()).append("cm ");
+        }
+        if (requirement.getEducationLevel() != null && requirement.getEducationLevel() != 0) {
+            sb.append(convertEducationLevel(requirement.getEducationLevel())).append(" ");
+        }
+        return sb.length() > 0 ? sb.toString().trim() : "暂无要求";
+    }
 }
 
 

+ 53 - 0
service/homePage/src/main/java/com/zhentao/service/impl/UserFollowUpServiceImpl.java

@@ -0,0 +1,53 @@
+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.UserFollowUp;
+import com.zhentao.mapper.UserFollowUpMapper;
+import com.zhentao.service.UserFollowUpService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Date;
+
+/**
+ * 用户跟进服务实现类
+ */
+@Service
+public class UserFollowUpServiceImpl extends ServiceImpl<UserFollowUpMapper, UserFollowUp> 
+        implements UserFollowUpService {
+    
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean saveOrUpdateFollowUp(UserFollowUp followUp) {
+        Date now = new Date();
+        
+        // 每次保存跟进信息都创建新记录,以支持累加统计
+        // 这样每次跟进都会留下历史记录,统计时可以累加所有记录
+        
+        // 处理备注字段:如果为空字符串,设置为null
+        if (followUp.getRemark() != null && followUp.getRemark().trim().isEmpty()) {
+            followUp.setRemark(null);
+        }
+        
+        // 设置时间
+        followUp.setCreateTime(now);
+        followUp.setUpdateTime(now);
+        
+        // 创建新记录(不更新现有记录)
+        return this.save(followUp);
+    }
+    
+    @Override
+    public UserFollowUp getLatestByResourceIdAndMatchmakerId(Integer resourceId, Integer matchmakerId) {
+        QueryWrapper<UserFollowUp> queryWrapper = new QueryWrapper<>();
+        queryWrapper.eq("resource_id", resourceId);
+        if (matchmakerId != null) {
+            queryWrapper.eq("matchmaker_id", matchmakerId);
+        }
+        queryWrapper.orderByDesc("update_time");
+        queryWrapper.last("LIMIT 1");
+        return this.getOne(queryWrapper);
+    }
+}
+

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

@@ -54,5 +54,10 @@ public class ClientDetailVO implements Serializable {
     
     // 客户标签(从my_resource_tag关联表查询)
     private java.util.List<String> tags; // 标签名称列表
+    
+    // 匹配和邀约状态
+    private Integer isMatch; // 该资源用户是否已匹配对象(0-未匹配,1-已匹配)
+    private Integer isInvitation; // 红娘是否邀约该用户(0-未邀约,1-已邀约)
+    private java.util.Date invitationTime; // 邀约用户的时间
 }
 

+ 102 - 0
service/homePage/src/main/java/com/zhentao/vo/MatchResultVO.java

@@ -0,0 +1,102 @@
+package com.zhentao.vo;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import java.io.Serializable;
+
+/**
+ * 匹配结果VO
+ */
+@Data
+public class MatchResultVO implements Serializable {
+    
+    private static final long serialVersionUID = 1L;
+    
+    /**
+     * 用户ID
+     */
+    @JsonProperty("userId")
+    private Integer userId;
+    
+    /**
+     * 昵称
+     */
+    private String nickname;
+    
+    /**
+     * 性别 (1-男, 2-女)
+     */
+    private Integer gender;
+    
+    /**
+     * 年龄
+     */
+    private Integer age;
+    
+    /**
+     * 身高(cm)
+     */
+    private Integer height;
+    
+    /**
+     * 头像URL
+     */
+    @JsonProperty("avatarUrl")
+    private String avatarUrl;
+    
+    /**
+     * 学历
+     */
+    private String diploma;
+    
+    /**
+     * 收入
+     */
+    private String income;
+    
+    /**
+     * 职业
+     */
+    private String occupation;
+    
+    /**
+     * 住址
+     */
+    private String address;
+    
+    /**
+     * 婚姻状况
+     */
+    private Integer maritalStatus;
+    
+    /**
+     * 购房情况 (0-无, 1-有)
+     */
+    private Integer house;
+    
+    /**
+     * 购车情况 (0-无, 1-有)
+     */
+    private Integer car;
+    
+    /**
+     * 手机号(脱敏)
+     */
+    private String phone;
+    
+    /**
+     * 匹配度分数 (0-100)
+     */
+    private Double matchScore;
+    
+    /**
+     * 标签列表
+     */
+    private java.util.List<String> tags;
+    
+    /**
+     * 择偶要求
+     */
+    private String requirement;
+}
+

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

@@ -132,5 +132,20 @@ public class MyResourceVO implements Serializable {
      * 标签列表(从my_resource_tag关联表查询)
      */
     private java.util.List<String> tags;
+    
+    /**
+     * 该资源用户是否已匹配对象(0-未匹配,1-已匹配)
+     */
+    private Integer isMatch;
+    
+    /**
+     * 红娘是否邀约该用户(0-未邀约,1-已邀约)
+     */
+    private Integer isInvitation;
+    
+    /**
+     * 邀约用户的时间
+     */
+    private Date invitationTime;
 }
 

+ 20 - 3
service/homePage/src/main/resources/mapper/MyResourceMapper.xml

@@ -84,6 +84,9 @@
         <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="isMatch" column="is_match" jdbcType="INTEGER"/>
+        <result property="isInvitation" column="is_invitation" jdbcType="INTEGER"/>
+        <result property="invitationTime" column="invitation_time" jdbcType="TIMESTAMP"/>
     </resultMap>
 
     <!-- 分页查询资源列表(关联users表获取头像) -->
@@ -111,9 +114,23 @@
             r.is_user,
             r.create_time,
             r.update_time,
-            u.avatar_url
-        FROM my_resource r
-        LEFT JOIN users u ON r.phone = u.phone
+            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
         WHERE 1=1
         <if test="matchmakerId != null">
             AND r.matchmaker_id = #{matchmakerId}