فهرست منبع

1.个人资料的功能进行实现
2.对VIP用户剩余天数不到2天的用户进行提醒
3.成功案例管理中的查看把图片进行移除
4.举报管理中的查看动态进行完善
5.活动管理中报名的所有功能进行实现
6.解决token过期不转登录界面
7.红娘审核完会给该用户发送消息

caojp 3 هفته پیش
والد
کامیت
7b624819fb
35فایلهای تغییر یافته به همراه2126 افزوده شده و 236 حذف شده
  1. 10 1
      marriageAdmin-vue/src/assets/list-common.css
  2. 8 1
      marriageAdmin-vue/src/config/api.js
  3. 2 6
      marriageAdmin-vue/src/layouts/MainLayout.vue
  4. 7 0
      marriageAdmin-vue/src/router/index.js
  5. 36 20
      marriageAdmin-vue/src/utils/request.js
  6. 370 0
      marriageAdmin-vue/src/views/Profile.vue
  7. 34 4
      marriageAdmin-vue/src/views/activity/ActivityForm.vue
  8. 91 8
      marriageAdmin-vue/src/views/activity/ActivityList.vue
  9. 150 33
      marriageAdmin-vue/src/views/activity/ActivityRegistrations.vue
  10. 3 2
      marriageAdmin-vue/src/views/banner/BannerList.vue
  11. 0 83
      marriageAdmin-vue/src/views/success-case/SuccessCaseList.vue
  12. 0 18
      marriageAdmin-vue/src/views/user/UserList.vue
  13. 161 3
      marriageAdmin-vue/src/views/user/UserVipList.vue
  14. 6 0
      service/admin/pom.xml
  15. 0 13
      service/admin/src/main/java/com/zhentao/controller/ActivityController.java
  16. 122 0
      service/admin/src/main/java/com/zhentao/controller/ActivityRegistrationController.java
  17. 154 0
      service/admin/src/main/java/com/zhentao/controller/AdminUserController.java
  18. 72 44
      service/admin/src/main/java/com/zhentao/controller/UserController.java
  19. 146 0
      service/admin/src/main/java/com/zhentao/entity/ActivityRegistration.java
  20. 4 0
      service/admin/src/main/java/com/zhentao/entity/AdminUser.java
  21. 144 0
      service/admin/src/main/java/com/zhentao/entity/HomeFunctionGrid.java
  22. 35 0
      service/admin/src/main/java/com/zhentao/mapper/ActivityRegistrationMapper.java
  23. 18 0
      service/admin/src/main/java/com/zhentao/mapper/HomeFunctionGridMapper.java
  24. 55 0
      service/admin/src/main/java/com/zhentao/service/ActivityRegistrationService.java
  25. 13 0
      service/admin/src/main/java/com/zhentao/service/HomeFunctionGridService.java
  26. 8 0
      service/admin/src/main/java/com/zhentao/service/SystemMessagesService.java
  27. 5 0
      service/admin/src/main/java/com/zhentao/service/UserService.java
  28. 121 0
      service/admin/src/main/java/com/zhentao/service/impl/ActivityRegistrationServiceImpl.java
  29. 22 0
      service/admin/src/main/java/com/zhentao/service/impl/HomeFunctionGridServiceImpl.java
  30. 16 0
      service/admin/src/main/java/com/zhentao/service/impl/MarrApplyServiceImpl.java
  31. 96 0
      service/admin/src/main/java/com/zhentao/service/impl/SystemMessagesServiceImpl.java
  32. 9 0
      service/admin/src/main/java/com/zhentao/service/impl/UserServiceImpl.java
  33. 100 0
      service/admin/src/main/java/com/zhentao/vo/ActivityRegistrationExcelVO.java
  34. 81 0
      service/admin/src/main/resources/com/zhentao/mapper/ActivityRegistrationMapper.xml
  35. 27 0
      service/admin/src/main/resources/com/zhentao/mapper/HomeFunctionGridMapper.xml

+ 10 - 1
marriageAdmin-vue/src/assets/list-common.css

@@ -64,6 +64,8 @@
 .toolbar-card .el-button {
   transition: all var(--transition-base);
   font-weight: var(--font-medium);
+  white-space: nowrap; /* 防止文字换行 */
+  padding: 8px 15px; /* 确保按钮有足够的padding */
 }
 
 .toolbar-card .el-button--primary {
@@ -73,8 +75,15 @@
   color: #ffffff !important;  /* 确保按钮文字为白色 */
 }
 
