Browse Source

Merge branch 'cjp' into test_dev

caojp 2 months ago
parent
commit
f78ad2b349

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

@@ -194,6 +194,12 @@ const router = createRouter({
           component: () => import('@/views/dynamic/DynamicList.vue'),
           meta: { title: '动态管理', icon: 'ChatDotSquare' }
         },
+        {
+          path: 'dynamic/detail/:id',
+          name: 'DynamicDetail',
+          component: () => import('@/views/dynamic/DynamicDetail.vue'),
+          meta: { title: '动态详情', hidden: true }
+        },
         // 举报管理
         {
           path: 'report',

+ 46 - 5
marriageAdmin-vue/src/views/activity/ActivityList.vue

@@ -63,11 +63,17 @@
         <el-table-column prop="coverImage" label="封面" width="120">
           <template #default="{ row }">
             <el-image
-              :src="row.coverImage"
+              :src="getCoverImage(row)"
               fit="cover"
               class="cover-image"
-              :preview-src-list="[row.coverImage]"
-            />
+              :preview-src-list="getCoverPreviewList(row)"
+            >
+              <template #error>
+                <div class="image-slot">
+                  <el-icon><Picture /></el-icon>
+                </div>
+              </template>
+            </el-image>
           </template>
         </el-table-column>
         
@@ -195,9 +201,9 @@
 import { ref, reactive, onMounted } from 'vue'
 import { useRouter } from 'vue-router'
 import { ElMessage, ElMessageBox } from 'element-plus'
-import { Calendar } from '@element-plus/icons-vue'
+import { Calendar, Picture } from '@element-plus/icons-vue'
 import request from '@/utils/request'
-import { API_ENDPOINTS } from '@/config/api'
+import { API_BASE_URL, API_ENDPOINTS } from '@/config/api'
 
 const router = useRouter()
 const loading = ref(false)
@@ -212,6 +218,28 @@ const filters = reactive({
   keyword: ''
 })
 
+// 规范化图片地址,兼容相对路径和缺少协议的情况
+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 getCoverImage = (row) => {
+  const url = row?.coverImage || row?.cover_image || row?.cover || ''
+  return resolveImageUrl(url)
+}
+
+const getCoverPreviewList = (row) => {
+  const url = getCoverImage(row)
+  return url ? [url] : []
+}
+
 // 加载活动列表
 const loadActivityList = async () => {
   loading.value = true
@@ -323,5 +351,18 @@ onMounted(() => {
   transform: scale(1.05);
   box-shadow: var(--shadow-md);
 }
+
+.image-slot {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 90px;
+  height: 60px;
+  background-color: var(--bg-tertiary);
+  color: var(--text-tertiary);
+  font-size: 20px;
+  border-radius: var(--radius-base);
+  border: 1px solid var(--border-color);
+}
 </style>
 

+ 2 - 1
marriageAdmin-vue/src/views/course/CourseForm.vue

@@ -199,7 +199,7 @@ const loadDetail = async (id) => {
         id: data.id,
         name: data.name || '',
         categoryName: data.categoryName || data.category_name || '',
-        instructor: data.instructor || '',
+        instructor: data.instructor || data.teacher_name || data.teacherName || '',
         duration: data.duration || 0,
         price: data.price || 0,
         rating: data.rating || 4.5,
@@ -232,6 +232,7 @@ const handleSubmit = async () => {
       name: form.name,
       category_name: form.categoryName,  // 转换为下划线格式
       instructor: form.instructor,
+      teacher_name: form.instructor, // 兼容后端字段
       duration: form.duration,
       price: form.price,
       rating: form.rating,

+ 5 - 1
marriageAdmin-vue/src/views/course/CourseList.vue

@@ -52,7 +52,11 @@
             <span class="data-meta">{{ row.categoryName || row.category_name || '-' }}</span>
           </template>
         </el-table-column>
-        <el-table-column prop="instructor" label="讲师" width="120" />
+        <el-table-column prop="instructor" label="讲师" width="120">
+          <template #default="{ row }">
+            <span class="data-meta">{{ row.instructor || row.teacher_name || row.teacherName || '-' }}</span>
+          </template>
+        </el-table-column>
         <el-table-column prop="price" label="价格" width="110" align="right">
           <template #default="{ row }">
             <span v-if="row.price > 0" class="amount-text">¥{{ row.price }}</span>

+ 12 - 1
marriageAdmin-vue/src/views/dynamic/DynamicList.vue

@@ -48,8 +48,11 @@
           </template>
         </el-table-column>
         <el-table-column prop="createTime" label="发布时间" width="180" />
-        <el-table-column label="操作" width="220" fixed="right">
+        <el-table-column label="操作" width="280" fixed="right">
           <template #default="{ row }">
+            <el-button type="primary" size="small" link @click="goDetail(row)">
+              查看
+            </el-button>
             <el-button v-if="row.auditStatus === 0" type="success" size="small" link @click="handleAudit(row, 1)">
               通过
             </el-button>
@@ -97,10 +100,13 @@
 
 <script setup>
 import { ref, reactive, onMounted } from 'vue'
+import { useRouter } from 'vue-router'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import request from '@/utils/request'
 import { API_ENDPOINTS } from '@/config/api'
 
+const router = useRouter()
+
 const loading = ref(false)
 const currentPage = ref(1)
 const pageSize = ref(10)
@@ -147,6 +153,11 @@ const getAuditStatusType = (status) => {
   return types[status] || ''
 }
 
+const goDetail = (row) => {
+  if (!row?.dynamicId) return
+  router.push(`/dynamic/detail/${row.dynamicId}`)
+}
+
 const handleAudit = async (row, auditStatus) => {
   try {
     const response = await request.post(`${API_ENDPOINTS.DYNAMIC_AUDIT}/${row.dynamicId}`, { auditStatus })

+ 42 - 17
marriageAdmin-vue/src/views/report/ReportList.vue

@@ -52,7 +52,7 @@
         <el-table-column prop="createdAt" label="举报时间" width="170" />
         <el-table-column label="操作" width="220" fixed="right">
           <template #default="{ row }">
-            <el-button size="small" type="primary" link @click="openDynamic(row.dynamicId)">查看动态</el-button>
+            <el-button size="small" type="primary" link @click="openDynamic(row)">查看动态</el-button>
             <el-button size="small" type="warning" link @click="openHandle(row)">处理举报</el-button>
             <el-divider direction="vertical" />
             <el-button size="small" type="danger" link @click="openModerate(row, 3)">删除</el-button>
@@ -133,7 +133,7 @@
 import { ref, reactive, onMounted } from 'vue'
 import { ElMessage } from 'element-plus'
 import request from '@/utils/request'
-import { API_ENDPOINTS } from '@/config/api'
+import { API_ENDPOINTS, API_BASE_URL } from '@/config/api'
 
 const loading = ref(false)
 const currentPage = ref(1)
@@ -154,8 +154,12 @@ const statusType = (s) => ({0:'danger',1:'warning',2:'success',3:'info'}[s] || '
 const typeText = (t) => ({ spam:'垃圾广告', porn:'色情低俗', violence:'暴力违法', attack:'人身攻击', fake:'虚假信息', plagiarism:'抄袭侵权', other:'其他' }[t] || t || '其他')
 const typeTagType = (t) => ({ spam:'warning', porn:'danger', violence:'danger', attack:'danger', fake:'warning', plagiarism:'warning', other:'info' }[t] || 'info')
 
-const BASE = import.meta.env.DEV ? 'http://localhost:8083' : ''
-const formatMedia = (u) => (u && !u.startsWith('http') ? `${BASE}${u}` : u)
+const BASE = import.meta.env.DEV ? 'http://localhost:8083' : API_BASE_URL || ''
+const formatMedia = (u) => {
+  if (!u) return ''
+  const url = String(u).trim()
+  return /^https?:\/\//i.test(url) ? url : `${BASE}${url}`
+}
 
 // 从后端临时塞入的 handleResult 前缀里解析昵称(@nick:李娜 ...)
 const extractNick = (row) => {
@@ -196,27 +200,48 @@ const loadList = async () => {
 
 // 动态详情
 const dynamicDialog = reactive({ visible: false, loading: false, data: null, medias: [] })
-const openDynamic = async (dynamicId) => {
+const parseMediaUrls = (raw) => {
+  if (!raw) return []
+  try {
+    const value = typeof raw === 'string' ? raw.trim() : raw
+    if (typeof value === 'string' && value.startsWith('[')) {
+      return JSON.parse(value)
+    }
+    if (typeof value === 'string') {
+      return value.split(',').map((s) => s.trim().replace(/^['"]|['"]$/g, '')).filter(Boolean)
+    }
+    return Array.isArray(value) ? value : []
+  } catch {
+    return []
+  }
+}
+const openDynamic = async (row) => {
+  const dynamicId = row?.dynamicId
+  if (!dynamicId) {
+    dynamicDialog.data = { content: '未获取到动态ID' }
+    dynamicDialog.medias = parseScreens(row?.screenshots)
+    dynamicDialog.visible = true
+    return
+  }
   dynamicDialog.visible = true
   dynamicDialog.loading = true
   dynamicDialog.data = null
   dynamicDialog.medias = []
   try {
     const res = await request.get(`${API_ENDPOINTS.DYNAMIC_DETAIL}/${dynamicId}`)
-    if (res.code === 200) {
+    if (res.code === 200 && res.data) {
       dynamicDialog.data = res.data
-      const raw = res.data?.mediaUrls
-      if (raw) {
-        if (typeof raw === 'string' && raw.trim().startsWith('[')) {
-          dynamicDialog.medias = JSON.parse(raw)
-        } else if (typeof raw === 'string') {
-          dynamicDialog.medias = raw.split(',').map(s=>s.trim()).filter(Boolean)
-        } else if (Array.isArray(raw)) {
-          dynamicDialog.medias = raw
-        }
-      }
+      const medias = parseMediaUrls(res.data?.mediaUrls)
+      dynamicDialog.medias = medias.length ? medias : parseScreens(row?.screenshots)
+    } else {
+      dynamicDialog.data = { content: res.msg || '动态不存在或已被删除' }
+      dynamicDialog.medias = parseScreens(row?.screenshots)
     }
-  } catch (e) { console.error('获取动态详情失败', e) }
+  } catch (e) {
+    console.error('获取动态详情失败', e)
+    dynamicDialog.data = { content: e?.message ? `加载失败:${e.message}` : '加载失败' }
+    dynamicDialog.medias = parseScreens(row?.screenshots)
+  }
   finally { dynamicDialog.loading = false }
 }
 

+ 58 - 10
service/admin/src/main/java/com/zhentao/controller/UserController.java

@@ -6,6 +6,7 @@ import com.zhentao.common.Result;
 import com.zhentao.entity.*;
 import com.zhentao.mapper.*;
 import com.zhentao.vo.UserVO;
+import com.zhentao.vo.UserVipListVO;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
@@ -36,6 +37,9 @@ public class UserController {
     
     @Autowired
     private MatchmakerMapper matchmakerMapper;
+
+    @Autowired
+    private VipPackageMapper vipPackageMapper;
     
     /**
      * 用户列表(分页)
@@ -186,17 +190,61 @@ public class UserController {
             @RequestParam(defaultValue = "10") Integer pageSize) {
         
         try {
-            Page<Users> pageInfo = new Page<>(page, pageSize);
-            QueryWrapper<Users> queryWrapper = new QueryWrapper<>();
-            
-            // 只查询资料完整的用户
-            queryWrapper.eq("is_profile_complete", 1);
-            queryWrapper.orderByDesc("created_at");
-            
-            Page<Users> result = usersMapper.selectPage(pageInfo, queryWrapper);
-            
+            Page<UserVip> pageInfo = new Page<>(page, pageSize);
+            QueryWrapper<UserVip> queryWrapper = new QueryWrapper<>();
+            queryWrapper.eq("status", 1) // 仅展示生效中的VIP
+                    .orderByDesc("end_time");
+
+            Page<UserVip> result = userVipMapper.selectPage(pageInfo, queryWrapper);
+
+            // 批量查询用户与套餐信息,避免 N+1
+            List<Long> userIds = result.getRecords().stream()
+                    .map(UserVip::getUserId)
+                    .filter(id -> id != null && id > 0)
+                    .distinct()
+                    .collect(Collectors.toList());
+            List<Long> packageIds = result.getRecords().stream()
+                    .map(UserVip::getPackageId)
+                    .filter(id -> id != null && id > 0)
+                    .distinct()
+                    .collect(Collectors.toList());
+
+            Map<Long, Users> userMap = userIds.isEmpty()
+                    ? new HashMap<>()
+                    : usersMapper.selectBatchIds(userIds).stream()
+                    .collect(Collectors.toMap(user -> Long.valueOf(user.getUserId()), user -> user));
+
+            Map<Long, VipPackage> packageMap = packageIds.isEmpty()
+                    ? new HashMap<>()
+                    : vipPackageMapper.selectBatchIds(packageIds).stream()
+                    .collect(Collectors.toMap(VipPackage::getPackageId, pkg -> pkg));
+
+            List<UserVipListVO> voList = new ArrayList<>();
+            for (UserVip vip : result.getRecords()) {
+                UserVipListVO vo = new UserVipListVO();
+                vo.setUserId(vip.getUserId() != null ? vip.getUserId().intValue() : null);
+
+                Users user = vip.getUserId() != null ? userMap.get(vip.getUserId()) : null;
+                vo.setNickname(user != null ? user.getNickname() : "-");
+
+                VipPackage vipPackage = vip.getPackageId() != null ? packageMap.get(vip.getPackageId()) : null;
+                vo.setVipLevel(vipPackage != null ? vipPackage.getPackageName() : "VIP会员");
+
+                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));
+                } else {
+                    vo.setRemainingDays(0);
+                }
+
+                voList.add(vo);
+            }
+
             Map<String, Object> data = new HashMap<>();
-            data.put("list", result.getRecords());
+            data.put("list", voList);
             data.put("total", result.getTotal());
             data.put("page", page);
             data.put("pageSize", pageSize);