18 Commits 517329823a ... c2afd0c985

Autore SHA1 Messaggio Data
  yuxy c2afd0c985 Merge remote-tracking branch 'origin/test_dev' into test_dev 1 mese fa
  yuxy 551574b7a8 Merge branch 'yxy' into test_dev 1 mese fa
  YH_0525 a32ef62180 上传平台未注册线索才可以获得积分 上传已注册的资源没有积分 1 mese fa
  mazhenhang f5f7cbb2a7 报错修复 1 mese fa
  mazhenhang 08f98e9072 Merge branch 'mzh' into test_dev 1 mese fa
  mazhenhang 1269333ba5 撮合成功审核 1 mese fa
  YH_0525 56fb76d6a0 Merge remote-tracking branch 'origin/test_dev' into test 1 mese fa
  YH_0525 5fa20e9332 上传资源线索获取积分 上传3个以上额外奖励3积分 10个以上额外奖励5积分 1 mese fa
  caojp 833f327c0f Merge branch 'cjp' into test_dev 1 mese fa
  caojp d98924de73 管理端列表bug更改 1 mese fa
  YH_0525 0da9f91e85 Merge remote-tracking branch 'origin/test_dev' into test 1 mese fa
  YH_0525 2c2f61e314 上传资源线索获取积分 1 mese fa
  caojp 25e105dadf Merge branch 'cjp' into test_dev 1 mese fa
  caojp 8f4ac55863 管理端红娘审核bug修改和新红娘案例审核功能 1 mese fa
  caojp 410d6bda0a 管理端红娘审核bug修改和新红娘案例审核功能 1 mese fa
  YH_0525 2571530511 签到获取积分 1 mese fa
  YH_0525 e311d26348 Merge remote-tracking branch 'origin/test_dev' into test 1 mese fa
  YH_0525 12e8423b68 . 1 mese fa
55 ha cambiato i file con 4936 aggiunte e 276 eliminazioni
  1. 14 0
      LiangZhiYUMao/pages.json
  2. 5 1
      LiangZhiYUMao/pages/index/index.vue
  3. 760 0
      LiangZhiYUMao/pages/matchmaker-workbench/audit-records.vue
  4. 39 19
      LiangZhiYUMao/pages/matchmaker-workbench/earn-points.vue
  5. 55 5
      LiangZhiYUMao/pages/matchmaker-workbench/message.vue
  6. 80 62
      LiangZhiYUMao/pages/matchmaker-workbench/mine.vue
  7. 968 0
      LiangZhiYUMao/pages/matchmaker-workbench/success-case-upload.vue
  8. 94 0
      LiangZhiYUMao/utils/api.js
  9. 16 0
      gateway/src/main/resources/application.yml
  10. 3 0
      marriageAdmin-vue/src/config/api.js
  11. 1 0
      marriageAdmin-vue/src/layouts/MainLayout.vue
  12. 6 0
      marriageAdmin-vue/src/router/index.js
  13. 359 0
      marriageAdmin-vue/src/views/matchmaker/CaseAudit.vue
  14. 16 9
      marriageAdmin-vue/src/views/matchmaker/MatchmakerAudit.vue
  15. 10 0
      marriageAdmin-vue/src/views/user/UserList.vue
  16. 0 1
      service/admin/src/main/java/com/zhentao/controller/AuthController.java
  17. 242 0
      service/admin/src/main/java/com/zhentao/controller/MatchmakerSuccessCaseUploadController.java
  18. 46 2
      service/admin/src/main/java/com/zhentao/controller/UserController.java
  19. 9 1
      service/admin/src/main/java/com/zhentao/entity/AdminUser.java
  20. 26 0
      service/admin/src/main/java/com/zhentao/entity/Area.java
  21. 26 0
      service/admin/src/main/java/com/zhentao/entity/City.java
  22. 4 0
      service/admin/src/main/java/com/zhentao/entity/MarrApply.java
  23. 5 0
      service/admin/src/main/java/com/zhentao/entity/Matchmaker.java
  24. 217 0
      service/admin/src/main/java/com/zhentao/entity/MatchmakerSuccessCaseUpload.java
  25. 24 0
      service/admin/src/main/java/com/zhentao/entity/Province.java
  26. 13 0
      service/admin/src/main/java/com/zhentao/mapper/AreaMapper.java
  27. 13 0
      service/admin/src/main/java/com/zhentao/mapper/CityMapper.java
  28. 18 0
      service/admin/src/main/java/com/zhentao/mapper/MatchmakerSuccessCaseUploadMapper.java
  29. 13 0
      service/admin/src/main/java/com/zhentao/mapper/ProvinceMapper.java
  30. 5 107
      service/admin/src/main/java/com/zhentao/service/AdminSecurityService.java
  31. 13 0
      service/admin/src/main/java/com/zhentao/service/MatchmakerSuccessCaseUploadService.java
  32. 84 0
      service/admin/src/main/java/com/zhentao/service/impl/AdminSecurityServiceImpl.java
  33. 186 16
      service/admin/src/main/java/com/zhentao/service/impl/MarrApplyServiceImpl.java
  34. 22 0
      service/admin/src/main/java/com/zhentao/service/impl/MatchmakerSuccessCaseUploadServiceImpl.java
  35. 122 0
      service/admin/src/main/java/com/zhentao/service/impl/RolePermissionBasedAdminSecurityServiceImpl.java
  36. 132 0
      service/admin/src/main/java/com/zhentao/vo/MatchmakerSuccessCaseUploadVO.java
  37. 1 0
      service/admin/src/main/java/com/zhentao/vo/UserVO.java
  38. 3 1
      service/admin/src/main/resources/com/zhentao/mapper/MarrApplyMapper.xml
  39. 37 0
      service/admin/src/main/resources/com/zhentao/mapper/MatchmakerSuccessCaseUploadMapper.xml
  40. 35 0
      service/admin/src/main/resources/com/zhentao/mapper/UsersMapper.xml
  41. 6 6
      service/homePage/src/main/java/com/zhentao/controller/ActivityController.java
  42. 252 27
      service/homePage/src/main/java/com/zhentao/controller/MyResourceController.java
  43. 11 1
      service/homePage/src/main/java/com/zhentao/service/MyResourceService.java
  44. 10 0
      service/homePage/src/main/java/com/zhentao/service/PointsManageService.java
  45. 177 14
      service/homePage/src/main/java/com/zhentao/service/impl/MyResourceServiceImpl.java
  46. 31 0
      service/homePage/src/main/java/com/zhentao/service/impl/PointsManageServiceImpl.java
  47. 5 0
      service/websocket/src/main/java/com/zhentao/config/MinioConfig.java
  48. 385 0
      service/websocket/src/main/java/com/zhentao/controller/SuccessCaseUploadController.java
  49. 52 0
      service/websocket/src/main/java/com/zhentao/dto/SuccessCaseUploadDTO.java
  50. 132 0
      service/websocket/src/main/java/com/zhentao/entity/MatchmakerSuccessCaseUpload.java
  51. 25 0
      service/websocket/src/main/java/com/zhentao/mapper/MatchmakerSuccessCaseUploadMapper.java
  52. 17 0
      service/websocket/src/main/java/com/zhentao/service/MatchmakerSuccessCaseUploadService.java
  53. 37 0
      service/websocket/src/main/java/com/zhentao/service/impl/MatchmakerSuccessCaseUploadServiceImpl.java
  54. 70 1
      service/websocket/src/main/java/com/zhentao/utils/MinioUtil.java
  55. 4 3
      service/websocket/src/main/resources/application.yml

+ 14 - 0
LiangZhiYUMao/pages.json

@@ -77,6 +77,13 @@
 				"navigationStyle": "custom"
 			}
 		},
+		{
+			"path": "pages/matchmaker-workbench/audit-records",
+			"style": {
+				"navigationBarTitleText": "审核记录",
+				"navigationStyle": "custom"
+			}
+		},
 		{
 			"path": "pages/matchmaker-workbench/mine",
 			"style": {
@@ -133,6 +140,13 @@
 				"navigationStyle": "custom"
 			}
 		},
+		{
+			"path": "pages/matchmaker-workbench/success-case-upload",
+			"style": {
+				"navigationBarTitleText": "撮合成功审核",
+				"navigationStyle": "custom"
+			}
+		},
 		{
 			"path": "pages/matchmaker-workbench/my-activities",
 			"style": {

+ 5 - 1
LiangZhiYUMao/pages/index/index.vue

@@ -420,7 +420,11 @@
 				}
 				
 				uni.navigateTo({
-					url: '/pages/matchmaker-workbench/index'
+					url: '/pages/matchmaker-workbench/index',
+					fail: (err) => {
+						console.error('❌ 跳转红娘工作台失败:', err)
+						uni.showToast({ title: '跳转失败', icon: 'none' })
+					}
 				})
 			},
 			

+ 760 - 0
LiangZhiYUMao/pages/matchmaker-workbench/audit-records.vue