+.toolbar-card .el-button--primary,
+.toolbar-card .el-button--primary * {
+  color: #ffffff !important;  /* 确保按钮所有文字为白色 */
+}
+
 .toolbar-card .el-button--primary span {
-  color: #ffffff !important;  /* 确保按钮内部文字为白色 */
+  display: inline !important; /* 确保span元素正常显示 */
+  visibility: visible !important; /* 确保文字可见 */
+  opacity: 1 !important; /* 确保文字不透明 */
 }
 
 .toolbar-card .el-button--primary .el-icon {

+ 8 - 1
marriageAdmin-vue/src/config/api.js

@@ -22,6 +22,9 @@ export const API_ENDPOINTS = {
   ADMIN_USER_DISABLE: '/admin/admin-user/disable',
   ADMIN_USER_ENABLE: '/admin/admin-user/enable',
   ADMIN_USER_ROLES: '/admin/admin-user/roles',
+  ADMIN_USER_CURRENT_PROFILE: '/admin/admin-user/current-profile',
+  ADMIN_USER_UPDATE_CURRENT_PROFILE: '/admin/admin-user/current-profile',
+  ADMIN_USER_UPDATE_CURRENT_PASSWORD: '/admin/admin-user/current-password',
   
   // 轮播图管理
   BANNER_LIST: '/admin/banner/list',
@@ -37,7 +40,9 @@ export const API_ENDPOINTS = {
   ACTIVITY_CREATE: '/admin/activity/create',
   ACTIVITY_UPDATE: '/admin/activity/update',
   ACTIVITY_DELETE: '/admin/activity/delete',
-  ACTIVITY_REGISTRATIONS: '/admin/activity/registrations',
+  ACTIVITY_REGISTRATIONS: '/admin/activity/registration/list',
+  ACTIVITY_REGISTRATION_STATS: '/admin/activity/registration/stats',
+  ACTIVITY_REGISTRATION_EXPORT: '/admin/activity/registration/export',
   ACTIVITY_STATS: '/admin/activity/stats',
   
   // 红娘管理
@@ -88,6 +93,8 @@ export const API_ENDPOINTS = {
   USER_UPDATE: '/admin/user/update',
   USER_STATS: '/admin/user/stats',
   USER_VIP_LIST: '/admin/user/vip/list',
+  // VIP 与系统消息
+  USER_VIP_REMIND: '/admin/user/vip/remind',
   
   // 动态管理
   DYNAMIC_LIST: '/admin/dynamic/list',

+ 2 - 6
marriageAdmin-vue/src/layouts/MainLayout.vue

@@ -129,11 +129,10 @@
         <div class="header-right">
           <span class="username">{{ userInfo?.username || '管理员' }}</span>
           <el-dropdown @command="handleCommand">
-            <el-avatar :size="35" src="https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png" />
+            <el-avatar :size="35" :src="userInfo?.avatar || 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'" />
             <template #dropdown>
               <el-dropdown-menu>
                 <el-dropdown-item command="profile">个人资料</el-dropdown-item>
-                <el-dropdown-item command="setting">系统设置</el-dropdown-item>
                 <el-dropdown-item divided command="logout">退出登录</el-dropdown-item>
               </el-dropdown-menu>
             </template>
@@ -193,10 +192,7 @@ const toggleCollapse = () => {
 const handleCommand = async (command) => {
   switch (command) {
     case 'profile':
-      ElMessage.info('个人资料功能开发中')
-      break
-    case 'setting':
-      ElMessage.info('系统设置功能开发中')
+      router.push('/profile')
       break
     case 'logout':
       try {

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

@@ -244,6 +244,13 @@ const router = createRouter({
           name: 'Report',
           component: () => import('@/views/report/ReportList.vue'),
           meta: { title: '举报管理', icon: 'Warning' }
+        },
+        // 个人资料
+        {
+          path: 'profile',
+          name: 'Profile',
+          component: () => import('@/views/Profile.vue'),
+          meta: { title: '个人资料', hidden: true }
         }
       ]
     },

+ 36 - 20
marriageAdmin-vue/src/utils/request.js

@@ -19,6 +19,18 @@ const showErrorMessage = (message) => {
   }
 }
 
+// 清除 token 并跳转登录页(用于服务重启等情况)
+const clearTokenAndRedirect = (message = '服务已重启,请重新登录') => {
+  showErrorMessage(message)
+  localStorage.removeItem('admin_token')
+  localStorage.removeItem('admin_user')
+  // 使用 replace 避免返回时再次触发
+  router.replace('/login').catch(() => {
+    // 如果路由跳转失败,强制刷新页面到登录页
+    window.location.href = '/login'
+  })
+}
+
 // 创建 axios 实例
 const request = axios.create({
   baseURL: API_BASE_URL,
@@ -47,6 +59,11 @@ request.interceptors.request.use(
 // 响应拦截器
 request.interceptors.response.use(
   response => {
+    // 如果是blob响应(文件下载),直接返回
+    if (response.config.responseType === 'blob') {
+      return response.data
+    }
+    
     const res = response.data
     
     // 根据后端返回的结构判断
@@ -54,17 +71,11 @@ request.interceptors.response.use(
       return res
     } else if (res.code === 401) {
       // 未授权,跳转登录
-      showErrorMessage(res.msg || '未授权,请重新登录')
-      localStorage.removeItem('admin_token')
-      localStorage.removeItem('admin_user')
-      router.push('/login')
+      clearTokenAndRedirect(res.msg || '未授权,请重新登录')
       return Promise.reject(new Error(res.msg || '未授权'))
     } else if (res.code === 403) {
       // 拒绝访问,清除token并跳转登录
-      showErrorMessage(res.msg || '拒绝访问,请重新登录')
-      localStorage.removeItem('admin_token')
-      localStorage.removeItem('admin_user')
-      router.push('/login')
+      clearTokenAndRedirect(res.msg || '拒绝访问,请重新登录')
       return Promise.reject(new Error(res.msg || '拒绝访问'))
     } else {
       // 其他错误
@@ -89,31 +100,36 @@ request.interceptors.response.use(
       
       switch (status) {
         case 401:
-          showErrorMessage('未授权,请重新登录')
-          localStorage.removeItem('admin_token')
-          localStorage.removeItem('admin_user')
-          router.push('/login')
+          clearTokenAndRedirect('未授权,请重新登录')
           break
         case 403:
-          showErrorMessage('拒绝访问,请重新登录')
-          localStorage.removeItem('admin_token')
-          localStorage.removeItem('admin_user')
-          router.push('/login')
+          clearTokenAndRedirect('拒绝访问,请重新登录')
           break
         case 404:
           // 只对API请求显示404提示
           showErrorMessage('请求的资源不存在')
           break
         case 500:
-          showErrorMessage('服务器内部错误')
+          // 500错误可能是服务重启,清除token并跳转登录
+          clearTokenAndRedirect('服务器错误,请重新登录')
+          break
+        case 502:
+          // 502 Bad Gateway - 通常表示后端服务不可用(服务重启)
+          clearTokenAndRedirect('服务已重启,请重新登录')
+          break
+        case 503:
+          // 503 Service Unavailable - 服务不可用(服务重启)
+          clearTokenAndRedirect('服务已重启,请重新登录')
           break
         default:
           showErrorMessage(error.response.data?.msg || '请求失败')
       }
     } else if (error.code === 'ECONNABORTED') {
-      showErrorMessage('请求超时,请重试')
-    } else if (error.message === 'Network Error') {
-      showErrorMessage('网络连接失败')
+      // 连接超时,可能是服务重启
+      clearTokenAndRedirect('连接超时,服务可能已重启,请重新登录')
+    } else if (error.message === 'Network Error' || error.code === 'ERR_NETWORK') {
+      // 网络连接失败,可能是服务重启
+      clearTokenAndRedirect('网络连接失败,服务可能已重启,请重新登录')
     }
     
     return Promise.reject(error)

+ 370 - 0
marriageAdmin-vue/src/views/Profile.vue

@@ -0,0 +1,370 @@
+<template>
+  <div class="profile-container">
+    <el-card class="profile-card">
+      <template #header>
+        <div class="card-header">
+          <h2>个人资料</h2>
+        </div>
+      </template>
+      
+      <el-tabs v-model="activeTab" type="border-card">
+        <!-- 基本信息 -->
+        <el-tab-pane label="基本信息" name="basic">
+          <el-form
+            ref="profileFormRef"
+            :model="profileForm"
+            :rules="profileRules"
+            label-width="120px"
+            style="max-width: 600px"
+          >
+            <el-form-item label="头像">
+              <div class="avatar-upload">
+                <el-avatar
+                  :size="100"
+                  :src="profileForm.avatar || defaultAvatar"
+                  class="avatar-preview"
+                >
+                  <el-icon><User /></el-icon>
+                </el-avatar>
+                <el-upload
+                  class="avatar-uploader"
+                  action="#"
+                  :show-file-list="false"
+                  :before-upload="beforeAvatarUpload"
+                  :http-request="handleAvatarUpload"
+                >
+                  <el-button type="primary" size="small">上传头像</el-button>
+                </el-upload>
+                <div class="avatar-tip">支持 JPG、PNG 格式,大小不超过 2MB</div>
+              </div>
+            </el-form-item>
+            
+            <el-form-item label="用户名">
+              <el-input v-model="profileForm.username" disabled />
+            </el-form-item>
+            
+            <el-form-item label="真实姓名" prop="realName">
+              <el-input v-model="profileForm.realName" placeholder="请输入真实姓名" />
+            </el-form-item>
+            
+            <el-form-item label="手机号" prop="phone">
+              <el-input v-model="profileForm.phone" placeholder="请输入手机号" />
+            </el-form-item>
+            
+            <el-form-item label="邮箱" prop="email">
+              <el-input v-model="profileForm.email" placeholder="请输入邮箱" />
+            </el-form-item>
+            
+            <el-form-item>
+              <el-button type="primary" @click="handleSaveProfile" :loading="saving">
+                保存
+              </el-button>
+              <el-button @click="handleReset">重置</el-button>
+            </el-form-item>
+          </el-form>
+        </el-tab-pane>
+        
+        <!-- 修改密码 -->
+        <el-tab-pane label="修改密码" name="password">
+          <el-form
+            ref="passwordFormRef"
+            :model="passwordForm"
+            :rules="passwordRules"
+            label-width="120px"
+            style="max-width: 600px"
+          >
+            <el-form-item label="原密码" prop="oldPassword">
+              <el-input
+                v-model="passwordForm.oldPassword"
+                type="password"
+                placeholder="请输入原密码"
+                show-password
+              />
+            </el-form-item>
+            
+            <el-form-item label="新密码" prop="newPassword">
+              <el-input
+                v-model="passwordForm.newPassword"
+                type="password"
+                placeholder="请输入新密码(至少6位)"
+                show-password
+              />
+            </el-form-item>
+            
+            <el-form-item label="确认新密码" prop="confirmPassword">
+              <el-input
+                v-model="passwordForm.confirmPassword"
+                type="password"
+                placeholder="请再次输入新密码"
+                show-password
+              />
+            </el-form-item>
+            
+            <el-form-item>
+              <el-button type="primary" @click="handleChangePassword" :loading="changingPassword">
+                修改密码
+              </el-button>
+              <el-button @click="handleResetPassword">重置</el-button>
+            </el-form-item>
+          </el-form>
+        </el-tab-pane>
+      </el-tabs>
+    </el-card>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted } from 'vue'
+import { ElMessage } from 'element-plus'
+import { User } from '@element-plus/icons-vue'
+import request from '@/utils/request'
+import { API_ENDPOINTS } from '@/config/api'
+import { useUserStore } from '@/stores/user'
+
+const userStore = useUserStore()
+
+const activeTab = ref('basic')
+const saving = ref(false)
+const changingPassword = ref(false)
+const profileFormRef = ref(null)
+const passwordFormRef = ref(null)
+
+const defaultAvatar = 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'
+
+const profileForm = reactive({
+  id: null,
+  username: '',
+  realName: '',
+  phone: '',
+  email: '',
+  avatar: ''
+})
+
+const passwordForm = reactive({
+  oldPassword: '',
+  newPassword: '',
+  confirmPassword: ''
+})
+
+// 表单验证规则
+const profileRules = {
+  realName: [
+    { max: 50, message: '真实姓名长度不能超过50个字符', trigger: 'blur' }
+  ],
+  phone: [
+    { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
+  ],
+  email: [
+    { type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
+  ]
+}
+
+const passwordRules = {
+  oldPassword: [
+    { required: true, message: '请输入原密码', trigger: 'blur' }
+  ],
+  newPassword: [
+    { required: true, message: '请输入新密码', trigger: 'blur' },
+    { min: 6, message: '密码长度不能少于6位', trigger: 'blur' }
+  ],
+  confirmPassword: [
+    { required: true, message: '请确认新密码', trigger: 'blur' },
+    {
+      validator: (rule, value, callback) => {
+        if (value !== passwordForm.newPassword) {
+          callback(new Error('两次输入的密码不一致'))
+        } else {
+          callback()
+        }
+      },
+      trigger: 'blur'
+    }
+  ]
+}
+
+// 加载个人资料
+const loadProfile = async () => {
+  try {
+    const response = await request.get(API_ENDPOINTS.ADMIN_USER_CURRENT_PROFILE)
+    if (response.code === 200) {
+      const data = response.data
+      profileForm.id = data.id
+      profileForm.username = data.username || ''
+      profileForm.realName = data.realName || ''
+      profileForm.phone = data.phone || ''
+      profileForm.email = data.email || ''
+      profileForm.avatar = data.avatar || ''
+    }
+  } catch (error) {
+    console.error('加载个人资料失败:', error)
+    ElMessage.error('加载个人资料失败')
+  }
+}
+
+// 保存个人资料
+const handleSaveProfile = async () => {
+  try {
+    await profileFormRef.value.validate()
+    
+    saving.value = true
+    const response = await request.put(API_ENDPOINTS.ADMIN_USER_UPDATE_CURRENT_PROFILE, {
+      realName: profileForm.realName,
+      phone: profileForm.phone,
+      email: profileForm.email,
+      avatar: profileForm.avatar
+    })
+    
+    if (response.code === 200) {
+      ElMessage.success('保存成功')
+      // 更新用户信息
+      await userStore.getUserInfo()
+    } else {
+      ElMessage.error(response.message || '保存失败')
+    }
+  } catch (error) {
+    if (error !== false) { // validate 返回 false 时不显示错误
+      console.error('保存个人资料失败:', error)
+      ElMessage.error(error.message || '保存失败')
+    }
+  } finally {
+    saving.value = false
+  }
+}
+
+// 重置表单
+const handleReset = () => {
+  loadProfile()
+}
+
+// 修改密码
+const handleChangePassword = async () => {
+  try {
+    await passwordFormRef.value.validate()
+    
+    changingPassword.value = true
+    const response = await request.put(API_ENDPOINTS.ADMIN_USER_UPDATE_CURRENT_PASSWORD, {
+      oldPassword: passwordForm.oldPassword,
+      newPassword: passwordForm.newPassword
+    })
+    
+    if (response.code === 200) {
+      ElMessage.success('密码修改成功')
+      handleResetPassword()
+    } else {
+      ElMessage.error(response.message || '密码修改失败')
+    }
+  } catch (error) {
+    if (error !== false) { // validate 返回 false 时不显示错误
+      console.error('修改密码失败:', error)
+      ElMessage.error(error.message || '修改密码失败')
+    }
+  } finally {
+    changingPassword.value = false
+  }
+}
+
+// 重置密码表单
+const handleResetPassword = () => {
+  passwordForm.oldPassword = ''
+  passwordForm.newPassword = ''
+  passwordForm.confirmPassword = ''
+  passwordFormRef.value?.clearValidate()
+}
+
+// 头像上传前验证
+const beforeAvatarUpload = (file) => {
+  const isImage = file.type === 'image/jpeg' || file.type === 'image/png' || file.type === 'image/jpg'
+  const isLt2M = file.size / 1024 / 1024 < 2
+
+  if (!isImage) {
+    ElMessage.error('头像图片只能是 JPG/PNG 格式!')
+    return false
+  }
+  if (!isLt2M) {
+    ElMessage.error('头像图片大小不能超过 2MB!')
+    return false
+  }
+  return true
+}
+
+// 头像上传
+const handleAvatarUpload = async (options) => {
+  try {
+    const formData = new FormData()
+    formData.append('file', options.file)
+    
+    const response = await request.post(API_ENDPOINTS.UPLOAD_IMAGE, formData, {
+      headers: {
+        'Content-Type': 'multipart/form-data'
+      }
+    })
+    
+    if (response.code === 200 && response.data) {
+      profileForm.avatar = response.data.url || response.data
+      ElMessage.success('头像上传成功')
+    } else {
+      ElMessage.error(response.message || '头像上传失败')
+    }
+  } catch (error) {
+    console.error('头像上传失败:', error)
+    ElMessage.error('头像上传失败')
+  }
+}
+
+onMounted(() => {
+  loadProfile()
+})
+</script>
+
+<style scoped>
+.profile-container {
+  padding: 20px;
+}
+
+.profile-card {
+  max-width: 900px;
+  margin: 0 auto;
+}
+
+.card-header h2 {
+  margin: 0;
+  font-size: 20px;
+  font-weight: 600;
+  color: #303133;
+}
+
+.avatar-upload {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+  gap: 12px;
+}
+
+.avatar-preview {
+  border: 2px solid #dcdfe6;
+  cursor: pointer;
+}
+
+.avatar-uploader {
+  margin-top: 8px;
+}
+
+.avatar-tip {
+  font-size: 12px;
+  color: #909399;
+  margin-top: -4px;
+}
+
+:deep(.el-tabs__content) {
+  padding: 24px;
+}
+
+:deep(.el-form-item__label) {
+  font-weight: 500;
+}
+
+:deep(.el-input__inner) {
+  max-width: 400px;
+}
+</style>
+

+ 34 - 4
marriageAdmin-vue/src/views/activity/ActivityForm.vue

@@ -163,7 +163,7 @@ import { useRoute, useRouter } from 'vue-router'
 import { ElMessage } from 'element-plus'
 import { Plus } from '@element-plus/icons-vue'
 import request from '@/utils/request'
-import { API_ENDPOINTS } from '@/config/api'
+import { API_ENDPOINTS, API_BASE_URL } from '@/config/api'
 
 const route = useRoute()
 const router = useRouter()
@@ -310,15 +310,45 @@ const handleUpload = async (options) => {
   }
 }
 
+// 规范化图片地址,兼容相对路径和缺少协议的情况
+const resolveImageUrl = (url) => {
+  if (!url || typeof url !== 'string') return ''
+  const normalized = url.trim()
+  if (/^https?:\/\//i.test(normalized)) return normalized
+  if (normalized.startsWith('//')) return `${window.location.protocol}${normalized}`
+  const base = API_BASE_URL || window.location.origin
+  if (normalized.startsWith('/')) return `${base}${normalized}`
+  return `${base}/${normalized}`
+}
+
 // 加载活动详情
 const loadActivityDetail = async (id) => {
   try {
     const response = await request.get(`${API_ENDPOINTS.ACTIVITY_DETAIL}/${id}`)
     
     if (response.code === 200) {
-      Object.assign(activityForm, response.data)
-      if (response.data.startTime && response.data.endTime) {
-        timeRange.value = [response.data.startTime, response.data.endTime]
+      const data = response.data
+      // 映射字段,兼容不同的命名方式
+      Object.assign(activityForm, {
+        id: data.id,
+        name: data.name || '',
+        type: data.type || 2,
+        category: data.category || '',
+        maxParticipants: data.maxParticipants || data.max_participants || 50,
+        location: data.location || '',
+        startTime: data.startTime || data.start_time || null,
+        endTime: data.endTime || data.end_time || null,
+        coverImage: resolveImageUrl(data.coverImage || data.cover_image || data.cover || ''),
+        price: data.price || 0,
+        status: data.status || 1,
+        registrationEndTime: data.registrationEndTime || data.registration_end_time || null,
+        description: data.description || '',
+        notes: data.notes || '',
+        isHot: data.isHot === true || data.isHot === 1 || data.isHot === '1' || data.is_hot === true || data.is_hot === 1
+      })
+      
+      if (activityForm.startTime && activityForm.endTime) {
+        timeRange.value = [activityForm.startTime, activityForm.endTime]
       }
       
       // 等待 DOM 更新后清除表单验证错误

+ 91 - 8
marriageAdmin-vue/src/views/activity/ActivityList.vue

@@ -138,15 +138,34 @@
         <el-table-column prop="isHot" label="热门" width="80" align="center">
           <template #default="{ row }">
             <el-switch
-              v-model="row.isHot"
+              :model-value="Boolean(row.isHot)"
               active-color="var(--danger-color)"
               inactive-color="var(--border-dark)"
-              @change="handleHotChange(row)"
+              @change="(val) => handleHotChange(row, val)"
             />
           </template>
         </el-table-column>
         
-        <el-table-column prop="startTime" label="开始时间" width="180" />
+        <el-table-column prop="startTime" label="开始时间" width="180">
+          <template #default="{ row }">
+            <span v-if="row.startTime">{{ formatDateTime(row.startTime) }}</span>
+            <span v-else class="data-meta">-</span>
+          </template>
+        </el-table-column>
+        
+        <el-table-column prop="endTime" label="结束时间" width="180">
+          <template #default="{ row }">
+            <span v-if="row.endTime">{{ formatDateTime(row.endTime) }}</span>
+            <span v-else class="data-meta">-</span>
+          </template>
+        </el-table-column>
+        
+        <el-table-column prop="registrationEndTime" label="报名截止时间" width="180">
+          <template #default="{ row }">
+            <span v-if="row.registrationEndTime">{{ formatDateTime(row.registrationEndTime) }}</span>
+            <span v-else class="data-meta">-</span>
+          </template>
+        </el-table-column>
         
         <el-table-column label="操作" width="240" fixed="right">
           <template #default="{ row }">
@@ -255,7 +274,25 @@ const loadActivityList = async () => {
     })
     
     if (response.code === 200) {
-      activityList.value = response.data.list || response.data || []
+      const list = response.data.list || response.data || []
+      // 字段兼容与转换:确保 isHot 为布尔,并兼容不同时间字段命名
+      activityList.value = list.map(item => ({
+        ...item,
+        isHot:
+          item.isHot === true ||
+          item.isHot === 1 ||
+          item.isHot === '1' ||
+          item.is_hot === true ||
+          item.is_hot === 1,
+        // 兼容下划线命名与不同字段名
+        startTime: item.startTime || item.start_time || item.start_at || '',
+        endTime: item.endTime || item.end_time || item.end_at || '',
+        registrationEndTime:
+          item.registrationEndTime ||
+          item.registration_end_time ||
+          item.signup_end_time ||
+          ''
+      }))
       total.value = response.data.total || activityList.value.length
     }
   } catch (error) {
@@ -283,21 +320,67 @@ const handleViewRegistrations = (row) => {
   router.push(`/activity/registrations/${row.id}`)
 }
 
+// 格式化日期时间
+const formatDateTime = (dateTime) => {
+  if (!dateTime) return '-'
+  // 如果是字符串,兼容 '2025-01-01T12:00:00' 或 '2025-01-01 12:00:00'
+  if (typeof dateTime === 'string') {
+    const normalized = dateTime.replace('T', ' ')
+    return normalized.substring(0, 19)
+  }
+  // 如果是时间戳(秒或毫秒),转换为日期字符串
+  if (typeof dateTime === 'number') {
+    const ts = dateTime > 1e12 ? dateTime : dateTime * 1000
+    const d = new Date(ts)
+    const pad = (n) => (n < 10 ? `0${n}` : n)
+    const Y = d.getFullYear()
+    const M = pad(d.getMonth() + 1)
+    const D = pad(d.getDate())
+    const h = pad(d.getHours())
+    const m = pad(d.getMinutes())
+    const s = pad(d.getSeconds())
+    return `${Y}-${M}-${D} ${h}:${m}:${s}`
+  }
+  // 其它类型(如 Date 对象)直接格式化
+  if (dateTime instanceof Date) {
+    const d = dateTime
+    const pad = (n) => (n < 10 ? `0${n}` : n)
+    const Y = d.getFullYear()
+    const M = pad(d.getMonth() + 1)
+    const D = pad(d.getDate())
+    const h = pad(d.getHours())
+    const m = pad(d.getMinutes())
+    const s = pad(d.getSeconds())
+    return `${Y}-${M}-${D} ${h}:${m}:${s}`
+  }
+  return String(dateTime)
+}
+
 // 切换热门状态
-const handleHotChange = async (row) => {
+const handleHotChange = async (row, newValue) => {
+  const oldValue = row.isHot
+  // 立即更新UI
+  row.isHot = newValue
+  
   try {
     const response = await request.put(`${API_ENDPOINTS.ACTIVITY_UPDATE}/${row.id}`, {
-      isHot: row.isHot
+      isHot: newValue
     })
     
     if (response.code === 200) {
       ElMessage.success('状态更新成功')
+      // 重新加载列表以确保数据同步
+      loadActivityList()
     } else {
-      row.isHot = !row.isHot
+      // 恢复原值
+      row.isHot = oldValue
+      ElMessage.error(response.message || '状态更新失败')
     }
   } catch (error) {
     console.error('状态更新失败:', error)
-    row.isHot = !row.isHot
+    // 恢复原值
+    row.isHot = oldValue
+    ElMessage.error('状态更新失败,请重试')
   }
 }
 

+ 150 - 33
marriageAdmin-vue/src/views/activity/ActivityRegistrations.vue

@@ -34,17 +34,49 @@
       </el-col>
     </el-row>
     
-    <!-- 报名列表 -->
-    <el-card shadow="never" class="table-card">
-      <template #header>
-        <el-row justify="space-between">
-          <span>报名列表</span>
-          <el-space>
-            <el-button type="success" size="small" icon="Download" @click="handleExport">
+    <!-- 搜索栏 -->
+    <el-card shadow="never" class="toolbar-card">
+      <el-row :gutter="20">
+        <el-col :span="24">
+          <el-space wrap>
+            <el-select v-model="filters.checkInStatus" placeholder="签到状态" clearable style="width: 120px" @change="loadRegistrationList">
+              <el-option label="全部" :value="undefined" />
+              <el-option label="已报名" :value="1" />
+              <el-option label="已取消" :value="2" />
+              <el-option label="已签到" :value="3" />
+            </el-select>
+            
+            <el-select v-model="filters.paymentStatus" placeholder="支付状态" clearable style="width: 120px" @change="loadRegistrationList">
+              <el-option label="全部" :value="undefined" />
+              <el-option label="未支付" :value="0" />
+              <el-option label="已支付" :value="1" />
+            </el-select>
+            
+            <el-input
+              v-model="filters.keyword"
+              placeholder="搜索用户名或手机号"
+              clearable
+              style="width: 200px"
+              @clear="loadRegistrationList"
+              @keyup.enter="loadRegistrationList"
+            >
+              <template #append>
+                <el-button icon="Search" @click="loadRegistrationList" />
+              </template>
+            </el-input>
+            
+            <el-button type="success" icon="Download" @click="handleExport">
               导出数据
             </el-button>
           </el-space>
-        </el-row>
+        </el-col>
+      </el-row>
+    </el-card>
+    
+    <!-- 报名列表 -->
+    <el-card shadow="never" class="table-card">
+      <template #header>
+        <span>报名列表</span>
       </template>
       
       <el-table
@@ -69,30 +101,26 @@
         
         <el-table-column prop="registrationTime" label="报名时间" width="180" />
         
-        <el-table-column prop="payStatus" label="支付状态" width="100">
+        <el-table-column prop="paymentStatus" label="支付状态" width="100">
           <template #default="{ row }">
-            <el-tag :type="row.payStatus === 1 ? 'success' : 'warning'" size="small">
-              {{ row.payStatus === 1 ? '已支付' : '未支付' }}
+            <el-tag :type="row.paymentStatus === 1 ? 'success' : 'warning'" size="small">
+              {{ row.paymentStatus === 1 ? '已支付' : '未支付' }}
             </el-tag>
           </template>
         </el-table-column>
         
-        <el-table-column prop="checkInStatus" label="签到状态" width="100">
+        <el-table-column prop="status" label="签到状态" width="100">
           <template #default="{ row }">
-            <el-tag :type="row.checkInStatus === 1 ? 'success' : 'info'" size="small">
-              {{ row.checkInStatus === 1 ? '已签到' : '未签到' }}
+            <el-tag :type="getCheckInStatusType(row.status)" size="small">
+              {{ getCheckInStatusText(row.status) }}
             </el-tag>
           </template>
         </el-table-column>
         
-        <el-table-column prop="checkInTime" label="签到时间" width="180" />
-        
-        <el-table-column prop="remark" label="备注" min-width="150" show-overflow-tooltip />
-        
         <el-table-column label="操作" width="150" fixed="right">
           <template #default="{ row }">
             <el-button
-              v-if="row.checkInStatus !== 1"
+              v-if="row.status !== 3"
               type="primary"
               size="small"
               link
@@ -101,6 +129,7 @@
               签到
             </el-button>
             <el-button
+              v-if="row.status !== 2"
               type="danger"
               size="small"
               link
@@ -133,10 +162,10 @@ import { ref, reactive, onMounted } from 'vue'
 import { useRoute } from 'vue-router'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import request from '@/utils/request'
-import { API_ENDPOINTS } from '@/config/api'
+import { API_BASE_URL, API_ENDPOINTS } from '@/config/api'
 
 const route = useRoute()
-const activityId = ref(route.params.id)
+const activityId = ref(parseInt(route.params.id))
 const activityName = ref('')
 const loading = ref(false)
 const currentPage = ref(1)
@@ -144,32 +173,59 @@ const pageSize = ref(10)
 const total = ref(0)
 const registrationList = ref([])
 
+const filters = reactive({
+  checkInStatus: undefined,
+  paymentStatus: undefined,
+  keyword: ''
+})
+
 const stats = reactive({
   totalCount: 0,
   checkedInCount: 0,
   notCheckedInCount: 0
 })
 
+// 加载统计数据
+const loadStats = async () => {
+  try {
+    const response = await request.get(API_ENDPOINTS.ACTIVITY_REGISTRATION_STATS, {
+      params: {
+        activityId: activityId.value
+      }
+    })
+    
+    if (response.code === 200) {
+      stats.totalCount = response.data.totalCount || 0
+      stats.checkedInCount = response.data.checkedInCount || 0
+      stats.notCheckedInCount = response.data.notCheckedInCount || 0
+    }
+  } catch (error) {
+    console.error('加载统计数据失败:', error)
+  }
+}
+
 // 加载报名列表
 const loadRegistrationList = async () => {
   loading.value = true
   try {
-    const response = await request.get(`${API_ENDPOINTS.ACTIVITY_REGISTRATIONS}/${activityId.value}`, {
+    const response = await request.get(API_ENDPOINTS.ACTIVITY_REGISTRATIONS, {
       params: {
         page: currentPage.value,
-        pageSize: pageSize.value
+        pageSize: pageSize.value,
+        activityId: activityId.value,
+        checkInStatus: filters.checkInStatus,
+        paymentStatus: filters.paymentStatus,
+        keyword: filters.keyword || undefined
       }
     })
     
     if (response.code === 200) {
       registrationList.value = response.data.list || []
-      total.value = response.data.total || registrationList.value.length
+      total.value = response.data.total || 0
       activityName.value = response.data.activityName || ''
       
-      // 计算统计数据
-      stats.totalCount = total.value
-      stats.checkedInCount = registrationList.value.filter(r => r.checkInStatus === 1).length
-      stats.notCheckedInCount = stats.totalCount - stats.checkedInCount
+      // 加载统计数据
+      await loadStats()
     }
   } catch (error) {
     console.error('加载报名列表失败:', error)
@@ -179,6 +235,18 @@ const loadRegistrationList = async () => {
   }
 }
 
+// 获取签到状态文本
+const getCheckInStatusText = (status) => {
+  const texts = { 1: '已报名', 2: '已取消', 3: '已签到' }
+  return texts[status] || '未知'
+}
+
+// 获取签到状态类型
+const getCheckInStatusType = (status) => {
+  const types = { 1: 'info', 2: 'danger', 3: 'success' }
+  return types[status] || ''
+}
+
 // 签到
 const handleCheckIn = async (row) => {
   try {
@@ -189,7 +257,9 @@ const handleCheckIn = async (row) => {
     
     if (response.code === 200) {
       ElMessage.success('签到成功')
-      loadRegistrationList()
+      // 重新加载列表和统计数据
+      await loadRegistrationList()
+      await loadStats()
     }
   } catch (error) {
     console.error('签到失败:', error)
@@ -214,7 +284,9 @@ const handleCancelRegistration = async (row) => {
     
     if (response.code === 200) {
       ElMessage.success('取消报名成功')
-      loadRegistrationList()
+      // 重新加载列表和统计数据
+      await loadRegistrationList()
+      await loadStats()
     }
   } catch (error) {
     if (error !== 'cancel') {
@@ -224,9 +296,50 @@ const handleCancelRegistration = async (row) => {
 }
 
 // 导出数据
-const handleExport = () => {
-  ElMessage.info('导出功能开发中')
-  // TODO: 实现导出功能
+const handleExport = async () => {
+  try {
+    loading.value = true
+    const params = {
+      activityId: activityId.value,
+      checkInStatus: filters.checkInStatus,
+      paymentStatus: filters.paymentStatus,
+      keyword: filters.keyword || undefined
+    }
+    
+    // 使用axios下载文件
+    const blob = await request.get(API_ENDPOINTS.ACTIVITY_REGISTRATION_EXPORT, {
+      params: params,
+      responseType: 'blob'
+    })
+    
+    // 创建下载链接
+    const url = window.URL.createObjectURL(blob)
+    const link = document.createElement('a')
+    link.href = url
+    link.download = `活动报名列表_${new Date().getTime()}.xlsx`
+    document.body.appendChild(link)
+    link.click()
+    document.body.removeChild(link)
+    window.URL.revokeObjectURL(url)
+    
+    ElMessage.success('导出成功')
+  } catch (error) {
+    console.error('导出失败:', error)
+    // 如果是blob响应但包含错误信息,尝试解析
+    if (error.response && error.response.data instanceof Blob) {
+      const text = await error.response.data.text()
+      try {
+        const errorData = JSON.parse(text)
+        ElMessage.error('导出失败: ' + (errorData.msg || '未知错误'))
+      } catch {
+        ElMessage.error('导出失败')
+      }
+    } else {
+      ElMessage.error('导出失败: ' + (error.message || '未知错误'))
+    }
+  } finally {
+    loading.value = false
+  }
 }
 
 onMounted(() => {
@@ -275,6 +388,10 @@ onMounted(() => {
   color: #e6a23c;
 }
 
+.toolbar-card {
+  margin-top: 20px;
+}
+
 .table-card {
   margin-top: 20px;
 }

+ 3 - 2
marriageAdmin-vue/src/views/banner/BannerList.vue

@@ -7,8 +7,9 @@
       <el-row :gutter="20">
         <el-col :span="18">
           <el-space wrap>
-            <el-button type="primary" icon="Plus" @click="handleCreate">
-              新增轮播图
+            <el-button type="primary" @click="handleCreate">
+              <el-icon><Plus /></el-icon>
+              <span>新增轮播图</span>
             </el-button>
             <el-button icon="Refresh" @click="loadBannerList">刷新</el-button>
           </el-space>

+ 0 - 83
marriageAdmin-vue/src/views/success-case/SuccessCaseList.vue

@@ -75,33 +75,6 @@
           </el-descriptions-item>
         </el-descriptions>
         
-        <!-- 案例图片 -->
-        <div v-if="detailData.images && detailData.images.length > 0" class="images-section">
-          <h3 class="section-title">案例图片</h3>
-          <div class="images-grid">
-            <div v-for="(img, index) in detailData.images" :key="img.imageId" class="image-item">
-              <el-image
-                :src="img.imageUrl"
-                fit="cover"
-                class="detail-image"
-                :preview-src-list="detailData.images.map(i => i.imageUrl)"
-                :initial-index="index"
-                preview-teleported
-                hide-on-click-modal
-              >
-                <template #error>
-                  <div class="image-error">
-                    <el-icon><Picture /></el-icon>
-                    <span>加载失败</span>
-                  </div>
-                </template>
-              </el-image>
-              <div v-if="img.imageDesc" class="image-desc">{{ img.imageDesc }}</div>
-            </div>
-          </div>
-        </div>
-        <el-empty v-else description="暂无图片" />
-        
         <!-- 时间线 -->
         <div v-if="detailData.timeline && detailData.timeline.length > 0" class="timeline-section">
           <h3 class="section-title">案例时间线</h3>
@@ -133,7 +106,6 @@
 import { ref, onMounted, computed } from 'vue'
 import { useRouter } from 'vue-router'
 import { ElMessage, ElMessageBox } from 'element-plus'
-import { Picture } from '@element-plus/icons-vue'
 import request from '@/utils/request'
 import { API_ENDPOINTS } from '@/config/api'
 
@@ -361,61 +333,6 @@ onMounted(() => {
   border-bottom: 2px solid #409eff;
 }
 
-/* 图片网格 */
-.images-section {
-  margin-top: 20px;
-}
-
-.images-grid {
-  display: grid;
-  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
-  gap: 15px;
-  margin-top: 15px;
-}
-
-.image-item {
-  position: relative;
-  border-radius: 8px;
-  overflow: hidden;
-  border: 1px solid #eee;
-  transition: all 0.3s;
-}
-
-.image-item:hover {
-  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
-  transform: translateY(-2px);
-}
-
-.detail-image {
-  width: 100%;
-  height: 200px;
-  cursor: pointer;
-}
-
-.image-desc {
-  padding: 8px;
-  background-color: #f5f7fa;
-  font-size: 13px;
-  color: #666;
-  text-align: center;
-  border-top: 1px solid #eee;
-}
-
-.image-error {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  height: 200px;
-  background-color: #f5f7fa;
-  color: #909399;
-}
-
-.image-error .el-icon {
-  font-size: 40px;
-  margin-bottom: 8px;
-}
-
 /* 时间线样式 */
 .timeline-section {
   margin-top: 20px;

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

@@ -90,15 +90,6 @@
             </div>
           </template>
         </el-table-column>
-        <el-table-column prop="matchmakerName" label="绑定红娘" min-width="120">
-          <template #default="{ row }">
-            <div v-if="row.matchmakerName">
-              <div>{{ row.matchmakerName }}</div>
-              <div style="font-size: 12px; color: #999;">{{ row.matchmakerPhone }}</div>
-            </div>
-            <span v-else style="color: #999;">-</span>
-          </template>
-        </el-table-column>
         <el-table-column prop="isProfileComplete" label="资料完整" width="100" align="center">
           <template #default="{ row }">
             <el-tag 
@@ -221,15 +212,6 @@
         <el-descriptions-item label="VIP结束时间" :span="2" v-if="currentUser.isVip">
           {{ currentUser.vipEndTime || '-' }}
         </el-descriptions-item>
-        <el-descriptions-item label="绑定红娘" :span="2">
-          <div v-if="currentUser.matchmakerName">
-            {{ currentUser.matchmakerName }} ({{ currentUser.matchmakerPhone }})
-            <div style="font-size: 12px; color: #999; margin-top: 5px;">
-              绑定时间:{{ currentUser.boundAt }}
-            </div>
-          </div>
-          <span v-else>-</span>
-        </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">

+ 161 - 3
marriageAdmin-vue/src/views/user/UserVipList.vue

@@ -3,7 +3,32 @@
     <h2 class="page-title">VIP用户管理</h2>
     
     <el-card shadow="never" class="table-card">
-      <el-table v-loading="loading" :data="list" stripe>
+      <div style="margin-bottom: 10px; display: flex; justify-content: space-between; align-items: center;">
+        <div>
+          <el-button 
+            type="warning" 
+            size="small" 
+            :disabled="multipleSelection.length === 0"
+            @click="handleBatchRemind"
+          >
+            批量提醒
+          </el-button>
+          <span v-if="multipleSelection.length" style="margin-left: 10px; color: #909399;">
+            已选 {{ multipleSelection.length }} 条
+          </span>
+        </div>
+      </div>
+      <el-table
+        v-loading="loading"
+        :data="list"
+        stripe
+        @selection-change="handleSelectionChange"
+      >
+        <el-table-column
+          type="selection"
+          width="55"
+          :selectable="isRowSelectable"
+        />
         <el-table-column type="index" label="序号" width="60" />
         <el-table-column prop="nickname" label="昵称" width="120" />
         <el-table-column prop="vipLevel" label="VIP等级" width="100">
@@ -30,9 +55,10 @@
             </span>
           </template>
         </el-table-column>
-        <el-table-column label="操作" width="150">
+        <el-table-column label="操作" width="220">
           <template #default="{ row }">
             <el-button type="primary" size="small" link @click="handleRenew(row)">续费</el-button>
+            <el-button type="warning" size="small" link @click="handleRemind(row)">提醒</el-button>
           </template>
         </el-table-column>
       </el-table>
@@ -53,7 +79,7 @@
 
 <script setup>
 import { ref, onMounted } from 'vue'
-import { ElMessage } from 'element-plus'
+import { ElMessage, ElMessageBox } from 'element-plus'
 import request from '@/utils/request'
 import { API_ENDPOINTS } from '@/config/api'
 
@@ -62,6 +88,18 @@ const currentPage = ref(1)
 const pageSize = ref(10)
 const total = ref(0)
 const list = ref([])
+const multipleSelection = ref([])
+
+// 仅允许勾选剩余天数 >= 2 天的用户
+const isRowSelectable = (row) => {
+  const days =
+    row.remainingDays !== undefined
+      ? row.remainingDays
+      : row.remaining_days !== undefined
+        ? row.remaining_days
+        : 0
+  return days >= 2
+}
 
 const loadList = async () => {
   loading.value = true
@@ -97,6 +135,126 @@ const handleRenew = (row) => {
   ElMessage.info('续费功能开发中')
 }
 
+const handleSelectionChange = (val) => {
+  multipleSelection.value = (val || []).filter(isRowSelectable)
+}
+
+// 发送系统提醒
+const handleRemind = async (row) => {
+  const userId = row.userId || row.user_id
+  if (!userId) {
+    ElMessage.error('缺少用户ID,无法发送提醒')
+    return
+  }
+
+  const days =
+    row.remainingDays !== undefined
+      ? row.remainingDays
+      : row.remaining_days !== undefined
+        ? row.remaining_days
+        : 0
+
+  let content = ''
+  if (days <= 0) {
+    content = '尊敬的用户,您的会员已经到期,请及时充值'
+  } else if (days <= 2) {
+    content = '尊敬的用户,您的会员不足2天,请及时充值'
+  } else {
+    ElMessage.info('该用户会员剩余时间大于2天,无需提醒')
+    return
+  }
+
+  try {
+    const res = await request.post(API_ENDPOINTS.USER_VIP_REMIND, {
+      userId,
+      content
+    })
+    if (res.code === 200) {
+      ElMessage.success('提醒发送成功')
+    } else {
+      ElMessage.error(res.message || '提醒发送失败')
+    }
+  } catch (error) {
+    console.error('发送提醒失败:', error)
+    ElMessage.error('发送提醒失败,请稍后重试')
+  }
+}
+
+// 批量提醒
+const handleBatchRemind = async () => {
+  if (!multipleSelection.value.length) {
+    ElMessage.warning('请先选择需要提醒的用户')
+    return
+  }
+
+  try {
+    await ElMessageBox.confirm(
+      `确定要对选中的 ${multipleSelection.value.length} 位用户发送会员到期提醒吗?`,
+      '批量提醒',
+      {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }
+    )
+  } catch {
+    return
+  }
+
+  let successCount = 0
+  let skipCount = 0
+  let failCount = 0
+
+  for (const row of multipleSelection.value) {
+    const userId = row.userId || row.user_id
+    if (!userId) {
+      failCount++
+      continue
+    }
+
+    const days =
+      row.remainingDays !== undefined
+        ? row.remainingDays
+        : row.remaining_days !== undefined
+          ? row.remaining_days
+          : 0
+
+    let content = ''
+    if (days <= 0) {
+      content = '尊敬的用户,您的会员已经到期,请及时充值'
+    } else if (days <= 2) {
+      content = '尊敬的用户,您的会员不足2天,请及时充值'
+    } else {
+      skipCount++
+      continue
+    }
+
+    try {
+      const res = await request.post(API_ENDPOINTS.USER_VIP_REMIND, {
+        userId,
+        content
+      })
+      if (res.code === 200) {
+        successCount++
+      } else {
+        failCount++
+      }
+    } catch {
+      failCount++
+    }
+  }
+
+  let msg = `批量提醒完成:成功 ${successCount} 条`
+  if (skipCount) msg += `,已跳过(剩余时间>2天)${skipCount} 条`
+  if (failCount) msg += `,失败 ${failCount} 条`
+
+  if (failCount) {
+    ElMessage.warning(msg)
+  } else {
+    ElMessage.success(msg)
+  }
+}
+
 onMounted(() => loadList())
 </script>
 

+ 6 - 0
service/admin/pom.xml

@@ -86,5 +86,11 @@
             <artifactId>java-jwt</artifactId>
             <version>4.4.0</version>
         </dependency>
+        <!-- EasyExcel -->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>easyexcel</artifactId>
+            <version>3.3.2</version>
+        </dependency>
     </dependencies>
 </project>

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

@@ -112,19 +112,6 @@ public class ActivityController {
         }
     }
     
-    /**
-     * 获取活动报名列表
-     */
-    @GetMapping("/registrations/{id}")
-    public Result<?> getRegistrations(@PathVariable Integer id) {
-        try {
-            // TODO: 实现获取报名列表的逻辑
-            return Result.success("功能开发中");
-        } catch (Exception e) {
-            e.printStackTrace();
-            return Result.error("获取报名列表失败:" + e.getMessage());
-        }
-    }
     
     /**
      * 获取活动统计

+ 122 - 0
service/admin/src/main/java/com/zhentao/controller/ActivityRegistrationController.java

@@ -0,0 +1,122 @@
+package com.zhentao.controller;
+
+import com.alibaba.excel.EasyExcel;
+import com.zhentao.common.Result;
+import com.zhentao.entity.Activity;
+import com.zhentao.entity.ActivityRegistration;
+import com.zhentao.mapper.ActivityMapper;
+import com.zhentao.service.ActivityRegistrationService;
+import com.zhentao.vo.ActivityRegistrationExcelVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * 活动报名管理控制器
+ */
+@RestController
+@RequestMapping("/admin/activity/registration")
+@CrossOrigin(origins = "*")
+public class ActivityRegistrationController {
+    
+    @Autowired
+    private ActivityRegistrationService activityRegistrationService;
+    
+    @Autowired
+    private ActivityMapper activityMapper;
+    
+    /**
+     * 获取报名列表(分页)
+     */
+    @GetMapping("/list")
+    public Result<Map<String, Object>> getRegistrationList(
+            @RequestParam(value = "page", defaultValue = "1") Integer page,
+            @RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize,
+            @RequestParam(value = "activityId", required = false) Integer activityId,
+            @RequestParam(value = "checkInStatus", required = false) Integer checkInStatus,
+            @RequestParam(value = "paymentStatus", required = false) Integer paymentStatus,
+            @RequestParam(value = "keyword", required = false) String keyword) {
+        try {
+            Map<String, Object> data = activityRegistrationService.getRegistrationList(
+                    page, pageSize, activityId, checkInStatus, paymentStatus, keyword);
+            
+            // 如果有活动ID,获取活动名称
+            if (activityId != null) {
+                Activity activity = activityMapper.selectById(activityId);
+                if (activity != null) {
+                    data.put("activityName", activity.getName());
+                }
+            }
+            
+            return Result.success(data);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("获取报名列表失败:" + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取报名统计数据
+     * 报名总数:签到状态为已报名(status=1)的记录数
+     * 已签到:签到状态为已签到(status=3)的记录数(从已报名中筛选)
+     * 未签到:签到状态为已报名(status=1)但未签到(status≠3)的记录数
+     */
+    @GetMapping("/stats")
+    public Result<Map<String, Object>> getRegistrationStats(
+            @RequestParam(value = "activityId", required = true) Integer activityId) {
+        try {
+            Map<String, Object> stats = activityRegistrationService.getRegistrationStats(activityId);
+            return Result.success(stats);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("获取统计数据失败:" + e.getMessage());
+        }
+    }
+    
+    /**
+     * 导出报名数据为Excel
+     */
+    @GetMapping("/export")
+    public void exportRegistrationList(
+            HttpServletResponse response,
+            @RequestParam(value = "activityId", required = false) Integer activityId,
+            @RequestParam(value = "checkInStatus", required = false) Integer checkInStatus,
+            @RequestParam(value = "paymentStatus", required = false) Integer paymentStatus,
+            @RequestParam(value = "keyword", required = false) String keyword) throws IOException {
+        try {
+            // 查询所有符合条件的记录
+            List<ActivityRegistration> registrations = activityRegistrationService.getAllRegistrations(
+                    activityId, checkInStatus, paymentStatus, keyword);
+            
+            // 转换为Excel VO
+            List<ActivityRegistrationExcelVO> excelData = registrations.stream()
+                    .map(ActivityRegistrationExcelVO::fromEntity)
+                    .collect(Collectors.toList());
+            
+            // 设置响应头
+            String fileName = "活动报名列表_" + System.currentTimeMillis() + ".xlsx";
+            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+            response.setCharacterEncoding("utf-8");
+            response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + URLEncoder.encode(fileName, "UTF-8"));
+            
+            // 写入Excel
+            EasyExcel.write(response.getOutputStream(), ActivityRegistrationExcelVO.class)
+                    .sheet("报名列表")
+                    .doWrite(excelData);
+            
+        } catch (Exception e) {
+            e.printStackTrace();
+            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+            response.setContentType("application/json;charset=UTF-8");
+            response.getWriter().write("{\"code\":500,\"msg\":\"导出失败:" + e.getMessage() + "\"}");
+        }
+    }
+}
+

+ 154 - 0
service/admin/src/main/java/com/zhentao/controller/AdminUserController.java

@@ -3,12 +3,15 @@ package com.zhentao.controller;
 import com.zhentao.common.Result;
 import com.zhentao.entity.AdminUser;
 import com.zhentao.entity.Role;
+import com.zhentao.mapper.AdminUserMapper;
 import com.zhentao.service.AuthService;
 import com.zhentao.service.RoleService;
 import com.zhentao.service.UserService;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.util.DigestUtils;
 import org.springframework.web.bind.annotation.*;
 
+import java.nio.charset.StandardCharsets;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -31,6 +34,9 @@ public class AdminUserController {
     @Autowired
     private RoleService roleService;
     
+    @Autowired
+    private AdminUserMapper adminUserMapper;
+    
     /**
      * 注册新管理员(仅超级管理员可用)
      */
@@ -386,5 +392,153 @@ public class AdminUserController {
             return Result.error("获取角色列表失败:" + e.getMessage());
         }
     }
+    
+    /**
+     * 获取当前登录用户的个人资料
+     */
+    @GetMapping("/current-profile")
+    public Result<Map<String, Object>> getCurrentProfile(@RequestHeader(value = "Authorization", required = false) String token) {
+        try {
+            // 验证token
+            if (token == null || token.isEmpty()) {
+                return Result.error(401, "未授权,请先登录");
+            }
+            
+            AdminUser currentUser = authService.getUserByToken(token);
+            if (currentUser == null) {
+                return Result.error(401, "Token无效或已过期,请重新登录");
+            }
+            
+            // 获取完整用户信息(不包括密码)
+            AdminUser user = userService.getUserById(currentUser.getId());
+            if (user == null) {
+                return Result.error("用户不存在");
+            }
+            
+            Map<String, Object> data = new HashMap<>();
+            data.put("id", user.getId());
+            data.put("username", user.getUsername());
+            data.put("realName", user.getRealName());
+            data.put("phone", user.getPhone());
+            data.put("email", user.getEmail());
+            data.put("avatar", user.getAvatar());
+            data.put("status", user.getStatus());
+            data.put("createTime", user.getCreateTime());
+            data.put("updateTime", user.getUpdateTime());
+            data.put("lastLoginTime", user.getLastLoginTime());
+            
+            return Result.success(data);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("获取个人资料失败:" + e.getMessage());
+        }
+    }
+    
+    /**
+     * 更新当前登录用户的个人资料
+     */
+    @PutMapping("/current-profile")
+    public Result<String> updateCurrentProfile(@RequestBody Map<String, Object> params,
+                                               @RequestHeader(value = "Authorization", required = false) String token) {
+        try {
+            // 验证token
+            if (token == null || token.isEmpty()) {
+                return Result.error(401, "未授权,请先登录");
+            }
+            
+            AdminUser currentUser = authService.getUserByToken(token);
+            if (currentUser == null) {
+                return Result.error(401, "Token无效或已过期,请重新登录");
+            }
+            
+            // 获取当前用户信息
+            AdminUser user = userService.getUserById(currentUser.getId());
+            if (user == null) {
+                return Result.error("用户不存在");
+            }
+            
+            // 更新字段(不包括密码、用户名、状态等敏感字段)
+            if (params.containsKey("realName")) {
+                user.setRealName((String) params.get("realName"));
+            }
+            if (params.containsKey("phone")) {
+                user.setPhone((String) params.get("phone"));
+            }
+            if (params.containsKey("email")) {
+                user.setEmail((String) params.get("email"));
+            }
+            if (params.containsKey("avatar")) {
+                user.setAvatar((String) params.get("avatar"));
+            }
+            
+            boolean success = userService.updateUserProfile(user);
+            if (success) {
+                return Result.success("更新成功");
+            } else {
+                return Result.error("更新失败");
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("更新失败:" + e.getMessage());
+        }
+    }
+    
+    /**
+     * 更新当前登录用户的密码
+     */
+    @PutMapping("/current-password")
+    public Result<String> updateCurrentPassword(@RequestBody Map<String, Object> params,
+                                                @RequestHeader(value = "Authorization", required = false) String token) {
+        try {
+            // 验证token
+            if (token == null || token.isEmpty()) {
+                return Result.error(401, "未授权,请先登录");
+            }
+            
+            AdminUser currentUser = authService.getUserByToken(token);
+            if (currentUser == null) {
+                return Result.error(401, "Token无效或已过期,请重新登录");
+            }
+            
+            // 获取参数
+            String oldPassword = (String) params.get("oldPassword");
+            String newPassword = (String) params.get("newPassword");
+            
+            if (oldPassword == null || oldPassword.isEmpty()) {
+                return Result.error("原密码不能为空");
+            }
+            if (newPassword == null || newPassword.isEmpty()) {
+                return Result.error("新密码不能为空");
+            }
+            if (newPassword.length() < 6) {
+                return Result.error("新密码长度不能少于6位");
+            }
+            
+            // 获取完整用户信息(包含密码和盐)
+            AdminUser user = adminUserMapper.selectById(currentUser.getId());
+            if (user == null) {
+                return Result.error("用户不存在");
+            }
+            
+            // 验证原密码
+            String passwordWithSalt = oldPassword + user.getSalt();
+            String encryptedOldPassword = DigestUtils.md5DigestAsHex(passwordWithSalt.getBytes(StandardCharsets.UTF_8));
+            if (!encryptedOldPassword.equals(user.getPassword())) {
+                return Result.error("原密码不正确");
+            }
+            
+            // 更新密码
+            user.setPassword(newPassword);
+            boolean success = userService.updateUser(user);
+            if (success) {
+                return Result.success("密码修改成功");
+            } else {
+                return Result.error("密码修改失败");
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("密码修改失败:" + e.getMessage());
+        }
+    }
 }
 

+ 72 - 44
service/admin/src/main/java/com/zhentao/controller/UserController.java

@@ -17,6 +17,7 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Date;
 import java.util.stream.Collectors;
 /**
  * 用户管理控制器
@@ -31,15 +32,12 @@ public class UserController {
     
     @Autowired
     private UserVipMapper userVipMapper;
-    
-    @Autowired
-    private UserMatchmakerBindingMapper userMatchmakerBindingMapper;
-    
-    @Autowired
-    private MatchmakerMapper matchmakerMapper;
 
     @Autowired
     private VipPackageMapper vipPackageMapper;
+
+    @Autowired
+    private com.zhentao.mapper.SystemMessagesMapper systemMessagesMapper;
     
     /**
      * 用户列表(分页)
@@ -81,7 +79,7 @@ public class UserController {
             
             Page<Users> result = usersMapper.selectPage(pageInfo, queryWrapper);
             
-            // 转换为UserVO并填充VIP和红娘信息
+            // 转换为UserVO并填充VIP信息
             List<UserVO> userVOList = result.getRecords().stream()
                     .map(this::convertToUserVO)
                     .collect(Collectors.toList());
@@ -112,9 +110,6 @@ public class UserController {
         // 填充VIP信息
         fillVipInfo(vo, user.getUserId());
         
-        // 填充红娘信息
-        fillMatchmakerInfo(vo, user.getUserId());
-        
         return vo;
     }
     
@@ -177,10 +172,14 @@ public class UserController {
                 vo.setVipLevel("VIP会员");
                 vo.setVipStartTime(userVip.getStartTime());
                 vo.setVipEndTime(userVip.getEndTime());
-                
-                // 计算剩余天数
-                long days = Duration.between(LocalDateTime.now(), userVip.getEndTime()).toDays();
-                vo.setVipRemainingDays((int) days);
+
+                // 计算剩余天数:到期时间 - 开通时间
+                if (userVip.getStartTime() != null && userVip.getEndTime() != null) {
+                    long days = Duration.between(userVip.getStartTime(), userVip.getEndTime()).toDays();
+                    vo.setVipRemainingDays((int) Math.max(days, 0));
+                } else {
+                    vo.setVipRemainingDays(0);
+                }
             } else {
                 vo.setIsVip(false);
                 vo.setVipLevel("普通用户");
@@ -193,36 +192,6 @@ public class UserController {
         }
     }
     
-    /**
-     * 填充红娘信息
-     */
-    private void fillMatchmakerInfo(UserVO vo, Integer userId) {
-        try {
-            // 查询红娘绑定关系
-            QueryWrapper<UserMatchmakerBinding> bindingWrapper = new QueryWrapper<>();
-            bindingWrapper.eq("user_id", userId)
-                    .eq("status", 1)  // 状态:有效
-                    .orderByDesc("bound_at")
-                    .last("LIMIT 1");
-            
-            UserMatchmakerBinding binding = userMatchmakerBindingMapper.selectOne(bindingWrapper);
-            
-            if (binding != null) {
-                vo.setMatchmakerId(binding.getMatchmakerId());
-                vo.setBoundAt(binding.getBoundAt());
-                
-                // 查询红娘信息
-                Matchmaker matchmaker = matchmakerMapper.selectById(binding.getMatchmakerId());
-                if (matchmaker != null) {
-                    vo.setMatchmakerName(matchmaker.getRealName());
-                    vo.setMatchmakerPhone(matchmaker.getPhone());
-                }
-            }
-        } catch (Exception e) {
-            // 忽略异常,不影响主流程
-        }
-    }
-    
     /**
      * 认证用户列表(资料完整的用户)
      */
@@ -275,6 +244,7 @@ public class UserController {
                 vo.setVipStartTime(vip.getStartTime());
                 vo.setVipEndTime(vip.getEndTime());
 
+                // 计算剩余天数:到期时间 - 当前时间
                 if (vip.getEndTime() != null) {
                     long days = Duration.between(LocalDateTime.now(), vip.getEndTime()).toDays();
                     vo.setRemainingDays((int) Math.max(days, 0));
@@ -297,6 +267,64 @@ public class UserController {
             return Result.error("查询失败:" + e.getMessage());
         }
     }
+
+    /**
+     * VIP 会员到期系统提醒
+     */
+    @PostMapping("/vip/remind")
+    public Result<String> vipRemind(@RequestBody Map<String, Object> params) {
+        try {
+            if (params == null) {
+                return Result.error("参数不能为空");
+            }
+
+            Object userIdObj = params.get("userId");
+            if (userIdObj == null) {
+                return Result.error("userId 不能为空");
+            }
+
+            Integer userId;
+            if (userIdObj instanceof Integer) {
+                userId = (Integer) userIdObj;
+            } else if (userIdObj instanceof Number) {
+                userId = ((Number) userIdObj).intValue();
+            } else {
+                try {
+                    userId = Integer.parseInt(userIdObj.toString());
+                } catch (NumberFormatException e) {
+                    return Result.error("userId 格式不正确");
+                }
+            }
+
+            if (userId <= 0) {
+                return Result.error("userId 必须大于 0");
+            }
+
+            String content = params.get("content") != null ? params.get("content").toString() : null;
+            if (content == null || content.trim().isEmpty()) {
+                return Result.error("content 不能为空");
+            }
+
+            // 构造系统消息
+            com.zhentao.entity.SystemMessages message = new com.zhentao.entity.SystemMessages();
+            message.setUserId(userId);
+            message.setTitle("系统通知-会员到期提醒");
+            message.setType("vip_expire_remind");
+            message.setContent(content);
+            message.setIsRead(0);
+            message.setCreatedAt(new Date());
+
+            int insert = systemMessagesMapper.insert(message);
+            if (insert <= 0) {
+                return Result.error("保存系统提醒失败");
+            }
+
+            return Result.success("发送成功");
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("发送失败:" + e.getMessage());
+        }
+    }
     
     /**
      * 用户详情

+ 146 - 0
service/admin/src/main/java/com/zhentao/entity/ActivityRegistration.java

@@ -0,0 +1,146 @@
+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 com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+import java.util.Date;
+
+/**
+ * 活动报名记录表
+ * @TableName activity_registration
+ */
+@TableName(value ="activity_registration")
+@Data
+public class ActivityRegistration implements Serializable {
+    
+    private static final long serialVersionUID = 1L;
+    
+    /**
+     * 报名记录ID
+     */
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 活动ID
+     */
+    private Integer activityId;
+
+    /**
+     * 用户ID
+     */
+    private Integer userId;
+
+    /**
+     * 报名时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime registrationTime;
+
+    /**
+     * 报名状态(1:已报名,2:已取消,3:已签到)
+     */
+    private Integer status;
+
+    /**
+     * 支付状态(0:未支付,1:已支付)- 如果数据库中没有此字段,查询时使用默认值1(已支付)
+     */
+    @TableField(value = "payment_status", exist = false)
+    private Integer paymentStatus;
+
+    /**
+     * 创建时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime createdTime;
+
+    /**
+     * 更新时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime updatedTime;
+    
+    // ========== 关联查询字段(非数据库字段) ==========
+    
+    /**
+     * 活动名称
+     */
+    @TableField(exist = false)
+    private String activityName;
+    
+    /**
+     * 用户名(昵称)
+     */
+    @TableField(exist = false)
+    private String userName;
+    
+    /**
+     * 用户手机号
+     */
+    @TableField(exist = false)
+    private String userPhone;
+    
+    /**
+     * 用户性别
+     */
+    @TableField(exist = false)
+    private Integer userGender;
+
+    @Override
+    public boolean equals(Object that) {
+        if (this == that) {
+            return true;
+        }
+        if (that == null) {
+            return false;
+        }
+        if (getClass() != that.getClass()) {
+            return false;
+        }
+        ActivityRegistration other = (ActivityRegistration) that;
+        return (this.getId() == null ? other.getId() == null : this.getId().equals(other.getId()))
+            && (this.getActivityId() == null ? other.getActivityId() == null : this.getActivityId().equals(other.getActivityId()))
+            && (this.getUserId() == null ? other.getUserId() == null : this.getUserId().equals(other.getUserId()))
+            && (this.getRegistrationTime() == null ? other.getRegistrationTime() == null : this.getRegistrationTime().equals(other.getRegistrationTime()))
+            && (this.getStatus() == null ? other.getStatus() == null : this.getStatus().equals(other.getStatus()))
+            && (this.getCreatedTime() == null ? other.getCreatedTime() == null : this.getCreatedTime().equals(other.getCreatedTime()))
+            && (this.getUpdatedTime() == null ? other.getUpdatedTime() == null : this.getUpdatedTime().equals(other.getUpdatedTime()));
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((getId() == null) ? 0 : getId().hashCode());
+        result = prime * result + ((getActivityId() == null) ? 0 : getActivityId().hashCode());
+        result = prime * result + ((getUserId() == null) ? 0 : getUserId().hashCode());
+        result = prime * result + ((getRegistrationTime() == null) ? 0 : getRegistrationTime().hashCode());
+        result = prime * result + ((getStatus() == null) ? 0 : getStatus().hashCode());
+        result = prime * result + ((getCreatedTime() == null) ? 0 : getCreatedTime().hashCode());
+        result = prime * result + ((getUpdatedTime() == null) ? 0 : getUpdatedTime().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(", activityId=").append(activityId);
+        sb.append(", userId=").append(userId);
+        sb.append(", registrationTime=").append(registrationTime);
+        sb.append(", status=").append(status);
+        sb.append(", createdTime=").append(createdTime);
+        sb.append(", updatedTime=").append(updatedTime);
+        sb.append("]");
+        return sb.toString();
+    }
+}

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

@@ -55,6 +55,10 @@ public class AdminUser {
      * 状态:0-禁用 1-启用
      */
     private Integer status;
+    /**
+     * 头像
+     * */
+    private String avatar;
 
     /**
      * 角色:admin-普通管理员 super-超级管理员

+ 144 - 0
service/admin/src/main/java/com/zhentao/entity/HomeFunctionGrid.java

@@ -0,0 +1,144 @@
+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.util.Date;
+import lombok.Data;
+
+/**
+ * 首页金刚区功能表
+ * @TableName home_function_grid
+ */
+@TableName(value ="home_function_grid")
+@Data
+public class HomeFunctionGrid {
+    /**
+     * 主键ID
+     */
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 功能名称
+     */
+    private String name;
+
+    /**
+     * 图标(emoji)
+     */
+    private String icon;
+
+    /**
+     * 跳转路径
+     */
+    private String path;
+
+    /**
+     * 背景颜色
+     */
+    private String bgColor;
+
+    /**
+     * 图标颜色
+     */
+    private String iconColor;
+
+    /**
+     * 渐变背景
+     */
+    private String gradient;
+
+    /**
+     * 是否需要登录:0-不需要 1-需要
+     */
+    private Integer needLogin;
+
+    /**
+     * 排序
+     */
+    private Integer sort;
+
+    /**
+     * 状态:0-禁用 1-启用
+     */
+    private Integer status;
+
+    /**
+     * 创建时间
+     */
+    private Date createTime;
+
+    /**
+     * 更新时间
+     */
+    private Date updateTime;
+
+    @Override
+    public boolean equals(Object that) {
+        if (this == that) {
+            return true;
+        }
+        if (that == null) {
+            return false;
+        }
+        if (getClass() != that.getClass()) {
+            return false;
+        }
+        HomeFunctionGrid other = (HomeFunctionGrid) that;
+        return (this.getId() == null ? other.getId() == null : this.getId().equals(other.getId()))
+            && (this.getName() == null ? other.getName() == null : this.getName().equals(other.getName()))
+            && (this.getIcon() == null ? other.getIcon() == null : this.getIcon().equals(other.getIcon()))
+            && (this.getPath() == null ? other.getPath() == null : this.getPath().equals(other.getPath()))
+            && (this.getBgColor() == null ? other.getBgColor() == null : this.getBgColor().equals(other.getBgColor()))
+            && (this.getIconColor() == null ? other.getIconColor() == null : this.getIconColor().equals(other.getIconColor()))
+            && (this.getGradient() == null ? other.getGradient() == null : this.getGradient().equals(other.getGradient()))
+            && (this.getNeedLogin() == null ? other.getNeedLogin() == null : this.getNeedLogin().equals(other.getNeedLogin()))
+            && (this.getSort() == null ? other.getSort() == null : this.getSort().equals(other.getSort()))
+            && (this.getStatus() == null ? other.getStatus() == null : this.getStatus().equals(other.getStatus()))
+            && (this.getCreateTime() == null ? other.getCreateTime() == null : this.getCreateTime().equals(other.getCreateTime()))
+            && (this.getUpdateTime() == null ? other.getUpdateTime() == null : this.getUpdateTime().equals(other.getUpdateTime()));
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((getId() == null) ? 0 : getId().hashCode());
+        result = prime * result + ((getName() == null) ? 0 : getName().hashCode());
+        result = prime * result + ((getIcon() == null) ? 0 : getIcon().hashCode());
+        result = prime * result + ((getPath() == null) ? 0 : getPath().hashCode());
+        result = prime * result + ((getBgColor() == null) ? 0 : getBgColor().hashCode());
+        result = prime * result + ((getIconColor() == null) ? 0 : getIconColor().hashCode());
+        result = prime * result + ((getGradient() == null) ? 0 : getGradient().hashCode());
+        result = prime * result + ((getNeedLogin() == null) ? 0 : getNeedLogin().hashCode());
+        result = prime * result + ((getSort() == null) ? 0 : getSort().hashCode());
+        result = prime * result + ((getStatus() == null) ? 0 : getStatus().hashCode());
+        result = prime * result + ((getCreateTime() == null) ? 0 : getCreateTime().hashCode());
+        result = prime * result + ((getUpdateTime() == null) ? 0 : getUpdateTime().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(", name=").append(name);
+        sb.append(", icon=").append(icon);
+        sb.append(", path=").append(path);
+        sb.append(", bgColor=").append(bgColor);
+        sb.append(", iconColor=").append(iconColor);
+        sb.append(", gradient=").append(gradient);
+        sb.append(", needLogin=").append(needLogin);
+        sb.append(", sort=").append(sort);
+        sb.append(", status=").append(status);
+        sb.append(", createTime=").append(createTime);
+        sb.append(", updateTime=").append(updateTime);
+        sb.append("]");
+        return sb.toString();
+    }
+}

+ 35 - 0
service/admin/src/main/java/com/zhentao/mapper/ActivityRegistrationMapper.java

@@ -0,0 +1,35 @@
+package com.zhentao.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zhentao.entity.ActivityRegistration;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+* @author 联想
+* @description 针对表【activity_registration(活动报名记录表)】的数据库操作Mapper
+* @createDate 2025-12-17 09:04:20
+* @Entity com.zhentao.entity.ActivityRegistration
+*/
+public interface ActivityRegistrationMapper extends BaseMapper<ActivityRegistration> {
+
+    /**
+     * 分页查询报名列表(关联查询活动和用户信息)
+     * @param activityId 活动ID
+     * @param checkInStatus 签到状态(1:已报名,2:已取消,3:已签到)
+     * @param paymentStatus 支付状态(0:未支付,1:已支付)
+     * @param keyword 搜索关键词(用户名或手机号)
+     * @return 报名列表
+     */
+    List<ActivityRegistration> selectRegistrationList(
+            @Param("activityId") Integer activityId,
+            @Param("checkInStatus") Integer checkInStatus,
+            @Param("paymentStatus") Integer paymentStatus,
+            @Param("keyword") String keyword
+    );
+}
+
+
+
+

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

@@ -0,0 +1,18 @@
+package com.zhentao.mapper;
+
+import com.zhentao.entity.HomeFunctionGrid;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+
+/**
+* @author 联想
+* @description 针对表【home_function_grid(首页金刚区功能表)】的数据库操作Mapper
+* @createDate 2025-12-17 10:56:17
+* @Entity com.zhentao.entity.HomeFunctionGrid
+*/
+public interface HomeFunctionGridMapper extends BaseMapper<HomeFunctionGrid> {
+
+}
+
+
+
+

+ 55 - 0
service/admin/src/main/java/com/zhentao/service/ActivityRegistrationService.java

@@ -0,0 +1,55 @@
+package com.zhentao.service;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zhentao.entity.ActivityRegistration;
+import com.baomidou.mybatisplus.extension.service.IService;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+* @author 联想
+* @description 针对表【activity_registration(活动报名记录表)】的数据库操作Service
+* @createDate 2025-12-17 09:04:20
+*/
+public interface ActivityRegistrationService extends IService<ActivityRegistration> {
+
+    /**
+     * 分页查询报名列表(关联查询活动和用户信息)
+     * @param page 页码
+     * @param pageSize 每页大小
+     * @param activityId 活动ID(可选)
+     * @param checkInStatus 签到状态(可选,1:已报名,2:已取消,3:已签到)
+     * @param paymentStatus 支付状态(可选,0:未支付,1:已支付)
+     * @param keyword 搜索关键词(可选,用户名或手机号)
+     * @return 分页结果
+     */
+    Map<String, Object> getRegistrationList(Integer page, Integer pageSize, 
+                                             Integer activityId, 
+                                             Integer checkInStatus, 
+                                             Integer paymentStatus, 
+                                             String keyword);
+
+    /**
+     * 查询所有符合条件的报名记录(用于导出)
+     * @param activityId 活动ID(可选)
+     * @param checkInStatus 签到状态(可选)
+     * @param paymentStatus 支付状态(可选)
+     * @param keyword 搜索关键词(可选)
+     * @return 报名列表
+     */
+    List<ActivityRegistration> getAllRegistrations(Integer activityId, 
+                                                    Integer checkInStatus, 
+                                                    Integer paymentStatus, 
+                                                    String keyword);
+
+    /**
+     * 获取报名统计数据
+     * 报名总数:签到状态为已报名(status=1)的记录数
+     * 已签到:签到状态为已签到(status=3)的记录数
+     * 未签到:签到状态为已报名(status=1)但未签到(status≠3)的记录数
+     * @param activityId 活动ID(必填)
+     * @return 统计数据
+     */
+    Map<String, Object> getRegistrationStats(Integer activityId);
+}

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

@@ -0,0 +1,13 @@
+package com.zhentao.service;
+
+import com.zhentao.entity.HomeFunctionGrid;
+import com.baomidou.mybatisplus.extension.service.IService;
+
+/**
+* @author 联想
+* @description 针对表【home_function_grid(首页金刚区功能表)】的数据库操作Service
+* @createDate 2025-12-17 10:56:17
+*/
+public interface HomeFunctionGridService extends IService<HomeFunctionGrid> {
+
+}

+ 8 - 0
service/admin/src/main/java/com/zhentao/service/SystemMessagesService.java

@@ -1,5 +1,6 @@
 package com.zhentao.service;
 
+import com.zhentao.entity.MarrApply;
 import com.zhentao.entity.PointsOrder;
 import com.zhentao.entity.SystemMessages;
 import com.baomidou.mybatisplus.extension.service.IService;
@@ -16,4 +17,11 @@ public interface SystemMessagesService extends IService<SystemMessages> {
      * @param pointsOrder 积分订单
      */
     void createPointsOrderReviewMessage(PointsOrder pointsOrder);
+    
+    /**
+     * 创建红娘申请审核系统消息
+     * @param marrApply 红娘申请
+     * @param approved 审核结果,true-通过,false-不通过
+     */
+    void createMatchmakerApplyReviewMessage(MarrApply marrApply, Boolean approved);
 }

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

@@ -31,6 +31,11 @@ public interface UserService {
      */
     boolean updateUser(AdminUser user);
     
+    /**
+     * 更新用户个人资料(不包括密码)
+     */
+    boolean updateUserProfile(AdminUser user);
+    
     /**
      * 删除用户
      */

+ 121 - 0
service/admin/src/main/java/com/zhentao/service/impl/ActivityRegistrationServiceImpl.java

@@ -0,0 +1,121 @@
+package com.zhentao.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.zhentao.entity.ActivityRegistration;
+import com.zhentao.mapper.ActivityRegistrationMapper;
+import com.zhentao.service.ActivityRegistrationService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+* @author 联想
+* @description 针对表【activity_registration(活动报名记录表)】的数据库操作Service实现
+* @createDate 2025-12-17 09:04:20
+*/
+@Service
+public class ActivityRegistrationServiceImpl extends ServiceImpl<ActivityRegistrationMapper, ActivityRegistration>
+    implements ActivityRegistrationService{
+
+    @Autowired
+    private ActivityRegistrationMapper activityRegistrationMapper;
+
+    @Override
+    public Map<String, Object> getRegistrationList(Integer page, Integer pageSize, 
+                                                    Integer activityId, 
+                                                    Integer checkInStatus, 
+                                                    Integer paymentStatus, 
+                                                    String keyword) {
+        // 查询所有符合条件的记录
+        List<ActivityRegistration> allList = activityRegistrationMapper.selectRegistrationList(
+                activityId, checkInStatus, paymentStatus, keyword);
+        
+        // 手动分页
+        int total = allList.size();
+        int start = (page - 1) * pageSize;
+        int end = Math.min(start + pageSize, total);
+        List<ActivityRegistration> pageList;
+        if (start < total) {
+            pageList = allList.subList(start, end);
+        } else {
+            pageList = new java.util.ArrayList<>();
+        }
+        
+        // 如果没有支付状态,默认设置为1(已支付)
+        for (ActivityRegistration registration : pageList) {
+            if (registration.getPaymentStatus() == null) {
+                registration.setPaymentStatus(1);
+            }
+        }
+        
+        Map<String, Object> result = new HashMap<>();
+        result.put("list", pageList);
+        result.put("total", total);
+        result.put("page", page);
+        result.put("pageSize", pageSize);
+        
+        return result;
+    }
+
+    @Override
+    public List<ActivityRegistration> getAllRegistrations(Integer activityId, 
+                                                           Integer checkInStatus, 
+                                                           Integer paymentStatus, 
+                                                           String keyword) {
+        List<ActivityRegistration> list = activityRegistrationMapper.selectRegistrationList(
+                activityId, checkInStatus, paymentStatus, keyword);
+        
+        // 如果没有支付状态,默认设置为1(已支付)
+        for (ActivityRegistration registration : list) {
+            if (registration.getPaymentStatus() == null) {
+                registration.setPaymentStatus(1);
+            }
+        }
+        
+        return list;
+    }
+
+    @Override
+    public Map<String, Object> getRegistrationStats(Integer activityId) {
+        Map<String, Object> stats = new HashMap<>();
+        
+        if (activityId == null) {
+            stats.put("totalCount", 0);
+            stats.put("checkedInCount", 0);
+            stats.put("notCheckedInCount", 0);
+            return stats;
+        }
+        
+        // 报名总数:签到状态为已报名(status=1)的记录数
+        List<ActivityRegistration> registeredList = activityRegistrationMapper.selectRegistrationList(
+                activityId, 1, null, null);
+        int totalCount = registeredList.size();
+        
+        // 已签到:查询status=3的记录数(这些记录之前是status=1,现在已签到)
+        // 注意:已签到的记录现在status=3,所以不会出现在status=1的查询结果中
+        List<ActivityRegistration> checkedInList = activityRegistrationMapper.selectRegistrationList(
+                activityId, 3, null, null);
+        int checkedInCount = checkedInList.size();
+        
+        // 未签到:签到状态为已报名(status=1)但未签到(status≠3)的记录数
+        // 根据用户需求,未签到也是根据签到状态是已报名进行显示的
+        // 所以未签到数 = 报名总数(status=1的记录数)
+        // 因为status=1的记录就是未签到的
+        int notCheckedInCount = totalCount;
+        
+        stats.put("totalCount", totalCount);
+        stats.put("checkedInCount", checkedInCount);
+        stats.put("notCheckedInCount", notCheckedInCount);
+        
+        return stats;
+    }
+}
+
+
+
+

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

@@ -0,0 +1,22 @@
+package com.zhentao.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.zhentao.entity.HomeFunctionGrid;
+import com.zhentao.service.HomeFunctionGridService;
+import com.zhentao.mapper.HomeFunctionGridMapper;
+import org.springframework.stereotype.Service;
+
+/**
+* @author 联想
+* @description 针对表【home_function_grid(首页金刚区功能表)】的数据库操作Service实现
+* @createDate 2025-12-17 10:56:17
+*/
+@Service
+public class HomeFunctionGridServiceImpl extends ServiceImpl<HomeFunctionGridMapper, HomeFunctionGrid>
+    implements HomeFunctionGridService{
+
+}
+
+
+
+

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

@@ -5,6 +5,7 @@ import com.zhentao.entity.MarrApply;
 import com.zhentao.entity.Matchmaker;
 import com.zhentao.entity.Users;
 import com.zhentao.service.MarrApplyService;
+import com.zhentao.service.SystemMessagesService;
 import com.zhentao.mapper.MarrApplyMapper;
 import com.zhentao.mapper.MatchmakerMapper;
 import com.zhentao.mapper.UsersMapper;
@@ -55,6 +56,9 @@ public class MarrApplyServiceImpl extends ServiceImpl<MarrApplyMapper, MarrApply
     @Autowired
     private AreaMapper areaMapper;
     
+    @Autowired
+    private SystemMessagesService systemMessagesService;
+    
     private final RestTemplate restTemplate = new RestTemplate();
     
     // websocket 服务的 IM 接口地址
@@ -205,6 +209,18 @@ public class MarrApplyServiceImpl extends ServiceImpl<MarrApplyMapper, MarrApply
         }
         
         this.updateById(apply);
+        
+        // 创建系统消息通知用户审核结果
+        try {
+            System.out.println("开始调用系统消息创建方法...");
+            systemMessagesService.createMatchmakerApplyReviewMessage(apply, approved);
+            System.out.println("系统消息创建方法调用完成");
+        } catch (Exception e) {
+            // 系统消息创建失败不影响审核结果,只记录日志
+            System.err.println("创建系统消息失败:" + e.getMessage());
+            e.printStackTrace();
+        }
+        
         System.out.println("========== 审核完成 ==========");
         return true;
     }

+ 96 - 0
service/admin/src/main/java/com/zhentao/service/impl/SystemMessagesServiceImpl.java

@@ -2,6 +2,7 @@ package com.zhentao.service.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.zhentao.entity.MarrApply;
 import com.zhentao.entity.PointsOrder;
 import com.zhentao.entity.SystemMessages;
 import com.zhentao.entity.Users;
@@ -135,6 +136,101 @@ public class SystemMessagesServiceImpl extends ServiceImpl<SystemMessagesMapper,
         
         System.out.println("========== 创建积分订单审核系统消息完成 ==========");
     }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void createMatchmakerApplyReviewMessage(MarrApply marrApply, Boolean approved) {
+        System.out.println("========== 开始创建红娘申请审核系统消息 ==========");
+        System.out.println("申请信息: " + (marrApply != null ? marrApply.toString() : "null"));
+        System.out.println("审核结果: " + (approved != null ? (approved ? "通过" : "不通过") : "null"));
+        
+        if (marrApply == null) {
+            System.err.println("错误:marrApply 为 null,无法创建系统消息");
+            return;
+        }
+        
+        if (marrApply.getUserId() == null) {
+            System.err.println("错误:红娘申请的用户ID为空,申请ID=" + marrApply.getApplyId());
+            return;
+        }
+
+        System.out.println("用户ID: " + marrApply.getUserId());
+        System.out.println("审核状态: " + (approved != null ? (approved ? "通过" : "不通过") : "null"));
+        
+        // 验证用户是否存在
+        Users user = usersMapper.selectById(marrApply.getUserId());
+        if (user == null) {
+            System.err.println("错误:未找到用户ID为 " + marrApply.getUserId() + " 的用户,无法创建系统消息");
+            return;
+        }
+        
+        if (user.getUserId() == null) {
+            System.err.println("错误:找到用户但 userId 为 null,用户ID=" + marrApply.getUserId());
+            return;
+        }
+
+        System.out.println("找到用户: userId=" + user.getUserId() + ", phone=" + user.getPhone());
+
+        // 创建系统消息
+        SystemMessages message = new SystemMessages();
+        message.setUserId(user.getUserId());
+        message.setTitle("系统通知-红娘申请审核通知");
+        message.setType("marr_apply_review"); // 设置消息类型为红娘申请审核
+        
+        // 根据审核状态设置内容
+        if (approved != null) {
+            if (approved) {
+                // 审核通过
+                String content = "经平台核实,已通过您的红娘申请,恭喜您成为青鸾之恋红娘大家族的一员!!!";
+                message.setContent(content);
+                System.out.println("审核通过,消息内容: " + content);
+            } else {
+                // 审核未通过
+                String reason = marrApply.getReason() != null && !marrApply.getReason().trim().isEmpty() 
+                    ? marrApply.getReason() : "未说明原因";
+                String content = "经平台核实,抱歉您的红娘申请未通过,原因是:" + reason;
+                message.setContent(content);
+                System.out.println("审核未通过,消息内容: " + content);
+            }
+        } else {
+            // 审核状态为空,不创建消息
+            System.err.println("错误:审核状态为 null,不创建消息");
+            return;
+        }
+
+        message.setIsRead(0); // 0-未读
+        message.setCreatedAt(new Date());
+        
+        System.out.println("准备保存系统消息: userId=" + message.getUserId() + ", title=" + message.getTitle() + ", content=" + message.getContent());
+        System.out.println("消息对象详情: " + message.toString());
+        
+        // 保存系统消息 - 使用 systemMessagesMapper.insert() 直接插入
+        try {
+            int insertResult = systemMessagesMapper.insert(message);
+            if (insertResult > 0) {
+                System.out.println("系统消息保存成功!插入行数=" + insertResult + ", 消息ID=" + (message.getId() != null ? message.getId() : "待生成"));
+                System.out.println("保存后的消息对象: " + message.toString());
+                
+                // 验证数据是否真的插入到数据库
+                if (message.getId() != null) {
+                    SystemMessages savedMessage = systemMessagesMapper.selectById(message.getId());
+                    if (savedMessage != null) {
+                        System.out.println("验证成功:从数据库查询到保存的消息,ID=" + savedMessage.getId());
+                    } else {
+                        System.err.println("警告:消息保存后无法从数据库查询到,可能存在事务问题");
+                    }
+                }
+            } else {
+                System.err.println("系统消息保存失败!insert() 方法返回 0,未插入任何数据");
+            }
+        } catch (Exception e) {
+            System.err.println("系统消息保存时发生异常:" + e.getMessage());
+            e.printStackTrace();
+            throw new RuntimeException("保存系统消息失败:" + e.getMessage(), e);
+        }
+        
+        System.out.println("========== 创建红娘申请审核系统消息完成 ==========");
+    }
 }
 
 

+ 9 - 0
service/admin/src/main/java/com/zhentao/service/impl/UserServiceImpl.java

@@ -139,6 +139,15 @@ public class UserServiceImpl implements UserService {
         return adminUserMapper.updateById(user) > 0;
     }
     
+    @Override
+    public boolean updateUserProfile(AdminUser user) {
+        user.setUpdateTime(new Date());
+        // 清除密码和盐字段,确保不会更新密码
+        user.setPassword(null);
+        user.setSalt(null);
+        return adminUserMapper.updateById(user) > 0;
+    }
+    
     @Override
     @Transactional
     public boolean deleteUser(Integer userId) {

+ 100 - 0
service/admin/src/main/java/com/zhentao/vo/ActivityRegistrationExcelVO.java

@@ -0,0 +1,100 @@
+package com.zhentao.vo;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import com.alibaba.excel.annotation.write.style.ColumnWidth;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * 活动报名Excel导出VO
+ */
+@Data
+@ColumnWidth(20)
+public class ActivityRegistrationExcelVO implements Serializable {
+    
+    private static final long serialVersionUID = 1L;
+    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+    
+    @ExcelProperty(value = "报名ID", index = 0)
+    private Long id;
+    
+    @ExcelProperty(value = "活动名称", index = 1)
+    private String activityName;
+    
+    @ExcelProperty(value = "用户ID", index = 2)
+    private Integer userId;
+    
+    @ExcelProperty(value = "用户名", index = 3)
+    private String userName;
+    
+    @ExcelProperty(value = "手机号", index = 4)
+    private String userPhone;
+    
+    @ExcelProperty(value = "性别", index = 5)
+    private String userGender;
+    
+    @ExcelProperty(value = "报名时间", index = 6)
+    private String registrationTime;
+    
+    @ExcelProperty(value = "签到状态", index = 7)
+    private String checkInStatus;
+    
+    @ExcelProperty(value = "支付状态", index = 8)
+    private String paymentStatus;
+    
+    @ExcelProperty(value = "创建时间", index = 9)
+    private String createdTime;
+    
+    /**
+     * 将实体类转换为ExcelVO
+     */
+    public static ActivityRegistrationExcelVO fromEntity(com.zhentao.entity.ActivityRegistration registration) {
+        ActivityRegistrationExcelVO vo = new ActivityRegistrationExcelVO();
+        vo.setId(registration.getId());
+        vo.setActivityName(registration.getActivityName());
+        vo.setUserId(registration.getUserId());
+        vo.setUserName(registration.getUserName());
+        vo.setUserPhone(registration.getUserPhone());
+        vo.setUserGender(registration.getUserGender() != null && registration.getUserGender() == 1 ? "男" : "女");
+        
+        if (registration.getRegistrationTime() != null) {
+            vo.setRegistrationTime(registration.getRegistrationTime().format(FORMATTER));
+        }
+        
+        // 签到状态:1-已报名,2-已取消,3-已签到
+        Integer status = registration.getStatus();
+        if (status != null) {
+            switch (status) {
+                case 1:
+                    vo.setCheckInStatus("已报名");
+                    break;
+                case 2:
+                    vo.setCheckInStatus("已取消");
+                    break;
+                case 3:
+                    vo.setCheckInStatus("已签到");
+                    break;
+                default:
+                    vo.setCheckInStatus("未知");
+            }
+        }
+        
+        // 支付状态:0-未支付,1-已支付
+        Integer paymentStatus = registration.getPaymentStatus();
+        if (paymentStatus != null) {
+            vo.setPaymentStatus(paymentStatus == 1 ? "已支付" : "未支付");
+        } else {
+            vo.setPaymentStatus("已支付"); // 默认已支付
+        }
+        
+        if (registration.getCreatedTime() != null) {
+            vo.setCreatedTime(registration.getCreatedTime().format(FORMATTER));
+        }
+        
+        return vo;
+    }
+}
+

+ 81 - 0
service/admin/src/main/resources/com/zhentao/mapper/ActivityRegistrationMapper.xml

@@ -0,0 +1,81 @@
+<?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.ActivityRegistrationMapper">
+
+    <resultMap id="BaseResultMap" type="com.zhentao.entity.ActivityRegistration">
+            <id property="id" column="id" />
+            <result property="activityId" column="activity_id" />
+            <result property="userId" column="user_id" />
+            <result property="registrationTime" column="registration_time" />
+            <result property="status" column="status" />
+            <result property="paymentStatus" column="payment_status" />
+            <result property="createdTime" column="created_time" />
+            <result property="updatedTime" column="updated_time" />
+            <!-- 关联查询字段 -->
+            <result property="activityName" column="activity_name" />
+            <result property="userName" column="user_name" />
+            <result property="userPhone" column="user_phone" />
+            <result property="userGender" column="user_gender" />
+    </resultMap>
+
+    <resultMap id="RegistrationWithDetailsMap" type="com.zhentao.entity.ActivityRegistration">
+            <id property="id" column="id" />
+            <result property="activityId" column="activity_id" />
+            <result property="userId" column="user_id" />
+            <result property="registrationTime" column="registration_time" />
+            <result property="status" column="status" />
+            <result property="paymentStatus" column="payment_status" />
+            <result property="createdTime" column="created_time" />
+            <result property="updatedTime" column="updated_time" />
+            <!-- 关联查询字段 -->
+            <result property="activityName" column="activity_name" />
+            <result property="userName" column="user_name" />
+            <result property="userPhone" column="user_phone" />
+            <result property="userGender" column="user_gender" />
+    </resultMap>
+
+    <sql id="Base_Column_List">
+        ar.id,ar.activity_id,ar.user_id,ar.registration_time,ar.status,
+        1 as payment_status,
+        ar.created_time,ar.updated_time
+    </sql>
+
+    <!-- 关联查询字段 -->
+    <sql id="Join_Column_List">
+        <include refid="Base_Column_List"/>,
+        a.name as activity_name,
+        u.nickname as user_name,
+        u.phone as user_phone,
+        u.gender as user_gender
+    </sql>
+
+    <!-- 分页查询报名列表(关联查询活动和用户信息) -->
+    <select id="selectRegistrationList" resultMap="RegistrationWithDetailsMap">
+        SELECT
+        <include refid="Join_Column_List"/>
+        FROM activity_registration ar
+        LEFT JOIN activity a ON ar.activity_id = a.id
+        LEFT JOIN users u ON ar.user_id = u.user_id
+        <where>
+            <if test="activityId != null">
+                AND ar.activity_id = #{activityId}
+            </if>
+            <if test="checkInStatus != null">
+                AND ar.status = #{checkInStatus}
+            </if>
+            <!-- 支付状态筛选(如果数据库有payment_status字段,可以启用此条件) -->
+            <!-- 
+            <if test="paymentStatus != null">
+                AND IFNULL(ar.payment_status, 1) = #{paymentStatus}
+            </if>
+            -->
+            <if test="keyword != null and keyword != ''">
+                AND (u.nickname LIKE CONCAT('%', #{keyword}, '%')
+                OR u.phone LIKE CONCAT('%', #{keyword}, '%'))
+            </if>
+        </where>
+        ORDER BY ar.registration_time DESC
+    </select>
+</mapper>

+ 27 - 0
service/admin/src/main/resources/com/zhentao/mapper/HomeFunctionGridMapper.xml

@@ -0,0 +1,27 @@
+<?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.HomeFunctionGridMapper">
+
+    <resultMap id="BaseResultMap" type="com.zhentao.entity.HomeFunctionGrid">
+            <id property="id" column="id" />
+            <result property="name" column="name" />
+            <result property="icon" column="icon" />
+            <result property="path" column="path" />
+            <result property="bgColor" column="bg_color" />
+            <result property="iconColor" column="icon_color" />
+            <result property="gradient" column="gradient" />
+            <result property="needLogin" column="need_login" />
+            <result property="sort" column="sort" />
+            <result property="status" column="status" />
+            <result property="createTime" column="create_time" />
+            <result property="updateTime" column="update_time" />
+    </resultMap>
+
+    <sql id="Base_Column_List">
+        id,name,icon,path,bg_color,icon_color,
+        gradient,need_login,sort,status,create_time,
+        update_time
+    </sql>
+</mapper>