Browse Source

feat(dynamic): 新增用户动态与互动功能模块

- 添加获取用户收藏、点赞、浏览记录列表接口
- 实现清空用户浏览记录功能
- 在动态详情页添加浏览记录异步记录逻辑
- 新增浏览历史服务接口及实现类
- 优化动态详情查询,支持IP与设备信息记录
- 移除聊天API路由配置
- 前端新增我的动态页面,支持动态、互动、浏览记录展示
- 调整小程序配置,移除地理位置权限声明
- 修改动态点赞实体主键字段名
- 完善动态相关Mapper与XML查询语句
李思佳 1 month ago
parent
commit
3e2d3ad17f

+ 2 - 7
LiangZhiYUMao/manifest.json

@@ -46,7 +46,7 @@
     },
     "quickapp" : {},
     "mp-weixin" : {
-        "appid" : "wx3e90d662a801266e",
+        "appid" : "",
         "setting" : {
             "urlCheck" : false,
             "postcss" : true,
@@ -54,12 +54,7 @@
             "es6" : true
         },
         "usingComponents" : true,
-        "lazyCodeLoading" : "requiredComponents",
-        "permission" : {
-            "scope.userLocation" : {
-                "desc" : "用于获取用户位置"
-            }
-        }
+        "lazyCodeLoading" : "requiredComponents"
     },
     "mp-alipay" : {
         "usingComponents" : true

+ 7 - 0
LiangZhiYUMao/pages.json

@@ -265,6 +265,13 @@
 				"navigationStyle": "custom"
 			}
 		},
+		{
+			"path": "pages/mine/my-dynamics",
+			"style": {
+				"navigationBarTitleText": "我的动态",
+				"navigationStyle": "custom"
+			}
+		},
 		{
 			"path": "pages/settings/id-verification",
 			"style": {

+ 23 - 0
LiangZhiYUMao/pages/mine/index.vue

@@ -158,6 +158,13 @@
 
 		<!-- 功能菜单列表 -->
 		<view class="menu-list">
+			<view class="menu-item" @click="goToPage('myDynamics')">
+				<view class="menu-left">
+					<text class="menu-icon">📝</text>
+					<text class="menu-text">我的动态</text>
+				</view>
+				<text class="menu-arrow">›</text>
+			</view>
 			<view class="menu-item" @click="goToPage('basicInfo')">
 				<view class="menu-left">
 					<text class="menu-icon">🆔</text>
@@ -961,6 +968,22 @@
 							})
 						}
 					})