@@ -0,0 +1,760 @@
+<template>
+	<view class="audit-records">
+		<!-- 顶部导航栏 -->
+		<view class="header">
+			<view class="back-btn" @click="goBack"></view>
+			<text class="header-title">撮合成功通知</text>
+			<view class="placeholder"></view>
+		</view>
+
+		<scroll-view scroll-y class="content" @scrolltolower="loadMore">
+			<!-- 加载中 -->
+			<view v-if="loading && records.length === 0" class="loading-container">
+				<text>加载中...</text>
+			</view>
+			
+			<!-- 记录列表 -->
+			<view v-else-if="records.length > 0" class="record-list">
+				<view 
+					v-for="record in records" 
+					:key="record.id" 
+					class="record-item"
+					:class="{ unread: !record.isRead && (record.auditStatus === 1 || record.auditStatus === 2) }"
+					@click="openDetail(record)"
+				>
+					<!-- 状态标签 -->
+					<view class="status-tag" 
+						:class="{
+							'status-pending': record.auditStatus === 0,
+							'status-success': record.auditStatus === 1,
+							'status-fail': record.auditStatus === 2,
+							'status-checking': record.auditStatus === 3
+						}">
+						{{ getStatusText(record.auditStatus) }}
+					</view>
+					
+					<!-- 未读红点 -->
+					<view v-if="!record.isRead && (record.auditStatus === 1 || record.auditStatus === 2)" class="unread-dot"></view>
+					
+					<!-- 记录内容 -->
+					<view class="record-content">
+						<view class="record-title">
+							<text class="couple-names">{{ record.maleRealName }} & {{ record.femaleRealName }}</text>
+							<text class="case-type">{{ record.caseType === 1 ? '订婚' : '结婚' }}</text>
+						</view>
+						<view class="record-summary">
+							<text v-if="record.auditStatus === 0">您提交的撮合成功案例正在等待审核</text>
+							<text v-else-if="record.auditStatus === 3">您的案例正在核实中,请耐心等待</text>
+							<text v-else-if="record.auditStatus === 1">
+								恭喜!审核通过,获得{{ record.pointsReward || 0 }}积分
+								<text v-if="record.cashReward > 0">+{{ record.cashReward }}元现金</text>
+								奖励
+							</text>
+							<text v-else-if="record.auditStatus === 2">审核未通过:{{ record.auditRemark || '未说明原因' }}</text>
+						</view>
+						<view class="record-time">{{ formatTime(record.createdAt) }}</view>
+					</view>
+				</view>
+				
+				<!-- 加载更多 -->
+				<view v-if="loading" class="loading-more">
+					<text>加载中...</text>
+				</view>
+				<view v-else-if="!hasMore" class="no-more">
+					<text>没有更多了</text>
+				</view>
+			</view>
+			
+			<!-- 空状态 -->
+			<view v-else class="empty-container">
+				<view class="empty-icon">📋</view>
+				<text class="empty-text">暂无审核记录</text>
+				<text class="empty-hint">提交撮合成功案例后,审核进度将在这里显示</text>
+			</view>
+		</scroll-view>
+
+		<!-- 详情弹窗 -->
+		<uni-popup ref="detailPopup" type="center">
+			<view class="detail-popup" v-if="currentRecord">
+				<view class="popup-header">
+					<text class="popup-title">审核详情</text>
+					<view class="close-btn" @click="closeDetail">×</view>
+				</view>
+				
+				<view class="popup-content">
+					<!-- 状态 -->
+					<view class="detail-status" 
+						:class="{
+							'status-pending': currentRecord.auditStatus === 0,
+							'status-success': currentRecord.auditStatus === 1,
+							'status-fail': currentRecord.auditStatus === 2,
+							'status-checking': currentRecord.auditStatus === 3
+						}">
+						<view class="status-icon" v-if="currentRecord.auditStatus !== 2">
+							<text v-if="currentRecord.auditStatus === 0">⏳</text>
+							<text v-else-if="currentRecord.auditStatus === 3">🔍</text>
+							<text v-else-if="currentRecord.auditStatus === 1">✅</text>
+						</view>
+						<text class="status-text">{{ getStatusText(currentRecord.auditStatus) }}</text>
+					</view>
+					
+					<!-- 案例信息 -->
+					<view class="detail-section">
+						<view class="section-title">案例信息</view>
+						<view class="info-row">
+							<text class="info-label">男方姓名</text>
+							<text class="info-value">{{ currentRecord.maleRealName }}</text>
+						</view>
+						<view class="info-row">
+							<text class="info-label">女方姓名</text>
+							<text class="info-value">{{ currentRecord.femaleRealName }}</text>
+						</view>
+						<view class="info-row">
+							<text class="info-label">案例类型</text>
+							<text class="info-value">{{ currentRecord.caseType === 1 ? '订婚' : '结婚' }}</text>
+						</view>
+						<view class="info-row" v-if="currentRecord.caseDate">
+							<text class="info-label">成功日期</text>
+							<text class="info-value">{{ currentRecord.caseDate }}</text>
+						</view>
+						<view class="info-row">
+							<text class="info-label">提交时间</text>
+							<text class="info-value">{{ formatTime(currentRecord.createdAt) }}</text>
+						</view>
+					</view>
+					
+					<!-- 审核通过:显示奖励 -->
+					<view v-if="currentRecord.auditStatus === 1" class="detail-section reward-section">
+						<view class="section-title">🎉 奖励信息</view>
+						<view class="reward-content">
+							<view class="reward-item" v-if="currentRecord.pointsReward > 0">
+								<text class="reward-icon">🏆</text>
+								<text class="reward-text">积分奖励:+{{ currentRecord.pointsReward }}积分</text>
+							</view>
+							<view class="reward-item" v-if="currentRecord.cashReward > 0">
+								<text class="reward-icon">💰</text>
+								<text class="reward-text">现金奖励:+{{ currentRecord.cashReward }}元</text>
+							</view>
+							<view class="reward-status">
+								<text v-if="currentRecord.rewardStatus === 1" class="issued">奖励已发放</text>
+								<text v-else class="pending">奖励待发放</text>
+							</view>
+						</view>
+					</view>
+					
+					<!-- 审核失败:显示原因 -->
+					<view v-if="currentRecord.auditStatus === 2" class="detail-section fail-section">
+						<view class="section-title">审核未通过</view>
+						<view class="fail-reason">
+							<text>{{ currentRecord.auditRemark || '未说明具体原因,如有疑问请联系客服' }}</text>
+						</view>
+						<view class="fail-hint" @click="goToResubmit">
+							<text>您可以修改后重新提交审核</text>
+							<text class="resubmit-link">去提交 ></text>
+						</view>
+					</view>
+					
+					<!-- 待审核/核实中 -->
+					<view v-if="currentRecord.auditStatus === 0 || currentRecord.auditStatus === 3" class="detail-section pending-section">
+						<view class="section-title">{{ currentRecord.auditStatus === 0 ? '⏳ 等待审核' : '🔍 核实中' }}</view>
+						<view class="pending-hint">
+							<text v-if="currentRecord.auditStatus === 0">您的案例已提交,工作人员将尽快审核,请耐心等待</text>
+							<text v-else>工作人员正在核实您的案例信息,请保持电话畅通</text>
+						</view>
+					</view>
+					
+					<!-- 审核时间 -->
+					<view v-if="currentRecord.updatedAt" class="detail-section">
+						<view class="info-row">
+							<text class="info-label">审核时间</text>
+							<text class="info-value">{{ formatTime(currentRecord.updatedAt) }}</text>
+						</view>
+					</view>
+				</view>
+				
+				<view class="popup-footer">
+					<button class="confirm-btn" @click="closeDetail">我知道了</button>
+				</view>
+			</view>
+		</uni-popup>
+	</view>
+</template>
+
+<script>
+import api from '@/utils/api.js'
+
+export default {
+	data() {
+		return {
+			loading: false,
+			records: [],
+			currentRecord: null,
+			matchmakerId: null,
+			pageNum: 1,
+			pageSize: 20,
+			hasMore: true
+		}
+	},
+	
+	onLoad() {
+		this.loadMatchmakerInfo()
+	},
+	
+	onShow() {
+		// 页面显示时刷新列表
+		if (this.matchmakerId) {
+			this.refreshList()
+		}
+	},
+	
+	methods: {
+		goBack() {
+			uni.navigateBack()
+		},
+		
+		async loadMatchmakerInfo() {
+			try {
+				const userInfo = uni.getStorageSync('userInfo')
+				if (userInfo && userInfo.userId) {
+					const matchmakerInfo = await api.matchmaker.getByUserId(userInfo.userId)
+					if (matchmakerInfo) {
+						this.matchmakerId = matchmakerInfo.matchmakerId || matchmakerInfo.matchmaker_id || null
+						if (this.matchmakerId) {
+							this.loadRecords()
+						} else {
+							uni.showToast({ title: '未找到红娘信息', icon: 'none' })
+						}
+					}
+				}
+			} catch (e) {
+				console.error('获取红娘信息失败', e)
+				uni.showToast({ title: '获取红娘信息失败', icon: 'none' })
+			}
+		},
+		
+		async loadRecords() {
+			if (this.loading || !this.hasMore) return
+			
+			this.loading = true
+			try {
+				const res = await api.successCaseUpload.getAuditRecords(
+					this.matchmakerId, 
+					null, // 不筛选状态,显示全部
+					this.pageNum, 
+					this.pageSize
+				)
+				
+				const pageData = res?.records || res?.data?.records || []
+				const total = res?.total || res?.data?.total || 0
+				
+				if (this.pageNum === 1) {
+					this.records = pageData
+				} else {
+					this.records = [...this.records, ...pageData]
+				}
+				
+				this.hasMore = this.records.length < total
+				this.pageNum++
+				
+			} catch (e) {
+				console.error('加载审核记录失败', e)
+				uni.showToast({ title: '加载失败', icon: 'none' })
+			} finally {
+				this.loading = false
+			}
+		},
+		
+		refreshList() {
+			this.pageNum = 1
+			this.hasMore = true
+			this.records = []
+			this.loadRecords()
+		},
+		
+		loadMore() {
+			if (this.hasMore && !this.loading) {
+				this.loadRecords()
+			}
+		},
+		
+		async openDetail(record) {
+			this.currentRecord = record
+			this.$refs.detailPopup.open()
+			
+			// 如果是已审核完成且未读,标记为已读
+			if ((record.auditStatus === 1 || record.auditStatus === 2) && !record.isRead) {
+				try {
+					await api.successCaseUpload.markAsRead(record.id)
+					// 更新本地状态
+					record.isRead = 1
+				} catch (e) {
+					console.error('标记已读失败', e)
+				}
+			}
+		},
+		
+		closeDetail() {
+			this.$refs.detailPopup.close()
+			this.currentRecord = null
+		},
+		
+		// 跳转到重新提交审核页面
+		goToResubmit() {
+			if (!this.currentRecord) return
+			// 关闭弹窗
+			this.$refs.detailPopup.close()
+			// 跳转到提交页面,携带记录ID用于回显数据
+			uni.navigateTo({
+				url: `/pages/matchmaker-workbench/success-case-upload?resubmitId=${this.currentRecord.id}`
+			})
+		},
+		
+		getStatusText(status) {
+			const statusMap = {
+				0: '待审核',
+				1: '审核通过',
+				2: '审核失败',
+				3: '核实中'
+			}
+			return statusMap[status] || '未知'
+		},
+		
+		// getStatusClass 方法已移除,改用内联对象语法
+		
+		formatTime(timeStr) {
+			if (!timeStr) return ''
+			const date = new Date(timeStr)
+			const now = new Date()
+			const diff = now - date
+			
+			// 一分钟内
+			if (diff < 60000) {
+				return '刚刚'
+			}
+			// 一小时内
+			if (diff < 3600000) {
+				return Math.floor(diff / 60000) + '分钟前'
+			}
+			// 今天
+			if (date.toDateString() === now.toDateString()) {
+				return date.getHours() + ':' + String(date.getMinutes()).padStart(2, '0')
+			}
+			// 昨天
+			const yesterday = new Date(now)
+			yesterday.setDate(yesterday.getDate() - 1)
+			if (date.toDateString() === yesterday.toDateString()) {
+				return '昨天 ' + date.getHours() + ':' + String(date.getMinutes()).padStart(2, '0')
+			}
+			// 更早
+			const month = date.getMonth() + 1
+			const day = date.getDate()
+			return `${month}-${day} ${date.getHours()}:${String(date.getMinutes()).padStart(2, '0')}`
+		}
+	}
+}
+</script>
+
+<style lang="scss" scoped>
+.audit-records {
+	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;
+
+	.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: 38rpx;
+		font-weight: bold;
+		color: #333;
+	}
+
+	.placeholder {
+		width: 70rpx;
+	}
+}
+
+.content {
+	flex: 1;
+	padding: 20rpx 24rpx;
+	box-sizing: border-box;
+}
+
+/* 记录列表 */
+.record-list {
+	padding-bottom: 40rpx;
+}
+
+.record-item {
+	background: #ffffff;
+	border-radius: 24rpx;
+	padding: 28rpx;
+	margin-bottom: 20rpx;
+	box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.05);
+	position: relative;
+	
+	&.unread {
+		background: linear-gradient(135deg, #fff8fb 0%, #fef5ff 100%);
+		border: 2rpx solid rgba(233, 30, 99, 0.15);
+	}
+}
+
+/* 状态标签 */
+.status-tag {
+	position: absolute;
+	top: 0;
+	left: 28rpx;
+	padding: 6rpx 16rpx;
+	border-radius: 0 0 12rpx 12rpx;
+	font-size: 22rpx;
+	font-weight: 500;
+	
+	&.status-pending {
+		background: #fff3e0;
+		color: #f57c00;
+	}
+	
+	&.status-success {
+		background: #e8f5e9;
+		color: #43a047;
+	}
+	
+	&.status-fail {
+		background: #ffebee;
+		color: #e53935;
+	}
+	
+	&.status-checking {
+		background: #e3f2fd;
+		color: #1976d2;
+	}
+}
+
+/* 未读红点 */
+.unread-dot {
+	position: absolute;
+	top: 20rpx;
+	right: 20rpx;
+	width: 16rpx;
+	height: 16rpx;
+	background: #e91e63;
+	border-radius: 50%;
+}
+
+/* 记录内容 */
+.record-content {
+	padding-top: 30rpx;
+}
+
+.record-title {
+	display: flex;
+	align-items: center;
+	margin-bottom: 12rpx;
+	
+	.couple-names {
+		font-size: 32rpx;
+		font-weight: 600;
+		color: #333;
+		margin-right: 16rpx;
+	}
+	
+	.case-type {
+		font-size: 24rpx;
+		color: #9c27b0;
+		background: #f3e5f5;
+		padding: 4rpx 12rpx;
+		border-radius: 8rpx;
+	}
+}
+
+.record-summary {
+	font-size: 26rpx;
+	color: #666;
+	line-height: 1.5;
+	margin-bottom: 12rpx;
+}
+
+.record-time {
+	font-size: 24rpx;
+	color: #999;
+}
+
+/* 加载状态 */
+.loading-container,
+.empty-container {
+	display: flex;
+	flex-direction: column;
+	justify-content: center;
+	align-items: center;
+	padding: 100rpx 0;
+	color: #999;
+	font-size: 28rpx;
+}
+
+.empty-icon {
+	font-size: 80rpx;
+	margin-bottom: 20rpx;
+}
+
+.empty-text {
+	font-size: 32rpx;
+	color: #666;
+	margin-bottom: 12rpx;
+}
+
+.empty-hint {
+	font-size: 26rpx;
+	color: #999;
+}
+
+.loading-more,
+.no-more {
+	text-align: center;
+	padding: 30rpx 0;
+	color: #999;
+	font-size: 26rpx;
+}
+
+/* 详情弹窗 */
+.detail-popup {
+	width: 650rpx;
+	background: #ffffff;
+	border-radius: 24rpx;
+	overflow: hidden;
+}
+
+.popup-header {
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	padding: 30rpx;
+	border-bottom: 1rpx solid #f0f0f0;
+	
+	.popup-title {
+		font-size: 34rpx;
+		font-weight: 600;
+		color: #333;
+	}
+	
+	.close-btn {
+		width: 50rpx;
+		height: 50rpx;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		font-size: 40rpx;
+		color: #999;
+	}
+}
+
+.popup-content {
+	padding: 30rpx;
+	max-height: 70vh;
+	overflow-y: auto;
+}
+
+/* 详情状态 */
+.detail-status {
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+	padding: 30rpx;
+	border-radius: 16rpx;
+	margin-bottom: 30rpx;
+	
+	&.status-pending {
+		background: #fff3e0;
+	}
+	
+	&.status-success {
+		background: #e8f5e9;
+	}
+	
+	&.status-fail {
+		background: #ffebee;
+	}
+	
+	&.status-checking {
+		background: #e3f2fd;
+	}
+	
+	.status-icon {
+		font-size: 60rpx;
+		margin-bottom: 12rpx;
+	}
+	
+	.status-text {
+		font-size: 32rpx;
+		font-weight: 600;
+	}
+}
+
+/* 详情区块 */
+.detail-section {
+	margin-bottom: 30rpx;
+	
+	.section-title {
+		font-size: 28rpx;
+		font-weight: 600;
+		color: #333;
+		margin-bottom: 16rpx;
+		padding-bottom: 12rpx;
+		border-bottom: 1rpx solid #f0f0f0;
+	}
+}
+
+.info-row {
+	display: flex;
+	justify-content: space-between;
+	padding: 12rpx 0;
+	
+	.info-label {
+		font-size: 26rpx;
+		color: #999;
+	}
+	
+	.info-value {
+		font-size: 26rpx;
+		color: #333;
+	}
+}
+
+/* 奖励区块 */
+.reward-section {
+	background: linear-gradient(135deg, #fff8e1 0%, #ffecb3 100%);
+	padding: 24rpx;
+	border-radius: 16rpx;
+	
+	.section-title {
+		border-bottom: none;
+		padding-bottom: 0;
+	}
+}
+
+.reward-content {
+	.reward-item {
+		display: flex;
+		align-items: center;
+		padding: 12rpx 0;
+		
+		.reward-icon {
+			font-size: 36rpx;
+			margin-right: 12rpx;
+		}
+		
+		.reward-text {
+			font-size: 28rpx;
+			color: #333;
+			font-weight: 500;
+		}
+	}
+	
+	.reward-status {
+		margin-top: 16rpx;
+		text-align: center;
+		
+		.issued {
+			color: #43a047;
+			font-size: 26rpx;
+		}
+		
+		.pending {
+			color: #f57c00;
+			font-size: 26rpx;
+		}
+	}
+}
+
+/* 失败区块 */
+.fail-section {
+	background: #fff5f5;
+	padding: 24rpx;
+	border-radius: 16rpx;
+	
+	.section-title {
+		border-bottom: none;
+		padding-bottom: 0;
+		color: #e53935;
+	}
+	
+	.fail-reason {
+		font-size: 28rpx;
+		color: #333;
+		line-height: 1.6;
+		padding: 12rpx 0;
+	}
+	
+	.fail-hint {
+		font-size: 24rpx;
+		color: #999;
+		margin-top: 12rpx;
+		display: flex;
+		justify-content: space-between;
+		align-items: center;
+		
+		.resubmit-link {
+			color: #9c27b0;
+			font-weight: 500;
+		}
+	}
+}
+
+/* 待审核区块 */
+.pending-section {
+	background: #f5f5f5;
+	padding: 24rpx;
+	border-radius: 16rpx;
+	
+	.section-title {
+		border-bottom: none;
+		padding-bottom: 0;
+	}
+	
+	.pending-hint {
+		font-size: 26rpx;
+		color: #666;
+		line-height: 1.6;
+	}
+}
+
+/* 弹窗底部 */
+.popup-footer {
+	padding: 20rpx 30rpx 30rpx;
+	
+	.confirm-btn {
+		width: 100%;
+		height: 88rpx;
+		background: linear-gradient(135deg, #e91e63 0%, #9c27b0 100%);
+		color: #ffffff;
+		font-size: 32rpx;
+		font-weight: 500;
+		border-radius: 44rpx;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		border: none;
+	}
+}
+</style>

+ 39 - 19
LiangZhiYUMao/pages/matchmaker-workbench/earn-points.vue

@@ -56,6 +56,7 @@ export default {
       balance: 0,
       rules: [],
       makerId: null,
+      userId: null,  // 用户ID,用于签到
       isSignedToday: false  // 今日是否已签到
     }
   },
@@ -72,6 +73,11 @@ export default {
   methods: {
     async initData() {
       const userInfo = uni.getStorageSync('userInfo')
+      // 保存userId用于签到
+      if (userInfo && userInfo.userId) {
+        this.userId = userInfo.userId
+      }
+      
       if (userInfo && userInfo.matchmakerId) {
         this.makerId = userInfo.matchmakerId
       } else if (userInfo && userInfo.userId) {
@@ -181,9 +187,9 @@ export default {
         }
         await this.doSignIn(rule)
       } else if (rule.ruleType === 2) {
-        // 上传线索
+        // 上传资源 - 跳转到我的资源的信息录入页面
         uni.navigateTo({
-          url: '/pages/matchmaker-workbench/add-resource'
+          url: '/pages/matchmaker-workbench/resource-input'
         })
       } else if (rule.ruleType === 4) {
         // 培训课程
@@ -199,35 +205,49 @@ export default {
     },
     
     async doSignIn(rule) {
-      if (!this.makerId) {
+      if (!this.userId) {
         uni.showToast({ title: '请先登录', icon: 'none' })
         return
       }
       
       try {
-        // 使用红娘签到接口
-        const res = await api.matchmaker.doCheckin(this.makerId)
+        // 使用userId进行签到(后端会根据userId查询matchmaker并添加积分)
+        const res = await api.matchmaker.doCheckin(this.userId)
         
-        // 签到成功后更新状态
-        this.isSignedToday = true
-        
-        // 刷新积分余额
-        await this.loadBalance()
+        // 检查签到结果
+        let success = false
+        if (res === true || res?.data === true) {
+          success = true
+        } else if (res?.msg === '今日已签到') {
+          this.isSignedToday = true
+          uni.showToast({ title: '今日已签到', icon: 'none' })
+          return
+        }
         
-        uni.showToast({
-          title: `签到成功,+${rule.pointValue}积分`,
-          icon: 'success'
-        })
+        if (success) {
+          // 签到成功后更新状态
+          this.isSignedToday = true
+          
+          // 刷新积分余额
+          await this.loadBalance()
+          
+          uni.showToast({
+            title: `签到成功,+${rule.pointValue}积分`,
+            icon: 'success'
+          })
+        }
       } catch (e) {
         console.error('签到失败:', e)
         // 如果是已签到的错误,更新状态
-        if (e.message && e.message.includes('已签到')) {
+        if (e.message && e.message.includes('已签到') || e.msg && e.msg.includes('已签到')) {
           this.isSignedToday = true
+          uni.showToast({ title: '今日已签到', icon: 'none' })
+        } else {
+          uni.showToast({
+            title: e.message || e.msg || '签到失败',
+            icon: 'none'
+          })
         }
-        uni.showToast({
-          title: e.message || '签到失败',
-          icon: 'none'
-        })
       }
     }
   }

+ 55 - 5
LiangZhiYUMao/pages/matchmaker-workbench/message.vue

@@ -24,13 +24,18 @@
 				</view>
 				
 				<!-- 撮合成功通知卡片 -->
-				<view class="message-item match-success" v-if="matchSuccessNotification">
+				<view class="message-item match-success" v-if="matchSuccessNotification" @click="openAuditRecords">
 					<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 class="message-right-area">
+						<text class="message-time">{{ matchSuccessNotification.timeText }}</text>
+						<view v-if="matchSuccessUnread > 0" class="match-unread-badge">
+							{{ matchSuccessUnread > 99 ? '99+' : matchSuccessUnread }}
+						</view>
+					</view>
 				</view>
 				
 				<!-- 今天 -->
@@ -181,6 +186,10 @@
 				totalUnreadCount: 0,
 				// 系统消息未读数
 				systemUnread: 0,
+				// 撮合成功通知未读数
+				matchSuccessUnread: 0,
+				// 红娘ID
+				matchmakerId: null,
 				
 				// 顶部系统通知占位(后续可从后端加载)
 				systemNotification: {
@@ -237,6 +246,8 @@
 			}
 			// 同步系统消息未读
 			this.loadSystemUnread()
+			// 同步撮合成功通知未读
+			this.loadMatchSuccessUnread()
 		},
 		
 		onUnload() {
@@ -316,6 +327,26 @@
 				})
 			},
 
+			// 跳转审核记录列表
+			openAuditRecords() {
+				uni.navigateTo({
+					url: '/pages/matchmaker-workbench/audit-records'
+				})
+			},
+
+			// 加载撮合成功通知未读数
+			async loadMatchSuccessUnread() {
+				try {
+					if (!this.matchmakerId) return
+					const res = await api.successCaseUpload.getUnreadCount(this.matchmakerId)
+					if (res && res.code === 200) {
+						this.matchSuccessUnread = res.data || 0
+					}
+				} catch (e) {
+					console.log('加载撮合成功通知未读数失败', e)
+				}
+			},
+
 			// 初始化 IM
 			async initIM() {
 				try {
@@ -342,9 +373,13 @@
 					
 					this.matchmakerInfo = res[1].data.data
 					this.imUserId = this.matchmakerInfo.imUserId  // m_1
+					this.matchmakerId = this.matchmakerInfo.matchmakerId || this.matchmakerInfo.matchmaker_id
 					
 					console.log('✅ 红娘信息:', this.matchmakerInfo)
 					
+					// 加载撮合成功通知未读数
+					this.loadMatchSuccessUnread()
+					
 					// 3. 获取 UserSig
 					const sigRes = await uni.request({
 						url: `http://localhost:8083/api/im/getUserSig?userId=${this.imUserId}`,
@@ -375,7 +410,6 @@
 					this.addListeners()
 					
 					// 7. 加载会话列表
-					// 6. 加载会话列表
 					await this.loadConversationList()
 					
 				} catch (error) {
@@ -652,7 +686,6 @@
 			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;
@@ -763,10 +796,27 @@
 				line-height: 1.4;
 			}
 
+			.message-right-area {
+				display: flex;
+				flex-direction: column;
+				align-items: flex-end;
+				margin-left: 20rpx;
+			}
+
 			.message-time {
 				font-size: 24rpx;
 				color: #a691c0;
-				margin-top: auto;
+				margin-bottom: 10rpx;
+			}
+
+			.match-unread-badge {
+				background: #ff5c9a;
+				color: #FFFFFF;
+				font-size: 20rpx;
+				padding: 4rpx 12rpx;
+				border-radius: 20rpx;
+				min-width: 36rpx;
+				text-align: center;
 			}
 		}
 

+ 80 - 62
LiangZhiYUMao/pages/matchmaker-workbench/mine.vue

@@ -71,6 +71,12 @@
             <view class="arrow-right"></view>
           </view>
 
+          <view class="settings-item" @click="handleSuccessCaseUpload">
+            <view class="settings-icon success-case"></view>
+            <text class="settings-text">撮合成功审核</text>
+            <view class="arrow-right"></view>
+          </view>
+
           <view class="settings-item" @click="handleAccountSettings">
             <view class="settings-icon info"></view>
             <text class="settings-text">关于我们</text>
@@ -133,7 +139,7 @@
               <text class="stats-unit">天</text>
             </view>
           </view>
-          
+
           <view class="calendar-container">
             <view class="calendar-header">
               <text class="calendar-title">{{ calendarTitle }}</text>
@@ -164,7 +170,7 @@
               </view>
             </view>
           </view>
-          
+
           <view class="sign-in-reward">
             <text class="reward-title">签到奖励</text>
             <text class="reward-points">+5积分</text>
@@ -349,7 +355,7 @@ export default {
           console.warn('没有有效的userId,无法检查签到状态')
           return
         }
-        
+
         // 首先获取红娘信息,以获取makerId
         const matchmakerInfo = await api.matchmaker.getByUserId(userInfo.userId)
         if (!matchmakerInfo || !matchmakerInfo.matchmakerId && !matchmakerInfo.matchmaker_id) {
@@ -360,9 +366,9 @@ export default {
           console.warn('没有有效的makerId,无法检查签到状态')
           return
         }
-        
+
         const makerId = matchmakerInfo.matchmakerId || matchmakerInfo.matchmaker_id
-        
+
         // 检查今日是否已签到
         const res = await api.matchmaker.checkinStatus(makerId)
         console.log('检查签到状态返回结果:', res)
@@ -384,21 +390,21 @@ export default {
           }
         }
         this.isSignedToday = !!signed
-        
+
         // 获取签到统计
         const stats = await api.matchmaker.checkinStats(makerId)
         console.log('获取签到统计返回结果:', stats)
-        
+
         // 初始化签到记录数组
         this.signedDays = []
-        
+
         if (stats) {
           this.continuousDays = stats.continuousDays || 0
           this.totalDays = stats.totalDays || 0
-          
+
           // 检查stats中是否包含本月的签到记录
           let signedDays = []
-          
+
           // 情况1: stats直接包含signedDays数组
           if (stats.signedDays && Array.isArray(stats.signedDays)) {
             signedDays = stats.signedDays
@@ -428,16 +434,16 @@ export default {
               }
               return days
             }
-            
+
             // 先处理stats对象本身
             signedDays = processStats(stats)
-            
+
             // 如果stats.data存在,也处理一下
             if (stats.data && typeof stats.data === 'object') {
               signedDays = [...signedDays, ...processStats(stats.data)]
             }
           }
-          
+
           // 检查签到状态响应中是否包含签到记录
           if (signedDays.length === 0 && res && typeof res === 'object') {
             const processRes = (obj) => {
@@ -447,24 +453,24 @@ export default {
               }
               return days
             }
-            
+
             signedDays = processRes(res)
-            
+
             if (res.data && typeof res.data === 'object') {
               signedDays = [...signedDays, ...processRes(res.data)]
             }
           }
-          
+
           // 检查今日是否已签到,如果已签到但不在signedDays中,添加进去
           const today = new Date().getDate()
           if (this.isSignedToday && !signedDays.includes(today)) {
             signedDays.push(today)
           }
-          
+
           // 去重并排序
           this.signedDays = [...new Set(signedDays)].sort((a, b) => a - b)
         }
-        
+
         console.log('最终的signedDays:', this.signedDays)
       } catch (error) {
         console.error('检查签到状态失败:', error)
@@ -501,44 +507,31 @@ export default {
           }, 1000)
           return
         }
-        
-        // 首先获取红娘信息,以获取makerId
-        const matchmakerInfo = await api.matchmaker.getByUserId(userInfo.userId)
-        if (!matchmakerInfo || !matchmakerInfo.matchmakerId && !matchmakerInfo.matchmaker_id) {
-          uni.showToast({
-            title: '没有有效的makerId,无法签到',
-            icon: 'none'
-          })
-          return
-        }
-        
-        const makerId = matchmakerInfo.matchmakerId || matchmakerInfo.matchmaker_id
-        
-        // 执行签到
-        const res = await api.matchmaker.doCheckin(makerId)
+
+        // 使用userId进行签到(后端会根据userId查询matchmaker并添加积分)
+        const res = await api.matchmaker.doCheckin(userInfo.userId)
         console.log('签到API返回结果:', res)
+
+        // 判断签到结果
         let success = false
         let alreadySigned = false
-        if (typeof res === 'boolean') {
-          success = res
-        } else if (typeof res === 'string') {
-          success = res === 'true' || res === 'ok' || res === 'success'
+
+        // 后端返回格式: { code: 200, msg: "签到成功", data: true/false }
+        if (res === true) {
+          success = true
+        } else if (res === false) {
+          alreadySigned = true
         } else if (res && typeof res === 'object') {
-          if (typeof res.success === 'boolean') success = res.success
-          if (typeof res.todaySigned === 'boolean') alreadySigned = res.todaySigned
-          else if (typeof res.signed === 'boolean') alreadySigned = res.signed
-          else if (typeof res.isSignedToday === 'boolean') alreadySigned = res.isSignedToday
-          if (res.data && typeof res.data === 'object') {
-            if (typeof res.data.success === 'boolean') success = res.data.success
-            if (typeof res.data.todaySigned === 'boolean') alreadySigned = res.data.todaySigned
-            else if (typeof res.data.signed === 'boolean') alreadySigned = res.data.signed
-            else if (typeof res.data.isSignedToday === 'boolean') alreadySigned = res.data.isSignedToday
+          if (res.data === true) {
+            success = true
+          } else if (res.data === false || res.msg === '今日已签到') {
+            alreadySigned = true
           }
         }
-        
+
         if (success) {
           uni.showToast({
-            title: '签到成功',
+            title: '签到成功,+5积分',
             icon: 'success'
           })
           this.isSignedToday = true
@@ -550,9 +543,11 @@ export default {
             this.signedDays.sort((a, b) => a - b)
           }
           this.generateCalendar()
-          this.loadProfileData()
+          // 刷新个人资料(包含积分)
+          await this.loadProfileData()
           this.closeSignInPopup()
         } else if (alreadySigned) {
+          this.isSignedToday = true
           uni.showToast({
             title: '今日已签到',
             icon: 'none'
@@ -565,10 +560,18 @@ export default {
         }
       } catch (error) {
         console.error('签到失败:', error)
-        uni.showToast({
-          title: error.msg || '签到失败,请稍后重试',
-          icon: 'none'
-        })
+        if (error.msg && error.msg.includes('已签到')) {
+          this.isSignedToday = true
+          uni.showToast({
+            title: '今日已签到',
+            icon: 'none'
+          })
+        } else {
+          uni.showToast({
+            title: error.msg || '签到失败,请稍后重试',
+            icon: 'none'
+          })
+        }
       }
     },
     // 签到(旧方法,保持兼容)
@@ -593,6 +596,12 @@ export default {
         url: '/pages/matchmaker-workbench/points-mall'
       })
     },
+    // 撮合成功审核
+    handleSuccessCaseUpload() {
+      uni.navigateTo({
+        url: '/pages/matchmaker-workbench/success-case-upload'
+      })
+    },
     // 编辑资料
     handleEditProfile() {
       console.log('编辑资料')
@@ -948,7 +957,7 @@ export default {
     .settings-icon.logout {
       background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%239C27B0"><path d="M17 7l-1.41 1.41L18.17 11H8v2h10.17l-2.58 2.58L17 17l5-5zM4 5h8V3H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h8v-2H4V5z"/></svg>');
     }
-    
+
     .settings-icon.info {
       background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%239C27B0"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>');
     }
@@ -958,6 +967,15 @@ export default {
       font-size: 30rpx;
       color: #333;
     }
+			.settings-icon.success-case {
+				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>');
+			}
+
+			.settings-text {
+				flex: 1;
+				font-size: 30rpx;
+				color: #333;
+			}
 
     .arrow-right {
       width: 24rpx;
@@ -1111,31 +1129,31 @@ export default {
       padding: 20rpx;
       background: #FFF3E0;
       border-radius: 15rpx;
-      
+
       .stats-item {
         display: flex;
         flex-direction: column;
         align-items: center;
         gap: 10rpx;
-        
+
         .stats-label {
           font-size: 24rpx;
           color: #666;
         }
-        
+
         .stats-value {
           font-size: 48rpx;
           font-weight: bold;
           color: #E91E63;
         }
-        
+
         .stats-unit {
           font-size: 24rpx;
           color: #666;
         }
       }
     }
-    
+
     .calendar-container {
       background: #FFFFFF;
       border-radius: 15rpx;
@@ -1204,13 +1222,13 @@ export default {
             color: #E91E63;
             font-weight: bold;
           }
-          
+
           &.today-checked {
             background: #E91E63;
             color: #FFFFFF;
             font-weight: bold;
           }
-          
+
           .check-mark {
             position: absolute;
             bottom: 5rpx;
@@ -1225,7 +1243,7 @@ export default {
             align-items: center;
             justify-content: center;
           }
-          
+
           .today-checked .check-mark {
             background: #FFFFFF;
             color: #E91E63;

+ 968 - 0
LiangZhiYUMao/pages/matchmaker-workbench/success-case-upload.vue

@@ -0,0 +1,968 @@
+<template>
+	<view class="success-case-upload">
+		<!-- 顶部导航栏 -->
+		<view class="header">
+			<view class="back-btn" @click="goBack"></view>
+			<text class="header-title">撮合成功审核</text>
+			<view class="placeholder"></view>
+		</view>
+
+		<scroll-view scroll-y class="content">
+			<view class="form-container">
+				<!-- 男方信息 -->
+				<view class="form-section">
+					<view class="section-title">
+						<text class="title-icon male">♂</text>
+						<text class="title-text">男方信息</text>
+					</view>
+					
+					<!-- 选择男方用户 -->
+					<view class="form-item">
+						<text class="form-label">选择男方用户 <text class="required">*</text></text>
+						<view class="picker-wrapper" @click="openMalePicker">
+							<view class="selected-user" v-if="selectedMale">
+								<image class="user-avatar" :src="selectedMale.avatarUrl || '/static/default-avatar.svg'" mode="aspectFill"></image>
+								<text class="user-name">{{ selectedMale.name }}</text>
+								<text class="user-age" v-if="selectedMale.age">{{ selectedMale.age }}岁</text>
+							</view>
+							<text class="placeholder-text" v-else>请选择或搜索男方用户</text>
+							<view class="arrow-down"></view>
+						</view>
+					</view>
+					
+					<!-- 男方真实姓名 -->
+					<view class="form-item">
+						<text class="form-label">男方真实姓名 <text class="required">*</text></text>
+						<input class="form-input" v-model="formData.maleRealName" placeholder="请输入男方真实姓名" />
+					</view>
+				</view>
+
+				<!-- 女方信息 -->
+				<view class="form-section">
+					<view class="section-title">
+						<text class="title-icon female">♀</text>
+						<text class="title-text">女方信息</text>
+					</view>
+					
+					<!-- 选择女方用户 -->
+					<view class="form-item">
+						<text class="form-label">选择女方用户 <text class="required">*</text></text>
+						<view class="picker-wrapper" @click="openFemalePicker">
+							<view class="selected-user" v-if="selectedFemale">
+								<image class="user-avatar" :src="selectedFemale.avatarUrl || '/static/default-avatar.svg'" mode="aspectFill"></image>
+								<text class="user-name">{{ selectedFemale.name }}</text>
+								<text class="user-age" v-if="selectedFemale.age">{{ selectedFemale.age }}岁</text>
+							</view>
+							<text class="placeholder-text" v-else>请选择或搜索女方用户</text>
+							<view class="arrow-down"></view>
+						</view>
+					</view>
+					
+					<!-- 女方真实姓名 -->
+					<view class="form-item">
+						<text class="form-label">女方真实姓名 <text class="required">*</text></text>
+						<input class="form-input" v-model="formData.femaleRealName" placeholder="请输入女方真实姓名" />
+					</view>
+				</view>
+
+				<!-- 成功凭证 -->
+				<view class="form-section">
+					<view class="section-title">
+						<text class="title-icon proof">📷</text>
+						<text class="title-text">成功凭证</text>
+					</view>
+					
+					<view class="form-item">
+						<text class="form-label">上传凭证图片 <text class="required">*</text></text>
+						<text class="form-hint">请上传结婚证、订婚照片等凭证(最多9张)</text>
+						<view class="image-upload-area">
+							<view class="image-item" v-for="(img, index) in proofImages" :key="index">
+								<image class="uploaded-image" :src="img" mode="aspectFill"></image>
+								<view class="delete-btn" @click="removeImage(index)">×</view>
+							</view>
+							<view class="add-image-btn" v-if="proofImages.length < 9" @click="chooseImage">
+								<text class="add-icon">+</text>
+								<text class="add-text">添加图片</text>
+							</view>
+						</view>
+					</view>
+				</view>
+
+				<!-- 案例信息 -->
+				<view class="form-section">
+					<view class="section-title">
+						<text class="title-icon info">💕</text>
+						<text class="title-text">案例信息</text>
+					</view>
+					
+					<!-- 案例类型 -->
+					<view class="form-item">
+						<text class="form-label">案例类型 <text class="required">*</text></text>
+						<view class="radio-group">
+							<view class="radio-item" :class="{ active: formData.caseType === 1 }" @click="formData.caseType = 1">
+								<view class="radio-circle"></view>
+								<text class="radio-text">订婚</text>
+							</view>
+							<view class="radio-item" :class="{ active: formData.caseType === 2 }" @click="formData.caseType = 2">
+								<view class="radio-circle"></view>
+								<text class="radio-text">结婚</text>
+							</view>
+						</view>
+					</view>
+					
+					<!-- 成功日期 -->
+					<view class="form-item">
+						<text class="form-label">成功日期</text>
+						<picker mode="date" :value="formData.caseDate" @change="onDateChange">
+							<view class="date-picker">
+								<text class="date-text" v-if="formData.caseDate">{{ formData.caseDate }}</text>
+								<text class="placeholder-text" v-else>请选择日期</text>
+								<view class="calendar-icon">📅</view>
+							</view>
+						</picker>
+					</view>
+				</view>
+
+				<!-- 提交按钮 -->
+				<view class="submit-section">
+					<button class="submit-btn" :disabled="submitting" @click="submitForm">
+						<text v-if="submitting">提交中...</text>
+						<text v-else>提交审核</text>
+					</button>
+					<text class="submit-hint">提交后将由平台审核,审核通过后可获得积分奖励</text>
+				</view>
+			</view>
+		</scroll-view>
+
+		<!-- 用户选择弹窗 -->
+		<uni-popup ref="userPickerPopup" type="bottom">
+			<view class="user-picker-popup">
+				<view class="popup-header">
+					<text class="popup-title">{{ pickerGender === 1 ? '选择男方用户' : '选择女方用户' }}</text>
+					<view class="close-btn" @click="closeUserPicker">×</view>
+				</view>
+				
+				<!-- 搜索框 -->
+				<view class="search-box">
+					<input class="search-input" v-model="searchKeyword" placeholder="搜索姓名或手机号" @input="onSearchInput" />
+					<view class="search-icon">🔍</view>
+				</view>
+				
+				<!-- 用户列表 -->
+				<scroll-view scroll-y class="user-list">
+					<view class="user-item" v-for="user in filteredUsers" :key="user.resourceId" @click="selectUser(user)">
+						<image class="user-avatar" :src="user.avatarUrl || '/static/default-avatar.svg'" mode="aspectFill"></image>
+						<view class="user-info">
+							<text class="user-name">{{ user.name }}</text>
+							<text class="user-detail">{{ user.age }}岁 · {{ user.phone }}</text>
+						</view>
+					</view>
+					<view class="empty-tip" v-if="filteredUsers.length === 0">
+						<text>暂无匹配的用户</text>
+					</view>
+				</scroll-view>
+			</view>
+		</uni-popup>
+	</view>
+</template>
+
+<script>
+import api from '@/utils/api.js'
+
+export default {
+	data() {
+		return {
+			matchmakerId: null,
+			formData: {
+				maleRealName: '',
+				femaleRealName: '',
+				caseType: 2, // 默认结婚
+				caseDate: ''
+			},
+			selectedMale: null,
+			selectedFemale: null,
+			proofImages: [],
+			submitting: false,
+			resubmitId: null, // 重新提交时的原记录ID
+			
+			// 用户选择器
+			pickerGender: 1, // 1-男 2-女
+			searchKeyword: '',
+			maleUsers: [],
+			femaleUsers: [],
+			searchTimer: null
+		}
+	},
+	computed: {
+		filteredUsers() {
+			const users = this.pickerGender === 1 ? this.maleUsers : this.femaleUsers
+			if (!this.searchKeyword) {
+				return users
+			}
+			const keyword = this.searchKeyword.toLowerCase()
+			return users.filter(u => 
+				(u.name && u.name.toLowerCase().includes(keyword)) ||
+				(u.phone && u.phone.includes(keyword))
+			)
+		}
+	},
+	onLoad(options) {
+		this.loadMatchmakerInfo()
+		// 检查是否是重新提交
+		if (options.resubmitId) {
+			this.resubmitId = options.resubmitId
+			this.loadResubmitData(options.resubmitId)
+		}
+	},
+	onShow() {
+		this.loadResources()
+	},
+	methods: {
+		goBack() {
+			uni.navigateBack()
+		},
+		
+		async loadMatchmakerInfo() {
+			try {
+				const userInfo = uni.getStorageSync('userInfo')
+				if (userInfo && userInfo.userId) {
+					// 通过userId获取红娘信息
+					const matchmakerInfo = await api.matchmaker.getByUserId(userInfo.userId)
+					console.log('红娘信息:', matchmakerInfo)
+					if (matchmakerInfo) {
+						// 兼容不同字段名
+						this.matchmakerId = matchmakerInfo.matchmakerId || matchmakerInfo.matchmaker_id || null
+						console.log('获取到红娘ID:', this.matchmakerId)
+					}
+					if (!this.matchmakerId) {
+						uni.showToast({ title: '未找到红娘信息', icon: 'none' })
+					}
+				}
+			} catch (e) {
+				console.error('获取红娘信息失败', e)
+				uni.showToast({ title: '获取红娘信息失败', icon: 'none' })
+			}
+		},
+		
+		// 加载重新提交的数据进行回显
+		async loadResubmitData(id) {
+			try {
+				const res = await api.successCaseUpload.getAuditRecordDetail(id)
+				console.log('回显数据:', res)
+				if (res) {
+					// 回显表单数据
+					this.formData.maleRealName = res.maleRealName || ''
+					this.formData.femaleRealName = res.femaleRealName || ''
+					this.formData.caseType = res.caseType || 2
+					this.formData.caseDate = res.caseDate || ''
+					
+					// 回显凭证图片(存储格式为JSON数组字符串)
+					if (res.proofImages) {
+						try {
+							const images = JSON.parse(res.proofImages)
+							this.proofImages = Array.isArray(images) ? images : []
+						} catch (e) {
+							// 兼容逗号分隔格式
+							this.proofImages = res.proofImages.split(',').filter(img => img)
+						}
+					}
+					
+					// 回显男方用户信息
+					if (res.maleUserId) {
+						const maleInfo = res.maleUserInfo || {}
+						this.selectedMale = {
+							userId: res.maleUserId,
+							name: maleInfo.name || res.maleRealName,
+							avatarUrl: maleInfo.avatarUrl || '',
+							age: maleInfo.age || null
+						}
+					}
+					
+					// 回显女方用户信息
+					if (res.femaleUserId) {
+						const femaleInfo = res.femaleUserInfo || {}
+						this.selectedFemale = {
+							userId: res.femaleUserId,
+							name: femaleInfo.name || res.femaleRealName,
+							avatarUrl: femaleInfo.avatarUrl || '',
+							age: femaleInfo.age || null
+						}
+					}
+				}
+			} catch (e) {
+				console.error('加载回显数据失败', e)
+				uni.showToast({ title: '加载数据失败', icon: 'none' })
+			}
+		},
+		
+		async loadResources() {
+			if (!this.matchmakerId) {
+				setTimeout(() => this.loadResources(), 500)
+				return
+			}
+			
+			try {
+				// 加载男方用户列表(只查询已注册用户,user_id不为空)
+				const maleRes = await api.myResource.getRegisteredDropdown(this.matchmakerId, 1)
+				console.log('男方资源返回:', maleRes)
+				this.maleUsers = Array.isArray(maleRes) ? maleRes : (maleRes?.data || [])
+				
+				// 加载女方用户列表(只查询已注册用户,user_id不为空)
+				const femaleRes = await api.myResource.getRegisteredDropdown(this.matchmakerId, 2)
+				console.log('女方资源返回:', femaleRes)
+				this.femaleUsers = Array.isArray(femaleRes) ? femaleRes : (femaleRes?.data || [])
+			} catch (e) {
+				console.error('加载资源列表失败', e)
+			}
+		},
+		
+		openMalePicker() {
+			this.pickerGender = 1
+			this.searchKeyword = ''
+			this.$refs.userPickerPopup.open()
+		},
+		
+		openFemalePicker() {
+			this.pickerGender = 2
+			this.searchKeyword = ''
+			this.$refs.userPickerPopup.open()
+		},
+		
+		closeUserPicker() {
+			this.$refs.userPickerPopup.close()
+		},
+		
+		selectUser(user) {
+			if (this.pickerGender === 1) {
+				this.selectedMale = user
+				// 自动填充真实姓名(每次选择都更新)
+				this.formData.maleRealName = user.name || ''
+			} else {
+				this.selectedFemale = user
+				// 自动填充真实姓名(每次选择都更新)
+				this.formData.femaleRealName = user.name || ''
+			}
+			this.closeUserPicker()
+		},
+		
+		onSearchInput() {
+			// 防抖搜索
+			if (this.searchTimer) {
+				clearTimeout(this.searchTimer)
+			}
+			this.searchTimer = setTimeout(() => {
+				this.searchResources()
+			}, 300)
+		},
+		
+		async searchResources() {
+			if (!this.matchmakerId) return
+			
+			try {
+				// 如果关键词为空,重新加载当前性别的完整列表
+				if (!this.searchKeyword || !this.searchKeyword.trim()) {
+					const res = await api.myResource.getRegisteredDropdown(this.matchmakerId, this.pickerGender)
+					console.log('重新加载资源列表:', res)
+					const users = Array.isArray(res) ? res : (res?.data || [])
+					if (this.pickerGender === 1) {
+						this.maleUsers = users
+					} else {
+						this.femaleUsers = users
+					}
+					return
+				}
+				
+				// 使用已注册用户搜索接口(只返回user_id不为空的资源)
+				const res = await api.myResource.searchRegistered(this.matchmakerId, this.searchKeyword.trim(), this.pickerGender)
+				console.log('搜索资源返回:', res)
+				const users = Array.isArray(res) ? res : (res?.data || [])
+				if (this.pickerGender === 1) {
+					this.maleUsers = users
+				} else {
+					this.femaleUsers = users
+				}
+			} catch (e) {
+				console.error('搜索资源失败', e)
+			}
+		},
+		
+		async chooseImage() {
+			try {
+				// uni.chooseImage 在 Promise 模式下返回 [err, res] 格式
+				const [err, res] = await uni.chooseImage({
+					count: 9 - this.proofImages.length,
+					sizeType: ['compressed'],
+					sourceType: ['album', 'camera']
+				})
+				
+				console.log('选择图片返回 err:', err)
+				console.log('选择图片返回 res:', res)
+				
+				if (err) {
+					console.error('选择图片失败', err)
+					return
+				}
+				
+				if (res && res.tempFilePaths && res.tempFilePaths.length > 0) {
+					uni.showLoading({ title: '上传中...' })
+					
+					for (const tempPath of res.tempFilePaths) {
+						console.log('准备上传图片:', tempPath)
+						try {
+							const uploadRes = await this.uploadImage(tempPath)
+							console.log('上传结果:', uploadRes)
+							if (uploadRes) {
+								this.proofImages.push(uploadRes)
+							}
+						} catch (e) {
+							console.error('上传图片失败', e)
+							uni.showToast({ title: '图片上传失败', icon: 'none' })
+						}
+					}
+					
+					uni.hideLoading()
+					console.log('当前图片列表:', this.proofImages)
+				}
+			} catch (e) {
+				console.error('选择图片异常', e)
+			}
+		},
+		
+		async uploadImage(tempPath) {
+			return new Promise((resolve, reject) => {
+				uni.uploadFile({
+					url: 'http://localhost:8083/api/success-case-upload/upload-image',
+					filePath: tempPath,
+					name: 'file',
+					success: (res) => {
+						try {
+							const data = JSON.parse(res.data)
+							if (data.code === 200 && data.data) {
+								resolve(data.data)
+							} else {
+								uni.showToast({ title: data.message || '上传失败', icon: 'none' })
+								reject(new Error(data.message))
+							}
+						} catch (e) {
+							reject(e)
+						}
+					},
+					fail: (err) => {
+						reject(err)
+					}
+				})
+			})
+		},
+		
+		removeImage(index) {
+			this.proofImages.splice(index, 1)
+		},
+		
+		onDateChange(e) {
+			this.formData.caseDate = e.detail.value
+		},
+		
+		async submitForm() {
+			// 表单验证
+			if (!this.selectedMale) {
+				uni.showToast({ title: '请选择男方用户', icon: 'none' })
+				return
+			}
+			if (!this.selectedFemale) {
+				uni.showToast({ title: '请选择女方用户', icon: 'none' })
+				return
+			}
+			if (this.selectedMale.userId === this.selectedFemale.userId) {
+				uni.showToast({ title: '男方和女方不能是同一人', icon: 'none' })
+				return
+			}
+			if (!this.formData.maleRealName.trim()) {
+				uni.showToast({ title: '请填写男方真实姓名', icon: 'none' })
+				return
+			}
+			if (!this.formData.femaleRealName.trim()) {
+				uni.showToast({ title: '请填写女方真实姓名', icon: 'none' })
+				return
+			}
+			if (this.proofImages.length === 0) {
+				uni.showToast({ title: '请上传至少一张成功凭证图片', icon: 'none' })
+				return
+			}
+			
+			this.submitting = true
+			
+			try {
+				const submitData = {
+					matchmakerId: this.matchmakerId,
+					maleUserId: this.selectedMale.userId,  // 传递users表的user_id
+					femaleUserId: this.selectedFemale.userId,  // 传递users表的user_id
+					maleRealName: this.formData.maleRealName.trim(),
+					femaleRealName: this.formData.femaleRealName.trim(),
+					proofImages: this.proofImages,
+					caseType: this.formData.caseType,
+					caseDate: this.formData.caseDate || null
+				}
+				
+				const res = await api.successCaseUpload.submit(submitData)
+				console.log('提交结果:', res)
+				
+				// request函数成功时返回的是data字段的内容(这里是id)
+				// 如果能走到这里说明请求成功了
+				if (res !== undefined && res !== null) {
+					uni.showToast({ title: '提交成功', icon: 'success' })
+					setTimeout(() => {
+						uni.navigateBack()
+					}, 1500)
+				} else {
+					uni.showToast({ title: '提交失败', icon: 'none' })
+				}
+			} catch (e) {
+				console.error('提交失败', e)
+				uni.showToast({ title: '提交失败,请稍后重试', icon: 'none' })
+			} finally {
+				this.submitting = false
+			}
+		}
+	}
+}
+</script>
+
+<style lang="scss" scoped>
+/* 确保所有元素使用border-box */
+* {
+	box-sizing: border-box;
+}
+
+.success-case-upload {
+	min-height: 100vh;
+	background: linear-gradient(180deg, #f8e1f4 0%, #fff5f5 100%);
+	display: flex;
+	flex-direction: column;
+	width: 100%;
+	overflow-x: hidden;
+}
+
+.header {
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	padding: 60rpx 30rpx 30rpx;
+	background: linear-gradient(135deg, #d63384 0%, #9c27b0 100%);
+	
+	.back-btn {
+		width: 60rpx;
+		height: 60rpx;
+		background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z'/%3E%3C/svg%3E") no-repeat center;
+		background-size: contain;
+	}
+	
+	.header-title {
+		font-size: 36rpx;
+		font-weight: bold;
+		color: #fff;
+	}
+	
+	.placeholder {
+		width: 60rpx;
+	}
+}
+
+.content {
+	flex: 1;
+	padding: 30rpx 30rpx 30rpx 30rpx;
+	box-sizing: border-box;
+}
+
+.form-container {
+	display: flex;
+	flex-direction: column;
+	gap: 30rpx;
+	width: 100%;
+}
+
+.form-section {
+	background: #fff;
+	border-radius: 20rpx;
+	padding: 30rpx;
+	box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
+	box-sizing: border-box;
+	width: 100%;
+}
+
+.section-title {
+	display: flex;
+	align-items: center;
+	margin-bottom: 30rpx;
+	
+	.title-icon {
+		font-size: 36rpx;
+		margin-right: 16rpx;
+		
+		&.male { color: #2196F3; }
+		&.female { color: #E91E63; }
+		&.proof { color: #FF9800; }
+		&.info { color: #9C27B0; }
+	}
+	
+	.title-text {
+		font-size: 32rpx;
+		font-weight: bold;
+		color: #333;
+	}
+}
+
+.form-item {
+	margin-bottom: 30rpx;
+	
+	&:last-child {
+		margin-bottom: 0;
+	}
+}
+
+.form-label {
+	display: block;
+	font-size: 28rpx;
+	color: #666;
+	margin-bottom: 16rpx;
+	
+	.required {
+		color: #E91E63;
+	}
+}
+
+.form-hint {
+	display: block;
+	font-size: 24rpx;
+	color: #999;
+	margin-bottom: 16rpx;
+}
+
+.form-input {
+	width: 100%;
+	height: 88rpx;
+	background: #f5f5f5;
+	border-radius: 12rpx;
+	padding: 0 24rpx;
+	font-size: 28rpx;
+	box-sizing: border-box;
+	border: none;
+}
+
+.form-textarea {
+	width: 100%;
+	height: 200rpx;
+	background: #f5f5f5;
+	border-radius: 12rpx;
+	padding: 24rpx;
+	font-size: 28rpx;
+	box-sizing: border-box;
+}
+
+.char-count {
+	display: block;
+	text-align: right;
+	font-size: 24rpx;
+	color: #999;
+	margin-top: 8rpx;
+}
+
+.picker-wrapper {
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	height: 88rpx;
+	background: #f5f5f5;
+	border-radius: 12rpx;
+	padding: 0 24rpx;
+	
+	.placeholder-text {
+		color: #999;
+		font-size: 28rpx;
+	}
+	
+	.arrow-down {
+		width: 24rpx;
+		height: 24rpx;
+		border-right: 4rpx solid #999;
+		border-bottom: 4rpx solid #999;
+		transform: rotate(45deg);
+	}
+}
+
+.selected-user {
+	display: flex;
+	align-items: center;
+	flex: 1;
+	
+	.user-avatar {
+		width: 56rpx;
+		height: 56rpx;
+		border-radius: 50%;
+		margin-right: 16rpx;
+	}
+	
+	.user-name {
+		font-size: 28rpx;
+		color: #333;
+		margin-right: 16rpx;
+	}
+	
+	.user-age {
+		font-size: 24rpx;
+		color: #999;
+	}
+}
+
+.image-upload-area {
+	display: flex;
+	flex-wrap: wrap;
+	gap: 20rpx;
+}
+
+.image-item {
+	position: relative;
+	width: 200rpx;
+	height: 200rpx;
+	
+	.uploaded-image {
+		width: 100%;
+		height: 100%;
+		border-radius: 12rpx;
+	}
+	
+	.delete-btn {
+		position: absolute;
+		top: -16rpx;
+		right: -16rpx;
+		width: 40rpx;
+		height: 40rpx;
+		background: #E91E63;
+		border-radius: 50%;
+		color: #fff;
+		font-size: 28rpx;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+	}
+}
+
+.add-image-btn {
+	width: 200rpx;
+	height: 200rpx;
+	background: #f5f5f5;
+	border: 2rpx dashed #ccc;
+	border-radius: 12rpx;
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+	justify-content: center;
+	
+	.add-icon {
+		font-size: 60rpx;
+		color: #999;
+	}
+	
+	.add-text {
+		font-size: 24rpx;
+		color: #999;
+		margin-top: 8rpx;
+	}
+}
+
+.radio-group {
+	display: flex;
+	gap: 40rpx;
+}
+
+.radio-item {
+	display: flex;
+	align-items: center;
+	padding: 20rpx 40rpx;
+	background: #f5f5f5;
+	border-radius: 12rpx;
+	
+	&.active {
+		background: linear-gradient(135deg, #d63384 0%, #9c27b0 100%);
+		
+		.radio-circle {
+			border-color: #fff;
+			
+			&::after {
+				content: '';
+				width: 16rpx;
+				height: 16rpx;
+				background: #fff;
+				border-radius: 50%;
+			}
+		}
+		
+		.radio-text {
+			color: #fff;
+		}
+	}
+	
+	.radio-circle {
+		width: 32rpx;
+		height: 32rpx;
+		border: 4rpx solid #999;
+		border-radius: 50%;
+		margin-right: 16rpx;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+	}
+	
+	.radio-text {
+		font-size: 28rpx;
+		color: #333;
+	}
+}
+
+.date-picker {
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	height: 88rpx;
+	background: #f5f5f5;
+	border-radius: 12rpx;
+	padding: 0 24rpx;
+	
+	.date-text {
+		font-size: 28rpx;
+		color: #333;
+	}
+	
+	.placeholder-text {
+		font-size: 28rpx;
+		color: #999;
+	}
+	
+	.calendar-icon {
+		font-size: 36rpx;
+	}
+}
+
+.submit-section {
+	margin-top: 20rpx;
+	padding: 0 0 60rpx 0;
+	width: 100%;
+	box-sizing: border-box;
+}
+
+.submit-btn {
+	width: 100%;
+	height: 96rpx;
+	background: linear-gradient(135deg, #d63384 0%, #9c27b0 100%);
+	border-radius: 48rpx;
+	color: #fff;
+	font-size: 32rpx;
+	font-weight: bold;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	border: none;
+	
+	&[disabled] {
+		opacity: 0.6;
+	}
+}
+
+.submit-hint {
+	display: block;
+	text-align: center;
+	font-size: 24rpx;
+	color: #999;
+	margin-top: 20rpx;
+}
+
+/* 用户选择弹窗 */
+.user-picker-popup {
+	background: #fff;
+	border-radius: 30rpx 30rpx 0 0;
+	max-height: 80vh;
+	display: flex;
+	flex-direction: column;
+}
+
+.popup-header {
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	padding: 30rpx;
+	border-bottom: 1rpx solid #eee;
+	
+	.popup-title {
+		font-size: 32rpx;
+		font-weight: bold;
+		color: #333;
+	}
+	
+	.close-btn {
+		font-size: 48rpx;
+		color: #999;
+		line-height: 1;
+	}
+}
+
+.search-box {
+	display: flex;
+	align-items: center;
+	margin: 20rpx 30rpx;
+	background: #f5f5f5;
+	border-radius: 40rpx;
+	padding: 0 30rpx;
+	
+	.search-input {
+		flex: 1;
+		height: 80rpx;
+		font-size: 28rpx;
+	}
+	
+	.search-icon {
+		font-size: 36rpx;
+	}
+}
+
+.user-list {
+	flex: 1;
+	max-height: 60vh;
+	padding: 0 30rpx;
+}
+
+.user-item {
+	display: flex;
+	align-items: center;
+	padding: 24rpx 0;
+	border-bottom: 1rpx solid #f5f5f5;
+	
+	.user-avatar {
+		width: 80rpx;
+		height: 80rpx;
+		border-radius: 50%;
+		margin-right: 24rpx;
+	}
+	
+	.user-info {
+		flex: 1;
+		
+		.user-name {
+			display: block;
+			font-size: 30rpx;
+			color: #333;
+			margin-bottom: 8rpx;
+		}
+		
+		.user-detail {
+			font-size: 24rpx;
+			color: #999;
+		}
+	}
+}
+
+.empty-tip {
+	padding: 60rpx 0;
+	text-align: center;
+	color: #999;
+	font-size: 28rpx;
+}
+</style>

+ 94 - 0
LiangZhiYUMao/utils/api.js

@@ -866,6 +866,100 @@ export default {
       method: 'POST',
       data: { makerId, ruleType, reason }
     })
+  },
+
+  // 我的资源相关(通过网关访问8081服务)
+  myResource: {
+    // 获取资源列表
+    getList: (matchmakerId, keyword, pageNum = 1, pageSize = 10) => {
+      let url = `/my-resource/list?matchmakerId=${matchmakerId}&pageNum=${pageNum}&pageSize=${pageSize}`
+      if (keyword) {
+        url += `&keyword=${encodeURIComponent(keyword)}`
+      }
+      return request({ url })
+    },
+    
+    // 搜索资源(按姓名或手机号)
+    search: (matchmakerId, keyword, gender) => {
+      let url = `/my-resource/search?matchmakerId=${matchmakerId}`
+      if (keyword) {
+        url += `&keyword=${encodeURIComponent(keyword)}`
+      }
+      if (gender) {
+        url += `&gender=${gender}`
+      }
+      return request({ url })
+    },
+    
+    // 获取资源下拉列表
+    getDropdown: (matchmakerId, gender) => {
+      let url = `/my-resource/dropdown?matchmakerId=${matchmakerId}`
+      if (gender) {
+        url += `&gender=${gender}`
+      }
+      return request({ url })
+    },
+    
+    // 获取已注册用户的资源下拉列表(user_id不为空)
+    getRegisteredDropdown: (matchmakerId, gender) => {
+      let url = `/my-resource/registered-dropdown?matchmakerId=${matchmakerId}`
+      if (gender) {
+        url += `&gender=${gender}`
+      }
+      return request({ url })
+    },
+    
+    // 搜索已注册用户的资源(user_id不为空)
+    searchRegistered: (matchmakerId, keyword, gender) => {
+      let url = `/my-resource/registered-search?matchmakerId=${matchmakerId}`
+      if (keyword) {
+        url += `&keyword=${encodeURIComponent(keyword)}`
+      }
+      if (gender) {
+        url += `&gender=${gender}`
+      }
+      return request({ url })
+    }
+  },
+
+  // 撮合成功案例上传相关(通过网关访问1004服务)
+  successCaseUpload: {
+    // 提交成功案例
+    submit: (data) => request({
+      url: '/success-case-upload/submit',
+      method: 'POST',
+      data
+    }),
+    
+    // 获取成功案例列表
+    getList: (matchmakerId, pageNum = 1, pageSize = 10) => request({
+      url: `/success-case-upload/list?matchmakerId=${matchmakerId}&pageNum=${pageNum}&pageSize=${pageSize}`
+    }),
+    
+    // 获取审核记录列表
+    getAuditRecords: (matchmakerId, auditStatus, pageNum = 1, pageSize = 20) => {
+      let url = `/success-case-upload/audit-records?matchmakerId=${matchmakerId}&pageNum=${pageNum}&pageSize=${pageSize}`
+      if (auditStatus !== null && auditStatus !== undefined) {
+        url += `&auditStatus=${auditStatus}`
+      }
+      return request({ url })
+    },
+    
+    // 获取审核记录详情
+    getAuditRecordDetail: (id) => request({
+      url: `/success-case-upload/audit-records/${id}`
+    }),
+    
+    // 标记审核记录为已读
+    markAsRead: (id) => request({
+      url: `/success-case-upload/audit-records/${id}/read`,
+      method: 'POST'
+    }),
+    
+    // 获取未读审核记录数量
+    getUnreadCount: (matchmakerId) => request({
+      url: `/success-case-upload/audit-records/unread-count?matchmakerId=${matchmakerId}`
+    })
   }
 }
 

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

@@ -188,6 +188,22 @@ spring:
           filters:
             - StripPrefix=0
 
+        # 我的资源服务路由(homePage服务)
+        - id: my-resource-route
+          uri: http://localhost:8081
+          predicates:
+            - Path=/api/my-resource/**
+          filters:
+            - StripPrefix=0
+
+        # 撮合成功案例上传路由(websocket服务)
+        - id: success-case-upload-route
+          uri: http://localhost:1004
+          predicates:
+            - Path=/api/success-case-upload/**
+          filters:
+            - StripPrefix=0
+
         # 首页服务路由(兜底路由)
         - id: homepage-route
           uri: http://localhost:8081

+ 3 - 0
marriageAdmin-vue/src/config/api.js

@@ -50,6 +50,9 @@ export const API_ENDPOINTS = {
   MATCHMAKER_AUDIT_LIST: '/admin/marr-apply/list',
   MATCHMAKER_AUDIT_APPROVE: '/admin/marr-apply/approve',
   MATCHMAKER_AUDIT_DELETE: '/admin/marr-apply/delete',
+  SUCCESS_CASE_UPLOAD_LIST: '/admin/success-case-upload/list',
+  SUCCESS_CASE_UPLOAD_APPROVE: '/admin/success-case-upload/approve',
+  SUCCESS_CASE_UPLOAD_REJECT: '/admin/success-case-upload/reject',
   
   // 课程管理
   COURSE_LIST: '/api/course/list',

+ 1 - 0
marriageAdmin-vue/src/layouts/MainLayout.vue

@@ -59,6 +59,7 @@
           <el-menu-item index="/matchmaker/audit">红娘审核</el-menu-item>
           <el-menu-item index="/matchmaker/create">添加红娘</el-menu-item>
           <el-menu-item index="/matchmaker/points-product">积分商品</el-menu-item>
+          <el-menu-item index="/matchmaker/case-audit">案例审核</el-menu-item>
         </el-sub-menu>
         
         <el-sub-menu index="/course">

+ 6 - 0
marriageAdmin-vue/src/router/index.js

@@ -115,6 +115,12 @@ const router = createRouter({
               name: 'PointsProduct',
               component: () => import('@/views/points-product/PointsProductList.vue'),
               meta: { title: '积分商品' }
+            },
+            {
+              path: 'case-audit',
+              name: 'CaseAudit',
+              component: () => import('@/views/matchmaker/CaseAudit.vue'),
+              meta: { title: '案例审核' }
             }
           ]
         },

+ 359 - 0
marriageAdmin-vue/src/views/matchmaker/CaseAudit.vue

@@ -0,0 +1,359 @@
+<template>
+  <div class="case-audit-container">
+    <h2 class="page-title">案例审核</h2>
+
+    <el-card shadow="never" class="toolbar-card">
+      <el-space wrap>
+        <el-input
+          v-model="filters.maleRealName"
+          placeholder="按男方姓名模糊搜索"
+          clearable
+          style="width: 200px"
+          @keyup.enter="loadList"
+        >
+          <template #append>
+            <el-button icon="Search" @click="loadList" />
+          </template>
+        </el-input>
+        <el-input
+          v-model="filters.femaleRealName"
+          placeholder="按女方姓名模糊搜索"
+          clearable
+          style="width: 200px"
+          @keyup.enter="loadList"
+        >
+          <template #append>
+            <el-button icon="Search" @click="loadList" />
+          </template>
+        </el-input>
+        <el-select
+          v-model="filters.auditStatus"
+          placeholder="审核状态"
+          clearable
+          style="width: 150px"
+          @change="loadList"
+        >
+          <el-option label="待审核" :value="0" />
+          <el-option label="审核通过" :value="1" />
+          <el-option label="审核失败" :value="2" />
+        </el-select>
+        <el-button icon="Refresh" @click="resetFilters">重置</el-button>
+      </el-space>
+    </el-card>
+
+    <el-card shadow="never" class="table-card">
+      <el-table v-loading="loading" :data="list" stripe>
+        <template #empty>
+          <div class="custom-empty-state">
+            <el-icon class="empty-icon"><Document /></el-icon>
+            <div class="empty-text">暂无案例记录</div>
+            <div class="empty-hint">等待红娘上传成功案例后在此审核</div>
+          </div>
+        </template>
+
+        <el-table-column type="index" label="序号" width="60" />
+        <el-table-column prop="maleRealName" label="男方姓名" min-width="120">
+          <template #default="{ row }">
+            <span class="data-highlight">{{ row.maleRealName || '-' }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="femaleRealName" label="女方姓名" min-width="120">
+          <template #default="{ row }">
+            <span class="data-highlight">{{ row.femaleRealName || '-' }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="caseType" label="案例类型" width="100" align="center">
+          <template #default="{ row }">
+            <el-tag size="small" :type="row.caseType === 1 ? 'success' : 'warning'">
+              {{ row.caseType === 1 ? '订婚' : row.caseType === 2 ? '领证结婚' : '-' }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="caseDate" label="成功日期" width="120" align="center">
+          <template #default="{ row }">
+            <span class="text-muted">{{ formatDate(row.caseDate) }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="auditStatus" label="审核状态" width="120" align="center">
+          <template #default="{ row }">
+            <el-tag
+              size="small"
+              :type="getAuditStatusType(row.auditStatus)"
+              effect="light"
+            >
+              {{ getAuditStatusText(row.auditStatus) }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="auditRemark" label="审核备注" min-width="200" show-overflow-tooltip>
+          <template #default="{ row }">
+            <span class="text-muted">{{ row.auditRemark || '-' }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="createdAt" label="上传时间" width="180">
+          <template #default="{ row }">
+            <span class="text-muted">{{ formatDateTime(row.createdAt) }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="updatedAt" label="更改时间" width="180">
+          <template #default="{ row }">
+            <span class="text-muted">{{ formatDateTime(row.updatedAt) }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="auditedAt" label="审核时间" width="180">
+          <template #default="{ row }">
+            <span class="text-muted">{{ formatDateTime(row.auditedAt) }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="auditDuration" label="审核耗时" width="120" align="center">
+          <template #default="{ row }">
+            <span v-if="row.auditDuration" class="data-highlight">{{ row.auditDuration }}</span>
+            <span v-else style="color: #999;">-</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="120" fixed="right">
+          <template #default="{ row }">
+            <el-button
+              type="primary"
+              size="small"
+              @click="handleAudit(row)"
+              :disabled="row.auditStatus === 1 || row.auditStatus === 2"
+            >
+              审核
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <div class="pagination-container">
+        <el-pagination
+          v-model:current-page="currentPage"
+          v-model:page-size="pageSize"
+          :total="total"
+          :page-sizes="[10, 20, 50, 100]"
+          layout="total, sizes, prev, pager, next, jumper"
+          @size-change="loadList"
+          @current-change="loadList"
+        />
+      </div>
+    </el-card>
+
+    <!-- 审核弹框 -->
+    <el-dialog
+      v-model="auditDialogVisible"
+      title="案例审核"
+      width="500px"
+      :close-on-click-modal="false"
+    >
+      <el-form :model="auditForm" label-width="100px">
+        <el-form-item label="男方姓名">
+          <span>{{ currentRow?.maleRealName || '-' }}</span>
+        </el-form-item>
+        <el-form-item label="女方姓名">
+          <span>{{ currentRow?.femaleRealName || '-' }}</span>
+        </el-form-item>
+        <el-form-item label="审核操作" required>
+          <el-radio-group v-model="auditForm.action">
+            <el-radio :label="1">审核通过</el-radio>
+            <el-radio :label="2">审核不通过</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item
+          v-if="auditForm.action === 2"
+          label="审核备注"
+          required
+          :rules="[{ required: true, message: '请输入审核备注', trigger: 'blur' }]"
+        >
+          <el-input
+            v-model="auditForm.auditRemark"
+            type="textarea"
+            :rows="4"
+            placeholder="请输入审核不通过的原因(失败原因)"
+            maxlength="500"
+            show-word-limit
+          />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="auditDialogVisible = false">取消</el-button>
+        <el-button
+          type="primary"
+          @click="submitAudit"
+          :loading="auditing"
+          :disabled="auditForm.action === 2 && !auditForm.auditRemark?.trim()"
+        >
+          确定
+        </el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Document } from '@element-plus/icons-vue'
+import request from '@/utils/request'
+import { API_ENDPOINTS } from '@/config/api'
+
+const loading = ref(false)
+const list = ref([])
+const total = ref(0)
+const currentPage = ref(1)
+const pageSize = ref(10)
+const auditing = ref(false)
+const auditDialogVisible = ref(false)
+const currentRow = ref(null)
+
+const filters = reactive({
+  maleRealName: '',
+  femaleRealName: '',
+  auditStatus: null
+})
+
+const auditForm = reactive({
+  action: 1, // 1-通过, 2-不通过
+  auditRemark: ''
+})
+
+const loadList = async () => {
+  loading.value = true
+  try {
+    const params = {
+      page: currentPage.value,
+      pageSize: pageSize.value
+    }
+    if (filters.maleRealName && filters.maleRealName.trim()) {
+      params.maleRealName = filters.maleRealName.trim()
+    }
+    if (filters.femaleRealName && filters.femaleRealName.trim()) {
+      params.femaleRealName = filters.femaleRealName.trim()
+    }
+    if (filters.auditStatus !== null && filters.auditStatus !== undefined) {
+      params.auditStatus = filters.auditStatus
+    }
+    const res = await request.get(API_ENDPOINTS.SUCCESS_CASE_UPLOAD_LIST, { params })
+    if (res.code === 200) {
+      const data = res.data || {}
+      list.value = data.list || data.records || []
+      total.value = data.total || list.value.length
+    }
+  } catch (error) {
+    console.error('加载案例列表失败:', error)
+    ElMessage.error('加载案例列表失败')
+  } finally {
+    loading.value = false
+  }
+}
+
+const resetFilters = () => {
+  filters.maleRealName = ''
+  filters.femaleRealName = ''
+  filters.auditStatus = null
+  currentPage.value = 1
+  loadList()
+}
+
+const getAuditStatusText = (status) => {
+  const statusMap = {
+    0: '待审核',
+    1: '审核通过',
+    2: '审核失败'
+  }
+  return statusMap[status] || '未知'
+}
+
+const getAuditStatusType = (status) => {
+  const typeMap = {
+    0: 'warning',
+    1: 'success',
+    2: 'danger'
+  }
+  return typeMap[status] || ''
+}
+
+const formatDate = (date) => {
+  if (!date) return '-'
+  const d = new Date(date)
+  return d.toLocaleDateString('zh-CN')
+}
+
+const formatDateTime = (date) => {
+  if (!date) return '-'
+  const d = new Date(date)
+  return d.toLocaleString('zh-CN', {
+    year: 'numeric',
+    month: '2-digit',
+    day: '2-digit',
+    hour: '2-digit',
+    minute: '2-digit'
+  })
+}
+
+const handleAudit = (row) => {
+  if (row.auditStatus === 1 || row.auditStatus === 2) {
+    ElMessage.warning('该案例已审核,无需重复操作')
+    return
+  }
+  currentRow.value = row
+  auditForm.action = 1
+  auditForm.auditRemark = ''
+  auditDialogVisible.value = true
+}
+
+const submitAudit = async () => {
+  if (!currentRow.value) {
+    ElMessage.error('请选择要审核的案例')
+    return
+  }
+
+  if (auditForm.action === 2 && !auditForm.auditRemark?.trim()) {
+    ElMessage.warning('审核不通过时,必须填写审核备注')
+    return
+  }
+
+  try {
+    auditing.value = true
+    let res
+
+    if (auditForm.action === 1) {
+      // 审核通过
+      res = await request.post(
+        `${API_ENDPOINTS.SUCCESS_CASE_UPLOAD_APPROVE}/${currentRow.value.id}`
+      )
+    } else {
+      // 审核不通过
+      res = await request.post(
+        `${API_ENDPOINTS.SUCCESS_CASE_UPLOAD_REJECT}/${currentRow.value.id}`,
+        {
+          auditRemark: auditForm.auditRemark.trim()
+        }
+      )
+    }
+
+    if (res.code === 200) {
+      ElMessage.success(auditForm.action === 1 ? '审核通过成功' : '审核不通过操作成功')
+      auditDialogVisible.value = false
+      loadList()
+    } else {
+      ElMessage.error(res.msg || '审核操作失败')
+    }
+  } catch (error) {
+    console.error('审核操作失败:', error)
+    ElMessage.error(error.response?.data?.msg || error.message || '审核操作失败')
+  } finally {
+    auditing.value = false
+  }
+}
+
+onMounted(loadList)
+</script>
+
+<style scoped>
+@import '@/assets/list-common.css';
+
+.case-audit-container {
+  padding: 0;
+}
+</style>
+

+ 16 - 9
marriageAdmin-vue/src/views/matchmaker/MatchmakerAudit.vue

@@ -155,9 +155,9 @@ const resetFilters = () => {
   loadList()
 }
 
-// 判断是否已审核通过(根据updateMan字段判断
+// 判断是否已审核通过(根据status字段判断,status=0表示审核通过
 const isApproved = (row) => {
-  return !!(row.updateMan || row.update_man)
+  return row.status === 0
 }
 
 // 审核通过
@@ -167,14 +167,20 @@ const handleApprove = async (row) => {
     return
   }
   
+  if (row.status === 0) {
+    ElMessage.warning('该申请已审核通过,无需重复操作')
+    return
+  }
+  
   try {
     await ElMessageBox.confirm(
-      `确定要通过 ${row.name || '该用户'} 的红娘申请吗?通过后该用户将成为红娘。`,
-      '确认审核',
+      `确定要通过 ${row.name || '该用户'} 的红娘申请吗?\n通过后该用户将成为红娘,此操作将更新审核状态。`,
+      '确认审核通过',
       {
-        confirmButtonText: '确定',
+        confirmButtonText: '确定通过',
         cancelButtonText: '取消',
-        type: 'warning'
+        type: 'warning',
+        dangerouslyUseHTMLString: false
       }
     )
     
@@ -205,12 +211,13 @@ const handleApprove = async (row) => {
 const handleDelete = async (row) => {
   try {
     await ElMessageBox.confirm(
-      `确定要删除 ${row.name || '该用户'} 的红娘申请吗?此操作不可恢复。`,
+      `确定要删除 ${row.name || '该用户'} 的红娘申请吗?\n删除后该申请将不再显示在列表中,此操作为逻辑删除。`,
       '确认删除',
       {
-        confirmButtonText: '确定',
+        confirmButtonText: '确定删除',
         cancelButtonText: '取消',
-        type: 'warning'
+        type: 'warning',
+        dangerouslyUseHTMLString: false
       }
     )
     

+ 10 - 0
marriageAdmin-vue/src/views/user/UserList.vue

@@ -111,6 +111,12 @@
           </template>
         </el-table-column>
         <el-table-column prop="createdAt" label="注册时间" width="170" />
+        <el-table-column prop="auditedAt" label="审核时间" width="120" align="center">
+          <template #default="{ row }">
+            <span v-if="row.auditedAt" class="data-highlight">{{ row.auditedAt }}</span>
+            <span v-else style="color: #999;">-</span>
+          </template>
+        </el-table-column>
         <el-table-column prop="lastLoginAt" label="最后登录" width="170">
           <template #default="{ row }">
             {{ row.lastLoginAt || '-' }}
@@ -226,6 +232,10 @@
         </el-descriptions-item>
         <el-descriptions-item label="注册时间" :span="2">{{ currentUser.createdAt }}</el-descriptions-item>
         <el-descriptions-item label="最后更新" :span="2">{{ currentUser.updatedAt }}</el-descriptions-item>
+        <el-descriptions-item label="审核时间" :span="2">
+          <span v-if="currentUser.auditedAt" class="data-highlight">{{ currentUser.auditedAt }}</span>
+          <span v-else>-</span>
+        </el-descriptions-item>
         <el-descriptions-item label="最后登录" :span="2">{{ currentUser.lastLoginAt || '-' }}</el-descriptions-item>
         <el-descriptions-item label="最后活跃" :span="2">{{ currentUser.lastActiveAt || '-' }}</el-descriptions-item>
         <el-descriptions-item label="头像" :span="2">

+ 0 - 1
service/admin/src/main/java/com/zhentao/controller/AuthController.java

@@ -54,7 +54,6 @@ public class AuthController {
             
             Map<String, Object> data = new HashMap<>();
             data.put("token", token);
-            
             // 返回用户信息(不包含密码)
             user.setPassword(null);
             data.put("userInfo", user);

+ 242 - 0
service/admin/src/main/java/com/zhentao/controller/MatchmakerSuccessCaseUploadController.java

@@ -0,0 +1,242 @@
+package com.zhentao.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zhentao.common.Result;
+import com.zhentao.entity.Matchmaker;
+import com.zhentao.entity.MatchmakerSuccessCaseUpload;
+import com.zhentao.mapper.MatchmakerMapper;
+import com.zhentao.service.MatchmakerSuccessCaseUploadService;
+import com.zhentao.vo.MatchmakerSuccessCaseUploadVO;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.bind.annotation.*;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 红娘成功案例审核控制器
+ */
+@RestController
+@RequestMapping("/admin/success-case-upload")
+@CrossOrigin(origins = "*")
+public class MatchmakerSuccessCaseUploadController {
+
+    @Autowired
+    private MatchmakerSuccessCaseUploadService successCaseUploadService;
+
+    @Autowired
+    private MatchmakerMapper matchmakerMapper;
+
+    /**
+     * 案例列表(分页查询)
+     * 支持按男方姓名、女方姓名模糊查询,按审核状态筛选
+     */
+    @GetMapping("/list")
+    public Result<Map<String, Object>> list(
+            @RequestParam(defaultValue = "1") Integer page,
+            @RequestParam(defaultValue = "10") Integer pageSize,
+            @RequestParam(required = false) String maleRealName,
+            @RequestParam(required = false) String femaleRealName,
+            @RequestParam(required = false) Integer auditStatus) {
+        try {
+            Page<MatchmakerSuccessCaseUpload> pageInfo = new Page<>(page, pageSize);
+            QueryWrapper<MatchmakerSuccessCaseUpload> queryWrapper = new QueryWrapper<>();
+
+            // 男方姓名模糊查询
+            if (maleRealName != null && !maleRealName.trim().isEmpty()) {
+                queryWrapper.like("male_real_name", maleRealName.trim());
+            }
+
+            // 女方姓名模糊查询
+            if (femaleRealName != null && !femaleRealName.trim().isEmpty()) {
+                queryWrapper.like("female_real_name", femaleRealName.trim());
+            }
+
+            // 审核状态筛选
+            if (auditStatus != null) {
+                queryWrapper.eq("audit_status", auditStatus);
+            }
+
+            // 按创建时间倒序
+            queryWrapper.orderByDesc("created_at");
+
+            Page<MatchmakerSuccessCaseUpload> result = successCaseUploadService.page(pageInfo, queryWrapper);
+
+            // 转换为VO列表
+            List<MatchmakerSuccessCaseUploadVO> voList = new ArrayList<>();
+            for (MatchmakerSuccessCaseUpload upload : result.getRecords()) {
+                MatchmakerSuccessCaseUploadVO vo = convertToVO(upload);
+                voList.add(vo);
+            }
+
+            Map<String, Object> data = new HashMap<>();
+            data.put("list", voList);
+            data.put("total", result.getTotal());
+            data.put("page", page);
+            data.put("pageSize", pageSize);
+
+            return Result.success(data);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("查询案例列表失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 审核通过
+     * 审核通过时,根据points_reward字段的值给对应红娘增加积分
+     */
+    @PostMapping("/approve/{id}")
+    @Transactional(rollbackFor = Exception.class)
+    public Result<String> approve(@PathVariable Long id) {
+        try {
+            MatchmakerSuccessCaseUpload upload = successCaseUploadService.getById(id);
+            if (upload == null) {
+                return Result.error("案例不存在");
+            }
+
+            // 根据matchmakerId查找红娘并增加积分
+            if (upload.getMatchmakerId() != null) {
+                // 获取积分奖励值,如果为null或小于等于0则不增加积分
+                Integer pointsReward = upload.getPointsReward();
+                if (pointsReward != null && pointsReward > 0) {
+                    Matchmaker matchmaker = matchmakerMapper.selectById(upload.getMatchmakerId());
+                    if (matchmaker != null) {
+                        // 获取当前积分,如果为null则默认为0
+                        Integer currentPoints = matchmaker.getPoints() != null ? matchmaker.getPoints() : 0;
+                        // 积分加上pointsReward的值
+                        matchmaker.setPoints(currentPoints + pointsReward);
+                        // 更新红娘积分
+                        matchmakerMapper.updateById(matchmaker);
+                    } else {
+                        return Result.error("红娘信息不存在");
+                    }
+                }
+            }
+
+            upload.setAuditStatus(1); // 审核通过
+            upload.setAuditedAt(new Date());
+            upload.setUpdatedAt(new Date());
+
+            boolean success = successCaseUploadService.updateById(upload);
+            if (success) {
+                Integer pointsReward = upload.getPointsReward();
+                if (pointsReward != null && pointsReward > 0) {
+                    return Result.success("审核通过成功,已为红娘增加" + pointsReward + "积分");
+                } else {
+                    return Result.success("审核通过成功");
+                }
+            } else {
+                return Result.error("审核通过失败");
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("审核通过失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 审核不通过
+     */
+    @PostMapping("/reject/{id}")
+    public Result<String> reject(@PathVariable Long id, @RequestBody Map<String, String> request) {
+        try {
+            MatchmakerSuccessCaseUpload upload = successCaseUploadService.getById(id);
+            if (upload == null) {
+                return Result.error("案例不存在");
+            }
+
+            String auditRemark = request.get("auditRemark");
+            if (auditRemark == null || auditRemark.trim().isEmpty()) {
+                return Result.error("审核备注不能为空");
+            }
+
+            upload.setAuditStatus(2); // 审核失败
+            upload.setAuditRemark(auditRemark.trim());
+            upload.setAuditedAt(new Date());
+            upload.setUpdatedAt(new Date());
+
+            boolean success = successCaseUploadService.updateById(upload);
+            if (success) {
+                return Result.success("审核不通过操作成功");
+            } else {
+                return Result.error("审核不通过操作失败");
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("审核不通过操作失败:" + e.getMessage());
+        }
+    }
+    
+    /**
+     * 转换为VO对象
+     */
+    private MatchmakerSuccessCaseUploadVO convertToVO(MatchmakerSuccessCaseUpload upload) {
+        MatchmakerSuccessCaseUploadVO vo = new MatchmakerSuccessCaseUploadVO();
+        BeanUtils.copyProperties(upload, vo);
+        
+        // 计算审核耗时(审核时间 - 上传时间)
+        vo.setAuditDuration(calculateAuditDuration(upload.getAuditedAt(), upload.getCreatedAt()));
+        
+        return vo;
+    }
+    
+    /**
+     * 计算审核耗时(审核时间 - 上传时间)
+     */
+    private String calculateAuditDuration(Date auditedAt, Date createdAt) {
+        if (auditedAt == null || createdAt == null) {
+            return "-";
+        }
+        
+        try {
+            Instant auditInstant = auditedAt.toInstant();
+            Instant createInstant = createdAt.toInstant();
+            Duration duration = Duration.between(createInstant, auditInstant);
+            
+            return formatDuration(duration);
+        } catch (Exception e) {
+            return "-";
+        }
+    }
+    
+    /**
+     * 格式化时间差为可读字符串
+     */
+    private String formatDuration(Duration duration) {
+        long totalSeconds = duration.getSeconds();
+        if (totalSeconds < 0) {
+            return "-";
+        }
+        
+        long days = totalSeconds / 86400;
+        long hours = (totalSeconds % 86400) / 3600;
+        long minutes = (totalSeconds % 3600) / 60;
+        long seconds = totalSeconds % 60;
+        
+        StringBuilder sb = new StringBuilder();
+        if (days > 0) {
+            sb.append(days).append("天");
+        }
+        if (hours > 0) {
+            sb.append(hours).append("小时");
+        }
+        if (minutes > 0) {
+            sb.append(minutes).append("分钟");
+        }
+        if (seconds > 0 && days == 0 && hours == 0) {
+            sb.append(seconds).append("秒");
+        }
+        
+        return sb.length() > 0 ? sb.toString() : "0秒";
+    }
+}
+

+ 46 - 2
service/admin/src/main/java/com/zhentao/controller/UserController.java

@@ -106,6 +106,9 @@ public class UserController {
         UserVO vo = new UserVO();
         BeanUtils.copyProperties(user, vo);
         
+        // 计算审核时间(updatedAt - createdAt)
+        calculateAuditedAt(vo, user);
+        
         // 填充VIP信息
         fillVipInfo(vo, user.getUserId());
         
@@ -115,6 +118,45 @@ public class UserController {
         return vo;
     }
     
+    /**
+     * 计算审核时间(updatedAt - createdAt)
+     */
+    private void calculateAuditedAt(UserVO vo, Users user) {
+        if (user.getUpdatedAt() != null && user.getCreatedAt() != null) {
+            Duration duration = Duration.between(user.getCreatedAt(), user.getUpdatedAt());
+            vo.setAuditedAt(formatDuration(duration));
+        } else {
+            vo.setAuditedAt(null);
+        }
+    }
+    
+    /**
+     * 格式化时间差为可读字符串
+     */
+    private String formatDuration(Duration duration) {
+        long totalSeconds = duration.getSeconds();
+        long days = totalSeconds / 86400;
+        long hours = (totalSeconds % 86400) / 3600;
+        long minutes = (totalSeconds % 3600) / 60;
+        long seconds = totalSeconds % 60;
+        
+        StringBuilder sb = new StringBuilder();
+        if (days > 0) {
+            sb.append(days).append("天");
+        }
+        if (hours > 0) {
+            sb.append(hours).append("小时");
+        }
+        if (minutes > 0) {
+            sb.append(minutes).append("分钟");
+        }
+        if (seconds > 0 && days == 0 && hours == 0) {
+            sb.append(seconds).append("秒");
+        }
+        
+        return sb.length() > 0 ? sb.toString() : "0秒";
+    }
+    
     /**
      * 填充VIP信息
      */
@@ -260,13 +302,15 @@ public class UserController {
      * 用户详情
      */
     @GetMapping("/detail/{userId}")
-    public Result<Users> detail(@PathVariable Integer userId) {
+    public Result<UserVO> detail(@PathVariable Integer userId) {
         try {
             Users user = usersMapper.selectById(userId);
             if (user == null) {
                 return Result.error("用户不存在");
             }
-            return Result.success(user);
+            // 转换为UserVO以包含审核时间等计算字段
+            UserVO vo = convertToUserVO(user);
+            return Result.success(vo);
         } catch (Exception e) {
             e.printStackTrace();
             return Result.error("查询失败:" + e.getMessage());

+ 9 - 1
service/admin/src/main/java/com/zhentao/entity/AdminUser.java

@@ -4,9 +4,10 @@ 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 java.util.Date;
 import lombok.Data;
 
+import java.util.Date;
+
 /**
  * 管理员用户表
  * @TableName admin_user
@@ -55,6 +56,13 @@ public class AdminUser {
      */
     private Integer status;
 
+    /**
+     * 角色:admin-普通管理员 super-超级管理员
+     * 注意:此字段已废弃,角色信息现在通过 admin_user_role 关联表获取
+     */
+    @TableField(exist = false)
+    private String role;
+
     /**
      * 创建时间
      */

+ 26 - 0
service/admin/src/main/java/com/zhentao/entity/Area.java

@@ -0,0 +1,26 @@
+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;
+
+/**
+ * 区域实体类
+ */
+@Data
+@TableName("area")
+public class Area implements Serializable {
+    
+    private static final long serialVersionUID = 1L;
+    
+    @TableId(type = IdType.AUTO)
+    private Integer id;
+    
+    private String name;
+    
+    private Integer cityId;
+}
+

+ 26 - 0
service/admin/src/main/java/com/zhentao/entity/City.java

@@ -0,0 +1,26 @@
+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;
+
+/**
+ * 城市实体类
+ */
+@Data
+@TableName("city")
+public class City implements Serializable {
+    
+    private static final long serialVersionUID = 1L;
+    
+    @TableId(type = IdType.AUTO)
+    private Integer id;
+    
+    private String name;
+    
+    private Integer provinceId;
+}
+

+ 4 - 0
service/admin/src/main/java/com/zhentao/entity/MarrApply.java

@@ -94,6 +94,10 @@ public class MarrApply {
      * 状态0-正常,1-禁止
      */
     private Integer status;
+    /**
+     * 删除状态0-正常,1-禁止   默认为0
+     * */
+    private String idStatus;
 
     @Override
     public boolean equals(Object that) {

+ 5 - 0
service/admin/src/main/java/com/zhentao/entity/Matchmaker.java

@@ -23,6 +23,11 @@ public class Matchmaker implements Serializable {
     @TableId(value = "matchmaker_id", type = IdType.AUTO)
     private Integer matchmakerId;
     
+    /**
+     * 红娘对应的用户ID
+     */
+    private Integer userId;
+    
     private String username;
     private String password;
     private String phone;

+ 217 - 0
service/admin/src/main/java/com/zhentao/entity/MatchmakerSuccessCaseUpload.java

@@ -0,0 +1,217 @@
+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 java.math.BigDecimal;
+import java.util.Date;
+import lombok.Data;
+
+/**
+ * 红娘上传成功案例表
+ * @TableName matchmaker_success_case_upload
+ */
+@TableName(value ="matchmaker_success_case_upload")
+@Data
+public class MatchmakerSuccessCaseUpload {
+    /**
+     * 主键ID
+     */
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 撮合红娘ID
+     */
+    private Integer matchmakerId;
+
+    /**
+     * 男方用户ID
+     */
+    private Integer maleUserId;
+
+    /**
+     * 女方用户ID
+     */
+    private Integer femaleUserId;
+
+    /**
+     * 男方真实姓名
+     */
+    private String maleRealName;
+
+    /**
+     * 女方真实姓名
+     */
+    private String femaleRealName;
+
+    /**
+     * 成功凭证图片路径(JSON数组格式)
+     */
+    private String proofImages;
+
+    /**
+     * 案例类型: 1-订婚 2-领证结婚
+     */
+    private Integer caseType;
+
+    /**
+     * 成功日期(结婚日期/确定关系日期)
+     */
+    private Date caseDate;
+
+    /**
+     * 审核状态:0-待审核 1-审核通过 2-审核失败 3-核实中
+     */
+    private Integer auditStatus;
+
+    /**
+     * 审核备注(失败原因)
+     */
+    private String auditRemark;
+
+    /**
+     * 审核人ID(管理员)
+     */
+    private Integer auditorId;
+
+    /**
+     * 积分奖励
+     */
+    private Integer pointsReward;
+
+    /**
+     * 现金奖励
+     */
+    private BigDecimal cashReward;
+
+    /**
+     * 奖励状态:0-未发放 1-已发放
+     */
+    private Integer rewardStatus;
+
+    /**
+     * 是否发布到成功案例展示:0-否 1-是
+     */
+    private Integer isPublished;
+
+    /**
+     * 关联的成功案例ID
+     */
+    private Integer publishedCaseId;
+
+    /**
+     * 0未读         1已读
+     */
+    private Integer isRead;
+
+    /**
+     * 上传时间
+     */
+    private Date createdAt;
+
+    /**
+     * 更新时间
+     */
+    private Date updatedAt;
+
+    /**
+     * 审核时间
+     */
+    private Date auditedAt;
+
+    @Override
+    public boolean equals(Object that) {
+        if (this == that) {
+            return true;
+        }
+        if (that == null) {
+            return false;
+        }
+        if (getClass() != that.getClass()) {
+            return false;
+        }
+        MatchmakerSuccessCaseUpload other = (MatchmakerSuccessCaseUpload) that;
+        return (this.getId() == null ? other.getId() == null : this.getId().equals(other.getId()))
+            && (this.getMatchmakerId() == null ? other.getMatchmakerId() == null : this.getMatchmakerId().equals(other.getMatchmakerId()))
+            && (this.getMaleUserId() == null ? other.getMaleUserId() == null : this.getMaleUserId().equals(other.getMaleUserId()))
+            && (this.getFemaleUserId() == null ? other.getFemaleUserId() == null : this.getFemaleUserId().equals(other.getFemaleUserId()))
+            && (this.getMaleRealName() == null ? other.getMaleRealName() == null : this.getMaleRealName().equals(other.getMaleRealName()))
+            && (this.getFemaleRealName() == null ? other.getFemaleRealName() == null : this.getFemaleRealName().equals(other.getFemaleRealName()))
+            && (this.getProofImages() == null ? other.getProofImages() == null : this.getProofImages().equals(other.getProofImages()))
+            && (this.getCaseType() == null ? other.getCaseType() == null : this.getCaseType().equals(other.getCaseType()))
+            && (this.getCaseDate() == null ? other.getCaseDate() == null : this.getCaseDate().equals(other.getCaseDate()))
+            && (this.getAuditStatus() == null ? other.getAuditStatus() == null : this.getAuditStatus().equals(other.getAuditStatus()))
+            && (this.getAuditRemark() == null ? other.getAuditRemark() == null : this.getAuditRemark().equals(other.getAuditRemark()))
+            && (this.getAuditorId() == null ? other.getAuditorId() == null : this.getAuditorId().equals(other.getAuditorId()))
+            && (this.getPointsReward() == null ? other.getPointsReward() == null : this.getPointsReward().equals(other.getPointsReward()))
+            && (this.getCashReward() == null ? other.getCashReward() == null : this.getCashReward().equals(other.getCashReward()))
+            && (this.getRewardStatus() == null ? other.getRewardStatus() == null : this.getRewardStatus().equals(other.getRewardStatus()))
+            && (this.getIsPublished() == null ? other.getIsPublished() == null : this.getIsPublished().equals(other.getIsPublished()))
+            && (this.getPublishedCaseId() == null ? other.getPublishedCaseId() == null : this.getPublishedCaseId().equals(other.getPublishedCaseId()))
+            && (this.getIsRead() == null ? other.getIsRead() == null : this.getIsRead().equals(other.getIsRead()))
+            && (this.getCreatedAt() == null ? other.getCreatedAt() == null : this.getCreatedAt().equals(other.getCreatedAt()))
+            && (this.getUpdatedAt() == null ? other.getUpdatedAt() == null : this.getUpdatedAt().equals(other.getUpdatedAt()))
+            && (this.getAuditedAt() == null ? other.getAuditedAt() == null : this.getAuditedAt().equals(other.getAuditedAt()));
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((getId() == null) ? 0 : getId().hashCode());
+        result = prime * result + ((getMatchmakerId() == null) ? 0 : getMatchmakerId().hashCode());
+        result = prime * result + ((getMaleUserId() == null) ? 0 : getMaleUserId().hashCode());
+        result = prime * result + ((getFemaleUserId() == null) ? 0 : getFemaleUserId().hashCode());
+        result = prime * result + ((getMaleRealName() == null) ? 0 : getMaleRealName().hashCode());
+        result = prime * result + ((getFemaleRealName() == null) ? 0 : getFemaleRealName().hashCode());
+        result = prime * result + ((getProofImages() == null) ? 0 : getProofImages().hashCode());
+        result = prime * result + ((getCaseType() == null) ? 0 : getCaseType().hashCode());
+        result = prime * result + ((getCaseDate() == null) ? 0 : getCaseDate().hashCode());
+        result = prime * result + ((getAuditStatus() == null) ? 0 : getAuditStatus().hashCode());
+        result = prime * result + ((getAuditRemark() == null) ? 0 : getAuditRemark().hashCode());
+        result = prime * result + ((getAuditorId() == null) ? 0 : getAuditorId().hashCode());
+        result = prime * result + ((getPointsReward() == null) ? 0 : getPointsReward().hashCode());
+        result = prime * result + ((getCashReward() == null) ? 0 : getCashReward().hashCode());
+        result = prime * result + ((getRewardStatus() == null) ? 0 : getRewardStatus().hashCode());
+        result = prime * result + ((getIsPublished() == null) ? 0 : getIsPublished().hashCode());
+        result = prime * result + ((getPublishedCaseId() == null) ? 0 : getPublishedCaseId().hashCode());
+        result = prime * result + ((getIsRead() == null) ? 0 : getIsRead().hashCode());
+        result = prime * result + ((getCreatedAt() == null) ? 0 : getCreatedAt().hashCode());
+        result = prime * result + ((getUpdatedAt() == null) ? 0 : getUpdatedAt().hashCode());
+        result = prime * result + ((getAuditedAt() == null) ? 0 : getAuditedAt().hashCode());
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append(getClass().getSimpleName());
+        sb.append(" [");
+        sb.append("Hash = ").append(hashCode());
+        sb.append(", id=").append(id);
+        sb.append(", matchmakerId=").append(matchmakerId);
+        sb.append(", maleUserId=").append(maleUserId);
+        sb.append(", femaleUserId=").append(femaleUserId);
+        sb.append(", maleRealName=").append(maleRealName);
+        sb.append(", femaleRealName=").append(femaleRealName);
+        sb.append(", proofImages=").append(proofImages);
+        sb.append(", caseType=").append(caseType);
+        sb.append(", caseDate=").append(caseDate);
+        sb.append(", auditStatus=").append(auditStatus);
+        sb.append(", auditRemark=").append(auditRemark);
+        sb.append(", auditorId=").append(auditorId);
+        sb.append(", pointsReward=").append(pointsReward);
+        sb.append(", cashReward=").append(cashReward);
+        sb.append(", rewardStatus=").append(rewardStatus);
+        sb.append(", isPublished=").append(isPublished);
+        sb.append(", publishedCaseId=").append(publishedCaseId);
+        sb.append(", isRead=").append(isRead);
+        sb.append(", createdAt=").append(createdAt);
+        sb.append(", updatedAt=").append(updatedAt);
+        sb.append(", auditedAt=").append(auditedAt);
+        sb.append("]");
+        return sb.toString();
+    }
+}

+ 24 - 0
service/admin/src/main/java/com/zhentao/entity/Province.java

@@ -0,0 +1,24 @@
+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;
+
+/**
+ * 省份实体类
+ */
+@Data
+@TableName("province")
+public class Province implements Serializable {
+    
+    private static final long serialVersionUID = 1L;
+    
+    @TableId(type = IdType.AUTO)
+    private Integer id;
+    
+    private String name;
+}
+

+ 13 - 0
service/admin/src/main/java/com/zhentao/mapper/AreaMapper.java

@@ -0,0 +1,13 @@
+package com.zhentao.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zhentao.entity.Area;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 区域Mapper
+ */
+@Mapper
+public interface AreaMapper extends BaseMapper<Area> {
+}
+

+ 13 - 0
service/admin/src/main/java/com/zhentao/mapper/CityMapper.java

@@ -0,0 +1,13 @@
+package com.zhentao.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zhentao.entity.City;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 城市Mapper
+ */
+@Mapper
+public interface CityMapper extends BaseMapper<City> {
+}
+

+ 18 - 0
service/admin/src/main/java/com/zhentao/mapper/MatchmakerSuccessCaseUploadMapper.java

@@ -0,0 +1,18 @@
+package com.zhentao.mapper;
+
+import com.zhentao.entity.MatchmakerSuccessCaseUpload;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+
+/**
+* @author 联想
+* @description 针对表【matchmaker_success_case_upload(红娘上传成功案例表)】的数据库操作Mapper
+* @createDate 2025-12-12 10:29:21
+* @Entity com.zhentao.entity.MatchmakerSuccessCaseUpload
+*/
+public interface MatchmakerSuccessCaseUploadMapper extends BaseMapper<MatchmakerSuccessCaseUpload> {
+
+}
+
+
+
+

+ 13 - 0
service/admin/src/main/java/com/zhentao/mapper/ProvinceMapper.java

@@ -0,0 +1,13 @@
+package com.zhentao.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zhentao.entity.Province;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 省份Mapper
+ */
+@Mapper
+public interface ProvinceMapper extends BaseMapper<Province> {
+}
+

+ 5 - 107
service/admin/src/main/java/com/zhentao/service/AdminSecurityService.java

@@ -1,126 +1,24 @@
 package com.zhentao.service;
 
-import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
-import com.zhentao.entity.AdminPermission;
-import com.zhentao.entity.AdminRole;
-import com.zhentao.entity.AdminRolePermission;
-import com.zhentao.entity.AdminUserRole;
-import com.zhentao.mapper.AdminPermissionMapper;
-import com.zhentao.mapper.AdminRoleMapper;
-import com.zhentao.mapper.AdminRolePermissionMapper;
-import com.zhentao.mapper.AdminUserRoleMapper;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Service;
-
-import java.util.ArrayList;
-import java.util.HashSet;
 import java.util.List;
-import java.util.Set;
-import java.util.stream.Collectors;
 
 /**
- * 管理员权限服务
+ * 管理员权限服务接口
  */
-@Service
-public class AdminSecurityService {
-    
-    @Autowired
-    private AdminUserRoleMapper adminUserRoleMapper;
-    
-    @Autowired
-    private AdminRoleMapper adminRoleMapper;
-    
-    @Autowired
-    private AdminRolePermissionMapper adminRolePermissionMapper;
-    
-    @Autowired
-    private AdminPermissionMapper adminPermissionMapper;
+public interface AdminSecurityService {
     
     /**
      * 检查用户是否是超级管理员
      */
-    public boolean isSuperAdmin(Integer userId) {
-        QueryWrapper<AdminUserRole> wrapper = new QueryWrapper<>();
-        wrapper.eq("user_id", userId);
-        List<AdminUserRole> userRoles = adminUserRoleMapper.selectList(wrapper);
-        
-        for (AdminUserRole userRole : userRoles) {
-            AdminRole role = adminRoleMapper.selectById(userRole.getRoleId());
-            if (role != null && "超级管理员".equals(role.getRoleName())) {
-                return true;
-            }
-        }
-        return false;
-    }
+    boolean isSuperAdmin(Integer userId);
     
     /**
      * 获取用户的所有权限编码列表
      */
-    public List<String> getUserPermissions(Integer userId) {
-        List<String> permissions = new ArrayList<>();
-        
-        // 检查是否是超级管理员
-        if (isSuperAdmin(userId)) {
-            permissions.add("*");
-            return permissions;
-        }
-        
-        // 查询用户角色
-        QueryWrapper<AdminUserRole> userRoleWrapper = new QueryWrapper<>();
-        userRoleWrapper.eq("user_id", userId);
-        List<AdminUserRole> userRoles = adminUserRoleMapper.selectList(userRoleWrapper);
-        
-        if (userRoles.isEmpty()) {
-            return permissions;
-        }
-        
-        List<Integer> roleIds = userRoles.stream()
-                .map(AdminUserRole::getRoleId)
-                .collect(Collectors.toList());
-        
-        // 查询角色权限
-        QueryWrapper<AdminRolePermission> rolePermissionWrapper = new QueryWrapper<>();
-        rolePermissionWrapper.in("role_id", roleIds);
-        List<AdminRolePermission> rolePermissions = adminRolePermissionMapper.selectList(rolePermissionWrapper);
-        
-        Set<Integer> permissionIds = rolePermissions.stream()
-                .map(AdminRolePermission::getPermissionId)
-                .collect(Collectors.toSet());
-        
-        if (!permissionIds.isEmpty()) {
-            QueryWrapper<AdminPermission> permissionWrapper = new QueryWrapper<>();
-            permissionWrapper.in("id", permissionIds);
-            permissionWrapper.eq("status", 1); // 只查询启用的权限
-            List<AdminPermission> permissionList = adminPermissionMapper.selectList(permissionWrapper);
-            
-            for (AdminPermission permission : permissionList) {
-                if (permission.getPermissionCode() != null) {
-                    permissions.add(permission.getPermissionCode());
-                }
-            }
-        }
-        
-        return permissions;
-    }
+    List<String> getUserPermissions(Integer userId);
     
     /**
      * 获取用户的角色名称列表
      */
-    public List<String> getUserRoles(Integer userId) {
-        List<String> roles = new ArrayList<>();
-        
-        QueryWrapper<AdminUserRole> wrapper = new QueryWrapper<>();
-        wrapper.eq("user_id", userId);
-        List<AdminUserRole> userRoles = adminUserRoleMapper.selectList(wrapper);
-        
-        for (AdminUserRole userRole : userRoles) {
-            AdminRole role = adminRoleMapper.selectById(userRole.getRoleId());
-            if (role != null && role.getStatus() == 1) {
-                roles.add(role.getRoleName());
-            }
-        }
-        
-        return roles;
-    }
+    List<String> getUserRoles(Integer userId);
 }
-

+ 13 - 0
service/admin/src/main/java/com/zhentao/service/MatchmakerSuccessCaseUploadService.java

@@ -0,0 +1,13 @@
+package com.zhentao.service;
+
+import com.zhentao.entity.MatchmakerSuccessCaseUpload;
+import com.baomidou.mybatisplus.extension.service.IService;
+
+/**
+* @author 联想
+* @description 针对表【matchmaker_success_case_upload(红娘上传成功案例表)】的数据库操作Service
+* @createDate 2025-12-12 10:29:21
+*/
+public interface MatchmakerSuccessCaseUploadService extends IService<MatchmakerSuccessCaseUpload> {
+
+}

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

@@ -0,0 +1,84 @@
+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());
+    }
+}

+ 186 - 16
service/admin/src/main/java/com/zhentao/service/impl/MarrApplyServiceImpl.java

@@ -17,6 +17,13 @@ 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 com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.zhentao.entity.Province;
+import com.zhentao.entity.City;
+import com.zhentao.entity.Area;
+import com.zhentao.mapper.ProvinceMapper;
+import com.zhentao.mapper.CityMapper;
+import com.zhentao.mapper.AreaMapper;
 
 import java.time.LocalDateTime;
 import java.util.Date;
@@ -38,6 +45,15 @@ public class MarrApplyServiceImpl extends ServiceImpl<MarrApplyMapper, MarrApply
     @Autowired
     private MatchmakerMapper matchmakerMapper;
     
+    @Autowired
+    private ProvinceMapper provinceMapper;
+    
+    @Autowired
+    private CityMapper cityMapper;
+    
+    @Autowired
+    private AreaMapper areaMapper;
+    
     private final RestTemplate restTemplate = new RestTemplate();
     
     // websocket 服务的 IM 接口地址
@@ -81,18 +97,46 @@ public class MarrApplyServiceImpl extends ServiceImpl<MarrApplyMapper, MarrApply
         }
         System.out.println("✅ 已更新 users 表,is_matchmaker = 1");
         
-        // 4. 创建 matchmakers 记录
-        Matchmaker matchmaker = new Matchmaker();
+        // 4. 创建或更新 matchmakers 记录
+        // 先检查该用户是否已经存在红娘记录
+        QueryWrapper<Matchmaker> matchmakerWrapper = new QueryWrapper<>();
+        matchmakerWrapper.eq("user_id", apply.getUserId());
+        Matchmaker existingMatchmaker = matchmakerMapper.selectOne(matchmakerWrapper);
         
-        // 从 marr_apply 表获取数据
-        matchmaker.setRealName(apply.getName());           // 真实姓名
+        Matchmaker matchmaker;
+        boolean isUpdate = false;
+        
+        if (existingMatchmaker != null) {
+            // 如果已存在,则更新现有记录
+            matchmaker = existingMatchmaker;
+            isUpdate = true;
+            System.out.println("⚠️ 该用户已存在红娘记录,将更新现有记录,matchmaker_id = " + matchmaker.getMatchmakerId());
+        } else {
+            // 如果不存在,创建新记录
+            matchmaker = new Matchmaker();
+            matchmaker.setCreateTime(LocalDateTime.now());
+        }
+        
+        // 从 marr_apply 表获取数据(按照用户要求映射)
+        matchmaker.setUserId(apply.getUserId());           // 用户ID
+        matchmaker.setRealName(apply.getName());           // 姓名 -> 真实姓名
         matchmaker.setPhone(apply.getPhone());             // 手机号
         matchmaker.setEmail(apply.getEmail());             // 邮箱
         matchmaker.setGender(apply.getGender());           // 性别
-        matchmaker.setProfile(apply.getIntroduction());    // 个人简介
+        matchmaker.setProfile(apply.getExperience());      // 经验 -> 个人简介
+        
+        // 解析地区信息:area 字段包含三级联动,需要解析为 provinceId, cityId, areaId
+        parseAreaToIds(apply.getArea(), matchmaker);
         
         // 从 users 表获取数据
-        matchmaker.setUsername(user.getNickname() != null ? user.getNickname() : user.getPhone()); // 用户名(优先使用昵称,否则使用手机号)
+        String baseUsername = user.getNickname() != null ? user.getNickname() : user.getPhone(); // 用户名(优先使用昵称,否则使用手机号)
+        
+        // 检查用户名是否已存在(如果不存在记录或用户名发生变化)
+        if (!isUpdate || matchmaker.getUsername() == null || !baseUsername.equals(matchmaker.getUsername())) {
+            String finalUsername = generateUniqueUsername(baseUsername, matchmaker.getUserId());
+            matchmaker.setUsername(finalUsername);
+        }
+        
         matchmaker.setPassword(user.getPassword());        // 密码(使用用户的密码)
         matchmaker.setBirthDate(user.getBirthDate());      // 出生日期(更准确)
         
@@ -106,18 +150,28 @@ public class MarrApplyServiceImpl extends ServiceImpl<MarrApplyMapper, MarrApply
         }
         
         // 设置默认值(无法从其他表获取的字段)
-        matchmaker.setMatchmakerType(1);                   // 默认类型:1-普通红娘
-        matchmaker.setLevel(1);                            // 默认等级:1级
-        matchmaker.setSuccessCouples(0);                   // 初始成功撮合数:0
+        if (!isUpdate) {
+            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("创建红娘记录失败");
+        int result;
+        if (isUpdate) {
+            result = matchmakerMapper.updateById(matchmaker);
+            if (result <= 0) {
+                throw new RuntimeException("更新红娘记录失败");
+            }
+            System.out.println("✅ 已更新 matchmakers 记录,matchmaker_id = " + matchmaker.getMatchmakerId());
+        } else {
+            result = matchmakerMapper.insert(matchmaker);
+            if (result <= 0) {
+                throw new RuntimeException("创建红娘记录失败");
+            }
+            System.out.println("✅ 已创建 matchmakers 记录,matchmaker_id = " + matchmaker.getMatchmakerId());
         }
-        System.out.println("✅ 已创建 matchmakers 记录,matchmaker_id = " + matchmaker.getMatchmakerId());
         
         // 5. 导入到腾讯云 IM(使用 m_ + matchmaker_id)
         String imUserId = "m_" + matchmaker.getMatchmakerId();
@@ -141,7 +195,8 @@ public class MarrApplyServiceImpl extends ServiceImpl<MarrApplyMapper, MarrApply
             // 导入失败不影响审核流程,只记录日志
         }
         
-        // 6. 更新申请记录的更新人和更新时间
+        // 6. 更新申请记录的审核状态、更新人和更新时间
+        apply.setStatus(0); // 0-正常(审核通过)
         apply.setUpdateTime(new Date());
         apply.setUpdateMan("系统");
         this.updateById(apply);
@@ -153,7 +208,122 @@ public class MarrApplyServiceImpl extends ServiceImpl<MarrApplyMapper, MarrApply
     @Override
     @Transactional(rollbackFor = Exception.class)
     public boolean delete(Long applyId) {
-        return this.removeById(applyId);
+        // 逻辑删除:将 idStatus 设置为 "1"(禁止状态)
+        MarrApply marrApply = new MarrApply();
+        marrApply.setApplyId(applyId);
+        marrApply.setIdStatus("1"); // 1-禁止(逻辑删除)
+        return this.updateById(marrApply);
+    }
+    
+    /**
+     * 解析地区字符串,将省市区名称转换为对应的ID
+     * area 格式可能是:"北京市 北京市 朝阳区" 或 "北京市 北京市 朝阳区"(用空格分隔)
+     * 
+     * @param area 地区字符串
+     * @param matchmaker 红娘对象,用于设置 provinceId, cityId, areaId
+     */
+    private void parseAreaToIds(String area, Matchmaker matchmaker) {
+        if (area == null || area.trim().isEmpty()) {
+            System.out.println("⚠️ 地区信息为空,跳过解析");
+            return;
+        }
+        
+        try {
+            // 按空格分割地区字符串
+            String[] areaParts = area.trim().split("\\s+");
+            
+            if (areaParts.length >= 1 && provinceMapper != null) {
+                // 解析省份
+                String provinceName = areaParts[0].trim();
+                QueryWrapper<Province> provinceWrapper = new QueryWrapper<>();
+                provinceWrapper.eq("name", provinceName);
+                Province province = provinceMapper.selectOne(provinceWrapper);
+                if (province != null) {
+                    matchmaker.setProvinceId(province.getId());
+                    System.out.println("✅ 解析省份: " + provinceName + " -> " + province.getId());
+                } else {
+                    System.out.println("⚠️ 未找到省份: " + provinceName);
+                }
+            }
+            
+            if (areaParts.length >= 2 && cityMapper != null && matchmaker.getProvinceId() != null) {
+                // 解析城市
+                String cityName = areaParts[1].trim();
+                QueryWrapper<City> cityWrapper = new QueryWrapper<>();
+                cityWrapper.eq("name", cityName);
+                cityWrapper.eq("province_id", matchmaker.getProvinceId());
+                City city = cityMapper.selectOne(cityWrapper);
+                if (city != null) {
+                    matchmaker.setCityId(city.getId());
+                    System.out.println("✅ 解析城市: " + cityName + " -> " + city.getId());
+                } else {
+                    System.out.println("⚠️ 未找到城市: " + cityName);
+                }
+            }
+            
+            if (areaParts.length >= 3 && areaMapper != null && matchmaker.getCityId() != null) {
+                // 解析区域
+                String areaName = areaParts[2].trim();
+                QueryWrapper<Area> areaWrapper = new QueryWrapper<>();
+                areaWrapper.eq("name", areaName);
+                areaWrapper.eq("city_id", matchmaker.getCityId());
+                Area areaObj = areaMapper.selectOne(areaWrapper);
+                if (areaObj != null) {
+                    matchmaker.setAreaId(areaObj.getId());
+                    System.out.println("✅ 解析区域: " + areaName + " -> " + areaObj.getId());
+                } else {
+                    System.out.println("⚠️ 未找到区域: " + areaName);
+                }
+            }
+            
+            if (areaParts.length < 3) {
+                System.out.println("⚠️ 地区信息不完整,只有 " + areaParts.length + " 部分: " + area);
+            }
+        } catch (Exception e) {
+            System.err.println("❌ 解析地区信息失败: " + e.getMessage());
+            e.printStackTrace();
+            // 解析失败不影响审核流程,只记录日志
+        }
+    }
+    
+    /**
+     * 生成唯一的用户名
+     * 如果用户名已存在,则添加后缀(_1, _2, ...)直到找到不存在的用户名
+     * 
+     * @param baseUsername 基础用户名
+     * @param currentUserId 当前用户ID(如果更新现有记录,排除自己)
+     * @return 唯一的用户名
+     */
+    private String generateUniqueUsername(String baseUsername, Integer currentUserId) {
+        if (baseUsername == null || baseUsername.trim().isEmpty()) {
+            baseUsername = "user_" + System.currentTimeMillis();
+        }
+        
+        String username = baseUsername.trim();
+        int suffix = 0;
+        
+        while (true) {
+            QueryWrapper<Matchmaker> wrapper = new QueryWrapper<>();
+            wrapper.eq("username", username);
+            // 如果当前用户ID不为空,排除自己的记录(用于更新场景)
+            if (currentUserId != null) {
+                wrapper.ne("user_id", currentUserId);
+            }
+            
+            Matchmaker existing = matchmakerMapper.selectOne(wrapper);
+            
+            if (existing == null) {
+                // 用户名不存在,可以使用
+                if (suffix > 0) {
+                    System.out.println("⚠️ 用户名 '" + baseUsername + "' 已存在,使用新用户名: " + username);
+                }
+                return username;
+            }
+            
+            // 用户名已存在,添加后缀
+            suffix++;
+            username = baseUsername + "_" + suffix;
+        }
     }
 }
 

+ 22 - 0
service/admin/src/main/java/com/zhentao/service/impl/MatchmakerSuccessCaseUploadServiceImpl.java

@@ -0,0 +1,22 @@
+package com.zhentao.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.zhentao.entity.MatchmakerSuccessCaseUpload;
+import com.zhentao.service.MatchmakerSuccessCaseUploadService;
+import com.zhentao.mapper.MatchmakerSuccessCaseUploadMapper;
+import org.springframework.stereotype.Service;
+
+/**
+* @author 联想
+* @description 针对表【matchmaker_success_case_upload(红娘上传成功案例表)】的数据库操作Service实现
+* @createDate 2025-12-12 10:29:21
+*/
+@Service
+public class MatchmakerSuccessCaseUploadServiceImpl extends ServiceImpl<MatchmakerSuccessCaseUploadMapper, MatchmakerSuccessCaseUpload>
+    implements MatchmakerSuccessCaseUploadService{
+
+}
+
+
+
+

+ 122 - 0
service/admin/src/main/java/com/zhentao/service/impl/RolePermissionBasedAdminSecurityServiceImpl.java

@@ -0,0 +1,122 @@
+package com.zhentao.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.zhentao.entity.AdminPermission;
+import com.zhentao.entity.AdminRole;
+import com.zhentao.entity.AdminRolePermission;
+import com.zhentao.entity.AdminUserRole;
+import com.zhentao.mapper.AdminPermissionMapper;
+import com.zhentao.mapper.AdminRoleMapper;
+import com.zhentao.mapper.AdminRolePermissionMapper;
+import com.zhentao.mapper.AdminUserRoleMapper;
+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.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * 基于角色权限表的管理员权限服务实现(默认实现)
+ */
+@Service
+@Primary
+public class RolePermissionBasedAdminSecurityServiceImpl implements AdminSecurityService {
+    
+    @Autowired
+    private AdminUserRoleMapper adminUserRoleMapper;
+    
+    @Autowired
+    private AdminRoleMapper adminRoleMapper;
+    
+    @Autowired
+    private AdminRolePermissionMapper adminRolePermissionMapper;
+    
+    @Autowired
+    private AdminPermissionMapper adminPermissionMapper;
+    
+    @Override
+    public boolean isSuperAdmin(Integer userId) {
+        QueryWrapper<AdminUserRole> wrapper = new QueryWrapper<>();
+        wrapper.eq("user_id", userId);
+        List<AdminUserRole> userRoles = adminUserRoleMapper.selectList(wrapper);
+        
+        for (AdminUserRole userRole : userRoles) {
+            AdminRole role = adminRoleMapper.selectById(userRole.getRoleId());
+            if (role != null && ("SUPER_ADMIN".equals(role.getRoleCode()) || "超级管理员".equals(role.getRoleName()))) {
+                return true;
+            }
+        }
+        return false;
+    }
+    
+    @Override
+    public List<String> getUserPermissions(Integer userId) {
+        List<String> permissions = new ArrayList<>();
+        
+        // 检查是否是超级管理员
+        if (isSuperAdmin(userId)) {
+            permissions.add("*");
+            return permissions;
+        }
+        
+        // 查询用户角色
+        QueryWrapper<AdminUserRole> userRoleWrapper = new QueryWrapper<>();
+        userRoleWrapper.eq("user_id", userId);
+        List<AdminUserRole> userRoles = adminUserRoleMapper.selectList(userRoleWrapper);
+        
+        if (userRoles.isEmpty()) {
+            return permissions;
+        }
+        
+        List<Integer> roleIds = userRoles.stream()
+                .map(AdminUserRole::getRoleId)
+                .collect(Collectors.toList());
+        
+        // 查询角色权限
+        QueryWrapper<AdminRolePermission> rolePermissionWrapper = new QueryWrapper<>();
+        rolePermissionWrapper.in("role_id", roleIds);
+        List<AdminRolePermission> rolePermissions = adminRolePermissionMapper.selectList(rolePermissionWrapper);
+        
+        Set<Integer> permissionIds = rolePermissions.stream()
+                .map(AdminRolePermission::getPermissionId)
+                .collect(Collectors.toSet());
+        
+        if (!permissionIds.isEmpty()) {
+            QueryWrapper<AdminPermission> permissionWrapper = new QueryWrapper<>();
+            permissionWrapper.in("id", permissionIds);
+            permissionWrapper.eq("status", 1); // 只查询启用的权限
+            List<AdminPermission> permissionList = adminPermissionMapper.selectList(permissionWrapper);
+            
+            for (AdminPermission permission : permissionList) {
+                if (permission.getPermissionCode() != null) {
+                    permissions.add(permission.getPermissionCode());
+                }
+            }
+        }
+        
+        return permissions;
+    }
+    
+    @Override
+    public List<String> getUserRoles(Integer userId) {
+        List<String> roles = new ArrayList<>();
+        
+        QueryWrapper<AdminUserRole> wrapper = new QueryWrapper<>();
+        wrapper.eq("user_id", userId);
+        List<AdminUserRole> userRoles = adminUserRoleMapper.selectList(wrapper);
+        
+        for (AdminUserRole userRole : userRoles) {
+            AdminRole role = adminRoleMapper.selectById(userRole.getRoleId());
+            if (role != null && role.getStatus() == 1) {
+                roles.add(role.getRoleName());
+            }
+        }
+        
+        return roles;
+    }
+}
+

+ 132 - 0
service/admin/src/main/java/com/zhentao/vo/MatchmakerSuccessCaseUploadVO.java

@@ -0,0 +1,132 @@
+package com.zhentao.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 红娘成功案例上传VO
+ */
+@Data
+public class MatchmakerSuccessCaseUploadVO implements Serializable {
+    
+    private static final long serialVersionUID = 1L;
+    
+    /**
+     * 主键ID
+     */
+    private Long id;
+
+    /**
+     * 撮合红娘ID
+     */
+    private Integer matchmakerId;
+
+    /**
+     * 男方用户ID
+     */
+    private Integer maleUserId;
+
+    /**
+     * 女方用户ID
+     */
+    private Integer femaleUserId;
+
+    /**
+     * 男方真实姓名
+     */
+    private String maleRealName;
+
+    /**
+     * 女方真实姓名
+     */
+    private String femaleRealName;
+
+    /**
+     * 成功凭证图片路径(JSON数组格式)
+     */
+    private String proofImages;
+
+    /**
+     * 案例类型: 1-订婚 2-领证结婚
+     */
+    private Integer caseType;
+
+    /**
+     * 成功日期(结婚日期/确定关系日期)
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private Date caseDate;
+
+    /**
+     * 审核状态:0-待审核 1-审核通过 2-审核失败 3-核实中
+     */
+    private Integer auditStatus;
+
+    /**
+     * 审核备注(失败原因)
+     */
+    private String auditRemark;
+
+    /**
+     * 审核人ID(管理员)
+     */
+    private Integer auditorId;
+
+    /**
+     * 积分奖励
+     */
+    private Integer pointsReward;
+
+    /**
+     * 现金奖励
+     */
+    private BigDecimal cashReward;
+
+    /**
+     * 奖励状态:0-未发放 1-已发放
+     */
+    private Integer rewardStatus;
+
+    /**
+     * 是否发布到成功案例展示:0-否 1-是
+     */
+    private Integer isPublished;
+
+    /**
+     * 关联的成功案例ID
+     */
+    private Integer publishedCaseId;
+
+    /**
+     * 0未读         1已读
+     */
+    private Integer isRead;
+
+    /**
+     * 上传时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdAt;
+
+    /**
+     * 更新时间(更改时间)
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updatedAt;
+
+    /**
+     * 审核时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date auditedAt;
+    
+    /**
+     * 审核耗时(审核时间 - 上传时间,格式化的可读字符串)
+     */
+    private String auditDuration;
+}
+

+ 1 - 0
service/admin/src/main/java/com/zhentao/vo/UserVO.java

@@ -47,6 +47,7 @@ public class UserVO implements Serializable {
     
     // 计算字段
     private Integer age;
+    private String auditedAt;  // 审核时间(updatedAt - createdAt,格式化的时间差)
     
     // VIP信息
     private Boolean isVip;  // 是否VIP

+ 3 - 1
service/admin/src/main/resources/com/zhentao/mapper/MarrApplyMapper.xml

@@ -21,12 +21,13 @@
             <result property="updateTime" column="update_time" />
             <result property="updateMan" column="update_man" />
             <result property="status" column="status" />
+            <result property="idStatus" column="id_status" />
     </resultMap>
 
     <sql id="Base_Column_List">
         apply_id,user_id,name,phone,email,age,
         gender,area,experience,server_time,introduction,
-        create_time,create_man,update_time,update_man,status
+        create_time,create_man,update_time,update_man,status,id_status
     </sql>
 
     <select id="selectPageByCondition" resultMap="BaseResultMap">
@@ -34,6 +35,7 @@
         <include refid="Base_Column_List"/>
         FROM marr_apply
         <where>
+            AND (id_status = '0' OR id_status IS NULL)
             <if test="name != null and name != ''">
                 AND name LIKE CONCAT('%', #{name}, '%')
             </if>

+ 37 - 0
service/admin/src/main/resources/com/zhentao/mapper/MatchmakerSuccessCaseUploadMapper.xml

@@ -0,0 +1,37 @@
+<?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.MatchmakerSuccessCaseUploadMapper">
+
+    <resultMap id="BaseResultMap" type="com.zhentao.entity.MatchmakerSuccessCaseUpload">
+            <id property="id" column="id" />
+            <result property="matchmakerId" column="matchmaker_id" />
+            <result property="maleUserId" column="male_user_id" />
+            <result property="femaleUserId" column="female_user_id" />
+            <result property="maleRealName" column="male_real_name" />
+            <result property="femaleRealName" column="female_real_name" />
+            <result property="proofImages" column="proof_images" />
+            <result property="caseType" column="case_type" />
+            <result property="caseDate" column="case_date" />
+            <result property="auditStatus" column="audit_status" />
+            <result property="auditRemark" column="audit_remark" />
+            <result property="auditorId" column="auditor_id" />
+            <result property="pointsReward" column="points_reward" />
+            <result property="cashReward" column="cash_reward" />
+            <result property="rewardStatus" column="reward_status" />
+            <result property="isPublished" column="is_published" />
+            <result property="publishedCaseId" column="published_case_id" />
+            <result property="isRead" column="is_read" />
+            <result property="createdAt" column="created_at" />
+            <result property="updatedAt" column="updated_at" />
+            <result property="auditedAt" column="audited_at" />
+    </resultMap>
+
+    <sql id="Base_Column_List">
+        id,matchmaker_id,male_user_id,female_user_id,male_real_name,female_real_name,
+        proof_images,case_type,case_date,audit_status,audit_remark,
+        auditor_id,points_reward,cash_reward,reward_status,is_published,
+        published_case_id,is_read,created_at,updated_at,audited_at
+    </sql>
+</mapper>

+ 35 - 0
service/admin/src/main/resources/com/zhentao/mapper/UsersMapper.xml

@@ -0,0 +1,35 @@
+<?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.UsersMapper">
+
+    <resultMap id="BaseResultMap" type="com.zhentao.entity.Users">
+            <id property="userId" column="user_id" />
+            <result property="phone" column="phone" />
+            <result property="email" column="email" />
+            <result property="nickname" column="nickname" />
+            <result property="password" column="password" />
+            <result property="gender" column="gender" />
+            <result property="birthDate" column="birth_date" />
+            <result property="avatarUrl" column="avatar_url" />
+            <result property="status" column="status" />
+            <result property="sourceChannel" column="source_channel" />
+            <result property="isProfileComplete" column="is_profile_complete" />
+            <result property="createdAt" column="created_at" />
+            <result property="updatedAt" column="updated_at" />
+            <result property="consentToCollect" column="consent_to_collect" />
+            <result property="lastLoginAt" column="last_login_at" />
+            <result property="lastActiveAt" column="last_active_at" />
+            <result property="hasWechatLogin" column="has_wechat_login" />
+            <result property="isMatchmaker" column="is_matchmaker" />
+    </resultMap>
+
+    <sql id="Base_Column_List">
+        user_id,phone,email,nickname,password,gender,
+        birth_date,avatar_url,status,source_channel,is_profile_complete,
+        created_at,updated_at,consent_to_collect,last_login_at,last_active_at,
+        has_wechat_login,is_matchmaker
+    </sql>
+</mapper>
+

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

@@ -62,12 +62,12 @@ public class ActivityController {
      */
     @GetMapping("/list")
     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) {
+            @RequestParam(value = "type", required = false) String type,
+            @RequestParam(value = "status", required = false) Integer status,
+            @RequestParam(value = "keyword", required = false) String keyword,
+            @RequestParam(value = "page", required = false) Integer page,
+            @RequestParam(value = "pageSize", required = false) Integer pageSize,
+            @RequestParam(value = "limit", required = false) Integer limit) {
         try {
             // 如果提供了分页参数,使用分页查询
             if (page != null && pageSize != null && page > 0 && pageSize > 0) {

+ 252 - 27
service/homePage/src/main/java/com/zhentao/controller/MyResourceController.java

@@ -35,7 +35,7 @@ public class MyResourceController {
     
     @Autowired
     private MatchmakerService matchmakerService;
-    
+
     /**
      * 添加资源信息
      * 
@@ -116,11 +116,11 @@ public class MyResourceController {
             // 检查手机号是否已在资源表中存在(检查整个my_resource表,不限制红娘)
             String phone = myResource.getPhone().trim();
             String backupPhone = myResource.getBackupPhone() != null ? myResource.getBackupPhone().trim() : null;
-            
+
             System.out.println("=== 开始检查手机号重复 ===");
             System.out.println("主手机号: " + phone);
             System.out.println("备用手机号: " + backupPhone);
-            
+
             // 检查主手机号是否已存在(使用count更可靠)
             QueryWrapper<MyResource> phoneQueryWrapper = new QueryWrapper<>();
             phoneQueryWrapper.eq("phone", phone);
@@ -131,7 +131,7 @@ public class MyResourceController {
                 System.out.println("找到重复的主手机号,资源名称: " + (existingResourceByPhone != null ? existingResourceByPhone.getName() : "未知"));
                 return Result.error("手机号 " + phone + " 已被资源\"" + (existingResourceByPhone != null && existingResourceByPhone.getName() != null ? existingResourceByPhone.getName() : "未知") + "\"使用,不能重复添加");
             }
-            
+
             // 检查主手机号是否作为其他资源的备用手机号存在
             QueryWrapper<MyResource> phoneAsBackupQueryWrapper = new QueryWrapper<>();
             phoneAsBackupQueryWrapper.eq("backup_phone", phone);
@@ -142,7 +142,7 @@ public class MyResourceController {
                 System.out.println("找到重复的主手机号(作为备用手机号),资源名称: " + (existingResourceByPhoneAsBackup != null ? existingResourceByPhoneAsBackup.getName() : "未知"));
                 return Result.error("手机号 " + phone + " 已被资源\"" + (existingResourceByPhoneAsBackup != null && existingResourceByPhoneAsBackup.getName() != null ? existingResourceByPhoneAsBackup.getName() : "未知") + "\"作为备用手机号使用,不能重复添加");
             }
-            
+
             // 检查备用手机号是否已存在(如果提供了备用手机号)
             if (backupPhone != null && !backupPhone.isEmpty()) {
                 // 检查备用手机号是否作为主手机号存在
@@ -155,7 +155,7 @@ public class MyResourceController {
                     System.out.println("找到重复的备用手机号(作为主手机号),资源名称: " + (existingResourceByBackupAsMain != null ? existingResourceByBackupAsMain.getName() : "未知"));
                     return Result.error("备用手机号 " + backupPhone + " 已被资源\"" + (existingResourceByBackupAsMain != null && existingResourceByBackupAsMain.getName() != null ? existingResourceByBackupAsMain.getName() : "未知") + "\"作为主手机号使用,不能重复添加");
                 }
-                
+
                 // 检查备用手机号是否作为备用手机号存在
                 QueryWrapper<MyResource> backupPhoneQueryWrapper = new QueryWrapper<>();
                 backupPhoneQueryWrapper.eq("backup_phone", backupPhone);
@@ -167,9 +167,9 @@ public class MyResourceController {
                     return Result.error("备用手机号 " + backupPhone + " 已被资源\"" + (existingResourceByBackup != null && existingResourceByBackup.getName() != null ? existingResourceByBackup.getName() : "未知") + "\"使用,不能重复添加");
                 }
             }
-            
+
             System.out.println("=== 手机号检查通过,没有重复 ===");
-            
+
             // 根据手机号查询用户表中是否存在该用户
             QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
             userQueryWrapper.eq("phone", myResource.getPhone());
@@ -209,7 +209,7 @@ public class MyResourceController {
                     QueryWrapper<Matchmaker> matchmakerQueryWrapper = new QueryWrapper<>();
                     matchmakerQueryWrapper.eq("user_id", currentUserId);
                     Matchmaker matchmaker = matchmakerService.getOne(matchmakerQueryWrapper);
-                    
+
                     if (matchmaker != null && matchmaker.getMatchmakerId() != null) {
                         // 查询到红娘信息,设置matchmaker_id
                         myResource.setMatchmakerId(matchmaker.getMatchmakerId());
@@ -228,11 +228,11 @@ public class MyResourceController {
                 myResource.setStatus(0);
                 System.out.println("设置默认审核状态: 0 (待审核)");
             }
-            
+
             // 判断资源类型(根据isUser字段:1是资源,0是线索)
             String resourceType = (myResource.getIsUser() != null && myResource.getIsUser() == 1) ? "资源" : "线索";
             System.out.println("资源类型: " + resourceType + " (isUser=" + myResource.getIsUser() + ")");
-            
+
             // 保存资源信息和标签关联
             boolean success = myResourceService.addResource(myResource, tagIds);
             if (success) {
@@ -241,12 +241,12 @@ public class MyResourceController {
                 resultData.put("resource", myResource);
                 resultData.put("resourceType", resourceType);
                 resultData.put("isUser", myResource.getIsUser());
-                
+
                 String successMessage = "资源信息添加成功";
                 if (resourceType.equals("线索")) {
                     successMessage = "线索信息添加成功";
                 }
-                
+
                 return Result.success(successMessage, resultData);
             } else {
                 return Result.error("资源信息添加失败");
@@ -750,10 +750,10 @@ public class MyResourceController {
             return Result.error("查询标签ID列表失败:" + e.getMessage());
         }
     }
-    
+
     /**
      * 检查手机号是否已存在(检查整个my_resource表)
-     * 
+     *
      * @param phone 主手机号
      * @param backupPhone 备用手机号(可选)
      * @return 检查结果,如果存在则返回资源信息
@@ -769,14 +769,14 @@ public class MyResourceController {
             result.put("phoneAsBackupExists", false);
             result.put("backupPhoneAsMainExists", false);
             result.put("existingResource", null);
-            
+
             String phoneTrimmed = phone != null ? phone.trim() : "";
             String backupPhoneTrimmed = backupPhone != null ? backupPhone.trim() : null;
-            
+
             System.out.println("=== check-phone 接口调用 ===");
             System.out.println("主手机号: " + phoneTrimmed);
             System.out.println("备用手机号: " + backupPhoneTrimmed);
-            
+
             // 检查主手机号是否已存在(使用count更可靠)
             QueryWrapper<MyResource> phoneQueryWrapper = new QueryWrapper<>();
             phoneQueryWrapper.eq("phone", phoneTrimmed);
@@ -789,7 +789,7 @@ public class MyResourceController {
                 System.out.println("找到重复的主手机号,资源名称: " + (existingResourceByPhone != null ? existingResourceByPhone.getName() : "未知"));
                 return Result.success(result);
             }
-            
+
             // 检查主手机号是否作为备用手机号存在
             QueryWrapper<MyResource> phoneAsBackupQueryWrapper = new QueryWrapper<>();
             phoneAsBackupQueryWrapper.eq("backup_phone", phoneTrimmed);
@@ -802,7 +802,7 @@ public class MyResourceController {
                 System.out.println("找到重复的主手机号(作为备用手机号),资源名称: " + (existingResourceByPhoneAsBackup != null ? existingResourceByPhoneAsBackup.getName() : "未知"));
                 return Result.success(result);
             }
-            
+
             // 如果提供了备用手机号,检查备用手机号
             if (backupPhoneTrimmed != null && !backupPhoneTrimmed.isEmpty()) {
                 // 检查备用手机号是否作为主手机号存在
@@ -817,7 +817,7 @@ public class MyResourceController {
                     System.out.println("找到重复的备用手机号(作为主手机号),资源名称: " + (existingResourceByBackupAsMain != null ? existingResourceByBackupAsMain.getName() : "未知"));
                     return Result.success(result);
                 }
-                
+
                 // 检查备用手机号是否作为备用手机号存在
                 QueryWrapper<MyResource> backupPhoneQueryWrapper = new QueryWrapper<>();
                 backupPhoneQueryWrapper.eq("backup_phone", backupPhoneTrimmed);
@@ -831,7 +831,7 @@ public class MyResourceController {
                     return Result.success(result);
                 }
             }
-            
+
             System.out.println("=== check-phone 检查通过,没有重复 ===");
             return Result.success(result);
         } catch (Exception e) {
@@ -839,12 +839,12 @@ public class MyResourceController {
             return Result.error("检查手机号失败:" + e.getMessage());
         }
     }
-    
+
     /**
      * 批量检查并更新线索用户的注册状态
      * 定时刷新线索列表中的所有线索用户,根据线索用户的手机号在users表中检索
      * 如果users表中存在该用户,则更新该线索用户的is_user字段为1和user_id字段
-     * 
+     *
      * @param matchmakerId 红娘ID(可选,如果提供则只检查该红娘的线索)
      * @return 更新结果,包含更新的资源数量
      */
@@ -856,18 +856,243 @@ public class MyResourceController {
             if (matchmakerId != null) {
                 System.out.println("红娘ID: " + matchmakerId);
             }
-            
+
             int updateCount = myResourceService.batchCheckAndUpdateClueRegistrationStatus(matchmakerId);
-            
+
             java.util.Map<String, Object> result = new java.util.HashMap<>();
             result.put("updateCount", updateCount);
             result.put("message", "批量检查完成,共更新 " + updateCount + " 条线索用户记录");
-            
+
             return Result.success("批量检查完成", result);
         } catch (Exception e) {
             e.printStackTrace();
             return Result.error("批量检查线索用户注册状态失败:" + e.getMessage());
         }
     }
+
+    /**
+     * 搜索资源(按姓名或手机号)
+     * 用于撮合成功审核页面的下拉搜索
+     *
+     * @param matchmakerId 红娘ID
+     * @param keyword 搜索关键词(姓名或手机号)
+     * @param gender 性别筛选(1-男,2-女,可选)
+     * @return 匹配的资源列表
+     */
+    @GetMapping("/search")
+    public Result<java.util.List<MyResourceVO>> searchResources(
+            @RequestParam Integer matchmakerId,
+            @RequestParam(required = false) String keyword,
+            @RequestParam(required = false) Integer gender) {
+        try {
+            System.out.println("=== 搜索资源 ===");
+            System.out.println("matchmakerId: " + matchmakerId);
+            System.out.println("keyword: " + keyword);
+            System.out.println("gender: " + gender);
+
+            java.util.List<MyResourceVO> resources = myResourceService.searchResources(matchmakerId, keyword, gender);
+
+            System.out.println("搜索结果数量: " + resources.size());
+            return Result.success(resources);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("搜索资源失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 获取资源下拉列表(用于撮合成功审核页面)
+     * 返回简化的资源信息:resourceId, name, avatarUrl, gender, phone
+     *
+     * @param matchmakerId 红娘ID
+     * @param gender 性别筛选(1-男,2-女)
+     * @return 资源下拉列表
+     */
+    @GetMapping("/dropdown")
+    public Result<java.util.List<java.util.Map<String, Object>>> getResourceDropdown(
+            @RequestParam Integer matchmakerId,
+            @RequestParam(required = false) Integer gender) {
+        try {
+            System.out.println("=== 获取资源下拉列表 ===");
+            System.out.println("matchmakerId: " + matchmakerId);
+            System.out.println("gender: " + gender);
+
+            java.util.List<MyResourceVO> resources = myResourceService.searchResources(matchmakerId, null, gender);
+
+            // 转换为简化的下拉列表格式
+            java.util.List<java.util.Map<String, Object>> dropdown = new java.util.ArrayList<>();
+            for (MyResourceVO resource : resources) {
+                java.util.Map<String, Object> item = new java.util.HashMap<>();
+                item.put("resourceId", resource.getResourceId());
+                item.put("name", resource.getName());
+                item.put("avatarUrl", resource.getAvatarUrl());
+                item.put("gender", resource.getGender());
+                item.put("phone", resource.getPhone());
+                item.put("age", resource.getAge());
+                dropdown.add(item);
+            }
+
+            System.out.println("下拉列表数量: " + dropdown.size());
+            return Result.success(dropdown);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("获取资源下拉列表失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 获取已注册用户的资源下拉列表(用于撮合成功审核页面)
+     * 只返回user_id不为空的资源(即已注册为平台用户的资源)
+     *
+     * @param matchmakerId 红娘ID
+     * @param gender 性别筛选(1-男,2-女)
+     * @return 已注册用户的资源下拉列表
+     */
+    @GetMapping("/registered-dropdown")
+    public Result<java.util.List<java.util.Map<String, Object>>> getRegisteredResourceDropdown(
+            @RequestParam Integer matchmakerId,
+            @RequestParam(required = false) Integer gender) {
+        try {
+            System.out.println("=== 获取已注册用户资源下拉列表 ===");
+            System.out.println("matchmakerId: " + matchmakerId);
+            System.out.println("gender: " + gender);
+
+            // 查询资源列表
+            com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<MyResource> queryWrapper =
+                new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<>();
+            queryWrapper.eq("matchmaker_id", matchmakerId);
+            // 只查询user_id不为空的资源(已注册用户)
+            queryWrapper.isNotNull("user_id");
+
+            // 性别筛选
+            if (gender != null) {
+                queryWrapper.eq("gender", gender);
+            }
+
+            // 按创建时间降序
+            queryWrapper.orderByDesc("create_time");
+            queryWrapper.last("LIMIT 100");
+
+            java.util.List<MyResource> resources = myResourceService.list(queryWrapper);
+            System.out.println("查询到已注册资源数量: " + resources.size());
+
+            // 转换为简化的下拉列表格式,并查询用户头像
+            java.util.List<java.util.Map<String, Object>> dropdown = new java.util.ArrayList<>();
+            for (MyResource resource : resources) {
+                java.util.Map<String, Object> item = new java.util.HashMap<>();
+                item.put("resourceId", resource.getResourceId());
+                item.put("userId", resource.getUserId());
+                item.put("name", resource.getName());
+                item.put("gender", resource.getGender());
+                item.put("phone", resource.getPhone());
+                item.put("age", resource.getAge());
+
+                // 查询用户头像
+                String avatarUrl = "/static/default-avatar.svg";
+                if (resource.getUserId() != null) {
+                    com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<User> userQuery =
+                        new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<>();
+                    userQuery.eq("user_id", resource.getUserId());
+                    User user = userMapper.selectOne(userQuery);
+                    if (user != null && user.getAvatarUrl() != null) {
+                        avatarUrl = user.getAvatarUrl();
+                    }
+                }
+                item.put("avatarUrl", avatarUrl);
+
+                dropdown.add(item);
+            }
+
+            System.out.println("已注册用户下拉列表数量: " + dropdown.size());
+            return Result.success(dropdown);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("获取已注册用户资源下拉列表失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 搜索已注册用户的资源(按姓名或手机号)
+     * 只返回user_id不为空的资源
+     *
+     * @param matchmakerId 红娘ID
+     * @param keyword 搜索关键词(姓名或手机号)
+     * @param gender 性别筛选(1-男,2-女,可选)
+     * @return 匹配的已注册用户资源列表
+     */
+    @GetMapping("/registered-search")
+    public Result<java.util.List<java.util.Map<String, Object>>> searchRegisteredResources(
+            @RequestParam Integer matchmakerId,
+            @RequestParam(required = false) String keyword,
+            @RequestParam(required = false) Integer gender) {
+        try {
+            System.out.println("=== 搜索已注册用户资源 ===");
+            System.out.println("matchmakerId: " + matchmakerId);
+            System.out.println("keyword: " + keyword);
+            System.out.println("gender: " + gender);
+
+            // 构建查询条件
+            com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<MyResource> queryWrapper =
+                new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<>();
+            queryWrapper.eq("matchmaker_id", matchmakerId);
+            // 只查询user_id不为空的资源(已注册用户)
+            queryWrapper.isNotNull("user_id");
+
+            // 性别筛选
+            if (gender != null) {
+                queryWrapper.eq("gender", gender);
+            }
+
+            // 关键词搜索(姓名或手机号)
+            if (keyword != null && !keyword.trim().isEmpty()) {
+                String kw = keyword.trim();
+                queryWrapper.and(wrapper ->
+                    wrapper.like("name", kw)
+                           .or()
+                           .like("phone", kw)
+                );
+            }
+
+            // 按创建时间降序
+            queryWrapper.orderByDesc("create_time");
+            queryWrapper.last("LIMIT 100");
+
+            java.util.List<MyResource> resources = myResourceService.list(queryWrapper);
+            System.out.println("搜索到已注册资源数量: " + resources.size());
+
+            // 转换为简化的列表格式,并查询用户头像
+            java.util.List<java.util.Map<String, Object>> result = new java.util.ArrayList<>();
+            for (MyResource resource : resources) {
+                java.util.Map<String, Object> item = new java.util.HashMap<>();
+                item.put("resourceId", resource.getResourceId());
+                item.put("userId", resource.getUserId());
+                item.put("name", resource.getName());
+                item.put("gender", resource.getGender());
+                item.put("phone", resource.getPhone());
+                item.put("age", resource.getAge());
+
+                // 查询用户头像
+                String avatarUrl = "/static/default-avatar.svg";
+                if (resource.getUserId() != null) {
+                    com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<User> userQuery =
+                        new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<>();
+                    userQuery.eq("user_id", resource.getUserId());
+                    User user = userMapper.selectOne(userQuery);
+                    if (user != null && user.getAvatarUrl() != null) {
+                        avatarUrl = user.getAvatarUrl();
+                    }
+                }
+                item.put("avatarUrl", avatarUrl);
+
+                result.add(item);
+            }
+
+            System.out.println("搜索结果数量: " + result.size());
+            return Result.success(result);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("搜索已注册用户资源失败:" + e.getMessage());
+        }
+    }
 }
 

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

@@ -91,7 +91,17 @@ public interface MyResourceService extends IService<MyResource> {
      * @return 标签ID列表
      */
     List<Integer> getTagIdsByResourceId(Integer resourceId);
-    
+
+    /**
+     * 搜索资源(按姓名或手机号)
+     * 用于撮合成功审核页面的下拉搜索
+     * @param matchmakerId 红娘ID
+     * @param keyword 搜索关键词(姓名或手机号)
+     * @param gender 性别筛选(1-男,2-女,可选)
+     * @return 匹配的资源列表
+     */
+    java.util.List<com.zhentao.vo.MyResourceVO> searchResources(Integer matchmakerId, String keyword, Integer gender);
+
     /**
      * 批量检查并更新线索用户的注册状态
      * 检查所有线索用户(is_user=0或null)的手机号是否在users表中已注册

+ 10 - 0
service/homePage/src/main/java/com/zhentao/service/PointsManageService.java

@@ -21,6 +21,16 @@ public interface PointsManageService {
      */
     boolean addPointsByRuleName(Integer makerId, String ruleName, String reason);
 
+    /**
+     * 根据规则类型为红娘添加积分
+     *
+     * @param makerId 红娘ID
+     * @param ruleType 规则类型:1-签到 2-上传线索 3-撮合成功 4-完成培训 5-活动奖励
+     * @param reason 额外原因
+     * @return 是否成功
+     */
+    boolean addPointsByRuleType(Integer makerId, Integer ruleType, String reason);
+
     /**
      * 直接为红娘添加指定数量的积分
      *

+ 177 - 14
service/homePage/src/main/java/com/zhentao/service/impl/MyResourceServiceImpl.java

@@ -52,6 +52,9 @@ public class MyResourceServiceImpl extends ServiceImpl<MyResourceMapper, MyResou
     
     @Autowired(required = false)
     private com.zhentao.service.UserFollowUpService userFollowUpService;
+    
+    @Autowired(required = false)
+    private com.zhentao.service.PointsManageService pointsManageService;
 
     @Override
     @Transactional(rollbackFor = Exception.class)
@@ -131,8 +134,87 @@ public class MyResourceServiceImpl extends ServiceImpl<MyResourceMapper, MyResou
             System.out.println("=== 标签关联保存完成 ===");
         }
         
+        // 资源录入成功后,为红娘添加积分奖励(阶梯奖励)
+        // 只有上传线索(未注册用户,isUser=0)才给积分,上传资源(已注册用户,isUser=1)不给积分
+        boolean isClue = myResource.getIsUser() == null || myResource.getIsUser() == 0;
+        if (result && isClue && myResource.getMatchmakerId() != null && pointsManageService != null) {
+            try {
+                String resourceName = myResource.getName() != null ? myResource.getName() : "未知";
+                
+                // 查询红娘当日已上传的线索数量(只统计未注册用户,包括刚录入的这条)
+                int todayCount = getTodayClueCount(myResource.getMatchmakerId());
+                
+                // 计算阶梯积分:基础10分,超3人+2分,超10人+5分
+                int basePoints = 10;
+                int bonusPoints = 0;
+                if (todayCount > 10) {
+                    bonusPoints = 5;  // 超过10人,每个额外+5分
+                } else if (todayCount > 3) {
+                    bonusPoints = 2;  // 超过3人,每个额外+2分
+                }
+                int totalPoints = basePoints + bonusPoints;
+                
+                String reason = "上传线索: " + resourceName + " (今日第" + todayCount + "个";
+                if (bonusPoints > 0) {
+                    reason += ",额外奖励+" + bonusPoints;
+                }
+                reason += ")";
+                
+                // 直接添加计算后的积分
+                boolean pointsAdded = pointsManageService.addPoints(
+                    myResource.getMatchmakerId(), 
+                    totalPoints,
+                    reason
+                );
+                if (pointsAdded) {
+                    System.out.println("✅ 线索录入积分奖励添加成功,红娘ID: " + myResource.getMatchmakerId() + 
+                                      ",今日第" + todayCount + "个线索,获得" + totalPoints + "积分");
+                } else {
+                    System.out.println("⚠️ 线索录入积分奖励添加失败,红娘ID: " + myResource.getMatchmakerId());
+                }
+            } catch (Exception e) {
+                // 积分添加失败不影响资源录入
+                System.err.println("⚠️ 添加积分奖励时发生异常: " + e.getMessage());
+                e.printStackTrace();
+            }
+        } else if (result && !isClue) {
+            System.out.println("📝 录入的是已注册用户(资源),不给积分奖励");
+        }
+        
         return result;
     }
+    
+    /**
+     * 获取红娘当日已上传的线索数量(只统计未注册用户,isUser=0或null)
+     * @param matchmakerId 红娘ID
+     * @return 当日上传线索数量
+     */
+    private int getTodayClueCount(Integer matchmakerId) {
+        if (matchmakerId == null) {
+            return 0;
+        }
+        
+        // 获取今天的开始时间和结束时间
+        java.util.Calendar calendar = java.util.Calendar.getInstance();
+        calendar.set(java.util.Calendar.HOUR_OF_DAY, 0);
+        calendar.set(java.util.Calendar.MINUTE, 0);
+        calendar.set(java.util.Calendar.SECOND, 0);
+        calendar.set(java.util.Calendar.MILLISECOND, 0);
+        Date todayStart = calendar.getTime();
+        
+        calendar.add(java.util.Calendar.DAY_OF_MONTH, 1);
+        Date todayEnd = calendar.getTime();
+        
+        // 查询当日该红娘上传的线索数量(只统计未注册用户,isUser=0或null)
+        QueryWrapper<MyResource> queryWrapper = new QueryWrapper<>();
+        queryWrapper.eq("matchmaker_id", matchmakerId)
+                   .ge("create_time", todayStart)
+                   .lt("create_time", todayEnd)
+                   .and(wrapper -> wrapper.eq("is_user", 0).or().isNull("is_user"));
+        
+        long count = this.count(queryWrapper);
+        return (int) count;
+    }
 
     @Override
     public Page<MyResource> getResourceList(Integer matchmakerId, String keyword, Integer pageNum, Integer pageSize) {
@@ -814,7 +896,88 @@ public class MyResourceServiceImpl extends ServiceImpl<MyResourceMapper, MyResou
         }
         return sb.length() > 0 ? sb.toString().trim() : "暂无要求";
     }
-    
+
+    @Override
+    public List<MyResourceVO> searchResources(Integer matchmakerId, String keyword, Integer gender) {
+        System.out.println("=== searchResources 开始 ===");
+        System.out.println("matchmakerId: " + matchmakerId + ", keyword: " + keyword + ", gender: " + gender);
+        
+        // 构建查询条件
+        QueryWrapper<MyResource> queryWrapper = new QueryWrapper<>();
+        queryWrapper.eq("matchmaker_id", matchmakerId);
+        
+        // 性别筛选
+        if (gender != null) {
+            queryWrapper.eq("gender", gender);
+        }
+        
+        // 关键词搜索(姓名或手机号)
+        if (StringUtils.hasText(keyword)) {
+            queryWrapper.and(wrapper -> 
+                wrapper.like("name", keyword)
+                       .or()
+                       .like("phone", keyword)
+            );
+        }
+        
+        // 按创建时间降序
+        queryWrapper.orderByDesc("create_time");
+        
+        // 限制返回数量,避免数据过多
+        queryWrapper.last("LIMIT 100");
+        
+        List<MyResource> resources = myResourceMapper.selectList(queryWrapper);
+        System.out.println("查询到资源数量: " + resources.size());
+        
+        // 转换为VO并填充头像
+        List<MyResourceVO> voList = new ArrayList<>();
+        for (MyResource resource : resources) {
+            MyResourceVO vo = new MyResourceVO();
+            vo.setResourceId(resource.getResourceId());
+            vo.setMatchmakerId(resource.getMatchmakerId());
+            vo.setName(resource.getName());
+            vo.setAge(resource.getAge());
+            vo.setGender(resource.getGender());
+            vo.setConstellation(resource.getConstellation());
+            vo.setHeight(resource.getHeight());
+            vo.setWeight(resource.getWeight());
+            vo.setMarrStatus(resource.getMarrStatus());
+            vo.setDiploma(resource.getDiploma());
+            vo.setIncome(resource.getIncome());
+            vo.setAddress(resource.getAddress());
+            vo.setDomicile(resource.getDomicile());
+            vo.setOccupation(resource.getOccupation());
+            vo.setHouse(resource.getHouse());
+            vo.setPhone(resource.getPhone());
+            vo.setBackupPhone(resource.getBackupPhone());
+            vo.setCar(resource.getCar());
+            vo.setMateSelectionCriteria(resource.getMateSelectionCriteria());
+            vo.setCreateTime(resource.getCreateTime());
+            vo.setUpdateTime(resource.getUpdateTime());
+            vo.setIsUser(resource.getIsUser());
+            
+            // 查询用户头像
+            if (resource.getIsUser() != null && resource.getIsUser() == 1) {
+                // 根据手机号查询用户头像
+                QueryWrapper<User> userQuery = new QueryWrapper<>();
+                userQuery.eq("phone", resource.getPhone());
+                User user = userMapper.selectOne(userQuery);
+                if (user != null && user.getAvatarUrl() != null) {
+                    vo.setAvatarUrl(user.getAvatarUrl());
+                } else {
+                    vo.setAvatarUrl("/static/default-avatar.svg");
+                }
+            } else {
+                vo.setAvatarUrl("/static/default-avatar.svg");
+            }
+            
+            voList.add(vo);
+        }
+        
+        System.out.println("返回VO数量: " + voList.size());
+        return voList;
+    }
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     public int batchCheckAndUpdateClueRegistrationStatus(Integer matchmakerId) {
@@ -824,7 +987,7 @@ public class MyResourceServiceImpl extends ServiceImpl<MyResourceMapper, MyResou
             if (matchmakerId != null) {
                 System.out.println("红娘ID: " + matchmakerId);
             }
-            
+
             // 查询所有线索用户(is_user=0或null,且user_id为null)
             QueryWrapper<MyResource> queryWrapper = new QueryWrapper<>();
             queryWrapper.and(wrapper -> wrapper
@@ -837,32 +1000,32 @@ public class MyResourceServiceImpl extends ServiceImpl<MyResourceMapper, MyResou
                 .or()
                 .eq("user_id", 0)
             );
-            
+
             // 如果提供了红娘ID,则只检查该红娘的线索
             if (matchmakerId != null) {
                 queryWrapper.eq("matchmaker_id", matchmakerId);
             }
-            
+
             List<MyResource> clueResources = this.list(queryWrapper);
-            
+
             if (clueResources == null || clueResources.isEmpty()) {
                 System.out.println("未找到需要检查的线索用户");
                 return 0;
             }
-            
+
             System.out.println("找到 " + clueResources.size() + " 个线索用户需要检查");
-            
+
             // 遍历每个线索用户,检查其手机号是否在users表中已注册
             for (MyResource clueResource : clueResources) {
                 String phone = clueResource.getPhone();
                 String backupPhone = clueResource.getBackupPhone();
-                
+
                 // 检查主手机号
                 if (phone != null && !phone.trim().isEmpty()) {
                     QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
                     userQueryWrapper.eq("phone", phone.trim());
                     User user = userMapper.selectOne(userQueryWrapper);
-                    
+
                     if (user != null && user.getUserId() != null) {
                         // 找到匹配的用户,更新线索资源
                         clueResource.setIsUser(1);
@@ -871,7 +1034,7 @@ public class MyResourceServiceImpl extends ServiceImpl<MyResourceMapper, MyResou
                         boolean updated = this.updateById(clueResource);
                         if (updated) {
                             updateCount++;
-                            System.out.println("✅ 更新线索用户注册状态成功 - 资源ID: " + clueResource.getResourceId() + 
+                            System.out.println("✅ 更新线索用户注册状态成功 - 资源ID: " + clueResource.getResourceId() +
                                              ", 手机号: " + phone + ", 用户ID: " + user.getUserId());
                         } else {
                             System.out.println("⚠️ 更新线索用户注册状态失败 - 资源ID: " + clueResource.getResourceId());
@@ -879,13 +1042,13 @@ public class MyResourceServiceImpl extends ServiceImpl<MyResourceMapper, MyResou
                         continue; // 已更新,跳过备用手机号检查
                     }
                 }
-                
+
                 // 如果主手机号未找到匹配,检查备用手机号
                 if (backupPhone != null && !backupPhone.trim().isEmpty()) {
                     QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
                     userQueryWrapper.eq("phone", backupPhone.trim());
                     User user = userMapper.selectOne(userQueryWrapper);
-                    
+
                     if (user != null && user.getUserId() != null) {
                         // 找到匹配的用户,更新线索资源
                         clueResource.setIsUser(1);
@@ -894,7 +1057,7 @@ public class MyResourceServiceImpl extends ServiceImpl<MyResourceMapper, MyResou
                         boolean updated = this.updateById(clueResource);
                         if (updated) {
                             updateCount++;
-                            System.out.println("✅ 更新线索用户注册状态成功(通过备用手机号) - 资源ID: " + clueResource.getResourceId() + 
+                            System.out.println("✅ 更新线索用户注册状态成功(通过备用手机号) - 资源ID: " + clueResource.getResourceId() +
                                              ", 备用手机号: " + backupPhone + ", 用户ID: " + user.getUserId());
                         } else {
                             System.out.println("⚠️ 更新线索用户注册状态失败 - 资源ID: " + clueResource.getResourceId());
@@ -902,7 +1065,7 @@ public class MyResourceServiceImpl extends ServiceImpl<MyResourceMapper, MyResou
                     }
                 }
             }
-            
+
             System.out.println("=== 批量检查并更新线索用户注册状态完成,共更新 " + updateCount + " 条记录 ===");
             return updateCount;
         } catch (Exception e) {

+ 31 - 0
service/homePage/src/main/java/com/zhentao/service/impl/PointsManageServiceImpl.java

@@ -61,6 +61,37 @@ public class PointsManageServiceImpl implements PointsManageService {
         return addPoints(makerId, rule.getPointValue(), fullReason);
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean addPointsByRuleType(Integer makerId, Integer ruleType, String reason) {
+        // 根据规则类型查询积分规则
+        com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<PointRule> wrapper = 
+            new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<>();
+        wrapper.eq("rule_type", ruleType).eq("status", 1);
+        PointRule rule = pointRuleMapper.selectOne(wrapper);
+        
+        if (rule == null) {
+            System.err.println("积分规则不存在,规则类型: " + ruleType);
+            return false;
+        }
+        if (rule.getPointValue() <= 0) {
+            System.err.println("积分值必须为正数,规则类型: " + ruleType);
+            return false;
+        }
+
+        // 构建积分变动原因
+        String fullReason = rule.getRuleName();
+        if (reason != null && !reason.isEmpty()) {
+            fullReason += "-" + reason;
+        }
+
+        System.out.println("添加积分 - 红娘ID: " + makerId + ", 规则类型: " + ruleType + 
+                          ", 规则名称: " + rule.getRuleName() + ", 积分值: " + rule.getPointValue());
+
+        // 调用添加积分方法
+        return addPoints(makerId, rule.getPointValue(), fullReason);
+    }
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     public boolean addPoints(Integer makerId, Integer points, String reason) {

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

@@ -36,6 +36,11 @@ public class MinioConfig {
      */
     private String bucketName;
 
+    /**
+     * 成功凭证存储桶名称
+     */
+    private String proofBucketName = "Proof-of-success";
+
     /**
      * 创建MinioClient Bean
      */

+ 385 - 0
service/websocket/src/main/java/com/zhentao/controller/SuccessCaseUploadController.java

@@ -0,0 +1,385 @@
+package com.zhentao.controller;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.zhentao.dto.SuccessCaseUploadDTO;
+import com.zhentao.entity.MatchmakerSuccessCaseUpload;
+import com.zhentao.service.MatchmakerSuccessCaseUploadService;
+import com.zhentao.utils.MinioUtil;
+import lombok.extern.slf4j.Slf4j;
+import lombok.var;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 成功案例上传控制器
+ */
+@Slf4j
+@RestController
+@RequestMapping("/api/success-case-upload")
+public class SuccessCaseUploadController {
+
+    @Autowired
+    private MatchmakerSuccessCaseUploadService successCaseUploadService;
+
+    @Autowired
+    private com.zhentao.mapper.MatchmakerSuccessCaseUploadMapper successCaseUploadMapper;
+
+    @Autowired
+    private MinioUtil minioUtil;
+
+    @Value("${minio.proofBucketName:Proof-of-success}")
+    private String proofBucketName;
+
+    private final ObjectMapper objectMapper = new ObjectMapper();
+
+    /**
+     * 上传成功凭证图片
+     * @param file 图片文件
+     * @return 图片URL
+     */
+    @PostMapping("/upload-image")
+    public ResponseEntity<Map<String, Object>> uploadImage(@RequestParam("file") MultipartFile file) {
+        Map<String, Object> result = new HashMap<>();
+        try {
+            if (file.isEmpty()) {
+                result.put("code", 400);
+                result.put("message", "请选择要上传的图片");
+                return ResponseEntity.badRequest().body(result);
+            }
+
+            // 验证文件类型
+            String contentType = file.getContentType();
+            if (contentType == null || !contentType.startsWith("image/")) {
+                result.put("code", 400);
+                result.put("message", "只能上传图片文件");
+                return ResponseEntity.badRequest().body(result);
+            }
+
+            // 上传到MinIO的Proof-of-success桶
+            String imageUrl = minioUtil.uploadFileToProofBucket(
+                    file.getBytes(),
+                    file.getOriginalFilename(),
+                    contentType
+            );
+
+            result.put("code", 200);
+            result.put("message", "上传成功");
+            result.put("data", imageUrl);
+            log.info("成功凭证图片上传成功: {}", imageUrl);
+            return ResponseEntity.ok(result);
+
+        } catch (Exception e) {
+            log.error("图片上传失败", e);
+            result.put("code", 500);
+            result.put("message", "图片上传失败:" + e.getMessage());
+            return ResponseEntity.internalServerError().body(result);
+        }
+    }
+
+    /**
+     * 提交成功案例
+     * @param dto 成功案例数据
+     * @return 提交结果
+     */
+    @PostMapping("/submit")
+    public ResponseEntity<Map<String, Object>> submitSuccessCase(@RequestBody SuccessCaseUploadDTO dto) {
+        Map<String, Object> result = new HashMap<>();
+        try {
+            // 参数校验
+            if (dto.getMatchmakerId() == null) {
+                result.put("code", 400);
+                result.put("message", "红娘ID不能为空");
+                return ResponseEntity.badRequest().body(result);
+            }
+            if (dto.getMaleUserId() == null) {
+                result.put("code", 400);
+                result.put("message", "请选择男方用户");
+                return ResponseEntity.badRequest().body(result);
+            }
+            if (dto.getFemaleUserId() == null) {
+                result.put("code", 400);
+                result.put("message", "请选择女方用户");
+                return ResponseEntity.badRequest().body(result);
+            }
+            if (dto.getMaleUserId().equals(dto.getFemaleUserId())) {
+                result.put("code", 400);
+                result.put("message", "男方和女方不能是同一人");
+                return ResponseEntity.badRequest().body(result);
+            }
+            if (dto.getMaleRealName() == null || dto.getMaleRealName().trim().isEmpty()) {
+                result.put("code", 400);
+                result.put("message", "请填写男方真实姓名");
+                return ResponseEntity.badRequest().body(result);
+            }
+            if (dto.getFemaleRealName() == null || dto.getFemaleRealName().trim().isEmpty()) {
+                result.put("code", 400);
+                result.put("message", "请填写女方真实姓名");
+                return ResponseEntity.badRequest().body(result);
+            }
+            if (dto.getProofImages() == null || dto.getProofImages().isEmpty()) {
+                result.put("code", 400);
+                result.put("message", "请上传至少一张成功凭证图片");
+                return ResponseEntity.badRequest().body(result);
+            }
+            if (dto.getCaseType() == null || (dto.getCaseType() != 1 && dto.getCaseType() != 2)) {
+                result.put("code", 400);
+                result.put("message", "请选择案例类型(订婚或结婚)");
+                return ResponseEntity.badRequest().body(result);
+            }
+
+            // 构建实体
+            MatchmakerSuccessCaseUpload upload = new MatchmakerSuccessCaseUpload();
+            upload.setMatchmakerId(dto.getMatchmakerId());
+            upload.setMaleUserId(dto.getMaleUserId());
+            upload.setFemaleUserId(dto.getFemaleUserId());
+            upload.setMaleRealName(dto.getMaleRealName().trim());
+            upload.setFemaleRealName(dto.getFemaleRealName().trim());
+            
+            // 将图片列表转为JSON字符串
+            upload.setProofImages(objectMapper.writeValueAsString(dto.getProofImages()));
+            upload.setCaseType(dto.getCaseType());
+            
+            // 解析日期
+            if (dto.getCaseDate() != null && !dto.getCaseDate().isEmpty()) {
+                upload.setCaseDate(LocalDate.parse(dto.getCaseDate(), DateTimeFormatter.ISO_LOCAL_DATE));
+            }
+
+            // 保存
+            boolean success = successCaseUploadService.submitSuccessCase(upload);
+            
+            if (success) {
+                result.put("code", 200);
+                result.put("message", "提交成功,请等待审核");
+                result.put("data", upload.getId());
+                log.info("成功案例提交成功: id={}, matchmakerId={}", upload.getId(), upload.getMatchmakerId());
+            } else {
+                result.put("code", 500);
+                result.put("message", "提交失败,请稍后重试");
+            }
+            
+            return ResponseEntity.ok(result);
+
+        } catch (Exception e) {
+            log.error("提交成功案例失败", e);
+            result.put("code", 500);
+            result.put("message", "提交失败:" + e.getMessage());
+            return ResponseEntity.internalServerError().body(result);
+        }
+    }
+
+    /**
+     * 获取红娘的成功案例列表
+     * @param matchmakerId 红娘ID
+     * @return 成功案例列表
+     */
+    @GetMapping("/list")
+    public ResponseEntity<Map<String, Object>> getSuccessCaseList(
+            @RequestParam Integer matchmakerId,
+            @RequestParam(defaultValue = "1") Integer pageNum,
+            @RequestParam(defaultValue = "10") Integer pageSize) {
+        Map<String, Object> result = new HashMap<>();
+        try {
+            var page = new com.baomidou.mybatisplus.extension.plugins.pagination.Page<MatchmakerSuccessCaseUpload>(pageNum, pageSize);
+            var queryWrapper = new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<MatchmakerSuccessCaseUpload>();
+            queryWrapper.eq(MatchmakerSuccessCaseUpload::getMatchmakerId, matchmakerId)
+                    .orderByDesc(MatchmakerSuccessCaseUpload::getCreatedAt);
+            
+            var pageResult = successCaseUploadService.page(page, queryWrapper);
+            
+            result.put("code", 200);
+            result.put("message", "查询成功");
+            result.put("data", pageResult);
+            return ResponseEntity.ok(result);
+
+        } catch (Exception e) {
+            log.error("查询成功案例列表失败", e);
+            result.put("code", 500);
+            result.put("message", "查询失败:" + e.getMessage());
+            return ResponseEntity.internalServerError().body(result);
+        }
+    }
+
+    /**
+     * 获取审核记录列表(用于消息通知页面)
+     * @param matchmakerId 红娘ID
+     * @param auditStatus 审核状态筛选(可选):0-待审核 1-审核通过 2-审核失败 3-核实中
+     * @return 审核记录列表
+     */
+    @GetMapping("/audit-records")
+    public ResponseEntity<Map<String, Object>> getAuditRecords(
+            @RequestParam Integer matchmakerId,
+            @RequestParam(required = false) Integer auditStatus,
+            @RequestParam(defaultValue = "1") Integer pageNum,
+            @RequestParam(defaultValue = "20") Integer pageSize) {
+        Map<String, Object> result = new HashMap<>();
+        try {
+            var page = new com.baomidou.mybatisplus.extension.plugins.pagination.Page<MatchmakerSuccessCaseUpload>(pageNum, pageSize);
+            var queryWrapper = new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<MatchmakerSuccessCaseUpload>();
+            queryWrapper.eq(MatchmakerSuccessCaseUpload::getMatchmakerId, matchmakerId);
+            
+            // 如果指定了审核状态,则筛选
+            if (auditStatus != null) {
+                queryWrapper.eq(MatchmakerSuccessCaseUpload::getAuditStatus, auditStatus);
+            }
+            
+            // 按创建时间降序
+            queryWrapper.orderByDesc(MatchmakerSuccessCaseUpload::getCreatedAt);
+            
+            var pageResult = successCaseUploadService.page(page, queryWrapper);
+            
+            result.put("code", 200);
+            result.put("message", "查询成功");
+            result.put("data", pageResult);
+            return ResponseEntity.ok(result);
+
+        } catch (Exception e) {
+            log.error("查询审核记录列表失败", e);
+            result.put("code", 500);
+            result.put("message", "查询失败:" + e.getMessage());
+            return ResponseEntity.internalServerError().body(result);
+        }
+    }
+
+    /**
+     * 获取审核记录详情(包含男女双方用户信息)
+     * @param id 记录ID
+     * @return 记录详情
+     */
+    @GetMapping("/audit-records/{id}")
+    public ResponseEntity<Map<String, Object>> getAuditRecordDetail(@PathVariable Long id) {
+        Map<String, Object> result = new HashMap<>();
+        try {
+            MatchmakerSuccessCaseUpload record = successCaseUploadService.getById(id);
+            if (record == null) {
+                result.put("code", 404);
+                result.put("message", "记录不存在");
+                return ResponseEntity.ok(result);
+            }
+            
+            // 构建返回数据,包含案例信息和用户信息
+            Map<String, Object> data = new HashMap<>();
+            data.put("id", record.getId());
+            data.put("matchmakerId", record.getMatchmakerId());
+            data.put("maleUserId", record.getMaleUserId());
+            data.put("femaleUserId", record.getFemaleUserId());
+            data.put("maleRealName", record.getMaleRealName());
+            data.put("femaleRealName", record.getFemaleRealName());
+            data.put("proofImages", record.getProofImages());
+            data.put("caseType", record.getCaseType());
+            data.put("caseDate", record.getCaseDate());
+            data.put("auditStatus", record.getAuditStatus());
+            data.put("auditRemark", record.getAuditRemark());
+            data.put("pointsReward", record.getPointsReward());
+            data.put("cashReward", record.getCashReward());
+            data.put("rewardStatus", record.getRewardStatus());
+            data.put("createdAt", record.getCreatedAt());
+            data.put("auditedAt", record.getAuditedAt());
+            data.put("isRead", record.getIsRead());
+            
+            // 查询男方用户信息
+            if (record.getMaleUserId() != null) {
+                Map<String, Object> maleUserInfo = successCaseUploadMapper.getUserBasicInfo(record.getMaleUserId());
+                if (maleUserInfo != null) {
+                    data.put("maleUserInfo", maleUserInfo);
+                }
+            }
+            
+            // 查询女方用户信息
+            if (record.getFemaleUserId() != null) {
+                Map<String, Object> femaleUserInfo = successCaseUploadMapper.getUserBasicInfo(record.getFemaleUserId());
+                if (femaleUserInfo != null) {
+                    data.put("femaleUserInfo", femaleUserInfo);
+                }
+            }
+            
+            result.put("code", 200);
+            result.put("message", "查询成功");
+            result.put("data", data);
+            return ResponseEntity.ok(result);
+
+        } catch (Exception e) {
+            log.error("查询审核记录详情失败", e);
+            result.put("code", 500);
+            result.put("message", "查询失败:" + e.getMessage());
+            return ResponseEntity.internalServerError().body(result);
+        }
+    }
+
+    /**
+     * 标记审核记录为已读
+     * @param id 记录ID
+     * @return 操作结果
+     */
+    @PostMapping("/audit-records/{id}/read")
+    public ResponseEntity<Map<String, Object>> markAsRead(@PathVariable Long id) {
+        Map<String, Object> result = new HashMap<>();
+        try {
+            MatchmakerSuccessCaseUpload record = successCaseUploadService.getById(id);
+            if (record == null) {
+                result.put("code", 404);
+                result.put("message", "记录不存在");
+                return ResponseEntity.ok(result);
+            }
+            
+            // 标记为已读
+            record.setIsRead(1);
+            boolean success = successCaseUploadService.updateById(record);
+            
+            if (success) {
+                result.put("code", 200);
+                result.put("message", "标记成功");
+            } else {
+                result.put("code", 500);
+                result.put("message", "标记失败");
+            }
+            return ResponseEntity.ok(result);
+
+        } catch (Exception e) {
+            log.error("标记已读失败", e);
+            result.put("code", 500);
+            result.put("message", "操作失败:" + e.getMessage());
+            return ResponseEntity.internalServerError().body(result);
+        }
+    }
+
+    /**
+     * 获取未读审核记录数量
+     * @param matchmakerId 红娘ID
+     * @return 未读数量
+     */
+    @GetMapping("/audit-records/unread-count")
+    public ResponseEntity<Map<String, Object>> getUnreadCount(@RequestParam Integer matchmakerId) {
+        Map<String, Object> result = new HashMap<>();
+        try {
+            var queryWrapper = new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<MatchmakerSuccessCaseUpload>();
+            queryWrapper.eq(MatchmakerSuccessCaseUpload::getMatchmakerId, matchmakerId)
+                    .and(w -> w.isNull(MatchmakerSuccessCaseUpload::getIsRead)
+                            .or()
+                            .eq(MatchmakerSuccessCaseUpload::getIsRead, 0))
+                    // 只统计已审核完成的(审核通过或审核失败)
+                    .in(MatchmakerSuccessCaseUpload::getAuditStatus, 1, 2);
+            
+            long count = successCaseUploadService.count(queryWrapper);
+            
+            result.put("code", 200);
+            result.put("message", "查询成功");
+            result.put("data", count);
+            return ResponseEntity.ok(result);
+
+        } catch (Exception e) {
+            log.error("查询未读数量失败", e);
+            result.put("code", 500);
+            result.put("message", "查询失败:" + e.getMessage());
+            return ResponseEntity.internalServerError().body(result);
+        }
+    }
+}

+ 52 - 0
service/websocket/src/main/java/com/zhentao/dto/SuccessCaseUploadDTO.java

@@ -0,0 +1,52 @@
+package com.zhentao.dto;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 成功案例上传DTO
+ */
+@Data
+public class SuccessCaseUploadDTO {
+
+    /**
+     * 红娘ID
+     */
+    private Integer matchmakerId;
+
+    /**
+     * 男方用户ID
+     */
+    private Integer maleUserId;
+
+    /**
+     * 女方用户ID
+     */
+    private Integer femaleUserId;
+
+    /**
+     * 男方真实姓名
+     */
+    private String maleRealName;
+
+    /**
+     * 女方真实姓名
+     */
+    private String femaleRealName;
+
+    /**
+     * 成功凭证图片URL列表
+     */
+    private List<String> proofImages;
+
+    /**
+     * 案例类型:1-订婚 2-结婚
+     */
+    private Integer caseType;
+
+    /**
+     * 成功日期(格式:yyyy-MM-dd)
+     */
+    private String caseDate;
+}

+ 132 - 0
service/websocket/src/main/java/com/zhentao/entity/MatchmakerSuccessCaseUpload.java

@@ -0,0 +1,132 @@
+package com.zhentao.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+/**
+ * 红娘上传成功案例实体类
+ */
+@Data
+@TableName("matchmaker_success_case_upload")
+public class MatchmakerSuccessCaseUpload implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 撮合红娘ID
+     */
+    private Integer matchmakerId;
+
+    /**
+     * 男方用户ID
+     */
+    private Integer maleUserId;
+
+    /**
+     * 女方用户ID
+     */
+    private Integer femaleUserId;
+
+    /**
+     * 男方真实姓名
+     */
+    private String maleRealName;
+
+    /**
+     * 女方真实姓名
+     */
+    private String femaleRealName;
+
+    /**
+     * 成功凭证图片路径(JSON数组格式)
+     */
+    private String proofImages;
+
+    /**
+     * 案例类型:1-订婚 2-领证结婚
+     */
+    private Integer caseType;
+
+    /**
+     * 成功日期(结婚日期/订婚日期)
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    private LocalDate caseDate;
+
+    /**
+     * 审核状态:0-待审核 1-审核通过 2-审核失败 3-核实中
+     */
+    private Integer auditStatus;
+
+    /**
+     * 审核备注(失败原因)
+     */
+    private String auditRemark;
+
+    /**
+     * 审核人ID(管理员)
+     */
+    private Integer auditorId;
+
+    /**
+     * 积分奖励
+     */
+    private Integer pointsReward;
+
+    /**
+     * 现金奖励
+     */
+    private BigDecimal cashReward;
+
+    /**
+     * 奖励状态:0-未发放 1-已发放
+     */
+    private Integer rewardStatus;
+
+    /**
+     * 是否发布到成功案例展示:0-否 1-是
+     */
+    private Integer isPublished;
+
+    /**
+     * 关联的成功案例ID
+     */
+    private Integer publishedCaseId;
+
+    /**
+     * 是否已读:0-未读 1-已读
+     */
+    private Integer isRead;
+
+    /**
+     * 上传时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @TableField(fill = FieldFill.INSERT)
+    private LocalDateTime createdAt;
+
+    /**
+     * 更新时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private LocalDateTime updatedAt;
+
+    /**
+     * 审核时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime auditedAt;
+}

+ 25 - 0
service/websocket/src/main/java/com/zhentao/mapper/MatchmakerSuccessCaseUploadMapper.java

@@ -0,0 +1,25 @@
+package com.zhentao.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zhentao.entity.MatchmakerSuccessCaseUpload;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.Map;
+
+/**
+ * 红娘上传成功案例Mapper
+ */
+@Mapper
+public interface MatchmakerSuccessCaseUploadMapper extends BaseMapper<MatchmakerSuccessCaseUpload> {
+    
+    /**
+     * 根据用户ID查询用户基本信息(头像、年龄等)
+     * @param userId 用户ID(users表的user_id)
+     * @return 用户信息Map
+     */
+    @Select("SELECT user_id as userId, nickname as name, avatar_url as avatarUrl, " +
+            "TIMESTAMPDIFF(YEAR, birth_date, CURDATE()) as age " +
+            "FROM users WHERE user_id = #{userId}")
+    Map<String, Object> getUserBasicInfo(Integer userId);
+}

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

@@ -0,0 +1,17 @@
+package com.zhentao.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.zhentao.entity.MatchmakerSuccessCaseUpload;
+
+/**
+ * 红娘上传成功案例Service接口
+ */
+public interface MatchmakerSuccessCaseUploadService extends IService<MatchmakerSuccessCaseUpload> {
+
+    /**
+     * 提交成功案例
+     * @param upload 成功案例数据
+     * @return 是否成功
+     */
+    boolean submitSuccessCase(MatchmakerSuccessCaseUpload upload);
+}

+ 37 - 0
service/websocket/src/main/java/com/zhentao/service/impl/MatchmakerSuccessCaseUploadServiceImpl.java

@@ -0,0 +1,37 @@
+package com.zhentao.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.zhentao.entity.MatchmakerSuccessCaseUpload;
+import com.zhentao.mapper.MatchmakerSuccessCaseUploadMapper;
+import com.zhentao.service.MatchmakerSuccessCaseUploadService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+
+/**
+ * 红娘上传成功案例Service实现
+ */
+@Slf4j
+@Service
+public class MatchmakerSuccessCaseUploadServiceImpl 
+        extends ServiceImpl<MatchmakerSuccessCaseUploadMapper, MatchmakerSuccessCaseUpload> 
+        implements MatchmakerSuccessCaseUploadService {
+
+    @Override
+    public boolean submitSuccessCase(MatchmakerSuccessCaseUpload upload) {
+        // 设置默认值
+        upload.setAuditStatus(0); // 待审核
+        upload.setPointsReward(0);
+        upload.setCashReward(new java.math.BigDecimal("0.00"));
+        upload.setRewardStatus(0); // 未发放
+        upload.setIsPublished(0); // 未发布
+        upload.setCreatedAt(LocalDateTime.now());
+        upload.setUpdatedAt(LocalDateTime.now());
+        
+        log.info("提交成功案例: matchmakerId={}, maleUserId={}, femaleUserId={}", 
+                upload.getMatchmakerId(), upload.getMaleUserId(), upload.getFemaleUserId());
+        
+        return this.save(upload);
+    }
+}

+ 70 - 1
service/websocket/src/main/java/com/zhentao/utils/MinioUtil.java

@@ -24,12 +24,15 @@ public class MinioUtil {
     @Autowired
     private MinioClient minioClient;
 
-    @Value("${minio.bucket-name}")
+    @Value("${minio.bucketName}")
     private String bucketName;
 
     @Value("${minio.endpoint}")
     private String endpoint;
 
+    @Value("${minio.proofBucketName:Proof-of-success}")
+    private String proofBucketName;
+
     /**
      * 初始化,检查并创建bucket
      */
@@ -257,4 +260,70 @@ public class MinioUtil {
             return false;
         }
     }
+
+    /**
+     * 上传文件到成功凭证桶(Proof-of-success)
+     *
+     * @param fileBytes 文件字节数组
+     * @param fileName  文件名
+     * @param contentType 文件类型
+     * @return 文件访问URL
+     */
+    public String uploadFileToProofBucket(byte[] fileBytes, String fileName, String contentType) throws Exception {
+        // 确保桶存在
+        createBucketIfNotExists(proofBucketName);
+        
+        try (java.io.InputStream inputStream = new java.io.ByteArrayInputStream(fileBytes)) {
+            // 生成唯一文件名
+            String objectName = generateProofFileName(fileName);
+            
+            // 上传文件
+            minioClient.putObject(
+                    PutObjectArgs.builder()
+                            .bucket(proofBucketName)
+                            .object(objectName)
+                            .stream(inputStream, fileBytes.length, -1)
+                            .contentType(contentType)
+                            .build()
+            );
+            
+            log.info("✅ 成功凭证图片上传成功: bucket={}, object={}", proofBucketName, objectName);
+            
+            // 返回文件访问URL
+            return String.format("%s/%s/%s", endpoint, proofBucketName, objectName);
+        }
+    }
+
+    /**
+     * 检查并创建指定的bucket
+     */
+    private void createBucketIfNotExists(String targetBucket) throws Exception {
+        boolean exists = minioClient.bucketExists(
+                BucketExistsArgs.builder()
+                        .bucket(targetBucket)
+                        .build()
+        );
+
+        if (!exists) {
+            minioClient.makeBucket(
+                    MakeBucketArgs.builder()
+                            .bucket(targetBucket)
+                            .build()
+            );
+            log.info("✅ 创建Bucket成功: {}", targetBucket);
+        }
+    }
+
+    /**
+     * 生成成功凭证文件名
+     *
+     * @param originalFileName 原始文件名
+     * @return 唯一文件名
+     */
+    private String generateProofFileName(String originalFileName) {
+        String uuid = java.util.UUID.randomUUID().toString().replace("-", "");
+        String extension = getFileExtension(originalFileName);
+        String dateFolder = new java.text.SimpleDateFormat("yyyy/MM/dd").format(new java.util.Date());
+        return String.format("proof/%s/%s%s", dateFolder, uuid, extension);
+    }
 }

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

@@ -81,6 +81,7 @@ 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
+  proofBucketName: proof-of-success