Kaynağa Gözat

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

李思佳 1 ay önce
ebeveyn
işleme
86f00288f8
79 değiştirilmiş dosya ile 8134 ekleme ve 1072 silme
  1. 13 0
      LiangZhiYUMao/App.vue
  2. 28 0
      LiangZhiYUMao/pages.json
  3. 85 26
      LiangZhiYUMao/pages/index/index.vue
  4. 777 136
      LiangZhiYUMao/pages/matchmaker-workbench/client-detail.vue
  5. 323 0
      LiangZhiYUMao/pages/matchmaker-workbench/earn-points.vue
  6. 627 125
      LiangZhiYUMao/pages/matchmaker-workbench/message.vue
  7. 636 291
      LiangZhiYUMao/pages/matchmaker-workbench/my-resources.vue
  8. 282 0
      LiangZhiYUMao/pages/matchmaker-workbench/points-detail.vue
  9. 159 55
      LiangZhiYUMao/pages/matchmaker-workbench/points-mall.vue
  10. 253 49
      LiangZhiYUMao/pages/matchmaker-workbench/precise-match.vue
  11. 392 0
      LiangZhiYUMao/pages/matchmaker-workbench/product-detail.vue
  12. 307 47
      LiangZhiYUMao/pages/matchmaker-workbench/resource-input.vue
  13. 290 0
      LiangZhiYUMao/pages/matchmaker-workbench/system-messages.vue
  14. 26 3
      LiangZhiYUMao/pages/matchmakers/detail.vue
  15. 182 184
      LiangZhiYUMao/pages/message/chat.vue
  16. 91 59
      LiangZhiYUMao/pages/message/index.vue
  17. 92 3
      LiangZhiYUMao/utils/api.js
  18. 14 0
      LiangZhiYUMao/utils/tim-manager.js
  19. 33 22
      LiangZhiYUMao/utils/tim-presence-manager.js
  20. 17 18
      LiangZhiYUMao/utils/websocket.js
  21. 1 1
      gateway/src/main/java/com/zhentao/filter/GatewayRoutes.java
  22. 16 0
      gateway/src/main/resources/application.yml
  23. 226 0
      service/admin/src/main/java/com/zhentao/controller/PointsMallController.java
  24. 38 0
      service/admin/src/main/java/com/zhentao/entity/PointRule.java
  25. 38 0
      service/admin/src/main/java/com/zhentao/entity/PointsDetail.java
  26. 74 0
      service/admin/src/main/java/com/zhentao/entity/PointsOrder.java
  27. 56 0
      service/admin/src/main/java/com/zhentao/entity/PointsProduct.java
  28. 12 0
      service/admin/src/main/java/com/zhentao/mapper/PointRuleMapper.java
  29. 12 0
      service/admin/src/main/java/com/zhentao/mapper/PointsDetailMapper.java
  30. 12 0
      service/admin/src/main/java/com/zhentao/mapper/PointsOrderMapper.java
  31. 12 0
      service/admin/src/main/java/com/zhentao/mapper/PointsProductMapper.java
  32. 99 0
      service/admin/src/main/java/com/zhentao/service/PointsMallService.java
  33. 98 11
      service/admin/src/main/java/com/zhentao/service/impl/MarrApplyServiceImpl.java
  34. 276 0
      service/admin/src/main/java/com/zhentao/service/impl/PointsMallServiceImpl.java
  35. 2 1
      service/homePage/src/main/java/com/zhentao/HomePageApplication.java
  36. 4 1
      service/homePage/src/main/java/com/zhentao/constant/RedisKeyConstants.java
  37. 31 3
      service/homePage/src/main/java/com/zhentao/controller/ActivityController.java
  38. 177 0
      service/homePage/src/main/java/com/zhentao/controller/MatchmakerController.java
  39. 401 10
      service/homePage/src/main/java/com/zhentao/controller/MyResourceController.java
  40. 38 0
      service/homePage/src/main/java/com/zhentao/controller/TagController.java
  41. 24 0
      service/homePage/src/main/java/com/zhentao/dto/MyResourceDTO.java
  42. 82 0
      service/homePage/src/main/java/com/zhentao/entity/FollowUpStatistics.java
  43. 35 3
      service/homePage/src/main/java/com/zhentao/entity/MyResource.java
  44. 41 0
      service/homePage/src/main/java/com/zhentao/entity/MyResourceTag.java
  45. 38 0
      service/homePage/src/main/java/com/zhentao/entity/Tag.java
  46. 5 0
      service/homePage/src/main/java/com/zhentao/entity/User.java
  47. 94 0
      service/homePage/src/main/java/com/zhentao/entity/UserFollowUp.java
  48. 26 0
      service/homePage/src/main/java/com/zhentao/mapper/FollowUpStatisticsMapper.java
  49. 8 0
      service/homePage/src/main/java/com/zhentao/mapper/MatchmakerMapper.java
  50. 25 0
      service/homePage/src/main/java/com/zhentao/mapper/MyResourceMapper.java
  51. 30 0
      service/homePage/src/main/java/com/zhentao/mapper/MyResourceTagMapper.java
  52. 13 0
      service/homePage/src/main/java/com/zhentao/mapper/TagMapper.java
  53. 26 0
      service/homePage/src/main/java/com/zhentao/mapper/UserFollowUpMapper.java
  54. 15 1
      service/homePage/src/main/java/com/zhentao/service/ActivityService.java
  55. 39 0
      service/homePage/src/main/java/com/zhentao/service/FollowUpStatisticsService.java
  56. 16 0
      service/homePage/src/main/java/com/zhentao/service/MatchmakerService.java
  57. 26 1
      service/homePage/src/main/java/com/zhentao/service/MyResourceService.java
  58. 19 0
      service/homePage/src/main/java/com/zhentao/service/TagService.java
  59. 28 0
      service/homePage/src/main/java/com/zhentao/service/UserFollowUpService.java
  60. 39 1
      service/homePage/src/main/java/com/zhentao/service/impl/ActivityServiceImpl.java
  61. 145 0
      service/homePage/src/main/java/com/zhentao/service/impl/FollowUpStatisticsServiceImpl.java
  62. 26 1
      service/homePage/src/main/java/com/zhentao/service/impl/MatchmakerServiceImpl.java
  63. 625 7
      service/homePage/src/main/java/com/zhentao/service/impl/MyResourceServiceImpl.java
  64. 26 0
      service/homePage/src/main/java/com/zhentao/service/impl/TagServiceImpl.java
  65. 53 0
      service/homePage/src/main/java/com/zhentao/service/impl/UserFollowUpServiceImpl.java
  66. 63 0
      service/homePage/src/main/java/com/zhentao/vo/ClientDetailVO.java
  67. 102 0
      service/homePage/src/main/java/com/zhentao/vo/MatchResultVO.java
  68. 30 0
      service/homePage/src/main/java/com/zhentao/vo/MyResourceVO.java
  69. 1 0
      service/homePage/src/main/resources/application.yml
  70. 33 0
      service/homePage/src/main/resources/mapper/MatchmakerMapper.xml
  71. 46 3
      service/homePage/src/main/resources/mapper/MyResourceMapper.xml
  72. 29 0
      service/homePage/src/main/resources/mapper/MyResourceTagMapper.xml
  73. 16 0
      service/homePage/src/main/resources/sql/create_my_resource_tag_table.sql
  74. 12 1
      service/websocket/src/main/java/com/zhentao/WebSocketApplication.java
  75. 17 0
      service/websocket/src/main/java/com/zhentao/config/MinioConfig.java
  76. 5 0
      service/websocket/src/main/java/com/zhentao/controller/ChatController.java
  77. 21 4
      service/websocket/src/main/java/com/zhentao/controller/OnlineStatusController.java
  78. 2 2
      service/websocket/src/main/java/com/zhentao/controller/TIMController.java
  79. 3 3
      service/websocket/src/main/resources/application.yml

+ 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
 			}
 		},
 		

+ 28 - 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": {
@@ -105,6 +112,27 @@
 				"navigationStyle": "custom"
 			}
 		},
+		{
+			"path": "pages/matchmaker-workbench/product-detail",
+			"style": {
+				"navigationBarTitleText": "商品详情",
+				"navigationStyle": "custom"
+			}
+		},
+		{
+			"path": "pages/matchmaker-workbench/points-detail",
+			"style": {
+				"navigationBarTitleText": "积分明细",
+				"navigationStyle": "custom"
+			}
+		},
+		{
+			"path": "pages/matchmaker-workbench/earn-points",
+			"style": {
+				"navigationBarTitleText": "赚取积分",
+				"navigationStyle": "custom"
+			}
+		},
 		{
 			"path": "pages/page3/page3",
 			"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'
 				})

+ 777 - 136
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>
@@ -37,7 +42,7 @@
 					<text class="copy-btn" @click="handleCopy(clientInfo.contact)">复制</text>
 				</view>
 			</view>
-			<view class="contact-item">
+			<view class="contact-item" v-if="clientInfo.backupContact">
 				<text class="contact-label">备用联系方式</text>
 				<view class="contact-value">
 					<text class="contact-number">{{ clientInfo.backupContact }}</text>
@@ -77,8 +82,6 @@
 
 		<!-- 详细信息 -->
 		<scroll-view scroll-y class="details-section" v-if="activeTab === 'details'">
-			<view class="edit-btn" @click="handleEdit">编辑</view>
-
 			<!-- 基本信息 -->
 			<view class="detail-item">
 				<text class="detail-label">婚姻状况:</text>
@@ -94,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>
@@ -102,7 +108,7 @@
 			</view>
 
 			<!-- 择偶要求详情 -->
-			<view class="sub-section-title">择偶要求</view>
+			<view class="sub-section-title requirement-section-title">择偶详细要求</view>
 			<view class="detail-item">
 				<text class="detail-label">年龄范围:</text>
 				<text class="detail-value">{{ clientInfo.details.requirement.ageRange }}</text>
@@ -125,7 +131,7 @@
 			</view>
 
 			<!-- 客户标签 -->
-			<view class="sub-section-title">客户标签</view>
+			<view class="sub-section-title tags-section-title">客户标签</view>
 			<view class="client-tags">
 				<text class="tag" v-for="(tag, index) in clientInfo.tags" :key="index">{{ tag }}</text>
 			</view>
@@ -152,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>
@@ -199,38 +209,42 @@ 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: '小明',
+				name: '',
 				gender: '男',
 				status: '已匹配',
-				tags: ['气质男', '小清新'],
-				avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?ixlib=rb-1.2.1&auto=format&fit=crop&w=200&q=60',
-				requirement: '165+ 本科 天津 本地户口 无婚姻经历',
-				contact: '123****9912',
-				backupContact: '155****4512',
+				tags: [],
+				avatar: '',
+				requirement: '暂无要求',
+				contact: '',
+				backupContact: '',
+				originalPhone: '', // 保存原始手机号用于复制
+				originalBackupPhone: '', // 保存原始备用手机号用于复制
 				stats: {
-					followTimes: 12,
-					matchCount: 3,
-					phoneCalls: 5,
-					interviews: 7
+					followTimes: 0,
+					matchCount: 0,
+					phoneCalls: 0,
+					interviews: 0
 				},
 				details: {
-					maritalStatus: '离异',
-					education: '本科',
-					occupation: '企业高管',
-					income: '50万/年',
-					housing: '已购房',
+					maritalStatus: '',
+					education: '',
+					occupation: '',
+					income: '',
+					housing: '',
 					requirement: {
-						ageRange: '30-40岁',
-						height: '175cm以上',
-						maritalStatus: '离异无孩或未婚',
-						education: '本科及以上',
-						other: '外貌或者性格'
+						ageRange: '',
+						height: '',
+						maritalStatus: '',
+						education: '',
+						other: ''
 					}
 				}
 			},
