8 Commits 2da671dbd0 ... 0e0a803340

Auteur SHA1 Bericht Datum
  yuxy 0e0a803340 Merge branch 'yxy' into test_dev 4 weken geleden
  YH_0525 a01607ae76 红娘排行榜 点击可查看详情 4 weken geleden
  YH_0525 45e110001b Merge remote-tracking branch 'origin/test_dev' into test 4 weken geleden
  YH_0525 eb4ad56d1a 红娘活动中心 和 我的活动 4 weken geleden
  mazhenhang 0ae5158168 红娘用户间消息存储 4 weken geleden
  YH_0525 ad676fde9e Merge remote-tracking branch 'origin/test_dev' into test 4 weken geleden
  YH_0525 c1071f675a fix: 修复排行榜点赞数显示为0的问题 4 weken geleden
  mazhenhang 82640f6d35 删除会话+登陆页面 4 weken geleden

+ 551 - 0
LiangZhiYUMao/pages/matchmaker-workbench/activities.vue

@@ -0,0 +1,551 @@
+<template>
+	<view class="activities-page">
+		<!-- 自定义导航栏 -->
+		<view class="custom-navbar">
+			<view class="navbar-left" @click="goBack">
+				<text class="back-icon">←</text>
+			</view>
+			<view class="navbar-title">活动中心</view>
+			<view class="navbar-right">
+				<view class="points-display">
+					<text class="points-label">积分</text>
+					<text class="points-value">{{ currentPoints }}</text>
+				</view>
+			</view>
+		</view>
+
+		<!-- 顶部公告 -->
+		<view class="announcement-bar">
+			<text class="announcement-icon">📢</text>
+			<text class="announcement-content">红娘活动报名开始啦!用积分兑换活动名额,积分兑好礼!</text>
+		</view>
+
+		<!-- 活动分类标签 - 横向滚动 -->
+		<scroll-view scroll-x class="category-scroll" show-scrollbar="false">
+			<view class="category-list">
+				<view 
+					v-for="(tab, index) in tabs" 
+					:key="index"
+					class="category-item" 
+					:class="{ active: activeTab === tab.type }"
+					@click="switchTab(tab.type)"
+				>
+					{{ tab.name }}
+				</view>
+			</view>
+		</scroll-view>
+
+		<!-- 活动列表 -->
+		<view class="activity-grid">
+			<view class="activity-card" v-for="(item, index) in activityList" :key="index" @click="goToDetail(item)">
+				<image :src="item.coverImage || item.cover_image" class="activity-image" mode="aspectFill"></image>
+				<view class="activity-info">
+					<view class="activity-name">{{ item.name }}</view>
+					<view class="activity-meta">
+						<text class="activity-location">📍 {{ item.location }}</text>
+					</view>
+					<view class="activity-footer">
+						<view class="activity-points">
+							<text class="points-symbol">💎</text>
+							<text class="points-value">{{ item.points }}</text>
+						</view>
+						<text class="activity-participants">{{ item.participants || 0 }}人参加</text>
+					</view>
+					<view class="activity-btn" 
+						:class="{ 'btn-disabled': hasExchanged(item.id) }"
+						@click.stop="handleActivityAction(item)">
+						{{ hasExchanged(item.id) ? '已报名' : '积分兑换' }}
+					</view>
+				</view>
+			</view>
+		</view>
+
+		<!-- 空状态 -->
+		<view class="empty-state" v-if="activityList.length === 0 && !loading">
+			<image src="https://img.icons8.com/color/96/000000/calendar--v1.png" class="empty-icon"></image>
+			<text class="empty-text">暂无活动</text>
+			<text class="empty-subtext">敬请期待更多精彩活动</text>
+		</view>
+
+		<!-- 加载更多 -->
+		<view class="load-more" v-if="hasMore && activityList.length > 0">
+			<text class="load-text">{{ loading ? '加载中...' : '上拉加载更多' }}</text>
+		</view>
+		<view class="no-more" v-else-if="activityList.length > 0">
+			<text class="no-more-text">没有更多了</text>
+		</view>
+	</view>
+</template>
+
+<script>
+import api from '../../utils/api.js'
+
+export default {
+	data() {
+		return {
+			activityList: [],
+			pageNum: 1,
+			pageSize: 10,
+			hasMore: true,
+			loading: false,
+			currentPoints: 0,
+			makerId: null,
+			exchangedActivities: [],
+			tabs: [
+				{ name: '全部活动', type: 'all' },
+				{ name: '推荐活动', type: 'premium' }
+			],
+			activeTab: 'all'
+		}
+	},
+
+	onLoad() {
+		this.initData()
+	},
+
+	onReachBottom() {
+		if (this.hasMore && !this.loading) {
+			this.pageNum++
+			this.loadActivityList()
+		}
+	},
+
+	methods: {
+		// 初始化数据
+		async initData() {
+			const userInfo = uni.getStorageSync('userInfo')
+			// 兼容多种字段名
+			this.makerId = userInfo && (userInfo.matchmakerId || userInfo.makerId || userInfo.matchmaker_id)
+			
+			// 如果没有makerId,尝试通过userId获取
+			if (!this.makerId && userInfo && userInfo.userId) {
+				try {
+					const res = await api.matchmaker.getByUserId(userInfo.userId)
+					let matchmaker = res
+					if (res && res.data) {
+						matchmaker = res.data
+					}
+					if (matchmaker) {
+						this.makerId = matchmaker.matchmakerId || matchmaker.matchmaker_id
+					}
+				} catch (e) {
+					console.error('获取红娘信息失败:', e)
+				}
+			}
+			
+			if (!this.makerId) {
+				uni.showToast({
+					title: '请先登录红娘账号',
+					icon: 'none'
+				})
+				return
+			}
+			
+			await this.loadMakerInfo()
+			await this.loadExchangedActivities()
+			await this.loadActivityList()
+		},
+
+		// 加载红娘信息(包括积分)
+		async loadMakerInfo() {
+			try {
+				const res = await api.matchmaker.getDetail(this.makerId)
+				// 兼容不同的返回格式
+				const makerInfo = res && res.data ? res.data : res
+				if (makerInfo) {
+					this.currentPoints = makerInfo.points || makerInfo.point || 0
+					console.log('红娘积分:', this.currentPoints)
+				}
+			} catch (error) {
+				console.error('加载红娘信息失败:', error)
+				this.currentPoints = 0
+			}
+		},
+
+		// 加载已兑换的活动列表
+		async loadExchangedActivities() {
+			try {
+				const list = await api.matchmakerActivity.getPurchasedList(this.makerId)
+				if (list && list.length > 0) {
+					this.exchangedActivities = list.map(item => item.activity_id || item.activityId)
+				}
+			} catch (error) {
+				console.error('加载已兑换活动失败:', error)
+				this.exchangedActivities = []
+			}
+		},
+
+		// 加载活动列表
+		async loadActivityList() {
+			if (this.loading) return
+			this.loading = true
+
+			try {
+				const params = {
+					categoryType: this.activeTab
+				}
+				const data = await api.matchmakerActivity.getList(params)
+
+				if (data && data.length > 0) {
+					this.activityList = [...this.activityList, ...data]
+					this.hasMore = data.length >= this.pageSize
+				} else {
+					this.hasMore = false
+				}
+			} catch (error) {
+				console.error('加载活动列表失败:', error)
+				this.hasMore = false
+			} finally {
+				this.loading = false
+			}
+		},
+
+		// 切换分类标签
+		switchTab(type) {
+			if (this.activeTab === type) return
+			this.activeTab = type
+			this.pageNum = 1
+			this.activityList = []
+			this.loadActivityList()
+		},
+
+		// 检查是否已兑换
+		hasExchanged(activityId) {
+			return this.exchangedActivities.includes(activityId)
+		},
+
+		// 处理活动操作(兑换或查看详情)
+		async handleActivityAction(item) {
+			if (this.hasExchanged(item.id)) {
+				// 已兑换,跳转到详情页
+				uni.navigateTo({
+					url: `/pages/matchmaker-workbench/activity-detail?id=${item.id}`
+				})
+			} else {
+				// 未兑换,显示兑换确认
+				uni.showModal({
+					title: '确认兑换',
+					content: `是否用 ${item.points} 积分兑换"${item.name}"活动?`,
+					confirmText: '确认',
+					cancelText: '取消',
+					success: (res) => {
+						if (res.confirm) {
+							this.exchangeActivity(item)
+						}
+					}
+				})
+			}
+		},
+
+		// 兑换活动
+		async exchangeActivity(item) {
+			const pointsNeeded = item.points || 0
+			
+			if (this.currentPoints < pointsNeeded) {
+				uni.showToast({
+					title: '积分不足',
+					icon: 'none'
+				})
+				return
+			}
+
+			try {
+				console.log('兑换参数:', {
+					makerId: this.makerId,
+					activityId: item.id,
+					points: pointsNeeded
+				})
+				
+				const result = await api.matchmakerActivity.exchange({
+					makerId: this.makerId,
+					activityId: item.id,
+					points: pointsNeeded
+				})
+				
+				console.log('兑换结果:', result)
+
+				// API请求成功(request函数已处理code=200的情况,能走到这里说明已成功)
+				uni.showToast({
+					title: '兑换成功',
+					icon: 'success'
+				})
+				this.currentPoints -= pointsNeeded
+				this.exchangedActivities.push(item.id)
+				
+				// 跳转到详情页
+				setTimeout(() => {
+					uni.navigateTo({
+						url: `/pages/matchmaker-workbench/activity-detail?id=${item.id}`
+					})
+				}, 500)
+			} catch (error) {
+				console.error('兑换失败:', error)
+				uni.showToast({
+					title: error.message || '兑换失败',
+					icon: 'none'
+				})
+			}
+		},
+
+		// 跳转到活动详情
+		goToDetail(item) {
+			uni.navigateTo({
+				url: `/pages/matchmaker-workbench/activity-detail?id=${item.id}`
+			})
+		},
+
+		// 返回
+		goBack() {
+			uni.navigateBack()
+		}
+	}
+}
+</script>
+
+<style lang="scss" scoped>
+	.activities-page {
+		min-height: 100vh;
+		background-color: #FFF9F9;
+		padding-top: 90rpx;
+	}
+
+	/* 自定义导航栏 */
+	.custom-navbar {
+		position: fixed;
+		top: 0;
+		left: 0;
+		right: 0;
+		height: 90rpx;
+		display: flex;
+		align-items: center;
+		justify-content: space-between;
+		padding: 0 20rpx;
+		background: linear-gradient(135deg, #FCE4EC 0%, #F8BBD0 100%);
+		z-index: 999;
+
+		.navbar-left,
+		.navbar-right {
+			width: 80rpx;
+		}
+
+		.back-icon {
+			font-size: 40rpx;
+			color: #333333;
+			font-weight: bold;
+		}
+
+		.navbar-title {
+			flex: 1;
+			text-align: center;
+			font-size: 32rpx;
+			font-weight: bold;
+			color: #333333;
+		}
+
+		.points-display {
+			display: flex;
+			align-items: center;
+			gap: 8rpx;
+			background-color: rgba(255, 255, 255, 0.8);
+			padding: 8rpx 16rpx;
+			border-radius: 20rpx;
+
+			.points-label {
+				font-size: 24rpx;
+				color: #666666;
+			}
+
+			.points-value {
+				font-size: 28rpx;
+				color: #FF6B8A;
+				font-weight: bold;
+			}
+		}
+	}
+
+	/* 公告栏 */
+	.announcement-bar {
+		display: flex;
+		align-items: center;
+		gap: 12rpx;
+		padding: 16rpx 20rpx;
+		background-color: #FFF3E0;
+		margin: 10rpx 0;
+
+		.announcement-icon {
+			font-size: 28rpx;
+			flex-shrink: 0;
+		}
+
+		.announcement-content {
+			font-size: 24rpx;
+			color: #E65100;
+			flex: 1;
+		}
+	}
+
+	/* 分类标签 - 横向滚动 */
+	.category-scroll {
+		white-space: nowrap;
+		padding: 0 20rpx;
+		margin: 10rpx 0;
+	}
+
+	.category-list {
+		display: flex;
+		gap: 12rpx;
+	}
+
+	.category-item {
+		display: inline-block;
+		padding: 12rpx 24rpx;
+		background-color: #FFFFFF;
+		border-radius: 30rpx;
+		font-size: 26rpx;
+		color: #666666;
+		border: 2rpx solid #EEEEEE;
+		transition: all 0.3s ease;
+
+		&.active {
+			background-color: #9C27B0;
+			color: #FFFFFF;
+			border-color: #9C27B0;
+		}
+	}
+
+	/* 活动列表 - 两列布局 */
+	.activity-grid {
+		display: grid;
+		grid-template-columns: 1fr 1fr;
+		gap: 20rpx;
+		padding: 20rpx;
+
+		.activity-card {
+			position: relative;
+			background-color: #FFFFFF;
+			border-radius: 20rpx;
+			overflow: hidden;
+			box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
+
+			.activity-image {
+				width: 100%;
+				height: 240rpx;
+				background-color: #F5F5F5;
+			}
+
+			.activity-info {
+				padding: 20rpx;
+
+				.activity-name {
+					font-size: 28rpx;
+					font-weight: bold;
+					color: #333333;
+					margin-bottom: 10rpx;
+					display: -webkit-box;
+					-webkit-box-orient: vertical;
+					-webkit-line-clamp: 2;
+					overflow: hidden;
+				}
+
+				.activity-meta {
+					margin-bottom: 15rpx;
+
+					.activity-location {
+						font-size: 24rpx;
+						color: #666666;
+					}
+				}
+
+				.activity-footer {
+					display: flex;
+					justify-content: space-between;
+					align-items: center;
+					margin-bottom: 15rpx;
+
+					.activity-points {
+						display: flex;
+						align-items: center;
+
+						.points-symbol {
+							font-size: 24rpx;
+							margin-right: 5rpx;
+						}
+
+						.points-value {
+							font-size: 26rpx;
+							color: #FF6B8A;
+							font-weight: bold;
+						}
+					}
+
+					.activity-participants {
+						font-size: 22rpx;
+						color: #999999;
+					}
+				}
+
+				.activity-btn {
+					width: 100%;
+					height: 60rpx;
+					display: flex;
+					align-items: center;
+					justify-content: center;
+					background-color: #9C27B0;
+					color: #FFFFFF;
+					border-radius: 30rpx;
+					font-size: 26rpx;
+					transition: all 0.3s ease;
+
+					&:active {
+						background-color: #7B1FA2;
+					}
+
+					&.btn-disabled {
+						background-color: #CCCCCC;
+						color: #999999;
+					}
+				}
+			}
+		}
+	}
+
+	/* 空状态 */
+	.empty-state {
+		display: flex;
+		flex-direction: column;
+		align-items: center;
+		justify-content: center;
+		padding: 100rpx 0;
+
+		.empty-icon {
+			width: 120rpx;
+			height: 120rpx;
+			opacity: 0.5;
+			margin-bottom: 20rpx;
+		}
+
+		.empty-text {
+			font-size: 32rpx;
+			color: #666666;
+			margin-bottom: 10rpx;
+		}
+
+		.empty-subtext {
+			font-size: 26rpx;
+			color: #999999;
+		}
+	}
+
+	/* 加载更多 */
+	.load-more,
+	.no-more {
+		padding: 30rpx 0;
+		text-align: center;
+
+		.load-text,
+		.no-more-text {
+			font-size: 24rpx;
+			color: #999999;
+		}
+	}
+</style>

+ 549 - 0
LiangZhiYUMao/pages/matchmaker-workbench/activity-detail.vue

@@ -0,0 +1,549 @@
+<template>
+	<view class="activity-detail-page">
+		<!-- 自定义导航栏 -->
+		<view class="custom-navbar">
+			<view class="navbar-left" @click="goBack">
+				<text class="back-icon">←</text>
+			</view>
+			<view class="navbar-title">活动详情</view>
+			<view class="navbar-right"></view>
+		</view>
+
+		<!-- 活动封面 -->
+		<view class="activity-cover">
+			<image :src="activity.coverImage || activity.cover_image" class="cover-image" mode="aspectFill"></image>
+			<view class="cover-mask">
+				<view class="activity-tag" v-if="activity.isRecommended">⭐ 推荐活动</view>
+			</view>
+		</view>
+
+		<!-- 活动信息 -->
+		<view class="activity-content">
+			<view class="activity-title">{{ activity.name }}</view>
+
+			<view class="info-section">
+				<view class="info-item">
+					<text class="info-icon">⏰</text>
+					<text class="info-label">活动时间:</text>
+					<text class="info-value">{{ formatActivityTime(activity.startTime || activity.start_time, activity.endTime || activity.end_time) }}</text>
+				</view>
+				<view class="info-item">
+					<text class="info-icon">📍</text>
+					<text class="info-label">活动地点:</text>
+					<text class="info-value">{{ activity.location || '待定' }}</text>
+				</view>
+				<view class="info-item">
+					<text class="info-icon">👥</text>
+					<text class="info-label">报名人数:</text>
+					<text class="info-value">{{ activity.participants || 0 }} / {{ activity.capacity || '不限' }}</text>
+				</view>
+				<view class="info-item">
+					<text class="info-icon">💎</text>
+					<text class="info-label">所需积分:</text>
+					<text class="info-value points">{{ activity.points || 0 }}</text>
+				</view>
+			</view>
+
+			<view class="divider"></view>
+
+			<view class="description-section">
+				<view class="section-title">活动介绍</view>
+				<view class="description-text">{{ activity.description || '暂无介绍' }}</view>
+			</view>
+
+			<view class="divider"></view>
+
+			<view class="content-section" v-if="activity.content">
+				<view class="section-title">活动详情</view>
+				<view class="content-text">{{ activity.content }}</view>
+			</view>
+		</view>
+
+		<!-- 底部兑换按钮 -->
+		<view class="bottom-bar" v-if="!hasExchanged">
+			<view class="points-info">
+				<text class="points-label">所需积分</text>
+				<view class="points-value">
+					<text class="points-symbol">💎</text>
+					<text class="points-num">{{ activity.points || 0 }}</text>
+				</view>
+			</view>
+			<view class="exchange-btn" 
+				:class="{ 'btn-disabled': currentPoints < (activity.points || 0) }"
+				@click="handleExchange">
+				<text class="btn-text">{{ currentPoints < (activity.points || 0) ? '积分不足' : '积分兑换报名' }}</text>
+			</view>
+		</view>
+
+		<!-- 已报名状态 -->
+		<view class="bottom-bar" v-else>
+			<view class="registered-status">
+				<text class="status-icon">✓</text>
+				<text class="status-text">已报名</text>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+import api from '../../utils/api.js'
+
+export default {
+	data() {
+		return {
+			activityId: null,
+			activity: {
+				name: '加载中...',
+				coverImage: 'https://images.unsplash.com/photo-1511671782779-c97d3d27a1d4?ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=60',
+				points: 0
+			},
+			currentPoints: 0,
+			makerId: null,
+			hasExchanged: false
+		}
+	},
+
+	onLoad(options) {
+		if (options.id) {
+			this.activityId = options.id
+			this.initData()
+		}
+	},
+
+	methods: {
+		// 初始化数据
+		async initData() {
+			const userInfo = uni.getStorageSync('userInfo')
+			// 兼容多种字段名
+			this.makerId = userInfo && (userInfo.matchmakerId || userInfo.makerId || userInfo.matchmaker_id)
+			
+			// 如果没有makerId,尝试通过userId获取
+			if (!this.makerId && userInfo && userInfo.userId) {
+				try {
+					const res = await api.matchmaker.getByUserId(userInfo.userId)
+					let matchmaker = res
+					if (res && res.data) {
+						matchmaker = res.data
+					}
+					if (matchmaker) {
+						this.makerId = matchmaker.matchmakerId || matchmaker.matchmaker_id
+					}
+				} catch (e) {
+					console.error('获取红娘信息失败:', e)
+				}
+			}
+			
+			await this.loadMakerInfo()
+			await this.loadActivityDetail()
+			await this.checkExchangeStatus()
+		},
+
+		// 加载红娘信息(包括积分)
+		async loadMakerInfo() {
+			try {
+				const res = await api.matchmaker.getDetail(this.makerId)
+				console.log('红娘信息返回:', res)
+				// 兼容不同的返回格式
+				let makerInfo = res
+				if (res && res.data) {
+					makerInfo = res.data
+				}
+				if (makerInfo) {
+					// 兼容 snake_case 和 camelCase
+					this.currentPoints = makerInfo.points ?? makerInfo.total_points ?? 0
+					console.log('当前积分:', this.currentPoints)
+				}
+			} catch (error) {
+				console.error('加载红娘信息失败:', error)
+				this.currentPoints = 0
+			}
+		},
+
+		// 加载活动详情
+		async loadActivityDetail() {
+			try {
+				const data = await api.matchmakerActivity.getDetail(this.activityId)
+				if (data) {
+					this.activity = data
+				}
+			} catch (error) {
+				console.error('加载活动详情失败:', error)
+				uni.showToast({
+					title: '加载失败',
+					icon: 'none'
+				})
+			}
+		},
+
+		// 检查是否已兑换
+		async checkExchangeStatus() {
+			try {
+				console.log('检查兑换状态, makerId:', this.makerId, 'activityId:', this.activityId)
+				const res = await api.matchmakerActivity.getPurchasedList(this.makerId)
+				console.log('已兑换活动列表返回:', res)
+				// 兼容不同的返回格式
+				let list = res
+				if (res && res.data) {
+					list = res.data
+				}
+				if (Array.isArray(list) && list.length > 0) {
+					this.hasExchanged = list.some(item => {
+						const itemActivityId = item.activity_id || item.activityId
+						console.log('比较活动ID:', itemActivityId, '==', this.activityId)
+						return itemActivityId == this.activityId
+					})
+				}
+				console.log('是否已兑换:', this.hasExchanged)
+			} catch (error) {
+				console.error('检查兑换状态失败:', error)
+				this.hasExchanged = false
+			}
+		},
+
+		// 格式化活动时间
+		formatActivityTime(startTime, endTime) {
+			if (!startTime) return '待定'
+			
+			const start = new Date(startTime)
+			const end = endTime ? new Date(endTime) : null
+			
+			const startStr = start.toLocaleDateString('zh-CN') + ' ' + 
+							start.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
+			
+			if (end) {
+				// 判断是否同一天
+				const isSameDay = start.toDateString() === end.toDateString()
+				let endStr
+				if (isSameDay) {
+					// 同一天只显示时间
+					endStr = end.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
+				} else {
+					// 不同天显示完整日期时间
+					endStr = end.toLocaleDateString('zh-CN') + ' ' + 
+							end.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
+				}
+				return `${startStr} - ${endStr}`
+			}
+			
+			return startStr
+		},
+
+		// 处理兑换
+		async handleExchange() {
+			if (this.currentPoints < (this.activity.points || 0)) {
+				uni.showToast({
+					title: '积分不足',
+					icon: 'none'
+				})
+				return
+			}
+
+			uni.showModal({
+				title: '确认兑换',
+				content: `是否用 ${this.activity.points} 积分兑换"${this.activity.name}"活动?`,
+				confirmText: '确认',
+				cancelText: '取消',
+				success: (res) => {
+					if (res.confirm) {
+						this.exchangeActivity()
+					}
+				}
+			})
+		},
+
+		// 兑换活动
+		async exchangeActivity() {
+			try {
+				const result = await api.matchmakerActivity.exchange({
+					makerId: this.makerId,
+					activityId: this.activityId,
+					points: this.activity.points
+				})
+
+				if (result) {
+					uni.showToast({
+						title: '兑换成功',
+						icon: 'success'
+					})
+					this.currentPoints -= this.activity.points
+					this.hasExchanged = true
+					
+					// 更新参与人数
+					if (this.activity.participants) {
+						this.activity.participants++
+					} else {
+						this.activity.participants = 1
+					}
+				}
+			} catch (error) {
+				console.error('兑换失败:', error)
+				uni.showToast({
+					title: error.message || '兑换失败',
+					icon: 'none'
+				})
+			}
+		},
+
+		// 返回
+		goBack() {
+			uni.navigateBack()
+		}
+	}
+}
+</script>
+
+<style lang="scss" scoped>
+	.activity-detail-page {
+		min-height: 100vh;
+		background-color: #FFF9F9;
+		padding-top: 90rpx;
+		padding-bottom: 120rpx;
+	}
+
+	/* 自定义导航栏 */
+	.custom-navbar {
+		position: fixed;
+		top: 0;
+		left: 0;
+		right: 0;
+		height: 90rpx;
+		display: flex;
+		align-items: center;
+		justify-content: space-between;
+		padding: 0 20rpx;
+		background: linear-gradient(135deg, #FCE4EC 0%, #F8BBD0 100%);
+		z-index: 999;
+
+		.navbar-left,
+		.navbar-right {
+			width: 80rpx;
+		}
+
+		.back-icon {
+			font-size: 40rpx;
+			color: #333333;
+			font-weight: bold;
+		}
+
+		.navbar-title {
+			flex: 1;
+			text-align: center;
+			font-size: 32rpx;
+			font-weight: bold;
+			color: #333333;
+		}
+	}
+
+	/* 活动封面 */
+	.activity-cover {
+		position: relative;
+		width: 100%;
+		height: 400rpx;
+		overflow: hidden;
+
+		.cover-image {
+			width: 100%;
+			height: 100%;
+			background-color: #F5F5F5;
+		}
+
+		.cover-mask {
+			position: absolute;
+			top: 0;
+			left: 0;
+			right: 0;
+			bottom: 0;
+			display: flex;
+			align-items: flex-start;
+			justify-content: flex-start;
+			padding: 20rpx;
+			background: linear-gradient(180deg, rgba(0, 0, 0, 0.3) 0%, transparent 100%);
+
+			.activity-tag {
+				background-color: rgba(255, 215, 0, 0.9);
+				color: #333333;
+				padding: 8rpx 16rpx;
+				border-radius: 20rpx;
+				font-size: 24rpx;
+				font-weight: bold;
+			}
+		}
+	}
+
+	/* 活动内容 */
+	.activity-content {
+		padding: 30rpx 20rpx;
+
+		.activity-title {
+			font-size: 36rpx;
+			font-weight: bold;
+			color: #333333;
+			margin-bottom: 30rpx;
+		}
+
+		.info-section {
+			background-color: #FFFFFF;
+			border-radius: 12rpx;
+			padding: 20rpx;
+			margin-bottom: 20rpx;
+
+			.info-item {
+				display: flex;
+				align-items: flex-start;
+				margin-bottom: 16rpx;
+
+				&:last-child {
+					margin-bottom: 0;
+				}
+
+				.info-icon {
+					font-size: 28rpx;
+					margin-right: 12rpx;
+					flex-shrink: 0;
+				}
+
+				.info-label {
+					font-size: 26rpx;
+					color: #666666;
+					min-width: 120rpx;
+				}
+
+				.info-value {
+					font-size: 26rpx;
+					color: #333333;
+					flex: 1;
+
+					&.points {
+						color: #FF6B8A;
+						font-weight: bold;
+					}
+				}
+			}
+		}
+
+		.divider {
+			height: 1rpx;
+			background-color: #EEEEEE;
+			margin: 20rpx 0;
+		}
+
+		.description-section,
+		.content-section {
+			background-color: #FFFFFF;
+			border-radius: 12rpx;
+			padding: 20rpx;
+			margin-bottom: 20rpx;
+
+			.section-title {
+				font-size: 28rpx;
+				font-weight: bold;
+				color: #333333;
+				margin-bottom: 16rpx;
+			}
+
+			.description-text,
+			.content-text {
+				font-size: 26rpx;
+				color: #666666;
+				line-height: 1.6;
+				white-space: pre-wrap;
+				word-break: break-word;
+			}
+		}
+	}
+
+	/* 底部栏 */
+	.bottom-bar {
+		position: fixed;
+		bottom: 0;
+		left: 0;
+		right: 0;
+		height: 120rpx;
+		display: flex;
+		align-items: center;
+		justify-content: space-between;
+		padding: 20rpx;
+		background-color: #FFFFFF;
+		border-top: 1rpx solid #EEEEEE;
+		z-index: 100;
+
+		.points-info {
+			display: flex;
+			flex-direction: column;
+			align-items: flex-start;
+
+			.points-label {
+				font-size: 24rpx;
+				color: #999999;
+				margin-bottom: 8rpx;
+			}
+
+			.points-value {
+				display: flex;
+				align-items: center;
+				gap: 8rpx;
+
+				.points-symbol {
+					font-size: 28rpx;
+				}
+
+				.points-num {
+					font-size: 32rpx;
+					color: #FF6B8A;
+					font-weight: bold;
+				}
+			}
+		}
+
+		.exchange-btn {
+			flex: 1;
+			height: 80rpx;
+			display: flex;
+			align-items: center;
+			justify-content: center;
+			background: linear-gradient(135deg, #9C27B0 0%, #7B1FA2 100%);
+			color: #FFFFFF;
+			border-radius: 40rpx;
+			font-size: 28rpx;
+			font-weight: bold;
+			margin-left: 20rpx;
+			transition: all 0.3s ease;
+
+			&:active {
+				transform: scale(0.95);
+			}
+
+			&.btn-disabled {
+				background: #CCCCCC;
+				color: #999999;
+			}
+
+			.btn-text {
+				color: inherit;
+			}
+		}
+
+		.registered-status {
+			flex: 1;
+			height: 80rpx;
+			display: flex;
+			align-items: center;
+			justify-content: center;
+			background-color: #E8F5E9;
+			border-radius: 40rpx;
+			margin-left: 20rpx;
+
+			.status-icon {
+				font-size: 40rpx;
+				color: #4CAF50;
+				margin-right: 12rpx;
+			}
+
+			.status-text {
+				font-size: 28rpx;
+				color: #4CAF50;
+				font-weight: bold;
+			}
+		}
+	}
+</style>

+ 12 - 2
LiangZhiYUMao/pages/matchmaker-workbench/index.vue

@@ -91,7 +91,7 @@
 					<text class="section-more" @click="navigateToRanking">排行榜 ></text>
 				</view>
 				<view class="best-matchmaker-list">
-					<view class="best-matchmaker-item" v-for="(item, index) in bestMatchmakers" :key="item.matchmaker_id || index">
+					<view class="best-matchmaker-item" v-for="(item, index) in bestMatchmakers" :key="item.matchmaker_id || index" @click="goToMatchmakerDetail(item)">
 						<text class="rank-number" :class="'rank-' + (index + 1)">{{ index + 1 }}</text>
 						<image class="avatar-small" :src="item.avatar_url || '/static/default-avatar.svg'" mode="aspectFill"></image>
 						<view class="matchmaker-info">
@@ -286,7 +286,7 @@ export default {
 			// 导航到活动中心
 			navigateToActivityCenter() {
 				uni.navigateTo({
-					url: '/pages/activities/list'
+					url: '/pages/matchmaker-workbench/activities'
 				})
 			},
 			// 导航到排行榜
@@ -295,6 +295,16 @@ export default {
 					url: '/pages/matchmaker-workbench/ranking'
 				})
 			},
+			// 跳转到红娘详情
+			goToMatchmakerDetail(item) {
+				if (!item) return
+				const id = item.matchmaker_id || item.matchmakerId
+				if (id) {
+					uni.navigateTo({
+						url: `/pages/matchmakers/detail?id=${id}`
+					})
+				}
+			},
 			// 导航到工作台
 			navigateToWorkbench() {
 				// 已在工作台,无需跳转

+ 33 - 16
LiangZhiYUMao/pages/matchmaker-workbench/ranking.vue

@@ -28,32 +28,32 @@
 
 		<!-- 前三名展示 -->
 		<view class="top-three-container" v-if="topThree.length >= 3">
-			<view class="top-three-item second">
+			<view class="top-three-item second" @click="goToDetail(topThree[1])">
 				<text class="rank-number">2</text>
 				<image class="avatar-large" :src="topThree[1].avatar_url || defaultAvatar" mode="aspectFill"></image>
 				<text class="matchmaker-name">{{ topThree[1].real_name }}</text>
 				<text class="success-count">成功人数: {{ topThree[1].success_couples || 0 }}</text>
-				<view class="like-btn" :class="{ liked: topThree[1].hasLiked }" @click="handleLike(topThree[1])">
+				<view class="like-btn" :class="{ liked: topThree[1].hasLiked }" @click.stop="handleLike(topThree[1])">
 					{{ topThree[1].hasLiked ? '已点赞' : '点赞' }}
 				</view>
 			</view>
 
-			<view class="top-three-item first">
+			<view class="top-three-item first" @click="goToDetail(topThree[0])">
 				<text class="rank-number">1</text>
 				<image class="avatar-large" :src="topThree[0].avatar_url || defaultAvatar" mode="aspectFill"></image>
 				<text class="matchmaker-name">{{ topThree[0].real_name }}</text>
 				<text class="success-count">成功人数: {{ topThree[0].success_couples || 0 }}</text>
-				<view class="like-btn" :class="{ liked: topThree[0].hasLiked }" @click="handleLike(topThree[0])">
+				<view class="like-btn" :class="{ liked: topThree[0].hasLiked }" @click.stop="handleLike(topThree[0])">
 					{{ topThree[0].hasLiked ? '已点赞' : '点赞' }}
 				</view>
 			</view>
 
-			<view class="top-three-item third">
+			<view class="top-three-item third" @click="goToDetail(topThree[2])">
 				<text class="rank-number">3</text>
 				<image class="avatar-large" :src="topThree[2].avatar_url || defaultAvatar" mode="aspectFill"></image>
 				<text class="matchmaker-name">{{ topThree[2].real_name }}</text>
 				<text class="success-count">成功人数: {{ topThree[2].success_couples || 0 }}</text>
-				<view class="like-btn" :class="{ liked: topThree[2].hasLiked }" @click="handleLike(topThree[2])">
+				<view class="like-btn" :class="{ liked: topThree[2].hasLiked }" @click.stop="handleLike(topThree[2])">
 					{{ topThree[2].hasLiked ? '已点赞' : '点赞' }}
 				</view>
 			</view>
@@ -61,7 +61,7 @@
 
 		<!-- 排行榜列表 -->
 		<scroll-view scroll-y class="ranking-list">
-			<view class="ranking-item" v-for="(item, index) in restList" :key="item.matchmaker_id">
+			<view class="ranking-item" v-for="(item, index) in restList" :key="item.matchmaker_id" @click="goToDetail(item)">
 				<text class="rank-number-normal">{{ index + 4 }}</text>
 				<image class="avatar-small" :src="item.avatar_url || defaultAvatar" mode="aspectFill"></image>
 				<view class="matchmaker-info">
@@ -69,7 +69,7 @@
 					<text class="success-count-normal">成功: {{ item.success_couples || 0 }} | 点赞: {{ item.weeklyLikes || 0 }}</text>
 					<text class="user-rating">等级: {{ item.level_name || '青铜红娘' }}</text>
 				</view>
-				<view class="like-btn-small" :class="{ liked: item.hasLiked }" @click="handleLike(item)">
+				<view class="like-btn-small" :class="{ liked: item.hasLiked }" @click.stop="handleLike(item)">
 					{{ item.hasLiked ? '已赞' : '点赞' }}
 				</view>
 			</view>
@@ -154,12 +154,20 @@ export default {
 						limit: 20,
 						userId: this.userId
 					})
+					console.log('API原始返回数据:', JSON.stringify(res))
+					
+					let dataList = []
 					if (res && Array.isArray(res)) {
-						this.rankingList = res.map(item => this.formatMatchmaker(item))
+						dataList = res
 					} else if (res && res.data && Array.isArray(res.data)) {
-						this.rankingList = res.data.map(item => this.formatMatchmaker(item))
+						dataList = res.data
+					} else if (res && typeof res === 'object') {
+						// 可能直接返回的是对象数组
+						dataList = Object.values(res).filter(item => typeof item === 'object' && item.matchmakerId)
 					}
-					console.log('本周排行榜数据:', this.rankingList)
+					
+					this.rankingList = dataList.map(item => this.formatMatchmaker(item))
+					console.log('格式化后的排行榜数据:', JSON.stringify(this.rankingList.slice(0, 3)))
 				} catch (e) {
 					console.error('加载排行榜数据失败:', e)
 					uni.showToast({
@@ -179,8 +187,8 @@ export default {
 					avatar_url: item.avatarUrl || item.avatar_url,
 					success_couples: item.successCouples || item.success_couples,
 					level_name: item.levelName || item.level_name,
-					weeklyLikes: item.weeklyLikes || 0,
-					hasLiked: item.hasLiked || false
+					weeklyLikes: item.weekly_likes || item.weeklyLikes || 0,
+					hasLiked: item.has_liked || item.hasLiked || false
 				}
 			},
 			// 点赞
@@ -212,11 +220,11 @@ export default {
 							title: '点赞成功',
 							icon: 'success'
 						})
-						// 更新本地状态
+						// 更新本地状态 - 直接修改数据,不重新加载
 						matchmaker.hasLiked = true
 						matchmaker.weeklyLikes = (matchmaker.weeklyLikes || 0) + 1
-						// 刷新列表
-						this.loadRankingData()
+						// 强制更新视图
+						this.$forceUpdate()
 					} else {
 						uni.showToast({
 							title: res.message || '点赞失败',
@@ -258,6 +266,15 @@ export default {
 				uni.redirectTo({
 					url: '/pages/matchmaker-workbench/mine'
 				})
+			},
+			// 跳转到红娘详情
+			goToDetail(matchmaker) {
+				const id = matchmaker.matchmaker_id || matchmaker.matchmakerId
+				if (id) {
+					uni.navigateTo({
+						url: `/pages/matchmakers/detail?id=${id}`
+					})
+				}
 			}
 		}
 	}

+ 27 - 6
LiangZhiYUMao/pages/message/chat.vue

@@ -1286,11 +1286,11 @@ export default {
       try {
         console.log('🔄 同步消息到MySQL...', timMessage.ID);
         
-        // 构建同步参数
+        // 构建同步参数(V2版本,支持红娘消息)
         const syncData = {
           messageId: timMessage.ID,
-          fromUserId: timMessage.from,
-          toUserId: timMessage.to,
+          fromTimUserId: String(timMessage.from),  // 保持字符串格式,支持 m_xxx
+          toTimUserId: String(timMessage.to),      // 保持字符串格式,支持 m_xxx
           messageType: this.getMessageType(timMessage),
           content: this.getMessageContent(timMessage),
           sendTime: timMessage.time  // TIM返回的是秒级时间戳
@@ -1324,9 +1324,9 @@ export default {
           }
         }
         
-        // 调用后端同步接口
+        // 调用后端同步接口(V2版本,支持红娘消息)
         const res = await uni.request({
-          url: 'http://localhost:8083/api/chat/syncTIMMessage',
+          url: 'http://localhost:8083/api/chat/syncTIMMessageV2',
           method: 'POST',
           data: syncData,
           header: {
@@ -1335,7 +1335,8 @@ export default {
         });
         
         if (res[1].data.code === 200) {
-          console.log('✅ 消息已同步到MySQL:', timMessage.ID);
+          const msgType = res[1].data.type === 'matchmaker' ? '红娘消息' : '用户消息';
+          console.log(`✅ 消息已同步到MySQL (${msgType}):`, timMessage.ID);
         } else {
           console.warn('⚠️ 消息同步失败:', res[1].data.message);
         }
@@ -1403,6 +1404,26 @@ export default {
       }
     },
     
+    /**
+     * 根据语音时长计算语音消息气泡宽度
+     * @param {number} duration 语音时长(秒)
+     * @returns {string} 宽度样式值
+     */
+    getVoiceWidth(duration) {
+      // 基础宽度 120rpx,每秒增加 10rpx,最大 300rpx
+      const baseWidth = 120;
+      const perSecondWidth = 10;
+      const maxWidth = 300;
+      const minWidth = 120;
+      
+      const seconds = parseInt(duration) || 0;
+      let width = baseWidth + seconds * perSecondWidth;
+      
+      // 限制在最小和最大范围内
+      width = Math.max(minWidth, Math.min(maxWidth, width));
+      
+      return width + 'rpx';
+    },
     
     /**
      * 滚动到底部

+ 30 - 6
LiangZhiYUMao/pages/message/index.vue

@@ -115,7 +115,10 @@
               <!-- 会话内容 -->
               <view class="conversation-content">
                 <view class="content-top">
-                  <text class="user-name">{{ conv.targetUserName }}</text>
+                  <view class="user-info">
+                    <text class="user-name">{{ conv.targetUserName }}</text>
+                    <text v-if="conv.isMatchmaker" class="matchmaker-tag">红娘</text>
+                  </view>
                   <text class="last-time">{{formatTime(conv.lastMessageTime) }}</text>
                 </view>
                 <view class="content-bottom">
@@ -831,6 +834,9 @@ export default {
       // 检查是否在本地置顶列表中
       const isPinned = this.pinnedConversations.includes(timConv.conversationID) ? 1 : 0;
       
+      // 判断是否是红娘:ID 以 m_ 开头
+      const isMatchmaker = targetUserId.startsWith('m_');
+      
       return {
         id: timConv.conversationID,
         userId: this.userId,
@@ -842,7 +848,8 @@ export default {
         lastMessageTime: timConv.lastMessage?.lastTime ? timConv.lastMessage.lastTime * 1000 : Date.now(),
         unreadCount: timConv.unreadCount || 0,
         isOnline: false,
-        isPinned: isPinned
+        isPinned: isPinned,
+        isMatchmaker: isMatchmaker
       };
     },
     
@@ -1029,6 +1036,9 @@ export default {
         return;
       }
       
+      // 先保存引用,避免被 closeActionMenu 清空
+      const conversation = this.selectedConversation;
+      
       uni.showModal({
         title: '确认删除',
         content: '确定要删除该会话吗?',
@@ -1036,10 +1046,10 @@ export default {
           if (res.confirm) {
             try {
               // 调用 TIM SDK 删除会话
-              await timManager.tim.deleteConversation(this.selectedConversation.id);
+              await timManager.tim.deleteConversation(conversation.id);
               
               // 从列表中移除
-              const index = this.conversations.findIndex(c => c.id === this.selectedConversation.id);
+              const index = this.conversations.findIndex(c => c.id === conversation.id);
               if (index > -1) {
                 this.conversations.splice(index, 1);
               }
@@ -1063,10 +1073,10 @@ export default {
               });
             }
           }
+          // 在回调结束后关闭菜单
+          this.closeActionMenu();
         }
       });
-      
-      this.closeActionMenu();
     },
     
     /**
@@ -1694,12 +1704,26 @@ export default {
       justify-content: space-between;
       margin-bottom: 12rpx;
       
+      .user-info {
+        display: flex;
+        align-items: center;
+      }
+      
       .user-name {
         font-size: 32rpx;
         font-weight: 500;
         color: #333333;
       }
       
+      .matchmaker-tag {
+        background: linear-gradient(135deg, #ff6b6b, #ee5a24);
+        color: #fff;
+        font-size: 20rpx;
+        padding: 4rpx 12rpx;
+        border-radius: 6rpx;
+        margin-left: 10rpx;
+      }
+      
       .last-time {
         font-size: 24rpx;
         color: #999999;

+ 3 - 3
LiangZhiYUMao/pages/page3/page3.vue

@@ -1,13 +1,13 @@
 <template>
   <view class="content">
     <!-- 背景图片 -->
-    <image class="bg-image" src="/static/login-bg.png" mode="aspectFill"></image>
+    <image class="bg-image" src="http://115.190.125.125:9000/static-images/login-bg.png" mode="aspectFill"></image>
 
 
     <!-- 顶部标题区域 -->
     <view class="header-section">
       <!-- <view class="app-logo">🪶🪶</view> -->
-      <image class="app-logo" src="/static/logo.png" mode="widthFix"></image>
+      <image class="app-logo" src="http://115.190.125.125:9000/static-images/logo.png" mode="widthFix"></image>
       <!-- <view class="app-name">青鸾之恋</view> -->
       <view class="app-slogan">真诚相遇 · 携手一生</view>
     </view>
@@ -16,7 +16,7 @@
     <view class="login-box">
       <!-- 微信一键登录 -->
       <button class="wechat-login-btn" @click="login_zheshow">
-        <image class="wechat-icon" src="/static/wechat-icon.png" mode="aspectFit"></image>
+        <image class="wechat-icon" src="http://115.190.125.125:9000/static-images/wechat-icon.png" mode="aspectFit"></image>
         <text>微信一键登录</text>
       </button>
 

+ 7 - 5
LiangZhiYUMao/utils/tim-manager.js

@@ -146,10 +146,11 @@ class TIMManager {
         }
       };
       
+      // 使用V2接口,支持红娘消息(TIM用户ID可能是 m_xxx 格式)
       const syncData = {
         messageId: timMessage.ID,
-        fromUserId: timMessage.from,
-        toUserId: timMessage.to,
+        fromTimUserId: String(timMessage.from),  // 保持字符串格式,支持 m_xxx
+        toTimUserId: String(timMessage.to),      // 保持字符串格式,支持 m_xxx
         messageType: getMessageType(timMessage),
         content: getMessageContent(timMessage),
         sendTime: timMessage.time
@@ -183,9 +184,9 @@ class TIMManager {
         syncData.thumbnailUrl = imageInfo.imageUrl;
       }
       
-      // 调用后端同步接口
+      // 调用后端同步接口(V2版本,支持红娘消息)
       const res = await uni.request({
-        url: 'http://localhost:8083/api/chat/syncTIMMessage',
+        url: 'http://localhost:8083/api/chat/syncTIMMessageV2',
         method: 'POST',
         data: syncData,
         header: {
@@ -194,7 +195,8 @@ class TIMManager {
       });
       
       if (res[1].data.code === 200) {
-        console.log('✅ 接收消息已同步到MySQL:', timMessage.ID);
+        const msgType = res[1].data.type === 'matchmaker' ? '红娘消息' : '用户消息';
+        console.log(`✅ 接收消息已同步到MySQL (${msgType}):`, timMessage.ID);
       }
     } catch (error) {
       console.error('❌ 同步接收消息失败:', error);

+ 0 - 84
service/admin/src/main/java/com/zhentao/service/impl/AdminSecurityServiceImpl.java

@@ -1,84 +0,0 @@
-package com.zhentao.service.impl;
-
-import com.zhentao.entity.AdminUser;
-import com.zhentao.mapper.AdminUserMapper;
-import com.zhentao.service.AdminSecurityService;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.context.annotation.Primary;
-import org.springframework.stereotype.Service;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-/**
- * 管理员安全服务实现类(基于role字段,已废弃)
- * 注意:此实现已废弃,因为数据库表中没有role字段
- * 请使用 RolePermissionBasedAdminSecurityServiceImpl
- */
-@Service
-public class AdminSecurityServiceImpl implements AdminSecurityService {
-    
-    @Autowired
-    private AdminUserMapper adminUserMapper;
-    
-    @Override
-    public List<String> getUserPermissions(Integer userId) {
-        // 获取用户信息
-        AdminUser user = adminUserMapper.selectById(userId);
-        if (user == null) {
-            return new ArrayList<>();
-        }
-        
-        // 超级管理员拥有所有权限
-        if ("super".equals(user.getRole())) {
-            return Arrays.asList(
-                "user:view", "user:edit", "user:delete",
-                "dynamic:view", "dynamic:edit", "dynamic:delete",
-                "activity:view", "activity:edit", "activity:delete",
-                "course:view", "course:edit", "course:delete",
-                "matchmaker:view", "matchmaker:edit", "matchmaker:delete",
-                "vip:view", "vip:edit",
-                "report:view", "report:handle",
-                "announcement:view", "announcement:edit",
-                "banner:view", "banner:edit",
-                "admin:view", "admin:edit", "admin:delete"
-            );
-        }
-        
-        // 普通管理员权限
-        return Arrays.asList(
-            "user:view",
-            "dynamic:view", "dynamic:edit",
-            "activity:view", "activity:edit",
-            "course:view",
-            "matchmaker:view",
-            "report:view", "report:handle",
-            "announcement:view",
-            "banner:view"
-        );
-    }
-    
-    @Override
-    public List<String> getUserRoles(Integer userId) {
-        AdminUser user = adminUserMapper.selectById(userId);
-        if (user == null) {
-            return new ArrayList<>();
-        }
-        
-        List<String> roles = new ArrayList<>();
-        if ("super".equals(user.getRole())) {
-            roles.add("super_admin");
-            roles.add("admin");
-        } else {
-            roles.add("admin");
-        }
-        return roles;
-    }
-    
-    @Override
-    public boolean isSuperAdmin(Integer userId) {
-        AdminUser user = adminUserMapper.selectById(userId);
-        return user != null && "super".equals(user.getRole());
-    }
-}

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

@@ -3,7 +3,9 @@ package com.zhentao.controller;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.zhentao.entity.ChatConversation;
 import com.zhentao.entity.ChatMessage;
+import com.zhentao.entity.MatchmakerChatMessage;
 import com.zhentao.service.ChatMessageService;
+import com.zhentao.service.MatchmakerChatMessageService;
 import com.zhentao.service.OnlineUserService;
 import com.zhentao.service.UserVipService;
 import com.zhentao.utils.ContactFilter;
@@ -34,6 +36,10 @@ public class ChatController {
 
     @Autowired
     private UserVipService userVipService;
+    
+    @Autowired
+    private MatchmakerChatMessageService matchmakerChatMessageService;
+    
     @GetMapping("/test")
     public String test() {
         return "ChatController is working!";
@@ -356,6 +362,198 @@ public class ChatController {
         return result;
     }
     
+    /**
+     * 同步TIM消息到MySQL(支持红娘消息)
+     * POST /api/chat/syncTIMMessageV2
+     * Body: {
+     *   "messageId": "xxx",
+     *   "fromTimUserId": "m_123" 或 "456",  // TIM用户ID,红娘以m_开头
+     *   "toTimUserId": "456" 或 "m_123",    // TIM用户ID
+     *   "messageType": 1,
+     *   "content": "消息内容",
+     *   "sendTime": 1234567890,
+     *   "mediaUrl": "xxx" (可选)
+     * }
+     */
+    @PostMapping("/syncTIMMessageV2")
+    public Map<String, Object> syncTIMMessageV2(@RequestBody Map<String, Object> params) {
+        Map<String, Object> result = new HashMap<>();
+        
+        try {
+            System.out.println("🔄 开始同步TIM消息到MySQL(V2版本)...");
+            
+            String messageId = params.get("messageId").toString();
+            String fromTimUserId = params.get("fromTimUserId").toString();
+            String toTimUserId = params.get("toTimUserId").toString();
+            Integer messageType = Integer.valueOf(params.get("messageType").toString());
+            String content = params.get("content") != null ? params.get("content").toString() : "";
+            String mediaUrl = params.containsKey("mediaUrl") ? params.get("mediaUrl").toString() : null;
+            
+            // 解析发送时间
+            java.util.Date sendTime;
+            if (params.containsKey("sendTime")) {
+                long sendTimeMs = Long.parseLong(params.get("sendTime").toString());
+                // TIM返回的是秒级时间戳,需要转换为毫秒
+                if (sendTimeMs < 10000000000L) {
+                    sendTimeMs = sendTimeMs * 1000;
+                }
+                sendTime = new java.util.Date(sendTimeMs);
+            } else {
+                sendTime = new java.util.Date();
+            }
+            
+            // 判断是否是红娘相关消息
+            boolean isFromMatchmaker = MatchmakerChatMessage.isMatchmaker(fromTimUserId);
+            boolean isToMatchmaker = MatchmakerChatMessage.isMatchmaker(toTimUserId);
+            
+            if (isFromMatchmaker || isToMatchmaker) {
+                // 红娘消息,保存到 matchmaker_chat_message 表
+                System.out.println("📌 检测到红娘消息,保存到红娘消息表...");
+                
+                MatchmakerChatMessage matchmakerMessage = matchmakerChatMessageService.saveFromTimMessage(
+                    fromTimUserId, toTimUserId, messageType, content, mediaUrl, messageId
+                );
+                
+                if (matchmakerMessage != null) {
+                    // 更新发送时间
+                    matchmakerMessage.setSendTime(sendTime);
+                    
+                    System.out.println("✅ 红娘消息已同步: " + messageId);
+                    result.put("code", 200);
+                    result.put("message", "红娘消息同步成功");
+                    result.put("data", messageId);
+                    result.put("type", "matchmaker");
+                } else {
+                    result.put("code", 400);
+                    result.put("message", "无法解析红娘消息");
+                }
+            } else {
+                // 普通用户消息,保存到 chat_message 表
+                System.out.println("📌 普通用户消息,保存到用户消息表...");
+                
+                Long fromUserId = Long.valueOf(fromTimUserId);
+                Long toUserId = Long.valueOf(toTimUserId);
+                
+                // 检查消息是否已存在
+                ChatMessage existingMessage = chatMessageService.getMessageByMessageId(messageId);
+                if (existingMessage != null) {
+                    System.out.println("⚠️ 消息已存在,跳过: " + messageId);
+                    result.put("code", 200);
+                    result.put("message", "消息已存在");
+                    result.put("data", null);
+                    return result;
+                }
+                
+                // 构建ChatMessage实体
+                ChatMessage chatMessage = new ChatMessage();
+                chatMessage.setMessageId(messageId);
+                chatMessage.setFromUserId(fromUserId);
+                chatMessage.setToUserId(toUserId);
+                chatMessage.setMessageType(messageType);
+                chatMessage.setContent(content);
+                chatMessage.setMediaUrl(mediaUrl);
+                chatMessage.setSendTime(sendTime);
+                chatMessage.setSendStatus(2); // 已送达
+                chatMessage.setDeliverTime(new java.util.Date());
+                
+                // 会话ID
+                String conversationId = fromUserId < toUserId ? 
+                    fromUserId + "_" + toUserId : toUserId + "_" + fromUserId;
+                chatMessage.setConversationId(conversationId);
+                
+                // 设置创建时间和更新时间
+                java.util.Date now = new java.util.Date();
+                chatMessage.setCreateTime(now);
+                chatMessage.setUpdateTime(now);
+                
+                // 保存到MySQL
+                chatMessageService.saveChatMessage(chatMessage);
+                
+                // 更新会话表
+                chatMessageService.updateOrCreateConversation(
+                    fromUserId, toUserId, content, messageType, sendTime
+                );
+                
+                System.out.println("✅ 用户消息已同步: " + messageId);
+                result.put("code", 200);
+                result.put("message", "用户消息同步成功");
+                result.put("data", messageId);
+                result.put("type", "user");
+            }
+            
+        } catch (Exception e) {
+            e.printStackTrace();
+            System.err.println("❌ 同步TIM消息失败: " + e.getMessage());
+            result.put("code", 500);
+            result.put("message", "同步失败: " + e.getMessage());
+        }
+        
+        return result;
+    }
+    
+    /**
+     * 获取红娘与用户的聊天记录
+     * GET /api/chat/matchmakerMessages?matchmakerId=123&userId=456&page=0&size=20
+     */
+    @GetMapping("/matchmakerMessages")
+    public Map<String, Object> getMatchmakerMessages(
+            @RequestParam Long matchmakerId,
+            @RequestParam Long userId,
+            @RequestParam(defaultValue = "0") int page,
+            @RequestParam(defaultValue = "20") int size) {
+        
+        Map<String, Object> result = new HashMap<>();
+        
+        try {
+            IPage<MatchmakerChatMessage> messages = matchmakerChatMessageService.getConversationMessages(
+                matchmakerId, userId, page, size
+            );
+            
+            result.put("code", 200);
+            result.put("message", "查询成功");
+            result.put("data", messages.getRecords());
+            result.put("total", messages.getTotal());
+            result.put("pages", messages.getPages());
+            result.put("current", messages.getCurrent());
+            
+        } catch (Exception e) {
+            e.printStackTrace();
+            result.put("code", 500);
+            result.put("message", "查询失败: " + e.getMessage());
+        }
+        
+        return result;
+    }
+    
+    /**
+     * 标记红娘消息为已读
+     * POST /api/chat/markMatchmakerMessagesRead
+     * Body: {"matchmakerId": 123, "userId": 456, "readerType": 1}
+     * readerType: 1-用户阅读 2-红娘阅读
+     */
+    @PostMapping("/markMatchmakerMessagesRead")
+    public Map<String, Object> markMatchmakerMessagesRead(@RequestBody Map<String, Object> params) {
+        Map<String, Object> result = new HashMap<>();
+        
+        try {
+            Long matchmakerId = Long.valueOf(params.get("matchmakerId").toString());
+            Long userId = Long.valueOf(params.get("userId").toString());
+            Integer readerType = Integer.valueOf(params.get("readerType").toString());
+            
+            matchmakerChatMessageService.markMessagesAsRead(matchmakerId, userId, readerType);
+            
+            result.put("code", 200);
+            result.put("message", "标记成功");
+            
+        } catch (Exception e) {
+            e.printStackTrace();
+            result.put("code", 500);
+            result.put("message", "标记失败: " + e.getMessage());
+        }
+        
+        return result;
+    }
+    
     /**
      * 健康检查
      * GET /api/chat/health

+ 172 - 0
service/websocket/src/main/java/com/zhentao/entity/MatchmakerChatMessage.java

@@ -0,0 +1,172 @@
+package com.zhentao.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 红娘与用户聊天消息实体类
+ * 存储在MySQL中
+ */
+@Data
+@TableName("matchmaker_chat_message")
+public class MatchmakerChatMessage implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 消息主键ID
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 消息唯一ID(UUID)
+     */
+    private String messageId;
+
+    /**
+     * 会话ID(格式:m_红娘ID_用户ID)
+     */
+    private String conversationId;
+
+    /**
+     * 红娘ID
+     */
+    private Long matchmakerId;
+
+    /**
+     * 用户ID
+     */
+    private Long userId;
+
+    /**
+     * 发送方类型:1-用户发送 2-红娘发送
+     */
+    private Integer senderType;
+
+    /**
+     * 消息类型:1-文本 2-图片 3-语音 4-视频 5-文件
+     */
+    private Integer messageType;
+
+    /**
+     * 消息内容(文本内容或媒体描述)
+     */
+    private String content;
+
+    /**
+     * 媒体文件URL
+     */
+    private String mediaUrl;
+
+    /**
+     * 缩略图URL
+     */
+    private String thumbnailUrl;
+
+    /**
+     * 文件大小(字节)
+     */
+    private Long mediaSize;
+
+    /**
+     * 语音/视频时长(秒)
+     */
+    private Integer duration;
+
+    /**
+     * 发送状态:1-发送中 2-已送达 3-已读 4-失败
+     */
+    private Integer sendStatus;
+
+    /**
+     * 发送时间
+     */
+    private Date sendTime;
+
+    /**
+     * 送达时间
+     */
+    private Date deliverTime;
+
+    /**
+     * 阅读时间
+     */
+    private Date readTime;
+
+    /**
+     * 是否已撤回(0:否 1:是)
+     */
+    private Integer isRecalled;
+
+    /**
+     * 撤回时间
+     */
+    private Date recallTime;
+
+    /**
+     * 额外数据(JSON格式字符串)
+     */
+    private String extraData;
+
+    /**
+     * 是否删除
+     */
+    @TableLogic
+    private Integer isDeleted;
+
+    /**
+     * 创建时间
+     */
+    @TableField(fill = FieldFill.INSERT)
+    private Date createTime;
+
+    /**
+     * 更新时间
+     */
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private Date updateTime;
+
+    /**
+     * 发送方类型常量
+     */
+    public static final int SENDER_TYPE_USER = 1;       // 用户发送
+    public static final int SENDER_TYPE_MATCHMAKER = 2; // 红娘发送
+
+    /**
+     * 生成会话ID
+     * 格式:m_红娘ID_用户ID
+     */
+    public static String generateConversationId(Long matchmakerId, Long userId) {
+        return "m_" + matchmakerId + "_" + userId;
+    }
+
+    /**
+     * 从TIM会话ID解析红娘ID
+     * TIM会话ID格式:C2Cm_红娘ID 或 m_红娘ID
+     */
+    public static Long parseMatchmakerIdFromTimId(String timUserId) {
+        if (timUserId == null) return null;
+        String id = timUserId.replace("C2C", "");
+        if (id.startsWith("m_")) {
+            try {
+                return Long.parseLong(id.substring(2));
+            } catch (NumberFormatException e) {
+                return null;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 判断TIM用户ID是否是红娘
+     */
+    public static boolean isMatchmaker(String timUserId) {
+        if (timUserId == null) return false;
+        String id = timUserId.replace("C2C", "");
+        return id.startsWith("m_");
+    }
+}

+ 172 - 0
service/websocket/src/main/java/com/zhentao/entity/MatchmakerChatMessageMongo.java

@@ -0,0 +1,172 @@
+package com.zhentao.entity;
+
+import lombok.Data;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.mongodb.core.index.CompoundIndex;
+import org.springframework.data.mongodb.core.index.CompoundIndexes;
+import org.springframework.data.mongodb.core.index.Indexed;
+import org.springframework.data.mongodb.core.mapping.Document;
+import org.springframework.data.mongodb.core.mapping.Field;
+
+import java.util.Date;
+
+/**
+ * MongoDB红娘与用户聊天消息实体(NoSQL存储)
+ * 用于大数据分析、历史存档、数据挖掘
+ */
+@Data
+@Document(collection = "matchmaker_chat_messages")
+@CompoundIndexes({
+    @CompoundIndex(name = "idx_conversation_time", def = "{'conversation_id': 1, 'send_time': -1}"),
+    @CompoundIndex(name = "idx_matchmaker_user", def = "{'matchmaker_id': 1, 'user_id': 1}")
+})
+public class MatchmakerChatMessageMongo {
+    
+    /**
+     * MongoDB主键(自动生成)
+     */
+    @Id
+    private String id;
+    
+    /**
+     * 消息ID(来自腾讯云IM)
+     */
+    @Field("message_id")
+    @Indexed(unique = true)
+    private String messageId;
+    
+    /**
+     * 会话ID(格式:m_红娘ID_用户ID)
+     */
+    @Field("conversation_id")
+    @Indexed
+    private String conversationId;
+    
+    /**
+     * 红娘ID
+     */
+    @Field("matchmaker_id")
+    @Indexed
+    private Long matchmakerId;
+    
+    /**
+     * 用户ID
+     */
+    @Field("user_id")
+    @Indexed
+    private Long userId;
+    
+    /**
+     * 发送方类型:1-用户发送 2-红娘发送
+     */
+    @Field("sender_type")
+    private Integer senderType;
+    
+    /**
+     * 消息类型
+     * 1: TEXT-文本消息
+     * 2: IMAGE-图片消息
+     * 3: VOICE-语音消息
+     * 4: VIDEO-视频消息
+     * 5: FILE-文件消息
+     */
+    @Field("message_type")
+    private Integer messageType;
+    
+    /**
+     * 消息内容
+     */
+    @Field("content")
+    private String content;
+    
+    /**
+     * 媒体文件URL(图片、语音、视频、文件)
+     */
+    @Field("media_url")
+    private String mediaUrl;
+    
+    /**
+     * 缩略图URL(图片、视频)
+     */
+    @Field("thumbnail_url")
+    private String thumbnailUrl;
+    
+    /**
+     * 媒体文件大小(字节)
+     */
+    @Field("media_size")
+    private Long mediaSize;
+    
+    /**
+     * 媒体时长(秒,用于语音和视频)
+     */
+    @Field("duration")
+    private Integer duration;
+    
+    /**
+     * 发送状态
+     * 1: SENDING-发送中
+     * 2: DELIVERED-已送达
+     * 3: READ-已读
+     * 4: FAILED-发送失败
+     */
+    @Field("send_status")
+    private Integer sendStatus;
+    
+    /**
+     * 发送时间
+     */
+    @Field("send_time")
+    @Indexed
+    private Date sendTime;
+    
+    /**
+     * 送达时间
+     */
+    @Field("deliver_time")
+    private Date deliverTime;
+    
+    /**
+     * 已读时间
+     */
+    @Field("read_time")
+    private Date readTime;
+    
+    /**
+     * 是否已撤回
+     */
+    @Field("is_recalled")
+    private Integer isRecalled;
+    
+    /**
+     * 撤回时间
+     */
+    @Field("recall_time")
+    private Date recallTime;
+    
+    /**
+     * 消息来源
+     * TIM: 腾讯云IM同步
+     * MANUAL: 手动创建
+     */
+    @Field("source")
+    private String source;
+    
+    /**
+     * 创建时间
+     */
+    @Field("created_at")
+    private Date createdAt;
+    
+    /**
+     * 更新时间
+     */
+    @Field("updated_at")
+    private Date updatedAt;
+    
+    /**
+     * 扩展字段(JSON格式,用于存储其他信息)
+     */
+    @Field("extra_data")
+    private String extraData;
+}

+ 89 - 0
service/websocket/src/main/java/com/zhentao/repository/MatchmakerChatMessageMapper.java

@@ -0,0 +1,89 @@
+package com.zhentao.repository;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zhentao.entity.MatchmakerChatMessage;
+import org.apache.ibatis.annotations.*;
+
+import java.util.List;
+
+/**
+ * 红娘与用户聊天消息Mapper
+ */
+@Mapper
+public interface MatchmakerChatMessageMapper extends BaseMapper<MatchmakerChatMessage> {
+
+    /**
+     * 根据会话ID分页查询消息
+     */
+    @Select("SELECT * FROM matchmaker_chat_message WHERE conversation_id = #{conversationId} AND is_deleted = 0 ORDER BY send_time DESC")
+    IPage<MatchmakerChatMessage> selectByConversationId(Page<MatchmakerChatMessage> page, @Param("conversationId") String conversationId);
+
+    /**
+     * 根据消息ID查询
+     */
+    @Select("SELECT * FROM matchmaker_chat_message WHERE message_id = #{messageId} AND is_deleted = 0")
+    MatchmakerChatMessage selectByMessageId(@Param("messageId") String messageId);
+
+    /**
+     * 查询红娘的所有会话消息(分页)
+     */
+    @Select("SELECT * FROM matchmaker_chat_message WHERE matchmaker_id = #{matchmakerId} AND is_deleted = 0 ORDER BY send_time DESC")
+    IPage<MatchmakerChatMessage> selectByMatchmakerId(Page<MatchmakerChatMessage> page, @Param("matchmakerId") Long matchmakerId);
+
+    /**
+     * 查询用户与红娘的所有会话消息(分页)
+     */
+    @Select("SELECT * FROM matchmaker_chat_message WHERE user_id = #{userId} AND is_deleted = 0 ORDER BY send_time DESC")
+    IPage<MatchmakerChatMessage> selectByUserId(Page<MatchmakerChatMessage> page, @Param("userId") Long userId);
+
+    /**
+     * 查询红娘与特定用户的聊天记录(分页)
+     */
+    @Select("SELECT * FROM matchmaker_chat_message WHERE matchmaker_id = #{matchmakerId} AND user_id = #{userId} AND is_deleted = 0 ORDER BY send_time DESC")
+    IPage<MatchmakerChatMessage> selectByMatchmakerAndUser(Page<MatchmakerChatMessage> page, 
+                                                           @Param("matchmakerId") Long matchmakerId, 
+                                                           @Param("userId") Long userId);
+
+    /**
+     * 查询离线消息(用户未读的消息)
+     */
+    @Select("SELECT * FROM matchmaker_chat_message WHERE user_id = #{userId} AND sender_type = 2 AND send_status < 3 AND is_deleted = 0 ORDER BY send_time ASC LIMIT #{limit}")
+    List<MatchmakerChatMessage> selectOfflineMessagesForUser(@Param("userId") Long userId, @Param("limit") int limit);
+
+    /**
+     * 查询离线消息(红娘未读的消息)
+     */
+    @Select("SELECT * FROM matchmaker_chat_message WHERE matchmaker_id = #{matchmakerId} AND sender_type = 1 AND send_status < 3 AND is_deleted = 0 ORDER BY send_time ASC LIMIT #{limit}")
+    List<MatchmakerChatMessage> selectOfflineMessagesForMatchmaker(@Param("matchmakerId") Long matchmakerId, @Param("limit") int limit);
+
+    /**
+     * 更新消息为已读
+     */
+    @Update("UPDATE matchmaker_chat_message SET send_status = 3, read_time = NOW(), update_time = NOW() " +
+            "WHERE matchmaker_id = #{matchmakerId} AND user_id = #{userId} AND sender_type = #{senderType} AND send_status < 3 AND is_deleted = 0")
+    int updateMessagesToRead(@Param("matchmakerId") Long matchmakerId, 
+                             @Param("userId") Long userId, 
+                             @Param("senderType") Integer senderType);
+
+    /**
+     * 撤回消息
+     */
+    @Update("UPDATE matchmaker_chat_message SET is_recalled = 1, recall_time = NOW(), update_time = NOW() WHERE message_id = #{messageId}")
+    int recallMessage(@Param("messageId") String messageId);
+
+    /**
+     * 统计红娘今日消息数
+     */
+    @Select("SELECT COUNT(*) FROM matchmaker_chat_message WHERE matchmaker_id = #{matchmakerId} AND sender_type = 2 AND DATE(send_time) = CURDATE() AND is_deleted = 0")
+    int countMatchmakerTodayMessages(@Param("matchmakerId") Long matchmakerId);
+
+    /**
+     * 统计红娘与用户的未读消息数
+     */
+    @Select("SELECT COUNT(*) FROM matchmaker_chat_message WHERE matchmaker_id = #{matchmakerId} AND user_id = #{userId} AND sender_type = #{senderType} AND send_status < 3 AND is_deleted = 0")
+    int countUnreadMessages(@Param("matchmakerId") Long matchmakerId, 
+                            @Param("userId") Long userId, 
+                            @Param("senderType") Integer senderType);
+}

+ 76 - 0
service/websocket/src/main/java/com/zhentao/repository/MatchmakerChatMessageMongoRepository.java

@@ -0,0 +1,76 @@
+package com.zhentao.repository;
+
+import com.zhentao.entity.MatchmakerChatMessageMongo;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.mongodb.repository.MongoRepository;
+import org.springframework.data.mongodb.repository.Query;
+import org.springframework.stereotype.Repository;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * 红娘与用户聊天消息MongoDB仓库
+ */
+@Repository
+public interface MatchmakerChatMessageMongoRepository extends MongoRepository<MatchmakerChatMessageMongo, String> {
+
+    /**
+     * 根据消息ID查询
+     */
+    Optional<MatchmakerChatMessageMongo> findByMessageId(String messageId);
+
+    /**
+     * 根据会话ID查询消息(分页,按时间倒序)
+     */
+    Page<MatchmakerChatMessageMongo> findByConversationIdOrderBySendTimeDesc(String conversationId, Pageable pageable);
+
+    /**
+     * 根据红娘ID查询所有消息(分页)
+     */
+    Page<MatchmakerChatMessageMongo> findByMatchmakerIdOrderBySendTimeDesc(Long matchmakerId, Pageable pageable);
+
+    /**
+     * 根据用户ID查询所有消息(分页)
+     */
+    Page<MatchmakerChatMessageMongo> findByUserIdOrderBySendTimeDesc(Long userId, Pageable pageable);
+
+    /**
+     * 根据红娘ID和用户ID查询消息(分页)
+     */
+    Page<MatchmakerChatMessageMongo> findByMatchmakerIdAndUserIdOrderBySendTimeDesc(Long matchmakerId, Long userId, Pageable pageable);
+
+    /**
+     * 查询某时间段内的消息
+     */
+    @Query("{'conversation_id': ?0, 'send_time': {$gte: ?1, $lte: ?2}}")
+    List<MatchmakerChatMessageMongo> findByConversationIdAndSendTimeBetween(String conversationId, Date startTime, Date endTime);
+
+    /**
+     * 统计红娘的消息数量
+     */
+    long countByMatchmakerIdAndSenderType(Long matchmakerId, Integer senderType);
+
+    /**
+     * 统计用户的消息数量
+     */
+    long countByUserIdAndSenderType(Long userId, Integer senderType);
+
+    /**
+     * 查询未读消息
+     */
+    List<MatchmakerChatMessageMongo> findByMatchmakerIdAndUserIdAndSenderTypeAndSendStatusLessThan(
+            Long matchmakerId, Long userId, Integer senderType, Integer sendStatus);
+
+    /**
+     * 根据消息ID删除
+     */
+    void deleteByMessageId(String messageId);
+
+    /**
+     * 检查消息是否存在
+     */
+    boolean existsByMessageId(String messageId);
+}

+ 320 - 0
service/websocket/src/main/java/com/zhentao/service/MatchmakerChatMessageService.java

@@ -0,0 +1,320 @@
+package com.zhentao.service;
+
+import cn.hutool.json.JSONUtil;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zhentao.entity.MatchmakerChatMessage;
+import com.zhentao.entity.MatchmakerChatMessageMongo;
+import com.zhentao.repository.MatchmakerChatMessageMapper;
+import com.zhentao.repository.MatchmakerChatMessageMongoRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.dao.DuplicateKeyException;
+import org.springframework.stereotype.Service;
+
+import java.util.Date;
+import java.util.UUID;
+
+/**
+ * 红娘与用户聊天消息服务
+ * 同时存储到MySQL和MongoDB
+ */
+@Service
+public class MatchmakerChatMessageService {
+
+    @Autowired
+    private MatchmakerChatMessageMapper matchmakerChatMessageMapper;
+
+    @Autowired(required = false)
+    private MatchmakerChatMessageMongoRepository mongoRepository;
+
+    /**
+     * 保存红娘与用户的聊天消息
+     * 同时存储到MySQL和MongoDB
+     * 
+     * @param matchmakerId 红娘ID
+     * @param userId 用户ID
+     * @param senderType 发送方类型:1-用户发送 2-红娘发送
+     * @param messageType 消息类型:1-文本 2-图片 3-语音 4-视频 5-文件
+     * @param content 消息内容
+     * @param mediaUrl 媒体文件URL(可选)
+     * @param timMessageId TIM消息ID(可选,如果没有则自动生成)
+     * @return 保存的消息实体
+     */
+    public MatchmakerChatMessage saveMessage(Long matchmakerId, Long userId, Integer senderType,
+                                              Integer messageType, String content, String mediaUrl,
+                                              String timMessageId) {
+        // 构建消息实体
+        MatchmakerChatMessage message = new MatchmakerChatMessage();
+        message.setMessageId(timMessageId != null ? timMessageId : UUID.randomUUID().toString());
+        message.setConversationId(MatchmakerChatMessage.generateConversationId(matchmakerId, userId));
+        message.setMatchmakerId(matchmakerId);
+        message.setUserId(userId);
+        message.setSenderType(senderType);
+        message.setMessageType(messageType);
+        message.setContent(content);
+        message.setMediaUrl(mediaUrl);
+        message.setSendStatus(1); // 发送中
+        message.setSendTime(new Date());
+        message.setIsRecalled(0);
+        message.setIsDeleted(0);
+        message.setCreateTime(new Date());
+        message.setUpdateTime(new Date());
+
+        // 保存到MySQL和MongoDB
+        return saveMessage(message);
+    }
+
+    /**
+     * 保存消息(完整实体)
+     * 同时存储到MySQL和MongoDB
+     * 
+     * @param message 消息实体
+     * @return 保存的消息实体
+     */
+    public MatchmakerChatMessage saveMessage(MatchmakerChatMessage message) {
+        try {
+            // 1. 保存到MySQL
+            matchmakerChatMessageMapper.insert(message);
+            System.out.println("✅ 红娘消息已保存到MySQL: " + message.getMessageId());
+
+            // 2. 同时保存到MongoDB
+            saveToMongoDB(message);
+
+            return message;
+        } catch (DuplicateKeyException e) {
+            // 消息已存在,忽略重复插入(幂等性保证)
+            System.out.println("⚠️ 红娘消息已存在,跳过保存: " + message.getMessageId());
+            return matchmakerChatMessageMapper.selectByMessageId(message.getMessageId());
+        } catch (Exception e) {
+            System.err.println("❌ 保存红娘消息失败: " + message.getMessageId() + ", 错误: " + e.getMessage());
+            throw e;
+        }
+    }
+
+    /**
+     * 保存消息到MongoDB
+     */
+    private void saveToMongoDB(MatchmakerChatMessage message) {
+        if (mongoRepository == null) {
+            System.out.println("⚠️ MongoDB未配置,跳过MongoDB存储");
+            return;
+        }
+
+        try {
+            // 检查是否已存在
+            if (mongoRepository.existsByMessageId(message.getMessageId())) {
+                System.out.println("⚠️ MongoDB中消息已存在,跳过: " + message.getMessageId());
+                return;
+            }
+
+            // 转换为MongoDB实体
+            MatchmakerChatMessageMongo mongoMessage = convertToMongoEntity(message);
+
+            // 保存到MongoDB
+            mongoRepository.save(mongoMessage);
+            System.out.println("✅ 红娘消息已同步到MongoDB: " + message.getMessageId());
+
+        } catch (Exception e) {
+            // MongoDB存储失败不影响主流程
+            System.err.println("⚠️ MongoDB存储失败(不影响主流程): " + e.getMessage());
+        }
+    }
+
+    /**
+     * 转换为MongoDB实体
+     */
+    private MatchmakerChatMessageMongo convertToMongoEntity(MatchmakerChatMessage message) {
+        MatchmakerChatMessageMongo mongoMessage = new MatchmakerChatMessageMongo();
+        mongoMessage.setMessageId(message.getMessageId());
+        mongoMessage.setConversationId(message.getConversationId());
+        mongoMessage.setMatchmakerId(message.getMatchmakerId());
+        mongoMessage.setUserId(message.getUserId());
+        mongoMessage.setSenderType(message.getSenderType());
+        mongoMessage.setMessageType(message.getMessageType());
+        mongoMessage.setContent(message.getContent());
+        mongoMessage.setMediaUrl(message.getMediaUrl());
+        mongoMessage.setThumbnailUrl(message.getThumbnailUrl());
+        mongoMessage.setMediaSize(message.getMediaSize());
+        mongoMessage.setDuration(message.getDuration());
+        mongoMessage.setSendStatus(message.getSendStatus());
+        mongoMessage.setSendTime(message.getSendTime());
+        mongoMessage.setDeliverTime(message.getDeliverTime());
+        mongoMessage.setReadTime(message.getReadTime());
+        mongoMessage.setIsRecalled(message.getIsRecalled());
+        mongoMessage.setRecallTime(message.getRecallTime());
+        mongoMessage.setSource("TIM");
+        mongoMessage.setCreatedAt(new Date());
+        mongoMessage.setUpdatedAt(new Date());
+        mongoMessage.setExtraData(message.getExtraData());
+        return mongoMessage;
+    }
+
+    /**
+     * 从TIM消息同步保存
+     * 根据发送者和接收者ID判断是用户发送还是红娘发送
+     * 
+     * @param fromTimUserId 发送者TIM用户ID(如 "m_123" 或 "456")
+     * @param toTimUserId 接收者TIM用户ID(如 "m_123" 或 "456")
+     * @param messageType 消息类型
+     * @param content 消息内容
+     * @param mediaUrl 媒体URL
+     * @param timMessageId TIM消息ID
+     * @return 保存的消息实体,如果不是红娘相关消息则返回null
+     */
+    public MatchmakerChatMessage saveFromTimMessage(String fromTimUserId, String toTimUserId,
+                                                     Integer messageType, String content,
+                                                     String mediaUrl, String timMessageId) {
+        Long matchmakerId = null;
+        Long userId = null;
+        Integer senderType = null;
+
+        // 判断发送者是否是红娘
+        if (MatchmakerChatMessage.isMatchmaker(fromTimUserId)) {
+            // 红娘发送给用户
+            matchmakerId = MatchmakerChatMessage.parseMatchmakerIdFromTimId(fromTimUserId);
+            try {
+                userId = Long.parseLong(toTimUserId.replace("C2C", ""));
+            } catch (NumberFormatException e) {
+                System.err.println("❌ 无法解析用户ID: " + toTimUserId);
+                return null;
+            }
+            senderType = MatchmakerChatMessage.SENDER_TYPE_MATCHMAKER;
+        } else if (MatchmakerChatMessage.isMatchmaker(toTimUserId)) {
+            // 用户发送给红娘
+            matchmakerId = MatchmakerChatMessage.parseMatchmakerIdFromTimId(toTimUserId);
+            try {
+                userId = Long.parseLong(fromTimUserId.replace("C2C", ""));
+            } catch (NumberFormatException e) {
+                System.err.println("❌ 无法解析用户ID: " + fromTimUserId);
+                return null;
+            }
+            senderType = MatchmakerChatMessage.SENDER_TYPE_USER;
+        } else {
+            // 不是红娘相关消息
+            return null;
+        }
+
+        if (matchmakerId == null || userId == null) {
+            System.err.println("❌ 无法解析红娘或用户ID");
+            return null;
+        }
+
+        // 保存消息
+        return saveMessage(matchmakerId, userId, senderType, messageType, content, mediaUrl, timMessageId);
+    }
+
+    /**
+     * 查询红娘与用户的聊天记录(分页)
+     */
+    public IPage<MatchmakerChatMessage> getConversationMessages(Long matchmakerId, Long userId, int page, int size) {
+        String conversationId = MatchmakerChatMessage.generateConversationId(matchmakerId, userId);
+        Page<MatchmakerChatMessage> pageParam = new Page<>(page + 1, size);
+        return matchmakerChatMessageMapper.selectByConversationId(pageParam, conversationId);
+    }
+
+    /**
+     * 查询红娘的所有聊天记录(分页)
+     */
+    public IPage<MatchmakerChatMessage> getMatchmakerMessages(Long matchmakerId, int page, int size) {
+        Page<MatchmakerChatMessage> pageParam = new Page<>(page + 1, size);
+        return matchmakerChatMessageMapper.selectByMatchmakerId(pageParam, matchmakerId);
+    }
+
+    /**
+     * 查询用户与红娘的所有聊天记录(分页)
+     */
+    public IPage<MatchmakerChatMessage> getUserMessages(Long userId, int page, int size) {
+        Page<MatchmakerChatMessage> pageParam = new Page<>(page + 1, size);
+        return matchmakerChatMessageMapper.selectByUserId(pageParam, userId);
+    }
+
+    /**
+     * 根据消息ID查询
+     */
+    public MatchmakerChatMessage getByMessageId(String messageId) {
+        return matchmakerChatMessageMapper.selectByMessageId(messageId);
+    }
+
+    /**
+     * 标记消息为已读
+     * 
+     * @param matchmakerId 红娘ID
+     * @param userId 用户ID
+     * @param readerType 阅读者类型:1-用户阅读(红娘发送的消息) 2-红娘阅读(用户发送的消息)
+     */
+    public void markMessagesAsRead(Long matchmakerId, Long userId, Integer readerType) {
+        // 如果是用户阅读,则标记红娘发送的消息为已读
+        // 如果是红娘阅读,则标记用户发送的消息为已读
+        Integer senderType = (readerType == 1) ? MatchmakerChatMessage.SENDER_TYPE_MATCHMAKER 
+                                                : MatchmakerChatMessage.SENDER_TYPE_USER;
+        
+        matchmakerChatMessageMapper.updateMessagesToRead(matchmakerId, userId, senderType);
+        System.out.println("✅ 已标记消息为已读: 红娘=" + matchmakerId + ", 用户=" + userId + ", 发送方=" + senderType);
+    }
+
+    /**
+     * 撤回消息
+     */
+    public boolean recallMessage(String messageId, String operatorTimId) {
+        MatchmakerChatMessage message = matchmakerChatMessageMapper.selectByMessageId(messageId);
+        if (message == null) {
+            System.err.println("❌ 消息不存在: " + messageId);
+            return false;
+        }
+
+        // 验证操作者是否是发送者
+        boolean isMatchmaker = MatchmakerChatMessage.isMatchmaker(operatorTimId);
+        if (isMatchmaker) {
+            Long operatorId = MatchmakerChatMessage.parseMatchmakerIdFromTimId(operatorTimId);
+            if (!message.getMatchmakerId().equals(operatorId) || 
+                message.getSenderType() != MatchmakerChatMessage.SENDER_TYPE_MATCHMAKER) {
+                System.err.println("❌ 只能撤回自己发送的消息");
+                return false;
+            }
+        } else {
+            try {
+                Long operatorId = Long.parseLong(operatorTimId.replace("C2C", ""));
+                if (!message.getUserId().equals(operatorId) || 
+                    message.getSenderType() != MatchmakerChatMessage.SENDER_TYPE_USER) {
+                    System.err.println("❌ 只能撤回自己发送的消息");
+                    return false;
+                }
+            } catch (NumberFormatException e) {
+                System.err.println("❌ 无法解析操作者ID: " + operatorTimId);
+                return false;
+            }
+        }
+
+        // 检查时间限制(2分钟内)
+        long diff = System.currentTimeMillis() - message.getSendTime().getTime();
+        if (diff > 2 * 60 * 1000) {
+            System.err.println("❌ 超过撤回时间限制");
+            return false;
+        }
+
+        // 执行撤回
+        matchmakerChatMessageMapper.recallMessage(messageId);
+        System.out.println("✅ 消息已撤回: " + messageId);
+        return true;
+    }
+
+    /**
+     * 统计红娘今日消息数
+     */
+    public int countMatchmakerTodayMessages(Long matchmakerId) {
+        return matchmakerChatMessageMapper.countMatchmakerTodayMessages(matchmakerId);
+    }
+
+    /**
+     * 统计未读消息数
+     * 
+     * @param matchmakerId 红娘ID
+     * @param userId 用户ID
+     * @param readerType 阅读者类型:1-用户(统计红娘发送的未读) 2-红娘(统计用户发送的未读)
+     */
+    public int countUnreadMessages(Long matchmakerId, Long userId, Integer readerType) {
+        Integer senderType = (readerType == 1) ? MatchmakerChatMessage.SENDER_TYPE_MATCHMAKER 
+                                                : MatchmakerChatMessage.SENDER_TYPE_USER;
+        return matchmakerChatMessageMapper.countUnreadMessages(matchmakerId, userId, senderType);
+    }
+}