+				} else if (page === 'myDynamics') {
+					console.log('✅ 跳转到我的动态页面')
+					// 跳转到我的动态页面
+					uni.navigateTo({
+						url: '/pages/mine/my-dynamics',
+						success: () => {
+							console.log('✅ 我的动态页面跳转成功')
+						},
+						fail: (err) => {
+							console.error('❌ 我的动态页面跳转失败:', err)
+							uni.showToast({
+								title: '页面跳转失败',
+								icon: 'none'
+							})
+						}
+					})
 				} else {
 					console.log('⚠️ 未知页面类型:', page)
 					uni.showToast({

+ 722 - 0
LiangZhiYUMao/pages/mine/my-dynamics.vue

@@ -0,0 +1,722 @@
+<template>
+  <view class="my-dynamics-page">
+    <!-- 顶部导航栏 -->
+    <view class="top-nav">
+      <view class="back-btn" @click="goBack">
+        <text class="back-icon">‹</text>
+      </view>
+      <view class="nav-title">我的动态</view>
+      <view class="nav-right"></view>
+    </view>
+
+    <!-- 用户信息区域 -->
+    <view class="user-info-section">
+      <image class="avatar" :src="userInfo.avatar" mode="aspectFill"></image>
+      <view class="user-details">
+        <text class="nickname">{{ userInfo.nickname }}</text>
+        <text class="dynamic-count">{{ userInfo.dynamicCount }} 动态</text>
+      </view>
+    </view>
+
+    <!-- 标签页导航 -->
+    <view class="tab-nav">
+      <view
+          class="tab-item"
+          :class="{ active: activeTab === 'dynamic' }"
+          @click="switchTab('dynamic')"
+      >
+        <text>动态</text>
+      </view>
+      <view
+          class="tab-item"
+          :class="{ active: activeTab === 'interaction' }"
+          @click="switchTab('interaction')"
+      >
+        <text>互动</text>
+      </view>
+      <view
+          class="tab-item"
+          :class="{ active: activeTab === 'browse' }"
+          @click="switchTab('browse')"
+      >
+        <text>浏览记录</text>
+      </view>
+    </view>
+
+    <!-- 互动子标签 -->
+    <view class="sub-tab-nav" v-if="activeTab === 'interaction'">
+      <view
+          class="sub-tab-item"
+          :class="{ active: activeSubTab === 'like' }"
+          @click="switchSubTab('like')"
+      >
+        <text>点赞</text>
+      </view>
+      <view
+          class="sub-tab-item"
+          :class="{ active: activeSubTab === 'collect' }"
+          @click="switchSubTab('collect')"
+      >
+        <text>收藏</text>
+      </view>
+    </view>
+
+    <!-- 内容区域 -->
+    <view class="content-area">
+      <!-- 动态列表 -->
+      <view v-if="activeTab === 'dynamic'" class="dynamic-list">
+        <view class="empty-tip" v-if="dynamicList.length === 0">
+          <text>暂无动态</text>
+        </view>
+        <view class="dynamic-item" v-for="item in dynamicList" :key="item.dynamicId" @click="goToDetail(item.dynamicId)">
+          <!-- 动态内容 -->
+          <view class="dynamic-content">
+            <text class="dynamic-text">{{ item.content }}</text>
+          </view>
+          <!-- 动态图片 -->
+          <view class="dynamic-images" v-if="item.mediaUrls">
+            <image
+                class="dynamic-image"
+                v-for="(img, index) in getMediaUrls(item.mediaUrls)"
+                :key="index"
+                :src="img"
+                mode="aspectFill"
+                @click.stop="previewImage(getMediaUrls(item.mediaUrls), index)"
+            ></image>
+          </view>
+          <!-- 动态信息 -->
+          <view class="dynamic-info">
+            <text class="dynamic-time">{{ formatTime(item.createdAt) }}</text>
+            <view class="dynamic-stats">
+              <text class="stat-item">{{ item.likeCount || 0 }} 赞</text>
+              <text class="stat-item">{{ item.commentCount || 0 }} 评论</text>
+            </view>
+          </view>
+        </view>
+      </view>
+
+      <!-- 互动列表 -->
+      <view v-else-if="activeTab === 'interaction'" class="interaction-list">
+        <view class="empty-tip" v-if="interactionList.length === 0">
+          <text>暂无互动</text>
+        </view>
+        <view class="interaction-item" v-for="item in interactionList" :key="item.dynamicId" @click="goToDetail(item.dynamicId)">
+          <!-- 用户信息 -->
+          <view class="user-info">
+            <image class="avatar" :src="getAvatar(item)" mode="aspectFill"></image>
+            <view class="user-details">
+              <text class="nickname">{{ getNickname(item) }}</text>
+            </view>
+          </view>
+          <!-- 互动内容 -->
+          <view class="interaction-content">
+            <text class="interaction-text">{{ item.content }}</text>
+          </view>
+          <!-- 互动图片 -->
+          <view class="interaction-images" v-if="item.mediaUrls">
+            <image
+                class="interaction-image"
+                v-for="(img, index) in getMediaUrls(item.mediaUrls)"
+                :key="index"
+                :src="img"
+                mode="aspectFill"
+                @click.stop="previewImage(getMediaUrls(item.mediaUrls), index)"
+            ></image>
+          </view>
+          <!-- 互动信息 -->
+          <view class="interaction-info">
+            <text class="interaction-time">{{ formatTime(item.createdAt) }}</text>
+            <view class="interaction-stats">
+              <text class="stat-item">{{ item.likeCount || 0 }} 赞</text>
+              <text class="stat-item">{{ item.commentCount || 0 }} 评论</text>
+            </view>
+          </view>
+        </view>
+      </view>
+
+      <!-- 浏览记录列表 -->
+      <view v-else-if="activeTab === 'browse'" class="browse-list">
+        <view class="empty-tip" v-if="browseList.length === 0">
+          <text>暂无浏览记录</text>
+        </view>
+        <view class="browse-item" v-for="item in browseList" :key="item.dynamicId" @click="goToDetail(item.dynamicId)">
+          <!-- 用户信息 -->
+          <view class="user-info">
+            <image class="avatar" :src="getAvatar(item)" mode="aspectFill"></image>
+            <view class="user-details">
+              <text class="nickname">{{ getNickname(item) }}</text>
+            </view>
+          </view>
+          <!-- 浏览内容 -->
+          <view class="browse-content">
+            <text class="browse-text">{{ item.content }}</text>
+          </view>
+          <!-- 浏览图片 -->
+          <view class="browse-images" v-if="item.mediaUrls">
+            <image
+                class="browse-image"
+                v-for="(img, index) in getMediaUrls(item.mediaUrls)"
+                :key="index"
+                :src="img"
+                mode="aspectFill"
+                @click.stop="previewImage(getMediaUrls(item.mediaUrls), index)"
+            ></image>
+          </view>
+          <!-- 浏览信息 -->
+          <view class="browse-info">
+            <text class="browse-time">{{ formatTime(item.createdAt) }}</text>
+            <view class="browse-stats">
+              <text class="stat-item">{{ item.likeCount || 0 }} 赞</text>
+              <text class="stat-item">{{ item.commentCount || 0 }} 评论</text>
+            </view>
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <!-- 发布动态按钮 -->
+    <view class="publish-btn" @click="goPublish">
+      <text class="publish-icon">+</text>
+    </view>
+  </view>
+</template>
+
+<script>
+import api from '../../utils/api.js'
+
+export default {
+  data() {
+        return {
+            // 用户信息
+            userInfo: {
+                avatar: 'https://via.placeholder.com/100',
+                nickname: '用户',
+                dynamicCount: 0
+            },
+            // 活跃标签页
+            activeTab: 'dynamic',
+            // 活跃子标签(互动下的点赞/收藏)
+            activeSubTab: 'like',
+            // 动态列表数据
+            dynamicList: [],
+            // 互动列表数据
+            interactionList: [],
+            // 浏览记录列表数据
+            browseList: [],
+            // 默认头像
+            defaultAvatar: 'https://via.placeholder.com/100x100.png?text=头像'
+        }
+    },
+  onLoad() {
+    // 页面加载时获取用户信息和动态数据
+    this.loadUserInfo();
+    this.loadDynamicData();
+  },
+  methods: {
+        // 返回上一页
+        goBack() {
+            uni.navigateBack();
+        },
+        // 获取头像
+        getAvatar(item) {
+            if (item && item.user && item.user.avatarUrl) {
+                return item.user.avatarUrl;
+            }
+            return this.defaultAvatar;
+        },
+        // 获取昵称
+        getNickname(item) {
+            if (item && item.user && item.user.nickname) {
+                return item.user.nickname;
+            }
+            return '匿名用户';
+        },
+    // 切换标签页
+    switchTab(tab) {
+      this.activeTab = tab;
+      // 根据标签页加载对应数据
+      if (tab === 'dynamic') {
+        this.loadDynamicData();
+      } else if (tab === 'interaction') {
+        this.loadInteractionData();
+      } else if (tab === 'browse') {
+        this.loadBrowseData();
+      }
+    },
+    // 切换互动子标签
+    switchSubTab(subTab) {
+      this.activeSubTab = subTab;
+      // 根据子标签加载对应数据
+      this.loadInteractionData();
+    },
+    // 加载用户信息
+    loadUserInfo() {
+      // 从本地存储获取用户信息
+      const userInfo = uni.getStorageSync('userInfo');
+      if (userInfo) {
+        this.userInfo = {
+          avatar: userInfo.avatarUrl || userInfo.avatar || 'https://via.placeholder.com/100',
+          nickname: userInfo.nickname || '用户',
+          dynamicCount: 0 // 后续从接口获取
+        };
+        // 加载用户动态数量
+        this.loadUserDynamicCount();
+      }
+    },
+    // 加载用户动态数量
+    loadUserDynamicCount() {
+      const userInfo = uni.getStorageSync('userInfo');
+      if (userInfo && userInfo.userId) {
+        // 获取用户动态数量
+        api.dynamic.getUserDynamics(userInfo.userId, { pageNum: 1, pageSize: 1 }).then(res => {
+          if (res && res.total) {
+            this.userInfo.dynamicCount = res.total;
+          }
+        }).catch(err => {
+          console.error('获取用户动态数量失败:', err);
+        });
+      }
+    },
+    // 加载动态数据
+    loadDynamicData() {
+      const userInfo = uni.getStorageSync('userInfo');
+      if (userInfo && userInfo.userId) {
+        // 获取用户发布的动态列表
+        api.dynamic.getUserDynamics(userInfo.userId, {
+          pageNum: 1,
+          pageSize: 10
+        }).then(res => {
+          if (res && res.records) {
+            this.dynamicList = res.records;
+          }
+        }).catch(err => {
+          console.error('获取用户动态列表失败:', err);
+        });
+      }
+    },
+    // 加载互动数据
+    loadInteractionData() {
+      const userInfo = uni.getStorageSync('userInfo');
+      if (userInfo && userInfo.userId) {
+        if (this.activeSubTab === 'like') {
+          // 获取用户点赞的动态列表
+          api.dynamic.getLikedList(userInfo.userId, 1, 10).then(res => {
+            if (res && res.records) {
+              this.interactionList = res.records;
+            }
+          }).catch(err => {
+            console.error('获取用户点赞列表失败:', err);
+          });
+        } else if (this.activeSubTab === 'collect') {
+          // 获取用户收藏的动态列表
+          api.dynamic.getFavoritesList(userInfo.userId, 1, 10).then(res => {
+            if (res && res.records) {
+              this.interactionList = res.records;
+            }
+          }).catch(err => {
+            console.error('获取用户收藏列表失败:', err);
+          });
+        }
+      }
+    },
+    // 加载浏览记录数据
+    loadBrowseData() {
+      const userInfo = uni.getStorageSync('userInfo');
+      if (userInfo && userInfo.userId) {
+        // 获取用户浏览记录列表
+        api.dynamic.getBrowseHistoryList(userInfo.userId, 1, 10).then(res => {
+          if (res && res.records) {
+            this.browseList = res.records;
+          }
+        }).catch(err => {
+          console.error('获取用户浏览记录失败:', err);
+        });
+      }
+    },
+    // 跳转到发布动态页面
+    goPublish() {
+      uni.navigateTo({
+        url: '/pages/plaza/publish'
+      });
+    },
+    // 跳转到动态详情页
+    goToDetail(dynamicId) {
+      uni.navigateTo({
+        url: `/pages/plaza/detail?id=${dynamicId}`
+      });
+    },
+    // 格式化时间
+    formatTime(timeStr) {
+      if (!timeStr) return '';
+      const date = new Date(timeStr);
+      const now = new Date();
+      const diff = now - date;
+      const minute = 60 * 1000;
+      const hour = minute * 60;
+      const day = hour * 24;
+      const month = day * 30;
+      const year = day * 365;
+      
+      if (diff < minute) {
+        return '刚刚';
+      } else if (diff < hour) {
+        return Math.floor(diff / minute) + '分钟前';
+      } else if (diff < day) {
+        return Math.floor(diff / hour) + '小时前';
+      } else if (diff < month) {
+        return Math.floor(diff / day) + '天前';
+      } else if (diff < year) {
+        return Math.floor(diff / month) + '个月前';
+      } else {
+        return Math.floor(diff / year) + '年前';
+      }
+    },
+    // 处理媒体URL,支持数组、JSON字符串、逗号分隔等多种格式
+    getMediaUrls(mediaUrls) {
+      if (!mediaUrls) return [];
+      
+      const isLikelyImage = (u) => {
+        if (typeof u !== 'string' || !u.trim()) return false;
+        const url = u.trim();
+        const hasExt = /(\.png|\.jpg|\.jpeg|\.gif|\.webp|\.bmp)(\?|$)/i.test(url);
+        const isHttp = /^https?:\/\//i.test(url);
+        return hasExt || isHttp;
+      };
+      
+      try {
+        if (Array.isArray(mediaUrls)) {
+          return mediaUrls.filter(isLikelyImage);
+        }
+        if (typeof mediaUrls === 'string') {
+          const s = mediaUrls.trim();
+          // JSON数组字符串: ["url1", "url2"]
+          if (s.startsWith('[')) {
+            const arr = JSON.parse(s);
+            return Array.isArray(arr) ? arr.filter(isLikelyImage) : [];
+          }
+          // 逗号分隔或带引号
+          return s.split(',')
+                  .map(x => x.trim().replace(/^\[|\]$/g, '').replace(/^['"]|['"]$/g, ''))
+                  .filter(isLikelyImage);
+        }
+      } catch (e) {
+        // ignore parse error
+        console.error('解析媒体URL失败:', e);
+      }
+      return [];
+    },
+    // 预览图片
+    previewImage(urls, current) {
+      uni.previewImage({
+        urls: urls,
+        current: current,
+        longPressActions: {
+          itemList: ['保存图片'],
+          success: function(data) {
+            console.log('长按图片操作结果:', data);
+          },
+          fail: function(err) {
+            console.error('长按图片操作失败:', err);
+          }
+        }
+      });
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.my-dynamics-page {
+  min-height: 100vh;
+  background: #F5F5F5;
+  padding-bottom: 100rpx;
+}
+
+/* 顶部导航栏 */
+.top-nav {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 40rpx 30rpx 20rpx;
+  background: #FFFFFF;
+  position: relative;
+  z-index: 10;
+
+  .back-btn {
+    width: 60rpx;
+    height: 60rpx;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    .back-icon {
+      font-size: 48rpx;
+      color: #333333;
+      font-weight: bold;
+    }
+  }
+
+  .nav-title {
+    font-size: 36rpx;
+    font-weight: bold;
+    color: #333333;
+  }
+
+  .nav-right {
+    width: 60rpx;
+  }
+}
+
+/* 用户信息区域 */
+.user-info-section {
+  display: flex;
+  align-items: center;
+  padding: 30rpx;
+  background: #FFFFFF;
+  margin-bottom: 20rpx;
+
+  .avatar {
+    width: 120rpx;
+    height: 120rpx;
+    border-radius: 60rpx;
+    margin-right: 20rpx;
+    background-color: #E0E0E0;
+  }
+
+  .user-details {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+
+    .nickname {
+      font-size: 32rpx;
+      font-weight: bold;
+      color: #333333;
+      margin-bottom: 8rpx;
+    }
+
+    .dynamic-count {
+      font-size: 24rpx;
+      color: #999999;
+    }
+  }
+}
+
+/* 标签页导航 */
+.tab-nav {
+  display: flex;
+  background: #FFFFFF;
+  border-bottom: 2rpx solid #F0F0F0;
+  margin-bottom: 20rpx;
+
+  .tab-item {
+    flex: 1;
+    text-align: center;
+    padding: 30rpx 0;
+    position: relative;
+
+    text {
+      font-size: 30rpx;
+      color: #666666;
+    }
+
+    &.active {
+      text {
+        color: #E91E63;
+        font-weight: bold;
+      }
+
+      &::after {
+        content: '';
+        position: absolute;
+        bottom: 0;
+        left: 50%;
+        transform: translateX(-50%);
+        width: 60rpx;
+        height: 6rpx;
+        background: #E91E63;
+        border-radius: 3rpx;
+      }
+    }
+  }
+}
+
+/* 互动子标签导航 */
+.sub-tab-nav {
+  display: flex;
+  background: #FFFFFF;
+  padding: 0 30rpx;
+  margin-bottom: 20rpx;
+
+  .sub-tab-item {
+    padding: 20rpx 30rpx;
+    margin-right: 40rpx;
+
+    text {
+      font-size: 26rpx;
+      color: #999999;
+    }
+
+    &.active {
+      text {
+        color: #E91E63;
+        font-weight: bold;
+      }
+    }
+  }
+}
+
+/* 内容区域 */
+.content-area {
+  padding: 0 30rpx;
+}
+
+/* 列表通用样式 */
+.dynamic-list,
+.interaction-list,
+.browse-list {
+  background: #FFFFFF;
+  border-radius: 12rpx;
+  overflow: hidden;
+}
+
+.dynamic-item,
+.interaction-item,
+.browse-item {
+  padding: 30rpx;
+  border-bottom: 2rpx solid #F5F5F5;
+
+  &:last-child {
+    border-bottom: none;
+  }
+}
+
+/* 用户信息 */
+.user-info {
+  display: flex;
+  align-items: center;
+  margin-bottom: 20rpx;
+
+  .avatar {
+    width: 80rpx;
+    height: 80rpx;
+    border-radius: 40rpx;
+    margin-right: 20rpx;
+    background-color: #E0E0E0;
+  }
+
+  .user-details {
+    display: flex;
+    flex-direction: column;
+
+    .nickname {
+      font-size: 28rpx;
+      font-weight: bold;
+      color: #333333;
+      margin-bottom: 4rpx;
+    }
+  }
+}
+
+/* 内容文本 */
+.dynamic-content,
+.interaction-content,
+.browse-content {
+  margin-bottom: 20rpx;
+
+  .dynamic-text,
+  .interaction-text,
+  .browse-text {
+    font-size: 28rpx;
+    color: #333333;
+    line-height: 44rpx;
+  }
+}
+
+/* 图片列表 */
+.dynamic-images,
+.interaction-images,
+.browse-images {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 15rpx;
+  margin-bottom: 20rpx;
+
+  .dynamic-image,
+  .interaction-image,
+  .browse-image {
+    width: calc((100% - 30rpx) / 3);
+    height: 200rpx;
+    border-radius: 8rpx;
+    background-color: #E0E0E0;
+  }
+}
+
+/* 信息栏 */
+.dynamic-info,
+.interaction-info,
+.browse-info {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+
+  .dynamic-time,
+  .interaction-time,
+  .browse-time {
+    font-size: 22rpx;
+    color: #999999;
+  }
+
+  .dynamic-stats,
+  .interaction-stats,
+  .browse-stats {
+    display: flex;
+    align-items: center;
+
+    .stat-item {
+      font-size: 22rpx;
+      color: #999999;
+      margin-left: 30rpx;
+    }
+  }
+}
+
+/* 空状态提示 */
+.empty-tip {
+  padding: 100rpx 0;
+  text-align: center;
+
+  text {
+    font-size: 28rpx;
+    color: #999999;
+  }
+}
+
+/* 发布动态按钮 */
+.publish-btn {
+  position: fixed;
+  bottom: 80rpx;
+  right: 40rpx;
+  width: 100rpx;
+  height: 100rpx;
+  background: #E91E63;
+  border-radius: 50rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  box-shadow: 0 8rpx 20rpx rgba(233, 30, 99, 0.3);
+  z-index: 999;
+
+  .publish-icon {
+    font-size: 60rpx;
+    color: #FFFFFF;
+    font-weight: bold;
+    line-height: 1;
+  }
+}
+
+/* 页面背景色 */
+.my-dynamics-page {
+  background-color: #F5F5F5;
+}
+</style>

+ 1 - 1
LiangZhiYUMao/project.private.config.json

@@ -17,7 +17,7 @@
     "useStaticServer": false,
     "useLanDebug": false,
     "ignoreDevUnusedFiles": true,
-    "bigPackageSizeSupport": false
+    "bigPackageSizeSupport": true
   },
   "localSetting": {
     "urlCheck": false

+ 21 - 0
LiangZhiYUMao/utils/api.js

@@ -487,6 +487,27 @@ export default {
       url: '/dynamic/report',
       method: 'POST',
       data
+    }),
+    
+    // 获取用户收藏列表
+    getFavoritesList: (userId, pageNum = 1, pageSize = 10) => request({
+      url: `/dynamic/favorites?userId=${userId}&pageNum=${pageNum}&pageSize=${pageSize}`
+    }),
+    
+    // 获取用户点赞列表
+    getLikedList: (userId, pageNum = 1, pageSize = 10) => request({
+      url: `/dynamic/likes?userId=${userId}&pageNum=${pageNum}&pageSize=${pageSize}`
+    }),
+    
+    // 获取用户浏览记录列表
+    getBrowseHistoryList: (userId, pageNum = 1, pageSize = 10) => request({
+      url: `/dynamic/browse-history?userId=${userId}&pageNum=${pageNum}&pageSize=${pageSize}`
+    }),
+    
+    // 清空用户浏览记录
+    clearBrowseHistory: (userId) => request({
+      url: `/dynamic/browse-history?userId=${userId}`,
+      method: 'DELETE'
     })
   }
 }

+ 1 - 10
gateway/src/main/resources/application.yml

@@ -103,14 +103,6 @@ spring:
           filters:
             - StripPrefix=0
 
-        # 聊天REST API路由
-        - id: chat-api-routev
-          uri: http://localhost:1004
-          predicates:
-            - Path=/api/chatfriend/**
-          filters:
-            - StripPrefix=0
-
         # 首页服务路由(兜底路由)
         - id: homepage-route
           uri: http://localhost:8081
@@ -118,8 +110,7 @@ spring:
             - Path=/api/**
           filters:
             - StripPrefix=0
-
-
+      
       # WebSocket支持配置
       websocket:
         enabled: true

+ 93 - 6
service/dynamic/src/main/java/com/zhentao/controller/DynamicController.java

@@ -4,11 +4,7 @@ import com.zhentao.common.PageResult;
 import com.zhentao.common.Result;
 import com.zhentao.dto.DynamicQueryDTO;
 import com.zhentao.dto.PublishDynamicDTO;
-import com.zhentao.service.PublishService;
-import com.zhentao.service.UserDynamicsService;
-import com.zhentao.service.InteractionService;
-import com.zhentao.service.CommentService;
-import com.zhentao.service.ReportService;
+import com.zhentao.service.*;
 import com.zhentao.entity.DynamicComments;
 import org.springframework.web.bind.annotation.RequestBody;
 import com.zhentao.dto.CommentCreateDTO;
@@ -43,6 +39,8 @@ public class DynamicController {
     private com.zhentao.service.MediaUploadService mediaUploadService;
     @Autowired
     private ReportService reportService;
+    @Autowired
+    private BrowseHistoryService browseHistoryService;
 
     /**
      * 分页查询动态列表
@@ -104,12 +102,40 @@ public class DynamicController {
     @GetMapping("/detail/{dynamicId}")
     public Result<DynamicVO> getDynamicDetail(
             @PathVariable Integer dynamicId,
-            @RequestParam(required = false) Integer userId) {
+            @RequestParam(required = false) Integer userId,
+            javax.servlet.http.HttpServletRequest request) {
         try {
             DynamicVO dynamic = userDynamicsService.getDynamicDetail(dynamicId, userId);
             if (dynamic == null) {
                 return Result.error(404, "动态不存在");
             }
+            
+            // 添加浏览记录(如果有用户ID)
+            if (userId != null) {
+                // 获取IP地址
+                String ip = request.getHeader("X-Forwarded-For");
+                if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+                    ip = request.getRemoteAddr();
+                } else {
+                    // 取第一个IP地址
+                    ip = ip.split(",")[0].trim();
+                }
+                
+                // 获取设备信息
+                String deviceInfo = request.getHeader("User-Agent");
+                
+                // 异步添加浏览记录,不影响主流程
+                String finalIp = ip;
+                new Thread(() -> {
+                    try {
+                        browseHistoryService.addBrowseHistory(userId, dynamicId, finalIp, deviceInfo);
+                    } catch (Exception e) {
+                        // 忽略异常,不影响主流程
+                        e.printStackTrace();
+                    }
+                }).start();
+            }
+            
             return Result.success(dynamic);
         } catch (Exception e) {
             return Result.error("查询动态详情失败:" + e.getMessage());
@@ -329,6 +355,67 @@ public class DynamicController {
             return Result.error("取消收藏失败:" + e.getMessage());
         }
     }
+    
+    /**
+     * 获取用户收藏列表
+     */
+    @GetMapping("/favorites")
+    public Result<PageResult<DynamicVO>> getFavoritesList(
+            @RequestParam Integer userId,
+            @RequestParam(defaultValue = "1") Integer pageNum,
+            @RequestParam(defaultValue = "10") Integer pageSize) {
+        try {
+            PageResult<DynamicVO> result = interactionService.listFavorites(userId, pageNum, pageSize);
+            return Result.success(result);
+        } catch (Exception e) {
+            return Result.error("获取收藏列表失败:" + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取用户点赞列表
+     */
+    @GetMapping("/likes")
+    public Result<PageResult<DynamicVO>> getLikedDynamicsList(
+            @RequestParam Integer userId,
+            @RequestParam(defaultValue = "1") Integer pageNum,
+            @RequestParam(defaultValue = "10") Integer pageSize) {
+        try {
+            PageResult<DynamicVO> result = interactionService.listLikedDynamics(userId, pageNum, pageSize);
+            return Result.success(result);
+        } catch (Exception e) {
+            return Result.error("获取点赞列表失败:" + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取用户浏览记录列表
+     */
+    @GetMapping("/browse-history")
+    public Result<PageResult<DynamicVO>> getBrowseHistoryList(
+            @RequestParam Integer userId,
+            @RequestParam(defaultValue = "1") Integer pageNum,
+            @RequestParam(defaultValue = "10") Integer pageSize) {
+        try {
+            PageResult<DynamicVO> result = browseHistoryService.getBrowseHistoryList(userId, pageNum, pageSize);
+            return Result.success(result);
+        } catch (Exception e) {
+            return Result.error("获取浏览记录失败:" + e.getMessage());
+        }
+    }
+    
+    /**
+     * 清空用户浏览记录
+     */
+    @DeleteMapping("/browse-history")
+    public Result<String> clearBrowseHistory(@RequestParam Integer userId) {
+        try {
+            browseHistoryService.clearBrowseHistory(userId);
+            return Result.success("OK", "success");
+        } catch (Exception e) {
+            return Result.error("清空浏览记录失败:" + e.getMessage());
+        }
+    }
 
     // ====== 举报接口 ======
     /**

+ 1 - 1
service/dynamic/src/main/java/com/zhentao/entity/DynamicLikes.java

@@ -13,7 +13,7 @@ public class DynamicLikes {
     /**
      * 主键ID
      */
-    @TableId(value = "like_id", type = IdType.AUTO)
+    @TableId(value = "id", type = IdType.AUTO)
     private Integer id;
 
     /**

+ 44 - 0
service/dynamic/src/main/java/com/zhentao/entity/UserBrowseHistory.java

@@ -0,0 +1,44 @@
+package com.zhentao.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+import java.time.LocalDateTime;
+
+/**
+ * 用户浏览历史实体类
+ */
+@Data
+@TableName("user_browse_history")
+public class UserBrowseHistory {
+    /**
+     * 记录ID
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    /**
+     * 用户ID
+     */
+    private Integer userId;
+
+    /**
+     * 动态ID
+     */
+    private Integer dynamicId;
+
+    /**
+     * 浏览时间
+     */
+    @TableField(fill = FieldFill.INSERT)
+    private LocalDateTime browseTime;
+
+    /**
+     * 浏览IP
+     */
+    private String browseIp;
+
+    /**
+     * 设备信息
+     */
+    private String deviceInfo;
+}

+ 10 - 0
service/dynamic/src/main/java/com/zhentao/mapper/UserBrowseHistoryMapper.java

@@ -0,0 +1,10 @@
+package com.zhentao.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zhentao.entity.UserBrowseHistory;
+
+/**
+ * 用户浏览历史Mapper
+ */
+public interface UserBrowseHistoryMapper extends BaseMapper<UserBrowseHistory> {
+}

+ 15 - 0
service/dynamic/src/main/java/com/zhentao/mapper/UserDynamicsMapper.java

@@ -7,6 +7,8 @@ import com.zhentao.entity.UserDynamics;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Mapper;
 
+import java.util.List;
+
 /**
  * 用户动态Mapper接口
  */
@@ -58,6 +60,19 @@ public interface UserDynamicsMapper extends BaseMapper<UserDynamics> {
     int updateDynamicStatus(@Param("dynamicId") Integer dynamicId,
                             @Param("status") Integer status,
                             @Param("remark") String remark);
+    
+    /**
+     * 根据动态ID列表查询动态详情(关联用户信息)
+     * @param page 分页对象
+     * @param dynamicIds 动态ID列表
+     * @param userId 当前登录用户ID
+     * @return 动态列表
+     */
+    IPage<UserDynamics> selectDynamicsByIds(
+            @Param("page") Page<UserDynamics> page,
+            @Param("dynamicIds") List<Integer> dynamicIds,
+            @Param("userId") Integer userId
+    );
 }
 
 

+ 34 - 0
service/dynamic/src/main/java/com/zhentao/service/BrowseHistoryService.java

@@ -0,0 +1,34 @@
+package com.zhentao.service;
+
+import com.zhentao.common.PageResult;
+import com.zhentao.vo.DynamicVO;
+
+/**
+ * 浏览历史服务接口
+ */
+public interface BrowseHistoryService {
+    
+    /**
+     * 添加浏览记录
+     * @param userId 用户ID
+     * @param dynamicId 动态ID
+     * @param browseIp 浏览IP
+     * @param deviceInfo 设备信息
+     */
+    void addBrowseHistory(Integer userId, Integer dynamicId, String browseIp, String deviceInfo);
+    
+    /**
+     * 获取用户浏览记录列表
+     * @param userId 用户ID
+     * @param pageNum 页码
+     * @param pageSize 每页大小
+     * @return 浏览记录列表
+     */
+    PageResult<DynamicVO> getBrowseHistoryList(Integer userId, Integer pageNum, Integer pageSize);
+    
+    /**
+     * 清空用户浏览记录
+     * @param userId 用户ID
+     */
+    void clearBrowseHistory(Integer userId);
+}

+ 5 - 0
service/dynamic/src/main/java/com/zhentao/service/InteractionService.java

@@ -32,6 +32,11 @@ public interface InteractionService {
      * 查询用户收藏列表(按时间倒序)
      */
     PageResult<DynamicVO> listFavorites(Integer userId, Integer pageNum, Integer pageSize);
+
+    /**
+     * 查询用户点赞列表(按时间倒序)
+     */
+    PageResult<DynamicVO> listLikedDynamics(Integer userId, Integer pageNum, Integer pageSize);
 }
 
 

+ 136 - 0
service/dynamic/src/main/java/com/zhentao/service/impl/BrowseHistoryServiceImpl.java

@@ -0,0 +1,136 @@
+package com.zhentao.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.zhentao.common.PageResult;
+import com.zhentao.entity.UserBrowseHistory;
+import com.zhentao.entity.UserDynamics;
+import com.zhentao.mapper.UserBrowseHistoryMapper;
+import com.zhentao.mapper.UserDynamicsMapper;
+import com.zhentao.service.BrowseHistoryService;
+import com.zhentao.vo.DynamicVO;
+import com.zhentao.vo.UserSimpleVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 浏览历史服务实现类
+ */
+@Service
+public class BrowseHistoryServiceImpl implements BrowseHistoryService {
+
+    @Autowired
+    private UserBrowseHistoryMapper browseHistoryMapper;
+    
+    @Autowired
+    private UserDynamicsMapper userDynamicsMapper;
+    
+    private static final ObjectMapper objectMapper = new ObjectMapper();
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void addBrowseHistory(Integer userId, Integer dynamicId, String browseIp, String deviceInfo) {
+        // 先检查是否已经存在相同的浏览记录(避免重复记录)
+        UserBrowseHistory existingRecord = browseHistoryMapper.selectOne(new QueryWrapper<UserBrowseHistory>()
+                .eq("user_id", userId)
+                .eq("dynamic_id", dynamicId));
+        
+        if (existingRecord != null) {
+            // 更新浏览时间
+            existingRecord.setBrowseTime(LocalDateTime.now());
+            existingRecord.setBrowseIp(browseIp);
+            existingRecord.setDeviceInfo(deviceInfo);
+            browseHistoryMapper.updateById(existingRecord);
+        } else {
+            // 添加新的浏览记录
+            UserBrowseHistory history = new UserBrowseHistory();
+            history.setUserId(userId);
+            history.setDynamicId(dynamicId);
+            history.setBrowseTime(LocalDateTime.now());
+            history.setBrowseIp(browseIp);
+            history.setDeviceInfo(deviceInfo);
+            browseHistoryMapper.insert(history);
+        }
+        
+        // 更新动态的浏览次数
+        userDynamicsMapper.update(null, new com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper<UserDynamics>()
+                .eq("dynamic_id", dynamicId)
+                .setSql("view_count = view_count + 1"));
+    }
+
+    @Override
+    public PageResult<DynamicVO> getBrowseHistoryList(Integer userId, Integer pageNum, Integer pageSize) {
+        // 查询用户浏览的动态ID列表(按浏览时间倒序)
+        List<Integer> browseDynamicIds = browseHistoryMapper.selectList(new QueryWrapper<UserBrowseHistory>()
+                .eq("user_id", userId)
+                .orderByDesc("browse_time"))
+                .stream()
+                .map(UserBrowseHistory::getDynamicId)
+                .collect(Collectors.toList());
+        
+        if (browseDynamicIds.isEmpty()) {
+            return new PageResult<DynamicVO>(new java.util.ArrayList<>(), 0L, Long.valueOf(pageNum), Long.valueOf(pageSize));
+        }
+        
+        // 查询动态详情(关联用户信息)
+        Page<UserDynamics> page = new Page<>(pageNum, pageSize);
+        com.baomidou.mybatisplus.core.metadata.IPage<UserDynamics> pg = userDynamicsMapper.selectDynamicsByIds(page, browseDynamicIds, userId);
+        
+        List<DynamicVO> list = pg.getRecords().stream()
+                .map(this::convertToVO)
+                .collect(Collectors.toList());
+        
+        return new PageResult<>(list, pg.getTotal(), pg.getCurrent(), pg.getSize());
+    }
+    
+    /**
+     * 将实体类转换为VO
+     */
+    private DynamicVO convertToVO(UserDynamics dynamic) {
+        DynamicVO vo = new DynamicVO();
+        org.springframework.beans.BeanUtils.copyProperties(dynamic, vo);
+
+        // 解析媒体URL JSON字符串
+        if (dynamic.getMediaUrls() != null && !dynamic.getMediaUrls().isEmpty()) {
+            try {
+                List<String> urls = objectMapper.readValue(
+                        dynamic.getMediaUrls(), 
+                        new TypeReference<List<String>>() {}
+                );
+                vo.setMediaUrls(urls);
+            } catch (Exception e) {
+                vo.setMediaUrls(new ArrayList<>());
+            }
+        } else {
+            vo.setMediaUrls(new ArrayList<>());
+        }
+
+        // 转换用户信息
+        if (dynamic.getUser() != null) {
+            UserSimpleVO userVO = new UserSimpleVO();
+            userVO.setUserId(dynamic.getUser().getUserId());
+            userVO.setNickname(dynamic.getUser().getNickname());
+            userVO.setAvatarUrl(dynamic.getUser().getAvatarUrl());
+            userVO.setGender(dynamic.getUser().getGender());
+            vo.setUser(userVO);
+        }
+
+        return vo;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void clearBrowseHistory(Integer userId) {
+        // 清空用户的所有浏览记录
+        browseHistoryMapper.delete(new QueryWrapper<UserBrowseHistory>()
+                .eq("user_id", userId));
+    }
+}

+ 86 - 7
service/dynamic/src/main/java/com/zhentao/service/impl/InteractionServiceImpl.java

@@ -3,6 +3,8 @@ package com.zhentao.service.impl;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import com.zhentao.common.PageResult;
 import com.zhentao.entity.DynamicFavorites;
 import com.zhentao.entity.DynamicLikes;
@@ -12,12 +14,16 @@ import com.zhentao.mapper.DynamicLikesMapper;
 import com.zhentao.mapper.UserDynamicsMapper;
 import com.zhentao.service.InteractionService;
 import com.zhentao.vo.DynamicVO;
+import com.zhentao.vo.UserSimpleVO;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.dao.DuplicateKeyException;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
+import java.util.ArrayList;
+import java.util.List;
+
 import java.time.LocalDateTime;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
@@ -35,6 +41,7 @@ public class InteractionServiceImpl implements InteractionService {
     @Autowired
     private RedisTemplate<String, Object> redisTemplate;
 
+    private static final ObjectMapper objectMapper = new ObjectMapper();
     private static final String LIKE_COUNT_KEY = "dynamic:like:";      // dynamic:like:{dynamicId}
     private static final String FAVORITE_COUNT_KEY = "dynamic:favorite:"; // dynamic:favorite:{dynamicId}
     private static final String LIKE_LOCK_KEY = "lock:dynamic:like:";  // lock:dynamic:like:{userId}:{dynamicId}
@@ -138,16 +145,88 @@ public class InteractionServiceImpl implements InteractionService {
 
     @Override
     public PageResult<DynamicVO> listFavorites(Integer userId, Integer pageNum, Integer pageSize) {
-        // 直接重用自定义SQL:按用户收藏时间倒序,拼接用户信息与点赞状态
+        // 查询用户收藏的动态ID列表
+        List<Integer> favoriteDynamicIds = favoritesMapper.selectList(new QueryWrapper<DynamicFavorites>()
+                .eq("user_id", userId)
+                .orderByDesc("created_at"))
+                .stream()
+                .map(DynamicFavorites::getDynamicId)
+                .collect(Collectors.toList());
+        
+        if (favoriteDynamicIds.isEmpty()) {
+            return new PageResult<DynamicVO>(new java.util.ArrayList<>(), 0L, Long.valueOf(pageNum), Long.valueOf(pageSize));
+        }
+        
+        // 查询动态详情(关联用户信息)
+        Page<UserDynamics> page = new Page<>(pageNum, pageSize);
+        IPage<UserDynamics> pg = userDynamicsMapper.selectDynamicsByIds(page, favoriteDynamicIds, userId);
+        
+        List<DynamicVO> list = pg.getRecords().stream()
+                .map(this::convertToVO)
+                .collect(Collectors.toList());
+        
+        return new PageResult<>(list, pg.getTotal(), pg.getCurrent(), pg.getSize());
+    }
+
+    @Override
+    public PageResult<DynamicVO> listLikedDynamics(Integer userId, Integer pageNum, Integer pageSize) {
+        // 查询用户点赞的动态ID列表
+        List<Integer> likedDynamicIds = likesMapper.selectList(new QueryWrapper<DynamicLikes>()
+                .eq("user_id", userId)
+                .orderByDesc("created_at"))
+                .stream()
+                .map(DynamicLikes::getDynamicId)
+                .collect(Collectors.toList());
+        
+        if (likedDynamicIds.isEmpty()) {
+            return new PageResult<DynamicVO>(new java.util.ArrayList<>(), 0L, Long.valueOf(pageNum), Long.valueOf(pageSize));
+        }
+        
+        // 查询动态详情(关联用户信息)
         Page<UserDynamics> page = new Page<>(pageNum, pageSize);
-        IPage<UserDynamics> pg = userDynamicsMapper.selectPage(page, new QueryWrapper<UserDynamics>());
-        List<DynamicVO> list = pg.getRecords().stream().map(u -> {
-            DynamicVO vo = new DynamicVO();
-            org.springframework.beans.BeanUtils.copyProperties(u, vo);
-            return vo;
-        }).collect(Collectors.toList());
+        IPage<UserDynamics> pg = userDynamicsMapper.selectDynamicsByIds(page, likedDynamicIds, userId);
+        
+        List<DynamicVO> list = pg.getRecords().stream()
+                .map(this::convertToVO)
+                .collect(Collectors.toList());
+        
         return new PageResult<>(list, pg.getTotal(), pg.getCurrent(), pg.getSize());
     }
+    
+    /**
+     * 将实体类转换为VO
+     */
+    private DynamicVO convertToVO(UserDynamics dynamic) {
+        DynamicVO vo = new DynamicVO();
+        org.springframework.beans.BeanUtils.copyProperties(dynamic, vo);
+
+        // 解析媒体URL JSON字符串
+        if (dynamic.getMediaUrls() != null && !dynamic.getMediaUrls().isEmpty()) {
+            try {
+                List<String> urls = objectMapper.readValue(
+                        dynamic.getMediaUrls(), 
+                        new TypeReference<List<String>>() {}
+                );
+                vo.setMediaUrls(urls);
+            } catch (Exception e) {
+                vo.setMediaUrls(new ArrayList<>());
+            }
+        } else {
+            vo.setMediaUrls(new ArrayList<>());
+        }
+
+        // 转换用户信息
+        if (dynamic.getUser() != null) {
+            UserSimpleVO userVO = new UserSimpleVO();
+            userVO.setUserId(dynamic.getUser().getUserId());
+            userVO.setNickname(dynamic.getUser().getNickname());
+            userVO.setAvatarUrl(dynamic.getUser().getAvatarUrl());
+            userVO.setGender(dynamic.getUser().getGender());
+            vo.setUser(userVO);
+        }
+
+        return vo;
+    }
 
     private void incrementCacheSafely(String key, long delta) {
         try {

+ 39 - 0
service/dynamic/src/main/resources/mapper/UserDynamicsMapper.xml

@@ -151,6 +151,45 @@
     WHERE dynamic_id = #{dynamicId}
   </update>
 
+  <!-- 根据动态ID列表查询动态详情(关联用户信息) -->
+  <select id="selectDynamicsByIds" resultMap="DynamicWithUserMap">
+    SELECT 
+        d.dynamic_id,
+        d.user_id,
+        d.content,
+        CAST(d.media_urls AS CHAR) as media_urls,
+        d.media_type,
+        d.audit_status,
+        d.audit_remark,
+        d.audit_time,
+        d.audit_admin_id,
+        d.like_count,
+        d.comment_count,
+        d.favorite_count,
+        d.share_count,
+        d.view_count,
+        d.status,
+        d.visibility,
+        d.created_at,
+        d.updated_at,
+        u.nickname,
+        u.avatar_url,
+        u.gender,
+        CASE WHEN dl.id IS NOT NULL THEN 1 ELSE 0 END as is_liked,
+        CASE WHEN df.id IS NOT NULL THEN 1 ELSE 0 END as is_favorited
+    FROM user_dynamics d
+    LEFT JOIN users u ON d.user_id = u.user_id
+    LEFT JOIN dynamic_likes dl ON d.dynamic_id = dl.dynamic_id AND dl.user_id = #{userId}
+    LEFT JOIN dynamic_favorites df ON d.dynamic_id = df.dynamic_id AND df.user_id = #{userId}
+    WHERE d.dynamic_id IN
+        <foreach collection="dynamicIds" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    AND d.status = 1
+    AND d.audit_status = 1
+    ORDER BY d.created_at DESC
+  </select>
+
 </mapper>