@@ -242,13 +256,47 @@ export default {
 		if (stored === true || stored === false) {
 			this.isDarkTheme = stored
 		}
+		
+		// 如果已经有resourceId,刷新统计数据
+		if (this.resourceId) {
+			this.loadFollowStatistics(this.resourceId)
+		}
 	},
 	onLoad(options) {
-		// 从URL参数获取客户ID
-		if (options.id) {
-			// 这里可以根据ID从API获取客户信息
-			console.log('客户ID:', options.id)
-			// this.loadClientInfo(options.id)
+		console.log('=== 客户详情页面 onLoad ===')
+		console.log('options:', JSON.stringify(options, null, 2))
+		
+		// 从URL参数获取资源ID
+		let resourceId = null
+		if (options.resourceId) {
+			resourceId = options.resourceId
+			console.log('从resourceId参数获取:', resourceId)
+		} else if (options.id) {
+			// 兼容旧的id参数
+			resourceId = options.id
+			console.log('从id参数获取 (兼容):', resourceId)
+		}
+		
+		if (resourceId) {
+			// 确保resourceId是数字类型
+			resourceId = parseInt(resourceId)
+			if (isNaN(resourceId) || resourceId <= 0) {
+				console.error('资源ID无效:', resourceId)
+				uni.showToast({
+					title: '资源ID无效',
+					icon: 'none'
+				})
+				return
+			}
+			console.log('最终使用的resourceId:', resourceId, '类型:', typeof resourceId)
+			this.resourceId = resourceId
+			this.loadClientInfo(resourceId)
+		} else {
+			console.error('未获取到资源ID')
+			uni.showToast({
+				title: '未获取到资源ID',
+				icon: 'none'
+			})
 		}
 	},
 	methods: {
@@ -258,13 +306,30 @@ export default {
 		},
 		// 复制联系方式
 		handleCopy(contact) {
+			// 如果传入的是脱敏的手机号,使用原始手机号
+			let phoneToCopy = contact
+			if (contact === this.clientInfo.contact && this.clientInfo.originalPhone) {
+				phoneToCopy = this.clientInfo.originalPhone
+			} else if (contact === this.clientInfo.backupContact && this.clientInfo.originalBackupPhone) {
+				phoneToCopy = this.clientInfo.originalBackupPhone
+			} else {
+				// 尝试从脱敏号码中提取
+				phoneToCopy = contact.replace(/\*+/g, '')
+			}
+			
 			uni.setClipboardData({
-				data: contact.replace(/\*+/g, ''),
+				data: phoneToCopy,
 				success: () => {
 					uni.showToast({
 						title: '复制成功',
 						icon: 'success'
 					})
+				},
+				fail: () => {
+					uni.showToast({
+						title: '复制失败',
+						icon: 'none'
+					})
 				}
 			})
 		},
@@ -272,37 +337,488 @@ export default {
 		switchTab(tab) {
 			this.activeTab = tab
 		},
-		// 编辑客户信息
-		handleEdit() {
-			uni.showToast({
-				title: '编辑功能正在开发中',
-				icon: 'none'
-			})
-		},
 		// 邀约日期变更
 		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(id) {
+		async loadClientInfo(resourceId) {
 			try {
-				// 这里调用API获取客户信息
-				// const res = await api.matchmaker.getClientInfo(id)
-				// this.clientInfo = res.data
+				console.log('=== 开始加载客户信息 ===')
+				console.log('resourceId:', resourceId, '类型:', typeof resourceId)
+				
+				uni.showLoading({
+					title: '加载中...'
+				})
+				
+				const baseUrl = process.env.NODE_ENV === 'development' 
+					? 'http://localhost:8083/api'  // 开发环境 - 通过网关
+					: 'https://your-domain.com/api'  // 生产环境
+				
+				const requestUrl = `${baseUrl}/my-resource/client-detail/${resourceId}`
+				console.log('请求URL:', requestUrl)
+				
+				const [error, res] = await uni.request({
+					url: requestUrl,
+					method: 'GET',
+					timeout: 30000, // 设置30秒超时
+					header: {
+						'Content-Type': 'application/json'
+					}
+				})
+				
+				console.log('请求结果 - error:', error)
+				console.log('请求结果 - res:', res)
+				
+				uni.hideLoading()
+				
+				if (error) {
+					console.error('加载客户信息失败:', error)
+					let errorMsg = '加载失败'
+					if (error.errMsg) {
+						if (error.errMsg.includes('timeout') || error.errMsg.includes('超时')) {
+							errorMsg = '请求超时,请检查网络连接'
+						} else if (error.errMsg.includes('fail')) {
+							errorMsg = '网络连接失败,请检查服务器是否启动'
+						}
+					}
+					uni.showToast({
+						title: errorMsg,
+						icon: 'none',
+						duration: 3000
+					})
+					return
+				}
+				
+				if (res.statusCode === 200 && res.data && res.data.code === 200) {
+					const data = res.data.data
+					
+					console.log('=== 客户详情数据 ===')
+					console.log('完整数据:', JSON.stringify(data, null, 2))
+					
+					// 支持驼峰和下划线两种字段名格式
+					const resourceId = data.resourceId || data.resource_id
+					const avatarUrl = data.avatarUrl || data.avatar_url
+					const backupPhone = data.backupPhone || data.backup_phone
+					const marrStatus = data.marrStatus !== undefined ? data.marrStatus : data.marr_status
+					// 择偶要求从my_resource表的mate_selection_criteria字段获取
+					const mateSelectionCriteria = data.mateSelectionCriteria || data.mate_selection_criteria
+					
+					// 择偶要求详情字段(从partner_requirement表,用于详情页)
+					const minAge = data.minAge !== undefined ? data.minAge : data.min_age
+					const maxAge = data.maxAge !== undefined ? data.maxAge : data.max_age
+					const minHeight = data.minHeight !== undefined ? data.minHeight : data.min_height
+					const maxHeight = data.maxHeight !== undefined ? data.maxHeight : data.max_height
+					const educationLevel = data.educationLevel !== undefined ? data.educationLevel : data.education_level
+					const salaryRange = data.salaryRange !== undefined ? data.salaryRange : data.salary_range
+					const houseRequirement = data.houseRequirement !== undefined ? data.houseRequirement : data.house_requirement
+					const carRequirement = data.carRequirement !== undefined ? data.carRequirement : data.car_requirement
+					const maritalStatusRequirement = data.maritalStatusRequirement !== undefined ? data.maritalStatusRequirement : data.marital_status_requirement
+					const preferredCity = data.preferredCity || data.preferred_city
+					const otherRequirements = data.otherRequirements || data.other_requirements
+					
+					console.log('资源ID:', resourceId)
+					console.log('姓名:', data.name)
+					console.log('头像URL:', avatarUrl)
+					console.log('备用手机号:', backupPhone)
+					console.log('择偶要求(mate_selection_criteria):', mateSelectionCriteria)
+					console.log('择偶要求详情数据:', {
+						minAge, maxAge, minHeight, maxHeight,
+						educationLevel, salaryRange, houseRequirement,
+						carRequirement, maritalStatusRequirement,
+						preferredCity, otherRequirements
+					})
+					
+					// 处理标签列表(从后端返回的tags字段,如果没有则使用星座和职业)
+					let clientTags = []
+					if (data.tags && Array.isArray(data.tags) && data.tags.length > 0) {
+						// 使用后端返回的标签
+						clientTags = data.tags
+					} else {
+						// 如果没有tags,使用星座和职业作为标签
+						if (data.constellation) {
+							clientTags.push(data.constellation)
+						}
+						if (data.occupation) {
+							clientTags.push(data.occupation)
+						}
+					}
+					
+					// 加载匹配状态和邀约状态
+					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,
+						name: data.name || '',
+						gender: data.gender === 1 ? '男' : data.gender === 2 ? '女' : '未知',
+						status: '已匹配', // 可以根据实际字段判断
+						tags: clientTags, // 使用处理后的标签列表
+						avatar: avatarUrl || '',
+						requirement: mateSelectionCriteria || '暂无要求', // 择偶要求显示my_resource表的mate_selection_criteria字段
+						contact: data.phone ? data.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') : '',
+						backupContact: backupPhone ? backupPhone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') : '',
+						originalPhone: data.phone || '', // 保存原始手机号
+						originalBackupPhone: backupPhone || '', // 保存原始备用手机号
+						stats: {
+							followTimes: 0, // 将从统计数据接口获取
+							matchCount: 0,
+							phoneCalls: 0,
+							interviews: 0
+						},
+						details: {
+							maritalStatus: this.formatMaritalStatus(marrStatus),
+							education: data.diploma || '未知',
+							occupation: data.occupation || '未知',
+							income: data.income || '未知',
+							housing: data.house === 1 ? '已购房' : data.house === 0 ? '未购房' : '未知',
+							requirement: {
+								ageRange: minAge && maxAge ? `${minAge}-${maxAge}岁` : '不限',
+								height: minHeight ? `${minHeight}cm以上` : '不限',
+								maritalStatus: this.formatMaritalStatusRequirement(maritalStatusRequirement),
+								education: this.formatEducationLevel(educationLevel),
+								other: otherRequirements || ''
+							}
+						}
+					}
+					
+					// 标签已经在上面处理过了,这里不需要再添加
+					
+					console.log('=== 映射后的clientInfo ===')
+					console.log('clientInfo:', JSON.stringify(this.clientInfo, null, 2))
+					
+					// 加载跟进统计数据
+					this.loadFollowStatistics(resourceId)
+				} else {
+					uni.showToast({
+						title: res.data.message || '加载失败',
+						icon: 'none'
+					})
+				}
 			} catch (e) {
+				uni.hideLoading()
 				console.error('加载客户信息失败:', e)
 				uni.showToast({
 					title: '加载失败',
 					icon: 'none'
 				})
 			}
+		},
+		// 格式化婚姻状况
+		formatMaritalStatus(status) {
+			if (status === null || status === undefined) return '未知'
+			switch (status) {
+				case 0: return '未婚'
+				case 1: return '离异'
+				case 2: return '丧偶'
+				default: return '未知'
+			}
+		},
+		// 格式化婚姻状况要求
+		formatMaritalStatusRequirement(status) {
+			if (status === null || status === undefined) return '不限'
+			switch (status) {
+				case 0: return '不限'
+				case 1: return '未婚'
+				case 2: return '离异'
+				case 3: return '丧偶'
+				default: return '不限'
+			}
+		},
+		// 格式化学历要求
+		formatEducationLevel(level) {
+			if (level === null || level === undefined) return '不限'
+			switch (level) {
+				case 0: return '不限'
+				case 1: return '高中及以上'
+				case 2: return '专科及以上'
+				case 3: return '本科及以上'
+				case 4: return '硕士及以上'
+				case 5: return '博士及以上'
+				default: return '不限'
+			}
 		}
 	}
 }
@@ -311,7 +827,7 @@ export default {
 <style lang="scss" scoped>
 .client-detail {
 	min-height: 100vh;
-	background-color: #FFF9F9;
+	background: linear-gradient(180deg, #FFF5F8 0%, #F8E8F0 50%, #FFF9F9 100%);
 	padding-top: 90rpx;
 }
 
@@ -353,11 +869,12 @@ export default {
 .client-basic-info {
 	display: flex;
 	align-items: flex-start;
-	padding: 30rpx;
-	background-color: #FFFFFF;
+	padding: 35rpx;
+	background: linear-gradient(135deg, #FFFFFF 0%, #FFFBFD 100%);
 	margin: 20rpx;
-	border-radius: 20rpx;
-	box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
+	border-radius: 24rpx;
+	box-shadow: 0 8rpx 24rpx rgba(156, 39, 176, 0.12), 0 2rpx 8rpx rgba(156, 39, 176, 0.08);
+	border: 1rpx solid rgba(243, 229, 245, 0.8);
 
 	.client-avatar {
 		width: 160rpx;
@@ -371,17 +888,57 @@ export default {
 		flex: 1;
 
 		.client-name {
-			font-size: 36rpx;
-			font-weight: bold;
-			color: #333;
-			margin-bottom: 10rpx;
+			font-size: 38rpx;
+			font-weight: 600;
+			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 {
-			font-size: 28rpx;
+			font-size: 26rpx;
 			color: #FF6B8A;
-			margin-bottom: 15rpx;
+			margin-bottom: 18rpx;
 			display: inline-block;
+			padding: 4rpx 16rpx;
+			background: linear-gradient(135deg, #FFE5EB 0%, #FFF0F5 100%);
+			border-radius: 12rpx;
+			font-weight: 500;
 		}
 
 		.client-tags {
@@ -390,12 +947,14 @@ export default {
 			gap: 10rpx;
 
 			.tag {
-				padding: 8rpx 20rpx;
-				background-color: #F3E5F5;
-				color: #9C27B0;
-				border-radius: 20rpx;
+				padding: 10rpx 22rpx;
+				background: linear-gradient(135deg, #F3E5F5 0%, #E1BEE7 100%);
+				color: #7B1FA2;
+				border-radius: 24rpx;
 				font-size: 24rpx;
-				font-weight: bold;
+				font-weight: 600;
+				box-shadow: 0 2rpx 8rpx rgba(156, 39, 176, 0.15);
+				border: 1rpx solid rgba(156, 39, 176, 0.2);
 			}
 		}
 	}
@@ -403,11 +962,12 @@ export default {
 
 /* 择偶要求 */
 .requirement-section {
-	padding: 30rpx;
-	background-color: #FFFFFF;
+	padding: 35rpx;
+	background: linear-gradient(135deg, #FFFFFF 0%, #FFFBFD 100%);
 	margin: 0 20rpx 20rpx;
-	border-radius: 20rpx;
-	box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
+	border-radius: 24rpx;
+	box-shadow: 0 8rpx 24rpx rgba(156, 39, 176, 0.12), 0 2rpx 8rpx rgba(156, 39, 176, 0.08);
+	border: 1rpx solid rgba(243, 229, 245, 0.8);
 
 	.section-title {
 		display: flex;
@@ -420,26 +980,29 @@ export default {
 		}
 
 		.title-text {
-			font-size: 28rpx;
-			font-weight: bold;
-			color: #333;
+			font-size: 30rpx;
+			font-weight: 600;
+			color: #2C2C2C;
+			letter-spacing: 0.5rpx;
 		}
 	}
 
 	.requirement-content {
 		font-size: 28rpx;
-		color: #666;
-		line-height: 1.5;
+		color: #555;
+		line-height: 1.8;
+		font-weight: 400;
 	}
 }
 
 /* 联系方式 */
 .contact-section {
-	padding: 30rpx;
-	background-color: #FFFFFF;
+	padding: 35rpx;
+	background: linear-gradient(135deg, #FFFFFF 0%, #FFFBFD 100%);
 	margin: 0 20rpx 20rpx;
-	border-radius: 20rpx;
-	box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
+	border-radius: 24rpx;
+	box-shadow: 0 8rpx 24rpx rgba(156, 39, 176, 0.12), 0 2rpx 8rpx rgba(156, 39, 176, 0.08);
+	border: 1rpx solid rgba(243, 229, 245, 0.8);
 
 	.contact-item {
 		display: flex;
@@ -453,7 +1016,8 @@ export default {
 
 		.contact-label {
 			font-size: 28rpx;
-			color: #333;
+			color: #2C2C2C;
+			font-weight: 500;
 		}
 
 		.contact-value {
@@ -463,16 +1027,25 @@ export default {
 
 			.contact-number {
 				font-size: 28rpx;
-				color: #666;
+				color: #555;
+				font-weight: 400;
 			}
 
 			.copy-btn {
-				padding: 8rpx 20rpx;
-				background-color: #F3E5F5;
-				color: #9C27B0;
-				border-radius: 20rpx;
+				padding: 10rpx 24rpx;
+				background: linear-gradient(135deg, #F3E5F5 0%, #E1BEE7 100%);
+				color: #7B1FA2;
+				border-radius: 24rpx;
 				font-size: 24rpx;
-				font-weight: bold;
+				font-weight: 600;
+				box-shadow: 0 2rpx 8rpx rgba(156, 39, 176, 0.2);
+				border: 1rpx solid rgba(156, 39, 176, 0.2);
+				transition: all 0.3s;
+				
+				&:active {
+					transform: scale(0.95);
+					box-shadow: 0 1rpx 4rpx rgba(156, 39, 176, 0.3);
+				}
 			}
 		}
 	}
@@ -526,87 +1099,155 @@ export default {
 .tabs-section {
 	display: flex;
 	padding: 0 20rpx;
-	background-color: #FFFFFF;
+	background: linear-gradient(135deg, #FFFFFF 0%, #FFFBFD 100%);
 	margin: 0 20rpx 20rpx;
-	border-radius: 20rpx;
-	box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
+	border-radius: 24rpx;
+	box-shadow: 0 8rpx 24rpx rgba(156, 39, 176, 0.12), 0 2rpx 8rpx rgba(156, 39, 176, 0.08);
+	border: 1rpx solid rgba(243, 229, 245, 0.8);
 
 	.tab {
 		flex: 1;
 		text-align: center;
-		padding: 20rpx 0;
-		font-size: 28rpx;
-		color: #666;
-		font-weight: bold;
+		padding: 24rpx 0;
+		font-size: 30rpx;
+		color: #888;
+		font-weight: 500;
 		transition: all 0.3s;
+		position: relative;
 
 		&.active {
-			color: #9C27B0;
-			border-bottom: 4rpx solid #9C27B0;
+			color: #7B1FA2;
+			font-weight: 600;
+			
+			&::after {
+				content: '';
+				position: absolute;
+				bottom: 0;
+				left: 50%;
+				transform: translateX(-50%);
+				width: 60rpx;
+				height: 6rpx;
+				background: linear-gradient(90deg, #9C27B0 0%, #E1BEE7 100%);
+				border-radius: 3rpx;
+			}
 		}
 	}
 }
 
 /* 详细信息滚动区域 */
 .details-section {
-	padding: 30rpx 20rpx 100rpx;
-	background-color: #FFFFFF;
+	padding: 35rpx 25rpx 100rpx;
+	background: linear-gradient(135deg, #FFFFFF 0%, #FFFBFD 100%);
 	margin: 0 20rpx 20rpx;
-	border-radius: 20rpx;
-	box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
+	border-radius: 24rpx;
+	box-shadow: 0 8rpx 24rpx rgba(156, 39, 176, 0.12), 0 2rpx 8rpx rgba(156, 39, 176, 0.08);
+	border: 1rpx solid rgba(243, 229, 245, 0.8);
 	box-sizing: border-box;
-
-	.edit-btn {
-		position: absolute;
-		top: 20rpx;
-		right: 30rpx;
-		padding: 8rpx 25rpx;
-		background-color: #F3E5F5;
-		color: #9C27B0;
-		border-radius: 20rpx;
-		font-size: 24rpx;
-		font-weight: bold;
-	}
+	position: relative;
 
 	.detail-item {
 		display: flex;
-		margin-bottom: 25rpx;
+		margin-bottom: 28rpx;
+		padding: 12rpx 0;
+		border-bottom: 1rpx solid rgba(240, 240, 240, 0.6);
+		
+		&:last-of-type {
+			border-bottom: none;
+		}
 
 		.detail-label {
-			width: 180rpx;
+			width: 200rpx;
 			font-size: 28rpx;
-			color: #333;
-			font-weight: bold;
+			color: #2C2C2C;
+			font-weight: 500;
+			letter-spacing: 0.3rpx;
 		}
 
 		.detail-value {
 			flex: 1;
 			font-size: 28rpx;
-			color: #666;
+			color: #555;
+			font-weight: 400;
+			text-align: right;
 		}
 	}
 
 	.sub-section-title {
-		font-size: 30rpx;
-		font-weight: bold;
-		color: #333;
+		font-size: 32rpx;
+		font-weight: 600;
+		color: #2C2C2C;
 		margin: 30rpx 0 20rpx;
-		padding-bottom: 10rpx;
-		border-bottom: 2rpx solid #F0F0F0;
+		padding-bottom: 12rpx;
+		border-bottom: 2rpx solid rgba(240, 240, 240, 0.8);
+		letter-spacing: 0.5rpx;
+	}
+	
+	/* 择偶详细要求部分 - 添加明显的上分隔 */
+	.requirement-section-title {
+		margin-top: 60rpx;
+		padding: 32rpx 24rpx 20rpx;
+		border-top: 3rpx solid rgba(156, 39, 176, 0.15);
+		border-bottom: 2rpx solid rgba(156, 39, 176, 0.1);
+		position: relative;
+		background: linear-gradient(135deg, #F8F4F9 0%, #FFFBFD 100%);
+		border-radius: 16rpx 16rpx 0 0;
+	}
+	
+	.requirement-section-title::before {
+		content: '';
+		position: absolute;
+		left: 0;
+		top: 0;
+		width: 8rpx;
+		height: 100%;
+		background: linear-gradient(180deg, #9C27B0 0%, #BA68C8 50%, #E1BEE7 100%);
+		border-radius: 0 4rpx 4rpx 0;
+		box-shadow: 2rpx 0 8rpx rgba(156, 39, 176, 0.2);
+	}
+	
+	/* 客户标签部分 - 添加明显的上分隔 */
+	.tags-section-title {
+		margin-top: 60rpx;
+		padding: 32rpx 24rpx 20rpx;
+		border-top: 3rpx solid rgba(156, 39, 176, 0.15);
+		border-bottom: 2rpx solid rgba(156, 39, 176, 0.1);
+		position: relative;
+		background: linear-gradient(135deg, #F8F4F9 0%, #FFFBFD 100%);
+		border-radius: 16rpx 16rpx 0 0;
+	}
+	
+	.tags-section-title::before {
+		content: '';
+		position: absolute;
+		left: 0;
+		top: 0;
+		width: 8rpx;
+		height: 100%;
+		background: linear-gradient(180deg, #9C27B0 0%, #BA68C8 50%, #E1BEE7 100%);
+		border-radius: 0 4rpx 4rpx 0;
+		box-shadow: 2rpx 0 8rpx rgba(156, 39, 176, 0.2);
 	}
 
 	.client-tags {
 		display: flex;
 		flex-wrap: wrap;
-		gap: 10rpx;
+		gap: 12rpx;
+		padding: 20rpx 0;
 
 		.tag {
-			padding: 8rpx 20rpx;
-			background-color: #F3E5F5;
-			color: #9C27B0;
-			border-radius: 20rpx;
-			font-size: 24rpx;
-			font-weight: bold;
+			padding: 12rpx 24rpx;
+			background: linear-gradient(135deg, #F3E5F5 0%, #E1BEE7 100%);
+			color: #7B1FA2;
+			border-radius: 24rpx;
+			font-size: 26rpx;
+			font-weight: 600;
+			box-shadow: 0 2rpx 8rpx rgba(156, 39, 176, 0.15);
+			border: 1rpx solid rgba(156, 39, 176, 0.2);
+			transition: all 0.3s;
+			
+			&:active {
+				transform: scale(0.95);
+			}
 		}
 	}
 }

+ 323 - 0
LiangZhiYUMao/pages/matchmaker-workbench/earn-points.vue

@@ -0,0 +1,323 @@
+<template>
+  <view class="earn-points">
+    <!-- 顶部导航栏 -->
+    <view class="header">
+      <view class="back-icon" @click="handleBack"></view>
+      <text class="header-title">赚取积分</text>
+      <view class="header-right"></view>
+    </view>
+
+    <!-- 当前积分 -->
+    <view class="current-points">
+      <text class="label">当前积分</text>
+      <text class="value">{{ balance }}</text>
+    </view>
+
+    <!-- 积分规则列表 -->
+    <view class="rules-section">
+      <view class="section-title">积分获取方式</view>
+      
+      <view class="rule-list">
+        <view class="rule-item" v-for="(rule, index) in rules" :key="index">
+          <view class="rule-icon">{{ getIcon(rule.ruleType) }}</view>
+          <view class="rule-info">
+            <view class="rule-name">{{ rule.ruleName }}</view>
+            <view class="rule-desc">{{ getDesc(rule.ruleType) }}</view>
+          </view>
+          <view class="rule-points">+{{ rule.pointValue }}</view>
+          <view class="rule-action" @click="handleAction(rule)">
+            {{ getActionText(rule.ruleType) }}
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <!-- 积分说明 -->
+    <view class="tips-section">
+      <view class="section-title">积分说明</view>
+      <view class="tips-content">
+        <view class="tip-item">1. 积分可在积分商城兑换精美礼品</view>
+        <view class="tip-item">2. 每日签到可获得积分奖励</view>
+        <view class="tip-item">3. 上传优质线索可获得额外积分</view>
+        <view class="tip-item">4. 撮合成功可获得大量积分奖励</view>
+        <view class="tip-item">5. 积分永久有效,请放心使用</view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import api from '@/utils/api.js'
+
+export default {
+  name: 'earn-points',
+  data() {
+    return {
+      balance: 0,
+      rules: [],
+      makerId: null
+    }
+  },
+  onLoad() {
+    this.initData()
+  },
+  methods: {
+    async initData() {
+      const userInfo = uni.getStorageSync('userInfo')
+      if (userInfo && userInfo.matchmakerId) {
+        this.makerId = userInfo.matchmakerId
+      } else if (userInfo && userInfo.userId) {
+        this.makerId = userInfo.userId
+      }
+      
+      await Promise.all([
+        this.loadBalance(),
+        this.loadRules()
+      ])
+    },
+    
+    async loadBalance() {
+      if (!this.makerId) return
+      try {
+        const res = await api.pointsMall.getBalance(this.makerId)
+        this.balance = res.balance || 0
+      } catch (e) {
+        console.error('获取积分余额失败:', e)
+      }
+    },
+    
+    async loadRules() {
+      try {
+        const res = await api.pointsMall.getRules()
+        this.rules = res || []
+      } catch (e) {
+        console.error('获取积分规则失败:', e)
+      }
+    },
+    
+    handleBack() {
+      uni.navigateBack()
+    },
+    
+    getIcon(ruleType) {
+      const icons = {
+        1: '📅', // 签到
+        2: '📤', // 上传线索
+        3: '💕', // 撮合成功
+        4: '📚', // 完成培训
+        5: '🎁'  // 活动奖励
+      }
+      return icons[ruleType] || '⭐'
+    },
+    
+    getDesc(ruleType) {
+      const descs = {
+        1: '每日签到即可获得积分',
+        2: '上传优质客户线索',
+        3: '成功撮合一对情侣',
+        4: '完成红娘培训课程',
+        5: '参与平台活动获得'
+      }
+      return descs[ruleType] || '完成任务获得积分'
+    },
+    
+    getActionText(ruleType) {
+      const texts = {
+        1: '去签到',
+        2: '去上传',
+        3: '查看',
+        4: '去学习',
+        5: '查看'
+      }
+      return texts[ruleType] || '去完成'
+    },
+    
+    async handleAction(rule) {
+      if (rule.ruleType === 1) {
+        // 签到
+        await this.doSignIn(rule)
+      } else if (rule.ruleType === 2) {
+        // 上传线索
+        uni.navigateTo({
+          url: '/pages/matchmaker-workbench/add-resource'
+        })
+      } else if (rule.ruleType === 4) {
+        // 培训课程
+        uni.navigateTo({
+          url: '/pages/courses/list'
+        })
+      } else {
+        uni.showToast({
+          title: '功能开发中',
+          icon: 'none'
+        })
+      }
+    },
+    
+    async doSignIn(rule) {
+      if (!this.makerId) {
+        uni.showToast({ title: '请先登录', icon: 'none' })
+        return
+      }
+      
+      try {
+        const res = await api.pointsMall.addPoints(this.makerId, rule.ruleType, '每日签到')
+        uni.showToast({
+          title: `签到成功,+${res.addedPoints}积分`,
+          icon: 'success'
+        })
+        this.balance = res.newBalance
+      } catch (e) {
+        console.error('签到失败:', e)
+        uni.showToast({
+          title: e.message || '签到失败',
+          icon: 'none'
+        })
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.earn-points {
+  min-height: 100vh;
+  background: #F5F5F5;
+}
+
+.header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 25rpx 30rpx;
+  padding-top: calc(25rpx + env(safe-area-inset-top));
+  background: #FFFFFF;
+  
+  .back-icon {
+    width: 44rpx;
+    height: 44rpx;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 32rpx;
+    color: #333;
+    &::before { content: '‹'; }
+  }
+  
+  .header-title {
+    font-size: 36rpx;
+    font-weight: bold;
+    color: #333;
+  }
+  
+  .header-right {
+    width: 44rpx;
+  }
+}
+
+.current-points {
+  background: linear-gradient(135deg, #9C27B0 0%, #E91E63 100%);
+  margin: 30rpx;
+  padding: 40rpx;
+  border-radius: 20rpx;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  
+  .label {
+    font-size: 30rpx;
+    color: rgba(255, 255, 255, 0.8);
+  }
+  
+  .value {
+    font-size: 56rpx;
+    font-weight: bold;
+    color: #FFFFFF;
+  }
+}
+
+.rules-section {
+  background: #FFFFFF;
+  margin: 0 30rpx 30rpx;
+  border-radius: 20rpx;
+  padding: 30rpx;
+  
+  .section-title {
+    font-size: 30rpx;
+    font-weight: bold;
+    color: #333;
+    margin-bottom: 30rpx;
+  }
+}
+
+.rule-list {
+  .rule-item {
+    display: flex;
+    align-items: center;
+    padding: 25rpx 0;
+    border-bottom: 1rpx solid #F0F0F0;
+    
+    &:last-child {
+      border-bottom: none;
+    }
+    
+    .rule-icon {
+      font-size: 48rpx;
+      margin-right: 20rpx;
+    }
+    
+    .rule-info {
+      flex: 1;
+      
+      .rule-name {
+        font-size: 28rpx;
+        font-weight: bold;
+        color: #333;
+        margin-bottom: 8rpx;
+      }
+      
+      .rule-desc {
+        font-size: 24rpx;
+        color: #999;
+      }
+    }
+    
+    .rule-points {
+      font-size: 32rpx;
+      font-weight: bold;
+      color: #FF9800;
+      margin-right: 20rpx;
+    }
+    
+    .rule-action {
+      padding: 12rpx 24rpx;
+      background: linear-gradient(135deg, #9C27B0 0%, #E91E63 100%);
+      color: #FFFFFF;
+      font-size: 24rpx;
+      border-radius: 30rpx;
+    }
+  }
+}
+
+.tips-section {
+  background: #FFFFFF;
+  margin: 0 30rpx 30rpx;
+  border-radius: 20rpx;
+  padding: 30rpx;
+  
+  .section-title {
+    font-size: 30rpx;
+    font-weight: bold;
+    color: #333;
+    margin-bottom: 20rpx;
+  }
+  
+  .tips-content {
+    .tip-item {
+      font-size: 26rpx;
+      color: #666;
+      line-height: 2;
+    }
+  }
+}
+</style>

+ 627 - 125
LiangZhiYUMao/pages/matchmaker-workbench/message.vue

@@ -8,67 +8,131 @@
 		</view>
 
 		<scroll-view scroll-y class="content">
-			<!-- 系统通知 -->
-			<view class="message-item system-notification active">
-				<text class="message-type">系统通知</text>
-				<text class="message-time">刚刚</text>
-				<text class="message-content">您的线索审核已通过,获得20积分奖励,当前积分可兑换【资源查看权限】x1</text>
-				<text class="message-footer">完成线索采集,积分+20 (距黄金级还差72分)</text>
+			<!-- 加载中 -->
+			<view v-if="loading" class="loading-container">
+				<text>加载中...</text>
 			</view>
-
-			<!-- 撮合成功通知 -->
-			<view class="message-item match-success">
-				<view class="message-icon heart"></view>
-				<view class="message-body">
-					<text class="message-type">撮合成功通知</text>
-					<text class="message-content">您推荐的王先生和刘女士已成功匹配,获得50积分+100元现金奖励</text>
+			
+			<!-- 消息内容 -->
+			<view v-else-if="conversationList.length > 0">
+				<!-- 系统通知卡片 -->
+				<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>
-				<text class="message-time">10分钟前</text>
-			</view>
-
-			<!-- 今天分组 -->
-			<view class="time-group">
-				<text class="time-label">今天</text>
-			</view>
-
-			<!-- 魏先生消息 -->
-			<view class="message-item user-message">
-				<view class="message-avatar">魏</view>
-				<view class="message-body">
-					<text class="message-type">魏先生</text>
-					<text class="message-content">您好,想了解一下李女士的详细情况,方便沟通吗?</text>
+				
+				<!-- 撮合成功通知卡片 -->
+				<view class="message-item match-success" v-if="matchSuccessNotification">
+					<view class="message-icon heart"></view>
+					<view class="message-body">
+						<text class="message-type">{{ matchSuccessNotification.title }}</text>
+						<text class="message-content">{{ matchSuccessNotification.content }}</text>
+					</view>
+					<text class="message-time">{{ matchSuccessNotification.timeText }}</text>
 				</view>
-				<text class="message-time">1小时前</text>
-			</view>
-
-			<!-- 昨日分组 -->
-			<view class="time-group">
-				<text class="time-label">昨日</text>
-			</view>
-
-			<!-- 张先生消息 -->
-			<view class="message-item user-message">
-				<view class="message-avatar">张</view>
-				<view class="message-body">
-					<text class="message-type">张先生</text>
-					<text class="message-content">您好,想了解一下王女士的详细情况,方便沟通吗?</text>
+				
+				<!-- 今天 -->
+				<view v-if="todayConversations.length > 0">
+					<view class="time-group">
+						<text class="time-label">今天</text>
+					</view>
+					<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>
-				<text class="message-time">1小时前</text>
-			</view>
-
-			<!-- 更早分组 -->
-			<view class="time-group">
-				<text class="time-label">更早</text>
-			</view>
-
-			<!-- 高先生消息 -->
-			<view class="message-item user-message">
-				<view class="message-avatar">高</view>
-				<view class="message-body">
-					<text class="message-type">高先生</text>
-					<text class="message-content">您好,想了解一下陈女士的详细情况,方便沟通吗?</text>
+				
+				<!-- 昨日 -->
+				<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>
-				<text class="message-time">1小时前</text>
+				
+				<!-- 更早 -->
+				<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>
+			</view>
+			
+			<!-- 空状态 -->
+			<view v-else class="empty-container">
+				<text>暂无消息</text>
 			</view>
 		</scroll-view>
 
@@ -88,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>
@@ -101,65 +167,454 @@
 </template>
 
 <script>
+	import timManager from '@/utils/tim-manager.js'
+	import TIM from 'tim-wx-sdk'
+	import api from '@/utils/api.js'
+	
 	export default {
 		data() {
 			return {
-				messages: [
-					{
-						id: 1,
-						type: 'system',
-						title: '系统通知',
-						content: '您的线索审核已通过,获得20积分奖励,当前积分可兑换【资源查看权限】x1',
-						footer: '完成线索采集,积分+20 (距黄金级还差72分)',
-						time: '刚刚',
-						isNew: true
-					},
-					{
-						id: 2,
-						type: 'matchSuccess',
-						title: '撮合成功通知',
-						content: '您推荐的王先生和刘女士已成功匹配,获得50积分+100元现金奖励',
-						time: '10分钟前',
-						isNew: false
-					},
-					{
-						id: 3,
-						type: 'user',
-						title: '王先生',
-						content: '您好,想了解一下李女士的详细情况,方便沟通吗?',
-						time: '1小时前',
-						isNew: false
-					}
-				]
+				loading: true,
+				conversationList: [],
+				matchmakerInfo: null,
+				imUserId: '',
+				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
+				})
+			}
+		},
+		
+		onLoad() {
+			// 为避免误用用户端 IM 账号,这里不再复用当前 TIM 登录状态,进入页面即按红娘流程重新初始化
+			console.log('🔍 红娘消息页强制按红娘流程初始化 IM')
+			this.initIM()
+		},
+		
+		onShow() {
+			// 页面显示时刷新会话列表
+			if (this.imUserId) {
+				this.loadConversationList()
+			}
+			// 同步系统消息未读
+			this.loadSystemUnread()
+		},
+		
+		onUnload() {
+			// 页面卸载时移除监听
+			this.removeListeners()
+		},
+		
 		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 {
+					this.loading = true
+					
+					// 1. 获取当前登录用户ID
+					const userId = uni.getStorageSync('userId')
+					if (!userId) {
+						uni.showToast({ title: '请先登录', icon: 'none' })
+						return
+					}
+					
+					// 2. 获取红娘信息
+					const res = await uni.request({
+						url: 'http://localhost:8081/api/matchmaker/current',
+						method: 'GET',
+						data: { userId }
+					})
+					
+					if (res[1].data.code !== 200) {
+						uni.showToast({ title: '获取红娘信息失败', icon: 'none' })
+						return
+					}
+					
+					this.matchmakerInfo = res[1].data.data
+					this.imUserId = this.matchmakerInfo.imUserId  // m_1
+					
+					console.log('✅ 红娘信息:', this.matchmakerInfo)
+					
+					// 3. 获取 UserSig
+					const sigRes = await uni.request({
+						url: `http://localhost:8083/api/im/getUserSig?userId=${this.imUserId}`,
+						method: 'GET'
+					})
+					
+					if (sigRes[1].data.code !== 200) {
+						uni.showToast({ title: '获取UserSig失败', icon: 'none' })
+						return
+					}
+					
+					const userSig = sigRes[1].data.data.userSig
+					console.log('✅ 获取到 UserSig:', userSig ? '成功' : '失败')
+					
+					// 4. 先登出当前账号(如果已登录)
+					try {
+						await timManager.logout()
+						console.log('✅ 已登出之前的账号')
+					} catch (e) {
+						console.log('⚠️ 登出失败或未登录:', e.message)
+					}
+					
+					// 5. 以红娘身份登录 IM
+					await timManager.login(this.imUserId, userSig)
+					console.log('✅ 红娘已登录 IM:', this.imUserId)
+					
+					// 6. 监听事件
+					this.addListeners()
+					
+					// 7. 加载会话列表
+					// 6. 加载会话列表
+					await this.loadConversationList()
+					
+				} catch (error) {
+					console.error('❌ 初始化 IM 失败:', error)
+					uni.showToast({ title: '初始化失败', icon: 'none' })
+				} finally {
+					this.loading = false
+				}
+			},
+			
+			// 加载会话列表
+			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 || []
+					
+					// 补全用户头像和昵称
+					await this.loadUserAvatars()
+					
+					// 计算总未读数
+					this.totalUnreadCount = this.conversationList.reduce((total, conv) => {
+						return total + (conv.unreadCount || 0)
+					}, 0)
+					
+					console.log('✅ 会话列表:', this.conversationList)
+				} catch (error) {
+					console.error('❌ 加载会话列表失败:', error)
+				}
+			},
+			
+			// 添加事件监听
+			addListeners() {
+				const tim = timManager.getTim()
+				
+				// 监听新消息
+				tim.on(TIM.EVENT.MESSAGE_RECEIVED, this.onMessageReceived)
+				
+				// 监听会话列表更新
+				tim.on(TIM.EVENT.CONVERSATION_LIST_UPDATED, this.onConversationListUpdated)
+			},
+			
+			// 移除事件监听
+			removeListeners() {
+				const tim = timManager.getTim()
+				tim.off(TIM.EVENT.MESSAGE_RECEIVED, this.onMessageReceived)
+				tim.off(TIM.EVENT.CONVERSATION_LIST_UPDATED, this.onConversationListUpdated)
+			},
+			
+			// 收到新消息
+			onMessageReceived(event) {
+				console.log('📩 收到新消息:', event.data)
+				// 刷新会话列表
+				this.loadConversationList()
+			},
+			
+			// 会话列表更新
+			onConversationListUpdated(event) {
+				console.log('🔄 会话列表更新:', event.data)
+				this.conversationList = event.data || []
+				
+				// 补全头像昵称后再计算未读
+				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)
+				}
+			},
+			
+			// 打开聊天页面
+			openChat(conversation) {
+				const targetUserId = conversation.userProfile.userID
+				const targetUserName = encodeURIComponent(conversation.userProfile.nick || `用户${targetUserId}`)
+				const targetUserAvatar = encodeURIComponent(conversation.userProfile.avatar || '')
+				uni.navigateTo({
+					url: `/pages/message/chat?targetUserId=${targetUserId}&targetUserName=${targetUserName}&targetUserAvatar=${targetUserAvatar}&fromMatchmaker=true`
+				})
+			},
+			
+			// 获取最后一条消息的文本
+			getLastMessageText(lastMessage) {
+				if (!lastMessage) return ''
+				
+				switch (lastMessage.type) {
+					case TIM.TYPES.MSG_TEXT:
+						return lastMessage.payload.text
+					case TIM.TYPES.MSG_IMAGE:
+						return '[图片]'
+					case TIM.TYPES.MSG_AUDIO:
+						return '[语音]'
+					case TIM.TYPES.MSG_VIDEO:
+						return '[视频]'
+					case TIM.TYPES.MSG_FILE:
+						return '[文件]'
+					default:
+						return '[消息]'
+				}
+			},
+			
+			// 格式化时间
+			formatTime(timestamp) {
+				if (!timestamp) return ''
+				
+				const now = new Date()
+				const msgTime = new Date(timestamp * 1000)
+				const diff = now - msgTime
+				
+				// 一分钟内
+				if (diff < 60000) {
+					return '刚刚'
+				}
+				
+				// 一小时内
+				if (diff < 3600000) {
+					return Math.floor(diff / 60000) + '分钟前'
+				}
+				
+				// 今天
+				if (msgTime.toDateString() === now.toDateString()) {
+					return msgTime.getHours() + ':' + String(msgTime.getMinutes()).padStart(2, '0')
+				}
+				
+				// 昨天
+				const yesterday = new Date(now)
+				yesterday.setDate(yesterday.getDate() - 1)
+				if (msgTime.toDateString() === yesterday.toDateString()) {
+					return '昨天'
+				}
+				
+				// 更早
+				return (msgTime.getMonth() + 1) + '-' + msgTime.getDate()
+			},
+			
 			// 返回上一页
 			goBack() {
 				uni.navigateBack()
 			},
+			
 			// 导航到工作台
 			navigateToWorkbench() {
 				uni.redirectTo({
 					url: '/pages/matchmaker-workbench/index'
 				})
 			},
+			
 			// 导航到我的资源
 			navigateToMyResources() {
 				uni.redirectTo({
 					url: '/pages/matchmaker-workbench/my-resources'
 				})
 			},
+			
 			// 导航到排行榜
 			navigateToRanking() {
 				uni.redirectTo({
 					url: '/pages/matchmaker-workbench/ranking'
 				})
 			},
+			
 			// 导航到消息
 			navigateToMessage() {
 				// 已在消息页面,无需跳转
 			},
+			
 			// 导航到我的
 			navigateToMine() {
 				uni.redirectTo({
@@ -173,7 +628,8 @@
 <style lang="scss" scoped>
 	.matchmaker-message {
 		min-height: 100vh;
-		background: #FFF9F9;
+		/* 顶部淡粉,中部淡紫,越到底部越接近白色 */
+		background: linear-gradient(180deg, #fff1f7 0%, #f6ebff 45%, #fbf7ff 75%, #ffffff 100%);
 		display: flex;
 		flex-direction: column;
 	}
@@ -185,8 +641,8 @@
 		justify-content: space-between;
 		padding: 25rpx 30rpx;
 		padding-top: calc(25rpx + env(safe-area-inset-top));
-		background: #FFF9F9;
-		border-bottom: 1rpx solid #F0F0F0;
+		background: transparent;
+		border-bottom-width: 0;
 
 		.back-btn {
 			width: 70rpx;
@@ -215,62 +671,63 @@
 
 	.content {
 		flex: 1;
-		padding: 20rpx 20rpx 120rpx;
+		padding: 20rpx 24rpx 140rpx;
+		box-sizing: border-box;
 	}
 
 	/* 消息项 */
 	.message-item {
-		background: #FFFFFF;
-		border-radius: 20rpx;
-		padding: 25rpx;
+		background: #ffffff;
+		border-radius: 24rpx;
+		padding: 28rpx;
 		margin-bottom: 20rpx;
-		box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
+		box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.05);
 		position: relative;
 
 		&.system-notification {
-			background: linear-gradient(135deg, #FFEBEE 0%, #FFCDD2 100%);
-			border: 2rpx solid #F8BBD0;
-
-			&.active {
-				border-color: #E91E63;
-			}
+			background: #ffffff;
+			border-radius: 28rpx;
+			border: 2rpx solid rgba(255, 128, 171, 0.85);
+			box-shadow: 0 10rpx 26rpx rgba(255, 128, 171, 0.28);
+			padding: 26rpx 30rpx;
 
 			.message-type {
 				display: block;
 				font-size: 32rpx;
-				font-weight: bold;
+				font-weight: 600;
 				color: #333;
-				margin-bottom: 15rpx;
+				margin-bottom: 12rpx;
 			}
 
 			.message-time {
-				display: block;
+				position: absolute;
+				top: 26rpx;
+				right: 30rpx;
 				font-size: 24rpx;
-				color: #999;
-				margin-bottom: 15rpx;
-				text-align: right;
+				color: #b88bb0;
 			}
 
 			.message-content {
 				display: block;
-				font-size: 28rpx;
-				color: #333;
-				line-height: 1.5;
-				margin-bottom: 15rpx;
+				font-size: 26rpx;
+				color: #555;
+				line-height: 1.6;
+				margin-bottom: 12rpx;
 			}
 
 			.message-footer {
 				display: block;
 				font-size: 24rpx;
-				color: #666;
+				color: #b27aa0;
 				line-height: 1.4;
 			}
 		}
 
 		&.match-success {
 			display: flex;
-			background: linear-gradient(135deg, #F3E5F5 0%, #E1BEE7 100%);
-			border: 2rpx solid #D1C4E9;
+			background: linear-gradient(135deg, #f8f3ff 0%, #f4e6ff 45%, #ffeaf7 100%);
+			border: 0;
+			box-shadow: 0 8rpx 20rpx rgba(186, 104, 200, 0.25);
 
 			.message-icon {
 				width: 60rpx;
@@ -283,8 +740,8 @@
 			}
 
 			.message-icon.heart {
-				background-color: #F8BBD0;
-				background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23E91E63"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>');
+				background-color: #f8d9ef;
+				background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23e573ab"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>');
 			}
 
 			.message-body {
@@ -294,7 +751,7 @@
 			.message-type {
 				display: block;
 				font-size: 30rpx;
-				font-weight: bold;
+				font-weight: 600;
 				color: #333;
 				margin-bottom: 10rpx;
 			}
@@ -308,21 +765,25 @@
 
 			.message-time {
 				font-size: 24rpx;
-				color: #999;
+				color: #a691c0;
 				margin-top: auto;
 			}
 		}
 
 		&.user-message {
 			display: flex;
-			background: #FFFFFF;
-			border: 2rpx solid #E0E0E0;
+			background: #ffffff;
+			border: 0;
+			box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.04);
+
+			align-items: center;
 
 			.message-avatar {
 				width: 60rpx;
 				height: 60rpx;
 				border-radius: 50%;
-				background: linear-gradient(135deg, #9C27B0 0%, #BA68C8 100%);
+				background: linear-gradient(135deg, #b39ddb 0%, #ce93d8 100%);
+
 				color: #FFFFFF;
 				font-size: 28rpx;
 				font-weight: bold;
@@ -330,35 +791,76 @@
 				align-items: center;
 				justify-content: center;
 				margin-right: 20rpx;
+				flex-shrink: 0;
+			}
+
+			.message-avatar-img {
+				width: 60rpx;
+				height: 60rpx;
+				border-radius: 50%;
+				margin-right: 20rpx;
+				flex-shrink: 0;
 			}
 
 			.message-body {
 				flex: 1;
+				min-width: 0;
 			}
 
 			.message-type {
 				display: block;
 				font-size: 30rpx;
-				font-weight: bold;
+				font-weight: 500;
 				color: #333;
-				margin-bottom: 10rpx;
+				margin-bottom: 6rpx;
 			}
 
 			.message-content {
 				display: block;
 				font-size: 26rpx;
-				color: #666;
+				color: #777;
 				line-height: 1.4;
+				overflow: hidden;
+				text-overflow: ellipsis;
+				white-space: nowrap;
+			}
+
+			.message-right {
+				display: flex;
+				flex-direction: column;
+				align-items: flex-end;
+				margin-left: 20rpx;
 			}
 
 			.message-time {
 				font-size: 24rpx;
-				color: #999;
-				margin-top: auto;
+				color: #b0a6c5;
+				margin-bottom: 10rpx;
+			}
+
+			.unread-badge {
+				background: #ff5c9a;
+				color: #FFFFFF;
+				font-size: 20rpx;
+				padding: 4rpx 10rpx;
+				border-radius: 20rpx;
+				min-width: 36rpx;
+				text-align: center;
 			}
 		}
 	}
 
+	/* 加载和空状态 */
+	.loading-container,
+	.empty-container {
+		display: flex;
+		justify-content: center;
+		align-items: center;
+		padding: 100rpx 0;
+		color: #999;
+		font-size: 28rpx;
+	}
+
 	/* 时间分组 */
 	.time-group {
 		display: flex;
@@ -367,8 +869,8 @@
 
 		.time-label {
 			display: inline-block;
-			background: #FFF3E0;
-			color: #FF9800;
+			background: #ffe6f0;
+			color: #e573ab;
 			font-size: 24rpx;
 			padding: 8rpx 20rpx;
 			border-radius: 20rpx;

Dosya farkı çok büyük olduğundan ihmal edildi
+ 636 - 291
LiangZhiYUMao/pages/matchmaker-workbench/my-resources.vue


+ 282 - 0
LiangZhiYUMao/pages/matchmaker-workbench/points-detail.vue

@@ -0,0 +1,282 @@
+<template>
+  <view class="points-detail">
+    <!-- 顶部导航栏 -->
+    <view class="header">
+      <view class="back-icon" @click="handleBack"></view>
+      <text class="header-title">积分明细</text>
+      <view class="header-right"></view>
+    </view>
+
+    <!-- 积分余额卡片 -->
+    <view class="balance-card">
+      <view class="balance-label">当前积分</view>
+      <view class="balance-value">{{ balance }}</view>
+    </view>
+
+    <!-- 明细列表 -->
+    <view class="records-section">
+      <view class="section-title">积分记录</view>
+      
+      <view v-if="records.length === 0 && !loading" class="empty-tip">
+        暂无积分记录
+      </view>
+      
+      <view class="record-list">
+        <view class="record-item" v-for="(item, index) in records" :key="index">
+          <view class="record-left">
+            <view class="record-icon" :class="item.points > 0 ? 'income' : 'expense'">
+              {{ item.points > 0 ? '+' : '-' }}
+            </view>
+            <view class="record-info">
+              <view class="record-reason">{{ item.reason }}</view>
+              <view class="record-time">{{ formatTime(item.createTime) }}</view>
+            </view>
+          </view>
+          <view class="record-points" :class="item.points > 0 ? 'income' : 'expense'">
+            {{ item.points > 0 ? '+' : '' }}{{ item.points }}
+          </view>
+        </view>
+      </view>
+      
+      <!-- 加载更多 -->
+      <view class="load-more" v-if="hasMore" @click="loadMore">
+        {{ loading ? '加载中...' : '加载更多' }}
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import api from '@/utils/api.js'
+
+export default {
+  name: 'points-detail',
+  data() {
+    return {
+      balance: 0,
+      records: [],
+      makerId: null,
+      pageNum: 1,
+      pageSize: 20,
+      total: 0,
+      loading: false
+    }
+  },
+  computed: {
+    hasMore() {
+      return this.records.length < this.total
+    }
+  },
+  onLoad() {
+    this.initData()
+  },
+  methods: {
+    async initData() {
+      const userInfo = uni.getStorageSync('userInfo')
+      if (userInfo && userInfo.matchmakerId) {
+        this.makerId = userInfo.matchmakerId
+      } else if (userInfo && userInfo.userId) {
+        this.makerId = userInfo.userId
+      }
+      
+      await this.loadRecords()
+    },
+    
+    async loadRecords() {
+      if (!this.makerId || this.loading) return
+      
+      this.loading = true
+      try {
+        const res = await api.pointsMall.getRecords(this.makerId, this.pageNum, this.pageSize)
+        this.balance = res.balance || 0
+        this.total = res.total || 0
+        
+        if (this.pageNum === 1) {
+          this.records = res.list || []
+        } else {
+          this.records = [...this.records, ...(res.list || [])]
+        }
+      } catch (e) {
+        console.error('获取积分明细失败:', e)
+        uni.showToast({ title: '获取积分明细失败', icon: 'none' })
+      } finally {
+        this.loading = false
+      }
+    },
+    
+    loadMore() {
+      if (this.hasMore && !this.loading) {
+        this.pageNum++
+        this.loadRecords()
+      }
+    },
+    
+    handleBack() {
+      uni.navigateBack()
+    },
+    
+    formatTime(timeStr) {
+      if (!timeStr) return ''
+      const date = new Date(timeStr)
+      const year = date.getFullYear()
+      const month = String(date.getMonth() + 1).padStart(2, '0')
+      const day = String(date.getDate()).padStart(2, '0')
+      const hour = String(date.getHours()).padStart(2, '0')
+      const minute = String(date.getMinutes()).padStart(2, '0')
+      return `${year}-${month}-${day} ${hour}:${minute}`
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.points-detail {
+  min-height: 100vh;
+  background: #F5F5F5;
+}
+
+.header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 25rpx 30rpx;
+  padding-top: calc(25rpx + env(safe-area-inset-top));
+  background: #FFFFFF;
+  
+  .back-icon {
+    width: 44rpx;
+    height: 44rpx;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 32rpx;
+    color: #333;
+    &::before { content: '‹'; }
+  }
+  
+  .header-title {
+    font-size: 36rpx;
+    font-weight: bold;
+    color: #333;
+  }
+  
+  .header-right {
+    width: 44rpx;
+  }
+}
+
+.balance-card {
+  background: linear-gradient(135deg, #9C27B0 0%, #E91E63 100%);
+  margin: 30rpx;
+  padding: 40rpx;
+  border-radius: 20rpx;
+  text-align: center;
+  
+  .balance-label {
+    font-size: 28rpx;
+    color: rgba(255, 255, 255, 0.8);
+    margin-bottom: 15rpx;
+  }
+  
+  .balance-value {
+    font-size: 72rpx;
+    font-weight: bold;
+    color: #FFFFFF;
+  }
+}
+
+.records-section {
+  background: #FFFFFF;
+  margin: 0 30rpx;
+  border-radius: 20rpx;
+  padding: 30rpx;
+  
+  .section-title {
+    font-size: 30rpx;
+    font-weight: bold;
+    color: #333;
+    margin-bottom: 30rpx;
+  }
+  
+  .empty-tip {
+    text-align: center;
+    color: #999;
+    font-size: 28rpx;
+    padding: 60rpx 0;
+  }
+}
+
+.record-list {
+  .record-item {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 25rpx 0;
+    border-bottom: 1rpx solid #F0F0F0;
+    
+    &:last-child {
+      border-bottom: none;
+    }
+    
+    .record-left {
+      display: flex;
+      align-items: center;
+      
+      .record-icon {
+        width: 60rpx;
+        height: 60rpx;
+        border-radius: 50%;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        font-size: 32rpx;
+        font-weight: bold;
+        margin-right: 20rpx;
+        
+        &.income {
+          background: #E8F5E9;
+          color: #4CAF50;
+        }
+        
+        &.expense {
+          background: #FFEBEE;
+          color: #F44336;
+        }
+      }
+      
+      .record-info {
+        .record-reason {
+          font-size: 28rpx;
+          color: #333;
+          margin-bottom: 8rpx;
+        }
+        
+        .record-time {
+          font-size: 24rpx;
+          color: #999;
+        }
+      }
+    }
+    
+    .record-points {
+      font-size: 32rpx;
+      font-weight: bold;
+      
+      &.income {
+        color: #4CAF50;
+      }
+      
+      &.expense {
+        color: #F44336;
+      }
+    }
+  }
+}
+
+.load-more {
+  text-align: center;
+  padding: 30rpx;
+  color: #9C27B0;
+  font-size: 26rpx;
+}
+</style>

+ 159 - 55
LiangZhiYUMao/pages/matchmaker-workbench/points-mall.vue

@@ -38,27 +38,29 @@
       </view>
     </view>
 
-    <!-- 推荐商品区域 -->
+    <!-- 商品区域 -->
     <scroll-view scroll-y class="products-container">
-      <!-- 推荐商品标题 -->
+      <!-- 商品标题 -->
       <view class="section-header">
-        <text class="section-title">推荐商品</text>
-        <view class="view-more" @click="handleViewMore">
-          <text class="view-more-text">查看更多</text>
-          <text class="arrow">›</text>
-        </view>
+        <text class="section-title">{{ activeCategory === 0 ? '全部商品' : categories[activeCategory] }}</text>
+        <text class="product-count">共 {{ displayProducts.length }} 件</text>
+      </view>
+
+      <!-- 空状态 -->
+      <view v-if="displayProducts.length === 0 && !loading" class="empty-state">
+        <text>暂无商品</text>
       </view>
 
       <!-- 商品网格 -->
       <view class="products-grid">
         <view 
-          v-for="(product, index) in recommendedProducts" 
-          :key="index"
+          v-for="(product, index) in displayProducts" 
+          :key="product.id"
           class="product-card"
           @click="handleProductClick(product)"
         >
           <view class="product-image">
-            <image :src="product.image" mode="aspectFill"></image>
+            <image :src="product.image || defaultImage" mode="aspectFit" @error="handleImageError($event, index)"></image>
           </view>
           <view class="product-info">
             <text class="product-name">{{ product.name }}</text>
@@ -77,75 +79,145 @@
 </template>
 
 <script>
+import api from '@/utils/api.js'
+
 export default {
   name: 'points-mall',
   data() {
     return {
-      userPoints: 5000,
+      userPoints: 0,
       cartCount: 0,
       activeCategory: 0,
-      categories: ['全部',  '实物商品', '虚拟商品'],
-      recommendedProducts: [
-        {
-          id: 1,
-          name: '清风原色无尘纸',
-          points: 1000,
-          image: 'https://via.placeholder.com/200x200?text=纸巾'
-        },
-        {
-          id: 2,
-          name: 'OPPO Enco Air2耳机',
-          points: 7000,
-          image: 'https://via.placeholder.com/200x200?text=耳机'
-        },
-        {
-          id: 3,
-          name: '五常大米',
-          points: 3000,
-          image: 'https://via.placeholder.com/200x200?text=大米'
-        },
-        {
-          id: 4,
-          name: '脸盆',
-          points: 30,
-          image: 'https://via.placeholder.com/200x200?text=脸盆'
-        }
-      ]
+      categories: ['全部', '实物商品', '虚拟商品'],
+      recommendedProducts: [],
+      allProducts: [],
+      makerId: null,
+      loading: false,
+      defaultImage: 'https://img.zcool.cn/community/01b8d95d3f8a8fa801211d53e8c6b0.jpg@1280w_1l_2o_100sh.jpg'
+    }
+  },
+  computed: {
+    // 根据分类筛选商品
+    displayProducts() {
+      if (this.activeCategory === 0) {
+        return this.allProducts
+      }
+      return this.allProducts.filter(p => p.category === this.activeCategory)
+    }
+  },
+  onLoad() {
+    this.initData()
+  },
+  onShow() {
+    // 每次显示页面时刷新积分余额
+    if (this.makerId) {
+      this.loadBalance()
     }
   },
   methods: {
+    // 初始化数据
+    async initData() {
+      // 获取红娘ID
+      const userInfo = uni.getStorageSync('userInfo')
+      if (userInfo && userInfo.matchmakerId) {
+        this.makerId = userInfo.matchmakerId
+      } else if (userInfo && userInfo.userId) {
+        this.makerId = userInfo.userId
+      }
+      
+      await Promise.all([
+        this.loadBalance(),
+        this.loadProducts(),
+        this.loadRecommendProducts()
+      ])
+    },
+    
+    // 加载积分余额
+    async loadBalance() {
+      if (!this.makerId) return
+      try {
+        const res = await api.pointsMall.getBalance(this.makerId)
+        this.userPoints = res.balance || 0
+      } catch (e) {
+        console.error('获取积分余额失败:', e)
+      }
+    },
+    
+    // 加载商品列表
+    async loadProducts() {
+      this.loading = true
+      try {
+        const res = await api.pointsMall.getProducts({ pageNum: 1, pageSize: 50 })
+        this.allProducts = (res.list || []).map(item => ({
+          id: item.id,
+          name: item.name,
+          points: item.pointsPrice,
+          image: item.imageUrl || this.defaultImage,
+          category: item.category,
+          stock: item.stock,
+          description: item.description
+        }))
+      } catch (e) {
+        console.error('获取商品列表失败:', e)
+      } finally {
+        this.loading = false
+      }
+    },
+    
+    // 加载推荐商品
+    async loadRecommendProducts() {
+      try {
+        const res = await api.pointsMall.getRecommendProducts(10)
+        this.recommendedProducts = (res || []).map(item => ({
+          id: item.id,
+          name: item.name,
+          points: item.pointsPrice,
+          image: item.imageUrl || this.defaultImage,
+          category: item.category,
+          stock: item.stock
+        }))
+      } catch (e) {
+        console.error('获取推荐商品失败:', e)
+      }
+    },
+    
     // 返回上一页
     handleBack() {
       uni.navigateBack()
     },
+    
     // 查看积分明细
     handlePointsDetail() {
-      uni.showToast({
-        title: '查看积分明细',
-        icon: 'none'
+      uni.navigateTo({
+        url: '/pages/matchmaker-workbench/points-detail'
       })
     },
+    
     // 赚取积分
     handleEarnPoints() {
-      uni.showToast({
-        title: '赚取积分',
-        icon: 'none'
+      uni.navigateTo({
+        url: '/pages/matchmaker-workbench/earn-points'
       })
     },
+    
     // 查看更多
     handleViewMore() {
-      uni.showToast({
-        title: '查看更多商品',
-        icon: 'none'
-      })
+      // 切换到全部分类
+      this.activeCategory = 0
     },
-    // 商品点击
+    
+    // 商品点击 - 跳转到商品详情
     handleProductClick(product) {
-      uni.showToast({
-        title: `已添加 ${product.name} 到购物车`,
-        icon: 'success'
+      uni.navigateTo({
+        url: `/pages/matchmaker-workbench/product-detail?id=${product.id}`
       })
-      this.cartCount++
+    },
+    
+    // 图片加载失败处理
+    handleImageError(e, index) {
+      if (this.recommendedProducts[index]) {
+        this.$set(this.recommendedProducts[index], 'image', this.defaultImage)
+      }
     }
   }
 }
@@ -336,6 +408,11 @@ export default {
     color: #333;
   }
 
+  .product-count {
+    font-size: 24rpx;
+    color: #999;
+  }
+
   .view-more {
     display: flex;
     align-items: center;
@@ -353,6 +430,31 @@ export default {
   }
 }
 
+/* 空状态 */
+.empty-state {
+  text-align: center;
+  padding: 100rpx 0;
+  color: #999;
+  font-size: 28rpx;
+}
+
+/* 废弃的view-more样式 */
+.view-more-deprecated {
+  display: flex;
+  align-items: center;
+  gap: 5rpx;
+
+  .view-more-text {
+    font-size: 24rpx;
+    color: #9C27B0;
+  }
+
+  .arrow {
+    font-size: 28rpx;
+    color: #9C27B0;
+  }
+}
+
 /* 商品网格 */
 .products-grid {
   display: grid;
@@ -374,9 +476,11 @@ export default {
 
     .product-image {
       width: 100%;
-      height: 200rpx;
+      height: 300rpx;
       background: #F5F5F5;
-      overflow: hidden;
+      display: flex;
+      align-items: center;
+      justify-content: center;
 
       image {
         width: 100%;

+ 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;
 		}
 	}
 

+ 392 - 0
LiangZhiYUMao/pages/matchmaker-workbench/product-detail.vue

@@ -0,0 +1,392 @@
+<template>
+  <view class="product-detail">
+    <!-- 顶部导航栏 -->
+    <view class="header">
+      <view class="back-icon" @click="handleBack"></view>
+      <text class="header-title">商品详情</text>
+      <view class="header-right"></view>
+    </view>
+
+    <!-- 商品图片 -->
+    <view class="product-image-section">
+      <image :src="product.imageUrl || '/static/default-product.png'" mode="aspectFit" class="product-image"></image>
+    </view>
+
+    <!-- 商品信息 -->
+    <view class="product-info-section">
+      <view class="product-name">{{ product.name }}</view>
+      <view class="product-price">
+        <text class="price-value">{{ product.pointsPrice }}</text>
+        <text class="price-unit">积分</text>
+      </view>
+      <view class="product-stock">库存: {{ product.stock || 0 }} 件</view>
+    </view>
+
+    <!-- 商品描述 -->
+    <view class="product-desc-section">
+      <view class="section-title">商品描述</view>
+      <view class="desc-content">{{ product.description || '暂无描述' }}</view>
+    </view>
+
+    <!-- 收货信息 -->
+    <view class="receiver-section" v-if="product.category === 1">
+      <view class="section-title">收货信息</view>
+      <view class="form-item">
+        <text class="label">收货人</text>
+        <input class="input" v-model="receiverName" placeholder="请输入收货人姓名" />
+      </view>
+      <view class="form-item">
+        <text class="label">联系电话</text>
+        <input class="input" v-model="receiverPhone" placeholder="请输入联系电话" type="number" />
+      </view>
+      <view class="form-item">
+        <text class="label">收货地址</text>
+        <textarea class="textarea" v-model="receiverAddress" placeholder="请输入详细收货地址"></textarea>
+      </view>
+    </view>
+
+    <!-- 底部操作栏 -->
+    <view class="bottom-bar">
+      <view class="points-info">
+        <text class="label">我的积分:</text>
+        <text class="value">{{ userPoints }}</text>
+      </view>
+      <view class="exchange-btn" :class="{ disabled: !canExchange }" @click="handleExchange">
+        立即兑换
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import api from '@/utils/api.js'
+
+export default {
+  name: 'product-detail',
+  data() {
+    return {
+      productId: null,
+      product: {},
+      userPoints: 0,
+      makerId: null,
+      receiverName: '',
+      receiverPhone: '',
+      receiverAddress: '',
+      loading: false
+    }
+  },
+  computed: {
+    canExchange() {
+      if (this.loading) return false
+      if (!this.product.pointsPrice) return false
+      if (this.userPoints < this.product.pointsPrice) return false
+      if (this.product.stock <= 0) return false
+      // 实物商品需要填写收货信息
+      if (this.product.category === 1) {
+        if (!this.receiverName || !this.receiverPhone || !this.receiverAddress) {
+          return false
+        }
+      }
+      return true
+    }
+  },
+  onLoad(options) {
+    if (options.id) {
+      this.productId = options.id
+    }
+    this.initData()
+  },
+  methods: {
+    async initData() {
+      // 获取红娘ID
+      const userInfo = uni.getStorageSync('userInfo')
+      if (userInfo && userInfo.matchmakerId) {
+        this.makerId = userInfo.matchmakerId
+      } else if (userInfo && userInfo.userId) {
+        this.makerId = userInfo.userId
+      }
+      
+      await Promise.all([
+        this.loadProduct(),
+        this.loadBalance()
+      ])
+    },
+    
+    async loadProduct() {
+      if (!this.productId) return
+      try {
+        const res = await api.pointsMall.getProductDetail(this.productId)
+        this.product = res || {}
+      } catch (e) {
+        console.error('获取商品详情失败:', e)
+        uni.showToast({ title: '获取商品详情失败', icon: 'none' })
+      }
+    },
+    
+    async loadBalance() {
+      if (!this.makerId) return
+      try {
+        const res = await api.pointsMall.getBalance(this.makerId)
+        this.userPoints = res.balance || 0
+      } catch (e) {
+        console.error('获取积分余额失败:', e)
+      }
+    },
+    
+    handleBack() {
+      uni.navigateBack()
+    },
+    
+    async handleExchange() {
+      if (!this.canExchange) {
+        if (this.userPoints < this.product.pointsPrice) {
+          uni.showToast({ title: '积分不足', icon: 'none' })
+        } else if (this.product.stock <= 0) {
+          uni.showToast({ title: '库存不足', icon: 'none' })
+        } else if (this.product.category === 1 && (!this.receiverName || !this.receiverPhone || !this.receiverAddress)) {
+          uni.showToast({ title: '请填写完整收货信息', icon: 'none' })
+        }
+        return
+      }
+      
+      uni.showModal({
+        title: '确认兑换',
+        content: `确定使用 ${this.product.pointsPrice} 积分兑换「${this.product.name}」吗?`,
+        success: async (res) => {
+          if (res.confirm) {
+            await this.doExchange()
+          }
+        }
+      })
+    },
+    
+    async doExchange() {
+      this.loading = true
+      try {
+        const data = {
+          makerId: this.makerId,
+          productId: this.productId,
+          quantity: 1,
+          receiverName: this.receiverName,
+          receiverPhone: this.receiverPhone,
+          receiverAddress: this.receiverAddress
+        }
+        
+        await api.pointsMall.exchange(data)
+        
+        uni.showToast({ title: '兑换成功', icon: 'success' })
+        
+        // 刷新积分余额
+        await this.loadBalance()
+        
+        // 延迟返回
+        setTimeout(() => {
+          uni.navigateBack()
+        }, 1500)
+      } catch (e) {
+        console.error('兑换失败:', e)
+        uni.showToast({ title: e.message || '兑换失败', icon: 'none' })
+      } finally {
+        this.loading = false
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.product-detail {
+  min-height: 100vh;
+  background: #F5F5F5;
+  padding-bottom: 120rpx;
+}
+
+.header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 25rpx 30rpx;
+  padding-top: calc(25rpx + env(safe-area-inset-top));
+  background: #FFFFFF;
+  
+  .back-icon {
+    width: 44rpx;
+    height: 44rpx;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 32rpx;
+    color: #333;
+    &::before { content: '‹'; }
+  }
+  
+  .header-title {
+    font-size: 36rpx;
+    font-weight: bold;
+    color: #333;
+  }
+  
+  .header-right {
+    width: 44rpx;
+  }
+}
+
+.product-image-section {
+  width: 100%;
+  height: 600rpx;
+  background: #FFFFFF;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  
+  .product-image {
+    width: 100%;
+    height: 100%;
+    object-fit: contain;
+  }
+}
+
+.product-info-section {
+  background: #FFFFFF;
+  padding: 30rpx;
+  margin-top: 20rpx;
+  
+  .product-name {
+    font-size: 36rpx;
+    font-weight: bold;
+    color: #333;
+    margin-bottom: 20rpx;
+  }
+  
+  .product-price {
+    display: flex;
+    align-items: baseline;
+    margin-bottom: 15rpx;
+    
+    .price-value {
+      font-size: 48rpx;
+      font-weight: bold;
+      color: #9C27B0;
+    }
+    
+    .price-unit {
+      font-size: 28rpx;
+      color: #9C27B0;
+      margin-left: 10rpx;
+    }
+  }
+  
+  .product-stock {
+    font-size: 26rpx;
+    color: #999;
+  }
+}
+
+.product-desc-section {
+  background: #FFFFFF;
+  padding: 30rpx;
+  margin-top: 20rpx;
+  
+  .section-title {
+    font-size: 30rpx;
+    font-weight: bold;
+    color: #333;
+    margin-bottom: 20rpx;
+  }
+  
+  .desc-content {
+    font-size: 28rpx;
+    color: #666;
+    line-height: 1.6;
+  }
+}
+
+.receiver-section {
+  background: #FFFFFF;
+  padding: 30rpx;
+  margin-top: 20rpx;
+  
+  .section-title {
+    font-size: 30rpx;
+    font-weight: bold;
+    color: #333;
+    margin-bottom: 20rpx;
+  }
+  
+  .form-item {
+    display: flex;
+    align-items: flex-start;
+    margin-bottom: 20rpx;
+    
+    .label {
+      width: 150rpx;
+      font-size: 28rpx;
+      color: #333;
+      padding-top: 10rpx;
+    }
+    
+    .input {
+      flex: 1;
+      height: 70rpx;
+      background: #F5F5F5;
+      border-radius: 10rpx;
+      padding: 0 20rpx;
+      font-size: 28rpx;
+    }
+    
+    .textarea {
+      flex: 1;
+      height: 150rpx;
+      background: #F5F5F5;
+      border-radius: 10rpx;
+      padding: 20rpx;
+      font-size: 28rpx;
+    }
+  }
+}
+
+.bottom-bar {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  height: 100rpx;
+  background: #FFFFFF;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 0 30rpx;
+  padding-bottom: env(safe-area-inset-bottom);
+  box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
+  
+  .points-info {
+    display: flex;
+    align-items: center;
+    
+    .label {
+      font-size: 26rpx;
+      color: #666;
+    }
+    
+    .value {
+      font-size: 32rpx;
+      font-weight: bold;
+      color: #9C27B0;
+      margin-left: 10rpx;
+    }
+  }
+  
+  .exchange-btn {
+    padding: 20rpx 60rpx;
+    background: linear-gradient(135deg, #9C27B0 0%, #E91E63 100%);
+    color: #FFFFFF;
+    font-size: 30rpx;
+    font-weight: bold;
+    border-radius: 50rpx;
+    
+    &.disabled {
+      background: #CCCCCC;
+    }
+  }
+}
+</style>

+ 307 - 47
LiangZhiYUMao/pages/matchmaker-workbench/resource-input.vue

@@ -21,11 +21,11 @@
 						<input type="text" class="form-input" placeholder="请输入姓名" v-model="formData.name" />
 					</view>
 					<view class="form-item">
-						<text class="form-label">年龄</text>
+						<text class="form-label">年龄<text class="required">*</text></text>
 						<input type="number" class="form-input" placeholder="请输入年龄" v-model="formData.age" />
 					</view>
 					<view class="form-item">
-						<text class="form-label">性别</text>
+						<text class="form-label">性别<text class="required">*</text></text>
 						<picker mode="selector" :range="genderOptions" :value="genderIndex" @change="onGenderChange">
 							<view class="form-input picker-input">
 								<text v-if="formData.gender">{{ genderOptions[genderIndex] }}</text>
@@ -50,11 +50,11 @@
 				<view class="form-section" :class="{ 'active': activeSection === 'physical' }">
 					<view class="section-title">身体状况</view>
 					<view class="form-item">
-						<text class="form-label">身高 (cm)</text>
+						<text class="form-label">身高 (cm)<text class="required">*</text></text>
 						<input type="number" class="form-input" placeholder="请输入身高" v-model="formData.height" />
 					</view>
 					<view class="form-item">
-						<text class="form-label">体重 (kg)</text>
+						<text class="form-label">体重 (kg)<text class="required">*</text></text>
 						<input type="number" class="form-input" placeholder="请输入体重" v-model="formData.weight" />
 					</view>
 				</view>
@@ -63,7 +63,7 @@
 				<view class="form-section" :class="{ 'active': activeSection === 'social' }">
 					<view class="section-title">社会属性</view>
 					<view class="form-item">
-						<text class="form-label">婚况</text>
+						<text class="form-label">婚况<text class="required">*</text></text>
 						<picker mode="selector" :range="maritalStatusOptions" :value="maritalStatusIndex >= 0 ? maritalStatusIndex : 0" @change="onMaritalStatusChange">
 							<view class="form-input picker-input">
 								<text v-if="maritalStatusIndex >= 0 && maritalStatusIndex < maritalStatusOptions.length">{{ maritalStatusOptions[maritalStatusIndex] }}</text>
@@ -87,7 +87,7 @@
 						<input type="text" class="form-input" placeholder="请输入年收入" v-model="formData.income" />
 					</view>
 					<view class="form-item">
-						<text class="form-label">住址</text>
+						<text class="form-label">住址<text class="required">*</text></text>
 						<input type="text" class="form-input" placeholder="请输入住址" v-model="formData.address" />
 					</view>
 					<view class="form-item">
@@ -120,6 +120,37 @@
 					</view>
 				</view>
 
+				<!-- 择偶标准 -->
+				<view class="form-section">
+					<view class="section-title">择偶标准</view>
+					<view class="form-item">
+						<text class="form-label">择偶要求</text>
+						<input type="text" class="form-input" placeholder="请输入择偶要求,如:176+本科" v-model="formData.mateSelectionCriteria" />
+					</view>
+				</view>
+
+				<!-- 客户标签 -->
+				<view class="form-section">
+					<view class="section-title">客户标签</view>
+					<view class="form-item">
+						<text class="form-label">选择标签</text>
+						<view class="tags-container">
+							<view 
+								class="tag-item" 
+								:class="{ 'selected': selectedTagIds.includes(tag.id) }"
+								v-for="tag in tagList" 
+								:key="tag.id"
+								@click="toggleTag(tag.id)"
+							>
+								<text class="tag-text">{{ tag.name }}</text>
+							</view>
+							<view v-if="tagList.length === 0" class="tag-loading">
+								<text>加载中...</text>
+							</view>
+						</view>
+					</view>
+				</view>
+
 				<!-- 联系方式 -->
 				<view class="form-section">
 					<view class="section-title">联系方式</view>
@@ -165,6 +196,7 @@ export default {
 				occupation: '',
 				house: null,
 				car: null,
+				mateSelectionCriteria: '',
 				phone: '',
 				backupPhone: ''
 			},
@@ -179,7 +211,9 @@ export default {
 			houseOptions: ['无', '有'],
 			houseIndex: 0,
 			carOptions: ['无', '有'],
-			carIndex: 0
+			carIndex: 0,
+			tagList: [], // 标签列表
+			selectedTagIds: [] // 选中的标签ID列表
 		}
 	},
 	onLoad() {
@@ -187,13 +221,64 @@ export default {
 		const userInfo = uni.getStorageSync('userInfo') || {}
 		const userId = uni.getStorageSync('userId')
 		// 从userInfo中获取用户ID,将作为红娘ID
-		this.currentUserId = userInfo.userId || userId || null
+		let rawUserId = userInfo.userId || userId || null
+		// 验证并转换currentUserId为有效的整数
+		if (rawUserId !== null && rawUserId !== undefined) {
+			rawUserId = parseInt(rawUserId)
+			if (isNaN(rawUserId) || rawUserId <= 0) {
+				rawUserId = null
+			}
+		}
+		this.currentUserId = rawUserId
 		console.log('当前登录用户ID:', this.currentUserId)
+		
+		// 加载标签列表
+		this.loadTags()
 	},
 	methods: {
 		goBack() {
 			uni.navigateBack()
 		},
+		// 加载标签列表
+		async loadTags() {
+			try {
+				const baseUrl = process.env.NODE_ENV === 'development' 
+					? 'http://localhost:8083/api'  // 开发环境 - 通过网关
+					: 'https://your-domain.com/api'  // 生产环境
+				
+				const [error, res] = await uni.request({
+					url: `${baseUrl}/tag/list`,
+					method: 'GET',
+					timeout: 10000
+				})
+				
+				if (error) {
+					console.error('加载标签失败:', error)
+					return
+				}
+				
+				if (res.statusCode === 200 && res.data && res.data.code === 200) {
+					this.tagList = res.data.data || []
+					console.log('标签列表加载成功:', this.tagList)
+				} else {
+					console.error('加载标签失败:', res.data.message)
+				}
+			} catch (e) {
+				console.error('加载标签异常:', e)
+			}
+		},
+		// 切换标签选择状态
+		toggleTag(tagId) {
+			const index = this.selectedTagIds.indexOf(tagId)
+			if (index > -1) {
+				// 已选中,取消选择
+				this.selectedTagIds.splice(index, 1)
+			} else {
+				// 未选中,添加到选中列表
+				this.selectedTagIds.push(tagId)
+			}
+			console.log('选中的标签ID:', this.selectedTagIds)
+		},
 		onGenderChange(e) {
 			this.genderIndex = e.detail.value
 			this.formData.gender = this.genderIndex + 1 // 1-男,2-女
@@ -205,6 +290,7 @@ export default {
 		onMaritalStatusChange(e) {
 			this.maritalStatusIndex = parseInt(e.detail.value)
 			// 0-未婚,1-离异,2-丧偶
+			// 直接使用索引值作为marrStatus的值
 			this.formData.marrStatus = this.maritalStatusIndex
 			console.log('婚姻状态选择:', this.maritalStatusIndex, '对应值:', this.formData.marrStatus, '选项:', this.maritalStatusOptions[this.maritalStatusIndex])
 		},
@@ -230,6 +316,61 @@ export default {
 				return
 			}
 			
+			// 验证年龄
+			if (this.formData.age === null || this.formData.age === undefined || this.formData.age === '') {
+				uni.showToast({
+					title: '请输入年龄',
+					icon: 'none'
+				})
+				return
+			}
+			
+			// 验证性别
+			if (this.formData.gender === null || this.formData.gender === undefined) {
+				uni.showToast({
+					title: '请选择性别',
+					icon: 'none'
+				})
+				return
+			}
+			
+			// 验证身高
+			if (this.formData.height === null || this.formData.height === undefined || this.formData.height === '') {
+				uni.showToast({
+					title: '请输入身高',
+					icon: 'none'
+				})
+				return
+			}
+			
+			// 验证体重
+			if (this.formData.weight === null || this.formData.weight === undefined || this.formData.weight === '') {
+				uni.showToast({
+					title: '请输入体重',
+					icon: 'none'
+				})
+				return
+			}
+			
+			// 验证婚况
+			if ((this.maritalStatusIndex < 0 || this.maritalStatusIndex === null || this.maritalStatusIndex === undefined) 
+				&& (this.formData.marrStatus === null || this.formData.marrStatus === undefined)) {
+				uni.showToast({
+					title: '请选择婚况',
+					icon: 'none'
+				})
+				return
+			}
+			
+			// 验证住址
+			if (!this.formData.address || !this.formData.address.trim()) {
+				uni.showToast({
+					title: '请输入住址',
+					icon: 'none'
+				})
+				return
+			}
+			
 			if (!this.formData.phone || !this.formData.phone.trim()) {
 				uni.showToast({
 					title: '请输入手机号',
@@ -254,7 +395,8 @@ export default {
 
 			try {
 				// 确保婚姻状态已选择
-				if (this.maritalStatusIndex < 0 && (this.formData.marrStatus === null || this.formData.marrStatus === undefined)) {
+				if ((this.maritalStatusIndex < 0 || this.maritalStatusIndex === null || this.maritalStatusIndex === undefined) 
+					&& (this.formData.marrStatus === null || this.formData.marrStatus === undefined)) {
 					uni.showToast({
 						title: '请选择婚况',
 						icon: 'none'
@@ -262,29 +404,105 @@ export default {
 					return
 				}
 				
-				// 转换数据类型
-				const submitData = {
-					...this.formData,
-					age: this.formData.age ? parseInt(this.formData.age) : null,
-					height: this.formData.height ? parseInt(this.formData.height) : null,
-					weight: this.formData.weight ? parseInt(this.formData.weight) : null,
-					// 确保婚姻状态被正确设置(0-未婚,1-离异,2-丧偶)
-					marrStatus: this.formData.marrStatus !== null && this.formData.marrStatus !== undefined 
-						? parseInt(this.formData.marrStatus) 
-						: (this.maritalStatusIndex >= 0 ? parseInt(this.maritalStatusIndex) : null),
-					// 确保备用手机号被正确传递(即使是空字符串也要传递)
-					backupPhone: this.formData.backupPhone !== null && this.formData.backupPhone !== undefined 
-						? this.formData.backupPhone.trim() 
-						: null
+				// 调试:打印原始值
+				console.log('=== 提交前的原始数据 ===')
+				console.log('formData.marrStatus:', this.formData.marrStatus, '类型:', typeof this.formData.marrStatus)
+				console.log('maritalStatusIndex:', this.maritalStatusIndex, '类型:', typeof this.maritalStatusIndex)
+				console.log('formData.backupPhone:', this.formData.backupPhone, '类型:', typeof this.formData.backupPhone)
+				console.log('formData.mateSelectionCriteria:', this.formData.mateSelectionCriteria, '类型:', typeof this.formData.mateSelectionCriteria)
+				
+				// 确定婚姻状态的值:优先使用formData.marrStatus,如果没有则使用maritalStatusIndex
+				let marrStatusValue = null
+				if (this.formData.marrStatus !== null && this.formData.marrStatus !== undefined && this.formData.marrStatus !== 'undefined' && this.formData.marrStatus !== 'null' && this.formData.marrStatus !== '') {
+					const parsed = parseInt(this.formData.marrStatus)
+					marrStatusValue = isNaN(parsed) ? null : parsed
+					console.log('婚姻状态处理: formData.marrStatus ->', marrStatusValue)
+				} else if (this.maritalStatusIndex !== null && this.maritalStatusIndex !== undefined && this.maritalStatusIndex >= 0) {
+					const parsed = parseInt(this.maritalStatusIndex)
+					marrStatusValue = isNaN(parsed) ? null : parsed
+					console.log('婚姻状态处理: maritalStatusIndex ->', marrStatusValue)
+				} else {
+					console.log('婚姻状态处理: 未找到有效值')
 				}
 				
-				// 如果婚姻状态仍然为null,但maritalStatusIndex有值,使用maritalStatusIndex
-				if (submitData.marrStatus === null && this.maritalStatusIndex >= 0) {
-					submitData.marrStatus = parseInt(this.maritalStatusIndex)
+				// 处理备用手机号:保留有效值,空字符串转为null
+				let backupPhoneValue = null
+				const backupPhoneRaw = this.formData.backupPhone
+				if (backupPhoneRaw !== null && backupPhoneRaw !== undefined && backupPhoneRaw !== 'undefined' && backupPhoneRaw !== 'null') {
+					const trimmed = String(backupPhoneRaw).trim()
+					if (trimmed !== '' && trimmed !== 'undefined' && trimmed !== 'null') {
+						backupPhoneValue = trimmed
+						console.log('备用手机号处理: 保留值 ->', backupPhoneValue)
+					} else {
+						console.log('备用手机号处理: 空值,转为null')
+					}
+				} else {
+					console.log('备用手机号处理: 原始值为null/undefined')
 				}
 				
-				console.log('提交数据 - 婚姻状态:', submitData.marrStatus, 'maritalStatusIndex:', this.maritalStatusIndex, '选项:', this.maritalStatusOptions[submitData.marrStatus])
+				// 处理择偶标准:保留有效值,空字符串转为null
+				let mateSelectionCriteriaValue = null
+				const mateSelectionCriteriaRaw = this.formData.mateSelectionCriteria
+				if (mateSelectionCriteriaRaw !== null && mateSelectionCriteriaRaw !== undefined && mateSelectionCriteriaRaw !== 'undefined' && mateSelectionCriteriaRaw !== 'null') {
+					const trimmed = String(mateSelectionCriteriaRaw).trim()
+					if (trimmed !== '' && trimmed !== 'undefined' && trimmed !== 'null') {
+						mateSelectionCriteriaValue = trimmed
+						console.log('择偶标准处理: 保留值 ->', mateSelectionCriteriaValue)
+					} else {
+						console.log('择偶标准处理: 空值,转为null')
+					}
+				} else {
+					console.log('择偶标准处理: 原始值为null/undefined')
+				}
+				
+				// 辅助函数:安全地将值转换为整数,无效值返回null
+				const safeParseInt = (value) => {
+					if (value === null || value === undefined || value === '' || value === 'undefined' || value === 'null') {
+						return null
+					}
+					const parsed = parseInt(value)
+					return isNaN(parsed) ? null : parsed
+				}
+				
+				// 辅助函数:安全地获取字符串值,无效值返回null
+				const safeString = (value) => {
+					if (value === null || value === undefined || value === '' || value === 'undefined' || value === 'null') {
+						return null
+					}
+					const str = String(value).trim()
+					return str === '' ? null : str
+				}
+				
+				// 转换数据类型,明确构建提交数据对象
+				const submitData = {
+					name: safeString(this.formData.name),
+					age: safeParseInt(this.formData.age),
+					gender: safeParseInt(this.formData.gender),
+					constellation: safeString(this.formData.constellation),
+					height: safeParseInt(this.formData.height),
+					weight: safeParseInt(this.formData.weight),
+					// 确保婚姻状态被正确设置(0-未婚,1-离异,2-丧偶)
+					marrStatus: marrStatusValue, // 直接使用处理后的值,不需要再次转换
+					diploma: safeString(this.formData.diploma),
+					income: safeString(this.formData.income),
+					address: safeString(this.formData.address),
+					domicile: safeString(this.formData.domicile),
+					occupation: safeString(this.formData.occupation),
+					house: safeParseInt(this.formData.house),
+					car: safeParseInt(this.formData.car),
+					phone: safeString(this.formData.phone),
+					// 确保备用手机号被正确传递(保留有效值,null表示空)
+					backupPhone: backupPhoneValue,
+					// 确保择偶标准被正确传递(保留有效值,null表示空)
+					mateSelectionCriteria: mateSelectionCriteriaValue,
+					// 选中的标签ID列表
+					tagIds: this.selectedTagIds.length > 0 ? this.selectedTagIds : null
+				}
+				
+				console.log('提交数据 - 婚姻状态:', submitData.marrStatus, 'maritalStatusIndex:', this.maritalStatusIndex, 'formData.marrStatus:', this.formData.marrStatus, '选项:', submitData.marrStatus !== null ? this.maritalStatusOptions[submitData.marrStatus] : '未选择')
 				console.log('提交数据 - 备用手机号:', submitData.backupPhone, '类型:', typeof submitData.backupPhone)
+				console.log('提交数据 - 择偶标准:', submitData.mateSelectionCriteria, '类型:', typeof submitData.mateSelectionCriteria)
+				console.log('提交数据 - 标签ID列表:', submitData.tagIds)
 				console.log('完整提交数据:', JSON.stringify(submitData, null, 2))
 
 				// 调用后端接口(通过网关)
@@ -294,8 +512,12 @@ export default {
 				
 				// 构建请求URL,包含当前登录用户的ID作为参数(将作为红娘ID)
 				let url = `${baseUrl}/my-resource/add`
-				if (this.currentUserId) {
-					url += `?currentUserId=${this.currentUserId}`
+				// 确保currentUserId是有效的整数才添加到URL中
+				if (this.currentUserId !== null && this.currentUserId !== undefined) {
+					const userId = parseInt(this.currentUserId)
+					if (!isNaN(userId) && userId > 0) {
+						url += `?currentUserId=${userId}`
+					}
 				}
 				
 				const res = await uni.request({
@@ -315,6 +537,9 @@ export default {
 						icon: 'success'
 					})
 					
+					// 发送刷新事件,通知我的资源页面刷新列表
+					uni.$emit('refreshResourceList')
+					
 					// 延迟返回上一页
 					setTimeout(() => {
 						uni.navigateBack()
@@ -414,24 +639,59 @@ export default {
 	}
 }
 
-.form-item {
-	margin-bottom: 30rpx;
+	.form-item {
+		margin-bottom: 30rpx;
 
-	&:last-child {
-		margin-bottom: 0;
-	}
+		&:last-child {
+			margin-bottom: 0;
+		}
 
-	.form-label {
-		display: block;
-		font-size: 28rpx;
-		color: #666;
-		margin-bottom: 15rpx;
+		.form-label {
+			display: block;
+			font-size: 28rpx;
+			color: #666;
+			margin-bottom: 15rpx;
 
-		.required {
-			color: #FF4444;
-			margin-left: 5rpx;
+			.required {
+				color: #FF4444;
+				margin-left: 5rpx;
+			}
+		}
+		
+		.tags-container {
+			display: flex;
+			flex-wrap: wrap;
+			gap: 15rpx;
+			margin-top: 10rpx;
+			
+			.tag-item {
+				padding: 12rpx 24rpx;
+				background: #F8F8F8;
+				border: 2rpx solid #E0E0E0;
+				border-radius: 24rpx;
+				font-size: 26rpx;
+				color: #666;
+				transition: all 0.3s;
+				
+				&.selected {
+					background: linear-gradient(135deg, #F3E5F5 0%, #E1BEE7 100%);
+					border-color: #9C27B0;
+					color: #7B1FA2;
+					font-weight: 600;
+					box-shadow: 0 2rpx 8rpx rgba(156, 39, 176, 0.2);
+				}
+				
+				.tag-text {
+					font-size: 26rpx;
+				}
+			}
+			
+			.tag-loading {
+				padding: 20rpx;
+				color: #999;
+				font-size: 26rpx;
+			}
 		}
-	}
 
 	.form-input {
 		width: 100%;
@@ -459,12 +719,12 @@ export default {
 			color: #999;
 		}
 
-		.picker-arrow {
-			font-size: 24rpx;
-			color: #999;
-		}
+	.picker-arrow {
+		font-size: 24rpx;
+		color: #999;
 	}
 }
+}
 
 .submit-bar {
 	position: fixed;

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

@@ -0,0 +1,290 @@
+<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: linear-gradient(180deg, #fff1f7 0%, #f6ebff 45%, #fbf7ff 75%, #ffffff 100%);
+  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: transparent;
+  border-bottom-width: 0;
+
+  .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 120rpx;
+}
+
+.system-item {
+  display: flex;
+  padding: 26rpx 24rpx;
+  margin-bottom: 20rpx;
+  border-radius: 24rpx;
+  background: #FFFFFF;
+  box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.04);
+}
+
+.item-left {
+  margin-right: 20rpx;
+
+  .icon-wrapper {
+    width: 64rpx;
+    height: 64rpx;
+    border-radius: 50%;
+    background: linear-gradient(135deg, #b39ddb 0%, #ce93d8 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: 500;
+      color: #333;
+    }
+
+    .item-time {
+      font-size: 24rpx;
+      color: #b0a6c5;
+      margin-left: 20rpx;
+      flex-shrink: 0;
+    }
+  }
+
+  .item-content {
+    font-size: 26rpx;
+    color: #777;
+    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>

+ 26 - 3
LiangZhiYUMao/pages/matchmakers/detail.vue

@@ -306,9 +306,32 @@ export default {
 		 * 在线咨询
 		 */
 		consultMatchmaker() {
-			uni.showToast({
-				title: '功能开发中',
-				icon: 'none'
+			if (!this.matchmaker) {
+				uni.showToast({
+					title: '红娘信息未加载',
+					icon: 'none'
+				})
+				return
+			}
+			
+			// 目标用户信息(红娘)
+			// 给红娘 ID 加上 m_ 前缀,避免与普通用户 ID 冲突
+			const matchmakerId = this.matchmaker.id || this.matchmakerId
+			const targetUserId = 'm_' + matchmakerId
+			const targetUserName = this.matchmaker.real_name || '红娘'
+			const targetUserAvatar = this.getAvatarUrl(this.matchmaker.avatar_url)
+			
+			if (!targetUserId) {
+				uni.showToast({
+					title: '缺少红娘ID',
+					icon: 'none'
+				})
+				return
+			}
+			
+			// 跳转到聊天页面,携带对方用户信息和红娘标记
+			uni.navigateTo({
+				url: `/pages/message/chat?targetUserId=${encodeURIComponent(targetUserId)}&targetUserName=${encodeURIComponent(targetUserName)}&targetUserAvatar=${encodeURIComponent(targetUserAvatar)}&fromMatchmaker=1`
 			})
 		},
 

+ 182 - 184
LiangZhiYUMao/pages/message/chat.vue

@@ -162,8 +162,8 @@
         </view>
       </view>
     </scroll-view>
-		<!-- 新增:消息发送限制提示 -->
-		<view class="message-limit-tip">
+		<!-- 新增:消息发送限制提示(双方都不是红娘,且不是从红娘工作台进入时才显示) -->
+		<view class="message-limit-tip" v-if="!(String(userId).startsWith('m_') || String(targetUserId).startsWith('m_') || fromMatchmaker)">
 		  <!-- 同时判断isVip和hasMessageLimit,增强可靠性     -->
 		  <text v-if="isVip || !hasMessageLimit" class="vip-tip">✨ VIP特权:无发送次数限制</text>
 		  <text v-else class="limit-tip">
@@ -307,6 +307,7 @@ export default {
 		isVip: false, // 是否VIP用户
 	    remainingCount: 5, // 剩余可发送消息数(非VIP默认5)
 	    hasMessageLimit: true, // 是否有发送限制(VIP为false)
+	    fromMatchmaker: false, // 是否来自红娘详情页的会话(用户与红娘聊天)
 	    
 	    // 语音录制相关
 	    recorderManager: null, // 录音管理器
@@ -362,28 +363,47 @@ export default {
       rawUserId = parseInt(rawUserId);
     }
     
-    if (!rawUserId || isNaN(rawUserId)) {
-      console.error('❌ 无法获取有效的用户ID');
-      uni.showModal({
-        title: '用户信息错误',
-        content: '无法获取用户ID,请重新登录',
-        showCancel: false,
-        success: () => {
-          uni.removeStorageSync('token');
-          uni.removeStorageSync('userInfo');
-          uni.removeStorageSync('userId');
-          uni.reLaunch({
-            url: '/pages/page3/page3'
-          });
-        }
-      });
-      return;
+    // 标记是否为用户与红娘的聊天,用于跳过每日5条限制和文本审核
+    // 支持两种格式:'1' 或 'true'
+    this.fromMatchmaker = options.fromMatchmaker === '1' || options.fromMatchmaker === 'true';
+    
+    // 根据入口来源确定当前会话中的“自己”是谁
+    if (this.fromMatchmaker) {
+      // 红娘工作台入口:使用当前 TIM 登录账号作为 userId(例如 m_22)
+      const imUserId = timManager.getCurrentUserId();
+      if (!imUserId) {
+        console.error('❌ TIM 未登录,无法获取红娘IM账号');
+        return;
+      }
+      this.userId = String(imUserId);
+    } else {
+      // 普通用户入口:仍然使用本地存储的 userId
+      if (!rawUserId || isNaN(rawUserId)) {
+        console.error('❌ 无法获取有效的用户ID');
+        uni.showModal({
+          title: '用户信息错误',
+          content: '无法获取用户ID,请重新登录',
+          showCancel: false,
+          success: () => {
+            uni.removeStorageSync('token');
+            uni.removeStorageSync('userInfo');
+            uni.removeStorageSync('userId');
+            uni.reLaunch({
+              url: '/pages/page3/page3'
+            });
+          }
+        });
+        return;
+      }
+      this.userId = String(rawUserId);
     }
-	
     
-    // 保存用户ID(TIM需要字符串格式)
-    this.userId = String(rawUserId);
+    // 头像:默认先用用户信息中的头像(红娘入口会被 loadMatchmakerAvatar 覆盖)
     this.userAvatar = userInfo.avatar || userInfo.avatarUrl || '/static/default-avatar.svg';
+    // 如果是红娘入口,再从红娘后台加载一次头像覆盖默认值
+    if (this.fromMatchmaker) {
+      await this.loadMatchmakerAvatar();
+    }
     
     // 获取对方用户信息(确保是字符串格式)
     this.targetUserId = String(options.targetUserId);
@@ -394,125 +414,84 @@ export default {
     this.conversationID = `C2C${this.targetUserId}`;
     
     console.log('✅ 聊天页面初始化成功:');
-    console.log('   - 当前用户ID:', rawUserId, '(TIM格式:', this.userId, ')');
+    console.log('   - 当前用户ID:', this.userId);
     console.log('   - 对方用户ID:', this.targetUserId);
     console.log('   - 会话ID:', this.conversationID);
-    
+
     // 初始化 TIM
     await this.initTIM();
-	// 获取用户消息发送限制(VIP状态+剩余次数)
+
+    // 获取用户消息发送限制(VIP 状态 + 剩余次数)
     await this.getUserMessageLimit();
+
     // 等待 SDK Ready 后再加载消息
     await this.waitForSDKReady();
+
     // 先检查拉黑状态
-      this.isBlockedByTarget = await this.checkIsBlockedByTarget();
-      if (this.isBlockedByTarget) {
-        uni.showToast({
-          title: '你已被对方拉黑,无法发送消息',
-          icon: 'none',
-          duration: 3000
-        });
-      }
+    this.isBlockedByTarget = await this.checkIsBlockedByTarget();
+    if (this.isBlockedByTarget) {
+      uni.showToast({
+        title: '你已被对方拉黑,无法发送消息',
+        icon: 'none',
+        duration: 3000
+      });
+    }
+
     // 加载历史消息
     await this.loadMessages();
-    
+
     // 监听新消息
     this.listenMessages();
-    
-    // 监听已读回执(确保在 SDK Ready 之后调用)
-    this.listenMessageReadReceipt();
-    
+
+    // 监听已读回执
+    this.listenMessageReadReceipt && this.listenMessageReadReceipt();
+
     // 标记当前会话的消息为已读
-    this.markConversationRead();
-    
-    // 初始化在线状态监听(HTTP 轮询方式)
-    this.initOnlineStatusPolling();
-    
+    this.markConversationRead && this.markConversationRead();
+
+    // 初始化在线状态轮询
+    this.initOnlineStatusPolling && this.initOnlineStatusPolling();
+
     // 如果有预设消息,自动发送
     if (options.message) {
       const message = decodeURIComponent(options.message);
       console.log('检测到预设消息,准备自动发送:', message);
-      // 等待一下再发送,确保TIM已经初始化完成
       setTimeout(() => {
         this.inputText = message;
         this.sendTextMessage();
       }, 1500);
     }
   },
-  
-  /**
-   * 页面显示时(从其他页面返回)
-   */
-  async onShow() {
-    console.log('=== 聊天页面显示 ===');
-    
-    // 🔥 重新查询会话的 peerReadTime,更新已读状态
-    // 这样即使 A 离开页面期间 B 阅读了消息,A 返回时也能看到最新的已读状态
-    if (this.conversationID && timManager.tim) {
-      try {
-        console.log('🔄 重新查询会话已读状态...');
-        const conversationRes = await timManager.tim.getConversationProfile(this.conversationID);
-        
-        if (conversationRes && conversationRes.data && conversationRes.data.conversation) {
-          const peerReadTime = conversationRes.data.conversation.peerReadTime;
-          console.log('   - 对方最后阅读时间:', peerReadTime, peerReadTime > 0 ? new Date(peerReadTime * 1000).toLocaleString() : '未读');
-          
-          if (peerReadTime && peerReadTime > 0) {
-            let updatedCount = 0;
-            
-            // 更新所有发送时间 <= peerReadTime 的消息为已读
-            this.messages.forEach((msg, index) => {
-              if (msg.fromUserId === this.userId && !msg.isPeerRead && msg.sendStatus !== 4) {
-                const msgTime = Math.floor(msg.sendTime.getTime() / 1000);
-                
-                if (msgTime <= peerReadTime) {
-                  this.$set(this.messages[index], 'isPeerRead', true);
-                  updatedCount++;
-                  console.log(`   - 消息 ${msg.messageId} 已标记为已读`);
-                }
-              }
-            });
-            
-            console.log(`✅ 页面显示时更新了 ${updatedCount} 条消息为已读状态`);
-          }
-        }
-      } catch (error) {
-        console.error('❌ 重新查询会话已读状态失败:', error);
-      }
-    }
-    
-    // 重新标记当前会话为已读(通知对方)
-    this.markConversationRead();
-  },
-  
-  onUnload() {
-    // 页面卸载时移除监听
-    timManager.offMessage(this.handleNewMessage);
-    
-    // 移除消息状态变更监听(已注释)
-    /*
-    if (timManager.tim && this.handleStatusChange) {
-      timManager.tim.off(TIM.EVENT.MESSAGE_STATUS_CHANGED, this.handleStatusChange);
-    }
-    */
-  },
-  
+
   methods: {
     /**
-     * 计算语音消息宽度(根据时长动态变化)
-     * @param {Number} duration 语音时长(秒)
-     * @return {String} 宽度值
+     * 红娘入口:根据当前登录用户ID获取红娘资料并设置头像
      */
-    getVoiceWidth(duration) {
-      // 基础宽度 100rpx,每秒增加 10rpx,最大 400rpx
-      const baseWidth = 100;
-      const widthPerSecond = 10;
-      const maxWidth = 400;
-      
-      const width = Math.min(baseWidth + (duration * widthPerSecond), maxWidth);
-      return width + 'rpx';
+    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
      */
@@ -520,19 +499,19 @@ export default {
       try {
         // 如果未初始化,先初始化
         if (!timManager.tim) {
-          timManager.init(1600109674);  // ✅ 已更新为正确的 SDKAppID
+          timManager.init(1600109674); // 使用正确的 SDKAppID
         }
-        
+
         // 如果未登录,获取 userSig 并登录
         if (!timManager.isLogin) {
-          // 先导入当前用户和目标用户到腾讯云IM
+          // 先导入当前用户和目标用户到腾讯云 IM
           await this.importUsers();
-          
+
           // 从后端获取 userSig
           const userSig = await this.getUserSig();
           await timManager.login(this.userId, userSig);
         }
-        
+
         this.isLogin = true;
         console.log('✅ TIM 初始化完成');
       } catch (error) {
@@ -543,26 +522,27 @@ export default {
         });
       }
     },
-	/**
-	   * 跳转到VIP页面
-	   */
-	  goToVipPage() {
-	    console.log('点击跳转VIP页面');
-	    // 替换为你的实际VIP页面路径(例如会员开通页面)
-	    uni.navigateTo({
-	      url: '/pages/vip/index', // 请根据项目实际路径修改
-	      success: () => {
-	        console.log('跳转VIP页面成功');
-	      },
-	      fail: (err) => {
-	        console.error('跳转VIP页面失败:', err);
-	        uni.showToast({
-	          title: 'VIP页面不存在',
-	          icon: 'none'
-	        });
-	      }
-	    });
-	  },
+
+    /**
+     * 跳转到VIP页面
+     */
+    goToVipPage() {
+      console.log('点击跳转VIP页面');
+      // 替换为你的实际VIP页面路径(例如会员开通页面)
+      uni.navigateTo({
+        url: '/pages/vip/index', // 请根据项目实际路径修改
+        success: () => {
+          console.log('跳转VIP页面成功');
+        },
+        fail: (err) => {
+          console.error('跳转VIP页面失败:', err);
+          uni.showToast({
+            title: 'VIP页面不存在',
+            icon: 'none'
+          });
+        }
+      });
+    },
     more(userid) {
       console.log('点击了更多按钮,开始跳转...', userid);
       // 如果目标页面是普通页面,用navigateTo(保留当前页面);如果是tabbar页面,用switchTab
@@ -599,7 +579,7 @@ export default {
         
         // 导入当前用户(确保userId是字符串)
         const currentUserRes = await uni.request({
-          url: 'http://localhost:1004/api/im/importUser',
+          url: 'http://localhost:8083/api/im/importUser',
           method: 'POST',
           data: {
             userId: String(this.userId),
@@ -614,7 +594,7 @@ export default {
         
         // 导入目标用户(确保userId是字符串)
         const targetUserRes = await uni.request({
-          url: 'http://localhost:1004/api/im/importUser',
+          url: 'http://localhost:8083/api/im/importUser',
           method: 'POST',
           data: {
             userId: String(this.targetUserId),
@@ -639,7 +619,7 @@ export default {
     async getUserSig() {
       try {
         const [err, res] = await uni.request({
-          url: 'http://localhost:1004/api/im/getUserSig',
+          url: 'http://localhost:8083/api/im/getUserSig',
           method: 'GET',
           data: {
             userId: this.userId
@@ -1095,8 +1075,11 @@ export default {
      * 发送文本消息
      */
     async sendTextMessage() {
+		  // 判断是否为涉及红娘的会话:任一方ID以 m_ 开头,或来自红娘工作台
+		  const isMatchmakerChat = this.fromMatchmaker || String(this.userId).startsWith('m_') || String(this.targetUserId).startsWith('m_');
 		
-		if (this.hasMessageLimit && this.remainingCount <= 0) {
+		  // 仅普通用户之间的会话才做消息次数限制
+		  if (!isMatchmakerChat && this.hasMessageLimit && this.remainingCount <= 0) {
 		    uni.showToast({
 		      title: '今日消息发送次数已用完,开通VIP无限制',
 		      icon: 'none',
@@ -1112,48 +1095,52 @@ export default {
       const content = this.inputText;
       this.inputText = '';
       
-      // 1. 先进行消息内容审核
-      try {
-        const checkRes = await uni.request({
-          url: 'http://localhost:1004/api/chat/checkMessage',
-          method: 'POST',
-          data: {
-            userId: String(this.userId),
-            content: content
-          },
-          header: {
-            'Content-Type': 'application/json'
-          }
-        });
-        
-        if (checkRes[1].data.code !== 200) {
-          // 审核未通过,显示失败消息
-          const failedMessage = {
-            messageId: 'failed_' + Date.now(),
-            fromUserId: this.userId,
-            toUserId: this.targetUserId,
-            messageType: 1,
-            content: content,
-            sendStatus: 4, // 4表示发送失败
-            failReason: 'audit_failed', // 标记失败原因:审核失败
-            sendTime: new Date(),
-            fromUserName: '我'
-          };
-          
-          this.messages.push(failedMessage);
-          this.scrollToBottom();
-          
-          uni.showToast({
-            title: checkRes[1].data.message || '消息发送失败',
-            icon: 'none',
-            duration: 2000
+      // 1. 先进行消息内容审核(只要有一方是红娘就跳过审核)
+      if (!isMatchmakerChat) {
+        try {
+          const checkRes = await uni.request({
+            url: 'http://localhost:8083/api/chat/checkMessage',
+            method: 'POST',
+            data: {
+              userId: String(this.userId),
+              content: content
+            },
+            header: {
+              'Content-Type': 'application/json'
+            }
           });
           
-          return;
+          if (checkRes[1].data.code !== 200) {
+            // 审核未通过,显示失败消息
+            const failedMessage = {
+              messageId: 'failed_' + Date.now(),
+              fromUserId: this.userId,
+              toUserId: this.targetUserId,
+              messageType: 1,
+              content: content,
+              sendStatus: 4, // 4表示发送失败
+              failReason: 'audit_failed', // 标记失败原因:审核失败
+              sendTime: new Date(),
+              fromUserName: '我'
+            };
+            
+            this.messages.push(failedMessage);
+            this.scrollToBottom();
+            
+            uni.showToast({
+              title: checkRes[1].data.message || '消息发送失败',
+              icon: 'none',
+              duration: 2000
+            });
+            
+            return;
+          }
+        } catch (error) {
+          console.error('❌ 消息审核失败:', error);
+          // 审核接口失败,为了不影响用户体验,继续发送
         }
-      } catch (error) {
-        console.error('❌ 消息审核失败:', error);
-        // 审核接口失败,为了不影响用户体验,继续发送
+      } else {
+        console.log('✅ 红娘聊天模式:跳过文本审核');
       }
       
       // 2. 检查是否被拉黑
@@ -1218,7 +1205,8 @@ export default {
         
         console.log('✅ 消息发送成功');
         this.syncMessageToMySQL(message);
-		if (!this.isVip) {
+		  // 仅普通用户之间的会话才扣减消息次数
+		  if (!isMatchmakerChat && !this.isVip) {
 		    await this.updateMessageCount();
 		  }
       } catch (error) {
@@ -1338,7 +1326,7 @@ export default {
         
         // 调用后端同步接口
         const res = await uni.request({
-          url: 'http://localhost:1004/api/chat/syncTIMMessage',
+          url: 'http://localhost:8083/api/chat/syncTIMMessage',
           method: 'POST',
           data: syncData,
           header: {
@@ -2084,7 +2072,7 @@ export default {
         
         // 使用uni.uploadFile上传到后端MinIO接口
         const [err, res] = await uni.uploadFile({
-          url: 'http://localhost:1004/api/voice/upload',
+          url: 'http://localhost:8083/api/voice/upload',
           filePath: this.voiceTempPath,
           name: 'file',
           header: {
@@ -2287,6 +2275,16 @@ export default {
 		     * 从后端获取VIP状态和今日剩余发送次数
 		     */
 		    async getUserMessageLimit() {
+		      // 只要一方是红娘(ID 以 m_ 开头),就不做每日5条限制,直接视为无限制
+		      const isMatchmakerChat = String(this.userId).startsWith('m_') || String(this.targetUserId).startsWith('m_');
+		      if (isMatchmakerChat || this.fromMatchmaker) {
+		        this.hasMessageLimit = false;
+		        this.isVip = true;
+		        this.remainingCount = 999;
+		        console.log('✅ 红娘聊天模式:无消息限制(跳过 getUserMessageLimit 接口)');
+		        return;
+		      }
+		      
 		      try {
 		        const [err, res] = await uni.request({
 		          url: 'http://localhost:1004/api/chat/getUserMessageLimit',

+ 91 - 59
LiangZhiYUMao/pages/message/index.vue

@@ -711,76 +711,109 @@ export default {
      */
     async loadUserAvatars() {
       try {
-        if (this.conversations.length === 0) {
-          return;
-        }
+        // 分离普通用户和红娘ID
+        const normalUserIds = [];
+        const matchmakerIds = [];
         
-        // 收集所有需要获取头像的用户ID
-        const userIds = this.conversations
-          .map(conv => conv.targetUserId)
-          .filter(id => id)  // 过滤掉空值
-          .join(',');
+        this.conversations.forEach(conv => {
+          if (!conv.targetUserId) return;
+          
+          if (conv.targetUserId.startsWith('m_')) {
+            // 红娘ID,去掉 m_ 前缀
+            const matchmakerId = conv.targetUserId.substring(2);
+            matchmakerIds.push(matchmakerId);
+          } else {
+            // 普通用户ID
+            normalUserIds.push(conv.targetUserId);
+          }
+        });
         
-        if (!userIds) {
-          console.log('⚠️ 没有需要获取头像的用户');
-          return;
-        }
+        console.log('🔄 开始批量获取信息 - 用户:', normalUserIds.length, '个, 红娘:', matchmakerIds.length, '个');
         
-        console.log('🔄 开始批量获取用户头像,用户ID列表:', userIds);
+        // 创建ID到信息的映射
+        const userInfoMap = {};
         
-        // 调用批量获取用户信息接口
-        const res = await uni.request({
-          url: 'http://localhost:8083/api/user/batch',
-          method: 'GET',
-          data: {
-            userIds: userIds
+        // 1. 批量查询普通用户
+        if (normalUserIds.length > 0) {
+          const userRes = await uni.request({
+            url: 'http://localhost:8083/api/user/batch',
+            method: 'GET',
+            data: {
+              userIds: normalUserIds.join(',')
+            }
+          });
+          
+          if (userRes[1].statusCode === 200 && userRes[1].data.code === 200) {
+            const userList = userRes[1].data.data || [];
+            userList.forEach(user => {
+              userInfoMap[user.userId] = {
+                nickname: user.nickname,
+                avatarUrl: user.avatarUrl
+              };
+            });
+            console.log('✅ 批量获取用户信息成功,数量:', userList.length);
           }
-        });
+        }
         
-        if (res[1].statusCode === 200 && res[1].data.code === 200) {
-          const userList = res[1].data.data || [];
-          console.log('✅ 批量获取用户信息成功,数量:', userList.length);
-          
-          // 创建用户ID到用户信息的映射
-          const userMap = {};
-          userList.forEach(user => {
-            userMap[user.userId] = user;
+        // 2. 批量查询红娘
+        if (matchmakerIds.length > 0) {
+          const matchmakerRes = await uni.request({
+            url: 'http://localhost:8081/api/matchmaker/batch',
+            method: 'GET',
+            data: {
+              matchmakerIds: matchmakerIds.join(',')
+            }
           });
           
-          // 收集数据库中不存在的用户ID
-          const deletedUserIds = [];
-          
-          // 更新会话列表中的头像和昵称
-          for (const conv of this.conversations) {
-            const userInfo = userMap[conv.targetUserId];
-            if (userInfo) {
-              // 更新头像(如果数据库中有头像)
-              if (userInfo.avatarUrl) {
-                conv.targetUserAvatar = userInfo.avatarUrl;
-              }
-              // 更新昵称(如果数据库中的昵称更准确)
-              if (userInfo.nickname && userInfo.nickname !== `用户${conv.targetUserId}`) {
-                conv.targetUserName = userInfo.nickname;
-              }
-              
-              console.log(`🖼️ 更新用户 ${conv.targetUserId} 的信息:`, {
-                昵称: conv.targetUserName,
-                头像: conv.targetUserAvatar
-              });
-            } else {
-              // 数据库中不存在该用户,标记为需要删除
+          if (matchmakerRes[1].statusCode === 200 && matchmakerRes[1].data.code === 200) {
+            const matchmakerList = matchmakerRes[1].data.data || [];
+            matchmakerList.forEach(matchmaker => {
+              // 红娘的 IM ID 是 m_ + matchmakerId
+              const imUserId = 'm_' + matchmaker.matchmakerId;
+              userInfoMap[imUserId] = {
+                nickname: matchmaker.realName,
+                avatarUrl: matchmaker.avatarUrl
+              };
+            });
+            console.log('✅ 批量获取红娘信息成功,数量:', matchmakerList.length);
+          }
+        }
+        
+        // 收集数据库中不存在的用户ID(排除红娘)
+        const deletedUserIds = [];
+        
+        // 3. 更新会话列表中的头像和昵称
+        for (const conv of this.conversations) {
+          const userInfo = userInfoMap[conv.targetUserId];
+          if (userInfo) {
+            // 更新头像
+            if (userInfo.avatarUrl) {
+              conv.targetUserAvatar = userInfo.avatarUrl;
+            }
+            // 更新昵称
+            if (userInfo.nickname && userInfo.nickname !== `用户${conv.targetUserId}`) {
+              conv.targetUserName = userInfo.nickname;
+            }
+            
+            console.log(`🖼️ 更新用户 ${conv.targetUserId} 的信息:`, {
+              昵称: conv.targetUserName,
+              头像: conv.targetUserAvatar
+            });
+          } else {
+            // 只有普通用户不存在时才标记为删除(红娘不删除)
+            if (!conv.targetUserId.startsWith('m_')) {
               console.warn(`⚠️ 用户 ${conv.targetUserId} 在数据库中不存在`);
               deletedUserIds.push(conv.targetUserId);
+            } else {
+              console.warn(`⚠️ 红娘 ${conv.targetUserId} 在数据库中不存在`);
             }
           }
-          
-          // 如果有不存在的用户,删除TIM中的会话和好友
-          if (deletedUserIds.length > 0) {
-            console.log(`🗑️ 发现 ${deletedUserIds.length} 个不存在的用户,开始清理TIM数据...`);
-            await this.cleanupDeletedUsers(deletedUserIds);
-          }
-        } else {
-          console.error('❌ 批量获取用户信息失败:', res[1].data);
+        }
+        
+        // 4. 如果有不存在的普通用户,删除TIM中的会话和好友
+        if (deletedUserIds.length > 0) {
+          console.log(`🗑️ 发现 ${deletedUserIds.length} 个不存在的用户,开始清理TIM数据...`);
+          await this.cleanupDeletedUsers(deletedUserIds);
         }
       } catch (error) {
         console.error('❌ 批量获取用户头像失败:', error);
@@ -790,7 +823,6 @@ export default {
     
     /**
      * 格式化会话数据
-     * 将TIM的会话格式转换为UI需要的格式
      */
     formatConversation(timConv) {
       // conversationID 格式: C2C{userId}

+ 92 - 3
LiangZhiYUMao/utils/api.js

@@ -218,9 +218,29 @@ export default {
       data: params 
     }),
     
-    // 获取课程详情
-    getDetail: (id) => request({ 
-      url: `/course/detail/${id}`,
+    // 获取课程详情(带学习进度)
+    getDetail: (courseId, makerId) => request({ 
+      url: `/course/detail/${courseId}${makerId ? '?makerId=' + makerId : ''}`,
+      method: 'GET'
+    }),
+    
+    // 更新学习进度
+    updateProgress: (makerId, courseId, progress) => request({
+      url: '/course/progress',
+      method: 'POST',
+      data: { makerId, courseId, progress }
+    }),
+    
+    // 完成课程(领取积分)
+    complete: (makerId, courseId) => request({
+      url: '/course/complete',
+      method: 'POST',
+      data: { makerId, courseId }
+    }),
+    
+    // 获取我的学习记录
+    getMyProgress: (makerId) => request({
+      url: `/course/my-progress?makerId=${makerId}`,
       method: 'GET'
     }),
     
@@ -717,6 +737,75 @@ export default {
         })
       })
     }
+  },
+
+  // 积分商城相关
+  pointsMall: {
+    // 获取商品列表
+    getProducts: (params) => request({
+      url: '/points/products',
+      method: 'GET',
+      data: params
+    }),
+    
+    // 获取推荐商品
+    getRecommendProducts: (limit = 10) => request({
+      url: `/points/products/recommend?limit=${limit}`,
+      method: 'GET'
+    }),
+    
+    // 获取商品详情
+    getProductDetail: (id) => request({
+      url: `/points/products/${id}`,
+      method: 'GET'
+    }),
+    
+    // 获取积分余额
+    getBalance: (makerId) => request({
+      url: `/points/balance?makerId=${makerId}`,
+      method: 'GET'
+    }),
+    
+    // 获取积分明细
+    getRecords: (makerId, pageNum = 1, pageSize = 20) => request({
+      url: `/points/records?makerId=${makerId}&pageNum=${pageNum}&pageSize=${pageSize}`,
+      method: 'GET'
+    }),
+    
+    // 获取积分规则
+    getRules: () => request({
+      url: '/points/rules',
+      method: 'GET'
+    }),
+    
+    // 兑换商品
+    exchange: (data) => request({
+      url: '/points/exchange',
+      method: 'POST',
+      data
+    }),
+    
+    // 获取订单列表
+    getOrders: (makerId, status, pageNum = 1, pageSize = 10) => {
+      let url = `/points/orders?makerId=${makerId}&pageNum=${pageNum}&pageSize=${pageSize}`
+      if (status !== undefined && status !== null) {
+        url += `&status=${status}`
+      }
+      return request({ url, method: 'GET' })
+    },
+    
+    // 获取订单详情
+    getOrderDetail: (orderNo) => request({
+      url: `/points/orders/${orderNo}`,
+      method: 'GET'
+    }),
+    
+    // 增加积分(签到等)
+    addPoints: (makerId, ruleType, reason) => request({
+      url: '/points/add',
+      method: 'POST',
+      data: { makerId, ruleType, reason }
+    })
   }
 }
 

+ 14 - 0
LiangZhiYUMao/utils/tim-manager.js

@@ -524,6 +524,20 @@ class TIMManager {
       console.log('✅ 已移除已读回执回调,剩余回调数:', this.messageReadCallbacks.length);
     }
   }
+
+  /**
+   * 获取 TIM 实例
+   */
+  getTim() {
+    return this.tim;
+  }
+
+  /**
+   * 获取当前登录的 IM 用户ID
+   */
+  getCurrentUserId() {
+    return this.userId ? String(this.userId) : null;
+  }
 }
 
 // 导出单例

+ 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);
     }
   }
 

+ 1 - 1
gateway/src/main/java/com/zhentao/filter/GatewayRoutes.java

@@ -43,7 +43,7 @@ public class GatewayRoutes {
                 // 首页服务路由 - 课程接口(端口8081)
                 .route("homepage_course_route", r -> r.path("/api/course/**")
                         .uri("http://localhost:8081"))
-                .route("homepage_chatfriend_route", r -> r.path("/api/chatfriend/**")
+                .route("homepage_chatfriend_route", r -> r.path("/api/chatfriend/**","/api/online/**","/api/im/**")
                         .uri("http://localhost:1004"))
                 
                 // 首页服务路由 - 我的资源接口(端口8081)

+ 16 - 0
gateway/src/main/resources/application.yml

@@ -150,6 +150,22 @@ spring:
             - Path=/api/partner-requirement/**
           filters:
             - StripPrefix=0
+        
+        # 积分商城路由(admin服务)
+        - id: points-mall-route
+          uri: http://localhost:8088
+          predicates:
+            - Path=/api/points/**
+          filters:
+            - StripPrefix=1
+        
+        # 课程服务路由(admin服务)
+        - id: course-route
+          uri: http://localhost:8088
+          predicates:
+            - Path=/api/course/**
+          filters:
+            - StripPrefix=1
 
         # 首页服务路由(兜底路由)
         - id: homepage-route

+ 226 - 0
service/admin/src/main/java/com/zhentao/controller/PointsMallController.java

@@ -0,0 +1,226 @@
+package com.zhentao.controller;
+
+import com.zhentao.common.Result;
+import com.zhentao.entity.PointRule;
+import com.zhentao.entity.PointsOrder;
+import com.zhentao.entity.PointsProduct;
+import com.zhentao.service.PointsMallService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 积分商城控制器
+ */
+@RestController
+@RequestMapping("/points")
+@CrossOrigin(origins = "*")
+public class PointsMallController {
+    
+    @Autowired
+    private PointsMallService pointsMallService;
+    
+    /**
+     * 获取商品列表
+     * @param category 分类(可选): 1-实物商品 2-虚拟商品
+     * @param pageNum 页码
+     * @param pageSize 每页数量
+     */
+    @GetMapping("/products")
+    public Result<Map<String, Object>> getProductList(
+            @RequestParam(required = false) Integer category,
+            @RequestParam(defaultValue = "1") Integer pageNum,
+            @RequestParam(defaultValue = "10") Integer pageSize) {
+        try {
+            Map<String, Object> data = pointsMallService.getProductList(category, pageNum, pageSize);
+            return Result.success(data);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("获取商品列表失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取推荐商品
+     * @param limit 数量限制
+     */
+    @GetMapping("/products/recommend")
+    public Result<List<PointsProduct>> getRecommendProducts(
+            @RequestParam(defaultValue = "10") Integer limit) {
+        try {
+            List<PointsProduct> products = pointsMallService.getRecommendProducts(limit);
+            return Result.success(products);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("获取推荐商品失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取商品详情
+     * @param id 商品ID
+     */
+    @GetMapping("/products/{id}")
+    public Result<PointsProduct> getProductDetail(@PathVariable Long id) {
+        try {
+            PointsProduct product = pointsMallService.getProductDetail(id);
+            if (product == null) {
+                return Result.error("商品不存在");
+            }
+            return Result.success(product);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("获取商品详情失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取积分余额
+     * @param makerId 红娘ID
+     */
+    @GetMapping("/balance")
+    public Result<Map<String, Object>> getPointsBalance(@RequestParam Long makerId) {
+        try {
+            Integer balance = pointsMallService.getPointsBalance(makerId);
+            Map<String, Object> data = new HashMap<>();
+            data.put("balance", balance);
+            data.put("makerId", makerId);
+            return Result.success(data);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("获取积分余额失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取积分明细
+     * @param makerId 红娘ID
+     * @param pageNum 页码
+     * @param pageSize 每页数量
+     */
+    @GetMapping("/records")
+    public Result<Map<String, Object>> getPointsRecords(
+            @RequestParam Long makerId,
+            @RequestParam(defaultValue = "1") Integer pageNum,
+            @RequestParam(defaultValue = "20") Integer pageSize) {
+        try {
+            Map<String, Object> data = pointsMallService.getPointsRecords(makerId, pageNum, pageSize);
+            // 同时返回当前余额
+            Integer balance = pointsMallService.getPointsBalance(makerId);
+            data.put("balance", balance);
+            return Result.success(data);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("获取积分明细失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取积分规则(赚取积分方式)
+     */
+    @GetMapping("/rules")
+    public Result<List<PointRule>> getPointRules() {
+        try {
+            List<PointRule> rules = pointsMallService.getPointRules();
+            return Result.success(rules);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("获取积分规则失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 兑换商品
+     */
+    @PostMapping("/exchange")
+    public Result<PointsOrder> exchangeProduct(@RequestBody Map<String, Object> params) {
+        try {
+            Long makerId = Long.valueOf(params.get("makerId").toString());
+            Long productId = Long.valueOf(params.get("productId").toString());
+            Integer quantity = params.get("quantity") != null ? 
+                    Integer.valueOf(params.get("quantity").toString()) : 1;
+            String receiverName = (String) params.get("receiverName");
+            String receiverPhone = (String) params.get("receiverPhone");
+            String receiverAddress = (String) params.get("receiverAddress");
+            
+            PointsOrder order = pointsMallService.exchangeProduct(
+                    makerId, productId, quantity, receiverName, receiverPhone, receiverAddress);
+            
+            return Result.success("兑换成功", order);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("兑换失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取兑换订单列表
+     * @param makerId 红娘ID
+     * @param status 订单状态(可选): 0-待发货 1-已发货 2-已完成 3-已取消
+     * @param pageNum 页码
+     * @param pageSize 每页数量
+     */
+    @GetMapping("/orders")
+    public Result<Map<String, Object>> getOrderList(
+            @RequestParam Long makerId,
+            @RequestParam(required = false) Integer status,
+            @RequestParam(defaultValue = "1") Integer pageNum,
+            @RequestParam(defaultValue = "10") Integer pageSize) {
+        try {
+            Map<String, Object> data = pointsMallService.getOrderList(makerId, status, pageNum, pageSize);
+            return Result.success(data);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("获取订单列表失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取订单详情
+     * @param orderNo 订单编号
+     */
+    @GetMapping("/orders/{orderNo}")
+    public Result<PointsOrder> getOrderDetail(@PathVariable String orderNo) {
+        try {
+            PointsOrder order = pointsMallService.getOrderDetail(orderNo);
+            if (order == null) {
+                return Result.error("订单不存在");
+            }
+            return Result.success(order);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("获取订单详情失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 增加积分(签到、上传线索等)
+     */
+    @PostMapping("/add")
+    public Result<Map<String, Object>> addPoints(@RequestBody Map<String, Object> params) {
+        try {
+            Long makerId = Long.valueOf(params.get("makerId").toString());
+            Integer ruleType = Integer.valueOf(params.get("ruleType").toString());
+            String reason = (String) params.get("reason");
+            
+            Integer addedPoints = pointsMallService.addPoints(makerId, ruleType, reason);
+            Integer newBalance = pointsMallService.getPointsBalance(makerId);
+            
+            Map<String, Object> data = new HashMap<>();
+            data.put("addedPoints", addedPoints);
+            data.put("newBalance", newBalance);
+            
+            return Result.success("积分增加成功", data);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("积分增加失败: " + e.getMessage());
+        }
+    }
+}

+ 38 - 0
service/admin/src/main/java/com/zhentao/entity/PointRule.java

@@ -0,0 +1,38 @@
+package com.zhentao.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 积分规则实体类
+ */
+@Data
+@TableName("point_rule")
+public class PointRule implements Serializable {
+    
+    private static final long serialVersionUID = 1L;
+    
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+    
+    /** 规则名称 */
+    private String ruleName;
+    
+    /** 规则类型 */
+    private Integer ruleType;
+    
+    /** 积分值 */
+    private Integer pointValue;
+    
+    /** 状态 */
+    private Integer status;
+    
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime createdTime;
+}

+ 38 - 0
service/admin/src/main/java/com/zhentao/entity/PointsDetail.java

@@ -0,0 +1,38 @@
+package com.zhentao.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 积分明细实体类
+ */
+@Data
+@TableName("points_detail")
+public class PointsDetail implements Serializable {
+    
+    private static final long serialVersionUID = 1L;
+    
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+    
+    /** 红娘ID */
+    private Long makerId;
+    
+    /** 积分变动(正数增加,负数减少) */
+    private Integer points;
+    
+    /** 变动原因 */
+    private String reason;
+    
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime createTime;
+    
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime updateTime;
+}

+ 74 - 0
service/admin/src/main/java/com/zhentao/entity/PointsOrder.java

@@ -0,0 +1,74 @@
+package com.zhentao.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 积分兑换订单实体类
+ */
+@Data
+@TableName("points_order")
+public class PointsOrder implements Serializable {
+    
+    private static final long serialVersionUID = 1L;
+    
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+    
+    /** 订单编号 */
+    private String orderNo;
+    
+    /** 红娘ID */
+    private Long makerId;
+    
+    /** 商品ID */
+    private Long productId;
+    
+    /** 商品名称 */
+    private String productName;
+    
+    /** 商品图片 */
+    private String productImage;
+    
+    /** 兑换积分 */
+    private Integer pointsPrice;
+    
+    /** 数量 */
+    private Integer quantity;
+    
+    /** 总积分 */
+    private Integer totalPoints;
+    
+    /** 状态: 0-待发货 1-已发货 2-已完成 3-已取消 */
+    private Integer status;
+    
+    /** 收货人姓名 */
+    private String receiverName;
+    
+    /** 收货人电话 */
+    private String receiverPhone;
+    
+    /** 收货地址 */
+    private String receiverAddress;
+    
+    /** 快递公司 */
+    private String expressCompany;
+    
+    /** 快递单号 */
+    private String expressNo;
+    
+    /** 备注 */
+    private String remark;
+    
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime createTime;
+    
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime updateTime;
+}

+ 56 - 0
service/admin/src/main/java/com/zhentao/entity/PointsProduct.java

@@ -0,0 +1,56 @@
+package com.zhentao.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 积分商品实体类
+ */
+@Data
+@TableName("points_product")
+public class PointsProduct implements Serializable {
+    
+    private static final long serialVersionUID = 1L;
+    
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+    
+    /** 商品名称 */
+    private String name;
+    
+    /** 商品描述 */
+    private String description;
+    
+    /** 商品图片 */
+    private String imageUrl;
+    
+    /** 积分价格 */
+    private Integer pointsPrice;
+    
+    /** 库存数量 */
+    private Integer stock;
+    
+    /** 分类: 1-实物商品 2-虚拟商品 */
+    private Integer category;
+    
+    /** 是否推荐: 0-否 1-是 */
+    private Integer isRecommend;
+    
+    /** 状态: 0-下架 1-上架 */
+    private Integer status;
+    
+    /** 排序 */
+    private Integer sortOrder;
+    
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime createTime;
+    
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime updateTime;
+}

+ 12 - 0
service/admin/src/main/java/com/zhentao/mapper/PointRuleMapper.java

@@ -0,0 +1,12 @@
+package com.zhentao.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zhentao.entity.PointRule;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 积分规则Mapper
+ */
+@Mapper
+public interface PointRuleMapper extends BaseMapper<PointRule> {
+}

+ 12 - 0
service/admin/src/main/java/com/zhentao/mapper/PointsDetailMapper.java

@@ -0,0 +1,12 @@
+package com.zhentao.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zhentao.entity.PointsDetail;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 积分明细Mapper
+ */
+@Mapper
+public interface PointsDetailMapper extends BaseMapper<PointsDetail> {
+}

+ 12 - 0
service/admin/src/main/java/com/zhentao/mapper/PointsOrderMapper.java

@@ -0,0 +1,12 @@
+package com.zhentao.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zhentao.entity.PointsOrder;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 积分兑换订单Mapper
+ */
+@Mapper
+public interface PointsOrderMapper extends BaseMapper<PointsOrder> {
+}

+ 12 - 0
service/admin/src/main/java/com/zhentao/mapper/PointsProductMapper.java

@@ -0,0 +1,12 @@
+package com.zhentao.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zhentao.entity.PointsProduct;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 积分商品Mapper
+ */
+@Mapper
+public interface PointsProductMapper extends BaseMapper<PointsProduct> {
+}

+ 99 - 0
service/admin/src/main/java/com/zhentao/service/PointsMallService.java

@@ -0,0 +1,99 @@
+package com.zhentao.service;
+
+import com.zhentao.entity.PointsDetail;
+import com.zhentao.entity.PointsOrder;
+import com.zhentao.entity.PointsProduct;
+import com.zhentao.entity.PointRule;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 积分商城服务接口
+ */
+public interface PointsMallService {
+    
+    /**
+     * 获取商品列表
+     * @param category 分类(可选)
+     * @param pageNum 页码
+     * @param pageSize 每页数量
+     * @return 商品列表
+     */
+    Map<String, Object> getProductList(Integer category, Integer pageNum, Integer pageSize);
+    
+    /**
+     * 获取推荐商品
+     * @param limit 数量限制
+     * @return 推荐商品列表
+     */
+    List<PointsProduct> getRecommendProducts(Integer limit);
+    
+    /**
+     * 获取商品详情
+     * @param productId 商品ID
+     * @return 商品详情
+     */
+    PointsProduct getProductDetail(Long productId);
+    
+    /**
+     * 获取红娘积分余额
+     * @param makerId 红娘ID
+     * @return 积分余额
+     */
+    Integer getPointsBalance(Long makerId);
+    
+    /**
+     * 获取积分明细
+     * @param makerId 红娘ID
+     * @param pageNum 页码
+     * @param pageSize 每页数量
+     * @return 积分明细列表
+     */
+    Map<String, Object> getPointsRecords(Long makerId, Integer pageNum, Integer pageSize);
+    
+    /**
+     * 获取积分规则列表
+     * @return 积分规则列表
+     */
+    List<PointRule> getPointRules();
+    
+    /**
+     * 兑换商品
+     * @param makerId 红娘ID
+     * @param productId 商品ID
+     * @param quantity 数量
+     * @param receiverName 收货人姓名
+     * @param receiverPhone 收货人电话
+     * @param receiverAddress 收货地址
+     * @return 订单信息
+     */
+    PointsOrder exchangeProduct(Long makerId, Long productId, Integer quantity,
+                                 String receiverName, String receiverPhone, String receiverAddress);
+    
+    /**
+     * 获取兑换订单列表
+     * @param makerId 红娘ID
+     * @param status 订单状态(可选)
+     * @param pageNum 页码
+     * @param pageSize 每页数量
+     * @return 订单列表
+     */
+    Map<String, Object> getOrderList(Long makerId, Integer status, Integer pageNum, Integer pageSize);
+    
+    /**
+     * 获取订单详情
+     * @param orderNo 订单编号
+     * @return 订单详情
+     */
+    PointsOrder getOrderDetail(String orderNo);
+    
+    /**
+     * 增加积分(签到、上传线索等)
+     * @param makerId 红娘ID
+     * @param ruleType 规则类型
+     * @param reason 原因描述
+     * @return 增加的积分数
+     */
+    Integer addPoints(Long makerId, Integer ruleType, String reason);
+}

+ 98 - 11
service/admin/src/main/java/com/zhentao/service/impl/MarrApplyServiceImpl.java

@@ -2,17 +2,26 @@ package com.zhentao.service.impl;
 
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.zhentao.entity.MarrApply;
+import com.zhentao.entity.Matchmaker;
 import com.zhentao.entity.Users;
 import com.zhentao.service.MarrApplyService;
 import com.zhentao.mapper.MarrApplyMapper;
+import com.zhentao.mapper.MatchmakerMapper;
 import com.zhentao.mapper.UsersMapper;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import org.springframework.util.StringUtils;
+import org.springframework.web.client.RestTemplate;
 
+import java.time.LocalDateTime;
 import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
 
 /**
 * @author 联想
@@ -25,6 +34,14 @@ public class MarrApplyServiceImpl extends ServiceImpl<MarrApplyMapper, MarrApply
 
     @Autowired
     private UsersMapper usersMapper;
+    
+    @Autowired
+    private MatchmakerMapper matchmakerMapper;
+    
+    private final RestTemplate restTemplate = new RestTemplate();
+    
+    // websocket 服务的 IM 接口地址
+    private static final String IM_SERVICE_URL = "http://localhost:1004/api/im";
 
     @Override
     public Page<MarrApply> pageQuery(Integer pageNum, Integer pageSize, String name, String phone) {
@@ -38,28 +55,98 @@ public class MarrApplyServiceImpl extends ServiceImpl<MarrApplyMapper, MarrApply
     @Override
     @Transactional(rollbackFor = Exception.class)
     public boolean approve(Long applyId, Integer userId) {
-        // 1. 查询用户是否存在
+        System.out.println("========== 开始审核红娘申请 ==========");
+        System.out.println("申请ID: " + applyId + ", 用户ID: " + userId);
+        
+        // 1. 查询申请信息
+        MarrApply apply = this.getById(applyId);
+        if (apply == null) {
+            throw new RuntimeException("申请记录不存在");
+        }
+        
+        // 2. 查询用户是否存在
         Users user = usersMapper.selectById(userId);
         if (user == null) {
             throw new RuntimeException("用户不存在");
         }
         
-        // 2. 更新用户的isMatchmaker为1
-        user.setIsMatchmaker(1);
-        int userUpdateResult = usersMapper.updateById(user);
+        // 3. 更新用户的isMatchmaker为1
+        // 直接使用 MyBatis-Plus 的 update 方法
+        Users updateUser = new Users();
+        updateUser.setUserId(userId);
+        updateUser.setIsMatchmaker(1);
+        int userUpdateResult = usersMapper.updateById(updateUser);
         if (userUpdateResult <= 0) {
             throw new RuntimeException("更新用户红娘状态失败");
         }
+        System.out.println("✅ 已更新 users 表,is_matchmaker = 1");
         
-        // 3. 更新申请记录的更新人和更新时间
-        MarrApply apply = this.getById(applyId);
-        if (apply != null) {
-            apply.setUpdateTime(new Date());
-            // 这里可以从当前登录用户获取,暂时设置为"系统"
-            apply.setUpdateMan("系统");
-            this.updateById(apply);
+        // 4. 创建 matchmakers 记录
+        Matchmaker matchmaker = new Matchmaker();
+        
+        // 从 marr_apply 表获取数据
+        matchmaker.setRealName(apply.getName());           // 真实姓名
+        matchmaker.setPhone(apply.getPhone());             // 手机号
+        matchmaker.setEmail(apply.getEmail());             // 邮箱
+        matchmaker.setGender(apply.getGender());           // 性别
+        matchmaker.setProfile(apply.getIntroduction());    // 个人简介
+        
+        // 从 users 表获取数据
+        matchmaker.setUsername(user.getNickname() != null ? user.getNickname() : user.getPhone()); // 用户名(优先使用昵称,否则使用手机号)
+        matchmaker.setPassword(user.getPassword());        // 密码(使用用户的密码)
+        matchmaker.setBirthDate(user.getBirthDate());      // 出生日期(更准确)
+        
+        // 头像:根据性别强制设置默认头像
+        if (apply.getGender() != null && apply.getGender() == 1) {
+            // 男性默认头像
+            matchmaker.setAvatarUrl("http://115.190.125.125:9000/dynamic-comments/dynamics/5c645152-9940-41d3-83a9-69ee6e0c0aaa.png");
+        } else {
+            // 女性默认头像(包括性别为2或null的情况)
+            matchmaker.setAvatarUrl("http://115.190.125.125:9000/dynamic-comments/dynamics/c7fb04d7-ee4d-4b3d-bcef-f246da9c841f.png");
+        }
+        
+        // 设置默认值(无法从其他表获取的字段)
+        matchmaker.setMatchmakerType(1);                   // 默认类型:1-普通红娘
+        matchmaker.setLevel(1);                            // 默认等级:1级
+        matchmaker.setSuccessCouples(0);                   // 初始成功撮合数:0
+        matchmaker.setStatus(1);                           // 状态:1-正常
+        matchmaker.setCreateTime(LocalDateTime.now());
+        matchmaker.setUpdateTime(LocalDateTime.now());
+        
+        int insertResult = matchmakerMapper.insert(matchmaker);
+        if (insertResult <= 0) {
+            throw new RuntimeException("创建红娘记录失败");
         }
+        System.out.println("✅ 已创建 matchmakers 记录,matchmaker_id = " + matchmaker.getMatchmakerId());
+        
+        // 5. 导入到腾讯云 IM(使用 m_ + matchmaker_id)
+        String imUserId = "m_" + matchmaker.getMatchmakerId();
+        try {
+            Map<String, String> imParams = new HashMap<>();
+            imParams.put("userId", imUserId);
+            imParams.put("nickname", matchmaker.getRealName());
+            if (matchmaker.getAvatarUrl() != null && !matchmaker.getAvatarUrl().isEmpty()) {
+                imParams.put("faceUrl", matchmaker.getAvatarUrl());
+            }
+            
+            HttpHeaders headers = new HttpHeaders();
+            headers.setContentType(MediaType.APPLICATION_JSON);
+            HttpEntity<Map<String, String>> request = new HttpEntity<>(imParams, headers);
+            
+            restTemplate.postForObject(IM_SERVICE_URL + "/importUser", request, Map.class);
+            System.out.println("✅ 已导入到腾讯云 IM: imUserId = " + imUserId);
+        } catch (Exception e) {
+            System.err.println("❌ 导入到 IM 失败: " + e.getMessage());
+            e.printStackTrace();
+            // 导入失败不影响审核流程,只记录日志
+        }
+        
+        // 6. 更新申请记录的更新人和更新时间
+        apply.setUpdateTime(new Date());
+        apply.setUpdateMan("系统");
+        this.updateById(apply);
         
+        System.out.println("========== 审核完成 ==========");
         return true;
     }
 

+ 276 - 0
service/admin/src/main/java/com/zhentao/service/impl/PointsMallServiceImpl.java

@@ -0,0 +1,276 @@
+package com.zhentao.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zhentao.entity.*;
+import com.zhentao.mapper.*;
+import com.zhentao.service.PointsMallService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+
+/**
+ * 积分商城服务实现类
+ */
+@Service
+public class PointsMallServiceImpl implements PointsMallService {
+    
+    @Autowired
+    private PointsProductMapper pointsProductMapper;
+    
+    @Autowired
+    private PointsOrderMapper pointsOrderMapper;
+    
+    @Autowired
+    private PointsDetailMapper pointsDetailMapper;
+    
+    @Autowired
+    private PointRuleMapper pointRuleMapper;
+    
+    @Autowired
+    private MatchmakerMapper matchmakerMapper;
+    
+    @Override
+    public Map<String, Object> getProductList(Integer category, Integer pageNum, Integer pageSize) {
+        Page<PointsProduct> page = new Page<>(pageNum, pageSize);
+        LambdaQueryWrapper<PointsProduct> wrapper = new LambdaQueryWrapper<>();
+        
+        // 只查询上架的商品
+        wrapper.eq(PointsProduct::getStatus, 1);
+        
+        // 按分类筛选
+        if (category != null && category > 0) {
+            wrapper.eq(PointsProduct::getCategory, category);
+        }
+        
+        // 按排序和创建时间排序
+        wrapper.orderByDesc(PointsProduct::getSortOrder)
+               .orderByDesc(PointsProduct::getCreateTime);
+        
+        Page<PointsProduct> result = pointsProductMapper.selectPage(page, wrapper);
+        
+        Map<String, Object> data = new HashMap<>();
+        data.put("list", result.getRecords());
+        data.put("total", result.getTotal());
+        data.put("pageNum", pageNum);
+        data.put("pageSize", pageSize);
+        data.put("pages", result.getPages());
+        
+        return data;
+    }
+    
+    @Override
+    public List<PointsProduct> getRecommendProducts(Integer limit) {
+        LambdaQueryWrapper<PointsProduct> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(PointsProduct::getStatus, 1)
+               .eq(PointsProduct::getIsRecommend, 1)
+               .orderByDesc(PointsProduct::getSortOrder)
+               .last("LIMIT " + (limit != null ? limit : 10));
+        
+        return pointsProductMapper.selectList(wrapper);
+    }
+    
+    @Override
+    public PointsProduct getProductDetail(Long productId) {
+        return pointsProductMapper.selectById(productId);
+    }
+    
+    @Override
+    public Integer getPointsBalance(Long makerId) {
+        Matchmaker matchmaker = matchmakerMapper.selectById(makerId);
+        if (matchmaker == null) {
+            return 0;
+        }
+        // 优先使用matchmakers表的points字段
+        if (matchmaker.getPoints() != null) {
+            return matchmaker.getPoints();
+        }
+        // 如果points字段为空,通过计算积分明细来获取余额
+        LambdaQueryWrapper<PointsDetail> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(PointsDetail::getMakerId, makerId);
+        List<PointsDetail> details = pointsDetailMapper.selectList(wrapper);
+        
+        int balance = 0;
+        for (PointsDetail detail : details) {
+            balance += detail.getPoints();
+        }
+        return balance;
+    }
+    
+    @Override
+    public Map<String, Object> getPointsRecords(Long makerId, Integer pageNum, Integer pageSize) {
+        Page<PointsDetail> page = new Page<>(pageNum, pageSize);
+        LambdaQueryWrapper<PointsDetail> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(PointsDetail::getMakerId, makerId)
+               .orderByDesc(PointsDetail::getCreateTime);
+        
+        Page<PointsDetail> result = pointsDetailMapper.selectPage(page, wrapper);
+        
+        Map<String, Object> data = new HashMap<>();
+        data.put("list", result.getRecords());
+        data.put("total", result.getTotal());
+        data.put("pageNum", pageNum);
+        data.put("pageSize", pageSize);
+        
+        return data;
+    }
+    
+    @Override
+    public List<PointRule> getPointRules() {
+        LambdaQueryWrapper<PointRule> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(PointRule::getStatus, 1)
+               .orderByAsc(PointRule::getRuleType);
+        return pointRuleMapper.selectList(wrapper);
+    }
+    
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public PointsOrder exchangeProduct(Long makerId, Long productId, Integer quantity,
+                                        String receiverName, String receiverPhone, String receiverAddress) {
+        // 1. 检查商品是否存在且上架
+        PointsProduct product = pointsProductMapper.selectById(productId);
+        if (product == null || product.getStatus() != 1) {
+            throw new RuntimeException("商品不存在或已下架");
+        }
+        
+        // 2. 检查库存
+        if (product.getStock() < quantity) {
+            throw new RuntimeException("商品库存不足");
+        }
+        
+        // 3. 计算所需积分
+        int totalPoints = product.getPointsPrice() * quantity;
+        
+        // 4. 检查积分余额
+        Integer balance = getPointsBalance(makerId);
+        if (balance < totalPoints) {
+            throw new RuntimeException("积分余额不足,当前积分: " + balance + ",需要: " + totalPoints);
+        }
+        
+        // 5. 扣减库存
+        LambdaUpdateWrapper<PointsProduct> productUpdate = new LambdaUpdateWrapper<>();
+        productUpdate.eq(PointsProduct::getId, productId)
+                     .ge(PointsProduct::getStock, quantity)
+                     .setSql("stock = stock - " + quantity);
+        int updateCount = pointsProductMapper.update(null, productUpdate);
+        if (updateCount == 0) {
+            throw new RuntimeException("库存扣减失败,请重试");
+        }
+        
+        // 6. 创建订单
+        PointsOrder order = new PointsOrder();
+        order.setOrderNo(generateOrderNo());
+        order.setMakerId(makerId);
+        order.setProductId(productId);
+        order.setProductName(product.getName());
+        order.setProductImage(product.getImageUrl());
+        order.setPointsPrice(product.getPointsPrice());
+        order.setQuantity(quantity);
+        order.setTotalPoints(totalPoints);
+        order.setStatus(0); // 待发货
+        order.setReceiverName(receiverName);
+        order.setReceiverPhone(receiverPhone);
+        order.setReceiverAddress(receiverAddress);
+        order.setCreateTime(LocalDateTime.now());
+        order.setUpdateTime(LocalDateTime.now());
+        
+        pointsOrderMapper.insert(order);
+        
+        // 7. 记录积分变动(扣减)
+        PointsDetail detail = new PointsDetail();
+        detail.setMakerId(makerId);
+        detail.setPoints(-totalPoints);
+        detail.setReason("兑换商品: " + product.getName() + " x" + quantity);
+        detail.setCreateTime(LocalDateTime.now());
+        detail.setUpdateTime(LocalDateTime.now());
+        
+        pointsDetailMapper.insert(detail);
+        
+        // 8. 更新红娘积分余额
+        LambdaUpdateWrapper<Matchmaker> makerUpdate = new LambdaUpdateWrapper<>();
+        makerUpdate.eq(Matchmaker::getMatchmakerId, makerId)
+                   .setSql("points = IFNULL(points, 0) - " + totalPoints);
+        matchmakerMapper.update(null, makerUpdate);
+        
+        return order;
+    }
+    
+    @Override
+    public Map<String, Object> getOrderList(Long makerId, Integer status, Integer pageNum, Integer pageSize) {
+        Page<PointsOrder> page = new Page<>(pageNum, pageSize);
+        LambdaQueryWrapper<PointsOrder> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(PointsOrder::getMakerId, makerId);
+        
+        if (status != null && status >= 0) {
+            wrapper.eq(PointsOrder::getStatus, status);
+        }
+        
+        wrapper.orderByDesc(PointsOrder::getCreateTime);
+        
+        Page<PointsOrder> result = pointsOrderMapper.selectPage(page, wrapper);
+        
+        Map<String, Object> data = new HashMap<>();
+        data.put("list", result.getRecords());
+        data.put("total", result.getTotal());
+        data.put("pageNum", pageNum);
+        data.put("pageSize", pageSize);
+        
+        return data;
+    }
+    
+    @Override
+    public PointsOrder getOrderDetail(String orderNo) {
+        LambdaQueryWrapper<PointsOrder> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(PointsOrder::getOrderNo, orderNo);
+        return pointsOrderMapper.selectOne(wrapper);
+    }
+    
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Integer addPoints(Long makerId, Integer ruleType, String reason) {
+        // 1. 查询积分规则
+        LambdaQueryWrapper<PointRule> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(PointRule::getRuleType, ruleType)
+               .eq(PointRule::getStatus, 1);
+        PointRule rule = pointRuleMapper.selectOne(wrapper);
+        
+        if (rule == null) {
+            throw new RuntimeException("积分规则不存在");
+        }
+        
+        int pointValue = rule.getPointValue();
+        
+        // 2. 记录积分变动
+        PointsDetail detail = new PointsDetail();
+        detail.setMakerId(makerId);
+        detail.setPoints(pointValue);
+        detail.setReason(reason != null ? reason : rule.getRuleName());
+        detail.setCreateTime(LocalDateTime.now());
+        detail.setUpdateTime(LocalDateTime.now());
+        
+        pointsDetailMapper.insert(detail);
+        
+        // 3. 更新红娘积分余额
+        LambdaUpdateWrapper<Matchmaker> makerUpdate = new LambdaUpdateWrapper<>();
+        makerUpdate.eq(Matchmaker::getMatchmakerId, makerId)
+                   .setSql("points = IFNULL(points, 0) + " + pointValue);
+        matchmakerMapper.update(null, makerUpdate);
+        
+        return pointValue;
+    }
+    
+    /**
+     * 生成订单编号
+     */
+    private String generateOrderNo() {
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
+        String timestamp = LocalDateTime.now().format(formatter);
+        String random = String.format("%04d", new Random().nextInt(10000));
+        return "PO" + timestamp + random;
+    }
+}

+ 2 - 1
service/homePage/src/main/java/com/zhentao/HomePageApplication.java

@@ -3,12 +3,13 @@ package com.zhentao;
 import org.mybatis.spring.annotation.MapperScan;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.ComponentScan;
 
 /**
  * Hello world!
  *
  */
-@SpringBootApplication
+@SpringBootApplication(scanBasePackages = {"com.zhentao"})
 // @EnableDiscoveryClient  // 暂时禁用服务发现
 @MapperScan("com.zhentao.mapper")
 public class HomePageApplication

+ 4 - 1
service/homePage/src/main/java/com/zhentao/constant/RedisKeyConstants.java

@@ -54,12 +54,15 @@ public class RedisKeyConstants {
      * 构建红娘列表缓存Key
      */
     public static String buildMatchmakerListKey(Integer type, Integer level, Integer provinceId, 
-                                                 Integer cityId, Integer pageNum, Integer pageSize) {
+                                                 Integer cityId, String keyword, Integer pageNum, Integer pageSize) {
+        // 将keyword转换为缓存键的一部分,如果为空则使用"all"
+        String keywordPart = (keyword != null && !keyword.trim().isEmpty()) ? keyword.trim() : "all";
         return MATCHMAKER_LIST + 
                (type != null ? type : "all") + ":" +
                (level != null ? level : "all") + ":" +
                (provinceId != null ? provinceId : "all") + ":" +
                (cityId != null ? cityId : "all") + ":" +
+               keywordPart + ":" +
                pageNum + ":" + pageSize;
     }
     

+ 31 - 3
service/homePage/src/main/java/com/zhentao/controller/ActivityController.java

@@ -1,5 +1,6 @@
 package com.zhentao.controller;
 
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.zhentao.common.Result;
 import com.zhentao.entity.Activity;
 import com.zhentao.service.ActivityService;
@@ -52,15 +53,42 @@ public class ActivityController {
      * 
      * @param type 活动类型(可选,支持数字类型或 "hot" 表示热门活动)
      * @param status 活动状态(可选)
-     * @param limit 限制数量(可选)
+     * @param keyword 搜索关键词(可选,用于活动名称模糊查询)
+     * @param page 页码(可选,用于分页)
+     * @param pageSize 每页大小(可选,用于分页)
+     * @param limit 限制数量(可选,当使用分页时此参数无效)
      * @return 活动列表
      */
     @GetMapping("/list")
-    public Result<List<Activity>> getActivityList(
+    public Result<?> getActivityList(
             @RequestParam(required = false) String type,
             @RequestParam(required = false) Integer status,
+            @RequestParam(required = false) String keyword,
+            @RequestParam(required = false) Integer page,
+            @RequestParam(required = false) Integer pageSize,
             @RequestParam(required = false) Integer limit) {
         try {
+            // 如果提供了分页参数,使用分页查询
+            if (page != null && pageSize != null && page > 0 && pageSize > 0) {
+                Integer typeInt = null;
+                if (type != null && !type.isEmpty() && !"hot".equalsIgnoreCase(type)) {
+                    try {
+                        typeInt = Integer.parseInt(type);
+                    } catch (NumberFormatException e) {
+                        // 忽略无法转换的类型
+                    }
+                }
+                Page<Activity> pageResult = activityService.getActivityPage(typeInt, status, keyword, page, pageSize);
+                // 将分页结果转换为前端需要的格式
+                Map<String, Object> data = new HashMap<>();
+                data.put("list", pageResult.getRecords());
+                data.put("total", pageResult.getTotal());
+                data.put("page", page);
+                data.put("pageSize", pageSize);
+                return Result.success(data);
+            }
+            
+            // 否则使用原来的逻辑(不分页)
             List<Activity> activityList;
             
             // 如果是热门活动
@@ -76,7 +104,7 @@ public class ActivityController {
                         // 忽略无法转换的类型
                     }
                 }
-                activityList = activityService.getActivityList(typeInt, status, limit);
+                activityList = activityService.getActivityList(typeInt, status, keyword, limit);
             }
             
             return Result.success(activityList);

+ 177 - 0
service/homePage/src/main/java/com/zhentao/controller/MatchmakerController.java

@@ -7,9 +7,16 @@ import com.zhentao.entity.Matchmaker;
 import com.zhentao.service.MatchmakerService;
 import com.zhentao.vo.MatchmakerVO;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
 import org.springframework.web.bind.annotation.*;
+import org.springframework.web.client.RestTemplate;
 
+import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 /**
  * 红娘控制器
@@ -21,6 +28,11 @@ public class MatchmakerController {
     @Autowired
     private MatchmakerService matchmakerService;
     
+    private final RestTemplate restTemplate = new RestTemplate();
+    
+    // websocket 服务的 IM 接口地址
+    private static final String IM_SERVICE_URL = "http://localhost:1004/api/im";
+    
     /**
      * 分页查询红娘列表
      * 
@@ -65,6 +77,34 @@ public class MatchmakerController {
         }
     }
     
+    /**
+     * 获取当前登录红娘的信息(通过 userId 查询)
+     * 
+     * @param userId 用户ID
+     * @return 红娘信息
+     */
+    @GetMapping("/current")
+    public Result<Map<String, Object>> getCurrentMatchmaker(@RequestParam Integer userId) {
+        try {
+            Matchmaker matchmaker = matchmakerService.getMatchmakerByUserId(userId);
+            if (matchmaker == null) {
+                return Result.error("该用户不是红娘");
+            }
+            
+            Map<String, Object> result = new HashMap<>();
+            result.put("matchmakerId", matchmaker.getMatchmakerId());
+            result.put("realName", matchmaker.getRealName());
+            result.put("avatarUrl", matchmaker.getAvatarUrl());
+            result.put("userId", userId);
+            result.put("imUserId", "m_" + matchmaker.getMatchmakerId());  // IM 用户 ID
+            
+            return Result.success(result);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("获取红娘信息失败:" + e.getMessage());
+        }
+    }
+    
     /**
      * 获取全职红娘列表(首页展示)
      * 
@@ -109,8 +149,32 @@ public class MatchmakerController {
                 matchmaker.setStatus(1); // 1-正常
             }
             
+            // 保存红娘到数据库
             boolean success = matchmakerService.save(matchmaker);
             if (success) {
+                // 导入红娘到腾讯云 IM(使用 m_ + matchmaker_id)
+                String imUserId = "m_" + matchmaker.getMatchmakerId();
+                try {
+                    // 调用 websocket 服务的 IM 导入接口
+                    Map<String, String> imParams = new HashMap<>();
+                    imParams.put("userId", imUserId);
+                    imParams.put("nickname", matchmaker.getRealName());
+                    if (matchmaker.getAvatarUrl() != null && !matchmaker.getAvatarUrl().isEmpty()) {
+                        imParams.put("faceUrl", matchmaker.getAvatarUrl());
+                    }
+                    
+                    HttpHeaders headers = new HttpHeaders();
+                    headers.setContentType(MediaType.APPLICATION_JSON);
+                    HttpEntity<Map<String, String>> request = new HttpEntity<>(imParams, headers);
+                    
+                    restTemplate.postForObject(IM_SERVICE_URL + "/importUser", request, Map.class);
+                    System.out.println("✅ 红娘已导入到腾讯云 IM: matchmakerId=" + matchmaker.getMatchmakerId() + ", imUserId=" + imUserId);
+                } catch (Exception e) {
+                    System.err.println("❌ 导入红娘到 IM 失败: " + e.getMessage());
+                    e.printStackTrace();
+                    // 导入失败不影响红娘创建,只记录日志
+                }
+                
                 // 清除列表缓存
                 matchmakerService.clearMatchmakerCache(null);
                 return Result.success(matchmaker);
@@ -159,8 +223,27 @@ public class MatchmakerController {
     @DeleteMapping("/delete/{matchmakerId}")
     public Result<Void> deleteMatchmaker(@PathVariable Integer matchmakerId) {
         try {
+            // 先查询红娘信息(用于后续删除 IM 账号)
+            Matchmaker matchmaker = matchmakerService.getById(matchmakerId);
+            if (matchmaker == null) {
+                return Result.error("红娘不存在");
+            }
+            
+            // 删除数据库记录
             boolean success = matchmakerService.removeById(matchmakerId);
             if (success) {
+                // 从腾讯云 IM 删除红娘账号(使用 m_ + matchmaker_id)
+                String imUserId = "m_" + matchmakerId;
+                try {
+                    // 调用 websocket 服务的 IM 删除接口
+                    restTemplate.delete(IM_SERVICE_URL + "/deleteAccount?userId=" + imUserId);
+                    System.out.println("✅ 已从腾讯云 IM 删除红娘账号: matchmakerId=" + matchmakerId + ", imUserId=" + imUserId);
+                } catch (Exception e) {
+                    System.err.println("❌ 从 IM 删除账号失败: " + e.getMessage());
+                    e.printStackTrace();
+                    // 删除失败不影响数据库删除,只记录日志
+                }
+                
                 // 清除相关缓存
                 matchmakerService.clearMatchmakerCache(matchmakerId);
                 return Result.success(null);
@@ -210,5 +293,99 @@ public class MatchmakerController {
             return Result.error("获取排行榜失败:" + e.getMessage());
         }
     }
+    
+    /**
+     * 批量查询红娘信息
+     * 
+     * @param matchmakerIds 红娘ID列表,逗号分隔(例如:22,23,24)
+     * @return 红娘信息列表
+     */
+    @GetMapping("/batch")
+    public Result<List<MatchmakerVO>> batchGetMatchmakers(@RequestParam String matchmakerIds) {
+        try {
+            if (matchmakerIds == null || matchmakerIds.trim().isEmpty()) {
+                return Result.success(Collections.emptyList());
+            }
+            
+            // 解析ID列表
+            String[] idArray = matchmakerIds.split(",");
+            List<Integer> ids = new java.util.ArrayList<>();
+            for (String id : idArray) {
+                try {
+                    ids.add(Integer.parseInt(id.trim()));
+                } catch (NumberFormatException e) {
+                    System.err.println("⚠️ 无效的红娘ID: " + id);
+                }
+            }
+            
+            if (ids.isEmpty()) {
+                return Result.success(Collections.emptyList());
+            }
+            
+            // 批量查询
+            List<MatchmakerVO> matchmakers = matchmakerService.batchGetMatchmakers(ids);
+            return Result.success(matchmakers);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("批量查询红娘信息失败:" + e.getMessage());
+        }
+    }
+    
+    /**
+     * 批量导入现有红娘到腾讯云 IM(一次性任务)
+     * 
+     * @return 导入结果
+     */
+    @PostMapping("/batch-import-to-im")
+    public Result<Map<String, Object>> batchImportMatchmakersToIM() {
+        try {
+            List<Matchmaker> matchmakers = matchmakerService.list();
+            
+            int successCount = 0;
+            int failCount = 0;
+            StringBuilder failedIds = new StringBuilder();
+            
+            for (Matchmaker matchmaker : matchmakers) {
+                String imUserId = "m_" + matchmaker.getMatchmakerId();
+                try {
+                    // 调用 websocket 服务的 IM 导入接口
+                    Map<String, String> imParams = new HashMap<>();
+                    imParams.put("userId", imUserId);
+                    imParams.put("nickname", matchmaker.getRealName());
+                    if (matchmaker.getAvatarUrl() != null && !matchmaker.getAvatarUrl().isEmpty()) {
+                        imParams.put("faceUrl", matchmaker.getAvatarUrl());
+                    }
+                    
+                    HttpHeaders headers = new HttpHeaders();
+                    headers.setContentType(MediaType.APPLICATION_JSON);
+                    HttpEntity<Map<String, String>> request = new HttpEntity<>(imParams, headers);
+                    
+                    restTemplate.postForObject(IM_SERVICE_URL + "/importUser", request, Map.class);
+                    successCount++;
+                    System.out.println("✅ 导入成功: matchmakerId=" + matchmaker.getMatchmakerId() + ", imUserId=" + imUserId);
+                    
+                    // 避免请求过快
+                    Thread.sleep(100);
+                } catch (Exception e) {
+                    failCount++;
+                    failedIds.append(matchmaker.getMatchmakerId()).append(",");
+                    System.err.println("❌ 导入失败: matchmakerId=" + matchmaker.getMatchmakerId() + ", error=" + e.getMessage());
+                }
+            }
+            
+            Map<String, Object> result = new HashMap<>();
+            result.put("total", matchmakers.size());
+            result.put("successCount", successCount);
+            result.put("failCount", failCount);
+            if (failCount > 0) {
+                result.put("failedIds", failedIds.toString());
+            }
+            
+            return Result.success(result);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("批量导入失败:" + e.getMessage());
+        }
+    }
 }
 

+ 401 - 10
service/homePage/src/main/java/com/zhentao/controller/MyResourceController.java

@@ -8,6 +8,7 @@ import com.zhentao.entity.User;
 import com.zhentao.mapper.UserMapper;
 import com.zhentao.service.MyResourceService;
 import com.zhentao.vo.MyResourceVO;
+import com.zhentao.vo.ClientDetailVO;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
@@ -21,28 +22,86 @@ 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;
     
     /**
      * 添加资源信息
      * 
-     * @param myResource 资源信息
+     * @param requestBody 请求体,包含资源信息和标签ID列表
      * @param currentUserId 当前登录用户的用户ID(将作为红娘ID)
      * @return 添加结果
      */
     @PostMapping("/add")
     public Result<MyResource> addResource(
-            @RequestBody MyResource myResource,
+            @RequestBody java.util.Map<String, Object> requestBody,
             @RequestParam(required = false) Integer currentUserId) {
         try {
+            // 从请求体中提取资源信息和标签ID列表
+            MyResource myResource = new MyResource();
+            
+            // 使用ObjectMapper转换资源信息
+            com.fasterxml.jackson.databind.ObjectMapper objectMapper = new com.fasterxml.jackson.databind.ObjectMapper();
+            try {
+                // 先创建一个Map,排除tagIds、resourceId、resource_id字段(确保使用数据库自增)
+                java.util.Map<String, Object> resourceMap = new java.util.HashMap<>(requestBody);
+                resourceMap.remove("tagIds");
+                resourceMap.remove("resourceId");  // 移除 resourceId,确保使用数据库自增
+                resourceMap.remove("resource_id"); // 移除 resource_id,确保使用数据库自增
+                myResource = objectMapper.convertValue(resourceMap, MyResource.class);
+                // 显式清除 resourceId,确保使用数据库自增机制
+                myResource.setResourceId(null);
+            } catch (Exception e) {
+                System.err.println("转换资源信息失败,使用手动设置: " + e.getMessage());
+                // 如果转换失败,手动设置字段
+                if (requestBody.containsKey("name")) myResource.setName((String) requestBody.get("name"));
+                if (requestBody.containsKey("age")) myResource.setAge(requestBody.get("age") != null ? Integer.valueOf(requestBody.get("age").toString()) : null);
+                if (requestBody.containsKey("gender")) myResource.setGender(requestBody.get("gender") != null ? Integer.valueOf(requestBody.get("gender").toString()) : null);
+                if (requestBody.containsKey("constellation")) myResource.setConstellation((String) requestBody.get("constellation"));
+                if (requestBody.containsKey("height")) myResource.setHeight(requestBody.get("height") != null ? Integer.valueOf(requestBody.get("height").toString()) : null);
+                if (requestBody.containsKey("weight")) myResource.setWeight(requestBody.get("weight") != null ? Integer.valueOf(requestBody.get("weight").toString()) : null);
+                if (requestBody.containsKey("marrStatus")) myResource.setMarrStatus(requestBody.get("marrStatus") != null ? Integer.valueOf(requestBody.get("marrStatus").toString()) : null);
+                if (requestBody.containsKey("diploma")) myResource.setDiploma((String) requestBody.get("diploma"));
+                if (requestBody.containsKey("income")) myResource.setIncome((String) requestBody.get("income"));
+                if (requestBody.containsKey("address")) myResource.setAddress((String) requestBody.get("address"));
+                if (requestBody.containsKey("domicile")) myResource.setDomicile((String) requestBody.get("domicile"));
+                if (requestBody.containsKey("occupation")) myResource.setOccupation((String) requestBody.get("occupation"));
+                if (requestBody.containsKey("house")) myResource.setHouse(requestBody.get("house") != null ? Integer.valueOf(requestBody.get("house").toString()) : null);
+                if (requestBody.containsKey("car")) myResource.setCar(requestBody.get("car") != null ? Integer.valueOf(requestBody.get("car").toString()) : null);
+                if (requestBody.containsKey("phone")) myResource.setPhone((String) requestBody.get("phone"));
+                if (requestBody.containsKey("backupPhone")) myResource.setBackupPhone((String) requestBody.get("backupPhone"));
+                if (requestBody.containsKey("mateSelectionCriteria")) myResource.setMateSelectionCriteria((String) requestBody.get("mateSelectionCriteria"));
+            }
+            
+            // 提取标签ID列表
+            java.util.List<Integer> tagIds = null;
+            if (requestBody.containsKey("tagIds") && requestBody.get("tagIds") != null) {
+                Object tagIdsObj = requestBody.get("tagIds");
+                if (tagIdsObj instanceof java.util.List) {
+                    tagIds = new java.util.ArrayList<>();
+                    for (Object item : (java.util.List<?>) tagIdsObj) {
+                        if (item != null) {
+                            tagIds.add(Integer.valueOf(item.toString()));
+                        }
+                    }
+                }
+            }
+            
             // 打印接收到的数据,用于调试
             System.out.println("=== 接收到的资源数据 ===");
+            System.out.println("resourceId: " + myResource.getResourceId() + " (应该为null,使用数据库自增)");
             System.out.println("姓名: " + myResource.getName());
             System.out.println("手机号: " + myResource.getPhone());
-            System.out.println("备用手机号: " + myResource.getBackupPhone());
-            System.out.println("备用手机号类型: " + (myResource.getBackupPhone() != null ? myResource.getBackupPhone().getClass().getName() : "null"));
-            System.out.println("完整对象: " + myResource.toString());
+            System.out.println("婚姻状况(marrStatus): " + myResource.getMarrStatus() + " (0-未婚, 1-离异, 2-丧偶)");
+            System.out.println("备用手机号(backupPhone): " + myResource.getBackupPhone());
+            System.out.println("择偶标准(mateSelectionCriteria): " + myResource.getMateSelectionCriteria());
+            System.out.println("标签ID列表: " + tagIds);
             
             // 验证手机号是否为空
             if (myResource.getPhone() == null || myResource.getPhone().trim().isEmpty()) {
@@ -54,9 +113,31 @@ public class MyResourceController {
             userQueryWrapper.eq("phone", myResource.getPhone());
             User user = userMapper.selectOne(userQueryWrapper);
             
-            // 如果用户不存在,返回错误提示
-            if (user == null) {
-                return Result.error("请先在客户端注册该用户");
+            // 根据用户是否存在,设置is_user和user_id字段
+            if (user != null) {
+                // 用户存在,设置is_user=1,user_id=user.getUserId()
+                myResource.setIsUser(1);
+                myResource.setUserId(user.getUserId());
+                System.out.println("用户存在,设置is_user=1, user_id=" + user.getUserId());
+            } else {
+                // 用户不存在,设置is_user=0,user_id=null
+                myResource.setIsUser(0);
+                myResource.setUserId(null);
+                System.out.println("用户不存在,设置is_user=0, user_id=null");
+            }
+            
+            // 确保关键字段被明确设置(即使为null也要设置,确保MyBatis-Plus能够识别)
+            // 如果marrStatus为null,确保它被明确设置为null(而不是未设置)
+            if (myResource.getMarrStatus() == null) {
+                System.out.println("警告:marrStatus为null,但字段会被保存为null");
+            }
+            // 如果backupPhone为null,确保它被明确设置为null
+            if (myResource.getBackupPhone() == null) {
+                System.out.println("信息:backupPhone为null,字段会被保存为null");
+            }
+            // 如果mateSelectionCriteria为null,确保它被明确设置为null
+            if (myResource.getMateSelectionCriteria() == null) {
+                System.out.println("信息:mateSelectionCriteria为null,字段会被保存为null");
             }
             
             // 如果matchmakerId为空,使用当前登录用户的userId作为matchmakerId
@@ -68,8 +149,8 @@ public class MyResourceController {
                 }
             }
             
-            // 保存资源信息
-            boolean success = myResourceService.addResource(myResource);
+            // 保存资源信息和标签关联
+            boolean success = myResourceService.addResource(myResource, tagIds);
             if (success) {
                 return Result.success("资源信息添加成功", myResource);
             } else {
@@ -132,6 +213,39 @@ public class MyResourceController {
         }
     }
     
+    /**
+     * 根据资源ID获取客户详情(包含择偶要求)
+     * 
+     * @param resourceId 资源ID
+     * @return 客户详情
+     */
+    @GetMapping("/client-detail/{resourceId}")
+    public Result<ClientDetailVO> getClientDetail(@PathVariable Integer resourceId) {
+        try {
+            System.out.println("=== 获取客户详情 ===");
+            System.out.println("接收到的resourceId: " + resourceId + " (类型: " + resourceId.getClass().getName() + ")");
+            
+            ClientDetailVO clientDetail = myResourceService.getClientDetail(resourceId);
+            if (clientDetail == null) {
+                System.out.println("资源不存在,resourceId: " + resourceId);
+                return Result.error("资源不存在");
+            }
+            
+            System.out.println("查询成功,返回的resourceId: " + clientDetail.getResourceId());
+            System.out.println("姓名: " + clientDetail.getName());
+            System.out.println("头像URL: " + clientDetail.getAvatarUrl());
+            System.out.println("备用手机号: " + clientDetail.getBackupPhone());
+            System.out.println("择偶要求 - 年龄范围: " + clientDetail.getMinAge() + "-" + clientDetail.getMaxAge());
+            System.out.println("择偶要求 - 身高: " + clientDetail.getMinHeight() + "cm以上");
+            System.out.println("择偶要求 - 其他: " + clientDetail.getOtherRequirements());
+            
+            return Result.success(clientDetail);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("查询客户详情失败:" + e.getMessage());
+        }
+    }
+    
     /**
      * 更新资源信息
      * 
@@ -179,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());
+        }
+    }
 }
 

+ 38 - 0
service/homePage/src/main/java/com/zhentao/controller/TagController.java

@@ -0,0 +1,38 @@
+package com.zhentao.controller;
+
+import com.zhentao.common.Result;
+import com.zhentao.entity.Tag;
+import com.zhentao.service.TagService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ * 标签Controller
+ */
+@RestController
+@RequestMapping("/api/tag")
+public class TagController {
+    
+    @Autowired
+    private TagService tagService;
+    
+    /**
+     * 获取所有启用的标签
+     * @return 标签列表
+     */
+    @GetMapping("/list")
+    public Result<List<Tag>> getAllTags() {
+        try {
+            List<Tag> tags = tagService.getAllActiveTags();
+            return Result.success(tags);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("获取标签列表失败:" + e.getMessage());
+        }
+    }
+}
+

+ 24 - 0
service/homePage/src/main/java/com/zhentao/dto/MyResourceDTO.java

@@ -0,0 +1,24 @@
+package com.zhentao.dto;
+
+import com.zhentao.entity.MyResource;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 资源信息DTO(包含标签ID列表)
+ */
+@Data
+public class MyResourceDTO {
+    
+    /**
+     * 资源信息
+     */
+    private MyResource resource;
+    
+    /**
+     * 标签ID列表
+     */
+    private List<Integer> tagIds;
+}
+

+ 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;
+}
+

+ 35 - 3
service/homePage/src/main/java/com/zhentao/entity/MyResource.java

@@ -4,6 +4,7 @@ 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 com.fasterxml.jackson.annotation.JsonProperty;
 import java.io.Serializable;
 import java.util.Date;
 import lombok.Data;
@@ -19,7 +20,7 @@ public class MyResource implements Serializable {
     /**
      * 资源id
      */
-    @TableId
+    @TableId(value = "resource_id", type = IdType.AUTO)
     private Integer resourceId;
 
     /**
@@ -58,8 +59,10 @@ public class MyResource implements Serializable {
     private Integer weight;
 
     /**
-     * 婚姻状况(未婚,离异,丧偶)
+     * 婚姻状况(0-未婚,1-离异,2-丧偶)
      */
+    @TableField(value = "marr_status", insertStrategy = com.baomidou.mybatisplus.annotation.FieldStrategy.IGNORED)
+    @JsonProperty("marrStatus")  // 明确指定JSON字段名为驼峰命名,避免SNAKE_CASE转换
     private Integer marrStatus;
 
     /**
@@ -100,7 +103,8 @@ public class MyResource implements Serializable {
     /**
      * 备用手机号
      */
-    @TableField("backup_phone")
+    @TableField(value = "backup_phone", insertStrategy = com.baomidou.mybatisplus.annotation.FieldStrategy.IGNORED)
+    @JsonProperty("backupPhone")  // 明确指定JSON字段名为驼峰命名,避免SNAKE_CASE转换
     private String backupPhone;
 
     /**
@@ -108,6 +112,25 @@ public class MyResource implements Serializable {
      */
     private Integer car;
 
+    /**
+     * 择偶标准
+     */
+    @TableField(value = "mate_selection_criteria", insertStrategy = com.baomidou.mybatisplus.annotation.FieldStrategy.IGNORED)
+    @JsonProperty("mateSelectionCriteria")  // 明确指定JSON字段名为驼峰命名,避免SNAKE_CASE转换
+    private String mateSelectionCriteria;
+
+    /**
+     * 是否为系统用户(0:否,1:是)
+     */
+    @TableField("is_user")
+    private Integer isUser;
+
+    /**
+     * 用户ID(关联users表的user_id)
+     */
+    @TableField("user_id")
+    private Integer userId;
+
     /**
      * 
      */
@@ -151,6 +174,9 @@ public class MyResource implements Serializable {
             && (this.getPhone() == null ? other.getPhone() == null : this.getPhone().equals(other.getPhone()))
             && (this.getBackupPhone() == null ? other.getBackupPhone() == null : this.getBackupPhone().equals(other.getBackupPhone()))
             && (this.getCar() == null ? other.getCar() == null : this.getCar().equals(other.getCar()))
+            && (this.getMateSelectionCriteria() == null ? other.getMateSelectionCriteria() == null : this.getMateSelectionCriteria().equals(other.getMateSelectionCriteria()))
+            && (this.getIsUser() == null ? other.getIsUser() == null : this.getIsUser().equals(other.getIsUser()))
+            && (this.getUserId() == null ? other.getUserId() == null : this.getUserId().equals(other.getUserId()))
             && (this.getCreateTime() == null ? other.getCreateTime() == null : this.getCreateTime().equals(other.getCreateTime()))
             && (this.getUpdateTime() == null ? other.getUpdateTime() == null : this.getUpdateTime().equals(other.getUpdateTime()));
     }
@@ -177,6 +203,9 @@ public class MyResource implements Serializable {
         result = prime * result + ((getPhone() == null) ? 0 : getPhone().hashCode());
         result = prime * result + ((getBackupPhone() == null) ? 0 : getBackupPhone().hashCode());
         result = prime * result + ((getCar() == null) ? 0 : getCar().hashCode());
+        result = prime * result + ((getMateSelectionCriteria() == null) ? 0 : getMateSelectionCriteria().hashCode());
+        result = prime * result + ((getIsUser() == null) ? 0 : getIsUser().hashCode());
+        result = prime * result + ((getUserId() == null) ? 0 : getUserId().hashCode());
         result = prime * result + ((getCreateTime() == null) ? 0 : getCreateTime().hashCode());
         result = prime * result + ((getUpdateTime() == null) ? 0 : getUpdateTime().hashCode());
         return result;
@@ -206,6 +235,9 @@ public class MyResource implements Serializable {
         sb.append(", phone=").append(phone);
         sb.append(", backupPhone=").append(backupPhone);
         sb.append(", car=").append(car);
+        sb.append(", mateSelectionCriteria=").append(mateSelectionCriteria);
+        sb.append(", isUser=").append(isUser);
+        sb.append(", userId=").append(userId);
         sb.append(", createTime=").append(createTime);
         sb.append(", updateTime=").append(updateTime);
         sb.append(", serialVersionUID=").append(serialVersionUID);

+ 41 - 0
service/homePage/src/main/java/com/zhentao/entity/MyResourceTag.java

@@ -0,0 +1,41 @@
+package com.zhentao.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 资源标签关联表
+ */
+@Data
+@TableName("my_resource_tag")
+public class MyResourceTag implements Serializable {
+    
+    private static final long serialVersionUID = 1L;
+    
+    /**
+     * 关联ID
+     */
+    @TableId(type = IdType.AUTO)
+    private Integer id;
+    
+    /**
+     * 资源ID(关联my_resource表的resource_id)
+     */
+    private Integer resourceId;
+    
+    /**
+     * 标签ID(关联tag表的tag_id)
+     */
+    private Integer tagId;
+    
+    /**
+     * 创建时间
+     */
+    private Date createTime;
+}
+

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

@@ -0,0 +1,38 @@
+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;
+
+/**
+ * 标签实体类
+ */
+@Data
+@TableName("tag")
+public class Tag implements Serializable {
+    
+    private static final long serialVersionUID = 1L;
+    
+    /**
+     * 标签ID
+     */
+    @TableId(value = "tag_id", type = IdType.AUTO)
+    private Integer id;
+    
+    /**
+     * 标签名称
+     */
+    @TableField("tag_name")
+    private String name;
+    
+    /**
+     * 标签状态(0-禁用,1-启用)
+     */
+    @TableField("status")
+    private Integer status;
+}
+

+ 5 - 0
service/homePage/src/main/java/com/zhentao/entity/User.java

@@ -46,5 +46,10 @@ public class User implements Serializable {
      * 性别 (0-未知 1-男 2-女)
      */
     private Integer gender;
+    
+    /**
+     * 头像地址
+     */
+    private String avatarUrl;
 }
 

+ 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
+    );
+}
+

+ 8 - 0
service/homePage/src/main/java/com/zhentao/mapper/MatchmakerMapper.java

@@ -62,5 +62,13 @@ public interface MatchmakerMapper extends BaseMapper<Matchmaker> {
      * @return 红娘排行榜列表
      */
     List<MatchmakerVO> selectRankingList(@Param("limit") Integer limit);
+    
+    /**
+     * 批量查询红娘信息
+     * 
+     * @param matchmakerIds 红娘ID列表
+     * @return 红娘VO列表
+     */
+    List<MatchmakerVO> selectBatchMatchmakers(@Param("matchmakerIds") List<Integer> matchmakerIds);
 }
 

+ 25 - 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
@@ -17,6 +20,14 @@ import org.apache.ibatis.annotations.Param;
 @Mapper
 public interface MyResourceMapper extends BaseMapper<MyResource> {
     
+    /**
+     * 自定义插入方法,确保所有字段(包括null值)都被插入
+     * 
+     * @param myResource 资源对象
+     * @return 插入结果
+     */
+    int insertWithNulls(MyResource myResource);
+    
     /**
      * 分页查询资源列表(关联users表获取头像)
      * 
@@ -30,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);
 }
 
 

+ 30 - 0
service/homePage/src/main/java/com/zhentao/mapper/MyResourceTagMapper.java

@@ -0,0 +1,30 @@
+package com.zhentao.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zhentao.entity.MyResourceTag;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 资源标签关联Mapper接口
+ */
+@Mapper
+public interface MyResourceTagMapper extends BaseMapper<MyResourceTag> {
+    
+    /**
+     * 根据资源ID删除所有标签关联
+     * @param resourceId 资源ID
+     * @return 删除数量
+     */
+    int deleteByResourceId(@Param("resourceId") Integer resourceId);
+    
+    /**
+     * 根据资源ID查询所有标签ID
+     * @param resourceId 资源ID
+     * @return 标签ID列表
+     */
+    List<Integer> selectTagIdsByResourceId(@Param("resourceId") Integer resourceId);
+}
+

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

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

+ 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
+    );
+}
+

+ 15 - 1
service/homePage/src/main/java/com/zhentao/service/ActivityService.java

@@ -1,5 +1,6 @@
 package com.zhentao.service;
 
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.zhentao.entity.Activity;
 import java.util.List;
 
@@ -13,10 +14,23 @@ public interface ActivityService {
      * 
      * @param type 活动类型(null表示全部)
      * @param status 活动状态(null表示全部)
+     * @param keyword 搜索关键词(可选,用于活动名称模糊查询)
      * @param limit 限制数量(可选)
      * @return 活动列表
      */
-    List<Activity> getActivityList(Integer type, Integer status, Integer limit);
+    List<Activity> getActivityList(Integer type, Integer status, String keyword, Integer limit);
+    
+    /**
+     * 分页获取活动列表
+     * 
+     * @param type 活动类型(null表示全部)
+     * @param status 活动状态(null表示全部)
+     * @param keyword 搜索关键词(可选,用于活动名称模糊查询)
+     * @param pageNum 页码
+     * @param pageSize 每页大小
+     * @return 分页结果
+     */
+    Page<Activity> getActivityPage(Integer type, Integer status, String keyword, Integer pageNum, Integer pageSize);
     
     /**
      * 获取活动详情

+ 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);
+}
+

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

@@ -56,5 +56,21 @@ public interface MatchmakerService extends IService<Matchmaker> {
      * @return 红娘排行榜列表
      */
     java.util.List<MatchmakerVO> getRankingList(Integer limit);
+    
+    /**
+     * 根据用户ID查询红娘信息
+     * 
+     * @param userId 用户ID
+     * @return 红娘实体
+     */
+    Matchmaker getMatchmakerByUserId(Integer userId);
+    
+    /**
+     * 批量查询红娘信息
+     * 
+     * @param matchmakerIds 红娘ID列表
+     * @return 红娘VO列表
+     */
+    java.util.List<MatchmakerVO> batchGetMatchmakers(java.util.List<Integer> matchmakerIds);
 }
 

+ 26 - 1
service/homePage/src/main/java/com/zhentao/service/MyResourceService.java

@@ -18,9 +18,10 @@ public interface MyResourceService extends IService<MyResource> {
     /**
      * 添加资源信息
      * @param myResource 资源信息
+     * @param tagIds 标签ID列表(可选)
      * @return 是否成功
      */
-    boolean addResource(MyResource myResource);
+    boolean addResource(MyResource myResource, List<Integer> tagIds);
     
     /**
      * 分页查询资源列表
@@ -48,4 +49,28 @@ public interface MyResourceService extends IService<MyResource> {
      * @return 资源列表
      */
     List<MyResource> getResourceByPhone(String phone);
+    
+    /**
+     * 根据资源ID获取客户详情(包含择偶要求)
+     * @param resourceId 资源ID
+     * @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);
 }

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

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

+ 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);
+}
+

+ 39 - 1
service/homePage/src/main/java/com/zhentao/service/impl/ActivityServiceImpl.java

@@ -2,6 +2,7 @@ package com.zhentao.service.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.zhentao.entity.Activity;
 import com.zhentao.entity.ActivityRegistration;
 import com.zhentao.mapper.ActivityMapper;
@@ -30,7 +31,7 @@ public class ActivityServiceImpl implements ActivityService {
      * 获取活动列表
      */
     @Override
-    public List<Activity> getActivityList(Integer type, Integer status, Integer limit) {
+    public List<Activity> getActivityList(Integer type, Integer status, String keyword, Integer limit) {
         QueryWrapper<Activity> queryWrapper = new QueryWrapper<>();
         
         // 只查询未删除的活动
@@ -46,6 +47,11 @@ public class ActivityServiceImpl implements ActivityService {
             queryWrapper.eq("status", status);
         }
         
+        // 关键词模糊查询(活动名称)
+        if (keyword != null && !keyword.trim().isEmpty()) {
+            queryWrapper.like("name", keyword.trim());
+        }
+        
         // 按开始时间降序排序
         queryWrapper.orderByDesc("start_time");
         
@@ -57,6 +63,38 @@ public class ActivityServiceImpl implements ActivityService {
         return activityMapper.selectList(queryWrapper);
     }
     
+    /**
+     * 分页获取活动列表
+     */
+    @Override
+    public Page<Activity> getActivityPage(Integer type, Integer status, String keyword, Integer pageNum, Integer pageSize) {
+        Page<Activity> page = new Page<>(pageNum, pageSize);
+        QueryWrapper<Activity> queryWrapper = new QueryWrapper<>();
+        
+        // 只查询未删除的活动
+        queryWrapper.eq("is_deleted", 0);
+        
+        // 按类型筛选
+        if (type != null) {
+            queryWrapper.eq("type", type);
+        }
+        
+        // 按状态筛选
+        if (status != null) {
+            queryWrapper.eq("status", status);
+        }
+        
+        // 关键词模糊查询(活动名称)
+        if (keyword != null && !keyword.trim().isEmpty()) {
+            queryWrapper.like("name", keyword.trim());
+        }
+        
+        // 按开始时间降序排序
+        queryWrapper.orderByDesc("start_time");
+        
+        return activityMapper.selectPage(page, queryWrapper);
+    }
+    
     /**
      * 获取活动详情
      */

+ 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);
+    }
+}
+

+ 26 - 1
service/homePage/src/main/java/com/zhentao/service/impl/MatchmakerServiceImpl.java

@@ -15,6 +15,7 @@ import org.springframework.stereotype.Service;
 
 import java.time.LocalDate;
 import java.time.Period;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
@@ -38,12 +39,13 @@ public class MatchmakerServiceImpl extends ServiceImpl<MatchmakerMapper, Matchma
     @Override
     @SuppressWarnings("unchecked")
     public Page<MatchmakerVO> getMatchmakerPage(MatchmakerQueryDTO queryDTO) {
-        // 1. 构建缓存Key
+        // 1. 构建缓存Key(包含keyword参数)
         String cacheKey = RedisKeyConstants.buildMatchmakerListKey(
                 queryDTO.getMatchmakerType(),
                 queryDTO.getLevel(),
                 queryDTO.getProvinceId(),
                 queryDTO.getCityId(),
+                queryDTO.getKeyword(),
                 queryDTO.getPageNum(),
                 queryDTO.getPageSize()
         );
@@ -429,4 +431,27 @@ public class MatchmakerServiceImpl extends ServiceImpl<MatchmakerMapper, Matchma
         
         return rankingList;
     }
+    
+    @Override
+    public Matchmaker getMatchmakerByUserId(Integer userId) {
+        // 根据 user_id 查询红娘信息
+        return this.lambdaQuery()
+                .eq(Matchmaker::getUserId, userId.longValue())
+                .one();
+    }
+    
+    @Override
+    public List<MatchmakerVO> batchGetMatchmakers(List<Integer> matchmakerIds) {
+        if (matchmakerIds == null || matchmakerIds.isEmpty()) {
+            return new ArrayList<>();
+        }
+        
+        // 批量查询红娘详情
+        List<MatchmakerVO> matchmakers = matchmakerMapper.selectBatchMatchmakers(matchmakerIds);
+        
+        // 处理数据(计算年龄、处理头像等)
+        matchmakers.forEach(this::processMatchmakerVO);
+        
+        return matchmakers;
+    }
 }

+ 625 - 7
service/homePage/src/main/java/com/zhentao/service/impl/MyResourceServiceImpl.java

@@ -4,15 +4,26 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.zhentao.entity.MyResource;
+import com.zhentao.entity.MyResourceTag;
+import com.zhentao.entity.Tag;
 import com.zhentao.service.MyResourceService;
+import com.zhentao.service.TagService;
 import com.zhentao.mapper.MyResourceMapper;
+import com.zhentao.mapper.MyResourceTagMapper;
 import com.zhentao.vo.MyResourceVO;
+import com.zhentao.vo.ClientDetailVO;
+import com.zhentao.mapper.UserMapper;
+import com.zhentao.entity.User;
+import com.zhentao.pojo.PartnerRequirement;
+import com.zhentao.service.PartnerRequirementService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.StringUtils;
 
 import java.util.Date;
 import java.util.List;
+import java.util.ArrayList;
 
 /**
 * @author Administrator
@@ -26,9 +37,25 @@ public class MyResourceServiceImpl extends ServiceImpl<MyResourceMapper, MyResou
     
     @Autowired
     private MyResourceMapper myResourceMapper;
+    
+    @Autowired
+    private UserMapper userMapper;
+    
+    @Autowired
+    private MyResourceTagMapper myResourceTagMapper;
+    
+    @Autowired
+    private TagService tagService;
+    
+    @Autowired(required = false)
+    private PartnerRequirementService partnerRequirementService;
+    
+    @Autowired(required = false)
+    private com.zhentao.service.UserFollowUpService userFollowUpService;
 
     @Override
-    public boolean addResource(MyResource myResource) {
+    @Transactional(rollbackFor = Exception.class)
+    public boolean addResource(MyResource myResource, List<Integer> tagIds) {
         // 设置创建时间和更新时间
         Date now = new Date();
         myResource.setCreateTime(now);
@@ -36,8 +63,9 @@ public class MyResourceServiceImpl extends ServiceImpl<MyResourceMapper, MyResou
         
         // 打印保存前的数据,用于调试
         System.out.println("=== 保存资源数据前 ===");
-        System.out.println("备用手机号: " + myResource.getBackupPhone());
-        System.out.println("备用手机号是否为空: " + (myResource.getBackupPhone() == null || myResource.getBackupPhone().isEmpty()));
+        System.out.println("婚姻状况(marrStatus): " + myResource.getMarrStatus() + " (0-未婚, 1-离异, 2-丧偶)");
+        System.out.println("备用手机号(backupPhone): " + myResource.getBackupPhone());
+        System.out.println("择偶标准(mateSelectionCriteria): " + myResource.getMateSelectionCriteria());
         
         // 处理备用手机号:如果为空字符串,设置为null;如果有值,去除首尾空格
         if (myResource.getBackupPhone() != null) {
@@ -49,14 +77,59 @@ public class MyResourceServiceImpl extends ServiceImpl<MyResourceMapper, MyResou
             }
         }
         
-        // 使用insert方法确保所有字段都被插入(包括null值)
-        boolean result = this.save(myResource);
+        // 处理择偶标准:如果为空字符串,设置为null;如果有值,去除首尾空格
+        if (myResource.getMateSelectionCriteria() != null) {
+            String trimmed = myResource.getMateSelectionCriteria().trim();
+            if (trimmed.isEmpty()) {
+                myResource.setMateSelectionCriteria(null);
+            } else {
+                myResource.setMateSelectionCriteria(trimmed);
+            }
+        }
+        
+        // 使用自定义insert方法确保所有字段都被插入(包括null值)
+        // 打印插入前的完整对象信息
+        System.out.println("=== 准备插入数据库的对象 ===");
+        System.out.println("resourceId: " + myResource.getResourceId() + " (应该为null,使用数据库自增)");
+        System.out.println("marrStatus: " + myResource.getMarrStatus());
+        System.out.println("backupPhone: " + myResource.getBackupPhone());
+        System.out.println("mateSelectionCriteria: " + myResource.getMateSelectionCriteria());
+        
+        // 使用自定义的insertWithNulls方法,它已经正确配置了useGeneratedKeys="true" keyProperty="resourceId"
+        // 这样可以确保自增主键被正确处理,同时null值字段也会被插入
+        int insertResult = myResourceMapper.insertWithNulls(myResource);
+        boolean result = insertResult > 0;
+        
+        // 打印插入结果
+        System.out.println("插入结果: " + insertResult + " (影响行数)");
         
         // 打印保存后的数据
         System.out.println("=== 保存资源数据后 ===");
         System.out.println("保存结果: " + result);
         System.out.println("资源ID: " + myResource.getResourceId());
-        System.out.println("备用手机号: " + myResource.getBackupPhone());
+        System.out.println("婚姻状况(marrStatus): " + myResource.getMarrStatus() + " (0-未婚, 1-离异, 2-丧偶)");
+        System.out.println("备用手机号(backupPhone): " + myResource.getBackupPhone());
+        System.out.println("择偶标准(mateSelectionCriteria): " + myResource.getMateSelectionCriteria());
+        
+        // 保存标签关联
+        if (result && myResource.getResourceId() != null && tagIds != null && !tagIds.isEmpty()) {
+            System.out.println("=== 开始保存标签关联 ===");
+            System.out.println("资源ID: " + myResource.getResourceId());
+            System.out.println("标签ID列表: " + tagIds);
+            
+            // 使用上面定义的now变量
+            for (Integer tagId : tagIds) {
+                if (tagId != null) {
+                    MyResourceTag resourceTag = new MyResourceTag();
+                    resourceTag.setResourceId(myResource.getResourceId());
+                    resourceTag.setTagId(tagId);
+                    resourceTag.setCreateTime(now);
+                    int tagInsertResult = myResourceTagMapper.insert(resourceTag);
+                    System.out.println("保存标签关联 - tagId: " + tagId + ", 结果: " + tagInsertResult);
+                }
+            }
+            System.out.println("=== 标签关联保存完成 ===");
+        }
         
         return result;
     }
@@ -98,19 +171,564 @@ public class MyResourceServiceImpl extends ServiceImpl<MyResourceMapper, MyResou
         Page<MyResourceVO> page = new Page<>(pageNum, pageSize);
         Page<MyResourceVO> result = myResourceMapper.selectResourceListWithAvatar(page, matchmakerId, keyword);
         
-        // 打印调试信息,检查头像URL
+        // 为每个资源查询标签
         if (result != null && result.getRecords() != null) {
             System.out.println("=== 查询到的资源列表(包含头像) ===");
             for (MyResourceVO vo : result.getRecords()) {
                 System.out.println("资源ID: " + vo.getResourceId() + 
+                                 " (类型: " + (vo.getResourceId() != null ? vo.getResourceId().getClass().getName() : "null") + ")" +
                                  ", 姓名: " + vo.getName() + 
                                  ", 手机号: " + vo.getPhone() + 
                                  ", 头像URL: " + vo.getAvatarUrl());
+                
+                // 查询该资源的标签
+                if (vo.getResourceId() != null) {
+                    List<Integer> tagIds = myResourceTagMapper.selectTagIdsByResourceId(vo.getResourceId());
+                    if (tagIds != null && !tagIds.isEmpty()) {
+                        // 根据标签ID查询标签名称
+                        List<String> tagNames = new java.util.ArrayList<>();
+                        for (Integer tagId : tagIds) {
+                            com.zhentao.entity.Tag tag = tagService.getById(tagId);
+                            if (tag != null && tag.getName() != null) {
+                                tagNames.add(tag.getName());
+                            }
+                        }
+                        vo.setTags(tagNames);
+                        System.out.println("  标签: " + tagNames);
+                    }
+                }
             }
         }
         
         return result;
     }
+    
+    @Override
+    public ClientDetailVO getClientDetail(Integer resourceId) {
+        System.out.println("=== MyResourceService.getClientDetail ===");
+        System.out.println("查询resourceId: " + resourceId);
+        
+        // 根据resourceId查询my_resource表
+        MyResource myResource = this.getById(resourceId);
+        if (myResource == null) {
+            System.out.println("未找到资源,resourceId: " + resourceId);
+            return null;
+        }
+        
+        System.out.println("找到资源:");
+        System.out.println("  resourceId: " + myResource.getResourceId());
+        System.out.println("  name: " + myResource.getName());
+        System.out.println("  userId: " + myResource.getUserId());
+        System.out.println("  backupPhone: " + myResource.getBackupPhone());
+        System.out.println("  mateSelectionCriteria: " + myResource.getMateSelectionCriteria());
+        
+        // 创建ClientDetailVO并填充基本信息
+        ClientDetailVO clientDetail = new ClientDetailVO();
+        clientDetail.setResourceId(myResource.getResourceId());
+        clientDetail.setName(myResource.getName());
+        clientDetail.setAge(myResource.getAge());
+        clientDetail.setGender(myResource.getGender());
+        clientDetail.setConstellation(myResource.getConstellation());
+        clientDetail.setHeight(myResource.getHeight());
+        clientDetail.setWeight(myResource.getWeight());
+        clientDetail.setMarrStatus(myResource.getMarrStatus());
+        clientDetail.setDiploma(myResource.getDiploma());
+        clientDetail.setIncome(myResource.getIncome());
+        clientDetail.setAddress(myResource.getAddress());
+        clientDetail.setDomicile(myResource.getDomicile());
+        clientDetail.setOccupation(myResource.getOccupation());
+        clientDetail.setHouse(myResource.getHouse());
+        clientDetail.setPhone(myResource.getPhone());
+        clientDetail.setBackupPhone(myResource.getBackupPhone());
+        clientDetail.setCar(myResource.getCar());
+        // 择偶要求从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());
+            if (tagIds != null && !tagIds.isEmpty()) {
+                List<String> tagNames = new ArrayList<>();
+                for (Integer tagId : tagIds) {
+                    Tag tag = tagService.getById(tagId);
+                    if (tag != null && tag.getName() != null) {
+                        tagNames.add(tag.getName());
+                    }
+                }
+                clientDetail.setTags(tagNames);
+                System.out.println("  客户标签: " + tagNames);
+            }
+        }
+        
+        // 如果user_id不为空,查询用户头像和择偶要求详情
+        if (myResource.getUserId() != null) {
+            // 查询用户头像
+            User user = userMapper.selectById(myResource.getUserId());
+            if (user != null && user.getAvatarUrl() != null) {
+                clientDetail.setAvatarUrl(user.getAvatarUrl());
+                System.out.println("  查询到用户头像: " + user.getAvatarUrl());
+            } else {
+                System.out.println("  未找到用户或用户头像为空");
+            }
+            
+            // 根据user_id查询partner_requirement表获取择偶要求
+            if (partnerRequirementService == null) {
+                System.err.println("  ⚠️ PartnerRequirementService未注入,跳过择偶要求查询");
+                // 即使服务未注入,也不影响返回其他数据
+            } else {
+                try {
+                    System.out.println("  开始查询择偶要求,userId: " + myResource.getUserId());
+                    // 设置查询超时,避免长时间等待
+                    PartnerRequirement requirement = partnerRequirementService.getByUserId(myResource.getUserId().longValue());
+                    if (requirement != null) {
+                        System.out.println("  ✅ 查询到择偶要求:");
+                        System.out.println("    minAge: " + requirement.getMinAge());
+                        System.out.println("    maxAge: " + requirement.getMaxAge());
+                        System.out.println("    minHeight: " + requirement.getMinHeight());
+                        System.out.println("    maxHeight: " + requirement.getMaxHeight());
+                        System.out.println("    educationLevel: " + requirement.getEducationLevel());
+                        System.out.println("    salaryRange: " + requirement.getSalaryRange());
+                        System.out.println("    house: " + requirement.getHouse());
+                        System.out.println("    car: " + requirement.getCar());
+                        System.out.println("    maritalStatus: " + requirement.getMaritalStatus());
+                        System.out.println("    preferredCity: " + requirement.getPreferredCity());
+                        System.out.println("    otherRequirements: " + requirement.getOtherRequirements());
+                        
+                        clientDetail.setMinAge(requirement.getMinAge());
+                        clientDetail.setMaxAge(requirement.getMaxAge());
+                        clientDetail.setMinHeight(requirement.getMinHeight());
+                        clientDetail.setMaxHeight(requirement.getMaxHeight());
+                        clientDetail.setEducationLevel(requirement.getEducationLevel());
+                        clientDetail.setSalaryRange(requirement.getSalaryRange());
+                        clientDetail.setHouseRequirement(requirement.getHouse());
+                        clientDetail.setCarRequirement(requirement.getCar());
+                        clientDetail.setMaritalStatusRequirement(requirement.getMaritalStatus());
+                        clientDetail.setPreferredCity(requirement.getPreferredCity());
+                        clientDetail.setOtherRequirements(requirement.getOtherRequirements());
+                    } else {
+                        System.out.println("  ⚠️ 未找到择偶要求记录,userId: " + myResource.getUserId());
+                    }
+                } catch (Exception e) {
+                    System.err.println("  ❌ 查询择偶要求时发生异常: " + e.getMessage());
+                    e.printStackTrace();
+                    // 即使查询择偶要求失败,也不影响返回其他数据
+                }
+            }
+        } else {
+            System.out.println("  userId为空,无法查询用户头像和择偶要求");
+        }
+        
+        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() : "暂无要求";
+    }
 }
 
 

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

@@ -0,0 +1,26 @@
+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.Tag;
+import com.zhentao.mapper.TagMapper;
+import com.zhentao.service.TagService;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 标签Service实现类
+ */
+@Service
+public class TagServiceImpl extends ServiceImpl<TagMapper, Tag> implements TagService {
+    
+    @Override
+    public List<Tag> getAllActiveTags() {
+        QueryWrapper<Tag> queryWrapper = new QueryWrapper<>();
+        queryWrapper.eq("status", 1);
+        queryWrapper.orderByAsc("id");
+        return this.list(queryWrapper);
+    }
+}
+

+ 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);
+    }
+}
+

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

@@ -0,0 +1,63 @@
+package com.zhentao.vo;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import java.io.Serializable;
+
+/**
+ * 客户详情VO(包含择偶要求)
+ */
+@Data
+public class ClientDetailVO implements Serializable {
+    
+    private static final long serialVersionUID = 1L;
+    
+    // 基本信息(从my_resource表)
+    @JsonProperty("resourceId")
+    private Integer resourceId;
+    private String name;
+    private Integer age;
+    private Integer gender;
+    private String constellation;
+    private Integer height;
+    private Integer weight;
+    private Integer marrStatus; // 婚姻状况
+    private String diploma; // 学历
+    private String income; // 收入
+    private String address; // 住址
+    private String domicile; // 户籍所在地
+    private String occupation; // 职业
+    private Integer house; // 购房情况
+    private String phone; // 手机号
+    @JsonProperty("backupPhone")
+    private String backupPhone; // 备用手机号
+    private Integer car; // 购车情况
+    @JsonProperty("avatarUrl")
+    private String avatarUrl; // 用户头像URL
+    
+    // 择偶要求(从my_resource表的mate_selection_criteria字段)
+    @JsonProperty("mateSelectionCriteria")
+    private String mateSelectionCriteria; // 择偶标准
+    
+    // 择偶要求详情(从partner_requirement表,根据user_id查询,用于详情页)
+    private Integer minAge; // 年龄范围-最小
+    private Integer maxAge; // 年龄范围-最大
+    private Integer minHeight; // 身高范围-最小
+    private Integer maxHeight; // 身高范围-最大
+    private Integer educationLevel; // 学历要求
+    private Integer salaryRange; // 薪资要求
+    private Integer houseRequirement; // 房产要求
+    private Integer carRequirement; // 车辆要求
+    private Integer maritalStatusRequirement; // 婚姻状况要求
+    private String preferredCity; // 期望城市
+    private String otherRequirements; // 其他要求
+    
+    // 客户标签(从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;
+}
+

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

@@ -103,6 +103,11 @@ public class MyResourceVO implements Serializable {
      */
     private Integer car;
 
+    /**
+     * 择偶标准
+     */
+    private String mateSelectionCriteria;
+
     /**
      * 创建时间
      */
@@ -117,5 +122,30 @@ public class MyResourceVO implements Serializable {
      * 用户头像URL(从users表查询)
      */
     private String avatarUrl;
+    
+    /**
+     * 是否为系统用户(0:否,1:是)
+     */
+    private Integer isUser;
+    
+    /**
+     * 标签列表(从my_resource_tag关联表查询)
+     */
+    private java.util.List<String> tags;
+    
+    /**
+     * 该资源用户是否已匹配对象(0-未匹配,1-已匹配)
+     */
+    private Integer isMatch;
+    
+    /**
+     * 红娘是否邀约该用户(0-未邀约,1-已邀约)
+     */
+    private Integer isInvitation;
+    
+    /**
+     * 邀约用户的时间
+     */
+    private Date invitationTime;
 }
 

+ 1 - 0
service/homePage/src/main/resources/application.yml

@@ -61,6 +61,7 @@ mybatis-plus:
     log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
   global-config:
     db-config:
+      id-type: auto  # 全局主键类型:AUTO(数据库自增)
       logic-delete-field: isDeleted
       logic-delete-value: 1
       logic-not-delete-value: 0

+ 33 - 0
service/homePage/src/main/resources/mapper/MatchmakerMapper.xml

@@ -186,5 +186,38 @@
         ORDER BY m.success_couples DESC, m.level DESC
         LIMIT #{limit}
     </select>
+    
+    <!-- 批量查询红娘信息 -->
+    <select id="selectBatchMatchmakers" resultMap="MatchmakerVOMap">
+        SELECT 
+            m.matchmaker_id,
+            m.real_name,
+            m.phone,
+            m.email,
+            m.gender,
+            m.birth_date,
+            m.avatar_url,
+            m.matchmaker_type,
+            m.level,
+            m.success_couples,
+            m.address_detail,
+            m.profile,
+            m.status,
+            m.created_at,
+            m.province_id,
+            p.name AS province_name,
+            m.city_id,
+            c.name AS city_name,
+            m.area_id,
+            a.name AS area_name
+        FROM matchmakers m
+        LEFT JOIN province p ON m.province_id = p.id
+        LEFT JOIN city c ON m.city_id = c.id
+        LEFT JOIN area a ON m.area_id = a.id
+        WHERE m.matchmaker_id IN
+        <foreach collection="matchmakerIds" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </select>
 </mapper>
 

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

@@ -23,6 +23,9 @@
             <result property="phone" column="phone" jdbcType="VARCHAR"/>
             <result property="backupPhone" column="backup_phone" jdbcType="VARCHAR"/>
             <result property="car" column="car" jdbcType="INTEGER"/>
+            <result property="mateSelectionCriteria" column="mate_selection_criteria" jdbcType="VARCHAR"/>
+            <result property="isUser" column="is_user" jdbcType="INTEGER"/>
+            <result property="userId" column="user_id" jdbcType="INTEGER"/>
             <result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
             <result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/>
     </resultMap>
@@ -34,9 +37,28 @@
         diploma,income,address,
         domicile,occupation,house,
         phone,backup_phone,car,
+        mate_selection_criteria,
+        is_user,user_id,
         create_time,update_time
     </sql>
 
+    <!-- 自定义插入方法,确保所有字段(包括null值)都被插入 -->
+    <insert id="insertWithNulls" parameterType="com.zhentao.entity.MyResource" useGeneratedKeys="true" keyProperty="resourceId">
+        INSERT INTO my_resource (
+            matchmaker_id, name, age, gender, constellation,
+            height, weight, marr_status, diploma, income,
+            address, domicile, occupation, house, phone,
+            backup_phone, car, mate_selection_criteria,
+            is_user, user_id, create_time, update_time
+        ) VALUES (
+            #{matchmakerId}, #{name}, #{age}, #{gender}, #{constellation},
+            #{height}, #{weight}, #{marrStatus}, #{diploma}, #{income},
+            #{address}, #{domicile}, #{occupation}, #{house}, #{phone},
+            #{backupPhone}, #{car}, #{mateSelectionCriteria},
+            #{isUser}, #{userId}, #{createTime}, #{updateTime}
+        )
+    </insert>
+
     <!-- MyResourceVO结果映射 -->
     <resultMap id="MyResourceVOMap" type="com.zhentao.vo.MyResourceVO">
         <id property="resourceId" column="resource_id" jdbcType="INTEGER"/>
@@ -57,9 +79,14 @@
         <result property="phone" column="phone" jdbcType="VARCHAR"/>
         <result property="backupPhone" column="backup_phone" jdbcType="VARCHAR"/>
         <result property="car" column="car" jdbcType="INTEGER"/>
+        <result property="mateSelectionCriteria" column="mate_selection_criteria" jdbcType="VARCHAR"/>
         <result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
         <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表获取头像) -->
@@ -83,11 +110,27 @@
             r.phone,
             r.backup_phone,
             r.car,
+            r.mate_selection_criteria,
+            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}

+ 29 - 0
service/homePage/src/main/resources/mapper/MyResourceTagMapper.xml

@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.zhentao.mapper.MyResourceTagMapper">
+
+    <resultMap id="BaseResultMap" type="com.zhentao.entity.MyResourceTag">
+        <id property="id" column="id" jdbcType="INTEGER"/>
+        <result property="resourceId" column="resource_id" jdbcType="INTEGER"/>
+        <result property="tagId" column="tag_id" jdbcType="INTEGER"/>
+        <result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
+    </resultMap>
+
+    <!-- 根据资源ID删除所有标签关联 -->
+    <delete id="deleteByResourceId" parameterType="java.lang.Integer">
+        DELETE FROM my_resource_tag
+        WHERE resource_id = #{resourceId}
+    </delete>
+
+    <!-- 根据资源ID查询所有标签ID -->
+    <select id="selectTagIdsByResourceId" parameterType="java.lang.Integer" resultType="java.lang.Integer">
+        SELECT tag_id
+        FROM my_resource_tag
+        WHERE resource_id = #{resourceId}
+        ORDER BY create_time ASC
+    </select>
+
+</mapper>
+

+ 16 - 0
service/homePage/src/main/resources/sql/create_my_resource_tag_table.sql

@@ -0,0 +1,16 @@
+-- 创建资源标签关联表
+-- 用于存储资源(my_resource)和标签(tag)之间的多对多关系
+
+CREATE TABLE IF NOT EXISTS `my_resource_tag` (
+  `id` INT NOT NULL AUTO_INCREMENT COMMENT '关联ID',
+  `resource_id` INT NOT NULL COMMENT '资源ID(关联my_resource表的resource_id)',
+  `tag_id` INT NOT NULL COMMENT '标签ID(关联tag表的tag_id)',
+  `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  PRIMARY KEY (`id`) USING BTREE,
+  UNIQUE KEY `uk_resource_tag` (`resource_id`, `tag_id`) USING BTREE COMMENT '唯一索引:确保同一资源不会重复关联同一标签',
+  KEY `idx_resource_id` (`resource_id`) USING BTREE COMMENT '索引:按资源ID查询标签',
+  KEY `idx_tag_id` (`tag_id`) USING BTREE COMMENT '索引:按标签ID查询资源',
+  CONSTRAINT `fk_resource_tag_resource` FOREIGN KEY (`resource_id`) REFERENCES `my_resource` (`resource_id`) ON DELETE CASCADE ON UPDATE RESTRICT,
+  CONSTRAINT `fk_resource_tag_tag` FOREIGN KEY (`tag_id`) REFERENCES `tag` (`tag_id`) ON DELETE CASCADE ON UPDATE RESTRICT
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='资源标签关联表';
+

+ 12 - 1
service/websocket/src/main/java/com/zhentao/WebSocketApplication.java

@@ -18,8 +18,19 @@ import org.springframework.context.annotation.ComponentScan;
 @MapperScan({"com.zhentao.repository", "com.zhentao.mapper"})
 public class WebSocketApplication {
     public static void main(String[] args) {
-        SpringApplication.run(WebSocketApplication.class, args);
+        org.springframework.context.ConfigurableApplicationContext context = SpringApplication.run(WebSocketApplication.class, args);
         System.out.println("WebSocket聊天服务启动成功!");
         System.out.println("WebSocket连接地址: ws://localhost:1004/ws/chat?userId={userId}");
+        
+        System.out.println("====== 检查 Bean 是否存在 ======");
+        boolean hasChatController = context.containsBean("chatController");
+        System.out.println("ChatController 存在: " + hasChatController);
+        
+        if (hasChatController) {
+            System.out.println("ChatController Bean: " + context.getBean("chatController"));
+        } else {
+            System.err.println("❌ 严重错误: ChatController 未被注册!请检查包扫描路径或编译输出。");
+        }
+        System.out.println("==============================");
     }
 }

+ 17 - 0
service/websocket/src/main/java/com/zhentao/config/MinioConfig.java

@@ -3,6 +3,7 @@ package com.zhentao.config;
 import io.minio.MinioClient;
 import lombok.Data;
 import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 
@@ -10,6 +11,7 @@ import org.springframework.context.annotation.Configuration;
  * MinIO配置类
  */
 @Configuration
+@EnableConfigurationProperties
 @ConfigurationProperties(prefix = "minio")
 @Data
 public class MinioConfig {
@@ -39,6 +41,21 @@ public class MinioConfig {
      */
     @Bean
     public MinioClient minioClient() {
+        if (endpoint == null || endpoint.isEmpty()) {
+            throw new IllegalStateException("MinIO endpoint 未配置!请检查 application.yml 中的 minio.endpoint 配置");
+        }
+        if (accessKey == null || accessKey.isEmpty()) {
+            throw new IllegalStateException("MinIO accessKey 未配置!请检查 application.yml 中的 minio.access-key 配置");
+        }
+        if (secretKey == null || secretKey.isEmpty()) {
+            throw new IllegalStateException("MinIO secretKey 未配置!请检查 application.yml 中的 minio.secret-key 配置");
+        }
+        
+        System.out.println("✅ MinIO 配置加载成功:");
+        System.out.println("   - endpoint: " + endpoint);
+        System.out.println("   - accessKey: " + accessKey);
+        System.out.println("   - bucketName: " + bucketName);
+        
         return MinioClient.builder()
                 .endpoint(endpoint)
                 .credentials(accessKey, secretKey)

+ 5 - 0
service/websocket/src/main/java/com/zhentao/controller/ChatController.java

@@ -34,6 +34,11 @@ public class ChatController {
 
     @Autowired
     private UserVipService userVipService;
+    @GetMapping("/test")
+    public String test() {
+        return "ChatController is working!";
+    }
+
     /**
      * 获取会话消息列表(优化版:添加更多参数支持)
      * GET /api/chat/messages?userId=10001&targetUserId=10002&page=0&size=20&lastMessageId=xxx

+ 21 - 4
service/websocket/src/main/java/com/zhentao/controller/OnlineStatusController.java

@@ -20,18 +20,30 @@ public class OnlineStatusController {
 
     /**
      * 检查用户是否在线
-     * @param userId 用户ID
+     * @param userId 用户ID(支持普通用户ID和红娘IM ID,如 "123" 或 "m_22")
      * @return 在线状态
      */
     @GetMapping("/checkStatus")
-    public Map<String, Object> checkUserOnlineStatus(@RequestParam("userId") Long userId) {
+    public Map<String, Object> checkUserOnlineStatus(@RequestParam("userId") String userId) {
         Map<String, Object> result = new HashMap<>();
         
         try {
-            boolean isOnline = onlineUserService.isUserOnline(userId);
+            // 将字符串 userId 转换为 Long(如果是红娘ID "m_22",需要特殊处理)
+            Long userIdLong;
+            if (userId.startsWith("m_")) {
+                // 红娘ID格式:m_22 -> 提取数字部分
+                // 注意:这里假设 OnlineUserService 使用的是红娘的 matchmaker_id
+                // 如果需要使用完整的 IM ID,需要修改 OnlineUserService
+                userIdLong = Long.parseLong(userId.substring(2));
+            } else {
+                // 普通用户ID
+                userIdLong = Long.parseLong(userId);
+            }
+            
+            boolean isOnline = onlineUserService.isUserOnline(userIdLong);
             
             Map<String, Object> data = new HashMap<>();
-            data.put("userId", userId);
+            data.put("userId", userId);  // 返回原始 userId
             data.put("online", isOnline);
             data.put("timestamp", System.currentTimeMillis());
             
@@ -40,6 +52,11 @@ public class OnlineStatusController {
             result.put("data", data);
             
             System.out.println("查询用户 " + userId + " 在线状态: " + (isOnline ? "在线" : "离线"));
+        } catch (NumberFormatException e) {
+            System.err.println("无效的用户ID格式: " + userId);
+            result.put("code", 400);
+            result.put("message", "无效的用户ID格式");
+            result.put("data", null);
         } catch (Exception e) {
             System.err.println("查询在线状态失败: " + e.getMessage());
             result.put("code", 500);

+ 2 - 2
service/websocket/src/main/java/com/zhentao/controller/TIMController.java

@@ -33,11 +33,11 @@ public class TIMController {
      * GET /api/im/getUserSig?userId=1
      */
     @GetMapping("/getUserSig")
-    public Map<String, Object> getUserSig(@RequestParam Long userId) {
+    public Map<String, Object> getUserSig(@RequestParam String userId) {
         Map<String, Object> result = new HashMap<>();
         
         try {
-            String userSig = timUtils.generateUserSig(String.valueOf(userId));
+            String userSig = timUtils.generateUserSig(userId);
             
             Map<String, String> data = new HashMap<>();
             data.put("userSig", userSig);

+ 3 - 3
service/websocket/src/main/resources/application.yml

@@ -81,6 +81,6 @@ tim:
 # MinIO 对象存储配置
 minio:
   endpoint: http://115.190.125.125:9000
-  access-key: minioadmin
-  secret-key: minioadmin
-  bucket-name: minion-voice-files
+  accessKey: minioadmin
+  secretKey: minioadmin
+  bucketName: minion-voice-files

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor