YH_0525 1 месяц назад
Родитель
Сommit
cf00014ba6

+ 21 - 0
LiangZhiYUMao/pages.json

@@ -105,6 +105,27 @@
 				"navigationStyle": "custom"
 			}
 		},
+		{
+			"path": "pages/matchmaker-workbench/product-detail",
+			"style": {
+				"navigationBarTitleText": "商品详情",
+				"navigationStyle": "custom"
+			}
+		},
+		{
+			"path": "pages/matchmaker-workbench/points-detail",
+			"style": {
+				"navigationBarTitleText": "积分明细",
+				"navigationStyle": "custom"
+			}
+		},
+		{
+			"path": "pages/matchmaker-workbench/earn-points",
+			"style": {
+				"navigationBarTitleText": "赚取积分",
+				"navigationStyle": "custom"
+			}
+		},
 		{
 			"path": "pages/page3/page3",
 			"style": {

+ 323 - 0
LiangZhiYUMao/pages/matchmaker-workbench/earn-points.vue

@@ -0,0 +1,323 @@
+<template>
+  <view class="earn-points">
+    <!-- 顶部导航栏 -->
+    <view class="header">
+      <view class="back-icon" @click="handleBack"></view>
+      <text class="header-title">赚取积分</text>
+      <view class="header-right"></view>
+    </view>
+
+    <!-- 当前积分 -->
+    <view class="current-points">
+      <text class="label">当前积分</text>
+      <text class="value">{{ balance }}</text>
+    </view>
+
+    <!-- 积分规则列表 -->
+    <view class="rules-section">
+      <view class="section-title">积分获取方式</view>
+      
+      <view class="rule-list">
+        <view class="rule-item" v-for="(rule, index) in rules" :key="index">
+          <view class="rule-icon">{{ getIcon(rule.ruleType) }}</view>
+          <view class="rule-info">
+            <view class="rule-name">{{ rule.ruleName }}</view>
+            <view class="rule-desc">{{ getDesc(rule.ruleType) }}</view>
+          </view>
+          <view class="rule-points">+{{ rule.pointValue }}</view>
+          <view class="rule-action" @click="handleAction(rule)">
+            {{ getActionText(rule.ruleType) }}
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <!-- 积分说明 -->
+    <view class="tips-section">
+      <view class="section-title">积分说明</view>
+      <view class="tips-content">
+        <view class="tip-item">1. 积分可在积分商城兑换精美礼品</view>
+        <view class="tip-item">2. 每日签到可获得积分奖励</view>
+        <view class="tip-item">3. 上传优质线索可获得额外积分</view>
+        <view class="tip-item">4. 撮合成功可获得大量积分奖励</view>
+        <view class="tip-item">5. 积分永久有效,请放心使用</view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import api from '@/utils/api.js'
+
+export default {
+  name: 'earn-points',
+  data() {
+    return {
+      balance: 0,
+      rules: [],
+      makerId: null
+    }
+  },
+  onLoad() {
+    this.initData()
+  },
+  methods: {
+    async initData() {
+      const userInfo = uni.getStorageSync('userInfo')
+      if (userInfo && userInfo.matchmakerId) {
+        this.makerId = userInfo.matchmakerId
+      } else if (userInfo && userInfo.userId) {
+        this.makerId = userInfo.userId
+      }
+      
+      await Promise.all([
+        this.loadBalance(),
+        this.loadRules()
+      ])
+    },
+    
+    async loadBalance() {
+      if (!this.makerId) return
+      try {
+        const res = await api.pointsMall.getBalance(this.makerId)
+        this.balance = res.balance || 0
+      } catch (e) {
+        console.error('获取积分余额失败:', e)
+      }
+    },
+    
+    async loadRules() {
+      try {
+        const res = await api.pointsMall.getRules()
+        this.rules = res || []
+      } catch (e) {
+        console.error('获取积分规则失败:', e)
+      }
+    },
+    
+    handleBack() {
+      uni.navigateBack()
+    },
+    
+    getIcon(ruleType) {
+      const icons = {
+        1: '📅', // 签到
+        2: '📤', // 上传线索
+        3: '💕', // 撮合成功
+        4: '📚', // 完成培训
+        5: '🎁'  // 活动奖励
+      }
+      return icons[ruleType] || '⭐'
+    },
+    
+    getDesc(ruleType) {
+      const descs = {
+        1: '每日签到即可获得积分',
+        2: '上传优质客户线索',
+        3: '成功撮合一对情侣',
+        4: '完成红娘培训课程',
+        5: '参与平台活动获得'
+      }
+      return descs[ruleType] || '完成任务获得积分'
+    },
+    
+    getActionText(ruleType) {
+      const texts = {
+        1: '去签到',
+        2: '去上传',
+        3: '查看',
+        4: '去学习',
+        5: '查看'
+      }
+      return texts[ruleType] || '去完成'
+    },
+    
+    async handleAction(rule) {
+      if (rule.ruleType === 1) {
+        // 签到
+        await this.doSignIn(rule)
+      } else if (rule.ruleType === 2) {
+        // 上传线索
+        uni.navigateTo({
+          url: '/pages/matchmaker-workbench/add-resource'
+        })
+      } else if (rule.ruleType === 4) {
+        // 培训课程
+        uni.navigateTo({
+          url: '/pages/courses/list'
+        })
+      } else {
+        uni.showToast({
+          title: '功能开发中',
+          icon: 'none'
+        })
+      }
+    },
+    
+    async doSignIn(rule) {
+      if (!this.makerId) {
+        uni.showToast({ title: '请先登录', icon: 'none' })
+        return
+      }
+      
+      try {
+        const res = await api.pointsMall.addPoints(this.makerId, rule.ruleType, '每日签到')
+        uni.showToast({
+          title: `签到成功,+${res.addedPoints}积分`,
+          icon: 'success'
+        })
+        this.balance = res.newBalance
+      } catch (e) {
+        console.error('签到失败:', e)
+        uni.showToast({
+          title: e.message || '签到失败',
+          icon: 'none'
+        })
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.earn-points {
+  min-height: 100vh;
+  background: #F5F5F5;
+}
+
+.header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 25rpx 30rpx;
+  padding-top: calc(25rpx + env(safe-area-inset-top));
+  background: #FFFFFF;
+  
+  .back-icon {
+    width: 44rpx;
+    height: 44rpx;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 32rpx;
+    color: #333;
+    &::before { content: '‹'; }
+  }
+  
+  .header-title {
+    font-size: 36rpx;
+    font-weight: bold;
+    color: #333;
+  }
+  
+  .header-right {
+    width: 44rpx;
+  }
+}
+
+.current-points {
+  background: linear-gradient(135deg, #9C27B0 0%, #E91E63 100%);
+  margin: 30rpx;
+  padding: 40rpx;
+  border-radius: 20rpx;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  
+  .label {
+    font-size: 30rpx;
+    color: rgba(255, 255, 255, 0.8);
+  }
+  
+  .value {
+    font-size: 56rpx;
+    font-weight: bold;
+    color: #FFFFFF;
+  }
+}
+
+.rules-section {
+  background: #FFFFFF;
+  margin: 0 30rpx 30rpx;
+  border-radius: 20rpx;
+  padding: 30rpx;
+  
+  .section-title {
+    font-size: 30rpx;
+    font-weight: bold;
+    color: #333;
+    margin-bottom: 30rpx;
+  }
+}
+
+.rule-list {
+  .rule-item {
+    display: flex;
+    align-items: center;
+    padding: 25rpx 0;
+    border-bottom: 1rpx solid #F0F0F0;
+    
+    &:last-child {
+      border-bottom: none;
+    }
+    
+    .rule-icon {
+      font-size: 48rpx;
+      margin-right: 20rpx;
+    }
+    
+    .rule-info {
+      flex: 1;
+      
+      .rule-name {
+        font-size: 28rpx;
+        font-weight: bold;
+        color: #333;
+        margin-bottom: 8rpx;
+      }
+      
+      .rule-desc {
+        font-size: 24rpx;
+        color: #999;
+      }
+    }
+    
+    .rule-points {
+      font-size: 32rpx;
+      font-weight: bold;
+      color: #FF9800;
+      margin-right: 20rpx;
+    }
+    
+    .rule-action {
+      padding: 12rpx 24rpx;
+      background: linear-gradient(135deg, #9C27B0 0%, #E91E63 100%);
+      color: #FFFFFF;
+      font-size: 24rpx;
+      border-radius: 30rpx;
+    }
+  }
+}
+
+.tips-section {
+  background: #FFFFFF;
+  margin: 0 30rpx 30rpx;
+  border-radius: 20rpx;
+  padding: 30rpx;
+  
+  .section-title {
+    font-size: 30rpx;
+    font-weight: bold;
+    color: #333;
+    margin-bottom: 20rpx;
+  }
+  
+  .tips-content {
+    .tip-item {
+      font-size: 26rpx;
+      color: #666;
+      line-height: 2;
+    }
+  }
+}
+</style>

+ 282 - 0
LiangZhiYUMao/pages/matchmaker-workbench/points-detail.vue

@@ -0,0 +1,282 @@
+<template>
+  <view class="points-detail">
+    <!-- 顶部导航栏 -->
+    <view class="header">
+      <view class="back-icon" @click="handleBack"></view>
+      <text class="header-title">积分明细</text>
+      <view class="header-right"></view>
+    </view>
+
+    <!-- 积分余额卡片 -->
+    <view class="balance-card">
+      <view class="balance-label">当前积分</view>
+      <view class="balance-value">{{ balance }}</view>
+    </view>
+
+    <!-- 明细列表 -->
+    <view class="records-section">
+      <view class="section-title">积分记录</view>
+      
+      <view v-if="records.length === 0 && !loading" class="empty-tip">
+        暂无积分记录
+      </view>
+      
+      <view class="record-list">
+        <view class="record-item" v-for="(item, index) in records" :key="index">
+          <view class="record-left">
+            <view class="record-icon" :class="item.points > 0 ? 'income' : 'expense'">
+              {{ item.points > 0 ? '+' : '-' }}
+            </view>
+            <view class="record-info">
+              <view class="record-reason">{{ item.reason }}</view>
+              <view class="record-time">{{ formatTime(item.createTime) }}</view>
+            </view>
+          </view>
+          <view class="record-points" :class="item.points > 0 ? 'income' : 'expense'">
+            {{ item.points > 0 ? '+' : '' }}{{ item.points }}
+          </view>
+        </view>
+      </view>
+      
+      <!-- 加载更多 -->
+      <view class="load-more" v-if="hasMore" @click="loadMore">
+        {{ loading ? '加载中...' : '加载更多' }}
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import api from '@/utils/api.js'
+
+export default {
+  name: 'points-detail',
+  data() {
+    return {
+      balance: 0,
+      records: [],
+      makerId: null,
+      pageNum: 1,
+      pageSize: 20,
+      total: 0,
+      loading: false
+    }
+  },
+  computed: {
+    hasMore() {
+      return this.records.length < this.total
+    }
+  },
+  onLoad() {
+    this.initData()
+  },
+  methods: {
+    async initData() {
+      const userInfo = uni.getStorageSync('userInfo')
+      if (userInfo && userInfo.matchmakerId) {
+        this.makerId = userInfo.matchmakerId
+      } else if (userInfo && userInfo.userId) {
+        this.makerId = userInfo.userId
+      }
+      
+      await this.loadRecords()
+    },
+    
+    async loadRecords() {
+      if (!this.makerId || this.loading) return
+      
+      this.loading = true
+      try {
+        const res = await api.pointsMall.getRecords(this.makerId, this.pageNum, this.pageSize)
+        this.balance = res.balance || 0
+        this.total = res.total || 0
+        
+        if (this.pageNum === 1) {
+          this.records = res.list || []
+        } else {
+          this.records = [...this.records, ...(res.list || [])]
+        }
+      } catch (e) {
+        console.error('获取积分明细失败:', e)
+        uni.showToast({ title: '获取积分明细失败', icon: 'none' })
+      } finally {
+        this.loading = false
+      }
+    },
+    
+    loadMore() {
+      if (this.hasMore && !this.loading) {
+        this.pageNum++
+        this.loadRecords()
+      }
+    },
+    
+    handleBack() {
+      uni.navigateBack()
+    },
+    
+    formatTime(timeStr) {
+      if (!timeStr) return ''
+      const date = new Date(timeStr)
+      const year = date.getFullYear()
+      const month = String(date.getMonth() + 1).padStart(2, '0')
+      const day = String(date.getDate()).padStart(2, '0')
+      const hour = String(date.getHours()).padStart(2, '0')
+      const minute = String(date.getMinutes()).padStart(2, '0')
+      return `${year}-${month}-${day} ${hour}:${minute}`
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.points-detail {
+  min-height: 100vh;
+  background: #F5F5F5;
+}
+
+.header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 25rpx 30rpx;
+  padding-top: calc(25rpx + env(safe-area-inset-top));
+  background: #FFFFFF;
+  
+  .back-icon {
+    width: 44rpx;
+    height: 44rpx;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 32rpx;
+    color: #333;
+    &::before { content: '‹'; }
+  }
+  
+  .header-title {
+    font-size: 36rpx;
+    font-weight: bold;
+    color: #333;
+  }
+  
+  .header-right {
+    width: 44rpx;
+  }
+}
+
+.balance-card {
+  background: linear-gradient(135deg, #9C27B0 0%, #E91E63 100%);
+  margin: 30rpx;
+  padding: 40rpx;
+  border-radius: 20rpx;
+  text-align: center;
+  
+  .balance-label {
+    font-size: 28rpx;
+    color: rgba(255, 255, 255, 0.8);
+    margin-bottom: 15rpx;
+  }
+  
+  .balance-value {
+    font-size: 72rpx;
+    font-weight: bold;
+    color: #FFFFFF;
+  }
+}
+
+.records-section {
+  background: #FFFFFF;
+  margin: 0 30rpx;
+  border-radius: 20rpx;
+  padding: 30rpx;
+  
+  .section-title {
+    font-size: 30rpx;
+    font-weight: bold;
+    color: #333;
+    margin-bottom: 30rpx;
+  }
+  
+  .empty-tip {
+    text-align: center;
+    color: #999;
+    font-size: 28rpx;
+    padding: 60rpx 0;
+  }
+}
+
+.record-list {
+  .record-item {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 25rpx 0;
+    border-bottom: 1rpx solid #F0F0F0;
+    
+    &:last-child {
+      border-bottom: none;
+    }
+    
+    .record-left {
+      display: flex;
+      align-items: center;
+      
+      .record-icon {
+        width: 60rpx;
+        height: 60rpx;
+        border-radius: 50%;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        font-size: 32rpx;
+        font-weight: bold;
+        margin-right: 20rpx;
+        
+        &.income {
+          background: #E8F5E9;
+          color: #4CAF50;
+        }
+        
+        &.expense {
+          background: #FFEBEE;
+          color: #F44336;
+        }
+      }
+      
+      .record-info {
+        .record-reason {
+          font-size: 28rpx;
+          color: #333;
+          margin-bottom: 8rpx;
+        }
+        
+        .record-time {
+          font-size: 24rpx;
+          color: #999;
+        }
+      }
+    }
+    
+    .record-points {
+      font-size: 32rpx;
+      font-weight: bold;
+      
+      &.income {
+        color: #4CAF50;
+      }
+      
+      &.expense {
+        color: #F44336;
+      }
+    }
+  }
+}
+
+.load-more {
+  text-align: center;
+  padding: 30rpx;
+  color: #9C27B0;
+  font-size: 26rpx;
+}
+</style>

+ 159 - 55
LiangZhiYUMao/pages/matchmaker-workbench/points-mall.vue

@@ -38,27 +38,29 @@
       </view>
     </view>
 
-    <!-- 推荐商品区域 -->
+    <!-- 商品区域 -->
     <scroll-view scroll-y class="products-container">
-      <!-- 推荐商品标题 -->
+      <!-- 商品标题 -->
       <view class="section-header">
-        <text class="section-title">推荐商品</text>
-        <view class="view-more" @click="handleViewMore">
-          <text class="view-more-text">查看更多</text>
-          <text class="arrow">›</text>
-        </view>
+        <text class="section-title">{{ activeCategory === 0 ? '全部商品' : categories[activeCategory] }}</text>
+        <text class="product-count">共 {{ displayProducts.length }} 件</text>
+      </view>
+
+      <!-- 空状态 -->
+      <view v-if="displayProducts.length === 0 && !loading" class="empty-state">
+        <text>暂无商品</text>
       </view>
 
       <!-- 商品网格 -->
       <view class="products-grid">
         <view 
-          v-for="(product, index) in recommendedProducts" 
-          :key="index"
+          v-for="(product, index) in displayProducts" 
+          :key="product.id"
           class="product-card"
           @click="handleProductClick(product)"
         >
           <view class="product-image">
-            <image :src="product.image" mode="aspectFill"></image>
+            <image :src="product.image || defaultImage" mode="aspectFit" @error="handleImageError($event, index)"></image>
           </view>
           <view class="product-info">
             <text class="product-name">{{ product.name }}</text>
@@ -77,75 +79,145 @@
 </template>
 
 <script>
+import api from '@/utils/api.js'
+
 export default {
   name: 'points-mall',
   data() {
     return {
-      userPoints: 5000,
+      userPoints: 0,
       cartCount: 0,
       activeCategory: 0,
-      categories: ['全部',  '实物商品', '虚拟商品'],
-      recommendedProducts: [
-        {
-          id: 1,
-          name: '清风原色无尘纸',
-          points: 1000,
-          image: 'https://via.placeholder.com/200x200?text=纸巾'
-        },
-        {
-          id: 2,
-          name: 'OPPO Enco Air2耳机',
-          points: 7000,
-          image: 'https://via.placeholder.com/200x200?text=耳机'
-        },
-        {
-          id: 3,
-          name: '五常大米',
-          points: 3000,
-          image: 'https://via.placeholder.com/200x200?text=大米'
-        },
-        {
-          id: 4,
-          name: '脸盆',
-          points: 30,
-          image: 'https://via.placeholder.com/200x200?text=脸盆'
-        }
-      ]
+      categories: ['全部', '实物商品', '虚拟商品'],
+      recommendedProducts: [],
+      allProducts: [],
+      makerId: null,
+      loading: false,
+      defaultImage: 'https://img.zcool.cn/community/01b8d95d3f8a8fa801211d53e8c6b0.jpg@1280w_1l_2o_100sh.jpg'
+    }
+  },
+  computed: {
+    // 根据分类筛选商品
+    displayProducts() {
+      if (this.activeCategory === 0) {
+        return this.allProducts
+      }
+      return this.allProducts.filter(p => p.category === this.activeCategory)
+    }
+  },
+  onLoad() {
+    this.initData()
+  },
+  onShow() {
+    // 每次显示页面时刷新积分余额
+    if (this.makerId) {
+      this.loadBalance()
     }
   },
   methods: {
+    // 初始化数据
+    async initData() {
+      // 获取红娘ID
+      const userInfo = uni.getStorageSync('userInfo')
+      if (userInfo && userInfo.matchmakerId) {
+        this.makerId = userInfo.matchmakerId
+      } else if (userInfo && userInfo.userId) {
+        this.makerId = userInfo.userId
+      }
+      
+      await Promise.all([
+        this.loadBalance(),
+        this.loadProducts(),
+        this.loadRecommendProducts()
+      ])
+    },
+    
+    // 加载积分余额
+    async loadBalance() {
+      if (!this.makerId) return
+      try {
+        const res = await api.pointsMall.getBalance(this.makerId)
+        this.userPoints = res.balance || 0
+      } catch (e) {
+        console.error('获取积分余额失败:', e)
+      }
+    },
+    
+    // 加载商品列表
+    async loadProducts() {
+      this.loading = true
+      try {
+        const res = await api.pointsMall.getProducts({ pageNum: 1, pageSize: 50 })
+        this.allProducts = (res.list || []).map(item => ({
+          id: item.id,
+          name: item.name,
+          points: item.pointsPrice,
+          image: item.imageUrl || this.defaultImage,
+          category: item.category,
+          stock: item.stock,
+          description: item.description
+        }))
+      } catch (e) {
+        console.error('获取商品列表失败:', e)
+      } finally {
+        this.loading = false
+      }
+    },
+    
+    // 加载推荐商品
+    async loadRecommendProducts() {
+      try {
+        const res = await api.pointsMall.getRecommendProducts(10)
+        this.recommendedProducts = (res || []).map(item => ({
+          id: item.id,
+          name: item.name,
+          points: item.pointsPrice,
+          image: item.imageUrl || this.defaultImage,
+          category: item.category,
+          stock: item.stock
+        }))
+      } catch (e) {
+        console.error('获取推荐商品失败:', e)
+      }
+    },
+    
     // 返回上一页
     handleBack() {
       uni.navigateBack()
     },
+    
     // 查看积分明细
     handlePointsDetail() {
-      uni.showToast({
-        title: '查看积分明细',
-        icon: 'none'
+      uni.navigateTo({
+        url: '/pages/matchmaker-workbench/points-detail'
       })
     },
+    
     // 赚取积分
     handleEarnPoints() {
-      uni.showToast({
-        title: '赚取积分',
-        icon: 'none'
+      uni.navigateTo({
+        url: '/pages/matchmaker-workbench/earn-points'
       })
     },
+    
     // 查看更多
     handleViewMore() {
-      uni.showToast({
-        title: '查看更多商品',
-        icon: 'none'
-      })
+      // 切换到全部分类
+      this.activeCategory = 0
     },
-    // 商品点击
+    
+    // 商品点击 - 跳转到商品详情
     handleProductClick(product) {
-      uni.showToast({
-        title: `已添加 ${product.name} 到购物车`,
-        icon: 'success'
+      uni.navigateTo({
+        url: `/pages/matchmaker-workbench/product-detail?id=${product.id}`
       })
-      this.cartCount++
+    },
+    
+    // 图片加载失败处理
+    handleImageError(e, index) {
+      if (this.recommendedProducts[index]) {
+        this.$set(this.recommendedProducts[index], 'image', this.defaultImage)
+      }
     }
   }
 }
@@ -336,6 +408,11 @@ export default {
     color: #333;
   }
 
+  .product-count {
+    font-size: 24rpx;
+    color: #999;
+  }
+
   .view-more {
     display: flex;
     align-items: center;
@@ -353,6 +430,31 @@ export default {
   }
 }
 
+/* 空状态 */
+.empty-state {
+  text-align: center;
+  padding: 100rpx 0;
+  color: #999;
+  font-size: 28rpx;
+}
+
+/* 废弃的view-more样式 */
+.view-more-deprecated {
+  display: flex;
+  align-items: center;
+  gap: 5rpx;
+
+  .view-more-text {
+    font-size: 24rpx;
+    color: #9C27B0;
+  }
+
+  .arrow {
+    font-size: 28rpx;
+    color: #9C27B0;
+  }
+}
+
 /* 商品网格 */
 .products-grid {
   display: grid;
@@ -374,9 +476,11 @@ export default {
 
     .product-image {
       width: 100%;
-      height: 200rpx;
+      height: 300rpx;
       background: #F5F5F5;
-      overflow: hidden;
+      display: flex;
+      align-items: center;
+      justify-content: center;
 
       image {
         width: 100%;

+ 392 - 0
LiangZhiYUMao/pages/matchmaker-workbench/product-detail.vue

@@ -0,0 +1,392 @@
+<template>
+  <view class="product-detail">
+    <!-- 顶部导航栏 -->
+    <view class="header">
+      <view class="back-icon" @click="handleBack"></view>
+      <text class="header-title">商品详情</text>
+      <view class="header-right"></view>
+    </view>
+
+    <!-- 商品图片 -->
+    <view class="product-image-section">
+      <image :src="product.imageUrl || '/static/default-product.png'" mode="aspectFit" class="product-image"></image>
+    </view>
+
+    <!-- 商品信息 -->
+    <view class="product-info-section">
+      <view class="product-name">{{ product.name }}</view>
+      <view class="product-price">
+        <text class="price-value">{{ product.pointsPrice }}</text>
+        <text class="price-unit">积分</text>
+      </view>
+      <view class="product-stock">库存: {{ product.stock || 0 }} 件</view>
+    </view>
+
+    <!-- 商品描述 -->
+    <view class="product-desc-section">
+      <view class="section-title">商品描述</view>
+      <view class="desc-content">{{ product.description || '暂无描述' }}</view>
+    </view>
+
+    <!-- 收货信息 -->
+    <view class="receiver-section" v-if="product.category === 1">
+      <view class="section-title">收货信息</view>
+      <view class="form-item">
+        <text class="label">收货人</text>
+        <input class="input" v-model="receiverName" placeholder="请输入收货人姓名" />
+      </view>
+      <view class="form-item">
+        <text class="label">联系电话</text>
+        <input class="input" v-model="receiverPhone" placeholder="请输入联系电话" type="number" />
+      </view>
+      <view class="form-item">
+        <text class="label">收货地址</text>
+        <textarea class="textarea" v-model="receiverAddress" placeholder="请输入详细收货地址"></textarea>
+      </view>
+    </view>
+
+    <!-- 底部操作栏 -->
+    <view class="bottom-bar">
+      <view class="points-info">
+        <text class="label">我的积分:</text>
+        <text class="value">{{ userPoints }}</text>
+      </view>
+      <view class="exchange-btn" :class="{ disabled: !canExchange }" @click="handleExchange">
+        立即兑换
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import api from '@/utils/api.js'
+
+export default {
+  name: 'product-detail',
+  data() {
+    return {
+      productId: null,
+      product: {},
+      userPoints: 0,
+      makerId: null,
+      receiverName: '',
+      receiverPhone: '',
+      receiverAddress: '',
+      loading: false
+    }
+  },
+  computed: {
+    canExchange() {
+      if (this.loading) return false
+      if (!this.product.pointsPrice) return false
+      if (this.userPoints < this.product.pointsPrice) return false
+      if (this.product.stock <= 0) return false
+      // 实物商品需要填写收货信息
+      if (this.product.category === 1) {
+        if (!this.receiverName || !this.receiverPhone || !this.receiverAddress) {
+          return false
+        }
+      }
+      return true
+    }
+  },
+  onLoad(options) {
+    if (options.id) {
+      this.productId = options.id
+    }
+    this.initData()
+  },
+  methods: {
+    async initData() {
+      // 获取红娘ID
+      const userInfo = uni.getStorageSync('userInfo')
+      if (userInfo && userInfo.matchmakerId) {
+        this.makerId = userInfo.matchmakerId
+      } else if (userInfo && userInfo.userId) {
+        this.makerId = userInfo.userId
+      }
+      
+      await Promise.all([
+        this.loadProduct(),
+        this.loadBalance()
+      ])
+    },
+    
+    async loadProduct() {
+      if (!this.productId) return
+      try {
+        const res = await api.pointsMall.getProductDetail(this.productId)
+        this.product = res || {}
+      } catch (e) {
+        console.error('获取商品详情失败:', e)
+        uni.showToast({ title: '获取商品详情失败', icon: 'none' })
+      }
+    },
+    
+    async loadBalance() {
+      if (!this.makerId) return
+      try {
+        const res = await api.pointsMall.getBalance(this.makerId)
+        this.userPoints = res.balance || 0
+      } catch (e) {
+        console.error('获取积分余额失败:', e)
+      }
+    },
+    
+    handleBack() {
+      uni.navigateBack()
+    },
+    
+    async handleExchange() {
+      if (!this.canExchange) {
+        if (this.userPoints < this.product.pointsPrice) {
+          uni.showToast({ title: '积分不足', icon: 'none' })
+        } else if (this.product.stock <= 0) {
+          uni.showToast({ title: '库存不足', icon: 'none' })
+        } else if (this.product.category === 1 && (!this.receiverName || !this.receiverPhone || !this.receiverAddress)) {
+          uni.showToast({ title: '请填写完整收货信息', icon: 'none' })
+        }
+        return
+      }
+      
+      uni.showModal({
+        title: '确认兑换',
+        content: `确定使用 ${this.product.pointsPrice} 积分兑换「${this.product.name}」吗?`,
+        success: async (res) => {
+          if (res.confirm) {
+            await this.doExchange()
+          }
+        }
+      })
+    },
+    
+    async doExchange() {
+      this.loading = true
+      try {
+        const data = {
+          makerId: this.makerId,
+          productId: this.productId,
+          quantity: 1,
+          receiverName: this.receiverName,
+          receiverPhone: this.receiverPhone,
+          receiverAddress: this.receiverAddress
+        }
+        
+        await api.pointsMall.exchange(data)
+        
+        uni.showToast({ title: '兑换成功', icon: 'success' })
+        
+        // 刷新积分余额
+        await this.loadBalance()
+        
+        // 延迟返回
+        setTimeout(() => {
+          uni.navigateBack()
+        }, 1500)
+      } catch (e) {
+        console.error('兑换失败:', e)
+        uni.showToast({ title: e.message || '兑换失败', icon: 'none' })
+      } finally {
+        this.loading = false
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.product-detail {
+  min-height: 100vh;
+  background: #F5F5F5;
+  padding-bottom: 120rpx;
+}
+
+.header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 25rpx 30rpx;
+  padding-top: calc(25rpx + env(safe-area-inset-top));
+  background: #FFFFFF;
+  
+  .back-icon {
+    width: 44rpx;
+    height: 44rpx;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 32rpx;
+    color: #333;
+    &::before { content: '‹'; }
+  }
+  
+  .header-title {
+    font-size: 36rpx;
+    font-weight: bold;
+    color: #333;
+  }
+  
+  .header-right {
+    width: 44rpx;
+  }
+}
+
+.product-image-section {
+  width: 100%;
+  height: 600rpx;
+  background: #FFFFFF;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  
+  .product-image {
+    width: 100%;
+    height: 100%;
+    object-fit: contain;
+  }
+}
+
+.product-info-section {
+  background: #FFFFFF;
+  padding: 30rpx;
+  margin-top: 20rpx;
+  
+  .product-name {
+    font-size: 36rpx;
+    font-weight: bold;
+    color: #333;
+    margin-bottom: 20rpx;
+  }
+  
+  .product-price {
+    display: flex;
+    align-items: baseline;
+    margin-bottom: 15rpx;
+    
+    .price-value {
+      font-size: 48rpx;
+      font-weight: bold;
+      color: #9C27B0;
+    }
+    
+    .price-unit {
+      font-size: 28rpx;
+      color: #9C27B0;
+      margin-left: 10rpx;
+    }
+  }
+  
+  .product-stock {
+    font-size: 26rpx;
+    color: #999;
+  }
+}
+
+.product-desc-section {
+  background: #FFFFFF;
+  padding: 30rpx;
+  margin-top: 20rpx;
+  
+  .section-title {
+    font-size: 30rpx;
+    font-weight: bold;
+    color: #333;
+    margin-bottom: 20rpx;
+  }
+  
+  .desc-content {
+    font-size: 28rpx;
+    color: #666;
+    line-height: 1.6;
+  }
+}
+
+.receiver-section {
+  background: #FFFFFF;
+  padding: 30rpx;
+  margin-top: 20rpx;
+  
+  .section-title {
+    font-size: 30rpx;
+    font-weight: bold;
+    color: #333;
+    margin-bottom: 20rpx;
+  }
+  
+  .form-item {
+    display: flex;
+    align-items: flex-start;
+    margin-bottom: 20rpx;
+    
+    .label {
+      width: 150rpx;
+      font-size: 28rpx;
+      color: #333;
+      padding-top: 10rpx;
+    }
+    
+    .input {
+      flex: 1;
+      height: 70rpx;
+      background: #F5F5F5;
+      border-radius: 10rpx;
+      padding: 0 20rpx;
+      font-size: 28rpx;
+    }
+    
+    .textarea {
+      flex: 1;
+      height: 150rpx;
+      background: #F5F5F5;
+      border-radius: 10rpx;
+      padding: 20rpx;
+      font-size: 28rpx;
+    }
+  }
+}
+
+.bottom-bar {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  height: 100rpx;
+  background: #FFFFFF;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 0 30rpx;
+  padding-bottom: env(safe-area-inset-bottom);
+  box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
+  
+  .points-info {
+    display: flex;
+    align-items: center;
+    
+    .label {
+      font-size: 26rpx;
+      color: #666;
+    }
+    
+    .value {
+      font-size: 32rpx;
+      font-weight: bold;
+      color: #9C27B0;
+      margin-left: 10rpx;
+    }
+  }
+  
+  .exchange-btn {
+    padding: 20rpx 60rpx;
+    background: linear-gradient(135deg, #9C27B0 0%, #E91E63 100%);
+    color: #FFFFFF;
+    font-size: 30rpx;
+    font-weight: bold;
+    border-radius: 50rpx;
+    
+    &.disabled {
+      background: #CCCCCC;
+    }
+  }
+}
+</style>

+ 92 - 3
LiangZhiYUMao/utils/api.js

@@ -216,9 +216,29 @@ export default {
       data: params 
     }),
     
-    // 获取课程详情
-    getDetail: (id) => request({ 
-      url: `/course/detail/${id}`,
+    // 获取课程详情(带学习进度)
+    getDetail: (courseId, makerId) => request({ 
+      url: `/course/detail/${courseId}${makerId ? '?makerId=' + makerId : ''}`,
+      method: 'GET'
+    }),
+    
+    // 更新学习进度
+    updateProgress: (makerId, courseId, progress) => request({
+      url: '/course/progress',
+      method: 'POST',
+      data: { makerId, courseId, progress }
+    }),
+    
+    // 完成课程(领取积分)
+    complete: (makerId, courseId) => request({
+      url: '/course/complete',
+      method: 'POST',
+      data: { makerId, courseId }
+    }),
+    
+    // 获取我的学习记录
+    getMyProgress: (makerId) => request({
+      url: `/course/my-progress?makerId=${makerId}`,
       method: 'GET'
     }),
     
@@ -696,6 +716,75 @@ export default {
         })
       })
     }
+  },
+
+  // 积分商城相关
+  pointsMall: {
+    // 获取商品列表
+    getProducts: (params) => request({
+      url: '/points/products',
+      method: 'GET',
+      data: params
+    }),
+    
+    // 获取推荐商品
+    getRecommendProducts: (limit = 10) => request({
+      url: `/points/products/recommend?limit=${limit}`,
+      method: 'GET'
+    }),
+    
+    // 获取商品详情
+    getProductDetail: (id) => request({
+      url: `/points/products/${id}`,
+      method: 'GET'
+    }),
+    
+    // 获取积分余额
+    getBalance: (makerId) => request({
+      url: `/points/balance?makerId=${makerId}`,
+      method: 'GET'
+    }),
+    
+    // 获取积分明细
+    getRecords: (makerId, pageNum = 1, pageSize = 20) => request({
+      url: `/points/records?makerId=${makerId}&pageNum=${pageNum}&pageSize=${pageSize}`,
+      method: 'GET'
+    }),
+    
+    // 获取积分规则
+    getRules: () => request({
+      url: '/points/rules',
+      method: 'GET'
+    }),
+    
+    // 兑换商品
+    exchange: (data) => request({
+      url: '/points/exchange',
+      method: 'POST',
+      data
+    }),
+    
+    // 获取订单列表
+    getOrders: (makerId, status, pageNum = 1, pageSize = 10) => {
+      let url = `/points/orders?makerId=${makerId}&pageNum=${pageNum}&pageSize=${pageSize}`
+      if (status !== undefined && status !== null) {
+        url += `&status=${status}`
+      }
+      return request({ url, method: 'GET' })
+    },
+    
+    // 获取订单详情
+    getOrderDetail: (orderNo) => request({
+      url: `/points/orders/${orderNo}`,
+      method: 'GET'
+    }),
+    
+    // 增加积分(签到等)
+    addPoints: (makerId, ruleType, reason) => request({
+      url: '/points/add',
+      method: 'POST',
+      data: { makerId, ruleType, reason }
+    })
   }
 }
 

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

@@ -150,6 +150,22 @@ spring:
             - Path=/api/partner-requirement/**
           filters:
             - StripPrefix=0
+        
+        # 积分商城路由(admin服务)
+        - id: points-mall-route
+          uri: http://localhost:8088
+          predicates:
+            - Path=/api/points/**
+          filters:
+            - StripPrefix=1
+        
+        # 课程服务路由(admin服务)
+        - id: course-route
+          uri: http://localhost:8088
+          predicates:
+            - Path=/api/course/**
+          filters:
+            - StripPrefix=1
 
         # 首页服务路由(兜底路由)
         - id: homepage-route

+ 226 - 0
service/admin/src/main/java/com/zhentao/controller/PointsMallController.java

@@ -0,0 +1,226 @@
+package com.zhentao.controller;
+
+import com.zhentao.common.Result;
+import com.zhentao.entity.PointRule;
+import com.zhentao.entity.PointsOrder;
+import com.zhentao.entity.PointsProduct;
+import com.zhentao.service.PointsMallService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 积分商城控制器
+ */
+@RestController
+@RequestMapping("/points")
+@CrossOrigin(origins = "*")
+public class PointsMallController {
+    
+    @Autowired
+    private PointsMallService pointsMallService;
+    
+    /**
+     * 获取商品列表
+     * @param category 分类(可选): 1-实物商品 2-虚拟商品
+     * @param pageNum 页码
+     * @param pageSize 每页数量
+     */
+    @GetMapping("/products")
+    public Result<Map<String, Object>> getProductList(
+            @RequestParam(required = false) Integer category,
+            @RequestParam(defaultValue = "1") Integer pageNum,
+            @RequestParam(defaultValue = "10") Integer pageSize) {
+        try {
+            Map<String, Object> data = pointsMallService.getProductList(category, pageNum, pageSize);
+            return Result.success(data);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("获取商品列表失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取推荐商品
+     * @param limit 数量限制
+     */
+    @GetMapping("/products/recommend")
+    public Result<List<PointsProduct>> getRecommendProducts(
+            @RequestParam(defaultValue = "10") Integer limit) {
+        try {
+            List<PointsProduct> products = pointsMallService.getRecommendProducts(limit);
+            return Result.success(products);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("获取推荐商品失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取商品详情
+     * @param id 商品ID
+     */
+    @GetMapping("/products/{id}")
+    public Result<PointsProduct> getProductDetail(@PathVariable Long id) {
+        try {
+            PointsProduct product = pointsMallService.getProductDetail(id);
+            if (product == null) {
+                return Result.error("商品不存在");
+            }
+            return Result.success(product);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("获取商品详情失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取积分余额
+     * @param makerId 红娘ID
+     */
+    @GetMapping("/balance")
+    public Result<Map<String, Object>> getPointsBalance(@RequestParam Long makerId) {
+        try {
+            Integer balance = pointsMallService.getPointsBalance(makerId);
+            Map<String, Object> data = new HashMap<>();
+            data.put("balance", balance);
+            data.put("makerId", makerId);
+            return Result.success(data);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("获取积分余额失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取积分明细
+     * @param makerId 红娘ID
+     * @param pageNum 页码
+     * @param pageSize 每页数量
+     */
+    @GetMapping("/records")
+    public Result<Map<String, Object>> getPointsRecords(
+            @RequestParam Long makerId,
+            @RequestParam(defaultValue = "1") Integer pageNum,
+            @RequestParam(defaultValue = "20") Integer pageSize) {
+        try {
+            Map<String, Object> data = pointsMallService.getPointsRecords(makerId, pageNum, pageSize);
+            // 同时返回当前余额
+            Integer balance = pointsMallService.getPointsBalance(makerId);
+            data.put("balance", balance);
+            return Result.success(data);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("获取积分明细失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取积分规则(赚取积分方式)
+     */
+    @GetMapping("/rules")
+    public Result<List<PointRule>> getPointRules() {
+        try {
+            List<PointRule> rules = pointsMallService.getPointRules();
+            return Result.success(rules);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("获取积分规则失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 兑换商品
+     */
+    @PostMapping("/exchange")
+    public Result<PointsOrder> exchangeProduct(@RequestBody Map<String, Object> params) {
+        try {
+            Long makerId = Long.valueOf(params.get("makerId").toString());
+            Long productId = Long.valueOf(params.get("productId").toString());
+            Integer quantity = params.get("quantity") != null ? 
+                    Integer.valueOf(params.get("quantity").toString()) : 1;
+            String receiverName = (String) params.get("receiverName");
+            String receiverPhone = (String) params.get("receiverPhone");
+            String receiverAddress = (String) params.get("receiverAddress");
+            
+            PointsOrder order = pointsMallService.exchangeProduct(
+                    makerId, productId, quantity, receiverName, receiverPhone, receiverAddress);
+            
+            return Result.success("兑换成功", order);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("兑换失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取兑换订单列表
+     * @param makerId 红娘ID
+     * @param status 订单状态(可选): 0-待发货 1-已发货 2-已完成 3-已取消
+     * @param pageNum 页码
+     * @param pageSize 每页数量
+     */
+    @GetMapping("/orders")
+    public Result<Map<String, Object>> getOrderList(
+            @RequestParam Long makerId,
+            @RequestParam(required = false) Integer status,
+            @RequestParam(defaultValue = "1") Integer pageNum,
+            @RequestParam(defaultValue = "10") Integer pageSize) {
+        try {
+            Map<String, Object> data = pointsMallService.getOrderList(makerId, status, pageNum, pageSize);
+            return Result.success(data);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("获取订单列表失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 获取订单详情
+     * @param orderNo 订单编号
+     */
+    @GetMapping("/orders/{orderNo}")
+    public Result<PointsOrder> getOrderDetail(@PathVariable String orderNo) {
+        try {
+            PointsOrder order = pointsMallService.getOrderDetail(orderNo);
+            if (order == null) {
+                return Result.error("订单不存在");
+            }
+            return Result.success(order);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("获取订单详情失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 增加积分(签到、上传线索等)
+     */
+    @PostMapping("/add")
+    public Result<Map<String, Object>> addPoints(@RequestBody Map<String, Object> params) {
+        try {
+            Long makerId = Long.valueOf(params.get("makerId").toString());
+            Integer ruleType = Integer.valueOf(params.get("ruleType").toString());
+            String reason = (String) params.get("reason");
+            
+            Integer addedPoints = pointsMallService.addPoints(makerId, ruleType, reason);
+            Integer newBalance = pointsMallService.getPointsBalance(makerId);
+            
+            Map<String, Object> data = new HashMap<>();
+            data.put("addedPoints", addedPoints);
+            data.put("newBalance", newBalance);
+            
+            return Result.success("积分增加成功", data);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Result.error("积分增加失败: " + e.getMessage());
+        }
+    }
+}

+ 38 - 0
service/admin/src/main/java/com/zhentao/entity/PointRule.java

@@ -0,0 +1,38 @@
+package com.zhentao.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+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;
+
+/**
+ * 积分规则实体类
+ */
+@Data
+@TableName("point_rule")
+public class PointRule implements Serializable {
+    
+    private static final long serialVersionUID = 1L;
+    
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+    
+    /** 规则名称 */
+    private String ruleName;
+    
+    /** 规则类型 */
+    private Integer ruleType;
+    
+    /** 积分值 */
+    private Integer pointValue;
+    
+    /** 状态 */
+    private Integer status;
+    
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime createdTime;
+}

+ 38 - 0
service/admin/src/main/java/com/zhentao/entity/PointsDetail.java

@@ -0,0 +1,38 @@
+package com.zhentao.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+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;
+
+/**
+ * 积分明细实体类
+ */
+@Data
+@TableName("points_detail")
+public class PointsDetail implements Serializable {
+    
+    private static final long serialVersionUID = 1L;
+    
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+    
+    /** 红娘ID */
+    private Long makerId;
+    
+    /** 积分变动(正数增加,负数减少) */
+    private Integer points;
+    
+    /** 变动原因 */
+    private String reason;
+    
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime createTime;
+    
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime updateTime;
+}

+ 74 - 0
service/admin/src/main/java/com/zhentao/entity/PointsOrder.java

@@ -0,0 +1,74 @@
+package com.zhentao.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+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;
+
+/**
+ * 积分兑换订单实体类
+ */
+@Data
+@TableName("points_order")
+public class PointsOrder implements Serializable {
+    
+    private static final long serialVersionUID = 1L;
+    
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+    
+    /** 订单编号 */
+    private String orderNo;
+    
+    /** 红娘ID */
+    private Long makerId;
+    
+    /** 商品ID */
+    private Long productId;
+    
+    /** 商品名称 */
+    private String productName;
+    
+    /** 商品图片 */
+    private String productImage;
+    
+    /** 兑换积分 */
+    private Integer pointsPrice;
+    
+    /** 数量 */
+    private Integer quantity;
+    
+    /** 总积分 */
+    private Integer totalPoints;
+    
+    /** 状态: 0-待发货 1-已发货 2-已完成 3-已取消 */
+    private Integer status;
+    
+    /** 收货人姓名 */
+    private String receiverName;
+    
+    /** 收货人电话 */
+    private String receiverPhone;
+    
+    /** 收货地址 */
+    private String receiverAddress;
+    
+    /** 快递公司 */
+    private String expressCompany;
+    
+    /** 快递单号 */
+    private String expressNo;
+    
+    /** 备注 */
+    private String remark;
+    
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime createTime;
+    
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime updateTime;
+}

+ 56 - 0
service/admin/src/main/java/com/zhentao/entity/PointsProduct.java

@@ -0,0 +1,56 @@
+package com.zhentao.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+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;
+
+/**
+ * 积分商品实体类
+ */
+@Data
+@TableName("points_product")
+public class PointsProduct implements Serializable {
+    
+    private static final long serialVersionUID = 1L;
+    
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+    
+    /** 商品名称 */
+    private String name;
+    
+    /** 商品描述 */
+    private String description;
+    
+    /** 商品图片 */
+    private String imageUrl;
+    
+    /** 积分价格 */
+    private Integer pointsPrice;
+    
+    /** 库存数量 */
+    private Integer stock;
+    
+    /** 分类: 1-实物商品 2-虚拟商品 */
+    private Integer category;
+    
+    /** 是否推荐: 0-否 1-是 */
+    private Integer isRecommend;
+    
+    /** 状态: 0-下架 1-上架 */
+    private Integer status;
+    
+    /** 排序 */
+    private Integer sortOrder;
+    
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime createTime;
+    
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime updateTime;
+}

+ 12 - 0
service/admin/src/main/java/com/zhentao/mapper/PointRuleMapper.java

@@ -0,0 +1,12 @@
+package com.zhentao.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zhentao.entity.PointRule;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 积分规则Mapper
+ */
+@Mapper
+public interface PointRuleMapper extends BaseMapper<PointRule> {
+}

+ 12 - 0
service/admin/src/main/java/com/zhentao/mapper/PointsDetailMapper.java

@@ -0,0 +1,12 @@
+package com.zhentao.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zhentao.entity.PointsDetail;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 积分明细Mapper
+ */
+@Mapper
+public interface PointsDetailMapper extends BaseMapper<PointsDetail> {
+}

+ 12 - 0
service/admin/src/main/java/com/zhentao/mapper/PointsOrderMapper.java

@@ -0,0 +1,12 @@
+package com.zhentao.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zhentao.entity.PointsOrder;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 积分兑换订单Mapper
+ */
+@Mapper
+public interface PointsOrderMapper extends BaseMapper<PointsOrder> {
+}

+ 12 - 0
service/admin/src/main/java/com/zhentao/mapper/PointsProductMapper.java

@@ -0,0 +1,12 @@
+package com.zhentao.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zhentao.entity.PointsProduct;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 积分商品Mapper
+ */
+@Mapper
+public interface PointsProductMapper extends BaseMapper<PointsProduct> {
+}

+ 99 - 0
service/admin/src/main/java/com/zhentao/service/PointsMallService.java

@@ -0,0 +1,99 @@
+package com.zhentao.service;
+
+import com.zhentao.entity.PointsDetail;
+import com.zhentao.entity.PointsOrder;
+import com.zhentao.entity.PointsProduct;
+import com.zhentao.entity.PointRule;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 积分商城服务接口
+ */
+public interface PointsMallService {
+    
+    /**
+     * 获取商品列表
+     * @param category 分类(可选)
+     * @param pageNum 页码
+     * @param pageSize 每页数量
+     * @return 商品列表
+     */
+    Map<String, Object> getProductList(Integer category, Integer pageNum, Integer pageSize);
+    
+    /**
+     * 获取推荐商品
+     * @param limit 数量限制
+     * @return 推荐商品列表
+     */
+    List<PointsProduct> getRecommendProducts(Integer limit);
+    
+    /**
+     * 获取商品详情
+     * @param productId 商品ID
+     * @return 商品详情
+     */
+    PointsProduct getProductDetail(Long productId);
+    
+    /**
+     * 获取红娘积分余额
+     * @param makerId 红娘ID
+     * @return 积分余额
+     */
+    Integer getPointsBalance(Long makerId);
+    
+    /**
+     * 获取积分明细
+     * @param makerId 红娘ID
+     * @param pageNum 页码
+     * @param pageSize 每页数量
+     * @return 积分明细列表
+     */
+    Map<String, Object> getPointsRecords(Long makerId, Integer pageNum, Integer pageSize);
+    
+    /**
+     * 获取积分规则列表
+     * @return 积分规则列表
+     */
+    List<PointRule> getPointRules();
+    
+    /**
+     * 兑换商品
+     * @param makerId 红娘ID
+     * @param productId 商品ID
+     * @param quantity 数量
+     * @param receiverName 收货人姓名
+     * @param receiverPhone 收货人电话
+     * @param receiverAddress 收货地址
+     * @return 订单信息
+     */
+    PointsOrder exchangeProduct(Long makerId, Long productId, Integer quantity,
+                                 String receiverName, String receiverPhone, String receiverAddress);
+    
+    /**
+     * 获取兑换订单列表
+     * @param makerId 红娘ID
+     * @param status 订单状态(可选)
+     * @param pageNum 页码
+     * @param pageSize 每页数量
+     * @return 订单列表
+     */
+    Map<String, Object> getOrderList(Long makerId, Integer status, Integer pageNum, Integer pageSize);
+    
+    /**
+     * 获取订单详情
+     * @param orderNo 订单编号
+     * @return 订单详情
+     */
+    PointsOrder getOrderDetail(String orderNo);
+    
+    /**
+     * 增加积分(签到、上传线索等)
+     * @param makerId 红娘ID
+     * @param ruleType 规则类型
+     * @param reason 原因描述
+     * @return 增加的积分数
+     */
+    Integer addPoints(Long makerId, Integer ruleType, String reason);
+}

+ 276 - 0
service/admin/src/main/java/com/zhentao/service/impl/PointsMallServiceImpl.java

@@ -0,0 +1,276 @@
+package com.zhentao.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zhentao.entity.*;
+import com.zhentao.mapper.*;
+import com.zhentao.service.PointsMallService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+
+/**
+ * 积分商城服务实现类
+ */
+@Service
+public class PointsMallServiceImpl implements PointsMallService {
+    
+    @Autowired
+    private PointsProductMapper pointsProductMapper;
+    
+    @Autowired
+    private PointsOrderMapper pointsOrderMapper;
+    
+    @Autowired
+    private PointsDetailMapper pointsDetailMapper;
+    
+    @Autowired
+    private PointRuleMapper pointRuleMapper;
+    
+    @Autowired
+    private MatchmakerMapper matchmakerMapper;
+    
+    @Override
+    public Map<String, Object> getProductList(Integer category, Integer pageNum, Integer pageSize) {
+        Page<PointsProduct> page = new Page<>(pageNum, pageSize);
+        LambdaQueryWrapper<PointsProduct> wrapper = new LambdaQueryWrapper<>();
+        
+        // 只查询上架的商品
+        wrapper.eq(PointsProduct::getStatus, 1);
+        
+        // 按分类筛选
+        if (category != null && category > 0) {
+            wrapper.eq(PointsProduct::getCategory, category);
+        }
+        
+        // 按排序和创建时间排序
+        wrapper.orderByDesc(PointsProduct::getSortOrder)
+               .orderByDesc(PointsProduct::getCreateTime);
+        
+        Page<PointsProduct> result = pointsProductMapper.selectPage(page, wrapper);
+        
+        Map<String, Object> data = new HashMap<>();
+        data.put("list", result.getRecords());
+        data.put("total", result.getTotal());
+        data.put("pageNum", pageNum);
+        data.put("pageSize", pageSize);
+        data.put("pages", result.getPages());
+        
+        return data;
+    }
+    
+    @Override
+    public List<PointsProduct> getRecommendProducts(Integer limit) {
+        LambdaQueryWrapper<PointsProduct> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(PointsProduct::getStatus, 1)
+               .eq(PointsProduct::getIsRecommend, 1)
+               .orderByDesc(PointsProduct::getSortOrder)
+               .last("LIMIT " + (limit != null ? limit : 10));
+        
+        return pointsProductMapper.selectList(wrapper);
+    }
+    
+    @Override
+    public PointsProduct getProductDetail(Long productId) {
+        return pointsProductMapper.selectById(productId);
+    }
+    
+    @Override
+    public Integer getPointsBalance(Long makerId) {
+        Matchmaker matchmaker = matchmakerMapper.selectById(makerId);
+        if (matchmaker == null) {
+            return 0;
+        }
+        // 优先使用matchmakers表的points字段
+        if (matchmaker.getPoints() != null) {
+            return matchmaker.getPoints();
+        }
+        // 如果points字段为空,通过计算积分明细来获取余额
+        LambdaQueryWrapper<PointsDetail> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(PointsDetail::getMakerId, makerId);
+        List<PointsDetail> details = pointsDetailMapper.selectList(wrapper);
+        
+        int balance = 0;
+        for (PointsDetail detail : details) {
+            balance += detail.getPoints();
+        }
+        return balance;
+    }
+    
+    @Override
+    public Map<String, Object> getPointsRecords(Long makerId, Integer pageNum, Integer pageSize) {
+        Page<PointsDetail> page = new Page<>(pageNum, pageSize);
+        LambdaQueryWrapper<PointsDetail> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(PointsDetail::getMakerId, makerId)
+               .orderByDesc(PointsDetail::getCreateTime);
+        
+        Page<PointsDetail> result = pointsDetailMapper.selectPage(page, wrapper);
+        
+        Map<String, Object> data = new HashMap<>();
+        data.put("list", result.getRecords());
+        data.put("total", result.getTotal());
+        data.put("pageNum", pageNum);
+        data.put("pageSize", pageSize);
+        
+        return data;
+    }
+    
+    @Override
+    public List<PointRule> getPointRules() {
+        LambdaQueryWrapper<PointRule> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(PointRule::getStatus, 1)
+               .orderByAsc(PointRule::getRuleType);
+        return pointRuleMapper.selectList(wrapper);
+    }
+    
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public PointsOrder exchangeProduct(Long makerId, Long productId, Integer quantity,
+                                        String receiverName, String receiverPhone, String receiverAddress) {
+        // 1. 检查商品是否存在且上架
+        PointsProduct product = pointsProductMapper.selectById(productId);
+        if (product == null || product.getStatus() != 1) {
+            throw new RuntimeException("商品不存在或已下架");
+        }
+        
+        // 2. 检查库存
+        if (product.getStock() < quantity) {
+            throw new RuntimeException("商品库存不足");
+        }
+        
+        // 3. 计算所需积分
+        int totalPoints = product.getPointsPrice() * quantity;
+        
+        // 4. 检查积分余额
+        Integer balance = getPointsBalance(makerId);
+        if (balance < totalPoints) {
+            throw new RuntimeException("积分余额不足,当前积分: " + balance + ",需要: " + totalPoints);
+        }
+        
+        // 5. 扣减库存
+        LambdaUpdateWrapper<PointsProduct> productUpdate = new LambdaUpdateWrapper<>();
+        productUpdate.eq(PointsProduct::getId, productId)
+                     .ge(PointsProduct::getStock, quantity)
+                     .setSql("stock = stock - " + quantity);
+        int updateCount = pointsProductMapper.update(null, productUpdate);
+        if (updateCount == 0) {
+            throw new RuntimeException("库存扣减失败,请重试");
+        }
+        
+        // 6. 创建订单
+        PointsOrder order = new PointsOrder();
+        order.setOrderNo(generateOrderNo());
+        order.setMakerId(makerId);
+        order.setProductId(productId);
+        order.setProductName(product.getName());
+        order.setProductImage(product.getImageUrl());
+        order.setPointsPrice(product.getPointsPrice());
+        order.setQuantity(quantity);
+        order.setTotalPoints(totalPoints);
+        order.setStatus(0); // 待发货
+        order.setReceiverName(receiverName);
+        order.setReceiverPhone(receiverPhone);
+        order.setReceiverAddress(receiverAddress);
+        order.setCreateTime(LocalDateTime.now());
+        order.setUpdateTime(LocalDateTime.now());
+        
+        pointsOrderMapper.insert(order);
+        
+        // 7. 记录积分变动(扣减)
+        PointsDetail detail = new PointsDetail();
+        detail.setMakerId(makerId);
+        detail.setPoints(-totalPoints);
+        detail.setReason("兑换商品: " + product.getName() + " x" + quantity);
+        detail.setCreateTime(LocalDateTime.now());
+        detail.setUpdateTime(LocalDateTime.now());
+        
+        pointsDetailMapper.insert(detail);
+        
+        // 8. 更新红娘积分余额
+        LambdaUpdateWrapper<Matchmaker> makerUpdate = new LambdaUpdateWrapper<>();
+        makerUpdate.eq(Matchmaker::getMatchmakerId, makerId)
+                   .setSql("points = IFNULL(points, 0) - " + totalPoints);
+        matchmakerMapper.update(null, makerUpdate);
+        
+        return order;
+    }
+    
+    @Override
+    public Map<String, Object> getOrderList(Long makerId, Integer status, Integer pageNum, Integer pageSize) {
+        Page<PointsOrder> page = new Page<>(pageNum, pageSize);
+        LambdaQueryWrapper<PointsOrder> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(PointsOrder::getMakerId, makerId);
+        
+        if (status != null && status >= 0) {
+            wrapper.eq(PointsOrder::getStatus, status);
+        }
+        
+        wrapper.orderByDesc(PointsOrder::getCreateTime);
+        
+        Page<PointsOrder> result = pointsOrderMapper.selectPage(page, wrapper);
+        
+        Map<String, Object> data = new HashMap<>();
+        data.put("list", result.getRecords());
+        data.put("total", result.getTotal());
+        data.put("pageNum", pageNum);
+        data.put("pageSize", pageSize);
+        
+        return data;
+    }
+    
+    @Override
+    public PointsOrder getOrderDetail(String orderNo) {
+        LambdaQueryWrapper<PointsOrder> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(PointsOrder::getOrderNo, orderNo);
+        return pointsOrderMapper.selectOne(wrapper);
+    }
+    
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Integer addPoints(Long makerId, Integer ruleType, String reason) {
+        // 1. 查询积分规则
+        LambdaQueryWrapper<PointRule> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(PointRule::getRuleType, ruleType)
+               .eq(PointRule::getStatus, 1);
+        PointRule rule = pointRuleMapper.selectOne(wrapper);
+        
+        if (rule == null) {
+            throw new RuntimeException("积分规则不存在");
+        }
+        
+        int pointValue = rule.getPointValue();
+        
+        // 2. 记录积分变动
+        PointsDetail detail = new PointsDetail();
+        detail.setMakerId(makerId);
+        detail.setPoints(pointValue);
+        detail.setReason(reason != null ? reason : rule.getRuleName());
+        detail.setCreateTime(LocalDateTime.now());
+        detail.setUpdateTime(LocalDateTime.now());
+        
+        pointsDetailMapper.insert(detail);
+        
+        // 3. 更新红娘积分余额
+        LambdaUpdateWrapper<Matchmaker> makerUpdate = new LambdaUpdateWrapper<>();
+        makerUpdate.eq(Matchmaker::getMatchmakerId, makerId)
+                   .setSql("points = IFNULL(points, 0) + " + pointValue);
+        matchmakerMapper.update(null, makerUpdate);
+        
+        return pointValue;
+    }
+    
+    /**
+     * 生成订单编号
+     */
+    private String generateOrderNo() {
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
+        String timestamp = LocalDateTime.now().format(formatter);
+        String random = String.format("%04d", new Random().nextInt(10000));
+        return "PO" + timestamp + random;
+    }
+}